From eb351466ae5467f2e58c6962100f4304206541ae Mon Sep 17 00:00:00 2001 From: Kuchizu Date: Mon, 22 Jun 2026 20:16:28 +0300 Subject: [PATCH 1/2] fix(limiter): handle workspace without tariffPlanId --- workers/limiter/src/dbHelper.ts | 7 ++++ workers/limiter/tests/dbHelper.test.ts | 54 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/workers/limiter/src/dbHelper.ts b/workers/limiter/src/dbHelper.ts index b4cc845f..a93e0019 100644 --- a/workers/limiter/src/dbHelper.ts +++ b/workers/limiter/src/dbHelper.ts @@ -203,6 +203,13 @@ export class DbHelper { * @param planId - id of the plan to find */ private async resolvePlan(planId: WorkspaceDBScheme['tariffPlanId']): Promise { + /** + * Workspace may have no tariff plan assigned + */ + if (!planId) { + return null; + } + let plan = this.findPlanById(planId); if (plan) { diff --git a/workers/limiter/tests/dbHelper.test.ts b/workers/limiter/tests/dbHelper.test.ts index 3e8eba54..f57be08d 100644 --- a/workers/limiter/tests/dbHelper.test.ts +++ b/workers/limiter/tests/dbHelper.test.ts @@ -363,6 +363,60 @@ describe('DbHelper', () => { ).rejects.toThrow(NonCriticalError); }); + test('Should skip and report a workspace that has no tariffPlanId', async () => { + /** + * Arrange — a workspace document with a missing tariffPlanId + */ + const workspace = createWorkspaceMock({ + plan: mockedPlans.eventsLimit10, + billingPeriodEventsCount: 0, + lastChargeDate: new Date(), + }); + + delete (workspace as Partial).tariffPlanId; + + await workspaceCollection.insertOne(workspace); + + const hawkCatcherSpy = jest.spyOn(HawkCatcher, 'send').mockImplementation(() => undefined); + + /** + * Act + */ + const workspaces = []; + + for await (const resolved of dbHelper.getWorkspacesWithTariffPlans()) { + workspaces.push(resolved); + } + + /** + * Assert — no crash, the workspace is skipped and reported + */ + expect(workspaces).toHaveLength(0); + expect(hawkCatcherSpy).toHaveBeenCalledTimes(1); + }); + + test('Should throw NonCriticalError when a single workspace has no tariffPlanId', async () => { + /** + * Arrange + */ + const workspace = createWorkspaceMock({ + plan: mockedPlans.eventsLimit10, + billingPeriodEventsCount: 0, + lastChargeDate: new Date(), + }); + + delete (workspace as Partial).tariffPlanId; + + await workspaceCollection.insertOne(workspace); + + /** + * Act & Assert + */ + await expect( + dbHelper.getWorkspacesWithTariffPlans(workspace._id.toString()) + ).rejects.toThrow(NonCriticalError); + }); + test('fetchPlans should throw CriticalError when the plans collection is empty', async () => { /** * Arrange — a helper pointed at an empty plans collection From b9aa91113d8a3f472a0dc9a7766aee69fc371157 Mon Sep 17 00:00:00 2001 From: Kuchizu Date: Mon, 22 Jun 2026 20:21:17 +0300 Subject: [PATCH 2/2] fix(limiter): avoid toString on missing tariffPlanId in error message --- workers/limiter/src/dbHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/limiter/src/dbHelper.ts b/workers/limiter/src/dbHelper.ts index a93e0019..3af60b34 100644 --- a/workers/limiter/src/dbHelper.ts +++ b/workers/limiter/src/dbHelper.ts @@ -259,7 +259,7 @@ export class DbHelper { const plan = await this.resolvePlan(workspace.tariffPlanId); if (!plan) { - throw new NonCriticalError(`Tariff plan ${workspace.tariffPlanId.toString()} not found for workspace ${id}`, { + throw new NonCriticalError(`Tariff plan ${workspace.tariffPlanId?.toString()} not found for workspace ${id}`, { workspaceId: id, }); }