learning_ai_common_plat/services/billing-service/src/modules/subscriptions/routes.ts
saravanakumardb1 86a56339ab fix: replace Math.random() IDs with crypto.randomUUID() across all services
- billing-service: licenses, subscriptions (pay_, lic_)
- growth-service: invitations, referrals (inv_, ref_)
- platform-service: auth, audit (usr_, aud_)
- tracker-service: items, comments, votes, public (trk_, cmt_, vote_)
- Add votes.test.ts — closes the only missing module test
2026-02-12 13:03:09 -08:00

109 lines
3.8 KiB
TypeScript

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