@seasonkoh/webaz 0.1.20 → 0.1.22

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.
@@ -14,6 +14,9 @@
14
14
  */
15
15
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
16
16
  import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
17
+ // RFC-014 PR5 — 争议资金处置走整数 base-units + 绝对值落库 + allocate 精确拆分。
18
+ import { toUnits, toDecimal, mulRate, allocate } from '../../money.js';
19
+ import { applyWalletDelta, debitStakeThenBalance } from '../../ledger.js';
17
20
  // ─── Schema 初始化(幂等,安全重复调用)────────────────────────
18
21
  /**
19
22
  * 为 disputes 表添加 L3 需要的新列
@@ -257,7 +260,7 @@ export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, re
257
260
  *
258
261
  * 这样确保托管资金守恒(无凭空创造/销毁),责任方额外受罚(去向:sys_protocol)。
259
262
  */
260
- function executeLiabilitySplit(db, orderId, liabilityParties, buyerRefund) {
263
+ export function executeLiabilitySplit(db, orderId, liabilityParties, buyerRefund) {
261
264
  const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(orderId);
262
265
  if (!order)
263
266
  return { success: false, error: '订单不存在' };
@@ -268,91 +271,78 @@ function executeLiabilitySplit(db, orderId, liabilityParties, buyerRefund) {
268
271
  const product = db.prepare('SELECT stake_amount FROM products WHERE id = ?')
269
272
  .get(order.product_id);
270
273
  const stakeAmount = product?.stake_amount ?? 0;
271
- const actualRefund = Math.min(buyerRefund ?? totalAmount, totalAmount);
272
- const sellerEscrowShare = Math.round((totalAmount - actualRefund) * 100) / 100;
273
- // 预先计算各责任方实际扣款(资金守恒:责任方扣款 = 协议金库收入)
274
+ // RFC-014:整数 base-units
275
+ const totalU = toUnits(totalAmount);
276
+ const stakeAmountU = toUnits(stakeAmount);
277
+ const actualRefundU = Math.min(toUnits(buyerRefund ?? totalAmount), totalU);
278
+ const sellerEscrowShareU = totalU - actualRefundU; // 残值,精确
279
+ // 预先计算各责任方实际扣款(资金守恒:责任方扣款 = 协议金库收入),全整数 units
274
280
  const settled = [];
275
281
  for (const entry of liabilityParties) {
276
- const wallet = db.prepare('SELECT balance, staked FROM wallets WHERE user_id = ?')
282
+ const wallet = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked FROM wallets WHERE user_id = ?')
277
283
  .get(entry.user_id);
278
- const available = (wallet?.balance ?? 0) + (wallet?.staked ?? 0);
279
- let actualPenalty;
280
- let insuranceCovered = 0;
281
- if (entry.insurance_cap !== undefined && entry.insurance_cap < entry.amount) {
284
+ const availableU = toUnits(wallet?.balance ?? 0) + toUnits(wallet?.staked ?? 0);
285
+ const amountU = toUnits(entry.amount);
286
+ let actualPenaltyU;
287
+ let insuranceCoveredU = 0;
288
+ if (entry.insurance_cap !== undefined && toUnits(entry.insurance_cap) < amountU) {
282
289
  // 有保险上限:责任方最多赔 insurance_cap,不足部分由协议垫付
283
- actualPenalty = Math.min(entry.insurance_cap, available);
284
- insuranceCovered = entry.amount - entry.insurance_cap; // 协议垫付
290
+ const capU = toUnits(entry.insurance_cap);
291
+ actualPenaltyU = Math.min(capU, availableU);
292
+ insuranceCoveredU = amountU - capU; // 协议垫付(信息性,不实际入账)
285
293
  }
286
294
  else {
287
295
  // 无保险上限:以实际可用余额为上限
288
- actualPenalty = Math.min(entry.amount, available);
289
- insuranceCovered = entry.amount - actualPenalty; // 余额不足部分
296
+ actualPenaltyU = Math.min(amountU, availableU);
297
+ insuranceCoveredU = amountU - actualPenaltyU; // 余额不足部分
290
298
  }
291
- settled.push({ userId: entry.user_id, role: entry.role, owed: entry.amount, actualPenalty, insuranceCovered });
299
+ settled.push({ userId: entry.user_id, role: entry.role, owedU: amountU, actualPenaltyU, insuranceCoveredU });
292
300
  }
293
301
  // 卖家是否在责任方列表中
294
302
  const sellerLiability = liabilityParties.find(p => p.user_id === sellerId);
295
303
  db.transaction(() => {
296
304
  // ── A. 托管拨付 ──────────────────────────────────────────────
297
- // 释放买家托管,退还 actualRefund 给买家,剩余给卖家
298
- db.prepare('UPDATE wallets SET escrowed = escrowed - ? WHERE user_id = ?').run(totalAmount, buyerId);
299
- if (actualRefund > 0) {
300
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(actualRefund, buyerId);
301
- }
302
- if (sellerEscrowShare > 0) {
303
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(sellerEscrowShare, sellerId);
304
- }
305
- // ── B. 责任罚款 → 协议金库 ────────────────────────────────────
306
- let totalToTreasury = 0;
305
+ applyWalletDelta(db, buyerId, { escrowed: -totalU });
306
+ if (actualRefundU > 0)
307
+ applyWalletDelta(db, buyerId, { balance: actualRefundU });
308
+ if (sellerEscrowShareU > 0)
309
+ applyWalletDelta(db, sellerId, { balance: sellerEscrowShareU });
310
+ // ── B. 责任罚款 → 协议金库(先质押后余额)─────────────────────
311
+ let totalToTreasuryU = 0;
307
312
  for (const s of settled) {
308
- if (s.actualPenalty > 0) {
309
- const w = db.prepare('SELECT balance, staked FROM wallets WHERE user_id = ?')
310
- .get(s.userId);
311
- if (w.staked >= s.actualPenalty) {
312
- db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(s.actualPenalty, s.userId);
313
- }
314
- else {
315
- const fromStake = w.staked;
316
- const fromBalance = s.actualPenalty - fromStake;
317
- db.prepare('UPDATE wallets SET staked = 0, balance = balance - ? WHERE user_id = ?').run(fromBalance, s.userId);
318
- }
319
- totalToTreasury += s.actualPenalty;
320
- }
321
- }
322
- if (totalToTreasury > 0) {
323
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(totalToTreasury, sysUser.id);
313
+ if (s.actualPenaltyU > 0)
314
+ totalToTreasuryU += debitStakeThenBalance(db, s.userId, s.actualPenaltyU);
324
315
  }
316
+ if (totalToTreasuryU > 0)
317
+ applyWalletDelta(db, sysUser.id, { balance: totalToTreasuryU });
325
318
  // ── C. 卖家商品质押处理 ───────────────────────────────────────
326
- if (stakeAmount > 0) {
319
+ if (stakeAmountU > 0) {
327
320
  if (sellerLiability) {
328
- // 卖家有责:按责任金额比例扣罚质押,剩余返还
329
- const stakeForfeited = Math.min(stakeAmount, sellerLiability.amount);
330
- const stakeReturn = stakeAmount - stakeForfeited;
331
- db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(stakeAmount, sellerId);
332
- if (stakeReturn > 0) {
333
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(stakeReturn, sellerId);
334
- }
335
- if (stakeForfeited > 0) {
336
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(stakeForfeited, sysUser.id);
337
- }
321
+ // 卖家有责:按责任金额扣罚质押(封顶),剩余返还
322
+ const stakeForfeitedU = Math.min(stakeAmountU, toUnits(sellerLiability.amount));
323
+ const stakeReturnU = stakeAmountU - stakeForfeitedU;
324
+ applyWalletDelta(db, sellerId, { staked: -stakeAmountU });
325
+ if (stakeReturnU > 0)
326
+ applyWalletDelta(db, sellerId, { balance: stakeReturnU });
327
+ if (stakeForfeitedU > 0)
328
+ applyWalletDelta(db, sysUser.id, { balance: stakeForfeitedU });
338
329
  }
339
330
  else {
340
331
  // 卖家无责:全额返还质押
341
- db.prepare('UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?')
342
- .run(stakeAmount, stakeAmount, sellerId);
332
+ applyWalletDelta(db, sellerId, { staked: -stakeAmountU, balance: stakeAmountU });
343
333
  }
344
334
  }
345
- transition(db, orderId, 'refunded_partial', sysUser.id, [], `争议裁定:责任分配,退款买家 ${actualRefund} WAZ`);
335
+ transition(db, orderId, 'refunded_partial', sysUser.id, [], `争议裁定:责任分配,退款买家 ${toDecimal(actualRefundU)} WAZ`);
346
336
  })();
347
337
  return {
348
338
  success: true,
349
339
  detail: {
350
340
  ruling: 'liability_split',
351
- buyer_refund: actualRefund,
352
- seller_escrow_share: sellerEscrowShare,
341
+ buyer_refund: toDecimal(actualRefundU),
342
+ seller_escrow_share: toDecimal(sellerEscrowShareU),
353
343
  liability_breakdown: settled.map(s => ({
354
344
  userId: s.userId, role: s.role,
355
- owed: s.owed, actualPenalty: s.actualPenalty, insuranceCovered: s.insuranceCovered
345
+ owed: toDecimal(s.owedU), actualPenalty: toDecimal(s.actualPenaltyU), insuranceCovered: toDecimal(s.insuranceCoveredU)
356
346
  })),
357
347
  }
358
348
  };
@@ -366,35 +356,27 @@ function executeLiabilitySplit(db, orderId, liabilityParties, buyerRefund) {
366
356
  * - 先扣质押,质押不足再扣余额
367
357
  */
368
358
  function chargeArbitrationFee(db, loserId, orderAmount, arbitratorId, sysUserId) {
369
- const fee = Math.max(1, Math.round(orderAmount * 0.01 * 100) / 100);
359
+ // RFC-014:整数 base-units。费 = max(1 WAZ, 订单额×1%)
360
+ const feeU = Math.max(toUnits(1), mulRate(toUnits(orderAmount), 0.01));
370
361
  const isHumanArbitrator = arbitratorId !== sysUserId;
371
- const wallet = db.prepare('SELECT balance, staked FROM wallets WHERE user_id = ?')
362
+ const wallet = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked FROM wallets WHERE user_id = ?')
372
363
  .get(loserId);
373
- const available = (wallet?.balance ?? 0) + (wallet?.staked ?? 0);
374
- const actualFee = Math.min(fee, available);
375
- if (actualFee <= 0)
364
+ const availableU = toUnits(wallet?.balance ?? 0) + toUnits(wallet?.staked ?? 0);
365
+ const actualFeeU = Math.min(feeU, availableU);
366
+ if (actualFeeU <= 0)
376
367
  return { fee: 0, arbitratorShare: 0, protocolShare: 0 };
377
368
  // 扣款:先质押后余额
378
- const staked = wallet?.staked ?? 0;
379
- if (staked >= actualFee) {
380
- db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(actualFee, loserId);
381
- }
382
- else {
383
- const fromBalance = actualFee - staked;
384
- db.prepare('UPDATE wallets SET staked = 0, balance = balance - ? WHERE user_id = ?').run(fromBalance, loserId);
385
- }
386
- // 分配:人工仲裁各一半,自动裁定全归协议
387
- const arbitratorShare = isHumanArbitrator ? Math.round(actualFee * 0.5 * 100) / 100 : 0;
388
- const protocolShare = actualFee - arbitratorShare;
389
- if (protocolShare > 0) {
390
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(protocolShare, sysUserId);
391
- }
392
- if (arbitratorShare > 0) {
393
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(arbitratorShare, arbitratorId);
394
- }
395
- return { fee: actualFee, arbitratorShare, protocolShare };
369
+ debitStakeThenBalance(db, loserId, actualFeeU);
370
+ // 分配:人工仲裁各一半(allocate 精确),自动裁定全归协议
371
+ const arbitratorShareU = isHumanArbitrator ? allocate(actualFeeU, [1, 1])[0] : 0;
372
+ const protocolShareU = actualFeeU - arbitratorShareU;
373
+ if (protocolShareU > 0)
374
+ applyWalletDelta(db, sysUserId, { balance: protocolShareU });
375
+ if (arbitratorShareU > 0)
376
+ applyWalletDelta(db, arbitratorId, { balance: arbitratorShareU });
377
+ return { fee: toDecimal(actualFeeU), arbitratorShare: toDecimal(arbitratorShareU), protocolShare: toDecimal(protocolShareU) };
396
378
  }
397
- function executeSettlement(db, orderId, ruling, refundAmount, liablePartyId) {
379
+ export function executeSettlement(db, orderId, ruling, refundAmount, liablePartyId) {
398
380
  const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(orderId);
399
381
  if (!order)
400
382
  return { success: false, error: '订单不存在' };
@@ -405,143 +387,131 @@ function executeSettlement(db, orderId, ruling, refundAmount, liablePartyId) {
405
387
  .get(order.product_id);
406
388
  const stakeAmount = product?.stake_amount ?? 0;
407
389
  const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
390
+ // RFC-014:整数 base-units
391
+ const totalU = toUnits(totalAmount);
392
+ const stakeAmountU = toUnits(stakeAmount);
408
393
  if (ruling === 'refund_buyer') {
409
- // ── 买家胜诉:退全款 + 卖家损失一半质押(惩罚)──────────────
410
- const penalty = Math.round(stakeAmount * 0.5 * 100) / 100;
411
- const stakeReturn = stakeAmount - penalty;
394
+ // ── 买家胜诉:退全款 + 卖家损失一半质押(惩罚,allocate 精确二分)──────────────
395
+ const [penaltyU, stakeReturnU] = stakeAmountU > 0 ? allocate(stakeAmountU, [1, 1]) : [0, 0];
412
396
  db.transaction(() => {
413
397
  // 退还买家托管资金
414
- db.prepare('UPDATE wallets SET escrowed = escrowed - ?, balance = balance + ? WHERE user_id = ?')
415
- .run(totalAmount, totalAmount, buyerId);
416
- // 卖家扣押质押 一半补偿给买家,一半归协议
417
- if (stakeAmount > 0) {
418
- db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(stakeAmount, sellerId);
419
- if (penalty > 0) {
420
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(penalty, buyerId);
421
- }
398
+ applyWalletDelta(db, buyerId, { escrowed: -totalU, balance: totalU });
399
+ // 卖家扣押全额质押 → 一半补偿买家,一半退回卖家(净损一半)
400
+ if (stakeAmountU > 0) {
401
+ applyWalletDelta(db, sellerId, { staked: -stakeAmountU });
402
+ if (penaltyU > 0)
403
+ applyWalletDelta(db, buyerId, { balance: penaltyU });
404
+ if (stakeReturnU > 0)
405
+ applyWalletDelta(db, sellerId, { balance: stakeReturnU });
422
406
  }
423
- transition(db, orderId, 'refunded_full', sysUser.id, [], `争议裁定:退款买家,质押惩罚 ${penalty} WAZ`);
407
+ transition(db, orderId, 'refunded_full', sysUser.id, [], `争议裁定:退款买家,质押惩罚 ${toDecimal(penaltyU)} WAZ`);
424
408
  })();
425
409
  return {
426
410
  success: true,
427
411
  detail: {
428
412
  ruling: 'refund_buyer',
429
- buyer_refund: totalAmount,
430
- buyer_compensation: penalty,
431
- seller_stake_forfeited: stakeAmount,
432
- seller_stake_returned: stakeReturn,
413
+ buyer_refund: toDecimal(totalU),
414
+ buyer_compensation: toDecimal(penaltyU),
415
+ seller_stake_forfeited: toDecimal(stakeAmountU),
416
+ seller_stake_returned: toDecimal(stakeReturnU),
433
417
  }
434
418
  };
435
419
  }
436
420
  else if (ruling === 'release_seller') {
437
421
  // ── 卖家胜诉:资金释放给卖家(正常结算逻辑)──────────────────
438
- const protocolFee = Math.round(totalAmount * 0.02 * 100) / 100;
439
- const logisticsFee = order.logistics_id ? Math.round(totalAmount * 0.05 * 100) / 100 : 0;
440
- const sellerAmount = totalAmount - protocolFee - logisticsFee;
422
+ const protocolFeeU = mulRate(totalU, 0.02);
423
+ const logisticsFeeU = order.logistics_id ? mulRate(totalU, 0.05) : 0;
424
+ const sellerAmountU = totalU - protocolFeeU - logisticsFeeU; // 残值
441
425
  db.transaction(() => {
442
- db.prepare('UPDATE wallets SET escrowed = escrowed - ? WHERE user_id = ?').run(totalAmount, buyerId);
443
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(sellerAmount, sellerId);
444
- if (order.logistics_id && logisticsFee > 0) {
445
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(logisticsFee, order.logistics_id);
446
- }
447
- // 返还卖家质押
448
- if (stakeAmount > 0) {
449
- db.prepare('UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?')
450
- .run(stakeAmount, stakeAmount, sellerId);
451
- }
426
+ applyWalletDelta(db, buyerId, { escrowed: -totalU });
427
+ applyWalletDelta(db, sellerId, { balance: sellerAmountU });
428
+ if (order.logistics_id && logisticsFeeU > 0)
429
+ applyWalletDelta(db, order.logistics_id, { balance: logisticsFeeU });
430
+ if (protocolFeeU > 0)
431
+ applyWalletDelta(db, sysUser.id, { balance: protocolFeeU }); // 协议费入金库
432
+ if (stakeAmountU > 0)
433
+ applyWalletDelta(db, sellerId, { staked: -stakeAmountU, balance: stakeAmountU }); // 返还质押
452
434
  transition(db, orderId, 'resolved_for_seller', sysUser.id, [], '争议裁定:卖家胜诉,资金释放完成');
453
435
  })();
454
436
  return {
455
437
  success: true,
456
438
  detail: {
457
439
  ruling: 'release_seller',
458
- seller_received: sellerAmount,
459
- logistics_fee: logisticsFee,
460
- protocol_fee: protocolFee,
461
- seller_stake_returned: stakeAmount,
440
+ seller_received: toDecimal(sellerAmountU),
441
+ logistics_fee: toDecimal(logisticsFeeU),
442
+ protocol_fee: toDecimal(protocolFeeU),
443
+ seller_stake_returned: toDecimal(stakeAmountU),
462
444
  }
463
445
  };
464
446
  }
465
447
  else if (ruling === 'partial_refund') {
466
- const refund = refundAmount ?? Math.round(totalAmount * 0.5 * 100) / 100;
467
- if (refund > totalAmount)
468
- return { success: false, error: `退款金额 ${refund} 超出订单总额 ${totalAmount}` };
448
+ const refundU = refundAmount != null ? toUnits(refundAmount) : mulRate(totalU, 0.5);
449
+ if (refundU > totalU)
450
+ return { success: false, error: `退款金额 ${toDecimal(refundU)} 超出订单总额 ${totalAmount}` };
469
451
  if (liablePartyId) {
470
452
  // ── 第三方责任 partial_refund ────────────────────────────────
471
453
  // 卖家全额结算(正常收款),买家赔偿由责任方钱包直接支付
472
- const protocolFee = Math.round(totalAmount * 0.02 * 100) / 100;
473
- const logisticsFee = order.logistics_id ? Math.round(totalAmount * 0.05 * 100) / 100 : 0;
474
- const sellerAmount = totalAmount - protocolFee - logisticsFee;
454
+ const protocolFeeU = mulRate(totalU, 0.02);
455
+ const logisticsFeeU = order.logistics_id ? mulRate(totalU, 0.05) : 0;
456
+ const sellerAmountU = totalU - protocolFeeU - logisticsFeeU;
475
457
  // 检查责任方余额是否足够
476
- const liableWallet = db.prepare('SELECT balance, staked FROM wallets WHERE user_id = ?')
458
+ const liableWallet = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked FROM wallets WHERE user_id = ?')
477
459
  .get(liablePartyId);
478
- const liableAvailable = (liableWallet?.balance ?? 0) + (liableWallet?.staked ?? 0);
479
- const actualRefund = Math.min(refund, liableAvailable);
460
+ const liableAvailableU = toUnits(liableWallet?.balance ?? 0) + toUnits(liableWallet?.staked ?? 0);
461
+ const actualRefundU = Math.min(refundU, liableAvailableU);
480
462
  db.transaction(() => {
481
463
  // 1. 释放托管 → 正常结算给卖家
482
- db.prepare('UPDATE wallets SET escrowed = escrowed - ? WHERE user_id = ?').run(totalAmount, buyerId);
483
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(sellerAmount, sellerId);
484
- if (order.logistics_id && logisticsFee > 0) {
485
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(logisticsFee, order.logistics_id);
486
- }
464
+ applyWalletDelta(db, buyerId, { escrowed: -totalU });
465
+ applyWalletDelta(db, sellerId, { balance: sellerAmountU });
466
+ if (order.logistics_id && logisticsFeeU > 0)
467
+ applyWalletDelta(db, order.logistics_id, { balance: logisticsFeeU });
468
+ if (protocolFeeU > 0)
469
+ applyWalletDelta(db, sysUser.id, { balance: protocolFeeU }); // 协议费入金库
487
470
  // 2. 返还卖家质押
488
- if (stakeAmount > 0) {
489
- db.prepare('UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?')
490
- .run(stakeAmount, stakeAmount, sellerId);
491
- }
492
- // 3. 从责任方钱包扣除赔偿(先质押后余额)
493
- if (actualRefund > 0) {
494
- const liableStaked = liableWallet?.staked ?? 0;
495
- if (liableStaked >= actualRefund) {
496
- db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(actualRefund, liablePartyId);
497
- }
498
- else {
499
- const fromBalance = actualRefund - liableStaked;
500
- db.prepare('UPDATE wallets SET staked = 0, balance = balance - ? WHERE user_id = ?').run(fromBalance, liablePartyId);
501
- }
502
- // 4. 赔偿金给买家
503
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(actualRefund, buyerId);
471
+ if (stakeAmountU > 0)
472
+ applyWalletDelta(db, sellerId, { staked: -stakeAmountU, balance: stakeAmountU });
473
+ // 3. 从责任方钱包扣赔偿(先质押后余额) 4. 给买家
474
+ if (actualRefundU > 0) {
475
+ debitStakeThenBalance(db, liablePartyId, actualRefundU);
476
+ applyWalletDelta(db, buyerId, { balance: actualRefundU });
504
477
  }
505
- transition(db, orderId, 'refunded_partial', sysUser.id, [], `争议裁定:第三方责任赔偿 ${actualRefund} WAZ,卖家全额结算`);
478
+ transition(db, orderId, 'refunded_partial', sysUser.id, [], `争议裁定:第三方责任赔偿 ${toDecimal(actualRefundU)} WAZ,卖家全额结算`);
506
479
  })();
507
480
  return {
508
481
  success: true,
509
482
  detail: {
510
483
  ruling: 'partial_refund',
511
484
  liable_party: liablePartyId,
512
- buyer_compensation: actualRefund,
513
- seller_received: sellerAmount,
514
- logistics_fee: logisticsFee,
515
- protocol_fee: protocolFee,
516
- seller_stake_returned: stakeAmount,
485
+ buyer_compensation: toDecimal(actualRefundU),
486
+ seller_received: toDecimal(sellerAmountU),
487
+ logistics_fee: toDecimal(logisticsFeeU),
488
+ protocol_fee: toDecimal(protocolFeeU),
489
+ seller_stake_returned: toDecimal(stakeAmountU),
517
490
  }
518
491
  };
519
492
  }
520
493
  else {
521
- // ── 买卖双方协商 partial_refund(原逻辑)───────────────────────
522
- const sellerGet = Math.round((totalAmount - refund) * 100) / 100;
523
- const stakeReturn = Math.round(stakeAmount * 0.5 * 100) / 100; // 质押返一半
494
+ // ── 买卖双方协商 partial_refund ───────────────────────
495
+ const sellerGetU = totalU - refundU;
496
+ // 政策(option a):协商和解无过错 全额退质押,不罚没。
497
+ const stakeReturnU = stakeAmountU;
524
498
  db.transaction(() => {
525
- db.prepare('UPDATE wallets SET escrowed = escrowed - ? WHERE user_id = ?').run(totalAmount, buyerId);
526
- if (refund > 0) {
527
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(refund, buyerId);
528
- }
529
- if (sellerGet > 0) {
530
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(sellerGet, sellerId);
531
- }
532
- if (stakeAmount > 0) {
533
- db.prepare('UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?')
534
- .run(stakeAmount, stakeReturn, sellerId);
535
- }
536
- transition(db, orderId, 'refunded_partial', sysUser.id, [], `争议裁定:部分退款 ${refund} WAZ`);
499
+ applyWalletDelta(db, buyerId, { escrowed: -totalU });
500
+ if (refundU > 0)
501
+ applyWalletDelta(db, buyerId, { balance: refundU });
502
+ if (sellerGetU > 0)
503
+ applyWalletDelta(db, sellerId, { balance: sellerGetU });
504
+ if (stakeAmountU > 0)
505
+ applyWalletDelta(db, sellerId, { staked: -stakeAmountU, balance: stakeReturnU });
506
+ transition(db, orderId, 'refunded_partial', sysUser.id, [], `争议裁定:部分退款 ${toDecimal(refundU)} WAZ`);
537
507
  })();
538
508
  return {
539
509
  success: true,
540
510
  detail: {
541
511
  ruling: 'partial_refund',
542
- buyer_refund: refund,
543
- seller_received: sellerGet,
544
- seller_stake_returned: stakeReturn,
512
+ buyer_refund: toDecimal(refundU),
513
+ seller_received: toDecimal(sellerGetU),
514
+ seller_stake_returned: toDecimal(stakeReturnU),
545
515
  }
546
516
  };
547
517
  }
@@ -16,6 +16,8 @@
16
16
  * - 其他技能目前免费(增强曝光,间接提升成交)
17
17
  */
18
18
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
19
+ import { toUnits, toDecimal } from '../../money.js';
20
+ import { applyWalletDelta } from '../../ledger.js';
19
21
  // ─── Schema ───────────────────────────────────────────────────
20
22
  export function initSkillSchema(db) {
21
23
  db.exec(`
@@ -223,15 +225,15 @@ export function recordSkillUsage(db, orderId, orderAmount) {
223
225
  `).get(order.buyer_id, order.seller_id);
224
226
  if (!skillSub)
225
227
  return;
226
- const fee = Math.round(orderAmount * 0.005 * 100) / 100; // 0.5% 推荐佣金
227
- if (fee <= 0)
228
+ const feeU = Math.round(toUnits(orderAmount) * 0.005); // 0.5% 推荐佣金(整数 base-units)
229
+ if (feeU <= 0)
228
230
  return;
229
231
  // 从系统账户(protocol fee 池)拨出佣金给 Skill 发布者
230
232
  // 简化实现:直接增加卖家钱包余额(Skill 发布者就是卖家本人)
231
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(fee, skillSub.seller_id);
233
+ applyWalletDelta(db, skillSub.seller_id, { balance: feeU });
232
234
  db.prepare('UPDATE skills SET total_uses = total_uses + 1 WHERE id = ?').run(skillSub.skill_id);
233
235
  const id = generateId('sul');
234
- db.prepare('INSERT INTO skill_usage_log (id, skill_id, user_id, order_id, fee_paid) VALUES (?,?,?,?,?)').run(id, skillSub.skill_id, order.buyer_id, orderId, fee);
236
+ db.prepare('INSERT INTO skill_usage_log (id, skill_id, user_id, order_id, fee_paid) VALUES (?,?,?,?,?)').run(id, skillSub.skill_id, order.buyer_id, orderId, toDecimal(feeU));
235
237
  }
236
238
  // ─── 自动接单 Skill 触发 ──────────────────────────────────────
237
239
  /**
@@ -18,6 +18,8 @@
18
18
  * 绝不进入 PV 二元匹配 / 推土机三级佣金 / fund_base —— 保持双引擎解耦、规避 PV 合规问题。
19
19
  */
20
20
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
21
+ import { toUnits, toDecimal } from '../../money.js';
22
+ import { applyWalletDelta } from '../../ledger.js';
21
23
  export const SKILL_KINDS = ['template', 'prompt', 'guide', 'checklist'];
22
24
  export const SKILL_BILLING_MODES = ['free', 'one_time', 'per_use'];
23
25
  export const SKILL_KIND_META = {
@@ -231,18 +233,20 @@ function hasUnlock(db, userId, listingId) {
231
233
  }
232
234
  /** 扣买家、加作者净额、协议费入 sys_protocol;记 skill_orders。feeRate 由路由层从协议参数传入。 */
233
235
  function settlePayment(db, listing, buyerId, feeRate) {
234
- const price = Number(listing.price);
235
- const fee = Math.round(price * feeRate * 100) / 100;
236
- const net = Math.round((price - fee) * 100) / 100;
236
+ // RFC-014:整数 base-units(买家付 price = 作者净额 net + 协议费 fee,精确守恒)
237
+ const priceU = toUnits(Number(listing.price));
238
+ const feeU = Math.round(priceU * feeRate);
239
+ const netU = priceU - feeU; // 残值,精确
240
+ const price = toDecimal(priceU), fee = toDecimal(feeU), net = toDecimal(netU);
237
241
  const tx = db.transaction(() => {
238
242
  const w = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(buyerId);
239
- if (!w || w.balance < price)
243
+ if (!w || toUnits(w.balance) < priceU)
240
244
  throw new Error('余额不足,请先充值');
241
- db.prepare('UPDATE wallets SET balance = balance - ? WHERE user_id = ?').run(price, buyerId);
242
- db.prepare('UPDATE wallets SET balance = balance + ?, earned = earned + ? WHERE user_id = ?').run(net, net, listing.author_id);
245
+ applyWalletDelta(db, buyerId, { balance: -priceU });
246
+ applyWalletDelta(db, listing.author_id, { balance: netU, earned: netU });
243
247
  db.prepare("INSERT OR IGNORE INTO wallets (user_id, balance) VALUES ('sys_protocol', 0)").run();
244
- if (fee > 0)
245
- db.prepare("UPDATE wallets SET balance = balance + ? WHERE user_id = 'sys_protocol'").run(fee);
248
+ if (feeU > 0)
249
+ applyWalletDelta(db, 'sys_protocol', { balance: feeU });
246
250
  db.prepare('UPDATE skill_listings SET total_sales = total_sales + 1, total_revenue = total_revenue + ? WHERE id = ?').run(price, listing.id);
247
251
  db.prepare('INSERT INTO skill_orders (id, listing_id, buyer_id, billing_mode, amount_paid, protocol_fee, author_net) VALUES (?,?,?,?,?,?,?)')
248
252
  .run(generateId('sko'), listing.id, buyerId, listing.billing_mode, price, fee, net);
package/dist/ledger.js ADDED
@@ -0,0 +1,58 @@
1
+ import { toUnits, toDecimal } from './money.js';
2
+ /** 读某用户钱包当前余额(整数 base-units)。无钱包行 → 全 0。 */
3
+ export function walletUnits(db, userId) {
4
+ const r = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked, COALESCE(escrowed,0) escrowed, COALESCE(earned,0) earned FROM wallets WHERE user_id = ?')
5
+ .get(userId);
6
+ return { balance: toUnits(r?.balance ?? 0), staked: toUnits(r?.staked ?? 0), escrowed: toUnits(r?.escrowed ?? 0), earned: toUnits(r?.earned ?? 0) };
7
+ }
8
+ /**
9
+ * 对钱包字段施加整数单位 delta,以【绝对值】落库(读当前→加 delta→写 toDecimal)。
10
+ * - 字段白名单固定(balance/staked/escrowed/earned),无 SQL 注入面。
11
+ * - 钱包行不存在 → UPDATE 影响 0 行(与历史相对更新的静默行为一致)。
12
+ * - 必须在调用方的 transaction 内使用(资金路径本就如此)。
13
+ */
14
+ export function applyWalletDelta(db, userId, deltas) {
15
+ const fields = Object.keys(deltas).filter(f => deltas[f] !== undefined && deltas[f] !== 0);
16
+ if (fields.length === 0)
17
+ return;
18
+ const cur = walletUnits(db, userId);
19
+ const sets = [];
20
+ const vals = [];
21
+ for (const f of fields) {
22
+ sets.push(`${f} = ?`);
23
+ vals.push(toDecimal(cur[f] + deltas[f]));
24
+ }
25
+ vals.push(userId);
26
+ db.prepare(`UPDATE wallets SET ${sets.join(', ')} WHERE user_id = ?`).run(...vals);
27
+ }
28
+ /**
29
+ * 从某用户【先扣 staked,不足再扣 balance】,封顶其可用额(staked+balance,不转负)。返回实扣 units。
30
+ * 争议罚没/仲裁费常用:质押优先承担,质押不够再动自由余额。绝对值落库,无浮点 dust。
31
+ */
32
+ export function debitStakeThenBalance(db, userId, amountU) {
33
+ const cur = walletUnits(db, userId);
34
+ const avail = Math.max(0, cur.staked) + Math.max(0, cur.balance);
35
+ const actual = Math.min(Math.max(0, amountU), avail);
36
+ if (actual <= 0)
37
+ return 0;
38
+ const fromStaked = Math.min(actual, Math.max(0, cur.staked));
39
+ const fromBalance = actual - fromStaked;
40
+ applyWalletDelta(db, userId, { staked: -fromStaked, balance: -fromBalance });
41
+ return actual;
42
+ }
43
+ /**
44
+ * 通用基金/池表的【绝对值】入账(整数 base-units):读当前→加 delta→写 toDecimal。
45
+ * 用于 global_fund / management_bonus_pool / commission_reserve / charity_fund 等单行池表。
46
+ * table / 列名 / whereClause 均为【代码字面量】(非用户输入)→ 无 SQL 注入面。
47
+ * 行不存在 → UPDATE 影响 0 行(与历史相对更新一致)。
48
+ */
49
+ export function creditColumns(db, table, whereClause, whereArgs, deltas) {
50
+ const cols = Object.keys(deltas).filter(c => deltas[c] !== 0);
51
+ if (cols.length === 0)
52
+ return;
53
+ const cur = db.prepare(`SELECT ${cols.map(c => `COALESCE(${c},0) AS ${c}`).join(', ')} FROM ${table} WHERE ${whereClause}`)
54
+ .get(...whereArgs);
55
+ const sets = cols.map(c => `${c} = ?`).join(', ');
56
+ const vals = cols.map(c => toDecimal(toUnits(cur?.[c] ?? 0) + deltas[c]));
57
+ db.prepare(`UPDATE ${table} SET ${sets} WHERE ${whereClause}`).run(...vals, ...whereArgs);
58
+ }