- Added @eslint/js dependency - Updated eslint.config.js for ESLint 9 compatibility - Added required globals (crypto, localStorage, React, etc.) - Fixed unused imports and variables - Disabled sort-imports temporarily - Formatted all files with Prettier
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;
|
|
});
|
|
}
|