Skip to content

Commit

Permalink
feat: create usage module
Browse files Browse the repository at this point in the history
  • Loading branch information
apsantiso committed Nov 26, 2024
1 parent bc770c5 commit 24cdf62
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 38 deletions.
48 changes: 48 additions & 0 deletions src/modules/file/file.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export interface FileRepository {
userId: User['id'],
fileIds: FileAttributes['fileId'][],
): Promise<File[]>;
sumFileSizesSinceDate(
userId: FileAttributes['userId'],
sinceDate: Date,
): Promise<number>;
}

@Injectable()
Expand Down Expand Up @@ -590,6 +594,50 @@ export class SequelizeFileRepository implements FileRepository {
});
}

async sumFileSizesSinceDate(
userId: FileAttributes['userId'],
sinceDate: Date,
): Promise<number> {
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<number> {
Expand Down
15 changes: 10 additions & 5 deletions src/modules/usage/usage.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
109 changes: 76 additions & 33 deletions src/modules/usage/usage.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class SequelizeUsageRepository {
return mostRecentUsage ? this.toDomain(mostRecentUsage) : null;
}

public async addFirstDailyUsage(userUuid: string): Promise<Usage[]> {
public async addFirstDailyUsage(userUuid: string): Promise<Usage> {
const query = `
INSERT INTO public.usages (id, user_id, delta, period, type, created_at, updated_at)
SELECT
Expand All @@ -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
Expand All @@ -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<Usage[]> {
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 {
Expand Down
41 changes: 41 additions & 0 deletions src/modules/usage/usage.usecase.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
}
5 changes: 5 additions & 0 deletions src/modules/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions src/modules/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -74,6 +75,7 @@ import { UserNotificationTokensModel } from './user-notification-tokens.model';
SecurityModule,
forwardRef(() => FeatureLimitModule),
forwardRef(() => WorkspacesModule),
UsageModule,
],
controllers: [UserController],
providers: [
Expand Down
6 changes: 6 additions & 0 deletions src/modules/user/user.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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<User | null> {
Expand Down Expand Up @@ -198,6 +200,10 @@ export class UserUseCases {
});
}

async getUserUsage(user: User) {
return this.usageUseCases.getUserUsage(user);
}

async applyReferral(
userId: UserAttributes['id'],
referralKey: ReferralAttributes['key'],
Expand Down

0 comments on commit 24cdf62

Please sign in to comment.