@seasonkoh/webaz 0.1.28 → 0.1.30

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 (198) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +7 -6
  3. package/dist/bond-refund-blockers.js +50 -0
  4. package/dist/bond-slash.js +100 -0
  5. package/dist/bond-terms.js +31 -0
  6. package/dist/currency.js +16 -0
  7. package/dist/deposit-rails.js +52 -0
  8. package/dist/direct-pay-aml-monitor.js +67 -0
  9. package/dist/direct-pay-aml-review.js +40 -0
  10. package/dist/direct-pay-base-bond-entry.js +5 -0
  11. package/dist/direct-pay-bond-rail-clearance.js +99 -0
  12. package/dist/direct-pay-cancel-refund.js +160 -0
  13. package/dist/direct-pay-compliance-ingress.js +145 -0
  14. package/dist/direct-pay-controls.js +136 -0
  15. package/dist/direct-pay-create.js +231 -0
  16. package/dist/direct-pay-deferral-quota.js +43 -0
  17. package/dist/direct-pay-disclosures.js +50 -0
  18. package/dist/direct-pay-eligibility.js +46 -0
  19. package/dist/direct-pay-fee-ar.js +241 -0
  20. package/dist/direct-pay-fee-prepay-request.js +80 -0
  21. package/dist/direct-pay-launch-readiness.js +108 -0
  22. package/dist/direct-pay-launch-summary.js +68 -0
  23. package/dist/direct-pay-ledger.js +94 -0
  24. package/dist/direct-pay-returns.js +104 -0
  25. package/dist/direct-pay-stock.js +9 -0
  26. package/dist/direct-receive-account-qr.js +77 -0
  27. package/dist/direct-receive-accounts.js +82 -0
  28. package/dist/direct-receive-deferral.js +171 -0
  29. package/dist/direct-receive-deposits.js +365 -0
  30. package/dist/direct-receive-payment-instruction.js +26 -0
  31. package/dist/free-shipping.js +37 -0
  32. package/dist/fx-rates.js +78 -0
  33. package/dist/layer0-foundation/L0-1-database/schema.js +777 -1
  34. package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
  35. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
  36. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +114 -0
  37. package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
  38. package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
  39. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +11 -0
  40. package/dist/layer1-agent/L1-1-mcp-server/server.js +99 -32
  41. package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
  42. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
  43. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
  44. package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
  45. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
  46. package/dist/ledger.js +12 -3
  47. package/dist/mcp.js +23 -4
  48. package/dist/merchant-bond-domain.js +64 -0
  49. package/dist/merchant-bond-exposure.js +78 -0
  50. package/dist/merchant-bond-watcher.js +26 -0
  51. package/dist/payment-rails.js +29 -0
  52. package/dist/platform-receive-accounts.js +94 -0
  53. package/dist/product-verification.js +93 -0
  54. package/dist/pwa/acp-feed.js +3 -2
  55. package/dist/pwa/arbitration-read-admin.js +37 -0
  56. package/dist/pwa/arbitrator-lifecycle.js +100 -0
  57. package/dist/pwa/contract-fingerprint.js +18 -0
  58. package/dist/pwa/direct-pay-guards.js +20 -0
  59. package/dist/pwa/direct-pay-order-redaction.js +42 -0
  60. package/dist/pwa/endpoint-actions.js +3 -0
  61. package/dist/pwa/human-presence.js +2 -6
  62. package/dist/pwa/public/app-account.js +9 -9
  63. package/dist/pwa/public/app-admin-disputes.js +55 -0
  64. package/dist/pwa/public/app-agent-appeal.js +90 -0
  65. package/dist/pwa/public/app-agent-approvals.js +93 -0
  66. package/dist/pwa/public/app-agent-pair.js +127 -0
  67. package/dist/pwa/public/app-ai.js +10 -10
  68. package/dist/pwa/public/app-arbitrator-admin.js +87 -0
  69. package/dist/pwa/public/app-arbitrator-entry.js +9 -0
  70. package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
  71. package/dist/pwa/public/app-bond-refund-ui.js +66 -0
  72. package/dist/pwa/public/app-bond-slash-ui.js +74 -0
  73. package/dist/pwa/public/app-bond-terms-ui.js +23 -0
  74. package/dist/pwa/public/app-bond-ui.js +108 -0
  75. package/dist/pwa/public/app-chat-poll.js +29 -0
  76. package/dist/pwa/public/app-contribution-hub.js +23 -0
  77. package/dist/pwa/public/app-create-kinds.js +17 -0
  78. package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
  79. package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
  80. package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
  81. package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
  82. package/dist/pwa/public/app-direct-pay-copy.js +12 -0
  83. package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
  84. package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
  85. package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
  86. package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
  87. package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
  88. package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
  89. package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
  90. package/dist/pwa/public/app-direct-pay-memo.js +14 -0
  91. package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
  92. package/dist/pwa/public/app-direct-pay-pay.js +15 -0
  93. package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
  94. package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
  95. package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
  96. package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
  97. package/dist/pwa/public/app-direct-pay-returns.js +56 -0
  98. package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
  99. package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
  100. package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
  101. package/dist/pwa/public/app-direct-pay.js +226 -0
  102. package/dist/pwa/public/app-discover.js +20 -20
  103. package/dist/pwa/public/app-dispute-close-ui.js +38 -0
  104. package/dist/pwa/public/app-external-links.js +32 -0
  105. package/dist/pwa/public/app-free-shipping-ui.js +29 -0
  106. package/dist/pwa/public/app-gmv-rail-split.js +12 -0
  107. package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
  108. package/dist/pwa/public/app-listings.js +4 -4
  109. package/dist/pwa/public/app-mutual-cancel.js +54 -0
  110. package/dist/pwa/public/app-notif-templates-orders.js +43 -0
  111. package/dist/pwa/public/app-notif-templates.js +22 -0
  112. package/dist/pwa/public/app-order-accept-ui.js +158 -0
  113. package/dist/pwa/public/app-order-errors.js +50 -0
  114. package/dist/pwa/public/app-order-labels.js +19 -0
  115. package/dist/pwa/public/app-order-rail-filter.js +26 -0
  116. package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
  117. package/dist/pwa/public/app-poll-governor.js +22 -0
  118. package/dist/pwa/public/app-prelaunch-waz.js +39 -0
  119. package/dist/pwa/public/app-price.js +55 -0
  120. package/dist/pwa/public/app-product-media.js +15 -0
  121. package/dist/pwa/public/app-profile.js +10 -10
  122. package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
  123. package/dist/pwa/public/app-sale-regions-ui.js +38 -0
  124. package/dist/pwa/public/app-seller.js +5 -5
  125. package/dist/pwa/public/app-shop.js +19 -19
  126. package/dist/pwa/public/app-trade-tax-ui.js +33 -0
  127. package/dist/pwa/public/app.js +264 -295
  128. package/dist/pwa/public/i18n.js +1068 -197
  129. package/dist/pwa/public/index.html +58 -0
  130. package/dist/pwa/public/openapi.json +1845 -653
  131. package/dist/pwa/public/style.css +3 -0
  132. package/dist/pwa/routes/admin-analytics.js +4 -2
  133. package/dist/pwa/routes/admin-direct-receive-deposits.js +533 -0
  134. package/dist/pwa/routes/admin-protocol-params.js +16 -0
  135. package/dist/pwa/routes/admin-reports.js +31 -5
  136. package/dist/pwa/routes/agent-governance.js +1 -1
  137. package/dist/pwa/routes/agent-grants.js +253 -32
  138. package/dist/pwa/routes/analytics.js +2 -0
  139. package/dist/pwa/routes/arbitrator.js +67 -14
  140. package/dist/pwa/routes/bond-seller.js +162 -0
  141. package/dist/pwa/routes/buyer-feeds.js +2 -1
  142. package/dist/pwa/routes/chat.js +6 -1
  143. package/dist/pwa/routes/dashboards.js +2 -1
  144. package/dist/pwa/routes/direct-pay-availability.js +196 -0
  145. package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
  146. package/dist/pwa/routes/direct-pay-disclosure-acks.js +79 -0
  147. package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
  148. package/dist/pwa/routes/direct-pay-returns.js +74 -0
  149. package/dist/pwa/routes/direct-pay-timeouts.js +248 -0
  150. package/dist/pwa/routes/direct-receive-accounts.js +155 -0
  151. package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
  152. package/dist/pwa/routes/disputes-read.js +20 -6
  153. package/dist/pwa/routes/disputes-write.js +91 -33
  154. package/dist/pwa/routes/external-anchors.js +4 -2
  155. package/dist/pwa/routes/fee-prepay-requests.js +66 -0
  156. package/dist/pwa/routes/fx.js +12 -0
  157. package/dist/pwa/routes/governance-onboarding.js +46 -8
  158. package/dist/pwa/routes/leaderboard.js +47 -9
  159. package/dist/pwa/routes/listings.js +2 -2
  160. package/dist/pwa/routes/logistics.js +6 -2
  161. package/dist/pwa/routes/manifests.js +38 -0
  162. package/dist/pwa/routes/me-data.js +5 -2
  163. package/dist/pwa/routes/mutual-cancel.js +62 -0
  164. package/dist/pwa/routes/orders-action.js +297 -16
  165. package/dist/pwa/routes/orders-create.js +21 -6
  166. package/dist/pwa/routes/orders-read.js +106 -8
  167. package/dist/pwa/routes/p2p-products.js +2 -2
  168. package/dist/pwa/routes/platform-receive-accounts.js +111 -0
  169. package/dist/pwa/routes/products-create.js +15 -4
  170. package/dist/pwa/routes/products-links.js +34 -0
  171. package/dist/pwa/routes/products-list.js +2 -1
  172. package/dist/pwa/routes/products-update.js +37 -3
  173. package/dist/pwa/routes/profile-identity.js +4 -2
  174. package/dist/pwa/routes/promoter.js +3 -0
  175. package/dist/pwa/routes/referral.js +4 -0
  176. package/dist/pwa/routes/returns.js +60 -4
  177. package/dist/pwa/routes/rewards-apply.js +10 -5
  178. package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
  179. package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
  180. package/dist/pwa/routes/seller-directpay-report.js +110 -0
  181. package/dist/pwa/routes/seller-quota.js +5 -1
  182. package/dist/pwa/routes/shipping-templates.js +219 -0
  183. package/dist/pwa/routes/shops.js +2 -1
  184. package/dist/pwa/routes/snf.js +4 -1
  185. package/dist/pwa/routes/url-claim.js +2 -2
  186. package/dist/pwa/routes/users-public.js +8 -0
  187. package/dist/pwa/routes/wallet-read.js +3 -0
  188. package/dist/pwa/routes/webauthn.js +3 -3
  189. package/dist/pwa/server.js +112 -132
  190. package/dist/runtime/agent-grant-scopes.js +69 -1
  191. package/dist/runtime/webaz-schema-helpers.js +161 -3
  192. package/dist/sale-regions.js +116 -0
  193. package/dist/shipping-templates.js +119 -0
  194. package/dist/store-verification.js +77 -0
  195. package/dist/trade-tax.js +99 -0
  196. package/dist/trade-terms.js +76 -0
  197. package/dist/version.js +1 -1
  198. package/package.json +128 -2
@@ -1,12 +1,27 @@
1
1
  // RFC-011 §⑥ 事件游标流(纯 db 函数,party-gated)
2
2
  import { listOrderEventsSince } from '../../layer0-foundation/L0-2-state-machine/order-chain.js';
3
3
  import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
4
+ import { requireBothDisclosuresAcked } from '../../direct-pay-disclosures.js'; // PR-4f-b: direct_p2p 收款说明响应契约门
5
+ import { projectDirectPayTargetForViewer } from '../direct-pay-order-redaction.js'; // 收款目标披露门:按查看者一次分派(买家=ack 门/卖家=收款方保留/第三方=剥离),所有 orders reader 必过
6
+ import { getMutualCancelState } from '../../layer3-trust/L3-1-dispute-engine/mutual-cancel.js'; // 协商取消(无责合意)可达性 + 当前提议(仅 disputed 计算,UI 便利字段)
7
+ import { getCancelRefundState } from '../../direct-pay-cancel-refund.js'; // 直付取消退款握手状态(仅 direct_p2p+accepted 计算,UI 便利字段)
8
+ import { isEligibleArbitrator } from '../arbitrator-lifecycle.js'; // 白名单仲裁员可查【争议中】订单(裁定所需);不看 legacy role==='arbitrator'
9
+ import { getQrImageForOwner } from '../../direct-receive-account-qr.js'; // Rail1 D2:ack 门后按订单快照 qr_ref 取收款码字节((ref,seller_id) 域内)
10
+ import { readTradeTermsSnapshot } from '../../trade-terms.js'; // S0:下单冻结的交易条款(时效/退货/清关/税责),争议书面依据
4
11
  export function registerOrdersReadRoutes(app, deps) {
5
12
  const { db, auth, getOrderStatus, getOrderChain, verifyOrderChain, getOrderDispute } = deps;
13
+ // 仲裁员订单可见性(详情 + 签名链共用同一谓词,防两路漂移):active 白名单(唯一能力源,不看 legacy user.role)
14
+ // 且【该订单存在争议记录】—— 含已裁定/已驳回:裁定后订单离开 disputed,但仲裁员复盘/申诉处理自己的案件仍需可见,
15
+ // 与 disputes-read"任意 active 仲裁员可读任意争议(含已结)"同口径。无争议记录的订单一律不可见 → 不可枚举任意订单。
16
+ const arbitratorCanViewOrder = async (orderId, userId) => isEligibleArbitrator(db, userId).ok && !!(await dbOne('SELECT 1 FROM disputes WHERE order_id = ? LIMIT 1', [orderId]));
6
17
  app.get('/api/orders', async (req, res) => {
7
18
  const user = auth(req, res);
8
19
  if (!user)
9
20
  return;
21
+ // 可选按支付轨筛选(escrow=托管 / direct_p2p=直接收款);非法值忽略。参与方 OR 必须整体括起,否则 AND 优先级会绑错。
22
+ const rail = req.query.rail === 'direct_p2p' ? 'direct_p2p' : req.query.rail === 'escrow' ? 'escrow' : null;
23
+ const railClause = rail ? ' AND o.payment_rail = ?' : '';
24
+ const railParams = rail ? [rail] : [];
10
25
  const orders = await dbAll(`
11
26
  SELECT o.*, p.title as product_title, p.images,
12
27
  ub.name as buyer_name, us.name as seller_name
@@ -14,9 +29,9 @@ export function registerOrdersReadRoutes(app, deps) {
14
29
  JOIN products p ON o.product_id = p.id
15
30
  JOIN users ub ON o.buyer_id = ub.id
16
31
  JOIN users us ON o.seller_id = us.id
17
- WHERE o.buyer_id = ? OR o.seller_id = ? OR o.logistics_id = ?
32
+ WHERE (o.buyer_id = ? OR o.seller_id = ? OR o.logistics_id = ?)${railClause}
18
33
  ORDER BY o.created_at DESC LIMIT 50
19
- `, [user.id, user.id, user.id]);
34
+ `, [user.id, user.id, user.id, ...railParams]);
20
35
  // B2 隐私购物:列表里也做相同 mask(防 seller/logistics 通过列表绕过详情 mask)
21
36
  for (const o of orders) {
22
37
  if (Number(o.anonymous_recipient) === 1 && o.buyer_id !== user.id) {
@@ -25,6 +40,8 @@ export function registerOrdersReadRoutes(app, deps) {
25
40
  delete o.recipient_code;
26
41
  o.buyer_name = '🔒 ' + (typeof code === 'string' ? code : 'PR-?????');
27
42
  }
43
+ projectDirectPayTargetForViewer(db, o, user.id); // #179/#218 审计:收款目标按查看者投影(买家=ack 门/卖家保留/第三方剥离),列表不得旁路
44
+ delete o.trade_terms_snapshot; // S0:列表不下放条款快照原始串(详情页解析后下放,列表省流量)
28
45
  }
29
46
  res.json(orders);
30
47
  });
@@ -37,6 +54,7 @@ export function registerOrdersReadRoutes(app, deps) {
37
54
  const field = role === 'seller' ? 'o.seller_id' : 'o.buyer_id';
38
55
  const from = req.query.from ? String(req.query.from) : null;
39
56
  const to = req.query.to ? String(req.query.to) : null;
57
+ const rail = req.query.rail === 'direct_p2p' ? 'direct_p2p' : req.query.rail === 'escrow' ? 'escrow' : null;
40
58
  const where = [`${field} = ?`];
41
59
  const params = [user.id];
42
60
  if (from) {
@@ -47,10 +65,15 @@ export function registerOrdersReadRoutes(app, deps) {
47
65
  where.push(`o.created_at <= ?`);
48
66
  params.push(to);
49
67
  }
68
+ if (rail) {
69
+ where.push(`o.payment_rail = ?`);
70
+ params.push(rail);
71
+ } // 按支付轨导出(对账:托管 vs 直接收款分开)
50
72
  const EXPORT_LIMIT = 5000;
51
73
  const rows = await dbAll(`
52
74
  SELECT o.id, o.created_at, o.status, o.quantity, o.unit_price, o.total_amount,
53
75
  o.coupon_discount, o.variant_options_snapshot, o.shipping_address,
76
+ COALESCE(o.payment_rail, 'escrow') as payment_rail,
54
77
  p.title as product_title, p.category,
55
78
  ub.handle as buyer_handle, ub.name as buyer_name,
56
79
  us.handle as seller_handle, us.name as seller_name
@@ -72,7 +95,7 @@ export function registerOrdersReadRoutes(app, deps) {
72
95
  }
73
96
  return s;
74
97
  };
75
- const headers = ['order_id', 'created_at', 'status', 'product', 'category', 'qty', 'unit_price', 'total', 'coupon_discount', 'variant', 'buyer', 'seller', 'address'];
98
+ const headers = ['order_id', 'created_at', 'status', 'payment_rail', 'product', 'category', 'qty', 'unit_price', 'total', 'coupon_discount', 'variant', 'buyer', 'seller', 'address'];
76
99
  const lines = [headers.join(',')];
77
100
  for (const r of rows) {
78
101
  let variantStr = '';
@@ -83,7 +106,7 @@ export function registerOrdersReadRoutes(app, deps) {
83
106
  }
84
107
  catch { }
85
108
  lines.push([
86
- r.id, r.created_at, r.status, r.product_title, r.category,
109
+ r.id, r.created_at, r.status, r.payment_rail, r.product_title, r.category,
87
110
  r.quantity, r.unit_price, r.total_amount, r.coupon_discount || 0,
88
111
  variantStr, r.buyer_handle, r.seller_handle, r.shipping_address,
89
112
  ].map(csvEscape).join(','));
@@ -99,7 +122,7 @@ export function registerOrdersReadRoutes(app, deps) {
99
122
  // UTF-8 BOM 帮 Excel 识别
100
123
  res.send('' + lines.join('\n'));
101
124
  });
102
- // 订单签名链 — 当事人 + arbitrator + admin 可查
125
+ // 订单签名链 — 当事人 + 白名单仲裁员(涉争议订单) + admin 可查
103
126
  app.get('/api/orders/:id/chain', async (req, res) => {
104
127
  const user = auth(req, res);
105
128
  if (!user)
@@ -108,9 +131,12 @@ export function registerOrdersReadRoutes(app, deps) {
108
131
  if (!order)
109
132
  return void res.status(404).json({ error: '订单不存在' });
110
133
  const uid = user.id;
111
- const isParty = uid === order.buyer_id || uid === order.seller_id || uid === order.logistics_id || user.role === 'arbitrator' || user.role === 'admin';
112
- if (!isParty)
134
+ // 仲裁员链访问与订单详情同一谓词(arbitratorCanViewOrder):裁定需验签名链;legacy role==='arbitrator' 旁路已移除
135
+ // (否则 role-only/已吊销账号可读任意订单链,而真·白名单仲裁员 role=buyer 反被 403,链徽标在裁定页显示"链异常")
136
+ const isParty = uid === order.buyer_id || uid === order.seller_id || uid === order.logistics_id;
137
+ if (!isParty && user.role !== 'admin' && !(await arbitratorCanViewOrder(req.params.id, uid))) {
113
138
  return void res.status(403).json({ error: '无权查看此订单链' });
139
+ }
114
140
  const chain = await getOrderChain(db, req.params.id);
115
141
  const verification = await verifyOrderChain(db, req.params.id);
116
142
  res.json({ chain, verification });
@@ -141,7 +167,12 @@ export function registerOrdersReadRoutes(app, deps) {
141
167
  const order = statusInfo.order;
142
168
  const isLogisticsPickup = user.role === 'logistics' &&
143
169
  !order.logistics_id && order.status === 'shipped';
144
- if (order.buyer_id !== user.id && order.seller_id !== user.id && order.logistics_id !== user.id && user.role !== 'arbitrator' && !isLogisticsPickup) {
170
+ // 白名单仲裁员(role 可能是 buyer,非 legacy 'arbitrator')需查看涉争议订单以裁定/复盘。能力源【唯一】= active
171
+ // arbitrator_whitelist;legacy role 旁路已移除(否则 role-only/已吊销账号可读任意订单)。谓词= arbitratorCanViewOrder:
172
+ // 订单须【存在争议记录】(含已裁定/已驳回 —— 只放行 disputed 会让仲裁员在裁定落地、订单离开 disputed 的瞬间失去访问,
173
+ // 已结 tab 的"查看订单"全部 403);无争议记录的订单不可见 → 不可枚举。
174
+ const isParty = order.buyer_id === user.id || order.seller_id === user.id || order.logistics_id === user.id;
175
+ if (!isParty && !isLogisticsPickup && !(await arbitratorCanViewOrder(req.params.id, user.id))) {
145
176
  return void res.status(403).json({ error: '无权查看此订单' });
146
177
  }
147
178
  // M8: 二手订单从 secondhand_items 查;商家订单从 products 查
@@ -194,6 +225,73 @@ export function registerOrdersReadRoutes(app, deps) {
194
225
  delete order.recipient_code;
195
226
  }
196
227
  }
228
+ // Direct Pay 响应契约门:收款目标按查看者投影 —— 买家在 D1/D2 both-acked 前不得拿到(非仅 UI 软门);卖家=收款方保留;
229
+ // 非买卖双方第三方(disputed-order 仲裁员、logistics-pickup)一律剥离(#179/#218 审计;仲裁若确需看收款目标,
230
+ // 应另做显式、可审计、受指派/案件/目的约束的 reveal 路径)。与 /api/orders 列表共用同一投影器,防旁路。
231
+ projectDirectPayTargetForViewer(db, order, user.id);
232
+ // Rail1 撤回仲裁可达性(与 orders-action pq_withdraw 权威门同谓词):仅当【当前 disputed + 最近一次进入
233
+ // disputed 是 from payment_query + 争议未裁定】。前端据此才显示"撤回仲裁"按钮 —— 履约类争议(货损/货不对版
234
+ // 的 delivered→disputed)不给该按钮。UI 便利字段,真正拦截仍在路由。
235
+ const disputedFroms = history.filter(h => h.to_status === 'disputed').map(h => h.from_status);
236
+ order.can_withdraw_payment_query_dispute =
237
+ order.status === 'disputed' && disputedFroms[disputedFroms.length - 1] === 'payment_query' && !!dispute;
238
+ // 争议协商收口·买家侧(与 orders-action dispute_withdraw_confirm 权威门同谓词):仅当【当前 disputed +
239
+ // 最近一次进入 disputed 是 from delivered(履约争议)+ 争议未裁定 + 查看者=买家=争议发起人】。
240
+ // 前端据此才显示"我已收到货·撤诉并确认收货";UI 便利字段,真正拦截仍在路由。
241
+ order.can_confirm_receipt_close_dispute =
242
+ order.status === 'disputed' && disputedFroms[disputedFroms.length - 1] === 'delivered'
243
+ && !!dispute && dispute.initiator_id === user.id && order.buyer_id === user.id;
244
+ // 协商取消(无责·双方合意)握手状态 —— 仅 disputed 单计算,供订单页同步渲染 propose/accept/decline/withdraw。真正边界在 mutual-cancel 路由。
245
+ if (order.status === 'disputed') {
246
+ const mc = getMutualCancelState(db, req.params.id, user.id);
247
+ order.mutual_cancel = mc.ok ? { proposal: mc.proposal ?? null, can_propose: !!mc.can_propose, can_accept: !!mc.can_accept, can_decline: !!mc.can_decline, can_withdraw: !!mc.can_withdraw } : null;
248
+ }
249
+ // 直付取消退款握手状态(审计项 C)—— 仅 direct_p2p + accepted(付款后·发货前)计算,供订单页同步渲染。
250
+ // party-gated(域内);非当事方(仲裁员等)拿 null。真正边界在 direct-pay-cancel-refund 路由。
251
+ if (order.payment_rail === 'direct_p2p' && order.status === 'accepted') {
252
+ const cr = getCancelRefundState(db, req.params.id, user.id);
253
+ order.cancel_refund = cr.ok ? { request: cr.request ?? null, can_request: !!cr.can_request, can_respond: !!cr.can_respond, can_confirm: !!cr.can_confirm, can_withdraw: !!cr.can_withdraw } : null;
254
+ // 审计项 F:卖家对账辅助 —— 同买家·同金额其它在途直付单计数(与 mark_paid D2 预警同口径,发货前每次打开订单页都能看到)。
255
+ // 仅卖家视角计算;买家/第三方不下发(无对账用途,少一分敞口)。
256
+ if (order.seller_id === user.id) {
257
+ const dupRow = await dbOne(`SELECT COUNT(*) n FROM orders WHERE buyer_id = ? AND seller_id = ? AND payment_rail = 'direct_p2p' AND total_amount = ? AND id != ? AND status IN ('accepted','shipped','picked_up','in_transit','delivered')`, [order.buyer_id, order.seller_id, order.total_amount, req.params.id]);
258
+ order.duplicate_amount_alert = dupRow?.n || 0;
259
+ }
260
+ }
261
+ // S0 交易条款快照:parse-don't-validate 后下放(坏 JSON/pre-S0 旧单 → null;原始串不下放)
262
+ order.trade_terms = readTradeTermsSnapshot(order.trade_terms_snapshot);
263
+ delete order.trade_terms_snapshot;
197
264
  res.json({ ...statusInfo, history, product, dispute, trackingInfo });
198
265
  });
266
+ // Rail1 D2:直付订单收款二维码(硬化转发)。仅【订单买家】+ 两次披露 both-acked 后可取;按建单时快照的 (qr_ref, seller_id)
267
+ // 取【当时那一版】图字节。未 ack / 非买家 / 无 QR / 非 direct_p2p → 统一 404(不枚举,不泄露)。图字节不入 order JSON。
268
+ app.get('/api/orders/:id/direct-pay-qr', async (req, res) => {
269
+ const user = auth(req, res);
270
+ if (!user)
271
+ return;
272
+ const orderId = String(req.params.id);
273
+ const order = await dbOne('SELECT buyer_id, seller_id, payment_rail, status, direct_pay_account_snapshot FROM orders WHERE id = ?', [orderId]);
274
+ if (!order || order.payment_rail !== 'direct_p2p' || order.buyer_id !== user.id)
275
+ return void res.status(404).end();
276
+ if (order.status === 'pending_accept')
277
+ return void res.status(404).end(); // 手动接单待确认:状态门,接单前不出示收款码(v16)
278
+ if (!requireBothDisclosuresAcked(db, orderId).ok)
279
+ return void res.status(404).end(); // 未 ack:与"无 QR"同样 404,不泄露存在性
280
+ let snap = {};
281
+ try {
282
+ snap = order.direct_pay_account_snapshot ? JSON.parse(order.direct_pay_account_snapshot) : {};
283
+ }
284
+ catch {
285
+ snap = {};
286
+ }
287
+ if (!snap.qr_ref)
288
+ return void res.status(404).end();
289
+ const img = getQrImageForOwner(db, snap.qr_ref, order.seller_id); // (ref, seller_id) 域内取字节;seller_id 取自订单,非用户输入
290
+ if (!img)
291
+ return void res.status(404).end();
292
+ res.setHeader('Content-Type', img.mime);
293
+ res.setHeader('X-Content-Type-Options', 'nosniff');
294
+ res.setHeader('Cache-Control', 'private, no-store');
295
+ res.send(img.buf);
296
+ });
199
297
  }
@@ -51,8 +51,8 @@ export function registerP2pProductsRoutes(app, deps) {
51
51
  await dbRun(`
52
52
  INSERT INTO products (id, seller_id, title, description, price, stock, status, images, ship_regions,
53
53
  handling_hours, commission_rate, category_id, stake_amount, p2p_mode, content_hash, peer_endpoint,
54
- content_signature, content_signed_at)
55
- VALUES (?,?,?,?,?,?,'active',?,?,24,0.10,'cat_default',0,1,?,?,?,?)
54
+ content_signature, content_signed_at, currency)
55
+ VALUES (?,?,?,?,?,?,'active',?,?,24,0.10,'cat_default',0,1,?,?,?,?,'WAZ')
56
56
  `, [
57
57
  id, user.id, title,
58
58
  `[P2P] ${title}(完整详情见卖家节点)`,
@@ -0,0 +1,111 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { listPlatformAccounts, getPlatformAccount, addPlatformAccount, updatePlatformAccount, deactivatePlatformAccount } from '../../platform-receive-accounts.js';
3
+ import { requireDirectPayHumanPasskey } from '../direct-pay-guards.js';
4
+ const sha256hex = (s) => createHash('sha256').update(s).digest('hex');
5
+ /** 稳定序列化(键排序)—— purpose_data 与请求体重算 payload 逐字比对,不受键序影响。 */
6
+ function stable(o) {
7
+ return JSON.stringify(Object.keys(o).sort().reduce((a, k) => { a[k] = o[k]; return a; }, {}));
8
+ }
9
+ const GATE_PURPOSE = 'platform_receive_account_manage';
10
+ export function registerPlatformReceiveAccountsRoutes(app, deps) {
11
+ const { db, requireRootAdmin, generateId, consumeGateToken, logAdminAction } = deps;
12
+ /** 从请求体重算【本次写入内容】的 canonical gate payload —— Passkey token 必须绑定它,证明 admin 确认的就是这份收款内容。
13
+ * 绑定字段:action, account_id?, instruction/method/currency/label(原文,与 body 逐字一致), qr_mode(keep|clear|set)+qr_sha256(set 时;绑 QR 内容而非塞 64KB 原文)。 */
14
+ function gateContentPayload(req, action, accountId) {
15
+ const b = req.body || {};
16
+ const p = { action };
17
+ if (accountId)
18
+ p.account_id = accountId;
19
+ if (action === 'add' || action === 'update') {
20
+ p.instruction = String(b.instruction ?? '');
21
+ p.method = String(b.method ?? '');
22
+ p.currency = String(b.currency ?? '');
23
+ p.label = String(b.label ?? '');
24
+ if (!('qr_data_uri' in b))
25
+ p.qr_mode = 'keep';
26
+ else if (b.qr_data_uri == null || String(b.qr_data_uri).trim() === '')
27
+ p.qr_mode = 'clear';
28
+ else {
29
+ p.qr_mode = 'set';
30
+ p.qr_sha256 = sha256hex(String(b.qr_data_uri));
31
+ }
32
+ }
33
+ return p;
34
+ }
35
+ /** ROOT + 现场真人 Passkey(写操作)。token 的 purpose_data 必须与本次请求体重算的 canonical payload 逐字相等
36
+ * —— 否则(如用批 A 的 token 写 B 的收款地址,或 body 被篡改)拒。返回 true=通过(否则已写 403)。 */
37
+ function passkeyOk(req, res, adminId, action, accountId) {
38
+ const expected = stable(gateContentPayload(req, action, accountId));
39
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
40
+ userId: adminId, webauthnToken: req.body?.webauthn_token,
41
+ purpose: GATE_PURPOSE,
42
+ validate: (data) => !!data && typeof data === 'object' && stable(data) === expected,
43
+ });
44
+ if (!gate.ok) {
45
+ res.status(403).json({ error: gate.reason, error_code: gate.error_code });
46
+ return false;
47
+ }
48
+ return true;
49
+ }
50
+ const bodyInput = (req) => {
51
+ const b = req.body || {};
52
+ const out = { instruction: b.instruction, label: b.label, method: b.method, currency: b.currency };
53
+ if ('qr_data_uri' in b)
54
+ out.qrDataUri = b.qr_data_uri; // 缺省不改;给了(含 null/'')才动 qr
55
+ return out;
56
+ };
57
+ /** 审计摘要(canonical,不含 raw QR):便于事后还原"当时展示给卖家的收款内容"。instruction/qr 用 sha256。 */
58
+ const summarize = (a) => a == null ? null : { method: a.method ?? null, currency: a.currency ?? null, label: a.label ?? null, instruction_sha256: a.instruction ? sha256hex(a.instruction) : null, qr_sha256: a.qr_data_uri ? sha256hex(a.qr_data_uri) : null, had_qr: !!a.qr_data_uri };
59
+ // ── list(ROOT 读;含 inactive + qr_data_uri)──
60
+ app.get('/api/admin/platform-receive-accounts', (req, res) => {
61
+ const admin = requireRootAdmin(req, res);
62
+ if (!admin)
63
+ return;
64
+ res.json({ accounts: listPlatformAccounts(db, { includeInactive: true }) });
65
+ });
66
+ // ── add(ROOT + Passkey)──
67
+ app.post('/api/admin/platform-receive-accounts', (req, res) => {
68
+ const admin = requireRootAdmin(req, res);
69
+ if (!admin)
70
+ return;
71
+ if (!passkeyOk(req, res, admin.id, 'add'))
72
+ return;
73
+ const r = addPlatformAccount(db, bodyInput(req), generateId);
74
+ if (!r.ok)
75
+ return void res.status(400).json({ error: r.reason, error_code: 'PLATFORM_ACCOUNT_INPUT_INVALID' });
76
+ logAdminAction?.(admin.id, 'platform_receive_account_add', 'platform_receive_account', r.account.id, { new: summarize(r.account) });
77
+ res.json({ ok: true, account: r.account });
78
+ });
79
+ // ── update(ROOT + Passkey + 存在性)──
80
+ app.put('/api/admin/platform-receive-accounts/:id', (req, res) => {
81
+ const admin = requireRootAdmin(req, res);
82
+ if (!admin)
83
+ return;
84
+ const id = String(req.params.id);
85
+ const before = getPlatformAccount(db, id);
86
+ if (!before)
87
+ return void res.status(404).json({ error: '平台收款方式不存在', error_code: 'PLATFORM_ACCOUNT_NOT_FOUND' });
88
+ if (!passkeyOk(req, res, admin.id, 'update', id))
89
+ return;
90
+ const r = updatePlatformAccount(db, id, bodyInput(req));
91
+ if (!r.ok)
92
+ return void res.status(400).json({ error: r.reason, error_code: 'PLATFORM_ACCOUNT_INPUT_INVALID' });
93
+ logAdminAction?.(admin.id, 'platform_receive_account_update', 'platform_receive_account', id, { old: summarize(before), new: summarize(getPlatformAccount(db, id)) });
94
+ res.json({ ok: true, changed: r.changed });
95
+ });
96
+ // ── deactivate(ROOT + Passkey + 存在性)──
97
+ app.delete('/api/admin/platform-receive-accounts/:id', (req, res) => {
98
+ const admin = requireRootAdmin(req, res);
99
+ if (!admin)
100
+ return;
101
+ const id = String(req.params.id);
102
+ const before = getPlatformAccount(db, id);
103
+ if (!before)
104
+ return void res.status(404).json({ error: '平台收款方式不存在', error_code: 'PLATFORM_ACCOUNT_NOT_FOUND' });
105
+ if (!passkeyOk(req, res, admin.id, 'deactivate', id))
106
+ return;
107
+ const changed = deactivatePlatformAccount(db, id);
108
+ logAdminAction?.(admin.id, 'platform_receive_account_deactivate', 'platform_receive_account', id, { old: summarize(before) });
109
+ res.json({ ok: true, changed });
110
+ });
111
+ }
@@ -23,9 +23,10 @@ export function registerProductsCreateRoutes(app, deps) {
23
23
  const quotaCheck = checkSellerCanList(user);
24
24
  if (!quotaCheck.ok)
25
25
  return void res.json({ error: quotaCheck.reason });
26
- const { title, description, price, stock = 1, category = '', specs, brand, model, source_url, source_price, external_title, weight_kg, ship_regions = '全国', handling_hours = 24, estimated_days, fragile = 0, return_days = 7, return_condition = '', warranty_days = 0, commission_rate, product_type = 'retail', // 里程碑 6
26
+ const { title, description, price, stock = 1, category = '', specs, brand, model, source_url, source_price, external_title, weight_kg, ship_regions = '全国', handling_hours = 24, estimated_days, fragile = 0, return_days = 7, return_condition = '', warranty_days = 0, low_stock_threshold = 3, auto_delist_on_zero = 1, commission_rate, product_type = 'retail', create_status, // 里程碑 6 / S4:create_status='warehouse' 让含单品覆盖的新品先落仓库,覆盖全落定再激活(避免公开 active 但缺可售/税费声明)
27
27
  aliases = [], // 里程碑 7.2:上架时同步声明的 alias 集合
28
28
  image_hashes = [], // 商品图片 — 只存 hash(64 hex),实际 blob 在卖家节点 IDB
29
+ package_size, origin_country, country_of_origin, customs_description, hs_code, // S0 跨境清关/物流证据字段(全可选,零计费逻辑;进条款快照)
29
30
  } = req.body;
30
31
  // 协议化原则:服务端只存 hash 引用,不存图片字节。校验仅做格式 + 数量。
31
32
  let imagesJsonForInsert = null;
@@ -69,6 +70,12 @@ export function registerProductsCreateRoutes(app, deps) {
69
70
  const priceNum = Number(price);
70
71
  const stakeDiscount = await getStakeDiscount(db, user.id);
71
72
  const stakeRate = Math.max(0.05, 0.15 - stakeDiscount);
73
+ // S0 清关字段轻校验(可选;长度界 + 区码大写 + HS 编码字符集)
74
+ const _cc = (x) => (typeof x === 'string' && x.trim()) ? x.trim().toUpperCase().slice(0, 8) : null;
75
+ const _tx = (x, n) => (typeof x === 'string' && x.trim()) ? x.trim().slice(0, n) : null;
76
+ const _hs = _tx(hs_code, 12);
77
+ if (_hs && !/^[0-9.]{4,12}$/.test(_hs))
78
+ return void res.status(400).json({ error: 'hs_code 须为 4-12 位数字(可含 .)', error_code: 'INVALID_HS_CODE' });
72
79
  const stakeAmount = Math.round(priceNum * stakeRate * 100) / 100;
73
80
  const now = new Date().toISOString();
74
81
  const id = generateId('prd');
@@ -80,17 +87,21 @@ export function registerProductsCreateRoutes(app, deps) {
80
87
  specs, brand, model, source_url, source_price, source_price_at,
81
88
  weight_kg, ship_regions, handling_hours, estimated_days, fragile,
82
89
  return_days, return_condition, warranty_days,
90
+ low_stock_threshold, auto_delist_on_zero,
83
91
  commitment_hash, description_hash, price_hash, hashed_at,
84
- commission_rate, product_type, images
85
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, [
92
+ commission_rate, product_type, images, currency,
93
+ package_size, origin_country, country_of_origin, customs_description, hs_code, status
94
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'WAZ',?,?,?,?,?,?)`, [
86
95
  id, user.id, title, description, priceNum, Number(stock), category, stakeAmount,
87
96
  specsJson, brand ?? null, model ?? null,
88
97
  source_url ?? null, source_price ? Number(source_price) : null, source_price ? now : null,
89
98
  weight_kg ? Number(weight_kg) : null, ship_regions, Number(handling_hours), estJson, fragile ? 1 : 0,
90
99
  Number(return_days), return_condition, Number(warranty_days),
100
+ Math.max(0, Math.floor(Number(low_stock_threshold) || 0)), auto_delist_on_zero ? 1 : 0,
91
101
  makeCommitmentHash(pFields), makeDescriptionHash({ title, description, specs: specsJson }),
92
102
  makePriceHash(priceNum, now), now,
93
- commissionRateNum, product_type, imagesJsonForInsert
103
+ commissionRateNum, product_type, imagesJsonForInsert,
104
+ _tx(package_size, 40), _cc(origin_country), _cc(country_of_origin), _tx(customs_description, 120), _hs, create_status === 'warehouse' ? 'warehouse' : 'active'
94
105
  ]);
95
106
  // M7.2.6:免质押上架 — 不再扣 stake;首单成交时 settleOrder 自动从订单 escrow 锁定
96
107
  // M7.2-6: 上架时同步入 aliases(卖家已勾选的 candidates)
@@ -1,4 +1,5 @@
1
1
  import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { invalidateProductVerification } from '../../product-verification.js';
2
3
  export function registerProductsLinksRoutes(app, deps) {
3
4
  // 只读/单写站点走 RFC-016 异步 seam;db 保留:认领冲突分支是 fee-lock 资金路径,
4
5
  // INSERT 链接 + INSERT 验证任务 + 钱包扣费必须原子(db.transaction + 守恒/dup guard),Phase 3 迁 pg 行锁。
@@ -15,6 +16,21 @@ export function registerProductsLinksRoutes(app, deps) {
15
16
  const links = await dbAll(`SELECT id, url, source, verified, revoked, verify_note, added_at, platform, external_id, external_title FROM product_external_links WHERE product_id = ? ORDER BY added_at ASC`, [req.params.id]);
16
17
  res.json(links);
17
18
  });
19
+ // Buyer-facing read for the product detail page: VERIFIED + non-revoked links only, shape { links }.
20
+ // NOT owner-gated (any logged-in viewer) — separate from the owner-only /links above (which returns a bare
21
+ // array the seller workbench consumes; that stays untouched). Only public-safe columns are returned.
22
+ app.get('/api/products/:id/external-links', async (req, res) => {
23
+ const user = auth(req, res);
24
+ if (!user)
25
+ return;
26
+ // Mirror product-detail visibility (products-crud GET /:id): others see active only; the seller sees own
27
+ // non-active. Otherwise 404 — never leak source URLs of a warehouse/deleted product to non-owners.
28
+ const product = await dbOne("SELECT id FROM products WHERE id = ? AND (status = 'active' OR seller_id = ?)", [req.params.id, user.id]);
29
+ if (!product)
30
+ return void res.status(404).json({ error: 'not_found' });
31
+ const links = await dbAll(`SELECT url, platform, external_title FROM product_external_links WHERE product_id = ? AND verified = 1 AND revoked = 0 ORDER BY added_at ASC`, [req.params.id]);
32
+ res.json({ links });
33
+ });
18
34
  // 新链接(无人认领)直接 verified=1;已被他人认领则发起众包验证任务
19
35
  app.post('/api/products/:id/links', async (req, res) => {
20
36
  const user = auth(req, res);
@@ -69,6 +85,12 @@ export function registerProductsLinksRoutes(app, deps) {
69
85
  await dbRun(`INSERT INTO product_external_links
70
86
  (id, product_id, url, source, verified, verified_at, platform, external_id, external_title)
71
87
  VALUES (?, ?, ?, 'manual', 1, datetime('now'), ?, ?, ?)`, [linkId, req.params.id, url, meta?.platform ?? null, meta?.external_id ?? null, linkExternalTitle]);
88
+ try {
89
+ invalidateProductVerification(db, String(req.params.id));
90
+ }
91
+ catch (e) {
92
+ console.error('[product-verify invalidate]', e);
93
+ } // PR-⑥ 反作弊:外链变更 → 作废直付逐品验证
72
94
  return void res.json({ link_id: linkId, verified: 1, external_title: linkExternalTitle, message: '链接已关联' });
73
95
  }
74
96
  // 已被他人认领:发起众包验证任务
@@ -129,6 +151,12 @@ export function registerProductsLinksRoutes(app, deps) {
129
151
  console.error('[products-links claim tx]', msg);
130
152
  return void res.status(500).json({ error: '发起认领失败,请重试' });
131
153
  }
154
+ try {
155
+ invalidateProductVerification(db, String(req.params.id));
156
+ }
157
+ catch (e) {
158
+ console.error('[product-verify invalidate]', e);
159
+ } // PR-⑥ 反作弊:外链变更 → 作废直付逐品验证
132
160
  res.json({
133
161
  link_id: linkId,
134
162
  task_id: taskId,
@@ -147,6 +175,12 @@ export function registerProductsLinksRoutes(app, deps) {
147
175
  if (!product || product.seller_id !== user.id)
148
176
  return void res.status(403).json({ error: '无权限' });
149
177
  await dbRun('DELETE FROM product_external_links WHERE id = ? AND product_id = ?', [req.params.linkId, req.params.id]);
178
+ try {
179
+ invalidateProductVerification(db, String(req.params.id));
180
+ }
181
+ catch (e) {
182
+ console.error('[product-verify invalidate]', e);
183
+ } // PR-⑥ 反作弊:外链变更 → 作废直付逐品验证
150
184
  res.json({ success: true });
151
185
  });
152
186
  }
@@ -1,5 +1,6 @@
1
1
  import { createHmac } from 'crypto';
2
2
  import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
3
+ import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // RFC-018 PR4: 真实成交(排除全额退货)
3
4
  export function registerProductsListRoutes(app, deps) {
4
5
  // db 已走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
5
6
  const { getUser, VALID_PRODUCT_TYPES, RAW_MODE_MIN_TRUST, getAgentTrustCached, VALID_SORTS, PRODUCT_LIMITS, TRENDING_SCORE_EXPR, findProductsByAlias, decodeProductCursor, encodeProductCursor, MASTER_SEED, formatProductForAgent } = deps;
@@ -52,7 +53,7 @@ export function registerProductsListRoutes(app, deps) {
52
53
  COALESCE(rs.total_points, 0) as rep_points, COALESCE(rs.level, 'new') as rep_level,
53
54
  COALESCE(rs.transactions_done, 0) as seller_tx_count,
54
55
  pc.seasonal_months as seasonal_months,
55
- (SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND o.status = 'completed') as sales_count,
56
+ (SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND ${genuineSalePredicate('o')}) as sales_count,
56
57
  (SELECT COUNT(DISTINCT buyer_id) FROM order_ratings r WHERE r.product_id = p.id AND r.stars >= 4) as recommend_count,
57
58
  (SELECT COUNT(*) FROM dispute_cases dc WHERE dc.seller_id = p.seller_id) as seller_dispute_count,
58
59
  -- 卖家仲裁胜率:无案件视为 0.8 中性(未经检验);有案件按真实比率
@@ -1,7 +1,8 @@
1
1
  import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { invalidateProductVerification } from '../../product-verification.js';
2
3
  export function registerProductsUpdateRoutes(app, deps) {
3
- // db 已走 RFC-016 异步 seam(dbOne/dbRun),不再直接用 deps.db
4
- const { auth, makeCommitmentHash, makeDescriptionHash, makePriceHash, notifyWaitlist, notifyWishlistPriceDrop, checkStockAndMaybeDelist } = deps;
4
+ // db 已走 RFC-016 异步 seam(dbOne/dbRun);deps.db 仅用于 direct-pay 逐品验证作废(sync helper,反作弊生命周期)
5
+ const { db, auth, makeCommitmentHash, makeDescriptionHash, makePriceHash, notifyWaitlist, notifyWishlistPriceDrop, checkStockAndMaybeDelist } = deps;
5
6
  app.put('/api/products/:id', async (req, res) => {
6
7
  const user = auth(req, res);
7
8
  if (!user)
@@ -9,7 +10,8 @@ export function registerProductsUpdateRoutes(app, deps) {
9
10
  const product = await dbOne('SELECT * FROM products WHERE id = ? AND seller_id = ?', [req.params.id, user.id]);
10
11
  if (!product)
11
12
  return void res.status(404).json({ error: '商品不存在或无权限' });
12
- const { title, description, price, stock, specs, brand, model, handling_hours, ship_regions, estimated_days, fragile, return_days, return_condition, warranty_days, low_stock_threshold, auto_delist_on_zero, origin_claims, i18n_titles, i18n_descs, } = req.body;
13
+ const { title, description, price, stock, specs, brand, model, handling_hours, ship_regions, estimated_days, fragile, return_days, return_condition, warranty_days, low_stock_threshold, auto_delist_on_zero, origin_claims, i18n_titles, i18n_descs, weight_kg, package_size, origin_country, country_of_origin, customs_description, hs_code, // S0 跨境清关/物流证据字段(可选;进后续订单条款快照,不影响在途单)
14
+ } = req.body;
13
15
  const now = new Date().toISOString();
14
16
  const specsJson = specs != null ? (typeof specs === 'object' ? JSON.stringify(specs) : specs) : product.specs;
15
17
  const estJson = estimated_days != null ? (typeof estimated_days === 'object' ? JSON.stringify(estimated_days) : String(estimated_days)) : product.estimated_days;
@@ -79,6 +81,12 @@ export function registerProductsUpdateRoutes(app, deps) {
79
81
  }
80
82
  return Object.keys(out).length > 0 ? JSON.stringify(out) : null;
81
83
  };
84
+ // S0 清关字段(可选;与 create 同校验;null 显式清除,undefined 保留)
85
+ const _cc = (x) => x === undefined ? undefined : ((typeof x === 'string' && x.trim()) ? x.trim().toUpperCase().slice(0, 8) : null);
86
+ const _tx = (x, n) => x === undefined ? undefined : ((typeof x === 'string' && x.trim()) ? x.trim().slice(0, n) : null);
87
+ const _hs = _tx(hs_code, 12);
88
+ if (typeof _hs === 'string' && !/^[0-9.]{4,12}$/.test(_hs))
89
+ return void res.status(400).json({ error: 'hs_code 须为 4-12 位数字(可含 .)', error_code: 'INVALID_HS_CODE' });
82
90
  const titlesResult = validateI18n(i18n_titles);
83
91
  const descsResult = validateI18n(i18n_descs);
84
92
  const newI18nTitles = titlesResult === undefined ? product.i18n_titles : titlesResult;
@@ -91,6 +99,7 @@ export function registerProductsUpdateRoutes(app, deps) {
91
99
  low_stock_alerted_at = CASE WHEN ?=1 THEN NULL ELSE low_stock_alerted_at END,
92
100
  origin_claims=?,
93
101
  i18n_titles=?, i18n_descs=?,
102
+ weight_kg=?, package_size=?, origin_country=?, country_of_origin=?, customs_description=?, hs_code=?,
94
103
  commitment_hash=?, description_hash=?, price_hash=?, hashed_at=?,
95
104
  updated_at=datetime('now')
96
105
  WHERE id=?`, [
@@ -101,6 +110,12 @@ export function registerProductsUpdateRoutes(app, deps) {
101
110
  newLowThreshold, newAutoDelist, resetAlert,
102
111
  newOriginClaims,
103
112
  newI18nTitles, newI18nDescs,
113
+ weight_kg === undefined ? product.weight_kg : (weight_kg === null || weight_kg === '' ? null : Number(weight_kg)),
114
+ _tx(package_size, 40) === undefined ? product.package_size : _tx(package_size, 40),
115
+ _cc(origin_country) === undefined ? product.origin_country : _cc(origin_country),
116
+ _cc(country_of_origin) === undefined ? product.country_of_origin : _cc(country_of_origin),
117
+ _tx(customs_description, 120) === undefined ? product.customs_description : _tx(customs_description, 120),
118
+ _hs === undefined ? product.hs_code : _hs,
104
119
  makeCommitmentHash(pFields),
105
120
  makeDescriptionHash({ title: newTitle, description: newDesc, specs: specsJson }),
106
121
  makePriceHash(newPrice, now), now,
@@ -134,6 +149,25 @@ export function registerProductsUpdateRoutes(app, deps) {
134
149
  console.error('[wishlist price-drop notify]', e);
135
150
  }
136
151
  }
152
+ // 反作弊(PR-⑥):任何【买家可见的商品身份字段】变更 → 作废该商品的直付逐品验证,强制重新验证,防"先验证商品 A,
153
+ // 再改成商品 B"绕过逐品硬门。external-link 变更在 products-links 路由另行作废。fail-soft,不阻断编辑。
154
+ // ⚠️ 必须含 i18n_titles/i18n_descs:formatProductForAgent 按 Accept-Language 把 title/description 换成对应语言,
155
+ // 只改 en/ja/ko 标题描述同样是"换货"(非中文买家看到的就变了);并含 brand/model(商品身份)。
156
+ const newBrand = brand ?? product.brand;
157
+ const newModel = model ?? product.model;
158
+ const eq = (a, b) => String(a ?? '') === String(b ?? '');
159
+ const materialChanged = !eq(newTitle, product.title) || !eq(newDesc, product.description)
160
+ || Number(newPrice) !== Number(product.price) || !eq(specsJson, product.specs)
161
+ || !eq(newI18nTitles, product.i18n_titles) || !eq(newI18nDescs, product.i18n_descs)
162
+ || !eq(newBrand, product.brand) || !eq(newModel, product.model);
163
+ if (materialChanged) {
164
+ try {
165
+ invalidateProductVerification(db, String(req.params.id));
166
+ }
167
+ catch (e) {
168
+ console.error('[product-verify invalidate]', e);
169
+ }
170
+ }
137
171
  res.json({ success: true });
138
172
  });
139
173
  }
@@ -59,8 +59,10 @@ export function registerProfileIdentityRoutes(app, deps) {
59
59
  const roles = safeRoles(user);
60
60
  if (!roles.includes(role))
61
61
  return void res.json({ error: '你还没有该角色,请先添加' });
62
- // 防御:已是 admin/verifier 的用户不能切到 buyer/seller(防遗留多角色账户绕过)
63
- if (roles.some(r => ROLE_LOCKED_ROLES.includes(r)) && !ROLE_LOCKED_ROLES.includes(role)) {
62
+ // 防御:已是 admin/verifier 的用户不能切到 buyer/seller(防遗留多角色账户绕过)。
63
+ // 锁的是【交易面目标】,不是一切切换 —— 此前 `!ROLE_LOCKED_ROLES.includes(role)` verifier→arbitrator/logistics
64
+ // (同为受信/治理身份,零交易能力)都拦 → 切到审核员后被永久卡死在审核员(梦想者1号案:回不到仲裁员)。
65
+ if (roles.some(r => ROLE_LOCKED_ROLES.includes(r)) && ['buyer', 'seller'].includes(role)) {
64
66
  return void res.json({
65
67
  error: '受信角色不能切换为 buyer / seller',
66
68
  hint: '权责分离原则;如需买卖请用其他账号。'
@@ -28,6 +28,8 @@ export function registerPromoterRoutes(app, deps) {
28
28
  for (const r of earned)
29
29
  byLevel[r.level] = { orders: r.orders, total: r.total };
30
30
  const grand = byLevel[1].total + byLevel[2].total + byLevel[3].total;
31
+ // RFC-018: commission accrued but still in the clearing window (matures into grand_total). Pure read.
32
+ const clearing = (await dbOne("SELECT COALESCE(SUM(amount),0) as s FROM pending_commission_escrow WHERE recipient_user_id = ? AND matures_at IS NOT NULL AND status = 'pending'", [userId])).s;
31
33
  const recent = await dbAll(`
32
34
  SELECT cr.id, cr.order_id, cr.level, cr.amount, cr.rate, cr.created_at,
33
35
  u.name as source_buyer_name
@@ -142,6 +144,7 @@ export function registerPromoterRoutes(app, deps) {
142
144
  team: { l1, l2, l3, total: l1 + l2 + l3 },
143
145
  earnings: {
144
146
  grand_total: grand,
147
+ clearing_total: clearing, // RFC-018: accrued, maturing after the return window (not yet paid)
145
148
  l1: byLevel[1],
146
149
  l2: byLevel[2],
147
150
  l3: byLevel[3],
@@ -23,6 +23,9 @@ export function registerReferralRoutes(app, deps) {
23
23
  `, [user.id]));
24
24
  const todayEarnings = (await dbOne(`SELECT COALESCE(SUM(amount), 0) as t FROM commission_records WHERE beneficiary_id = ? AND created_at > datetime('now', '-1 day')`, [user.id])).t;
25
25
  const monthEarnings = (await dbOne(`SELECT COALESCE(SUM(amount), 0) as t FROM commission_records WHERE beneficiary_id = ? AND created_at > datetime('now', '-30 days')`, [user.id])).t;
26
+ // RFC-018: commission accrued but still in the clearing window (pending → matures into total_waz).
27
+ // Pure read; surfaced so earnings don't appear to vanish during clearing (Option A keeps total_waz = paid).
28
+ const clearingWaz = (await dbOne(`SELECT COALESCE(SUM(amount), 0) as t FROM pending_commission_escrow WHERE recipient_user_id = ? AND matures_at IS NOT NULL AND status = 'pending'`, [user.id])).t;
26
29
  res.json({
27
30
  invite_code: code,
28
31
  invite_link: code ? `${req.protocol}://${req.get('host')}/i/${code}` : null,
@@ -34,6 +37,7 @@ export function registerReferralRoutes(app, deps) {
34
37
  total_waz: earnings.total,
35
38
  today_waz: todayEarnings,
36
39
  month_waz: monthEarnings,
40
+ clearing_waz: clearingWaz, // RFC-018: accrued, maturing after the return window (not yet paid)
37
41
  },
38
42
  });
39
43
  });