/** * 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; }); }