Skip to content

Commit 81b4a39

Browse files
fix(billing): extract activeStatuses constant and use test.each for inactive statuses
- Hoist activeStatuses to shared lib/constants.js to avoid duplication between requireQuota middleware and billing controller - Collapse individual inactive-status test cases into test.each table for easier maintenance and extensibility
1 parent 32ce64d commit 81b4a39

4 files changed

Lines changed: 23 additions & 57 deletions

File tree

modules/billing/controllers/billing.controller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* Module dependencies
33
*/
4+
import { activeStatuses } from '../lib/constants.js';
45
import config from '../../../config/index.js';
56
import responses from '../../../lib/helpers/responses.js';
67
import BillingService from '../services/billing.service.js';
@@ -67,7 +68,6 @@ const getUsage = async (req, res) => {
6768
try {
6869
// Determine current plan via service layer
6970
const subscription = await BillingService.getSubscription(req.organization._id);
70-
const activeStatuses = ['active', 'trialing'];
7171
const plan = (!subscription || !activeStatuses.includes(subscription.status)) ? 'free' : (subscription.plan || 'free');
7272

7373
// Get usage counters (includes month field)

modules/billing/lib/constants.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Stripe subscription statuses that indicate an active subscription.
3+
* Any status not in this list is treated as inactive (falls back to free plan).
4+
*/
5+
export const activeStatuses = ['active', 'trialing'];

modules/billing/middlewares/billing.requireQuota.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import SubscriptionRepository from '../repositories/billing.subscription.repository.js';
55
import BillingUsageService from '../services/billing.usage.service.js';
66

7+
import { activeStatuses } from '../lib/constants.js';
78
import config from '../../../config/index.js';
89
import responses from '../../../lib/helpers/responses.js';
910

@@ -39,7 +40,6 @@ function requireQuota(resource, action) {
3940
try {
4041
// Determine current plan — default to free when subscription is missing or inactive
4142
const subscription = await SubscriptionRepository.findByOrganization(req.organization._id);
42-
const activeStatuses = ['active', 'trialing'];
4343
const plan = (!subscription || !activeStatuses.includes(subscription.status)) ? 'free' : (subscription.plan || 'free');
4444

4545
// Look up quota limit from config

modules/billing/tests/billing.quota.unit.tests.js

Lines changed: 16 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -117,19 +117,22 @@ describe('requireQuota middleware:', () => {
117117
}));
118118
});
119119

120-
test('should treat past_due subscription as free plan', async () => {
121-
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'starter', status: 'past_due' });
122-
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.create': 3 } });
123-
124-
await requireQuota('scraps', 'create')(req, res, next);
125-
126-
expect(next).not.toHaveBeenCalled();
127-
expect(res.status).toHaveBeenCalledWith(429);
128-
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
129-
type: 'error',
130-
code: 429,
131-
}));
132-
});
120+
test.each(['past_due', 'canceled', 'unpaid', 'incomplete', 'incomplete_expired', 'paused'])(
121+
'should treat %s subscription as free plan',
122+
async (status) => {
123+
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'starter', status });
124+
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.create': 3 } });
125+
126+
await requireQuota('scraps', 'create')(req, res, next);
127+
128+
expect(next).not.toHaveBeenCalled();
129+
expect(res.status).toHaveBeenCalledWith(429);
130+
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
131+
type: 'error',
132+
code: 429,
133+
}));
134+
},
135+
);
133136

134137
test('should allow unlimited (Infinity) without checking usage', async () => {
135138
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'pro', status: 'active' });
@@ -173,48 +176,6 @@ describe('requireQuota middleware:', () => {
173176
expect(next).toHaveBeenCalled();
174177
});
175178

176-
test('should treat canceled subscription as free plan', async () => {
177-
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'starter', status: 'canceled' });
178-
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.create': 3 } });
179-
180-
await requireQuota('scraps', 'create')(req, res, next);
181-
182-
expect(next).not.toHaveBeenCalled();
183-
expect(res.status).toHaveBeenCalledWith(429);
184-
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
185-
type: 'error',
186-
code: 429,
187-
}));
188-
});
189-
190-
test('should treat unpaid subscription as free plan', async () => {
191-
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'starter', status: 'unpaid' });
192-
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.create': 3 } });
193-
194-
await requireQuota('scraps', 'create')(req, res, next);
195-
196-
expect(next).not.toHaveBeenCalled();
197-
expect(res.status).toHaveBeenCalledWith(429);
198-
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
199-
type: 'error',
200-
code: 429,
201-
}));
202-
});
203-
204-
test('should treat incomplete subscription as free plan', async () => {
205-
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'starter', status: 'incomplete' });
206-
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.create': 3 } });
207-
208-
await requireQuota('scraps', 'create')(req, res, next);
209-
210-
expect(next).not.toHaveBeenCalled();
211-
expect(res.status).toHaveBeenCalledWith(429);
212-
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
213-
type: 'error',
214-
code: 429,
215-
}));
216-
});
217-
218179
test('should use subscription plan when status is trialing', async () => {
219180
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'starter', status: 'trialing' });
220181
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.create': 15 } });

0 commit comments

Comments
 (0)