@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,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const payment_controller_1 = require("./payment.controller");
|
|
4
|
+
const mockCreateCheckoutSession = jest.fn();
|
|
5
|
+
const mockParseWebhookEvent = jest.fn();
|
|
6
|
+
describe('PaymentController', () => {
|
|
7
|
+
let controller;
|
|
8
|
+
let res;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
res = {
|
|
12
|
+
status: jest.fn().mockReturnThis(),
|
|
13
|
+
json: jest.fn().mockReturnThis(),
|
|
14
|
+
send: jest.fn().mockReturnThis(),
|
|
15
|
+
};
|
|
16
|
+
const paymentService = {
|
|
17
|
+
createCheckoutSession: mockCreateCheckoutSession,
|
|
18
|
+
parseWebhookEvent: mockParseWebhookEvent,
|
|
19
|
+
};
|
|
20
|
+
controller = new payment_controller_1.PaymentController(paymentService);
|
|
21
|
+
});
|
|
22
|
+
describe('createCheckoutSession', () => {
|
|
23
|
+
it('returns 200 and session result on success', async () => {
|
|
24
|
+
mockCreateCheckoutSession.mockResolvedValue({
|
|
25
|
+
sessionId: 'cs_123',
|
|
26
|
+
url: 'https://checkout.stripe.com',
|
|
27
|
+
});
|
|
28
|
+
await controller.createCheckoutSession({
|
|
29
|
+
successUrl: 'https://a.com/s',
|
|
30
|
+
cancelUrl: 'https://a.com/c',
|
|
31
|
+
}, res);
|
|
32
|
+
expect(mockCreateCheckoutSession).toHaveBeenCalledWith({ successUrl: 'https://a.com/s', cancelUrl: 'https://a.com/c' }, undefined);
|
|
33
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
34
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
35
|
+
sessionId: 'cs_123',
|
|
36
|
+
url: 'https://checkout.stripe.com',
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it('passes provider from body when present', async () => {
|
|
40
|
+
mockCreateCheckoutSession.mockResolvedValue({ sessionId: 'cs_456', url: null });
|
|
41
|
+
await controller.createCheckoutSession({
|
|
42
|
+
successUrl: 'https://a.com/s',
|
|
43
|
+
cancelUrl: 'https://a.com/c',
|
|
44
|
+
provider: 'stripe',
|
|
45
|
+
}, res);
|
|
46
|
+
expect(mockCreateCheckoutSession).toHaveBeenCalledWith({ successUrl: 'https://a.com/s', cancelUrl: 'https://a.com/c' }, 'stripe');
|
|
47
|
+
});
|
|
48
|
+
it('returns 400 and error message on failure', async () => {
|
|
49
|
+
mockCreateCheckoutSession.mockRejectedValue(new Error('Invalid price ID'));
|
|
50
|
+
await controller.createCheckoutSession({ successUrl: 'https://a.com/s', cancelUrl: 'https://a.com/c' }, res);
|
|
51
|
+
expect(res.status).toHaveBeenCalledWith(400);
|
|
52
|
+
expect(res.json).toHaveBeenCalledWith({ error: 'Invalid price ID' });
|
|
53
|
+
});
|
|
54
|
+
it('returns 400 with generic message when error is not Error instance', async () => {
|
|
55
|
+
mockCreateCheckoutSession.mockRejectedValue('string error');
|
|
56
|
+
await controller.createCheckoutSession({ successUrl: 'https://a.com/s', cancelUrl: 'https://a.com/c' }, res);
|
|
57
|
+
expect(res.status).toHaveBeenCalledWith(400);
|
|
58
|
+
expect(res.json).toHaveBeenCalledWith({ error: 'Failed to create checkout session' });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('webhook', () => {
|
|
62
|
+
it('returns 400 when raw body is missing', async () => {
|
|
63
|
+
const req = { headers: { 'stripe-signature': 'sig' } };
|
|
64
|
+
await controller.webhook('stripe', req, res);
|
|
65
|
+
expect(res.status).toHaveBeenCalledWith(400);
|
|
66
|
+
expect(res.send).toHaveBeenCalledWith('Webhook requires raw body');
|
|
67
|
+
expect(mockParseWebhookEvent).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
it('uses rawBody when present', async () => {
|
|
70
|
+
mockParseWebhookEvent.mockReturnValue({ type: 'checkout.session.completed' });
|
|
71
|
+
const req = {
|
|
72
|
+
headers: { 'stripe-signature': 'v1,sig' },
|
|
73
|
+
rawBody: Buffer.from('{"id":"evt_1"}'),
|
|
74
|
+
};
|
|
75
|
+
await controller.webhook('stripe', req, res);
|
|
76
|
+
expect(mockParseWebhookEvent).toHaveBeenCalledWith('stripe', req.rawBody, 'v1,sig');
|
|
77
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
78
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
79
|
+
received: true,
|
|
80
|
+
provider: 'stripe',
|
|
81
|
+
type: 'checkout.session.completed',
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
it('uses body as string when rawBody absent and body is string', async () => {
|
|
85
|
+
mockParseWebhookEvent.mockReturnValue({ type: 'invoice.paid' });
|
|
86
|
+
const req = {
|
|
87
|
+
headers: { 'stripe-signature': 'sig' },
|
|
88
|
+
body: '{"id":"evt_2"}',
|
|
89
|
+
};
|
|
90
|
+
await controller.webhook('stripe', req, res);
|
|
91
|
+
expect(mockParseWebhookEvent).toHaveBeenCalledWith('stripe', '{"id":"evt_2"}', 'sig');
|
|
92
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
93
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
94
|
+
received: true,
|
|
95
|
+
provider: 'stripe',
|
|
96
|
+
type: 'invoice.paid',
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it('uses body as Buffer when rawBody absent and body is Buffer', async () => {
|
|
100
|
+
const buf = Buffer.from('{}');
|
|
101
|
+
mockParseWebhookEvent.mockReturnValue({});
|
|
102
|
+
const req = { headers: {}, body: buf };
|
|
103
|
+
await controller.webhook('paypal', req, res);
|
|
104
|
+
expect(mockParseWebhookEvent).toHaveBeenCalledWith('paypal', buf, '');
|
|
105
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
106
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
107
|
+
received: true,
|
|
108
|
+
provider: 'paypal',
|
|
109
|
+
type: undefined,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
it('returns 400 when parseWebhookEvent throws', async () => {
|
|
113
|
+
mockParseWebhookEvent.mockImplementation(() => {
|
|
114
|
+
throw new Error('Invalid signature');
|
|
115
|
+
});
|
|
116
|
+
const req = { headers: { 'stripe-signature': 'bad' }, rawBody: 'payload' };
|
|
117
|
+
await controller.webhook('stripe', req, res);
|
|
118
|
+
expect(res.status).toHaveBeenCalledWith(400);
|
|
119
|
+
expect(res.send).toHaveBeenCalledWith('Invalid signature');
|
|
120
|
+
});
|
|
121
|
+
it('uses paypal-transmission-sig header when stripe-signature absent', async () => {
|
|
122
|
+
mockParseWebhookEvent.mockReturnValue({ type: 'payment.capture.completed' });
|
|
123
|
+
const req = {
|
|
124
|
+
headers: { 'paypal-transmission-sig': 'paypal_sig' },
|
|
125
|
+
rawBody: 'payload',
|
|
126
|
+
};
|
|
127
|
+
await controller.webhook('paypal', req, res);
|
|
128
|
+
expect(mockParseWebhookEvent).toHaveBeenCalledWith('paypal', 'payload', 'paypal_sig');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PaymentProvider } from './providers/provider.interface';
|
|
2
|
+
import type { StripeProviderOptions } from './providers/stripe/stripe.types';
|
|
3
|
+
/**
|
|
4
|
+
* Options for PaymentModule.forRoot().
|
|
5
|
+
* Register providers via convenience keys (e.g. stripe) and/or pass pre-built provider instances.
|
|
6
|
+
*/
|
|
7
|
+
export interface PaymentModuleOptions {
|
|
8
|
+
/** Provider to use when none is specified (default: first registered). */
|
|
9
|
+
defaultProvider?: string;
|
|
10
|
+
/** Convenience: Stripe config; we create StripePaymentProvider. */
|
|
11
|
+
stripe?: StripeProviderOptions;
|
|
12
|
+
/** Additional or custom provider instances (merged with convenience providers). */
|
|
13
|
+
providers?: Record<string, PaymentProvider>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Payment module for Stripe, and other providers. Register with forRoot().
|
|
17
|
+
*/
|
|
18
|
+
export declare class PaymentModule {
|
|
19
|
+
static forRoot(options: PaymentModuleOptions): typeof PaymentModule;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=payment.module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment.module.d.ts","sourceRoot":"","sources":["../src/payment.module.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAE7E;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,0EAA0E;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAC7C;AAED;;GAEG;AACH,qBAKa,aAAa;IACxB,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,aAAa;CAapE"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var PaymentModule_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.PaymentModule = void 0;
|
|
11
|
+
const core_1 = require("@hazeljs/core");
|
|
12
|
+
const payment_controller_1 = require("./payment.controller");
|
|
13
|
+
const payment_service_1 = require("./payment.service");
|
|
14
|
+
const stripe_provider_1 = require("./providers/stripe/stripe.provider");
|
|
15
|
+
/**
|
|
16
|
+
* Payment module for Stripe, and other providers. Register with forRoot().
|
|
17
|
+
*/
|
|
18
|
+
let PaymentModule = PaymentModule_1 = class PaymentModule {
|
|
19
|
+
static forRoot(options) {
|
|
20
|
+
const providers = {
|
|
21
|
+
...(options.providers ?? {}),
|
|
22
|
+
};
|
|
23
|
+
if (options.stripe) {
|
|
24
|
+
providers[stripe_provider_1.STRIPE_PROVIDER_NAME] = new stripe_provider_1.StripePaymentProvider(options.stripe);
|
|
25
|
+
}
|
|
26
|
+
payment_service_1.PaymentService.configure({
|
|
27
|
+
defaultProvider: options.defaultProvider,
|
|
28
|
+
providers,
|
|
29
|
+
});
|
|
30
|
+
return PaymentModule_1;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
exports.PaymentModule = PaymentModule;
|
|
34
|
+
exports.PaymentModule = PaymentModule = PaymentModule_1 = __decorate([
|
|
35
|
+
(0, core_1.HazelModule)({
|
|
36
|
+
controllers: [payment_controller_1.PaymentController],
|
|
37
|
+
providers: [payment_service_1.PaymentService],
|
|
38
|
+
exports: [payment_service_1.PaymentService],
|
|
39
|
+
})
|
|
40
|
+
], PaymentModule);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment.module.test.d.ts","sourceRoot":"","sources":["../src/payment.module.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
jest.mock('stripe', () => jest.fn().mockImplementation(() => ({})));
|
|
4
|
+
const payment_module_1 = require("./payment.module");
|
|
5
|
+
const payment_service_1 = require("./payment.service");
|
|
6
|
+
const stripe_provider_1 = require("./providers/stripe/stripe.provider");
|
|
7
|
+
const mockProvider = {
|
|
8
|
+
name: 'mock',
|
|
9
|
+
createCheckoutSession: jest
|
|
10
|
+
.fn()
|
|
11
|
+
.mockResolvedValue({ sessionId: 'sess_1', url: 'https://pay.com' }),
|
|
12
|
+
createCustomer: jest
|
|
13
|
+
.fn()
|
|
14
|
+
.mockResolvedValue({ id: 'cus_1', email: 'a@b.com', name: null, metadata: {} }),
|
|
15
|
+
getCustomer: jest
|
|
16
|
+
.fn()
|
|
17
|
+
.mockResolvedValue({ id: 'cus_1', email: 'a@b.com', name: null, metadata: {} }),
|
|
18
|
+
listSubscriptions: jest.fn().mockResolvedValue({ data: [] }),
|
|
19
|
+
getCheckoutSession: jest.fn().mockResolvedValue({ id: 'sess_1', url: null }),
|
|
20
|
+
isWebhookConfigured: jest.fn().mockReturnValue(false),
|
|
21
|
+
parseWebhookEvent: jest.fn().mockReturnValue({}),
|
|
22
|
+
};
|
|
23
|
+
describe('PaymentModule', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
payment_service_1.PaymentService.config = null;
|
|
27
|
+
});
|
|
28
|
+
it('forRoot with stripe option registers StripePaymentProvider', () => {
|
|
29
|
+
const result = payment_module_1.PaymentModule.forRoot({
|
|
30
|
+
stripe: { secretKey: 'sk_test_xxx' },
|
|
31
|
+
});
|
|
32
|
+
expect(result).toBe(payment_module_1.PaymentModule);
|
|
33
|
+
const service = new payment_service_1.PaymentService();
|
|
34
|
+
expect(service.getProviderNames()).toContain(stripe_provider_1.STRIPE_PROVIDER_NAME);
|
|
35
|
+
});
|
|
36
|
+
it('forRoot with providers option registers custom providers', () => {
|
|
37
|
+
payment_module_1.PaymentModule.forRoot({
|
|
38
|
+
providers: { mock: mockProvider },
|
|
39
|
+
});
|
|
40
|
+
const service = new payment_service_1.PaymentService();
|
|
41
|
+
expect(service.getProviderNames()).toContain('mock');
|
|
42
|
+
expect(service.getProvider('mock').name).toBe('mock');
|
|
43
|
+
});
|
|
44
|
+
it('forRoot merges stripe and providers', () => {
|
|
45
|
+
payment_module_1.PaymentModule.forRoot({
|
|
46
|
+
stripe: { secretKey: 'sk_test_xxx' },
|
|
47
|
+
providers: { custom: mockProvider },
|
|
48
|
+
});
|
|
49
|
+
const service = new payment_service_1.PaymentService();
|
|
50
|
+
expect(service.getProviderNames()).toContain(stripe_provider_1.STRIPE_PROVIDER_NAME);
|
|
51
|
+
expect(service.getProviderNames()).toContain('custom');
|
|
52
|
+
});
|
|
53
|
+
it('forRoot with defaultProvider sets it', async () => {
|
|
54
|
+
payment_module_1.PaymentModule.forRoot({
|
|
55
|
+
defaultProvider: 'mock',
|
|
56
|
+
providers: { stripe: mockProvider, mock: mockProvider },
|
|
57
|
+
});
|
|
58
|
+
const service = new payment_service_1.PaymentService();
|
|
59
|
+
const result = await service.createCheckoutSession({
|
|
60
|
+
successUrl: 'https://a.com/s',
|
|
61
|
+
cancelUrl: 'https://a.com/c',
|
|
62
|
+
});
|
|
63
|
+
expect(result.sessionId).toBe('sess_1');
|
|
64
|
+
expect(mockProvider.createCheckoutSession).toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
it('forRoot with only providers (no stripe) works', () => {
|
|
67
|
+
payment_module_1.PaymentModule.forRoot({ providers: { mock: mockProvider } });
|
|
68
|
+
const service = new payment_service_1.PaymentService();
|
|
69
|
+
expect(service.getProviderNames()).toEqual(['mock']);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment service that delegates to registered providers (Stripe, PayPal, etc.).
|
|
3
|
+
*/
|
|
4
|
+
import type { PaymentProvider } from './providers/provider.interface';
|
|
5
|
+
import type { ListSubscriptionsResult } from './providers/provider.interface';
|
|
6
|
+
import type { CreateCheckoutSessionOptions, CreateCheckoutSessionResult, CreateCustomerOptions, Customer, CheckoutSessionInfo, SubscriptionStatusFilter } from './types/payment.types';
|
|
7
|
+
export interface PaymentServiceConfig {
|
|
8
|
+
defaultProvider?: string;
|
|
9
|
+
providers: Record<string, PaymentProvider>;
|
|
10
|
+
}
|
|
11
|
+
export declare class PaymentService {
|
|
12
|
+
private config;
|
|
13
|
+
constructor();
|
|
14
|
+
private static config;
|
|
15
|
+
static configure(config: PaymentServiceConfig): void;
|
|
16
|
+
private static getConfig;
|
|
17
|
+
private resolveProvider;
|
|
18
|
+
/** Get a provider by name for provider-specific APIs (e.g. Stripe client). */
|
|
19
|
+
getProvider<T extends PaymentProvider = PaymentProvider>(name: string): T;
|
|
20
|
+
/** List registered provider names. */
|
|
21
|
+
getProviderNames(): string[];
|
|
22
|
+
createCheckoutSession(options: CreateCheckoutSessionOptions, providerName?: string): Promise<CreateCheckoutSessionResult>;
|
|
23
|
+
createCustomer(options: CreateCustomerOptions, providerName?: string): Promise<Customer>;
|
|
24
|
+
getCustomer(customerId: string, providerName?: string): Promise<Customer | null>;
|
|
25
|
+
listSubscriptions(customerId: string, status?: SubscriptionStatusFilter, providerName?: string): Promise<ListSubscriptionsResult>;
|
|
26
|
+
getCheckoutSession(sessionId: string, providerName?: string): Promise<CheckoutSessionInfo>;
|
|
27
|
+
/**
|
|
28
|
+
* Verify and parse a webhook event. Use the provider name that received the webhook (e.g. 'stripe').
|
|
29
|
+
*/
|
|
30
|
+
parseWebhookEvent(providerName: string, payload: string | Buffer, signature: string): unknown;
|
|
31
|
+
isWebhookConfigured(providerName?: string): boolean;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=payment.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment.service.d.ts","sourceRoot":"","sources":["../src/payment.service.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,KAAK,EACV,4BAA4B,EAC5B,2BAA2B,EAC3B,qBAAqB,EACrB,QAAQ,EACR,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,oBAAoB;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAC5C;AAED,qBACa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAuB;;IAYrC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAqC;IAE1D,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,oBAAoB,GAAG,IAAI;IAIpD,OAAO,CAAC,MAAM,CAAC,SAAS;IASxB,OAAO,CAAC,eAAe;IAYvB,8EAA8E;IAC9E,WAAW,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC;IAUzE,sCAAsC;IACtC,gBAAgB,IAAI,MAAM,EAAE;IAItB,qBAAqB,CACzB,OAAO,EAAE,4BAA4B,EACrC,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,2BAA2B,CAAC;IAIjC,cAAc,CAAC,OAAO,EAAE,qBAAqB,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAIxF,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAIhF,iBAAiB,CACrB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,wBAAwB,EACjC,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,uBAAuB,CAAC;IAI7B,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAIhG;;OAEG;IACH,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAI7F,mBAAmB,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO;CAMpD"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Payment service that delegates to registered providers (Stripe, PayPal, etc.).
|
|
4
|
+
*/
|
|
5
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
6
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
7
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
8
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
};
|
|
11
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
12
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
13
|
+
};
|
|
14
|
+
var PaymentService_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.PaymentService = void 0;
|
|
17
|
+
const core_1 = require("@hazeljs/core");
|
|
18
|
+
let PaymentService = PaymentService_1 = class PaymentService {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.config = PaymentService_1.getConfig();
|
|
21
|
+
const names = Object.keys(this.config.providers);
|
|
22
|
+
if (names.length === 0) {
|
|
23
|
+
throw new Error('No payment providers registered. Call PaymentModule.forRoot({ providers: { stripe: new StripePaymentProvider(...) } }).');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
static configure(config) {
|
|
27
|
+
PaymentService_1.config = config;
|
|
28
|
+
}
|
|
29
|
+
static getConfig() {
|
|
30
|
+
if (!PaymentService_1.config) {
|
|
31
|
+
throw new Error('PaymentModule not configured. Call PaymentModule.forRoot({ providers: { ... } }) in your app module.');
|
|
32
|
+
}
|
|
33
|
+
return PaymentService_1.config;
|
|
34
|
+
}
|
|
35
|
+
resolveProvider(providerName) {
|
|
36
|
+
const name = providerName ?? this.config.defaultProvider ?? Object.keys(this.config.providers)[0];
|
|
37
|
+
const provider = this.config.providers[name];
|
|
38
|
+
if (!provider) {
|
|
39
|
+
throw new Error(`Payment provider "${name}" not found. Registered: ${Object.keys(this.config.providers).join(', ')}.`);
|
|
40
|
+
}
|
|
41
|
+
return provider;
|
|
42
|
+
}
|
|
43
|
+
/** Get a provider by name for provider-specific APIs (e.g. Stripe client). */
|
|
44
|
+
getProvider(name) {
|
|
45
|
+
const provider = this.config.providers[name];
|
|
46
|
+
if (!provider) {
|
|
47
|
+
throw new Error(`Payment provider "${name}" not found. Registered: ${Object.keys(this.config.providers).join(', ')}.`);
|
|
48
|
+
}
|
|
49
|
+
return provider;
|
|
50
|
+
}
|
|
51
|
+
/** List registered provider names. */
|
|
52
|
+
getProviderNames() {
|
|
53
|
+
return Object.keys(this.config.providers);
|
|
54
|
+
}
|
|
55
|
+
async createCheckoutSession(options, providerName) {
|
|
56
|
+
return this.resolveProvider(providerName).createCheckoutSession(options);
|
|
57
|
+
}
|
|
58
|
+
async createCustomer(options, providerName) {
|
|
59
|
+
return this.resolveProvider(providerName).createCustomer(options);
|
|
60
|
+
}
|
|
61
|
+
async getCustomer(customerId, providerName) {
|
|
62
|
+
return this.resolveProvider(providerName).getCustomer(customerId);
|
|
63
|
+
}
|
|
64
|
+
async listSubscriptions(customerId, status, providerName) {
|
|
65
|
+
return this.resolveProvider(providerName).listSubscriptions(customerId, status);
|
|
66
|
+
}
|
|
67
|
+
async getCheckoutSession(sessionId, providerName) {
|
|
68
|
+
return this.resolveProvider(providerName).getCheckoutSession(sessionId);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Verify and parse a webhook event. Use the provider name that received the webhook (e.g. 'stripe').
|
|
72
|
+
*/
|
|
73
|
+
parseWebhookEvent(providerName, payload, signature) {
|
|
74
|
+
return this.getProvider(providerName).parseWebhookEvent(payload, signature);
|
|
75
|
+
}
|
|
76
|
+
isWebhookConfigured(providerName) {
|
|
77
|
+
if (providerName) {
|
|
78
|
+
return this.getProvider(providerName).isWebhookConfigured();
|
|
79
|
+
}
|
|
80
|
+
return Object.values(this.config.providers).some((p) => p.isWebhookConfigured());
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
exports.PaymentService = PaymentService;
|
|
84
|
+
PaymentService.config = null;
|
|
85
|
+
exports.PaymentService = PaymentService = PaymentService_1 = __decorate([
|
|
86
|
+
(0, core_1.Service)(),
|
|
87
|
+
__metadata("design:paramtypes", [])
|
|
88
|
+
], PaymentService);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment.service.test.d.ts","sourceRoot":"","sources":["../src/payment.service.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const payment_service_1 = require("./payment.service");
|
|
4
|
+
function createMockProvider(name) {
|
|
5
|
+
return {
|
|
6
|
+
name,
|
|
7
|
+
createCheckoutSession: jest
|
|
8
|
+
.fn()
|
|
9
|
+
.mockResolvedValue({ sessionId: `sess_${name}`, url: `https://pay.${name}.com/checkout` }),
|
|
10
|
+
createCustomer: jest.fn().mockResolvedValue({
|
|
11
|
+
id: `cus_${name}`,
|
|
12
|
+
email: 'test@example.com',
|
|
13
|
+
name: null,
|
|
14
|
+
metadata: {},
|
|
15
|
+
}),
|
|
16
|
+
getCustomer: jest.fn().mockResolvedValue({
|
|
17
|
+
id: `cus_${name}`,
|
|
18
|
+
email: 'test@example.com',
|
|
19
|
+
name: null,
|
|
20
|
+
metadata: {},
|
|
21
|
+
}),
|
|
22
|
+
listSubscriptions: jest
|
|
23
|
+
.fn()
|
|
24
|
+
.mockResolvedValue({ data: [{ id: 'sub_1', status: 'active', customerId: `cus_${name}` }] }),
|
|
25
|
+
getCheckoutSession: jest.fn().mockResolvedValue({
|
|
26
|
+
id: `sess_${name}`,
|
|
27
|
+
url: null,
|
|
28
|
+
customerId: `cus_${name}`,
|
|
29
|
+
status: 'complete',
|
|
30
|
+
}),
|
|
31
|
+
isWebhookConfigured: jest.fn().mockReturnValue(true),
|
|
32
|
+
parseWebhookEvent: jest.fn().mockReturnValue({ type: 'checkout.session.completed' }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
describe('PaymentService', () => {
|
|
36
|
+
const mockStripe = createMockProvider('stripe');
|
|
37
|
+
const mockPaypal = createMockProvider('paypal');
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
jest.clearAllMocks();
|
|
40
|
+
payment_service_1.PaymentService.configure({
|
|
41
|
+
providers: { stripe: mockStripe, paypal: mockPaypal },
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
payment_service_1.PaymentService.configure({ providers: {} });
|
|
46
|
+
});
|
|
47
|
+
describe('constructor', () => {
|
|
48
|
+
it('uses first provider as default when no defaultProvider set', async () => {
|
|
49
|
+
const service = new payment_service_1.PaymentService();
|
|
50
|
+
const result = await service.createCheckoutSession({
|
|
51
|
+
successUrl: 'https://a.com/s',
|
|
52
|
+
cancelUrl: 'https://a.com/c',
|
|
53
|
+
});
|
|
54
|
+
expect(result.sessionId).toBe('sess_stripe');
|
|
55
|
+
expect(mockStripe.createCheckoutSession).toHaveBeenCalled();
|
|
56
|
+
expect(mockPaypal.createCheckoutSession).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
it('throws when no providers registered', () => {
|
|
59
|
+
payment_service_1.PaymentService.configure({ providers: {} });
|
|
60
|
+
expect(() => new payment_service_1.PaymentService()).toThrow('No payment providers registered');
|
|
61
|
+
});
|
|
62
|
+
it('throws when module not configured', () => {
|
|
63
|
+
payment_service_1.PaymentService.configure({ providers: {} });
|
|
64
|
+
payment_service_1.PaymentService.config = null;
|
|
65
|
+
expect(() => new payment_service_1.PaymentService()).toThrow('PaymentModule not configured');
|
|
66
|
+
payment_service_1.PaymentService.configure({ providers: { stripe: mockStripe } });
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('defaultProvider', () => {
|
|
70
|
+
it('uses defaultProvider when set', async () => {
|
|
71
|
+
payment_service_1.PaymentService.configure({
|
|
72
|
+
defaultProvider: 'paypal',
|
|
73
|
+
providers: { stripe: mockStripe, paypal: mockPaypal },
|
|
74
|
+
});
|
|
75
|
+
const service = new payment_service_1.PaymentService();
|
|
76
|
+
const result = await service.createCheckoutSession({
|
|
77
|
+
successUrl: 'https://a.com/s',
|
|
78
|
+
cancelUrl: 'https://a.com/c',
|
|
79
|
+
});
|
|
80
|
+
expect(result.sessionId).toBe('sess_paypal');
|
|
81
|
+
expect(mockPaypal.createCheckoutSession).toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
it('throws when defaultProvider is not in providers', async () => {
|
|
84
|
+
payment_service_1.PaymentService.configure({
|
|
85
|
+
defaultProvider: 'nonexistent',
|
|
86
|
+
providers: { stripe: mockStripe },
|
|
87
|
+
});
|
|
88
|
+
const service = new payment_service_1.PaymentService();
|
|
89
|
+
await expect(service.createCheckoutSession({
|
|
90
|
+
successUrl: 'https://a.com/s',
|
|
91
|
+
cancelUrl: 'https://a.com/c',
|
|
92
|
+
})).rejects.toThrow('Payment provider "nonexistent" not found');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('getProviderNames', () => {
|
|
96
|
+
it('returns registered provider names', () => {
|
|
97
|
+
const service = new payment_service_1.PaymentService();
|
|
98
|
+
expect(service.getProviderNames()).toEqual(['stripe', 'paypal']);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('getProvider', () => {
|
|
102
|
+
it('returns provider by name', () => {
|
|
103
|
+
const service = new payment_service_1.PaymentService();
|
|
104
|
+
const p = service.getProvider('paypal');
|
|
105
|
+
expect(p.name).toBe('paypal');
|
|
106
|
+
});
|
|
107
|
+
it('throws when provider not found', () => {
|
|
108
|
+
const service = new payment_service_1.PaymentService();
|
|
109
|
+
expect(() => service.getProvider('unknown')).toThrow('Payment provider "unknown" not found');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('createCheckoutSession', () => {
|
|
113
|
+
it('delegates to default provider without providerName', async () => {
|
|
114
|
+
const service = new payment_service_1.PaymentService();
|
|
115
|
+
const options = {
|
|
116
|
+
successUrl: 'https://a.com/s',
|
|
117
|
+
cancelUrl: 'https://a.com/c',
|
|
118
|
+
customerEmail: 'u@example.com',
|
|
119
|
+
};
|
|
120
|
+
await service.createCheckoutSession(options);
|
|
121
|
+
expect(mockStripe.createCheckoutSession).toHaveBeenCalledWith(options);
|
|
122
|
+
});
|
|
123
|
+
it('delegates to named provider when providerName given', async () => {
|
|
124
|
+
const service = new payment_service_1.PaymentService();
|
|
125
|
+
const options = {
|
|
126
|
+
successUrl: 'https://a.com/s',
|
|
127
|
+
cancelUrl: 'https://a.com/c',
|
|
128
|
+
};
|
|
129
|
+
await service.createCheckoutSession(options, 'paypal');
|
|
130
|
+
expect(mockPaypal.createCheckoutSession).toHaveBeenCalledWith(options);
|
|
131
|
+
});
|
|
132
|
+
it('throws when named provider does not exist', async () => {
|
|
133
|
+
const service = new payment_service_1.PaymentService();
|
|
134
|
+
await expect(service.createCheckoutSession({ successUrl: 'https://a.com/s', cancelUrl: 'https://a.com/c' }, 'nonexistent')).rejects.toThrow('Payment provider "nonexistent" not found');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
describe('createCustomer', () => {
|
|
138
|
+
it('delegates to default provider', async () => {
|
|
139
|
+
const service = new payment_service_1.PaymentService();
|
|
140
|
+
const options = { email: 'new@example.com', name: 'Alice' };
|
|
141
|
+
const customer = await service.createCustomer(options);
|
|
142
|
+
expect(customer.id).toBe('cus_stripe');
|
|
143
|
+
expect(mockStripe.createCustomer).toHaveBeenCalledWith(options);
|
|
144
|
+
});
|
|
145
|
+
it('delegates to named provider', async () => {
|
|
146
|
+
const service = new payment_service_1.PaymentService();
|
|
147
|
+
await service.createCustomer({ email: 'b@example.com' }, 'paypal');
|
|
148
|
+
expect(mockPaypal.createCustomer).toHaveBeenCalledWith({ email: 'b@example.com' });
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe('getCustomer', () => {
|
|
152
|
+
it('delegates to default provider', async () => {
|
|
153
|
+
const service = new payment_service_1.PaymentService();
|
|
154
|
+
const customer = await service.getCustomer('cus_123');
|
|
155
|
+
expect(customer).not.toBeNull();
|
|
156
|
+
expect(mockStripe.getCustomer).toHaveBeenCalledWith('cus_123');
|
|
157
|
+
});
|
|
158
|
+
it('delegates to named provider', async () => {
|
|
159
|
+
const service = new payment_service_1.PaymentService();
|
|
160
|
+
await service.getCustomer('cus_456', 'paypal');
|
|
161
|
+
expect(mockPaypal.getCustomer).toHaveBeenCalledWith('cus_456');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe('listSubscriptions', () => {
|
|
165
|
+
it('delegates to default provider', async () => {
|
|
166
|
+
const service = new payment_service_1.PaymentService();
|
|
167
|
+
const result = await service.listSubscriptions('cus_1');
|
|
168
|
+
expect(result.data).toHaveLength(1);
|
|
169
|
+
expect(mockStripe.listSubscriptions).toHaveBeenCalledWith('cus_1', undefined);
|
|
170
|
+
});
|
|
171
|
+
it('passes status when provided', async () => {
|
|
172
|
+
const service = new payment_service_1.PaymentService();
|
|
173
|
+
await service.listSubscriptions('cus_1', 'active', 'stripe');
|
|
174
|
+
expect(mockStripe.listSubscriptions).toHaveBeenCalledWith('cus_1', 'active');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
describe('getCheckoutSession', () => {
|
|
178
|
+
it('delegates to default provider', async () => {
|
|
179
|
+
const service = new payment_service_1.PaymentService();
|
|
180
|
+
const session = await service.getCheckoutSession('sess_abc');
|
|
181
|
+
expect(session.id).toBe('sess_stripe');
|
|
182
|
+
expect(mockStripe.getCheckoutSession).toHaveBeenCalledWith('sess_abc');
|
|
183
|
+
});
|
|
184
|
+
it('delegates to named provider', async () => {
|
|
185
|
+
const service = new payment_service_1.PaymentService();
|
|
186
|
+
await service.getCheckoutSession('sess_xyz', 'paypal');
|
|
187
|
+
expect(mockPaypal.getCheckoutSession).toHaveBeenCalledWith('sess_xyz');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe('parseWebhookEvent', () => {
|
|
191
|
+
it('delegates to named provider', () => {
|
|
192
|
+
const service = new payment_service_1.PaymentService();
|
|
193
|
+
const event = service.parseWebhookEvent('stripe', 'payload', 'sig');
|
|
194
|
+
expect(event).toEqual({ type: 'checkout.session.completed' });
|
|
195
|
+
expect(mockStripe.parseWebhookEvent).toHaveBeenCalledWith('payload', 'sig');
|
|
196
|
+
});
|
|
197
|
+
it('throws when provider not found', () => {
|
|
198
|
+
const service = new payment_service_1.PaymentService();
|
|
199
|
+
expect(() => service.parseWebhookEvent('unknown', 'p', 's')).toThrow('Payment provider "unknown" not found');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe('isWebhookConfigured', () => {
|
|
203
|
+
it('returns true for named provider when configured', () => {
|
|
204
|
+
const service = new payment_service_1.PaymentService();
|
|
205
|
+
expect(service.isWebhookConfigured('stripe')).toBe(true);
|
|
206
|
+
expect(mockStripe.isWebhookConfigured).toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
it('returns true when any provider has webhook configured', () => {
|
|
209
|
+
const service = new payment_service_1.PaymentService();
|
|
210
|
+
expect(service.isWebhookConfigured()).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
it('returns false when no provider has webhook configured', () => {
|
|
213
|
+
mockStripe.isWebhookConfigured.mockReturnValue(false);
|
|
214
|
+
mockPaypal.isWebhookConfigured.mockReturnValue(false);
|
|
215
|
+
const service = new payment_service_1.PaymentService();
|
|
216
|
+
expect(service.isWebhookConfigured()).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment providers. Add new providers here and register in PaymentModule.
|
|
3
|
+
*/
|
|
4
|
+
export type { PaymentProvider } from './provider.interface';
|
|
5
|
+
export type { ListSubscriptionsResult } from './provider.interface';
|
|
6
|
+
export { StripePaymentProvider, STRIPE_PROVIDER_NAME } from './stripe/stripe.provider';
|
|
7
|
+
export type { StripeProviderOptions, StripeWebhookEvent } from './stripe/stripe.types';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,YAAY,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAEpE,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACvF,YAAY,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Payment providers. Add new providers here and register in PaymentModule.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.STRIPE_PROVIDER_NAME = exports.StripePaymentProvider = void 0;
|
|
7
|
+
var stripe_provider_1 = require("./stripe/stripe.provider");
|
|
8
|
+
Object.defineProperty(exports, "StripePaymentProvider", { enumerable: true, get: function () { return stripe_provider_1.StripePaymentProvider; } });
|
|
9
|
+
Object.defineProperty(exports, "STRIPE_PROVIDER_NAME", { enumerable: true, get: function () { return stripe_provider_1.STRIPE_PROVIDER_NAME; } });
|