@seasonkoh/webaz 0.1.29 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/bond-refund-blockers.js +50 -0
  2. package/dist/bond-slash.js +100 -0
  3. package/dist/bond-terms.js +31 -0
  4. package/dist/direct-pay-bond-rail-clearance.js +10 -5
  5. package/dist/direct-pay-cancel-refund.js +160 -0
  6. package/dist/direct-pay-create.js +78 -9
  7. package/dist/direct-pay-disclosures.js +17 -11
  8. package/dist/direct-pay-fee-ar.js +2 -2
  9. package/dist/direct-pay-fee-prepay-request.js +80 -0
  10. package/dist/direct-pay-returns.js +104 -0
  11. package/dist/direct-pay-stock.js +9 -0
  12. package/dist/direct-receive-deferral.js +30 -1
  13. package/dist/direct-receive-deposits.js +122 -17
  14. package/dist/free-shipping.js +37 -0
  15. package/dist/fx-rates.js +7 -0
  16. package/dist/layer0-foundation/L0-1-database/schema.js +246 -0
  17. package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
  18. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +73 -0
  19. package/dist/layer1-agent/L1-1-mcp-server/server.js +150 -33
  20. package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
  21. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
  22. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
  23. package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
  24. package/dist/platform-receive-accounts.js +94 -0
  25. package/dist/pwa/arbitration-read-admin.js +37 -0
  26. package/dist/pwa/arbitrator-lifecycle.js +100 -0
  27. package/dist/pwa/contract-fingerprint.js +15 -0
  28. package/dist/pwa/direct-pay-order-redaction.js +20 -2
  29. package/dist/pwa/human-presence.js +2 -6
  30. package/dist/pwa/public/app-account.js +9 -9
  31. package/dist/pwa/public/app-admin-disputes.js +55 -0
  32. package/dist/pwa/public/app-agent-appeal.js +90 -0
  33. package/dist/pwa/public/app-agent-approvals.js +93 -0
  34. package/dist/pwa/public/app-agent-pair.js +127 -0
  35. package/dist/pwa/public/app-arbitrator-admin.js +87 -0
  36. package/dist/pwa/public/app-arbitrator-entry.js +9 -0
  37. package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
  38. package/dist/pwa/public/app-bond-refund-ui.js +66 -0
  39. package/dist/pwa/public/app-bond-slash-ui.js +74 -0
  40. package/dist/pwa/public/app-bond-terms-ui.js +23 -0
  41. package/dist/pwa/public/app-bond-ui.js +108 -0
  42. package/dist/pwa/public/app-chat-poll.js +6 -6
  43. package/dist/pwa/public/app-contribution-hub.js +23 -0
  44. package/dist/pwa/public/app-direct-pay-accounts.js +1 -1
  45. package/dist/pwa/public/app-direct-pay-buyer.js +1 -1
  46. package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
  47. package/dist/pwa/public/app-direct-pay-copy.js +12 -0
  48. package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
  49. package/dist/pwa/public/app-direct-pay-fee-ops.js +1 -1
  50. package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
  51. package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
  52. package/dist/pwa/public/app-direct-pay-memo.js +14 -0
  53. package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
  54. package/dist/pwa/public/app-direct-pay-pay.js +15 -0
  55. package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
  56. package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
  57. package/dist/pwa/public/app-direct-pay-returns.js +56 -0
  58. package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
  59. package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
  60. package/dist/pwa/public/app-direct-pay.js +36 -37
  61. package/dist/pwa/public/app-dispute-close-ui.js +38 -0
  62. package/dist/pwa/public/app-free-shipping-ui.js +29 -0
  63. package/dist/pwa/public/app-gmv-rail-split.js +12 -0
  64. package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
  65. package/dist/pwa/public/app-mutual-cancel.js +54 -0
  66. package/dist/pwa/public/app-notif-templates-orders.js +43 -0
  67. package/dist/pwa/public/app-notif-templates.js +22 -0
  68. package/dist/pwa/public/app-order-accept-ui.js +158 -0
  69. package/dist/pwa/public/app-order-errors.js +50 -0
  70. package/dist/pwa/public/app-order-labels.js +19 -0
  71. package/dist/pwa/public/app-order-rail-filter.js +26 -0
  72. package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
  73. package/dist/pwa/public/app-poll-governor.js +22 -0
  74. package/dist/pwa/public/app-profile.js +4 -4
  75. package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
  76. package/dist/pwa/public/app-sale-regions-ui.js +38 -0
  77. package/dist/pwa/public/app-seller.js +1 -1
  78. package/dist/pwa/public/app-trade-tax-ui.js +33 -0
  79. package/dist/pwa/public/app.js +133 -160
  80. package/dist/pwa/public/i18n.js +675 -5
  81. package/dist/pwa/public/index.html +41 -0
  82. package/dist/pwa/public/openapi.json +719 -11
  83. package/dist/pwa/public/style.css +3 -0
  84. package/dist/pwa/routes/admin-direct-receive-deposits.js +215 -3
  85. package/dist/pwa/routes/admin-protocol-params.js +16 -0
  86. package/dist/pwa/routes/admin-reports.js +31 -5
  87. package/dist/pwa/routes/agent-governance.js +1 -1
  88. package/dist/pwa/routes/agent-grants.js +281 -32
  89. package/dist/pwa/routes/analytics.js +2 -0
  90. package/dist/pwa/routes/arbitrator.js +67 -14
  91. package/dist/pwa/routes/bond-seller.js +162 -0
  92. package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
  93. package/dist/pwa/routes/direct-pay-disclosure-acks.js +9 -4
  94. package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
  95. package/dist/pwa/routes/direct-pay-returns.js +74 -0
  96. package/dist/pwa/routes/direct-pay-timeouts.js +186 -9
  97. package/dist/pwa/routes/disputes-read.js +20 -6
  98. package/dist/pwa/routes/disputes-write.js +91 -33
  99. package/dist/pwa/routes/external-anchors.js +4 -2
  100. package/dist/pwa/routes/fee-prepay-requests.js +66 -0
  101. package/dist/pwa/routes/governance-onboarding.js +46 -8
  102. package/dist/pwa/routes/logistics.js +4 -4
  103. package/dist/pwa/routes/me-data.js +2 -2
  104. package/dist/pwa/routes/mutual-cancel.js +62 -0
  105. package/dist/pwa/routes/orders-action.js +182 -9
  106. package/dist/pwa/routes/orders-create.js +18 -7
  107. package/dist/pwa/routes/orders-read.js +76 -14
  108. package/dist/pwa/routes/platform-receive-accounts.js +111 -0
  109. package/dist/pwa/routes/products-create.js +45 -16
  110. package/dist/pwa/routes/products-update.js +15 -1
  111. package/dist/pwa/routes/profile-identity.js +4 -2
  112. package/dist/pwa/routes/returns.js +39 -8
  113. package/dist/pwa/routes/seller-directpay-report.js +110 -0
  114. package/dist/pwa/routes/seller-quota.js +5 -1
  115. package/dist/pwa/routes/shipping-templates.js +219 -0
  116. package/dist/pwa/routes/snf.js +4 -1
  117. package/dist/pwa/routes/webauthn.js +3 -3
  118. package/dist/pwa/server.js +63 -40
  119. package/dist/runtime/agent-grant-scopes.js +69 -1
  120. package/dist/runtime/webaz-schema-helpers.js +57 -3
  121. package/dist/sale-regions.js +116 -0
  122. package/dist/shipping-templates.js +119 -0
  123. package/dist/trade-tax.js +99 -0
  124. package/dist/trade-terms.js +76 -0
  125. package/dist/version.js +1 -1
  126. package/package.json +61 -2
@@ -0,0 +1,14 @@
1
+ // Direct Pay — 可验证付款附言(参考号)。UI ONLY;不碰钱路。
2
+ // 每单从订单号派生【确定性唯一参考号】,提示买家付款时填入收款附言/备注 → mark_paid 的 note 回填订单时间线
3
+ // (买卖双方可见),相同金额也能靠参考号区分付款方。协议不验证,仅辅助对账。
4
+ // 参考号【只读 + 一键复制】:锁定派生值买家不可改 —— 防"两单复用同一参考号"骗卖家重复确认;中文 t()/英文 i18n.js _EN。
5
+ window.dpPayRef = (orderId) => 'WAZ-' + String(orderId || '').replace(/[^a-zA-Z0-9]/g, '').slice(-8).toUpperCase()
6
+ window.dpMemoInputHtml = (orderId) => {
7
+ const ref = window.dpPayRef(orderId)
8
+ return `<div style="margin-top:8px;border-top:1px dashed #fde68a;padding-top:8px">
9
+ <div style="font-size:11px;color:#92400e;font-weight:600;margin-bottom:3px">${t('付款时请在附言/备注填入(便于卖家核对)')}</div>
10
+ <div style="display:flex;align-items:center;gap:6px"><code id="dp-buyer-memo" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:13px;font-weight:700;letter-spacing:0.5px;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px">${escHtml(ref)}</code>${window.dpCopyBtn ? window.dpCopyBtn(ref) : ''}</div>
11
+ <div style="font-size:10px;color:#9ca3af;margin-top:2px">${t('此参考号系统生成、不可修改;相同金额时卖家靠它区分付款方,标记"我已付款"时自动记入订单流程。')}</div></div>`
12
+ }
13
+ // "我已付款"时:note 恒为【派生参考号】(不再读可改输入框)—— 卖家对账口径唯一,买家无法用别单参考号冒充。
14
+ window.dpReadMemo = (orderId) => `${t('付款参考')}: ${window.dpPayRef(orderId)}`
@@ -0,0 +1,35 @@
1
+ // Direct Pay 货款协商(payment_query)+ 付款窗过期(direct_expired_unconfirmed)前端 —— 标签/动作/说明卡。UI ONLY。
2
+ // payment_query = 双方【协商】非仲裁;expired = 窗口超时未标记付款(宽限期内买家仍可主张已付或确认关单,审计项 H:
3
+ // 此前该状态零 UI 动作=死角)。动作走通用 /orders/:id/action;非托管无退款语义。中文 t()/英文 i18n.js _EN。
4
+ window.dpNegotiationBadge = (status) => status === 'payment_query' ? ['yellow', t('货款协商中')] : null
5
+ window.dpNegotiationLabel = (status) => status === 'payment_query' ? t('货款协商中(非仲裁)') : null
6
+
7
+ // 订单页按状态/角色返回协商动作(仅 direct_p2p 调用)。返回数组=接管;null=交回默认。
8
+ window.dpNegotiationActions = (order, isBuyer, isSeller) => {
9
+ const s = order.status
10
+ if (s === 'payment_query') {
11
+ if (isSeller) return [
12
+ { action: 'confirm_received', label: '确认已收到货款(恢复订单)', style: 'success' },
13
+ { action: 'request_cancel', label: '申请取消订单(买家未回应时)', style: 'secondary' },
14
+ ]
15
+ if (isBuyer) return [
16
+ { action: 'pq_escalate', label: '我已付款 · 提交凭证升级举证', style: 'primary', needsEvidence: true, noteLabel: '付款凭证 / 参考号', evidencePlaceholder: '链上 tx / 银行回执 / 付款参考号(与下单时附言一致)' },
17
+ { action: 'cancel', label: '我未付款 · 取消订单', style: 'secondary' },
18
+ ]
19
+ }
20
+ // 审计项 H:付款窗过期(宽限期)买家双出口 —— 卡点付款的主张已付(证据升级争议),没付的即刻关单(免等 48h 自动取消)
21
+ if (s === 'direct_expired_unconfirmed' && isBuyer) return [
22
+ { action: 'dispute', label: '我已付款 · 提交凭证发起争议', style: 'primary', needsEvidence: true, noteLabel: '付款凭证 / 参考号', evidencePlaceholder: '链上 tx / 银行回执 / 付款参考号(与下单时附言一致)' },
23
+ { action: 'cancel', label: '我未付款 · 关闭订单', style: 'secondary' },
24
+ ]
25
+ // 撤回仲裁【仅限货款协商升级】:后端 DTO can_withdraw_payment_query_dispute 判定;履约类争议拿不到此按钮。
26
+ if (s === 'disputed' && (isBuyer || isSeller) && order.can_withdraw_payment_query_dispute) return [
27
+ { action: 'pq_withdraw', label: '撤回仲裁 · 回到协商', style: 'secondary' },
28
+ ]
29
+ return null
30
+ }
31
+
32
+ // 协商说明卡(payment_query 时展示):解释状态 + 提示买家复述付款参考。
33
+ window.dpNegotiationCard = (order) => (!order || order.status !== 'payment_query') ? '' : `
34
+ <div class="card" style="border:1px solid #fde68a;background:linear-gradient(135deg,#fffbeb,#fef3c7)"><div style="font-size:13px;font-weight:700;color:#92400e;margin-bottom:6px">🔎 ${t('货款协商中(非仲裁)')}</div>
35
+ <div style="font-size:12px;color:#374151;line-height:1.7">${t('卖家报告未收到货款。这是买卖双方【协商】阶段,不是仲裁:若你确已付款,请提交付款凭证升级举证;若未付款,可取消订单。谈不拢再进举证仲裁。直付非托管,WebAZ 不代收/不退款。')}</div></div>`
@@ -0,0 +1,15 @@
1
+ // Direct Pay (Rail 1) — 付款时刻 UI 辅助。UI ONLY;不碰钱路。dpPayAmountText(order):应付金额纯文本,
2
+ // 用于 D2 弹窗/风险确认弹窗/订单收款说明框。审计项 E:优先【下单时冻结的应付参考换算】(snapshot payable_* —
3
+ // 买家/卖家/时间线同一稳定数字,不随实时汇率漂移;标注"下单时参考,以卖家收款说明为准");旧单无快照 → 回落
4
+ // 实时换算(dpFxInCurrency);再无 → 仅 USDC。USDC/USD 账户:另附买家本地法币参考(_fxLocal)。中英 i18n parity。
5
+ window.dpPayAmountText = (order) => {
6
+ if (!order) return ''
7
+ const usdc = Number(order.total_amount)
8
+ const s = Number.isFinite(usdc) ? (Number.isInteger(usdc) ? String(usdc) : usdc.toFixed(2)) : '—'
9
+ let snap = {}; try { snap = JSON.parse(order.direct_pay_account_snapshot || '{}') || {} } catch {}
10
+ const cur = String(snap.currency || '').toUpperCase()
11
+ if (!cur || cur === 'USDC' || cur === 'USD') { const loc = window._fxLocal ? window._fxLocal(usdc) : ''; return `${t('应付')} ${s} USDC${loc ? ' ≈ ' + loc : ''}` }
12
+ if (Number.isFinite(Number(snap.payable_approx))) return `${t('应付')} ≈ ${cur} ${Number(snap.payable_approx).toFixed(2)}(${s} USDC · ${t('下单时参考价,以卖家收款说明为准')})`
13
+ const fx = window.dpFxInCurrency ? window.dpFxInCurrency(usdc, cur) : ''
14
+ return (fx && fx !== cur) ? `${t('应付')} ≈ ${fx}(${s} USDC)` : `${t('应付')} ${s} USDC · ${cur}`
15
+ }
@@ -0,0 +1,32 @@
1
+ // Direct Pay (Rail 1) — 融合付款弹窗。UI ONLY;不碰钱路(mark_paid 仍走 dpHandleAction 的 Passkey 门)。
2
+ // 两次风险披露完成后弹出:把【收款账号/说明 · 应付金额 · 付款附言(参考号)】三要素融合展示,每项一键复制
3
+ // (买家直接抄进银行/收款App),付款后就地点"我已付款"→ dpHandleAction('mark_paid')(不新增 Passkey 次数,
4
+ // 省掉"回订单页找按钮"一步)。也可"稍后处理"→ 订单页(同样能标记付款)。中文 t(),英文 i18n.js _EN。
5
+ // 复制用【纯数字+币种】(避免抄进 "应付 ≈ …" 整句):有下单冻结 payable 用其币种+值,否则 USDC 总额。
6
+ window.dpPayModalCopyAmount = (order) => {
7
+ try { const s = JSON.parse(order.direct_pay_account_snapshot || '{}'); if (Number.isFinite(Number(s.payable_approx)) && s.payable_currency) return s.payable_currency + ' ' + Number(s.payable_approx).toFixed(2) } catch {} return String(order.total_amount) + ' USDC'
8
+ }
9
+ window.dpShowPaymentModal = (order) => {
10
+ if (!order || !order.id) return
11
+ const oid = order.id
12
+ const instr = order.direct_pay_instruction_snapshot || ''
13
+ const amtText = window.dpPayAmountText ? window.dpPayAmountText(order) : (String(order.total_amount) + ' USDC') // 展示串:复用 #225 唯一真相(含 ≈/下单参考价 caveat/FX 回落),防三处漂移
14
+ const amtCopy = window.dpPayModalCopyAmount(order)
15
+ const ref = window.dpPayRef ? window.dpPayRef(oid) : ''
16
+ const row = (label, valueHtml, copyText) => `<div style="margin-bottom:10px">
17
+ <div style="font-size:11px;color:#6b7280;margin-bottom:3px">${label}${window.dpCopyBtn ? window.dpCopyBtn(copyText) : ''}</div>
18
+ <div style="font-size:13px;color:#111827;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;padding:8px 10px;white-space:pre-wrap;word-break:break-word">${valueHtml}</div></div>`
19
+ window._openModal(`
20
+ <h2 style="font-size:16px;font-weight:700;margin:0 0 4px">💸 ${t('请按以下信息付款')}</h2>
21
+ <div style="font-size:12px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:8px 10px;margin-bottom:12px">${t('风险确认已完成。这是卖家收款信息(下单时快照,WebAZ 不验证也不经手)。请【场外】完成付款后回来标记。')}</div>
22
+ ${instr ? row(t('收款账号 / 说明'), escHtml(instr), instr) : ''}
23
+ ${row(t('应付金额'), escHtml(amtText), amtCopy)}
24
+ ${row(t('付款附言(务必填写,用于卖家核对)'), '<b>' + escHtml(ref) + '</b>', ref)}
25
+ <div style="font-size:11px;color:#9ca3af;margin:6px 0 12px">${t('付款时请在银行/收款App的附言/备注里填入上面的参考号;完成付款后点下方按钮(需 Passkey)。')}</div>
26
+ <div style="display:flex;gap:8px">
27
+ <button class="btn btn-outline" style="flex:1" onclick="dpPayModalLater('${escHtml(oid)}')">${t('稍后在订单页处理')}</button>
28
+ <button class="btn btn-primary" style="flex:1;background:#16a34a;border-color:#16a34a" onclick="dpPayModalMarkPaid('${escHtml(oid)}')">${t('我已付款')}</button>
29
+ </div>`)
30
+ }
31
+ window.dpPayModalLater = (oid) => { if (typeof closeModal === 'function') closeModal(); navigate('#order/' + oid) }
32
+ window.dpPayModalMarkPaid = async (oid) => { if (typeof closeModal === 'function') closeModal(); await window.dpHandleAction(oid, 'mark_paid') } // mark_paid 自带 Passkey 门 + 成功后跳订单页
@@ -0,0 +1,19 @@
1
+ // 直付卖家对账卡(审计项 F)。UI ONLY;不碰钱路。买家 mark_paid 后(accepted,发货前)在卖家视角显性化
2
+ // 对账三要素:①期望参考号(dpPayRef 派生,银行流水附言应恰为此值,一键复制便于流水搜索)②应付金额
3
+ // (E 冻结的 payable 快照,与买家所见同一数字)③同买家同金额在途多单告警(DTO duplicate_amount_alert,
4
+ // 与 mark_paid D2 时间线预警同口径 —— 防一笔转账冒充两单)。买家声称的参考在时间线(mark_paid note)可见。
5
+ // 发货即确认收款(D4 弹窗把关),此卡把核对动作前置到打开订单页的每一刻。中文 t(),英文 i18n.js _EN。
6
+ window.dpReconcileCard = (order, isSeller) => {
7
+ if (!order || !isSeller || order.payment_rail !== 'direct_p2p' || order.status !== 'accepted') return ''
8
+ const ref = window.dpPayRef ? window.dpPayRef(order.id) : ''
9
+ const pay = window.dpPayAmountText ? window.dpPayAmountText(order) : ''
10
+ const dup = Number(order.duplicate_amount_alert) || 0
11
+ return `<div class="card" style="border:1px solid #bbf7d0;background:linear-gradient(135deg,#f0fdf4,#dcfce7)">
12
+ <div style="font-size:13px;font-weight:700;color:#166534;margin-bottom:4px">🧾 ${t('发货前对账(买家已标记付款)')}</div>
13
+ <div style="font-size:12px;color:#374151;line-height:1.9">
14
+ <div>${t('银行/收款App流水附言应为')} <code style="font-size:13px;font-weight:700;background:#fff;border:1px solid #bbf7d0;border-radius:6px;padding:2px 6px">${escHtml(ref)}</code>${window.dpCopyBtn ? window.dpCopyBtn(ref) : ''}</div>
15
+ ${pay ? `<div>💸 ${escHtml(pay)}</div>` : ''}
16
+ ${dup > 0 ? `<div style="color:#b91c1c;font-weight:600">⚠️ ${t('同买家另有')} ${dup} ${t('笔同金额直付订单在途 —— 每笔转账只能核销一个订单,请逐单核对参考号,谨防一笔款冒充多单。')}</div>` : ''}
17
+ <div style="font-size:11px;color:#6b7280">${t('请核实款项【已到账】且附言/金额与本单一致再发货;发货即视为确认收款。未收到请点"未收到货款"。')}</div>
18
+ </div></div>`
19
+ }
@@ -0,0 +1,56 @@
1
+ // 直付(direct_p2p)送达后退货·场外退款握手前端 —— 退货骨架 UI 在 app.js renderReturnWidgetForOrder,
2
+ // 本模块只提供直付分支的 hooks(状态标签/握手动作块/升级判定与文案)。UI ONLY,真正边界在后端
3
+ // routes/direct-pay-returns.ts + 域模块。中文 t(),英文 i18n.js _EN。库存绝不自动恢复(退货验收上架)。
4
+
5
+ window.dpReturnStatusLabels = () => ({ await_refund: t('待卖家场外退款'), refund_marked: t('卖家已声明退款 · 待你确认') })
6
+ window.dpReturnStatusColors = () => ({ await_refund: '#d97706', refund_marked: '#2563eb', accepted_pickup_pending: '#16a34a', picked_up: '#2563eb' })
7
+
8
+ // 握手动作块:卖家在 await_refund 声明退款;买家在 refund_marked 确认(Passkey)。其余状态返回 ''。
9
+ window.dpReturnHandshake = (item, isBuyer, isSellerView, order) => {
10
+ if (!order || order.payment_rail !== 'direct_p2p') return ''
11
+ const note = `<div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px;margin-bottom:8px">${t('直付非托管:退款由卖家在协议外完成,平台只记录握手,不经手货款。')}</div>`
12
+ if (isSellerView && item.status === 'await_refund') {
13
+ return `${note}<div style="font-size:12px;color:#374151;margin-bottom:6px">${t('你已同意退货。请在协议外向买家退款,然后声明;买家确认后退货完成。')}</div>
14
+ <input id="dp-ret-ref-${item.id}" class="form-control" maxlength="200" placeholder="${t('退款参考(转账单号等,可选,买家可见)')}" style="margin-bottom:8px;font-size:12px">
15
+ <button class="btn btn-primary btn-sm" style="width:auto;font-size:12px" onclick="dpReturnMarkRefunded('${item.id}','${order.id}')">${t('我已退款')}</button>`
16
+ }
17
+ if (isBuyer && item.status === 'await_refund') {
18
+ return `${note}<div style="font-size:12px;color:#6b7280;margin-bottom:8px">⏳ ${t('卖家已同意退货,等待其在协议外向你退款并声明。超期未退款可升级争议。')}</div>`
19
+ }
20
+ if (isBuyer && item.status === 'refund_marked') {
21
+ return `${note}<div style="font-size:12px;color:#374151;margin-bottom:8px">💸 ${t('卖家已声明退款')}${item.refund_reference ? '「' + escHtml(item.refund_reference) + '」' : ''}。${t('请先核实退款已到账;未收到请勿确认,可发起争议。')}</div>
22
+ <button class="btn btn-primary btn-sm" style="width:auto;font-size:12px" onclick="dpReturnConfirmRefund('${item.id}','${order.id}')">${t('已收到退款,完成退货(需 Passkey)')}</button>`
23
+ }
24
+ if (isSellerView && item.status === 'refund_marked') {
25
+ return `${note}<div style="font-size:12px;color:#6b7280">⏳ ${t('你已声明退款,等待买家确认收到。提示:退回货物须验收后手动上架,库存不会自动恢复。')}</div>`
26
+ }
27
+ return ''
28
+ }
29
+
30
+ // 升级争议判定(与后端 directPayReturnEscalatable 同口径;窗口天数由后端最终裁决,前端只做展示预判)
31
+ window.dpReturnCanEscalate = (item, order) => {
32
+ if (!order || order.payment_rail !== 'direct_p2p') return false
33
+ if (item.status === 'refund_marked') return true
34
+ if (item.status === 'await_refund' && item.await_refund_since) return (Date.now() - new Date(String(item.await_refund_since).replace(' ', 'T') + 'Z').getTime()) >= 5 * 86400 * 1000
35
+ return false
36
+ }
37
+ window.dpReturnEscalateHint = (item) => item.status === 'refund_marked'
38
+ ? t('若卖家声明退款但你并未收到,可升级至平台仲裁(直付为信誉裁决)')
39
+ : t('卖家同意退货后超期未退款 — 可升级至平台仲裁(直付为信誉裁决)')
40
+
41
+ window.dpReturnMarkRefunded = async (rid, oid) => {
42
+ if (typeof confirmModal === 'function' && !(await confirmModal(t('确认你已在协议外向买家完成退货退款?声明后买家确认即完成;虚假声明将留痕并可被追责。'), t('我已退款'), { danger: true }))) return
43
+ const el = document.getElementById('dp-ret-ref-' + rid)
44
+ const r = await POST('/return-requests/' + rid + '/mark-refunded', { refund_reference: (el && el.value || '').trim() })
45
+ if (r.error) return void toast$(r.error, 'error')
46
+ toast$(t('已声明退款,等待买家确认'), 'success'); renderOrderDetail(document.getElementById('app'), oid)
47
+ }
48
+ window.dpReturnConfirmRefund = async (rid, oid) => {
49
+ if (typeof confirmModal === 'function' && !(await confirmModal(t('确认已收到卖家退款?确认后退货完成,不可撤销。若尚未到账请勿确认。'), t('已收到,完成退货'), { danger: true }))) return
50
+ let token
51
+ try { token = await requestPasskeyGate('direct_pay_order_action', { order_id: oid, action: 'return_refund_confirm' }) }
52
+ catch (e) { if (window.dpPromptRegisterPasskey) await window.dpPromptRegisterPasskey(e); return }
53
+ const r = await POST('/return-requests/' + rid + '/confirm-refund', { webauthn_token: token })
54
+ if (r.error) return void toast$(window.dpErrorText ? window.dpErrorText(r.error_code, r.error) : r.error, 'error')
55
+ toast$(t('退货已完成'), 'success'); renderOrderDetail(document.getElementById('app'), oid)
56
+ }
@@ -0,0 +1,82 @@
1
+ // Direct Pay (Rail 1) — 收款信息可见性生命周期 (PR-2)。UI ONLY。
2
+ // 模型:待支付(direct_pay_window)→ 自动展示卖家收款说明+QR 一个【30 分钟窗口】,超时自动隐藏,可【轻量】重新显示(重置窗口);
3
+ // 其它所有状态(已付/完成/取消/过期/争议/退款…)→ 默认隐藏,需【二次验证:现场 Passkey + 风险提示】才临时展示(同样 30 分钟窗口)。
4
+ // 金额(应付)非敏感,任何状态都常显。
5
+ // 诚实边界:这是【客户端展示/同意】控制 —— 防误付空单 + 不长留敏感收款信息在屏 + 现场在场二次同意;
6
+ // 【并非】新的服务器机密边界(收款目标对 both-acked 的订单买家经订单 API 仍可得,后端授权门仍是 both-acked 归属)。
7
+ // 订单隔离(P1):所有 DOM 写入只经 dpInstrBox(orderId) 取容器 —— 容器 data-order-id 必须仍等于该 orderId,
8
+ // 否则(已切到别的订单页)一律 bail。切换订单渲染时先清掉所有旧 timer,杜绝旧订单 timer/重看改写当前订单面板。
9
+ window._dpRevealTimers = window._dpRevealTimers || {}
10
+ window.DP_REVEAL_MS = 30 * 60 * 1000
11
+
12
+ // 终态/无效单(不应再付款)——重看时给强风险提示。
13
+ window.dpIsVoidOrder = (status) => ['cancelled', 'expired', 'direct_expired_unconfirmed', 'disputed', 'dispute_dismissed', 'refunded_full', 'refunded_partial', 'fault_seller', 'fault_buyer', 'fault_logistics'].includes(status)
14
+
15
+ // 订单隔离守卫:仅当当前 #dp-order-instr 容器仍属于 orderId 才返回它;否则 null(已切换订单 → 任何写入都不得落到别的订单页)。
16
+ window.dpInstrBox = (orderId) => { const b = document.getElementById('dp-order-instr'); return (b && b.getAttribute('data-order-id') === String(orderId)) ? b : null }
17
+ window.dpClearRevealTimer = (orderId) => { const tmr = window._dpRevealTimers[orderId]; if (tmr) { clearTimeout(tmr.to); clearInterval(tmr.iv); delete window._dpRevealTimers[orderId] } }
18
+ window.dpClearAllRevealTimers = () => { for (const k of Object.keys(window._dpRevealTimers)) window.dpClearRevealTimer(k) }
19
+
20
+ // 收款说明+QR 展示块(含倒计时);到点 → 隐藏。lightweight=true:重新显示无需 Passkey(待支付轨)。
21
+ window.dpShowPaymentInfo = (order, orderId, lightweight) => {
22
+ const box = window.dpInstrBox(orderId); if (!box) return // 已切到别的订单页 → 不写
23
+ window.dpClearRevealTimer(orderId)
24
+ const snap = order && order.direct_pay_instruction_snapshot ? order.direct_pay_instruction_snapshot : ''
25
+ const pay = window.dpPayAmountText ? window.dpPayAmountText(order) : ''
26
+ if (!snap) { box.innerHTML = `<div style="font-size:12px;color:#9ca3af">${t('卖家尚未设置收款说明,暂不可直付')}</div>`; return }
27
+ box.innerHTML = `<div style="font-size:12px;color:#374151;background:#fff;border:1px solid #fde68a;border-radius:8px;padding:8px 10px">
28
+ ${pay ? `<div style="font-size:13px;font-weight:700;color:#92400e;margin-bottom:4px">💸 ${escHtml(pay)}</div>` : ''}
29
+ <div style="font-size:11px;color:#9ca3af;margin-bottom:2px">${t('卖家收款说明(下单时快照)')}${window.dpCopyBtn ? window.dpCopyBtn(snap, t('复制收款说明')) : ''}</div><div style="white-space:pre-wrap;word-break:break-word">${escHtml(snap)}</div><div id="dp-order-qr"></div>${lightweight && window.dpMemoInputHtml ? window.dpMemoInputHtml(orderId) : ''}
30
+ <div style="font-size:10px;color:#9ca3af;margin-top:6px">${t('自动隐藏倒计时')}: <span id="dp-reveal-countdown">30:00</span></div></div>`
31
+ if (window.dpLoadOrderQr) window.dpLoadOrderQr(orderId)
32
+ const end = Date.now() + window.DP_REVEAL_MS
33
+ const tick = () => { const b = window.dpInstrBox(orderId); if (!b) return window.dpClearRevealTimer(orderId); const el = b.querySelector('#dp-reveal-countdown'); if (!el) return; const s = Math.max(0, Math.round((end - Date.now()) / 1000)); el.textContent = `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}` }
34
+ tick()
35
+ window._dpRevealTimers[orderId] = { iv: setInterval(tick, 1000), to: setTimeout(() => window.dpHidePaymentInfo(order, orderId, lightweight), window.DP_REVEAL_MS) }
36
+ }
37
+
38
+ // 隐藏收款信息 → 显示重看入口。lightweight:待支付轨点一下即可;否则需 Passkey+风险提示。
39
+ window.dpHidePaymentInfo = (order, orderId, lightweight) => {
40
+ window.dpClearRevealTimer(orderId)
41
+ const box = window.dpInstrBox(orderId); if (!box) return // 已切到别的订单页 → 不写
42
+ const pay = window.dpPayAmountText ? window.dpPayAmountText(order) : ''
43
+ const hint = lightweight ? t('为保护你的收款信息,已隐藏。') : t('订单已不在待支付阶段,收款信息默认隐藏。')
44
+ const btn = lightweight
45
+ ? `<button class="btn btn-outline btn-sm" onclick="dpReShowPaymentInfo('${orderId}')">${t('重新显示')}</button>`
46
+ : `<button class="btn btn-outline btn-sm" onclick="dpGatedRevealPaymentInfo('${orderId}')">${t('查看收款信息(需 Passkey 验证)')}</button>`
47
+ box.innerHTML = `<div style="font-size:12px;color:#374151;background:#fff;border:1px solid #fde68a;border-radius:8px;padding:8px 10px">
48
+ ${pay ? `<div style="font-size:13px;font-weight:700;color:#92400e;margin-bottom:4px">💸 ${escHtml(pay)}</div>` : ''}
49
+ <div style="font-size:11px;color:#9ca3af;margin-bottom:6px">${escHtml(hint)}</div>${btn}</div>`
50
+ }
51
+
52
+ // 轻量重新显示:GET【最新】订单后按【当前状态】重新分派 —— 仍待支付才轻量重显;若期间已取消/过期/争议等,
53
+ // dpRenderPaymentInfo 会走隐藏+Passkey 二次验证路径(绝不用旧按钮 lightweight 展示一个已失效订单的收款信息)。
54
+ window.dpReShowPaymentInfo = async (orderId) => {
55
+ if (!window.dpInstrBox(orderId)) return
56
+ const o = await GET(`/orders/${orderId}`); const ord = o && o.order ? o.order : null
57
+ const box = window.dpInstrBox(orderId); if (ord && box) window.dpRenderPaymentInfo(box, ord, orderId)
58
+ }
59
+
60
+ // 非待支付:二次验证(风险提示 + 现场 Passkey)后临时展示一个 30 分钟窗口。
61
+ window.dpGatedRevealPaymentInfo = async (orderId) => {
62
+ if (!window.dpInstrBox(orderId)) return
63
+ const o = await GET(`/orders/${orderId}`); const ord = o && o.order ? o.order : null
64
+ if (!ord) return
65
+ const warn = window.dpIsVoidOrder(ord.status)
66
+ ? t('⚠️ 此订单已取消/终止,请【勿再付款】。收款信息仅供对账/存证查看。')
67
+ : t('⚠️ 你已完成付款。收款信息仅供对账/维权查看,请勿重复付款。')
68
+ const go = await confirmModal(`${warn}\n\n${t('继续查看需现场 Passkey 验证。')}`, t('继续(需 Passkey)'), { danger: true })
69
+ if (!go || !window.dpInstrBox(orderId)) return
70
+ try { await requestPasskeyGate('direct_pay_payment_info_reveal', { order_id: orderId }) }
71
+ catch (e) { if (window.dpPromptRegisterPasskey) await window.dpPromptRegisterPasskey(e); return }
72
+ window.dpShowPaymentInfo(ord, orderId, false)
73
+ }
74
+
75
+ // 订单详情:按状态渲染收款信息可见性。【先确认当前容器仍属于该 orderId,再清 timer】——否则旧订单 stale hydrate 回包
76
+ // 会把当前订单的自动隐藏 timer 一并清掉(敏感收款信息永不隐藏)。both-acked 由调用方(dpHydrateOrderDisclosure)先保证。
77
+ window.dpRenderPaymentInfo = (_box, order, orderId) => {
78
+ if (!order || !window.dpInstrBox(orderId)) return // 当前页已不是该订单 → 不动任何 timer/DOM
79
+ window.dpClearAllRevealTimers()
80
+ if (order.payment_rail === 'direct_p2p' && order.status === 'direct_pay_window') window.dpShowPaymentInfo(order, orderId, true)
81
+ else window.dpHidePaymentInfo(order, orderId, false)
82
+ }
@@ -0,0 +1,70 @@
1
+ // Direct Pay 卖家【直接收款销售统计 + 对账 + 逐单平台费明细】section。UI ONLY,只读。
2
+ // 数据:GET /api/sellers/me/direct-pay-report(requireSeller,仅本人)。直付=非托管,货款买家直付卖家,平台不经手 →
3
+ // 钱包/收入视图看不到直付销售,故此处按订单聚合销售额 + 逐单平台服务费,便于对账(可选日期区间 + 客户端 CSV 导出)。
4
+ // 销售额=下单计价币(买家应付金额);平台服务费=USDC。两者不同币种,分列不混加。
5
+ ;(function () {
6
+ const _n = (v) => (Number(v) || 0)
7
+ const _amt = (v) => _n(v).toLocaleString(undefined, { maximumFractionDigits: 2 }) // 销售额(计价币)
8
+ const _fee = (v) => v == null ? '—' : (_n(v).toFixed(6).replace(/\.?0+$/, '') || '0') + ' USDC' // 平台费(USDC)
9
+ let _last = null
10
+
11
+ window.dpSalesReportSection = () => {
12
+ setTimeout(() => window.dpHydrateSalesReport(), 60)
13
+ return `<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">
14
+ <div style="font-size:14px;font-weight:700;margin-bottom:4px">🧾 ${t('直接收款销售统计 + 对账(仅你可见)')}</div>
15
+ <div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('直付为非托管:货款由买家直接付给你,平台不经手。此处按你的订单聚合销售额与逐单平台服务费,便于对账。')}</div>
16
+ <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:10px">
17
+ <input type="date" id="dpsr-from" style="font-size:12px;padding:4px 6px;border:1px solid #e5e7eb;border-radius:6px">
18
+ <span style="color:#9ca3af">→</span>
19
+ <input type="date" id="dpsr-to" style="font-size:12px;padding:4px 6px;border:1px solid #e5e7eb;border-radius:6px">
20
+ <button class="btn btn-outline btn-sm" style="font-size:11px;padding:4px 10px" onclick="dpHydrateSalesReport(document.getElementById('dpsr-from').value, document.getElementById('dpsr-to').value)">${t('查询')}</button>
21
+ <button class="btn btn-outline btn-sm" style="font-size:11px;padding:4px 10px" onclick="dpSalesReportExport()">📥 ${t('导出对账 CSV')}</button>
22
+ </div>
23
+ <div id="dpsr-body">${typeof loading$ === 'function' ? loading$() : ''}</div>
24
+ </div>`
25
+ }
26
+
27
+ window.dpHydrateSalesReport = async (from, to) => {
28
+ const box = document.getElementById('dpsr-body'); if (!box) return
29
+ const q = []
30
+ if (from && /^\d{4}-\d{2}-\d{2}$/.test(from)) q.push('from=' + from)
31
+ if (to && /^\d{4}-\d{2}-\d{2}$/.test(to)) q.push('to=' + to)
32
+ const r = await GET('/sellers/me/direct-pay-report' + (q.length ? '?' + q.join('&') : '')).catch(() => null)
33
+ if (!r || r.error) { box.innerHTML = `<div style="font-size:12px;color:#dc2626">${t('操作失败,请重试')}</div>`; return }
34
+ _last = r
35
+ const s = r.summary
36
+ if (!s || s.order_count === 0) { box.innerHTML = `<div style="font-size:12px;color:#9ca3af;padding:8px 0">${t('该区间暂无直接收款订单')}</div>`; return }
37
+ const tile = (label, val, sub) => `<div style="flex:1;min-width:96px;background:#faf5ff;border:1px solid #ede9fe;border-radius:8px;padding:8px 10px"><div style="font-size:11px;color:#7c3aed">${label}</div><div style="font-size:16px;font-weight:700;color:#5b21b6">${val}</div>${sub ? `<div style="font-size:10px;color:#9ca3af">${sub}</div>` : ''}</div>`
38
+ const tiles = `<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px">
39
+ ${tile(t('已完成销售额'), _amt(s.completed_sales), t('已完成') + ' ' + s.completed_count + ' ' + t('单'))}
40
+ ${tile(t('订单总额'), _amt(s.gross_order_total), t('含在途/已关闭') + ' · ' + s.order_count + ' ' + t('单'))}
41
+ ${tile(t('已计提平台费'), _fee(s.fee_accrued_total), s.fee_order_count + ' ' + t('单'))}
42
+ ${tile(t('在途 / 已关闭'), s.in_flight_count + ' / ' + s.closed_count, t('单'))}
43
+ </div>`
44
+ const byMonth = (r.by_month && r.by_month.length > 1) ? `<div style="margin-bottom:10px"><div style="font-size:12px;color:#6b7280;margin-bottom:4px">${t('按月(已完成 / 订单总额)')}</div>${r.by_month.map(m => `<div style="display:flex;justify-content:space-between;font-size:12px;padding:2px 0;border-bottom:1px solid #f3f4f6"><span>${escHtml(m.month)}</span><span>${_amt(m.completed_sales)} <span style="color:#9ca3af">/ ${_amt(m.gross_order_total)}</span></span></div>`).join('')}</div>` : ''
45
+ const th = (txt, right) => `<th style="text-align:${right ? 'right' : 'left'};padding:5px 4px;font-size:11px;color:#9ca3af;white-space:nowrap">${txt}</th>`
46
+ const rowsHtml = r.orders.map(o => `<tr style="border-bottom:1px solid #f3f4f6">
47
+ <td style="padding:5px 4px;font-size:11px"><a href="#order/${o.id}" style="color:#6366f1;text-decoration:none">${escHtml(String(o.id).slice(0, 10))}</a></td>
48
+ <td style="padding:5px 4px;font-size:11px;color:#6b7280;white-space:nowrap">${typeof fmtTime === 'function' ? fmtTime(o.created_at) : escHtml(o.created_at)}</td>
49
+ <td style="padding:5px 4px;font-size:11px">${typeof statusBadge === 'function' ? statusBadge(o.status, 'direct_p2p') : escHtml(o.status)}</td>
50
+ <td style="padding:5px 4px;font-size:11px;text-align:right;font-weight:600">${_amt(o.total_amount)}</td>
51
+ <td style="padding:5px 4px;font-size:11px;text-align:right;color:#7c3aed">${_fee(o.fee_amount)}</td>
52
+ </tr>`).join('')
53
+ const table = `<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse">
54
+ <thead><tr style="border-bottom:1px solid #e5e7eb">${th(t('订单'))}${th(t('时间'))}${th(t('状态'))}${th(t('销售额'), true)}${th(t('平台费'), true)}</tr></thead>
55
+ <tbody>${rowsHtml}</tbody></table></div>${r.truncated ? `<div style="font-size:11px;color:#b45309;margin-top:6px">${t('仅显示最近 500 单,请用日期区间缩小范围')}</div>` : ''}`
56
+ box.innerHTML = tiles + byMonth + table
57
+ }
58
+
59
+ // 客户端 CSV 导出(对账用:逐单销售额 + 平台费一张表,对银行流水)。
60
+ window.dpSalesReportExport = () => {
61
+ if (!_last || !_last.orders || !_last.orders.length) { (window.toast$ || window.alert)(t('暂无数据可导出')); return }
62
+ const esc = (v) => { const x = v == null ? '' : String(v); return /[",\n]/.test(x) ? '"' + x.replace(/"/g, '""') + '"' : x }
63
+ const lines = [['order_id', 'created_at', 'status', 'sales_amount', 'platform_fee_usdc', 'fee_accrued_at', 'ship_to_region', 'product'].join(',')]
64
+ for (const o of _last.orders) lines.push([o.id, o.created_at, o.status, o.total_amount, o.fee_amount == null ? '' : o.fee_amount, o.fee_accrued_at || '', o.ship_to_region || '', o.product_title || ''].map(esc).join(','))
65
+ const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob(['' + lines.join('\n')], { type: 'text/csv;charset=utf-8' }))
66
+ a.download = 'webaz-directpay-reconcile-' + new Date().toISOString().slice(0, 10) + '.csv'
67
+ document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href)
68
+ if (window.toast$) window.toast$(t('导出完成'))
69
+ }
70
+ })()
@@ -23,7 +23,7 @@ window.dpErrorText = (code, fallback) => {
23
23
  DIRECT_PAY_SIMPLE_PRODUCT_ONLY: t('直付当前仅支持简单商品(无规格)'),
24
24
  DIRECT_PAY_UNSUPPORTED_OPTION: t('直付当前不支持该下单选项'),
25
25
  }
26
- return M[code] || fallback || t('操作失败,请重试')
26
+ return M[code] || (window.orderErrorLookup && window.orderErrorLookup(code)) || fallback || t('操作失败,请重试') // 回退查订单错误码表(NOT_ORDER_BUYER 等披露/动作码)
27
27
  }
28
28
 
29
29
  // ── 买家结算:支付方式(rail)选择。escrow 默认;direct_p2p 可选,选中先查可用性(见 dpOnRailChange);D1/D2 ack 在建单后用 Passkey 完成。
@@ -40,7 +40,7 @@ window.dpRailSelectorHtml = (productId, priceUsdc) => `
40
40
  <span><b>${t('直付(Direct Pay · 非托管)')}</b><br><span style="font-size:11px;color:#6b7280">${t('你直接付款给卖家(场外),本金不经 WebAZ')}</span></span>
41
41
  </label>
42
42
  <div id="dp-rail-note" style="display:none;margin-top:8px;font-size:11px;line-height:1.6;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:8px 10px">
43
- ⚠️ ${t('直付风险提醒:WebAZ 不托管本金、不担保、不退款、不代维权,也不验证卖家的付款方式或币种。下单后需用 Passkey 完成两次风险确认,再标记付款。')}
43
+ ⚠️ ${t('直付风险提醒:非担保交易 —— WebAZ 不托管本金、无退款能力、不代维权,也不验证卖家的付款方式或币种,仅对卖家有信誉处罚权。下单后需用 Passkey 完成两次风险确认,再标记付款。')}
44
44
  <div style="margin-top:6px">${t('需要 Passkey。')}<a href="#me" style="color:#854d0e;font-weight:600;text-decoration:underline">${t('前往「我的 → 安全与存储」注册 →')}</a></div>
45
45
  </div>
46
46
  <div id="dp-rail-unavailable" style="display:none;margin-top:8px;font-size:11px;line-height:1.6;color:#991b1b;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:8px 10px"></div><div id="dp-account-picker"></div>
@@ -81,64 +81,66 @@ window.dpAfterCreate = async (res) => {
81
81
  return
82
82
  }
83
83
  // 两次披露已 ack —— 收款说明【不】来自 create 响应(后端在 both-acked 前不下发);现在才 GET 订单读取 redaction-gated 快照。
84
+ // 【先落订单页,融合付款弹窗叠在其上】:订单页本身有收款信息+对账卡+自动隐藏倒计时,所以无论用户点按钮、点遮罩还是
85
+ // 按 ESC 关掉弹窗,都留在订单页而非产品页 —— 杜绝"关弹窗后失联→重复下单"(审计:弹窗被 overlay/ESC 关会 strand)。
84
86
  const o = await GET(`/orders/${orderId}`)
85
- const instr = o && o.order ? (o.order.direct_pay_instruction_snapshot || '') : ''
86
- await confirmModal(
87
- `${t('风险确认完成 · 这是卖家的收款说明(下单时快照,WebAZ 不验证)')}\n\n${instr}\n\n${t('请按此付款,然后在订单页标记"我已付款"')}`,
88
- t('我知道了'), {})
89
- setTimeout(() => navigate(`#order/${orderId}`), 300)
87
+ navigate(`#order/${orderId}`)
88
+ if (o && o.order && window.dpShowPaymentModal) setTimeout(() => window.dpShowPaymentModal(o.order), 120) // 等订单页渲染后叠弹窗
90
89
  }
91
- // ── 确保两次披露都已 ack(缺哪个补哪个;每次都需现场真人 Passkey)。返回 both-acked 与否。 ──────────
90
+ // ── 确保两次披露都已 ack。两屏文本各自展示确认(证据不减);缺两个 → 一次 Passkey ceremony 覆盖(stage:'both',首单
91
+ // 3→2,2026-07-04 决策),缺一个 → 单独 ceremony。会话缓存 _dpAcked:both 过的单热路径(mark_paid/confirm)不再重复 2 GET;
92
+ // 后端 requireBothDisclosuresAcked 仍硬门。懒取金额:仅 D2 将展示才 GET 订单。──
92
93
  window.dpEnsureAcks = async (orderId) => {
94
+ if (window._dpAcked && window._dpAcked[orderId]) return true
93
95
  const st = await GET(`/direct-pay/disclosure-acks/${orderId}`)
94
96
  if (st.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText(st.error_code, st.error), 'error'); return false }
97
+ const cache = () => { (window._dpAcked = window._dpAcked || {})[orderId] = true; return true }
98
+ if (st.both === true) return cache()
95
99
  const disc = st.disclosures || {}
96
- const stages = [
97
- { key: 'pre_select', acked: st.acked?.pre_select, text: disc.pre_select },
98
- { key: 'pre_confirm', acked: st.acked?.pre_confirm, text: disc.pre_confirm },
99
- ]
100
- for (const s of stages) {
101
- if (s.acked) continue
102
- const body = s.text ? (window._lang === 'en' ? s.text.en : s.text.zh) : t('风险披露')
103
- const go = await confirmModal(`${body}\n\n${t('我已阅读并理解上述风险')}`, t('确认(需 Passkey)'), { danger: true })
100
+ const missing = ['pre_select', 'pre_confirm'].filter(k => !(st.acked && st.acked[k]))
101
+ let _pay = ''
102
+ if (missing.includes('pre_confirm')) { try { const _od = await GET(`/orders/${orderId}`); if (_od && _od.order) _pay = window.dpPayAmountText(_od.order) } catch {} }
103
+ for (let i = 0; i < missing.length; i++) {
104
+ const key = missing[i], tx = key === 'pre_select' ? disc.pre_select : disc.pre_confirm
105
+ const body = (tx ? (window._lang === 'en' ? tx.en : tx.zh) : t('风险披露')) + (key === 'pre_confirm' && _pay ? `\n\n💸 ${_pay}` : '')
106
+ const go = await confirmModal(`${body}\n\n${t('我已阅读并理解上述风险')}`, i === missing.length - 1 ? t('了解直接付款(需 Passkey)') : t('下一步'), { danger: true })
104
107
  if (!go) return false
105
- const ok = await window.dpDoAck(orderId, s.key)
106
- if (!ok) return false
107
108
  }
108
- return true
109
+ const ok = await window.dpDoAck(orderId, missing.length === 2 ? 'both' : missing[0])
110
+ return ok ? cache() : false
109
111
  }
110
- // Passkey 门失败( Passkey / 设备不支持 / 用户取消)→ 给出明确的【注册 Passkey 入口】(前往「我的 → 安全与存储」)。
111
- window.dpPromptRegisterPasskey = async (reason) => {
112
- const go = await confirmModal(
113
- `${reason ? reason + '\n\n' : ''}${t('直付的风险确认与付款标记需要 Passkey。前往「我的 → 安全与存储」注册一个?')}`,
114
- t('前往注册 Passkey'), {})
115
- if (go) navigate('#me')
112
+ // Passkey 门失败:仅【确实未注册】(后端 NO_PASSKEY_REGISTERED,由 requestPasskeyGate 透传到 err.code)才引导注册;
113
+ // 已注册但取消/设备不支持/超时(含 navigator DOMException,其 .code legacy 数字非本值) 仅本地化"请重试",
114
+ // 不导注册(修用户反馈:已注册者被重复提醒)、不把英文 DOMException 文案 toast 给中文用户(bilingual)。全部 5 调用点传 err 对象。
115
+ window.dpPromptRegisterPasskey = async (err) => {
116
+ if (!(err && typeof err === 'object' && err.code === 'NO_PASSKEY_REGISTERED')) { if (typeof toast$ === 'function') toast$(t('验证未完成,请重试'), 'info'); return }
117
+ if (await confirmModal(t('直付的风险确认与付款标记需要 Passkey。你还没有注册 Passkey,前往「我的 → 安全与存储」注册一个?'), t('前往注册 Passkey'), {})) navigate('#me')
116
118
  }
117
119
  // 单次披露 ack:Passkey ceremony(purpose=direct_pay_disclosure_ack,order+stage 绑 purpose_data)→ POST ack。
118
120
  window.dpDoAck = async (orderId, stage) => {
119
121
  let token
120
122
  try { token = await requestPasskeyGate('direct_pay_disclosure_ack', { order_id: orderId, stage }) }
121
- catch (e) { await window.dpPromptRegisterPasskey(e && e.message); return false }
123
+ catch (e) { await window.dpPromptRegisterPasskey(e); return false }
122
124
  const r = await POST('/direct-pay/disclosure-acks', { order_id: orderId, stage, webauthn_token: token })
123
125
  if (r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText(r.error_code, r.error), 'error'); return false }
124
126
  return true
125
127
  }
126
128
  // ── 订单详情:direct_p2p 诚实边界始终显示;卖家收款说明快照【不】内联进 HTML,由 dpHydrateOrderDisclosure 在 both-acked 后才另取渲染(未 ack 时 DOM 里也没有快照)。
127
- window.dpOrderDisclosureHtml = (_order) => `
129
+ window.dpOrderDisclosureHtml = (order) => `
128
130
  <div class="card" style="border:1px solid #fde68a;background:linear-gradient(135deg,#fffbeb,#fef3c7)">
129
131
  <div style="font-size:13px;font-weight:700;color:#92400e;margin-bottom:8px">💸 ${t('直付订单(非托管)')}</div>
130
132
  <ul style="margin:0 0 8px;padding-left:18px;font-size:12px;line-height:1.7;color:#374151">
131
133
  <li>${t('本金不经 WebAZ —— 你直接付款给卖家(场外)')}</li>
132
- <li>${t('WebAZ 不托管、不担保、不退款、不代维权')}</li>
134
+ <li>${t('非担保交易:WebAZ 不托管本金、无退款能力,仅对卖家有信誉处罚权')}</li>
133
135
  <li>${t('卖家收款说明来自卖家自填,WebAZ 不验证付款方式或币种')}</li>
134
136
  </ul>
135
- <div id="dp-order-instr">${loading$()}</div>
137
+ <div id="dp-order-instr" data-order-id="${escHtml(String(order && order.id || ''))}">${loading$()}</div>
136
138
  </div>`
137
139
  // ack-gated 收款说明:both-acked → 另取订单快照并展示;否则只显示"先完成 D1/D2 Passkey"的门(快照不入 DOM)。
138
140
  window.dpHydrateOrderDisclosure = async (orderId) => {
139
- const box = document.getElementById('dp-order-instr')
140
- if (!box) return
141
+ if (!window.dpInstrBox(orderId)) return
141
142
  const st = await GET(`/direct-pay/disclosure-acks/${orderId}`)
143
+ const box = window.dpInstrBox(orderId); if (!box) return // async 回包后再确认当前页仍是该订单(切页后旧回包不得写 DOM)
142
144
  if (st.error) { box.innerHTML = `<div style="font-size:12px;color:#dc2626">${window.dpErrorText(st.error_code, st.error)}</div>`; return }
143
145
  if (!st.both) {
144
146
  box.innerHTML = `<div style="font-size:12px;color:#92400e;background:#fff;border:1px solid #fde68a;border-radius:8px;padding:8px 10px">
@@ -147,10 +149,7 @@ window.dpHydrateOrderDisclosure = async (orderId) => {
147
149
  return
148
150
  }
149
151
  const o = await GET(`/orders/${orderId}`)
150
- const snap = o && o.order ? (o.order.direct_pay_instruction_snapshot || '') : ''
151
- box.innerHTML = snap ? `<div style="font-size:12px;color:#374151;background:#fff;border:1px solid #fde68a;border-radius:8px;padding:8px 10px">
152
- <div style="font-size:11px;color:#9ca3af;margin-bottom:2px">${t('卖家收款说明(下单时快照)')}</div>${escHtml(snap)}<div id="dp-order-qr"></div></div>`
153
- : `<div style="font-size:12px;color:#9ca3af">${t('卖家尚未设置收款说明,暂不可直付')}</div>`; if (snap && window.dpLoadOrderQr) window.dpLoadOrderQr(orderId) // D3:ack 门后取所选账号的收款二维码(若有)
152
+ if (window.dpRenderPaymentInfo) window.dpRenderPaymentInfo(box, o && o.order ? o.order : null, orderId) // PR-2:按订单状态渲染收款信息可见性(待支付=5min 自动窗口 / 其它状态=默认隐藏,需 Passkey 二次验证+风险提示)
154
153
  }
155
154
  window.dpCompleteAcksThenReveal = async (orderId) => {
156
155
  const ok = await window.dpEnsureAcks(orderId)
@@ -166,10 +165,10 @@ window.dpHandleAction = async (orderId, action) => {
166
165
  if (!acked) { show('error', window.dpErrorText('DISCLOSURE_NOT_ACKED')); return }
167
166
  let token
168
167
  try { token = await requestPasskeyGate('direct_pay_order_action', { order_id: orderId, action }) }
169
- catch (e) { await window.dpPromptRegisterPasskey(e && e.message); return }
168
+ catch (e) { await window.dpPromptRegisterPasskey(e); return }
170
169
  show('info', `<span class="spinner"></span>${t('处理中...')}`)
171
170
  const path = action === 'confirm_in_person' ? `/orders/${orderId}/confirm-in-person` : `/orders/${orderId}/action`
172
- const body = action === 'confirm_in_person' ? { webauthn_token: token } : { action, webauthn_token: token }
171
+ const body = action === 'confirm_in_person' ? { webauthn_token: token } : { action, webauthn_token: token, ...(action === 'mark_paid' && window.dpReadMemo ? { notes: window.dpReadMemo(orderId) } : {}) }
173
172
  const r = await POST(path, body)
174
173
  if (r.error) { show('error', window.dpErrorText(r.error_code, r.error)); return }
175
174
  show('success', t('操作成功'))
@@ -0,0 +1,38 @@
1
+ // 争议协商收口·买家侧 —— "我已收到货 · 撤诉并确认收货"。UI ONLY(权威门在 orders-action dispute_withdraw_confirm:
2
+ // delivered 来源履约争议 + 发起人本人 + 裁定前;dp 轨 D1/D2+Passkey)。可达性来自订单 DTO can_confirm_receipt_close_dispute。
3
+ // 注入:包装 mutualCancelCard(协商取消卡下追加;卖家侧收口=协商取消全额退款,买家侧=本卡),app.js 净零行。
4
+ ;(function () {
5
+ const S = window._notifSub
6
+ Object.assign(window.NOTIF_TEMPLATES, {
7
+ dispute_withdrawn_confirmed: (p) => ({ title: '✅ ' + t('买家已撤诉并确认收货'), body: S(t('买家撤回争议并确认收货,订单已完成结算,双方信誉不受影响。'), p) }),
8
+ })
9
+
10
+ const closeCard = (order) => {
11
+ if (!order || !order.can_confirm_receipt_close_dispute) return ''
12
+ const settleNote = order.payment_rail === 'direct_p2p'
13
+ ? t('确认后订单完成:货款你已线下支付,平台服务费照常计提。需 Passkey 确认。')
14
+ : t('确认后订单完成:托管货款将释放给卖家。')
15
+ return `<div class="card" style="border:1px solid #bbf7d0;background:linear-gradient(135deg,#f0fdf4,#dcfce7)">
16
+ <div style="font-size:13px;font-weight:700;color:#166534;margin-bottom:4px">📦 ${t('后来收到货了?')}</div>
17
+ <div style="font-size:12px;color:#4b5563;line-height:1.7;margin-bottom:8px">${t('包裹晚到/在代收点找到了?你可以撤回这条争议并确认收货,订单正常完成,双方信誉都不受影响。')}${settleNote}</div>
18
+ <button class="btn btn-primary btn-sm" style="width:auto" onclick="dcWithdrawConfirm('${order.id}','${order.payment_rail === 'direct_p2p' ? 1 : ''}')">${t('我已收到货 · 撤诉并确认收货')}</button>
19
+ </div>`
20
+ }
21
+
22
+ window.dcWithdrawConfirm = async (oid, isDp) => {
23
+ if (typeof confirmModal === 'function' && !(await confirmModal(t('确认撤回争议并确认收货?争议将关闭(不可恢复),订单立即完成结算。'), t('撤诉并确认收货'), { danger: true }))) return
24
+ const body = { action: 'dispute_withdraw_confirm' }
25
+ if (isDp) {
26
+ try { body.webauthn_token = await requestPasskeyGate('direct_pay_order_action', { order_id: oid, action: 'dispute_withdraw_confirm' }) }
27
+ catch (e) { if (window.dpPromptRegisterPasskey) await window.dpPromptRegisterPasskey(e); return }
28
+ }
29
+ const r = await POST('/orders/' + oid + '/action', body)
30
+ if (r.error) return void toast$(r.error, 'error')
31
+ toast$(t('已撤诉并确认收货,订单完成'), 'success')
32
+ renderOrderDetail(document.getElementById('app'), oid)
33
+ }
34
+
35
+ // 包装协商取消卡:disputed 订单页在其后追加买家收口卡(app.js 调用点不变,净零行)
36
+ const _origMcCard = window.mutualCancelCard
37
+ window.mutualCancelCard = (order, isBuyer, isSeller) => (_origMcCard ? _origMcCard(order, isBuyer, isSeller) : '') + (isBuyer ? closeCard(order) : '')
38
+ })()
@@ -0,0 +1,29 @@
1
+ // 满额免邮 UI(营销域,S2 返工)。UI ONLY —— 校验/判免全在后端(free-shipping.ts,建单 gate 应用)。入口:卖家后台「营销」tab 卡(app.js 净零 hook);点开就地 sheet 配店铺阈值,单品覆盖走 API。语义:促销非运费结构,券后货款≥阈值运费商家承担;供应商报价期规则不搬家。
2
+ ;(function () {
3
+ window.freeShippingMarketingCard = () => `
4
+ <div onclick="fsOpenSheet()" class="card" style="padding:14px;cursor:pointer;background:linear-gradient(135deg,#ecfeff,#cffafe);border-color:#67e8f9">
5
+ <div style="font-size:24px">🚚</div>
6
+ <div style="font-weight:600;font-size:13px;color:#155e75;margin-top:6px">${t('满额免邮')}</div>
7
+ <div style="font-size:10px;color:#0e7490;margin-top:2px">${t('拉客单价 · 运费你承担')}</div>
8
+ </div>`
9
+ window.fsOpenSheet = async () => {
10
+ const s = await GET('/seller/shipping-settings').catch(() => null)
11
+ const cur = s && s.store_free_shipping_threshold
12
+ const html = `
13
+ <div style="font-size:14px;font-weight:700;margin-bottom:6px">🚚 ${t('满额免邮(店铺默认)')}</div>
14
+ <div style="font-size:12px;color:#6b7280;margin-bottom:10px">${t('买家券后货款(不含保险/捐赠)达到阈值时,本单运费免收、由你承担。人工询价订单不适用;单品可经接口单独覆盖;只影响之后的新订单。')}</div>
15
+ <input class="form-control" id="fs-threshold" type="number" min="0" step="0.01" placeholder="${t('阈值金额(留空=关闭)')}" value="${cur ?? ''}" style="margin-bottom:10px">
16
+ <button class="btn btn-primary btn-sm" style="width:auto" onclick="fsSave()">${t('保存免邮设置')}</button>
17
+ <div id="fs-msg" style="margin-top:6px;font-size:11px"></div>`
18
+ if (typeof openSheet === 'function') openSheet(html)
19
+ else { const d = document.createElement('div'); d.className = 'card js-sheet'; d.style.cssText = 'position:fixed;left:50%;top:20%;transform:translateX(-50%);z-index:120;max-width:360px;width:92%'; d.innerHTML = html + `<button class="btn btn-outline btn-sm" style="width:auto;margin-top:8px" onclick="this.closest('.js-sheet').remove()">${t('关闭')}</button>`; document.body.appendChild(d) }
20
+ }
21
+ window.fsSave = async () => {
22
+ const raw = (document.getElementById('fs-threshold')?.value || '').trim()
23
+ const msg = document.getElementById('fs-msg')
24
+ if (raw !== '' && !(Number.isFinite(Number(raw)) && Number(raw) > 0)) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('阈值必须是正数')}</span>`; return } // 审计 P3:1e999→Infinity→JSON null→静默清除,前端就地拒
25
+ const r = await POST('/seller/shipping-template', { store_free_shipping_threshold: raw === '' ? null : Number(raw) })
26
+ if (r.error) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${escHtml(r.error)}</span>`; return }
27
+ if (msg) msg.innerHTML = `<span style="color:#16a34a">✓ ${t('已保存')}${raw === '' ? ' · ' + t('已关闭') : ''}</span>`
28
+ }
29
+ })()
@@ -0,0 +1,12 @@
1
+ // GMV 按支付轨拆分小注(卖家仪表盘共用)。UI ONLY。
2
+ // 托管(escrow)= 平台真实托管收入;直接收款(direct_p2p)= 场外收款,平台不经手 —— 二者不同性质,不该混成一个 GMV。
3
+ // 仅当【有直接收款】时显示(纯托管卖家不打扰);金额沿用调用方的计价显示。
4
+ ;(function () {
5
+ window.gmvRailSplitHtml = (escrow, directPay, fmt) => {
6
+ const d = Number(directPay) || 0
7
+ if (d <= 0) return '' // 无直接收款 → 不显示,GMV 就是纯托管
8
+ const e = Number(escrow) || 0
9
+ const f = typeof fmt === 'function' ? fmt : (n) => Number(n || 0).toFixed(0)
10
+ return `<div style="font-size:10px;color:#6b7280;margin-top:3px;line-height:1.5" title="${t('托管=平台托管收入;直接收款=场外收款,平台不经手')}">🏦 ${t('托管')} ${f(e)} · 🤝 ${t('直接收款')} ${f(d)}</div>`
11
+ }
12
+ })()