@seasonkoh/webaz 0.1.28 → 0.1.30

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 (198) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +7 -6
  3. package/dist/bond-refund-blockers.js +50 -0
  4. package/dist/bond-slash.js +100 -0
  5. package/dist/bond-terms.js +31 -0
  6. package/dist/currency.js +16 -0
  7. package/dist/deposit-rails.js +52 -0
  8. package/dist/direct-pay-aml-monitor.js +67 -0
  9. package/dist/direct-pay-aml-review.js +40 -0
  10. package/dist/direct-pay-base-bond-entry.js +5 -0
  11. package/dist/direct-pay-bond-rail-clearance.js +99 -0
  12. package/dist/direct-pay-cancel-refund.js +160 -0
  13. package/dist/direct-pay-compliance-ingress.js +145 -0
  14. package/dist/direct-pay-controls.js +136 -0
  15. package/dist/direct-pay-create.js +231 -0
  16. package/dist/direct-pay-deferral-quota.js +43 -0
  17. package/dist/direct-pay-disclosures.js +50 -0
  18. package/dist/direct-pay-eligibility.js +46 -0
  19. package/dist/direct-pay-fee-ar.js +241 -0
  20. package/dist/direct-pay-fee-prepay-request.js +80 -0
  21. package/dist/direct-pay-launch-readiness.js +108 -0
  22. package/dist/direct-pay-launch-summary.js +68 -0
  23. package/dist/direct-pay-ledger.js +94 -0
  24. package/dist/direct-pay-returns.js +104 -0
  25. package/dist/direct-pay-stock.js +9 -0
  26. package/dist/direct-receive-account-qr.js +77 -0
  27. package/dist/direct-receive-accounts.js +82 -0
  28. package/dist/direct-receive-deferral.js +171 -0
  29. package/dist/direct-receive-deposits.js +365 -0
  30. package/dist/direct-receive-payment-instruction.js +26 -0
  31. package/dist/free-shipping.js +37 -0
  32. package/dist/fx-rates.js +78 -0
  33. package/dist/layer0-foundation/L0-1-database/schema.js +777 -1
  34. package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
  35. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
  36. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +114 -0
  37. package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
  38. package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
  39. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +11 -0
  40. package/dist/layer1-agent/L1-1-mcp-server/server.js +99 -32
  41. package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
  42. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
  43. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
  44. package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
  45. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
  46. package/dist/ledger.js +12 -3
  47. package/dist/mcp.js +23 -4
  48. package/dist/merchant-bond-domain.js +64 -0
  49. package/dist/merchant-bond-exposure.js +78 -0
  50. package/dist/merchant-bond-watcher.js +26 -0
  51. package/dist/payment-rails.js +29 -0
  52. package/dist/platform-receive-accounts.js +94 -0
  53. package/dist/product-verification.js +93 -0
  54. package/dist/pwa/acp-feed.js +3 -2
  55. package/dist/pwa/arbitration-read-admin.js +37 -0
  56. package/dist/pwa/arbitrator-lifecycle.js +100 -0
  57. package/dist/pwa/contract-fingerprint.js +18 -0
  58. package/dist/pwa/direct-pay-guards.js +20 -0
  59. package/dist/pwa/direct-pay-order-redaction.js +42 -0
  60. package/dist/pwa/endpoint-actions.js +3 -0
  61. package/dist/pwa/human-presence.js +2 -6
  62. package/dist/pwa/public/app-account.js +9 -9
  63. package/dist/pwa/public/app-admin-disputes.js +55 -0
  64. package/dist/pwa/public/app-agent-appeal.js +90 -0
  65. package/dist/pwa/public/app-agent-approvals.js +93 -0
  66. package/dist/pwa/public/app-agent-pair.js +127 -0
  67. package/dist/pwa/public/app-ai.js +10 -10
  68. package/dist/pwa/public/app-arbitrator-admin.js +87 -0
  69. package/dist/pwa/public/app-arbitrator-entry.js +9 -0
  70. package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
  71. package/dist/pwa/public/app-bond-refund-ui.js +66 -0
  72. package/dist/pwa/public/app-bond-slash-ui.js +74 -0
  73. package/dist/pwa/public/app-bond-terms-ui.js +23 -0
  74. package/dist/pwa/public/app-bond-ui.js +108 -0
  75. package/dist/pwa/public/app-chat-poll.js +29 -0
  76. package/dist/pwa/public/app-contribution-hub.js +23 -0
  77. package/dist/pwa/public/app-create-kinds.js +17 -0
  78. package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
  79. package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
  80. package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
  81. package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
  82. package/dist/pwa/public/app-direct-pay-copy.js +12 -0
  83. package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
  84. package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
  85. package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
  86. package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
  87. package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
  88. package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
  89. package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
  90. package/dist/pwa/public/app-direct-pay-memo.js +14 -0
  91. package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
  92. package/dist/pwa/public/app-direct-pay-pay.js +15 -0
  93. package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
  94. package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
  95. package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
  96. package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
  97. package/dist/pwa/public/app-direct-pay-returns.js +56 -0
  98. package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
  99. package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
  100. package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
  101. package/dist/pwa/public/app-direct-pay.js +226 -0
  102. package/dist/pwa/public/app-discover.js +20 -20
  103. package/dist/pwa/public/app-dispute-close-ui.js +38 -0
  104. package/dist/pwa/public/app-external-links.js +32 -0
  105. package/dist/pwa/public/app-free-shipping-ui.js +29 -0
  106. package/dist/pwa/public/app-gmv-rail-split.js +12 -0
  107. package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
  108. package/dist/pwa/public/app-listings.js +4 -4
  109. package/dist/pwa/public/app-mutual-cancel.js +54 -0
  110. package/dist/pwa/public/app-notif-templates-orders.js +43 -0
  111. package/dist/pwa/public/app-notif-templates.js +22 -0
  112. package/dist/pwa/public/app-order-accept-ui.js +158 -0
  113. package/dist/pwa/public/app-order-errors.js +50 -0
  114. package/dist/pwa/public/app-order-labels.js +19 -0
  115. package/dist/pwa/public/app-order-rail-filter.js +26 -0
  116. package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
  117. package/dist/pwa/public/app-poll-governor.js +22 -0
  118. package/dist/pwa/public/app-prelaunch-waz.js +39 -0
  119. package/dist/pwa/public/app-price.js +55 -0
  120. package/dist/pwa/public/app-product-media.js +15 -0
  121. package/dist/pwa/public/app-profile.js +10 -10
  122. package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
  123. package/dist/pwa/public/app-sale-regions-ui.js +38 -0
  124. package/dist/pwa/public/app-seller.js +5 -5
  125. package/dist/pwa/public/app-shop.js +19 -19
  126. package/dist/pwa/public/app-trade-tax-ui.js +33 -0
  127. package/dist/pwa/public/app.js +264 -295
  128. package/dist/pwa/public/i18n.js +1068 -197
  129. package/dist/pwa/public/index.html +58 -0
  130. package/dist/pwa/public/openapi.json +1845 -653
  131. package/dist/pwa/public/style.css +3 -0
  132. package/dist/pwa/routes/admin-analytics.js +4 -2
  133. package/dist/pwa/routes/admin-direct-receive-deposits.js +533 -0
  134. package/dist/pwa/routes/admin-protocol-params.js +16 -0
  135. package/dist/pwa/routes/admin-reports.js +31 -5
  136. package/dist/pwa/routes/agent-governance.js +1 -1
  137. package/dist/pwa/routes/agent-grants.js +253 -32
  138. package/dist/pwa/routes/analytics.js +2 -0
  139. package/dist/pwa/routes/arbitrator.js +67 -14
  140. package/dist/pwa/routes/bond-seller.js +162 -0
  141. package/dist/pwa/routes/buyer-feeds.js +2 -1
  142. package/dist/pwa/routes/chat.js +6 -1
  143. package/dist/pwa/routes/dashboards.js +2 -1
  144. package/dist/pwa/routes/direct-pay-availability.js +196 -0
  145. package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
  146. package/dist/pwa/routes/direct-pay-disclosure-acks.js +79 -0
  147. package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
  148. package/dist/pwa/routes/direct-pay-returns.js +74 -0
  149. package/dist/pwa/routes/direct-pay-timeouts.js +248 -0
  150. package/dist/pwa/routes/direct-receive-accounts.js +155 -0
  151. package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
  152. package/dist/pwa/routes/disputes-read.js +20 -6
  153. package/dist/pwa/routes/disputes-write.js +91 -33
  154. package/dist/pwa/routes/external-anchors.js +4 -2
  155. package/dist/pwa/routes/fee-prepay-requests.js +66 -0
  156. package/dist/pwa/routes/fx.js +12 -0
  157. package/dist/pwa/routes/governance-onboarding.js +46 -8
  158. package/dist/pwa/routes/leaderboard.js +47 -9
  159. package/dist/pwa/routes/listings.js +2 -2
  160. package/dist/pwa/routes/logistics.js +6 -2
  161. package/dist/pwa/routes/manifests.js +38 -0
  162. package/dist/pwa/routes/me-data.js +5 -2
  163. package/dist/pwa/routes/mutual-cancel.js +62 -0
  164. package/dist/pwa/routes/orders-action.js +297 -16
  165. package/dist/pwa/routes/orders-create.js +21 -6
  166. package/dist/pwa/routes/orders-read.js +106 -8
  167. package/dist/pwa/routes/p2p-products.js +2 -2
  168. package/dist/pwa/routes/platform-receive-accounts.js +111 -0
  169. package/dist/pwa/routes/products-create.js +15 -4
  170. package/dist/pwa/routes/products-links.js +34 -0
  171. package/dist/pwa/routes/products-list.js +2 -1
  172. package/dist/pwa/routes/products-update.js +37 -3
  173. package/dist/pwa/routes/profile-identity.js +4 -2
  174. package/dist/pwa/routes/promoter.js +3 -0
  175. package/dist/pwa/routes/referral.js +4 -0
  176. package/dist/pwa/routes/returns.js +60 -4
  177. package/dist/pwa/routes/rewards-apply.js +10 -5
  178. package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
  179. package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
  180. package/dist/pwa/routes/seller-directpay-report.js +110 -0
  181. package/dist/pwa/routes/seller-quota.js +5 -1
  182. package/dist/pwa/routes/shipping-templates.js +219 -0
  183. package/dist/pwa/routes/shops.js +2 -1
  184. package/dist/pwa/routes/snf.js +4 -1
  185. package/dist/pwa/routes/url-claim.js +2 -2
  186. package/dist/pwa/routes/users-public.js +8 -0
  187. package/dist/pwa/routes/wallet-read.js +3 -0
  188. package/dist/pwa/routes/webauthn.js +3 -3
  189. package/dist/pwa/server.js +112 -132
  190. package/dist/runtime/agent-grant-scopes.js +69 -1
  191. package/dist/runtime/webaz-schema-helpers.js +161 -3
  192. package/dist/sale-regions.js +116 -0
  193. package/dist/shipping-templates.js +119 -0
  194. package/dist/store-verification.js +77 -0
  195. package/dist/trade-tax.js +99 -0
  196. package/dist/trade-terms.js +76 -0
  197. package/dist/version.js +1 -1
  198. package/package.json +128 -2
@@ -0,0 +1,155 @@
1
+ import { listSellerAccounts, getAccount, addAccount, updateAccount, deactivateAccount, listSellerAccountOptions } from '../../direct-receive-accounts.js';
2
+ import { storeQrImage, getQrImageForOwner, appendAccountEvent } from '../../direct-receive-account-qr.js';
3
+ import { requireDirectPayHumanPasskey } from '../direct-pay-guards.js';
4
+ import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam(route 层读走 async 口)
5
+ const GATE_PURPOSE = 'direct_receive_account_manage';
6
+ export function registerDirectReceiveAccountsRoutes(app, deps) {
7
+ const { db, auth, generateId, consumeGateToken } = deps;
8
+ /** 登录 + seller 角色。返回 user 或 null(已写响应)。 */
9
+ function requireSeller(req, res) {
10
+ const user = auth(req, res);
11
+ if (!user)
12
+ return null;
13
+ if (user.role !== 'seller') {
14
+ res.status(403).json({ error: '仅卖家可管理直付收款账号', error_code: 'SELLER_ONLY' });
15
+ return null;
16
+ }
17
+ return user;
18
+ }
19
+ /** 现场真人 Passkey 门(写操作)。action(+可选 account_id)绑 purpose_data。返回 true=通过(否则已写 403)。 */
20
+ function passkeyOk(req, res, userId, action, accountId) {
21
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
22
+ userId, webauthnToken: req.body?.webauthn_token,
23
+ purpose: GATE_PURPOSE,
24
+ validate: (data) => {
25
+ const d = data;
26
+ if (!d || d.action !== action)
27
+ return false;
28
+ return accountId ? d.account_id === accountId : true;
29
+ },
30
+ });
31
+ if (!gate.ok) {
32
+ res.status(403).json({ error: gate.reason, error_code: gate.error_code });
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+ /** owner-scoped 取账号;不存在/非本人 → null + 404(不枚举)。 */
38
+ function ownedAccount(res, id, sellerId) {
39
+ const acc = getAccount(db, id);
40
+ if (!acc || acc.seller_id !== sellerId) {
41
+ res.status(404).json({ error: '账号不存在', error_code: 'ACCOUNT_NOT_FOUND' });
42
+ return null;
43
+ }
44
+ return acc;
45
+ }
46
+ // ── list(本人;不返回 raw QR,只含 qr_image_ref)──
47
+ app.get('/api/direct-receive/accounts', (req, res) => {
48
+ const user = requireSeller(req, res);
49
+ if (!user)
50
+ return;
51
+ res.json({ accounts: listSellerAccounts(db, user.id, { includeInactive: true }) });
52
+ });
53
+ // ── buyer-facing:某商品卖家的【可选收款账号】(结算前选"怎么付")。任意登录用户可读;字面路径,不与 /accounts/:id 冲突。
54
+ // ⚠️ 只下发元数据 method/currency/label —— instruction 原文与 QR 受披露门保护,D1/D2 ack 后才随订单快照给买家。
55
+ app.get('/api/direct-receive/selectable-accounts', async (req, res) => {
56
+ const user = auth(req, res);
57
+ if (!user)
58
+ return;
59
+ const productId = String(req.query.product_id || '');
60
+ if (!productId)
61
+ return void res.status(400).json({ error: '缺少 product_id', error_code: 'PRODUCT_ID_REQUIRED' });
62
+ const product = await dbOne('SELECT seller_id, status FROM products WHERE id = ?', [productId]);
63
+ if (!product || product.status === 'deleted')
64
+ return void res.status(404).json({ error: '商品不存在', error_code: 'PRODUCT_NOT_FOUND' });
65
+ res.json({ options: listSellerAccountOptions(db, product.seller_id) });
66
+ });
67
+ // ── add(Passkey)──
68
+ app.post('/api/direct-receive/accounts', (req, res) => {
69
+ const user = requireSeller(req, res);
70
+ if (!user)
71
+ return;
72
+ if (!passkeyOk(req, res, user.id, 'add'))
73
+ return;
74
+ const b = req.body || {};
75
+ const out = db.transaction(() => {
76
+ const r = addAccount(db, user.id, { method: b.method, currency: b.currency, instruction: b.instruction, label: b.label }, generateId);
77
+ if (r.ok)
78
+ appendAccountEvent(db, { accountId: r.account.id, sellerId: user.id, eventType: 'account_added' }, generateId);
79
+ return r;
80
+ })();
81
+ if (!out.ok)
82
+ return void res.status(400).json({ error: out.reason, error_code: 'ACCOUNT_INPUT_INVALID' });
83
+ res.json({ ok: true, account: out.account });
84
+ });
85
+ // ── update(Passkey + owner)──
86
+ app.put('/api/direct-receive/accounts/:id', (req, res) => {
87
+ const user = requireSeller(req, res);
88
+ if (!user)
89
+ return;
90
+ const id = String(req.params.id);
91
+ if (!ownedAccount(res, id, user.id))
92
+ return;
93
+ if (!passkeyOk(req, res, user.id, 'update', id))
94
+ return;
95
+ const b = req.body || {};
96
+ const out = db.transaction(() => {
97
+ const r = updateAccount(db, id, user.id, { method: b.method, currency: b.currency, instruction: b.instruction, label: b.label });
98
+ if (r.ok && r.changed)
99
+ appendAccountEvent(db, { accountId: id, sellerId: user.id, eventType: 'account_updated' }, generateId);
100
+ return r;
101
+ })();
102
+ if (!out.ok)
103
+ return void res.status(400).json({ error: out.reason, error_code: 'ACCOUNT_INPUT_INVALID' });
104
+ res.json({ ok: true, changed: out.changed });
105
+ });
106
+ // ── deactivate(Passkey + owner)──
107
+ app.delete('/api/direct-receive/accounts/:id', (req, res) => {
108
+ const user = requireSeller(req, res);
109
+ if (!user)
110
+ return;
111
+ const id = String(req.params.id);
112
+ if (!ownedAccount(res, id, user.id))
113
+ return;
114
+ if (!passkeyOk(req, res, user.id, 'deactivate', id))
115
+ return;
116
+ let changed = false;
117
+ db.transaction(() => {
118
+ changed = deactivateAccount(db, id, user.id);
119
+ if (changed)
120
+ appendAccountEvent(db, { accountId: id, sellerId: user.id, eventType: 'account_deactivated' }, generateId);
121
+ })();
122
+ res.json({ ok: true, changed });
123
+ });
124
+ // ── upload / replace QR(Passkey + owner;immutable content-addressed store)──
125
+ app.put('/api/direct-receive/accounts/:id/qr', (req, res) => {
126
+ const user = requireSeller(req, res);
127
+ if (!user)
128
+ return;
129
+ const id = String(req.params.id);
130
+ if (!ownedAccount(res, id, user.id))
131
+ return;
132
+ if (!passkeyOk(req, res, user.id, 'qr', id))
133
+ return;
134
+ const r = storeQrImage(db, { accountId: id, sellerId: user.id, dataUri: req.body?.qr_data_uri }, generateId);
135
+ if (!r.ok)
136
+ return void res.status(400).json({ error: r.reason, error_code: 'QR_INVALID' });
137
+ res.json({ ok: true, qr_image_ref: r.ref });
138
+ });
139
+ // ── QR preview(owner-only read;硬化转发;不存在/非本人 → 404)──
140
+ app.get('/api/direct-receive/accounts/:id/qr', (req, res) => {
141
+ const user = requireSeller(req, res);
142
+ if (!user)
143
+ return;
144
+ const acc = getAccount(db, String(req.params.id));
145
+ if (!acc || acc.seller_id !== user.id || !acc.qr_image_ref)
146
+ return void res.status(404).end();
147
+ const img = getQrImageForOwner(db, acc.qr_image_ref, user.id);
148
+ if (!img)
149
+ return void res.status(404).end();
150
+ res.setHeader('Content-Type', img.mime); // server-set from whitelist, never echoed from client
151
+ res.setHeader('X-Content-Type-Options', 'nosniff');
152
+ res.setHeader('Cache-Control', 'private, no-store'); // private seller data — not the public cache thumbnails use
153
+ res.send(img.buf);
154
+ });
155
+ }
@@ -0,0 +1,45 @@
1
+ import { getActivePaymentInstruction, setActivePaymentInstruction, deactivatePaymentInstruction, MAX_INSTRUCTION_LEN, MAX_LABEL_LEN, } from '../../direct-receive-payment-instruction.js';
2
+ export function registerDirectReceivePaymentInstructionRoutes(app, deps) {
3
+ const { db, auth, generateId } = deps;
4
+ /** 登录 + seller 角色门。返回 user 或 null(已写错误响应)。 */
5
+ function requireSeller(req, res) {
6
+ const user = auth(req, res);
7
+ if (!user)
8
+ return null;
9
+ if (user.role !== 'seller') {
10
+ res.status(403).json({ error: '仅卖家可设置收款说明', error_code: 'SELLER_ONLY' });
11
+ return null;
12
+ }
13
+ return user;
14
+ }
15
+ // GET — 卖家本人当前 active 收款说明;无则 instruction:null(200,显式空状态,便于 UI 渲染“尚未设置”)。
16
+ app.get('/api/direct-receive/payment-instruction', (req, res) => {
17
+ const user = requireSeller(req, res);
18
+ if (!user)
19
+ return;
20
+ return void res.json({ instruction: getActivePaymentInstruction(db, user.id) });
21
+ });
22
+ // PUT — 设置/替换卖家当前 active 收款说明。instruction 必填、trim、长度上限;label 可选、trim、长度上限。
23
+ app.put('/api/direct-receive/payment-instruction', (req, res) => {
24
+ const user = requireSeller(req, res);
25
+ if (!user)
26
+ return;
27
+ const instruction = typeof req.body?.instruction === 'string' ? req.body.instruction.trim() : '';
28
+ if (!instruction)
29
+ return void res.status(400).json({ error: '收款说明不能为空', error_code: 'INSTRUCTION_REQUIRED' });
30
+ if (instruction.length > MAX_INSTRUCTION_LEN)
31
+ return void res.status(400).json({ error: `收款说明过长(上限 ${MAX_INSTRUCTION_LEN} 字符)`, error_code: 'INSTRUCTION_TOO_LONG' });
32
+ const rawLabel = typeof req.body?.label === 'string' ? req.body.label.trim() : '';
33
+ if (rawLabel.length > MAX_LABEL_LEN)
34
+ return void res.status(400).json({ error: `标签过长(上限 ${MAX_LABEL_LEN} 字符)`, error_code: 'LABEL_TOO_LONG' });
35
+ const saved = setActivePaymentInstruction(db, user.id, { instruction, label: rawLabel || null }, generateId);
36
+ return void res.json({ ok: true, instruction: saved });
37
+ });
38
+ // DELETE — 停用卖家当前 active 收款说明(软停用,留历史为 inactive)。停用后 create route fail-closed。
39
+ app.delete('/api/direct-receive/payment-instruction', (req, res) => {
40
+ const user = requireSeller(req, res);
41
+ if (!user)
42
+ return;
43
+ return void res.json({ ok: true, deactivated: deactivatePaymentInstruction(db, user.id) });
44
+ });
45
+ }
@@ -1,6 +1,6 @@
1
1
  import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
2
  export function registerDisputesReadRoutes(app, deps) {
3
- const { db, auth, errorRes, getOpenDisputes, getDisputeDetails, getEvidenceRequests, listEvidenceFiles, isEligibleArbitrator } = deps;
3
+ const { db, auth, errorRes, getOpenDisputes, getDisputeDetails, getEvidenceRequests, listEvidenceFiles, isEligibleArbitrator, isArbitrationAdmin } = deps;
4
4
  // 仲裁员:查看所有开放争议
5
5
  app.get('/api/disputes', async (req, res) => {
6
6
  const user = auth(req, res);
@@ -19,8 +19,8 @@ export function registerDisputesReadRoutes(app, deps) {
19
19
  const dispute = await dbOne('SELECT id, order_id, reason, initiator_id, defendant_id FROM disputes WHERE id = ?', [req.params.id]);
20
20
  if (!dispute)
21
21
  return void res.status(404).json({ error: '争议不存在' });
22
- const role = user.role;
23
- if (dispute.initiator_id !== user.id && dispute.defendant_id !== user.id && role !== 'arbitrator') {
22
+ // PR-E fix:同详情/parties —— party 或 active whitelist 仲裁员;不再用 role === 'arbitrator'。
23
+ if (dispute.initiator_id !== user.id && dispute.defendant_id !== user.id && !isEligibleArbitrator(user.id).ok) {
24
24
  return void res.status(403).json({ error: '无权查看' });
25
25
  }
26
26
  const order = await dbOne('SELECT product_id FROM orders WHERE id = ?', [dispute.order_id]);
@@ -90,12 +90,16 @@ export function registerDisputesReadRoutes(app, deps) {
90
90
  const dispute = await getDisputeDetails(db, req.params.id);
91
91
  if (!dispute)
92
92
  return void res.status(404).json({ error: '争议不存在' });
93
- const role = user.role;
94
- // 允许:发起方、被告方、物流方、仲裁员
93
+ // PR-E:允许 发起方 / 被告方 / 物流方 / active whitelist 仲裁员。不再用 role === 'arbitrator'(旧旁路 →
94
+ // suspended/revoked/role-only 可越权读、whitelist-only 买家被错误挡)。授权源统一为 isEligibleArbitrator。
95
95
  const orderForAuth = await dbOne('SELECT logistics_id FROM orders WHERE id = ?', [dispute.order_id]);
96
96
  const isLogisticsParty = orderForAuth?.logistics_id === user.id;
97
+ // 当事方 / active 白名单仲裁员 / arbitration-admin(争议查看 —— 只读监督)。admin 门用 isArbitrationAdmin(user)
98
+ // 而非 user.role==='admin':与后台列表 /admin/disputes(requireArbitrationAdmin)【同一】授权模型,否则会错放
99
+ // (role='admin' 但无 arbitration 权限的 admin 绕过列表边界直读详情)/ 错挡(roles=["buyer","admin"] 的 admin 进不去)。
100
+ // 裁定/驳回等【动作】仍在 disputes-write 由 isEligibleArbitrator + 指派 + COI 门守,admin 只看不能裁。
97
101
  if (dispute.initiator_id !== user.id && dispute.defendant_id !== user.id
98
- && !isLogisticsParty && role !== 'arbitrator') {
102
+ && !isLogisticsParty && !isEligibleArbitrator(user.id).ok && !isArbitrationAdmin(user)) {
99
103
  return void res.status(403).json({ error: '无权查看此争议' });
100
104
  }
101
105
  // 原告证据 — 从状态机历史中取 disputed 转移时附带的
@@ -308,8 +312,13 @@ export function registerDisputesReadRoutes(app, deps) {
308
312
  });
309
313
  }
310
314
  events.sort((a, b) => String(a.ts).localeCompare(String(b.ts)));
315
+ // 仲裁员【驳回仲裁,退回协商】可达性(与 dismissDisputeToNegotiation / pq_withdraw 同谓词):direct_p2p + 最近一次进入
316
+ // disputed 是 from payment_query + 未裁定。前端据此才在裁定面板显示"驳回仲裁"选项;真正拦截仍在路由/域函数。
317
+ const lastDispFrom = await dbOne("SELECT from_status FROM order_state_history WHERE order_id = ? AND to_status = 'disputed' ORDER BY created_at DESC, rowid DESC LIMIT 1", [dispute.order_id]);
318
+ const canDismissToNegotiation = dispute.payment_rail === 'direct_p2p' && lastDispFrom?.from_status === 'payment_query' && (dispute.status === 'open' || dispute.status === 'in_review');
311
319
  res.json({
312
320
  ...dispute,
321
+ can_dismiss_to_negotiation: canDismissToNegotiation,
313
322
  plaintiff_evidence: plaintiffEvidence,
314
323
  defendant_evidence: defendantEvidence,
315
324
  party_evidence: partyEvidence,
@@ -344,6 +353,11 @@ export function registerDisputesReadRoutes(app, deps) {
344
353
  if (!dispute)
345
354
  return void res.status(404).json({ error: '争议不存在' });
346
355
  const order = await dbOne('SELECT buyer_id, seller_id, logistics_id FROM orders WHERE id = ?', [dispute.order_id]);
356
+ // PR-E:同详情权限门 —— 仅 涉案方 或 active whitelist 仲裁员可读涉案方名单(此前任意登录用户可读=泄露)。
357
+ const isParty = [order?.buyer_id, order?.seller_id, order?.logistics_id, dispute.initiator_id, dispute.defendant_id].filter(Boolean).includes(user.id);
358
+ if (!isParty && !isEligibleArbitrator(user.id).ok) {
359
+ return void res.status(403).json({ error: '无权查看涉案方' });
360
+ }
347
361
  const partyIds = [dispute.initiator_id, dispute.defendant_id, order?.logistics_id].filter(Boolean);
348
362
  const uniqueIds = [...new Set(partyIds)];
349
363
  const parties = (await Promise.all(uniqueIds.map(id => dbOne('SELECT id, name, role FROM users WHERE id = ?', [id])))).filter(Boolean);
@@ -7,8 +7,33 @@ import { applyWalletDelta } from '../../ledger.js';
7
7
  // RFC-016 Phase 1 — 纯只读端点/校验读/SNF 分发读/标记写 → async seam;arbitrate 仲裁核心(原子领取 +
8
8
  // 2 settlement db.transaction + reputation/strike/publish)与 tx 内 appendAuditLog 保持同步(Phase 3 迁 pg)。
9
9
  import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
10
+ import { arbitratorHasConflict } from '../arbitrator-lifecycle.js'; // PR-B COI 硬门
11
+ import { dismissDisputeToNegotiation } from '../../layer3-trust/L3-1-dispute-engine/dispute-engine.js'; // 仲裁员驳回仲裁,退回协商(非胜负裁决)
10
12
  export function registerDisputesWriteRoutes(app, deps) {
11
- const { db, auth, generateId, detectFraud, errorRes, isEligibleArbitrator, requireHumanPresence, getDisputeDetails, respondToDispute, arbitrateDispute, addPartyEvidence, requestEvidence, markEvidenceExpiry, uploadEvidence, EVIDENCE_MAX_BYTES, EVIDENCE_ALLOWED_MIME, appendOrderEvent, FUND_BASE_RATE, settleCommission, depositToFund, calculatePv, recordDisputeReputation, issueAgentStrike, publishDisputeCase, logAdminAction, snfSend, getProtocolParam } = deps;
13
+ const { db, auth, generateId, detectFraud, errorRes, isEligibleArbitrator, requireHumanPresence, getDisputeDetails, respondToDispute, arbitrateDispute, addPartyEvidence, requestEvidence, markEvidenceExpiry, uploadEvidence, EVIDENCE_MAX_BYTES, EVIDENCE_ALLOWED_MIME, appendOrderEvent, FUND_BASE_RATE, settleCommission, depositToFund, calculatePv, recordDisputeReputation, issueAgentStrike, publishDisputeCase, logAdminAction, snfSend, getProtocolParam, notifyTransition } = deps;
14
+ // PR-E:案件级仲裁操作(裁定 / 补证)统一的【原子领取或校验指派】。assigned 为空 → CAS 首位领取;
15
+ // 非空且不含 caller → NOT_ASSIGNED_ARBITRATOR。arbitrate 与 request-evidence 共用,防"任意 active 仲裁员操作任意案"。
16
+ const claimOrCheckAssignment = (disputeId, userId) => {
17
+ const row = db.prepare('SELECT assigned_arbitrators FROM disputes WHERE id = ?').get(disputeId);
18
+ let assigned = [];
19
+ try {
20
+ assigned = JSON.parse(row?.assigned_arbitrators || '[]');
21
+ }
22
+ catch { }
23
+ if (assigned.length === 0) {
24
+ const claim = db.prepare(`UPDATE disputes SET assigned_arbitrators = ? WHERE id = ? AND (assigned_arbitrators IS NULL OR assigned_arbitrators = '[]')`).run(JSON.stringify([userId]), disputeId);
25
+ if (claim.changes === 0) {
26
+ const fresh = db.prepare('SELECT assigned_arbitrators FROM disputes WHERE id = ?').get(disputeId);
27
+ try {
28
+ assigned = JSON.parse(fresh?.assigned_arbitrators || '[]');
29
+ }
30
+ catch { }
31
+ }
32
+ else
33
+ assigned = [userId];
34
+ }
35
+ return assigned.includes(userId) ? { ok: true } : { ok: false, error_code: 'NOT_ASSIGNED_ARBITRATOR' };
36
+ };
12
37
  // ── RFC-007 stage 5:客观拒单【临时判责】的仲裁翻案 ─────────────────────────────
13
38
  // 卖家 contest_decline 后,订单 = fault_seller + decline_objective_pending=1 + decline_contested=1(未结算)。
14
39
  // 仲裁员(真实人工 + WebAuthn)裁决:uphold → declined_nofault(免责全退+退质押);reject → 违约结算。
@@ -117,7 +142,7 @@ export function registerDisputesWriteRoutes(app, deps) {
117
142
  const { ruling, reason, refund_amount, liable_party_id, liability_parties } = req.body;
118
143
  if (!ruling || !reason)
119
144
  return void res.json({ error: '请提供裁定结果(ruling)和理由(reason)' });
120
- const validRulings = ['refund_buyer', 'release_seller', 'partial_refund', 'liability_split'];
145
+ const validRulings = ['refund_buyer', 'release_seller', 'partial_refund', 'liability_split', 'dismiss_to_negotiation'];
121
146
  if (!validRulings.includes(ruling)) {
122
147
  return void res.json({ error: `ruling 必须是 ${validRulings.join(' / ')} 之一` });
123
148
  }
@@ -134,32 +159,52 @@ export function registerDisputesWriteRoutes(app, deps) {
134
159
  const dispute = await getDisputeDetails(db, req.params.id);
135
160
  if (!dispute)
136
161
  return void res.status(404).json({ error: '争议不存在' });
137
- // P0: 防"任意仲裁员裁决任意争议"
138
- // assigned_arbitrators 为空 首位调用者原子领取
139
- const arbRow = db.prepare(`SELECT assigned_arbitrators FROM disputes WHERE id = ?`).get(req.params.id);
140
- let assignedArbitrators = [];
141
- try {
142
- assignedArbitrators = JSON.parse(arbRow?.assigned_arbitrators || '[]');
143
- }
144
- catch { }
145
- if (assignedArbitrators.length === 0) {
146
- const claimRes = db.prepare(`UPDATE disputes SET assigned_arbitrators = ? WHERE id = ? AND (assigned_arbitrators IS NULL OR assigned_arbitrators = '[]')`)
147
- .run(JSON.stringify([user.id]), req.params.id);
148
- if (claimRes.changes === 0) {
149
- const fresh = db.prepare(`SELECT assigned_arbitrators FROM disputes WHERE id = ?`).get(req.params.id);
150
- try {
151
- assignedArbitrators = JSON.parse(fresh?.assigned_arbitrators || '[]');
152
- }
153
- catch { }
162
+ // PR-B COI 硬门:当事方(买家/卖家/物流/发起人/被诉人)不得仲裁本案。在领取/裁定前拦截。
163
+ if (arbitratorHasConflict(db, dispute.order_id, dispute.initiator_id ?? null, dispute.defendant_id ?? null, user.id)) {
164
+ return void res.status(403).json({ error: '你是本案当事方,不可仲裁(利益冲突)', error_code: 'ARBITRATOR_CONFLICT_OF_INTEREST' });
165
+ }
166
+ // P0: 防"任意仲裁员裁决任意争议" —— 原子领取或校验指派(与 request-evidence 共用 helper)。
167
+ const claim = claimOrCheckAssignment(req.params.id, user.id);
168
+ if (!claim.ok) {
169
+ return void res.status(403).json({ error: '此争议未分配给你 — 仅指派的仲裁员可裁定', error_code: claim.error_code || 'NOT_ASSIGNED_ARBITRATOR' });
170
+ }
171
+ // 仲裁员【驳回仲裁,退回协商】—— 非胜负裁决:不结算/不退款/不收仲裁费/不扣信誉/不动 escrow。仅 direct_p2p 且由 payment_query 升级来的争议;
172
+ // 域函数硬校验 rail + payment_query-origin + 未裁定,否则 NOT_PAYMENT_QUERY_DISPUTE / ARBITRATION_DISMISS_NOT_ALLOWED。状态回退 + dispute 关闭同一原子事务。
173
+ if (ruling === 'dismiss_to_negotiation') {
174
+ let dm;
175
+ try {
176
+ dm = db.transaction(() => dismissDisputeToNegotiation(db, req.params.id, reason))();
177
+ }
178
+ catch (e) {
179
+ return void errorRes(res, 500, 'DISMISS_FAILED', e.message);
154
180
  }
155
- else {
156
- assignedArbitrators = [user.id];
181
+ if (!dm.success) {
182
+ const st = dm.error_code === 'DISPUTE_NOT_FOUND' ? 404 : (dm.error_code === 'SYS_MISSING' || dm.error_code === 'TRANSITION_FAILED') ? 500 : 409;
183
+ return void errorRes(res, st, dm.error_code || 'DISMISS_FAILED', dm.error || '驳回仲裁失败');
157
184
  }
185
+ try {
186
+ appendOrderEvent(db, { orderId: dm.order_id, eventType: 'transition', fromStatus: 'disputed', toStatus: 'payment_query', actorId: user.id, actorRole: 'arbitrator', extra: { action: 'dismiss_to_negotiation', dispute_id: req.params.id, reason, non_custodial: true } });
187
+ }
188
+ catch (e) {
189
+ console.warn('[order-chain] dismiss event:', e.message);
190
+ }
191
+ try {
192
+ markEvidenceExpiry(db, req.params.id);
193
+ }
194
+ catch (e) {
195
+ console.warn('[evidence] mark expiry:', e.message);
196
+ }
197
+ try {
198
+ notifyTransition(db, dm.order_id, 'disputed', 'payment_query');
199
+ }
200
+ catch (e) {
201
+ console.warn('[notify] dismiss:', e.message);
202
+ }
203
+ return void res.json({ success: true, status: 'payment_query', dispute_dismissed: true, returned_to_negotiation: true, message: '已驳回仲裁,退回买卖双方协商' });
158
204
  }
159
- if (!assignedArbitrators.includes(user.id)) {
160
- return void res.status(403).json({ error: '此争议未分配给你 仅指派的仲裁员可裁定', error_code: 'NOT_ASSIGNED_ARBITRATOR' });
161
- }
162
- // 协议层:仲裁员签名的 ruling 入订单链
205
+ // 协议层:仲裁员签名的 ruling 入订单链。direct_p2p 非托管=仅信誉裁决,绝不把请求里的退款/赔付金额写进【签名链/timeline】
206
+ // (即便资金层不动钱,签名链会永久留假语义) 强制 non_custodial + 金额/责任方全清。
207
+ const ncRail = dispute.payment_rail === 'direct_p2p';
163
208
  try {
164
209
  appendOrderEvent(db, {
165
210
  orderId: dispute.order_id,
@@ -172,9 +217,10 @@ export function registerDisputesWriteRoutes(app, deps) {
172
217
  action: 'arbitration_ruling',
173
218
  dispute_id: req.params.id,
174
219
  ruling, reason,
175
- refund_amount: refund_amount ? Number(refund_amount) : null,
176
- liable_party_id: liable_party_id || null,
177
- liability_parties: liability_parties || null,
220
+ non_custodial: ncRail || undefined,
221
+ refund_amount: ncRail ? null : (refund_amount ? Number(refund_amount) : null),
222
+ liable_party_id: ncRail ? null : (liable_party_id || null),
223
+ liability_parties: ncRail ? null : (liability_parties || null),
178
224
  },
179
225
  });
180
226
  }
@@ -191,8 +237,10 @@ export function registerDisputesWriteRoutes(app, deps) {
191
237
  catch (e) {
192
238
  console.warn('[evidence] mark expiry:', e.message);
193
239
  }
240
+ // 非托管(直付)争议:协议不持货款、不做托管结算 → 【跳过】所有佣金/PV/基金分润钩子(否则会从卖家余额扣不存在的佣金)。
241
+ const nonCustodial = !!result.non_custodial;
194
242
  // release_seller 等同正常完成 → 触发推土机分润 + 原子能
195
- if (ruling === 'release_seller') {
243
+ if (ruling === 'release_seller' && !nonCustodial) {
196
244
  try {
197
245
  db.transaction(() => {
198
246
  const order = db.prepare("SELECT * FROM orders WHERE id = ?").get(dispute.order_id);
@@ -233,8 +281,8 @@ export function registerDisputesWriteRoutes(app, deps) {
233
281
  console.error('[dispute commission/pv hook]', e);
234
282
  }
235
283
  }
236
- // Bug-A fix:partial_refund / liability_split 也按 effectiveBase 发 commission/PV/基金池
237
- if (ruling === 'partial_refund' || ruling === 'liability_split') {
284
+ // Bug-A fix:partial_refund / liability_split 也按 effectiveBase 发 commission/PV/基金池(非托管跳过)
285
+ if ((ruling === 'partial_refund' || ruling === 'liability_split') && !nonCustodial) {
238
286
  try {
239
287
  db.transaction(() => {
240
288
  const order = db.prepare("SELECT * FROM orders WHERE id = ?").get(dispute.order_id);
@@ -519,13 +567,23 @@ export function registerDisputesWriteRoutes(app, deps) {
519
567
  }
520
568
  });
521
569
  // 仲裁员:请求某方补证
522
- app.post('/api/disputes/:id/request-evidence', (req, res) => {
570
+ app.post('/api/disputes/:id/request-evidence', async (req, res) => {
523
571
  const user = auth(req, res);
524
572
  if (!user)
525
573
  return;
574
+ // PR-E:① active whitelist ② COI(当事方不可补证本案,与 arbitrate 同门) ③ 已分配/领取该案(claim-if-empty)④ 涉案方目标(engine)。
526
575
  const elig = isEligibleArbitrator(user.id);
527
576
  if (!elig.ok)
528
577
  return void errorRes(res, 403, 'NOT_ARBITRATOR', elig.reason || '仅限仲裁员');
578
+ const disp = await getDisputeDetails(db, req.params.id);
579
+ if (!disp)
580
+ return void res.status(404).json({ error: '争议不存在' });
581
+ if (arbitratorHasConflict(db, disp.order_id, disp.initiator_id ?? null, disp.defendant_id ?? null, user.id)) {
582
+ return void res.status(403).json({ error: '你是本案当事方,不可补证(利益冲突)', error_code: 'ARBITRATOR_CONFLICT_OF_INTEREST' });
583
+ }
584
+ const claim = claimOrCheckAssignment(req.params.id, user.id);
585
+ if (!claim.ok)
586
+ return void errorRes(res, 403, claim.error_code || 'NOT_ASSIGNED_ARBITRATOR', '此争议未分配给你 — 仅指派的仲裁员可补证');
529
587
  const { requested_from_id, evidence_types, description, deadline_hours = 48 } = req.body;
530
588
  if (!requested_from_id || !description)
531
589
  return void res.json({ error: '请指定被要求方和证据要求说明' });
@@ -538,7 +596,7 @@ export function registerDisputesWriteRoutes(app, deps) {
538
596
  }
539
597
  const result = requestEvidence(db, req.params.id, user.id, requested_from_id, evidence_types, description, Number(deadline_hours));
540
598
  if (!result.success)
541
- return void res.json({ error: result.error });
599
+ return void res.json({ error: result.error, error_code: result.error_code });
542
600
  res.json({ success: true, request_id: result.requestId });
543
601
  });
544
602
  // ─── task #1093 stage 6: arbitrator_pause / resume auto_judge ─────
@@ -1,5 +1,6 @@
1
1
  import { dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
2
  import { createAnchor, verifyAnchorSignature, revokeAnchor, issueOwnershipToken, submitVerification, getAnchor, listAnchorsByProduct, listAnchorsBySeller, distributeAnchorRewards, ANCHOR_VERIFICATION_FEE_RECOMMENDED, } from '../../layer1-agent/L1-2-external-anchor/anchor-engine.js';
3
+ import { isEligibleArbitrator } from '../arbitrator-lifecycle.js'; // 仲裁能力唯一源=active 白名单(distribute 是动钱动作,不认 legacy role)
3
4
  export function registerExternalAnchorsRoutes(app, deps) {
4
5
  const { db, auth } = deps;
5
6
  app.post('/api/external-anchors', (req, res) => {
@@ -42,12 +43,13 @@ export function registerExternalAnchorsRoutes(app, deps) {
42
43
  total_paid_out: verifications.reduce((s, v) => s + Number(v.reward_amount || 0), 0),
43
44
  });
44
45
  });
45
- // 手动 distribute(admin/arbitrator 补救:anchor 已 community 但 fee_paid_out=0
46
+ // 手动 distribute(admin/白名单仲裁员 补救:anchor 已 community 但 fee_paid_out=0)——动钱动作,仲裁员认 active
47
+ // 白名单(isEligibleArbitrator),不认 legacy user.role(否则已 suspend/revoke 但 role 未同步的账号仍可触发放款)。
46
48
  app.post('/api/external-anchors/:id/distribute-rewards', (req, res) => {
47
49
  const user = auth(req, res);
48
50
  if (!user)
49
51
  return;
50
- if (user.role !== 'admin' && user.role !== 'arbitrator')
52
+ if (user.role !== 'admin' && !isEligibleArbitrator(db, user.id).ok)
51
53
  return void res.status(403).json({ error: '仅管理员/仲裁员可手动分发' });
52
54
  const paid = distributeAnchorRewards(db, req.params.id);
53
55
  res.json({ ok: true, paid });
@@ -0,0 +1,66 @@
1
+ import { listActivePlatformAccounts } from '../../platform-receive-accounts.js';
2
+ import { createFeePrepayRequest, listSellerRequests, cancelRequest } from '../../direct-pay-fee-prepay-request.js';
3
+ // N3:申请提交 → 通知有审批权的 admin(approve/reject 是 ROOT 门,只通知 root;regional 收到也办不了)。
4
+ import { createNotification } from '../../layer2-business/L2-6-notifications/notification-engine.js';
5
+ import { dbAll } from '../../layer0-foundation/L0-1-database/db.js';
6
+ export function registerFeePrepayRequestRoutes(app, deps) {
7
+ const { db, auth, generateId } = deps;
8
+ /** 登录 + seller 角色。返回 user 或 null(已写响应)。 */
9
+ function requireSeller(req, res) {
10
+ const user = auth(req, res);
11
+ if (!user)
12
+ return null;
13
+ if (user.role !== 'seller') {
14
+ res.status(403).json({ error: '仅卖家可申请平台服务费预充值', error_code: 'SELLER_ONLY' });
15
+ return null;
16
+ }
17
+ return user;
18
+ }
19
+ // ── 卖家看平台收款方式(active;含 instruction + qr_data_uri —— 平台公开收款明细,据此付款)──
20
+ app.get('/api/direct-receive/platform-receive-accounts', (req, res) => {
21
+ const user = requireSeller(req, res);
22
+ if (!user)
23
+ return;
24
+ res.json({ accounts: listActivePlatformAccounts(db) });
25
+ });
26
+ // ── 提交预充值申请(不 Passkey;凭据必填)──
27
+ app.post('/api/direct-receive/fee-prepay-request', async (req, res) => {
28
+ const user = requireSeller(req, res);
29
+ if (!user)
30
+ return;
31
+ const b = req.body || {};
32
+ const r = createFeePrepayRequest(db, user.id, {
33
+ amountUnits: Number(b.amount_units), currency: b.currency, platformAccountId: b.platform_account_id,
34
+ evidenceRef: b.evidence_ref, evidenceNote: b.evidence_note,
35
+ }, generateId);
36
+ if (!r.ok)
37
+ return void res.status(400).json({ error: r.reason, error_code: 'FEE_PREPAY_REQUEST_INVALID' });
38
+ // N3:通知 root admin 有待审申请(此前提交后无人收到任何未读信号,全靠人肉翻队列)。best-effort 不阻断。
39
+ try {
40
+ const roots = await dbAll("SELECT id FROM users WHERE role = 'admin' AND (admin_type = 'root' OR admin_type IS NULL)");
41
+ const amountUsdc = Number(b.amount_units) / 1_000_000;
42
+ for (const a of roots) {
43
+ createNotification(db, a.id, null, 'dp_fee_prepay_requested', '💳 新预充值申请待审', `卖家 ${String(user.name ?? user.id)} 申请平台服务费预充值 ${amountUsdc} USDC,请到 admin 后台核对到账后处理。`, { templateKey: 'dp_fee_prepay_requested', params: { seller: String(user.name ?? user.id), amount: amountUsdc } });
44
+ }
45
+ }
46
+ catch (e) {
47
+ console.warn('[fee-prepay-request notify]', e.message);
48
+ }
49
+ res.json({ ok: true, request: r.request });
50
+ });
51
+ // ── 卖家看自己的申请(全状态)──
52
+ app.get('/api/direct-receive/fee-prepay-requests', (req, res) => {
53
+ const user = requireSeller(req, res);
54
+ if (!user)
55
+ return;
56
+ res.json({ requests: listSellerRequests(db, user.id) });
57
+ });
58
+ // ── 卖家撤销自己的 pending 申请 ──
59
+ app.post('/api/direct-receive/fee-prepay-request/:id/cancel', (req, res) => {
60
+ const user = requireSeller(req, res);
61
+ if (!user)
62
+ return;
63
+ const changed = cancelRequest(db, String(req.params.id), user.id);
64
+ res.json({ ok: true, changed });
65
+ });
66
+ }
@@ -0,0 +1,12 @@
1
+ import { getUsdRates, SUPPORTED_CURRENCIES } from '../../fx-rates.js';
2
+ export function registerFxRoutes(app, deps) {
3
+ const { rateLimitOk } = deps;
4
+ app.get('/api/fx/rates', async (req, res) => {
5
+ const ip = req.ip || 'unknown';
6
+ if (!rateLimitOk(`fx:${ip}`, 60, 60_000))
7
+ return void res.status(429).json({ error: 'rate-limited' });
8
+ const snap = await getUsdRates();
9
+ res.setHeader('Cache-Control', 'public, max-age=300');
10
+ res.json({ ...snap, currencies: SUPPORTED_CURRENCIES });
11
+ });
12
+ }