Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 {
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",
"@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
37 changes: 27 additions & 10 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 Expand Up @@ -269,19 +274,31 @@ export default class LimiterWorker extends Worker {
const isAlreadyBlocked = workspace.isBlocked;

/**
* Send notification if workspace will be blocked cause events limit
* Check quota and send notifications if needed
* - if should be blocked by quota and is not blocked yet -> block and notify
* - if is about to reach limit -> notify
*/
if (!isAlreadyBlocked && shouldBeBlockedByQuota) {
if (shouldBeBlockedByQuota) {
if (!isAlreadyBlocked) {
this.logger.info(`Workspace ${workspace._id} will be blocked by quota: ${workspaceEventsCount} of ${workspace.tariffPlan.eventsLimit} events used`);

/**
* Add task for Sender worker
*/
await this.addTask(WorkerNames.EMAIL, {
type: 'block-workspace',
payload: {
workspaceId: workspace._id,
},
});
}
} else if (quotaNotification) {
/**
* Add task for Sender worker
* Notify that workspace is about to reach events limit
*/
await this.addTask(WorkerNames.EMAIL, {
type: 'block-workspace',
payload: {
workspaceId: workspace._id,
},
});
} else if (quotaNotification) {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
this.logger.info(`Workspace ${workspace._id} is about to reach events limit: ${Math.floor(usedQuota * 100)}% used`);

/**
* Add task for Sender worker
*/
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
Loading
Loading