Skip to content

Commit

Permalink
feat: added daily usage rows on file size change, added daily usage c…
Browse files Browse the repository at this point in the history
…ount towards usage calculation
  • Loading branch information
apsantiso committed Dec 5, 2024
1 parent 0a03f0c commit 73fd5ed
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ module.exports = {
(f.status != 'DELETED' AND f.created_at BETWEEN CURRENT_DATE - INTERVAL '1 day' AND CURRENT_DATE - INTERVAL '1 millisecond')
OR
(f.status = 'DELETED' AND f.updated_at BETWEEN CURRENT_DATE - INTERVAL '1 day' AND CURRENT_DATE - INTERVAL '1 millisecond')
OR
(f.status != 'DELETED' AND f.modification_time BETWEEN CURRENT_DATE - INTERVAL '1 day' AND CURRENT_DATE - INTERVAL '1 millisecond')
)
GROUP BY
u.uuid;
Expand Down
4 changes: 2 additions & 2 deletions migrations/20241120010831-create-yearly-usage-procedure.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {
WHERE
period >= date_trunc('year', CURRENT_DATE) - INTERVAL '1 year'
AND period < date_trunc('year', CURRENT_DATE)
AND type = 'monthly'
AND type IN ('monthly', 'daily')
GROUP BY
user_id
)
Expand Down Expand Up @@ -46,7 +46,7 @@ module.exports = {
WHERE
period >= date_trunc('year', CURRENT_DATE) - INTERVAL '1 year'
AND period < date_trunc('year', CURRENT_DATE)
AND type = 'monthly';
AND type IN ('monthly', 'daily');
END;
$$;
`);
Expand Down

This file was deleted.

44 changes: 44 additions & 0 deletions src/modules/usage/usage.domain.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { v4 } from 'uuid';
import { Usage, UsageType } from './usage.domain';

describe('Usage Domain', () => {
const usageAttributes = {
id: v4(),
userId: v4(),
delta: 100,
period: new Date(),
type: UsageType.Daily,
createdAt: new Date(),
updatedAt: new Date(),
};

it('When Usage type is Yearly, then isYearly should return true', () => {
const usage = Usage.build({ ...usageAttributes, type: UsageType.Yearly });

expect(usage.isYearly()).toBe(true);
expect(usage.isMonthly()).toBe(false);
expect(usage.isDaily()).toBe(false);
});

it('When Usage type is Monthly, then isMonthly should return true', () => {
const usage = Usage.build({ ...usageAttributes, type: UsageType.Monthly });

expect(usage.isYearly()).toBe(false);
expect(usage.isMonthly()).toBe(true);
expect(usage.isDaily()).toBe(false);
});

it('When Usage type is Daily, then isDaily should return true', () => {
const usage = Usage.build({ ...usageAttributes, type: UsageType.Daily });

expect(usage.isYearly()).toBe(false);
expect(usage.isMonthly()).toBe(false);
expect(usage.isDaily()).toBe(true);
});

it('When instance is created, then period should be parsed to date', () => {
const usage = Usage.build(usageAttributes);

expect(usage.period).toBeInstanceOf(Date);
});
});
12 changes: 0 additions & 12 deletions src/modules/usage/usage.domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,4 @@ export class Usage implements UsageAttributes {
isDaily(): boolean {
return this.type === UsageType.Daily;
}

toJSON(): Partial<UsageAttributes> {
return {
id: this.id,
userId: this.userId,
delta: this.delta,
period: this.period,
type: this.type,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
};
}
}
153 changes: 89 additions & 64 deletions src/modules/usage/usage.repository.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { InjectModel } from '@nestjs/sequelize';
import { Injectable } from '@nestjs/common';
import { UsageModel } from './usage.model';
import { Usage } from './usage.domain';
import { Usage, UsageType } from './usage.domain';
import { Op } from 'sequelize';

@Injectable()
export class SequelizeUsageRepository {
Expand All @@ -24,9 +25,14 @@ export class SequelizeUsageRepository {
return this.toDomain(newUsage);
}

public async getMostRecentUsage(userUuid: string): Promise<Usage | null> {
public async getMostRecentMonthlyOrYearlyUsage(
userUuid: string,
): Promise<Usage | null> {
const mostRecentUsage = await this.usageModel.findOne({
where: { userId: userUuid },
where: {
userId: userUuid,
[Op.or]: [{ type: UsageType.Monthly }, { type: UsageType.Yearly }],
},
order: [['period', 'DESC']],
});

Expand All @@ -45,7 +51,7 @@ export class SequelizeUsageRepository {
return mostRecentUsage ? this.toDomain(mostRecentUsage) : null;
}

public async addFirstDailyUsage(userUuid: string): Promise<Usage> {
public async addFirstMonthlyUsage(userUuid: string): Promise<Usage> {
const query = `
INSERT INTO public.usages (id, user_id, delta, period, type, created_at, updated_at)
SELECT
Expand Down Expand Up @@ -79,66 +85,85 @@ export class SequelizeUsageRepository {
public async getUserUsage(userUuid: string) {
const query = `
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;
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,
date_trunc('month', period) AS month,
SUM(delta) AS total_delta
FROM
public.usages
WHERE
type = 'monthly'
AND user_id = :userUuid
GROUP BY
date_trunc('year', period), date_trunc('month', period)
),
daily_sums AS (
SELECT
date_trunc('year', period) AS year,
date_trunc('month', period) AS month,
SUM(delta) AS total_delta
FROM
public.usages
WHERE
type = 'daily'
AND user_id = :userUuid
GROUP BY
date_trunc('year', period), date_trunc('month', period)
),
combined_monthly_and_daily AS (
SELECT
COALESCE(m.year, d.year) AS year,
COALESCE(m.month, d.month) AS month,
COALESCE(m.total_delta, 0) + COALESCE(d.total_delta, 0) AS total_delta
FROM
monthly_sums m
FULL JOIN daily_sums d ON m.year = d.year AND m.month = d.month
),
combined_sums AS (
SELECT
y.year,
NULL AS month,
y.total_delta AS total_delta
FROM
yearly_sums y
UNION ALL
SELECT
cmd.year,
cmd.month,
cmd.total_delta
FROM
combined_monthly_and_daily cmd
LEFT JOIN yearly_sums ys ON cmd.year = ys.year
WHERE
ys.year IS NULL -- Exclude months and days where a yearly row exists
)
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, {
Expand Down
36 changes: 27 additions & 9 deletions src/modules/usage/usage.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,28 @@ export class UsageUseCases {
const userUuid = user.uuid;

let mostRecentUsage =
await this.usageRepository.getMostRecentUsage(userUuid);
await this.usageRepository.getMostRecentMonthlyOrYearlyUsage(userUuid);

if (!mostRecentUsage) {
mostRecentUsage = await this.usageRepository.addFirstDailyUsage(userUuid);
mostRecentUsage =
await this.usageRepository.addFirstMonthlyUsage(userUuid);
}

const mostRecentUsageNextDay = new Date(mostRecentUsage.period);
mostRecentUsageNextDay.setUTCDate(mostRecentUsageNextDay.getUTCDate() + 1);
const calculateSizeChangesSince = new Date(mostRecentUsage.period);

if (mostRecentUsage.isYearly()) {
calculateSizeChangesSince.setUTCFullYear(
calculateSizeChangesSince.getUTCFullYear() + 1,
);
} else {
calculateSizeChangesSince.setUTCDate(
calculateSizeChangesSince.getUTCDate() + 1,
);
}

const totalStorageChanged = await this.fileRepository.sumFileSizesSinceDate(
user.id,
mostRecentUsageNextDay,
calculateSizeChangesSince,
);

const totalUsage = await this.usageRepository.getUserUsage(userUuid);
Expand Down Expand Up @@ -108,15 +118,23 @@ export class UsageUseCases {
user: User,
oldFileData: File,
newFileData: File,
) {
const isFileCreatedToday = Time.isToday(newFileData.createdAt);
): Promise<Usage | null> {
const maybeExistentUsage =
await this.usageRepository.getMostRecentMonthlyOrYearlyUsage(user.uuid);

if (isFileCreatedToday) {
return;
if (!maybeExistentUsage) {
return null;
}

const delta = Number(newFileData.size) - Number(oldFileData.size);

// Files created the same day are going to be included in the next cronjob run
const isFileCreatedToday = Time.isToday(newFileData.createdAt);

if (delta === 0 || isFileCreatedToday) {
return null;
}

return this.createDailyUsage(user.uuid, new Date(), delta);
}
}

0 comments on commit 73fd5ed

Please sign in to comment.