From 57e721b88c566bc1a1abc91112cf93bc6287741b Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Mon, 2 Mar 2026 21:19:33 +0000 Subject: [PATCH 01/11] prefixing for converted event --- .../ui/data/api-data/createCase.api.data.ts | 1 + .../custom-actions/createCaseAPI.action.ts | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/test/ui/data/api-data/createCase.api.data.ts b/src/test/ui/data/api-data/createCase.api.data.ts index cd70355e6..ec7c562fd 100644 --- a/src/test/ui/data/api-data/createCase.api.data.ts +++ b/src/test/ui/data/api-data/createCase.api.data.ts @@ -1,5 +1,6 @@ export const createCaseApiData = { createCaseEventName: 'createPossessionClaim', + createCaseDataPrefix: 'cpc', createCasePayload: { feeAmount: '£404', propertyAddress: { diff --git a/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts b/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts index 854757af6..c12cb508d 100644 --- a/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts +++ b/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts @@ -13,6 +13,22 @@ import { IAction, actionData, actionRecord } from '../../interfaces'; export const caseInfo: { id: string; fid: string; state: string } = { id: '', fid: '', state: '' }; export class CreateCaseAPIAction implements IAction { + private mapToPrefixedEventData(data: actionData): actionData { + if (typeof data !== 'object' || data === null || Array.isArray(data)) { + return data; + } + + const prefix = createCaseApiData.createCaseDataPrefix; + if (!prefix) { + return data; + } + + const typedData = data as Record; + return Object.fromEntries( + Object.entries(typedData).map(([key, value]) => [prefix + key.charAt(0).toUpperCase() + key.slice(1), value]) + ); + } + async execute(page: Page, action: string, fieldName: actionData | actionRecord): Promise { const actionsMap = new Map Promise>([ ['createCaseAPI', () => this.createCaseAPI(fieldName)], @@ -30,8 +46,9 @@ export class CreateCaseAPIAction implements IAction { const CREATE_EVENT_TOKEN = (await createCaseApi.get(createCaseEventTokenApiData.createCaseEventTokenApiEndPoint)) .data.token; const createCasePayloadData = typeof caseData === 'object' && 'data' in caseData ? caseData.data : caseData; + const createCaseEventData = this.mapToPrefixedEventData(createCasePayloadData); const createResponse = await createCaseApi.post(createCaseApiData.createCaseApiEndPoint, { - data: createCasePayloadData, + data: createCaseEventData, event: { id: createCaseApiData.createCaseEventName }, event_token: CREATE_EVENT_TOKEN, }); From b42e3cdb74ba568c8d3f19773fd041833a95c02b Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Tue, 3 Mar 2026 16:43:06 +0000 Subject: [PATCH 02/11] add generated poc ts client for create possession claim --- .gradle/yarn/yarn-latest/bin/yarn | 1 + .gradle/yarn/yarn-latest/bin/yarnpkg | 1 + .../createPossessionClaimClientExample.ts | 74 ++++++++++ src/main/generated/ccd/PCS/client.ts | 127 ++++++++++++++++++ src/main/generated/ccd/PCS/dto-types.ts | 43 ++++++ src/main/generated/ccd/PCS/event-contracts.ts | 16 +++ src/main/generated/ccd/PCS/index.ts | 4 + src/main/modules/oidc/oidc.ts | 4 +- .../ui/data/api-data/createCase.api.data.ts | 31 +++-- 9 files changed, 286 insertions(+), 15 deletions(-) create mode 120000 .gradle/yarn/yarn-latest/bin/yarn create mode 120000 .gradle/yarn/yarn-latest/bin/yarnpkg create mode 100644 src/main/examples/createPossessionClaimClientExample.ts create mode 100644 src/main/generated/ccd/PCS/client.ts create mode 100644 src/main/generated/ccd/PCS/dto-types.ts create mode 100644 src/main/generated/ccd/PCS/event-contracts.ts create mode 100644 src/main/generated/ccd/PCS/index.ts diff --git a/.gradle/yarn/yarn-latest/bin/yarn b/.gradle/yarn/yarn-latest/bin/yarn new file mode 120000 index 000000000..0d279c302 --- /dev/null +++ b/.gradle/yarn/yarn-latest/bin/yarn @@ -0,0 +1 @@ +../lib/node_modules/yarn/bin/yarn.js \ No newline at end of file diff --git a/.gradle/yarn/yarn-latest/bin/yarnpkg b/.gradle/yarn/yarn-latest/bin/yarnpkg new file mode 120000 index 000000000..0d279c302 --- /dev/null +++ b/.gradle/yarn/yarn-latest/bin/yarnpkg @@ -0,0 +1 @@ +../lib/node_modules/yarn/bin/yarn.js \ No newline at end of file diff --git a/src/main/examples/createPossessionClaimClientExample.ts b/src/main/examples/createPossessionClaimClientExample.ts new file mode 100644 index 000000000..84ab33b5f --- /dev/null +++ b/src/main/examples/createPossessionClaimClientExample.ts @@ -0,0 +1,74 @@ +// Example usage of generated CCD client for createPossessionClaim. +// This module is for developer reference and is not wired into runtime routes. +// eslint-disable-next-line import/no-named-as-default +import Axios, { AxiosInstance } from 'axios'; + +import { + CcdClientConfig, + CcdTransport, + CreateClaimData, + GeneratedCcdClient, +} from '../generated/ccd/PCS'; + +function createTransport(api: AxiosInstance): CcdTransport { + return { + get: async (url, headers) => (await api.get(url, { headers })).data, + post: async (url, data, headers) => (await api.post(url, data, { headers })).data, + }; +} + +function createClient( + baseUrl: string, + caseTypeId: string, + bearerToken: string, + serviceToken: string +): GeneratedCcdClient { + const api = Axios.create({ + baseURL: baseUrl, + headers: { + Authorization: `Bearer ${bearerToken}`, + ServiceAuthorization: `Bearer ${serviceToken}`, + 'Content-Type': 'application/json', + experimental: 'experimental', + Accept: '*/*', + }, + }); + + const clientConfig: CcdClientConfig = { + baseUrl, + caseTypeId, + getAuthHeaders: () => ({}), + transport: createTransport(api), + }; + + return new GeneratedCcdClient(clientConfig); +} + +export async function createPossessionClaimExample( + baseUrl: string, + caseTypeId: string, + bearerToken: string, + serviceToken: string +): Promise { + const client = createClient(baseUrl, caseTypeId, bearerToken, serviceToken); + + // 1) Start event to get token + current event data. + const flow = await client.events.createPossessionClaim.start(); + + // If the event defines an about-to-start callback, any pre-populated values are returned. + const startData: CreateClaimData = flow.data; + + // payload is typed + startData.feeAmount = '£12345'; + startData.propertyAddress = { + AddressLine1: '2 Second Avenue', + AddressLine2: '', + AddressLine3: '', + PostTown: 'London', + County: '', + PostCode: 'W3 7RX', + Country: 'United Kingdom', + }; + + await flow.submit(startData); +} diff --git a/src/main/generated/ccd/PCS/client.ts b/src/main/generated/ccd/PCS/client.ts new file mode 100644 index 000000000..1a7929dd3 --- /dev/null +++ b/src/main/generated/ccd/PCS/client.ts @@ -0,0 +1,127 @@ +// Generated by CCD SDK. Do not edit manually. +import { eventContracts, type EventDtoMap, type EventId } from "./event-contracts"; + +export interface CcdTransport { + get(url: string, headers: Record): Promise; + post(url: string, data: unknown, headers: Record): Promise; +} + +export interface CcdClientConfig { + baseUrl: string; + caseTypeId: string; + getAuthHeaders: () => Promise> | Record; + transport: CcdTransport; +} + +export interface EventFlow { + readonly eventId: K; + readonly data: EventDtoMap[K]; + submit(data: EventDtoMap[K]): Promise; +} + +interface EventTriggerResponse { + token?: string; + case_details?: { + case_data?: Record; + }; +} + +type EventAccessor = { + [K in EventId]: { + start(caseId?: string | number): Promise>; + }; +}; + +export class GeneratedCcdClient { + readonly events: EventAccessor; + + constructor(private readonly config: CcdClientConfig) { + this.events = this.buildEvents(); + } + + private buildEvents(): EventAccessor { + const entries = Object.keys(eventContracts).map(eventId => [ + eventId, + { + start: (caseId?: string | number) => this.start(eventId as EventId, caseId), + }, + ]); + return Object.fromEntries(entries) as EventAccessor; + } + + private async start(eventId: K, caseId?: string | number): Promise> { + const headers = await this.config.getAuthHeaders(); + const triggerUrl = this.buildEventTriggerUrl(eventId, caseId); + const triggerResponse = (await this.config.transport.get(triggerUrl, headers)) as EventTriggerResponse; + const token = triggerResponse.token; + if (!token) { + throw new Error(`Missing event token for ${String(eventId)}`); + } + + const data = this.unmarshal(eventId, triggerResponse.case_details?.case_data ?? {}); + + return { + eventId, + data, + submit: (updatedData: EventDtoMap[K]) => this.submit(eventId, token, updatedData, headers, caseId), + }; + } + + private async submit( + eventId: K, + eventToken: string, + data: EventDtoMap[K], + headers: Record, + caseId?: string | number + ): Promise { + const payload = { + data: this.marshal(eventId, data), + event: { id: eventId }, + event_token: eventToken, + ignore_warning: false, + }; + return this.config.transport.post(this.buildEventSubmitUrl(caseId), payload, headers); + } + + private buildEventTriggerUrl(eventId: EventId, caseId?: string | number): string { + if (caseId) { + return `${this.config.baseUrl}/cases/${caseId}/event-triggers/${eventId}?ignore-warning=false`; + } + return `${this.config.baseUrl}/case-types/${this.config.caseTypeId}/event-triggers/${eventId}`; + } + + private buildEventSubmitUrl(caseId?: string | number): string { + if (caseId) { + return `${this.config.baseUrl}/cases/${caseId}/events`; + } + return `${this.config.baseUrl}/case-types/${this.config.caseTypeId}/cases`; + } + + private marshal(eventId: K, data: EventDtoMap[K]): Record { + const { prefix, fields } = eventContracts[eventId]; + const source = data as unknown as Record; + const marshalled: Record = {}; + for (const field of fields) { + if (Object.prototype.hasOwnProperty.call(source, field)) { + marshalled[prefix + capitalise(field)] = source[field]; + } + } + return marshalled; + } + + private unmarshal(eventId: K, ccdData: Record): EventDtoMap[K] { + const { prefix, fields } = eventContracts[eventId]; + const unmarshalled: Record = {}; + for (const field of fields) { + const prefixedField = prefix + capitalise(field); + if (Object.prototype.hasOwnProperty.call(ccdData, prefixedField)) { + unmarshalled[field] = ccdData[prefixedField]; + } + } + return unmarshalled as unknown as EventDtoMap[K]; + } +} + +function capitalise(value: string): string { + return value.length === 0 ? value : value[0].toUpperCase() + value.slice(1); +} diff --git a/src/main/generated/ccd/PCS/dto-types.ts b/src/main/generated/ccd/PCS/dto-types.ts new file mode 100644 index 000000000..c24fcdee1 --- /dev/null +++ b/src/main/generated/ccd/PCS/dto-types.ts @@ -0,0 +1,43 @@ +/* tslint:disable */ +/* eslint-disable */ + +export interface CreateClaimData { + propertyAddress: AddressUK; + legislativeCountry: LegislativeCountry; + feeAmount: string; + showCrossBorderPage: YesOrNo; + showPropertyNotEligiblePage: YesOrNo; + showPostcodeNotAssignedToCourt: YesOrNo; + crossBorderCountriesList: DynamicStringList; + crossBorderCountry1: string; + crossBorderCountry2: string; + postcodeNotAssignedView: string; +} + +export interface AddressUK extends Address { +} + +export interface DynamicStringList { + value: DynamicStringListElement; + list_items: DynamicStringListElement[]; + valueCode: string; +} + +export interface Address { + AddressLine1: string; + AddressLine2: string; + AddressLine3: string; + PostTown: string; + County: string; + PostCode: string; + Country: string; +} + +export interface DynamicStringListElement { + code: string; + label: string; +} + +export type LegislativeCountry = "England" | "Northern Ireland" | "Scotland" | "Wales" | "Isle of Man" | "Channel Islands"; + +export type YesOrNo = "Yes" | "No"; diff --git a/src/main/generated/ccd/PCS/event-contracts.ts b/src/main/generated/ccd/PCS/event-contracts.ts new file mode 100644 index 000000000..768c8efaf --- /dev/null +++ b/src/main/generated/ccd/PCS/event-contracts.ts @@ -0,0 +1,16 @@ +// Generated by CCD SDK. Do not edit manually. +// Module: pcs +import type { CreateClaimData } from "./dto-types"; + +export interface EventDtoMap { + "createPossessionClaim": CreateClaimData; +} + +export type EventId = keyof EventDtoMap; + +export const eventContracts = { + "createPossessionClaim": { + prefix: "cpc", + fields: ["crossBorderCountriesList", "crossBorderCountry1", "crossBorderCountry2", "feeAmount", "legislativeCountry", "postcodeNotAssignedView", "propertyAddress", "showCrossBorderPage", "showPostcodeNotAssignedToCourt", "showPropertyNotEligiblePage"], + }, +} as const; diff --git a/src/main/generated/ccd/PCS/index.ts b/src/main/generated/ccd/PCS/index.ts new file mode 100644 index 000000000..149e536f1 --- /dev/null +++ b/src/main/generated/ccd/PCS/index.ts @@ -0,0 +1,4 @@ +// Generated by CCD SDK. Do not edit manually. +export * from "./dto-types"; +export * from "./event-contracts"; +export * from "./client"; diff --git a/src/main/modules/oidc/oidc.ts b/src/main/modules/oidc/oidc.ts index f903a110c..81acf4f2d 100644 --- a/src/main/modules/oidc/oidc.ts +++ b/src/main/modules/oidc/oidc.ts @@ -27,9 +27,11 @@ export class OIDCModule { // Create client with the actual issuer const clientId = this.oidcConfig.clientId; const clientSecret = config.get('secrets.pcs.pcs-frontend-idam-secret'); + const discoveryOptions = + issuer.protocol === 'http:' ? { execute: [client.allowInsecureRequests] } : undefined; // Create the client configuration with the server discovery - this.clientConfig = await client.discovery(issuer, clientId, clientSecret); + this.clientConfig = await client.discovery(issuer, clientId, clientSecret, undefined, discoveryOptions); } catch (error) { this.logger.error('Failed to setup OIDC client:', error); throw new OIDCAuthenticationError('Failed to initialize OIDC client'); diff --git a/src/test/ui/data/api-data/createCase.api.data.ts b/src/test/ui/data/api-data/createCase.api.data.ts index ec7c562fd..ca917a019 100644 --- a/src/test/ui/data/api-data/createCase.api.data.ts +++ b/src/test/ui/data/api-data/createCase.api.data.ts @@ -1,19 +1,22 @@ +import type { CreateClaimData } from '../../../../main/generated/ccd/PCS'; + +const createCasePayload = { + feeAmount: '£404', + propertyAddress: { + AddressLine1: '2 Second Avenue', + AddressLine2: '', + AddressLine3: '', + PostTown: 'London', + County: '', + PostCode: 'W3 7RX', + Country: 'United Kingdom', + }, + legislativeCountry: 'England', +} satisfies Partial; + export const createCaseApiData = { createCaseEventName: 'createPossessionClaim', - createCaseDataPrefix: 'cpc', - createCasePayload: { - feeAmount: '£404', - propertyAddress: { - AddressLine1: '2 Second Avenue', - AddressLine2: '', - AddressLine3: '', - PostTown: 'London', - County: '', - PostCode: 'W3 7RX', - Country: 'United Kingdom', - }, - legislativeCountry: 'England', - }, + createCasePayload, createCaseApiEndPoint: `/case-types/PCS${ process.env.PCS_API_CHANGE_ID ? '-' + process.env.PCS_API_CHANGE_ID : '' }/cases`, From 6d421c6e3e87adb3a32fed4218abc555f9f49119 Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Tue, 17 Mar 2026 01:22:26 +0000 Subject: [PATCH 03/11] Use generated client for defendant response --- jest.config.js | 3 +- package.json | 2 + .../createPossessionClaimClientExample.ts | 23 ++- src/main/generated/ccd/PCS/client.ts | 127 ------------- src/main/generated/ccd/PCS/dto-types.ts | 5 + src/main/generated/ccd/PCS/event-contracts.ts | 21 ++- src/main/generated/ccd/PCS/index.ts | 1 - src/main/interfaces/ccdCase.interface.ts | 24 --- src/main/modules/oidc/oidc.ts | 4 +- src/main/services/ccdCaseService.ts | 112 +++++++++-- .../correspondence-address/index.ts | 63 ++++--- .../populateResponseToClaimPayloadmap.ts | 24 +-- .../custom-actions/createCaseAPI.action.ts | 82 +++++--- .../unit/generated/pcs.case-bindings.test.ts | 21 +++ src/test/unit/modules/oidc/oidc.test.ts | 10 +- src/test/unit/services/ccdCaseService.test.ts | 176 +++++++++++++++++- src/test/unit/ui/createCaseAPI.action.test.ts | 76 ++++++++ tsconfig.generated-consumers.json | 18 ++ yarn.lock | 8 + 19 files changed, 527 insertions(+), 273 deletions(-) delete mode 100644 src/main/generated/ccd/PCS/client.ts create mode 100644 src/test/unit/generated/pcs.case-bindings.test.ts create mode 100644 src/test/unit/ui/createCaseAPI.action.test.ts create mode 100644 tsconfig.generated-consumers.json diff --git a/jest.config.js b/jest.config.js index dd7a4008f..f84f0757f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,6 +7,7 @@ module.exports = { '^.+\\.ts?$': 'ts-jest', }, moduleNameMapper: { + '^@hmcts/ccd-event-runtime$': '/../../sdk/ccd-event-runtime/src/index.ts', '^openid-client$': '/src/test/unit/modules/oidc/__mocks__/openid-client.ts', '^steps$': '/src/main/steps', '^app/(.*)$': '/src/main/app/$1', @@ -15,5 +16,5 @@ module.exports = { }, testPathIgnorePatterns: ['/__mocks__/'], coverageProvider: 'v8', - transformIgnorePatterns: ['node_modules/(?!(jose|@panva|oidc-token-hash)/)'], + transformIgnorePatterns: ['node_modules/(?!(jose|@panva|oidc-token-hash|@hmcts/ccd-event-runtime)/)'], }; diff --git a/package.json b/package.json index 9c616311e..8caeeb342 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "test:unit": "jest", "test:coverage": "jest --coverage", "test:routes": "jest -c jest.routes.config.ts", + "typecheck:generated-consumers": "tsc --noEmit -p tsconfig.generated-consumers.json", "test:a11y": "echo 'Accessibility tests implemented later in the pipeline'", "test:accessibility": "yarn playwright install && yarn playwright test --project chromium --grep @accessibility; EXIT_CODE=$?; allure generate --clean; ts-node src/test/ui/config/clean-attachments.config.ts; exit $EXIT_CODE", "test:smoke": "NODE_TLS_REJECT_UNAUTHORIZED=0 jest -c jest.smoke.config.ts", @@ -39,6 +40,7 @@ "prepare": "husky" }, "dependencies": { + "@hmcts/ccd-event-runtime": "file:../../sdk/ccd-event-runtime", "@hmcts/info-provider": "^1.1.0", "@hmcts/nodejs-healthcheck": "^1.8.0", "@hmcts/nodejs-logging": "^4.0.4", diff --git a/src/main/examples/createPossessionClaimClientExample.ts b/src/main/examples/createPossessionClaimClientExample.ts index 84ab33b5f..191f6b925 100644 --- a/src/main/examples/createPossessionClaimClientExample.ts +++ b/src/main/examples/createPossessionClaimClientExample.ts @@ -2,12 +2,11 @@ // This module is for developer reference and is not wired into runtime routes. // eslint-disable-next-line import/no-named-as-default import Axios, { AxiosInstance } from 'axios'; +import { createCcdClient, type CcdClientConfig, type CcdTransport } from '@hmcts/ccd-event-runtime'; import { - CcdClientConfig, - CcdTransport, CreateClaimData, - GeneratedCcdClient, + caseBindings, } from '../generated/ccd/PCS'; function createTransport(api: AxiosInstance): CcdTransport { @@ -22,7 +21,7 @@ function createClient( caseTypeId: string, bearerToken: string, serviceToken: string -): GeneratedCcdClient { +) { const api = Axios.create({ baseURL: baseUrl, headers: { @@ -41,7 +40,7 @@ function createClient( transport: createTransport(api), }; - return new GeneratedCcdClient(clientConfig); + return createCcdClient(clientConfig, caseBindings); } export async function createPossessionClaimExample( @@ -53,7 +52,7 @@ export async function createPossessionClaimExample( const client = createClient(baseUrl, caseTypeId, bearerToken, serviceToken); // 1) Start event to get token + current event data. - const flow = await client.events.createPossessionClaim.start(); + const flow = await client.event('createPossessionClaim').start(); // If the event defines an about-to-start callback, any pre-populated values are returned. const startData: CreateClaimData = flow.data; @@ -62,12 +61,12 @@ export async function createPossessionClaimExample( startData.feeAmount = '£12345'; startData.propertyAddress = { AddressLine1: '2 Second Avenue', - AddressLine2: '', - AddressLine3: '', - PostTown: 'London', - County: '', - PostCode: 'W3 7RX', - Country: 'United Kingdom', + AddressLine2: '', + AddressLine3: '', + PostTown: 'London', + County: '', + PostCode: 'W3 7RX', + Country: 'United Kingdom', }; await flow.submit(startData); diff --git a/src/main/generated/ccd/PCS/client.ts b/src/main/generated/ccd/PCS/client.ts deleted file mode 100644 index 1a7929dd3..000000000 --- a/src/main/generated/ccd/PCS/client.ts +++ /dev/null @@ -1,127 +0,0 @@ -// Generated by CCD SDK. Do not edit manually. -import { eventContracts, type EventDtoMap, type EventId } from "./event-contracts"; - -export interface CcdTransport { - get(url: string, headers: Record): Promise; - post(url: string, data: unknown, headers: Record): Promise; -} - -export interface CcdClientConfig { - baseUrl: string; - caseTypeId: string; - getAuthHeaders: () => Promise> | Record; - transport: CcdTransport; -} - -export interface EventFlow { - readonly eventId: K; - readonly data: EventDtoMap[K]; - submit(data: EventDtoMap[K]): Promise; -} - -interface EventTriggerResponse { - token?: string; - case_details?: { - case_data?: Record; - }; -} - -type EventAccessor = { - [K in EventId]: { - start(caseId?: string | number): Promise>; - }; -}; - -export class GeneratedCcdClient { - readonly events: EventAccessor; - - constructor(private readonly config: CcdClientConfig) { - this.events = this.buildEvents(); - } - - private buildEvents(): EventAccessor { - const entries = Object.keys(eventContracts).map(eventId => [ - eventId, - { - start: (caseId?: string | number) => this.start(eventId as EventId, caseId), - }, - ]); - return Object.fromEntries(entries) as EventAccessor; - } - - private async start(eventId: K, caseId?: string | number): Promise> { - const headers = await this.config.getAuthHeaders(); - const triggerUrl = this.buildEventTriggerUrl(eventId, caseId); - const triggerResponse = (await this.config.transport.get(triggerUrl, headers)) as EventTriggerResponse; - const token = triggerResponse.token; - if (!token) { - throw new Error(`Missing event token for ${String(eventId)}`); - } - - const data = this.unmarshal(eventId, triggerResponse.case_details?.case_data ?? {}); - - return { - eventId, - data, - submit: (updatedData: EventDtoMap[K]) => this.submit(eventId, token, updatedData, headers, caseId), - }; - } - - private async submit( - eventId: K, - eventToken: string, - data: EventDtoMap[K], - headers: Record, - caseId?: string | number - ): Promise { - const payload = { - data: this.marshal(eventId, data), - event: { id: eventId }, - event_token: eventToken, - ignore_warning: false, - }; - return this.config.transport.post(this.buildEventSubmitUrl(caseId), payload, headers); - } - - private buildEventTriggerUrl(eventId: EventId, caseId?: string | number): string { - if (caseId) { - return `${this.config.baseUrl}/cases/${caseId}/event-triggers/${eventId}?ignore-warning=false`; - } - return `${this.config.baseUrl}/case-types/${this.config.caseTypeId}/event-triggers/${eventId}`; - } - - private buildEventSubmitUrl(caseId?: string | number): string { - if (caseId) { - return `${this.config.baseUrl}/cases/${caseId}/events`; - } - return `${this.config.baseUrl}/case-types/${this.config.caseTypeId}/cases`; - } - - private marshal(eventId: K, data: EventDtoMap[K]): Record { - const { prefix, fields } = eventContracts[eventId]; - const source = data as unknown as Record; - const marshalled: Record = {}; - for (const field of fields) { - if (Object.prototype.hasOwnProperty.call(source, field)) { - marshalled[prefix + capitalise(field)] = source[field]; - } - } - return marshalled; - } - - private unmarshal(eventId: K, ccdData: Record): EventDtoMap[K] { - const { prefix, fields } = eventContracts[eventId]; - const unmarshalled: Record = {}; - for (const field of fields) { - const prefixedField = prefix + capitalise(field); - if (Object.prototype.hasOwnProperty.call(ccdData, prefixedField)) { - unmarshalled[field] = ccdData[prefixedField]; - } - } - return unmarshalled as unknown as EventDtoMap[K]; - } -} - -function capitalise(value: string): string { - return value.length === 0 ? value : value[0].toUpperCase() + value.slice(1); -} diff --git a/src/main/generated/ccd/PCS/dto-types.ts b/src/main/generated/ccd/PCS/dto-types.ts index c24fcdee1..c7dc381b9 100644 --- a/src/main/generated/ccd/PCS/dto-types.ts +++ b/src/main/generated/ccd/PCS/dto-types.ts @@ -14,6 +14,11 @@ export interface CreateClaimData { postcodeNotAssignedView: string; } +export interface SubmitDefendantResponseData { + correspondenceAddress: AddressUK; + submitDraftAnswers: YesOrNo; +} + export interface AddressUK extends Address { } diff --git a/src/main/generated/ccd/PCS/event-contracts.ts b/src/main/generated/ccd/PCS/event-contracts.ts index 768c8efaf..0e4de5e5e 100644 --- a/src/main/generated/ccd/PCS/event-contracts.ts +++ b/src/main/generated/ccd/PCS/event-contracts.ts @@ -1,16 +1,25 @@ // Generated by CCD SDK. Do not edit manually. // Module: pcs -import type { CreateClaimData } from "./dto-types"; +import { defineCaseBindings, type CcdCaseBindings } from "@hmcts/ccd-event-runtime"; +import type { CreateClaimData, SubmitDefendantResponseData } from "./dto-types"; export interface EventDtoMap { "createPossessionClaim": CreateClaimData; + "submitDefendantResponse": SubmitDefendantResponseData; } -export type EventId = keyof EventDtoMap; - -export const eventContracts = { +export const caseBindings = defineCaseBindings({ + caseTypeId: "PCS", + events: { "createPossessionClaim": { - prefix: "cpc", + fieldNamespace: "claim.create", fields: ["crossBorderCountriesList", "crossBorderCountry1", "crossBorderCountry2", "feeAmount", "legislativeCountry", "postcodeNotAssignedView", "propertyAddress", "showCrossBorderPage", "showPostcodeNotAssignedToCourt", "showPropertyNotEligiblePage"], + pages: ["crossBorderPostcodeSelection", "enterPropertyAddress", "postcodeNotAssignedToCourt", "propertyNotEligible", "startTheService"], + }, + "submitDefendantResponse": { + fieldNamespace: "resp.def", + fields: ["correspondenceAddress", "submitDraftAnswers"], + pages: [], + }, }, -} as const; +} satisfies CcdCaseBindings); diff --git a/src/main/generated/ccd/PCS/index.ts b/src/main/generated/ccd/PCS/index.ts index 149e536f1..e8134728c 100644 --- a/src/main/generated/ccd/PCS/index.ts +++ b/src/main/generated/ccd/PCS/index.ts @@ -1,4 +1,3 @@ // Generated by CCD SDK. Do not edit manually. export * from "./dto-types"; export * from "./event-contracts"; -export * from "./client"; diff --git a/src/main/interfaces/ccdCase.interface.ts b/src/main/interfaces/ccdCase.interface.ts index 623be243f..240e665ef 100644 --- a/src/main/interfaces/ccdCase.interface.ts +++ b/src/main/interfaces/ccdCase.interface.ts @@ -32,27 +32,3 @@ export interface Address { PostCode: string; Country?: string; } - -export interface PossessionClaimResponse { - defendantContactDetails: { - party: { - firstName?: string; - lastName?: string; - address?: Address; - }; - }; -} - -export interface StartCallbackData { - case_details: { - case_data: { - possessionClaimResponse?: { - defendantContactDetails?: { - party?: { - address?: Address; - }; - }; - }; - }; - }; -} diff --git a/src/main/modules/oidc/oidc.ts b/src/main/modules/oidc/oidc.ts index 81acf4f2d..554200e91 100644 --- a/src/main/modules/oidc/oidc.ts +++ b/src/main/modules/oidc/oidc.ts @@ -13,7 +13,9 @@ export class OIDCModule { private readonly logger = Logger.getLogger('oidc'); constructor() { - this.setupClient(); + void this.setupClient().catch(error => { + this.logger.error('Initial OIDC client setup failed, middleware will retry on demand:', error); + }); } private async setupClient(): Promise { diff --git a/src/main/services/ccdCaseService.ts b/src/main/services/ccdCaseService.ts index fc2108682..b7c5b51b7 100644 --- a/src/main/services/ccdCaseService.ts +++ b/src/main/services/ccdCaseService.ts @@ -1,10 +1,12 @@ import { Logger } from '@hmcts/nodejs-logging'; +import { createCcdClient, type CcdClientConfig, type CcdSubmitResult, type CcdTransport } from '@hmcts/ccd-event-runtime'; import { AxiosError } from 'axios'; import config from 'config'; +import { caseBindings, type CreateClaimData, type SubmitDefendantResponseData } from '../generated/ccd/PCS'; import { HTTPError } from '../HttpError'; import { CaseState } from '../interfaces/ccdCase.interface'; -import type { CcdCase, CcdUserCases, StartCallbackData } from '../interfaces/ccdCase.interface'; +import type { CcdCase, CcdUserCases } from '../interfaces/ccdCase.interface'; import { http } from '../modules/http'; const logger = Logger.getLogger('ccdCaseService'); @@ -32,6 +34,46 @@ function getCaseHeaders(token: string) { }; } +function getGeneratedClientHeaders(token: string): Record { + return { + Authorization: `Bearer ${token}`, + experimental: 'true', + Accept: '*/*', + 'Content-Type': 'application/json', + }; +} + +function createTransport(): CcdTransport { + return { + get: async (url, headers) => (await http.get(url, { headers })).data, + post: async (url, data, headers) => (await http.post(url, data, { headers })).data, + }; +} + +function createGeneratedClient(userToken: string) { + const clientConfig: CcdClientConfig = { + baseUrl: getBaseUrl(), + caseTypeId: getCaseTypeId(), + getAuthHeaders: () => getGeneratedClientHeaders(userToken), + transport: createTransport(), + }; + + return createCcdClient(clientConfig, caseBindings); +} + +function toRecord(value: unknown): Record { + return typeof value === 'object' && value !== null ? (value as Record) : {}; +} + +function normaliseSubmitResult(result: CcdSubmitResult, fallbackData: Record): CcdCase { + const resultData = toRecord(result.data); + + return { + id: String(result.id ?? ''), + data: Object.keys(resultData).length > 0 ? resultData : fallbackData, + }; +} + function convertAxiosErrorToHttpError(error: unknown, context: string): HTTPError { const axiosError = error as AxiosError; const status = axiosError.response?.status; @@ -89,7 +131,11 @@ async function submitEvent( } export const ccdCaseService = { - async getCaseById(accessToken: string, caseId: string, eventId: string = 'respondPossessionClaim'): Promise { + async getCaseById( + accessToken: string, + caseId: string, + eventId: string = 'submitDefendantResponse' + ): Promise { const eventUrl = `${getBaseUrl()}/cases/${caseId}/event-triggers/${eventId}?ignore-warning=false`; try { @@ -152,11 +198,20 @@ export const ccdCaseService = { } }, - async createCase(accessToken: string | undefined, data: Record): Promise { - const eventUrl = `${getBaseUrl()}/case-types/${getCaseTypeId()}/event-triggers/citizenCreateApplication`; - const eventToken = await getEventToken(accessToken || '', eventUrl); - const url = `${getBaseUrl()}/case-types/${getCaseTypeId()}/cases`; - return submitEvent(accessToken || '', url, 'citizenCreateApplication', eventToken, data); + async createCase(accessToken: string | undefined, data: Partial): Promise { + try { + const client = createGeneratedClient(accessToken || ''); + const flow = await client.event('createPossessionClaim').start(); + const submitData: CreateClaimData = { + ...flow.data, + ...(data as Partial), + }; + const result = await flow.submit(submitData); + + return normaliseSubmitResult(result, submitData as unknown as Record); + } catch (error) { + throw convertAxiosErrorToHttpError(error, 'createCase'); + } }, async updateCase(accessToken: string | undefined, ccdCase: CcdCase): Promise { @@ -180,25 +235,44 @@ export const ccdCaseService = { return submitEvent(accessToken || '', url, 'citizenSubmitApplication', eventToken, ccdCase.data); }, - async submitResponseToClaim(accessToken: string | undefined, ccdCase: CcdCase): Promise { - if (!ccdCase.id) { + async submitResponseToClaim( + accessToken: string | undefined, + ccdCaseId: string, + data: Partial + ): Promise { + if (!ccdCaseId) { throw new HTTPError('Cannot Submit Response to Case, CCD Case Not found', 500); } - const eventUrl = `${getBaseUrl()}/cases/${ccdCase.id}/event-triggers/respondPossessionClaim`; - const eventToken = await getEventToken(accessToken || '', eventUrl); - const url = `${getBaseUrl()}/cases/${ccdCase.id}/events`; - return submitEvent(accessToken || '', url, 'respondPossessionClaim', eventToken, ccdCase.data); + try { + const client = createGeneratedClient(accessToken || ''); + const flow = await client.event('submitDefendantResponse').start(ccdCaseId); + const submitData: SubmitDefendantResponseData = { + ...flow.data, + ...data, + }; + const result = await flow.submit(submitData); + + return normaliseSubmitResult(result, submitData as unknown as Record); + } catch (error) { + throw convertAxiosErrorToHttpError(error, 'submitResponseToClaim'); + } }, - async getExistingCaseData(accessToken: string | undefined, ccdCaseId: string): Promise { - const eventUrl = `${getBaseUrl()}/cases/${ccdCaseId}/event-triggers/respondPossessionClaim?ignore-warning=false`; - logger.info('getExistingCaseData event URL', { eventUrl }); + async getResponseToClaimData( + accessToken: string | undefined, + ccdCaseId: string + ): Promise { + if (!ccdCaseId) { + throw new HTTPError('Cannot Load Response to Claim Data, CCD Case Not found', 500); + } + try { - const response = await http.get(eventUrl, getCaseHeaders(accessToken || '')); - return response.data; + const client = createGeneratedClient(accessToken || ''); + const flow = await client.event('submitDefendantResponse').start(ccdCaseId); + return flow.data; } catch (error) { - throw convertAxiosErrorToHttpError(error, 'getExistingCaseDataError'); + throw convertAxiosErrorToHttpError(error, 'getResponseToClaimData'); } }, }; diff --git a/src/main/steps/respond-to-claim/correspondence-address/index.ts b/src/main/steps/respond-to-claim/correspondence-address/index.ts index 62fef92cb..1a513ff1f 100644 --- a/src/main/steps/respond-to-claim/correspondence-address/index.ts +++ b/src/main/steps/respond-to-claim/correspondence-address/index.ts @@ -1,11 +1,12 @@ import isPostalCode from 'validator/lib/isPostalCode'; -import type { Address, PossessionClaimResponse } from '../../../interfaces/ccdCase.interface'; +import type { SubmitDefendantResponseData } from '../../../generated/ccd/PCS'; +import type { Address } from '../../../interfaces/ccdCase.interface'; import type { FormFieldConfig } from '../../../interfaces/formFieldConfig.interface'; import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; import { createFormStep, getFormData, getTranslationFunction, setFormData } from '../../../modules/steps'; import { ccdCaseService } from '../../../services/ccdCaseService'; -import { buildCcdCaseForPossessionClaimResponse as buildAndSubmitPossessionClaimResponse } from '../../utils/populateResponseToClaimPayloadmap'; +import { submitDefendantResponseDraft } from '../../utils/populateResponseToClaimPayloadmap'; import { flowConfig } from '../flow.config'; const STEP_NAME = 'postcode-finder'; @@ -112,15 +113,16 @@ export const step: StepDefinition = createFormStep({ pageTitle: 'pageTitle', }, beforeRedirect: async req => { - let possessionClaimResponse: PossessionClaimResponse; - //prepopulate address is correct + let responseData: SubmitDefendantResponseData; + if (req.body?.['correspondenceAddressConfirm'] === 'yes') { - possessionClaimResponse = { - defendantContactDetails: { - party: { - address: prepopulateAddress, - }, - }, + if (!prepopulateAddress) { + throw new Error('Expected a prepopulated correspondence address'); + } + + responseData = { + correspondenceAddress: toGeneratedAddress(prepopulateAddress), + submitDraftAnswers: 'No', }; } else { const addressLine1 = req.body?.['correspondenceAddressConfirm.addressLine1'] ?? ''; @@ -129,23 +131,19 @@ export const step: StepDefinition = createFormStep({ const county = req.body?.['correspondenceAddressConfirm.county']; const postcode = req.body?.['correspondenceAddressConfirm.postcode'] ?? ''; - //only the details the defendant provides - possessionClaimResponse = { - defendantContactDetails: { - party: { - address: { - AddressLine1: addressLine1, - ...(addressLine2 !== undefined && addressLine2 !== '' && { AddressLine2: addressLine2 }), - PostTown: townOrCity, - ...(county !== undefined && county !== '' && { County: county }), - PostCode: postcode, - }, - }, - }, + responseData = { + correspondenceAddress: toGeneratedAddress({ + AddressLine1: addressLine1, + ...(addressLine2 !== undefined && addressLine2 !== '' && { AddressLine2: addressLine2 }), + PostTown: townOrCity, + ...(county !== undefined && county !== '' && { County: county }), + PostCode: postcode, + }), + submitDraftAnswers: 'No', }; } - await buildAndSubmitPossessionClaimResponse(req, possessionClaimResponse, false); + await submitDefendantResponseDraft(req, responseData); }, extendGetContent: async (req, formContent) => { const t = getTranslationFunction(req, 'correspondence-address', ['common']); @@ -233,9 +231,8 @@ export const step: StepDefinition = createFormStep({ }); async function getExistingAddress(accessToken: string, caseReference: string): Promise { - // Pull data from API - const response = await ccdCaseService.getExistingCaseData(accessToken, caseReference); - prepopulateAddress = response.case_details.case_data.possessionClaimResponse?.defendantContactDetails?.party?.address; + const responseData = await ccdCaseService.getResponseToClaimData(accessToken, caseReference); + prepopulateAddress = responseData.correspondenceAddress; if (prepopulateAddress) { const formattedAddress = @@ -257,3 +254,15 @@ async function getExistingAddress(accessToken: string, caseReference: string): P return '?'; //no address } } + +function toGeneratedAddress(address: Address): SubmitDefendantResponseData['correspondenceAddress'] { + return { + AddressLine1: address.AddressLine1, + AddressLine2: address.AddressLine2 ?? '', + AddressLine3: address.AddressLine3 ?? '', + PostTown: address.PostTown, + County: address.County ?? '', + PostCode: address.PostCode, + Country: address.Country ?? '', + }; +} diff --git a/src/main/steps/utils/populateResponseToClaimPayloadmap.ts b/src/main/steps/utils/populateResponseToClaimPayloadmap.ts index 8a84d0494..5812c74fd 100644 --- a/src/main/steps/utils/populateResponseToClaimPayloadmap.ts +++ b/src/main/steps/utils/populateResponseToClaimPayloadmap.ts @@ -1,20 +1,14 @@ import { Request } from 'express'; -import { CcdCase, PossessionClaimResponse } from '../../interfaces/ccdCase.interface'; +import type { SubmitDefendantResponseData } from '../../generated/ccd/PCS'; import { ccdCaseService } from '../../services/ccdCaseService'; -// Wrap the possession claim response in a ccd case object and submit via ccdCaseService -export const buildCcdCaseForPossessionClaimResponse = async ( +export const submitDefendantResponseDraft = async ( req: Request, - possessionClaimResponse: PossessionClaimResponse, - submitDraftAnswers: boolean -): Promise => { - const ccdCase: CcdCase = { - id: req.res?.locals.validatedCase?.id, - data: { - possessionClaimResponse, - submitDraftAnswers: submitDraftAnswers ? 'Yes' : 'No', - }, - }; - return ccdCaseService.submitResponseToClaim(req.session?.user?.accessToken, ccdCase); -}; + responseData: SubmitDefendantResponseData +) => + ccdCaseService.submitResponseToClaim( + req.session?.user?.accessToken, + req.res?.locals.validatedCase?.id || '', + responseData + ); diff --git a/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts b/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts index c12cb508d..26178812a 100644 --- a/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts +++ b/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts @@ -1,34 +1,65 @@ import { Page } from '@playwright/test'; // eslint-disable-next-line import/no-named-as-default -import Axios from 'axios'; +import Axios, { AxiosInstance } from 'axios'; +import { createCcdClient, type CcdClientConfig, type CcdTransport } from '@hmcts/ccd-event-runtime'; import { - createCaseApiData, createCaseEventTokenApiData, submitCaseApiData, submitCaseEventTokenApiData, } from '../../../data/api-data'; import { IAction, actionData, actionRecord } from '../../interfaces'; +import { caseBindings, type CreateClaimData } from '../../../../../main/generated/ccd/PCS'; export const caseInfo: { id: string; fid: string; state: string } = { id: '', fid: '', state: '' }; -export class CreateCaseAPIAction implements IAction { - private mapToPrefixedEventData(data: actionData): actionData { - if (typeof data !== 'object' || data === null || Array.isArray(data)) { - return data; - } +interface CreatedCaseResponse { + id?: string; + state?: string; +} - const prefix = createCaseApiData.createCaseDataPrefix; - if (!prefix) { - return data; - } +function createTransport(api: AxiosInstance): CcdTransport { + return { + get: async (url, headers) => (await api.get(url, { headers })).data, + post: async (url, data, headers) => (await api.post(url, data, { headers })).data, + }; +} + +function createCaseClient() { + const requestConfig = createCaseEventTokenApiData.createCaseApiInstance(); + const api = Axios.create(requestConfig); + const baseUrl = requestConfig.baseURL; + if (!baseUrl) { + throw new Error('Missing DATA_STORE_URL_BASE for createCaseAPI'); + } + + const clientConfig: CcdClientConfig = { + baseUrl, + getAuthHeaders: () => (requestConfig.headers ?? {}) as Record, + transport: createTransport(api), + }; - const typedData = data as Record; - return Object.fromEntries( - Object.entries(typedData).map(([key, value]) => [prefix + key.charAt(0).toUpperCase() + key.slice(1), value]) - ); + return createCcdClient(clientConfig, caseBindings); +} + +function extractCreateCasePayload(caseData: actionData): Partial { + const payload = typeof caseData === 'object' && caseData !== null && 'data' in caseData ? caseData.data : caseData; + return (payload ?? {}) as Partial; +} + +function setCaseInfo(createResponse: CreatedCaseResponse): void { + const caseId = String(createResponse.id ?? ''); + if (!caseId) { + throw new Error('Create case response did not include a case id'); } + process.env.CASE_NUMBER = caseId; + caseInfo.id = caseId; + caseInfo.fid = caseId.replace(/(.{4})(?=.)/g, '$1-'); + caseInfo.state = createResponse.state ?? ''; +} + +export class CreateCaseAPIAction implements IAction { async execute(page: Page, action: string, fieldName: actionData | actionRecord): Promise { const actionsMap = new Map Promise>([ ['createCaseAPI', () => this.createCaseAPI(fieldName)], @@ -42,20 +73,13 @@ export class CreateCaseAPIAction implements IAction { } private async createCaseAPI(caseData: actionData): Promise { - const createCaseApi = Axios.create(createCaseEventTokenApiData.createCaseApiInstance()); - const CREATE_EVENT_TOKEN = (await createCaseApi.get(createCaseEventTokenApiData.createCaseEventTokenApiEndPoint)) - .data.token; - const createCasePayloadData = typeof caseData === 'object' && 'data' in caseData ? caseData.data : caseData; - const createCaseEventData = this.mapToPrefixedEventData(createCasePayloadData); - const createResponse = await createCaseApi.post(createCaseApiData.createCaseApiEndPoint, { - data: createCaseEventData, - event: { id: createCaseApiData.createCaseEventName }, - event_token: CREATE_EVENT_TOKEN, - }); - process.env.CASE_NUMBER = createResponse.data.id; - caseInfo.id = createResponse.data.id; - caseInfo.fid = createResponse.data.id.replace(/(.{4})(?=.)/g, '$1-'); - caseInfo.state = createResponse.data.state; + const client = createCaseClient(); + const flow = await client.event('createPossessionClaim').start(); + const createResponse = (await flow.submit({ + ...flow.data, + ...extractCreateCasePayload(caseData), + } as CreateClaimData)) as CreatedCaseResponse; + setCaseInfo(createResponse); } private async submitCaseAPI(caseData: actionData): Promise { diff --git a/src/test/unit/generated/pcs.case-bindings.test.ts b/src/test/unit/generated/pcs.case-bindings.test.ts new file mode 100644 index 000000000..878fd4b37 --- /dev/null +++ b/src/test/unit/generated/pcs.case-bindings.test.ts @@ -0,0 +1,21 @@ +import { caseBindings } from '../../../main/generated/ccd/PCS'; + +describe('PCS generated case bindings', () => { + it('captures the createPossessionClaim runtime contract', () => { + expect(caseBindings.caseTypeId).toBe('PCS'); + expect(caseBindings.events.createPossessionClaim.fieldNamespace).toBe('claim.create'); + expect(caseBindings.events.createPossessionClaim.fields).toEqual( + expect.arrayContaining(['feeAmount', 'legislativeCountry', 'propertyAddress']) + ); + expect(caseBindings.events.createPossessionClaim.pages).toEqual( + expect.arrayContaining(['enterPropertyAddress', 'startTheService']) + ); + }); + + it('captures the submitDefendantResponse runtime contract with a terse namespace', () => { + expect(caseBindings.events.submitDefendantResponse.fieldNamespace).toBe('resp.def'); + expect(caseBindings.events.submitDefendantResponse.fields).toEqual( + expect.arrayContaining(['correspondenceAddress', 'submitDraftAnswers']) + ); + }); +}); diff --git a/src/test/unit/modules/oidc/oidc.test.ts b/src/test/unit/modules/oidc/oidc.test.ts index cd4607e8b..69aa2ca6b 100644 --- a/src/test/unit/modules/oidc/oidc.test.ts +++ b/src/test/unit/modules/oidc/oidc.test.ts @@ -148,7 +148,15 @@ describe('OIDCModule', () => { it('should successfully setup the OIDC client', async () => { await oidcModule['setupClient'](); - expect(discovery).toHaveBeenCalledWith(expect.any(URL), 'test-client-id', 'test-secret'); + expect(discovery).toHaveBeenCalledWith( + expect.any(URL), + 'test-client-id', + 'test-secret', + undefined, + expect.objectContaining({ + execute: expect.any(Array), + }) + ); }); it('should throw OIDCAuthenticationError when setup fails', async () => { diff --git a/src/test/unit/services/ccdCaseService.test.ts b/src/test/unit/services/ccdCaseService.test.ts index 07ed0905d..618f8da76 100644 --- a/src/test/unit/services/ccdCaseService.test.ts +++ b/src/test/unit/services/ccdCaseService.test.ts @@ -44,7 +44,7 @@ describe('ccdCaseService', () => { const result = await ccdCaseService.getCaseById(accessToken, caseId); expect(mockGet).toHaveBeenCalledWith( - `${mockUrl}/cases/${caseId}/event-triggers/respondPossessionClaim?ignore-warning=false`, + `${mockUrl}/cases/${caseId}/event-triggers/submitDefendantResponse?ignore-warning=false`, expect.objectContaining({ headers: expect.objectContaining({ Authorization: `Bearer ${accessToken}`, @@ -206,13 +206,68 @@ describe('ccdCaseService', () => { }); describe('createCase', () => { - it('calls submitEvent with correct args', async () => { - mockGet.mockResolvedValue({ data: { token: 'event-token' } }); - mockPost.mockResolvedValue({ data: { id: '999', data: { applicantForename: 'bar' } } }); + it('uses generated bindings to submit createPossessionClaim data', async () => { + const propertyAddress = { + AddressLine1: '123 Baker Street', + AddressLine2: '', + AddressLine3: '', + PostTown: 'London', + County: 'Greater London', + PostCode: 'NW1 6XE', + Country: 'United Kingdom', + }; - const result = await ccdCaseService.createCase(accessToken, { applicantForename: 'bar' }); + mockGet.mockResolvedValue({ + data: { + token: 'event-token', + case_details: { + case_data: { + claimCreateFeeAmount: '£999999.99', + }, + }, + }, + }); + mockPost.mockResolvedValue({ data: { id: '999' } }); + + const result = await ccdCaseService.createCase(accessToken, { + legislativeCountry: 'England', + propertyAddress, + }); - expect(result).toEqual({ id: '999', data: { applicantForename: 'bar' } }); + expect(mockGet).toHaveBeenCalledWith( + `${mockUrl}/case-types/PCS/event-triggers/createPossessionClaim`, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${accessToken}`, + }), + }) + ); + expect(mockPost).toHaveBeenCalledWith( + `${mockUrl}/case-types/PCS/cases`, + { + data: { + claimCreateFeeAmount: '£999999.99', + claimCreateLegislativeCountry: 'England', + claimCreatePropertyAddress: propertyAddress, + }, + event: { id: 'createPossessionClaim' }, + event_token: 'event-token', + ignore_warning: false, + }, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${accessToken}`, + }), + }) + ); + expect(result).toEqual({ + id: '999', + data: { + feeAmount: '£999999.99', + legislativeCountry: 'England', + propertyAddress, + }, + }); }); }); @@ -236,17 +291,118 @@ describe('ccdCaseService', () => { describe('submitResponseToClaim', () => { it('throws HTTPError if case id is missing', async () => { - await expect(ccdCaseService.submitResponseToClaim(accessToken, { id: '', data: {} })).rejects.toThrow(HTTPError); - await expect(ccdCaseService.submitResponseToClaim(accessToken, { id: '', data: {} })).rejects.toThrow( + await expect(ccdCaseService.submitResponseToClaim(accessToken, '', {})).rejects.toThrow(HTTPError); + await expect(ccdCaseService.submitResponseToClaim(accessToken, '', {})).rejects.toThrow( 'Cannot Submit Response to Case, CCD Case Not found' ); }); + + it('uses generated bindings to submit response-to-claim data', async () => { + const correspondenceAddress = { + AddressLine1: '10 Example Street', + AddressLine2: '', + AddressLine3: '', + PostTown: 'London', + County: '', + PostCode: 'SW1 1AA', + Country: '', + }; + + mockGet.mockResolvedValue({ + data: { + token: 'event-token', + case_details: { + case_data: { + respDefSubmitDraftAnswers: 'Yes', + }, + }, + }, + }); + mockPost.mockResolvedValue({ data: { id: '1234567890123456' } }); + + const result = await ccdCaseService.submitResponseToClaim(accessToken, '1234567890123456', { + correspondenceAddress, + submitDraftAnswers: 'No', + }); + + expect(mockGet).toHaveBeenCalledWith( + `${mockUrl}/cases/1234567890123456/event-triggers/submitDefendantResponse?ignore-warning=false`, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${accessToken}`, + }), + }) + ); + expect(mockPost).toHaveBeenCalledWith( + `${mockUrl}/cases/1234567890123456/events`, + { + data: { + respDefCorrespondenceAddress: correspondenceAddress, + respDefSubmitDraftAnswers: 'No', + }, + event: { id: 'submitDefendantResponse' }, + event_token: 'event-token', + ignore_warning: false, + }, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${accessToken}`, + }), + }) + ); + expect(result).toEqual({ + id: '1234567890123456', + data: { + correspondenceAddress, + submitDraftAnswers: 'No', + }, + }); + }); }); - describe('getExistingCaseData', () => { + describe('getResponseToClaimData', () => { + it('uses generated bindings to load submitDefendantResponse data', async () => { + const correspondenceAddress = { + AddressLine1: '10 Example Street', + AddressLine2: '', + AddressLine3: '', + PostTown: 'London', + County: '', + PostCode: 'SW1 1AA', + Country: '', + }; + + mockGet.mockResolvedValue({ + data: { + token: 'event-token', + case_details: { + case_data: { + respDefCorrespondenceAddress: correspondenceAddress, + respDefSubmitDraftAnswers: 'No', + }, + }, + }, + }); + + const result = await ccdCaseService.getResponseToClaimData(accessToken, '1234567890123456'); + + expect(mockGet).toHaveBeenCalledWith( + `${mockUrl}/cases/1234567890123456/event-triggers/submitDefendantResponse?ignore-warning=false`, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${accessToken}`, + }), + }) + ); + expect(result).toEqual({ + correspondenceAddress, + submitDraftAnswers: 'No', + }); + }); + it('throws if case data errors', async () => { mockGet.mockRejectedValue({ response: { status: 400 } }); - await expect(ccdCaseService.getExistingCaseData(accessToken, '')).rejects.toThrow(HTTPError); + await expect(ccdCaseService.getResponseToClaimData(accessToken, '')).rejects.toThrow(HTTPError); }); }); }); diff --git a/src/test/unit/ui/createCaseAPI.action.test.ts b/src/test/unit/ui/createCaseAPI.action.test.ts new file mode 100644 index 000000000..72d4267ce --- /dev/null +++ b/src/test/unit/ui/createCaseAPI.action.test.ts @@ -0,0 +1,76 @@ +import type { AxiosInstance } from 'axios'; +import Axios from 'axios'; + +import { createCaseApiData } from '../../ui/data/api-data'; +import { + caseInfo, + CreateCaseAPIAction, +} from '../../ui/utils/actions/custom-actions/createCaseAPI.action'; + +jest.mock('axios'); + +describe('CreateCaseAPIAction', () => { + const mockGet = jest.fn(); + const mockPost = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + process.env.DATA_STORE_URL_BASE = 'http://ccd.example.com'; + process.env.BEARER_TOKEN = 'bearer-token'; + process.env.SERVICE_AUTH_TOKEN = 'service-token'; + delete process.env.CASE_NUMBER; + caseInfo.id = ''; + caseInfo.fid = ''; + caseInfo.state = ''; + + (Axios.create as jest.Mock).mockReturnValue({ + get: mockGet, + post: mockPost, + } as Partial); + }); + + it('creates a CCD case via the generated runtime client', async () => { + mockGet.mockResolvedValue({ + data: { + token: 'event-token', + case_details: { + case_data: { + claimCreateFeeAmount: '£999999.99', + }, + }, + }, + }); + mockPost.mockResolvedValue({ + data: { + id: '1773696232722684', + state: 'AWAITING_SUBMISSION_TO_HMCTS', + }, + }); + + const action = new CreateCaseAPIAction(); + await action.execute({} as never, 'createCaseAPI', createCaseApiData.createCasePayload); + + expect(mockGet).toHaveBeenCalledWith( + 'http://ccd.example.com/case-types/PCS/event-triggers/createPossessionClaim', + expect.any(Object) + ); + expect(mockPost).toHaveBeenCalledWith( + 'http://ccd.example.com/case-types/PCS/cases', + expect.objectContaining({ + data: expect.objectContaining({ + claimCreateFeeAmount: '£404', + claimCreateLegislativeCountry: 'England', + claimCreatePropertyAddress: createCaseApiData.createCasePayload.propertyAddress, + }), + event: { id: 'createPossessionClaim' }, + event_token: 'event-token', + ignore_warning: false, + }), + expect.any(Object) + ); + expect(process.env.CASE_NUMBER).toBe('1773696232722684'); + expect(caseInfo.id).toBe('1773696232722684'); + expect(caseInfo.fid).toBe('1773-6962-3272-2684'); + expect(caseInfo.state).toBe('AWAITING_SUBMISSION_TO_HMCTS'); + }); +}); diff --git a/tsconfig.generated-consumers.json b/tsconfig.generated-consumers.json new file mode 100644 index 000000000..4ad31792e --- /dev/null +++ b/tsconfig.generated-consumers.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": [ + "src/main/generated/**/*", + "src/main/examples/**/*", + "src/test/ui/data/api-data/createCase.api.data.ts", + "src/test/ui/data/api-data/index.ts", + "src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts", + "src/test/ui/utils/actions/custom-actions/index.ts", + "src/test/unit/generated/**/*.ts", + "src/test/unit/ui/**/*.ts" + ], + "exclude": [] +} diff --git a/yarn.lock b/yarn.lock index 392c5a661..0a3f3dd13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2069,6 +2069,13 @@ __metadata: languageName: node linkType: hard +"@hmcts/ccd-event-runtime@file:../../sdk/ccd-event-runtime::locator=pcs-frontend%40workspace%3A.": + version: 0.0.1 + resolution: "@hmcts/ccd-event-runtime@file:../../sdk/ccd-event-runtime#../../sdk/ccd-event-runtime::hash=806de2&locator=pcs-frontend%40workspace%3A." + checksum: 10/010de30256160eb66b291177a7e36536955d8cc936594102235eb241abaa40b3af9a3891c4a3344b647be56cfb5e39abaa8c45e2caeeda7b0568bc52679f8e71 + languageName: node + linkType: hard + "@hmcts/info-provider@npm:^1.1.0": version: 1.3.0 resolution: "@hmcts/info-provider@npm:1.3.0" @@ -12623,6 +12630,7 @@ __metadata: "@eslint/eslintrc": "npm:^3.3.1" "@eslint/js": "npm:^9.29.0" "@eslint/plugin-kit": "npm:^0.5.0" + "@hmcts/ccd-event-runtime": "file:../../sdk/ccd-event-runtime" "@hmcts/info-provider": "npm:^1.1.0" "@hmcts/nodejs-healthcheck": "npm:^1.8.0" "@hmcts/nodejs-logging": "npm:^4.0.4" From 3b146779f5fb2c0b9d9a7b6a240df7de7e390f4e Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Thu, 19 Mar 2026 10:36:40 +0000 Subject: [PATCH 04/11] Use the slimmed event contract in PCS frontend --- src/main/generated/ccd/PCS/event-contracts.ts | 2 -- .../unit/generated/pcs.case-bindings.test.ts | 21 ------------------- yarn.lock | 4 ++-- 3 files changed, 2 insertions(+), 25 deletions(-) delete mode 100644 src/test/unit/generated/pcs.case-bindings.test.ts diff --git a/src/main/generated/ccd/PCS/event-contracts.ts b/src/main/generated/ccd/PCS/event-contracts.ts index 0e4de5e5e..b77f8f78e 100644 --- a/src/main/generated/ccd/PCS/event-contracts.ts +++ b/src/main/generated/ccd/PCS/event-contracts.ts @@ -13,12 +13,10 @@ export const caseBindings = defineCaseBindings({ events: { "createPossessionClaim": { fieldNamespace: "claim.create", - fields: ["crossBorderCountriesList", "crossBorderCountry1", "crossBorderCountry2", "feeAmount", "legislativeCountry", "postcodeNotAssignedView", "propertyAddress", "showCrossBorderPage", "showPostcodeNotAssignedToCourt", "showPropertyNotEligiblePage"], pages: ["crossBorderPostcodeSelection", "enterPropertyAddress", "postcodeNotAssignedToCourt", "propertyNotEligible", "startTheService"], }, "submitDefendantResponse": { fieldNamespace: "resp.def", - fields: ["correspondenceAddress", "submitDraftAnswers"], pages: [], }, }, diff --git a/src/test/unit/generated/pcs.case-bindings.test.ts b/src/test/unit/generated/pcs.case-bindings.test.ts deleted file mode 100644 index 878fd4b37..000000000 --- a/src/test/unit/generated/pcs.case-bindings.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { caseBindings } from '../../../main/generated/ccd/PCS'; - -describe('PCS generated case bindings', () => { - it('captures the createPossessionClaim runtime contract', () => { - expect(caseBindings.caseTypeId).toBe('PCS'); - expect(caseBindings.events.createPossessionClaim.fieldNamespace).toBe('claim.create'); - expect(caseBindings.events.createPossessionClaim.fields).toEqual( - expect.arrayContaining(['feeAmount', 'legislativeCountry', 'propertyAddress']) - ); - expect(caseBindings.events.createPossessionClaim.pages).toEqual( - expect.arrayContaining(['enterPropertyAddress', 'startTheService']) - ); - }); - - it('captures the submitDefendantResponse runtime contract with a terse namespace', () => { - expect(caseBindings.events.submitDefendantResponse.fieldNamespace).toBe('resp.def'); - expect(caseBindings.events.submitDefendantResponse.fields).toEqual( - expect.arrayContaining(['correspondenceAddress', 'submitDraftAnswers']) - ); - }); -}); diff --git a/yarn.lock b/yarn.lock index 0a3f3dd13..2183186c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2071,8 +2071,8 @@ __metadata: "@hmcts/ccd-event-runtime@file:../../sdk/ccd-event-runtime::locator=pcs-frontend%40workspace%3A.": version: 0.0.1 - resolution: "@hmcts/ccd-event-runtime@file:../../sdk/ccd-event-runtime#../../sdk/ccd-event-runtime::hash=806de2&locator=pcs-frontend%40workspace%3A." - checksum: 10/010de30256160eb66b291177a7e36536955d8cc936594102235eb241abaa40b3af9a3891c4a3344b647be56cfb5e39abaa8c45e2caeeda7b0568bc52679f8e71 + resolution: "@hmcts/ccd-event-runtime@file:../../sdk/ccd-event-runtime#../../sdk/ccd-event-runtime::hash=013df5&locator=pcs-frontend%40workspace%3A." + checksum: 10/44964facd692fb168438cde932d8b4ac1e67ae8f65e206266e705875503f8494359572ec7d90221e71fd8f49d7b3a2d53687526935493a7160313581452fc207 languageName: node linkType: hard From 6fae7b2e05eb159007a8b6a4bcc751098467969e Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Thu, 19 Mar 2026 10:55:13 +0000 Subject: [PATCH 05/11] tidy --- .gradle/yarn/yarn-latest/bin/yarn | 1 - .gradle/yarn/yarn-latest/bin/yarnpkg | 1 - 2 files changed, 2 deletions(-) delete mode 120000 .gradle/yarn/yarn-latest/bin/yarn delete mode 120000 .gradle/yarn/yarn-latest/bin/yarnpkg diff --git a/.gradle/yarn/yarn-latest/bin/yarn b/.gradle/yarn/yarn-latest/bin/yarn deleted file mode 120000 index 0d279c302..000000000 --- a/.gradle/yarn/yarn-latest/bin/yarn +++ /dev/null @@ -1 +0,0 @@ -../lib/node_modules/yarn/bin/yarn.js \ No newline at end of file diff --git a/.gradle/yarn/yarn-latest/bin/yarnpkg b/.gradle/yarn/yarn-latest/bin/yarnpkg deleted file mode 120000 index 0d279c302..000000000 --- a/.gradle/yarn/yarn-latest/bin/yarnpkg +++ /dev/null @@ -1 +0,0 @@ -../lib/node_modules/yarn/bin/yarn.js \ No newline at end of file From bfc09c99b09a9054b7c2f3f72781beee19f59355 Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Thu, 19 Mar 2026 13:07:34 +0000 Subject: [PATCH 06/11] new api shape --- src/main/generated/ccd/PCS/event-contracts.ts | 8 +- src/main/interfaces/ccdCase.interface.ts | 24 +++ src/main/services/ccdCaseService.ts | 112 ++--------- src/test/unit/services/ccdCaseService.test.ts | 176 +----------------- src/test/unit/ui/createCaseAPI.action.test.ts | 8 +- yarn.lock | 4 +- 6 files changed, 63 insertions(+), 269 deletions(-) diff --git a/src/main/generated/ccd/PCS/event-contracts.ts b/src/main/generated/ccd/PCS/event-contracts.ts index b77f8f78e..3b798158d 100644 --- a/src/main/generated/ccd/PCS/event-contracts.ts +++ b/src/main/generated/ccd/PCS/event-contracts.ts @@ -8,16 +8,16 @@ export interface EventDtoMap { "submitDefendantResponse": SubmitDefendantResponseData; } -export const caseBindings = defineCaseBindings({ +export const caseBindings = defineCaseBindings()({ caseTypeId: "PCS", events: { "createPossessionClaim": { - fieldNamespace: "claim.create", + fieldPrefix: "cpc", pages: ["crossBorderPostcodeSelection", "enterPropertyAddress", "postcodeNotAssignedToCourt", "propertyNotEligible", "startTheService"], }, "submitDefendantResponse": { - fieldNamespace: "resp.def", + fieldPrefix: "sdr", pages: [], }, }, -} satisfies CcdCaseBindings); +} as const satisfies CcdCaseBindings); diff --git a/src/main/interfaces/ccdCase.interface.ts b/src/main/interfaces/ccdCase.interface.ts index 240e665ef..623be243f 100644 --- a/src/main/interfaces/ccdCase.interface.ts +++ b/src/main/interfaces/ccdCase.interface.ts @@ -32,3 +32,27 @@ export interface Address { PostCode: string; Country?: string; } + +export interface PossessionClaimResponse { + defendantContactDetails: { + party: { + firstName?: string; + lastName?: string; + address?: Address; + }; + }; +} + +export interface StartCallbackData { + case_details: { + case_data: { + possessionClaimResponse?: { + defendantContactDetails?: { + party?: { + address?: Address; + }; + }; + }; + }; + }; +} diff --git a/src/main/services/ccdCaseService.ts b/src/main/services/ccdCaseService.ts index b7c5b51b7..fc2108682 100644 --- a/src/main/services/ccdCaseService.ts +++ b/src/main/services/ccdCaseService.ts @@ -1,12 +1,10 @@ import { Logger } from '@hmcts/nodejs-logging'; -import { createCcdClient, type CcdClientConfig, type CcdSubmitResult, type CcdTransport } from '@hmcts/ccd-event-runtime'; import { AxiosError } from 'axios'; import config from 'config'; -import { caseBindings, type CreateClaimData, type SubmitDefendantResponseData } from '../generated/ccd/PCS'; import { HTTPError } from '../HttpError'; import { CaseState } from '../interfaces/ccdCase.interface'; -import type { CcdCase, CcdUserCases } from '../interfaces/ccdCase.interface'; +import type { CcdCase, CcdUserCases, StartCallbackData } from '../interfaces/ccdCase.interface'; import { http } from '../modules/http'; const logger = Logger.getLogger('ccdCaseService'); @@ -34,46 +32,6 @@ function getCaseHeaders(token: string) { }; } -function getGeneratedClientHeaders(token: string): Record { - return { - Authorization: `Bearer ${token}`, - experimental: 'true', - Accept: '*/*', - 'Content-Type': 'application/json', - }; -} - -function createTransport(): CcdTransport { - return { - get: async (url, headers) => (await http.get(url, { headers })).data, - post: async (url, data, headers) => (await http.post(url, data, { headers })).data, - }; -} - -function createGeneratedClient(userToken: string) { - const clientConfig: CcdClientConfig = { - baseUrl: getBaseUrl(), - caseTypeId: getCaseTypeId(), - getAuthHeaders: () => getGeneratedClientHeaders(userToken), - transport: createTransport(), - }; - - return createCcdClient(clientConfig, caseBindings); -} - -function toRecord(value: unknown): Record { - return typeof value === 'object' && value !== null ? (value as Record) : {}; -} - -function normaliseSubmitResult(result: CcdSubmitResult, fallbackData: Record): CcdCase { - const resultData = toRecord(result.data); - - return { - id: String(result.id ?? ''), - data: Object.keys(resultData).length > 0 ? resultData : fallbackData, - }; -} - function convertAxiosErrorToHttpError(error: unknown, context: string): HTTPError { const axiosError = error as AxiosError; const status = axiosError.response?.status; @@ -131,11 +89,7 @@ async function submitEvent( } export const ccdCaseService = { - async getCaseById( - accessToken: string, - caseId: string, - eventId: string = 'submitDefendantResponse' - ): Promise { + async getCaseById(accessToken: string, caseId: string, eventId: string = 'respondPossessionClaim'): Promise { const eventUrl = `${getBaseUrl()}/cases/${caseId}/event-triggers/${eventId}?ignore-warning=false`; try { @@ -198,20 +152,11 @@ export const ccdCaseService = { } }, - async createCase(accessToken: string | undefined, data: Partial): Promise { - try { - const client = createGeneratedClient(accessToken || ''); - const flow = await client.event('createPossessionClaim').start(); - const submitData: CreateClaimData = { - ...flow.data, - ...(data as Partial), - }; - const result = await flow.submit(submitData); - - return normaliseSubmitResult(result, submitData as unknown as Record); - } catch (error) { - throw convertAxiosErrorToHttpError(error, 'createCase'); - } + async createCase(accessToken: string | undefined, data: Record): Promise { + const eventUrl = `${getBaseUrl()}/case-types/${getCaseTypeId()}/event-triggers/citizenCreateApplication`; + const eventToken = await getEventToken(accessToken || '', eventUrl); + const url = `${getBaseUrl()}/case-types/${getCaseTypeId()}/cases`; + return submitEvent(accessToken || '', url, 'citizenCreateApplication', eventToken, data); }, async updateCase(accessToken: string | undefined, ccdCase: CcdCase): Promise { @@ -235,44 +180,25 @@ export const ccdCaseService = { return submitEvent(accessToken || '', url, 'citizenSubmitApplication', eventToken, ccdCase.data); }, - async submitResponseToClaim( - accessToken: string | undefined, - ccdCaseId: string, - data: Partial - ): Promise { - if (!ccdCaseId) { + async submitResponseToClaim(accessToken: string | undefined, ccdCase: CcdCase): Promise { + if (!ccdCase.id) { throw new HTTPError('Cannot Submit Response to Case, CCD Case Not found', 500); } + const eventUrl = `${getBaseUrl()}/cases/${ccdCase.id}/event-triggers/respondPossessionClaim`; + const eventToken = await getEventToken(accessToken || '', eventUrl); + const url = `${getBaseUrl()}/cases/${ccdCase.id}/events`; - try { - const client = createGeneratedClient(accessToken || ''); - const flow = await client.event('submitDefendantResponse').start(ccdCaseId); - const submitData: SubmitDefendantResponseData = { - ...flow.data, - ...data, - }; - const result = await flow.submit(submitData); - - return normaliseSubmitResult(result, submitData as unknown as Record); - } catch (error) { - throw convertAxiosErrorToHttpError(error, 'submitResponseToClaim'); - } + return submitEvent(accessToken || '', url, 'respondPossessionClaim', eventToken, ccdCase.data); }, - async getResponseToClaimData( - accessToken: string | undefined, - ccdCaseId: string - ): Promise { - if (!ccdCaseId) { - throw new HTTPError('Cannot Load Response to Claim Data, CCD Case Not found', 500); - } - + async getExistingCaseData(accessToken: string | undefined, ccdCaseId: string): Promise { + const eventUrl = `${getBaseUrl()}/cases/${ccdCaseId}/event-triggers/respondPossessionClaim?ignore-warning=false`; + logger.info('getExistingCaseData event URL', { eventUrl }); try { - const client = createGeneratedClient(accessToken || ''); - const flow = await client.event('submitDefendantResponse').start(ccdCaseId); - return flow.data; + const response = await http.get(eventUrl, getCaseHeaders(accessToken || '')); + return response.data; } catch (error) { - throw convertAxiosErrorToHttpError(error, 'getResponseToClaimData'); + throw convertAxiosErrorToHttpError(error, 'getExistingCaseDataError'); } }, }; diff --git a/src/test/unit/services/ccdCaseService.test.ts b/src/test/unit/services/ccdCaseService.test.ts index 618f8da76..07ed0905d 100644 --- a/src/test/unit/services/ccdCaseService.test.ts +++ b/src/test/unit/services/ccdCaseService.test.ts @@ -44,7 +44,7 @@ describe('ccdCaseService', () => { const result = await ccdCaseService.getCaseById(accessToken, caseId); expect(mockGet).toHaveBeenCalledWith( - `${mockUrl}/cases/${caseId}/event-triggers/submitDefendantResponse?ignore-warning=false`, + `${mockUrl}/cases/${caseId}/event-triggers/respondPossessionClaim?ignore-warning=false`, expect.objectContaining({ headers: expect.objectContaining({ Authorization: `Bearer ${accessToken}`, @@ -206,68 +206,13 @@ describe('ccdCaseService', () => { }); describe('createCase', () => { - it('uses generated bindings to submit createPossessionClaim data', async () => { - const propertyAddress = { - AddressLine1: '123 Baker Street', - AddressLine2: '', - AddressLine3: '', - PostTown: 'London', - County: 'Greater London', - PostCode: 'NW1 6XE', - Country: 'United Kingdom', - }; + it('calls submitEvent with correct args', async () => { + mockGet.mockResolvedValue({ data: { token: 'event-token' } }); + mockPost.mockResolvedValue({ data: { id: '999', data: { applicantForename: 'bar' } } }); - mockGet.mockResolvedValue({ - data: { - token: 'event-token', - case_details: { - case_data: { - claimCreateFeeAmount: '£999999.99', - }, - }, - }, - }); - mockPost.mockResolvedValue({ data: { id: '999' } }); - - const result = await ccdCaseService.createCase(accessToken, { - legislativeCountry: 'England', - propertyAddress, - }); + const result = await ccdCaseService.createCase(accessToken, { applicantForename: 'bar' }); - expect(mockGet).toHaveBeenCalledWith( - `${mockUrl}/case-types/PCS/event-triggers/createPossessionClaim`, - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: `Bearer ${accessToken}`, - }), - }) - ); - expect(mockPost).toHaveBeenCalledWith( - `${mockUrl}/case-types/PCS/cases`, - { - data: { - claimCreateFeeAmount: '£999999.99', - claimCreateLegislativeCountry: 'England', - claimCreatePropertyAddress: propertyAddress, - }, - event: { id: 'createPossessionClaim' }, - event_token: 'event-token', - ignore_warning: false, - }, - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: `Bearer ${accessToken}`, - }), - }) - ); - expect(result).toEqual({ - id: '999', - data: { - feeAmount: '£999999.99', - legislativeCountry: 'England', - propertyAddress, - }, - }); + expect(result).toEqual({ id: '999', data: { applicantForename: 'bar' } }); }); }); @@ -291,118 +236,17 @@ describe('ccdCaseService', () => { describe('submitResponseToClaim', () => { it('throws HTTPError if case id is missing', async () => { - await expect(ccdCaseService.submitResponseToClaim(accessToken, '', {})).rejects.toThrow(HTTPError); - await expect(ccdCaseService.submitResponseToClaim(accessToken, '', {})).rejects.toThrow( + await expect(ccdCaseService.submitResponseToClaim(accessToken, { id: '', data: {} })).rejects.toThrow(HTTPError); + await expect(ccdCaseService.submitResponseToClaim(accessToken, { id: '', data: {} })).rejects.toThrow( 'Cannot Submit Response to Case, CCD Case Not found' ); }); - - it('uses generated bindings to submit response-to-claim data', async () => { - const correspondenceAddress = { - AddressLine1: '10 Example Street', - AddressLine2: '', - AddressLine3: '', - PostTown: 'London', - County: '', - PostCode: 'SW1 1AA', - Country: '', - }; - - mockGet.mockResolvedValue({ - data: { - token: 'event-token', - case_details: { - case_data: { - respDefSubmitDraftAnswers: 'Yes', - }, - }, - }, - }); - mockPost.mockResolvedValue({ data: { id: '1234567890123456' } }); - - const result = await ccdCaseService.submitResponseToClaim(accessToken, '1234567890123456', { - correspondenceAddress, - submitDraftAnswers: 'No', - }); - - expect(mockGet).toHaveBeenCalledWith( - `${mockUrl}/cases/1234567890123456/event-triggers/submitDefendantResponse?ignore-warning=false`, - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: `Bearer ${accessToken}`, - }), - }) - ); - expect(mockPost).toHaveBeenCalledWith( - `${mockUrl}/cases/1234567890123456/events`, - { - data: { - respDefCorrespondenceAddress: correspondenceAddress, - respDefSubmitDraftAnswers: 'No', - }, - event: { id: 'submitDefendantResponse' }, - event_token: 'event-token', - ignore_warning: false, - }, - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: `Bearer ${accessToken}`, - }), - }) - ); - expect(result).toEqual({ - id: '1234567890123456', - data: { - correspondenceAddress, - submitDraftAnswers: 'No', - }, - }); - }); }); - describe('getResponseToClaimData', () => { - it('uses generated bindings to load submitDefendantResponse data', async () => { - const correspondenceAddress = { - AddressLine1: '10 Example Street', - AddressLine2: '', - AddressLine3: '', - PostTown: 'London', - County: '', - PostCode: 'SW1 1AA', - Country: '', - }; - - mockGet.mockResolvedValue({ - data: { - token: 'event-token', - case_details: { - case_data: { - respDefCorrespondenceAddress: correspondenceAddress, - respDefSubmitDraftAnswers: 'No', - }, - }, - }, - }); - - const result = await ccdCaseService.getResponseToClaimData(accessToken, '1234567890123456'); - - expect(mockGet).toHaveBeenCalledWith( - `${mockUrl}/cases/1234567890123456/event-triggers/submitDefendantResponse?ignore-warning=false`, - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: `Bearer ${accessToken}`, - }), - }) - ); - expect(result).toEqual({ - correspondenceAddress, - submitDraftAnswers: 'No', - }); - }); - + describe('getExistingCaseData', () => { it('throws if case data errors', async () => { mockGet.mockRejectedValue({ response: { status: 400 } }); - await expect(ccdCaseService.getResponseToClaimData(accessToken, '')).rejects.toThrow(HTTPError); + await expect(ccdCaseService.getExistingCaseData(accessToken, '')).rejects.toThrow(HTTPError); }); }); }); diff --git a/src/test/unit/ui/createCaseAPI.action.test.ts b/src/test/unit/ui/createCaseAPI.action.test.ts index 72d4267ce..2dbb32093 100644 --- a/src/test/unit/ui/createCaseAPI.action.test.ts +++ b/src/test/unit/ui/createCaseAPI.action.test.ts @@ -35,7 +35,7 @@ describe('CreateCaseAPIAction', () => { token: 'event-token', case_details: { case_data: { - claimCreateFeeAmount: '£999999.99', + cpcfeeAmount: '£999999.99', }, }, }, @@ -58,9 +58,9 @@ describe('CreateCaseAPIAction', () => { 'http://ccd.example.com/case-types/PCS/cases', expect.objectContaining({ data: expect.objectContaining({ - claimCreateFeeAmount: '£404', - claimCreateLegislativeCountry: 'England', - claimCreatePropertyAddress: createCaseApiData.createCasePayload.propertyAddress, + cpcfeeAmount: '£404', + cpclegislativeCountry: 'England', + cpcpropertyAddress: createCaseApiData.createCasePayload.propertyAddress, }), event: { id: 'createPossessionClaim' }, event_token: 'event-token', diff --git a/yarn.lock b/yarn.lock index 2183186c5..163689378 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2071,8 +2071,8 @@ __metadata: "@hmcts/ccd-event-runtime@file:../../sdk/ccd-event-runtime::locator=pcs-frontend%40workspace%3A.": version: 0.0.1 - resolution: "@hmcts/ccd-event-runtime@file:../../sdk/ccd-event-runtime#../../sdk/ccd-event-runtime::hash=013df5&locator=pcs-frontend%40workspace%3A." - checksum: 10/44964facd692fb168438cde932d8b4ac1e67ae8f65e206266e705875503f8494359572ec7d90221e71fd8f49d7b3a2d53687526935493a7160313581452fc207 + resolution: "@hmcts/ccd-event-runtime@file:../../sdk/ccd-event-runtime#../../sdk/ccd-event-runtime::hash=70a76c&locator=pcs-frontend%40workspace%3A." + checksum: 10/5f5a2bced183e19deb4770985c866188c5a9806ebb898b7801c9758940f0098cb832b3e0ac4dc89a425f8d1c9df3bfc414bb83219484d36a4358c3e84abd3173 languageName: node linkType: hard From afd8de22990128f81dbb06b2026e465391b4e6d3 Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Thu, 19 Mar 2026 13:17:45 +0000 Subject: [PATCH 07/11] tidy --- jest.config.js | 3 +- .../createPossessionClaimClientExample.ts | 73 ------------------ src/main/generated/ccd/PCS/index.ts | 3 - src/main/modules/oidc/oidc.ts | 8 +- .../correspondence-address/index.ts | 63 +++++++-------- .../populateResponseToClaimPayloadmap.ts | 24 +++--- .../ui/data/api-data/createCase.api.data.ts | 30 ++++---- .../custom-actions/createCaseAPI.action.ts | 3 +- src/test/unit/modules/oidc/oidc.test.ts | 10 +-- src/test/unit/ui/createCaseAPI.action.test.ts | 76 ------------------- tsconfig.generated-consumers.json | 4 +- 11 files changed, 62 insertions(+), 235 deletions(-) delete mode 100644 src/main/examples/createPossessionClaimClientExample.ts delete mode 100644 src/main/generated/ccd/PCS/index.ts delete mode 100644 src/test/unit/ui/createCaseAPI.action.test.ts diff --git a/jest.config.js b/jest.config.js index f84f0757f..dd7a4008f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,7 +7,6 @@ module.exports = { '^.+\\.ts?$': 'ts-jest', }, moduleNameMapper: { - '^@hmcts/ccd-event-runtime$': '/../../sdk/ccd-event-runtime/src/index.ts', '^openid-client$': '/src/test/unit/modules/oidc/__mocks__/openid-client.ts', '^steps$': '/src/main/steps', '^app/(.*)$': '/src/main/app/$1', @@ -16,5 +15,5 @@ module.exports = { }, testPathIgnorePatterns: ['/__mocks__/'], coverageProvider: 'v8', - transformIgnorePatterns: ['node_modules/(?!(jose|@panva|oidc-token-hash|@hmcts/ccd-event-runtime)/)'], + transformIgnorePatterns: ['node_modules/(?!(jose|@panva|oidc-token-hash)/)'], }; diff --git a/src/main/examples/createPossessionClaimClientExample.ts b/src/main/examples/createPossessionClaimClientExample.ts deleted file mode 100644 index 191f6b925..000000000 --- a/src/main/examples/createPossessionClaimClientExample.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Example usage of generated CCD client for createPossessionClaim. -// This module is for developer reference and is not wired into runtime routes. -// eslint-disable-next-line import/no-named-as-default -import Axios, { AxiosInstance } from 'axios'; -import { createCcdClient, type CcdClientConfig, type CcdTransport } from '@hmcts/ccd-event-runtime'; - -import { - CreateClaimData, - caseBindings, -} from '../generated/ccd/PCS'; - -function createTransport(api: AxiosInstance): CcdTransport { - return { - get: async (url, headers) => (await api.get(url, { headers })).data, - post: async (url, data, headers) => (await api.post(url, data, { headers })).data, - }; -} - -function createClient( - baseUrl: string, - caseTypeId: string, - bearerToken: string, - serviceToken: string -) { - const api = Axios.create({ - baseURL: baseUrl, - headers: { - Authorization: `Bearer ${bearerToken}`, - ServiceAuthorization: `Bearer ${serviceToken}`, - 'Content-Type': 'application/json', - experimental: 'experimental', - Accept: '*/*', - }, - }); - - const clientConfig: CcdClientConfig = { - baseUrl, - caseTypeId, - getAuthHeaders: () => ({}), - transport: createTransport(api), - }; - - return createCcdClient(clientConfig, caseBindings); -} - -export async function createPossessionClaimExample( - baseUrl: string, - caseTypeId: string, - bearerToken: string, - serviceToken: string -): Promise { - const client = createClient(baseUrl, caseTypeId, bearerToken, serviceToken); - - // 1) Start event to get token + current event data. - const flow = await client.event('createPossessionClaim').start(); - - // If the event defines an about-to-start callback, any pre-populated values are returned. - const startData: CreateClaimData = flow.data; - - // payload is typed - startData.feeAmount = '£12345'; - startData.propertyAddress = { - AddressLine1: '2 Second Avenue', - AddressLine2: '', - AddressLine3: '', - PostTown: 'London', - County: '', - PostCode: 'W3 7RX', - Country: 'United Kingdom', - }; - - await flow.submit(startData); -} diff --git a/src/main/generated/ccd/PCS/index.ts b/src/main/generated/ccd/PCS/index.ts deleted file mode 100644 index e8134728c..000000000 --- a/src/main/generated/ccd/PCS/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Generated by CCD SDK. Do not edit manually. -export * from "./dto-types"; -export * from "./event-contracts"; diff --git a/src/main/modules/oidc/oidc.ts b/src/main/modules/oidc/oidc.ts index 554200e91..f903a110c 100644 --- a/src/main/modules/oidc/oidc.ts +++ b/src/main/modules/oidc/oidc.ts @@ -13,9 +13,7 @@ export class OIDCModule { private readonly logger = Logger.getLogger('oidc'); constructor() { - void this.setupClient().catch(error => { - this.logger.error('Initial OIDC client setup failed, middleware will retry on demand:', error); - }); + this.setupClient(); } private async setupClient(): Promise { @@ -29,11 +27,9 @@ export class OIDCModule { // Create client with the actual issuer const clientId = this.oidcConfig.clientId; const clientSecret = config.get('secrets.pcs.pcs-frontend-idam-secret'); - const discoveryOptions = - issuer.protocol === 'http:' ? { execute: [client.allowInsecureRequests] } : undefined; // Create the client configuration with the server discovery - this.clientConfig = await client.discovery(issuer, clientId, clientSecret, undefined, discoveryOptions); + this.clientConfig = await client.discovery(issuer, clientId, clientSecret); } catch (error) { this.logger.error('Failed to setup OIDC client:', error); throw new OIDCAuthenticationError('Failed to initialize OIDC client'); diff --git a/src/main/steps/respond-to-claim/correspondence-address/index.ts b/src/main/steps/respond-to-claim/correspondence-address/index.ts index 1a513ff1f..62fef92cb 100644 --- a/src/main/steps/respond-to-claim/correspondence-address/index.ts +++ b/src/main/steps/respond-to-claim/correspondence-address/index.ts @@ -1,12 +1,11 @@ import isPostalCode from 'validator/lib/isPostalCode'; -import type { SubmitDefendantResponseData } from '../../../generated/ccd/PCS'; -import type { Address } from '../../../interfaces/ccdCase.interface'; +import type { Address, PossessionClaimResponse } from '../../../interfaces/ccdCase.interface'; import type { FormFieldConfig } from '../../../interfaces/formFieldConfig.interface'; import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; import { createFormStep, getFormData, getTranslationFunction, setFormData } from '../../../modules/steps'; import { ccdCaseService } from '../../../services/ccdCaseService'; -import { submitDefendantResponseDraft } from '../../utils/populateResponseToClaimPayloadmap'; +import { buildCcdCaseForPossessionClaimResponse as buildAndSubmitPossessionClaimResponse } from '../../utils/populateResponseToClaimPayloadmap'; import { flowConfig } from '../flow.config'; const STEP_NAME = 'postcode-finder'; @@ -113,16 +112,15 @@ export const step: StepDefinition = createFormStep({ pageTitle: 'pageTitle', }, beforeRedirect: async req => { - let responseData: SubmitDefendantResponseData; - + let possessionClaimResponse: PossessionClaimResponse; + //prepopulate address is correct if (req.body?.['correspondenceAddressConfirm'] === 'yes') { - if (!prepopulateAddress) { - throw new Error('Expected a prepopulated correspondence address'); - } - - responseData = { - correspondenceAddress: toGeneratedAddress(prepopulateAddress), - submitDraftAnswers: 'No', + possessionClaimResponse = { + defendantContactDetails: { + party: { + address: prepopulateAddress, + }, + }, }; } else { const addressLine1 = req.body?.['correspondenceAddressConfirm.addressLine1'] ?? ''; @@ -131,19 +129,23 @@ export const step: StepDefinition = createFormStep({ const county = req.body?.['correspondenceAddressConfirm.county']; const postcode = req.body?.['correspondenceAddressConfirm.postcode'] ?? ''; - responseData = { - correspondenceAddress: toGeneratedAddress({ - AddressLine1: addressLine1, - ...(addressLine2 !== undefined && addressLine2 !== '' && { AddressLine2: addressLine2 }), - PostTown: townOrCity, - ...(county !== undefined && county !== '' && { County: county }), - PostCode: postcode, - }), - submitDraftAnswers: 'No', + //only the details the defendant provides + possessionClaimResponse = { + defendantContactDetails: { + party: { + address: { + AddressLine1: addressLine1, + ...(addressLine2 !== undefined && addressLine2 !== '' && { AddressLine2: addressLine2 }), + PostTown: townOrCity, + ...(county !== undefined && county !== '' && { County: county }), + PostCode: postcode, + }, + }, + }, }; } - await submitDefendantResponseDraft(req, responseData); + await buildAndSubmitPossessionClaimResponse(req, possessionClaimResponse, false); }, extendGetContent: async (req, formContent) => { const t = getTranslationFunction(req, 'correspondence-address', ['common']); @@ -231,8 +233,9 @@ export const step: StepDefinition = createFormStep({ }); async function getExistingAddress(accessToken: string, caseReference: string): Promise { - const responseData = await ccdCaseService.getResponseToClaimData(accessToken, caseReference); - prepopulateAddress = responseData.correspondenceAddress; + // Pull data from API + const response = await ccdCaseService.getExistingCaseData(accessToken, caseReference); + prepopulateAddress = response.case_details.case_data.possessionClaimResponse?.defendantContactDetails?.party?.address; if (prepopulateAddress) { const formattedAddress = @@ -254,15 +257,3 @@ async function getExistingAddress(accessToken: string, caseReference: string): P return '?'; //no address } } - -function toGeneratedAddress(address: Address): SubmitDefendantResponseData['correspondenceAddress'] { - return { - AddressLine1: address.AddressLine1, - AddressLine2: address.AddressLine2 ?? '', - AddressLine3: address.AddressLine3 ?? '', - PostTown: address.PostTown, - County: address.County ?? '', - PostCode: address.PostCode, - Country: address.Country ?? '', - }; -} diff --git a/src/main/steps/utils/populateResponseToClaimPayloadmap.ts b/src/main/steps/utils/populateResponseToClaimPayloadmap.ts index 5812c74fd..8a84d0494 100644 --- a/src/main/steps/utils/populateResponseToClaimPayloadmap.ts +++ b/src/main/steps/utils/populateResponseToClaimPayloadmap.ts @@ -1,14 +1,20 @@ import { Request } from 'express'; -import type { SubmitDefendantResponseData } from '../../generated/ccd/PCS'; +import { CcdCase, PossessionClaimResponse } from '../../interfaces/ccdCase.interface'; import { ccdCaseService } from '../../services/ccdCaseService'; -export const submitDefendantResponseDraft = async ( +// Wrap the possession claim response in a ccd case object and submit via ccdCaseService +export const buildCcdCaseForPossessionClaimResponse = async ( req: Request, - responseData: SubmitDefendantResponseData -) => - ccdCaseService.submitResponseToClaim( - req.session?.user?.accessToken, - req.res?.locals.validatedCase?.id || '', - responseData - ); + possessionClaimResponse: PossessionClaimResponse, + submitDraftAnswers: boolean +): Promise => { + const ccdCase: CcdCase = { + id: req.res?.locals.validatedCase?.id, + data: { + possessionClaimResponse, + submitDraftAnswers: submitDraftAnswers ? 'Yes' : 'No', + }, + }; + return ccdCaseService.submitResponseToClaim(req.session?.user?.accessToken, ccdCase); +}; diff --git a/src/test/ui/data/api-data/createCase.api.data.ts b/src/test/ui/data/api-data/createCase.api.data.ts index ca917a019..cd70355e6 100644 --- a/src/test/ui/data/api-data/createCase.api.data.ts +++ b/src/test/ui/data/api-data/createCase.api.data.ts @@ -1,22 +1,18 @@ -import type { CreateClaimData } from '../../../../main/generated/ccd/PCS'; - -const createCasePayload = { - feeAmount: '£404', - propertyAddress: { - AddressLine1: '2 Second Avenue', - AddressLine2: '', - AddressLine3: '', - PostTown: 'London', - County: '', - PostCode: 'W3 7RX', - Country: 'United Kingdom', - }, - legislativeCountry: 'England', -} satisfies Partial; - export const createCaseApiData = { createCaseEventName: 'createPossessionClaim', - createCasePayload, + createCasePayload: { + feeAmount: '£404', + propertyAddress: { + AddressLine1: '2 Second Avenue', + AddressLine2: '', + AddressLine3: '', + PostTown: 'London', + County: '', + PostCode: 'W3 7RX', + Country: 'United Kingdom', + }, + legislativeCountry: 'England', + }, createCaseApiEndPoint: `/case-types/PCS${ process.env.PCS_API_CHANGE_ID ? '-' + process.env.PCS_API_CHANGE_ID : '' }/cases`, diff --git a/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts b/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts index 26178812a..5d2937cbf 100644 --- a/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts +++ b/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts @@ -9,7 +9,8 @@ import { submitCaseEventTokenApiData, } from '../../../data/api-data'; import { IAction, actionData, actionRecord } from '../../interfaces'; -import { caseBindings, type CreateClaimData } from '../../../../../main/generated/ccd/PCS'; +import { caseBindings } from '../../../../../main/generated/ccd/PCS/event-contracts'; +import type { CreateClaimData } from '../../../../../main/generated/ccd/PCS/dto-types'; export const caseInfo: { id: string; fid: string; state: string } = { id: '', fid: '', state: '' }; diff --git a/src/test/unit/modules/oidc/oidc.test.ts b/src/test/unit/modules/oidc/oidc.test.ts index 69aa2ca6b..cd4607e8b 100644 --- a/src/test/unit/modules/oidc/oidc.test.ts +++ b/src/test/unit/modules/oidc/oidc.test.ts @@ -148,15 +148,7 @@ describe('OIDCModule', () => { it('should successfully setup the OIDC client', async () => { await oidcModule['setupClient'](); - expect(discovery).toHaveBeenCalledWith( - expect.any(URL), - 'test-client-id', - 'test-secret', - undefined, - expect.objectContaining({ - execute: expect.any(Array), - }) - ); + expect(discovery).toHaveBeenCalledWith(expect.any(URL), 'test-client-id', 'test-secret'); }); it('should throw OIDCAuthenticationError when setup fails', async () => { diff --git a/src/test/unit/ui/createCaseAPI.action.test.ts b/src/test/unit/ui/createCaseAPI.action.test.ts deleted file mode 100644 index 2dbb32093..000000000 --- a/src/test/unit/ui/createCaseAPI.action.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { AxiosInstance } from 'axios'; -import Axios from 'axios'; - -import { createCaseApiData } from '../../ui/data/api-data'; -import { - caseInfo, - CreateCaseAPIAction, -} from '../../ui/utils/actions/custom-actions/createCaseAPI.action'; - -jest.mock('axios'); - -describe('CreateCaseAPIAction', () => { - const mockGet = jest.fn(); - const mockPost = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - process.env.DATA_STORE_URL_BASE = 'http://ccd.example.com'; - process.env.BEARER_TOKEN = 'bearer-token'; - process.env.SERVICE_AUTH_TOKEN = 'service-token'; - delete process.env.CASE_NUMBER; - caseInfo.id = ''; - caseInfo.fid = ''; - caseInfo.state = ''; - - (Axios.create as jest.Mock).mockReturnValue({ - get: mockGet, - post: mockPost, - } as Partial); - }); - - it('creates a CCD case via the generated runtime client', async () => { - mockGet.mockResolvedValue({ - data: { - token: 'event-token', - case_details: { - case_data: { - cpcfeeAmount: '£999999.99', - }, - }, - }, - }); - mockPost.mockResolvedValue({ - data: { - id: '1773696232722684', - state: 'AWAITING_SUBMISSION_TO_HMCTS', - }, - }); - - const action = new CreateCaseAPIAction(); - await action.execute({} as never, 'createCaseAPI', createCaseApiData.createCasePayload); - - expect(mockGet).toHaveBeenCalledWith( - 'http://ccd.example.com/case-types/PCS/event-triggers/createPossessionClaim', - expect.any(Object) - ); - expect(mockPost).toHaveBeenCalledWith( - 'http://ccd.example.com/case-types/PCS/cases', - expect.objectContaining({ - data: expect.objectContaining({ - cpcfeeAmount: '£404', - cpclegislativeCountry: 'England', - cpcpropertyAddress: createCaseApiData.createCasePayload.propertyAddress, - }), - event: { id: 'createPossessionClaim' }, - event_token: 'event-token', - ignore_warning: false, - }), - expect.any(Object) - ); - expect(process.env.CASE_NUMBER).toBe('1773696232722684'); - expect(caseInfo.id).toBe('1773696232722684'); - expect(caseInfo.fid).toBe('1773-6962-3272-2684'); - expect(caseInfo.state).toBe('AWAITING_SUBMISSION_TO_HMCTS'); - }); -}); diff --git a/tsconfig.generated-consumers.json b/tsconfig.generated-consumers.json index 4ad31792e..b97b3a71d 100644 --- a/tsconfig.generated-consumers.json +++ b/tsconfig.generated-consumers.json @@ -6,13 +6,11 @@ }, "include": [ "src/main/generated/**/*", - "src/main/examples/**/*", "src/test/ui/data/api-data/createCase.api.data.ts", "src/test/ui/data/api-data/index.ts", "src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts", "src/test/ui/utils/actions/custom-actions/index.ts", - "src/test/unit/generated/**/*.ts", - "src/test/unit/ui/**/*.ts" + "src/test/unit/generated/**/*.ts" ], "exclude": [] } From 87be17a434bc2f198ea9ca339f5d951a0a32547e Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Thu, 19 Mar 2026 20:44:05 +0000 Subject: [PATCH 08/11] trim --- src/main/generated/ccd/PCS/dto-types.ts | 5 ----- src/main/generated/ccd/PCS/event-contracts.ts | 7 +------ 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/main/generated/ccd/PCS/dto-types.ts b/src/main/generated/ccd/PCS/dto-types.ts index c7dc381b9..c24fcdee1 100644 --- a/src/main/generated/ccd/PCS/dto-types.ts +++ b/src/main/generated/ccd/PCS/dto-types.ts @@ -14,11 +14,6 @@ export interface CreateClaimData { postcodeNotAssignedView: string; } -export interface SubmitDefendantResponseData { - correspondenceAddress: AddressUK; - submitDraftAnswers: YesOrNo; -} - export interface AddressUK extends Address { } diff --git a/src/main/generated/ccd/PCS/event-contracts.ts b/src/main/generated/ccd/PCS/event-contracts.ts index 3b798158d..15ad2a1f0 100644 --- a/src/main/generated/ccd/PCS/event-contracts.ts +++ b/src/main/generated/ccd/PCS/event-contracts.ts @@ -1,11 +1,10 @@ // Generated by CCD SDK. Do not edit manually. // Module: pcs import { defineCaseBindings, type CcdCaseBindings } from "@hmcts/ccd-event-runtime"; -import type { CreateClaimData, SubmitDefendantResponseData } from "./dto-types"; +import type { CreateClaimData } from "./dto-types"; export interface EventDtoMap { "createPossessionClaim": CreateClaimData; - "submitDefendantResponse": SubmitDefendantResponseData; } export const caseBindings = defineCaseBindings()({ @@ -15,9 +14,5 @@ export const caseBindings = defineCaseBindings()({ fieldPrefix: "cpc", pages: ["crossBorderPostcodeSelection", "enterPropertyAddress", "postcodeNotAssignedToCourt", "propertyNotEligible", "startTheService"], }, - "submitDefendantResponse": { - fieldPrefix: "sdr", - pages: [], - }, }, } as const satisfies CcdCaseBindings); From eb6e1a832fed0a770e077796f739c31797cbc7e0 Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Thu, 19 Mar 2026 20:54:06 +0000 Subject: [PATCH 09/11] trim --- .../custom-actions/createCaseAPI.action.ts | 89 ++++++++----------- 1 file changed, 36 insertions(+), 53 deletions(-) diff --git a/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts b/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts index 5d2937cbf..6bc8327f3 100644 --- a/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts +++ b/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts @@ -1,9 +1,10 @@ import { Page } from '@playwright/test'; // eslint-disable-next-line import/no-named-as-default -import Axios, { AxiosInstance } from 'axios'; -import { createCcdClient, type CcdClientConfig, type CcdTransport } from '@hmcts/ccd-event-runtime'; +import Axios from 'axios'; +import { createCcdClient } from '@hmcts/ccd-event-runtime'; import { + createCaseApiData, createCaseEventTokenApiData, submitCaseApiData, submitCaseEventTokenApiData, @@ -14,52 +15,6 @@ import type { CreateClaimData } from '../../../../../main/generated/ccd/PCS/dto- export const caseInfo: { id: string; fid: string; state: string } = { id: '', fid: '', state: '' }; -interface CreatedCaseResponse { - id?: string; - state?: string; -} - -function createTransport(api: AxiosInstance): CcdTransport { - return { - get: async (url, headers) => (await api.get(url, { headers })).data, - post: async (url, data, headers) => (await api.post(url, data, { headers })).data, - }; -} - -function createCaseClient() { - const requestConfig = createCaseEventTokenApiData.createCaseApiInstance(); - const api = Axios.create(requestConfig); - const baseUrl = requestConfig.baseURL; - if (!baseUrl) { - throw new Error('Missing DATA_STORE_URL_BASE for createCaseAPI'); - } - - const clientConfig: CcdClientConfig = { - baseUrl, - getAuthHeaders: () => (requestConfig.headers ?? {}) as Record, - transport: createTransport(api), - }; - - return createCcdClient(clientConfig, caseBindings); -} - -function extractCreateCasePayload(caseData: actionData): Partial { - const payload = typeof caseData === 'object' && caseData !== null && 'data' in caseData ? caseData.data : caseData; - return (payload ?? {}) as Partial; -} - -function setCaseInfo(createResponse: CreatedCaseResponse): void { - const caseId = String(createResponse.id ?? ''); - if (!caseId) { - throw new Error('Create case response did not include a case id'); - } - - process.env.CASE_NUMBER = caseId; - caseInfo.id = caseId; - caseInfo.fid = caseId.replace(/(.{4})(?=.)/g, '$1-'); - caseInfo.state = createResponse.state ?? ''; -} - export class CreateCaseAPIAction implements IAction { async execute(page: Page, action: string, fieldName: actionData | actionRecord): Promise { const actionsMap = new Map Promise>([ @@ -74,13 +29,41 @@ export class CreateCaseAPIAction implements IAction { } private async createCaseAPI(caseData: actionData): Promise { - const client = createCaseClient(); + const requestConfig = createCaseEventTokenApiData.createCaseApiInstance(); + const baseUrl = requestConfig.baseURL; + if (!baseUrl) { + throw new Error('Missing DATA_STORE_URL_BASE for createCaseAPI'); + } + + const createCaseApi = Axios.create(requestConfig); + const client = createCcdClient( + { + baseUrl, + getAuthHeaders: () => (requestConfig.headers ?? {}) as Record, + transport: { + get: async (url, headers) => (await createCaseApi.get(url, { headers })).data, + post: async (url, data, headers) => (await createCaseApi.post(url, data, { headers })).data, + }, + }, + caseBindings + ); const flow = await client.event('createPossessionClaim').start(); - const createResponse = (await flow.submit({ + const createCasePayloadData = ( + typeof caseData === 'object' && caseData !== null && 'data' in caseData ? caseData.data : caseData + ) as Partial; + const createResponse = await flow.submit({ ...flow.data, - ...extractCreateCasePayload(caseData), - } as CreateClaimData)) as CreatedCaseResponse; - setCaseInfo(createResponse); + ...createCasePayloadData, + } as CreateClaimData); + const caseId = String(createResponse.id ?? ''); + if (!caseId) { + throw new Error(`Create case response did not include a case id for ${createCaseApiData.createCaseEventName}`); + } + + process.env.CASE_NUMBER = caseId; + caseInfo.id = caseId; + caseInfo.fid = caseId.replace(/(.{4})(?=.)/g, '$1-'); + caseInfo.state = String(createResponse.state ?? ''); } private async submitCaseAPI(caseData: actionData): Promise { From ba841f2a03d0212d600c9333a86e5ebd5bd670e6 Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Thu, 19 Mar 2026 21:08:03 +0000 Subject: [PATCH 10/11] compare --- .../custom-actions/createCaseAPI.action.ts | 51 ++++------------ .../createCaseTypedAPI.action.ts | 59 +++++++++++++++++++ .../ui/utils/actions/custom-actions/index.ts | 1 + src/test/ui/utils/registry/action.registry.ts | 2 + tsconfig.generated-consumers.json | 2 +- 5 files changed, 76 insertions(+), 39 deletions(-) create mode 100644 src/test/ui/utils/actions/custom-actions/createCaseTypedAPI.action.ts diff --git a/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts b/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts index 6bc8327f3..854757af6 100644 --- a/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts +++ b/src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts @@ -1,7 +1,6 @@ import { Page } from '@playwright/test'; // eslint-disable-next-line import/no-named-as-default import Axios from 'axios'; -import { createCcdClient } from '@hmcts/ccd-event-runtime'; import { createCaseApiData, @@ -10,8 +9,6 @@ import { submitCaseEventTokenApiData, } from '../../../data/api-data'; import { IAction, actionData, actionRecord } from '../../interfaces'; -import { caseBindings } from '../../../../../main/generated/ccd/PCS/event-contracts'; -import type { CreateClaimData } from '../../../../../main/generated/ccd/PCS/dto-types'; export const caseInfo: { id: string; fid: string; state: string } = { id: '', fid: '', state: '' }; @@ -29,41 +26,19 @@ export class CreateCaseAPIAction implements IAction { } private async createCaseAPI(caseData: actionData): Promise { - const requestConfig = createCaseEventTokenApiData.createCaseApiInstance(); - const baseUrl = requestConfig.baseURL; - if (!baseUrl) { - throw new Error('Missing DATA_STORE_URL_BASE for createCaseAPI'); - } - - const createCaseApi = Axios.create(requestConfig); - const client = createCcdClient( - { - baseUrl, - getAuthHeaders: () => (requestConfig.headers ?? {}) as Record, - transport: { - get: async (url, headers) => (await createCaseApi.get(url, { headers })).data, - post: async (url, data, headers) => (await createCaseApi.post(url, data, { headers })).data, - }, - }, - caseBindings - ); - const flow = await client.event('createPossessionClaim').start(); - const createCasePayloadData = ( - typeof caseData === 'object' && caseData !== null && 'data' in caseData ? caseData.data : caseData - ) as Partial; - const createResponse = await flow.submit({ - ...flow.data, - ...createCasePayloadData, - } as CreateClaimData); - const caseId = String(createResponse.id ?? ''); - if (!caseId) { - throw new Error(`Create case response did not include a case id for ${createCaseApiData.createCaseEventName}`); - } - - process.env.CASE_NUMBER = caseId; - caseInfo.id = caseId; - caseInfo.fid = caseId.replace(/(.{4})(?=.)/g, '$1-'); - caseInfo.state = String(createResponse.state ?? ''); + const createCaseApi = Axios.create(createCaseEventTokenApiData.createCaseApiInstance()); + const CREATE_EVENT_TOKEN = (await createCaseApi.get(createCaseEventTokenApiData.createCaseEventTokenApiEndPoint)) + .data.token; + const createCasePayloadData = typeof caseData === 'object' && 'data' in caseData ? caseData.data : caseData; + const createResponse = await createCaseApi.post(createCaseApiData.createCaseApiEndPoint, { + data: createCasePayloadData, + event: { id: createCaseApiData.createCaseEventName }, + event_token: CREATE_EVENT_TOKEN, + }); + process.env.CASE_NUMBER = createResponse.data.id; + caseInfo.id = createResponse.data.id; + caseInfo.fid = createResponse.data.id.replace(/(.{4})(?=.)/g, '$1-'); + caseInfo.state = createResponse.data.state; } private async submitCaseAPI(caseData: actionData): Promise { diff --git a/src/test/ui/utils/actions/custom-actions/createCaseTypedAPI.action.ts b/src/test/ui/utils/actions/custom-actions/createCaseTypedAPI.action.ts new file mode 100644 index 000000000..e4f3efdcb --- /dev/null +++ b/src/test/ui/utils/actions/custom-actions/createCaseTypedAPI.action.ts @@ -0,0 +1,59 @@ +import { Page } from '@playwright/test'; +// eslint-disable-next-line import/no-named-as-default +import Axios from 'axios'; +import { createCcdClient } from '@hmcts/ccd-event-runtime'; + +import { createCaseEventTokenApiData } from '../../../data/api-data'; +import { IAction, actionData, actionRecord } from '../../interfaces'; +import { caseInfo } from './createCaseAPI.action'; +import { caseBindings } from '../../../../../main/generated/ccd/PCS/event-contracts'; +import type { CreateClaimData } from '../../../../../main/generated/ccd/PCS/dto-types'; + +export class CreateCaseTypedAPIAction implements IAction { + async execute(page: Page, action: string, fieldName: actionData | actionRecord): Promise { + if (action !== 'createCaseTypedAPI') { + throw new Error(`No action found for '${action}'`); + } + await this.createCaseTypedAPI(fieldName); + } + + private async createCaseTypedAPI(caseData: actionData): Promise { + const requestConfig = createCaseEventTokenApiData.createCaseApiInstance(); + const baseUrl = requestConfig.baseURL; + if (!baseUrl) { + throw new Error('Missing DATA_STORE_URL_BASE for createCaseTypedAPI'); + } + + const createCaseApi = Axios.create(requestConfig); + const client = createCcdClient( + { + baseUrl, + getAuthHeaders: () => (requestConfig.headers ?? {}) as Record, + transport: { + get: async (url, headers) => (await createCaseApi.get(url, { headers })).data, + post: async (url, data, headers) => (await createCaseApi.post(url, data, { headers })).data, + }, + }, + caseBindings + ); + + const flow = await client.event('createPossessionClaim').start(); + const createCasePayloadData = ( + typeof caseData === 'object' && caseData !== null && 'data' in caseData ? caseData.data : caseData + ) as Partial; + const createResponse = await flow.submit({ + ...flow.data, + ...createCasePayloadData, + } as CreateClaimData); + const caseId = String(createResponse.id ?? ''); + + if (!caseId) { + throw new Error('Create case response did not include a case id'); + } + + process.env.CASE_NUMBER = caseId; + caseInfo.id = caseId; + caseInfo.fid = caseId.replace(/(.{4})(?=.)/g, '$1-'); + caseInfo.state = String(createResponse.state ?? ''); + } +} diff --git a/src/test/ui/utils/actions/custom-actions/index.ts b/src/test/ui/utils/actions/custom-actions/index.ts index 4a1583a20..39e04ab9e 100644 --- a/src/test/ui/utils/actions/custom-actions/index.ts +++ b/src/test/ui/utils/actions/custom-actions/index.ts @@ -1,4 +1,5 @@ export * from '../../actions/custom-actions/createCaseAPI.action'; +export * from '../../actions/custom-actions/createCaseTypedAPI.action'; export * from '../../actions/custom-actions/login.action'; export * from '../../actions/custom-actions/navigateToUrl.action'; export * from '../../actions/custom-actions/fetchPINsAndValidateAccessCodeAPI.action'; diff --git a/src/test/ui/utils/registry/action.registry.ts b/src/test/ui/utils/registry/action.registry.ts index 2bf71c5f7..e7e96bc4e 100644 --- a/src/test/ui/utils/registry/action.registry.ts +++ b/src/test/ui/utils/registry/action.registry.ts @@ -1,5 +1,6 @@ import { CreateCaseAPIAction, + CreateCaseTypedAPIAction, FetchPINsAndValidateAccessCodeAPIAction, LoginAction, NavigateToUrlAction, @@ -36,6 +37,7 @@ export class ActionRegistry { ['createUser', new LoginAction()], ['navigateToUrl', new NavigateToUrlAction()], ['createCaseAPI', new CreateCaseAPIAction()], + ['createCaseTypedAPI', new CreateCaseTypedAPIAction()], ['submitCaseAPI', new CreateCaseAPIAction()], ['selectLegalAdvice', new RespondToClaimAction()], ['inputDefendantDetails', new RespondToClaimAction()], diff --git a/tsconfig.generated-consumers.json b/tsconfig.generated-consumers.json index b97b3a71d..e7679b985 100644 --- a/tsconfig.generated-consumers.json +++ b/tsconfig.generated-consumers.json @@ -8,7 +8,7 @@ "src/main/generated/**/*", "src/test/ui/data/api-data/createCase.api.data.ts", "src/test/ui/data/api-data/index.ts", - "src/test/ui/utils/actions/custom-actions/createCaseAPI.action.ts", + "src/test/ui/utils/actions/custom-actions/createCaseTypedAPI.action.ts", "src/test/ui/utils/actions/custom-actions/index.ts", "src/test/unit/generated/**/*.ts" ], From cfb8a3c7c7fa1f245633028e0051acf21ebe791d Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Fri, 20 Mar 2026 09:39:23 +0000 Subject: [PATCH 11/11] tidy --- package.json | 1 - .../ui/utils/actions/custom-actions/index.ts | 1 - src/test/ui/utils/registry/action.registry.ts | 2 +- tsconfig.generated-consumers.json | 16 ---------------- 4 files changed, 1 insertion(+), 19 deletions(-) delete mode 100644 tsconfig.generated-consumers.json diff --git a/package.json b/package.json index 8caeeb342..63a2332f9 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "test:unit": "jest", "test:coverage": "jest --coverage", "test:routes": "jest -c jest.routes.config.ts", - "typecheck:generated-consumers": "tsc --noEmit -p tsconfig.generated-consumers.json", "test:a11y": "echo 'Accessibility tests implemented later in the pipeline'", "test:accessibility": "yarn playwright install && yarn playwright test --project chromium --grep @accessibility; EXIT_CODE=$?; allure generate --clean; ts-node src/test/ui/config/clean-attachments.config.ts; exit $EXIT_CODE", "test:smoke": "NODE_TLS_REJECT_UNAUTHORIZED=0 jest -c jest.smoke.config.ts", diff --git a/src/test/ui/utils/actions/custom-actions/index.ts b/src/test/ui/utils/actions/custom-actions/index.ts index 39e04ab9e..4a1583a20 100644 --- a/src/test/ui/utils/actions/custom-actions/index.ts +++ b/src/test/ui/utils/actions/custom-actions/index.ts @@ -1,5 +1,4 @@ export * from '../../actions/custom-actions/createCaseAPI.action'; -export * from '../../actions/custom-actions/createCaseTypedAPI.action'; export * from '../../actions/custom-actions/login.action'; export * from '../../actions/custom-actions/navigateToUrl.action'; export * from '../../actions/custom-actions/fetchPINsAndValidateAccessCodeAPI.action'; diff --git a/src/test/ui/utils/registry/action.registry.ts b/src/test/ui/utils/registry/action.registry.ts index e7e96bc4e..8d552e750 100644 --- a/src/test/ui/utils/registry/action.registry.ts +++ b/src/test/ui/utils/registry/action.registry.ts @@ -1,12 +1,12 @@ import { CreateCaseAPIAction, - CreateCaseTypedAPIAction, FetchPINsAndValidateAccessCodeAPIAction, LoginAction, NavigateToUrlAction, RespondToClaimAction, TriggerErrorMessagesAction, } from '../actions/custom-actions'; +import { CreateCaseTypedAPIAction } from '../actions/custom-actions/createCaseTypedAPI.action'; import { CheckAction, ClickButtonAction, diff --git a/tsconfig.generated-consumers.json b/tsconfig.generated-consumers.json deleted file mode 100644 index e7679b985..000000000 --- a/tsconfig.generated-consumers.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "noEmit": true, - "rootDir": "." - }, - "include": [ - "src/main/generated/**/*", - "src/test/ui/data/api-data/createCase.api.data.ts", - "src/test/ui/data/api-data/index.ts", - "src/test/ui/utils/actions/custom-actions/createCaseTypedAPI.action.ts", - "src/test/ui/utils/actions/custom-actions/index.ts", - "src/test/unit/generated/**/*.ts" - ], - "exclude": [] -}