diff --git a/mocks/collections.json b/mocks/collections.json index 0dc2c0f8c3..e8727df428 100644 --- a/mocks/collections.json +++ b/mocks/collections.json @@ -55,12 +55,14 @@ "get-wpi-specificaties:standard", "post-afis-businesspartner-bsn:standard", "post-afis-businesspartner-kvk:standard", - "post-afis-factuur-document:standard", + "get-afis-factuur-document:standard", "get-afis-businesspartner-details:standard", + "get-afis-businesspartner-address:standard", "get-afis-businesspartner-phonenumber:standard", "get-afis-businesspartner-emailaddress:standard", "get-afis-facturen:standard", "get-afis-factuur-id:standard", + "get-afis-paylink:standard", "get-powerbrowser-bb-vergunning-attachments:standard", "post-powerbrowser-bb-vergunning-attachments:standard", "post-powerbrowser-token:standard", diff --git a/mocks/fixtures/afis/afgehandelde-facturen.json b/mocks/fixtures/afis/afgehandelde-facturen.json index b75bcfa293..05e5ed0200 100644 --- a/mocks/fixtures/afis/afgehandelde-facturen.json +++ b/mocks/fixtures/afis/afgehandelde-facturen.json @@ -15,6 +15,7 @@ "@rel": "self", "@title": "SomeITEM" }, + "count": 10, "entry": [ { "content": { @@ -24,8 +25,10 @@ "DunningLevel": 0, "ProfitCenterName": "Lisan al Gaib inc.", "NetDueDate": "2023-12-21T00:00:00", + "ClearingDate": "2024-05-01", "ReverseDocument": "someReverseDocument123", - "InvoiceNo": "INV-2023-010" + "DocumentReferenceID": "INV-2023-010", + "AccountingDocument": "INV-2023-010" } } }, @@ -37,8 +40,10 @@ "DunningLevel": 0, "ProfitCenterName": "Lisan al Gaib inc.", "NetDueDate": "2023-12-12T00:00:00", + "ClearingDate": "2024-05-01", "ReverseDocument": "someReverseDocument123", - "InvoiceNo": "INV-2023-008" + "DocumentReferenceID": "INV-2023-008", + "AccountingDocument": "INV-2023-008" } } }, @@ -50,8 +55,10 @@ "DunningLevel": 0, "ProfitCenterName": "Lisan al Gaib inc.", "NetDueDate": "2023-10-26T00:00:00", + "ClearingDate": "2024-05-01", "ReverseDocument": "someReverseDocument123", - "InvoiceNo": "INV-2023-006" + "DocumentReferenceID": "INV-2023-006", + "AccountingDocument": "INV-2023-006" } } }, @@ -63,8 +70,10 @@ "DunningLevel": 0, "ProfitCenterName": "Stellar Innovations Corp.", "NetDueDate": "2023-08-09T00:00:00", + "ClearingDate": "2024-05-01", "ReverseDocument": "someReverseDocument123", - "InvoiceNo": "INV-2023-005" + "DocumentReferenceID": "INV-2023-005", + "AccountingDocument": "INV-2023-005" } } }, @@ -76,8 +85,10 @@ "DunningLevel": 2, "ProfitCenterName": "Eco Synergy Solutions", "NetDueDate": "2023-07-27T00:00:00", + "ClearingDate": "2024-05-01", "ReverseDocument": "", - "InvoiceNo": "INV-2023-003" + "DocumentReferenceID": "INV-2023-003", + "AccountingDocument": "INV-2023-003" } } }, @@ -89,8 +100,10 @@ "DunningLevel": 0, "ProfitCenterName": "Nebula Dynamics Ltd.", "NetDueDate": "2023-06-12T00:00:00", + "ClearingDate": "2024-05-01", "ReverseDocument": "", - "InvoiceNo": "INV-2023-001" + "DocumentReferenceID": "INV-2023-001", + "AccountingDocument": "INV-2023-001" } } }, @@ -102,8 +115,10 @@ "DunningLevel": 0, "ProfitCenterName": "Quantum Horizons GmbH", "NetDueDate": "2023-05-11T00:00:00", + "ClearingDate": "2024-05-01", "ReverseDocument": "", - "InvoiceNo": "INV-2023-002" + "DocumentReferenceID": "INV-2023-002", + "AccountingDocument": "INV-2023-002" } } }, @@ -115,8 +130,10 @@ "DunningLevel": 0, "ProfitCenterName": "Cyber Nexus Technologies", "NetDueDate": "2023-04-28T00:00:00", + "ClearingDate": "2024-05-01", "ReverseDocument": "someReverseDocument123", - "InvoiceNo": "INV-2023-004" + "DocumentReferenceID": "INV-2023-004", + "AccountingDocument": "INV-2023-004" } } }, @@ -128,8 +145,10 @@ "DunningLevel": 0, "ProfitCenterName": "Lisan al Gaib inc.", "NetDueDate": "2023-01-31T00:00:00", + "ClearingDate": "2024-05-01", "ReverseDocument": "someReverseDocument123", - "InvoiceNo": "INV-2023-007" + "DocumentReferenceID": "INV-2023-007", + "AccountingDocument": "INV-2023-007" } } }, @@ -141,8 +160,10 @@ "DunningLevel": 0, "ProfitCenterName": "Lisan al Gaib inc.", "NetDueDate": "2023-01-01T00:00:00", + "ClearingDate": "2024-05-01", "ReverseDocument": "someReverseDocument123", - "InvoiceNo": "INV-2023-009" + "DocumentReferenceID": "INV-2023-009", + "AccountingDocument": "INV-2023-009" } } } diff --git a/mocks/fixtures/afis/openstaande-facturen.json b/mocks/fixtures/afis/openstaande-facturen.json index bef1e9cd88..a72c83e2c7 100644 --- a/mocks/fixtures/afis/openstaande-facturen.json +++ b/mocks/fixtures/afis/openstaande-facturen.json @@ -15,6 +15,7 @@ "@rel": "self", "@title": "SomeITEM" }, + "count": 7, "entry": [ { "content": { @@ -28,8 +29,9 @@ "NetDueDate": "2023-12-21T00:00:00", "NetPaymentAmount": "0.00", "AmountInBalanceTransacCrcy": "343.00", - "InvoiceNo": "5555555", - "Paylink": "http://localhost:3100/mocks-server/afis/paylink" + "DocumentReferenceID": "5555555", + "AccountingDocument": "5555555", + "Paylink": "http://localhost:3100/mocks-api/afis/paylink" } } }, @@ -45,8 +47,9 @@ "NetDueDate": "2023-12-12T00:00:00", "NetPaymentAmount": "0.00", "AmountInBalanceTransacCrcy": "-80.00", - "InvoiceNo": "5555555", - "Paylink": "http://localhost:3100/mocks-server/afis/paylink" + "DocumentReferenceID": "5555555", + "AccountingDocument": "5555555", + "Paylink": "http://localhost:3100/mocks-api/afis/paylink" } } }, @@ -62,8 +65,9 @@ "NetDueDate": "2023-10-26T00:00:00", "NetPaymentAmount": "0.00", "AmountInBalanceTransacCrcy": "-12.10", - "InvoiceNo": "5555555", - "Paylink": "http://localhost:3100/mocks-server/afis/paylink" + "DocumentReferenceID": "5555555", + "AccountingDocument": "5555555", + "Paylink": "http://localhost:3100/mocks-api/afis/paylink" } } }, @@ -79,8 +83,9 @@ "NetDueDate": "2023-08-09T00:00:00", "NetPaymentAmount": "0.00", "AmountInBalanceTransacCrcy": "-16.00", - "InvoiceNo": "5555555", - "Paylink": "http://localhost:3100/mocks-server/afis/paylink" + "DocumentReferenceID": "5555555", + "AccountingDocument": "5555555", + "Paylink": "http://localhost:3100/mocks-api/afis/paylink" } } }, @@ -96,8 +101,9 @@ "NetDueDate": "2023-03-23T00:00:00", "NetPaymentAmount": "-900.98", "AmountInBalanceTransacCrcy": "1000.00", - "InvoiceNo": "5555555", - "Paylink": "http://localhost:3100/mocks-server/afis/paylink" + "DocumentReferenceID": "5555555", + "AccountingDocument": "5555555", + "Paylink": "http://localhost:3100/mocks-api/afis/paylink" } } }, @@ -113,8 +119,9 @@ "NetDueDate": "2023-01-31T00:00:00", "NetPaymentAmount": "0.00", "AmountInBalanceTransacCrcy": "121.00", - "InvoiceNo": "5555555", - "Paylink": "http://localhost:3100/mocks-server/afis/paylink" + "DocumentReferenceID": "5555555", + "AccountingDocument": "5555555", + "Paylink": "http://localhost:3100/mocks-api/afis/paylink" } } }, @@ -132,8 +139,9 @@ }, "NetPaymentAmount": "0.00", "AmountInBalanceTransacCrcy": "230.00", - "InvoiceNo": "5555555", - "Paylink": "http://localhost:3100/mocks-server/afis/paylink" + "DocumentReferenceID": "5555555", + "AccountingDocument": "5555555", + "Paylink": "http://localhost:3100/mocks-api/afis/paylink" } } } diff --git a/mocks/fixtures/afis/overgedragen-facturen.json b/mocks/fixtures/afis/overgedragen-facturen.json new file mode 100644 index 0000000000..f14b4e29f6 --- /dev/null +++ b/mocks/fixtures/afis/overgedragen-facturen.json @@ -0,0 +1,37 @@ +{ + "feed": { + "@base": "https://sap:8080OPERACCTGDOCITEM_CDS/", + "id": "https://sap:8080OPERACCTGDOCITEM_CDS/SomeITEM", + "title": { + "@type": "text", + "$": "SomeITEM" + }, + "updated": "2000-01-22T11:32:02Z", + "author": { + "name": "" + }, + "link": { + "@href": "SomeITEM", + "@rel": "self", + "@title": "SomeITEM" + }, + "count": 1, + "entry": [ + { + "content": { + "@type": "application/xml", + "properties": { + "IsCleared": true, + "DunningLevel": 3, + "ProfitCenterName": "Qui-gon jinn", + "NetDueDate": "2023-12-21T00:00:00", + "ClearingDate": "2024-05-01", + "ReverseDocument": "", + "DocumentReferenceID": "INV-2024-001", + "AccountingDocument": "INV-2024-001" + } + } + } + ] + } +} diff --git a/mocks/routes/afis.js b/mocks/routes/afis.js index b6b60e9b57..114a1721c7 100644 --- a/mocks/routes/afis.js +++ b/mocks/routes/afis.js @@ -65,17 +65,35 @@ module.exports = [ '@type': 'application/xml', properties: { BusinessPartner: 515177, - FullName: 'Taxon Expeditions BV', + BusinessPartnerFullName: 'Taxon Expeditions BV', + }, + }, + }, + ], + }, + }, + }, + }, + ], + }, + { + id: 'get-afis-businesspartner-address', + url: `${settings.MOCK_BASE_PATH}${REST_BASE}/API/ZAPI_BUSINESS_PARTNER_DET_SRV/A_BusinessPartnerAddress`, + method: 'GET', + variants: [ + { + id: 'standard', + type: 'json', + options: { + status: 200, + body: { + feed: { + entry: [ + { + content: { + '@type': 'application/xml', + properties: { AddressID: 430844, - CityName: 'Leiden', - Country: 'NL', - HouseNumber: 20, - HouseNumberSupplementText: '', - PostalCode: '2311 VW', - Region: '', - StreetName: 'Rembrandtstraat', - StreetPrefixName: '', - StreetSuffixName: '', }, }, }, @@ -152,16 +170,41 @@ module.exports = [ type: 'middleware', options: { middleware: (req, res) => { - const isAboutClosedInvoices = req.query?.['$filter']?.includes( - `IsCleared eq true and (DunningLevel eq '0' or ReverseDocument ne '')` - ); - const filename = isAboutClosedInvoices - ? 'afgehandelde-facturen' - : 'openstaande-facturen'; + const stateFilters = { + openstaande: 'IsCleared eq false', + afgehandelde: `DunningLevel ne '3' or ReverseDocument ne ''`, + overgedragen: `DunningLevel eq '3'`, + }; + + const stateName = Object.entries(stateFilters).find( + ([name, filterValueSegment]) => { + return req.query?.['$filter']?.includes(filterValueSegment); + } + )?.[0]; + + if (!stateName) { + return res.status(500).end(); + } // DO NOT adjust this mock data (tests depend on it). // If needed copy, mutate and let it point to the newly made copy. - return res.send(require(`../fixtures/afis/${filename}.json`)); + const facturenData = require( + `../fixtures/afis/${stateName}-facturen.json` + ); + + if (req.query?.['$top']) { + return res.send({ + feed: { + count: facturenData.feed.count, + entry: facturenData.feed.entry.slice( + 0, + parseInt(req.query?.['$top'], 10) + ), + }, + }); + } + + return res.send(facturenData); }, }, }, @@ -183,9 +226,9 @@ module.exports = [ ], }, { - id: 'post-afis-factuur-document', - url: `${settings.MOCK_BASE_PATH}${REST_BASE}/getDebtorInvoice/API_CV_ATTACHMENT_SRV/`, - method: 'POST', + id: 'get-afis-factuur-document', + url: `${settings.MOCK_BASE_PATH}${REST_BASE}/API/ZFI_OPERACCTGDOCITEM_CDS/ZFI_CDS_TOA02`, + method: 'GET', variants: [ { id: 'standard', @@ -197,4 +240,21 @@ module.exports = [ }, ], }, + { + id: 'get-afis-paylink', + url: `${settings.MOCK_BASE_PATH}/afis/paylink`, + method: 'GET', + variants: [ + { + id: 'standard', + type: 'middleware', + options: { + middleware: (req, res, next, core) => { + const htmlResponse = `

Afis factuur betalen

`; + res.send(htmlResponse); + }, + }, + }, + ], + }, ]; diff --git a/src/client/components/DocumentList/DocumentLink.tsx b/src/client/components/DocumentList/DocumentLink.tsx index 0ec8b59baf..5ff1ef0841 100644 --- a/src/client/components/DocumentList/DocumentLink.tsx +++ b/src/client/components/DocumentList/DocumentLink.tsx @@ -53,6 +53,7 @@ export function DocumentLink({ if (isLoading) { return false; } + setErrorVisible(false); setLoading(true); if (!('fetch' in window) || document?.external) { @@ -122,7 +123,7 @@ export function DocumentLink({ }); return false; }, - [document, profileType, isLoading, trackPath, userCity] + [document, profileType, isLoading, trackPath, userCity, setErrorVisible] ); return ( diff --git a/src/client/components/ListPagePaginated/ListPagePaginated.tsx b/src/client/components/ListPagePaginated/ListPagePaginated.tsx index 2673a6eb79..84b7095a30 100644 --- a/src/client/components/ListPagePaginated/ListPagePaginated.tsx +++ b/src/client/components/ListPagePaginated/ListPagePaginated.tsx @@ -20,17 +20,18 @@ interface ListPagePaginatedProps { appRouteBack: string; appRouteParams?: Record | null; backLinkTitle?: string; + body?: ReactNode; displayProps: DisplayProps | null; + errorText?: string; isError: boolean; isLoading: boolean; items: T[]; - title: string; - body?: ReactNode; - thema?: Thema; - tableClassName?: string; - errorText?: string; noItemsText?: string; pageSize?: number; + tableClassName?: string; + thema?: Thema; + title: string; + totalCount?: number; } export function ListPagePaginated({ @@ -47,6 +48,7 @@ export function ListPagePaginated({ isLoading, items, pageSize = DEFAULT_PAGE_SIZE, + totalCount, tableClassName, title, }: ListPagePaginatedProps) { @@ -70,7 +72,7 @@ export function ListPagePaginated({ return items.slice(start, end); }, [currentPage, items, pageSize]); - const total = items.length; + const total = totalCount ?? items.length; return ( diff --git a/src/client/components/Table/TableV2.tsx b/src/client/components/Table/TableV2.tsx index 06eed8c3b5..f56f0efb96 100644 --- a/src/client/components/Table/TableV2.tsx +++ b/src/client/components/Table/TableV2.tsx @@ -1,4 +1,4 @@ -import { Table } from '@amsterdam/design-system-react'; +import { Heading, Paragraph, Table } from '@amsterdam/design-system-react'; import classNames from 'classnames'; import { ReactNode } from 'react'; import { capitalizeFirstLetter } from '../../../universal/helpers/text'; @@ -54,10 +54,12 @@ export interface TableV2Props { className?: string; showTHead?: boolean; caption?: string; + subTitle?: string; } export function TableV2({ caption, + subTitle, items, displayProps, className, @@ -65,40 +67,50 @@ export function TableV2({ }: TableV2Props) { const displayPropEntries = displayProps !== null ? entries(displayProps) : []; return ( - - {!!caption && {caption}} - {showTHead && ( - - - {displayPropEntries.map(([key, label], index) => { - if (!!label) { - return ( - {label} - ); - } - return {label}; - })} - - + <> + {!!caption && ( + + {caption} + )} - - {items.map((item, index) => { - const key = String( - 'id' in item ? item.id : `item-${index}` ?? `tr-${index}` - ); - return ( - - {displayPropEntries.map(([key]) => { - return ( - - {item[key] as ReactNode} - - ); + + {!!subTitle && {subTitle}} +
+ {showTHead && ( + + + {displayPropEntries.map(([key, label], index) => { + if (!!label) { + return ( + + {label} + + ); + } + return {label}; })} - ); - })} - -
+ + )} + + {items.map((item, index) => { + const key = String( + ('id' in item ? item.id : `item-${index}`) ?? `tr-${index}` + ); + return ( + + {displayPropEntries.map(([key]) => { + return ( + + {item[key] as ReactNode} + + ); + })} + + ); + })} + + + ); } diff --git a/src/client/config/thema.ts b/src/client/config/thema.ts index c4a9810ae3..afd06bae94 100644 --- a/src/client/config/thema.ts +++ b/src/client/config/thema.ts @@ -128,7 +128,14 @@ export const DocumentTitles: DocumentTitlesConfig = { [AppRoutes.API1_LOGIN]: 'Inloggen | Mijn Amsterdam', [AppRoutes.API2_LOGIN]: 'Inloggen | Mijn Amsterdam', [AppRoutes.ZAAK_STATUS]: 'Status van uw Zaak | Mijn Amsterdam', - [AppRoutes.AFIS]: 'Facturen en betalen | Mijn Amsterdam', + [AppRoutes.AFIS]: 'Facturen en betalen | overzicht', + [generatePath(AppRoutes['AFIS/FACTUREN'], { state: 'open' })]: + 'Open facturen | Facturen en betalen', + [generatePath(AppRoutes['AFIS/FACTUREN'], { state: 'afgehandeld' })]: + 'Afgehanelde facturen | Facturen en betalen', + [generatePath(AppRoutes['AFIS/FACTUREN'], { state: 'overgedragen' })]: + 'Overgedragen aan belastingen facturen | Facturen en betalen', + [AppRoutes['AFIS/FACTUREN']]: 'Lijst met facturen | Facturen en betalen', [AppRoutes['AFIS/BETAALVOORKEUREN']]: 'Betaalvoorkeuren | Facturen en betalen', }; diff --git a/src/client/hooks/useAppState.ts b/src/client/hooks/useAppState.ts index 7043767380..d6ff509699 100644 --- a/src/client/hooks/useAppState.ts +++ b/src/client/hooks/useAppState.ts @@ -1,5 +1,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { SetterOrUpdater, atom, useRecoilState, useRecoilValue } from 'recoil'; +import { + SetterOrUpdater, + atom, + useRecoilState, + useRecoilValue, + useSetRecoilState, +} from 'recoil'; import { streamEndpointQueryParamKeys } from '../../universal/config/app'; import { FeatureToggle } from '../../universal/config/feature-toggles'; import { @@ -19,6 +25,7 @@ import { captureMessage } from '../utils/monitoring'; import { useDataApi } from './api/useDataApi'; import { useProfileTypeValue } from './useProfileType'; import { SSE_ERROR_MESSAGE, useSSE } from './useSSE'; +import { entries } from '../../universal/helpers/utils'; const fallbackServiceRequestOptions = { postpone: true, @@ -253,13 +260,15 @@ export function useAppStateBagApi({ ); useEffect(() => { + // Initial automatic fetch if (url && !isApiDataCached && !api.isDirty && !api.isLoading) { fetch({ url, postpone: false, }); } - if (!isApiDataCached && !!api.data.content) { + + if (!isApiDataCached && api.isDirty && !api.isLoading) { setAppState((state) => { let localState = state[bagThema]; if (!localState) { @@ -268,7 +277,7 @@ export function useAppStateBagApi({ localState = { ...localState, - [key]: api.data.content as T, + [key]: api.data as ApiResponse, }; return { @@ -279,7 +288,19 @@ export function useAppStateBagApi({ } }, [isApiDataCached, api, key, url]); - return [appState?.[bagThema]?.[key] as T, api, fetch] as const; + return [ + (appState?.[bagThema]?.[key] as ApiResponse) || api.data, // Return the response data from remote system or the pristine data provided to useApiData. + fetch, + isApiDataCached, + ] as const; +} + +export function useGetAppStateBagDataByKey({ + bagThema, + key, +}: Omit): ApiResponse | null { + const appState = useRecoilValue(appStateAtom); + return appState?.[bagThema]?.[key] ?? null; } export function useRemoveAppStateBagData() { @@ -291,7 +312,7 @@ export function useRemoveAppStateBagData() { setAppState( Object.assign({}, appState, { [bagThema]: Object.fromEntries( - Object.entries(local).filter(([key]) => { + entries(local).filter(([key]) => { return keyExpected !== key; }) ), diff --git a/src/client/pages/Afis/Afis-thema-config.ts b/src/client/pages/Afis/Afis-thema-config.ts index fd62251e0e..03adf6fde6 100644 --- a/src/client/pages/Afis/Afis-thema-config.ts +++ b/src/client/pages/Afis/Afis-thema-config.ts @@ -1,50 +1,102 @@ import { ReactNode } from 'react'; +import { generatePath } from 'react-router-dom'; +import { + AfisFacturenByStateResponse, + AfisFacturenResponse, + AfisFactuur, + AfisFactuurState, +} from '../../../server/services/afis/afis-types'; import { AppRoutes } from '../../../universal/config/routes'; import { ZaakDetail } from '../../../universal/types'; -import { MAX_TABLE_ROWS_ON_THEMA_PAGINA } from '../../config/app'; -import { AfisFactuur } from '../../../server/services/afis/afis-types'; import { DisplayProps } from '../../components/Table/TableV2'; - -export type AfisEmandateStub = ZaakDetail & Record; - -export type AfisFactuurFrontend = AfisFactuur & { - factuurNummerEl: ReactNode; -}; +import { MAX_TABLE_ROWS_ON_THEMA_PAGINA } from '../../config/app'; // Themapagina const MAX_TABLE_ROWS_ON_THEMA_PAGINA_OPEN = 5; +const MAX_TABLE_ROWS_ON_THEMA_PAGINA_TRANSFERRED = + MAX_TABLE_ROWS_ON_THEMA_PAGINA; const MAX_TABLE_ROWS_ON_THEMA_PAGINA_CLOSED = MAX_TABLE_ROWS_ON_THEMA_PAGINA; -const displayPropsFacturen: DisplayProps = { +const displayPropsFacturenOpen: DisplayProps = { afzender: 'Afzender', - factuurNummer: 'Factuurnummer', + factuurNummerEl: 'Factuurnummer', statusDescription: 'Status', paymentDueDateFormatted: 'Vervaldatum', }; -export const listPageParamKind = { - open: 'open', - closed: 'closed', -} as const; +const displayPropsFacturenAfgehandeld: DisplayProps = { + afzender: 'Afzender', + factuurNummerEl: 'Factuurnummer', + statusDescription: 'Status', +}; -export type ListPageParamKey = keyof typeof listPageParamKind; -export type ListPageParamKind = (typeof listPageParamKind)[ListPageParamKey]; +const displayPropsFacturenOvergedragen: DisplayProps = { + afzender: 'Afzender', + factuurNummerEl: 'Factuurnummer', + statusDescription: 'Status', +}; -export const listPageTitle = { - [listPageParamKind.open]: 'Openstaande facturen', - [listPageParamKind.closed]: 'Afgehandelde facturen', -} as const; +export const listPageTitle: Record = { + open: 'Openstaande facturen', + afgehandeld: 'Afgehandelde facturen', + overgedragen: 'Overgedragen facturen', +}; -export const facturenTableConfig = { - [listPageParamKind.open]: { - title: listPageTitle[listPageParamKind.open], - displayProps: displayPropsFacturen, +export type AfisEmandateStub = ZaakDetail & Record; + +export type AfisFactuurFrontend = AfisFactuur & { + factuurNummerEl: ReactNode; +}; + +export type AfisFacturenResponseFrontend = AfisFacturenResponse & { + facturen: AfisFactuurFrontend[]; +}; + +export type AfisFacturenByStateFrontend = { + [key in keyof AfisFacturenByStateResponse]: AfisFacturenResponseFrontend; +}; + +type AfisFacturenTableConfig = { + title: string; + subTitle?: string; + displayProps: DisplayProps; + maxItems: number; + listPageLinkLabel: string; + listPageRoute: string; +}; + +type AfisFacturenTableConfigByState = Record< + AfisFactuurState, + AfisFacturenTableConfig +>; + +export const facturenTableConfig: AfisFacturenTableConfigByState = { + open: { + title: listPageTitle.open, + displayProps: displayPropsFacturenOpen, maxItems: MAX_TABLE_ROWS_ON_THEMA_PAGINA_OPEN, + listPageLinkLabel: 'Alle openstaande facturen', + listPageRoute: generatePath(AppRoutes['AFIS/FACTUREN'], { + state: 'open', + }), }, - [listPageParamKind.closed]: { - title: listPageTitle[listPageParamKind.closed], - displayProps: displayPropsFacturen, + afgehandeld: { + title: listPageTitle.afgehandeld, + displayProps: displayPropsFacturenAfgehandeld, maxItems: MAX_TABLE_ROWS_ON_THEMA_PAGINA_CLOSED, + listPageLinkLabel: 'Alle afgehandelde facturen', + listPageRoute: generatePath(AppRoutes['AFIS/FACTUREN'], { + state: 'afgehandeld', + }), + }, + overgedragen: { + title: listPageTitle.overgedragen, + displayProps: displayPropsFacturenOvergedragen, + maxItems: MAX_TABLE_ROWS_ON_THEMA_PAGINA_TRANSFERRED, + listPageLinkLabel: 'Alle overgedragen facturen', + listPageRoute: generatePath(AppRoutes['AFIS/FACTUREN'], { + state: 'overgedragen', + }), }, } as const; @@ -53,6 +105,13 @@ const displayPropsEmandates: DisplayProps = { name: 'Naam', }; +export const businessPartnerDetailsLabels = { + fullName: 'Debiteurnaam', + businessPartnerId: 'Debiteurnummer', + email: 'E-mailadres factuur', + phone: 'Telefoonnummer', +}; + export const eMandateTableConfig = { active: { title: `Actieve automatische incasso's`, diff --git a/src/client/pages/Afis/Afis.module.scss b/src/client/pages/Afis/Afis.module.scss index daa8f230d9..cad0d6c754 100644 --- a/src/client/pages/Afis/Afis.module.scss +++ b/src/client/pages/Afis/Afis.module.scss @@ -1,6 +1,19 @@ +@import '../../styles/_global.scss'; + .FacturenTable { - tr td:nth-child(2) { - white-space: normal; - word-wrap: break-word; + tr { + th, + td { + @media screen and (min-width: $ams-breakpoint-medium) { + &:nth-child(1), + &:nth-child(2) { + width: 25%; + } + } + &:nth-child(2) { + white-space: normal; + word-wrap: break-word; + } + } } } diff --git a/src/client/pages/Afis/Afis.test.tsx b/src/client/pages/Afis/Afis.test.tsx index 1b2145229c..cfd5d41347 100644 --- a/src/client/pages/Afis/Afis.test.tsx +++ b/src/client/pages/Afis/Afis.test.tsx @@ -1,17 +1,16 @@ import { render, waitFor } from '@testing-library/react'; - import { generatePath } from 'react-router-dom'; import { MutableSnapshot } from 'recoil'; +import { + AfisBusinessPartnerDetailsTransformed, + AfisFacturenByStateResponse, +} from '../../../server/services/afis/afis-types'; +import { bffApi } from '../../../test-utils'; import { AppRoutes } from '../../../universal/config/routes'; import { AppState } from '../../../universal/types'; import { appStateAtom } from '../../hooks/useAppState'; import MockApp from '../MockApp'; import { AfisThemaPagina } from './Afis'; -import { bffApi } from '../../../test-utils'; -import { - AfisBusinessPartnerDetailsTransformed, - AfisFactuur, -} from '../../../server/services/afis/afis-types'; const businessPartnerIdEncrypted = 'xxx-123-xxx'; const testState = { @@ -31,68 +30,75 @@ function initializeState(snapshot: MutableSnapshot) { describe('', () => { const businessPartnerDetails: AfisBusinessPartnerDetailsTransformed = { addressId: 999, - businessPartnerId: 515177, + businessPartnerId: '000515177', fullName: 'Taxon Expeditions BV', phone: null, email: null, }; - const mockFacturen = { - open: [ - { - id: 'F001', - title: 'Invoice F001', - afzender: 'Company A', - datePublished: '2023-01-15', - datePublishedFormatted: '15 januari 2023', - paymentDueDate: '2023-02-15', - paymentDueDateFormatted: '15 februari 2023', - debtClearingDate: null, - debtClearingDateFormatted: null, - amountOwed: 1000, - amountOwedFormatted: '€ 1.000,00', - factuurNummer: 'F001', - status: 'openstaand', - paylink: 'https://payment.example.com/F001', - documentDownloadLink: 'https://download.example.com/F001', - }, - { - id: 'F003', - title: 'Invoice F003', - afzender: 'Company C', - datePublished: '2023-03-10', - datePublishedFormatted: '10 maart 2023', - paymentDueDate: '2023-04-10', - paymentDueDateFormatted: '10 april 2023', - debtClearingDate: null, - debtClearingDateFormatted: null, - amountOwed: 2000, - amountOwedFormatted: '€ 2.000,00', - factuurNummer: 'F003', - status: 'openstaand', - paylink: 'https://payment.example.com/F003', - documentDownloadLink: 'https://download.example.com/F003', - }, - ], - closed: [ - { - id: 'F002', - title: 'Invoice F002', - afzender: 'Company B', - datePublished: '2023-02-01', - datePublishedFormatted: '1 februari 2023', - paymentDueDate: '2023-03-01', - paymentDueDateFormatted: '1 maart 2023', - debtClearingDate: '2023-02-15', - debtClearingDateFormatted: '15 februari 2023', - amountOwed: 0, - amountOwedFormatted: '€ 0,00', - factuurNummer: 'F002', - status: 'betaald', - paylink: null, - documentDownloadLink: 'https://download.example.com/F002', - }, - ], + const mockFacturen: AfisFacturenByStateResponse = { + open: { + count: 2, + facturen: [ + { + afzender: 'Company A', + datePublished: '2023-01-15', + datePublishedFormatted: '15 januari 2023', + paymentDueDate: '2023-02-15', + paymentDueDateFormatted: '15 februari 2023', + debtClearingDate: null, + debtClearingDateFormatted: null, + amountOwed: 1000, + amountOwedFormatted: '€ 1.000,00', + factuurNummer: 'F001', + status: 'openstaand', + statusDescription: 'openstaand', + paylink: 'https://payment.example.com/F001', + documentDownloadLink: 'https://download.example.com/F001', + factuurDocumentId: '1', + }, + { + afzender: 'Company C', + datePublished: '2023-03-10', + datePublishedFormatted: '10 maart 2023', + paymentDueDate: '2023-04-10', + paymentDueDateFormatted: '10 april 2023', + debtClearingDate: null, + debtClearingDateFormatted: null, + amountOwed: 2000, + amountOwedFormatted: '€ 2.000,00', + factuurNummer: 'F003', + status: 'openstaand', + statusDescription: 'openstaand', + paylink: 'https://payment.example.com/F003', + documentDownloadLink: 'https://download.example.com/F003', + factuurDocumentId: '2', + }, + ], + }, + afgehandeld: { + count: 1, + facturen: [ + { + afzender: 'Company B', + datePublished: '2023-02-01', + datePublishedFormatted: '1 februari 2023', + paymentDueDate: '2023-03-01', + paymentDueDateFormatted: '1 maart 2023', + debtClearingDate: '2023-02-15', + debtClearingDateFormatted: '15 februari 2023', + amountOwed: 0, + amountOwedFormatted: '€ 0,00', + factuurNummer: 'F002', + status: 'betaald', + statusDescription: 'betaald', + paylink: null, + documentDownloadLink: 'https://download.example.com/F002', + factuurDocumentId: '3', + }, + ], + }, + overgedragen: { count: 0, facturen: [] }, }; bffApi diff --git a/src/client/pages/Afis/Afis.tsx b/src/client/pages/Afis/Afis.tsx index 73ec6d194d..fbb389ac73 100644 --- a/src/client/pages/Afis/Afis.tsx +++ b/src/client/pages/Afis/Afis.tsx @@ -1,59 +1,105 @@ -import { Button, Paragraph } from '@amsterdam/design-system-react'; -import { generatePath, useHistory } from 'react-router-dom'; +import { + Alert, + Button, + Grid, + Paragraph, + UnorderedList, +} from '@amsterdam/design-system-react'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { entries } from '../../../universal/helpers/utils'; import { ThemaTitles } from '../../config/thema'; import ThemaPagina from '../ThemaPagina/ThemaPagina'; import ThemaPaginaTable from '../ThemaPagina/ThemaPaginaTable'; -import { useAfisThemaData } from './useAfisThemaData.hook'; -import { AppRoutes } from '../../../universal/config/routes'; -import { AfisFactuur } from '../../../server/services/afis/afis-types'; -import { ListPageParamKind } from './Afis-thema-config'; +import { AfisFactuurFrontend } from './Afis-thema-config'; import styles from './Afis.module.scss'; +import { useAfisThemaData } from './useAfisThemaData.hook'; + +const pageContentTop = ( + + Hieronder ziet u een overzicht van uw facturen. U ziet hier niet de facturen + van belastingen. U kunt deze bij belastingen vinden. + +); export function AfisThemaPagina() { const history = useHistory(); const { - facturen, + dependencyErrors, + facturenByState, facturenTableConfig, - isFacturenError, - isFacturenLoading, isThemaPaginaError, isThemaPaginaLoading, + isOverviewApiError, + isOverviewApiLoading, + listPageTitle, routes, - hasFailedFacturenOpenDependency, - hasFailedFacturenClosedDependency, } = useAfisThemaData(); - const pageContentTop = ( - <> - - Hieronder kunt u uw facturatiegegevens inzien en een automatische - incasso instellen per afdeling van de gemeente. - + const isPartialError = entries(dependencyErrors).some( + ([, hasError]) => hasError + ); + + const pageContentSecondary = ( + + + + + De betaalstatus kan 3 werkdagen achterlopen op de doorgevoerde + wijzigingen. + + + Betalingsregelingen zijn niet zichtbaar in dit overzicht. + + + + + ); + + const pageContentErrorAlert = ( + <> + We kunnen niet alle gegevens tonen.{' '} + {entries(dependencyErrors) + .filter(([, hasError]) => hasError) + .map(([state]) => ( + +
- {listPageTitle[state]} kunnen nu niet getoond worden. +
+ ))} ); - const pageContentTables = Object.entries(facturenTableConfig).map( - ([kind, { title, displayProps, maxItems }]) => { - const kindFacturen = ( - facturen as { [K in ListPageParamKind]: AfisFactuur[] } - )[kind as ListPageParamKind]; + const pageContentTables = entries(facturenTableConfig).map( + ([ + state, + { + title, + subTitle, + displayProps, + maxItems, + listPageLinkLabel, + listPageRoute, + }, + ]) => { return ( - - key={kind} + + key={state} title={title} - zaken={kindFacturen} + subTitle={subTitle} + zaken={facturenByState?.[state]?.facturen ?? []} displayProps={displayProps} textNoContent={`U heeft geen ${title.toLowerCase()}`} maxItems={maxItems} - listPageRoute={generatePath(AppRoutes['AFIS/FACTUREN'], { - kind, - })} + totalItems={facturenByState?.[state]?.count} + listPageLinkLabel={listPageLinkLabel} + listPageRoute={listPageRoute} className={styles.FacturenTable} /> ); @@ -63,21 +109,27 @@ export function AfisThemaPagina() { return ( + {pageContentSecondary} + {pageContentTables} + + } /> ); } diff --git a/src/client/pages/Afis/AfisBetaalVoorkeuren.test.tsx b/src/client/pages/Afis/AfisBetaalVoorkeuren.test.tsx index b3bc372bb7..0ef5ff092c 100644 --- a/src/client/pages/Afis/AfisBetaalVoorkeuren.test.tsx +++ b/src/client/pages/Afis/AfisBetaalVoorkeuren.test.tsx @@ -30,7 +30,7 @@ function initializeState(snapshot: MutableSnapshot) { describe('', () => { const businessPartnerDetails: AfisBusinessPartnerDetailsTransformed = { addressId: 999, - businessPartnerId: 515177, + businessPartnerId: '515177', fullName: 'Taxon Expeditions BV', phone: null, email: null, @@ -44,14 +44,18 @@ describe('', () => { }); bffApi - .get(`/services/afis/facturen/${businessPartnerIdEncrypted}`) + .get(`/services/afis/facturen/overzicht/${businessPartnerIdEncrypted}`) .reply(200, { - content: [], + content: { + open: { facturen: [], count: 0 }, + afgehandeld: { facturen: [], count: 0 }, + overgedragen: { facturen: [], count: 0 }, + }, status: 'OK', }); - const routeEntry = generatePath(AppRoutes['AFIS/BETAALVOORKEUREN']); const routePath = AppRoutes['AFIS/BETAALVOORKEUREN']; + const routeEntry = generatePath(routePath); const Component = () => ( ', () => { /> ); - it('Matches the Full Page snapshot', async () => { - const screen = render(); + test('Display business partner details', async () => { const user = userEvent.setup(); - const toonButton = screen.getByText('Toon'); - await user.click(toonButton); + let screen = render(); await waitFor(() => { expect(screen.getByText('Taxon Expeditions BV')).toBeInTheDocument(); }); - expect(screen.asFragment()).toMatchSnapshot(); + const verbergKnop = screen.getByText('Verberg'); + expect(verbergKnop).toBeInTheDocument(); + + await user.click(verbergKnop); + + expect(screen.getByText('Toon')).toBeInTheDocument(); }); }); diff --git a/src/client/pages/Afis/AfisBetaalVoorkeuren.tsx b/src/client/pages/Afis/AfisBetaalVoorkeuren.tsx index 32db64d4f4..af78bbcda3 100644 --- a/src/client/pages/Afis/AfisBetaalVoorkeuren.tsx +++ b/src/client/pages/Afis/AfisBetaalVoorkeuren.tsx @@ -1,42 +1,41 @@ import { Grid, - Heading, Link, LinkList, Paragraph, } from '@amsterdam/design-system-react'; -import { - AfisBusinessPartnerDetailsTransformed, - AfisBusinessPartnerKnownResponse, -} from '../../../server/services/afis/afis-types'; +import { AfisBusinessPartnerDetailsTransformed } from '../../../server/services/afis/afis-types'; +import { FeatureToggle } from '../../../universal/config/feature-toggles'; +import { AppRoutes } from '../../../universal/config/routes'; +import { entries } from '../../../universal/helpers/utils'; +import { LoadingContent } from '../../components'; +import { CollapsiblePanel } from '../../components/CollapsiblePanel/CollapsiblePanel'; import { Datalist } from '../../components/Datalist/Datalist'; import { DisplayProps } from '../../components/Table/TableV2'; +import { ThemaTitles } from '../../config/thema'; import ThemaPagina from '../ThemaPagina/ThemaPagina'; -import styles from './AfisBetaalVoorkeuren.module.scss'; +import ThemaPaginaTable from '../ThemaPagina/ThemaPaginaTable'; +import { AfisEmandateStub } from './Afis-thema-config'; import { useAfisBetaalVoorkeurenData, useAfisThemaData, } from './useAfisThemaData.hook'; -import { AppRoutes } from '../../../universal/config/routes'; -import { ThemaTitles } from '../../config/thema'; -import ThemaPaginaTable from '../ThemaPagina/ThemaPaginaTable'; -import { AfisEmandateStub } from './Afis-thema-config'; -import { CollapsiblePanel } from '../../components/CollapsiblePanel/CollapsiblePanel'; -import { LoadingContent } from '../../components'; type AfisBusinessPartnerProps = { businesspartner: AfisBusinessPartnerDetailsTransformed | null; labels: DisplayProps; isLoading: boolean; + startCollapsed: boolean; }; function AfisBusinessPartnerDetails({ businesspartner, labels, isLoading, + startCollapsed = true, }: AfisBusinessPartnerProps) { const rows = !!businesspartner - ? Object.entries(labels).map(([key, label]) => { + ? entries(labels).map(([key, label]) => { const value = businesspartner[key as keyof typeof businesspartner]; return { label, @@ -54,7 +53,7 @@ function AfisBusinessPartnerDetails({ return ( - + {isLoading && } {!isLoading && !!rows.length && ( @@ -78,19 +77,21 @@ function AfisBusinessPartnerDetails({ export function AfisBetaalVoorkeuren() { const { businessPartnerIdEncrypted, - isThemaPaginaLoading, isThemaPaginaError, + isThemaPaginaLoading, } = useAfisThemaData(); + const { businesspartnerDetails, businessPartnerDetailsLabels, + eMandates, + eMandateTableConfig, hasBusinessPartnerDetailsError, hasEmandatesError, hasFailedEmailDependency, hasFailedPhoneDependency, + hasFailedFullNameDependency, isLoadingBusinessPartnerDetails, - eMandateTableConfig, - eMandates, isLoadingEmandates, } = useAfisBetaalVoorkeurenData(businessPartnerIdEncrypted); @@ -99,25 +100,27 @@ export function AfisBetaalVoorkeuren() { isLoadingBusinessPartnerDetails && isLoadingEmandates; - const eMandateTables = Object.entries(eMandateTableConfig).map( - ([kind, { title, displayProps, filter }]) => { - return ( - - key={kind} - title={title} - zaken={eMandates.filter(filter)} - displayProps={displayProps} - textNoContent={`U heeft geen ${title.toLowerCase()}`} - maxItems={-1} - /> - ); - } - ); + const eMandateTables = + FeatureToggle.afisEmandatesActive && + entries(eMandateTableConfig).map( + ([kind, { title, displayProps, filter }]) => { + return ( + + key={kind} + title={title} + zaken={eMandates.filter(filter)} + displayProps={displayProps} + textNoContent={`U heeft geen ${title.toLowerCase()}`} + maxItems={-1} + /> + ); + } + ); const pageContentTop = ( <> - Hier kunt u uw betaal gegevens inzien en automatische incasso gegevens + Hier kunt u uw betaalgegevens inzien en automatische incasso gegevens instellen. @@ -129,20 +132,30 @@ export function AfisBetaalVoorkeuren() { businesspartner={businesspartnerDetails} labels={businessPartnerDetailsLabels} isLoading={isLoadingBusinessPartnerDetails || isThemaPaginaLoading} + startCollapsed={FeatureToggle.afisEmandatesActive} /> {eMandateTables} ); - const errorAlertContent = ( + const errorAlertContent = isThemaPaginaError ? ( + <>Wij kunnen nu niet alle gegevens laten zien. + ) : ( <> {!hasBusinessPartnerDetailsError && - (hasFailedEmailDependency || hasFailedPhoneDependency) && ( + (hasFailedEmailDependency || + hasFailedPhoneDependency || + hasFailedFullNameDependency) && ( <> De volgende gegevens konden niet worden opgehaald: + {hasFailedFullNameDependency && ( + <> +
- Debiteurnaam + + )} {hasFailedEmailDependency && ( <> -
- Email +
- E-mailadres )} {hasFailedPhoneDependency && ( @@ -172,7 +185,8 @@ export function AfisBetaalVoorkeuren() { (hasBusinessPartnerDetailsError && hasEmandatesError) } isPartialError={ - hasFailedEmailDependency || + hasFailedFullNameDependency || + hasFailedPhoneDependency || hasFailedPhoneDependency || hasBusinessPartnerDetailsError || hasEmandatesError diff --git a/src/client/pages/Afis/AfisFacturen.test.tsx b/src/client/pages/Afis/AfisFacturen.test.tsx index a4a7730306..b9019c8c7a 100644 --- a/src/client/pages/Afis/AfisFacturen.test.tsx +++ b/src/client/pages/Afis/AfisFacturen.test.tsx @@ -8,6 +8,7 @@ import MockApp from '../MockApp'; import { AfisFacturen } from './AfisFacturen'; import { bffApi } from '../../../test-utils'; import { AfisFactuur } from '../../../server/services/afis/afis-types'; +import { AfisFactuurFrontend } from './Afis-thema-config'; const businessPartnerIdEncrypted = 'yyy-456-yyy'; const testState = { @@ -25,65 +26,130 @@ function initializeState(snapshot: MutableSnapshot) { } describe('', () => { - const mockFacturen: AfisFactuur[] = [ - { - afzender: 'Company D', - datePublished: '2023-04-01', - datePublishedFormatted: '1 april 2023', - paymentDueDate: '2023-05-01', - paymentDueDateFormatted: '1 mei 2023', - debtClearingDate: null, - debtClearingDateFormatted: null, - amountOwed: 1500, - amountOwedFormatted: '€ 1.500,00', - factuurNummer: 'F004', - status: 'openstaand', - paylink: 'https://payment.example.com/F004', - documentDownloadLink: 'https://download.example.com/F004', - }, - { - afzender: 'Company E', - datePublished: '2023-04-15', - datePublishedFormatted: '15 april 2023', - paymentDueDate: '2023-05-15', - paymentDueDateFormatted: '15 mei 2023', - debtClearingDate: null, - debtClearingDateFormatted: null, - amountOwed: 2500, - amountOwedFormatted: '€ 2.500,00', - factuurNummer: 'F005', - status: 'openstaand', - paylink: 'https://payment.example.com/F005', - documentDownloadLink: 'https://download.example.com/F005', - }, - ]; + const routePath = AppRoutes['AFIS/FACTUREN']; - bffApi - .get(`/services/afis/facturen/open/${businessPartnerIdEncrypted}`) - .reply(200, { - content: mockFacturen, - status: 'OK', + test('Lists Open facturen', async () => { + bffApi + .get(`/services/afis/facturen/overzicht/${businessPartnerIdEncrypted}`) + .reply(200, { + content: { + open: { + count: 1, + facturen: [ + { + id: '2', + factuurDocumentId: '2', + afzender: 'Company E', + datePublished: '2023-04-15', + datePublishedFormatted: '15 april 2023', + paymentDueDate: '2023-05-15', + paymentDueDateFormatted: '15 mei 2023', + debtClearingDate: null, + debtClearingDateFormatted: null, + amountOwed: 2500, + amountOwedFormatted: '€ 2.500,00', + factuurNummer: 'F005', + factuurNummerEl: 'F005', + status: 'openstaand', + statusDescription: 'Openstaand description', + paylink: 'https://payment.example.com/F005', + documentDownloadLink: 'https://download.example.com/F005', + }, + ], + }, + afgehandeld: { facturen: [], count: 0 }, + overgedragen: { facturen: [], count: 0 }, + }, + status: 'OK', + }); + + const routeEntry = generatePath(routePath, { + state: 'open', }); - const routeEntry = generatePath(AppRoutes['AFIS/FACTUREN'], { kind: 'open' }); - const routePath = AppRoutes['AFIS/FACTUREN']; + const Component = () => ( + + ); + const screen = render(); - const Component = () => ( - - ); + await waitFor(() => { + expect(screen.getByText('15 mei 2023')).toBeInTheDocument(); + expect(screen.getByText('Openstaand description')).toBeInTheDocument(); + expect(screen.getByText('Company E')).toBeInTheDocument(); + }); + }); + + test('Lists Closed facturen', async () => { + bffApi + .get(`/services/afis/facturen/afgehandeld/${businessPartnerIdEncrypted}`) + .reply(200, { + content: { + afgehandeld: { + count: 1, + facturen: [ + { + id: '1', + factuurDocumentId: '1', + afzender: 'Company D', + datePublished: '2023-04-01', + datePublishedFormatted: '1 april 2023', + paymentDueDate: '2023-05-01', + paymentDueDateFormatted: '1 mei 2023', + debtClearingDate: null, + debtClearingDateFormatted: null, + amountOwed: 1500, + amountOwedFormatted: '€ 1.500,00', + factuurNummer: 'F001', + factuurNummerEl: 'F001', + status: 'betaald', + statusDescription: 'Betaalde description', + paylink: null, + documentDownloadLink: 'https://download.example.com/F004', + }, + ], + }, + }, + status: 'OK', + }); + + bffApi + .get(`/services/afis/facturen/overzicht/${businessPartnerIdEncrypted}`) + .reply(200, { + content: { + open: { count: 0, facturen: [] }, + afgehandeld: { facturen: [], count: 0 }, + overgedragen: { facturen: [], count: 0 }, + }, + status: 'OK', + }); + + const routeEntry = generatePath(routePath, { + state: 'afgehandeld', + }); - it('Matches the Full Page snapshot', async () => { + const Component = () => ( + + ); const screen = render(); - expect(screen.asFragment()).toMatchSnapshot(); await waitFor(() => { expect(screen.getByText('Company D')).toBeInTheDocument(); - expect(screen.getByText('Company E')).toBeInTheDocument(); + expect(screen.getByText('Betaalde description')).toBeInTheDocument(); + expect(screen.queryByText('15 mei 2023')).not.toBeInTheDocument(); }); }); + + test('Partial error display', () => {}); + + test('Error display', () => {}); }); diff --git a/src/client/pages/Afis/AfisFacturen.tsx b/src/client/pages/Afis/AfisFacturen.tsx index c133b1c4e4..969d163a7c 100644 --- a/src/client/pages/Afis/AfisFacturen.tsx +++ b/src/client/pages/Afis/AfisFacturen.tsx @@ -1,27 +1,39 @@ import { useParams } from 'react-router-dom'; -import { ListPageParamKind } from './Afis-thema-config'; +import { + AfisFactuur, + AfisFactuurState, +} from '../../../server/services/afis/afis-types'; import { ListPagePaginated } from '../../components/ListPagePaginated/ListPagePaginated'; -import { useAfisThemaData } from './useAfisThemaData.hook'; -import { AfisFactuur } from '../../../server/services/afis/afis-types'; +import { ThemaTitles } from '../../config/thema'; import styles from './Afis.module.scss'; +import { useAfisListPageData } from './useAfisThemaData.hook'; export const AfisFacturen = () => { - const { kind } = useParams<{ kind: ListPageParamKind }>(); - const { facturen, facturenTableConfig, routes, isFacturenLoading } = - useAfisThemaData(kind); - const listPageTableConfig = facturenTableConfig[kind]; + const { state } = useParams<{ state: AfisFactuurState }>(); + const { + facturenListResponse, + isListPageError, + isListPageLoading, + facturenTableConfig, + isThemaPaginaError, + isThemaPaginaLoading, + routes, + } = useAfisListPageData(state); + + const listPageTableConfig = facturenTableConfig[state]; + const facturen = facturenListResponse?.facturen ?? []; return ( - items={facturen as AfisFactuur[]} - backLinkTitle={listPageTableConfig.title} + items={facturen} + backLinkTitle={ThemaTitles.AFIS} title={listPageTableConfig.title} appRoute={routes.listPageFacturen} - appRouteParams={{ kind }} + appRouteParams={{ state }} appRouteBack={routes.themaPage} displayProps={listPageTableConfig.displayProps} - isLoading={isFacturenLoading} - isError={false} + isLoading={isThemaPaginaLoading || isListPageLoading} + isError={isThemaPaginaError || isListPageError} tableClassName={styles.FacturenTable} /> ); diff --git a/src/client/pages/Afis/__snapshots__/Afis.test.tsx.snap b/src/client/pages/Afis/__snapshots__/Afis.test.tsx.snap index f5d5287414..c8921b088c 100644 --- a/src/client/pages/Afis/__snapshots__/Afis.test.tsx.snap +++ b/src/client/pages/Afis/__snapshots__/Afis.test.tsx.snap @@ -81,16 +81,10 @@ exports[` > Matches the Full Page snapshot 1`] = ` class="ams-grid__cell ams-grid__cell--span-all" >

- Hieronder kunt u uw facturatiegegevens inzien en een automatische incasso instellen per afdeling van de gemeente. + Hieronder ziet u een overzicht van uw facturen. U ziet hier niet de facturen van belastingen. U kunt deze bij belastingen vinden.

-
> Matches the Full Page snapshot 1`] = ` Meer over facturen van de gemeente +
  • + + + + + Belastingen op Mijn Amsterdam + +
  • > Matches the Full Page snapshot 1`] = ` - -
    - -
    -
    -
    -

    - Hier kunt u uw betaal gegevens inzien en automatische incasso gegevens instellen. -

    -
    - -
    -
    -

    - Betaalgegevens -

    - -
    -
    -
    -
    -
    -
    - Debiteurnaam -
    -
    - Taxon Expeditions BV -
    -
    - Debiteurnummer -
    -
    - 515177 -
    -
    - E-mailadres factuur -
    -
    - -
    -
    - Telefoonnummer -
    -
    - -
    -
    -
    - -
    -
    -
    -
    -
    - - - -
    - Actieve automatische incasso's -
    -
    -

    - U heeft geen actieve automatische incasso's -

    -
    -
    -
    - - - -
    - Niet actieve automatische incasso's -
    -
    -

    - U heeft geen niet actieve automatische incasso's -

    -
    -
    -
    -
    -
    -`; diff --git a/src/client/pages/Afis/__snapshots__/AfisFacturen.test.tsx.snap b/src/client/pages/Afis/__snapshots__/AfisFacturen.test.tsx.snap deleted file mode 100644 index ddd48fa7d3..0000000000 --- a/src/client/pages/Afis/__snapshots__/AfisFacturen.test.tsx.snap +++ /dev/null @@ -1,101 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[` > Matches the Full Page snapshot 1`] = ` - -
    - -
    -
    -
    - - - - - -
    -
    -
    -
    -
    -`; diff --git a/src/client/pages/Afis/useAfisThemaData.hook.tsx b/src/client/pages/Afis/useAfisThemaData.hook.tsx index 5a37c191e5..ed7d66cc82 100644 --- a/src/client/pages/Afis/useAfisThemaData.hook.tsx +++ b/src/client/pages/Afis/useAfisThemaData.hook.tsx @@ -2,91 +2,58 @@ import { ReactNode, useEffect, useMemo } from 'react'; import { AfisBusinessPartnerDetailsTransformed, AfisBusinessPartnerKnownResponse, + AfisFacturenByStateResponse, AfisFactuur, + AfisFactuurState, } from '../../../server/services/afis/afis-types'; import { + ApiResponse, hasFailedDependency, isError, isLoading, } from '../../../universal/helpers/api'; -import { DisplayProps } from '../../components/Table/TableV2'; +import { capitalizeFirstLetter } from '../../../universal/helpers/text'; +import { entries } from '../../../universal/helpers/utils'; +import { DocumentLink } from '../../components/DocumentList/DocumentLink'; +import { MaLink } from '../../components/MaLink/MaLink'; import { BFFApiUrls } from '../../config/api'; import { BagThemas } from '../../config/thema'; import { useAppStateBagApi, useAppStateGetter } from '../../hooks/useAppState'; import { + AfisFacturenByStateFrontend, + businessPartnerDetailsLabels, eMandateTableConfig, facturenTableConfig, - ListPageParamKind, - listPageParamKind, listPageTitle, routes, } from './Afis-thema-config'; -import { MaLink } from '../../components/MaLink/MaLink'; -import { DocumentLink } from '../../components/DocumentList/DocumentLink'; +const AFIS_OVERVIEW_STATE_KEY = 'afis-facturen-overzicht'; -export function useAfisThemaData(kind?: ListPageParamKind) { - const { AFIS } = useAppStateGetter(); - const businessPartnerIdEncrypted = AFIS.content?.businessPartnerIdEncrypted; - const [facturen, facturenApi, fetchFacturen] = useAppStateBagApi< - { [key in ListPageParamKind]: AfisFactuur[] } | AfisFactuur[] | null - >({ - bagThema: BagThemas.AFIS, - key: `${businessPartnerIdEncrypted}-facturen-${kind}`, - }); - - useEffect(() => { - if (businessPartnerIdEncrypted) { - const url = kind - ? `${BFFApiUrls.AFIS_FACTUREN}/${kind}/${businessPartnerIdEncrypted}` - : `${BFFApiUrls.AFIS_FACTUREN_OVERZICHT}/${businessPartnerIdEncrypted}`; - fetchFacturen({ url }); - } - }, [businessPartnerIdEncrypted, fetchFacturen, kind]); - - const updatedFacturen = useMemo(() => { - return kind - ? Array.isArray(facturen) - ? (facturen as AfisFactuur[])?.map(mapFactuur) - : [] - : facturen && Object.keys(facturen).length > 0 - ? Object.fromEntries( - Object.entries(facturen).map(([key, value]) => [ - key, - value.map(mapFactuur), - ]) - ) - : { open: [], closed: [] }; - }, [facturen, kind]); - - return { - businessPartnerIdEncrypted: - AFIS.content?.businessPartnerIdEncrypted ?? null, - isThemaPaginaLoading: isLoading(AFIS), - isThemaPaginaError: isError(AFIS, false), - routes, - facturenTableConfig, - listPageTitle, - listPageParamKind, - isFacturenError: facturenApi.isError, - isFacturenLoading: facturenApi.isLoading, - hasFailedFacturenOpenDependency: hasFailedDependency( - facturenApi.data, - 'open' - ), - hasFailedFacturenClosedDependency: hasFailedDependency( - facturenApi.data, - 'closed' - ), - facturen: updatedFacturen, - }; +function getInvoiceStatusDescriptionFrontend(factuur: AfisFactuur): ReactNode { + switch (factuur.status) { + case 'openstaand': + return ( + <> + {capitalizeFirstLetter(factuur.status)}:{' '} + + {factuur.statusDescription} + + + ); + default: + return factuur.statusDescription; + } } function mapFactuur(factuur: AfisFactuur) { return { ...factuur, - statusDescription: getInvoiceStatusDescription(factuur), - factuurNummer: factuur.documentDownloadLink ? ( + statusDescription: getInvoiceStatusDescriptionFrontend(factuur), + factuurNummerEl: factuur.documentDownloadLink ? ( = - { - fullName: 'Debiteurnaam', - businessPartnerId: 'Debiteurnummer', - email: 'E-mailadres factuur', - phone: 'Telefoonnummer', +function useTransformFacturen( + facturenByStateApiResponse: ApiResponse +): ApiResponse { + const facturenByStateTransfomed: AfisFacturenByStateFrontend | null = + useMemo(() => { + if (facturenByStateApiResponse.content) { + return Object.fromEntries( + entries(facturenByStateApiResponse.content) + .filter(([state, facturenResponse]) => facturenResponse !== null) + .map(([state, facturenResponse]) => [ + state, + { + ...facturenResponse, + facturen: facturenResponse?.facturen?.map(mapFactuur) ?? [], + }, + ]) + ); + } + return null; + }, [facturenByStateApiResponse]); + + return Object.assign({}, facturenByStateApiResponse, { + content: facturenByStateTransfomed, + }); +} + +/** + * Uses /overview endpoint for Open and Overview facturen (All the Open facuren are loaded with this endpoint) + * Uses /facturen/(afgehandeld|overgedragen) for the facturen with this state. + */ +function useAfisFacturenApi( + businessPartnerIdEncrypted: + | AfisBusinessPartnerKnownResponse['businessPartnerIdEncrypted'] + | undefined, + state?: AfisFactuurState +) { + const isOpenfacturenState = !state || state === 'open'; + + const [facturenByStateApiResponse, fetchFacturen, isApiDataCached] = + useAppStateBagApi({ + bagThema: BagThemas.AFIS, + key: isOpenfacturenState + ? AFIS_OVERVIEW_STATE_KEY + : `afis-facturen-${state}`, + }); + + useEffect(() => { + if (businessPartnerIdEncrypted && !isApiDataCached) { + let url = `${BFFApiUrls.AFIS_FACTUREN}/${state}/${businessPartnerIdEncrypted}`; + if (isOpenfacturenState) { + url = `${BFFApiUrls.AFIS_FACTUREN_OVERZICHT}/${businessPartnerIdEncrypted}`; + } + fetchFacturen({ + url, + }); + } + }, [businessPartnerIdEncrypted, fetchFacturen, isApiDataCached, state]); + + const facturenByStateApiResponseUpdated = useTransformFacturen( + facturenByStateApiResponse + ); + + return [ + facturenByStateApiResponseUpdated, + fetchFacturen, + isApiDataCached, + ] as const; +} + +export function useAfisListPageData(state: AfisFactuurState) { + const { AFIS } = useAppStateGetter(); + const businessPartnerIdEncrypted = + AFIS.content?.businessPartnerIdEncrypted ?? null; + + const [facturenByStateApiResponse] = useAfisFacturenApi( + businessPartnerIdEncrypted, + state + ); + + return { + facturenListResponse: facturenByStateApiResponse.content?.[state] ?? null, + facturenTableConfig, + isThemaPaginaError: isError(AFIS, false), + isThemaPaginaLoading: isLoading(AFIS), + isListPageError: isError(facturenByStateApiResponse, false), + isListPageLoading: isLoading(facturenByStateApiResponse), + listPageTitle, + routes, + }; +} + +export function useAfisThemaData() { + const { AFIS } = useAppStateGetter(); + const businessPartnerIdEncrypted = + AFIS.content?.businessPartnerIdEncrypted ?? null; + + const [facturenByStateApiResponse] = useAfisFacturenApi( + businessPartnerIdEncrypted + ); + + return { + businessPartnerIdEncrypted, + facturenByState: facturenByStateApiResponse.content, + facturenTableConfig, + isThemaPaginaError: isError(AFIS, false), + isThemaPaginaLoading: isLoading(AFIS), + listPageTitle, + routes, + isOverviewApiError: isError(facturenByStateApiResponse), + isOverviewApiLoading: isLoading(facturenByStateApiResponse), + dependencyErrors: { + open: hasFailedDependency(facturenByStateApiResponse, 'open'), + afgehandeld: hasFailedDependency( + facturenByStateApiResponse, + 'afgehandeld' + ), + overgedragen: hasFailedDependency( + facturenByStateApiResponse, + 'overgedragen' + ), + }, }; +} export function useAfisBetaalVoorkeurenData( - businessPartnerIdEncrypted: AfisBusinessPartnerKnownResponse['businessPartnerIdEncrypted'] + businessPartnerIdEncrypted: + | AfisBusinessPartnerKnownResponse['businessPartnerIdEncrypted'] + | undefined ) { const [ - businesspartnerDetails, - businesspartnerDetailsApi, - fetchBusinessPartner, + businesspartnerDetailsApiResponse, + fetchBusinessPartnerDetails, + isApiDataCached, ] = useAppStateBagApi({ bagThema: BagThemas.AFIS, - key: `${businessPartnerIdEncrypted}-betaalvoorkeuren`, + key: `afis-betaalvoorkeuren`, }); useEffect(() => { - if (businessPartnerIdEncrypted) { - fetchBusinessPartner({ + if (businessPartnerIdEncrypted && !isApiDataCached) { + fetchBusinessPartnerDetails({ url: `${BFFApiUrls.AFIS_BUSINESSPARTNER}/${businessPartnerIdEncrypted}`, }); } - }, [businessPartnerIdEncrypted, fetchBusinessPartner]); + }, [ + businessPartnerIdEncrypted, + fetchBusinessPartnerDetails, + isApiDataCached, + ]); return { - businesspartnerDetails, + businesspartnerDetails: businesspartnerDetailsApiResponse.content, businessPartnerDetailsLabels, - isLoadingBusinessPartnerDetails: businesspartnerDetailsApi.isLoading, + isLoadingBusinessPartnerDetails: isLoading( + businesspartnerDetailsApiResponse + ), hasBusinessPartnerDetailsError: isError( - businesspartnerDetailsApi.data, + businesspartnerDetailsApiResponse, false ), hasEmandatesError: false, hasFailedEmailDependency: hasFailedDependency( - businesspartnerDetailsApi.data, + businesspartnerDetailsApiResponse, 'email' ), hasFailedPhoneDependency: hasFailedDependency( - businesspartnerDetailsApi.data, + businesspartnerDetailsApiResponse, 'phone' ), + hasFailedFullNameDependency: hasFailedDependency( + businesspartnerDetailsApiResponse, + 'fullName' + ), eMandateTableConfig, eMandates: [], isLoadingEmandates: false, }; } - -function getInvoiceStatusDescription(factuur: AfisFactuur): ReactNode { - switch (factuur.status) { - case 'openstaand': - const [label = factuur.statusDescription || '', linkText = ''] = - factuur.statusDescription?.split(':') || []; - return ( - <> - {label}:{' '} - - {linkText} - - - ); - default: - return factuur.statusDescription; - } -} diff --git a/src/client/pages/BezwarenDetail/BezwarenDetail.test.tsx b/src/client/pages/BezwarenDetail/BezwarenDetail.test.tsx index 78afd576e2..2e6fb2af7d 100644 --- a/src/client/pages/BezwarenDetail/BezwarenDetail.test.tsx +++ b/src/client/pages/BezwarenDetail/BezwarenDetail.test.tsx @@ -39,53 +39,55 @@ const testState = { BEZWAREN_BAG: { abc: null, 'asdasd98asd098asdjalmsndas-d9aps9dapsdja.sdasd': { - statussen: [ - { - uuid: 'b62fdaa9-f7ec-45d1-b23c-7f36fa00b393', - datum: '2023-03-29T10:00:00+02:00', - statustoelichting: 'Ontvangen', - }, - { - uuid: '50dcd25b-a826-422b-b51c-9049dda9600c', - datum: '2023-03-30T02:53:00+02:00', - statustoelichting: 'In behandeling', - }, - { - uuid: '00000000-0000-0000-0000-000000000000', - datum: '', - statustoelichting: 'Afgehandeld', - }, - ], - documents: [ - { - id: 'd0fe0ace-6f53-4342-b978-1cf12f8675be', - title: 'connector.docx', - datePublished: '30 augustus 2023', - url: 'http://localhost:5000/api/v1/services/bezwaren/d0fe0ace-6f53-4342-b978-1cf12f8675be/attachments', - dossiertype: 'Online Aangeleverd', - }, - { - id: '736ae47e-e703-4238-a664-100cde4c90a1', - title: 'Bezwaar_JB20230049.pdf', - datePublished: '30 augustus 2023', - url: 'http://localhost:5000/api/v1/services/bezwaren/736ae47e-e703-4238-a664-100cde4c90a1/attachments', - dossiertype: 'Online Aangeleverd', - }, - { - id: 'd0fe0ace-6f53-4342-b978-1cf12f8675be', - title: 'connector.docx', - datePublished: '30 augustus 2023', - url: 'http://localhost:5000/api/v1/services/bezwaren/d0fe0ace-6f53-4342-b978-1cf12f8675be/attachments', - dossiertype: 'Online Besluitvorming', - }, - { - id: '736ae47e-e703-4238-a664-100cde4c90a1', - title: 'Bezwaar_JB20230049.pdf', - datePublished: '30 augustus 2023', - url: 'http://localhost:5000/api/v1/services/bezwaren/736ae47e-e703-4238-a664-100cde4c90a1/attachments', - dossiertype: 'Online Procesdossier', - }, - ], + content: { + statussen: [ + { + uuid: 'b62fdaa9-f7ec-45d1-b23c-7f36fa00b393', + datum: '2023-03-29T10:00:00+02:00', + statustoelichting: 'Ontvangen', + }, + { + uuid: '50dcd25b-a826-422b-b51c-9049dda9600c', + datum: '2023-03-30T02:53:00+02:00', + statustoelichting: 'In behandeling', + }, + { + uuid: '00000000-0000-0000-0000-000000000000', + datum: '', + statustoelichting: 'Afgehandeld', + }, + ], + documents: [ + { + id: 'd0fe0ace-6f53-4342-b978-1cf12f8675be', + title: 'connector.docx', + datePublished: '30 augustus 2023', + url: 'http://localhost:5000/api/v1/services/bezwaren/d0fe0ace-6f53-4342-b978-1cf12f8675be/attachments', + dossiertype: 'Online Aangeleverd', + }, + { + id: '736ae47e-e703-4238-a664-100cde4c90a1', + title: 'Bezwaar_JB20230049.pdf', + datePublished: '30 augustus 2023', + url: 'http://localhost:5000/api/v1/services/bezwaren/736ae47e-e703-4238-a664-100cde4c90a1/attachments', + dossiertype: 'Online Aangeleverd', + }, + { + id: 'd0fe0ace-6f53-4342-b978-1cf12f8675be', + title: 'connector.docx', + datePublished: '30 augustus 2023', + url: 'http://localhost:5000/api/v1/services/bezwaren/d0fe0ace-6f53-4342-b978-1cf12f8675be/attachments', + dossiertype: 'Online Besluitvorming', + }, + { + id: '736ae47e-e703-4238-a664-100cde4c90a1', + title: 'Bezwaar_JB20230049.pdf', + datePublished: '30 augustus 2023', + url: 'http://localhost:5000/api/v1/services/bezwaren/736ae47e-e703-4238-a664-100cde4c90a1/attachments', + dossiertype: 'Online Procesdossier', + }, + ], + }, }, }, }; diff --git a/src/client/pages/BezwarenDetail/BezwarenDetail.tsx b/src/client/pages/BezwarenDetail/BezwarenDetail.tsx index 20a3d4c5e6..f8f7ee4558 100644 --- a/src/client/pages/BezwarenDetail/BezwarenDetail.tsx +++ b/src/client/pages/BezwarenDetail/BezwarenDetail.tsx @@ -39,12 +39,13 @@ interface BezwarenDetailPartialProps { } function BezwarenDetailPartial({ uuidEncrypted }: BezwarenDetailPartialProps) { - const [bezwaarDetail, api] = useAppStateBagApi({ - url: `${BFFApiUrls.BEZWAREN_DETAIL}/${uuidEncrypted}`, - bagThema: BagThemas.BEZWAREN, - key: uuidEncrypted, - }); - + const [bezwaarDetailApiResponse, api] = + useAppStateBagApi({ + url: `${BFFApiUrls.BEZWAREN_DETAIL}/${uuidEncrypted}`, + bagThema: BagThemas.BEZWAREN, + key: uuidEncrypted, + }); + const bezwaarDetail = bezwaarDetailApiResponse.content; const documents = bezwaarDetail?.documents ?? []; const statussen = bezwaarDetail?.statussen ?? []; @@ -85,7 +86,7 @@ function BezwarenDetailPartial({ uuidEncrypted }: BezwarenDetailPartialProps) { })} )} - {api.isLoading && ( + {isLoading(bezwaarDetailApiResponse) && ( <> Status diff --git a/src/client/pages/Erfpacht/DossierDetail/ErfpachtDossierDetail.tsx b/src/client/pages/Erfpacht/DossierDetail/ErfpachtDossierDetail.tsx index 2c933161d9..4fc0fa0c63 100644 --- a/src/client/pages/Erfpacht/DossierDetail/ErfpachtDossierDetail.tsx +++ b/src/client/pages/Erfpacht/DossierDetail/ErfpachtDossierDetail.tsx @@ -21,6 +21,7 @@ import { DatalistGeneral } from './DatalistGeneral'; import { DatalistJuridisch } from './DatalistJuridisch'; import { DatalistsFinancieel } from './DatalistsFinancieel'; import styles from './ErfpachtDossierDetail.module.scss'; +import { isError, isLoading } from '../../../../universal/helpers/api'; const loadingContentBarConfig: BarConfig = [ ['12rem', '2rem', '.5rem'], @@ -45,12 +46,13 @@ export default function ErfpachtDossierDetail() { dossierNummerUrlParam: string; }>(); const { ERFPACHTv2 } = useErfpachtV2Data(); - const [dossier, api] = useAppStateBagApi({ + const [dossierApiResponse] = useAppStateBagApi({ url: `${BFFApiUrls.ERFPACHTv2_DOSSIER_DETAILS}/${dossierNummerUrlParam}`, bagThema: BagThemas.ERFPACHTv2, key: dossierNummerUrlParam, }); - const noContent = !api.isLoading && !dossier; + const dossier = dossierApiResponse.content; + const noContent = !isLoading(dossierApiResponse) && !dossier; return ( @@ -65,10 +67,10 @@ export default function ErfpachtDossierDetail() { - {api.isLoading && ( + {isLoading(dossierApiResponse) && ( )} - {(api.isError || noContent) && ( + {(isError(dossierApiResponse) || noContent) && ( We kunnen op dit moment geen erfpachtdossier tonen. diff --git a/src/client/pages/Erfpacht/ErfpachtFacturen.tsx b/src/client/pages/Erfpacht/ErfpachtFacturen.tsx index 5236689f26..47e28865e1 100644 --- a/src/client/pages/Erfpacht/ErfpachtFacturen.tsx +++ b/src/client/pages/Erfpacht/ErfpachtFacturen.tsx @@ -8,6 +8,7 @@ import { BFFApiUrls } from '../../config/api'; import { BagThemas } from '../../config/thema'; import { useAppStateBagApi } from '../../hooks/useAppState'; import { useErfpachtV2Data } from './erfpachtData.hook'; +import { isError, isLoading } from '../../../universal/helpers/api'; export default function ErfpachtFacturen() { const { displayPropsAlleFacturen } = useErfpachtV2Data(); @@ -16,11 +17,15 @@ export default function ErfpachtFacturen() { dossierNummerUrlParam: string; }>(); - const [dossier, api] = useAppStateBagApi({ - url: `${BFFApiUrls.ERFPACHTv2_DOSSIER_DETAILS}/${dossierNummerUrlParam}`, - bagThema: BagThemas.ERFPACHTv2, - key: dossierNummerUrlParam, - }); + const [dossierApiResponse, api] = useAppStateBagApi( + { + url: `${BFFApiUrls.ERFPACHTv2_DOSSIER_DETAILS}/${dossierNummerUrlParam}`, + bagThema: BagThemas.ERFPACHTv2, + key: dossierNummerUrlParam, + } + ); + + const dossier = dossierApiResponse.content; return ( ); } diff --git a/src/client/pages/Parkeren/__snapshots__/Parkeren.test.tsx.snap b/src/client/pages/Parkeren/__snapshots__/Parkeren.test.tsx.snap index b5a5555227..3f44519749 100644 --- a/src/client/pages/Parkeren/__snapshots__/Parkeren.test.tsx.snap +++ b/src/client/pages/Parkeren/__snapshots__/Parkeren.test.tsx.snap @@ -140,17 +140,17 @@ exports[`Parkeren > should display the list of parkeervergunningen 1`] = `
    +

    + Lopende aanvragen +

    - @@ -232,17 +232,17 @@ exports[`Parkeren > should display the list of parkeervergunningen 1`] = `
    +

    + Huidige vergunningen en ontheffingen +

    - Lopende aanvragen -
    - @@ -257,17 +257,17 @@ exports[`Parkeren > should display the list of parkeervergunningen 1`] = `
    +

    + Eerdere en niet verleende vergunningen en ontheffingen +

    - Huidige vergunningen en ontheffingen -
    - diff --git a/src/client/pages/ThemaPagina/ThemaPaginaTable.tsx b/src/client/pages/ThemaPagina/ThemaPaginaTable.tsx index e916dab6a8..683e6bc405 100644 --- a/src/client/pages/ThemaPagina/ThemaPaginaTable.tsx +++ b/src/client/pages/ThemaPagina/ThemaPaginaTable.tsx @@ -9,25 +9,32 @@ interface ThemaPaginaTableProps { displayProps?: DisplayProps; listPageRoute?: string; maxItems?: number | -1; + totalItems?: number; textNoContent?: string; + subTitle?: string; title?: string; + listPageLinkLabel?: string; zaken: T[]; } export default function ThemaPaginaTable({ title = 'Zaken', + subTitle = '', zaken, className, textNoContent = 'U heeft (nog) geen zaken.', displayProps, listPageRoute, maxItems = MAX_TABLE_ROWS_ON_THEMA_PAGINA, + totalItems, + listPageLinkLabel = 'Toon meer', }: ThemaPaginaTableProps) { return ( ({ {!zaken.length && {textNoContent}} - {!!listPageRoute && ( - + {!!listPageRoute && maxItems !== -1 && ( + )} ); diff --git a/src/client/pages/VergunningenV2/VergunningDetail.tsx b/src/client/pages/VergunningenV2/VergunningDetail.tsx index e770b45506..43c2a8feb0 100644 --- a/src/client/pages/VergunningenV2/VergunningDetail.tsx +++ b/src/client/pages/VergunningenV2/VergunningDetail.tsx @@ -5,7 +5,7 @@ import { VergunningFrontendV2, } from '../../../server/services/vergunningen-v2/config-and-types'; import { AppRoutes } from '../../../universal/config/routes'; -import { isLoading } from '../../../universal/helpers/api'; +import { isError, isLoading } from '../../../universal/helpers/api'; import { CaseTypeV2 } from '../../../universal/types/vergunningen'; import { ThemaIcon } from '../../components'; import { Datalist } from '../../components/Datalist/Datalist'; @@ -54,7 +54,7 @@ export default function VergunningV2Detail() { const { id } = useParams<{ id: VergunningFrontendV2['id'] }>(); const vergunning = VERGUNNINGENv2.content?.find((item) => item.id === id); const fetchUrl = vergunning?.fetchUrl ?? ''; - const [vergunningDetailResponseContent, api] = useAppStateBagApi<{ + const [vergunningDetailApiResponse] = useAppStateBagApi<{ vergunning: VergunningFrontendV2 | null; documents: VergunningDocument[]; }>({ @@ -62,7 +62,7 @@ export default function VergunningV2Detail() { bagThema: BagThemas.VERGUNNINGEN, key: id, }); - + const vergunningDetailResponseContent = vergunningDetailApiResponse.content; const vergunningDetail = vergunningDetailResponseContent?.vergunning ?? null; const vergunningDocuments = vergunningDetailResponseContent?.documents ?? []; @@ -70,8 +70,10 @@ export default function VergunningV2Detail() { title={vergunningDetail?.title ?? 'Vergunning'} zaak={vergunningDetail} - isError={api.isError} - isLoading={api.isLoading || isLoading(VERGUNNINGENv2)} + isError={isError(vergunningDetailApiResponse)} + isLoading={ + isLoading(vergunningDetailApiResponse) || isLoading(VERGUNNINGENv2) + } icon={} pageContentTop={ vergunningDetail && ( diff --git a/src/server/config/source-api.ts b/src/server/config/source-api.ts index c74fa46a6f..e578dea60e 100644 --- a/src/server/config/source-api.ts +++ b/src/server/config/source-api.ts @@ -36,8 +36,8 @@ export interface DataRequestConfig extends AxiosRequestConfig { request?: (requestConfig: DataRequestConfig) => Promise>; } -export const DEFAULT_API_CACHE_TTL_MS = (IS_OT ? 65 : 45) * ONE_SECOND_MS; // This means that every request that depends on the response of another will use the cached version of the response for a maximum of 45 seconds. -export const DEFAULT_CANCEL_TIMEOUT_MS = (IS_OT ? 60 : 20) * ONE_SECOND_MS; // This means a request will be aborted after 20 seconds without a response. +export const DEFAULT_API_CACHE_TTL_MS = 45 * ONE_SECOND_MS; // This means that every request that depends on the response of another will use the cached version of the response for a maximum of 45 seconds. +export const DEFAULT_CANCEL_TIMEOUT_MS = 30 * ONE_SECOND_MS; // This means a request will be aborted after 20 seconds without a response. export const DEFAULT_REQUEST_CONFIG: DataRequestConfig = { cancelTimeout: DEFAULT_CANCEL_TIMEOUT_MS, diff --git a/src/server/routing/bff-routes.ts b/src/server/routing/bff-routes.ts index e3a15f3a4f..ba95f6829e 100644 --- a/src/server/routing/bff-routes.ts +++ b/src/server/routing/bff-routes.ts @@ -24,7 +24,7 @@ export const BffEndpoints = { AFIS_BUSINESSPARTNER: '/services/afis/businesspartner/:businessPartnerIdEncrypted', AFIS_FACTUREN: - '/services/afis/facturen/:state(open|closed)/:businessPartnerIdEncrypted', + '/services/afis/facturen/:state(open|afgehandeld|overgedragen)/:businessPartnerIdEncrypted', AFIS_FACTUREN_OVERZICHT: '/services/afis/facturen/overzicht/:businessPartnerIdEncrypted', AFIS_DOCUMENT_DOWNLOAD: '/services/afis/facturen/document/:id', diff --git a/src/server/routing/router-development.ts b/src/server/routing/router-development.ts index 99d46ba62d..50fc919b97 100644 --- a/src/server/routing/router-development.ts +++ b/src/server/routing/router-development.ts @@ -27,6 +27,7 @@ import { addToBlackList } from '../services/session-blacklist'; import { countLoggedInVisit } from '../services/visitors'; import { DevelopmentRoutes, PREDEFINED_REDIRECT_URLS } from './bff-routes'; import { sendUnauthorized } from './route-helpers'; +import { ONE_SECOND_MS } from '../config/app'; export const authRouterDevelopment = express.Router(); authRouterDevelopment.BFF_ID = 'router-dev'; @@ -78,7 +79,7 @@ authRouterDevelopment.get( async (req: Request, res: Response, next: NextFunction) => { const appSessionCookieOptions: CookieOptions = { expires: new Date( - new Date().getTime() + OIDC_SESSION_MAX_AGE_SECONDS * 1000 * 2000 + new Date().getTime() + OIDC_SESSION_MAX_AGE_SECONDS * ONE_SECOND_MS ), httpOnly: true, path: '/', diff --git a/src/server/routing/router-protected.ts b/src/server/routing/router-protected.ts index d4a7c4f33b..0b49bdc88d 100644 --- a/src/server/routing/router-protected.ts +++ b/src/server/routing/router-protected.ts @@ -6,7 +6,7 @@ import { fetchVergunningenDocument, fetchVergunningenDocumentsList, } from '../services'; -import { fetchAfisDocument } from '../services/afis/afis'; +import { fetchAfisDocument } from '../services/afis/afis-documents'; import { handleFetchAfisBusinessPartner, handleFetchAfisFacturen, diff --git a/src/server/services/afis/afis-business-partner.ts b/src/server/services/afis/afis-business-partner.ts new file mode 100644 index 0000000000..9d0104c642 --- /dev/null +++ b/src/server/services/afis/afis-business-partner.ts @@ -0,0 +1,213 @@ +import { + apiErrorResult, + ApiResponse, + apiSuccessResult, + getFailedDependencies, + getSettledResult, +} from '../../../universal/helpers/api'; +import { DataRequestConfig } from '../../config/source-api'; +import { getApiConfig } from '../../helpers/source-api-helpers'; +import { requestData } from '../../helpers/source-api-request'; +import { getFeedEntryProperties } from './afis-helpers'; +import { + AfisApiFeedResponseSource, + AfisBusinessPartnerAddressId, + AfisBusinessPartnerAddressSource, + AfisBusinessPartnerDetails, + AfisBusinessPartnerDetailsSource, + AfisBusinessPartnerDetailsTransformed, + AfisBusinessPartnerEmail, + AfisBusinessPartnerEmailSource, + AfisBusinessPartnerPhone, + AfisBusinessPartnerPhoneSource, +} from './afis-types'; + +function transformBusinessPartnerAddressResponse( + response: AfisApiFeedResponseSource +): string | null { + const [addressEntry] = getFeedEntryProperties(response); + + if (addressEntry) { + return addressEntry.AddressID; + } + + return null; +} + +async function fetchBusinessPartnerFullNameAddressId( + requestID: RequestID, + businessPartnerId: string +): Promise> { + const additionalConfig: DataRequestConfig = { + transformResponse: transformBusinessPartnerAddressResponse, + formatUrl(config) { + return `${config.url}/API/ZAPI_BUSINESS_PARTNER_DET_SRV/A_BusinessPartnerAddress?$filter=BusinessPartner eq '${businessPartnerId}'&$select=AddressID`; + }, + }; + + const businessPartnerRequestConfig = getApiConfig('AFIS', additionalConfig); + + return requestData( + businessPartnerRequestConfig, + requestID + ); +} + +function transformBusinessPartnerFullNameResponse( + response: AfisApiFeedResponseSource +) { + const [businessPartnerEntry] = getFeedEntryProperties(response); + + if (businessPartnerEntry) { + const transformedResponse: AfisBusinessPartnerDetails = { + fullName: businessPartnerEntry.BusinessPartnerFullName ?? null, + }; + + return transformedResponse; + } + + return null; +} + +async function fetchBusinessPartnerFullName( + requestID: RequestID, + businessPartnerId: string +) { + const additionalConfig: DataRequestConfig = { + transformResponse: transformBusinessPartnerFullNameResponse, + formatUrl(config) { + return `${config.url}/API/ZAPI_BUSINESS_PARTNER_DET_SRV/A_BusinessPartner?$filter=BusinessPartner eq '${businessPartnerId}'&$select=BusinessPartnerFullName`; + }, + }; + + const businessPartnerRequestConfig = getApiConfig('AFIS', additionalConfig); + + return requestData( + businessPartnerRequestConfig, + requestID + ); +} + +function transformPhoneResponse( + response: AfisApiFeedResponseSource +) { + const [phoneNumberEntry] = getFeedEntryProperties(response); + + const transformedResponse: AfisBusinessPartnerPhone = { + phone: phoneNumberEntry?.InternationalPhoneNumber ?? null, + }; + + return transformedResponse; +} + +async function fetchPhoneNumber( + requestID: RequestID, + addressId: AfisBusinessPartnerAddressId +) { + const additionalConfig: DataRequestConfig = { + transformResponse: transformPhoneResponse, + formatUrl(config) { + return `${config.url}/API/ZAPI_BUSINESS_PARTNER_DET_SRV/A_AddressPhoneNumber?$filter=AddressID eq '${addressId}'`; + }, + }; + + const businessPartnerRequestConfig = getApiConfig('AFIS', additionalConfig); + + return requestData( + businessPartnerRequestConfig, + requestID + ); +} + +function transformEmailResponse( + response: AfisApiFeedResponseSource +) { + const [emailAddressEntry] = getFeedEntryProperties(response); + + const transformedResponse: AfisBusinessPartnerEmail = { + email: emailAddressEntry?.SearchEmailAddress ?? null, + }; + + return transformedResponse; +} + +async function fetchEmail( + requestID: RequestID, + addressId: AfisBusinessPartnerAddressId +) { + const additionalConfig: DataRequestConfig = { + transformResponse: transformEmailResponse, + formatUrl(config) { + return `${config.url}/API/ZAPI_BUSINESS_PARTNER_DET_SRV/A_AddressEmailAddress?$filter=AddressID eq '${addressId}'`; + }, + }; + + const businessPartnerRequestConfig = getApiConfig('AFIS', additionalConfig); + + return requestData( + businessPartnerRequestConfig, + requestID + ); +} + +/** Fetches the business partner details, phonenumber and emailaddress from the AFIS source API and combines then into a single response */ +export async function fetchAfisBusinessPartnerDetails( + requestID: RequestID, + businessPartnerId: string +) { + const fullNameRequest = fetchBusinessPartnerFullName( + requestID, + businessPartnerId + ); + const addressIdRequest = fetchBusinessPartnerFullNameAddressId( + requestID, + businessPartnerId + ); + + const [fullNameResult, addressIdResult] = await Promise.allSettled([ + fullNameRequest, + addressIdRequest, + ]); + + const fullNameResponse = getSettledResult(fullNameResult); + const addressIdResponse = getSettledResult(addressIdResult); + + let phoneResponse: ApiResponse; + let emailResponse: ApiResponse; + + if (addressIdResponse.status === 'OK' && !!addressIdResponse.content) { + const phoneRequest = fetchPhoneNumber(requestID, addressIdResponse.content); + const emailRequest = fetchEmail(requestID, addressIdResponse.content); + + const [phoneResponseSettled, emailResponseSettled] = + await Promise.allSettled([phoneRequest, emailRequest]); + + phoneResponse = getSettledResult(phoneResponseSettled); + emailResponse = getSettledResult(emailResponseSettled); + } else { + phoneResponse = apiErrorResult( + 'Could not get phone, missing required query param addressId', + null + ); + emailResponse = apiErrorResult( + 'Could not get email, missing required query param addressId', + null + ); + } + + const detailsCombined: AfisBusinessPartnerDetailsTransformed = { + businessPartnerId, + ...fullNameResponse.content, + ...phoneResponse.content, + ...emailResponse.content, + }; + + return apiSuccessResult( + detailsCombined, + getFailedDependencies({ + email: emailResponse, + phone: phoneResponse, + fullName: fullNameResponse, + }) + ); +} diff --git a/src/server/services/afis/afis-documents.ts b/src/server/services/afis/afis-documents.ts new file mode 100644 index 0000000000..2b42b4e90b --- /dev/null +++ b/src/server/services/afis/afis-documents.ts @@ -0,0 +1,86 @@ +import { AuthProfileAndToken } from '../../auth/auth-types'; +import { getApiConfig } from '../../helpers/source-api-helpers'; +import { requestData } from '../../helpers/source-api-request'; +import { + DEFAULT_DOCUMENT_DOWNLOAD_MIME_TYPE, + DocumentDownloadData, + DocumentDownloadResponse, +} from '../shared/document-download-route-handler'; +import { getFeedEntryProperties } from './afis-helpers'; +import { + AfisArcDocID, + AfisDocumentDownloadSource, + AfisDocumentIDSource, + AfisFactuur, +} from './afis-types'; + +export async function fetchAfisDocument( + requestID: RequestID, + _authProfileAndToken: AuthProfileAndToken, + factuurDocumentId: AfisFactuur['factuurDocumentId'] +): Promise { + const ArchiveDocumentIDResponse = await fetchAfisDocumentID( + requestID, + factuurDocumentId + ); + if (ArchiveDocumentIDResponse.status !== 'OK') { + return ArchiveDocumentIDResponse; + } + + const config = getApiConfig('AFIS', { + formatUrl: ({ url }) => { + return `${url}/getDebtorInvoice/API_CV_ATTACHMENT_SRV/`; + }, + method: 'post', + data: { + Record: { + ArchiveDocumentID: ArchiveDocumentIDResponse.content, + BusinessObjectTypeName: 'BKPF', + }, + }, + transformResponse: ( + data: AfisDocumentDownloadSource + ): DocumentDownloadData => { + if (typeof data?.Record?.attachment !== 'string') { + throw new Error( + 'Afis document download - no valid response data provided' + ); + } + const decodedDocument = Buffer.from(data.Record.attachment, 'base64'); + return { + data: decodedDocument, + mimetype: DEFAULT_DOCUMENT_DOWNLOAD_MIME_TYPE, + filename: data.Record.attachmentname ?? 'factuur.pdf', + }; + }, + }); + + return requestData(config, requestID); +} + +/** Retrieve an ArcDocID from the AFIS source API. + * + * This ID uniquely identifies a document and can be used - + * to download one with our document downloading endpoint for example. + * + * There can be more then one ArcDocID's pointing to the same document. + */ +async function fetchAfisDocumentID( + requestID: RequestID, + factuurDocumentId: AfisFactuur['factuurDocumentId'] +) { + const config = getApiConfig('AFIS', { + formatUrl: ({ url }) => { + return `${url}/API/ZFI_OPERACCTGDOCITEM_CDS/ZFI_CDS_TOA02?$filter=AccountNumber eq '${factuurDocumentId}'&$select=ArcDocId`; + }, + transformResponse: (data: AfisDocumentIDSource) => { + const entryProperties = getFeedEntryProperties(data); + if (entryProperties.length) { + return entryProperties[0].ArcDocId; + } + return null; + }, + }); + + return requestData(config, requestID); +} diff --git a/src/server/services/afis/afis-facturen.ts b/src/server/services/afis/afis-facturen.ts new file mode 100644 index 0000000000..caec2b8648 --- /dev/null +++ b/src/server/services/afis/afis-facturen.ts @@ -0,0 +1,319 @@ +import { + ApiResponse, + apiSuccessResult, + getFailedDependencies, + getSettledResult, +} from '../../../universal/helpers/api'; +import { + defaultDateFormat, + isDateInPast, +} from '../../../universal/helpers/date'; +import displayAmount, { + capitalizeFirstLetter, +} from '../../../universal/helpers/text'; +import { encryptSessionIdWithRouteIdParam } from '../../helpers/encrypt-decrypt'; +import { getApiConfig } from '../../helpers/source-api-helpers'; +import { requestData } from '../../helpers/source-api-request'; +import { BffEndpoints } from '../../routing/bff-routes'; +import { generateFullApiUrlBFF } from '../../routing/route-helpers'; +import { captureMessage } from '../monitoring'; +import { getFeedEntryProperties } from './afis-helpers'; +import { + AfisFacturenByStateResponse, + AfisFacturenParams, + AfisFacturenResponse, + AfisFactuur, + AfisFactuurPropertiesSource, + AfisFactuurState, + AfisOpenInvoiceSource, +} from './afis-types'; + +const DEFAULT_PROFIT_CENTER_NAME = 'Gemeente Amsterdam'; + +export const FACTUUR_STATE_KEYS: AfisFactuurState[] = [ + 'open', + 'afgehandeld', + 'overgedragen', +]; + +export async function fetchAfisFacturen( + requestID: RequestID, + sessionID: SessionID, + params: AfisFacturenParams +): Promise> { + const config = getApiConfig('AFIS', { + formatUrl: ({ url }) => formatFactuurRequestURL(url, params), + transformResponse: (responseData) => + transformFacturen(responseData, sessionID), + }); + + return requestData(config, requestID); +} + +function formatFactuurRequestURL( + baseUrl: string | undefined, + params: AfisFacturenParams +): string { + const baseRoute = '/API/ZFI_OPERACCTGDOCITEM_CDS/ZFI_OPERACCTGDOCITEM'; + + const filters: Record = { + // Openstaaand (met betaallink of sepamandaat) + open: `$filter=Customer eq '${params.businessPartnerID}' and IsCleared eq false`, + // Afgehandeld (ge-incasseerd, betaald, geannuleerd) + afgehandeld: `$filter=Customer eq '${params.businessPartnerID}' and IsCleared eq true and (DunningLevel ne '3' or ReverseDocument ne '')`, + // Overgedragen aan belastingen + overgedragen: `$filter=Customer eq '${params.businessPartnerID}' and IsCleared eq true and DunningLevel eq '3'`, + }; + + const select = `$select=IsCleared,ReverseDocument,Paylink,PostingDate,ProfitCenterName,DocumentReferenceID,AccountingDocument,AmountInBalanceTransacCrcy,NetPaymentAmount,NetDueDate,DunningLevel,DunningBlockingReason,SEPAMandate,ClearingDate`; + const orderBy = '$orderBy=NetDueDate asc, PostingDate asc'; + + let query = `?$inlinecount=allpages&${filters[params.state]}&${select}&${orderBy}`; + + if (params.top) { + query += `&$top=${params.top}`; + } + + return `${baseUrl}${baseRoute}${query}`; +} + +export async function fetchAfisFacturenOverview( + requestID: RequestID, + sessionID: SessionID, + params: Omit +) { + const facturenOpenRequest = fetchAfisFacturen(requestID, sessionID, { + state: 'open', + businessPartnerID: params.businessPartnerID, + }); + + const facturenClosedRequest = fetchAfisFacturen(requestID, sessionID, { + state: 'afgehandeld', + businessPartnerID: params.businessPartnerID, + top: '3', + }); + + const facturenTransferredRequest = fetchAfisFacturen(requestID, sessionID, { + state: 'overgedragen', + businessPartnerID: params.businessPartnerID, + top: '3', + }); + + const [ + facturenOpenResponse, + facturenClosedResponse, + facturenTransferredResponse, + ] = await Promise.allSettled([ + facturenOpenRequest, + facturenClosedRequest, + facturenTransferredRequest, + ]); + + const facturenOpenResult = getSettledResult(facturenOpenResponse); + const facturenClosedResult = getSettledResult(facturenClosedResponse); + const facturenTransferredResult = getSettledResult( + facturenTransferredResponse + ); + + const facturenOverview: AfisFacturenByStateResponse = { + open: facturenOpenResult.content ?? null, + afgehandeld: facturenClosedResult.content ?? null, + overgedragen: facturenTransferredResult.content ?? null, + }; + + return apiSuccessResult( + facturenOverview, + getFailedDependencies({ + open: facturenOpenResult, + afgehandeld: facturenClosedResult, + overgedragen: facturenTransferredResult, + }) + ); +} + +export async function fetchAfisFacturenByState( + requestID: RequestID, + sessionID: SessionID, + params: AfisFacturenParams +) { + const facturenResponse = await fetchAfisFacturen( + requestID, + sessionID, + params + ); + if ((await facturenResponse.status) === 'OK') { + return apiSuccessResult({ + [params.state]: facturenResponse.content, + }); + } + return facturenResponse; +} + +function transformFacturen( + responseData: AfisOpenInvoiceSource, + sessionID: SessionID +): AfisFacturenResponse { + const feedProperties = getFeedEntryProperties(responseData); + const count = responseData?.feed?.count ?? feedProperties.length; + const facturenTransformed = feedProperties.map((invoiceProperties) => { + return transformFactuur(invoiceProperties, sessionID); + }); + return { + count, + facturen: facturenTransformed, + }; +} + +function transformFactuur( + sourceInvoice: XmlNullable, + sessionID: SessionID +): AfisFactuur { + const invoice = replaceXmlNulls(sourceInvoice); + const factuurDocumentId = invoice.AccountingDocument; + const factuurNummer = factuurDocumentId || invoice.DocumentReferenceID; // NOTE: This has to be verified with proper test data. + const factuurDocumentIdEncrypted = factuurDocumentId + ? encryptSessionIdWithRouteIdParam(sessionID, factuurDocumentId) + : null; + + const netPaymentAmountInCents = parseFloat(invoice.NetPaymentAmount) * 100; + const amountInBalanceTransacCrcyInCents = + parseFloat(invoice.AmountInBalanceTransacCrcy) * 100; + + const amountOwed = + (amountInBalanceTransacCrcyInCents + netPaymentAmountInCents) / 100; + const amountOwedFormatted = `€ ${amountOwed ? displayAmount(amountOwed) : 0}`; + + let debtClearingDate = null; + let debtClearingDateFormatted = null; + if (invoice.ClearingDate) { + debtClearingDate = invoice.ClearingDate; + debtClearingDateFormatted = defaultDateFormat(debtClearingDate); + } + + const status = determineFactuurStatus( + invoice, + amountInBalanceTransacCrcyInCents + ); + + return { + id: factuurDocumentId, + afzender: invoice.ProfitCenterName || DEFAULT_PROFIT_CENTER_NAME, + datePublished: invoice.PostingDate || null, + datePublishedFormatted: defaultDateFormat(invoice.PostingDate) || null, + paymentDueDate: invoice.NetDueDate, + paymentDueDateFormatted: defaultDateFormat(invoice.NetDueDate), + debtClearingDate, + debtClearingDateFormatted, + amountOwed: amountOwed ? amountOwed : 0, + amountOwedFormatted, + factuurNummer, + factuurDocumentId, + status, + statusDescription: determineFactuurStatusDescription( + status, + amountOwedFormatted, + debtClearingDateFormatted + ), + paylink: invoice.Paylink ? invoice.Paylink : null, + documentDownloadLink: factuurDocumentIdEncrypted + ? generateFullApiUrlBFF(BffEndpoints.AFIS_DOCUMENT_DOWNLOAD, { + id: factuurDocumentIdEncrypted, + }) + : null, + }; +} + +type XmlNullable> = { + [key in keyof T]: { '@null': true } | T[key]; +}; + +/** Replace all values that is an XML Null value with just the value `null`. */ +function replaceXmlNulls( + sourceInvoice: XmlNullable +): AfisFactuurPropertiesSource { + const withoutXmlNullable = Object.entries(sourceInvoice).map(([key, val]) => { + if (typeof val === 'object' && val !== null && val['@null']) { + return [key, null]; + } + return [key, val]; + }); + return Object.fromEntries(withoutXmlNullable); +} + +function determineFactuurStatus( + sourceInvoice: AfisFactuurPropertiesSource, + amountInBalanceTransacCrcyInCents: number +): AfisFactuur['status'] { + switch (true) { + // Closed invoices + case !!sourceInvoice.ReverseDocument: + return 'geannuleerd'; + + case sourceInvoice.IsCleared && sourceInvoice.DunningLevel === 3: + return 'overgedragen-aan-belastingen'; + + case sourceInvoice.IsCleared && sourceInvoice.DunningLevel === 0: + return 'betaald'; + + // Open invoices + case amountInBalanceTransacCrcyInCents < 0: + return 'geld-terug'; + + case !!sourceInvoice.NetDueDate && + isDateInPast(sourceInvoice.NetDueDate) && + (sourceInvoice.DunningLevel == 1 || sourceInvoice.DunningLevel == 2): + return 'herinnering'; + + case sourceInvoice.DunningBlockingReason === 'D': + return 'in-dispuut'; + + case sourceInvoice.DunningBlockingReason === 'BA': + return 'gedeeltelijke-betaling'; + + case !!sourceInvoice.SEPAMandate: + return 'automatische-incasso'; + + case !sourceInvoice.IsCleared && sourceInvoice.DunningLevel === 0: + return 'openstaand'; + + default: + captureMessage( + `Error: invoice status 'onbekend' (unknown) +Source Invoice Properties that determine this are: +\tReverseDocument: ${sourceInvoice.ReverseDocument} +\tIsCleared: ${sourceInvoice.IsCleared} +\tDunningLevel: ${sourceInvoice.DunningLevel} +\tDunningBlockingReason: ${sourceInvoice.DunningBlockingReason}`, + { severity: 'error' } + ); + // Unknown status + return 'onbekend'; + } +} + +function determineFactuurStatusDescription( + status: AfisFactuur['status'], + amountOwedFormatted: AfisFactuur['amountOwedFormatted'], + debtClearingDateFormatted: AfisFactuur['debtClearingDateFormatted'] +) { + switch (status) { + case 'openstaand': + return `${amountOwedFormatted} betaal nu`; + case 'herinnering': + return 'Betaaltermijn verstreken: betaal via de herinneringsbrief'; + case 'in-dispuut': + return 'In dispuut'; + case 'gedeeltelijke-betaling': + return `Automatische incasso - Betaal het openstaande bedrag van ${amountOwedFormatted} via bankoverschrijving`; + case 'geld-terug': + return `U krijgt ${amountOwedFormatted.replace('-', '')} terug`; + case 'betaald': + return `Betaald ${debtClearingDateFormatted ? `op ${debtClearingDateFormatted}` : ''}`; + case 'automatische-incasso': + return `${amountOwedFormatted} wordt automatisch van uw rekening afgeschreven`; + case 'overgedragen-aan-belastingen': + return `Overgedragen aan belastingen ${debtClearingDateFormatted ? `op ${debtClearingDateFormatted}` : ''}`; + default: + return capitalizeFirstLetter(status ?? ''); + } +} diff --git a/src/server/services/afis/afis-route-handlers.ts b/src/server/services/afis/afis-route-handlers.ts index da8127921f..2d46046e4c 100644 --- a/src/server/services/afis/afis-route-handlers.ts +++ b/src/server/services/afis/afis-route-handlers.ts @@ -1,12 +1,17 @@ import { Request, Response } from 'express'; import { getAuth } from '../../auth/auth-helpers'; -import { sendResponse, sendUnauthorized } from '../../routing/route-helpers'; +import { + sendBadRequest, + sendResponse, + sendUnauthorized, +} from '../../routing/route-helpers'; import { decryptEncryptedRouteParamAndValidateSessionID } from '../shared/decrypt-route-param'; +import { fetchAfisBusinessPartnerDetails } from './afis-business-partner'; import { - fetchAfisBusinessPartnerDetails, - fetchAfisFacturen, + FACTUUR_STATE_KEYS, + fetchAfisFacturenByState, fetchAfisFacturenOverview, -} from './afis'; +} from './afis-facturen'; import { AfisFactuurState } from './afis-types'; export async function handleFetchAfisBusinessPartner( @@ -53,6 +58,10 @@ export async function handleFetchAfisFacturen( req: Request<{ businessPartnerIdEncrypted: string; state: AfisFactuurState }>, res: Response ) { + if (!FACTUUR_STATE_KEYS.includes(req.params.state)) { + return sendBadRequest(res, 'Unknown state param provided'); + } + const authProfileAndToken = getAuth(req); if (!authProfileAndToken) { @@ -74,11 +83,12 @@ export async function handleFetchAfisFacturen( top = undefined; } - const response = await fetchAfisFacturen( + const response = await fetchAfisFacturenByState( res.locals.requestID, authProfileAndToken.profile.sid, { state: req.params.state, businessPartnerID, top } ); + return sendResponse(res, response); } diff --git a/src/server/services/afis/afis-types.ts b/src/server/services/afis/afis-types.ts index 8173817513..3e56cd61a0 100644 --- a/src/server/services/afis/afis-types.ts +++ b/src/server/services/afis/afis-types.ts @@ -1,5 +1,3 @@ -import { ZaakDetail } from '../../../universal/types'; - type JaOfNee = 'Ja' | 'Nee'; /** Business partner private response from external AFIS API. @@ -42,6 +40,7 @@ export type AfisBusinessPartnerKnownResponse = { export type AfisApiFeedResponseSource = { feed?: { + count?: number; entry?: [ { content?: { @@ -52,19 +51,13 @@ export type AfisApiFeedResponseSource = { }; }; +export type AfisBusinessPartnerAddressSource = { + AddressID: string; +}; + export type AfisBusinessPartnerDetailsSource = { BusinessPartner: number; - FullName: string; - AddressID: number; - CityName: string; - Country: string; - HouseNumber: number; - HouseNumberSupplementText: string; - PostalCode: string; - Region: string; - StreetName: string; - StreetPrefixName: string; - StreetSuffixName: string; + BusinessPartnerFullName: string; }; export type AfisBusinessPartnerPhoneSource = { @@ -76,11 +69,11 @@ export type AfisBusinessPartnerEmailSource = { }; export type AfisBusinessPartnerDetails = { - businessPartnerId: string; fullName: string; - addressId: number; }; +export type AfisBusinessPartnerAddressId = number | null; + export type AfisBusinessPartnerPhone = { phone: string | null; }; @@ -89,13 +82,32 @@ export type AfisBusinessPartnerEmail = { email: string | null; }; -export type AfisBusinessPartnerDetailsTransformed = AfisBusinessPartnerDetails & - AfisBusinessPartnerPhone & - AfisBusinessPartnerEmail; +export type AfisBusinessPartnerDetailsTransformed = { + businessPartnerId: string; + email?: string | null; + fullName?: string | null; + phone?: string | null; +}; -export type AfisFactuurState = 'open' | 'closed'; +export type AfisFactuurState = 'open' | 'afgehandeld' | 'overgedragen'; + +export type AfisFacturenResponse = { + count: number; + facturen: AfisFactuur[]; +}; + +export type AfisFacturenByStateResponse = { + [key in AfisFactuurState]?: AfisFacturenResponse | null; +}; + +export type AfisFacturenParams = { + state: AfisFactuurState; + businessPartnerID: string; + top?: string; +}; export type AfisFactuur = { + id: string; afzender: string; datePublished: string | null; datePublishedFormatted: string | null; @@ -106,20 +118,23 @@ export type AfisFactuur = { amountOwed: number; amountOwedFormatted: string; factuurNummer: string; + factuurDocumentId: string; status: AfisFactuurStatus; paylink: string | null; - documentDownloadLink: string; + documentDownloadLink: string | null; statusDescription: string; -} & Omit; +}; type AfisFactuurStatus = | 'openstaand' | 'automatische-incasso' | 'in-dispuut' | 'gedeeltelijke-betaling' + | 'overgedragen-aan-belastingen' | 'geld-terug' | 'betaald' | 'geannuleerd' + | 'herinnering' | 'onbekend'; export type AfisOpenInvoiceSource = @@ -136,20 +151,21 @@ export type AfisOpenInvoiceSource = * `IsCleared`: `true` means the 'factuur' is fully payed for. */ export type AfisFactuurPropertiesSource = { - DunningLevel: number; - DunningBlockingReason: string; - ProfitCenterName: string; - SEPAMandate: string; - PostingDate: string; + AccountingDocument: string; AccountingDocumentType: string; + AmountInBalanceTransacCrcy: string; + ClearingDate?: string; + DocumentReferenceID: string; + DunningBlockingReason: string; + DunningLevel: number; + IsCleared?: boolean; NetDueDate: string; NetPaymentAmount: string; - AmountInBalanceTransacCrcy: string; - InvoiceNo: string; Paylink: string | null; - IsCleared?: boolean; - ClearingDate?: string; + PostingDate: string; + ProfitCenterName: string; ReverseDocument?: string; + SEPAMandate: string; }; export type AfisArcDocID = AfisDocumentIDPropertiesSource['ArcDocId']; diff --git a/src/server/services/afis/afis.test.ts b/src/server/services/afis/afis.test.ts index a4809d9362..dca7b10437 100644 --- a/src/server/services/afis/afis.test.ts +++ b/src/server/services/afis/afis.test.ts @@ -1,5 +1,5 @@ import { describe } from 'vitest'; -import { remoteApi, getAuthProfileAndToken } from '../../../test-utils'; +import { getAuthProfileAndToken, remoteApi } from '../../../test-utils'; const mocks = vi.hoisted(() => { const MOCK_VALUE_ENCRYPTED = 'xx-encrypted-xx'; @@ -22,32 +22,53 @@ vi.mock('../../../server/helpers/encrypt-decrypt', async (importOriginal) => { }; }); -import { - fetchAfisBusinessPartnerDetails, - fetchAfisFacturen, - fetchAfisDocument, - fetchIsKnownInAFIS, -} from './afis'; import { jsonCopy } from '../../../universal/helpers/utils'; -import { AfisFactuur } from './afis-types'; -import { ApiSuccessResponse } from '../../../universal/helpers/api'; +import { fetchIsKnownInAFIS } from './afis'; +import { fetchAfisBusinessPartnerDetails } from './afis-business-partner'; +import { fetchAfisDocument } from './afis-documents'; +import { fetchAfisFacturen } from './afis-facturen'; +import { AfisFacturenParams } from './afis-types'; const FACTUUR_NUMMER = '12346789'; const GENERIC_ID = '12346789'; const ADRRESS_ID = 430844; - const BASE_ROUTE = '/afis/RESTAdapter'; -const FACTUREN_ROUTE = `${BASE_ROUTE}/API/ZFI_OPERACCTGDOCITEM_CDS/ZFI_OPERACCTGDOCITEM`; + const ROUTES = { businesspartnerBSN: `${BASE_ROUTE}/businesspartner/BSN/`, businesspartnerKVK: `${BASE_ROUTE}/businesspartner/KVK/`, - businesspartnerDetails: `${BASE_ROUTE}/API/ZAPI_BUSINESS_PARTNER_DET_SRV/A_BusinessPartner?$filter=BusinessPartner%20eq%20%27${GENERIC_ID}%27&$select=BusinessPartner,%20FullName,%20AddressID,%20CityName,%20Country,%20HouseNumber,%20HouseNumberSupplementText,%20PostalCode,%20Region,%20StreetName,%20StreetPrefixName,%20StreetSuffixName`, - businesspartnerPhonenumber: `${BASE_ROUTE}/API/ZAPI_BUSINESS_PARTNER_DET_SRV/A_AddressPhoneNumber?$filter=AddressID%20eq%20%27${ADRRESS_ID}%27`, - businesspartnerEmailAddress: `${BASE_ROUTE}/API/ZAPI_BUSINESS_PARTNER_DET_SRV/A_AddressEmailAddress?$filter=AddressID%20eq%20%27${ADRRESS_ID}%27`, - openstaandeFacturen: `${FACTUREN_ROUTE}?$inlinecount=allpages&$filter=Customer eq '${GENERIC_ID}' and IsCleared eq false and (DunningLevel eq '0' or DunningBlockingReason eq 'D')&$select=IsCleared,ReverseDocument,Paylink,PostingDate,ProfitCenterName,InvoiceNo,AmountInBalanceTransacCrcy,NetPaymentAmount,NetDueDate,DunningLevel,DunningBlockingReason,SEPAMandate&$orderBy=NetDueDate asc, PostingDate asc`, - geslotenFacturen: `${FACTUREN_ROUTE}?$inlinecount=allpages&$filter=Customer eq '${GENERIC_ID}' and IsCleared eq true and (DunningLevel eq '0' or ReverseDocument ne '')&$select=IsCleared,ReverseDocument,Paylink,PostingDate,ProfitCenterName,InvoiceNo,AmountInBalanceTransacCrcy,NetPaymentAmount,NetDueDate,DunningLevel,DunningBlockingReason,SEPAMandate&$orderBy=NetDueDate asc, PostingDate asc`, - documentDownload: `${BASE_ROUTE}/getDebtorInvoice/API_CV_ATTACHMENT_SRV/`, - documentID: `${BASE_ROUTE}/API/ZFI_OPERACCTGDOCITEM_CDS/ZFI_CDS_TOA02?$filter=AccountNumber eq '${FACTUUR_NUMMER}'&$select=ArcDocId`, + businesspartnerFullName: (uri: string) => { + return decodeURI(uri).includes( + 'ZAPI_BUSINESS_PARTNER_DET_SRV/A_BusinessPartner' + ); + }, + businesspartnerAddressId: (uri: string) => { + return decodeURI(uri).includes( + 'ZAPI_BUSINESS_PARTNER_DET_SRV/A_BusinessPartnerAddress' + ); + }, + businesspartnerPhonenumber: (uri: string) => { + return decodeURI(uri).includes( + `ZAPI_BUSINESS_PARTNER_DET_SRV/A_AddressPhoneNumber` + ); + }, + businesspartnerEmailAddress: (uri: string) => { + return decodeURI(uri).includes( + `ZAPI_BUSINESS_PARTNER_DET_SRV/A_AddressEmailAddress` + ); + }, + openstaandeFacturen: (uri: string) => { + return decodeURI(uri).includes(`IsCleared eq false`); + }, + geslotenFacturen: (uri: string) => { + return decodeURI(uri).includes('and IsCleared eq true'); + }, + documentDownload: (uri: string) => { + return decodeURI(uri).includes('API_CV_ATTACHMENT_SRV'); + }, + documentID: (uri: string) => { + return decodeURI(uri).includes('ZFI_CDS_TOA02'); + }, }; const REQUEST_ID = '456'; @@ -256,26 +277,30 @@ describe('Afis', () => { }); }); - describe('fetchAfisBusinessPartner ', () => { - const responseBodyBusinessDetails = { + describe('fetchAfisBusinessPartnerDetails ', () => { + const responseBodyBusinessPartnerAddressId = { feed: { entry: [ { content: { '@type': 'application/xml', properties: { - BusinessPartner: GENERIC_ID, - FullName: 'Taxon Expeditions BV', AddressID: 430844, - CityName: 'Leiden', - Country: 'NL', - HouseNumber: 20, - HouseNumberSupplementText: '', - PostalCode: '2311 VW', - Region: '', - StreetName: 'Rembrandtstraat', - StreetPrefixName: '', - StreetSuffixName: '', + }, + }, + }, + ], + }, + }; + + const responseBodyBusinessPartnerFullName = { + feed: { + entry: [ + { + content: { + '@type': 'application/xml', + properties: { + BusinessPartnerFullName: 'Taxon Expeditions BV', }, }, }, @@ -315,8 +340,12 @@ describe('Afis', () => { it('fetches and transforms business partner details correctly', async () => { remoteApi - .get(ROUTES.businesspartnerDetails) - .reply(200, responseBodyBusinessDetails); + .get(ROUTES.businesspartnerFullName) + .reply(200, responseBodyBusinessPartnerFullName); + + remoteApi + .get(ROUTES.businesspartnerAddressId) + .reply(200, responseBodyBusinessPartnerAddressId); remoteApi .get(ROUTES.businesspartnerPhonenumber) @@ -334,7 +363,6 @@ describe('Afis', () => { expect(response).toMatchInlineSnapshot(` { "content": { - "addressId": 430844, "businessPartnerId": "12346789", "email": "xxmail@arjanappel.nl", "fullName": "Taxon Expeditions BV", @@ -345,17 +373,10 @@ describe('Afis', () => { `); }); - it('returns just the business partner details when there is no AddressID', async () => { - const responseBodyBusinessDetailsWithoutAddressID = jsonCopy( - responseBodyBusinessDetails - ); - - delete responseBodyBusinessDetailsWithoutAddressID.feed.entry[0].content - .properties.AddressID; - + it('returns just the business partner fullname when there is no AddressID', async () => { remoteApi - .get(ROUTES.businesspartnerDetails) - .reply(200, responseBodyBusinessDetailsWithoutAddressID); + .get(ROUTES.businesspartnerFullName) + .reply(200, responseBodyBusinessPartnerFullName); const response = await fetchAfisBusinessPartnerDetails( REQUEST_ID, @@ -365,10 +386,21 @@ describe('Afis', () => { expect(response).toMatchInlineSnapshot(` { "content": { - "addressId": null, "businessPartnerId": "12346789", "fullName": "Taxon Expeditions BV", }, + "failedDependencies": { + "email": { + "content": null, + "message": "Could not get email, missing required query param addressId", + "status": "ERROR", + }, + "phone": { + "content": null, + "message": "Could not get phone, missing required query param addressId", + "status": "ERROR", + }, + }, "status": "OK", } `); @@ -376,48 +408,39 @@ describe('Afis', () => { it('returns a partial error when there is an error fetching the phone number or email address', async () => { remoteApi - .get(ROUTES.businesspartnerDetails) - .reply(200, responseBodyBusinessDetails); - - remoteApi - .get(ROUTES.businesspartnerPhonenumber) - .replyWithError('error retrieving doc'); - - remoteApi - .get(ROUTES.businesspartnerEmailAddress) - .replyWithError('error retrieving doc'); + .get(ROUTES.businesspartnerFullName) + .reply(200, responseBodyBusinessPartnerAddressId); let response = await fetchAfisBusinessPartnerDetails( REQUEST_ID, GENERIC_ID ); - expect(response).toMatchObject({ - content: { - businessPartnerId: GENERIC_ID, - fullName: 'Taxon Expeditions BV', - addressId: ADRRESS_ID, - }, - status: 'OK', - failedDependencies: { - phone: { - content: null, - message: 'error retrieving doc', - status: 'ERROR', + expect(response).toMatchInlineSnapshot(` + { + "content": { + "businessPartnerId": "12346789", + "fullName": null, }, - email: { - content: null, - message: 'error retrieving doc', - status: 'ERROR', + "failedDependencies": { + "email": { + "content": null, + "message": "Could not get email, missing required query param addressId", + "status": "ERROR", + }, + "phone": { + "content": null, + "message": "Could not get phone, missing required query param addressId", + "status": "ERROR", + }, }, - }, - }); + "status": "OK", + } + `); }); it('returns an error when there is an error fetching the business partner details', async () => { - remoteApi - .get(ROUTES.businesspartnerDetails) - .replyWithError('error retrieving doc'); + remoteApi.get(ROUTES.businesspartnerFullName).reply(500); const response = await fetchAfisBusinessPartnerDetails( REQUEST_ID, @@ -426,16 +449,35 @@ describe('Afis', () => { expect(response).toMatchInlineSnapshot(` { - "content": null, - "message": "error retrieving doc", - "status": "ERROR", + "content": { + "businessPartnerId": "12346789", + }, + "failedDependencies": { + "email": { + "content": null, + "message": "Could not get email, missing required query param addressId", + "status": "ERROR", + }, + "fullName": { + "code": 500, + "content": null, + "message": "Request failed with status code 500", + "status": "ERROR", + }, + "phone": { + "content": null, + "message": "Could not get phone, missing required query param addressId", + "status": "ERROR", + }, + }, + "status": "OK", } `); }); }); - it('returns null properties when the business partner details data quality is not sufficient', async () => { - remoteApi.get(ROUTES.businesspartnerDetails).reply(200, { + it('Omits email and phone properties when the business partner details data quality is not sufficient', async () => { + remoteApi.get(ROUTES.businesspartnerFullName).reply(200, { feed: { entry: [ { @@ -455,16 +497,27 @@ describe('Afis', () => { expect(response).toMatchInlineSnapshot(` { "content": { - "addressId": null, - "businessPartnerId": "", + "businessPartnerId": "12346789", "fullName": null, }, + "failedDependencies": { + "email": { + "content": null, + "message": "Could not get email, missing required query param addressId", + "status": "ERROR", + }, + "phone": { + "content": null, + "message": "Could not get phone, missing required query param addressId", + "status": "ERROR", + }, + }, "status": "OK", } `); // also test when the response is an array - remoteApi.get(ROUTES.businesspartnerDetails).reply(200, { + remoteApi.get(ROUTES.businesspartnerFullName).reply(200, { feed: { entry: [ { @@ -484,10 +537,21 @@ describe('Afis', () => { expect(response2).toMatchInlineSnapshot(` { "content": { - "addressId": null, - "businessPartnerId": "", + "businessPartnerId": "12346789", "fullName": null, }, + "failedDependencies": { + "email": { + "content": null, + "message": "Could not get email, missing required query param addressId", + "status": "ERROR", + }, + "phone": { + "content": null, + "message": "Could not get phone, missing required query param addressId", + "status": "ERROR", + }, + }, "status": "OK", } `); @@ -501,53 +565,55 @@ describe('Afis', () => { .get(ROUTES.openstaandeFacturen) .reply(200, require('./test-fixtures/openstaande-facturen.json')); - const openParams = { - state: 'open' as 'open', + const openParams: AfisFacturenParams = { + state: 'open', businessPartnerID: GENERIC_ID, - top: undefined, }; - const response = (await fetchAfisFacturen( + const response = await fetchAfisFacturen( REQUEST_ID, authProfileAndToken.profile.sid, openParams - )) as ApiSuccessResponse; + ); // All fields are listed here to test correct formatting. - const openFactuur = response.content[0]; - expect(openFactuur).toStrictEqual({ - afzender: 'Bedrijf: Ok', - amountOwed: 343, - amountOwedFormatted: '€ 343,00', - datePublished: '2023-11-21T00:00:00', - datePublishedFormatted: '21 november 2023', - debtClearingDate: null, - debtClearingDateFormatted: null, - documentDownloadLink: - 'http://bff-api-host/api/v1/services/afis/facturen/document/xx-encrypted-xx', - factuurNummer: '5555555', - paylink: 'http://localhost:3100/mocks-server/afis/paylink', - paymentDueDate: '2023-12-21T00:00:00', - paymentDueDateFormatted: '21 december 2023', - status: 'in-dispuut', - statusDescription: 'In dispuut', - }); + const openFactuur = response.content?.facturen[0]; + expect(openFactuur).toMatchInlineSnapshot(` + { + "afzender": "Bedrijf: Ok", + "amountOwed": 343, + "amountOwedFormatted": "€ 343,00", + "datePublished": "2023-11-21T00:00:00", + "datePublishedFormatted": "21 november 2023", + "debtClearingDate": null, + "debtClearingDateFormatted": null, + "documentDownloadLink": "http://bff-api-host/api/v1/services/afis/facturen/document/xx-encrypted-xx", + "factuurDocumentId": "5555555", + "factuurNummer": "5555555", + "id": "5555555", + "paylink": "http://localhost:3100/mocks-server/afis/paylink", + "paymentDueDate": "2023-12-21T00:00:00", + "paymentDueDateFormatted": "21 december 2023", + "status": "in-dispuut", + "statusDescription": "In dispuut", + } + `); - const automatischeIncassoFactuur = response.content[1]; - expect(automatischeIncassoFactuur.status).toBe('openstaand'); - expect(automatischeIncassoFactuur.paymentDueDate).toBe( + const automatischeIncassoFactuur = response.content?.facturen[1]; + expect(automatischeIncassoFactuur?.status).toBe('openstaand'); + expect(automatischeIncassoFactuur?.paymentDueDate).toBe( '2023-12-12T00:00:00' ); - const inDispuutInvoice = response.content[2]; - expect(inDispuutInvoice.status).toBe('automatische-incasso'); + const inDispuutInvoice = response.content?.facturen[2]; + expect(inDispuutInvoice?.status).toBe('automatische-incasso'); - const geldTerugInvoice = response.content[3]; - expect(geldTerugInvoice.status).toBe('geld-terug'); - expect(geldTerugInvoice.statusDescription.includes('-')).toBe(false); + const geldTerugInvoice = response.content?.facturen[3]; + expect(geldTerugInvoice?.status).toBe('geld-terug'); + expect(geldTerugInvoice?.statusDescription.includes('-')).toBe(false); - const unknownStatusInvoice = response.content[4]; - expect(unknownStatusInvoice.status).toBe('onbekend'); + const unknownStatusInvoice = response.content?.facturen[4]; + expect(unknownStatusInvoice?.status).toBe('onbekend'); }); test('Afgehandelde factuur data is transformed and url is correctly formatted', async () => { @@ -555,42 +621,45 @@ describe('Afis', () => { .get(ROUTES.geslotenFacturen) .reply(200, require('./test-fixtures/afgehandelde-facturen.json')); - const closedParams = { - state: 'closed' as 'closed', + const closedParams: AfisFacturenParams = { + state: 'afgehandeld', businessPartnerID: GENERIC_ID, top: undefined, }; - const response = (await fetchAfisFacturen( + const response = await fetchAfisFacturen( REQUEST_ID, authProfileAndToken.profile.sid, closedParams - )) as ApiSuccessResponse; - - const geannuleerdeInvoice = response.content[0]; - expect(geannuleerdeInvoice).toStrictEqual({ - afzender: 'Lisan al Gaib inc.', - amountOwed: 0, - amountOwedFormatted: '€ 0', - datePublished: null, - datePublishedFormatted: null, - debtClearingDate: null, - debtClearingDateFormatted: null, - documentDownloadLink: - 'http://bff-api-host/api/v1/services/afis/facturen/document/xx-encrypted-xx', - factuurNummer: 'INV-2023-010', - paylink: null, - paymentDueDate: '2023-12-21T00:00:00', - paymentDueDateFormatted: '21 december 2023', - status: 'geannuleerd', - statusDescription: 'Geannuleerd', - }); + ); + + const geannuleerdeInvoice = response.content?.facturen[0]; + expect(geannuleerdeInvoice).toMatchInlineSnapshot(` + { + "afzender": "Lisan al Gaib inc.", + "amountOwed": 0, + "amountOwedFormatted": "€ 0", + "datePublished": null, + "datePublishedFormatted": null, + "debtClearingDate": null, + "debtClearingDateFormatted": null, + "documentDownloadLink": "http://bff-api-host/api/v1/services/afis/facturen/document/xx-encrypted-xx", + "factuurDocumentId": "INV-2023-010", + "factuurNummer": "INV-2023-010", + "id": "INV-2023-010", + "paylink": null, + "paymentDueDate": "2023-12-21T00:00:00", + "paymentDueDateFormatted": "21 december 2023", + "status": "geannuleerd", + "statusDescription": "Geannuleerd", + } + `); - const betaaldeInvoice = response.content[1]; - expect(betaaldeInvoice.status).toStrictEqual('geannuleerd'); + const betaaldeInvoice = response.content?.facturen[1]; + expect(betaaldeInvoice?.status).toStrictEqual('geannuleerd'); - const unknownStatusInvoice = response.content[2]; - expect(unknownStatusInvoice.status).toStrictEqual('geannuleerd'); + const unknownStatusInvoice = response.content?.facturen[2]; + expect(unknownStatusInvoice?.status).toStrictEqual('geannuleerd'); }); }); diff --git a/src/server/services/afis/afis.ts b/src/server/services/afis/afis.ts index 4d69c13870..2078ffcd1a 100644 --- a/src/server/services/afis/afis.ts +++ b/src/server/services/afis/afis.ts @@ -1,47 +1,12 @@ -import { - apiSuccessResult, - getFailedDependencies, - getSettledResult, -} from '../../../universal/helpers/api'; -import { defaultDateFormat } from '../../../universal/helpers/date'; -import displayAmount, { - capitalizeFirstLetter, -} from '../../../universal/helpers/text'; import { AuthProfileAndToken } from '../../auth/auth-types'; import { DataRequestConfig } from '../../config/source-api'; -import { - encrypt, - encryptSessionIdWithRouteIdParam, -} from '../../helpers/encrypt-decrypt'; +import { encryptSessionIdWithRouteIdParam } from '../../helpers/encrypt-decrypt'; import { getApiConfig } from '../../helpers/source-api-helpers'; import { requestData } from '../../helpers/source-api-request'; -import { BffEndpoints } from '../../routing/bff-routes'; -import { generateFullApiUrlBFF } from '../../routing/route-helpers'; -import { captureMessage } from '../monitoring'; -import { - DEFAULT_DOCUMENT_DOWNLOAD_MIME_TYPE, - DocumentDownloadData, - DocumentDownloadResponse, -} from '../shared/document-download-route-handler'; -import { getFeedEntryProperties } from './afis-helpers'; import { - AfisApiFeedResponseSource, - AfisArcDocID, AfisBusinessPartnerCommercialResponseSource, - AfisBusinessPartnerDetails, - AfisBusinessPartnerDetailsSource, - AfisBusinessPartnerEmail, - AfisBusinessPartnerEmailSource, AfisBusinessPartnerKnownResponse, - AfisBusinessPartnerPhone, - AfisBusinessPartnerPhoneSource, AfisBusinessPartnerPrivateResponseSource, - AfisDocumentDownloadSource, - AfisDocumentIDSource, - AfisFactuur, - AfisFactuurPropertiesSource, - AfisFactuurState, - AfisOpenInvoiceSource, } from './afis-types'; /** Returns if the person logging in, is known in the AFIS source API */ @@ -122,439 +87,3 @@ function transformBusinessPartnerisKnownResponse( businessPartnerIdEncrypted, }; } - -async function fetchBusinessPartner( - requestID: RequestID, - businessPartnerId: string -) { - const additionalConfig: DataRequestConfig = { - transformResponse: transformBusinessPartnerDetailsResponse, - formatUrl(config) { - return `${config.url}/API/ZAPI_BUSINESS_PARTNER_DET_SRV/A_BusinessPartner?$filter=BusinessPartner eq '${businessPartnerId}'&$select=BusinessPartner, FullName, AddressID, CityName, Country, HouseNumber, HouseNumberSupplementText, PostalCode, Region, StreetName, StreetPrefixName, StreetSuffixName`; - }, - }; - - const businessPartnerRequestConfig = getApiConfig('AFIS', additionalConfig); - - return requestData( - businessPartnerRequestConfig, - requestID - ); -} - -function transformBusinessPartnerDetailsResponse( - response: AfisApiFeedResponseSource -) { - const [businessPartnerEntry] = getFeedEntryProperties(response); - - if (businessPartnerEntry) { - const transformedResponse: AfisBusinessPartnerDetails = { - businessPartnerId: businessPartnerEntry.BusinessPartner?.toString() ?? '', - fullName: businessPartnerEntry.FullName ?? null, - addressId: businessPartnerEntry.AddressID ?? null, - }; - - return transformedResponse; - } - - return null; -} - -async function fetchPhoneNumber( - requestID: RequestID, - addressId: AfisBusinessPartnerDetails['addressId'] -) { - const additionalConfig: DataRequestConfig = { - transformResponse: transformPhoneResponse, - formatUrl(config) { - return `${config.url}/API/ZAPI_BUSINESS_PARTNER_DET_SRV/A_AddressPhoneNumber?$filter=AddressID eq '${addressId}'`; - }, - }; - - const businessPartnerRequestConfig = getApiConfig('AFIS', additionalConfig); - - return requestData( - businessPartnerRequestConfig, - requestID - ); -} - -function transformPhoneResponse( - response: AfisApiFeedResponseSource -) { - const [phoneNumberEntry] = getFeedEntryProperties(response); - - const transformedResponse: AfisBusinessPartnerPhone = { - phone: phoneNumberEntry?.InternationalPhoneNumber ?? null, - }; - - return transformedResponse; -} - -async function fetchEmail( - requestID: RequestID, - addressId: AfisBusinessPartnerDetails['addressId'] -) { - const additionalConfig: DataRequestConfig = { - transformResponse: transformEmailResponse, - formatUrl(config) { - return `${config.url}/API/ZAPI_BUSINESS_PARTNER_DET_SRV/A_AddressEmailAddress?$filter=AddressID eq '${addressId}'`; - }, - }; - - const businessPartnerRequestConfig = getApiConfig('AFIS', additionalConfig); - - return requestData( - businessPartnerRequestConfig, - requestID - ); -} - -function transformEmailResponse( - response: AfisApiFeedResponseSource -) { - const [emailAddressEntry] = getFeedEntryProperties(response); - - const transformedResponse: AfisBusinessPartnerEmail = { - email: emailAddressEntry?.SearchEmailAddress ?? null, - }; - - return transformedResponse; -} - -/** Fetches the business partner details, phonenumber and emailaddress from the AFIS source API and combines then into a single response */ -export async function fetchAfisBusinessPartnerDetails( - requestID: RequestID, - businessPartnerId: string -) { - const detailsResponse = await fetchBusinessPartner( - requestID, - businessPartnerId - ); - - if (detailsResponse.status === 'OK' && detailsResponse.content?.addressId) { - const phoneRequest = fetchPhoneNumber( - requestID, - detailsResponse.content.addressId - ); - const emailRequest = fetchEmail( - requestID, - detailsResponse.content.addressId - ); - - const [phoneResponseSettled, emailResponseSettled] = - await Promise.allSettled([phoneRequest, emailRequest]); - - const phoneResponse = getSettledResult(phoneResponseSettled); - const emailResponse = getSettledResult(emailResponseSettled); - - const detailsCombined: AfisBusinessPartnerDetails = { - ...detailsResponse.content, - ...phoneResponse.content, - ...emailResponse.content, - }; - - return apiSuccessResult( - detailsCombined, - getFailedDependencies({ email: emailResponse, phone: phoneResponse }) - ); - } - - // Returns error response or (partial) success response without phone/email. - return detailsResponse; -} - -type AfisFacturenParams = { - state: AfisFactuurState; - businessPartnerID: string; - top?: string; -}; - -export async function fetchAfisFacturen( - requestID: RequestID, - sessionID: SessionID, - params: AfisFacturenParams -) { - const config = getApiConfig('AFIS', { - formatUrl: ({ url }) => formatFactuurRequestURL(url, params), - transformResponse: (responseData) => - transformFacturen(responseData, sessionID), - }); - - const response = await requestData(config, requestID); - return response; -} - -function formatFactuurRequestURL( - baseUrl: string | undefined, - params: AfisFacturenParams -): string { - const baseRoute = '/API/ZFI_OPERACCTGDOCITEM_CDS/ZFI_OPERACCTGDOCITEM'; - - const filters: Record = { - open: `$filter=Customer eq '${params.businessPartnerID}' and IsCleared eq false and (DunningLevel eq '0' or DunningBlockingReason eq 'D')`, - closed: `$filter=Customer eq '${params.businessPartnerID}' and IsCleared eq true and (DunningLevel eq '0' or ReverseDocument ne '')`, - }; - - const select = `$select=IsCleared,ReverseDocument,Paylink,PostingDate,ProfitCenterName,InvoiceNo,AmountInBalanceTransacCrcy,NetPaymentAmount,NetDueDate,DunningLevel,DunningBlockingReason,SEPAMandate`; - const orderBy = '$orderBy=NetDueDate asc, PostingDate asc'; - - let query = `?$inlinecount=allpages&${filters[params.state]}&${select}&${orderBy}`; - - if (params.top) { - query += `&$top=${params.top}`; - } - - return `${baseUrl}${baseRoute}${query}`; -} - -export async function fetchAfisFacturenOverview( - requestID: RequestID, - sessionID: SessionID, - params: Omit -) { - const facturenOpenRequest = fetchAfisFacturen(requestID, sessionID, { - state: 'open', - businessPartnerID: params.businessPartnerID, - }); - - const facturenClosedRequest = fetchAfisFacturen(requestID, sessionID, { - state: 'closed', - businessPartnerID: params.businessPartnerID, - top: '3', - }); - - const [facturenOpenResponse, facturenClosedResponse] = - await Promise.allSettled([facturenOpenRequest, facturenClosedRequest]); - - const facturenOpenResult = getSettledResult(facturenOpenResponse); - const facturenClosedResult = getSettledResult(facturenClosedResponse); - - const facturenOverview = { - open: facturenOpenResult.content ?? [], - closed: facturenClosedResult.content ?? [], - }; - - return apiSuccessResult( - facturenOverview, - getFailedDependencies({ - open: facturenOpenResult, - closed: facturenClosedResult, - }) - ); -} - -function transformFacturen( - responseData: AfisOpenInvoiceSource, - sessionID: SessionID -) { - const feedProperties = getFeedEntryProperties(responseData); - return feedProperties.map((invoiceProperties) => { - return transformFactuur(invoiceProperties, sessionID); - }); -} - -function transformFactuur( - sourceInvoice: XmlNullable, - sessionID: SessionID -): AfisFactuur { - const invoice = replaceXmlNulls(sourceInvoice); - - const factuurNummerEncrypted = encryptSessionIdWithRouteIdParam( - sessionID, - invoice.InvoiceNo - ); - - const netPaymentAmountInCents = parseFloat(invoice.NetPaymentAmount) * 100; - const amountInBalanceTransacCrcyInCents = - parseFloat(invoice.AmountInBalanceTransacCrcy) * 100; - - const amountOwed = - (amountInBalanceTransacCrcyInCents + netPaymentAmountInCents) / 100; - const amountOwedFormatted = `€ ${amountOwed ? displayAmount(amountOwed) : 0}`; - - let debtClearingDate = null; - let debtClearingDateFormatted = null; - if (invoice.ClearingDate) { - debtClearingDate = invoice.ClearingDate; - debtClearingDateFormatted = defaultDateFormat(debtClearingDate); - } - - const status = determineFactuurStatus( - invoice, - amountInBalanceTransacCrcyInCents - ); - - return { - afzender: invoice.ProfitCenterName, - datePublished: invoice.PostingDate || null, - datePublishedFormatted: defaultDateFormat(invoice.PostingDate) || null, - paymentDueDate: invoice.NetDueDate, - paymentDueDateFormatted: defaultDateFormat(invoice.NetDueDate), - debtClearingDate, - debtClearingDateFormatted, - amountOwed: amountOwed ? amountOwed : 0, - amountOwedFormatted, - factuurNummer: invoice.InvoiceNo, - status, - statusDescription: determineFactuurStatusDescription( - status, - amountOwedFormatted, - debtClearingDateFormatted - ), - paylink: invoice.Paylink ? invoice.Paylink : null, - documentDownloadLink: generateFullApiUrlBFF( - BffEndpoints.AFIS_DOCUMENT_DOWNLOAD, - { id: factuurNummerEncrypted } - ), - }; -} - -type XmlNullable> = { - [key in keyof T]: { '@null': true } | T[key]; -}; - -/** Replace all values that is an XML Null value with just the value `null`. */ -function replaceXmlNulls( - sourceInvoice: XmlNullable -): AfisFactuurPropertiesSource { - const withoutXmlNullable = Object.entries(sourceInvoice).map(([key, val]) => { - if (typeof val === 'object' && val !== null && val['@null']) { - return [key, null]; - } - return [key, val]; - }); - return Object.fromEntries(withoutXmlNullable); -} - -function determineFactuurStatus( - sourceInvoice: AfisFactuurPropertiesSource, - amountInBalanceTransacCrcyInCents: number -): AfisFactuur['status'] { - switch (true) { - // Closed invoices - case !!sourceInvoice.ReverseDocument: - return 'geannuleerd'; - - case sourceInvoice.IsCleared && sourceInvoice.DunningLevel === 0: - return 'betaald'; - - // Open invoices - case amountInBalanceTransacCrcyInCents < 0: { - return 'geld-terug'; - } - - case sourceInvoice.DunningBlockingReason === 'D': - return 'in-dispuut'; - - case sourceInvoice.DunningBlockingReason === 'BA': - return 'gedeeltelijke-betaling'; - - case !!sourceInvoice.SEPAMandate: - return 'automatische-incasso'; - - case !sourceInvoice.IsCleared && sourceInvoice.DunningLevel === 0: - return 'openstaand'; - - default: - captureMessage( - `Error: invoice status 'onbekend' (unknown) -Source Invoice Properties that determine this are: -\tReverseDocument: ${sourceInvoice.ReverseDocument} -\tIsCleared: ${sourceInvoice.IsCleared} -\tDunningLevel: ${sourceInvoice.DunningLevel} -\tDunningBlockingReason: ${sourceInvoice.DunningBlockingReason}`, - { severity: 'error' } - ); - // Unknown status - return 'onbekend'; - } -} - -function determineFactuurStatusDescription( - status: AfisFactuur['status'], - amountOwedFormatted: AfisFactuur['amountOwedFormatted'], - debtClearingDateFormatted: AfisFactuur['debtClearingDateFormatted'] -) { - switch (status) { - case 'openstaand': - return `${amountOwedFormatted} betaal nu`; - case 'in-dispuut': - return 'In dispuut'; - case 'gedeeltelijke-betaling': - return `Automatische incasso - Betaal het openstaande bedrag van ${amountOwedFormatted} via bankoverschrijving`; - case 'geld-terug': - return `U krijgt ${amountOwedFormatted.replace('-', '')} terug`; - case 'betaald': - return `Betaald ${debtClearingDateFormatted ? `op ${debtClearingDateFormatted}` : ''}`; - case 'automatische-incasso': - return `${amountOwedFormatted} wordt automatisch van uw rekening afgeschreven`; - default: - return capitalizeFirstLetter(status ?? ''); - } -} - -export async function fetchAfisDocument( - requestID: RequestID, - _authProfileAndToken: AuthProfileAndToken, - factuurNummer: AfisFactuur['factuurNummer'] -): Promise { - const ArchiveDocumentIDResponse = await fetchAfisDocumentID( - requestID, - factuurNummer - ); - if (ArchiveDocumentIDResponse.status !== 'OK') { - return ArchiveDocumentIDResponse; - } - - const config = getApiConfig('AFIS', { - formatUrl: ({ url }) => { - return `${url}/getDebtorInvoice/API_CV_ATTACHMENT_SRV/`; - }, - method: 'post', - data: { - Record: { - ArchiveDocumentID: ArchiveDocumentIDResponse.id, - BusinessObjectTypeName: 'BKPF', - }, - }, - transformResponse: ( - data: AfisDocumentDownloadSource - ): DocumentDownloadData => { - const decodedDocument = Buffer.from(data.Record.attachment, 'base64'); - return { - data: decodedDocument, - mimetype: DEFAULT_DOCUMENT_DOWNLOAD_MIME_TYPE, - filename: data.Record.attachmentname ?? 'factuur.pdf', - }; - }, - }); - - return requestData(config, requestID); -} - -/** Retrieve an ArcDocID from the AFIS source API. - * - * This ID uniquely identifies a document and can be used - - * to download one with our document downloading endpoint for example. - * - * There can be more then one ArcDocID's pointing to the same document. - */ -async function fetchAfisDocumentID( - requestID: RequestID, - factuurNummer: AfisFactuur['factuurNummer'] -) { - const config = getApiConfig('AFIS', { - formatUrl: ({ url }) => - `${url}/API/ZFI_OPERACCTGDOCITEM_CDS/ZFI_CDS_TOA02?$filter=AccountNumber eq '${factuurNummer}'&$select=ArcDocId`, - transformResponse: (data: AfisDocumentIDSource) => { - const entryProperties = getFeedEntryProperties(data); - if (entryProperties.length) { - return entryProperties[0].ArcDocId; - } - return null; - }, - }); - - return requestData(config, requestID); -} diff --git a/src/server/services/afis/test-fixtures/afgehandelde-facturen.json b/src/server/services/afis/test-fixtures/afgehandelde-facturen.json index f78a828202..2e54a0477d 100644 --- a/src/server/services/afis/test-fixtures/afgehandelde-facturen.json +++ b/src/server/services/afis/test-fixtures/afgehandelde-facturen.json @@ -25,7 +25,8 @@ "ProfitCenterName": "Lisan al Gaib inc.", "NetDueDate": "2023-12-21T00:00:00", "ReverseDocument": "someReverseDocument123", - "InvoiceNo": "INV-2023-010" + "DocumentReferenceID": "INV-2023-010", + "AccountingDocument": "INV-2023-010" } } }, @@ -38,7 +39,8 @@ "ProfitCenterName": "Lisan al Gaib inc.", "NetDueDate": "2023-12-12T00:00:00", "ReverseDocument": "someReverseDocument123", - "InvoiceNo": "INV-2023-008" + "DocumentReferenceID": "INV-2023-008", + "AccountingDocument": "INV-2023-008" } } }, @@ -51,7 +53,8 @@ "ProfitCenterName": "Lisan al Gaib inc.", "NetDueDate": "2023-10-26T00:00:00", "ReverseDocument": "someReverseDocument123", - "InvoiceNo": "INV-2023-006" + "DocumentReferenceID": "INV-2023-006", + "AccountingDocument": "INV-2023-006" } } } diff --git a/src/server/services/afis/test-fixtures/openstaande-facturen.json b/src/server/services/afis/test-fixtures/openstaande-facturen.json index bd95c5e2ce..7593632be6 100644 --- a/src/server/services/afis/test-fixtures/openstaande-facturen.json +++ b/src/server/services/afis/test-fixtures/openstaande-facturen.json @@ -28,7 +28,8 @@ "NetDueDate": "2023-12-21T00:00:00", "NetPaymentAmount": "0.00", "AmountInBalanceTransacCrcy": "343.00", - "InvoiceNo": "5555555", + "DocumentReferenceID": "5555555", + "AccountingDocument": "5555555", "Paylink": "http://localhost:3100/mocks-server/afis/paylink" } } @@ -45,7 +46,8 @@ "NetDueDate": "2023-12-12T00:00:00", "NetPaymentAmount": "0.00", "AmountInBalanceTransacCrcy": "343.00", - "InvoiceNo": "5555555", + "DocumentReferenceID": "5555555", + "AccountingDocument": "5555555", "Paylink": "http://localhost:3100/mocks-server/afis/paylink" } } @@ -62,7 +64,8 @@ "NetDueDate": "2023-10-26T00:00:00", "NetPaymentAmount": "0.00", "AmountInBalanceTransacCrcy": "5.50", - "InvoiceNo": "5555555", + "DocumentReferenceID": "5555555", + "AccountingDocument": "5555555", "Paylink": "http://localhost:3100/mocks-server/afis/paylink" } } @@ -79,7 +82,8 @@ "NetDueDate": "2023-08-09T00:00:00", "NetPaymentAmount": "0.00", "AmountInBalanceTransacCrcy": "-16.00", - "InvoiceNo": "5555555", + "DocumentReferenceID": "5555555", + "AccountingDocument": "5555555", "Paylink": "http://localhost:3100/mocks-server/afis/paylink" } } @@ -96,7 +100,8 @@ "NetDueDate": "2023-03-23T00:00:00", "NetPaymentAmount": "-1000.98", "AmountInBalanceTransacCrcy": "1000.00", - "InvoiceNo": "5555555", + "DocumentReferenceID": "5555555", + "AccountingDocument": "5555555", "Paylink": "http://localhost:3100/mocks-server/afis/paylink" } } diff --git a/src/server/services/db/config.ts b/src/server/services/db/config.ts index 2199ab7bf0..3c642a9d87 100644 --- a/src/server/services/db/config.ts +++ b/src/server/services/db/config.ts @@ -7,7 +7,7 @@ export const IS_DB_ENABLED = FeatureToggle.dbEnabled; export const IS_VERBOSE = IS_OT && APP_MODE !== 'unittest'; export const tableNameLoginCount = - getFromEnv('BFF_LOGIN_COUNT_TABLE') || 'visitor_count'; + getFromEnv('BFF_LOGIN_COUNT_TABLE', false) || 'visitor_count'; export const tableNameSessionBlacklist = - getFromEnv('BFF_LOGIN_SESSION_BLACKLIST_TABLE') || 'session_blacklist'; + getFromEnv('BFF_LOGIN_SESSION_BLACKLIST_TABLE', false) || 'session_blacklist'; diff --git a/src/server/services/visitors.ts b/src/server/services/visitors.ts index f9e7ec2e37..b67ed2c569 100644 --- a/src/server/services/visitors.ts +++ b/src/server/services/visitors.ts @@ -97,7 +97,7 @@ export async function countLoggedInVisit( } export async function loginStats(req: Request, res: Response) { - if (!IS_TAP && !process.env.BFF_LOGIN_COUNT_TABLE) { + if (!IS_TAP && !tableNameLoginCount) { return res.send( 'Supply database credentials and enable your Datapunt VPN to use this view locally.' ); @@ -147,7 +147,7 @@ export async function loginStats(req: Request, res: Response) { dateEnd: sub(startOfMonth(today), { days: 1 }), }, { - label: 'Dit kwartaal', + label: 'Dit kwartaal tot nu', dateStart: startOfQuarter(today), dateEnd: todayEnd, }, diff --git a/src/setupTests.ts b/src/setupTests.ts index 54c44cee5e..9c20594cd4 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -147,3 +147,4 @@ process.env.REACT_APP_SSO_URL_PARKEREN = `${remoteApiHost}/sso/portaal/parkeren` process.env.BFF_AMSAPP_ADMINISTRATIENUMMER_DELIVERY_ENDPOINT = `${remoteApiHost}/amsapp/session/credentials`; process.env.BFF_AMSAPP_NONCE = '123456789123456789123456'; +process.env.BFF_DEBUG_RESPONSE_DATA = 'afis'; diff --git a/src/universal/config/auth.development.ts b/src/universal/config/auth.development.ts index b532bf77d7..2b2ea20469 100644 --- a/src/universal/config/auth.development.ts +++ b/src/universal/config/auth.development.ts @@ -1,13 +1,13 @@ import { getFromEnv } from '../../server/helpers/env'; export const DEV_USER_ID_DEFAULT = - getFromEnv('MA_PROFILE_DEV_ID') || 'I.M Mokum'; + getFromEnv('MA_PROFILE_DEV_ID', false) || 'I.M Mokum'; // accounts in a string: foo=1234,bar=8098 const accountsDigid = - getFromEnv('MA_TEST_ACCOUNTS') || `dev=${DEV_USER_ID_DEFAULT}`; + getFromEnv('MA_TEST_ACCOUNTS', false) || `dev=${DEV_USER_ID_DEFAULT}`; const accountsEherkenning = - getFromEnv('MA_TEST_ACCOUNTS_EH') || `dev=${DEV_USER_ID_DEFAULT}`; + getFromEnv('MA_TEST_ACCOUNTS_EH', false) || `dev=${DEV_USER_ID_DEFAULT}`; function splitUsersIntoRecord(accounts: string) { return accounts.split(',').reduce( diff --git a/src/universal/config/feature-toggles.ts b/src/universal/config/feature-toggles.ts index 10ea16e2ea..a7a5350423 100644 --- a/src/universal/config/feature-toggles.ts +++ b/src/universal/config/feature-toggles.ts @@ -2,6 +2,7 @@ import { IS_AP, IS_DEVELOPMENT, IS_OT, IS_PRODUCTION, IS_TAP } from './env'; export const FeatureToggle = { afisActive: !IS_PRODUCTION, + afisEmandatesActive: false, avgActive: true, bbDocumentDownloadsActive: IS_OT, bekendmakingenDatasetActive: false, @@ -12,7 +13,7 @@ export const FeatureToggle = { cmsFooterActive: true, dbEnabled: IS_TAP, dbSessionsEnabled: IS_TAP, - decosServiceActive: IS_OT, + decosServiceActive: IS_DEVELOPMENT, // TODO: Enable when working on MIJN-8914 ehKetenmachtigingActive: !IS_PRODUCTION, eherkenningActive: true, erfpachtV2Active: !IS_PRODUCTION, diff --git a/src/universal/config/routes.ts b/src/universal/config/routes.ts index b1532b0896..0d49b1ce63 100644 --- a/src/universal/config/routes.ts +++ b/src/universal/config/routes.ts @@ -21,7 +21,7 @@ export const AppRoutes = { INKOMEN: '/inkomen', AFIS: '/afis', 'AFIS/BETAALVOORKEUREN': '/afis/betaalvoorkeuren', - 'AFIS/FACTUREN': '/afis/facturen/:kind/:page?', + 'AFIS/FACTUREN': '/afis/facturen/:state/:page?', BRP: '/persoonlijke-gegevens', KVK: '/gegevens-handelsregister', BUURT: '/buurt', diff --git a/src/universal/helpers/api.ts b/src/universal/helpers/api.ts index b7f818682a..d6daee25c7 100644 --- a/src/universal/helpers/api.ts +++ b/src/universal/helpers/api.ts @@ -67,6 +67,10 @@ export function isLoading(apiResponseData: ApiResponse) { ); } +export function isOk(apiResponseData: ApiResponse) { + return apiResponseData?.status === 'OK'; +} + export function isError( apiResponseData: ApiResponse, includeFailedDependencies: boolean = true
    - Eerdere en niet verleende vergunningen en ontheffingen -