@seasonkoh/webaz 0.1.28 → 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.
- package/README.md +3 -2
- package/README.zh-CN.md +7 -6
- package/dist/currency.js +16 -0
- package/dist/deposit-rails.js +52 -0
- package/dist/direct-pay-aml-monitor.js +67 -0
- package/dist/direct-pay-aml-review.js +40 -0
- package/dist/direct-pay-base-bond-entry.js +5 -0
- package/dist/direct-pay-bond-rail-clearance.js +94 -0
- package/dist/direct-pay-compliance-ingress.js +145 -0
- package/dist/direct-pay-controls.js +136 -0
- package/dist/direct-pay-create.js +162 -0
- package/dist/direct-pay-deferral-quota.js +43 -0
- package/dist/direct-pay-disclosures.js +44 -0
- package/dist/direct-pay-eligibility.js +46 -0
- package/dist/direct-pay-fee-ar.js +241 -0
- package/dist/direct-pay-launch-readiness.js +108 -0
- package/dist/direct-pay-launch-summary.js +68 -0
- package/dist/direct-pay-ledger.js +94 -0
- package/dist/direct-receive-account-qr.js +77 -0
- package/dist/direct-receive-accounts.js +82 -0
- package/dist/direct-receive-deferral.js +142 -0
- package/dist/direct-receive-deposits.js +260 -0
- package/dist/direct-receive-payment-instruction.js +26 -0
- package/dist/fx-rates.js +71 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +531 -1
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +41 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +11 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +18 -16
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
- package/dist/ledger.js +12 -3
- package/dist/mcp.js +23 -4
- package/dist/merchant-bond-domain.js +64 -0
- package/dist/merchant-bond-exposure.js +78 -0
- package/dist/merchant-bond-watcher.js +26 -0
- package/dist/payment-rails.js +29 -0
- package/dist/product-verification.js +93 -0
- package/dist/pwa/acp-feed.js +3 -2
- package/dist/pwa/contract-fingerprint.js +3 -0
- package/dist/pwa/direct-pay-guards.js +20 -0
- package/dist/pwa/direct-pay-order-redaction.js +24 -0
- package/dist/pwa/endpoint-actions.js +3 -0
- package/dist/pwa/public/app-ai.js +10 -10
- package/dist/pwa/public/app-chat-poll.js +29 -0
- package/dist/pwa/public/app-create-kinds.js +17 -0
- package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
- package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
- package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
- package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
- package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
- package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
- package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
- package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
- package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
- package/dist/pwa/public/app-direct-pay.js +227 -0
- package/dist/pwa/public/app-discover.js +20 -20
- package/dist/pwa/public/app-external-links.js +32 -0
- package/dist/pwa/public/app-listings.js +4 -4
- package/dist/pwa/public/app-prelaunch-waz.js +39 -0
- package/dist/pwa/public/app-price.js +55 -0
- package/dist/pwa/public/app-product-media.js +15 -0
- package/dist/pwa/public/app-profile.js +6 -6
- package/dist/pwa/public/app-seller.js +5 -5
- package/dist/pwa/public/app-shop.js +19 -19
- package/dist/pwa/public/app.js +142 -146
- package/dist/pwa/public/i18n.js +398 -197
- package/dist/pwa/public/index.html +17 -0
- package/dist/pwa/public/openapi.json +495 -1
- package/dist/pwa/routes/admin-analytics.js +4 -2
- package/dist/pwa/routes/admin-direct-receive-deposits.js +321 -0
- package/dist/pwa/routes/buyer-feeds.js +2 -1
- package/dist/pwa/routes/chat.js +6 -1
- package/dist/pwa/routes/dashboards.js +2 -1
- package/dist/pwa/routes/direct-pay-availability.js +196 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +71 -0
- package/dist/pwa/routes/direct-receive-accounts.js +155 -0
- package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
- package/dist/pwa/routes/fx.js +12 -0
- package/dist/pwa/routes/leaderboard.js +47 -9
- package/dist/pwa/routes/listings.js +2 -2
- package/dist/pwa/routes/logistics.js +4 -0
- package/dist/pwa/routes/manifests.js +38 -0
- package/dist/pwa/routes/me-data.js +5 -2
- package/dist/pwa/routes/orders-action.js +117 -9
- package/dist/pwa/routes/orders-create.js +4 -0
- package/dist/pwa/routes/orders-read.js +36 -0
- package/dist/pwa/routes/p2p-products.js +2 -2
- package/dist/pwa/routes/products-create.js +5 -3
- package/dist/pwa/routes/products-links.js +34 -0
- package/dist/pwa/routes/products-list.js +2 -1
- package/dist/pwa/routes/products-update.js +22 -2
- package/dist/pwa/routes/promoter.js +3 -0
- package/dist/pwa/routes/referral.js +4 -0
- package/dist/pwa/routes/returns.js +26 -1
- package/dist/pwa/routes/rewards-apply.js +10 -5
- package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
- package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
- package/dist/pwa/routes/shops.js +2 -1
- package/dist/pwa/routes/url-claim.js +2 -2
- package/dist/pwa/routes/users-public.js +8 -0
- package/dist/pwa/routes/wallet-read.js +3 -0
- package/dist/pwa/routes/webauthn.js +1 -1
- package/dist/pwa/server.js +59 -102
- package/dist/runtime/webaz-schema-helpers.js +104 -0
- package/dist/store-verification.js +77 -0
- package/dist/version.js +1 -1
- package/package.json +71 -2
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — ADMIN 缓交(deferred base-bond)审批队列 (PR-②c)。UI ONLY。
|
|
2
|
+
// ROOT admin 审批商户的缓交申请:列出 pending,批准(设压低额度系数 + 宽限天数)或拒绝。
|
|
3
|
+
// 铁律:批准/拒绝是 RISK 动作 → 必须真人 Passkey(requestPasskeyGate),purpose_data 绑定【完整条款】
|
|
4
|
+
// (deferral_id + reduced_quota_factor + grace_days),与请求体逐字一致;agent 无 Passkey 凭证会被后端硬拒。
|
|
5
|
+
// 纯前端:只调既有受门控端点 GET/POST /api/admin/direct-receive/deferrals[/:id/approve|reject];
|
|
6
|
+
// 不碰 wallet/escrow/settlement/refund;授予绝不自动(approveDeferral 是唯一 writer,后端 ROOT+Passkey 强制)。
|
|
7
|
+
|
|
8
|
+
window.renderAdminDirectPayDeferrals = async function (app) {
|
|
9
|
+
if (!state.user) { renderLogin(); return }
|
|
10
|
+
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
11
|
+
app.innerHTML = shell(loading$(), 'admin')
|
|
12
|
+
const data = await GET('/admin/direct-receive/deferrals?status=pending')
|
|
13
|
+
if (!data || data.error) { app.innerHTML = shell(alert$('error', (data && data.error) || t('加载失败')), 'admin'); return }
|
|
14
|
+
const rows = (data.deferrals || [])
|
|
15
|
+
const list = rows.length ? rows.map(d => `
|
|
16
|
+
<div class="card" style="margin-bottom:10px">
|
|
17
|
+
<div style="font-size:11px;color:#9ca3af">${escHtml(d.user_id)} · ${fmtTime(d.created_at)}</div>
|
|
18
|
+
<div style="font-size:13px;margin-top:6px">${t('缓交期(天)')}: <strong>${d.period_days}</strong></div>
|
|
19
|
+
${d.reason ? `<div style="font-size:12px;color:#6b7280;margin-top:6px;padding:6px;background:#f9fafb;border-radius:4px">${escHtml(d.reason)}</div>` : ''}
|
|
20
|
+
<div style="display:flex;gap:6px;margin-top:10px">
|
|
21
|
+
<button class="btn btn-primary btn-sm" style="font-size:12px" onclick="toggleInline('dfr-approve-${d.id}')">✓ ${t('批准')}</button>
|
|
22
|
+
<button class="btn btn-outline btn-sm" style="font-size:12px;color:#dc2626;border-color:#dc2626" onclick="toggleInline('dfr-reject-${d.id}')">✗ ${t('拒绝')}</button>
|
|
23
|
+
</div>
|
|
24
|
+
<div id="dfr-approve-${d.id}" style="display:none;margin-top:10px;padding:10px;background:#f0fdf4;border:1px solid #86efac;border-radius:6px">
|
|
25
|
+
<div style="font-size:12px;font-weight:600;color:#166534;margin-bottom:6px">✓ ${t('批准缓交(需真人 Passkey)')}</div>
|
|
26
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:6px">${t('缓交期额度系数(0–1,留空用默认;缓交期内必压低,有下限)')}</div>
|
|
27
|
+
<input id="dfr-approve-factor-${d.id}" type="number" min="0" max="1" step="0.05" placeholder="${t('额度系数(如 0.5)')}" style="width:100%;padding:6px;border:1px solid #d1d5db;border-radius:4px;font-size:12px;margin-bottom:6px">
|
|
28
|
+
<input id="dfr-approve-grace-${d.id}" type="number" min="0" placeholder="${t('宽限天数(留空用默认)')}" style="width:100%;padding:6px;border:1px solid #d1d5db;border-radius:4px;font-size:12px;margin-bottom:6px">
|
|
29
|
+
<div style="display:flex;gap:6px">
|
|
30
|
+
<button class="btn btn-outline btn-sm" style="font-size:11px" onclick="toggleInline('dfr-approve-${d.id}')">${t('取消')}</button>
|
|
31
|
+
<button class="btn btn-primary btn-sm" style="font-size:11px" onclick="doApproveDeferralInline('${d.id}')">${t('真人确认批准')}</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div id="dfr-reject-${d.id}" style="display:none;margin-top:10px;padding:10px;background:#fef2f2;border:1px solid #fca5a5;border-radius:6px">
|
|
35
|
+
<div style="font-size:12px;font-weight:600;color:#991b1b;margin-bottom:6px">✗ ${t('拒绝缓交(需真人 Passkey)')}</div>
|
|
36
|
+
<div style="display:flex;gap:6px">
|
|
37
|
+
<button class="btn btn-outline btn-sm" style="font-size:11px" onclick="toggleInline('dfr-reject-${d.id}')">${t('取消')}</button>
|
|
38
|
+
<button class="btn btn-sm" style="font-size:11px;background:#dc2626;border-color:#dc2626;color:#fff" onclick="doRejectDeferralInline('${d.id}')">${t('真人确认拒绝')}</button>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>`).join('') : `<div class="empty"><div class="empty-icon">📥</div><div class="empty-text">${t('暂无待审缓交申请')}</div></div>`
|
|
42
|
+
app.innerHTML = shell(`
|
|
43
|
+
<h1 class="page-title">🪙 ${t('履约保证金缓交审批')}</h1>
|
|
44
|
+
<div style="margin-bottom:12px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin')">${t('返回概览')}</button></div>
|
|
45
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('批准后该卖家可先入场直付、保证金延后交;直付仍需满足全部合规条件,且缓交期内额度被压低。批准/拒绝均需真人 Passkey。')}</div>
|
|
46
|
+
${list}
|
|
47
|
+
`, 'admin')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 批准:purpose_data 必须与请求体逐字一致(后端绑定 deferral_id + reduced_quota_factor + grace_days)。
|
|
51
|
+
window.doApproveDeferralInline = async function (id) {
|
|
52
|
+
const fEl = document.getElementById('dfr-approve-factor-' + id), gEl = document.getElementById('dfr-approve-grace-' + id)
|
|
53
|
+
const body = { deferral_id: id }
|
|
54
|
+
if (fEl && fEl.value !== '') body.reduced_quota_factor = Number(fEl.value)
|
|
55
|
+
if (gEl && gEl.value !== '') body.grace_days = Number(gEl.value)
|
|
56
|
+
const token = await requestPasskeyGate('direct_pay_deferral_approve', body)
|
|
57
|
+
if (!token) { if (typeof toast$ === 'function') toast$(t('需要真人 Passkey 确认'), 'error'); return }
|
|
58
|
+
const r = await POST(`/admin/direct-receive/deferrals/${id}/approve`, { ...body, webauthn_token: token })
|
|
59
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
60
|
+
if (typeof toast$ === 'function') toast$(t('缓交已批准'))
|
|
61
|
+
window.renderAdminDirectPayDeferrals(document.getElementById('app'))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 拒绝:purpose_data 绑 deferral_id。
|
|
65
|
+
window.doRejectDeferralInline = async function (id) {
|
|
66
|
+
const token = await requestPasskeyGate('direct_pay_deferral_reject', { deferral_id: id })
|
|
67
|
+
if (!token) { if (typeof toast$ === 'function') toast$(t('需要真人 Passkey 确认'), 'error'); return }
|
|
68
|
+
const r = await POST(`/admin/direct-receive/deferrals/${id}/reject`, { deferral_id: id, webauthn_token: token })
|
|
69
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
70
|
+
if (typeof toast$ === 'function') toast$(t('缓交申请已拒绝'))
|
|
71
|
+
window.renderAdminDirectPayDeferrals(document.getElementById('app'))
|
|
72
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — SELLER 缓交(deferred base-bond)apply + status panel (PR-②b)。UI ONLY。
|
|
2
|
+
// 缓交 = 先入场、履约保证金延后交,由管理员【人工审批】(绝不自动批)。本面板让卖家:① 申请缓交;② 查看自己申请状态。
|
|
3
|
+
// 数据/动作:GET + POST /api/direct-receive/deferral(后端脱敏:不含 admin 身份)。申请只创建 pending,【不授予任何资格】;
|
|
4
|
+
// 真正放行 = 管理员 ROOT + 真人 Passkey 审批。即便批准,直付仍需满足全部合规门(实名/制裁/AML/Passkey/收款说明)。
|
|
5
|
+
// 不碰 wallet/escrow/settlement/refund/钱路;不开真实 rail;不声称已上线。买家侧不展示本面板。
|
|
6
|
+
|
|
7
|
+
window.dpDeferralCopy = () => ({
|
|
8
|
+
pending: { icon: '⏳', text: t('缓交申请审核中,等待管理员人工审批') },
|
|
9
|
+
granted: { icon: '✅', text: t('缓交已批准') },
|
|
10
|
+
rejected: { icon: '🚫', text: t('缓交申请未通过') },
|
|
11
|
+
expired: { icon: '⌛', text: t('缓交已到期') },
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
window.dpSellerDeferralSection = () => `
|
|
15
|
+
<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">
|
|
16
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:4px">🪙 ${t('履约保证金缓交(仅你可见)')}</div>
|
|
17
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('缓交 = 先入场、履约保证金延后交,由管理员人工审批。批准后直付仍需满足全部合规条件(身份与商户合规审核、Passkey、收款说明);缓交期内额度会被压低。')}</div>
|
|
18
|
+
<div id="dp-seller-deferral">${loading$()}</div>
|
|
19
|
+
</div>`
|
|
20
|
+
|
|
21
|
+
// 申请表单(无活跃申请时显示)。period 选填(留空走后端默认 30);reason 选填。
|
|
22
|
+
window.dpDeferralApplyForm = () => `
|
|
23
|
+
<div style="display:flex;flex-direction:column;gap:8px">
|
|
24
|
+
<textarea id="dp-dfr-reason" maxlength="500" placeholder="${t('申请原因(选填)')}" style="width:100%;min-height:56px;padding:8px;border:1px solid #d1d5db;border-radius:6px;font-size:13px"></textarea>
|
|
25
|
+
<input id="dp-dfr-days" type="number" min="1" placeholder="${t('缓交天数(选填,默认 30)')}" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;font-size:13px">
|
|
26
|
+
<button onclick="window.dpSubmitDeferral()" style="align-self:flex-start;padding:8px 16px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer">${t('提交缓交申请')}</button>
|
|
27
|
+
</div>`
|
|
28
|
+
|
|
29
|
+
window.dpHydrateSellerDeferral = async () => {
|
|
30
|
+
const box = document.getElementById('dp-seller-deferral')
|
|
31
|
+
if (!box) return
|
|
32
|
+
const r = await GET('/direct-receive/deferral')
|
|
33
|
+
if (!r || r.error) { box.innerHTML = `<div style="font-size:12px;color:#dc2626">${window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试')}</div>`; return }
|
|
34
|
+
const d = r.deferral, active = r.active
|
|
35
|
+
// 无申请,或上一条已拒绝/过期(且当前无生效缓交)→ 可(重新)申请。
|
|
36
|
+
if (!d || (!active && (d.status === 'rejected' || d.status === 'expired'))) {
|
|
37
|
+
const prior = d ? `<div style="font-size:12px;color:#6b7280;margin-bottom:8px">${window.dpDeferralCopy()[d.status].icon} ${escHtml(window.dpDeferralCopy()[d.status].text)} · ${t('可重新申请')}</div>` : ''
|
|
38
|
+
box.innerHTML = prior + window.dpDeferralApplyForm()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const meta = window.dpDeferralCopy()[d.status] || { icon: 'ℹ️', text: d.status }
|
|
42
|
+
const lines = [`<div style="font-size:13px;font-weight:600;color:#374151">${meta.icon} ${escHtml(meta.text)}</div>`]
|
|
43
|
+
if (active) {
|
|
44
|
+
lines.push(`<div style="font-size:12px;color:#6b7280;line-height:1.7">${t('缓交期额度系数')}: <b>${active.reduced_quota_factor}</b></div>`)
|
|
45
|
+
if (active.expires_at) lines.push(`<div style="font-size:12px;color:#6b7280">${t('保证金到期日')}: ${escHtml(active.expires_at)}</div>`)
|
|
46
|
+
if (active.grace_until) lines.push(`<div style="font-size:12px;color:#6b7280">${t('宽限至')}: ${escHtml(active.grace_until)}</div>`)
|
|
47
|
+
if (active.in_grace) lines.push(`<div style="font-size:12px;color:#dc2626;font-weight:600">⚠️ ${t('已进入宽限期,请尽快补交履约保证金,否则直付资格将被暂停')}</div>`)
|
|
48
|
+
}
|
|
49
|
+
box.innerHTML = `<div style="display:flex;flex-direction:column;gap:4px">${lines.join('')}</div>`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
window.dpSubmitDeferral = async () => {
|
|
53
|
+
const reasonEl = document.getElementById('dp-dfr-reason'), daysEl = document.getElementById('dp-dfr-days')
|
|
54
|
+
const body = {}
|
|
55
|
+
if (reasonEl && reasonEl.value.trim()) body.reason = reasonEl.value.trim()
|
|
56
|
+
if (daysEl && daysEl.value) body.period_days = Number(daysEl.value)
|
|
57
|
+
const r = await POST('/direct-receive/deferral', body)
|
|
58
|
+
if (!r || r.error) { alert(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试')); return }
|
|
59
|
+
toast$(t('缓交申请已提交,等待管理员审批'))
|
|
60
|
+
window.dpHydrateSellerDeferral()
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — SELLER 平台服务费账户 section(PR-C)。UI ONLY。
|
|
2
|
+
// 卖家自查【本人】平台服务费预充值余额 / 已计提 / 退款 / 在途预估 / 首单宽限。
|
|
3
|
+
// 数据来自 GET /api/direct-receive/my-fee-account(requireSeller,仅本人;买家侧不展示)。
|
|
4
|
+
// 预充值 = 商家平台服务费预付款(非买家货款 / 非 escrow / 非保证金);充值由平台核实收款后由 admin 登记,卖家不自助。
|
|
5
|
+
// 纯读;不碰 wallet/escrow/settlement/refund/钱路。section 形态,嵌入卖家「设置」子页(与 readiness/缓交/验证/收款说明并列)。
|
|
6
|
+
|
|
7
|
+
function _dpFeeFmt2(u) { return (Number(u || 0) / 1e6).toFixed(6).replace(/\.?0+$/, '') + ' USDC' }
|
|
8
|
+
|
|
9
|
+
window.dpSellerFeeSection = () => `
|
|
10
|
+
<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">
|
|
11
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:4px">💰 ${t('平台服务费账户(仅你可见)')}</div>
|
|
12
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('直付平台服务费的预充值余额与已计提明细;预充值由平台核实收款后登记,如需充值请联系平台。')}</div>
|
|
13
|
+
<div id="dp-seller-fee">${typeof loading$ === 'function' ? loading$() : ''}</div>
|
|
14
|
+
</div>`
|
|
15
|
+
|
|
16
|
+
window.dpHydrateSellerFee = async () => {
|
|
17
|
+
const box = document.getElementById('dp-seller-fee')
|
|
18
|
+
if (!box) return
|
|
19
|
+
const r = await GET('/direct-receive/my-fee-account')
|
|
20
|
+
if (!r || r.error || !r.account) { box.innerHTML = `<div style="font-size:12px;color:#dc2626">${window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试')}</div>`; return }
|
|
21
|
+
const a = r.account
|
|
22
|
+
const owed = Number(a.availableUnits) < 0
|
|
23
|
+
const row = (k, v) => `<div style="display:flex;justify-content:space-between;font-size:13px;padding:3px 0"><span style="color:#6b7280">${k}</span><span style="font-weight:600">${v}</span></div>`
|
|
24
|
+
box.innerHTML = `
|
|
25
|
+
<div style="font-size:15px;font-weight:700;margin-bottom:6px">${owed ? t('待补平台服务费') : t('可用预充值余额')}: <span style="color:${owed ? '#dc2626' : '#059669'}">${_dpFeeFmt2(Math.abs(Number(a.availableUnits)))}</span></div>
|
|
26
|
+
${owed ? `<div style="font-size:12px;color:#b45309;line-height:1.6;margin-bottom:6px">${t('余额不足时新直付订单会被暂停,请联系平台补充平台服务费预充值。')}</div>` : ''}
|
|
27
|
+
${row(t('累计预充值'), _dpFeeFmt2(a.topupUnits))}
|
|
28
|
+
${row(t('已计提平台费'), _dpFeeFmt2(a.accruedUnits))}
|
|
29
|
+
${Number(a.adjustmentUnits) !== 0 ? row(t('账务更正合计'), _dpFeeFmt2(a.adjustmentUnits)) : ''}
|
|
30
|
+
${row(t('已退款合计'), _dpFeeFmt2(a.refundUnits))}
|
|
31
|
+
${row(t('在途单预估费'), _dpFeeFmt2(a.openEstFeeUnits))}
|
|
32
|
+
${a.graceEligible ? `<div style="font-size:12px;color:#059669;margin-top:6px">${t('首单宽限可用:你的第一笔直付无需预充值。')}</div>` : ''}`
|
|
33
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — ADMIN 商户运营 hub + 平台服务费(预充值)账户。UI ONLY(PR-B)。
|
|
2
|
+
// hub 把散落的直付 admin 功能归类(资格合规 / 平台服务费 / 上线控制),只链接既有路由。
|
|
3
|
+
// fee 账户调既有 ROOT + 真人 Passkey 端点(admin-direct-receive-deposits):
|
|
4
|
+
// GET /admin/direct-receive/fee-account/:seller_id (ROOT 只读)
|
|
5
|
+
// POST /admin/direct-receive/fee-prepay (purpose direct_pay_fee_prepay_record)
|
|
6
|
+
// POST /admin/direct-receive/fee-adjust (purpose direct_pay_fee_adjust)
|
|
7
|
+
// POST /admin/direct-receive/fee-refund (purpose direct_pay_fee_refund)
|
|
8
|
+
// 钱相关写动作均 requestPasskeyGate(purpose, body) → POST(purpose_data 与 body 逐字绑定)。
|
|
9
|
+
// 不碰 buyer wallet/escrow/order/状态机;预充值 = 商家平台服务费预付款,非买家货款/escrow/保证金。
|
|
10
|
+
|
|
11
|
+
window.renderAdminDirectPayHub = function (app) {
|
|
12
|
+
if (!state.user) { renderLogin(); return }
|
|
13
|
+
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
14
|
+
const root = (state.user.admin_type || 'root') === 'root'
|
|
15
|
+
const grp = (title, cards) => `<div style="font-size:13px;font-weight:700;color:#374151;margin:14px 0 6px">${title}</div>${cards}`
|
|
16
|
+
app.innerHTML = shell(`
|
|
17
|
+
<h1 class="page-title">💳 ${t('Direct Pay 商户运营')}</h1>
|
|
18
|
+
<div style="margin-bottom:8px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin/protocol')">${t('返回协议管理')}</button></div>
|
|
19
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:6px">${t('直付(Rail 1)商户运营集中入口:资格合规、平台服务费预充值、上线控制。')}</div>
|
|
20
|
+
${root ? grp(t('资格与合规'),
|
|
21
|
+
adminLinkCard('🧾', t('商户合规录入'), t('KYB / 制裁筛查结论'), '#admin/compliance') +
|
|
22
|
+
adminLinkCard('🪙', t('履约保证金缓交审批'), t('缓交申请 + 压低额度'), '#admin/deferrals') +
|
|
23
|
+
adminLinkCard('🔖', t('逐产品直付验证'), t('核验商品外链验证码'), '#admin/product-verifications') +
|
|
24
|
+
adminLinkCard('🏬', t('店铺认证审核'), t('核验店铺外链 + 免逐品'), '#admin/store-verifications')) : ''}
|
|
25
|
+
${root ? grp(t('平台服务费(预充值)'),
|
|
26
|
+
adminLinkCard('💰', t('预充值与账户'), t('充值 / 调整 / 退款 / 余额 / 应收'), '#admin/dp-fee')) : ''}
|
|
27
|
+
${grp(t('上线控制'),
|
|
28
|
+
adminLinkCard('⚙️', t('直付参数'), t('开关 / 地区 / 单笔上限'), '#admin/params'))}
|
|
29
|
+
<div style="font-size:12px;color:#6b7280;margin-top:12px">${t('就绪报告(CLI):')} <code>npm run direct-pay:readiness</code></div>
|
|
30
|
+
`, 'admin')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// USDC 显示(units → 去尾零)
|
|
34
|
+
function _dpFeeFmt(u) { return (Number(u || 0) / 1e6).toFixed(6).replace(/\.?0+$/, '') + ' USDC' }
|
|
35
|
+
// 金额输入(USDC,最多 6 位小数)→ 整数 base-units(纯整数运算,无浮点四舍五入);松散/超精度输入 → NaN。
|
|
36
|
+
function _dpFeeUnits(id) {
|
|
37
|
+
const e = document.getElementById(id); const raw = e ? e.value.trim() : ''
|
|
38
|
+
if (!/^-?\d+(\.\d{1,6})?$/.test(raw)) return NaN // 拒 '1abc' / 超 6 位小数 / 空
|
|
39
|
+
const neg = raw[0] === '-', s = neg ? raw.slice(1) : raw
|
|
40
|
+
const [intp, frac = ''] = s.split('.')
|
|
41
|
+
const units = Number(intp) * 1e6 + Number((frac + '000000').slice(0, 6))
|
|
42
|
+
return neg ? -units : units
|
|
43
|
+
}
|
|
44
|
+
function _dpFeeStr(id) { const e = document.getElementById(id); return e ? e.value.trim() : '' }
|
|
45
|
+
|
|
46
|
+
window.renderAdminDirectPayFeeOps = function (app) {
|
|
47
|
+
if (!state.user) { renderLogin(); return }
|
|
48
|
+
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
49
|
+
if ((state.user.admin_type || 'root') !== 'root') { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限根管理员')}</div>`, 'admin'); return }
|
|
50
|
+
const inp = (id, ph, num) => `<input id="${id}" ${num ? 'type="number" step="any"' : ''} placeholder="${ph}" style="width:100%;padding:7px;border:1px solid #d1d5db;border-radius:6px;font-size:13px">`
|
|
51
|
+
const meth = (id) => `<select id="${id}" style="width:100%;padding:7px;border:1px solid #d1d5db;border-radius:6px;font-size:13px"><option value="usdc">USDC</option><option value="fiat">${t('法币 fiat')}</option></select>`
|
|
52
|
+
const card = (inner) => `<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">${inner}</div>`
|
|
53
|
+
app.innerHTML = shell(`
|
|
54
|
+
<h1 class="page-title">💰 ${t('平台服务费预充值与账户')}</h1>
|
|
55
|
+
<div style="margin-bottom:8px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin/dp-ops')">${t('返回 Direct Pay 商户运营')}</button></div>
|
|
56
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:6px">${t('预充值 = 商家平台服务费预付款(非买家货款 / 非 escrow / 非保证金)。链下实际收款/退款由你核实后在此登记;均需真人 Passkey。')}</div>
|
|
57
|
+
${card(`<div style="font-size:13px;font-weight:700;margin-bottom:8px">${t('卖家用户 ID')}</div>${inp('fee-seller', t('卖家用户 ID(seller user id)'))}<button class="btn btn-primary btn-sm" style="margin-top:8px;width:auto" onclick="window.dpFeeLoad()">${t('加载账户')}</button>`)}
|
|
58
|
+
<div id="fee-acct"></div>
|
|
59
|
+
${card(`<div style="font-size:13px;font-weight:700;margin-bottom:8px">➕ ${t('记录预充值(收款)')}</div><div style="display:flex;flex-direction:column;gap:6px">${inp('fee-topup-amt', t('金额 USDC'), true)}${meth('fee-topup-method')}${inp('fee-topup-ev', t('收款凭证号 evidence_ref(选填)'))}${inp('fee-topup-note', t('备注(选填)'))}<button class="btn btn-primary btn-sm" style="align-self:flex-start;font-size:12px" onclick="window.dpFeeTopup()">${t('记录预充值(真人 Passkey)')}</button></div>`)}
|
|
60
|
+
${card(`<div style="font-size:13px;font-weight:700;margin-bottom:8px">✏️ ${t('账务更正(可正可负,非退款)')}</div><div style="display:flex;flex-direction:column;gap:6px">${inp('fee-adj-amt', t('更正额 USDC(负数=调减)'), true)}${inp('fee-adj-reason', t('原因(必填)'))}<button class="btn btn-outline btn-sm" style="align-self:flex-start;font-size:12px" onclick="window.dpFeeAdjust()">${t('记录更正(真人 Passkey)')}</button></div>`)}
|
|
61
|
+
${card(`<div style="font-size:13px;font-weight:700;margin-bottom:8px">↩️ ${t('退款(真实退还未消耗预付款)')}</div><div style="display:flex;flex-direction:column;gap:6px">${inp('fee-ref-amt', t('退款额 USDC'), true)}${meth('fee-ref-method')}${inp('fee-ref-ev', t('退款凭证号 evidence_ref(选填)'))}${inp('fee-ref-reason', t('原因(选填)'))}<button class="btn btn-outline btn-sm" style="align-self:flex-start;font-size:12px;color:#b45309;border-color:#fcd34d" onclick="window.dpFeeRefund()">${t('退款(真人 Passkey)')}</button></div>`)}
|
|
62
|
+
`, 'admin')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
window.dpFeeLoad = async function () {
|
|
66
|
+
const sid = _dpFeeStr('fee-seller'); const box = document.getElementById('fee-acct'); if (!box) return
|
|
67
|
+
if (!sid) { box.innerHTML = ''; return }
|
|
68
|
+
const r = await GET('/admin/direct-receive/fee-account/' + encodeURIComponent(sid))
|
|
69
|
+
if (!r || !r.account) { box.innerHTML = `<div class="alert alert-error">${t('加载失败')}</div>`; return }
|
|
70
|
+
const a = r.account
|
|
71
|
+
const row = (k, v) => `<div style="display:flex;justify-content:space-between;font-size:13px;padding:3px 0"><span style="color:#6b7280">${k}</span><span style="font-weight:600">${v}</span></div>`
|
|
72
|
+
box.innerHTML = `<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">
|
|
73
|
+
<div style="font-size:15px;font-weight:700;margin-bottom:6px">${t('可用预充值余额')}: <span style="color:${a.availableUnits < 0 ? '#dc2626' : '#059669'}">${_dpFeeFmt(a.availableUnits)}</span></div>
|
|
74
|
+
${row(t('累计预充值'), _dpFeeFmt(a.topupUnits))}
|
|
75
|
+
${row(t('已计提平台费'), _dpFeeFmt(a.accruedUnits))}
|
|
76
|
+
${row(t('账务更正合计'), _dpFeeFmt(a.adjustmentUnits))}
|
|
77
|
+
${row(t('已退款合计'), _dpFeeFmt(a.refundUnits))}
|
|
78
|
+
${row(t('在途单预估费'), _dpFeeFmt(a.openEstFeeUnits))}
|
|
79
|
+
${row(t('可退款余额'), _dpFeeFmt(Math.max(0, Number(a.availableUnits) - Number(a.openEstFeeUnits))))}
|
|
80
|
+
${row(t('首单宽限'), a.graceEligible ? t('是(尚未成交)') : t('否(已用/有在途)'))}
|
|
81
|
+
</div>`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function _dpFeeAction(purpose, path, bind, extraBody, okMsg) {
|
|
85
|
+
const token = await requestPasskeyGate(purpose, bind)
|
|
86
|
+
if (!token) { if (typeof toast$ === 'function') toast$(t('需要真人 Passkey 确认'), 'error'); return }
|
|
87
|
+
const r = await POST(path, { ...bind, ...extraBody, webauthn_token: token })
|
|
88
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
89
|
+
if (typeof toast$ === 'function') toast$(okMsg)
|
|
90
|
+
window.dpFeeLoad()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
window.dpFeeTopup = function () {
|
|
94
|
+
const seller_id = _dpFeeStr('fee-seller'); const amount_units = _dpFeeUnits('fee-topup-amt')
|
|
95
|
+
if (!seller_id || !(amount_units > 0)) { if (typeof toast$ === 'function') toast$(t('请填写卖家 ID 和正数金额'), 'error'); return }
|
|
96
|
+
const bind = { seller_id, amount_units, method: _dpFeeStr('fee-topup-method') || 'usdc', evidence_ref: _dpFeeStr('fee-topup-ev') }
|
|
97
|
+
return _dpFeeAction('direct_pay_fee_prepay_record', '/admin/direct-receive/fee-prepay', bind, { note: _dpFeeStr('fee-topup-note') }, t('预充值已记录'))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
window.dpFeeAdjust = function () {
|
|
101
|
+
const seller_id = _dpFeeStr('fee-seller'); const delta_units = _dpFeeUnits('fee-adj-amt'); const reason = _dpFeeStr('fee-adj-reason')
|
|
102
|
+
if (!seller_id || !Number.isFinite(delta_units) || delta_units === 0 || !reason) { if (typeof toast$ === 'function') toast$(t('请填写卖家 ID、非零更正额、原因'), 'error'); return }
|
|
103
|
+
const bind = { seller_id, delta_units, reason }
|
|
104
|
+
return _dpFeeAction('direct_pay_fee_adjust', '/admin/direct-receive/fee-adjust', bind, {}, t('账务更正已记录'))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
window.dpFeeRefund = function () {
|
|
108
|
+
const seller_id = _dpFeeStr('fee-seller'); const amount_units = _dpFeeUnits('fee-ref-amt')
|
|
109
|
+
if (!seller_id || !(amount_units > 0)) { if (typeof toast$ === 'function') toast$(t('请填写卖家 ID 和正数退款额'), 'error'); return }
|
|
110
|
+
const bind = { seller_id, amount_units, method: _dpFeeStr('fee-ref-method') || 'usdc', evidence_ref: _dpFeeStr('fee-ref-ev') }
|
|
111
|
+
return _dpFeeAction('direct_pay_fee_refund', '/admin/direct-receive/fee-refund', bind, { reason: _dpFeeStr('fee-ref-reason') }, t('退款已记录'))
|
|
112
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — 按产品认证 UI (PR-④b)。UI ONLY。两个面:
|
|
2
|
+
// ① SELLER 自助(seller workbench settings):逐产品申领验证码 → 贴到外部商品页 → 提交该产品外链 → 看逐产品状态。
|
|
3
|
+
// ② ADMIN 审核队列(#admin/product-verifications):打开卖家提交的外链手动核对验证码,verify/reject(ROOT + 真人 Passkey)。
|
|
4
|
+
// 硬门:每个产品都须【单独】被 admin 核验才可直付;一次验证绝不放行所有产品。诚实:WebAZ 不抓取链接,admin 手动核。
|
|
5
|
+
// 不碰 wallet/escrow/settlement/refund;只调既有受门控端点。
|
|
6
|
+
|
|
7
|
+
// ── 状态 → 双语文案 ──
|
|
8
|
+
window.dpPvCopy = () => ({
|
|
9
|
+
issued: { icon: '①', text: t('已签发验证码,请贴到该商品的外部页面后提交链接') },
|
|
10
|
+
submitted: { icon: '⏳', text: t('已提交,等待管理员核验') },
|
|
11
|
+
verified: { icon: '✅', text: t('已通过验证,可直付') },
|
|
12
|
+
rejected: { icon: '🚫', text: t('未通过,请修正后重新申请') },
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// ══════ SELLER 自助面板 ══════
|
|
16
|
+
window.dpSellerProductVerifySection = () => `
|
|
17
|
+
<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">
|
|
18
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:4px">🔖 ${t('逐产品直付验证(仅你可见)')}</div>
|
|
19
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('每个走直付收款的商品都需【单独】通过平台人工验证(防作弊):申领验证码 → 贴到该商品的外部平台页面 → 回来提交该商品链接 → 管理员手动核对。未验证的商品只能用托管交易。')}</div>
|
|
20
|
+
<div id="dp-seller-pv">${loading$()}</div>
|
|
21
|
+
</div>`
|
|
22
|
+
|
|
23
|
+
window.dpHydrateSellerProductVerify = async () => {
|
|
24
|
+
const box = document.getElementById('dp-seller-pv')
|
|
25
|
+
if (!box) return
|
|
26
|
+
const [products, vres] = await Promise.all([GET('/my-products'), GET('/direct-receive/product-verifications')])
|
|
27
|
+
if (!Array.isArray(products) || !vres || vres.error) { box.innerHTML = `<div style="font-size:12px;color:#dc2626">${window.dpErrorText ? window.dpErrorText(vres && vres.error_code, vres && vres.error) : t('操作失败,请重试')}</div>`; return }
|
|
28
|
+
const byProduct = {}; for (const v of (vres.verifications || [])) { if (!byProduct[v.product_id]) byProduct[v.product_id] = v } // 最新在前
|
|
29
|
+
const copy = window.dpPvCopy()
|
|
30
|
+
if (!products.length) { box.innerHTML = `<div style="font-size:12px;color:#9ca3af">${t('你还没有商品')}</div>`; return }
|
|
31
|
+
box.innerHTML = products.map(p => {
|
|
32
|
+
const v = byProduct[p.id]
|
|
33
|
+
const meta = v ? (copy[v.status] || { icon: 'ℹ️', text: v.status }) : null
|
|
34
|
+
let action = ''
|
|
35
|
+
if (!v || v.status === 'rejected') {
|
|
36
|
+
action = `<button onclick="window.dpRequestProductVerify('${p.id}')" style="padding:6px 12px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer">${t('申请验证')}</button>`
|
|
37
|
+
} else if (v.status === 'issued') {
|
|
38
|
+
action = `
|
|
39
|
+
<div style="font-size:12px;color:#374151;margin-bottom:6px">${t('验证码')}: <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-weight:700">${escHtml(v.code)}</code> · ${t('把它展示在该商品的外部页面')}</div>
|
|
40
|
+
<input id="dp-pv-url-${p.id}" type="url" placeholder="${t('该商品的外部链接(http/https)')}" style="width:100%;padding:6px;border:1px solid #d1d5db;border-radius:4px;font-size:12px;margin-bottom:6px">
|
|
41
|
+
<button onclick="window.dpSubmitProductVerify('${p.id}')" style="padding:6px 12px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer">${t('提交链接')}</button>`
|
|
42
|
+
}
|
|
43
|
+
return `<div style="padding:8px 0;border-top:1px solid #f3f4f6">
|
|
44
|
+
<div style="font-size:13px;font-weight:600;color:#374151">${escHtml(p.title || p.id)}</div>
|
|
45
|
+
${meta ? `<div style="font-size:12px;color:#6b7280;margin:4px 0">${meta.icon} ${escHtml(meta.text)}</div>` : ''}
|
|
46
|
+
${action}
|
|
47
|
+
</div>`
|
|
48
|
+
}).join('')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
window.dpRequestProductVerify = async (productId) => {
|
|
52
|
+
const r = await POST('/direct-receive/product-verification', { product_id: productId })
|
|
53
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
54
|
+
if (typeof toast$ === 'function') toast$(t('验证码已签发'))
|
|
55
|
+
window.dpHydrateSellerProductVerify()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
window.dpSubmitProductVerify = async (productId) => {
|
|
59
|
+
const el = document.getElementById('dp-pv-url-' + productId)
|
|
60
|
+
const url = el && el.value ? el.value.trim() : ''
|
|
61
|
+
if (!url) { if (typeof toast$ === 'function') toast$(t('请填写该商品的外部链接'), 'error'); return }
|
|
62
|
+
const r = await PUT('/direct-receive/product-verification', { product_id: productId, external_url: url })
|
|
63
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
64
|
+
if (typeof toast$ === 'function') toast$(t('已提交,等待管理员核验'))
|
|
65
|
+
window.dpHydrateSellerProductVerify()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ══════ ADMIN 审核队列 ══════
|
|
69
|
+
window.renderAdminProductVerifications = async function (app) {
|
|
70
|
+
if (!state.user) { renderLogin(); return }
|
|
71
|
+
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
72
|
+
app.innerHTML = shell(loading$(), 'admin')
|
|
73
|
+
const data = await GET('/admin/direct-receive/product-verifications?status=submitted')
|
|
74
|
+
if (!data || data.error) { app.innerHTML = shell(alert$('error', (data && data.error) || t('加载失败')), 'admin'); return }
|
|
75
|
+
const rows = data.verifications || []
|
|
76
|
+
const list = rows.length ? rows.map(v => `
|
|
77
|
+
<div class="card" style="margin-bottom:10px">
|
|
78
|
+
<div style="font-size:11px;color:#9ca3af">${escHtml(v.seller_id)} · ${escHtml(v.product_id)} · ${fmtTime(v.created_at)}</div>
|
|
79
|
+
<div style="font-size:13px;margin-top:6px">${t('验证码')}: <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-weight:700">${escHtml(v.code)}</code>${v.platform ? ` · ${escHtml(v.platform)}` : ''}</div>
|
|
80
|
+
<div style="font-size:12px;margin-top:4px">${t('外部链接')}: ${v.external_url ? `<a href="${encodeURI(v.external_url)}" target="_blank" rel="noopener noreferrer nofollow" style="color:#2563eb;text-decoration:underline;word-break:break-all">${escHtml(v.external_url)}</a>` : '—'}</div>
|
|
81
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:4px">${t('请手动打开链接,确认该商品页面上展示了上面的验证码,再决定通过/拒绝。')}</div>
|
|
82
|
+
<div style="display:flex;gap:6px;margin-top:10px">
|
|
83
|
+
<button class="btn btn-primary btn-sm" style="font-size:12px" onclick="window.doReviewProductVerify('${v.id}','verified')">✓ ${t('通过(真人 Passkey)')}</button>
|
|
84
|
+
<button class="btn btn-sm" style="font-size:12px;background:#dc2626;border-color:#dc2626;color:#fff" onclick="window.doReviewProductVerify('${v.id}','rejected')">✗ ${t('拒绝(真人 Passkey)')}</button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>`).join('') : `<div class="empty"><div class="empty-icon">📥</div><div class="empty-text">${t('暂无待核验商品')}</div></div>`
|
|
87
|
+
app.innerHTML = shell(`
|
|
88
|
+
<h1 class="page-title">🔖 ${t('逐产品直付验证审核')}</h1>
|
|
89
|
+
<div style="margin-bottom:12px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin')">${t('返回概览')}</button></div>
|
|
90
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('每个商品需单独核验:打开卖家提交的外部链接,确认页面展示了验证码再通过。通过=该商品可直付(逐品,绝不放行全部)。通过/拒绝均需真人 Passkey。')}</div>
|
|
91
|
+
${list}
|
|
92
|
+
`, 'admin')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// purpose_data 绑 verification_id + decision(签 A 用 B / 改结论一律拒)。
|
|
96
|
+
window.doReviewProductVerify = async function (id, decision) {
|
|
97
|
+
const token = await requestPasskeyGate('direct_pay_product_verify', { verification_id: id, decision })
|
|
98
|
+
if (!token) { if (typeof toast$ === 'function') toast$(t('需要真人 Passkey 确认'), 'error'); return }
|
|
99
|
+
const r = await POST(`/admin/direct-receive/product-verifications/${id}/review`, { decision, webauthn_token: token })
|
|
100
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
101
|
+
if (typeof toast$ === 'function') toast$(decision === 'verified' ? t('该商品已通过验证') : t('该商品已拒绝'))
|
|
102
|
+
window.renderAdminProductVerifications(document.getElementById('app'))
|
|
103
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — SELLER de-identified readiness panel (PR readiness-seller-ui)。UI ONLY。
|
|
2
|
+
// 只渲染【卖家可行动 / 脱敏状态】项(收款说明 / Passkey / 履约保证金 / 商户审核 / 暂停 / 平台开放)。
|
|
3
|
+
// 数据来自 GET /api/direct-receive/readiness(后端已脱敏:绝不下发 raw blocker、也绝不暴露 KYB/制裁/AML 分项)。
|
|
4
|
+
// 前端只做 code→卖家可读双语文案 的映射,绝不展示原始内部判定码。Direct Pay 仍 non-launchable / fail-closed。
|
|
5
|
+
// 不碰 wallet/escrow/settlement/refund/钱路;不开真实 rail;不声称已上线。买家侧不展示本面板。
|
|
6
|
+
|
|
7
|
+
// 内部 code → 卖家安全双语文案(ok / 未达成 两态 + 可行动入口)。绝不出现 KYB/sanctions/AML 字样。
|
|
8
|
+
window.dpSellerReadinessCopy = () => ({
|
|
9
|
+
PLATFORM_OPEN: { ok: t('平台侧直付已开放'), no: t('直付平台侧暂未开放(无需你操作)') },
|
|
10
|
+
PAYMENT_INSTRUCTION: { ok: t('已设置收款说明'), no: t('未设置收款说明'), action: t('去设置收款说明'), href: '#seller/settings' },
|
|
11
|
+
PASSKEY: { ok: t('已注册 Passkey'), no: t('未注册 Passkey'), action: t('去注册 Passkey'), href: '#me' },
|
|
12
|
+
BASE_BOND: { ok: t('履约保证金已完成'), no: t('履约保证金未完成') },
|
|
13
|
+
COMPLIANCE_REVIEW: { ok: t('商户审核已通过'), no: t('商户审核进行中或未通过') },
|
|
14
|
+
NOT_SUSPENDED: { ok: t('直付资格正常'), no: t('直付资格已被暂停') },
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
window.dpSellerReadinessSection = () => `
|
|
18
|
+
<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">
|
|
19
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:4px">🧭 ${t('直付开通进度(仅你可见)')}</div>
|
|
20
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('以上为你的直付开通进度;直付按轨道分阶段开放,完成可行动项不代表立即可用。')}</div>
|
|
21
|
+
<div id="dp-seller-readiness">${loading$()}</div>
|
|
22
|
+
</div>`
|
|
23
|
+
|
|
24
|
+
window.dpHydrateSellerReadiness = async () => {
|
|
25
|
+
const box = document.getElementById('dp-seller-readiness')
|
|
26
|
+
if (!box) return
|
|
27
|
+
const r = await GET('/direct-receive/readiness')
|
|
28
|
+
if (!r || r.error) { box.innerHTML = `<div style="font-size:12px;color:#dc2626">${window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试')}</div>`; return }
|
|
29
|
+
const copy = window.dpSellerReadinessCopy()
|
|
30
|
+
const rows = (r.items || []).map(it => {
|
|
31
|
+
const c = copy[it.code]; if (!c) return ''
|
|
32
|
+
const icon = it.ok ? '✅' : (it.actionable ? '⚠️' : '⏳')
|
|
33
|
+
const text = it.ok ? c.ok : c.no
|
|
34
|
+
const act = (!it.ok && it.actionable && c.action) ? ` · <a href="${c.href}" style="color:#2563eb;font-weight:600;text-decoration:underline">${c.action}</a>` : ''
|
|
35
|
+
return `<li style="display:flex;gap:8px;align-items:flex-start;font-size:13px;line-height:1.7;color:#374151"><span>${icon}</span><span>${escHtml(text)}${act}</span></li>`
|
|
36
|
+
}).join('')
|
|
37
|
+
box.innerHTML = `<ul style="margin:0;padding-left:2px;list-style:none">${rows}</ul>`
|
|
38
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — 店铺认证 UI (PR-⑤b) = 逐品验证的【豁免】路径。UI ONLY。两个面:
|
|
2
|
+
// ① SELLER 自助(seller workbench settings):申领店铺验证码 → 贴到外部店铺页 → 提交店铺链接 → 看状态(含是否已豁免)。
|
|
3
|
+
// ② ADMIN 审核队列(#admin/store-verifications):打开卖家提交的店铺链接手动核对验证码,verify/reject;verify 时可【勾选
|
|
4
|
+
// “免逐品验证”】(per_product_exempt)→ 该卖家所有商品免逐品、可直付。ROOT + 真人 Passkey。
|
|
5
|
+
// 诚实:WebAZ 不抓取链接,admin 手动核。不碰 wallet/escrow/settlement/refund;只调既有受门控端点。
|
|
6
|
+
|
|
7
|
+
window.dpSvCopy = () => ({
|
|
8
|
+
issued: { icon: '①', text: t('已签发验证码,请贴到你的外部店铺页后提交店铺链接') },
|
|
9
|
+
submitted: { icon: '⏳', text: t('已提交,等待管理员核验') },
|
|
10
|
+
verified: { icon: '✅', text: t('店铺已通过验证') },
|
|
11
|
+
rejected: { icon: '🚫', text: t('未通过,请修正后重新申请') },
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// ══════ SELLER 自助面板 ══════
|
|
15
|
+
window.dpSellerStoreVerifySection = () => `
|
|
16
|
+
<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">
|
|
17
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:4px">🏬 ${t('店铺认证(可申请免逐品验证)')}</div>
|
|
18
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('默认每个商品需单独验证才能直付。你也可以申请【店铺认证】:申领验证码 → 贴到你的外部店铺主页 → 提交店铺链接 → 管理员手动核对;通过且管理员勾选豁免后,你【所有商品】免逐品验证即可直付。')}</div>
|
|
19
|
+
<div id="dp-seller-sv">${loading$()}</div>
|
|
20
|
+
</div>`
|
|
21
|
+
|
|
22
|
+
window.dpHydrateSellerStoreVerify = async () => {
|
|
23
|
+
const box = document.getElementById('dp-seller-sv')
|
|
24
|
+
if (!box) return
|
|
25
|
+
const r = await GET('/direct-receive/store-verification')
|
|
26
|
+
if (!r || r.error) { box.innerHTML = `<div style="font-size:12px;color:#dc2626">${window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试')}</div>`; return }
|
|
27
|
+
const v = r.verification, copy = window.dpSvCopy()
|
|
28
|
+
if (!v || v.status === 'rejected') {
|
|
29
|
+
const prior = v ? `<div style="font-size:12px;color:#6b7280;margin-bottom:8px">${copy.rejected.icon} ${escHtml(copy.rejected.text)}</div>` : ''
|
|
30
|
+
box.innerHTML = prior + `<button onclick="window.dpRequestStoreVerify()" style="padding:8px 16px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer">${t('申请店铺认证')}</button>`
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
const meta = copy[v.status] || { icon: 'ℹ️', text: v.status }
|
|
34
|
+
const lines = [`<div style="font-size:13px;font-weight:600;color:#374151">${meta.icon} ${escHtml(meta.text)}</div>`]
|
|
35
|
+
if (v.status === 'issued') {
|
|
36
|
+
lines.push(`<div style="font-size:12px;color:#374151;margin:6px 0">${t('验证码')}: <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-weight:700">${escHtml(v.code)}</code> · ${t('把它展示在你的外部店铺主页')}</div>`)
|
|
37
|
+
lines.push(`<input id="dp-sv-url" type="url" placeholder="${t('你的外部店铺链接(http/https)')}" style="width:100%;padding:6px;border:1px solid #d1d5db;border-radius:4px;font-size:12px;margin-bottom:6px">`)
|
|
38
|
+
lines.push(`<button onclick="window.dpSubmitStoreVerify()" style="padding:6px 12px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer">${t('提交店铺链接')}</button>`)
|
|
39
|
+
} else if (v.status === 'verified') {
|
|
40
|
+
lines.push(`<div style="font-size:12px;color:${r.exempt ? '#166534' : '#6b7280'};margin-top:4px">${r.exempt ? '✅ ' + t('已豁免逐品验证:你的所有商品可直付') : t('已通过,但未获逐品豁免:商品仍需逐个验证')}</div>`)
|
|
41
|
+
}
|
|
42
|
+
box.innerHTML = `<div style="display:flex;flex-direction:column;gap:2px">${lines.join('')}</div>`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
window.dpRequestStoreVerify = async () => {
|
|
46
|
+
const r = await POST('/direct-receive/store-verification', {})
|
|
47
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
48
|
+
if (typeof toast$ === 'function') toast$(t('验证码已签发'))
|
|
49
|
+
window.dpHydrateSellerStoreVerify()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
window.dpSubmitStoreVerify = async () => {
|
|
53
|
+
const el = document.getElementById('dp-sv-url')
|
|
54
|
+
const url = el && el.value ? el.value.trim() : ''
|
|
55
|
+
if (!url) { if (typeof toast$ === 'function') toast$(t('请填写你的外部店铺链接'), 'error'); return }
|
|
56
|
+
const r = await PUT('/direct-receive/store-verification', { external_url: url })
|
|
57
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
58
|
+
if (typeof toast$ === 'function') toast$(t('已提交,等待管理员核验'))
|
|
59
|
+
window.dpHydrateSellerStoreVerify()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ══════ ADMIN 审核队列 ══════
|
|
63
|
+
window.renderAdminStoreVerifications = async function (app) {
|
|
64
|
+
if (!state.user) { renderLogin(); return }
|
|
65
|
+
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
66
|
+
app.innerHTML = shell(loading$(), 'admin')
|
|
67
|
+
const data = await GET('/admin/direct-receive/store-verifications?status=submitted')
|
|
68
|
+
if (!data || data.error) { app.innerHTML = shell(alert$('error', (data && data.error) || t('加载失败')), 'admin'); return }
|
|
69
|
+
const rows = data.verifications || []
|
|
70
|
+
const list = rows.length ? rows.map(v => `
|
|
71
|
+
<div class="card" style="margin-bottom:10px">
|
|
72
|
+
<div style="font-size:11px;color:#9ca3af">${escHtml(v.user_id)} · ${fmtTime(v.created_at)}</div>
|
|
73
|
+
<div style="font-size:13px;margin-top:6px">${t('验证码')}: <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-weight:700">${escHtml(v.code)}</code>${v.platform ? ` · ${escHtml(v.platform)}` : ''}</div>
|
|
74
|
+
<div style="font-size:12px;margin-top:4px">${t('店铺链接')}: ${v.external_url ? `<a href="${encodeURI(v.external_url)}" target="_blank" rel="noopener noreferrer nofollow" style="color:#2563eb;text-decoration:underline;word-break:break-all">${escHtml(v.external_url)}</a>` : '—'}</div>
|
|
75
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:4px">${t('请手动打开链接,确认店铺主页展示了上面的验证码,再决定通过/拒绝。')}</div>
|
|
76
|
+
<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:#374151;margin-top:8px"><input type="checkbox" id="sv-exempt-${v.id}"> ${t('免逐品验证(通过后该卖家所有商品可直付)')}</label>
|
|
77
|
+
<div style="display:flex;gap:6px;margin-top:8px">
|
|
78
|
+
<button class="btn btn-primary btn-sm" style="font-size:12px" onclick="window.doReviewStoreVerify('${v.id}','verified')">✓ ${t('通过(真人 Passkey)')}</button>
|
|
79
|
+
<button class="btn btn-sm" style="font-size:12px;background:#dc2626;border-color:#dc2626;color:#fff" onclick="window.doReviewStoreVerify('${v.id}','rejected')">✗ ${t('拒绝(真人 Passkey)')}</button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>`).join('') : `<div class="empty"><div class="empty-icon">📥</div><div class="empty-text">${t('暂无待核验店铺')}</div></div>`
|
|
82
|
+
app.innerHTML = shell(`
|
|
83
|
+
<h1 class="page-title">🏬 ${t('店铺认证审核')}</h1>
|
|
84
|
+
<div style="margin-bottom:12px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin')">${t('返回概览')}</button></div>
|
|
85
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('打开卖家提交的店铺链接,确认店铺页展示了验证码再通过。勾选“免逐品验证”=该卖家所有商品免逐个验证即可直付(请仅对可信商户勾选)。通过/拒绝均需真人 Passkey。')}</div>
|
|
86
|
+
${list}
|
|
87
|
+
`, 'admin')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// purpose_data 绑 verification_id + decision + per_product_exempt(改任一项一律拒)。
|
|
91
|
+
window.doReviewStoreVerify = async function (id, decision) {
|
|
92
|
+
const exempt = decision === 'verified' && !!(document.getElementById('sv-exempt-' + id) || {}).checked
|
|
93
|
+
const body = { verification_id: id, decision, per_product_exempt: exempt }
|
|
94
|
+
const token = await requestPasskeyGate('direct_pay_store_verify', body)
|
|
95
|
+
if (!token) { if (typeof toast$ === 'function') toast$(t('需要真人 Passkey 确认'), 'error'); return }
|
|
96
|
+
const r = await POST(`/admin/direct-receive/store-verifications/${id}/review`, { decision, per_product_exempt: exempt, webauthn_token: token })
|
|
97
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
98
|
+
if (typeof toast$ === 'function') toast$(decision === 'verified' ? (r.per_product_exempt ? t('店铺已通过,已豁免逐品验证') : t('店铺已通过')) : t('店铺申请已拒绝'))
|
|
99
|
+
window.renderAdminStoreVerifications(document.getElementById('app'))
|
|
100
|
+
}
|