feat: add wallet receipt review loop
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s
This commit is contained in:
@@ -24,6 +24,33 @@ type TreasuryStats = {
|
||||
direction?: "in" | "out";
|
||||
date: string;
|
||||
}>;
|
||||
pending_wallet_receipts?: Array<{
|
||||
id: string;
|
||||
task_id: string;
|
||||
payment_reference: string;
|
||||
raw_payment_reference: string;
|
||||
payer_wallet?: string | null;
|
||||
network?: string | null;
|
||||
amount_cents?: number;
|
||||
package_id?: string | null;
|
||||
referral_agent?: string | null;
|
||||
note?: string | null;
|
||||
submitted_at: string;
|
||||
}>;
|
||||
pending_affiliate_payouts?: Array<{
|
||||
id: string;
|
||||
scout_id: string;
|
||||
scout_wallet?: string | null;
|
||||
scout_status: string;
|
||||
task_id: string;
|
||||
task_title: string;
|
||||
proposal_status: string;
|
||||
payment_reference?: string | null;
|
||||
amount_cents: number;
|
||||
currency: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export default function TreasuryDashboard() {
|
||||
@@ -41,6 +68,8 @@ export default function TreasuryDashboard() {
|
||||
const [verifyingWallet, setVerifyingWallet] = useState(false);
|
||||
const [walletMessage, setWalletMessage] = useState("");
|
||||
const cents = (value: number | undefined) => value ?? 0;
|
||||
const pendingWalletReceipts = stats?.pending_wallet_receipts || [];
|
||||
const pendingAffiliatePayouts = stats?.pending_affiliate_payouts || [];
|
||||
|
||||
const loadStats = useCallback(async () => {
|
||||
try {
|
||||
@@ -114,6 +143,22 @@ export default function TreasuryDashboard() {
|
||||
}
|
||||
};
|
||||
|
||||
const selectWalletReceipt = (receipt: NonNullable<TreasuryStats["pending_wallet_receipts"]>[number]) => {
|
||||
setWalletTaskId(receipt.task_id);
|
||||
setWalletReference(receipt.raw_payment_reference || receipt.payment_reference.replace(/^wallet:/, ""));
|
||||
setWalletAmountUsd(receipt.amount_cents ? (receipt.amount_cents / 100).toFixed(2) : "");
|
||||
setWalletNote(
|
||||
[
|
||||
receipt.network ? `network=${receipt.network}` : "",
|
||||
receipt.payer_wallet ? `payer=${receipt.payer_wallet}` : "",
|
||||
receipt.note ? `note=${receipt.note}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("; ")
|
||||
);
|
||||
setWalletMessage("Pending receipt loaded. Verify only after checking the transaction on-chain.");
|
||||
};
|
||||
|
||||
const handleWithdraw = async () => {
|
||||
if (!stats) {
|
||||
setMessage("⚠️ Treasury stats are unavailable.");
|
||||
@@ -246,6 +291,40 @@ export default function TreasuryDashboard() {
|
||||
Verify USDC Proposal Receipt
|
||||
</h2>
|
||||
|
||||
<div className="mb-6 rounded-xl border border-amber-500/20 bg-amber-500/5 p-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<h3 className="text-sm font-semibold text-amber-100">Pending wallet receipts</h3>
|
||||
<span className="rounded bg-amber-400/10 px-2 py-1 text-xs font-mono text-amber-200">
|
||||
{pendingWalletReceipts.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2 max-h-52 overflow-auto">
|
||||
{pendingWalletReceipts.map((receipt) => (
|
||||
<button
|
||||
key={receipt.id}
|
||||
type="button"
|
||||
onClick={() => selectWalletReceipt(receipt)}
|
||||
className="grid w-full gap-2 rounded-lg border border-white/10 bg-black/30 p-3 text-left hover:border-amber-300/50 md:grid-cols-[1fr_auto]"
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-sm font-mono text-white">{receipt.task_id}</span>
|
||||
<span className="block truncate text-xs text-gray-400">
|
||||
{receipt.raw_payment_reference} · {receipt.network || "network n/a"} · {receipt.referral_agent || "direct"}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-sm font-mono text-amber-200">
|
||||
${((receipt.amount_cents || 0) / 100).toFixed(2)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
{pendingWalletReceipts.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-[#333] p-3 text-sm text-gray-500">
|
||||
No submitted wallet receipts are waiting for verification.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Proposal ID</label>
|
||||
@@ -360,6 +439,33 @@ export default function TreasuryDashboard() {
|
||||
|
||||
{/* Activity Feed */}
|
||||
<div className="bg-[#111] border border-[#333] rounded-2xl p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-300 mb-4">Pending Affiliate Payouts</h3>
|
||||
<div className="mb-8 space-y-3">
|
||||
{pendingAffiliatePayouts.map((row) => (
|
||||
<div key={row.id} className="rounded-xl border border-white/10 bg-black/30 p-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-white">{row.scout_id}</p>
|
||||
<p className="mt-1 truncate text-xs text-gray-500">{row.task_title}</p>
|
||||
</div>
|
||||
<p className="shrink-0 text-sm font-mono text-emerald-300">
|
||||
${(row.amount_cents / 100).toFixed(2)} {row.currency}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2 text-[11px] text-gray-400">
|
||||
<span>{row.scout_status}</span>
|
||||
<span>{row.proposal_status}</span>
|
||||
<span>{row.scout_wallet ? "wallet bound" : "wallet missing"}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{pendingAffiliatePayouts.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-[#333] p-4 text-sm text-gray-500">
|
||||
No pending affiliate payouts.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-300 mb-6">Recent Cashflows</h3>
|
||||
<div className="space-y-4">
|
||||
{stats.recent_transactions?.map((tx, idx) => (
|
||||
|
||||
@@ -14,6 +14,7 @@ const TRACKED_REFERRAL_ACTIONS = [
|
||||
"EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_WALLET_RECEIPT_SUBMITTED",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED",
|
||||
] as const;
|
||||
|
||||
@@ -248,6 +249,7 @@ export async function GET(request: NextRequest) {
|
||||
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_wallet_receipt_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_WALLET_RECEIPT_SUBMITTED || 0,
|
||||
proposal_paid_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED || 0,
|
||||
latest_event_at: latestTrafficAt ? latestTrafficAt.toISOString() : null,
|
||||
},
|
||||
|
||||
@@ -443,6 +443,7 @@ export async function GET(request: NextRequest) {
|
||||
const proposalCreatedEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED"] || 0;
|
||||
const proposalCheckoutEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED"] || 0;
|
||||
const proposalWalletPendingEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING"] || 0;
|
||||
const proposalWalletReceiptEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_WALLET_RECEIPT_SUBMITTED"] || 0;
|
||||
const proposalPaidEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED"] || 0;
|
||||
const claimEvents = realExternalActionSummary["EXTERNAL_CLAIM_TASK_SUCCESS"] || 0;
|
||||
const submitEvents = realExternalActionSummary["EXTERNAL_SUBMIT_SOLUTION_SUCCESS"] || 0;
|
||||
@@ -467,6 +468,7 @@ export async function GET(request: NextRequest) {
|
||||
proposal_created_events: proposalCreatedEvents,
|
||||
proposal_checkout_events: proposalCheckoutEvents,
|
||||
proposal_wallet_pending_events: proposalWalletPendingEvents,
|
||||
proposal_wallet_receipt_events: proposalWalletReceiptEvents,
|
||||
proposal_paid_events: proposalPaidEvents,
|
||||
claim_events: claimEvents,
|
||||
submit_events: submitEvents,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { submitWalletReceipt } from "@/app/propose/wallet/actions";
|
||||
import {
|
||||
getProposalPackage,
|
||||
TREASURY_USDC_ADDRESS,
|
||||
@@ -25,6 +26,7 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
|
||||
const params = searchParams ? await searchParams : {};
|
||||
const taskId = getParam(params, "task_id");
|
||||
const payment = getParam(params, "payment") || "stripe";
|
||||
const receiptStatus = getParam(params, "receipt");
|
||||
const proposalPackage = getProposalPackage(getParam(params, "package"));
|
||||
const task = taskId
|
||||
? await prisma.task.findUnique({
|
||||
@@ -40,8 +42,34 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
|
||||
},
|
||||
})
|
||||
: null;
|
||||
const latestWalletReceipt = taskId
|
||||
? await prisma.auditEvent.findFirst({
|
||||
where: {
|
||||
action: "DEMAND_PROPOSAL_WALLET_RECEIPT_SUBMITTED",
|
||||
entityType: "TASK",
|
||||
entityId: taskId,
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: {
|
||||
metadata: true,
|
||||
createdAt: true,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
const stripeCaptured = payment === "stripe" && Boolean(task?.stripe_payment_intent_id);
|
||||
const walletCaptured = payment === "wallet" && Boolean(task?.stripe_payment_intent_id?.startsWith("wallet:"));
|
||||
const receiptMetadata =
|
||||
typeof latestWalletReceipt?.metadata === "object" &&
|
||||
latestWalletReceipt.metadata !== null &&
|
||||
!Array.isArray(latestWalletReceipt.metadata)
|
||||
? (latestWalletReceipt.metadata as Record<string, unknown>)
|
||||
: {};
|
||||
const latestReceiptReference =
|
||||
typeof receiptMetadata.raw_payment_reference === "string"
|
||||
? receiptMetadata.raw_payment_reference
|
||||
: typeof receiptMetadata.payment_reference === "string"
|
||||
? receiptMetadata.payment_reference.replace(/^wallet:/, "")
|
||||
: "";
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-zinc-950 px-5 py-10 text-zinc-100">
|
||||
@@ -138,6 +166,76 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
|
||||
轉帳備註請包含 Proposal ID;入帳確認前 referral ledger 不會進入可出金狀態。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!walletCaptured ? (
|
||||
<div className="mt-4 rounded-md border border-zinc-700 bg-zinc-950/80 p-4">
|
||||
<h2 className="text-sm font-semibold text-white">提交付款 receipt</h2>
|
||||
<p className="mt-1 text-xs leading-5 text-zinc-400">
|
||||
送出 tx hash 後會進入人工確認;確認前不會計入 paid conversion,也不會建立可出金 referral ledger。
|
||||
</p>
|
||||
|
||||
{receiptStatus === "submitted" || latestWalletReceipt ? (
|
||||
<div className="mt-3 rounded-md border border-amber-300/30 bg-amber-300/10 px-3 py-2 text-xs leading-5 text-amber-100">
|
||||
已收到 wallet receipt
|
||||
{latestReceiptReference ? `:${latestReceiptReference}` : ""}。目前狀態:等待 VibeWork 人工確認。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{receiptStatus === "already_verified" ? (
|
||||
<div className="mt-3 rounded-md border border-emerald-300/30 bg-emerald-300/10 px-3 py-2 text-xs leading-5 text-emerald-100">
|
||||
此 wallet receipt 已經確認過,請勿重複轉帳。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<form action={submitWalletReceipt} className="mt-4 grid gap-3">
|
||||
<input type="hidden" name="taskId" value={task?.id || taskId} />
|
||||
<input type="hidden" name="amountUsdc" value={formatUsdcAmount(proposalPackage.feeCents)} />
|
||||
<label className="grid gap-2 text-xs font-medium text-zinc-300">
|
||||
Tx hash / payment reference
|
||||
<input
|
||||
required
|
||||
name="paymentReference"
|
||||
defaultValue={latestReceiptReference}
|
||||
className="h-10 rounded-md border border-zinc-700 bg-black px-3 text-sm text-white outline-none focus:border-emerald-400"
|
||||
placeholder="0x... 或交易 reference"
|
||||
/>
|
||||
</label>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label className="grid gap-2 text-xs font-medium text-zinc-300">
|
||||
Payer wallet
|
||||
<input
|
||||
name="payerWallet"
|
||||
className="h-10 rounded-md border border-zinc-700 bg-black px-3 text-sm text-white outline-none focus:border-emerald-400"
|
||||
placeholder="0x..."
|
||||
/>
|
||||
</label>
|
||||
<label className="grid gap-2 text-xs font-medium text-zinc-300">
|
||||
Network
|
||||
<input
|
||||
name="network"
|
||||
defaultValue={TREASURY_USDC_NETWORK || ""}
|
||||
className="h-10 rounded-md border border-zinc-700 bg-black px-3 text-sm text-white outline-none focus:border-emerald-400"
|
||||
placeholder="Base / Ethereum / Polygon"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label className="grid gap-2 text-xs font-medium text-zinc-300">
|
||||
Note
|
||||
<input
|
||||
name="note"
|
||||
className="h-10 rounded-md border border-zinc-700 bg-black px-3 text-sm text-white outline-none focus:border-emerald-400"
|
||||
placeholder="可填交易時間、付款人名稱或補充說明"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex h-10 items-center justify-center rounded-md bg-emerald-400 px-4 text-sm font-semibold text-zinc-950 hover:bg-emerald-300"
|
||||
>
|
||||
送出 receipt 等待確認
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
167
apps/web/src/app/propose/wallet/actions.ts
Normal file
167
apps/web/src/app/propose/wallet/actions.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { logA2aTrafficEvent } from "@/lib/a2a-traffic";
|
||||
import { sanitizeAgentId } from "@/lib/a2a-growth";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
function getString(formData: FormData, key: string) {
|
||||
const value = formData.get(key);
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function normalizePaymentReference(value: string) {
|
||||
return value.trim().replace(/\s+/g, "").slice(0, 180);
|
||||
}
|
||||
|
||||
function cleanText(value: string, maxLength: number) {
|
||||
return value.replace(/\r/g, "").replace(/[^\S\n]+/g, " ").trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
function positiveInteger(value: unknown) {
|
||||
const parsed = typeof value === "number" ? value : Number.parseInt(String(value || ""), 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return 0;
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
|
||||
function parseAmountCents(value: string) {
|
||||
const normalized = value.replace(/[,$]/g, "").trim();
|
||||
if (!normalized) return 0;
|
||||
const amount = Number.parseFloat(normalized);
|
||||
if (!Number.isFinite(amount) || amount <= 0) return 0;
|
||||
return Math.round(amount * 100);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function buildWalletSuccessUrl(taskId: string, packageId: string, receipt: string) {
|
||||
const params = new URLSearchParams({
|
||||
task_id: taskId,
|
||||
payment: "wallet",
|
||||
receipt,
|
||||
});
|
||||
if (packageId) params.set("package", packageId);
|
||||
return `/propose/success?${params.toString()}`;
|
||||
}
|
||||
|
||||
export async function submitWalletReceipt(formData: FormData) {
|
||||
const requestHeaders = await headers();
|
||||
const taskId = getString(formData, "taskId");
|
||||
const paymentReference = normalizePaymentReference(getString(formData, "paymentReference"));
|
||||
const payerWallet = cleanText(getString(formData, "payerWallet"), 180);
|
||||
const network = cleanText(getString(formData, "network"), 80);
|
||||
const amountCents = parseAmountCents(getString(formData, "amountUsdc"));
|
||||
const note = cleanText(getString(formData, "note"), 500);
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error("缺少 Proposal ID。");
|
||||
}
|
||||
|
||||
if (!paymentReference || paymentReference.length < 8) {
|
||||
throw new Error("請提供有效的 tx hash 或付款 reference。");
|
||||
}
|
||||
|
||||
const task = await prisma.task.findUnique({
|
||||
where: { id: taskId },
|
||||
select: {
|
||||
id: true,
|
||||
referred_by_agent: true,
|
||||
scout_id: true,
|
||||
stripe_payment_intent_id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new Error("找不到 proposal draft。");
|
||||
}
|
||||
|
||||
const intakeEvent = await prisma.auditEvent.findFirst({
|
||||
where: {
|
||||
action: "DEMAND_PROPOSAL_INTAKE_CREATED",
|
||||
entityType: "TASK",
|
||||
entityId: task.id,
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: { metadata: true },
|
||||
});
|
||||
|
||||
const metadata = asRecord(intakeEvent?.metadata);
|
||||
const packageId = typeof metadata.package_id === "string" ? metadata.package_id : "";
|
||||
const proposalFeeCents = positiveInteger(metadata.proposal_fee_cents);
|
||||
const referralAgent = sanitizeAgentId(
|
||||
task.referred_by_agent || task.scout_id || (typeof metadata.referral_agent === "string" ? metadata.referral_agent : "")
|
||||
);
|
||||
const source = typeof metadata.source === "string" ? metadata.source : "wallet-receipt";
|
||||
const campaign = typeof metadata.campaign === "string" ? metadata.campaign : null;
|
||||
const normalizedAmountCents = proposalFeeCents || amountCents;
|
||||
const idempotencyKey = `proposal-wallet-receipt:${paymentReference}`;
|
||||
const capturedReference = `wallet:${paymentReference}`;
|
||||
const existingCapture = await prisma.ledgerEntry.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ idempotency_key: `proposal-wallet-fee:${paymentReference}` },
|
||||
{ stripe_object_id: capturedReference },
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
await prisma.auditEvent.create({
|
||||
data: {
|
||||
actorType: "USER",
|
||||
actorId: "wallet-proposer",
|
||||
action: "DEMAND_PROPOSAL_WALLET_RECEIPT_SUBMITTED",
|
||||
entityType: "TASK",
|
||||
entityId: task.id,
|
||||
metadata: {
|
||||
payment_provider: "wallet",
|
||||
payment_reference: capturedReference,
|
||||
raw_payment_reference: paymentReference,
|
||||
payer_wallet: payerWallet || null,
|
||||
network: network || null,
|
||||
amount_cents: normalizedAmountCents || null,
|
||||
expected_fee_cents: proposalFeeCents || null,
|
||||
package_id: packageId || null,
|
||||
referral_agent: referralAgent || null,
|
||||
source,
|
||||
campaign,
|
||||
note: note || null,
|
||||
idempotency_key: idempotencyKey,
|
||||
already_verified: Boolean(existingCapture),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (referralAgent) {
|
||||
await logA2aTrafficEvent({
|
||||
headers: requestHeaders,
|
||||
fallbackAgentId: referralAgent,
|
||||
action: "EXTERNAL_DEMAND_PROPOSAL_WALLET_RECEIPT_SUBMITTED",
|
||||
surface: "propose/wallet-receipt",
|
||||
entityType: "TASK",
|
||||
entityId: task.id,
|
||||
reason: "wallet_receipt_submitted_by_referred_demand_proposer",
|
||||
metadata: {
|
||||
payment_provider: "wallet",
|
||||
payment_reference: capturedReference,
|
||||
amount_cents: normalizedAmountCents || null,
|
||||
expected_fee_cents: proposalFeeCents || null,
|
||||
package_id: packageId || null,
|
||||
referral_agent: referralAgent,
|
||||
source,
|
||||
campaign,
|
||||
response_status: 202,
|
||||
response_summary: existingCapture
|
||||
? "wallet_receipt_already_verified"
|
||||
: "wallet_receipt_pending_manual_verification",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
redirect(buildWalletSuccessUrl(task.id, packageId, existingCapture ? "already_verified" : "submitted"));
|
||||
}
|
||||
@@ -26,6 +26,7 @@ const EVENT_LABELS: Record<string, string> = {
|
||||
EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED: "外部導流需求方建立提案",
|
||||
EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED: "外部導流需求方開始線上結帳",
|
||||
EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING: "外部導流需求方取得錢包付款指示",
|
||||
EXTERNAL_DEMAND_PROPOSAL_WALLET_RECEIPT_SUBMITTED: "外部導流需求方提交錢包 receipt",
|
||||
EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED: "外部導流提案費付款成功",
|
||||
EXTERNAL_LIST_OPEN_TASKS_SURGE: "外部公開流量突增告警",
|
||||
EXTERNAL_LIST_OPEN_TASKS_MCP: "外部整合入口查看可接需求",
|
||||
@@ -347,6 +348,7 @@ async function getTrafficSummary(minutes: number) {
|
||||
const proposalCreatedEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED"] || 0;
|
||||
const proposalCheckoutEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED"] || 0;
|
||||
const proposalWalletPendingEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING"] || 0;
|
||||
const proposalWalletReceiptEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_WALLET_RECEIPT_SUBMITTED"] || 0;
|
||||
const proposalPaidEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED"] || 0;
|
||||
const claimEvents = realExternalActionSummary["EXTERNAL_CLAIM_TASK_SUCCESS"] || 0;
|
||||
const submitEvents = realExternalActionSummary["EXTERNAL_SUBMIT_SOLUTION_SUCCESS"] || 0;
|
||||
@@ -366,6 +368,7 @@ async function getTrafficSummary(minutes: number) {
|
||||
proposal_created_events: proposalCreatedEvents,
|
||||
proposal_checkout_events: proposalCheckoutEvents,
|
||||
proposal_wallet_pending_events: proposalWalletPendingEvents,
|
||||
proposal_wallet_receipt_events: proposalWalletReceiptEvents,
|
||||
proposal_paid_events: proposalPaidEvents,
|
||||
claim_events: claimEvents,
|
||||
submit_events: submitEvents,
|
||||
@@ -828,6 +831,7 @@ export default async function TrafficDashboard({
|
||||
<div className="mt-4 text-sm text-gray-300 space-y-1">
|
||||
<div className="flex justify-between"><span>線上結帳開始</span><span>{conversionSummary.proposal_checkout_events}</span></div>
|
||||
<div className="flex justify-between"><span>錢包付款待確認</span><span>{conversionSummary.proposal_wallet_pending_events}</span></div>
|
||||
<div className="flex justify-between"><span>錢包 receipt 已提交</span><span>{conversionSummary.proposal_wallet_receipt_events}</span></div>
|
||||
<div className="flex justify-between"><span>通過任務</span><span>{conversionSummary.judge_pass_events}</span></div>
|
||||
<div className="flex justify-between"><span>未通過任務</span><span>{conversionSummary.judge_fail_events}</span></div>
|
||||
<div className="flex justify-between"><span>已出金</span><span>{conversionSummary.payout_captured}</span></div>
|
||||
|
||||
@@ -236,6 +236,7 @@ export function buildAgentDemandCampaignKit(params: {
|
||||
"EXTERNAL_DEMAND_PROPOSAL_VIEW",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_WALLET_RECEIPT_SUBMITTED",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED",
|
||||
],
|
||||
payout_boundaries: {
|
||||
|
||||
@@ -49,6 +49,8 @@ export async function getTreasurySnapshot() {
|
||||
completedTasks,
|
||||
arbitrationCount,
|
||||
proposalFeeEvents,
|
||||
walletReceiptEvents,
|
||||
pendingAffiliateRows,
|
||||
withdrawalEvents,
|
||||
recentAuditEvents,
|
||||
] = await Promise.all([
|
||||
@@ -65,6 +67,40 @@ export async function getTreasurySnapshot() {
|
||||
select: { id: true, entityId: true, metadata: true, createdAt: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
}),
|
||||
prisma.auditEvent.findMany({
|
||||
where: { action: "DEMAND_PROPOSAL_WALLET_RECEIPT_SUBMITTED" },
|
||||
select: { id: true, entityId: true, metadata: true, createdAt: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 50,
|
||||
}),
|
||||
prisma.affiliateLedger.findMany({
|
||||
where: { status: "PENDING" },
|
||||
orderBy: { created_at: "desc" },
|
||||
take: 25,
|
||||
select: {
|
||||
id: true,
|
||||
scout_id: true,
|
||||
task_id: true,
|
||||
amount: true,
|
||||
currency: true,
|
||||
status: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
scout_agent: {
|
||||
select: {
|
||||
wallet_address: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
task: {
|
||||
select: {
|
||||
title: true,
|
||||
status: true,
|
||||
stripe_payment_intent_id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.auditEvent.findMany({
|
||||
where: { action: "TREASURY_WITHDRAWAL" },
|
||||
select: { metadata: true },
|
||||
@@ -90,6 +126,11 @@ export async function getTreasurySnapshot() {
|
||||
const proposalFeeRevenue = emptyBucket();
|
||||
const arbitrationRevenue = emptyBucket();
|
||||
const withdrawn = emptyBucket();
|
||||
const capturedWalletReferences = new Set(
|
||||
proposalFeeEvents
|
||||
.map((event) => stringValue(asRecord(event.metadata).payment_reference))
|
||||
.filter((reference) => reference.startsWith("wallet:"))
|
||||
);
|
||||
|
||||
for (const task of allTasks) {
|
||||
addToBucket(gmv, task.reward_currency === "USDC" ? "usdc" : "fiat", task.reward_amount);
|
||||
@@ -144,6 +185,51 @@ export async function getTreasurySnapshot() {
|
||||
date: event.createdAt.toISOString(),
|
||||
};
|
||||
});
|
||||
const seenPendingWalletReceipts = new Set<string>();
|
||||
const pendingWalletReceipts = walletReceiptEvents
|
||||
.map((event) => {
|
||||
const metadata = asRecord(event.metadata);
|
||||
const paymentReference = stringValue(metadata.payment_reference);
|
||||
const rawPaymentReference = stringValue(metadata.raw_payment_reference) || paymentReference.replace(/^wallet:/, "");
|
||||
return {
|
||||
id: event.id,
|
||||
task_id: event.entityId,
|
||||
payment_reference: paymentReference,
|
||||
raw_payment_reference: rawPaymentReference,
|
||||
payer_wallet: stringValue(metadata.payer_wallet) || null,
|
||||
network: stringValue(metadata.network) || null,
|
||||
amount_cents: numberValue(metadata.amount_cents) || numberValue(metadata.expected_fee_cents),
|
||||
package_id: stringValue(metadata.package_id) || null,
|
||||
referral_agent: stringValue(metadata.referral_agent) || null,
|
||||
note: stringValue(metadata.note) || null,
|
||||
already_verified: capturedWalletReferences.has(paymentReference) || metadata.already_verified === true,
|
||||
submitted_at: event.createdAt.toISOString(),
|
||||
};
|
||||
})
|
||||
.filter((receipt) => {
|
||||
if (receipt.already_verified) return false;
|
||||
const receiptKey = receipt.payment_reference || `${receipt.task_id}:${receipt.raw_payment_reference}` || receipt.id;
|
||||
if (seenPendingWalletReceipts.has(receiptKey)) return false;
|
||||
seenPendingWalletReceipts.add(receiptKey);
|
||||
return true;
|
||||
})
|
||||
.slice(0, 20);
|
||||
|
||||
const pendingAffiliatePayouts = pendingAffiliateRows.map((row) => ({
|
||||
id: row.id,
|
||||
scout_id: row.scout_id,
|
||||
scout_wallet: row.scout_agent?.wallet_address || null,
|
||||
scout_status: row.scout_agent?.status || "UNKNOWN",
|
||||
task_id: row.task_id,
|
||||
task_title: row.task?.title || row.task_id,
|
||||
proposal_status: row.task?.status || "UNKNOWN",
|
||||
payment_reference: row.task?.stripe_payment_intent_id || null,
|
||||
amount_cents: row.amount,
|
||||
currency: row.currency,
|
||||
status: row.status,
|
||||
created_at: row.created_at.toISOString(),
|
||||
updated_at: row.updated_at.toISOString(),
|
||||
}));
|
||||
|
||||
return {
|
||||
gmv,
|
||||
@@ -154,5 +240,7 @@ export async function getTreasurySnapshot() {
|
||||
task_platform_fee_revenue: taskPlatformFees,
|
||||
arbitration_fee_revenue: arbitrationRevenue,
|
||||
recent_transactions: recentTransactions,
|
||||
pending_wallet_receipts: pendingWalletReceipts,
|
||||
pending_affiliate_payouts: pendingAffiliatePayouts,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user