import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import fp from 'fastify-plugin'; import { eq } from 'drizzle-orm'; import { subscriptions } from '../db/schema/subscriptions.js'; import { forbidden } from '../utils/errors.js'; export type SubscriptionInfo = { plan: 'free' | 'pro'; status: 'active' | 'trialing' | 'cancelled' | 'expired'; isPro: boolean; expiresAt: Date | null; }; declare module 'fastify' { interface FastifyRequest { subscription?: SubscriptionInfo | null; } interface FastifyInstance { withSubscription: (req: FastifyRequest, reply: FastifyReply) => Promise; requirePro: (req: FastifyRequest, reply: FastifyReply) => Promise; } } async function loadSubscription(db: FastifyInstance['db'], userId: string): Promise { const [sub] = await db .select() .from(subscriptions) .where(eq(subscriptions.userId, userId)) .limit(1); if (!sub) return null; const now = new Date(); const isExpired = sub.expiresAt && sub.expiresAt < now; const isPro = sub.plan === 'pro' && (sub.status === 'active' || sub.status === 'trialing') && !isExpired; return { plan: sub.plan as 'free' | 'pro', status: sub.status as SubscriptionInfo['status'], isPro, expiresAt: sub.expiresAt, }; } export async function requirePro(req: FastifyRequest, _reply: FastifyReply): Promise { const sub = req.subscription; if (!sub?.isPro) { throw forbidden('Pro subscription required'); } } const subscriptionPlugin = async (app: FastifyInstance) => { app.decorateRequest('subscription', undefined); app.decorate('withSubscription', async (req: FastifyRequest, _reply: FastifyReply) => { if (!req.user?.id) return; const sub = await loadSubscription(app.db, req.user.id); req.subscription = sub; }); app.decorate('requirePro', requirePro); }; export default fp(subscriptionPlugin, { name: 'subscription', dependencies: ['database', 'auth'], });