@vobs/payment 0.3.0 → 1.2.1
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/LICENSE +1 -1
- package/README.md +182 -0
- package/dist/alipay/client.cjs +112 -0
- package/dist/alipay/client.cjs.map +1 -0
- package/dist/alipay/client.d.cts +36 -0
- package/dist/alipay/client.d.ts +36 -0
- package/dist/alipay/client.js +110 -0
- package/dist/alipay/client.js.map +1 -0
- package/dist/alipay/config.cjs +67 -0
- package/dist/alipay/config.cjs.map +1 -0
- package/dist/alipay/config.d.cts +30 -0
- package/dist/alipay/config.d.ts +30 -0
- package/dist/alipay/config.js +65 -0
- package/dist/alipay/config.js.map +1 -0
- package/dist/alipay/index.cjs +592 -0
- package/dist/alipay/index.cjs.map +1 -0
- package/dist/alipay/index.d.cts +12 -0
- package/dist/alipay/index.d.ts +12 -0
- package/dist/alipay/index.js +580 -0
- package/dist/alipay/index.js.map +1 -0
- package/dist/alipay/notify.cjs +70 -0
- package/dist/alipay/notify.cjs.map +1 -0
- package/dist/alipay/notify.d.cts +52 -0
- package/dist/alipay/notify.d.ts +52 -0
- package/dist/alipay/notify.js +68 -0
- package/dist/alipay/notify.js.map +1 -0
- package/dist/alipay/payment.cjs +49 -0
- package/dist/alipay/payment.cjs.map +1 -0
- package/dist/alipay/payment.d.cts +24 -0
- package/dist/alipay/payment.d.ts +24 -0
- package/dist/alipay/payment.js +47 -0
- package/dist/alipay/payment.js.map +1 -0
- package/dist/alipay/plugin.cjs +300 -0
- package/dist/alipay/plugin.cjs.map +1 -0
- package/dist/alipay/plugin.d.cts +43 -0
- package/dist/alipay/plugin.d.ts +43 -0
- package/dist/alipay/plugin.js +296 -0
- package/dist/alipay/plugin.js.map +1 -0
- package/dist/alipay/query.cjs +80 -0
- package/dist/alipay/query.cjs.map +1 -0
- package/dist/alipay/query.d.cts +26 -0
- package/dist/alipay/query.d.ts +26 -0
- package/dist/alipay/query.js +78 -0
- package/dist/alipay/query.js.map +1 -0
- package/dist/alipay/refund.cjs +104 -0
- package/dist/alipay/refund.cjs.map +1 -0
- package/dist/alipay/refund.d.cts +32 -0
- package/dist/alipay/refund.d.ts +32 -0
- package/dist/alipay/refund.js +102 -0
- package/dist/alipay/refund.js.map +1 -0
- package/dist/alipay/response.cjs +38 -0
- package/dist/alipay/response.cjs.map +1 -0
- package/dist/alipay/response.d.cts +19 -0
- package/dist/alipay/response.d.ts +19 -0
- package/dist/alipay/response.js +35 -0
- package/dist/alipay/response.js.map +1 -0
- package/dist/alipay/types.cjs +4 -0
- package/dist/alipay/types.cjs.map +1 -0
- package/dist/alipay/types.d.cts +242 -0
- package/dist/alipay/types.d.ts +242 -0
- package/dist/alipay/types.js +3 -0
- package/dist/alipay/types.js.map +1 -0
- package/dist/alipay/wap.cjs +50 -0
- package/dist/alipay/wap.cjs.map +1 -0
- package/dist/alipay/wap.d.cts +24 -0
- package/dist/alipay/wap.d.ts +24 -0
- package/dist/alipay/wap.js +48 -0
- package/dist/alipay/wap.js.map +1 -0
- package/dist/index-B1aUR7Vt.d.ts +49 -0
- package/dist/index-B8pzZxGq.d.cts +44 -0
- package/dist/index-BWnIrwRi.d.ts +44 -0
- package/dist/index-Dyrkl86r.d.cts +49 -0
- package/dist/index.cjs +1071 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -37
- package/dist/index.js +1052 -37
- package/dist/index.js.map +1 -0
- package/dist/wechat/client.cjs +119 -0
- package/dist/wechat/client.cjs.map +1 -0
- package/dist/wechat/client.d.cts +39 -0
- package/dist/wechat/client.d.ts +39 -0
- package/dist/wechat/client.js +117 -0
- package/dist/wechat/client.js.map +1 -0
- package/dist/wechat/config.cjs +76 -0
- package/dist/wechat/config.cjs.map +1 -0
- package/dist/wechat/config.d.cts +29 -0
- package/dist/wechat/config.d.ts +29 -0
- package/dist/wechat/config.js +74 -0
- package/dist/wechat/config.js.map +1 -0
- package/dist/wechat/index.cjs +612 -0
- package/dist/wechat/index.cjs.map +1 -0
- package/dist/wechat/index.d.cts +10 -0
- package/dist/wechat/index.d.ts +10 -0
- package/dist/wechat/index.js +602 -0
- package/dist/wechat/index.js.map +1 -0
- package/dist/wechat/notify.cjs +73 -0
- package/dist/wechat/notify.cjs.map +1 -0
- package/dist/wechat/notify.d.cts +53 -0
- package/dist/wechat/notify.d.ts +53 -0
- package/dist/wechat/notify.js +71 -0
- package/dist/wechat/notify.js.map +1 -0
- package/dist/wechat/payment.cjs +186 -0
- package/dist/wechat/payment.cjs.map +1 -0
- package/dist/wechat/payment.d.cts +50 -0
- package/dist/wechat/payment.d.ts +50 -0
- package/dist/wechat/payment.js +184 -0
- package/dist/wechat/payment.js.map +1 -0
- package/dist/wechat/plugin.cjs +305 -0
- package/dist/wechat/plugin.cjs.map +1 -0
- package/dist/wechat/plugin.d.cts +40 -0
- package/dist/wechat/plugin.d.ts +40 -0
- package/dist/wechat/plugin.js +301 -0
- package/dist/wechat/plugin.js.map +1 -0
- package/dist/wechat/refund.cjs +112 -0
- package/dist/wechat/refund.cjs.map +1 -0
- package/dist/wechat/refund.d.cts +26 -0
- package/dist/wechat/refund.d.ts +26 -0
- package/dist/wechat/refund.js +110 -0
- package/dist/wechat/refund.js.map +1 -0
- package/dist/wechat/response.cjs +50 -0
- package/dist/wechat/response.cjs.map +1 -0
- package/dist/wechat/response.d.cts +20 -0
- package/dist/wechat/response.d.ts +20 -0
- package/dist/wechat/response.js +46 -0
- package/dist/wechat/response.js.map +1 -0
- package/dist/wechat/types.cjs +4 -0
- package/dist/wechat/types.cjs.map +1 -0
- package/dist/wechat/types.d.cts +200 -0
- package/dist/wechat/types.d.ts +200 -0
- package/dist/wechat/types.js +3 -0
- package/dist/wechat/types.js.map +1 -0
- package/package.json +40 -27
- package/src/alipay/alipay.test.ts +231 -0
- package/src/alipay/client.ts +67 -0
- package/src/alipay/config.ts +72 -0
- package/src/alipay/index.ts +32 -0
- package/src/alipay/notify.ts +92 -0
- package/src/alipay/payment.ts +60 -0
- package/src/alipay/plugin.ts +64 -0
- package/src/alipay/query.ts +52 -0
- package/src/alipay/refund.ts +85 -0
- package/src/alipay/response.ts +40 -0
- package/src/alipay/types.ts +271 -0
- package/src/alipay/wap.ts +55 -0
- package/src/index.ts +78 -0
- package/src/wechat/client.ts +68 -0
- package/src/wechat/config.ts +81 -0
- package/src/wechat/index.ts +30 -0
- package/src/wechat/notify.ts +93 -0
- package/src/wechat/payment.ts +177 -0
- package/src/wechat/plugin.ts +59 -0
- package/src/wechat/refund.ts +80 -0
- package/src/wechat/response.ts +54 -0
- package/src/wechat/types.ts +223 -0
- package/src/wechat/wechat.test.ts +393 -0
- package/dist/alipay-official.d.ts +0 -34
- package/dist/alipay-official.js +0 -206
- package/dist/handlers.d.ts +0 -45
- package/dist/handlers.js +0 -105
- package/dist/jeepay.d.ts +0 -39
- package/dist/jeepay.js +0 -206
- package/dist/lakala.d.ts +0 -41
- package/dist/lakala.js +0 -243
- package/dist/manager.d.ts +0 -67
- package/dist/manager.js +0 -538
- package/dist/mock-driver.d.ts +0 -22
- package/dist/mock-driver.js +0 -59
- package/dist/state-machine.d.ts +0 -41
- package/dist/state-machine.js +0 -60
- package/dist/store.d.ts +0 -33
- package/dist/store.js +0 -76
- package/dist/types.d.ts +0 -168
- package/dist/types.js +0 -11
- package/dist/verify.d.ts +0 -34
- package/dist/verify.js +0 -55
- package/dist/wechat-official.d.ts +0 -40
- package/dist/wechat-official.js +0 -208
package/dist/manager.d.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* PaymentManager — payment core service layer (framework-agnostic, pure logic).
|
|
7
|
-
*
|
|
8
|
-
* Security baseline coverage (design doc §3/§5):
|
|
9
|
-
* - Server-side pricing (amount comes from the business's getOrderAmount; the frontend amount is only a consistency check)
|
|
10
|
-
* - MAX_AMOUNT per-order limit (over-limit rejected, avoids channel errors/risk control)
|
|
11
|
-
* - Order idempotency (idempotencyKey) + duplicate-order guard for the same business order (duplicateOrderStrategy)
|
|
12
|
-
* - Callback verification + time window + nonce anti-replay + callback idempotency (persist before responding)
|
|
13
|
-
* - Conflict freeze / paid-after-close freeze / large-amount manual review (AUDIT_THRESHOLD + confirm)
|
|
14
|
-
* - Operation audit (order/callback-book/freeze/confirm all logged)
|
|
15
|
-
* - onVerified business hook (must be idempotent) / onAlert alert hook
|
|
16
|
-
*/
|
|
17
|
-
import type { AuditEntry, CallbackResult, CreateOrderRequest, CreateOrderResult, GetOrderAmount, OrderState, OrderStatus, PaymentAlert, PaymentConfig, PaymentDriver, PaymentScene, VerifiedResult } from './types.js';
|
|
18
|
-
import { type PaymentStore } from './store.js';
|
|
19
|
-
export type OrderErrorCode = 'order_not_found' | 'amount_mismatch' | 'amount_exceeds_limit' | 'order_conflict' | 'channel_unavailable' | 'bad_request';
|
|
20
|
-
export type OrderError = {
|
|
21
|
-
readonly error: OrderErrorCode;
|
|
22
|
-
readonly message?: string;
|
|
23
|
-
readonly expected?: number;
|
|
24
|
-
readonly max?: number;
|
|
25
|
-
};
|
|
26
|
-
export type CreateOrderOutcome = CreateOrderResult | OrderError;
|
|
27
|
-
export interface PaymentManagerOptions {
|
|
28
|
-
readonly config?: PaymentConfig;
|
|
29
|
-
/** Defaults to an in-memory store. */
|
|
30
|
-
readonly store?: PaymentStore;
|
|
31
|
-
/** Callback signature key (HMAC-SHA256). Inject via KMS/env in production. */
|
|
32
|
-
readonly apiKey?: string;
|
|
33
|
-
/** Server-owned callback URL, or a resolver for channel-specific callback URLs. */
|
|
34
|
-
readonly notifyUrl?: string | ((driverName: string) => string);
|
|
35
|
-
/** Server-pricing hook (business: orderId → amount in fen; undefined = order not found). */
|
|
36
|
-
readonly getOrderAmount?: GetOrderAmount;
|
|
37
|
-
/** Business action after booking (deliver/activate) — must be idempotent; on failure the business must retry or compensate manually. */
|
|
38
|
-
readonly onVerified?: (result: VerifiedResult) => void | Promise<void>;
|
|
39
|
-
/** Alerts (frozen/conflict/large-amount/signature-failure). */
|
|
40
|
-
readonly onAlert?: (alert: PaymentAlert) => void;
|
|
41
|
-
}
|
|
42
|
-
export interface PaymentManager {
|
|
43
|
-
registerDriver(driver: PaymentDriver): void;
|
|
44
|
-
available(): readonly {
|
|
45
|
-
readonly name: string;
|
|
46
|
-
readonly scenes: readonly PaymentScene[];
|
|
47
|
-
}[];
|
|
48
|
-
createOrder(req: CreateOrderRequest): Promise<CreateOrderOutcome>;
|
|
49
|
-
getStatus(orderId: string): OrderStatus | undefined;
|
|
50
|
-
close(orderId: string): OrderStatus | undefined;
|
|
51
|
-
confirm(orderId: string): OrderStatus | undefined;
|
|
52
|
-
/** Active reconciliation (compensates lost callbacks): query channel status → sync locally (channel paid & local created → migrate via state machine, incl. large-amount audit). */
|
|
53
|
-
syncOrderStatus(orderId: string): Promise<OrderStatus | undefined>;
|
|
54
|
-
/** Expiry scan: created & past ttlMs → closed; returns closed orders. Call from a scheduled task. */
|
|
55
|
-
scanExpiredOrders(ttlMs: number): readonly {
|
|
56
|
-
readonly orderId: string;
|
|
57
|
-
readonly state: OrderState;
|
|
58
|
-
}[];
|
|
59
|
-
/** Retries a persisted pending business delivery. Returns undefined when the order is missing. */
|
|
60
|
-
retryDelivery(orderId: string): Promise<boolean | undefined>;
|
|
61
|
-
handleCallback(driverName: string, ctx: {
|
|
62
|
-
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
63
|
-
readonly body: string;
|
|
64
|
-
}): Promise<CallbackResult>;
|
|
65
|
-
auditLog(): readonly AuditEntry[];
|
|
66
|
-
}
|
|
67
|
-
export declare function createPaymentManager(options?: PaymentManagerOptions): PaymentManager;
|
package/dist/manager.js
DELETED
|
@@ -1,538 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* PaymentManager — payment core service layer (framework-agnostic, pure logic).
|
|
7
|
-
*
|
|
8
|
-
* Security baseline coverage (design doc §3/§5):
|
|
9
|
-
* - Server-side pricing (amount comes from the business's getOrderAmount; the frontend amount is only a consistency check)
|
|
10
|
-
* - MAX_AMOUNT per-order limit (over-limit rejected, avoids channel errors/risk control)
|
|
11
|
-
* - Order idempotency (idempotencyKey) + duplicate-order guard for the same business order (duplicateOrderStrategy)
|
|
12
|
-
* - Callback verification + time window + nonce anti-replay + callback idempotency (persist before responding)
|
|
13
|
-
* - Conflict freeze / paid-after-close freeze / large-amount manual review (AUDIT_THRESHOLD + confirm)
|
|
14
|
-
* - Operation audit (order/callback-book/freeze/confirm all logged)
|
|
15
|
-
* - onVerified business hook (must be idempotent) / onAlert alert hook
|
|
16
|
-
*/
|
|
17
|
-
import { applyClose, applyConfirm, applyPaidCallback } from './state-machine.js';
|
|
18
|
-
import { createMemoryStore } from './store.js';
|
|
19
|
-
import { verifySignedCallback } from './verify.js';
|
|
20
|
-
function isValidAmount(value) {
|
|
21
|
-
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
22
|
-
}
|
|
23
|
-
function isValidCreateOrderRequest(req) {
|
|
24
|
-
if (typeof req !== 'object' || req === null || Array.isArray(req))
|
|
25
|
-
return false;
|
|
26
|
-
const value = req;
|
|
27
|
-
return (typeof value.orderId === 'string' &&
|
|
28
|
-
value.orderId.trim().length > 0 &&
|
|
29
|
-
typeof value.subject === 'string' &&
|
|
30
|
-
value.subject.trim().length > 0 &&
|
|
31
|
-
isValidAmount(value.amount) &&
|
|
32
|
-
(value.idempotencyKey === undefined ||
|
|
33
|
-
(typeof value.idempotencyKey === 'string' && value.idempotencyKey.length > 0)));
|
|
34
|
-
}
|
|
35
|
-
export function createPaymentManager(options = {}) {
|
|
36
|
-
const config = options.config ?? {};
|
|
37
|
-
const store = options.store ??
|
|
38
|
-
createMemoryStore({
|
|
39
|
-
...(config.idempotencyTtlMs === undefined ? {} : { retentionMs: config.idempotencyTtlMs }),
|
|
40
|
-
});
|
|
41
|
-
const apiKey = options.apiKey;
|
|
42
|
-
const configuredNotifyUrl = options.notifyUrl;
|
|
43
|
-
const getOrderAmount = options.getOrderAmount;
|
|
44
|
-
const drivers = new Map();
|
|
45
|
-
const pendingOrderCreates = new Map();
|
|
46
|
-
const pendingIdempotencyCreates = new Map();
|
|
47
|
-
const pendingDeliveries = new Map();
|
|
48
|
-
const notifyVerified = options.onVerified;
|
|
49
|
-
const notifyAlert = options.onAlert;
|
|
50
|
-
const notifyUrlFor = (driverName) => {
|
|
51
|
-
const value = typeof configuredNotifyUrl === 'function'
|
|
52
|
-
? configuredNotifyUrl(driverName)
|
|
53
|
-
: configuredNotifyUrl;
|
|
54
|
-
return typeof value === 'string' ? value : '';
|
|
55
|
-
};
|
|
56
|
-
const audit = (entry) => {
|
|
57
|
-
store.appendAudit(entry);
|
|
58
|
-
};
|
|
59
|
-
const alert = (alert) => {
|
|
60
|
-
notifyAlert?.(alert);
|
|
61
|
-
};
|
|
62
|
-
const verifiedResultFor = (order) => ({
|
|
63
|
-
orderId: order.orderId,
|
|
64
|
-
state: 'paid',
|
|
65
|
-
...(order.paidAt ? { paidAt: order.paidAt } : {}),
|
|
66
|
-
...(order.channelTxnId ? { channelTxnId: order.channelTxnId } : {}),
|
|
67
|
-
});
|
|
68
|
-
async function deliverVerified(order) {
|
|
69
|
-
if (notifyVerified === undefined || order.deliveryState === 'delivered')
|
|
70
|
-
return true;
|
|
71
|
-
order.deliveryState = 'pending';
|
|
72
|
-
delete order.deliveryError;
|
|
73
|
-
store.saveOrder(order);
|
|
74
|
-
try {
|
|
75
|
-
await notifyVerified(verifiedResultFor(order));
|
|
76
|
-
order.deliveryState = 'delivered';
|
|
77
|
-
store.saveOrder(order);
|
|
78
|
-
audit({ operation: 'delivery', orderId: order.orderId, reason: 'delivered', at: Date.now() });
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
order.deliveryState = 'pending';
|
|
83
|
-
order.deliveryError = String(error);
|
|
84
|
-
store.saveOrder(order);
|
|
85
|
-
audit({ operation: 'delivery', orderId: order.orderId, reason: 'failed', at: Date.now() });
|
|
86
|
-
alert({
|
|
87
|
-
level: 'error',
|
|
88
|
-
code: 'delivery_failed',
|
|
89
|
-
message: String(error),
|
|
90
|
-
orderId: order.orderId,
|
|
91
|
-
});
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
function scheduleDelivery(order) {
|
|
96
|
-
const pending = pendingDeliveries.get(order.orderId);
|
|
97
|
-
if (pending !== undefined)
|
|
98
|
-
return pending;
|
|
99
|
-
const delivery = deliverVerified(order)
|
|
100
|
-
.catch((error) => {
|
|
101
|
-
alert({
|
|
102
|
-
level: 'error',
|
|
103
|
-
code: 'delivery_failed',
|
|
104
|
-
message: String(error),
|
|
105
|
-
orderId: order.orderId,
|
|
106
|
-
});
|
|
107
|
-
return false;
|
|
108
|
-
})
|
|
109
|
-
.finally(() => {
|
|
110
|
-
if (pendingDeliveries.get(order.orderId) === delivery) {
|
|
111
|
-
pendingDeliveries.delete(order.orderId);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
pendingDeliveries.set(order.orderId, delivery);
|
|
115
|
-
return delivery;
|
|
116
|
-
}
|
|
117
|
-
function driverOf(name) {
|
|
118
|
-
const driver = drivers.get(name);
|
|
119
|
-
if (!driver)
|
|
120
|
-
throw new Error(`driver_not_found: ${name}`);
|
|
121
|
-
return driver;
|
|
122
|
-
}
|
|
123
|
-
/** Create an order record (zero-amount orders skip the channel and have no init). */
|
|
124
|
-
function newOrder(input, driver, state) {
|
|
125
|
-
return {
|
|
126
|
-
orderId: input.orderId,
|
|
127
|
-
amount: input.amount,
|
|
128
|
-
subject: input.subject,
|
|
129
|
-
scene: input.scene,
|
|
130
|
-
driver: driver.name,
|
|
131
|
-
state,
|
|
132
|
-
createdAt: Date.now(),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function createOrder(req) {
|
|
136
|
-
if (!isValidCreateOrderRequest(req)) {
|
|
137
|
-
return Promise.resolve({ error: 'bad_request', message: 'Invalid payment order request' });
|
|
138
|
-
}
|
|
139
|
-
const pendingByKey = req.idempotencyKey === undefined
|
|
140
|
-
? undefined
|
|
141
|
-
: pendingIdempotencyCreates.get(req.idempotencyKey);
|
|
142
|
-
if (pendingByKey !== undefined)
|
|
143
|
-
return pendingByKey;
|
|
144
|
-
const pendingByOrder = pendingOrderCreates.get(req.orderId);
|
|
145
|
-
if (pendingByOrder !== undefined)
|
|
146
|
-
return pendingByOrder;
|
|
147
|
-
const operation = createOrderInternal(req);
|
|
148
|
-
pendingOrderCreates.set(req.orderId, operation);
|
|
149
|
-
if (req.idempotencyKey !== undefined) {
|
|
150
|
-
pendingIdempotencyCreates.set(req.idempotencyKey, operation);
|
|
151
|
-
}
|
|
152
|
-
const releasePending = () => {
|
|
153
|
-
if (pendingOrderCreates.get(req.orderId) === operation) {
|
|
154
|
-
pendingOrderCreates.delete(req.orderId);
|
|
155
|
-
}
|
|
156
|
-
if (req.idempotencyKey !== undefined &&
|
|
157
|
-
pendingIdempotencyCreates.get(req.idempotencyKey) === operation) {
|
|
158
|
-
pendingIdempotencyCreates.delete(req.idempotencyKey);
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
void operation.then(releasePending, releasePending);
|
|
162
|
-
return operation;
|
|
163
|
-
}
|
|
164
|
-
async function createOrderInternal(req) {
|
|
165
|
-
const orderId = req.orderId;
|
|
166
|
-
if (getOrderAmount === undefined) {
|
|
167
|
-
return { error: 'bad_request', message: 'Server-side order pricing is required' };
|
|
168
|
-
}
|
|
169
|
-
// ① idempotency: same idempotencyKey returns the same order
|
|
170
|
-
if (req.idempotencyKey) {
|
|
171
|
-
const existingId = store.getOrderByIdempotencyKey(req.idempotencyKey);
|
|
172
|
-
if (existingId) {
|
|
173
|
-
const existing = store.getOrder(existingId);
|
|
174
|
-
if (existing) {
|
|
175
|
-
return {
|
|
176
|
-
kind: 'ok',
|
|
177
|
-
driver: existing.driver,
|
|
178
|
-
orderId: existing.orderId,
|
|
179
|
-
state: existing.state,
|
|
180
|
-
...(existing.init ? { init: existing.init } : {}),
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
const existingOrder = store.getOrder(orderId);
|
|
186
|
-
const reserved = existingOrder === undefined && store.reserveOrder(orderId);
|
|
187
|
-
if (existingOrder === undefined && !reserved) {
|
|
188
|
-
return { error: 'order_conflict', message: `order is being created for ${orderId}` };
|
|
189
|
-
}
|
|
190
|
-
try {
|
|
191
|
-
// ② server-side pricing: amount from business (security red line — never trust the client amount)
|
|
192
|
-
const expected = await getOrderAmount(orderId);
|
|
193
|
-
if (expected === undefined)
|
|
194
|
-
return { error: 'order_not_found' };
|
|
195
|
-
if (!isValidAmount(expected)) {
|
|
196
|
-
return { error: 'bad_request', message: 'Invalid server-side order amount' };
|
|
197
|
-
}
|
|
198
|
-
if (req.amount !== expected)
|
|
199
|
-
return { error: 'amount_mismatch', expected };
|
|
200
|
-
// ③ per-order limit: reject over-limit (avoids channel errors/risk control)
|
|
201
|
-
if (config.maxAmount !== undefined && expected > config.maxAmount) {
|
|
202
|
-
return { error: 'amount_exceeds_limit', max: config.maxAmount };
|
|
203
|
-
}
|
|
204
|
-
// ④ duplicate guard: an unpaid order already exists
|
|
205
|
-
if (existingOrder && existingOrder.state === 'created') {
|
|
206
|
-
const strategy = req.duplicateStrategy ?? config.duplicateOrderStrategy ?? 'ask';
|
|
207
|
-
if (strategy === 'ask') {
|
|
208
|
-
return {
|
|
209
|
-
kind: 'duplicate',
|
|
210
|
-
existingOrderId: existingOrder.orderId,
|
|
211
|
-
existingAmount: existingOrder.amount,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
if (strategy === 'reuse') {
|
|
215
|
-
return {
|
|
216
|
-
kind: 'ok',
|
|
217
|
-
driver: existingOrder.driver,
|
|
218
|
-
orderId: existingOrder.orderId,
|
|
219
|
-
state: existingOrder.state,
|
|
220
|
-
...(existingOrder.init ? { init: existingOrder.init } : {}),
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
if (strategy === 'reject') {
|
|
224
|
-
return { error: 'order_conflict', message: `active order exists for ${orderId}` };
|
|
225
|
-
}
|
|
226
|
-
existingOrder.state = 'closed';
|
|
227
|
-
store.saveOrder(existingOrder);
|
|
228
|
-
audit({
|
|
229
|
-
operation: 'close',
|
|
230
|
-
orderId,
|
|
231
|
-
from: 'created',
|
|
232
|
-
to: 'closed',
|
|
233
|
-
reason: 'duplicate_replace',
|
|
234
|
-
at: Date.now(),
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
// ⑤ zero-amount: skip the channel, mark paid directly
|
|
238
|
-
if (expected === 0) {
|
|
239
|
-
const driver = driverOf(req.driver ?? availableDrivers()[0]?.name ?? '');
|
|
240
|
-
const record = newOrder({ orderId, amount: 0, subject: req.subject, scene: req.scene ?? 'pc', notifyUrl: '' }, driver, 'paid');
|
|
241
|
-
record.paidAt = new Date().toISOString();
|
|
242
|
-
store.saveOrder(record);
|
|
243
|
-
if (req.idempotencyKey)
|
|
244
|
-
store.setIdempotencyKey(req.idempotencyKey, orderId);
|
|
245
|
-
audit({ operation: 'create', orderId, to: 'paid', reason: 'zero_amount', at: Date.now() });
|
|
246
|
-
void scheduleDelivery(record);
|
|
247
|
-
return { kind: 'ok', driver: driver.name, orderId, state: 'paid' };
|
|
248
|
-
}
|
|
249
|
-
// ⑥ place the order via the channel
|
|
250
|
-
const driver = driverOf(req.driver ?? availableDrivers()[0]?.name ?? '');
|
|
251
|
-
const notifyUrl = notifyUrlFor(driver.name);
|
|
252
|
-
if (driver.callbackVerification === 'driver' && notifyUrl.length === 0) {
|
|
253
|
-
return {
|
|
254
|
-
error: 'bad_request',
|
|
255
|
-
message: 'A server-owned notifyUrl is required for driver-verified callbacks',
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
let init;
|
|
259
|
-
try {
|
|
260
|
-
init = await driver.createOrder({
|
|
261
|
-
orderId,
|
|
262
|
-
amount: expected,
|
|
263
|
-
subject: req.subject,
|
|
264
|
-
scene: req.scene ?? 'pc',
|
|
265
|
-
notifyUrl,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
catch (error) {
|
|
269
|
-
alert({
|
|
270
|
-
level: 'error',
|
|
271
|
-
code: 'channel_error',
|
|
272
|
-
message: String(error),
|
|
273
|
-
driver: driver.name,
|
|
274
|
-
});
|
|
275
|
-
return { error: 'channel_unavailable', message: String(error) };
|
|
276
|
-
}
|
|
277
|
-
const record = newOrder({
|
|
278
|
-
orderId,
|
|
279
|
-
amount: expected,
|
|
280
|
-
subject: req.subject,
|
|
281
|
-
scene: req.scene ?? 'pc',
|
|
282
|
-
notifyUrl,
|
|
283
|
-
}, driver, 'created');
|
|
284
|
-
record.init = init;
|
|
285
|
-
store.saveOrder(record);
|
|
286
|
-
if (req.idempotencyKey)
|
|
287
|
-
store.setIdempotencyKey(req.idempotencyKey, orderId);
|
|
288
|
-
audit({ operation: 'create', orderId, to: 'created', at: Date.now() });
|
|
289
|
-
return {
|
|
290
|
-
kind: 'ok',
|
|
291
|
-
driver: driver.name,
|
|
292
|
-
orderId,
|
|
293
|
-
state: 'created',
|
|
294
|
-
...(init ? { init } : {}),
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
finally {
|
|
298
|
-
if (reserved)
|
|
299
|
-
store.releaseOrderReservation(orderId);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
function availableDrivers() {
|
|
303
|
-
return Array.from(drivers.values()).map((d) => ({ name: d.name, scenes: d.scenes }));
|
|
304
|
-
}
|
|
305
|
-
function getStatus(orderId) {
|
|
306
|
-
const order = store.getOrder(orderId);
|
|
307
|
-
if (!order)
|
|
308
|
-
return undefined;
|
|
309
|
-
return { state: order.state, ...(order.paidAt ? { paidAt: order.paidAt } : {}) };
|
|
310
|
-
}
|
|
311
|
-
function close(orderId) {
|
|
312
|
-
const order = store.getOrder(orderId);
|
|
313
|
-
if (!order)
|
|
314
|
-
return undefined;
|
|
315
|
-
const before = order.state;
|
|
316
|
-
const { state, ok } = applyClose(order.state);
|
|
317
|
-
if (ok) {
|
|
318
|
-
order.state = state;
|
|
319
|
-
store.saveOrder(order);
|
|
320
|
-
audit({ operation: 'close', orderId, from: before, to: state, at: Date.now() });
|
|
321
|
-
}
|
|
322
|
-
return { state: order.state, ...(order.paidAt ? { paidAt: order.paidAt } : {}) };
|
|
323
|
-
}
|
|
324
|
-
function confirm(orderId) {
|
|
325
|
-
const order = store.getOrder(orderId);
|
|
326
|
-
if (!order)
|
|
327
|
-
return undefined;
|
|
328
|
-
const before = order.state;
|
|
329
|
-
const { state, ok } = applyConfirm(order.state);
|
|
330
|
-
if (ok) {
|
|
331
|
-
order.state = state;
|
|
332
|
-
order.paidAt = new Date().toISOString();
|
|
333
|
-
store.saveOrder(order);
|
|
334
|
-
audit({ operation: 'confirm', orderId, from: before, to: state, at: Date.now() });
|
|
335
|
-
void scheduleDelivery(order);
|
|
336
|
-
}
|
|
337
|
-
return { state: order.state, ...(order.paidAt ? { paidAt: order.paidAt } : {}) };
|
|
338
|
-
}
|
|
339
|
-
async function syncOrderStatus(orderId) {
|
|
340
|
-
const order = store.getOrder(orderId);
|
|
341
|
-
if (!order)
|
|
342
|
-
return undefined;
|
|
343
|
-
const driver = drivers.get(order.driver);
|
|
344
|
-
if (!driver)
|
|
345
|
-
return undefined;
|
|
346
|
-
const status = await driver.queryOrder(orderId);
|
|
347
|
-
// reconciliation: channel paid & local unpaid → migrate via state machine (amount from order; large-amount audit also applies)
|
|
348
|
-
if (status.state === 'paid' && order.state === 'created') {
|
|
349
|
-
const before = order.state;
|
|
350
|
-
const transition = applyPaidCallback(order.state, order.amount, order.amount, config.auditThreshold);
|
|
351
|
-
if (transition.state !== before) {
|
|
352
|
-
order.state = transition.state;
|
|
353
|
-
if (transition.state === 'paid') {
|
|
354
|
-
order.paidAt = new Date().toISOString();
|
|
355
|
-
}
|
|
356
|
-
store.saveOrder(order);
|
|
357
|
-
audit({
|
|
358
|
-
operation: 'callback',
|
|
359
|
-
orderId,
|
|
360
|
-
from: before,
|
|
361
|
-
to: transition.state,
|
|
362
|
-
reason: transition.reason ?? 'sync',
|
|
363
|
-
at: Date.now(),
|
|
364
|
-
});
|
|
365
|
-
if (transition.state === 'paid') {
|
|
366
|
-
void scheduleDelivery(order);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
return { state: order.state, ...(order.paidAt ? { paidAt: order.paidAt } : {}) };
|
|
371
|
-
}
|
|
372
|
-
function scanExpiredOrders(ttlMs) {
|
|
373
|
-
const now = Date.now();
|
|
374
|
-
const closed = [];
|
|
375
|
-
for (const order of store.allOrders()) {
|
|
376
|
-
if (order.state === 'created' && now - order.createdAt >= ttlMs) {
|
|
377
|
-
const before = order.state;
|
|
378
|
-
const { state, ok } = applyClose(order.state);
|
|
379
|
-
if (ok) {
|
|
380
|
-
order.state = state;
|
|
381
|
-
store.saveOrder(order);
|
|
382
|
-
audit({
|
|
383
|
-
operation: 'close',
|
|
384
|
-
orderId: order.orderId,
|
|
385
|
-
from: before,
|
|
386
|
-
to: state,
|
|
387
|
-
reason: 'expired',
|
|
388
|
-
at: now,
|
|
389
|
-
});
|
|
390
|
-
closed.push({ orderId: order.orderId, state });
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
return closed;
|
|
395
|
-
}
|
|
396
|
-
async function handleCallback(driverName, ctx) {
|
|
397
|
-
const driver = drivers.get(driverName);
|
|
398
|
-
if (!driver) {
|
|
399
|
-
return { ok: false, status: 404, body: { ok: false, error: 'driver_not_found' } };
|
|
400
|
-
}
|
|
401
|
-
const timestamp = ctx.headers['x-timestamp'];
|
|
402
|
-
const nonce = ctx.headers['x-nonce'];
|
|
403
|
-
const signature = ctx.headers['x-signature'];
|
|
404
|
-
const isGenericProtocol = driver.callbackVerification !== 'driver';
|
|
405
|
-
if (isGenericProtocol) {
|
|
406
|
-
if (apiKey === undefined || apiKey.length === 0) {
|
|
407
|
-
return { ok: false, status: 500, body: { ok: false, error: 'server_configuration' } };
|
|
408
|
-
}
|
|
409
|
-
// generic verification protocol (X-* headers + HMAC-SHA256): for mock/self-built backends
|
|
410
|
-
const verified = await verifySignedCallback({
|
|
411
|
-
apiKey,
|
|
412
|
-
timestamp: timestamp,
|
|
413
|
-
nonce: nonce,
|
|
414
|
-
signature: signature,
|
|
415
|
-
body: ctx.body,
|
|
416
|
-
...(config.timeWindowSec !== undefined ? { timeWindowSec: config.timeWindowSec } : {}),
|
|
417
|
-
});
|
|
418
|
-
if (!verified.ok) {
|
|
419
|
-
alert({
|
|
420
|
-
level: 'warn',
|
|
421
|
-
code: 'verify_failed',
|
|
422
|
-
message: verified.reason,
|
|
423
|
-
driver: driverName,
|
|
424
|
-
});
|
|
425
|
-
return { ok: false, status: 401, body: { ok: false, error: verified.reason } };
|
|
426
|
-
}
|
|
427
|
-
// nonce anti-replay (idempotency stops double-booking; nonce stops forged replays)
|
|
428
|
-
if (!store.addNonce(nonce)) {
|
|
429
|
-
return { ok: false, status: 401, body: { ok: false, error: 'nonce_reused' } };
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
// otherwise: channel self-verification path — real channels (WeChat/Alipay) use their own verification protocol (platform cert/RSA2),
|
|
433
|
-
// handled internally by driver.verifyCallback (verify/decrypt/anti-replay; the driver holds the store for nonce dedup).
|
|
434
|
-
// Parse payload:
|
|
435
|
-
// - generic protocol path: try JSON.parse (mock path; payload is an object)
|
|
436
|
-
// - channel self-verify path: raw body (WeChat callback is outer JSON; Alipay notify is form-encoded),
|
|
437
|
-
// parsed + verified (platform cert/RSA2) + anti-replay inside driver.verifyCallback
|
|
438
|
-
let payload;
|
|
439
|
-
if (isGenericProtocol) {
|
|
440
|
-
try {
|
|
441
|
-
payload = JSON.parse(ctx.body);
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
payload = ctx.body;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
else {
|
|
448
|
-
payload = ctx.body;
|
|
449
|
-
}
|
|
450
|
-
const callback = driver.verifyCallback(payload, ctx.headers);
|
|
451
|
-
if (!callback.ok || !callback.orderId || callback.paidAmount === undefined) {
|
|
452
|
-
return {
|
|
453
|
-
ok: false,
|
|
454
|
-
status: 400,
|
|
455
|
-
body: { ok: false, error: callback.reason ?? 'bad_callback' },
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
const order = store.getOrder(callback.orderId);
|
|
459
|
-
if (!order) {
|
|
460
|
-
return { ok: false, status: 404, body: { ok: false, error: 'order_not_found' } };
|
|
461
|
-
}
|
|
462
|
-
// state machine (amount match / idempotency / closed→frozen / large-amount pending / paid)
|
|
463
|
-
const before = order.state;
|
|
464
|
-
const transition = applyPaidCallback(order.state, order.amount, callback.paidAmount, config.auditThreshold);
|
|
465
|
-
// persist first, then respond (callback response protocol)
|
|
466
|
-
if (transition.state !== order.state) {
|
|
467
|
-
order.state = transition.state;
|
|
468
|
-
if (transition.state === 'paid') {
|
|
469
|
-
order.paidAt = new Date().toISOString();
|
|
470
|
-
if (callback.channelTxnId)
|
|
471
|
-
order.channelTxnId = callback.channelTxnId;
|
|
472
|
-
}
|
|
473
|
-
if (transition.state === 'pending' && transition.reason) {
|
|
474
|
-
order.auditReason = transition.reason;
|
|
475
|
-
}
|
|
476
|
-
audit({
|
|
477
|
-
operation: transition.state === 'frozen' ? 'freeze' : 'callback',
|
|
478
|
-
orderId: order.orderId,
|
|
479
|
-
from: before,
|
|
480
|
-
to: transition.state,
|
|
481
|
-
...(transition.reason ? { reason: transition.reason } : {}),
|
|
482
|
-
at: Date.now(),
|
|
483
|
-
});
|
|
484
|
-
store.saveOrder(order);
|
|
485
|
-
}
|
|
486
|
-
if (order.state === 'paid' && order.deliveryState !== 'delivered') {
|
|
487
|
-
void scheduleDelivery(order);
|
|
488
|
-
}
|
|
489
|
-
if (transition.state === 'frozen') {
|
|
490
|
-
alert({
|
|
491
|
-
level: 'error',
|
|
492
|
-
code: 'order_frozen',
|
|
493
|
-
message: transition.reason ?? 'frozen',
|
|
494
|
-
orderId: order.orderId,
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
if (transition.state === 'pending') {
|
|
498
|
-
alert({
|
|
499
|
-
level: 'warn',
|
|
500
|
-
code: 'audit_required',
|
|
501
|
-
message: 'large amount pending review',
|
|
502
|
-
orderId: order.orderId,
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
const body = { ok: true, orderId: order.orderId, state: order.state };
|
|
506
|
-
if (transition.reason)
|
|
507
|
-
body['reason'] = transition.reason;
|
|
508
|
-
return {
|
|
509
|
-
ok: true,
|
|
510
|
-
status: 200,
|
|
511
|
-
body,
|
|
512
|
-
orderId: order.orderId,
|
|
513
|
-
state: order.state,
|
|
514
|
-
...(transition.reason ? { reason: transition.reason } : {}),
|
|
515
|
-
...(driver.successResponseText ? { text: driver.successResponseText } : {}),
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
return {
|
|
519
|
-
registerDriver(driver) {
|
|
520
|
-
drivers.set(driver.name, driver);
|
|
521
|
-
},
|
|
522
|
-
available: availableDrivers,
|
|
523
|
-
createOrder,
|
|
524
|
-
getStatus,
|
|
525
|
-
close,
|
|
526
|
-
confirm,
|
|
527
|
-
syncOrderStatus,
|
|
528
|
-
scanExpiredOrders,
|
|
529
|
-
async retryDelivery(orderId) {
|
|
530
|
-
const order = store.getOrder(orderId);
|
|
531
|
-
if (order === undefined)
|
|
532
|
-
return undefined;
|
|
533
|
-
return scheduleDelivery(order);
|
|
534
|
-
},
|
|
535
|
-
handleCallback,
|
|
536
|
-
auditLog: () => store.auditLog(),
|
|
537
|
-
};
|
|
538
|
-
}
|
package/dist/mock-driver.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Mock payment driver — for P0 end-to-end demos and integration.
|
|
7
|
-
*
|
|
8
|
-
* - `createOrder`: doesn't call a real channel; returns one-time QR pay params (simulated qrUrl).
|
|
9
|
-
* - `verifyCallback`: package-level verification is done by PaymentManager (generic HMAC protocol);
|
|
10
|
-
* the mock driver's verifyCallback only does **normalization parsing** (body → VerifiedCallback).
|
|
11
|
-
* - `queryOrder`: reads the store (no real channel state).
|
|
12
|
-
*
|
|
13
|
-
* Real channels (wechat_official / alipay_official) are implemented in P1, replacing this with real signing/messages.
|
|
14
|
-
*/
|
|
15
|
-
import type { PaymentDriver } from './types.js';
|
|
16
|
-
import type { PaymentStore } from './store.js';
|
|
17
|
-
export interface MockDriverOptions {
|
|
18
|
-
readonly name?: string;
|
|
19
|
-
/** Simulates a channel that always succeeds (default true); set false to demo channel failure. */
|
|
20
|
-
readonly alwaysSucceed?: boolean;
|
|
21
|
-
}
|
|
22
|
-
export declare function createMockDriver(store: PaymentStore, options?: MockDriverOptions): PaymentDriver;
|
package/dist/mock-driver.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Mock payment driver — for P0 end-to-end demos and integration.
|
|
7
|
-
*
|
|
8
|
-
* - `createOrder`: doesn't call a real channel; returns one-time QR pay params (simulated qrUrl).
|
|
9
|
-
* - `verifyCallback`: package-level verification is done by PaymentManager (generic HMAC protocol);
|
|
10
|
-
* the mock driver's verifyCallback only does **normalization parsing** (body → VerifiedCallback).
|
|
11
|
-
* - `queryOrder`: reads the store (no real channel state).
|
|
12
|
-
*
|
|
13
|
-
* Real channels (wechat_official / alipay_official) are implemented in P1, replacing this with real signing/messages.
|
|
14
|
-
*/
|
|
15
|
-
export function createMockDriver(store, options) {
|
|
16
|
-
const name = options?.name ?? 'mock';
|
|
17
|
-
const alwaysSucceed = options?.alwaysSucceed ?? true;
|
|
18
|
-
return {
|
|
19
|
-
name,
|
|
20
|
-
scenes: ['pc', 'h5'],
|
|
21
|
-
callbackVerification: 'manager-hmac',
|
|
22
|
-
createOrder(input) {
|
|
23
|
-
if (!alwaysSucceed) {
|
|
24
|
-
return Promise.reject(new Error('channel_unavailable'));
|
|
25
|
-
}
|
|
26
|
-
// mock channel: scan scenes return qr; real channels return jsapi/qr/redirect per scene
|
|
27
|
-
return Promise.resolve({
|
|
28
|
-
kind: 'qr',
|
|
29
|
-
qrUrl: `https://pay.example.com/qr/${input.orderId}`,
|
|
30
|
-
expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
|
|
31
|
-
});
|
|
32
|
-
},
|
|
33
|
-
verifyCallback(payload, _headers) {
|
|
34
|
-
// mock normalization: parse the unified callback body (real channels verify signatures + map fields in their own drivers)
|
|
35
|
-
const body = payload;
|
|
36
|
-
const orderId = body['orderId'];
|
|
37
|
-
const paidAmount = body['paidAmount'];
|
|
38
|
-
if (typeof orderId !== 'string' || typeof paidAmount !== 'number') {
|
|
39
|
-
return { ok: false, reason: 'bad_payload' };
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
ok: true,
|
|
43
|
-
orderId,
|
|
44
|
-
paidAmount,
|
|
45
|
-
...(typeof body['channelTxnId'] === 'string' ? { channelTxnId: body['channelTxnId'] } : {}),
|
|
46
|
-
};
|
|
47
|
-
},
|
|
48
|
-
queryOrder(orderId) {
|
|
49
|
-
const order = store.getOrder(orderId);
|
|
50
|
-
if (!order) {
|
|
51
|
-
return Promise.resolve({ state: 'unknown', raw: { error: 'order_not_found' } });
|
|
52
|
-
}
|
|
53
|
-
return Promise.resolve({
|
|
54
|
-
state: order.state,
|
|
55
|
-
...(order.paidAt ? { paidAt: order.paidAt } : {}),
|
|
56
|
-
});
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|