@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.
Files changed (126) hide show
  1. package/dist/bond-refund-blockers.js +50 -0
  2. package/dist/bond-slash.js +100 -0
  3. package/dist/bond-terms.js +31 -0
  4. package/dist/direct-pay-bond-rail-clearance.js +10 -5
  5. package/dist/direct-pay-cancel-refund.js +160 -0
  6. package/dist/direct-pay-create.js +78 -9
  7. package/dist/direct-pay-disclosures.js +17 -11
  8. package/dist/direct-pay-fee-ar.js +2 -2
  9. package/dist/direct-pay-fee-prepay-request.js +80 -0
  10. package/dist/direct-pay-returns.js +104 -0
  11. package/dist/direct-pay-stock.js +9 -0
  12. package/dist/direct-receive-deferral.js +30 -1
  13. package/dist/direct-receive-deposits.js +122 -17
  14. package/dist/free-shipping.js +37 -0
  15. package/dist/fx-rates.js +7 -0
  16. package/dist/layer0-foundation/L0-1-database/schema.js +246 -0
  17. package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
  18. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +73 -0
  19. package/dist/layer1-agent/L1-1-mcp-server/server.js +150 -33
  20. package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
  21. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
  22. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
  23. package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
  24. package/dist/platform-receive-accounts.js +94 -0
  25. package/dist/pwa/arbitration-read-admin.js +37 -0
  26. package/dist/pwa/arbitrator-lifecycle.js +100 -0
  27. package/dist/pwa/contract-fingerprint.js +15 -0
  28. package/dist/pwa/direct-pay-order-redaction.js +20 -2
  29. package/dist/pwa/human-presence.js +2 -6
  30. package/dist/pwa/public/app-account.js +9 -9
  31. package/dist/pwa/public/app-admin-disputes.js +55 -0
  32. package/dist/pwa/public/app-agent-appeal.js +90 -0
  33. package/dist/pwa/public/app-agent-approvals.js +93 -0
  34. package/dist/pwa/public/app-agent-pair.js +127 -0
  35. package/dist/pwa/public/app-arbitrator-admin.js +87 -0
  36. package/dist/pwa/public/app-arbitrator-entry.js +9 -0
  37. package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
  38. package/dist/pwa/public/app-bond-refund-ui.js +66 -0
  39. package/dist/pwa/public/app-bond-slash-ui.js +74 -0
  40. package/dist/pwa/public/app-bond-terms-ui.js +23 -0
  41. package/dist/pwa/public/app-bond-ui.js +108 -0
  42. package/dist/pwa/public/app-chat-poll.js +6 -6
  43. package/dist/pwa/public/app-contribution-hub.js +23 -0
  44. package/dist/pwa/public/app-direct-pay-accounts.js +1 -1
  45. package/dist/pwa/public/app-direct-pay-buyer.js +1 -1
  46. package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
  47. package/dist/pwa/public/app-direct-pay-copy.js +12 -0
  48. package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
  49. package/dist/pwa/public/app-direct-pay-fee-ops.js +1 -1
  50. package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
  51. package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
  52. package/dist/pwa/public/app-direct-pay-memo.js +14 -0
  53. package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
  54. package/dist/pwa/public/app-direct-pay-pay.js +15 -0
  55. package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
  56. package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
  57. package/dist/pwa/public/app-direct-pay-returns.js +56 -0
  58. package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
  59. package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
  60. package/dist/pwa/public/app-direct-pay.js +36 -37
  61. package/dist/pwa/public/app-dispute-close-ui.js +38 -0
  62. package/dist/pwa/public/app-free-shipping-ui.js +29 -0
  63. package/dist/pwa/public/app-gmv-rail-split.js +12 -0
  64. package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
  65. package/dist/pwa/public/app-mutual-cancel.js +54 -0
  66. package/dist/pwa/public/app-notif-templates-orders.js +43 -0
  67. package/dist/pwa/public/app-notif-templates.js +22 -0
  68. package/dist/pwa/public/app-order-accept-ui.js +158 -0
  69. package/dist/pwa/public/app-order-errors.js +50 -0
  70. package/dist/pwa/public/app-order-labels.js +19 -0
  71. package/dist/pwa/public/app-order-rail-filter.js +26 -0
  72. package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
  73. package/dist/pwa/public/app-poll-governor.js +22 -0
  74. package/dist/pwa/public/app-profile.js +4 -4
  75. package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
  76. package/dist/pwa/public/app-sale-regions-ui.js +38 -0
  77. package/dist/pwa/public/app-seller.js +1 -1
  78. package/dist/pwa/public/app-trade-tax-ui.js +33 -0
  79. package/dist/pwa/public/app.js +133 -160
  80. package/dist/pwa/public/i18n.js +675 -5
  81. package/dist/pwa/public/index.html +41 -0
  82. package/dist/pwa/public/openapi.json +719 -11
  83. package/dist/pwa/public/style.css +3 -0
  84. package/dist/pwa/routes/admin-direct-receive-deposits.js +215 -3
  85. package/dist/pwa/routes/admin-protocol-params.js +16 -0
  86. package/dist/pwa/routes/admin-reports.js +31 -5
  87. package/dist/pwa/routes/agent-governance.js +1 -1
  88. package/dist/pwa/routes/agent-grants.js +281 -32
  89. package/dist/pwa/routes/analytics.js +2 -0
  90. package/dist/pwa/routes/arbitrator.js +67 -14
  91. package/dist/pwa/routes/bond-seller.js +162 -0
  92. package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
  93. package/dist/pwa/routes/direct-pay-disclosure-acks.js +9 -4
  94. package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
  95. package/dist/pwa/routes/direct-pay-returns.js +74 -0
  96. package/dist/pwa/routes/direct-pay-timeouts.js +186 -9
  97. package/dist/pwa/routes/disputes-read.js +20 -6
  98. package/dist/pwa/routes/disputes-write.js +91 -33
  99. package/dist/pwa/routes/external-anchors.js +4 -2
  100. package/dist/pwa/routes/fee-prepay-requests.js +66 -0
  101. package/dist/pwa/routes/governance-onboarding.js +46 -8
  102. package/dist/pwa/routes/logistics.js +4 -4
  103. package/dist/pwa/routes/me-data.js +2 -2
  104. package/dist/pwa/routes/mutual-cancel.js +62 -0
  105. package/dist/pwa/routes/orders-action.js +182 -9
  106. package/dist/pwa/routes/orders-create.js +18 -7
  107. package/dist/pwa/routes/orders-read.js +76 -14
  108. package/dist/pwa/routes/platform-receive-accounts.js +111 -0
  109. package/dist/pwa/routes/products-create.js +45 -16
  110. package/dist/pwa/routes/products-update.js +15 -1
  111. package/dist/pwa/routes/profile-identity.js +4 -2
  112. package/dist/pwa/routes/returns.js +39 -8
  113. package/dist/pwa/routes/seller-directpay-report.js +110 -0
  114. package/dist/pwa/routes/seller-quota.js +5 -1
  115. package/dist/pwa/routes/shipping-templates.js +219 -0
  116. package/dist/pwa/routes/snf.js +4 -1
  117. package/dist/pwa/routes/webauthn.js +3 -3
  118. package/dist/pwa/server.js +63 -40
  119. package/dist/runtime/agent-grant-scopes.js +69 -1
  120. package/dist/runtime/webaz-schema-helpers.js +57 -3
  121. package/dist/sale-regions.js +116 -0
  122. package/dist/shipping-templates.js +119 -0
  123. package/dist/trade-tax.js +99 -0
  124. package/dist/trade-terms.js +76 -0
  125. package/dist/version.js +1 -1
  126. package/package.json +61 -2
@@ -0,0 +1,110 @@
1
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js';
2
+ // 直付订单状态桶(与状态机一致):已完成(计提平台费)/ 在途 / 已取消·退款。
3
+ const COMPLETED = ['completed', 'confirmed'];
4
+ const IN_FLIGHT = ['pending_accept', 'direct_pay_window', 'accepted', 'shipped', 'picked_up', 'in_transit', 'delivered', 'payment_query', 'direct_expired_unconfirmed'];
5
+ const CLOSED_NEG = ['cancelled', 'expired', 'refunded_full', 'refunded_partial', 'dispute_dismissed', 'resolved_for_seller', 'fault_seller', 'fault_buyer', 'fault_logistics'];
6
+ const inList = (col, arr) => `${col} IN (${arr.map(() => '?').join(',')})`;
7
+ export function registerSellerDirectPayReportRoutes(app, deps) {
8
+ const { db, auth } = deps;
9
+ function requireSeller(req, res) {
10
+ const user = auth(req, res);
11
+ if (!user)
12
+ return null;
13
+ const roles = (() => { try {
14
+ return JSON.parse(String(user.roles ?? '[]'));
15
+ }
16
+ catch {
17
+ return [];
18
+ } })();
19
+ if (user.role !== 'seller' && !(Array.isArray(roles) && roles.includes('seller'))) {
20
+ res.status(403).json({ error: '仅卖家可查看', error_code: 'SELLER_ONLY' });
21
+ return null;
22
+ }
23
+ return user;
24
+ }
25
+ // GET /api/sellers/me/direct-pay-report?from=YYYY-MM-DD&to=YYYY-MM-DD
26
+ // from/to 均可选(闭区间,按日期比较 substr(created_at,1,10));返回汇总 + 按月 + 逐单(含平台费明细)。
27
+ app.get('/api/sellers/me/direct-pay-report', async (req, res) => {
28
+ const user = requireSeller(req, res);
29
+ if (!user)
30
+ return;
31
+ const sellerId = user.id;
32
+ const from = typeof req.query.from === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(req.query.from) ? req.query.from : null;
33
+ const to = typeof req.query.to === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(req.query.to) ? req.query.to : null;
34
+ const range = [];
35
+ const rp = [];
36
+ if (from) {
37
+ range.push('substr(o.created_at,1,10) >= ?');
38
+ rp.push(from);
39
+ }
40
+ if (to) {
41
+ range.push('substr(o.created_at,1,10) <= ?');
42
+ rp.push(to);
43
+ }
44
+ const rangeClause = range.length ? ' AND ' + range.join(' AND ') : '';
45
+ // ① 汇总(单查:订单数 / 销售额 / 各桶计数与已完成销售额)
46
+ const summary = (await dbOne(`
47
+ SELECT
48
+ COUNT(*) as order_count,
49
+ COALESCE(SUM(o.total_amount), 0) as gross_order_total,
50
+ SUM(CASE WHEN ${inList('o.status', COMPLETED)} THEN 1 ELSE 0 END) as completed_count,
51
+ COALESCE(SUM(CASE WHEN ${inList('o.status', COMPLETED)} THEN o.total_amount ELSE 0 END), 0) as completed_sales,
52
+ SUM(CASE WHEN ${inList('o.status', IN_FLIGHT)} THEN 1 ELSE 0 END) as in_flight_count,
53
+ SUM(CASE WHEN ${inList('o.status', CLOSED_NEG)} THEN 1 ELSE 0 END) as closed_count
54
+ FROM orders o
55
+ WHERE o.seller_id = ? AND o.payment_rail = 'direct_p2p'${rangeClause}
56
+ `, [...COMPLETED, ...COMPLETED, ...IN_FLIGHT, ...CLOSED_NEG, sellerId, ...rp]));
57
+ // ② 区间内已计提平台费合计(逐单应收 join 订单,按订单下单日筛)
58
+ const feeAgg = (await dbOne(`
59
+ SELECT COALESCE(SUM(r.amount), 0) as fee_accrued, COUNT(*) as fee_count
60
+ FROM direct_pay_fee_receivables r JOIN orders o ON o.id = r.order_id
61
+ WHERE o.seller_id = ? AND o.payment_rail = 'direct_p2p'${rangeClause}
62
+ `, [sellerId, ...rp]));
63
+ // ③ 按月(最多 24 个月)—— 订单总额(含各状态)与完成单销售额分列,不混
64
+ const byMonth = await dbAll(`
65
+ SELECT substr(o.created_at, 1, 7) as month, COUNT(*) as order_count,
66
+ COALESCE(SUM(o.total_amount), 0) as gross_order_total,
67
+ COALESCE(SUM(CASE WHEN ${inList('o.status', COMPLETED)} THEN o.total_amount ELSE 0 END), 0) as completed_sales
68
+ FROM orders o
69
+ WHERE o.seller_id = ? AND o.payment_rail = 'direct_p2p'${rangeClause}
70
+ GROUP BY month ORDER BY month DESC LIMIT 24
71
+ `, [...COMPLETED, sellerId, ...rp]);
72
+ // ④ 逐单明细(含平台费:LEFT JOIN 应收表 —— 未完成单尚无 fee 行,fee 为 null)。上限 500。
73
+ const LIMIT = 500;
74
+ const rows = await dbAll(`
75
+ SELECT o.id, o.created_at, o.status, o.total_amount, o.ship_to_region, p.title as product_title,
76
+ r.amount as fee_amount, r.accrued_at as fee_accrued_at
77
+ FROM orders o
78
+ LEFT JOIN products p ON p.id = o.product_id
79
+ LEFT JOIN direct_pay_fee_receivables r ON r.order_id = o.id
80
+ WHERE o.seller_id = ? AND o.payment_rail = 'direct_p2p'${rangeClause}
81
+ ORDER BY o.created_at DESC LIMIT ?
82
+ `, [sellerId, ...rp, LIMIT + 1]);
83
+ const truncated = rows.length > LIMIT;
84
+ if (truncated)
85
+ rows.length = LIMIT;
86
+ res.setHeader('Cache-Control', 'no-store');
87
+ res.json({
88
+ range: { from, to },
89
+ currency_note: '销售额=下单计价币(买家应付金额);平台服务费=USDC。两者不同币种,分列展示。',
90
+ summary: {
91
+ order_count: Number(summary.order_count) || 0,
92
+ gross_order_total: Number(summary.gross_order_total) || 0, // 所有状态订单总额(含在途/已关闭)—— 非真实销售
93
+ completed_count: Number(summary.completed_count) || 0,
94
+ completed_sales: Number(summary.completed_sales) || 0, // 真实销售(仅 completed/confirmed)
95
+ in_flight_count: Number(summary.in_flight_count) || 0,
96
+ closed_count: Number(summary.closed_count) || 0,
97
+ fee_accrued_total: Number(feeAgg.fee_accrued) || 0, // 区间已计提平台费(USDC)
98
+ fee_order_count: Number(feeAgg.fee_count) || 0,
99
+ },
100
+ by_month: byMonth.map(m => ({ month: m.month, order_count: Number(m.order_count) || 0, gross_order_total: Number(m.gross_order_total) || 0, completed_sales: Number(m.completed_sales) || 0 })),
101
+ orders: rows.map(r => ({
102
+ id: r.id, created_at: r.created_at, status: r.status, total_amount: Number(r.total_amount) || 0,
103
+ ship_to_region: r.ship_to_region || null, product_title: r.product_title || null,
104
+ fee_amount: r.fee_amount == null ? null : Number(r.fee_amount), // null=未完成尚未计提
105
+ fee_accrued_at: r.fee_accrued_at || null,
106
+ })),
107
+ truncated,
108
+ });
109
+ });
110
+ }
@@ -45,7 +45,7 @@ export function registerSellerQuotaRoutes(app, deps) {
45
45
  const d30 = fmt(new Date(now - 30 * 86400000));
46
46
  const d60 = fmt(new Date(now - 60 * 86400000));
47
47
  const orders = await dbAll(`
48
- SELECT o.id, o.product_id, o.buyer_id, o.status, o.total_amount, o.created_at,
48
+ SELECT o.id, o.product_id, o.buyer_id, o.status, o.total_amount, o.created_at, o.payment_rail,
49
49
  COALESCE(p.title, '已下架商品') as product_title,
50
50
  COALESCE(ub.name, '匿名') as buyer_name
51
51
  FROM orders o
@@ -60,6 +60,8 @@ export function registerSellerQuotaRoutes(app, deps) {
60
60
  const inLast30 = orders.filter(o => o.created_at >= d30);
61
61
  const inPrev30 = orders.filter(o => o.created_at < d30);
62
62
  const gmv = (arr) => arr.filter(o => completedStatuses.has(o.status)).reduce((s, o) => s + Number(o.total_amount || 0), 0);
63
+ // GMV 按支付轨拆分:托管=平台真实托管收入,直接收款=场外收款(平台不经手)—— 不再混算(诚实口径)
64
+ const gmvRail = (arr, rail) => arr.filter(o => completedStatuses.has(o.status) && (rail === 'direct_p2p' ? o.payment_rail === 'direct_p2p' : (o.payment_rail || 'escrow') === 'escrow')).reduce((s, o) => s + Number(o.total_amount || 0), 0);
63
65
  const curGmv = gmv(inLast30);
64
66
  const prevGmv = gmv(inPrev30);
65
67
  const curCount = inLast30.length;
@@ -116,6 +118,8 @@ export function registerSellerQuotaRoutes(app, deps) {
116
118
  period_days: 30,
117
119
  summary: {
118
120
  gmv: curGmv,
121
+ gmv_escrow: gmvRail(inLast30, 'escrow'),
122
+ gmv_direct_pay: gmvRail(inLast30, 'direct_p2p'),
119
123
  order_count: curCount,
120
124
  completed_count: completedOrders30.length,
121
125
  aov,
@@ -0,0 +1,219 @@
1
+ import { parseShippingTemplate, loadTemplateJson, resolveShipping } from '../../shipping-templates.js';
2
+ import { validateSaleRegionsInput, parseSaleRegionsRule, regionAllowedByRule, parsePlatformBlocklist } from '../../sale-regions.js'; // S1 可售区域(店铺+单品写入;gate 在 orders-create;S5 只读披露复用判定 + 与建单门同一 blocklist parser)
3
+ import { validateFreeShippingThreshold } from '../../free-shipping.js'; // 营销域满额免邮(S2 返工:写入口暂列本设置面,规则/判定在 free-shipping.ts)
4
+ import { validateImportDutyTerms, validateTaxLines, effectiveImportDutyTerms, effectiveTaxLines, parseTaxLines, taxLinesForRegion } from '../../trade-tax.js'; // S3 跨境税费/进口责任声明层(seller-declared,平台不算不收)
5
+ import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
6
+ export function registerShippingTemplateRoutes(app, deps) {
7
+ const { auth, errorRes } = deps;
8
+ app.post('/api/seller/shipping-template', async (req, res) => {
9
+ const user = auth(req, res);
10
+ if (!user)
11
+ return;
12
+ if (user.role !== 'seller')
13
+ return void errorRes(res, 403, 'SELLER_ONLY', '仅卖家可设置运费模板');
14
+ const b = req.body || {};
15
+ const touched = {};
16
+ if ('store_template' in b) {
17
+ const p = parseShippingTemplate(b.store_template);
18
+ if (!p.ok)
19
+ return void errorRes(res, 400, 'BAD_SHIPPING_TEMPLATE', p.error);
20
+ await dbRun('UPDATE users SET store_shipping_template = ? WHERE id = ?', [p.entries ? JSON.stringify(p.entries) : null, user.id]);
21
+ touched.store_template = p.entries;
22
+ }
23
+ if ('store_quote_ok' in b) { // PR-3:模板外地区询价 opt-in(店铺默认;直付轨生效)
24
+ if (b.store_quote_ok !== null && typeof b.store_quote_ok !== 'boolean')
25
+ return void errorRes(res, 400, 'BAD_QUOTE_OK', 'store_quote_ok 只允许 true|false|null');
26
+ await dbRun('UPDATE users SET store_shipping_quote_ok = ? WHERE id = ?', [b.store_quote_ok === null ? null : (b.store_quote_ok ? 1 : 0), user.id]);
27
+ touched.store_quote_ok = b.store_quote_ok;
28
+ }
29
+ if ('store_sale_regions' in b) { // S1 可售区域(店铺默认):{mode:'all'|'list', include?, exclude?};null=清除(不限)
30
+ const v = validateSaleRegionsInput(b.store_sale_regions);
31
+ if ('error' in v)
32
+ return void errorRes(res, 400, 'BAD_SALE_REGIONS', v.error);
33
+ await dbRun('UPDATE users SET store_sale_regions = ? WHERE id = ?', [v.json, user.id]);
34
+ touched.store_sale_regions = v.json ? JSON.parse(v.json) : null;
35
+ }
36
+ if ('product_id' in b && 'sale_regions' in b) { // S1 单品覆盖(优先于店铺;null=回落店铺)
37
+ const v = validateSaleRegionsInput(b.sale_regions);
38
+ if ('error' in v)
39
+ return void errorRes(res, 400, 'BAD_SALE_REGIONS', v.error);
40
+ const prodS = await dbOne('SELECT seller_id FROM products WHERE id = ?', [b.product_id]);
41
+ if (!prodS)
42
+ return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
43
+ if (prodS.seller_id !== user.id)
44
+ return void errorRes(res, 403, 'NOT_PRODUCT_OWNER', '只能设置自己商品');
45
+ await dbRun('UPDATE products SET sale_regions = ? WHERE id = ?', [v.json, b.product_id]);
46
+ touched.product_sale_regions = v.json ? JSON.parse(v.json) : null;
47
+ }
48
+ if ('store_free_shipping_threshold' in b) { // 营销:满额免邮(店铺默认;null=清除)。券后货款≥阈值→运费商家承担
49
+ const v = validateFreeShippingThreshold(b.store_free_shipping_threshold);
50
+ if ('error' in v)
51
+ return void errorRes(res, 400, 'BAD_FREE_SHIPPING_THRESHOLD', v.error);
52
+ await dbRun('UPDATE users SET store_free_shipping_threshold = ? WHERE id = ?', [v.value, user.id]);
53
+ touched.store_free_shipping_threshold = v.value;
54
+ }
55
+ if ('product_id' in b && 'free_shipping_threshold' in b) { // 营销:单品覆盖(null=回落店铺)
56
+ const v = validateFreeShippingThreshold(b.free_shipping_threshold);
57
+ if ('error' in v)
58
+ return void errorRes(res, 400, 'BAD_FREE_SHIPPING_THRESHOLD', v.error);
59
+ const prodF = await dbOne('SELECT seller_id FROM products WHERE id = ?', [b.product_id]);
60
+ if (!prodF)
61
+ return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
62
+ if (prodF.seller_id !== user.id)
63
+ return void errorRes(res, 403, 'NOT_PRODUCT_OWNER', '只能设置自己商品');
64
+ await dbRun('UPDATE products SET free_shipping_threshold = ? WHERE id = ?', [v.value, b.product_id]);
65
+ touched.product_free_shipping_threshold = v.value;
66
+ }
67
+ if ('store_import_duty_terms' in b) { // S3:DDP/DDU 进口责任声明(店铺默认;null=清除)
68
+ const v = validateImportDutyTerms(b.store_import_duty_terms);
69
+ if ('error' in v)
70
+ return void errorRes(res, 400, 'BAD_IMPORT_DUTY_TERMS', v.error);
71
+ await dbRun('UPDATE users SET store_import_duty_terms = ? WHERE id = ?', [v.value, user.id]);
72
+ touched.store_import_duty_terms = v.value;
73
+ }
74
+ if ('product_id' in b && 'import_duty_terms' in b) { // S3:单品覆盖(null=回落店铺)
75
+ const v = validateImportDutyTerms(b.import_duty_terms);
76
+ if ('error' in v)
77
+ return void errorRes(res, 400, 'BAD_IMPORT_DUTY_TERMS', v.error);
78
+ const prodD = await dbOne('SELECT seller_id FROM products WHERE id = ?', [b.product_id]);
79
+ if (!prodD)
80
+ return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
81
+ if (prodD.seller_id !== user.id)
82
+ return void errorRes(res, 403, 'NOT_PRODUCT_OWNER', '只能设置自己商品');
83
+ await dbRun('UPDATE products SET import_duty_terms = ? WHERE id = ?', [v.value, b.product_id]);
84
+ touched.product_import_duty_terms = v.value;
85
+ }
86
+ if ('store_tax_lines' in b) { // S3:价内已含税声明(店铺默认;仅 'included';null=清除)
87
+ const v = validateTaxLines(b.store_tax_lines);
88
+ if ('error' in v)
89
+ return void errorRes(res, 400, 'BAD_TAX_LINES', v.error);
90
+ await dbRun('UPDATE users SET store_tax_lines = ? WHERE id = ?', [v.value, user.id]);
91
+ touched.store_tax_lines = v.value ? JSON.parse(v.value) : null;
92
+ }
93
+ if ('product_id' in b && 'tax_lines' in b) { // S3:单品覆盖(null=回落店铺)
94
+ const v = validateTaxLines(b.tax_lines);
95
+ if ('error' in v)
96
+ return void errorRes(res, 400, 'BAD_TAX_LINES', v.error);
97
+ const prodT = await dbOne('SELECT seller_id FROM products WHERE id = ?', [b.product_id]);
98
+ if (!prodT)
99
+ return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
100
+ if (prodT.seller_id !== user.id)
101
+ return void errorRes(res, 403, 'NOT_PRODUCT_OWNER', '只能设置自己商品');
102
+ await dbRun('UPDATE products SET tax_lines = ? WHERE id = ?', [v.value, b.product_id]);
103
+ touched.product_tax_lines = v.value ? JSON.parse(v.value) : null;
104
+ }
105
+ if ('product_id' in b && 'quote_ok' in b) {
106
+ if (b.quote_ok !== null && typeof b.quote_ok !== 'boolean')
107
+ return void errorRes(res, 400, 'BAD_QUOTE_OK', 'quote_ok 只允许 true|false|null');
108
+ const prodQ = await dbOne('SELECT seller_id FROM products WHERE id = ?', [b.product_id]);
109
+ if (!prodQ)
110
+ return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
111
+ if (prodQ.seller_id !== user.id)
112
+ return void errorRes(res, 403, 'NOT_PRODUCT_OWNER', '只能设置自己商品');
113
+ await dbRun('UPDATE products SET shipping_quote_ok = ? WHERE id = ?', [b.quote_ok === null ? null : (b.quote_ok ? 1 : 0), b.product_id]);
114
+ touched.product_quote_ok = b.quote_ok;
115
+ }
116
+ if ('template' in b) { // 单品运费模板:仅在显式带 template 时进入 —— 关闭整类隐患(此前 'product_id' in b 会让 {product_id, store_*} 等组合尾随进来把模板静默清成 NULL;审计 P2)。清除走 {product_id, template:null}('template' in b 成立)
117
+ if (!b.product_id)
118
+ return void errorRes(res, 400, 'MISSING_PRODUCT_ID', '设置单品运费模板须带 product_id');
119
+ const p = parseShippingTemplate(b.template);
120
+ if (!p.ok)
121
+ return void errorRes(res, 400, 'BAD_SHIPPING_TEMPLATE', p.error);
122
+ const prod = await dbOne('SELECT seller_id FROM products WHERE id = ?', [b.product_id]);
123
+ if (!prod)
124
+ return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
125
+ if (prod.seller_id !== user.id)
126
+ return void errorRes(res, 403, 'NOT_PRODUCT_OWNER', '只能设置自己商品的运费模板');
127
+ await dbRun('UPDATE products SET shipping_template = ? WHERE id = ?', [p.entries ? JSON.stringify(p.entries) : null, b.product_id]);
128
+ touched.product_template = p.entries;
129
+ }
130
+ if (Object.keys(touched).length === 0)
131
+ return void errorRes(res, 400, 'NOTHING_TO_SET', '未提供任何设置项');
132
+ return void res.json({ success: true, ...touched });
133
+ });
134
+ // 卖家读自己的店铺级设置(设置 UI 回显):接单模式 + 运费模板 + 询价开关。
135
+ app.get('/api/seller/shipping-settings', async (req, res) => {
136
+ const user = auth(req, res);
137
+ if (!user)
138
+ return;
139
+ if (user.role !== 'seller')
140
+ return void errorRes(res, 403, 'SELLER_ONLY', '仅卖家');
141
+ const row = await dbOne('SELECT store_accept_mode, store_shipping_template, store_shipping_quote_ok, store_sale_regions, store_free_shipping_threshold, store_import_duty_terms, store_tax_lines FROM users WHERE id = ?', [user.id]);
142
+ return void res.json({
143
+ store_accept_mode: row?.store_accept_mode ?? null,
144
+ store_sale_regions: parseSaleRegionsRule(row?.store_sale_regions ?? null),
145
+ store_free_shipping_threshold: row?.store_free_shipping_threshold ?? null,
146
+ store_import_duty_terms: (row?.store_import_duty_terms === 'ddu' || row?.store_import_duty_terms === 'ddp') ? row.store_import_duty_terms : null,
147
+ store_tax_lines: parseTaxLines(row?.store_tax_lines ?? null),
148
+ store_template: loadTemplateJson(row?.store_shipping_template),
149
+ store_quote_ok: row?.store_shipping_quote_ok == null ? null : Number(row.store_shipping_quote_ok) === 1,
150
+ });
151
+ });
152
+ // 公开读:买家下单前查配送范围。生效 = 单品覆盖 ?? 店铺默认;template=null → 不按地区计费(下单不要求选地区)。
153
+ app.get('/api/products/:id/shipping-options', async (req, res) => {
154
+ const prod = await dbOne('SELECT id, seller_id, shipping_template, shipping_quote_ok, import_duty_terms, tax_lines, sale_regions, free_shipping_threshold FROM products WHERE id = ?', [req.params.id]);
155
+ if (!prod)
156
+ return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
157
+ let entries = loadTemplateJson(prod.shipping_template);
158
+ let source = entries ? 'product' : null;
159
+ const seller = await dbOne('SELECT store_shipping_template, store_shipping_quote_ok, store_import_duty_terms, store_tax_lines, store_sale_regions, store_free_shipping_threshold FROM users WHERE id = ?', [prod.seller_id]);
160
+ if (!entries) {
161
+ entries = loadTemplateJson(seller?.store_shipping_template);
162
+ if (entries)
163
+ source = 'store';
164
+ }
165
+ const quoteOk = prod.shipping_quote_ok != null ? Number(prod.shipping_quote_ok) === 1 : Number(seller?.store_shipping_quote_ok) === 1;
166
+ // S3 税费/进口责任披露(买家下单前可见;卖家 seller-declared,平台不算不收)。
167
+ // 价内含税【按收货地区过滤】(?ship_to_region):目的区匹配项 + '*' 通用项;无地区参数=仅 '*'(只披露必然适用的,
168
+ // 不把不属于该目的地的税误示给买家)。DDP/DDU 是整单单一声明,不按区分。
169
+ const importDuty = effectiveImportDutyTerms(prod.import_duty_terms, seller?.store_import_duty_terms);
170
+ const shipToRegion = typeof req.query.ship_to_region === 'string' && req.query.ship_to_region.trim() ? req.query.ship_to_region.trim().toUpperCase() : null;
171
+ const taxLines = taxLinesForRegion(effectiveTaxLines(prod.tax_lines, seller?.store_tax_lines), shipToRegion);
172
+ // S5 买家下单前聚合(只读,复用 S1-S4 判定;不动订单金额/不收税)。
173
+ const saleRule = parseSaleRegionsRule(prod.sale_regions) ?? parseSaleRegionsRule(seller?.store_sale_regions ?? null); // 可售规则(商品??店铺)
174
+ // 平台合规名单:与建单门 gateSaleRegionForCreate 共用 parsePlatformBlocklist,坏配置 fail-closed 一致(建单 503 ⇄ 预检 platform_policy_invalid),
175
+ // 不再把坏配置当空名单静默放行(否则预检显示"可买"、提交才 503,与 create gate 不一致)。缺省 '[]'/无行=空名单=可售。
176
+ const pr = await dbOne("SELECT value FROM protocol_params WHERE key = 'trade.platform_region_blocklist'").catch(() => null);
177
+ const parsedBlock = parsePlatformBlocklist(pr ? pr.value : '[]');
178
+ const policyInvalid = !parsedBlock.ok;
179
+ const platformBlock = parsedBlock.ok ? parsedBlock.list : [];
180
+ const freeThreshold = (prod.free_shipping_threshold != null && Number(prod.free_shipping_threshold) > 0) ? Number(prod.free_shipping_threshold)
181
+ : (seller?.store_free_shipping_threshold != null && Number(seller.store_free_shipping_threshold) > 0 ? Number(seller.store_free_shipping_threshold) : null);
182
+ // 可售裁定(镜像建单 gateSaleRegionForCreate 的判定,只读不抛):平台配置异常 > 平台合规 > 商家意愿。
183
+ const restricted = policyInvalid || !!saleRule || platformBlock.length > 0;
184
+ let sellable = { ok: true, reason: 'ok' };
185
+ if (policyInvalid)
186
+ sellable = { ok: false, reason: 'platform_policy_invalid' }; // 坏配置 fail-closed:显示不可下单,不诱导买家提交后才 503
187
+ else if (restricted) {
188
+ if (!shipToRegion)
189
+ sellable = { ok: false, reason: 'region_required' };
190
+ else if (platformBlock.includes(shipToRegion))
191
+ sellable = { ok: false, reason: 'product_restricted' }; // 平台合规,商家不可放宽
192
+ else if (saleRule && !regionAllowedByRule(saleRule, shipToRegion))
193
+ sellable = { ok: false, reason: 'region_not_for_sale' }; // 商家不销往
194
+ }
195
+ // 目的区运费裁定(有模板时;免邮阈值仅回传数值供 UI 提示"满 X 免邮",实际是否免取决于买家券后货款,建单时定)
196
+ let resolvedShipping = null;
197
+ if (entries && shipToRegion) {
198
+ const r = resolveShipping(entries, shipToRegion);
199
+ resolvedShipping = { covered: r.covered, fee: r.covered ? r.fee : null, est_days: r.est_days, quote_required: !r.covered && quoteOk };
200
+ }
201
+ return void res.json({
202
+ product_id: prod.id,
203
+ region_required: !!entries || restricted, // S5:可售/合规限制也需买家指定目的地(与建单 SHIP_REGION_REQUIRED 一致)
204
+ template: entries,
205
+ source,
206
+ sellable, // S5:{ ok, reason: ok|region_required|product_restricted|region_not_for_sale|platform_policy_invalid }
207
+ sale_regions: saleRule, // S5:生效可售规则(商品??店铺){mode:'list',include:[...]} | {mode:'all',exclude:[...]} | null —— UI 据此在无运费模板时也能生成地区入口
208
+ resolved_shipping: resolvedShipping, // S5:目的区运费裁定(covered/fee/est_days/quote_required)| null(无模板或未选区)
209
+ free_shipping_threshold: freeThreshold, // S5:生效满额免邮阈值(UI 提示"满 X 免邮";实际免取决于建单时券后货款)
210
+ import_duty_terms: importDuty, // 'ddu'(买家到境自付关税/税)| 'ddp'(卖家已含)| null(未声明)
211
+ tax_included_lines: taxLines, // 价内已含税声明(如 [{region:'SG',label:'GST',rate_pct:9,kind:'included'}])
212
+ tax_disclosure: 'seller_declared_platform_no_collect', // S5:税费/进口责任均为卖家声明,平台不代收代缴(直付非托管)
213
+ quote_outside_template: !!entries && quoteOk, // 直付轨:模板外地区可询价(卖家报运费/时效,确认后付款)
214
+ note: entries
215
+ ? (quoteOk ? '下单须选择收货地区;模板内地区运费自动计入,模板外地区(直付)可先询价再付款。' : '下单须选择收货地区;运费按模板计入总额(快照)。未覆盖地区暂不可下单。')
216
+ : (restricted ? '该商品设有可售地区限制,请选择收货地区确认。' : '该商品不按地区计运费。'),
217
+ });
218
+ });
219
+ }
@@ -1,5 +1,6 @@
1
1
  import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
2
  import { snfSend, snfPullInbox, snfListInbox, snfAck, snfPendingCount, snfVerify, snfDesignate, snfGetDesignation, snfNack, snfListDeadLetter, snfRevive, } from '../../layer2-business/L2-7-snf/snf-engine.js';
3
+ import { isEligibleArbitrator } from '../arbitrator-lifecycle.js'; // 仲裁能力唯一源=active 白名单(verify 是争议证据面,不认 legacy role)
3
4
  export function registerSnfRoutes(app, deps) {
4
5
  const { db, auth } = deps;
5
6
  app.post('/api/snf/send', (req, res) => {
@@ -105,7 +106,9 @@ export function registerSnfRoutes(app, deps) {
105
106
  if (!r)
106
107
  return void res.status(404).json({ error: '消息不存在' });
107
108
  const uid = user.id;
108
- if (uid !== r.sender_id && uid !== r.recipient_id && user.role !== 'arbitrator' && user.role !== 'admin') {
109
+ // 仲裁员认 active 白名单(争议证据验签是仲裁工作面);legacy role 旁路移除 —— 否则 role-only/已吊销账号可按 id
110
+ // 探测任意用户间私信的存在性+签名有效性,而真·白名单仲裁员(role=buyer)反被 403。admin 保持原样。
111
+ if (uid !== r.sender_id && uid !== r.recipient_id && !isEligibleArbitrator(db, uid).ok && user.role !== 'admin') {
109
112
  return void res.status(403).json({ error: '无权验证' });
110
113
  }
111
114
  res.json(await snfVerify(db, req.params.id));
@@ -74,13 +74,13 @@ export function registerWebauthnRoutes(app, deps) {
74
74
  if (!user)
75
75
  return;
76
76
  const purpose = String(req.body?.purpose || '').trim();
77
- const allowed = new Set(['withdraw', 'change-password', 'reveal-key', 'region', 'delete_passkey', 'governance_apply', 'governance_activate', 'governance_resign', 'governance_appeal_resolve', 'rewards_apply', 'rewards_deactivate', 'identity_claim', 'operator_claim_unlink', 'direct_pay_disclosure_ack', 'direct_pay_order_action', 'direct_receive_production_confirm', 'direct_pay_aml_review', 'direct_pay_kyb_ingress', 'direct_pay_sanctions_ingress', 'direct_pay_aml_ingress', 'direct_pay_admin_readiness', 'direct_pay_deferral_approve', 'direct_pay_deferral_reject', 'direct_pay_product_verify', 'direct_pay_store_verify', 'direct_pay_fee_prepay_record', 'direct_pay_fee_adjust', 'direct_pay_fee_refund', 'direct_receive_account_manage']);
77
+ const allowed = new Set(['withdraw', 'change-password', 'reveal-key', 'region', 'delete_passkey', 'governance_apply', 'governance_activate', 'governance_resign', 'governance_appeal_resolve', 'rewards_apply', 'rewards_deactivate', 'identity_claim', 'operator_claim_unlink', 'direct_pay_disclosure_ack', 'direct_pay_order_action', 'direct_receive_production_confirm', 'direct_receive_bond_refund', 'direct_pay_bond_slash', 'direct_pay_aml_review', 'direct_pay_kyb_ingress', 'direct_pay_sanctions_ingress', 'direct_pay_aml_ingress', 'direct_pay_admin_readiness', 'direct_pay_deferral_approve', 'direct_pay_deferral_reject', 'direct_pay_product_verify', 'direct_pay_store_verify', 'direct_pay_fee_prepay_record', 'direct_pay_fee_adjust', 'direct_pay_fee_refund', 'direct_receive_account_manage', 'platform_receive_account_manage', 'direct_pay_fee_prepay_reject', 'direct_pay_fee_prepay_request_approve', 'direct_pay_payment_info_reveal', 'arbitrator_grant', 'arbitrator_suspend', 'arbitrator_reinstate', 'arbitrator_revoke', 'arbitrate', 'agent_pair_approve', 'agent_permission_approve']);
78
78
  if (!allowed.has(purpose))
79
79
  return void res.status(400).json({ error: 'invalid purpose' });
80
80
  const purpose_data = req.body?.purpose_data ?? null;
81
81
  const creds = await dbAll('SELECT id, transports FROM webauthn_credentials WHERE user_id = ?', [user.id]);
82
82
  if (creds.length === 0)
83
- return void res.status(403).json({ error: '尚未注册任何 Passkey' });
83
+ return void res.status(403).json({ error: '尚未注册任何 Passkey', error_code: 'NO_PASSKEY_REGISTERED' }); // 前端据此才提示注册;已注册者的取消/设备失败不误导去注册
84
84
  const opts = await generateAuthenticationOptions({
85
85
  rpID: rpId,
86
86
  // H-1: 闸门必须真正 UV,不接受 "硬件按一下" 兜底
@@ -196,5 +196,5 @@ export function registerWebauthnRoutes(app, deps) {
196
196
  // RFC-020 PR-B — agent delegation grants (issue/read/revoke). Co-registered with the
197
197
  // Passkey security routes so the money-dense server.ts stays untouched. Safe scopes
198
198
  // only; risk scopes default-hard-reject. Reuses db/auth/generateId from WebauthnDeps.
199
- registerAgentGrantsRoutes(app, { db, auth, generateId, rateLimitOk });
199
+ registerAgentGrantsRoutes(app, { db, auth, generateId, rateLimitOk, requireHumanPresence, createProductDraftHandler: deps.createProductDraftHandler });
200
200
  }