@seasonkoh/webaz 0.1.28 → 0.1.29

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 (111) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +7 -6
  3. package/dist/currency.js +16 -0
  4. package/dist/deposit-rails.js +52 -0
  5. package/dist/direct-pay-aml-monitor.js +67 -0
  6. package/dist/direct-pay-aml-review.js +40 -0
  7. package/dist/direct-pay-base-bond-entry.js +5 -0
  8. package/dist/direct-pay-bond-rail-clearance.js +94 -0
  9. package/dist/direct-pay-compliance-ingress.js +145 -0
  10. package/dist/direct-pay-controls.js +136 -0
  11. package/dist/direct-pay-create.js +162 -0
  12. package/dist/direct-pay-deferral-quota.js +43 -0
  13. package/dist/direct-pay-disclosures.js +44 -0
  14. package/dist/direct-pay-eligibility.js +46 -0
  15. package/dist/direct-pay-fee-ar.js +241 -0
  16. package/dist/direct-pay-launch-readiness.js +108 -0
  17. package/dist/direct-pay-launch-summary.js +68 -0
  18. package/dist/direct-pay-ledger.js +94 -0
  19. package/dist/direct-receive-account-qr.js +77 -0
  20. package/dist/direct-receive-accounts.js +82 -0
  21. package/dist/direct-receive-deferral.js +142 -0
  22. package/dist/direct-receive-deposits.js +260 -0
  23. package/dist/direct-receive-payment-instruction.js +26 -0
  24. package/dist/fx-rates.js +71 -0
  25. package/dist/layer0-foundation/L0-1-database/schema.js +531 -1
  26. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
  27. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +41 -0
  28. package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
  29. package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
  30. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +11 -0
  31. package/dist/layer1-agent/L1-1-mcp-server/server.js +18 -16
  32. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
  33. package/dist/ledger.js +12 -3
  34. package/dist/mcp.js +23 -4
  35. package/dist/merchant-bond-domain.js +64 -0
  36. package/dist/merchant-bond-exposure.js +78 -0
  37. package/dist/merchant-bond-watcher.js +26 -0
  38. package/dist/payment-rails.js +29 -0
  39. package/dist/product-verification.js +93 -0
  40. package/dist/pwa/acp-feed.js +3 -2
  41. package/dist/pwa/contract-fingerprint.js +3 -0
  42. package/dist/pwa/direct-pay-guards.js +20 -0
  43. package/dist/pwa/direct-pay-order-redaction.js +24 -0
  44. package/dist/pwa/endpoint-actions.js +3 -0
  45. package/dist/pwa/public/app-ai.js +10 -10
  46. package/dist/pwa/public/app-chat-poll.js +29 -0
  47. package/dist/pwa/public/app-create-kinds.js +17 -0
  48. package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
  49. package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
  50. package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
  51. package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
  52. package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
  53. package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
  54. package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
  55. package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
  56. package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
  57. package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
  58. package/dist/pwa/public/app-direct-pay.js +227 -0
  59. package/dist/pwa/public/app-discover.js +20 -20
  60. package/dist/pwa/public/app-external-links.js +32 -0
  61. package/dist/pwa/public/app-listings.js +4 -4
  62. package/dist/pwa/public/app-prelaunch-waz.js +39 -0
  63. package/dist/pwa/public/app-price.js +55 -0
  64. package/dist/pwa/public/app-product-media.js +15 -0
  65. package/dist/pwa/public/app-profile.js +6 -6
  66. package/dist/pwa/public/app-seller.js +5 -5
  67. package/dist/pwa/public/app-shop.js +19 -19
  68. package/dist/pwa/public/app.js +142 -146
  69. package/dist/pwa/public/i18n.js +398 -197
  70. package/dist/pwa/public/index.html +17 -0
  71. package/dist/pwa/public/openapi.json +495 -1
  72. package/dist/pwa/routes/admin-analytics.js +4 -2
  73. package/dist/pwa/routes/admin-direct-receive-deposits.js +321 -0
  74. package/dist/pwa/routes/buyer-feeds.js +2 -1
  75. package/dist/pwa/routes/chat.js +6 -1
  76. package/dist/pwa/routes/dashboards.js +2 -1
  77. package/dist/pwa/routes/direct-pay-availability.js +196 -0
  78. package/dist/pwa/routes/direct-pay-disclosure-acks.js +74 -0
  79. package/dist/pwa/routes/direct-pay-timeouts.js +71 -0
  80. package/dist/pwa/routes/direct-receive-accounts.js +155 -0
  81. package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
  82. package/dist/pwa/routes/fx.js +12 -0
  83. package/dist/pwa/routes/leaderboard.js +47 -9
  84. package/dist/pwa/routes/listings.js +2 -2
  85. package/dist/pwa/routes/logistics.js +4 -0
  86. package/dist/pwa/routes/manifests.js +38 -0
  87. package/dist/pwa/routes/me-data.js +5 -2
  88. package/dist/pwa/routes/orders-action.js +117 -9
  89. package/dist/pwa/routes/orders-create.js +4 -0
  90. package/dist/pwa/routes/orders-read.js +36 -0
  91. package/dist/pwa/routes/p2p-products.js +2 -2
  92. package/dist/pwa/routes/products-create.js +5 -3
  93. package/dist/pwa/routes/products-links.js +34 -0
  94. package/dist/pwa/routes/products-list.js +2 -1
  95. package/dist/pwa/routes/products-update.js +22 -2
  96. package/dist/pwa/routes/promoter.js +3 -0
  97. package/dist/pwa/routes/referral.js +4 -0
  98. package/dist/pwa/routes/returns.js +26 -1
  99. package/dist/pwa/routes/rewards-apply.js +10 -5
  100. package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
  101. package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
  102. package/dist/pwa/routes/shops.js +2 -1
  103. package/dist/pwa/routes/url-claim.js +2 -2
  104. package/dist/pwa/routes/users-public.js +8 -0
  105. package/dist/pwa/routes/wallet-read.js +3 -0
  106. package/dist/pwa/routes/webauthn.js +1 -1
  107. package/dist/pwa/server.js +59 -102
  108. package/dist/runtime/webaz-schema-helpers.js +104 -0
  109. package/dist/store-verification.js +77 -0
  110. package/dist/version.js +1 -1
  111. package/package.json +71 -2
@@ -0,0 +1,96 @@
1
+ // RFC-016 Phase 1 — sweep read → async seam; the per-row pay tx stays sync (Phase 3 → pg).
2
+ import { dbAll } from '../../layer0-foundation/L0-1-database/db.js';
3
+ import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
4
+ import { applyWalletDelta } from '../../ledger.js';
5
+ import { toUnits, toDecimal } from '../../money.js';
6
+ // #7 Commission source_type (moved from settleCommission). Pure read — stamps commission_records.source_type.
7
+ function commissionSourceType(db, productId, uid) {
8
+ if (!uid)
9
+ return 'sponsor';
10
+ const attr = db.prepare(`SELECT shareable_id FROM product_share_attribution WHERE product_id = ? AND sharer_id = ? AND shareable_id IS NOT NULL ORDER BY created_at DESC LIMIT 1`).get(productId, uid);
11
+ if (!attr)
12
+ return 'sponsor';
13
+ const sh = db.prepare(`SELECT type FROM shareables WHERE id = ?`).get(attr.shareable_id);
14
+ if (!sh)
15
+ return 'sponsor';
16
+ return sh.type === 'note' ? 'note' : 'link';
17
+ }
18
+ /**
19
+ * Mature ONE clearing commission row. Re-validates genuine closure (completed + no open dispute), then
20
+ * writes commission_records + credits the wallet in one tx. CAS on status='pending' → idempotent; races
21
+ * and re-runs lose. Holds (no write) while a dispute is open. Exported for direct unit testing.
22
+ */
23
+ export function matureClearingRow(db, row) {
24
+ let outcome = 'skipped';
25
+ db.transaction(() => {
26
+ const order = db.prepare("SELECT buyer_id, product_id, snapshot_commission_rate, buyer_region, status FROM orders WHERE id = ?").get(row.order_id);
27
+ if (!order || order.status !== 'completed') {
28
+ outcome = 'held';
29
+ return;
30
+ } // completed is terminal; defensive hold
31
+ const openDispute = db.prepare("SELECT 1 FROM disputes WHERE order_id = ? AND status NOT IN ('resolved','dismissed','closed') LIMIT 1").get(row.order_id);
32
+ if (openDispute) {
33
+ outcome = 'held';
34
+ return;
35
+ } // hold while unresolved (e.g. return escalated to dispute)
36
+ const claimed = db.prepare("UPDATE pending_commission_escrow SET status='settled', settled_at=? WHERE id=? AND status='pending' AND matures_at IS NOT NULL").run(Date.now(), row.id);
37
+ if (claimed.changes !== 1) {
38
+ outcome = 'skipped';
39
+ return;
40
+ } // race lost / already settled / reversed
41
+ const amountU = toUnits(Number(row.amount));
42
+ if (amountU > 0) {
43
+ const level = Number(String(row.attribution_path).replace(/^L/, '')) || 0;
44
+ const rate = Number(order.snapshot_commission_rate ?? 0.10);
45
+ let region = order.buyer_region || null;
46
+ if (!region)
47
+ region = db.prepare("SELECT region FROM users WHERE id = ?").get(order.buyer_id)?.region ?? 'global';
48
+ const srcType = commissionSourceType(db, order.product_id, row.recipient_user_id);
49
+ try {
50
+ db.prepare(`INSERT INTO commission_records (id, order_id, beneficiary_id, source_buyer_id, level, amount, rate, region, source, source_type) VALUES (?,?,?,?,?,?,?,?,?,?)`)
51
+ .run(generateId('comm'), row.order_id, row.recipient_user_id, order.buyer_id, level, toDecimal(amountU), rate, region, 'static', srcType);
52
+ }
53
+ catch (e) { /* UNIQUE — already recorded on a prior partial run */ }
54
+ applyWalletDelta(db, row.recipient_user_id, { balance: amountU, earned: amountU });
55
+ }
56
+ outcome = 'settled';
57
+ })();
58
+ return outcome;
59
+ }
60
+ export async function runClearingMatureSweep(deps) {
61
+ const { db } = deps;
62
+ const now = Date.now();
63
+ const rows = await dbAll(`
64
+ SELECT id, recipient_user_id, order_id, amount, attribution_path
65
+ FROM pending_commission_escrow
66
+ WHERE status = 'pending' AND matures_at IS NOT NULL AND matures_at <= ?
67
+ ORDER BY matures_at ASC
68
+ LIMIT 1000
69
+ `, [now]);
70
+ let settled = 0, held = 0, skipped = 0;
71
+ for (const r of rows) {
72
+ const o = matureClearingRow(db, r);
73
+ if (o === 'settled')
74
+ settled++;
75
+ else if (o === 'held')
76
+ held++;
77
+ else
78
+ skipped++;
79
+ }
80
+ return { scanned: rows.length, settled, held, skipped };
81
+ }
82
+ export function startClearingMatureCron(deps) {
83
+ const ms = 60 * 60 * 1000; // 1h — clearing window granularity is days
84
+ setInterval(async () => {
85
+ try {
86
+ const r = await runClearingMatureSweep(deps);
87
+ if (r.settled > 0 || r.held > 0) {
88
+ console.log(`[clearing-mature] scanned ${r.scanned}, settled ${r.settled}, held ${r.held}, skipped ${r.skipped}`);
89
+ }
90
+ }
91
+ catch (e) {
92
+ console.error('[clearing-mature-cron]', e);
93
+ }
94
+ }, ms);
95
+ console.log('⏳ RFC-018 commission clearing maturation cron 已启动 (每 1h, anchor=matures_at)');
96
+ }
@@ -3,17 +3,21 @@ import { dbAll } from '../../layer0-foundation/L0-1-database/db.js';
3
3
  export async function runEscrowExpireSweep(deps) {
4
4
  const { db, redirectToCommissionReserve } = deps;
5
5
  const now = Date.now();
6
+ // RFC-018: pending_commission_escrow now holds TWO lifecycles, discriminated by matures_at:
7
+ // - matures_at IS NULL → opt-out escrow (this cron: expire → commission_reserve / pool).
8
+ // - matures_at NOT NULL → clearing rows (the SEPARATE rewards-clearing-mature cron: pay/reverse).
9
+ // We MUST exclude clearing rows here, or they'd be wrongly expired to the pool instead of paid.
6
10
  const rows = await dbAll(`
7
11
  SELECT id, recipient_user_id, order_id, amount, attribution_path, expires_at
8
12
  FROM pending_commission_escrow
9
- WHERE status = 'pending' AND expires_at <= ?
13
+ WHERE status = 'pending' AND matures_at IS NULL AND expires_at <= ?
10
14
  ORDER BY expires_at ASC
11
15
  LIMIT 1000
12
16
  `, [now]);
13
17
  const expired = [];
14
18
  for (const r of rows) {
15
19
  db.transaction(() => {
16
- const upd = db.prepare(`UPDATE pending_commission_escrow SET status='expired', expired_to_charity_at=? WHERE id=? AND status='pending'`).run(now, r.id);
20
+ const upd = db.prepare(`UPDATE pending_commission_escrow SET status='expired', expired_to_charity_at=? WHERE id=? AND status='pending' AND matures_at IS NULL`).run(now, r.id);
17
21
  if (upd.changes === 0)
18
22
  return; // race lost — another sweep already took it
19
23
  if (r.attribution_path === 'pv_pair') {
@@ -1,4 +1,5 @@
1
1
  import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // RFC-018 PR4: 真实成交(排除全额退货)
2
3
  export function registerShopsRoutes(app, deps) {
3
4
  // db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
4
5
  const { auth } = deps;
@@ -20,7 +21,7 @@ export function registerShopsRoutes(app, deps) {
20
21
  const sellerId = String(seller.id);
21
22
  const products = await dbAll(`
22
23
  SELECT p.id, p.title, p.price, p.stock, p.category, p.images, p.has_variants, p.commission_rate,
23
- (SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND o.status = 'completed') as sales_count
24
+ (SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND ${genuineSalePredicate('o')}) as sales_count
24
25
  FROM products p
25
26
  WHERE p.seller_id = ? AND p.status = 'active'
26
27
  ORDER BY sales_count DESC, p.created_at DESC
@@ -119,8 +119,8 @@ export function registerUrlClaimRoutes(app, deps) {
119
119
  db.prepare(`INSERT INTO products (
120
120
  id, seller_id, title, description, price, stock, category, stake_amount,
121
121
  specs, source_url, handling_hours, return_days, warranty_days,
122
- commitment_hash, description_hash, price_hash, hashed_at, status, stake_locked_at
123
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'warehouse',datetime('now'))`)
122
+ commitment_hash, description_hash, price_hash, hashed_at, status, stake_locked_at, currency
123
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'warehouse',datetime('now'),'WAZ')`)
124
124
  .run(productId, user.id, title, description, priceNum, Number(stock), category, stakeAmount, specsJson, url, Number(handling_hours), Number(return_days), Number(warranty_days), makeCommitmentHash(pFields), makeDescriptionHash({ title, description, specs: specsJson }), makePriceHash(priceNum, now), now);
125
125
  db.prepare(`INSERT INTO product_external_links
126
126
  (id, product_id, url, source, verified, verify_note, platform, external_id, external_title)
@@ -25,6 +25,14 @@ export function registerUsersPublicRoutes(app, deps) {
25
25
  return;
26
26
  userId = user.id;
27
27
  }
28
+ else {
29
+ // ref 三态(usr_xxx / permanent_code / @handle)必须解析为 canonical id;
30
+ // 漏掉解析会让 @handle/permanent_code 当字面 user_id 去查 → 永远落到默认 'new'。
31
+ const resolved = await resolveUserId(userId);
32
+ if (!resolved)
33
+ return void res.status(404).json({ error: 'user not found' });
34
+ userId = resolved;
35
+ }
28
36
  const row = await dbOne(`
29
37
  SELECT level, MAX(trust_score) as max_score
30
38
  FROM agent_reputation WHERE user_id = ?
@@ -181,8 +181,11 @@ export function registerWalletReadRoutes(app, deps) {
181
181
  FROM orders WHERE seller_id = ? AND status = 'completed'
182
182
  `, [user.id]));
183
183
  const totalIncome = commMap.l1.total + commMap.l2.total + commMap.l3.total + Number(sales.total);
184
+ // RFC-018: commission accrued but still in the clearing window (matures into commissions/total_income). Pure read.
185
+ 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'", [user.id])).s;
184
186
  res.json({
185
187
  commissions: commMap,
188
+ commission_clearing: Number(clearing.toFixed(2)), // RFC-018: accrued, maturing after the return window (not yet paid)
186
189
  sales: { count: sales.cnt, total: Number(Number(sales.total).toFixed(2)) },
187
190
  total_income: Number(totalIncome.toFixed(2)),
188
191
  });
@@ -74,7 +74,7 @@ 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']);
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']);
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;
@@ -22,12 +22,15 @@ import crypto from 'crypto';
22
22
  import { initDatabase, generateId } from '../layer0-foundation/L0-1-database/schema.js';
23
23
  import { setSeamDb } from '../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
24
24
  import { initSystemUser, transition, getOrderStatus, checkTimeouts, settleFault } from '../layer0-foundation/L0-2-state-machine/engine.js';
25
+ import { genuineSalePredicate } from '../layer0-foundation/L0-2-state-machine/genuine-sale.js';
26
+ import { settleDirectPayFeeAtCompletion } from '../direct-pay-fee-ar.js'; // RFC-018 PR4 genuine-sale SSOT + Direct Pay Rail 1 平台费链下应收(完成时释放遗留模拟 stake + accrue)
25
27
  import { endpointToAction, endpointToReadAction } from './endpoint-actions.js';
26
28
  import { AGENT_RATE_PER_MIN_DEFAULTS, CROSS_USER_READ_DAILY_CAP, MASS_ACTION_TYPES, MASS_ACTION_DAILY_CAPS } from './limits.js';
27
29
  // #420 P1-2/P1-3/P1-4 — 反滥用阈值单一真相源(governance-adjustable protocol_params)+ 纯决策函数
28
30
  import { ANTI_ABUSE_PARAMS, readAntiAbuseThresholds, agentTrustLevel, agentSybilPenalty, agentStrikeSeverity, verifierOutlierBand } from './anti-abuse-thresholds.js';
31
+ import { DIRECT_PAY_CONTROL_PARAMS, DIRECT_PAY_AML_PARAMS } from '../direct-pay-controls.js'; // PR-4a 控制面 + PR-6D AML 监控 param 默认 seed(均 fail-closed / inert)
29
32
  import { initOrderChainSchema, appendOrderEvent, getOrderChain, verifyOrderChain } from '../layer0-foundation/L0-2-state-machine/order-chain.js';
30
- import { initVerifierWhitelistSchema, initMcpToolCallsSchema, initNotePhotoIndexSchema, initUserWishlistSchema, initProductQaSchema, initCouponsSchema, initAnnouncementsSchema, initProductWaitlistSchema, initFlashSalesSchema, initPublicIdeasSchema, initAuctionRemindersSchema, initEmailSubscriptionsSchema, initFeedbackTicketsSchema, initFeedbackMessagesSchema, initDisputeCasesSchema, initDisputeCommentsSchema, initDisputeCommentRepliesSchema, initShareableCommentsSchema, initDisputeFairnessVotesSchema, initOrderRatingsSchema, initBuyerRatingsSchema, initUserAddressesSchema, initP2pShopsSchema, initShareableLikesSchema, initShareableBookmarksSchema, initShareableTagsSchema, initManifestRegistrySchema, initPeerDirectorySchema, initSignalingQueueSchema, initConversationsSchema, initMessagesSchema, initChatReportsSchema, initQuotaIncreaseApplicationsSchema, initVerifierApplicationsSchema, initArbitratorReviewSchema, initVerifierAppealsSchema, initUserModerationSchema, initAdminAuditLogSchema, initVerificationCodesSchema, initAgentCallLogSchema, initAgentReputationSchema, initAgentDeclarationsSchema, initAgentAttestationsSchema, initAgentStrikesSchema, initAgentRevocationsSchema, initProductAliasesSchema, initRegionChangeLogSchema, initCartItemsSchema, initFollowsSchema, initPushSubscriptionsSchema, initUserSessionsSchema, initUserBlocklistSchema, initImportLogsSchema, initErrorLogSchema, initSecondhandItemsSchema, initProductTrialCampaignsSchema, initProductTrialClaimsSchema, initReturnRequestsSchema, initReturnMessagesSchema, initProductVariantsSchema, initEditorPicksSchema, initKycRecordsSchema, initWebauthnSchema, initClaimVerificationBaseSchema, initClaimVerifierSuspensionsSchema, initProductClaimSchema, initReviewClaimSchema, initSecondhandClaimSchema, initAuctionClaimSchema, initWishClaimSchema, initShareableClickLogSchema, initCommissionAuditLogSchema, initRegistrationAuditLogSchema, initProductExternalLinksBaseSchema, initLinkChallengesSchema, initVerifyTasksSchema, initVerifySubmissionsSchema, initVerifierStatsSchema, initRegisterListSearchColumns } from './server-schema.js';
33
+ import { initVerifierWhitelistSchema, initMcpToolCallsSchema, initNotePhotoIndexSchema, initUserWishlistSchema, initProductQaSchema, initCouponsSchema, initAnnouncementsSchema, initProductWaitlistSchema, initFlashSalesSchema, initPublicIdeasSchema, initAuctionRemindersSchema, initEmailSubscriptionsSchema, initFeedbackTicketsSchema, initFeedbackMessagesSchema, initDisputeCasesSchema, initDisputeCommentsSchema, initDisputeCommentRepliesSchema, initShareableCommentsSchema, initDisputeFairnessVotesSchema, initOrderRatingsSchema, initBuyerRatingsSchema, initUserAddressesSchema, initP2pShopsSchema, initShareableLikesSchema, initShareableBookmarksSchema, initShareableTagsSchema, initManifestRegistrySchema, initPeerDirectorySchema, initSignalingQueueSchema, initConversationsSchema, initMessagesSchema, initChatReportsSchema, initQuotaIncreaseApplicationsSchema, initVerifierApplicationsSchema, initArbitratorReviewSchema, initVerifierAppealsSchema, initUserModerationSchema, initAdminAuditLogSchema, initVerificationCodesSchema, initAgentCallLogSchema, initAgentReputationSchema, initAgentDeclarationsSchema, initAgentAttestationsSchema, initAgentStrikesSchema, initAgentRevocationsSchema, initProductAliasesSchema, initRegionChangeLogSchema, initCartItemsSchema, initFollowsSchema, initPushSubscriptionsSchema, initUserSessionsSchema, initUserBlocklistSchema, initImportLogsSchema, initErrorLogSchema, initSecondhandItemsSchema, initProductTrialCampaignsSchema, initProductTrialClaimsSchema, initReturnRequestsSchema, initReturnMessagesSchema, initProductVariantsSchema, initEditorPicksSchema, initKycRecordsSchema, initWebauthnSchema, initClaimVerificationBaseSchema, initClaimVerifierSuspensionsSchema, initProductClaimSchema, initReviewClaimSchema, initSecondhandClaimSchema, initAuctionClaimSchema, initWishClaimSchema, initShareableClickLogSchema, initCommissionAuditLogSchema, initRegistrationAuditLogSchema, initProductExternalLinksBaseSchema, initLinkChallengesSchema, initVerifyTasksSchema, initVerifySubmissionsSchema, initVerifierStatsSchema, initRegisterListSearchColumns, initPendingCommissionEscrowSchema } from './server-schema.js';
31
34
  // RFC-014 PR4 — 正常成交结算走整数 base-units + allocate + 绝对值落库。
32
35
  import { toUnits, toDecimal, mulRate, allocate } from '../money.js';
33
36
  import { applyWalletDelta, creditColumns } from '../ledger.js';
@@ -84,6 +87,7 @@ import { registerClaimVerifyRoutes, processClaimTaskQueue, isEligibleClaimVerifi
84
87
  import { registerFollowsRoutes } from './routes/follows.js';
85
88
  // Leaderboard (#1013 Phase 11) — 单 endpoint 8 kinds(products/creators/buyers/sellers/value/agents/arbitrators/verifiers)
86
89
  import { registerLeaderboardRoutes } from './routes/leaderboard.js';
90
+ import { registerFxRoutes } from './routes/fx.js'; // FX = display-only USDC→local rates
87
91
  // Shareables 互动 (#1013 Phase 12) — click/like/comments/bookmark 8 endpoints
88
92
  import { registerShareablesInteractionsRoutes } from './routes/shareables-interactions.js';
89
93
  // Shareables CRUD (#1013 Phase 13) — 11 endpoints (notes-photo + create + me + creator-stats + by-* + feed + detail + PATCH + DELETE)
@@ -154,6 +158,8 @@ import { registerArbitratorRoutes } from './routes/arbitrator.js';
154
158
  import { registerGovernanceOnboardingRoutes } from './routes/governance-onboarding.js';
155
159
  import { startAutoDeactivateCron, runAutoDeactivateSweep } from './routes/governance-auto-deactivate.js';
156
160
  import { startEscrowExpireCron } from './routes/rewards-escrow-expire.js';
161
+ import { startDirectPayTimeoutCron } from './routes/direct-pay-timeouts.js';
162
+ import { startClearingMatureCron } from './routes/rewards-clearing-mature.js';
157
163
  import { startAutoDowngradeCron } from './routes/rewards-auto-downgrade.js';
158
164
  import { registerRewardsApplyRoutes } from './routes/rewards-apply.js';
159
165
  // 卖家配额 + 数据中心 (#1013 Phase 45) — 4 user + 3 admin
@@ -242,6 +248,11 @@ import { registerOrdersReadRoutes } from './routes/orders-read.js';
242
248
  import { registerOrdersActionRoutes } from './routes/orders-action.js';
243
249
  // Orders 下单端点 (#1013 Phase 85) — 1 endpoint (338-line POST /api/orders)
244
250
  import { registerOrdersCreateRoutes } from './routes/orders-create.js';
251
+ import { registerDirectPayDisclosureAckRoutes } from './routes/direct-pay-disclosure-acks.js'; // PR-4d: Direct Pay 风险披露 ack 端点(薄 adapter)
252
+ import { registerDirectReceivePaymentInstructionRoutes } from './routes/direct-receive-payment-instructions.js';
253
+ import { registerDirectReceiveAccountsRoutes } from './routes/direct-receive-accounts.js'; // PR-4f-a instruction CRUD + Phase C1 multi-account+QR
254
+ import { registerDirectPayAvailabilityRoutes } from './routes/direct-pay-availability.js'; // PR-4a: Direct Pay 可用性只读(控制面 SSOT)
255
+ import { registerAdminDirectReceiveDepositsRoutes } from './routes/admin-direct-receive-deposits.js'; // PR-4b-3: ROOT 生产保证金 receipt 确认(fail-closed scaffold)
245
256
  // Disputes 读端点 (#1013 Phase 86) — 5 endpoints (list/similar/detail/evidence-list/parties)
246
257
  import { registerDisputesReadRoutes } from './routes/disputes-read.js';
247
258
  // Disputes 写端点 (#1013 Phase 87) — 5 endpoints (respond/arbitrate/add-evidence/evidence-blob/request-evidence)
@@ -389,6 +400,7 @@ initSkillSchema(db);
389
400
  initSkillMarketSchema(db);
390
401
  initReputationSchema(db);
391
402
  initOrderChainSchema(db);
403
+ initPendingCommissionEscrowSchema(db); // RFC-018 PR1: relocated from inline (was ~8250) so all escrow schema is built in this early batch — see helper. matures_at + reversed added (schema-only).
392
404
  initBuildFeedbackSchema(db); // RFC-004 build_feedback
393
405
  initBuildTasksSchema(db); // RFC-006 build_tasks(协调层)
394
406
  initBuildTaskAgentMetadataSchema(db); // PR9B — agent-ready task metadata satellite(schema only;FUTURE-TASK-BOARD-V1-DESIGN #326)
@@ -917,6 +929,7 @@ const DEFAULT_PARAMS = [
917
929
  // #420 P1-2/P1-3/P1-4:反滥用阈值(agent 信任公式 / strike 阶梯 / verifier outlier)→ 治理可调。
918
930
  // 默认值 === 抽取前硬编码字面量(单一真相源在 anti-abuse-thresholds.ts;测试强制校验一致)。
919
931
  ...ANTI_ABUSE_PARAMS,
932
+ ...DIRECT_PAY_CONTROL_PARAMS, ...DIRECT_PAY_AML_PARAMS,
920
933
  ];
921
934
  for (const p of DEFAULT_PARAMS) {
922
935
  try {
@@ -2957,7 +2970,7 @@ catch { }
2957
2970
  const upd = db.prepare(`UPDATE products SET
2958
2971
  last_sold_at = (SELECT MAX(COALESCE(updated_at, created_at)) FROM orders WHERE product_id = ? AND status = 'completed'),
2959
2972
  first_sold_at = (SELECT MIN(COALESCE(updated_at, created_at)) FROM orders WHERE product_id = ? AND status = 'completed'),
2960
- completion_count = (SELECT COUNT(1) FROM orders WHERE product_id = ? AND status = 'completed'),
2973
+ completion_count = (SELECT COUNT(1) FROM orders WHERE product_id = ? AND ${genuineSalePredicate('orders')}),
2961
2974
  dispute_loss_count = (
2962
2975
  SELECT COUNT(1) FROM disputes d JOIN orders o ON o.id = d.order_id
2963
2976
  WHERE o.product_id = ? AND d.ruling_type IN ('refund_buyer','partial_refund')
@@ -3233,8 +3246,8 @@ function computeAgentTrust(apiKey) {
3233
3246
  if (!user)
3234
3247
  return null;
3235
3248
  const ageDays = Math.max(0, (Date.now() - new Date(user.created_at.replace(' ', 'T') + 'Z').getTime()) / 86400_000);
3236
- const completedBuyer = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'`).get(user.id).n;
3237
- const completedSeller = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE seller_id = ? AND status = 'completed'`).get(user.id).n;
3249
+ const completedBuyer = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`).get(user.id).n;
3250
+ const completedSeller = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE seller_id = ? AND ${genuineSalePredicate('orders')}`).get(user.id).n;
3238
3251
  const disputeLoss = db.prepare(`SELECT COUNT(*) as n FROM disputes WHERE defendant_id = ? AND ruling_type IN ('refund_buyer','partial_refund')`).get(user.id).n;
3239
3252
  // 商品分享带来的真实成交(创作者贡献)
3240
3253
  const shareConversions = db.prepare(`
@@ -4460,6 +4473,11 @@ registerAgentReputationRoutes(app, {
4460
4473
  });
4461
4474
  // #1013 Phase 47: 6 公开用户主页 endpoints 已迁出到 routes/users-public.ts
4462
4475
  registerUsersPublicRoutes(app, { db, auth, noteAuthenticityBadges });
4476
+ registerDirectPayDisclosureAckRoutes(app, { db, auth, generateId, consumeGateToken }); // PR-4d
4477
+ registerDirectReceivePaymentInstructionRoutes(app, { db, auth, generateId });
4478
+ registerDirectReceiveAccountsRoutes(app, { db, auth, generateId, consumeGateToken }); // PR-4f-a + Phase C1
4479
+ registerDirectPayAvailabilityRoutes(app, { db, auth, getProtocolParam, generateId }); // PR-4a + 缓交 apply/status
4480
+ registerAdminDirectReceiveDepositsRoutes(app, { db, requireRootAdmin: (req, res) => requireRootAdmin(req, res), consumeGateToken, logAdminAction, getProtocolParam }); // PR-4b-3 + readiness
4463
4481
  // RFC-004 build_feedback — agent-native "use → build" 反馈管道
4464
4482
  registerBuildFeedbackRoutes(app, {
4465
4483
  db, auth,
@@ -5652,7 +5670,7 @@ function checkVerifierEligibility(userId) {
5652
5670
  const ageDays = Math.floor((Date.now() - new Date(user.created_at).getTime()) / 86400_000);
5653
5671
  items.push({ key: 'age', label: '账户年龄 ≥ 60 天', current: ageDays, required: 60, ok: ageDays >= 60 });
5654
5672
  items.push({ key: 'email', label: '邮箱已验证', current: user.email_verified ? '✓' : '✗', required: '✓', ok: !!user.email_verified });
5655
- const orders = db.prepare("SELECT COUNT(*) as n FROM orders WHERE (buyer_id = ? OR seller_id = ?) AND status = 'completed'").get(userId, userId).n;
5673
+ const orders = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE (buyer_id = ? OR seller_id = ?) AND ${genuineSalePredicate('orders')}`).get(userId, userId).n;
5656
5674
  items.push({ key: 'orders', label: '完成订单 ≥ 20 笔', current: orders, required: 20, ok: orders >= 20 });
5657
5675
  const disputeLost = db.prepare(`
5658
5676
  SELECT COUNT(*) as n FROM disputes
@@ -5799,7 +5817,7 @@ function checkArbitratorEligibility(userId) {
5799
5817
  const ageDays = Math.floor((Date.now() - new Date(user.created_at).getTime()) / 86400_000);
5800
5818
  items.push({ key: 'age', label: '账户年龄 ≥ 90 天', current: ageDays, required: 90, ok: ageDays >= 90 });
5801
5819
  items.push({ key: 'email', label: '邮箱已验证', current: user.email_verified ? '✓' : '✗', required: '✓', ok: !!user.email_verified });
5802
- const orders = db.prepare("SELECT COUNT(*) as n FROM orders WHERE (buyer_id = ? OR seller_id = ?) AND status = 'completed'").get(userId, userId).n;
5820
+ const orders = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE (buyer_id = ? OR seller_id = ?) AND ${genuineSalePredicate('orders')}`).get(userId, userId).n;
5803
5821
  items.push({ key: 'orders', label: '完成订单 ≥ 50 笔', current: orders, required: 50, ok: orders >= 50 });
5804
5822
  const disputeLost = db.prepare(`
5805
5823
  SELECT COUNT(*) as n FROM disputes
@@ -6019,7 +6037,7 @@ registerOrdersReadRoutes(app, { db, auth, getOrderStatus, getOrderChain, verifyO
6019
6037
  registerOrdersActionRoutes(app, {
6020
6038
  db, auth, isTrustedRole, generateId, transition, notifyTransition,
6021
6039
  settleOrder, settleFault, detectFraud, createDispute, checkTimeouts, recordViolationReputation,
6022
- broadcastSystemEvent,
6040
+ broadcastSystemEvent, consumeGateToken,
6023
6041
  });
6024
6042
  // #1013 Phase 85: POST /api/orders 巨型事务已迁出
6025
6043
  registerOrdersCreateRoutes(app, {
@@ -6134,8 +6152,8 @@ function awardBidAndCreateOrder(rfq, winner) {
6134
6152
  db.prepare(`
6135
6153
  INSERT INTO products (id, seller_id, title, description, price, stock, status, images,
6136
6154
  ship_regions, handling_hours, commission_rate, category_id, stake_amount,
6137
- listing_id, fulfillment_type, eta_hours, freshness_ts, cold_start_remaining)
6138
- VALUES (?,?,?,?,?,?,'active','[]',?,?,?,?,0,?,?,?,datetime('now'),?)
6155
+ listing_id, fulfillment_type, eta_hours, freshness_ts, cold_start_remaining, currency)
6156
+ VALUES (?,?,?,?,?,?,'active','[]',?,?,?,?,0,?,?,?,datetime('now'),?,'WAZ')
6139
6157
  `).run(productId, sellerId, String(rfq.title), `[RFQ ${rfqId}] ` + (rfq.notes ? String(rfq.notes).slice(0, 200) : '协议撮合订单'), price, qty, String(rfq.region_required || '全国'), 24, 0, 'cat_default', lstId, fulfillmentType, etaHours, 30);
6140
6158
  }
6141
6159
  }
@@ -6589,8 +6607,8 @@ function settleAuctionInner(aucId) {
6589
6607
  productId = generateId('p');
6590
6608
  db.prepare(`
6591
6609
  INSERT INTO products (id, seller_id, title, description, price, stock, status, images,
6592
- ship_regions, handling_hours, commission_rate, category_id, stake_amount, listing_id, freshness_ts, cold_start_remaining)
6593
- VALUES (?,?,?,?,?,?,'active','[]',?,?,?,?,0,?,datetime('now'),?)
6610
+ ship_regions, handling_hours, commission_rate, category_id, stake_amount, listing_id, freshness_ts, cold_start_remaining, currency)
6611
+ VALUES (?,?,?,?,?,?,'active','[]',?,?,?,?,0,?,datetime('now'),?,'WAZ')
6594
6612
  `).run(productId, sellerId, String(auc.title), `[AUC ${aucId}] ` + (auc.notes ? String(auc.notes).slice(0, 200) : '拍卖成交'), price, qty, '全国', 24, 0.10, 'cat_default', auc.listing_id ? String(auc.listing_id) : null, 30);
6595
6613
  }
6596
6614
  // 5) 截止期 — 拍卖订单走标准 17 天链(拍卖结束后再谈履约时效)
@@ -6718,7 +6736,7 @@ const TASK_DEFS = {
6718
6736
  };
6719
6737
  // 计算用户当前任务进度(不写库,纯读)
6720
6738
  function computeTaskProgress(userId) {
6721
- const completed = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'`).get(userId).n;
6739
+ const completed = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`).get(userId).n;
6722
6740
  const ratingsGiven = db.prepare(`SELECT COUNT(*) as n FROM order_ratings WHERE buyer_id = ?`).get(userId).n;
6723
6741
  const ratingsReceived = db.prepare(`SELECT COUNT(*) as n FROM order_ratings WHERE seller_id = ?`).get(userId).n;
6724
6742
  const follows = db.prepare(`SELECT COUNT(*) as n FROM follows WHERE follower_id = ?`).get(userId).n;
@@ -6910,6 +6928,7 @@ registerShareablesInteractionsRoutes(app, {
6910
6928
  parseMentions, notifyMentions,
6911
6929
  });
6912
6930
  registerLeaderboardRoutes(app, { db, internalAuditorId: INTERNAL_AUDITOR_ID, rateLimitOk });
6931
+ registerFxRoutes(app, { rateLimitOk });
6913
6932
  // like-status / bookmark-status / bookmarked-shareables 已迁出 (#1013 Phase 12)
6914
6933
  // ─── Manifest Registry API(原生 P2P 内容索引,零字节存储)─────
6915
6934
  // ============================================================
@@ -7187,28 +7206,19 @@ function settleCommission(orderId, effectiveBase) {
7187
7206
  { level: 2, beneficiary: l2Uid },
7188
7207
  { level: 3, beneficiary: l3Uid },
7189
7208
  ];
7190
- // #7 Commission source_type attribution 找到带来 buyer shareable,确定 channel 类型
7191
- // 反推: uid 作为 sharer product 分享给"下家"的 shareable 类型
7192
- function resolveSourceType(uid) {
7193
- if (!uid)
7194
- return 'sponsor';
7195
- const attr = db.prepare(`
7196
- SELECT shareable_id FROM product_share_attribution
7197
- WHERE product_id = ? AND sharer_id = ? AND shareable_id IS NOT NULL
7198
- ORDER BY created_at DESC LIMIT 1
7199
- `).get(order.product_id, uid);
7200
- if (!attr)
7201
- return 'sponsor';
7202
- const sh = db.prepare(`SELECT type FROM shareables WHERE id = ?`).get(attr.shareable_id);
7203
- if (!sh)
7204
- return 'sponsor';
7205
- return sh.type === 'note' ? 'note' : 'link';
7206
- }
7209
+ // #7 Commission source_type resolution moved to module-level commissionSourceType() RFC-018
7210
+ // Option A writes commission_records at MATURATION (not here), so source_type is stamped there.
7207
7211
  // 2026-06-04:所有兜底统一入 commission_reserve(三级公池,独立科目,只进不出)。
7208
7212
  // commission 不再回流 global_fund(PV 资金)—— 三套科目解耦,redirected 始终 0。
7209
7213
  let toCommissionReserve = 0; // → commission_reserve(仅作日志/返回信息用)
7210
7214
  // 三级金额一次性 allocate(精确求和 ≡ poolU);各级再按 gate 路由到钱包/公池/escrow。
7211
7215
  const levelAmtU = allocate(poolU, [LEVEL_RATES[1], LEVEL_RATES[2], LEVEL_RATES[3]]);
7216
+ // RFC-018 clearing window: opted-in commission accrues to pending_commission_escrow and matures
7217
+ // after the product's return window + buffer. Anchor = now (settleCommission runs at order→completed).
7218
+ const nowMsClearing = Date.now();
7219
+ const returnDaysClearing = Number(db.prepare("SELECT return_days FROM products WHERE id = ?").get(order.product_id)?.return_days ?? 7);
7220
+ const clearingBufferDays = Number(db.prepare("SELECT value FROM protocol_params WHERE key = 'settlement.clearing_buffer_days'").get()?.value ?? 2);
7221
+ const maturesAtClearing = nowMsClearing + Math.max(0, returnDaysClearing + clearingBufferDays) * 86400 * 1000;
7212
7222
  for (const { level, beneficiary } of recipients) {
7213
7223
  const amountU = levelAmtU[level - 1];
7214
7224
  const amount = toDecimal(amountU);
@@ -7254,15 +7264,17 @@ function settleCommission(orderId, effectiveBase) {
7254
7264
  catch (e) { /* UNIQUE 冲突 — settleCommission 重入幂等 */ }
7255
7265
  continue;
7256
7266
  }
7257
- // ⑤ 正常分账
7267
+ // ⑤ RFC-018 clearing (Option A): opted-in commission ACCRUES to pending_commission_escrow with a
7268
+ // matures_at — it is NOT credited to the wallet now and writes NO commission_records yet. The
7269
+ // maturation cron (rewards-clearing-mature) re-validates the order is still genuinely closed, then
7270
+ // writes commission_records + credits the wallet. A return in-window reverses the pending row
7271
+ // (proportionally) — never paid, never clawed back. expires_at is a NOT-NULL filler = matures_at;
7272
+ // the escrow-expire cron ignores rows where matures_at IS NOT NULL (i.e. clearing rows).
7258
7273
  try {
7259
- const srcType = resolveSourceType(beneficiary);
7260
- db.prepare(`INSERT INTO commission_records (id, order_id, beneficiary_id, source_buyer_id, level, amount, rate, region, source, source_type)
7261
- VALUES (?,?,?,?,?,?,?,?,?,?)`)
7262
- .run(generateId('comm'), orderId, beneficiary, order.buyer_id, level, amount, rate, region, routeSource, srcType);
7263
- applyWalletDelta(db, beneficiary, { balance: amountU, earned: amountU });
7274
+ db.prepare(`INSERT INTO pending_commission_escrow (recipient_user_id, order_id, amount, attribution_path, status, created_at, expires_at, matures_at) VALUES (?,?,?,?,'pending',?,?,?)`)
7275
+ .run(beneficiary, orderId, amount, `L${level}`, nowMsClearing, maturesAtClearing, maturesAtClearing);
7264
7276
  }
7265
- catch (e) { /* UNIQUE 冲突 */ }
7277
+ catch (e) { /* UNIQUE (recipient,order,path) — settleCommission re-entry idempotent */ }
7266
7278
  }
7267
7279
  db.prepare("UPDATE orders SET settled_commission_at = datetime('now') WHERE id = ?").run(orderId);
7268
7280
  // redirected 恒为 0:commission 兜底全部入 commission_reserve,不再回流 global_fund(PV 资金)。
@@ -7378,6 +7390,10 @@ function settleOrder(orderId) {
7378
7390
  // 内部 try/catch 用于非关键 hook(settlePinRewards / metrics 更新)失败不回滚资金主流程
7379
7391
  db.transaction(() => {
7380
7392
  const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(orderId);
7393
+ if (order && order.payment_rail === 'direct_p2p') {
7394
+ settleDirectPayFeeAtCompletion(db, order, generateId('dpfr'));
7395
+ return;
7396
+ } // Rail1 直付:跳过 escrow 结算;平台费=链下应收(释放遗留模拟 stake + accrue,fail-closed 同原子边界)
7381
7397
  const total = order.total_amount;
7382
7398
  const isSecondhand = order.source === 'secondhand';
7383
7399
  const isInPerson = order.fulfillment_mode === 'in_person';
@@ -8704,72 +8720,10 @@ try {
8704
8720
  db.exec('CREATE INDEX IF NOT EXISTS idx_rewards_apps_action ON rewards_applications(user_id, action, created_at DESC)');
8705
8721
  }
8706
8722
  catch { }
8707
- // 5. pending_commission_escrow:opt-out promoter 待激活领取队列(§3.5b)
8708
- // PR-1c-b: order_id is NULLable so attribution_path='pv_pair' rows (which accrue
8709
- // across many orders) can record amount without inventing a fake order_id.
8710
- // L1/L2/L3 rows still carry a real order_id, enforced by the gate code path
8711
- // in settleCommission (not by NOT NULL constraint).
8712
- db.exec(`
8713
- CREATE TABLE IF NOT EXISTS pending_commission_escrow (
8714
- id INTEGER PRIMARY KEY AUTOINCREMENT,
8715
- recipient_user_id TEXT NOT NULL,
8716
- order_id TEXT, -- NULL for pv_pair (PR-1c-b)
8717
- amount REAL NOT NULL, -- WAZ amount
8718
- attribution_path TEXT NOT NULL, -- 'L1' | 'L2' | 'L3' | 'pv_pair' | etc.
8719
- status TEXT NOT NULL DEFAULT 'pending', -- 'pending' | 'settled' | 'expired'
8720
- created_at INTEGER NOT NULL,
8721
- expires_at INTEGER NOT NULL,
8722
- settled_at INTEGER,
8723
- expired_to_charity_at INTEGER,
8724
- FOREIGN KEY (recipient_user_id) REFERENCES users(id),
8725
- FOREIGN KEY (order_id) REFERENCES orders(id)
8726
- )
8727
- `);
8728
- (function migrateEscrowOrderIdNullable() {
8729
- const cols = db.prepare("PRAGMA table_info(pending_commission_escrow)").all();
8730
- const orderIdCol = cols.find(c => c.name === 'order_id');
8731
- if (!orderIdCol || orderIdCol.notnull === 0)
8732
- return; // already nullable (or table missing entirely)
8733
- console.log('[pc-escrow-migrate] order_id is NOT NULL — recreating table to allow NULL for pv_pair');
8734
- db.exec('PRAGMA foreign_keys = OFF');
8735
- db.transaction(() => {
8736
- db.exec(`
8737
- CREATE TABLE pending_commission_escrow_new (
8738
- id INTEGER PRIMARY KEY AUTOINCREMENT,
8739
- recipient_user_id TEXT NOT NULL,
8740
- order_id TEXT,
8741
- amount REAL NOT NULL,
8742
- attribution_path TEXT NOT NULL,
8743
- status TEXT NOT NULL DEFAULT 'pending',
8744
- created_at INTEGER NOT NULL,
8745
- expires_at INTEGER NOT NULL,
8746
- settled_at INTEGER,
8747
- expired_to_charity_at INTEGER,
8748
- FOREIGN KEY (recipient_user_id) REFERENCES users(id),
8749
- FOREIGN KEY (order_id) REFERENCES orders(id)
8750
- )
8751
- `);
8752
- db.exec('INSERT INTO pending_commission_escrow_new SELECT * FROM pending_commission_escrow');
8753
- db.exec('DROP TABLE pending_commission_escrow');
8754
- db.exec('ALTER TABLE pending_commission_escrow_new RENAME TO pending_commission_escrow');
8755
- })();
8756
- db.exec('PRAGMA foreign_keys = ON');
8757
- })();
8758
- try {
8759
- db.exec('CREATE INDEX IF NOT EXISTS idx_escrow_recipient ON pending_commission_escrow(recipient_user_id, status, expires_at)');
8760
- }
8761
- catch { }
8762
- try {
8763
- db.exec('CREATE INDEX IF NOT EXISTS idx_escrow_expiry ON pending_commission_escrow(status, expires_at)');
8764
- }
8765
- catch { }
8766
- // PR-1c-a: UNIQUE (recipient, order, path) defends against double-insert if settleCommission ever retries
8767
- // Note: NULL order_id (PR-1c-b pv_pair) is distinct in SQLite UNIQUE — idempotency for pv_pair relies
8768
- // on binary_score_records.settled_at instead (source-side dedup).
8769
- try {
8770
- db.exec('CREATE UNIQUE INDEX IF NOT EXISTS uniq_escrow_recipient_order_path ON pending_commission_escrow(recipient_user_id, order_id, attribution_path)');
8771
- }
8772
- catch { }
8723
+ // pending_commission_escrow schema (table + migration + indexes) lives in
8724
+ // initPendingCommissionEscrowSchema (early helper batch above) RFC-018 PR1 relocated it there so
8725
+ // all of its DDL is built once, never half-inline (fresh-DB silent-fail铁律). The backfill below
8726
+ // only READS the table; it still runs after the build (early batch precedes this line).
8773
8727
  // Codex #69 P1:pv_escrow_reserve(#1106 隔离负债账)回填历史 pending pv_pair escrow —— 按 delta 对账,不全量转。
8774
8728
  // 该列在 global_fund 上新增(line ~2319);#1106 之后新建的 pv_pair escrow 结算时【已】pv_escrow_reserve += wazAmount,
8775
8729
  // 但加列【之前】产生的 pending pv_pair 从没进过 reserve。升级窗口里两者混存。
@@ -8819,6 +8773,7 @@ const RFC002_PARAMS = [
8819
8773
  { key: 'rewards_opt_in.min_completed_orders', value: '1', type: 'number', description: 'RFC-002 §3.2:申请 rewards opt-in 的最小已完成订单数', category: 'rewards', min: 0, max: 100, metaRuleLocked: false },
8820
8774
  { key: 'rewards_opt_in.require_passkey', value: '1', type: 'number', description: 'RFC-002 §3.3:申请 / 关闭是否需 Passkey(1=必须,0=允许 password)— META-RULE LOCKED,降低需 60d meta-rule track', category: 'rewards', min: 0, max: 1, metaRuleLocked: true },
8821
8775
  { key: 'rewards_opt_in.escrow_days', value: '30', type: 'number', description: 'RFC-002 §3.5b:pending commission escrow 过期天数(过期后流入 charity_fund)', category: 'rewards', min: 7, max: 180, metaRuleLocked: false },
8776
+ { key: 'settlement.clearing_buffer_days', value: '2', type: 'number', description: 'RFC-018:佣金清算期 = 商品 return_days + 此 buffer(成熟才入钱包;窗内退货则冲销,永不 clawback)', category: 'rewards', min: 0, max: 30, metaRuleLocked: false },
8822
8777
  { key: 'rewards_opt_in.consent_delay_seconds', value: '8', type: 'number', description: 'RFC-002 §3.3:server-side 8s 反诱导延迟 — META-RULE LOCKED,降低需 60d meta-rule track', category: 'rewards', min: 0, max: 60, metaRuleLocked: true },
8823
8778
  { key: 'rewards_opt_in.reconfirm_grace_days', value: '14', type: 'number', description: 'RFC-002 §3.10:major consent 变更后用户重新确认 grace 期(过期 auto_downgrade)', category: 'rewards', min: 3, max: 90, metaRuleLocked: false },
8824
8779
  ];
@@ -8907,6 +8862,8 @@ app.listen(PORT, () => {
8907
8862
  startAutoDeactivateCron({ db, generateId, getProtocolParam });
8908
8863
  // #1090 RFC-002 PR-1c-a: escrow expire cron (every 1h)
8909
8864
  startEscrowExpireCron({ db, redirectToCommissionReserve });
8865
+ startDirectPayTimeoutCron({ db });
8866
+ startClearingMatureCron({ db }); // RFC-018: pay/hold matured clearing commission (logic in the cron module)
8910
8867
  // #1090 RFC-002 PR-3 slice 2: auto_downgrade cron (every 24h)
8911
8868
  // Triggered when a new major consent text is published; opted-in users
8912
8869
  // who don't reconfirm within reconfirm_grace_days (14d default) get