@seasonkoh/webaz 0.1.29 → 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/dist/bond-refund-blockers.js +50 -0
- package/dist/bond-slash.js +100 -0
- package/dist/bond-terms.js +31 -0
- package/dist/direct-pay-bond-rail-clearance.js +10 -5
- package/dist/direct-pay-cancel-refund.js +160 -0
- package/dist/direct-pay-create.js +78 -9
- package/dist/direct-pay-disclosures.js +17 -11
- package/dist/direct-pay-fee-ar.js +2 -2
- package/dist/direct-pay-fee-prepay-request.js +80 -0
- package/dist/direct-pay-returns.js +104 -0
- package/dist/direct-pay-stock.js +9 -0
- package/dist/direct-receive-deferral.js +30 -1
- package/dist/direct-receive-deposits.js +122 -17
- package/dist/free-shipping.js +37 -0
- package/dist/fx-rates.js +7 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +246 -0
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +73 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +81 -16
- 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/platform-receive-accounts.js +94 -0
- package/dist/pwa/arbitration-read-admin.js +37 -0
- package/dist/pwa/arbitrator-lifecycle.js +100 -0
- package/dist/pwa/contract-fingerprint.js +15 -0
- package/dist/pwa/direct-pay-order-redaction.js +20 -2
- 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-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 +6 -6
- package/dist/pwa/public/app-contribution-hub.js +23 -0
- package/dist/pwa/public/app-direct-pay-accounts.js +1 -1
- package/dist/pwa/public/app-direct-pay-buyer.js +1 -1
- package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
- package/dist/pwa/public/app-direct-pay-copy.js +12 -0
- package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +1 -1
- 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-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.js +36 -37
- package/dist/pwa/public/app-dispute-close-ui.js +38 -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-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-profile.js +4 -4
- 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 +1 -1
- package/dist/pwa/public/app-trade-tax-ui.js +33 -0
- package/dist/pwa/public/app.js +133 -160
- package/dist/pwa/public/i18n.js +675 -5
- package/dist/pwa/public/index.html +41 -0
- package/dist/pwa/public/openapi.json +709 -11
- package/dist/pwa/public/style.css +3 -0
- package/dist/pwa/routes/admin-direct-receive-deposits.js +215 -3
- 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/direct-pay-cancel-refund.js +111 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +9 -4
- 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 +186 -9
- 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/governance-onboarding.js +46 -8
- package/dist/pwa/routes/logistics.js +4 -4
- package/dist/pwa/routes/me-data.js +2 -2
- package/dist/pwa/routes/mutual-cancel.js +62 -0
- package/dist/pwa/routes/orders-action.js +182 -9
- package/dist/pwa/routes/orders-create.js +18 -7
- package/dist/pwa/routes/orders-read.js +76 -14
- package/dist/pwa/routes/platform-receive-accounts.js +111 -0
- package/dist/pwa/routes/products-create.js +13 -4
- package/dist/pwa/routes/products-update.js +15 -1
- package/dist/pwa/routes/profile-identity.js +4 -2
- package/dist/pwa/routes/returns.js +39 -8
- 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/snf.js +4 -1
- package/dist/pwa/routes/webauthn.js +3 -3
- package/dist/pwa/server.js +59 -36
- package/dist/runtime/agent-grant-scopes.js +69 -1
- package/dist/runtime/webaz-schema-helpers.js +57 -3
- package/dist/sale-regions.js +116 -0
- package/dist/shipping-templates.js +119 -0
- package/dist/trade-tax.js +99 -0
- package/dist/trade-terms.js +76 -0
- package/dist/version.js +1 -1
- package/package.json +59 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { OPEN_FEE_ACCRUING_STATUSES, readAvailableFeePrepayUnits } from './direct-pay-fee-ar.js';
|
|
2
|
+
import { sellerHasPendingSlash } from './bond-slash.js'; // B3:待复核罚没提案挡退出
|
|
3
|
+
export function enumerateBondRefundBlockers(db, sellerId) {
|
|
4
|
+
const out = [];
|
|
5
|
+
// ① 在途直付单(OPEN_FEE_ACCRUING_STATUSES 已含 pending_accept..disputed 全部非终态)
|
|
6
|
+
try {
|
|
7
|
+
const ph = OPEN_FEE_ACCRUING_STATUSES.map(() => '?').join(',');
|
|
8
|
+
const n = db.prepare(`SELECT COUNT(*) n FROM orders WHERE seller_id = ? AND payment_rail = 'direct_p2p' AND status IN (${ph})`)
|
|
9
|
+
.get(sellerId, ...OPEN_FEE_ACCRUING_STATUSES).n;
|
|
10
|
+
if (n > 0)
|
|
11
|
+
out.push({ code: 'OPEN_DIRECT_PAY_ORDERS', count: n });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
out.push({ code: 'UNVERIFIABLE_OPEN_ORDERS' });
|
|
15
|
+
}
|
|
16
|
+
// ② 取消退款握手进行中(防御性)
|
|
17
|
+
try {
|
|
18
|
+
const n = db.prepare(`SELECT COUNT(*) n FROM direct_pay_cancel_requests WHERE seller_id = ? AND status IN ('requested','refund_marked')`)
|
|
19
|
+
.get(sellerId).n;
|
|
20
|
+
if (n > 0)
|
|
21
|
+
out.push({ code: 'OPEN_CANCEL_REFUND_HANDSHAKE', count: n });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
out.push({ code: 'UNVERIFIABLE_CANCEL_REFUND' });
|
|
25
|
+
}
|
|
26
|
+
// ③ 退货流进行中(completed 直付单的退货/场外退款握手 —— ① 覆盖不到)
|
|
27
|
+
try {
|
|
28
|
+
const n = db.prepare(`SELECT COUNT(*) n FROM return_requests r JOIN orders o ON o.id = r.order_id
|
|
29
|
+
WHERE o.seller_id = ? AND o.payment_rail = 'direct_p2p'
|
|
30
|
+
AND r.status IN ('pending','accepted_pickup_pending','picked_up','await_refund','refund_marked')`)
|
|
31
|
+
.get(sellerId).n;
|
|
32
|
+
if (n > 0)
|
|
33
|
+
out.push({ code: 'OPEN_RETURN_FLOW', count: n });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
out.push({ code: 'UNVERIFIABLE_RETURNS' });
|
|
37
|
+
}
|
|
38
|
+
// ⑤ 待复核罚没提案(B3;sellerHasPendingSlash 内部 fail-closed)
|
|
39
|
+
if (sellerHasPendingSlash(db, sellerId))
|
|
40
|
+
out.push({ code: 'PENDING_SLASH_REVIEW' });
|
|
41
|
+
// ④ 平台服务费欠费(available = Σ预充值 + Σ调整 − Σ已计提;负 = 欠)
|
|
42
|
+
try {
|
|
43
|
+
if (readAvailableFeePrepayUnits(db, sellerId) < 0)
|
|
44
|
+
out.push({ code: 'UNPAID_PLATFORM_FEES' });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
out.push({ code: 'UNVERIFIABLE_FEES' });
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { slashBond } from './direct-receive-deposits.js';
|
|
2
|
+
const SELLER_FAULT_RULINGS = ['refund_buyer', 'partial_refund'];
|
|
3
|
+
/** 卖家是否有待复核罚没提案(B2 退出 blockers 用)。 */
|
|
4
|
+
export function sellerHasPendingSlash(db, sellerId) {
|
|
5
|
+
try {
|
|
6
|
+
return !!db.prepare("SELECT 1 FROM bond_slash_proposals WHERE seller_id = ? AND status = 'proposed' LIMIT 1").get(sellerId);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return true;
|
|
10
|
+
} // 表读不了 → fail-closed(视为有,挡退出)
|
|
11
|
+
}
|
|
12
|
+
/** admin 发起罚没提案。校验依据争议 + deposit 状态;冷静期算成绝对截止存库。 */
|
|
13
|
+
export function proposeBondSlash(db, args) {
|
|
14
|
+
const dep = db.prepare('SELECT id, user_id, status, production_receipt_confirmed_at FROM direct_receive_deposits WHERE id = ?')
|
|
15
|
+
.get(args.depositId);
|
|
16
|
+
if (!dep)
|
|
17
|
+
return { ok: false, reason: 'deposit not found' };
|
|
18
|
+
if (dep.status !== 'locked' && dep.status !== 'refunding')
|
|
19
|
+
return { ok: false, reason: `can only propose slash on a locked/refunding bond (got '${dep.status}')` };
|
|
20
|
+
const dsp = db.prepare(`SELECT d.id, d.status, d.ruling_type, o.payment_rail, o.seller_id
|
|
21
|
+
FROM disputes d JOIN orders o ON o.id = d.order_id WHERE d.id = ?`)
|
|
22
|
+
.get(args.disputeId);
|
|
23
|
+
if (!dsp)
|
|
24
|
+
return { ok: false, reason: 'dispute not found' };
|
|
25
|
+
if (dsp.payment_rail !== 'direct_p2p')
|
|
26
|
+
return { ok: false, reason: 'dispute is not on a direct_p2p order(罚没口径仅直付争议)' };
|
|
27
|
+
if (dsp.seller_id !== dep.user_id)
|
|
28
|
+
return { ok: false, reason: 'dispute seller does not match bond owner' };
|
|
29
|
+
if (dsp.status !== 'resolved' || !dsp.ruling_type || !SELLER_FAULT_RULINGS.includes(dsp.ruling_type)) {
|
|
30
|
+
return { ok: false, reason: `dispute must be resolved with a seller-fault ruling (${SELLER_FAULT_RULINGS.join('|')}); got status='${dsp.status}' ruling='${dsp.ruling_type ?? 'none'}'` };
|
|
31
|
+
}
|
|
32
|
+
const open = db.prepare("SELECT 1 FROM bond_slash_proposals WHERE deposit_id = ? AND status = 'proposed' LIMIT 1").get(args.depositId);
|
|
33
|
+
if (open)
|
|
34
|
+
return { ok: false, reason: 'an open slash proposal already exists for this deposit' };
|
|
35
|
+
const days = Math.max(0, args.coolingDays);
|
|
36
|
+
db.prepare(`INSERT INTO bond_slash_proposals (id, deposit_id, seller_id, dispute_id, reason, status, cooling_until, proposed_by)
|
|
37
|
+
VALUES (?,?,?,?,?, 'proposed', datetime('now', ?), ?)`)
|
|
38
|
+
.run(args.proposalId, args.depositId, dep.user_id, args.disputeId, args.reason ? String(args.reason).slice(0, 300) : null, `+${days} days`, args.proposedBy);
|
|
39
|
+
return { ok: true, proposalId: args.proposalId, status: 'proposed' };
|
|
40
|
+
}
|
|
41
|
+
/** admin 撤销提案(执行前任意时刻)。 */
|
|
42
|
+
export function cancelBondSlashProposal(db, args) {
|
|
43
|
+
const row = db.prepare('SELECT status FROM bond_slash_proposals WHERE id = ?').get(args.proposalId);
|
|
44
|
+
if (!row)
|
|
45
|
+
return { ok: false, reason: 'proposal not found' };
|
|
46
|
+
if (row.status === 'cancelled')
|
|
47
|
+
return { ok: true, status: 'cancelled', already: true };
|
|
48
|
+
if (row.status !== 'proposed')
|
|
49
|
+
return { ok: false, reason: `cannot cancel from status '${row.status}'` };
|
|
50
|
+
db.prepare("UPDATE bond_slash_proposals SET status = 'cancelled', cancelled_at = datetime('now'), cancel_note = ? WHERE id = ? AND status = 'proposed'")
|
|
51
|
+
.run(args.note ? String(args.note).slice(0, 300) : null, args.proposalId);
|
|
52
|
+
return { ok: true, status: 'cancelled' };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 执行罚没:提案 proposed + 冷静期已过 → 同一事务内 CAS(proposed→executed)+ slashBond(全额 provenance
|
|
56
|
+
* + status slashed + 资格吊销)。txnId 由调用方生成(审计关联)。
|
|
57
|
+
*/
|
|
58
|
+
export function executeBondSlashProposal(db, args) {
|
|
59
|
+
const row = db.prepare('SELECT id, deposit_id, seller_id, dispute_id, reason, status, cooling_until FROM bond_slash_proposals WHERE id = ?')
|
|
60
|
+
.get(args.proposalId);
|
|
61
|
+
if (!row)
|
|
62
|
+
return { ok: false, reason: 'proposal not found' };
|
|
63
|
+
if (row.status === 'executed')
|
|
64
|
+
return { ok: true, status: 'executed', already: true };
|
|
65
|
+
if (row.status !== 'proposed')
|
|
66
|
+
return { ok: false, reason: `cannot execute from status '${row.status}'` };
|
|
67
|
+
const coolMs = Date.parse(row.cooling_until.includes('T') ? row.cooling_until : row.cooling_until.replace(' ', 'T') + 'Z');
|
|
68
|
+
const nowMs = Date.parse(args.nowIso);
|
|
69
|
+
if (!Number.isFinite(coolMs) || !Number.isFinite(nowMs))
|
|
70
|
+
return { ok: false, reason: 'unparseable cooling timestamps' };
|
|
71
|
+
if (nowMs < coolMs)
|
|
72
|
+
return { ok: false, reason: `cooling window not over (until ${row.cooling_until})` };
|
|
73
|
+
let out = { ok: true, status: 'executed' };
|
|
74
|
+
db.transaction(() => {
|
|
75
|
+
const cas = db.prepare("UPDATE bond_slash_proposals SET status = 'executed', executed_at = datetime('now'), executed_txn_id = ? WHERE id = ? AND status = 'proposed'")
|
|
76
|
+
.run(args.txnId, args.proposalId);
|
|
77
|
+
if (cas.changes !== 1)
|
|
78
|
+
throw new Error('execute race: already processed');
|
|
79
|
+
const s = slashBond(db, { depositId: row.deposit_id, txnId: args.txnId, reason: `bond_slash_proposal:${row.id} dispute:${row.dispute_id}${row.reason ? ` ${row.reason}` : ''}` });
|
|
80
|
+
if (!s.ok)
|
|
81
|
+
throw new Error(s.reason);
|
|
82
|
+
})();
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** 提案列表(admin 队列/卖家告知)。 */
|
|
86
|
+
export function listBondSlashProposals(db, filter = {}) {
|
|
87
|
+
const where = [];
|
|
88
|
+
const params = [];
|
|
89
|
+
if (filter.sellerId) {
|
|
90
|
+
where.push('p.seller_id = ?');
|
|
91
|
+
params.push(filter.sellerId);
|
|
92
|
+
}
|
|
93
|
+
if (filter.status) {
|
|
94
|
+
where.push('p.status = ?');
|
|
95
|
+
params.push(filter.status);
|
|
96
|
+
}
|
|
97
|
+
return db.prepare(`SELECT p.*, u.name AS seller_name, u.handle AS seller_handle
|
|
98
|
+
FROM bond_slash_proposals p JOIN users u ON u.id = p.seller_id
|
|
99
|
+
${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY p.proposed_at DESC LIMIT 200`).all(...params);
|
|
100
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 商家履约保证金条款(B 决策前置,2026-07-05)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么存在:罚没/退还必须有【商家缴纳前显式同意】的合同依据 —— 没有条款文本,罚没在合同法上站不住
|
|
5
|
+
* (SG 语境:罚没条款须像"约定履约损害赔偿"而非"惩罚",penalty clause 可能不可执行,故文本刻意
|
|
6
|
+
* 采用 liquidated-damages 表述并写明用途去向)。条款版本化:缴纳申报时快照 terms_version 进 deposit 行,
|
|
7
|
+
* 新版本只影响之后的缴纳;版本串同时作为 rail-clearance 的 policy_version(可审计对齐)。
|
|
8
|
+
*
|
|
9
|
+
* 变更本文本 = 变更商家合同条款:必须 bump BOND_TERMS.version,且只对新申报生效。
|
|
10
|
+
*/
|
|
11
|
+
export const BOND_TERMS = {
|
|
12
|
+
version: 'bond-terms.v1.2026-07-05',
|
|
13
|
+
zh: `【商家履约保证金条款 v1】
|
|
14
|
+
1. 性质:本保证金是你(商家)向 WebAZ 平台缴纳的【履约担保物】(security deposit),用于担保你在直付(direct_p2p)交易中的履约义务。它不是投资、不是存款、不是买家资金,也不产生利息。
|
|
15
|
+
2. 金额与币种:按平台公示档位(当前 T0 = 500 USDC 等值;固定 token 数,档位参考 ≈ S$500,非实时汇率换算)。可按平台公示的收款账户以 USDC 或对应法币缴纳;跨币种等值由平台运营在核实到账时按公示口径认定。
|
|
16
|
+
3. 生效:缴纳后须经平台运营核实真实到账并确认锁定方才生效;申报本身不授予任何资格。
|
|
17
|
+
4. 退还:你可申请退出并退还保证金。前提是【无任何未了结的直付责任】(在途订单、售后/退货流程、争议、待复核罚没、平台服务费欠费等),并经过平台公示的冷静期;冷静期内你的直付资格暂停,期间可随时撤销申请。退还在协议外原路/约定方式完成并留痕。
|
|
18
|
+
5. 罚没(约定履约损害赔偿):仅当直付交易争议经平台仲裁【裁定你承担责任】后,平台方可发起罚没提案;提案设有公示冷静期(即你的申诉窗口),期满经人工复核后执行。你同意:被执行的罚没金额作为【约定的履约损害赔偿与生态修复金】全额转入平台处罚金专户 —— 该专户只进不出、不用于平台收益、不向个案裁决者发放,用途受治理公示约束。
|
|
19
|
+
6. 链上风险:以 USDC 缴纳时,链、地址、金额由你自行核对;错链/错地址造成的损失由你承担。
|
|
20
|
+
7. 条款版本:本条款版本化。你每次缴纳申报时同意的版本会被记录;条款更新只影响之后的新缴纳。
|
|
21
|
+
8. 你确认:已阅读并理解上述条款,自愿缴纳。`,
|
|
22
|
+
en: `[Merchant Performance Bond Terms v1]
|
|
23
|
+
1. Nature: this bond is a SECURITY DEPOSIT you (the merchant) place with the WebAZ platform to secure your fulfilment obligations in Direct Pay (direct_p2p) trades. It is not an investment, not a deposit account, not buyer funds, and bears no interest.
|
|
24
|
+
2. Amount & currency: per the published tier (currently T0 = 500 USDC equivalent; a fixed token amount — the "≈ S$500" label is a tier reference, not a live FX conversion). Payable in USDC or the corresponding fiat via the platform's published receiving accounts; cross-currency equivalence is determined by platform operations at verification time per the published basis.
|
|
25
|
+
3. Effectiveness: the bond takes effect only after platform operations verify the actual arrival of funds and confirm the lock; a declaration by itself grants nothing.
|
|
26
|
+
4. Refund: you may request exit and refund, provided you have NO outstanding Direct Pay liabilities (open orders, after-sales/return flows, disputes, pending slash reviews, unpaid platform service fees, etc.) and after the published cooling window; your Direct Pay eligibility is suspended during the window and you may cancel the request at any time. The refund is completed off-protocol via the original/agreed channel and recorded.
|
|
27
|
+
5. Slash (agreed liquidated damages): only after a Direct Pay dispute is RULED against you by platform arbitration may the platform open a slash proposal; the proposal carries a published cooling window (your appeal window) and is executed after human review. You agree that a slashed amount, as AGREED LIQUIDATED DAMAGES and ecosystem-remediation funds, is transferred in full to the platform's penalty reserve — inflow-only, never platform profit, never paid to per-case adjudicators, with usage bound by published governance.
|
|
28
|
+
6. On-chain risk: when paying in USDC you are responsible for verifying the chain, address and amount; losses from wrong-chain/wrong-address transfers are yours.
|
|
29
|
+
7. Versioning: these terms are versioned. The version you agree to at each declaration is recorded; updates affect only subsequent deposits.
|
|
30
|
+
8. You confirm you have read and understood these terms and deposit voluntarily.`,
|
|
31
|
+
};
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* Lock B — registry 放行锁(本模块):某 rail 的 legal_cleared + production_ready + 非占位 policy_version +
|
|
11
11
|
* jurisdiction ∈ allowlist。当前【全部默认 fail-closed】→ 全拒。由 assertBondRailCleared 守。
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* 现状(2026-07-05 更新):**operator_attested 轨已放行(SG)** —— Holden 决策 B(经营者知情自担风险,
|
|
14
|
+
* 非律师意见):保证金是商家履约担保物、不涉买家资金/货款;法币收取=常规合同担保物;USDC 收取的 PS Act
|
|
15
|
+
* DPT 定性风险已知并接受(security-deposit 豁免待律师确认,确认前敞口=pre-launch 无真实商家)。前置已建:
|
|
16
|
+
* 条款文本+缴纳前强制同意(src/bond-terms.ts,liquidated-damages 表述;policy_version=条款版本,审计对齐)。
|
|
17
|
+
* usdc_onchain / fiat_psp【自动收款轨】仍全 fail-closed(未实现+未清门,与本次放行无关)。
|
|
17
18
|
*/
|
|
18
19
|
import { getDepositRail } from './deposit-rails.js';
|
|
19
20
|
/** policy_version 占位值(= 未设)。等于此值即视为未放行。 */
|
|
@@ -24,6 +25,10 @@ export const BOND_POLICY_VERSION_PLACEHOLDER = 'pre-legal-unset';
|
|
|
24
25
|
* 全部默认 fail-closed —— 真值由后续【法务清门 + 外审】PR 配置,不在本 scaffold。
|
|
25
26
|
*/
|
|
26
27
|
const BOND_RAIL_CLEARANCE = {
|
|
28
|
+
// ✅ 2026-07-05 放行(Holden 决策 B,经营者知情自担;详见文件头)。policy_version = 条款版本(bond-terms.ts,
|
|
29
|
+
// 缴纳前强制同意)。运营就绪:B1 申报/核实队列 + confirm-production(ROOT+Passkey)+ B2 退还 + B3 罚没全链已建。
|
|
30
|
+
operator_attested: { railId: 'operator_attested', assetCategory: 'usdc_fiat', legalCleared: true, jurisdictionAllowlist: ['SG'], policyVersion: 'bond-terms.v1.2026-07-05', productionReady: true },
|
|
31
|
+
// 自动收款轨:未实现(Lock A 也拒),维持全 fail-closed —— 与 operator_attested 放行无关。
|
|
27
32
|
usdc_onchain: { railId: 'usdc_onchain', assetCategory: 'usdc', legalCleared: false, jurisdictionAllowlist: [], policyVersion: BOND_POLICY_VERSION_PLACEHOLDER, productionReady: false },
|
|
28
33
|
fiat_psp: { railId: 'fiat_psp', assetCategory: 'fiat', legalCleared: false, jurisdictionAllowlist: [], policyVersion: BOND_POLICY_VERSION_PLACEHOLDER, productionReady: false },
|
|
29
34
|
};
|
|
@@ -33,7 +38,7 @@ export function getBondRailClearance(railId) {
|
|
|
33
38
|
}
|
|
34
39
|
/**
|
|
35
40
|
* 该 rail 在该 jurisdiction 下是否【可用于生产 base-bond】= Lock A(真实实现)且 Lock B(registry 放行)全过。
|
|
36
|
-
*
|
|
41
|
+
* jurisdiction 语义=【平台收款主体法域】(非卖家法域)。2026-07-05 起 operator_attested/SG 为 true(#240)。纯读,无副作用。
|
|
37
42
|
*/
|
|
38
43
|
export function isBondRailClearedForProduction(railId, jurisdiction) {
|
|
39
44
|
const c = getBondRailClearance(railId);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { restorePreShipDirectPayStock } from './direct-pay-stock.js'; // 库存回补唯一入口(pre-ship 放行;已出库拒绝,走退货验收)
|
|
2
|
+
export function initDirectPayCancelRefundSchema(db) {
|
|
3
|
+
db.exec(`
|
|
4
|
+
CREATE TABLE IF NOT EXISTS direct_pay_cancel_requests (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
order_id TEXT NOT NULL,
|
|
7
|
+
buyer_id TEXT NOT NULL,
|
|
8
|
+
seller_id TEXT NOT NULL,
|
|
9
|
+
status TEXT NOT NULL DEFAULT 'requested' CHECK (status IN ('requested','refund_marked','settled','declined','withdrawn')),
|
|
10
|
+
reason TEXT,
|
|
11
|
+
refund_reference TEXT,
|
|
12
|
+
respond_deadline TEXT NOT NULL,
|
|
13
|
+
seller_responded_at TEXT,
|
|
14
|
+
settled_at TEXT,
|
|
15
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
16
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_dpcr_order ON direct_pay_cancel_requests(order_id);
|
|
19
|
+
`);
|
|
20
|
+
}
|
|
21
|
+
function loadOrder(db, orderId) {
|
|
22
|
+
return db.prepare('SELECT id, buyer_id, seller_id, status, payment_rail, product_id, quantity FROM orders WHERE id = ?').get(orderId);
|
|
23
|
+
}
|
|
24
|
+
/** 最新一条请求(任意状态)。 */
|
|
25
|
+
function latestRequest(db, orderId) {
|
|
26
|
+
return db.prepare('SELECT * FROM direct_pay_cancel_requests WHERE order_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1').get(orderId);
|
|
27
|
+
}
|
|
28
|
+
/** requested 且 respond_deadline 已过 → 视为 expired(lazy,不写库;可重新 request)。 */
|
|
29
|
+
function effectiveStatus(db, r) {
|
|
30
|
+
if (r.status === 'requested') {
|
|
31
|
+
const over = db.prepare("SELECT datetime(?) < datetime('now') AS o").get(r.respond_deadline);
|
|
32
|
+
if (over.o)
|
|
33
|
+
return 'expired';
|
|
34
|
+
}
|
|
35
|
+
return r.status;
|
|
36
|
+
}
|
|
37
|
+
function respondDays(db) {
|
|
38
|
+
try {
|
|
39
|
+
const p = db.prepare("SELECT value FROM protocol_params WHERE key = 'direct_pay.cancel_refund_respond_days'").get();
|
|
40
|
+
if (p)
|
|
41
|
+
return Math.max(1, Number(p.value) || 5);
|
|
42
|
+
}
|
|
43
|
+
catch { /* 表缺失 → 默认 */ }
|
|
44
|
+
return 5;
|
|
45
|
+
}
|
|
46
|
+
/** ① 买家发起取消退款请求。 */
|
|
47
|
+
export function requestCancelRefund(db, args) {
|
|
48
|
+
const order = loadOrder(db, args.orderId);
|
|
49
|
+
if (!order)
|
|
50
|
+
return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
|
|
51
|
+
if (order.buyer_id !== args.buyerId)
|
|
52
|
+
return { ok: false, error: '只有订单买家可发起取消退款', error_code: 'NOT_ORDER_BUYER' };
|
|
53
|
+
if (order.payment_rail !== 'direct_p2p')
|
|
54
|
+
return { ok: false, error: '仅直付订单适用取消退款握手(托管单走托管退款)', error_code: 'NOT_DIRECT_PAY' };
|
|
55
|
+
if (order.status !== 'accepted')
|
|
56
|
+
return { ok: false, error: '仅付款后、发货前(accepted)可申请取消退款', error_code: 'ORDER_NOT_ACCEPTED' };
|
|
57
|
+
const last = latestRequest(db, args.orderId);
|
|
58
|
+
if (last && ['requested', 'refund_marked'].includes(effectiveStatus(db, last)))
|
|
59
|
+
return { ok: false, error: '已有进行中的取消退款请求', error_code: 'REQUEST_ALREADY_OPEN' };
|
|
60
|
+
const n = db.prepare('SELECT COUNT(*) AS n FROM direct_pay_cancel_requests WHERE order_id = ?').get(args.orderId).n;
|
|
61
|
+
if (n >= 3)
|
|
62
|
+
return { ok: false, error: '本订单取消退款请求次数已达上限(3),请与卖家协商或发起争议', error_code: 'REQUEST_CAP_REACHED' };
|
|
63
|
+
const reason = typeof args.reason === 'string' ? args.reason.trim().slice(0, 200) : null;
|
|
64
|
+
db.prepare(`INSERT INTO direct_pay_cancel_requests (id, order_id, buyer_id, seller_id, reason, respond_deadline)
|
|
65
|
+
VALUES (?, ?, ?, ?, ?, datetime('now', ?))`)
|
|
66
|
+
.run(args.requestId, args.orderId, order.buyer_id, order.seller_id, reason, `+${respondDays(db)} days`);
|
|
67
|
+
return { ok: true, request: latestRequest(db, args.orderId), status: 'requested' };
|
|
68
|
+
}
|
|
69
|
+
/** ② a. 卖家拒绝(如已备货)→ 履约继续。 */
|
|
70
|
+
export function declineCancelRefund(db, args) {
|
|
71
|
+
const order = loadOrder(db, args.orderId);
|
|
72
|
+
if (!order)
|
|
73
|
+
return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
|
|
74
|
+
if (order.seller_id !== args.sellerId)
|
|
75
|
+
return { ok: false, error: '只有订单卖家可拒绝', error_code: 'NOT_ORDER_SELLER' };
|
|
76
|
+
const r = db.prepare(`UPDATE direct_pay_cancel_requests SET status = 'declined', seller_responded_at = datetime('now'), updated_at = datetime('now')
|
|
77
|
+
WHERE order_id = ? AND status = 'requested'`).run(args.orderId);
|
|
78
|
+
if (r.changes !== 1)
|
|
79
|
+
return { ok: false, error: '没有待响应的取消退款请求', error_code: 'NO_OPEN_REQUEST' };
|
|
80
|
+
return { ok: true, status: 'declined' };
|
|
81
|
+
}
|
|
82
|
+
/** ② b. 卖家声明已场外退款(可附退款参考)。此后买家只能 confirm 或走争议(不可 withdraw)。 */
|
|
83
|
+
export function markRefunded(db, args) {
|
|
84
|
+
const order = loadOrder(db, args.orderId);
|
|
85
|
+
if (!order)
|
|
86
|
+
return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
|
|
87
|
+
if (order.seller_id !== args.sellerId)
|
|
88
|
+
return { ok: false, error: '只有订单卖家可声明退款', error_code: 'NOT_ORDER_SELLER' };
|
|
89
|
+
if (order.status !== 'accepted')
|
|
90
|
+
return { ok: false, error: '订单已不在待发货阶段,握手已失效', error_code: 'ORDER_NOT_ACCEPTED' };
|
|
91
|
+
const ref = typeof args.refundReference === 'string' ? args.refundReference.trim().slice(0, 200) : null;
|
|
92
|
+
const r = db.prepare(`UPDATE direct_pay_cancel_requests SET status = 'refund_marked', refund_reference = ?, seller_responded_at = datetime('now'), updated_at = datetime('now')
|
|
93
|
+
WHERE order_id = ? AND status = 'requested'`).run(ref, args.orderId);
|
|
94
|
+
if (r.changes !== 1)
|
|
95
|
+
return { ok: false, error: '没有待响应的取消退款请求', error_code: 'NO_OPEN_REQUEST' };
|
|
96
|
+
return { ok: true, status: 'refund_marked' };
|
|
97
|
+
}
|
|
98
|
+
/** 买家撤回 —— 仅限卖家尚未响应(requested)。卖家已 mark_refunded 后禁撤(防"收了退款还让订单继续履约")。 */
|
|
99
|
+
export function withdrawCancelRefund(db, args) {
|
|
100
|
+
const order = loadOrder(db, args.orderId);
|
|
101
|
+
if (!order)
|
|
102
|
+
return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
|
|
103
|
+
if (order.buyer_id !== args.buyerId)
|
|
104
|
+
return { ok: false, error: '只有订单买家可撤回', error_code: 'NOT_ORDER_BUYER' };
|
|
105
|
+
const r = db.prepare(`UPDATE direct_pay_cancel_requests SET status = 'withdrawn', updated_at = datetime('now')
|
|
106
|
+
WHERE order_id = ? AND status = 'requested'`).run(args.orderId);
|
|
107
|
+
if (r.changes !== 1)
|
|
108
|
+
return { ok: false, error: '没有可撤回的请求(卖家已声明退款后不可撤回,请确认收款或发起争议)', error_code: 'NO_OPEN_REQUEST' };
|
|
109
|
+
return { ok: true, status: 'withdrawn' };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* ③ 买家确认已收到场外退款 → settled + accepted→cancelled(system)+ 恢复库存。
|
|
113
|
+
* 【必须由路由 db.transaction 包裹】;transition 依赖注入(状态机 adapter,便于单测与防循环依赖)。
|
|
114
|
+
*/
|
|
115
|
+
export function confirmRefundReceived(db, args, transition) {
|
|
116
|
+
const order = loadOrder(db, args.orderId);
|
|
117
|
+
if (!order)
|
|
118
|
+
return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
|
|
119
|
+
if (order.buyer_id !== args.buyerId)
|
|
120
|
+
return { ok: false, error: '只有订单买家可确认收到退款', error_code: 'NOT_ORDER_BUYER' };
|
|
121
|
+
// 竞态重校验:卖家已发货/已争议 → 握手失效,绝不在非 accepted 状态上关单
|
|
122
|
+
if (order.status !== 'accepted')
|
|
123
|
+
return { ok: false, error: '订单已不在待发货阶段(可能已发货/争议),握手已失效', error_code: 'ORDER_NOT_ACCEPTED' };
|
|
124
|
+
const sys = db.prepare("SELECT id FROM users WHERE role = 'system' LIMIT 1").get();
|
|
125
|
+
if (!sys)
|
|
126
|
+
return { ok: false, error: '系统账号缺失', error_code: 'SYS_MISSING' };
|
|
127
|
+
// 请求行 CAS:仅 refund_marked → settled(卖家未声明退款前买家不可单方确认关单)
|
|
128
|
+
const cas = db.prepare(`UPDATE direct_pay_cancel_requests SET status = 'settled', settled_at = datetime('now'), updated_at = datetime('now')
|
|
129
|
+
WHERE order_id = ? AND status = 'refund_marked'`).run(args.orderId);
|
|
130
|
+
if (cas.changes !== 1)
|
|
131
|
+
return { ok: false, error: '卖家尚未声明退款,不可确认', error_code: 'REFUND_NOT_MARKED' };
|
|
132
|
+
const t = transition(db, args.orderId, 'cancelled', sys.id, [], '直付取消退款握手:卖家已场外退款,买家确认收到 → 无责取消');
|
|
133
|
+
if (!t.success)
|
|
134
|
+
throw new Error(t.error || 'TRANSITION_FAILED'); // 抛错让路由 db.transaction 整体回滚(含上面 CAS)
|
|
135
|
+
// 库存回补:经唯一守卫入口(accepted=已付未发,A 类放行;已出库来源在入口被拒 —— 见 direct-pay-stock.ts 情形矩阵)
|
|
136
|
+
restorePreShipDirectPayStock(db, { fromStatus: 'accepted', productId: order.product_id, quantity: Number(order.quantity) || 1 });
|
|
137
|
+
return { ok: true, status: 'cancelled' };
|
|
138
|
+
}
|
|
139
|
+
/** 状态读取(party-gated):非当事方一律 NOT_A_PARTY,不泄露请求存在性。 */
|
|
140
|
+
export function getCancelRefundState(db, orderId, viewerId) {
|
|
141
|
+
const order = loadOrder(db, orderId);
|
|
142
|
+
if (!order)
|
|
143
|
+
return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
|
|
144
|
+
const isBuyer = order.buyer_id === viewerId;
|
|
145
|
+
const isSeller = order.seller_id === viewerId;
|
|
146
|
+
if (!isBuyer && !isSeller)
|
|
147
|
+
return { ok: false, error: '仅订单当事方可查看', error_code: 'NOT_A_PARTY' };
|
|
148
|
+
const last = latestRequest(db, orderId);
|
|
149
|
+
const eff = last ? effectiveStatus(db, last) : null;
|
|
150
|
+
const open = eff === 'requested' || eff === 'refund_marked';
|
|
151
|
+
const req = last ? { ...last, status: eff, stale: open && order.status !== 'accepted' } : null;
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
request: req ?? undefined,
|
|
155
|
+
can_request: isBuyer && order.payment_rail === 'direct_p2p' && order.status === 'accepted' && !open,
|
|
156
|
+
can_respond: isSeller && eff === 'requested' && order.status === 'accepted',
|
|
157
|
+
can_confirm: isBuyer && eff === 'refund_marked' && order.status === 'accepted',
|
|
158
|
+
can_withdraw: isBuyer && eff === 'requested',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -8,6 +8,27 @@ import { enforceCollateralExposureGate } from './merchant-bond-exposure.js'; //
|
|
|
8
8
|
import { productStoreVerified } from './product-verification.js';
|
|
9
9
|
import { sellerExemptFromPerProduct } from './store-verification.js';
|
|
10
10
|
import { safeRunDirectPayAmlMonitor } from './direct-pay-aml-monitor.js';
|
|
11
|
+
import { getUsdRatesSync, convertUsdcToLocal, SUPPORTED_CURRENCIES } from './fx-rates.js'; // 审计项 E:建单冻结应付参考换算(display-only,同步缓存)
|
|
12
|
+
import { createNotification } from './layer2-business/L2-6-notifications/notification-engine.js'; // 审计项 B:直付转移此前通知黑洞(卖家不知有单/已付款)
|
|
13
|
+
import { buildTradeTermsSnapshot, writeTradeTermsSnapshot } from './trade-terms.js'; // S0 交易条款快照(证据,fail-soft 零钱路)
|
|
14
|
+
/** 应付参考换算快照(审计项 E):同步缓存 FX(display-only,fallback 亦可,永不抛/永不阻塞建单)。
|
|
15
|
+
* USD/USDC/未知币种 → 只记 payable_usdc(前端回落仅 USDC);换算失败同样只记 usdc,建单照常。 */
|
|
16
|
+
export function buildPayableSnapshot(totalUsdc, accountCurrency) {
|
|
17
|
+
const base = { payable_usdc: totalUsdc };
|
|
18
|
+
const cur = String(accountCurrency || '').toUpperCase();
|
|
19
|
+
if (!cur || cur === 'USD' || cur === 'USDC' || !SUPPORTED_CURRENCIES.includes(cur))
|
|
20
|
+
return base;
|
|
21
|
+
try {
|
|
22
|
+
const snap = getUsdRatesSync();
|
|
23
|
+
const approx = convertUsdcToLocal(totalUsdc, cur, snap.rates);
|
|
24
|
+
if (!Number.isFinite(approx))
|
|
25
|
+
return base;
|
|
26
|
+
return { ...base, payable_approx: Math.round(approx * 100) / 100, payable_currency: cur, payable_rate: snap.rates[cur], payable_asof: snap.as_of, payable_stale: snap.stale };
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return base;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
11
32
|
/** 原子创建 direct_p2p 订单。成功返回 { orderId };任一步失败抛错(调用方回 409,事务已回滚)。 */
|
|
12
33
|
export function createDirectPayOrder(db, deps, args) {
|
|
13
34
|
const { generateId, transition, appendOrderEvent } = deps;
|
|
@@ -15,26 +36,38 @@ export function createDirectPayOrder(db, deps, args) {
|
|
|
15
36
|
db.transaction(() => {
|
|
16
37
|
// 本金不入协议:escrow_amount=0,不写 buyer wallet。同一 INSERT 写入【入口控制 policy 快照】(frozen-at-create)。
|
|
17
38
|
const s = args.snapshot;
|
|
39
|
+
const quoteRequired = !!args.shipping?.quoteRequired;
|
|
40
|
+
const manual = args.acceptMode === 'manual' || quoteRequired; // 询价单必须先接单报价(无论接单模式)
|
|
41
|
+
// manual:不设 window deadline(接单时才起表),记 accept_mode 快照 + 接单截止。
|
|
18
42
|
db.prepare(`INSERT INTO orders (id, product_id, buyer_id, seller_id, quantity, unit_price, total_amount, escrow_amount,
|
|
19
43
|
status, payment_rail, shipping_address, direct_pay_instruction_snapshot, direct_pay_account_snapshot, direct_pay_window_deadline,
|
|
44
|
+
accept_mode_snapshot, pending_accept_deadline, ship_to_region, shipping_fee, shipping_est_days, shipping_quote_required,
|
|
20
45
|
direct_pay_enabled_snapshot, direct_pay_rail_breaker_snapshot, direct_pay_region_snapshot,
|
|
21
46
|
direct_pay_region_allowlist_snapshot, direct_pay_per_tx_cap_units_snapshot, direct_pay_seller_breaker_snapshot, direct_pay_decision_code)
|
|
22
|
-
VALUES (?,?,?,?,?,?,?,0,'created','direct_p2p'
|
|
23
|
-
.run(orderId, args.productId, args.buyerId, args.sellerId, args.quantity, args.unitPrice, args.totalAmount, args.shippingAddress, args.instructionSnapshot, args.accountSnapshot ? JSON.stringify(args.accountSnapshot) : null, args.windowDeadlineIso, s.enabled ? 1 : 0, s.railBreakerTripped ? 1 : 0, s.region, JSON.stringify(s.regionAllowlist), s.perTxCapUnits, s.sellerBreakerTripped ? 1 : 0, s.decisionCode);
|
|
47
|
+
VALUES (?,?,?,?,?,?,?,0,'created','direct_p2p',?,?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?)`)
|
|
48
|
+
.run(orderId, args.productId, args.buyerId, args.sellerId, args.quantity, args.unitPrice, args.totalAmount, args.shippingAddress, args.instructionSnapshot, args.accountSnapshot ? JSON.stringify(args.accountSnapshot) : null, manual ? null : args.windowDeadlineIso, args.acceptMode, manual ? (args.pendingAcceptDeadlineIso ?? null) : null, args.shipping?.region ?? null, (args.shipping && (args.shipping.fee > 0 || args.shipping.region)) ? args.shipping.fee : null, args.shipping?.estDays ?? null, quoteRequired ? 1 : null, s.enabled ? 1 : 0, s.railBreakerTripped ? 1 : 0, s.region, JSON.stringify(s.regionAllowlist), s.perTxCapUnits, s.sellerBreakerTripped ? 1 : 0, s.decisionCode);
|
|
24
49
|
appendOrderEvent(db, {
|
|
25
50
|
orderId, eventType: 'open', fromStatus: null, toStatus: 'created', actorId: args.buyerId, actorRole: 'buyer',
|
|
26
51
|
extra: { product_id: args.productId, seller_id: args.sellerId, quantity: args.quantity, total_amount: args.totalAmount, payment_rail: 'direct_p2p' },
|
|
27
52
|
});
|
|
28
|
-
// created → direct_pay_window(
|
|
29
|
-
const rc =
|
|
53
|
+
// created → direct_pay_window(auto)| pending_accept(manual,v16:卖家确认接单前不开付款窗口)。失败回滚。
|
|
54
|
+
const rc = manual
|
|
55
|
+
? transition(db, orderId, 'pending_accept', 'sys_protocol', [], '手动接单:等待卖家确认接单(付款前,不展示收款信息)')
|
|
56
|
+
: transition(db, orderId, 'direct_pay_window', 'sys_protocol', [], 'Rail1 直付:进入付款窗口(平台费完成时记应收,建单不收费)');
|
|
30
57
|
if (!rc.success)
|
|
31
|
-
throw new Error(rc.error || '
|
|
58
|
+
throw new Error(rc.error || `transition→${manual ? 'pending_accept' : 'direct_pay_window'} failed`);
|
|
32
59
|
// 【无建单资金写】平台费完成时 accrue;建单门是【首单宽限 + 预充值续用】只读门(在 createDirectPayResponse 内,建单前)。
|
|
33
60
|
// 扣库存(原子;售罄即抛回滚)。变体/flash 直付 v1 不支持。
|
|
34
61
|
const upd = db.prepare('UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?').run(args.quantity, args.productId, args.quantity);
|
|
35
62
|
if (upd.changes !== 1)
|
|
36
63
|
throw new Error('stock depleted');
|
|
37
64
|
})();
|
|
65
|
+
// S0 条款快照:冻结下单时的时效/退货/清关/税责声明(事务外 fail-soft —— 快照是证据非计价输入,缺失=pre-S0 同待遇)
|
|
66
|
+
writeTradeTermsSnapshot(db, orderId, buildTradeTermsSnapshot(db, {
|
|
67
|
+
productId: args.productId, sellerId: args.sellerId,
|
|
68
|
+
shipping: { source: args.shipping?.quoteRequired ? 'quote_pending' : (args.shipping?.region ? 'template' : 'none'), region: args.shipping?.region ?? null, fee: args.shipping?.quoteRequired ? null : ((args.shipping && (args.shipping.fee > 0 || args.shipping.region)) ? args.shipping.fee : null), estDays: args.shipping?.estDays ?? null, freeThresholdApplied: args.shipping?.freeThresholdApplied },
|
|
69
|
+
acceptModeEffective: args.acceptMode,
|
|
70
|
+
}));
|
|
38
71
|
return { orderId };
|
|
39
72
|
}
|
|
40
73
|
/**
|
|
@@ -94,7 +127,7 @@ export function createDirectPayResponse(res, db, deps, ctx) {
|
|
|
94
127
|
return;
|
|
95
128
|
}
|
|
96
129
|
instructionSnapshot = acc.instruction;
|
|
97
|
-
accountSnapshot = { account_id: acc.id, method: acc.method, currency: acc.currency, label: acc.label, qr_ref: acc.qr_image_ref };
|
|
130
|
+
accountSnapshot = { account_id: acc.id, method: acc.method, currency: acc.currency, label: acc.label, qr_ref: acc.qr_image_ref, ...buildPayableSnapshot(ctx.totalAmount, acc.currency) };
|
|
98
131
|
}
|
|
99
132
|
else {
|
|
100
133
|
const instr = getActivePaymentInstruction(db, sellerId);
|
|
@@ -113,6 +146,15 @@ export function createDirectPayResponse(res, db, deps, ctx) {
|
|
|
113
146
|
res.status(409).json({ error_code: coarsenBuyerFacingDirectPayCode(quota.code), error: '该卖家暂不支持直付' });
|
|
114
147
|
return;
|
|
115
148
|
}
|
|
149
|
+
// 审计项 G:单买家·单卖家在途直付单上限(param direct_pay.max_open_per_buyer_seller,默认 5)——
|
|
150
|
+
// 防单个买家刷单锁库存(建单即扣库存,窗口+宽限可占 ~52h)+ 耗尽卖家缓交额度的 griefing。只读门,任何写之前;
|
|
151
|
+
// 买家自身行为所致 → 精确 code 直接透出(非卖家隐私,无需脱敏)。
|
|
152
|
+
const openCap = Math.max(1, Number(deps.getProtocolParam('direct_pay.max_open_per_buyer_seller', 5)) || 5);
|
|
153
|
+
const openN = db.prepare(`SELECT COUNT(*) n FROM orders WHERE buyer_id = ? AND seller_id = ? AND payment_rail = 'direct_p2p' AND status IN ('pending_accept','direct_pay_window','direct_expired_unconfirmed','accepted','payment_query')`).get(ctx.buyerId, sellerId).n;
|
|
154
|
+
if (openN >= openCap) {
|
|
155
|
+
res.status(429).json({ error_code: 'DIRECT_PAY_TOO_MANY_OPEN', error: `你在该卖家已有 ${openN} 笔进行中的直付订单(上限 ${openCap}),请先完成或取消后再下单` });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
116
158
|
// §6.5 抵押背书开放敞口上限(backend create-gate)。休眠安全:无真实链上抵押(collateral=0)→ N/A;
|
|
117
159
|
// 有抵押才读 exposure_factor_bps(fail-closed)+ 比较 open_exposure+new ≤ collateral×bps/10000。
|
|
118
160
|
// 当前 merchant_bond 关闭、无 active 存款 → 对缓交卖家零影响(返回 ok)。买家面脱敏。
|
|
@@ -137,23 +179,50 @@ export function createDirectPayResponse(res, db, deps, ctx) {
|
|
|
137
179
|
return;
|
|
138
180
|
}
|
|
139
181
|
const windowHours = deps.getProtocolParam('direct_pay.payment_window_hours', 4);
|
|
182
|
+
// 手动接单模式(v16):单品覆盖 ?? 店铺默认 ?? 'auto'。快照进订单(卖家事后改不影响在途单)。
|
|
183
|
+
// 'manual' → 先进 pending_accept:不开付款窗口、不展示收款信息(时序门=非托管唯一正确的付款风控)。
|
|
184
|
+
const storeMode = db.prepare('SELECT store_accept_mode FROM users WHERE id = ?').get(sellerId)?.store_accept_mode;
|
|
185
|
+
const rawMode = ctx.product.accept_mode ?? storeMode;
|
|
186
|
+
const acceptMode = rawMode === 'manual' ? 'manual' : 'auto';
|
|
187
|
+
const acceptWindowHours = Math.max(1, Number(deps.getProtocolParam('direct_pay.accept_window_hours', 24)) || 24);
|
|
140
188
|
try {
|
|
141
189
|
const { orderId } = createDirectPayOrder(db, deps, {
|
|
142
190
|
productId: ctx.product.id, sellerId, buyerId: ctx.buyerId, quantity: ctx.reqQty,
|
|
143
191
|
unitPrice: ctx.basePrice, totalAmount: ctx.totalAmount,
|
|
144
192
|
instructionSnapshot, accountSnapshot, windowDeadlineIso: new Date(Date.now() + windowHours * 3600_000).toISOString(),
|
|
145
|
-
shippingAddress: ctx.shippingAddress,
|
|
193
|
+
shippingAddress: ctx.shippingAddress, acceptMode, shipping: ctx.shipping,
|
|
194
|
+
pendingAcceptDeadlineIso: (acceptMode === 'manual' || ctx.shipping?.quoteRequired) ? new Date(Date.now() + acceptWindowHours * 3600_000).toISOString() : undefined,
|
|
146
195
|
// frozen-at-create policy 快照:control 全过(ctrl.ok)才到此,decisionCode='OK'。
|
|
147
196
|
snapshot: { enabled: cfg.enabled, railBreakerTripped: cfg.railBreakerTripped, region: cfg.region, regionAllowlist: cfg.regionAllowlist, perTxCapUnits: cfg.perTxCapUnits, sellerBreakerTripped, decisionCode: 'OK' },
|
|
148
197
|
});
|
|
149
198
|
// PR-6C: 建单事务已提交后,运行【append-only AML 监控】(fail-soft;命中治理阈值即 append aml_flags,仅影响【后续】
|
|
150
199
|
// create/availability 的 #107 breaker)。safe 包装吞异常 → 监控失败【绝不】回流成建单失败,也不碰当前订单。
|
|
151
200
|
safeRunDirectPayAmlMonitor(db, { sellerId, orderId, nowIso: new Date().toISOString(), getProtocolParam: deps.getProtocolParam });
|
|
201
|
+
// 审计项 B(N2):通知卖家。manual → 待接单提醒(带截止);auto → 原"等买家付款"。fail-soft 不阻断建单响应。
|
|
202
|
+
try {
|
|
203
|
+
if (ctx.shipping?.quoteRequired) {
|
|
204
|
+
createNotification(db, sellerId, orderId, 'direct_pay_quote_needed', '🛎️ 新直付订单(模板外地区),待你报价运费', `商品「${String(ctx.product.title || '').slice(0, 40)}」× ${ctx.reqQty},货款 ${ctx.totalAmount} USDC,收货地区 ${ctx.shipping.region ?? '-'} 不在你的运费模板内。请在 ${acceptWindowHours} 小时内核实可达并报价(运费+预计时效),买家确认后才进入付款;超时订单自动取消。`, { templateKey: 'dp_quote_needed', params: { product: String(ctx.product.title || '').slice(0, 40), qty: ctx.reqQty, amount: ctx.totalAmount, region: ctx.shipping.region ?? '-', hours: acceptWindowHours } });
|
|
205
|
+
}
|
|
206
|
+
else if (acceptMode === 'manual') {
|
|
207
|
+
createNotification(db, sellerId, orderId, 'direct_pay_pending_accept', '🛎️ 新直付订单,待你确认接单', `商品「${String(ctx.product.title || '').slice(0, 40)}」× ${ctx.reqQty},应付 ${ctx.totalAmount} USDC。请在 ${acceptWindowHours} 小时内确认接单(核实可发货/物流),超时订单自动取消;接单后买家才会看到收款方式。`, { templateKey: 'dp_pending_accept_new', params: { product: String(ctx.product.title || '').slice(0, 40), qty: ctx.reqQty, amount: ctx.totalAmount, hours: acceptWindowHours } });
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
createNotification(db, sellerId, orderId, 'direct_pay_order_created', '🛒 新直付订单,等买家付款', `商品「${String(ctx.product.title || '').slice(0, 40)}」× ${ctx.reqQty},应付 ${ctx.totalAmount} USDC。买家完成场外付款并标记后你会收到发货提醒。`, { templateKey: 'dp_new_order', params: { product: String(ctx.product.title || '').slice(0, 40), qty: ctx.reqQty, amount: ctx.totalAmount } });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch { /* 通知失败不阻断 */ }
|
|
152
214
|
// ⚠️ 不在 create 响应里下发卖家收款说明(payment_instruction/label)——D1/D2 both-acked 前不得泄露(响应契约门,
|
|
153
215
|
// 非仅 UI 软门)。买家先完成披露 ack,再经 GET /orders/:id 读取 redaction-gated 的 direct_pay_instruction_snapshot。
|
|
216
|
+
// manual 单更进一步:pending_accept 阶段【状态门】也挡收款信息(orders-read),卖家接单后才可见。
|
|
217
|
+
const _pending = acceptMode === 'manual' || !!ctx.shipping?.quoteRequired;
|
|
154
218
|
res.json({
|
|
155
|
-
success: true, order_id: orderId, status: 'direct_pay_window', payment_rail: 'direct_p2p',
|
|
156
|
-
|
|
219
|
+
success: true, order_id: orderId, status: _pending ? 'pending_accept' : 'direct_pay_window', payment_rail: 'direct_p2p',
|
|
220
|
+
shipping_quote_required: ctx.shipping?.quoteRequired || undefined,
|
|
221
|
+
note: ctx.shipping?.quoteRequired
|
|
222
|
+
? '收货地区在卖家运费模板之外:卖家将核实可达性并报价运费/时效,你确认新总额后才进入付款环节;超时未报价将自动取消,你无需任何操作。'
|
|
223
|
+
: _pending
|
|
224
|
+
? '本单为手动接单:卖家确认可发货并接单后,你才会看到收款方式并进入付款环节;超时未接单将自动取消,你无需任何操作。'
|
|
225
|
+
: '本金不经 WebAZ;完成 D1/D2 风险确认(Passkey)后即可在订单页查看卖家收款说明,请【场外】付款后点"我已付款"。本档无经济保障、不退款,仅对卖家信誉处罚。',
|
|
157
226
|
});
|
|
158
227
|
}
|
|
159
228
|
catch (e) {
|
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
export const STAGE = { PRE_SELECT: 'pre_select', PRE_CONFIRM: 'pre_confirm' };
|
|
2
|
-
/** D1 = PRE_SELECT 提醒(买家单视角,无任何卖家机制)。 */
|
|
2
|
+
/** D1 = PRE_SELECT 提醒(买家单视角,无任何卖家机制)。措辞准确化:强调"无退款能力(从不托管本金)"而非"拒绝退款"。 */
|
|
3
3
|
export const D1 = {
|
|
4
4
|
stage: STAGE.PRE_SELECT,
|
|
5
|
-
version: 'd1.
|
|
6
|
-
zh: '
|
|
7
|
-
en: 'This
|
|
5
|
+
version: 'd1.v2.2026-07-04',
|
|
6
|
+
zh: '本次订单为非担保交易:你直接付款给卖家(场外),WebAZ 不托管本金、不能承诺退款或任何经济保障,仅对卖家有信誉处罚权。请自行判断后决定是否付款。',
|
|
7
|
+
en: 'This is a non-guaranteed transaction: you pay the seller directly (off-platform). WebAZ does not custody the funds and cannot promise refunds or any economic protection — only reputation penalties against the seller. Decide for yourself whether to pay.',
|
|
8
8
|
};
|
|
9
|
-
/** D2 = PRE_CONFIRM 提醒(paid-but-timeout + 最终风险再确认)。 */
|
|
9
|
+
/** D2 = PRE_CONFIRM 提醒(paid-but-timeout + 最终风险再确认)。措辞准确化同 D1。 */
|
|
10
10
|
export const D2 = {
|
|
11
11
|
stage: STAGE.PRE_CONFIRM,
|
|
12
|
-
version: 'd2.
|
|
13
|
-
zh: "
|
|
14
|
-
en: "
|
|
12
|
+
version: 'd2.v2.2026-07-04',
|
|
13
|
+
zh: "付款后请立即回来点'我已付款'。若付款窗口过期,你需自行发起争议(证据裁决,仅影响卖家信誉)。WebAZ 无退款能力(从不托管本金)。确认已了解后再付款。",
|
|
14
|
+
en: "After paying, come back and tap 'I have paid' right away. If the payment window lapses, you must raise a dispute yourself (evidence-based ruling; affects only the seller's reputation). WebAZ has no refund capability (it never custodies the funds). Confirm you understand before paying.",
|
|
15
15
|
};
|
|
16
16
|
const STAGE_VERSION = { [STAGE.PRE_SELECT]: D1.version, [STAGE.PRE_CONFIRM]: D2.version };
|
|
17
17
|
/** 买家面披露载荷(只含买家自己的部分)。 */
|
|
18
18
|
export function getBuyerDisclosures() {
|
|
19
19
|
return { preSelect: D1, preConfirm: D2 };
|
|
20
20
|
}
|
|
21
|
-
/** 记录一次提醒 ack(
|
|
21
|
+
/** 记录一次提醒 ack(每单每 stage 一行;同版本重复 ack 幂等不刷时间)。stage 决定 notice_version。
|
|
22
|
+
* 版本升级可刷新(审计 P0 修):披露文本 bump 后旧版本 ack 失效(disclosureStageAcked 按当前版本判),此前
|
|
23
|
+
* INSERT OR IGNORE 会让重新确认【静默 no-op】(行永远停在旧版本)→ POST 返回 ok 但 requireBothDisclosuresAcked
|
|
24
|
+
* 永远 403,在途单不可恢复地变砖。现改 upsert:版本不同才覆盖(notice_version+acked_at 同步刷新为新确认时刻)。 */
|
|
22
25
|
export function recordDisclosureAck(db, args) {
|
|
23
|
-
db.prepare(`INSERT
|
|
24
|
-
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
26
|
+
db.prepare(`INSERT INTO direct_pay_disclosure_acks (id, order_id, buyer_id, stage, notice_version, acked_at)
|
|
27
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
28
|
+
ON CONFLICT(order_id, stage) DO UPDATE SET notice_version = excluded.notice_version, acked_at = excluded.acked_at
|
|
29
|
+
WHERE excluded.notice_version != direct_pay_disclosure_acks.notice_version`)
|
|
30
|
+
.run(args.ackId, args.orderId, args.buyerId, args.stage, STAGE_VERSION[args.stage]);
|
|
25
31
|
}
|
|
26
32
|
/** 某 stage 是否已 ack(且版本为当前)。 */
|
|
27
33
|
export function disclosureStageAcked(db, orderId, stage) {
|
|
@@ -8,9 +8,9 @@ export function feeUnitsForOrder(totalAmountU, source) {
|
|
|
8
8
|
}
|
|
9
9
|
/** 开放(未完全关闭、仍会 accrue 费)的 direct_p2p 单状态。完成单已在 receivables(借记预充值),故【不含】completed/终态。 */
|
|
10
10
|
export const OPEN_FEE_ACCRUING_STATUSES = [
|
|
11
|
-
'created', 'direct_pay_window', 'direct_expired_unconfirmed',
|
|
11
|
+
'created', 'pending_accept', 'direct_pay_window', 'direct_expired_unconfirmed',
|
|
12
12
|
'accepted', 'shipped', 'picked_up', 'in_transit', 'delivered', 'confirmed', 'disputed',
|
|
13
|
-
];
|
|
13
|
+
]; // pending_accept(手动接单待确认)保守计入在途:未接单也占预估费口径,防开一堆待接单绕过预充值门
|
|
14
14
|
/** 某 SUM 查询结果 → units(空表/NULL → 0)。SUM 的是整数 units 值存成的 REAL,toUnits 精确还原。 */
|
|
15
15
|
function sumUnits(db, sql, sellerId) {
|
|
16
16
|
const row = db.prepare(sql).get(sellerId);
|