diff --git a/mocks/collections.json b/mocks/collections.json index 3cb07081f7..6fbfe92897 100644 --- a/mocks/collections.json +++ b/mocks/collections.json @@ -34,6 +34,7 @@ "get-gpass-stadspas:standard", "get-gpass-transacties:standard", "get-gpass-aanbiedingen-transacties:standard", + "post-toggle-stadspas:standard", "get-krefia:standard", "get-kvk:standard", "post-loodmetingen-auth:standard", diff --git a/mocks/fixtures/gpass-pashouders.json b/mocks/fixtures/gpass-pashouders.json index a8901f7543..e3e60ddeed 100644 --- a/mocks/fixtures/gpass-pashouders.json +++ b/mocks/fixtures/gpass-pashouders.json @@ -66,7 +66,7 @@ "categorie": "Minima stadspas", "categorie_code": "M", "actief": true, - "expiry_date": "2024-07-31T21:59:59.000Z", + "expiry_date": "2090-07-31T21:59:59.000Z", "heeft_budget": false, "vervangen": false, "securitycode": "012346", diff --git a/mocks/routes/gpass.js b/mocks/routes/gpass.js index e1ae6af72b..b57988fb45 100644 --- a/mocks/routes/gpass.js +++ b/mocks/routes/gpass.js @@ -1,5 +1,3 @@ -const UID = require('uid-safe'); - const settings = require('../settings'); const RESPONSES = { PASHOUDER: require('../fixtures/gpass-pashouders.json'), @@ -25,15 +23,21 @@ module.exports = [ }, { id: 'get-gpass-stadspas', - url: `${settings.MOCK_BASE_PATH}/gpass/rest/sales/v1/pas/*`, + url: `${settings.MOCK_BASE_PATH}/gpass/rest/sales/v1/pas/:pasnummer`, method: 'GET', variants: [ { id: 'standard', - type: 'json', + type: 'middleware', options: { - status: 200, - body: RESPONSES.STADSPAS, + middleware: (req, res) => { + res.send({ + ...RESPONSES.STADSPAS, + pasnummer: req.params.pasnummer, + pasnummer_volledig: `volledig.${req.params.pasnummer}`, + id: req.params.pasnummer, + }); + }, }, }, ], @@ -68,4 +72,28 @@ module.exports = [ }, ], }, + { + id: 'post-toggle-stadspas', + url: `${settings.MOCK_BASE_PATH}/gpass/rest/sales/v1/togglepas/:pasnummer`, + method: 'POST', + // Add delay to make loading icon visibile in the front end when pressing the block button. + delay: 2500, + variants: [ + { + id: 'standard', + type: 'middleware', + options: { + middleware: (req, res) => { + // return res.status(500).end(); + res.send({ + // NOT sure if this is the same response as the real API + ...RESPONSES.STADSPAS, + pasnummer: req.params.pasnummer, + actief: false, + }); + }, + }, + }, + ], + }, ]; diff --git a/package-lock.json b/package-lock.json index 722699edf9..a1d9d7e296 100644 --- a/package-lock.json +++ b/package-lock.json @@ -110,6 +110,7 @@ "sass": "^1.83.0", "slugme": "^1.1.1", "supercluster": "^7.1.5", + "swr": "^2.3.0", "thenby": "^1.3.4", "throttle-debounce": "^5.0.2", "ts-node": "^10.9.2", @@ -16877,6 +16878,19 @@ "integrity": "sha512-TBzhheU15s+o54Cgk9qxuYcZMiqSm/SkvKnapoGHOF66kz0Y5aGjpzj5BT/vpBbn6rTPJ9tUYXQxuDWfsjiGMw==", "dev": true }, + "node_modules/swr": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.0.tgz", + "integrity": "sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -17597,6 +17611,15 @@ "react": "*" } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 4f55c95b29..3319605ebf 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "sass": "^1.83.0", "slugme": "^1.1.1", "supercluster": "^7.1.5", + "swr": "^2.3.0", "thenby": "^1.3.4", "throttle-debounce": "^5.0.2", "ts-node": "^10.9.2", diff --git a/src/client/components/Modal/Modal.tsx b/src/client/components/Modal/Modal.tsx index 1835c80ae4..bc2541ce17 100644 --- a/src/client/components/Modal/Modal.tsx +++ b/src/client/components/Modal/Modal.tsx @@ -36,10 +36,7 @@ export function Modal({ return isOpen ? ReactDOM.createPortal(
-
+
{children} diff --git a/src/client/components/Search/useSearch.tsx b/src/client/components/Search/useSearch.tsx index 0de5f613d2..4bbe6d9111 100644 --- a/src/client/components/Search/useSearch.tsx +++ b/src/client/components/Search/useSearch.tsx @@ -26,7 +26,10 @@ import { displayPath, } from './search-config'; import { AppRoutes } from '../../../universal/config/routes'; -import { ApiResponse, isError } from '../../../universal/helpers/api'; +import { + ApiResponse_DEPRECATED, + isError, +} from '../../../universal/helpers/api'; import { pick, uniqueArray } from '../../../universal/helpers/utils'; import { AppState, AppStateKey } from '../../../universal/types/App.types'; import { IconMarker } from '../../assets/icons'; @@ -373,7 +376,7 @@ export const searchConfigRemote = selector({ get: async ({ get }) => { // Subscribe to updates from requestID to re-evaluate selector to reload the SEARCH_CONFIG get(requestID); - const response: AxiosResponse> = + const response: AxiosResponse> = await axios.get(BFFApiUrls.SEARCH_CONFIGURATION, { responseType: 'json', withCredentials: true, diff --git a/src/client/config/api.ts b/src/client/config/api.ts index 0614b71508..ef4819c9ec 100644 --- a/src/client/config/api.ts +++ b/src/client/config/api.ts @@ -1,5 +1,8 @@ import { IS_PRODUCTION } from '../../universal/config/env'; -import { ApiResponse, FailedDependencies } from '../../universal/helpers/api'; +import { + ApiResponse_DEPRECATED, + FailedDependencies, +} from '../../universal/helpers/api'; import { ApiError, AppState } from '../../universal/types'; export const BFF_API_BASE_URL = import.meta.env.REACT_APP_BFF_API_URL; @@ -89,7 +92,7 @@ export const ErrorNames: Record = { export function createErrorDisplayData( stateKey: string, - apiResponseData: ApiResponse | null | string + apiResponseData: ApiResponse_DEPRECATED | null | string ): ApiError { const name = ErrorNames[stateKey] || stateKey; const errorMessage = @@ -116,7 +119,7 @@ export function createFailedDependenciesError( apiErrors.push( createErrorDisplayData( `${stateKey}_${stateDependencyKey}`, - apiDependencyResponseData as ApiResponse + apiDependencyResponseData as ApiResponse_DEPRECATED ) ); } @@ -158,7 +161,7 @@ export function getApiErrors(appState: AppState): ApiError[] { apiErrors.push( createErrorDisplayData( stateKey, - apiResponseData as ApiResponse + apiResponseData as ApiResponse_DEPRECATED ) ); } diff --git a/src/client/helpers/test-utils.ts b/src/client/helpers/test-utils.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/client/hooks/api/useCmsMaintenanceNotifications.ts b/src/client/hooks/api/useCmsMaintenanceNotifications.ts index 7527b37745..4bb45d3758 100644 --- a/src/client/hooks/api/useCmsMaintenanceNotifications.ts +++ b/src/client/hooks/api/useCmsMaintenanceNotifications.ts @@ -1,5 +1,8 @@ import type { CMSMaintenanceNotification } from '../../../server/services/cms-maintenance-notifications'; -import { ApiResponse, apiPristineResult } from '../../../universal/helpers/api'; +import { + ApiResponse_DEPRECATED, + apiPristineResult, +} from '../../../universal/helpers/api'; import { BFFApiUrls } from '../../config/api'; import { useAppStateGetter } from '../useAppState'; import { useDataApi } from './useDataApi'; @@ -9,7 +12,9 @@ export function useCmsMaintenanceNotifications( fromApiDirectly: boolean = false ) { const { CMS_MAINTENANCE_NOTIFICATIONS } = useAppStateGetter(); - const [api] = useDataApi>( + const [api] = useDataApi< + ApiResponse_DEPRECATED + >( { url: BFFApiUrls.SERVICES_CMS_MAINTENANCE_NOTIFICATIONS_URL + diff --git a/src/client/hooks/useAppState.ts b/src/client/hooks/useAppState.ts index 2079329ff5..c35eaab0e5 100644 --- a/src/client/hooks/useAppState.ts +++ b/src/client/hooks/useAppState.ts @@ -4,7 +4,10 @@ import { SetterOrUpdater, atom, useRecoilState, useRecoilValue } from 'recoil'; import { streamEndpointQueryParamKeys } from '../../universal/config/app'; import { FeatureToggle } from '../../universal/config/feature-toggles'; -import { ApiResponse, apiPristineResult } from '../../universal/helpers/api'; +import { + ApiResponse_DEPRECATED, + apiPristineResult, +} from '../../universal/helpers/api'; import { AppState, BagThema } from '../../universal/types/App.types'; import { PRISTINE_APPSTATE, createAllErrorState } from '../AppState'; import { BFFApiUrls } from '../config/api'; @@ -214,7 +217,7 @@ export function useAppStateBagApi({ typeof appState[bagThema] !== 'undefined' && key in appState[bagThema]!; - const [api, fetch] = useDataApi>( + const [api, fetch] = useDataApi>( { url, postpone: isApiDataCached || !url, @@ -240,7 +243,7 @@ export function useAppStateBagApi({ localState = { ...localState, - [key]: api.data as ApiResponse, + [key]: api.data as ApiResponse_DEPRECATED, }; return { @@ -252,7 +255,8 @@ export function useAppStateBagApi({ }, [isApiDataCached, api, key, url]); return [ - (appState?.[bagThema]?.[key] as ApiResponse) || api.data, // Return the response data from remote system or the pristine data provided to useApiData. + (appState?.[bagThema]?.[key] as ApiResponse_DEPRECATED) || + api.data, // Return the response data from remote system or the pristine data provided to useApiData. fetch, isApiDataCached, ] as const; @@ -261,7 +265,7 @@ export function useAppStateBagApi({ export function useGetAppStateBagDataByKey({ bagThema, key, -}: Omit): ApiResponse | null { +}: Omit): ApiResponse_DEPRECATED | null { const appState = useRecoilValue(appStateAtom); return appState?.[bagThema]?.[key] ?? null; } diff --git a/src/client/pages/HLI/HLI.hooks.tsx b/src/client/pages/HLI/HLI.hooks.tsx new file mode 100644 index 0000000000..64c0c96fb7 --- /dev/null +++ b/src/client/pages/HLI/HLI.hooks.tsx @@ -0,0 +1,57 @@ +import { atom, useRecoilState } from 'recoil'; +import useSWRMutation from 'swr/mutation'; + +import { StadspasFrontend } from '../../../server/services/hli/stadspas-types'; +import { useAppStateGetter } from '../../hooks/useAppState'; + +type StadspasActiefByID = { + [id: string]: boolean; +}; + +const stadspasActiefAtom = atom({ + key: 'stadspasActief', + default: {}, +}); + +export function useStadspassen() { + const { HLI } = useAppStateGetter(); + const [stadspasActief, setStadspassenActiefStatus] = + useRecoilState(stadspasActiefAtom); + + const stadspassen: StadspasFrontend[] = (HLI.content?.stadspas || []).map( + (pas) => { + const stadspas = { + ...pas, + actief: stadspasActief[pas.id] ?? true, + }; + return stadspas; + } + ); + + return [stadspassen, setStadspassenActiefStatus] as const; +} + +export function useBlockStadspas(url: string | null, stadspasId: string) { + const setStadspassenActiefStatus = useStadspassen()[1]; + + return useSWRMutation( + url, + async (url) => { + const response = await fetch(url, { + method: 'POST', + credentials: 'include', + }); + + if (!response.ok) { + throw new Error('Request returned with an error'); + } + + setStadspassenActiefStatus((stadspasActiefState) => { + return { ...stadspasActiefState, [stadspasId]: false }; + }); + + return response; + }, + { revalidate: false, populateCache: false } + ); +} diff --git a/src/client/pages/HLI/HLI.module.scss b/src/client/pages/HLI/HLI.module.scss index 74d4e8444a..86c3ebe8db 100644 --- a/src/client/pages/HLI/HLI.module.scss +++ b/src/client/pages/HLI/HLI.module.scss @@ -21,8 +21,25 @@ --ams-paragraph-color: #{$color-neutral-grey4}; } +.Stadspassen { + height: fit-content; + tr { + height: 100%; + } +} + +.StatusValue { + // Hack to align table cell content with other cells content + &:before { + content: ''; + line-height: var(--ams-link-standalone-line-height); + font-size: var(--ams-text-level-2-font-size); + } +} + @media screen and (min-width: $ams-breakpoint-medium) { .EerdereRegelingen, + .Stadspassen, .HuidigeRegelingen { tr { > th, diff --git a/src/client/pages/HLI/HLI.test.tsx b/src/client/pages/HLI/HLI.test.tsx index 8bf52d1f4c..7f97eb79bb 100644 --- a/src/client/pages/HLI/HLI.test.tsx +++ b/src/client/pages/HLI/HLI.test.tsx @@ -2,37 +2,16 @@ import { render } from '@testing-library/react'; import { generatePath } from 'react-router-dom'; import { MutableSnapshot } from 'recoil'; -import { - StadspasFrontend, - StadspasOwner, -} from '../../../server/services/hli/stadspas-types'; import { AppRoutes } from '../../../universal/config/routes'; import { appStateAtom } from '../../hooks/useAppState'; import MockApp from '../MockApp'; import ThemaPaginaHLI from './HLI'; +import { stadspasCreator } from './test-helpers'; import { AppState } from '../../../universal/types'; -const owner: StadspasOwner = { - initials: '', - firstname: '', - lastname: '', -}; +const createStadspas = stadspasCreator(); -const stadspas: StadspasFrontend = { - urlTransactions: '', - transactionsKeyEncrypted: '123-xxx-000', - id: 'stadspas-1', - passNumber: 123123123, - passNumberComplete: '0303123123123', - owner, - dateEnd: '31-07-2025', - dateEndFormatted: '31 juli 2025', - budgets: [], - balanceFormatted: '', - balance: 0, -}; - -const testState: Pick = { +const testState = { HLI: { status: 'OK', content: { @@ -74,29 +53,36 @@ const testState: Pick = { decision: 'toegewezen', }, ], - stadspas: [stadspas], + stadspas: [createStadspas('Kerub', true), createStadspas('Lou', false)], }, }, -}; +} as unknown as AppState; -function initializeState(snapshot: MutableSnapshot) { - snapshot.set(appStateAtom, testState as AppState); -} +const routeEntry = generatePath(AppRoutes.HLI); +const routePath = AppRoutes.HLI; -describe('', () => { - const routeEntry = generatePath(AppRoutes.HLI); - const routePath = AppRoutes.HLI; +export function createComponent(state: AppState, component: () => JSX.Element) { + function initializeState(snapshot: MutableSnapshot) { + snapshot.set(appStateAtom, state); + } - const Component = () => ( - - ); + function Component() { + return ( + + ); + } - it('Matches the Full Page snapshot', () => { + return Component; +} + +describe('', () => { + it('Matches the Full Page snapshot with an active and a blocked pas', () => { + const Component = createComponent(testState, ThemaPaginaHLI); const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); diff --git a/src/client/pages/HLI/HLI.tsx b/src/client/pages/HLI/HLI.tsx index 60a6507cef..177a746c85 100644 --- a/src/client/pages/HLI/HLI.tsx +++ b/src/client/pages/HLI/HLI.tsx @@ -1,6 +1,7 @@ -import { Grid, Paragraph, UnorderedList } from '@amsterdam/design-system-react'; +import { Grid, Paragraph } from '@amsterdam/design-system-react'; import { generatePath } from 'react-router-dom'; +import { useStadspassen } from './HLI.hooks'; import styles from './HLI.module.scss'; import { useHliThemaData } from './useHliThemaData'; import { HLIRegeling } from '../../../server/services/hli/hli-regelingen-types'; @@ -8,6 +9,7 @@ import { StadspasFrontend } from '../../../server/services/hli/stadspas-types'; import { FeatureToggle } from '../../../universal/config/feature-toggles'; import { LinkProps } from '../../../universal/types/App.types'; import { MaRouterLink } from '../../components/MaLink/MaLink'; +import { DisplayProps } from '../../components/Table/TableV2'; import ThemaPagina from '../ThemaPagina/ThemaPagina'; import ThemaPaginaTable from '../ThemaPagina/ThemaPaginaTable'; @@ -20,35 +22,52 @@ export function HistoricItemsMention() { ); } -function StadspasListItem({ stadspas }: { stadspas: StadspasFrontend }) { - return ( - - - - {stadspas.owner.firstname} - - {!!stadspas.balance && ( - - Saldo {stadspas.balanceFormatted} - - )} - - - ); -} +type StadspasDisplayProps = { + owner: React.JSX.Element; + actief: string; +}; type StadspassenProps = { stadspassen: StadspasFrontend[]; }; +const displayProps: DisplayProps = { + owner: '', + actief: 'Status', +}; + function Stadspassen({ stadspassen }: StadspassenProps) { + const passen = stadspassen.map((pas) => { + return { + owner: ( + + {`Stadspas van ${pas.owner.firstname}`} + {!!pas.balance && ( + + Saldo {pas.balanceFormatted} + + )} + + ), + actief: ( + + {pas.actief ? 'Actief' : 'Geblokkeerd'} + + ), + }; + }); + return ( - - {stadspassen?.map((stadspas) => ( - - ))} - + + title="" + displayProps={displayProps} + zaken={passen} + className={styles.Stadspassen} + /> + {!!stadspassen?.length && ( {stadspassen.length > 1 ? ( @@ -73,12 +92,12 @@ export default function ThemaPaginaHLI() { hasKindtegoed, isError, isLoading, - stadspassen, regelingen, title, routes, tableConfig, dependencyError, + stadspassen, } = useHliThemaData(); const pageContentTop = ( diff --git a/src/client/pages/HLI/HLIStadspas.module.scss b/src/client/pages/HLI/HLIStadspas.module.scss index 295c932ad6..c82c5d0a2e 100644 --- a/src/client/pages/HLI/HLIStadspas.module.scss +++ b/src/client/pages/HLI/HLIStadspas.module.scss @@ -24,3 +24,7 @@ .StadspasNummerInfo { padding-bottom: var(--ams-grid-row-gap-sm); } + +.BlokkeerDialog { + --ams-dialog-max-inline-size: 40em; +} diff --git a/src/client/pages/HLI/HLIStadspas.test.tsx b/src/client/pages/HLI/HLIStadspas.test.tsx new file mode 100644 index 0000000000..00ee31a08b --- /dev/null +++ b/src/client/pages/HLI/HLIStadspas.test.tsx @@ -0,0 +1,51 @@ +import { render } from '@testing-library/react'; +import { generatePath } from 'react-router-dom'; + +import HLIStadspas from './HLIStadspas'; +import { AppState } from '../../../universal/types'; +import { componentCreator } from '../MockApp'; +import { stadspasCreator } from './test-helpers'; +import { AppRoutes } from '../../../universal/config/routes'; + +const createStadspas = stadspasCreator(); +const passNumber = 12345678; + +const activePasState = { + HLI: { + status: 'OK', + content: { + regelingen: [], + stadspas: [createStadspas('Kerub', true, passNumber)], + }, + }, +} as unknown as AppState; + +const pasBlockedState = { + HLI: { + status: 'OK', + content: { + regelingen: [], + stadspas: [createStadspas('Lou', false, passNumber)], + }, + }, +} as unknown as AppState; + +const createHLIStadspasComponent = componentCreator({ + component: HLIStadspas, + routePath: AppRoutes['HLI/STADSPAS'], + routeEntry: generatePath(AppRoutes['HLI/STADSPAS'], { passNumber }), +}); + +describe('', () => { + test('Finds the block button', () => { + const HLIStadspas = createHLIStadspasComponent(activePasState); + const screen = render(); + expect(screen.getByTestId('block-stadspas-button')).toBeInTheDocument(); + }); + + test('Find label communicating that the stadspas is blocked', () => { + const HLIStadspas = createHLIStadspasComponent(pasBlockedState); + const screen = render(); + expect(screen.getByTestId('stadspas-blocked-alert')).toBeInTheDocument(); + }); +}); diff --git a/src/client/pages/HLI/HLIStadspas.tsx b/src/client/pages/HLI/HLIStadspas.tsx index 70c06960fb..d40ff5e4a3 100644 --- a/src/client/pages/HLI/HLIStadspas.tsx +++ b/src/client/pages/HLI/HLIStadspas.tsx @@ -1,6 +1,9 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { + ActionGroup, + Alert, + Button, Grid, Heading, Paragraph, @@ -10,13 +13,15 @@ import { useParams } from 'react-router-dom'; import { getThemaTitleWithAppState } from './helpers'; import styles from './HLIStadspas.module.scss'; +import { useBlockStadspas, useStadspassen } from './useStadspassen.hook'; import { StadspasBudget, StadspasBudgetTransaction, + StadspasFrontend, } from '../../../server/services/hli/stadspas-types'; import { AppRoutes } from '../../../universal/config/routes'; import { - ApiResponse, + ApiResponse_DEPRECATED, apiPristineResult, isError, isLoading, @@ -25,12 +30,14 @@ import { DetailPage, ErrorAlert, LoadingContent, + Modal, PageHeading, ThemaIcon, } from '../../components'; import { Datalist } from '../../components/Datalist/Datalist'; import { BarConfig } from '../../components/LoadingContent/LoadingContent'; import { MaRouterLink } from '../../components/MaLink/MaLink'; +import { Spinner } from '../../components/Spinner/Spinner'; import { TableV2 } from '../../components/Table/TableV2'; import { useDataApi } from '../../hooks/api/useDataApi'; import { usePhoneScreen } from '../../hooks/media.hook'; @@ -68,14 +75,23 @@ const displayPropsBudgets = { budgetAssignedFormatted: 'Bedrag', }; +const PHONENUMBERS = { + CCA: '14020', + WerkEnInkomen: '020 252 6000', +} as const; + export default function HLIStadspas() { const isPhoneScreen = usePhoneScreen(); const appState = useAppStateGetter(); + const { HLI } = appState; - const { id } = useParams<{ id: string }>(); - const stadspas = id - ? HLI?.content?.stadspas?.find((pass) => pass.id === id) + const { passNumber } = useParams<{ passNumber: string }>(); + const stadspassen = useStadspassen(); + + const stadspas = passNumber + ? stadspassen.find((pass) => pass.passNumber.toString() === passNumber) : null; + const isErrorStadspas = isError(HLI); const isLoadingStadspas = isLoading(HLI); const noContent = !stadspas; @@ -102,7 +118,7 @@ export default function HLIStadspas() { }; const [transactionsApi, fetchTransactions] = useDataApi< - ApiResponse + ApiResponse_DEPRECATED >(requestOptions, apiPristineResult([])); const isLoadingTransacties = transactionsApi.isLoading; @@ -137,7 +153,18 @@ export default function HLIStadspas() { - {!stadspas && ( + {stadspas ? ( + + + + Hieronder staat het Stadspasnummer van uw actieve pas. +
Dit pasnummer staat ook op de achterkant van uw pas. +
+ + {!!stadspas.budgets.length && } + +
+ ) : ( {isLoadingStadspas && ( @@ -152,18 +179,6 @@ export default function HLIStadspas() { )} )} - {!!stadspas && ( - - - - Hieronder staat het Stadspasnummer van uw actieve pas. -
Dit pasnummer staat ook op de achterkant van uw pas. -
- - {!!stadspas.budgets.length && } -
- )} - <> Gekregen tegoed @@ -233,3 +248,119 @@ export default function HLIStadspas() { ); } + +function BlockStadspas({ stadspas }: { stadspas: StadspasFrontend }) { + if (!stadspas.actief) { + return ; + } + + const [isModalOpen, setIsModalOpen] = useState(false); + const [showError, setShowError] = useState(false); + + const { error, isMutating, trigger: blokkeerStadspas } = useBlockStadspas(); + + useEffect(() => { + if (error && !isMutating && !showError) { + setShowError(true); + } + }, [error, showError, isMutating]); + + return ( + <> + {showError && ( + + Probeer het later nog eens. Als dit niet lukt bel dan naar{' '} + {PHONENUMBERS.WerkEnInkomen} + + )} + {isMutating ? ( + + + Bezig met het blokkeren van de pas... + + + ) : ( + + )} + + + + + + } + > + + Is uw Stadspas gestolen of bent u deze kwijt? Blokkeer dan hier uw + Stadspas. Zo zorgt u ervoor dat niemand de Stadspas en eventueel + tegoed van uw kind uitgeeft. + + + Wilt u een nieuwe pas aanvragen of wilt u liever telefonisch + blokkeren? Bel dan meteen naar {PHONENUMBERS.WerkEnInkomen}. De nieuwe + pas wordt dan binnen drie weken thuisgestuurd en is dan gelijk te + gebruiken. + + + + ); +} + +function PassBlockedAlert() { + return ( + + + Wilt u uw pas deblokkeren of wilt u een nieuwe pas aanvragen? Bel dan + naar {PHONENUMBERS.WerkEnInkomen} of {PHONENUMBERS.CCA}. + + + Het aanvragen van een nieuwe pas is gratis. De pas wordt binnen drie + weken thuisgestuurd en is dan gelijk te gebruiken. + + + Stond er nog tegoed op de Stadspas? Dan staat het tegoed dat over was + ook op weer op de nieuwe pas. + + + ); +} diff --git a/src/client/pages/HLI/__snapshots__/HLI.test.tsx.snap b/src/client/pages/HLI/__snapshots__/HLI.test.tsx.snap index 6e2f9a786b..f73fc81a96 100644 --- a/src/client/pages/HLI/__snapshots__/HLI.test.tsx.snap +++ b/src/client/pages/HLI/__snapshots__/HLI.test.tsx.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[` > Matches the Full Page snapshot 1`] = ` +exports[` > Matches the Full Page snapshot with an active and a blocked pas 1`] = `
> Matches the Full Page snapshot 1`] = `
- + + + + + Status + + + + + + + + + Stadspas van Kerub + + + Saldo €5,50 + + + + + + Actief + + + + + + + + Stadspas van Lou + + + Saldo €5,50 + + + + + + Geblokkeerd + + + + + +
+

- De stadspas heeft een einddatum van 31 juli 2025 + Voor alle stadspassen geldt de einddatum van 31 juli 2025

{ describe('getThemaTitle', () => { diff --git a/src/client/pages/HLI/test-helpers.ts b/src/client/pages/HLI/test-helpers.ts new file mode 100644 index 0000000000..c55ff1391f --- /dev/null +++ b/src/client/pages/HLI/test-helpers.ts @@ -0,0 +1,45 @@ +import { + StadspasFrontend, + StadspasOwner, +} from '../../../server/services/hli/stadspas-types'; + +/** Create a createStadspas function that returns stadspassen with incrementing ID's */ +export function stadspasCreator() { + let id = 0; + + function create( + firstname: string, + actief: boolean, + // eslint-disable-next-line no-magic-numbers + passNumber: number = 123123123 + ): StadspasFrontend { + id++; + + const owner: StadspasOwner = { + firstname, + lastname: 'Crepin', + initials: 'KC', + }; + + const passNumberComplete = 'volledig.' + passNumber; + + return { + urlTransactions: 'http://example.com/url-transactions', + transactionsKeyEncrypted: '123-xxx-000', + id: `stadspas-id-${id}`, + passNumber, + passNumberComplete, + owner, + dateEnd: '31-07-2025', + dateEndFormatted: '31 juli 2025', + budgets: [], + balanceFormatted: '€5,50', + balance: 5.5, + blockPassURL: 'http://example.com/stadspas/block', + actief, + securityCode: '123-securitycode-123', + }; + } + + return create; +} diff --git a/src/client/pages/HLI/useHliThemaData.ts b/src/client/pages/HLI/useHliThemaData.ts index ed48dca8b0..1f4a868408 100644 --- a/src/client/pages/HLI/useHliThemaData.ts +++ b/src/client/pages/HLI/useHliThemaData.ts @@ -5,6 +5,7 @@ import { routes, tableConfig, } from './HLI-thema-config'; +import { useStadspassen } from './useStadspassen.hook'; import { HLIRegeling } from '../../../server/services/hli/hli-regelingen-types'; import { hasFailedDependency, @@ -16,7 +17,7 @@ import { useAppStateGetter } from '../../hooks/useAppState'; export function useHliThemaData() { const { HLI } = useAppStateGetter(); - const stadspassen = HLI.content?.stadspas; + const stadspassen = useStadspassen(); const hasStadspas = !!HLI.content?.stadspas?.length; const regelingen = addLinkElementToProperty( HLI.content?.regelingen ?? [], diff --git a/src/client/pages/HLI/useStadspassen.hook.tsx b/src/client/pages/HLI/useStadspassen.hook.tsx new file mode 100644 index 0000000000..63b1f9e0ba --- /dev/null +++ b/src/client/pages/HLI/useStadspassen.hook.tsx @@ -0,0 +1,63 @@ +import useSWR from 'swr'; +import useSWRMutation from 'swr/mutation'; + +import { PasblokkadeByPasnummer } from '../../../server/services/hli/stadspas-types'; +import { ApiResponse_DEPRECATED } from '../../../universal/helpers/api'; +import { useAppStateGetter } from '../../hooks/useAppState'; + +export function useStadspassen() { + const { HLI } = useAppStateGetter(); + const { data: passBlokkadeByPasnummer } = useBlockStadspas(); + const stadspassen = (HLI.content?.stadspas || []).map((pas) => { + const isGeblokkeerd = passBlokkadeByPasnummer?.[pas.passNumber]; + const stadspas = { + ...pas, + actief: isGeblokkeerd ?? pas.actief, + }; + + return stadspas; + }); + + return stadspassen; +} + +type BlokkeerURL = string; + +export function useBlockStadspas() { + const { data } = useSWR('pasblokkades'); + const mutation = useSWRMutation< + | PasblokkadeByPasnummer + | ApiResponse_DEPRECATED, + Error, + 'pasblokkades', + BlokkeerURL + >( + 'pasblokkades', + async (_key, { arg }) => { + const response = await fetch(arg, { + credentials: 'include', + }).then((response) => response.json()); + + if (response.status !== 'OK') { + throw new Error(response.message); + } + + return response; + }, + { + revalidate: false, + populateCache: (response, pasBlokkadeByPasnummer) => { + const newState = { + ...pasBlokkadeByPasnummer, + ...('content' in response ? response.content : {}), + }; + return newState; + }, + } + ); + + return { + ...mutation, + data, + }; +} diff --git a/src/client/pages/MockApp.tsx b/src/client/pages/MockApp.tsx index 6d90fad77d..0a3da3d9f7 100644 --- a/src/client/pages/MockApp.tsx +++ b/src/client/pages/MockApp.tsx @@ -3,6 +3,9 @@ import { Component } from 'react'; import { MemoryRouter, Route } from 'react-router-dom'; import { MutableSnapshot, RecoilRoot } from 'recoil'; +import { AppState } from '../../universal/types'; +import { appStateAtom } from '../hooks/useAppState'; + interface MockAppProps { routePath: string; routeEntry: string; @@ -24,3 +27,29 @@ export default function MockApp({ ); } + +export function componentCreator(conf: { + component: () => JSX.Element; + routeEntry: string; + routePath: string; +}) { + function createComponent(state: AppState) { + function initializeState(snapshot: MutableSnapshot) { + snapshot.set(appStateAtom, state); + } + + function Component() { + return ( + + ); + } + + return Component; + } + return createComponent; +} diff --git a/src/client/pages/Profile/private/useAantalBewonersOpAdres.hook.ts b/src/client/pages/Profile/private/useAantalBewonersOpAdres.hook.ts index 83313845aa..9038cbd572 100644 --- a/src/client/pages/Profile/private/useAantalBewonersOpAdres.hook.ts +++ b/src/client/pages/Profile/private/useAantalBewonersOpAdres.hook.ts @@ -3,7 +3,7 @@ import { useEffect } from 'react'; import { FeatureToggle } from '../../../../universal/config/feature-toggles'; import { apiPristineResult, - ApiResponse, + ApiResponse_DEPRECATED, } from '../../../../universal/helpers/api'; import { AppState } from '../../../../universal/types'; import { useDataApi } from '../../../hooks/api/useDataApi'; @@ -12,7 +12,7 @@ export function useAantalBewonersOpAdres( brpContent: AppState['BRP']['content'] ) { const [{ data: residentData }, fetchResidentCount] = useDataApi< - ApiResponse<{ residentCount: number }> + ApiResponse_DEPRECATED<{ residentCount: number }> >( { url: brpContent?.fetchUrlAantalBewoners ?? '', diff --git a/src/client/pages/VergunningDetail/DocumentDetails.tsx b/src/client/pages/VergunningDetail/DocumentDetails.tsx index c49a36b85f..05fe33bb80 100644 --- a/src/client/pages/VergunningDetail/DocumentDetails.tsx +++ b/src/client/pages/VergunningDetail/DocumentDetails.tsx @@ -5,7 +5,7 @@ import type { VergunningDocument, } from '../../../server/services/vergunningen/vergunningen'; import { - ApiResponse, + ApiResponse_DEPRECATED, apiPristineResult, apiSuccessResult, } from '../../../universal/helpers/api'; @@ -35,7 +35,7 @@ export function DocumentDetails({ isLoading: isLoadingDocuments, }, fetchDocuments, - ] = useDataApi>( + ] = useDataApi>( { postpone: true, transformResponse: ({ content }) => { diff --git a/src/server/openapi-amsapp.yml b/src/server/openapi-amsapp.yml index 3ad25c9c02..0685cc1c2d 100644 --- a/src/server/openapi-amsapp.yml +++ b/src/server/openapi-amsapp.yml @@ -188,6 +188,40 @@ paths: 'application/json': schema: $ref: '#/components/schemas/ApplicationError' + /private/api/v1/services/amsapp/stadspas/block/{transactionsKeyEncrypted}: + post: + security: + - ApiKeyAuth: [] + description: | + Blocks a stadspas with the passNumber supplied in transactionsKeyEncrypted. + parameters: + - name: transactionsKeyEncrypted + in: path + required: true + schema: + type: string + responses: + '200': + description: Blocking stadspas was successful + '400': + description: Returns bad request error + content: + 'application/json': + schema: + $ref: '#/components/schemas/ErrorBadRequest' + '401': + description: Returns unauthorized error (invalid api key) + content: + 'application/json': + schema: + $ref: '#/components/schemas/ErrorUnauthorized' + '500': + description: | + Returns Application error, any error in code or from failed requests. + content: + 'application/json': + schema: + $ref: '#/components/schemas/ApplicationError' components: securitySchemes: diff --git a/src/server/routing/bff-routes.ts b/src/server/routing/bff-routes.ts index d55391ee26..b8eabf6a7e 100644 --- a/src/server/routing/bff-routes.ts +++ b/src/server/routing/bff-routes.ts @@ -29,6 +29,7 @@ export const BffEndpoints = { // Stadspas STADSPAS_TRANSACTIONS: '/services/stadspas/transactions/:transactionsKeyEncrypted?', + STADSPAS_BLOCK_PASS: '/services/stadspas/block/:transactionsKeyEncrypted', // Vergunningen V2 VERGUNNINGENv2_ZAKEN_SOURCE: '/services/vergunningen/v2/zaken/:id?', @@ -75,18 +76,21 @@ export const BffEndpoints = { LOODMETING_DOCUMENT_DOWNLOAD: '/services/lood/document/:id', }; +const AMSAPP_BASE = '/services/amsapp'; + export const ExternalConsumerEndpoints = { // Publicly accessible public: { - STADSPAS_AMSAPP_LOGIN: `/services/amsapp/stadspas/login/:token`, - STADSPAS_ADMINISTRATIENUMMER: `/services/amsapp/stadspas/administratienummer/:token`, - STADSPAS_APP_LANDING: `/services/amsapp/stadspas/app-landing`, + STADSPAS_AMSAPP_LOGIN: `${AMSAPP_BASE}/stadspas/login/:token`, + STADSPAS_ADMINISTRATIENUMMER: `${AMSAPP_BASE}/stadspas/administratienummer/:token`, + STADSPAS_APP_LANDING: `${AMSAPP_BASE}/stadspas/app-landing`, }, // Privately accessible private: { - STADSPAS_PASSEN: `${BFF_BASE_PATH_PRIVATE}/services/amsapp/stadspas/passen/:administratienummerEncrypted`, - STADSPAS_DISCOUNT_TRANSACTIONS: `${BFF_BASE_PATH_PRIVATE}/services/amsapp/stadspas/aanbiedingen/transactions/:transactionsKeyEncrypted`, - STADSPAS_BUDGET_TRANSACTIONS: `${BFF_BASE_PATH_PRIVATE}/services/amsapp/stadspas/budget/transactions/:transactionsKeyEncrypted`, + STADSPAS_PASSEN: `${BFF_BASE_PATH_PRIVATE}${AMSAPP_BASE}/stadspas/passen/:administratienummerEncrypted`, + STADSPAS_DISCOUNT_TRANSACTIONS: `${BFF_BASE_PATH_PRIVATE}${AMSAPP_BASE}/stadspas/aanbiedingen/transactions/:transactionsKeyEncrypted`, + STADSPAS_BUDGET_TRANSACTIONS: `${BFF_BASE_PATH_PRIVATE}${AMSAPP_BASE}/stadspas/budget/transactions/:transactionsKeyEncrypted`, + STADSPAS_BLOCK_PAS: `${BFF_BASE_PATH_PRIVATE}${AMSAPP_BASE}/stadspas/block/:transactionsKeyEncrypted`, }, }; diff --git a/src/server/routing/route-handlers.ts b/src/server/routing/route-handlers.ts index c9177bae3c..c33c920276 100644 --- a/src/server/routing/route-handlers.ts +++ b/src/server/routing/route-handlers.ts @@ -82,13 +82,13 @@ export function nocache(_req: Request, res: Response, next: NextFunction) { next(); } -export function requestID(req: Request, res: Response, next: NextFunction) { +export function requestID(_req: Request, res: Response, next: NextFunction) { const REQUEST_ID_BYTE_LENGTH = 18; res.locals.requestID = uid.sync(REQUEST_ID_BYTE_LENGTH); next(); } -export function clearRequestCache(req: Request, res: Response) { +export function clearRequestCache(_req: Request, res: Response) { const requestID = res.locals.requestID!; clearSessionCache(requestID); } diff --git a/src/server/routing/route-helpers.test.ts b/src/server/routing/route-helpers.test.ts index 57847632ee..6e67a0138a 100644 --- a/src/server/routing/route-helpers.test.ts +++ b/src/server/routing/route-helpers.test.ts @@ -14,7 +14,10 @@ import { } from './route-helpers'; import { bffApiHost } from '../../testing/setup'; import { RequestMock, ResponseMock } from '../../testing/utils'; -import { ApiResponse, apiErrorResult } from '../../universal/helpers/api'; +import { + ApiResponse_DEPRECATED, + apiErrorResult, +} from '../../universal/helpers/api'; import { oidcConfigDigid, oidcConfigEherkenning } from '../auth/auth-config'; import { cache } from '../helpers/source-api-request'; @@ -44,7 +47,7 @@ describe('route-helpers', () => { describe('sendResponse tests', async () => { test('Sends 200 when status OK', () => { - const response: ApiResponse = { + const response: ApiResponse_DEPRECATED = { status: 'OK', content: null, }; diff --git a/src/server/routing/route-helpers.ts b/src/server/routing/route-helpers.ts index 163875e17d..102be8e5ba 100644 --- a/src/server/routing/route-helpers.ts +++ b/src/server/routing/route-helpers.ts @@ -3,7 +3,10 @@ import { generatePath, matchPath } from 'react-router-dom'; import { PUBLIC_BFF_ENDPOINTS } from './bff-routes'; import { HTTP_STATUS_CODES } from '../../universal/constants/errorCodes'; -import { ApiResponse, apiErrorResult } from '../../universal/helpers/api'; +import { + ApiResponse_DEPRECATED, + apiErrorResult, +} from '../../universal/helpers/api'; import { BFF_API_BASE_URL } from '../config/app'; /* eslint-disable @typescript-eslint/no-empty-object-type */ @@ -53,7 +56,10 @@ export function generateFullApiUrlBFF( } /** Sets the right statuscode and sends a response. */ -export function sendResponse(res: Response, apiResponse: ApiResponse) { +export function sendResponse( + res: Response, + apiResponse: ApiResponse_DEPRECATED +) { if (apiResponse.status === 'ERROR') { res.status( typeof apiResponse.code === 'number' diff --git a/src/server/routing/router-protected.ts b/src/server/routing/router-protected.ts index 08ed25c85d..113f0d467f 100644 --- a/src/server/routing/router-protected.ts +++ b/src/server/routing/router-protected.ts @@ -26,14 +26,15 @@ import { loadServicesAll, loadServicesSSE, } from '../services/controller'; +import { fetchDecosDocument } from '../services/decos/decos-service'; import { fetchZorgnedAVDocument, + handleBlockStadspas, handleFetchTransactionsRequest, } from '../services/hli/hli-route-handlers'; import { attachDocumentDownloadRoute } from '../services/shared/document-download-route-handler'; import { fetchErfpachtV2DossiersDetail } from '../services/simple-connect/erfpacht'; import { fetchBBDocument } from '../services/toeristische-verhuur/toeristische-verhuur-powerbrowser-bb-vergunning'; -import { fetchDecosDocument } from '../services/decos/decos-service'; import { fetchVergunningDetail, fetchZakenFromSource, @@ -240,6 +241,7 @@ attachDocumentDownloadRoute( // HLI Stadspas transacties router.get(BffEndpoints.STADSPAS_TRANSACTIONS, handleFetchTransactionsRequest); +router.get(BffEndpoints.STADSPAS_BLOCK_PASS, handleBlockStadspas); // HLI Regelingen / doc download attachDocumentDownloadRoute( diff --git a/src/server/routing/router-public.ts b/src/server/routing/router-public.ts index 0fa7524be3..926f119bd6 100644 --- a/src/server/routing/router-public.ts +++ b/src/server/routing/router-public.ts @@ -10,10 +10,14 @@ import { } from '../../universal/config/myarea-datasets'; import { AppRoutes } from '../../universal/config/routes'; import { HTTP_STATUS_CODES } from '../../universal/constants/errorCodes'; -import { ApiResponse, apiSuccessResult } from '../../universal/helpers/api'; +import { + ApiResponse_DEPRECATED, + apiSuccessResult, +} from '../../universal/helpers/api'; import { getAuth, getReturnToUrlZaakStatus } from '../auth/auth-helpers'; import { authRoutes } from '../auth/auth-routes'; import { RELEASE_VERSION } from '../config/app'; +import { getFromEnv } from '../helpers/env'; import { QueryParamsCMSFooter, fetchCMSCONTENT, @@ -29,7 +33,6 @@ import { QueryParamsMaintenanceNotifications, fetchMaintenanceNotificationsActual, } from '../services/cms-maintenance-notifications'; -import { getFromEnv } from '../helpers/env'; export const router = express.Router(); @@ -138,7 +141,7 @@ router.get( const id = req.params.id; const datasetCategoryId = getDatasetCategoryId(datasetId); - let response: ApiResponse | null = null; + let response: ApiResponse_DEPRECATED | null = null; try { if (datasetCategoryId && datasetId && id) { diff --git a/src/server/routing/router-stadspas-external-consumer.ts b/src/server/routing/router-stadspas-external-consumer.ts index 83af71057e..799538c451 100644 --- a/src/server/routing/router-stadspas-external-consumer.ts +++ b/src/server/routing/router-stadspas-external-consumer.ts @@ -22,6 +22,7 @@ import { getApiConfig } from '../helpers/source-api-helpers'; import { requestData } from '../helpers/source-api-request'; import { fetchAdministratienummer } from '../services/hli/hli-zorgned-service'; import { + blockStadspas, fetchStadspasBudgetTransactions, fetchStadspasDiscountTransactions, } from '../services/hli/stadspas'; @@ -29,12 +30,72 @@ import { fetchStadspassenByAdministratienummer } from '../services/hli/stadspas- import { StadspasAMSAPPFrontend, StadspasBudget, + TransactionKeysEncryptedWithoutSessionID, } from '../services/hli/stadspas-types'; import { captureException, captureMessage } from '../services/monitoring'; const AMSAPP_PROTOCOl = 'amsterdam://'; const AMSAPP_STADSPAS_DEEP_LINK = `${AMSAPP_PROTOCOl}stadspas`; +// PUBLIC INTERNET NETWORK ROUTER +// ============================== +export const routerInternet = express.Router(); +routerInternet.BFF_ID = 'external-consumer-public'; + +routerInternet.get( + ExternalConsumerEndpoints.public.STADSPAS_AMSAPP_LOGIN, + async (req: Request<{ token: string }>, res: Response) => { + return res.redirect( + authRoutes.AUTH_LOGIN_DIGID + + `?returnTo=${RETURNTO_AMSAPP_STADSPAS_ADMINISTRATIENUMMER}&amsapp-session-token=${req.params.token}` + ); + } +); + +routerInternet.get( + ExternalConsumerEndpoints.public.STADSPAS_ADMINISTRATIENUMMER, + sendAdministratienummerResponse +); + +routerInternet.get( + ExternalConsumerEndpoints.public.STADSPAS_APP_LANDING, + sendAppLandingResponse +); + +// PRIVATE NETWORK ROUTER +// ====================== +export const routerPrivateNetwork = express.Router(); +routerPrivateNetwork.BFF_ID = 'external-consumer-private-network'; + +export const stadspasExternalConsumerRouter = { + internet: routerInternet, + privateNetwork: routerPrivateNetwork, +}; + +routerPrivateNetwork.get( + ExternalConsumerEndpoints.private.STADSPAS_PASSEN, + apiKeyVerificationHandler, + sendStadspassenResponse +); + +routerPrivateNetwork.get( + ExternalConsumerEndpoints.private.STADSPAS_DISCOUNT_TRANSACTIONS, + apiKeyVerificationHandler, + sendDiscountTransactionsResponse +); + +routerPrivateNetwork.get( + ExternalConsumerEndpoints.private.STADSPAS_BUDGET_TRANSACTIONS, + apiKeyVerificationHandler, + sendBudgetTransactionsResponse +); + +routerPrivateNetwork.post( + ExternalConsumerEndpoints.private.STADSPAS_BLOCK_PAS, + apiKeyVerificationHandler, + sendStadspasBlockRequest +); + type ApiError = { code: string; message: string; @@ -65,22 +126,6 @@ const apiResponseErrors: Record = { }, } as const; -export const routerInternet = express.Router(); -routerInternet.BFF_ID = 'external-consumer-public'; - -export const routerPrivateNetwork = express.Router(); -routerPrivateNetwork.BFF_ID = 'external-consumer-private-network'; - -routerInternet.get( - ExternalConsumerEndpoints.public.STADSPAS_AMSAPP_LOGIN, - async (req: Request<{ token: string }>, res: Response) => { - return res.redirect( - authRoutes.AUTH_LOGIN_DIGID + - `?returnTo=${RETURNTO_AMSAPP_STADSPAS_ADMINISTRATIENUMMER}&amsapp-session-token=${req.params.token}` - ); - } -); - type RenderProps = { nonce: string; promptOpenApp: boolean; @@ -196,11 +241,6 @@ async function sendAdministratienummerResponse( return res.render('amsapp-stadspas-administratienummer', renderProps); } -routerInternet.get( - ExternalConsumerEndpoints.public.STADSPAS_ADMINISTRATIENUMMER, - sendAdministratienummerResponse -); - function sendAppLandingResponse(_req: Request, res: Response) { const renderProps: RenderProps = { ...baseRenderProps, @@ -209,11 +249,6 @@ function sendAppLandingResponse(_req: Request, res: Response) { return res.render('amsapp-stadspas-administratienummer', renderProps); } -routerInternet.get( - ExternalConsumerEndpoints.public.STADSPAS_APP_LANDING, - sendAppLandingResponse -); - async function sendStadspassenResponse( req: Request<{ administratienummerEncrypted: string }>, res: Response @@ -264,14 +299,12 @@ async function sendStadspassenResponse( ); } -routerPrivateNetwork.get( - ExternalConsumerEndpoints.private.STADSPAS_PASSEN, - apiKeyVerificationHandler, - sendStadspassenResponse -); +type TransactionKeysEncryptedRequest = Request<{ + transactionsKeyEncrypted: TransactionKeysEncryptedWithoutSessionID; +}>; async function sendDiscountTransactionsResponse( - req: Request<{ transactionsKeyEncrypted: string }>, + req: TransactionKeysEncryptedRequest, res: Response ) { const response = await fetchStadspasDiscountTransactions( @@ -282,12 +315,6 @@ async function sendDiscountTransactionsResponse( sendResponse(res, response); } -routerPrivateNetwork.get( - ExternalConsumerEndpoints.private.STADSPAS_DISCOUNT_TRANSACTIONS, - apiKeyVerificationHandler, - sendDiscountTransactionsResponse -); - /** Sends transformed budget transactions. * * # Url Params @@ -295,7 +322,7 @@ routerPrivateNetwork.get( * `transactionsKeyEncrypted`: is available in the response of `sendStadspassenResponse`. */ async function sendBudgetTransactionsResponse( - req: Request<{ transactionsKeyEncrypted: string }>, + req: TransactionKeysEncryptedRequest, res: Response ) { const response = await fetchStadspasBudgetTransactions( @@ -307,16 +334,16 @@ async function sendBudgetTransactionsResponse( return sendResponse(res, response); } -routerPrivateNetwork.get( - ExternalConsumerEndpoints.private.STADSPAS_BUDGET_TRANSACTIONS, - apiKeyVerificationHandler, - sendBudgetTransactionsResponse -); - -export const stadspasExternalConsumerRouter = { - internet: routerInternet, - privateNetwork: routerPrivateNetwork, -}; +async function sendStadspasBlockRequest( + req: TransactionKeysEncryptedRequest, + res: Response +) { + const response = await blockStadspas( + res.locals.requestID, + req.params.transactionsKeyEncrypted + ); + return sendResponse(res, response); +} export const forTesting = { sendAdministratienummerResponse, diff --git a/src/server/services/afis/afis-business-partner.ts b/src/server/services/afis/afis-business-partner.ts index 0cf3c2806a..2af224e060 100644 --- a/src/server/services/afis/afis-business-partner.ts +++ b/src/server/services/afis/afis-business-partner.ts @@ -14,7 +14,7 @@ import { import { FeatureToggle } from '../../../universal/config/feature-toggles'; import { apiErrorResult, - ApiResponse, + ApiResponse_DEPRECATED, apiSuccessResult, getFailedDependencies, getSettledResult, @@ -54,7 +54,7 @@ function transformBusinessPartnerAddressResponse( async function fetchBusinessPartnerAddress( requestID: RequestID, businessPartnerId: string -): Promise> { +): Promise> { const additionalConfig: DataRequestConfig = { transformResponse: transformBusinessPartnerAddressResponse, formatUrl(config) { @@ -202,8 +202,8 @@ export async function fetchAfisBusinessPartnerDetails( const fullNameResponse = getSettledResult(fullNameResult); const addressResponse = getSettledResult(addressResult); - let phoneResponse: ApiResponse; - let emailResponse: ApiResponse; + let phoneResponse: ApiResponse_DEPRECATED; + let emailResponse: ApiResponse_DEPRECATED; if (addressResponse.status === 'OK' && addressResponse.content) { const phoneRequest = fetchPhoneNumber( diff --git a/src/server/services/afis/afis-facturen.ts b/src/server/services/afis/afis-facturen.ts index 1e8721f22c..18eebab382 100644 --- a/src/server/services/afis/afis-facturen.ts +++ b/src/server/services/afis/afis-facturen.ts @@ -6,7 +6,7 @@ import { firstBy } from 'thenby'; import { FeatureToggle } from '../../../universal/config/feature-toggles'; import { apiErrorResult, - ApiResponse, + ApiResponse_DEPRECATED, apiSuccessResult, getFailedDependencies, getSettledResult, @@ -141,7 +141,7 @@ function transformDeelbetalingenResponse( async function fetchAfisFacturenDeelbetalingen( requestID: RequestID, params: AfisFacturenParams -): Promise> { +): Promise> { const config = await getAfisApiConfig( { formatUrl: ({ url }) => formatFactuurRequestURL(url, params), @@ -415,7 +415,7 @@ export async function fetchAfisFacturen( requestID: RequestID, sessionID: SessionID, params: AfisFacturenParams -): Promise> { +): Promise> { let deelbetalingen: AfisFactuurDeelbetalingen | undefined; if (params.state === 'open' || params.state === 'afgehandeld') { diff --git a/src/server/services/bag.ts b/src/server/services/bag.ts index e2737eeb77..43f0132123 100644 --- a/src/server/services/bag.ts +++ b/src/server/services/bag.ts @@ -1,6 +1,9 @@ import { LatLngLiteral } from 'leaflet'; -import { apiErrorResult, ApiResponse } from '../../universal/helpers/api'; +import { + apiErrorResult, + ApiResponse_DEPRECATED, +} from '../../universal/helpers/api'; import { getLatLngCoordinates } from '../../universal/helpers/bag'; import { Adres } from '../../universal/types'; import { BAGQueryParams } from '../../universal/types/bag'; @@ -17,7 +20,7 @@ export interface BAGData { export async function fetchBAG( requestID: RequestID, sourceAddress: Adres | null -): Promise> { +): Promise> { if (!sourceAddress?.straatnaam || !sourceAddress.huisnummer) { return apiErrorResult('Could not query BAG, no address supplied.', null); } diff --git a/src/server/services/brp.ts b/src/server/services/brp.ts index 537835c342..ff064da06a 100644 --- a/src/server/services/brp.ts +++ b/src/server/services/brp.ts @@ -5,7 +5,7 @@ import slug from 'slugme'; import { AppRoutes } from '../../universal/config/routes'; import { Themas } from '../../universal/config/thema'; import { - ApiResponse, + ApiResponse_DEPRECATED, ApiSuccessResponse, apiDependencyError, apiSuccessResult, @@ -278,7 +278,7 @@ export async function fetchAantalBewoners( data: { addressKey: addressKeyEncrypted, }, - transformResponse: (responseData: ApiResponse) => { + transformResponse: (responseData: ApiResponse_DEPRECATED) => { if (responseData.status === 'OK') { return responseData.content; } diff --git a/src/server/services/buurt/buurt.ts b/src/server/services/buurt/buurt.ts index 46a0b1c536..d8f9e62d6d 100644 --- a/src/server/services/buurt/buurt.ts +++ b/src/server/services/buurt/buurt.ts @@ -6,7 +6,7 @@ import { POLYLINE_GEOMETRY_TYPES, } from '../../../universal/config/myarea-datasets'; import { - ApiResponse, + ApiResponse_DEPRECATED, apiErrorResult, apiSuccessResult, } from '../../../universal/helpers/api'; @@ -174,7 +174,7 @@ export async function fetchDataset( return response; } -type ApiDatasetResponse = ApiResponse; +type ApiDatasetResponse = ApiResponse_DEPRECATED; export async function loadDatasetFeatures( requestID: RequestID, diff --git a/src/server/services/buurt/helpers.test.ts b/src/server/services/buurt/helpers.test.ts index d52cdb983a..48037224c9 100644 --- a/src/server/services/buurt/helpers.test.ts +++ b/src/server/services/buurt/helpers.test.ts @@ -31,7 +31,7 @@ import { refineFilterSelection, } from './helpers'; import { remoteApiHost } from '../../../testing/setup'; -import { ApiResponse } from '../../../universal/helpers/api'; +import { ApiResponse_DEPRECATED } from '../../../universal/helpers/api'; const DSO_API_RESULT = { _links: { @@ -614,7 +614,7 @@ describe('Buurt helpers', () => { }); it('Should datasetApiResult', () => { - const apiResponses: ApiResponse[] = [ + const apiResponses: ApiResponse_DEPRECATED[] = [ { status: 'OK', content: { features: features.slice(0, 2) }, diff --git a/src/server/services/buurt/helpers.ts b/src/server/services/buurt/helpers.ts index 4faddae81e..57a3039dc2 100644 --- a/src/server/services/buurt/helpers.ts +++ b/src/server/services/buurt/helpers.ts @@ -25,7 +25,7 @@ import { } from '../../../universal/config/myarea-datasets'; import { ApiErrorResponse, - ApiResponse, + ApiResponse_DEPRECATED, ApiSuccessResponse, } from '../../../universal/helpers/api'; import { capitalizeFirstLetter } from '../../../universal/helpers/text'; @@ -432,7 +432,7 @@ export function createFeaturePropertiesFromPropertyFilterConfig( } export function datasetApiResult( - results: ApiResponse[] + results: ApiResponse_DEPRECATED[] ) { const errors = results .filter( diff --git a/src/server/services/cms-content.ts b/src/server/services/cms-content.ts index 912819cfc6..f29d55b63d 100644 --- a/src/server/services/cms-content.ts +++ b/src/server/services/cms-content.ts @@ -8,7 +8,7 @@ import sanitizeHtml, { IOptions } from 'sanitize-html'; import { IS_TAP } from '../../universal/config/env'; import { apiErrorResult, - ApiResponse, + ApiResponse_DEPRECATED, ApiSuccessResponse, apiSuccessResult, getSettledResult, @@ -190,7 +190,7 @@ async function getGeneralPage( requestID: RequestID, profileType: ProfileType = 'private', forceRenew: boolean = false -): Promise> { +): Promise> { const apiData = fileCache.getKey>( 'CMS_CONTENT_GENERAL_INFO_' + profileType ); @@ -246,10 +246,11 @@ async function getGeneralPage( async function getFooter( requestID: RequestID, forceRenew: boolean = false -): Promise> { +): Promise> { const apiData = - fileCache.getKey>('CMS_CONTENT_FOOTER') ?? - null; + fileCache.getKey>( + 'CMS_CONTENT_FOOTER' + ) ?? null; if (apiData && !forceRenew) { return Promise.resolve(apiData); @@ -268,7 +269,7 @@ async function getFooter( } // Try to get stale cache instead. const staleApiData = - fileCache.getKeyStale>( + fileCache.getKeyStale>( 'CMS_CONTENT_FOOTER' ); @@ -299,7 +300,7 @@ async function fetchCmsBase( const footerInfoPageRequest = getFooter(requestID, forceRenew); const requests: Promise< - ApiResponse + ApiResponse_DEPRECATED >[] = [generalInfoPageRequest, footerInfoPageRequest]; const [generalInfo, footer] = await Promise.allSettled(requests); diff --git a/src/server/services/cms-maintenance-notifications.ts b/src/server/services/cms-maintenance-notifications.ts index bb7b446648..10c462557b 100644 --- a/src/server/services/cms-maintenance-notifications.ts +++ b/src/server/services/cms-maintenance-notifications.ts @@ -4,7 +4,7 @@ import { marked } from 'marked'; import { IS_TAP } from '../../universal/config/env'; import { Themas } from '../../universal/config/thema'; import { - ApiResponse, + ApiResponse_DEPRECATED, ApiSuccessResponse, apiSuccessResult, } from '../../universal/helpers/api'; @@ -124,7 +124,7 @@ function transformCMSEventResponse( async function fetchCMSMaintenanceNotifications( requestID: RequestID, useCache: boolean = true -): Promise> { +): Promise> { const cachedData = fileCache.getKey< ApiSuccessResponse >('CMS_MAINTENANCE_NOTIFICATIONS'); diff --git a/src/server/services/controller.ts b/src/server/services/controller.ts index 25143d6b2f..935e3fc0fd 100644 --- a/src/server/services/controller.ts +++ b/src/server/services/controller.ts @@ -4,7 +4,7 @@ import { streamEndpointQueryParamKeys } from '../../universal/config/app'; import { FeatureToggle } from '../../universal/config/feature-toggles'; import { apiErrorResult, - ApiResponse, + ApiResponse_DEPRECATED, apiSuccessResult, getSettledResult, } from '../../universal/helpers/api'; @@ -94,7 +94,7 @@ function getServiceTipsMap(profileType: ProfileType) { } export function addServiceResultHandler< - T extends Promise>>, + T extends Promise>>, >(res: Response, servicePromise: T, serviceName: string) { if (IS_DEBUG) { // eslint-disable-next-line no-console diff --git a/src/server/services/decos/decos-service.ts b/src/server/services/decos/decos-service.ts index bd22f1cb56..0c5884b149 100644 --- a/src/server/services/decos/decos-service.ts +++ b/src/server/services/decos/decos-service.ts @@ -28,7 +28,7 @@ import { } from './helpers'; import { ApiErrorResponse, - ApiResponse, + ApiResponse_DEPRECATED, ApiSuccessResponse, apiSuccessResult, getSettledResult, @@ -447,7 +447,9 @@ export async function fetchDecosWorkflowDates( requestID: RequestID, zaakID: DecosZaakBase['key'], stepTitles: DecosWorkflowStepTitle[] -): Promise | null>> { +): Promise< + ApiResponse_DEPRECATED | null> +> { const apiConfigWorkflows = getApiConfig('DECOS_API', { formatUrl: (config) => { return `${config.url}/items/${zaakID}/workflows`; diff --git a/src/server/services/decos/decos-types.ts b/src/server/services/decos/decos-types.ts index 14b259ea32..5771b06178 100644 --- a/src/server/services/decos/decos-types.ts +++ b/src/server/services/decos/decos-types.ts @@ -1,4 +1,4 @@ -import { ApiResponse } from '../../../universal/helpers/api'; +import { ApiResponse_DEPRECATED } from '../../../universal/helpers/api'; import { SomeOtherString } from '../../../universal/helpers/types'; import { GenericDocument } from '../../../universal/types'; import { DecosCaseType } from '../../../universal/types/vergunningen'; @@ -124,7 +124,7 @@ export type DecosTransformerOptions = { decosZaakTransformer?: DecosZaakTransformer; fetchDecosWorkflowDates?: ( stepTitles: DecosWorkflowStepTitle[] - ) => Promise | null>>; + ) => Promise | null>>; }; export type DecosZaakTransformer = { diff --git a/src/server/services/hli/hli-route-handlers.ts b/src/server/services/hli/hli-route-handlers.ts index 28b465c536..44be37f744 100644 --- a/src/server/services/hli/hli-route-handlers.ts +++ b/src/server/services/hli/hli-route-handlers.ts @@ -1,16 +1,18 @@ import { Request, Response } from 'express'; -import { fetchStadspasBudgetTransactions } from './stadspas'; +import { blockStadspas, fetchStadspasBudgetTransactions } from './stadspas'; import { StadspasBudget, StadspasFrontend } from './stadspas-types'; import { getAuth } from '../../auth/auth-helpers'; import { AuthProfileAndToken } from '../../auth/auth-types'; import { sendResponse, sendUnauthorized } from '../../routing/route-helpers'; import { fetchDocument } from '../zorgned/zorgned-service'; +type TransactionKeysEncryptedRequest = Request<{ + transactionsKeyEncrypted: StadspasFrontend['transactionsKeyEncrypted']; +}>; + export async function handleFetchTransactionsRequest( - req: Request<{ - transactionsKeyEncrypted: StadspasFrontend['transactionsKeyEncrypted']; - }>, + req: TransactionKeysEncryptedRequest, res: Response ) { const authProfileAndToken = getAuth(req); @@ -40,3 +42,22 @@ export async function fetchZorgnedAVDocument( ); return response; } + +export async function handleBlockStadspas( + req: TransactionKeysEncryptedRequest, + res: Response +) { + const authProfileAndToken = getAuth(req); + + if (!authProfileAndToken) { + return sendUnauthorized(res); + } + + const response = await blockStadspas( + res.locals.requestID, + req.params.transactionsKeyEncrypted, + authProfileAndToken.profile.sid + ); + + return sendResponse(res, response); +} diff --git a/src/server/services/hli/stadspas-config-and-content.ts b/src/server/services/hli/stadspas-config-and-content.ts index 6a172a7303..f259243619 100644 --- a/src/server/services/hli/stadspas-config-and-content.ts +++ b/src/server/services/hli/stadspas-config-and-content.ts @@ -26,7 +26,7 @@ export function getBudgetNotifications(stadspassen: StadspasFrontend[]) { const createNotificationBudget = ( description: string, - stadspasId?: string + stadspasPassNumber?: string ) => ({ id: `stadspas-budget-notification`, datePublished: dateFormat(new Date(), 'yyyy-MM-dd'), @@ -36,8 +36,10 @@ export function getBudgetNotifications(stadspassen: StadspasFrontend[]) { )}!`, description, link: { - to: stadspasId - ? generatePath(AppRoutes['HLI/STADSPAS'], { id: stadspasId }) + to: stadspasPassNumber + ? generatePath(AppRoutes['HLI/STADSPAS'], { + passNumber: stadspasPassNumber, + }) : AppRoutes.HLI, title: 'Check het saldo', }, @@ -64,7 +66,10 @@ export function getBudgetNotifications(stadspassen: StadspasFrontend[]) { ) { const notification = isParent ? createNotificationBudget(BUDGET_NOTIFICATION_PARENT) - : createNotificationBudget(BUDGET_NOTIFICATION_CHILD, stadspas?.id); + : createNotificationBudget( + BUDGET_NOTIFICATION_CHILD, + stadspas?.passNumber.toString() + ); notifications.push(notification); } diff --git a/src/server/services/hli/stadspas-gpass-service.test.ts b/src/server/services/hli/stadspas-gpass-service.test.ts index 500c747b85..53e7a19283 100644 --- a/src/server/services/hli/stadspas-gpass-service.test.ts +++ b/src/server/services/hli/stadspas-gpass-service.test.ts @@ -1,9 +1,11 @@ import { describe, expect, Mock } from 'vitest'; +import { blockStadspas } from './stadspas'; import { GPASS_API_TOKEN } from './stadspas-config-and-content'; import { fetchGpassDiscountTransactions, forTesting, + mutateGpassBlockPass, } from './stadspas-gpass-service'; import { fetchStadspassenByAdministratienummer } from './stadspas-gpass-service'; import { @@ -15,10 +17,18 @@ import { StadspasHouderSource, StadspasTransactiesResponseSource, } from './stadspas-types'; +import { remoteApi } from '../../../testing/utils'; import { HTTP_STATUS_CODES } from '../../../universal/constants/errorCodes'; -import { apiSuccessResult } from '../../../universal/helpers/api'; +import { + ApiErrorResponse, + apiSuccessResult, +} from '../../../universal/helpers/api'; import { getApiConfig } from '../../helpers/source-api-helpers'; import { requestData } from '../../helpers/source-api-request'; +import { BffEndpoints } from '../../routing/bff-routes'; + +vi.mock('../../helpers/source-api-request'); +vi.mock('../../helpers/source-api-helpers'); describe('stadspas-gpass-service', () => { describe('getHeaders', () => { @@ -144,6 +154,7 @@ describe('stadspas-gpass-service', () => { ); expect(transformedResponse).toStrictEqual({ id: '1', + actief: false, owner: { firstname: 'John', lastname: 'Doe', @@ -452,8 +463,6 @@ describe('stadspas-gpass-service', () => { ]); }); }); - vi.mock('../../helpers/source-api-request'); - vi.mock('../../helpers/source-api-helpers'); describe('fetchStadspassenByAdministratienummer', () => { const requestID = 'test-request-id'; @@ -500,7 +509,7 @@ describe('stadspas-gpass-service', () => { }); test('should return transformed stadspassen if stadspasHouderResponse status is OK', async () => { - const pashouder: StadspasHouderSource = { + const pashouder = { voornaam: 'John', achternaam: 'Doe', tussenvoegsel: 'van', @@ -605,6 +614,7 @@ describe('stadspas-gpass-service', () => { infix: 'van', initials: 'J.D.', }, + actief: false, dateEnd: '2023-12-31', dateEndFormatted: '31 december 2023', budgets: [ @@ -758,4 +768,75 @@ describe('stadspas-gpass-service', () => { expect(result).toStrictEqual(errorResponse); }); }); + + describe('blockStadspass', async () => { + const PassBlockedSuccessfulResponse = { + content: null, + status: 'OK', + }; + + const requestId = '123'; + const transactionKeysEncrypted = '123'; + const passNumber = 123; + + test('Uses decrypt and fetcher', async () => { + remoteApi.post( + BffEndpoints.STADSPAS_BLOCK_PASS, + PassBlockedSuccessfulResponse + ); + + const response = (await blockStadspas( + requestId, + // This cannot be decrypted so we expect an error response. + transactionKeysEncrypted + )) as ApiErrorResponse; + expect(response.message).toContain('Failed to decrypt'); + }); + + test('Will block a pass that is active', async () => { + remoteApi + .get(`/stadspas/rest/sales/v1/pas/${passNumber}?include_balance=true`) + .reply(200, { actief: false }); + (requestData as Mock).mockResolvedValueOnce({ + status: 'OK', + content: { actief: true }, + }); + + remoteApi.post( + BffEndpoints.STADSPAS_BLOCK_PASS, + PassBlockedSuccessfulResponse + ); + (requestData as Mock).mockResolvedValueOnce({ + status: 'OK', + content: null, + }); + + const response = await mutateGpassBlockPass(requestId, 123, '123'); + expect(response).toStrictEqual({ status: 'OK', content: null }); + }); + + test('Can only block and not toggle the stadspas', async () => { + remoteApi + .get(`/stadspas/rest/sales/v1/pas/${passNumber}?include_balance=true`) + .reply(200, { actief: false }); + (requestData as Mock).mockResolvedValueOnce({ + status: 'OK', + content: { actief: false }, + }); + + remoteApi.post( + BffEndpoints.STADSPAS_BLOCK_PASS, + PassBlockedSuccessfulResponse + ); + + const response = await mutateGpassBlockPass(requestId, 123, '123'); + expect(response).toStrictEqual({ + code: 403, + content: null, + message: + 'The citypass is not active. We cannot unblock an active pass.', + status: 'ERROR', + }); + }); + }); }); diff --git a/src/server/services/hli/stadspas-gpass-service.ts b/src/server/services/hli/stadspas-gpass-service.ts index f2c1c92e47..b1754df34e 100644 --- a/src/server/services/hli/stadspas-gpass-service.ts +++ b/src/server/services/hli/stadspas-gpass-service.ts @@ -1,3 +1,5 @@ +import { HttpStatusCode } from 'axios'; +import { isPast } from 'date-fns'; import memoizee from 'memoizee'; import { fetchAdministratienummer } from './hli-zorgned-service'; @@ -9,19 +11,24 @@ import { StadspasBudget, StadspasBudgetTransaction, StadspasDetailBudgetSource, - StadspasDetailSource, StadspasDiscountTransaction, StadspasDiscountTransactions, StadspasDiscountTransactionsResponseSource, StadspasHouderSource, StadspasOwner, StadspasPasHouderResponse, + StadspasDetailSource, StadspasTransactieSource, StadspasTransactiesResponseSource, StadspasTransactionQueryParams, + PasblokkadeByPasnummer, } from './stadspas-types'; import { HTTP_STATUS_CODES } from '../../../universal/constants/errorCodes'; import { + apiErrorResult, + ApiResponse_DEPRECATED, + ApiResponse, + ApiSuccessResponse, apiSuccessResult, getSettledResult, } from '../../../universal/helpers/api'; @@ -99,6 +106,7 @@ function transformStadspasResponse( balanceFormatted: `€${displayAmount(balance)}`, passNumber: gpassStadspasResonseData.pasnummer, passNumberComplete: gpassStadspasResonseData.pasnummer_volledig, + actief: gpassStadspasResonseData.actief, securityCode, }; @@ -108,6 +116,21 @@ function transformStadspasResponse( return gpassStadspasResonseData; } +export async function fetchStadspasSource( + requestID: RequestID, + passNumber: number, + administratienummer: string +): Promise> { + const dataRequestConfig = getApiConfig('GPASS', { + formatUrl: ({ url }) => `${url}/rest/sales/v1/pas/${passNumber}`, + headers: getHeaders(administratienummer), + params: { + include_balance: true, + }, + }); + return requestData(dataRequestConfig, requestID); +} + export async function fetchStadspassenByAdministratienummer( requestID: RequestID, administratienummer: string @@ -115,7 +138,6 @@ export async function fetchStadspassenByAdministratienummer( const dataRequestConfig = getApiConfig('GPASS'); const GPASS_ENDPOINT_PASHOUDER = `${dataRequestConfig.url}/rest/sales/v1/pashouder`; - const GPASS_ENDPOINT_PAS = `${dataRequestConfig.url}/rest/sales/v1/pas`; const headers = getHeaders(administratienummer); const stadspasHouderResponse = await requestData( @@ -150,22 +172,32 @@ export async function fetchStadspassenByAdministratienummer( const pasRequests = []; for (const pashouder of pashouders) { - for (const pas of pashouder.passen.filter((pas) => pas.actief)) { - const url = `${GPASS_ENDPOINT_PAS}/${pas.pasnummer}`; - const request = requestData( - { - ...dataRequestConfig, - url, - transformResponse: (stadspas) => - transformStadspasResponse(stadspas, pashouder, pas.securitycode), - headers, - params: { - include_balance: true, - }, - }, - requestID - ); - pasRequests.push(request); + const passen = pashouder.passen.filter( + (pas) => pas.actief || !isPast(new Date(pas.expiry_date)) + ); + for (const pas of passen) { + const response = fetchStadspasSource( + requestID, + pas.pasnummer, + administratienummer + ).then((response) => { + if (response.content && response.status === 'OK') { + const pasTransformed = transformStadspasResponse( + response.content, + pashouder, + pas.securitycode + ); + const stadspas: ApiSuccessResponse = { + ...response, + content: pasTransformed, + }; + + return stadspas; + } + return response; + }); + + pasRequests.push(response); } } @@ -319,6 +351,45 @@ export async function fetchGpassDiscountTransactions( ); } +export async function mutateGpassBlockPass( + requestID: RequestID, + passNumber: number, + administratienummer: string +): Promise> { + const passResponse = await fetchStadspasSource( + requestID, + passNumber, + administratienummer + ); + if (passResponse.status !== 'OK') { + return passResponse; + } + // This may not give unexpected results so we do extra typechecking on the source input. + if ( + typeof passResponse.content?.actief !== 'boolean' || + !passResponse.content.actief + ) { + return apiErrorResult( + 'The citypass is not active. We cannot unblock an active pass.', + null, + HttpStatusCode.Forbidden + ); + } + + const config = getApiConfig('GPASS', { + method: 'POST', + formatUrl: ({ url }) => `${url}/rest/sales/v1/togglepas/${passNumber}`, + transformResponse: (pas: StadspasDetailSource) => { + if (pas.actief) { + throw Error('City pass is still active after trying to block it.'); + } + return { [pas.pasnummer]: pas.actief }; + }, + }); + + return requestData(config, requestID); +} + export const forTesting = { transformTransactions, transformGpassAanbiedingenResponse, diff --git a/src/server/services/hli/stadspas-types.ts b/src/server/services/hli/stadspas-types.ts index eb0d1839e6..c161a7fadb 100644 --- a/src/server/services/hli/stadspas-types.ts +++ b/src/server/services/hli/stadspas-types.ts @@ -85,7 +85,7 @@ export type SecurityCode = string; export interface StadspasHouderPasSource { actief: boolean; - budgetten: unknown[]; // Did not see the exact shape of this data, encountered an empty array. + budgetten: unknown[]; // Did not see the exact shape of this data, encountered an empty array. categorie: string; categorie_code: string; expiry_date: string; @@ -93,7 +93,7 @@ export interface StadspasHouderPasSource { id: number; pasnummer: number; pasnummer_volledig: string; - passoort: { id: number, naam: string }; + passoort: { id: number; naam: string }; securitycode: SecurityCode; vervangen: boolean; } @@ -173,17 +173,23 @@ export interface Stadspas { budgets: StadspasBudget[]; balanceFormatted: string; balance: number; + actief: boolean; securityCode: SecurityCode; } +export type TransactionKeysEncrypted = string; + export interface StadspasFrontend extends Stadspas { urlTransactions: string; transactionsKeyEncrypted: string; + blockPassURL: string | null; link?: LinkProps; } +export type TransactionKeysEncryptedWithoutSessionID = string; + export interface StadspasAMSAPPFrontend extends Stadspas { - transactionsKeyEncrypted: string; + transactionsKeyEncrypted: TransactionKeysEncryptedWithoutSessionID; } export interface StadspasTransactionQueryParams { @@ -222,3 +228,8 @@ export interface StadspasDiscountTransaction { } export type StadspasAdministratieNummer = string; + +export type PasblokkadeByPasnummer = Record< + StadspasFrontend['passNumber'], + boolean +>; diff --git a/src/server/services/hli/stadspas.test.ts b/src/server/services/hli/stadspas.test.ts index 57a913b09f..091febfb19 100644 --- a/src/server/services/hli/stadspas.test.ts +++ b/src/server/services/hli/stadspas.test.ts @@ -5,6 +5,7 @@ import { fetchStadspassen, } from './stadspas-gpass-service'; import { + Stadspas, StadspasDiscountTransactions, StadspasDiscountTransactionsResponseSource, } from './stadspas-types'; @@ -38,6 +39,7 @@ function createStadspasHouderResponse() { function createPas( actief: boolean, + // eslint-disable-next-line no-magic-numbers pasnummer: number = 777777777777, securitycode: string = '012345' ) { @@ -74,8 +76,9 @@ function createPas( }; } -function createTransformedPas(firstname: string, initials: string) { +function createTransformedPas(firstname: string, initials: string): Stadspas { return { + actief: true, balance: 0, balanceFormatted: '€0,00', budgets: [ diff --git a/src/server/services/hli/stadspas.ts b/src/server/services/hli/stadspas.ts index a92442d615..b3b8db001a 100644 --- a/src/server/services/hli/stadspas.ts +++ b/src/server/services/hli/stadspas.ts @@ -2,6 +2,7 @@ import { generatePath } from 'react-router-dom'; import { getBudgetNotifications } from './stadspas-config-and-content'; import { + mutateGpassBlockPass, fetchGpassBudgetTransactions, fetchGpassDiscountTransactions, fetchStadspassen, @@ -15,6 +16,7 @@ import { AppRoutes } from '../../../universal/config/routes'; import { HTTP_STATUS_CODES } from '../../../universal/constants/errorCodes'; import { apiErrorResult, + ApiResponse_DEPRECATED, apiSuccessResult, } from '../../../universal/helpers/api'; import { AuthProfileAndToken } from '../../auth/auth-types'; @@ -26,43 +28,52 @@ import { captureException } from '../monitoring'; export async function fetchStadspas( requestID: RequestID, authProfileAndToken: AuthProfileAndToken -) { +): Promise> { const stadspasResponse = await fetchStadspassen( requestID, authProfileAndToken ); - if (stadspasResponse.status === 'OK') { - const stadspassen: StadspasFrontend[] = - stadspasResponse.content.stadspassen.map((stadspas) => { - const [transactionsKeyEncrypted] = encrypt( - `${authProfileAndToken.profile.sid}:${stadspasResponse.content.administratienummer}:${stadspas.passNumber}` - ); - - const urlTransactions = generateFullApiUrlBFF( - BffEndpoints.STADSPAS_TRANSACTIONS, - { - transactionsKeyEncrypted, - } - ); - - return { - ...stadspas, - urlTransactions, - transactionsKeyEncrypted, - link: { - to: generatePath(AppRoutes['HLI/STADSPAS'], { - id: stadspas.id, - }), - title: `Stadspas van ${stadspas.owner.firstname}`, - }, - }; - }); - - return apiSuccessResult(stadspassen); + if (stadspasResponse.status !== 'OK') { + return stadspasResponse; } - return stadspasResponse; + const stadspassen: StadspasFrontend[] = + stadspasResponse.content.stadspassen.map((stadspas) => { + const [transactionsKeyEncrypted] = encrypt( + `${authProfileAndToken.profile.sid}:${stadspasResponse.content.administratienummer}:${stadspas.passNumber}` + ); + + const urlTransactions = generateFullApiUrlBFF( + BffEndpoints.STADSPAS_TRANSACTIONS, + { + transactionsKeyEncrypted, + } + ); + + let blockPassURL = null; + if (stadspas.actief) { + blockPassURL = generateFullApiUrlBFF(BffEndpoints.STADSPAS_BLOCK_PASS, { + transactionsKeyEncrypted, + }); + } + + const stadspasFrontend: StadspasFrontend = { + ...stadspas, + urlTransactions, + transactionsKeyEncrypted, + blockPassURL, + link: { + to: generatePath(AppRoutes['HLI/STADSPAS'], { + passNumber: stadspas.passNumber, + }), + title: `Stadspas van ${stadspas.owner.firstname}`, + }, + }; + return stadspasFrontend; + }); + + return apiSuccessResult(stadspassen); } async function decryptEncryptedRouteParamAndValidateSessionIDStadspasTransactionsKey( @@ -112,18 +123,18 @@ async function decryptEncryptedRouteParamAndValidateSessionIDStadspasTransaction }); } -async function decryptAndFetch( +export async function stadspasDecryptAndFetch( fetchTransactionFn: ( administratienummer: StadspasAdministratieNummer, pasnummer: StadspasFrontend['passNumber'] ) => T, transactionsKeyEncrypted: string, - verifySessionId?: AuthProfileAndToken['profile']['sid'] + sessionId?: AuthProfileAndToken['profile']['sid'] ) { const decryptResult = await decryptEncryptedRouteParamAndValidateSessionIDStadspasTransactionsKey( transactionsKeyEncrypted, - verifySessionId + sessionId ); if (decryptResult.status === 'OK') { @@ -140,7 +151,7 @@ export async function fetchStadspasDiscountTransactions( requestID: RequestID, transactionsKeyEncrypted: StadspasFrontend['transactionsKeyEncrypted'] ) { - return decryptAndFetch( + return stadspasDecryptAndFetch( (administratienummer, pasnummer) => fetchGpassDiscountTransactions(requestID, administratienummer, pasnummer), transactionsKeyEncrypted @@ -153,7 +164,7 @@ export async function fetchStadspasBudgetTransactions( budgetCode?: StadspasBudget['code'], verifySessionId?: AuthProfileAndToken['profile']['sid'] ) { - return decryptAndFetch( + return stadspasDecryptAndFetch( (administratienummer, pasnummer) => fetchGpassBudgetTransactions( requestID, @@ -166,6 +177,25 @@ export async function fetchStadspasBudgetTransactions( ); } +/** Block a stadspas with it's passNumber. + * + * The passNumber is encrypted inside the transactionsKeyEncrypted. + * The endpoint in use can also unblock cards, but we prevent this so its block only. + */ +export async function blockStadspas( + requestID: RequestID, + transactionsKeyEncrypted: string, + verifySessionId?: AuthProfileAndToken['profile']['sid'] +) { + return stadspasDecryptAndFetch( + (administratienummer, pasnummer) => { + return mutateGpassBlockPass(requestID, pasnummer, administratienummer); + }, + transactionsKeyEncrypted, + verifySessionId + ); +} + export async function fetchStadspasNotifications( requestID: RequestID, authProfileAndToken: AuthProfileAndToken @@ -179,5 +209,5 @@ export async function fetchStadspasNotifications( export const forTesting = { decryptEncryptedRouteParamAndValidateSessionIDStadspasTransactionsKey, - decryptAndFetch, + decryptAndFetch: stadspasDecryptAndFetch, }; diff --git a/src/server/services/my-locations.ts b/src/server/services/my-locations.ts index c037dd82c6..225462a488 100644 --- a/src/server/services/my-locations.ts +++ b/src/server/services/my-locations.ts @@ -6,7 +6,7 @@ import { DEFAULT_LNG, } from '../../universal/config/myarea-datasets'; import { - ApiResponse, + ApiResponse_DEPRECATED, apiDependencyError, apiErrorResult, apiSuccessResult, @@ -18,7 +18,7 @@ import { AuthProfileAndToken } from '../auth/auth-types'; async function fetchPrivate( requestID: RequestID, authProfileAndToken: AuthProfileAndToken -): Promise> { +): Promise> { const BRP = await fetchBRP(requestID, authProfileAndToken); if (BRP.status === 'OK') { @@ -59,10 +59,10 @@ async function fetchPrivate( async function fetchCommercial( requestID: RequestID, authProfileAndToken: AuthProfileAndToken -): Promise> { +): Promise> { const KVK = await fetchKVK(requestID, authProfileAndToken); - let MY_LOCATION: ApiResponse; + let MY_LOCATION: ApiResponse_DEPRECATED; if (KVK.status === 'OK') { const addresses: Adres[] = getKvkAddresses(KVK.content); @@ -99,7 +99,7 @@ async function fetchCommercial( export async function fetchMyLocation( requestID: RequestID, authProfileAndToken: AuthProfileAndToken -): Promise> { +): Promise> { const commercialResponse = await fetchCommercial( requestID, authProfileAndToken diff --git a/src/server/services/simple-connect/api-service.ts b/src/server/services/simple-connect/api-service.ts index f89024541f..d99503586c 100644 --- a/src/server/services/simple-connect/api-service.ts +++ b/src/server/services/simple-connect/api-service.ts @@ -1,7 +1,10 @@ import { AxiosResponseTransformer } from 'axios'; import { Thema } from '../../../universal/config/thema'; -import { ApiResponse, apiSuccessResult } from '../../../universal/helpers/api'; +import { + ApiResponse_DEPRECATED, + apiSuccessResult, +} from '../../../universal/helpers/api'; import { omit } from '../../../universal/helpers/utils'; import { MyNotification, MyTip } from '../../../universal/types'; import { AuthProfileAndToken } from '../../auth/auth-types'; @@ -15,7 +18,7 @@ export interface ApiPatternResponseA { } const transformApiResponseDefault: AxiosResponseTransformer = ( - response: ApiResponse | ApiPatternResponseA + response: ApiResponse_DEPRECATED | ApiPatternResponseA ) => { if ( response !== null && @@ -33,7 +36,7 @@ export async function fetchService( apiConfig: DataRequestConfig = {}, includeTipsAndNotifications: boolean = false, authProfileAndToken?: AuthProfileAndToken -): Promise> { +): Promise> { const transformResponse = [transformApiResponseDefault].concat( apiConfig.transformResponse ?? [] ); @@ -87,7 +90,10 @@ export async function fetchTipsAndNotifications( thema: Thema, authProfileAndToken?: AuthProfileAndToken ): Promise< - ApiResponse | null> + ApiResponse_DEPRECATED | null> > { const response = await fetchService( requestID, diff --git a/src/server/services/tips-and-notifications.ts b/src/server/services/tips-and-notifications.ts index 8dd9a9db8c..4d4d5c42a6 100644 --- a/src/server/services/tips-and-notifications.ts +++ b/src/server/services/tips-and-notifications.ts @@ -3,7 +3,10 @@ import memoize from 'memoizee'; import { fetchAdoptableTrashContainers } from './adoptable-trash-containers'; import { FeatureToggle } from '../../universal/config/feature-toggles'; -import { ApiResponse, getSettledResult } from '../../universal/helpers/api'; +import { + ApiResponse_DEPRECATED, + getSettledResult, +} from '../../universal/helpers/api'; import { dateSort } from '../../universal/helpers/date'; import type { MyNotification, MyTip } from '../../universal/types'; import { AuthProfileAndToken } from '../auth/auth-types'; @@ -89,7 +92,7 @@ export function sortNotifications( } export function getTipsAndNotificationsFromApiResults( - responses: Array> + responses: Array> ): MyNotification[] { const notifications: MyNotification[] = []; const tips: MyTip[] = []; @@ -146,7 +149,7 @@ export function getTipsAndNotificationsFromApiResults( type FetchNotificationFunction = ( requestID: RequestID, authProfileAndToken: AuthProfileAndToken -) => Promise>; +) => Promise>; type NotificationServices = Record; diff --git a/src/server/services/tips/predicates.test.ts b/src/server/services/tips/predicates.test.ts index d9e9bdba05..f1ff85f819 100644 --- a/src/server/services/tips/predicates.test.ts +++ b/src/server/services/tips/predicates.test.ts @@ -26,7 +26,7 @@ import BRP from '../../../../mocks/fixtures/brp.json'; import WPI_AANVRAGEN from '../../../../mocks/fixtures/wpi-aanvragen.json'; import WPI_E from '../../../../mocks/fixtures/wpi-e-aanvragen.json'; import { - ApiResponse, + ApiResponse_DEPRECATED, ApiSuccessResponse, } from '../../../universal/helpers/api'; import { AppState, BRPData, BRPDataFromSource } from '../../../universal/types'; @@ -77,7 +77,7 @@ describe('predicates', () => { BRP as ApiSuccessResponse ), status: 'OK', - } as ApiResponse, + } as ApiResponse_DEPRECATED, }; }; @@ -391,8 +391,10 @@ describe('predicates', () => { return { WPI_TOZO: TOZO as unknown as AppState['WPI_TOZO'], - WPI_TONK: TONK as unknown as ApiResponse, - WPI_AANVRAGEN: UITKERINGEN as unknown as ApiResponse< + WPI_TONK: TONK as unknown as ApiResponse_DEPRECATED< + WpiRequestProcess[] | null + >, + WPI_AANVRAGEN: UITKERINGEN as unknown as ApiResponse_DEPRECATED< WpiRequestProcess[] | null >, }; @@ -474,7 +476,7 @@ describe('predicates', () => { UITKERINGEN.content[0].datePublished = datePublished; return { - WPI_AANVRAGEN: UITKERINGEN as unknown as ApiResponse< + WPI_AANVRAGEN: UITKERINGEN as unknown as ApiResponse_DEPRECATED< WpiRequestProcess[] | null >, }; @@ -498,7 +500,9 @@ describe('predicates', () => { describe('hasTozo', () => { it('should return true when there is some content', () => { const appState = { - WPI_TOZO: TOZO as unknown as ApiResponse, + WPI_TOZO: TOZO as unknown as ApiResponse_DEPRECATED< + WpiRequestProcess[] | null + >, }; expect(hasTozo(appState)).toBe(true); @@ -506,7 +510,7 @@ describe('predicates', () => { it('should return false when no content', () => { const appState = { - WPI_TOZO: {} as ApiResponse, + WPI_TOZO: {} as ApiResponse_DEPRECATED, }; expect(hasTozo(appState)).toBe(false); }); diff --git a/src/server/services/tips/tip-types.ts b/src/server/services/tips/tip-types.ts index ab9b1c0f26..65d3a41ae0 100644 --- a/src/server/services/tips/tip-types.ts +++ b/src/server/services/tips/tip-types.ts @@ -1,8 +1,10 @@ import { Thema } from '../../../universal/config/thema'; -import { ApiResponse } from '../../../universal/helpers/api'; +import { ApiResponse_DEPRECATED } from '../../../universal/helpers/api'; import { AppState, LinkProps } from '../../../universal/types'; -export type ServiceResults = { [serviceId: string]: ApiResponse }; +export type ServiceResults = { + [serviceId: string]: ApiResponse_DEPRECATED; +}; export type Tip = { id: string; diff --git a/src/server/services/toeristische-verhuur/toeristische-verhuur-powerbrowser-bb-vergunning.ts b/src/server/services/toeristische-verhuur/toeristische-verhuur-powerbrowser-bb-vergunning.ts index a6be9c89b6..5b88047593 100644 --- a/src/server/services/toeristische-verhuur/toeristische-verhuur-powerbrowser-bb-vergunning.ts +++ b/src/server/services/toeristische-verhuur/toeristische-verhuur-powerbrowser-bb-vergunning.ts @@ -20,7 +20,7 @@ import { import { AppRoutes } from '../../../universal/config/routes'; import { apiErrorResult, - ApiResponse, + ApiResponse_DEPRECATED, apiSuccessResult, getSettledResult, } from '../../../universal/helpers/api'; @@ -281,7 +281,7 @@ function transformZaakStatusResponse( async function fetchZaakAdres( requestID: RequestID, zaakId: PBZaakRecord['id'] -): Promise> { +): Promise> { const addressResponse = await fetchPowerBrowserData(requestID, { method: 'post', formatUrl({ url }) { @@ -315,7 +315,7 @@ async function fetchZaakAdres( async function fetchZaakStatussen( requestID: RequestID, zaak: BBVergunning -): Promise> { +): Promise> { const statusResponse = await fetchPowerBrowserData( requestID, { @@ -490,7 +490,7 @@ async function fetchZakenByIds( requestID: RequestID, authProfile: AuthProfile, zaakIds: string[] -): Promise> { +): Promise> { const requestConfig: DataRequestConfig = { method: 'get', formatUrl({ url }) { @@ -532,7 +532,7 @@ async function fetchZakenByIds( export async function fetchBBVergunningen( requestID: RequestID, authProfile: AuthProfile -): Promise> { +): Promise> { // Set-up the options for the PowerBrowser API request based on the profile type. const optionsByProfileType: Record< ProfileType, @@ -682,7 +682,7 @@ export async function fetchBBDocumentsList( requestID: RequestID, authProfile: AuthProfile, zaakId: BBVergunning['id'] -): Promise> { +): Promise> { const dataRequestConfig: DataRequestConfig = { method: 'post', formatUrl({ url }) { diff --git a/src/server/services/vergunningen/vergunningen.ts b/src/server/services/vergunningen/vergunningen.ts index 4852182876..fcffd9e7e4 100644 --- a/src/server/services/vergunningen/vergunningen.ts +++ b/src/server/services/vergunningen/vergunningen.ts @@ -10,7 +10,7 @@ import { import { AppRoutes } from '../../../universal/config/routes'; import { Themas } from '../../../universal/config/thema'; import { - ApiResponse, + ApiResponse_DEPRECATED, apiDependencyError, apiSuccessResult, } from '../../../universal/helpers/api'; @@ -634,7 +634,9 @@ export async function fetchVergunningenDocumentsList( { url, passthroughOIDCToken: true, - transformResponse: (responseData: ApiResponse) => { + transformResponse: ( + responseData: ApiResponse_DEPRECATED + ) => { if (responseData.status === 'OK') { const documents: GenericDocument[] = responseData.content.map( (document) => { diff --git a/src/server/services/wpi/api-service.ts b/src/server/services/wpi/api-service.ts index 4f7360f87d..2635d6dea9 100644 --- a/src/server/services/wpi/api-service.ts +++ b/src/server/services/wpi/api-service.ts @@ -1,6 +1,6 @@ import { Themas } from '../../../universal/config/thema'; import { - ApiResponse, + ApiResponse_DEPRECATED, ApiSuccessResponse, apiSuccessResult, } from '../../../universal/helpers/api'; @@ -80,7 +80,7 @@ export async function fetchRequestProcess( requestProcess: WpiRequestProcess ) => WpiRequestProcessLabels | undefined, fetchConfig: FetchConfig -): Promise> { +): Promise> { const apiConfig = getApiConfig(fetchConfig.apiConfigName, { cacheKey: fetchConfig.requestCacheKey, transformResponse: [ diff --git a/src/server/services/zorgned/zorgned-service.test.ts b/src/server/services/zorgned/zorgned-service.test.ts index 5877537632..cf8da806f7 100644 --- a/src/server/services/zorgned/zorgned-service.test.ts +++ b/src/server/services/zorgned/zorgned-service.test.ts @@ -150,10 +150,14 @@ describe('zorgned-service', () => { }); it('should fetch document successfully', async () => { + const filename = 'Naam documentje'; + const mimetype = 'foo/bar'; + const base64Data = 'Zm9vLWJhcg=='; + remoteApi.post('/zorgned/document').reply(200, { - inhoud: 'Zm9vLWJhcg==', - omschrijving: 'Naam documentje', - mimetype: 'foo/bar', + inhoud: base64Data, + omschrijving: filename, + mimetype, }); const result = await fetchDocument( @@ -165,6 +169,7 @@ describe('zorgned-service', () => { expect(requestData).toHaveBeenCalledWith( { + httpsAgent: expect.any(Object), url: `${remoteApiHost}/zorgned/document`, data: { burgerservicenummer: mocks.mockAuthProfileAndToken.profile.id, @@ -178,18 +183,16 @@ describe('zorgned-service', () => { 'Content-type': 'application/json; charset=utf-8', 'X-Mams-Api-User': 'JZD', }, - httpsAgent: expect.any(Object), }, - mocks.mockRequestID, - mocks.mockAuthProfileAndToken as AuthProfileAndToken + mocks.mockRequestID ); expect(result).toEqual({ status: 'OK', content: { - filename: 'Naam documentje', - mimetype: 'foo/bar', - data: Buffer.from('Zm9vLWJhcg==', 'base64'), + filename, + mimetype, + data: Buffer.from(base64Data, 'base64'), }, }); }); diff --git a/src/server/services/zorgned/zorgned-service.ts b/src/server/services/zorgned/zorgned-service.ts index 423b9e4e34..729a1510bd 100644 --- a/src/server/services/zorgned/zorgned-service.ts +++ b/src/server/services/zorgned/zorgned-service.ts @@ -282,8 +282,7 @@ export async function fetchDocument( }; }, }, - requestID, - authProfileAndToken + requestID ); } diff --git a/src/universal/config/routes.ts b/src/universal/config/routes.ts index 1e54694670..425ed95ca1 100644 --- a/src/universal/config/routes.ts +++ b/src/universal/config/routes.ts @@ -10,7 +10,7 @@ export const AppRoutes = { 'ZORG/VOORZIENINGEN_LIST': '/zorg-en-ondersteuning/:kind/:page?', HLI: '/regelingen-bij-laag-inkomen', - 'HLI/STADSPAS': '/regelingen-bij-laag-inkomen/stadspas/:id', + 'HLI/STADSPAS': '/regelingen-bij-laag-inkomen/stadspas/:passNumber', 'HLI/REGELING': '/regelingen-bij-laag-inkomen/regeling/:regeling/:id', 'HLI/REGELINGEN_LIST': '/regelingen-bij-laag-inkomen/:kind/:page?', diff --git a/src/universal/helpers/api.ts b/src/universal/helpers/api.ts index 633b1819f5..5646b90c8b 100644 --- a/src/universal/helpers/api.ts +++ b/src/universal/helpers/api.ts @@ -48,14 +48,19 @@ export type ResponseStatus = | 'POSTPONE' | 'DEPENDENCY_ERROR'; -export type ApiResponse = +export type ApiResponse_DEPRECATED = | ApiErrorResponse | ApiSuccessResponse | ApiPristineResponse | ApiPostponeResponse | ApiDependencyErrorResponse; -export function isLoading(apiResponseData: ApiResponse) { +export type ApiResponse = + | ApiErrorResponse + | ApiSuccessResponse + | ApiPostponeResponse; + +export function isLoading(apiResponseData: ApiResponse_DEPRECATED) { // If no responseData was found, assumes it's still loading return ( (!apiResponseData && !isError(apiResponseData)) || @@ -63,12 +68,12 @@ export function isLoading(apiResponseData: ApiResponse) { ); } -export function isOk(apiResponseData: ApiResponse) { +export function isOk(apiResponseData: ApiResponse_DEPRECATED) { return apiResponseData?.status === 'OK'; } export function isError( - apiResponseData: ApiResponse, + apiResponseData: ApiResponse_DEPRECATED, includeFailedDependencies: boolean = true ) { return ( @@ -81,7 +86,7 @@ export function isError( } export function hasFailedDependency( - apiResponseData: ApiResponse, + apiResponseData: ApiResponse_DEPRECATED, dependencyKey: string ) { return ( @@ -161,7 +166,7 @@ export function apiPostponeResult(content: T): ApiPostponeResponse { } export function apiDependencyError( - apiResponses: Record> + apiResponses: Record> ): ApiDependencyErrorResponse { return { message: Object.entries(apiResponses).reduce((acc, [key, response]) => { diff --git a/src/universal/types/App.types.ts b/src/universal/types/App.types.ts index 910c3f5182..786a1738dc 100644 --- a/src/universal/types/App.types.ts +++ b/src/universal/types/App.types.ts @@ -2,12 +2,12 @@ import { FunctionComponent, ReactNode, SVGProps } from 'react'; import { ServiceID, ServicesType } from '../../server/services/controller'; import { Thema } from '../config/thema'; -import { ApiResponse } from '../helpers/api'; +import { ApiResponse_DEPRECATED } from '../helpers/api'; export type BagThema = `${Thema}_BAG`; export type AppState = { - [key in ServiceID]: ApiResponse< + [key in ServiceID]: ApiResponse_DEPRECATED< ReturnTypeAsync['content'] >; } & {