Skip to content

Commit 231786e

Browse files
authored
[PB-2394] feat: calculate yesterday usage and first usage on first /usage call (#668)
* feat: add get usage incrementally calculation in background when the current /usage endpoint is hit * feat(usage): add tests for usage.domain * chore(usage): added tests to usage service * chore: remove logging * fix: use CURRENT_DATE so it uses Database default timezone (UTC) * fix: added tests and fixed date when calling createMonthlyUsage in getUserUsedStorageIncrementally * refactor: change repository function name
1 parent 3a2bb63 commit 231786e

File tree

9 files changed

+693
-6
lines changed

9 files changed

+693
-6
lines changed

src/lib/time.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@ export class Time {
1818
return new Date();
1919
}
2020

21-
public static dateWithDaysAdded(days: number): Date {
22-
const date = Time.now();
23-
date.setDate(date.getDate() + days);
24-
return date;
21+
public static dateWithDaysAdded(days: number, initialDate?: Date): Date {
22+
const date = dayjs(initialDate ?? Time.now()).utc();
23+
return date.add(days, 'day').toDate();
2524
}
2625

2726
public static setTime(now: Date): void {

src/modules/file/file.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { SharingModule } from '../sharing/sharing.module';
1313
import { WorkspacesModule } from '../workspaces/workspaces.module';
1414
import { UserModule } from '../user/user.module';
1515
import { NotificationModule } from '../../externals/notifications/notifications.module';
16+
import { UsageModule } from '../usage/usage.module';
1617

1718
@Module({
1819
imports: [
@@ -25,6 +26,7 @@ import { NotificationModule } from '../../externals/notifications/notifications.
2526
CryptoModule,
2627
UserModule,
2728
NotificationModule,
29+
UsageModule,
2830
],
2931
controllers: [FileController],
3032
providers: [SequelizeFileRepository, FileUseCases],

src/modules/file/file.repository.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ export interface FileRepository {
125125
order?: [keyof FileModel, 'ASC' | 'DESC'][],
126126
): Promise<File[]>;
127127
deleteUserTrashedFilesBatch(userId: number, limit: number): Promise<number>;
128+
sumFileSizeDeltaBetweenDates(
129+
userId: FileAttributes['userId'],
130+
sinceDate: Date,
131+
untilDate?: Date,
132+
): Promise<number>;
128133
}
129134

130135
@Injectable()
@@ -893,4 +898,50 @@ export class SequelizeFileRepository implements FileRepository {
893898

894899
return Number(result[0]['total']) as unknown as number;
895900
}
901+
902+
async sumFileSizeDeltaBetweenDates(
903+
userId: FileAttributes['userId'],
904+
sinceDate: Date,
905+
untilDate?: Date,
906+
): Promise<number> {
907+
const timeCondition = {
908+
[Op.gte]: sinceDate,
909+
...(untilDate ? { [Op.lte]: untilDate } : null),
910+
};
911+
912+
const result = await this.fileModel.findAll({
913+
attributes: [
914+
[
915+
Sequelize.literal(`
916+
SUM(
917+
CASE
918+
WHEN status = 'DELETED' AND date_trunc('day', created_at) = date_trunc('day', updated_at) THEN 0
919+
WHEN status = 'DELETED' THEN -size
920+
ELSE size
921+
END
922+
)
923+
`),
924+
'total',
925+
],
926+
],
927+
where: {
928+
userId,
929+
[Op.or]: [
930+
{
931+
status: {
932+
[Op.ne]: 'DELETED',
933+
},
934+
createdAt: timeCondition,
935+
},
936+
{
937+
status: 'DELETED',
938+
updatedAt: timeCondition,
939+
},
940+
],
941+
},
942+
raw: true,
943+
});
944+
945+
return Number(result[0]['total']) as unknown as number;
946+
}
896947
}

src/modules/file/file.usecase.spec.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
newFolder,
2424
newUser,
2525
newWorkspace,
26+
newUsage,
2627
} from '../../../test/fixtures';
2728
import { FolderUseCases } from '../folder/folder.usecase';
2829
import { v4 } from 'uuid';
@@ -31,6 +32,8 @@ import { SharingItemType } from '../sharing/sharing.domain';
3132
import { CreateFileDto } from './dto/create-file.dto';
3233
import { UpdateFileMetaDto } from './dto/update-file-meta.dto';
3334
import { ThumbnailUseCases } from '../thumbnail/thumbnail.usecase';
35+
import { UsageService } from '../usage/usage.service';
36+
import { Time } from '../../lib/time';
3437

3538
const fileId = '6295c99a241bb000083f1c6a';
3639
const userId = 1;
@@ -43,6 +46,7 @@ describe('FileUseCases', () => {
4346
let bridgeService: BridgeService;
4447
let cryptoService: CryptoService;
4548
let thumbnailUseCases: ThumbnailUseCases;
49+
let usageService: UsageService;
4650

4751
const userMocked = newUser();
4852

@@ -61,6 +65,7 @@ describe('FileUseCases', () => {
6165
cryptoService = module.get<CryptoService>(CryptoService);
6266
sharingService = module.get<SharingService>(SharingService);
6367
thumbnailUseCases = module.get<ThumbnailUseCases>(ThumbnailUseCases);
68+
usageService = module.get<UsageService>(UsageService);
6469
});
6570

6671
afterEach(() => {
@@ -1443,4 +1448,106 @@ describe('FileUseCases', () => {
14431448
});
14441449
});
14451450
});
1451+
1452+
describe('getUserUsedStorageIncrementally', () => {
1453+
beforeEach(() => {
1454+
jest.useFakeTimers();
1455+
});
1456+
1457+
afterEach(() => {
1458+
jest.useRealTimers();
1459+
jest.clearAllMocks();
1460+
});
1461+
1462+
it('When user has no existing usage, then it should create first usage calculation', async () => {
1463+
const mockUser = newUser();
1464+
const today = new Date('2024-01-02T10:00:00Z');
1465+
const mockFirstUsage = newUsage({
1466+
attributes: { period: new Date('2024-01-01T00:00:00Z') },
1467+
});
1468+
1469+
// Set today to the next period start date according to mockUsage
1470+
jest.setSystemTime(today);
1471+
1472+
jest
1473+
.spyOn(usageService, 'getUserMostRecentUsage')
1474+
.mockResolvedValue(null);
1475+
jest
1476+
.spyOn(usageService, 'createFirstUsageCalculation')
1477+
.mockResolvedValue(mockFirstUsage);
1478+
1479+
await service.getUserUsedStorageIncrementally(mockUser);
1480+
1481+
expect(usageService.getUserMostRecentUsage).toHaveBeenCalledWith(
1482+
mockUser.uuid,
1483+
);
1484+
expect(usageService.createFirstUsageCalculation).toHaveBeenCalledWith(
1485+
mockUser.uuid,
1486+
);
1487+
});
1488+
1489+
it('When user has recent usage and is up to date, then it should not create new usage', async () => {
1490+
const mockUser = newUser();
1491+
const today = new Date('2024-01-02T00:00:00Z');
1492+
1493+
const mockUsage = newUsage({
1494+
attributes: { period: new Date('2024-01-01T00:00:00Z') },
1495+
});
1496+
1497+
// Set today to the next period start date according to mockUsage
1498+
jest.setSystemTime(today);
1499+
1500+
jest
1501+
.spyOn(usageService, 'getUserMostRecentUsage')
1502+
.mockResolvedValue(mockUsage);
1503+
1504+
await service.getUserUsedStorageIncrementally(mockUser);
1505+
1506+
expect(usageService.getUserMostRecentUsage).toHaveBeenCalledWith(
1507+
mockUser.uuid,
1508+
);
1509+
expect(usageService.createMonthlyUsage).not.toHaveBeenCalled();
1510+
expect(
1511+
fileRepository.sumFileSizeDeltaBetweenDates,
1512+
).not.toHaveBeenCalled();
1513+
});
1514+
1515+
it('When user has recent usage but needs update, then it should calculate gap delta and create monthly usage', async () => {
1516+
const mockUser = newUser();
1517+
const today = new Date('2024-01-04T10:00:00Z');
1518+
const yesterday = Time.dateWithDaysAdded(-1, today);
1519+
const mockUsage = newUsage({
1520+
attributes: { period: new Date('2024-01-01T00:00:00Z') },
1521+
});
1522+
const mockGapDelta = 500;
1523+
1524+
// Set today to a date after the next period start date according to mockUsage
1525+
jest.setSystemTime(today);
1526+
jest
1527+
.spyOn(usageService, 'getUserMostRecentUsage')
1528+
.mockResolvedValue(mockUsage);
1529+
jest
1530+
.spyOn(fileRepository, 'sumFileSizeDeltaBetweenDates')
1531+
.mockResolvedValue(mockGapDelta);
1532+
jest
1533+
.spyOn(usageService, 'createMonthlyUsage')
1534+
.mockResolvedValue(undefined);
1535+
1536+
await service.getUserUsedStorageIncrementally(mockUser);
1537+
1538+
expect(usageService.getUserMostRecentUsage).toHaveBeenCalledWith(
1539+
mockUser.uuid,
1540+
);
1541+
expect(fileRepository.sumFileSizeDeltaBetweenDates).toHaveBeenCalledWith(
1542+
mockUser.id,
1543+
mockUsage.getNextPeriodStartDate(),
1544+
Time.endOfDay(yesterday),
1545+
);
1546+
expect(usageService.createMonthlyUsage).toHaveBeenCalledWith(
1547+
mockUser.uuid,
1548+
yesterday,
1549+
mockGapDelta,
1550+
);
1551+
});
1552+
});
14461553
});

src/modules/file/file.usecase.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import { getPathFileData } from '../../lib/path';
3939
import { isStringEmpty } from '../../lib/validators';
4040
import { FileModel } from './file.model';
4141
import { ThumbnailUseCases } from '../thumbnail/thumbnail.usecase';
42+
import { UsageService } from '../usage/usage.service';
43+
import { Time } from '../../lib/time';
4244

4345
export type SortParamsFile = Array<[SortableFileAttributes, 'ASC' | 'DESC']>;
4446

@@ -53,6 +55,7 @@ export class FileUseCases {
5355
private readonly network: BridgeService,
5456
private readonly cryptoService: CryptoService,
5557
private readonly thumbnailUsecases: ThumbnailUseCases,
58+
private readonly usageService: UsageService,
5659
) {}
5760

5861
getByUuid(uuid: FileAttributes['uuid']): Promise<File> {
@@ -73,10 +76,44 @@ export class FileUseCases {
7376
return this.fileRepository.findByUuids(uuids, { userId: user.id });
7477
}
7578

76-
getUserUsedStorage(user: User) {
79+
async getUserUsedStorage(user: User): Promise<number> {
80+
await this.getUserUsedStorageIncrementally(user);
7781
return this.fileRepository.sumExistentFileSizes(user.id);
7882
}
7983

84+
async getUserUsedStorageIncrementally(user: User) {
85+
let mostRecentUsage = await this.usageService.getUserMostRecentUsage(
86+
user.uuid,
87+
);
88+
89+
if (!mostRecentUsage) {
90+
mostRecentUsage = await this.usageService.createFirstUsageCalculation(
91+
user.uuid,
92+
);
93+
}
94+
95+
const nextPeriodStart = mostRecentUsage.getNextPeriodStartDate();
96+
const isUpToDate = Time.isToday(nextPeriodStart);
97+
98+
if (!isUpToDate) {
99+
const yesterday = Time.dateWithDaysAdded(-1);
100+
const yesterdayEndOfDay = Time.endOfDay(yesterday);
101+
102+
const gapDelta = await this.fileRepository.sumFileSizeDeltaBetweenDates(
103+
user.id,
104+
nextPeriodStart,
105+
yesterdayEndOfDay,
106+
);
107+
await this.usageService.createMonthlyUsage(
108+
user.uuid,
109+
yesterday,
110+
gapDelta,
111+
);
112+
}
113+
114+
// TODO: add calculation of the current day and sum of all the usages
115+
}
116+
80117
async deleteFilePermanently(
81118
user: User,
82119
where: Partial<File>,

0 commit comments

Comments
 (0)