@seasonkoh/webaz 0.1.29 → 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 (126) hide show
  1. package/dist/bond-refund-blockers.js +50 -0
  2. package/dist/bond-slash.js +100 -0
  3. package/dist/bond-terms.js +31 -0
  4. package/dist/direct-pay-bond-rail-clearance.js +10 -5
  5. package/dist/direct-pay-cancel-refund.js +160 -0
  6. package/dist/direct-pay-create.js +78 -9
  7. package/dist/direct-pay-disclosures.js +17 -11
  8. package/dist/direct-pay-fee-ar.js +2 -2
  9. package/dist/direct-pay-fee-prepay-request.js +80 -0
  10. package/dist/direct-pay-returns.js +104 -0
  11. package/dist/direct-pay-stock.js +9 -0
  12. package/dist/direct-receive-deferral.js +30 -1
  13. package/dist/direct-receive-deposits.js +122 -17
  14. package/dist/free-shipping.js +37 -0
  15. package/dist/fx-rates.js +7 -0
  16. package/dist/layer0-foundation/L0-1-database/schema.js +246 -0
  17. package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
  18. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +73 -0
  19. package/dist/layer1-agent/L1-1-mcp-server/server.js +81 -16
  20. package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
  21. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
  22. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
  23. package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
  24. package/dist/platform-receive-accounts.js +94 -0
  25. package/dist/pwa/arbitration-read-admin.js +37 -0
  26. package/dist/pwa/arbitrator-lifecycle.js +100 -0
  27. package/dist/pwa/contract-fingerprint.js +15 -0
  28. package/dist/pwa/direct-pay-order-redaction.js +20 -2
  29. package/dist/pwa/human-presence.js +2 -6
  30. package/dist/pwa/public/app-account.js +9 -9
  31. package/dist/pwa/public/app-admin-disputes.js +55 -0
  32. package/dist/pwa/public/app-agent-appeal.js +90 -0
  33. package/dist/pwa/public/app-agent-approvals.js +93 -0
  34. package/dist/pwa/public/app-agent-pair.js +127 -0
  35. package/dist/pwa/public/app-arbitrator-admin.js +87 -0
  36. package/dist/pwa/public/app-arbitrator-entry.js +9 -0
  37. package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
  38. package/dist/pwa/public/app-bond-refund-ui.js +66 -0
  39. package/dist/pwa/public/app-bond-slash-ui.js +74 -0
  40. package/dist/pwa/public/app-bond-terms-ui.js +23 -0
  41. package/dist/pwa/public/app-bond-ui.js +108 -0
  42. package/dist/pwa/public/app-chat-poll.js +6 -6
  43. package/dist/pwa/public/app-contribution-hub.js +23 -0
  44. package/dist/pwa/public/app-direct-pay-accounts.js +1 -1
  45. package/dist/pwa/public/app-direct-pay-buyer.js +1 -1
  46. package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
  47. package/dist/pwa/public/app-direct-pay-copy.js +12 -0
  48. package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
  49. package/dist/pwa/public/app-direct-pay-fee-ops.js +1 -1
  50. package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
  51. package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
  52. package/dist/pwa/public/app-direct-pay-memo.js +14 -0
  53. package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
  54. package/dist/pwa/public/app-direct-pay-pay.js +15 -0
  55. package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
  56. package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
  57. package/dist/pwa/public/app-direct-pay-returns.js +56 -0
  58. package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
  59. package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
  60. package/dist/pwa/public/app-direct-pay.js +36 -37
  61. package/dist/pwa/public/app-dispute-close-ui.js +38 -0
  62. package/dist/pwa/public/app-free-shipping-ui.js +29 -0
  63. package/dist/pwa/public/app-gmv-rail-split.js +12 -0
  64. package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
  65. package/dist/pwa/public/app-mutual-cancel.js +54 -0
  66. package/dist/pwa/public/app-notif-templates-orders.js +43 -0
  67. package/dist/pwa/public/app-notif-templates.js +22 -0
  68. package/dist/pwa/public/app-order-accept-ui.js +158 -0
  69. package/dist/pwa/public/app-order-errors.js +50 -0
  70. package/dist/pwa/public/app-order-labels.js +19 -0
  71. package/dist/pwa/public/app-order-rail-filter.js +26 -0
  72. package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
  73. package/dist/pwa/public/app-poll-governor.js +22 -0
  74. package/dist/pwa/public/app-profile.js +4 -4
  75. package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
  76. package/dist/pwa/public/app-sale-regions-ui.js +38 -0
  77. package/dist/pwa/public/app-seller.js +1 -1
  78. package/dist/pwa/public/app-trade-tax-ui.js +33 -0
  79. package/dist/pwa/public/app.js +133 -160
  80. package/dist/pwa/public/i18n.js +675 -5
  81. package/dist/pwa/public/index.html +41 -0
  82. package/dist/pwa/public/openapi.json +709 -11
  83. package/dist/pwa/public/style.css +3 -0
  84. package/dist/pwa/routes/admin-direct-receive-deposits.js +215 -3
  85. package/dist/pwa/routes/admin-protocol-params.js +16 -0
  86. package/dist/pwa/routes/admin-reports.js +31 -5
  87. package/dist/pwa/routes/agent-governance.js +1 -1
  88. package/dist/pwa/routes/agent-grants.js +253 -32
  89. package/dist/pwa/routes/analytics.js +2 -0
  90. package/dist/pwa/routes/arbitrator.js +67 -14
  91. package/dist/pwa/routes/bond-seller.js +162 -0
  92. package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
  93. package/dist/pwa/routes/direct-pay-disclosure-acks.js +9 -4
  94. package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
  95. package/dist/pwa/routes/direct-pay-returns.js +74 -0
  96. package/dist/pwa/routes/direct-pay-timeouts.js +186 -9
  97. package/dist/pwa/routes/disputes-read.js +20 -6
  98. package/dist/pwa/routes/disputes-write.js +91 -33
  99. package/dist/pwa/routes/external-anchors.js +4 -2
  100. package/dist/pwa/routes/fee-prepay-requests.js +66 -0
  101. package/dist/pwa/routes/governance-onboarding.js +46 -8
  102. package/dist/pwa/routes/logistics.js +4 -4
  103. package/dist/pwa/routes/me-data.js +2 -2
  104. package/dist/pwa/routes/mutual-cancel.js +62 -0
  105. package/dist/pwa/routes/orders-action.js +182 -9
  106. package/dist/pwa/routes/orders-create.js +18 -7
  107. package/dist/pwa/routes/orders-read.js +76 -14
  108. package/dist/pwa/routes/platform-receive-accounts.js +111 -0
  109. package/dist/pwa/routes/products-create.js +13 -4
  110. package/dist/pwa/routes/products-update.js +15 -1
  111. package/dist/pwa/routes/profile-identity.js +4 -2
  112. package/dist/pwa/routes/returns.js +39 -8
  113. package/dist/pwa/routes/seller-directpay-report.js +110 -0
  114. package/dist/pwa/routes/seller-quota.js +5 -1
  115. package/dist/pwa/routes/shipping-templates.js +219 -0
  116. package/dist/pwa/routes/snf.js +4 -1
  117. package/dist/pwa/routes/webauthn.js +3 -3
  118. package/dist/pwa/server.js +59 -36
  119. package/dist/runtime/agent-grant-scopes.js +69 -1
  120. package/dist/runtime/webaz-schema-helpers.js +57 -3
  121. package/dist/sale-regions.js +116 -0
  122. package/dist/shipping-templates.js +119 -0
  123. package/dist/trade-tax.js +99 -0
  124. package/dist/trade-terms.js +76 -0
  125. package/dist/version.js +1 -1
  126. package/package.json +59 -2
@@ -1,6 +1,20 @@
1
1
  import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
2
2
  import { releaseFeeStake } from '../../direct-pay-ledger.js';
3
+ import { restorePreShipDirectPayStock } from '../../direct-pay-stock.js'; // D3 库存回补唯一入口(pre-ship 放行;已出库拒绝)
4
+ import { notifyTransition, createNotification } from '../../layer2-business/L2-6-notifications/notification-engine.js';
5
+ import { expireDeferrals, listExpiringDeferrals, markDeferralReminded, suspendPrivilegeOnDeferralExpiry } from '../../direct-receive-deferral.js'; // B4:缓交到期提醒/收口
3
6
  const SYS = 'sys_protocol';
7
+ /** 货款协商买家申诉窗(天),governance 可调,默认 7d。与 request_cancel 同源 param,保证手动/自动一致。 */
8
+ function recourseDays(db) {
9
+ try {
10
+ const row = db.prepare("SELECT value FROM protocol_params WHERE key = 'direct_pay.payment_query_recourse_days'").get();
11
+ const n = Number(row?.value ?? 7);
12
+ return Number.isFinite(n) && n > 0 ? Math.max(1, n) : 7;
13
+ }
14
+ catch {
15
+ return 7;
16
+ }
17
+ }
4
18
  /** 宽限期(小时),governance 可调,默认 48h(2d)。不硬编码。 */
5
19
  function graceHours(db) {
6
20
  try {
@@ -17,16 +31,92 @@ export function runDirectPayTimeoutSweep(deps) {
17
31
  const gh = graceHours(db);
18
32
  const windowExpired = [];
19
33
  const graceCancelled = [];
34
+ const pqRecourseOpened = [];
35
+ const pqCancelled = [];
36
+ const acceptExpired = [];
37
+ const deferralReminded = [];
38
+ const deferralExpired = [];
39
+ // F. 缓交收口(B4):① 到期前提醒(param direct_pay.deferral_reminder_days,默认 3d;reminder_sent_at 去重)
40
+ // ② 过 grace 到期 → expired + 无生产 bond 则停权(有 bond=已缴清兜底不停)+ 通知。均 fail-soft 不断 cron。
41
+ try {
42
+ const remindDays = (() => { try {
43
+ const r = db.prepare("SELECT value FROM protocol_params WHERE key = 'direct_pay.deferral_reminder_days'").get();
44
+ const n = Number(r?.value ?? 3);
45
+ return Number.isFinite(n) && n > 0 ? n : 3;
46
+ }
47
+ catch {
48
+ return 3;
49
+ } })();
50
+ for (const d of listExpiringDeferrals(db, new Date().toISOString(), remindDays)) {
51
+ markDeferralReminded(db, d.id);
52
+ deferralReminded.push(d.id);
53
+ try {
54
+ createNotification(db, d.user_id, null, 'deferral_expiring_soon', '⏰ 保证金缓交即将到期', `你的缓交资格将于 ${d.expires_at} 到期。请在到期前缴纳履约保证金转正式(设置页-直付履约保证金),否则宽限期后直付资格将关闭。`, { templateKey: 'deferral_expiring_soon', params: { expires: d.expires_at } });
55
+ }
56
+ catch (e) {
57
+ console.warn('[direct-pay-timeouts] notify deferral-remind:', e.message);
58
+ }
59
+ }
60
+ for (const uid of expireDeferrals(db, new Date().toISOString()).expired) {
61
+ const suspended = suspendPrivilegeOnDeferralExpiry(db, uid);
62
+ deferralExpired.push(uid);
63
+ try {
64
+ createNotification(db, uid, null, 'deferral_expired', '🚫 保证金缓交已到期', suspended ? '缓交资格已到期且未缴纳保证金,直付资格已关闭。缴纳履约保证金并经运营确认后可重新开通;在途订单不受影响,请正常履约完成。' : '缓交资格已到期;你已缴纳保证金,直付资格不受影响。', { templateKey: 'deferral_expired', params: { closed: suspended ? 1 : 0 } });
65
+ }
66
+ catch (e) {
67
+ console.warn('[direct-pay-timeouts] notify deferral-expired:', e.message);
68
+ }
69
+ }
70
+ }
71
+ catch (e) {
72
+ console.error('[direct-pay-timeouts] deferral closure sweep:', e);
73
+ }
74
+ // E. 手动接单窗超时(v16):pending_accept 过 pending_accept_deadline → 无责取消 + 回补库存。
75
+ // 零资金(此阶段没人付过钱 —— 时序门);双方通知。WHERE 硬门:仅 now > deadline 命中。
76
+ const acceptRows = db.prepare(`
77
+ SELECT id, product_id, quantity, buyer_id, seller_id FROM orders
78
+ WHERE status = 'pending_accept'
79
+ AND payment_rail = 'direct_p2p'
80
+ AND pending_accept_deadline IS NOT NULL
81
+ AND datetime(pending_accept_deadline) < datetime('now')
82
+ LIMIT 1000
83
+ `).all();
84
+ for (const { id, product_id, quantity, buyer_id, seller_id } of acceptRows) {
85
+ let done = false;
86
+ db.transaction(() => {
87
+ const r = transition(db, id, 'cancelled', SYS, [], '手动接单:卖家超时未确认接单,系统无责取消(付款前,零资金)');
88
+ if (!r.success)
89
+ return;
90
+ restorePreShipDirectPayStock(db, { fromStatus: 'pending_accept', productId: product_id, quantity });
91
+ acceptExpired.push(id);
92
+ done = true;
93
+ })();
94
+ if (done) {
95
+ try {
96
+ createNotification(db, buyer_id, id, 'direct_pay_accept_expired', '⏰ 卖家超时未接单,订单已取消', '卖家未在接单窗口内确认,订单已自动取消 —— 你尚未付款,无需任何操作。可换商品或联系卖家后重新下单。', { templateKey: 'dp_pending_accept_expired_buyer', params: {} });
97
+ }
98
+ catch (e) {
99
+ console.warn('[direct-pay-timeouts] notify accept-expired buyer:', e.message);
100
+ }
101
+ try {
102
+ createNotification(db, seller_id, id, 'direct_pay_accept_expired', '⏰ 订单因超时未接单已取消', '你未在接单窗口内确认接单,订单已自动取消,库存已恢复。频繁超时会影响买家体验,可考虑改用自动接单或缩短响应时间。', { templateKey: 'dp_pending_accept_expired_seller', params: {} });
103
+ }
104
+ catch (e) {
105
+ console.warn('[direct-pay-timeouts] notify accept-expired seller:', e.message);
106
+ }
107
+ }
108
+ }
20
109
  // A. 付款窗口超时 → 可争议态(非静默关单)+ 退费用质押 + 设宽限期
21
110
  const windowRows = db.prepare(`
22
- SELECT id FROM orders
111
+ SELECT id, buyer_id FROM orders
23
112
  WHERE status = 'direct_pay_window'
24
113
  AND payment_rail = 'direct_p2p'
25
114
  AND direct_pay_window_deadline IS NOT NULL
26
115
  AND datetime(direct_pay_window_deadline) < datetime('now')
27
116
  LIMIT 1000
28
117
  `).all();
29
- for (const { id } of windowRows) {
118
+ for (const { id, buyer_id } of windowRows) {
119
+ let done = false;
30
120
  db.transaction(() => {
31
121
  const r = transition(db, id, 'direct_expired_unconfirmed', SYS, [], 'Rail1 直付:付款窗口超时未标记');
32
122
  if (!r.success)
@@ -34,33 +124,120 @@ export function runDirectPayTimeoutSweep(deps) {
34
124
  releaseFeeStake(db, { orderId: id }); // 费用质押退卖家(无完成,不收费)
35
125
  db.prepare(`UPDATE orders SET direct_grace_deadline = datetime('now', ?) WHERE id = ?`).run(`+${gh} hours`, id);
36
126
  windowExpired.push(id);
127
+ done = true;
37
128
  })();
129
+ // 审计项 B(N2):此前窗口静默过期,卡点付款的买家毫不知情直到 48h 后自动取消。tx 外 fail-soft。
130
+ if (done) {
131
+ try {
132
+ createNotification(db, buyer_id, id, 'direct_pay_window_expired', '⏰ 直付付款窗口已过期', `若你已付款:请在 ${gh} 小时宽限期内到订单页提交付款凭证发起争议;未付款可直接关闭订单,否则宽限期满将自动取消。`, { templateKey: 'dp_window_expired', params: { graceHours: gh } });
133
+ }
134
+ catch (e) {
135
+ console.warn('[direct-pay-timeouts] notify window-expired:', e.message);
136
+ }
137
+ }
38
138
  }
39
139
  // B. 宽限期满 → 系统关单(★ WHERE 保证仅 now>grace 命中;宽限期内绝不关,买家 →disputed 全程可用)
40
140
  const graceRows = db.prepare(`
41
- SELECT id FROM orders
141
+ SELECT id, product_id, quantity, buyer_id, seller_id FROM orders
42
142
  WHERE status = 'direct_expired_unconfirmed'
43
143
  AND payment_rail = 'direct_p2p'
44
144
  AND direct_grace_deadline IS NOT NULL
45
145
  AND datetime(direct_grace_deadline) < datetime('now')
46
146
  LIMIT 1000
47
147
  `).all();
48
- for (const { id } of graceRows) {
148
+ for (const { id, product_id, quantity, buyer_id, seller_id } of graceRows) {
149
+ let done = false;
49
150
  db.transaction(() => {
50
151
  const r = transition(db, id, 'cancelled', SYS, [], 'Rail1 直付:宽限期满,买家未付款/未升级争议,系统关单');
51
- if (r.success)
52
- graceCancelled.push(id);
152
+ if (!r.success)
153
+ return;
154
+ restorePreShipDirectPayStock(db, { fromStatus: 'direct_expired_unconfirmed', productId: product_id, quantity }); // D3 回补(pre-ship 唯一入口)
155
+ graceCancelled.push(id);
156
+ done = true;
157
+ })();
158
+ // 审计项 B(N2):自动关单也要告知双方(此前静默,买家不知单没了、卖家不知库存已回)。tx 外 fail-soft。
159
+ if (done) {
160
+ try {
161
+ createNotification(db, buyer_id, id, 'direct_pay_grace_cancelled', '🚫 直付订单已自动取消', '付款窗口与宽限期均已过且未收到你的付款标记/凭证,订单已关闭。若你确已付款,请通过订单页联系卖家协商。', { templateKey: 'dp_grace_cancelled_buyer', params: {} });
162
+ }
163
+ catch (e) {
164
+ console.warn('[direct-pay-timeouts] notify grace-cancel buyer:', e.message);
165
+ }
166
+ try {
167
+ createNotification(db, seller_id, id, 'direct_pay_grace_cancelled', '🚫 直付订单已自动取消(买家未付款)', '买家未在付款窗口+宽限期内付款,订单已自动关闭,库存已恢复。', { templateKey: 'dp_grace_cancelled_seller', params: {} });
168
+ }
169
+ catch (e) {
170
+ console.warn('[direct-pay-timeouts] notify grace-cancel seller:', e.message);
171
+ }
172
+ }
173
+ }
174
+ // D. 货款协商买家静默:payment_query 已过买家响应宽限(payment_query_deadline)且卖家未手动请求取消
175
+ // (payment_query_cancel_deadline 仍空)→ 系统【代卖家自动启动取消申请】(等价 request_cancel):
176
+ // 设 payment_query_cancel_deadline = now + recourse_days + 通知买家最后申诉窗已开。★ 不关单、不动状态、不涉资金;
177
+ // 买家窗内仍可 pq_escalate 主张已付,订单离开 payment_query 后本 sweep 与 Sweep C 均不再命中。之后由 Sweep C 窗满关单。
178
+ // 幂等:WHERE payment_query_cancel_deadline IS NULL —— 卖家已手动 request_cancel 的单不被重置,重复 cron 也不刷新窗口。
179
+ const rd = recourseDays(db);
180
+ const pqSilentRows = db.prepare(`
181
+ SELECT id, buyer_id FROM orders
182
+ WHERE status = 'payment_query'
183
+ AND payment_rail = 'direct_p2p'
184
+ AND payment_query_deadline IS NOT NULL
185
+ AND datetime(payment_query_deadline) < datetime('now')
186
+ AND payment_query_cancel_deadline IS NULL
187
+ LIMIT 1000
188
+ `).all();
189
+ for (const { id, buyer_id } of pqSilentRows) {
190
+ const res = db.prepare(`UPDATE orders SET payment_query_cancel_deadline = datetime('now', ?) WHERE id = ? AND payment_query_cancel_deadline IS NULL`).run(`+${rd} days`, id);
191
+ if (res.changes === 0)
192
+ continue; // 竞态:同 tick 已被设(卖家 request_cancel)——不重复处理
193
+ pqRecourseOpened.push(id);
194
+ try {
195
+ const dl = db.prepare("SELECT payment_query_cancel_deadline d FROM orders WHERE id = ?").get(id).d;
196
+ createNotification(db, buyer_id, id, 'payment_query_cancel_requested', '⏳ 卖家申请取消订单', `卖家称未收到货款且你未在响应期内回应,系统已代卖家发起取消。你约有 ${rd} 天:若确已付款请提供付款参考或发起举证(pq_escalate),否则订单将于 ${dl} 自动取消。直付非托管,无平台退款。`);
197
+ }
198
+ catch (e) {
199
+ console.warn('[direct-pay-timeouts] notify pq-recourse:', e.message);
200
+ }
201
+ }
202
+ // C. 货款协商:卖家已请求取消(payment_query_cancel_deadline 已设)+ 买家申诉窗满 → 系统关单 + 退费用质押。
203
+ // ★ 窗内(now < deadline)绝不关:买家全程可主张已付/升级举证(pq_escalate 回 disputed)。
204
+ const pqCancelRows = db.prepare(`
205
+ SELECT id, product_id, quantity FROM orders
206
+ WHERE status = 'payment_query'
207
+ AND payment_rail = 'direct_p2p'
208
+ AND payment_query_cancel_deadline IS NOT NULL
209
+ AND datetime(payment_query_cancel_deadline) < datetime('now')
210
+ LIMIT 1000
211
+ `).all();
212
+ for (const { id, product_id, quantity } of pqCancelRows) {
213
+ let done = false;
214
+ db.transaction(() => {
215
+ const r = transition(db, id, 'cancelled', SYS, [], 'Rail1 直付:货款协商申诉窗满,买家未回应/未升级,系统关单');
216
+ if (!r.success)
217
+ return;
218
+ releaseFeeStake(db, { orderId: id }); // 无完成不收费:退卖家费用质押
219
+ restorePreShipDirectPayStock(db, { fromStatus: 'payment_query', productId: product_id, quantity }); // D3 回补(pre-ship 唯一入口)
220
+ done = true;
53
221
  })();
222
+ if (done) {
223
+ pqCancelled.push(id);
224
+ try {
225
+ notifyTransition(db, id, 'payment_query', 'cancelled');
226
+ }
227
+ catch (e) {
228
+ console.warn('[direct-pay-timeouts] notify pq-cancel:', e.message);
229
+ }
230
+ } // 通知买卖双方(cron 系统关单也要发,route 之外)
54
231
  }
55
- return { windowExpired, graceCancelled };
232
+ return { windowExpired, graceCancelled, pqRecourseOpened, pqCancelled, acceptExpired, deferralReminded, deferralExpired };
56
233
  }
57
234
  export function startDirectPayTimeoutCron(deps) {
58
235
  const ms = 10 * 60 * 1000; // 10min — 付款窗口最短 30min,10min 粒度足够
59
236
  setInterval(() => {
60
237
  try {
61
238
  const r = runDirectPayTimeoutSweep(deps);
62
- if (r.windowExpired.length || r.graceCancelled.length) {
63
- console.log(`[direct-pay-timeouts] window→expired ${r.windowExpired.length}, grace→cancelled ${r.graceCancelled.length}`);
239
+ if (r.windowExpired.length || r.graceCancelled.length || r.pqRecourseOpened.length || r.pqCancelled.length || r.acceptExpired.length || r.deferralReminded.length || r.deferralExpired.length) {
240
+ console.log(`[direct-pay-timeouts] window→expired ${r.windowExpired.length}, grace→cancelled ${r.graceCancelled.length}, pq→recourse ${r.pqRecourseOpened.length}, pq→cancelled ${r.pqCancelled.length}, accept→expired ${r.acceptExpired.length}, deferral remind/expired ${r.deferralReminded.length}/${r.deferralExpired.length}`);
64
241
  }
65
242
  }
66
243
  catch (e) {
@@ -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
+ }