@zucker-framework/payment 1.0.0 → 1.0.2

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 ADDED
@@ -0,0 +1,35 @@
1
+ # Payment persistence and provider access
2
+
3
+ `PaymentService` routes external provider calls. `OrderPersistence<T>` owns asynchronous order storage, and `DatabaseOrderPersistence<T>` binds that contract to an existing `ModelOperations<T>` database model. It preserves native Decimal, Date and JSON values, accepts existing include/select/filter arguments and performs `compareAndSet(id, expected, data)` through one conditional `updateMany` statement. The database must enforce a unique order ID. A transaction-bound model stays on that transaction; the persistence class never starts a transaction or switches to another connection.
4
+
5
+ Applications with existing order schemas use this public persistence contract directly. They retain their status vocabulary, pricing, product line items, fulfillment and event projections. They do not need a second order table or to translate amounts into another storage format. Confirmed external payment status must be validated before invoking a paid transition.
6
+
7
+ ## Configured Framework order workflow
8
+
9
+ `OrderService` now requires `OrderPersistence<Order>` as its second constructor argument. `ZuckerPaymentModule.forRoot` / `forRootAsync` require `orderPersistence`; there is no implicit Map store. Configure the module once. `getOrder`, `listOrders`, `cancelOrder` and recovery reads are asynchronous.
10
+
11
+ Provider operations execute outside database transactions. The service persists creation before calling a provider and uses conditional status updates to claim confirmation/refund work. Concurrent callers cannot both claim the same transition. Unknown create/confirm/refund outcomes remain `created` / `confirming` / `refunding` for reconciliation; errors do not authorize automatic repeated payment attempts. A confirmation response of `pending` or `processing` also stays `confirming`, while `refunded` stays terminal. A failed write of a returned provider ID is surfaced instead of returning a successful unsaved order.
12
+
13
+ The supplied store must persist the `Order` contract's fields. In-memory implementations may be explicitly supplied in isolated tests but do not prove durability or database concurrency. The generic workflow is not an application fulfillment engine and does not guarantee exactly-once external effects; applications retain provider idempotency, reconciliation and any required durable delivery/outbox.
14
+
15
+ ## Recovery attempts
16
+
17
+ Recovery mail requires `RecoveryAttemptStore`, configured as `recoveryAttempts` or through `RECOVERY_ATTEMPT_STORE`. Its `claim` must atomically persist a unique attempt number under the configured limit before returning. `get` reads persisted attempts. There is no built-in counter, implicit memory fallback or database table assumption; callers supply storage matching their existing schema. A missing sender skips without reserving; a missing attempt store fails explicitly; database failures propagate.
18
+
19
+ Each reserved mail attempt receives `payment-recovery:<orderId>:<attemptNumber>` as its idempotency key. Failed or unknown sends keep their reservation. The sender must forward the key to its provider; this is an attempt limit, not a promise of exactly-once mail delivery. Recipient selection, cooldown windows, templates and business recovery policy remain in the consumer.
20
+
21
+ ## Migration and verification
22
+
23
+ This is a breaking source change: configure an order store, await the formerly synchronous methods, provide recovery-attempt persistence when enabling recovery mail, and replace direct mutation of a returned order with persisted operations. `retryOrder` now delegates to the durable order service. `OrderNotFoundError` distinguishes absence from database failure.
24
+
25
+ Validation on 2026-09-11: all 7 payment unit-test files (38 tests) and the package no-emit typecheck passed, covering durable-order ports, in-flight outcomes, recovery attempts and mocked provider adapters. The consumer PostgreSQL contract and real provider paths are not established by this unit run. Consumers require a new immutable version and application/CI verification before release; existing published versions remain unchanged.
26
+
27
+ ## Hosted provider observations
28
+
29
+ Stripe access exposes `createStripeCheckoutSession`, `retrieveStripeCheckoutSession` and `retrieveStripePaymentIntent`. Each delegates exactly once with the supplied SDK request parameters and returns both the untouched `raw` response and normalized evidence. The pure `observeStripeCheckoutSession` / `observeStripePaymentIntent` functions also handle verified webhook objects. Expandable payment-intent/subscription/charge IDs are normalized; payment-method type is returned only when the charge is already expanded. Expanding a charge remains an explicit caller request.
30
+
31
+ `PayPalPaymentTransport.createOrder`, `captureOrder` and `getOrder` preserve the installed SDK request shape and return `raw`, ID, approval URL, first capture ID and settlement evidence. `observePayPalOrder` works on stored responses without a request. `getOrder` never invokes capture; `APPROVED`, `CREATED` and other nonterminal states are `unknown`, `COMPLETED` is `paid`, and `VOIDED` is `unpaid`. Stripe likewise distinguishes paid evidence from expired/canceled objects and leaves pending evidence unknown.
32
+
33
+ `decodePayPalWebhookEvent` decodes standard capture event/resource fields after the caller has verified the signature. Recognized capture events require a nonempty resource ID, preventing an undefined identity from reaching a consumer's database filter. Unknown event types remain unhandled observations. Decoding does not verify signatures or apply product effects.
34
+
35
+ These APIs do not interpret product metadata, choose prices/currencies or success URLs, change retries/SDK versions, activate refunds, or write orders/subscriptions. Consumers decide whether an explicit capture result is a business failure and own all reconciliation/fulfillment. The existing optional refund API is unchanged; this migration covers active hosted-payment paths only.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,9 @@
1
+ import { ModelOperations } from '@zucker-framework/core';
1
2
  import { EventEmitter2 } from '@nestjs/event-emitter';
2
3
  import { Type, DynamicModule, InjectionToken } from '@nestjs/common';
4
+ import Stripe from 'stripe';
5
+ import * as _paypal_paypal_server_sdk from '@paypal/paypal-server-sdk';
6
+ import { OrdersController } from '@paypal/paypal-server-sdk';
3
7
 
4
8
  /**
5
9
  * 支付网关接口 — 不绑定任何具体支付商。
@@ -113,7 +117,31 @@ declare class PaymentService {
113
117
  handleWebhook(providerId: string, payload: unknown, signature: string): Promise<WebhookEvent>;
114
118
  }
115
119
 
116
- type OrderStatus = 'created' | 'paying' | 'paid' | 'failed' | 'refunding' | 'refunded' | 'cancelled';
120
+ declare const ORDER_PERSISTENCE = "PAYMENT_ORDER_PERSISTENCE";
121
+ /**
122
+ * Durable order data access. Implementations preserve native money/date/JSON values.
123
+ * A transaction-bound model keeps every operation in the caller's transaction.
124
+ * compareAndSet must execute its predicate and update as one database statement.
125
+ */
126
+ interface OrderPersistence<T> extends Pick<ModelOperations<T>, 'create' | 'findUnique' | 'findMany' | 'update' | 'updateMany' | 'count'> {
127
+ compareAndSet(id: string, expected: Record<string, unknown>, data: Record<string, unknown>): Promise<boolean>;
128
+ }
129
+ /** Uses the existing database port; there is no implicit in-memory fallback. */
130
+ declare class DatabaseOrderPersistence<T> implements OrderPersistence<T> {
131
+ private readonly model;
132
+ constructor(model: Pick<ModelOperations<T>, 'create' | 'findUnique' | 'findMany' | 'update' | 'updateMany' | 'count'>);
133
+ create(args: Record<string, unknown>): Promise<T>;
134
+ findUnique(args: Record<string, unknown>): Promise<T | null>;
135
+ findMany(args?: Record<string, unknown>): Promise<T[]>;
136
+ update(args: Record<string, unknown>): Promise<T>;
137
+ updateMany(args: Record<string, unknown>): Promise<{
138
+ count: number;
139
+ }>;
140
+ count(args?: Record<string, unknown>): Promise<number>;
141
+ compareAndSet(id: string, expected: Record<string, unknown>, data: Record<string, unknown>): Promise<boolean>;
142
+ }
143
+
144
+ type OrderStatus = 'created' | 'paying' | 'confirming' | 'paid' | 'failed' | 'refunding' | 'refunded' | 'cancelled';
117
145
  interface Order {
118
146
  id: string;
119
147
  providerId: string;
@@ -133,28 +161,24 @@ interface CreateOrderParams {
133
161
  providerId: string;
134
162
  payment: CreatePaymentParams;
135
163
  }
136
- /**
137
- * 订单管理服务 — 创建订单 -> 支付 -> 完成/退款。
138
- *
139
- * 提供内存订单存储(生产环境应替换为数据库持久化)。
140
- */
164
+ declare class OrderNotFoundError extends Error {
165
+ readonly orderId: string;
166
+ constructor(orderId: string);
167
+ }
168
+ /** Durable provider workflow. External calls never run inside a database transaction here. */
141
169
  declare class OrderService {
142
170
  private readonly paymentService;
143
- private readonly logger;
144
171
  private readonly orders;
145
- constructor(paymentService: PaymentService);
146
- /** 创建订单并发起支付 */
172
+ constructor(paymentService: PaymentService, orders: OrderPersistence<Order>);
147
173
  createOrder(params: CreateOrderParams): Promise<Order>;
148
- /** 确认订单支付 */
174
+ private initiatePayment;
149
175
  confirmOrder(orderId: string): Promise<PaymentResult>;
150
- /** 订单退款 */
151
176
  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[];
177
+ retryOrder(orderId: string): Promise<Order>;
178
+ cancelOrder(orderId: string): Promise<Order>;
179
+ getOrder(orderId: string): Promise<Order>;
180
+ listOrders(): Promise<Order[]>;
181
+ private transition;
158
182
  }
159
183
 
160
184
  /** 挽回邮件发送接口 — 由业务项目实现 */
@@ -171,6 +195,8 @@ interface RecoveryEmailParams {
171
195
  errorReason: string;
172
196
  retryUrl: string;
173
197
  expireHours: number;
198
+ /** Stable key for this persisted send attempt. */
199
+ idempotencyKey: string;
174
200
  }
175
201
  /** 用户信息查询接口 — 由业务项目实现 */
176
202
  interface RecoveryUserLookup {
@@ -186,6 +212,17 @@ interface RecoveryUrlGenerator {
186
212
  declare const RECOVERY_MAIL_SENDER = "RECOVERY_MAIL_SENDER";
187
213
  declare const RECOVERY_USER_LOOKUP = "RECOVERY_USER_LOOKUP";
188
214
  declare const RECOVERY_URL_GENERATOR = "RECOVERY_URL_GENERATOR";
215
+ declare const RECOVERY_ATTEMPT_STORE = "RECOVERY_ATTEMPT_STORE";
216
+ /** Persisted attempt limits. claim must atomically reserve a unique number below maxAttempts. */
217
+ interface RecoveryAttemptStore {
218
+ claim(orderId: string, maxAttempts: number, at: Date): Promise<{
219
+ attemptNumber: number;
220
+ } | null>;
221
+ get(orderId: string): Promise<{
222
+ count: number;
223
+ lastAt: Date;
224
+ } | undefined>;
225
+ }
189
226
  interface RecoveryOptions {
190
227
  /** 最大挽回次数(默认 3) */
191
228
  maxAttempts?: number;
@@ -206,16 +243,14 @@ interface RecoveryOptions {
206
243
  */
207
244
  declare class PaymentRecoveryService {
208
245
  private readonly orderService;
209
- private readonly paymentService;
246
+ private readonly attempts;
210
247
  private readonly mailSender?;
211
248
  private readonly userLookup?;
212
249
  private readonly urlGenerator?;
213
250
  private readonly eventEmitter?;
214
251
  private readonly logger;
215
252
  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);
253
+ constructor(orderService: OrderService, attempts: RecoveryAttemptStore | null, mailSender?: RecoveryMailSender | undefined, userLookup?: RecoveryUserLookup | undefined, urlGenerator?: RecoveryUrlGenerator | undefined, eventEmitter?: EventEmitter2);
219
254
  /** 设置挽回选项 */
220
255
  configure(options: RecoveryOptions): void;
221
256
  /**
@@ -226,11 +261,10 @@ declare class PaymentRecoveryService {
226
261
  * 重试失败订单 — 将 failed → created 并重新发起支付
227
262
  */
228
263
  retryOrder(orderId: string): Promise<Order>;
229
- /** 获取订单挽回记录 */
230
- getRecoveryAttempts(orderId: string): {
264
+ getRecoveryAttempts(orderId: string): Promise<{
231
265
  count: number;
232
266
  lastAt: Date;
233
- } | undefined;
267
+ } | undefined>;
234
268
  private generateRetryUrl;
235
269
  }
236
270
  /** Stripe 错误码 → 用户友好中文描述 */
@@ -269,6 +303,9 @@ interface PaymentRecoverySentEvent {
269
303
  declare const PAYMENT_PROVIDERS = "PAYMENT_PROVIDERS";
270
304
  interface PaymentModuleOptions {
271
305
  providers?: PaymentProvider[];
306
+ orderPersistence: OrderPersistence<Order>;
307
+ /** Required when using payment recovery mail; no process-memory default. */
308
+ recoveryAttempts?: RecoveryAttemptStore;
272
309
  }
273
310
  interface PaymentModuleAsyncOptions {
274
311
  imports?: Array<Type | DynamicModule>;
@@ -276,8 +313,122 @@ interface PaymentModuleAsyncOptions {
276
313
  inject?: InjectionToken[];
277
314
  }
278
315
  declare class ZuckerPaymentModule {
279
- static forRoot(options?: PaymentModuleOptions): DynamicModule;
316
+ static forRoot(options: PaymentModuleOptions): DynamicModule;
280
317
  static forRootAsync(options: PaymentModuleAsyncOptions): DynamicModule;
281
318
  }
282
319
 
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 };
320
+ /** Read-only provider evidence; unknown must never be treated as an instruction to charge. */
321
+ type PaymentSettlementState = 'paid' | 'unpaid' | 'unknown';
322
+
323
+ /** Construct the installed Stripe peer with the consumer's explicit API/transport configuration. */
324
+ declare function createStripeClient(secretKey: string, options: Stripe.StripeConfig): Stripe;
325
+ /** Verify raw webhook bytes using the installed SDK's timestamp/signature checks. */
326
+ declare function verifyStripeWebhookEvent(client: Pick<Stripe, 'webhooks'>, payload: Buffer | string, signature: string, webhookSecret: string): Stripe.Event;
327
+ /** Stripe expandable references can be an ID, an expanded object, or absent. */
328
+ declare function stripeResourceId(value: string | {
329
+ id: string;
330
+ } | null | undefined): string | undefined;
331
+ interface StripeCheckoutObservation {
332
+ raw: Stripe.Checkout.Session;
333
+ id: string;
334
+ url?: string;
335
+ clientReferenceId?: string;
336
+ paymentIntentId?: string;
337
+ subscriptionId?: string;
338
+ settlement: PaymentSettlementState;
339
+ }
340
+ declare function observeStripeCheckoutSession(raw: Stripe.Checkout.Session): StripeCheckoutObservation;
341
+ declare function createStripeCheckoutSession(client: Pick<Stripe, 'checkout'>, params: Stripe.Checkout.SessionCreateParams): Promise<StripeCheckoutObservation>;
342
+ declare function retrieveStripeCheckoutSession(client: Pick<Stripe, 'checkout'>, id: string): Promise<StripeCheckoutObservation>;
343
+ interface StripePaymentIntentObservation {
344
+ raw: Stripe.PaymentIntent;
345
+ id: string;
346
+ chargeId: string | null;
347
+ paymentMethodType?: string;
348
+ failureCode?: string;
349
+ failureMessage?: string;
350
+ settlement: PaymentSettlementState;
351
+ }
352
+ declare function observeStripePaymentIntent(raw: Stripe.PaymentIntent): StripePaymentIntentObservation;
353
+ declare function retrieveStripePaymentIntent(client: Pick<Stripe, 'paymentIntents'>, id: string, params?: Stripe.PaymentIntentRetrieveParams): Promise<StripePaymentIntentObservation>;
354
+
355
+ interface PayPalPaymentTransportOptions {
356
+ clientId: string;
357
+ clientSecret: string;
358
+ mode: 'live' | 'sandbox';
359
+ fetcher?: typeof fetch;
360
+ logger?: {
361
+ warn: (message: string) => void;
362
+ error: (message: string) => void;
363
+ };
364
+ }
365
+ interface PayPalCaptureRefundRequest {
366
+ amount: {
367
+ value: string;
368
+ currency_code: string;
369
+ };
370
+ note_to_payer?: string;
371
+ }
372
+ interface PayPalCaptureRefundResponse {
373
+ id?: string;
374
+ status?: string;
375
+ message?: string;
376
+ }
377
+ interface PayPalOrderRepresentation {
378
+ id?: string;
379
+ status?: string;
380
+ links?: Array<{
381
+ rel?: string;
382
+ href?: string;
383
+ }>;
384
+ purchaseUnits?: Array<{
385
+ payments?: {
386
+ captures?: Array<{
387
+ id?: string;
388
+ }>;
389
+ };
390
+ }>;
391
+ }
392
+ interface PayPalOrderObservation<T extends PayPalOrderRepresentation = PayPalOrderRepresentation> {
393
+ raw: T;
394
+ id?: string;
395
+ approveUrl?: string;
396
+ captureId?: string;
397
+ settlement: PaymentSettlementState;
398
+ }
399
+ /** APPROVED means approval only, never evidence of a captured payment. */
400
+ declare function observePayPalOrder<T extends PayPalOrderRepresentation>(raw: T): PayPalOrderObservation<T>;
401
+ interface PayPalCaptureObservation {
402
+ raw: Record<string, unknown>;
403
+ id: string;
404
+ failureReason?: string;
405
+ }
406
+ type PayPalWebhookObservation = {
407
+ raw: Record<string, unknown>;
408
+ type: string;
409
+ kind: 'capture-succeeded' | 'capture-failed';
410
+ capture: PayPalCaptureObservation;
411
+ } | {
412
+ raw: Record<string, unknown>;
413
+ type: string;
414
+ kind: 'unknown';
415
+ };
416
+ /** Decode only after signature verification; this performs no authentication or network call. */
417
+ declare function decodePayPalWebhookEvent(payload: string): PayPalWebhookObservation;
418
+ /** SDK order transport plus the OAuth-backed REST endpoints absent from the current SDK integration. */
419
+ declare class PayPalPaymentTransport {
420
+ private readonly options;
421
+ readonly orders: OrdersController;
422
+ private readonly baseUrl;
423
+ private readonly fetcher;
424
+ constructor(options: PayPalPaymentTransportOptions);
425
+ createOrder(input: Parameters<OrdersController['createOrder']>[0]): Promise<PayPalOrderObservation<_paypal_paypal_server_sdk.Order>>;
426
+ /** An explicit capture command; never invoked by getOrder or status observation. */
427
+ captureOrder(input: Parameters<OrdersController['captureOrder']>[0]): Promise<PayPalOrderObservation<_paypal_paypal_server_sdk.Order>>;
428
+ getOrder(input: Parameters<OrdersController['getOrder']>[0]): Promise<PayPalOrderObservation<_paypal_paypal_server_sdk.Order>>;
429
+ private getAccessToken;
430
+ verifyWebhookSignature(payload: string, headers: Record<string, string>, webhookId: string): Promise<boolean>;
431
+ refundCapture(captureId: string, request: PayPalCaptureRefundRequest): Promise<PayPalCaptureRefundResponse>;
432
+ }
433
+
434
+ export { type CreateOrderParams, type CreatePaymentParams, DatabaseOrderPersistence, ORDER_PERSISTENCE, type Order, type OrderFailedEvent, OrderNotFoundError, type OrderPersistence, OrderService, type OrderStatus, PAYMENT_PROVIDERS, type PayPalCaptureObservation, type PayPalCaptureRefundRequest, type PayPalCaptureRefundResponse, type PayPalOrderObservation, type PayPalOrderRepresentation, PayPalPaymentTransport, type PayPalPaymentTransportOptions, type PayPalWebhookObservation, type PaymentEventType, PaymentEvents, type PaymentIntent, type PaymentModuleAsyncOptions, type PaymentModuleOptions, type PaymentProvider, type PaymentRecoverySentEvent, PaymentRecoveryService, type PaymentResult, PaymentService, type PaymentSettlementState, type PaymentStatus, 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 RefundResult, type StripeCheckoutObservation, type StripePaymentIntentObservation, type Subscription, type SubscriptionPlan, type SubscriptionProvider, type WebhookEvent, ZuckerPaymentModule, createStripeCheckoutSession, createStripeClient, decodePayPalWebhookEvent, friendlyErrorReason, observePayPalOrder, observeStripeCheckoutSession, observeStripePaymentIntent, retrieveStripeCheckoutSession, retrieveStripePaymentIntent, stripeResourceId, verifyStripeWebhookEvent };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
+ import { ModelOperations } from '@zucker-framework/core';
1
2
  import { EventEmitter2 } from '@nestjs/event-emitter';
2
3
  import { Type, DynamicModule, InjectionToken } from '@nestjs/common';
4
+ import Stripe from 'stripe';
5
+ import * as _paypal_paypal_server_sdk from '@paypal/paypal-server-sdk';
6
+ import { OrdersController } from '@paypal/paypal-server-sdk';
3
7
 
4
8
  /**
5
9
  * 支付网关接口 — 不绑定任何具体支付商。
@@ -113,7 +117,31 @@ declare class PaymentService {
113
117
  handleWebhook(providerId: string, payload: unknown, signature: string): Promise<WebhookEvent>;
114
118
  }
115
119
 
116
- type OrderStatus = 'created' | 'paying' | 'paid' | 'failed' | 'refunding' | 'refunded' | 'cancelled';
120
+ declare const ORDER_PERSISTENCE = "PAYMENT_ORDER_PERSISTENCE";
121
+ /**
122
+ * Durable order data access. Implementations preserve native money/date/JSON values.
123
+ * A transaction-bound model keeps every operation in the caller's transaction.
124
+ * compareAndSet must execute its predicate and update as one database statement.
125
+ */
126
+ interface OrderPersistence<T> extends Pick<ModelOperations<T>, 'create' | 'findUnique' | 'findMany' | 'update' | 'updateMany' | 'count'> {
127
+ compareAndSet(id: string, expected: Record<string, unknown>, data: Record<string, unknown>): Promise<boolean>;
128
+ }
129
+ /** Uses the existing database port; there is no implicit in-memory fallback. */
130
+ declare class DatabaseOrderPersistence<T> implements OrderPersistence<T> {
131
+ private readonly model;
132
+ constructor(model: Pick<ModelOperations<T>, 'create' | 'findUnique' | 'findMany' | 'update' | 'updateMany' | 'count'>);
133
+ create(args: Record<string, unknown>): Promise<T>;
134
+ findUnique(args: Record<string, unknown>): Promise<T | null>;
135
+ findMany(args?: Record<string, unknown>): Promise<T[]>;
136
+ update(args: Record<string, unknown>): Promise<T>;
137
+ updateMany(args: Record<string, unknown>): Promise<{
138
+ count: number;
139
+ }>;
140
+ count(args?: Record<string, unknown>): Promise<number>;
141
+ compareAndSet(id: string, expected: Record<string, unknown>, data: Record<string, unknown>): Promise<boolean>;
142
+ }
143
+
144
+ type OrderStatus = 'created' | 'paying' | 'confirming' | 'paid' | 'failed' | 'refunding' | 'refunded' | 'cancelled';
117
145
  interface Order {
118
146
  id: string;
119
147
  providerId: string;
@@ -133,28 +161,24 @@ interface CreateOrderParams {
133
161
  providerId: string;
134
162
  payment: CreatePaymentParams;
135
163
  }
136
- /**
137
- * 订单管理服务 — 创建订单 -> 支付 -> 完成/退款。
138
- *
139
- * 提供内存订单存储(生产环境应替换为数据库持久化)。
140
- */
164
+ declare class OrderNotFoundError extends Error {
165
+ readonly orderId: string;
166
+ constructor(orderId: string);
167
+ }
168
+ /** Durable provider workflow. External calls never run inside a database transaction here. */
141
169
  declare class OrderService {
142
170
  private readonly paymentService;
143
- private readonly logger;
144
171
  private readonly orders;
145
- constructor(paymentService: PaymentService);
146
- /** 创建订单并发起支付 */
172
+ constructor(paymentService: PaymentService, orders: OrderPersistence<Order>);
147
173
  createOrder(params: CreateOrderParams): Promise<Order>;
148
- /** 确认订单支付 */
174
+ private initiatePayment;
149
175
  confirmOrder(orderId: string): Promise<PaymentResult>;
150
- /** 订单退款 */
151
176
  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[];
177
+ retryOrder(orderId: string): Promise<Order>;
178
+ cancelOrder(orderId: string): Promise<Order>;
179
+ getOrder(orderId: string): Promise<Order>;
180
+ listOrders(): Promise<Order[]>;
181
+ private transition;
158
182
  }
159
183
 
160
184
  /** 挽回邮件发送接口 — 由业务项目实现 */
@@ -171,6 +195,8 @@ interface RecoveryEmailParams {
171
195
  errorReason: string;
172
196
  retryUrl: string;
173
197
  expireHours: number;
198
+ /** Stable key for this persisted send attempt. */
199
+ idempotencyKey: string;
174
200
  }
175
201
  /** 用户信息查询接口 — 由业务项目实现 */
176
202
  interface RecoveryUserLookup {
@@ -186,6 +212,17 @@ interface RecoveryUrlGenerator {
186
212
  declare const RECOVERY_MAIL_SENDER = "RECOVERY_MAIL_SENDER";
187
213
  declare const RECOVERY_USER_LOOKUP = "RECOVERY_USER_LOOKUP";
188
214
  declare const RECOVERY_URL_GENERATOR = "RECOVERY_URL_GENERATOR";
215
+ declare const RECOVERY_ATTEMPT_STORE = "RECOVERY_ATTEMPT_STORE";
216
+ /** Persisted attempt limits. claim must atomically reserve a unique number below maxAttempts. */
217
+ interface RecoveryAttemptStore {
218
+ claim(orderId: string, maxAttempts: number, at: Date): Promise<{
219
+ attemptNumber: number;
220
+ } | null>;
221
+ get(orderId: string): Promise<{
222
+ count: number;
223
+ lastAt: Date;
224
+ } | undefined>;
225
+ }
189
226
  interface RecoveryOptions {
190
227
  /** 最大挽回次数(默认 3) */
191
228
  maxAttempts?: number;
@@ -206,16 +243,14 @@ interface RecoveryOptions {
206
243
  */
207
244
  declare class PaymentRecoveryService {
208
245
  private readonly orderService;
209
- private readonly paymentService;
246
+ private readonly attempts;
210
247
  private readonly mailSender?;
211
248
  private readonly userLookup?;
212
249
  private readonly urlGenerator?;
213
250
  private readonly eventEmitter?;
214
251
  private readonly logger;
215
252
  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);
253
+ constructor(orderService: OrderService, attempts: RecoveryAttemptStore | null, mailSender?: RecoveryMailSender | undefined, userLookup?: RecoveryUserLookup | undefined, urlGenerator?: RecoveryUrlGenerator | undefined, eventEmitter?: EventEmitter2);
219
254
  /** 设置挽回选项 */
220
255
  configure(options: RecoveryOptions): void;
221
256
  /**
@@ -226,11 +261,10 @@ declare class PaymentRecoveryService {
226
261
  * 重试失败订单 — 将 failed → created 并重新发起支付
227
262
  */
228
263
  retryOrder(orderId: string): Promise<Order>;
229
- /** 获取订单挽回记录 */
230
- getRecoveryAttempts(orderId: string): {
264
+ getRecoveryAttempts(orderId: string): Promise<{
231
265
  count: number;
232
266
  lastAt: Date;
233
- } | undefined;
267
+ } | undefined>;
234
268
  private generateRetryUrl;
235
269
  }
236
270
  /** Stripe 错误码 → 用户友好中文描述 */
@@ -269,6 +303,9 @@ interface PaymentRecoverySentEvent {
269
303
  declare const PAYMENT_PROVIDERS = "PAYMENT_PROVIDERS";
270
304
  interface PaymentModuleOptions {
271
305
  providers?: PaymentProvider[];
306
+ orderPersistence: OrderPersistence<Order>;
307
+ /** Required when using payment recovery mail; no process-memory default. */
308
+ recoveryAttempts?: RecoveryAttemptStore;
272
309
  }
273
310
  interface PaymentModuleAsyncOptions {
274
311
  imports?: Array<Type | DynamicModule>;
@@ -276,8 +313,122 @@ interface PaymentModuleAsyncOptions {
276
313
  inject?: InjectionToken[];
277
314
  }
278
315
  declare class ZuckerPaymentModule {
279
- static forRoot(options?: PaymentModuleOptions): DynamicModule;
316
+ static forRoot(options: PaymentModuleOptions): DynamicModule;
280
317
  static forRootAsync(options: PaymentModuleAsyncOptions): DynamicModule;
281
318
  }
282
319
 
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 };
320
+ /** Read-only provider evidence; unknown must never be treated as an instruction to charge. */
321
+ type PaymentSettlementState = 'paid' | 'unpaid' | 'unknown';
322
+
323
+ /** Construct the installed Stripe peer with the consumer's explicit API/transport configuration. */
324
+ declare function createStripeClient(secretKey: string, options: Stripe.StripeConfig): Stripe;
325
+ /** Verify raw webhook bytes using the installed SDK's timestamp/signature checks. */
326
+ declare function verifyStripeWebhookEvent(client: Pick<Stripe, 'webhooks'>, payload: Buffer | string, signature: string, webhookSecret: string): Stripe.Event;
327
+ /** Stripe expandable references can be an ID, an expanded object, or absent. */
328
+ declare function stripeResourceId(value: string | {
329
+ id: string;
330
+ } | null | undefined): string | undefined;
331
+ interface StripeCheckoutObservation {
332
+ raw: Stripe.Checkout.Session;
333
+ id: string;
334
+ url?: string;
335
+ clientReferenceId?: string;
336
+ paymentIntentId?: string;
337
+ subscriptionId?: string;
338
+ settlement: PaymentSettlementState;
339
+ }
340
+ declare function observeStripeCheckoutSession(raw: Stripe.Checkout.Session): StripeCheckoutObservation;
341
+ declare function createStripeCheckoutSession(client: Pick<Stripe, 'checkout'>, params: Stripe.Checkout.SessionCreateParams): Promise<StripeCheckoutObservation>;
342
+ declare function retrieveStripeCheckoutSession(client: Pick<Stripe, 'checkout'>, id: string): Promise<StripeCheckoutObservation>;
343
+ interface StripePaymentIntentObservation {
344
+ raw: Stripe.PaymentIntent;
345
+ id: string;
346
+ chargeId: string | null;
347
+ paymentMethodType?: string;
348
+ failureCode?: string;
349
+ failureMessage?: string;
350
+ settlement: PaymentSettlementState;
351
+ }
352
+ declare function observeStripePaymentIntent(raw: Stripe.PaymentIntent): StripePaymentIntentObservation;
353
+ declare function retrieveStripePaymentIntent(client: Pick<Stripe, 'paymentIntents'>, id: string, params?: Stripe.PaymentIntentRetrieveParams): Promise<StripePaymentIntentObservation>;
354
+
355
+ interface PayPalPaymentTransportOptions {
356
+ clientId: string;
357
+ clientSecret: string;
358
+ mode: 'live' | 'sandbox';
359
+ fetcher?: typeof fetch;
360
+ logger?: {
361
+ warn: (message: string) => void;
362
+ error: (message: string) => void;
363
+ };
364
+ }
365
+ interface PayPalCaptureRefundRequest {
366
+ amount: {
367
+ value: string;
368
+ currency_code: string;
369
+ };
370
+ note_to_payer?: string;
371
+ }
372
+ interface PayPalCaptureRefundResponse {
373
+ id?: string;
374
+ status?: string;
375
+ message?: string;
376
+ }
377
+ interface PayPalOrderRepresentation {
378
+ id?: string;
379
+ status?: string;
380
+ links?: Array<{
381
+ rel?: string;
382
+ href?: string;
383
+ }>;
384
+ purchaseUnits?: Array<{
385
+ payments?: {
386
+ captures?: Array<{
387
+ id?: string;
388
+ }>;
389
+ };
390
+ }>;
391
+ }
392
+ interface PayPalOrderObservation<T extends PayPalOrderRepresentation = PayPalOrderRepresentation> {
393
+ raw: T;
394
+ id?: string;
395
+ approveUrl?: string;
396
+ captureId?: string;
397
+ settlement: PaymentSettlementState;
398
+ }
399
+ /** APPROVED means approval only, never evidence of a captured payment. */
400
+ declare function observePayPalOrder<T extends PayPalOrderRepresentation>(raw: T): PayPalOrderObservation<T>;
401
+ interface PayPalCaptureObservation {
402
+ raw: Record<string, unknown>;
403
+ id: string;
404
+ failureReason?: string;
405
+ }
406
+ type PayPalWebhookObservation = {
407
+ raw: Record<string, unknown>;
408
+ type: string;
409
+ kind: 'capture-succeeded' | 'capture-failed';
410
+ capture: PayPalCaptureObservation;
411
+ } | {
412
+ raw: Record<string, unknown>;
413
+ type: string;
414
+ kind: 'unknown';
415
+ };
416
+ /** Decode only after signature verification; this performs no authentication or network call. */
417
+ declare function decodePayPalWebhookEvent(payload: string): PayPalWebhookObservation;
418
+ /** SDK order transport plus the OAuth-backed REST endpoints absent from the current SDK integration. */
419
+ declare class PayPalPaymentTransport {
420
+ private readonly options;
421
+ readonly orders: OrdersController;
422
+ private readonly baseUrl;
423
+ private readonly fetcher;
424
+ constructor(options: PayPalPaymentTransportOptions);
425
+ createOrder(input: Parameters<OrdersController['createOrder']>[0]): Promise<PayPalOrderObservation<_paypal_paypal_server_sdk.Order>>;
426
+ /** An explicit capture command; never invoked by getOrder or status observation. */
427
+ captureOrder(input: Parameters<OrdersController['captureOrder']>[0]): Promise<PayPalOrderObservation<_paypal_paypal_server_sdk.Order>>;
428
+ getOrder(input: Parameters<OrdersController['getOrder']>[0]): Promise<PayPalOrderObservation<_paypal_paypal_server_sdk.Order>>;
429
+ private getAccessToken;
430
+ verifyWebhookSignature(payload: string, headers: Record<string, string>, webhookId: string): Promise<boolean>;
431
+ refundCapture(captureId: string, request: PayPalCaptureRefundRequest): Promise<PayPalCaptureRefundResponse>;
432
+ }
433
+
434
+ export { type CreateOrderParams, type CreatePaymentParams, DatabaseOrderPersistence, ORDER_PERSISTENCE, type Order, type OrderFailedEvent, OrderNotFoundError, type OrderPersistence, OrderService, type OrderStatus, PAYMENT_PROVIDERS, type PayPalCaptureObservation, type PayPalCaptureRefundRequest, type PayPalCaptureRefundResponse, type PayPalOrderObservation, type PayPalOrderRepresentation, PayPalPaymentTransport, type PayPalPaymentTransportOptions, type PayPalWebhookObservation, type PaymentEventType, PaymentEvents, type PaymentIntent, type PaymentModuleAsyncOptions, type PaymentModuleOptions, type PaymentProvider, type PaymentRecoverySentEvent, PaymentRecoveryService, type PaymentResult, PaymentService, type PaymentSettlementState, type PaymentStatus, 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 RefundResult, type StripeCheckoutObservation, type StripePaymentIntentObservation, type Subscription, type SubscriptionPlan, type SubscriptionProvider, type WebhookEvent, ZuckerPaymentModule, createStripeCheckoutSession, createStripeClient, decodePayPalWebhookEvent, friendlyErrorReason, observePayPalOrder, observeStripeCheckoutSession, observeStripePaymentIntent, retrieveStripeCheckoutSession, retrieveStripePaymentIntent, stripeResourceId, verifyStripeWebhookEvent };