@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
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
|
|
2
2
|
import { releaseFeeStake } from '../../direct-pay-ledger.js';
|
|
3
|
+
import { restorePreShipDirectPayStock } from '../../direct-pay-stock.js'; // D3 库存回补唯一入口(pre-ship 放行;已出库拒绝)
|
|
4
|
+
import { notifyTransition, createNotification } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
5
|
+
import { expireDeferrals, listExpiringDeferrals, markDeferralReminded, suspendPrivilegeOnDeferralExpiry } from '../../direct-receive-deferral.js'; // B4:缓交到期提醒/收口
|
|
3
6
|
const SYS = 'sys_protocol';
|
|
7
|
+
/** 货款协商买家申诉窗(天),governance 可调,默认 7d。与 request_cancel 同源 param,保证手动/自动一致。 */
|
|
8
|
+
function recourseDays(db) {
|
|
9
|
+
try {
|
|
10
|
+
const row = db.prepare("SELECT value FROM protocol_params WHERE key = 'direct_pay.payment_query_recourse_days'").get();
|
|
11
|
+
const n = Number(row?.value ?? 7);
|
|
12
|
+
return Number.isFinite(n) && n > 0 ? Math.max(1, n) : 7;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return 7;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
4
18
|
/** 宽限期(小时),governance 可调,默认 48h(2d)。不硬编码。 */
|
|
5
19
|
function graceHours(db) {
|
|
6
20
|
try {
|
|
@@ -17,16 +31,92 @@ export function runDirectPayTimeoutSweep(deps) {
|
|
|
17
31
|
const gh = graceHours(db);
|
|
18
32
|
const windowExpired = [];
|
|
19
33
|
const graceCancelled = [];
|
|
34
|
+
const pqRecourseOpened = [];
|
|
35
|
+
const pqCancelled = [];
|
|
36
|
+
const acceptExpired = [];
|
|
37
|
+
const deferralReminded = [];
|
|
38
|
+
const deferralExpired = [];
|
|
39
|
+
// F. 缓交收口(B4):① 到期前提醒(param direct_pay.deferral_reminder_days,默认 3d;reminder_sent_at 去重)
|
|
40
|
+
// ② 过 grace 到期 → expired + 无生产 bond 则停权(有 bond=已缴清兜底不停)+ 通知。均 fail-soft 不断 cron。
|
|
41
|
+
try {
|
|
42
|
+
const remindDays = (() => { try {
|
|
43
|
+
const r = db.prepare("SELECT value FROM protocol_params WHERE key = 'direct_pay.deferral_reminder_days'").get();
|
|
44
|
+
const n = Number(r?.value ?? 3);
|
|
45
|
+
return Number.isFinite(n) && n > 0 ? n : 3;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return 3;
|
|
49
|
+
} })();
|
|
50
|
+
for (const d of listExpiringDeferrals(db, new Date().toISOString(), remindDays)) {
|
|
51
|
+
markDeferralReminded(db, d.id);
|
|
52
|
+
deferralReminded.push(d.id);
|
|
53
|
+
try {
|
|
54
|
+
createNotification(db, d.user_id, null, 'deferral_expiring_soon', '⏰ 保证金缓交即将到期', `你的缓交资格将于 ${d.expires_at} 到期。请在到期前缴纳履约保证金转正式(设置页-直付履约保证金),否则宽限期后直付资格将关闭。`, { templateKey: 'deferral_expiring_soon', params: { expires: d.expires_at } });
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.warn('[direct-pay-timeouts] notify deferral-remind:', e.message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const uid of expireDeferrals(db, new Date().toISOString()).expired) {
|
|
61
|
+
const suspended = suspendPrivilegeOnDeferralExpiry(db, uid);
|
|
62
|
+
deferralExpired.push(uid);
|
|
63
|
+
try {
|
|
64
|
+
createNotification(db, uid, null, 'deferral_expired', '🚫 保证金缓交已到期', suspended ? '缓交资格已到期且未缴纳保证金,直付资格已关闭。缴纳履约保证金并经运营确认后可重新开通;在途订单不受影响,请正常履约完成。' : '缓交资格已到期;你已缴纳保证金,直付资格不受影响。', { templateKey: 'deferral_expired', params: { closed: suspended ? 1 : 0 } });
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
console.warn('[direct-pay-timeouts] notify deferral-expired:', e.message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
console.error('[direct-pay-timeouts] deferral closure sweep:', e);
|
|
73
|
+
}
|
|
74
|
+
// E. 手动接单窗超时(v16):pending_accept 过 pending_accept_deadline → 无责取消 + 回补库存。
|
|
75
|
+
// 零资金(此阶段没人付过钱 —— 时序门);双方通知。WHERE 硬门:仅 now > deadline 命中。
|
|
76
|
+
const acceptRows = db.prepare(`
|
|
77
|
+
SELECT id, product_id, quantity, buyer_id, seller_id FROM orders
|
|
78
|
+
WHERE status = 'pending_accept'
|
|
79
|
+
AND payment_rail = 'direct_p2p'
|
|
80
|
+
AND pending_accept_deadline IS NOT NULL
|
|
81
|
+
AND datetime(pending_accept_deadline) < datetime('now')
|
|
82
|
+
LIMIT 1000
|
|
83
|
+
`).all();
|
|
84
|
+
for (const { id, product_id, quantity, buyer_id, seller_id } of acceptRows) {
|
|
85
|
+
let done = false;
|
|
86
|
+
db.transaction(() => {
|
|
87
|
+
const r = transition(db, id, 'cancelled', SYS, [], '手动接单:卖家超时未确认接单,系统无责取消(付款前,零资金)');
|
|
88
|
+
if (!r.success)
|
|
89
|
+
return;
|
|
90
|
+
restorePreShipDirectPayStock(db, { fromStatus: 'pending_accept', productId: product_id, quantity });
|
|
91
|
+
acceptExpired.push(id);
|
|
92
|
+
done = true;
|
|
93
|
+
})();
|
|
94
|
+
if (done) {
|
|
95
|
+
try {
|
|
96
|
+
createNotification(db, buyer_id, id, 'direct_pay_accept_expired', '⏰ 卖家超时未接单,订单已取消', '卖家未在接单窗口内确认,订单已自动取消 —— 你尚未付款,无需任何操作。可换商品或联系卖家后重新下单。', { templateKey: 'dp_pending_accept_expired_buyer', params: {} });
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.warn('[direct-pay-timeouts] notify accept-expired buyer:', e.message);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
createNotification(db, seller_id, id, 'direct_pay_accept_expired', '⏰ 订单因超时未接单已取消', '你未在接单窗口内确认接单,订单已自动取消,库存已恢复。频繁超时会影响买家体验,可考虑改用自动接单或缩短响应时间。', { templateKey: 'dp_pending_accept_expired_seller', params: {} });
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
console.warn('[direct-pay-timeouts] notify accept-expired seller:', e.message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
20
109
|
// A. 付款窗口超时 → 可争议态(非静默关单)+ 退费用质押 + 设宽限期
|
|
21
110
|
const windowRows = db.prepare(`
|
|
22
|
-
SELECT id FROM orders
|
|
111
|
+
SELECT id, buyer_id FROM orders
|
|
23
112
|
WHERE status = 'direct_pay_window'
|
|
24
113
|
AND payment_rail = 'direct_p2p'
|
|
25
114
|
AND direct_pay_window_deadline IS NOT NULL
|
|
26
115
|
AND datetime(direct_pay_window_deadline) < datetime('now')
|
|
27
116
|
LIMIT 1000
|
|
28
117
|
`).all();
|
|
29
|
-
for (const { id } of windowRows) {
|
|
118
|
+
for (const { id, buyer_id } of windowRows) {
|
|
119
|
+
let done = false;
|
|
30
120
|
db.transaction(() => {
|
|
31
121
|
const r = transition(db, id, 'direct_expired_unconfirmed', SYS, [], 'Rail1 直付:付款窗口超时未标记');
|
|
32
122
|
if (!r.success)
|
|
@@ -34,33 +124,120 @@ export function runDirectPayTimeoutSweep(deps) {
|
|
|
34
124
|
releaseFeeStake(db, { orderId: id }); // 费用质押退卖家(无完成,不收费)
|
|
35
125
|
db.prepare(`UPDATE orders SET direct_grace_deadline = datetime('now', ?) WHERE id = ?`).run(`+${gh} hours`, id);
|
|
36
126
|
windowExpired.push(id);
|
|
127
|
+
done = true;
|
|
37
128
|
})();
|
|
129
|
+
// 审计项 B(N2):此前窗口静默过期,卡点付款的买家毫不知情直到 48h 后自动取消。tx 外 fail-soft。
|
|
130
|
+
if (done) {
|
|
131
|
+
try {
|
|
132
|
+
createNotification(db, buyer_id, id, 'direct_pay_window_expired', '⏰ 直付付款窗口已过期', `若你已付款:请在 ${gh} 小时宽限期内到订单页提交付款凭证发起争议;未付款可直接关闭订单,否则宽限期满将自动取消。`, { templateKey: 'dp_window_expired', params: { graceHours: gh } });
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
console.warn('[direct-pay-timeouts] notify window-expired:', e.message);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
38
138
|
}
|
|
39
139
|
// B. 宽限期满 → 系统关单(★ WHERE 保证仅 now>grace 命中;宽限期内绝不关,买家 →disputed 全程可用)
|
|
40
140
|
const graceRows = db.prepare(`
|
|
41
|
-
SELECT id FROM orders
|
|
141
|
+
SELECT id, product_id, quantity, buyer_id, seller_id FROM orders
|
|
42
142
|
WHERE status = 'direct_expired_unconfirmed'
|
|
43
143
|
AND payment_rail = 'direct_p2p'
|
|
44
144
|
AND direct_grace_deadline IS NOT NULL
|
|
45
145
|
AND datetime(direct_grace_deadline) < datetime('now')
|
|
46
146
|
LIMIT 1000
|
|
47
147
|
`).all();
|
|
48
|
-
for (const { id } of graceRows) {
|
|
148
|
+
for (const { id, product_id, quantity, buyer_id, seller_id } of graceRows) {
|
|
149
|
+
let done = false;
|
|
49
150
|
db.transaction(() => {
|
|
50
151
|
const r = transition(db, id, 'cancelled', SYS, [], 'Rail1 直付:宽限期满,买家未付款/未升级争议,系统关单');
|
|
51
|
-
if (r.success)
|
|
52
|
-
|
|
152
|
+
if (!r.success)
|
|
153
|
+
return;
|
|
154
|
+
restorePreShipDirectPayStock(db, { fromStatus: 'direct_expired_unconfirmed', productId: product_id, quantity }); // D3 回补(pre-ship 唯一入口)
|
|
155
|
+
graceCancelled.push(id);
|
|
156
|
+
done = true;
|
|
157
|
+
})();
|
|
158
|
+
// 审计项 B(N2):自动关单也要告知双方(此前静默,买家不知单没了、卖家不知库存已回)。tx 外 fail-soft。
|
|
159
|
+
if (done) {
|
|
160
|
+
try {
|
|
161
|
+
createNotification(db, buyer_id, id, 'direct_pay_grace_cancelled', '🚫 直付订单已自动取消', '付款窗口与宽限期均已过且未收到你的付款标记/凭证,订单已关闭。若你确已付款,请通过订单页联系卖家协商。', { templateKey: 'dp_grace_cancelled_buyer', params: {} });
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
console.warn('[direct-pay-timeouts] notify grace-cancel buyer:', e.message);
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
createNotification(db, seller_id, id, 'direct_pay_grace_cancelled', '🚫 直付订单已自动取消(买家未付款)', '买家未在付款窗口+宽限期内付款,订单已自动关闭,库存已恢复。', { templateKey: 'dp_grace_cancelled_seller', params: {} });
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
console.warn('[direct-pay-timeouts] notify grace-cancel seller:', e.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// D. 货款协商买家静默:payment_query 已过买家响应宽限(payment_query_deadline)且卖家未手动请求取消
|
|
175
|
+
// (payment_query_cancel_deadline 仍空)→ 系统【代卖家自动启动取消申请】(等价 request_cancel):
|
|
176
|
+
// 设 payment_query_cancel_deadline = now + recourse_days + 通知买家最后申诉窗已开。★ 不关单、不动状态、不涉资金;
|
|
177
|
+
// 买家窗内仍可 pq_escalate 主张已付,订单离开 payment_query 后本 sweep 与 Sweep C 均不再命中。之后由 Sweep C 窗满关单。
|
|
178
|
+
// 幂等:WHERE payment_query_cancel_deadline IS NULL —— 卖家已手动 request_cancel 的单不被重置,重复 cron 也不刷新窗口。
|
|
179
|
+
const rd = recourseDays(db);
|
|
180
|
+
const pqSilentRows = db.prepare(`
|
|
181
|
+
SELECT id, buyer_id FROM orders
|
|
182
|
+
WHERE status = 'payment_query'
|
|
183
|
+
AND payment_rail = 'direct_p2p'
|
|
184
|
+
AND payment_query_deadline IS NOT NULL
|
|
185
|
+
AND datetime(payment_query_deadline) < datetime('now')
|
|
186
|
+
AND payment_query_cancel_deadline IS NULL
|
|
187
|
+
LIMIT 1000
|
|
188
|
+
`).all();
|
|
189
|
+
for (const { id, buyer_id } of pqSilentRows) {
|
|
190
|
+
const res = db.prepare(`UPDATE orders SET payment_query_cancel_deadline = datetime('now', ?) WHERE id = ? AND payment_query_cancel_deadline IS NULL`).run(`+${rd} days`, id);
|
|
191
|
+
if (res.changes === 0)
|
|
192
|
+
continue; // 竞态:同 tick 已被设(卖家 request_cancel)——不重复处理
|
|
193
|
+
pqRecourseOpened.push(id);
|
|
194
|
+
try {
|
|
195
|
+
const dl = db.prepare("SELECT payment_query_cancel_deadline d FROM orders WHERE id = ?").get(id).d;
|
|
196
|
+
createNotification(db, buyer_id, id, 'payment_query_cancel_requested', '⏳ 卖家申请取消订单', `卖家称未收到货款且你未在响应期内回应,系统已代卖家发起取消。你约有 ${rd} 天:若确已付款请提供付款参考或发起举证(pq_escalate),否则订单将于 ${dl} 自动取消。直付非托管,无平台退款。`);
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
console.warn('[direct-pay-timeouts] notify pq-recourse:', e.message);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// C. 货款协商:卖家已请求取消(payment_query_cancel_deadline 已设)+ 买家申诉窗满 → 系统关单 + 退费用质押。
|
|
203
|
+
// ★ 窗内(now < deadline)绝不关:买家全程可主张已付/升级举证(pq_escalate 回 disputed)。
|
|
204
|
+
const pqCancelRows = db.prepare(`
|
|
205
|
+
SELECT id, product_id, quantity FROM orders
|
|
206
|
+
WHERE status = 'payment_query'
|
|
207
|
+
AND payment_rail = 'direct_p2p'
|
|
208
|
+
AND payment_query_cancel_deadline IS NOT NULL
|
|
209
|
+
AND datetime(payment_query_cancel_deadline) < datetime('now')
|
|
210
|
+
LIMIT 1000
|
|
211
|
+
`).all();
|
|
212
|
+
for (const { id, product_id, quantity } of pqCancelRows) {
|
|
213
|
+
let done = false;
|
|
214
|
+
db.transaction(() => {
|
|
215
|
+
const r = transition(db, id, 'cancelled', SYS, [], 'Rail1 直付:货款协商申诉窗满,买家未回应/未升级,系统关单');
|
|
216
|
+
if (!r.success)
|
|
217
|
+
return;
|
|
218
|
+
releaseFeeStake(db, { orderId: id }); // 无完成不收费:退卖家费用质押
|
|
219
|
+
restorePreShipDirectPayStock(db, { fromStatus: 'payment_query', productId: product_id, quantity }); // D3 回补(pre-ship 唯一入口)
|
|
220
|
+
done = true;
|
|
53
221
|
})();
|
|
222
|
+
if (done) {
|
|
223
|
+
pqCancelled.push(id);
|
|
224
|
+
try {
|
|
225
|
+
notifyTransition(db, id, 'payment_query', 'cancelled');
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
console.warn('[direct-pay-timeouts] notify pq-cancel:', e.message);
|
|
229
|
+
}
|
|
230
|
+
} // 通知买卖双方(cron 系统关单也要发,route 之外)
|
|
54
231
|
}
|
|
55
|
-
return { windowExpired, graceCancelled };
|
|
232
|
+
return { windowExpired, graceCancelled, pqRecourseOpened, pqCancelled, acceptExpired, deferralReminded, deferralExpired };
|
|
56
233
|
}
|
|
57
234
|
export function startDirectPayTimeoutCron(deps) {
|
|
58
235
|
const ms = 10 * 60 * 1000; // 10min — 付款窗口最短 30min,10min 粒度足够
|
|
59
236
|
setInterval(() => {
|
|
60
237
|
try {
|
|
61
238
|
const r = runDirectPayTimeoutSweep(deps);
|
|
62
|
-
if (r.windowExpired.length || r.graceCancelled.length) {
|
|
63
|
-
console.log(`[direct-pay-timeouts] window→expired ${r.windowExpired.length}, grace→cancelled ${r.graceCancelled.length}`);
|
|
239
|
+
if (r.windowExpired.length || r.graceCancelled.length || r.pqRecourseOpened.length || r.pqCancelled.length || r.acceptExpired.length || r.deferralReminded.length || r.deferralExpired.length) {
|
|
240
|
+
console.log(`[direct-pay-timeouts] window→expired ${r.windowExpired.length}, grace→cancelled ${r.graceCancelled.length}, pq→recourse ${r.pqRecourseOpened.length}, pq→cancelled ${r.pqCancelled.length}, accept→expired ${r.acceptExpired.length}, deferral remind/expired ${r.deferralReminded.length}/${r.deferralExpired.length}`);
|
|
64
241
|
}
|
|
65
242
|
}
|
|
66
243
|
catch (e) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
2
|
export function registerDisputesReadRoutes(app, deps) {
|
|
3
|
-
const { db, auth, errorRes, getOpenDisputes, getDisputeDetails, getEvidenceRequests, listEvidenceFiles, isEligibleArbitrator } = deps;
|
|
3
|
+
const { db, auth, errorRes, getOpenDisputes, getDisputeDetails, getEvidenceRequests, listEvidenceFiles, isEligibleArbitrator, isArbitrationAdmin } = deps;
|
|
4
4
|
// 仲裁员:查看所有开放争议
|
|
5
5
|
app.get('/api/disputes', async (req, res) => {
|
|
6
6
|
const user = auth(req, res);
|
|
@@ -19,8 +19,8 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
19
19
|
const dispute = await dbOne('SELECT id, order_id, reason, initiator_id, defendant_id FROM disputes WHERE id = ?', [req.params.id]);
|
|
20
20
|
if (!dispute)
|
|
21
21
|
return void res.status(404).json({ error: '争议不存在' });
|
|
22
|
-
|
|
23
|
-
if (dispute.initiator_id !== user.id && dispute.defendant_id !== user.id &&
|
|
22
|
+
// PR-E fix:同详情/parties —— party 或 active whitelist 仲裁员;不再用 role === 'arbitrator'。
|
|
23
|
+
if (dispute.initiator_id !== user.id && dispute.defendant_id !== user.id && !isEligibleArbitrator(user.id).ok) {
|
|
24
24
|
return void res.status(403).json({ error: '无权查看' });
|
|
25
25
|
}
|
|
26
26
|
const order = await dbOne('SELECT product_id FROM orders WHERE id = ?', [dispute.order_id]);
|
|
@@ -90,12 +90,16 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
90
90
|
const dispute = await getDisputeDetails(db, req.params.id);
|
|
91
91
|
if (!dispute)
|
|
92
92
|
return void res.status(404).json({ error: '争议不存在' });
|
|
93
|
-
|
|
94
|
-
//
|
|
93
|
+
// PR-E:允许 发起方 / 被告方 / 物流方 / active whitelist 仲裁员。不再用 role === 'arbitrator'(旧旁路 →
|
|
94
|
+
// suspended/revoked/role-only 可越权读、whitelist-only 买家被错误挡)。授权源统一为 isEligibleArbitrator。
|
|
95
95
|
const orderForAuth = await dbOne('SELECT logistics_id FROM orders WHERE id = ?', [dispute.order_id]);
|
|
96
96
|
const isLogisticsParty = orderForAuth?.logistics_id === user.id;
|
|
97
|
+
// 当事方 / active 白名单仲裁员 / arbitration-admin(争议查看 —— 只读监督)。admin 门用 isArbitrationAdmin(user)
|
|
98
|
+
// 而非 user.role==='admin':与后台列表 /admin/disputes(requireArbitrationAdmin)【同一】授权模型,否则会错放
|
|
99
|
+
// (role='admin' 但无 arbitration 权限的 admin 绕过列表边界直读详情)/ 错挡(roles=["buyer","admin"] 的 admin 进不去)。
|
|
100
|
+
// 裁定/驳回等【动作】仍在 disputes-write 由 isEligibleArbitrator + 指派 + COI 门守,admin 只看不能裁。
|
|
97
101
|
if (dispute.initiator_id !== user.id && dispute.defendant_id !== user.id
|
|
98
|
-
&& !isLogisticsParty &&
|
|
102
|
+
&& !isLogisticsParty && !isEligibleArbitrator(user.id).ok && !isArbitrationAdmin(user)) {
|
|
99
103
|
return void res.status(403).json({ error: '无权查看此争议' });
|
|
100
104
|
}
|
|
101
105
|
// 原告证据 — 从状态机历史中取 disputed 转移时附带的
|
|
@@ -308,8 +312,13 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
308
312
|
});
|
|
309
313
|
}
|
|
310
314
|
events.sort((a, b) => String(a.ts).localeCompare(String(b.ts)));
|
|
315
|
+
// 仲裁员【驳回仲裁,退回协商】可达性(与 dismissDisputeToNegotiation / pq_withdraw 同谓词):direct_p2p + 最近一次进入
|
|
316
|
+
// disputed 是 from payment_query + 未裁定。前端据此才在裁定面板显示"驳回仲裁"选项;真正拦截仍在路由/域函数。
|
|
317
|
+
const lastDispFrom = 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", [dispute.order_id]);
|
|
318
|
+
const canDismissToNegotiation = dispute.payment_rail === 'direct_p2p' && lastDispFrom?.from_status === 'payment_query' && (dispute.status === 'open' || dispute.status === 'in_review');
|
|
311
319
|
res.json({
|
|
312
320
|
...dispute,
|
|
321
|
+
can_dismiss_to_negotiation: canDismissToNegotiation,
|
|
313
322
|
plaintiff_evidence: plaintiffEvidence,
|
|
314
323
|
defendant_evidence: defendantEvidence,
|
|
315
324
|
party_evidence: partyEvidence,
|
|
@@ -344,6 +353,11 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
344
353
|
if (!dispute)
|
|
345
354
|
return void res.status(404).json({ error: '争议不存在' });
|
|
346
355
|
const order = await dbOne('SELECT buyer_id, seller_id, logistics_id FROM orders WHERE id = ?', [dispute.order_id]);
|
|
356
|
+
// PR-E:同详情权限门 —— 仅 涉案方 或 active whitelist 仲裁员可读涉案方名单(此前任意登录用户可读=泄露)。
|
|
357
|
+
const isParty = [order?.buyer_id, order?.seller_id, order?.logistics_id, dispute.initiator_id, dispute.defendant_id].filter(Boolean).includes(user.id);
|
|
358
|
+
if (!isParty && !isEligibleArbitrator(user.id).ok) {
|
|
359
|
+
return void res.status(403).json({ error: '无权查看涉案方' });
|
|
360
|
+
}
|
|
347
361
|
const partyIds = [dispute.initiator_id, dispute.defendant_id, order?.logistics_id].filter(Boolean);
|
|
348
362
|
const uniqueIds = [...new Set(partyIds)];
|
|
349
363
|
const parties = (await Promise.all(uniqueIds.map(id => dbOne('SELECT id, name, role FROM users WHERE id = ?', [id])))).filter(Boolean);
|
|
@@ -7,8 +7,33 @@ import { applyWalletDelta } from '../../ledger.js';
|
|
|
7
7
|
// RFC-016 Phase 1 — 纯只读端点/校验读/SNF 分发读/标记写 → async seam;arbitrate 仲裁核心(原子领取 +
|
|
8
8
|
// 2 settlement db.transaction + reputation/strike/publish)与 tx 内 appendAuditLog 保持同步(Phase 3 迁 pg)。
|
|
9
9
|
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
|
|
10
|
+
import { arbitratorHasConflict } from '../arbitrator-lifecycle.js'; // PR-B COI 硬门
|
|
11
|
+
import { dismissDisputeToNegotiation } from '../../layer3-trust/L3-1-dispute-engine/dispute-engine.js'; // 仲裁员驳回仲裁,退回协商(非胜负裁决)
|
|
10
12
|
export function registerDisputesWriteRoutes(app, deps) {
|
|
11
|
-
const { db, auth, generateId, detectFraud, errorRes, isEligibleArbitrator, requireHumanPresence, getDisputeDetails, respondToDispute, arbitrateDispute, addPartyEvidence, requestEvidence, markEvidenceExpiry, uploadEvidence, EVIDENCE_MAX_BYTES, EVIDENCE_ALLOWED_MIME, appendOrderEvent, FUND_BASE_RATE, settleCommission, depositToFund, calculatePv, recordDisputeReputation, issueAgentStrike, publishDisputeCase, logAdminAction, snfSend, getProtocolParam } = deps;
|
|
13
|
+
const { db, auth, generateId, detectFraud, errorRes, isEligibleArbitrator, requireHumanPresence, getDisputeDetails, respondToDispute, arbitrateDispute, addPartyEvidence, requestEvidence, markEvidenceExpiry, uploadEvidence, EVIDENCE_MAX_BYTES, EVIDENCE_ALLOWED_MIME, appendOrderEvent, FUND_BASE_RATE, settleCommission, depositToFund, calculatePv, recordDisputeReputation, issueAgentStrike, publishDisputeCase, logAdminAction, snfSend, getProtocolParam, notifyTransition } = deps;
|
|
14
|
+
// PR-E:案件级仲裁操作(裁定 / 补证)统一的【原子领取或校验指派】。assigned 为空 → CAS 首位领取;
|
|
15
|
+
// 非空且不含 caller → NOT_ASSIGNED_ARBITRATOR。arbitrate 与 request-evidence 共用,防"任意 active 仲裁员操作任意案"。
|
|
16
|
+
const claimOrCheckAssignment = (disputeId, userId) => {
|
|
17
|
+
const row = db.prepare('SELECT assigned_arbitrators FROM disputes WHERE id = ?').get(disputeId);
|
|
18
|
+
let assigned = [];
|
|
19
|
+
try {
|
|
20
|
+
assigned = JSON.parse(row?.assigned_arbitrators || '[]');
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
if (assigned.length === 0) {
|
|
24
|
+
const claim = db.prepare(`UPDATE disputes SET assigned_arbitrators = ? WHERE id = ? AND (assigned_arbitrators IS NULL OR assigned_arbitrators = '[]')`).run(JSON.stringify([userId]), disputeId);
|
|
25
|
+
if (claim.changes === 0) {
|
|
26
|
+
const fresh = db.prepare('SELECT assigned_arbitrators FROM disputes WHERE id = ?').get(disputeId);
|
|
27
|
+
try {
|
|
28
|
+
assigned = JSON.parse(fresh?.assigned_arbitrators || '[]');
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
else
|
|
33
|
+
assigned = [userId];
|
|
34
|
+
}
|
|
35
|
+
return assigned.includes(userId) ? { ok: true } : { ok: false, error_code: 'NOT_ASSIGNED_ARBITRATOR' };
|
|
36
|
+
};
|
|
12
37
|
// ── RFC-007 stage 5:客观拒单【临时判责】的仲裁翻案 ─────────────────────────────
|
|
13
38
|
// 卖家 contest_decline 后,订单 = fault_seller + decline_objective_pending=1 + decline_contested=1(未结算)。
|
|
14
39
|
// 仲裁员(真实人工 + WebAuthn)裁决:uphold → declined_nofault(免责全退+退质押);reject → 违约结算。
|
|
@@ -117,7 +142,7 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
117
142
|
const { ruling, reason, refund_amount, liable_party_id, liability_parties } = req.body;
|
|
118
143
|
if (!ruling || !reason)
|
|
119
144
|
return void res.json({ error: '请提供裁定结果(ruling)和理由(reason)' });
|
|
120
|
-
const validRulings = ['refund_buyer', 'release_seller', 'partial_refund', 'liability_split'];
|
|
145
|
+
const validRulings = ['refund_buyer', 'release_seller', 'partial_refund', 'liability_split', 'dismiss_to_negotiation'];
|
|
121
146
|
if (!validRulings.includes(ruling)) {
|
|
122
147
|
return void res.json({ error: `ruling 必须是 ${validRulings.join(' / ')} 之一` });
|
|
123
148
|
}
|
|
@@ -134,32 +159,52 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
134
159
|
const dispute = await getDisputeDetails(db, req.params.id);
|
|
135
160
|
if (!dispute)
|
|
136
161
|
return void res.status(404).json({ error: '争议不存在' });
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
162
|
+
// PR-B COI 硬门:当事方(买家/卖家/物流/发起人/被诉人)不得仲裁本案。在领取/裁定前拦截。
|
|
163
|
+
if (arbitratorHasConflict(db, dispute.order_id, dispute.initiator_id ?? null, dispute.defendant_id ?? null, user.id)) {
|
|
164
|
+
return void res.status(403).json({ error: '你是本案当事方,不可仲裁(利益冲突)', error_code: 'ARBITRATOR_CONFLICT_OF_INTEREST' });
|
|
165
|
+
}
|
|
166
|
+
// P0: 防"任意仲裁员裁决任意争议" —— 原子领取或校验指派(与 request-evidence 共用 helper)。
|
|
167
|
+
const claim = claimOrCheckAssignment(req.params.id, user.id);
|
|
168
|
+
if (!claim.ok) {
|
|
169
|
+
return void res.status(403).json({ error: '此争议未分配给你 — 仅指派的仲裁员可裁定', error_code: claim.error_code || 'NOT_ASSIGNED_ARBITRATOR' });
|
|
170
|
+
}
|
|
171
|
+
// 仲裁员【驳回仲裁,退回协商】—— 非胜负裁决:不结算/不退款/不收仲裁费/不扣信誉/不动 escrow。仅 direct_p2p 且由 payment_query 升级来的争议;
|
|
172
|
+
// 域函数硬校验 rail + payment_query-origin + 未裁定,否则 NOT_PAYMENT_QUERY_DISPUTE / ARBITRATION_DISMISS_NOT_ALLOWED。状态回退 + dispute 关闭同一原子事务。
|
|
173
|
+
if (ruling === 'dismiss_to_negotiation') {
|
|
174
|
+
let dm;
|
|
175
|
+
try {
|
|
176
|
+
dm = db.transaction(() => dismissDisputeToNegotiation(db, req.params.id, reason))();
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
return void errorRes(res, 500, 'DISMISS_FAILED', e.message);
|
|
154
180
|
}
|
|
155
|
-
|
|
156
|
-
|
|
181
|
+
if (!dm.success) {
|
|
182
|
+
const st = dm.error_code === 'DISPUTE_NOT_FOUND' ? 404 : (dm.error_code === 'SYS_MISSING' || dm.error_code === 'TRANSITION_FAILED') ? 500 : 409;
|
|
183
|
+
return void errorRes(res, st, dm.error_code || 'DISMISS_FAILED', dm.error || '驳回仲裁失败');
|
|
157
184
|
}
|
|
185
|
+
try {
|
|
186
|
+
appendOrderEvent(db, { orderId: dm.order_id, eventType: 'transition', fromStatus: 'disputed', toStatus: 'payment_query', actorId: user.id, actorRole: 'arbitrator', extra: { action: 'dismiss_to_negotiation', dispute_id: req.params.id, reason, non_custodial: true } });
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
console.warn('[order-chain] dismiss event:', e.message);
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
markEvidenceExpiry(db, req.params.id);
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
console.warn('[evidence] mark expiry:', e.message);
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
notifyTransition(db, dm.order_id, 'disputed', 'payment_query');
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
console.warn('[notify] dismiss:', e.message);
|
|
202
|
+
}
|
|
203
|
+
return void res.json({ success: true, status: 'payment_query', dispute_dismissed: true, returned_to_negotiation: true, message: '已驳回仲裁,退回买卖双方协商' });
|
|
158
204
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// 协议层:仲裁员签名的 ruling 入订单链
|
|
205
|
+
// 协议层:仲裁员签名的 ruling 入订单链。direct_p2p 非托管=仅信誉裁决,绝不把请求里的退款/赔付金额写进【签名链/timeline】
|
|
206
|
+
// (即便资金层不动钱,签名链会永久留假语义)→ 强制 non_custodial + 金额/责任方全清。
|
|
207
|
+
const ncRail = dispute.payment_rail === 'direct_p2p';
|
|
163
208
|
try {
|
|
164
209
|
appendOrderEvent(db, {
|
|
165
210
|
orderId: dispute.order_id,
|
|
@@ -172,9 +217,10 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
172
217
|
action: 'arbitration_ruling',
|
|
173
218
|
dispute_id: req.params.id,
|
|
174
219
|
ruling, reason,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
220
|
+
non_custodial: ncRail || undefined,
|
|
221
|
+
refund_amount: ncRail ? null : (refund_amount ? Number(refund_amount) : null),
|
|
222
|
+
liable_party_id: ncRail ? null : (liable_party_id || null),
|
|
223
|
+
liability_parties: ncRail ? null : (liability_parties || null),
|
|
178
224
|
},
|
|
179
225
|
});
|
|
180
226
|
}
|
|
@@ -191,8 +237,10 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
191
237
|
catch (e) {
|
|
192
238
|
console.warn('[evidence] mark expiry:', e.message);
|
|
193
239
|
}
|
|
240
|
+
// 非托管(直付)争议:协议不持货款、不做托管结算 → 【跳过】所有佣金/PV/基金分润钩子(否则会从卖家余额扣不存在的佣金)。
|
|
241
|
+
const nonCustodial = !!result.non_custodial;
|
|
194
242
|
// release_seller 等同正常完成 → 触发推土机分润 + 原子能
|
|
195
|
-
if (ruling === 'release_seller') {
|
|
243
|
+
if (ruling === 'release_seller' && !nonCustodial) {
|
|
196
244
|
try {
|
|
197
245
|
db.transaction(() => {
|
|
198
246
|
const order = db.prepare("SELECT * FROM orders WHERE id = ?").get(dispute.order_id);
|
|
@@ -233,8 +281,8 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
233
281
|
console.error('[dispute commission/pv hook]', e);
|
|
234
282
|
}
|
|
235
283
|
}
|
|
236
|
-
// Bug-A fix:partial_refund / liability_split 也按 effectiveBase 发 commission/PV/基金池
|
|
237
|
-
if (ruling === 'partial_refund' || ruling === 'liability_split') {
|
|
284
|
+
// Bug-A fix:partial_refund / liability_split 也按 effectiveBase 发 commission/PV/基金池(非托管跳过)
|
|
285
|
+
if ((ruling === 'partial_refund' || ruling === 'liability_split') && !nonCustodial) {
|
|
238
286
|
try {
|
|
239
287
|
db.transaction(() => {
|
|
240
288
|
const order = db.prepare("SELECT * FROM orders WHERE id = ?").get(dispute.order_id);
|
|
@@ -519,13 +567,23 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
519
567
|
}
|
|
520
568
|
});
|
|
521
569
|
// 仲裁员:请求某方补证
|
|
522
|
-
app.post('/api/disputes/:id/request-evidence', (req, res) => {
|
|
570
|
+
app.post('/api/disputes/:id/request-evidence', async (req, res) => {
|
|
523
571
|
const user = auth(req, res);
|
|
524
572
|
if (!user)
|
|
525
573
|
return;
|
|
574
|
+
// PR-E:① active whitelist ② COI(当事方不可补证本案,与 arbitrate 同门) ③ 已分配/领取该案(claim-if-empty)④ 涉案方目标(engine)。
|
|
526
575
|
const elig = isEligibleArbitrator(user.id);
|
|
527
576
|
if (!elig.ok)
|
|
528
577
|
return void errorRes(res, 403, 'NOT_ARBITRATOR', elig.reason || '仅限仲裁员');
|
|
578
|
+
const disp = await getDisputeDetails(db, req.params.id);
|
|
579
|
+
if (!disp)
|
|
580
|
+
return void res.status(404).json({ error: '争议不存在' });
|
|
581
|
+
if (arbitratorHasConflict(db, disp.order_id, disp.initiator_id ?? null, disp.defendant_id ?? null, user.id)) {
|
|
582
|
+
return void res.status(403).json({ error: '你是本案当事方,不可补证(利益冲突)', error_code: 'ARBITRATOR_CONFLICT_OF_INTEREST' });
|
|
583
|
+
}
|
|
584
|
+
const claim = claimOrCheckAssignment(req.params.id, user.id);
|
|
585
|
+
if (!claim.ok)
|
|
586
|
+
return void errorRes(res, 403, claim.error_code || 'NOT_ASSIGNED_ARBITRATOR', '此争议未分配给你 — 仅指派的仲裁员可补证');
|
|
529
587
|
const { requested_from_id, evidence_types, description, deadline_hours = 48 } = req.body;
|
|
530
588
|
if (!requested_from_id || !description)
|
|
531
589
|
return void res.json({ error: '请指定被要求方和证据要求说明' });
|
|
@@ -538,7 +596,7 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
538
596
|
}
|
|
539
597
|
const result = requestEvidence(db, req.params.id, user.id, requested_from_id, evidence_types, description, Number(deadline_hours));
|
|
540
598
|
if (!result.success)
|
|
541
|
-
return void res.json({ error: result.error });
|
|
599
|
+
return void res.json({ error: result.error, error_code: result.error_code });
|
|
542
600
|
res.json({ success: true, request_id: result.requestId });
|
|
543
601
|
});
|
|
544
602
|
// ─── task #1093 stage 6: arbitrator_pause / resume auto_judge ─────
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
2
|
import { createAnchor, verifyAnchorSignature, revokeAnchor, issueOwnershipToken, submitVerification, getAnchor, listAnchorsByProduct, listAnchorsBySeller, distributeAnchorRewards, ANCHOR_VERIFICATION_FEE_RECOMMENDED, } from '../../layer1-agent/L1-2-external-anchor/anchor-engine.js';
|
|
3
|
+
import { isEligibleArbitrator } from '../arbitrator-lifecycle.js'; // 仲裁能力唯一源=active 白名单(distribute 是动钱动作,不认 legacy role)
|
|
3
4
|
export function registerExternalAnchorsRoutes(app, deps) {
|
|
4
5
|
const { db, auth } = deps;
|
|
5
6
|
app.post('/api/external-anchors', (req, res) => {
|
|
@@ -42,12 +43,13 @@ export function registerExternalAnchorsRoutes(app, deps) {
|
|
|
42
43
|
total_paid_out: verifications.reduce((s, v) => s + Number(v.reward_amount || 0), 0),
|
|
43
44
|
});
|
|
44
45
|
});
|
|
45
|
-
// 手动 distribute(admin
|
|
46
|
+
// 手动 distribute(admin/白名单仲裁员 补救:anchor 已 community 但 fee_paid_out=0)——动钱动作,仲裁员认 active
|
|
47
|
+
// 白名单(isEligibleArbitrator),不认 legacy user.role(否则已 suspend/revoke 但 role 未同步的账号仍可触发放款)。
|
|
46
48
|
app.post('/api/external-anchors/:id/distribute-rewards', (req, res) => {
|
|
47
49
|
const user = auth(req, res);
|
|
48
50
|
if (!user)
|
|
49
51
|
return;
|
|
50
|
-
if (user.role !== 'admin' && user.
|
|
52
|
+
if (user.role !== 'admin' && !isEligibleArbitrator(db, user.id).ok)
|
|
51
53
|
return void res.status(403).json({ error: '仅管理员/仲裁员可手动分发' });
|
|
52
54
|
const paid = distributeAnchorRewards(db, req.params.id);
|
|
53
55
|
res.json({ ok: true, paid });
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { listActivePlatformAccounts } from '../../platform-receive-accounts.js';
|
|
2
|
+
import { createFeePrepayRequest, listSellerRequests, cancelRequest } from '../../direct-pay-fee-prepay-request.js';
|
|
3
|
+
// N3:申请提交 → 通知有审批权的 admin(approve/reject 是 ROOT 门,只通知 root;regional 收到也办不了)。
|
|
4
|
+
import { createNotification } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
5
|
+
import { dbAll } from '../../layer0-foundation/L0-1-database/db.js';
|
|
6
|
+
export function registerFeePrepayRequestRoutes(app, deps) {
|
|
7
|
+
const { db, auth, generateId } = deps;
|
|
8
|
+
/** 登录 + seller 角色。返回 user 或 null(已写响应)。 */
|
|
9
|
+
function requireSeller(req, res) {
|
|
10
|
+
const user = auth(req, res);
|
|
11
|
+
if (!user)
|
|
12
|
+
return null;
|
|
13
|
+
if (user.role !== 'seller') {
|
|
14
|
+
res.status(403).json({ error: '仅卖家可申请平台服务费预充值', error_code: 'SELLER_ONLY' });
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return user;
|
|
18
|
+
}
|
|
19
|
+
// ── 卖家看平台收款方式(active;含 instruction + qr_data_uri —— 平台公开收款明细,据此付款)──
|
|
20
|
+
app.get('/api/direct-receive/platform-receive-accounts', (req, res) => {
|
|
21
|
+
const user = requireSeller(req, res);
|
|
22
|
+
if (!user)
|
|
23
|
+
return;
|
|
24
|
+
res.json({ accounts: listActivePlatformAccounts(db) });
|
|
25
|
+
});
|
|
26
|
+
// ── 提交预充值申请(不 Passkey;凭据必填)──
|
|
27
|
+
app.post('/api/direct-receive/fee-prepay-request', async (req, res) => {
|
|
28
|
+
const user = requireSeller(req, res);
|
|
29
|
+
if (!user)
|
|
30
|
+
return;
|
|
31
|
+
const b = req.body || {};
|
|
32
|
+
const r = createFeePrepayRequest(db, user.id, {
|
|
33
|
+
amountUnits: Number(b.amount_units), currency: b.currency, platformAccountId: b.platform_account_id,
|
|
34
|
+
evidenceRef: b.evidence_ref, evidenceNote: b.evidence_note,
|
|
35
|
+
}, generateId);
|
|
36
|
+
if (!r.ok)
|
|
37
|
+
return void res.status(400).json({ error: r.reason, error_code: 'FEE_PREPAY_REQUEST_INVALID' });
|
|
38
|
+
// N3:通知 root admin 有待审申请(此前提交后无人收到任何未读信号,全靠人肉翻队列)。best-effort 不阻断。
|
|
39
|
+
try {
|
|
40
|
+
const roots = await dbAll("SELECT id FROM users WHERE role = 'admin' AND (admin_type = 'root' OR admin_type IS NULL)");
|
|
41
|
+
const amountUsdc = Number(b.amount_units) / 1_000_000;
|
|
42
|
+
for (const a of roots) {
|
|
43
|
+
createNotification(db, a.id, null, 'dp_fee_prepay_requested', '💳 新预充值申请待审', `卖家 ${String(user.name ?? user.id)} 申请平台服务费预充值 ${amountUsdc} USDC,请到 admin 后台核对到账后处理。`, { templateKey: 'dp_fee_prepay_requested', params: { seller: String(user.name ?? user.id), amount: amountUsdc } });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
console.warn('[fee-prepay-request notify]', e.message);
|
|
48
|
+
}
|
|
49
|
+
res.json({ ok: true, request: r.request });
|
|
50
|
+
});
|
|
51
|
+
// ── 卖家看自己的申请(全状态)──
|
|
52
|
+
app.get('/api/direct-receive/fee-prepay-requests', (req, res) => {
|
|
53
|
+
const user = requireSeller(req, res);
|
|
54
|
+
if (!user)
|
|
55
|
+
return;
|
|
56
|
+
res.json({ requests: listSellerRequests(db, user.id) });
|
|
57
|
+
});
|
|
58
|
+
// ── 卖家撤销自己的 pending 申请 ──
|
|
59
|
+
app.post('/api/direct-receive/fee-prepay-request/:id/cancel', (req, res) => {
|
|
60
|
+
const user = requireSeller(req, res);
|
|
61
|
+
if (!user)
|
|
62
|
+
return;
|
|
63
|
+
const changed = cancelRequest(db, String(req.params.id), user.id);
|
|
64
|
+
res.json({ ok: true, changed });
|
|
65
|
+
});
|
|
66
|
+
}
|