import express, { Request, Response } from "express"; import { stripe } from "./stripe"; import { ENV } from "./_core/env"; import * as db from "./db"; export function registerWebhookRoutes(app: express.Application) { // Stripe webhook endpoint - must use raw body for signature verification app.post( "/api/stripe/webhook", express.raw({ type: "application/json" }), async (req: Request, res: Response) => { const sig = req.headers["stripe-signature"]; if (!sig) { console.log("[Webhook] Missing stripe-signature header"); return res.status(400).send("Missing stripe-signature header"); } let event; try { event = stripe.webhooks.constructEvent( req.body, sig, ENV.stripeWebhookSecret ); } catch (err: any) { console.log(`[Webhook] Signature verification failed: ${err.message}`); return res.status(400).send(`Webhook Error: ${err.message}`); } // Handle test events if (event.id.startsWith("evt_test_")) { console.log("[Webhook] Test event detected, returning verification response"); return res.json({ verified: true, }); } console.log(`[Webhook] Received event: ${event.type} (${event.id})`); try { switch (event.type) { case "checkout.session.completed": { const session = event.data.object; console.log(`[Webhook] Checkout session completed: ${session.id}`); // Extract metadata const bountyId = session.metadata?.bounty_id; const userId = session.metadata?.user_id; const type = session.metadata?.type; if (type === "bounty_escrow" && bountyId) { // Update bounty as escrowed await db.updateBounty(parseInt(bountyId), { isEscrowed: true, stripePaymentIntentId: session.payment_intent as string, }); console.log(`[Webhook] Bounty ${bountyId} marked as escrowed`); // Notify the bounty publisher if (userId) { await db.createNotification({ userId: parseInt(userId), type: "payment_received", title: "赏金托管成功", content: "您的悬赏赏金已成功托管,悬赏现已开放接单", relatedId: parseInt(bountyId), relatedType: "bounty", }); } } break; } case "payment_intent.succeeded": { const paymentIntent = event.data.object; console.log(`[Webhook] Payment intent succeeded: ${paymentIntent.id}`); break; } case "transfer.created": { const transfer = event.data.object; console.log(`[Webhook] Transfer created: ${transfer.id}`); const bountyId = transfer.metadata?.bounty_id; if (bountyId) { await db.updateBounty(parseInt(bountyId), { stripeTransferId: transfer.id, isPaid: true, }); console.log(`[Webhook] Bounty ${bountyId} marked as paid`); } break; } case "account.updated": { const account = event.data.object; console.log(`[Webhook] Account updated: ${account.id}`); // Account status changes are handled on-demand via API break; } default: console.log(`[Webhook] Unhandled event type: ${event.type}`); } res.json({ received: true }); } catch (error) { console.error(`[Webhook] Error processing event: ${error}`); res.status(500).json({ error: "Webhook handler failed" }); } } ); }