@zucker-framework/payment 1.0.0 → 1.0.3

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/dist/index.d.mts CHANGED
@@ -1,125 +1,462 @@
1
+ import * as _paypal_paypal_server_sdk from '@paypal/paypal-server-sdk';
2
+ import { OrdersController, Order as Order$1 } from '@paypal/paypal-server-sdk';
3
+ import Stripe from 'stripe';
4
+ import { ModelOperations } from '@zucker-framework/core';
1
5
  import { EventEmitter2 } from '@nestjs/event-emitter';
2
6
  import { Type, DynamicModule, InjectionToken } from '@nestjs/common';
3
7
 
4
8
  /**
5
- * 支付网关接口 — 不绑定任何具体支付商。
6
- * 用户通过注入 Stripe/PayPal/微信支付适配器来使用。
9
+ * 结算状态的中立模型与迁移规则。
10
+ *
11
+ * 这是多渠道支付里最容易出错、也最值得统一的一层:各渠道的状态名不同
12
+ * (Stripe 的 requires_capture / succeeded、PayPal 的 APPROVED / COMPLETED、
13
+ * 微信的 NOTPAY / SUCCESS / CLOSED),但"钱有没有动"的语义是共通的。
14
+ *
15
+ * 适配器只负责把供应商状态映射成这里的 SettlementState,不做任何持久化决策;
16
+ * 迁移是否允许由 reconcile() 统一裁决,保证所有渠道遵守同一套安全性质。
7
17
  */
8
- interface PaymentProvider {
9
- id: string;
10
- name: string;
11
- /** 创建支付意图/订单 */
12
- createPaymentIntent(params: CreatePaymentParams): Promise<PaymentIntent>;
13
- /** 确认支付 */
14
- confirmPayment(intentId: string): Promise<PaymentResult>;
15
- /** 退款 */
16
- refund(paymentId: string, amount?: number, reason?: string): Promise<RefundResult>;
17
- /** 查询支付状态 */
18
- getPaymentStatus(intentId: string): Promise<PaymentStatus>;
19
- /** 处理 Webhook */
20
- handleWebhook(payload: unknown, signature: string): Promise<WebhookEvent>;
21
- }
22
- interface CreatePaymentParams {
23
- amount: number;
18
+ declare const SETTLEMENT_STATES: readonly ["created", "authorized", "captured", "refunded", "partially_refunded", "failed", "canceled", "unknown"];
19
+ type SettlementState = (typeof SETTLEMENT_STATES)[number];
20
+ declare function isMoneyMoved(state: SettlementState): boolean;
21
+ declare function isTerminal(state: SettlementState): boolean;
22
+ /** reconcile 的裁决结果,供调用方决定是否写库、是否告警。 */
23
+ interface SettlementDecision {
24
+ /** 应当持久化的状态;与 current 相同表示不必写库 */
25
+ next: SettlementState;
26
+ /** 是否发生了实际变化 */
27
+ changed: boolean;
28
+ /**
29
+ * 观察到的状态被拒绝采纳时给出原因,调用方应据此告警而不是静默丢弃。
30
+ * 例如已扣款的订单收到 failed 通知——这通常意味着两边对不上,需要人工核对。
31
+ */
32
+ rejected?: "money-already-moved" | "already-terminal" | "not-observable";
33
+ }
34
+ /**
35
+ * 依据当前持久化状态与本次观察到的状态,裁决应当落库的状态。
36
+ *
37
+ * 安全性质(由测试锁定):
38
+ * 1. unknown 永不覆盖任何已知状态——查不到不等于没付。
39
+ * 2. 钱已移动过的状态不会被 failed / canceled 回退——迟到的失败回调不能抹掉已收款。
40
+ * 3. 终态不会被非终态覆盖——已退款的订单不会因为一条旧的 captured 回调变回已付。
41
+ * 4. captured → partially_refunded → refunded 单向推进,不可逆。
42
+ * 5. 幂等:同样的观察重复裁决,结果不变且 changed=false。
43
+ */
44
+ declare function reconcile(current: SettlementState, observed: SettlementState): SettlementDecision;
45
+
46
+ /**
47
+ * 多渠道支付的中立契约。
48
+ *
49
+ * 取代 ../payment.interface.ts 里那份未经任何适配器验证的 PaymentProvider。
50
+ * 形状取自 CNIPA 已跑通 Stripe + PayPal 的 PaymentGateway,并按 Medusa
51
+ * AbstractPaymentProcessor 的生命周期补齐授权/扣款分离,同时预留微信、
52
+ * 支付宝这类非跳转渠道所需的结构。
53
+ */
54
+ /**
55
+ * 金额。
56
+ *
57
+ * 刻意用十进制字符串而不是 number:
58
+ * - 旧 PaymentProvider 用 `number` + 注释"最小单位(分)",CNIPA 用 Decimal 元,
59
+ * 两套单位并存迟早在某个边界上错一位。
60
+ * - 浮点数表示钱本身就是错的。
61
+ * - JPY/HUF/TWD 无小数位,cents 模型对它们是特例;字符串对所有币种一致。
62
+ * 适配器负责在边界上转成各自 SDK 需要的形式。
63
+ */
64
+ interface Money {
65
+ /** 十进制字符串,如 "29.00";不接受 number */
66
+ amount: string;
67
+ /** ISO 4217,大写 */
24
68
  currency: string;
69
+ }
70
+ /**
71
+ * 用户需要执行的下一步动作。
72
+ *
73
+ * 不能简化成 `redirectUrl?: string`——微信 Native 下单返回的是
74
+ * `weixin://wxpay/bizpayurl?pr=...`,是要渲染成二维码给用户扫的字符串,
75
+ * 不是能 location.assign 过去的地址;支付宝 PC 常返回需自动提交的表单。
76
+ * 用判别联合表达,调用方按 kind 分派,新增渠道不必改已有分支。
77
+ */
78
+ type PaymentNextAction = {
79
+ kind: "redirect";
80
+ url: string;
81
+ } | {
82
+ kind: "qrcode";
83
+ value: string;
84
+ } | {
85
+ kind: "form_post";
86
+ action: string;
87
+ fields: Record<string, string>;
88
+ } | {
89
+ kind: "sdk_params";
90
+ params: Record<string, unknown>;
91
+ } | {
92
+ kind: "none";
93
+ };
94
+ interface PaymentSession {
95
+ /** 供应商侧的支付标识,后续 capture / retrieve / refund 都用它 */
96
+ providerRef: string;
97
+ nextAction: PaymentNextAction;
98
+ state: SettlementState;
99
+ /**
100
+ * 供应商侧记录的金额。
101
+ * 放在契约里而不是让调用方去 raw 里翻:金额是每个渠道都必须如实回传的字段,
102
+ * 调用方要拿它和本地订单核对,不该为此了解某个供应商的响应结构。
103
+ */
104
+ money: Money;
105
+ /** 供应商原始响应,供排查与对账留痕;不参与业务判断 */
106
+ raw?: unknown;
107
+ }
108
+ interface CreatePaymentInput {
109
+ /** 调用方的订单标识,会回传到供应商以便双向核对 */
110
+ reference: string;
111
+ money: Money;
25
112
  description?: string;
26
- metadata?: Record<string, string>;
27
- customerId?: string;
113
+ /** 付款完成/取消后的回跳地址;不适用的渠道可忽略 */
28
114
  returnUrl?: string;
29
115
  cancelUrl?: string;
116
+ /**
117
+ * 幂等键。同一个键重试必须返回同一笔支付,不得产生第二笔。
118
+ * 旧接口完全没有这个概念,重试即重复下单。
119
+ */
120
+ idempotencyKey: string;
121
+ metadata?: Record<string, string>;
30
122
  }
31
- interface PaymentIntent {
32
- id: string;
33
- clientSecret?: string;
34
- status: PaymentStatus;
35
- amount: number;
36
- currency: string;
37
- redirectUrl?: string;
123
+ /**
124
+ * Webhook 原始请求。
125
+ *
126
+ * 必须同时给出原始字节与**全部**请求头:PayPal 验签需要 5 个头
127
+ * (paypal-transmission-id/time/sig、paypal-cert-url、paypal-auth-algo),
128
+ * 旧接口的 `signature: string` 结构上装不下;微信 v3 同样需要多个头。
129
+ * 验签必须针对原始字节,序列化过一轮再验会因键序/数字规范化失败。
130
+ */
131
+ interface WebhookRequest {
132
+ rawBody: Uint8Array | string;
133
+ headers: Record<string, string>;
38
134
  }
39
- type PaymentStatus = 'pending' | 'processing' | 'succeeded' | 'failed' | 'cancelled' | 'refunded';
40
- interface PaymentResult {
135
+ interface WebhookObservation {
136
+ /** 供应商事件类型原文,便于排查 */
137
+ type: string;
138
+ /** 本次事件指向的支付;无法关联时为 undefined,调用方应告警而非静默丢弃 */
139
+ providerRef?: string;
140
+ /** 归一化后的状态;无法判定时为 unknown */
141
+ state: SettlementState;
142
+ raw: unknown;
143
+ }
144
+ interface RefundInput {
145
+ providerRef: string;
146
+ /** 省略表示全额退款 */
147
+ money?: Money;
148
+ reason?: string;
149
+ idempotencyKey: string;
150
+ }
151
+ /**
152
+ * 渠道适配器。
153
+ *
154
+ * 实现约定:
155
+ * - 只做协议与状态映射,不做任何产品决策(金额、资格、时机、文案)。
156
+ * - retrieve 必须是只读的。对账会调用它,绝不能在对账过程中扣款——
157
+ * "查询顺手把 APPROVED 捕获了"是这类系统最贵的一类 bug。
158
+ * - 判定不了就返回 unknown,不要猜成 failed。
159
+ */
160
+ interface PaymentProviderAdapter {
161
+ readonly id: string;
162
+ /**
163
+ * 该渠道客观支持的结算币种;undefined 表示未声明限制。
164
+ * 这是供应商事实,不是产品策略——"我们只卖 USD"属于上层应用。
165
+ */
166
+ readonly supportedCurrencies?: readonly string[];
167
+ createPayment(input: CreatePaymentInput): Promise<PaymentSession>;
168
+ /** 授权与扣款分离的渠道用它完成扣款(PayPal APPROVED → capture) */
169
+ capture(providerRef: string, idempotencyKey: string): Promise<PaymentSession>;
170
+ /** 只读查询,供回跳后主动确认与定时对账使用 */
171
+ retrieve(providerRef: string): Promise<PaymentSession>;
172
+ refund(input: RefundInput): Promise<{
173
+ refundRef: string;
174
+ state: SettlementState;
175
+ }>;
176
+ cancel?(providerRef: string): Promise<PaymentSession>;
177
+ /** 验签并归一化;验签失败必须抛错,不得返回一个"未知"事件蒙混过去 */
178
+ parseWebhook(request: WebhookRequest): Promise<WebhookObservation>;
179
+ }
180
+ /** 该渠道能否处理这个币种;未声明限制时一律放行。 */
181
+ declare function adapterSupportsCurrency(adapter: Pick<PaymentProviderAdapter, "supportedCurrencies">, currency: string): boolean;
182
+
183
+ /**
184
+ * 十进制金额字符串与供应商最小单位之间的转换。
185
+ *
186
+ * 契约对外一律用十进制字符串(见 Money),但 Stripe 等渠道的 API 收最小单位整数。
187
+ * 转换只发生在适配器边界。用字符串补位而不是 `Math.round(x * 100)`,真正的收益有两点
188
+ * (不是"浮点一定算错"——对两位小数的常规金额,乘 100 再取整其实是可靠的):
189
+ * 1. **精度超限会报错而不是静默取整**。`Math.round(1.005 * 100)` 得到 100,
190
+ * 悄悄吞掉半分钱;这里直接拒绝,让调用方去面对它本就不该产生的金额。
191
+ * 2. **零小数位币种不会被乘以 100**。JPY 的 500 就是 500,按两位小数处理会放大 100 倍。
192
+ */
193
+ declare function currencyExponent(currency: string): number;
194
+ /** "29.00" + USD → 2900;"500" + JPY → 500 */
195
+ declare function toMinorUnits(amount: string, currency: string): number;
196
+ /** 2900 + USD → "29.00";500 + JPY → "500" */
197
+ declare function fromMinorUnits(value: number, currency: string): string;
198
+
199
+ /**
200
+ * 渠道适配器注册表。
201
+ *
202
+ * 取代旧的 PaymentService:那一版路由的是从未被任何适配器实现过的 PaymentProvider,
203
+ * 并且把 createPaymentIntent / confirmPayment 之类的业务动作也挂在自己身上。
204
+ * 这里只做注册与查找——编排(建单、履约、对账)属于上层应用,不属于注册表。
205
+ */
206
+ declare class PaymentProviderRegistry {
207
+ private readonly logger;
208
+ private readonly adapters;
209
+ constructor(adapters?: readonly PaymentProviderAdapter[]);
210
+ register(adapter: PaymentProviderAdapter): void;
211
+ has(id: string): boolean;
212
+ get(id: string): PaymentProviderAdapter;
213
+ list(): PaymentProviderAdapter[];
214
+ /** 能处理该币种的渠道;"本产品卖不卖这个币种"由上层决定,不在这里过滤。 */
215
+ listForCurrency(currency: string): PaymentProviderAdapter[];
216
+ }
217
+
218
+ /** Read-only provider evidence; unknown must never be treated as an instruction to charge. */
219
+ type PaymentSettlementState = 'paid' | 'unpaid' | 'unknown';
220
+
221
+ interface PayPalPaymentTransportOptions {
222
+ clientId: string;
223
+ clientSecret: string;
224
+ mode: 'live' | 'sandbox';
225
+ fetcher?: typeof fetch;
226
+ logger?: {
227
+ warn: (message: string) => void;
228
+ error: (message: string) => void;
229
+ };
230
+ }
231
+ interface PayPalCaptureRefundRequest {
232
+ /**
233
+ * 省略表示全额退款——PayPal 的 refund 接口本就允许不传 amount。
234
+ * 原先声明为必填,导致调用方为了"全额退"必须自己算出金额再传回去,
235
+ * 一旦本地金额与供应商记录有出入就会退错数额。
236
+ */
237
+ amount?: {
238
+ value: string;
239
+ currency_code: string;
240
+ };
241
+ note_to_payer?: string;
242
+ }
243
+ interface PayPalCaptureRefundResponse {
244
+ id?: string;
245
+ status?: string;
246
+ message?: string;
247
+ }
248
+ interface PayPalOrderRepresentation {
249
+ id?: string;
250
+ status?: string;
251
+ links?: Array<{
252
+ rel?: string;
253
+ href?: string;
254
+ }>;
255
+ purchaseUnits?: Array<{
256
+ payments?: {
257
+ captures?: Array<{
258
+ id?: string;
259
+ }>;
260
+ };
261
+ }>;
262
+ }
263
+ interface PayPalOrderObservation<T extends PayPalOrderRepresentation = PayPalOrderRepresentation> {
264
+ raw: T;
265
+ id?: string;
266
+ approveUrl?: string;
267
+ captureId?: string;
268
+ settlement: PaymentSettlementState;
269
+ }
270
+ /** APPROVED means approval only, never evidence of a captured payment. */
271
+ declare function observePayPalOrder<T extends PayPalOrderRepresentation>(raw: T): PayPalOrderObservation<T>;
272
+ interface PayPalCaptureObservation {
273
+ raw: Record<string, unknown>;
41
274
  id: string;
42
- status: PaymentStatus;
43
- amount: number;
44
- currency: string;
45
- paidAt?: Date;
275
+ failureReason?: string;
46
276
  }
47
- interface RefundResult {
277
+ type PayPalWebhookObservation = {
278
+ raw: Record<string, unknown>;
279
+ type: string;
280
+ kind: 'capture-succeeded' | 'capture-failed';
281
+ capture: PayPalCaptureObservation;
282
+ } | {
283
+ raw: Record<string, unknown>;
284
+ type: string;
285
+ kind: 'unknown';
286
+ };
287
+ interface PayPalMoneyReturnedObservation {
48
288
  id: string;
49
- paymentId: string;
50
- amount: number;
51
- status: 'pending' | 'succeeded' | 'failed';
52
- reason?: string;
289
+ captureId?: string;
290
+ orderId?: string;
291
+ amount: string;
292
+ currency: string;
293
+ status: 'PENDING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';
53
294
  }
54
- interface WebhookEvent {
295
+ /** Decode only after signature verification; this performs no authentication or network call. */
296
+ declare function decodePayPalWebhookEvent(payload: string): PayPalWebhookObservation;
297
+ /** Additive decoder: the original capture-event discriminated union stays compatible. */
298
+ declare function decodePayPalMoneyReturnedEvent(payload: string): {
299
+ raw: Record<string, unknown>;
55
300
  type: string;
56
- data: Record<string, unknown>;
301
+ kind: 'capture-refunded' | 'capture-reversed';
302
+ returned: PayPalMoneyReturnedObservation;
303
+ } | null;
304
+ /** SDK order transport plus the OAuth-backed REST endpoints absent from the current SDK integration. */
305
+ declare class PayPalPaymentTransport {
306
+ private readonly options;
307
+ readonly orders: OrdersController;
308
+ private readonly baseUrl;
309
+ private readonly fetcher;
310
+ constructor(options: PayPalPaymentTransportOptions);
311
+ createOrder(input: Parameters<OrdersController['createOrder']>[0]): Promise<PayPalOrderObservation<_paypal_paypal_server_sdk.Order>>;
312
+ /** An explicit capture command; never invoked by getOrder or status observation. */
313
+ captureOrder(input: Parameters<OrdersController['captureOrder']>[0]): Promise<PayPalOrderObservation<_paypal_paypal_server_sdk.Order>>;
314
+ getOrder(input: Parameters<OrdersController['getOrder']>[0]): Promise<PayPalOrderObservation<_paypal_paypal_server_sdk.Order>>;
315
+ private getAccessToken;
316
+ verifyWebhookSignature(payload: string, headers: Record<string, string>, webhookId: string): Promise<boolean>;
317
+ refundCapture(captureId: string, request: PayPalCaptureRefundRequest, requestId?: string): Promise<PayPalCaptureRefundResponse>;
57
318
  }
58
- interface SubscriptionPlan {
59
- id: string;
319
+
320
+ /**
321
+ * PayPal Orders v2 适配器。
322
+ *
323
+ * 只做协议转换与状态映射,不含任何产品策略。传输层沿用已在生产验证过的
324
+ * PayPalPaymentTransport,本适配器不重写它。
325
+ */
326
+ interface PayPalAdapterOptions {
327
+ transport: PayPalPaymentTransport;
328
+ /** Webhook 验签用;缺失时 parseWebhook 必须拒绝,而不是放行 */
329
+ webhookId?: string;
330
+ brandName?: string;
331
+ }
332
+ /**
333
+ * PayPal REST 支持的 25 种结算币种。
334
+ * 来源:https://developer.paypal.com/api/rest/reference/currency-codes/
335
+ * 这是供应商事实;"本产品只卖哪几种"属于上层应用,不写在这里。
336
+ */
337
+ declare const PAYPAL_SUPPORTED_CURRENCIES: readonly ["AUD", "BRL", "CAD", "CNY", "CZK", "DKK", "EUR", "HKD", "HUF", "ILS", "JPY", "MYR", "MXN", "TWD", "NZD", "NOK", "PHP", "PLN", "GBP", "RUB", "SGD", "SEK", "CHF", "THB", "USD"];
338
+ /**
339
+ * PayPal 订单状态 → 中立状态。
340
+ *
341
+ * 关键约定:顶层 COMPLETED 还不足以判定已扣款,必须同时看到一条 COMPLETED 的
342
+ * capture;否则停在 authorized 继续观察。识别不了的状态一律 unknown,绝不猜成 failed。
343
+ */
344
+ declare function observePayPalState(order: Order$1): SettlementState;
345
+ declare function createPayPalAdapter(options: PayPalAdapterOptions): PaymentProviderAdapter;
346
+
347
+ interface StripeAdapterOptions {
348
+ client: Stripe;
349
+ /** Webhook 验签密钥;缺失时 parseWebhook 必须拒绝 */
350
+ webhookSecret?: string;
351
+ /** 传给 Checkout 的支付方式配置(pmc_xxx),由上层按业务决定 */
352
+ paymentMethodConfiguration?: string;
353
+ }
354
+ /**
355
+ * Stripe Checkout 会话状态 → 中立状态。
356
+ * 识别不了的组合一律 unknown:Stripe 的异步支付方式(如银行转账)会长时间停在
357
+ * complete + unpaid,那不是失败。
358
+ */
359
+ declare function observeStripeState(session: Stripe.Checkout.Session): SettlementState;
360
+ declare function createStripeAdapter(options: StripeAdapterOptions): PaymentProviderAdapter;
361
+
362
+ /**
363
+ * 渠道适配器契约套件。
364
+ *
365
+ * 多渠道抽象的价值不在接口长什么样,而在于**所有渠道遵守同一组安全性质**。
366
+ * 每接一个渠道都要跑这套;它抓的是那些只在生产环境、只在特定时序下才暴露的错误。
367
+ *
368
+ * 刻意不依赖任何测试框架:返回结构化结果,既能在 vitest/jest 里包一层,
369
+ * 也能被 node --test 或 CI 脚本直接调用。
370
+ */
371
+ interface ContractCheck {
60
372
  name: string;
61
- amount: number;
62
- currency: string;
63
- interval: 'day' | 'week' | 'month' | 'year';
64
- intervalCount: number;
65
- trialDays?: number;
66
- features?: string[];
373
+ ok: boolean;
374
+ detail?: string;
67
375
  }
68
- interface Subscription {
69
- id: string;
70
- planId: string;
71
- customerId: string;
72
- status: 'active' | 'past_due' | 'cancelled' | 'trialing' | 'paused';
73
- currentPeriodStart: Date;
74
- currentPeriodEnd: Date;
75
- cancelAtPeriodEnd: boolean;
76
- }
77
- interface SubscriptionProvider {
78
- createSubscription(customerId: string, planId: string): Promise<Subscription>;
79
- cancelSubscription(subscriptionId: string, immediately?: boolean): Promise<Subscription>;
80
- getSubscription(subscriptionId: string): Promise<Subscription>;
81
- listSubscriptions(customerId: string): Promise<Subscription[]>;
82
- changePlan(subscriptionId: string, newPlanId: string): Promise<Subscription>;
376
+ interface AdapterProbe {
377
+ /** 每次返回全新的适配器实例,以及它对供应商发起过的**写操作**记录 */
378
+ create(): {
379
+ adapter: PaymentProviderAdapter;
380
+ mutatingCalls: () => string[];
381
+ };
382
+ /** 该渠道支持的一个币种 */
383
+ supportedCurrency: string;
384
+ /** 该渠道不支持的一个币种;未声明限制时传 null */
385
+ unsupportedCurrency: string | null;
386
+ /** 一个签名无效的 webhook 请求 */
387
+ invalidWebhook: WebhookRequest;
388
+ /** 写操作端点名中代表"扣款"的片段,用于检测只读违规 */
389
+ captureCallNames?: readonly string[];
83
390
  }
391
+ declare function checkPaymentAdapterContract(probe: AdapterProbe): Promise<ContractCheck[]>;
84
392
 
85
393
  /**
86
- * 支付服务 — 管理多个 PaymentProvider 的注册和路由。
394
+ * 内存参考适配器。
87
395
  *
88
- * 使用方式:
89
- * paymentService.register(stripeProvider);
90
- * paymentService.register(wechatPayProvider);
91
- * await paymentService.createPaymentIntent('stripe', params);
396
+ * 两个用途:
397
+ * 1. 让接入方在不连真实网关的情况下测自己的编排逻辑。
398
+ * 2. 给 contract-suite 当基准——一个**正确**的适配器必须全过,
399
+ * 故意写坏的变体必须被逐条抓出来。没有这一步,契约套件只是一组永远
400
+ * 绿灯的断言,证明不了任何事。
92
401
  */
93
- declare class PaymentService {
94
- private readonly logger;
95
- private readonly providers;
96
- /** 注册支付提供商 */
97
- register(provider: PaymentProvider): void;
98
- /** 注销支付提供商 */
99
- unregister(providerId: string): boolean;
100
- /** 获取已注册的提供商列表 */
101
- listProviders(): PaymentProvider[];
102
- /** 获取指定提供商 */
103
- getProvider(providerId: string): PaymentProvider;
104
- /** 创建支付意图 */
105
- createPaymentIntent(providerId: string, params: CreatePaymentParams): Promise<PaymentIntent>;
106
- /** 确认支付 */
107
- confirmPayment(providerId: string, intentId: string): Promise<PaymentResult>;
108
- /** 退款 */
109
- refund(providerId: string, paymentId: string, amount?: number, reason?: string): Promise<RefundResult>;
110
- /** 查询支付状态 */
111
- getPaymentStatus(providerId: string, intentId: string): Promise<PaymentStatus>;
112
- /** 处理 Webhook */
113
- handleWebhook(providerId: string, payload: unknown, signature: string): Promise<WebhookEvent>;
402
+ interface FakeAdapterOptions {
403
+ id?: string;
404
+ supportedCurrencies?: readonly string[];
405
+ /** 故意违约,用于验证 contract-suite 的检出能力 */
406
+ defects?: {
407
+ /** retrieve 里偷偷扣款——最贵的一类 bug */
408
+ retrieveCaptures?: boolean;
409
+ /** 忽略幂等键,每次都开新支付 */
410
+ ignoreIdempotency?: boolean;
411
+ /** 验签失败也不抛错,返回一个"未知"事件蒙混过去 */
412
+ acceptsBadSignature?: boolean;
413
+ /** 把中间态猜成 failed */
414
+ guessesPendingAsFailed?: boolean;
415
+ /** 金额按浮点处理 */
416
+ mangleAmount?: boolean;
417
+ };
114
418
  }
419
+ declare function createFakeAdapter(options?: FakeAdapterOptions): {
420
+ adapter: PaymentProviderAdapter;
421
+ mutatingCalls: () => string[];
422
+ };
115
423
 
116
- type OrderStatus = 'created' | 'paying' | 'paid' | 'failed' | 'refunding' | 'refunded' | 'cancelled';
424
+ declare const ORDER_PERSISTENCE = "PAYMENT_ORDER_PERSISTENCE";
425
+ /**
426
+ * Durable order data access. Implementations preserve native money/date/JSON values.
427
+ * A transaction-bound model keeps every operation in the caller's transaction.
428
+ * compareAndSet must execute its predicate and update as one database statement.
429
+ */
430
+ interface OrderPersistence<T> extends Pick<ModelOperations<T>, 'create' | 'findUnique' | 'findMany' | 'update' | 'updateMany' | 'count'> {
431
+ compareAndSet(id: string, expected: Record<string, unknown>, data: Record<string, unknown>): Promise<boolean>;
432
+ }
433
+ /** Uses the existing database port; there is no implicit in-memory fallback. */
434
+ declare class DatabaseOrderPersistence<T> implements OrderPersistence<T> {
435
+ private readonly model;
436
+ constructor(model: Pick<ModelOperations<T>, 'create' | 'findUnique' | 'findMany' | 'update' | 'updateMany' | 'count'>);
437
+ create(args: Record<string, unknown>): Promise<T>;
438
+ findUnique(args: Record<string, unknown>): Promise<T | null>;
439
+ findMany(args?: Record<string, unknown>): Promise<T[]>;
440
+ update(args: Record<string, unknown>): Promise<T>;
441
+ updateMany(args: Record<string, unknown>): Promise<{
442
+ count: number;
443
+ }>;
444
+ count(args?: Record<string, unknown>): Promise<number>;
445
+ compareAndSet(id: string, expected: Record<string, unknown>, data: Record<string, unknown>): Promise<boolean>;
446
+ }
447
+
448
+ type OrderStatus = 'created' | 'paying' | 'confirming' | 'paid' | 'failed' | 'refunding' | 'refunded' | 'cancelled';
117
449
  interface Order {
118
450
  id: string;
119
451
  providerId: string;
120
452
  paymentIntentId?: string;
121
453
  paymentId?: string;
122
- amount: number;
454
+ /**
455
+ * 十进制字符串,与 Money.amount 同一表示。
456
+ * 原先是 number:一旦金额在 number 与字符串之间来回转换,"29.00" 会变成 29,
457
+ * 三位小数币种会被静默截断。整条链路只保留一种表示。
458
+ */
459
+ amount: string;
123
460
  currency: string;
124
461
  status: OrderStatus;
125
462
  metadata?: Record<string, string>;
@@ -131,30 +468,47 @@ interface Order {
131
468
  interface CreateOrderParams {
132
469
  orderId: string;
133
470
  providerId: string;
134
- payment: CreatePaymentParams;
471
+ payment: CreatePaymentInput;
135
472
  }
136
- /**
137
- * 订单管理服务 — 创建订单 -> 支付 -> 完成/退款。
138
- *
139
- * 提供内存订单存储(生产环境应替换为数据库持久化)。
140
- */
473
+ declare class OrderNotFoundError extends Error {
474
+ readonly orderId: string;
475
+ constructor(orderId: string);
476
+ }
477
+ /** Durable provider workflow. External calls never run inside a database transaction here. */
141
478
  declare class OrderService {
142
- private readonly paymentService;
143
- private readonly logger;
479
+ private readonly providers;
144
480
  private readonly orders;
145
- constructor(paymentService: PaymentService);
146
- /** 创建订单并发起支付 */
481
+ constructor(providers: PaymentProviderRegistry, orders: OrderPersistence<Order>);
147
482
  createOrder(params: CreateOrderParams): Promise<Order>;
148
- /** 确认订单支付 */
149
- confirmOrder(orderId: string): Promise<PaymentResult>;
150
- /** 订单退款 */
151
- refundOrder(orderId: string, amount?: number, reason?: string): Promise<RefundResult>;
152
- /** 取消订单 */
153
- cancelOrder(orderId: string): Order;
154
- /** 查询订单 */
155
- getOrder(orderId: string): Order;
156
- /** 列出所有订单 */
157
- listOrders(): Order[];
483
+ private initiatePayment;
484
+ /**
485
+ * 扣款。幂等键由调用方给出——同一笔订单重试必须复用同一个键,否则渠道会认为是新支付。
486
+ * 判定不了的结果保持 confirming,交由只读对账解决,绝不在这里重复扣款。
487
+ */
488
+ confirmOrder(orderId: string, idempotencyKey: string): Promise<PaymentSession>;
489
+ /**
490
+ * 把渠道观察到的结算状态落到订单上。
491
+ * 迁移是否允许由 reconcile() 统一裁决,保证所有渠道遵守同一套安全性质
492
+ * (unknown 不覆盖已知状态、已收款不被迟到的失败回调回退)。
493
+ */
494
+ private applySettlement;
495
+ refundOrder(orderId: string, idempotencyKey: string, options?: {
496
+ money?: CreatePaymentInput['money'];
497
+ reason?: string;
498
+ }): Promise<{
499
+ refundRef: string;
500
+ state: SettlementState;
501
+ }>;
502
+ /**
503
+ * 重试失败订单。
504
+ * idempotencyKey 必须与上一次不同:沿用旧键会让渠道原样返回那笔已失败的支付,
505
+ * 看起来"重试了"其实什么都没发生。
506
+ */
507
+ retryOrder(orderId: string, idempotencyKey: string): Promise<Order>;
508
+ cancelOrder(orderId: string): Promise<Order>;
509
+ getOrder(orderId: string): Promise<Order>;
510
+ listOrders(): Promise<Order[]>;
511
+ private transition;
158
512
  }
159
513
 
160
514
  /** 挽回邮件发送接口 — 由业务项目实现 */
@@ -166,11 +520,14 @@ interface RecoveryEmailParams {
166
520
  userName: string;
167
521
  orderId: string;
168
522
  orderNumber?: string;
169
- amount: number;
523
+ /** 十进制字符串,与 Order.amount / Money.amount 同一表示 */
524
+ amount: string;
170
525
  currency: string;
171
526
  errorReason: string;
172
527
  retryUrl: string;
173
528
  expireHours: number;
529
+ /** Stable key for this persisted send attempt. */
530
+ idempotencyKey: string;
174
531
  }
175
532
  /** 用户信息查询接口 — 由业务项目实现 */
176
533
  interface RecoveryUserLookup {
@@ -186,6 +543,17 @@ interface RecoveryUrlGenerator {
186
543
  declare const RECOVERY_MAIL_SENDER = "RECOVERY_MAIL_SENDER";
187
544
  declare const RECOVERY_USER_LOOKUP = "RECOVERY_USER_LOOKUP";
188
545
  declare const RECOVERY_URL_GENERATOR = "RECOVERY_URL_GENERATOR";
546
+ declare const RECOVERY_ATTEMPT_STORE = "RECOVERY_ATTEMPT_STORE";
547
+ /** Persisted attempt limits. claim must atomically reserve a unique number below maxAttempts. */
548
+ interface RecoveryAttemptStore {
549
+ claim(orderId: string, maxAttempts: number, at: Date): Promise<{
550
+ attemptNumber: number;
551
+ } | null>;
552
+ get(orderId: string): Promise<{
553
+ count: number;
554
+ lastAt: Date;
555
+ } | undefined>;
556
+ }
189
557
  interface RecoveryOptions {
190
558
  /** 最大挽回次数(默认 3) */
191
559
  maxAttempts?: number;
@@ -206,16 +574,14 @@ interface RecoveryOptions {
206
574
  */
207
575
  declare class PaymentRecoveryService {
208
576
  private readonly orderService;
209
- private readonly paymentService;
577
+ private readonly attempts;
210
578
  private readonly mailSender?;
211
579
  private readonly userLookup?;
212
580
  private readonly urlGenerator?;
213
581
  private readonly eventEmitter?;
214
582
  private readonly logger;
215
583
  private readonly options;
216
- /** 内存挽回计数(生产环境建议由业务项目持久化) */
217
- private readonly recoveryAttempts;
218
- constructor(orderService: OrderService, paymentService: PaymentService, mailSender?: RecoveryMailSender | undefined, userLookup?: RecoveryUserLookup | undefined, urlGenerator?: RecoveryUrlGenerator | undefined, eventEmitter?: EventEmitter2);
584
+ constructor(orderService: OrderService, attempts: RecoveryAttemptStore | null, mailSender?: RecoveryMailSender | undefined, userLookup?: RecoveryUserLookup | undefined, urlGenerator?: RecoveryUrlGenerator | undefined, eventEmitter?: EventEmitter2);
219
585
  /** 设置挽回选项 */
220
586
  configure(options: RecoveryOptions): void;
221
587
  /**
@@ -223,14 +589,14 @@ declare class PaymentRecoveryService {
223
589
  */
224
590
  handleFailedPayment(orderId: string, errorCode?: string, errorMessage?: string): Promise<void>;
225
591
  /**
226
- * 重试失败订单 — 将 failed → created 并重新发起支付
592
+ * 重试失败订单 — 将 failed → created 并重新发起支付。
593
+ * 幂等键由调用方给出且必须与上次不同,否则渠道会原样返回那笔已失败的支付。
227
594
  */
228
- retryOrder(orderId: string): Promise<Order>;
229
- /** 获取订单挽回记录 */
230
- getRecoveryAttempts(orderId: string): {
595
+ retryOrder(orderId: string, idempotencyKey: string): Promise<Order>;
596
+ getRecoveryAttempts(orderId: string): Promise<{
231
597
  count: number;
232
598
  lastAt: Date;
233
- } | undefined;
599
+ } | undefined>;
234
600
  private generateRetryUrl;
235
601
  }
236
602
  /** Stripe 错误码 → 用户友好中文描述 */
@@ -268,7 +634,10 @@ interface PaymentRecoverySentEvent {
268
634
 
269
635
  declare const PAYMENT_PROVIDERS = "PAYMENT_PROVIDERS";
270
636
  interface PaymentModuleOptions {
271
- providers?: PaymentProvider[];
637
+ providers?: PaymentProviderAdapter[];
638
+ orderPersistence: OrderPersistence<Order>;
639
+ /** Required when using payment recovery mail; no process-memory default. */
640
+ recoveryAttempts?: RecoveryAttemptStore;
272
641
  }
273
642
  interface PaymentModuleAsyncOptions {
274
643
  imports?: Array<Type | DynamicModule>;
@@ -276,8 +645,42 @@ interface PaymentModuleAsyncOptions {
276
645
  inject?: InjectionToken[];
277
646
  }
278
647
  declare class ZuckerPaymentModule {
279
- static forRoot(options?: PaymentModuleOptions): DynamicModule;
648
+ static forRoot(options: PaymentModuleOptions): DynamicModule;
280
649
  static forRootAsync(options: PaymentModuleAsyncOptions): DynamicModule;
281
650
  }
282
651
 
283
- export { type CreateOrderParams, type CreatePaymentParams, type Order, type OrderFailedEvent, OrderService, type OrderStatus, PAYMENT_PROVIDERS, type PaymentEventType, PaymentEvents, type PaymentIntent, type PaymentModuleAsyncOptions, type PaymentModuleOptions, type PaymentProvider, type PaymentRecoverySentEvent, PaymentRecoveryService, type PaymentResult, PaymentService, type PaymentStatus, RECOVERY_MAIL_SENDER, RECOVERY_URL_GENERATOR, RECOVERY_USER_LOOKUP, type RecoveryEmailParams, type RecoveryMailSender, type RecoveryOptions, type RecoveryUrlGenerator, type RecoveryUserLookup, type RefundResult, type Subscription, type SubscriptionPlan, type SubscriptionProvider, type WebhookEvent, ZuckerPaymentModule, friendlyErrorReason };
652
+ /** Construct the installed Stripe peer with the consumer's explicit API/transport configuration. */
653
+ declare function createStripeClient(secretKey: string, options: Stripe.StripeConfig): Stripe;
654
+ /** Verify raw webhook bytes using the installed SDK's timestamp/signature checks. */
655
+ declare function verifyStripeWebhookEvent(client: Pick<Stripe, 'webhooks'>, payload: Buffer | string, signature: string, webhookSecret: string): Stripe.Event;
656
+ /** Stripe expandable references can be an ID, an expanded object, or absent. */
657
+ declare function stripeResourceId(value: string | {
658
+ id: string;
659
+ } | null | undefined): string | undefined;
660
+ interface StripeCheckoutObservation {
661
+ raw: Stripe.Checkout.Session;
662
+ id: string;
663
+ url?: string;
664
+ clientReferenceId?: string;
665
+ paymentIntentId?: string;
666
+ subscriptionId?: string;
667
+ settlement: PaymentSettlementState;
668
+ }
669
+ declare function observeStripeCheckoutSession(raw: Stripe.Checkout.Session): StripeCheckoutObservation;
670
+ declare function createStripeCheckoutSession(client: Pick<Stripe, 'checkout'>, params: Stripe.Checkout.SessionCreateParams,
671
+ /** 传入 idempotencyKey 后,重试同一请求不会产生第二个 Session */
672
+ options?: Stripe.RequestOptions): Promise<StripeCheckoutObservation>;
673
+ declare function retrieveStripeCheckoutSession(client: Pick<Stripe, 'checkout'>, id: string): Promise<StripeCheckoutObservation>;
674
+ interface StripePaymentIntentObservation {
675
+ raw: Stripe.PaymentIntent;
676
+ id: string;
677
+ chargeId: string | null;
678
+ paymentMethodType?: string;
679
+ failureCode?: string;
680
+ failureMessage?: string;
681
+ settlement: PaymentSettlementState;
682
+ }
683
+ declare function observeStripePaymentIntent(raw: Stripe.PaymentIntent): StripePaymentIntentObservation;
684
+ declare function retrieveStripePaymentIntent(client: Pick<Stripe, 'paymentIntents'>, id: string, params?: Stripe.PaymentIntentRetrieveParams): Promise<StripePaymentIntentObservation>;
685
+
686
+ export { type AdapterProbe, type ContractCheck, type CreateOrderParams, type CreatePaymentInput, DatabaseOrderPersistence, type FakeAdapterOptions, type Money, ORDER_PERSISTENCE, type Order, type OrderFailedEvent, OrderNotFoundError, type OrderPersistence, OrderService, type OrderStatus, PAYMENT_PROVIDERS, PAYPAL_SUPPORTED_CURRENCIES, type PayPalAdapterOptions, type PayPalCaptureObservation, type PayPalCaptureRefundRequest, type PayPalCaptureRefundResponse, type PayPalMoneyReturnedObservation, type PayPalOrderObservation, type PayPalOrderRepresentation, PayPalPaymentTransport, type PayPalPaymentTransportOptions, type PayPalWebhookObservation, type PaymentEventType, PaymentEvents, type PaymentModuleAsyncOptions, type PaymentModuleOptions, type PaymentNextAction, type PaymentProviderAdapter, PaymentProviderRegistry, type PaymentRecoverySentEvent, PaymentRecoveryService, type PaymentSession, type PaymentSettlementState, RECOVERY_ATTEMPT_STORE, RECOVERY_MAIL_SENDER, RECOVERY_URL_GENERATOR, RECOVERY_USER_LOOKUP, type RecoveryAttemptStore, type RecoveryEmailParams, type RecoveryMailSender, type RecoveryOptions, type RecoveryUrlGenerator, type RecoveryUserLookup, type RefundInput, SETTLEMENT_STATES, type SettlementDecision, type SettlementState, type StripeAdapterOptions, type StripeCheckoutObservation, type StripePaymentIntentObservation, type WebhookObservation, type WebhookRequest, ZuckerPaymentModule, adapterSupportsCurrency, checkPaymentAdapterContract, createFakeAdapter, createPayPalAdapter, createStripeAdapter, createStripeCheckoutSession, createStripeClient, currencyExponent, decodePayPalMoneyReturnedEvent, decodePayPalWebhookEvent, friendlyErrorReason, fromMinorUnits, isMoneyMoved, isTerminal, observePayPalOrder, observePayPalState, observeStripeCheckoutSession, observeStripePaymentIntent, observeStripeState, reconcile, retrieveStripeCheckoutSession, retrieveStripePaymentIntent, stripeResourceId, toMinorUnits, verifyStripeWebhookEvent };