116 lines
3.8 KiB
TypeScript
116 lines
3.8 KiB
TypeScript
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" });
|
|
}
|
|
}
|
|
);
|
|
}
|