From 69959d43792c0b6e9e51950f0116067399406aca Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Sat, 15 Nov 2025 19:30:46 +0000 Subject: [PATCH] Stripe webhook handler for payment events cgen-4e859257a9b846cb9db6a4ea535a1ed3 --- api/nexus/payments/webhook.ts | 178 ++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 api/nexus/payments/webhook.ts diff --git a/api/nexus/payments/webhook.ts b/api/nexus/payments/webhook.ts new file mode 100644 index 00000000..adc506c5 --- /dev/null +++ b/api/nexus/payments/webhook.ts @@ -0,0 +1,178 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; +import Stripe from "stripe"; +import { getAdminClient } from "../../_supabase"; + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", { + apiVersion: "2024-11-20", +}); + +const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || ""; + +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) { + return res.status(400).json({ error: "Missing webhook signature or secret" }); + } + + const body = typeof req.body === "string" ? req.body : JSON.stringify(req.body); + event = stripe.webhooks.constructEvent(body, 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 "payment_intent.succeeded": { + const paymentIntent = event.data.object as Stripe.PaymentIntent; + const { opportunityId, creatorId, clientId } = paymentIntent.metadata; + + // Find contract by payment intent ID + const { data: contract } = await admin + .from("nexus_contracts") + .select("*") + .eq("stripe_payment_intent_id", paymentIntent.id) + .single(); + + if (contract) { + // Update contract status + await admin + .from("nexus_contracts") + .update({ + status: "active", + start_date: new Date().toISOString(), + }) + .eq("id", contract.id); + + // Create payment record if not already created + const { data: existingPayment } = await admin + .from("nexus_payments") + .select("id") + .eq("stripe_payment_intent_id", paymentIntent.id) + .single(); + + if (!existingPayment) { + await admin + .from("nexus_payments") + .insert({ + contract_id: contract.id, + amount: contract.total_amount, + creator_payout: contract.creator_payout_amount, + aethex_commission: contract.aethex_commission_amount, + payment_method: "stripe", + payment_status: "completed", + payment_date: new Date().toISOString(), + stripe_payment_intent_id: paymentIntent.id, + }); + } + + // Update creator earnings + const { data: creatorProfile } = await admin + .from("nexus_creator_profiles") + .select("total_earnings") + .eq("user_id", creatorId) + .single(); + + const newEarnings = + (creatorProfile?.total_earnings || 0) + contract.creator_payout_amount; + + await admin + .from("nexus_creator_profiles") + .update({ total_earnings: newEarnings }) + .eq("user_id", creatorId); + + // Update opportunity status + await admin + .from("nexus_opportunities") + .update({ status: "filled", selected_creator_id: creatorId }) + .eq("id", opportunityId); + + // Update application status + await admin + .from("nexus_applications") + .update({ status: "hired" }) + .eq("opportunity_id", opportunityId) + .eq("creator_id", creatorId); + } + break; + } + + case "payment_intent.payment_failed": { + const paymentIntent = event.data.object as Stripe.PaymentIntent; + + // Find and update contract + const { data: contract } = await admin + .from("nexus_contracts") + .select("*") + .eq("stripe_payment_intent_id", paymentIntent.id) + .single(); + + if (contract) { + // Update contract to cancelled + await admin + .from("nexus_contracts") + .update({ status: "cancelled" }) + .eq("id", contract.id); + + // Create failed payment record + await admin + .from("nexus_payments") + .insert({ + contract_id: contract.id, + amount: contract.total_amount, + creator_payout: 0, + aethex_commission: 0, + payment_method: "stripe", + payment_status: "failed", + stripe_payment_intent_id: paymentIntent.id, + }); + } + break; + } + + case "charge.refunded": { + const charge = event.data.object as Stripe.Charge; + + // Find payment by stripe charge ID + const { data: payment } = await admin + .from("nexus_payments") + .select("*") + .eq("stripe_charge_id", charge.id) + .single(); + + if (payment) { + // Update payment status + await admin + .from("nexus_payments") + .update({ payment_status: "refunded" }) + .eq("id", payment.id); + + // Update contract status + await admin + .from("nexus_contracts") + .update({ status: "cancelled" }) + .eq("id", payment.contract_id); + } + break; + } + + default: + console.log(`Unhandled event type: ${event.type}`); + } + + return res.status(200).json({ received: true }); + } catch (error: any) { + console.error("Webhook processing error:", error); + return res + .status(500) + .json({ error: error?.message || "Webhook processing failed" }); + } +}