@seasonkoh/webaz 0.1.28 → 0.1.29

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.
Files changed (111) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +7 -6
  3. package/dist/currency.js +16 -0
  4. package/dist/deposit-rails.js +52 -0
  5. package/dist/direct-pay-aml-monitor.js +67 -0
  6. package/dist/direct-pay-aml-review.js +40 -0
  7. package/dist/direct-pay-base-bond-entry.js +5 -0
  8. package/dist/direct-pay-bond-rail-clearance.js +94 -0
  9. package/dist/direct-pay-compliance-ingress.js +145 -0
  10. package/dist/direct-pay-controls.js +136 -0
  11. package/dist/direct-pay-create.js +162 -0
  12. package/dist/direct-pay-deferral-quota.js +43 -0
  13. package/dist/direct-pay-disclosures.js +44 -0
  14. package/dist/direct-pay-eligibility.js +46 -0
  15. package/dist/direct-pay-fee-ar.js +241 -0
  16. package/dist/direct-pay-launch-readiness.js +108 -0
  17. package/dist/direct-pay-launch-summary.js +68 -0
  18. package/dist/direct-pay-ledger.js +94 -0
  19. package/dist/direct-receive-account-qr.js +77 -0
  20. package/dist/direct-receive-accounts.js +82 -0
  21. package/dist/direct-receive-deferral.js +142 -0
  22. package/dist/direct-receive-deposits.js +260 -0
  23. package/dist/direct-receive-payment-instruction.js +26 -0
  24. package/dist/fx-rates.js +71 -0
  25. package/dist/layer0-foundation/L0-1-database/schema.js +531 -1
  26. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
  27. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +41 -0
  28. package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
  29. package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
  30. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +11 -0
  31. package/dist/layer1-agent/L1-1-mcp-server/server.js +18 -16
  32. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
  33. package/dist/ledger.js +12 -3
  34. package/dist/mcp.js +23 -4
  35. package/dist/merchant-bond-domain.js +64 -0
  36. package/dist/merchant-bond-exposure.js +78 -0
  37. package/dist/merchant-bond-watcher.js +26 -0
  38. package/dist/payment-rails.js +29 -0
  39. package/dist/product-verification.js +93 -0
  40. package/dist/pwa/acp-feed.js +3 -2
  41. package/dist/pwa/contract-fingerprint.js +3 -0
  42. package/dist/pwa/direct-pay-guards.js +20 -0
  43. package/dist/pwa/direct-pay-order-redaction.js +24 -0
  44. package/dist/pwa/endpoint-actions.js +3 -0
  45. package/dist/pwa/public/app-ai.js +10 -10
  46. package/dist/pwa/public/app-chat-poll.js +29 -0
  47. package/dist/pwa/public/app-create-kinds.js +17 -0
  48. package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
  49. package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
  50. package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
  51. package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
  52. package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
  53. package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
  54. package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
  55. package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
  56. package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
  57. package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
  58. package/dist/pwa/public/app-direct-pay.js +227 -0
  59. package/dist/pwa/public/app-discover.js +20 -20
  60. package/dist/pwa/public/app-external-links.js +32 -0
  61. package/dist/pwa/public/app-listings.js +4 -4
  62. package/dist/pwa/public/app-prelaunch-waz.js +39 -0
  63. package/dist/pwa/public/app-price.js +55 -0
  64. package/dist/pwa/public/app-product-media.js +15 -0
  65. package/dist/pwa/public/app-profile.js +6 -6
  66. package/dist/pwa/public/app-seller.js +5 -5
  67. package/dist/pwa/public/app-shop.js +19 -19
  68. package/dist/pwa/public/app.js +142 -146
  69. package/dist/pwa/public/i18n.js +398 -197
  70. package/dist/pwa/public/index.html +17 -0
  71. package/dist/pwa/public/openapi.json +495 -1
  72. package/dist/pwa/routes/admin-analytics.js +4 -2
  73. package/dist/pwa/routes/admin-direct-receive-deposits.js +321 -0
  74. package/dist/pwa/routes/buyer-feeds.js +2 -1
  75. package/dist/pwa/routes/chat.js +6 -1
  76. package/dist/pwa/routes/dashboards.js +2 -1
  77. package/dist/pwa/routes/direct-pay-availability.js +196 -0
  78. package/dist/pwa/routes/direct-pay-disclosure-acks.js +74 -0
  79. package/dist/pwa/routes/direct-pay-timeouts.js +71 -0
  80. package/dist/pwa/routes/direct-receive-accounts.js +155 -0
  81. package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
  82. package/dist/pwa/routes/fx.js +12 -0
  83. package/dist/pwa/routes/leaderboard.js +47 -9
  84. package/dist/pwa/routes/listings.js +2 -2
  85. package/dist/pwa/routes/logistics.js +4 -0
  86. package/dist/pwa/routes/manifests.js +38 -0
  87. package/dist/pwa/routes/me-data.js +5 -2
  88. package/dist/pwa/routes/orders-action.js +117 -9
  89. package/dist/pwa/routes/orders-create.js +4 -0
  90. package/dist/pwa/routes/orders-read.js +36 -0
  91. package/dist/pwa/routes/p2p-products.js +2 -2
  92. package/dist/pwa/routes/products-create.js +5 -3
  93. package/dist/pwa/routes/products-links.js +34 -0
  94. package/dist/pwa/routes/products-list.js +2 -1
  95. package/dist/pwa/routes/products-update.js +22 -2
  96. package/dist/pwa/routes/promoter.js +3 -0
  97. package/dist/pwa/routes/referral.js +4 -0
  98. package/dist/pwa/routes/returns.js +26 -1
  99. package/dist/pwa/routes/rewards-apply.js +10 -5
  100. package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
  101. package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
  102. package/dist/pwa/routes/shops.js +2 -1
  103. package/dist/pwa/routes/url-claim.js +2 -2
  104. package/dist/pwa/routes/users-public.js +8 -0
  105. package/dist/pwa/routes/wallet-read.js +3 -0
  106. package/dist/pwa/routes/webauthn.js +1 -1
  107. package/dist/pwa/server.js +59 -102
  108. package/dist/runtime/webaz-schema-helpers.js +104 -0
  109. package/dist/store-verification.js +77 -0
  110. package/dist/version.js +1 -1
  111. package/package.json +71 -2
@@ -0,0 +1,321 @@
1
+ import { confirmProductionReceipt } from '../../direct-receive-deposits.js';
2
+ import { reviewAmlFlag } from '../../direct-pay-aml-review.js';
3
+ import { recordKybReview, recordSanctionsScreening, recordAmlFlagIngress, amlDetailHash } from '../../direct-pay-compliance-ingress.js';
4
+ import { readDirectPayLaunchReadiness } from '../../direct-pay-launch-readiness.js';
5
+ import { approveDeferral, rejectDeferral, listDeferrals } from '../../direct-receive-deferral.js';
6
+ import { listProductVerifications, reviewProductVerification } from '../../product-verification.js';
7
+ import { listStoreVerifications, reviewStoreVerification } from '../../store-verification.js';
8
+ import { requireDirectPayHumanPasskey } from '../direct-pay-guards.js';
9
+ import { recordFeePrepayTopup, recordFeePrepayAdjustment, recordFeePrepayRefund, getDirectPayFeeAccount } from '../../direct-pay-fee-ar.js';
10
+ export function registerAdminDirectReceiveDepositsRoutes(app, deps) {
11
+ const { db, requireRootAdmin, consumeGateToken, logAdminAction, getProtocolParam } = deps;
12
+ // POST /api/admin/direct-receive/deposits/:id/confirm-production — ROOT + 真人 Passkey 手动确认生产保证金 receipt。
13
+ // 当前恒 fail-closed(无 legal-cleared rail → assert 抛 → PRODUCTION_RAIL_NOT_CLEARED)。
14
+ app.post('/api/admin/direct-receive/deposits/:id/confirm-production', (req, res) => {
15
+ const admin = requireRootAdmin(req, res);
16
+ if (!admin)
17
+ return;
18
+ const depositId = req.params.id;
19
+ const railId = String(req.body?.rail_id || '');
20
+ const amountUnits = Number(req.body?.expected_amount_units);
21
+ const receiptRef = String(req.body?.receipt_ref || '');
22
+ const jurisdiction = String(req.body?.jurisdiction || '');
23
+ const webauthnToken = req.body?.webauthn_token;
24
+ // 现场真人 Passkey(铁律)。purpose_data 绑定【完整动作字段】:deposit/rail/amount/receipt/jurisdiction 全等才放行,
25
+ // 防止把一次签名复用到另一笔/另一参数的确认上。
26
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
27
+ userId: admin.id, webauthnToken, purpose: 'direct_receive_production_confirm',
28
+ validate: (data) => {
29
+ const d = data;
30
+ return !!d && d.deposit_id === depositId && d.rail_id === railId && Number(d.amount_units) === amountUnits
31
+ && d.receipt_ref === receiptRef && d.jurisdiction === jurisdiction;
32
+ },
33
+ });
34
+ // 每次 ROOT 尝试都审计 —— 含【gate 失败】(生产保证金确认本身是敏感控制事件,缺 Passkey / 缺 token /
35
+ // purpose_data 不符的 ROOT 尝试同样要留痕)。非 ROOT 不审计(requireRootAdmin 已拦,无可信 admin 身份)。
36
+ if (!gate.ok) {
37
+ logAdminAction(admin.id, 'direct_receive.production_confirm', 'direct_receive_deposit', depositId, { rail_id: railId, jurisdiction, ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
38
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
39
+ }
40
+ // 写入只走唯一 writer。assert 抛 → 当前恒 fail-closed。每次 ROOT 尝试都审计(含结果)。
41
+ try {
42
+ const r = confirmProductionReceipt(db, { depositId, railId, expectedAmountUnits: amountUnits, receiptRef, jurisdiction });
43
+ 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
+ if (!r.ok)
45
+ return void res.status(409).json({ error: r.reason, error_code: 'PRODUCTION_CONFIRM_REJECTED' });
46
+ return void res.json({ ok: true, status: r.status, already: !!r.already });
47
+ }
48
+ catch (e) {
49
+ // assertProductionDepositRail(及任何写前异常)→ 生产 rail 未 legal-clear → fail-closed。
50
+ logAdminAction(admin.id, 'direct_receive.production_confirm', 'direct_receive_deposit', depositId, { rail_id: railId, jurisdiction, ok: false, outcome: 'rail_not_cleared', error: e.message });
51
+ return void res.status(409).json({ error: '生产保证金收款轨未通过法务/生产放行,暂不可确认', error_code: 'PRODUCTION_RAIL_NOT_CLEARED' });
52
+ }
53
+ });
54
+ // POST /api/admin/direct-receive/aml-flags/:id/review — ROOT + 真人 Passkey 复核单条 AML flag (PR-6E)。
55
+ // route 只做 auth + gate + 参数校验 + 调 reviewAmlFlag(唯一 review writer,原子改 flag + 写 audit)。
56
+ app.post('/api/admin/direct-receive/aml-flags/:id/review', (req, res) => {
57
+ const admin = requireRootAdmin(req, res);
58
+ if (!admin)
59
+ return;
60
+ const flagId = req.params.id;
61
+ const decision = String(req.body?.decision || '');
62
+ const notes = req.body?.notes != null ? String(req.body.notes) : undefined;
63
+ const webauthnToken = req.body?.webauthn_token;
64
+ // 现场真人 Passkey(铁律)。purpose_data 绑 flag_id + decision:一次签名只用于该 flag 的该决策,杜绝复用。
65
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
66
+ userId: admin.id, webauthnToken, purpose: 'direct_pay_aml_review',
67
+ validate: (data) => {
68
+ const d = data;
69
+ return !!d && d.flag_id === flagId && d.decision === decision;
70
+ },
71
+ });
72
+ if (!gate.ok) {
73
+ // gate 失败也审计(无 PII:flag_id + decision + 结果);成功复核的审计由 reviewAmlFlag 原子写入。
74
+ logAdminAction(admin.id, 'direct_pay.aml_review', 'aml_flag', flagId, { decision, ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
75
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
76
+ }
77
+ const r = reviewAmlFlag(db, { flagId, reviewerId: admin.id, decision, notes });
78
+ if (!r.ok) {
79
+ const status = r.error === 'FLAG_NOT_FOUND' ? 404 : 400;
80
+ return void res.status(status).json({ error: 'AML flag 复核失败', error_code: r.error });
81
+ }
82
+ return void res.json({ ok: true, flag_id: r.flagId, decision: r.decision, status: r.status, disposition: r.disposition });
83
+ });
84
+ // ── PR-6F 合规 ingress(受控写入入口)。三端点共用:ROOT + 真人 Passkey(purpose-bound)+ 调 ingress helper。 ──
85
+ // route 只做 auth + gate + 取参 + 调 helper(helper 原子写台账 + 审计);gate 失败也审计。无 vendor 外呼、无资金/状态机改动。
86
+ const str = (v) => v != null ? String(v) : '';
87
+ const opt = (v) => v != null ? String(v) : undefined;
88
+ // gatedIngress 返回一个 express handler(literal-path app.post 调用之 —— 让 API-doc 扫描器能识别端点)。
89
+ const gatedIngress = (purpose, makeValidate, run, failAudit) => (req, res) => {
90
+ const admin = requireRootAdmin(req, res);
91
+ if (!admin)
92
+ return;
93
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
94
+ userId: admin.id, webauthnToken: req.body?.webauthn_token, purpose, validate: makeValidate(req),
95
+ });
96
+ if (!gate.ok) {
97
+ const fa = failAudit(req);
98
+ logAdminAction(admin.id, purpose, 'user', fa.targetId, { ...fa.detail, ok: false, error_code: gate.error_code });
99
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
100
+ }
101
+ const r = run(req, admin.id);
102
+ if (!r.ok) {
103
+ // 业务校验失败(Passkey 已过)也留痕:money-adjacent admin 尝试(如退款余额不足被拒)必须可审计。
104
+ const fa = failAudit(req);
105
+ logAdminAction(admin.id, purpose, 'user', fa.targetId, { ...fa.detail, ok: false, error_code: r.error });
106
+ return void res.status(400).json({ error: '合规 ingress 失败', error_code: r.error });
107
+ }
108
+ return void res.json({ ok: true, id: r.id });
109
+ };
110
+ // POST /api/admin/direct-receive/kyb-reviews — KYB 复核结论 ingress(ROOT + 真人 Passkey)。
111
+ // Passkey purpose_data 绑定【完整写入内容】(user_id+status+provider_ref+expires_at):签 A 写 B 一律拒。
112
+ app.post('/api/admin/direct-receive/kyb-reviews', gatedIngress('direct_pay_kyb_ingress', (req) => (data) => {
113
+ const d = data;
114
+ return !!d && str(d.user_id) === str(req.body?.user_id) && str(d.status) === str(req.body?.status)
115
+ && str(d.provider_ref) === str(req.body?.provider_ref) && str(d.expires_at) === str(req.body?.expires_at);
116
+ }, (req, adminId) => recordKybReview(db, { userId: str(req.body?.user_id), reviewerId: adminId, status: str(req.body?.status), providerRef: opt(req.body?.provider_ref), expiresAt: opt(req.body?.expires_at) }), (req) => ({ targetId: str(req.body?.user_id), detail: { kyb_status: str(req.body?.status) } })));
117
+ // POST /api/admin/direct-receive/sanctions-screenings — 制裁筛查结论 ingress(ROOT + 真人 Passkey;high-risk)。
118
+ // purpose_data 绑定 user_id+status+provider_ref+expires_at。
119
+ app.post('/api/admin/direct-receive/sanctions-screenings', gatedIngress('direct_pay_sanctions_ingress', (req) => (data) => {
120
+ const d = data;
121
+ return !!d && str(d.user_id) === str(req.body?.user_id) && str(d.status) === str(req.body?.status)
122
+ && str(d.provider_ref) === str(req.body?.provider_ref) && str(d.expires_at) === str(req.body?.expires_at);
123
+ }, (req, adminId) => recordSanctionsScreening(db, { userId: str(req.body?.user_id), reviewerId: adminId, status: str(req.body?.status), providerRef: opt(req.body?.provider_ref), expiresAt: opt(req.body?.expires_at) }), (req) => ({ targetId: str(req.body?.user_id), detail: { sanctions_status: str(req.body?.status) } })));
124
+ // POST /api/admin/direct-receive/aml-flags — AML flag ingress(ROOT + 真人 Passkey;high-risk)。与 .../aml-flags/:id/review 区分。
125
+ // purpose_data 绑定 user_id+rule+severity+status+related_order_id+detail_hash(canonical):签 A 写 B 一律拒。
126
+ const amlDetail = (req) => (req.body?.detail && typeof req.body.detail === 'object' && !Array.isArray(req.body.detail)) ? req.body.detail : undefined;
127
+ app.post('/api/admin/direct-receive/aml-flags', gatedIngress('direct_pay_aml_ingress', (req) => (data) => {
128
+ const d = data;
129
+ return !!d && str(d.user_id) === str(req.body?.user_id) && str(d.rule) === str(req.body?.rule) && str(d.severity) === str(req.body?.severity)
130
+ && str(d.status) === str(req.body?.status) && str(d.related_order_id) === str(req.body?.related_order_id) && str(d.detail_hash) === amlDetailHash(amlDetail(req));
131
+ }, (req, adminId) => recordAmlFlagIngress(db, { userId: str(req.body?.user_id), reviewerId: adminId, rule: str(req.body?.rule), severity: str(req.body?.severity), status: str(req.body?.status), relatedOrderId: opt(req.body?.related_order_id), detail: amlDetail(req) }), (req) => ({ targetId: str(req.body?.user_id), detail: { rule: str(req.body?.rule), severity: str(req.body?.severity), aml_status: str(req.body?.status) } })));
132
+ // POST /api/admin/direct-receive/fee-prepay — 记录商家【平台服务费预付款】(ROOT + 真人 Passkey)。append-only;
133
+ // invoice_id NULL = 未分配预充值 → 计入 available_prepay(首单后续建单门)。purpose_data 绑定 seller_id+amount_units+method+evidence_ref。
134
+ // 不碰 buyer wallet/escrow/order/settlement/refund;非买家 escrow/保证金/penalty。本轮无"余额退款"(仅正向预付款登记)。
135
+ app.post('/api/admin/direct-receive/fee-prepay', gatedIngress('direct_pay_fee_prepay_record', (req) => (data) => {
136
+ const d = data;
137
+ return !!d && str(d.seller_id) === str(req.body?.seller_id) && Number(d.amount_units) === Number(req.body?.amount_units)
138
+ && str(d.method) === str(req.body?.method) && str(d.evidence_ref) === str(req.body?.evidence_ref);
139
+ }, (req, adminId) => recordFeePrepayTopup(db, { sellerId: str(req.body?.seller_id), amountUnits: Number(req.body?.amount_units), method: str(req.body?.method), recordedBy: adminId, evidenceRef: opt(req.body?.evidence_ref), note: opt(req.body?.note) }), (req) => ({ targetId: str(req.body?.seller_id), detail: { amount_units: Number(req.body?.amount_units), method: str(req.body?.method) } })));
140
+ // POST /api/admin/direct-receive/fee-adjust — 平台服务费账务【更正】(ROOT + 真人 Passkey)。带符号 delta;append-only + audit。
141
+ // ≠ 退款(不动真钱,只调记账)。purpose_data 绑 seller_id+delta_units+reason。
142
+ app.post('/api/admin/direct-receive/fee-adjust', gatedIngress('direct_pay_fee_adjust', (req) => (data) => {
143
+ const d = data;
144
+ return !!d && str(d.seller_id) === str(req.body?.seller_id) && Number(d.delta_units) === Number(req.body?.delta_units) && str(d.reason) === str(req.body?.reason);
145
+ }, (req, adminId) => recordFeePrepayAdjustment(db, { sellerId: str(req.body?.seller_id), deltaUnits: Number(req.body?.delta_units), reason: str(req.body?.reason), recordedBy: adminId }), (req) => ({ targetId: str(req.body?.seller_id), detail: { delta_units: Number(req.body?.delta_units) } })));
146
+ // POST /api/admin/direct-receive/fee-refund — 平台服务费余额【退款】(ROOT + 真人 Passkey)。真实退还未消耗预付款;
147
+ // amount ≤ 当前 available(helper 同事务校验)。append-only + audit。purpose_data 绑 seller_id+amount_units+method+evidence_ref。
148
+ app.post('/api/admin/direct-receive/fee-refund', gatedIngress('direct_pay_fee_refund', (req) => (data) => {
149
+ const d = data;
150
+ return !!d && str(d.seller_id) === str(req.body?.seller_id) && Number(d.amount_units) === Number(req.body?.amount_units)
151
+ && str(d.method) === str(req.body?.method) && str(d.evidence_ref) === str(req.body?.evidence_ref);
152
+ }, (req, adminId) => recordFeePrepayRefund(db, { sellerId: str(req.body?.seller_id), amountUnits: Number(req.body?.amount_units), method: str(req.body?.method), recordedBy: adminId, evidenceRef: opt(req.body?.evidence_ref), reason: opt(req.body?.reason) }), (req) => ({ targetId: str(req.body?.seller_id), detail: { amount_units: Number(req.body?.amount_units), method: str(req.body?.method) } })));
153
+ // GET /api/admin/direct-receive/fee-account/:seller_id — ROOT 只读:某商家平台服务费账户汇总(余额/预充值/应收/调整/退款/在途预估/宽限)。
154
+ // 只读诊断,不写、无 Passkey(读不授权能力);卖家私密财务,买家/卖家拿不到此 admin 视图。
155
+ app.get('/api/admin/direct-receive/fee-account/:seller_id', (req, res) => {
156
+ const admin = requireRootAdmin(req, res);
157
+ if (!admin)
158
+ return;
159
+ return void res.json({ ok: true, account: getDirectPayFeeAccount(db, str(req.params.seller_id)) });
160
+ });
161
+ // POST /api/admin/direct-receive/readiness — ROOT + 真人 Passkey:返回【完整】Direct Pay launch readiness(blockers + facts,
162
+ // 含 KYB/sanctions/AML/base-bond/rail clearance 全细节)。只读诊断(不写库、不 flip launch);ROOT 专用,买家/卖家拿不到。
163
+ app.post('/api/admin/direct-receive/readiness', (req, res) => {
164
+ const admin = requireRootAdmin(req, res);
165
+ if (!admin)
166
+ return;
167
+ const sellerId = req.body?.seller_id != null ? String(req.body.seller_id) : undefined;
168
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
169
+ userId: admin.id, webauthnToken: req.body?.webauthn_token, purpose: 'direct_pay_admin_readiness',
170
+ validate: (data) => { const d = data; return !!d && String(d.seller_id ?? '') === String(sellerId ?? ''); },
171
+ });
172
+ if (!gate.ok) {
173
+ logAdminAction(admin.id, 'direct_pay.admin_readiness', 'user', sellerId ?? null, { ok: false, error_code: gate.error_code });
174
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
175
+ }
176
+ const readiness = readDirectPayLaunchReadiness(db, { getProtocolParam, sellerId });
177
+ logAdminAction(admin.id, 'direct_pay.admin_readiness', 'user', sellerId ?? null, { ok: true, ready: readiness.ready, blocker_count: readiness.blockers.length });
178
+ return void res.json(readiness);
179
+ });
180
+ // ── 缓交(deferred base-bond)审批队列。读 = ROOT;批准/拒绝 = ROOT + 真人 Passkey(铁律,授予绝不自动)。 ──
181
+ // approveDeferral/rejectDeferral 是唯一 writer;本文件零 direct_receive_deferrals 直写。审批只改缓交状态机,
182
+ // 【不】碰 buyer wallet/escrow/order/settlement/refund/privileges,也不 flip launch。
183
+ const DEFERRAL_STATUSES = new Set(['pending', 'granted', 'rejected', 'expired']);
184
+ // GET /api/admin/direct-receive/deferrals?status=pending — ROOT 审批队列(默认全部;可按 status 过滤)。纯读。
185
+ app.get('/api/admin/direct-receive/deferrals', (req, res) => {
186
+ const admin = requireRootAdmin(req, res);
187
+ if (!admin)
188
+ return;
189
+ const status = req.query?.status != null ? String(req.query.status) : '';
190
+ if (status && !DEFERRAL_STATUSES.has(status))
191
+ return void res.status(400).json({ error: '非法 status', error_code: 'BAD_STATUS' });
192
+ return void res.json({ deferrals: listDeferrals(db, status ? { status: status } : {}) });
193
+ });
194
+ // POST /api/admin/direct-receive/deferrals/:id/approve — ROOT + 真人 Passkey 批准缓交。
195
+ // Passkey purpose_data 绑定【完整审批条款】(deferral_id + reduced_quota_factor + grace_days):签 A 批 B / 改条款一律拒。
196
+ app.post('/api/admin/direct-receive/deferrals/:id/approve', (req, res) => {
197
+ const admin = requireRootAdmin(req, res);
198
+ if (!admin)
199
+ return;
200
+ const deferralId = req.params.id;
201
+ const rqfRaw = req.body?.reduced_quota_factor;
202
+ const graceRaw = req.body?.grace_days;
203
+ const webauthnToken = req.body?.webauthn_token;
204
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
205
+ userId: admin.id, webauthnToken, purpose: 'direct_pay_deferral_approve',
206
+ validate: (data) => {
207
+ const d = data;
208
+ return !!d && str(d.deferral_id) === deferralId && str(d.reduced_quota_factor) === str(rqfRaw) && str(d.grace_days) === str(graceRaw);
209
+ },
210
+ });
211
+ if (!gate.ok) {
212
+ logAdminAction(admin.id, 'direct_pay.deferral_approve', 'direct_receive_deferral', deferralId, { ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
213
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
214
+ }
215
+ const r = approveDeferral(db, { deferralId, adminId: admin.id, nowIso: new Date().toISOString(),
216
+ graceDays: graceRaw != null ? Number(graceRaw) : undefined, reducedQuotaFactor: rqfRaw != null ? Number(rqfRaw) : undefined });
217
+ logAdminAction(admin.id, 'direct_pay.deferral_approve', 'direct_receive_deferral', deferralId, { ok: r.ok, outcome: r.ok ? (r.already ? 'already' : 'granted') : r.reason });
218
+ if (!r.ok)
219
+ return void res.status(409).json({ error: r.reason, error_code: 'DEFERRAL_APPROVE_REJECTED' });
220
+ return void res.json({ ok: true, status: r.status, already: !!r.already });
221
+ });
222
+ // POST /api/admin/direct-receive/deferrals/:id/reject — ROOT + 真人 Passkey 拒绝缓交。purpose_data 绑 deferral_id。
223
+ app.post('/api/admin/direct-receive/deferrals/:id/reject', (req, res) => {
224
+ const admin = requireRootAdmin(req, res);
225
+ if (!admin)
226
+ return;
227
+ const deferralId = req.params.id;
228
+ const webauthnToken = req.body?.webauthn_token;
229
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
230
+ userId: admin.id, webauthnToken, purpose: 'direct_pay_deferral_reject',
231
+ validate: (data) => { const d = data; return !!d && str(d.deferral_id) === deferralId; },
232
+ });
233
+ if (!gate.ok) {
234
+ logAdminAction(admin.id, 'direct_pay.deferral_reject', 'direct_receive_deferral', deferralId, { ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
235
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
236
+ }
237
+ const r = rejectDeferral(db, { deferralId, adminId: admin.id });
238
+ logAdminAction(admin.id, 'direct_pay.deferral_reject', 'direct_receive_deferral', deferralId, { ok: r.ok, outcome: r.ok ? (r.already ? 'already' : 'rejected') : r.reason });
239
+ if (!r.ok)
240
+ return void res.status(409).json({ error: r.reason, error_code: 'DEFERRAL_REJECT_REJECTED' });
241
+ return void res.json({ ok: true, status: r.status, already: !!r.already });
242
+ });
243
+ // ── 按产品认证(per-product verification)审批。读 = ROOT;verify/reject = ROOT + 真人 Passkey(硬门:核验=放行该产品直付,
244
+ // capability-granting → 铁律 Passkey)。reviewProductVerification 是唯一 writer;本文件零 product_verifications 直写。 ──
245
+ const PV_STATUSES = new Set(['issued', 'submitted', 'verified', 'rejected']);
246
+ // GET /api/admin/direct-receive/product-verifications?status=submitted — ROOT 审核队列(默认全部)。纯读。
247
+ app.get('/api/admin/direct-receive/product-verifications', (req, res) => {
248
+ const admin = requireRootAdmin(req, res);
249
+ if (!admin)
250
+ return;
251
+ const status = req.query?.status != null ? String(req.query.status) : '';
252
+ if (status && !PV_STATUSES.has(status))
253
+ return void res.status(400).json({ error: '非法 status', error_code: 'BAD_STATUS' });
254
+ return void res.json({ verifications: listProductVerifications(db, status ? { status: status } : {}) });
255
+ });
256
+ // POST /api/admin/direct-receive/product-verifications/:id/review — ROOT + 真人 Passkey 手动核对结论(verified|rejected)。
257
+ // Passkey purpose_data 绑 verification_id + decision:签 A 用 B / 改结论一律拒。verify = 放行该产品直付(逐品,绝不放行全部)。
258
+ app.post('/api/admin/direct-receive/product-verifications/:id/review', (req, res) => {
259
+ const admin = requireRootAdmin(req, res);
260
+ if (!admin)
261
+ return;
262
+ const verificationId = req.params.id;
263
+ const decision = String(req.body?.decision || '');
264
+ const notes = req.body?.notes != null ? String(req.body.notes) : undefined;
265
+ const webauthnToken = req.body?.webauthn_token;
266
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
267
+ userId: admin.id, webauthnToken, purpose: 'direct_pay_product_verify',
268
+ validate: (data) => { const d = data; return !!d && str(d.verification_id) === verificationId && str(d.decision) === decision; },
269
+ });
270
+ if (!gate.ok) {
271
+ logAdminAction(admin.id, 'direct_pay.product_verify', 'product_verification', verificationId, { decision, ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
272
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
273
+ }
274
+ const r = reviewProductVerification(db, { id: verificationId, reviewerId: admin.id, decision: decision, notes });
275
+ logAdminAction(admin.id, 'direct_pay.product_verify', 'product_verification', verificationId, { decision, ok: r.ok, outcome: r.ok ? (r.already ? 'already' : r.status) : r.reason });
276
+ if (!r.ok)
277
+ return void res.status(409).json({ error: r.reason, error_code: 'PRODUCT_VERIFICATION_REVIEW_REJECTED' });
278
+ return void res.json({ ok: true, status: r.status, already: !!r.already });
279
+ });
280
+ // ── 店铺认证(per-seller)审批 = 逐品验证豁免决定。读 = ROOT;verify/reject = ROOT + 真人 Passkey。 ──
281
+ // 核店铺时勾选 per_product_exempt:true → 该卖家所有商品免逐品验证(capability-granting → 铁律 Passkey;
282
+ // purpose_data 绑 verification_id+decision+per_product_exempt,签 A 用 B / 改豁免位一律拒)。reviewStoreVerification 唯一 writer。
283
+ const SV_STATUSES = new Set(['issued', 'submitted', 'verified', 'rejected']);
284
+ // GET /api/admin/direct-receive/store-verifications?status=submitted — ROOT 审核队列(默认全部)。纯读。
285
+ app.get('/api/admin/direct-receive/store-verifications', (req, res) => {
286
+ const admin = requireRootAdmin(req, res);
287
+ if (!admin)
288
+ return;
289
+ const status = req.query?.status != null ? String(req.query.status) : '';
290
+ if (status && !SV_STATUSES.has(status))
291
+ return void res.status(400).json({ error: '非法 status', error_code: 'BAD_STATUS' });
292
+ return void res.json({ verifications: listStoreVerifications(db, status ? { status: status } : {}) });
293
+ });
294
+ // POST /api/admin/direct-receive/store-verifications/:id/review — ROOT + 真人 Passkey 核店铺 + 勾选逐品豁免。
295
+ app.post('/api/admin/direct-receive/store-verifications/:id/review', (req, res) => {
296
+ const admin = requireRootAdmin(req, res);
297
+ if (!admin)
298
+ return;
299
+ const verificationId = req.params.id;
300
+ const decision = String(req.body?.decision || '');
301
+ const perProductExempt = req.body?.per_product_exempt === true;
302
+ const notes = req.body?.notes != null ? String(req.body.notes) : undefined;
303
+ const webauthnToken = req.body?.webauthn_token;
304
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
305
+ userId: admin.id, webauthnToken, purpose: 'direct_pay_store_verify',
306
+ validate: (data) => {
307
+ const d = data;
308
+ return !!d && str(d.verification_id) === verificationId && str(d.decision) === decision && (d.per_product_exempt === true) === perProductExempt;
309
+ },
310
+ });
311
+ if (!gate.ok) {
312
+ logAdminAction(admin.id, 'direct_pay.store_verify', 'store_verification', verificationId, { decision, per_product_exempt: perProductExempt, ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
313
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
314
+ }
315
+ const r = reviewStoreVerification(db, { id: verificationId, reviewerId: admin.id, decision: decision, perProductExempt, notes });
316
+ logAdminAction(admin.id, 'direct_pay.store_verify', 'store_verification', verificationId, { decision, per_product_exempt: perProductExempt, ok: r.ok, outcome: r.ok ? (r.already ? 'already' : r.status) : r.reason });
317
+ if (!r.ok)
318
+ return void res.status(409).json({ error: r.reason, error_code: 'STORE_VERIFICATION_REVIEW_REJECTED' });
319
+ return void res.json({ ok: true, status: r.status, per_product_exempt: !!r.perProductExempt, already: !!r.already });
320
+ });
321
+ }
@@ -1,4 +1,5 @@
1
1
  import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // RFC-018 PR4: 真实成交(排除全额退货)
2
3
  export function registerBuyerFeedsRoutes(app, deps) {
3
4
  // db 已走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
4
5
  const { auth, isTrustedRole, errorRes, getNearbyCellPrecision, getProtocolParam } = deps;
@@ -28,7 +29,7 @@ export function registerBuyerFeedsRoutes(app, deps) {
28
29
  ? `AND p.id NOT IN (${exclArgs.map(() => '?').join(',')})`
29
30
  : '';
30
31
  const baseCols = `p.id, p.title, p.price, p.stock, p.category, p.images, p.has_variants, p.seller_id,
31
- (SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND o.status = 'completed') as sales_count,
32
+ (SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND ${genuineSalePredicate('o')}) as sales_count,
32
33
  u.name as seller_name, u.handle as seller_handle`;
33
34
  let followedProducts = [];
34
35
  if (followedSellerIds.length > 0) {
@@ -108,7 +108,12 @@ export function registerChatRoutes(app, deps) {
108
108
  CASE WHEN c.user_a = ? THEN c.unread_a ELSE c.unread_b END as my_unread,
109
109
  CASE WHEN c.user_a = ? THEN c.user_b ELSE c.user_a END as other_id,
110
110
  (SELECT handle FROM users WHERE id = CASE WHEN c.user_a = ? THEN c.user_b ELSE c.user_a END) as other_handle,
111
- (SELECT name FROM users WHERE id = CASE WHEN c.user_a = ? THEN c.user_b ELSE c.user_a END) as other_name
111
+ (SELECT name FROM users WHERE id = CASE WHEN c.user_a = ? THEN c.user_b ELSE c.user_a END) as other_name,
112
+ -- 友好标签:订单/问商品 → 商品名,让收件箱里同一对方的多个会话可区分(不只显示 kind)。无新 bind 参数(仅用 c.kind/c.context_id)。
113
+ (CASE c.kind
114
+ WHEN 'order' THEN (SELECT p.title FROM orders o JOIN products p ON p.id = o.product_id WHERE o.id = c.context_id)
115
+ WHEN 'listing_qa' THEN (SELECT title FROM products WHERE id = c.context_id)
116
+ ELSE NULL END) as context_title
112
117
  FROM conversations c
113
118
  WHERE (c.user_a = ? OR c.user_b = ?) AND c.status NOT IN ('blocked','archived')
114
119
  ORDER BY COALESCE(c.last_message_at, c.created_at) DESC
@@ -1,4 +1,5 @@
1
1
  import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // RFC-018 PR4: 真实成交(排除全额退货)
2
3
  export function registerDashboardsRoutes(app, deps) {
3
4
  // db 已走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
4
5
  const { auth } = deps;
@@ -70,7 +71,7 @@ export function registerDashboardsRoutes(app, deps) {
70
71
  `, [userId, userId, userId, userId, userId, userId]);
71
72
  const highComm = await dbAll(`
72
73
  SELECT p.id, p.title, p.price, p.commission_rate, p.images, p.category,
73
- (SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id AND o.status = 'completed') as sales_count
74
+ (SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id AND ${genuineSalePredicate('o')}) as sales_count
74
75
  FROM products p
75
76
  WHERE p.status = 'active'
76
77
  AND p.commission_rate > 0
@@ -0,0 +1,196 @@
1
+ import { dbOne } from '../../layer0-foundation/L0-1-database/db.js';
2
+ import { toUnits } from '../../money.js';
3
+ import { sellerBaseBondEntrySatisfied } from '../../direct-pay-base-bond-entry.js';
4
+ import { evaluateDirectPayLaunchControls, readDirectPayControlsConfig, sellerDirectPayKybPassed, sellerDirectPaySanctionsClear, sellerDirectPayAmlClear, sellerDirectPayBreakerTripped, coarsenBuyerFacingDirectPayCode, DIRECT_PAY_SELLER_NOT_ELIGIBLE } from '../../direct-pay-controls.js';
5
+ import { checkDeferralQuota, readDeferralQuotaConfig } from '../../direct-pay-deferral-quota.js';
6
+ import { sellerDirectPayReadinessView } from '../../direct-pay-launch-readiness.js';
7
+ import { requestDeferral, getActiveDeferral, getLatestDeferral } from '../../direct-receive-deferral.js';
8
+ import { requestProductVerification, submitProductVerificationLink, listSellerProductVerifications, toSellerProductVerificationView, productStoreVerified } from '../../product-verification.js';
9
+ import { requestStoreVerification, submitStoreVerificationLink, getStoreVerification, toSellerStoreVerificationView, sellerExemptFromPerProduct } from '../../store-verification.js';
10
+ import { getDirectPayFeeAccount } from '../../direct-pay-fee-ar.js';
11
+ export function registerDirectPayAvailabilityRoutes(app, deps) {
12
+ const { db, auth, getProtocolParam, generateId } = deps;
13
+ /** 登录 + seller 角色门(缓交申请仅卖家本人)。返回 user 或 null(已写错误响应)。 */
14
+ function requireSeller(req, res) {
15
+ const user = auth(req, res);
16
+ if (!user)
17
+ return null;
18
+ if (user.role !== 'seller') {
19
+ res.status(403).json({ error: '仅卖家可申请缓交', error_code: 'SELLER_ONLY' });
20
+ return null;
21
+ }
22
+ return user;
23
+ }
24
+ // GET /api/direct-pay/availability?product_id=... — 该商品(以 qty=1 计)当前是否可直付 + 不可用原因(脱敏)。
25
+ app.get('/api/direct-pay/availability', async (req, res) => {
26
+ const user = auth(req, res);
27
+ if (!user)
28
+ return;
29
+ const productId = String(req.query.product_id || '');
30
+ if (!productId)
31
+ return void res.status(400).json({ error: '缺少 product_id', error_code: 'MISSING_PRODUCT_ID' });
32
+ const product = await dbOne('SELECT seller_id, price FROM products WHERE id = ?', [productId]);
33
+ if (!product)
34
+ return void res.status(404).json({ error: '商品不存在', error_code: 'PRODUCT_NOT_FOUND' });
35
+ const cfg = readDirectPayControlsConfig(getProtocolParam);
36
+ const decision = evaluateDirectPayLaunchControls(cfg, {
37
+ amountUnits: toUnits(Number(product.price) || 0),
38
+ sellerBreakerTripped: sellerDirectPayBreakerTripped(db, product.seller_id), // 与 create 路径同源:卖家熔断也判不可用
39
+ baseBondSatisfied: sellerBaseBondEntrySatisfied(db, product.seller_id, new Date().toISOString()),
40
+ kycSanctionsPassed: sellerDirectPayKybPassed(db, product.seller_id) && sellerDirectPaySanctionsClear(db, product.seller_id),
41
+ amlClear: sellerDirectPayAmlClear(db, product.seller_id),
42
+ });
43
+ if (decision.ok) {
44
+ // 硬门(镜像 create):产品逐品验证 OR 卖家被豁免(店铺 verified + per_product_exempt)。都不满足 → 不可直付(产品级、非敏感)。
45
+ if (!(productStoreVerified(db, productId) || sellerExemptFromPerProduct(db, product.seller_id)))
46
+ return void res.json({ available: false, error_code: 'DIRECT_PAY_PRODUCT_NOT_VERIFIED', reason: '该商品暂不支持直付(待平台验证)', per_tx_cap_units: cfg.perTxCapUnits });
47
+ // 镜像 create 的缓交额度门(qty=1 预览;以商品单价为本次拟建单金额)。超额是【缓交卖家私密状态】→ 收敛为通用不可用,
48
+ // 不向买家暴露"该卖家在缓交期/已超额"。create 仍是权威强制点。
49
+ const quota = checkDeferralQuota(db, product.seller_id, toUnits(Number(product.price) || 0), new Date().toISOString(), readDeferralQuotaConfig(getProtocolParam));
50
+ if (!quota.ok)
51
+ return void res.json({ available: false, error_code: coarsenBuyerFacingDirectPayCode(quota.code), reason: '该卖家暂不支持直付', per_tx_cap_units: cfg.perTxCapUnits });
52
+ return void res.json({ available: true, per_tx_cap_units: cfg.perTxCapUnits });
53
+ }
54
+ const code = coarsenBuyerFacingDirectPayCode(decision.error_code);
55
+ return void res.json({
56
+ available: false,
57
+ error_code: code,
58
+ reason: code === DIRECT_PAY_SELLER_NOT_ELIGIBLE ? '该卖家暂不支持直付' : decision.reason,
59
+ per_tx_cap_units: cfg.perTxCapUnits,
60
+ });
61
+ });
62
+ // GET /api/direct-receive/readiness — 卖家【自助脱敏】readiness:仅可行动/状态项(收款说明/Passkey/保证金/审核/暂停/平台开放)。
63
+ // 绝不下发 raw blocker / KYB·制裁·AML 分项(见 sellerDirectPayReadinessView)。只读 self(auth 用户自身 id)。
64
+ app.get('/api/direct-receive/readiness', (req, res) => {
65
+ const user = auth(req, res);
66
+ if (!user)
67
+ return;
68
+ return void res.json(sellerDirectPayReadinessView(db, { getProtocolParam, sellerId: user.id }));
69
+ });
70
+ // ── 缓交(base-bond deferred deposit)卖家自助:申请 + 查看自己的状态。授予【绝不】在此发生 ──
71
+ // 申请只创建 pending 行(requestDeferral),【不授予任何权限/资格】;真正放行 = admin ROOT + 真人 Passkey 审批
72
+ // (POST /api/admin/direct-receive/deferrals/:id/approve)。故 apply 本身非 RISK 动作、不需 Passkey:无论谁(含 agent)
73
+ // 申请都拿不到资格,安全门坐落在审批侧。所有合规门(KYC/KYB/制裁/AML/收款说明/Passkey)在直付建单时仍逐一 AND,缓交只免"先交保证金"。
74
+ // POST /api/direct-receive/deferral — 卖家申请缓交。helper 强制:单一活跃、periodDays 正整数、id 唯一。
75
+ app.post('/api/direct-receive/deferral', (req, res) => {
76
+ const user = requireSeller(req, res);
77
+ if (!user)
78
+ return;
79
+ const reason = typeof req.body?.reason === 'string' ? req.body.reason.trim().slice(0, 500) : undefined;
80
+ const periodDays = req.body?.period_days != null ? Number(req.body.period_days) : undefined;
81
+ const r = requestDeferral(db, { deferralId: generateId('dfr'), userId: user.id, periodDays, reason, nowIso: new Date().toISOString() });
82
+ if (!r.ok)
83
+ return void res.status(400).json({ error: r.reason, error_code: 'DEFERRAL_REQUEST_REJECTED' });
84
+ return void res.json({ ok: true, status: r.status });
85
+ });
86
+ // GET /api/direct-receive/deferral — 卖家本人缓交状态:最新一条申请(脱敏:不含 admin 身份)+ 是否当前生效(active)。
87
+ app.get('/api/direct-receive/deferral', (req, res) => {
88
+ const user = requireSeller(req, res);
89
+ if (!user)
90
+ return;
91
+ const sellerId = user.id;
92
+ const latest = getLatestDeferral(db, sellerId); // 脱敏:不含 approved_by/approved_at(admin 身份)
93
+ const active = getActiveDeferral(db, sellerId, new Date().toISOString());
94
+ return void res.json({
95
+ deferral: latest,
96
+ active: active ? { reduced_quota_factor: active.reducedQuotaFactor, expires_at: active.expiresAt, grace_until: active.graceUntil, in_grace: active.inGrace } : null,
97
+ });
98
+ });
99
+ // GET /api/direct-receive/my-fee-account — 卖家【本人】平台服务费账户(余额/已计提/退款/在途预估/首单宽限)。
100
+ // 仅本人(requireSeller),买家拿不到;只读、不碰任何资金动作。供 seller fee center 展示。
101
+ app.get('/api/direct-receive/my-fee-account', (req, res) => {
102
+ const user = requireSeller(req, res);
103
+ if (!user)
104
+ return;
105
+ return void res.json({ account: getDirectPayFeeAccount(db, user.id) });
106
+ });
107
+ // ── 按产品认证(per-product verification)卖家自助:申领验证码 → 提交外部商品链接 → 查看逐产品状态 ──
108
+ // 硬门:每个直付商品都须【单独】被真人 admin 核验;一次验证绝不放行所有产品。这里只建/改记录,verify 在 admin 侧。
109
+ // 所有权:必须卖家本人拥有该产品(读 products.seller_id 校验),否则 403。
110
+ /** 校验登录卖家拥有该产品;返回 owner user(含 id)或 null(已写错误响应)。productId 由调用方持有。 */
111
+ async function requireOwnedProduct(req, res, productId) {
112
+ const user = requireSeller(req, res);
113
+ if (!user)
114
+ return null;
115
+ if (!productId) {
116
+ res.status(400).json({ error: '缺少 product_id', error_code: 'MISSING_PRODUCT_ID' });
117
+ return null;
118
+ }
119
+ const product = await dbOne('SELECT seller_id FROM products WHERE id = ?', [productId]);
120
+ if (!product) {
121
+ res.status(404).json({ error: '商品不存在', error_code: 'PRODUCT_NOT_FOUND' });
122
+ return null;
123
+ }
124
+ if (product.seller_id !== user.id) {
125
+ res.status(403).json({ error: '只能为自己的商品申请认证', error_code: 'NOT_PRODUCT_OWNER' });
126
+ return null;
127
+ }
128
+ return user;
129
+ }
130
+ // POST /api/direct-receive/product-verification — 卖家为某产品申领验证码(单一活跃 per product)。
131
+ app.post('/api/direct-receive/product-verification', async (req, res) => {
132
+ const productId = String(req.body?.product_id || '');
133
+ const user = await requireOwnedProduct(req, res, productId);
134
+ if (!user)
135
+ return;
136
+ const platform = typeof req.body?.platform === 'string' ? req.body.platform.trim().slice(0, 60) : undefined;
137
+ const r = requestProductVerification(db, { id: generateId('pv'), productId, sellerId: user.id, code: generateId('wzv'), platform });
138
+ if (!r.ok)
139
+ return void res.status(400).json({ error: r.reason, error_code: 'PRODUCT_VERIFICATION_REJECTED' });
140
+ return void res.json({ ok: true, status: r.status, code: r.code });
141
+ });
142
+ // PUT /api/direct-receive/product-verification — 卖家为某产品提交外部商品链接(链接仅存储,WebAZ 不抓取)。
143
+ app.put('/api/direct-receive/product-verification', async (req, res) => {
144
+ const productId = String(req.body?.product_id || '');
145
+ const user = await requireOwnedProduct(req, res, productId);
146
+ if (!user)
147
+ return;
148
+ const externalUrl = typeof req.body?.external_url === 'string' ? req.body.external_url.trim() : '';
149
+ const platform = typeof req.body?.platform === 'string' ? req.body.platform.trim().slice(0, 60) : undefined;
150
+ const r = submitProductVerificationLink(db, { productId, externalUrl, platform });
151
+ if (!r.ok)
152
+ return void res.status(400).json({ error: r.reason, error_code: 'PRODUCT_VERIFICATION_SUBMIT_REJECTED' });
153
+ return void res.json({ ok: true, status: r.status });
154
+ });
155
+ // GET /api/direct-receive/product-verifications — 卖家本人所有产品的认证状态(逐产品)。
156
+ app.get('/api/direct-receive/product-verifications', (req, res) => {
157
+ const user = requireSeller(req, res);
158
+ if (!user)
159
+ return;
160
+ // 脱敏:DTO 去掉 reviewed_by(admin 身份)+ notes(内部审核备注),与缓交/readiness 卖家侧一致。
161
+ return void res.json({ verifications: listSellerProductVerifications(db, user.id).map(toSellerProductVerificationView) });
162
+ });
163
+ // ── 店铺认证(per-seller)= 逐品验证的豁免申请路径。卖家申领码 → 提交店铺链接 → admin 核对时勾选豁免。 ──
164
+ // 申请/提交【不】门 Passkey(只建记录,不授予;豁免授予 = admin ROOT+Passkey 在审核侧)。
165
+ // POST /api/direct-receive/store-verification — 卖家申领店铺验证码(单一活跃 per seller)。
166
+ app.post('/api/direct-receive/store-verification', (req, res) => {
167
+ const user = requireSeller(req, res);
168
+ if (!user)
169
+ return;
170
+ const platform = typeof req.body?.platform === 'string' ? req.body.platform.trim().slice(0, 60) : undefined;
171
+ const r = requestStoreVerification(db, { id: generateId('sv'), userId: user.id, code: generateId('wzs'), platform });
172
+ if (!r.ok)
173
+ return void res.status(400).json({ error: r.reason, error_code: 'STORE_VERIFICATION_REJECTED' });
174
+ return void res.json({ ok: true, status: r.status, code: r.code });
175
+ });
176
+ // PUT /api/direct-receive/store-verification — 卖家提交店铺外链(仅存储,不抓取)。
177
+ app.put('/api/direct-receive/store-verification', (req, res) => {
178
+ const user = requireSeller(req, res);
179
+ if (!user)
180
+ return;
181
+ const externalUrl = typeof req.body?.external_url === 'string' ? req.body.external_url.trim() : '';
182
+ const platform = typeof req.body?.platform === 'string' ? req.body.platform.trim().slice(0, 60) : undefined;
183
+ const r = submitStoreVerificationLink(db, { userId: user.id, externalUrl, platform });
184
+ if (!r.ok)
185
+ return void res.status(400).json({ error: r.reason, error_code: 'STORE_VERIFICATION_SUBMIT_REJECTED' });
186
+ return void res.json({ ok: true, status: r.status });
187
+ });
188
+ // GET /api/direct-receive/store-verification — 卖家本人店铺认证状态(脱敏 DTO,含豁免位)。
189
+ app.get('/api/direct-receive/store-verification', (req, res) => {
190
+ const user = requireSeller(req, res);
191
+ if (!user)
192
+ return;
193
+ const row = getStoreVerification(db, user.id);
194
+ return void res.json({ verification: row ? toSellerStoreVerificationView(row) : null, exempt: sellerExemptFromPerProduct(db, user.id) });
195
+ });
196
+ }