import { FastifyInstance } from 'fastify'; import { BillingService } from '@shieldai/shared-billing/src/services/billing.service'; import { SubscriptionService, customerService, webhookService } from '@shieldai/shared-billing/src/services/billing.services'; import { SubscriptionTier, isValidReturnUrl } from '@shieldai/shared-billing/src/config/billing.config'; import { AuthRequest } from './auth.middleware'; const billingService = BillingService.getInstance(); const subscriptionService = new SubscriptionService(); export async function subscriptionRoutes(fastify: FastifyInstance) { // Get current user's subscription fastify.get( '/subscription', { preHandler: async (request, reply) => { await fastify.requireAuth(request as AuthRequest); }, }, async (request, reply) => { const authReq = request as AuthRequest; try { const subscription = await billingService.getUserSubscription(authReq.user!.id); if (!subscription) { return reply.status(404).send({ error: 'No active subscription found', message: 'Please create a subscription to access premium features', }); } return { subscription: { id: subscription.id, status: subscription.status, currentPeriodStart: new Date(subscription.current_period_start * 1000).toISOString(), currentPeriodEnd: new Date(subscription.current_period_end * 1000).toISOString(), cancelAtPeriodEnd: subscription.cancel_at_period_end, created: new Date(subscription.created * 1000).toISOString(), }, customer: { id: subscription.customer as string, }, }; } catch (error) { reply.status(500).send({ error: 'Failed to fetch subscription', message: error instanceof Error ? error.message : 'Unknown error', }); } } ); // Create a new subscription (for mobile app in-app purchases) fastify.post( '/subscription/create', { preHandler: async (request, reply) => { await fastify.requireAuth(request as AuthRequest); }, }, async (request, reply) => { const authReq = request as AuthRequest; const { tier, customerId } = request.body as { tier: SubscriptionTier; customerId: string }; if (!authReq.user?.id) { return reply.status(401).send({ error: 'Authentication required' }); } if (!tier || !customerId) { return reply.status(400).send({ error: 'Missing required fields', required: ['tier', 'customerId'], }); } try { const result = await billingService.createSubscription( authReq.user.id, tier, customerId ); return { subscription: { id: result.subscription.id, status: result.subscription.status, currentPeriodStart: new Date(result.subscription.current_period_start * 1000).toISOString(), currentPeriodEnd: new Date(result.subscription.current_period_end * 1000).toISOString(), }, customer: { id: result.customer.id, email: result.customer.email, }, }; } catch (error) { reply.status(500).send({ error: 'Failed to create subscription', message: error instanceof Error ? error.message : 'Unknown error', }); } } ); // Update subscription tier fastify.put( '/subscription/:subscriptionId/tier', { preHandler: async (request, reply) => { await fastify.requireAuth(request as AuthRequest); }, }, async (request, reply) => { const authReq = request as AuthRequest; const { subscriptionId } = request.params as { subscriptionId: string }; const { tier } = request.body as { tier: SubscriptionTier }; if (!authReq.user?.id) { return reply.status(401).send({ error: 'Authentication required' }); } if (!tier) { return reply.status(400).send({ error: 'Missing required field', required: ['tier'], }); } try { const updated = await billingService.updateSubscription( subscriptionId, authReq.user.id, tier ); return { subscription: { id: updated.id, status: updated.status, currentPeriodStart: new Date(updated.current_period_start * 1000).toISOString(), currentPeriodEnd: new Date(updated.current_period_end * 1000).toISOString(), }, message: 'Subscription updated successfully', }; } catch (error) { reply.status(500).send({ error: 'Failed to update subscription', message: error instanceof Error ? error.message : 'Unknown error', }); } } ); // Cancel subscription fastify.delete( '/subscription/:subscriptionId', { preHandler: async (request, reply) => { await fastify.requireAuth(request as AuthRequest); }, }, async (request, reply) => { const authReq = request as AuthRequest; const { subscriptionId } = request.params as { subscriptionId: string }; const { cancelAtPeriodEnd } = request.body as { cancelAtPeriodEnd?: boolean }; if (!authReq.user?.id) { return reply.status(401).send({ error: 'Authentication required' }); } try { const cancelled = await billingService.cancelSubscription( subscriptionId, authReq.user.id, cancelAtPeriodEnd ?? true ); return { subscription: { id: cancelled.id, status: cancelled.status, cancelAtPeriodEnd: cancelled.cancel_at_period_end, canceledAt: cancelled.canceled_at ? new Date(cancelled.canceled_at * 1000).toISOString() : null, }, message: cancelAtPeriodEnd ? 'Subscription will cancel at the end of the billing period' : 'Subscription cancelled immediately', }; } catch (error) { reply.status(500).send({ error: 'Failed to cancel subscription', message: error instanceof Error ? error.message : 'Unknown error', }); } } ); // Create customer portal session fastify.post( '/customer/portal', { preHandler: async (request, reply) => { await fastify.requireAuth(request as AuthRequest); }, }, async (request, reply) => { const authReq = request as AuthRequest; const { customerId, returnUrl } = request.body as { customerId: string; returnUrl: string }; if (!authReq.user?.id) { return reply.status(401).send({ error: 'Authentication required' }); } if (!customerId || !returnUrl) { return reply.status(400).send({ error: 'Missing required fields', required: ['customerId', 'returnUrl'], }); } if (!isValidReturnUrl(returnUrl)) { return reply.status(400).send({ error: 'Invalid return URL', message: 'returnUrl must be from an allowed origin', }); } try { const portalSession = await billingService.createCustomerPortalSession( customerId, returnUrl ); return { url: portalSession.url, expiresAt: new Date(portalSession.expires_at * 1000).toISOString(), }; } catch (error) { reply.status(500).send({ error: 'Failed to create portal session', message: error instanceof Error ? error.message : 'Unknown error', }); } } ); // Create customer fastify.post( '/customer', { preHandler: async (request, reply) => { await fastify.requireAuth(request as AuthRequest); }, }, async (request, reply) => { const authReq = request as AuthRequest; const { email, name } = request.body as { email: string; name?: string }; if (!authReq.user?.id) { return reply.status(401).send({ error: 'Authentication required' }); } if (!email) { return reply.status(400).send({ error: 'Email is required', }); } try { const customer = await billingService.createCustomer(email, authReq.user.id); return { customer: { id: customer.id, email: customer.email, name: customer.name, metadata: customer.metadata, }, }; } catch (error) { reply.status(500).send({ error: 'Failed to create customer', message: error instanceof Error ? error.message : 'Unknown error', }); } } ); // Get user tier fastify.get( '/user/tier', { preHandler: async (request, reply) => { await fastify.requireAuth(request as AuthRequest); }, }, async (request, reply) => { const authReq = request as AuthRequest; if (!authReq.user?.id) { return reply.status(401).send({ error: 'Authentication required' }); } try { const tier = await billingService.getUserTier(authReq.user.id); if (!tier) { return { tier: 'free' as SubscriptionTier, limits: await billingService.getTierLimits('free'), }; } const limits = await billingService.getTierLimits(tier); return { tier, limits, }; } catch (error) { reply.status(500).send({ error: 'Failed to fetch user tier', message: error instanceof Error ? error.message : 'Unknown error', }); } } ); // Get invoice history fastify.get( '/invoices', { preHandler: async (request, reply) => { await fastify.requireAuth(request as AuthRequest); }, }, async (request, reply) => { const authReq = request as AuthRequest; const { customerId } = request.query as { customerId?: string }; if (!authReq.user?.id) { return reply.status(401).send({ error: 'Authentication required' }); } if (!customerId) { return reply.status(400).send({ error: 'customerId is required', }); } // Verify the customer belongs to the authenticated user (IDOR prevention) try { await billingService.verifyCustomerOwnership(customerId, authReq.user.id); } catch { return reply.status(403).send({ error: 'Forbidden', message: 'You do not have access to this customer', }); } try { const invoices = await billingService.getInvoiceHistory(customerId); return { invoices: invoices.data.map((invoice) => ({ id: invoice.id, amountDue: invoice.amount_due, amountPaid: invoice.amount_paid, status: invoice.status, created: new Date(invoice.created * 1000).toISOString(), hostedInvoiceUrl: invoice.hosted_invoice_url, })), hasMore: invoices.has_more, }; } catch (error) { reply.status(500).send({ error: 'Failed to fetch invoice history', message: error instanceof Error ? error.message : 'Unknown error', }); } } ); // Webhook handler (public endpoint) fastify.post( '/webhooks/stripe', { // Skip authentication for webhooks preHandler: async (request, reply) => { // Don't require auth for webhooks }, }, async (request, reply) => { const sig = request.headers['stripe-signature'] as string; if (!sig) { return reply.status(400).send({ error: 'Missing Stripe signature', }); } try { const event = await billingService.handleWebhook(sig, request.rawBody as Buffer); if (!event) { return reply.status(200).send({ received: true, message: 'Event already processed', }); } // Handle different event types using webhook service await webhookService.handleWebhook(event); return { received: true }; } catch (error) { console.error('Webhook error:', error); return reply.status(400).send({ error: 'Webhook handler failed', message: error instanceof Error ? error.message : 'Unknown error', }); } } ); }