diff --git a/src/checkpoints/checkpoints.controller.spec.ts b/src/checkpoints/checkpoints.controller.spec.ts index 3e731b39..2c5a1c67 100644 --- a/src/checkpoints/checkpoints.controller.spec.ts +++ b/src/checkpoints/checkpoints.controller.spec.ts @@ -6,6 +6,7 @@ import { CheckpointsController } from '~/checkpoints/checkpoints.controller'; import { CheckpointsService } from '~/checkpoints/checkpoints.service'; import { CheckpointDetailsModel } from '~/checkpoints/models/checkpoint-details.model'; import { CheckpointScheduleModel } from '~/checkpoints/models/checkpoint-schedule.model'; +import { PeriodComplexityModel } from '~/checkpoints/models/period-complexity.model'; import { ScheduleComplexityModel } from '~/checkpoints/models/schedule-complexity.model'; import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ResultsModel } from '~/common/models/results.model'; @@ -13,7 +14,7 @@ import { testValues } from '~/test-utils/consts'; import { MockCheckpoint, MockCheckpointSchedule } from '~/test-utils/mocks'; import { MockCheckpointsService } from '~/test-utils/service-mocks'; -const { did, signer, txResult } = testValues; +const { did, signer, txResult, ticker } = testValues; describe('CheckpointsController', () => { let controller: CheckpointsController; @@ -41,7 +42,6 @@ describe('CheckpointsController', () => { const createdAt = new Date(); const totalSupply = new BigNumber(1000); const id = new BigNumber(1); - const ticker = 'TICKER'; const mockCheckpoint = new MockCheckpoint(); mockCheckpoint.createdAt.mockResolvedValue(createdAt); @@ -83,10 +83,7 @@ describe('CheckpointsController', () => { it('should return the list of Checkpoints created on an Asset', async () => { mockCheckpointsService.findAllByTicker.mockResolvedValue(mockCheckpoints); - const result = await controller.getCheckpoints( - { ticker: 'TICKER' }, - { size: new BigNumber(1) } - ); + const result = await controller.getCheckpoints({ ticker }, { size: new BigNumber(1) }); expect(result).toEqual(mockResult); }); @@ -95,7 +92,7 @@ describe('CheckpointsController', () => { mockCheckpointsService.findAllByTicker.mockResolvedValue(mockCheckpoints); const result = await controller.getCheckpoints( - { ticker: 'TICKER' }, + { ticker }, { size: new BigNumber(1), start: 'START_KEY' } ); @@ -115,7 +112,7 @@ describe('CheckpointsController', () => { signer: 'signer', }; - const result = await controller.createCheckpoint({ ticker: 'TICKER' }, body); + const result = await controller.createCheckpoint({ ticker }, body); expect(result).toEqual({ ...txResult, @@ -144,12 +141,12 @@ describe('CheckpointsController', () => { mockCheckpointsService.findSchedulesByTicker.mockResolvedValue(mockSchedules); - const result = await controller.getSchedules({ ticker: 'TICKER' }); + const result = await controller.getSchedules({ ticker }); const mockResult = [ new CheckpointScheduleModel({ id: new BigNumber(1), - ticker: 'TICKER', + ticker, pendingPoints: [mockDate], expiryDate: null, remainingCheckpoints: new BigNumber(1), @@ -164,8 +161,10 @@ describe('CheckpointsController', () => { describe('getSchedule', () => { it('should call the service and return the Checkpoint Schedule details', async () => { const mockDate = new Date('10/14/1987'); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { getCheckpoints, ...schedule } = new MockCheckpointSchedule(); const mockScheduleWithDetails = { - schedule: new MockCheckpointSchedule(), + schedule, details: { remainingCheckpoints: new BigNumber(1), nextCheckpointDate: mockDate, @@ -173,7 +172,7 @@ describe('CheckpointsController', () => { }; mockCheckpointsService.findScheduleById.mockResolvedValue(mockScheduleWithDetails); - const result = await controller.getSchedule({ ticker: 'TICKER', id: new BigNumber(1) }); + const result = await controller.getSchedule({ ticker, id: new BigNumber(1) }); const mockResult = new CheckpointScheduleModel({ id: mockScheduleWithDetails.schedule.id, @@ -197,8 +196,10 @@ describe('CheckpointsController', () => { }; mockCheckpointsService.createScheduleByTicker.mockResolvedValue(response); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { getCheckpoints, ...schedule } = new MockCheckpointSchedule(); const mockScheduleWithDetails = { - schedule: new MockCheckpointSchedule(), + schedule, details: { remainingCheckpoints: new BigNumber(1), nextCheckpointDate: mockDate, @@ -211,7 +212,7 @@ describe('CheckpointsController', () => { points: [mockDate], }; - const result = await controller.createSchedule({ ticker: 'TICKER' }, body); + const result = await controller.createSchedule({ ticker }, body); const mockCreatedSchedule = new CheckpointScheduleModel({ id: mockScheduleWithDetails.schedule.id, @@ -262,7 +263,7 @@ describe('CheckpointsController', () => { const result = await controller.getHolders( { - ticker: 'TICKER', + ticker, id: new BigNumber(1), }, { size: new BigNumber(10) } @@ -275,7 +276,6 @@ describe('CheckpointsController', () => { describe('getAssetBalance', () => { it('should return the balance of an Asset for an Identity at a given Checkpoint', async () => { const balance = new BigNumber(10); - const ticker = 'TICKER'; const id = new BigNumber(1); const balanceModel = new IdentityBalanceModel({ balance, identity: did }); @@ -297,10 +297,7 @@ describe('CheckpointsController', () => { it('should return the transaction details', async () => { mockCheckpointsService.deleteScheduleByTicker.mockResolvedValue(txResult); - const result = await controller.deleteSchedule( - { id: new BigNumber(1), ticker: 'TICKER' }, - { signer } - ); + const result = await controller.deleteSchedule({ id: new BigNumber(1), ticker }, { signer }); expect(result).toEqual(txResult); }); @@ -311,7 +308,6 @@ describe('CheckpointsController', () => { const createdAt = new Date(); const totalSupply = new BigNumber(1000); const id = new BigNumber(1); - const ticker = 'TICKER'; const mockCheckpoint = new MockCheckpoint(); mockCheckpointsService.findCheckpointsByScheduleId.mockResolvedValue([ @@ -354,4 +350,28 @@ describe('CheckpointsController', () => { ]); }); }); + + describe('getPeriodComplexity', () => { + it('should call the service and return the Checkpoint Schedule complexity for given period', async () => { + const complexity = new BigNumber(10000); + mockCheckpointsService.getComplexityForPeriod.mockResolvedValue(complexity); + const start = new Date(); + const end = new Date(); + const result = await controller.getPeriodComplexity( + { ticker, id: new BigNumber(1) }, + { start, end } + ); + + const mockResult = new PeriodComplexityModel({ + complexity, + }); + expect(result).toEqual(mockResult); + expect(mockCheckpointsService.getComplexityForPeriod).toBeCalledWith( + ticker, + new BigNumber(1), + start, + end + ); + }); + }); }); diff --git a/src/checkpoints/checkpoints.controller.ts b/src/checkpoints/checkpoints.controller.ts index f4f06922..a5cd9c6d 100644 --- a/src/checkpoints/checkpoints.controller.ts +++ b/src/checkpoints/checkpoints.controller.ts @@ -17,10 +17,12 @@ import { CheckpointsService } from '~/checkpoints/checkpoints.service'; import { CheckpointParamsDto } from '~/checkpoints/dto/checkpoint.dto'; import { CheckPointBalanceParamsDto } from '~/checkpoints/dto/checkpoint-balance.dto'; import { CreateCheckpointScheduleDto } from '~/checkpoints/dto/create-checkpoint-schedule.dto'; +import { PeriodQueryDto } from '~/checkpoints/dto/period-query.dto'; import { CheckpointDetailsModel } from '~/checkpoints/models/checkpoint-details.model'; import { CheckpointScheduleModel } from '~/checkpoints/models/checkpoint-schedule.model'; import { CreatedCheckpointModel } from '~/checkpoints/models/created-checkpoint.model'; import { CreatedCheckpointScheduleModel } from '~/checkpoints/models/created-checkpoint-schedule.model'; +import { PeriodComplexityModel } from '~/checkpoints/models/period-complexity.model'; import { ScheduleComplexityModel } from '~/checkpoints/models/schedule-complexity.model'; import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; import { IsTicker } from '~/common/decorators/validation'; @@ -463,7 +465,7 @@ export class CheckpointsController { new CheckpointDetailsModel({ id, createdAt, totalSupply }) ); } - + @ApiOperation({ summary: 'Fetch Asset Schedules complexity', }) @@ -497,4 +499,52 @@ export class CheckpointsController { }); }); } + + @ApiOperation({ + summary: 'Fetch details of an Asset Checkpoint Schedule', + }) + @ApiParam({ + name: 'ticker', + description: 'The ticker of the Asset whose Checkpoint Schedule is to be fetched', + type: 'string', + example: 'TICKER', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Checkpoint Schedule to be fetched', + type: 'string', + example: '1', + }) + @ApiQuery({ + name: 'start', + description: 'Start date for the period for which to fetch complexity', + type: 'string', + required: false, + example: '2021-01-01', + }) + @ApiQuery({ + name: 'end', + description: 'End date for the period for which to fetch complexity', + type: 'string', + required: false, + example: '2021-01-31', + }) + @ApiOkResponse({ + description: 'The complexity of the Schedule for the given period', + type: ScheduleComplexityModel, + }) + @ApiNotFoundResponse({ + description: 'Either the Asset or the Checkpoint Schedule does not exist', + }) + @Get('schedules/:id/complexity') + public async getPeriodComplexity( + @Param() { ticker, id }: CheckpointScheduleParamsDto, + @Query() { start, end }: PeriodQueryDto + ): Promise { + const complexity = await this.checkpointsService.getComplexityForPeriod(ticker, id, start, end); + + return new PeriodComplexityModel({ + complexity, + }); + } } diff --git a/src/checkpoints/checkpoints.service.spec.ts b/src/checkpoints/checkpoints.service.spec.ts index 8cbe6b27..56c4acc6 100644 --- a/src/checkpoints/checkpoints.service.spec.ts +++ b/src/checkpoints/checkpoints.service.spec.ts @@ -477,4 +477,66 @@ describe('CheckpointsService', () => { expect(mockAssetsService.findFungible).toBeCalledWith('TICKER'); }); }); + + describe('getComplexityForPeriod', () => { + let mockAsset: MockAsset; + const ticker = 'TICKER'; + const id = new BigNumber(1); + + const CHECKPOINT_DATE = new Date('2021-01-01'); + const START_DATE = new Date('2021-01-15'); + const END_DATE = new Date('2021-01-31'); + const PENDING_POINT_DATE_1 = new Date('2021-01-21'); + const PENDING_POINT_DATE_2 = new Date('2021-01-15'); + + beforeEach(() => { + mockAsset = new MockAsset(); + mockAssetsService.findFungible.mockResolvedValue(mockAsset); + }); + + const setupMocks = (createdAtDate: Date, pendingPoints: Date[] = []): void => { + const schedule = new MockCheckpointSchedule(); + schedule.pendingPoints = pendingPoints; + const mockCheckpoint = new MockCheckpoint(); + mockCheckpoint.createdAt.mockResolvedValue(createdAtDate); + schedule.getCheckpoints.mockResolvedValue([mockCheckpoint]); + + const mockScheduleWithDetails = { + schedule, + }; + mockAsset.checkpoints.schedules.getOne.mockResolvedValue(mockScheduleWithDetails); + }; + + it('should equal the length of all checkpoints for schedule if no period given', async () => { + setupMocks(new Date()); + + const result = await service.getComplexityForPeriod(ticker, id); + + expect(result).toEqual(new BigNumber(1)); + }); + + it('should filter out checkpoints that are outside given period', async () => { + setupMocks(CHECKPOINT_DATE, [PENDING_POINT_DATE_2]); + + const result = await service.getComplexityForPeriod(ticker, id, START_DATE, END_DATE); + + expect(result).toEqual(new BigNumber(1)); + }); + + it('should filter if just start is given', async () => { + setupMocks(CHECKPOINT_DATE, [PENDING_POINT_DATE_1]); + + const result = await service.getComplexityForPeriod(ticker, id, START_DATE); + + expect(result).toEqual(new BigNumber(1)); + }); + + it('should filter if just end is given', async () => { + setupMocks(CHECKPOINT_DATE, [PENDING_POINT_DATE_1]); + + const result = await service.getComplexityForPeriod(ticker, id, undefined, START_DATE); + + expect(result).toEqual(new BigNumber(1)); + }); + }); }); diff --git a/src/checkpoints/checkpoints.service.ts b/src/checkpoints/checkpoints.service.ts index f783a558..5d6b9a62 100644 --- a/src/checkpoints/checkpoints.service.ts +++ b/src/checkpoints/checkpoints.service.ts @@ -152,4 +152,27 @@ export class CheckpointsService { return { schedules, maxComplexity }; } + + public async getComplexityForPeriod( + ticker: string, + id: BigNumber, + start?: Date, + end?: Date + ): Promise { + const { schedule } = await this.findScheduleById(ticker, id); + + const checkpoints = await schedule.getCheckpoints(); + const pendingPoints = schedule.pendingPoints; + const checkpointDatePromises = checkpoints.map(checkpoint => checkpoint.createdAt()); + + const pastCheckpoints = await Promise.all(checkpointDatePromises); + + const allCheckpoints = [...pendingPoints, ...pastCheckpoints]; + + const checkpointsInPeriod = allCheckpoints.filter( + date => (!start || date >= start) && (!end || date <= end) + ); + + return new BigNumber(checkpointsInPeriod.length); + } } diff --git a/src/checkpoints/dto/period-query.dto.ts b/src/checkpoints/dto/period-query.dto.ts new file mode 100644 index 00000000..c0d02a6d --- /dev/null +++ b/src/checkpoints/dto/period-query.dto.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ + +import { IsDate, IsOptional } from 'class-validator'; + +export class PeriodQueryDto { + @IsOptional() + @IsDate() + readonly start?: Date; + + @IsOptional() + @IsDate() + readonly end?: Date; +} diff --git a/src/checkpoints/models/period-complexity.model.ts b/src/checkpoints/models/period-complexity.model.ts new file mode 100644 index 00000000..bb1523df --- /dev/null +++ b/src/checkpoints/models/period-complexity.model.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class PeriodComplexityModel { + @ApiProperty({ + description: 'Total calculated complexity for given period', + type: 'string', + example: '10000', + }) + @FromBigNumber() + readonly complexity: BigNumber; + + constructor(model: PeriodComplexityModel) { + Object.assign(this, model); + } +} diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index d8f10bdd..a12223f2 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -221,6 +221,7 @@ export class MockCheckpointsService { findOne = jest.fn(); findCheckpointsByScheduleId = jest.fn(); getComplexityForAsset = jest.fn(); + getComplexityForPeriod = jest.fn(); } export class MockAuthService {