@seasonkoh/webaz 0.1.29 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bond-refund-blockers.js +50 -0
- package/dist/bond-slash.js +100 -0
- package/dist/bond-terms.js +31 -0
- package/dist/direct-pay-bond-rail-clearance.js +10 -5
- package/dist/direct-pay-cancel-refund.js +160 -0
- package/dist/direct-pay-create.js +78 -9
- package/dist/direct-pay-disclosures.js +17 -11
- package/dist/direct-pay-fee-ar.js +2 -2
- package/dist/direct-pay-fee-prepay-request.js +80 -0
- package/dist/direct-pay-returns.js +104 -0
- package/dist/direct-pay-stock.js +9 -0
- package/dist/direct-receive-deferral.js +30 -1
- package/dist/direct-receive-deposits.js +122 -17
- package/dist/free-shipping.js +37 -0
- package/dist/fx-rates.js +7 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +246 -0
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +73 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +150 -33
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
- package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
- package/dist/platform-receive-accounts.js +94 -0
- package/dist/pwa/arbitration-read-admin.js +37 -0
- package/dist/pwa/arbitrator-lifecycle.js +100 -0
- package/dist/pwa/contract-fingerprint.js +15 -0
- package/dist/pwa/direct-pay-order-redaction.js +20 -2
- package/dist/pwa/human-presence.js +2 -6
- package/dist/pwa/public/app-account.js +9 -9
- package/dist/pwa/public/app-admin-disputes.js +55 -0
- package/dist/pwa/public/app-agent-appeal.js +90 -0
- package/dist/pwa/public/app-agent-approvals.js +93 -0
- package/dist/pwa/public/app-agent-pair.js +127 -0
- package/dist/pwa/public/app-arbitrator-admin.js +87 -0
- package/dist/pwa/public/app-arbitrator-entry.js +9 -0
- package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
- package/dist/pwa/public/app-bond-refund-ui.js +66 -0
- package/dist/pwa/public/app-bond-slash-ui.js +74 -0
- package/dist/pwa/public/app-bond-terms-ui.js +23 -0
- package/dist/pwa/public/app-bond-ui.js +108 -0
- package/dist/pwa/public/app-chat-poll.js +6 -6
- package/dist/pwa/public/app-contribution-hub.js +23 -0
- package/dist/pwa/public/app-direct-pay-accounts.js +1 -1
- package/dist/pwa/public/app-direct-pay-buyer.js +1 -1
- package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
- package/dist/pwa/public/app-direct-pay-copy.js +12 -0
- package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +1 -1
- package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
- package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
- package/dist/pwa/public/app-direct-pay-memo.js +14 -0
- package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
- package/dist/pwa/public/app-direct-pay-pay.js +15 -0
- package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
- package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
- package/dist/pwa/public/app-direct-pay-returns.js +56 -0
- package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
- package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
- package/dist/pwa/public/app-direct-pay.js +36 -37
- package/dist/pwa/public/app-dispute-close-ui.js +38 -0
- package/dist/pwa/public/app-free-shipping-ui.js +29 -0
- package/dist/pwa/public/app-gmv-rail-split.js +12 -0
- package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
- package/dist/pwa/public/app-mutual-cancel.js +54 -0
- package/dist/pwa/public/app-notif-templates-orders.js +43 -0
- package/dist/pwa/public/app-notif-templates.js +22 -0
- package/dist/pwa/public/app-order-accept-ui.js +158 -0
- package/dist/pwa/public/app-order-errors.js +50 -0
- package/dist/pwa/public/app-order-labels.js +19 -0
- package/dist/pwa/public/app-order-rail-filter.js +26 -0
- package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
- package/dist/pwa/public/app-poll-governor.js +22 -0
- package/dist/pwa/public/app-profile.js +4 -4
- package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
- package/dist/pwa/public/app-sale-regions-ui.js +38 -0
- package/dist/pwa/public/app-seller.js +1 -1
- package/dist/pwa/public/app-trade-tax-ui.js +33 -0
- package/dist/pwa/public/app.js +133 -160
- package/dist/pwa/public/i18n.js +675 -5
- package/dist/pwa/public/index.html +41 -0
- package/dist/pwa/public/openapi.json +719 -11
- package/dist/pwa/public/style.css +3 -0
- package/dist/pwa/routes/admin-direct-receive-deposits.js +215 -3
- package/dist/pwa/routes/admin-protocol-params.js +16 -0
- package/dist/pwa/routes/admin-reports.js +31 -5
- package/dist/pwa/routes/agent-governance.js +1 -1
- package/dist/pwa/routes/agent-grants.js +281 -32
- package/dist/pwa/routes/analytics.js +2 -0
- package/dist/pwa/routes/arbitrator.js +67 -14
- package/dist/pwa/routes/bond-seller.js +162 -0
- package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +9 -4
- package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
- package/dist/pwa/routes/direct-pay-returns.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +186 -9
- package/dist/pwa/routes/disputes-read.js +20 -6
- package/dist/pwa/routes/disputes-write.js +91 -33
- package/dist/pwa/routes/external-anchors.js +4 -2
- package/dist/pwa/routes/fee-prepay-requests.js +66 -0
- package/dist/pwa/routes/governance-onboarding.js +46 -8
- package/dist/pwa/routes/logistics.js +4 -4
- package/dist/pwa/routes/me-data.js +2 -2
- package/dist/pwa/routes/mutual-cancel.js +62 -0
- package/dist/pwa/routes/orders-action.js +182 -9
- package/dist/pwa/routes/orders-create.js +18 -7
- package/dist/pwa/routes/orders-read.js +76 -14
- package/dist/pwa/routes/platform-receive-accounts.js +111 -0
- package/dist/pwa/routes/products-create.js +45 -16
- package/dist/pwa/routes/products-update.js +15 -1
- package/dist/pwa/routes/profile-identity.js +4 -2
- package/dist/pwa/routes/returns.js +39 -8
- package/dist/pwa/routes/seller-directpay-report.js +110 -0
- package/dist/pwa/routes/seller-quota.js +5 -1
- package/dist/pwa/routes/shipping-templates.js +219 -0
- package/dist/pwa/routes/snf.js +4 -1
- package/dist/pwa/routes/webauthn.js +3 -3
- package/dist/pwa/server.js +63 -40
- package/dist/runtime/agent-grant-scopes.js +69 -1
- package/dist/runtime/webaz-schema-helpers.js +57 -3
- package/dist/sale-regions.js +116 -0
- package/dist/shipping-templates.js +119 -0
- package/dist/trade-tax.js +99 -0
- package/dist/trade-terms.js +76 -0
- package/dist/version.js +1 -1
- package/package.json +61 -2
|
@@ -516,6 +516,9 @@ code, kbd, .mono { word-break: break-all; overflow-wrap: anywhere }
|
|
|
516
516
|
animation: slide-up 0.3s ease-out;
|
|
517
517
|
}
|
|
518
518
|
@keyframes slide-up { from { opacity: 0; transform: translate(-50%, 20px) } to { opacity: 1; transform: translate(-50%, 0) } }
|
|
519
|
+
/* 走查批次2:浮条悬在内容上方会盖住页面底部操作(如争议页上传控件)— 浮条在场时给内容区让位。
|
|
520
|
+
数值构成:banner bottom 70px + banner 自高 ~64px + 余量 16px = 150px(.main 默认 padding-bottom 80px 不够,曾误设 96px 实际只多让 16px) */
|
|
521
|
+
body:has(#install-banner) .main { padding-bottom: 150px }
|
|
519
522
|
.install-banner-icon { font-size: 22px; line-height: 1 }
|
|
520
523
|
.install-banner-body { flex: 1; min-width: 0 }
|
|
521
524
|
.install-banner-title { font-size: 13px; font-weight: 600; line-height: 1.2 }
|
|
@@ -1,17 +1,176 @@
|
|
|
1
|
-
import { confirmProductionReceipt } from '../../direct-receive-deposits.js';
|
|
1
|
+
import { confirmProductionReceipt, rejectDeposit, executeBondRefund } from '../../direct-receive-deposits.js';
|
|
2
|
+
import { enumerateBondRefundBlockers } from '../../bond-refund-blockers.js'; // B2:执行前复核(冷静期内可能新增退货等责任)
|
|
3
|
+
import { proposeBondSlash, cancelBondSlashProposal, executeBondSlashProposal, listBondSlashProposals } from '../../bond-slash.js'; // B3:罚没提案/冷静期/执行(人工铁律)
|
|
4
|
+
import { dbAll, dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // B1:保证金申报队列/通知收件人只读(async seam)
|
|
2
5
|
import { reviewAmlFlag } from '../../direct-pay-aml-review.js';
|
|
3
6
|
import { recordKybReview, recordSanctionsScreening, recordAmlFlagIngress, amlDetailHash } from '../../direct-pay-compliance-ingress.js';
|
|
4
7
|
import { readDirectPayLaunchReadiness } from '../../direct-pay-launch-readiness.js';
|
|
5
|
-
import { approveDeferral, rejectDeferral, listDeferrals } from '../../direct-receive-deferral.js';
|
|
8
|
+
import { approveDeferral, rejectDeferral, listDeferrals, satisfyDeferralOnBond } from '../../direct-receive-deferral.js';
|
|
6
9
|
import { listProductVerifications, reviewProductVerification } from '../../product-verification.js';
|
|
7
10
|
import { listStoreVerifications, reviewStoreVerification } from '../../store-verification.js';
|
|
8
11
|
import { requireDirectPayHumanPasskey } from '../direct-pay-guards.js';
|
|
9
12
|
import { recordFeePrepayTopup, recordFeePrepayAdjustment, recordFeePrepayRefund, getDirectPayFeeAccount } from '../../direct-pay-fee-ar.js';
|
|
13
|
+
import { listAllRequests, getRequest as getFeePrepayRequest, approveFeePrepayRequest, rejectFeePrepayRequest } from '../../direct-pay-fee-prepay-request.js';
|
|
14
|
+
// N3:审批结果通知卖家(approve 入账/reject 驳回;此前审批后卖家无任何未读信号)。
|
|
15
|
+
import { createNotification } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
10
16
|
export function registerAdminDirectReceiveDepositsRoutes(app, deps) {
|
|
11
17
|
const { db, requireRootAdmin, consumeGateToken, logAdminAction, getProtocolParam } = deps;
|
|
18
|
+
// GET /api/admin/direct-receive/deposits?status=pending — ROOT 只读:保证金申报队列(核对到账用)。B1。
|
|
19
|
+
app.get('/api/admin/direct-receive/deposits', async (req, res) => {
|
|
20
|
+
const admin = requireRootAdmin(req, res);
|
|
21
|
+
if (!admin)
|
|
22
|
+
return;
|
|
23
|
+
const status = req.query?.status != null ? String(req.query.status) : '';
|
|
24
|
+
const rows = await dbAll(`
|
|
25
|
+
SELECT d.id, d.user_id, d.tier, d.required_amount, d.amount, d.currency, d.deposit_rail, d.status,
|
|
26
|
+
d.external_ref, d.reject_note, d.production_receipt_confirmed_at, d.created_at, d.locked_at,
|
|
27
|
+
u.name AS seller_name, u.handle AS seller_handle
|
|
28
|
+
FROM direct_receive_deposits d JOIN users u ON u.id = d.user_id
|
|
29
|
+
${status ? 'WHERE d.status = ?' : ''} ORDER BY d.created_at DESC LIMIT 200`, status ? [status] : []);
|
|
30
|
+
return void res.json({ deposits: rows });
|
|
31
|
+
});
|
|
32
|
+
// POST /api/admin/direct-receive/deposits/:id/reject — ROOT 驳回申报(未 lock 的申报;不动钱,留说明通知卖家)。B1。
|
|
33
|
+
// 不 Passkey:驳回不授予/不移动任何东西(与缓交 reject 不同 —— 那是资格决定;这里只是"到账核不上"退回重报)。
|
|
34
|
+
app.post('/api/admin/direct-receive/deposits/:id/reject', async (req, res) => {
|
|
35
|
+
const admin = requireRootAdmin(req, res);
|
|
36
|
+
if (!admin)
|
|
37
|
+
return;
|
|
38
|
+
const note = req.body?.note != null ? String(req.body.note).slice(0, 300) : '';
|
|
39
|
+
const r = rejectDeposit(db, { depositId: req.params.id, note });
|
|
40
|
+
logAdminAction(admin.id, 'direct_receive.bond_deposit_reject', 'direct_receive_deposit', req.params.id, { ok: r.ok, note_present: !!note, outcome: r.ok ? r.status : r.reason });
|
|
41
|
+
if (!r.ok)
|
|
42
|
+
return void res.status(409).json({ error: r.reason, error_code: 'BOND_REJECT_FAILED' });
|
|
43
|
+
try {
|
|
44
|
+
const dep = await dbOne('SELECT user_id FROM direct_receive_deposits WHERE id = ?', [req.params.id]);
|
|
45
|
+
if (dep)
|
|
46
|
+
createNotification(db, dep.user_id, null, 'bond_deposit_rejected', '❌ 保证金申报未通过核实', `你的保证金缴纳申报未通过核实${note ? `(${note})` : ''}。请核对付款凭据后重新提交,或联系平台。`, { templateKey: 'bond_deposit_rejected', params: { note: note ? `(${note})` : '' } });
|
|
47
|
+
}
|
|
48
|
+
catch { /* 不阻断 */ }
|
|
49
|
+
return void res.json({ ok: true, status: r.status });
|
|
50
|
+
});
|
|
51
|
+
// ── B3:保证金罚没(人工铁律:仲裁裁定卖家责的直付争议 → 提案 → 冷静期 → ROOT+Passkey 执行;绝不自动)──
|
|
52
|
+
app.get('/api/admin/direct-receive/bond-slash', (req, res) => {
|
|
53
|
+
const admin = requireRootAdmin(req, res);
|
|
54
|
+
if (!admin)
|
|
55
|
+
return;
|
|
56
|
+
const status = req.query?.status != null ? String(req.query.status) : '';
|
|
57
|
+
return void res.json({ proposals: listBondSlashProposals(db, status ? { status } : {}) });
|
|
58
|
+
});
|
|
59
|
+
// 提案(ROOT,审计留痕,不 Passkey —— 提案不动任何东西;执行才是终局动作)。通知卖家(冷静期=申诉窗)。
|
|
60
|
+
app.post('/api/admin/direct-receive/bond-slash/propose', async (req, res) => {
|
|
61
|
+
const admin = requireRootAdmin(req, res);
|
|
62
|
+
if (!admin)
|
|
63
|
+
return;
|
|
64
|
+
const depositId = String(req.body?.deposit_id || '');
|
|
65
|
+
const disputeId = String(req.body?.dispute_id || '');
|
|
66
|
+
if (!depositId || !disputeId)
|
|
67
|
+
return void res.status(400).json({ error: '须提供 deposit_id 与依据 dispute_id', error_code: 'MISSING_BASIS' });
|
|
68
|
+
const coolingDays = Math.max(0, Number(getProtocolParam('direct_pay.bond_slash_cooling_days', 7)) || 7);
|
|
69
|
+
const proposalId = 'bslash_' + Math.random().toString(36).slice(2, 10);
|
|
70
|
+
const r = proposeBondSlash(db, { proposalId, depositId, disputeId, reason: req.body?.reason != null ? String(req.body.reason) : null, proposedBy: admin.id, coolingDays });
|
|
71
|
+
logAdminAction(admin.id, 'direct_receive.bond_slash_propose', 'direct_receive_deposit', depositId, { ok: r.ok, dispute_id: disputeId, outcome: r.ok ? proposalId : r.reason });
|
|
72
|
+
if (!r.ok)
|
|
73
|
+
return void res.status(409).json({ error: r.reason, error_code: 'SLASH_PROPOSE_REJECTED' });
|
|
74
|
+
try {
|
|
75
|
+
const dep = await dbOne('SELECT user_id FROM direct_receive_deposits WHERE id = ?', [depositId]);
|
|
76
|
+
if (dep)
|
|
77
|
+
createNotification(db, dep.user_id, null, 'bond_slash_proposed', '⚠️ 保证金罚没提案(待复核)', `因争议 ${disputeId} 裁定卖家责任,平台已发起保证金罚没提案。冷静期 ${coolingDays} 天内如有异议请联系平台并提供依据;冷静期满后将复核执行(全额罚没,进入处罚金专户,平台不获益)。`, { templateKey: 'bond_slash_proposed', params: { dispute: disputeId, days: coolingDays } });
|
|
78
|
+
}
|
|
79
|
+
catch { /* 不阻断 */ }
|
|
80
|
+
return void res.json({ ok: true, proposal_id: proposalId, cooling_days: coolingDays });
|
|
81
|
+
});
|
|
82
|
+
app.post('/api/admin/direct-receive/bond-slash/:id/cancel', async (req, res) => {
|
|
83
|
+
const admin = requireRootAdmin(req, res);
|
|
84
|
+
if (!admin)
|
|
85
|
+
return;
|
|
86
|
+
const r = cancelBondSlashProposal(db, { proposalId: req.params.id, note: req.body?.note != null ? String(req.body.note) : null });
|
|
87
|
+
logAdminAction(admin.id, 'direct_receive.bond_slash_cancel', 'bond_slash_proposal', req.params.id, { ok: r.ok, outcome: r.ok ? 'cancelled' : r.reason });
|
|
88
|
+
if (!r.ok)
|
|
89
|
+
return void res.status(409).json({ error: r.reason, error_code: 'SLASH_CANCEL_REJECTED' });
|
|
90
|
+
try {
|
|
91
|
+
const p = await dbOne('SELECT seller_id FROM bond_slash_proposals WHERE id = ?', [req.params.id]);
|
|
92
|
+
if (p)
|
|
93
|
+
createNotification(db, p.seller_id, null, 'bond_slash_cancelled', '✅ 保证金罚没提案已撤销', '此前的罚没提案经复核已撤销,你的保证金不受影响。', { templateKey: 'bond_slash_cancelled', params: {} });
|
|
94
|
+
}
|
|
95
|
+
catch { /* 不阻断 */ }
|
|
96
|
+
return void res.json({ ok: true, status: r.status, already: !!r.already });
|
|
97
|
+
});
|
|
98
|
+
// 执行(ROOT + 真人 Passkey,purpose direct_pay_bond_slash 绑 proposal_id;冷静期由域内绝对截止校验)。
|
|
99
|
+
app.post('/api/admin/direct-receive/bond-slash/:id/execute', async (req, res) => {
|
|
100
|
+
const admin = requireRootAdmin(req, res);
|
|
101
|
+
if (!admin)
|
|
102
|
+
return;
|
|
103
|
+
const proposalId = req.params.id;
|
|
104
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
105
|
+
userId: admin.id, webauthnToken: req.body?.webauthn_token, purpose: 'direct_pay_bond_slash',
|
|
106
|
+
validate: (data) => { const d = data; return !!d && d.proposal_id === proposalId; },
|
|
107
|
+
});
|
|
108
|
+
if (!gate.ok) {
|
|
109
|
+
logAdminAction(admin.id, 'direct_receive.bond_slash_execute', 'bond_slash_proposal', proposalId, { ok: false, error_code: gate.error_code });
|
|
110
|
+
return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
|
|
111
|
+
}
|
|
112
|
+
const txnId = 'bslashtxn_' + Math.random().toString(36).slice(2, 10);
|
|
113
|
+
let r;
|
|
114
|
+
try {
|
|
115
|
+
r = executeBondSlashProposal(db, { proposalId, txnId, nowIso: new Date().toISOString() });
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
r = { ok: false, reason: e.message };
|
|
119
|
+
}
|
|
120
|
+
logAdminAction(admin.id, 'direct_receive.bond_slash_execute', 'bond_slash_proposal', proposalId, { ok: r.ok, txn_id: txnId, outcome: r.ok ? (r.already ? 'already' : 'executed') : r.reason });
|
|
121
|
+
if (!r.ok)
|
|
122
|
+
return void res.status(409).json({ error: r.reason, error_code: 'SLASH_EXECUTE_REJECTED' });
|
|
123
|
+
if (!r.already) {
|
|
124
|
+
try {
|
|
125
|
+
const p = await dbOne('SELECT seller_id, dispute_id FROM bond_slash_proposals WHERE id = ?', [proposalId]);
|
|
126
|
+
if (p)
|
|
127
|
+
createNotification(db, p.seller_id, null, 'bond_slash_executed', '❌ 保证金已罚没', `依据争议 ${p.dispute_id} 的卖家责任裁定,你的履约保证金已全额罚没(进入处罚金专户,平台不获益),直付资格已吊销。重新缴纳保证金并通过审核后可再次申请开通。`, { templateKey: 'bond_slash_executed', params: { dispute: p.dispute_id } });
|
|
128
|
+
}
|
|
129
|
+
catch { /* 不阻断 */ }
|
|
130
|
+
}
|
|
131
|
+
return void res.json({ ok: true, status: r.status, already: !!r.already });
|
|
132
|
+
});
|
|
133
|
+
// POST /api/admin/direct-receive/deposits/:id/execute-refund — ROOT + 真人 Passkey:记录已完成的【场外】保证金退还(B2)。
|
|
134
|
+
// 前置:refunding + 冷静期满(param direct_pay.bond_refund_cooling_days,默认 14d,域内校验)+ 【执行时复核】
|
|
135
|
+
// unlock blockers(冷静期内可能新增退货/欠费等责任 —— 有任一即拒)。凭据必填;真实退款发生在协议外,此处只记录。
|
|
136
|
+
app.post('/api/admin/direct-receive/deposits/:id/execute-refund', async (req, res) => {
|
|
137
|
+
const admin = requireRootAdmin(req, res);
|
|
138
|
+
if (!admin)
|
|
139
|
+
return;
|
|
140
|
+
const depositId = req.params.id;
|
|
141
|
+
const evidenceRef = String(req.body?.evidence_ref || '').trim();
|
|
142
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
143
|
+
userId: admin.id, webauthnToken: req.body?.webauthn_token, purpose: 'direct_receive_bond_refund',
|
|
144
|
+
validate: (data) => { const d = data; return !!d && d.deposit_id === depositId && d.evidence_ref === evidenceRef; },
|
|
145
|
+
});
|
|
146
|
+
if (!gate.ok) {
|
|
147
|
+
logAdminAction(admin.id, 'direct_receive.bond_refund_execute', 'direct_receive_deposit', depositId, { ok: false, error_code: gate.error_code });
|
|
148
|
+
return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
|
|
149
|
+
}
|
|
150
|
+
const dep = await dbOne('SELECT user_id FROM direct_receive_deposits WHERE id = ?', [depositId]);
|
|
151
|
+
if (!dep)
|
|
152
|
+
return void res.status(404).json({ error: '存款不存在', error_code: 'DEPOSIT_NOT_FOUND' });
|
|
153
|
+
const blockers = enumerateBondRefundBlockers(db, dep.user_id);
|
|
154
|
+
if (blockers.length > 0) {
|
|
155
|
+
logAdminAction(admin.id, 'direct_receive.bond_refund_execute', 'direct_receive_deposit', depositId, { ok: false, outcome: 'blocked', blockers: blockers.map(b => b.code) });
|
|
156
|
+
return void res.status(409).json({ error: '卖家仍有未了结直付责任,不可退还', error_code: 'REFUND_BLOCKED', blockers });
|
|
157
|
+
}
|
|
158
|
+
const coolingDays = Math.max(0, Number(getProtocolParam('direct_pay.bond_refund_cooling_days', 14)) || 14);
|
|
159
|
+
const r = executeBondRefund(db, { depositId, nowIso: new Date().toISOString(), coolingDays, evidenceRef });
|
|
160
|
+
logAdminAction(admin.id, 'direct_receive.bond_refund_execute', 'direct_receive_deposit', depositId, { ok: r.ok, evidence_present: !!evidenceRef, outcome: r.ok ? (r.already ? 'already' : 'refunded') : r.reason });
|
|
161
|
+
if (!r.ok)
|
|
162
|
+
return void res.status(409).json({ error: r.reason, error_code: 'REFUND_EXECUTE_REJECTED' });
|
|
163
|
+
if (!r.already) {
|
|
164
|
+
try {
|
|
165
|
+
createNotification(db, dep.user_id, null, 'bond_refund_executed', '✅ 履约保证金已退还', `你的保证金已在协议外退还并记录(凭据:${evidenceRef.slice(0, 60)})。直付资格随保证金退出关闭;重新缴纳后可再次开通。`, { templateKey: 'bond_refund_executed', params: { evidence: evidenceRef.slice(0, 60) } });
|
|
166
|
+
}
|
|
167
|
+
catch { /* 不阻断 */ }
|
|
168
|
+
}
|
|
169
|
+
return void res.json({ ok: true, status: r.status, already: !!r.already });
|
|
170
|
+
});
|
|
12
171
|
// POST /api/admin/direct-receive/deposits/:id/confirm-production — ROOT + 真人 Passkey 手动确认生产保证金 receipt。
|
|
13
172
|
// 当前恒 fail-closed(无 legal-cleared rail → assert 抛 → PRODUCTION_RAIL_NOT_CLEARED)。
|
|
14
|
-
app.post('/api/admin/direct-receive/deposits/:id/confirm-production', (req, res) => {
|
|
173
|
+
app.post('/api/admin/direct-receive/deposits/:id/confirm-production', async (req, res) => {
|
|
15
174
|
const admin = requireRootAdmin(req, res);
|
|
16
175
|
if (!admin)
|
|
17
176
|
return;
|
|
@@ -19,6 +178,8 @@ export function registerAdminDirectReceiveDepositsRoutes(app, deps) {
|
|
|
19
178
|
const railId = String(req.body?.rail_id || '');
|
|
20
179
|
const amountUnits = Number(req.body?.expected_amount_units);
|
|
21
180
|
const receiptRef = String(req.body?.receipt_ref || '');
|
|
181
|
+
// jurisdiction=【平台收款主体法域】(P2 澄清:非卖家法域;卖家资格由 KYB/制裁/AML 独立把守)。
|
|
182
|
+
// 自由输入但被域内 DIRECT_PAY_BOND_JURISDICTIONS 严格白名单硬约束(非白名单值必拒),并进 Passkey purpose_data + 审计。
|
|
22
183
|
const jurisdiction = String(req.body?.jurisdiction || '');
|
|
23
184
|
const webauthnToken = req.body?.webauthn_token;
|
|
24
185
|
// 现场真人 Passkey(铁律)。purpose_data 绑定【完整动作字段】:deposit/rail/amount/receipt/jurisdiction 全等才放行,
|
|
@@ -43,6 +204,17 @@ export function registerAdminDirectReceiveDepositsRoutes(app, deps) {
|
|
|
43
204
|
logAdminAction(admin.id, 'direct_receive.production_confirm', 'direct_receive_deposit', depositId, { rail_id: railId, jurisdiction, ok: r.ok, outcome: r.ok ? (r.already ? 'already' : 'confirmed') : r.reason });
|
|
44
205
|
if (!r.ok)
|
|
45
206
|
return void res.status(409).json({ error: r.reason, error_code: 'PRODUCTION_CONFIRM_REJECTED' });
|
|
207
|
+
// B1:确认成功 → 通知卖家保证金已生效;B4:缓交期间缴清 → granted 缓交转 satisfied(解除额度压低)+ 告知。best-effort 不阻断。
|
|
208
|
+
if (!r.already) {
|
|
209
|
+
try {
|
|
210
|
+
const dep = await dbOne('SELECT user_id FROM direct_receive_deposits WHERE id = ?', [depositId]);
|
|
211
|
+
if (dep) {
|
|
212
|
+
const converted = satisfyDeferralOnBond(db, dep.user_id);
|
|
213
|
+
createNotification(db, dep.user_id, null, 'bond_deposit_confirmed', '✅ 履约保证金已确认锁定', `你的保证金已核实到账并正式锁定,直付入场的保证金门已满足。${converted > 0 ? '缓交资格已转正式,额度限制同步解除。' : ''}退出时可申请退还(须无未了结直付责任)。`, { templateKey: 'bond_deposit_confirmed', params: { converted } });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch { /* 通知失败不阻断 */ }
|
|
217
|
+
}
|
|
46
218
|
return void res.json({ ok: true, status: r.status, already: !!r.already });
|
|
47
219
|
}
|
|
48
220
|
catch (e) {
|
|
@@ -158,6 +330,46 @@ export function registerAdminDirectReceiveDepositsRoutes(app, deps) {
|
|
|
158
330
|
return;
|
|
159
331
|
return void res.json({ ok: true, account: getDirectPayFeeAccount(db, str(req.params.seller_id)) });
|
|
160
332
|
});
|
|
333
|
+
// GET /api/admin/direct-receive/fee-prepay-requests?status=pending — ROOT 只读:预充值申请队列(核对到账用)。
|
|
334
|
+
app.get('/api/admin/direct-receive/fee-prepay-requests', (req, res) => {
|
|
335
|
+
const admin = requireRootAdmin(req, res);
|
|
336
|
+
if (!admin)
|
|
337
|
+
return;
|
|
338
|
+
return void res.json({ ok: true, requests: listAllRequests(db, req.query.status ? str(req.query.status) : undefined) });
|
|
339
|
+
});
|
|
340
|
+
// POST /api/admin/direct-receive/fee-prepay-requests/:id/approve — ROOT + Passkey【确认真实到账 → 入账】(唯一动钱)。
|
|
341
|
+
// ⚠️ 独立 purpose 'direct_pay_fee_prepay_request_approve'(≠ 手动入账的 'direct_pay_fee_prepay_record')——
|
|
342
|
+
// 否则"批准申请"的 token 能被拿去打手动 /fee-prepay 记一笔【未关联 fpr】的预充值,原申请仍 pending → 双入账/断链。
|
|
343
|
+
// purpose_data 绑 request_id + seller_id + amount_units + method(把入账金额/对象钉进 Passkey)。原子:approved + recordFeePrepay + 关联 payment。
|
|
344
|
+
app.post('/api/admin/direct-receive/fee-prepay-requests/:id/approve', gatedIngress('direct_pay_fee_prepay_request_approve', (req) => (data) => {
|
|
345
|
+
const d = data;
|
|
346
|
+
const fr = getFeePrepayRequest(db, str(req.params.id));
|
|
347
|
+
return !!d && !!fr && d.request_id === str(req.params.id) && d.seller_id === fr.seller_id && Number(d.amount_units) === Number(fr.amount_units) && str(d.method) === str(req.body?.method);
|
|
348
|
+
}, (req, adminId) => {
|
|
349
|
+
const fr = getFeePrepayRequest(db, str(req.params.id));
|
|
350
|
+
const r = approveFeePrepayRequest(db, { requestId: str(req.params.id), adminId, method: str(req.body?.method), reviewNote: opt(req.body?.note) });
|
|
351
|
+
if (r.ok && fr) {
|
|
352
|
+
const amt = Number(fr.amount_units) / 1_000_000;
|
|
353
|
+
try {
|
|
354
|
+
createNotification(db, fr.seller_id, null, 'dp_fee_prepay_approved', '✅ 预充值已确认入账', `你的平台服务费预充值 ${amt} USDC 已确认入账,直付新单额度已恢复。`, { templateKey: 'dp_fee_prepay_approved', params: { amount: amt } });
|
|
355
|
+
}
|
|
356
|
+
catch { /* 通知失败不阻断入账 */ }
|
|
357
|
+
}
|
|
358
|
+
return r.ok ? { ok: true, id: r.paymentId } : { ok: false, error: r.error };
|
|
359
|
+
}, (req) => { const fr = getFeePrepayRequest(db, str(req.params.id)); return { targetId: fr?.seller_id ?? str(req.params.id), detail: { request_id: str(req.params.id), amount_units: fr?.amount_units, method: str(req.body?.method) } }; }));
|
|
360
|
+
// POST /api/admin/direct-receive/fee-prepay-requests/:id/reject — ROOT + Passkey(不动钱)。purpose_data 绑 request_id。
|
|
361
|
+
app.post('/api/admin/direct-receive/fee-prepay-requests/:id/reject', gatedIngress('direct_pay_fee_prepay_reject', (req) => (data) => { const d = data; return !!d && d.request_id === str(req.params.id); }, (req, adminId) => {
|
|
362
|
+
const fr = getFeePrepayRequest(db, str(req.params.id));
|
|
363
|
+
const r = rejectFeePrepayRequest(db, { requestId: str(req.params.id), adminId, reviewNote: opt(req.body?.note) });
|
|
364
|
+
if (r.ok && fr) {
|
|
365
|
+
const note = opt(req.body?.note) ? `(${opt(req.body?.note)})` : '';
|
|
366
|
+
try {
|
|
367
|
+
createNotification(db, fr.seller_id, null, 'dp_fee_prepay_rejected', '❌ 预充值申请未通过', `你的平台服务费预充值申请未通过${note}。请核对付款凭据后重新提交,或联系平台。`, { templateKey: 'dp_fee_prepay_rejected', params: { note } });
|
|
368
|
+
}
|
|
369
|
+
catch { /* 通知失败不阻断 */ }
|
|
370
|
+
}
|
|
371
|
+
return r;
|
|
372
|
+
}, (req) => ({ targetId: str(req.params.id), detail: { request_id: str(req.params.id) } })));
|
|
161
373
|
// POST /api/admin/direct-receive/readiness — ROOT + 真人 Passkey:返回【完整】Direct Pay launch readiness(blockers + facts,
|
|
162
374
|
// 含 KYB/sanctions/AML/base-bond/rail clearance 全细节)。只读诊断(不写库、不 flip launch);ROOT 专用,买家/卖家拿不到。
|
|
163
375
|
app.post('/api/admin/direct-receive/readiness', (req, res) => {
|
|
@@ -75,6 +75,22 @@ export function registerAdminProtocolParamsRoutes(app, deps) {
|
|
|
75
75
|
if (param.type === 'boolean' && !['true', 'false', '1', '0'].includes(strVal)) {
|
|
76
76
|
return void res.status(400).json({ error: '类型不匹配(需 boolean)' });
|
|
77
77
|
}
|
|
78
|
+
// S1 审计:json 型参数必须能被 parse(此前只有 number/boolean 校验,json 型能写进任意串 → 消费方 fail-open/closed 两难)
|
|
79
|
+
if (param.type === 'json') {
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(strVal);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return void res.status(400).json({ error: '类型不匹配(需合法 JSON)' });
|
|
86
|
+
}
|
|
87
|
+
// key 专项:平台禁售名单必须是大写区码数组(2-8 位字母/数字/-)—— 该参数直接进建单硬门,坏值会 fail-closed 挡全部下单
|
|
88
|
+
if (req.params.key === 'trade.platform_region_blocklist') {
|
|
89
|
+
if (!Array.isArray(parsed) || !parsed.every(x => typeof x === 'string' && /^[A-Z0-9-]{2,8}$/.test(x))) {
|
|
90
|
+
return void res.status(400).json({ error: 'trade.platform_region_blocklist 须为大写区码字符串数组,如 ["KP"]', code: 'BAD_REGION_BLOCKLIST' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
78
94
|
// A-3: 变更前快照旧值
|
|
79
95
|
const oldRow = await dbOne('SELECT value FROM protocol_params WHERE key = ?', [req.params.key]);
|
|
80
96
|
db.transaction(() => {
|
|
@@ -6,20 +6,29 @@ export function registerAdminReportsRoutes(app, deps) {
|
|
|
6
6
|
const admin = requireContentAdmin(req, res);
|
|
7
7
|
if (!admin)
|
|
8
8
|
return;
|
|
9
|
+
// 审计项 G:补 payment_rail 列 + ?rail 过滤 —— admin/AML 此前在订单总览里区分不出 direct_p2p(非托管轨监控盲区)
|
|
9
10
|
const status = req.query.status;
|
|
11
|
+
const rail = typeof req.query.rail === 'string' ? req.query.rail.trim() : '';
|
|
10
12
|
let sql = `SELECT o.id, o.product_id, o.buyer_id, o.seller_id, o.logistics_id, o.status,
|
|
11
|
-
o.total_amount, o.created_at,
|
|
13
|
+
o.total_amount, o.created_at, o.payment_rail,
|
|
12
14
|
p.title as product_title,
|
|
13
15
|
ub.name as buyer_name, us.name as seller_name
|
|
14
16
|
FROM orders o
|
|
15
17
|
JOIN products p ON o.product_id = p.id
|
|
16
18
|
JOIN users ub ON o.buyer_id = ub.id
|
|
17
19
|
JOIN users us ON o.seller_id = us.id`;
|
|
20
|
+
const where = [];
|
|
18
21
|
const params = [];
|
|
19
22
|
if (status && status.trim()) {
|
|
20
|
-
|
|
23
|
+
where.push('o.status = ?');
|
|
21
24
|
params.push(status);
|
|
22
25
|
}
|
|
26
|
+
if (rail) {
|
|
27
|
+
where.push('o.payment_rail = ?');
|
|
28
|
+
params.push(rail);
|
|
29
|
+
}
|
|
30
|
+
if (where.length)
|
|
31
|
+
sql += ' WHERE ' + where.join(' AND ');
|
|
23
32
|
sql += ` ORDER BY o.created_at DESC LIMIT 100`;
|
|
24
33
|
res.json({ orders: await dbAll(sql, params) });
|
|
25
34
|
});
|
|
@@ -27,20 +36,37 @@ export function registerAdminReportsRoutes(app, deps) {
|
|
|
27
36
|
const admin = requireArbitrationAdmin(req, res);
|
|
28
37
|
if (!admin)
|
|
29
38
|
return;
|
|
39
|
+
// 区分:按 status(open/in_review/resolved/dismissed)+ rail(direct_p2p/escrow)过滤;附 verdict/ruling_type/
|
|
40
|
+
// assigned_arbitrators/payment_rail 让 admin 分辨类型/进度/是否已指派/如何结案。summary 给全量分状态计数。
|
|
41
|
+
const status = typeof req.query.status === 'string' ? req.query.status.trim() : '';
|
|
42
|
+
const rail = typeof req.query.rail === 'string' ? req.query.rail.trim() : '';
|
|
43
|
+
const where = [];
|
|
44
|
+
const params = [];
|
|
45
|
+
if (status) {
|
|
46
|
+
where.push('d.status = ?');
|
|
47
|
+
params.push(status);
|
|
48
|
+
}
|
|
49
|
+
if (rail) {
|
|
50
|
+
where.push('o.payment_rail = ?');
|
|
51
|
+
params.push(rail);
|
|
52
|
+
}
|
|
30
53
|
const rows = await dbAll(`
|
|
31
54
|
SELECT d.id, d.order_id, d.initiator_id, d.defendant_id, d.reason, d.status,
|
|
32
55
|
d.created_at, d.respond_deadline, d.arbitrate_deadline, d.resolved_at,
|
|
56
|
+
d.verdict, d.ruling_type, d.assigned_arbitrators,
|
|
33
57
|
u1.name as initiator_name, u2.name as defendant_name,
|
|
34
|
-
o.total_amount, o.status as order_status,
|
|
58
|
+
o.total_amount, o.status as order_status, o.payment_rail,
|
|
35
59
|
p.title as product_title
|
|
36
60
|
FROM disputes d
|
|
37
61
|
LEFT JOIN users u1 ON d.initiator_id = u1.id
|
|
38
62
|
LEFT JOIN users u2 ON d.defendant_id = u2.id
|
|
39
63
|
LEFT JOIN orders o ON d.order_id = o.id
|
|
40
64
|
LEFT JOIN products p ON o.product_id = p.id
|
|
65
|
+
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
|
41
66
|
ORDER BY d.created_at DESC LIMIT 100
|
|
42
|
-
|
|
43
|
-
|
|
67
|
+
`, params);
|
|
68
|
+
const counts = await dbAll('SELECT status, COUNT(*) n FROM disputes GROUP BY status');
|
|
69
|
+
res.json({ disputes: rows, counts: Object.fromEntries(counts.map(c => [c.status, c.n])), total: counts.reduce((s, c) => s + c.n, 0) });
|
|
44
70
|
});
|
|
45
71
|
app.get('/api/admin/verify-tasks', async (req, res) => {
|
|
46
72
|
const admin = requireArbitrationAdmin(req, res);
|
|
@@ -22,7 +22,7 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
22
22
|
}
|
|
23
23
|
const calls30d = (await dbOne(`SELECT COUNT(*) as n FROM agent_call_log WHERE api_key = ? AND created_at > datetime('now', '-30 days')`, [k.api_key])).n;
|
|
24
24
|
const last = await dbOne(`SELECT endpoint, method, status_code, created_at FROM agent_call_log WHERE api_key = ? ORDER BY created_at DESC LIMIT 1`, [k.api_key]);
|
|
25
|
-
const strikes = await dbAll(`SELECT severity, reason_code, issued_at, expires_at, appeal_status FROM agent_strikes WHERE api_key = ? ORDER BY issued_at DESC LIMIT 5`, [k.api_key]);
|
|
25
|
+
const strikes = await dbAll(`SELECT id, severity, reason_code, reason_detail, issued_at, expires_at, appeal_status FROM agent_strikes WHERE api_key = ? ORDER BY issued_at DESC LIMIT 5`, [k.api_key]); // +id/detail:被封申诉 UI 需要(此端点封禁豁免)
|
|
26
26
|
let passport = null;
|
|
27
27
|
try {
|
|
28
28
|
passport = computeAgentPassport(db, k.api_key, user.id, custodianFingerprint);
|