diff --git a/src/corporate-actions/corporate-actions.controller.spec.ts b/src/corporate-actions/corporate-actions.controller.spec.ts index 66f413ab..f43afdfb 100644 --- a/src/corporate-actions/corporate-actions.controller.spec.ts +++ b/src/corporate-actions/corporate-actions.controller.spec.ts @@ -1,7 +1,10 @@ +import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { DistributionPayment, ResultSet } from '@polymeshassociation/polymesh-sdk/types'; import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; +import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ResultsModel } from '~/common/models/results.model'; import { CorporateActionsController } from '~/corporate-actions/corporate-actions.controller'; import { CorporateActionsService } from '~/corporate-actions/corporate-actions.service'; @@ -12,9 +15,10 @@ import { import { MockCorporateActionDefaultConfig } from '~/corporate-actions/mocks/corporate-action-default-config.mock'; import { MockDistributionWithDetails } from '~/corporate-actions/mocks/distribution-with-details.mock'; import { MockDistribution } from '~/corporate-actions/mocks/dividend-distribution.mock'; +import { DistributionPaymentModel } from '~/corporate-actions/models/distribution-payment.model'; import { processedTxResult, testValues } from '~/test-utils/consts'; -const { did, signer, txResult, assetId } = testValues; +const { did, signer, txResult, assetId, blockHash, blockNumber } = testValues; describe('CorporateActionsController', () => { let controller: CorporateActionsController; @@ -32,6 +36,7 @@ describe('CorporateActionsController', () => { reclaimRemainingFunds: jest.fn(), modifyCheckpoint: jest.fn(), findUnclaimedDistributionsByAsset: jest.fn(), + getPaymentHistory: jest.fn(), }; beforeEach(async () => { @@ -278,4 +283,37 @@ describe('CorporateActionsController', () => { ); }); }); + + describe('getPaymentHistory', () => { + it('should return a paginated list of payments for a specific Dividend Distribution', async () => { + const mockDistributionPayment = createMock({ + target: { did }, + amount: new BigNumber(100), + date: new Date(), + blockHash, + blockNumber, + withheldTax: new BigNumber(10), + }); + const { target, ...rest } = mockDistributionPayment; + + const mockPaginatedResult = createMock>({ + data: [mockDistributionPayment], + next: new BigNumber(2), + }); + + mockCorporateActionsService.getPaymentHistory.mockResolvedValue(mockPaginatedResult); + + const result = await controller.getPaymentHistory( + { asset: assetId, id: new BigNumber(1) }, + { size: new BigNumber(10), start: new BigNumber(0) } + ); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: [new DistributionPaymentModel({ ...rest, did: target.did })], + next: new BigNumber(2), + }) + ); + }); + }); }); diff --git a/src/corporate-actions/corporate-actions.controller.ts b/src/corporate-actions/corporate-actions.controller.ts index 604b7d5b..23959e9b 100644 --- a/src/corporate-actions/corporate-actions.controller.ts +++ b/src/corporate-actions/corporate-actions.controller.ts @@ -8,13 +8,16 @@ import { ApiTags, ApiUnprocessableEntityResponse, } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { DividendDistribution } from '@polymeshassociation/polymesh-sdk/types'; import { AssetParamsDto } from '~/assets/dto/asset-params.dto'; import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/'; import { IsAsset } from '~/common/decorators/validation'; import { IdParamsDto } from '~/common/dto/id-params.dto'; +import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ResultsModel } from '~/common/models/results.model'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; @@ -31,6 +34,7 @@ import { PayDividendsDto } from '~/corporate-actions/dto/pay-dividends.dto'; import { CorporateActionDefaultConfigModel } from '~/corporate-actions/models/corporate-action-default-config.model'; import { CorporateActionTargetsModel } from '~/corporate-actions/models/corporate-action-targets.model'; import { CreatedDividendDistributionModel } from '~/corporate-actions/models/created-dividend-distribution.model'; +import { DistributionPaymentModel } from '~/corporate-actions/models/distribution-payment.model'; import { DividendDistributionModel } from '~/corporate-actions/models/dividend-distribution.model'; import { DividendDistributionDetailsModel } from '~/corporate-actions/models/dividend-distribution-details.model'; import { TaxWithholdingModel } from '~/corporate-actions/models/tax-withholding.model'; @@ -484,4 +488,51 @@ export class CorporateActionsController { ); return handleServiceResult(result); } + + @ApiTags('dividend-distributions') + @ApiOperation({ + summary: 'Get Payment History of a Dividend Distribution', + description: 'This endpoint retrieves the Payment History of a Dividend Distribution', + }) + @ApiParam({ + name: 'asset', + description: + 'The Asset (Ticker/Asset ID) whose Dividend Distribution Payment History is to be retrieved', + type: 'string', + example: '3616b82e-8e10-80ae-dc95-2ea28b9db8b3', + }) + @ApiParam({ + name: 'id', + description: + 'The Corporate Action number for the the Dividend Distribution (Dividend Distribution ID)', + type: 'string', + example: '123', + }) + @ApiNotFoundResponse({ + description: + '', + }) + @ApiArrayResponse(DistributionPaymentModel, { + description: 'List of payments made for the Distribution', + paginated: true, + }) + @Get('dividend-distributions/:id/payment-history') + public async getPaymentHistory( + @Param() { id, asset }: DividendDistributionParamsDto, + @Query() { size, start }: PaginatedParamsDto + ): Promise> { + const result = await this.corporateActionsService.getPaymentHistory( + asset, + id, + size, + new BigNumber(start || 0) + ); + + return new PaginatedResultsModel({ + results: result.data.map( + ({ target: { did }, ...rest }) => new DistributionPaymentModel({ did, ...rest }) + ), + next: result.next, + }); + } } diff --git a/src/corporate-actions/corporate-actions.service.spec.ts b/src/corporate-actions/corporate-actions.service.spec.ts index 8a65dde8..14f09368 100644 --- a/src/corporate-actions/corporate-actions.service.spec.ts +++ b/src/corporate-actions/corporate-actions.service.spec.ts @@ -1,9 +1,16 @@ /* eslint-disable import/first */ const mockIsPolymeshTransaction = jest.fn(); +import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { CaCheckpointType, TxTags } from '@polymeshassociation/polymesh-sdk/types'; +import { + CaCheckpointType, + DistributionPayment, + DistributionWithDetails, + DividendDistribution, + TxTags, +} from '@polymeshassociation/polymesh-sdk/types'; import { AssetsService } from '~/assets/assets.service'; import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; @@ -402,4 +409,34 @@ describe('CorporateActionsService', () => { }); }); }); + + describe('getPaymentHistory', () => { + it('should return the payment history for a specific Dividend Distribution', async () => { + const mockPaginatedResult = { + data: [createMock()], + next: new BigNumber(2), + count: new BigNumber(2), + }; + + const mockDistribution = createMock({ + getPaymentHistory: jest.fn().mockResolvedValue(mockPaginatedResult), + }); + + const mockDistributionWithDetails = createMock({ + distribution: mockDistribution, + }); + + const findDistributionSpy = jest.spyOn(service, 'findDistribution'); + findDistributionSpy.mockResolvedValue(mockDistributionWithDetails); + + const result = await service.getPaymentHistory( + assetId, + new BigNumber(1), + new BigNumber(10), + new BigNumber(0) + ); + + expect(result).toEqual(mockPaginatedResult); + }); + }); }); diff --git a/src/corporate-actions/corporate-actions.service.ts b/src/corporate-actions/corporate-actions.service.ts index 4f9ca667..ae9f0e02 100644 --- a/src/corporate-actions/corporate-actions.service.ts +++ b/src/corporate-actions/corporate-actions.service.ts @@ -2,8 +2,10 @@ import { Injectable } from '@nestjs/common'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { CorporateActionDefaultConfig, + DistributionPayment, DistributionWithDetails, DividendDistribution, + ResultSet, } from '@polymeshassociation/polymesh-sdk/types'; import { AssetsService } from '~/assets/assets.service'; @@ -152,4 +154,15 @@ export class CorporateActionsService { return this.transactionService.submit(distribution.modifyCheckpoint, args, options); } + + public async getPaymentHistory( + asset: string, + id: BigNumber, + size: BigNumber, + start?: BigNumber + ): Promise> { + const { distribution } = await this.findDistribution(asset, id); + + return distribution.getPaymentHistory({ size, start }); + } } diff --git a/src/corporate-actions/models/distribution-payment.model.ts b/src/corporate-actions/models/distribution-payment.model.ts new file mode 100644 index 00000000..d6cd34ed --- /dev/null +++ b/src/corporate-actions/models/distribution-payment.model.ts @@ -0,0 +1,57 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { FromBigNumber } from '~/common/decorators'; + +export class DistributionPaymentModel { + @ApiProperty({ + description: 'Block number when the payment was made', + type: 'string', + example: '1234567', + }) + @FromBigNumber() + blockNumber: BigNumber; + + @ApiProperty({ + description: 'Hash of the block when the payment was made', + type: 'string', + example: '0xec1d41dd553ce03c3e462aab8bcfba0e1726e6bf310db6e06a933bf0430419c0', + }) + blockHash: string; + + @ApiProperty({ + description: 'Date when the payment was made', + example: new Date('10/14/1987').toISOString(), + type: 'string', + }) + date: Date; + + @ApiProperty({ + description: 'The DID of the payment recipient', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + did: string; + + @ApiProperty({ + description: 'Amount of the payment', + type: 'string', + example: '1000000', + }) + @FromBigNumber() + amount: BigNumber; + + @ApiProperty({ + description: 'Percentage (0-100) of tax withholding for the target identity', + type: 'string', + example: '15', + }) + @FromBigNumber() + withheldTax: BigNumber; + + constructor(model: DistributionPaymentModel) { + Object.assign(this, model); + } +} diff --git a/src/test-utils/consts.ts b/src/test-utils/consts.ts index f1e7645e..5f019008 100644 --- a/src/test-utils/consts.ts +++ b/src/test-utils/consts.ts @@ -19,6 +19,9 @@ const did = '0x01'.padEnd(66, '0'); const dryRun = false; const ticker = 'TICKER'; const assetId = '3616b82e-8e10-80ae-dc95-2ea28b9db8b3'; +const blockNumber = new BigNumber(1); +const blockHash = '0xec1d41dd553ce03c3e462aab8bcfba0e1726e6bf310db6e06a933bf0430419c0'; +const date = new Date('2001-01-01'); const user = new UserModel({ id: '-1', @@ -110,6 +113,9 @@ export const testValues = { dryRun, ticker, assetId, + blockNumber, + blockHash, + date, }; export const extrinsic = {