Skip to content

Commit 55c024a

Browse files
refactor(billing): extract shared Stripe client, relax URL check in dev
Closes #3263, closes #3264
1 parent 0fe2056 commit 55c024a

5 files changed

Lines changed: 36 additions & 60 deletions

File tree

modules/billing/controllers/billing.webhook.controller.js

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,10 @@
11
/**
22
* Module dependencies
33
*/
4-
import Stripe from 'stripe';
5-
64
import config from '../../../config/index.js';
5+
import getStripe from '../lib/stripe.js';
76
import BillingWebhookService from '../services/billing.webhook.service.js';
87

9-
/**
10-
* Lazily instantiated Stripe client
11-
*/
12-
let stripeClient = null;
13-
14-
/**
15-
* @desc Get or create the Stripe client instance
16-
* @returns {Object|null} Stripe client or null if not configured
17-
*/
18-
const getStripe = () => {
19-
if (stripeClient) return stripeClient;
20-
if (!config.stripe?.secretKey) return null;
21-
stripeClient = new Stripe(config.stripe.secretKey);
22-
return stripeClient;
23-
};
24-
258
/**
269
* @desc Endpoint to handle Stripe webhook events
2710
* @param {Object} req - Express request object

modules/billing/lib/stripe.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Module dependencies
3+
*/
4+
import Stripe from 'stripe';
5+
6+
import config from '../../../config/index.js';
7+
8+
/**
9+
* Lazily instantiated Stripe client
10+
*/
11+
let stripeClient = null;
12+
13+
/**
14+
* @desc Get or create the Stripe client instance
15+
* @returns {Object|null} Stripe client or null if not configured
16+
*/
17+
const getStripe = () => {
18+
if (stripeClient) return stripeClient;
19+
if (!config.stripe?.secretKey) return null;
20+
stripeClient = new Stripe(config.stripe.secretKey);
21+
return stripeClient;
22+
};
23+
24+
export default getStripe;

modules/billing/services/billing.plans.service.js

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
/**
22
* Module dependencies
33
*/
4-
import Stripe from 'stripe';
5-
6-
import config from '../../../config/index.js';
4+
import getStripe from '../lib/stripe.js';
75

86
/**
97
* In-memory cache for plans
@@ -24,22 +22,6 @@ const DEFAULT_FREE_PLAN = {
2422
stripePriceAnnual: null,
2523
};
2624

27-
/**
28-
* Lazily instantiated Stripe client
29-
*/
30-
let stripeClient = null;
31-
32-
/**
33-
* @desc Get or create the Stripe client instance
34-
* @returns {Object|null} Stripe client or null if not configured
35-
*/
36-
const getStripe = () => {
37-
if (stripeClient) return stripeClient;
38-
if (!config.stripe?.secretKey) return null;
39-
stripeClient = new Stripe(config.stripe.secretKey);
40-
return stripeClient;
41-
};
42-
4325
/**
4426
* @desc Fetch plans from Stripe and map to normalized format
4527
* @param {Object} stripe - Stripe client instance

modules/billing/services/billing.service.js

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,11 @@
11
/**
22
* Module dependencies
33
*/
4-
import Stripe from 'stripe';
5-
64
import config from '../../../config/index.js';
5+
import getStripe from '../lib/stripe.js';
76
import BillingPlansService from './billing.plans.service.js';
87
import SubscriptionRepository from '../repositories/billing.subscription.repository.js';
98

10-
/**
11-
* Lazily instantiated Stripe client
12-
*/
13-
let stripeClient = null;
14-
15-
/**
16-
* @desc Get or create the Stripe client instance
17-
* @returns {Object|null} Stripe client or null if not configured
18-
*/
19-
const getStripe = () => {
20-
if (stripeClient) return stripeClient;
21-
if (!config.stripe?.secretKey) return null;
22-
stripeClient = new Stripe(config.stripe.secretKey);
23-
return stripeClient;
24-
};
25-
269
/**
2710
* @desc Validate that a URL uses https (or http in dev/test) and belongs to the app domain
2811
* @param {String} url - URL to validate
@@ -34,7 +17,7 @@ const isAllowedUrl = (url) => {
3417
const env = process.env.NODE_ENV || 'development';
3518
const allowHttp = env === 'development' || env === 'test';
3619
if (!allowHttp && parsed.protocol !== 'https:') return false;
37-
if (config.domain) {
20+
if (config.domain && !allowHttp) {
3821
const configHost = new URL(config.domain.startsWith('http') ? config.domain : `https://${config.domain}`).hostname;
3922
if (parsed.hostname !== configHost) return false;
4023
}

modules/billing/tests/billing.checkout.unit.tests.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,21 @@ describe('Billing service unit tests:', () => {
105105
).rejects.toThrow('Invalid redirect URL');
106106
});
107107

108-
test('should reject URL with wrong domain when config.domain is set', async () => {
108+
test('should allow any domain in dev/test even when config.domain is set', async () => {
109109
jest.unstable_mockModule('../../../config/index.js', () => ({
110110
default: { stripe: { secretKey: 'sk_test_domain' }, domain: 'https://myapp.com' },
111111
}));
112112

113+
mockSubscriptionRepository.findByOrganization.mockResolvedValue({
114+
stripeCustomerId: 'cus_existing',
115+
});
116+
113117
const mod = await import('../services/billing.service.js');
114118
BillingService = mod.default;
115119

116-
await expect(
117-
BillingService.createCheckout(mockOrganization, 'price_starter_m', 'http://evil.com/success', 'http://myapp.com/cancel'),
118-
).rejects.toThrow('Invalid redirect URL');
120+
const url = await BillingService.createCheckout(mockOrganization, 'price_starter_m', 'http://evil.com/success', 'http://myapp.com/cancel');
121+
122+
expect(url).toBe('https://checkout.stripe.com/session_123');
119123
});
120124

121125
test('should throw when priceId is not in allowed plans', async () => {

0 commit comments

Comments
 (0)