From feb2d38aa16db8b2373d334b25092f20ad9c5c08 Mon Sep 17 00:00:00 2001 From: Taly Date: Thu, 18 Dec 2025 17:44:40 +0300 Subject: [PATCH 1/6] Refetch plans if workspace tariff is missing from cache Introduces logic to refetch tariff plans from the database if a workspace references a plan not present in the cached list. Adds tests to verify that plans are refetched when needed and that an error is thrown if the plan is still missing after refetching. --- workers/paymaster/src/index.ts | 55 +++++++++++++-- workers/paymaster/tests/index.test.ts | 97 +++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 7 deletions(-) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index 999f5011..c9b443bc 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -53,10 +53,15 @@ export default class PaymasterWorker extends Worker { */ private workspaces: Collection; + /** + * Collection with plans + */ + private plansCollection: Collection; + /** * List of tariff plans */ - private plans: PlanDBScheme[]; + private plans: PlanDBScheme[] = []; /** * Check if today is a payday for passed timestamp @@ -111,15 +116,53 @@ export default class PaymasterWorker extends Worker { const connection = await this.db.connect(); this.workspaces = connection.collection('workspaces'); - const plansCollection = connection.collection('plans'); + this.plansCollection = connection.collection('plans'); - this.plans = await plansCollection.find({}).toArray(); + await this.fetchPlans(); + + await super.start(); + } + + /** + * Fetches tariff plans from database and keeps them cached + */ + private async fetchPlans(): Promise { + if (!this.plansCollection) { + throw new Error('Plans collection is not initialized'); + } + + this.plans = await this.plansCollection.find({}).toArray(); if (this.plans.length === 0) { throw new Error('Please add tariff plans to the database'); } + } - await super.start(); + /** + * Finds plan by id from cached plans + */ + private findPlanById(planId: WorkspaceDBScheme['tariffPlanId']): PlanDBScheme | undefined { + return this.plans.find((plan) => plan._id.toString() === planId.toString()); + } + + /** + * Returns workspace plan, refreshes cache when plan is missing + */ + private async getWorkspacePlan(workspace: WorkspaceDBScheme): Promise { + let currentPlan = this.findPlanById(workspace.tariffPlanId); + + if (currentPlan) { + return currentPlan; + } + + await this.fetchPlans(); + currentPlan = this.findPlanById(workspace.tariffPlanId); + + if (!currentPlan) { + throw new Error(`[Paymaster] Tariff plan ${workspace.tariffPlanId.toString()} not found for workspace ${workspace._id.toString()} (${workspace.name})`); + } + + return currentPlan; } /** @@ -180,9 +223,7 @@ export default class PaymasterWorker extends Worker { */ private async processWorkspaceSubscriptionCheck(workspace: WorkspaceDBScheme): Promise<[WorkspaceDBScheme, boolean]> { const date = new Date(); - const currentPlan = this.plans.find( - (plan) => plan._id.toString() === workspace.tariffPlanId.toString() - ); + const currentPlan = await this.getWorkspacePlan(workspace); /** Define readable values */ diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index 4217802b..ef8f68d5 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -695,6 +695,103 @@ describe('PaymasterWorker', () => { MockDate.reset(); }); + test('Should refetch plans if workspace tariff appears after worker start', async () => { + /** + * Arrange + */ + const currentDate = new Date('2005-12-22'); + const cachedPlan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + + await tariffCollection.insertOne(cachedPlan); + + const worker = new PaymasterWorker(); + + await worker.start(); + + const newPlan = createPlanMock({ + monthlyCharge: 0, + isDefault: false, + }); + + await tariffCollection.insertOne(newPlan); + + const workspace = createWorkspaceMock({ + plan: newPlan, + subscriptionId: null, + lastChargeDate: new Date('2005-11-22'), + isBlocked: true, + billingPeriodEventsCount: 10, + }); + + await workspacesCollection.insertOne(workspace); + + MockDate.set(currentDate); + + /** + * Act + */ + await expect(worker.handle(WORKSPACE_SUBSCRIPTION_CHECK)).resolves.not.toThrow(); + + /** + * Assert + */ + const updatedWorkspace = await workspacesCollection.findOne({ _id: workspace._id }); + + expect(updatedWorkspace.lastChargeDate).toEqual(currentDate); + expect(updatedWorkspace.billingPeriodEventsCount).toEqual(0); + expect(updatedWorkspace.isBlocked).toEqual(false); + + await worker.finish(); + MockDate.reset(); + }); + + test('Should throw an error when workspace plan is still missing after refetch', async () => { + /** + * Arrange + */ + const currentDate = new Date('2005-12-22'); + const cachedPlan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + + await tariffCollection.insertOne(cachedPlan); + + const worker = new PaymasterWorker(); + + await worker.start(); + + const missingPlan = createPlanMock({ + monthlyCharge: 50, + isDefault: false, + }); + + const workspace = createWorkspaceMock({ + plan: missingPlan, + subscriptionId: null, + lastChargeDate: new Date('2005-11-22'), + isBlocked: false, + billingPeriodEventsCount: 10, + }); + + await workspacesCollection.insertOne(workspace); + + MockDate.set(currentDate); + + /** + * Act + Assert + */ + await expect(worker.handle(WORKSPACE_SUBSCRIPTION_CHECK)).rejects.toThrow( + `[Paymaster] Tariff plan ${missingPlan._id.toString()} not found for workspace ${workspace._id.toString()} (${workspace.name})` + ); + + await worker.finish(); + MockDate.reset(); + }); + afterAll(async () => { await connection.close(); MockDate.reset(); From bcf145cdf46ff4fe4ec59d0e4c76174b730b52c6 Mon Sep 17 00:00:00 2001 From: Taly Date: Thu, 18 Dec 2025 17:49:19 +0300 Subject: [PATCH 2/6] Refactor plan fetching and lookup methods Moved helper methods below the public ones to satisfy @typescript-eslint/member-ordering. Lint should now pass for those ordering errors. --- workers/paymaster/src/index.ts | 57 +++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index c9b443bc..48799e20 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -123,21 +123,6 @@ export default class PaymasterWorker extends Worker { await super.start(); } - /** - * Fetches tariff plans from database and keeps them cached - */ - private async fetchPlans(): Promise { - if (!this.plansCollection) { - throw new Error('Plans collection is not initialized'); - } - - this.plans = await this.plansCollection.find({}).toArray(); - - if (this.plans.length === 0) { - throw new Error('Please add tariff plans to the database'); - } - } - /** * Finds plan by id from cached plans */ @@ -185,6 +170,48 @@ export default class PaymasterWorker extends Worker { } } + /** + * Fetches tariff plans from database and keeps them cached + */ + private async fetchPlans(): Promise { + if (!this.plansCollection) { + throw new Error('Plans collection is not initialized'); + } + + this.plans = await this.plansCollection.find({}).toArray(); + + if (this.plans.length === 0) { + throw new Error('Please add tariff plans to the database'); + } + } + + /** + * Finds plan by id from cached plans + */ + private findPlanById(planId: WorkspaceDBScheme['tariffPlanId']): PlanDBScheme | undefined { + return this.plans.find((plan) => plan._id.toString() === planId.toString()); + } + + /** + * Returns workspace plan, refreshes cache when plan is missing + */ + private async getWorkspacePlan(workspace: WorkspaceDBScheme): Promise { + let currentPlan = this.findPlanById(workspace.tariffPlanId); + + if (currentPlan) { + return currentPlan; + } + + await this.fetchPlans(); + currentPlan = this.findPlanById(workspace.tariffPlanId); + + if (!currentPlan) { + throw new Error(`[Paymaster] Tariff plan ${workspace.tariffPlanId.toString()} not found for workspace ${workspace._id.toString()} (${workspace.name})`); + } + + return currentPlan; + } + /** * WorkspaceSubscriptionCheckEvent event handler * From e3b9a9622087af29fa497046642b663fc3fb7b19 Mon Sep 17 00:00:00 2001 From: Taly Date: Thu, 18 Dec 2025 17:50:06 +0300 Subject: [PATCH 3/6] Update workspace block status in database Added database updates to set the isBlocked flag and blockedDate when blocking a workspace, and to unset isBlocked when unblocking. This ensures the workspace's block status is accurately reflected in the database. --- workers/paymaster/src/index.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index 48799e20..0322e610 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -421,6 +421,15 @@ export default class PaymasterWorker extends Worker { }, }); + await this.workspaces.updateOne({ + _id: workspace._id, + }, { + $set: { + isBlocked: true, + blockedDate: workspace.blockedDate || new Date(), + }, + }); + await this.sendWorkspaceBlockedReport(workspace); } @@ -434,6 +443,14 @@ export default class PaymasterWorker extends Worker { type: 'unblock-workspace', workspaceId: workspace._id.toString(), }); + + await this.workspaces.updateOne({ + _id: workspace._id, + }, { + $set: { + isBlocked: false, + }, + }); } From e2d141685a3a7365dcdcc8bed3c5cacede393823 Mon Sep 17 00:00:00 2001 From: Taly Date: Thu, 18 Dec 2025 17:52:24 +0300 Subject: [PATCH 4/6] Remove unused plan lookup methods Deleted the private methods findPlanById and getWorkspacePlan from PaymasterWorker as they are no longer used in the codebase. --- workers/paymaster/src/index.ts | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index 0322e610..b774aa86 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -123,33 +123,6 @@ export default class PaymasterWorker extends Worker { await super.start(); } - /** - * Finds plan by id from cached plans - */ - private findPlanById(planId: WorkspaceDBScheme['tariffPlanId']): PlanDBScheme | undefined { - return this.plans.find((plan) => plan._id.toString() === planId.toString()); - } - - /** - * Returns workspace plan, refreshes cache when plan is missing - */ - private async getWorkspacePlan(workspace: WorkspaceDBScheme): Promise { - let currentPlan = this.findPlanById(workspace.tariffPlanId); - - if (currentPlan) { - return currentPlan; - } - - await this.fetchPlans(); - currentPlan = this.findPlanById(workspace.tariffPlanId); - - if (!currentPlan) { - throw new Error(`[Paymaster] Tariff plan ${workspace.tariffPlanId.toString()} not found for workspace ${workspace._id.toString()} (${workspace.name})`); - } - - return currentPlan; - } - /** * Finish everything */ From 0b2c66126649401b25e42fe89937cef0e5c99483 Mon Sep 17 00:00:00 2001 From: Taly Date: Thu, 18 Dec 2025 19:41:12 +0300 Subject: [PATCH 5/6] Update index.ts --- workers/paymaster/src/index.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index b774aa86..a2b630d1 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -394,15 +394,6 @@ export default class PaymasterWorker extends Worker { }, }); - await this.workspaces.updateOne({ - _id: workspace._id, - }, { - $set: { - isBlocked: true, - blockedDate: workspace.blockedDate || new Date(), - }, - }); - await this.sendWorkspaceBlockedReport(workspace); } From c73d38ea74e3cb067728f8b79f3fd34c0d45ee45 Mon Sep 17 00:00:00 2001 From: Taly Date: Thu, 18 Dec 2025 19:41:51 +0300 Subject: [PATCH 6/6] Update index.ts --- workers/paymaster/src/index.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index a2b630d1..bde8e284 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -407,14 +407,6 @@ export default class PaymasterWorker extends Worker { type: 'unblock-workspace', workspaceId: workspace._id.toString(), }); - - await this.workspaces.updateOne({ - _id: workspace._id, - }, { - $set: { - isBlocked: false, - }, - }); }