sare26's picture
Upload 21 files
ccc21f3 verified
Raw
History Blame Contribute Delete
4.34 kB
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;