@seasonkoh/webaz 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/README.zh-CN.md +7 -6
- package/dist/bond-refund-blockers.js +50 -0
- package/dist/bond-slash.js +100 -0
- package/dist/bond-terms.js +31 -0
- 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 +99 -0
- package/dist/direct-pay-cancel-refund.js +160 -0
- package/dist/direct-pay-compliance-ingress.js +145 -0
- package/dist/direct-pay-controls.js +136 -0
- package/dist/direct-pay-create.js +231 -0
- package/dist/direct-pay-deferral-quota.js +43 -0
- package/dist/direct-pay-disclosures.js +50 -0
- package/dist/direct-pay-eligibility.js +46 -0
- package/dist/direct-pay-fee-ar.js +241 -0
- package/dist/direct-pay-fee-prepay-request.js +80 -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-pay-returns.js +104 -0
- package/dist/direct-pay-stock.js +9 -0
- package/dist/direct-receive-account-qr.js +77 -0
- package/dist/direct-receive-accounts.js +82 -0
- package/dist/direct-receive-deferral.js +171 -0
- package/dist/direct-receive-deposits.js +365 -0
- package/dist/direct-receive-payment-instruction.js +26 -0
- package/dist/free-shipping.js +37 -0
- package/dist/fx-rates.js +78 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +777 -1
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +114 -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 +99 -32
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
- package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
- 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/platform-receive-accounts.js +94 -0
- package/dist/product-verification.js +93 -0
- package/dist/pwa/acp-feed.js +3 -2
- package/dist/pwa/arbitration-read-admin.js +37 -0
- package/dist/pwa/arbitrator-lifecycle.js +100 -0
- package/dist/pwa/contract-fingerprint.js +18 -0
- package/dist/pwa/direct-pay-guards.js +20 -0
- package/dist/pwa/direct-pay-order-redaction.js +42 -0
- package/dist/pwa/endpoint-actions.js +3 -0
- package/dist/pwa/human-presence.js +2 -6
- package/dist/pwa/public/app-account.js +9 -9
- package/dist/pwa/public/app-admin-disputes.js +55 -0
- package/dist/pwa/public/app-agent-appeal.js +90 -0
- package/dist/pwa/public/app-agent-approvals.js +93 -0
- package/dist/pwa/public/app-agent-pair.js +127 -0
- package/dist/pwa/public/app-ai.js +10 -10
- package/dist/pwa/public/app-arbitrator-admin.js +87 -0
- package/dist/pwa/public/app-arbitrator-entry.js +9 -0
- package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
- package/dist/pwa/public/app-bond-refund-ui.js +66 -0
- package/dist/pwa/public/app-bond-slash-ui.js +74 -0
- package/dist/pwa/public/app-bond-terms-ui.js +23 -0
- package/dist/pwa/public/app-bond-ui.js +108 -0
- package/dist/pwa/public/app-chat-poll.js +29 -0
- package/dist/pwa/public/app-contribution-hub.js +23 -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-cancel-refund.js +72 -0
- package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
- package/dist/pwa/public/app-direct-pay-copy.js +12 -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-history.js +39 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
- package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
- package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
- package/dist/pwa/public/app-direct-pay-memo.js +14 -0
- package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
- package/dist/pwa/public/app-direct-pay-pay.js +15 -0
- package/dist/pwa/public/app-direct-pay-paymodal.js +32 -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-reconcile.js +19 -0
- package/dist/pwa/public/app-direct-pay-returns.js +56 -0
- package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
- package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
- package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
- package/dist/pwa/public/app-direct-pay.js +226 -0
- package/dist/pwa/public/app-discover.js +20 -20
- package/dist/pwa/public/app-dispute-close-ui.js +38 -0
- package/dist/pwa/public/app-external-links.js +32 -0
- package/dist/pwa/public/app-free-shipping-ui.js +29 -0
- package/dist/pwa/public/app-gmv-rail-split.js +12 -0
- package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
- package/dist/pwa/public/app-listings.js +4 -4
- package/dist/pwa/public/app-mutual-cancel.js +54 -0
- package/dist/pwa/public/app-notif-templates-orders.js +43 -0
- package/dist/pwa/public/app-notif-templates.js +22 -0
- package/dist/pwa/public/app-order-accept-ui.js +158 -0
- package/dist/pwa/public/app-order-errors.js +50 -0
- package/dist/pwa/public/app-order-labels.js +19 -0
- package/dist/pwa/public/app-order-rail-filter.js +26 -0
- package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
- package/dist/pwa/public/app-poll-governor.js +22 -0
- 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 +10 -10
- package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
- package/dist/pwa/public/app-sale-regions-ui.js +38 -0
- package/dist/pwa/public/app-seller.js +5 -5
- package/dist/pwa/public/app-shop.js +19 -19
- package/dist/pwa/public/app-trade-tax-ui.js +33 -0
- package/dist/pwa/public/app.js +264 -295
- package/dist/pwa/public/i18n.js +1068 -197
- package/dist/pwa/public/index.html +58 -0
- package/dist/pwa/public/openapi.json +1845 -653
- package/dist/pwa/public/style.css +3 -0
- package/dist/pwa/routes/admin-analytics.js +4 -2
- package/dist/pwa/routes/admin-direct-receive-deposits.js +533 -0
- package/dist/pwa/routes/admin-protocol-params.js +16 -0
- package/dist/pwa/routes/admin-reports.js +31 -5
- package/dist/pwa/routes/agent-governance.js +1 -1
- package/dist/pwa/routes/agent-grants.js +253 -32
- package/dist/pwa/routes/analytics.js +2 -0
- package/dist/pwa/routes/arbitrator.js +67 -14
- package/dist/pwa/routes/bond-seller.js +162 -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-cancel-refund.js +111 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +79 -0
- package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
- package/dist/pwa/routes/direct-pay-returns.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +248 -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/disputes-read.js +20 -6
- package/dist/pwa/routes/disputes-write.js +91 -33
- package/dist/pwa/routes/external-anchors.js +4 -2
- package/dist/pwa/routes/fee-prepay-requests.js +66 -0
- package/dist/pwa/routes/fx.js +12 -0
- package/dist/pwa/routes/governance-onboarding.js +46 -8
- package/dist/pwa/routes/leaderboard.js +47 -9
- package/dist/pwa/routes/listings.js +2 -2
- package/dist/pwa/routes/logistics.js +6 -2
- package/dist/pwa/routes/manifests.js +38 -0
- package/dist/pwa/routes/me-data.js +5 -2
- package/dist/pwa/routes/mutual-cancel.js +62 -0
- package/dist/pwa/routes/orders-action.js +297 -16
- package/dist/pwa/routes/orders-create.js +21 -6
- package/dist/pwa/routes/orders-read.js +106 -8
- package/dist/pwa/routes/p2p-products.js +2 -2
- package/dist/pwa/routes/platform-receive-accounts.js +111 -0
- package/dist/pwa/routes/products-create.js +15 -4
- 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 +37 -3
- package/dist/pwa/routes/profile-identity.js +4 -2
- package/dist/pwa/routes/promoter.js +3 -0
- package/dist/pwa/routes/referral.js +4 -0
- package/dist/pwa/routes/returns.js +60 -4
- 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/seller-directpay-report.js +110 -0
- package/dist/pwa/routes/seller-quota.js +5 -1
- package/dist/pwa/routes/shipping-templates.js +219 -0
- package/dist/pwa/routes/shops.js +2 -1
- package/dist/pwa/routes/snf.js +4 -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 +3 -3
- package/dist/pwa/server.js +112 -132
- package/dist/runtime/agent-grant-scopes.js +69 -1
- package/dist/runtime/webaz-schema-helpers.js +161 -3
- package/dist/sale-regions.js +116 -0
- package/dist/shipping-templates.js +119 -0
- package/dist/store-verification.js +77 -0
- package/dist/trade-tax.js +99 -0
- package/dist/trade-terms.js +76 -0
- package/dist/version.js +1 -1
- package/package.json +128 -2
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { openDeposit, getSellerLatestDeposit, expireDeposit, requiredBondUnits, cancelPendingDeposit, requestBondRefund, cancelBondRefundRequest, DIRECT_PAY_BOND_JURISDICTIONS } from '../../direct-receive-deposits.js';
|
|
2
|
+
import { enumerateBondRefundBlockers } from '../../bond-refund-blockers.js'; // B2:§5 unlock blockers(fail-closed)
|
|
3
|
+
import { listBondSlashProposals } from '../../bond-slash.js'; // B3:待复核罚没提案(卖家须被告知)
|
|
4
|
+
import { BOND_TERMS } from '../../bond-terms.js'; // 条款(缴纳前强制同意;版本快照进 deposit 行)
|
|
5
|
+
import { getPlatformAccount } from '../../platform-receive-accounts.js';
|
|
6
|
+
import { bondRailClearanceBlockers, isBondRailClearedForProduction } from '../../direct-pay-bond-rail-clearance.js';
|
|
7
|
+
import { getActiveDeferral } from '../../direct-receive-deferral.js';
|
|
8
|
+
import { listActivePlatformAccounts } from '../../platform-receive-accounts.js';
|
|
9
|
+
import { toDecimal } from '../../money.js';
|
|
10
|
+
import { createNotification } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
11
|
+
import { dbAll } from '../../layer0-foundation/L0-1-database/db.js';
|
|
12
|
+
/** operator_attested 生产轨是否已放行 —— 按【平台收款主体法域】(DIRECT_PAY_BOND_JURISDICTIONS,非卖家法域;
|
|
13
|
+
* 卖家侧资格由 KYB/制裁/AML 门独立把守)判 Lock A+B 全过。2026-07-05 起 SG 放行(#240 决策 B)。 */
|
|
14
|
+
function bondRailCleared() {
|
|
15
|
+
return DIRECT_PAY_BOND_JURISDICTIONS.some(j => isBondRailClearedForProduction('operator_attested', j));
|
|
16
|
+
}
|
|
17
|
+
export function registerBondSellerRoutes(app, deps) {
|
|
18
|
+
const { db, auth, generateId, errorRes, getProtocolParam } = deps;
|
|
19
|
+
const coolingDays = () => Math.max(0, Number(getProtocolParam('direct_pay.bond_refund_cooling_days', 14)) || 14);
|
|
20
|
+
function requireSeller(req, res) {
|
|
21
|
+
const user = auth(req, res);
|
|
22
|
+
if (!user)
|
|
23
|
+
return null;
|
|
24
|
+
if (user.role !== 'seller') {
|
|
25
|
+
errorRes(res, 403, 'SELLER_ONLY', '仅卖家有履约保证金');
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return user;
|
|
29
|
+
}
|
|
30
|
+
app.get('/api/direct-receive/bond-status', (req, res) => {
|
|
31
|
+
const user = requireSeller(req, res);
|
|
32
|
+
if (!user)
|
|
33
|
+
return;
|
|
34
|
+
const sellerId = user.id;
|
|
35
|
+
// lazy expiry:pending 超 TTL 顺手过期(无 cron 依赖)
|
|
36
|
+
const latest0 = getSellerLatestDeposit(db, sellerId);
|
|
37
|
+
if (latest0 && latest0.status === 'pending')
|
|
38
|
+
expireDeposit(db, { depositId: latest0.id, nowIso: new Date().toISOString() });
|
|
39
|
+
const latest = getSellerLatestDeposit(db, sellerId);
|
|
40
|
+
const deferral = getActiveDeferral(db, sellerId, new Date().toISOString());
|
|
41
|
+
const cleared = bondRailCleared();
|
|
42
|
+
return void res.json({
|
|
43
|
+
required: { tier: 'T0', units: requiredBondUnits('T0'), display: toDecimal(requiredBondUnits('T0')), currency: 'USDC', note: '固定 token 数(档位参考 ≈ S$500,非汇率换算);治理可调' },
|
|
44
|
+
deposit: latest ? {
|
|
45
|
+
id: latest.id, status: latest.status, amount: latest.amount, required_amount: latest.required_amount,
|
|
46
|
+
rail: latest.deposit_rail, evidence_ref: latest.external_ref, reject_note: latest.reject_note,
|
|
47
|
+
production_confirmed: latest.production_receipt_confirmed_at != null, refund_evidence_ref: latest.refund_evidence_ref,
|
|
48
|
+
created_at: latest.created_at, locked_at: latest.locked_at,
|
|
49
|
+
} : null,
|
|
50
|
+
deferral: deferral ? { id: deferral.id, expires_at: deferral.expiresAt, grace_until: deferral.graceUntil, in_grace: deferral.inGrace, reduced_quota_factor: deferral.reducedQuotaFactor } : null,
|
|
51
|
+
// B2:退出退还视图 —— locked 时预览 blockers(能不能申请);refunding 时给冷静期/可执行时间
|
|
52
|
+
refund: latest && latest.status === 'locked' ? { can_request: enumerateBondRefundBlockers(db, sellerId).length === 0, blockers: enumerateBondRefundBlockers(db, sellerId), cooling_days: coolingDays() }
|
|
53
|
+
: latest && latest.status === 'refunding' ? { requested_at: latest.refund_requested_at, cooling_days: coolingDays() } : null,
|
|
54
|
+
// B3:待复核罚没提案(冷静期=卖家申诉窗,必须让卖家看见)
|
|
55
|
+
pending_slash: (() => { try {
|
|
56
|
+
const p = listBondSlashProposals(db, { sellerId, status: 'proposed' })[0];
|
|
57
|
+
return p ? { id: p.id, dispute_id: p.dispute_id, cooling_until: p.cooling_until, reason: p.reason } : null;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
} })(),
|
|
62
|
+
rail_cleared: cleared,
|
|
63
|
+
rail_blockers: cleared ? [] : bondRailClearanceBlockers('operator_attested').filter(b => b !== 'NO_PRODUCTION_RECEIPT'),
|
|
64
|
+
payment_accounts: cleared ? listActivePlatformAccounts(db) : [], // 放行前不展示收款方式(不引导无法生效的转账);多币种账户由 admin 在 #admin/platform-receive 维护
|
|
65
|
+
terms: { version: BOND_TERMS.version, zh: BOND_TERMS.zh, en: BOND_TERMS.en }, // 缴纳前必须同意(版本快照)
|
|
66
|
+
note: cleared
|
|
67
|
+
? '按平台收款方式转账后提交申报(凭据必填);运营核实到账并确认后保证金正式锁定、直付入场门即满足。'
|
|
68
|
+
: '保证金缴纳通道待平台放行(合规审查中)。当前可通过【缓交申请】先行入场(额度受限);放行后再补缴转正式。',
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
app.post('/api/direct-receive/bond-deposit', async (req, res) => {
|
|
72
|
+
const user = requireSeller(req, res);
|
|
73
|
+
if (!user)
|
|
74
|
+
return;
|
|
75
|
+
const sellerId = user.id;
|
|
76
|
+
// fail-closed:Lock B 未放行 → 不收申报(收了也无法核实生效,只会积压误导)
|
|
77
|
+
if (!bondRailCleared())
|
|
78
|
+
return void errorRes(res, 409, 'BOND_RAIL_NOT_CLEARED', '保证金缴纳通道待平台放行(合规审查中);当前请使用缓交申请入场');
|
|
79
|
+
const evidence = String(req.body?.evidence_ref ?? '').trim();
|
|
80
|
+
if (!evidence || evidence.length > 120)
|
|
81
|
+
return void errorRes(res, 400, 'EVIDENCE_REQUIRED', '付款凭据号必填(≤120 字;转账单号/链上 tx 等,运营据此核对到账)');
|
|
82
|
+
// 条款强制同意(合同依据;版本精确匹配,旧版本同意无效)
|
|
83
|
+
if (String(req.body?.agree_terms_version ?? '') !== BOND_TERMS.version) {
|
|
84
|
+
return void res.status(428).json({ error: '缴纳前须阅读并同意当前版本保证金条款', error_code: 'TERMS_NOT_AGREED', terms: { version: BOND_TERMS.version, zh: BOND_TERMS.zh, en: BOND_TERMS.en } });
|
|
85
|
+
}
|
|
86
|
+
// 多币种:必须选一个 active 平台收款账户;币种由账户推导(USDC/USDT→usdc,其余→fiat)
|
|
87
|
+
const acc = getPlatformAccount(db, String(req.body?.platform_account_id ?? ''));
|
|
88
|
+
if (!acc || acc.status !== 'active')
|
|
89
|
+
return void errorRes(res, 400, 'PLATFORM_ACCOUNT_REQUIRED', '须选择缴纳到的平台收款账户(有效且启用)');
|
|
90
|
+
const currency = ['USDC', 'USDT'].includes(String(acc.currency ?? '').toUpperCase()) ? 'usdc' : 'fiat';
|
|
91
|
+
const latest0 = getSellerLatestDeposit(db, sellerId);
|
|
92
|
+
if (latest0 && latest0.status === 'pending')
|
|
93
|
+
expireDeposit(db, { depositId: latest0.id, nowIso: new Date().toISOString() }); // lazy expiry
|
|
94
|
+
const latest = getSellerLatestDeposit(db, sellerId);
|
|
95
|
+
if (latest && ['pending', 'confirmed', 'insufficient'].includes(latest.status))
|
|
96
|
+
return void errorRes(res, 409, 'DEPOSIT_IN_FLIGHT', `已有在途申报(${latest.status}),请等待运营核实或先撤回`);
|
|
97
|
+
if (latest && latest.status === 'locked')
|
|
98
|
+
return void errorRes(res, 409, 'BOND_ALREADY_LOCKED', '保证金已锁定生效,无需重复缴纳');
|
|
99
|
+
const depositId = generateId('bond');
|
|
100
|
+
const r = openDeposit(db, { depositId, userId: sellerId, tier: 'T0', currency, depositRail: 'operator_attested', externalRef: evidence, termsVersion: BOND_TERMS.version, platformAccountId: acc.id });
|
|
101
|
+
if (!r.ok)
|
|
102
|
+
return void errorRes(res, 400, 'BOND_OPEN_FAILED', r.reason);
|
|
103
|
+
// N3 同款:通知 root admin 有待核实申报(best-effort)
|
|
104
|
+
try {
|
|
105
|
+
const roots = await dbAll("SELECT id FROM users WHERE role = 'admin' AND (admin_type = 'root' OR admin_type IS NULL)");
|
|
106
|
+
for (const a of roots) {
|
|
107
|
+
createNotification(db, a.id, null, 'bond_deposit_submitted', '🏦 新保证金缴纳申报待核实', `卖家 ${String(user.name ?? sellerId)} 申报已缴纳履约保证金(T0,凭据 ${evidence.slice(0, 40)})。请核对真实到账后在 admin 后台确认(ROOT+Passkey)。`, { templateKey: 'bond_deposit_submitted', params: { seller: String(user.name ?? sellerId), evidence: evidence.slice(0, 40) } });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
console.warn('[bond-deposit notify]', e.message);
|
|
112
|
+
}
|
|
113
|
+
return void res.json({ success: true, deposit_id: depositId, status: 'pending', note: '申报已提交;运营核实真实到账并确认后生效。申报本身不授予任何入场资格。' });
|
|
114
|
+
});
|
|
115
|
+
// ── B2:退出退还 —— 申请(§5 blockers fail-closed)→ 冷静期 → admin 执行;申请期间直付资格暂停,可撤销 ──
|
|
116
|
+
app.post('/api/direct-receive/bond-refund-request', async (req, res) => {
|
|
117
|
+
const user = requireSeller(req, res);
|
|
118
|
+
if (!user)
|
|
119
|
+
return;
|
|
120
|
+
const sellerId = user.id;
|
|
121
|
+
const latest = getSellerLatestDeposit(db, sellerId);
|
|
122
|
+
if (!latest || latest.status !== 'locked')
|
|
123
|
+
return void errorRes(res, 409, 'NO_LOCKED_BOND', '没有已锁定的保证金可退');
|
|
124
|
+
const blockers = enumerateBondRefundBlockers(db, sellerId);
|
|
125
|
+
if (blockers.length > 0)
|
|
126
|
+
return void res.status(409).json({ error: '有未了结的直付责任,暂不能申请退还', error_code: 'REFUND_BLOCKED', blockers });
|
|
127
|
+
const r = requestBondRefund(db, { depositId: latest.id, userId: sellerId });
|
|
128
|
+
if (!r.ok)
|
|
129
|
+
return void errorRes(res, 409, 'REFUND_REQUEST_FAILED', r.reason);
|
|
130
|
+
try {
|
|
131
|
+
const roots = await dbAll("SELECT id FROM users WHERE role = 'admin' AND (admin_type = 'root' OR admin_type IS NULL)");
|
|
132
|
+
for (const a of roots)
|
|
133
|
+
createNotification(db, a.id, null, 'bond_refund_requested', '↩️ 保证金退出申请待处理', `卖家 ${String(user.name ?? sellerId)} 申请退还履约保证金(冷静期 ${coolingDays()} 天,期间其直付资格已暂停)。冷静期满且复核无未了结责任后,场外退还并在 admin 后台记录执行。`, { templateKey: 'bond_refund_requested', params: { seller: String(user.name ?? sellerId), days: coolingDays() } });
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
console.warn('[bond-refund notify]', e.message);
|
|
137
|
+
}
|
|
138
|
+
return void res.json({ success: true, status: 'refunding', cooling_days: coolingDays(), note: '申请已提交:冷静期内你的直付资格暂停(不可接新直付单);冷静期满、复核无未了结责任后,平台在协议外退还并记录。可随时撤销申请恢复资格。' });
|
|
139
|
+
});
|
|
140
|
+
app.post('/api/direct-receive/bond-refund-request/cancel', (req, res) => {
|
|
141
|
+
const user = requireSeller(req, res);
|
|
142
|
+
if (!user)
|
|
143
|
+
return;
|
|
144
|
+
const latest = getSellerLatestDeposit(db, user.id);
|
|
145
|
+
if (!latest)
|
|
146
|
+
return void errorRes(res, 404, 'DEPOSIT_NOT_FOUND', '无保证金记录');
|
|
147
|
+
const r = cancelBondRefundRequest(db, { depositId: latest.id, userId: user.id });
|
|
148
|
+
if (!r.ok)
|
|
149
|
+
return void errorRes(res, 409, 'REFUND_CANCEL_FAILED', r.reason);
|
|
150
|
+
return void res.json({ success: true, status: 'locked', note: '退出申请已撤销,直付资格已恢复。' });
|
|
151
|
+
});
|
|
152
|
+
app.post('/api/direct-receive/bond-deposit/:id/cancel', (req, res) => {
|
|
153
|
+
const user = requireSeller(req, res);
|
|
154
|
+
if (!user)
|
|
155
|
+
return;
|
|
156
|
+
// 单一 writer 边界:deposits 写只经域模块(guard:direct-pay-deposit 机器强制,route 零裸写)
|
|
157
|
+
const r = cancelPendingDeposit(db, { depositId: req.params.id, userId: user.id });
|
|
158
|
+
if (!r.ok)
|
|
159
|
+
return void errorRes(res, r.reason === 'deposit not found' ? 404 : 409, 'BOND_CANCEL_FAILED', r.reason);
|
|
160
|
+
return void res.json({ success: true });
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // RFC-018 PR4: 真实成交(排除全额退货)
|
|
2
3
|
export function registerBuyerFeedsRoutes(app, deps) {
|
|
3
4
|
// db 已走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
|
|
4
5
|
const { auth, isTrustedRole, errorRes, getNearbyCellPrecision, getProtocolParam } = deps;
|
|
@@ -28,7 +29,7 @@ export function registerBuyerFeedsRoutes(app, deps) {
|
|
|
28
29
|
? `AND p.id NOT IN (${exclArgs.map(() => '?').join(',')})`
|
|
29
30
|
: '';
|
|
30
31
|
const baseCols = `p.id, p.title, p.price, p.stock, p.category, p.images, p.has_variants, p.seller_id,
|
|
31
|
-
(SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND o
|
|
32
|
+
(SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND ${genuineSalePredicate('o')}) as sales_count,
|
|
32
33
|
u.name as seller_name, u.handle as seller_handle`;
|
|
33
34
|
let followedProducts = [];
|
|
34
35
|
if (followedSellerIds.length > 0) {
|
package/dist/pwa/routes/chat.js
CHANGED
|
@@ -108,7 +108,12 @@ export function registerChatRoutes(app, deps) {
|
|
|
108
108
|
CASE WHEN c.user_a = ? THEN c.unread_a ELSE c.unread_b END as my_unread,
|
|
109
109
|
CASE WHEN c.user_a = ? THEN c.user_b ELSE c.user_a END as other_id,
|
|
110
110
|
(SELECT handle FROM users WHERE id = CASE WHEN c.user_a = ? THEN c.user_b ELSE c.user_a END) as other_handle,
|
|
111
|
-
(SELECT name FROM users WHERE id = CASE WHEN c.user_a = ? THEN c.user_b ELSE c.user_a END) as other_name
|
|
111
|
+
(SELECT name FROM users WHERE id = CASE WHEN c.user_a = ? THEN c.user_b ELSE c.user_a END) as other_name,
|
|
112
|
+
-- 友好标签:订单/问商品 → 商品名,让收件箱里同一对方的多个会话可区分(不只显示 kind)。无新 bind 参数(仅用 c.kind/c.context_id)。
|
|
113
|
+
(CASE c.kind
|
|
114
|
+
WHEN 'order' THEN (SELECT p.title FROM orders o JOIN products p ON p.id = o.product_id WHERE o.id = c.context_id)
|
|
115
|
+
WHEN 'listing_qa' THEN (SELECT title FROM products WHERE id = c.context_id)
|
|
116
|
+
ELSE NULL END) as context_title
|
|
112
117
|
FROM conversations c
|
|
113
118
|
WHERE (c.user_a = ? OR c.user_b = ?) AND c.status NOT IN ('blocked','archived')
|
|
114
119
|
ORDER BY COALESCE(c.last_message_at, c.created_at) DESC
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // RFC-018 PR4: 真实成交(排除全额退货)
|
|
2
3
|
export function registerDashboardsRoutes(app, deps) {
|
|
3
4
|
// db 已走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
|
|
4
5
|
const { auth } = deps;
|
|
@@ -70,7 +71,7 @@ export function registerDashboardsRoutes(app, deps) {
|
|
|
70
71
|
`, [userId, userId, userId, userId, userId, userId]);
|
|
71
72
|
const highComm = await dbAll(`
|
|
72
73
|
SELECT p.id, p.title, p.price, p.commission_rate, p.images, p.category,
|
|
73
|
-
(SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id AND o
|
|
74
|
+
(SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id AND ${genuineSalePredicate('o')}) as sales_count
|
|
74
75
|
FROM products p
|
|
75
76
|
WHERE p.status = 'active'
|
|
76
77
|
AND p.commission_rate > 0
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js';
|
|
2
|
+
import { toUnits } from '../../money.js';
|
|
3
|
+
import { sellerBaseBondEntrySatisfied } from '../../direct-pay-base-bond-entry.js';
|
|
4
|
+
import { evaluateDirectPayLaunchControls, readDirectPayControlsConfig, sellerDirectPayKybPassed, sellerDirectPaySanctionsClear, sellerDirectPayAmlClear, sellerDirectPayBreakerTripped, coarsenBuyerFacingDirectPayCode, DIRECT_PAY_SELLER_NOT_ELIGIBLE } from '../../direct-pay-controls.js';
|
|
5
|
+
import { checkDeferralQuota, readDeferralQuotaConfig } from '../../direct-pay-deferral-quota.js';
|
|
6
|
+
import { sellerDirectPayReadinessView } from '../../direct-pay-launch-readiness.js';
|
|
7
|
+
import { requestDeferral, getActiveDeferral, getLatestDeferral } from '../../direct-receive-deferral.js';
|
|
8
|
+
import { requestProductVerification, submitProductVerificationLink, listSellerProductVerifications, toSellerProductVerificationView, productStoreVerified } from '../../product-verification.js';
|
|
9
|
+
import { requestStoreVerification, submitStoreVerificationLink, getStoreVerification, toSellerStoreVerificationView, sellerExemptFromPerProduct } from '../../store-verification.js';
|
|
10
|
+
import { getDirectPayFeeAccount } from '../../direct-pay-fee-ar.js';
|
|
11
|
+
export function registerDirectPayAvailabilityRoutes(app, deps) {
|
|
12
|
+
const { db, auth, getProtocolParam, generateId } = deps;
|
|
13
|
+
/** 登录 + seller 角色门(缓交申请仅卖家本人)。返回 user 或 null(已写错误响应)。 */
|
|
14
|
+
function requireSeller(req, res) {
|
|
15
|
+
const user = auth(req, res);
|
|
16
|
+
if (!user)
|
|
17
|
+
return null;
|
|
18
|
+
if (user.role !== 'seller') {
|
|
19
|
+
res.status(403).json({ error: '仅卖家可申请缓交', error_code: 'SELLER_ONLY' });
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return user;
|
|
23
|
+
}
|
|
24
|
+
// GET /api/direct-pay/availability?product_id=... — 该商品(以 qty=1 计)当前是否可直付 + 不可用原因(脱敏)。
|
|
25
|
+
app.get('/api/direct-pay/availability', async (req, res) => {
|
|
26
|
+
const user = auth(req, res);
|
|
27
|
+
if (!user)
|
|
28
|
+
return;
|
|
29
|
+
const productId = String(req.query.product_id || '');
|
|
30
|
+
if (!productId)
|
|
31
|
+
return void res.status(400).json({ error: '缺少 product_id', error_code: 'MISSING_PRODUCT_ID' });
|
|
32
|
+
const product = await dbOne('SELECT seller_id, price FROM products WHERE id = ?', [productId]);
|
|
33
|
+
if (!product)
|
|
34
|
+
return void res.status(404).json({ error: '商品不存在', error_code: 'PRODUCT_NOT_FOUND' });
|
|
35
|
+
const cfg = readDirectPayControlsConfig(getProtocolParam);
|
|
36
|
+
const decision = evaluateDirectPayLaunchControls(cfg, {
|
|
37
|
+
amountUnits: toUnits(Number(product.price) || 0),
|
|
38
|
+
sellerBreakerTripped: sellerDirectPayBreakerTripped(db, product.seller_id), // 与 create 路径同源:卖家熔断也判不可用
|
|
39
|
+
baseBondSatisfied: sellerBaseBondEntrySatisfied(db, product.seller_id, new Date().toISOString()),
|
|
40
|
+
kycSanctionsPassed: sellerDirectPayKybPassed(db, product.seller_id) && sellerDirectPaySanctionsClear(db, product.seller_id),
|
|
41
|
+
amlClear: sellerDirectPayAmlClear(db, product.seller_id),
|
|
42
|
+
});
|
|
43
|
+
if (decision.ok) {
|
|
44
|
+
// 硬门(镜像 create):产品逐品验证 OR 卖家被豁免(店铺 verified + per_product_exempt)。都不满足 → 不可直付(产品级、非敏感)。
|
|
45
|
+
if (!(productStoreVerified(db, productId) || sellerExemptFromPerProduct(db, product.seller_id)))
|
|
46
|
+
return void res.json({ available: false, error_code: 'DIRECT_PAY_PRODUCT_NOT_VERIFIED', reason: '该商品暂不支持直付(待平台验证)', per_tx_cap_units: cfg.perTxCapUnits });
|
|
47
|
+
// 镜像 create 的缓交额度门(qty=1 预览;以商品单价为本次拟建单金额)。超额是【缓交卖家私密状态】→ 收敛为通用不可用,
|
|
48
|
+
// 不向买家暴露"该卖家在缓交期/已超额"。create 仍是权威强制点。
|
|
49
|
+
const quota = checkDeferralQuota(db, product.seller_id, toUnits(Number(product.price) || 0), new Date().toISOString(), readDeferralQuotaConfig(getProtocolParam));
|
|
50
|
+
if (!quota.ok)
|
|
51
|
+
return void res.json({ available: false, error_code: coarsenBuyerFacingDirectPayCode(quota.code), reason: '该卖家暂不支持直付', per_tx_cap_units: cfg.perTxCapUnits });
|
|
52
|
+
return void res.json({ available: true, per_tx_cap_units: cfg.perTxCapUnits });
|
|
53
|
+
}
|
|
54
|
+
const code = coarsenBuyerFacingDirectPayCode(decision.error_code);
|
|
55
|
+
return void res.json({
|
|
56
|
+
available: false,
|
|
57
|
+
error_code: code,
|
|
58
|
+
reason: code === DIRECT_PAY_SELLER_NOT_ELIGIBLE ? '该卖家暂不支持直付' : decision.reason,
|
|
59
|
+
per_tx_cap_units: cfg.perTxCapUnits,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
// GET /api/direct-receive/readiness — 卖家【自助脱敏】readiness:仅可行动/状态项(收款说明/Passkey/保证金/审核/暂停/平台开放)。
|
|
63
|
+
// 绝不下发 raw blocker / KYB·制裁·AML 分项(见 sellerDirectPayReadinessView)。只读 self(auth 用户自身 id)。
|
|
64
|
+
app.get('/api/direct-receive/readiness', (req, res) => {
|
|
65
|
+
const user = auth(req, res);
|
|
66
|
+
if (!user)
|
|
67
|
+
return;
|
|
68
|
+
return void res.json(sellerDirectPayReadinessView(db, { getProtocolParam, sellerId: user.id }));
|
|
69
|
+
});
|
|
70
|
+
// ── 缓交(base-bond deferred deposit)卖家自助:申请 + 查看自己的状态。授予【绝不】在此发生 ──
|
|
71
|
+
// 申请只创建 pending 行(requestDeferral),【不授予任何权限/资格】;真正放行 = admin ROOT + 真人 Passkey 审批
|
|
72
|
+
// (POST /api/admin/direct-receive/deferrals/:id/approve)。故 apply 本身非 RISK 动作、不需 Passkey:无论谁(含 agent)
|
|
73
|
+
// 申请都拿不到资格,安全门坐落在审批侧。所有合规门(KYC/KYB/制裁/AML/收款说明/Passkey)在直付建单时仍逐一 AND,缓交只免"先交保证金"。
|
|
74
|
+
// POST /api/direct-receive/deferral — 卖家申请缓交。helper 强制:单一活跃、periodDays 正整数、id 唯一。
|
|
75
|
+
app.post('/api/direct-receive/deferral', (req, res) => {
|
|
76
|
+
const user = requireSeller(req, res);
|
|
77
|
+
if (!user)
|
|
78
|
+
return;
|
|
79
|
+
const reason = typeof req.body?.reason === 'string' ? req.body.reason.trim().slice(0, 500) : undefined;
|
|
80
|
+
const periodDays = req.body?.period_days != null ? Number(req.body.period_days) : undefined;
|
|
81
|
+
const r = requestDeferral(db, { deferralId: generateId('dfr'), userId: user.id, periodDays, reason, nowIso: new Date().toISOString() });
|
|
82
|
+
if (!r.ok)
|
|
83
|
+
return void res.status(400).json({ error: r.reason, error_code: 'DEFERRAL_REQUEST_REJECTED' });
|
|
84
|
+
return void res.json({ ok: true, status: r.status });
|
|
85
|
+
});
|
|
86
|
+
// GET /api/direct-receive/deferral — 卖家本人缓交状态:最新一条申请(脱敏:不含 admin 身份)+ 是否当前生效(active)。
|
|
87
|
+
app.get('/api/direct-receive/deferral', (req, res) => {
|
|
88
|
+
const user = requireSeller(req, res);
|
|
89
|
+
if (!user)
|
|
90
|
+
return;
|
|
91
|
+
const sellerId = user.id;
|
|
92
|
+
const latest = getLatestDeferral(db, sellerId); // 脱敏:不含 approved_by/approved_at(admin 身份)
|
|
93
|
+
const active = getActiveDeferral(db, sellerId, new Date().toISOString());
|
|
94
|
+
return void res.json({
|
|
95
|
+
deferral: latest,
|
|
96
|
+
active: active ? { reduced_quota_factor: active.reducedQuotaFactor, expires_at: active.expiresAt, grace_until: active.graceUntil, in_grace: active.inGrace } : null,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
// GET /api/direct-receive/my-fee-account — 卖家【本人】平台服务费账户(余额/已计提/退款/在途预估/首单宽限)。
|
|
100
|
+
// 仅本人(requireSeller),买家拿不到;只读、不碰任何资金动作。供 seller fee center 展示。
|
|
101
|
+
app.get('/api/direct-receive/my-fee-account', (req, res) => {
|
|
102
|
+
const user = requireSeller(req, res);
|
|
103
|
+
if (!user)
|
|
104
|
+
return;
|
|
105
|
+
return void res.json({ account: getDirectPayFeeAccount(db, user.id) });
|
|
106
|
+
});
|
|
107
|
+
// ── 按产品认证(per-product verification)卖家自助:申领验证码 → 提交外部商品链接 → 查看逐产品状态 ──
|
|
108
|
+
// 硬门:每个直付商品都须【单独】被真人 admin 核验;一次验证绝不放行所有产品。这里只建/改记录,verify 在 admin 侧。
|
|
109
|
+
// 所有权:必须卖家本人拥有该产品(读 products.seller_id 校验),否则 403。
|
|
110
|
+
/** 校验登录卖家拥有该产品;返回 owner user(含 id)或 null(已写错误响应)。productId 由调用方持有。 */
|
|
111
|
+
async function requireOwnedProduct(req, res, productId) {
|
|
112
|
+
const user = requireSeller(req, res);
|
|
113
|
+
if (!user)
|
|
114
|
+
return null;
|
|
115
|
+
if (!productId) {
|
|
116
|
+
res.status(400).json({ error: '缺少 product_id', error_code: 'MISSING_PRODUCT_ID' });
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const product = await dbOne('SELECT seller_id FROM products WHERE id = ?', [productId]);
|
|
120
|
+
if (!product) {
|
|
121
|
+
res.status(404).json({ error: '商品不存在', error_code: 'PRODUCT_NOT_FOUND' });
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (product.seller_id !== user.id) {
|
|
125
|
+
res.status(403).json({ error: '只能为自己的商品申请认证', error_code: 'NOT_PRODUCT_OWNER' });
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return user;
|
|
129
|
+
}
|
|
130
|
+
// POST /api/direct-receive/product-verification — 卖家为某产品申领验证码(单一活跃 per product)。
|
|
131
|
+
app.post('/api/direct-receive/product-verification', async (req, res) => {
|
|
132
|
+
const productId = String(req.body?.product_id || '');
|
|
133
|
+
const user = await requireOwnedProduct(req, res, productId);
|
|
134
|
+
if (!user)
|
|
135
|
+
return;
|
|
136
|
+
const platform = typeof req.body?.platform === 'string' ? req.body.platform.trim().slice(0, 60) : undefined;
|
|
137
|
+
const r = requestProductVerification(db, { id: generateId('pv'), productId, sellerId: user.id, code: generateId('wzv'), platform });
|
|
138
|
+
if (!r.ok)
|
|
139
|
+
return void res.status(400).json({ error: r.reason, error_code: 'PRODUCT_VERIFICATION_REJECTED' });
|
|
140
|
+
return void res.json({ ok: true, status: r.status, code: r.code });
|
|
141
|
+
});
|
|
142
|
+
// PUT /api/direct-receive/product-verification — 卖家为某产品提交外部商品链接(链接仅存储,WebAZ 不抓取)。
|
|
143
|
+
app.put('/api/direct-receive/product-verification', async (req, res) => {
|
|
144
|
+
const productId = String(req.body?.product_id || '');
|
|
145
|
+
const user = await requireOwnedProduct(req, res, productId);
|
|
146
|
+
if (!user)
|
|
147
|
+
return;
|
|
148
|
+
const externalUrl = typeof req.body?.external_url === 'string' ? req.body.external_url.trim() : '';
|
|
149
|
+
const platform = typeof req.body?.platform === 'string' ? req.body.platform.trim().slice(0, 60) : undefined;
|
|
150
|
+
const r = submitProductVerificationLink(db, { productId, externalUrl, platform });
|
|
151
|
+
if (!r.ok)
|
|
152
|
+
return void res.status(400).json({ error: r.reason, error_code: 'PRODUCT_VERIFICATION_SUBMIT_REJECTED' });
|
|
153
|
+
return void res.json({ ok: true, status: r.status });
|
|
154
|
+
});
|
|
155
|
+
// GET /api/direct-receive/product-verifications — 卖家本人所有产品的认证状态(逐产品)。
|
|
156
|
+
app.get('/api/direct-receive/product-verifications', (req, res) => {
|
|
157
|
+
const user = requireSeller(req, res);
|
|
158
|
+
if (!user)
|
|
159
|
+
return;
|
|
160
|
+
// 脱敏:DTO 去掉 reviewed_by(admin 身份)+ notes(内部审核备注),与缓交/readiness 卖家侧一致。
|
|
161
|
+
return void res.json({ verifications: listSellerProductVerifications(db, user.id).map(toSellerProductVerificationView) });
|
|
162
|
+
});
|
|
163
|
+
// ── 店铺认证(per-seller)= 逐品验证的豁免申请路径。卖家申领码 → 提交店铺链接 → admin 核对时勾选豁免。 ──
|
|
164
|
+
// 申请/提交【不】门 Passkey(只建记录,不授予;豁免授予 = admin ROOT+Passkey 在审核侧)。
|
|
165
|
+
// POST /api/direct-receive/store-verification — 卖家申领店铺验证码(单一活跃 per seller)。
|
|
166
|
+
app.post('/api/direct-receive/store-verification', (req, res) => {
|
|
167
|
+
const user = requireSeller(req, res);
|
|
168
|
+
if (!user)
|
|
169
|
+
return;
|
|
170
|
+
const platform = typeof req.body?.platform === 'string' ? req.body.platform.trim().slice(0, 60) : undefined;
|
|
171
|
+
const r = requestStoreVerification(db, { id: generateId('sv'), userId: user.id, code: generateId('wzs'), platform });
|
|
172
|
+
if (!r.ok)
|
|
173
|
+
return void res.status(400).json({ error: r.reason, error_code: 'STORE_VERIFICATION_REJECTED' });
|
|
174
|
+
return void res.json({ ok: true, status: r.status, code: r.code });
|
|
175
|
+
});
|
|
176
|
+
// PUT /api/direct-receive/store-verification — 卖家提交店铺外链(仅存储,不抓取)。
|
|
177
|
+
app.put('/api/direct-receive/store-verification', (req, res) => {
|
|
178
|
+
const user = requireSeller(req, res);
|
|
179
|
+
if (!user)
|
|
180
|
+
return;
|
|
181
|
+
const externalUrl = typeof req.body?.external_url === 'string' ? req.body.external_url.trim() : '';
|
|
182
|
+
const platform = typeof req.body?.platform === 'string' ? req.body.platform.trim().slice(0, 60) : undefined;
|
|
183
|
+
const r = submitStoreVerificationLink(db, { userId: user.id, externalUrl, platform });
|
|
184
|
+
if (!r.ok)
|
|
185
|
+
return void res.status(400).json({ error: r.reason, error_code: 'STORE_VERIFICATION_SUBMIT_REJECTED' });
|
|
186
|
+
return void res.json({ ok: true, status: r.status });
|
|
187
|
+
});
|
|
188
|
+
// GET /api/direct-receive/store-verification — 卖家本人店铺认证状态(脱敏 DTO,含豁免位)。
|
|
189
|
+
app.get('/api/direct-receive/store-verification', (req, res) => {
|
|
190
|
+
const user = requireSeller(req, res);
|
|
191
|
+
if (!user)
|
|
192
|
+
return;
|
|
193
|
+
const row = getStoreVerification(db, user.id);
|
|
194
|
+
return void res.json({ verification: row ? toSellerStoreVerificationView(row) : null, exempt: sellerExemptFromPerProduct(db, user.id) });
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { requestCancelRefund, declineCancelRefund, markRefunded, withdrawCancelRefund, confirmRefundReceived, getCancelRefundState, } from '../../direct-pay-cancel-refund.js';
|
|
2
|
+
import { requireDirectPayHumanPasskey } from '../direct-pay-guards.js';
|
|
3
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam(通知收件人读)
|
|
4
|
+
import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
|
|
5
|
+
import { createNotification } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
6
|
+
export function registerDirectPayCancelRefundRoutes(app, deps) {
|
|
7
|
+
const { db, auth, generateId, errorRes, consumeGateToken } = deps;
|
|
8
|
+
const httpFor = (code) => code === 'ORDER_NOT_FOUND' ? 404
|
|
9
|
+
: code === 'NOT_A_PARTY' || code === 'NOT_ORDER_BUYER' || code === 'NOT_ORDER_SELLER' ? 403
|
|
10
|
+
: code === 'SYS_MISSING' ? 500
|
|
11
|
+
: 409;
|
|
12
|
+
const notify = (userId, orderId, type, title, body) => {
|
|
13
|
+
try {
|
|
14
|
+
createNotification(db, userId, orderId, type, title, body);
|
|
15
|
+
}
|
|
16
|
+
catch { /* 通知失败不阻断业务 */ }
|
|
17
|
+
};
|
|
18
|
+
const party = async (orderId) => await dbOne('SELECT buyer_id, seller_id FROM orders WHERE id = ?', [orderId]);
|
|
19
|
+
app.get('/api/orders/:id/cancel-refund', (req, res) => {
|
|
20
|
+
const user = auth(req, res);
|
|
21
|
+
if (!user)
|
|
22
|
+
return;
|
|
23
|
+
const r = getCancelRefundState(db, req.params.id, user.id);
|
|
24
|
+
if (!r.ok)
|
|
25
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code || 'CANCEL_REFUND_STATE_ERROR', r.error || '读取失败');
|
|
26
|
+
res.json({ success: true, request: r.request ?? null, can_request: !!r.can_request, can_respond: !!r.can_respond, can_confirm: !!r.can_confirm, can_withdraw: !!r.can_withdraw });
|
|
27
|
+
});
|
|
28
|
+
app.post('/api/orders/:id/cancel-refund/request', async (req, res) => {
|
|
29
|
+
const user = auth(req, res);
|
|
30
|
+
if (!user)
|
|
31
|
+
return;
|
|
32
|
+
const reason = typeof req.body?.reason === 'string' ? req.body.reason : null;
|
|
33
|
+
const r = requestCancelRefund(db, { orderId: req.params.id, buyerId: user.id, reason, requestId: generateId('dpcr') });
|
|
34
|
+
if (!r.ok)
|
|
35
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code || 'CANCEL_REFUND_REQUEST_ERROR', r.error || '发起失败');
|
|
36
|
+
const p = await party(req.params.id);
|
|
37
|
+
if (p)
|
|
38
|
+
notify(p.seller_id, req.params.id, 'direct_pay_cancel_requested', '↩️ 买家申请取消订单并退款', `买家已付款但希望取消订单${reason ? `(理由:${String(reason).slice(0, 80)})` : ''}。请在期限内响应:同意则场外退款后点"我已退款",不同意可拒绝(继续发货)。直付非托管,退款在协议外完成。`);
|
|
39
|
+
return void res.json({ success: true, status: r.status, request: r.request ?? null });
|
|
40
|
+
});
|
|
41
|
+
app.post('/api/orders/:id/cancel-refund/decline', async (req, res) => {
|
|
42
|
+
const user = auth(req, res);
|
|
43
|
+
if (!user)
|
|
44
|
+
return;
|
|
45
|
+
const r = declineCancelRefund(db, { orderId: req.params.id, sellerId: user.id });
|
|
46
|
+
if (!r.ok)
|
|
47
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code || 'CANCEL_REFUND_DECLINE_ERROR', r.error || '拒绝失败');
|
|
48
|
+
const p = await party(req.params.id);
|
|
49
|
+
if (p)
|
|
50
|
+
notify(p.buyer_id, req.params.id, 'direct_pay_cancel_declined', '❌ 卖家拒绝了取消退款请求', '卖家选择继续履约发货。如有异议可与卖家沟通,或在有证据时发起争议。');
|
|
51
|
+
return void res.json({ success: true, status: r.status });
|
|
52
|
+
});
|
|
53
|
+
app.post('/api/orders/:id/cancel-refund/mark-refunded', async (req, res) => {
|
|
54
|
+
const user = auth(req, res);
|
|
55
|
+
if (!user)
|
|
56
|
+
return;
|
|
57
|
+
const refundReference = typeof req.body?.refund_reference === 'string' ? req.body.refund_reference : null;
|
|
58
|
+
const r = markRefunded(db, { orderId: req.params.id, sellerId: user.id, refundReference });
|
|
59
|
+
if (!r.ok)
|
|
60
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code || 'CANCEL_REFUND_MARK_ERROR', r.error || '声明退款失败');
|
|
61
|
+
const p = await party(req.params.id);
|
|
62
|
+
if (p)
|
|
63
|
+
notify(p.buyer_id, req.params.id, 'direct_pay_refund_marked', '💸 卖家已声明退款', `卖家声明已在协议外向你退款${refundReference ? `(退款参考:${String(refundReference).slice(0, 80)})` : ''}。请核实到账后在订单页确认(需 Passkey),订单将无责取消;若未收到退款请勿确认,可发起争议。`);
|
|
64
|
+
return void res.json({ success: true, status: r.status });
|
|
65
|
+
});
|
|
66
|
+
app.post('/api/orders/:id/cancel-refund/withdraw', async (req, res) => {
|
|
67
|
+
const user = auth(req, res);
|
|
68
|
+
if (!user)
|
|
69
|
+
return;
|
|
70
|
+
const r = withdrawCancelRefund(db, { orderId: req.params.id, buyerId: user.id });
|
|
71
|
+
if (!r.ok)
|
|
72
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code || 'CANCEL_REFUND_WITHDRAW_ERROR', r.error || '撤回失败');
|
|
73
|
+
const p = await party(req.params.id);
|
|
74
|
+
if (p)
|
|
75
|
+
notify(p.seller_id, req.params.id, 'direct_pay_cancel_withdrawn', '↩️ 买家撤回了取消退款请求', '订单继续正常履约,请按流程发货。');
|
|
76
|
+
return void res.json({ success: true, status: r.status });
|
|
77
|
+
});
|
|
78
|
+
app.post('/api/orders/:id/cancel-refund/confirm', async (req, res) => {
|
|
79
|
+
const user = auth(req, res);
|
|
80
|
+
if (!user)
|
|
81
|
+
return;
|
|
82
|
+
const uid = user.id;
|
|
83
|
+
// 只读预检(不消费 token):必须存在 refund_marked 的请求且订单仍 accepted,否则不浪费一次性 Passkey token
|
|
84
|
+
const pre = getCancelRefundState(db, req.params.id, uid);
|
|
85
|
+
if (!pre.ok)
|
|
86
|
+
return void errorRes(res, httpFor(pre.error_code), pre.error_code || 'CANCEL_REFUND_STATE_ERROR', pre.error || '读取失败');
|
|
87
|
+
if (!pre.can_confirm)
|
|
88
|
+
return void errorRes(res, 409, 'REFUND_NOT_MARKED', '当前不可确认(卖家尚未声明退款,或订单已不在待发货阶段)');
|
|
89
|
+
// RISK 门:确认收款并关单 = 终局动作 → 现场真人 Passkey(agent 无 Passkey 硬拒)
|
|
90
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
91
|
+
userId: uid, webauthnToken: req.body?.webauthn_token, purpose: 'direct_pay_order_action',
|
|
92
|
+
validate: (data) => { const d = data; return !!d && d.order_id === req.params.id && d.action === 'cancel_refund_confirm'; },
|
|
93
|
+
});
|
|
94
|
+
if (!gate.ok)
|
|
95
|
+
return void errorRes(res, 403, gate.error_code || 'HUMAN_PRESENCE_REQUIRED', gate.reason || '需现场真人 Passkey 确认');
|
|
96
|
+
let r;
|
|
97
|
+
try {
|
|
98
|
+
// 状态手术:请求行 CAS + accepted→cancelled + 库存恢复,同一原子边界(域内重校验防 ship/争议竞态)
|
|
99
|
+
r = db.transaction(() => confirmRefundReceived(db, { orderId: req.params.id, buyerId: uid }, transition))();
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
return void errorRes(res, 409, 'CANCEL_REFUND_CONFIRM_FAILED', e.message);
|
|
103
|
+
}
|
|
104
|
+
if (!r.ok)
|
|
105
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code || 'CANCEL_REFUND_CONFIRM_ERROR', r.error || '确认失败');
|
|
106
|
+
const p = await party(req.params.id);
|
|
107
|
+
if (p)
|
|
108
|
+
notify(p.seller_id, req.params.id, 'direct_pay_cancel_settled', '✅ 取消退款握手完成', '买家已确认收到退款,订单已无责取消,库存已恢复。双方信誉均不受影响。');
|
|
109
|
+
return void res.json({ success: true, status: r.status });
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js';
|
|
2
|
+
import { STAGE, recordDisclosureAck, disclosureStageAcked, requireBothDisclosuresAcked, getBuyerDisclosures } from '../../direct-pay-disclosures.js';
|
|
3
|
+
import { requireDirectPayHumanPasskey } from '../direct-pay-guards.js';
|
|
4
|
+
const VALID_STAGES = [STAGE.PRE_SELECT, STAGE.PRE_CONFIRM];
|
|
5
|
+
export function registerDirectPayDisclosureAckRoutes(app, deps) {
|
|
6
|
+
const { db, auth, generateId, consumeGateToken } = deps;
|
|
7
|
+
/** 订单存在 + direct_p2p + 调用者本人是 buyer。返回 order 或 null(已写错误响应)。 */
|
|
8
|
+
async function requireOwnDirectPayOrder(orderId, res, user) {
|
|
9
|
+
if (!orderId) {
|
|
10
|
+
res.status(400).json({ error: '缺少 order_id', error_code: 'MISSING_ORDER_ID' });
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const order = await dbOne('SELECT id, buyer_id, payment_rail, status FROM orders WHERE id = ?', [orderId]);
|
|
14
|
+
if (!order) {
|
|
15
|
+
res.status(404).json({ error: '订单不存在', error_code: 'ORDER_NOT_FOUND' });
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (order.payment_rail !== 'direct_p2p') {
|
|
19
|
+
res.status(409).json({ error: '风险披露仅适用于直付(direct_p2p)订单', error_code: 'NOT_DIRECT_PAY_ORDER' });
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (order.buyer_id !== user.id) {
|
|
23
|
+
res.status(403).json({ error: '只能确认自己订单的风险披露', error_code: 'NOT_ORDER_BUYER' });
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return order;
|
|
27
|
+
}
|
|
28
|
+
// POST — 记录 ack。stage = pre_select | pre_confirm | both。需现场真人(Passkey + gate token)。幂等(INSERT OR IGNORE)。
|
|
29
|
+
// 'both'(2026-07-04 用户决策,contract v14):两屏披露【仍各自展示并确认】(文本证据不减),但一次真人 ceremony
|
|
30
|
+
// 可覆盖两个 stage → 落库仍是【两行】ack(各带 notice_version+acked_at,requireBothDisclosuresAcked 证据模型不变);
|
|
31
|
+
// 变化仅是"人在场证明"从两次合并为一次(首单 Passkey 3→2;mark_paid 的独立 RISK 门不变)。token purpose_data 绑
|
|
32
|
+
// stage:'both' —— 单 stage token 冒充不了 both,both token 也重放不了单 stage(一次性消费 + validate 精确匹配)。
|
|
33
|
+
app.post('/api/direct-pay/disclosure-acks', async (req, res) => {
|
|
34
|
+
const user = auth(req, res);
|
|
35
|
+
if (!user)
|
|
36
|
+
return;
|
|
37
|
+
const stage = req.body?.stage;
|
|
38
|
+
if (stage !== 'both' && !VALID_STAGES.includes(stage))
|
|
39
|
+
return void res.status(400).json({ error: `无效 stage(只允许 ${VALID_STAGES.join('|')}|both)`, error_code: 'INVALID_STAGE' });
|
|
40
|
+
const order = await requireOwnDirectPayOrder(req.body?.order_id, res, user);
|
|
41
|
+
if (!order)
|
|
42
|
+
return;
|
|
43
|
+
// human-only:现场真人 Passkey 二次确认。purpose 用【固定白名单值】(/api/webauthn/auth/start 才放行申请 challenge);
|
|
44
|
+
// order+stage 绑定走 purpose_data + validate(杜绝跨单/跨阶段复用 token),不能塞进 purpose 字符串(那样真实 UI 拿不到 token)。
|
|
45
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
46
|
+
userId: user.id, webauthnToken: req.body?.webauthn_token,
|
|
47
|
+
purpose: 'direct_pay_disclosure_ack',
|
|
48
|
+
validate: (data) => { const d = data; return !!d && d.order_id === order.id && d.stage === stage; },
|
|
49
|
+
});
|
|
50
|
+
if (!gate.ok)
|
|
51
|
+
return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
|
|
52
|
+
for (const s of (stage === 'both' ? VALID_STAGES : [stage]))
|
|
53
|
+
recordDisclosureAck(db, { orderId: order.id, buyerId: user.id, stage: s, ackId: generateId('dpa') });
|
|
54
|
+
return void res.json({
|
|
55
|
+
ok: true, stage,
|
|
56
|
+
both: requireBothDisclosuresAcked(db, order.id).ok,
|
|
57
|
+
note: 'ack = 真人本人已阅读并确认风险披露;不代表付款完成,不代表 WebAZ 提供担保/维权/退款',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
// GET — 查询某单两次 ack 状态 + 买家视角披露文案(无卖家机制)。只读(本人),不需 gate token。
|
|
61
|
+
app.get('/api/direct-pay/disclosure-acks/:orderId', async (req, res) => {
|
|
62
|
+
const user = auth(req, res);
|
|
63
|
+
if (!user)
|
|
64
|
+
return;
|
|
65
|
+
const order = await requireOwnDirectPayOrder(req.params.orderId, res, user);
|
|
66
|
+
if (!order)
|
|
67
|
+
return;
|
|
68
|
+
const d = getBuyerDisclosures();
|
|
69
|
+
return void res.json({
|
|
70
|
+
order_id: order.id,
|
|
71
|
+
acked: { pre_select: disclosureStageAcked(db, order.id, STAGE.PRE_SELECT), pre_confirm: disclosureStageAcked(db, order.id, STAGE.PRE_CONFIRM) },
|
|
72
|
+
both: requireBothDisclosuresAcked(db, order.id).ok,
|
|
73
|
+
disclosures: {
|
|
74
|
+
pre_select: { zh: d.preSelect.zh, en: d.preSelect.en, version: d.preSelect.version },
|
|
75
|
+
pre_confirm: { zh: d.preConfirm.zh, en: d.preConfirm.en, version: d.preConfirm.version },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|