@seasonkoh/webaz 0.1.29 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +150 -33
- 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 +719 -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 +281 -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 +45 -16
- 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 +63 -40
- 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 +61 -2
|
@@ -14,7 +14,7 @@ export function initNotificationSchema(db) {
|
|
|
14
14
|
id TEXT PRIMARY KEY,
|
|
15
15
|
user_id TEXT NOT NULL REFERENCES users(id),
|
|
16
16
|
order_id TEXT REFERENCES orders(id),
|
|
17
|
-
type TEXT NOT NULL,
|
|
17
|
+
type TEXT NOT NULL DEFAULT 'system', -- fresh 库直接带默认:server 的 2026-05-24 重建迁移只在"无默认"时触发,曾在 fresh boot 上重建并吞掉本函数刚 ALTER 的 template_key/params(pg 逐列 parity 抓出)
|
|
18
18
|
title TEXT NOT NULL,
|
|
19
19
|
body TEXT NOT NULL,
|
|
20
20
|
read INTEGER DEFAULT 0,
|
|
@@ -22,6 +22,16 @@ export function initNotificationSchema(db) {
|
|
|
22
22
|
);
|
|
23
23
|
CREATE INDEX IF NOT EXISTS idx_notif_user ON notifications(user_id, read, created_at DESC);
|
|
24
24
|
`);
|
|
25
|
+
// N1 通知 i18n 架构(审计项 B):template_key + params(JSON)—— 客户端按 viewer locale 用 t() 渲染;
|
|
26
|
+
// 旧行两列为 NULL → 前端回退存量中文 title/body(不 backfill)。ALTER 必须在 CREATE 之后(schema 铁律)。
|
|
27
|
+
try {
|
|
28
|
+
db.exec('ALTER TABLE notifications ADD COLUMN template_key TEXT');
|
|
29
|
+
}
|
|
30
|
+
catch { /* 已有 */ }
|
|
31
|
+
try {
|
|
32
|
+
db.exec('ALTER TABLE notifications ADD COLUMN params TEXT');
|
|
33
|
+
}
|
|
34
|
+
catch { /* 已有 */ }
|
|
25
35
|
}
|
|
26
36
|
// 实时推送回调(由 PWA server 注入,解耦依赖)
|
|
27
37
|
let pushCallback = null;
|
|
@@ -30,103 +40,139 @@ export function setPushCallback(cb) {
|
|
|
30
40
|
}
|
|
31
41
|
const RULES = {
|
|
32
42
|
'created→paid': {
|
|
33
|
-
recipients: ['seller'],
|
|
43
|
+
recipients: ['seller'], key: 'ord_created_paid',
|
|
34
44
|
title: '🛍️ 新订单',
|
|
35
45
|
body: ctx => `${ctx.buyerName} 下单了「${ctx.productTitle}」,金额 ${ctx.totalAmount} WAZ。请在 24h 内接单,否则自动退款。`,
|
|
36
46
|
},
|
|
37
47
|
'paid→accepted': {
|
|
38
|
-
recipients: ['buyer'],
|
|
48
|
+
recipients: ['buyer'], key: 'ord_paid_accepted',
|
|
39
49
|
title: '✅ 卖家已接单',
|
|
40
50
|
body: ctx => `${ctx.sellerName} 已接受你的订单,预计 5 天内发货。`,
|
|
41
51
|
},
|
|
42
52
|
'paid→cancelled': {
|
|
43
|
-
recipients: ['buyer'],
|
|
53
|
+
recipients: ['buyer'], key: 'ord_paid_cancelled',
|
|
44
54
|
title: '❌ 订单已取消',
|
|
45
55
|
body: ctx => `订单「${ctx.productTitle}」已取消,${ctx.totalAmount} WAZ 将原路退回。`,
|
|
46
56
|
},
|
|
47
57
|
'accepted→shipped': {
|
|
48
|
-
recipients: ['buyer'],
|
|
58
|
+
recipients: ['buyer'], key: 'ord_accepted_shipped',
|
|
49
59
|
title: '📦 商品已发货',
|
|
50
60
|
body: ctx => `${ctx.sellerName} 已发货,物流 48h 内揽收后你可以追踪包裹。`,
|
|
51
61
|
},
|
|
52
62
|
'shipped→picked_up': {
|
|
53
|
-
recipients: ['buyer', 'seller'],
|
|
63
|
+
recipients: ['buyer', 'seller'], key: 'ord_shipped_picked_up',
|
|
54
64
|
title: '🚚 物流已揽收',
|
|
55
65
|
body: ctx => `包裹已由${ctx.logisticsName ?? '物流方'}揽收,正在运输中。`,
|
|
56
66
|
},
|
|
57
67
|
'picked_up→in_transit': {
|
|
58
|
-
recipients: ['buyer'],
|
|
68
|
+
recipients: ['buyer'], key: 'ord_picked_up_in_transit',
|
|
59
69
|
title: '🚛 包裹运输中',
|
|
60
70
|
body: ctx => `你的「${ctx.productTitle}」正在运输途中。`,
|
|
61
71
|
},
|
|
62
|
-
// 注:曾有 'accepted→cancelled' 通知规则 —
|
|
63
|
-
//
|
|
64
|
-
// 若以后开"卖家接单后主动取消"通道,需先在 L0-2 状态机加 transition,再恢复此规则。
|
|
72
|
+
// 注:曾有 'accepted→cancelled' 通知规则 — 该边现已存在但仅 system actor(直付取消退款握手,v12),
|
|
73
|
+
// 通知由握手路由自己发(direct_pay_cancel_settled),不走本规则表。
|
|
65
74
|
'in_transit→delivered': {
|
|
66
|
-
recipients: ['buyer'],
|
|
75
|
+
recipients: ['buyer'], key: 'ord_in_transit_delivered',
|
|
67
76
|
title: '📬 包裹已投递',
|
|
68
77
|
body: ctx => `你的包裹已送达,请确认收货。72 小时内未确认将自动完成。`,
|
|
69
78
|
},
|
|
70
79
|
'delivered→confirmed': {
|
|
71
|
-
recipients: ['seller'],
|
|
72
|
-
title: '💰 买家确认收货',
|
|
73
|
-
body: ctx =>
|
|
80
|
+
recipients: ['seller'], key: ctx => ctx.paymentRail === 'direct_p2p' ? 'ord_delivered_confirmed_dp' : 'ord_delivered_confirmed',
|
|
81
|
+
title: ctx => ctx.paymentRail === 'direct_p2p' ? '✅ 买家确认收货' : '💰 买家确认收货',
|
|
82
|
+
body: ctx => ctx.paymentRail === 'direct_p2p'
|
|
83
|
+
? `${ctx.buyerName} 已确认收货,订单完成。直付为非托管:货款由你与买家场外结算,协议不代收、无平台资金入账。`
|
|
84
|
+
: `${ctx.buyerName} 已确认收货,${ctx.totalAmount} WAZ 结算中。`,
|
|
74
85
|
},
|
|
75
86
|
'confirmed→completed': {
|
|
76
|
-
recipients: ['seller'],
|
|
77
|
-
title: '✅ 交易完成,资金到账',
|
|
78
|
-
body: ctx =>
|
|
87
|
+
recipients: ['seller'], key: ctx => ctx.paymentRail === 'direct_p2p' ? 'ord_confirmed_completed_dp' : 'ord_confirmed_completed',
|
|
88
|
+
title: ctx => ctx.paymentRail === 'direct_p2p' ? '✅ 交易完成' : '✅ 交易完成,资金到账',
|
|
89
|
+
body: ctx => ctx.paymentRail === 'direct_p2p'
|
|
90
|
+
? `订单「${ctx.productTitle}」交易完成。直付为非托管:无平台资金结算,货款以你与买家场外结算为准。`
|
|
91
|
+
: `订单「${ctx.productTitle}」交易完成,收益已入账,查看钱包确认。`,
|
|
79
92
|
},
|
|
80
93
|
'paid→disputed': {
|
|
81
|
-
recipients: ['seller'],
|
|
94
|
+
recipients: ['seller'], key: 'ord_paid_disputed',
|
|
82
95
|
title: '⚠️ 买家发起争议',
|
|
83
96
|
body: ctx => `${ctx.buyerName} 对订单「${ctx.productTitle}」发起了争议。请在 48 小时内提交反驳证据,否则协议自动裁定退款。`,
|
|
84
97
|
},
|
|
85
98
|
'accepted→disputed': {
|
|
86
|
-
recipients: ['seller'],
|
|
99
|
+
recipients: ['seller'], key: 'ord_accepted_disputed',
|
|
87
100
|
title: '⚠️ 买家发起争议',
|
|
88
101
|
body: ctx => `${ctx.buyerName} 对订单「${ctx.productTitle}」发起了争议,请在 48h 内回应。`,
|
|
89
102
|
},
|
|
90
103
|
'shipped→disputed': {
|
|
91
|
-
recipients: ['seller', 'logistics'],
|
|
104
|
+
recipients: ['seller', 'logistics'], key: 'ord_shipped_disputed',
|
|
92
105
|
title: '⚠️ 发生争议',
|
|
93
106
|
body: ctx => `订单「${ctx.productTitle}」出现争议,请提交相关证据。`,
|
|
94
107
|
},
|
|
95
108
|
'in_transit→disputed': {
|
|
96
|
-
recipients: ['seller', 'logistics'],
|
|
109
|
+
recipients: ['seller', 'logistics'], key: 'ord_in_transit_disputed',
|
|
97
110
|
title: '⚠️ 运输中发生争议',
|
|
98
111
|
body: ctx => `订单「${ctx.productTitle}」运输过程中发生争议,请及时回应。`,
|
|
99
112
|
},
|
|
100
113
|
'delivered→disputed': {
|
|
101
|
-
recipients: ['seller'],
|
|
114
|
+
recipients: ['seller'], key: 'ord_delivered_disputed',
|
|
102
115
|
title: '⚠️ 买家对收货发起争议',
|
|
103
116
|
body: ctx => `${ctx.buyerName} 声称货物有问题,已发起争议。请在 48h 内提交证据。`,
|
|
104
117
|
},
|
|
118
|
+
// ── 裁定/违约:资金语义 rail-fork —— direct_p2p 非托管,平台不持货款,绝不能写"资金已释放/退回"(那是假话) ──
|
|
105
119
|
'disputed→completed': {
|
|
106
|
-
recipients: ['buyer', 'seller'],
|
|
120
|
+
recipients: ['buyer', 'seller'], key: ctx => ctx.paymentRail === 'direct_p2p' ? 'ord_disputed_completed_dp' : 'ord_disputed_completed',
|
|
107
121
|
title: '⚖️ 争议裁定:卖家胜诉',
|
|
108
|
-
body: ctx =>
|
|
122
|
+
body: ctx => ctx.paymentRail === 'direct_p2p'
|
|
123
|
+
? `订单「${ctx.productTitle}」争议已裁定:卖家胜诉(直付为信誉裁决,不涉资金流转)。`
|
|
124
|
+
: `订单「${ctx.productTitle}」争议已裁定,资金已释放给卖家。`,
|
|
109
125
|
},
|
|
110
126
|
'disputed→cancelled': {
|
|
111
|
-
recipients: ['buyer', 'seller'],
|
|
112
|
-
title: '⚖️ 争议裁定:退款买家',
|
|
113
|
-
body: ctx =>
|
|
127
|
+
recipients: ['buyer', 'seller'], key: ctx => ctx.paymentRail === 'direct_p2p' ? 'ord_disputed_cancelled_dp' : 'ord_disputed_cancelled',
|
|
128
|
+
title: ctx => ctx.paymentRail === 'direct_p2p' ? '⚖️ 争议裁定:支持买家' : '⚖️ 争议裁定:退款买家',
|
|
129
|
+
body: ctx => ctx.paymentRail === 'direct_p2p'
|
|
130
|
+
? `订单「${ctx.productTitle}」争议已裁定支持买家。直付非托管:平台无资金可退,退款由双方场外处理;卖家信誉处罚已记录。`
|
|
131
|
+
: `订单「${ctx.productTitle}」争议已裁定,${ctx.totalAmount} WAZ 已退回买家。`,
|
|
114
132
|
},
|
|
115
133
|
'paid→fault_seller': {
|
|
116
|
-
recipients: ['buyer', 'seller'],
|
|
134
|
+
recipients: ['buyer', 'seller'], key: 'ord_paid_fault_seller',
|
|
117
135
|
title: '⏰ 卖家超时违约',
|
|
118
136
|
body: ctx => `卖家超时未接单,订单已自动取消,${ctx.totalAmount} WAZ 退款处理中。`,
|
|
119
137
|
},
|
|
120
138
|
'accepted→fault_seller': {
|
|
121
|
-
recipients: ['buyer', 'seller'],
|
|
139
|
+
recipients: ['buyer', 'seller'], key: ctx => ctx.paymentRail === 'direct_p2p' ? 'ord_accepted_fault_seller_dp' : 'ord_accepted_fault_seller',
|
|
122
140
|
title: '⏰ 卖家超时未发货',
|
|
123
|
-
body: ctx =>
|
|
141
|
+
body: ctx => ctx.paymentRail === 'direct_p2p'
|
|
142
|
+
? `卖家超时未发货,订单已判卖家违约(直付非托管:平台无资金可退,违约已记入卖家信誉;退款请与卖家场外协商,协商未果可发起争议)。`
|
|
143
|
+
: `卖家超时未发货,订单已判违约,资金退回。`,
|
|
124
144
|
},
|
|
125
145
|
'in_transit→fault_logistics': {
|
|
126
|
-
recipients: ['buyer', 'seller'],
|
|
146
|
+
recipients: ['buyer', 'seller'], key: 'ord_in_transit_fault_logistics',
|
|
127
147
|
title: '⏰ 物流超时',
|
|
128
148
|
body: ctx => `物流方超时未完成投递,已自动记录违约。`,
|
|
129
149
|
},
|
|
150
|
+
// ── Direct Pay 货款协商(争议≠仲裁,非托管:全程无退款/资金语义)──────────────────
|
|
151
|
+
'accepted→payment_query': {
|
|
152
|
+
recipients: ['buyer'], key: 'ord_accepted_payment_query',
|
|
153
|
+
title: '🔎 卖家未收到货款',
|
|
154
|
+
body: ctx => `卖家报告尚未收到「${ctx.productTitle}」的货款,请核实:若确已付款请提供付款参考,若未付款可取消订单。直付非托管,协议不代收/不退款。`,
|
|
155
|
+
},
|
|
156
|
+
'payment_query→accepted': {
|
|
157
|
+
recipients: ['buyer'], key: 'ord_payment_query_accepted',
|
|
158
|
+
title: '✅ 卖家已确认收款',
|
|
159
|
+
body: ctx => `卖家已确认收到「${ctx.productTitle}」的货款,订单恢复,等待发货。`,
|
|
160
|
+
},
|
|
161
|
+
'payment_query→disputed': {
|
|
162
|
+
recipients: ['buyer', 'seller'], key: 'ord_payment_query_disputed',
|
|
163
|
+
title: '⚖️ 货款协商升级举证仲裁',
|
|
164
|
+
body: ctx => `「${ctx.productTitle}」货款协商未果,已进入举证仲裁(证据制信誉裁决,非托管:不涉退款/放款)。请提交证据。`,
|
|
165
|
+
},
|
|
166
|
+
'disputed→payment_query': {
|
|
167
|
+
recipients: ['buyer', 'seller'], key: 'ord_disputed_payment_query',
|
|
168
|
+
title: '↩️ 仲裁已撤回,回到协商',
|
|
169
|
+
body: ctx => `「${ctx.productTitle}」的仲裁申请已撤回,回到买卖双方协商。`,
|
|
170
|
+
},
|
|
171
|
+
'payment_query→cancelled': {
|
|
172
|
+
recipients: ['buyer', 'seller'], key: 'ord_payment_query_cancelled', // 买家取消(卖家知)/ 系统申诉窗满关单(买家知)
|
|
173
|
+
title: '🚫 直付订单已取消(协商)',
|
|
174
|
+
body: ctx => `「${ctx.productTitle}」订单已取消(货款协商未达成)。直付非托管,无平台退款。`,
|
|
175
|
+
},
|
|
130
176
|
};
|
|
131
177
|
// ─── 主入口:状态变更后调用 ───────────────────────────────────
|
|
132
178
|
export function notifyTransition(db, orderId, fromStatus, toStatus) {
|
|
@@ -137,19 +183,23 @@ export function notifyTransition(db, orderId, fromStatus, toStatus) {
|
|
|
137
183
|
const ctx = getOrderCtx(db, orderId);
|
|
138
184
|
if (!ctx)
|
|
139
185
|
return;
|
|
140
|
-
const title = rule.title;
|
|
186
|
+
const title = typeof rule.title === 'function' ? rule.title(ctx) : rule.title;
|
|
141
187
|
const body = rule.body(ctx);
|
|
142
188
|
const type = `${fromStatus}→${toStatus}`;
|
|
189
|
+
// N1 迁移:模板 key + 参数落库,客户端按 viewer locale 渲染;title/body 保持中文回退(零迁移)。
|
|
190
|
+
// logistics 缺省的兜底词交给客户端模板本地化(服务端不硬编码中文进 params)。
|
|
191
|
+
const templateKey = typeof rule.key === 'function' ? rule.key(ctx) : rule.key;
|
|
192
|
+
const params = { buyer: ctx.buyerName, seller: ctx.sellerName, product: ctx.productTitle, amount: ctx.totalAmount, ...(ctx.logisticsName ? { logistics: ctx.logisticsName } : {}) };
|
|
143
193
|
// 确定收件人 ID 列表
|
|
144
194
|
const recipientIds = resolveRecipients(db, rule.recipients, ctx, orderId);
|
|
145
195
|
for (const userId of recipientIds) {
|
|
146
|
-
createNotification(db, userId, orderId, type, title, body);
|
|
196
|
+
createNotification(db, userId, orderId, type, title, body, { templateKey, params });
|
|
147
197
|
}
|
|
148
198
|
}
|
|
149
199
|
// ─── 工具函数 ─────────────────────────────────────────────────
|
|
150
200
|
function getOrderCtx(db, orderId) {
|
|
151
201
|
const row = db.prepare(`
|
|
152
|
-
SELECT o.buyer_id, o.seller_id, o.logistics_id, o.total_amount,
|
|
202
|
+
SELECT o.buyer_id, o.seller_id, o.logistics_id, o.total_amount, o.payment_rail,
|
|
153
203
|
ub.name as buyer_name, us.name as seller_name,
|
|
154
204
|
ul.name as logistics_name, p.title as product_title
|
|
155
205
|
FROM orders o
|
|
@@ -168,6 +218,7 @@ function getOrderCtx(db, orderId) {
|
|
|
168
218
|
logisticsName: row.logistics_name,
|
|
169
219
|
productTitle: row.product_title,
|
|
170
220
|
totalAmount: row.total_amount,
|
|
221
|
+
paymentRail: row.payment_rail,
|
|
171
222
|
};
|
|
172
223
|
}
|
|
173
224
|
function resolveRecipients(db, roles, ctx, orderId) {
|
|
@@ -180,14 +231,31 @@ function resolveRecipients(db, roles, ctx, orderId) {
|
|
|
180
231
|
ids.add(order.seller_id);
|
|
181
232
|
if (role === 'logistics' && order.logistics_id)
|
|
182
233
|
ids.add(order.logistics_id);
|
|
234
|
+
if (role === 'admins') {
|
|
235
|
+
// 收件人=全体 admin(users.role='admin';root/regional 由具体规则语义决定是否细分 —— 需要仅 root 的
|
|
236
|
+
// 通知(如 fee-prepay 审批,ROOT 门)不走本规则表,在各自路由直查 admin_type='root')。
|
|
237
|
+
try {
|
|
238
|
+
const admins = db.prepare("SELECT id FROM users WHERE role = 'admin'").all();
|
|
239
|
+
admins.forEach(a => ids.add(a.id));
|
|
240
|
+
}
|
|
241
|
+
catch { /* fresh-DB → 无收件人 */ }
|
|
242
|
+
}
|
|
183
243
|
if (role === 'arbitrators') {
|
|
184
|
-
|
|
185
|
-
|
|
244
|
+
// 收件人=active arbitrator_whitelist(唯一仲裁能力源;legacy role='arbitrator' 会通知到打不开案件的人、漏掉真仲裁员)。
|
|
245
|
+
// 当前无 NOTIF_RULES 使用 'arbitrators'(仲裁员靠工作台拉取);此分支为未来规则备好正确的授权源。表缺失 → 静默空集。
|
|
246
|
+
try {
|
|
247
|
+
const arbs = db.prepare("SELECT user_id FROM arbitrator_whitelist WHERE status IS NULL OR status = 'active'").all();
|
|
248
|
+
arbs.forEach(a => ids.add(a.user_id));
|
|
249
|
+
}
|
|
250
|
+
catch { /* fresh-DB 无表 → 无收件人 */ }
|
|
186
251
|
}
|
|
187
252
|
}
|
|
188
253
|
return [...ids];
|
|
189
254
|
}
|
|
190
|
-
export function createNotification(db, userId, orderId, type, title, body
|
|
255
|
+
export function createNotification(db, userId, orderId, type, title, body,
|
|
256
|
+
// N1(审计项 B):可选 i18n 模板 —— template_key + params 落库,客户端按 viewer locale 用 t() 渲染;
|
|
257
|
+
// title/body 仍必填 = 中文回退(旧客户端/未知 key 一律回退,向后兼容,零迁移)。
|
|
258
|
+
opts) {
|
|
191
259
|
const notif = {
|
|
192
260
|
id: generateId('ntf'),
|
|
193
261
|
user_id: userId,
|
|
@@ -197,12 +265,13 @@ export function createNotification(db, userId, orderId, type, title, body) {
|
|
|
197
265
|
body,
|
|
198
266
|
read: 0,
|
|
199
267
|
created_at: new Date().toISOString(),
|
|
268
|
+
...(opts?.templateKey ? { template_key: opts.templateKey, params: JSON.stringify(opts.params ?? {}) } : {}),
|
|
200
269
|
};
|
|
201
270
|
db.prepare(`
|
|
202
|
-
INSERT INTO notifications (id, user_id, order_id, type, title, body)
|
|
203
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
204
|
-
`).run(notif.id, userId, orderId, type, title, body);
|
|
205
|
-
//
|
|
271
|
+
INSERT INTO notifications (id, user_id, order_id, type, title, body, template_key, params)
|
|
272
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
273
|
+
`).run(notif.id, userId, orderId, type, title, body, opts?.templateKey ?? null, opts?.templateKey ? JSON.stringify(opts.params ?? {}) : null);
|
|
274
|
+
// 实时推送(如果 PWA SSE 连接在线;payload 含模板字段,前端 toast 同样可本地化渲染)
|
|
206
275
|
pushCallback?.(userId, notif);
|
|
207
276
|
return notif;
|
|
208
277
|
}
|
|
@@ -17,7 +17,7 @@ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // R
|
|
|
17
17
|
import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
|
|
18
18
|
// RFC-014 PR5 — 争议资金处置走整数 base-units + 绝对值落库 + allocate 精确拆分。
|
|
19
19
|
import { toUnits, toDecimal, mulRate, allocate } from '../../money.js';
|
|
20
|
-
import { applyWalletDelta, debitStakeThenBalance } from '../../ledger.js';
|
|
20
|
+
import { applyWalletDelta, debitStakeThenBalance, walletUnits } from '../../ledger.js';
|
|
21
21
|
// ─── Schema 初始化(幂等,安全重复调用)────────────────────────
|
|
22
22
|
/**
|
|
23
23
|
* 为 disputes 表添加 L3 需要的新列
|
|
@@ -156,6 +156,47 @@ export function respondToDispute(db, disputeId, responderId, notes, evidenceIds)
|
|
|
156
156
|
message: '反驳证据已提交,争议进入仲裁阶段。仲裁员将在 120 小时(5 天)内做出裁定,超时协议自动判初诉方胜。',
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* 非托管订单(direct_p2p / escrow_amount=0)的争议裁定 —— 【只做状态终结,绝不动任何资金】。
|
|
161
|
+
* 协议从未持有买家货款(买家场外直付卖家),故 executeSettlement 那套"退托管/扣质押/入协议费"会【凭空】给买家记
|
|
162
|
+
* balance、把不存在的托管退款、腐蚀账本(P0)。此路径只把订单推进到与 ruling 对应的既有争议终态,金额一律 0;
|
|
163
|
+
* reputation/strike 由路由层照常施加(那才是本轨"证据制信誉裁决,不涉资金赔付"的裁决效力)。
|
|
164
|
+
* 返回 non_custodial 标记 → 路由据此跳过 release_seller / partial_refund / liability_split 的佣金/PV/基金分润钩子。
|
|
165
|
+
*/
|
|
166
|
+
export function executeNonCustodialSettlement(db, orderId, ruling) {
|
|
167
|
+
const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
|
|
168
|
+
if (!sysUser)
|
|
169
|
+
return { success: false, error: 'sys_protocol 用户不存在' };
|
|
170
|
+
const terminal = ruling === 'refund_buyer' ? 'refunded_full'
|
|
171
|
+
: ruling === 'release_seller' ? 'resolved_for_seller' : 'refunded_partial';
|
|
172
|
+
const r = transition(db, orderId, terminal, sysUser.id, [], `非托管(直付)争议裁定:协议不持货款,仅信誉裁决、不动任何资金 — ${ruling}`);
|
|
173
|
+
if (!r.success)
|
|
174
|
+
return { success: false, error: r.error };
|
|
175
|
+
// 金额全 0(供路由分润钩子据此算出 0),并打 non_custodial 标记。
|
|
176
|
+
return { success: true, detail: { non_custodial: true, ruling, buyer_refund: 0, buyer_compensation: 0, seller_received: 0, seller_escrow_share: 0, actual_refund: 0, note: '非托管订单:仅信誉裁决,不动用任何托管/钱包/质押/佣金资金' } };
|
|
177
|
+
}
|
|
178
|
+
/** active arbitrator_whitelist 谓词(唯一 runtime 人类仲裁授权源;legacy NULL status 视为 active)。导出给
|
|
179
|
+
* MCP 等 pwa 之外的消费方(layer1 不 import src/pwa,故从本引擎导出);表缺失 → fail-closed false。 */
|
|
180
|
+
export function isActiveWhitelistArbitrator(db, userId) {
|
|
181
|
+
try {
|
|
182
|
+
const wl = db.prepare('SELECT status FROM arbitrator_whitelist WHERE user_id = ?').get(userId);
|
|
183
|
+
return !!wl && (wl.status ?? 'active') === 'active';
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** 仲裁授权源(PR-B/PR-C):sys_protocol 自动裁决(role='system') 或 active arbitrator_whitelist(唯一 runtime 人类
|
|
190
|
+
* 授权源;role='arbitrator' 旁路已移除)。裁定 arbitrateDispute 与补证 requestEvidence 共用,保证授权一致。
|
|
191
|
+
* system 分支不查白名单表 → 超时自动裁决即便表缺失也不受影响。 */
|
|
192
|
+
function isAuthorizedArbitrator(db, userId) {
|
|
193
|
+
const u = db.prepare('SELECT role FROM users WHERE id = ?').get(userId);
|
|
194
|
+
if (!u)
|
|
195
|
+
return false;
|
|
196
|
+
if (u.role === 'system')
|
|
197
|
+
return true;
|
|
198
|
+
return isActiveWhitelistArbitrator(db, userId);
|
|
199
|
+
}
|
|
159
200
|
export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, refundAmount, liabilityParties, liablePartyId // 指定责任方 user_id(用于 partial_refund 第三方责任场景)
|
|
160
201
|
) {
|
|
161
202
|
const dispute = db.prepare('SELECT * FROM disputes WHERE id = ?').get(disputeId);
|
|
@@ -164,16 +205,19 @@ export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, re
|
|
|
164
205
|
if (dispute.status === 'resolved' || dispute.status === 'dismissed') {
|
|
165
206
|
return { success: false, error: '该争议已处理完毕' };
|
|
166
207
|
}
|
|
167
|
-
|
|
168
|
-
if (!
|
|
169
|
-
return { success: false, error: '
|
|
170
|
-
if (arbitrator.role !== 'arbitrator' && arbitrator.role !== 'system') {
|
|
171
|
-
return { success: false, error: `只有仲裁员才能做出裁定,你的角色是:${arbitrator.role}` };
|
|
208
|
+
// 授权源(PR-B/PR-C):sys_protocol(role=system)或 active arbitrator_whitelist。见 isAuthorizedArbitrator。
|
|
209
|
+
if (!isAuthorizedArbitrator(db, arbitratorId)) {
|
|
210
|
+
return { success: false, error: '仅 active 仲裁员(arbitrator_whitelist)或系统可裁定' };
|
|
172
211
|
}
|
|
173
|
-
//
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
212
|
+
// 非托管(直付)订单:协议不持货款 → 走【只信誉、不动资金】路径,绝不跑 executeSettlement/executeLiabilitySplit 的托管资金链。
|
|
213
|
+
const ord0 = db.prepare('SELECT payment_rail FROM orders WHERE id = ?').get(dispute.order_id);
|
|
214
|
+
const nonCustodial = !!ord0 && ord0.payment_rail === 'direct_p2p';
|
|
215
|
+
// 执行资金处置(非托管 → 零资金终结)
|
|
216
|
+
const settlement = nonCustodial
|
|
217
|
+
? executeNonCustodialSettlement(db, dispute.order_id, ruling)
|
|
218
|
+
: ruling === 'liability_split' && liabilityParties
|
|
219
|
+
? executeLiabilitySplit(db, dispute.order_id, liabilityParties, refundAmount)
|
|
220
|
+
: executeSettlement(db, dispute.order_id, ruling, refundAmount, liablePartyId);
|
|
177
221
|
if (!settlement.success)
|
|
178
222
|
return { success: false, error: settlement.error };
|
|
179
223
|
// 收取仲裁费(败诉/责任方付 1%,最低 1 WAZ)
|
|
@@ -181,7 +225,7 @@ export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, re
|
|
|
181
225
|
.get(dispute.order_id);
|
|
182
226
|
const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
|
|
183
227
|
const arbFees = {};
|
|
184
|
-
if (order) {
|
|
228
|
+
if (order && !nonCustodial) { // 非托管:不收仲裁费(不动任何钱包资金)
|
|
185
229
|
const amt = order.total_amount;
|
|
186
230
|
if (ruling === 'refund_buyer') {
|
|
187
231
|
const f = chargeArbitrationFee(db, order.seller_id, amt, arbitratorId, sysUser.id);
|
|
@@ -233,16 +277,57 @@ export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, re
|
|
|
233
277
|
liability_parties = ?,
|
|
234
278
|
resolved_at = datetime('now')
|
|
235
279
|
WHERE id = ?
|
|
236
|
-
`).run(ruling, reason, ruling,
|
|
280
|
+
`).run(ruling, reason, ruling, nonCustodial ? null : (refundAmount ?? null), // 非托管:不落任何退款/赔付金额(不退款、不赔付)
|
|
281
|
+
JSON.stringify(nonCustodial ? [] : (liabilityParties ?? [])), disputeId);
|
|
237
282
|
return {
|
|
238
283
|
success: true,
|
|
239
|
-
|
|
284
|
+
non_custodial: nonCustodial, // 路由据此跳过佣金/PV/基金分润钩子(非托管无托管结算)
|
|
285
|
+
message: nonCustodial ? getNonCustodialRulingDescription(ruling) : `裁定已执行:${getRulingDescription(ruling, refundAmount)}`,
|
|
240
286
|
settlement: {
|
|
241
287
|
...settlement.detail,
|
|
242
288
|
arbitration_fees: arbFees,
|
|
243
289
|
},
|
|
244
290
|
};
|
|
245
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* 仲裁员【驳回仲裁,退回协商】—— 非胜负裁决:不设 refund/liability、不动 escrow/settlement、不收仲裁费、不扣信誉。
|
|
294
|
+
* 仅对【direct_p2p 且由 payment_query 升级来】的争议有效(履约类争议须裁定,不可退回)。收口与 pq_withdraw 一致:
|
|
295
|
+
* order disputed→payment_query(sys_protocol 代执行,授权由路由的 active-arbitrator + 现场 Passkey + COI/指派门守)、
|
|
296
|
+
* active dispute→dismissed、重建 payment_query_deadline、清 payment_query_cancel_deadline。
|
|
297
|
+
* 【必须由路由用 db.transaction 包裹】(状态回退 + dispute 关闭同一原子边界;否则订单回协商但仲裁视图仍见活跃争议)。
|
|
298
|
+
*/
|
|
299
|
+
export function dismissDisputeToNegotiation(db, disputeId, reason) {
|
|
300
|
+
const dispute = db.prepare('SELECT id, order_id, status FROM disputes WHERE id = ?').get(disputeId);
|
|
301
|
+
if (!dispute)
|
|
302
|
+
return { success: false, error: '争议不存在', error_code: 'DISPUTE_NOT_FOUND' };
|
|
303
|
+
if (dispute.status === 'resolved' || dispute.status === 'dismissed')
|
|
304
|
+
return { success: false, error: '该争议已处理完毕', error_code: 'DISPUTE_ALREADY_RULED' };
|
|
305
|
+
const order = db.prepare('SELECT payment_rail FROM orders WHERE id = ?').get(dispute.order_id);
|
|
306
|
+
if (!order || order.payment_rail !== 'direct_p2p')
|
|
307
|
+
return { success: false, error: '仅直付(direct_p2p)货款协商争议可驳回退回协商', error_code: 'ARBITRATION_DISMISS_NOT_ALLOWED' };
|
|
308
|
+
// ★ 仅可退回【由货款协商升级】的仲裁:最近一次进入 disputed 必须 from payment_query。履约类(delivered→disputed 货损/货不对版)须裁定。fail-closed。
|
|
309
|
+
const lastDisputed = db.prepare("SELECT from_status FROM order_state_history WHERE order_id = ? AND to_status = 'disputed' ORDER BY created_at DESC, rowid DESC LIMIT 1").get(dispute.order_id);
|
|
310
|
+
if (!lastDisputed || lastDisputed.from_status !== 'payment_query')
|
|
311
|
+
return { success: false, error: '仅可退回由货款协商升级的仲裁;履约类争议(货损/货不对版)须经仲裁裁定', error_code: 'NOT_PAYMENT_QUERY_DISPUTE' };
|
|
312
|
+
const sys = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
|
|
313
|
+
if (!sys)
|
|
314
|
+
return { success: false, error: 'sys_protocol 不存在', error_code: 'SYS_MISSING' };
|
|
315
|
+
const tr = transition(db, dispute.order_id, 'payment_query', sys.id, [], '仲裁员驳回仲裁,退回买卖双方协商');
|
|
316
|
+
if (!tr.success)
|
|
317
|
+
return { success: false, error: tr.error, error_code: 'TRANSITION_FAILED' };
|
|
318
|
+
db.prepare("UPDATE disputes SET status='dismissed', verdict_reason=?, ruling_type='dismiss_to_negotiation', resolved_at=datetime('now') WHERE id=?")
|
|
319
|
+
.run(reason ? ('仲裁员驳回仲裁,退回买卖双方协商:' + String(reason).slice(0, 500)) : '仲裁员驳回仲裁,退回买卖双方协商', disputeId);
|
|
320
|
+
// 重建买家响应宽限 + 清卖家取消申诉窗(与 pq_withdraw 收口一致,让 PR-B2 宽限/超时 cron 有截止可依赖)
|
|
321
|
+
let gh = 72;
|
|
322
|
+
try {
|
|
323
|
+
const p = db.prepare("SELECT value FROM protocol_params WHERE key = 'direct_pay.payment_query_grace_hours'").get();
|
|
324
|
+
if (p)
|
|
325
|
+
gh = Math.max(1, Number(p.value) || 72);
|
|
326
|
+
}
|
|
327
|
+
catch { /* 用默认 72h */ }
|
|
328
|
+
db.prepare("UPDATE orders SET payment_query_deadline = datetime('now', ?), payment_query_cancel_deadline = NULL WHERE id = ?").run(`+${gh} hours`, dispute.order_id);
|
|
329
|
+
return { success: true, order_id: dispute.order_id };
|
|
330
|
+
}
|
|
246
331
|
/**
|
|
247
332
|
* 执行多方责任分配结算
|
|
248
333
|
*
|
|
@@ -269,12 +354,8 @@ export function executeLiabilitySplit(db, orderId, liabilityParties, buyerRefund
|
|
|
269
354
|
const buyerId = order.buyer_id;
|
|
270
355
|
const sellerId = order.seller_id;
|
|
271
356
|
const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
|
|
272
|
-
const product = db.prepare('SELECT stake_amount FROM products WHERE id = ?')
|
|
273
|
-
.get(order.product_id);
|
|
274
|
-
const stakeAmount = product?.stake_amount ?? 0;
|
|
275
357
|
// RFC-014:整数 base-units
|
|
276
358
|
const totalU = toUnits(totalAmount);
|
|
277
|
-
const stakeAmountU = toUnits(stakeAmount);
|
|
278
359
|
const actualRefundU = Math.min(toUnits(buyerRefund ?? totalAmount), totalU);
|
|
279
360
|
const sellerEscrowShareU = totalU - actualRefundU; // 残值,精确
|
|
280
361
|
// 预先计算各责任方实际扣款(资金守恒:责任方扣款 = 协议金库收入),全整数 units
|
|
@@ -317,6 +398,10 @@ export function executeLiabilitySplit(db, orderId, liabilityParties, buyerRefund
|
|
|
317
398
|
if (totalToTreasuryU > 0)
|
|
318
399
|
applyWalletDelta(db, sysUser.id, { balance: totalToTreasuryU });
|
|
319
400
|
// ── C. 卖家商品质押处理 ───────────────────────────────────────
|
|
401
|
+
// 【必须在 B 段责任罚款之后 re-read staked】:若 seller 同为责任方,B 段已从其 staked 扣过一次;此处只动【本单锁定
|
|
402
|
+
// 背书 order.stake_backing】且 cap 到【剩余】staked → 避免同一笔质押被扣两次 / staked 打负 / 凭空印钱。
|
|
403
|
+
// 不读名义 products.stake_amount(见 executeSettlement 注释);stage-1 backing=0 → 本段恒 no-op。
|
|
404
|
+
const stakeAmountU = Math.max(0, Math.min(toUnits(Number(order.stake_backing) || 0), walletUnits(db, sellerId).staked));
|
|
320
405
|
if (stakeAmountU > 0) {
|
|
321
406
|
if (sellerLiability) {
|
|
322
407
|
// 卖家有责:按责任金额扣罚质押(封顶),剩余返还
|
|
@@ -384,13 +469,14 @@ export function executeSettlement(db, orderId, ruling, refundAmount, liableParty
|
|
|
384
469
|
const totalAmount = order.total_amount;
|
|
385
470
|
const buyerId = order.buyer_id;
|
|
386
471
|
const sellerId = order.seller_id;
|
|
387
|
-
const product = db.prepare('SELECT stake_amount FROM products WHERE id = ?')
|
|
388
|
-
.get(order.product_id);
|
|
389
|
-
const stakeAmount = product?.stake_amount ?? 0;
|
|
390
472
|
const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
|
|
391
473
|
// RFC-014:整数 base-units
|
|
392
474
|
const totalU = toUnits(totalAmount);
|
|
393
|
-
|
|
475
|
+
// 卖家质押 forfeit/return 只动【本订单实际锁定的赔付背书】order.stake_backing,再 cap 到卖家真实 staked。
|
|
476
|
+
// 绝不读 products.stake_amount —— 那是名义"预期 stake",完成时才锁(server.ts 结算);争议单 pre-completion 从未锁,
|
|
477
|
+
// 读它会没收/返还从未锁定的质押 → 打负 staked / 凭空印钱。RFC-008 stage-1 免赔付:stake_backing 恒 0 → 本段恒 no-op,
|
|
478
|
+
// 与"违约只退款不没收"一致;Phase-3 真正在建单锁 stake 后,同一段自动按真实 backing 处置。
|
|
479
|
+
const stakeAmountU = Math.max(0, Math.min(toUnits(Number(order.stake_backing) || 0), walletUnits(db, sellerId).staked));
|
|
394
480
|
if (ruling === 'refund_buyer') {
|
|
395
481
|
// ── 买家胜诉:退全款 + 卖家损失一半质押(惩罚,allocate 精确二分)──────────────
|
|
396
482
|
const [penaltyU, stakeReturnU] = stakeAmountU > 0 ? allocate(stakeAmountU, [1, 1]) : [0, 0];
|
|
@@ -589,19 +675,21 @@ export function initEvidenceRequestSchema(db) {
|
|
|
589
675
|
* 仲裁员向任意角色发出"补充证据"请求
|
|
590
676
|
*/
|
|
591
677
|
export function requestEvidence(db, disputeId, arbitratorId, requestedFromId, evidenceTypes, description, deadlineHours = 48) {
|
|
592
|
-
|
|
593
|
-
if (!
|
|
594
|
-
return { success: false, error: '
|
|
678
|
+
// PR-C:授权源同 arbitrateDispute —— system 或 active arbitrator_whitelist(role 旁路已移除)。
|
|
679
|
+
if (!isAuthorizedArbitrator(db, arbitratorId)) {
|
|
680
|
+
return { success: false, error: '仅 active 仲裁员可发出证据请求', error_code: 'NOT_ARBITRATOR' };
|
|
595
681
|
}
|
|
596
|
-
const dispute = db.prepare('SELECT status FROM disputes WHERE id = ?').get(disputeId);
|
|
682
|
+
const dispute = db.prepare('SELECT status, order_id, initiator_id, defendant_id FROM disputes WHERE id = ?').get(disputeId);
|
|
597
683
|
if (!dispute)
|
|
598
684
|
return { success: false, error: '争议不存在' };
|
|
599
685
|
if (dispute.status === 'resolved' || dispute.status === 'dismissed') {
|
|
600
686
|
return { success: false, error: '该争议已结案' };
|
|
601
687
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
688
|
+
// PR-E:补证对象必须是本案涉案方(buyer/seller/logistics/initiator/defendant),不得向无关用户要证据。
|
|
689
|
+
const order = db.prepare('SELECT buyer_id, seller_id, logistics_id FROM orders WHERE id = ?').get(dispute.order_id);
|
|
690
|
+
const parties = new Set([order?.buyer_id, order?.seller_id, order?.logistics_id, dispute.initiator_id, dispute.defendant_id].filter(Boolean));
|
|
691
|
+
if (!parties.has(requestedFromId))
|
|
692
|
+
return { success: false, error: '补证对象必须是本案涉案方', error_code: 'INVALID_EVIDENCE_TARGET' };
|
|
605
693
|
const requestId = generateId('evr');
|
|
606
694
|
db.prepare(`
|
|
607
695
|
INSERT INTO dispute_evidence_requests
|
|
@@ -679,10 +767,12 @@ export async function getDisputeDetails(_db, disputeId) {
|
|
|
679
767
|
return (await dbOne(`
|
|
680
768
|
SELECT d.*,
|
|
681
769
|
u1.name as initiator_name, u1.role as initiator_role,
|
|
682
|
-
u2.name as defendant_name, u2.role as defendant_role
|
|
770
|
+
u2.name as defendant_name, u2.role as defendant_role,
|
|
771
|
+
o.payment_rail as payment_rail
|
|
683
772
|
FROM disputes d
|
|
684
773
|
LEFT JOIN users u1 ON d.initiator_id = u1.id
|
|
685
774
|
LEFT JOIN users u2 ON d.defendant_id = u2.id
|
|
775
|
+
LEFT JOIN orders o ON d.order_id = o.id
|
|
686
776
|
WHERE d.id = ?
|
|
687
777
|
`, [disputeId])) ?? null;
|
|
688
778
|
}
|
|
@@ -690,10 +780,12 @@ export async function getOrderDispute(_db, orderId) {
|
|
|
690
780
|
return (await dbOne(`
|
|
691
781
|
SELECT d.*,
|
|
692
782
|
u1.name as initiator_name, u1.role as initiator_role,
|
|
693
|
-
u2.name as defendant_name, u2.role as defendant_role
|
|
783
|
+
u2.name as defendant_name, u2.role as defendant_role,
|
|
784
|
+
o.payment_rail as payment_rail
|
|
694
785
|
FROM disputes d
|
|
695
786
|
LEFT JOIN users u1 ON d.initiator_id = u1.id
|
|
696
787
|
LEFT JOIN users u2 ON d.defendant_id = u2.id
|
|
788
|
+
LEFT JOIN orders o ON d.order_id = o.id
|
|
697
789
|
WHERE d.order_id = ? AND d.status NOT IN ('resolved', 'dismissed')
|
|
698
790
|
ORDER BY d.created_at DESC LIMIT 1
|
|
699
791
|
`, [orderId])) ?? null;
|
|
@@ -703,7 +795,7 @@ export async function getOpenDisputes(_db) {
|
|
|
703
795
|
SELECT d.*,
|
|
704
796
|
u1.name as initiator_name, u1.role as initiator_role,
|
|
705
797
|
u2.name as defendant_name, u2.role as defendant_role,
|
|
706
|
-
o.total_amount, o.status as order_status
|
|
798
|
+
o.total_amount, o.status as order_status, o.payment_rail
|
|
707
799
|
FROM disputes d
|
|
708
800
|
LEFT JOIN users u1 ON d.initiator_id = u1.id
|
|
709
801
|
LEFT JOIN users u2 ON d.defendant_id = u2.id
|
|
@@ -724,3 +816,13 @@ function getRulingDescription(ruling, refundAmount) {
|
|
|
724
816
|
default: return ruling;
|
|
725
817
|
}
|
|
726
818
|
}
|
|
819
|
+
// 非托管(直付)裁定文案:协议不持货款 → 只表达胜负/责任(信誉裁决),【绝不】写退款/资金释放/仲裁费(那些都不发生)。
|
|
820
|
+
function getNonCustodialRulingDescription(ruling) {
|
|
821
|
+
switch (ruling) {
|
|
822
|
+
case 'refund_buyer': return '裁定已执行:买家胜诉(非托管信誉裁决 —— 协议不持货款,不发生退款/资金释放/仲裁费)';
|
|
823
|
+
case 'release_seller': return '裁定已执行:卖家胜诉(非托管信誉裁决 —— 不发生退款/资金释放/仲裁费)';
|
|
824
|
+
case 'partial_refund': return '裁定已执行:部分责任(非托管信誉裁决 —— 不发生退款/资金释放/仲裁费)';
|
|
825
|
+
case 'liability_split': return '裁定已执行:责任分配(非托管信誉裁决 —— 不发生退款/赔付/仲裁费)';
|
|
826
|
+
default: return '裁定已执行:非托管信誉裁决(不动任何资金)';
|
|
827
|
+
}
|
|
828
|
+
}
|
|
@@ -58,18 +58,30 @@ export function ensureEvidenceColumns(db) {
|
|
|
58
58
|
function canonicalEvidenceMeta(m) {
|
|
59
59
|
return [m.uploaderId, m.disputeId, m.hash, String(m.size), m.mime, m.description].join('|');
|
|
60
60
|
}
|
|
61
|
+
// PR-E:被指派仲裁员读/写证据须【仍是 active whitelist】—— suspended/revoked 立即失去权限。表缺失 → 视为非 active。
|
|
62
|
+
function isActiveArbitratorWL(db, userId) {
|
|
63
|
+
try {
|
|
64
|
+
const wl = db.prepare('SELECT status FROM arbitrator_whitelist WHERE user_id = ?').get(userId);
|
|
65
|
+
return !!wl && ((wl.status ?? 'active') === 'active'); // legacy NULL = active
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
61
71
|
function isPartyOrArbitrator(db, disputeId, userId) {
|
|
62
72
|
const dispute = db.prepare('SELECT order_id, initiator_id, defendant_id, assigned_arbitrators FROM disputes WHERE id = ?').get(disputeId);
|
|
63
73
|
if (!dispute)
|
|
64
74
|
return false;
|
|
65
75
|
const order = db.prepare('SELECT buyer_id, seller_id, logistics_id FROM orders WHERE id = ?').get(dispute.order_id);
|
|
66
|
-
const
|
|
67
|
-
const parties = new Set([
|
|
76
|
+
const caseParties = new Set([
|
|
68
77
|
order?.buyer_id, order?.seller_id, order?.logistics_id,
|
|
69
78
|
dispute.initiator_id, dispute.defendant_id,
|
|
70
|
-
...arbitrators,
|
|
71
79
|
].filter(Boolean));
|
|
72
|
-
|
|
80
|
+
if (caseParties.has(userId))
|
|
81
|
+
return true;
|
|
82
|
+
// 仅【指派本案 且 当前 active】的仲裁员可读/写证据。
|
|
83
|
+
const arbitrators = JSON.parse(dispute.assigned_arbitrators || '[]');
|
|
84
|
+
return arbitrators.includes(userId) && isActiveArbitratorWL(db, userId);
|
|
73
85
|
}
|
|
74
86
|
export function uploadEvidence(db, args) {
|
|
75
87
|
if (!args.blob || args.blob.length === 0)
|