feat: expose A2A referral status
Some checks failed
CI and Production Smoke / smoke (push) Has been cancelled
Some checks failed
CI and Production Smoke / smoke (push) Has been cancelled
This commit is contained in:
237
apps/web/src/app/api/a2a/referrals/status/route.ts
Normal file
237
apps/web/src/app/api/a2a/referrals/status/route.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { asA2aAgentActorId, logA2aTrafficEvent } from "@/lib/a2a-traffic";
|
||||
import { buildDemandProposalUrl, sanitizeAgentId } from "@/lib/a2a-growth";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const TRACKED_REFERRAL_ACTIONS = [
|
||||
"EXTERNAL_A2A_GROWTH_KIT_ISSUED",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_VIEW",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED",
|
||||
] as const;
|
||||
|
||||
type CurrencyBreakdown = Record<string, {
|
||||
pending_cents: number;
|
||||
paid_cents: number;
|
||||
refunded_cents: number;
|
||||
total_cents: number;
|
||||
}>;
|
||||
|
||||
function emptyCurrencyRow() {
|
||||
return {
|
||||
pending_cents: 0,
|
||||
paid_cents: 0,
|
||||
refunded_cents: 0,
|
||||
total_cents: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function addAffiliateAmount(
|
||||
breakdown: CurrencyBreakdown,
|
||||
currency: string,
|
||||
status: string,
|
||||
amount: number
|
||||
) {
|
||||
const key = currency || "USD";
|
||||
breakdown[key] ||= emptyCurrencyRow();
|
||||
breakdown[key].total_cents += amount;
|
||||
|
||||
const normalizedStatus = status.toUpperCase();
|
||||
if (normalizedStatus === "PAID") {
|
||||
breakdown[key].paid_cents += amount;
|
||||
} else if (normalizedStatus === "REFUNDED") {
|
||||
breakdown[key].refunded_cents += amount;
|
||||
} else {
|
||||
breakdown[key].pending_cents += amount;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const agentId = sanitizeAgentId(request.nextUrl.searchParams.get("agent_id"));
|
||||
|
||||
if (!agentId) {
|
||||
return NextResponse.json({ error: "agent_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const actorId = asA2aAgentActorId(agentId);
|
||||
|
||||
const [
|
||||
agentProfile,
|
||||
scoutReputation,
|
||||
referredTaskCount,
|
||||
referredTasks,
|
||||
affiliateRows,
|
||||
actionCountValues,
|
||||
latestTrafficEvent,
|
||||
] = await Promise.all([
|
||||
prisma.agentProfile.findUnique({
|
||||
where: { agent_id: agentId },
|
||||
select: {
|
||||
status: true,
|
||||
type: true,
|
||||
wallet_address: true,
|
||||
discovery_source: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
},
|
||||
}),
|
||||
prisma.scoutReputation.findUnique({
|
||||
where: { scout_id: agentId },
|
||||
select: {
|
||||
successful_conversions: true,
|
||||
spam_score: true,
|
||||
chargeback_count: true,
|
||||
},
|
||||
}),
|
||||
prisma.task.count({
|
||||
where: {
|
||||
OR: [
|
||||
{ referred_by_agent: agentId },
|
||||
{ scout_id: agentId },
|
||||
],
|
||||
},
|
||||
}),
|
||||
prisma.task.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ referred_by_agent: agentId },
|
||||
{ scout_id: agentId },
|
||||
],
|
||||
},
|
||||
orderBy: { created_at: "desc" },
|
||||
take: 25,
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
reward_amount: true,
|
||||
reward_currency: true,
|
||||
stripe_payment_intent_id: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
},
|
||||
}),
|
||||
prisma.affiliateLedger.findMany({
|
||||
where: { scout_id: agentId },
|
||||
orderBy: { created_at: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
task_id: true,
|
||||
amount: true,
|
||||
currency: true,
|
||||
status: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
},
|
||||
}),
|
||||
Promise.all(
|
||||
TRACKED_REFERRAL_ACTIONS.map((action) =>
|
||||
prisma.auditEvent.count({
|
||||
where: {
|
||||
actorId,
|
||||
action,
|
||||
},
|
||||
})
|
||||
)
|
||||
),
|
||||
prisma.auditEvent.findMany({
|
||||
where: {
|
||||
actorId,
|
||||
action: { in: [...TRACKED_REFERRAL_ACTIONS] },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 1,
|
||||
select: {
|
||||
action: true,
|
||||
entityId: true,
|
||||
createdAt: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const actionCounts = Object.fromEntries(
|
||||
TRACKED_REFERRAL_ACTIONS.map((action, index) => [action, actionCountValues[index] || 0])
|
||||
);
|
||||
|
||||
const affiliateBreakdown: CurrencyBreakdown = {};
|
||||
for (const row of affiliateRows) {
|
||||
addAffiliateAmount(affiliateBreakdown, row.currency, row.status, row.amount);
|
||||
}
|
||||
|
||||
const paidTaskIds = new Set(affiliateRows.map((row) => row.task_id));
|
||||
const taskById = new Map(referredTasks.map((task) => [task.id, task]));
|
||||
const recentConversions = affiliateRows.slice(0, 10).map((row) => {
|
||||
const task = taskById.get(row.task_id);
|
||||
return {
|
||||
task_id: row.task_id,
|
||||
affiliate_ledger_id: row.id,
|
||||
affiliate_amount_cents: row.amount,
|
||||
affiliate_currency: row.currency,
|
||||
affiliate_status: row.status,
|
||||
proposal_status: task?.status || "UNKNOWN",
|
||||
payment_method: task?.stripe_payment_intent_id?.startsWith("wallet:") ? "wallet" : "stripe_or_card",
|
||||
created_at: row.created_at.toISOString(),
|
||||
updated_at: row.updated_at.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
const latestTrafficAt = latestTrafficEvent[0]?.createdAt || null;
|
||||
|
||||
await logA2aTrafficEvent({
|
||||
headers: request.headers,
|
||||
fallbackAgentId: agentId,
|
||||
action: "EXTERNAL_A2A_REFERRAL_STATUS_VIEW",
|
||||
surface: "a2a/referrals/status",
|
||||
entityId: `referral-status:${agentId}`,
|
||||
reason: "external_agent_referral_status_view",
|
||||
metadata: {
|
||||
agent_id: agentId,
|
||||
response_status: 200,
|
||||
response_summary: "a2a_referral_status_ok",
|
||||
proposal_count: referredTaskCount,
|
||||
paid_conversion_count: affiliateRows.length,
|
||||
pending_affiliate_ledgers: affiliateRows.filter((row) => row.status.toUpperCase() === "PENDING").length,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
agent_id: agentId,
|
||||
agent_status: agentProfile?.status || "UNREGISTERED",
|
||||
agent_type: agentProfile?.type || "SCOUT",
|
||||
has_wallet: Boolean(agentProfile?.wallet_address),
|
||||
discovery_source: agentProfile?.discovery_source || null,
|
||||
referral_url: buildDemandProposalUrl({
|
||||
referralAgent: agentId,
|
||||
campaign: "a2a-agent-referral",
|
||||
source: "external-agent",
|
||||
}),
|
||||
payout_policy: {
|
||||
referral_fee: "10% of collected proposal routing fees.",
|
||||
status: "Affiliate ledger starts PENDING. Payout requires platform review and wallet or Stripe Connect binding.",
|
||||
payment_truth: "Paid conversion is counted only after Stripe webhook or verified USDC wallet receipt.",
|
||||
},
|
||||
traffic_funnel: {
|
||||
growth_kit_events: actionCounts.EXTERNAL_A2A_GROWTH_KIT_ISSUED || 0,
|
||||
proposal_view_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_VIEW || 0,
|
||||
proposal_created_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED || 0,
|
||||
proposal_checkout_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED || 0,
|
||||
proposal_wallet_pending_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING || 0,
|
||||
proposal_paid_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED || 0,
|
||||
latest_event_at: latestTrafficAt ? latestTrafficAt.toISOString() : null,
|
||||
},
|
||||
proposal_summary: {
|
||||
referred_private_drafts: referredTaskCount,
|
||||
paid_conversions: paidTaskIds.size,
|
||||
pending_affiliate_ledgers: affiliateRows.filter((row) => row.status.toUpperCase() === "PENDING").length,
|
||||
successful_conversions: scoutReputation?.successful_conversions || 0,
|
||||
spam_score: scoutReputation?.spam_score || 0,
|
||||
chargeback_count: scoutReputation?.chargeback_count || 0,
|
||||
},
|
||||
affiliate_breakdown: affiliateBreakdown,
|
||||
recent_conversions: recentConversions,
|
||||
});
|
||||
}
|
||||
@@ -11,6 +11,7 @@ const EVENT_LABELS: Record<string, string> = {
|
||||
EXTERNAL_LIST_OPEN_TASKS: "外部公開流量頁讀取 open tasks",
|
||||
EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW: "外部 Agent 讀取 A2A 整合目錄",
|
||||
EXTERNAL_A2A_GROWTH_KIT_ISSUED: "外部 Agent 領取 growth kit",
|
||||
EXTERNAL_A2A_REFERRAL_STATUS_VIEW: "外部 Agent 查詢 referral 狀態",
|
||||
EXTERNAL_DEMAND_PROPOSAL_VIEW: "外部導流需求方查看提案頁",
|
||||
EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED: "外部導流需求方建立提案",
|
||||
EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED: "外部導流需求方開始 Stripe 結帳",
|
||||
|
||||
Reference in New Issue
Block a user