From 0ce43212294fb11db28826006b95e8b0608ad82f Mon Sep 17 00:00:00 2001 From: arun Date: Sun, 8 Mar 2026 22:02:11 +0000 Subject: [PATCH 01/23] HDPI-3451: Add rent arrears dispute step with framework validation pattern and subField error translation support --- .../cy/respondToClaim/rentArrearsDispute.json | 32 +++- .../en/respondToClaim/rentArrearsDispute.json | 32 +++- .../interfaces/formFieldConfig.interface.ts | 4 + src/main/modules/index.ts | 14 +- src/main/modules/steps/formBuilder/helpers.ts | 28 +-- src/main/services/ccdCaseService.ts | 10 +- .../rent-arrears-dispute/index.ts | 177 +++++++++++++++++- .../rent-arrears-dispute/rentArrears.njk | 56 ++++++ src/main/steps/utils/getClaimantName.ts | 10 + .../steps/utils/hasOnlyRentArrearsGrounds.ts | 9 + src/main/steps/utils/index.ts | 2 + 11 files changed, 352 insertions(+), 22 deletions(-) create mode 100644 src/main/steps/respond-to-claim/rent-arrears-dispute/rentArrears.njk create mode 100644 src/main/steps/utils/getClaimantName.ts create mode 100644 src/main/steps/utils/hasOnlyRentArrearsGrounds.ts diff --git a/src/main/assets/locales/cy/respondToClaim/rentArrearsDispute.json b/src/main/assets/locales/cy/respondToClaim/rentArrearsDispute.json index 1aac483ad..93954603c 100644 --- a/src/main/assets/locales/cy/respondToClaim/rentArrearsDispute.json +++ b/src/main/assets/locales/cy/respondToClaim/rentArrearsDispute.json @@ -1,4 +1,32 @@ { - "caption": "cyRespond to a property possession claim", - "pageTitle": "cyRent Arrears Dispute(Placeholder)" + "title": "cyRent arrears", + "captionHeading": "cyRespond to a property possession claim", + "pageTitle": "cyRent arrears", + "insetIntroText": "cyRent arrears are money you owe in rent payments.", + "insetDetailsText": "cyWhen making their claim, {{claimantName}} had to provide a copy of the rent statement for your property, showing the total rent arrears you owe.", + "insetConditionalYesText": "cyThe rent statement will have been included in the pack you received on the post letting you know a claim had been made against you, and is also available to view from 'View the claim' on your case dashboard.", + "amountOwedHeading": "cyAmount you owe in rent arrears given by {{claimantName}}:", + "rentArrearsQuestion": "cyDo you owe this amount in rent arrears?", + "rentArrearsOptions": { + "yes": "cyYes", + "no": "cyNo", + "or": "cyor", + "notSure": "cyI'm not sure" + }, + "rentArrearsAmountCorrection": { + "label": "cyWhat amount do you believe you owe?", + "hint": "cyEnter the amount in pounds and pence, for example 123.45" + }, + "errors": { + "rentArrears": { + "required": "cySelect yes if you owe this amount in rent arrears" + }, + "rentArrearsAmountCorrection": { + "required": "cyEnter the amount you believe you owe in rent arrears", + "negativeAmount": "cyThe amount you believe you owe in rent arrears must be £0.00 or above", + "largeAmount": "cyEnter how much you believe you owe in rent arrears, in the correct format, up to £1,000,000,000.00", + "invalidFormat": "cyEnter how much you believe you owe in rent arrears, in the correct format (e.g. if you owe £148, please write £148.00)" + }, + "rentArrears.rentArrearsAmountCorrection": "cyEnter the amount you believe you owe in rent arrears" + } } diff --git a/src/main/assets/locales/en/respondToClaim/rentArrearsDispute.json b/src/main/assets/locales/en/respondToClaim/rentArrearsDispute.json index 8c48617e7..ec6f8aace 100644 --- a/src/main/assets/locales/en/respondToClaim/rentArrearsDispute.json +++ b/src/main/assets/locales/en/respondToClaim/rentArrearsDispute.json @@ -1,4 +1,32 @@ { - "caption": "Respond to a property possession claim", - "pageTitle": "Rent Arrears Dispute(Placeholder)" + "title": "Rent arrears", + "captionHeading": "Respond to a property possession claim", + "pageTitle": "Rent arrears", + "insetIntroText": "Rent arrears are money you owe in rent payments.", + "insetDetailsText": "When making their claim, {{claimantName}} had to provide a copy of the rent statement for your property, showing the total rent arrears you owe.", + "insetConditionalYesText": "The rent statement will have been included in the pack you received on the post letting you know a claim had been made against you, and is also available to view from 'View the claim' on your case dashboard.", + "amountOwedHeading": "Amount you owe in rent arrears given by {{claimantName}}:", + "rentArrearsQuestion": "Do you owe this amount in rent arrears?", + "rentArrearsOptions": { + "yes": "Yes", + "no": "No", + "or": "or", + "notSure": "I'm not sure" + }, + "rentArrearsAmountCorrection": { + "label": "What amount do you believe you owe?", + "hint": "Enter the amount in pounds and pence, for example 123.45" + }, + "errors": { + "rentArrears": { + "required": "Select yes if you owe this amount in rent arrears" + }, + "rentArrearsAmountCorrection": { + "required": "Enter the amount you believe you owe in rent arrears", + "negativeAmount": "The amount you believe you owe in rent arrears must be £0.00 or above", + "largeAmount": "Enter how much you believe you owe in rent arrears, in the correct format, up to £1,000,000,000.00", + "invalidFormat": "Enter how much you believe you owe in rent arrears, in the correct format (e.g. if you owe £148, please write £148.00)" + }, + "rentArrears.rentArrearsAmountCorrection": "Enter the amount you believe you owe in rent arrears" + } } diff --git a/src/main/interfaces/formFieldConfig.interface.ts b/src/main/interfaces/formFieldConfig.interface.ts index e1b8912b8..782dbb693 100644 --- a/src/main/interfaces/formFieldConfig.interface.ts +++ b/src/main/interfaces/formFieldConfig.interface.ts @@ -41,6 +41,10 @@ export interface FormFieldConfig { options?: FormFieldOption[]; classes?: string; attributes?: Record; + // Optional prefix for input fields (e.g. currency symbol) + prefix?: { + text: string; + }; legendClasses?: string; // Pre-built component config for Nunjucks template rendering component?: Record; diff --git a/src/main/modules/index.ts b/src/main/modules/index.ts index 58c8534a6..6dc55692e 100644 --- a/src/main/modules/index.ts +++ b/src/main/modules/index.ts @@ -1,12 +1,24 @@ +import config from 'config'; + +import type { OIDCConfig } from './oidc/config.interface'; +import { OIDCModule as OIDCProductionModule } from './oidc/oidc'; +import { OIDCLocalModule } from './oidc/oidc-local'; + export { http } from './http'; export { S2S } from './s2s'; export { Helmet } from './helmet'; export { Nunjucks } from './nunjucks'; -export { OIDCModule } from './oidc'; export { Session } from './session'; export { LaunchDarkly } from './launch-darkly'; export { I18n } from './i18n'; export { Logger } from './logger'; +// Dynamic OIDC module selection based on issuer URL +const oidcConfig: OIDCConfig = config.get('oidc'); +const isLocalDevelopment = oidcConfig.issuer.includes('localhost'); + +// Export the appropriate OIDC module based on environment +export const OIDCModule = isLocalDevelopment ? OIDCLocalModule : OIDCProductionModule; + // this is used to register the modules with the app in a certain order export const modules = ['I18n', 'Nunjucks', 'Helmet', 'Session', 'S2S', 'OIDCModule', 'LaunchDarkly']; diff --git a/src/main/modules/steps/formBuilder/helpers.ts b/src/main/modules/steps/formBuilder/helpers.ts index 46c32eb08..b7ac9526b 100644 --- a/src/main/modules/steps/formBuilder/helpers.ts +++ b/src/main/modules/steps/formBuilder/helpers.ts @@ -173,7 +173,6 @@ export function getTranslationErrors( export function getCustomErrorTranslations(t: TFunction, fields: FormFieldConfig[]): Record { const stepSpecificErrors: Record = {}; - const nestedKeys = ['required', 'custom', 'missingOne', 'missingTwo', 'futureDate']; const commonErrorKeys = ['defaultRequired', 'defaultInvalid', 'defaultMaxLength']; for (const key of commonErrorKeys) { @@ -196,19 +195,26 @@ export function getCustomErrorTranslations(t: TFunction, fields: FormFieldConfig const visitField = (field: FormFieldConfig): void => { addMaxLengthTranslation(field.name); - // Keep existing nested error support for non-nested names (e.g., date fields) + // Auto-discover all error translations for this field from the step's translation file + // This eliminates the need for a hardcoded nestedKeys list if (!field.name.includes('.')) { - for (const nestedKey of nestedKeys) { - const nestedErrorKey = `errors.${field.name}.${nestedKey}`; - const nestedError = t(nestedErrorKey); - if (nestedError && nestedError !== nestedErrorKey) { - if (field.type === 'date') { - const dateKey = getDateTranslationKey(nestedKey); - if (dateKey) { - stepSpecificErrors[dateKey] = nestedError; + const errorNamespace = `errors.${field.name}`; + const allFieldErrors = t(errorNamespace, { returnObjects: true }); + + // If the translation exists and is an object (not a string), it contains error keys + if (allFieldErrors && typeof allFieldErrors === 'object' && !Array.isArray(allFieldErrors)) { + for (const [errorKey, errorMessage] of Object.entries(allFieldErrors)) { + if (typeof errorMessage === 'string') { + // Special handling for date fields - map to date-specific keys + if (field.type === 'date') { + const dateKey = getDateTranslationKey(errorKey); + if (dateKey) { + stepSpecificErrors[dateKey] = errorMessage; + } } + // Add the standard field.errorKey mapping + stepSpecificErrors[`${field.name}.${errorKey}`] = errorMessage; } - stepSpecificErrors[`${field.name}.${nestedKey}`] = nestedError; } } } diff --git a/src/main/services/ccdCaseService.ts b/src/main/services/ccdCaseService.ts index 071c84ff2..4dc2d8da9 100644 --- a/src/main/services/ccdCaseService.ts +++ b/src/main/services/ccdCaseService.ts @@ -47,7 +47,7 @@ interface EventTokenResponse { } function getBaseUrl(): string { - return config.get('ccd.url'); + return config.get('ccd.url'); } function getApiUrl(): string { @@ -158,10 +158,15 @@ async function submitEvent( export const ccdCaseService = { async getCaseById(accessToken: string, caseId: string, eventId: string = 'respondPossessionClaim'): Promise { - const eventUrl = `${getBaseUrl()}/cases/${caseId}/event-triggers/${eventId}?ignore-warning=false`; + const ccdUrl = getBaseUrl(); + const eventUrl = `${ccdUrl}/cases/${caseId}/event-triggers/${eventId}?ignore-warning=false`; try { logger.info(`[ccdCaseService] Validating case access for caseId: ${caseId}, eventId: ${eventId}`); + logger.info(`[ccdCaseService] CCD URL: ${ccdUrl}`); + logger.info(`[ccdCaseService] Full URL: ${eventUrl}`); + logger.info(`[ccdCaseService] Access token: ${accessToken ? 'present' : 'MISSING'}`); + const response = await http.get<{ case_details?: { case_data?: Record } }>( eventUrl, getCaseHeaders(accessToken) @@ -173,6 +178,7 @@ export const ccdCaseService = { data: response.data.case_details?.case_data || {}, }; } catch (error) { + logger.error('[ccdCaseService] getCaseById failed:', error); throw convertAxiosErrorToHttpError(error, 'getCaseById'); } }, diff --git a/src/main/steps/respond-to-claim/rent-arrears-dispute/index.ts b/src/main/steps/respond-to-claim/rent-arrears-dispute/index.ts index 2d71ff5be..6c01a2a75 100644 --- a/src/main/steps/respond-to-claim/rent-arrears-dispute/index.ts +++ b/src/main/steps/respond-to-claim/rent-arrears-dispute/index.ts @@ -1,17 +1,186 @@ +import type { Request } from 'express'; + +import type { FormFieldConfig } from '../../../interfaces/formFieldConfig.interface'; import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; +import { currency } from '../../../modules/nunjucks/filters/currency'; +import { createFormStep, getTranslationFunction } from '../../../modules/steps'; +import { getClaimantName } from '../../utils'; import { flowConfig } from '../flow.config'; -import { createFormStep } from '@modules/steps'; +// Define fields separately so we can dynamically inject validator +const fieldsConfig: FormFieldConfig[] = [ + { + name: 'rentArrears', + type: 'radio', + required: true, + translationKey: { + label: 'rentArrearsQuestion', + }, + legendClasses: 'govuk-fieldset__legend--m', + options: [ + { + value: 'yes', + translationKey: 'rentArrearsOptions.yes', + }, + { + value: 'no', + translationKey: 'rentArrearsOptions.no', + subFields: { + rentArrearsAmountCorrection: { + name: 'rentArrearsAmountCorrection', + type: 'text', + required: true, + translationKey: { + label: 'rentArrearsAmountCorrection.label', + hint: 'rentArrearsAmountCorrection.hint', + }, + classes: 'govuk-input--width-10', + prefix: { + text: '£', + }, + attributes: { + inputmode: 'decimal', + spellcheck: false, + }, + }, + }, + }, + { divider: 'rentArrearsOptions.or', translationKey: 'rentArrearsOptions.or' }, + { value: 'notSure', translationKey: 'rentArrearsOptions.notSure' }, + ], + }, +]; export const step: StepDefinition = createFormStep({ stepName: 'rent-arrears-dispute', journeyFolder: 'respondToClaim', stepDir: __dirname, flowConfig, - customTemplate: `${__dirname}/rentArrearsDispute.njk`, + customTemplate: `${__dirname}/rentArrears.njk`, translationKeys: { pageTitle: 'pageTitle', - caption: 'caption', + caption: 'captionHeading', + }, + ccdMapping: { + backendPath: 'possessionClaimResponse.defendantResponses', + frontendFields: ['rentArrears', 'rentArrears.rentArrearsAmountCorrection'], + valueMapper: (formData, _ctx) => { + // Handle single value case (shouldn't happen for this step, but for type safety) + if (typeof formData === 'string' || Array.isArray(formData)) { + return {}; + } + + const rentArrears = formData.rentArrears as 'yes' | 'no' | 'notSure' | undefined; + const amountRaw = formData['rentArrears.rentArrearsAmountCorrection'] as string | undefined; + + const result: Record = {}; + + // Map frontend radio value to backend enum + if (rentArrears === 'yes') { + result.oweRentArrears = 'YES'; + } else if (rentArrears === 'no') { + result.oweRentArrears = 'NO'; + // Convert pounds to pence (backend stores as pence string) + if (amountRaw) { + const normalized = amountRaw.replace(/,/g, ''); // Remove comma separators + const amountInPounds = parseFloat(normalized); + if (!Number.isNaN(amountInPounds)) { + result.rentArrearsAmount = String(Math.round(amountInPounds * 100)); + } + } + } else if (rentArrears === 'notSure') { + result.oweRentArrears = 'NOT_SURE'; + } + + return result; + }, + }, + getInitialFormData: (req: Request) => { + const caseData = req.res?.locals?.validatedCase?.data; + const response = caseData?.possessionClaimResponse?.defendantResponses; + + if (!response?.oweRentArrears) { + return {}; + } + + const formData: Record = {}; + + // Map backend enum to frontend radio value + if (response.oweRentArrears === 'YES') { + formData.rentArrears = 'yes'; + } else if (response.oweRentArrears === 'NO') { + formData.rentArrears = 'no'; + // Prepopulate the amount if it exists (convert pence to pounds) + if (response.rentArrearsAmount) { + const amountInPence = parseFloat(response.rentArrearsAmount as string); + const amountInPounds = amountInPence / 100; + formData['rentArrears.rentArrearsAmountCorrection'] = amountInPounds.toFixed(2); + } + } else if (response.oweRentArrears === 'NOT_SURE') { + formData.rentArrears = 'notSure'; + } + + return formData; + }, + extendGetContent: (req: Request) => { + const claimantName = getClaimantName(req); + + const caseData = req.res?.locals.validatedCase?.data; + const amountInPence = (caseData?.rentArrears_Total as string | number) || 0; + const amountInPounds = typeof amountInPence === 'string' ? parseFloat(amountInPence) / 100 : amountInPence / 100; + const rentArrearsAmount = currency(amountInPounds); + + const t = getTranslationFunction(req, 'rent-arrears-dispute', ['common']); + + // Dynamically inject validator with translation function (following postcode pattern) + const rentArrearsField = fieldsConfig[0]; + const noOption = rentArrearsField.options?.find(opt => opt.value === 'no'); + const amountField = noOption?.subFields?.rentArrearsAmountCorrection; + + if (amountField) { + amountField.validator = (value: unknown): boolean | string => { + if (typeof value !== 'string') {return true;} + + const trimmed = value.trim(); + if (!trimmed) {return true;} // Let required validation handle empty values + + // Remove commas to handle user input like 1,234.56 + const normalized = trimmed.replace(/,/g, ''); + const numericValue = parseFloat(normalized); + + // Range validation + if (!Number.isNaN(numericValue)) { + if (numericValue < 0) { + return t('errors.rentArrearsAmountCorrection.negativeAmount'); + } + if (numericValue > 1000000000) { + return t('errors.rentArrearsAmountCorrection.largeAmount'); + } + } + + // Format validation: 1-10 digits, decimal point, exactly 2 decimal places + const formatRegex = /^\d{1,10}\.\d{2}$/; + if (!formatRegex.test(normalized)) { + return t('errors.rentArrearsAmountCorrection.invalidFormat'); + } + + return true; + }; + } + + const insetIntroText = t('insetIntroText'); + const insetDetailsText = t('insetDetailsText', { claimantName }); + const insetConditionalYesText = t('insetConditionalYesText'); + const amountOwedHeading = t('amountOwedHeading', { claimantName }); + const rentArrearsAmountCorrection = t('rentArrearsAmountCorrection'); + return { + insetIntroText, + insetDetailsText, + insetConditionalYesText, + amountOwedHeading, + rentArrearsAmount, + rentArrearsAmountCorrection, + }; }, - fields: [], + fields: fieldsConfig, }); diff --git a/src/main/steps/respond-to-claim/rent-arrears-dispute/rentArrears.njk b/src/main/steps/respond-to-claim/rent-arrears-dispute/rentArrears.njk new file mode 100644 index 000000000..1359fbf7d --- /dev/null +++ b/src/main/steps/respond-to-claim/rent-arrears-dispute/rentArrears.njk @@ -0,0 +1,56 @@ +{% extends "stepsTemplate.njk" %} +{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} +{% from "govuk/components/button/macro.njk" import govukButton %} +{% from "govuk/components/radios/macro.njk" import govukRadios %} +{% from "govuk/components/character-count/macro.njk" import govukCharacterCount %} +{% from "govuk/components/inset-text/macro.njk" import govukInsetText %} + +{% block pageTitle %} + {{ title }} - HM Courts & Tribunals Service – GOV.UK +{% endblock %} + +{% block mainContent %} + {% if errorSummary %} + {{ govukErrorSummary(errorSummary) }} + {% endif %} + + {% if caption %} + {{ caption }} + {% endif %} + +

{{ title }}

+ +
+

{{ insetIntroText }}

+

{{ insetDetailsText }}

+

{{ insetConditionalYesText }}

+
+ +

{{ amountOwedHeading }}

+ +

{{ rentArrearsAmount }}

+ +
+ {% for field in fields %} + {% if field.component and field.componentType %} + {% if field.componentType == 'radios' %} + {{ govukRadios(field.component) }} + {% elif field.componentType == 'characterCount' %} + {{ govukCharacterCount(field.component) }} + {% endif %} + {% endif %} + {% endfor %} + +
+ {{ govukButton({ + text: saveAndContinue, + attributes: { type: 'submit', name: 'action', value: 'continue' } + }) }} + {{ govukButton({ + text: saveForLater, + classes: 'govuk-button--secondary', + attributes: { type: 'submit', name: 'action', value: 'saveForLater' } + }) }} +
+
+{% endblock %} diff --git a/src/main/steps/utils/getClaimantName.ts b/src/main/steps/utils/getClaimantName.ts new file mode 100644 index 000000000..56d8acf43 --- /dev/null +++ b/src/main/steps/utils/getClaimantName.ts @@ -0,0 +1,10 @@ +import type { Request } from 'express'; + +export const getClaimantName = (req: Request, fallback?: string): string | undefined => { + const claimantNameFromValidatedCase = req.res?.locals?.validatedCase?.data?.possessionClaimResponse + ?.claimantOrganisations?.[0]?.value as string | undefined; + + const claimantNameFromSession = req.session?.ccdCase?.data?.claimantName as string | undefined; + + return claimantNameFromValidatedCase || claimantNameFromSession || fallback; +}; diff --git a/src/main/steps/utils/hasOnlyRentArrearsGrounds.ts b/src/main/steps/utils/hasOnlyRentArrearsGrounds.ts new file mode 100644 index 000000000..49cb3c8c0 --- /dev/null +++ b/src/main/steps/utils/hasOnlyRentArrearsGrounds.ts @@ -0,0 +1,9 @@ +import type { Request } from 'express'; + +export const hasOnlyRentArrearsGrounds = (req: Request): boolean => { + const caseData = req.res?.locals.validatedCase?.data; + const claimDueToRentArrears = caseData?.claimDueToRentArrears; + const hasOtherAdditionalGrounds = caseData?.hasOtherAdditionalGrounds; + + return claimDueToRentArrears === 'Yes' && hasOtherAdditionalGrounds !== 'Yes'; +}; diff --git a/src/main/steps/utils/index.ts b/src/main/steps/utils/index.ts index 6be77bfc5..b003b6362 100644 --- a/src/main/steps/utils/index.ts +++ b/src/main/steps/utils/index.ts @@ -4,3 +4,5 @@ export { isNoticeDateProvided } from './isNoticeDateProvided'; export { isRentArrearsClaim } from './isRentArrearsClaim'; export { isNoticeServed } from './isNoticeServed'; export { getPreviousPageForArrears } from './journeyHelpers'; +export { getClaimantName } from './getClaimantName'; +export { hasOnlyRentArrearsGrounds } from './hasOnlyRentArrearsGrounds'; From 5f85467dc71c207be247dcad6d2aaa173eb24937 Mon Sep 17 00:00:00 2001 From: arun Date: Mon, 9 Mar 2026 07:10:49 +0000 Subject: [PATCH 02/23] HDPI-3451: Fix grammar in rent arrears translation - change 'received on the post' to 'received in the post' --- .../assets/locales/cy/respondToClaim/rentArrearsDispute.json | 2 +- .../assets/locales/en/respondToClaim/rentArrearsDispute.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/assets/locales/cy/respondToClaim/rentArrearsDispute.json b/src/main/assets/locales/cy/respondToClaim/rentArrearsDispute.json index 93954603c..3281ed3f7 100644 --- a/src/main/assets/locales/cy/respondToClaim/rentArrearsDispute.json +++ b/src/main/assets/locales/cy/respondToClaim/rentArrearsDispute.json @@ -4,7 +4,7 @@ "pageTitle": "cyRent arrears", "insetIntroText": "cyRent arrears are money you owe in rent payments.", "insetDetailsText": "cyWhen making their claim, {{claimantName}} had to provide a copy of the rent statement for your property, showing the total rent arrears you owe.", - "insetConditionalYesText": "cyThe rent statement will have been included in the pack you received on the post letting you know a claim had been made against you, and is also available to view from 'View the claim' on your case dashboard.", + "insetConditionalYesText": "cyThe rent statement will have been included in the pack you received in the post letting you know a claim had been made against you, and is also available to view from 'View the claim' on your case dashboard.", "amountOwedHeading": "cyAmount you owe in rent arrears given by {{claimantName}}:", "rentArrearsQuestion": "cyDo you owe this amount in rent arrears?", "rentArrearsOptions": { diff --git a/src/main/assets/locales/en/respondToClaim/rentArrearsDispute.json b/src/main/assets/locales/en/respondToClaim/rentArrearsDispute.json index ec6f8aace..1d05860a2 100644 --- a/src/main/assets/locales/en/respondToClaim/rentArrearsDispute.json +++ b/src/main/assets/locales/en/respondToClaim/rentArrearsDispute.json @@ -4,7 +4,7 @@ "pageTitle": "Rent arrears", "insetIntroText": "Rent arrears are money you owe in rent payments.", "insetDetailsText": "When making their claim, {{claimantName}} had to provide a copy of the rent statement for your property, showing the total rent arrears you owe.", - "insetConditionalYesText": "The rent statement will have been included in the pack you received on the post letting you know a claim had been made against you, and is also available to view from 'View the claim' on your case dashboard.", + "insetConditionalYesText": "The rent statement will have been included in the pack you received in the post letting you know a claim had been made against you, and is also available to view from 'View the claim' on your case dashboard.", "amountOwedHeading": "Amount you owe in rent arrears given by {{claimantName}}:", "rentArrearsQuestion": "Do you owe this amount in rent arrears?", "rentArrearsOptions": { From c9b3b2b883f4ce683c1778c9fdc7b013d2ecec38 Mon Sep 17 00:00:00 2001 From: arun Date: Mon, 9 Mar 2026 10:05:43 +0000 Subject: [PATCH 03/23] HDPI-3451: Fix rent-arrears-dispute subField data persistence using dotted notation pattern --- src/main/middleware/autoSaveDraftToCCD.ts | 5 +++-- src/main/modules/steps/formBuilder/helpers.ts | 19 ++++++++++++++++++- .../rent-arrears-dispute/index.ts | 9 +++++++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/main/middleware/autoSaveDraftToCCD.ts b/src/main/middleware/autoSaveDraftToCCD.ts index 82f2203c5..d2ab7773f 100644 --- a/src/main/middleware/autoSaveDraftToCCD.ts +++ b/src/main/middleware/autoSaveDraftToCCD.ts @@ -207,7 +207,7 @@ async function saveToCCD( } try { - logger.debug(`[${stepName}] Auto-saving to CCD draft`); + logger.debug(`[${stepName}] Starting auto-save with form data:`, JSON.stringify(formData, null, 2)); let relevantData: string | string[] | Record; if (ccdMapping.frontendField) { @@ -237,7 +237,7 @@ async function saveToCCD( // Skip save if transformed data is empty (nothing to update) if (Object.keys(transformedData).length === 0) { - logger.debug(`[${stepName}] Transformed data is empty, skipping CCD save`); + logger.warn(`[${stepName}] Transformed data is empty, skipping CCD save`); return; } @@ -246,6 +246,7 @@ async function saveToCCD( const ccdPayload = { ...nestedData, }; + logger.debug(`[${stepName}] Sending CCD payload:`, JSON.stringify(ccdPayload, null, 2)); await ccdCaseService.updateDraftRespondToClaim(accessToken, validatedCase.id, ccdPayload); diff --git a/src/main/modules/steps/formBuilder/helpers.ts b/src/main/modules/steps/formBuilder/helpers.ts index b7ac9526b..a157fb656 100644 --- a/src/main/modules/steps/formBuilder/helpers.ts +++ b/src/main/modules/steps/formBuilder/helpers.ts @@ -79,9 +79,10 @@ export function normalizeCheckboxFields(req: Request, fields: FormFieldConfig[]) /** * Processes all field data (checkbox normalization + date field consolidation) * This should run AFTER validation because date field validation expects individual day/month/year keys + * Now also handles subFields within radio/checkbox options */ export function processFieldData(req: Request, fields: FormFieldConfig[]): void { - for (const field of fields) { + const processField = (field: FormFieldConfig): void => { if (field.type === 'checkbox') { // Normalize checkbox values (in case they weren't normalized before validation) req.body[field.name] = normalizeCheckboxValue(req.body[field.name]); @@ -95,6 +96,22 @@ export function processFieldData(req: Request, fields: FormFieldConfig[]): void delete req.body[`${field.name}-month`]; delete req.body[`${field.name}-year`]; } + + // Process subFields if this is a radio or checkbox with options + if ((field.type === 'radio' || field.type === 'checkbox') && field.options) { + for (const option of field.options) { + if (option.subFields) { + for (const subField of Object.values(option.subFields)) { + // Recursively process subFields (they might be dates or checkboxes too) + processField(subField); + } + } + } + } + }; + + for (const field of fields) { + processField(field); } } diff --git a/src/main/steps/respond-to-claim/rent-arrears-dispute/index.ts b/src/main/steps/respond-to-claim/rent-arrears-dispute/index.ts index 6c01a2a75..e10bb52c2 100644 --- a/src/main/steps/respond-to-claim/rent-arrears-dispute/index.ts +++ b/src/main/steps/respond-to-claim/rent-arrears-dispute/index.ts @@ -111,6 +111,7 @@ export const step: StepDefinition = createFormStep({ } else if (response.oweRentArrears === 'NO') { formData.rentArrears = 'no'; // Prepopulate the amount if it exists (convert pence to pounds) + // Use dotted notation for subField, matching defendant-name-confirmation pattern if (response.rentArrearsAmount) { const amountInPence = parseFloat(response.rentArrearsAmount as string); const amountInPounds = amountInPence / 100; @@ -139,10 +140,14 @@ export const step: StepDefinition = createFormStep({ if (amountField) { amountField.validator = (value: unknown): boolean | string => { - if (typeof value !== 'string') {return true;} + if (typeof value !== 'string') { + return true; + } const trimmed = value.trim(); - if (!trimmed) {return true;} // Let required validation handle empty values + if (!trimmed) { + return true; + } // Let required validation handle empty values // Remove commas to handle user input like 1,234.56 const normalized = trimmed.replace(/,/g, ''); From ee882f4b1f237afd29a44d57c707ab05150bb588 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 11 Mar 2026 19:23:42 +0000 Subject: [PATCH 04/23] HDPI-3451: Add forward routing logic to rent-arrears-dispute step for AC06 compliance --- src/main/steps/respond-to-claim/flow.config.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/steps/respond-to-claim/flow.config.ts b/src/main/steps/respond-to-claim/flow.config.ts index 2b53296bc..282436341 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -3,6 +3,7 @@ import { type Request } from 'express'; import type { JourneyFlowConfig } from '../../interfaces/stepFlow.interface'; import { getPreviousPageForArrears, + hasOnlyRentArrearsGrounds, isDefendantNameKnown, isNoticeDateProvided, isNoticeServed, @@ -250,6 +251,16 @@ export const flowConfig: JourneyFlowConfig = { 'rent-arrears-dispute': { defaultNext: 'counter-claim', previousStep: (req: Request, _formData: Record) => getPreviousPageForArrears(req), + routes: [ + { + condition: async (req: Request): Promise => hasOnlyRentArrearsGrounds(req), + nextStep: 'counter-claim', + }, + { + condition: async (req: Request): Promise => !hasOnlyRentArrearsGrounds(req), + nextStep: 'non-rent-arrears-dispute', + }, + ], }, 'non-rent-arrears-dispute': { defaultNext: 'counter-claim', From f4274fc9f2ba41c7614aabb1d8c880d71cd7a226 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 11 Mar 2026 20:21:38 +0000 Subject: [PATCH 05/23] HDPI-3451: Refactor test mocks to use reusable createMockT factory for maintainability --- jest.config.js | 2 +- .../unit/app/controller/formHelpers.test.ts | 132 +++++++++--------- src/test/unit/helpers/mockTranslation.ts | 56 ++++++++ .../modules/steps/formBuilder/helpers.test.ts | 26 ++-- 4 files changed, 137 insertions(+), 79 deletions(-) create mode 100644 src/test/unit/helpers/mockTranslation.ts diff --git a/jest.config.js b/jest.config.js index e0e7c7fea..7dab1d2f9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,7 +20,7 @@ module.exports = { '^jose$': '/src/test/unit/modules/s2s/__mocks__/jose.ts', '^uuid$': '/src/test/unit/__mocks__/uuid.ts', }, - testPathIgnorePatterns: ['/__mocks__/'], + testPathIgnorePatterns: ['/__mocks__/', '/helpers/'], coverageProvider: 'v8', transformIgnorePatterns: ['node_modules/(?!(jose|@panva|oidc-token-hash)/)'], }; diff --git a/src/test/unit/app/controller/formHelpers.test.ts b/src/test/unit/app/controller/formHelpers.test.ts index 599646a6a..896e0962e 100644 --- a/src/test/unit/app/controller/formHelpers.test.ts +++ b/src/test/unit/app/controller/formHelpers.test.ts @@ -11,6 +11,7 @@ import { validateForm, } from '../../../../main/modules/steps'; import { getErrorMessage } from '../../../../main/modules/steps/formBuilder/errorUtils'; +import { createMockT } from '../../helpers/mockTranslation'; describe('formHelpers', () => { describe('getFormData', () => { @@ -1510,15 +1511,13 @@ describe('formHelpers', () => { }, ]; - const mockT = ((key: string) => { - if (key === 'errors.dateOfBirth.futureDate') { - return 'Your date of birth must be in the past'; - } - if (key === 'errors.date.futureDate') { - return 'Date must be in the past'; - } - return key; - }) as TFunction; + const mockT = createMockT({ + 'errors.dateOfBirth': { + futureDate: 'Your date of birth must be in the past', + }, + 'errors.dateOfBirth.futureDate': 'Your date of birth must be in the past', + 'errors.date.futureDate': 'Date must be in the past', + }); const stepSpecificErrors = getCustomErrorTranslations(mockT, fields); const translations = { ...stepSpecificErrors }; @@ -2084,15 +2083,14 @@ describe('formHelpers', () => { }); it('should return step-specific date error translations', () => { - const mockT = ((key: string) => { - if (key === 'errors.dateOfBirth.required') { - return 'Enter your date of birth'; - } - if (key === 'errors.dateOfBirth.missingOne') { - return 'Your date of birth must include a {{missingField}}'; - } - return key; - }) as TFunction; + const mockT = createMockT({ + 'errors.dateOfBirth': { + required: 'Enter your date of birth', + missingOne: 'Your date of birth must include a {{missingField}}', + }, + 'errors.dateOfBirth.required': 'Enter your date of birth', + 'errors.dateOfBirth.missingOne': 'Your date of birth must include a {{missingField}}', + }); const fields = [ { @@ -2111,12 +2109,12 @@ describe('formHelpers', () => { }); it('should return custom error translations', () => { - const mockT = ((key: string) => { - if (key === 'errors.dateOfBirth.custom') { - return 'Custom validation error'; - } - return key; - }) as TFunction; + const mockT = createMockT({ + 'errors.dateOfBirth': { + custom: 'Custom validation error', + }, + 'errors.dateOfBirth.custom': 'Custom validation error', + }); const fields = [ { @@ -2132,15 +2130,16 @@ describe('formHelpers', () => { }); it('should handle multiple fields with custom errors', () => { - const mockT = ((key: string) => { - if (key === 'errors.dateOfBirth.required') { - return 'Enter your date of birth'; - } - if (key === 'errors.startDate.required') { - return 'Enter a start date'; - } - return key; - }) as TFunction; + const mockT = createMockT({ + 'errors.dateOfBirth': { + required: 'Enter your date of birth', + }, + 'errors.startDate': { + required: 'Enter a start date', + }, + 'errors.dateOfBirth.required': 'Enter your date of birth', + 'errors.startDate.required': 'Enter a start date', + }); const fields = [ { @@ -2162,12 +2161,12 @@ describe('formHelpers', () => { }); it('should handle missingTwo translation key', () => { - const mockT = ((key: string) => { - if (key === 'errors.dateOfBirth.missingTwo') { - return 'Your date of birth must include two parts'; - } - return key; - }) as TFunction; + const mockT = createMockT({ + 'errors.dateOfBirth': { + missingTwo: 'Your date of birth must include two parts', + }, + 'errors.dateOfBirth.missingTwo': 'Your date of birth must include two parts', + }); const fields = [ { @@ -2184,12 +2183,12 @@ describe('formHelpers', () => { }); it('should handle futureDate translation key for date fields', () => { - const mockT = ((key: string) => { - if (key === 'errors.dateOfBirth.futureDate') { - return 'Your date of birth must be in the past'; - } - return key; - }) as TFunction; + const mockT = createMockT({ + 'errors.dateOfBirth': { + futureDate: 'Your date of birth must be in the past', + }, + 'errors.dateOfBirth.futureDate': 'Your date of birth must be in the past', + }); const fields = [ { @@ -2206,12 +2205,12 @@ describe('formHelpers', () => { }); it('should not add date key for non-date fields', () => { - const mockT = ((key: string) => { - if (key === 'errors.textField.required') { - return 'Text field is required'; - } - return key; - }) as TFunction; + const mockT = createMockT({ + 'errors.textField': { + required: 'Text field is required', + }, + 'errors.textField.required': 'Text field is required', + }); const fields = [ { @@ -2227,12 +2226,12 @@ describe('formHelpers', () => { }); it('should handle nested keys that do not map to date keys', () => { - const mockT = ((key: string) => { - if (key === 'errors.dateOfBirth.unknownKey') { - return 'Unknown error'; - } - return key; - }) as TFunction; + const mockT = createMockT({ + 'errors.dateOfBirth': { + unknownKey: 'Unknown error', + }, + 'errors.dateOfBirth.unknownKey': 'Unknown error', + }); const fields = [ { @@ -2242,17 +2241,20 @@ describe('formHelpers', () => { ]; const result = getCustomErrorTranslations(mockT, fields); - // unknownKey doesn't map to a date key, so it won't be in result - expect(result).toEqual({}); + // unknownKey doesn't map to a date-specific key (like dateRequired), but it does get included + // as a field-specific error translation (dateOfBirth.unknownKey) + expect(result).toEqual({ + 'dateOfBirth.unknownKey': 'Unknown error', + }); }); it('should handle non-date field custom errors', () => { - const mockT = ((key: string) => { - if (key === 'errors.textField.required') { - return 'Text field is required'; - } - return key; - }) as TFunction; + const mockT = createMockT({ + 'errors.textField': { + required: 'Text field is required', + }, + 'errors.textField.required': 'Text field is required', + }); const fields = [ { diff --git a/src/test/unit/helpers/mockTranslation.ts b/src/test/unit/helpers/mockTranslation.ts new file mode 100644 index 000000000..4ec51878c --- /dev/null +++ b/src/test/unit/helpers/mockTranslation.ts @@ -0,0 +1,56 @@ +import type { TFunction } from 'i18next'; + +/** + * Creates a mock TFunction for testing that properly supports returnObjects option. + * + * This factory eliminates the need to duplicate mock logic across test files. + * It handles both string translations and object translations (via returnObjects: true). + * + * @param translations - A map of translation keys to their values (strings or nested objects) + * @returns A mock TFunction that can be used in tests + * + * @example + * ```typescript + * const mockT = createMockT({ + * 'errors.dateOfBirth': { + * required: 'Enter your date of birth', + * maxLength: 'Must be 60 characters or less', + * }, + * 'errors.dateOfBirth.required': 'Enter your date of birth', // Also support direct key access + * }); + * + * // When called with returnObjects: true + * mockT('errors.dateOfBirth', { returnObjects: true }) + * // Returns: { required: 'Enter your date of birth', maxLength: 'Must be 60 characters or less' } + * + * // When called without returnObjects + * mockT('errors.dateOfBirth.required') + * // Returns: 'Enter your date of birth' + * ``` + */ +export function createMockT(translations: Record): TFunction { + return ((key: string, options?: Record) => { + const value = translations[key]; + + // Handle returnObjects: true - return object if it exists + if (options?.returnObjects && typeof value === 'object' && !Array.isArray(value)) { + return value; + } + + // Handle string translations + if (typeof value === 'string') { + // Support interpolation if provided + if (options && typeof options === 'object' && !options.returnObjects) { + let result = value; + for (const [placeholder, replacement] of Object.entries(options)) { + result = result.replace(new RegExp(`{{\\s*${placeholder}\\s*}}`, 'g'), String(replacement)); + } + return result; + } + return value; + } + + // Fallback: return the key itself (i18next behavior for missing translations) + return key; + }) as TFunction; +} diff --git a/src/test/unit/modules/steps/formBuilder/helpers.test.ts b/src/test/unit/modules/steps/formBuilder/helpers.test.ts index e8ab325ea..9eddd9573 100644 --- a/src/test/unit/modules/steps/formBuilder/helpers.test.ts +++ b/src/test/unit/modules/steps/formBuilder/helpers.test.ts @@ -11,6 +11,7 @@ import { setFormData, validateForm, } from '../../../../../main/modules/steps/formBuilder/helpers'; +import { createMockT } from '../../../helpers/mockTranslation'; describe('formBuilder helpers', () => { describe('getTranslation', () => { @@ -206,12 +207,12 @@ describe('formBuilder helpers', () => { }); describe('getTranslationErrors', () => { - const createMockT = (translations: Record = {}) => { + const createSimpleMockT = (translations: Record = {}) => { return jest.fn((key: string) => translations[key] || key) as unknown as TFunction; }; it('should return empty object when no fields provided', () => { - const mockT = createMockT(); + const mockT = createSimpleMockT(); const result = getTranslationErrors(mockT, []); expect(result).toEqual({}); }); @@ -391,15 +392,14 @@ describe('formBuilder helpers', () => { }); it('should include both common errors and field-specific errors', () => { - const mockT = jest.fn((key: string) => { - const translations: Record = { - 'errors.defaultRequired': 'This field is required', - 'errors.defaultInvalid': 'Invalid format', - 'errors.defaultMaxLength': 'Must be {max} characters or fewer', - 'errors.dateOfBirth.required': 'Enter your date of birth', - }; - return translations[key] || key; - }) as unknown as TFunction; + const mockT = createMockT({ + 'errors.defaultRequired': 'This field is required', + 'errors.defaultInvalid': 'Invalid format', + 'errors.defaultMaxLength': 'Must be {max} characters or fewer', + 'errors.dateOfBirth': { + required: 'Enter your date of birth', + }, + }); const fields = [ { @@ -421,12 +421,12 @@ describe('formBuilder helpers', () => { }); describe('getCustomErrorTranslations - field-specific maxLength', () => { - const createMockT = (translations: Record = {}) => { + const createSimpleMockT2 = (translations: Record = {}) => { return jest.fn((key: string) => translations[key] || key) as unknown as TFunction; }; it('should map errors.MaxLength to .maxLength', () => { - const mockT = createMockT({ + const mockT = createSimpleMockT2({ 'errors.firstNameMaxLength': 'First name must be 60 characters or less', }); From 457892e4012c86e41275d0b663940bdf2c3492da Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 11 Mar 2026 20:46:08 +0000 Subject: [PATCH 06/23] HDPI-3495: Remove getClaimantName utility and use inline pattern consistent with codebase --- .../respond-to-claim/rent-arrears-dispute/index.ts | 4 +--- src/main/steps/utils/getClaimantName.ts | 10 ---------- src/main/steps/utils/index.ts | 1 - 3 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 src/main/steps/utils/getClaimantName.ts diff --git a/src/main/steps/respond-to-claim/rent-arrears-dispute/index.ts b/src/main/steps/respond-to-claim/rent-arrears-dispute/index.ts index a382d398e..d05c809aa 100644 --- a/src/main/steps/respond-to-claim/rent-arrears-dispute/index.ts +++ b/src/main/steps/respond-to-claim/rent-arrears-dispute/index.ts @@ -4,7 +4,6 @@ import type { FormFieldConfig } from '../../../interfaces/formFieldConfig.interf import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; import { currency } from '../../../modules/nunjucks/filters/currency'; import { createFormStep, getTranslationFunction } from '../../../modules/steps'; -import { getClaimantName } from '../../utils'; import { flowConfig } from '../flow.config'; // Define fields separately so we can dynamically inject validator @@ -124,9 +123,8 @@ export const step: StepDefinition = createFormStep({ return formData; }, extendGetContent: (req: Request) => { - const claimantName = getClaimantName(req); - const caseData = req.res?.locals.validatedCase?.data; + const claimantName = caseData?.possessionClaimResponse?.claimantOrganisations?.[0]?.value as string | undefined; const amountInPence = (caseData?.rentArrears_Total as string | number) || 0; const amountInPounds = typeof amountInPence === 'string' ? parseFloat(amountInPence) / 100 : amountInPence / 100; const rentArrearsAmount = currency(amountInPounds); diff --git a/src/main/steps/utils/getClaimantName.ts b/src/main/steps/utils/getClaimantName.ts deleted file mode 100644 index 56d8acf43..000000000 --- a/src/main/steps/utils/getClaimantName.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Request } from 'express'; - -export const getClaimantName = (req: Request, fallback?: string): string | undefined => { - const claimantNameFromValidatedCase = req.res?.locals?.validatedCase?.data?.possessionClaimResponse - ?.claimantOrganisations?.[0]?.value as string | undefined; - - const claimantNameFromSession = req.session?.ccdCase?.data?.claimantName as string | undefined; - - return claimantNameFromValidatedCase || claimantNameFromSession || fallback; -}; diff --git a/src/main/steps/utils/index.ts b/src/main/steps/utils/index.ts index b003b6362..8b8293fbe 100644 --- a/src/main/steps/utils/index.ts +++ b/src/main/steps/utils/index.ts @@ -4,5 +4,4 @@ export { isNoticeDateProvided } from './isNoticeDateProvided'; export { isRentArrearsClaim } from './isRentArrearsClaim'; export { isNoticeServed } from './isNoticeServed'; export { getPreviousPageForArrears } from './journeyHelpers'; -export { getClaimantName } from './getClaimantName'; export { hasOnlyRentArrearsGrounds } from './hasOnlyRentArrearsGrounds'; From 5105286775ba4b06fe104b156ab77130d789b589 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 11 Mar 2026 20:58:40 +0000 Subject: [PATCH 07/23] HDPI-3495: Fix logger issues - remove PII logging and reduce production noise --- src/main/middleware/autoSaveDraftToCCD.ts | 6 +++--- src/main/services/ccdCaseService.ts | 6 +----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/middleware/autoSaveDraftToCCD.ts b/src/main/middleware/autoSaveDraftToCCD.ts index d2ab7773f..4429309e4 100644 --- a/src/main/middleware/autoSaveDraftToCCD.ts +++ b/src/main/middleware/autoSaveDraftToCCD.ts @@ -207,7 +207,7 @@ async function saveToCCD( } try { - logger.debug(`[${stepName}] Starting auto-save with form data:`, JSON.stringify(formData, null, 2)); + logger.debug(`[${stepName}] Starting auto-save with ${Object.keys(formData).length} fields`); let relevantData: string | string[] | Record; if (ccdMapping.frontendField) { @@ -237,7 +237,7 @@ async function saveToCCD( // Skip save if transformed data is empty (nothing to update) if (Object.keys(transformedData).length === 0) { - logger.warn(`[${stepName}] Transformed data is empty, skipping CCD save`); + logger.debug(`[${stepName}] Transformed data is empty, skipping CCD save`); return; } @@ -246,7 +246,7 @@ async function saveToCCD( const ccdPayload = { ...nestedData, }; - logger.debug(`[${stepName}] Sending CCD payload:`, JSON.stringify(ccdPayload, null, 2)); + logger.debug(`[${stepName}] Sending CCD payload for case ${validatedCase.id}`); await ccdCaseService.updateDraftRespondToClaim(accessToken, validatedCase.id, ccdPayload); diff --git a/src/main/services/ccdCaseService.ts b/src/main/services/ccdCaseService.ts index 7f6050db3..092998ee1 100644 --- a/src/main/services/ccdCaseService.ts +++ b/src/main/services/ccdCaseService.ts @@ -167,16 +167,12 @@ export const ccdCaseService = { const eventUrl = `${ccdUrl}/cases/${caseId}/event-triggers/${eventId}?ignore-warning=false`; try { - logger.info(`[ccdCaseService] Validating case access for caseId: ${caseId}, eventId: ${eventId}`); - logger.info(`[ccdCaseService] CCD URL: ${ccdUrl}`); - logger.info(`[ccdCaseService] Full URL: ${eventUrl}`); - logger.info(`[ccdCaseService] Access token: ${accessToken ? 'present' : 'MISSING'}`); + logger.debug(`[ccdCaseService] Validating case ${caseId} for event ${eventId}`); const response = await http.get<{ case_details?: { case_data?: Record } }>( eventUrl, getCaseHeaders(accessToken) ); - logger.info(`[ccdCaseService] Case access validated successfully for caseId: ${caseId}`); return { id: caseId, From eb8b6310961ab8bfe8ec5097c10d026ff834b275 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 11 Mar 2026 22:23:57 +0000 Subject: [PATCH 08/23] HDPI-3549: Implement non-rent arrears dispute screen with CCD persistence --- src/main/modules/index.ts | 14 ++- .../steps/respond-to-claim/flow.config.ts | 9 +- .../non-rent-arrears-dispute/index.ts | 96 ++++++++++++++++++- .../nonRentArrearsDispute.njk | 62 +++++++----- 4 files changed, 156 insertions(+), 25 deletions(-) diff --git a/src/main/modules/index.ts b/src/main/modules/index.ts index 58c8534a6..6dc55692e 100644 --- a/src/main/modules/index.ts +++ b/src/main/modules/index.ts @@ -1,12 +1,24 @@ +import config from 'config'; + +import type { OIDCConfig } from './oidc/config.interface'; +import { OIDCModule as OIDCProductionModule } from './oidc/oidc'; +import { OIDCLocalModule } from './oidc/oidc-local'; + export { http } from './http'; export { S2S } from './s2s'; export { Helmet } from './helmet'; export { Nunjucks } from './nunjucks'; -export { OIDCModule } from './oidc'; export { Session } from './session'; export { LaunchDarkly } from './launch-darkly'; export { I18n } from './i18n'; export { Logger } from './logger'; +// Dynamic OIDC module selection based on issuer URL +const oidcConfig: OIDCConfig = config.get('oidc'); +const isLocalDevelopment = oidcConfig.issuer.includes('localhost'); + +// Export the appropriate OIDC module based on environment +export const OIDCModule = isLocalDevelopment ? OIDCLocalModule : OIDCProductionModule; + // this is used to register the modules with the app in a certain order export const modules = ['I18n', 'Nunjucks', 'Helmet', 'Session', 'S2S', 'OIDCModule', 'LaunchDarkly']; diff --git a/src/main/steps/respond-to-claim/flow.config.ts b/src/main/steps/respond-to-claim/flow.config.ts index 282436341..2791bf145 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -264,7 +264,14 @@ export const flowConfig: JourneyFlowConfig = { }, 'non-rent-arrears-dispute': { defaultNext: 'counter-claim', - previousStep: (req: Request, _formData: Record) => getPreviousPageForArrears(req), + previousStep: (req: Request, formData: Record) => { + // Check if came from rent-arrears (mixed grounds) + if ('rent-arrears-dispute' in formData) { + return 'rent-arrears-dispute'; + } + // Otherwise use same logic as rent-arrears + return getPreviousPageForArrears(req); + }, }, 'counter-claim': { defaultNext: 'payment-interstitial', diff --git a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts index 794fd15d4..900cccdd3 100644 --- a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts +++ b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts @@ -1,3 +1,6 @@ +import type { Request } from 'express'; + +import type { FormFieldValue } from '../../../interfaces/formFieldConfig.interface'; import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; import { flowConfig } from '../flow.config'; @@ -13,5 +16,96 @@ export const step: StepDefinition = createFormStep({ pageTitle: 'pageTitle', caption: 'caption', }, - fields: [], + ccdMapping: { + backendPath: 'possessionClaimResponse.defendantResponses', + frontendFields: ['disputeOtherParts', 'disputeOtherParts.disputeDetails'], + valueMapper: (formData: FormFieldValue) => { + if (typeof formData === 'string' || Array.isArray(formData)) { + return {}; + } + + const disputeOtherParts = formData.disputeOtherParts as 'yes' | 'no' | undefined; + const disputeDetailsRaw = formData['disputeOtherParts.disputeDetails'] as string | undefined; + + const result: Record = {}; + + if (disputeOtherParts === 'yes') { + result.disputeClaim = 'YES'; + if (disputeDetailsRaw && typeof disputeDetailsRaw === 'string') { + const trimmed = disputeDetailsRaw.trim(); + if (trimmed) { + result.disputeDetails = trimmed; + } + } + } else if (disputeOtherParts === 'no') { + result.disputeClaim = 'NO'; + result.disputeDetails = null; + } + + return result; + }, + }, + getInitialFormData: (req: Request) => { + const caseData = req.res?.locals?.validatedCase?.data; + const response = caseData?.possessionClaimResponse?.defendantResponses; + + if (!response?.disputeClaim) { + return {}; + } + + const formData: Record = {}; + + if (response.disputeClaim === 'YES') { + formData.disputeOtherParts = 'yes'; + if (response.disputeDetails) { + formData['disputeOtherParts.disputeDetails'] = response.disputeDetails as string; + } + } else if (response.disputeClaim === 'NO') { + formData.disputeOtherParts = 'no'; + } + + return formData; + }, + extendGetContent: (req: Request) => { + const caseData = req.res?.locals.validatedCase?.data; + const claimantName = caseData?.possessionClaimResponse?.claimantOrganisations?.[0]?.value as string | undefined; + + return { + claimantName, + }; + }, + fields: [ + { + name: 'disputeOtherParts', + type: 'radio', + required: true, + translationKey: { + label: 'disputeOtherPartsQuestion', + }, + legendClasses: 'govuk-fieldset__legend--m', + options: [ + { + value: 'yes', + translationKey: 'options.yes', + subFields: { + disputeDetails: { + name: 'disputeDetails', + type: 'character-count', + required: true, + maxLength: 6500, + translationKey: { + label: 'disputeDetails.label', + hint: 'disputeDetails.hint', + }, + rows: 10, + }, + }, + }, + { + value: 'no', + translationKey: 'options.no', + }, + ], + }, + ], }); diff --git a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk index 469b68aea..0c5e13a22 100644 --- a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk +++ b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk @@ -1,30 +1,48 @@ -{% extends "stepsTemplate.njk" %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} +{% extends "layouts/main.njk" %} {% from "govuk/components/radios/macro.njk" import govukRadios %} +{% from "govuk/components/character-count/macro.njk" import govukCharacterCount %} {% from "govuk/components/button/macro.njk" import govukButton %} +{% from "macros/csrf.njk" import csrfProtection %} -{% block pageTitle %}{{ pageTitle }}{% endblock %} +{% block pageTitle %} + {{ t('pageTitle') }} +{% endblock %} -{% block mainContent %} - {% if caption %} - {{ caption }} - {% endif %} +{% block content %} +
+
+ {% include "includes/caption.njk" %} -

{{ pageTitle }}

+
+ {{ csrfProtection(csrfToken) }} - + {% if fields %} + {% for field in fields %} + {% if field.componentType === 'radios' %} + {{ govukRadios(field.component) }} + {% elif field.componentType === 'character-count' %} + {{ govukCharacterCount(field.component) }} + {% endif %} + {% endfor %} + {% endif %} -
- {{ govukButton({ - text: continue, - attributes: { type: 'submit', name: 'action', value: 'continue' } - }) }} - {{ govukButton({ - text: saveForLater, - classes: 'govuk-button--secondary', - attributes: { type: 'submit', name: 'action', value: 'saveForLater' } - }) }} -
-
+
+ {{ govukButton({ + text: t('buttons.saveAndContinue'), + type: 'submit', + name: 'saveAndContinue', + value: 'true' + }) }} -{% endblock %} \ No newline at end of file + {{ govukButton({ + text: t('buttons.saveForLater'), + type: 'submit', + name: 'saveForLater', + value: 'true', + classes: 'govuk-button--secondary' + }) }} +
+ +
+
+{% endblock %} From 5a8b29289bf7fb97f0101a5a05c2765a6f52adc1 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 11 Mar 2026 22:41:39 +0000 Subject: [PATCH 09/23] HDPI-3549: Remove rows property from character-count field config --- .../steps/respond-to-claim/non-rent-arrears-dispute/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts index 900cccdd3..f1550424f 100644 --- a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts +++ b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts @@ -97,7 +97,6 @@ export const step: StepDefinition = createFormStep({ label: 'disputeDetails.label', hint: 'disputeDetails.hint', }, - rows: 10, }, }, }, From ce3d607cd9264de4bc84ca9bc702d7a1b7031cbe Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 11 Mar 2026 22:46:52 +0000 Subject: [PATCH 10/23] HDPI-3549: Fix template button text to use variables instead of translation keys --- .../non-rent-arrears-dispute/nonRentArrearsDispute.njk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk index 0c5e13a22..eb6886d53 100644 --- a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk +++ b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk @@ -28,14 +28,14 @@
{{ govukButton({ - text: t('buttons.saveAndContinue'), + text: saveAndContinue, type: 'submit', name: 'saveAndContinue', value: 'true' }) }} {{ govukButton({ - text: t('buttons.saveForLater'), + text: saveForLater, type: 'submit', name: 'saveForLater', value: 'true', From e8820af5194a26ec63ec297e4e2f28808f945f2e Mon Sep 17 00:00:00 2001 From: arun Date: Thu, 12 Mar 2026 07:16:17 +0000 Subject: [PATCH 11/23] HDPI-3549: Implement non-rent-arrears-dispute page with GOV.UK Design System compliance and fix OIDC token refresh in local development --- .../respondToClaim/nonRentArrearsDispute.json | 24 +++++- .../respondToClaim/nonRentArrearsDispute.json | 25 +++++- src/main/middleware/oidc.ts | 11 +++ .../non-rent-arrears-dispute/index.ts | 24 +++++- .../nonRentArrearsDispute.njk | 83 ++++++++++--------- 5 files changed, 122 insertions(+), 45 deletions(-) diff --git a/src/main/assets/locales/cy/respondToClaim/nonRentArrearsDispute.json b/src/main/assets/locales/cy/respondToClaim/nonRentArrearsDispute.json index a0f4bd3ce..b6f552a85 100644 --- a/src/main/assets/locales/cy/respondToClaim/nonRentArrearsDispute.json +++ b/src/main/assets/locales/cy/respondToClaim/nonRentArrearsDispute.json @@ -1,4 +1,24 @@ { - "caption": "cyRespond to a property possession claim", - "pageTitle": "cyNon-Rent Arrears Dispute(Placeholder)" + "pageTitle": "cyDisputing other parts of the claim - Respond to a possession claim - HM Courts & Tribunals Service - GOV.UK", + "caption": "cyRespond to a possession claim", + "heading": "cyDisputing other parts of the claim", + "introParagraph": "cyYou should view the claim (opens in new tab) to see if there's any other parts of the claim that are incorrect or you disagree with.", + "includesHeading": "cyThis includes:", + "includesBullet1": "cy{{claimantName}}'s grounds for possession (their reasons for making the claim)", + "includesBullet2": "cyany documents they've uploaded to support their claim", + "includesBullet3": "cyany other information they've given as part of their claim", + "disputeOtherPartsQuestion": "cyDo you want to dispute any other parts of the claim?", + "disputeOtherPartsOptions": { + "yes": "cyYes", + "no": "cyNo" + }, + "disputeDetails": { + "label": "cyGive more details", + "hint": "cyExplain which parts of the claim you do not agree with." + }, + "errors": { + "disputeOtherParts": "cyYou must select whether you want to dispute any other parts of the claim", + "disputeDetails": "cyEnter the parts of the claim you do not agree with", + "disputeDetailsMaxLength": "cyYour explanation must be 6500 characters or fewer" + } } diff --git a/src/main/assets/locales/en/respondToClaim/nonRentArrearsDispute.json b/src/main/assets/locales/en/respondToClaim/nonRentArrearsDispute.json index d4cdf2372..ab8541350 100644 --- a/src/main/assets/locales/en/respondToClaim/nonRentArrearsDispute.json +++ b/src/main/assets/locales/en/respondToClaim/nonRentArrearsDispute.json @@ -1,4 +1,25 @@ { - "caption": "Respond to a property possession claim", - "pageTitle": "Non-Rent Arrears Dispute(Placeholder)" + "pageTitle": "Disputing other parts of the claim - Respond to a possession claim - HM Courts & Tribunals Service - GOV.UK", + "caption": "Respond to a possession claim", + "heading": "Disputing other parts of the claim", + "introParagraph": "You should view the claim (opens in new tab) to see if there's any other parts of the claim that are incorrect or you disagree with.", + "includesHeading": "This includes:", + "includesBullet1": "{{claimantName}}'s grounds for possession (their reasons for making the claim)", + "includesBullet2": "any documents they've uploaded to support their claim", + "includesBullet3": "any other information they've given as part of their claim", + "disputeOtherPartsQuestion": "Do you want to dispute any other parts of the claim?", + "disputeOtherPartsOptions": { + "yes": "Yes", + "no": "No" + }, + "disputeDetails": { + "label": "Give more details", + "hint": "Explain which parts of the claim you do not agree with." + }, + "errors": { + "disputeOtherParts": "You must select whether you want to dispute any other parts of the claim", + "disputeOtherParts.disputeDetails": "Enter the parts of the claim you do not agree with", + "disputeDetails": "Enter the parts of the claim you do not agree with", + "disputeDetailsMaxLength": "Your explanation must be 6500 characters or fewer" + } } diff --git a/src/main/middleware/oidc.ts b/src/main/middleware/oidc.ts index e613e6339..143ea19a4 100644 --- a/src/main/middleware/oidc.ts +++ b/src/main/middleware/oidc.ts @@ -3,9 +3,14 @@ import { NextFunction, Request, RequestHandler, Response } from 'express'; import * as jose from 'jose'; import { Logger } from '@modules/logger'; +import type { OIDCConfig } from '@modules/oidc/config.interface'; const logger = Logger.getLogger('oidcMiddleware'); +// Use the same environment detection pattern as modules/index.ts +const oidcConfig: OIDCConfig = config.get('oidc'); +const isLocalDevelopment = oidcConfig.issuer.includes('localhost'); + // In-memory map to prevent parallel refresh attempts for the same session const refreshInProgress = new Map>(); const REFRESH_TIMEOUT_MS = 10000; // 10 seconds timeout for refresh operations @@ -116,6 +121,12 @@ export const oidcMiddleware: RequestHandler = async ( // Start new refresh const refreshPromise = (async () => { try { + // Skip token refresh in local development (uses password grant flow, no refresh tokens) + if (isLocalDevelopment) { + logger.debug('Token refresh skipped in local development mode'); + return; + } + const oidcModule = req.app.locals.oidc; if (!oidcModule) { throw new Error('OIDC module not available in app.locals'); diff --git a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts index f1550424f..4f18ae132 100644 --- a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts +++ b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts @@ -4,7 +4,7 @@ import type { FormFieldValue } from '../../../interfaces/formFieldConfig.interfa import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; import { flowConfig } from '../flow.config'; -import { createFormStep } from '@modules/steps'; +import { createFormStep, getTranslationFunction } from '@modules/steps'; export const step: StepDefinition = createFormStep({ stepName: 'non-rent-arrears-dispute', @@ -15,6 +15,12 @@ export const step: StepDefinition = createFormStep({ translationKeys: { pageTitle: 'pageTitle', caption: 'caption', + heading: 'heading', + introParagraph: 'introParagraph', + includesHeading: 'includesHeading', + includesBullet1: 'includesBullet1', + includesBullet2: 'includesBullet2', + includesBullet3: 'includesBullet3', }, ccdMapping: { backendPath: 'possessionClaimResponse.defendantResponses', @@ -68,10 +74,21 @@ export const step: StepDefinition = createFormStep({ }, extendGetContent: (req: Request) => { const caseData = req.res?.locals.validatedCase?.data; + const caseReference = req.params.caseReference; const claimantName = caseData?.possessionClaimResponse?.claimantOrganisations?.[0]?.value as string | undefined; + // Use getTranslationFunction to properly set up namespace + const t = getTranslationFunction(req, 'non-rent-arrears-dispute', ['common']); + + // Interpolate claimantName into translation strings (like dispute-claim-interstitial) return { claimantName, + caseReference, + introParagraph: t('introParagraph', { caseReference }), + includesHeading: t('includesHeading'), + includesBullet1: t('includesBullet1', { claimantName }), + includesBullet2: t('includesBullet2'), + includesBullet3: t('includesBullet3'), }; }, fields: [ @@ -86,13 +103,14 @@ export const step: StepDefinition = createFormStep({ options: [ { value: 'yes', - translationKey: 'options.yes', + translationKey: 'disputeOtherPartsOptions.yes', subFields: { disputeDetails: { name: 'disputeDetails', type: 'character-count', required: true, maxLength: 6500, + errorMessage: 'errors.disputeDetails', translationKey: { label: 'disputeDetails.label', hint: 'disputeDetails.hint', @@ -102,7 +120,7 @@ export const step: StepDefinition = createFormStep({ }, { value: 'no', - translationKey: 'options.no', + translationKey: 'disputeOtherPartsOptions.no', }, ], }, diff --git a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk index eb6886d53..5515c7d6b 100644 --- a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk +++ b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk @@ -1,48 +1,55 @@ -{% extends "layouts/main.njk" %} +{% extends "stepsTemplate.njk" %} +{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} +{% from "govuk/components/button/macro.njk" import govukButton %} {% from "govuk/components/radios/macro.njk" import govukRadios %} {% from "govuk/components/character-count/macro.njk" import govukCharacterCount %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "macros/csrf.njk" import csrfProtection %} {% block pageTitle %} - {{ t('pageTitle') }} + {{ heading }} - HM Courts & Tribunals Service – GOV.UK {% endblock %} -{% block content %} -
-
- {% include "includes/caption.njk" %} - -
- {{ csrfProtection(csrfToken) }} - - {% if fields %} - {% for field in fields %} - {% if field.componentType === 'radios' %} - {{ govukRadios(field.component) }} - {% elif field.componentType === 'character-count' %} - {{ govukCharacterCount(field.component) }} - {% endif %} - {% endfor %} +{% block mainContent %} + {% if errorSummary %} + {{ govukErrorSummary(errorSummary) }} + {% endif %} + + {% if caption %} + {{ caption }} + {% endif %} + +

{{ heading }}

+ +

{{ introParagraph | safe }}

+ +

{{ includesHeading }}

+ +
    +
  • {{ includesBullet1 | safe }}
  • +
  • {{ includesBullet2 }}
  • +
  • {{ includesBullet3 }}
  • +
+ + + {% for field in fields %} + {% if field.component and field.componentType %} + {% if field.componentType == 'radios' %} + {{ govukRadios(field.component) }} + {% elif field.componentType == 'characterCount' %} + {{ govukCharacterCount(field.component) }} {% endif %} + {% endif %} + {% endfor %} -
- {{ govukButton({ - text: saveAndContinue, - type: 'submit', - name: 'saveAndContinue', - value: 'true' - }) }} - - {{ govukButton({ - text: saveForLater, - type: 'submit', - name: 'saveForLater', - value: 'true', - classes: 'govuk-button--secondary' - }) }} -
-
+
+ {{ govukButton({ + text: saveAndContinue, + attributes: { type: 'submit', name: 'action', value: 'continue' } + }) }} + {{ govukButton({ + text: saveForLater, + classes: 'govuk-button--secondary', + attributes: { type: 'submit', name: 'action', value: 'saveForLater' } + }) }}
-
+ {% endblock %} From 9179eaae62bdf8651f3d648e95e267e08dc39944 Mon Sep 17 00:00:00 2001 From: arun Date: Thu, 12 Mar 2026 07:18:43 +0000 Subject: [PATCH 12/23] HDPI-3549: Revert OIDC local development changes (unrelated to feature) --- src/main/middleware/oidc.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/main/middleware/oidc.ts b/src/main/middleware/oidc.ts index 143ea19a4..e613e6339 100644 --- a/src/main/middleware/oidc.ts +++ b/src/main/middleware/oidc.ts @@ -3,14 +3,9 @@ import { NextFunction, Request, RequestHandler, Response } from 'express'; import * as jose from 'jose'; import { Logger } from '@modules/logger'; -import type { OIDCConfig } from '@modules/oidc/config.interface'; const logger = Logger.getLogger('oidcMiddleware'); -// Use the same environment detection pattern as modules/index.ts -const oidcConfig: OIDCConfig = config.get('oidc'); -const isLocalDevelopment = oidcConfig.issuer.includes('localhost'); - // In-memory map to prevent parallel refresh attempts for the same session const refreshInProgress = new Map>(); const REFRESH_TIMEOUT_MS = 10000; // 10 seconds timeout for refresh operations @@ -121,12 +116,6 @@ export const oidcMiddleware: RequestHandler = async ( // Start new refresh const refreshPromise = (async () => { try { - // Skip token refresh in local development (uses password grant flow, no refresh tokens) - if (isLocalDevelopment) { - logger.debug('Token refresh skipped in local development mode'); - return; - } - const oidcModule = req.app.locals.oidc; if (!oidcModule) { throw new Error('OIDC module not available in app.locals'); From 9db7ed8e1560a9d194cef02eeecab629bb2326c5 Mon Sep 17 00:00:00 2001 From: arun Date: Thu, 12 Mar 2026 08:07:12 +0000 Subject: [PATCH 13/23] HDPI-3549: Revert index.ts to production version (remove local dev OIDC module selection) --- src/main/modules/index.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/main/modules/index.ts b/src/main/modules/index.ts index 6dc55692e..58c8534a6 100644 --- a/src/main/modules/index.ts +++ b/src/main/modules/index.ts @@ -1,24 +1,12 @@ -import config from 'config'; - -import type { OIDCConfig } from './oidc/config.interface'; -import { OIDCModule as OIDCProductionModule } from './oidc/oidc'; -import { OIDCLocalModule } from './oidc/oidc-local'; - export { http } from './http'; export { S2S } from './s2s'; export { Helmet } from './helmet'; export { Nunjucks } from './nunjucks'; +export { OIDCModule } from './oidc'; export { Session } from './session'; export { LaunchDarkly } from './launch-darkly'; export { I18n } from './i18n'; export { Logger } from './logger'; -// Dynamic OIDC module selection based on issuer URL -const oidcConfig: OIDCConfig = config.get('oidc'); -const isLocalDevelopment = oidcConfig.issuer.includes('localhost'); - -// Export the appropriate OIDC module based on environment -export const OIDCModule = isLocalDevelopment ? OIDCLocalModule : OIDCProductionModule; - // this is used to register the modules with the app in a certain order export const modules = ['I18n', 'Nunjucks', 'Helmet', 'Session', 'S2S', 'OIDCModule', 'LaunchDarkly']; From f31bdb6d0382119199ff58011db5e2e851f749e6 Mon Sep 17 00:00:00 2001 From: arun Date: Thu, 12 Mar 2026 21:30:00 +0000 Subject: [PATCH 14/23] HDPI-3495: Fix rent arrears routing and back button navigation for mixed grounds --- .../steps/respond-to-claim/flow.config.ts | 19 +- .../rent-arrears-dispute/rentArrears.njk | 4 +- .../steps/utils/hasOnlyRentArrearsGrounds.ts | 25 +- .../utils/hasOnlyRentArrearsGrounds.test.ts | 712 ++++++++++++++++++ 4 files changed, 745 insertions(+), 15 deletions(-) create mode 100644 src/test/unit/steps/utils/hasOnlyRentArrearsGrounds.test.ts diff --git a/src/main/steps/respond-to-claim/flow.config.ts b/src/main/steps/respond-to-claim/flow.config.ts index 282436341..2ad655f1e 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -253,27 +253,30 @@ export const flowConfig: JourneyFlowConfig = { previousStep: (req: Request, _formData: Record) => getPreviousPageForArrears(req), routes: [ { - condition: async (req: Request): Promise => hasOnlyRentArrearsGrounds(req), + condition: (req: Request): Promise => hasOnlyRentArrearsGrounds(req), nextStep: 'counter-claim', }, { - condition: async (req: Request): Promise => !hasOnlyRentArrearsGrounds(req), + condition: async (req: Request): Promise => !(await hasOnlyRentArrearsGrounds(req)), nextStep: 'non-rent-arrears-dispute', }, ], }, 'non-rent-arrears-dispute': { defaultNext: 'counter-claim', - previousStep: (req: Request, _formData: Record) => getPreviousPageForArrears(req), - }, - 'counter-claim': { - defaultNext: 'payment-interstitial', - previousStep: async (req: Request, _formData: Record) => { + previousStep: async (req: Request) => { const rentArrearsClaim = await isRentArrearsClaim(req); if (rentArrearsClaim) { return 'rent-arrears-dispute'; } - return 'non-rent-arrears-dispute'; + return getPreviousPageForArrears(req); + }, + }, + 'counter-claim': { + defaultNext: 'payment-interstitial', + previousStep: async (req: Request) => { + const onlyRentArrears = await hasOnlyRentArrearsGrounds(req); + return onlyRentArrears ? 'rent-arrears-dispute' : 'non-rent-arrears-dispute'; }, }, 'payment-interstitial': { diff --git a/src/main/steps/respond-to-claim/rent-arrears-dispute/rentArrears.njk b/src/main/steps/respond-to-claim/rent-arrears-dispute/rentArrears.njk index 1359fbf7d..a5d2c4bd3 100644 --- a/src/main/steps/respond-to-claim/rent-arrears-dispute/rentArrears.njk +++ b/src/main/steps/respond-to-claim/rent-arrears-dispute/rentArrears.njk @@ -6,7 +6,7 @@ {% from "govuk/components/inset-text/macro.njk" import govukInsetText %} {% block pageTitle %} - {{ title }} - HM Courts & Tribunals Service – GOV.UK + {{ pageTitle }} - HM Courts & Tribunals Service – GOV.UK {% endblock %} {% block mainContent %} @@ -18,7 +18,7 @@ {{ caption }} {% endif %} -

{{ title }}

+

{{ pageTitle }}

{{ insetIntroText }}

diff --git a/src/main/steps/utils/hasOnlyRentArrearsGrounds.ts b/src/main/steps/utils/hasOnlyRentArrearsGrounds.ts index 49cb3c8c0..2ebc03d9b 100644 --- a/src/main/steps/utils/hasOnlyRentArrearsGrounds.ts +++ b/src/main/steps/utils/hasOnlyRentArrearsGrounds.ts @@ -1,9 +1,24 @@ import type { Request } from 'express'; -export const hasOnlyRentArrearsGrounds = (req: Request): boolean => { - const caseData = req.res?.locals.validatedCase?.data; - const claimDueToRentArrears = caseData?.claimDueToRentArrears; - const hasOtherAdditionalGrounds = caseData?.hasOtherAdditionalGrounds; +export const hasOnlyRentArrearsGrounds = async (req: Request): Promise => { + const caseData = req.res?.locals?.validatedCase?.data; + const claimGroundSummaries = caseData?.claimGroundSummaries; - return claimDueToRentArrears === 'Yes' && hasOtherAdditionalGrounds !== 'Yes'; + if (!Array.isArray(claimGroundSummaries) || claimGroundSummaries.length === 0) { + return false; + } + + const hasRentArrearsGround = claimGroundSummaries.some( + ground => ground?.value?.isRentArrears?.toUpperCase() === 'YES' + ); + + if (!hasRentArrearsGround) { + return false; + } + + const allGroundsAreRentArrears = claimGroundSummaries.every( + ground => ground?.value?.isRentArrears?.toUpperCase() === 'YES' + ); + + return allGroundsAreRentArrears; }; diff --git a/src/test/unit/steps/utils/hasOnlyRentArrearsGrounds.test.ts b/src/test/unit/steps/utils/hasOnlyRentArrearsGrounds.test.ts new file mode 100644 index 000000000..9ace1d474 --- /dev/null +++ b/src/test/unit/steps/utils/hasOnlyRentArrearsGrounds.test.ts @@ -0,0 +1,712 @@ +import { Request } from 'express'; + +import { hasOnlyRentArrearsGrounds } from '../../../../main/steps/utils/hasOnlyRentArrearsGrounds'; + +describe('hasOnlyRentArrearsGrounds', () => { + describe('when all grounds are rent arrears (rent-only)', () => { + it('should return true when single ground has isRentArrears=Yes', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + isRentArrears: 'Yes', + code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', + }, + id: 'ground-1', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(true); + }); + + it('should return true when isRentArrears=YES (uppercase)', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + isRentArrears: 'YES', + code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', + }, + id: 'ground-1', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(true); + }); + + it('should return true when isRentArrears=yes (lowercase)', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + isRentArrears: 'yes', + }, + id: 'ground-1', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(true); + }); + + it('should return true when multiple grounds all have isRentArrears=Yes', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + isRentArrears: 'Yes', + code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', + }, + id: 'ground-1', + }, + { + value: { + isRentArrears: 'Yes', + code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', + }, + id: 'ground-2', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(true); + }); + }); + + describe('when some grounds are NOT rent arrears (mixed)', () => { + it('should return false when one ground is rent arrears and another is not', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + isRentArrears: 'Yes', + code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', + }, + id: 'ground-1', + }, + { + value: { + isRentArrears: 'No', + code: 'NUISANCE_OR_IMMORAL_USE', + }, + id: 'ground-2', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false with multiple non-rent grounds', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + isRentArrears: 'Yes', + code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', + }, + id: 'ground-1', + }, + { + value: { + isRentArrears: 'No', + code: 'NUISANCE_OR_IMMORAL_USE', + }, + id: 'ground-2', + }, + { + value: { + isRentArrears: 'No', + code: 'LANDLORDS_WORKS', + }, + id: 'ground-3', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false when first ground is non-rent and second is rent', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + isRentArrears: 'No', + code: 'ANTISOCIAL_BEHAVIOUR', + }, + id: 'ground-1', + }, + { + value: { + isRentArrears: 'Yes', + code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', + }, + id: 'ground-2', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + }); + + describe('when NO grounds are rent arrears (non-rent only)', () => { + it('should return false when no grounds have isRentArrears=Yes', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + isRentArrears: 'No', + code: 'PREMIUM_PAID_MUTUAL_EXCHANGE', + }, + id: 'ground-1', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false with multiple non-rent grounds only', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + isRentArrears: 'No', + code: 'NUISANCE_OR_IMMORAL_USE', + }, + id: 'ground-1', + }, + { + value: { + isRentArrears: 'No', + code: 'DOMESTIC_VIOLENCE', + }, + id: 'ground-2', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false when isRentArrears is undefined', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + code: 'ANTISOCIAL_BEHAVIOUR', + }, + id: 'ground-1', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false when isRentArrears is null', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + isRentArrears: null, + code: 'ANTISOCIAL_BEHAVIOUR', + }, + id: 'ground-1', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + }); + + describe('edge cases', () => { + it('should return false when claimGroundSummaries is empty array', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false when claimGroundSummaries is undefined', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: {}, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false when claimGroundSummaries is null', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: null, + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false when claimGroundSummaries is not an array', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: 'not-an-array', + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false when validatedCase is undefined', async () => { + const mockReq = { + res: { + locals: {}, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false when res.locals is undefined', async () => { + const mockReq = { + res: {}, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false when res is undefined', async () => { + const mockReq = {} as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false when ground.value is undefined', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + id: 'ground-1', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false when ground is null', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [null], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should handle mixed null/undefined grounds with valid rent arrears ground', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + null, + { + value: { + isRentArrears: 'Yes', + code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', + }, + id: 'ground-1', + }, + undefined, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + // Should return false because not ALL grounds are rent arrears (null/undefined are not rent arrears) + expect(result).toBe(false); + }); + }); + + describe('regression tests - verify against real CCD data', () => { + it('should return true for scenario 1: rent-only, no notice (real CCD data)', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY', + code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', + label: 'Rent arrears or breach of the tenancy (ground 1)', + isRentArrears: 'Yes', + }, + id: '46c3fe9c-c786-4737-b565-a90ff33aef08', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(true); + }); + + it('should return false for scenario 4: mixed grounds (real CCD data)', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY', + code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', + label: 'Rent arrears or breach of the tenancy (ground 1)', + reason: 'rent arrears reason', + isRentArrears: 'Yes', + }, + id: 'ground-1', + }, + { + value: { + category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY_ALT', + code: 'ANTISOCIAL_BEHAVIOUR_S158', + label: 'Antisocial behaviour (ground 14)', + reason: 'antisocial behaviour reason', + isRentArrears: 'No', + }, + id: 'ground-2', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + // Critical: Mixed grounds should return FALSE + expect(result).toBe(false); + }); + + it('should return false for scenario 5: mixed grounds with 3 grounds (real CCD data)', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY', + code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', + isRentArrears: 'Yes', + }, + id: 'ground-1', + }, + { + value: { + category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY_ALT', + code: 'ANTISOCIAL_BEHAVIOUR_S158', + isRentArrears: 'No', + }, + id: 'ground-2', + }, + { + value: { + category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY_ALT', + code: 'SPECIAL_NEEDS_ACCOMMODATION', + isRentArrears: 'No', + }, + id: 'ground-3', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + + it('should return false for scenario 7: non-rent-only (real CCD data)', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY_ALT', + code: 'ANTISOCIAL_BEHAVIOUR_S157', + label: 'Antisocial behaviour (ground 2)', + isRentArrears: 'No', + }, + id: 'ground-1', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + expect(result).toBe(false); + }); + }); + + describe('.every() and .some() method behavior verification', () => { + it('should use .some() to check if at least one ground is rent arrears', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + isRentArrears: 'No', + code: 'ANTISOCIAL_BEHAVIOUR', + }, + id: 'ground-1', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + // No rent arrears at all - should return false + expect(result).toBe(false); + }); + + it('should use .every() to verify ALL grounds are rent arrears', async () => { + const mockReq = { + res: { + locals: { + validatedCase: { + data: { + claimGroundSummaries: [ + { + value: { + isRentArrears: 'Yes', + code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', + }, + id: 'ground-1', + }, + { + value: { + isRentArrears: 'No', // This makes .every() return false + code: 'ANTISOCIAL_BEHAVIOUR', + }, + id: 'ground-2', + }, + ], + }, + }, + }, + }, + } as unknown as Request; + + const result = await hasOnlyRentArrearsGrounds(mockReq); + + // Has rent arrears BUT not ALL are rent arrears - should return false + expect(result).toBe(false); + }); + }); +}); From 700432d6b974a1f3ced83cd5809c202eba37ef06 Mon Sep 17 00:00:00 2001 From: arun Date: Fri, 13 Mar 2026 07:24:28 +0000 Subject: [PATCH 15/23] HDPI-3495: Rename helper functions for better readability --- .../steps/respond-to-claim/flow.config.ts | 26 ++++---- ...arsClaim.ts => hasAnyRentArrearsGround.ts} | 4 +- src/main/steps/utils/index.ts | 4 +- src/main/steps/utils/journeyHelpers.ts | 2 +- ...est.ts => hasAnyRentArrearsGround.test.ts} | 66 +++++++++---------- .../unit/steps/utils/journeyHelpers.test.ts | 46 ++++++------- 6 files changed, 74 insertions(+), 74 deletions(-) rename src/main/steps/utils/{isRentArrearsClaim.ts => hasAnyRentArrearsGround.ts} (78%) rename src/test/unit/steps/utils/{isRentArrearsClaim.test.ts => hasAnyRentArrearsGround.test.ts} (90%) diff --git a/src/main/steps/respond-to-claim/flow.config.ts b/src/main/steps/respond-to-claim/flow.config.ts index 2ad655f1e..5dd1ed6b7 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -2,12 +2,12 @@ import { type Request } from 'express'; import type { JourneyFlowConfig } from '../../interfaces/stepFlow.interface'; import { - getPreviousPageForArrears, + getPreviousNoticeStep, + hasAnyRentArrearsGround, hasOnlyRentArrearsGrounds, isDefendantNameKnown, isNoticeDateProvided, isNoticeServed, - isRentArrearsClaim, isWelshProperty, } from '../utils'; @@ -132,14 +132,14 @@ export const flowConfig: JourneyFlowConfig = { }, { condition: async (req: Request): Promise => { - const rentArrears = await isRentArrearsClaim(req); + const rentArrears = await hasAnyRentArrearsGround(req); return rentArrears; }, nextStep: 'rent-arrears-dispute', }, { condition: async (req: Request): Promise => { - const rentArrears = await isRentArrearsClaim(req); + const rentArrears = await hasAnyRentArrearsGround(req); return !rentArrears; }, nextStep: 'non-rent-arrears-dispute', @@ -190,7 +190,7 @@ export const flowConfig: JourneyFlowConfig = { if (confirmNoticeGiven !== 'no' && confirmNoticeGiven !== 'imNotSure') { return false; } - const rentArrears = await isRentArrearsClaim(req); + const rentArrears = await hasAnyRentArrearsGround(req); return rentArrears; }, nextStep: 'rent-arrears-dispute', @@ -201,7 +201,7 @@ export const flowConfig: JourneyFlowConfig = { if (confirmNoticeGiven !== 'no' && confirmNoticeGiven !== 'imNotSure') { return false; } - const rentArrears = await isRentArrearsClaim(req); + const rentArrears = await hasAnyRentArrearsGround(req); return !rentArrears; }, nextStep: 'non-rent-arrears-dispute', @@ -214,14 +214,14 @@ export const flowConfig: JourneyFlowConfig = { routes: [ { condition: async (req: Request): Promise => { - const rentArrears = await isRentArrearsClaim(req); + const rentArrears = await hasAnyRentArrearsGround(req); return rentArrears; }, nextStep: 'rent-arrears-dispute', }, { condition: async (req: Request): Promise => { - const rentArrears = await isRentArrearsClaim(req); + const rentArrears = await hasAnyRentArrearsGround(req); return !rentArrears; }, nextStep: 'non-rent-arrears-dispute', @@ -233,14 +233,14 @@ export const flowConfig: JourneyFlowConfig = { routes: [ { condition: async (req: Request): Promise => { - const rentArrears = await isRentArrearsClaim(req); + const rentArrears = await hasAnyRentArrearsGround(req); return rentArrears; }, nextStep: 'rent-arrears-dispute', }, { condition: async (req: Request): Promise => { - const rentArrears = await isRentArrearsClaim(req); + const rentArrears = await hasAnyRentArrearsGround(req); return !rentArrears; }, nextStep: 'non-rent-arrears-dispute', @@ -250,7 +250,7 @@ export const flowConfig: JourneyFlowConfig = { }, 'rent-arrears-dispute': { defaultNext: 'counter-claim', - previousStep: (req: Request, _formData: Record) => getPreviousPageForArrears(req), + previousStep: (req: Request, _formData: Record) => getPreviousNoticeStep(req), routes: [ { condition: (req: Request): Promise => hasOnlyRentArrearsGrounds(req), @@ -265,11 +265,11 @@ export const flowConfig: JourneyFlowConfig = { 'non-rent-arrears-dispute': { defaultNext: 'counter-claim', previousStep: async (req: Request) => { - const rentArrearsClaim = await isRentArrearsClaim(req); + const rentArrearsClaim = await hasAnyRentArrearsGround(req); if (rentArrearsClaim) { return 'rent-arrears-dispute'; } - return getPreviousPageForArrears(req); + return getPreviousNoticeStep(req); }, }, 'counter-claim': { diff --git a/src/main/steps/utils/isRentArrearsClaim.ts b/src/main/steps/utils/hasAnyRentArrearsGround.ts similarity index 78% rename from src/main/steps/utils/isRentArrearsClaim.ts rename to src/main/steps/utils/hasAnyRentArrearsGround.ts index 9a4520d22..0171cf93a 100644 --- a/src/main/steps/utils/isRentArrearsClaim.ts +++ b/src/main/steps/utils/hasAnyRentArrearsGround.ts @@ -1,12 +1,12 @@ import type { Request } from 'express'; /** - * Checks if the claim includes rent arrears from CCD case data. + * Checks if the claim includes any rent arrears ground from CCD case data. * * Checks claimGroundSummaries array from CCD case data. * Returns true if ANY claim ground has isRentArrears: "Yes" (case-insensitive), false otherwise. */ -export const isRentArrearsClaim = async (req: Request): Promise => { +export const hasAnyRentArrearsGround = async (req: Request): Promise => { const caseData = req.res?.locals?.validatedCase?.data; const claimGroundSummaries = caseData?.claimGroundSummaries; diff --git a/src/main/steps/utils/index.ts b/src/main/steps/utils/index.ts index 8b8293fbe..de0d7d4f9 100644 --- a/src/main/steps/utils/index.ts +++ b/src/main/steps/utils/index.ts @@ -1,7 +1,7 @@ export { isDefendantNameKnown } from './isDefendantNameKnown'; export { isWelshProperty } from './isWelshProperty'; export { isNoticeDateProvided } from './isNoticeDateProvided'; -export { isRentArrearsClaim } from './isRentArrearsClaim'; +export { hasAnyRentArrearsGround } from './hasAnyRentArrearsGround'; export { isNoticeServed } from './isNoticeServed'; -export { getPreviousPageForArrears } from './journeyHelpers'; +export { getPreviousNoticeStep } from './journeyHelpers'; export { hasOnlyRentArrearsGrounds } from './hasOnlyRentArrearsGrounds'; diff --git a/src/main/steps/utils/journeyHelpers.ts b/src/main/steps/utils/journeyHelpers.ts index 6f52616bd..e355300d3 100644 --- a/src/main/steps/utils/journeyHelpers.ts +++ b/src/main/steps/utils/journeyHelpers.ts @@ -3,7 +3,7 @@ import { Request } from 'express'; import { isNoticeDateProvided } from './isNoticeDateProvided'; import { isNoticeServed } from './isNoticeServed'; -export async function getPreviousPageForArrears(req: Request): Promise { +export async function getPreviousNoticeStep(req: Request): Promise { const noticeServed = await isNoticeServed(req); const noticeDateProvided = await isNoticeDateProvided(req); const confirmed = req.session?.formData?.['confirmation-of-notice-given']?.confirmNoticeGiven; diff --git a/src/test/unit/steps/utils/isRentArrearsClaim.test.ts b/src/test/unit/steps/utils/hasAnyRentArrearsGround.test.ts similarity index 90% rename from src/test/unit/steps/utils/isRentArrearsClaim.test.ts rename to src/test/unit/steps/utils/hasAnyRentArrearsGround.test.ts index 2ef38504b..4acad905d 100644 --- a/src/test/unit/steps/utils/isRentArrearsClaim.test.ts +++ b/src/test/unit/steps/utils/hasAnyRentArrearsGround.test.ts @@ -1,8 +1,8 @@ import { Request } from 'express'; -import { isRentArrearsClaim } from '../../../../main/steps/utils/isRentArrearsClaim'; +import { hasAnyRentArrearsGround } from '../../../../main/steps/utils/hasAnyRentArrearsGround'; -describe('isRentArrearsClaim', () => { +describe('hasAnyRentArrearsGround', () => { describe('when claim includes rent arrears', () => { it('should return true when single ground has isRentArrears=Yes', async () => { const mockReq = { @@ -25,7 +25,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(true); }); @@ -50,7 +50,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(true); }); @@ -75,7 +75,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(true); }); @@ -100,7 +100,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(true); }); @@ -140,7 +140,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(true); }); @@ -173,7 +173,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(true); }); @@ -201,7 +201,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -241,7 +241,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -266,7 +266,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -292,7 +292,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -318,7 +318,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -344,7 +344,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -362,7 +362,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -380,7 +380,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -398,7 +398,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -416,7 +416,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -434,7 +434,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -450,7 +450,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -462,7 +462,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -472,7 +472,7 @@ describe('isRentArrearsClaim', () => { res: {}, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -480,7 +480,7 @@ describe('isRentArrearsClaim', () => { it('should return false when res is undefined', async () => { const mockReq = {} as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -504,7 +504,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -522,7 +522,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -540,7 +540,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -568,7 +568,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(true); }); @@ -597,7 +597,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(true); }); @@ -630,7 +630,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(true); }); @@ -680,7 +680,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); // Should return true because ground-1 has isRentArrears: 'Yes' expect(result).toBe(true); @@ -711,7 +711,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); @@ -746,7 +746,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(true); }); @@ -786,7 +786,7 @@ describe('isRentArrearsClaim', () => { }, } as unknown as Request; - const result = await isRentArrearsClaim(mockReq); + const result = await hasAnyRentArrearsGround(mockReq); expect(result).toBe(false); }); diff --git a/src/test/unit/steps/utils/journeyHelpers.test.ts b/src/test/unit/steps/utils/journeyHelpers.test.ts index 8fd1c106e..b627fb5fb 100644 --- a/src/test/unit/steps/utils/journeyHelpers.test.ts +++ b/src/test/unit/steps/utils/journeyHelpers.test.ts @@ -1,11 +1,11 @@ import { isNoticeDateProvided } from '../../../../main/steps/utils/isNoticeDateProvided'; import { isNoticeServed } from '../../../../main/steps/utils/isNoticeServed'; -import { getPreviousPageForArrears } from '../../../../main/steps/utils/journeyHelpers'; +import { getPreviousNoticeStep } from '../../../../main/steps/utils/journeyHelpers'; jest.mock('../../../../main/steps/utils/isNoticeDateProvided'); jest.mock('../../../../main/steps/utils/isNoticeServed'); -describe('getPreviousPageForArrears', () => { +describe('getPreviousNoticeStep', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockReq: any; @@ -22,7 +22,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); mockReq.session.formData = { 'confirmation-of-notice-given': { confirmNoticeGiven: 'no' } }; - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-given'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-given'); }); it('returns confirmation-of-notice-given when user said imNotSure and notice served', async () => { @@ -30,7 +30,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); mockReq.session.formData = { 'confirmation-of-notice-given': { confirmNoticeGiven: 'imNotSure' } }; - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-given'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-given'); }); it('returns confirmation-of-notice-given when user said no, notice served, even if date was provided', async () => { @@ -38,7 +38,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(true); // Date provided, but user rejected mockReq.session.formData = { 'confirmation-of-notice-given': { confirmNoticeGiven: 'no' } }; - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-given'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-given'); }); it('returns confirmation-of-notice-given when user said imNotSure, notice served, even if date was provided', async () => { @@ -46,7 +46,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(true); // Date provided, but user rejected mockReq.session.formData = { 'confirmation-of-notice-given': { confirmNoticeGiven: 'imNotSure' } }; - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-given'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-given'); }); }); @@ -56,7 +56,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(true); mockReq.session.formData = { 'confirmation-of-notice-given': { confirmNoticeGiven: 'yes' } }; - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-date-when-provided'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-date-when-provided'); }); it('returns confirmation-of-notice-date-when-provided when date provided, notice served, no user answer', async () => { @@ -64,7 +64,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(true); mockReq.session.formData = {}; // No confirmation answer - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-date-when-provided'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-date-when-provided'); }); it('returns confirmation-of-notice-date-when-provided when date provided, notice served, confirmNoticeGiven undefined', async () => { @@ -72,7 +72,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(true); mockReq.session.formData = { 'confirmation-of-notice-given': {} }; - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-date-when-provided'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-date-when-provided'); }); }); @@ -82,7 +82,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); mockReq.session.formData = { 'confirmation-of-notice-given': { confirmNoticeGiven: 'yes' } }; - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-date-when-not-provided'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-date-when-not-provided'); }); it('returns confirmation-of-notice-date-when-not-provided when date not provided, notice served, no user answer', async () => { @@ -90,7 +90,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); mockReq.session.formData = {}; // No confirmation answer - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-date-when-not-provided'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-date-when-not-provided'); }); it('returns confirmation-of-notice-date-when-not-provided when date not provided, notice served, confirmNoticeGiven undefined', async () => { @@ -98,7 +98,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); mockReq.session.formData = { 'confirmation-of-notice-given': {} }; - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-date-when-not-provided'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-date-when-not-provided'); }); }); @@ -107,14 +107,14 @@ describe('getPreviousPageForArrears', () => { (isNoticeServed as jest.Mock).mockResolvedValue(false); (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); - expect(await getPreviousPageForArrears(mockReq)).toBe('tenancy-details'); + expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-details'); }); it('returns tenancy-details when notice not served, even if date provided', async () => { (isNoticeServed as jest.Mock).mockResolvedValue(false); (isNoticeDateProvided as jest.Mock).mockResolvedValue(true); // Date provided but notice not served - expect(await getPreviousPageForArrears(mockReq)).toBe('tenancy-details'); + expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-details'); }); it('returns tenancy-details when notice not served, user said no', async () => { @@ -122,7 +122,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); mockReq.session.formData = { 'confirmation-of-notice-given': { confirmNoticeGiven: 'no' } }; - expect(await getPreviousPageForArrears(mockReq)).toBe('tenancy-details'); + expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-details'); }); it('returns tenancy-details when notice not served, user said yes', async () => { @@ -130,7 +130,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); mockReq.session.formData = { 'confirmation-of-notice-given': { confirmNoticeGiven: 'yes' } }; - expect(await getPreviousPageForArrears(mockReq)).toBe('tenancy-details'); + expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-details'); }); }); @@ -140,7 +140,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(true); mockReq.session = {}; // No formData - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-date-when-provided'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-date-when-provided'); }); it('returns correct page when session is undefined', async () => { @@ -148,7 +148,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(true); mockReq = {}; // No session - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-date-when-provided'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-date-when-provided'); }); it('returns tenancy-details when session is undefined and notice not served', async () => { @@ -156,7 +156,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); mockReq = {}; // No session - expect(await getPreviousPageForArrears(mockReq)).toBe('tenancy-details'); + expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-details'); }); }); @@ -168,7 +168,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(true); mockReq.session.formData = { 'confirmation-of-notice-given': { confirmNoticeGiven: 'yes' } }; - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-date-when-provided'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-date-when-provided'); }); it('handles non-rent arrears scenario without notice date', async () => { @@ -178,7 +178,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); mockReq.session.formData = { 'confirmation-of-notice-given': { confirmNoticeGiven: 'yes' } }; - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-date-when-not-provided'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-date-when-not-provided'); }); it('handles user rejection scenario', async () => { @@ -188,7 +188,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeDateProvided as jest.Mock).mockResolvedValue(true); mockReq.session.formData = { 'confirmation-of-notice-given': { confirmNoticeGiven: 'no' } }; - expect(await getPreviousPageForArrears(mockReq)).toBe('confirmation-of-notice-given'); + expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-given'); }); it('handles no notice served scenario', async () => { @@ -196,7 +196,7 @@ describe('getPreviousPageForArrears', () => { (isNoticeServed as jest.Mock).mockResolvedValue(false); (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); - expect(await getPreviousPageForArrears(mockReq)).toBe('tenancy-details'); + expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-details'); }); }); }); From 7932d16320188e97d8f07ba6ce37b992156fa45a Mon Sep 17 00:00:00 2001 From: arun Date: Fri, 13 Mar 2026 07:38:01 +0000 Subject: [PATCH 16/23] HDPI-3495: Simplify hasOnlyRentArrearsGrounds tests by removing redundant cases --- .../utils/hasOnlyRentArrearsGrounds.test.ts | 331 ++---------------- 1 file changed, 21 insertions(+), 310 deletions(-) diff --git a/src/test/unit/steps/utils/hasOnlyRentArrearsGrounds.test.ts b/src/test/unit/steps/utils/hasOnlyRentArrearsGrounds.test.ts index 9ace1d474..d04ba7088 100644 --- a/src/test/unit/steps/utils/hasOnlyRentArrearsGrounds.test.ts +++ b/src/test/unit/steps/utils/hasOnlyRentArrearsGrounds.test.ts @@ -4,7 +4,7 @@ import { hasOnlyRentArrearsGrounds } from '../../../../main/steps/utils/hasOnlyR describe('hasOnlyRentArrearsGrounds', () => { describe('when all grounds are rent arrears (rent-only)', () => { - it('should return true when single ground has isRentArrears=Yes', async () => { + it('should return true when single ground has isRentArrears=Yes (case-insensitive)', async () => { const mockReq = { res: { locals: { @@ -30,57 +30,6 @@ describe('hasOnlyRentArrearsGrounds', () => { expect(result).toBe(true); }); - it('should return true when isRentArrears=YES (uppercase)', async () => { - const mockReq = { - res: { - locals: { - validatedCase: { - data: { - claimGroundSummaries: [ - { - value: { - isRentArrears: 'YES', - code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', - }, - id: 'ground-1', - }, - ], - }, - }, - }, - }, - } as unknown as Request; - - const result = await hasOnlyRentArrearsGrounds(mockReq); - - expect(result).toBe(true); - }); - - it('should return true when isRentArrears=yes (lowercase)', async () => { - const mockReq = { - res: { - locals: { - validatedCase: { - data: { - claimGroundSummaries: [ - { - value: { - isRentArrears: 'yes', - }, - id: 'ground-1', - }, - ], - }, - }, - }, - }, - } as unknown as Request; - - const result = await hasOnlyRentArrearsGrounds(mockReq); - - expect(result).toBe(true); - }); - it('should return true when multiple grounds all have isRentArrears=Yes', async () => { const mockReq = { res: { @@ -173,7 +122,7 @@ describe('hasOnlyRentArrearsGrounds', () => { { value: { isRentArrears: 'No', - code: 'LANDLORDS_WORKS', + code: 'CRIMINAL_CONVICTION', }, id: 'ground-3', }, @@ -188,39 +137,6 @@ describe('hasOnlyRentArrearsGrounds', () => { expect(result).toBe(false); }); - - it('should return false when first ground is non-rent and second is rent', async () => { - const mockReq = { - res: { - locals: { - validatedCase: { - data: { - claimGroundSummaries: [ - { - value: { - isRentArrears: 'No', - code: 'ANTISOCIAL_BEHAVIOUR', - }, - id: 'ground-1', - }, - { - value: { - isRentArrears: 'Yes', - code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', - }, - id: 'ground-2', - }, - ], - }, - }, - }, - }, - } as unknown as Request; - - const result = await hasOnlyRentArrearsGrounds(mockReq); - - expect(result).toBe(false); - }); }); describe('when NO grounds are rent arrears (non-rent only)', () => { @@ -234,7 +150,7 @@ describe('hasOnlyRentArrearsGrounds', () => { { value: { isRentArrears: 'No', - code: 'PREMIUM_PAID_MUTUAL_EXCHANGE', + code: 'NUISANCE_OR_IMMORAL_USE', }, id: 'ground-1', }, @@ -267,7 +183,7 @@ describe('hasOnlyRentArrearsGrounds', () => { { value: { isRentArrears: 'No', - code: 'DOMESTIC_VIOLENCE', + code: 'CRIMINAL_CONVICTION', }, id: 'ground-2', }, @@ -282,57 +198,6 @@ describe('hasOnlyRentArrearsGrounds', () => { expect(result).toBe(false); }); - - it('should return false when isRentArrears is undefined', async () => { - const mockReq = { - res: { - locals: { - validatedCase: { - data: { - claimGroundSummaries: [ - { - value: { - code: 'ANTISOCIAL_BEHAVIOUR', - }, - id: 'ground-1', - }, - ], - }, - }, - }, - }, - } as unknown as Request; - - const result = await hasOnlyRentArrearsGrounds(mockReq); - - expect(result).toBe(false); - }); - - it('should return false when isRentArrears is null', async () => { - const mockReq = { - res: { - locals: { - validatedCase: { - data: { - claimGroundSummaries: [ - { - value: { - isRentArrears: null, - code: 'ANTISOCIAL_BEHAVIOUR', - }, - id: 'ground-1', - }, - ], - }, - }, - }, - }, - } as unknown as Request; - - const result = await hasOnlyRentArrearsGrounds(mockReq); - - expect(result).toBe(false); - }); }); describe('edge cases', () => { @@ -370,24 +235,6 @@ describe('hasOnlyRentArrearsGrounds', () => { expect(result).toBe(false); }); - it('should return false when claimGroundSummaries is null', async () => { - const mockReq = { - res: { - locals: { - validatedCase: { - data: { - claimGroundSummaries: null, - }, - }, - }, - }, - } as unknown as Request; - - const result = await hasOnlyRentArrearsGrounds(mockReq); - - expect(result).toBe(false); - }); - it('should return false when claimGroundSummaries is not an array', async () => { const mockReq = { res: { @@ -428,14 +275,6 @@ describe('hasOnlyRentArrearsGrounds', () => { expect(result).toBe(false); }); - it('should return false when res is undefined', async () => { - const mockReq = {} as unknown as Request; - - const result = await hasOnlyRentArrearsGrounds(mockReq); - - expect(result).toBe(false); - }); - it('should return false when ground.value is undefined', async () => { const mockReq = { res: { @@ -458,24 +297,6 @@ describe('hasOnlyRentArrearsGrounds', () => { expect(result).toBe(false); }); - it('should return false when ground is null', async () => { - const mockReq = { - res: { - locals: { - validatedCase: { - data: { - claimGroundSummaries: [null], - }, - }, - }, - }, - } as unknown as Request; - - const result = await hasOnlyRentArrearsGrounds(mockReq); - - expect(result).toBe(false); - }); - it('should handle mixed null/undefined grounds with valid rent arrears ground', async () => { const mockReq = { res: { @@ -483,7 +304,6 @@ describe('hasOnlyRentArrearsGrounds', () => { validatedCase: { data: { claimGroundSummaries: [ - null, { value: { isRentArrears: 'Yes', @@ -491,7 +311,10 @@ describe('hasOnlyRentArrearsGrounds', () => { }, id: 'ground-1', }, - undefined, + { + value: null, + id: 'ground-2', + }, ], }, }, @@ -501,13 +324,12 @@ describe('hasOnlyRentArrearsGrounds', () => { const result = await hasOnlyRentArrearsGrounds(mockReq); - // Should return false because not ALL grounds are rent arrears (null/undefined are not rent arrears) expect(result).toBe(false); }); }); describe('regression tests - verify against real CCD data', () => { - it('should return true for scenario 1: rent-only, no notice (real CCD data)', async () => { + it('should return true for rent-only scenario (real CCD data)', async () => { const mockReq = { res: { locals: { @@ -516,12 +338,11 @@ describe('hasOnlyRentArrearsGrounds', () => { claimGroundSummaries: [ { value: { - category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY', - code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', - label: 'Rent arrears or breach of the tenancy (ground 1)', isRentArrears: 'Yes', + code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', + description: 'Rent arrears or breach of other obligation of the tenancy', }, - id: '46c3fe9c-c786-4737-b565-a90ff33aef08', + id: '4bd94bb9-e72f-473e-95ae-a6d2b3f8e8be', }, ], }, @@ -535,7 +356,7 @@ describe('hasOnlyRentArrearsGrounds', () => { expect(result).toBe(true); }); - it('should return false for scenario 4: mixed grounds (real CCD data)', async () => { + it('should return false for mixed grounds scenario (real CCD data)', async () => { const mockReq = { res: { locals: { @@ -544,68 +365,21 @@ describe('hasOnlyRentArrearsGrounds', () => { claimGroundSummaries: [ { value: { - category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY', - code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', - label: 'Rent arrears or breach of the tenancy (ground 1)', - reason: 'rent arrears reason', isRentArrears: 'Yes', - }, - id: 'ground-1', - }, - { - value: { - category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY_ALT', - code: 'ANTISOCIAL_BEHAVIOUR_S158', - label: 'Antisocial behaviour (ground 14)', - reason: 'antisocial behaviour reason', - isRentArrears: 'No', - }, - id: 'ground-2', - }, - ], - }, - }, - }, - }, - } as unknown as Request; - - const result = await hasOnlyRentArrearsGrounds(mockReq); - - // Critical: Mixed grounds should return FALSE - expect(result).toBe(false); - }); - - it('should return false for scenario 5: mixed grounds with 3 grounds (real CCD data)', async () => { - const mockReq = { - res: { - locals: { - validatedCase: { - data: { - claimGroundSummaries: [ - { - value: { - category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY', code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', - isRentArrears: 'Yes', + description: 'Rent arrears or breach of other obligation of the tenancy', }, id: 'ground-1', }, { value: { - category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY_ALT', - code: 'ANTISOCIAL_BEHAVIOUR_S158', isRentArrears: 'No', + code: 'NUISANCE_OR_IMMORAL_USE', + description: + 'Nuisance or annoyance to neighbours, illegal or immoral use of the property, or conviction for an arrestable offence in or near the property', }, id: 'ground-2', }, - { - value: { - category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY_ALT', - code: 'SPECIAL_NEEDS_ACCOMMODATION', - isRentArrears: 'No', - }, - id: 'ground-3', - }, ], }, }, @@ -618,7 +392,7 @@ describe('hasOnlyRentArrearsGrounds', () => { expect(result).toBe(false); }); - it('should return false for scenario 7: non-rent-only (real CCD data)', async () => { + it('should return false for non-rent-only scenario (real CCD data)', async () => { const mockReq = { res: { locals: { @@ -627,10 +401,10 @@ describe('hasOnlyRentArrearsGrounds', () => { claimGroundSummaries: [ { value: { - category: 'SECURE_OR_FLEXIBLE_DISCRETIONARY_ALT', - code: 'ANTISOCIAL_BEHAVIOUR_S157', - label: 'Antisocial behaviour (ground 2)', isRentArrears: 'No', + code: 'NUISANCE_OR_IMMORAL_USE', + description: + 'Nuisance or annoyance to neighbours, illegal or immoral use of the property, or conviction for an arrestable offence in or near the property', }, id: 'ground-1', }, @@ -646,67 +420,4 @@ describe('hasOnlyRentArrearsGrounds', () => { expect(result).toBe(false); }); }); - - describe('.every() and .some() method behavior verification', () => { - it('should use .some() to check if at least one ground is rent arrears', async () => { - const mockReq = { - res: { - locals: { - validatedCase: { - data: { - claimGroundSummaries: [ - { - value: { - isRentArrears: 'No', - code: 'ANTISOCIAL_BEHAVIOUR', - }, - id: 'ground-1', - }, - ], - }, - }, - }, - }, - } as unknown as Request; - - const result = await hasOnlyRentArrearsGrounds(mockReq); - - // No rent arrears at all - should return false - expect(result).toBe(false); - }); - - it('should use .every() to verify ALL grounds are rent arrears', async () => { - const mockReq = { - res: { - locals: { - validatedCase: { - data: { - claimGroundSummaries: [ - { - value: { - isRentArrears: 'Yes', - code: 'RENT_ARREARS_OR_BREACH_OF_TENANCY', - }, - id: 'ground-1', - }, - { - value: { - isRentArrears: 'No', // This makes .every() return false - code: 'ANTISOCIAL_BEHAVIOUR', - }, - id: 'ground-2', - }, - ], - }, - }, - }, - }, - } as unknown as Request; - - const result = await hasOnlyRentArrearsGrounds(mockReq); - - // Has rent arrears BUT not ALL are rent arrears - should return false - expect(result).toBe(false); - }); - }); }); From ad8d3d7d0b28b6781e00ec012a578782c2df20f8 Mon Sep 17 00:00:00 2001 From: arun Date: Fri, 13 Mar 2026 12:48:59 +0000 Subject: [PATCH 17/23] HDPI-3495: Update journeyHelpers tests to match master's new tenancy flow --- .../unit/steps/utils/journeyHelpers.test.ts | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/test/unit/steps/utils/journeyHelpers.test.ts b/src/test/unit/steps/utils/journeyHelpers.test.ts index 823af11c1..c11236490 100644 --- a/src/test/unit/steps/utils/journeyHelpers.test.ts +++ b/src/test/unit/steps/utils/journeyHelpers.test.ts @@ -1,5 +1,6 @@ import { isNoticeDateProvided } from '../../../../main/steps/utils/isNoticeDateProvided'; import { isNoticeServed } from '../../../../main/steps/utils/isNoticeServed'; +import { isTenancyStartDateKnown } from '../../../../main/steps/utils/isTenancyStartDateKnown'; import { getPreviousNoticeStep } from '../../../../main/steps/utils/journeyHelpers'; jest.mock('../../../../main/steps/utils/isNoticeDateProvided'); @@ -104,34 +105,38 @@ describe('getPreviousNoticeStep', () => { }); describe('Priority 4: No notice served (fallback)', () => { - it('returns tenancy-details when notice not served and tenancy start date is unknown', async () => { + it('returns tenancy-date-unknown when notice not served and tenancy start date is unknown', async () => { (isNoticeServed as jest.Mock).mockResolvedValue(false); (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); + (isTenancyStartDateKnown as jest.Mock).mockResolvedValue(false); - expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-details'); + expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-date-unknown'); }); - it('returns tenancy-details when notice not served, even if date provided', async () => { + it('returns tenancy-date-details when notice not served and tenancy start date is known', async () => { (isNoticeServed as jest.Mock).mockResolvedValue(false); (isNoticeDateProvided as jest.Mock).mockResolvedValue(true); // Date provided but notice not served + (isTenancyStartDateKnown as jest.Mock).mockResolvedValue(true); - expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-details'); + expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-date-details'); }); - it('returns tenancy-details when notice not served, user said no', async () => { + it('returns tenancy-date-unknown when notice not served, user said no', async () => { (isNoticeServed as jest.Mock).mockResolvedValue(false); (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); + (isTenancyStartDateKnown as jest.Mock).mockResolvedValue(false); mockReq.session.formData = { 'confirmation-of-notice-given': { confirmNoticeGiven: 'no' } }; - expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-details'); + expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-date-unknown'); }); - it('returns tenancy-details when notice not served, user said yes', async () => { + it('returns tenancy-date-unknown when notice not served, user said yes', async () => { (isNoticeServed as jest.Mock).mockResolvedValue(false); (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); + (isTenancyStartDateKnown as jest.Mock).mockResolvedValue(false); mockReq.session.formData = { 'confirmation-of-notice-given': { confirmNoticeGiven: 'yes' } }; - expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-details'); + expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-date-unknown'); }); }); @@ -152,12 +157,13 @@ describe('getPreviousNoticeStep', () => { expect(await getPreviousNoticeStep(mockReq)).toBe('confirmation-of-notice-date-when-provided'); }); - it('returns tenancy-details when session is undefined and notice not served', async () => { + it('returns tenancy-date-unknown when session is undefined and notice not served', async () => { (isNoticeServed as jest.Mock).mockResolvedValue(false); (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); + (isTenancyStartDateKnown as jest.Mock).mockResolvedValue(false); mockReq = {}; // No session - expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-details'); + expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-date-unknown'); }); }); @@ -189,8 +195,9 @@ describe('getPreviousNoticeStep', () => { it('handles no notice served scenario', async () => { (isNoticeServed as jest.Mock).mockResolvedValue(false); (isNoticeDateProvided as jest.Mock).mockResolvedValue(false); + (isTenancyStartDateKnown as jest.Mock).mockResolvedValue(false); - expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-details'); + expect(await getPreviousNoticeStep(mockReq)).toBe('tenancy-date-unknown'); }); }); }); From 063a79bc5dc1e4cfb1614789cb8d9441ee530cde Mon Sep 17 00:00:00 2001 From: arun Date: Fri, 13 Mar 2026 12:50:26 +0000 Subject: [PATCH 18/23] HDPI-3495: Replace isRentArrearsClaim with hasAnyRentArrearsGround in flow config --- src/main/steps/respond-to-claim/flow.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/steps/respond-to-claim/flow.config.ts b/src/main/steps/respond-to-claim/flow.config.ts index b959e4826..235eb3dc3 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -185,14 +185,14 @@ export const flowConfig: JourneyFlowConfig = { }, { condition: async (req: Request): Promise => { - const rentArrears = await isRentArrearsClaim(req); + const rentArrears = await hasAnyRentArrearsGround(req); return rentArrears; }, nextStep: 'rent-arrears-dispute', }, { condition: async (req: Request): Promise => { - const rentArrears = await isRentArrearsClaim(req); + const rentArrears = await hasAnyRentArrearsGround(req); return !rentArrears; }, nextStep: 'non-rent-arrears-dispute', From 2ffa360731b94eb822e2bdaf875f7c03343016d6 Mon Sep 17 00:00:00 2001 From: ashajayaprakash Date: Fri, 13 Mar 2026 15:04:59 +0000 Subject: [PATCH 19/23] Automation HDPI-2495 --- src/test/ui/data/page-data/index.ts | 2 +- .../data/page-data/rentArrears.page.data.ts | 22 +++++++++++++++++++ .../page-data/rentArrearsDispute.page.data.ts | 4 ---- 3 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 src/test/ui/data/page-data/rentArrears.page.data.ts delete mode 100644 src/test/ui/data/page-data/rentArrearsDispute.page.data.ts diff --git a/src/test/ui/data/page-data/index.ts b/src/test/ui/data/page-data/index.ts index a3fac81b3..673977e0e 100644 --- a/src/test/ui/data/page-data/index.ts +++ b/src/test/ui/data/page-data/index.ts @@ -14,7 +14,7 @@ export * from './contactByTextMessage.page.data'; export * from './noticeDetails.page.data'; export * from './noticeDateKnown.page.data'; export * from './noticeDateUnknown.page.data'; -export * from './rentArrearsDispute.page.data'; +export * from './rentArrears.page.data'; export * from './nonRentArrearsDispute.page.data'; export * from './counterClaim.page.data'; export * from './repaymentsMade.page.data'; diff --git a/src/test/ui/data/page-data/rentArrears.page.data.ts b/src/test/ui/data/page-data/rentArrears.page.data.ts new file mode 100644 index 000000000..4c8967f0d --- /dev/null +++ b/src/test/ui/data/page-data/rentArrears.page.data.ts @@ -0,0 +1,22 @@ +export const rentArrears = { + mainHeader: `Rent arrears`, + saveAndContinueButton: `Save and continue`, + rentAmountTextInput: `1000.00`, + incorrectFormatTextInput: `1000`, + negativeTextInput: `-100.00`, + billionTextInput: `1000001.00`, + doYouOweThisQuestion: `Do you owe this amount in rent arrears?`, + howMuchDoYouBelieveHiddenTextLabel: `How much do you believe you owe in rent arrears?`, + yesRadioOption: `Yes`, + noRadioOption: `No`, + imNotSureRadioOption: `I’m not sure`, + saveForLaterButton: `Save for later`, + backLink: `Back`, + thereIsAProblemErrorMessageHeader: `There is a problem`, + errorValidationHeader: `There is a problem`, + enterHowMuchYouBelieveErrorMessage: `Enter the amount you believe you owe in rent arrears`, + theAmountYouBelieveErrorMessage: `The amount you believe you owe in rent arrears must be £0.00 or above`, + lessThanBillionErrorMessage: `The amount you believe you owe in rent arrears must be less than £1 billion`, + doYouOweThisAmountErrorMessage: `Do you owe this amount in rent arrears?`, + enterAmountInCorrectFormat: `Enter an amount in the correct format, for example 148.00 or 148.50`, +}; diff --git a/src/test/ui/data/page-data/rentArrearsDispute.page.data.ts b/src/test/ui/data/page-data/rentArrearsDispute.page.data.ts deleted file mode 100644 index 1ce132dcf..000000000 --- a/src/test/ui/data/page-data/rentArrearsDispute.page.data.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const rentArrearsDispute = { - mainHeader: `Rent Arrears Dispute(Placeholder)`, - continueButton: `Continue`, -}; From a7eb4b116691f87acff9910adc4e7aa0b77b1666 Mon Sep 17 00:00:00 2001 From: arun Date: Fri, 13 Mar 2026 15:46:34 +0000 Subject: [PATCH 20/23] HDPI-3495: Make jest testPathIgnorePatterns more specific to avoid hiding future test files --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 7dab1d2f9..9ffd41840 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,7 +20,7 @@ module.exports = { '^jose$': '/src/test/unit/modules/s2s/__mocks__/jose.ts', '^uuid$': '/src/test/unit/__mocks__/uuid.ts', }, - testPathIgnorePatterns: ['/__mocks__/', '/helpers/'], + testPathIgnorePatterns: ['/__mocks__/', '/src/test/unit/helpers/'], coverageProvider: 'v8', transformIgnorePatterns: ['node_modules/(?!(jose|@panva|oidc-token-hash)/)'], }; From e8592b989d6bc57f600f1b449f20519668c304c4 Mon Sep 17 00:00:00 2001 From: arun Date: Fri, 13 Mar 2026 16:03:47 +0000 Subject: [PATCH 21/23] HDPI-3495: Use specific filename pattern instead of directory to avoid hiding future test files --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 9ffd41840..1051baa34 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,7 +20,7 @@ module.exports = { '^jose$': '/src/test/unit/modules/s2s/__mocks__/jose.ts', '^uuid$': '/src/test/unit/__mocks__/uuid.ts', }, - testPathIgnorePatterns: ['/__mocks__/', '/src/test/unit/helpers/'], + testPathIgnorePatterns: ['/__mocks__/', '/mockTranslation.ts$'], coverageProvider: 'v8', transformIgnorePatterns: ['node_modules/(?!(jose|@panva|oidc-token-hash)/)'], }; From e81059109a5239b8d45fd86a7527bb865174c3b2 Mon Sep 17 00:00:00 2001 From: arun Date: Fri, 13 Mar 2026 20:54:50 +0000 Subject: [PATCH 22/23] HDPI-3495: Revert non-rent-arrears-dispute to placeholder version --- .../respondToClaim/nonRentArrearsDispute.json | 24 +--- .../respondToClaim/nonRentArrearsDispute.json | 25 +--- .../non-rent-arrears-dispute/index.ts | 115 +----------------- .../nonRentArrearsDispute.njk | 35 +----- 4 files changed, 11 insertions(+), 188 deletions(-) diff --git a/src/main/assets/locales/cy/respondToClaim/nonRentArrearsDispute.json b/src/main/assets/locales/cy/respondToClaim/nonRentArrearsDispute.json index b6f552a85..a0f4bd3ce 100644 --- a/src/main/assets/locales/cy/respondToClaim/nonRentArrearsDispute.json +++ b/src/main/assets/locales/cy/respondToClaim/nonRentArrearsDispute.json @@ -1,24 +1,4 @@ { - "pageTitle": "cyDisputing other parts of the claim - Respond to a possession claim - HM Courts & Tribunals Service - GOV.UK", - "caption": "cyRespond to a possession claim", - "heading": "cyDisputing other parts of the claim", - "introParagraph": "cyYou should view the claim (opens in new tab) to see if there's any other parts of the claim that are incorrect or you disagree with.", - "includesHeading": "cyThis includes:", - "includesBullet1": "cy{{claimantName}}'s grounds for possession (their reasons for making the claim)", - "includesBullet2": "cyany documents they've uploaded to support their claim", - "includesBullet3": "cyany other information they've given as part of their claim", - "disputeOtherPartsQuestion": "cyDo you want to dispute any other parts of the claim?", - "disputeOtherPartsOptions": { - "yes": "cyYes", - "no": "cyNo" - }, - "disputeDetails": { - "label": "cyGive more details", - "hint": "cyExplain which parts of the claim you do not agree with." - }, - "errors": { - "disputeOtherParts": "cyYou must select whether you want to dispute any other parts of the claim", - "disputeDetails": "cyEnter the parts of the claim you do not agree with", - "disputeDetailsMaxLength": "cyYour explanation must be 6500 characters or fewer" - } + "caption": "cyRespond to a property possession claim", + "pageTitle": "cyNon-Rent Arrears Dispute(Placeholder)" } diff --git a/src/main/assets/locales/en/respondToClaim/nonRentArrearsDispute.json b/src/main/assets/locales/en/respondToClaim/nonRentArrearsDispute.json index ab8541350..d4cdf2372 100644 --- a/src/main/assets/locales/en/respondToClaim/nonRentArrearsDispute.json +++ b/src/main/assets/locales/en/respondToClaim/nonRentArrearsDispute.json @@ -1,25 +1,4 @@ { - "pageTitle": "Disputing other parts of the claim - Respond to a possession claim - HM Courts & Tribunals Service - GOV.UK", - "caption": "Respond to a possession claim", - "heading": "Disputing other parts of the claim", - "introParagraph": "You should view the claim (opens in new tab) to see if there's any other parts of the claim that are incorrect or you disagree with.", - "includesHeading": "This includes:", - "includesBullet1": "{{claimantName}}'s grounds for possession (their reasons for making the claim)", - "includesBullet2": "any documents they've uploaded to support their claim", - "includesBullet3": "any other information they've given as part of their claim", - "disputeOtherPartsQuestion": "Do you want to dispute any other parts of the claim?", - "disputeOtherPartsOptions": { - "yes": "Yes", - "no": "No" - }, - "disputeDetails": { - "label": "Give more details", - "hint": "Explain which parts of the claim you do not agree with." - }, - "errors": { - "disputeOtherParts": "You must select whether you want to dispute any other parts of the claim", - "disputeOtherParts.disputeDetails": "Enter the parts of the claim you do not agree with", - "disputeDetails": "Enter the parts of the claim you do not agree with", - "disputeDetailsMaxLength": "Your explanation must be 6500 characters or fewer" - } + "caption": "Respond to a property possession claim", + "pageTitle": "Non-Rent Arrears Dispute(Placeholder)" } diff --git a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts index 4f18ae132..794fd15d4 100644 --- a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts +++ b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/index.ts @@ -1,10 +1,7 @@ -import type { Request } from 'express'; - -import type { FormFieldValue } from '../../../interfaces/formFieldConfig.interface'; import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; import { flowConfig } from '../flow.config'; -import { createFormStep, getTranslationFunction } from '@modules/steps'; +import { createFormStep } from '@modules/steps'; export const step: StepDefinition = createFormStep({ stepName: 'non-rent-arrears-dispute', @@ -15,114 +12,6 @@ export const step: StepDefinition = createFormStep({ translationKeys: { pageTitle: 'pageTitle', caption: 'caption', - heading: 'heading', - introParagraph: 'introParagraph', - includesHeading: 'includesHeading', - includesBullet1: 'includesBullet1', - includesBullet2: 'includesBullet2', - includesBullet3: 'includesBullet3', - }, - ccdMapping: { - backendPath: 'possessionClaimResponse.defendantResponses', - frontendFields: ['disputeOtherParts', 'disputeOtherParts.disputeDetails'], - valueMapper: (formData: FormFieldValue) => { - if (typeof formData === 'string' || Array.isArray(formData)) { - return {}; - } - - const disputeOtherParts = formData.disputeOtherParts as 'yes' | 'no' | undefined; - const disputeDetailsRaw = formData['disputeOtherParts.disputeDetails'] as string | undefined; - - const result: Record = {}; - - if (disputeOtherParts === 'yes') { - result.disputeClaim = 'YES'; - if (disputeDetailsRaw && typeof disputeDetailsRaw === 'string') { - const trimmed = disputeDetailsRaw.trim(); - if (trimmed) { - result.disputeDetails = trimmed; - } - } - } else if (disputeOtherParts === 'no') { - result.disputeClaim = 'NO'; - result.disputeDetails = null; - } - - return result; - }, - }, - getInitialFormData: (req: Request) => { - const caseData = req.res?.locals?.validatedCase?.data; - const response = caseData?.possessionClaimResponse?.defendantResponses; - - if (!response?.disputeClaim) { - return {}; - } - - const formData: Record = {}; - - if (response.disputeClaim === 'YES') { - formData.disputeOtherParts = 'yes'; - if (response.disputeDetails) { - formData['disputeOtherParts.disputeDetails'] = response.disputeDetails as string; - } - } else if (response.disputeClaim === 'NO') { - formData.disputeOtherParts = 'no'; - } - - return formData; - }, - extendGetContent: (req: Request) => { - const caseData = req.res?.locals.validatedCase?.data; - const caseReference = req.params.caseReference; - const claimantName = caseData?.possessionClaimResponse?.claimantOrganisations?.[0]?.value as string | undefined; - - // Use getTranslationFunction to properly set up namespace - const t = getTranslationFunction(req, 'non-rent-arrears-dispute', ['common']); - - // Interpolate claimantName into translation strings (like dispute-claim-interstitial) - return { - claimantName, - caseReference, - introParagraph: t('introParagraph', { caseReference }), - includesHeading: t('includesHeading'), - includesBullet1: t('includesBullet1', { claimantName }), - includesBullet2: t('includesBullet2'), - includesBullet3: t('includesBullet3'), - }; }, - fields: [ - { - name: 'disputeOtherParts', - type: 'radio', - required: true, - translationKey: { - label: 'disputeOtherPartsQuestion', - }, - legendClasses: 'govuk-fieldset__legend--m', - options: [ - { - value: 'yes', - translationKey: 'disputeOtherPartsOptions.yes', - subFields: { - disputeDetails: { - name: 'disputeDetails', - type: 'character-count', - required: true, - maxLength: 6500, - errorMessage: 'errors.disputeDetails', - translationKey: { - label: 'disputeDetails.label', - hint: 'disputeDetails.hint', - }, - }, - }, - }, - { - value: 'no', - translationKey: 'disputeOtherPartsOptions.no', - }, - ], - }, - ], + fields: [], }); diff --git a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk index 4b1b1c3f7..763e11e87 100644 --- a/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk +++ b/src/main/steps/respond-to-claim/non-rent-arrears-dispute/nonRentArrearsDispute.njk @@ -1,48 +1,22 @@ {% extends "stepsTemplate.njk" %} {% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/button/macro.njk" import govukButton %} {% from "govuk/components/radios/macro.njk" import govukRadios %} -{% from "govuk/components/character-count/macro.njk" import govukCharacterCount %} +{% from "govuk/components/button/macro.njk" import govukButton %} -{% block pageTitle %} - {{ heading }} - HM Courts & Tribunals Service – GOV.UK -{% endblock %} +{% block pageTitle %}{{ pageTitle }}{% endblock %} {% block mainContent %} - {% if errorSummary %} - {{ govukErrorSummary(errorSummary) }} - {% endif %} - {% if caption %} {{ caption }} {% endif %} -

{{ heading }}

- -

{{ introParagraph | safe }}

- -

{{ includesHeading }}

- -
    -
  • {{ includesBullet1 | safe }}
  • -
  • {{ includesBullet2 }}
  • -
  • {{ includesBullet3 }}
  • -
+

{{ pageTitle }}

- {% for field in fields %} - {% if field.component and field.componentType %} - {% if field.componentType == 'radios' %} - {{ govukRadios(field.component) }} - {% elif field.componentType == 'characterCount' %} - {{ govukCharacterCount(field.component) }} - {% endif %} - {% endif %} - {% endfor %}
{{ govukButton({ - text: saveAndContinue, + text: continue, attributes: { type: 'submit', name: 'action', value: 'continue' } }) }} {{ govukButton({ @@ -52,4 +26,5 @@ }) }}
+ {% endblock %} From 86bd3d228650589282fd4d6a054b47f04ddd1b6d Mon Sep 17 00:00:00 2001 From: ashajayaprakash Date: Mon, 16 Mar 2026 09:12:33 +0000 Subject: [PATCH 23/23] Automation HDPI-2495 --- .../ui/data/api-data/submitCase.api.data.ts | 217 ++++++++++++++++++ src/test/ui/e2eTest/respondToAClaim.spec.ts | 28 ++- .../ui/functional/respondToAClaim.spec.ts | 7 +- .../custom-actions/respondToClaim.action.ts | 35 +++ src/test/ui/utils/registry/action.registry.ts | 1 + 5 files changed, 274 insertions(+), 14 deletions(-) diff --git a/src/test/ui/data/api-data/submitCase.api.data.ts b/src/test/ui/data/api-data/submitCase.api.data.ts index f64f05eb3..2e43ddea6 100644 --- a/src/test/ui/data/api-data/submitCase.api.data.ts +++ b/src/test/ui/data/api-data/submitCase.api.data.ts @@ -196,5 +196,222 @@ export const submitCaseApiData = { }, }; }, + get submitCasePayloadDemotedOld() { + return { + legislativeCountry: 'England', + claimantType: { + value: { + code: 'PROVIDER_OF_SOCIAL_HOUSING', + label: 'Registered provider of social housing', + }, + list_items: [ + { + code: 'PRIVATE_LANDLORD', + label: 'Private landlord', + }, + { + code: 'PROVIDER_OF_SOCIAL_HOUSING', + label: 'Registered provider of social housing', + }, + { + code: 'MORTGAGE_LENDER', + label: 'Mortgage lender', + }, + { + code: 'OTHER', + label: 'Other', + }, + ], + valueCode: 'PROVIDER_OF_SOCIAL_HOUSING', + }, + claimAgainstTrespassers: 'NO', + orgNameFound: 'Yes', + claimantName: 'Possession Claims Solicitor Org', + isClaimantNameCorrect: 'YES', + claimantContactEmail: 'pcs-solicitor-automation@test.com', + isCorrectClaimantContactEmail: 'YES', + orgAddressFound: 'Yes', + organisationAddress: { + AddressLine1: 'Ministry Of Justice', + AddressLine2: 'Seventh Floor 102 Petty France', + PostTown: 'London', + PostCode: 'SW1H 9AJ', + Country: 'United Kingdom', + }, + formattedClaimantContactAddress: 'Ministry Of Justice
Seventh Floor 102 Petty France
London
SW1H 9AJ', + isCorrectClaimantContactAddress: 'YES', + claimantProvidePhoneNumber: 'NO', + defendant1: { + nameKnown: 'YES', + firstName: 'Jeremiah', + lastName: 'Test', + addressKnown: 'YES', + addressSameAsPossession: 'YES', + }, + addAnotherDefendant: 'NO', + tenancy_TypeOfTenancyLicence: 'DEMOTED_TENANCY', + tenancy_TenancyLicenceDate: '2020-05-25', + tenancy_TenancyLicenceDocuments: [], + showIntroductoryDemotedOtherGroundReasonPage: 'No', + introGrounds_HasIntroductoryDemotedOtherGroundsForPossession: 'YES', + introGrounds_IntroductoryDemotedOrOtherGrounds: ['RENT_ARREARS'], + preActionProtocolCompleted: 'NO', + mediationAttempted: 'NO', + settlementAttempted: 'NO', + noticeServed: 'Yes', + rentDetails_CurrentRent: '250000', + rentDetails_Frequency: 'MONTHLY', + rentDetails_CalculatedDailyCharge: '8213', + notice_NoticeServiceMethod: 'FIRST_CLASS_POST', + notice_NoticePostedDate: '2022-05-20', + notice_NoticeDocuments: [], + rentSectionPaymentFrequency: 'MONTHLY', + rentDetails_FormattedCalculatedDailyCharge: '£82.13', + rentDetails_PerDayCorrect: 'YES', + showRentArrearsPage: 'Yes', + rentArrears_StatementDocuments: [ + { + id: '5b0334b3-2bdd-4ff5-a6bb-31a4850b61dc', + value: { + document_url: + 'http://dm-store-aat.service.core-compute-aat.internal/documents/a05c8b78-8c6e-43b8-b576-1254af8f51d8', + document_binary_url: + 'http://dm-store-aat.service.core-compute-aat.internal/documents/a05c8b78-8c6e-43b8-b576-1254af8f51d8/binary', + document_filename: 'Rent Arrears.docx', + document_hash: '62fcdba0614031ef277cc266972b5f03a2cb6f74589418d354c786f4e7ec6bbe', + }, + }, + ], + rentArrears_Total: '200000', + rentArrears_ThirdPartyPayments: 'NO', + arrearsJudgmentWanted: 'NO', + claimantNamePossessiveForm: 'Possession Claims Solicitor Org’s', + claimantCircumstancesSelect: 'NO', + hasDefendantCircumstancesInfo: 'NO', + suspensionOfRTB_ShowHousingActsPage: 'No', + demotionOfTenancy_ShowHousingActsPage: 'No', + suspensionToBuyDemotionOfTenancyPages: 'No', + alternativesToPossession: [], + claimingCostsWanted: 'NO', + additionalReasonsForPossession: { + hasReasons: 'NO', + }, + hasUnderlesseeOrMortgagee: 'NO', + wantToUploadDocuments: 'NO', + applicationWithClaim: 'NO', + languageUsed: 'ENGLISH_AND_WELSH', + completionNextStep: 'SUBMIT_AND_PAY_NOW', + statementOfTruth: { + completedBy: 'CLAIMANT', + fullNameClaimant: 'fg', + positionClaimant: 'fg', + agreementClaimant: ['BELIEVE_TRUE'], + }, + }; + }, + get submitCasePayloadDemoted() { + return { + legislativeCountry: 'England', + claimantType: { + value: { + code: 'PROVIDER_OF_SOCIAL_HOUSING', + label: 'Registered provider of social housing', + }, + list_items: [ + { + code: 'PRIVATE_LANDLORD', + label: 'Private landlord', + }, + { + code: 'PROVIDER_OF_SOCIAL_HOUSING', + label: 'Registered provider of social housing', + }, + { + code: 'MORTGAGE_LENDER', + label: 'Mortgage lender', + }, + { + code: 'OTHER', + label: 'Other', + }, + ], + valueCode: 'PROVIDER_OF_SOCIAL_HOUSING', + }, + claimAgainstTrespassers: 'NO', + orgNameFound: 'Yes', + claimantName: 'Possession Claims Solicitor Org', + isClaimantNameCorrect: 'YES', + claimantContactEmail: 'pcs-solicitor-automation@test.com', + isCorrectClaimantContactEmail: 'YES', + orgAddressFound: 'Yes', + organisationAddress: { + AddressLine1: 'Ministry Of Justice', + AddressLine2: 'Seventh Floor 102 Petty France', + PostTown: 'London', + PostCode: 'SW1H 9AJ', + Country: 'United Kingdom', + }, + formattedClaimantContactAddress: 'Ministry Of Justice
Seventh Floor 102 Petty France
London
SW1H 9AJ', + isCorrectClaimantContactAddress: 'YES', + claimantProvidePhoneNumber: 'NO', + addAnotherDefendant: 'NO', + tenancy_TypeOfTenancyLicence: 'DEMOTED_TENANCY', + tenancy_TenancyLicenceDate: '2020-05-20', + defendant1: { + nameKnown: 'YES', + firstName: 'Jeremiah', + lastName: 'Test', + addressKnown: 'YES', + addressSameAsPossession: 'YES', + }, + tenancy_TenancyLicenceDocuments: [], + showIntroductoryDemotedOtherGroundReasonPage: 'No', + introGrounds_HasIntroductoryDemotedOtherGroundsForPossession: 'YES', + introGrounds_IntroductoryDemotedOrOtherGrounds: ['RENT_ARREARS'], + preActionProtocolCompleted: 'NO', + mediationAttempted: 'NO', + settlementAttempted: 'NO', + noticeServed: 'No', + rentDetails_CurrentRent: '200000', + rentDetails_Frequency: 'MONTHLY', + rentDetails_CalculatedDailyCharge: '6570', + rentSectionPaymentFrequency: 'MONTHLY', + rentDetails_FormattedCalculatedDailyCharge: '£65.70', + rentDetails_PerDayCorrect: 'YES', + showRentArrearsPage: 'Yes', + rentArrears_StatementDocuments: [ + { + id: '7cd96ae9-5cb9-499c-a59c-4a8c8abf5cf6', + value: { + document_url: + 'http://dm-store-aat.service.core-compute-aat.internal/documents/78df59c5-df94-462c-9fe9-eaa0563a628f', + document_binary_url: + 'http://dm-store-aat.service.core-compute-aat.internal/documents/78df59c5-df94-462c-9fe9-eaa0563a628f/binary', + document_filename: 'Rent Arrears.docx', + document_hash: 'f80a62d4af70b26fdc97c2f65531191d1d664f9f5b6497e7fdcd21e7b646d774', + }, + }, + ], + rentArrears_Total: '500000', + rentArrears_ThirdPartyPayments: 'NO', + arrearsJudgmentWanted: 'NO', + claimantNamePossessiveForm: 'Possession Claims Solicitor Org’s', + claimantCircumstancesSelect: 'NO', + hasDefendantCircumstancesInfo: 'NO', + suspensionOfRTB_ShowHousingActsPage: 'No', + demotionOfTenancy_ShowHousingActsPage: 'No', + suspensionToBuyDemotionOfTenancyPages: 'No', + alternativesToPossession: [], + claimingCostsWanted: 'NO', + additionalReasonsForPossession: { + hasReasons: 'NO', + }, + hasUnderlesseeOrMortgagee: 'NO', + wantToUploadDocuments: 'NO', + applicationWithClaim: 'NO', + languageUsed: 'ENGLISH_AND_WELSH', + completionNextStep: 'SUBMIT_AND_PAY_NOW', + }; + }, submitCaseApiEndPoint: (): string => `/cases/${process.env.CASE_NUMBER}/events`, }; diff --git a/src/test/ui/e2eTest/respondToAClaim.spec.ts b/src/test/ui/e2eTest/respondToAClaim.spec.ts index 31a5ded8a..23084e2be 100644 --- a/src/test/ui/e2eTest/respondToAClaim.spec.ts +++ b/src/test/ui/e2eTest/respondToAClaim.spec.ts @@ -14,7 +14,7 @@ import { freeLegalAdvice, nonRentArrearsDispute, noticeDetails, - rentArrearsDispute, + rentArrears, repaymentsAgreed, repaymentsMade, startNow, @@ -338,8 +338,9 @@ test.describe('Respond to a claim - e2e Journey @nightly', async () => { option: noticeDetails.yesRadioOption, }); await performAction('enterNoticeDateUnknown'); - await performValidation('mainHeader', rentArrearsDispute.mainHeader); - await performAction('clickButton', rentArrearsDispute.continueButton); + await performAction('rentArrears', { + option: rentArrears.yesRadioOption, + }); // placeholder page, so need to be replaced with custom action when actual page is implemented await performValidation('mainHeader', counterClaim.mainHeader); await performAction('clickButton', counterClaim.saveAndContinueButton); @@ -383,8 +384,10 @@ test.describe('Respond to a claim - e2e Journey @nightly', async () => { month: '2', year: '2020', }); - await performValidation('mainHeader', rentArrearsDispute.mainHeader); - await performAction('clickButton', rentArrearsDispute.continueButton); + await performAction('rentArrears', { + option: rentArrears.noRadioOption, + rentAmount: rentArrears.rentAmountTextInput, + }); }); test('RentArrears - NoticeServed - Yes NoticeDateProvided - No - NoticeDetails - No - RentArrearsDispute @regression', async () => { @@ -418,8 +421,9 @@ test.describe('Respond to a claim - e2e Journey @nightly', async () => { await performAction('selectNoticeDetails', { option: noticeDetails.noRadioOption, }); - await performValidation('mainHeader', rentArrearsDispute.mainHeader); - await performAction('clickButton', rentArrearsDispute.continueButton); + await performAction('rentArrears', { + option: rentArrears.yesRadioOption, + }); // placeholder page, so need to be replaced with custom action when actual page is implemented await performValidation('mainHeader', counterClaim.mainHeader); await performAction('clickButton', counterClaim.saveAndContinueButton); @@ -457,8 +461,9 @@ test.describe('Respond to a claim - e2e Journey @nightly', async () => { await performAction('selectNoticeDetails', { option: noticeDetails.imNotSureRadioOption, }); - await performValidation('mainHeader', rentArrearsDispute.mainHeader); - await performAction('clickButton', rentArrearsDispute.continueButton); + await performAction('rentArrears', { + option: rentArrears.imNotSureRadioOption, + }); // placeholder page, so need to be replaced with custom action when actual page is implemented await performValidation('mainHeader', counterClaim.mainHeader); await performAction('clickButton', counterClaim.saveAndContinueButton); @@ -497,8 +502,9 @@ test.describe('Respond to a claim - e2e Journey @nightly', async () => { month: '12', year: '2025', }); - await performValidation('mainHeader', rentArrearsDispute.mainHeader); - await performAction('clickButton', rentArrearsDispute.continueButton); + await performAction('rentArrears', { + option: rentArrears.yesRadioOption, + }); // placeholder page, so need to be replaced with custom action when actual page is implemented await performValidation('mainHeader', counterClaim.mainHeader); await performAction('clickButton', counterClaim.saveAndContinueButton); diff --git a/src/test/ui/functional/respondToAClaim.spec.ts b/src/test/ui/functional/respondToAClaim.spec.ts index 358328e41..49064f96e 100644 --- a/src/test/ui/functional/respondToAClaim.spec.ts +++ b/src/test/ui/functional/respondToAClaim.spec.ts @@ -18,7 +18,7 @@ import { noticeDateUnknown, noticeDetails, paymentInterstitial, - rentArrearsDispute, + rentArrears, repaymentsMade, startNow, tenancyDateDetails, @@ -435,8 +435,9 @@ test.describe('Respond to a claim - functional @nightly', async () => { await performAction('selectNoticeDetails', { option: noticeDetails.imNotSureRadioOption, }); - await performValidation('mainHeader', rentArrearsDispute.mainHeader); - await performAction('clickButton', rentArrearsDispute.continueButton); + await performAction('rentArrears', { + option: rentArrears.yesRadioOption, + }); await performValidation('mainHeader', counterClaim.mainHeader); await performAction('clickButton', counterClaim.saveAndContinueButton); await performAction('clickButton', paymentInterstitial.continueButton); diff --git a/src/test/ui/utils/actions/custom-actions/respondToClaim.action.ts b/src/test/ui/utils/actions/custom-actions/respondToClaim.action.ts index 97625477b..429a6326b 100644 --- a/src/test/ui/utils/actions/custom-actions/respondToClaim.action.ts +++ b/src/test/ui/utils/actions/custom-actions/respondToClaim.action.ts @@ -14,6 +14,7 @@ import { noticeDateUnknown, noticeDetails, paymentInterstitial, + rentArrears, repaymentsMade, tenancyDateDetails, tenancyDateUnknown, @@ -40,6 +41,7 @@ export class RespondToClaimAction implements IAction { ['readPaymentInterstitial', () => this.readPaymentInterstitial()], ['repaymentsMade', () => this.repaymentsMade(fieldName as actionRecord)], ['disputeClaimInterstitial', () => this.disputeClaimInterstitial(fieldName as actionData)], + ['rentArrears', () => this.rentArrears(fieldName as actionRecord)], ['enterTenancyStartDetailsUnKnown', () => this.enterTenancyStartDetailsUnKnown(fieldName as actionRecord)], ]); const actionToPerform = actionsMap.get(action); @@ -217,6 +219,39 @@ export class RespondToClaimAction implements IAction { await performAction('clickButton', noticeDateUnknown.saveAndContinueButton); } + private async rentArrears(rentArrearsInfo: actionRecord): Promise { + // let rentArrears_Total: string | null = null; + // let claimantName: string | null = null; + // if (rentArrearsInfo.tenancy === 'flexible') { + // rentArrears_Total = `${submitCaseApiData.submitCasePayloadFlexibleTenancyDate.rentArrears_Total}`; + // claimantName = `${submitCaseApiData.submitCasePayloadFlexibleTenancyDate.claimantName}`; + // } + // if (rentArrearsInfo.tenancy === 'introductory') { + // rentArrears_Total = `${submitCaseApiData.submitCasePayloadIntroductoryTenancy.rentArrears_Total}`; + // claimantName = `${submitCaseApiData.submitCasePayloadIntroductoryTenancy.claimantName}`; + // } + + await performValidation('text', { + elementType: 'paragraph', + text: `Amount you owe in rent arrears given by ${submitCaseApiData.submitCasePayload.claimantName}:`, + }); + await performValidation('text', { + elementType: 'paragraph', + text: `Rent arrears are money you owe in rent payments. +When making their claim, ${submitCaseApiData.submitCasePayload.claimantName} had to provide a copy of the rent statement for your property, showing the total rent arrears you owe. +The rent statement will have been included in the pack you received in the post letting you know a claim had been made against you, and is also available to view from ‘View the claim’ on your case overview.`, + }); + //await performValidation('text', { elementType: 'heading', text: `${submitCaseApiData.submitCasePayload.rentArrears_Total}` }); + await performAction('clickRadioButton', { + question: rentArrears.doYouOweThisQuestion, + option: rentArrearsInfo.option, + }); + if (rentArrearsInfo.option === rentArrears.noRadioOption) { + await performAction('inputText', rentArrears.howMuchDoYouBelieveHiddenTextLabel, rentArrearsInfo.rentAmount); + } + await performAction('clickButton', rentArrears.saveAndContinueButton); + } + private async enterTenancyStartDetailsUnKnown(tenancyStartData: actionRecord) { const getDidNotProvideParagraph = tenancyDateUnknown.getDidNotProvideParagraph(claimantsName); await performValidation('text', { elementType: 'paragraph', text: getDidNotProvideParagraph }); diff --git a/src/test/ui/utils/registry/action.registry.ts b/src/test/ui/utils/registry/action.registry.ts index 8549314d7..f4e83fb07 100644 --- a/src/test/ui/utils/registry/action.registry.ts +++ b/src/test/ui/utils/registry/action.registry.ts @@ -58,6 +58,7 @@ export class ActionRegistry { ['enterTenancyStartDetailsUnKnown', new RespondToClaimAction()], ['triggerFunctionalTests', new TriggerPageFunctionalTestsAction()], ['selectTenancyStartDateKnown', new RespondToClaimAction()], + ['rentArrears', new RespondToClaimAction()], ]); static getAction(actionName: string): IAction {