@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.
- package/README.md +3 -2
- package/README.zh-CN.md +7 -6
- package/dist/bond-refund-blockers.js +50 -0
- package/dist/bond-slash.js +100 -0
- package/dist/bond-terms.js +31 -0
- 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 +99 -0
- package/dist/direct-pay-cancel-refund.js +160 -0
- package/dist/direct-pay-compliance-ingress.js +145 -0
- package/dist/direct-pay-controls.js +136 -0
- package/dist/direct-pay-create.js +231 -0
- package/dist/direct-pay-deferral-quota.js +43 -0
- package/dist/direct-pay-disclosures.js +50 -0
- package/dist/direct-pay-eligibility.js +46 -0
- package/dist/direct-pay-fee-ar.js +241 -0
- package/dist/direct-pay-fee-prepay-request.js +80 -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-pay-returns.js +104 -0
- package/dist/direct-pay-stock.js +9 -0
- package/dist/direct-receive-account-qr.js +77 -0
- package/dist/direct-receive-accounts.js +82 -0
- package/dist/direct-receive-deferral.js +171 -0
- package/dist/direct-receive-deposits.js +365 -0
- package/dist/direct-receive-payment-instruction.js +26 -0
- package/dist/free-shipping.js +37 -0
- package/dist/fx-rates.js +78 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +777 -1
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +114 -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 +11 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +99 -32
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
- package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
- 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/platform-receive-accounts.js +94 -0
- package/dist/product-verification.js +93 -0
- package/dist/pwa/acp-feed.js +3 -2
- package/dist/pwa/arbitration-read-admin.js +37 -0
- package/dist/pwa/arbitrator-lifecycle.js +100 -0
- package/dist/pwa/contract-fingerprint.js +18 -0
- package/dist/pwa/direct-pay-guards.js +20 -0
- package/dist/pwa/direct-pay-order-redaction.js +42 -0
- package/dist/pwa/endpoint-actions.js +3 -0
- package/dist/pwa/human-presence.js +2 -6
- package/dist/pwa/public/app-account.js +9 -9
- package/dist/pwa/public/app-admin-disputes.js +55 -0
- package/dist/pwa/public/app-agent-appeal.js +90 -0
- package/dist/pwa/public/app-agent-approvals.js +93 -0
- package/dist/pwa/public/app-agent-pair.js +127 -0
- package/dist/pwa/public/app-ai.js +10 -10
- package/dist/pwa/public/app-arbitrator-admin.js +87 -0
- package/dist/pwa/public/app-arbitrator-entry.js +9 -0
- package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
- package/dist/pwa/public/app-bond-refund-ui.js +66 -0
- package/dist/pwa/public/app-bond-slash-ui.js +74 -0
- package/dist/pwa/public/app-bond-terms-ui.js +23 -0
- package/dist/pwa/public/app-bond-ui.js +108 -0
- package/dist/pwa/public/app-chat-poll.js +29 -0
- package/dist/pwa/public/app-contribution-hub.js +23 -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-cancel-refund.js +72 -0
- package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
- package/dist/pwa/public/app-direct-pay-copy.js +12 -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-history.js +39 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
- package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
- package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
- package/dist/pwa/public/app-direct-pay-memo.js +14 -0
- package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
- package/dist/pwa/public/app-direct-pay-pay.js +15 -0
- package/dist/pwa/public/app-direct-pay-paymodal.js +32 -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-reconcile.js +19 -0
- package/dist/pwa/public/app-direct-pay-returns.js +56 -0
- package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
- package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
- package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
- package/dist/pwa/public/app-direct-pay.js +226 -0
- package/dist/pwa/public/app-discover.js +20 -20
- package/dist/pwa/public/app-dispute-close-ui.js +38 -0
- package/dist/pwa/public/app-external-links.js +32 -0
- package/dist/pwa/public/app-free-shipping-ui.js +29 -0
- package/dist/pwa/public/app-gmv-rail-split.js +12 -0
- package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
- package/dist/pwa/public/app-listings.js +4 -4
- package/dist/pwa/public/app-mutual-cancel.js +54 -0
- package/dist/pwa/public/app-notif-templates-orders.js +43 -0
- package/dist/pwa/public/app-notif-templates.js +22 -0
- package/dist/pwa/public/app-order-accept-ui.js +158 -0
- package/dist/pwa/public/app-order-errors.js +50 -0
- package/dist/pwa/public/app-order-labels.js +19 -0
- package/dist/pwa/public/app-order-rail-filter.js +26 -0
- package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
- package/dist/pwa/public/app-poll-governor.js +22 -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 +10 -10
- package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
- package/dist/pwa/public/app-sale-regions-ui.js +38 -0
- package/dist/pwa/public/app-seller.js +5 -5
- package/dist/pwa/public/app-shop.js +19 -19
- package/dist/pwa/public/app-trade-tax-ui.js +33 -0
- package/dist/pwa/public/app.js +264 -295
- package/dist/pwa/public/i18n.js +1068 -197
- package/dist/pwa/public/index.html +58 -0
- package/dist/pwa/public/openapi.json +1845 -653
- package/dist/pwa/public/style.css +3 -0
- package/dist/pwa/routes/admin-analytics.js +4 -2
- package/dist/pwa/routes/admin-direct-receive-deposits.js +533 -0
- package/dist/pwa/routes/admin-protocol-params.js +16 -0
- package/dist/pwa/routes/admin-reports.js +31 -5
- package/dist/pwa/routes/agent-governance.js +1 -1
- package/dist/pwa/routes/agent-grants.js +253 -32
- package/dist/pwa/routes/analytics.js +2 -0
- package/dist/pwa/routes/arbitrator.js +67 -14
- package/dist/pwa/routes/bond-seller.js +162 -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-cancel-refund.js +111 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +79 -0
- package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
- package/dist/pwa/routes/direct-pay-returns.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +248 -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/disputes-read.js +20 -6
- package/dist/pwa/routes/disputes-write.js +91 -33
- package/dist/pwa/routes/external-anchors.js +4 -2
- package/dist/pwa/routes/fee-prepay-requests.js +66 -0
- package/dist/pwa/routes/fx.js +12 -0
- package/dist/pwa/routes/governance-onboarding.js +46 -8
- package/dist/pwa/routes/leaderboard.js +47 -9
- package/dist/pwa/routes/listings.js +2 -2
- package/dist/pwa/routes/logistics.js +6 -2
- package/dist/pwa/routes/manifests.js +38 -0
- package/dist/pwa/routes/me-data.js +5 -2
- package/dist/pwa/routes/mutual-cancel.js +62 -0
- package/dist/pwa/routes/orders-action.js +297 -16
- package/dist/pwa/routes/orders-create.js +21 -6
- package/dist/pwa/routes/orders-read.js +106 -8
- package/dist/pwa/routes/p2p-products.js +2 -2
- package/dist/pwa/routes/platform-receive-accounts.js +111 -0
- package/dist/pwa/routes/products-create.js +15 -4
- 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 +37 -3
- package/dist/pwa/routes/profile-identity.js +4 -2
- package/dist/pwa/routes/promoter.js +3 -0
- package/dist/pwa/routes/referral.js +4 -0
- package/dist/pwa/routes/returns.js +60 -4
- 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/seller-directpay-report.js +110 -0
- package/dist/pwa/routes/seller-quota.js +5 -1
- package/dist/pwa/routes/shipping-templates.js +219 -0
- package/dist/pwa/routes/shops.js +2 -1
- package/dist/pwa/routes/snf.js +4 -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 +3 -3
- package/dist/pwa/server.js +112 -132
- package/dist/runtime/agent-grant-scopes.js +69 -1
- package/dist/runtime/webaz-schema-helpers.js +161 -3
- package/dist/sale-regions.js +116 -0
- package/dist/shipping-templates.js +119 -0
- package/dist/store-verification.js +77 -0
- package/dist/trade-tax.js +99 -0
- package/dist/trade-terms.js +76 -0
- package/dist/version.js +1 -1
- package/package.json +128 -2
|
@@ -2,6 +2,9 @@ import { recordRepEvent } from '../../layer4-economics/L4-3-reputation/reputatio
|
|
|
2
2
|
// RFC-016 Phase 1 — 端点校验读/列表读 + 状态翻转/消息/通知单写 → async seam;
|
|
3
3
|
// executeReturnRefund 退款 db.transaction(钱+库存)与 escalate 建争议 tx 保持同步(Phase 3 迁 pg)。
|
|
4
4
|
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
|
|
5
|
+
// 直付(direct_p2p)退货:物流/协商骨架共用,退款执行环节换成场外握手(零资金零库存回补)。
|
|
6
|
+
// mark-refunded / confirm-refund 端点在 routes/direct-pay-returns.ts。
|
|
7
|
+
import { enterAwaitRefund, directPayReturnEscalatable, returnRefundRespondDays } from '../../direct-pay-returns.js';
|
|
5
8
|
const VALID_RETURN_REASONS = new Set(['quality', 'wrong_item', 'damaged', 'no_longer_needed', 'other']);
|
|
6
9
|
const RETURN_REASON_DEFAULT_LABEL = {
|
|
7
10
|
quality: '质量问题',
|
|
@@ -30,6 +33,26 @@ export function registerReturnsRoutes(app, deps) {
|
|
|
30
33
|
if (debited.changes !== 1)
|
|
31
34
|
throw new Error('INSUFFICIENT_SELLER_BALANCE');
|
|
32
35
|
db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(refundAmt, rr.buyer_id);
|
|
36
|
+
// RFC-018: reverse this order's in-window clearing commission, proportional to the refund.
|
|
37
|
+
// These pending rows were never paid (maturation is gated on matures_at), so this is a pure
|
|
38
|
+
// reversal — no clawback. matures_at IS NOT NULL targets clearing rows only; opt-out escrow
|
|
39
|
+
// rows (matures_at IS NULL) are untouched. Atomic with the refund.
|
|
40
|
+
const ordTotal = Number(db.prepare('SELECT total_amount FROM orders WHERE id = ?').get(rr.order_id)?.total_amount ?? 0);
|
|
41
|
+
const refundFrac = ordTotal > 0 ? Math.min(1, refundAmt / ordTotal) : 1;
|
|
42
|
+
if (refundFrac >= 1) {
|
|
43
|
+
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);
|
|
44
|
+
// RFC-018 PR4: a single FULL return is no longer a genuine sale — decrement the stored product
|
|
45
|
+
// completion_count (incremented at settleOrder). Idempotent: the return CAS'd to 'refunded'
|
|
46
|
+
// exactly once above, so this runs once. Partial refund stays a genuine sale (no decrement).
|
|
47
|
+
// Edge: multiple partial refunds cumulatively reaching full aren't caught here (each refundFrac<1),
|
|
48
|
+
// so the stored counter may transiently over-count by 1 — but the AUTHORITATIVE genuine count
|
|
49
|
+
// (genuineSalePredicate, cumulative SUM) is correct everywhere on-read, and the completion_count
|
|
50
|
+
// backfill (genuine-aware) self-heals the stored counter. completion_count is best-effort social proof.
|
|
51
|
+
db.prepare(`UPDATE products SET completion_count = MAX(0, COALESCE(completion_count,0) - 1) WHERE id = ?`).run(rr.product_id);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
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);
|
|
55
|
+
}
|
|
33
56
|
// 3. 恢复库存(CAS + 扣款成功后)
|
|
34
57
|
const ord = db.prepare('SELECT quantity, source, variant_id FROM orders WHERE id = ?').get(rr.order_id);
|
|
35
58
|
if (ord && ord.source !== 'secondhand') {
|
|
@@ -69,7 +92,7 @@ export function registerReturnsRoutes(app, deps) {
|
|
|
69
92
|
return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无购物功能');
|
|
70
93
|
const order = await dbOne(`
|
|
71
94
|
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
|
|
95
|
+
o.payment_rail, p.return_days, p.title as product_title
|
|
73
96
|
FROM orders o JOIN products p ON p.id = o.product_id
|
|
74
97
|
WHERE o.id = ?
|
|
75
98
|
`, [req.params.order_id]);
|
|
@@ -77,7 +100,10 @@ export function registerReturnsRoutes(app, deps) {
|
|
|
77
100
|
return void res.status(404).json({ error: '订单不存在' });
|
|
78
101
|
if (order.buyer_id !== user.id)
|
|
79
102
|
return void res.status(403).json({ error: '仅买家可申请退货' });
|
|
80
|
-
//
|
|
103
|
+
// 直付(Rail1)= 非托管:退货【物流/协商骨架照用】,但退款执行环节换成场外握手(src/direct-pay-returns.ts,
|
|
104
|
+
// accept → await_refund → 卖家 mark_refunded → 买家 Passkey confirm → refunded)。零资金零库存回补
|
|
105
|
+
// (已出库=B 类,退货验收上架);平台费应收的冲销仍由治理/admin 调整处理(非此路径)。
|
|
106
|
+
// P0-1: 只允许 completed 退货 — escrow 已结算(直付同口径:确认收货后)
|
|
81
107
|
if (order.status !== 'completed') {
|
|
82
108
|
return void res.status(400).json({ error: '仅订单完成后可申请退货(确认收货后)' });
|
|
83
109
|
}
|
|
@@ -90,7 +116,7 @@ export function registerReturnsRoutes(app, deps) {
|
|
|
90
116
|
return void res.status(400).json({ error: `已超过 ${returnDays} 天退货窗口` });
|
|
91
117
|
}
|
|
92
118
|
const existing = await dbOne(`
|
|
93
|
-
SELECT id, status FROM return_requests WHERE order_id = ? AND status IN ('pending', 'accepted') LIMIT 1
|
|
119
|
+
SELECT id, status FROM return_requests WHERE order_id = ? AND status IN ('pending', 'accepted', 'accepted_pickup_pending', 'picked_up', 'await_refund', 'refund_marked') LIMIT 1
|
|
94
120
|
`, [order.id]);
|
|
95
121
|
if (existing)
|
|
96
122
|
return void res.status(400).json({ error: `已存在退货请求 (${existing.status})` });
|
|
@@ -196,6 +222,10 @@ export function registerReturnsRoutes(app, deps) {
|
|
|
196
222
|
const response = req.body?.response ? String(req.body.response).slice(0, 500) : null;
|
|
197
223
|
if (decision === 'reject' && !response)
|
|
198
224
|
return void res.status(400).json({ error: '拒绝时必须填写说明' });
|
|
225
|
+
// 直付(direct_p2p):同意退货后不走 escrow 钱包退款,进入场外退款握手(await_refund)。取件流照用
|
|
226
|
+
// (accepted_pickup_pending → picked_up → received 时再进 await_refund)。
|
|
227
|
+
const railRow = await dbOne('SELECT payment_rail FROM orders WHERE id = ?', [rr.order_id]);
|
|
228
|
+
const isDirectPay = railRow?.payment_rail === 'direct_p2p';
|
|
199
229
|
if (decision === 'accept') {
|
|
200
230
|
if (Number(rr.pickup_requested) === 1) {
|
|
201
231
|
await dbRun(`UPDATE return_requests SET status = 'accepted_pickup_pending', seller_response = ? WHERE id = ?`, [response, rr.id]);
|
|
@@ -209,6 +239,16 @@ export function registerReturnsRoutes(app, deps) {
|
|
|
209
239
|
catch { }
|
|
210
240
|
return void res.json({ success: true, status: 'accepted_pickup_pending' });
|
|
211
241
|
}
|
|
242
|
+
if (isDirectPay) {
|
|
243
|
+
const r = enterAwaitRefund(db, { returnId: String(rr.id), fromStatus: 'pending', sellerResponse: response, messageId: generateId('rmsg') });
|
|
244
|
+
if (!r.ok)
|
|
245
|
+
return void res.status(409).json({ error: r.error, error_code: r.error_code });
|
|
246
|
+
try {
|
|
247
|
+
await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`, [generateId('ntf'), rr.buyer_id, 'direct_pay_return_await_refund', '✓ 退货已同意 · 等待卖家场外退款', '直付订单非托管:卖家将在协议外向你退款并声明,请核实到账后在订单页确认(需 Passkey)。卖家超期未退款可升级争议。', rr.order_id]);
|
|
248
|
+
}
|
|
249
|
+
catch { }
|
|
250
|
+
return void res.json({ success: true, status: 'await_refund' });
|
|
251
|
+
}
|
|
212
252
|
try {
|
|
213
253
|
executeReturnRefund(rr, response, 'pending');
|
|
214
254
|
}
|
|
@@ -377,6 +417,18 @@ export function registerReturnsRoutes(app, deps) {
|
|
|
377
417
|
if (rr.status !== 'picked_up')
|
|
378
418
|
return void res.status(400).json({ error: `当前状态 ${rr.status},不可确认(应在 picked_up 状态)` });
|
|
379
419
|
const note = req.body?.note ? String(req.body.note).slice(0, 300) : null;
|
|
420
|
+
// 直付:收到退货后进入场外退款握手(不走 escrow 钱包退款)
|
|
421
|
+
const railRow = await dbOne('SELECT payment_rail FROM orders WHERE id = ?', [rr.order_id]);
|
|
422
|
+
if (railRow?.payment_rail === 'direct_p2p') {
|
|
423
|
+
const r = enterAwaitRefund(db, { returnId: String(rr.id), fromStatus: 'picked_up', sellerResponse: note, messageId: generateId('rmsg') });
|
|
424
|
+
if (!r.ok)
|
|
425
|
+
return void res.status(409).json({ error: r.error, error_code: r.error_code });
|
|
426
|
+
try {
|
|
427
|
+
await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`, [generateId('ntf'), rr.buyer_id, 'direct_pay_return_await_refund', '✓ 卖家已收到退货 · 等待场外退款', '直付订单非托管:卖家将在协议外向你退款并声明,请核实到账后在订单页确认(需 Passkey)。卖家超期未退款可升级争议。', rr.order_id]);
|
|
428
|
+
}
|
|
429
|
+
catch { }
|
|
430
|
+
return void res.json({ success: true, status: 'await_refund' });
|
|
431
|
+
}
|
|
380
432
|
try {
|
|
381
433
|
executeReturnRefund(rr, note, 'picked_up');
|
|
382
434
|
}
|
|
@@ -449,7 +501,11 @@ export function registerReturnsRoutes(app, deps) {
|
|
|
449
501
|
return void res.status(404).json({ error: '不存在' });
|
|
450
502
|
if (rr.buyer_id !== user.id)
|
|
451
503
|
return void res.status(403).json({ error: '仅买家可升级' });
|
|
452
|
-
|
|
504
|
+
// 直付握手状态可升级:refund_marked 随时(声明≠到账);await_refund 超 respond 窗(卖家同意却不退款)
|
|
505
|
+
const isDpEsc = ['await_refund', 'refund_marked'].includes(String(rr.status)) && directPayReturnEscalatable(db, rr);
|
|
506
|
+
if (rr.status !== 'rejected' && rr.status !== 'pending' && !isDpEsc) {
|
|
507
|
+
if (rr.status === 'await_refund')
|
|
508
|
+
return void res.status(400).json({ error: `卖家有 ${returnRefundRespondDays(db)} 天场外退款窗口,超期后可升级` });
|
|
453
509
|
return void res.status(400).json({ error: `当前状态 ${rr.status},无法升级` });
|
|
454
510
|
}
|
|
455
511
|
if (rr.status === 'pending') {
|
|
@@ -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') {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js';
|
|
2
|
+
// 直付订单状态桶(与状态机一致):已完成(计提平台费)/ 在途 / 已取消·退款。
|
|
3
|
+
const COMPLETED = ['completed', 'confirmed'];
|
|
4
|
+
const IN_FLIGHT = ['pending_accept', 'direct_pay_window', 'accepted', 'shipped', 'picked_up', 'in_transit', 'delivered', 'payment_query', 'direct_expired_unconfirmed'];
|
|
5
|
+
const CLOSED_NEG = ['cancelled', 'expired', 'refunded_full', 'refunded_partial', 'dispute_dismissed', 'resolved_for_seller', 'fault_seller', 'fault_buyer', 'fault_logistics'];
|
|
6
|
+
const inList = (col, arr) => `${col} IN (${arr.map(() => '?').join(',')})`;
|
|
7
|
+
export function registerSellerDirectPayReportRoutes(app, deps) {
|
|
8
|
+
const { db, auth } = deps;
|
|
9
|
+
function requireSeller(req, res) {
|
|
10
|
+
const user = auth(req, res);
|
|
11
|
+
if (!user)
|
|
12
|
+
return null;
|
|
13
|
+
const roles = (() => { try {
|
|
14
|
+
return JSON.parse(String(user.roles ?? '[]'));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
} })();
|
|
19
|
+
if (user.role !== 'seller' && !(Array.isArray(roles) && roles.includes('seller'))) {
|
|
20
|
+
res.status(403).json({ error: '仅卖家可查看', error_code: 'SELLER_ONLY' });
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return user;
|
|
24
|
+
}
|
|
25
|
+
// GET /api/sellers/me/direct-pay-report?from=YYYY-MM-DD&to=YYYY-MM-DD
|
|
26
|
+
// from/to 均可选(闭区间,按日期比较 substr(created_at,1,10));返回汇总 + 按月 + 逐单(含平台费明细)。
|
|
27
|
+
app.get('/api/sellers/me/direct-pay-report', async (req, res) => {
|
|
28
|
+
const user = requireSeller(req, res);
|
|
29
|
+
if (!user)
|
|
30
|
+
return;
|
|
31
|
+
const sellerId = user.id;
|
|
32
|
+
const from = typeof req.query.from === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(req.query.from) ? req.query.from : null;
|
|
33
|
+
const to = typeof req.query.to === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(req.query.to) ? req.query.to : null;
|
|
34
|
+
const range = [];
|
|
35
|
+
const rp = [];
|
|
36
|
+
if (from) {
|
|
37
|
+
range.push('substr(o.created_at,1,10) >= ?');
|
|
38
|
+
rp.push(from);
|
|
39
|
+
}
|
|
40
|
+
if (to) {
|
|
41
|
+
range.push('substr(o.created_at,1,10) <= ?');
|
|
42
|
+
rp.push(to);
|
|
43
|
+
}
|
|
44
|
+
const rangeClause = range.length ? ' AND ' + range.join(' AND ') : '';
|
|
45
|
+
// ① 汇总(单查:订单数 / 销售额 / 各桶计数与已完成销售额)
|
|
46
|
+
const summary = (await dbOne(`
|
|
47
|
+
SELECT
|
|
48
|
+
COUNT(*) as order_count,
|
|
49
|
+
COALESCE(SUM(o.total_amount), 0) as gross_order_total,
|
|
50
|
+
SUM(CASE WHEN ${inList('o.status', COMPLETED)} THEN 1 ELSE 0 END) as completed_count,
|
|
51
|
+
COALESCE(SUM(CASE WHEN ${inList('o.status', COMPLETED)} THEN o.total_amount ELSE 0 END), 0) as completed_sales,
|
|
52
|
+
SUM(CASE WHEN ${inList('o.status', IN_FLIGHT)} THEN 1 ELSE 0 END) as in_flight_count,
|
|
53
|
+
SUM(CASE WHEN ${inList('o.status', CLOSED_NEG)} THEN 1 ELSE 0 END) as closed_count
|
|
54
|
+
FROM orders o
|
|
55
|
+
WHERE o.seller_id = ? AND o.payment_rail = 'direct_p2p'${rangeClause}
|
|
56
|
+
`, [...COMPLETED, ...COMPLETED, ...IN_FLIGHT, ...CLOSED_NEG, sellerId, ...rp]));
|
|
57
|
+
// ② 区间内已计提平台费合计(逐单应收 join 订单,按订单下单日筛)
|
|
58
|
+
const feeAgg = (await dbOne(`
|
|
59
|
+
SELECT COALESCE(SUM(r.amount), 0) as fee_accrued, COUNT(*) as fee_count
|
|
60
|
+
FROM direct_pay_fee_receivables r JOIN orders o ON o.id = r.order_id
|
|
61
|
+
WHERE o.seller_id = ? AND o.payment_rail = 'direct_p2p'${rangeClause}
|
|
62
|
+
`, [sellerId, ...rp]));
|
|
63
|
+
// ③ 按月(最多 24 个月)—— 订单总额(含各状态)与完成单销售额分列,不混
|
|
64
|
+
const byMonth = await dbAll(`
|
|
65
|
+
SELECT substr(o.created_at, 1, 7) as month, COUNT(*) as order_count,
|
|
66
|
+
COALESCE(SUM(o.total_amount), 0) as gross_order_total,
|
|
67
|
+
COALESCE(SUM(CASE WHEN ${inList('o.status', COMPLETED)} THEN o.total_amount ELSE 0 END), 0) as completed_sales
|
|
68
|
+
FROM orders o
|
|
69
|
+
WHERE o.seller_id = ? AND o.payment_rail = 'direct_p2p'${rangeClause}
|
|
70
|
+
GROUP BY month ORDER BY month DESC LIMIT 24
|
|
71
|
+
`, [...COMPLETED, sellerId, ...rp]);
|
|
72
|
+
// ④ 逐单明细(含平台费:LEFT JOIN 应收表 —— 未完成单尚无 fee 行,fee 为 null)。上限 500。
|
|
73
|
+
const LIMIT = 500;
|
|
74
|
+
const rows = await dbAll(`
|
|
75
|
+
SELECT o.id, o.created_at, o.status, o.total_amount, o.ship_to_region, p.title as product_title,
|
|
76
|
+
r.amount as fee_amount, r.accrued_at as fee_accrued_at
|
|
77
|
+
FROM orders o
|
|
78
|
+
LEFT JOIN products p ON p.id = o.product_id
|
|
79
|
+
LEFT JOIN direct_pay_fee_receivables r ON r.order_id = o.id
|
|
80
|
+
WHERE o.seller_id = ? AND o.payment_rail = 'direct_p2p'${rangeClause}
|
|
81
|
+
ORDER BY o.created_at DESC LIMIT ?
|
|
82
|
+
`, [sellerId, ...rp, LIMIT + 1]);
|
|
83
|
+
const truncated = rows.length > LIMIT;
|
|
84
|
+
if (truncated)
|
|
85
|
+
rows.length = LIMIT;
|
|
86
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
87
|
+
res.json({
|
|
88
|
+
range: { from, to },
|
|
89
|
+
currency_note: '销售额=下单计价币(买家应付金额);平台服务费=USDC。两者不同币种,分列展示。',
|
|
90
|
+
summary: {
|
|
91
|
+
order_count: Number(summary.order_count) || 0,
|
|
92
|
+
gross_order_total: Number(summary.gross_order_total) || 0, // 所有状态订单总额(含在途/已关闭)—— 非真实销售
|
|
93
|
+
completed_count: Number(summary.completed_count) || 0,
|
|
94
|
+
completed_sales: Number(summary.completed_sales) || 0, // 真实销售(仅 completed/confirmed)
|
|
95
|
+
in_flight_count: Number(summary.in_flight_count) || 0,
|
|
96
|
+
closed_count: Number(summary.closed_count) || 0,
|
|
97
|
+
fee_accrued_total: Number(feeAgg.fee_accrued) || 0, // 区间已计提平台费(USDC)
|
|
98
|
+
fee_order_count: Number(feeAgg.fee_count) || 0,
|
|
99
|
+
},
|
|
100
|
+
by_month: byMonth.map(m => ({ month: m.month, order_count: Number(m.order_count) || 0, gross_order_total: Number(m.gross_order_total) || 0, completed_sales: Number(m.completed_sales) || 0 })),
|
|
101
|
+
orders: rows.map(r => ({
|
|
102
|
+
id: r.id, created_at: r.created_at, status: r.status, total_amount: Number(r.total_amount) || 0,
|
|
103
|
+
ship_to_region: r.ship_to_region || null, product_title: r.product_title || null,
|
|
104
|
+
fee_amount: r.fee_amount == null ? null : Number(r.fee_amount), // null=未完成尚未计提
|
|
105
|
+
fee_accrued_at: r.fee_accrued_at || null,
|
|
106
|
+
})),
|
|
107
|
+
truncated,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -45,7 +45,7 @@ export function registerSellerQuotaRoutes(app, deps) {
|
|
|
45
45
|
const d30 = fmt(new Date(now - 30 * 86400000));
|
|
46
46
|
const d60 = fmt(new Date(now - 60 * 86400000));
|
|
47
47
|
const orders = await dbAll(`
|
|
48
|
-
SELECT o.id, o.product_id, o.buyer_id, o.status, o.total_amount, o.created_at,
|
|
48
|
+
SELECT o.id, o.product_id, o.buyer_id, o.status, o.total_amount, o.created_at, o.payment_rail,
|
|
49
49
|
COALESCE(p.title, '已下架商品') as product_title,
|
|
50
50
|
COALESCE(ub.name, '匿名') as buyer_name
|
|
51
51
|
FROM orders o
|
|
@@ -60,6 +60,8 @@ export function registerSellerQuotaRoutes(app, deps) {
|
|
|
60
60
|
const inLast30 = orders.filter(o => o.created_at >= d30);
|
|
61
61
|
const inPrev30 = orders.filter(o => o.created_at < d30);
|
|
62
62
|
const gmv = (arr) => arr.filter(o => completedStatuses.has(o.status)).reduce((s, o) => s + Number(o.total_amount || 0), 0);
|
|
63
|
+
// GMV 按支付轨拆分:托管=平台真实托管收入,直接收款=场外收款(平台不经手)—— 不再混算(诚实口径)
|
|
64
|
+
const gmvRail = (arr, rail) => arr.filter(o => completedStatuses.has(o.status) && (rail === 'direct_p2p' ? o.payment_rail === 'direct_p2p' : (o.payment_rail || 'escrow') === 'escrow')).reduce((s, o) => s + Number(o.total_amount || 0), 0);
|
|
63
65
|
const curGmv = gmv(inLast30);
|
|
64
66
|
const prevGmv = gmv(inPrev30);
|
|
65
67
|
const curCount = inLast30.length;
|
|
@@ -116,6 +118,8 @@ export function registerSellerQuotaRoutes(app, deps) {
|
|
|
116
118
|
period_days: 30,
|
|
117
119
|
summary: {
|
|
118
120
|
gmv: curGmv,
|
|
121
|
+
gmv_escrow: gmvRail(inLast30, 'escrow'),
|
|
122
|
+
gmv_direct_pay: gmvRail(inLast30, 'direct_p2p'),
|
|
119
123
|
order_count: curCount,
|
|
120
124
|
completed_count: completedOrders30.length,
|
|
121
125
|
aov,
|