From 2452285a576d5b1b0edd2a7e31d4a83ea2d24a35 Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 3 Dec 2025 01:35:03 +0300 Subject: [PATCH 01/40] Add blocked workspace reminder email template Introduces a new email template for reminding users about blocked workspaces due to expired or depleted plans. Adds pluralization utility for Russian, updates template names, and includes logic to calculate days since the last payment. --- workers/email/scripts/emailOverview.ts | 21 ++++++++++ .../src/templates/components/layout.twig | 2 + .../email/src/templates/components/utils.twig | 14 +++++++ .../blocked-workspace-reminder/html.twig | 39 +++++++++++++++++++ .../blocked-workspace-reminder/subject.twig | 3 ++ .../blocked-workspace-reminder/text.twig | 14 +++++++ workers/email/src/templates/names.ts | 1 + 7 files changed, 94 insertions(+) create mode 100644 workers/email/src/templates/components/utils.twig create mode 100644 workers/email/src/templates/emails/blocked-workspace-reminder/html.twig create mode 100644 workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig create mode 100644 workers/email/src/templates/emails/blocked-workspace-reminder/text.twig diff --git a/workers/email/scripts/emailOverview.ts b/workers/email/scripts/emailOverview.ts index 981093dbd..6bfd92c32 100644 --- a/workers/email/scripts/emailOverview.ts +++ b/workers/email/scripts/emailOverview.ts @@ -147,6 +147,7 @@ class EmailTestServer { user, period: 10, reason: 'error on the payment server side', + daysAfterPayday: await this.calculateDaysAfterPayday(workspace), }; try { @@ -211,6 +212,7 @@ class EmailTestServer { private sendHTML(html: string, response: http.ServerResponse): void { response.writeHead(HttpStatusCode.Ok, { 'Content-Type': 'text/html', + 'Content-Type': 'text/html; charset=utf-8', }); response.write(html); response.end(); @@ -319,8 +321,27 @@ class EmailTestServer { */ private async getWorkspace(workspaceId: string): Promise { const connection = await this.accountsDb.getConnection(); + } + + private async calculateDaysAfterPayday( + workspace: WorkspaceDBScheme + ): Promise { + if (!workspace.paidUntil) { + return 0; + } + + const now = new Date(); + const paidUntil = new Date(workspace.paidUntil); + const diffTime = now.getTime() - paidUntil.getTime(); return connection.collection('workspaces').findOne({ _id: new ObjectId(workspaceId) }); + if (diffTime <= 0) { + return 0; + } + + const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + + return diffDays; } /** diff --git a/workers/email/src/templates/components/layout.twig b/workers/email/src/templates/components/layout.twig index 539967d8d..98a3f7083 100644 --- a/workers/email/src/templates/components/layout.twig +++ b/workers/email/src/templates/components/layout.twig @@ -1,3 +1,5 @@ +{% import "./utils.twig" as utils %} + diff --git a/workers/email/src/templates/components/utils.twig b/workers/email/src/templates/components/utils.twig new file mode 100644 index 000000000..f7cc075fe --- /dev/null +++ b/workers/email/src/templates/components/utils.twig @@ -0,0 +1,14 @@ +{% macro pluralize_ru(n, forms) %} + {% if n % 100 >= 11 and n % 100 <= 19 %} + {{ forms[2] }} + {% else %} + {% set last = n % 10 %} + {% if last == 1 %} + {{ forms[0] }} + {% elseif last >= 2 and last <= 4 %} + {{ forms[1] }} + {% else %} + {{ forms[2] }} + {% endif %} + {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig new file mode 100644 index 000000000..00128fef4 --- /dev/null +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig @@ -0,0 +1,39 @@ +{% extends '../../components/layout.twig' %} + +{% block header %} + {% include '../../components/workspace.twig' with {workspace: workspace} %} +{% endblock %} + +{% block content %} + + + + + + + + + + {{ daysAfterPayday }} {{ daysAfterPayday ? utils.pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} без мониторинга + + + + + + + +

+ Напоминаем, что мониторинг ошибок в «{{ workspace.name | escape }}» всё ещё остановлен, потому что закончился лимит или срок действия тарифного плана. +

+

+ Чтобы снова видеть актуальные события, выберите подходящий тарифный план и продлите подписку в настройках оплаты. +

+
+ + + + + {% include '../../components/button.twig' with {href: host ~ '/workspace/' ~ workspace._id ~ '/settings/billing', label: workspace.tariffPlanId is same as('5f47f031ff71510040f433c1') ? 'Выбрать тариф от 99 ₽' : 'Открыть настройки'} %} + + +{% endblock %} diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig new file mode 100644 index 000000000..900a9ec2f --- /dev/null +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig @@ -0,0 +1,3 @@ +{% import "../../components/utils.twig" as utils %} + +Вы уже {{ daysAfterPayday }} {{ daysAfterPayday ? utils.pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки \ No newline at end of file diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig new file mode 100644 index 000000000..8d819efb2 --- /dev/null +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig @@ -0,0 +1,14 @@ +{% import "../../components/utils.twig" as utils %} + +Вы уже {{ daysAfterPayday }} {{ daysAfterPayday ? utils.pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки. + +Напоминаем, что мониторинг ошибок в «{{ workspace.name | escape }}» всё ещё остановлен, потому что закончился лимит или срок действия тарифного плана. + +Чтобы снова видеть актуальные события, выберите подходящий тарифный план и продлите подписку в настройках оплаты: {{ host }}/workspace/{{ workspace._id }}/settings/billing + +*** + +Хоук +Российский трекер ошибок + +Made by CodeX \ No newline at end of file diff --git a/workers/email/src/templates/names.ts b/workers/email/src/templates/names.ts index e01db2921..82983533f 100644 --- a/workers/email/src/templates/names.ts +++ b/workers/email/src/templates/names.ts @@ -4,6 +4,7 @@ enum Templates { Assignee = 'assignee', BlockWorkspace = 'block-workspace', + BlockedWorkspaceReminder = 'blocked-workspace-reminder', DaysLimitAlmostReached = 'days-limit-almost-reached', Event = 'event', EventsLimitAlmostReached = 'events-limit-almost-reached', From 130decc23c4438723b2e04b25cb2caf45ae6611a Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 3 Dec 2025 01:35:41 +0300 Subject: [PATCH 02/40] Send reminder emails to blocked workspace admins Added logic to send reminder emails to admins of blocked workspaces at specific intervals after payday. Introduced the sendBlockedWorkspaceReminders method and updated tests to cover this new behavior. --- workers/paymaster/src/index.ts | 26 +++++++++++++++++++++++++- workers/paymaster/tests/index.test.ts | 8 ++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index 0ce4d681e..892e48fe2 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -319,9 +319,13 @@ export default class PaymasterWorker extends Worker { /** * Time to pay but workspace has paid plan - * If it is blocked then do nothing + * If it is blocked then remind admins about it */ if (workspace.isBlocked) { + if (daysAfterPayday in [1, 2, 3, 5, 7, 30]) { + await this.sendBlockedWorkspaceReminders(workspace, daysAfterPayday); + } + return [workspace, true]; } @@ -403,6 +407,26 @@ export default class PaymasterWorker extends Worker { }); } + + /** + * Sends reminder emails to blocked workspace admins + * + * @param workspace - workspace to send reminders for + * @param daysBlocked - number of days the workspace has been blocked + */ + private async sendBlockedWorkspaceReminders( + workspace: WorkspaceDBScheme, + daysBlocked: number = null + ): Promise { + await this.addTask(WorkerNames.EMAIL, { + type: 'blocked-workspace-reminder', + payload: { + workspaceId: workspace._id.toString(), + daysBlocked: daysBlocked, + }, + }); + } + /** * Sets BillingPeriodEventsCount to 0 in workspace * diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index 74f52e1f3..432652398 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -203,6 +203,14 @@ describe('PaymasterWorker', () => { workspaceId: workspace._id.toString(), }, }); + + expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { + type: 'blocked-workspace-reminder', + payload: { + workspaceId: workspace._id.toString(), + daysAfterPayday: 2, + }, + }); MockDate.reset(); }); From a830c395b5fdc65af4a5b1e4201acaeec1570983 Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 3 Dec 2025 01:38:07 +0300 Subject: [PATCH 03/40] Fix workspace retrieval and header in EmailTestServer Corrects the Content-Type header to include charset in sendHTML and fixes the getWorkspace method to properly return the workspace document. Also removes a redundant line in calculateDaysAfterPayday. --- workers/email/scripts/emailOverview.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/workers/email/scripts/emailOverview.ts b/workers/email/scripts/emailOverview.ts index 6bfd92c32..f1bc2c593 100644 --- a/workers/email/scripts/emailOverview.ts +++ b/workers/email/scripts/emailOverview.ts @@ -211,7 +211,6 @@ class EmailTestServer { */ private sendHTML(html: string, response: http.ServerResponse): void { response.writeHead(HttpStatusCode.Ok, { - 'Content-Type': 'text/html', 'Content-Type': 'text/html; charset=utf-8', }); response.write(html); @@ -321,6 +320,8 @@ class EmailTestServer { */ private async getWorkspace(workspaceId: string): Promise { const connection = await this.accountsDb.getConnection(); + + return connection.collection('workspaces').findOne({ _id: new ObjectId(workspaceId) }); } private async calculateDaysAfterPayday( @@ -334,7 +335,7 @@ class EmailTestServer { const paidUntil = new Date(workspace.paidUntil); const diffTime = now.getTime() - paidUntil.getTime(); - return connection.collection('workspaces').findOne({ _id: new ObjectId(workspaceId) }); + if (diffTime <= 0) { return 0; } From a02732bec21bb8e3c765ea6dda83e2d80d6be09b Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 3 Dec 2025 01:44:54 +0300 Subject: [PATCH 04/40] Add comments and eslint disables for magic numbers Added explanatory comments and disabled the @typescript-eslint/no-magic-numbers rule for specific lines in emailOverview.ts and paymaster/src/index.ts where magic numbers are used for date calculations and reminder scheduling. --- workers/email/scripts/emailOverview.ts | 2 ++ workers/paymaster/src/index.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/workers/email/scripts/emailOverview.ts b/workers/email/scripts/emailOverview.ts index f1bc2c593..6836a6a07 100644 --- a/workers/email/scripts/emailOverview.ts +++ b/workers/email/scripts/emailOverview.ts @@ -340,6 +340,8 @@ class EmailTestServer { return 0; } + // Calculate difference in days + // eslint-disable-next-line @typescript-eslint/no-magic-numbers const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); return diffDays; diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index 892e48fe2..bef2d007c 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -322,6 +322,8 @@ export default class PaymasterWorker extends Worker { * If it is blocked then remind admins about it */ if (workspace.isBlocked) { + // Send reminders on certain days after payday + // eslint-disable-next-line @typescript-eslint/no-magic-numbers if (daysAfterPayday in [1, 2, 3, 5, 7, 30]) { await this.sendBlockedWorkspaceReminders(workspace, daysAfterPayday); } From 91a30143e6d067d0b4c623833cc0832a888cc978 Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 3 Dec 2025 01:54:37 +0300 Subject: [PATCH 05/40] Update index.test.ts --- workers/paymaster/tests/index.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index 432652398..d9a36d153 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -167,6 +167,8 @@ describe('PaymasterWorker', () => { plan, subscriptionId: 'some-subscription-id', lastChargeDate: new Date('2005-11-22'), + // + one month, right? + paidUntil: new Date('2005-12-22'), isBlocked: false, billingPeriodEventsCount: 10, }); From 9a564e0e5ad8956cbfeea5cb36c9a9c605638ba1 Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 3 Dec 2025 01:57:45 +0300 Subject: [PATCH 06/40] Update test to use expect.any(Number) for daysAfterPayday Replaces the hardcoded value for daysAfterPayday with expect.any(Number) in the blocked-workspace-reminder test payload to allow for dynamic number matching. --- workers/paymaster/tests/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index d9a36d153..0eddcc609 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -210,7 +210,7 @@ describe('PaymasterWorker', () => { type: 'blocked-workspace-reminder', payload: { workspaceId: workspace._id.toString(), - daysAfterPayday: 2, + daysAfterPayday: expect.any(Number), }, }); MockDate.reset(); From 89bac2c2d740d3c6a06a34ce1d99d1a3374b345c Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 3 Dec 2025 02:11:33 +0300 Subject: [PATCH 07/40] Move pluralize_ru macro from Twig to TypeScript extension Removed the utils.twig file and its pluralize_ru macro, replacing it with a TypeScript implementation registered as a Twig function in extensions.ts. Updated all template usages to call pluralize_ru directly, simplifying imports and improving maintainability. --- .../src/templates/components/layout.twig | 2 -- .../email/src/templates/components/utils.twig | 14 ----------- .../blocked-workspace-reminder/html.twig | 2 +- .../blocked-workspace-reminder/subject.twig | 4 +--- .../blocked-workspace-reminder/text.twig | 4 +--- workers/email/src/templates/extensions.ts | 23 +++++++++++++++++++ 6 files changed, 26 insertions(+), 23 deletions(-) delete mode 100644 workers/email/src/templates/components/utils.twig diff --git a/workers/email/src/templates/components/layout.twig b/workers/email/src/templates/components/layout.twig index 98a3f7083..539967d8d 100644 --- a/workers/email/src/templates/components/layout.twig +++ b/workers/email/src/templates/components/layout.twig @@ -1,5 +1,3 @@ -{% import "./utils.twig" as utils %} - diff --git a/workers/email/src/templates/components/utils.twig b/workers/email/src/templates/components/utils.twig deleted file mode 100644 index f7cc075fe..000000000 --- a/workers/email/src/templates/components/utils.twig +++ /dev/null @@ -1,14 +0,0 @@ -{% macro pluralize_ru(n, forms) %} - {% if n % 100 >= 11 and n % 100 <= 19 %} - {{ forms[2] }} - {% else %} - {% set last = n % 10 %} - {% if last == 1 %} - {{ forms[0] }} - {% elseif last >= 2 and last <= 4 %} - {{ forms[1] }} - {% else %} - {{ forms[2] }} - {% endif %} - {% endif %} -{% endmacro %} \ No newline at end of file diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig index 00128fef4..25e3c7b9e 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig @@ -14,7 +14,7 @@ - {{ daysAfterPayday }} {{ daysAfterPayday ? utils.pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} без мониторинга + {{ daysAfterPayday }} {{ daysAfterPayday ? pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} без мониторинга diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig index 900a9ec2f..fb2e57393 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig @@ -1,3 +1 @@ -{% import "../../components/utils.twig" as utils %} - -Вы уже {{ daysAfterPayday }} {{ daysAfterPayday ? utils.pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки \ No newline at end of file +Вы уже {{ daysAfterPayday }} {{ daysAfterPayday ? pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки \ No newline at end of file diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig index 8d819efb2..24a3c203f 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig @@ -1,6 +1,4 @@ -{% import "../../components/utils.twig" as utils %} - -Вы уже {{ daysAfterPayday }} {{ daysAfterPayday ? utils.pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки. +Вы уже {{ daysAfterPayday }} {{ daysAfterPayday ? pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки. Напоминаем, что мониторинг ошибок в «{{ workspace.name | escape }}» всё ещё остановлен, потому что закончился лимит или срок действия тарифного плана. diff --git a/workers/email/src/templates/extensions.ts b/workers/email/src/templates/extensions.ts index a61ac35ff..8b8184b2a 100644 --- a/workers/email/src/templates/extensions.ts +++ b/workers/email/src/templates/extensions.ts @@ -154,3 +154,26 @@ Twig.extendFilter('abbrNumber', (value: number): string => { Twig.extendFilter('sortEvents', (events: TemplateEventData[]): TemplateEventData[] => { return events.sort((a, b) => a.newCount - b.newCount); }); + +/** + * Pluralize Russian words based on a number + * + * @param {number} n - the number to determine the form + * @param {string[]} forms - array of word forms [singular, few, many] + * @returns {string} + */ +Twig.extendFunction('pluralize_ru', (n: number, forms: string[]): string => { + if (n % 100 >= 11 && n % 100 <= 19) { + return forms[2]; + } + + const last = n % 10; + + if (last === 1) { + return forms[0]; + } else if (last >= 2 && last <= 4) { + return forms[1]; + } else { + return forms[2]; + } +}); \ No newline at end of file From dd71fc5ce84253b7d44e0759a85480e5fec004ea Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 3 Dec 2025 02:11:39 +0300 Subject: [PATCH 08/40] Refactor blocked workspace reminder days to constant Introduced DAYS_AFTER_PAYDAY_TO_REMIND constant to define the days after payday when admins are reminded about blocked workspaces. Updated the logic to use this constant for better maintainability and clarity. --- workers/paymaster/src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index bef2d007c..40e9ed758 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -33,6 +33,12 @@ const DAYS_AFTER_PAYDAY_TO_TRY_PAYING = 3; // eslint-disable-next-line @typescript-eslint/no-magic-numbers const DAYS_LEFT_ALERT = [3, 2, 1, 0]; +/** + * Days after payday to remind admins about blocked workspace + */ +// eslint-disable-next-line @typescript-eslint/no-magic-numbers +const DAYS_AFTER_PAYDAY_TO_REMIND = [1, 2, 3, 5, 7, 30]; + /** * Worker to check workspaces subscription status and ban workspaces without actual subscription */ @@ -324,7 +330,7 @@ export default class PaymasterWorker extends Worker { if (workspace.isBlocked) { // Send reminders on certain days after payday // eslint-disable-next-line @typescript-eslint/no-magic-numbers - if (daysAfterPayday in [1, 2, 3, 5, 7, 30]) { + if (DAYS_AFTER_PAYDAY_TO_REMIND.includes(daysAfterPayday)) { await this.sendBlockedWorkspaceReminders(workspace, daysAfterPayday); } From bbf8538c6651a33ac237104a4cd3b83c4e0484c6 Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 3 Dec 2025 02:13:18 +0300 Subject: [PATCH 09/40] Fix test dates and assertions in PaymasterWorker tests Updated test dates and moved the blocked-workspace-reminder assertion to the correct test case in index.test.ts. This ensures the tests accurately reflect the intended scenarios and improve test reliability. --- workers/paymaster/tests/index.test.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index 0eddcc609..2935690e2 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -167,8 +167,6 @@ describe('PaymasterWorker', () => { plan, subscriptionId: 'some-subscription-id', lastChargeDate: new Date('2005-11-22'), - // + one month, right? - paidUntil: new Date('2005-12-22'), isBlocked: false, billingPeriodEventsCount: 10, }); @@ -205,14 +203,6 @@ describe('PaymasterWorker', () => { workspaceId: workspace._id.toString(), }, }); - - expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { - type: 'blocked-workspace-reminder', - payload: { - workspaceId: workspace._id.toString(), - daysAfterPayday: expect.any(Number), - }, - }); MockDate.reset(); }); @@ -220,7 +210,7 @@ describe('PaymasterWorker', () => { /** * Arrange */ - const currentDate = new Date('2005-12-26'); + const currentDate = new Date('2005-12-27'); const plan = createPlanMock({ monthlyCharge: 100, isDefault: true, @@ -264,6 +254,13 @@ describe('PaymasterWorker', () => { workspaceId: workspace._id.toString(), }, }); + expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { + type: 'blocked-workspace-reminder', + payload: { + workspaceId: workspace._id.toString(), + daysAfterPayday: expect.any(Number), + }, + }); MockDate.reset(); }); From e9155f44d95c6b412bec2cd8848295e9a5677adc Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 3 Dec 2025 02:18:51 +0300 Subject: [PATCH 10/40] Add test for blocked workspace reminder after payday Introduces a test to verify that admins are reminded for blocked workspaces with active subscriptions when 3 days have passed since payday. Ensures the reminder includes the correct number of days after payday. --- workers/paymaster/tests/index.test.ts | 42 ++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index 2935690e2..eea7187fe 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -254,11 +254,51 @@ describe('PaymasterWorker', () => { workspaceId: workspace._id.toString(), }, }); + MockDate.reset(); + }); + + test('Should remind admins for blocked workspace if it has subscription and after payday passed 3 days', async () => { + /** + * Arrange + */ + const currentDate = new Date('2005-12-27'); + const plan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + const workspace = createWorkspaceMock({ + plan, + subscriptionId: 'some-subscription-id', + lastChargeDate: new Date('2005-11-22'), + isBlocked: true, + billingPeriodEventsCount: 10, + }); + + await fillDatabaseWithMockedData({ + workspace, + plan, + }); + + MockDate.set(currentDate); + + /** + * Act + */ + const worker = new PaymasterWorker(); + const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); + + await worker.start(); + await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); + await worker.finish(); + + /** + * Assert + */ expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { type: 'blocked-workspace-reminder', payload: { workspaceId: workspace._id.toString(), - daysAfterPayday: expect.any(Number), + daysAfterPayday: 5, }, }); MockDate.reset(); From 994ff1ebe88dd46caa6c0abef70cabe082e7c27b Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 3 Dec 2025 02:43:08 +0300 Subject: [PATCH 11/40] Rename daysBlocked to daysAfterPayday in reminder method Updated the sendBlockedWorkspaceReminders method to use 'daysAfterPayday' instead of 'daysBlocked' for clarity and consistency in parameter naming and payload structure. --- workers/paymaster/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index 40e9ed758..69c481044 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -420,17 +420,17 @@ export default class PaymasterWorker extends Worker { * Sends reminder emails to blocked workspace admins * * @param workspace - workspace to send reminders for - * @param daysBlocked - number of days the workspace has been blocked + * @param daysAfterPayday - number of days the workspace spent after payday */ private async sendBlockedWorkspaceReminders( workspace: WorkspaceDBScheme, - daysBlocked: number = null + daysAfterPayday: number = null ): Promise { await this.addTask(WorkerNames.EMAIL, { type: 'blocked-workspace-reminder', payload: { workspaceId: workspace._id.toString(), - daysBlocked: daysBlocked, + daysAfterPayday: daysAfterPayday, }, }); } From 088e2b68e0b8218cb145671c0cb4996c9a25ed3c Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 3 Dec 2025 02:54:19 +0300 Subject: [PATCH 12/40] Add eslint-disable for magic numbers in pluralize_ru Added eslint-disable-next-line comments to suppress magic number warnings in the pluralize_ru Twig function. This helps maintain code clarity while adhering to linting rules. --- workers/email/src/templates/extensions.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workers/email/src/templates/extensions.ts b/workers/email/src/templates/extensions.ts index 8b8184b2a..c14f66c09 100644 --- a/workers/email/src/templates/extensions.ts +++ b/workers/email/src/templates/extensions.ts @@ -163,14 +163,17 @@ Twig.extendFilter('sortEvents', (events: TemplateEventData[]): TemplateEventData * @returns {string} */ Twig.extendFunction('pluralize_ru', (n: number, forms: string[]): string => { + // eslint-disable-next-line no-magic-numbers if (n % 100 >= 11 && n % 100 <= 19) { return forms[2]; } + // eslint-disable-next-line no-magic-numbers const last = n % 10; if (last === 1) { return forms[0]; + // eslint-disable-next-line no-magic-numbers } else if (last >= 2 && last <= 4) { return forms[1]; } else { From b2298170f7b4403454dc968eba00b1743ebe1230 Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 3 Dec 2025 03:00:59 +0300 Subject: [PATCH 13/40] Update extensions.ts --- workers/email/src/templates/extensions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/workers/email/src/templates/extensions.ts b/workers/email/src/templates/extensions.ts index c14f66c09..2df6a526e 100644 --- a/workers/email/src/templates/extensions.ts +++ b/workers/email/src/templates/extensions.ts @@ -163,17 +163,17 @@ Twig.extendFilter('sortEvents', (events: TemplateEventData[]): TemplateEventData * @returns {string} */ Twig.extendFunction('pluralize_ru', (n: number, forms: string[]): string => { - // eslint-disable-next-line no-magic-numbers + // eslint-disable-next-line @typescript-eslint/no-magic-numbers if (n % 100 >= 11 && n % 100 <= 19) { return forms[2]; } - // eslint-disable-next-line no-magic-numbers + // eslint-disable-next-line @typescript-eslint/no-magic-numbers const last = n % 10; if (last === 1) { return forms[0]; - // eslint-disable-next-line no-magic-numbers + // eslint-disable-next-line @typescript-eslint/no-magic-numbers } else if (last >= 2 && last <= 4) { return forms[1]; } else { From 8dabf962ced2c83d36f5325dc696d38187ab105b Mon Sep 17 00:00:00 2001 From: Taly Date: Fri, 5 Dec 2025 23:42:05 +0300 Subject: [PATCH 14/40] Add blocked workspace reminder notification support Introduces handling for 'blocked-workspace-reminder' tasks in SenderWorker, including new types and template variables. Renames files for consistency and updates type exports to support the new notification event. --- workers/sender/src/index.ts | 62 ++++++++++++++++++- .../{blockWorkspace.ts => block-workspace.ts} | 0 .../sender-task/blocked-workspace-reminder.ts | 29 +++++++++ workers/sender/types/sender-task/index.ts | 7 ++- .../{blockWorkspace.ts => block-workspace.ts} | 0 .../blocked-workspace-reminder.ts | 32 ++++++++++ .../sender/types/template-variables/index.ts | 8 ++- 7 files changed, 132 insertions(+), 6 deletions(-) rename workers/sender/types/sender-task/{blockWorkspace.ts => block-workspace.ts} (100%) create mode 100644 workers/sender/types/sender-task/blocked-workspace-reminder.ts rename workers/sender/types/template-variables/{blockWorkspace.ts => block-workspace.ts} (100%) create mode 100644 workers/sender/types/template-variables/blocked-workspace-reminder.ts diff --git a/workers/sender/src/index.ts b/workers/sender/src/index.ts index a92c9d190..fed1c4064 100644 --- a/workers/sender/src/index.ts +++ b/workers/sender/src/index.ts @@ -29,7 +29,8 @@ import { SenderWorkerEventsLimitAlmostReachedTask, SenderWorkerSignUpTask, SenderWorkerPasswordResetTask, - SenderWorkerWorkspaceInviteTask + SenderWorkerWorkspaceInviteTask, + SenderWorkerBlockedWorkspaceReminderTask } from '../types/sender-task'; import { decodeUnsafeFields } from '../../../lib/utils/unsafeFields'; import { Notification, EventNotification, SeveralEventsNotification, PaymentFailedNotification, AssigneeNotification, SignUpNotification } from '../types/template-variables'; @@ -115,6 +116,8 @@ export default abstract class SenderWorker extends Worker { return this.handleAssigneeTask(task as SenderWorkerAssigneeTask); case 'block-workspace': return this.handleBlockWorkspaceTask(task as SenderWorkerBlockWorkspaceTask); + case 'blocked-workspace-reminder': + return this.handleBlockedWorkspaceReminderTask(task as SenderWorkerBlockedWorkspaceReminderTask); case 'days-limit-almost-reached': return this.handleDaysLimitAlmostReachedTask(task as SenderWorkerDaysLimitAlmostReachedTask); case 'event': @@ -196,7 +199,7 @@ export default abstract class SenderWorker extends Worker { project, events: eventsData, period: channel.minPeriod, - notificationRuleId: rule._id, + notificationRuleId: rule._id.toString(), }, } as EventNotification | SeveralEventsNotification); } @@ -297,6 +300,61 @@ export default abstract class SenderWorker extends Worker { })); } + private async handleBlockedWorkspaceReminderTask(task: SenderWorkerBlockedWorkspaceReminderTask): Promise { + const eventType = 'blocked-workspace-reminder'; + + /** + * Send message not often than once per day + */ + const throttleInterval = TimeMs.DAY; + + const { workspaceId, daysAfterPayday } = task.payload; + + const workspace = await this.getWorkspace(workspaceId); + + if (!workspace) { + this.logger.error(`Cannot send blocked workspace reminder notification: workspace not found. Payload: ${task}`); + + return; + } + + const allowToSendNotification = this.needToSendNextNotification(workspace, eventType, throttleInterval); + + /** + * Do not send any notifications if we have already done in target throttle time + */ + if (!allowToSendNotification) { + return; + } + + const admins = await this.getWorkspaceAdmins(workspaceId); + + if (!admins) { + this.logger.error(`Cannot send blocked workspace reminder notification: workspace team not found. Payload: ${task}`); + + return; + } + + const adminIds = admins.map(admin => admin.userId.toString()); + const users = await this.getUsers(adminIds); + + await Promise.all(users.map(async user => { + const channel = user.notifications.channels[this.channelType]; + + if (channel.isEnabled) { + await this.provider.send(channel.endpoint, { + type: 'blocked-workspace-reminder', + payload: { + host: process.env.GARAGE_URL, + hostOfStatic: process.env.API_STATIC_URL, + workspace, + daysAfterPayday, + }, + }); + } + })); + } + /** * Handle task when days limit is almost reached * diff --git a/workers/sender/types/sender-task/blockWorkspace.ts b/workers/sender/types/sender-task/block-workspace.ts similarity index 100% rename from workers/sender/types/sender-task/blockWorkspace.ts rename to workers/sender/types/sender-task/block-workspace.ts diff --git a/workers/sender/types/sender-task/blocked-workspace-reminder.ts b/workers/sender/types/sender-task/blocked-workspace-reminder.ts new file mode 100644 index 000000000..dc75e124e --- /dev/null +++ b/workers/sender/types/sender-task/blocked-workspace-reminder.ts @@ -0,0 +1,29 @@ +/** + * Payload for task for blocked workspace reminder + */ +export interface SenderWorkerBlockedWorkspaceReminderPayload { + /** + * Blocked workspace id + */ + workspaceId: string; + + /** + * Days after payday + */ + daysAfterPayday: number; +} + +/** + * Payload of an event for blocked workspace reminder + */ +export interface SenderWorkerBlockedWorkspaceReminderTask { + /** + * Task for blocked workspace reminder + */ + type: 'blocked-workspace-reminder', + + /** + * Payload for task for blocked workspace reminder + */ + payload: SenderWorkerBlockedWorkspaceReminderPayload +} diff --git a/workers/sender/types/sender-task/index.ts b/workers/sender/types/sender-task/index.ts index fb9e89c87..5733e6681 100644 --- a/workers/sender/types/sender-task/index.ts +++ b/workers/sender/types/sender-task/index.ts @@ -1,6 +1,7 @@ import { SenderWorkerAssigneeTask } from './assignee'; import { SenderWorkerEventTask } from './event'; -import { SenderWorkerBlockWorkspaceTask } from './blockWorkspace'; +import { SenderWorkerBlockWorkspaceTask } from './block-workspace'; +import { SenderWorkerBlockedWorkspaceReminderTask } from './blocked-workspace-reminder'; import { SenderWorkerPaymentFailedTask } from './payment-failed'; import { SenderWorkerPaymentSuccessTask } from './payment-success'; import { SenderWorkerDaysLimitAlmostReachedTask } from './days-limit-almost-reached'; @@ -11,7 +12,8 @@ import { SenderWorkerWorkspaceInviteTask } from './workspace-invite'; export { SenderWorkerEventTask, SenderWorkerEventPayload } from './event'; export { SenderWorkerAssigneeTask, SenderWorkerAssigneePayload } from './assignee'; -export { SenderWorkerBlockWorkspaceTask, SenderWorkerBlockWorkspacePayload } from './blockWorkspace'; +export { SenderWorkerBlockWorkspaceTask, SenderWorkerBlockWorkspacePayload } from './block-workspace'; +export { SenderWorkerBlockedWorkspaceReminderTask, SenderWorkerBlockedWorkspaceReminderPayload } from './blocked-workspace-reminder'; export { SenderWorkerPaymentFailedTask, SenderWorkerPaymentFailedPayload } from './payment-failed'; export { SenderWorkerPaymentSuccessTask, SenderWorkerPaymentSuccessPayload } from './payment-success'; export { SenderWorkerDaysLimitAlmostReachedTask, SenderWorkerDaysLimitAlmostReachedPayload } from './days-limit-almost-reached'; @@ -23,6 +25,7 @@ export { SenderWorkerWorkspaceInviteTask, SenderWorkerWorkspaceInvitePayload } f export type SenderWorkerTask = SenderWorkerEventTask | SenderWorkerAssigneeTask | SenderWorkerBlockWorkspaceTask + | SenderWorkerBlockedWorkspaceReminderTask | SenderWorkerPaymentFailedTask | SenderWorkerPaymentSuccessTask | SenderWorkerDaysLimitAlmostReachedTask diff --git a/workers/sender/types/template-variables/blockWorkspace.ts b/workers/sender/types/template-variables/block-workspace.ts similarity index 100% rename from workers/sender/types/template-variables/blockWorkspace.ts rename to workers/sender/types/template-variables/block-workspace.ts diff --git a/workers/sender/types/template-variables/blocked-workspace-reminder.ts b/workers/sender/types/template-variables/blocked-workspace-reminder.ts new file mode 100644 index 000000000..2c4fd61cf --- /dev/null +++ b/workers/sender/types/template-variables/blocked-workspace-reminder.ts @@ -0,0 +1,32 @@ +import { CommonTemplateVariables } from './common-template'; +import { WorkspaceDBScheme } from '@hawk.so/types'; +import { Notification } from './notification'; + +/** + * Variables for block workspace template + */ +export interface BlockedWorkspaceReminderTemplateVariables extends CommonTemplateVariables { + /** + * Blocked workspace data + */ + workspace: WorkspaceDBScheme; + + /** + * Number of days after payday when workspace was blocked + */ + daysAfterPayday: number; +} + +/** + * Object with notification type and variables for the block workspace event template + */ +export interface BlockedWorkspaceReminderNotification extends Notification { + /** + * Notification when workspace blocked + */ + type: 'blocked-workspace-reminder'; + /** + * Notification payload + */ + payload: BlockedWorkspaceReminderTemplateVariables; +} diff --git a/workers/sender/types/template-variables/index.ts b/workers/sender/types/template-variables/index.ts index 058ad34f3..864060b52 100644 --- a/workers/sender/types/template-variables/index.ts +++ b/workers/sender/types/template-variables/index.ts @@ -1,7 +1,8 @@ import { EventsTemplateVariables, EventNotification } from './event'; import { SeveralEventsNotification } from './several-events'; import { AssigneeTemplateVariables, AssigneeNotification } from './assignee'; -import { BlockWorkspaceTemplateVariables, BlockWorkspaceNotification } from './blockWorkspace'; +import { BlockWorkspaceTemplateVariables, BlockWorkspaceNotification } from './block-workspace'; +import { BlockedWorkspaceReminderTemplateVariables, BlockedWorkspaceReminderNotification } from './blocked-workspace-reminder'; import { PaymentFailedTemplateVariables, PaymentFailedNotification } from './payment-failed'; import { PaymentSuccessNotification, PaymentSuccessTemplateVariables } from './payment-success'; import { DaysLimitAlmostReachedTemplateVariables, DaysLimitAlmostReachedNotification } from './days-limit-almost-reached'; @@ -14,7 +15,8 @@ export { CommonTemplateVariables } from './common-template'; export { TemplateEventData, EventsTemplateVariables, EventNotification } from './event'; export { SeveralEventsNotification } from './several-events'; export { AssigneeTemplateVariables, AssigneeNotification } from './assignee'; -export { BlockWorkspaceTemplateVariables, BlockWorkspaceNotification } from './blockWorkspace'; +export { BlockWorkspaceTemplateVariables, BlockWorkspaceNotification } from './block-workspace'; +export { BlockedWorkspaceReminderTemplateVariables, BlockedWorkspaceReminderNotification } from './blocked-workspace-reminder'; export { PaymentFailedTemplateVariables, PaymentFailedNotification } from './payment-failed'; export { PaymentSuccessNotification, PaymentSuccessTemplateVariables } from './payment-success'; export { SignUpNotification, SignUpVariables } from './sign-up'; @@ -28,6 +30,7 @@ export type Notification = EventNotification | SeveralEventsNotification | AssigneeNotification | BlockWorkspaceNotification + | BlockedWorkspaceReminderNotification | PaymentFailedNotification | PaymentSuccessNotification | DaysLimitAlmostReachedNotification @@ -42,6 +45,7 @@ export type Notification = EventNotification export type TemplateVariables = EventsTemplateVariables | AssigneeTemplateVariables | BlockWorkspaceTemplateVariables + | BlockedWorkspaceReminderTemplateVariables | PaymentFailedTemplateVariables | PaymentSuccessTemplateVariables | DaysLimitAlmostReachedTemplateVariables From b518d557161bf72ae268c94f7a4f65d56e9c8cb8 Mon Sep 17 00:00:00 2001 From: Taly Date: Fri, 5 Dec 2025 23:43:56 +0300 Subject: [PATCH 15/40] Update provider.ts --- workers/email/src/provider.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/workers/email/src/provider.ts b/workers/email/src/provider.ts index b8d0ce478..9ffaba12c 100644 --- a/workers/email/src/provider.ts +++ b/workers/email/src/provider.ts @@ -44,6 +44,7 @@ export default class EmailProvider extends NotificationsProvider { switch (notification.type) { case 'assignee': templateName = Templates.Assignee; break; case 'block-workspace': templateName = Templates.BlockWorkspace; break; + case 'blocked-workspace-reminder': templateName = Templates.BlockedWorkspaceReminder; break; case 'days-limit-almost-reached': templateName = Templates.DaysLimitAlmostReached; break; case 'event': templateName = Templates.Event; break; case 'events-limit-almost-reached': templateName = Templates.EventsLimitAlmostReached; break; From ec8e589fc27761790448d017ba40ff3e31cd5234 Mon Sep 17 00:00:00 2001 From: Taly Date: Fri, 5 Dec 2025 23:58:24 +0300 Subject: [PATCH 16/40] Add documentation to calculateDaysAfterPayday method Added JSDoc comments to the calculateDaysAfterPayday method in EmailTestServer to clarify its purpose and usage. This improves code readability and maintainability. --- workers/email/scripts/emailOverview.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/workers/email/scripts/emailOverview.ts b/workers/email/scripts/emailOverview.ts index 6836a6a07..2514780e7 100644 --- a/workers/email/scripts/emailOverview.ts +++ b/workers/email/scripts/emailOverview.ts @@ -324,9 +324,20 @@ class EmailTestServer { return connection.collection('workspaces').findOne({ _id: new ObjectId(workspaceId) }); } + /** + * Calculate days after payday + * Return number of days after payday. If payday is in the future, return 0 + * + * @param workspace - workspace data + * @returns number of days after payday + */ private async calculateDaysAfterPayday( workspace: WorkspaceDBScheme ): Promise { + /** + * Calculate number of days after payday + * If workspace.paidUntil is in the future, return 0 + */ if (!workspace.paidUntil) { return 0; } From 8d12c5442a51f78be36dfee6ce12f69b8f307f83 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 00:00:01 +0300 Subject: [PATCH 17/40] Simplify pluralization logic in blocked workspace emails Removed redundant ternary check for daysAfterPayday in pluralize_ru calls across HTML, subject, and text templates for blocked workspace reminder emails. This streamlines the template code and ensures consistent pluralization. --- .../src/templates/emails/blocked-workspace-reminder/html.twig | 2 +- .../templates/emails/blocked-workspace-reminder/subject.twig | 2 +- .../src/templates/emails/blocked-workspace-reminder/text.twig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig index 25e3c7b9e..f1d1dee5d 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig @@ -14,7 +14,7 @@ - {{ daysAfterPayday }} {{ daysAfterPayday ? pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} без мониторинга + {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} без мониторинга diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig index fb2e57393..d0ba2e000 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig @@ -1 +1 @@ -Вы уже {{ daysAfterPayday }} {{ daysAfterPayday ? pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки \ No newline at end of file +Вы уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки \ No newline at end of file diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig index 24a3c203f..d70f2af67 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig @@ -1,4 +1,4 @@ -Вы уже {{ daysAfterPayday }} {{ daysAfterPayday ? pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки. +Вы уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки. Напоминаем, что мониторинг ошибок в «{{ workspace.name | escape }}» всё ещё остановлен, потому что закончился лимит или срок действия тарифного плана. From 3823bd5a17ca96c221297fd18a0a7e5b4d601d1a Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 00:01:34 +0300 Subject: [PATCH 18/40] Update workers/email/scripts/emailOverview.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- workers/email/scripts/emailOverview.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/workers/email/scripts/emailOverview.ts b/workers/email/scripts/emailOverview.ts index 2514780e7..be1612f3e 100644 --- a/workers/email/scripts/emailOverview.ts +++ b/workers/email/scripts/emailOverview.ts @@ -346,7 +346,6 @@ class EmailTestServer { const paidUntil = new Date(workspace.paidUntil); const diffTime = now.getTime() - paidUntil.getTime(); - if (diffTime <= 0) { return 0; } From b38e43953007b487065ff5de9cfe751d4bec1036 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 00:02:02 +0300 Subject: [PATCH 19/40] Update workers/paymaster/tests/index.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- workers/paymaster/tests/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index eea7187fe..abc25c8bf 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -257,7 +257,7 @@ describe('PaymasterWorker', () => { MockDate.reset(); }); - test('Should remind admins for blocked workspace if it has subscription and after payday passed 3 days', async () => { + test('Should remind admins for blocked workspace if it has subscription and after payday passed 5 days', async () => { /** * Arrange */ From 9760aca8f0add399a6cdc6e26c9ce96f4de70728 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 00:02:28 +0300 Subject: [PATCH 20/40] Update workers/paymaster/src/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- workers/paymaster/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index 69c481044..3bf3a28d4 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -424,7 +424,7 @@ export default class PaymasterWorker extends Worker { */ private async sendBlockedWorkspaceReminders( workspace: WorkspaceDBScheme, - daysAfterPayday: number = null + daysAfterPayday: number ): Promise { await this.addTask(WorkerNames.EMAIL, { type: 'blocked-workspace-reminder', From 597303b43f8219ecc161d101ff45e2e87355beff Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 00:02:43 +0300 Subject: [PATCH 21/40] Update workers/paymaster/src/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- workers/paymaster/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index 3bf3a28d4..166e282e5 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -430,7 +430,7 @@ export default class PaymasterWorker extends Worker { type: 'blocked-workspace-reminder', payload: { workspaceId: workspace._id.toString(), - daysAfterPayday: daysAfterPayday, + daysAfterPayday, }, }); } From 41b0bd7082909c957e07548bae52dd0bd58f8a53 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 11:27:17 +0300 Subject: [PATCH 22/40] Update workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../templates/emails/blocked-workspace-reminder/subject.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig index d0ba2e000..d29a0e74c 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig @@ -1 +1 @@ -Вы уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки \ No newline at end of file +Вы уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }} не получаете новые ошибки \ No newline at end of file From 09464e00ec8fc207504b0b84e658a02191f86db1 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 11:27:41 +0300 Subject: [PATCH 23/40] Update workers/email/src/templates/emails/blocked-workspace-reminder/text.twig Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/templates/emails/blocked-workspace-reminder/text.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig index d70f2af67..f28ddd5c7 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig @@ -1,4 +1,4 @@ -Вы уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки. +Вы уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }} не получаете новые ошибки. Напоминаем, что мониторинг ошибок в «{{ workspace.name | escape }}» всё ещё остановлен, потому что закончился лимит или срок действия тарифного плана. From e49c1e83d2319a33e5abfc4a4b9599d9c4aa87b2 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 11:28:00 +0300 Subject: [PATCH 24/40] Update workers/email/src/templates/emails/blocked-workspace-reminder/html.twig Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/templates/emails/blocked-workspace-reminder/html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig index f1d1dee5d..80828db6f 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig @@ -14,7 +14,7 @@ - {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} без мониторинга + {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }} без мониторинга From 05fea23257ae483cb3ae6a33bdae680bf76e4f2b Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 11:28:14 +0300 Subject: [PATCH 25/40] Update workers/sender/types/sender-task/blocked-workspace-reminder.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- workers/sender/types/sender-task/blocked-workspace-reminder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/sender/types/sender-task/blocked-workspace-reminder.ts b/workers/sender/types/sender-task/blocked-workspace-reminder.ts index dc75e124e..67e25e7a8 100644 --- a/workers/sender/types/sender-task/blocked-workspace-reminder.ts +++ b/workers/sender/types/sender-task/blocked-workspace-reminder.ts @@ -25,5 +25,5 @@ export interface SenderWorkerBlockedWorkspaceReminderTask { /** * Payload for task for blocked workspace reminder */ - payload: SenderWorkerBlockedWorkspaceReminderPayload + payload: SenderWorkerBlockedWorkspaceReminderPayload; } From 2553e95ad561e726d191735e3477149641f6c5b5 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 11:28:36 +0300 Subject: [PATCH 26/40] Update workers/sender/types/sender-task/blocked-workspace-reminder.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- workers/sender/types/sender-task/blocked-workspace-reminder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/sender/types/sender-task/blocked-workspace-reminder.ts b/workers/sender/types/sender-task/blocked-workspace-reminder.ts index 67e25e7a8..df502a67d 100644 --- a/workers/sender/types/sender-task/blocked-workspace-reminder.ts +++ b/workers/sender/types/sender-task/blocked-workspace-reminder.ts @@ -20,7 +20,7 @@ export interface SenderWorkerBlockedWorkspaceReminderTask { /** * Task for blocked workspace reminder */ - type: 'blocked-workspace-reminder', + type: 'blocked-workspace-reminder'; /** * Payload for task for blocked workspace reminder From 6c668bc36b564481a923079cdae8707d3610c302 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 14:19:40 +0300 Subject: [PATCH 27/40] Add lastChargeDate fallback to calculateDaysAfterPayday in email overview (#485) * Initial plan * Add fallback logic for lastChargeDate in calculateDaysAfterPayday Co-authored-by: talyguryn <15259299+talyguryn@users.noreply.github.com> * Remove accidentally committed build artifacts Co-authored-by: talyguryn <15259299+talyguryn@users.noreply.github.com> * Remove env-test.js build artifact Co-authored-by: talyguryn <15259299+talyguryn@users.noreply.github.com> * Fix setMonth mutation issue in calculateDaysAfterPayday Co-authored-by: talyguryn <15259299+talyguryn@users.noreply.github.com> * Clean up build artifacts Co-authored-by: talyguryn <15259299+talyguryn@users.noreply.github.com> * Move MILLISECONDS_IN_DAY to module level Co-authored-by: talyguryn <15259299+talyguryn@users.noreply.github.com> * Stop tracking build artifacts and update gitignore Co-authored-by: talyguryn <15259299+talyguryn@users.noreply.github.com> * Fix grammar in MILLISECONDS_IN_DAY comment Co-authored-by: talyguryn <15259299+talyguryn@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: talyguryn <15259299+talyguryn@users.noreply.github.com> --- .gitignore | 12 +++++++++++ workers/email/scripts/emailOverview.ts | 30 +++++++++++++++++++------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index cb3f9358e..b4b457984 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,15 @@ coverage .DS_Store globalConfig.json *.log +*.js +!jest.config.js +!jest.global-teardown.js +!jest.setup.js +!jest.setup.mongo-repl-set.js +!jest.setup.redis-mock.js +!jest-mongodb-config.js +!migrate-mongo-config.js +!/env.js +!convertors/**/*.js +!tools/**/*.js +!bin/**/*.js diff --git a/workers/email/scripts/emailOverview.ts b/workers/email/scripts/emailOverview.ts index be1612f3e..9b6877200 100644 --- a/workers/email/scripts/emailOverview.ts +++ b/workers/email/scripts/emailOverview.ts @@ -18,7 +18,7 @@ import { GroupedEventDBScheme, ProjectDBScheme, UserDBScheme, WorkspaceDBScheme import { ObjectId } from 'mongodb'; import * as path from 'path'; import * as dotenv from 'dotenv'; -import { HttpStatusCode } from '../../../lib/utils/consts'; +import { HttpStatusCode, HOURS_IN_DAY, MINUTES_IN_HOUR, SECONDS_IN_MINUTE, MS_IN_SEC } from '../../../lib/utils/consts'; /** * Merge email worker .env and root workers .env @@ -28,6 +28,11 @@ const localEnv = dotenv.config({ path: path.resolve(__dirname, '../.env') }).par Object.assign(process.env, rootEnv, localEnv); +/** + * Milliseconds in day. Needed for calculating difference between dates in days. + */ +const MILLISECONDS_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MS_IN_SEC; + /** * Server for rendering email templates */ @@ -329,30 +334,39 @@ class EmailTestServer { * Return number of days after payday. If payday is in the future, return 0 * * @param workspace - workspace data - * @returns number of days after payday + * @returns {Promise} number of days after payday */ private async calculateDaysAfterPayday( workspace: WorkspaceDBScheme ): Promise { /** * Calculate number of days after payday - * If workspace.paidUntil is in the future, return 0 + * The expected payday is either paidUntil or lastChargeDate + 1 month + * This follows the same logic as PaymasterWorker */ - if (!workspace.paidUntil) { + let expectedPayDay: Date | null = null; + + if (workspace.paidUntil) { + expectedPayDay = new Date(workspace.paidUntil); + } else if (workspace.lastChargeDate) { + const lastCharge = new Date(workspace.lastChargeDate); + + expectedPayDay = new Date(lastCharge.getFullYear(), lastCharge.getMonth() + 1, lastCharge.getDate()); + } + + if (!expectedPayDay) { return 0; } const now = new Date(); - const paidUntil = new Date(workspace.paidUntil); - const diffTime = now.getTime() - paidUntil.getTime(); + const diffTime = now.getTime() - expectedPayDay.getTime(); if (diffTime <= 0) { return 0; } // Calculate difference in days - // eslint-disable-next-line @typescript-eslint/no-magic-numbers - const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + const diffDays = Math.floor(diffTime / MILLISECONDS_IN_DAY); return diffDays; } From 6c51cee867e6f8a45eaa038943523e93d11ca3f7 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 14:19:56 +0300 Subject: [PATCH 28/40] Expand test coverage for blocked workspace reminder days (#486) * Initial plan * Add comprehensive test coverage for blocked workspace reminder days Co-authored-by: talyguryn <15259299+talyguryn@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: talyguryn <15259299+talyguryn@users.noreply.github.com> --- workers/paymaster/tests/index.test.ts | 141 ++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index abc25c8bf..f7945f69c 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -257,6 +257,53 @@ describe('PaymasterWorker', () => { MockDate.reset(); }); + test('Should remind admins for blocked workspace if it has subscription and after payday passed 1 day', async () => { + /** + * Arrange + */ + const currentDate = new Date('2005-12-23'); + const plan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + const workspace = createWorkspaceMock({ + plan, + subscriptionId: 'some-subscription-id', + lastChargeDate: new Date('2005-11-22'), + isBlocked: true, + billingPeriodEventsCount: 10, + }); + + await fillDatabaseWithMockedData({ + workspace, + plan, + }); + + MockDate.set(currentDate); + + /** + * Act + */ + const worker = new PaymasterWorker(); + const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); + + await worker.start(); + await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); + await worker.finish(); + + /** + * Assert + */ + expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { + type: 'blocked-workspace-reminder', + payload: { + workspaceId: workspace._id.toString(), + daysAfterPayday: 1, + }, + }); + MockDate.reset(); + }); + test('Should remind admins for blocked workspace if it has subscription and after payday passed 5 days', async () => { /** * Arrange @@ -304,6 +351,100 @@ describe('PaymasterWorker', () => { MockDate.reset(); }); + test('Should remind admins for blocked workspace if it has subscription and after payday passed 30 days', async () => { + /** + * Arrange + */ + const currentDate = new Date('2006-01-21'); + const plan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + const workspace = createWorkspaceMock({ + plan, + subscriptionId: 'some-subscription-id', + lastChargeDate: new Date('2005-11-22'), + isBlocked: true, + billingPeriodEventsCount: 10, + }); + + await fillDatabaseWithMockedData({ + workspace, + plan, + }); + + MockDate.set(currentDate); + + /** + * Act + */ + const worker = new PaymasterWorker(); + const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); + + await worker.start(); + await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); + await worker.finish(); + + /** + * Assert + */ + expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { + type: 'blocked-workspace-reminder', + payload: { + workspaceId: workspace._id.toString(), + daysAfterPayday: 30, + }, + }); + MockDate.reset(); + }); + + test('Should not remind admins for blocked workspace on days not in reminder schedule (day 4)', async () => { + /** + * Arrange + */ + const currentDate = new Date('2005-12-26'); + const plan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + const workspace = createWorkspaceMock({ + plan, + subscriptionId: 'some-subscription-id', + lastChargeDate: new Date('2005-11-22'), + isBlocked: true, + billingPeriodEventsCount: 10, + }); + + await fillDatabaseWithMockedData({ + workspace, + plan, + }); + + MockDate.set(currentDate); + + /** + * Act + */ + const worker = new PaymasterWorker(); + const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); + + await worker.start(); + await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); + await worker.finish(); + + /** + * Assert + */ + expect(blockWorkspaceSpy).not.toHaveBeenCalledWith('sender/email', { + type: 'blocked-workspace-reminder', + payload: { + workspaceId: workspace._id.toString(), + daysAfterPayday: 4, + }, + }); + MockDate.reset(); + }); + test('Should update lastChargeDate and billingPeriodEventsCount if workspace has free tariff plan and it\'s time to pay', async () => { /** * Arrange From cc3aa5894cbc0bc52d0c483c6071d4e617d780d7 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 17:53:20 +0300 Subject: [PATCH 29/40] Update blocked workspace reminder email text Revised subject and body to emphasize required action and clarify that error monitoring is not working. The new wording provides clearer instructions for users to renew their subscription. --- .../templates/emails/blocked-workspace-reminder/subject.twig | 2 +- .../src/templates/emails/blocked-workspace-reminder/text.twig | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig index d0ba2e000..d6f9941c3 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig @@ -1 +1 @@ -Вы уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки \ No newline at end of file +Требуется действие: мониторинг ошибок в {{workspace.name }} не работает уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }} \ No newline at end of file diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig index d70f2af67..564f8572c 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig @@ -1,6 +1,4 @@ -Вы уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) : '' }} не получаете новые ошибки. - -Напоминаем, что мониторинг ошибок в «{{ workspace.name | escape }}» всё ещё остановлен, потому что закончился лимит или срок действия тарифного плана. +Требуется действие: мониторинг ошибок в {{workspace.name }} не работает уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }} Чтобы снова видеть актуальные события, выберите подходящий тарифный план и продлите подписку в настройках оплаты: {{ host }}/workspace/{{ workspace._id }}/settings/billing From e2097909c55c84e710ce20f870589862dcf6e72d Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 17:57:51 +0300 Subject: [PATCH 30/40] Group blocked workspace reminder tests Refactored blocked workspace reminder tests into a dedicated describe block for better organization and readability. --- workers/paymaster/tests/index.test.ts | 372 +++++++++++++------------- 1 file changed, 187 insertions(+), 185 deletions(-) diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index f7945f69c..ad793c254 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -257,192 +257,194 @@ describe('PaymasterWorker', () => { MockDate.reset(); }); - test('Should remind admins for blocked workspace if it has subscription and after payday passed 1 day', async () => { - /** - * Arrange - */ - const currentDate = new Date('2005-12-23'); - const plan = createPlanMock({ - monthlyCharge: 100, - isDefault: true, - }); - const workspace = createWorkspaceMock({ - plan, - subscriptionId: 'some-subscription-id', - lastChargeDate: new Date('2005-11-22'), - isBlocked: true, - billingPeriodEventsCount: 10, - }); - - await fillDatabaseWithMockedData({ - workspace, - plan, - }); - - MockDate.set(currentDate); - - /** - * Act - */ - const worker = new PaymasterWorker(); - const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); - - await worker.start(); - await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); - await worker.finish(); - - /** - * Assert - */ - expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { - type: 'blocked-workspace-reminder', - payload: { - workspaceId: workspace._id.toString(), - daysAfterPayday: 1, - }, - }); - MockDate.reset(); - }); - - test('Should remind admins for blocked workspace if it has subscription and after payday passed 5 days', async () => { - /** - * Arrange - */ - const currentDate = new Date('2005-12-27'); - const plan = createPlanMock({ - monthlyCharge: 100, - isDefault: true, - }); - const workspace = createWorkspaceMock({ - plan, - subscriptionId: 'some-subscription-id', - lastChargeDate: new Date('2005-11-22'), - isBlocked: true, - billingPeriodEventsCount: 10, - }); - - await fillDatabaseWithMockedData({ - workspace, - plan, - }); - - MockDate.set(currentDate); - - /** - * Act - */ - const worker = new PaymasterWorker(); - const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); - - await worker.start(); - await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); - await worker.finish(); - - /** - * Assert - */ - expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { - type: 'blocked-workspace-reminder', - payload: { - workspaceId: workspace._id.toString(), - daysAfterPayday: 5, - }, - }); - MockDate.reset(); - }); - - test('Should remind admins for blocked workspace if it has subscription and after payday passed 30 days', async () => { - /** - * Arrange - */ - const currentDate = new Date('2006-01-21'); - const plan = createPlanMock({ - monthlyCharge: 100, - isDefault: true, - }); - const workspace = createWorkspaceMock({ - plan, - subscriptionId: 'some-subscription-id', - lastChargeDate: new Date('2005-11-22'), - isBlocked: true, - billingPeriodEventsCount: 10, - }); - - await fillDatabaseWithMockedData({ - workspace, - plan, - }); - - MockDate.set(currentDate); - - /** - * Act - */ - const worker = new PaymasterWorker(); - const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); - - await worker.start(); - await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); - await worker.finish(); - - /** - * Assert - */ - expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { - type: 'blocked-workspace-reminder', - payload: { - workspaceId: workspace._id.toString(), - daysAfterPayday: 30, - }, - }); - MockDate.reset(); - }); - - test('Should not remind admins for blocked workspace on days not in reminder schedule (day 4)', async () => { - /** - * Arrange - */ - const currentDate = new Date('2005-12-26'); - const plan = createPlanMock({ - monthlyCharge: 100, - isDefault: true, - }); - const workspace = createWorkspaceMock({ - plan, - subscriptionId: 'some-subscription-id', - lastChargeDate: new Date('2005-11-22'), - isBlocked: true, - billingPeriodEventsCount: 10, - }); - - await fillDatabaseWithMockedData({ - workspace, - plan, - }); - - MockDate.set(currentDate); - - /** - * Act - */ - const worker = new PaymasterWorker(); - const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); - - await worker.start(); - await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); - await worker.finish(); - - /** - * Assert - */ - expect(blockWorkspaceSpy).not.toHaveBeenCalledWith('sender/email', { - type: 'blocked-workspace-reminder', - payload: { - workspaceId: workspace._id.toString(), - daysAfterPayday: 4, - }, + describe('Blocked workspace reminder tests', () => { + test('Should remind admins for blocked workspace if it has subscription and after payday passed 1 day', async () => { + /** + * Arrange + */ + const currentDate = new Date('2005-12-23'); + const plan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + const workspace = createWorkspaceMock({ + plan, + subscriptionId: 'some-subscription-id', + lastChargeDate: new Date('2005-11-22'), + isBlocked: true, + billingPeriodEventsCount: 10, + }); + + await fillDatabaseWithMockedData({ + workspace, + plan, + }); + + MockDate.set(currentDate); + + /** + * Act + */ + const worker = new PaymasterWorker(); + const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); + + await worker.start(); + await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); + await worker.finish(); + + /** + * Assert + */ + expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { + type: 'blocked-workspace-reminder', + payload: { + workspaceId: workspace._id.toString(), + daysAfterPayday: 1, + }, + }); + MockDate.reset(); + }); + + test('Should remind admins for blocked workspace if it has subscription and after payday passed 5 days', async () => { + /** + * Arrange + */ + const currentDate = new Date('2005-12-27'); + const plan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + const workspace = createWorkspaceMock({ + plan, + subscriptionId: 'some-subscription-id', + lastChargeDate: new Date('2005-11-22'), + isBlocked: true, + billingPeriodEventsCount: 10, + }); + + await fillDatabaseWithMockedData({ + workspace, + plan, + }); + + MockDate.set(currentDate); + + /** + * Act + */ + const worker = new PaymasterWorker(); + const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); + + await worker.start(); + await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); + await worker.finish(); + + /** + * Assert + */ + expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { + type: 'blocked-workspace-reminder', + payload: { + workspaceId: workspace._id.toString(), + daysAfterPayday: 5, + }, + }); + MockDate.reset(); + }); + + test('Should remind admins for blocked workspace if it has subscription and after payday passed 30 days', async () => { + /** + * Arrange + */ + const currentDate = new Date('2006-01-21'); + const plan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + const workspace = createWorkspaceMock({ + plan, + subscriptionId: 'some-subscription-id', + lastChargeDate: new Date('2005-11-22'), + isBlocked: true, + billingPeriodEventsCount: 10, + }); + + await fillDatabaseWithMockedData({ + workspace, + plan, + }); + + MockDate.set(currentDate); + + /** + * Act + */ + const worker = new PaymasterWorker(); + const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); + + await worker.start(); + await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); + await worker.finish(); + + /** + * Assert + */ + expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { + type: 'blocked-workspace-reminder', + payload: { + workspaceId: workspace._id.toString(), + daysAfterPayday: 30, + }, + }); + MockDate.reset(); + }); + + test('Should not remind admins for blocked workspace on days not in reminder schedule (day 4)', async () => { + /** + * Arrange + */ + const currentDate = new Date('2005-12-26'); + const plan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + const workspace = createWorkspaceMock({ + plan, + subscriptionId: 'some-subscription-id', + lastChargeDate: new Date('2005-11-22'), + isBlocked: true, + billingPeriodEventsCount: 10, + }); + + await fillDatabaseWithMockedData({ + workspace, + plan, + }); + + MockDate.set(currentDate); + + /** + * Act + */ + const worker = new PaymasterWorker(); + const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); + + await worker.start(); + await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); + await worker.finish(); + + /** + * Assert + */ + expect(blockWorkspaceSpy).not.toHaveBeenCalledWith('sender/email', { + type: 'blocked-workspace-reminder', + payload: { + workspaceId: workspace._id.toString(), + daysAfterPayday: 4, + }, + }); + MockDate.reset(); }); - MockDate.reset(); }); test('Should update lastChargeDate and billingPeriodEventsCount if workspace has free tariff plan and it\'s time to pay', async () => { From cf3dcd3147d8b7cc5235c7adf96d607d77888547 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 17:58:59 +0300 Subject: [PATCH 31/40] Refactor blocked workspace reminder tests Consolidated repetitive test logic for blocked workspace reminders into a reusable helper function. This improves maintainability and readability of the test suite by reducing code duplication. --- workers/paymaster/tests/index.test.ts | 242 ++++++++------------------ 1 file changed, 74 insertions(+), 168 deletions(-) diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index ad793c254..a94f6b44e 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -257,195 +257,101 @@ describe('PaymasterWorker', () => { MockDate.reset(); }); - describe('Blocked workspace reminder tests', () => { - test('Should remind admins for blocked workspace if it has subscription and after payday passed 1 day', async () => { - /** - * Arrange - */ - const currentDate = new Date('2005-12-23'); - const plan = createPlanMock({ - monthlyCharge: 100, - isDefault: true, - }); - const workspace = createWorkspaceMock({ - plan, - subscriptionId: 'some-subscription-id', - lastChargeDate: new Date('2005-11-22'), - isBlocked: true, - billingPeriodEventsCount: 10, - }); + /** + * Helper function to run blocked workspace reminder test + * + * @param lastChargeDate - date of last charge + * @param currentDate - current date to test + * @param shouldBeCalled - whether the reminder should be called + * @param expectedDaysAfterPayday - expected days after payday in the call + */ + const testBlockedWorkspaceReminder = async ( + lastChargeDate: Date, + currentDate: Date, + shouldBeCalled: boolean, + expectedDaysAfterPayday?: number + ): Promise => { + const plan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + const workspace = createWorkspaceMock({ + plan, + subscriptionId: 'some-subscription-id', + lastChargeDate, + isBlocked: true, + billingPeriodEventsCount: 10, + }); - await fillDatabaseWithMockedData({ - workspace, - plan, - }); + await fillDatabaseWithMockedData({ + workspace, + plan, + }); - MockDate.set(currentDate); + MockDate.set(currentDate); - /** - * Act - */ - const worker = new PaymasterWorker(); - const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); + const worker = new PaymasterWorker(); + const addTaskSpy = jest.spyOn(worker, 'addTask'); - await worker.start(); - await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); - await worker.finish(); + await worker.start(); + await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); + await worker.finish(); - /** - * Assert - */ - expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { + if (shouldBeCalled) { + expect(addTaskSpy).toHaveBeenCalledWith('sender/email', { type: 'blocked-workspace-reminder', payload: { workspaceId: workspace._id.toString(), - daysAfterPayday: 1, + daysAfterPayday: expectedDaysAfterPayday, }, }); - MockDate.reset(); - }); - - test('Should remind admins for blocked workspace if it has subscription and after payday passed 5 days', async () => { - /** - * Arrange - */ - const currentDate = new Date('2005-12-27'); - const plan = createPlanMock({ - monthlyCharge: 100, - isDefault: true, - }); - const workspace = createWorkspaceMock({ - plan, - subscriptionId: 'some-subscription-id', - lastChargeDate: new Date('2005-11-22'), - isBlocked: true, - billingPeriodEventsCount: 10, - }); - - await fillDatabaseWithMockedData({ - workspace, - plan, - }); - - MockDate.set(currentDate); + } else { + expect(addTaskSpy).not.toHaveBeenCalledWith('sender/email', expect.objectContaining({ + type: 'blocked-workspace-reminder', + })); + } - /** - * Act - */ - const worker = new PaymasterWorker(); - const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); + MockDate.reset(); + return addTaskSpy; + }; - await worker.start(); - await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); - await worker.finish(); + describe('Blocked workspace reminder tests', () => { + test('Should remind admins for blocked workspace if it has subscription and after payday passed 1 day', async () => { + await testBlockedWorkspaceReminder( + new Date('2005-11-22'), + new Date('2005-12-23'), + true, + 1 + ); + }); - /** - * Assert - */ - expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { - type: 'blocked-workspace-reminder', - payload: { - workspaceId: workspace._id.toString(), - daysAfterPayday: 5, - }, - }); - MockDate.reset(); + test('Should remind admins for blocked workspace if it has subscription and after payday passed 5 days', async () => { + await testBlockedWorkspaceReminder( + new Date('2005-11-22'), + new Date('2005-12-27'), + true, + 5 + ); }); test('Should remind admins for blocked workspace if it has subscription and after payday passed 30 days', async () => { - /** - * Arrange - */ - const currentDate = new Date('2006-01-21'); - const plan = createPlanMock({ - monthlyCharge: 100, - isDefault: true, - }); - const workspace = createWorkspaceMock({ - plan, - subscriptionId: 'some-subscription-id', - lastChargeDate: new Date('2005-11-22'), - isBlocked: true, - billingPeriodEventsCount: 10, - }); - - await fillDatabaseWithMockedData({ - workspace, - plan, - }); - - MockDate.set(currentDate); - - /** - * Act - */ - const worker = new PaymasterWorker(); - const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); - - await worker.start(); - await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); - await worker.finish(); - - /** - * Assert - */ - expect(blockWorkspaceSpy).toHaveBeenCalledWith('sender/email', { - type: 'blocked-workspace-reminder', - payload: { - workspaceId: workspace._id.toString(), - daysAfterPayday: 30, - }, - }); - MockDate.reset(); + await testBlockedWorkspaceReminder( + new Date('2005-11-22'), + new Date('2006-01-21'), + true, + 30 + ); }); test('Should not remind admins for blocked workspace on days not in reminder schedule (day 4)', async () => { - /** - * Arrange - */ - const currentDate = new Date('2005-12-26'); - const plan = createPlanMock({ - monthlyCharge: 100, - isDefault: true, - }); - const workspace = createWorkspaceMock({ - plan, - subscriptionId: 'some-subscription-id', - lastChargeDate: new Date('2005-11-22'), - isBlocked: true, - billingPeriodEventsCount: 10, - }); - - await fillDatabaseWithMockedData({ - workspace, - plan, - }); - - MockDate.set(currentDate); - - /** - * Act - */ - const worker = new PaymasterWorker(); - const blockWorkspaceSpy = jest.spyOn(worker, 'addTask'); - - await worker.start(); - await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); - await worker.finish(); - - /** - * Assert - */ - expect(blockWorkspaceSpy).not.toHaveBeenCalledWith('sender/email', { - type: 'blocked-workspace-reminder', - payload: { - workspaceId: workspace._id.toString(), - daysAfterPayday: 4, - }, - }); - MockDate.reset(); + await testBlockedWorkspaceReminder( + new Date('2005-11-22'), + new Date('2005-12-26'), + false + ); }); }); + }); test('Should update lastChargeDate and billingPeriodEventsCount if workspace has free tariff plan and it\'s time to pay', async () => { /** From f8bf2a50c3ba1f064ec01a044923f4602a6e0579 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 18:03:00 +0300 Subject: [PATCH 32/40] Fix extra closing bracket in test suite Removed an unnecessary closing bracket in the PaymasterWorker test suite to correct the test structure. --- workers/paymaster/tests/index.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index a94f6b44e..964f32e06 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -351,7 +351,6 @@ describe('PaymasterWorker', () => { ); }); }); - }); test('Should update lastChargeDate and billingPeriodEventsCount if workspace has free tariff plan and it\'s time to pay', async () => { /** From 25b910a166fa9ff8549720de615c5a1dc3067f7c Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 18:53:23 +0300 Subject: [PATCH 33/40] Refactor payday calculation logic to shared utility Moved daysBeforePayday and daysAfterPayday functions to a new shared utility file (lib/utils/payday.ts) and updated emailOverview and paymaster worker to use these functions. This reduces code duplication and centralizes payday-related calculations. --- lib/utils/payday.ts | 52 +++++++++++++++++++ workers/email/scripts/emailOverview.ts | 37 ++------------ workers/paymaster/src/index.ts | 69 ++++---------------------- 3 files changed, 66 insertions(+), 92 deletions(-) create mode 100644 lib/utils/payday.ts diff --git a/lib/utils/payday.ts b/lib/utils/payday.ts new file mode 100644 index 000000000..0826c8169 --- /dev/null +++ b/lib/utils/payday.ts @@ -0,0 +1,52 @@ +import { HOURS_IN_DAY, MINUTES_IN_HOUR, SECONDS_IN_MINUTE, MS_IN_SEC } from './consts'; + +/** + * Milliseconds in day. Needs for calculating difference between dates in days. + */ +const MILLISECONDS_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MS_IN_SEC; + +/** + * Returns difference between now and payday in days + * + * Pay day is calculated by formula: paidUntil date or last charge date + 1 month + * + * @param date - last charge date + * @param paidUntil - paid until date + * @param isDebug - flag for debug purposes + */ +export function daysBeforePayday(date: Date, paidUntil: Date = null, isDebug = false): number { + const expectedPayDay = paidUntil ? new Date(paidUntil) : new Date(date); + + if (isDebug) { + expectedPayDay.setDate(date.getDate() + 1); + } else if (!paidUntil) { + expectedPayDay.setMonth(date.getMonth() + 1); + } + + const now = new Date().getTime(); + + return Math.floor((expectedPayDay.getTime() - now) / MILLISECONDS_IN_DAY); +} + +/** + * Returns difference between payday and now in days + * + * Pay day is calculated by formula: paidUntil date or last charge date + 1 month + * + * @param date - last charge date + * @param paidUntil - paid until date + * @param isDebug - flag for debug purposes + */ +export function daysAfterPayday(date: Date, paidUntil: Date = null, isDebug = false): number { + const expectedPayDay = paidUntil ? new Date(paidUntil) : new Date(date); + + if (isDebug) { + expectedPayDay.setDate(date.getDate() + 1); + } else if (!paidUntil) { + expectedPayDay.setMonth(date.getMonth() + 1); + } + + const now = new Date().getTime(); + + return Math.floor((now - expectedPayDay.getTime()) / MILLISECONDS_IN_DAY); +} \ No newline at end of file diff --git a/workers/email/scripts/emailOverview.ts b/workers/email/scripts/emailOverview.ts index 9b6877200..aa31a0998 100644 --- a/workers/email/scripts/emailOverview.ts +++ b/workers/email/scripts/emailOverview.ts @@ -18,7 +18,8 @@ import { GroupedEventDBScheme, ProjectDBScheme, UserDBScheme, WorkspaceDBScheme import { ObjectId } from 'mongodb'; import * as path from 'path'; import * as dotenv from 'dotenv'; -import { HttpStatusCode, HOURS_IN_DAY, MINUTES_IN_HOUR, SECONDS_IN_MINUTE, MS_IN_SEC } from '../../../lib/utils/consts'; +import { HttpStatusCode } from '../../../lib/utils/consts'; +import { daysAfterPayday } from '../../../lib/utils/payday'; /** * Merge email worker .env and root workers .env @@ -28,11 +29,6 @@ const localEnv = dotenv.config({ path: path.resolve(__dirname, '../.env') }).par Object.assign(process.env, rootEnv, localEnv); -/** - * Milliseconds in day. Needed for calculating difference between dates in days. - */ -const MILLISECONDS_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MS_IN_SEC; - /** * Server for rendering email templates */ @@ -339,36 +335,13 @@ class EmailTestServer { private async calculateDaysAfterPayday( workspace: WorkspaceDBScheme ): Promise { - /** - * Calculate number of days after payday - * The expected payday is either paidUntil or lastChargeDate + 1 month - * This follows the same logic as PaymasterWorker - */ - let expectedPayDay: Date | null = null; - - if (workspace.paidUntil) { - expectedPayDay = new Date(workspace.paidUntil); - } else if (workspace.lastChargeDate) { - const lastCharge = new Date(workspace.lastChargeDate); - - expectedPayDay = new Date(lastCharge.getFullYear(), lastCharge.getMonth() + 1, lastCharge.getDate()); - } - - if (!expectedPayDay) { - return 0; - } - - const now = new Date(); - const diffTime = now.getTime() - expectedPayDay.getTime(); - - if (diffTime <= 0) { + if (!workspace.lastChargeDate) { return 0; } - // Calculate difference in days - const diffDays = Math.floor(diffTime / MILLISECONDS_IN_DAY); + const days = daysAfterPayday(workspace.lastChargeDate, workspace.paidUntil); - return diffDays; + return days > 0 ? days : 0; } /** diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index 166e282e5..a57b54c16 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -7,19 +7,14 @@ import { Collection } from 'mongodb'; import { PlanDBScheme, WorkspaceDBScheme } from '@hawk.so/types'; import { EventType, PaymasterEvent } from '../types/paymaster-worker-events'; import axios from 'axios'; -import { HOURS_IN_DAY, MINUTES_IN_HOUR, MS_IN_SEC, SECONDS_IN_MINUTE } from '../../../lib/utils/consts'; import * as WorkerNames from '../../../lib/workerNames'; import HawkCatcher from '@hawk.so/nodejs'; +import { daysBeforePayday, daysAfterPayday } from '../../../lib/utils/payday'; dotenv.config({ path: path.resolve(__dirname, '../.env'), }); -/** - * Milliseconds in day. Needs for calculating difference between dates in days. - */ -const MILLISECONDS_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MS_IN_SEC; - /** * Days after payday to try paying in actual subscription * When days after payday is more than this const and we still @@ -109,52 +104,6 @@ export default class PaymasterWorker extends Worker { return endDate; } - /** - * Returns difference between now and payday in days - * - * Pay day is calculated by formula: paidUntil date or last charge date + 1 month - * - * @param date - last charge date - * @param paidUntil - paid until date - * @param isDebug - flag for debug purposes - */ - private static daysBeforePayday(date: Date, paidUntil: Date = null, isDebug = false): number { - const expectedPayDay = paidUntil ? new Date(paidUntil) : new Date(date); - - if (isDebug) { - expectedPayDay.setDate(date.getDate() + 1); - } else if (!paidUntil) { - expectedPayDay.setMonth(date.getMonth() + 1); - } - - const now = new Date().getTime(); - - return Math.floor((expectedPayDay.getTime() - now) / MILLISECONDS_IN_DAY); - } - - /** - * Returns difference between payday and now in days - * - * Pay day is calculated by formula: paidUntil date or last charge date + 1 month - * - * @param date - last charge date - * @param paidUntil - paid until date - * @param isDebug - flag for debug purposes - */ - private static daysAfterPayday(date: Date, paidUntil: Date = null, isDebug = false): number { - const expectedPayDay = paidUntil ? new Date(paidUntil) : new Date(date); - - if (isDebug) { - expectedPayDay.setDate(date.getDate() + 1); - } else if (!paidUntil) { - expectedPayDay.setMonth(date.getMonth() + 1); - } - - const now = new Date().getTime(); - - return Math.floor((now - expectedPayDay.getTime()) / MILLISECONDS_IN_DAY); - } - /** * Start consuming messages */ @@ -253,13 +202,13 @@ export default class PaymasterWorker extends Worker { * How many days have passed since payments the expected day of payments */ // @ts-expect-error debug - const daysAfterPayday = PaymasterWorker.daysAfterPayday(workspace.lastChargeDate, workspace.paidUntil, workspace.isDebug); + const daysAfterPaydayValue = daysAfterPayday(workspace.lastChargeDate, workspace.paidUntil, workspace.isDebug); /** * How many days left for the expected day of payments */ // @ts-expect-error debug - const daysLeft = PaymasterWorker.daysBeforePayday(workspace.lastChargeDate, workspace.paidUntil, workspace.isDebug); + const daysLeft = daysBeforePayday(workspace.lastChargeDate, workspace.paidUntil, workspace.isDebug); /** * Do we need to ask for money @@ -330,8 +279,8 @@ export default class PaymasterWorker extends Worker { if (workspace.isBlocked) { // Send reminders on certain days after payday // eslint-disable-next-line @typescript-eslint/no-magic-numbers - if (DAYS_AFTER_PAYDAY_TO_REMIND.includes(daysAfterPayday)) { - await this.sendBlockedWorkspaceReminders(workspace, daysAfterPayday); + if (DAYS_AFTER_PAYDAY_TO_REMIND.includes(daysAfterPaydayValue)) { + await this.sendBlockedWorkspaceReminders(workspace, daysAfterPaydayValue); } return [workspace, true]; @@ -350,7 +299,7 @@ export default class PaymasterWorker extends Worker { * Block workspace if it has paid subscription, * but a few days have passed after payday */ - if (daysAfterPayday > DAYS_AFTER_PAYDAY_TO_TRY_PAYING) { + if (daysAfterPaydayValue > DAYS_AFTER_PAYDAY_TO_TRY_PAYING) { await this.blockWorkspace(workspace); return [workspace, true]; @@ -420,17 +369,17 @@ export default class PaymasterWorker extends Worker { * Sends reminder emails to blocked workspace admins * * @param workspace - workspace to send reminders for - * @param daysAfterPayday - number of days the workspace spent after payday + * @param days - number of days the workspace spent after payday */ private async sendBlockedWorkspaceReminders( workspace: WorkspaceDBScheme, - daysAfterPayday: number + days: number ): Promise { await this.addTask(WorkerNames.EMAIL, { type: 'blocked-workspace-reminder', payload: { workspaceId: workspace._id.toString(), - daysAfterPayday, + daysAfterPayday: days, }, }); } From a65c54e50f71b5fe93e8e92260c4d20945ff2fa4 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 19:02:18 +0300 Subject: [PATCH 34/40] Update workers/sender/src/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- workers/sender/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/sender/src/index.ts b/workers/sender/src/index.ts index fed1c4064..86d015a05 100644 --- a/workers/sender/src/index.ts +++ b/workers/sender/src/index.ts @@ -330,7 +330,7 @@ export default abstract class SenderWorker extends Worker { const admins = await this.getWorkspaceAdmins(workspaceId); if (!admins) { - this.logger.error(`Cannot send blocked workspace reminder notification: workspace team not found. Payload: ${task}`); + this.logger.error(`Cannot send blocked workspace reminder notification: workspace team not found. Payload: ${JSON.stringify(task)}`); return; } From 28617cbab329d437321444a8db35122436e67c1a Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 19:03:15 +0300 Subject: [PATCH 35/40] Fix spacing in blocked workspace email templates Added missing space after '{{' in variable references in subject and text templates for blocked workspace reminder emails to improve readability and consistency. --- .../templates/emails/blocked-workspace-reminder/subject.twig | 2 +- .../src/templates/emails/blocked-workspace-reminder/text.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig index d6f9941c3..e2c4b7a0a 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig @@ -1 +1 @@ -Требуется действие: мониторинг ошибок в {{workspace.name }} не работает уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }} \ No newline at end of file +Требуется действие: мониторинг ошибок в {{ workspace.name }} не работает уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }} \ No newline at end of file diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig index 564f8572c..235015668 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig @@ -1,4 +1,4 @@ -Требуется действие: мониторинг ошибок в {{workspace.name }} не работает уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }} +Требуется действие: мониторинг ошибок в {{ workspace.name }} не работает уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }} Чтобы снова видеть актуальные события, выберите подходящий тарифный план и продлите подписку в настройках оплаты: {{ host }}/workspace/{{ workspace._id }}/settings/billing From e86a2b33e8a0d3986a9d61958c03be9349f9249a Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 19:05:51 +0300 Subject: [PATCH 36/40] Update index.ts --- workers/sender/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/sender/src/index.ts b/workers/sender/src/index.ts index 86d015a05..6ba1702c9 100644 --- a/workers/sender/src/index.ts +++ b/workers/sender/src/index.ts @@ -313,7 +313,7 @@ export default abstract class SenderWorker extends Worker { const workspace = await this.getWorkspace(workspaceId); if (!workspace) { - this.logger.error(`Cannot send blocked workspace reminder notification: workspace not found. Payload: ${task}`); + this.logger.error(`Cannot send blocked workspace reminder notification: workspace not found. Payload: ${JSON.stringify(task)}`); return; } From 29928faf783f18f3294bfd1e398ff08bab07f6fc Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 19:22:38 +0300 Subject: [PATCH 37/40] Move updateLastNoticationDate call outside conditionals Refactored the code to call updateLastNoticationDate after processing notifications, regardless of whether the conditional block is entered. This ensures the last notification date is always updated for each workspace and event type. --- workers/sender/src/index.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/workers/sender/src/index.ts b/workers/sender/src/index.ts index 6ba1702c9..82b5c4ebb 100644 --- a/workers/sender/src/index.ts +++ b/workers/sender/src/index.ts @@ -352,7 +352,11 @@ export default abstract class SenderWorker extends Worker { }, }); } + + await this.updateLastNoticationDate(workspace, eventType); })); + + } /** @@ -414,12 +418,9 @@ export default abstract class SenderWorker extends Worker { daysLeft, }, }); - - /** - * Update last notification data in DB - */ - await this.updateLastNoticationDate(workspace, eventType); } + + await this.updateLastNoticationDate(workspace, eventType); })); } @@ -480,9 +481,9 @@ export default abstract class SenderWorker extends Worker { eventsLimit, }, }); - - await this.updateLastNoticationDate(workspace, eventType); } + + await this.updateLastNoticationDate(workspace, eventType); })); } From 9aa5af2e53323f3b4e58476cb74f7a580d27dd1b Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 19:33:16 +0300 Subject: [PATCH 38/40] Move updateLastNoticationDate call outside Promise.all Refactored the updateLastNoticationDate invocation to occur after Promise.all resolves, ensuring it is called once per method execution rather than for each iteration. --- workers/sender/src/index.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/workers/sender/src/index.ts b/workers/sender/src/index.ts index 82b5c4ebb..d08dc66dc 100644 --- a/workers/sender/src/index.ts +++ b/workers/sender/src/index.ts @@ -352,11 +352,9 @@ export default abstract class SenderWorker extends Worker { }, }); } - - await this.updateLastNoticationDate(workspace, eventType); })); - + await this.updateLastNoticationDate(workspace, eventType); } /** @@ -419,9 +417,9 @@ export default abstract class SenderWorker extends Worker { }, }); } - - await this.updateLastNoticationDate(workspace, eventType); })); + + await this.updateLastNoticationDate(workspace, eventType); } /** @@ -482,9 +480,9 @@ export default abstract class SenderWorker extends Worker { }, }); } - - await this.updateLastNoticationDate(workspace, eventType); })); + + await this.updateLastNoticationDate(workspace, eventType); } /** From 3fb0dbc286b1a69eba35a09e7621df052a82a45e Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 19:41:26 +0300 Subject: [PATCH 39/40] Fix typo in updateLastNotificationDate method name Renamed updateLastNoticationDate to updateLastNotificationDate for consistency and correctness across all usages in SenderWorker. --- workers/sender/src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/workers/sender/src/index.ts b/workers/sender/src/index.ts index d08dc66dc..022f63f60 100644 --- a/workers/sender/src/index.ts +++ b/workers/sender/src/index.ts @@ -354,7 +354,7 @@ export default abstract class SenderWorker extends Worker { } })); - await this.updateLastNoticationDate(workspace, eventType); + await this.updateLastNotificationDate(workspace, eventType); } /** @@ -419,7 +419,7 @@ export default abstract class SenderWorker extends Worker { } })); - await this.updateLastNoticationDate(workspace, eventType); + await this.updateLastNotificationDate(workspace, eventType); } /** @@ -482,7 +482,7 @@ export default abstract class SenderWorker extends Worker { } })); - await this.updateLastNoticationDate(workspace, eventType); + await this.updateLastNotificationDate(workspace, eventType); } /** @@ -772,7 +772,7 @@ export default abstract class SenderWorker extends Worker { * @param {string} type - event type * @param {number} date - date to be set */ - private async updateLastNoticationDate(workspace: WorkspaceDBScheme, type: string, date = new Date()): Promise { + private async updateLastNotificationDate(workspace: WorkspaceDBScheme, type: string, date = new Date()): Promise { /** * Throw an error if workspace is missing */ From 4e082d8b386e45e9507c24c94418006f54b469bc Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 6 Dec 2025 19:54:08 +0300 Subject: [PATCH 40/40] Update blocked-workspace-reminder.ts --- .../types/template-variables/blocked-workspace-reminder.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/workers/sender/types/template-variables/blocked-workspace-reminder.ts b/workers/sender/types/template-variables/blocked-workspace-reminder.ts index 2c4fd61cf..9b3aaa96b 100644 --- a/workers/sender/types/template-variables/blocked-workspace-reminder.ts +++ b/workers/sender/types/template-variables/blocked-workspace-reminder.ts @@ -25,6 +25,7 @@ export interface BlockedWorkspaceReminderNotification extends Notification