@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,80 @@
1
+ import { getPlatformAccount } from './platform-receive-accounts.js';
2
+ import { recordFeePrepayTopup } from './direct-pay-fee-ar.js';
3
+ export const MAX_EVIDENCE_LEN = 200;
4
+ export const MAX_EVIDENCE_NOTE_LEN = 500;
5
+ const COLS = 'id, seller_id, amount_units, currency, platform_account_id, evidence_ref, evidence_note, status, created_at, reviewed_by, reviewed_at, review_note, resulting_payment_id';
6
+ /** 卖家发起预充值申请。校验:金额正整数、凭据必填(不能无据)、平台收款方式须为 active。建 pending。不动钱。 */
7
+ export function createFeePrepayRequest(db, sellerId, input, generateId) {
8
+ const amount = Number(input.amountUnits);
9
+ if (!Number.isSafeInteger(amount) || amount <= 0)
10
+ return { ok: false, reason: 'amount must be a positive integer (base units)' }; // 严格整数,不静默截断非整数
11
+ const evidenceRef = String(input.evidenceRef ?? '').trim();
12
+ if (!evidenceRef)
13
+ return { ok: false, reason: '付款凭证号必填(不能无据)' };
14
+ if (evidenceRef.length > MAX_EVIDENCE_LEN)
15
+ return { ok: false, reason: `evidence_ref must be ≤ ${MAX_EVIDENCE_LEN} chars` };
16
+ const note = input.evidenceNote == null ? null : String(input.evidenceNote).trim().slice(0, MAX_EVIDENCE_NOTE_LEN) || null;
17
+ const currency = input.currency == null || !String(input.currency).trim() ? null : String(input.currency).trim().toUpperCase().slice(0, 8);
18
+ // platform_account_id 必填 + 必须 active:充值必须对准一个 WebAZ 收款账户,admin 才能据此核对到账来源(不能无账户的半场外流程)。
19
+ const pid = input.platformAccountId == null ? '' : String(input.platformAccountId).trim();
20
+ if (!pid)
21
+ return { ok: false, reason: '必须选择平台收款方式' };
22
+ const acc = getPlatformAccount(db, pid);
23
+ if (!acc || acc.status !== 'active')
24
+ return { ok: false, reason: '所选平台收款方式无效或已停用' };
25
+ const id = generateId('fpr');
26
+ db.prepare(`INSERT INTO direct_pay_fee_prepay_requests (id, seller_id, amount_units, currency, platform_account_id, evidence_ref, evidence_note, status) VALUES (?,?,?,?,?,?,?, 'pending')`)
27
+ .run(id, sellerId, amount, currency, pid, evidenceRef, note);
28
+ return { ok: true, request: getRequest(db, id) };
29
+ }
30
+ /** 卖家自己的申请(全状态,新→旧)。 */
31
+ export function listSellerRequests(db, sellerId) {
32
+ return db.prepare(`SELECT ${COLS} FROM direct_pay_fee_prepay_requests WHERE seller_id = ? ORDER BY created_at DESC, id DESC`).all(sellerId);
33
+ }
34
+ export function getRequest(db, id) {
35
+ return db.prepare(`SELECT ${COLS} FROM direct_pay_fee_prepay_requests WHERE id = ?`).get(id) ?? null;
36
+ }
37
+ /** 卖家撤销自己的 pending 申请(owner-scoped;仅 pending 可撤)。返回是否改动。 */
38
+ export function cancelRequest(db, id, sellerId) {
39
+ return db.prepare("UPDATE direct_pay_fee_prepay_requests SET status='cancelled', reviewed_at=datetime('now') WHERE id=? AND seller_id=? AND status='pending'").run(id, sellerId).changes > 0;
40
+ }
41
+ /** admin 队列:按状态列出所有卖家的申请(pending 升序供审核;无状态时取最近)。 */
42
+ export function listAllRequests(db, status) {
43
+ if (status)
44
+ return db.prepare(`SELECT ${COLS} FROM direct_pay_fee_prepay_requests WHERE status = ? ORDER BY created_at ASC, id ASC`).all(status);
45
+ return db.prepare(`SELECT ${COLS} FROM direct_pay_fee_prepay_requests ORDER BY created_at DESC, id DESC LIMIT 200`).all();
46
+ }
47
+ /**
48
+ * admin 确认真实到账 → 入账(【唯一动钱处】)。原子:pending→approved + recordFeePrepayTopup(记 direct_pay_fee_payments,
49
+ * 带申请的 evidence_ref)+ 回填 resulting_payment_id,全在一个 tx。非 pending / 记账失败 → 整体回滚不动钱。method=usdc|fiat。
50
+ */
51
+ export function approveFeePrepayRequest(db, args) {
52
+ if (args.method !== 'usdc' && args.method !== 'fiat')
53
+ return { ok: false, error: 'BAD_METHOD' };
54
+ const req = getRequest(db, args.requestId);
55
+ if (!req)
56
+ return { ok: false, error: 'REQUEST_NOT_FOUND' };
57
+ if (req.status !== 'pending')
58
+ return { ok: false, error: 'NOT_PENDING' };
59
+ let paymentId = '';
60
+ try {
61
+ db.transaction(() => {
62
+ const r = recordFeePrepayTopup(db, { sellerId: req.seller_id, amountUnits: req.amount_units, method: args.method, recordedBy: args.adminId, evidenceRef: req.evidence_ref, note: args.reviewNote ?? undefined });
63
+ if (!r.ok)
64
+ throw new Error(r.error);
65
+ const upd = db.prepare("UPDATE direct_pay_fee_prepay_requests SET status='approved', reviewed_by=?, reviewed_at=datetime('now'), review_note=?, resulting_payment_id=? WHERE id=? AND status='pending'").run(args.adminId, args.reviewNote ?? null, r.id, args.requestId);
66
+ if (upd.changes !== 1)
67
+ throw new Error('NOT_PENDING'); // 并发保险:非 pending → 回滚(含刚记的 topup)
68
+ paymentId = r.id;
69
+ })();
70
+ }
71
+ catch (e) {
72
+ return { ok: false, error: e.message };
73
+ }
74
+ return { ok: true, paymentId };
75
+ }
76
+ /** admin 驳回 pending 申请(不动钱)。返回 ok / 非 pending。 */
77
+ export function rejectFeePrepayRequest(db, args) {
78
+ const upd = db.prepare("UPDATE direct_pay_fee_prepay_requests SET status='rejected', reviewed_by=?, reviewed_at=datetime('now'), review_note=? WHERE id=? AND status='pending'").run(args.adminId, args.reviewNote ?? null, args.requestId);
79
+ return upd.changes === 1 ? { ok: true } : { ok: false, error: 'NOT_PENDING' };
80
+ }
@@ -0,0 +1,108 @@
1
+ import { readDirectPayControlsConfig, sellerDirectPayBreakerTripped, sellerDirectPayKybPassed, sellerDirectPaySanctionsClear, sellerDirectPayAmlClear, } from './direct-pay-controls.js';
2
+ import { sellerHasProductionBaseBondLocked } from './direct-receive-deposits.js';
3
+ import { sellerBaseBondEntrySatisfied } from './direct-pay-base-bond-entry.js';
4
+ import { getActiveDeferral } from './direct-receive-deferral.js';
5
+ import { bondRailClearanceBlockers, isBondRailClearedForProduction } from './direct-pay-bond-rail-clearance.js';
6
+ import { getActivePaymentInstruction } from './direct-receive-payment-instruction.js';
7
+ /** 候选生产 base-bond 收款轨(与 #112 registry 一致;manual 是非生产确认轨,不算)。 */
8
+ // 候选生产 base-bond 收款轨。operator_attested(#116,已实现的运营核实轨)是 v1 实际要用的那条,必须纳入诊断,
9
+ // 否则它被 registry 放行后 readiness 仍误报"无 legal-cleared rail"。manual=非生产确认轨,不算。
10
+ const PRODUCTION_BOND_RAILS = ['operator_attested', 'usdc_onchain', 'fiat_psp'];
11
+ /**
12
+ * 只读 readiness 汇总。args.getProtocolParam 读控制面配置;args.sellerId 提供时附加 seller-specific blockers。
13
+ * ready ⟺ blockers 为空。绝不写库。
14
+ */
15
+ export function readDirectPayLaunchReadiness(db, args) {
16
+ const { getProtocolParam, sellerId } = args;
17
+ const cfg = readDirectPayControlsConfig(getProtocolParam);
18
+ const blockers = [];
19
+ // ── global / policy(镜像 evaluateDirectPayLaunchControls 的非订单条件;不短路,收集全部)──
20
+ if (cfg.enabled !== true)
21
+ blockers.push('DIRECT_PAY_NOT_ENABLED');
22
+ if (cfg.railBreakerTripped === true)
23
+ blockers.push('DIRECT_PAY_RAIL_BREAKER_TRIPPED');
24
+ const allow = Array.isArray(cfg.regionAllowlist) ? cfg.regionAllowlist : [];
25
+ if (!allow.length || !cfg.region || !allow.includes(cfg.region))
26
+ blockers.push('DIRECT_PAY_REGION_NOT_ALLOWED');
27
+ if (!(Number.isSafeInteger(cfg.perTxCapUnits) && cfg.perTxCapUnits > 0))
28
+ blockers.push('DIRECT_PAY_PER_TX_CAP_UNSET');
29
+ // ── production bond-rail clearance(#112;rail-level,jurisdiction-aware)── 【诊断 facts,非 launch blocker】
30
+ // 关键:真实建单/可用性门(evaluateDirectPayLaunchControls + direct-pay-create/availability)从不校验 rail-clearance。
31
+ // rail-clearance 只 gate【有人交真实保证金】这一条路(confirmProductionReceipt 的 Lock B);缓交卖家根本不交 bond,
32
+ // 也就不需要任何 cleared rail 即可直付。因此把 rail-clearance 当全局 launch blocker 会让 readiness 误报"未开放"——
33
+ // 与真实门不一致(Codex #117 P1)。这里只【计算并下发为诊断 facts】(anyRailLegalCleared / perRailClearance),
34
+ // 供运营判断"若要接收真实保证金,bonded-deposit 轨是否就绪",但绝不进 blockers / 不影响 ready / PLATFORM_OPEN。
35
+ const perRailClearance = {};
36
+ for (const rid of PRODUCTION_BOND_RAILS)
37
+ perRailClearance[rid] = bondRailClearanceBlockers(rid, { hasProductionReceipt: true });
38
+ // jurisdiction-aware:把【当前部署 region】传进去,校验 region ∈ rail 的 legal jurisdictionAllowlist(coarse 判定会误报 cleared)。
39
+ const anyRailLegalCleared = PRODUCTION_BOND_RAILS.some(rid => isBondRailClearedForProduction(rid, cfg.region));
40
+ // ── seller-specific(仅当传入 sellerId)──
41
+ let sellerSuspended = null, productionBaseBondLocked = null;
42
+ let activeDeferral = null, baseBondSatisfied = null;
43
+ let kybPassed = null, sanctionsClear = null, amlClear = null;
44
+ let paymentInstructionPresent = null;
45
+ if (sellerId) {
46
+ const nowIso = new Date().toISOString();
47
+ sellerSuspended = sellerDirectPayBreakerTripped(db, sellerId);
48
+ productionBaseBondLocked = sellerHasProductionBaseBondLocked(db, sellerId);
49
+ activeDeferral = getActiveDeferral(db, sellerId, nowIso) != null;
50
+ // 镜像 create gate(direct-pay-base-bond-entry):保证金门 = 生产 bond OR 有效缓交。
51
+ baseBondSatisfied = sellerBaseBondEntrySatisfied(db, sellerId, nowIso);
52
+ kybPassed = sellerDirectPayKybPassed(db, sellerId);
53
+ sanctionsClear = sellerDirectPaySanctionsClear(db, sellerId);
54
+ amlClear = sellerDirectPayAmlClear(db, sellerId);
55
+ // 镜像真实建单硬门(direct-pay-create.ts):无 active 收款说明 → create 返回 NO_PAYMENT_INSTRUCTION,绝不建单。
56
+ paymentInstructionPresent = getActivePaymentInstruction(db, sellerId) != null;
57
+ if (sellerSuspended)
58
+ blockers.push('DIRECT_PAY_SELLER_SUSPENDED');
59
+ // 与 create gate 一致:有生产 bond 或有效缓交即满足;两者都无才 blocker。
60
+ if (!baseBondSatisfied)
61
+ blockers.push('DIRECT_PAY_SELLER_NO_PRODUCTION_BASE_BOND');
62
+ if (!kybPassed)
63
+ blockers.push('DIRECT_PAY_SELLER_KYB_NOT_APPROVED');
64
+ if (!sanctionsClear)
65
+ blockers.push('DIRECT_PAY_SELLER_SANCTIONS_NOT_CLEARED');
66
+ if (!amlClear)
67
+ blockers.push('DIRECT_PAY_SELLER_AML_REVIEW_REQUIRED');
68
+ if (!paymentInstructionPresent)
69
+ blockers.push('DIRECT_PAY_SELLER_PAYMENT_INSTRUCTION_MISSING');
70
+ }
71
+ return {
72
+ ready: blockers.length === 0,
73
+ blockers,
74
+ facts: {
75
+ enabled: cfg.enabled, railBreakerTripped: cfg.railBreakerTripped, region: cfg.region,
76
+ regionAllowlist: cfg.regionAllowlist, perTxCapUnits: cfg.perTxCapUnits,
77
+ perRailClearance, anyRailLegalCleared,
78
+ sellerEvaluated: !!sellerId, sellerId: sellerId ?? null,
79
+ sellerSuspended, productionBaseBondLocked, activeDeferral, baseBondSatisfied, kybPassed, sanctionsClear, amlClear, paymentInstructionPresent,
80
+ },
81
+ };
82
+ }
83
+ // PLATFORM_OPEN 只折叠【真实全局建单门】(镜像 evaluateDirectPayLaunchControls 的非订单条件):enabled / rail-breaker /
84
+ // region / per-tx cap。rail-clearance 不在此列(它不 gate 直付,见上),否则缓交卖家会被误显示"平台未开放"。
85
+ const GLOBAL_LAUNCH_BLOCKERS = new Set([
86
+ 'DIRECT_PAY_NOT_ENABLED', 'DIRECT_PAY_RAIL_BREAKER_TRIPPED', 'DIRECT_PAY_REGION_NOT_ALLOWED', 'DIRECT_PAY_PER_TX_CAP_UNSET',
87
+ ]);
88
+ /**
89
+ * 卖家脱敏 readiness 视图。复用 readDirectPayLaunchReadiness(只读),把内部 blockers/facts 折叠成卖家安全的项目集。
90
+ * 输出【不含】任何 raw blocker code、KYB/sanctions/AML 分项或法务/rail 细节。纯读。
91
+ */
92
+ export function sellerDirectPayReadinessView(db, args) {
93
+ const { getProtocolParam, sellerId } = args;
94
+ const r = readDirectPayLaunchReadiness(db, { getProtocolParam, sellerId });
95
+ const f = r.facts;
96
+ const platformOpen = !r.blockers.some(b => GLOBAL_LAUNCH_BLOCKERS.has(b));
97
+ const hasPasskey = !!db.prepare('SELECT 1 FROM webauthn_credentials WHERE user_id = ? LIMIT 1').get(sellerId);
98
+ const complianceCleared = f.kybPassed === true && f.sanctionsClear === true && f.amlClear === true; // 折叠;不分项
99
+ const items = [
100
+ { code: 'PLATFORM_OPEN', ok: platformOpen, actionable: false },
101
+ { code: 'PAYMENT_INSTRUCTION', ok: f.paymentInstructionPresent === true, actionable: true },
102
+ { code: 'PASSKEY', ok: hasPasskey, actionable: true },
103
+ { code: 'BASE_BOND', ok: f.baseBondSatisfied === true, actionable: false }, // 生产 bond 或有效缓交即算满足(与 create gate 一致)
104
+ { code: 'COMPLIANCE_REVIEW', ok: complianceCleared, actionable: false },
105
+ { code: 'NOT_SUSPENDED', ok: f.sellerSuspended === false, actionable: false },
106
+ ];
107
+ return { directPayReady: items.every(i => i.ok), items };
108
+ }
@@ -0,0 +1,68 @@
1
+ import { readDirectPayLaunchReadiness } from './direct-pay-launch-readiness.js';
2
+ import { readDirectPayControlsConfig } from './direct-pay-controls.js';
3
+ import { checkDeferralQuota, readDeferralQuotaConfig } from './direct-pay-deferral-quota.js';
4
+ import { productStoreVerified } from './product-verification.js';
5
+ import { sellerExemptFromPerProduct } from './store-verification.js';
6
+ import { toUnits } from './money.js';
7
+ /** 候选卖家:在任一 direct-pay 相关表里出现过的卖家(有意接入直付者)。 */
8
+ function candidateSellers(db) {
9
+ const rows = db.prepare(`
10
+ SELECT DISTINCT seller_id FROM (
11
+ SELECT seller_id FROM direct_receive_payment_instructions WHERE status = 'active'
12
+ UNION SELECT user_id FROM direct_receive_deferrals
13
+ UNION SELECT user_id FROM direct_receive_deposits
14
+ UNION SELECT seller_id FROM product_verifications
15
+ UNION SELECT user_id FROM store_verifications
16
+ ) WHERE seller_id IS NOT NULL
17
+ `).all();
18
+ return rows.map(r => r.seller_id);
19
+ }
20
+ /** 聚合 go/no-go。纯读。getProtocolParam 由调用方按部署注入(CLI 从 protocol_params 构造)。 */
21
+ export function summarizeDirectPayLaunchReadiness(db, getProtocolParam) {
22
+ const globalRd = readDirectPayLaunchReadiness(db, { getProtocolParam }); // 无 sellerId → 仅全局 blockers
23
+ const global = { ready: globalRd.blockers.length === 0, blockers: globalRd.blockers, facts: globalRd.facts };
24
+ const globalSet = new Set(global.blockers);
25
+ const nowIso = new Date().toISOString();
26
+ const quotaCfg = readDeferralQuotaConfig(getProtocolParam);
27
+ const controlsCfg = readDirectPayControlsConfig(getProtocolParam);
28
+ const sellers = candidateSellers(db).map(sellerId => {
29
+ const rd = readDirectPayLaunchReadiness(db, { getProtocolParam, sellerId });
30
+ // 仅 seller-specific blockers(去掉全局 blockers,避免每个卖家都重复挂全局问题)。
31
+ const sellerBlockers = rd.blockers.filter(b => !globalSet.has(b));
32
+ const ready = sellerBlockers.length === 0;
33
+ const storeExempt = sellerExemptFromPerProduct(db, sellerId);
34
+ const products = db.prepare("SELECT id, price, stock, has_variants FROM products WHERE seller_id = ? AND status = 'active'").all(sellerId);
35
+ // 可直付商品 = 必须能真正走 direct_p2p 建单(镜像 create gate / evaluateDirectPayLaunchControls + 建单库存门),全部满足:
36
+ // ① 简单商品(direct_p2p v1 拒规格商品 has_variants=1);② 逐品 verified 或卖家店铺豁免;
37
+ // ③ 单笔上限:amount>0 且 ≤ perTxCapUnits(且 cap 已配 >0)—— 镜像 DIRECT_PAY_CAP_EXCEEDED;
38
+ // ④ 有货(stock≥1)—— 镜像 createDirectPayOrder 的 `stock >= qty`(否则 'stock depleted');
39
+ // ⑤ 无进行中限时促销(direct_p2p v1 拒 flash_sale=DIRECT_PAY_UNSUPPORTED_OPTION;谓词镜像 getActiveFlashSale@flash-sales.ts 的简单商品分支);
40
+ // ⑥ 通过缓交额度(checkDeferralQuota qty=1 该单价;非缓交卖家=no-op)。否则报告会误判 go=true 而真实下单被拒。
41
+ const flashActive = db.prepare(`SELECT 1 FROM flash_sales WHERE product_id = ? AND is_active = 1
42
+ AND starts_at <= datetime('now') AND ends_at > datetime('now') AND (max_qty = 0 OR sold_count < max_qty) AND variant_id IS NULL LIMIT 1`);
43
+ const eligibleProductCount = products.filter(p => {
44
+ const priceU = toUnits(Number(p.price) || 0);
45
+ return Number(p.has_variants) !== 1
46
+ && (storeExempt || productStoreVerified(db, p.id))
47
+ && controlsCfg.perTxCapUnits > 0 && priceU > 0 && priceU <= controlsCfg.perTxCapUnits
48
+ && Number(p.stock) >= 1
49
+ && !flashActive.get(p.id)
50
+ && checkDeferralQuota(db, sellerId, priceU, nowIso, quotaCfg).ok;
51
+ }).length;
52
+ return { sellerId, ready, blockers: sellerBlockers, storeExempt, activeProductCount: products.length, eligibleProductCount, launchable: ready && eligibleProductCount > 0 };
53
+ });
54
+ const launchableSellerCount = sellers.filter(s => s.launchable).length;
55
+ // PRE-FLIP 自检:忽略【且仅忽略】"总开关未开"这一项,回答"除了翻 enabled,还差什么"。其余(region/cap/卖家/商品)照常要求。
56
+ const preflipReady = global.blockers.filter(b => b !== 'DIRECT_PAY_NOT_ENABLED').length === 0;
57
+ const pendingEnable = global.facts.enabled !== true;
58
+ return {
59
+ go: global.ready && launchableSellerCount > 0, // LIVE(含 enabled)
60
+ preflipGo: preflipReady && launchableSellerCount > 0, // 只差翻 enabled 即就绪
61
+ pendingEnable,
62
+ global, sellers, launchableSellerCount,
63
+ };
64
+ }
65
+ /** 便捷:从控制面配置取关键运营数值(供 CLI 打印)。 */
66
+ export function directPayControlsSnapshot(getProtocolParam) {
67
+ return readDirectPayControlsConfig(getProtocolParam);
68
+ }
@@ -0,0 +1,94 @@
1
+ import { toUnits, toDecimal, allocate } from './money.js';
2
+ import { applyWalletDelta, walletUnits, creditColumns } from './ledger.js';
3
+ /**
4
+ * penalty 科目【唯一】写入口(只进,无对应出账函数)。
5
+ * - source='fee_stake' → 真入 WAZ balance(fee-stake 本就是 WAZ)。
6
+ * - source='base_bond' → 仅记 provenance 计数(外部资产,真实处置 gated;不进 balance)。
7
+ */
8
+ function creditPenalty(db, opts) {
9
+ const { kind, source, fromUserId = null, amountU, relatedOrderId = null, reason = null, txnId } = opts;
10
+ const deltas = source === 'fee_stake'
11
+ ? { balance: amountU, total_fee_stake_slash: amountU } // WAZ 真入池
12
+ : { total_base_bond_slash: amountU }; // 外部资产:仅计数,不进 balance
13
+ creditColumns(db, 'penalty_fund', "id = 'main'", [], deltas);
14
+ db.prepare("UPDATE penalty_fund SET updated_at = datetime('now') WHERE id = 'main'").run();
15
+ db.prepare(`INSERT INTO penalty_fund_txns (id, kind, source, from_user_id, amount, related_order_id, reason, created_at) VALUES (?,?,?,?,?,?,?,datetime('now'))`)
16
+ .run(txnId, kind, source, fromUserId, toDecimal(amountU), relatedOrderId, reason);
17
+ }
18
+ function getStake(db, orderId) {
19
+ return db.prepare('SELECT id, order_id, seller_id, amount, status FROM direct_pay_fee_stakes WHERE order_id = ?')
20
+ .get(orderId);
21
+ }
22
+ /** 只读断言:订单是否有【locked】fee-stake(可在完成时取费)。完成路径预检用,缺则拒绝 direct_p2p 进 completed。 */
23
+ export function hasLockedFeeStake(db, orderId) {
24
+ return getStake(db, orderId)?.status === 'locked';
25
+ }
26
+ /** 锁定逐单费用质押(= 平台费):卖家可用余额 → fee_staked。余额不足返回 {ok:false}(调用方不开单)。 */
27
+ export function lockFeeStake(db, opts) {
28
+ const { orderId, sellerId, feeUnits, stakeId } = opts;
29
+ if (feeUnits <= 0)
30
+ return { ok: false, reason: 'fee must be > 0' };
31
+ if (getStake(db, orderId))
32
+ return { ok: false, reason: 'fee-stake already exists for order' };
33
+ const w = walletUnits(db, sellerId);
34
+ if (w.balance < feeUnits)
35
+ return { ok: false, reason: 'insufficient balance for fee-stake' };
36
+ applyWalletDelta(db, sellerId, { balance: -feeUnits, fee_staked: feeUnits });
37
+ db.prepare(`INSERT INTO direct_pay_fee_stakes (id, order_id, seller_id, amount, status, created_at) VALUES (?,?,?,?, 'locked', datetime('now'))`)
38
+ .run(stakeId, orderId, sellerId, toDecimal(feeUnits));
39
+ return { ok: true };
40
+ }
41
+ /**
42
+ * 完成时取费:整笔 fee-stake → 协议(reserve 50% + sys_protocol 50%,与 settleOrder 协议费去向一致)。
43
+ * **FAIL-CLOSED(钱路不变量):** direct_p2p 单完成必须有可取的 locked fee-stake,否则平台费会被静默落空。
44
+ * - 无 stake 行 / 状态非 locked 且非 fee_taken → **抛错**(调用方 settleOrder 在 db.transaction 内 → 回滚,
45
+ * 拒绝"完成但零平台费")。绝不静默 return(那正是审计 P1-2 指出的 fail-open 漏洞)。
46
+ * - 已 fee_taken → 幂等:返回 'already_taken',不重复取费(支持重结算)。
47
+ * fee-stake == 平台费,故无 remainder(若将来 stake>fee 可在此退差额给卖家)。
48
+ */
49
+ export function takeFeeAtCompletion(db, opts) {
50
+ const s = getStake(db, opts.orderId);
51
+ if (!s)
52
+ throw new Error(`takeFeeAtCompletion: no fee-stake for direct_p2p order ${opts.orderId} — refusing to settle without platform fee (fail-closed)`);
53
+ if (s.status === 'fee_taken')
54
+ return { outcome: 'already_taken' }; // 幂等:重结算不重复取费
55
+ if (s.status !== 'locked')
56
+ throw new Error(`takeFeeAtCompletion: fee-stake for order ${opts.orderId} is '${s.status}', not 'locked' — refusing to settle (fail-closed)`);
57
+ const feeU = toUnits(s.amount);
58
+ applyWalletDelta(db, s.seller_id, { fee_staked: -feeU });
59
+ const [toReserveU, toOpsU] = allocate(feeU, [1, 1]);
60
+ if (toReserveU > 0)
61
+ creditColumns(db, 'protocol_reserve_pool', 'id = 1', [], { balance: toReserveU });
62
+ if (toOpsU > 0)
63
+ applyWalletDelta(db, 'sys_protocol', { balance: toOpsU });
64
+ db.prepare("UPDATE direct_pay_fee_stakes SET status='fee_taken', settled_at=datetime('now') WHERE order_id=? AND status='locked'").run(opts.orderId);
65
+ return { outcome: 'taken' };
66
+ }
67
+ /** 释放(未付/取消/超时):整笔退回卖家可用余额。幂等。 */
68
+ export function releaseFeeStake(db, opts) {
69
+ const s = getStake(db, opts.orderId);
70
+ if (!s || s.status !== 'locked')
71
+ return;
72
+ const feeU = toUnits(s.amount);
73
+ applyWalletDelta(db, s.seller_id, { fee_staked: -feeU, balance: feeU });
74
+ db.prepare("UPDATE direct_pay_fee_stakes SET status='released', settled_at=datetime('now') WHERE order_id=? AND status='locked'").run(opts.orderId);
75
+ }
76
+ /** 违约罚没:整笔 fee-stake → penalty 科目(feeRetainedOnFault=false → WebAZ 不收费,整笔入池)。幂等。 */
77
+ export function slashFeeStakeToPenalty(db, opts) {
78
+ const s = getStake(db, opts.orderId);
79
+ if (!s || s.status !== 'locked')
80
+ return;
81
+ const feeU = toUnits(s.amount);
82
+ applyWalletDelta(db, s.seller_id, { fee_staked: -feeU });
83
+ creditPenalty(db, { kind: 'fee_stake_slash', source: 'fee_stake', fromUserId: s.seller_id, amountU: feeU, relatedOrderId: opts.orderId, reason: opts.reason ?? null, txnId: opts.txnId });
84
+ db.prepare("UPDATE direct_pay_fee_stakes SET status='slashed', settled_at=datetime('now') WHERE order_id=? AND status='locked'").run(opts.orderId);
85
+ }
86
+ /**
87
+ * base bond(外部担保物)罚没:仅记 provenance(total_base_bond_slash + txn),不进 WAZ balance。
88
+ * 真实外部资产(USDC/法币)处置 GATED(deposit-rail 非生产 / 法务)。由 Phase 4 deposit 生命周期调用。
89
+ */
90
+ export function recordBaseBondSlash(db, opts) {
91
+ if (opts.amountUnits <= 0)
92
+ return;
93
+ creditPenalty(db, { kind: 'base_bond_slash', source: 'base_bond', fromUserId: opts.userId, amountU: opts.amountUnits, relatedOrderId: null, reason: opts.reason ?? null, txnId: opts.txnId });
94
+ }
@@ -0,0 +1,104 @@
1
+ function loadReturn(db, returnId) {
2
+ return db.prepare(`SELECT r.id, r.order_id, r.buyer_id, r.seller_id, r.product_id, r.status, r.reason,
3
+ r.refund_amount, r.await_refund_since, o.payment_rail, o.total_amount AS order_total
4
+ FROM return_requests r JOIN orders o ON o.id = r.order_id WHERE r.id = ?`).get(returnId);
5
+ }
6
+ export function returnRefundRespondDays(db) {
7
+ try {
8
+ const p = db.prepare("SELECT value FROM protocol_params WHERE key = 'direct_pay.return_refund_respond_days'").get();
9
+ if (p)
10
+ return Math.max(1, Number(p.value) || 5);
11
+ }
12
+ catch { /* 表缺失 → 默认 */ }
13
+ return 5;
14
+ }
15
+ /**
16
+ * 卖家同意退货后进入 await_refund(替代 escrow 的 executeReturnRefund)。
17
+ * fromStatus = 'pending'(同意·无取件)| 'picked_up'(取件流·确认收到退货)。CAS 防并发。
18
+ * 同 tx 内写协商时间线消息(与 escrow accept 同模式)。调用方负责通知。
19
+ */
20
+ export function enterAwaitRefund(db, args) {
21
+ const rr = loadReturn(db, args.returnId);
22
+ if (!rr)
23
+ return { ok: false, error: '退货请求不存在', error_code: 'RETURN_NOT_FOUND' };
24
+ if (rr.payment_rail !== 'direct_p2p')
25
+ return { ok: false, error: '仅直付订单走场外退款握手', error_code: 'NOT_DIRECT_PAY' };
26
+ const resp = typeof args.sellerResponse === 'string' ? args.sellerResponse.slice(0, 500) : null;
27
+ const r = db.prepare(`UPDATE return_requests SET status = 'await_refund', await_refund_since = datetime('now'),
28
+ seller_response = COALESCE(?, seller_response) WHERE id = ? AND status = ?`)
29
+ .run(resp, args.returnId, args.fromStatus);
30
+ if (r.changes !== 1)
31
+ return { ok: false, error: '退货请求已被处理(请刷新后查看)', error_code: 'RETURN_ALREADY_SETTLED' };
32
+ try {
33
+ db.prepare(`INSERT INTO return_messages (id, return_id, sender_id, sender_role, body) VALUES (?,?,?,?,?)`)
34
+ .run(args.messageId, rr.id, rr.seller_id, 'seller', `[✓ 同意退货 · 待场外退款] ${resp || ''}`);
35
+ }
36
+ catch { /* 时间线消息 best-effort */ }
37
+ return { ok: true, status: 'await_refund' };
38
+ }
39
+ /** 卖家声明已场外退款(await_refund → refund_marked,可附退款参考)。此后买家 confirm 或走争议。 */
40
+ export function markReturnRefunded(db, args) {
41
+ const rr = loadReturn(db, args.returnId);
42
+ if (!rr)
43
+ return { ok: false, error: '退货请求不存在', error_code: 'RETURN_NOT_FOUND' };
44
+ if (rr.seller_id !== args.sellerId)
45
+ return { ok: false, error: '只有订单卖家可声明退款', error_code: 'NOT_ORDER_SELLER' };
46
+ if (rr.payment_rail !== 'direct_p2p')
47
+ return { ok: false, error: '仅直付订单走场外退款握手', error_code: 'NOT_DIRECT_PAY' };
48
+ const ref = typeof args.refundReference === 'string' ? args.refundReference.trim().slice(0, 200) : null;
49
+ const r = db.prepare(`UPDATE return_requests SET status = 'refund_marked', refund_reference = ? WHERE id = ? AND status = 'await_refund'`)
50
+ .run(ref, args.returnId);
51
+ if (r.changes !== 1)
52
+ return { ok: false, error: '当前状态不可声明退款(须先同意退货,或已被处理)', error_code: 'NOT_AWAIT_REFUND' };
53
+ try {
54
+ db.prepare(`INSERT INTO return_messages (id, return_id, sender_id, sender_role, body) VALUES (?,?,?,?,?)`)
55
+ .run(args.messageId, rr.id, rr.seller_id, 'seller', `[💸 已声明场外退款] ${ref ? `退款参考:${ref}` : ''}`);
56
+ }
57
+ catch { /* best-effort */ }
58
+ return { ok: true, status: 'refund_marked' };
59
+ }
60
+ /**
61
+ * 买家确认已收到场外退款(refund_marked → refunded,终态)。【必须由路由 db.transaction 包裹 + Passkey 门】。
62
+ * 全额退款 → completion_count -1(与 escrow 同口径的 best-effort 社交计数;无钱、无库存、无 clearing)。
63
+ */
64
+ export function confirmReturnRefundReceived(db, args) {
65
+ const rr = loadReturn(db, args.returnId);
66
+ if (!rr)
67
+ return { ok: false, error: '退货请求不存在', error_code: 'RETURN_NOT_FOUND' };
68
+ if (rr.buyer_id !== args.buyerId)
69
+ return { ok: false, error: '只有订单买家可确认收到退款', error_code: 'NOT_ORDER_BUYER' };
70
+ if (rr.payment_rail !== 'direct_p2p')
71
+ return { ok: false, error: '仅直付订单走场外退款握手', error_code: 'NOT_DIRECT_PAY' };
72
+ const cas = db.prepare(`UPDATE return_requests SET status = 'refunded', resolved_at = datetime('now') WHERE id = ? AND status = 'refund_marked'`)
73
+ .run(args.returnId);
74
+ if (cas.changes !== 1)
75
+ return { ok: false, error: '卖家尚未声明退款,不可确认', error_code: 'REFUND_NOT_MARKED' };
76
+ if (Number(rr.refund_amount) >= Number(rr.order_total)) {
77
+ db.prepare(`UPDATE products SET completion_count = MAX(0, COALESCE(completion_count,0) - 1) WHERE id = ?`).run(rr.product_id);
78
+ }
79
+ try {
80
+ db.prepare(`INSERT INTO return_messages (id, return_id, sender_id, sender_role, body) VALUES (?,?,?,?,?)`)
81
+ .run(args.messageId, rr.id, rr.buyer_id, 'buyer', '[✅ 已确认收到场外退款] 退货完成');
82
+ }
83
+ catch { /* best-effort */ }
84
+ const SELLER_FAULT_REASONS = new Set(['quality', 'wrong_item', 'damaged']);
85
+ return {
86
+ ok: true, status: 'refunded', order_id: rr.order_id, seller_id: rr.seller_id,
87
+ seller_fault_reason: SELLER_FAULT_REASONS.has(String(rr.reason)) ? String(rr.reason) : null,
88
+ };
89
+ }
90
+ /**
91
+ * 直付退货是否可升级争议(escalate 的 direct_p2p 分支;通用分支 rejected / pending≥7d 仍适用):
92
+ * - refund_marked:随时可(卖家声明≠买家收到,声明不实的收口权在买家)。
93
+ * - await_refund:超 respond 窗(默认 5 天)—— 卖家同意了却迟迟不退款。
94
+ */
95
+ export function directPayReturnEscalatable(db, rr) {
96
+ if (rr.status === 'refund_marked')
97
+ return true;
98
+ if (rr.status === 'await_refund' && rr.await_refund_since) {
99
+ const over = db.prepare(`SELECT datetime(?, '+' || ? || ' days') < datetime('now') AS o`)
100
+ .get(String(rr.await_refund_since), returnRefundRespondDays(db));
101
+ return !!over.o;
102
+ }
103
+ return false;
104
+ }
@@ -0,0 +1,9 @@
1
+ /** 允许自动回补的取消来源状态(= A 类:货从未出库)。pending_accept=手动接单待确认(v16),付款前必然未出库。 */
2
+ export const PRE_SHIP_RESTOCK_STATUSES = new Set(['pending_accept', 'direct_pay_window', 'direct_expired_unconfirmed', 'payment_query', 'accepted']);
3
+ /** 回补库存(仅 A 类 pre-ship 来源;B 类已出库来源拒绝 —— 走退货验收上架)。返回是否真的回补了。 */
4
+ export function restorePreShipDirectPayStock(db, args) {
5
+ if (!PRE_SHIP_RESTOCK_STATUSES.has(args.fromStatus))
6
+ return false; // 已出库/争议来源:绝不直接回补
7
+ db.prepare('UPDATE products SET stock = stock + ? WHERE id = ?').run(args.quantity, args.productId);
8
+ return true;
9
+ }
@@ -0,0 +1,77 @@
1
+ import { createHash } from 'crypto';
2
+ export const QR_DATA_URI_RE = /^data:image\/(png|webp);base64,[A-Za-z0-9+/=]+$/;
3
+ export const QR_MAX_BYTES = 64 * 1024;
4
+ /** PNG magic 8 bytes / WEBP RIFF....WEBP。仅接受与声明 mime 一致的字节。 */
5
+ function magicMatches(subtype, buf) {
6
+ if (subtype === 'png')
7
+ return buf.length >= 8 && buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
8
+ if (subtype === 'webp')
9
+ return buf.length >= 12 && buf.subarray(0, 4).toString('latin1') === 'RIFF' && buf.subarray(8, 12).toString('latin1') === 'WEBP';
10
+ return false;
11
+ }
12
+ /** PURE: 校验收款码 data-URI。返回 {ok, ...} 或 {ok:false, reason}。绝不解析二维码。 */
13
+ export function validateQrDataUri(dataUri) {
14
+ const s = typeof dataUri === 'string' ? dataUri.trim() : '';
15
+ const m = QR_DATA_URI_RE.exec(s);
16
+ if (!m)
17
+ return { ok: false, reason: 'QR must be a data:image/(png|webp);base64 image (svg/html/text/jpeg rejected)' };
18
+ const subtype = m[1];
19
+ let buf;
20
+ try {
21
+ buf = Buffer.from(s.slice(s.indexOf(',') + 1), 'base64');
22
+ }
23
+ catch {
24
+ return { ok: false, reason: 'QR base64 decode failed' };
25
+ }
26
+ if (buf.length === 0)
27
+ return { ok: false, reason: 'QR is empty' };
28
+ if (buf.length > QR_MAX_BYTES)
29
+ return { ok: false, reason: `QR must decode to ≤ ${QR_MAX_BYTES} bytes` };
30
+ if (!magicMatches(subtype, buf))
31
+ return { ok: false, reason: `QR bytes do not match declared image/${subtype} (magic-byte mismatch)` };
32
+ const sha256 = createHash('sha256').update(buf).digest('hex');
33
+ return { ok: true, value: { mime: `image/${subtype}`, buf, sha256 } };
34
+ }
35
+ /** append-only 审计事件。只记 ref,【绝不】写 raw instruction / raw QR。 */
36
+ export function appendAccountEvent(db, args, generateId) {
37
+ db.prepare('INSERT INTO direct_receive_account_events (id, account_id, seller_id, event_type, qr_ref) VALUES (?,?,?,?,?)')
38
+ .run(generateId('drae'), args.accountId, args.sellerId, args.eventType, args.qrRef ?? null);
39
+ }
40
+ /**
41
+ * 存收款码(owner-scoped)。校验 → sha256 → 【一个同步事务】内:INSERT OR IGNORE 不可变行 + 更新 account.qr_image_ref
42
+ * + append qr_uploaded 事件。account 必须属于 sellerId(不属于 → {ok:false})。返回 ref。
43
+ */
44
+ export function storeQrImage(db, args, generateId) {
45
+ const v = validateQrDataUri(args.dataUri);
46
+ if (!v.ok)
47
+ return v;
48
+ const owns = db.prepare("SELECT 1 FROM direct_receive_accounts WHERE id = ? AND seller_id = ?").get(args.accountId, args.sellerId);
49
+ if (!owns)
50
+ return { ok: false, reason: 'account not found or not owned' };
51
+ const ref = v.value.sha256;
52
+ db.transaction(() => {
53
+ db.prepare('INSERT OR IGNORE INTO direct_receive_account_qr_images (ref, account_id, seller_id, mime, data_b64, byte_len, sha256) VALUES (?,?,?,?,?,?,?)')
54
+ .run(ref, args.accountId, args.sellerId, v.value.mime, v.value.buf.toString('base64'), v.value.buf.length, ref);
55
+ db.prepare("UPDATE direct_receive_accounts SET qr_image_ref = ?, updated_at = datetime('now') WHERE id = ? AND seller_id = ?").run(ref, args.accountId, args.sellerId);
56
+ appendAccountEvent(db, { accountId: args.accountId, sellerId: args.sellerId, eventType: 'qr_uploaded', qrRef: ref }, generateId);
57
+ })();
58
+ return { ok: true, ref };
59
+ }
60
+ /** owner-scoped 取码字节(供硬化端点转发)。非本人 / 不存在 → null(端点统一 404,不枚举)。 */
61
+ export function getQrImageForOwner(db, ref, sellerId) {
62
+ const row = db.prepare('SELECT mime, data_b64 FROM direct_receive_account_qr_images WHERE ref = ? AND seller_id = ?').get(ref, sellerId);
63
+ if (!row)
64
+ return null;
65
+ let buf;
66
+ try {
67
+ buf = Buffer.from(row.data_b64, 'base64');
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ // defense-in-depth: re-validate on read (mime whitelist + magic + size) — never serve unexpected bytes.
73
+ const re = validateQrDataUri(`data:${row.mime};base64,${row.data_b64}`);
74
+ if (!re.ok)
75
+ return null;
76
+ return { mime: row.mime, buf };
77
+ }
@@ -0,0 +1,82 @@
1
+ export const MAX_INSTRUCTION_LEN = 500;
2
+ export const MAX_LABEL_LEN = 40;
3
+ export const MAX_METHOD_LEN = 40;
4
+ export const MAX_QR_REF_LEN = 200;
5
+ // currency:卖家声明的币种码(自由文本,2-8 位大写字母数字)。WebAZ 不限制卖家怎么收钱;买家侧只在 FX 支持时才显换算。
6
+ export const CURRENCY_RE = /^[A-Z0-9]{2,8}$/;
7
+ /** PURE: 校验 + 规范化卖家入参(trim / 长度 / 币种格式 / 大写)。不碰 DB。
8
+ * 长度超限 = 【显式拒绝】(非静默截断)—— 卖家看到的收款标签 / 方式 / 二维码引用不能被悄悄改短。 */
9
+ export function normalizeAccountInput(input) {
10
+ const instruction = String(input?.instruction ?? '').trim();
11
+ if (!instruction)
12
+ return { ok: false, reason: 'instruction required' };
13
+ if (instruction.length > MAX_INSTRUCTION_LEN)
14
+ return { ok: false, reason: `instruction must be ≤ ${MAX_INSTRUCTION_LEN} chars` };
15
+ // trim + reject-if-over-limit (no silent slice). null/absent → null.
16
+ const field = (v, max, name) => {
17
+ if (v == null)
18
+ return { ok: true, v: null };
19
+ const s = String(v).trim();
20
+ if (s.length > max)
21
+ return { ok: false, reason: `${name} must be ≤ ${max} chars` };
22
+ return { ok: true, v: s || null };
23
+ };
24
+ const L = field(input.label, MAX_LABEL_LEN, 'label');
25
+ if (!L.ok)
26
+ return L;
27
+ const M = field(input.method, MAX_METHOD_LEN, 'method');
28
+ if (!M.ok)
29
+ return M;
30
+ const Q = field(input.qrImageRef, MAX_QR_REF_LEN, 'qr image ref');
31
+ if (!Q.ok)
32
+ return Q;
33
+ let currency = null;
34
+ if (input.currency != null && String(input.currency).trim()) {
35
+ currency = String(input.currency).trim().toUpperCase();
36
+ if (!CURRENCY_RE.test(currency))
37
+ return { ok: false, reason: 'currency must be a 2-8 char code (e.g. THB, IDR, USDC)' };
38
+ }
39
+ return { ok: true, value: { instruction, label: L.v, method: M.v, currency, qr_image_ref: Q.v } };
40
+ }
41
+ const COLS = 'id, seller_id, method, currency, instruction, label, qr_image_ref, status';
42
+ /** 卖家的收款账号(默认仅 active;{ includeInactive:true } 取全部)。多行。 */
43
+ export function listSellerAccounts(db, sellerId, opts = {}) {
44
+ const where = opts.includeInactive ? '' : " AND status = 'active'";
45
+ return db.prepare(`SELECT ${COLS} FROM direct_receive_accounts WHERE seller_id = ?${where} ORDER BY created_at ASC, id ASC`).all(sellerId);
46
+ }
47
+ /** 单个账号(任意状态);调用方自行做 owner 校验。 */
48
+ export function getAccount(db, id) {
49
+ return db.prepare(`SELECT ${COLS} FROM direct_receive_accounts WHERE id = ?`).get(id) ?? null;
50
+ }
51
+ /** 买家侧【可选收款账号】(仅 active)—— 只投影元数据 method/currency/label 供结算前选"怎么付"+FX 换算展示。
52
+ * ⚠️【绝不】含 instruction 原文 / qr_image_ref —— 收款目标受披露门保护(D1/D2 ack 后才随订单快照下发)。 */
53
+ export function listSellerAccountOptions(db, sellerId) {
54
+ return db.prepare("SELECT id AS account_id, method, currency, label FROM direct_receive_accounts WHERE seller_id = ? AND status = 'active' ORDER BY created_at ASC, id ASC").all(sellerId);
55
+ }
56
+ /** 新增一个 active 收款账号。入参经 normalizeAccountInput 校验;只写本表。 */
57
+ export function addAccount(db, sellerId, input, generateId) {
58
+ const norm = normalizeAccountInput(input);
59
+ if (!norm.ok)
60
+ return norm;
61
+ const id = generateId('dra');
62
+ const v = norm.value;
63
+ db.prepare(`INSERT INTO direct_receive_accounts (id, seller_id, method, currency, instruction, label, qr_image_ref, status) VALUES (?,?,?,?,?,?,?, 'active')`)
64
+ .run(id, sellerId, v.method, v.currency, v.instruction, v.label, v.qr_image_ref);
65
+ return { ok: true, account: { id, seller_id: sellerId, status: 'active', ...v } };
66
+ }
67
+ /** owner-scoped 文本字段更新(仅该 seller 拥有的行)。返回是否有行被改。
68
+ * ⚠️ 【绝不】碰 qr_image_ref —— 收款码生命周期【只】由 storeQrImage(/qr 端点)管理。
69
+ * 否则卖家改一次收款说明/标签/币种就会把已上传的 QR 解绑(qr_image_ref→null),核心功能掉链子。 */
70
+ export function updateAccount(db, id, sellerId, input) {
71
+ const norm = normalizeAccountInput(input);
72
+ if (!norm.ok)
73
+ return norm;
74
+ const v = norm.value;
75
+ const info = db.prepare(`UPDATE direct_receive_accounts SET method = ?, currency = ?, instruction = ?, label = ?, updated_at = datetime('now') WHERE id = ? AND seller_id = ?`).run(v.method, v.currency, v.instruction, v.label, id, sellerId);
76
+ return { ok: true, changed: info.changes > 0 };
77
+ }
78
+ /** owner-scoped 软停用。返回是否有行被停用。 */
79
+ export function deactivateAccount(db, id, sellerId) {
80
+ const info = db.prepare("UPDATE direct_receive_accounts SET status = 'inactive', updated_at = datetime('now') WHERE id = ? AND seller_id = ? AND status = 'active'").run(id, sellerId);
81
+ return info.changes > 0;
82
+ }