@seasonkoh/webaz 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +4 -3
  2. package/README.zh-CN.md +7 -6
  3. package/dist/currency.js +16 -0
  4. package/dist/deposit-rails.js +52 -0
  5. package/dist/direct-pay-aml-monitor.js +67 -0
  6. package/dist/direct-pay-aml-review.js +40 -0
  7. package/dist/direct-pay-base-bond-entry.js +5 -0
  8. package/dist/direct-pay-bond-rail-clearance.js +94 -0
  9. package/dist/direct-pay-compliance-ingress.js +145 -0
  10. package/dist/direct-pay-controls.js +136 -0
  11. package/dist/direct-pay-create.js +162 -0
  12. package/dist/direct-pay-deferral-quota.js +43 -0
  13. package/dist/direct-pay-disclosures.js +44 -0
  14. package/dist/direct-pay-eligibility.js +46 -0
  15. package/dist/direct-pay-fee-ar.js +241 -0
  16. package/dist/direct-pay-launch-readiness.js +108 -0
  17. package/dist/direct-pay-launch-summary.js +68 -0
  18. package/dist/direct-pay-ledger.js +94 -0
  19. package/dist/direct-receive-account-qr.js +77 -0
  20. package/dist/direct-receive-accounts.js +82 -0
  21. package/dist/direct-receive-deferral.js +142 -0
  22. package/dist/direct-receive-deposits.js +260 -0
  23. package/dist/direct-receive-payment-instruction.js +26 -0
  24. package/dist/fx-rates.js +71 -0
  25. package/dist/layer0-foundation/L0-1-database/schema.js +531 -1
  26. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
  27. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +41 -0
  28. package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
  29. package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
  30. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +80 -0
  31. package/dist/layer1-agent/L1-1-mcp-server/server.js +251 -69
  32. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
  33. package/dist/ledger.js +12 -3
  34. package/dist/mcp.js +23 -4
  35. package/dist/merchant-bond-domain.js +64 -0
  36. package/dist/merchant-bond-exposure.js +78 -0
  37. package/dist/merchant-bond-watcher.js +26 -0
  38. package/dist/payment-rails.js +29 -0
  39. package/dist/product-verification.js +93 -0
  40. package/dist/pwa/acp-feed.js +3 -2
  41. package/dist/pwa/contract-fingerprint.js +3 -0
  42. package/dist/pwa/direct-pay-guards.js +20 -0
  43. package/dist/pwa/direct-pay-order-redaction.js +24 -0
  44. package/dist/pwa/endpoint-actions.js +3 -0
  45. package/dist/pwa/public/app-account.js +977 -0
  46. package/dist/pwa/public/app-admin.js +608 -0
  47. package/dist/pwa/public/app-agents.js +63 -0
  48. package/dist/pwa/public/app-ai.js +2162 -0
  49. package/dist/pwa/public/app-chat-poll.js +29 -0
  50. package/dist/pwa/public/app-contribution.js +836 -0
  51. package/dist/pwa/public/app-create-kinds.js +17 -0
  52. package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
  53. package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
  54. package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
  55. package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
  56. package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
  57. package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
  58. package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
  59. package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
  60. package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
  61. package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
  62. package/dist/pwa/public/app-direct-pay.js +227 -0
  63. package/dist/pwa/public/app-discover.js +1296 -0
  64. package/dist/pwa/public/app-external-links.js +32 -0
  65. package/dist/pwa/public/app-listings.js +226 -0
  66. package/dist/pwa/public/app-prelaunch-waz.js +39 -0
  67. package/dist/pwa/public/app-price.js +55 -0
  68. package/dist/pwa/public/app-product-media.js +15 -0
  69. package/dist/pwa/public/app-profile.js +1692 -0
  70. package/dist/pwa/public/app-seller.js +199 -0
  71. package/dist/pwa/public/app-shop.js +1145 -0
  72. package/dist/pwa/public/app.js +13388 -22277
  73. package/dist/pwa/public/i18n.js +413 -196
  74. package/dist/pwa/public/index.html +27 -0
  75. package/dist/pwa/public/openapi.json +579 -1
  76. package/dist/pwa/routes/admin-analytics.js +4 -2
  77. package/dist/pwa/routes/admin-direct-receive-deposits.js +321 -0
  78. package/dist/pwa/routes/agent-grants.js +255 -0
  79. package/dist/pwa/routes/buyer-feeds.js +2 -1
  80. package/dist/pwa/routes/chat.js +6 -1
  81. package/dist/pwa/routes/dashboards.js +2 -1
  82. package/dist/pwa/routes/direct-pay-availability.js +196 -0
  83. package/dist/pwa/routes/direct-pay-disclosure-acks.js +74 -0
  84. package/dist/pwa/routes/direct-pay-timeouts.js +71 -0
  85. package/dist/pwa/routes/direct-receive-accounts.js +155 -0
  86. package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
  87. package/dist/pwa/routes/fx.js +12 -0
  88. package/dist/pwa/routes/leaderboard.js +47 -9
  89. package/dist/pwa/routes/listings.js +2 -2
  90. package/dist/pwa/routes/logistics.js +4 -0
  91. package/dist/pwa/routes/manifests.js +38 -0
  92. package/dist/pwa/routes/me-data.js +5 -2
  93. package/dist/pwa/routes/orders-action.js +117 -9
  94. package/dist/pwa/routes/orders-create.js +4 -0
  95. package/dist/pwa/routes/orders-read.js +36 -0
  96. package/dist/pwa/routes/p2p-products.js +2 -2
  97. package/dist/pwa/routes/products-create.js +5 -3
  98. package/dist/pwa/routes/products-links.js +34 -0
  99. package/dist/pwa/routes/products-list.js +2 -1
  100. package/dist/pwa/routes/products-update.js +22 -2
  101. package/dist/pwa/routes/promoter.js +3 -0
  102. package/dist/pwa/routes/referral.js +4 -0
  103. package/dist/pwa/routes/returns.js +26 -1
  104. package/dist/pwa/routes/rewards-apply.js +10 -5
  105. package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
  106. package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
  107. package/dist/pwa/routes/shops.js +2 -1
  108. package/dist/pwa/routes/url-claim.js +2 -2
  109. package/dist/pwa/routes/users-public.js +8 -0
  110. package/dist/pwa/routes/wallet-read.js +3 -0
  111. package/dist/pwa/routes/webauthn.js +7 -2
  112. package/dist/pwa/server-schema.js +9 -0
  113. package/dist/pwa/server.js +261 -1706
  114. package/dist/runtime/agent-grant-scopes.js +128 -0
  115. package/dist/runtime/agent-grant-verifier.js +67 -0
  116. package/dist/runtime/agent-pairing.js +60 -0
  117. package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
  118. package/dist/runtime/webaz-schema-helpers.js +1952 -0
  119. package/dist/store-verification.js +77 -0
  120. package/dist/version.js +1 -1
  121. package/package.json +80 -2
@@ -0,0 +1,162 @@
1
+ import { feeUnitsForOrder, estimateOpenDirectPayFeeUnits, readAvailableFeePrepayUnits, sellerDirectPayGraceEligible, feePrepayGateOk } from './direct-pay-fee-ar.js';
2
+ import { sellerBaseBondEntrySatisfied } from './direct-pay-base-bond-entry.js';
3
+ import { getActivePaymentInstruction } from './direct-receive-payment-instruction.js';
4
+ import { getAccount } from './direct-receive-accounts.js'; // Rail1 D2:买家所选多收款账号(dual-read;缺省回落 legacy 单条 instruction)
5
+ import { evaluateDirectPayLaunchControls, readDirectPayControlsConfig, sellerDirectPayKybPassed, sellerDirectPaySanctionsClear, sellerDirectPayAmlClear, sellerDirectPayBreakerTripped, coarsenBuyerFacingDirectPayCode } from './direct-pay-controls.js';
6
+ import { checkDeferralQuota, readDeferralQuotaConfig } from './direct-pay-deferral-quota.js';
7
+ import { enforceCollateralExposureGate } from './merchant-bond-exposure.js'; // §6.5 抵押背书开放敞口上限(休眠安全:collateral=0 时 N/A)
8
+ import { productStoreVerified } from './product-verification.js';
9
+ import { sellerExemptFromPerProduct } from './store-verification.js';
10
+ import { safeRunDirectPayAmlMonitor } from './direct-pay-aml-monitor.js';
11
+ /** 原子创建 direct_p2p 订单。成功返回 { orderId };任一步失败抛错(调用方回 409,事务已回滚)。 */
12
+ export function createDirectPayOrder(db, deps, args) {
13
+ const { generateId, transition, appendOrderEvent } = deps;
14
+ const orderId = generateId('ord');
15
+ db.transaction(() => {
16
+ // 本金不入协议:escrow_amount=0,不写 buyer wallet。同一 INSERT 写入【入口控制 policy 快照】(frozen-at-create)。
17
+ const s = args.snapshot;
18
+ db.prepare(`INSERT INTO orders (id, product_id, buyer_id, seller_id, quantity, unit_price, total_amount, escrow_amount,
19
+ status, payment_rail, shipping_address, direct_pay_instruction_snapshot, direct_pay_account_snapshot, direct_pay_window_deadline,
20
+ direct_pay_enabled_snapshot, direct_pay_rail_breaker_snapshot, direct_pay_region_snapshot,
21
+ direct_pay_region_allowlist_snapshot, direct_pay_per_tx_cap_units_snapshot, direct_pay_seller_breaker_snapshot, direct_pay_decision_code)
22
+ VALUES (?,?,?,?,?,?,?,0,'created','direct_p2p',?,?,?,?, ?,?,?,?,?,?,?)`)
23
+ .run(orderId, args.productId, args.buyerId, args.sellerId, args.quantity, args.unitPrice, args.totalAmount, args.shippingAddress, args.instructionSnapshot, args.accountSnapshot ? JSON.stringify(args.accountSnapshot) : null, args.windowDeadlineIso, s.enabled ? 1 : 0, s.railBreakerTripped ? 1 : 0, s.region, JSON.stringify(s.regionAllowlist), s.perTxCapUnits, s.sellerBreakerTripped ? 1 : 0, s.decisionCode);
24
+ appendOrderEvent(db, {
25
+ orderId, eventType: 'open', fromStatus: null, toStatus: 'created', actorId: args.buyerId, actorRole: 'buyer',
26
+ extra: { product_id: args.productId, seller_id: args.sellerId, quantity: args.quantity, total_amount: args.totalAmount, payment_rail: 'direct_p2p' },
27
+ });
28
+ // created → direct_pay_window(system-only edge);失败回滚。
29
+ const rc = transition(db, orderId, 'direct_pay_window', 'sys_protocol', [], 'Rail1 直付:进入付款窗口(平台费完成时记应收,建单不收费)');
30
+ if (!rc.success)
31
+ throw new Error(rc.error || 'transition→direct_pay_window failed');
32
+ // 【无建单资金写】平台费完成时 accrue;建单门是【首单宽限 + 预充值续用】只读门(在 createDirectPayResponse 内,建单前)。
33
+ // 扣库存(原子;售罄即抛回滚)。变体/flash 直付 v1 不支持。
34
+ const upd = db.prepare('UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?').run(args.quantity, args.productId, args.quantity);
35
+ if (upd.changes !== 1)
36
+ throw new Error('stock depleted');
37
+ })();
38
+ return { orderId };
39
+ }
40
+ /**
41
+ * direct_p2p 建单【完整分叉处理】(供 orders-create.ts 单行调用,保持该 route 文件不臌胀)。
42
+ * 顺序:① v1 only-simple-product 门(任一 escrow-only 修饰 → fail-closed)→ ② 生产 base-bond 门(production receipt,
43
+ * 非仅 privilege active)→ ③ 收款指令门(只读+快照)→ ④ 原子建单。任一门未过 → 直接写 res 并 return,
44
+ * 【绝不】建单 / 锁质押 / 扣库存。不碰 buyer wallet/escrow/principal/refund/settlement。
45
+ */
46
+ export function createDirectPayResponse(res, db, deps, ctx) {
47
+ // ① direct_p2p v1 = simple product only。escrow-only 修饰一律 fail-closed(本片不支持,不半支持)。
48
+ const o = ctx.opts ?? {};
49
+ if (o.hasVariants || o.variantId != null) {
50
+ res.status(409).json({ error: '直付 v1 仅支持简单商品(无规格);该商品有规格或传了 variant_id', error_code: 'DIRECT_PAY_SIMPLE_PRODUCT_ONLY' });
51
+ return;
52
+ }
53
+ const unsupported = o.flashActive ? 'flash_sale' : o.couponCode ? 'coupon' : o.buyInsurance ? 'insurance' : (Number(o.donationPct) > 0) ? 'donation' : o.isGift ? 'gift' : o.anonymous ? 'anonymous_recipient' : o.deliveryWindow ? 'delivery_window' : null;
54
+ if (unsupported) {
55
+ res.status(409).json({ error: `直付 v1 不支持该选项:${unsupported}`, error_code: 'DIRECT_PAY_UNSUPPORTED_OPTION', option: unsupported });
56
+ return;
57
+ }
58
+ const sellerId = ctx.product.seller_uid;
59
+ // ② Phase 4a 入口控制(SSOT,默认 fail-closed):全局开关/熔断 → 地区白名单 → 单笔上限 → production base-bond → KYC/制裁 → AML 断路器。
60
+ // 任一不过即拒(不建单/不锁质押/不扣库存)。base-bond 已折进控制面(DIRECT_PAY_NOT_AVAILABLE),不再单独判。
61
+ const cfg = readDirectPayControlsConfig(deps.getProtocolParam);
62
+ const sellerBreakerTripped = sellerDirectPayBreakerTripped(db, sellerId);
63
+ const ctrl = evaluateDirectPayLaunchControls(cfg, {
64
+ amountUnits: ctx.totalAmountU,
65
+ sellerBreakerTripped,
66
+ baseBondSatisfied: sellerBaseBondEntrySatisfied(db, sellerId, new Date().toISOString()),
67
+ kycSanctionsPassed: sellerDirectPayKybPassed(db, sellerId) && sellerDirectPaySanctionsClear(db, sellerId),
68
+ amlClear: sellerDirectPayAmlClear(db, sellerId),
69
+ });
70
+ // control deny 发生在【任何 DB write / order insert / fee-stake lock / stock decrement 之前】(fail-closed)。
71
+ // 买家面脱敏:卖家私密拒因(暂停/保证金/KYC·制裁/AML)收敛为通用 SELLER_NOT_ELIGIBLE,与 availability 同源,
72
+ // 不向买家泄露卖家具体合规状态;全局/运营类(DISABLED/REGION/CAP)原样透出。精确 code 由 controls 单测覆盖。
73
+ if (!ctrl.ok) {
74
+ const code = coarsenBuyerFacingDirectPayCode(ctrl.error_code);
75
+ res.status(ctrl.status).json({ error_code: code, error: code === ctrl.error_code ? ctrl.reason : '该卖家暂不支持直付' });
76
+ return;
77
+ }
78
+ // 硬门:该产品必须【单独】通过验证 —— 防"验证一个店再上架一堆假货"。豁免路径:卖家店铺已 verified 且被 admin 勾选
79
+ // per_product_exempt(申请一次店铺即可),则其所有商品免逐品。combiner = productVerified OR sellerExempt。
80
+ // 都不满足 → direct-pay 不可用(产品级、非敏感,买家退回托管轨)。fail-closed,在任何 DB write 前。
81
+ if (!(productStoreVerified(db, ctx.product.id) || sellerExemptFromPerProduct(db, sellerId))) {
82
+ res.status(409).json({ error_code: 'DIRECT_PAY_PRODUCT_NOT_VERIFIED', error: '该商品暂不支持直付(待平台验证),请使用托管交易' });
83
+ return;
84
+ }
85
+ // 收款目标解析(dual-read):买家选了具体收款账号 → 用该账号(须属本卖家且 active,否则 fail-closed,【绝不】静默回落);
86
+ // 没选 → 回落 legacy 单条 instruction(向后兼容:只有旧单条说明的卖家照旧可下单)。快照冻结买家所见,卖家事后改/停用不影响。
87
+ let instructionSnapshot;
88
+ let accountSnapshot = null;
89
+ const chosenAccountId = ctx.directReceiveAccountId;
90
+ if (chosenAccountId) {
91
+ const acc = getAccount(db, chosenAccountId);
92
+ if (!acc || acc.seller_id !== sellerId || acc.status !== 'active') {
93
+ res.status(409).json({ error: '所选收款账号无效或已停用', error_code: 'DIRECT_RECEIVE_ACCOUNT_INVALID' });
94
+ return;
95
+ }
96
+ instructionSnapshot = acc.instruction;
97
+ accountSnapshot = { account_id: acc.id, method: acc.method, currency: acc.currency, label: acc.label, qr_ref: acc.qr_image_ref };
98
+ }
99
+ else {
100
+ const instr = getActivePaymentInstruction(db, sellerId);
101
+ if (!instr) {
102
+ res.status(409).json({ error: '卖家未设置收款说明,无法创建直付订单', error_code: 'NO_PAYMENT_INSTRUCTION' });
103
+ return;
104
+ }
105
+ instructionSnapshot = instr.instruction;
106
+ }
107
+ // ③ 缓交期额度门(launch blocker):靠 active deferral 入场(无生产 bond)的卖家,缓交期内笔数/累计金额压低。
108
+ // 控制面全过后、任何 DB write 之前判(fail-closed);非缓交卖家 = no-op。超额 → 409,绝不建单。
109
+ const quota = checkDeferralQuota(db, sellerId, ctx.totalAmountU, new Date().toISOString(), readDeferralQuotaConfig(deps.getProtocolParam));
110
+ // 买家面脱敏:缓交额度拒因(笔数/金额)也收敛为通用 SELLER_NOT_ELIGIBLE,不向买家泄露卖家处于缓交/超额。
111
+ // 精确 code(quota.code)留在 checkDeferralQuota 返回值 + 其单测,供运营/调试。
112
+ if (!quota.ok) {
113
+ res.status(409).json({ error_code: coarsenBuyerFacingDirectPayCode(quota.code), error: '该卖家暂不支持直付' });
114
+ return;
115
+ }
116
+ // §6.5 抵押背书开放敞口上限(backend create-gate)。休眠安全:无真实链上抵押(collateral=0)→ N/A;
117
+ // 有抵押才读 exposure_factor_bps(fail-closed)+ 比较 open_exposure+new ≤ collateral×bps/10000。
118
+ // 当前 merchant_bond 关闭、无 active 存款 → 对缓交卖家零影响(返回 ok)。买家面脱敏。
119
+ const expGate = enforceCollateralExposureGate(db, sellerId, ctx.totalAmountU, deps.getProtocolParam);
120
+ if (!expGate.ok) {
121
+ res.status(409).json({ error_code: coarsenBuyerFacingDirectPayCode(expGate.error_code), error: '该卖家暂不支持直付' });
122
+ return;
123
+ }
124
+ const feeU = feeUnitsForOrder(ctx.totalAmountU, ctx.product.source);
125
+ // 平台服务费【首单宽限 + 预充值续用门】(替代旧 fee-stake 预付,非赊账):
126
+ // ① 首单宽限:商家从无 direct_p2p 成交且无在途单 → 放行第一笔(降低首次使用摩擦;其余资格门照旧已判)。
127
+ // ② 非首单:available_prepay(Σ预充值 + Σ调整 − Σ已计提费)≥ 在途单预估费 + 本单预估费,否则拒。
128
+ // 纯只读门(无任何资金写),在任何 DB write 前。买家面脱敏(FEE_PREPAY_INSUFFICIENT → SELLER_NOT_ELIGIBLE);
129
+ // fail-closed:grace 查询异常→不给宽限;available 读不到→0→拒非首单。预付款=商家平台服务费,非买家货款/escrow/保证金。
130
+ if (!feePrepayGateOk({
131
+ graceEligible: sellerDirectPayGraceEligible(db, sellerId),
132
+ availablePrepayUnits: readAvailableFeePrepayUnits(db, sellerId),
133
+ openOrdersEstFeeUnits: estimateOpenDirectPayFeeUnits(db, sellerId),
134
+ newOrderFeeUnits: feeU,
135
+ })) {
136
+ res.status(409).json({ error_code: coarsenBuyerFacingDirectPayCode('FEE_PREPAY_INSUFFICIENT'), error: '该卖家暂不支持直付' });
137
+ return;
138
+ }
139
+ const windowHours = deps.getProtocolParam('direct_pay.payment_window_hours', 4);
140
+ try {
141
+ const { orderId } = createDirectPayOrder(db, deps, {
142
+ productId: ctx.product.id, sellerId, buyerId: ctx.buyerId, quantity: ctx.reqQty,
143
+ unitPrice: ctx.basePrice, totalAmount: ctx.totalAmount,
144
+ instructionSnapshot, accountSnapshot, windowDeadlineIso: new Date(Date.now() + windowHours * 3600_000).toISOString(),
145
+ shippingAddress: ctx.shippingAddress,
146
+ // frozen-at-create policy 快照:control 全过(ctrl.ok)才到此,decisionCode='OK'。
147
+ snapshot: { enabled: cfg.enabled, railBreakerTripped: cfg.railBreakerTripped, region: cfg.region, regionAllowlist: cfg.regionAllowlist, perTxCapUnits: cfg.perTxCapUnits, sellerBreakerTripped, decisionCode: 'OK' },
148
+ });
149
+ // PR-6C: 建单事务已提交后,运行【append-only AML 监控】(fail-soft;命中治理阈值即 append aml_flags,仅影响【后续】
150
+ // create/availability 的 #107 breaker)。safe 包装吞异常 → 监控失败【绝不】回流成建单失败,也不碰当前订单。
151
+ safeRunDirectPayAmlMonitor(db, { sellerId, orderId, nowIso: new Date().toISOString(), getProtocolParam: deps.getProtocolParam });
152
+ // ⚠️ 不在 create 响应里下发卖家收款说明(payment_instruction/label)——D1/D2 both-acked 前不得泄露(响应契约门,
153
+ // 非仅 UI 软门)。买家先完成披露 ack,再经 GET /orders/:id 读取 redaction-gated 的 direct_pay_instruction_snapshot。
154
+ res.json({
155
+ success: true, order_id: orderId, status: 'direct_pay_window', payment_rail: 'direct_p2p',
156
+ note: '本金不经 WebAZ;完成 D1/D2 风险确认(Passkey)后即可在订单页查看卖家收款说明,请【场外】付款后点"我已付款"。本档无经济保障、不退款,仅对卖家信誉处罚。',
157
+ });
158
+ }
159
+ catch (e) {
160
+ res.status(409).json({ error: '直付订单创建失败:' + e.message, error_code: 'DIRECT_PAY_CREATE_FAILED' });
161
+ }
162
+ }
@@ -0,0 +1,43 @@
1
+ import { getActiveDeferral } from './direct-receive-deferral.js';
2
+ import { sellerHasProductionBaseBondLocked } from './direct-receive-deposits.js';
3
+ import { toUnits } from './money.js';
4
+ export const DEFERRAL_QUOTA_CODES = {
5
+ COUNT: 'DIRECT_PAY_DEFERRAL_QUOTA_EXCEEDED',
6
+ AMOUNT: 'DIRECT_PAY_DEFERRAL_AMOUNT_EXCEEDED',
7
+ };
8
+ /** 从治理参数读取额度配置(全部带保守默认;真值由治理调)。 */
9
+ export function readDeferralQuotaConfig(getProtocolParam) {
10
+ const windowDays = Math.max(1, Math.floor(getProtocolParam('direct_pay.deferral_window_days', 30)));
11
+ const baseOrderCount = Math.max(1, Math.floor(getProtocolParam('direct_pay.deferral_base_order_count', 20)));
12
+ const maxWindowAmountUnits = Math.max(0, Math.floor(getProtocolParam('direct_pay.deferral_max_window_amount_units', toUnits(500))));
13
+ return { windowDays, baseOrderCount, maxWindowAmountUnits };
14
+ }
15
+ /** SQLite datetime('now') 格式的窗口起点('YYYY-MM-DD HH:MM:SS'),用于和 orders.created_at 同格式比较。 */
16
+ function windowStartSql(nowIso, windowDays) {
17
+ return new Date(Date.parse(nowIso) - windowDays * 86_400_000).toISOString().slice(0, 19).replace('T', ' ');
18
+ }
19
+ /**
20
+ * 缓交期额度检查。返回 ok 或结构化拒因(COUNT / AMOUNT)。newOrderAmountUnits = 本次拟建单金额(units)。
21
+ * 非缓交卖家(有生产 bond 或无 active deferral)→ 直接 ok(本检查不适用)。
22
+ */
23
+ export function checkDeferralQuota(db, sellerId, newOrderAmountUnits, nowIso, cfg) {
24
+ // 有生产保证金 → 不在缓交期 → 不压(即便同时存在 deferral,以真实担保物为准)。
25
+ if (sellerHasProductionBaseBondLocked(db, sellerId))
26
+ return { ok: true };
27
+ const active = getActiveDeferral(db, sellerId, nowIso);
28
+ if (!active)
29
+ return { ok: true }; // 非缓交卖家 → no-op
30
+ const since = windowStartSql(nowIso, cfg.windowDays);
31
+ const rows = db.prepare(`SELECT total_amount FROM orders WHERE seller_id = ? AND payment_rail = 'direct_p2p' AND status != 'cancelled' AND created_at >= ?`).all(sellerId, since);
32
+ // ① 笔数上限(factor 缩放,下限 ≥ 1)。
33
+ const countLimit = Math.max(1, Math.floor(cfg.baseOrderCount * active.reducedQuotaFactor));
34
+ if (rows.length + 1 > countLimit) {
35
+ return { ok: false, code: DEFERRAL_QUOTA_CODES.COUNT, reason: `缓交期内直付订单数已达上限(${countLimit} 单/${cfg.windowDays} 天);交纳履约保证金后可恢复满额` };
36
+ }
37
+ // ② 累计金额绝对封顶(units;不随 factor 缩放)。
38
+ const windowAmountU = (rows.reduce((s, r) => s + toUnits(Number(r.total_amount) || 0), 0) + newOrderAmountUnits);
39
+ if (windowAmountU > cfg.maxWindowAmountUnits) {
40
+ return { ok: false, code: DEFERRAL_QUOTA_CODES.AMOUNT, reason: `缓交期内直付累计金额已达上限;交纳履约保证金后可恢复满额` };
41
+ }
42
+ return { ok: true };
43
+ }
@@ -0,0 +1,44 @@
1
+ export const STAGE = { PRE_SELECT: 'pre_select', PRE_CONFIRM: 'pre_confirm' };
2
+ /** D1 = PRE_SELECT 提醒(买家单视角,无任何卖家机制)。 */
3
+ export const D1 = {
4
+ stage: STAGE.PRE_SELECT,
5
+ version: 'd1.v1.2026-06-27',
6
+ zh: '本单无经济保障,出问题不退款,仅对卖家作信誉处罚。请自行判断风险后付款。',
7
+ en: 'This order has NO economic protection: no refund if it goes wrong, only reputation penalties on the seller. Decide your own risk before paying.',
8
+ };
9
+ /** D2 = PRE_CONFIRM 提醒(paid-but-timeout + 最终风险再确认)。 */
10
+ export const D2 = {
11
+ stage: STAGE.PRE_CONFIRM,
12
+ version: 'd2.v1.2026-06-27',
13
+ zh: "付款后请立即点'我已付款';若窗口过期你须发起争议,本档不退款、仅信誉处罚。确认承担风险后再付款。",
14
+ en: "Mark 'I have paid' immediately after paying; if the window lapses you must raise a dispute — this tier issues no refund, only reputation penalties. Confirm you accept the risk before paying.",
15
+ };
16
+ const STAGE_VERSION = { [STAGE.PRE_SELECT]: D1.version, [STAGE.PRE_CONFIRM]: D2.version };
17
+ /** 买家面披露载荷(只含买家自己的部分)。 */
18
+ export function getBuyerDisclosures() {
19
+ return { preSelect: D1, preConfirm: D2 };
20
+ }
21
+ /** 记录一次提醒 ack(append-only;同单同 stage 幂等)。stage 决定 notice_version。 */
22
+ export function recordDisclosureAck(db, args) {
23
+ db.prepare(`INSERT OR IGNORE INTO direct_pay_disclosure_acks (id, order_id, buyer_id, stage, notice_version, acked_at)
24
+ VALUES (?, ?, ?, ?, ?, datetime('now'))`).run(args.ackId, args.orderId, args.buyerId, args.stage, STAGE_VERSION[args.stage]);
25
+ }
26
+ /** 某 stage 是否已 ack(且版本为当前)。 */
27
+ export function disclosureStageAcked(db, orderId, stage) {
28
+ return !!db.prepare('SELECT 1 FROM direct_pay_disclosure_acks WHERE order_id = ? AND stage = ? AND notice_version = ?')
29
+ .get(orderId, stage, STAGE_VERSION[stage]);
30
+ }
31
+ /**
32
+ * 最终确认契约门:pre_select + pre_confirm 两次提醒都已 ack 且版本为当前,才放行。
33
+ * 缺任一 → 硬失败(最终确认/付款确认拒绝)。这是「无经济保障、风险自担」边界的证据门 —— 不可压成单次 ack。
34
+ */
35
+ export function requireBothDisclosuresAcked(db, orderId) {
36
+ const missing = [];
37
+ if (!disclosureStageAcked(db, orderId, STAGE.PRE_SELECT))
38
+ missing.push(STAGE.PRE_SELECT);
39
+ if (!disclosureStageAcked(db, orderId, STAGE.PRE_CONFIRM))
40
+ missing.push(STAGE.PRE_CONFIRM);
41
+ if (missing.length)
42
+ return { ok: false, error_code: 'DISCLOSURE_NOT_ACKED', reason: `直付最终确认前必须完成两次风险提醒:缺 ${missing.join(',')}`, missing };
43
+ return { ok: true };
44
+ }
@@ -0,0 +1,46 @@
1
+ /** §10.3 LOCKED 默认(治理可调:protocol_params 'direct_pay.min_account_age_days')。 */
2
+ export const DEFAULT_DIRECT_RECEIVE_ELIGIBILITY = { minAccountAgeDays: 30 };
3
+ const isFiniteNum = (x) => typeof x === 'number' && Number.isFinite(x);
4
+ /** 合法非负 base-units:整数 + 在安全整数范围内(对齐 money.ts assertUnits 的 finite+integer+|u|≤MAX_SAFE)。
5
+ * Units 是【整数】base-units(money.ts §21)—— 分数(如 500.5)不是合法 units,必须 fail-closed 拒绝。 */
6
+ const isNonNegUnits = (x) => typeof x === 'number' && Number.isSafeInteger(x) && x >= 0;
7
+ /**
8
+ * 入门门判定(纯、fail-closed、total)。eligible ⇔ 四项全过;返回【所有】未过项,便于 UI 显示完整清单。
9
+ * 任何缺失/坏值都收敛到"不过 + reason",不抛错。
10
+ */
11
+ export function evaluateDirectReceiveEligibility(facts, config = DEFAULT_DIRECT_RECEIVE_ELIGIBILITY) {
12
+ const f = facts ?? {};
13
+ const minAge = isFiniteNum(config?.minAccountAgeDays) && config.minAccountAgeDays >= 0
14
+ ? config.minAccountAgeDays
15
+ : DEFAULT_DIRECT_RECEIVE_ELIGIBILITY.minAccountAgeDays;
16
+ // 正向事实 —— 必须显式满足,否则 fail-closed
17
+ const kyc = f.kycVerified === true;
18
+ const sanctions = f.sanctionsCleared === true;
19
+ const accountAge = isFiniteNum(f.accountAgeDays) && f.accountAgeDays >= minAge;
20
+ // base bond:两边都必须是合法非负整数 base-units(分数/非安全整数 → fail-closed);
21
+ // required 还必须【正】(spec:always some bond, never 0),否则无从对照 → insufficient。
22
+ const baseBond = isNonNegUnits(f.requiredBaseBondUnits) && f.requiredBaseBondUnits > 0
23
+ && isNonNegUnits(f.baseBondLockedUnits) && f.baseBondLockedUnits >= f.requiredBaseBondUnits;
24
+ const reasons = [];
25
+ if (!kyc)
26
+ reasons.push('KYC_NOT_VERIFIED');
27
+ if (!sanctions)
28
+ reasons.push('SANCTIONS_NOT_CLEARED');
29
+ if (!accountAge)
30
+ reasons.push('ACCOUNT_TOO_NEW');
31
+ if (!baseBond)
32
+ reasons.push('BASE_BOND_INSUFFICIENT');
33
+ return { eligible: reasons.length === 0, reasons, checks: { kyc, sanctions, accountAge, baseBond } };
34
+ }
35
+ /** 账龄(整天,floor)。纯:now 由调用方传入(便于测试/确定性)。无法解析/未来时间 → 0(fail-closed:视作最新账户)。 */
36
+ export function accountAgeDays(createdAtIso, nowIso) {
37
+ if (!createdAtIso)
38
+ return 0;
39
+ const created = Date.parse(createdAtIso);
40
+ const now = Date.parse(nowIso);
41
+ if (!Number.isFinite(created) || !Number.isFinite(now) || now < created)
42
+ return 0;
43
+ return Math.floor((now - created) / 86_400_000);
44
+ }
45
+ // NOTE: minAccountAgeDays 的 protocol_params 装配(`direct_pay.min_account_age_days`,缺行回落 DEFAULT)+
46
+ // KYC/制裁/base-bond 事实的 DB 装配 → 由 PR-4c 的 route adapter 提供;本模块刻意【保持纯】(不读库)。
@@ -0,0 +1,241 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { toUnits, toDecimal, mulRate } from './money.js';
3
+ import { releaseFeeStake } from './direct-pay-ledger.js';
4
+ export const FEE_AR_CURRENCY = 'usdc'; // v1 单一计价币
5
+ /** 平台费率单一真相源:二手 1% / 其它 2%(与 settleOrder escrow 分支同口径)。create / 完成 accrue / 在途预估 全走这里防漂移。 */
6
+ export function feeUnitsForOrder(totalAmountU, source) {
7
+ return mulRate(totalAmountU, source === 'secondhand' ? 0.01 : 0.02);
8
+ }
9
+ /** 开放(未完全关闭、仍会 accrue 费)的 direct_p2p 单状态。完成单已在 receivables(借记预充值),故【不含】completed/终态。 */
10
+ export const OPEN_FEE_ACCRUING_STATUSES = [
11
+ 'created', 'direct_pay_window', 'direct_expired_unconfirmed',
12
+ 'accepted', 'shipped', 'picked_up', 'in_transit', 'delivered', 'confirmed', 'disputed',
13
+ ];
14
+ /** 某 SUM 查询结果 → units(空表/NULL → 0)。SUM 的是整数 units 值存成的 REAL,toUnits 精确还原。 */
15
+ function sumUnits(db, sql, sellerId) {
16
+ const row = db.prepare(sql).get(sellerId);
17
+ const v = row?.s;
18
+ if (v === null || v === undefined || !Number.isFinite(v))
19
+ return 0;
20
+ return toUnits(v);
21
+ }
22
+ /** 商家已计提平台费合计(units)= Σ receivables.amount。表缺失→0(休眠安全)。 */
23
+ export function getSellerAccruedFeeUnits(db, sellerId) {
24
+ try {
25
+ return sumUnits(db, 'SELECT SUM(amount) AS s FROM direct_pay_fee_receivables WHERE seller_id = ?', sellerId);
26
+ }
27
+ catch {
28
+ return 0;
29
+ }
30
+ }
31
+ /**
32
+ * 商家平台服务费【可用预充值余额】(units)。模型 = 预付款续用(非赊账):
33
+ * available = Σ top-ups(direct_pay_fee_payments, invoice_id IS NULL = 未分配预充值)
34
+ * + Σ adjustments.delta_amount(签名;正=预充值贷记,负=借记;本轮无写入)
35
+ * − Σ receivables.amount(已计提平台费 = 对预充值的借记)
36
+ * 余额可为负 = 首单宽限完成后未充值形成的欠款(见 §grace)。表缺失→0(休眠安全,配合 gate fail-closed)。
37
+ * ⚠️ 这是【商家平台服务费预付款】事实派生,非买家 escrow / 非保证金 / 非 penalty。
38
+ */
39
+ export function readAvailableFeePrepayUnits(db, sellerId) {
40
+ let topups = 0, adjusted = 0, accrued = 0, refunded = 0;
41
+ try {
42
+ topups = sumUnits(db, 'SELECT SUM(amount) AS s FROM direct_pay_fee_payments WHERE seller_id = ? AND invoice_id IS NULL', sellerId);
43
+ }
44
+ catch {
45
+ return 0;
46
+ }
47
+ try {
48
+ adjusted = sumUnits(db, 'SELECT SUM(delta_amount) AS s FROM direct_pay_fee_adjustments WHERE seller_id = ?', sellerId);
49
+ }
50
+ catch {
51
+ adjusted = 0;
52
+ }
53
+ try {
54
+ accrued = sumUnits(db, 'SELECT SUM(amount) AS s FROM direct_pay_fee_receivables WHERE seller_id = ?', sellerId);
55
+ }
56
+ catch {
57
+ accrued = 0;
58
+ }
59
+ try {
60
+ refunded = sumUnits(db, 'SELECT SUM(amount) AS s FROM direct_pay_fee_prepay_refunds WHERE seller_id = ?', sellerId);
61
+ }
62
+ catch {
63
+ refunded = 0;
64
+ }
65
+ return topups + adjusted - accrued - refunded;
66
+ }
67
+ /** 单一真相源:某商家平台服务费账户汇总(admin 视图 / seller fee center 都读它,口径一致)。 */
68
+ export function getDirectPayFeeAccount(db, sellerId) {
69
+ const topupUnits = (() => { try {
70
+ return sumUnits(db, 'SELECT SUM(amount) AS s FROM direct_pay_fee_payments WHERE seller_id = ? AND invoice_id IS NULL', sellerId);
71
+ }
72
+ catch {
73
+ return 0;
74
+ } })();
75
+ const accruedUnits = getSellerAccruedFeeUnits(db, sellerId);
76
+ const adjustmentUnits = (() => { try {
77
+ return sumUnits(db, 'SELECT SUM(delta_amount) AS s FROM direct_pay_fee_adjustments WHERE seller_id = ?', sellerId);
78
+ }
79
+ catch {
80
+ return 0;
81
+ } })();
82
+ const refundUnits = (() => { try {
83
+ return sumUnits(db, 'SELECT SUM(amount) AS s FROM direct_pay_fee_prepay_refunds WHERE seller_id = ?', sellerId);
84
+ }
85
+ catch {
86
+ return 0;
87
+ } })();
88
+ return {
89
+ sellerId,
90
+ availableUnits: topupUnits + adjustmentUnits - accruedUnits - refundUnits,
91
+ topupUnits, accruedUnits, adjustmentUnits, refundUnits,
92
+ openEstFeeUnits: estimateOpenDirectPayFeeUnits(db, sellerId),
93
+ graceEligible: sellerDirectPayGraceEligible(db, sellerId),
94
+ };
95
+ }
96
+ /**
97
+ * 首单宽限资格:商家此前【从无】direct_p2p 成交且【无】在途单 —— 即这是其第一笔 direct_p2p。
98
+ * grace = (completed direct_p2p 数 == 0) AND (open/in-flight direct_p2p 数 == 0)。
99
+ * 杜绝"多笔并发首单都拿宽限":有任何在途单即不再宽限。已完成单不计入(cancelled/expired 等终态不阻 grace,
100
+ * 因其从未计提费、无欠款)。fail-closed:查询异常 → 返回 false(= 不给宽限,要求预充值,安全侧)。
101
+ */
102
+ export function sellerDirectPayGraceEligible(db, sellerId) {
103
+ const ph = OPEN_FEE_ACCRUING_STATUSES.map(() => '?').join(',');
104
+ try {
105
+ const row = db.prepare(`SELECT COUNT(*) AS n FROM orders WHERE seller_id = ? AND payment_rail = 'direct_p2p' AND status IN (${ph}, 'completed')`).get(sellerId, ...OPEN_FEE_ACCRUING_STATUSES);
106
+ return (row?.n ?? 1) === 0;
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
112
+ /**
113
+ * 纯函数:建单【预充值门】判定。
114
+ * - 首单宽限(graceEligible)→ 直接放行(降低首次使用摩擦;其余资格门照旧另判)。
115
+ * - 非首单 → 要求 available_prepay ≥ 在途单预估费 + 本单预估费(预付款须覆盖在途+本单平台费)。
116
+ * 余额不足/为负 → 拒(fail-closed:available 读不到=0=拒非首单)。
117
+ */
118
+ export function feePrepayGateOk(args) {
119
+ if (args.graceEligible)
120
+ return true;
121
+ return args.availablePrepayUnits >= args.openOrdersEstFeeUnits + args.newOrderFeeUnits;
122
+ }
123
+ /**
124
+ * 记录一笔【商家平台服务费预付款】(append-only 事实行,invoice_id NULL = 未分配预充值 → 计入 available)。
125
+ * 唯一 prepay top-up 写入口;由 ROOT + 真人 Passkey 的 admin 端点调用(身份/Passkey 由调用方 route 强制,本 helper 不验证)。
126
+ * ⚠️ 商家平台服务费预付款,非买家 escrow / 非保证金 / 非 penalty。不碰 buyer wallet/escrow/order/settlement。
127
+ * 校验:seller 非空、amount > 0(整数 base-units)、method ∈ {usdc,fiat}。本轮【不做】余额退款(无负额/无 refund kind)。
128
+ */
129
+ export function recordFeePrepayTopup(db, args) {
130
+ if (!args.sellerId)
131
+ return { ok: false, error: 'MISSING_SELLER' };
132
+ if (!Number.isFinite(args.amountUnits) || !Number.isInteger(args.amountUnits) || args.amountUnits <= 0)
133
+ return { ok: false, error: 'AMOUNT_MUST_BE_POSITIVE' };
134
+ if (args.method !== 'usdc' && args.method !== 'fiat')
135
+ return { ok: false, error: 'BAD_METHOD' };
136
+ const id = 'dpfp_' + randomUUID();
137
+ // 台账行 + 统一 admin_audit_log 同事务写(与 KYB/sanctions/AML ingress 一致;money-adjacent admin 动作必留痕)。
138
+ // 审计 detail PII-free:不存 raw evidence_ref / note,仅记 presence。
139
+ db.transaction(() => {
140
+ db.prepare(`INSERT INTO direct_pay_fee_payments (id, seller_id, invoice_id, amount, currency, method, received_at, recorded_by, evidence_ref, note)
141
+ VALUES (?,?,NULL,?, '${FEE_AR_CURRENCY}', ?, datetime('now'), ?, ?, ?)`).run(id, args.sellerId, toDecimal(args.amountUnits), args.method, args.recordedBy, args.evidenceRef ?? null, args.note ?? null);
142
+ db.prepare(`INSERT INTO admin_audit_log (id, admin_id, action, target_type, target_id, detail) VALUES (?,?,?,?,?,?)`)
143
+ .run('dpfpaud_' + randomUUID(), args.recordedBy, 'direct_pay.fee_prepay_record', 'user', args.sellerId, JSON.stringify({ payment_id: id, amount_units: args.amountUnits, method: args.method, evidence_ref_present: !!args.evidenceRef }));
144
+ })();
145
+ return { ok: true, id };
146
+ }
147
+ /**
148
+ * 账务【更正】(adjustments.kind='correction',带符号 delta)。≠ 退款:更正只调记账(不一定动真钱),
149
+ * 退款(recordFeePrepayRefund)是真实退钱。delta 正=增 available / 负=减。append-only + admin_audit_log 同事务。
150
+ */
151
+ export function recordFeePrepayAdjustment(db, args) {
152
+ if (!args.sellerId)
153
+ return { ok: false, error: 'MISSING_SELLER' };
154
+ if (!Number.isFinite(args.deltaUnits) || !Number.isInteger(args.deltaUnits) || args.deltaUnits === 0)
155
+ return { ok: false, error: 'DELTA_MUST_BE_NONZERO_INT' };
156
+ if (!args.reason)
157
+ return { ok: false, error: 'MISSING_REASON' };
158
+ const id = 'dpadj_' + randomUUID();
159
+ db.transaction(() => {
160
+ db.prepare(`INSERT INTO direct_pay_fee_adjustments (id, receivable_id, seller_id, delta_amount, currency, kind, reason, created_at, created_by)
161
+ VALUES (?,NULL,?,?, '${FEE_AR_CURRENCY}', 'correction', ?, datetime('now'), ?)`).run(id, args.sellerId, toDecimal(args.deltaUnits), args.reason, args.recordedBy);
162
+ db.prepare(`INSERT INTO admin_audit_log (id, admin_id, action, target_type, target_id, detail) VALUES (?,?,?,?,?,?)`)
163
+ .run('dpadjaud_' + randomUUID(), args.recordedBy, 'direct_pay.fee_adjust', 'user', args.sellerId, JSON.stringify({ adjustment_id: id, delta_units: args.deltaUnits, reason_present: !!args.reason }));
164
+ })();
165
+ return { ok: true, id };
166
+ }
167
+ /**
168
+ * 余额【退款】(真实退还已预付未消耗的平台服务费;链下退款,记 evidence_ref)。append-only + admin_audit_log 同事务。
169
+ * 校验:seller 非空、amount>0 整数 base-units、method ∈ {usdc,fiat}、**amount ≤ 可退自由余额 = available − 在途预估费**。
170
+ * ⚠️ 退款必须【预留在途单将计提的平台费】(openEst):否则把"已被在途单占用"的预充值退掉,在途单完成计提后卖家会倒欠。
171
+ * 可退额校验 + openEst + 写入【同一 db.transaction】读取(better-sqlite3 同步事务串行 → 无 TOCTOU / 无并发双退)。
172
+ * 退的是商家平台服务费预付款,非买家货款/escrow/保证金。
173
+ */
174
+ export function recordFeePrepayRefund(db, args) {
175
+ if (!args.sellerId)
176
+ return { ok: false, error: 'MISSING_SELLER' };
177
+ if (!Number.isFinite(args.amountUnits) || !Number.isInteger(args.amountUnits) || args.amountUnits <= 0)
178
+ return { ok: false, error: 'AMOUNT_MUST_BE_POSITIVE' };
179
+ if (args.method !== 'usdc' && args.method !== 'fiat')
180
+ return { ok: false, error: 'BAD_METHOD' };
181
+ const id = 'dpref_' + randomUUID();
182
+ let outcome = { ok: true, id };
183
+ db.transaction(() => {
184
+ const refundable = readAvailableFeePrepayUnits(db, args.sellerId) - estimateOpenDirectPayFeeUnits(db, args.sellerId);
185
+ if (args.amountUnits > refundable) {
186
+ outcome = { ok: false, error: 'REFUND_EXCEEDS_AVAILABLE' };
187
+ return;
188
+ }
189
+ db.prepare(`INSERT INTO direct_pay_fee_prepay_refunds (id, seller_id, amount, currency, method, evidence_ref, reason, recorded_by, created_at)
190
+ VALUES (?,?,?, '${FEE_AR_CURRENCY}', ?, ?, ?, ?, datetime('now'))`).run(id, args.sellerId, toDecimal(args.amountUnits), args.method, args.evidenceRef ?? null, args.reason ?? null, args.recordedBy);
191
+ db.prepare(`INSERT INTO admin_audit_log (id, admin_id, action, target_type, target_id, detail) VALUES (?,?,?,?,?,?)`)
192
+ .run('dprefaud_' + randomUUID(), args.recordedBy, 'direct_pay.fee_refund', 'user', args.sellerId, JSON.stringify({ refund_id: id, amount_units: args.amountUnits, method: args.method, evidence_ref_present: !!args.evidenceRef }));
193
+ })();
194
+ return outcome;
195
+ }
196
+ /**
197
+ * 卖家【在途】direct_p2p 单的预估费合计(units)。建单信用门用:防止短时开大量在途单完成后一次性超限。
198
+ * 与已计提 receivables 不重叠:在途单尚未 accrue(不在 receivables);完成即转入 receivables、离开本集合。
199
+ * 表/列缺失(旧库)→ 0(休眠安全)。
200
+ */
201
+ export function estimateOpenDirectPayFeeUnits(db, sellerId) {
202
+ const ph = OPEN_FEE_ACCRUING_STATUSES.map(() => '?').join(',');
203
+ let rows = [];
204
+ try {
205
+ rows = db.prepare(`SELECT total_amount, source FROM orders WHERE seller_id = ? AND payment_rail = 'direct_p2p' AND status IN (${ph})`).all(sellerId, ...OPEN_FEE_ACCRUING_STATUSES);
206
+ }
207
+ catch {
208
+ return 0;
209
+ }
210
+ let sum = 0;
211
+ for (const r of rows)
212
+ sum += feeUnitsForOrder(toUnits(Number(r.total_amount) || 0), r.source);
213
+ return sum;
214
+ }
215
+ /**
216
+ * 完成时记一笔平台费应收(原始 immutable accrual 行)。fail-closed + 幂等:
217
+ * - feeUnits ≤ 0 → 抛(拒绝完成而费用为零;调用方在 settle 同一 db.transaction 内 → 回滚 completed)。
218
+ * - 该单已有 receivable(UNIQUE order_id)→ 返回 'already'(支持重结算,不重复计费)。
219
+ * 必须在调用方的 db.transaction 内调用(与 completed 转移同原子边界)。
220
+ */
221
+ export function accrueFeeReceivable(db, opts) {
222
+ if (opts.feeUnits <= 0)
223
+ throw new Error(`accrueFeeReceivable: fee must be > 0 for order ${opts.orderId} (fail-closed)`);
224
+ const existing = db.prepare('SELECT id FROM direct_pay_fee_receivables WHERE order_id = ?').get(opts.orderId);
225
+ if (existing)
226
+ return { outcome: 'already' }; // 幂等:重结算不重复计费
227
+ db.prepare(`INSERT INTO direct_pay_fee_receivables (id, order_id, seller_id, amount, currency, accrued_at) VALUES (?,?,?,?, '${FEE_AR_CURRENCY}', datetime('now'))`).run(opts.receivableId, opts.orderId, opts.sellerId, toDecimal(opts.feeUnits));
228
+ return { outcome: 'accrued' };
229
+ }
230
+ /**
231
+ * 完成结算时的 direct_p2p 平台费收口(供 settleOrder 单行调用,保持 server.ts 净零)。
232
+ * ① 释放任何遗留模拟 WAZ fee-stake(cutover 前建单;模拟无价值,不取,退回卖家;无则 no-op);
233
+ * ② accrue 一笔链下应收(fail-closed + 幂等)。必须在调用方 settleOrder 的 db.transaction 内调用。
234
+ */
235
+ export function settleDirectPayFeeAtCompletion(db, order, receivableId) {
236
+ releaseFeeStake(db, { orderId: order.id });
237
+ accrueFeeReceivable(db, {
238
+ orderId: order.id, sellerId: order.seller_id,
239
+ feeUnits: feeUnitsForOrder(toUnits(Number(order.total_amount) || 0), order.source), receivableId,
240
+ });
241
+ }