@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,160 @@
1
+ import { restorePreShipDirectPayStock } from './direct-pay-stock.js'; // 库存回补唯一入口(pre-ship 放行;已出库拒绝,走退货验收)
2
+ export function initDirectPayCancelRefundSchema(db) {
3
+ db.exec(`
4
+ CREATE TABLE IF NOT EXISTS direct_pay_cancel_requests (
5
+ id TEXT PRIMARY KEY,
6
+ order_id TEXT NOT NULL,
7
+ buyer_id TEXT NOT NULL,
8
+ seller_id TEXT NOT NULL,
9
+ status TEXT NOT NULL DEFAULT 'requested' CHECK (status IN ('requested','refund_marked','settled','declined','withdrawn')),
10
+ reason TEXT,
11
+ refund_reference TEXT,
12
+ respond_deadline TEXT NOT NULL,
13
+ seller_responded_at TEXT,
14
+ settled_at TEXT,
15
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
16
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
17
+ );
18
+ CREATE INDEX IF NOT EXISTS idx_dpcr_order ON direct_pay_cancel_requests(order_id);
19
+ `);
20
+ }
21
+ function loadOrder(db, orderId) {
22
+ return db.prepare('SELECT id, buyer_id, seller_id, status, payment_rail, product_id, quantity FROM orders WHERE id = ?').get(orderId);
23
+ }
24
+ /** 最新一条请求(任意状态)。 */
25
+ function latestRequest(db, orderId) {
26
+ return db.prepare('SELECT * FROM direct_pay_cancel_requests WHERE order_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1').get(orderId);
27
+ }
28
+ /** requested 且 respond_deadline 已过 → 视为 expired(lazy,不写库;可重新 request)。 */
29
+ function effectiveStatus(db, r) {
30
+ if (r.status === 'requested') {
31
+ const over = db.prepare("SELECT datetime(?) < datetime('now') AS o").get(r.respond_deadline);
32
+ if (over.o)
33
+ return 'expired';
34
+ }
35
+ return r.status;
36
+ }
37
+ function respondDays(db) {
38
+ try {
39
+ const p = db.prepare("SELECT value FROM protocol_params WHERE key = 'direct_pay.cancel_refund_respond_days'").get();
40
+ if (p)
41
+ return Math.max(1, Number(p.value) || 5);
42
+ }
43
+ catch { /* 表缺失 → 默认 */ }
44
+ return 5;
45
+ }
46
+ /** ① 买家发起取消退款请求。 */
47
+ export function requestCancelRefund(db, args) {
48
+ const order = loadOrder(db, args.orderId);
49
+ if (!order)
50
+ return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
51
+ if (order.buyer_id !== args.buyerId)
52
+ return { ok: false, error: '只有订单买家可发起取消退款', error_code: 'NOT_ORDER_BUYER' };
53
+ if (order.payment_rail !== 'direct_p2p')
54
+ return { ok: false, error: '仅直付订单适用取消退款握手(托管单走托管退款)', error_code: 'NOT_DIRECT_PAY' };
55
+ if (order.status !== 'accepted')
56
+ return { ok: false, error: '仅付款后、发货前(accepted)可申请取消退款', error_code: 'ORDER_NOT_ACCEPTED' };
57
+ const last = latestRequest(db, args.orderId);
58
+ if (last && ['requested', 'refund_marked'].includes(effectiveStatus(db, last)))
59
+ return { ok: false, error: '已有进行中的取消退款请求', error_code: 'REQUEST_ALREADY_OPEN' };
60
+ const n = db.prepare('SELECT COUNT(*) AS n FROM direct_pay_cancel_requests WHERE order_id = ?').get(args.orderId).n;
61
+ if (n >= 3)
62
+ return { ok: false, error: '本订单取消退款请求次数已达上限(3),请与卖家协商或发起争议', error_code: 'REQUEST_CAP_REACHED' };
63
+ const reason = typeof args.reason === 'string' ? args.reason.trim().slice(0, 200) : null;
64
+ db.prepare(`INSERT INTO direct_pay_cancel_requests (id, order_id, buyer_id, seller_id, reason, respond_deadline)
65
+ VALUES (?, ?, ?, ?, ?, datetime('now', ?))`)
66
+ .run(args.requestId, args.orderId, order.buyer_id, order.seller_id, reason, `+${respondDays(db)} days`);
67
+ return { ok: true, request: latestRequest(db, args.orderId), status: 'requested' };
68
+ }
69
+ /** ② a. 卖家拒绝(如已备货)→ 履约继续。 */
70
+ export function declineCancelRefund(db, args) {
71
+ const order = loadOrder(db, args.orderId);
72
+ if (!order)
73
+ return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
74
+ if (order.seller_id !== args.sellerId)
75
+ return { ok: false, error: '只有订单卖家可拒绝', error_code: 'NOT_ORDER_SELLER' };
76
+ const r = db.prepare(`UPDATE direct_pay_cancel_requests SET status = 'declined', seller_responded_at = datetime('now'), updated_at = datetime('now')
77
+ WHERE order_id = ? AND status = 'requested'`).run(args.orderId);
78
+ if (r.changes !== 1)
79
+ return { ok: false, error: '没有待响应的取消退款请求', error_code: 'NO_OPEN_REQUEST' };
80
+ return { ok: true, status: 'declined' };
81
+ }
82
+ /** ② b. 卖家声明已场外退款(可附退款参考)。此后买家只能 confirm 或走争议(不可 withdraw)。 */
83
+ export function markRefunded(db, args) {
84
+ const order = loadOrder(db, args.orderId);
85
+ if (!order)
86
+ return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
87
+ if (order.seller_id !== args.sellerId)
88
+ return { ok: false, error: '只有订单卖家可声明退款', error_code: 'NOT_ORDER_SELLER' };
89
+ if (order.status !== 'accepted')
90
+ return { ok: false, error: '订单已不在待发货阶段,握手已失效', error_code: 'ORDER_NOT_ACCEPTED' };
91
+ const ref = typeof args.refundReference === 'string' ? args.refundReference.trim().slice(0, 200) : null;
92
+ const r = db.prepare(`UPDATE direct_pay_cancel_requests SET status = 'refund_marked', refund_reference = ?, seller_responded_at = datetime('now'), updated_at = datetime('now')
93
+ WHERE order_id = ? AND status = 'requested'`).run(ref, args.orderId);
94
+ if (r.changes !== 1)
95
+ return { ok: false, error: '没有待响应的取消退款请求', error_code: 'NO_OPEN_REQUEST' };
96
+ return { ok: true, status: 'refund_marked' };
97
+ }
98
+ /** 买家撤回 —— 仅限卖家尚未响应(requested)。卖家已 mark_refunded 后禁撤(防"收了退款还让订单继续履约")。 */
99
+ export function withdrawCancelRefund(db, args) {
100
+ const order = loadOrder(db, args.orderId);
101
+ if (!order)
102
+ return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
103
+ if (order.buyer_id !== args.buyerId)
104
+ return { ok: false, error: '只有订单买家可撤回', error_code: 'NOT_ORDER_BUYER' };
105
+ const r = db.prepare(`UPDATE direct_pay_cancel_requests SET status = 'withdrawn', updated_at = datetime('now')
106
+ WHERE order_id = ? AND status = 'requested'`).run(args.orderId);
107
+ if (r.changes !== 1)
108
+ return { ok: false, error: '没有可撤回的请求(卖家已声明退款后不可撤回,请确认收款或发起争议)', error_code: 'NO_OPEN_REQUEST' };
109
+ return { ok: true, status: 'withdrawn' };
110
+ }
111
+ /**
112
+ * ③ 买家确认已收到场外退款 → settled + accepted→cancelled(system)+ 恢复库存。
113
+ * 【必须由路由 db.transaction 包裹】;transition 依赖注入(状态机 adapter,便于单测与防循环依赖)。
114
+ */
115
+ export function confirmRefundReceived(db, args, transition) {
116
+ const order = loadOrder(db, args.orderId);
117
+ if (!order)
118
+ return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
119
+ if (order.buyer_id !== args.buyerId)
120
+ return { ok: false, error: '只有订单买家可确认收到退款', error_code: 'NOT_ORDER_BUYER' };
121
+ // 竞态重校验:卖家已发货/已争议 → 握手失效,绝不在非 accepted 状态上关单
122
+ if (order.status !== 'accepted')
123
+ return { ok: false, error: '订单已不在待发货阶段(可能已发货/争议),握手已失效', error_code: 'ORDER_NOT_ACCEPTED' };
124
+ const sys = db.prepare("SELECT id FROM users WHERE role = 'system' LIMIT 1").get();
125
+ if (!sys)
126
+ return { ok: false, error: '系统账号缺失', error_code: 'SYS_MISSING' };
127
+ // 请求行 CAS:仅 refund_marked → settled(卖家未声明退款前买家不可单方确认关单)
128
+ const cas = db.prepare(`UPDATE direct_pay_cancel_requests SET status = 'settled', settled_at = datetime('now'), updated_at = datetime('now')
129
+ WHERE order_id = ? AND status = 'refund_marked'`).run(args.orderId);
130
+ if (cas.changes !== 1)
131
+ return { ok: false, error: '卖家尚未声明退款,不可确认', error_code: 'REFUND_NOT_MARKED' };
132
+ const t = transition(db, args.orderId, 'cancelled', sys.id, [], '直付取消退款握手:卖家已场外退款,买家确认收到 → 无责取消');
133
+ if (!t.success)
134
+ throw new Error(t.error || 'TRANSITION_FAILED'); // 抛错让路由 db.transaction 整体回滚(含上面 CAS)
135
+ // 库存回补:经唯一守卫入口(accepted=已付未发,A 类放行;已出库来源在入口被拒 —— 见 direct-pay-stock.ts 情形矩阵)
136
+ restorePreShipDirectPayStock(db, { fromStatus: 'accepted', productId: order.product_id, quantity: Number(order.quantity) || 1 });
137
+ return { ok: true, status: 'cancelled' };
138
+ }
139
+ /** 状态读取(party-gated):非当事方一律 NOT_A_PARTY,不泄露请求存在性。 */
140
+ export function getCancelRefundState(db, orderId, viewerId) {
141
+ const order = loadOrder(db, orderId);
142
+ if (!order)
143
+ return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
144
+ const isBuyer = order.buyer_id === viewerId;
145
+ const isSeller = order.seller_id === viewerId;
146
+ if (!isBuyer && !isSeller)
147
+ return { ok: false, error: '仅订单当事方可查看', error_code: 'NOT_A_PARTY' };
148
+ const last = latestRequest(db, orderId);
149
+ const eff = last ? effectiveStatus(db, last) : null;
150
+ const open = eff === 'requested' || eff === 'refund_marked';
151
+ const req = last ? { ...last, status: eff, stale: open && order.status !== 'accepted' } : null;
152
+ return {
153
+ ok: true,
154
+ request: req ?? undefined,
155
+ can_request: isBuyer && order.payment_rail === 'direct_p2p' && order.status === 'accepted' && !open,
156
+ can_respond: isSeller && eff === 'requested' && order.status === 'accepted',
157
+ can_confirm: isBuyer && eff === 'refund_marked' && order.status === 'accepted',
158
+ can_withdraw: isBuyer && eff === 'requested',
159
+ };
160
+ }
@@ -0,0 +1,145 @@
1
+ import { randomUUID, createHash } from 'crypto';
2
+ // ── allowlists(未知值 fail-closed)──
3
+ const KYB_STATUSES = new Set(['pending', 'approved', 'rejected', 'revoked']);
4
+ const SANCTIONS_STATUSES = new Set(['clear', 'flagged', 'blocked']);
5
+ const AML_RULES = new Set(['structuring', 'concentration', 'cumulative', 'crypto', 'velocity']);
6
+ const AML_SEVERITIES = new Set(['low', 'medium', 'high']);
7
+ const AML_STATUSES = new Set(['open', 'reviewing', 'cleared', 'escalated', 'str_filed']);
8
+ // expires_at 形态:reader 用 `expires_at > datetime('now')` 做【字符串比较】,datetime('now') = 'YYYY-MM-DD HH:MM:SS'。
9
+ // 任意字符串(如 'not-a-date')会被字典序当成"未来"→ 错误地通过 fail-closed reader。故 ingress 必须校验+规范化:
10
+ // 只接受 SQLite datetime 'YYYY-MM-DD HH:MM:SS' 或完整 ISO-8601,且能真实解析;ISO 一律规范化为可比的 SQLite 格式。
11
+ // SQLite UTC civil 形态 'YYYY-MM-DD HH:MM:SS';ISO 形态【强制带 timezone】(Z 或 ±hh:mm)——无 tz 会按服务器本地时区归一,拒。
12
+ const SQLITE_DT_RE = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/;
13
+ const ISO_DT_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/;
14
+ /**
15
+ * 逐字段校验【真实存在】的 civil datetime —— 用 Date.UTC 重建后逐字段回比,杜绝 JS 自动滚动
16
+ * (如 2099-02-31 → 03-03、25:00 → 次日)。把字段当 UTC 校验真实性即可(与 SQLite datetime('now') UTC 语义一致)。
17
+ */
18
+ function isRealCivil(y, mo, d, h, mi, s) {
19
+ const t = Date.UTC(y, mo - 1, d, h, mi, s);
20
+ if (!Number.isFinite(t))
21
+ return false;
22
+ const dt = new Date(t);
23
+ return dt.getUTCFullYear() === y && dt.getUTCMonth() === mo - 1 && dt.getUTCDate() === d
24
+ && dt.getUTCHours() === h && dt.getUTCMinutes() === mi && dt.getUTCSeconds() === s;
25
+ }
26
+ /**
27
+ * 校验+规范化 expiresAt:空→null(无期限,允许);合法→可比的 'YYYY-MM-DD HH:MM:SS'(UTC);非法→ false。
28
+ * 不依赖 Date.parse 的宽松解析:先正则定形、再逐字段 isRealCivil 校验(拒不存在的日期),ISO 必须显式带 tz。
29
+ */
30
+ export function normalizeExpiry(expiresAt) {
31
+ if (expiresAt === undefined || expiresAt === null || expiresAt === '')
32
+ return { ok: true, value: null };
33
+ const sm = SQLITE_DT_RE.exec(expiresAt);
34
+ if (sm) {
35
+ if (!isRealCivil(+sm[1], +sm[2], +sm[3], +sm[4], +sm[5], +sm[6]))
36
+ return { ok: false };
37
+ return { ok: true, value: expiresAt }; // 已是可比的 UTC civil 形态
38
+ }
39
+ const im = ISO_DT_RE.exec(expiresAt);
40
+ if (im) {
41
+ // 先逐字段校验【字面 civil 日期】真实存在(2099-02-31 此处即被拒,与 tz 无关)。
42
+ if (!isRealCivil(+im[1], +im[2], +im[3], +im[4], +im[5], +im[6]))
43
+ return { ok: false };
44
+ const ms = Date.parse(expiresAt); // 字面已合法 + 强制带 tz → 此处只做 tz 偏移换算得真实 UTC instant
45
+ if (!Number.isFinite(ms))
46
+ return { ok: false };
47
+ return { ok: true, value: new Date(ms).toISOString().slice(0, 19).replace('T', ' ') };
48
+ }
49
+ return { ok: false };
50
+ }
51
+ /** providerRef → 审计用短引用(sha256 前 16 hex);无则 undefined。原值不进审计明文。 */
52
+ function providerRefHash(providerRef) {
53
+ if (!providerRef)
54
+ return undefined;
55
+ return createHash('sha256').update(providerRef).digest('hex').slice(0, 16);
56
+ }
57
+ /**
58
+ * AML detail allowlist key 集 —— 与 #108 monitor 实际写入的聚合字段一致(velocity/concentration)。
59
+ * key 也走 allowlist(不止 value):杜绝把 PII 藏进 key(如 {"alice@example.com":1} / {"wallet_0x..":1})。
60
+ */
61
+ export const AML_DETAIL_KEYS = new Set(['window_hours', 'order_count', 'threshold', 'small_order_count', 'small_order_amount']);
62
+ /**
63
+ * AML detail = 仅【allowlist key + 聚合数字 value】(与 #108 monitor 纪律一致,防 PII 落库:
64
+ * 邮箱/地址/钱包/叙述性笔记无论藏在 value 还是 key 一律拒)。undefined 视为"无 detail"(合法)。
65
+ */
66
+ export function isNumericDetail(detail) {
67
+ if (detail === undefined || detail === null)
68
+ return true;
69
+ if (typeof detail !== 'object' || Array.isArray(detail))
70
+ return false;
71
+ return Object.entries(detail).every(([k, v]) => AML_DETAIL_KEYS.has(k) && typeof v === 'number' && Number.isFinite(v));
72
+ }
73
+ /**
74
+ * AML detail 的 canonical hash(key 排序后 JSON → sha256 前 16 hex),供 Passkey purpose_data 绑定【写入内容】用
75
+ * (route 与签名方用同一函数,保证"签的 detail = 写的 detail")。无 detail → 固定 'none'。
76
+ */
77
+ export function amlDetailHash(detail) {
78
+ if (detail === undefined || detail === null)
79
+ return 'none';
80
+ const canonical = JSON.stringify(Object.keys(detail).sort().reduce((o, k) => { o[k] = detail[k]; return o; }, {}));
81
+ return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
82
+ }
83
+ /** 审计写入(append-only;PII-free)。与台账 INSERT 同事务调用。 */
84
+ function writeAudit(db, adminId, action, targetType, targetId, detail) {
85
+ db.prepare(`INSERT INTO admin_audit_log (id, admin_id, action, target_type, target_id, detail) VALUES (?,?,?,?,?,?)`)
86
+ .run('cing_' + randomUUID(), adminId, action, targetType, targetId, JSON.stringify(detail));
87
+ }
88
+ /** KYB 复核结论 ingress(append-only)。status ∈ {pending,approved,rejected,revoked}。 */
89
+ export function recordKybReview(db, args) {
90
+ const { userId, reviewerId, status, providerRef, expiresAt } = args;
91
+ if (!userId || !reviewerId)
92
+ return { ok: false, error: 'MISSING_ARGS' };
93
+ if (!KYB_STATUSES.has(status))
94
+ return { ok: false, error: 'INVALID_STATUS' };
95
+ const exp = normalizeExpiry(expiresAt);
96
+ if (!exp.ok)
97
+ return { ok: false, error: 'INVALID_EXPIRES_AT' };
98
+ const id = 'kyb_' + randomUUID();
99
+ db.transaction(() => {
100
+ db.prepare(`INSERT INTO direct_receive_kyb_reviews (id, user_id, status, reviewed_by, reviewed_at, expires_at, reason)
101
+ VALUES (?,?,?,?,datetime('now'),?,?)`).run(id, userId, status, reviewerId, exp.value, providerRef ?? null);
102
+ writeAudit(db, reviewerId, 'direct_pay.kyb_ingress', 'user', userId, { kyb_status: status, provider_ref_hash: providerRefHash(providerRef), has_expiry: !!expiresAt, review_id: id });
103
+ })();
104
+ return { ok: true, id };
105
+ }
106
+ /** 制裁筛查结论 ingress(append-only)。status ∈ {clear,flagged,blocked}。 */
107
+ export function recordSanctionsScreening(db, args) {
108
+ const { userId, reviewerId, status, providerRef, expiresAt } = args;
109
+ if (!userId || !reviewerId)
110
+ return { ok: false, error: 'MISSING_ARGS' };
111
+ if (!SANCTIONS_STATUSES.has(status))
112
+ return { ok: false, error: 'INVALID_STATUS' };
113
+ const exp = normalizeExpiry(expiresAt);
114
+ if (!exp.ok)
115
+ return { ok: false, error: 'INVALID_EXPIRES_AT' };
116
+ const id = 'sc_' + randomUUID();
117
+ db.transaction(() => {
118
+ db.prepare(`INSERT INTO sanctions_screening (id, user_id, status, source, reason, screened_at, expires_at)
119
+ VALUES (?,?,?,?,?,datetime('now'),?)`).run(id, userId, status, 'manual_ingress', providerRef ?? null, exp.value);
120
+ writeAudit(db, reviewerId, 'direct_pay.sanctions_ingress', 'user', userId, { sanctions_status: status, provider_ref_hash: providerRefHash(providerRef), has_expiry: !!expiresAt, screening_id: id });
121
+ })();
122
+ return { ok: true, id };
123
+ }
124
+ /** AML flag ingress(append-only,新建 flag)。rule/severity/status 全 allowlist。 */
125
+ export function recordAmlFlagIngress(db, args) {
126
+ const { userId, reviewerId, rule, severity, status, relatedOrderId, detail } = args;
127
+ if (!userId || !reviewerId)
128
+ return { ok: false, error: 'MISSING_ARGS' };
129
+ if (!AML_RULES.has(rule))
130
+ return { ok: false, error: 'INVALID_RULE' };
131
+ if (!AML_SEVERITIES.has(severity))
132
+ return { ok: false, error: 'INVALID_SEVERITY' };
133
+ if (!AML_STATUSES.has(status))
134
+ return { ok: false, error: 'INVALID_STATUS' };
135
+ // P2: detail 仅允许聚合数字(防 PII 落库)。非纯数字 → fail-closed,不写。
136
+ if (!isNumericDetail(detail))
137
+ return { ok: false, error: 'INVALID_DETAIL' };
138
+ const id = 'amlf_' + randomUUID();
139
+ db.transaction(() => {
140
+ db.prepare(`INSERT INTO aml_flags (id, subject_user_id, related_order_id, rule, severity, detail, status)
141
+ VALUES (?,?,?,?,?,?,?)`).run(id, userId, relatedOrderId ?? null, rule, severity, detail ? JSON.stringify(detail) : null, status);
142
+ writeAudit(db, reviewerId, 'direct_pay.aml_ingress', 'aml_flag', id, { subject_user_id: userId, rule, severity, aml_status: status, related_order_id: relatedOrderId ?? null });
143
+ })();
144
+ return { ok: true, id };
145
+ }
@@ -0,0 +1,136 @@
1
+ // PR-6D: 把 #108 的 AML 监控 param 描述(默认 inert)经控制面统一再导出,使 server.ts DEFAULT_PARAMS
2
+ // 可在【既有 import 行】上一并取用(server.ts 已到 LOC 上限,避免新增行)。SSOT 仍在 direct-pay-aml-monitor.ts。
3
+ export { DIRECT_PAY_AML_PARAMS } from './direct-pay-aml-monitor.js';
4
+ // ── buyer-facing de-identification(SSOT)─────────────────────────────────────────────────────────
5
+ // 买家边界脱敏:卖家【私密类】拒因(暂停 / 保证金未交 / KYC·制裁 / AML / 缓交额度)一律收敛为单一
6
+ // DIRECT_PAY_SELLER_NOT_ELIGIBLE,绝不向买家暴露卖家具体合规/额度状态。全局/运营类(DISABLED / RAIL_BREAKER /
7
+ // REGION / CAP)非敏感,原样透出。create 与 availability 两个买家面端点【共用】本判定,避免 de-id 集合漂移。
8
+ // 精确 code 仍保留在 helper 返回值 + 单测(controls / deferral-quota)层,供运营/调试与 gate 逻辑验证。
9
+ // 缓交额度码须与 direct-pay-deferral-quota.ts 的 DEFERRAL_QUOTA_CODES 对齐(test-direct-pay-deferral-quota 漂移断言守护)。
10
+ export const DIRECT_PAY_SELLER_NOT_ELIGIBLE = 'DIRECT_PAY_SELLER_NOT_ELIGIBLE';
11
+ export const BUYER_FACING_SELLER_PRIVATE_CODES = new Set([
12
+ 'DIRECT_PAY_SELLER_SUSPENDED', 'DIRECT_PAY_NOT_AVAILABLE', 'DIRECT_PAY_KYC_REQUIRED', 'DIRECT_PAY_AML_REVIEW_REQUIRED',
13
+ 'DIRECT_PAY_DEFERRAL_QUOTA_EXCEEDED', 'DIRECT_PAY_DEFERRAL_AMOUNT_EXCEEDED',
14
+ 'EXPOSURE_CAP_EXCEEDED', 'EXPOSURE_CAP_CONFIG', // §6.5 抵押背书敞口上限:卖家私密风险态,不向买家泄露
15
+ 'FEE_PREPAY_INSUFFICIENT', // 平台服务费预充值余额不足(非首单):卖家私密风险态,不向买家泄露
16
+ ]);
17
+ /** 买家面脱敏:私密拒因 → 通用 SELLER_NOT_ELIGIBLE;全局/运营类原样;undefined → 通用(fail-safe,绝不泄露)。 */
18
+ export function coarsenBuyerFacingDirectPayCode(code) {
19
+ return !code || BUYER_FACING_SELLER_PRIVATE_CODES.has(code) ? DIRECT_PAY_SELLER_NOT_ELIGIBLE : code;
20
+ }
21
+ export const DEFAULT_DIRECT_PAY_CONTROLS = {
22
+ enabled: false, railBreakerTripped: false, region: '', regionAllowlist: [], perTxCapUnits: 0,
23
+ };
24
+ const isNonNegUnits = (x) => typeof x === 'number' && Number.isSafeInteger(x) && x >= 0;
25
+ /**
26
+ * 入口控制判定(纯、fail-closed、total)。顺序(便宜/广 → 卖家专属 → 硬不变量):
27
+ * 全局开关 → 运营熔断 → 地区 → 单笔上限 → 卖家熔断 → production base-bond → KYC/制裁 → AML 断路器。
28
+ * 任一不过即返回该项拒绝码(短路;先全局/政策、后卖家专属,避免提前泄露卖家专属原因);全过返回 { ok:true }。绝不抛错。
29
+ * production base-bond、KYC/制裁、AML 断路器都是【不可配置硬不变量】(无 cfg 开关、治理不可绕过),恒在最后强制。
30
+ */
31
+ export function evaluateDirectPayLaunchControls(cfg, facts) {
32
+ const c = { ...DEFAULT_DIRECT_PAY_CONTROLS, ...(cfg ?? {}) };
33
+ const f = facts ?? {};
34
+ const deny = (error_code, reason) => ({ ok: false, status: 409, error_code, reason });
35
+ if (c.enabled !== true)
36
+ return deny('DIRECT_PAY_DISABLED', '直付当前未开放(全局开关关闭)');
37
+ if (c.railBreakerTripped === true)
38
+ return deny('DIRECT_PAY_RAIL_BREAKER', '直付已被运营紧急熔断,暂停受理');
39
+ const allow = Array.isArray(c.regionAllowlist) ? c.regionAllowlist : [];
40
+ if (!allow.length || !c.region || !allow.includes(c.region))
41
+ return deny('DIRECT_PAY_REGION_UNSUPPORTED', '直付在本地区暂未开放');
42
+ if (!isNonNegUnits(c.perTxCapUnits) || c.perTxCapUnits <= 0)
43
+ return deny('DIRECT_PAY_CAP_EXCEEDED', '直付单笔上限未配置');
44
+ if (!isNonNegUnits(f.amountUnits) || f.amountUnits <= 0 || f.amountUnits > c.perTxCapUnits)
45
+ return deny('DIRECT_PAY_CAP_EXCEEDED', '直付订单总额超出单笔上限(WebAZ 记录的订单金额上限;不约束场外实付)');
46
+ if (f.sellerBreakerTripped === true)
47
+ return deny('DIRECT_PAY_SELLER_SUSPENDED', '该卖家直付已被暂停');
48
+ // 硬不变量(launch blockers):production base-bond 与 KYC/制裁【始终强制】,无 cfg 开关、治理不可绕过。
49
+ if (f.baseBondSatisfied !== true)
50
+ return deny('DIRECT_PAY_NOT_AVAILABLE', '直付暂不可用:卖家未交履约保证金且无有效缓交');
51
+ if (f.kycSanctionsPassed !== true)
52
+ return deny('DIRECT_PAY_KYC_REQUIRED', '直付暂不可用:卖家未通过 KYC/制裁筛查');
53
+ if (f.amlClear !== true)
54
+ return deny('DIRECT_PAY_AML_REVIEW_REQUIRED', '直付暂不可用:卖家存在未清除的 AML 风险复核');
55
+ return { ok: true, status: 200 };
56
+ }
57
+ /** protocol_params 薄装配器(治理可调;缺行回落 fail-closed 默认)。不读资金/状态,只读控制参数。 */
58
+ export function readDirectPayControlsConfig(getProtocolParam) {
59
+ const csv = String(getProtocolParam('direct_pay.region_allowlist', '') || '');
60
+ const cap = Number(getProtocolParam('direct_pay.per_tx_cap_units', 0));
61
+ return {
62
+ enabled: getProtocolParam('direct_pay.enabled', false) === true,
63
+ railBreakerTripped: getProtocolParam('direct_pay.rail_breaker_tripped', false) === true,
64
+ region: String(getProtocolParam('direct_pay.region', '') || ''),
65
+ regionAllowlist: csv.split(',').map(s => s.trim()).filter(Boolean),
66
+ perTxCapUnits: Number.isSafeInteger(cap) && cap >= 0 ? cap : 0,
67
+ };
68
+ }
69
+ /**
70
+ * protocol_params 默认 seed(供 server.ts DEFAULT_PARAMS 展开)。默认全 fail-closed —— Direct Pay non-launchable;
71
+ * 治理经 PATCH /api/admin/protocol-params/<key> 开通(无 seed 则 PATCH 404、boot 不建行 → 控制面打不开)。
72
+ * 只含【运营节流】可调项(开关/地区/上限)。production base-bond 与 KYC/制裁是【不可关闭的硬不变量】,
73
+ * 刻意【不】做成 param —— 治理无法关掉它们绕过 launch blockers(evaluate 始终强制)。
74
+ * key/默认值必须与 readDirectPayControlsConfig / DEFAULT_DIRECT_PAY_CONTROLS 对齐。
75
+ */
76
+ export const DIRECT_PAY_CONTROL_PARAMS = [
77
+ { key: 'direct_pay.enabled', value: 'false', type: 'boolean', description: 'Direct Pay 全局主开关(上线决定);默认 false=关(non-launchable)。', category: 'system' },
78
+ { key: 'direct_pay.rail_breaker_tripped', value: 'false', type: 'boolean', description: 'Direct Pay 运营紧急熔断(ops emergency stop,与 enabled 分离);true=暂停受理。默认 false。', category: 'system' },
79
+ { key: 'direct_pay.region', value: '', type: 'string', description: 'Direct Pay 本部署/运营所在地区码(与白名单比对);默认空=fail-closed。', category: 'system' },
80
+ { key: 'direct_pay.region_allowlist', value: '', type: 'string', description: 'Direct Pay 已开放地区白名单(逗号分隔);默认空=无地区开放。', category: 'system' },
81
+ // 单笔上限:对【WebAZ 记录的 direct_p2p 订单总额】在建单时的天花板(整数 policy base-units)。默认 0 = 无放行(fail-closed),
82
+ // 治理设正值方生效。【不是】对买卖双方场外真实付款金额的担保或控制——WebAZ 控不了场外金额,此处也不声称能控。
83
+ // 具体数值(如 SG v1 的 policy units)由独立的 launch-policy PR 配置,本 PR 只提供 cap 能力、默认仍 fail-closed。
84
+ { key: 'direct_pay.per_tx_cap_units', value: '0', type: 'number', description: 'Direct Pay 单笔上限:WebAZ 记录的订单总额天花板(整数 policy base-units);默认 0=无放行,治理设正值方可。仅约束协议侧建单金额,不控制/不担保场外真实付款。', category: 'system', min: 0 },
85
+ { key: 'direct_pay.exposure_factor_bps', value: '8000', type: 'number', description: '§6.5 抵押背书的开放敞口上限系数(bps):open_exposure + new_order ≤ active_collateral × bps/10000。仅对有真实链上抵押(collateral>0)的卖家生效;缓交卖家不受此门。默认 8000(=80%)。风险控制,非买家赔付。', category: 'system', min: 1, max: 10000 },
86
+ // 注:平台服务费门 = 首单宽限 + 预充值续用(数据驱动:available_prepay = Σ预充值 − Σ已计提费),【无 protocol_param 旋钮】
87
+ // (额度即商家实际预付余额,宽限自动判定);故此处不再有 fee_ar_credit_ceiling_units 参数。
88
+ ];
89
+ /**
90
+ * PR-6A AML/CFT runtime — KYB(商户尽调)事实(fail-closed)。来源 direct_receive_kyb_reviews(真人/合规复核结论;
91
+ * 无第三方 vendor、无真实 API 调用)。通过 ⟺ 存在 status='approved' 且未过期的复核 且【无】rejected/revoked 行。
92
+ * missing / pending / rejected / revoked / expired 一律不通过。该表无生产写入方 → 真实卖家天然 fail-closed。
93
+ */
94
+ export function sellerDirectPayKybPassed(db, sellerId) {
95
+ const approved = db.prepare("SELECT 1 FROM direct_receive_kyb_reviews WHERE user_id = ? AND status = 'approved' AND (expires_at IS NULL OR expires_at > datetime('now')) LIMIT 1").get(sellerId);
96
+ const blocked = db.prepare("SELECT 1 FROM direct_receive_kyb_reviews WHERE user_id = ? AND status IN ('rejected','revoked') LIMIT 1").get(sellerId);
97
+ return !!approved && !blocked;
98
+ }
99
+ /**
100
+ * PR-6A AML/CFT runtime — 制裁筛查事实(fail-closed)。来源 sanctions_screening:存在 status='clear' 且未过期的结论,
101
+ * 且【无】flagged/blocked 行。expired(expires_at 已过)视作未通过;NULL expires_at = 无期限。无第三方集成。
102
+ * 该表无生产写入方 → 真实卖家天然 fail-closed。
103
+ */
104
+ export function sellerDirectPaySanctionsClear(db, sellerId) {
105
+ const clear = db.prepare("SELECT 1 FROM sanctions_screening WHERE user_id = ? AND status = 'clear' AND (expires_at IS NULL OR expires_at > datetime('now')) LIMIT 1").get(sellerId);
106
+ const bad = db.prepare("SELECT 1 FROM sanctions_screening WHERE user_id = ? AND status IN ('flagged','blocked') LIMIT 1").get(sellerId);
107
+ return !!clear && !bad;
108
+ }
109
+ /**
110
+ * 卖家熔断事实(per-seller breaker)。【唯一来源】= direct_receive_privileges.status = 'suspended'(复用既有暂停语义,
111
+ * 如 slashBond 罚没后置 suspended);【不】新增第二套 suspension 概念。无行 / 'none' / 'active' → false;'suspended' → true。
112
+ */
113
+ export function sellerDirectPayBreakerTripped(db, sellerId) {
114
+ return !!db.prepare("SELECT 1 FROM direct_receive_privileges WHERE user_id = ? AND status = 'suspended' LIMIT 1").get(sellerId);
115
+ }
116
+ /**
117
+ * PR-6B AML 运行期断路器事实(fail-closed)。来源 aml_flags(本仓内部监控 flag;无第三方 vendor、无真实 API)。
118
+ * key = subject_user_id。返回 true(清白/不阻断)⟺ 卖家【没有任何阻断性 flag】。
119
+ *
120
+ * 单条 flag 的【阻断】判定(任一命中即阻断;precedence 由 SQL OR 短路保证 suspend 优先于 cleared):
121
+ * ① malformed enum —— severity/status/disposition 越界(本表 TEXT 无 CHECK,脏值可入)→ fail-closed 阻断;
122
+ * ② disposition='suspend' —— 无论 severity/status【一律阻断】(含 status='cleared' 的矛盾数据:suspend 取胜,从严);
123
+ * ③ status∈(open|reviewing|escalated|str_filed) 且 severity∈(medium|high) —— 未清除的中/高风险 → 阻断。
124
+ * 非阻断(放行):无 flag / status='cleared'(且非 suspend) / severity='low'(且非 suspend)。
125
+ * 纯读;不写;不碰资金/状态机。aml_flags 当前无生产写入方 → 真实卖家天然无阻断(待 AML 检测引擎接入再产生 flag)。
126
+ */
127
+ export function sellerDirectPayAmlClear(db, sellerId) {
128
+ const blocking = db.prepare(`SELECT 1 FROM aml_flags WHERE subject_user_id = ? AND (
129
+ severity NOT IN ('low','medium','high')
130
+ OR status NOT IN ('open','reviewing','cleared','escalated','str_filed')
131
+ OR (disposition IS NOT NULL AND disposition NOT IN ('review_queue','downgrade','suspend'))
132
+ OR disposition = 'suspend'
133
+ OR (status IN ('open','reviewing','escalated','str_filed') AND severity IN ('medium','high'))
134
+ ) LIMIT 1`).get(sellerId);
135
+ return !blocking;
136
+ }