| 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; |
|
|