Skip to content

Commit 1b1001a

Browse files
fix(billing): warn on getUsage meterQuota fallback when live plan config missing
Addresses review: getUsage silently fell back to the DB snapshot/0 when getActivePlan returned null, masking plan/config mismatches. getUsage is a read-only display endpoint (not a gate — billing.requireQuota already fails safe with 503 on the work path), so it degrades gracefully but now emits a warn log for ops visibility. Comment updated to document the fallback.
1 parent 1811d75 commit 1b1001a

2 files changed

Lines changed: 23 additions & 3 deletions

File tree

modules/billing/controllers/billing.controller.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,21 @@ const getUsage = async (req, res) => {
9494
const extrasRemaining = await BillingExtraService.getOrgBalanceContext(req.organization._id.toString());
9595
const packsAvailable = config.billing?.packs ?? [];
9696

97-
// Derive meterQuota from the live plan config — DB snapshot is stale after a plan upgrade
98-
// (free → growth) until the next incrementMeter call. Live config is authoritative.
97+
// Derive meterQuota from the live plan config — the DB snapshot is stale after a
98+
// plan upgrade (free → growth) until the next incrementMeter call, so live config
99+
// is authoritative when present. When it is absent (legacy plan dropped from config,
100+
// or seeding / version bump in progress) this is a read-only display endpoint, not a
101+
// gate: billing.requireQuota already fails safe with 503 on the work path, so here we
102+
// degrade gracefully to the DB snapshot (or 0) and warn for ops visibility rather than
103+
// failing the usage page.
99104
const livePlan = BillingPlanService.getActivePlan(plan);
105+
if (!livePlan) {
106+
logger.warn('[billing.getUsage] no live plan definition for planId — falling back to DB snapshot quota', {
107+
planId: plan,
108+
organizationId: req.organization._id.toString(),
109+
snapshotQuota: meter?.meterQuota ?? null,
110+
});
111+
}
100112
const liveQuota = livePlan?.meterQuota ?? meter?.meterQuota ?? 0;
101113

102114
return responses.success(res, 'billing usage')({

modules/billing/tests/billing.usage.endpoint.unit.tests.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ describe('Billing usage endpoint unit tests:', () => {
240240
describe('meterMode — meterQuota live override', () => {
241241
let mockBillingPlanService;
242242
let mockMeterUsageService;
243+
let mockLogger;
243244

244245
beforeEach(async () => {
245246
jest.resetModules();
@@ -274,8 +275,9 @@ describe('Billing usage endpoint unit tests:', () => {
274275
default: mockBillingPlanService,
275276
}));
276277

278+
mockLogger = { info: jest.fn(), error: jest.fn(), warn: jest.fn() };
277279
jest.unstable_mockModule('../../../lib/services/logger.js', () => ({
278-
default: { info: jest.fn(), error: jest.fn(), warn: jest.fn() },
280+
default: mockLogger,
279281
}));
280282
jest.unstable_mockModule('../lib/events.js', () => ({
281283
default: { emit: jest.fn() },
@@ -321,6 +323,7 @@ describe('Billing usage endpoint unit tests:', () => {
321323
expect(payload.meterQuota).toBe(1600); // live plan config, not stale DB snapshot
322324
expect(payload.meterUsed).toBe(46);
323325
expect(payload.plan).toBe('growth');
326+
expect(mockLogger.warn).not.toHaveBeenCalled(); // live plan present — no fallback warn
324327
});
325328

326329
test('falls back to DB snapshot quota when live plan config returns null (unknown plan)', async () => {
@@ -341,6 +344,11 @@ describe('Billing usage endpoint unit tests:', () => {
341344
expect(res.status).toHaveBeenCalledWith(200);
342345
const payload = res.json.mock.calls[0][0].data;
343346
expect(payload.meterQuota).toBe(50); // falls back to DB snapshot
347+
// Fallback path masks a plan/config mismatch — warn for ops visibility.
348+
expect(mockLogger.warn).toHaveBeenCalledWith(
349+
expect.stringContaining('no live plan definition'),
350+
expect.objectContaining({ planId: 'legacy', snapshotQuota: 50 }),
351+
);
344352
});
345353

346354
test('returns 0 meterQuota when no DB snapshot and no live config plan', async () => {

0 commit comments

Comments
 (0)