aethex-forge/api/subscriptions/webhook.ts
sirpiglr 2decee28e2 Add subscription management and pricing tiers to the platform
Implement Stripe integration for subscription creation, management, and webhook handling. Update pricing page to display membership tiers and integrate checkout functionality.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 6d5c407a-473f-4820-a33d-abb2ae3e6b37
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/MdI1YXa
Replit-Helium-Checkpoint-Created: true
2025-12-12 23:27:03 +00:00

206 lines
6 KiB
TypeScript

import type { VercelRequest, VercelResponse } from "@vercel/node";
import Stripe from "stripe";
import { getAdminClient } from "../_supabase.js";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2024-04-10",
});
const webhookSecret = process.env.STRIPE_SUBSCRIPTION_WEBHOOK_SECRET || "";
async function getUserIdBySubscription(admin: any, subscriptionId: string): Promise<string | null> {
const { data } = await admin
.from("user_profiles")
.select("id")
.eq("stripe_subscription_id", subscriptionId)
.single();
return data?.id || null;
}
export const config = {
api: {
bodyParser: false,
},
};
async function getRawBody(req: VercelRequest): Promise<string> {
if (typeof req.body === "string") {
return req.body;
}
const chunks: Buffer[] = [];
for await (const chunk of req as any) {
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
}
return Buffer.concat(chunks).toString("utf8");
}
export default async function handler(req: VercelRequest, res: VercelResponse) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const admin = getAdminClient();
const sig = req.headers["stripe-signature"] as string;
let event: Stripe.Event;
try {
if (!sig || !webhookSecret) {
console.warn("Missing webhook signature or secret");
return res
.status(400)
.json({ error: "Missing webhook signature or secret" });
}
const rawBody = await getRawBody(req);
event = stripe.webhooks.constructEvent(rawBody, sig, webhookSecret);
} catch (error: any) {
console.error("Webhook signature verification failed:", error.message);
return res
.status(400)
.json({ error: "Webhook signature verification failed" });
}
try {
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
const { userId, tier } = session.metadata || {};
if (userId && tier && session.subscription) {
await admin
.from("user_profiles")
.update({
tier: tier,
stripe_subscription_id: session.subscription as string,
})
.eq("id", userId);
console.log(
`[Subscription] User ${userId} upgraded to ${tier} tier`
);
}
break;
}
case "customer.subscription.updated": {
const subscription = event.data.object as Stripe.Subscription;
let { userId, tier } = subscription.metadata || {};
if (!userId) {
userId = await getUserIdBySubscription(admin, subscription.id);
}
if (userId) {
const newStatus = subscription.status;
if (newStatus === "active") {
await admin
.from("user_profiles")
.update({
tier: tier || "pro",
stripe_subscription_id: subscription.id,
})
.eq("id", userId);
console.log(
`[Subscription] User ${userId} subscription active, tier: ${tier || "pro"}`
);
} else if (
newStatus === "canceled" ||
newStatus === "unpaid" ||
newStatus === "past_due"
) {
await admin
.from("user_profiles")
.update({
tier: "free",
})
.eq("id", userId);
console.log(
`[Subscription] User ${userId} subscription ${newStatus}, downgraded to free`
);
}
} else {
console.warn(
`[Subscription] Could not find user for subscription ${subscription.id}`
);
}
break;
}
case "customer.subscription.deleted": {
const subscription = event.data.object as Stripe.Subscription;
let { userId } = subscription.metadata || {};
if (!userId) {
userId = await getUserIdBySubscription(admin, subscription.id);
}
if (userId) {
await admin
.from("user_profiles")
.update({
tier: "free",
stripe_subscription_id: null,
})
.eq("id", userId);
console.log(
`[Subscription] User ${userId} subscription deleted, downgraded to free`
);
} else {
console.warn(
`[Subscription] Could not find user for deleted subscription ${subscription.id}`
);
}
break;
}
case "invoice.payment_succeeded": {
const invoice = event.data.object as Stripe.Invoice;
const subscriptionId = invoice.subscription as string;
if (subscriptionId) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const { userId } = subscription.metadata || {};
if (userId) {
console.log(
`[Subscription] Payment succeeded for user ${userId}, subscription ${subscriptionId}`
);
}
}
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
const subscriptionId = invoice.subscription as string;
if (subscriptionId) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const { userId } = subscription.metadata || {};
if (userId) {
console.warn(
`[Subscription] Payment failed for user ${userId}, subscription ${subscriptionId}`
);
}
}
break;
}
default:
console.log(`[Subscription Webhook] Unhandled event type: ${event.type}`);
}
return res.status(200).json({ received: true });
} catch (error: any) {
console.error("Subscription webhook processing error:", error);
return res
.status(500)
.json({ error: error?.message || "Webhook processing failed" });
}
}