@seasonkoh/webaz 0.1.27 → 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.
- package/README.md +4 -3
- package/README.zh-CN.md +7 -6
- package/dist/currency.js +16 -0
- package/dist/deposit-rails.js +52 -0
- package/dist/direct-pay-aml-monitor.js +67 -0
- package/dist/direct-pay-aml-review.js +40 -0
- package/dist/direct-pay-base-bond-entry.js +5 -0
- package/dist/direct-pay-bond-rail-clearance.js +94 -0
- package/dist/direct-pay-compliance-ingress.js +145 -0
- package/dist/direct-pay-controls.js +136 -0
- package/dist/direct-pay-create.js +162 -0
- package/dist/direct-pay-deferral-quota.js +43 -0
- package/dist/direct-pay-disclosures.js +44 -0
- package/dist/direct-pay-eligibility.js +46 -0
- package/dist/direct-pay-fee-ar.js +241 -0
- package/dist/direct-pay-launch-readiness.js +108 -0
- package/dist/direct-pay-launch-summary.js +68 -0
- package/dist/direct-pay-ledger.js +94 -0
- package/dist/direct-receive-account-qr.js +77 -0
- package/dist/direct-receive-accounts.js +82 -0
- package/dist/direct-receive-deferral.js +142 -0
- package/dist/direct-receive-deposits.js +260 -0
- package/dist/direct-receive-payment-instruction.js +26 -0
- package/dist/fx-rates.js +71 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +531 -1
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +41 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +80 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +251 -69
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
- package/dist/ledger.js +12 -3
- package/dist/mcp.js +23 -4
- package/dist/merchant-bond-domain.js +64 -0
- package/dist/merchant-bond-exposure.js +78 -0
- package/dist/merchant-bond-watcher.js +26 -0
- package/dist/payment-rails.js +29 -0
- package/dist/product-verification.js +93 -0
- package/dist/pwa/acp-feed.js +3 -2
- package/dist/pwa/contract-fingerprint.js +3 -0
- package/dist/pwa/direct-pay-guards.js +20 -0
- package/dist/pwa/direct-pay-order-redaction.js +24 -0
- package/dist/pwa/endpoint-actions.js +3 -0
- package/dist/pwa/public/app-account.js +977 -0
- package/dist/pwa/public/app-admin.js +608 -0
- package/dist/pwa/public/app-agents.js +63 -0
- package/dist/pwa/public/app-ai.js +2162 -0
- package/dist/pwa/public/app-chat-poll.js +29 -0
- package/dist/pwa/public/app-contribution.js +836 -0
- package/dist/pwa/public/app-create-kinds.js +17 -0
- package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
- package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
- package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
- package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
- package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
- package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
- package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
- package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
- package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
- package/dist/pwa/public/app-direct-pay.js +227 -0
- package/dist/pwa/public/app-discover.js +1296 -0
- package/dist/pwa/public/app-external-links.js +32 -0
- package/dist/pwa/public/app-listings.js +226 -0
- package/dist/pwa/public/app-prelaunch-waz.js +39 -0
- package/dist/pwa/public/app-price.js +55 -0
- package/dist/pwa/public/app-product-media.js +15 -0
- package/dist/pwa/public/app-profile.js +1692 -0
- package/dist/pwa/public/app-seller.js +199 -0
- package/dist/pwa/public/app-shop.js +1145 -0
- package/dist/pwa/public/app.js +13388 -22277
- package/dist/pwa/public/i18n.js +413 -196
- package/dist/pwa/public/index.html +27 -0
- package/dist/pwa/public/openapi.json +579 -1
- package/dist/pwa/routes/admin-analytics.js +4 -2
- package/dist/pwa/routes/admin-direct-receive-deposits.js +321 -0
- package/dist/pwa/routes/agent-grants.js +255 -0
- package/dist/pwa/routes/buyer-feeds.js +2 -1
- package/dist/pwa/routes/chat.js +6 -1
- package/dist/pwa/routes/dashboards.js +2 -1
- package/dist/pwa/routes/direct-pay-availability.js +196 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +71 -0
- package/dist/pwa/routes/direct-receive-accounts.js +155 -0
- package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
- package/dist/pwa/routes/fx.js +12 -0
- package/dist/pwa/routes/leaderboard.js +47 -9
- package/dist/pwa/routes/listings.js +2 -2
- package/dist/pwa/routes/logistics.js +4 -0
- package/dist/pwa/routes/manifests.js +38 -0
- package/dist/pwa/routes/me-data.js +5 -2
- package/dist/pwa/routes/orders-action.js +117 -9
- package/dist/pwa/routes/orders-create.js +4 -0
- package/dist/pwa/routes/orders-read.js +36 -0
- package/dist/pwa/routes/p2p-products.js +2 -2
- package/dist/pwa/routes/products-create.js +5 -3
- package/dist/pwa/routes/products-links.js +34 -0
- package/dist/pwa/routes/products-list.js +2 -1
- package/dist/pwa/routes/products-update.js +22 -2
- package/dist/pwa/routes/promoter.js +3 -0
- package/dist/pwa/routes/referral.js +4 -0
- package/dist/pwa/routes/returns.js +26 -1
- package/dist/pwa/routes/rewards-apply.js +10 -5
- package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
- package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
- package/dist/pwa/routes/shops.js +2 -1
- package/dist/pwa/routes/url-claim.js +2 -2
- package/dist/pwa/routes/users-public.js +8 -0
- package/dist/pwa/routes/wallet-read.js +3 -0
- package/dist/pwa/routes/webauthn.js +7 -2
- package/dist/pwa/server-schema.js +9 -0
- package/dist/pwa/server.js +261 -1706
- package/dist/runtime/agent-grant-scopes.js +128 -0
- package/dist/runtime/agent-grant-verifier.js +67 -0
- package/dist/runtime/agent-pairing.js +60 -0
- package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
- package/dist/runtime/webaz-schema-helpers.js +1952 -0
- package/dist/store-verification.js +77 -0
- package/dist/version.js +1 -1
- package/package.json +80 -2
|
@@ -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)
|
|
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)
|
|
@@ -134,6 +135,25 @@ export function registerProductsUpdateRoutes(app, deps) {
|
|
|
134
135
|
console.error('[wishlist price-drop notify]', e);
|
|
135
136
|
}
|
|
136
137
|
}
|
|
138
|
+
// 反作弊(PR-⑥):任何【买家可见的商品身份字段】变更 → 作废该商品的直付逐品验证,强制重新验证,防"先验证商品 A,
|
|
139
|
+
// 再改成商品 B"绕过逐品硬门。external-link 变更在 products-links 路由另行作废。fail-soft,不阻断编辑。
|
|
140
|
+
// ⚠️ 必须含 i18n_titles/i18n_descs:formatProductForAgent 按 Accept-Language 把 title/description 换成对应语言,
|
|
141
|
+
// 只改 en/ja/ko 标题描述同样是"换货"(非中文买家看到的就变了);并含 brand/model(商品身份)。
|
|
142
|
+
const newBrand = brand ?? product.brand;
|
|
143
|
+
const newModel = model ?? product.model;
|
|
144
|
+
const eq = (a, b) => String(a ?? '') === String(b ?? '');
|
|
145
|
+
const materialChanged = !eq(newTitle, product.title) || !eq(newDesc, product.description)
|
|
146
|
+
|| Number(newPrice) !== Number(product.price) || !eq(specsJson, product.specs)
|
|
147
|
+
|| !eq(newI18nTitles, product.i18n_titles) || !eq(newI18nDescs, product.i18n_descs)
|
|
148
|
+
|| !eq(newBrand, product.brand) || !eq(newModel, product.model);
|
|
149
|
+
if (materialChanged) {
|
|
150
|
+
try {
|
|
151
|
+
invalidateProductVerification(db, String(req.params.id));
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
console.error('[product-verify invalidate]', e);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
137
157
|
res.json({ success: true });
|
|
138
158
|
});
|
|
139
159
|
}
|
|
@@ -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
|
});
|
|
@@ -30,6 +30,26 @@ export function registerReturnsRoutes(app, deps) {
|
|
|
30
30
|
if (debited.changes !== 1)
|
|
31
31
|
throw new Error('INSUFFICIENT_SELLER_BALANCE');
|
|
32
32
|
db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(refundAmt, rr.buyer_id);
|
|
33
|
+
// RFC-018: reverse this order's in-window clearing commission, proportional to the refund.
|
|
34
|
+
// These pending rows were never paid (maturation is gated on matures_at), so this is a pure
|
|
35
|
+
// reversal — no clawback. matures_at IS NOT NULL targets clearing rows only; opt-out escrow
|
|
36
|
+
// rows (matures_at IS NULL) are untouched. Atomic with the refund.
|
|
37
|
+
const ordTotal = Number(db.prepare('SELECT total_amount FROM orders WHERE id = ?').get(rr.order_id)?.total_amount ?? 0);
|
|
38
|
+
const refundFrac = ordTotal > 0 ? Math.min(1, refundAmt / ordTotal) : 1;
|
|
39
|
+
if (refundFrac >= 1) {
|
|
40
|
+
db.prepare(`UPDATE pending_commission_escrow SET status='reversed' WHERE order_id = ? AND status='pending' AND matures_at IS NOT NULL`).run(rr.order_id);
|
|
41
|
+
// RFC-018 PR4: a single FULL return is no longer a genuine sale — decrement the stored product
|
|
42
|
+
// completion_count (incremented at settleOrder). Idempotent: the return CAS'd to 'refunded'
|
|
43
|
+
// exactly once above, so this runs once. Partial refund stays a genuine sale (no decrement).
|
|
44
|
+
// Edge: multiple partial refunds cumulatively reaching full aren't caught here (each refundFrac<1),
|
|
45
|
+
// so the stored counter may transiently over-count by 1 — but the AUTHORITATIVE genuine count
|
|
46
|
+
// (genuineSalePredicate, cumulative SUM) is correct everywhere on-read, and the completion_count
|
|
47
|
+
// backfill (genuine-aware) self-heals the stored counter. completion_count is best-effort social proof.
|
|
48
|
+
db.prepare(`UPDATE products SET completion_count = MAX(0, COALESCE(completion_count,0) - 1) WHERE id = ?`).run(rr.product_id);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
db.prepare(`UPDATE pending_commission_escrow SET amount = amount * ? WHERE order_id = ? AND status='pending' AND matures_at IS NOT NULL`).run(1 - refundFrac, rr.order_id);
|
|
52
|
+
}
|
|
33
53
|
// 3. 恢复库存(CAS + 扣款成功后)
|
|
34
54
|
const ord = db.prepare('SELECT quantity, source, variant_id FROM orders WHERE id = ?').get(rr.order_id);
|
|
35
55
|
if (ord && ord.source !== 'secondhand') {
|
|
@@ -69,7 +89,7 @@ export function registerReturnsRoutes(app, deps) {
|
|
|
69
89
|
return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无购物功能');
|
|
70
90
|
const order = await dbOne(`
|
|
71
91
|
SELECT o.id, o.buyer_id, o.seller_id, o.product_id, o.status, o.total_amount, o.created_at, o.updated_at,
|
|
72
|
-
p.return_days, p.title as product_title
|
|
92
|
+
o.payment_rail, p.return_days, p.title as product_title
|
|
73
93
|
FROM orders o JOIN products p ON p.id = o.product_id
|
|
74
94
|
WHERE o.id = ?
|
|
75
95
|
`, [req.params.order_id]);
|
|
@@ -77,6 +97,11 @@ export function registerReturnsRoutes(app, deps) {
|
|
|
77
97
|
return void res.status(404).json({ error: '订单不存在' });
|
|
78
98
|
if (order.buyer_id !== user.id)
|
|
79
99
|
return void res.status(403).json({ error: '仅买家可申请退货' });
|
|
100
|
+
// 直付(Rail1)= 非托管、refund:none —— WebAZ 不持货款,退款经现有 escrow 钱包路径会错误移动无关 WAZ。
|
|
101
|
+
// 故 direct_p2p 不走退货/退款流程(买家不满走争议=信誉处理);平台费应收的冲销由治理/admin 调整处理(非此路径)。
|
|
102
|
+
if (order.payment_rail === 'direct_p2p') {
|
|
103
|
+
return void res.status(400).json({ error: '直付(非托管)订单不支持退款流程;如有问题请发起争议', error_code: 'DIRECT_PAY_NO_REFUND' });
|
|
104
|
+
}
|
|
80
105
|
// P0-1: 只允许 completed 退货 — escrow 已结算
|
|
81
106
|
if (order.status !== 'completed') {
|
|
82
107
|
return void res.status(400).json({ error: '仅订单完成后可申请退货(确认收货后)' });
|
|
@@ -37,8 +37,10 @@ export function registerRewardsApplyRoutes(app, deps) {
|
|
|
37
37
|
missing.push(`completed_orders ${completedOrders}/${minOrders}`);
|
|
38
38
|
if (requirePasskey === 1 && passkeyCount === 0)
|
|
39
39
|
missing.push('passkey_not_registered');
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// RFC-018: matures_at IS NULL = opt-out escrow (claimable on activation). Clearing rows
|
|
41
|
+
// (matures_at NOT NULL) auto-mature via rewards-clearing-mature and must NOT be drained here.
|
|
42
|
+
const pending = (await dbOne("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'pending' AND matures_at IS NULL", [userId]));
|
|
43
|
+
const expired = (await dbOne("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'expired' AND matures_at IS NULL", [userId]));
|
|
42
44
|
res.json({
|
|
43
45
|
state,
|
|
44
46
|
opted_in: optIn === 1,
|
|
@@ -138,12 +140,15 @@ export function registerRewardsApplyRoutes(app, deps) {
|
|
|
138
140
|
VALUES (?, 'activate', ?, ?, ?, ?, ?, ?, ?)`)
|
|
139
141
|
.run(userId, consent_version, currentMajor.hash, webauthn_token || null, // store gate_token id as audit cross-ref to webauthn_gate_tokens
|
|
140
142
|
requirePasskey === 1 ? 'passkey' : 'password', sha256_hex(req.ip || '').slice(0, 16), sha256_hex(String(req.headers['user-agent'] || '')).slice(0, 16), now);
|
|
141
|
-
// Activate batch settle: drain
|
|
143
|
+
// Activate batch settle: drain OPT-OUT escrow to wallet. RFC-018: matures_at IS NULL excludes
|
|
144
|
+
// clearing rows — those are NOT claimable on opt-in; they auto-mature after the return window
|
|
145
|
+
// (rewards-clearing-mature). Draining them here would pay commission early and defeat the
|
|
146
|
+
// clearing window's reversal guarantee (a returned order could no longer be reversed → clawback).
|
|
142
147
|
const pending = db.prepare(`SELECT id, amount, attribution_path FROM pending_commission_escrow
|
|
143
|
-
WHERE recipient_user_id = ? AND status = 'pending' AND expires_at > ?`).all(userId, now);
|
|
148
|
+
WHERE recipient_user_id = ? AND status = 'pending' AND matures_at IS NULL AND expires_at > ?`).all(userId, now);
|
|
144
149
|
let total = 0;
|
|
145
150
|
for (const p of pending) {
|
|
146
|
-
const upd = db.prepare(`UPDATE pending_commission_escrow SET status='settled', settled_at=? WHERE id=? AND status='pending'`).run(now, p.id);
|
|
151
|
+
const upd = db.prepare(`UPDATE pending_commission_escrow SET status='settled', settled_at=? WHERE id=? AND status='pending' AND matures_at IS NULL`).run(now, p.id);
|
|
147
152
|
if (upd.changes === 0)
|
|
148
153
|
continue; // race: expire cron took it
|
|
149
154
|
db.prepare(`UPDATE wallets SET balance = balance + ?, earned = earned + ? WHERE user_id = ?`).run(p.amount, p.amount, userId);
|
|
@@ -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') {
|
package/dist/pwa/routes/shops.js
CHANGED
|
@@ -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
|
|
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
|
});
|
|
@@ -3,8 +3,9 @@ import { randomBytes } from 'node:crypto';
|
|
|
3
3
|
// RFC-004 体验补:绑定 Passkey 后,追溯补发此前"已受理但无锚点跳过"的建设信誉。
|
|
4
4
|
import { grantPendingAnchorCredits } from '../../layer2-business/L2-8-feedback/build-feedback-engine.js';
|
|
5
5
|
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
6
|
+
import { registerAgentGrantsRoutes } from './agent-grants.js'; // RFC-020 PR-B — Passkey-domain delegation grants (keeps server.ts untouched)
|
|
6
7
|
export function registerWebauthnRoutes(app, deps) {
|
|
7
|
-
const { db, auth, generateId, rpId, rpName, origin, challengeTtlMs, gateTtlMs, invalidateAgentRiskCacheForUser, requireHumanPresence } = deps;
|
|
8
|
+
const { db, auth, generateId, rateLimitOk, rpId, rpName, origin, challengeTtlMs, gateTtlMs, invalidateAgentRiskCacheForUser, requireHumanPresence } = deps;
|
|
8
9
|
// 1. 注册:start — 生成 challenge + 选项
|
|
9
10
|
app.post('/api/webauthn/register/start', async (req, res) => {
|
|
10
11
|
const user = auth(req, res);
|
|
@@ -73,7 +74,7 @@ export function registerWebauthnRoutes(app, deps) {
|
|
|
73
74
|
if (!user)
|
|
74
75
|
return;
|
|
75
76
|
const purpose = String(req.body?.purpose || '').trim();
|
|
76
|
-
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']);
|
|
77
78
|
if (!allowed.has(purpose))
|
|
78
79
|
return void res.status(400).json({ error: 'invalid purpose' });
|
|
79
80
|
const purpose_data = req.body?.purpose_data ?? null;
|
|
@@ -192,4 +193,8 @@ export function registerWebauthnRoutes(app, deps) {
|
|
|
192
193
|
await dbRun('UPDATE users SET webauthn_required_for_withdraw = ? WHERE id = ?', [required, user.id]);
|
|
193
194
|
res.json({ success: true, required_for_withdraw: !!required });
|
|
194
195
|
});
|
|
196
|
+
// RFC-020 PR-B — agent delegation grants (issue/read/revoke). Co-registered with the
|
|
197
|
+
// Passkey security routes so the money-dense server.ts stays untouched. Safe scopes
|
|
198
|
+
// only; risk scopes default-hard-reject. Reuses db/auth/generateId from WebauthnDeps.
|
|
199
|
+
registerAgentGrantsRoutes(app, { db, auth, generateId, rateLimitOk });
|
|
195
200
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility re-export.
|
|
3
|
+
*
|
|
4
|
+
* The pure idempotent schema helpers were relocated to a neutral top-level
|
|
5
|
+
* module (src/runtime/webaz-schema-helpers.ts) so the PWA boot path and the MCP
|
|
6
|
+
* runtime schema composition root can share ONE source. This file keeps the
|
|
7
|
+
* historical `./server-schema.js` import path working for src/pwa/server.ts.
|
|
8
|
+
*/
|
|
9
|
+
export * from '../runtime/webaz-schema-helpers.js';
|