Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
078cdeb
pull new hawk types
talyguryn Dec 10, 2025
e0a39cc
Track blocked date for workspaces in LimiterWorker
talyguryn Dec 10, 2025
4e12dcb
Add and populate blockedDate for blocked workspaces
talyguryn Dec 10, 2025
62169a9
Add tests for blockedDate handling in workspaces
talyguryn Dec 10, 2025
54c7965
Update index.ts
talyguryn Dec 10, 2025
a01f058
Refactor blocked workspace reminder to use days after block
talyguryn Dec 10, 2025
857e8bc
Update yarn.lock to deduplicate and reorder entries
talyguryn Dec 10, 2025
d3e82b2
Rename daysAfterPayday to daysAfterBlock in payload
talyguryn Dec 10, 2025
39f6a19
Handle undefined daysAfterBlock in reminders logic
talyguryn Dec 10, 2025
697d090
Adjust blockedDate calculation in PaymasterWorker test
talyguryn Dec 10, 2025
3b16ca7
Update workers/paymaster/tests/index.test.ts
talyguryn Dec 10, 2025
a01a12b
Update workers/paymaster/src/index.ts
talyguryn Dec 10, 2025
3cd7142
Update lib/utils/payday.ts
talyguryn Dec 10, 2025
f250942
Update dbHelper.test.ts
talyguryn Dec 10, 2025
55be33c
Merge branch 'fix/save-workspace-blocked-date' of https://github.com/…
talyguryn Dec 10, 2025
ed2f352
Remove obsolete test and update blocked workspace logic
talyguryn Dec 10, 2025
9551b3f
Refactor blockedDate handling in workspace mocks
talyguryn Dec 10, 2025
8c34583
Update index.test.ts
talyguryn Dec 10, 2025
86e041d
Update dbHelper.test.ts
talyguryn Dec 10, 2025
009004f
Update index.test.ts
talyguryn Dec 10, 2025
91a2e63
Update index.test.ts
talyguryn Dec 10, 2025
199a98d
Update workers/paymaster/tests/index.test.ts
talyguryn Dec 10, 2025
45a8dce
Update index.test.ts
talyguryn Dec 10, 2025
1f28410
Update workers/paymaster/src/index.ts
talyguryn Dec 10, 2025
bb84647
Update lib/utils/payday.ts
talyguryn Dec 10, 2025
267497c
Update workers/paymaster/src/index.ts
talyguryn Dec 10, 2025
09c12d9
Update emailOverview.ts
talyguryn Dec 10, 2025
e888848
Update workers/limiter/src/index.ts
talyguryn Dec 10, 2025
ef9e851
Update lib/utils/payday.ts
talyguryn Dec 10, 2025
a411041
Update index.test.ts
talyguryn Dec 10, 2025
2f5edc1
Update index.test.ts
talyguryn Dec 10, 2025
92bb5b9
Update index.ts
talyguryn Dec 10, 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
22 changes: 20 additions & 2 deletions lib/utils/payday.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { WorkspaceDBScheme } from '@hawk.so/types';
import { HOURS_IN_DAY, MINUTES_IN_HOUR, SECONDS_IN_MINUTE, MS_IN_SEC } from './consts';

/**
Expand All @@ -14,7 +15,7 @@ const MILLISECONDS_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR * SECONDS_IN_MINUTE *
* @param paidUntil - paid until date
* @param isDebug - flag for debug purposes
*/
export function daysBeforePayday(date: Date, paidUntil: Date = null, isDebug = false): number {
export function countDaysBeforePayday(date: Date, paidUntil: Date = null, isDebug = false): number {
const expectedPayDay = paidUntil ? new Date(paidUntil) : new Date(date);

if (isDebug) {
Expand All @@ -37,7 +38,7 @@ export function daysBeforePayday(date: Date, paidUntil: Date = null, isDebug = f
* @param paidUntil - paid until date
* @param isDebug - flag for debug purposes
*/
export function daysAfterPayday(date: Date, paidUntil: Date = null, isDebug = false): number {
export function countDaysAfterPayday(date: Date, paidUntil: Date = null, isDebug = false): number {
Comment thread
talyguryn marked this conversation as resolved.
const expectedPayDay = paidUntil ? new Date(paidUntil) : new Date(date);

if (isDebug) {
Expand All @@ -49,4 +50,21 @@ export function daysAfterPayday(date: Date, paidUntil: Date = null, isDebug = fa
const now = new Date().getTime();

return Math.floor((now - expectedPayDay.getTime()) / MILLISECONDS_IN_DAY);
}

/**
* Returns difference between day when workspace was blocked and now in days. Undefined for workspaces blocked before the "blockedDate" implemented.
*
* @param workspace - workspace object
*/
export function countDaysAfterBlock(workspace: WorkspaceDBScheme): number | undefined {
if (!workspace.blockedDate) {
return undefined;
}

const blockedDay = new Date(workspace.blockedDate);

const now = new Date().getTime();

return Math.floor((now - blockedDay.getTime()) / MILLISECONDS_IN_DAY);
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@
"run-limiter": "yarn worker hawk-worker-limiter"
},
"dependencies": {
"@babel/parser": "^7.26.9",
"@babel/traverse": "7.26.9",
"@hawk.so/nodejs": "^3.1.1",
"@hawk.so/types": "^0.1.35",
"@hawk.so/types": "^0.2.0",
Comment thread
talyguryn marked this conversation as resolved.
"@types/amqplib": "^0.8.2",
"@types/jest": "^29.2.3",
"@types/mongodb": "^3.5.15",
Expand All @@ -73,9 +75,7 @@
"typescript": "^4.9.4",
"uuid": "^8.3.0",
"winston": "^3.2.1",
"yup": "^0.28.5",
"@babel/parser": "^7.26.9",
"@babel/traverse": "7.26.9"
"yup": "^0.28.5"
},
"devDependencies": {
"@shelf/jest-mongodb": "^1.2.3",
Expand Down
6 changes: 3 additions & 3 deletions workers/email/scripts/emailOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +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';
import { countDaysAfterPayday } from '../../../lib/utils/payday';

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

try {
Expand Down Expand Up @@ -339,7 +339,7 @@ class EmailTestServer {
return 0;
}

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

return days > 0 ? days : 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<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, ['день', 'дня', 'дней']) }} без мониторинга
{{ daysAfterBlock }} {{ pluralize_ru(daysAfterBlock, ['день', 'дня', 'дней']) }} без мониторинга
</span>
</font>
</td>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Требуется действие: мониторинг ошибок в {{ workspace.name }} не работает уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }}
Требуется действие: мониторинг ошибок в {{ workspace.name }} не работает уже {{ daysAfterBlock }} {{ pluralize_ru(daysAfterBlock, ['день', 'дня', 'дней']) }}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Требуется действие: мониторинг ошибок в {{ workspace.name }} не работает уже {{ daysAfterPayday }} {{ pluralize_ru(daysAfterPayday, ['день', 'дня', 'дней']) }}
Требуется действие: мониторинг ошибок в {{ workspace.name }} не работает уже {{ daysAfterBlock }} {{ pluralize_ru(daysAfterBlock, ['день', 'дня', 'дней']) }}

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

Expand Down
1 change: 1 addition & 0 deletions workers/limiter/src/dbHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export class DbHelper {
$set: {
billingPeriodEventsCount: workspace.billingPeriodEventsCount,
isBlocked: workspace.isBlocked,
blockedDate: workspace.blockedDate,
},
},
},
Expand Down
5 changes: 5 additions & 0 deletions workers/limiter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export default class LimiterWorker extends Worker {
const { updatedWorkspace } = await this.prepareWorkspaceUsageUpdate(workspace, workspaceProjects);

updatedWorkspace.isBlocked = true;
updatedWorkspace.blockedDate = new Date();
await this.dbHelper.updateWorkspacesEventsCountAndIsBlocked([ updatedWorkspace ]);

this.logger.info('workspace blocked in db ', event.workspaceId);
Expand Down Expand Up @@ -174,6 +175,7 @@ export default class LimiterWorker extends Worker {
}

updatedWorkspace.isBlocked = false;
updatedWorkspace.blockedDate = null;

await this.dbHelper.updateWorkspacesEventsCountAndIsBlocked([ updatedWorkspace ]);
await this.redis.removeBannedProjects(projectIds);
Expand Down Expand Up @@ -218,6 +220,9 @@ export default class LimiterWorker extends Worker {
if (shouldBeBlockedByQuota) {
const projectIds = projectsToUpdate.map(project => project._id.toString());

updatedWorkspace.isBlocked = true;
updatedWorkspace.blockedDate = new Date();

this.redis.appendBannedProjects(projectIds);
message += this.formSingleWorkspaceMessage(updatedWorkspace, projectsToUpdate, 'blocked');
}
Expand Down
44 changes: 44 additions & 0 deletions workers/limiter/tests/dbHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('DbHelper', () => {
billingPeriodEventsCount: number;
lastChargeDate: Date;
isBlocked?: boolean;
blockedDate?: Date;
}): WorkspaceDBScheme => {
return {
_id: new ObjectId(),
Expand All @@ -39,6 +40,7 @@ describe('DbHelper', () => {
accountId: '',
balance: 0,
isBlocked: parameters.isBlocked,
blockedDate: parameters.blockedDate,
};
};

Expand Down Expand Up @@ -213,17 +215,20 @@ describe('DbHelper', () => {

await workspaceCollection.insertMany([workspace1, workspace2]);

const blockedDate = new Date();
const updatedWorkspaces = [
{
...workspace1,
billingPeriodEventsCount: 5,
isBlocked: true,
blockedDate: blockedDate,
tariffPlan: mockedPlans.eventsLimit10,
},
{
...workspace2,
billingPeriodEventsCount: 5000,
isBlocked: true,
blockedDate: blockedDate,
tariffPlan: mockedPlans.eventsLimit10000,
},
];
Expand All @@ -241,8 +246,10 @@ describe('DbHelper', () => {

expect(updatedWorkspace1.billingPeriodEventsCount).toBe(5);
expect(updatedWorkspace1.isBlocked).toBe(true);
expect(updatedWorkspace1.blockedDate).toEqual(blockedDate);
expect(updatedWorkspace2.billingPeriodEventsCount).toBe(5000);
expect(updatedWorkspace2.isBlocked).toBe(true);
expect(updatedWorkspace2.blockedDate).toEqual(blockedDate);
});

test('Should not update anything if empty array provided', async () => {
Expand All @@ -254,6 +261,7 @@ describe('DbHelper', () => {
billingPeriodEventsCount: 0,
lastChargeDate: new Date(),
isBlocked: false,
blockedDate: null,
});

await workspaceCollection.insertOne(workspace);
Expand All @@ -270,6 +278,42 @@ describe('DbHelper', () => {

expect(unchangedWorkspace).toEqual(workspace);
});

test('Should set blockedDate to null when unblocking workspace', async () => {
/**
* Arrange
*/
const blockedDate = new Date();
const workspace = createWorkspaceMock({
plan: mockedPlans.eventsLimit10,
billingPeriodEventsCount: 0,
lastChargeDate: new Date(),
isBlocked: true,
blockedDate: blockedDate,
});

await workspaceCollection.insertOne(workspace);

const updatedWorkspace = {
...workspace,
isBlocked: false,
blockedDate: null,
tariffPlan: mockedPlans.eventsLimit10,
};

/**
* Act
*/
await dbHelper.updateWorkspacesEventsCountAndIsBlocked([updatedWorkspace]);

/**
* Assert
*/
const result = await workspaceCollection.findOne({ _id: workspace._id });

expect(result.isBlocked).toBe(false);
expect(result.blockedDate).toBeNull();
});
});

describe('getEventsCountByProject', () => {
Expand Down
5 changes: 5 additions & 0 deletions workers/limiter/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe('Limiter worker', () => {
billingPeriodEventsCount: number;
lastChargeDate: Date;
isBlocked?: boolean;
blockedDate?: Date;
}): WorkspaceDBScheme => {
return {
_id: new ObjectId(),
Expand All @@ -56,6 +57,7 @@ describe('Limiter worker', () => {
accountId: '',
balance: 0,
isBlocked: parameters.isBlocked,
blockedDate: parameters.blockedDate,
};
};

Expand Down Expand Up @@ -391,6 +393,7 @@ describe('Limiter worker', () => {
const blockedProjects = await redisClient.sMembers('DisabledProjectsSet');

expect(updatedWorkspace.isBlocked).toBe(true);
expect(updatedWorkspace.blockedDate).toBeInstanceOf(Date);
expect(blockedProjects).toContain(project._id.toString());
expect(telegram.sendMessage).toHaveBeenCalledWith(
expect.stringContaining('⛔️ Workspace <b>Mocked workspace</b> blocked <b>(id: <code>'),
Expand All @@ -416,6 +419,7 @@ describe('Limiter worker', () => {
billingPeriodEventsCount: 0,
lastChargeDate: LAST_CHARGE_DATE,
isBlocked: true,
blockedDate: new Date(),
});
const project = createProjectMock({ workspaceId: workspace._id });

Expand Down Expand Up @@ -449,6 +453,7 @@ describe('Limiter worker', () => {
const blockedProjects = await redisClient.sMembers('DisabledProjectsSet');

expect(updatedWorkspace.isBlocked).toBe(false);
expect(updatedWorkspace.blockedDate).toBeNull();
expect(blockedProjects).not.toContain(project._id.toString());
expect(telegram.sendMessage).toHaveBeenCalledWith(
expect.stringContaining('✅ Workspace <b>Mocked workspace</b> unblocked <b>(id: <code>'),
Expand Down
34 changes: 19 additions & 15 deletions workers/paymaster/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { EventType, PaymasterEvent } from '../types/paymaster-worker-events';
import axios from 'axios';
import * as WorkerNames from '../../../lib/workerNames';
import HawkCatcher from '@hawk.so/nodejs';
import { daysBeforePayday, daysAfterPayday } from '../../../lib/utils/payday';
import { countDaysBeforePayday, countDaysAfterPayday, countDaysAfterBlock } from '../../../lib/utils/payday';

dotenv.config({
path: path.resolve(__dirname, '../.env'),
Expand All @@ -29,10 +29,10 @@ const DAYS_AFTER_PAYDAY_TO_TRY_PAYING = 3;
const DAYS_LEFT_ALERT = [3, 2, 1, 0];

/**
* Days after payday to remind admins about blocked workspace
* Days after block 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];
const DAYS_AFTER_BLOCK_TO_REMIND = [1, 2, 3, 5, 7, 30];

/**
* Worker to check workspaces subscription status and ban workspaces without actual subscription
Expand Down Expand Up @@ -199,16 +199,21 @@ export default class PaymasterWorker extends Worker {
const isTimeToRecharge = PaymasterWorker.isTimeToRecharge(workspace.lastChargeDate, workspace.isDebug);

/**
* How many days have passed since payments the expected day of payments
* How many days left for the expected day of payments
*/
// @ts-expect-error debug
const daysAfterPaydayValue = daysAfterPayday(workspace.lastChargeDate, workspace.paidUntil, workspace.isDebug);
const daysLeft = countDaysBeforePayday(workspace.lastChargeDate, workspace.paidUntil, workspace.isDebug);

/**
* How many days left for the expected day of payments
* How many days have passed since the expected day of payments
*/
// @ts-expect-error debug
const daysLeft = daysBeforePayday(workspace.lastChargeDate, workspace.paidUntil, workspace.isDebug);
const daysAfterPayday = countDaysAfterPayday(workspace.lastChargeDate, workspace.paidUntil, workspace.isDebug);

/**
* How many days have passed since the workspace was blocked
*/
const daysAfterBlock = countDaysAfterBlock(workspace);

/**
* Do we need to ask for money
Expand Down Expand Up @@ -277,10 +282,9 @@ 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 (DAYS_AFTER_PAYDAY_TO_REMIND.includes(daysAfterPaydayValue)) {
await this.sendBlockedWorkspaceReminders(workspace, daysAfterPaydayValue);
// Send reminders on certain days after block
if (daysAfterBlock !== undefined && DAYS_AFTER_BLOCK_TO_REMIND.includes(daysAfterBlock)) {
await this.sendBlockedWorkspaceReminders(workspace, daysAfterBlock);
Comment thread
talyguryn marked this conversation as resolved.
}

return [workspace, true];
Expand All @@ -299,7 +303,7 @@ export default class PaymasterWorker extends Worker {
* Block workspace if it has paid subscription,
* but a few days have passed after payday
*/
if (daysAfterPaydayValue > DAYS_AFTER_PAYDAY_TO_TRY_PAYING) {
if (daysAfterPayday > DAYS_AFTER_PAYDAY_TO_TRY_PAYING) {
await this.blockWorkspace(workspace);

return [workspace, true];
Expand Down Expand Up @@ -369,17 +373,17 @@ 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
* @param daysAfterBlock - number of days since the workspace was blocked
*/
private async sendBlockedWorkspaceReminders(
workspace: WorkspaceDBScheme,
days: number
daysAfterBlock: number
): Promise<void> {
await this.addTask(WorkerNames.EMAIL, {
type: 'blocked-workspace-reminder',
payload: {
workspaceId: workspace._id.toString(),
daysAfterPayday: days,
daysAfterBlock,
},
});
}
Expand Down
Loading
Loading