Skip to content

Commit 32ce64d

Browse files
fix(billing): use active/trialing status allowlist in requireQuota middleware
1 parent 0c2efb2 commit 32ce64d

2 files changed

Lines changed: 55 additions & 2 deletions

File tree

modules/billing/middlewares/billing.requireQuota.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ function requireQuota(resource, action) {
3737
}
3838

3939
try {
40-
// Determine current plan — default to free when no subscription or past_due
40+
// Determine current plan — default to free when subscription is missing or inactive
4141
const subscription = await SubscriptionRepository.findByOrganization(req.organization._id);
42-
const plan = (!subscription || subscription.status === 'past_due') ? 'free' : (subscription.plan || 'free');
42+
const activeStatuses = ['active', 'trialing'];
43+
const plan = (!subscription || !activeStatuses.includes(subscription.status)) ? 'free' : (subscription.plan || 'free');
4344

4445
// Look up quota limit from config
4546
const quotas = config.billing?.quotas;

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,58 @@ describe('requireQuota middleware:', () => {
173173
expect(next).toHaveBeenCalled();
174174
});
175175

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+
218+
test('should use subscription plan when status is trialing', async () => {
219+
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'starter', status: 'trialing' });
220+
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.create': 15 } });
221+
222+
await requireQuota('scraps', 'create')(req, res, next);
223+
224+
expect(next).toHaveBeenCalled();
225+
expect(res.status).not.toHaveBeenCalled();
226+
});
227+
176228
test('should treat zero usage as under quota', async () => {
177229
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'free', status: 'active' });
178230
mockBillingUsageService.get.mockResolvedValue({ counters: {} });

0 commit comments

Comments
 (0)