/** * @ai-summary Subscriptions API controller * @ai-context Handles subscription management API requests */ import { FastifyRequest, FastifyReply } from 'fastify'; import { logger } from '../../../core/logging/logger'; import { SubscriptionsService } from '../domain/subscriptions.service'; import { SubscriptionsRepository } from '../data/subscriptions.repository'; import { StripeClient } from '../external/stripe/stripe.client'; import { pool } from '../../../core/config/database'; export class SubscriptionsController { private service: SubscriptionsService; constructor() { const repository = new SubscriptionsRepository(pool); const stripeClient = new StripeClient(); this.service = new SubscriptionsService(repository, stripeClient, pool); } /** * GET /api/subscriptions - Get current subscription */ async getSubscription(request: FastifyRequest, reply: FastifyReply): Promise { try { const userId = request.userContext!.userId; const subscription = await this.service.getSubscription(userId); if (!subscription) { reply.status(404).send({ error: 'Subscription not found', message: 'No subscription exists for this user', }); return; } reply.status(200).send(subscription); } catch (error: any) { logger.error('Failed to get subscription', { userId: request.userContext?.userId, error: error.message, }); reply.status(500).send({ error: 'Failed to get subscription', message: error.message, }); } } /** * GET /api/subscriptions/needs-vehicle-selection - Check if user needs vehicle selection */ async checkNeedsVehicleSelection(request: FastifyRequest, reply: FastifyReply): Promise { try { const userId = request.userContext!.userId; 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.userContext?.userId, 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 */ async createCheckout( request: FastifyRequest<{ Body: { tier: 'pro' | 'enterprise'; billingCycle: 'monthly' | 'yearly'; paymentMethodId?: string; }; }>, reply: FastifyReply ): Promise { try { const userId = request.userContext!.userId; const email = request.userContext!.email || ''; const { tier, billingCycle, paymentMethodId } = request.body; // Validate inputs if (!tier || !billingCycle) { reply.status(400).send({ error: 'Missing required fields', message: 'tier and billingCycle are required', }); return; } if (!['pro', 'enterprise'].includes(tier)) { reply.status(400).send({ error: 'Invalid tier', message: 'tier must be "pro" or "enterprise"', }); return; } if (!['monthly', 'yearly'].includes(billingCycle)) { reply.status(400).send({ error: 'Invalid billing cycle', message: 'billingCycle must be "monthly" or "yearly"', }); return; } // Create or get existing subscription let subscription = await this.service.getSubscription(userId); if (!subscription) { await this.service.createSubscription(userId, email); subscription = await this.service.getSubscription(userId); } if (!subscription) { reply.status(500).send({ error: 'Failed to create subscription', message: 'Could not initialize subscription', }); return; } // Upgrade subscription const updatedSubscription = await this.service.upgradeSubscription( userId, tier, billingCycle, paymentMethodId || '', email ); reply.status(200).send(updatedSubscription); } catch (error: any) { logger.error('Failed to create checkout', { userId: request.userContext?.userId, error: error.message, }); reply.status(500).send({ error: 'Failed to create checkout', message: error.message, }); } } /** * POST /api/subscriptions/cancel - Schedule cancellation */ async cancelSubscription(request: FastifyRequest, reply: FastifyReply): Promise { try { const userId = request.userContext!.userId; const subscription = await this.service.cancelSubscription(userId); reply.status(200).send(subscription); } catch (error: any) { logger.error('Failed to cancel subscription', { userId: request.userContext?.userId, error: error.message, }); reply.status(500).send({ error: 'Failed to cancel subscription', message: error.message, }); } } /** * POST /api/subscriptions/reactivate - Cancel pending cancellation */ async reactivateSubscription(request: FastifyRequest, reply: FastifyReply): Promise { try { const userId = request.userContext!.userId; const subscription = await this.service.reactivateSubscription(userId); reply.status(200).send(subscription); } catch (error: any) { logger.error('Failed to reactivate subscription', { userId: request.userContext?.userId, error: error.message, }); reply.status(500).send({ error: 'Failed to reactivate subscription', message: error.message, }); } } /** * PUT /api/subscriptions/payment-method - Update payment method */ async updatePaymentMethod( request: FastifyRequest<{ Body: { paymentMethodId: string; }; }>, reply: FastifyReply ): Promise { try { const userId = request.userContext!.userId; const email = request.userContext!.email || ''; const { paymentMethodId } = request.body; // Validate input if (!paymentMethodId) { reply.status(400).send({ error: 'Missing required field', message: 'paymentMethodId is required', }); return; } // Update payment method via service (creates Stripe customer if needed) await this.service.updatePaymentMethod(userId, paymentMethodId, email); reply.status(200).send({ message: 'Payment method updated successfully', }); } catch (error: any) { logger.error('Failed to update payment method', { userId: request.userContext?.userId, error: error.message, }); reply.status(500).send({ error: 'Failed to update payment method', message: error.message, }); } } /** * GET /api/subscriptions/invoices - Get billing history */ async getInvoices(request: FastifyRequest, reply: FastifyReply): Promise { try { const userId = request.userContext!.userId; const invoices = await this.service.getInvoices(userId); reply.status(200).send(invoices); } catch (error: any) { logger.error('Failed to get invoices', { userId: request.userContext?.userId, error: error.message, }); reply.status(500).send({ error: 'Failed to get invoices', message: error.message, }); } } /** * POST /api/subscriptions/downgrade - Downgrade subscription with vehicle selection */ async downgrade( request: FastifyRequest<{ Body: { targetTier: 'free' | 'pro'; vehicleIdsToKeep: string[]; }; }>, reply: FastifyReply ): Promise { try { const userId = request.userContext!.userId; const { targetTier, vehicleIdsToKeep } = request.body; // Validate inputs if (!targetTier || !vehicleIdsToKeep) { reply.status(400).send({ error: 'Missing required fields', message: 'targetTier and vehicleIdsToKeep are required', }); return; } if (!['free', 'pro'].includes(targetTier)) { reply.status(400).send({ error: 'Invalid tier', message: 'targetTier must be "free" or "pro"', }); return; } if (!Array.isArray(vehicleIdsToKeep)) { reply.status(400).send({ error: 'Invalid vehicle selection', message: 'vehicleIdsToKeep must be an array', }); return; } // Downgrade subscription const updatedSubscription = await this.service.downgradeSubscription( userId, targetTier, vehicleIdsToKeep ); reply.status(200).send(updatedSubscription); } catch (error: any) { logger.error('Failed to downgrade subscription', { userId: request.userContext?.userId, error: error.message, }); reply.status(500).send({ error: 'Failed to downgrade subscription', message: error.message, }); } } }