From cb8992900df65c7a920d25806717c85e7658ce90 Mon Sep 17 00:00:00 2001 From: Yusef Habib Date: Wed, 18 Dec 2024 12:54:17 +0100 Subject: [PATCH] NNS1-3486: updates reporting transactions button to provide period to the service (#6033) # Motivation We want to filter transactions by a specific period. Follow up PR will hook the `PeriodDateRangeSelector` to the `PeriodTransactionsButton` so users selection will be propagated to the service. # Changes - Forwards the `period` property from the `ReportingTransactionsButton` to the service. - Changes the utility `filterTransactionsByRange` to exclude dates that are equal to `to` this.https://github.com/dfinity/nns-dapp/blob/bc7b3b64cafd286bb76a5996ae29447849220163/frontend/src/lib/types/reporting.ts#L6 # Tests - New unit tests for the component - Updated test for the service # Todos - [ ] Add entry to changelog (if necessary). Not necessary --- .../ReportingTransactionsButton.svelte | 6 +++ .../src/lib/services/reporting.services.ts | 5 +- .../ReportingTransactionsButton.spec.ts | 48 ++++++++++++++++++- .../lib/services/reporting.services.spec.ts | 46 ++++++++++++++++-- 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/components/reporting/ReportingTransactionsButton.svelte b/frontend/src/lib/components/reporting/ReportingTransactionsButton.svelte index 2264c4b2a2e..1e83feabdb6 100644 --- a/frontend/src/lib/components/reporting/ReportingTransactionsButton.svelte +++ b/frontend/src/lib/components/reporting/ReportingTransactionsButton.svelte @@ -4,6 +4,7 @@ import { ICPToken, nonNullish } from "@dfinity/utils"; import { buildTransactionsDatasets, + convertPeriodToNanosecondRange, CsvGenerationError, FileSystemAccessError, generateCsvFileToSave, @@ -24,6 +25,9 @@ import { sortNeuronsByStake } from "$lib/utils/neuron.utils"; import { nnsAccountsListStore } from "$lib/derived/accounts-list.derived"; import { startBusy, stopBusy } from "$lib/stores/busy.store"; + import type { ReportingPeriod } from "$lib/types/reporting"; + + export let period: ReportingPeriod = "all"; let identity: Identity | null | undefined; let swapCanisterAccounts: Set; @@ -70,9 +74,11 @@ ); const entities = [...nnsAccounts, ...nnsNeurons]; + const range = convertPeriodToNanosecondRange(period); const transactions = await getAccountTransactionsConcurrently({ entities, identity: signIdentity, + range, }); const datasets = buildTransactionsDatasets({ transactions, diff --git a/frontend/src/lib/services/reporting.services.ts b/frontend/src/lib/services/reporting.services.ts index e2a26facd7a..54b32e7bce6 100644 --- a/frontend/src/lib/services/reporting.services.ts +++ b/frontend/src/lib/services/reporting.services.ts @@ -39,9 +39,11 @@ export const mapAccountOrNeuronToTransactionEntity = ( export const getAccountTransactionsConcurrently = async ({ entities, identity, + range, }: { entities: (Account | NeuronInfo)[]; identity: SignIdentity; + range?: TransactionsDateRange; }): Promise => { const transactionEntities = entities.map( mapAccountOrNeuronToTransactionEntity @@ -51,6 +53,7 @@ export const getAccountTransactionsConcurrently = async ({ getAllTransactionsFromAccountAndIdentity({ accountId: entity.identifier, identity, + range, }) ); @@ -177,7 +180,7 @@ const filterTransactionsByRange = ( } const to = range.to; - if (nonNullish(to) && timestamp > to) { + if (nonNullish(to) && timestamp >= to) { return false; } diff --git a/frontend/src/tests/lib/components/reporting/ReportingTransactionsButton.spec.ts b/frontend/src/tests/lib/components/reporting/ReportingTransactionsButton.spec.ts index 1375582d22f..35686e5c83f 100644 --- a/frontend/src/tests/lib/components/reporting/ReportingTransactionsButton.spec.ts +++ b/frontend/src/tests/lib/components/reporting/ReportingTransactionsButton.spec.ts @@ -3,6 +3,7 @@ import * as icpIndexApi from "$lib/api/icp-index.api"; import ReportingTransactionsButton from "$lib/components/reporting/ReportingTransactionsButton.svelte"; import * as exportDataService from "$lib/services/reporting.services"; import * as toastsStore from "$lib/stores/toasts.store"; +import type { ReportingPeriod } from "$lib/types/reporting"; import * as exportToCsv from "$lib/utils/reporting.utils"; import { mockIdentity, resetIdentity } from "$tests/mocks/auth.store.mock"; import { @@ -73,8 +74,15 @@ describe("ReportingTransactionsButton", () => { }); }); - const renderComponent = ({ onTrigger }: { onTrigger?: () => void } = {}) => { - const { container, component } = render(ReportingTransactionsButton); + const renderComponent = ( + { + onTrigger, + period, + }: { onTrigger?: () => void; period: ReportingPeriod } = { period: "all" } + ) => { + const { container, component } = render(ReportingTransactionsButton, { + period, + }); const po = ReportingTransactionsButtonPo.under({ element: new JestPageObjectElement(container), }); @@ -186,6 +194,7 @@ describe("ReportingTransactionsButton", () => { expect(spyExportDataService).toHaveBeenCalledWith({ entities: expectation, identity: mockIdentity, + range: {}, }); }); @@ -226,6 +235,41 @@ describe("ReportingTransactionsButton", () => { expect(spyExportDataService).toHaveBeenCalledWith({ entities: expectation, identity: mockIdentity, + range: {}, + }); + }); + + it("should fetch transactions filtered by period", async () => { + const beginningOfYear = new Date("2023-01-01T00:00:00Z"); + const NANOS_IN_MS = BigInt(1_000_000); + const beginningOfYearInNanoseconds = + BigInt(beginningOfYear.getTime()) * NANOS_IN_MS; + + resetAccountsForTesting(); + setAccountsForTesting({ + main: mockMainAccount, + }); + + const mockNeurons: NeuronInfo[] = [mockNeuron]; + spyQueryNeurons.mockResolvedValue(mockNeurons); + + const po = renderComponent({ period: "year-to-date" }); + + expect(spyExportDataService).toBeCalledTimes(0); + expect(spyQueryNeurons).toBeCalledTimes(0); + + await po.click(); + await runResolvedPromises(); + + const expectation = [mockMainAccount, mockNeuron]; + expect(spyQueryNeurons).toBeCalledTimes(1); + expect(spyExportDataService).toHaveBeenCalledTimes(1); + expect(spyExportDataService).toHaveBeenCalledWith({ + entities: expectation, + identity: mockIdentity, + range: { + from: beginningOfYearInNanoseconds, + }, }); }); diff --git a/frontend/src/tests/lib/services/reporting.services.spec.ts b/frontend/src/tests/lib/services/reporting.services.spec.ts index 75170720fa0..6bc5a9cc980 100644 --- a/frontend/src/tests/lib/services/reporting.services.spec.ts +++ b/frontend/src/tests/lib/services/reporting.services.spec.ts @@ -137,7 +137,7 @@ describe("reporting service", () => { expect(spyGetTransactions).toHaveBeenCalledTimes(2); }); - it('should filter "to" the provided date', async () => { + it('should filter "to" the provided date excluding to', async () => { const allTransactions = [ createTransactionWithId({ id: 3n, @@ -166,8 +166,8 @@ describe("reporting service", () => { }, }); - expect(result).toHaveLength(2); - expect(result).toEqual(allTransactions.slice(1)); + expect(result).toHaveLength(1); + expect(result).toEqual(allTransactions.slice(2)); expect(spyGetTransactions).toHaveBeenCalledTimes(1); }); @@ -455,6 +455,46 @@ describe("reporting service", () => { expect(result[2].error).toBeUndefined(); }); + it("should fetch transactions for the specified period", async () => { + const allTransactions = [ + createTransactionWithId({ + id: 3n, + timestamp: new Date("2023-01-02T00:00:00.000Z"), + }), + createTransactionWithId({ + id: 2n, + timestamp: new Date("2023-01-01T00:00:00.000Z"), + }), + createTransactionWithId({ + id: 1n, + timestamp: new Date("2022-12-31T00:00:00.000Z"), + }), + ]; + spyGetTransactions.mockResolvedValue({ + transactions: allTransactions, + oldestTxId: 1n, + }); + + const beginningOfYear = dateToNanoSeconds( + new Date("2023-01-01T00:00:00.000Z") + ); + + const result = await getAccountTransactionsConcurrently({ + entities: [mockMainAccount], + identity: mockIdentity, + range: { + from: beginningOfYear, + }, + }); + + expect(result).toHaveLength(1); + expect(spyGetTransactions).toHaveBeenCalledTimes(1); + + expect(result[0].entity).toEqual(mainAccountEntity); + expect(result[0].transactions).toEqual(allTransactions.slice(0, 2)); + expect(result[0].error).toBeUndefined(); + }); + // TODO: To be implemented once getAccountTransactionsConcurrently handles errors it.skip("should handle failed transactions fetch for some accounts", async () => { spyGetTransactions