feat: prompt vehicle selection on login after auto-downgrade (#60) #62
@@ -49,6 +49,28 @@ export class SubscriptionsController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/subscriptions/needs-vehicle-selection - Check if user needs vehicle selection
|
||||
*/
|
||||
async checkNeedsVehicleSelection(request: FastifyRequest, reply: FastifyReply): Promise<void> {
|
||||
try {
|
||||
const userId = (request as any).user.sub;
|
||||
|
||||
const result = await this.service.checkNeedsVehicleSelection(userId);
|
||||
|
||||
reply.status(200).send(result);
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to check needs vehicle selection', {
|
||||
userId: (request as any).user?.sub,
|
||||
error: error.message,
|
||||
});
|
||||
reply.status(500).send({
|
||||
error: 'Failed to check needs vehicle selection',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/subscriptions/checkout - Create Stripe checkout session
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,12 @@ export const subscriptionsRoutes: FastifyPluginAsync = async (
|
||||
handler: subscriptionsController.getSubscription.bind(subscriptionsController)
|
||||
});
|
||||
|
||||
// GET /api/subscriptions/needs-vehicle-selection - Check if user needs vehicle selection
|
||||
fastify.get('/subscriptions/needs-vehicle-selection', {
|
||||
preHandler: [fastify.authenticate],
|
||||
handler: subscriptionsController.checkNeedsVehicleSelection.bind(subscriptionsController)
|
||||
});
|
||||
|
||||
// POST /api/subscriptions/checkout - Create Stripe checkout session
|
||||
fastify.post('/subscriptions/checkout', {
|
||||
preHandler: [fastify.authenticate],
|
||||
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
BillingCycle,
|
||||
SubscriptionStatus,
|
||||
UpdateSubscriptionData,
|
||||
NeedsVehicleSelectionResponse,
|
||||
} from './subscriptions.types';
|
||||
import { VehiclesRepository } from '../../vehicles/data/vehicles.repository';
|
||||
|
||||
interface StripeWebhookEvent {
|
||||
id: string;
|
||||
@@ -27,6 +29,7 @@ interface StripeWebhookEvent {
|
||||
|
||||
export class SubscriptionsService {
|
||||
private userProfileRepository: UserProfileRepository;
|
||||
private vehiclesRepository: VehiclesRepository;
|
||||
|
||||
constructor(
|
||||
private repository: SubscriptionsRepository,
|
||||
@@ -34,6 +37,7 @@ export class SubscriptionsService {
|
||||
pool: Pool
|
||||
) {
|
||||
this.userProfileRepository = new UserProfileRepository(pool);
|
||||
this.vehiclesRepository = new VehiclesRepository(pool);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,6 +61,65 @@ export class SubscriptionsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user needs to make a vehicle selection after auto-downgrade
|
||||
* Returns true if: user is on free tier, has >2 vehicles, and hasn't made selections
|
||||
*/
|
||||
async checkNeedsVehicleSelection(userId: string): Promise<NeedsVehicleSelectionResponse> {
|
||||
const FREE_TIER_VEHICLE_LIMIT = 2;
|
||||
|
||||
try {
|
||||
// Get current subscription
|
||||
const subscription = await this.repository.findByUserId(userId);
|
||||
|
||||
// No subscription or not on free tier = no selection needed
|
||||
if (!subscription || subscription.tier !== 'free') {
|
||||
return {
|
||||
needsSelection: false,
|
||||
vehicleCount: 0,
|
||||
maxAllowed: FREE_TIER_VEHICLE_LIMIT,
|
||||
};
|
||||
}
|
||||
|
||||
// Count user's active vehicles
|
||||
const vehicleCount = await this.vehiclesRepository.countByUserId(userId);
|
||||
|
||||
// If within limit, no selection needed
|
||||
if (vehicleCount <= FREE_TIER_VEHICLE_LIMIT) {
|
||||
return {
|
||||
needsSelection: false,
|
||||
vehicleCount,
|
||||
maxAllowed: FREE_TIER_VEHICLE_LIMIT,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if user already has vehicle selections
|
||||
const existingSelections = await this.repository.findVehicleSelectionsByUserId(userId);
|
||||
|
||||
// If user already made selections, no selection needed
|
||||
if (existingSelections.length > 0) {
|
||||
return {
|
||||
needsSelection: false,
|
||||
vehicleCount,
|
||||
maxAllowed: FREE_TIER_VEHICLE_LIMIT,
|
||||
};
|
||||
}
|
||||
|
||||
// User is on free tier, has >2 vehicles, and hasn't made selections
|
||||
return {
|
||||
needsSelection: true,
|
||||
vehicleCount,
|
||||
maxAllowed: FREE_TIER_VEHICLE_LIMIT,
|
||||
};
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to check needs vehicle selection', {
|
||||
userId,
|
||||
error: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new subscription (Stripe customer + initial free tier record)
|
||||
*/
|
||||
|
||||
@@ -131,3 +131,10 @@ export interface UpdateSubscriptionData {
|
||||
export interface UpdateDonationData {
|
||||
status?: DonationStatus;
|
||||
}
|
||||
|
||||
// Needs vehicle selection check response
|
||||
export interface NeedsVehicleSelectionResponse {
|
||||
needsSelection: boolean;
|
||||
vehicleCount: number;
|
||||
maxAllowed: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user