From 491961f775d1477dded8baf911cb6af987f621c5 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Tue, 10 Jun 2025 14:52:27 +0300 Subject: [PATCH 1/5] feat(paymaster): add paidUntil recharge logic --- lib/utils/hasValue.ts | 1 + workers/paymaster/src/index.ts | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/utils/hasValue.ts b/lib/utils/hasValue.ts index 6bbc44ab..5a74a93f 100644 --- a/lib/utils/hasValue.ts +++ b/lib/utils/hasValue.ts @@ -1,5 +1,6 @@ /** * Returns true if specified value is not undefined, null and empty string + * * @param v - value to check */ export function hasValue(v: T | undefined | null): v is T { diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index 10009ce6..3d3bee94 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -80,6 +80,22 @@ export default class PaymasterWorker extends Worker { return now >= expectedPayDay.getTime(); } + /** + * Method that returns date of the last charge date + 1 month + * + * It is used to recharge prepaid workspaces + * + * @param date - last charge date + * @returns - last charge date + 1 month + */ + private static monthSinceLastChargeDate(date: Date): Date { + const monthSinceLastChargeDate = new Date(date.getTime()); + + monthSinceLastChargeDate.setMonth(monthSinceLastChargeDate.getMonth() + 1); + + return monthSinceLastChargeDate; + } + /** * Returns difference between now and payday in days * @@ -214,6 +230,11 @@ export default class PaymasterWorker extends Worker { // @ts-expect-error debug const isTimeToPay = PaymasterWorker.isTimeToPay(workspace.lastChargeDate, workspace.paidUntil, workspace.isDebug); + /** + * Date one month after last charge date + */ + const monthSinceLastChargeDate: Date = PaymasterWorker.monthSinceLastChargeDate(workspace.lastChargeDate); + /** * How many days have passed since payments the expected day of payments */ @@ -237,7 +258,17 @@ export default class PaymasterWorker extends Worker { */ if (!isTimeToPay) { /** - * If workspace was manually unblocked (reset of billingPeriodEventsCount and lastChargeDate) in db + * If month since last charge date passed, but it is still not time to pay - then workspace has prepaid period + * Start new month - recharge billing period events count and update last charge date + */ + if (date >= monthSinceLastChargeDate) { + await this.updateLastChargeDate(workspace, date); + await this.clearBillingPeriodEventsCount(workspace); + } + + /** + * If workspace is blocked, but it is not time to pay + * This case could be reached by prepaid workspaces and manually recharged ones */ if (workspace.isBlocked) { await this.unblockWorkspace(workspace); From 4784588a786aca5729f61882640cb0dd1061a7ca Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Tue, 10 Jun 2025 14:52:46 +0300 Subject: [PATCH 2/5] test(paymaster): cover paidUntil recharge logic --- workers/paymaster/tests/index.test.ts | 63 ++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index a4cc33f5..74f52e1f 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -481,11 +481,6 @@ describe('PaymasterWorker', () => { MockDate.reset(); }); - afterAll(async () => { - await connection.close(); - MockDate.reset(); - }); - test('Should send notification if payday is coming for workspace with paidUntil value', async () => { /** * Arrange @@ -542,4 +537,62 @@ describe('PaymasterWorker', () => { MockDate.reset(); }); + + test('Should recharge workspace billing period when month passes since last charge date and paidUntil is set to several months in the future', async () => { + /** + * Arrange + */ + const currentDate = new Date(); + const lastChargeDate = new Date(currentDate.getTime()); + + lastChargeDate.setMonth(lastChargeDate.getMonth() - 1); // Set last charge date to 1 month ago + + const paidUntil = new Date(currentDate.getTime()); + + paidUntil.setMonth(paidUntil.getMonth() + 3); // Set paidUntil to 3 months in the future + + const plan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + const workspace = createWorkspaceMock({ + plan, + subscriptionId: null, + lastChargeDate, + isBlocked: false, + billingPeriodEventsCount: 10, + paidUntil, + }); + + await fillDatabaseWithMockedData({ + workspace, + plan, + }); + + MockDate.set(currentDate); + + /** + * Act + */ + const worker = new PaymasterWorker(); + + await worker.start(); + await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); + await worker.finish(); + + /** + * Assert + */ + const updatedWorkspace = await workspacesCollection.findOne({ _id: workspace._id }); + + expect(updatedWorkspace.lastChargeDate).toEqual(currentDate); + expect(updatedWorkspace.billingPeriodEventsCount).toEqual(0); + + MockDate.reset(); + }); + + afterAll(async () => { + await connection.close(); + MockDate.reset(); + }); }); From a50d52f7aa08f2415ac898f7dd81c8df60064942 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:33:08 +0300 Subject: [PATCH 3/5] imp(paymaster): isTimeToRecharge added --- workers/paymaster/src/index.ts | 52 +++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index 3d3bee94..b3b152c3 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -60,40 +60,46 @@ export default class PaymasterWorker extends Worker { /** * Check if today is a payday for passed timestamp * - * Pay day is calculated by formula: last charge date + 30 days - * * @param date - last charge date * @param paidUntil - paid until date * @param isDebug - flag for debug purposes */ - private static isTimeToPay(date: Date, paidUntil: Date, isDebug = false): boolean { - const expectedPayDay = paidUntil ? new Date(paidUntil) : new Date(date); + private static isTimeToPay(date: Date, paidUntil?: Date, isDebug = false): boolean { + const expectedPayDay = paidUntil ? new Date(paidUntil) : this.composeBillingPeriodEndDate(date, isDebug); + const now = new Date(); - if (isDebug) { - expectedPayDay.setDate(date.getDate() + 1); - } else if (!paidUntil) { - expectedPayDay.setMonth(date.getMonth() + 1); - } + return now >= expectedPayDay; + } - const now = new Date().getTime(); + /** + * Check if today is a recharge day for passed timestamp + * + * @param date - last charge date + * @param isDebug - flag for debug purposes + */ + private static isTimeToRecharge(date: Date, isDebug = false): boolean { + const nexTimeToRecharge = this.composeBillingPeriodEndDate(date, isDebug); + const now = new Date(); - return now >= expectedPayDay.getTime(); + return now >= nexTimeToRecharge; } /** - * Method that returns date of the last charge date + 1 month - * - * It is used to recharge prepaid workspaces + * Returns the date - end of the billing period * * @param date - last charge date - * @returns - last charge date + 1 month + * @param isDebug - flag for debug workspaces */ - private static monthSinceLastChargeDate(date: Date): Date { - const monthSinceLastChargeDate = new Date(date.getTime()); + private static composeBillingPeriodEndDate(date: Date, isDebug = false): Date { + const endDate = new Date(date); - monthSinceLastChargeDate.setMonth(monthSinceLastChargeDate.getMonth() + 1); + if (isDebug) { + endDate.setDate(date.getDate() + 1); + } else { + endDate.setMonth(date.getMonth() + 1); + } - return monthSinceLastChargeDate; + return endDate; } /** @@ -231,9 +237,9 @@ export default class PaymasterWorker extends Worker { const isTimeToPay = PaymasterWorker.isTimeToPay(workspace.lastChargeDate, workspace.paidUntil, workspace.isDebug); /** - * Date one month after last charge date + * Is it time to recharge workspace limits */ - const monthSinceLastChargeDate: Date = PaymasterWorker.monthSinceLastChargeDate(workspace.lastChargeDate); + const isTimeToRecharge = PaymasterWorker.isTimeToRecharge(workspace.lastChargeDate, workspace.isDebug); /** * How many days have passed since payments the expected day of payments @@ -258,10 +264,10 @@ export default class PaymasterWorker extends Worker { */ if (!isTimeToPay) { /** - * If month since last charge date passed, but it is still not time to pay - then workspace has prepaid period + * If it is time to recharge workspace limits, but not time to pay * Start new month - recharge billing period events count and update last charge date */ - if (date >= monthSinceLastChargeDate) { + if (isTimeToRecharge) { await this.updateLastChargeDate(workspace, date); await this.clearBillingPeriodEventsCount(workspace); } From 1b5353d6de0fae0d2f44283b7bd5efe713c2a022 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:34:58 +0300 Subject: [PATCH 4/5] fix(): expect ts error --- workers/paymaster/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index b3b152c3..a557f7d2 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -239,6 +239,7 @@ export default class PaymasterWorker extends Worker { /** * Is it time to recharge workspace limits */ + // @ts-expect-error debug const isTimeToRecharge = PaymasterWorker.isTimeToRecharge(workspace.lastChargeDate, workspace.isDebug); /** From fc45a1eb85b595d5daaf78652b8531893cc99a06 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:37:04 +0300 Subject: [PATCH 5/5] imp(): comment --- workers/paymaster/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index a557f7d2..0ce4d681 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -73,6 +73,7 @@ export default class PaymasterWorker extends Worker { /** * Check if today is a recharge day for passed timestamp + * It equals to the isTimeToPay in all cases except prePaid workspaces * * @param date - last charge date * @param isDebug - flag for debug purposes