diff --git a/backend/src/features/subscriptions/api/subscriptions.controller.ts b/backend/src/features/subscriptions/api/subscriptions.controller.ts index 719eba3..36616c1 100644 --- a/backend/src/features/subscriptions/api/subscriptions.controller.ts +++ b/backend/src/features/subscriptions/api/subscriptions.controller.ts @@ -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 { + 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 */ diff --git a/backend/src/features/subscriptions/api/subscriptions.routes.ts b/backend/src/features/subscriptions/api/subscriptions.routes.ts index cca8439..7ef9595 100644 --- a/backend/src/features/subscriptions/api/subscriptions.routes.ts +++ b/backend/src/features/subscriptions/api/subscriptions.routes.ts @@ -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], diff --git a/backend/src/features/subscriptions/domain/subscriptions.service.ts b/backend/src/features/subscriptions/domain/subscriptions.service.ts index e760f0f..4a2665e 100644 --- a/backend/src/features/subscriptions/domain/subscriptions.service.ts +++ b/backend/src/features/subscriptions/domain/subscriptions.service.ts @@ -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 { + 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) */ diff --git a/backend/src/features/subscriptions/domain/subscriptions.types.ts b/backend/src/features/subscriptions/domain/subscriptions.types.ts index 2bcab80..1d6ca83 100644 --- a/backend/src/features/subscriptions/domain/subscriptions.types.ts +++ b/backend/src/features/subscriptions/domain/subscriptions.types.ts @@ -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; +}