From 24cdf62e4a3abb238d1833ee8984ad5ec289c06a Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Tue, 26 Nov 2024 02:07:22 -0400 Subject: [PATCH] feat: create usage module --- src/modules/file/file.repository.ts | 48 ++++++++++++ src/modules/usage/usage.module.ts | 15 ++-- src/modules/usage/usage.repository.ts | 109 ++++++++++++++++++-------- src/modules/usage/usage.usecase.ts | 41 ++++++++++ src/modules/user/user.controller.ts | 5 ++ src/modules/user/user.module.ts | 2 + src/modules/user/user.usecase.ts | 6 ++ 7 files changed, 188 insertions(+), 38 deletions(-) create mode 100644 src/modules/usage/usage.usecase.ts diff --git a/src/modules/file/file.repository.ts b/src/modules/file/file.repository.ts index 34ea9fd5f..5136d9e74 100644 --- a/src/modules/file/file.repository.ts +++ b/src/modules/file/file.repository.ts @@ -84,6 +84,10 @@ export interface FileRepository { userId: User['id'], fileIds: FileAttributes['fileId'][], ): Promise; + sumFileSizesSinceDate( + userId: FileAttributes['userId'], + sinceDate: Date, + ): Promise; } @Injectable() @@ -590,6 +594,50 @@ export class SequelizeFileRepository implements FileRepository { }); } + async sumFileSizesSinceDate( + userId: FileAttributes['userId'], + sinceDate: Date, + ): Promise { + const result = await this.fileModel.findAll({ + attributes: [ + [ + Sequelize.literal(` + SUM( + CASE + WHEN status = 'DELETED' AND date_trunc('day', created_at) = date_trunc('day', updated_at) THEN 0 + WHEN status = 'DELETED' THEN -size + ELSE size + END + ) + `), + 'total', + ], + ], + where: { + userId, + [Op.or]: [ + { + status: { + [Op.ne]: 'DELETED', + }, + createdAt: { + [Op.gte]: sinceDate, + }, + }, + { + status: 'DELETED', + updatedAt: { + [Op.gte]: sinceDate, + }, + }, + ], + }, + raw: true, + }); + + return Number(result[0]['total']) as unknown as number; + } + async getFilesWhoseFolderIdDoesNotExist( userId: File['userId'], ): Promise { diff --git a/src/modules/usage/usage.module.ts b/src/modules/usage/usage.module.ts index 823d2fd5d..f98e0ee8f 100644 --- a/src/modules/usage/usage.module.ts +++ b/src/modules/usage/usage.module.ts @@ -1,11 +1,16 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { SequelizeModule } from '@nestjs/sequelize'; import { UsageModel } from './usage.model'; import { SequelizeUsageRepository } from './usage.repository'; +import { UsageUseCases } from './usage.usecase'; +import { FileModule } from '../file/file.module'; @Module({ - imports: [SequelizeModule.forFeature([UsageModel])], - providers: [SequelizeUsageRepository], - exports: [SequelizeUsageRepository], + imports: [ + SequelizeModule.forFeature([UsageModel]), + forwardRef(() => FileModule), + ], + providers: [SequelizeUsageRepository, UsageUseCases], + exports: [SequelizeUsageRepository, UsageUseCases, SequelizeModule], }) -export class PlanModule {} +export class UsageModule {} diff --git a/src/modules/usage/usage.repository.ts b/src/modules/usage/usage.repository.ts index 5a7cfd268..01fbe171a 100644 --- a/src/modules/usage/usage.repository.ts +++ b/src/modules/usage/usage.repository.ts @@ -27,7 +27,7 @@ export class SequelizeUsageRepository { return mostRecentUsage ? this.toDomain(mostRecentUsage) : null; } - public async addFirstDailyUsage(userUuid: string): Promise { + public async addFirstDailyUsage(userUuid: string): Promise { const query = ` INSERT INTO public.usages (id, user_id, delta, period, type, created_at, updated_at) SELECT @@ -42,10 +42,7 @@ export class SequelizeUsageRepository { users u LEFT JOIN public.files f ON u.id = f.user_id AND f.status != 'DELETED' - AND ( - f.created_at < CURRENT_DATE - AND f.updated_at < CURRENT_DATE - ) + AND f.created_at < CURRENT_DATE WHERE u.uuid = :userUuid GROUP BY @@ -58,41 +55,87 @@ export class SequelizeUsageRepository { model: UsageModel, }); - return result.map((result) => this.toDomain(result)); + return result.length > 0 ? this.toDomain(result[0]) : null; } - public async getUserUsage(userUuid: string): Promise { + public async getUserUsage(userUuid: string) { const query = ` - INSERT INTO public.usages (id, user_id, delta, period, type, created_at, updated_at) - SELECT - uuid_generate_v4(), - u.uuid::uuid AS user_id, - COALESCE(SUM(f.size), 0) AS delta, - (CURRENT_DATE - INTERVAL '2 day')::DATE AS period, - 'monthly' AS type, - NOW() AS created_at, - NOW() AS updated_at - FROM - users u - LEFT JOIN public.files f ON u.id = f.user_id - AND f.status != 'DELETED' - AND ( - f.created_at < CURRENT_DATE - AND f.updated_at < CURRENT_DATE - ) - WHERE - u.uuid = :userUuid - GROUP BY - u.uuid - RETURNING *; + WITH yearly_sums AS ( + SELECT + date_trunc('year', period) AS year, + SUM(delta) AS total_delta + FROM + public.usages + WHERE + type = 'yearly' + AND user_id = :userUuid + GROUP BY + date_trunc('year', period) + ), + monthly_sums AS ( + SELECT + date_trunc('year', period) AS year, + SUM(delta) AS total_delta + FROM + public.usages + WHERE + type = 'monthly' + AND user_id = :userUuid + GROUP BY + date_trunc('year', period) + ), + filtered_monthly_sums AS ( + SELECT + m.year, + m.total_delta + FROM + monthly_sums m + LEFT JOIN yearly_sums y ON m.year = y.year + WHERE y.year IS NULL + ), + combined_sums AS ( + SELECT + year, + total_delta + FROM + yearly_sums + UNION ALL + SELECT + year, + total_delta + FROM + filtered_monthly_sums + ) + SELECT + SUM( + CASE + WHEN year < date_trunc('year', CURRENT_DATE) THEN total_delta + ELSE 0 + END + ) AS total_yearly_delta, + SUM( + CASE + WHEN year = date_trunc('year', CURRENT_DATE) THEN total_delta + ELSE 0 + END + ) AS total_monthly_delta + FROM + combined_sums; `; - const result = await this.usageModel.sequelize.query(query, { + const [result] = (await this.usageModel.sequelize.query(query, { replacements: { userUuid }, - model: UsageModel, - }); + })) as unknown as [ + { + total_yearly_delta: number; + total_monthly_delta: number; + }[], + ]; - return result.map((result) => this.toDomain(result)); + return { + total_yearly_delta: Number(result[0].total_yearly_delta || 0), + total_monthly_delta: Number(result[0].total_monthly_delta || 0), + }; } toDomain(model: UsageModel): Usage { diff --git a/src/modules/usage/usage.usecase.ts b/src/modules/usage/usage.usecase.ts new file mode 100644 index 000000000..650432272 --- /dev/null +++ b/src/modules/usage/usage.usecase.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { SequelizeUsageRepository } from './usage.repository'; +import { SequelizeFileRepository } from '../file/file.repository'; +import { User } from '../user/user.domain'; + +@Injectable() +export class UsageUseCases { + constructor( + private readonly usageRepository: SequelizeUsageRepository, + private readonly fileRepository: SequelizeFileRepository, + ) {} + + async getUserUsage(user: User) { + const userUuid = user.uuid; + + let mostRecentUsage = + await this.usageRepository.getMostRecentUsage(userUuid); + + if (!mostRecentUsage) { + mostRecentUsage = await this.usageRepository.addFirstDailyUsage(userUuid); + } + + const mostRecentUsageNextDay = new Date(mostRecentUsage.period); + mostRecentUsageNextDay.setUTCDate(mostRecentUsageNextDay.getUTCDate() + 1); + + const totalStorageChanged = await this.fileRepository.sumFileSizesSinceDate( + user.id, + mostRecentUsageNextDay, + ); + + const totalUsage = await this.usageRepository.getUserUsage(userUuid); + + return { + drive: + totalUsage.total_yearly_delta + + totalUsage.total_monthly_delta + + totalStorageChanged, + id: user.email, + }; + } +} diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 81f02464a..6636a7d5d 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -420,6 +420,11 @@ export class UserController { return this.userUseCases.getAuthTokens(user); } + @Get('/usage') + getUser(@UserDecorator() user: User) { + return this.userUseCases.getUserUsage(user); + } + @Patch('password') @ApiBearerAuth() async updatePassword( diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index c51a385fb..87566b3ad 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -47,6 +47,7 @@ import { FeatureLimitModule } from '../feature-limit/feature-limit.module'; import { WorkspacesModule } from '../workspaces/workspaces.module'; import { SequelizeWorkspaceRepository } from '../workspaces/repositories/workspaces.repository'; import { UserNotificationTokensModel } from './user-notification-tokens.model'; +import { UsageModule } from '../usage/usage.module'; @Module({ imports: [ @@ -74,6 +75,7 @@ import { UserNotificationTokensModel } from './user-notification-tokens.model'; SecurityModule, forwardRef(() => FeatureLimitModule), forwardRef(() => WorkspacesModule), + UsageModule, ], controllers: [UserController], providers: [ diff --git a/src/modules/user/user.usecase.ts b/src/modules/user/user.usecase.ts index b3371b378..f675e94be 100644 --- a/src/modules/user/user.usecase.ts +++ b/src/modules/user/user.usecase.ts @@ -72,6 +72,7 @@ import { SequelizeFeatureLimitsRepository } from '../feature-limit/feature-limit import { SequelizeWorkspaceRepository } from '../workspaces/repositories/workspaces.repository'; import { UserNotificationTokens } from './user-notification-tokens.domain'; import { RegisterNotificationTokenDto } from './dto/register-notification-token.dto'; +import { UsageUseCases } from '../usage/usage.usecase'; class ReferralsNotAvailableError extends Error { constructor() { @@ -150,6 +151,7 @@ export class UserUseCases { private readonly mailerService: MailerService, private readonly mailLimitRepository: SequelizeMailLimitRepository, private readonly featureLimitRepository: SequelizeFeatureLimitsRepository, + private readonly usageUseCases: UsageUseCases, ) {} findByEmail(email: User['email']): Promise { @@ -198,6 +200,10 @@ export class UserUseCases { }); } + async getUserUsage(user: User) { + return this.usageUseCases.getUserUsage(user); + } + async applyReferral( userId: UserAttributes['id'], referralKey: ReferralAttributes['key'],