diff --git a/mocks/routes/cms.js b/mocks/routes/cms.js index 5cd8f38bb3..5eab6c3d67 100644 --- a/mocks/routes/cms.js +++ b/mocks/routes/cms.js @@ -1,5 +1,5 @@ -const settings = require('../settings'); const loadFixtureAndReplaceBaseUrl = require('../loadFixtureAndReplaceBaseUrl'); +const settings = require('../settings'); const ALLE_RESPONSE = loadFixtureAndReplaceBaseUrl( 'cms-maintenance-notifications-alle.json' @@ -65,10 +65,17 @@ module.exports = [ variants: [ { id: 'standard', - type: 'json', + type: 'middleware', options: { - status: 200, - body: PRODUCTEN_OP_MA, + middleware: (req, res, next, core) => { + const { articleslug } = req.params; + if (articleslug === 'overzicht-producten-ondernemers') { + const productenOndernemer = structuredClone(PRODUCTEN_OP_MA); + productenOndernemer.applicatie.inhoud.inleiding = `

Mock content voor BEDRIJVEN

`; + return res.send(productenOndernemer); + } + return res.send(PRODUCTEN_OP_MA); + }, }, }, ], diff --git a/src/client/pages/Parkeren/Parkeren.test.tsx b/src/client/pages/Parkeren/Parkeren.test.tsx index d153e51464..74cbfefcca 100644 --- a/src/client/pages/Parkeren/Parkeren.test.tsx +++ b/src/client/pages/Parkeren/Parkeren.test.tsx @@ -30,9 +30,6 @@ describe('Parkeren', () => { ); } - function initializeState(snapshot: MutableSnapshot) { - snapshot.set(appStateAtom, testState); - } beforeAll(() => { window.scrollTo = vi.fn(); }); @@ -80,7 +77,14 @@ describe('determinePageContentTop', () => { test('Renders button with parkeer vergunningen', () => { const PageContentTop = determinePageContentTop(true, EXTERNAL_PARKEREN_URL); - const screen = render(); + const screen = render( + + ); expect(screen.queryByText(linkButtonTxt)).toBeInTheDocument(); }); @@ -94,6 +98,10 @@ describe('determinePageContentTop', () => { }); }); +function initializeState(snapshot: MutableSnapshot) { + snapshot.set(appStateAtom, testState); +} + const testState = { PARKEREN: { content: { diff --git a/src/client/pages/Parkeren/Parkeren.tsx b/src/client/pages/Parkeren/Parkeren.tsx index e69597430b..a12dd1cfec 100644 --- a/src/client/pages/Parkeren/Parkeren.tsx +++ b/src/client/pages/Parkeren/Parkeren.tsx @@ -8,6 +8,7 @@ import { VergunningFrontendV2 } from '../../../server/services/vergunningen-v2/c import { AppRoutes } from '../../../universal/config/routes'; import { MaButtonLink } from '../../components/MaLink/MaLink'; import { ThemaTitles } from '../../config/thema'; +import { useProfileTypeValue } from '../../hooks/useProfileType'; import ThemaPagina from '../ThemaPagina/ThemaPagina'; import ThemaPaginaTable from '../ThemaPagina/ThemaPaginaTable'; @@ -63,12 +64,20 @@ function determinePageContentTop( parkerenUrlSSO: string ) { if (hasMijnParkerenVergunningen) { + const profileType = useProfileTypeValue(); + + const profileTypeLabel = + profileType === 'commercial' ? 'bedrijven' : 'bewoners'; + return ( <> - + - Het inzien, aanvragen of wijzigen van een parkeervergunning voor - bewoners kan via Mijn Parkeren. + Het inzien, aanvragen of wijzigen van een parkeervergunning voor{' '} + {profileTypeLabel} kan via Mijn Parkeren. diff --git a/src/client/pages/ZaakStatus/ZaakStatus.tsx b/src/client/pages/ZaakStatus/ZaakStatus.tsx index b7867093a6..0fc8c04b0d 100644 --- a/src/client/pages/ZaakStatus/ZaakStatus.tsx +++ b/src/client/pages/ZaakStatus/ZaakStatus.tsx @@ -20,7 +20,7 @@ import { useAppStateGetter, useAppStateReady } from '../../hooks/useAppState'; const ITEM_NOT_FOUND = 'not-found'; const STATE_ERROR = 'state-error'; -type ThemaQueryParam = 'vergunningen'; +type ThemaQueryParam = 'vergunningen' | 'toeristischeVerhuur'; type PageRouteResolver = { baseRoute: AppRoute; @@ -41,13 +41,34 @@ const pageRouteResolvers: PageRouteResolvers = { } if (!isLoading(appState.VERGUNNINGEN)) { return ( - appState.VERGUNNINGEN.content?.find( + (appState.VERGUNNINGEN.content || []).find( (vergunning) => vergunning.identifier === detailPageItemId )?.link.to ?? ITEM_NOT_FOUND ); } }, }, + toeristischeVerhuur: { + baseRoute: AppRoutes.TOERISTISCHE_VERHUUR, + getRoute: (detailPageItemId, appState) => { + if (isError(appState.TOERISTISCHE_VERHUUR)) { + return STATE_ERROR; + } + + if (!isLoading(appState.TOERISTISCHE_VERHUUR)) { + return ( + ( + appState.TOERISTISCHE_VERHUUR.content + ?.vakantieverhuurVergunningen || [] + ).find((toeristischeVerhuur) => { + if (toeristischeVerhuur.zaaknummer === detailPageItemId) { + return toeristischeVerhuur; + } + })?.link.to ?? ITEM_NOT_FOUND + ); + } + }, + }, }; function useNavigateToPage(queryParams: URLSearchParams) { diff --git a/src/mijnamsterdam.d.ts b/src/mijnamsterdam.d.ts index db2eb41cdc..16f1d978f0 100644 --- a/src/mijnamsterdam.d.ts +++ b/src/mijnamsterdam.d.ts @@ -22,10 +22,6 @@ type ReturnTypeAsync any> = T extends ( ? R : any; -type ProfileType = 'private' | 'private-attributes' | 'commercial'; - -type AuthMethod = 'digid' | 'eherkenning'; - type Optional = Pick, K> & Omit; type Prettify = { diff --git a/src/server/auth/auth-helpers.ts b/src/server/auth/auth-helpers.ts index 226a40d800..82b45b673e 100644 --- a/src/server/auth/auth-helpers.ts +++ b/src/server/auth/auth-helpers.ts @@ -19,6 +19,7 @@ import { } from './auth-types'; import { FeatureToggle } from '../../universal/config/feature-toggles'; import { AppRoutes } from '../../universal/config/routes'; +import { PROFILE_TYPES } from '../../universal/types/App.types'; import { ExternalConsumerEndpoints } from '../routing/bff-routes'; import { generateFullApiUrlBFF } from '../routing/route-helpers'; import { captureException } from '../services/monitoring'; @@ -172,3 +173,7 @@ export function createLogoutHandler( return res.redirect(postLogoutRedirectUrl); }; } + +export function isValidProfileType(profileType: unknown) { + return PROFILE_TYPES.includes(profileType as ProfileType); +} diff --git a/src/server/config/source-api.ts b/src/server/config/source-api.ts index d4404aff7d..e188d1d948 100644 --- a/src/server/config/source-api.ts +++ b/src/server/config/source-api.ts @@ -12,7 +12,6 @@ export interface DataRequestConfig extends AxiosRequestConfig { cacheTimeout?: number; cancelTimeout?: number; postponeFetch?: boolean; - urls?: Record; // Construct an url that will be assigned to the url key in the local requestConfig. // Example: formatUrl: (requestConfig) => requestConfig.url + '/some/additional/path/segments/, @@ -205,11 +204,7 @@ export const ApiConfig: ApiDataRequestConfig = { CMS_CONTENT_GENERAL_INFO: { // eslint-disable-next-line no-magic-numbers cacheTimeout: 4 * ONE_HOUR_MS, - urls: { - private: `${getFromEnv('BFF_CMS_BASE_URL')}/mijn-content/artikelen/ziet-amsterdam/?AppIdt=app-data`, - 'private-attributes': `${getFromEnv('BFF_CMS_BASE_URL')}/mijn-content/artikelen/ziet-amsterdam/?AppIdt=app-data`, - commercial: `${getFromEnv('BFF_CMS_BASE_URL')}/mijn-content/artikelen/overzicht-producten-ondernemers/?AppIdt=app-data`, - }, + url: `${getFromEnv('BFF_CMS_BASE_URL')}/mijn-content/artikelen`, }, CMS_CONTENT_FOOTER: { url: `${getFromEnv('BFF_CMS_BASE_URL')}/algemene_onderdelen/overige/footer/?AppIdt=app-data`, diff --git a/src/server/services/afis/afis-facturen.test.ts b/src/server/services/afis/afis-facturen.test.ts index 04ebecfcb3..acbc77208f 100644 --- a/src/server/services/afis/afis-facturen.test.ts +++ b/src/server/services/afis/afis-facturen.test.ts @@ -71,7 +71,7 @@ const ROUTES = { }, deelbetalingen: (uri: string) => decodeURI(uri).includes( - `IsCleared eq false and InvoiceReference ne '' and (AccountingDocumentType eq 'AB')` + `IsCleared eq false and InvoiceReference ne '' and` ), }; @@ -699,13 +699,7 @@ describe('afis-facturen', async () => { }, }; - remoteApi - .get((uri) => - decodeURI(uri).includes( - `IsCleared eq false and InvoiceReference ne '' and (AccountingDocumentType eq 'AB')` - ) - ) - .reply(200, deelbetalingenResponse); + remoteApi.get(ROUTES.deelbetalingen).reply(200, deelbetalingenResponse); const params: AfisFacturenParams = { state: 'deelbetalingen', diff --git a/src/server/services/afis/afis-facturen.ts b/src/server/services/afis/afis-facturen.ts index 856ab1ec1f..1e8721f22c 100644 --- a/src/server/services/afis/afis-facturen.ts +++ b/src/server/services/afis/afis-facturen.ts @@ -56,7 +56,7 @@ const accountingDocumentTypesByState: Record< open: ['DR', 'DG', 'DM', 'DE', 'DF', 'DV', 'DW'], afgehandeld: ['DR', 'DE', 'DM', 'DV', 'DG', 'DF', 'DM', 'DW'], overgedragen: ['DR', 'DE', 'DM', 'DV', 'DG', 'DF', 'DM', 'DW'], - deelbetalingen: ['AB'], + deelbetalingen: ['AB', 'BA'], }; const select = `$select=IsCleared,ReverseDocument,Paylink,PostingDate,ProfitCenterName,DocumentReferenceID,AccountingDocument,AmountInBalanceTransacCrcy,NetDueDate,DunningLevel,DunningBlockingReason,SEPAMandate,ClearingDate,PaymentMethod`; diff --git a/src/server/services/cms-content.ts b/src/server/services/cms-content.ts index 530ef4c4eb..912819cfc6 100644 --- a/src/server/services/cms-content.ts +++ b/src/server/services/cms-content.ts @@ -15,7 +15,7 @@ import { } from '../../universal/helpers/api'; import { hash } from '../../universal/helpers/utils'; import { LinkProps } from '../../universal/types/App.types'; -import { DataRequestConfig } from '../config/source-api'; +import { isValidProfileType } from '../auth/auth-helpers'; import FileCache from '../helpers/file-cache'; import { getApiConfig } from '../helpers/source-api-helpers'; import { requestData } from '../helpers/source-api-request'; @@ -211,14 +211,14 @@ async function getGeneralPage( sanitizeCmsContent(responseData.applicatie.inhoud.tekst), }; }, + formatUrl({ url }) { + return profileType === 'commercial' + ? `${url}/overzicht-producten-ondernemers/?AppIdt=app-data` + : `${url}/ziet-amsterdam/?AppIdt=app-data`; + }, }); - const requestConfigFinal: DataRequestConfig = { - ...requestConfig, - url: requestConfig.urls![profileType], - }; - - return requestData(requestConfigFinal, requestID).then( + return requestData(requestConfig, requestID).then( (apiData) => { if ( apiData.status === 'OK' && @@ -285,11 +285,14 @@ async function fetchCmsBase( requestID: RequestID, query?: QueryParamsCMSFooter ) { - const forceRenew = !!(query?.forceRenew === 'true'); - + const forceRenew = query?.forceRenew === 'true'; + const profileType = + query?.profileType && isValidProfileType(query?.profileType) + ? query.profileType + : undefined; const generalInfoPageRequest = getGeneralPage( requestID, - query?.profileType as ProfileType, + profileType, forceRenew ); @@ -310,10 +313,10 @@ async function fetchCmsBase( }; } -export interface QueryParamsCMSFooter extends Record { - forceRenew: 'true'; - profileType: ProfileType; -} +export type QueryParamsCMSFooter = { + forceRenew?: 'true'; + profileType?: ProfileType; +}; export async function fetchCmsFooter( requestID: RequestID, diff --git a/src/server/services/controller.test.ts b/src/server/services/controller.test.ts index 2da3e1362c..6a8661f148 100644 --- a/src/server/services/controller.test.ts +++ b/src/server/services/controller.test.ts @@ -9,13 +9,19 @@ import { vi, } from 'vitest'; +import { fetchCMSCONTENT } from './cms-content'; import { addServiceResultHandler, + forTesting, getServiceResultsForTips, getTipNotifications, servicesTipsByProfileType, } from './controller'; -import { getReqMockWithOidc, ResponseMock } from '../../testing/utils'; +import { + getReqMockWithOidc, + RequestMock, + ResponseMock, +} from '../../testing/utils'; const mocks = vi.hoisted(() => { return { @@ -35,6 +41,12 @@ const mocks = vi.hoisted(() => { }; }); +vi.mock('./cms-content', () => { + return { + fetchCMSCONTENT: vi.fn(), + }; +}); + vi.mock('./tips-and-notifications', async () => { return { getTipsAndNotificationsFromApiResults: vi.fn(), @@ -192,3 +204,57 @@ describe('controller', () => { expect(result).toEqual(data); }); }); + +describe('request handlers', () => { + describe('CMS_CONTENT', async () => { + const reqID = 'xx-req-id-yy'; + + test('profileType: private', async () => { + const reqMock = await getReqMockWithOidc({ + sid: 'x123y', + authMethod: 'digid', + profileType: 'private', + id: '9988', + }); + + await forTesting.CMS_CONTENT(reqID, reqMock); + + expect(fetchCMSCONTENT).toHaveBeenCalledWith(reqID, { + profileType: 'private', + }); + }); + + test('profileType: commercial', async () => { + const reqMock = await getReqMockWithOidc({ + sid: 'x123y', + authMethod: 'eherkenning', + profileType: 'commercial', + id: '9988', + }); + + await forTesting.CMS_CONTENT(reqID, reqMock); + + expect(fetchCMSCONTENT).toHaveBeenCalledWith(reqID, { + profileType: 'commercial', + }); + }); + + test('arbitrary query params are passed', async () => { + const reqMock = await getReqMockWithOidc({ + sid: 'x123y', + authMethod: 'eherkenning', + profileType: 'commercial', + id: '9988', + }); + + (reqMock as unknown as RequestMock).setQuery({ forceRenew: 'true' }); + + await forTesting.CMS_CONTENT(reqID, reqMock); + + expect(fetchCMSCONTENT).toHaveBeenCalledWith(reqID, { + profileType: 'commercial', + forceRenew: 'true', + }); + }); + }); +}); diff --git a/src/server/services/controller.ts b/src/server/services/controller.ts index 8bc047d20d..4931c5c4db 100644 --- a/src/server/services/controller.ts +++ b/src/server/services/controller.ts @@ -117,7 +117,13 @@ export function addServiceResultHandler( * The service methods */ // Public services -const CMS_CONTENT = callPublicService(fetchCMSCONTENT); +const CMS_CONTENT = (requestID: RequestID, req: Request) => { + const auth = getAuth(req); + return fetchCMSCONTENT(requestID, { + profileType: auth?.profile.profileType, + ...queryParams(req), + }); +}; const CMS_MAINTENANCE_NOTIFICATIONS = callPublicService( fetchMaintenanceNotificationsActual ); @@ -485,3 +491,7 @@ export async function getTipNotifications( return []; } + +export const forTesting = { + CMS_CONTENT, +}; diff --git a/src/universal/types/App.types.ts b/src/universal/types/App.types.ts index 31a6b7015b..910c3f5182 100644 --- a/src/universal/types/App.types.ts +++ b/src/universal/types/App.types.ts @@ -122,3 +122,16 @@ export interface Match { path: string; url: string; } + +export const PROFILE_TYPES = [ + 'private', + 'commercial', + 'private-attributes', +] as const; + +export const AUTH_METHODS = ['eherkenning', 'digid'] as const; + +declare global { + type ProfileType = (typeof PROFILE_TYPES)[number]; + type AuthMethod = (typeof AUTH_METHODS)[number]; +}