import { Router, type Request, type Response } from "express"; import { db, subscriptionsTable } from "@workspace/db"; import { eq } from "drizzle-orm"; import { requireAuth, type AuthedRequest } from "../../lib/auth"; import { createSubscription, getSubscription, cancelSubscriptionApi, } from "../../lib/paypal"; import { ActivateSubscriptionBody } from "@workspace/api-zod"; const router = Router(); function buildOrigin(req: Request): string { const proto = (req.headers["x-forwarded-proto"] as string) || req.protocol; const host = req.headers["x-forwarded-host"] || req.headers.host; return `${proto}://${host}`; } function mapStatus(paypalStatus: string): string { switch (paypalStatus) { case "APPROVAL_PENDING": case "APPROVED": case "ACTIVE": return "active"; case "SUSPENDED": return "suspended"; case "CANCELLED": return "cancelled"; case "EXPIRED": return "expired"; default: return "none"; } } router.post("/create", requireAuth, async (req: Request, res: Response) => { const { userId } = req as AuthedRequest; const origin = buildOrigin(req); const returnUrl = `${origin}/subscription/return`; const cancelUrl = `${origin}/subscription/cancel`; try { const result = await createSubscription(returnUrl, cancelUrl, userId); res.json({ subscriptionId: result.id, approvalUrl: result.approvalUrl }); } catch (err) { req.log.error({ err }, "Failed to create PayPal subscription"); res.status(500).json({ error: "Failed to create subscription" }); } }); router.post("/activate", requireAuth, async (req: Request, res: Response) => { const { userId } = req as AuthedRequest; const parsed = ActivateSubscriptionBody.safeParse(req.body); if (!parsed.success) { res.status(400).json({ error: "Invalid body" }); return; } const { subscriptionId } = parsed.data; try { const sub = await getSubscription(subscriptionId); if (sub.custom_id !== userId) { res.status(403).json({ error: "Subscription does not belong to user" }); return; } const status = mapStatus(sub.status); let trialEnd: Date | null = null; let currentPeriodEnd: Date | null = null; if (sub.billing_info?.next_billing_time) { const next = new Date(sub.billing_info.next_billing_time); if (status === "trialing") trialEnd = next; else currentPeriodEnd = next; } await db .insert(subscriptionsTable) .values({ userId, paypalSubscriptionId: subscriptionId, status, trialEnd, currentPeriodEnd, updatedAt: new Date(), }) .onConflictDoUpdate({ target: subscriptionsTable.userId, set: { paypalSubscriptionId: subscriptionId, status, trialEnd, currentPeriodEnd, updatedAt: new Date(), }, }); res.json({ status, trialEnd: trialEnd?.toISOString() ?? null, currentPeriodEnd: currentPeriodEnd?.toISOString() ?? null, paypalSubscriptionId: subscriptionId, hasAccess: status === "active" || status === "trialing", }); } catch (err) { req.log.error({ err }, "Failed to activate subscription"); res.status(500).json({ error: "Failed to activate" }); } }); router.post("/cancel", requireAuth, async (req: Request, res: Response) => { const { userId } = req as AuthedRequest; const [sub] = await db .select() .from(subscriptionsTable) .where(eq(subscriptionsTable.userId, userId)); if (!sub?.paypalSubscriptionId) { res.status(404).json({ error: "No active subscription" }); return; } try { await cancelSubscriptionApi(sub.paypalSubscriptionId, "User requested cancellation"); await db .update(subscriptionsTable) .set({ status: "cancelled", updatedAt: new Date() }) .where(eq(subscriptionsTable.userId, userId)); res.json({ status: "cancelled", trialEnd: sub.trialEnd?.toISOString() ?? null, currentPeriodEnd: sub.currentPeriodEnd?.toISOString() ?? null, paypalSubscriptionId: sub.paypalSubscriptionId, hasAccess: false, }); } catch (err) { req.log.error({ err }, "Failed to cancel subscription"); res.status(500).json({ error: "Failed to cancel" }); } }); export default router;