/** * Subscription + payment REST endpoints. * * GET /subscriptions/:userId — get user subscription * POST /subscriptions — create subscription * PUT /subscriptions/:id — update subscription * GET /payments/:userId — list user payments * POST /payments — record a payment */ import type { FastifyInstance } from 'fastify'; import { PRODUCT_ID } from '../../lib/product-config.js'; import { BadRequestError, NotFoundError } from '../../lib/errors.js'; import * as repo from './repository.js'; import { CreateSubscriptionSchema, UpdateSubscriptionSchema, CreatePaymentSchema, type SubscriptionDoc, type PaymentDoc, } from './types.js'; export async function subscriptionRoutes(app: FastifyInstance) { // Get subscription by userId app.get('/subscriptions/:userId', async req => { const { userId } = req.params as { userId: string }; const sub = await repo.getByUserId(userId); if (!sub) throw new NotFoundError('Subscription not found'); return sub; }); // Create subscription app.post('/subscriptions', async (req, reply) => { const parsed = CreateSubscriptionSchema.safeParse(req.body); if (!parsed.success) { throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); } const input = parsed.data; const now = new Date(); const periodEnd = new Date(now); if (input.trialDays && input.trialDays > 0) { periodEnd.setDate(periodEnd.getDate() + input.trialDays); } else { periodEnd.setMonth(periodEnd.getMonth() + 1); } const doc: SubscriptionDoc = { id: `sub_${input.userId}_${Date.now()}`, productId: PRODUCT_ID, userId: input.userId, plan: input.plan, status: input.status, currentPeriodStart: now.toISOString(), currentPeriodEnd: periodEnd.toISOString(), cancelAtPeriodEnd: false, monthlyPrice: input.monthlyPrice, tokensIncluded: input.tokensIncluded, tokensUsed: 0, ...(input.stripeCustomerId && { stripeCustomerId: input.stripeCustomerId }), ...(input.stripeSubscriptionId && { stripeSubscriptionId: input.stripeSubscriptionId }), createdAt: now.toISOString(), updatedAt: now.toISOString(), }; const created = await repo.createSubscription(doc); reply.code(201); return created; }); // Update subscription by userId (looks up subscription, then updates) app.put('/subscriptions/:userId', async req => { const { userId } = req.params as { userId: string }; const parsed = UpdateSubscriptionSchema.safeParse(req.body); if (!parsed.success) { throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); } const existing = await repo.getByUserId(userId); if (!existing) throw new NotFoundError('Subscription not found'); const updated = await repo.updateSubscription(existing.id, userId, parsed.data); if (!updated) throw new NotFoundError('Subscription update failed'); return updated; }); // List payments app.get('/payments/:userId', async req => { const { userId } = req.params as { userId: string }; const { limit = '50' } = req.query as { limit?: string }; return { payments: await repo.getPaymentsByUser(userId, Number(limit)) }; }); // Create payment app.post('/payments', async (req, reply) => { const parsed = CreatePaymentSchema.safeParse(req.body); if (!parsed.success) { throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); } const input = parsed.data; const doc: PaymentDoc = { id: `pay_${crypto.randomUUID()}`, productId: PRODUCT_ID, ...input, createdAt: new Date().toISOString(), }; const created = await repo.createPayment(doc); reply.code(201); return created; }); }