@hazeljs/payment 0.2.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +192 -0
- package/README.md +224 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/payment.controller.d.ts +29 -0
- package/dist/payment.controller.d.ts.map +1 -0
- package/dist/payment.controller.js +98 -0
- package/dist/payment.controller.test.d.ts +2 -0
- package/dist/payment.controller.test.d.ts.map +1 -0
- package/dist/payment.controller.test.js +131 -0
- package/dist/payment.module.d.ts +21 -0
- package/dist/payment.module.d.ts.map +1 -0
- package/dist/payment.module.js +40 -0
- package/dist/payment.module.test.d.ts +2 -0
- package/dist/payment.module.test.d.ts.map +1 -0
- package/dist/payment.module.test.js +71 -0
- package/dist/payment.service.d.ts +33 -0
- package/dist/payment.service.d.ts.map +1 -0
- package/dist/payment.service.js +88 -0
- package/dist/payment.service.test.d.ts +2 -0
- package/dist/payment.service.test.d.ts.map +1 -0
- package/dist/payment.service.test.js +219 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +9 -0
- package/dist/providers/provider.interface.d.ts +32 -0
- package/dist/providers/provider.interface.d.ts.map +1 -0
- package/dist/providers/provider.interface.js +5 -0
- package/dist/providers/stripe/stripe.provider.d.ts +26 -0
- package/dist/providers/stripe/stripe.provider.d.ts.map +1 -0
- package/dist/providers/stripe/stripe.provider.js +143 -0
- package/dist/providers/stripe/stripe.provider.test.d.ts +2 -0
- package/dist/providers/stripe/stripe.provider.test.d.ts.map +1 -0
- package/dist/providers/stripe/stripe.provider.test.js +383 -0
- package/dist/providers/stripe/stripe.types.d.ts +15 -0
- package/dist/providers/stripe/stripe.types.d.ts.map +1 -0
- package/dist/providers/stripe/stripe.types.js +5 -0
- package/dist/types/payment.types.d.ts +94 -0
- package/dist/types/payment.types.d.ts.map +1 -0
- package/dist/types/payment.types.js +5 -0
- package/package.json +55 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment provider interface. Implement this to add new payment methods (Stripe, PayPal, Paddle, etc.).
|
|
3
|
+
*/
|
|
4
|
+
import type { CreateCheckoutSessionOptions, CreateCheckoutSessionResult, CreateCustomerOptions, Customer, CheckoutSessionInfo, Subscription, SubscriptionStatusFilter } from '../types/payment.types';
|
|
5
|
+
export interface ListSubscriptionsResult {
|
|
6
|
+
data: Subscription[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Implement this interface to add a payment provider to @hazeljs/payment.
|
|
10
|
+
*/
|
|
11
|
+
export interface PaymentProvider {
|
|
12
|
+
/** Provider identifier (e.g. 'stripe', 'paypal'). */
|
|
13
|
+
readonly name: string;
|
|
14
|
+
/** Create a checkout session; returns URL to redirect the customer. */
|
|
15
|
+
createCheckoutSession(options: CreateCheckoutSessionOptions): Promise<CreateCheckoutSessionResult>;
|
|
16
|
+
/** Create a customer in the provider's system. */
|
|
17
|
+
createCustomer(options: CreateCustomerOptions): Promise<Customer>;
|
|
18
|
+
/** Retrieve a customer by ID. */
|
|
19
|
+
getCustomer(customerId: string): Promise<Customer | null>;
|
|
20
|
+
/** List subscriptions for a customer. */
|
|
21
|
+
listSubscriptions(customerId: string, status?: SubscriptionStatusFilter): Promise<ListSubscriptionsResult>;
|
|
22
|
+
/** Retrieve a checkout session (e.g. after success redirect). */
|
|
23
|
+
getCheckoutSession(sessionId: string): Promise<CheckoutSessionInfo>;
|
|
24
|
+
/** Whether webhook verification is configured for this provider. */
|
|
25
|
+
isWebhookConfigured(): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Verify and parse a webhook event from raw body and signature.
|
|
28
|
+
* Returns provider-specific event object. Throws if signature invalid or not configured.
|
|
29
|
+
*/
|
|
30
|
+
parseWebhookEvent(payload: string | Buffer, signature: string): unknown;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=provider.interface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.interface.d.ts","sourceRoot":"","sources":["../../src/providers/provider.interface.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,4BAA4B,EAC5B,2BAA2B,EAC3B,qBAAqB,EACrB,QAAQ,EACR,mBAAmB,EACnB,YAAY,EACZ,wBAAwB,EACzB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,YAAY,EAAE,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,uEAAuE;IACvE,qBAAqB,CACnB,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAExC,kDAAkD;IAClD,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAElE,iCAAiC;IACjC,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IAE1D,yCAAyC;IACzC,iBAAiB,CACf,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,wBAAwB,GAChC,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAEpC,iEAAiE;IACjE,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAEpE,oEAAoE;IACpE,mBAAmB,IAAI,OAAO,CAAC;IAE/B;;;OAGG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CACzE"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe implementation of PaymentProvider.
|
|
3
|
+
*/
|
|
4
|
+
import Stripe from 'stripe';
|
|
5
|
+
import type { PaymentProvider } from '../provider.interface';
|
|
6
|
+
import type { StripeProviderOptions } from './stripe.types';
|
|
7
|
+
import type { ListSubscriptionsResult } from '../provider.interface';
|
|
8
|
+
import type { CreateCheckoutSessionOptions, CreateCheckoutSessionResult, CreateCustomerOptions, Customer, CheckoutSessionInfo, SubscriptionStatusFilter } from '../../types/payment.types';
|
|
9
|
+
export declare const STRIPE_PROVIDER_NAME = "stripe";
|
|
10
|
+
export declare class StripePaymentProvider implements PaymentProvider {
|
|
11
|
+
readonly name = "stripe";
|
|
12
|
+
private readonly stripe;
|
|
13
|
+
private readonly webhookSecret;
|
|
14
|
+
constructor(options: StripeProviderOptions);
|
|
15
|
+
/** Raw Stripe client for advanced usage. */
|
|
16
|
+
getClient(): Stripe;
|
|
17
|
+
createCheckoutSession(options: CreateCheckoutSessionOptions): Promise<CreateCheckoutSessionResult>;
|
|
18
|
+
createCustomer(options: CreateCustomerOptions): Promise<Customer>;
|
|
19
|
+
getCustomer(customerId: string): Promise<Customer | null>;
|
|
20
|
+
listSubscriptions(customerId: string, status?: SubscriptionStatusFilter): Promise<ListSubscriptionsResult>;
|
|
21
|
+
getCheckoutSession(sessionId: string): Promise<CheckoutSessionInfo>;
|
|
22
|
+
isWebhookConfigured(): boolean;
|
|
23
|
+
parseWebhookEvent(payload: string | Buffer, signature: string): Stripe.Event;
|
|
24
|
+
private toCustomer;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=stripe.provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe.provider.d.ts","sourceRoot":"","sources":["../../../src/providers/stripe/stripe.provider.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EACV,4BAA4B,EAC5B,2BAA2B,EAC3B,qBAAqB,EACrB,QAAQ,EACR,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,2BAA2B,CAAC;AAEnC,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAE7C,qBAAa,qBAAsB,YAAW,eAAe;IAC3D,QAAQ,CAAC,IAAI,YAAwB;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;gBAEvC,OAAO,EAAE,qBAAqB;IAa1C,4CAA4C;IAC5C,SAAS,IAAI,MAAM;IAIb,qBAAqB,CACzB,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,2BAA2B,CAAC;IAyDjC,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,QAAQ,CAAC;IASjE,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAMzD,iBAAiB,CACrB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,wBAAwB,GAChC,OAAO,CAAC,uBAAuB,CAAC;IAgB7B,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAkBzE,mBAAmB,IAAI,OAAO;IAI9B,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK;IAa5E,OAAO,CAAC,UAAU;CAQnB"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Stripe implementation of PaymentProvider.
|
|
4
|
+
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.StripePaymentProvider = exports.STRIPE_PROVIDER_NAME = void 0;
|
|
10
|
+
const stripe_1 = __importDefault(require("stripe"));
|
|
11
|
+
exports.STRIPE_PROVIDER_NAME = 'stripe';
|
|
12
|
+
class StripePaymentProvider {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.name = exports.STRIPE_PROVIDER_NAME;
|
|
15
|
+
const secretKey = options.secretKey ?? process.env.STRIPE_SECRET_KEY;
|
|
16
|
+
if (!secretKey) {
|
|
17
|
+
throw new Error('Stripe secret key is required. Set STRIPE_SECRET_KEY or pass secretKey in StripeProviderOptions.');
|
|
18
|
+
}
|
|
19
|
+
this.stripe = new stripe_1.default(secretKey, {
|
|
20
|
+
apiVersion: options.apiVersion,
|
|
21
|
+
});
|
|
22
|
+
this.webhookSecret = options.webhookSecret ?? process.env.STRIPE_WEBHOOK_SECRET;
|
|
23
|
+
}
|
|
24
|
+
/** Raw Stripe client for advanced usage. */
|
|
25
|
+
getClient() {
|
|
26
|
+
return this.stripe;
|
|
27
|
+
}
|
|
28
|
+
async createCheckoutSession(options) {
|
|
29
|
+
const mode = options.mode ?? (options.subscription ? 'subscription' : 'payment');
|
|
30
|
+
const stripeOptions = (options.providerOptions?.stripe ??
|
|
31
|
+
{});
|
|
32
|
+
const params = {
|
|
33
|
+
success_url: options.successUrl,
|
|
34
|
+
cancel_url: options.cancelUrl,
|
|
35
|
+
mode,
|
|
36
|
+
...(options.clientReferenceId && { client_reference_id: options.clientReferenceId }),
|
|
37
|
+
...(options.customerId && { customer: options.customerId }),
|
|
38
|
+
...(options.customerEmail &&
|
|
39
|
+
!options.customerId && { customer_email: options.customerEmail }),
|
|
40
|
+
...(options.allowPromotionCodes && { allow_promotion_codes: true }),
|
|
41
|
+
...stripeOptions,
|
|
42
|
+
};
|
|
43
|
+
if (mode === 'subscription' && options.subscription) {
|
|
44
|
+
params.line_items = [
|
|
45
|
+
{
|
|
46
|
+
price: options.subscription.priceId,
|
|
47
|
+
quantity: options.subscription.quantity ?? 1,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
if (options.subscription.trialPeriodDays) {
|
|
51
|
+
params.subscription_data = {
|
|
52
|
+
trial_period_days: options.subscription.trialPeriodDays,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (options.lineItems && options.lineItems.length > 0) {
|
|
57
|
+
params.line_items = options.lineItems.map((item) => {
|
|
58
|
+
if (item.priceId) {
|
|
59
|
+
return { price: item.priceId, quantity: item.quantity ?? 1 };
|
|
60
|
+
}
|
|
61
|
+
if (item.priceData) {
|
|
62
|
+
return {
|
|
63
|
+
price_data: {
|
|
64
|
+
currency: item.priceData.currency,
|
|
65
|
+
unit_amount: item.priceData.unitAmount,
|
|
66
|
+
product_data: {
|
|
67
|
+
name: item.priceData.productData.name,
|
|
68
|
+
description: item.priceData.productData.description,
|
|
69
|
+
images: item.priceData.productData.images,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
quantity: item.quantity ?? 1,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
throw new Error('Each line item must have priceId or priceData');
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const session = await this.stripe.checkout.sessions.create(params);
|
|
79
|
+
return { sessionId: session.id, url: session.url ?? null };
|
|
80
|
+
}
|
|
81
|
+
async createCustomer(options) {
|
|
82
|
+
const customer = await this.stripe.customers.create({
|
|
83
|
+
email: options.email,
|
|
84
|
+
name: options.name,
|
|
85
|
+
metadata: options.metadata,
|
|
86
|
+
});
|
|
87
|
+
return this.toCustomer(customer);
|
|
88
|
+
}
|
|
89
|
+
async getCustomer(customerId) {
|
|
90
|
+
const customer = await this.stripe.customers.retrieve(customerId);
|
|
91
|
+
if (customer.deleted)
|
|
92
|
+
return null;
|
|
93
|
+
return this.toCustomer(customer);
|
|
94
|
+
}
|
|
95
|
+
async listSubscriptions(customerId, status) {
|
|
96
|
+
const list = await this.stripe.subscriptions.list({
|
|
97
|
+
customer: customerId,
|
|
98
|
+
status: status ?? 'all',
|
|
99
|
+
expand: ['data.items.data.price'],
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
data: list.data.map((s) => ({
|
|
103
|
+
...s,
|
|
104
|
+
id: s.id,
|
|
105
|
+
status: s.status,
|
|
106
|
+
customerId: s.customer,
|
|
107
|
+
})),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async getCheckoutSession(sessionId) {
|
|
111
|
+
const session = await this.stripe.checkout.sessions.retrieve(sessionId, {
|
|
112
|
+
expand: ['customer', 'subscription'],
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
...session,
|
|
116
|
+
id: session.id,
|
|
117
|
+
url: session.url ?? null,
|
|
118
|
+
customerId: typeof session.customer === 'string' ? session.customer : (session.customer?.id ?? null),
|
|
119
|
+
subscriptionId: typeof session.subscription === 'string'
|
|
120
|
+
? session.subscription
|
|
121
|
+
: (session.subscription?.id ?? null),
|
|
122
|
+
status: session.status ?? undefined,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
isWebhookConfigured() {
|
|
126
|
+
return Boolean(this.webhookSecret);
|
|
127
|
+
}
|
|
128
|
+
parseWebhookEvent(payload, signature) {
|
|
129
|
+
if (!this.webhookSecret) {
|
|
130
|
+
throw new Error('Stripe webhook secret is required. Set STRIPE_WEBHOOK_SECRET or pass webhookSecret in StripeProviderOptions.');
|
|
131
|
+
}
|
|
132
|
+
return this.stripe.webhooks.constructEvent(payload, signature, this.webhookSecret);
|
|
133
|
+
}
|
|
134
|
+
toCustomer(c) {
|
|
135
|
+
return {
|
|
136
|
+
id: c.id,
|
|
137
|
+
email: c.email ?? null,
|
|
138
|
+
name: c.name ?? null,
|
|
139
|
+
metadata: c.metadata,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
exports.StripePaymentProvider = StripePaymentProvider;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe.provider.test.d.ts","sourceRoot":"","sources":["../../../src/providers/stripe/stripe.provider.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const stripe_provider_1 = require("./stripe.provider");
|
|
4
|
+
const mockCheckoutSessionsCreate = jest.fn();
|
|
5
|
+
const mockCustomersCreate = jest.fn();
|
|
6
|
+
const mockCustomersRetrieve = jest.fn();
|
|
7
|
+
const mockSubscriptionsList = jest.fn();
|
|
8
|
+
const mockCheckoutSessionsRetrieve = jest.fn();
|
|
9
|
+
const mockWebhooksConstructEvent = jest.fn();
|
|
10
|
+
jest.mock('stripe', () => {
|
|
11
|
+
return jest.fn().mockImplementation(() => ({
|
|
12
|
+
checkout: {
|
|
13
|
+
sessions: {
|
|
14
|
+
create: mockCheckoutSessionsCreate,
|
|
15
|
+
retrieve: mockCheckoutSessionsRetrieve,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
customers: {
|
|
19
|
+
create: mockCustomersCreate,
|
|
20
|
+
retrieve: mockCustomersRetrieve,
|
|
21
|
+
},
|
|
22
|
+
subscriptions: {
|
|
23
|
+
list: mockSubscriptionsList,
|
|
24
|
+
},
|
|
25
|
+
webhooks: {
|
|
26
|
+
constructEvent: mockWebhooksConstructEvent,
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
});
|
|
30
|
+
describe('StripePaymentProvider', () => {
|
|
31
|
+
const baseOptions = { secretKey: 'sk_test_xxx' };
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
jest.clearAllMocks();
|
|
34
|
+
delete process.env.STRIPE_SECRET_KEY;
|
|
35
|
+
delete process.env.STRIPE_WEBHOOK_SECRET;
|
|
36
|
+
mockCheckoutSessionsCreate.mockResolvedValue({
|
|
37
|
+
id: 'cs_123',
|
|
38
|
+
url: 'https://checkout.stripe.com/pay',
|
|
39
|
+
});
|
|
40
|
+
mockCustomersCreate.mockResolvedValue({
|
|
41
|
+
id: 'cus_123',
|
|
42
|
+
email: 'u@example.com',
|
|
43
|
+
name: 'User',
|
|
44
|
+
metadata: {},
|
|
45
|
+
deleted: false,
|
|
46
|
+
});
|
|
47
|
+
mockCustomersRetrieve.mockResolvedValue({
|
|
48
|
+
id: 'cus_123',
|
|
49
|
+
email: 'u@example.com',
|
|
50
|
+
name: 'User',
|
|
51
|
+
metadata: {},
|
|
52
|
+
deleted: false,
|
|
53
|
+
});
|
|
54
|
+
mockSubscriptionsList.mockResolvedValue({
|
|
55
|
+
data: [{ id: 'sub_1', status: 'active', customer: 'cus_123' }],
|
|
56
|
+
});
|
|
57
|
+
mockCheckoutSessionsRetrieve.mockResolvedValue({
|
|
58
|
+
id: 'cs_123',
|
|
59
|
+
url: null,
|
|
60
|
+
customer: 'cus_123',
|
|
61
|
+
subscription: 'sub_1',
|
|
62
|
+
status: 'complete',
|
|
63
|
+
});
|
|
64
|
+
mockWebhooksConstructEvent.mockReturnValue({ type: 'checkout.session.completed' });
|
|
65
|
+
});
|
|
66
|
+
describe('constructor', () => {
|
|
67
|
+
it('throws when no secret key', () => {
|
|
68
|
+
expect(() => new stripe_provider_1.StripePaymentProvider({})).toThrow('Stripe secret key is required');
|
|
69
|
+
});
|
|
70
|
+
it('uses options.secretKey', () => {
|
|
71
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
72
|
+
expect(provider).toBeDefined();
|
|
73
|
+
expect(provider.getClient()).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
it('uses STRIPE_SECRET_KEY env when secretKey not in options', () => {
|
|
76
|
+
process.env.STRIPE_SECRET_KEY = 'sk_env_xxx';
|
|
77
|
+
const provider = new stripe_provider_1.StripePaymentProvider({});
|
|
78
|
+
expect(provider).toBeDefined();
|
|
79
|
+
delete process.env.STRIPE_SECRET_KEY;
|
|
80
|
+
});
|
|
81
|
+
it('sets webhookSecret from options', () => {
|
|
82
|
+
const provider = new stripe_provider_1.StripePaymentProvider({ ...baseOptions, webhookSecret: 'whsec_xxx' });
|
|
83
|
+
expect(provider.isWebhookConfigured()).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
it('sets webhookSecret from env', () => {
|
|
86
|
+
process.env.STRIPE_SECRET_KEY = 'sk_xxx';
|
|
87
|
+
process.env.STRIPE_WEBHOOK_SECRET = 'whsec_env';
|
|
88
|
+
const provider = new stripe_provider_1.StripePaymentProvider({});
|
|
89
|
+
expect(provider.isWebhookConfigured()).toBe(true);
|
|
90
|
+
delete process.env.STRIPE_SECRET_KEY;
|
|
91
|
+
delete process.env.STRIPE_WEBHOOK_SECRET;
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('name', () => {
|
|
95
|
+
it('has correct provider name', () => {
|
|
96
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
97
|
+
expect(provider.name).toBe(stripe_provider_1.STRIPE_PROVIDER_NAME);
|
|
98
|
+
expect(stripe_provider_1.STRIPE_PROVIDER_NAME).toBe('stripe');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('createCheckoutSession', () => {
|
|
102
|
+
it('creates one-time payment session with lineItems', async () => {
|
|
103
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
104
|
+
const result = await provider.createCheckoutSession({
|
|
105
|
+
successUrl: 'https://a.com/s',
|
|
106
|
+
cancelUrl: 'https://a.com/c',
|
|
107
|
+
lineItems: [
|
|
108
|
+
{
|
|
109
|
+
priceData: {
|
|
110
|
+
currency: 'usd',
|
|
111
|
+
unitAmount: 1999,
|
|
112
|
+
productData: { name: 'Pro Plan' },
|
|
113
|
+
},
|
|
114
|
+
quantity: 1,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
expect(result).toEqual({ sessionId: 'cs_123', url: 'https://checkout.stripe.com/pay' });
|
|
119
|
+
expect(mockCheckoutSessionsCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
120
|
+
success_url: 'https://a.com/s',
|
|
121
|
+
cancel_url: 'https://a.com/c',
|
|
122
|
+
mode: 'payment',
|
|
123
|
+
line_items: expect.any(Array),
|
|
124
|
+
}));
|
|
125
|
+
});
|
|
126
|
+
it('creates session with priceId line items', async () => {
|
|
127
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
128
|
+
await provider.createCheckoutSession({
|
|
129
|
+
successUrl: 'https://a.com/s',
|
|
130
|
+
cancelUrl: 'https://a.com/c',
|
|
131
|
+
lineItems: [{ priceId: 'price_xxx', quantity: 2 }],
|
|
132
|
+
});
|
|
133
|
+
expect(mockCheckoutSessionsCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
134
|
+
line_items: [{ price: 'price_xxx', quantity: 2 }],
|
|
135
|
+
}));
|
|
136
|
+
});
|
|
137
|
+
it('creates subscription session with trial', async () => {
|
|
138
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
139
|
+
await provider.createCheckoutSession({
|
|
140
|
+
successUrl: 'https://a.com/s',
|
|
141
|
+
cancelUrl: 'https://a.com/c',
|
|
142
|
+
subscription: { priceId: 'price_sub', quantity: 1, trialPeriodDays: 14 },
|
|
143
|
+
});
|
|
144
|
+
expect(mockCheckoutSessionsCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
145
|
+
mode: 'subscription',
|
|
146
|
+
line_items: [{ price: 'price_sub', quantity: 1 }],
|
|
147
|
+
subscription_data: { trial_period_days: 14 },
|
|
148
|
+
}));
|
|
149
|
+
});
|
|
150
|
+
it('passes customerId and clientReferenceId', async () => {
|
|
151
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
152
|
+
await provider.createCheckoutSession({
|
|
153
|
+
successUrl: 'https://a.com/s',
|
|
154
|
+
cancelUrl: 'https://a.com/c',
|
|
155
|
+
customerId: 'cus_existing',
|
|
156
|
+
clientReferenceId: 'order_123',
|
|
157
|
+
});
|
|
158
|
+
expect(mockCheckoutSessionsCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
159
|
+
customer: 'cus_existing',
|
|
160
|
+
client_reference_id: 'order_123',
|
|
161
|
+
}));
|
|
162
|
+
});
|
|
163
|
+
it('passes customerEmail when no customerId', async () => {
|
|
164
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
165
|
+
await provider.createCheckoutSession({
|
|
166
|
+
successUrl: 'https://a.com/s',
|
|
167
|
+
cancelUrl: 'https://a.com/c',
|
|
168
|
+
customerEmail: 'new@example.com',
|
|
169
|
+
});
|
|
170
|
+
expect(mockCheckoutSessionsCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
171
|
+
customer_email: 'new@example.com',
|
|
172
|
+
}));
|
|
173
|
+
});
|
|
174
|
+
it('does not pass customer_email when customerId is set', async () => {
|
|
175
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
176
|
+
await provider.createCheckoutSession({
|
|
177
|
+
successUrl: 'https://a.com/s',
|
|
178
|
+
cancelUrl: 'https://a.com/c',
|
|
179
|
+
customerId: 'cus_exist',
|
|
180
|
+
customerEmail: 'ignore@example.com',
|
|
181
|
+
});
|
|
182
|
+
const call = mockCheckoutSessionsCreate.mock.calls[0][0];
|
|
183
|
+
expect(call.customer).toBe('cus_exist');
|
|
184
|
+
expect(call.customer_email).toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
it('passes allowPromotionCodes when true', async () => {
|
|
187
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
188
|
+
await provider.createCheckoutSession({
|
|
189
|
+
successUrl: 'https://a.com/s',
|
|
190
|
+
cancelUrl: 'https://a.com/c',
|
|
191
|
+
allowPromotionCodes: true,
|
|
192
|
+
});
|
|
193
|
+
expect(mockCheckoutSessionsCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
194
|
+
allow_promotion_codes: true,
|
|
195
|
+
}));
|
|
196
|
+
});
|
|
197
|
+
it('creates subscription session without trial', async () => {
|
|
198
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
199
|
+
await provider.createCheckoutSession({
|
|
200
|
+
successUrl: 'https://a.com/s',
|
|
201
|
+
cancelUrl: 'https://a.com/c',
|
|
202
|
+
subscription: { priceId: 'price_sub', quantity: 2 },
|
|
203
|
+
});
|
|
204
|
+
expect(mockCheckoutSessionsCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
205
|
+
mode: 'subscription',
|
|
206
|
+
line_items: [{ price: 'price_sub', quantity: 2 }],
|
|
207
|
+
}));
|
|
208
|
+
const call = mockCheckoutSessionsCreate.mock.calls[0][0];
|
|
209
|
+
expect(call.subscription_data).toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
it('passes description and images in priceData productData', async () => {
|
|
212
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
213
|
+
await provider.createCheckoutSession({
|
|
214
|
+
successUrl: 'https://a.com/s',
|
|
215
|
+
cancelUrl: 'https://a.com/c',
|
|
216
|
+
lineItems: [
|
|
217
|
+
{
|
|
218
|
+
priceData: {
|
|
219
|
+
currency: 'eur',
|
|
220
|
+
unitAmount: 999,
|
|
221
|
+
productData: {
|
|
222
|
+
name: 'Pro',
|
|
223
|
+
description: 'Pro plan',
|
|
224
|
+
images: ['https://example.com/img.png'],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
quantity: 1,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
expect(mockCheckoutSessionsCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
232
|
+
line_items: [
|
|
233
|
+
expect.objectContaining({
|
|
234
|
+
price_data: expect.objectContaining({
|
|
235
|
+
product_data: expect.objectContaining({
|
|
236
|
+
name: 'Pro',
|
|
237
|
+
description: 'Pro plan',
|
|
238
|
+
images: ['https://example.com/img.png'],
|
|
239
|
+
}),
|
|
240
|
+
}),
|
|
241
|
+
}),
|
|
242
|
+
],
|
|
243
|
+
}));
|
|
244
|
+
});
|
|
245
|
+
it('merges providerOptions.stripe', async () => {
|
|
246
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
247
|
+
await provider.createCheckoutSession({
|
|
248
|
+
successUrl: 'https://a.com/s',
|
|
249
|
+
cancelUrl: 'https://a.com/c',
|
|
250
|
+
providerOptions: {
|
|
251
|
+
stripe: { payment_intent_data: { setup_future_usage: 'off_session' } },
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
expect(mockCheckoutSessionsCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
255
|
+
payment_intent_data: { setup_future_usage: 'off_session' },
|
|
256
|
+
}));
|
|
257
|
+
});
|
|
258
|
+
it('throws when line item has neither priceId nor priceData', async () => {
|
|
259
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
260
|
+
await expect(provider.createCheckoutSession({
|
|
261
|
+
successUrl: 'https://a.com/s',
|
|
262
|
+
cancelUrl: 'https://a.com/c',
|
|
263
|
+
lineItems: [{}],
|
|
264
|
+
})).rejects.toThrow('Each line item must have priceId or priceData');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
describe('createCustomer', () => {
|
|
268
|
+
it('creates customer and maps to Customer type', async () => {
|
|
269
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
270
|
+
const customer = await provider.createCustomer({
|
|
271
|
+
email: 'u@example.com',
|
|
272
|
+
name: 'Alice',
|
|
273
|
+
metadata: { userId: 'u1' },
|
|
274
|
+
});
|
|
275
|
+
expect(customer).toEqual({
|
|
276
|
+
id: 'cus_123',
|
|
277
|
+
email: 'u@example.com',
|
|
278
|
+
name: 'User',
|
|
279
|
+
metadata: {},
|
|
280
|
+
});
|
|
281
|
+
expect(mockCustomersCreate).toHaveBeenCalledWith({
|
|
282
|
+
email: 'u@example.com',
|
|
283
|
+
name: 'Alice',
|
|
284
|
+
metadata: { userId: 'u1' },
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
it('maps customer with undefined metadata', async () => {
|
|
288
|
+
mockCustomersCreate.mockResolvedValueOnce({
|
|
289
|
+
id: 'cus_no_meta',
|
|
290
|
+
email: 'n@example.com',
|
|
291
|
+
name: null,
|
|
292
|
+
metadata: undefined,
|
|
293
|
+
deleted: false,
|
|
294
|
+
});
|
|
295
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
296
|
+
const customer = await provider.createCustomer({ email: 'n@example.com' });
|
|
297
|
+
expect(customer.metadata).toBeUndefined();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
describe('getCustomer', () => {
|
|
301
|
+
it('returns null for deleted customer', async () => {
|
|
302
|
+
mockCustomersRetrieve.mockResolvedValueOnce({ id: 'cus_del', deleted: true });
|
|
303
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
304
|
+
const customer = await provider.getCustomer('cus_del');
|
|
305
|
+
expect(customer).toBeNull();
|
|
306
|
+
});
|
|
307
|
+
it('returns mapped customer for existing', async () => {
|
|
308
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
309
|
+
const customer = await provider.getCustomer('cus_123');
|
|
310
|
+
expect(customer).not.toBeNull();
|
|
311
|
+
expect(customer?.id).toBe('cus_123');
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
describe('listSubscriptions', () => {
|
|
315
|
+
it('returns list with data array', async () => {
|
|
316
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
317
|
+
const result = await provider.listSubscriptions('cus_123');
|
|
318
|
+
expect(result.data).toHaveLength(1);
|
|
319
|
+
expect(result.data[0].id).toBe('sub_1');
|
|
320
|
+
expect(result.data[0].status).toBe('active');
|
|
321
|
+
expect(result.data[0].customerId).toBe('cus_123');
|
|
322
|
+
});
|
|
323
|
+
it('passes status filter', async () => {
|
|
324
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
325
|
+
await provider.listSubscriptions('cus_123', 'active');
|
|
326
|
+
expect(mockSubscriptionsList).toHaveBeenCalledWith(expect.objectContaining({ status: 'active' }));
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
describe('getCheckoutSession', () => {
|
|
330
|
+
it('returns CheckoutSessionInfo with id, url, customerId, subscriptionId, status', async () => {
|
|
331
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
332
|
+
const session = await provider.getCheckoutSession('cs_123');
|
|
333
|
+
expect(session.id).toBe('cs_123');
|
|
334
|
+
expect(session.url).toBeNull();
|
|
335
|
+
expect(session.customerId).toBe('cus_123');
|
|
336
|
+
expect(session.subscriptionId).toBe('sub_1');
|
|
337
|
+
expect(session.status).toBe('complete');
|
|
338
|
+
});
|
|
339
|
+
it('handles expanded customer and subscription objects', async () => {
|
|
340
|
+
mockCheckoutSessionsRetrieve.mockResolvedValueOnce({
|
|
341
|
+
id: 'cs_expand',
|
|
342
|
+
url: 'https://checkout.stripe.com',
|
|
343
|
+
customer: { id: 'cus_expanded', object: 'customer' },
|
|
344
|
+
subscription: { id: 'sub_expanded', object: 'subscription' },
|
|
345
|
+
status: 'complete',
|
|
346
|
+
});
|
|
347
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
348
|
+
const session = await provider.getCheckoutSession('cs_expand');
|
|
349
|
+
expect(session.customerId).toBe('cus_expanded');
|
|
350
|
+
expect(session.subscriptionId).toBe('sub_expanded');
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
describe('isWebhookConfigured', () => {
|
|
354
|
+
it('returns false when no webhook secret', () => {
|
|
355
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
356
|
+
expect(provider.isWebhookConfigured()).toBe(false);
|
|
357
|
+
});
|
|
358
|
+
it('returns true when webhook secret set', () => {
|
|
359
|
+
const provider = new stripe_provider_1.StripePaymentProvider({ ...baseOptions, webhookSecret: 'whsec_xxx' });
|
|
360
|
+
expect(provider.isWebhookConfigured()).toBe(true);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
describe('parseWebhookEvent', () => {
|
|
364
|
+
it('throws when webhook secret not configured', () => {
|
|
365
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
366
|
+
expect(() => provider.parseWebhookEvent('payload', 'sig')).toThrow('Stripe webhook secret is required');
|
|
367
|
+
});
|
|
368
|
+
it('calls stripe.webhooks.constructEvent and returns event', () => {
|
|
369
|
+
const provider = new stripe_provider_1.StripePaymentProvider({ ...baseOptions, webhookSecret: 'whsec_xxx' });
|
|
370
|
+
const event = provider.parseWebhookEvent('payload', 'sig');
|
|
371
|
+
expect(mockWebhooksConstructEvent).toHaveBeenCalledWith('payload', 'sig', 'whsec_xxx');
|
|
372
|
+
expect(event).toEqual({ type: 'checkout.session.completed' });
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
describe('getClient', () => {
|
|
376
|
+
it('returns Stripe instance', () => {
|
|
377
|
+
const provider = new stripe_provider_1.StripePaymentProvider(baseOptions);
|
|
378
|
+
const client = provider.getClient();
|
|
379
|
+
expect(client).toBeDefined();
|
|
380
|
+
expect(client.checkout.sessions.create).toBe(mockCheckoutSessionsCreate);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe-specific configuration and types.
|
|
3
|
+
*/
|
|
4
|
+
import type Stripe from 'stripe';
|
|
5
|
+
export interface StripeProviderOptions {
|
|
6
|
+
/** Secret key (e.g. sk_test_...). Defaults to process.env.STRIPE_SECRET_KEY. */
|
|
7
|
+
secretKey?: string;
|
|
8
|
+
/** Webhook signing secret (e.g. whsec_...). Defaults to process.env.STRIPE_WEBHOOK_SECRET. */
|
|
9
|
+
webhookSecret?: string;
|
|
10
|
+
/** Optional Stripe API version. */
|
|
11
|
+
apiVersion?: Stripe.LatestApiVersion;
|
|
12
|
+
}
|
|
13
|
+
/** Re-export Stripe Event for webhook handlers. */
|
|
14
|
+
export type StripeWebhookEvent = Stripe.Event;
|
|
15
|
+
//# sourceMappingURL=stripe.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe.types.d.ts","sourceRoot":"","sources":["../../../src/providers/stripe/stripe.types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,MAAM,WAAW,qBAAqB;IACpC,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8FAA8F;IAC9F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mCAAmC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC,gBAAgB,CAAC;CACtC;AAED,mDAAmD;AACnD,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,KAAK,CAAC"}
|