@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 +35 -0
- package/dist/index.d.mts +177 -26
- package/dist/index.d.ts +177 -26
- package/dist/index.js +497 -190
- package/dist/index.mjs +482 -192
- package/package.json +16 -4
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
316
|
+
static forRoot(options: PaymentModuleOptions): DynamicModule;
|
|
280
317
|
static forRootAsync(options: PaymentModuleAsyncOptions): DynamicModule;
|
|
281
318
|
}
|
|
282
319
|
|
|
283
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
316
|
+
static forRoot(options: PaymentModuleOptions): DynamicModule;
|
|
280
317
|
static forRootAsync(options: PaymentModuleAsyncOptions): DynamicModule;
|
|
281
318
|
}
|
|
282
319
|
|
|
283
|
-
|
|
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 };
|