From 165681d9e1c740b723ed26dacbdbb899421d4d4a Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Tue, 5 Nov 2024 10:07:44 -0500 Subject: [PATCH 1/4] use fetch instead of GraphQL Proxy due to 502 errors we get on server when the query takes to long. --- .../FourteenMonthReport.tsx | 70 +++++++-- .../handleFourteenMonthReportData.ts | 148 ++++++++++++++++++ 2 files changed, 202 insertions(+), 16 deletions(-) create mode 100644 src/components/Reports/FourteenMonthReports/handleFourteenMonthReportData.ts diff --git a/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx b/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx index 0c959903d..6bd08a14f 100644 --- a/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx +++ b/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx @@ -1,17 +1,20 @@ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Box, CircularProgress, useMediaQuery } from '@mui/material'; import { Theme } from '@mui/material/styles'; +import { DateTime } from 'luxon'; import { useTranslation } from 'react-i18next'; +import { FourteenMonthReport as FourteenMonthReportQueryResponse } from 'pages/api/graphql-rest.page.generated'; import { Notification } from 'src/components/Notification/Notification'; import { EmptyReport } from 'src/components/Reports/EmptyReport/EmptyReport'; import { FourteenMonthReportCurrencyType } from 'src/graphql/types.generated'; -import { useFourteenMonthReportQuery } from './GetFourteenMonthReport.generated'; +import { useRequiredSession } from 'src/hooks/useRequiredSession'; import { FourteenMonthReportHeader as Header } from './Layout/Header/Header'; import { FourteenMonthReportTable as Table, FourteenMonthReportTableProps as TableProps, } from './Layout/Table/Table'; import { calculateTotals, sortContacts } from './Layout/Table/helpers'; +import { mapFourteenMonthReport } from './handleFourteenMonthReportData'; import { useCsvData } from './useCsvData'; import type { Order } from '../Reports.type'; import type { OrderBy } from './Layout/Table/TableHead/TableHead'; @@ -48,7 +51,14 @@ export const FourteenMonthReport: React.FC = ({ const [isExpanded, setExpanded] = useState(false); const [order, setOrder] = useState('asc'); const [orderBy, setOrderBy] = useState(null); + const [fourteenMonthReport, setFourteenMonthReport] = useState< + FourteenMonthReportQueryResponse | undefined + >(undefined); + const [fourteenMonthReportError, setFourteenMonthReportError] = + useState(''); + const { t } = useTranslation(); + const { apiToken } = useRequiredSession(); const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'), @@ -56,25 +66,53 @@ export const FourteenMonthReport: React.FC = ({ const isPrint = useMediaQuery('print'); - const { data, error } = useFourteenMonthReportQuery({ - variables: { - accountListId, - designationAccountIds: designationAccounts?.length - ? designationAccounts - : null, - currencyType, - }, - }); + useEffect(() => { + (async () => { + try { + const designationAccountFilter = designationAccounts?.length + ? `&filter[designation_account_id]=${designationAccounts.join(',')}` + : ''; + const requestUrl = `${ + currencyType === 'salary' + ? 'salary_currency_donations' + : 'donor_currency_donations' + }?filter[account_list_id]=${accountListId}${designationAccountFilter}&filter[month_range]=${DateTime.now() + .minus({ months: 13 }) + .toISODate()}...${DateTime.now().toISODate()}`; + + const response = await fetch( + `${process.env.REST_API_URL}reports/${requestUrl}`, + { + headers: { + authorization: `Bearer ${apiToken}`, + 'Content-Type': 'application/vnd.api+json', + }, + method: 'GET', + }, + ); + + const { data } = await response.json(); + + setFourteenMonthReport(mapFourteenMonthReport(data, currencyType)); + } catch (error: unknown) { + if (error instanceof Error) { + setFourteenMonthReportError(error.message); + } else { + setFourteenMonthReportError(String(error)); + } + } + })(); + }, [designationAccounts, currencyType]); // Generate a table for each currency group in the report const currencyTables = useMemo( () => - data?.fourteenMonthReport.currencyGroups.map((currencyGroup) => ({ + fourteenMonthReport?.currencyGroups.map((currencyGroup) => ({ currency: currencyGroup.currency, orderedContacts: sortContacts(currencyGroup.contacts, orderBy, order), totals: calculateTotals(currencyGroup.contacts), })) ?? [], - [data, orderBy, order], + [fourteenMonthReport, orderBy, order], ); const handleExpandToggle = (): void => { @@ -107,7 +145,7 @@ export const FourteenMonthReport: React.FC = ({ onPrint={handlePrint} title={title} /> - {!data && !error ? ( + {!fourteenMonthReport && !fourteenMonthReportError ? ( = ({ > - ) : error ? ( - + ) : fourteenMonthReportError ? ( + ) : currencyTables.length > 0 ? ( {currencyTables.map(({ currency, orderedContacts, totals }) => ( diff --git a/src/components/Reports/FourteenMonthReports/handleFourteenMonthReportData.ts b/src/components/Reports/FourteenMonthReports/handleFourteenMonthReportData.ts new file mode 100644 index 000000000..8df22e57d --- /dev/null +++ b/src/components/Reports/FourteenMonthReports/handleFourteenMonthReportData.ts @@ -0,0 +1,148 @@ +import { FourteenMonthReport } from 'pages/api/graphql-rest.page.generated'; +import { FourteenMonthReportCurrencyType } from 'src/graphql/types.generated'; +import { convertStatus } from 'src/utils/functions/convertContactStatus'; + +export interface FourteenMonthReportResponse { + id: string; + type: + | 'reports_salary_currency_donations' + | 'reports_donor_currency_donations'; + attributes: { + created_at: string; + currency_groups: { + [currency: string]: { + totals: { + year: string; + year_converted: number | string; + months: (number | string)[]; + }; + donation_infos: { + average: number | string; + contact_id: string; + maximum: number | string; + minimum: number | string; + months: { + total: number | string; + donations: { + amount: string; + contact_id: string; + contact_name: string; + converted_amount: number | string; + converted_currency: string; + currency: string; + donation_date: string; + donation_id: string; + likelihood_type: string; + payment_method: string | null; + }[]; + }[]; + total: number | string; + }[]; + }; + }; + default_currency: string; + donor_infos: { + account_numbers: string[]; + contact_id: string; + contact_name: string; + late_by_30_days: boolean; + late_by_60_days: boolean; + pledge_amount: string | null; + pledge_currency: string; + pledge_frequency: string | null; + status: string | null; + }[]; + months: string[]; + salary_currency: string; + updated_at: null; + updated_in_db_at: null; + }; + relationships: { + account_list: { + data: { + id: string; + type: 'account_lists'; + }; + }; + }; +} + +export const mapFourteenMonthReport = ( + data: FourteenMonthReportResponse, + currencyType: FourteenMonthReportCurrencyType, +): FourteenMonthReport => { + const isSalaryType = currencyType === FourteenMonthReportCurrencyType.Salary; + return { + currencyType, + salaryCurrency: data.attributes.salary_currency, + currencyGroups: Object.entries(data.attributes.currency_groups).map( + ([currency, currencyGroup]) => ({ + currency: currency.toUpperCase(), + totals: { + year: Number( + isSalaryType + ? currencyGroup.totals.year_converted + : currencyGroup.totals.year, + ), + months: currencyGroup.totals.months.map((total, index) => ({ + month: data.attributes.months[index], + total: Number(total), + })), + average: currencyGroup.donation_infos.reduce( + (averageTotal, contactDonationInfo) => + averageTotal + Number(contactDonationInfo.average), + 0, + ), + minimum: currencyGroup.donation_infos.reduce( + (minimumTotal, contactDonationInfo) => + minimumTotal + Number(contactDonationInfo.minimum), + 0, + ), + }, + contacts: currencyGroup.donation_infos + .map((contactDonationInfo) => { + const contact = data.attributes.donor_infos.find( + (donor) => donor.contact_id === contactDonationInfo.contact_id, + ); + return { + id: contactDonationInfo.contact_id, + name: contact?.contact_name ?? '', + total: Number(contactDonationInfo.total), + average: Number(contactDonationInfo.average), + minimum: Number(contactDonationInfo.minimum), + months: contactDonationInfo.months.map((month, index) => { + const salaryCurrencyTotal = month.donations.reduce( + (convertedTotal, donation) => + convertedTotal + Number(donation.converted_amount), + 0, + ); + return { + month: data.attributes.months[index], + total: isSalaryType + ? salaryCurrencyTotal + : Number(month.total), + salaryCurrencyTotal, + donations: month.donations.map((donation) => ({ + amount: Number(donation.amount), + date: donation.donation_date, + paymentMethod: donation.payment_method, + currency: donation.currency, + })), + }; + }), + accountNumbers: contact?.account_numbers ?? [], + lateBy30Days: contact?.late_by_30_days ?? false, + lateBy60Days: contact?.late_by_60_days ?? false, + pledgeAmount: contact?.pledge_amount + ? Number(contact?.pledge_amount) + : null, + pledgeCurrency: contact?.pledge_currency, + pledgeFrequency: contact?.pledge_frequency, + status: convertStatus(contact?.status), + }; + }) + .sort((a, b) => a.name.localeCompare(b.name)), + }), + ), + }; +}; From 304b617fcc72c6c729c37d0505abce0669be80f3 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Tue, 5 Nov 2024 10:07:53 -0500 Subject: [PATCH 2/4] Fixed console log warning --- .../FourteenMonthReports/Layout/Table/Table.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/Reports/FourteenMonthReports/Layout/Table/Table.tsx b/src/components/Reports/FourteenMonthReports/Layout/Table/Table.tsx index c75af6dfb..009bd1d16 100644 --- a/src/components/Reports/FourteenMonthReports/Layout/Table/Table.tsx +++ b/src/components/Reports/FourteenMonthReports/Layout/Table/Table.tsx @@ -147,12 +147,10 @@ export const FourteenMonthReportTable: React.FC< {!isExpanded && } - - {contact.name} + + + {contact.name} + {(contact.lateBy30Days || contact.lateBy60Days) && ( From 9beea7d91bb4bc9c701c9654e1faad3ef8b070a4 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Tue, 5 Nov 2024 13:16:36 -0500 Subject: [PATCH 3/4] fixup! use fetch instead of GraphQL Proxy due to 502 errors we get on server when the query takes to long. --- pages/api/Schema/reports/fourteenMonth/datahandler.ts | 3 +++ .../Reports/FourteenMonthReports/FourteenMonthReport.tsx | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pages/api/Schema/reports/fourteenMonth/datahandler.ts b/pages/api/Schema/reports/fourteenMonth/datahandler.ts index 38f0968cc..39619ad30 100644 --- a/pages/api/Schema/reports/fourteenMonth/datahandler.ts +++ b/pages/api/Schema/reports/fourteenMonth/datahandler.ts @@ -4,6 +4,9 @@ import { FourteenMonthReportCurrencyType, } from '../../../graphql-rest.page.generated'; +// We have switched to call the REST API directly from the frontend +// due to Next.js having issues when the size of the response is too large. + export interface FourteenMonthReportResponse { id: string; type: diff --git a/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx b/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx index 6bd08a14f..853527769 100644 --- a/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx +++ b/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx @@ -87,7 +87,6 @@ export const FourteenMonthReport: React.FC = ({ authorization: `Bearer ${apiToken}`, 'Content-Type': 'application/vnd.api+json', }, - method: 'GET', }, ); From 4279e9d8f6558cb97eb8b46fcb4936b213bbd363 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Tue, 5 Nov 2024 16:46:25 -0500 Subject: [PATCH 4/4] Adding tests for REST query --- .../[[...contactId]].page.test.tsx | 32 +- .../[[...contactId]].page.test.tsx | 38 +- .../FourteenMonthReport.test.tsx | 534 ++++----------- .../FourteenMonthReport.tsx | 2 +- .../FourteenMonthReportMock.ts | 607 ++++++++++++++++++ .../Layout/Table/Table.test.tsx | 184 +----- .../handleFourteenMonthReportData.ts | 148 ----- 7 files changed, 775 insertions(+), 770 deletions(-) create mode 100644 src/components/Reports/FourteenMonthReports/FourteenMonthReportMock.ts delete mode 100644 src/components/Reports/FourteenMonthReports/handleFourteenMonthReportData.ts diff --git a/pages/accountLists/[accountListId]/reports/partnerCurrency/[[...contactId]].page.test.tsx b/pages/accountLists/[accountListId]/reports/partnerCurrency/[[...contactId]].page.test.tsx index db24fc8db..76f4527ea 100644 --- a/pages/accountLists/[accountListId]/reports/partnerCurrency/[[...contactId]].page.test.tsx +++ b/pages/accountLists/[accountListId]/reports/partnerCurrency/[[...contactId]].page.test.tsx @@ -1,9 +1,11 @@ import { ThemeProvider } from '@mui/material/styles'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import fetchMock from 'jest-fetch-mock'; import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { fourteenMonthReportMock } from 'src/components/Reports/FourteenMonthReports/FourteenMonthReportMock'; import theme from 'src/theme'; import PartnerCurrencyReportPage from './[[...contactId]].page'; @@ -25,29 +27,10 @@ const TestingComponent: React.FC = ({ push, }; - const mocks = { - FourteenMonthReport: { - fourteenMonthReport: { - currencyGroups: [ - { - contacts: [ - { - id: 'contact-1', - name: 'John Doe', - lastDonationCurrency: 'USD', - pledgeCurrency: 'USD', - }, - ], - }, - ], - }, - }, - }; - return ( - + @@ -58,6 +41,15 @@ const TestingComponent: React.FC = ({ }; describe('partnerCurrency page', () => { + fetchMock.enableMocks(); + beforeEach(() => { + fetchMock.resetMocks(); + fetchMock.mockResponses([ + JSON.stringify(fourteenMonthReportMock), + { status: 200 }, + ]); + process.env.REST_API_URL = 'https://api.stage.mpdx.org/api/v2/'; + }); it('renders', () => { const { getByRole } = render(); diff --git a/pages/accountLists/[accountListId]/reports/salaryCurrency/[[...contactId]].page.test.tsx b/pages/accountLists/[accountListId]/reports/salaryCurrency/[[...contactId]].page.test.tsx index 950bef74e..d9de55c39 100644 --- a/pages/accountLists/[accountListId]/reports/salaryCurrency/[[...contactId]].page.test.tsx +++ b/pages/accountLists/[accountListId]/reports/salaryCurrency/[[...contactId]].page.test.tsx @@ -1,9 +1,11 @@ import { ThemeProvider } from '@mui/material/styles'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import fetchMock from 'jest-fetch-mock'; import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { fourteenMonthReportMock } from 'src/components/Reports/FourteenMonthReports/FourteenMonthReportMock'; import theme from 'src/theme'; import SalaryCurrencyReportPage from './[[...contactId]].page'; @@ -25,39 +27,29 @@ const TestingComponent: React.FC = ({ push, }; - const mocks = { - FourteenMonthReport: { - fourteenMonthReport: { - currencyGroups: [ - { - contacts: [ - { - id: 'contact-1', - name: 'John Doe', - lastDonationCurrency: 'USD', - pledgeCurrency: 'USD', - }, - ], - }, - ], - }, - }, - }; - return ( - - + + - - + + ); }; describe('salaryCurrency page', () => { + fetchMock.enableMocks(); + beforeEach(() => { + fetchMock.resetMocks(); + fetchMock.mockResponses([ + JSON.stringify(fourteenMonthReportMock), + { status: 200 }, + ]); + process.env.REST_API_URL = 'https://api.stage.mpdx.org/api/v2/'; + }); it('renders', () => { const { getByRole } = render(); diff --git a/src/components/Reports/FourteenMonthReports/FourteenMonthReport.test.tsx b/src/components/Reports/FourteenMonthReports/FourteenMonthReport.test.tsx index fc57673e7..f953ef398 100644 --- a/src/components/Reports/FourteenMonthReports/FourteenMonthReport.test.tsx +++ b/src/components/Reports/FourteenMonthReports/FourteenMonthReport.test.tsx @@ -1,15 +1,11 @@ import React from 'react'; -import { MockedProvider, MockedResponse } from '@apollo/client/testing'; import { ThemeProvider } from '@mui/material/styles'; import { render, waitFor, within } from '@testing-library/react'; -import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import fetchMock from 'jest-fetch-mock'; import { FourteenMonthReportCurrencyType } from 'src/graphql/types.generated'; import theme from 'src/theme'; import { FourteenMonthReport } from './FourteenMonthReport'; -import { - FourteenMonthReportDocument, - FourteenMonthReportQuery, -} from './GetFourteenMonthReport.generated'; +import { fourteenMonthReportRestMock } from './FourteenMonthReportMock'; const accountListId = '111'; const title = 'test title'; @@ -23,282 +19,28 @@ const defaultProps = { isNavListOpen: false, }; -const mocks = { - FourteenMonthReport: { - fourteenMonthReport: { - currencyGroups: [ - { - contacts: [ - { - accountNumbers: ['10182'], - average: 86, - id: 'contact-1', - lateBy30Days: false, - lateBy60Days: false, - minimum: 85, - months: [ - { - donations: [ - { - amount: 85, - currency: 'CAD', - date: '2020-07-15', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-10-01', - total: 50, - }, - { - donations: [ - { - amount: 85, - currency: 'CAD', - date: '2020-11-15', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-11-01', - total: 50, - }, - { - donations: [ - { - amount: 85, - currency: 'CAD', - date: '2020-12-15', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-12-01', - total: 50, - }, - { - donations: [ - { - amount: 85, - currency: 'CAD', - date: '2021-1-15', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2021-1-01', - total: 50, - }, - ], - name: 'test name', - pledgeAmount: null, - pledgeCurrency: 'CAD', - pledgeFrequency: null, - status: null, - total: 1290, - }, - { - accountNumbers: ['10182'], - average: 86, - id: 'contact-2', - lateBy30Days: false, - lateBy60Days: false, - minimum: 85, - months: [ - { - donations: [ - { - amount: 85, - currency: 'CAD', - date: '2020-07-15', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-10-01', - total: 50, - }, - { - donations: [ - { - amount: 85, - currency: 'CAD', - date: '2020-11-15', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-11-01', - total: 50, - }, - { - donations: [ - { - amount: 85, - currency: 'CAD', - date: '2020-12-15', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-12-01', - total: 50, - }, - { - donations: [ - { - amount: 85, - currency: 'CAD', - date: '2021-1-15', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2021-1-01', - total: 50, - }, - ], - name: 'test name', - pledgeAmount: null, - pledgeCurrency: 'CAD', - pledgeFrequency: null, - status: null, - total: 1290, - }, - ], - currency: 'CAD', - totals: { - months: [ - { - month: '2020-10-01', - total: 1836.32, - }, - { - month: '2020-11-01', - total: 1836.32, - }, - { - month: '2020-12-01', - total: 1836.32, - }, - { - month: '2021-1-01', - total: 1836.32, - }, - ], - }, - }, - { - contacts: [ - { - accountNumbers: ['101823'], - average: 86, - id: 'contact-1', - lateBy30Days: false, - lateBy60Days: false, - minimum: 85, - months: [ - { - donations: [ - { - amount: 85, - currency: 'USD', - date: '2020-07-15', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-10-01', - total: 50, - }, - { - donations: [ - { - amount: 85, - currency: 'USD', - date: '2020-11-15', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-11-01', - total: 50, - }, - { - donations: [ - { - amount: 85, - currency: 'USD', - date: '2020-12-15', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-12-01', - total: 50, - }, - { - donations: [ - { - amount: 85, - currency: 'USD', - date: '2021-1-15', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2021-1-01', - total: 50, - }, - ], - name: 'test name', - pledgeAmount: null, - pledgeCurrency: 'USD', - pledgeFrequency: null, - status: null, - total: 1290, - }, - ], - currency: 'USD', - totals: { - months: [ - { - month: '2020-10-01', - total: 1836.32, - }, - { - month: '2020-11-01', - total: 1836.32, - }, - { - month: '2020-12-01', - total: 1836.32, - }, - { - month: '2021-1-01', - total: 1836.32, - }, - ], - }, - }, - ], - salaryCurrency: 'CAD', - }, - }, -}; - -const errorMock: MockedResponse = { - request: { - query: FourteenMonthReportDocument, - }, - error: { - name: 'error', - message: 'Error loading data. Try again.', - }, -}; - describe('FourteenMonthReport', () => { + fetchMock.enableMocks(); + beforeEach(() => { + fetchMock.resetMocks(); + fetchMock.mockResponses([ + JSON.stringify(fourteenMonthReportRestMock), + { status: 200 }, + ]); + process.env.REST_API_URL = 'https://api.stage.mpdx.org/api/v2/'; + }); + it('salary report loading', async () => { const { getByTestId, getByText, queryByTestId } = render( - - - + , ); @@ -310,18 +52,14 @@ describe('FourteenMonthReport', () => { it('salary report loaded', async () => { const { getAllByTestId, queryByTestId, getAllByRole } = render( - - mocks={mocks} - > - - + , ); @@ -339,16 +77,14 @@ describe('FourteenMonthReport', () => { it('partner report loading', async () => { const { getByTestId, getByText, queryByTestId } = render( - - - + , ); @@ -360,18 +96,14 @@ describe('FourteenMonthReport', () => { it('partner report loaded', async () => { const { getAllByTestId, queryByTestId, getByText } = render( - - mocks={mocks} - > - - + , ); @@ -385,10 +117,15 @@ describe('FourteenMonthReport', () => { expect(getAllByTestId('FourteenMonthReport')).toHaveLength(2); }); - it('salary report error', async () => { - const { queryByTestId, getByTestId, getByText } = render( - - + describe('Errors', () => { + beforeEach(() => { + fetchMock.resetMocks(); + fetchMock.mockReject(new Error('Error loading data. Try again.')); + }); + + it('salary report error', async () => { + const { queryByTestId, getByTestId, getByText } = render( + { onNavListToggle={onNavListToggle} getContactUrl={getContactUrl} /> - - , - ); + , + ); - await waitFor(() => { - expect( - queryByTestId('LoadingFourteenMonthReport'), - ).not.toBeInTheDocument(); - }); + await waitFor(() => { + expect( + queryByTestId('LoadingFourteenMonthReport'), + ).not.toBeInTheDocument(); + }); - expect(getByText(title)).toBeInTheDocument(); - expect(getByTestId('Notification')).toBeInTheDocument(); - }); + expect(getByText(title)).toBeInTheDocument(); + expect(getByTestId('Notification')).toBeInTheDocument(); + }); - it('partner report error', async () => { - const { queryByTestId, getByTestId, getByText } = render( - - + it('partner report error', async () => { + const { queryByTestId, getByTestId, getByText } = render( + { onNavListToggle={onNavListToggle} getContactUrl={getContactUrl} /> - - , - ); + , + ); - await waitFor(() => { - expect( - queryByTestId('LoadingFourteenMonthReport'), - ).not.toBeInTheDocument(); - }); + await waitFor(() => { + expect( + queryByTestId('LoadingFourteenMonthReport'), + ).not.toBeInTheDocument(); + }); - expect(getByText(title)).toBeInTheDocument(); - expect(getByTestId('Notification')).toBeInTheDocument(); + expect(getByText(title)).toBeInTheDocument(); + expect(getByTestId('Notification')).toBeInTheDocument(); + }); }); it('nav list closed', async () => { const { getAllByTestId, getByText, queryByTestId } = render( - - mocks={mocks} - > - - + , ); @@ -467,74 +198,69 @@ describe('FourteenMonthReport', () => { }); it('filters report by designation account', async () => { - const mutationSpy = jest.fn(); + const designationAccount = 'account-1'; render( - - mocks={mocks} - onCall={mutationSpy} - > - - + , ); await waitFor(() => - expect(mutationSpy).toHaveGraphqlOperation('FourteenMonthReport', { - designationAccountIds: ['account-1'], - }), + expect(fetchMock).toHaveBeenCalledWith( + `https://api.stage.mpdx.org/api/v2/reports/donor_currency_donations?filter[account_list_id]=111&filter[designation_account_id]=${designationAccount}&filter[month_range]=2018-12-01...2020-01-01`, + { + headers: { + 'Content-Type': 'application/vnd.api+json', + authorization: 'Bearer apiToken', + }, + }, + ), ); }); it('does not filter report by designation account', async () => { - const mutationSpy = jest.fn(); render( - - mocks={mocks} - onCall={mutationSpy} - > - - + , ); await waitFor(() => - expect(mutationSpy).toHaveGraphqlOperation('FourteenMonthReport', { - designationAccountIds: null, - }), + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.stage.mpdx.org/api/v2/reports/donor_currency_donations?filter[account_list_id]=111&filter[month_range]=2018-12-01...2020-01-01', + { + headers: { + 'Content-Type': 'application/vnd.api+json', + authorization: 'Bearer apiToken', + }, + }, + ), ); }); it('can click on a contact name', async () => { - const mutationSpy = jest.fn(); const { findAllByRole, getAllByText, queryByTestId } = render( - - mocks={mocks} - onCall={mutationSpy} - > - - + , ); @@ -554,12 +280,10 @@ describe('FourteenMonthReport', () => { it('should render one table for each partner currency', async () => { const { findAllByRole } = render( - mocks={mocks}> - - + , ); diff --git a/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx b/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx index 853527769..74a877d8e 100644 --- a/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx +++ b/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx @@ -3,6 +3,7 @@ import { Box, CircularProgress, useMediaQuery } from '@mui/material'; import { Theme } from '@mui/material/styles'; import { DateTime } from 'luxon'; import { useTranslation } from 'react-i18next'; +import { mapFourteenMonthReport } from 'pages/api/Schema/reports/fourteenMonth/datahandler'; import { FourteenMonthReport as FourteenMonthReportQueryResponse } from 'pages/api/graphql-rest.page.generated'; import { Notification } from 'src/components/Notification/Notification'; import { EmptyReport } from 'src/components/Reports/EmptyReport/EmptyReport'; @@ -14,7 +15,6 @@ import { FourteenMonthReportTableProps as TableProps, } from './Layout/Table/Table'; import { calculateTotals, sortContacts } from './Layout/Table/helpers'; -import { mapFourteenMonthReport } from './handleFourteenMonthReportData'; import { useCsvData } from './useCsvData'; import type { Order } from '../Reports.type'; import type { OrderBy } from './Layout/Table/TableHead/TableHead'; diff --git a/src/components/Reports/FourteenMonthReports/FourteenMonthReportMock.ts b/src/components/Reports/FourteenMonthReports/FourteenMonthReportMock.ts new file mode 100644 index 000000000..2b065795d --- /dev/null +++ b/src/components/Reports/FourteenMonthReports/FourteenMonthReportMock.ts @@ -0,0 +1,607 @@ +import { FourteenMonthReportQuery } from './GetFourteenMonthReport.generated'; + +export const fourteenMonthReportMock = { + data: { + attributes: { + currency_groups: { + usd: { + totals: { + year: '4032.36', + year_converted: 4032.36, + months: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '329.36', '3703.0', 0, 0], + }, + donation_infos: [ + { + contact_id: 'abaf06b6-7fbf-4c5a-b4cb-d039f6e0c68d', + total: 1329.36, + average: 189.9085714285714, + minimum: 36.0, + maximum: 800.0, + months: [ + { + total: 0, + donations: [], + }, + { + total: 0, + donations: [], + }, + { + total: 0, + donations: [], + }, + { + total: 0, + donations: [], + }, + { + total: 0, + donations: [], + }, + { + total: 0, + donations: [], + }, + { + total: 0, + donations: [], + }, + { + total: 0, + donations: [], + }, + { + total: 0, + donations: [], + }, + { + total: 0, + donations: [], + }, + { + total: '450', + donations: [ + { + amount: '450.0', + contact_id: 'abaf06b6-7fbf-4c5a-b4cb-d039f6e0c68d', + contact_name: 'John Doe', + converted_amount: 450.0, + converted_currency: 'USD', + currency: 'USD', + donation_date: '2024-01-24', + donation_id: 'e6201a6d-c7bf-48c8-9ba7-6594f0fc4902', + likelihood_type: 'received', + payment_method: null, + }, + ], + }, + { + total: '1000.0', + donations: [ + { + amount: '1000.0', + contact_id: 'abaf06b6-7fbf-4c5a-b4cb-d039f6e0c68d', + contact_name: 'John Doe', + converted_amount: 1000.0, + converted_currency: 'USD', + currency: 'USD', + donation_date: '2023-12-18', + donation_id: '4f389397-7ba2-4f65-8f20-8419a68c9f2c', + likelihood_type: 'received', + payment_method: null, + }, + ], + }, + { + total: 0, + donations: [], + }, + { + total: 0, + donations: [], + }, + ], + }, + ], + }, + }, + default_currency: 'USD', + donor_infos: [ + { + account_numbers: ['pipTheCat'], + contact_id: 'abaf06b6-7fbf-4c5a-b4cb-d039f6e0c68d', + contact_name: 'John Doe', + late_by_30_days: false, + late_by_60_days: true, + pledge_amount: '90.0', + pledge_currency: 'USD', + pledge_frequency: '1.0', + status: 'partner_financial', + }, + ], + months: [ + '2024-11-01', + '2024-10-01', + '2024-09-01', + '2024-08-01', + '2024-07-01', + '2024-06-01', + '2024-05-01', + '2024-04-01', + '2024-03-01', + '2024-02-01', + '2024-01-01', + '2023-12-01', + '2023-11-01', + '2023-10-01', + ], + salary_currency: 'USD', + updated_at: null, + updated_in_db_at: null, + }, + relationships: { + account_list: { + data: { + id: '1ebb5ce4-410e-4100-8fdb-735b1f6c11d9', + type: 'account_lists', + }, + }, + }, + }, +}; + +export const fourteenMonthReportRestMock = { + data: { + id: '', + type: 'reports_donor_currency_donations', + attributes: { + created_at: '', + currency_groups: { + cad: { + totals: { months: ['1836.32', '1836.32', '1836.32', '1836.32'] }, + donation_infos: [ + { + average: '86', + contact_id: 'contact-1', + maximum: '86', + minimum: '85', + months: [ + { + total: '50', + donations: [ + { + amount: '85', + contact_id: 'contact-1', + contact_name: 'test name', + converted_amount: '85', + converted_currency: 'CAD', + currency: 'CAD', + donation_date: '2020-07-15', + donation_id: '', + likelihood_type: '', + payment_method: 'BANK_TRANS', + }, + ], + }, + { + total: '50', + donations: [ + { + amount: '85', + contact_id: 'contact-1', + contact_name: 'test name', + converted_amount: '85', + converted_currency: 'CAD', + currency: 'CAD', + donation_date: '2020-11-15', + donation_id: '', + likelihood_type: '', + payment_method: 'BANK_TRANS', + }, + ], + }, + { + total: '50', + donations: [ + { + amount: '85', + contact_id: 'contact-1', + contact_name: 'test name', + converted_amount: '85', + converted_currency: 'CAD', + currency: 'CAD', + donation_date: '2020-12-15', + donation_id: '', + likelihood_type: '', + payment_method: 'BANK_TRANS', + }, + ], + }, + { + total: '50', + donations: [ + { + amount: '85', + contact_id: 'contact-1', + contact_name: 'test name', + converted_amount: '85', + converted_currency: 'CAD', + currency: 'CAD', + donation_date: '2021-1-15', + donation_id: '', + likelihood_type: '', + payment_method: 'BANK_TRANS', + }, + ], + }, + ], + total: '1290', + }, + { + average: '86', + contact_id: 'contact-2', + maximum: '86', + minimum: '85', + months: [ + { + total: '50', + donations: [ + { + amount: '85', + contact_id: 'contact-2', + contact_name: 'test name', + converted_amount: '85', + converted_currency: 'CAD', + currency: 'CAD', + donation_date: '2020-07-15', + donation_id: '', + likelihood_type: '', + payment_method: 'BANK_TRANS', + }, + ], + }, + { + total: '50', + donations: [ + { + amount: '85', + contact_id: 'contact-2', + contact_name: 'test name', + converted_amount: '85', + converted_currency: 'CAD', + currency: 'CAD', + donation_date: '2020-11-15', + donation_id: '', + likelihood_type: '', + payment_method: 'BANK_TRANS', + }, + ], + }, + { + total: '50', + donations: [ + { + amount: '85', + contact_id: 'contact-2', + contact_name: 'test name', + converted_amount: '85', + converted_currency: 'CAD', + currency: 'CAD', + donation_date: '2020-12-15', + donation_id: '', + likelihood_type: '', + payment_method: 'BANK_TRANS', + }, + ], + }, + { + total: '50', + donations: [ + { + amount: '85', + contact_id: 'contact-2', + contact_name: 'test name', + converted_amount: '85', + converted_currency: 'CAD', + currency: 'CAD', + donation_date: '2021-1-15', + donation_id: '', + likelihood_type: '', + payment_method: 'BANK_TRANS', + }, + ], + }, + ], + total: '1290', + }, + ], + }, + usd: { + totals: { months: ['1836.32', '1836.32', '1836.32', '1836.32'] }, + donation_infos: [ + { + average: '86', + contact_id: 'contact-1', + maximum: '86', + minimum: '85', + months: [ + { + total: '50', + donations: [ + { + amount: '85', + contact_id: 'contact-1', + contact_name: 'test name', + converted_amount: '85', + converted_currency: 'USD', + currency: 'USD', + donation_date: '2020-07-15', + donation_id: '', + likelihood_type: '', + payment_method: 'BANK_TRANS', + }, + ], + }, + { + total: '50', + donations: [ + { + amount: '85', + contact_id: 'contact-1', + contact_name: 'test name', + converted_amount: '85', + converted_currency: 'USD', + currency: 'USD', + donation_date: '2020-11-15', + donation_id: '', + likelihood_type: '', + payment_method: 'BANK_TRANS', + }, + ], + }, + { + total: '50', + donations: [ + { + amount: '85', + contact_id: 'contact-1', + contact_name: 'test name', + converted_amount: '85', + converted_currency: 'USD', + currency: 'USD', + donation_date: '2020-12-15', + donation_id: '', + likelihood_type: '', + payment_method: 'BANK_TRANS', + }, + ], + }, + { + total: '50', + donations: [ + { + amount: '85', + contact_id: 'contact-1', + contact_name: 'test name', + converted_amount: '85', + converted_currency: 'USD', + currency: 'USD', + donation_date: '2021-1-15', + donation_id: '', + likelihood_type: '', + payment_method: 'BANK_TRANS', + }, + ], + }, + ], + total: '1290', + }, + ], + }, + }, + default_currency: '', + donor_infos: [ + { + account_numbers: ['10182'], + contact_id: 'contact-1', + contact_name: 'test name', + late_by_30_days: false, + late_by_60_days: false, + pledge_amount: null, + pledge_currency: 'CAD', + pledge_frequency: null, + status: null, + }, + { + account_numbers: ['10182'], + contact_id: 'contact-2', + contact_name: 'test name', + late_by_30_days: false, + late_by_60_days: false, + pledge_amount: null, + pledge_currency: 'CAD', + pledge_frequency: null, + status: null, + }, + { + account_numbers: ['101823'], + contact_id: 'contact-1', + contact_name: 'test name', + late_by_30_days: false, + late_by_60_days: false, + pledge_amount: null, + pledge_currency: 'USD', + pledge_frequency: null, + status: null, + }, + ], + months: ['2020-10-01', '2020-11-01', '2020-12-01', '2021-1-01'], + salary_currency: 'CAD', + updated_at: null, + updated_in_db_at: null, + }, + relationships: { + account_list: { data: { id: '', type: 'account_lists' } }, + }, + }, +}; + +export const defaultFourteenMonthReport = { + fourteenMonthReport: { + currencyGroups: [ + { + contacts: [ + { + accountNumbers: ['11609'], + average: 258, + id: 'contact-1', + lateBy30Days: false, + lateBy60Days: true, + months: [ + { + donations: [ + { + amount: 255, + currency: 'CAD', + date: '2020-06-04', + paymentMethod: 'BANK_TRANS', + }, + ], + month: '2020-10-01', + total: 255, + }, + { + donations: [ + { + amount: 255, + currency: 'CAD', + date: '2020-06-04', + paymentMethod: 'BANK_TRANS', + }, + ], + month: '2020-11-01', + total: 255, + }, + { + donations: [ + { + amount: 255, + currency: 'CAD', + date: '2020-06-04', + paymentMethod: 'BANK_TRANS', + }, + ], + month: '2020-12-01', + total: 255, + }, + { + donations: [ + { + amount: 255, + currency: 'CAD', + date: '2020-06-04', + paymentMethod: 'BANK_TRANS', + }, + ], + month: '2021-1-01', + total: 255, + }, + ], + minimum: 255, + name: 'test name', + pledgeAmount: null, + status: null, + total: 3366, + }, + { + accountNumbers: ['11610'], + average: 258, + id: 'contact-2', + lateBy30Days: false, + lateBy60Days: false, + months: [ + { + donations: [ + { + amount: 255, + currency: 'CAD', + date: '2020-06-04', + paymentMethod: 'BANK_TRANS', + }, + ], + month: '2020-10-01', + total: 255, + }, + { + donations: [ + { + amount: 255, + currency: 'CAD', + date: '2020-06-04', + paymentMethod: 'BANK_TRANS', + }, + ], + month: '2020-11-01', + total: 255, + }, + { + donations: [ + { + amount: 255, + currency: 'CAD', + date: '2020-06-04', + paymentMethod: 'BANK_TRANS', + }, + ], + month: '2020-12-01', + total: 255, + }, + { + donations: [ + { + amount: 255, + currency: 'CAD', + date: '2020-06-04', + paymentMethod: 'BANK_TRANS', + }, + ], + month: '2021-1-01', + total: 255, + }, + ], + minimum: 255, + name: 'name again', + pledgeAmount: 15.65, + pledgeCurrency: 'USD', + status: null, + total: 3366, + }, + ], + currency: 'cad', + totals: { + average: 1831, + minimum: 1583, + months: [ + { + month: '2020-10-01', + total: 1836.32, + }, + { + month: '2020-11-01', + total: 1486.99, + }, + { + month: '2020-12-01', + total: 1836.32, + }, + { + month: '2021-1-01', + total: 1836.32, + }, + ], + year: 24613, + }, + }, + ], + }, +} as unknown as FourteenMonthReportQuery; diff --git a/src/components/Reports/FourteenMonthReports/Layout/Table/Table.test.tsx b/src/components/Reports/FourteenMonthReports/Layout/Table/Table.test.tsx index 73e2b93d0..e04e29510 100644 --- a/src/components/Reports/FourteenMonthReports/Layout/Table/Table.test.tsx +++ b/src/components/Reports/FourteenMonthReports/Layout/Table/Table.test.tsx @@ -3,8 +3,8 @@ import { ThemeProvider } from '@mui/material/styles'; import { render, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import TestRouter from '__tests__/util/TestRouter'; -import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import theme from 'src/theme'; +import { defaultFourteenMonthReport } from '../../FourteenMonthReportMock'; import { FourteenMonthReportQuery } from '../../GetFourteenMonthReport.generated'; import { FourteenMonthReportTable } from './Table'; import { OrderBy } from './TableHead/TableHead'; @@ -19,166 +19,6 @@ const router = { const onRequestSort = jest.fn(); const getContactUrl = jest.fn().mockReturnValue('contact-url'); -const defaultFourteenMonthReport = { - fourteenMonthReport: { - currencyGroups: [ - { - contacts: [ - { - accountNumbers: ['11609'], - average: 258, - id: 'contact-1', - lateBy30Days: false, - lateBy60Days: true, - months: [ - { - donations: [ - { - amount: 255, - currency: 'CAD', - date: '2020-06-04', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-10-01', - total: 255, - }, - { - donations: [ - { - amount: 255, - currency: 'CAD', - date: '2020-06-04', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-11-01', - total: 255, - }, - { - donations: [ - { - amount: 255, - currency: 'CAD', - date: '2020-06-04', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-12-01', - total: 255, - }, - { - donations: [ - { - amount: 255, - currency: 'CAD', - date: '2020-06-04', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2021-1-01', - total: 255, - }, - ], - minimum: 255, - name: 'test name', - pledgeAmount: null, - status: null, - total: 3366, - }, - { - accountNumbers: ['11610'], - average: 258, - id: 'contact-2', - lateBy30Days: false, - lateBy60Days: false, - months: [ - { - donations: [ - { - amount: 255, - currency: 'CAD', - date: '2020-06-04', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-10-01', - total: 255, - }, - { - donations: [ - { - amount: 255, - currency: 'CAD', - date: '2020-06-04', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-11-01', - total: 255, - }, - { - donations: [ - { - amount: 255, - currency: 'CAD', - date: '2020-06-04', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2020-12-01', - total: 255, - }, - { - donations: [ - { - amount: 255, - currency: 'CAD', - date: '2020-06-04', - paymentMethod: 'BANK_TRANS', - }, - ], - month: '2021-1-01', - total: 255, - }, - ], - minimum: 255, - name: 'name again', - pledgeAmount: 15.65, - pledgeCurrency: 'USD', - status: null, - total: 3366, - }, - ], - currency: 'cad', - totals: { - average: 1831, - minimum: 1583, - months: [ - { - month: '2020-10-01', - total: 1836.32, - }, - { - month: '2020-11-01', - total: 1486.99, - }, - { - month: '2020-12-01', - total: 1836.32, - }, - { - month: '2021-1-01', - total: 1836.32, - }, - ], - year: 24613, - }, - }, - ], - }, -} as unknown as FourteenMonthReportQuery; - const totals = [ { month: '2020-10-01', @@ -208,18 +48,16 @@ const Components: React.FC = ({ }) => ( - - - + ); diff --git a/src/components/Reports/FourteenMonthReports/handleFourteenMonthReportData.ts b/src/components/Reports/FourteenMonthReports/handleFourteenMonthReportData.ts deleted file mode 100644 index 8df22e57d..000000000 --- a/src/components/Reports/FourteenMonthReports/handleFourteenMonthReportData.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { FourteenMonthReport } from 'pages/api/graphql-rest.page.generated'; -import { FourteenMonthReportCurrencyType } from 'src/graphql/types.generated'; -import { convertStatus } from 'src/utils/functions/convertContactStatus'; - -export interface FourteenMonthReportResponse { - id: string; - type: - | 'reports_salary_currency_donations' - | 'reports_donor_currency_donations'; - attributes: { - created_at: string; - currency_groups: { - [currency: string]: { - totals: { - year: string; - year_converted: number | string; - months: (number | string)[]; - }; - donation_infos: { - average: number | string; - contact_id: string; - maximum: number | string; - minimum: number | string; - months: { - total: number | string; - donations: { - amount: string; - contact_id: string; - contact_name: string; - converted_amount: number | string; - converted_currency: string; - currency: string; - donation_date: string; - donation_id: string; - likelihood_type: string; - payment_method: string | null; - }[]; - }[]; - total: number | string; - }[]; - }; - }; - default_currency: string; - donor_infos: { - account_numbers: string[]; - contact_id: string; - contact_name: string; - late_by_30_days: boolean; - late_by_60_days: boolean; - pledge_amount: string | null; - pledge_currency: string; - pledge_frequency: string | null; - status: string | null; - }[]; - months: string[]; - salary_currency: string; - updated_at: null; - updated_in_db_at: null; - }; - relationships: { - account_list: { - data: { - id: string; - type: 'account_lists'; - }; - }; - }; -} - -export const mapFourteenMonthReport = ( - data: FourteenMonthReportResponse, - currencyType: FourteenMonthReportCurrencyType, -): FourteenMonthReport => { - const isSalaryType = currencyType === FourteenMonthReportCurrencyType.Salary; - return { - currencyType, - salaryCurrency: data.attributes.salary_currency, - currencyGroups: Object.entries(data.attributes.currency_groups).map( - ([currency, currencyGroup]) => ({ - currency: currency.toUpperCase(), - totals: { - year: Number( - isSalaryType - ? currencyGroup.totals.year_converted - : currencyGroup.totals.year, - ), - months: currencyGroup.totals.months.map((total, index) => ({ - month: data.attributes.months[index], - total: Number(total), - })), - average: currencyGroup.donation_infos.reduce( - (averageTotal, contactDonationInfo) => - averageTotal + Number(contactDonationInfo.average), - 0, - ), - minimum: currencyGroup.donation_infos.reduce( - (minimumTotal, contactDonationInfo) => - minimumTotal + Number(contactDonationInfo.minimum), - 0, - ), - }, - contacts: currencyGroup.donation_infos - .map((contactDonationInfo) => { - const contact = data.attributes.donor_infos.find( - (donor) => donor.contact_id === contactDonationInfo.contact_id, - ); - return { - id: contactDonationInfo.contact_id, - name: contact?.contact_name ?? '', - total: Number(contactDonationInfo.total), - average: Number(contactDonationInfo.average), - minimum: Number(contactDonationInfo.minimum), - months: contactDonationInfo.months.map((month, index) => { - const salaryCurrencyTotal = month.donations.reduce( - (convertedTotal, donation) => - convertedTotal + Number(donation.converted_amount), - 0, - ); - return { - month: data.attributes.months[index], - total: isSalaryType - ? salaryCurrencyTotal - : Number(month.total), - salaryCurrencyTotal, - donations: month.donations.map((donation) => ({ - amount: Number(donation.amount), - date: donation.donation_date, - paymentMethod: donation.payment_method, - currency: donation.currency, - })), - }; - }), - accountNumbers: contact?.account_numbers ?? [], - lateBy30Days: contact?.late_by_30_days ?? false, - lateBy60Days: contact?.late_by_60_days ?? false, - pledgeAmount: contact?.pledge_amount - ? Number(contact?.pledge_amount) - : null, - pledgeCurrency: contact?.pledge_currency, - pledgeFrequency: contact?.pledge_frequency, - status: convertStatus(contact?.status), - }; - }) - .sort((a, b) => a.name.localeCompare(b.name)), - }), - ), - }; -};