- 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
109 lines
3.8 KiB
TypeScript
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;
|
|
});
|
|
}
|