@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
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js';
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
|
|
2
|
+
import { createNotification } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
3
|
+
import { releaseFeeStake } from '../../direct-pay-ledger.js'; // Rail1 直付:取消/超时释放任何遗留模拟质押(AR 订单无 stake → no-op)
|
|
4
|
+
import { restorePreShipDirectPayStock } from '../../direct-pay-stock.js'; // D3 库存回补唯一入口(pre-ship 放行;已出库拒绝)
|
|
5
|
+
import { requireBothDisclosuresAcked } from '../../direct-pay-disclosures.js'; // PR-4e: D1/D2 披露契约门
|
|
6
|
+
import { requireDirectPayHumanPasskey } from '../direct-pay-guards.js'; // PR-4e: 现场真人 Passkey/gate-token 门
|
|
2
7
|
export function registerOrdersActionRoutes(app, deps) {
|
|
3
|
-
const { db, auth, isTrustedRole, generateId, transition, notifyTransition, settleOrder, settleFault, detectFraud, createDispute, checkTimeouts, recordViolationReputation, broadcastSystemEvent } = deps;
|
|
8
|
+
const { db, auth, isTrustedRole, generateId, transition, notifyTransition, settleOrder, settleFault, detectFraud, createDispute, checkTimeouts, recordViolationReputation, broadcastSystemEvent, consumeGateToken } = deps;
|
|
9
|
+
// PR-4e: direct_p2p 风险动作门 —— ① D1/D2 两次披露都 ack(缺则 DISCLOSURE_NOT_ACKED);② 现场真人 Passkey + 一次性
|
|
10
|
+
// WebAuthn gate token(purpose 固定 direct_pay_order_action,order+action 走 purpose_data + validate)。
|
|
11
|
+
// 纯前置门:返回 ok 才允许后续写入。【先 disclosure(只读)再 Passkey(消费 token)】→ 缺 ack 不浪费 token。
|
|
12
|
+
// 调用方必须在【任何写入前 + 只读状态预检之后】调用,避免错误状态消耗 token。
|
|
13
|
+
function directPayActionGate(orderId, action, userId, webauthnToken) {
|
|
14
|
+
const disc = requireBothDisclosuresAcked(db, orderId);
|
|
15
|
+
if (!disc.ok)
|
|
16
|
+
return { ok: false, status: 409, error: disc.reason || '需先完成两次风险披露确认', error_code: disc.error_code || 'DISCLOSURE_NOT_ACKED' };
|
|
17
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
18
|
+
userId, webauthnToken, purpose: 'direct_pay_order_action',
|
|
19
|
+
validate: (data) => { const d = data; return !!d && d.order_id === orderId && d.action === action; },
|
|
20
|
+
});
|
|
21
|
+
if (!gate.ok)
|
|
22
|
+
return { ok: false, status: 403, error: gate.reason || '需现场真人 Passkey 确认', error_code: gate.error_code || 'HUMAN_PRESENCE_REQUIRED' };
|
|
23
|
+
return { ok: true };
|
|
24
|
+
}
|
|
4
25
|
// RFC-007 stage 2:卖家主动拒单 reason_code 白名单。
|
|
5
26
|
// classification(客观无责 vs 主观有责)是 stage 3 auto-verify 的事;stage 2 仅捕获 + 一律走违约结算。
|
|
6
27
|
// objective-claimed(stage 3 将尝试确定性核验):stock_consumed_concurrent / stale_price_snapshot / force_majeure
|
|
@@ -84,32 +105,47 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
84
105
|
// RFC-016: 校验读 → 异步 seam;下方 completed+history 写仍是同步 db.transaction(Phase 3 迁 pg 事务)
|
|
85
106
|
const order = await dbOne('SELECT * FROM orders WHERE id = ?', [req.params.id]);
|
|
86
107
|
if (!order)
|
|
87
|
-
return void res.status(404).json({ error: '订单不存在' });
|
|
108
|
+
return void res.status(404).json({ error: '订单不存在', error_code: 'ORDER_NOT_FOUND' });
|
|
88
109
|
if (order.fulfillment_mode !== 'in_person')
|
|
89
|
-
return void res.status(400).json({ error: '该订单非面交' });
|
|
110
|
+
return void res.status(400).json({ error: '该订单非面交', error_code: 'NOT_IN_PERSON' });
|
|
90
111
|
if (order.buyer_id !== user.id)
|
|
91
|
-
return void res.status(403).json({ error: '仅买家可确认面交完成' });
|
|
112
|
+
return void res.status(403).json({ error: '仅买家可确认面交完成', error_code: 'NOT_ORDER_BUYER' });
|
|
92
113
|
if (!['paid', 'accepted'].includes(order.status))
|
|
93
|
-
return void res.status(400).json({ error: `订单状态 ${order.status}
|
|
114
|
+
return void res.status(400).json({ error: `订单状态 ${order.status} 不可确认面交`, error_code: 'NOT_CONFIRMABLE_IN_PERSON' });
|
|
94
115
|
if (order.has_pending_claim)
|
|
95
|
-
return void res.status(400).json({ error: '存在进行中的验证任务,不可确认' });
|
|
116
|
+
return void res.status(400).json({ error: '存在进行中的验证任务,不可确认', error_code: 'HAS_PENDING_CLAIM' });
|
|
117
|
+
// Rail1:平台费已切换为链下应收(accrue 在完成结算时,与 completed 同一原子边界,fail-closed)。
|
|
118
|
+
// 建单不再锁 fee-stake,故【不再】前置要求 locked stake(AR 订单本就无 stake)。
|
|
119
|
+
const isDirectP2p = order.payment_rail === 'direct_p2p';
|
|
120
|
+
// PR-4e:direct_p2p 面交完成 = RISK 动作 → 两次披露门 + 现场真人 Passkey 门。
|
|
121
|
+
if (isDirectP2p) {
|
|
122
|
+
const g = directPayActionGate(req.params.id, 'confirm_in_person', user.id, req.body?.webauthn_token);
|
|
123
|
+
if (!g.ok)
|
|
124
|
+
return void res.status(g.status).json({ error: g.error, error_code: g.error_code });
|
|
125
|
+
}
|
|
96
126
|
const tx = db.transaction(() => {
|
|
97
127
|
db.prepare(`UPDATE orders SET status='completed', updated_at=datetime('now') WHERE id = ?`).run(req.params.id);
|
|
98
128
|
db.prepare(`INSERT INTO order_state_history (id, order_id, from_status, to_status, actor_id, actor_role, notes)
|
|
99
129
|
VALUES (?,?,?,?,?,?, '面交完成 — 买家确认')`)
|
|
100
130
|
.run(generateId('hst'), req.params.id, order.status, 'completed', user.id, user.role || 'buyer');
|
|
131
|
+
// direct_p2p:取平台费进同一事务 → 取费失败(缺 locked stake)回滚 completed,订单不会 terminal-completed 而费用落空。
|
|
132
|
+
if (isDirectP2p)
|
|
133
|
+
settleOrder(req.params.id);
|
|
101
134
|
});
|
|
102
135
|
try {
|
|
103
136
|
tx();
|
|
104
137
|
}
|
|
105
138
|
catch (e) {
|
|
106
|
-
return void res.status(500).json({ error: '状态写入失败:' + e.message });
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
settleOrder(req.params.id);
|
|
139
|
+
return void res.status(isDirectP2p ? 409 : 500).json({ error: (isDirectP2p ? '直付完成结算失败,订单未完成:' : '状态写入失败:') + e.message, ...(isDirectP2p ? { error_code: 'DIRECT_PAY_SETTLE_FAILED' } : {}) });
|
|
110
140
|
}
|
|
111
|
-
|
|
112
|
-
|
|
141
|
+
// escrow:沿用原有非原子结算(失败仅记日志,不回滚 completed —— 与既有行为一致,本次不改 escrow)。
|
|
142
|
+
if (!isDirectP2p) {
|
|
143
|
+
try {
|
|
144
|
+
settleOrder(req.params.id);
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
console.error('[settleOrder in-person]', e);
|
|
148
|
+
}
|
|
113
149
|
}
|
|
114
150
|
res.json({ success: true });
|
|
115
151
|
});
|
|
@@ -121,7 +157,8 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
121
157
|
// P0 fix: 受信角色禁交易
|
|
122
158
|
if (isTrustedRole(user))
|
|
123
159
|
return void res.status(403).json({ error: '受信角色不可参与订单流转', error_code: 'TRUSTED_ROLE_NO_TRADE' });
|
|
124
|
-
const { action,
|
|
160
|
+
const { action, evidence_description = '', logistics_company_id = '' } = req.body;
|
|
161
|
+
let notes = String(req.body?.notes ?? '').slice(0, 500); // 服务端硬界:附言/备注最长 500 字符;mark_paid 时被服务端权威付款参考覆盖(见下)
|
|
125
162
|
// RFC-016: 顶层校验读 → 异步 seam;state-machine / settle / decline 写序列仍同步(Phase 3 迁 pg 行锁+事务)
|
|
126
163
|
const order = await dbOne('SELECT * FROM orders WHERE id = ?', [req.params.id]);
|
|
127
164
|
if (!order)
|
|
@@ -137,6 +174,98 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
137
174
|
if (action === 'confirm' && uid !== buyerId) {
|
|
138
175
|
return void res.status(403).json({ error: '你不是本订单的买家', error_code: 'NOT_ORDER_BUYER' });
|
|
139
176
|
}
|
|
177
|
+
// Rail1 直付:买家专属动作。mark_paid = 买家声明"我已付款"→ accepted(进卖家发货流程);
|
|
178
|
+
// cancel = 付款前买家取消 → cancelled(释放费用质押)。仅 buyer、仅 direct_p2p 且仍在付款窗口。
|
|
179
|
+
if (action === 'mark_paid' || action === 'cancel') {
|
|
180
|
+
if (uid !== buyerId) {
|
|
181
|
+
return void res.status(403).json({ error: '你不是本订单的买家', error_code: 'NOT_ORDER_BUYER' });
|
|
182
|
+
}
|
|
183
|
+
// 买家取消:付款窗口内(未付/后悔)/ 货款协商阶段(承认未付)/ 付款窗过期宽限期(确认未付,免等 48h 自动关单 ——
|
|
184
|
+
// 审计项 H:状态机本就允许 buyer direct_expired_unconfirmed→cancelled,此 guard 曾与状态机矛盾=死能力)。mark_paid 仍仅付款窗口。
|
|
185
|
+
const cancelInQuery = action === 'cancel' && ['payment_query', 'direct_expired_unconfirmed'].includes(order.status);
|
|
186
|
+
if (order.payment_rail !== 'direct_p2p' || (order.status !== 'direct_pay_window' && !cancelInQuery)) {
|
|
187
|
+
return void res.status(409).json({ error: '该操作仅适用于直付订单的付款窗口/过期宽限/协商阶段', error_code: 'NOT_DIRECT_PAY_WINDOW' });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// ── Rail1 直付货款协商(争议≠仲裁)动作。均仅 direct_p2p + 角色/状态门。PR-B1 人驱动协商 + PR-B2 卖家宽限后请求取消。 ──
|
|
191
|
+
const PQ_ACTIONS = ['report_nonpayment', 'confirm_received', 'pq_escalate', 'pq_withdraw', 'request_cancel'];
|
|
192
|
+
if (PQ_ACTIONS.includes(action)) {
|
|
193
|
+
if (order.payment_rail !== 'direct_p2p')
|
|
194
|
+
return void res.status(409).json({ error: '该操作仅适用于直付订单', error_code: 'NOT_DIRECT_PAY' });
|
|
195
|
+
if ((action === 'report_nonpayment' || action === 'confirm_received' || action === 'request_cancel') && uid !== sellerId)
|
|
196
|
+
return void res.status(403).json({ error: '你不是本订单的卖家', error_code: 'NOT_ORDER_SELLER' });
|
|
197
|
+
if ((action === 'pq_escalate' || action === 'pq_withdraw') && uid !== buyerId && uid !== sellerId)
|
|
198
|
+
return void res.status(403).json({ error: '只有买卖双方可操作', error_code: 'NOT_ORDER_PARTY' });
|
|
199
|
+
if (action === 'report_nonpayment' && order.status !== 'accepted')
|
|
200
|
+
return void res.status(409).json({ error: `仅可在【待发货(accepted)】阶段报告未收款,当前 ${order.status}`, error_code: 'NOT_ACCEPTED' });
|
|
201
|
+
if ((action === 'confirm_received' || action === 'pq_escalate' || action === 'request_cancel') && order.status !== 'payment_query')
|
|
202
|
+
return void res.status(409).json({ error: `仅可在【货款协商(payment_query)】阶段操作,当前 ${order.status}`, error_code: 'NOT_PAYMENT_QUERY' });
|
|
203
|
+
if (action === 'pq_withdraw') {
|
|
204
|
+
if (order.status !== 'disputed')
|
|
205
|
+
return void res.status(409).json({ error: `仅可在【争议(disputed)】阶段撤回,当前 ${order.status}`, error_code: 'NOT_DISPUTED' });
|
|
206
|
+
// 已裁定的争议不可撤回(disputed 已终结成 resolved/dismissed 前,dispute.status 仍 open/in_review)
|
|
207
|
+
const d = await dbOne("SELECT status FROM disputes WHERE order_id = ? ORDER BY created_at DESC LIMIT 1", [req.params.id]);
|
|
208
|
+
if (d && (d.status === 'resolved' || d.status === 'dismissed'))
|
|
209
|
+
return void res.status(409).json({ error: '争议已裁定,不可撤回', error_code: 'DISPUTE_ALREADY_RULED' });
|
|
210
|
+
// ★ 撤回【仅限货款协商升级】的仲裁:最近一次进入 disputed 必须 from payment_query。
|
|
211
|
+
// 否则履约类争议(delivered→disputed 货损/货不对版)会被错误撤回、dismiss、状态倒退回 payment_query。fail-closed。
|
|
212
|
+
const lastDisputed = await dbOne("SELECT from_status FROM order_state_history WHERE order_id = ? AND to_status = 'disputed' ORDER BY created_at DESC, rowid DESC LIMIT 1", [req.params.id]);
|
|
213
|
+
if (!lastDisputed || lastDisputed.from_status !== 'payment_query')
|
|
214
|
+
return void res.status(409).json({ error: '仅可撤回由货款协商升级的仲裁;履约类争议(货损/货不对版)须经仲裁裁定', error_code: 'NOT_PAYMENT_QUERY_DISPUTE' });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// PR-B2:卖家在【买家响应宽限已过】后请求取消 —— 不立即关单,而是开【N 天买家申诉窗】(payment_query_cancel_deadline)。
|
|
218
|
+
// 窗内买家仍可 confirm-side/主张已付/升级举证(pq_escalate);窗满由 cron 关单。不动状态,不涉资金。幂等(deadline 已设则不重置)。
|
|
219
|
+
if (action === 'request_cancel') {
|
|
220
|
+
const g = await dbOne("SELECT (payment_query_deadline IS NOT NULL AND datetime(payment_query_deadline) < datetime('now')) AS over FROM orders WHERE id = ?", [req.params.id]);
|
|
221
|
+
if (!g?.over)
|
|
222
|
+
return void res.status(409).json({ error: '买家响应宽限未到,暂不可请求取消', error_code: 'GRACE_NOT_ELAPSED' });
|
|
223
|
+
let rd = 7;
|
|
224
|
+
try {
|
|
225
|
+
const p = await dbOne("SELECT value FROM protocol_params WHERE key = 'direct_pay.payment_query_recourse_days'");
|
|
226
|
+
if (p)
|
|
227
|
+
rd = Math.max(1, Number(p.value) || 7);
|
|
228
|
+
}
|
|
229
|
+
catch { }
|
|
230
|
+
await dbRun(`UPDATE orders SET payment_query_cancel_deadline = datetime('now', ? ) WHERE id = ? AND payment_query_cancel_deadline IS NULL`, [`+${rd} days`, req.params.id]);
|
|
231
|
+
const dl = (await dbOne("SELECT payment_query_cancel_deadline d FROM orders WHERE id = ?", [req.params.id]))?.d;
|
|
232
|
+
try {
|
|
233
|
+
createNotification(db, order.buyer_id, req.params.id, 'payment_query_cancel_requested', '⏳ 卖家申请取消订单', `卖家称未收到货款且你未回应,已申请取消。你约有 ${rd} 天:若确已付款请提供付款参考或发起举证(pq_escalate),否则订单将于 ${dl} 自动取消。直付非托管,无平台退款。`);
|
|
234
|
+
}
|
|
235
|
+
catch { }
|
|
236
|
+
return void res.json({ success: true, status: 'payment_query', cancel_recourse_deadline: dl });
|
|
237
|
+
}
|
|
238
|
+
// PR-4e:mark_paid = direct_p2p RISK 动作(已过 direct_pay_window 只读预检)→ 两次披露门 + 现场真人 Passkey 门。cancel 不门控。
|
|
239
|
+
if (action === 'mark_paid') {
|
|
240
|
+
const g = directPayActionGate(req.params.id, 'mark_paid', uid, req.body?.webauthn_token);
|
|
241
|
+
if (!g.ok)
|
|
242
|
+
return void res.status(g.status).json({ error: g.error, error_code: g.error_code });
|
|
243
|
+
// 防作弊 D1:付款参考【服务端权威派生】(与前端 dpPayRef 同算法),忽略客户端可伪造的参考文本 —— UI 已锁只读(#222),
|
|
244
|
+
// 这里把 API 级也收口:直接 POST 伪造 notes 冒充别单参考号 → 无效。客户端附言仅作补充备注,剥掉任何"付款参考"字样。
|
|
245
|
+
const canonicalRef = 'WAZ-' + req.params.id.replace(/[^a-zA-Z0-9]/g, '').slice(-8).toUpperCase();
|
|
246
|
+
const extra = notes.replace(/付款参考[::]?\s*[^\s·;,。]*/g, '').replace(/WAZ-[A-Z0-9]{8}/g, '').trim().slice(0, 120);
|
|
247
|
+
// 防作弊 D2:同买家·同卖家·同金额的其它在途直付单 → 时间线预警(一笔真实转账冒充两单的重复确认,卖家第一道防线)
|
|
248
|
+
const dup = await dbOne(`SELECT COUNT(*) n FROM orders WHERE buyer_id = ? AND seller_id = ? AND payment_rail = 'direct_p2p' AND total_amount = ? AND id != ? AND status IN ('accepted','shipped','picked_up','in_transit','delivered')`, [buyerId, sellerId, order.total_amount, req.params.id]);
|
|
249
|
+
// 审计项 E:时间线带上应付金额(下单冻结的参考换算)—— 卖家对账三要素:参考号 + 金额 + 币种,与买家所见同一数字
|
|
250
|
+
let payable = `应付 ${order.total_amount} USDC`;
|
|
251
|
+
try {
|
|
252
|
+
const ps = JSON.parse(order.direct_pay_account_snapshot || '{}');
|
|
253
|
+
if (Number.isFinite(Number(ps?.payable_approx)) && ps?.payable_currency)
|
|
254
|
+
payable += `(≈${ps.payable_currency} ${Number(ps.payable_approx).toFixed(2)},下单时参考价)`;
|
|
255
|
+
}
|
|
256
|
+
catch { /* 快照缺失/坏 → 仅 USDC */ }
|
|
257
|
+
notes = `付款参考: ${canonicalRef} · ${payable}${extra ? ' · ' + extra : ''}${(dup?.n || 0) > 0 ? ` ⚠️ 同买家另有 ${dup.n} 笔同金额直付订单在途,请逐单核对付款参考再发货` : ''}`;
|
|
258
|
+
}
|
|
259
|
+
// Rail1:平台费链下应收,accrue 在完成结算时(settleOrder direct_p2p 分支)与 completed 同一原子边界、fail-closed
|
|
260
|
+
// (accrueFeeReceivable 缺费即抛 → 回滚 completed)。故【不再】前置要求 locked fee-stake。
|
|
261
|
+
// PR-4e:direct_p2p confirm = RISK 动作。先【只读状态预检】(仅 delivered 可确认收货,杜绝错误状态消耗 token),再两次披露门 + 现场真人 Passkey 门。
|
|
262
|
+
if (action === 'confirm' && order.payment_rail === 'direct_p2p') {
|
|
263
|
+
if (order.status !== 'delivered')
|
|
264
|
+
return void res.status(409).json({ error: `订单状态 ${order.status} 不可确认收货(仅 delivered)`, error_code: 'ORDER_NOT_DELIVERED' });
|
|
265
|
+
const g = directPayActionGate(req.params.id, 'confirm', uid, req.body?.webauthn_token);
|
|
266
|
+
if (!g.ok)
|
|
267
|
+
return void res.status(g.status).json({ error: g.error, error_code: g.error_code });
|
|
268
|
+
}
|
|
140
269
|
if (action === 'pickup' || action === 'transit' || action === 'deliver') {
|
|
141
270
|
// pickup 时若订单尚无物流,允许领取(孤儿单兜底)
|
|
142
271
|
const isOrphanPickup = action === 'pickup' && !logisticsId;
|
|
@@ -254,9 +383,85 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
254
383
|
note: '已就客观无责拒单发起人工仲裁举证。自动终结已暂停,等待仲裁员裁决:维持→免责全退+退质押,驳回→违约结算。',
|
|
255
384
|
});
|
|
256
385
|
}
|
|
386
|
+
// Rail1 直付:付款窗口内买家取消 → cancelled + 释放费用质押(单事务:转移成功必同步放质押,
|
|
387
|
+
// 杜绝"已取消但质押漏放"泄漏 —— cancelled 单超时 cron 不再扫,放质押无后备路径,故必须原子)。
|
|
388
|
+
if (action === 'cancel') {
|
|
389
|
+
const fromStatusCancel = order.status;
|
|
390
|
+
try {
|
|
391
|
+
db.transaction(() => {
|
|
392
|
+
const r = transition(db, req.params.id, 'cancelled', uid, [], notes);
|
|
393
|
+
if (!r.success)
|
|
394
|
+
throw new Error(r.error || '状态转移失败');
|
|
395
|
+
releaseFeeStake(db, { orderId: req.params.id });
|
|
396
|
+
// D3 库存回补:经唯一守卫入口(仅 pre-ship 来源放行;已出库走退货验收,绝不直接回补 —— 见 direct-pay-stock.ts 情形矩阵)
|
|
397
|
+
restorePreShipDirectPayStock(db, { fromStatus: fromStatusCancel, productId: order.product_id, quantity: Number(order.quantity) || 1 });
|
|
398
|
+
})();
|
|
399
|
+
}
|
|
400
|
+
catch (e) {
|
|
401
|
+
return void res.status(409).json({ error: e.message, error_code: 'CANCEL_FAILED' });
|
|
402
|
+
}
|
|
403
|
+
notifyTransition(db, req.params.id, fromStatusCancel, 'cancelled');
|
|
404
|
+
return void res.json({ success: true, status: 'cancelled', fee_stake_released: true });
|
|
405
|
+
}
|
|
406
|
+
// 争议协商收口(买家侧):买家撤诉并确认收货 —— 限【delivered 来源履约争议 + 争议发起人本人 + 裁定前】。
|
|
407
|
+
// 为什么存在:买家发起"未收到货"争议后包裹晚到/找到代收点,双方合意收口不该只剩仲裁(卖家侧收口=协商取消)。
|
|
408
|
+
// 货款争议(payment_query 来源)不走这里 —— 那是 pq_withdraw 的撤回语义(回协商,不确认收货)。
|
|
409
|
+
// 全原子(Codex P1 模式):争议 dismissed + disputed→confirmed→completed + settleOrder 同一 db.transaction,
|
|
410
|
+
// 任一步失败整体回滚(订单仍 disputed、争议仍 open,可重试);两轨同块(escrow 释放托管货款,dp 计提平台费)。
|
|
411
|
+
if (action === 'dispute_withdraw_confirm') {
|
|
412
|
+
if (uid !== buyerId)
|
|
413
|
+
return void res.status(403).json({ error: '你不是本订单的买家', error_code: 'NOT_ORDER_BUYER' });
|
|
414
|
+
if (order.status !== 'disputed')
|
|
415
|
+
return void res.status(409).json({ error: `订单状态 ${order.status} 不可撤诉确认收货(仅争议中)`, error_code: 'ORDER_NOT_DISPUTED' });
|
|
416
|
+
// RFC-016:两条纯预检读 → 异步 seam;事务内的 disputes UPDATE + transition + settle 写序列仍同步(原子性)
|
|
417
|
+
const dsp = await dbOne("SELECT id, initiator_id FROM disputes WHERE order_id = ? AND status IN ('open','in_review') ORDER BY created_at DESC, rowid DESC LIMIT 1", [req.params.id]);
|
|
418
|
+
if (!dsp)
|
|
419
|
+
return void res.status(409).json({ error: '争议已裁定或不存在,不可撤诉', error_code: 'DISPUTE_ALREADY_RULED' });
|
|
420
|
+
if (dsp.initiator_id !== uid)
|
|
421
|
+
return void res.status(403).json({ error: '仅争议发起人可撤诉', error_code: 'NOT_DISPUTE_INITIATOR' });
|
|
422
|
+
const lastDisputedFrom = await dbOne("SELECT from_status FROM order_state_history WHERE order_id = ? AND to_status = 'disputed' ORDER BY created_at DESC, rowid DESC LIMIT 1", [req.params.id]);
|
|
423
|
+
if (lastDisputedFrom?.from_status !== 'delivered') {
|
|
424
|
+
return void res.status(409).json({ error: '仅"已投递后未收到货/货有问题"类争议可撤诉确认收货;货款争议请用"撤回仲裁·回到协商"', error_code: 'NOT_FULFILMENT_DISPUTE' });
|
|
425
|
+
}
|
|
426
|
+
// dp = RISK 动作(确认收货即完成结算、计提平台费):所有只读预检过后才消费 Passkey token(与 confirm 同门)
|
|
427
|
+
if (order.payment_rail === 'direct_p2p') {
|
|
428
|
+
const g = directPayActionGate(req.params.id, 'dispute_withdraw_confirm', uid, req.body?.webauthn_token);
|
|
429
|
+
if (!g.ok)
|
|
430
|
+
return void res.status(g.status).json({ error: g.error, error_code: g.error_code });
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
db.transaction(() => {
|
|
434
|
+
db.prepare("UPDATE disputes SET status = 'dismissed', verdict_reason = ?, resolved_at = datetime('now') WHERE id = ? AND status IN ('open','in_review')")
|
|
435
|
+
.run(notes ? `买家撤诉并确认收货:${notes}` : '买家撤诉并确认收货', dsp.id);
|
|
436
|
+
const r1 = transition(db, req.params.id, 'confirmed', uid, [], notes);
|
|
437
|
+
if (!r1.success)
|
|
438
|
+
throw new Error(r1.error || 'confirmed transition failed');
|
|
439
|
+
const r2 = transition(db, req.params.id, 'completed', 'sys_protocol', [], '系统自动结算');
|
|
440
|
+
if (!r2.success)
|
|
441
|
+
throw new Error(r2.error || 'completed transition failed');
|
|
442
|
+
settleOrder(req.params.id);
|
|
443
|
+
})();
|
|
444
|
+
}
|
|
445
|
+
catch (e) {
|
|
446
|
+
return void res.status(409).json({ error: `撤诉确认收货失败,订单未变更(仍在争议中,可重试):${e.message}`, error_code: 'DISPUTE_CLOSE_SETTLE_FAILED' });
|
|
447
|
+
}
|
|
448
|
+
notifyTransition(db, req.params.id, 'confirmed', 'completed');
|
|
449
|
+
try {
|
|
450
|
+
createNotification(db, sellerId, req.params.id, 'dispute_withdrawn_confirmed', '✅ 买家已撤诉并确认收货', '买家撤回争议并确认收货,订单已完成结算,双方信誉不受影响。', { templateKey: 'dispute_withdrawn_confirmed', params: {} });
|
|
451
|
+
}
|
|
452
|
+
catch { /* 通知失败不阻断 */ }
|
|
453
|
+
try {
|
|
454
|
+
broadcastSystemEvent('order_completed', '✓', `订单完成 ${req.params.id}`, req.params.id);
|
|
455
|
+
}
|
|
456
|
+
catch { }
|
|
457
|
+
return void res.json({ success: true, status: 'completed', dispute_dismissed: true });
|
|
458
|
+
}
|
|
257
459
|
const actionMap = {
|
|
258
460
|
accept: 'accepted', ship: 'shipped', pickup: 'picked_up',
|
|
259
|
-
transit: 'in_transit', deliver: 'delivered', confirm: 'confirmed', dispute: 'disputed'
|
|
461
|
+
transit: 'in_transit', deliver: 'delivered', confirm: 'confirmed', dispute: 'disputed',
|
|
462
|
+
mark_paid: 'accepted', // Rail1 直付:买家声明"我已付款" → accepted(汇入既有卖家发货流程;协议不验真实付款)
|
|
463
|
+
// Rail1 货款协商:卖家报未收款→协商;卖家确认已收→恢复;升级→举证仲裁;撤回→回协商(pq_withdraw 由上方原子块 early-return 处理,列此仅为通过 !toStatus 校验)。
|
|
464
|
+
report_nonpayment: 'payment_query', confirm_received: 'accepted', pq_escalate: 'disputed', pq_withdraw: 'payment_query',
|
|
260
465
|
};
|
|
261
466
|
const toStatus = actionMap[action];
|
|
262
467
|
if (!toStatus)
|
|
@@ -270,11 +475,84 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
270
475
|
VALUES (?,?,?,'description',?,?,?)`).run(eid, req.params.id, user.id, evidence_description, `hash_${Date.now()}`, evReasons.length ? JSON.stringify(evReasons) : null);
|
|
271
476
|
evidenceIds.push(eid);
|
|
272
477
|
}
|
|
478
|
+
// Rail1 全原子(Codex P1):direct_p2p confirm 必须把 delivered→confirmed→completed→settle/accrue 包进【同一 db.transaction】。
|
|
479
|
+
// 否则 delivered→confirmed 先单独提交、随后 accrue 失败 → 订单卡在 confirmed(retry 被 ORDER_NOT_DELIVERED 拒)。
|
|
480
|
+
// 任一步失败 → 整体回滚到 delivered(可重试);成功后再发通知。confirm-in-person 是另一端点、已单事务原子。
|
|
481
|
+
if (action === 'confirm' && order.payment_rail === 'direct_p2p') {
|
|
482
|
+
try {
|
|
483
|
+
db.transaction(() => {
|
|
484
|
+
const r1 = transition(db, req.params.id, 'confirmed', user.id, evidenceIds, notes);
|
|
485
|
+
if (!r1.success)
|
|
486
|
+
throw new Error(r1.error || 'confirmed transition failed');
|
|
487
|
+
const r2 = transition(db, req.params.id, 'completed', 'sys_protocol', [], '系统自动结算');
|
|
488
|
+
if (!r2.success)
|
|
489
|
+
throw new Error(r2.error || 'completed transition failed');
|
|
490
|
+
settleOrder(req.params.id); // direct_p2p 分支:释放遗留模拟 stake + accrueFeeReceivable(fail-closed)
|
|
491
|
+
})();
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
return void res.status(409).json({ error: `直付完成结算失败,订单未完成(仍停在 delivered,可重试):${e.message}`, error_code: 'DIRECT_PAY_SETTLE_FAILED' });
|
|
495
|
+
}
|
|
496
|
+
notifyTransition(db, req.params.id, 'delivered', 'confirmed');
|
|
497
|
+
notifyTransition(db, req.params.id, 'confirmed', 'completed');
|
|
498
|
+
try {
|
|
499
|
+
broadcastSystemEvent('order_completed', '✓', `订单完成 ${req.params.id}`, req.params.id);
|
|
500
|
+
}
|
|
501
|
+
catch { }
|
|
502
|
+
return void res.json({ success: true, status: 'completed', settlement: { rail: 'direct_p2p', fee_accrued: true } });
|
|
503
|
+
}
|
|
504
|
+
// Rail1 撤回仲裁:disputed→payment_query + 【同一事务】关闭活跃 dispute(否则订单回协商但仲裁视图仍见活跃争议 → 脏状态,
|
|
505
|
+
// 仲裁员可能对已回协商的单裁定)。原子后重建买家响应宽限,让 PR-B2 的宽限/超时 cron 有截止时间可依赖。
|
|
506
|
+
if (action === 'pq_withdraw') {
|
|
507
|
+
try {
|
|
508
|
+
db.transaction(() => {
|
|
509
|
+
const r = transition(db, req.params.id, 'payment_query', user.id, [], notes);
|
|
510
|
+
if (!r.success)
|
|
511
|
+
throw new Error(r.error || 'withdraw transition failed');
|
|
512
|
+
db.prepare("UPDATE disputes SET status = 'dismissed', verdict_reason = COALESCE(verdict_reason, '撤回仲裁,回到协商'), resolved_at = datetime('now') WHERE order_id = ? AND status IN ('open', 'in_review')").run(req.params.id);
|
|
513
|
+
})();
|
|
514
|
+
}
|
|
515
|
+
catch (e) {
|
|
516
|
+
return void res.status(409).json({ error: `撤回仲裁失败:${e.message}`, error_code: 'WITHDRAW_FAILED' });
|
|
517
|
+
}
|
|
518
|
+
let gh = 72;
|
|
519
|
+
try {
|
|
520
|
+
const p = await dbOne("SELECT value FROM protocol_params WHERE key = 'direct_pay.payment_query_grace_hours'");
|
|
521
|
+
if (p)
|
|
522
|
+
gh = Math.max(1, Number(p.value) || 72);
|
|
523
|
+
}
|
|
524
|
+
catch { }
|
|
525
|
+
await dbRun(`UPDATE orders SET payment_query_deadline = datetime('now', ? ), payment_query_cancel_deadline = NULL WHERE id = ?`, [`+${gh} hours`, req.params.id]);
|
|
526
|
+
notifyTransition(db, req.params.id, 'disputed', 'payment_query');
|
|
527
|
+
return void res.json({ success: true, status: 'payment_query', dispute_withdrawn: true });
|
|
528
|
+
}
|
|
273
529
|
const fromStatus = order.status;
|
|
274
530
|
const result = transition(db, req.params.id, toStatus, user.id, evidenceIds, notes);
|
|
275
531
|
if (!result.success)
|
|
276
532
|
return void res.json({ error: result.error });
|
|
277
533
|
notifyTransition(db, req.params.id, fromStatus, toStatus);
|
|
534
|
+
// 审计项 B(N2):买家标记付款 → 通知卖家核款发货(direct_pay_window→accepted 不在 notifyTransition RULES,
|
|
535
|
+
// 此前卖家全程收不到"已付款"信号只能手动刷)。notes 即 D1/D2/E 的权威对账串(参考号+应付+多单预警),直接复用。
|
|
536
|
+
if (action === 'mark_paid') {
|
|
537
|
+
try {
|
|
538
|
+
createNotification(db, sellerId, req.params.id, 'direct_pay_marked_paid', '💰 买家已标记付款,请核对后发货', `${notes}。请核对银行/收款App流水后再发货;未收到请点"未收到货款"。`, { templateKey: 'dp_marked_paid', params: { detail: notes } });
|
|
539
|
+
}
|
|
540
|
+
catch { /* 通知失败不阻断 */ }
|
|
541
|
+
}
|
|
542
|
+
// Rail1 货款协商:卖家报未收款 → 设买家响应宽限(PR-B2 卖家据此才可请求取消);确认已收/升级 → 清协商截止。
|
|
543
|
+
if (action === 'report_nonpayment') {
|
|
544
|
+
let gh = 72;
|
|
545
|
+
try {
|
|
546
|
+
const p = await dbOne("SELECT value FROM protocol_params WHERE key = 'direct_pay.payment_query_grace_hours'");
|
|
547
|
+
if (p)
|
|
548
|
+
gh = Math.max(1, Number(p.value) || 72);
|
|
549
|
+
}
|
|
550
|
+
catch { }
|
|
551
|
+
await dbRun(`UPDATE orders SET payment_query_deadline = datetime('now', ? ) WHERE id = ?`, [`+${gh} hours`, req.params.id]);
|
|
552
|
+
}
|
|
553
|
+
if (action === 'confirm_received' || action === 'pq_escalate') {
|
|
554
|
+
await dbRun(`UPDATE orders SET payment_query_deadline = NULL, payment_query_cancel_deadline = NULL WHERE id = ?`, [req.params.id]);
|
|
555
|
+
}
|
|
278
556
|
if (toStatus === 'disputed') {
|
|
279
557
|
createDispute(db, req.params.id, user.id, notes || evidence_description || '买家发起争议', evidenceIds);
|
|
280
558
|
try {
|
|
@@ -292,6 +570,7 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
292
570
|
let settlementBreakdown = null;
|
|
293
571
|
if (toStatus === 'confirmed') {
|
|
294
572
|
const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
|
|
573
|
+
// direct_p2p confirm 已在上方【全原子早返回块】处理(delivered→confirmed→completed→settle 同一 tx);此处仅 escrow。
|
|
295
574
|
transition(db, req.params.id, 'completed', sysUser.id, [], '系统自动结算');
|
|
296
575
|
notifyTransition(db, req.params.id, 'confirmed', 'completed');
|
|
297
576
|
settleOrder(req.params.id);
|
|
@@ -339,7 +618,9 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
339
618
|
reserveByKind.escrow_expired = round2(Number(r.s));
|
|
340
619
|
}
|
|
341
620
|
const redirectedToCommissionReserve = round2(reserveByKind.region_cap + reserveByKind.chain_gap + reserveByKind.orphan_sponsor + reserveByKind.opt_out_deactivated + reserveByKind.escrow_expired);
|
|
342
|
-
|
|
621
|
+
// RFC-018: matures_at IS NULL = opt-out escrow only; clearing rows (matures_at NOT NULL) are
|
|
622
|
+
// surfaced separately (PR3), not mislabeled as opt-out escrow here.
|
|
623
|
+
const escrowRow = (await dbOne("SELECT COALESCE(SUM(amount),0) AS s FROM pending_commission_escrow WHERE order_id = ? AND status = 'pending' AND matures_at IS NULL", [req.params.id]));
|
|
343
624
|
const heldInOptOutEscrow = round2(Number(escrowRow.s));
|
|
344
625
|
// QA 轮 9.5 P2:payouts 表只 MCP legacy 写,PWA settleOrder 直更 wallet.balance 不写 payouts
|
|
345
626
|
// 改用公式推算 sellerAmount(跟 PWA settleOrder 内部计算一致),更可靠
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { buildCartMandate, buildPaymentMandate, signMandate } from './ap2-mandate.js';
|
|
2
2
|
// RFC-014 PR3 — 金额走整数 base-units;钱包写绝对值(防 REAL 浮点加法 dust)。
|
|
3
3
|
import { toUnits, toDecimal, mulQty, mulRate } from '../../money.js';
|
|
4
|
+
import { createDirectPayResponse } from '../../direct-pay-create.js'; // PR-4c: direct_p2p 建单分叉(生产门+收款指令门+原子建单;本金不入协议)
|
|
4
5
|
import { applyWalletDelta } from '../../ledger.js';
|
|
6
|
+
import { gateShippingForCreate } from '../../shipping-templates.js';
|
|
7
|
+
import { buildTradeTermsSnapshot, writeTradeTermsSnapshot } from '../../trade-terms.js';
|
|
8
|
+
import { gateSaleRegionForCreate } from '../../sale-regions.js'; // PR-2 运费守门 + S0 条款快照 + S1 可售门(意愿/合规先于物流)
|
|
5
9
|
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam(仅下单事务外的预检查;事务内 escrow/INSERT 保持同步)
|
|
6
10
|
// 店铺推荐 → 商品三级归因的【懒升级】(sync,跑在下单事务内、getProductShareChain 之前)。
|
|
7
11
|
// 严格门槛(任一不满足则不升级,绝不覆盖已有有效直接归因):
|
|
@@ -228,7 +232,12 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
228
232
|
const priceAfterCouponU = Math.max(0, subtotalU - toUnits(couponDiscount));
|
|
229
233
|
const insuranceRate = getProtocolParam('order_insurance_rate', 0.01);
|
|
230
234
|
const insurancePremiumU = buy_insurance ? mulRate(priceAfterCouponU, insuranceRate) : 0;
|
|
231
|
-
|
|
235
|
+
if (!gateSaleRegionForCreate(db, res, product, String(product.seller_uid), req.body?.ship_to_region, getProtocolParam))
|
|
236
|
+
return;
|
|
237
|
+
const _ship = gateShippingForCreate(db, res, product, String(product.seller_uid), req.body?.ship_to_region, String(req.body?.payment_rail || '') === 'direct_p2p' ? 'direct_p2p' : 'escrow', priceAfterCouponU);
|
|
238
|
+
if (!_ship)
|
|
239
|
+
return; // PR-2 运费模板:有模板必选地区且须命中(400/409 已写);无模板=原行为
|
|
240
|
+
const totalAmountU = Math.max(0, priceAfterCouponU + insurancePremiumU + _ship.feeU); // 运费并入总额(与保险费同惯例;快照 orders.shipping_fee)
|
|
232
241
|
// B5 主动捐赠 — 按订单总额 × 比例算(额外扣款,进 charity_fund)
|
|
233
242
|
const donationAmountU = donationPctNum > 0 ? mulRate(totalAmountU, donationPctNum) : 0;
|
|
234
243
|
// decimal 视图(落库 / 下游 AP2 / 响应,均为 base-unit 干净值)
|
|
@@ -236,6 +245,10 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
236
245
|
const insurancePremium = toDecimal(insurancePremiumU);
|
|
237
246
|
const totalAmount = toDecimal(totalAmountU);
|
|
238
247
|
const donationAmount = toDecimal(donationAmountU);
|
|
248
|
+
const shippingFee = toDecimal(_ship.feeU);
|
|
249
|
+
// PR-4c:direct_p2p 分叉 —— 本金不入协议,跳过下方 escrow 预检/事务,改走直付建单(生产门+收款指令门+原子建单,仅锁卖家 fee-stake)。
|
|
250
|
+
if (String(req.body?.payment_rail || '') === 'direct_p2p')
|
|
251
|
+
return void createDirectPayResponse(res, db, { generateId, transition, appendOrderEvent, getProtocolParam }, { product, buyerId: user.id, reqQty, basePrice, totalAmount, totalAmountU, shippingAddress: String(shipping_address), directReceiveAccountId: (typeof req.body?.direct_receive_account_id === 'string' && req.body.direct_receive_account_id) ? String(req.body.direct_receive_account_id) : undefined, opts: { variantId: variant_id, hasVariants: Number(product.has_variants) === 1, flashActive: !!flashSale, couponCode: coupon_code, buyInsurance: !!buy_insurance, donationPct: donationPctNum, isGift: !!is_gift, anonymous: anonymousFlag === 1, deliveryWindow: !!delivery_window }, shipping: { region: _ship.region, fee: _ship.fee, estDays: _ship.estDays, quoteRequired: _ship.quoteRequired, freeThresholdApplied: _ship.freeThresholdApplied } });
|
|
239
252
|
// 友好预检查(读):真正的守恒在下面的同步事务内(applyWalletDelta 绝对值落库)。
|
|
240
253
|
const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
241
254
|
if (!wallet)
|
|
@@ -245,6 +258,7 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
245
258
|
const now = new Date();
|
|
246
259
|
const orderId = generateId('ord');
|
|
247
260
|
let autoAccepted = false;
|
|
261
|
+
const _acceptModeAuto = ((product.accept_mode ?? (await dbOne('SELECT store_accept_mode FROM users WHERE id = ?', [product.seller_uid]))?.store_accept_mode) === 'auto'); // 接单模式 v16:单品??店铺默认;'auto'→paid 后系统自动接单;escrow manual/未设=原流程(托管中间态+24h 超时退款)零钱路改动;tx 外 seam 读
|
|
248
262
|
// P0: 整个下单流程原子化 — INSERT order + UPDATE wallet + UPDATE products + transition 任一步抛错全部回滚
|
|
249
263
|
try {
|
|
250
264
|
db.transaction(() => {
|
|
@@ -300,8 +314,9 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
300
314
|
l1_uid, l2_uid, l3_uid, snapshot_commission_rate, buyer_region, content_hash_at_order,
|
|
301
315
|
delivery_window, variant_id, variant_options_snapshot,
|
|
302
316
|
gift_recipient_name, gift_recipient_phone, gift_message, insurance_premium,
|
|
303
|
-
anonymous_recipient, recipient_code, donation_amount, stake_backing
|
|
304
|
-
) VALUES (?,?,?,?,?,?,?,?,'created'
|
|
317
|
+
anonymous_recipient, recipient_code, donation_amount, stake_backing, ship_to_region, shipping_fee, shipping_est_days
|
|
318
|
+
) VALUES (?,?,?,?,?,?,?,?,'created',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(orderId, product.id, user.id, product.seller_uid, reqQty, basePrice, totalAmount, totalAmount, shipping_address, notes || null, addHours(now, 24), addHours(now, 48), addHours(now, 120), addHours(now, 168), addHours(now, 336), addHours(now, 408), l1, l2, l3, snapshotRate, buyerRegionSnapshot, contentHashSnapshot, deliveryWindowJson, variant ? variant.id : null, variant ? variant.options_json : null, giftRecipientName, giftRecipientPhone, giftMessage, insurancePremium, anonymousFlag, recipientCode, donationAmount, stakeBacking, _ship.region, _ship.feeU > 0 || _ship.region ? shippingFee : null, _ship.estDays);
|
|
319
|
+
writeTradeTermsSnapshot(db, orderId, buildTradeTermsSnapshot(db, { productId: String(product.id), sellerId: String(product.seller_uid), shipping: { source: _ship.region ? 'template' : 'none', region: _ship.region ?? null, fee: _ship.region ? shippingFee : null, estDays: _ship.estDays ?? null, freeThresholdApplied: _ship.freeThresholdApplied }, acceptModeEffective: _acceptModeAuto ? 'auto' : null })); // S0 条款快照:冻结下单时效/退货/清关/税责声明(fail-soft,非计价输入)
|
|
305
320
|
// 协议层:写 genesis 事件 — order 创建(必然是 buyer 自己)
|
|
306
321
|
try {
|
|
307
322
|
appendOrderEvent(db, {
|
|
@@ -378,11 +393,11 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
378
393
|
catch (e) {
|
|
379
394
|
console.error('[M3-C audit]', e);
|
|
380
395
|
}
|
|
381
|
-
//
|
|
382
|
-
if (shouldAutoAccept(db, orderId)) {
|
|
396
|
+
// 自动接单:卖家接单模式 'auto'(v16)或 auto_accept Skill。
|
|
397
|
+
if (_acceptModeAuto || shouldAutoAccept(db, orderId)) {
|
|
383
398
|
const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
|
|
384
399
|
if (sysUser) {
|
|
385
|
-
const ar = transition(db, orderId, 'accepted', sysUser.id, [], '⚡ auto_accept Skill 自动接单');
|
|
400
|
+
const ar = transition(db, orderId, 'accepted', sysUser.id, [], _acceptModeAuto ? '⚡ 自动接单(卖家接单模式设置)' : '⚡ auto_accept Skill 自动接单');
|
|
386
401
|
if (ar.success) {
|
|
387
402
|
notifyTransition(db, orderId, 'paid', 'accepted');
|
|
388
403
|
autoAccepted = true;
|