Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
2452285
Add blocked workspace reminder email template
talyguryn Dec 2, 2025
130decc
Send reminder emails to blocked workspace admins
talyguryn Dec 2, 2025
a830c39
Fix workspace retrieval and header in EmailTestServer
talyguryn Dec 2, 2025
a02732b
Add comments and eslint disables for magic numbers
talyguryn Dec 2, 2025
91a3014
Update index.test.ts
talyguryn Dec 2, 2025
9a564e0
Update test to use expect.any(Number) for daysAfterPayday
talyguryn Dec 2, 2025
89bac2c
Move pluralize_ru macro from Twig to TypeScript extension
talyguryn Dec 2, 2025
dd71fc5
Refactor blocked workspace reminder days to constant
talyguryn Dec 2, 2025
bbf8538
Fix test dates and assertions in PaymasterWorker tests
talyguryn Dec 2, 2025
e9155f4
Add test for blocked workspace reminder after payday
talyguryn Dec 2, 2025
994ff1e
Rename daysBlocked to daysAfterPayday in reminder method
talyguryn Dec 2, 2025
088e2b6
Add eslint-disable for magic numbers in pluralize_ru
talyguryn Dec 2, 2025
b229817
Update extensions.ts
talyguryn Dec 3, 2025
8dabf96
Add blocked workspace reminder notification support
talyguryn Dec 5, 2025
b518d55
Update provider.ts
talyguryn Dec 5, 2025
ec8e589
Add documentation to calculateDaysAfterPayday method
talyguryn Dec 5, 2025
8d12c54
Simplify pluralization logic in blocked workspace emails
talyguryn Dec 5, 2025
3823bd5
Update workers/email/scripts/emailOverview.ts
talyguryn Dec 5, 2025
b38e439
Update workers/paymaster/tests/index.test.ts
talyguryn Dec 5, 2025
9760aca
Update workers/paymaster/src/index.ts
talyguryn Dec 5, 2025
597303b
Update workers/paymaster/src/index.ts
talyguryn Dec 5, 2025
41b0bd7
Update workers/email/src/templates/emails/blocked-workspace-reminder/…
talyguryn Dec 6, 2025
09464e0
Update workers/email/src/templates/emails/blocked-workspace-reminder/…
talyguryn Dec 6, 2025
e49c1e8
Update workers/email/src/templates/emails/blocked-workspace-reminder/…
talyguryn Dec 6, 2025
05fea23
Update workers/sender/types/sender-task/blocked-workspace-reminder.ts
talyguryn Dec 6, 2025
2553e95
Update workers/sender/types/sender-task/blocked-workspace-reminder.ts
talyguryn Dec 6, 2025
6c668bc
Add lastChargeDate fallback to calculateDaysAfterPayday in email over…
Copilot Dec 6, 2025
6c51cee
Expand test coverage for blocked workspace reminder days (#486)
Copilot Dec 6, 2025
cc3aa58
Update blocked workspace reminder email text
talyguryn Dec 6, 2025
fae76fb
Merge branch 'feat/reminder-email-for-blocked-workspace' of https://g…
talyguryn Dec 6, 2025
e209790
Group blocked workspace reminder tests
talyguryn Dec 6, 2025
cf3dcd3
Refactor blocked workspace reminder tests
talyguryn Dec 6, 2025
f8bf2a5
Fix extra closing bracket in test suite
talyguryn Dec 6, 2025
25b910a
Refactor payday calculation logic to shared utility
talyguryn Dec 6, 2025
a65c54e
Update workers/sender/src/index.ts
talyguryn Dec 6, 2025
28617cb
Fix spacing in blocked workspace email templates
talyguryn Dec 6, 2025
6a8a1bd
Merge branch 'feat/reminder-email-for-blocked-workspace' of https://g…
talyguryn Dec 6, 2025
e86a2b3
Update index.ts
talyguryn Dec 6, 2025
29928fa
Move updateLastNoticationDate call outside conditionals
talyguryn Dec 6, 2025
9aa5af2
Move updateLastNoticationDate call outside Promise.all
talyguryn Dec 6, 2025
3fb0dbc
Fix typo in updateLastNotificationDate method name
talyguryn Dec 6, 2025
4e082d8
Update blocked-workspace-reminder.ts
talyguryn Dec 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
52 changes: 52 additions & 0 deletions lib/utils/payday.ts
Original file line number Diff line number Diff line change
@@ -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);
}
23 changes: 22 additions & 1 deletion workers/email/scripts/emailOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ObjectId } from 'mongodb';
import * as path from 'path';
import * as dotenv from 'dotenv';
import { HttpStatusCode } from '../../../lib/utils/consts';
import { daysAfterPayday } from '../../../lib/utils/payday';

/**
* Merge email worker .env and root workers .env
Expand Down Expand Up @@ -147,6 +148,7 @@ class EmailTestServer {
user,
period: 10,
reason: 'error on the payment server side',
daysAfterPayday: await this.calculateDaysAfterPayday(workspace),
};

try {
Expand Down Expand Up @@ -210,7 +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();
Expand Down Expand Up @@ -323,6 +325,25 @@ 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 {Promise<number>} number of days after payday
*/
private async calculateDaysAfterPayday(
Comment thread
talyguryn marked this conversation as resolved.
workspace: WorkspaceDBScheme
): Promise<number> {
if (!workspace.lastChargeDate) {
return 0;
}

const days = daysAfterPayday(workspace.lastChargeDate, workspace.paidUntil);

return days > 0 ? days : 0;
}
Comment thread
talyguryn marked this conversation as resolved.

/**
* Get user info
*
Expand Down
1 change: 1 addition & 0 deletions workers/email/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{% extends '../../components/layout.twig' %}

{% block header %}
{% include '../../components/workspace.twig' with {workspace: workspace} %}
{% endblock %}

{% block content %}
<tr>
<td>
<img src="{{ hostOfStatic }}/email/low-balance-icon.png" width="32" height="32" hspace="3" style="display: block; vertical-align: middle; margin: 23px auto 0;">
</td>
</tr>
<tr>
<td align="center" style="padding: 15px 0;">
<font color="#dbe6ff" style="font-size: 15px; text-align: center; color: #dbe6ff; letter-spacing: 0.4px;">
<span style="vertical-align: middle; display: inline-block;">
{{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }} без мониторинга
</span>
</font>
</td>
</tr>
<tr>
<td style="display: block; padding: 20px; margin-bottom: 30px; border-width: 1px; border-color: #494f5e; border-style: solid; border-radius: 10px; line-height: 1.47">
<font color="#dbe6ff" style="font-size: 15px; letter-spacing: 0.4px;">
<p style="margin-top: 0;">
Напоминаем, что мониторинг ошибок в «{{ workspace.name | escape }}» всё ещё остановлен, потому что закончился лимит или срок действия тарифного плана.
</p>
<p style="margin-bottom: 0;">
Чтобы снова видеть актуальные события, выберите подходящий тарифный план и продлите подписку в настройках оплаты.
</p>
</font>
</td>
</tr>
<tr>
<td style="padding-right: 20px; padding-left: 20px; padding-bottom: 40px;">
{% include '../../components/button.twig' with {href: host ~ '/workspace/' ~ workspace._id ~ '/settings/billing', label: workspace.tariffPlanId is same as('5f47f031ff71510040f433c1') ? 'Выбрать тариф от 99 ₽' : 'Открыть настройки'} %}
</td>
</tr>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Требуется действие: мониторинг ошибок в {{ workspace.name }} не работает уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Требуется действие: мониторинг ошибок в {{ workspace.name }} не работает уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }}

Чтобы снова видеть актуальные события, выберите подходящий тарифный план и продлите подписку в настройках оплаты: {{ host }}/workspace/{{ workspace._id }}/settings/billing

***

Хоук
Российский трекер ошибок

Made by CodeX
26 changes: 26 additions & 0 deletions workers/email/src/templates/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,29 @@ 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 => {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
if (n % 100 >= 11 && n % 100 <= 19) {
return forms[2];
}

// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const last = n % 10;

if (last === 1) {
return forms[0];
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
} else if (last >= 2 && last <= 4) {
return forms[1];
} else {
return forms[2];
}
});
1 change: 1 addition & 0 deletions workers/email/src/templates/names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
93 changes: 37 additions & 56 deletions workers/paymaster/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,6 +28,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
*/
Expand Down Expand Up @@ -103,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
*/
Expand Down Expand Up @@ -247,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
Expand Down Expand Up @@ -319,9 +274,15 @@ 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) {
// Send reminders on certain days after payday
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
if (DAYS_AFTER_PAYDAY_TO_REMIND.includes(daysAfterPaydayValue)) {
await this.sendBlockedWorkspaceReminders(workspace, daysAfterPaydayValue);
}

return [workspace, true];
}

Expand All @@ -338,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];
Expand Down Expand Up @@ -403,6 +364,26 @@ export default class PaymasterWorker extends Worker {
});
}


/**
* Sends reminder emails to blocked workspace admins
*
* @param workspace - workspace to send reminders for
* @param days - number of days the workspace spent after payday
*/
private async sendBlockedWorkspaceReminders(
workspace: WorkspaceDBScheme,
days: number
): Promise<void> {
await this.addTask(WorkerNames.EMAIL, {
type: 'blocked-workspace-reminder',
payload: {
workspaceId: workspace._id.toString(),
daysAfterPayday: days,
},
});
}

/**
* Sets BillingPeriodEventsCount to 0 in workspace
*
Expand Down
Loading
Loading