diff --git a/lib/utils/hasValue.ts b/lib/utils/hasValue.ts index 6bbc44ab..5a74a93f 100644 --- a/lib/utils/hasValue.ts +++ b/lib/utils/hasValue.ts @@ -1,5 +1,6 @@ /** * Returns true if specified value is not undefined, null and empty string + * * @param v - value to check */ export function hasValue(v: T | undefined | null): v is T { diff --git a/workers/limiter/src/dbHelper.ts b/workers/limiter/src/dbHelper.ts index a2ad3726..7ad4c3e5 100644 --- a/workers/limiter/src/dbHelper.ts +++ b/workers/limiter/src/dbHelper.ts @@ -109,23 +109,6 @@ export class DbHelper { await this.workspacesCollection.bulkWrite(operations); } - /** - * Method to change workspace isBlocked state - * - * @param workspaceId - id of the workspace to be changed - * @param isBlocked - new isBlocked state of the workspace - */ - public async changeWorkspaceBlockedState(workspaceId: string, isBlocked: boolean): Promise { - await this.workspacesCollection.updateOne( - { _id: new ObjectId(workspaceId) }, - { - $set: { - isBlocked, - }, - } - ); - } - /** * Returns total event counts for last billing period * diff --git a/workers/limiter/src/index.ts b/workers/limiter/src/index.ts index 45cb2382..8bb2d2c3 100644 --- a/workers/limiter/src/index.ts +++ b/workers/limiter/src/index.ts @@ -108,6 +108,8 @@ export default class LimiterWorker extends Worker { * @param event - event to handle */ private async handleBlockWorkspaceEvent(event: BlockWorkspaceEvent): Promise { + this.logger.info('handle block workspace event', event); + const workspace = await this.dbHelper.getWorkspacesWithTariffPlans(event.workspaceId); if (!workspace) { @@ -126,7 +128,13 @@ export default class LimiterWorker extends Worker { const workspaceProjects = await this.dbHelper.getProjects(event.workspaceId); const projectIds = workspaceProjects.map(project => project._id.toString()); - await this.dbHelper.changeWorkspaceBlockedState(event.workspaceId, true); + const { updatedWorkspace } = await this.prepareWorkspaceUsageUpdate(workspace, workspaceProjects); + + updatedWorkspace.isBlocked = true; + await this.dbHelper.updateWorkspacesEventsCountAndIsBlocked([updatedWorkspace]); + + this.logger.info('workspace blocked in db ', event.workspaceId) + await this.redis.appendBannedProjects(projectIds); this.sendSingleWorkspaceReport(workspaceProjects, workspace, 'blocked'); @@ -159,16 +167,18 @@ export default class LimiterWorker extends Worker { /** * If workspace should be blocked by quota - then do not unblock it */ - const { shouldBeBlockedByQuota } = await this.prepareWorkspaceUsageUpdate(workspace, workspaceProjects); + const { shouldBeBlockedByQuota, updatedWorkspace } = await this.prepareWorkspaceUsageUpdate(workspace, workspaceProjects); if (shouldBeBlockedByQuota) { return; } - await this.dbHelper.changeWorkspaceBlockedState(event.workspaceId, false); + updatedWorkspace.isBlocked = false; + + await this.dbHelper.updateWorkspacesEventsCountAndIsBlocked([updatedWorkspace]); await this.redis.removeBannedProjects(projectIds); - this.sendSingleWorkspaceReport(workspaceProjects, workspace, 'unblocked'); + this.sendSingleWorkspaceReport(workspaceProjects, updatedWorkspace, 'unblocked'); } /** @@ -182,6 +192,13 @@ export default class LimiterWorker extends Worker { const updatedWorkspaces: WorkspaceWithTariffPlan[] = []; await Promise.all(workspaces.map(async (workspace) => { + /** + * If workspace is already blocked - do nothing + */ + if (workspace.isBlocked) { + return; + } + const workspaceProjects = await this.dbHelper.getProjects(workspace._id.toString()); const { shouldBeBlockedByQuota, updatedWorkspace, projectsToUpdate } = await this.prepareWorkspaceUsageUpdate(workspace, workspaceProjects); @@ -198,7 +215,7 @@ export default class LimiterWorker extends Worker { /** * If workspace is not blocked yet and it should be blocked by quota - then block it */ - if (!workspace.isBlocked && shouldBeBlockedByQuota) { + if (shouldBeBlockedByQuota) { const projectIds = projectsToUpdate.map(project => project._id.toString()); this.redis.appendBannedProjects(projectIds); @@ -222,6 +239,8 @@ export default class LimiterWorker extends Worker { private async prepareWorkspaceUsageUpdate( workspace: WorkspaceWithTariffPlan, projects: ProjectDBScheme[] ): Promise { + this.logger.info('prepareWorkspaceUsageUpdate'); + /** * If last charge date is not specified, then we skip checking it * In the next time the Paymaster worker starts, it will set lastChargeDate for this workspace @@ -240,6 +259,9 @@ export default class LimiterWorker extends Worker { const since = Math.floor(new Date(workspace.lastChargeDate).getTime() / MS_IN_SEC); const workspaceEventsCount = await this.dbHelper.getEventsCountByProjects(projects, since); + + this.logger.info(`workspace ${workspace._id} events count since last charge date: ${workspaceEventsCount}`); + const usedQuota = workspaceEventsCount / workspace.tariffPlan.eventsLimit; const quotaNotification = NOTIFY_ABOUT_LIMIT.reverse().find(quota => quota < usedQuota); diff --git a/workers/limiter/tests/dbHelper.test.ts b/workers/limiter/tests/dbHelper.test.ts index 6c4c8f0f..bdff1cb5 100644 --- a/workers/limiter/tests/dbHelper.test.ts +++ b/workers/limiter/tests/dbHelper.test.ts @@ -272,34 +272,6 @@ describe('DbHelper', () => { }); }); - describe('changeWorkspaceBlockedState', () => { - test('Should change workspace blocked state', async () => { - /** - * Arrange - */ - const workspace = createWorkspaceMock({ - plan: mockedPlans.eventsLimit10, - billingPeriodEventsCount: 0, - lastChargeDate: new Date(), - isBlocked: false, - }); - - await workspaceCollection.insertOne(workspace); - - /** - * Act - */ - await dbHelper.changeWorkspaceBlockedState(workspace._id.toString(), true); - - /** - * Assert - */ - const updatedWorkspace = await workspaceCollection.findOne({ _id: workspace._id }); - - expect(updatedWorkspace.isBlocked).toBe(true); - }); - }); - describe('getEventsCountByProject', () => { test('Should count events and repetitions for a project', async () => { /** diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index 10009ce6..0ce4d681 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -60,24 +60,47 @@ export default class PaymasterWorker extends Worker { /** * Check if today is a payday for passed timestamp * - * Pay day is calculated by formula: last charge date + 30 days - * * @param date - last charge date * @param paidUntil - paid until date * @param isDebug - flag for debug purposes */ - private static isTimeToPay(date: Date, paidUntil: Date, isDebug = false): boolean { - const expectedPayDay = paidUntil ? new Date(paidUntil) : new Date(date); + private static isTimeToPay(date: Date, paidUntil?: Date, isDebug = false): boolean { + const expectedPayDay = paidUntil ? new Date(paidUntil) : this.composeBillingPeriodEndDate(date, isDebug); + const now = new Date(); + + return now >= expectedPayDay; + } + + /** + * Check if today is a recharge day for passed timestamp + * It equals to the isTimeToPay in all cases except prePaid workspaces + * + * @param date - last charge date + * @param isDebug - flag for debug purposes + */ + private static isTimeToRecharge(date: Date, isDebug = false): boolean { + const nexTimeToRecharge = this.composeBillingPeriodEndDate(date, isDebug); + const now = new Date(); + + return now >= nexTimeToRecharge; + } + + /** + * Returns the date - end of the billing period + * + * @param date - last charge date + * @param isDebug - flag for debug workspaces + */ + private static composeBillingPeriodEndDate(date: Date, isDebug = false): Date { + const endDate = new Date(date); if (isDebug) { - expectedPayDay.setDate(date.getDate() + 1); - } else if (!paidUntil) { - expectedPayDay.setMonth(date.getMonth() + 1); + endDate.setDate(date.getDate() + 1); + } else { + endDate.setMonth(date.getMonth() + 1); } - const now = new Date().getTime(); - - return now >= expectedPayDay.getTime(); + return endDate; } /** @@ -214,6 +237,12 @@ export default class PaymasterWorker extends Worker { // @ts-expect-error debug const isTimeToPay = PaymasterWorker.isTimeToPay(workspace.lastChargeDate, workspace.paidUntil, workspace.isDebug); + /** + * Is it time to recharge workspace limits + */ + // @ts-expect-error debug + const isTimeToRecharge = PaymasterWorker.isTimeToRecharge(workspace.lastChargeDate, workspace.isDebug); + /** * How many days have passed since payments the expected day of payments */ @@ -237,7 +266,17 @@ export default class PaymasterWorker extends Worker { */ if (!isTimeToPay) { /** - * If workspace was manually unblocked (reset of billingPeriodEventsCount and lastChargeDate) in db + * If it is time to recharge workspace limits, but not time to pay + * Start new month - recharge billing period events count and update last charge date + */ + if (isTimeToRecharge) { + await this.updateLastChargeDate(workspace, date); + await this.clearBillingPeriodEventsCount(workspace); + } + + /** + * If workspace is blocked, but it is not time to pay + * This case could be reached by prepaid workspaces and manually recharged ones */ if (workspace.isBlocked) { await this.unblockWorkspace(workspace); diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index a4cc33f5..74f52e1f 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -481,11 +481,6 @@ describe('PaymasterWorker', () => { MockDate.reset(); }); - afterAll(async () => { - await connection.close(); - MockDate.reset(); - }); - test('Should send notification if payday is coming for workspace with paidUntil value', async () => { /** * Arrange @@ -542,4 +537,62 @@ describe('PaymasterWorker', () => { MockDate.reset(); }); + + test('Should recharge workspace billing period when month passes since last charge date and paidUntil is set to several months in the future', async () => { + /** + * Arrange + */ + const currentDate = new Date(); + const lastChargeDate = new Date(currentDate.getTime()); + + lastChargeDate.setMonth(lastChargeDate.getMonth() - 1); // Set last charge date to 1 month ago + + const paidUntil = new Date(currentDate.getTime()); + + paidUntil.setMonth(paidUntil.getMonth() + 3); // Set paidUntil to 3 months in the future + + const plan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + const workspace = createWorkspaceMock({ + plan, + subscriptionId: null, + lastChargeDate, + isBlocked: false, + billingPeriodEventsCount: 10, + paidUntil, + }); + + await fillDatabaseWithMockedData({ + workspace, + plan, + }); + + MockDate.set(currentDate); + + /** + * Act + */ + const worker = new PaymasterWorker(); + + await worker.start(); + await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK); + await worker.finish(); + + /** + * Assert + */ + const updatedWorkspace = await workspacesCollection.findOne({ _id: workspace._id }); + + expect(updatedWorkspace.lastChargeDate).toEqual(currentDate); + expect(updatedWorkspace.billingPeriodEventsCount).toEqual(0); + + MockDate.reset(); + }); + + afterAll(async () => { + await connection.close(); + MockDate.reset(); + }); }); diff --git a/workers/release/src/index.ts b/workers/release/src/index.ts index 9e8de3b4..6fd3a820 100644 --- a/workers/release/src/index.ts +++ b/workers/release/src/index.ts @@ -179,10 +179,9 @@ export default class ReleaseWorker extends Worker { const fileInfo = await this.saveFile(map); /** - * Remove 'content' and save id of saved file instead + * Save id of saved file instead */ map._id = fileInfo._id; - delete map.content; return map; } catch (error) { @@ -190,6 +189,13 @@ export default class ReleaseWorker extends Worker { } })); + /** + * Delete file content after it is saved to the GridFS + */ + savedFiles.forEach(file => { + delete file.content; + }) + /** * Filter unsaved maps */ @@ -282,6 +288,10 @@ export default class ReleaseWorker extends Worker { */ private saveFile(file: SourceMapDataExtended): Promise { return new Promise((resolve, reject) => { + if (!file.content) { + return reject(new Error('Source map content is empty')); + } + const readable = Readable.from([ file.content ]); const writeStream = this.db.getBucket().openUploadStream(file.mapFileName);