Files
agent-bounty-protocol/apps/web/src/app/api/webhooks/stripe/route.ts
OG T 745ff300b5
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
feat: harden A2A funnel and paid proposal intake
2026-06-11 11:28:08 +08:00

192 lines
5.9 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import Stripe from "stripe";
import { TaskStatus } from "@agent-bounty/contracts";
import { broadcastFomoEvent } from "@/lib/x-broadcaster";
import { sanitizeAgentId } from "@/lib/a2a-growth";
const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2026-05-27.dahlia",
}) : null;
function getStripeObjectId(value: string | Stripe.PaymentIntent | Stripe.SetupIntent | null) {
if (typeof value === "string") return value;
return value?.id || null;
}
async function handleDemandProposalFee(session: Stripe.Checkout.Session) {
const metadata = session.metadata || {};
const taskId = metadata.task_id;
const referralAgent = sanitizeAgentId(metadata.referral_agent);
const proposalFeeCents = Number(metadata.proposal_fee_cents || session.amount_total || 0);
const paymentIntentId = getStripeObjectId(session.payment_intent);
const task = await prisma.task.findFirst({
where: taskId
? { id: taskId }
: { stripe_checkout_session_id: session.id },
});
if (!task) {
console.error(`[Webhook] Proposal task not found for session: ${session.id}`);
return;
}
await prisma.$transaction(async (tx) => {
await tx.task.update({
where: { id: task.id },
data: {
stripe_checkout_session_id: session.id,
stripe_payment_intent_id: paymentIntentId,
status: TaskStatus.DRAFT,
},
});
if (referralAgent) {
await tx.agentProfile.upsert({
where: { agent_id: referralAgent },
update: {
discovery_source: "DEMAND_PROPOSAL_PAID_REFERRAL",
},
create: {
agent_id: referralAgent,
type: "SCOUT",
status: "PENDING",
discovery_source: "DEMAND_PROPOSAL_PAID_REFERRAL",
capabilities: {
growth_referral: true,
source: metadata.source || "stripe-webhook",
},
},
});
await tx.scoutReputation.upsert({
where: { scout_id: referralAgent },
update: {
successful_conversions: { increment: 1 },
},
create: {
scout_id: referralAgent,
successful_conversions: 1,
},
});
const existingAffiliate = await tx.affiliateLedger.findFirst({
where: {
scout_id: referralAgent,
task_id: task.id,
status: "PENDING",
},
});
if (!existingAffiliate && proposalFeeCents > 0) {
await tx.affiliateLedger.create({
data: {
scout_id: referralAgent,
task_id: task.id,
amount: Math.floor(proposalFeeCents * 0.1),
currency: "USD",
status: "PENDING",
},
});
}
}
await tx.auditEvent.create({
data: {
actorType: "SYSTEM",
actorId: "stripe-webhook",
action: "DEMAND_PROPOSAL_FEE_CAPTURED",
entityType: "TASK",
entityId: task.id,
metadata: {
stripe_session_id: session.id,
stripe_payment_intent_id: paymentIntentId,
fee_cents: proposalFeeCents,
package_id: metadata.package_id || null,
referral_agent: referralAgent || null,
affiliate_fee_cents: referralAgent ? Math.floor(proposalFeeCents * 0.1) : 0,
source: metadata.source || null,
campaign: metadata.campaign || null,
},
},
});
});
console.log(`[Webhook] Proposal fee captured for task ${task.id}. Payment Intent: ${paymentIntentId}`);
}
export async function POST(request: NextRequest) {
const payload = await request.text();
const signature = request.headers.get("stripe-signature");
if (!signature || !process.env.STRIPE_WEBHOOK_SECRET) {
return NextResponse.json({ error: "Missing signature or webhook secret" }, { status: 400 });
}
let event: Stripe.Event;
try {
if (!stripe) {
throw new Error("Stripe SDK is not initialized");
}
event = stripe.webhooks.constructEvent(
payload,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (error: unknown) {
console.error("[Webhook Error]", error);
return NextResponse.json({ error: "Webhook signature verification failed" }, { status: 400 });
}
try {
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
if (session.metadata?.intent === "DEMAND_PROPOSAL_FEE") {
await handleDemandProposalFee(session);
return NextResponse.json({ received: true });
}
const task = await prisma.task.findFirst({
where: { stripe_checkout_session_id: session.id }
});
if (!task) {
console.error(`[Webhook] Task not found for session: ${session.id}`);
return NextResponse.json({ received: true });
}
// Payment is authorized (Auth Hold)
// Save the payment_intent_id and set status to OPEN
await prisma.task.update({
where: { id: task.id },
data: {
stripe_payment_intent_id: session.payment_intent as string,
status: TaskStatus.OPEN
}
});
console.log(`[Webhook] Task ${task.id} is now OPEN. Payment Intent: ${session.payment_intent}`);
// High-Value Bounty FOMO trigger ($50 USD = 5000 cents)
if (task.reward_amount >= 5000) {
const formatted = task.reward_currency === "USD"
? `$${(task.reward_amount / 100).toFixed(0)}`
: `NT$${task.reward_amount}`;
void broadcastFomoEvent({
type: "HIGH_VALUE_BOUNTY",
taskId: task.id,
amountFormatted: formatted
});
}
}
return NextResponse.json({ received: true });
} catch (error: unknown) {
console.error("[Webhook Processing Error]", error);
return NextResponse.json({ error: "Internal Error" }, { status: 500 });
}
}