192 lines
5.9 KiB
TypeScript
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 });
|
|
}
|
|
}
|