@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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
|
|
2
|
+
import { restorePreShipDirectPayStock } from '../../direct-pay-stock.js';
|
|
3
|
+
import { createNotification } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
4
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
|
|
5
|
+
import { toUnits, toDecimal } from '../../money.js';
|
|
6
|
+
import { buildPayableSnapshot } from '../../direct-pay-create.js';
|
|
7
|
+
import { safeRunDirectPayAmlMonitor } from '../../direct-pay-aml-monitor.js';
|
|
8
|
+
import { updateSnapshotShippingQuote } from '../../trade-terms.js'; // S0:询价确认后补记条款快照 shipping 槽(quote_pending→quote,fail-soft)
|
|
9
|
+
export function registerDirectPayPendingAcceptRoutes(app, deps) {
|
|
10
|
+
const { db, auth, errorRes, getProtocolParam } = deps;
|
|
11
|
+
async function loadPendingOrder(req, res) {
|
|
12
|
+
const order = await dbOne('SELECT id, buyer_id, seller_id, status, payment_rail, product_id, quantity, total_amount, shipping_quote_required, shipping_quote_fee, shipping_quote_est_days, direct_pay_per_tx_cap_units_snapshot, direct_pay_account_snapshot FROM orders WHERE id = ?', [req.params.id]);
|
|
13
|
+
if (!order) {
|
|
14
|
+
errorRes(res, 404, 'ORDER_NOT_FOUND', '订单不存在');
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (order.payment_rail !== 'direct_p2p') {
|
|
18
|
+
errorRes(res, 409, 'NOT_DIRECT_PAY', '仅直付订单有待接单阶段');
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (order.status !== 'pending_accept') {
|
|
22
|
+
errorRes(res, 409, 'NOT_PENDING_ACCEPT', `当前状态 ${order.status},不在待接单阶段`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return order;
|
|
26
|
+
}
|
|
27
|
+
const notify = (userId, orderId, type, title, body, tpl) => {
|
|
28
|
+
try {
|
|
29
|
+
createNotification(db, userId, orderId, type, title, body, tpl);
|
|
30
|
+
}
|
|
31
|
+
catch { /* 通知失败不阻断 */ }
|
|
32
|
+
};
|
|
33
|
+
// ── 接单模式设置(卖家):店铺默认 + 单品覆盖。值 ∈ {'auto','manual',null=清除(单品回落店铺默认/店铺回落 auto)}。
|
|
34
|
+
// 只影响【之后】的新订单(下单时快照);对两轨生效(escrow 'auto'=付款后系统自动接单;dp 'manual'=先进 pending_accept)。
|
|
35
|
+
const MODE_OK = (v) => v === null || v === 'auto' || v === 'manual';
|
|
36
|
+
app.post('/api/seller/accept-mode', async (req, res) => {
|
|
37
|
+
const user = auth(req, res);
|
|
38
|
+
if (!user)
|
|
39
|
+
return;
|
|
40
|
+
if (user.role !== 'seller')
|
|
41
|
+
return void errorRes(res, 403, 'SELLER_ONLY', '仅卖家可设置接单模式');
|
|
42
|
+
const b = req.body || {};
|
|
43
|
+
const touched = {};
|
|
44
|
+
if ('store_accept_mode' in b) {
|
|
45
|
+
if (!MODE_OK(b.store_accept_mode))
|
|
46
|
+
return void errorRes(res, 400, 'BAD_ACCEPT_MODE', "store_accept_mode 只允许 'auto'|'manual'|null");
|
|
47
|
+
await dbRun('UPDATE users SET store_accept_mode = ? WHERE id = ?', [b.store_accept_mode, user.id]);
|
|
48
|
+
touched.store_accept_mode = b.store_accept_mode;
|
|
49
|
+
}
|
|
50
|
+
if ('product_id' in b || 'accept_mode' in b) {
|
|
51
|
+
if (!b.product_id)
|
|
52
|
+
return void errorRes(res, 400, 'MISSING_PRODUCT_ID', '设置单品接单模式须带 product_id');
|
|
53
|
+
if (!MODE_OK(b.accept_mode))
|
|
54
|
+
return void errorRes(res, 400, 'BAD_ACCEPT_MODE', "accept_mode 只允许 'auto'|'manual'|null");
|
|
55
|
+
const p = await dbOne('SELECT seller_id FROM products WHERE id = ?', [b.product_id]);
|
|
56
|
+
if (!p)
|
|
57
|
+
return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
|
|
58
|
+
if (p.seller_id !== user.id)
|
|
59
|
+
return void errorRes(res, 403, 'NOT_PRODUCT_OWNER', '只能设置自己商品的接单模式');
|
|
60
|
+
await dbRun('UPDATE products SET accept_mode = ? WHERE id = ?', [b.accept_mode, b.product_id]);
|
|
61
|
+
touched.product_accept_mode = b.accept_mode;
|
|
62
|
+
}
|
|
63
|
+
if (Object.keys(touched).length === 0)
|
|
64
|
+
return void errorRes(res, 400, 'NOTHING_TO_SET', '未提供任何设置项');
|
|
65
|
+
return void res.json({ success: true, ...touched });
|
|
66
|
+
});
|
|
67
|
+
// 卖家确认接单 → 开付款窗口(deadline 此刻起表;收款信息此刻起买家可见 —— orders-read 状态门放行)
|
|
68
|
+
app.post('/api/orders/:id/pending-accept/accept', async (req, res) => {
|
|
69
|
+
const user = auth(req, res);
|
|
70
|
+
if (!user)
|
|
71
|
+
return;
|
|
72
|
+
const order = await loadPendingOrder(req, res);
|
|
73
|
+
if (!order)
|
|
74
|
+
return;
|
|
75
|
+
if (order.seller_id !== user.id)
|
|
76
|
+
return void errorRes(res, 403, 'NOT_ORDER_SELLER', '只有订单卖家可接单');
|
|
77
|
+
// 询价单不可裸接单(会绕过报价 → 无运费进付款窗):必须 quote → 买家 confirm-quote(PR-3)
|
|
78
|
+
if (Number(order.shipping_quote_required) === 1)
|
|
79
|
+
return void errorRes(res, 409, 'QUOTE_REQUIRED', '本单收货地区在运费模板外,请先报价(运费+时效),买家确认后自动进入付款');
|
|
80
|
+
const windowHours = Math.max(1, Number(getProtocolParam('direct_pay.payment_window_hours', 4)) || 4);
|
|
81
|
+
try {
|
|
82
|
+
db.transaction(() => {
|
|
83
|
+
const t = transition(db, order.id, 'direct_pay_window', user.id, [], '卖家确认接单 → 进入直付付款窗口');
|
|
84
|
+
if (!t.success)
|
|
85
|
+
throw new Error(t.error || 'TRANSITION_FAILED');
|
|
86
|
+
db.prepare('UPDATE orders SET direct_pay_window_deadline = ? WHERE id = ?')
|
|
87
|
+
.run(new Date(Date.now() + windowHours * 3600_000).toISOString(), order.id);
|
|
88
|
+
})();
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
return void errorRes(res, 409, 'ACCEPT_FAILED', e.message);
|
|
92
|
+
}
|
|
93
|
+
notify(order.buyer_id, order.id, 'direct_pay_accepted_by_seller', '✅ 卖家已确认接单,请付款', `卖家已确认可发货并接单。请在 ${windowHours} 小时内完成风险确认后查看收款方式并付款;逾期未付订单将进入超时流程。`, { templateKey: 'dp_pending_accept_accepted', params: { hours: windowHours } });
|
|
94
|
+
return void res.json({ success: true, status: 'direct_pay_window' });
|
|
95
|
+
});
|
|
96
|
+
// 卖家谢绝(无法发货/物流不可达等;理由可选,买家可见)→ 无责取消 + 回补库存
|
|
97
|
+
app.post('/api/orders/:id/pending-accept/decline', async (req, res) => {
|
|
98
|
+
const user = auth(req, res);
|
|
99
|
+
if (!user)
|
|
100
|
+
return;
|
|
101
|
+
const order = await loadPendingOrder(req, res);
|
|
102
|
+
if (!order)
|
|
103
|
+
return;
|
|
104
|
+
if (order.seller_id !== user.id)
|
|
105
|
+
return void errorRes(res, 403, 'NOT_ORDER_SELLER', '只有订单卖家可谢绝');
|
|
106
|
+
const reason = typeof req.body?.reason === 'string' ? req.body.reason.trim().slice(0, 200) : '';
|
|
107
|
+
try {
|
|
108
|
+
db.transaction(() => {
|
|
109
|
+
const t = transition(db, order.id, 'cancelled', user.id, [], `卖家谢绝接单(付款前,无责取消)${reason ? `:${reason}` : ''}`);
|
|
110
|
+
if (!t.success)
|
|
111
|
+
throw new Error(t.error || 'TRANSITION_FAILED');
|
|
112
|
+
restorePreShipDirectPayStock(db, { fromStatus: 'pending_accept', productId: order.product_id, quantity: Number(order.quantity) || 1 });
|
|
113
|
+
})();
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
return void errorRes(res, 409, 'DECLINE_FAILED', e.message);
|
|
117
|
+
}
|
|
118
|
+
notify(order.buyer_id, order.id, 'direct_pay_accept_declined', '❌ 卖家未能接单,订单已取消', `卖家未能确认发货${reason ? `(${reason})` : ''}。订单已无责取消 —— 你尚未付款,无需任何操作;双方信誉均不受影响。`, { templateKey: 'dp_pending_accept_declined', params: { reason } });
|
|
119
|
+
return void res.json({ success: true, status: 'cancelled' });
|
|
120
|
+
});
|
|
121
|
+
// ── 询价握手(PR-3,模板外地区)──────────────────────────────────────────
|
|
122
|
+
// 卖家报价:运费+预计时效(+备注,买家可见)。只能加"运费"一个科目,不能动货款单价(防坐地起价披运费皮);
|
|
123
|
+
// 货款+运费受【建单快照】单笔上限约束(cap 是买家最大裸损口径,必须含运费;用快照防事后调参绕过)。
|
|
124
|
+
// 可重复报价(买家确认前修正);每次报价重置响应窗(param direct_pay.quote_confirm_hours,默认 48h)。
|
|
125
|
+
app.post('/api/orders/:id/pending-accept/quote', async (req, res) => {
|
|
126
|
+
const user = auth(req, res);
|
|
127
|
+
if (!user)
|
|
128
|
+
return;
|
|
129
|
+
const order = await loadPendingOrder(req, res);
|
|
130
|
+
if (!order)
|
|
131
|
+
return;
|
|
132
|
+
if (order.seller_id !== user.id)
|
|
133
|
+
return void errorRes(res, 403, 'NOT_ORDER_SELLER', '只有订单卖家可报价');
|
|
134
|
+
if (Number(order.shipping_quote_required) !== 1)
|
|
135
|
+
return void errorRes(res, 409, 'NOT_QUOTE_ORDER', '本单不需询价(模板已覆盖或走普通接单)');
|
|
136
|
+
const fee = Number(req.body?.shipping_fee);
|
|
137
|
+
if (!Number.isFinite(fee) || fee < 0 || fee > 1_000_000)
|
|
138
|
+
return void errorRes(res, 400, 'BAD_QUOTE_FEE', '运费必须是 0~1000000 的数字');
|
|
139
|
+
const feeR = Math.round(fee * 100) / 100;
|
|
140
|
+
const cap = Number(order.direct_pay_per_tx_cap_units_snapshot);
|
|
141
|
+
if (Number.isFinite(cap) && cap > 0 && toUnits(Number(order.total_amount)) + toUnits(feeR) > cap) {
|
|
142
|
+
return void errorRes(res, 409, 'QUOTE_EXCEEDS_CAP', `货款+运费超出本单单笔上限(${toDecimal(cap)} USDC),不能以运费形式突破小额直付边界`);
|
|
143
|
+
}
|
|
144
|
+
const est = req.body?.est_days == null ? null : String(req.body.est_days).trim().slice(0, 20) || null;
|
|
145
|
+
const note = req.body?.note == null ? null : String(req.body.note).trim().slice(0, 200) || null;
|
|
146
|
+
const confirmHours = Math.max(1, Number(getProtocolParam('direct_pay.quote_confirm_hours', 48)) || 48);
|
|
147
|
+
await dbRun(`UPDATE orders SET shipping_quote_fee = ?, shipping_quote_est_days = ?, shipping_quote_note = ?, shipping_quote_at = datetime('now'), pending_accept_deadline = ? WHERE id = ? AND status = 'pending_accept'`, [feeR, est, note, new Date(Date.now() + confirmHours * 3600_000).toISOString(), order.id]);
|
|
148
|
+
notify(order.buyer_id, order.id, 'direct_pay_quote_submitted', '📦 卖家已报价运费,请确认', `卖家确认可发货并报价:运费 ${feeR} USDC${est ? `,预计时效 ${est} 天` : ''}${note ? `(${note})` : ''}。新总额 ${Math.round((Number(order.total_amount) + feeR) * 100) / 100} USDC。请在 ${confirmHours} 小时内确认(确认后进入付款环节)或撤单;逾期订单自动取消。`, { templateKey: 'dp_quote_submitted', params: { fee: feeR, est: est ?? '', note: note ?? '', total: Math.round((Number(order.total_amount) + feeR) * 100) / 100, hours: confirmHours } });
|
|
149
|
+
return void res.json({ success: true, shipping_quote_fee: feeR, est_days: est, new_total: Math.round((Number(order.total_amount) + feeR) * 100) / 100 });
|
|
150
|
+
});
|
|
151
|
+
// 买家确认报价 → 运费并入 total_amount(整数 units 精确加)+ 快照三列 + 重建 payable 参考换算 → 进付款窗。
|
|
152
|
+
// CAS:仅 pending_accept 且已报价;总额变更与状态转移同一 db.transaction(要么全生效要么全回滚)。
|
|
153
|
+
app.post('/api/orders/:id/pending-accept/confirm-quote', async (req, res) => {
|
|
154
|
+
const user = auth(req, res);
|
|
155
|
+
if (!user)
|
|
156
|
+
return;
|
|
157
|
+
const order = await loadPendingOrder(req, res);
|
|
158
|
+
if (!order)
|
|
159
|
+
return;
|
|
160
|
+
if (order.buyer_id !== user.id)
|
|
161
|
+
return void errorRes(res, 403, 'NOT_ORDER_BUYER', '只有订单买家可确认报价');
|
|
162
|
+
if (Number(order.shipping_quote_required) !== 1 || order.shipping_quote_fee == null)
|
|
163
|
+
return void errorRes(res, 409, 'QUOTE_NOT_SUBMITTED', '卖家尚未报价,不可确认');
|
|
164
|
+
const feeR = Number(order.shipping_quote_fee);
|
|
165
|
+
const newTotal = toDecimal(toUnits(Number(order.total_amount)) + toUnits(feeR));
|
|
166
|
+
const windowHours = Math.max(1, Number(getProtocolParam('direct_pay.payment_window_hours', 4)) || 4);
|
|
167
|
+
// payable 参考换算按新总额重建(display-only;账户快照缺失/坏 JSON → 保持原样,零阻断)
|
|
168
|
+
let snapJson = order.direct_pay_account_snapshot;
|
|
169
|
+
try {
|
|
170
|
+
if (snapJson) {
|
|
171
|
+
const snap = JSON.parse(snapJson);
|
|
172
|
+
snapJson = JSON.stringify({ ...snap, ...buildPayableSnapshot(newTotal, snap.currency ?? null) });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
snapJson = order.direct_pay_account_snapshot;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
db.transaction(() => {
|
|
180
|
+
const u = db.prepare(`UPDATE orders SET total_amount = ?, shipping_fee = ?, shipping_est_days = ?, direct_pay_account_snapshot = ?, direct_pay_window_deadline = ? WHERE id = ? AND status = 'pending_accept' AND shipping_quote_fee IS NOT NULL`)
|
|
181
|
+
.run(newTotal, feeR, order.shipping_quote_est_days, snapJson, new Date(Date.now() + windowHours * 3600_000).toISOString(), order.id);
|
|
182
|
+
if (u.changes !== 1)
|
|
183
|
+
throw new Error('QUOTE_CONFIRM_RACE');
|
|
184
|
+
const t = transition(db, order.id, 'direct_pay_window', 'sys_protocol', [], `买家确认运费报价(${feeR} USDC,新总额 ${newTotal})→ 进入直付付款窗口`);
|
|
185
|
+
if (!t.success)
|
|
186
|
+
throw new Error(t.error || 'TRANSITION_FAILED');
|
|
187
|
+
})();
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
return void errorRes(res, 409, 'QUOTE_CONFIRM_FAILED', e.message);
|
|
191
|
+
}
|
|
192
|
+
// 总额变更后补跑 AML 监控(fail-soft,绝不回流为失败);条款快照补记运费裁决(quote_pending→quote)
|
|
193
|
+
safeRunDirectPayAmlMonitor(db, { sellerId: order.seller_id, orderId: order.id, nowIso: new Date().toISOString(), getProtocolParam });
|
|
194
|
+
updateSnapshotShippingQuote(db, order.id, feeR, order.shipping_quote_est_days ?? null);
|
|
195
|
+
notify(order.seller_id, order.id, 'direct_pay_quote_confirmed', '✅ 买家已确认运费报价', `买家已确认新总额 ${newTotal} USDC(含运费 ${feeR}),订单进入付款窗口。买家完成场外付款并标记后你会收到发货提醒。`, { templateKey: 'dp_quote_confirmed', params: { total: newTotal, fee: feeR } });
|
|
196
|
+
return void res.json({ success: true, status: 'direct_pay_window', total_amount: newTotal });
|
|
197
|
+
});
|
|
198
|
+
// 买家撤单(接单前反悔)→ 无责取消 + 回补库存
|
|
199
|
+
app.post('/api/orders/:id/pending-accept/cancel', async (req, res) => {
|
|
200
|
+
const user = auth(req, res);
|
|
201
|
+
if (!user)
|
|
202
|
+
return;
|
|
203
|
+
const order = await loadPendingOrder(req, res);
|
|
204
|
+
if (!order)
|
|
205
|
+
return;
|
|
206
|
+
if (order.buyer_id !== user.id)
|
|
207
|
+
return void errorRes(res, 403, 'NOT_ORDER_BUYER', '只有订单买家可撤单');
|
|
208
|
+
try {
|
|
209
|
+
db.transaction(() => {
|
|
210
|
+
const t = transition(db, order.id, 'cancelled', user.id, [], '买家在卖家接单前撤单(付款前,无责取消)');
|
|
211
|
+
if (!t.success)
|
|
212
|
+
throw new Error(t.error || 'TRANSITION_FAILED');
|
|
213
|
+
restorePreShipDirectPayStock(db, { fromStatus: 'pending_accept', productId: order.product_id, quantity: Number(order.quantity) || 1 });
|
|
214
|
+
})();
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
return void errorRes(res, 409, 'CANCEL_FAILED', e.message);
|
|
218
|
+
}
|
|
219
|
+
notify(order.seller_id, order.id, 'direct_pay_accept_cancelled', '↩️ 买家已撤单', '买家在你确认接单前撤回了订单。订单已无责取消,库存已恢复。', { templateKey: 'dp_pending_accept_cancelled', params: {} });
|
|
220
|
+
return void res.json({ success: true, status: 'cancelled' });
|
|
221
|
+
});
|
|
222
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { markReturnRefunded, confirmReturnRefundReceived } from '../../direct-pay-returns.js';
|
|
2
|
+
import { requireDirectPayHumanPasskey } from '../direct-pay-guards.js';
|
|
3
|
+
import { recordRepEvent } from '../../layer4-economics/L4-3-reputation/reputation-engine.js';
|
|
4
|
+
import { createNotification } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
5
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js';
|
|
6
|
+
export function registerDirectPayReturnsRoutes(app, deps) {
|
|
7
|
+
const { db, auth, generateId, errorRes, consumeGateToken } = deps;
|
|
8
|
+
const httpFor = (code) => code === 'RETURN_NOT_FOUND' ? 404
|
|
9
|
+
: code === 'NOT_ORDER_BUYER' || code === 'NOT_ORDER_SELLER' ? 403
|
|
10
|
+
: 409;
|
|
11
|
+
const notify = (userId, orderId, type, title, body) => {
|
|
12
|
+
try {
|
|
13
|
+
createNotification(db, userId, orderId, type, title, body);
|
|
14
|
+
}
|
|
15
|
+
catch { /* 通知失败不阻断业务 */ }
|
|
16
|
+
};
|
|
17
|
+
app.post('/api/return-requests/:id/mark-refunded', async (req, res) => {
|
|
18
|
+
const user = auth(req, res);
|
|
19
|
+
if (!user)
|
|
20
|
+
return;
|
|
21
|
+
const refundReference = typeof req.body?.refund_reference === 'string' ? req.body.refund_reference : null;
|
|
22
|
+
const r = markReturnRefunded(db, { returnId: req.params.id, sellerId: user.id, refundReference, messageId: generateId('rmsg') });
|
|
23
|
+
if (!r.ok)
|
|
24
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code || 'RETURN_MARK_REFUNDED_ERROR', r.error || '声明退款失败');
|
|
25
|
+
const rr = await dbOne('SELECT buyer_id, order_id FROM return_requests WHERE id = ?', [req.params.id]);
|
|
26
|
+
if (rr)
|
|
27
|
+
notify(rr.buyer_id, rr.order_id, 'direct_pay_return_refund_marked', '💸 卖家已声明退货退款', `卖家声明已在协议外向你退款${refundReference ? `(退款参考:${String(refundReference).slice(0, 80)})` : ''}。请核实到账后在订单页确认(需 Passkey),退货即完成;若未收到退款请勿确认,可发起争议。`);
|
|
28
|
+
return void res.json({ success: true, status: r.status });
|
|
29
|
+
});
|
|
30
|
+
app.post('/api/return-requests/:id/confirm-refund', async (req, res) => {
|
|
31
|
+
const user = auth(req, res);
|
|
32
|
+
if (!user)
|
|
33
|
+
return;
|
|
34
|
+
const uid = user.id;
|
|
35
|
+
// 只读预检(不消费一次性 Passkey token):必须是本人 buyer 的 refund_marked 直付退货
|
|
36
|
+
const pre = await dbOne('SELECT r.buyer_id, r.status, r.order_id, o.payment_rail FROM return_requests r JOIN orders o ON o.id = r.order_id WHERE r.id = ?', [req.params.id]);
|
|
37
|
+
if (!pre)
|
|
38
|
+
return void errorRes(res, 404, 'RETURN_NOT_FOUND', '退货请求不存在');
|
|
39
|
+
if (pre.buyer_id !== uid)
|
|
40
|
+
return void errorRes(res, 403, 'NOT_ORDER_BUYER', '只有订单买家可确认收到退款');
|
|
41
|
+
if (pre.payment_rail !== 'direct_p2p' || pre.status !== 'refund_marked') {
|
|
42
|
+
return void errorRes(res, 409, 'REFUND_NOT_MARKED', '当前不可确认(卖家尚未声明退款,或非直付退货)');
|
|
43
|
+
}
|
|
44
|
+
// RISK 门:确认收款并终结退货 = 终局动作 → 现场真人 Passkey(agent 无 Passkey 硬拒)
|
|
45
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
46
|
+
userId: uid, webauthnToken: req.body?.webauthn_token, purpose: 'direct_pay_order_action',
|
|
47
|
+
validate: (data) => { const d = data; return !!d && d.order_id === pre.order_id && d.action === 'return_refund_confirm'; },
|
|
48
|
+
});
|
|
49
|
+
if (!gate.ok)
|
|
50
|
+
return void errorRes(res, 403, gate.error_code || 'HUMAN_PRESENCE_REQUIRED', gate.reason || '需现场真人 Passkey 确认');
|
|
51
|
+
let r;
|
|
52
|
+
try {
|
|
53
|
+
// CAS 终态翻转 + 全额退款社交计数 -1,同一原子边界(零资金零库存)
|
|
54
|
+
r = db.transaction(() => confirmReturnRefundReceived(db, { returnId: req.params.id, buyerId: uid, messageId: generateId('rmsg') }))();
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
return void errorRes(res, 409, 'RETURN_CONFIRM_FAILED', e.message);
|
|
58
|
+
}
|
|
59
|
+
if (!r.ok)
|
|
60
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code || 'RETURN_CONFIRM_ERROR', r.error || '确认失败');
|
|
61
|
+
// 卖家过错原因 → rep event(与 escrow executeReturnRefund 同口径,tx 外 best-effort)
|
|
62
|
+
try {
|
|
63
|
+
if (r.seller_fault_reason && r.seller_id) {
|
|
64
|
+
recordRepEvent(db, r.seller_id, 'claim_upheld_against', `退货接受 (reason=${r.seller_fault_reason}, return=${req.params.id})`, r.order_id || undefined);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
console.error('[dp return rep event]', e);
|
|
69
|
+
}
|
|
70
|
+
if (r.seller_id && r.order_id)
|
|
71
|
+
notify(r.seller_id, r.order_id, 'direct_pay_return_settled', '✅ 退货已完成', '买家已确认收到场外退款,退货流程结束。提示:退回货物须经验收后方可重新上架(库存不会自动恢复)。');
|
|
72
|
+
return void res.json({ success: true, status: r.status });
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
|
|
2
|
+
import { releaseFeeStake } from '../../direct-pay-ledger.js';
|
|
3
|
+
import { restorePreShipDirectPayStock } from '../../direct-pay-stock.js'; // D3 库存回补唯一入口(pre-ship 放行;已出库拒绝)
|
|
4
|
+
import { notifyTransition, createNotification } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
5
|
+
import { expireDeferrals, listExpiringDeferrals, markDeferralReminded, suspendPrivilegeOnDeferralExpiry } from '../../direct-receive-deferral.js'; // B4:缓交到期提醒/收口
|
|
6
|
+
const SYS = 'sys_protocol';
|
|
7
|
+
/** 货款协商买家申诉窗(天),governance 可调,默认 7d。与 request_cancel 同源 param,保证手动/自动一致。 */
|
|
8
|
+
function recourseDays(db) {
|
|
9
|
+
try {
|
|
10
|
+
const row = db.prepare("SELECT value FROM protocol_params WHERE key = 'direct_pay.payment_query_recourse_days'").get();
|
|
11
|
+
const n = Number(row?.value ?? 7);
|
|
12
|
+
return Number.isFinite(n) && n > 0 ? Math.max(1, n) : 7;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return 7;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** 宽限期(小时),governance 可调,默认 48h(2d)。不硬编码。 */
|
|
19
|
+
function graceHours(db) {
|
|
20
|
+
try {
|
|
21
|
+
const row = db.prepare("SELECT value FROM protocol_params WHERE key = 'direct_pay.grace_hours'").get();
|
|
22
|
+
const n = Number(row?.value ?? 48);
|
|
23
|
+
return Number.isFinite(n) && n > 0 ? n : 48;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return 48;
|
|
27
|
+
} // param 表缺失/读失败 → 安全默认,cron 不崩
|
|
28
|
+
}
|
|
29
|
+
export function runDirectPayTimeoutSweep(deps) {
|
|
30
|
+
const { db } = deps;
|
|
31
|
+
const gh = graceHours(db);
|
|
32
|
+
const windowExpired = [];
|
|
33
|
+
const graceCancelled = [];
|
|
34
|
+
const pqRecourseOpened = [];
|
|
35
|
+
const pqCancelled = [];
|
|
36
|
+
const acceptExpired = [];
|
|
37
|
+
const deferralReminded = [];
|
|
38
|
+
const deferralExpired = [];
|
|
39
|
+
// F. 缓交收口(B4):① 到期前提醒(param direct_pay.deferral_reminder_days,默认 3d;reminder_sent_at 去重)
|
|
40
|
+
// ② 过 grace 到期 → expired + 无生产 bond 则停权(有 bond=已缴清兜底不停)+ 通知。均 fail-soft 不断 cron。
|
|
41
|
+
try {
|
|
42
|
+
const remindDays = (() => { try {
|
|
43
|
+
const r = db.prepare("SELECT value FROM protocol_params WHERE key = 'direct_pay.deferral_reminder_days'").get();
|
|
44
|
+
const n = Number(r?.value ?? 3);
|
|
45
|
+
return Number.isFinite(n) && n > 0 ? n : 3;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return 3;
|
|
49
|
+
} })();
|
|
50
|
+
for (const d of listExpiringDeferrals(db, new Date().toISOString(), remindDays)) {
|
|
51
|
+
markDeferralReminded(db, d.id);
|
|
52
|
+
deferralReminded.push(d.id);
|
|
53
|
+
try {
|
|
54
|
+
createNotification(db, d.user_id, null, 'deferral_expiring_soon', '⏰ 保证金缓交即将到期', `你的缓交资格将于 ${d.expires_at} 到期。请在到期前缴纳履约保证金转正式(设置页-直付履约保证金),否则宽限期后直付资格将关闭。`, { templateKey: 'deferral_expiring_soon', params: { expires: d.expires_at } });
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.warn('[direct-pay-timeouts] notify deferral-remind:', e.message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const uid of expireDeferrals(db, new Date().toISOString()).expired) {
|
|
61
|
+
const suspended = suspendPrivilegeOnDeferralExpiry(db, uid);
|
|
62
|
+
deferralExpired.push(uid);
|
|
63
|
+
try {
|
|
64
|
+
createNotification(db, uid, null, 'deferral_expired', '🚫 保证金缓交已到期', suspended ? '缓交资格已到期且未缴纳保证金,直付资格已关闭。缴纳履约保证金并经运营确认后可重新开通;在途订单不受影响,请正常履约完成。' : '缓交资格已到期;你已缴纳保证金,直付资格不受影响。', { templateKey: 'deferral_expired', params: { closed: suspended ? 1 : 0 } });
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
console.warn('[direct-pay-timeouts] notify deferral-expired:', e.message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
console.error('[direct-pay-timeouts] deferral closure sweep:', e);
|
|
73
|
+
}
|
|
74
|
+
// E. 手动接单窗超时(v16):pending_accept 过 pending_accept_deadline → 无责取消 + 回补库存。
|
|
75
|
+
// 零资金(此阶段没人付过钱 —— 时序门);双方通知。WHERE 硬门:仅 now > deadline 命中。
|
|
76
|
+
const acceptRows = db.prepare(`
|
|
77
|
+
SELECT id, product_id, quantity, buyer_id, seller_id FROM orders
|
|
78
|
+
WHERE status = 'pending_accept'
|
|
79
|
+
AND payment_rail = 'direct_p2p'
|
|
80
|
+
AND pending_accept_deadline IS NOT NULL
|
|
81
|
+
AND datetime(pending_accept_deadline) < datetime('now')
|
|
82
|
+
LIMIT 1000
|
|
83
|
+
`).all();
|
|
84
|
+
for (const { id, product_id, quantity, buyer_id, seller_id } of acceptRows) {
|
|
85
|
+
let done = false;
|
|
86
|
+
db.transaction(() => {
|
|
87
|
+
const r = transition(db, id, 'cancelled', SYS, [], '手动接单:卖家超时未确认接单,系统无责取消(付款前,零资金)');
|
|
88
|
+
if (!r.success)
|
|
89
|
+
return;
|
|
90
|
+
restorePreShipDirectPayStock(db, { fromStatus: 'pending_accept', productId: product_id, quantity });
|
|
91
|
+
acceptExpired.push(id);
|
|
92
|
+
done = true;
|
|
93
|
+
})();
|
|
94
|
+
if (done) {
|
|
95
|
+
try {
|
|
96
|
+
createNotification(db, buyer_id, id, 'direct_pay_accept_expired', '⏰ 卖家超时未接单,订单已取消', '卖家未在接单窗口内确认,订单已自动取消 —— 你尚未付款,无需任何操作。可换商品或联系卖家后重新下单。', { templateKey: 'dp_pending_accept_expired_buyer', params: {} });
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.warn('[direct-pay-timeouts] notify accept-expired buyer:', e.message);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
createNotification(db, seller_id, id, 'direct_pay_accept_expired', '⏰ 订单因超时未接单已取消', '你未在接单窗口内确认接单,订单已自动取消,库存已恢复。频繁超时会影响买家体验,可考虑改用自动接单或缩短响应时间。', { templateKey: 'dp_pending_accept_expired_seller', params: {} });
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
console.warn('[direct-pay-timeouts] notify accept-expired seller:', e.message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// A. 付款窗口超时 → 可争议态(非静默关单)+ 退费用质押 + 设宽限期
|
|
110
|
+
const windowRows = db.prepare(`
|
|
111
|
+
SELECT id, buyer_id FROM orders
|
|
112
|
+
WHERE status = 'direct_pay_window'
|
|
113
|
+
AND payment_rail = 'direct_p2p'
|
|
114
|
+
AND direct_pay_window_deadline IS NOT NULL
|
|
115
|
+
AND datetime(direct_pay_window_deadline) < datetime('now')
|
|
116
|
+
LIMIT 1000
|
|
117
|
+
`).all();
|
|
118
|
+
for (const { id, buyer_id } of windowRows) {
|
|
119
|
+
let done = false;
|
|
120
|
+
db.transaction(() => {
|
|
121
|
+
const r = transition(db, id, 'direct_expired_unconfirmed', SYS, [], 'Rail1 直付:付款窗口超时未标记');
|
|
122
|
+
if (!r.success)
|
|
123
|
+
return;
|
|
124
|
+
releaseFeeStake(db, { orderId: id }); // 费用质押退卖家(无完成,不收费)
|
|
125
|
+
db.prepare(`UPDATE orders SET direct_grace_deadline = datetime('now', ?) WHERE id = ?`).run(`+${gh} hours`, id);
|
|
126
|
+
windowExpired.push(id);
|
|
127
|
+
done = true;
|
|
128
|
+
})();
|
|
129
|
+
// 审计项 B(N2):此前窗口静默过期,卡点付款的买家毫不知情直到 48h 后自动取消。tx 外 fail-soft。
|
|
130
|
+
if (done) {
|
|
131
|
+
try {
|
|
132
|
+
createNotification(db, buyer_id, id, 'direct_pay_window_expired', '⏰ 直付付款窗口已过期', `若你已付款:请在 ${gh} 小时宽限期内到订单页提交付款凭证发起争议;未付款可直接关闭订单,否则宽限期满将自动取消。`, { templateKey: 'dp_window_expired', params: { graceHours: gh } });
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
console.warn('[direct-pay-timeouts] notify window-expired:', e.message);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// B. 宽限期满 → 系统关单(★ WHERE 保证仅 now>grace 命中;宽限期内绝不关,买家 →disputed 全程可用)
|
|
140
|
+
const graceRows = db.prepare(`
|
|
141
|
+
SELECT id, product_id, quantity, buyer_id, seller_id FROM orders
|
|
142
|
+
WHERE status = 'direct_expired_unconfirmed'
|
|
143
|
+
AND payment_rail = 'direct_p2p'
|
|
144
|
+
AND direct_grace_deadline IS NOT NULL
|
|
145
|
+
AND datetime(direct_grace_deadline) < datetime('now')
|
|
146
|
+
LIMIT 1000
|
|
147
|
+
`).all();
|
|
148
|
+
for (const { id, product_id, quantity, buyer_id, seller_id } of graceRows) {
|
|
149
|
+
let done = false;
|
|
150
|
+
db.transaction(() => {
|
|
151
|
+
const r = transition(db, id, 'cancelled', SYS, [], 'Rail1 直付:宽限期满,买家未付款/未升级争议,系统关单');
|
|
152
|
+
if (!r.success)
|
|
153
|
+
return;
|
|
154
|
+
restorePreShipDirectPayStock(db, { fromStatus: 'direct_expired_unconfirmed', productId: product_id, quantity }); // D3 回补(pre-ship 唯一入口)
|
|
155
|
+
graceCancelled.push(id);
|
|
156
|
+
done = true;
|
|
157
|
+
})();
|
|
158
|
+
// 审计项 B(N2):自动关单也要告知双方(此前静默,买家不知单没了、卖家不知库存已回)。tx 外 fail-soft。
|
|
159
|
+
if (done) {
|
|
160
|
+
try {
|
|
161
|
+
createNotification(db, buyer_id, id, 'direct_pay_grace_cancelled', '🚫 直付订单已自动取消', '付款窗口与宽限期均已过且未收到你的付款标记/凭证,订单已关闭。若你确已付款,请通过订单页联系卖家协商。', { templateKey: 'dp_grace_cancelled_buyer', params: {} });
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
console.warn('[direct-pay-timeouts] notify grace-cancel buyer:', e.message);
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
createNotification(db, seller_id, id, 'direct_pay_grace_cancelled', '🚫 直付订单已自动取消(买家未付款)', '买家未在付款窗口+宽限期内付款,订单已自动关闭,库存已恢复。', { templateKey: 'dp_grace_cancelled_seller', params: {} });
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
console.warn('[direct-pay-timeouts] notify grace-cancel seller:', e.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// D. 货款协商买家静默:payment_query 已过买家响应宽限(payment_query_deadline)且卖家未手动请求取消
|
|
175
|
+
// (payment_query_cancel_deadline 仍空)→ 系统【代卖家自动启动取消申请】(等价 request_cancel):
|
|
176
|
+
// 设 payment_query_cancel_deadline = now + recourse_days + 通知买家最后申诉窗已开。★ 不关单、不动状态、不涉资金;
|
|
177
|
+
// 买家窗内仍可 pq_escalate 主张已付,订单离开 payment_query 后本 sweep 与 Sweep C 均不再命中。之后由 Sweep C 窗满关单。
|
|
178
|
+
// 幂等:WHERE payment_query_cancel_deadline IS NULL —— 卖家已手动 request_cancel 的单不被重置,重复 cron 也不刷新窗口。
|
|
179
|
+
const rd = recourseDays(db);
|
|
180
|
+
const pqSilentRows = db.prepare(`
|
|
181
|
+
SELECT id, buyer_id FROM orders
|
|
182
|
+
WHERE status = 'payment_query'
|
|
183
|
+
AND payment_rail = 'direct_p2p'
|
|
184
|
+
AND payment_query_deadline IS NOT NULL
|
|
185
|
+
AND datetime(payment_query_deadline) < datetime('now')
|
|
186
|
+
AND payment_query_cancel_deadline IS NULL
|
|
187
|
+
LIMIT 1000
|
|
188
|
+
`).all();
|
|
189
|
+
for (const { id, buyer_id } of pqSilentRows) {
|
|
190
|
+
const res = db.prepare(`UPDATE orders SET payment_query_cancel_deadline = datetime('now', ?) WHERE id = ? AND payment_query_cancel_deadline IS NULL`).run(`+${rd} days`, id);
|
|
191
|
+
if (res.changes === 0)
|
|
192
|
+
continue; // 竞态:同 tick 已被设(卖家 request_cancel)——不重复处理
|
|
193
|
+
pqRecourseOpened.push(id);
|
|
194
|
+
try {
|
|
195
|
+
const dl = db.prepare("SELECT payment_query_cancel_deadline d FROM orders WHERE id = ?").get(id).d;
|
|
196
|
+
createNotification(db, buyer_id, id, 'payment_query_cancel_requested', '⏳ 卖家申请取消订单', `卖家称未收到货款且你未在响应期内回应,系统已代卖家发起取消。你约有 ${rd} 天:若确已付款请提供付款参考或发起举证(pq_escalate),否则订单将于 ${dl} 自动取消。直付非托管,无平台退款。`);
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
console.warn('[direct-pay-timeouts] notify pq-recourse:', e.message);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// C. 货款协商:卖家已请求取消(payment_query_cancel_deadline 已设)+ 买家申诉窗满 → 系统关单 + 退费用质押。
|
|
203
|
+
// ★ 窗内(now < deadline)绝不关:买家全程可主张已付/升级举证(pq_escalate 回 disputed)。
|
|
204
|
+
const pqCancelRows = db.prepare(`
|
|
205
|
+
SELECT id, product_id, quantity FROM orders
|
|
206
|
+
WHERE status = 'payment_query'
|
|
207
|
+
AND payment_rail = 'direct_p2p'
|
|
208
|
+
AND payment_query_cancel_deadline IS NOT NULL
|
|
209
|
+
AND datetime(payment_query_cancel_deadline) < datetime('now')
|
|
210
|
+
LIMIT 1000
|
|
211
|
+
`).all();
|
|
212
|
+
for (const { id, product_id, quantity } of pqCancelRows) {
|
|
213
|
+
let done = false;
|
|
214
|
+
db.transaction(() => {
|
|
215
|
+
const r = transition(db, id, 'cancelled', SYS, [], 'Rail1 直付:货款协商申诉窗满,买家未回应/未升级,系统关单');
|
|
216
|
+
if (!r.success)
|
|
217
|
+
return;
|
|
218
|
+
releaseFeeStake(db, { orderId: id }); // 无完成不收费:退卖家费用质押
|
|
219
|
+
restorePreShipDirectPayStock(db, { fromStatus: 'payment_query', productId: product_id, quantity }); // D3 回补(pre-ship 唯一入口)
|
|
220
|
+
done = true;
|
|
221
|
+
})();
|
|
222
|
+
if (done) {
|
|
223
|
+
pqCancelled.push(id);
|
|
224
|
+
try {
|
|
225
|
+
notifyTransition(db, id, 'payment_query', 'cancelled');
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
console.warn('[direct-pay-timeouts] notify pq-cancel:', e.message);
|
|
229
|
+
}
|
|
230
|
+
} // 通知买卖双方(cron 系统关单也要发,route 之外)
|
|
231
|
+
}
|
|
232
|
+
return { windowExpired, graceCancelled, pqRecourseOpened, pqCancelled, acceptExpired, deferralReminded, deferralExpired };
|
|
233
|
+
}
|
|
234
|
+
export function startDirectPayTimeoutCron(deps) {
|
|
235
|
+
const ms = 10 * 60 * 1000; // 10min — 付款窗口最短 30min,10min 粒度足够
|
|
236
|
+
setInterval(() => {
|
|
237
|
+
try {
|
|
238
|
+
const r = runDirectPayTimeoutSweep(deps);
|
|
239
|
+
if (r.windowExpired.length || r.graceCancelled.length || r.pqRecourseOpened.length || r.pqCancelled.length || r.acceptExpired.length || r.deferralReminded.length || r.deferralExpired.length) {
|
|
240
|
+
console.log(`[direct-pay-timeouts] window→expired ${r.windowExpired.length}, grace→cancelled ${r.graceCancelled.length}, pq→recourse ${r.pqRecourseOpened.length}, pq→cancelled ${r.pqCancelled.length}, accept→expired ${r.acceptExpired.length}, deferral remind/expired ${r.deferralReminded.length}/${r.deferralExpired.length}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
console.error('[direct-pay-timeouts-cron]', e);
|
|
245
|
+
}
|
|
246
|
+
}, ms);
|
|
247
|
+
console.log('⏳ Direct Pay (Rail 1) 超时 cron 已启动 (每 10min:付款窗口超时 + 宽限关单)');
|
|
248
|
+
}
|