@seasonkoh/webaz 0.1.6 → 0.1.7
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.
|
@@ -227,7 +227,7 @@ action 说明:
|
|
|
227
227
|
ruling 裁定选项(arbitrate 时使用):
|
|
228
228
|
- refund_buyer:全额退款给买家,扣押卖家部分保证金
|
|
229
229
|
- release_seller:资金释放给卖家(卖家胜诉)
|
|
230
|
-
- partial_refund:部分退款(需指定 refund_amount
|
|
230
|
+
- partial_refund:部分退款(需指定 refund_amount);若责任在第三方(如物流),同时指定 liable_party(责任方 user_id),此时卖家全额结算,赔偿金从责任方扣除`,
|
|
231
231
|
inputSchema: {
|
|
232
232
|
type: 'object',
|
|
233
233
|
properties: {
|
|
@@ -247,6 +247,7 @@ ruling 裁定选项(arbitrate 时使用):
|
|
|
247
247
|
description: '裁定结果(arbitrate 时必填)',
|
|
248
248
|
},
|
|
249
249
|
refund_amount: { type: 'number', description: '部分退款金额,仅 ruling=partial_refund 时使用' },
|
|
250
|
+
liable_party: { type: 'string', description: '第三方责任方 user_id(partial_refund 时可选):指定后赔偿金从该方扣除,卖家全额结算' },
|
|
250
251
|
ruling_reason: { type: 'string', description: '裁定理由(arbitrate 时必填,将永久记录在链上)' },
|
|
251
252
|
},
|
|
252
253
|
required: ['api_key', 'action'],
|
|
@@ -866,7 +867,7 @@ function handleDispute(args) {
|
|
|
866
867
|
if (args.ruling === 'partial_refund' && !args.refund_amount) {
|
|
867
868
|
return { error: 'partial_refund 需要提供 refund_amount' };
|
|
868
869
|
}
|
|
869
|
-
const result = arbitrateDispute(db, args.dispute_id, user.id, args.ruling, args.ruling_reason, args.refund_amount);
|
|
870
|
+
const result = arbitrateDispute(db, args.dispute_id, user.id, args.ruling, args.ruling_reason, args.refund_amount, undefined, args.liable_party);
|
|
870
871
|
// L4-3 争议声誉:裁定完成后更新声誉
|
|
871
872
|
if (result.success) {
|
|
872
873
|
const dispute = getDisputeDetails(db, args.dispute_id);
|
|
@@ -143,7 +143,8 @@ export function respondToDispute(db, disputeId, responderId, notes, evidenceIds)
|
|
|
143
143
|
message: '反驳证据已提交,争议进入仲裁阶段。仲裁员将在 72 小时内做出裁定。',
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
|
-
export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, refundAmount, liabilityParties
|
|
146
|
+
export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, refundAmount, liabilityParties, liablePartyId // 指定责任方 user_id(用于 partial_refund 第三方责任场景)
|
|
147
|
+
) {
|
|
147
148
|
const dispute = db.prepare('SELECT * FROM disputes WHERE id = ?').get(disputeId);
|
|
148
149
|
if (!dispute)
|
|
149
150
|
return { success: false, error: `争议不存在:${disputeId}` };
|
|
@@ -159,10 +160,10 @@ export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, re
|
|
|
159
160
|
// 执行资金处置
|
|
160
161
|
const settlement = ruling === 'liability_split' && liabilityParties
|
|
161
162
|
? executeLiabilitySplit(db, dispute.order_id, liabilityParties, refundAmount)
|
|
162
|
-
: executeSettlement(db, dispute.order_id, ruling, refundAmount);
|
|
163
|
+
: executeSettlement(db, dispute.order_id, ruling, refundAmount, liablePartyId);
|
|
163
164
|
if (!settlement.success)
|
|
164
165
|
return { success: false, error: settlement.error };
|
|
165
|
-
//
|
|
166
|
+
// 收取仲裁费(败诉/责任方付 1%,最低 1 WAZ)
|
|
166
167
|
const order = db.prepare('SELECT total_amount, buyer_id, seller_id FROM orders WHERE id = ?')
|
|
167
168
|
.get(dispute.order_id);
|
|
168
169
|
const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
|
|
@@ -170,29 +171,35 @@ export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, re
|
|
|
170
171
|
if (order) {
|
|
171
172
|
const amt = order.total_amount;
|
|
172
173
|
if (ruling === 'refund_buyer') {
|
|
173
|
-
// 卖家败诉
|
|
174
174
|
const f = chargeArbitrationFee(db, order.seller_id, amt, arbitratorId, sysUser.id);
|
|
175
175
|
if (f.fee > 0)
|
|
176
176
|
arbFees[order.seller_id] = f.fee;
|
|
177
177
|
}
|
|
178
178
|
else if (ruling === 'release_seller') {
|
|
179
|
-
// 买家败诉
|
|
180
179
|
const f = chargeArbitrationFee(db, order.buyer_id, amt, arbitratorId, sysUser.id);
|
|
181
180
|
if (f.fee > 0)
|
|
182
181
|
arbFees[order.buyer_id] = f.fee;
|
|
183
182
|
}
|
|
184
183
|
else if (ruling === 'partial_refund') {
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
184
|
+
// 有指定责任方:仲裁费全由责任方承担
|
|
185
|
+
// 无责任方:买卖双方各付 0.5%
|
|
186
|
+
const payerId = liablePartyId ?? null;
|
|
187
|
+
if (payerId) {
|
|
188
|
+
const f = chargeArbitrationFee(db, payerId, amt, arbitratorId, sysUser.id);
|
|
189
|
+
if (f.fee > 0)
|
|
190
|
+
arbFees[payerId] = f.fee;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const halfAmt = amt * 0.5;
|
|
194
|
+
const fb = chargeArbitrationFee(db, order.buyer_id, halfAmt, arbitratorId, sysUser.id);
|
|
195
|
+
const fs = chargeArbitrationFee(db, order.seller_id, halfAmt, arbitratorId, sysUser.id);
|
|
196
|
+
if (fb.fee > 0)
|
|
197
|
+
arbFees[order.buyer_id] = fb.fee;
|
|
198
|
+
if (fs.fee > 0)
|
|
199
|
+
arbFees[order.seller_id] = fs.fee;
|
|
200
|
+
}
|
|
193
201
|
}
|
|
194
202
|
else if (ruling === 'liability_split' && liabilityParties) {
|
|
195
|
-
// 各责任方按比例分担
|
|
196
203
|
const totalLiability = liabilityParties.reduce((s, p) => s + p.amount, 0) || amt;
|
|
197
204
|
for (const p of liabilityParties) {
|
|
198
205
|
const share = (p.amount / totalLiability) * amt;
|
|
@@ -378,7 +385,7 @@ function chargeArbitrationFee(db, loserId, orderAmount, arbitratorId, sysUserId)
|
|
|
378
385
|
}
|
|
379
386
|
return { fee: actualFee, arbitratorShare, protocolShare };
|
|
380
387
|
}
|
|
381
|
-
function executeSettlement(db, orderId, ruling, refundAmount) {
|
|
388
|
+
function executeSettlement(db, orderId, ruling, refundAmount, liablePartyId) {
|
|
382
389
|
const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(orderId);
|
|
383
390
|
if (!order)
|
|
384
391
|
return { success: false, error: '订单不存在' };
|
|
@@ -447,35 +454,88 @@ function executeSettlement(db, orderId, ruling, refundAmount) {
|
|
|
447
454
|
};
|
|
448
455
|
}
|
|
449
456
|
else if (ruling === 'partial_refund') {
|
|
450
|
-
// ── 折中处理:部分退款 ────────────────────────────────────────
|
|
451
457
|
const refund = refundAmount ?? Math.round(totalAmount * 0.5 * 100) / 100;
|
|
452
458
|
if (refund > totalAmount)
|
|
453
459
|
return { success: false, error: `退款金额 ${refund} 超出订单总额 ${totalAmount}` };
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
460
|
+
if (liablePartyId) {
|
|
461
|
+
// ── 第三方责任 partial_refund ────────────────────────────────
|
|
462
|
+
// 卖家全额结算(正常收款),买家赔偿由责任方钱包直接支付
|
|
463
|
+
const protocolFee = Math.round(totalAmount * 0.02 * 100) / 100;
|
|
464
|
+
const logisticsFee = order.logistics_id ? Math.round(totalAmount * 0.05 * 100) / 100 : 0;
|
|
465
|
+
const sellerAmount = totalAmount - protocolFee - logisticsFee;
|
|
466
|
+
// 检查责任方余额是否足够
|
|
467
|
+
const liableWallet = db.prepare('SELECT balance, staked FROM wallets WHERE user_id = ?')
|
|
468
|
+
.get(liablePartyId);
|
|
469
|
+
const liableAvailable = (liableWallet?.balance ?? 0) + (liableWallet?.staked ?? 0);
|
|
470
|
+
const actualRefund = Math.min(refund, liableAvailable);
|
|
471
|
+
db.transaction(() => {
|
|
472
|
+
// 1. 释放托管 → 正常结算给卖家
|
|
473
|
+
db.prepare('UPDATE wallets SET escrowed = escrowed - ? WHERE user_id = ?').run(totalAmount, buyerId);
|
|
474
|
+
db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(sellerAmount, sellerId);
|
|
475
|
+
if (order.logistics_id && logisticsFee > 0) {
|
|
476
|
+
db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(logisticsFee, order.logistics_id);
|
|
477
|
+
}
|
|
478
|
+
// 2. 返还卖家质押
|
|
479
|
+
if (stakeAmount > 0) {
|
|
480
|
+
db.prepare('UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?')
|
|
481
|
+
.run(stakeAmount, stakeAmount, sellerId);
|
|
482
|
+
}
|
|
483
|
+
// 3. 从责任方钱包扣除赔偿(先质押后余额)
|
|
484
|
+
if (actualRefund > 0) {
|
|
485
|
+
const liableStaked = liableWallet?.staked ?? 0;
|
|
486
|
+
if (liableStaked >= actualRefund) {
|
|
487
|
+
db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(actualRefund, liablePartyId);
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
const fromBalance = actualRefund - liableStaked;
|
|
491
|
+
db.prepare('UPDATE wallets SET staked = 0, balance = balance - ? WHERE user_id = ?').run(fromBalance, liablePartyId);
|
|
492
|
+
}
|
|
493
|
+
// 4. 赔偿金给买家
|
|
494
|
+
db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(actualRefund, buyerId);
|
|
495
|
+
}
|
|
496
|
+
transition(db, orderId, 'completed', sysUser.id, [], `争议裁定:第三方责任赔偿 ${actualRefund} WAZ,卖家全额结算`);
|
|
497
|
+
})();
|
|
498
|
+
return {
|
|
499
|
+
success: true,
|
|
500
|
+
detail: {
|
|
501
|
+
ruling: 'partial_refund',
|
|
502
|
+
liable_party: liablePartyId,
|
|
503
|
+
buyer_compensation: actualRefund,
|
|
504
|
+
seller_received: sellerAmount,
|
|
505
|
+
logistics_fee: logisticsFee,
|
|
506
|
+
protocol_fee: protocolFee,
|
|
507
|
+
seller_stake_returned: stakeAmount,
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
// ── 买卖双方协商 partial_refund(原逻辑)───────────────────────
|
|
513
|
+
const sellerGet = Math.round((totalAmount - refund) * 100) / 100;
|
|
514
|
+
const stakeReturn = Math.round(stakeAmount * 0.5 * 100) / 100; // 质押返一半
|
|
515
|
+
db.transaction(() => {
|
|
516
|
+
db.prepare('UPDATE wallets SET escrowed = escrowed - ? WHERE user_id = ?').run(totalAmount, buyerId);
|
|
517
|
+
if (refund > 0) {
|
|
518
|
+
db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(refund, buyerId);
|
|
519
|
+
}
|
|
520
|
+
if (sellerGet > 0) {
|
|
521
|
+
db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(sellerGet, sellerId);
|
|
522
|
+
}
|
|
523
|
+
if (stakeAmount > 0) {
|
|
524
|
+
db.prepare('UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?')
|
|
525
|
+
.run(stakeAmount, stakeReturn, sellerId);
|
|
526
|
+
}
|
|
527
|
+
transition(db, orderId, 'cancelled', sysUser.id, [], `争议裁定:部分退款 ${refund} WAZ`);
|
|
528
|
+
})();
|
|
529
|
+
return {
|
|
530
|
+
success: true,
|
|
531
|
+
detail: {
|
|
532
|
+
ruling: 'partial_refund',
|
|
533
|
+
buyer_refund: refund,
|
|
534
|
+
seller_received: sellerGet,
|
|
535
|
+
seller_stake_returned: stakeReturn,
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
479
539
|
}
|
|
480
540
|
return { success: false, error: `未知裁定类型:${ruling}` };
|
|
481
541
|
}
|
package/dist/pwa/server.js
CHANGED
|
@@ -654,7 +654,7 @@ app.post('/api/disputes/:id/arbitrate', (req, res) => {
|
|
|
654
654
|
return;
|
|
655
655
|
if (user.role !== 'arbitrator')
|
|
656
656
|
return void res.status(403).json({ error: '仅限仲裁员' });
|
|
657
|
-
const { ruling, reason, refund_amount, liability_parties } = req.body;
|
|
657
|
+
const { ruling, reason, refund_amount, liable_party_id, liability_parties } = req.body;
|
|
658
658
|
if (!ruling || !reason)
|
|
659
659
|
return void res.json({ error: '请提供裁定结果(ruling)和理由(reason)' });
|
|
660
660
|
const validRulings = ['refund_buyer', 'release_seller', 'partial_refund', 'liability_split'];
|
|
@@ -674,7 +674,7 @@ app.post('/api/disputes/:id/arbitrate', (req, res) => {
|
|
|
674
674
|
const dispute = getDisputeDetails(db, req.params.id);
|
|
675
675
|
if (!dispute)
|
|
676
676
|
return void res.status(404).json({ error: '争议不存在' });
|
|
677
|
-
const result = arbitrateDispute(db, req.params.id, user.id, ruling, reason, refund_amount ? Number(refund_amount) : undefined, liability_parties);
|
|
677
|
+
const result = arbitrateDispute(db, req.params.id, user.id, ruling, reason, refund_amount ? Number(refund_amount) : undefined, liability_parties, liable_party_id);
|
|
678
678
|
if (!result.success)
|
|
679
679
|
return void res.json({ error: result.error });
|
|
680
680
|
// 争议声誉更新(责任分配时以主要责任方为败诉方)
|
package/package.json
CHANGED