From 4388591944a0d51b92b05505840240f37c3f282a Mon Sep 17 00:00:00 2001 From: arun Date: Sun, 22 Mar 2026 21:44:59 +0000 Subject: [PATCH 01/20] HDPI-3764: Add income and expenditure journey steps with bilingual support and conditional routing --- .../respondToClaim/incomeAndExpenditure.json | 14 + .../cy/respondToClaim/priorityDebts.json | 31 ++ .../cy/respondToClaim/regularExpenses.json | 4 + .../cy/respondToClaim/regularIncome.json | 55 +++ .../cy/respondToClaim/universalCredit.json | 30 ++ .../respondToClaim/incomeAndExpenditure.json | 14 + .../en/respondToClaim/priorityDebts.json | 31 ++ .../en/respondToClaim/regularExpenses.json | 4 + .../en/respondToClaim/regularIncome.json | 55 +++ .../en/respondToClaim/universalCredit.json | 30 ++ src/main/modules/index.ts | 17 +- .../steps/respond-to-claim/flow.config.ts | 42 +- .../incomeAndExpenditure.njk | 33 +- .../income-and-expenditure/index.ts | 50 ++- .../respond-to-claim/priority-debts/index.ts | 58 ++- .../priority-debts/priorityDebts.njk | 44 +- .../regular-expenses/index.ts | 24 +- .../respond-to-claim/regular-income/index.ts | 386 +++++++++++++++++- .../steps/respond-to-claim/stepRegistry.ts | 12 +- .../universal-credit/index.ts | 105 ++++- src/main/steps/utils/currencyConversion.ts | 56 +++ src/main/steps/utils/index.ts | 2 + src/main/steps/utils/yesNoEnum.ts | 44 ++ .../steps/utils/currencyConversion.test.ts | 130 ++++++ 24 files changed, 1216 insertions(+), 55 deletions(-) create mode 100644 src/main/assets/locales/cy/respondToClaim/incomeAndExpenditure.json create mode 100644 src/main/assets/locales/cy/respondToClaim/priorityDebts.json create mode 100644 src/main/assets/locales/cy/respondToClaim/regularExpenses.json create mode 100644 src/main/assets/locales/cy/respondToClaim/regularIncome.json create mode 100644 src/main/assets/locales/cy/respondToClaim/universalCredit.json create mode 100644 src/main/assets/locales/en/respondToClaim/incomeAndExpenditure.json create mode 100644 src/main/assets/locales/en/respondToClaim/priorityDebts.json create mode 100644 src/main/assets/locales/en/respondToClaim/regularExpenses.json create mode 100644 src/main/assets/locales/en/respondToClaim/regularIncome.json create mode 100644 src/main/assets/locales/en/respondToClaim/universalCredit.json create mode 100644 src/main/steps/utils/currencyConversion.ts create mode 100644 src/main/steps/utils/yesNoEnum.ts create mode 100644 src/test/unit/steps/utils/currencyConversion.test.ts diff --git a/src/main/assets/locales/cy/respondToClaim/incomeAndExpenditure.json b/src/main/assets/locales/cy/respondToClaim/incomeAndExpenditure.json new file mode 100644 index 000000000..37ef351b9 --- /dev/null +++ b/src/main/assets/locales/cy/respondToClaim/incomeAndExpenditure.json @@ -0,0 +1,14 @@ +{ + "caption": "cyResponding to the possession claim", + "pageTitle": "cyIncome and expenses", + "infoParagraph1": "cyProviding details about your finances could help the judge when they're deciding whether to grant the landlord a possession order, or if they can delay the date you have to leave your home.", + "infoParagraph2": "cyOn the day of the hearing, there may be a duty adviser at the court who can give you advice and represent you for free. They can also use this information to see if there's any additional benefits or support you might be entitled to.", + "question": "cyDo you want to provide details of your income and expenses?", + "options": { + "yes": "cyYes", + "no": "cyNo" + }, + "errors": { + "provideFinanceDetails": "cySelect if you want to provide details of your income and expenses" + } +} diff --git a/src/main/assets/locales/cy/respondToClaim/priorityDebts.json b/src/main/assets/locales/cy/respondToClaim/priorityDebts.json new file mode 100644 index 000000000..9d4415c33 --- /dev/null +++ b/src/main/assets/locales/cy/respondToClaim/priorityDebts.json @@ -0,0 +1,31 @@ +{ + "caption": "cyRespond to a property possession claim", + "pageTitle": "cyPriority debts", + "paragraph1": "cyPriority debts are debts which could lead to serious consequences if you do not pay them.", + "paragraph2": "cyExcluding rent or mortgage arrears, priority debts are:", + "bulletList": [ + "cycouncil tax arrears", + "cygas or electricity arrears", + "cyphone or internet arrears", + "cyTV licence arrears", + "cycourt fines", + "cyoverpaid tax credits", + "cypayments for goods bought on hire purchase or conditional sale", + "cyunpaid income tax, National Insurance or VAT", + "cyunpaid child maintenance" + ], + "guidanceLink": { + "text": "cyRead guidance from Citizens Advice on what counts as a priority debt (opens in new tab)", + "url": "https://www.citizensadvice.org.uk/debt-and-money/priority-debts/" + }, + "question": "cyDo you have any priority debts?", + "options": { + "yes": "cyYes", + "no": "cyNo" + }, + "errors": { + "hasPriorityDebts": { + "required": "cySelect if you have any priority debts" + } + } +} diff --git a/src/main/assets/locales/cy/respondToClaim/regularExpenses.json b/src/main/assets/locales/cy/respondToClaim/regularExpenses.json new file mode 100644 index 000000000..592f0c80a --- /dev/null +++ b/src/main/assets/locales/cy/respondToClaim/regularExpenses.json @@ -0,0 +1,4 @@ +{ + "pageTitle": "cyWhat other regular expenses do you have?", + "question": "cyWhat other regular expenses do you have?" +} diff --git a/src/main/assets/locales/cy/respondToClaim/regularIncome.json b/src/main/assets/locales/cy/respondToClaim/regularIncome.json new file mode 100644 index 000000000..06c408085 --- /dev/null +++ b/src/main/assets/locales/cy/respondToClaim/regularIncome.json @@ -0,0 +1,55 @@ +{ + "caption": "cyRespond to a property possession claim", + "pageTitle": "cyWhat regular income do you receive? (Optional)", + "hintText": "cySelect all that apply. Enter total amount in pounds and pence, for example £148.00 or £148.50. The information you provide must be truthful and accurate.", + "options": { + "incomeFromJobs": "cyIncome from all jobs you do", + "pension": "cyPension – state and private", + "universalCredit": "cyUniversal Credit", + "otherBenefits": "cyOther benefits and credits", + "moneyFromElsewhere": "cyMoney from somewhere else (for example, child maintenance payments or someone in your household gives you money)" + }, + "subFields": { + "amount": "cyTotal amount received", + "frequency": "cyReceived every:", + "moneyFromElsewhereDetails": "cyGive details about the other sources of income and how much you usually receive" + }, + "frequency": { + "week": "cyWeek", + "month": "cyMonth" + }, + "errors": { + "incomeFromJobsAmount": { + "required": "cyEnter the total amount you receive from all jobs you do" + }, + "incomeFromJobsFrequency": { + "required": "cySelect how frequently you receive income from all jobs you do" + }, + "pensionAmount": { + "required": "cyEnter the total amount you receive from a state or private pension" + }, + "pensionFrequency": { + "required": "cySelect how frequently you receive income from a state or private pension" + }, + "universalCreditAmount": { + "required": "cyEnter the total amount you receive from Universal Credit" + }, + "universalCreditFrequency": { + "required": "cySelect how frequently you receive Universal Credit" + }, + "otherBenefitsAmount": { + "required": "cyEnter the total amount you receive from other benefits and credits" + }, + "otherBenefitsFrequency": { + "required": "cySelect how frequently you receive income from other benefits and credits" + }, + "moneyFromElsewhereDetails": { + "required": "cyGive details about the other sources of income and how much you usually receive" + }, + "amount": { + "negative": "cyAmount cannot be negative", + "tooLarge": "cyAmount is too large", + "invalidFormat": "cyEnter amount in pounds and pence, for example 148.00 or 148.50" + } + } +} diff --git a/src/main/assets/locales/cy/respondToClaim/universalCredit.json b/src/main/assets/locales/cy/respondToClaim/universalCredit.json new file mode 100644 index 000000000..4ded96c86 --- /dev/null +++ b/src/main/assets/locales/cy/respondToClaim/universalCredit.json @@ -0,0 +1,30 @@ +{ + "caption": "cyRespond to a property possession claim", + "pageTitle": "cyHave you applied for Universal Credit?", + "options": { + "yes": "cyYes", + "no": "cyNo" + }, + "subFields": { + "applicationDate": { + "label": "cyWhen did you apply?", + "hint": "cyFor example, 27 9 2022" + } + }, + "errors": { + "appliedForUniversalCredit": { + "required": "cySelect if you've applied for Universal Credit" + }, + "date": { + "day": "cyday", + "month": "cymonth", + "year": "cyyear", + "notRealDate": "cyThe date you applied for Universal Credit must be a real date", + "futureDate": "cyThe date you applied for Universal Credit must either be today's date or in the past", + "invalidValue": "cyThe date you applied for Universal Credit must be a real date", + "missingOne": "cyThe date you applied for Universal Credit must include a {{missingField}}", + "missingTwo": "cyThe date you applied for Universal Credit must include a {{first}} and {{second}}", + "required": "cyEnter the date you applied for Universal Credit" + } + } +} diff --git a/src/main/assets/locales/en/respondToClaim/incomeAndExpenditure.json b/src/main/assets/locales/en/respondToClaim/incomeAndExpenditure.json new file mode 100644 index 000000000..12794550e --- /dev/null +++ b/src/main/assets/locales/en/respondToClaim/incomeAndExpenditure.json @@ -0,0 +1,14 @@ +{ + "caption": "Responding to the possession claim", + "pageTitle": "Income and expenses", + "infoParagraph1": "Providing details about your finances could help the judge when they're deciding whether to grant the landlord a possession order, or if they can delay the date you have to leave your home.", + "infoParagraph2": "On the day of the hearing, there may be a duty adviser at the court who can give you advice and represent you for free. They can also use this information to see if there's any additional benefits or support you might be entitled to.", + "question": "Do you want to provide details of your income and expenses?", + "options": { + "yes": "Yes", + "no": "No" + }, + "errors": { + "provideFinanceDetails": "Select if you want to provide details of your income and expenses" + } +} diff --git a/src/main/assets/locales/en/respondToClaim/priorityDebts.json b/src/main/assets/locales/en/respondToClaim/priorityDebts.json new file mode 100644 index 000000000..e0a9f1128 --- /dev/null +++ b/src/main/assets/locales/en/respondToClaim/priorityDebts.json @@ -0,0 +1,31 @@ +{ + "caption": "Respond to a property possession claim", + "pageTitle": "Priority debts", + "paragraph1": "Priority debts are debts which could lead to serious consequences if you do not pay them.", + "paragraph2": "Excluding rent or mortgage arrears, priority debts are:", + "bulletList": [ + "council tax arrears", + "gas or electricity arrears", + "phone or internet arrears", + "TV licence arrears", + "court fines", + "overpaid tax credits", + "payments for goods bought on hire purchase or conditional sale", + "unpaid income tax, National Insurance or VAT", + "unpaid child maintenance" + ], + "guidanceLink": { + "text": "Read guidance from Citizens Advice on what counts as a priority debt (opens in new tab)", + "url": "https://www.citizensadvice.org.uk/debt-and-money/priority-debts/" + }, + "question": "Do you have any priority debts?", + "options": { + "yes": "Yes", + "no": "No" + }, + "errors": { + "hasPriorityDebts": { + "required": "Select if you have any priority debts" + } + } +} diff --git a/src/main/assets/locales/en/respondToClaim/regularExpenses.json b/src/main/assets/locales/en/respondToClaim/regularExpenses.json new file mode 100644 index 000000000..93cbf02ed --- /dev/null +++ b/src/main/assets/locales/en/respondToClaim/regularExpenses.json @@ -0,0 +1,4 @@ +{ + "pageTitle": "What other regular expenses do you have?", + "question": "What other regular expenses do you have?" +} diff --git a/src/main/assets/locales/en/respondToClaim/regularIncome.json b/src/main/assets/locales/en/respondToClaim/regularIncome.json new file mode 100644 index 000000000..6d211420e --- /dev/null +++ b/src/main/assets/locales/en/respondToClaim/regularIncome.json @@ -0,0 +1,55 @@ +{ + "caption": "Respond to a property possession claim", + "pageTitle": "What regular income do you receive? (Optional)", + "hintText": "Select all that apply. Enter total amount in pounds and pence, for example £148.00 or £148.50. The information you provide must be truthful and accurate.", + "options": { + "incomeFromJobs": "Income from all jobs you do", + "pension": "Pension – state and private", + "universalCredit": "Universal Credit", + "otherBenefits": "Other benefits and credits", + "moneyFromElsewhere": "Money from somewhere else (for example, child maintenance payments or someone in your household gives you money)" + }, + "subFields": { + "amount": "Total amount received", + "frequency": "Received every:", + "moneyFromElsewhereDetails": "Give details about the other sources of income and how much you usually receive" + }, + "frequency": { + "week": "Week", + "month": "Month" + }, + "errors": { + "incomeFromJobsAmount": { + "required": "Enter the total amount you receive from all jobs you do" + }, + "incomeFromJobsFrequency": { + "required": "Select how frequently you receive income from all jobs you do" + }, + "pensionAmount": { + "required": "Enter the total amount you receive from a state or private pension" + }, + "pensionFrequency": { + "required": "Select how frequently you receive income from a state or private pension" + }, + "universalCreditAmount": { + "required": "Enter the total amount you receive from Universal Credit" + }, + "universalCreditFrequency": { + "required": "Select how frequently you receive Universal Credit" + }, + "otherBenefitsAmount": { + "required": "Enter the total amount you receive from other benefits and credits" + }, + "otherBenefitsFrequency": { + "required": "Select how frequently you receive income from other benefits and credits" + }, + "moneyFromElsewhereDetails": { + "required": "Give details about the other sources of income and how much you usually receive" + }, + "amount": { + "negative": "Amount cannot be negative", + "tooLarge": "Amount is too large", + "invalidFormat": "Enter amount in pounds and pence, for example 148.00 or 148.50" + } + } +} diff --git a/src/main/assets/locales/en/respondToClaim/universalCredit.json b/src/main/assets/locales/en/respondToClaim/universalCredit.json new file mode 100644 index 000000000..c6377dd35 --- /dev/null +++ b/src/main/assets/locales/en/respondToClaim/universalCredit.json @@ -0,0 +1,30 @@ +{ + "caption": "Respond to a property possession claim", + "pageTitle": "Have you applied for Universal Credit?", + "options": { + "yes": "Yes", + "no": "No" + }, + "subFields": { + "applicationDate": { + "label": "When did you apply?", + "hint": "For example, 27 9 2022" + } + }, + "errors": { + "appliedForUniversalCredit": { + "required": "Select if you've applied for Universal Credit" + }, + "date": { + "day": "day", + "month": "month", + "year": "year", + "notRealDate": "The date you applied for Universal Credit must be a real date", + "futureDate": "The date you applied for Universal Credit must either be today's date or in the past", + "invalidValue": "The date you applied for Universal Credit must be a real date", + "missingOne": "The date you applied for Universal Credit must include a {{missingField}}", + "missingTwo": "The date you applied for Universal Credit must include a {{first}} and {{second}}", + "required": "Enter the date you applied for Universal Credit" + } + } +} diff --git a/src/main/modules/index.ts b/src/main/modules/index.ts index 2004c5d33..6dc55692e 100644 --- a/src/main/modules/index.ts +++ b/src/main/modules/index.ts @@ -1,13 +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'; -export { Csrf } from './csrf'; + +// 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', 'Csrf']; +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 1963b83fb..dc06a7330 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -49,11 +49,11 @@ export const flowConfig: JourneyFlowConfig = { 'your-circumstances', 'exceptional-hardship', 'income-and-expenditure', - 'what-regular-income-do-you-receive', - 'have-you-applied-for-universal-credit', + 'regular-income', + 'universal-credit', 'priority-debts', 'priority-debt-details', - 'what-other-regular-expenses-do-you-have', + 'regular-expenses', 'end-now', ], steps: { @@ -358,25 +358,45 @@ export const flowConfig: JourneyFlowConfig = { }, 'income-and-expenditure': { previousStep: 'exceptional-hardship', - defaultNext: 'what-regular-income-do-you-receive', + routes: [ + { + condition: async (req, formData) => formData.provideFinanceDetails === 'yes', + nextStep: 'regular-income', + }, + { + condition: async (req, formData) => formData.provideFinanceDetails === 'no', + nextStep: 'end-now', + }, + ], + defaultNext: 'regular-income', }, - 'what-regular-income-do-you-receive': { + 'regular-income': { previousStep: 'income-and-expenditure', - defaultNext: 'have-you-applied-for-universal-credit', + defaultNext: 'universal-credit', }, - 'have-you-applied-for-universal-credit': { - previousStep: 'what-regular-income-do-you-receive', + 'universal-credit': { + previousStep: 'regular-income', defaultNext: 'priority-debts', }, 'priority-debts': { - previousStep: 'have-you-applied-for-universal-credit', + previousStep: 'universal-credit', + routes: [ + { + condition: async (req, formData) => formData.hasPriorityDebts === 'yes', + nextStep: 'priority-debt-details', + }, + { + condition: async (req, formData) => formData.hasPriorityDebts === 'no', + nextStep: 'regular-expenses', + }, + ], defaultNext: 'priority-debt-details', }, 'priority-debt-details': { previousStep: 'priority-debts', - defaultNext: 'what-other-regular-expenses-do-you-have', + defaultNext: 'regular-expenses', }, - 'what-other-regular-expenses-do-you-have': { + 'regular-expenses': { previousStep: 'priority-debt-details', defaultNext: 'end-now', }, diff --git a/src/main/steps/respond-to-claim/income-and-expenditure/incomeAndExpenditure.njk b/src/main/steps/respond-to-claim/income-and-expenditure/incomeAndExpenditure.njk index 4c6bb2978..3b3bf3892 100644 --- a/src/main/steps/respond-to-claim/income-and-expenditure/incomeAndExpenditure.njk +++ b/src/main/steps/respond-to-claim/income-and-expenditure/incomeAndExpenditure.njk @@ -1,17 +1,40 @@ {% extends "stepsTemplate.njk" %} -{% from "govuk/components/button/macro.njk" import govukButton %} {% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} +{% from "govuk/components/radios/macro.njk" import govukRadios %} +{% from "govuk/components/button/macro.njk" import govukButton %} {% from "macros/csrf.njk" import csrfProtection %} + +{% block pageTitle %}{{ pageTitle }} – HM Courts & Tribunals Service – GOV.UK{% endblock %} + {% block mainContent %} {% if errorSummary %} {{ govukErrorSummary(errorSummary) }} {% endif %} -

Income and expenses (placeholder)

-

This is a placeholder for Income and Expenditure step.

-
+ + {% if caption %} + {{ caption }} + {% endif %} + +

{{ pageTitle }}

+ + {% if infoParagraph1 %} +

{{ infoParagraph1 }}

+ {% endif %} + + {% if infoParagraph2 %} +

{{ infoParagraph2 }}

+ {% endif %} + + + {% for field in fields %} + {% if field.componentType == 'radios' %} + {{ govukRadios(field.component) }} + {% endif %} + {% endfor %} +
{{ govukButton({ - text: continue, + text: saveAndContinue, attributes: { type: 'submit', name: 'action', value: 'continue' } }) }} {{ govukButton({ diff --git a/src/main/steps/respond-to-claim/income-and-expenditure/index.ts b/src/main/steps/respond-to-claim/income-and-expenditure/index.ts index dc9f3fe99..012033edf 100644 --- a/src/main/steps/respond-to-claim/income-and-expenditure/index.ts +++ b/src/main/steps/respond-to-claim/income-and-expenditure/index.ts @@ -1,13 +1,61 @@ import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; +import { fromYesNoEnum, toYesNoEnum } from '../../utils/yesNoEnum'; import { flowConfig } from '../flow.config'; import { createFormStep } from '@modules/steps'; +import type { PossessionClaimResponse } from '@services/pcsApi'; +import { buildCcdCaseForPossessionClaimResponse } from '@services/pcsApi'; export const step: StepDefinition = createFormStep({ stepName: 'income-and-expenditure', journeyFolder: 'respondToClaim', stepDir: __dirname, flowConfig, - fields: [], customTemplate: `${__dirname}/incomeAndExpenditure.njk`, + + getInitialFormData: req => { + const caseData = req.res?.locals?.validatedCase?.data; + const existingAnswer = + caseData?.possessionClaimResponse?.defendantResponses?.householdCircumstances?.shareIncomeExpenseDetails; + const formValue = fromYesNoEnum(existingAnswer); + return formValue ? { provideFinanceDetails: formValue } : {}; + }, + + beforeRedirect: async req => { + const provideFinanceDetails = req.body?.provideFinanceDetails as 'yes' | 'no' | undefined; + if (!provideFinanceDetails) { + return; + } + + const possessionClaimResponse: PossessionClaimResponse = { + defendantResponses: { + householdCircumstances: { + shareIncomeExpenseDetails: toYesNoEnum(provideFinanceDetails), + }, + }, + }; + await buildCcdCaseForPossessionClaimResponse(req, possessionClaimResponse); + }, + + translationKeys: { + caption: 'caption', + pageTitle: 'pageTitle', + infoParagraph1: 'infoParagraph1', + infoParagraph2: 'infoParagraph2', + question: 'question', + }, + + fields: [ + { + name: 'provideFinanceDetails', + type: 'radio', + required: true, + translationKey: { label: 'question' }, + legendClasses: 'govuk-fieldset__legend--m', + options: [ + { value: 'yes', translationKey: 'options.yes' }, + { value: 'no', translationKey: 'options.no' }, + ], + }, + ], }); diff --git a/src/main/steps/respond-to-claim/priority-debts/index.ts b/src/main/steps/respond-to-claim/priority-debts/index.ts index b925177a5..ad6841b6c 100644 --- a/src/main/steps/respond-to-claim/priority-debts/index.ts +++ b/src/main/steps/respond-to-claim/priority-debts/index.ts @@ -8,6 +8,62 @@ export const step: StepDefinition = createFormStep({ journeyFolder: 'respondToClaim', stepDir: __dirname, flowConfig, - fields: [], customTemplate: `${__dirname}/priorityDebts.njk`, + + // TODO: Uncomment when backend API field is added to CCD + // getInitialFormData: (req: Request) => { + // const caseData = req.res?.locals?.validatedCase?.data; + // const response = caseData?.possessionClaimResponse?.defendantResponses; + // + // if (!response) return {}; + // + // const formData: Record = {}; + // + // if (response.hasPriorityDebts) { + // formData.hasPriorityDebts = response.hasPriorityDebts; + // } + // + // return formData; + // }, + + // TODO: Uncomment when backend API field is added to CCD + // beforeRedirect: async (req: Request) => { + // const hasPriorityDebts = req.body?.hasPriorityDebts as string | undefined; + // + // if (!hasPriorityDebts) return; + // + // const possessionClaimResponse: PossessionClaimResponse = { + // defendantResponses: { + // hasPriorityDebts, + // }, + // }; + // + // await buildCcdCaseForPossessionClaimResponse(req, possessionClaimResponse); + // }, + + translationKeys: { + caption: 'caption', + pageTitle: 'pageTitle', + paragraph1: 'paragraph1', + paragraph2: 'paragraph2', + bulletList: 'bulletList', + guidanceLinkText: 'guidanceLink.text', + guidanceLinkUrl: 'guidanceLink.url', + question: 'question', + }, + + fields: [ + { + name: 'hasPriorityDebts', + type: 'radio', + required: true, + errorMessage: 'errors.hasPriorityDebts.required', + translationKey: { label: 'question' }, + legendClasses: 'govuk-fieldset__legend--m', + options: [ + { value: 'yes', translationKey: 'options.yes' }, + { value: 'no', translationKey: 'options.no' }, + ], + }, + ], }); diff --git a/src/main/steps/respond-to-claim/priority-debts/priorityDebts.njk b/src/main/steps/respond-to-claim/priority-debts/priorityDebts.njk index 39c1190c6..8c1ee4072 100644 --- a/src/main/steps/respond-to-claim/priority-debts/priorityDebts.njk +++ b/src/main/steps/respond-to-claim/priority-debts/priorityDebts.njk @@ -1,25 +1,25 @@ {% extends "stepsTemplate.njk" %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "macros/csrf.njk" import csrfProtection %} +{% from "govuk/components/radios/macro.njk" import govukRadios %} +{% from "macros/formButtons.njk" import formButtons %} + {% block mainContent %} - {% if errorSummary %} - {{ govukErrorSummary(errorSummary) }} - {% endif %} -

Priority debts (placeholder)

-

This is a placeholder for Priority Debts step.

- -
- {{ govukButton({ - text: continue, - attributes: { type: 'submit', name: 'action', value: 'continue' } - }) }} - {{ govukButton({ - text: saveForLater, - classes: 'govuk-button--secondary', - attributes: { type: 'submit', name: 'action', value: 'saveForLater' } - }) }} -
- {{ csrfProtection(csrfToken) }} - +

{{ pageTitle }}

+ +

{{ paragraph1 }}

+ +

{{ paragraph2 }}

+ +
    + {% for item in bulletList %} +
  • {{ item }}
  • + {% endfor %} +
+ +

+ {{ guidanceLinkText }}. +

+ + {{ formHtml | safe }} + + {{ formButtons(buttons) }} {% endblock %} diff --git a/src/main/steps/respond-to-claim/regular-expenses/index.ts b/src/main/steps/respond-to-claim/regular-expenses/index.ts index cf63638d6..1e800c84c 100644 --- a/src/main/steps/respond-to-claim/regular-expenses/index.ts +++ b/src/main/steps/respond-to-claim/regular-expenses/index.ts @@ -4,10 +4,30 @@ import { flowConfig } from '../flow.config'; import { createFormStep } from '@modules/steps'; export const step: StepDefinition = createFormStep({ - stepName: 'what-other-regular-expenses-do-you-have', + stepName: 'regular-expenses', journeyFolder: 'respondToClaim', stepDir: __dirname, flowConfig, - fields: [], customTemplate: `${__dirname}/regularExpenses.njk`, + + // TODO: Add field configuration for expense categories + // TODO: Uncomment when backend API field is added to CCD + // getInitialFormData: req => { + // const caseData = req.res?.locals?.validatedCase?.data; + // // Map CCD data to form values + // return {}; + // }, + + // TODO: Uncomment when backend API field is added to CCD + // beforeRedirect: async req => { + // // Save to CCD before redirect + // await buildCcdCaseForPossessionClaimResponse(req, possessionClaimResponse); + // }, + + translationKeys: { + pageTitle: 'pageTitle', + question: 'question', + }, + + fields: [], }); diff --git a/src/main/steps/respond-to-claim/regular-income/index.ts b/src/main/steps/respond-to-claim/regular-income/index.ts index f0a5879cf..f827b3f5e 100644 --- a/src/main/steps/respond-to-claim/regular-income/index.ts +++ b/src/main/steps/respond-to-claim/regular-income/index.ts @@ -1,13 +1,395 @@ +import type { Request } from 'express'; + +import type { PossessionClaimResponse } from '../../../interfaces/ccdCase.interface'; import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; +import { penceToPounds, poundsToPence, toYesNoEnum } from '../../utils'; +import { buildCcdCaseForPossessionClaimResponse } from '../../utils/populateResponseToClaimPayloadmap'; import { flowConfig } from '../flow.config'; import { createFormStep } from '@modules/steps'; +// Validation constants (copied from rent-arrears-dispute) +const MAX_INCOME_AMOUNT = 1_000_000_000; // £1 billion maximum +const AMOUNT_FORMAT_REGEX = /^\d{1,10}\.\d{2}$/; // Up to 10 digits, exactly 2 decimal places + +// Amount validator helper (copied from rent-arrears-dispute pattern) +const validateAmount = (value: unknown): boolean | string => { + if (typeof value !== 'string') { + return true; + } + + const trimmed = value.trim(); + if (!trimmed) { + return true; + } // Let required validation handle empty values + + const normalized = trimmed.replace(/,/g, ''); + const numericValue = parseFloat(normalized); + + if (!Number.isNaN(numericValue)) { + if (numericValue < 0) { + return 'errors.amount.negative'; + } + if (numericValue > MAX_INCOME_AMOUNT) { + return 'errors.amount.tooLarge'; + } + } + + if (!AMOUNT_FORMAT_REGEX.test(normalized)) { + return 'errors.amount.invalidFormat'; + } + + return true; +}; + export const step: StepDefinition = createFormStep({ - stepName: 'what-regular-income-do-you-receive', + stepName: 'regular-income', journeyFolder: 'respondToClaim', stepDir: __dirname, flowConfig, - fields: [], customTemplate: `${__dirname}/regularIncome.njk`, + + getInitialFormData: (req: Request) => { + const caseData = req.res?.locals?.validatedCase?.data; + const hc = caseData?.possessionClaimResponse?.defendantResponses?.householdCircumstances; + + if (!hc) { + return {}; + } + + const formData: Record = {}; + const selectedIncome: string[] = []; + + // Income from jobs + if (hc.incomeFromJobs === 'YES') { + selectedIncome.push('incomeFromJobs'); + if (hc.incomeFromJobsAmount) { + formData['regularIncome.incomeFromJobsAmount'] = penceToPounds(hc.incomeFromJobsAmount as string); + } + if (hc.incomeFromJobsFrequency) { + formData['regularIncome.incomeFromJobsFrequency'] = (hc.incomeFromJobsFrequency as string).toLowerCase(); + } + } + + // Pension + if (hc.pension === 'YES') { + selectedIncome.push('pension'); + if (hc.pensionAmount) { + formData['regularIncome.pensionAmount'] = penceToPounds(hc.pensionAmount as string); + } + if (hc.pensionFrequency) { + formData['regularIncome.pensionFrequency'] = (hc.pensionFrequency as string).toLowerCase(); + } + } + + // Universal Credit (Note: amount/frequency may not exist - see BA clarification) + if (hc.universalCreditIncome === 'YES') { + selectedIncome.push('universalCredit'); + // UC amount/frequency commented out pending BA clarification + // if (hc.universalCreditAmount) { + // const amountInPence = parseFloat(hc.universalCreditAmount as string); + // const amountInPounds = amountInPence / 100; + // formData['regularIncome.universalCreditAmount'] = amountInPounds.toFixed(2); + // } + // if (hc.universalCreditFrequency) { + // formData['regularIncome.universalCreditFrequency'] = (hc.universalCreditFrequency as string).toLowerCase(); + // } + } + + // Other benefits + if (hc.otherBenefits === 'YES') { + selectedIncome.push('otherBenefits'); + if (hc.otherBenefitsAmount) { + formData['regularIncome.otherBenefitsAmount'] = penceToPounds(hc.otherBenefitsAmount as string); + } + if (hc.otherBenefitsFrequency) { + formData['regularIncome.otherBenefitsFrequency'] = (hc.otherBenefitsFrequency as string).toLowerCase(); + } + } + + // Money from elsewhere + if (hc.moneyFromElsewhere === 'YES') { + selectedIncome.push('moneyFromElsewhere'); + if (hc.moneyFromElsewhereDetails) { + formData['regularIncome.moneyFromElsewhereDetails'] = hc.moneyFromElsewhereDetails; + } + } + + if (selectedIncome.length > 0) { + formData.regularIncome = selectedIncome; + } + + return formData; + }, + + beforeRedirect: async (req: Request) => { + const selectedIncome = req.body?.regularIncome as string | string[] | undefined; + + const incomeArray = Array.isArray(selectedIncome) ? selectedIncome : selectedIncome ? [selectedIncome] : []; + const householdCircumstances: Record = {}; + + // Income from jobs + householdCircumstances.incomeFromJobs = toYesNoEnum(incomeArray.includes('incomeFromJobs') ? 'yes' : 'no'); + if (incomeArray.includes('incomeFromJobs')) { + const amountRaw = req.body?.['regularIncome.incomeFromJobsAmount'] as string | undefined; + const frequency = req.body?.['regularIncome.incomeFromJobsFrequency'] as string | undefined; + + if (amountRaw) { + householdCircumstances.incomeFromJobsAmount = poundsToPence(amountRaw); + } + if (frequency) { + householdCircumstances.incomeFromJobsFrequency = frequency.toUpperCase(); + } + } + + // Pension + householdCircumstances.pension = toYesNoEnum(incomeArray.includes('pension') ? 'yes' : 'no'); + if (incomeArray.includes('pension')) { + const amountRaw = req.body?.['regularIncome.pensionAmount'] as string | undefined; + const frequency = req.body?.['regularIncome.pensionFrequency'] as string | undefined; + + if (amountRaw) { + householdCircumstances.pensionAmount = poundsToPence(amountRaw); + } + if (frequency) { + householdCircumstances.pensionFrequency = frequency.toUpperCase(); + } + } + + // Universal Credit (checkbox only - amount/frequency pending BA clarification) + householdCircumstances.universalCreditIncome = toYesNoEnum(incomeArray.includes('universalCredit') ? 'yes' : 'no'); + // UC amount/frequency commented out pending BA clarification + // if (incomeArray.includes('universalCredit')) { + // const amountRaw = req.body?.['regularIncome.universalCreditAmount'] as string | undefined; + // const frequency = req.body?.['regularIncome.universalCreditFrequency'] as string | undefined; + // if (amountRaw) { + // const normalized = amountRaw.replace(/,/g, ''); + // const amountInPounds = parseFloat(normalized); + // if (!Number.isNaN(amountInPounds)) { + // householdCircumstances.universalCreditAmount = String(Math.round(amountInPounds * 100)); + // } + // } + // if (frequency) { + // householdCircumstances.universalCreditFrequency = frequency.toUpperCase(); + // } + // } + + // Other benefits + householdCircumstances.otherBenefits = toYesNoEnum(incomeArray.includes('otherBenefits') ? 'yes' : 'no'); + if (incomeArray.includes('otherBenefits')) { + const amountRaw = req.body?.['regularIncome.otherBenefitsAmount'] as string | undefined; + const frequency = req.body?.['regularIncome.otherBenefitsFrequency'] as string | undefined; + + if (amountRaw) { + householdCircumstances.otherBenefitsAmount = poundsToPence(amountRaw); + } + if (frequency) { + householdCircumstances.otherBenefitsFrequency = frequency.toUpperCase(); + } + } + + // Money from elsewhere + householdCircumstances.moneyFromElsewhere = toYesNoEnum(incomeArray.includes('moneyFromElsewhere') ? 'yes' : 'no'); + if (incomeArray.includes('moneyFromElsewhere')) { + const details = req.body?.['regularIncome.moneyFromElsewhereDetails'] as string | undefined; + if (details) { + householdCircumstances.moneyFromElsewhereDetails = details; + } + } + + const possessionClaimResponse: PossessionClaimResponse = { + defendantResponses: { + householdCircumstances, + }, + }; + await buildCcdCaseForPossessionClaimResponse(req, possessionClaimResponse); + }, + + translationKeys: { + caption: 'caption', + pageTitle: 'pageTitle', + hintText: 'hintText', + }, + + fields: [ + { + name: 'regularIncome', + type: 'checkbox', + required: false, // Page is optional - can select zero checkboxes + translationKey: { + hint: 'hintText', + }, + options: [ + // Option 1: Income from all jobs you do + { + value: 'incomeFromJobs', + translationKey: 'options.incomeFromJobs', + subFields: { + incomeFromJobsAmount: { + name: 'incomeFromJobsAmount', + type: 'text', + required: true, + errorMessage: 'errors.incomeFromJobsAmount.required', + translationKey: { + label: 'subFields.amount', + }, + prefix: { + text: '£', + }, + classes: 'govuk-input--width-10', + attributes: { + inputmode: 'decimal', + spellcheck: false, + }, + validator: validateAmount, + }, + incomeFromJobsFrequency: { + name: 'incomeFromJobsFrequency', + type: 'radio', + required: true, + errorMessage: 'errors.incomeFromJobsFrequency.required', + translationKey: { + label: 'subFields.frequency', + }, + options: [ + { value: 'week', translationKey: 'frequency.week' }, + { value: 'month', translationKey: 'frequency.month' }, + ], + }, + }, + }, + // Option 2: Pension - state and private + { + value: 'pension', + translationKey: 'options.pension', + subFields: { + pensionAmount: { + name: 'pensionAmount', + type: 'text', + required: true, + errorMessage: 'errors.pensionAmount.required', + translationKey: { + label: 'subFields.amount', + }, + prefix: { + text: '£', + }, + classes: 'govuk-input--width-10', + attributes: { + inputmode: 'decimal', + spellcheck: false, + }, + validator: validateAmount, + }, + pensionFrequency: { + name: 'pensionFrequency', + type: 'radio', + required: true, + errorMessage: 'errors.pensionFrequency.required', + translationKey: { + label: 'subFields.frequency', + }, + options: [ + { value: 'week', translationKey: 'frequency.week' }, + { value: 'month', translationKey: 'frequency.month' }, + ], + }, + }, + }, + // Option 3: Universal Credit + { + value: 'universalCredit', + translationKey: 'options.universalCredit', + subFields: { + universalCreditAmount: { + name: 'universalCreditAmount', + type: 'text', + required: true, + errorMessage: 'errors.universalCreditAmount.required', + translationKey: { + label: 'subFields.amount', + }, + prefix: { + text: '£', + }, + classes: 'govuk-input--width-10', + attributes: { + inputmode: 'decimal', + spellcheck: false, + }, + validator: validateAmount, + }, + universalCreditFrequency: { + name: 'universalCreditFrequency', + type: 'radio', + required: true, + errorMessage: 'errors.universalCreditFrequency.required', + translationKey: { + label: 'subFields.frequency', + }, + options: [ + { value: 'week', translationKey: 'frequency.week' }, + { value: 'month', translationKey: 'frequency.month' }, + ], + }, + }, + }, + // Option 4: Other benefits and credits + { + value: 'otherBenefits', + translationKey: 'options.otherBenefits', + subFields: { + otherBenefitsAmount: { + name: 'otherBenefitsAmount', + type: 'text', + required: true, + errorMessage: 'errors.otherBenefitsAmount.required', + translationKey: { + label: 'subFields.amount', + }, + prefix: { + text: '£', + }, + classes: 'govuk-input--width-10', + attributes: { + inputmode: 'decimal', + spellcheck: false, + }, + validator: validateAmount, + }, + otherBenefitsFrequency: { + name: 'otherBenefitsFrequency', + type: 'radio', + required: true, + errorMessage: 'errors.otherBenefitsFrequency.required', + translationKey: { + label: 'subFields.frequency', + }, + options: [ + { value: 'week', translationKey: 'frequency.week' }, + { value: 'month', translationKey: 'frequency.month' }, + ], + }, + }, + }, + // Option 5: Money from somewhere else - TEXTAREA ONLY (no amount/frequency) + { + value: 'moneyFromElsewhere', + translationKey: 'options.moneyFromElsewhere', + subFields: { + moneyFromElsewhereDetails: { + name: 'moneyFromElsewhereDetails', + type: 'character-count', + maxLength: 500, + required: true, + errorMessage: 'errors.moneyFromElsewhereDetails.required', + translationKey: { + label: 'subFields.moneyFromElsewhereDetails', + }, + }, + }, + }, + ], + }, + ], }); diff --git a/src/main/steps/respond-to-claim/stepRegistry.ts b/src/main/steps/respond-to-claim/stepRegistry.ts index b5ac8d94a..5746c9940 100644 --- a/src/main/steps/respond-to-claim/stepRegistry.ts +++ b/src/main/steps/respond-to-claim/stepRegistry.ts @@ -26,8 +26,8 @@ import { step as doAnyOtherAdultsLiveInYourHome } from './other-adults'; import { step as paymentInterstitial } from './payment-interstitial'; import { step as priorityDebtDetails } from './priority-debt-details'; import { step as priorityDebts } from './priority-debts'; -import { step as whatOtherRegularExpensesDoYouHave } from './regular-expenses'; -import { step as whatRegularIncomeDoYouReceive } from './regular-income'; +import { step as regularExpenses } from './regular-expenses'; +import { step as regularIncome } from './regular-income'; import { step as rentArrearsDispute } from './rent-arrears-dispute'; import { step as repaymentsAgreed } from './repayments-agreed'; import { step as repaymentsMade } from './repayments-made'; @@ -36,7 +36,7 @@ import { step as startNow } from './start-now'; import { step as tenancyDateDetails } from './tenancy-date-details'; import { step as tenancyDateUnknown } from './tenancy-date-unknown'; import { step as tenancyTypeDetails } from './tenancy-type-details'; -import { step as haveYouAppliedForUniversalCredit } from './universal-credit'; +import { step as universalCredit } from './universal-credit'; export const stepRegistry: Record = { 'start-now': startNow, @@ -72,9 +72,9 @@ export const stepRegistry: Record = { 'your-circumstances': yourCircumstances, 'exceptional-hardship': exceptionalHardship, 'income-and-expenditure': incomeAndExpenditure, - 'what-regular-income-do-you-receive': whatRegularIncomeDoYouReceive, - 'have-you-applied-for-universal-credit': haveYouAppliedForUniversalCredit, + 'regular-income': regularIncome, + 'universal-credit': universalCredit, 'priority-debts': priorityDebts, 'priority-debt-details': priorityDebtDetails, - 'what-other-regular-expenses-do-you-have': whatOtherRegularExpensesDoYouHave, + 'regular-expenses': regularExpenses, }; diff --git a/src/main/steps/respond-to-claim/universal-credit/index.ts b/src/main/steps/respond-to-claim/universal-credit/index.ts index 78f9a6dac..9f80608e0 100644 --- a/src/main/steps/respond-to-claim/universal-credit/index.ts +++ b/src/main/steps/respond-to-claim/universal-credit/index.ts @@ -1,13 +1,114 @@ +import type { Request } from 'express'; + +import type { PossessionClaimResponse } from '../../../interfaces/ccdCase.interface'; import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; +import { buildCcdCaseForPossessionClaimResponse } from '../../utils/populateResponseToClaimPayloadmap'; +import { fromYesNoEnum, toYesNoEnum } from '../../utils/yesNoEnum'; import { flowConfig } from '../flow.config'; import { createFormStep } from '@modules/steps'; export const step: StepDefinition = createFormStep({ - stepName: 'have-you-applied-for-universal-credit', + stepName: 'universal-credit', journeyFolder: 'respondToClaim', stepDir: __dirname, flowConfig, - fields: [], customTemplate: `${__dirname}/universalCredit.njk`, + + getInitialFormData: (req: Request) => { + const caseData = req.res?.locals?.validatedCase?.data; + const hc = caseData?.possessionClaimResponse?.defendantResponses?.householdCircumstances; + + if (!hc) { + return {}; + } + + const formData: Record = {}; + + // Convert YES/NO to yes/no + if (hc.universalCredit) { + formData.appliedForUniversalCredit = fromYesNoEnum(hc.universalCredit); + } + + // Convert date from YYYY-MM-DD to day/month/year fields + if (hc.ucApplicationDate) { + const dateStr = hc.ucApplicationDate as string; + const [year, month, day] = dateStr.split('-'); + formData['applicationDate-day'] = day; + formData['applicationDate-month'] = month; + formData['applicationDate-year'] = year; + } + + return formData; + }, + + beforeRedirect: async (req: Request) => { + const appliedForUC = req.body?.appliedForUniversalCredit as 'yes' | 'no' | undefined; + + if (!appliedForUC) { + return; + } + + const householdCircumstances: Record = { + universalCredit: toYesNoEnum(appliedForUC), + }; + + // Only save date if they answered "yes" + if (appliedForUC === 'yes') { + const day = req.body?.['applicationDate-day'] as string | undefined; + const month = req.body?.['applicationDate-month'] as string | undefined; + const year = req.body?.['applicationDate-year'] as string | undefined; + + if (day && month && year) { + // Store as ISO date string (YYYY-MM-DD) + householdCircumstances.ucApplicationDate = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; + } + } + + const possessionClaimResponse: PossessionClaimResponse = { + defendantResponses: { + householdCircumstances, + }, + }; + await buildCcdCaseForPossessionClaimResponse(req, possessionClaimResponse); + }, + + translationKeys: { + caption: 'caption', + pageTitle: 'pageTitle', + }, + + fields: [ + { + name: 'appliedForUniversalCredit', + type: 'radio', + required: true, + errorMessage: 'errors.appliedForUniversalCredit.required', + translationKey: { label: 'pageTitle' }, + legendClasses: 'govuk-fieldset__legend--l', + legendIsPageHeading: true, + options: [ + { + value: 'yes', + translationKey: 'options.yes', + subFields: { + applicationDate: { + name: 'applicationDate', + type: 'date', + required: true, + noFutureDate: true, + translationKey: { + label: 'subFields.applicationDate.label', + hint: 'subFields.applicationDate.hint', + }, + }, + }, + }, + { + value: 'no', + translationKey: 'options.no', + }, + ], + }, + ], }); diff --git a/src/main/steps/utils/currencyConversion.ts b/src/main/steps/utils/currencyConversion.ts new file mode 100644 index 000000000..0959b81d8 --- /dev/null +++ b/src/main/steps/utils/currencyConversion.ts @@ -0,0 +1,56 @@ +/** + * Utility functions for converting between pounds (frontend) and pence (backend). + * Backend stores currency amounts as integers in pence to avoid floating point issues. + */ + +/** + * Converts pence (backend) to pounds (frontend display) + * @param pence - Amount in pence as string or number + * @returns Amount in pounds with 2 decimal places, or undefined if invalid + * @example + * penceToPounds('14850') // returns '148.50' + * penceToPounds(14850) // returns '148.50' + * penceToPounds(null) // returns undefined + */ +export function penceToPounds(pence: string | number | undefined): string | undefined { + if (pence === undefined || pence === null || pence === '') { + return undefined; + } + + const penceValue = typeof pence === 'string' ? parseFloat(pence) : pence; + + if (Number.isNaN(penceValue)) { + return undefined; + } + + const pounds = penceValue / 100; + return pounds.toFixed(2); +} + +/** + * Converts pounds (frontend) to pence (backend storage) + * Handles comma-separated input and rounds to nearest pence + * @param pounds - Amount in pounds as string (may contain commas) + * @returns Amount in pence as string, or undefined if invalid + * @example + * poundsToPence('148.50') // returns '14850' + * poundsToPence('1,234.56') // returns '123456' + * poundsToPence('invalid') // returns undefined + */ +export function poundsToPence(pounds: string | undefined): string | undefined { + if (!pounds) { + return undefined; + } + + // Remove commas from formatted input + const normalized = pounds.replace(/,/g, ''); + const poundsValue = parseFloat(normalized); + + if (Number.isNaN(poundsValue)) { + return undefined; + } + + // Round to nearest pence (avoids floating point issues) + const pence = Math.round(poundsValue * 100); + return String(pence); +} diff --git a/src/main/steps/utils/index.ts b/src/main/steps/utils/index.ts index b9842c158..2541ea088 100644 --- a/src/main/steps/utils/index.ts +++ b/src/main/steps/utils/index.ts @@ -6,3 +6,5 @@ export { isNoticeServed } from './isNoticeServed'; export { isTenancyStartDateKnown } from './isTenancyStartDateKnown'; export { getPreviousPageForArrears } from './journeyHelpers'; export { formatDatePartsToISODate } from './dateUtils'; +export { toYesNoEnum, fromYesNoEnum } from './yesNoEnum'; +export { penceToPounds, poundsToPence } from './currencyConversion'; diff --git a/src/main/steps/utils/yesNoEnum.ts b/src/main/steps/utils/yesNoEnum.ts new file mode 100644 index 000000000..d2c300b36 --- /dev/null +++ b/src/main/steps/utils/yesNoEnum.ts @@ -0,0 +1,44 @@ +/** + * Utility functions for converting between frontend yes/no values and backend YES/NO enums. + * Used for CCD API integration where boolean choices are represented as enum strings. + */ + +import type { YesNoValue } from '../../interfaces/ccdCase.interface'; + +/** + * Converts frontend 'yes'/'no' string to backend CCD enum 'YES'/'NO' + * Case-insensitive conversion to handle any casing from user input + * @param value - Frontend radio button value ('yes' or 'no') + * @returns CCD enum value ('YES' or 'NO') + * @example + * toYesNoEnum('yes') // returns 'YES' + * toYesNoEnum('Yes') // returns 'YES' + * toYesNoEnum('no') // returns 'NO' + * toYesNoEnum('NO') // returns 'NO' + */ +export function toYesNoEnum(value: 'yes' | 'no'): YesNoValue { + return value.toLowerCase() === 'yes' ? 'YES' : 'NO'; +} + +/** + * Converts backend CCD enum 'YES'/'NO' to frontend 'yes'/'no' string + * @param value - CCD enum value ('YES' or 'NO') + * @returns Frontend radio button value ('yes' or 'no'), or undefined if value is null/invalid + * @example + * fromYesNoEnum('YES') // returns 'yes' + * fromYesNoEnum('NO') // returns 'no' + * fromYesNoEnum(null) // returns undefined + */ +export function fromYesNoEnum(value: YesNoValue | string | undefined): 'yes' | 'no' | undefined { + if (!value) { + return undefined; + } + const upperValue = value.toUpperCase(); + if (upperValue === 'YES') { + return 'yes'; + } + if (upperValue === 'NO') { + return 'no'; + } + return undefined; +} diff --git a/src/test/unit/steps/utils/currencyConversion.test.ts b/src/test/unit/steps/utils/currencyConversion.test.ts new file mode 100644 index 000000000..2ae278d92 --- /dev/null +++ b/src/test/unit/steps/utils/currencyConversion.test.ts @@ -0,0 +1,130 @@ +import { penceToPounds, poundsToPence } from '../../../../main/steps/utils/currencyConversion'; + +describe('currencyConversion', () => { + describe('penceToPounds', () => { + it('should convert pence string to pounds with 2 decimal places', () => { + expect(penceToPounds('14850')).toBe('148.50'); + expect(penceToPounds('100')).toBe('1.00'); + expect(penceToPounds('50')).toBe('0.50'); + expect(penceToPounds('5')).toBe('0.05'); + expect(penceToPounds('0')).toBe('0.00'); + }); + + it('should convert pence number to pounds with 2 decimal places', () => { + expect(penceToPounds(14850)).toBe('148.50'); + expect(penceToPounds(100)).toBe('1.00'); + expect(penceToPounds(50)).toBe('0.50'); + expect(penceToPounds(5)).toBe('0.05'); + expect(penceToPounds(0)).toBe('0.00'); + }); + + it('should handle large amounts', () => { + expect(penceToPounds('100000000')).toBe('1000000.00'); // £1 million + expect(penceToPounds(100000000)).toBe('1000000.00'); + }); + + it('should return undefined for null', () => { + expect(penceToPounds(null as unknown as string)).toBeUndefined(); + }); + + it('should return undefined for undefined', () => { + expect(penceToPounds(undefined)).toBeUndefined(); + }); + + it('should return undefined for empty string', () => { + expect(penceToPounds('')).toBeUndefined(); + }); + + it('should return undefined for invalid string', () => { + expect(penceToPounds('invalid')).toBeUndefined(); + expect(penceToPounds('abc123')).toBeUndefined(); + expect(penceToPounds('£100')).toBeUndefined(); + }); + + it('should handle negative values', () => { + expect(penceToPounds('-14850')).toBe('-148.50'); + expect(penceToPounds(-14850)).toBe('-148.50'); + }); + + it('should handle decimal pence values', () => { + expect(penceToPounds('14850.50')).toBe('148.50'); // 148.505 with toFixed(2) + expect(penceToPounds(14850.5)).toBe('148.50'); + }); + }); + + describe('poundsToPence', () => { + it('should convert pounds string to pence', () => { + expect(poundsToPence('148.50')).toBe('14850'); + expect(poundsToPence('1.00')).toBe('100'); + expect(poundsToPence('0.50')).toBe('50'); + expect(poundsToPence('0.05')).toBe('5'); + expect(poundsToPence('0.00')).toBe('0'); + }); + + it('should handle comma-separated values', () => { + expect(poundsToPence('1,234.56')).toBe('123456'); + expect(poundsToPence('10,000.00')).toBe('1000000'); + expect(poundsToPence('1,000,000.00')).toBe('100000000'); + }); + + it('should round to nearest pence (avoids floating point issues)', () => { + expect(poundsToPence('148.505')).toBe('14851'); // Rounds up + expect(poundsToPence('148.504')).toBe('14850'); // Rounds down + expect(poundsToPence('0.555')).toBe('56'); // Rounds up + }); + + it('should handle whole numbers (no decimal)', () => { + expect(poundsToPence('148')).toBe('14800'); + expect(poundsToPence('1')).toBe('100'); + expect(poundsToPence('0')).toBe('0'); + }); + + it('should handle single decimal place', () => { + expect(poundsToPence('148.5')).toBe('14850'); + expect(poundsToPence('1.5')).toBe('150'); + }); + + it('should return undefined for undefined', () => { + expect(poundsToPence(undefined)).toBeUndefined(); + }); + + it('should return undefined for empty string', () => { + expect(poundsToPence('')).toBeUndefined(); + }); + + it('should return undefined for invalid string', () => { + expect(poundsToPence('invalid')).toBeUndefined(); + expect(poundsToPence('abc123')).toBeUndefined(); + expect(poundsToPence('£100')).toBeUndefined(); + }); + + it('should handle negative values', () => { + expect(poundsToPence('-148.50')).toBe('-14850'); + expect(poundsToPence('-1.00')).toBe('-100'); + }); + + it('should handle whitespace by normalizing', () => { + expect(poundsToPence(' 148.50 ')).toBe('14850'); + expect(poundsToPence(' 1.00 ')).toBe('100'); + }); + }); + + describe('penceToPounds and poundsToPence round-trip', () => { + it('should maintain value integrity in round-trip conversion', () => { + const testValues = ['14850', '100', '50', '0', '123456']; + + testValues.forEach(pence => { + const pounds = penceToPounds(pence); + const backToPence = poundsToPence(pounds as string); + expect(backToPence).toBe(pence); + }); + }); + + it('should handle round-trip with comma-formatted input', () => { + const poundsWithCommas = '1,234.56'; + const pence = poundsToPence(poundsWithCommas); + const backToPounds = penceToPounds(pence as string); + expect(backToPounds).toBe('1234.56'); // Commas removed but value preserved + }); + }); +}); From d6866fd99e5fb2609d609063e84599b809353008 Mon Sep 17 00:00:00 2001 From: arun Date: Tue, 24 Mar 2026 08:51:16 +0000 Subject: [PATCH 02/20] HDPI-3764: Add UC routing tests, fix moneyFromElsewhere hint text, and make getInitialFormData case-insensitive --- .../cy/respondToClaim/regularIncome.json | 15 +- .../en/respondToClaim/regularIncome.json | 15 +- .../respond-to-claim/regular-income/index.ts | 89 ++++---- .../regular-income/regularIncome.njk | 25 --- .../flow-config-universal-credit.test.ts | 211 ++++++++++++++++++ 5 files changed, 282 insertions(+), 73 deletions(-) delete mode 100644 src/main/steps/respond-to-claim/regular-income/regularIncome.njk create mode 100644 src/test/unit/steps/respond-to-claim/flow-config-universal-credit.test.ts diff --git a/src/main/assets/locales/cy/respondToClaim/regularIncome.json b/src/main/assets/locales/cy/respondToClaim/regularIncome.json index 06c408085..6bda8e65d 100644 --- a/src/main/assets/locales/cy/respondToClaim/regularIncome.json +++ b/src/main/assets/locales/cy/respondToClaim/regularIncome.json @@ -12,7 +12,8 @@ "subFields": { "amount": "cyTotal amount received", "frequency": "cyReceived every:", - "moneyFromElsewhereDetails": "cyGive details about the other sources of income and how much you usually receive" + "moneyFromElsewhereDetailsLabel": "cyGive details", + "moneyFromElsewhereDetailsHint": "cyGive details about the other sources of income and how much you usually receive" }, "frequency": { "week": "cyWeek", @@ -20,25 +21,29 @@ }, "errors": { "incomeFromJobsAmount": { - "required": "cyEnter the total amount you receive from all jobs you do" + "required": "cyEnter the total amount you receive from all jobs you do", + "largeAmount": "cyThe total amount you receive from all jobs you do each week or month must be less than £1 billion" }, "incomeFromJobsFrequency": { "required": "cySelect how frequently you receive income from all jobs you do" }, "pensionAmount": { - "required": "cyEnter the total amount you receive from a state or private pension" + "required": "cyEnter the total amount you receive from a state or private pension", + "largeAmount": "cyThe total amount you receive from pension (state and private) each week or month must be less than £1 billion" }, "pensionFrequency": { "required": "cySelect how frequently you receive income from a state or private pension" }, "universalCreditAmount": { - "required": "cyEnter the total amount you receive from Universal Credit" + "required": "cyEnter the total amount you receive from Universal Credit", + "largeAmount": "cyThe total amount you receive from Universal Credit each week or month must be less than £1 billion" }, "universalCreditFrequency": { "required": "cySelect how frequently you receive Universal Credit" }, "otherBenefitsAmount": { - "required": "cyEnter the total amount you receive from other benefits and credits" + "required": "cyEnter the total amount you receive from other benefits and credits", + "largeAmount": "cyThe total amount you receive from other benefits and credit each week or month must be less than £1 billion" }, "otherBenefitsFrequency": { "required": "cySelect how frequently you receive income from other benefits and credits" diff --git a/src/main/assets/locales/en/respondToClaim/regularIncome.json b/src/main/assets/locales/en/respondToClaim/regularIncome.json index 6d211420e..473b8b8a4 100644 --- a/src/main/assets/locales/en/respondToClaim/regularIncome.json +++ b/src/main/assets/locales/en/respondToClaim/regularIncome.json @@ -12,7 +12,8 @@ "subFields": { "amount": "Total amount received", "frequency": "Received every:", - "moneyFromElsewhereDetails": "Give details about the other sources of income and how much you usually receive" + "moneyFromElsewhereDetailsLabel": "Give details", + "moneyFromElsewhereDetailsHint": "Give details about the other sources of income and how much you usually receive" }, "frequency": { "week": "Week", @@ -20,25 +21,29 @@ }, "errors": { "incomeFromJobsAmount": { - "required": "Enter the total amount you receive from all jobs you do" + "required": "Enter the total amount you receive from all jobs you do", + "largeAmount": "The total amount you receive from all jobs you do each week or month must be less than £1 billion" }, "incomeFromJobsFrequency": { "required": "Select how frequently you receive income from all jobs you do" }, "pensionAmount": { - "required": "Enter the total amount you receive from a state or private pension" + "required": "Enter the total amount you receive from a state or private pension", + "largeAmount": "The total amount you receive from pension (state and private) each week or month must be less than £1 billion" }, "pensionFrequency": { "required": "Select how frequently you receive income from a state or private pension" }, "universalCreditAmount": { - "required": "Enter the total amount you receive from Universal Credit" + "required": "Enter the total amount you receive from Universal Credit", + "largeAmount": "The total amount you receive from Universal Credit each week or month must be less than £1 billion" }, "universalCreditFrequency": { "required": "Select how frequently you receive Universal Credit" }, "otherBenefitsAmount": { - "required": "Enter the total amount you receive from other benefits and credits" + "required": "Enter the total amount you receive from other benefits and credits", + "largeAmount": "The total amount you receive from other benefits and credit each week or month must be less than £1 billion" }, "otherBenefitsFrequency": { "required": "Select how frequently you receive income from other benefits and credits" diff --git a/src/main/steps/respond-to-claim/regular-income/index.ts b/src/main/steps/respond-to-claim/regular-income/index.ts index f827b3f5e..78c94b1eb 100644 --- a/src/main/steps/respond-to-claim/regular-income/index.ts +++ b/src/main/steps/respond-to-claim/regular-income/index.ts @@ -12,42 +12,49 @@ import { createFormStep } from '@modules/steps'; const MAX_INCOME_AMOUNT = 1_000_000_000; // £1 billion maximum const AMOUNT_FORMAT_REGEX = /^\d{1,10}\.\d{2}$/; // Up to 10 digits, exactly 2 decimal places -// Amount validator helper (copied from rent-arrears-dispute pattern) -const validateAmount = (value: unknown): boolean | string => { - if (typeof value !== 'string') { - return true; - } +const createAmountValidator = + (largeAmountErrorKey: string) => + (value: unknown): boolean | string => { + if (typeof value !== 'string') { + return true; + } - const trimmed = value.trim(); - if (!trimmed) { - return true; - } // Let required validation handle empty values + const trimmed = value.trim(); + if (!trimmed) { + return true; + } // Let required validation handle empty values - const normalized = trimmed.replace(/,/g, ''); - const numericValue = parseFloat(normalized); + const normalized = trimmed.replace(/,/g, ''); + const numericValue = parseFloat(normalized); - if (!Number.isNaN(numericValue)) { - if (numericValue < 0) { - return 'errors.amount.negative'; + if (!Number.isNaN(numericValue)) { + if (numericValue < 0) { + return 'errors.amount.negative'; + } + // AC: £1bn or more should error + if (numericValue >= MAX_INCOME_AMOUNT) { + return largeAmountErrorKey; + } } - if (numericValue > MAX_INCOME_AMOUNT) { - return 'errors.amount.tooLarge'; + + if (!AMOUNT_FORMAT_REGEX.test(normalized)) { + return 'errors.amount.invalidFormat'; } - } - if (!AMOUNT_FORMAT_REGEX.test(normalized)) { - return 'errors.amount.invalidFormat'; - } + return true; + }; - return true; -}; +const validateIncomeFromJobsAmount = createAmountValidator('errors.incomeFromJobsAmount.largeAmount'); +const validatePensionAmount = createAmountValidator('errors.pensionAmount.largeAmount'); +const validateUniversalCreditAmount = createAmountValidator('errors.universalCreditAmount.largeAmount'); +const validateOtherBenefitsAmount = createAmountValidator('errors.otherBenefitsAmount.largeAmount'); export const step: StepDefinition = createFormStep({ stepName: 'regular-income', journeyFolder: 'respondToClaim', stepDir: __dirname, flowConfig, - customTemplate: `${__dirname}/regularIncome.njk`, + showCancelButton: false, getInitialFormData: (req: Request) => { const caseData = req.res?.locals?.validatedCase?.data; @@ -60,8 +67,8 @@ export const step: StepDefinition = createFormStep({ const formData: Record = {}; const selectedIncome: string[] = []; - // Income from jobs - if (hc.incomeFromJobs === 'YES') { + // Income from jobs - Case-insensitive check to handle backend variations (YES/Yes/yes) + if (hc.incomeFromJobs && (hc.incomeFromJobs as string).toUpperCase() === 'YES') { selectedIncome.push('incomeFromJobs'); if (hc.incomeFromJobsAmount) { formData['regularIncome.incomeFromJobsAmount'] = penceToPounds(hc.incomeFromJobsAmount as string); @@ -71,8 +78,8 @@ export const step: StepDefinition = createFormStep({ } } - // Pension - if (hc.pension === 'YES') { + // Pension - Case-insensitive check to handle backend variations (YES/Yes/yes) + if (hc.pension && (hc.pension as string).toUpperCase() === 'YES') { selectedIncome.push('pension'); if (hc.pensionAmount) { formData['regularIncome.pensionAmount'] = penceToPounds(hc.pensionAmount as string); @@ -82,8 +89,9 @@ export const step: StepDefinition = createFormStep({ } } - // Universal Credit (Note: amount/frequency may not exist - see BA clarification) - if (hc.universalCreditIncome === 'YES') { + // Universal Credit - Case-insensitive check to handle backend variations (YES/Yes/yes) + // Note: amount/frequency may not exist - see BA clarification + if (hc.universalCreditIncome && (hc.universalCreditIncome as string).toUpperCase() === 'YES') { selectedIncome.push('universalCredit'); // UC amount/frequency commented out pending BA clarification // if (hc.universalCreditAmount) { @@ -96,8 +104,8 @@ export const step: StepDefinition = createFormStep({ // } } - // Other benefits - if (hc.otherBenefits === 'YES') { + // Other benefits - Case-insensitive check to handle backend variations (YES/Yes/yes) + if (hc.otherBenefits && (hc.otherBenefits as string).toUpperCase() === 'YES') { selectedIncome.push('otherBenefits'); if (hc.otherBenefitsAmount) { formData['regularIncome.otherBenefitsAmount'] = penceToPounds(hc.otherBenefitsAmount as string); @@ -107,8 +115,8 @@ export const step: StepDefinition = createFormStep({ } } - // Money from elsewhere - if (hc.moneyFromElsewhere === 'YES') { + // Money from elsewhere - Case-insensitive check to handle backend variations (YES/Yes/yes) + if (hc.moneyFromElsewhere && (hc.moneyFromElsewhere as string).toUpperCase() === 'YES') { selectedIncome.push('moneyFromElsewhere'); if (hc.moneyFromElsewhereDetails) { formData['regularIncome.moneyFromElsewhereDetails'] = hc.moneyFromElsewhereDetails; @@ -207,6 +215,7 @@ export const step: StepDefinition = createFormStep({ translationKeys: { caption: 'caption', + heading: 'pageTitle', pageTitle: 'pageTitle', hintText: 'hintText', }, @@ -216,7 +225,9 @@ export const step: StepDefinition = createFormStep({ name: 'regularIncome', type: 'checkbox', required: false, // Page is optional - can select zero checkboxes + legendClasses: 'govuk-visually-hidden', translationKey: { + label: 'pageTitle', hint: 'hintText', }, options: [ @@ -241,7 +252,7 @@ export const step: StepDefinition = createFormStep({ inputmode: 'decimal', spellcheck: false, }, - validator: validateAmount, + validator: validateIncomeFromJobsAmount, }, incomeFromJobsFrequency: { name: 'incomeFromJobsFrequency', @@ -279,7 +290,7 @@ export const step: StepDefinition = createFormStep({ inputmode: 'decimal', spellcheck: false, }, - validator: validateAmount, + validator: validatePensionAmount, }, pensionFrequency: { name: 'pensionFrequency', @@ -317,7 +328,7 @@ export const step: StepDefinition = createFormStep({ inputmode: 'decimal', spellcheck: false, }, - validator: validateAmount, + validator: validateUniversalCreditAmount, }, universalCreditFrequency: { name: 'universalCreditFrequency', @@ -355,7 +366,7 @@ export const step: StepDefinition = createFormStep({ inputmode: 'decimal', spellcheck: false, }, - validator: validateAmount, + validator: validateOtherBenefitsAmount, }, otherBenefitsFrequency: { name: 'otherBenefitsFrequency', @@ -383,8 +394,10 @@ export const step: StepDefinition = createFormStep({ maxLength: 500, required: true, errorMessage: 'errors.moneyFromElsewhereDetails.required', + labelClasses: 'govuk-visually-hidden', translationKey: { - label: 'subFields.moneyFromElsewhereDetails', + label: 'subFields.moneyFromElsewhereDetailsLabel', + hint: 'subFields.moneyFromElsewhereDetailsHint', }, }, }, diff --git a/src/main/steps/respond-to-claim/regular-income/regularIncome.njk b/src/main/steps/respond-to-claim/regular-income/regularIncome.njk deleted file mode 100644 index f8b9a709c..000000000 --- a/src/main/steps/respond-to-claim/regular-income/regularIncome.njk +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "stepsTemplate.njk" %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "macros/csrf.njk" import csrfProtection %} -{% block mainContent %} - {% if errorSummary %} - {{ govukErrorSummary(errorSummary) }} - {% endif %} -

What regular income do you receive? (placeholder)

-

This is a placeholder for What Regular Income Do You Receive step.

-
-
- {{ govukButton({ - text: continue, - attributes: { type: 'submit', name: 'action', value: 'continue' } - }) }} - {{ govukButton({ - text: saveForLater, - classes: 'govuk-button--secondary', - attributes: { type: 'submit', name: 'action', value: 'saveForLater' } - }) }} -
- {{ csrfProtection(csrfToken) }} -
-{% endblock %} diff --git a/src/test/unit/steps/respond-to-claim/flow-config-universal-credit.test.ts b/src/test/unit/steps/respond-to-claim/flow-config-universal-credit.test.ts new file mode 100644 index 000000000..633b42548 --- /dev/null +++ b/src/test/unit/steps/respond-to-claim/flow-config-universal-credit.test.ts @@ -0,0 +1,211 @@ +import type { Request } from 'express'; + +import { flowConfig } from '../../../../main/steps/respond-to-claim/flow.config'; + +describe('respond-to-claim flow config - Universal Credit conditional routing', () => { + const createMockRequest = (): Request => + ({ + session: { formData: {} }, + }) as Request; + + describe('regular-income step conditional routing', () => { + const regularIncomeStep = flowConfig.steps['regular-income']; + + it('routes to priority-debts when universalCredit checkbox is selected (single value)', async () => { + const req = createMockRequest(); + const currentStepData = { + regularIncome: 'universalCredit', + }; + + const route = regularIncomeStep.routes?.[0]; + expect(route).toBeDefined(); + + const shouldRoute = await route!.condition!(req, {}, currentStepData); + expect(shouldRoute).toBe(true); + expect(route!.nextStep).toBe('priority-debts'); + }); + + it('routes to priority-debts when universalCredit is in array (multiple selections)', async () => { + const req = createMockRequest(); + const currentStepData = { + regularIncome: ['incomeFromJobs', 'universalCredit', 'pension'], + }; + + const route = regularIncomeStep.routes?.[0]; + const shouldRoute = await route!.condition!(req, {}, currentStepData); + expect(shouldRoute).toBe(true); + }); + + it('does not route to priority-debts when universalCredit is NOT selected', async () => { + const req = createMockRequest(); + const currentStepData = { + regularIncome: ['incomeFromJobs', 'pension'], + }; + + const route = regularIncomeStep.routes?.[0]; + const shouldRoute = await route!.condition!(req, {}, currentStepData); + expect(shouldRoute).toBe(false); + }); + + it('does not route to priority-debts when regularIncome is empty', async () => { + const req = createMockRequest(); + const currentStepData = { + regularIncome: [], + }; + + const route = regularIncomeStep.routes?.[0]; + const shouldRoute = await route!.condition!(req, {}, currentStepData); + expect(shouldRoute).toBe(false); + }); + + it('does not route to priority-debts when regularIncome is undefined', async () => { + const req = createMockRequest(); + const currentStepData = {}; + + const route = regularIncomeStep.routes?.[0]; + const shouldRoute = await route!.condition!(req, {}, currentStepData); + expect(shouldRoute).toBe(false); + }); + + it('uses persisted formData when currentStepData is empty (back button scenario)', async () => { + const req = createMockRequest(); + const persistedFormData = { + 'regular-income': { + regularIncome: ['universalCredit', 'pension'], + }, + }; + const currentStepData = {}; + + const route = regularIncomeStep.routes?.[0]; + const shouldRoute = await route!.condition!(req, persistedFormData, currentStepData); + expect(shouldRoute).toBe(true); + }); + + it('uses universal-credit as defaultNext when condition does not match', () => { + expect(regularIncomeStep.defaultNext).toBe('universal-credit'); + }); + }); + + describe('priority-debts step previousStep function', () => { + const priorityDebtsStep = flowConfig.steps['priority-debts']; + + it('returns regular-income when user selected universalCredit', async () => { + const req = createMockRequest(); + const formData = { + 'regular-income': { + regularIncome: ['incomeFromJobs', 'universalCredit'], + }, + }; + + const previousStepFn = priorityDebtsStep.previousStep; + expect(typeof previousStepFn).toBe('function'); + + const previousStep = await ( + previousStepFn as (req: Request, formData: Record) => Promise + )(req, formData); + expect(previousStep).toBe('regular-income'); + }); + + it('returns universal-credit when user did NOT select universalCredit', async () => { + const req = createMockRequest(); + const formData = { + 'regular-income': { + regularIncome: ['incomeFromJobs', 'pension'], + }, + }; + + const previousStepFn = priorityDebtsStep.previousStep; + const previousStep = await ( + previousStepFn as (req: Request, formData: Record) => Promise + )(req, formData); + expect(previousStep).toBe('universal-credit'); + }); + + it('returns universal-credit when regular-income data is missing', async () => { + const req = createMockRequest(); + const formData = {}; + + const previousStepFn = priorityDebtsStep.previousStep; + const previousStep = await ( + previousStepFn as (req: Request, formData: Record) => Promise + )(req, formData); + expect(previousStep).toBe('universal-credit'); + }); + + it('returns universal-credit when regularIncome is undefined', async () => { + const req = createMockRequest(); + const formData = { + 'regular-income': {}, + }; + + const previousStepFn = priorityDebtsStep.previousStep; + const previousStep = await ( + previousStepFn as (req: Request, formData: Record) => Promise + )(req, formData); + expect(previousStep).toBe('universal-credit'); + }); + + it('handles regularIncome as single string value', async () => { + const req = createMockRequest(); + const formData = { + 'regular-income': { + regularIncome: 'universalCredit', + }, + }; + + const previousStepFn = priorityDebtsStep.previousStep; + const previousStep = await ( + previousStepFn as (req: Request, formData: Record) => Promise + )(req, formData); + expect(previousStep).toBe('regular-income'); + }); + }); + + describe('AC13: Skip universal-credit step when UC selected', () => { + it('full journey path: regular-income -> priority-debts (UC selected)', async () => { + const req = createMockRequest(); + const currentStepData = { + regularIncome: ['universalCredit', 'pension'], + }; + + // Step 1: User submits regular-income with UC selected + const regularIncomeRoute = flowConfig.steps['regular-income'].routes?.[0]; + const shouldSkipUC = await regularIncomeRoute!.condition!(req, {}, currentStepData); + expect(shouldSkipUC).toBe(true); + expect(regularIncomeRoute!.nextStep).toBe('priority-debts'); + + // Step 2: User clicks back from priority-debts + const fullFormData = { 'regular-income': { regularIncome: ['universalCredit', 'pension'] } }; + const priorityDebtsPreviousStep = await ( + flowConfig.steps['priority-debts'].previousStep as ( + req: Request, + formData: Record + ) => Promise + )(req, fullFormData); + expect(priorityDebtsPreviousStep).toBe('regular-income'); + }); + + it('full journey path: regular-income -> universal-credit -> priority-debts (UC NOT selected)', async () => { + const req = createMockRequest(); + const currentStepData = { + regularIncome: ['incomeFromJobs', 'pension'], + }; + + // Step 1: User submits regular-income without UC + const regularIncomeRoute = flowConfig.steps['regular-income'].routes?.[0]; + const shouldSkipUC = await regularIncomeRoute!.condition!(req, {}, currentStepData); + expect(shouldSkipUC).toBe(false); + expect(flowConfig.steps['regular-income'].defaultNext).toBe('universal-credit'); + + // Step 2: User clicks back from priority-debts + const fullFormData = { 'regular-income': { regularIncome: ['incomeFromJobs', 'pension'] } }; + const priorityDebtsPreviousStep = await ( + flowConfig.steps['priority-debts'].previousStep as ( + req: Request, + formData: Record + ) => Promise + )(req, fullFormData); + expect(priorityDebtsPreviousStep).toBe('universal-credit'); + }); + }); +}); From 9ba67c913d8bfc3d950667ffeaa7b951ce090e02 Mon Sep 17 00:00:00 2001 From: arun Date: Tue, 24 Mar 2026 14:20:24 +0000 Subject: [PATCH 03/20] HDPI-3764: Add UC conditional routing, prefix/suffix field support, and HouseholdCircumstances CCD interface --- src/main/interfaces/ccdCase.interface.ts | 22 ++++++++++++ .../interfaces/formFieldConfig.interface.ts | 2 ++ .../steps/formBuilder/componentBuilders.ts | 6 ++++ .../steps/respond-to-claim/flow.config.ts | 35 ++++++++++++++++++- .../income-and-expenditure/index.ts | 4 +-- .../universal-credit/index.ts | 2 +- src/main/views/components/subFields.njk | 6 ++++ .../formBuilder/fieldTranslation.test.ts | 28 +++++++++++++++ 8 files changed, 101 insertions(+), 4 deletions(-) diff --git a/src/main/interfaces/ccdCase.interface.ts b/src/main/interfaces/ccdCase.interface.ts index 7ed005ab4..e86aca2f5 100644 --- a/src/main/interfaces/ccdCase.interface.ts +++ b/src/main/interfaces/ccdCase.interface.ts @@ -36,6 +36,27 @@ export interface Address { Country?: string; } +export interface HouseholdCircumstances { + dependantChildren?: YesNoValue; + shareIncomeExpenseDetails?: YesNoValue; + incomeFromJobs?: YesNoValue; + incomeFromJobsAmount?: string; + incomeFromJobsFrequency?: string; + pension?: YesNoValue; + pensionAmount?: string; + pensionFrequency?: string; + universalCreditIncome?: YesNoValue; + universalCreditIncomeAmount?: string; + universalCreditIncomeFrequency?: string; + universalCredit?: YesNoValue; + ucApplicationDate?: string; + otherBenefits?: YesNoValue; + otherBenefitsAmount?: string; + otherBenefitsFrequency?: string; + moneyFromElsewhere?: YesNoValue; + moneyFromElsewhereDetails?: string; +} + export interface PossessionClaimResponse { defendantContactDetails?: { party?: { @@ -57,6 +78,7 @@ export interface PossessionClaimResponse { defendantNameConfirmation?: string; dateOfBirth?: string; landlordRegistered?: YesNoNotSureValue; + householdCircumstances?: HouseholdCircumstances; }; } diff --git a/src/main/interfaces/formFieldConfig.interface.ts b/src/main/interfaces/formFieldConfig.interface.ts index 401e39925..ac9004677 100644 --- a/src/main/interfaces/formFieldConfig.interface.ts +++ b/src/main/interfaces/formFieldConfig.interface.ts @@ -42,6 +42,8 @@ export interface FormFieldConfig { classes?: string; attributes?: Record; legendClasses?: string; + prefix?: { text: string }; + suffix?: { text: string }; // Pre-built component config for Nunjucks template rendering component?: Record; componentType?: ComponentType; diff --git a/src/main/modules/steps/formBuilder/componentBuilders.ts b/src/main/modules/steps/formBuilder/componentBuilders.ts index d046deafb..546f364bc 100644 --- a/src/main/modules/steps/formBuilder/componentBuilders.ts +++ b/src/main/modules/steps/formBuilder/componentBuilders.ts @@ -55,6 +55,12 @@ export function buildComponentConfig( switch (field.type) { case 'text': { component.value = (fieldValue as string) || ''; + if (field.prefix) { + component.prefix = field.prefix; + } + if (field.suffix) { + component.suffix = field.suffix; + } componentType = 'input'; break; } diff --git a/src/main/steps/respond-to-claim/flow.config.ts b/src/main/steps/respond-to-claim/flow.config.ts index dc06a7330..38be794e6 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -372,6 +372,26 @@ export const flowConfig: JourneyFlowConfig = { }, 'regular-income': { previousStep: 'income-and-expenditure', + routes: [ + { + // AC13: If Universal Credit checkbox selected, skip universal-credit step + condition: async ( + _req: Request, + formData: Record, + currentStepData: Record + ): Promise => { + const persisted = formData?.['regular-income'] as Record | undefined; + const selectedIncome = (currentStepData.regularIncome ?? persisted?.regularIncome) as + | string + | string[] + | undefined; + const incomeArray = Array.isArray(selectedIncome) ? selectedIncome : selectedIncome ? [selectedIncome] : []; + + return incomeArray.includes('universalCredit'); + }, + nextStep: 'priority-debts', + }, + ], defaultNext: 'universal-credit', }, 'universal-credit': { @@ -379,7 +399,20 @@ export const flowConfig: JourneyFlowConfig = { defaultNext: 'priority-debts', }, 'priority-debts': { - previousStep: 'universal-credit', + previousStep: async (req: Request, formData: Record): Promise => { + // Check if user selected UC on regular-income page + const regularIncomeData = formData?.['regular-income'] as Record | undefined; + const selectedIncome = regularIncomeData?.regularIncome as string | string[] | undefined; + const incomeArray = Array.isArray(selectedIncome) ? selectedIncome : selectedIncome ? [selectedIncome] : []; + + // If UC was selected, user came directly from regular-income (skipped universal-credit) + if (incomeArray.includes('universalCredit')) { + return 'regular-income'; + } + + // Otherwise, user came from universal-credit step + return 'universal-credit'; + }, routes: [ { condition: async (req, formData) => formData.hasPriorityDebts === 'yes', diff --git a/src/main/steps/respond-to-claim/income-and-expenditure/index.ts b/src/main/steps/respond-to-claim/income-and-expenditure/index.ts index 012033edf..75b3579da 100644 --- a/src/main/steps/respond-to-claim/income-and-expenditure/index.ts +++ b/src/main/steps/respond-to-claim/income-and-expenditure/index.ts @@ -1,10 +1,10 @@ +import type { PossessionClaimResponse } from '../../../interfaces/ccdCase.interface'; import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; +import { buildCcdCaseForPossessionClaimResponse } from '../../utils/populateResponseToClaimPayloadmap'; import { fromYesNoEnum, toYesNoEnum } from '../../utils/yesNoEnum'; import { flowConfig } from '../flow.config'; import { createFormStep } from '@modules/steps'; -import type { PossessionClaimResponse } from '@services/pcsApi'; -import { buildCcdCaseForPossessionClaimResponse } from '@services/pcsApi'; export const step: StepDefinition = createFormStep({ stepName: 'income-and-expenditure', diff --git a/src/main/steps/respond-to-claim/universal-credit/index.ts b/src/main/steps/respond-to-claim/universal-credit/index.ts index 9f80608e0..ee411277a 100644 --- a/src/main/steps/respond-to-claim/universal-credit/index.ts +++ b/src/main/steps/respond-to-claim/universal-credit/index.ts @@ -86,7 +86,7 @@ export const step: StepDefinition = createFormStep({ errorMessage: 'errors.appliedForUniversalCredit.required', translationKey: { label: 'pageTitle' }, legendClasses: 'govuk-fieldset__legend--l', - legendIsPageHeading: true, + isPageHeading: true, options: [ { value: 'yes', diff --git a/src/main/views/components/subFields.njk b/src/main/views/components/subFields.njk index 1a5947866..b54ecb539 100644 --- a/src/main/views/components/subFields.njk +++ b/src/main/views/components/subFields.njk @@ -1,6 +1,8 @@ {% from "govuk/components/input/macro.njk" import govukInput %} {% from "govuk/components/textarea/macro.njk" import govukTextarea %} {% from "govuk/components/character-count/macro.njk" import govukCharacterCount %} +{% from "govuk/components/radios/macro.njk" import govukRadios %} +{% from "govuk/components/checkboxes/macro.njk" import govukCheckboxes %} {% from "govuk/components/date-input/macro.njk" import govukDateInput %} @@ -12,6 +14,10 @@ {{ govukTextarea(subField.component) }} {% elif subField.componentType == 'characterCount' %} {{ govukCharacterCount(subField.component) }} + {% elif subField.componentType == 'radios' %} + {{ govukRadios(subField.component) }} + {% elif subField.componentType == 'checkboxes' %} + {{ govukCheckboxes(subField.component) }} {% elif subField.componentType == 'dateInput' %} {{ govukDateInput(subField.component) }} {% elif subField.componentType == 'postcodeLookup' %} diff --git a/src/test/unit/modules/steps/formBuilder/fieldTranslation.test.ts b/src/test/unit/modules/steps/formBuilder/fieldTranslation.test.ts index b090c181d..24cd56d49 100644 --- a/src/test/unit/modules/steps/formBuilder/fieldTranslation.test.ts +++ b/src/test/unit/modules/steps/formBuilder/fieldTranslation.test.ts @@ -73,4 +73,32 @@ describe('translateFields', () => { expect(month.value).toBe(''); expect(year.value).toBe(''); }); + + it('passes prefix and suffix into input component config', () => { + const amountFields: FormFieldConfig[] = [ + { + name: 'amount', + type: 'text', + translationKey: { label: 'amountLabel' }, + prefix: { text: '£' }, + suffix: { text: 'per month' }, + }, + ]; + + const result = translateFields( + amountFields, + mockT as unknown as TFunction, + { amount: '10.00' }, + {}, + false, + '', + {}, + mockNunjucksEnv + ); + const field = result[0] as FormFieldConfig; + const component = field.component as { prefix?: { text: string }; suffix?: { text: string } } | undefined; + + expect(component?.prefix).toEqual({ text: '£' }); + expect(component?.suffix).toEqual({ text: 'per month' }); + }); }); From 34c61f8b97397f4551046cbf301bc1b1ee7387be Mon Sep 17 00:00:00 2001 From: arun Date: Tue, 24 Mar 2026 14:23:16 +0000 Subject: [PATCH 04/20] HDPI-3764: Remove local OIDC development code from modules/index.ts --- src/main/modules/index.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/main/modules/index.ts b/src/main/modules/index.ts index 6dc55692e..2004c5d33 100644 --- a/src/main/modules/index.ts +++ b/src/main/modules/index.ts @@ -1,24 +1,13 @@ -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; +export { Csrf } from './csrf'; // this is used to register the modules with the app in a certain order -export const modules = ['I18n', 'Nunjucks', 'Helmet', 'Session', 'S2S', 'OIDCModule', 'LaunchDarkly']; +export const modules = ['I18n', 'Nunjucks', 'Helmet', 'Session', 'S2S', 'OIDCModule', 'LaunchDarkly', 'Csrf']; From 3b08446191aa4a9c39028233c3d0e380d628a2ca Mon Sep 17 00:00:00 2001 From: arun Date: Tue, 24 Mar 2026 15:24:04 +0000 Subject: [PATCH 05/20] HDPI-3764: Remove unwanted steps - keep only income-and-expenditure and regular-income steps --- .../steps/respond-to-claim/flow.config.ts | 63 ----------- .../respond-to-claim/priority-debts/index.ts | 58 +--------- .../priority-debts/priorityDebts.njk | 44 ++++---- .../regular-expenses/index.ts | 24 +--- .../universal-credit/index.ts | 105 +----------------- 5 files changed, 27 insertions(+), 267 deletions(-) diff --git a/src/main/steps/respond-to-claim/flow.config.ts b/src/main/steps/respond-to-claim/flow.config.ts index 38be794e6..8c342f643 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -50,10 +50,6 @@ export const flowConfig: JourneyFlowConfig = { 'exceptional-hardship', 'income-and-expenditure', 'regular-income', - 'universal-credit', - 'priority-debts', - 'priority-debt-details', - 'regular-expenses', 'end-now', ], steps: { @@ -372,65 +368,6 @@ export const flowConfig: JourneyFlowConfig = { }, 'regular-income': { previousStep: 'income-and-expenditure', - routes: [ - { - // AC13: If Universal Credit checkbox selected, skip universal-credit step - condition: async ( - _req: Request, - formData: Record, - currentStepData: Record - ): Promise => { - const persisted = formData?.['regular-income'] as Record | undefined; - const selectedIncome = (currentStepData.regularIncome ?? persisted?.regularIncome) as - | string - | string[] - | undefined; - const incomeArray = Array.isArray(selectedIncome) ? selectedIncome : selectedIncome ? [selectedIncome] : []; - - return incomeArray.includes('universalCredit'); - }, - nextStep: 'priority-debts', - }, - ], - defaultNext: 'universal-credit', - }, - 'universal-credit': { - previousStep: 'regular-income', - defaultNext: 'priority-debts', - }, - 'priority-debts': { - previousStep: async (req: Request, formData: Record): Promise => { - // Check if user selected UC on regular-income page - const regularIncomeData = formData?.['regular-income'] as Record | undefined; - const selectedIncome = regularIncomeData?.regularIncome as string | string[] | undefined; - const incomeArray = Array.isArray(selectedIncome) ? selectedIncome : selectedIncome ? [selectedIncome] : []; - - // If UC was selected, user came directly from regular-income (skipped universal-credit) - if (incomeArray.includes('universalCredit')) { - return 'regular-income'; - } - - // Otherwise, user came from universal-credit step - return 'universal-credit'; - }, - routes: [ - { - condition: async (req, formData) => formData.hasPriorityDebts === 'yes', - nextStep: 'priority-debt-details', - }, - { - condition: async (req, formData) => formData.hasPriorityDebts === 'no', - nextStep: 'regular-expenses', - }, - ], - defaultNext: 'priority-debt-details', - }, - 'priority-debt-details': { - previousStep: 'priority-debts', - defaultNext: 'regular-expenses', - }, - 'regular-expenses': { - previousStep: 'priority-debt-details', defaultNext: 'end-now', }, }, diff --git a/src/main/steps/respond-to-claim/priority-debts/index.ts b/src/main/steps/respond-to-claim/priority-debts/index.ts index ad6841b6c..b925177a5 100644 --- a/src/main/steps/respond-to-claim/priority-debts/index.ts +++ b/src/main/steps/respond-to-claim/priority-debts/index.ts @@ -8,62 +8,6 @@ export const step: StepDefinition = createFormStep({ journeyFolder: 'respondToClaim', stepDir: __dirname, flowConfig, + fields: [], customTemplate: `${__dirname}/priorityDebts.njk`, - - // TODO: Uncomment when backend API field is added to CCD - // getInitialFormData: (req: Request) => { - // const caseData = req.res?.locals?.validatedCase?.data; - // const response = caseData?.possessionClaimResponse?.defendantResponses; - // - // if (!response) return {}; - // - // const formData: Record = {}; - // - // if (response.hasPriorityDebts) { - // formData.hasPriorityDebts = response.hasPriorityDebts; - // } - // - // return formData; - // }, - - // TODO: Uncomment when backend API field is added to CCD - // beforeRedirect: async (req: Request) => { - // const hasPriorityDebts = req.body?.hasPriorityDebts as string | undefined; - // - // if (!hasPriorityDebts) return; - // - // const possessionClaimResponse: PossessionClaimResponse = { - // defendantResponses: { - // hasPriorityDebts, - // }, - // }; - // - // await buildCcdCaseForPossessionClaimResponse(req, possessionClaimResponse); - // }, - - translationKeys: { - caption: 'caption', - pageTitle: 'pageTitle', - paragraph1: 'paragraph1', - paragraph2: 'paragraph2', - bulletList: 'bulletList', - guidanceLinkText: 'guidanceLink.text', - guidanceLinkUrl: 'guidanceLink.url', - question: 'question', - }, - - fields: [ - { - name: 'hasPriorityDebts', - type: 'radio', - required: true, - errorMessage: 'errors.hasPriorityDebts.required', - translationKey: { label: 'question' }, - legendClasses: 'govuk-fieldset__legend--m', - options: [ - { value: 'yes', translationKey: 'options.yes' }, - { value: 'no', translationKey: 'options.no' }, - ], - }, - ], }); diff --git a/src/main/steps/respond-to-claim/priority-debts/priorityDebts.njk b/src/main/steps/respond-to-claim/priority-debts/priorityDebts.njk index 8c1ee4072..39c1190c6 100644 --- a/src/main/steps/respond-to-claim/priority-debts/priorityDebts.njk +++ b/src/main/steps/respond-to-claim/priority-debts/priorityDebts.njk @@ -1,25 +1,25 @@ {% extends "stepsTemplate.njk" %} -{% from "govuk/components/radios/macro.njk" import govukRadios %} -{% from "macros/formButtons.njk" import formButtons %} - +{% from "govuk/components/button/macro.njk" import govukButton %} +{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} +{% from "macros/csrf.njk" import csrfProtection %} {% block mainContent %} -

{{ pageTitle }}

- -

{{ paragraph1 }}

- -

{{ paragraph2 }}

- -
    - {% for item in bulletList %} -
  • {{ item }}
  • - {% endfor %} -
- -

- {{ guidanceLinkText }}. -

- - {{ formHtml | safe }} - - {{ formButtons(buttons) }} + {% if errorSummary %} + {{ govukErrorSummary(errorSummary) }} + {% endif %} +

Priority debts (placeholder)

+

This is a placeholder for Priority Debts step.

+
+
+ {{ govukButton({ + text: continue, + attributes: { type: 'submit', name: 'action', value: 'continue' } + }) }} + {{ govukButton({ + text: saveForLater, + classes: 'govuk-button--secondary', + attributes: { type: 'submit', name: 'action', value: 'saveForLater' } + }) }} +
+ {{ csrfProtection(csrfToken) }} +
{% endblock %} diff --git a/src/main/steps/respond-to-claim/regular-expenses/index.ts b/src/main/steps/respond-to-claim/regular-expenses/index.ts index 1e800c84c..cf63638d6 100644 --- a/src/main/steps/respond-to-claim/regular-expenses/index.ts +++ b/src/main/steps/respond-to-claim/regular-expenses/index.ts @@ -4,30 +4,10 @@ import { flowConfig } from '../flow.config'; import { createFormStep } from '@modules/steps'; export const step: StepDefinition = createFormStep({ - stepName: 'regular-expenses', + stepName: 'what-other-regular-expenses-do-you-have', journeyFolder: 'respondToClaim', stepDir: __dirname, flowConfig, - customTemplate: `${__dirname}/regularExpenses.njk`, - - // TODO: Add field configuration for expense categories - // TODO: Uncomment when backend API field is added to CCD - // getInitialFormData: req => { - // const caseData = req.res?.locals?.validatedCase?.data; - // // Map CCD data to form values - // return {}; - // }, - - // TODO: Uncomment when backend API field is added to CCD - // beforeRedirect: async req => { - // // Save to CCD before redirect - // await buildCcdCaseForPossessionClaimResponse(req, possessionClaimResponse); - // }, - - translationKeys: { - pageTitle: 'pageTitle', - question: 'question', - }, - fields: [], + customTemplate: `${__dirname}/regularExpenses.njk`, }); diff --git a/src/main/steps/respond-to-claim/universal-credit/index.ts b/src/main/steps/respond-to-claim/universal-credit/index.ts index ee411277a..78f9a6dac 100644 --- a/src/main/steps/respond-to-claim/universal-credit/index.ts +++ b/src/main/steps/respond-to-claim/universal-credit/index.ts @@ -1,114 +1,13 @@ -import type { Request } from 'express'; - -import type { PossessionClaimResponse } from '../../../interfaces/ccdCase.interface'; import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; -import { buildCcdCaseForPossessionClaimResponse } from '../../utils/populateResponseToClaimPayloadmap'; -import { fromYesNoEnum, toYesNoEnum } from '../../utils/yesNoEnum'; import { flowConfig } from '../flow.config'; import { createFormStep } from '@modules/steps'; export const step: StepDefinition = createFormStep({ - stepName: 'universal-credit', + stepName: 'have-you-applied-for-universal-credit', journeyFolder: 'respondToClaim', stepDir: __dirname, flowConfig, + fields: [], customTemplate: `${__dirname}/universalCredit.njk`, - - getInitialFormData: (req: Request) => { - const caseData = req.res?.locals?.validatedCase?.data; - const hc = caseData?.possessionClaimResponse?.defendantResponses?.householdCircumstances; - - if (!hc) { - return {}; - } - - const formData: Record = {}; - - // Convert YES/NO to yes/no - if (hc.universalCredit) { - formData.appliedForUniversalCredit = fromYesNoEnum(hc.universalCredit); - } - - // Convert date from YYYY-MM-DD to day/month/year fields - if (hc.ucApplicationDate) { - const dateStr = hc.ucApplicationDate as string; - const [year, month, day] = dateStr.split('-'); - formData['applicationDate-day'] = day; - formData['applicationDate-month'] = month; - formData['applicationDate-year'] = year; - } - - return formData; - }, - - beforeRedirect: async (req: Request) => { - const appliedForUC = req.body?.appliedForUniversalCredit as 'yes' | 'no' | undefined; - - if (!appliedForUC) { - return; - } - - const householdCircumstances: Record = { - universalCredit: toYesNoEnum(appliedForUC), - }; - - // Only save date if they answered "yes" - if (appliedForUC === 'yes') { - const day = req.body?.['applicationDate-day'] as string | undefined; - const month = req.body?.['applicationDate-month'] as string | undefined; - const year = req.body?.['applicationDate-year'] as string | undefined; - - if (day && month && year) { - // Store as ISO date string (YYYY-MM-DD) - householdCircumstances.ucApplicationDate = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; - } - } - - const possessionClaimResponse: PossessionClaimResponse = { - defendantResponses: { - householdCircumstances, - }, - }; - await buildCcdCaseForPossessionClaimResponse(req, possessionClaimResponse); - }, - - translationKeys: { - caption: 'caption', - pageTitle: 'pageTitle', - }, - - fields: [ - { - name: 'appliedForUniversalCredit', - type: 'radio', - required: true, - errorMessage: 'errors.appliedForUniversalCredit.required', - translationKey: { label: 'pageTitle' }, - legendClasses: 'govuk-fieldset__legend--l', - isPageHeading: true, - options: [ - { - value: 'yes', - translationKey: 'options.yes', - subFields: { - applicationDate: { - name: 'applicationDate', - type: 'date', - required: true, - noFutureDate: true, - translationKey: { - label: 'subFields.applicationDate.label', - hint: 'subFields.applicationDate.hint', - }, - }, - }, - }, - { - value: 'no', - translationKey: 'options.no', - }, - ], - }, - ], }); From 42e5d575bfd16e3a7f9acc46d8578bdf42cda369 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 25 Mar 2026 07:23:11 +0000 Subject: [PATCH 06/20] HDPI-3764: Fix Universal Credit field mapping to match pcs-api backend schema --- src/main/interfaces/ccdCase.interface.ts | 5 +- .../respond-to-claim/regular-income/index.ts | 4 +- .../flow-config-universal-credit.test.ts | 211 ------------------ 3 files changed, 4 insertions(+), 216 deletions(-) delete mode 100644 src/test/unit/steps/respond-to-claim/flow-config-universal-credit.test.ts diff --git a/src/main/interfaces/ccdCase.interface.ts b/src/main/interfaces/ccdCase.interface.ts index e86aca2f5..2611cb845 100644 --- a/src/main/interfaces/ccdCase.interface.ts +++ b/src/main/interfaces/ccdCase.interface.ts @@ -45,10 +45,9 @@ export interface HouseholdCircumstances { pension?: YesNoValue; pensionAmount?: string; pensionFrequency?: string; - universalCreditIncome?: YesNoValue; - universalCreditIncomeAmount?: string; - universalCreditIncomeFrequency?: string; universalCredit?: YesNoValue; + universalCreditAmount?: string; + universalCreditFrequency?: string; ucApplicationDate?: string; otherBenefits?: YesNoValue; otherBenefitsAmount?: string; diff --git a/src/main/steps/respond-to-claim/regular-income/index.ts b/src/main/steps/respond-to-claim/regular-income/index.ts index 78c94b1eb..1a13e11cc 100644 --- a/src/main/steps/respond-to-claim/regular-income/index.ts +++ b/src/main/steps/respond-to-claim/regular-income/index.ts @@ -91,7 +91,7 @@ export const step: StepDefinition = createFormStep({ // Universal Credit - Case-insensitive check to handle backend variations (YES/Yes/yes) // Note: amount/frequency may not exist - see BA clarification - if (hc.universalCreditIncome && (hc.universalCreditIncome as string).toUpperCase() === 'YES') { + if (hc.universalCredit && (hc.universalCredit as string).toUpperCase() === 'YES') { selectedIncome.push('universalCredit'); // UC amount/frequency commented out pending BA clarification // if (hc.universalCreditAmount) { @@ -165,7 +165,7 @@ export const step: StepDefinition = createFormStep({ } // Universal Credit (checkbox only - amount/frequency pending BA clarification) - householdCircumstances.universalCreditIncome = toYesNoEnum(incomeArray.includes('universalCredit') ? 'yes' : 'no'); + householdCircumstances.universalCredit = toYesNoEnum(incomeArray.includes('universalCredit') ? 'yes' : 'no'); // UC amount/frequency commented out pending BA clarification // if (incomeArray.includes('universalCredit')) { // const amountRaw = req.body?.['regularIncome.universalCreditAmount'] as string | undefined; diff --git a/src/test/unit/steps/respond-to-claim/flow-config-universal-credit.test.ts b/src/test/unit/steps/respond-to-claim/flow-config-universal-credit.test.ts deleted file mode 100644 index 633b42548..000000000 --- a/src/test/unit/steps/respond-to-claim/flow-config-universal-credit.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import type { Request } from 'express'; - -import { flowConfig } from '../../../../main/steps/respond-to-claim/flow.config'; - -describe('respond-to-claim flow config - Universal Credit conditional routing', () => { - const createMockRequest = (): Request => - ({ - session: { formData: {} }, - }) as Request; - - describe('regular-income step conditional routing', () => { - const regularIncomeStep = flowConfig.steps['regular-income']; - - it('routes to priority-debts when universalCredit checkbox is selected (single value)', async () => { - const req = createMockRequest(); - const currentStepData = { - regularIncome: 'universalCredit', - }; - - const route = regularIncomeStep.routes?.[0]; - expect(route).toBeDefined(); - - const shouldRoute = await route!.condition!(req, {}, currentStepData); - expect(shouldRoute).toBe(true); - expect(route!.nextStep).toBe('priority-debts'); - }); - - it('routes to priority-debts when universalCredit is in array (multiple selections)', async () => { - const req = createMockRequest(); - const currentStepData = { - regularIncome: ['incomeFromJobs', 'universalCredit', 'pension'], - }; - - const route = regularIncomeStep.routes?.[0]; - const shouldRoute = await route!.condition!(req, {}, currentStepData); - expect(shouldRoute).toBe(true); - }); - - it('does not route to priority-debts when universalCredit is NOT selected', async () => { - const req = createMockRequest(); - const currentStepData = { - regularIncome: ['incomeFromJobs', 'pension'], - }; - - const route = regularIncomeStep.routes?.[0]; - const shouldRoute = await route!.condition!(req, {}, currentStepData); - expect(shouldRoute).toBe(false); - }); - - it('does not route to priority-debts when regularIncome is empty', async () => { - const req = createMockRequest(); - const currentStepData = { - regularIncome: [], - }; - - const route = regularIncomeStep.routes?.[0]; - const shouldRoute = await route!.condition!(req, {}, currentStepData); - expect(shouldRoute).toBe(false); - }); - - it('does not route to priority-debts when regularIncome is undefined', async () => { - const req = createMockRequest(); - const currentStepData = {}; - - const route = regularIncomeStep.routes?.[0]; - const shouldRoute = await route!.condition!(req, {}, currentStepData); - expect(shouldRoute).toBe(false); - }); - - it('uses persisted formData when currentStepData is empty (back button scenario)', async () => { - const req = createMockRequest(); - const persistedFormData = { - 'regular-income': { - regularIncome: ['universalCredit', 'pension'], - }, - }; - const currentStepData = {}; - - const route = regularIncomeStep.routes?.[0]; - const shouldRoute = await route!.condition!(req, persistedFormData, currentStepData); - expect(shouldRoute).toBe(true); - }); - - it('uses universal-credit as defaultNext when condition does not match', () => { - expect(regularIncomeStep.defaultNext).toBe('universal-credit'); - }); - }); - - describe('priority-debts step previousStep function', () => { - const priorityDebtsStep = flowConfig.steps['priority-debts']; - - it('returns regular-income when user selected universalCredit', async () => { - const req = createMockRequest(); - const formData = { - 'regular-income': { - regularIncome: ['incomeFromJobs', 'universalCredit'], - }, - }; - - const previousStepFn = priorityDebtsStep.previousStep; - expect(typeof previousStepFn).toBe('function'); - - const previousStep = await ( - previousStepFn as (req: Request, formData: Record) => Promise - )(req, formData); - expect(previousStep).toBe('regular-income'); - }); - - it('returns universal-credit when user did NOT select universalCredit', async () => { - const req = createMockRequest(); - const formData = { - 'regular-income': { - regularIncome: ['incomeFromJobs', 'pension'], - }, - }; - - const previousStepFn = priorityDebtsStep.previousStep; - const previousStep = await ( - previousStepFn as (req: Request, formData: Record) => Promise - )(req, formData); - expect(previousStep).toBe('universal-credit'); - }); - - it('returns universal-credit when regular-income data is missing', async () => { - const req = createMockRequest(); - const formData = {}; - - const previousStepFn = priorityDebtsStep.previousStep; - const previousStep = await ( - previousStepFn as (req: Request, formData: Record) => Promise - )(req, formData); - expect(previousStep).toBe('universal-credit'); - }); - - it('returns universal-credit when regularIncome is undefined', async () => { - const req = createMockRequest(); - const formData = { - 'regular-income': {}, - }; - - const previousStepFn = priorityDebtsStep.previousStep; - const previousStep = await ( - previousStepFn as (req: Request, formData: Record) => Promise - )(req, formData); - expect(previousStep).toBe('universal-credit'); - }); - - it('handles regularIncome as single string value', async () => { - const req = createMockRequest(); - const formData = { - 'regular-income': { - regularIncome: 'universalCredit', - }, - }; - - const previousStepFn = priorityDebtsStep.previousStep; - const previousStep = await ( - previousStepFn as (req: Request, formData: Record) => Promise - )(req, formData); - expect(previousStep).toBe('regular-income'); - }); - }); - - describe('AC13: Skip universal-credit step when UC selected', () => { - it('full journey path: regular-income -> priority-debts (UC selected)', async () => { - const req = createMockRequest(); - const currentStepData = { - regularIncome: ['universalCredit', 'pension'], - }; - - // Step 1: User submits regular-income with UC selected - const regularIncomeRoute = flowConfig.steps['regular-income'].routes?.[0]; - const shouldSkipUC = await regularIncomeRoute!.condition!(req, {}, currentStepData); - expect(shouldSkipUC).toBe(true); - expect(regularIncomeRoute!.nextStep).toBe('priority-debts'); - - // Step 2: User clicks back from priority-debts - const fullFormData = { 'regular-income': { regularIncome: ['universalCredit', 'pension'] } }; - const priorityDebtsPreviousStep = await ( - flowConfig.steps['priority-debts'].previousStep as ( - req: Request, - formData: Record - ) => Promise - )(req, fullFormData); - expect(priorityDebtsPreviousStep).toBe('regular-income'); - }); - - it('full journey path: regular-income -> universal-credit -> priority-debts (UC NOT selected)', async () => { - const req = createMockRequest(); - const currentStepData = { - regularIncome: ['incomeFromJobs', 'pension'], - }; - - // Step 1: User submits regular-income without UC - const regularIncomeRoute = flowConfig.steps['regular-income'].routes?.[0]; - const shouldSkipUC = await regularIncomeRoute!.condition!(req, {}, currentStepData); - expect(shouldSkipUC).toBe(false); - expect(flowConfig.steps['regular-income'].defaultNext).toBe('universal-credit'); - - // Step 2: User clicks back from priority-debts - const fullFormData = { 'regular-income': { regularIncome: ['incomeFromJobs', 'pension'] } }; - const priorityDebtsPreviousStep = await ( - flowConfig.steps['priority-debts'].previousStep as ( - req: Request, - formData: Record - ) => Promise - )(req, fullFormData); - expect(priorityDebtsPreviousStep).toBe('universal-credit'); - }); - }); -}); From ae01f5fb7a00de67b48e2f64ab16d9019ccdce8e Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 25 Mar 2026 07:25:08 +0000 Subject: [PATCH 07/20] HDPI-3764: Enable Universal Credit amount and frequency mapping --- .../respond-to-claim/regular-income/index.ts | 44 ++++++++----------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/src/main/steps/respond-to-claim/regular-income/index.ts b/src/main/steps/respond-to-claim/regular-income/index.ts index 1a13e11cc..79ba9731d 100644 --- a/src/main/steps/respond-to-claim/regular-income/index.ts +++ b/src/main/steps/respond-to-claim/regular-income/index.ts @@ -90,18 +90,14 @@ export const step: StepDefinition = createFormStep({ } // Universal Credit - Case-insensitive check to handle backend variations (YES/Yes/yes) - // Note: amount/frequency may not exist - see BA clarification if (hc.universalCredit && (hc.universalCredit as string).toUpperCase() === 'YES') { selectedIncome.push('universalCredit'); - // UC amount/frequency commented out pending BA clarification - // if (hc.universalCreditAmount) { - // const amountInPence = parseFloat(hc.universalCreditAmount as string); - // const amountInPounds = amountInPence / 100; - // formData['regularIncome.universalCreditAmount'] = amountInPounds.toFixed(2); - // } - // if (hc.universalCreditFrequency) { - // formData['regularIncome.universalCreditFrequency'] = (hc.universalCreditFrequency as string).toLowerCase(); - // } + if (hc.universalCreditAmount) { + formData['regularIncome.universalCreditAmount'] = penceToPounds(hc.universalCreditAmount as string); + } + if (hc.universalCreditFrequency) { + formData['regularIncome.universalCreditFrequency'] = (hc.universalCreditFrequency as string).toLowerCase(); + } } // Other benefits - Case-insensitive check to handle backend variations (YES/Yes/yes) @@ -164,23 +160,19 @@ export const step: StepDefinition = createFormStep({ } } - // Universal Credit (checkbox only - amount/frequency pending BA clarification) + // Universal Credit householdCircumstances.universalCredit = toYesNoEnum(incomeArray.includes('universalCredit') ? 'yes' : 'no'); - // UC amount/frequency commented out pending BA clarification - // if (incomeArray.includes('universalCredit')) { - // const amountRaw = req.body?.['regularIncome.universalCreditAmount'] as string | undefined; - // const frequency = req.body?.['regularIncome.universalCreditFrequency'] as string | undefined; - // if (amountRaw) { - // const normalized = amountRaw.replace(/,/g, ''); - // const amountInPounds = parseFloat(normalized); - // if (!Number.isNaN(amountInPounds)) { - // householdCircumstances.universalCreditAmount = String(Math.round(amountInPounds * 100)); - // } - // } - // if (frequency) { - // householdCircumstances.universalCreditFrequency = frequency.toUpperCase(); - // } - // } + if (incomeArray.includes('universalCredit')) { + const amountRaw = req.body?.['regularIncome.universalCreditAmount'] as string | undefined; + const frequency = req.body?.['regularIncome.universalCreditFrequency'] as string | undefined; + + if (amountRaw) { + householdCircumstances.universalCreditAmount = poundsToPence(amountRaw); + } + if (frequency) { + householdCircumstances.universalCreditFrequency = frequency.toUpperCase(); + } + } // Other benefits householdCircumstances.otherBenefits = toYesNoEnum(incomeArray.includes('otherBenefits') ? 'yes' : 'no'); From 9fa0a5aead9a9c00fffbd5369c3ca6591bbe2b78 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 25 Mar 2026 08:11:34 +0000 Subject: [PATCH 08/20] HDPI-3764: Add conditional UC routing and upload-docs placeholder step --- .../steps/respond-to-claim/flow.config.ts | 41 +++++++++++++++---- .../steps/respond-to-claim/stepRegistry.ts | 6 ++- .../respond-to-claim/upload-docs/index.ts | 13 ++++++ .../upload-docs/uploadDocs.njk | 25 +++++++++++ 4 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 src/main/steps/respond-to-claim/upload-docs/index.ts create mode 100644 src/main/steps/respond-to-claim/upload-docs/uploadDocs.njk diff --git a/src/main/steps/respond-to-claim/flow.config.ts b/src/main/steps/respond-to-claim/flow.config.ts index 8c342f643..c68b1f165 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -50,7 +50,11 @@ export const flowConfig: JourneyFlowConfig = { 'exceptional-hardship', 'income-and-expenditure', 'regular-income', - 'end-now', + 'have-you-applied-for-universal-credit', + 'priority-debts', + 'priority-debt-details', + 'what-other-regular-expenses-do-you-have', + 'upload-docs', ], steps: { 'start-now': { @@ -354,21 +358,40 @@ export const flowConfig: JourneyFlowConfig = { }, 'income-and-expenditure': { previousStep: 'exceptional-hardship', + defaultNext: 'regular-income', + }, + 'regular-income': { + previousStep: 'income-and-expenditure', routes: [ { - condition: async (req, formData) => formData.provideFinanceDetails === 'yes', - nextStep: 'regular-income', + condition: async (req: Request, formData: Record): Promise => { + const regularIncome = (formData['regular-income'] as Record)?.regularIncome; + return [regularIncome].flat().filter(Boolean).includes('universalCredit'); + }, + nextStep: 'priority-debts', }, { - condition: async (req, formData) => formData.provideFinanceDetails === 'no', - nextStep: 'end-now', + condition: async (req: Request, formData: Record): Promise => { + const regularIncome = (formData['regular-income'] as Record)?.regularIncome; + return ![regularIncome].flat().filter(Boolean).includes('universalCredit'); + }, + nextStep: 'have-you-applied-for-universal-credit', }, ], - defaultNext: 'regular-income', + defaultNext: 'have-you-applied-for-universal-credit', }, - 'regular-income': { - previousStep: 'income-and-expenditure', - defaultNext: 'end-now', + 'have-you-applied-for-universal-credit': { + defaultNext: 'priority-debts', + }, + 'priority-debts': { + defaultNext: 'priority-debt-details', + }, + 'priority-debt-details': { + defaultNext: 'what-other-regular-expenses-do-you-have', + }, + 'what-other-regular-expenses-do-you-have': { + defaultNext: 'upload-docs', }, + 'upload-docs': {}, }, }; diff --git a/src/main/steps/respond-to-claim/stepRegistry.ts b/src/main/steps/respond-to-claim/stepRegistry.ts index 5746c9940..044a5aaee 100644 --- a/src/main/steps/respond-to-claim/stepRegistry.ts +++ b/src/main/steps/respond-to-claim/stepRegistry.ts @@ -37,6 +37,7 @@ import { step as tenancyDateDetails } from './tenancy-date-details'; import { step as tenancyDateUnknown } from './tenancy-date-unknown'; import { step as tenancyTypeDetails } from './tenancy-type-details'; import { step as universalCredit } from './universal-credit'; +import { step as uploadDocs } from './upload-docs'; export const stepRegistry: Record = { 'start-now': startNow, @@ -73,8 +74,9 @@ export const stepRegistry: Record = { 'exceptional-hardship': exceptionalHardship, 'income-and-expenditure': incomeAndExpenditure, 'regular-income': regularIncome, - 'universal-credit': universalCredit, + 'have-you-applied-for-universal-credit': universalCredit, 'priority-debts': priorityDebts, 'priority-debt-details': priorityDebtDetails, - 'regular-expenses': regularExpenses, + 'what-other-regular-expenses-do-you-have': regularExpenses, + 'upload-docs': uploadDocs, }; diff --git a/src/main/steps/respond-to-claim/upload-docs/index.ts b/src/main/steps/respond-to-claim/upload-docs/index.ts new file mode 100644 index 000000000..3c513ccc0 --- /dev/null +++ b/src/main/steps/respond-to-claim/upload-docs/index.ts @@ -0,0 +1,13 @@ +import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; +import { flowConfig } from '../flow.config'; + +import { createFormStep } from '@modules/steps'; + +export const step: StepDefinition = createFormStep({ + stepName: 'upload-docs', + journeyFolder: 'respondToClaim', + stepDir: __dirname, + flowConfig, + fields: [], + customTemplate: `${__dirname}/uploadDocs.njk`, +}); diff --git a/src/main/steps/respond-to-claim/upload-docs/uploadDocs.njk b/src/main/steps/respond-to-claim/upload-docs/uploadDocs.njk new file mode 100644 index 000000000..16da4ca6c --- /dev/null +++ b/src/main/steps/respond-to-claim/upload-docs/uploadDocs.njk @@ -0,0 +1,25 @@ +{% extends "stepsTemplate.njk" %} +{% from "govuk/components/button/macro.njk" import govukButton %} +{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} +{% from "macros/csrf.njk" import csrfProtection %} +{% block mainContent %} + {% if errorSummary %} + {{ govukErrorSummary(errorSummary) }} + {% endif %} +

Upload documents (placeholder)

+

This is a placeholder for Upload Documents step.

+
+
+ {{ govukButton({ + text: continue, + attributes: { type: 'submit', name: 'action', value: 'continue' } + }) }} + {{ govukButton({ + text: saveForLater, + classes: 'govuk-button--secondary', + attributes: { type: 'submit', name: 'action', value: 'saveForLater' } + }) }} +
+ {{ csrfProtection(csrfToken) }} +
+{% endblock %} From e30d7edbec582e2db87ade110f01d39f2d1a53d2 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 25 Mar 2026 08:19:57 +0000 Subject: [PATCH 09/20] HDPI-3764: Use req.session.formData directly for consistency with existing branches --- src/main/steps/respond-to-claim/flow.config.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/steps/respond-to-claim/flow.config.ts b/src/main/steps/respond-to-claim/flow.config.ts index c68b1f165..5baf8371f 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -364,15 +364,15 @@ export const flowConfig: JourneyFlowConfig = { previousStep: 'income-and-expenditure', routes: [ { - condition: async (req: Request, formData: Record): Promise => { - const regularIncome = (formData['regular-income'] as Record)?.regularIncome; + condition: async (req: Request): Promise => { + const regularIncome = req.session?.formData?.['regular-income']?.regularIncome; return [regularIncome].flat().filter(Boolean).includes('universalCredit'); }, nextStep: 'priority-debts', }, { - condition: async (req: Request, formData: Record): Promise => { - const regularIncome = (formData['regular-income'] as Record)?.regularIncome; + condition: async (req: Request): Promise => { + const regularIncome = req.session?.formData?.['regular-income']?.regularIncome; return ![regularIncome].flat().filter(Boolean).includes('universalCredit'); }, nextStep: 'have-you-applied-for-universal-credit', From 292ff605955834022fe01909b7711bb13aed2bb0 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 25 Mar 2026 08:30:15 +0000 Subject: [PATCH 10/20] HDPI-3764: Use currentStepData (req.body) for stateless routing decisions --- src/main/steps/respond-to-claim/flow.config.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/steps/respond-to-claim/flow.config.ts b/src/main/steps/respond-to-claim/flow.config.ts index 5baf8371f..f6a683bd3 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -364,15 +364,23 @@ export const flowConfig: JourneyFlowConfig = { previousStep: 'income-and-expenditure', routes: [ { - condition: async (req: Request): Promise => { - const regularIncome = req.session?.formData?.['regular-income']?.regularIncome; + condition: async ( + req: Request, + formData: Record, + currentStepData: Record + ): Promise => { + const regularIncome = currentStepData.regularIncome; return [regularIncome].flat().filter(Boolean).includes('universalCredit'); }, nextStep: 'priority-debts', }, { - condition: async (req: Request): Promise => { - const regularIncome = req.session?.formData?.['regular-income']?.regularIncome; + condition: async ( + req: Request, + formData: Record, + currentStepData: Record + ): Promise => { + const regularIncome = currentStepData.regularIncome; return ![regularIncome].flat().filter(Boolean).includes('universalCredit'); }, nextStep: 'have-you-applied-for-universal-credit', From 69693e44e717c13a095ce0105282715869e5766c Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 25 Mar 2026 09:17:44 +0000 Subject: [PATCH 11/20] HDPI-3764: Remove placeholder translation files to sync with master --- .../cy/respondToClaim/priorityDebts.json | 31 ------------------- .../cy/respondToClaim/regularExpenses.json | 4 --- .../cy/respondToClaim/universalCredit.json | 30 ------------------ .../en/respondToClaim/priorityDebts.json | 31 ------------------- .../en/respondToClaim/regularExpenses.json | 4 --- .../en/respondToClaim/universalCredit.json | 30 ------------------ 6 files changed, 130 deletions(-) delete mode 100644 src/main/assets/locales/cy/respondToClaim/priorityDebts.json delete mode 100644 src/main/assets/locales/cy/respondToClaim/regularExpenses.json delete mode 100644 src/main/assets/locales/cy/respondToClaim/universalCredit.json delete mode 100644 src/main/assets/locales/en/respondToClaim/priorityDebts.json delete mode 100644 src/main/assets/locales/en/respondToClaim/regularExpenses.json delete mode 100644 src/main/assets/locales/en/respondToClaim/universalCredit.json diff --git a/src/main/assets/locales/cy/respondToClaim/priorityDebts.json b/src/main/assets/locales/cy/respondToClaim/priorityDebts.json deleted file mode 100644 index 9d4415c33..000000000 --- a/src/main/assets/locales/cy/respondToClaim/priorityDebts.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "caption": "cyRespond to a property possession claim", - "pageTitle": "cyPriority debts", - "paragraph1": "cyPriority debts are debts which could lead to serious consequences if you do not pay them.", - "paragraph2": "cyExcluding rent or mortgage arrears, priority debts are:", - "bulletList": [ - "cycouncil tax arrears", - "cygas or electricity arrears", - "cyphone or internet arrears", - "cyTV licence arrears", - "cycourt fines", - "cyoverpaid tax credits", - "cypayments for goods bought on hire purchase or conditional sale", - "cyunpaid income tax, National Insurance or VAT", - "cyunpaid child maintenance" - ], - "guidanceLink": { - "text": "cyRead guidance from Citizens Advice on what counts as a priority debt (opens in new tab)", - "url": "https://www.citizensadvice.org.uk/debt-and-money/priority-debts/" - }, - "question": "cyDo you have any priority debts?", - "options": { - "yes": "cyYes", - "no": "cyNo" - }, - "errors": { - "hasPriorityDebts": { - "required": "cySelect if you have any priority debts" - } - } -} diff --git a/src/main/assets/locales/cy/respondToClaim/regularExpenses.json b/src/main/assets/locales/cy/respondToClaim/regularExpenses.json deleted file mode 100644 index 592f0c80a..000000000 --- a/src/main/assets/locales/cy/respondToClaim/regularExpenses.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "pageTitle": "cyWhat other regular expenses do you have?", - "question": "cyWhat other regular expenses do you have?" -} diff --git a/src/main/assets/locales/cy/respondToClaim/universalCredit.json b/src/main/assets/locales/cy/respondToClaim/universalCredit.json deleted file mode 100644 index 4ded96c86..000000000 --- a/src/main/assets/locales/cy/respondToClaim/universalCredit.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "caption": "cyRespond to a property possession claim", - "pageTitle": "cyHave you applied for Universal Credit?", - "options": { - "yes": "cyYes", - "no": "cyNo" - }, - "subFields": { - "applicationDate": { - "label": "cyWhen did you apply?", - "hint": "cyFor example, 27 9 2022" - } - }, - "errors": { - "appliedForUniversalCredit": { - "required": "cySelect if you've applied for Universal Credit" - }, - "date": { - "day": "cyday", - "month": "cymonth", - "year": "cyyear", - "notRealDate": "cyThe date you applied for Universal Credit must be a real date", - "futureDate": "cyThe date you applied for Universal Credit must either be today's date or in the past", - "invalidValue": "cyThe date you applied for Universal Credit must be a real date", - "missingOne": "cyThe date you applied for Universal Credit must include a {{missingField}}", - "missingTwo": "cyThe date you applied for Universal Credit must include a {{first}} and {{second}}", - "required": "cyEnter the date you applied for Universal Credit" - } - } -} diff --git a/src/main/assets/locales/en/respondToClaim/priorityDebts.json b/src/main/assets/locales/en/respondToClaim/priorityDebts.json deleted file mode 100644 index e0a9f1128..000000000 --- a/src/main/assets/locales/en/respondToClaim/priorityDebts.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "caption": "Respond to a property possession claim", - "pageTitle": "Priority debts", - "paragraph1": "Priority debts are debts which could lead to serious consequences if you do not pay them.", - "paragraph2": "Excluding rent or mortgage arrears, priority debts are:", - "bulletList": [ - "council tax arrears", - "gas or electricity arrears", - "phone or internet arrears", - "TV licence arrears", - "court fines", - "overpaid tax credits", - "payments for goods bought on hire purchase or conditional sale", - "unpaid income tax, National Insurance or VAT", - "unpaid child maintenance" - ], - "guidanceLink": { - "text": "Read guidance from Citizens Advice on what counts as a priority debt (opens in new tab)", - "url": "https://www.citizensadvice.org.uk/debt-and-money/priority-debts/" - }, - "question": "Do you have any priority debts?", - "options": { - "yes": "Yes", - "no": "No" - }, - "errors": { - "hasPriorityDebts": { - "required": "Select if you have any priority debts" - } - } -} diff --git a/src/main/assets/locales/en/respondToClaim/regularExpenses.json b/src/main/assets/locales/en/respondToClaim/regularExpenses.json deleted file mode 100644 index 93cbf02ed..000000000 --- a/src/main/assets/locales/en/respondToClaim/regularExpenses.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "pageTitle": "What other regular expenses do you have?", - "question": "What other regular expenses do you have?" -} diff --git a/src/main/assets/locales/en/respondToClaim/universalCredit.json b/src/main/assets/locales/en/respondToClaim/universalCredit.json deleted file mode 100644 index c6377dd35..000000000 --- a/src/main/assets/locales/en/respondToClaim/universalCredit.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "caption": "Respond to a property possession claim", - "pageTitle": "Have you applied for Universal Credit?", - "options": { - "yes": "Yes", - "no": "No" - }, - "subFields": { - "applicationDate": { - "label": "When did you apply?", - "hint": "For example, 27 9 2022" - } - }, - "errors": { - "appliedForUniversalCredit": { - "required": "Select if you've applied for Universal Credit" - }, - "date": { - "day": "day", - "month": "month", - "year": "year", - "notRealDate": "The date you applied for Universal Credit must be a real date", - "futureDate": "The date you applied for Universal Credit must either be today's date or in the past", - "invalidValue": "The date you applied for Universal Credit must be a real date", - "missingOne": "The date you applied for Universal Credit must include a {{missingField}}", - "missingTwo": "The date you applied for Universal Credit must include a {{first}} and {{second}}", - "required": "Enter the date you applied for Universal Credit" - } - } -} From 96bcf8a1fef6abe8d7f36a32fe160cdd3350aef4 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 25 Mar 2026 18:19:07 +0000 Subject: [PATCH 12/20] HDPI-3764: Add conditional routing between income-and-expenditure and upload-docs based on CCD data --- .../steps/respond-to-claim/flow.config.ts | 34 ++++++++++++++++++- .../utils/cameFromIncomeAndExpenditure.ts | 16 +++++++++ src/main/steps/utils/index.ts | 1 + 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/main/steps/utils/cameFromIncomeAndExpenditure.ts diff --git a/src/main/steps/respond-to-claim/flow.config.ts b/src/main/steps/respond-to-claim/flow.config.ts index f6a683bd3..82462e4a0 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -2,6 +2,7 @@ import { type Request } from 'express'; import type { JourneyFlowConfig } from '../../interfaces/stepFlow.interface'; import { + cameFromIncomeAndExpenditure, getPreviousPageForArrears, isDefendantNameKnown, isNoticeDateProvided, @@ -358,6 +359,28 @@ export const flowConfig: JourneyFlowConfig = { }, 'income-and-expenditure': { previousStep: 'exceptional-hardship', + routes: [ + { + condition: async ( + req: Request, + formData: Record, + currentStepData: Record + ): Promise => { + return currentStepData.provideFinanceDetails === 'no'; + }, + nextStep: 'upload-docs', + }, + { + condition: async ( + req: Request, + formData: Record, + currentStepData: Record + ): Promise => { + return currentStepData.provideFinanceDetails === 'yes'; + }, + nextStep: 'regular-income', + }, + ], defaultNext: 'regular-income', }, 'regular-income': { @@ -400,6 +423,15 @@ export const flowConfig: JourneyFlowConfig = { 'what-other-regular-expenses-do-you-have': { defaultNext: 'upload-docs', }, - 'upload-docs': {}, + 'upload-docs': { + routes: [ + { + condition: async (req: Request): Promise => { + return cameFromIncomeAndExpenditure(req); + }, + nextStep: 'income-and-expenditure', + }, + ], + }, }, }; diff --git a/src/main/steps/utils/cameFromIncomeAndExpenditure.ts b/src/main/steps/utils/cameFromIncomeAndExpenditure.ts new file mode 100644 index 000000000..82d7dd8a5 --- /dev/null +++ b/src/main/steps/utils/cameFromIncomeAndExpenditure.ts @@ -0,0 +1,16 @@ +import type { Request } from 'express'; + +/** + * Checks if the journey came from the income-and-expenditure step. + * + * Checks if the shareIncomeExpenseDetails field exists in CCD case data. + * This field is only set when user completes the income-and-expenditure step. + * Returns true if field exists, indicating journey came from income-and-expenditure. + */ +export const cameFromIncomeAndExpenditure = async (req: Request): Promise => { + const caseData = req.res?.locals?.validatedCase?.data; + const shareIncomeExpenseDetails = + caseData?.possessionClaimResponse?.defendantResponses?.householdCircumstances?.shareIncomeExpenseDetails; + + return shareIncomeExpenseDetails !== undefined; +}; diff --git a/src/main/steps/utils/index.ts b/src/main/steps/utils/index.ts index 2541ea088..741722f8e 100644 --- a/src/main/steps/utils/index.ts +++ b/src/main/steps/utils/index.ts @@ -8,3 +8,4 @@ export { getPreviousPageForArrears } from './journeyHelpers'; export { formatDatePartsToISODate } from './dateUtils'; export { toYesNoEnum, fromYesNoEnum } from './yesNoEnum'; export { penceToPounds, poundsToPence } from './currencyConversion'; +export { cameFromIncomeAndExpenditure } from './cameFromIncomeAndExpenditure'; From 493dbca5a20a68d01e28f247519a67332cf874a3 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 25 Mar 2026 19:24:44 +0000 Subject: [PATCH 13/20] HDPI-3764: Update income and expenditure page content and add third paragraph --- .../locales/cy/respondToClaim/incomeAndExpenditure.json | 3 ++- .../locales/en/respondToClaim/incomeAndExpenditure.json | 3 ++- .../income-and-expenditure/incomeAndExpenditure.njk | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/assets/locales/cy/respondToClaim/incomeAndExpenditure.json b/src/main/assets/locales/cy/respondToClaim/incomeAndExpenditure.json index 37ef351b9..4c329c661 100644 --- a/src/main/assets/locales/cy/respondToClaim/incomeAndExpenditure.json +++ b/src/main/assets/locales/cy/respondToClaim/incomeAndExpenditure.json @@ -1,8 +1,9 @@ { "caption": "cyResponding to the possession claim", "pageTitle": "cyIncome and expenses", - "infoParagraph1": "cyProviding details about your finances could help the judge when they're deciding whether to grant the landlord a possession order, or if they can delay the date you have to leave your home.", + "infoParagraph1": "cyDetails about your income and expenses can be used by a judge to reach a decision on your case, and also to decide how much you could afford to repay if you have rent arrears.", "infoParagraph2": "cyOn the day of the hearing, there may be a duty adviser at the court who can give you advice and represent you for free. They can also use this information to see if there's any additional benefits or support you might be entitled to.", + "infoParagraph3": "cyThe claimant will be able to see your answers.", "question": "cyDo you want to provide details of your income and expenses?", "options": { "yes": "cyYes", diff --git a/src/main/assets/locales/en/respondToClaim/incomeAndExpenditure.json b/src/main/assets/locales/en/respondToClaim/incomeAndExpenditure.json index 12794550e..24a05316b 100644 --- a/src/main/assets/locales/en/respondToClaim/incomeAndExpenditure.json +++ b/src/main/assets/locales/en/respondToClaim/incomeAndExpenditure.json @@ -1,8 +1,9 @@ { "caption": "Responding to the possession claim", "pageTitle": "Income and expenses", - "infoParagraph1": "Providing details about your finances could help the judge when they're deciding whether to grant the landlord a possession order, or if they can delay the date you have to leave your home.", + "infoParagraph1": "Details about your income and expenses can be used by a judge to reach a decision on your case, and also to decide how much you could afford to repay if you have rent arrears.", "infoParagraph2": "On the day of the hearing, there may be a duty adviser at the court who can give you advice and represent you for free. They can also use this information to see if there's any additional benefits or support you might be entitled to.", + "infoParagraph3": "The claimant will be able to see your answers.", "question": "Do you want to provide details of your income and expenses?", "options": { "yes": "Yes", diff --git a/src/main/steps/respond-to-claim/income-and-expenditure/incomeAndExpenditure.njk b/src/main/steps/respond-to-claim/income-and-expenditure/incomeAndExpenditure.njk index 3b3bf3892..c8974e918 100644 --- a/src/main/steps/respond-to-claim/income-and-expenditure/incomeAndExpenditure.njk +++ b/src/main/steps/respond-to-claim/income-and-expenditure/incomeAndExpenditure.njk @@ -25,6 +25,10 @@

{{ infoParagraph2 }}

{% endif %} + {% if infoParagraph3 %} +

{{ infoParagraph3 }}

+ {% endif %} +
{% for field in fields %} {% if field.componentType == 'radios' %} From 28f2fd55650dd4e29990f868ff409c7a22f013c1 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 25 Mar 2026 20:48:22 +0000 Subject: [PATCH 14/20] HDPI-3764: Refactor income and expenditure routing to use helper functions --- src/main/modules/index.ts | 17 +++++-- .../steps/respond-to-claim/flow.config.ts | 50 ++++++++----------- .../steps/utils/hasSelectedUniversalCredit.ts | 15 ++++++ src/main/steps/utils/index.ts | 5 +- .../steps/utils/isFinanceDetailsProvided.ts | 13 +++++ ...iture.ts => isFromIncomeAndExpenditure.ts} | 2 +- .../steps/utils/isUniversalCreditSelected.ts | 13 +++++ 7 files changed, 82 insertions(+), 33 deletions(-) create mode 100644 src/main/steps/utils/hasSelectedUniversalCredit.ts create mode 100644 src/main/steps/utils/isFinanceDetailsProvided.ts rename src/main/steps/utils/{cameFromIncomeAndExpenditure.ts => isFromIncomeAndExpenditure.ts} (87%) create mode 100644 src/main/steps/utils/isUniversalCreditSelected.ts diff --git a/src/main/modules/index.ts b/src/main/modules/index.ts index 2004c5d33..6dc55692e 100644 --- a/src/main/modules/index.ts +++ b/src/main/modules/index.ts @@ -1,13 +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'; -export { Csrf } from './csrf'; + +// 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', 'Csrf']; +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 82462e4a0..ac0123ef6 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -2,13 +2,16 @@ import { type Request } from 'express'; import type { JourneyFlowConfig } from '../../interfaces/stepFlow.interface'; import { - cameFromIncomeAndExpenditure, getPreviousPageForArrears, + hasSelectedUniversalCredit, isDefendantNameKnown, + isFinanceDetailsProvided, + isFromIncomeAndExpenditure, isNoticeDateProvided, isNoticeServed, isRentArrearsClaim, isTenancyStartDateKnown, + isUniversalCreditSelected, isWelshProperty, } from '../utils'; @@ -361,22 +364,14 @@ export const flowConfig: JourneyFlowConfig = { previousStep: 'exceptional-hardship', routes: [ { - condition: async ( - req: Request, - formData: Record, - currentStepData: Record - ): Promise => { - return currentStepData.provideFinanceDetails === 'no'; + condition: async (req: Request): Promise => { + return !(await isFinanceDetailsProvided(req)); }, nextStep: 'upload-docs', }, { - condition: async ( - req: Request, - formData: Record, - currentStepData: Record - ): Promise => { - return currentStepData.provideFinanceDetails === 'yes'; + condition: async (req: Request): Promise => { + return isFinanceDetailsProvided(req); }, nextStep: 'regular-income', }, @@ -387,24 +382,14 @@ export const flowConfig: JourneyFlowConfig = { previousStep: 'income-and-expenditure', routes: [ { - condition: async ( - req: Request, - formData: Record, - currentStepData: Record - ): Promise => { - const regularIncome = currentStepData.regularIncome; - return [regularIncome].flat().filter(Boolean).includes('universalCredit'); + condition: async (req: Request): Promise => { + return isUniversalCreditSelected(req); }, nextStep: 'priority-debts', }, { - condition: async ( - req: Request, - formData: Record, - currentStepData: Record - ): Promise => { - const regularIncome = currentStepData.regularIncome; - return ![regularIncome].flat().filter(Boolean).includes('universalCredit'); + condition: async (req: Request): Promise => { + return !(await isUniversalCreditSelected(req)); }, nextStep: 'have-you-applied-for-universal-credit', }, @@ -412,9 +397,14 @@ export const flowConfig: JourneyFlowConfig = { defaultNext: 'have-you-applied-for-universal-credit', }, 'have-you-applied-for-universal-credit': { + previousStep: 'regular-income', defaultNext: 'priority-debts', }, 'priority-debts': { + previousStep: async (req: Request): Promise => { + const selectedUniversalCredit = await hasSelectedUniversalCredit(req); + return selectedUniversalCredit ? 'regular-income' : 'have-you-applied-for-universal-credit'; + }, defaultNext: 'priority-debt-details', }, 'priority-debt-details': { @@ -424,10 +414,14 @@ export const flowConfig: JourneyFlowConfig = { defaultNext: 'upload-docs', }, 'upload-docs': { + previousStep: async (req: Request): Promise => { + const fromIncomeExpenditure = await isFromIncomeAndExpenditure(req); + return fromIncomeExpenditure ? 'income-and-expenditure' : 'what-other-regular-expenses-do-you-have'; + }, routes: [ { condition: async (req: Request): Promise => { - return cameFromIncomeAndExpenditure(req); + return isFromIncomeAndExpenditure(req); }, nextStep: 'income-and-expenditure', }, diff --git a/src/main/steps/utils/hasSelectedUniversalCredit.ts b/src/main/steps/utils/hasSelectedUniversalCredit.ts new file mode 100644 index 000000000..8141c0377 --- /dev/null +++ b/src/main/steps/utils/hasSelectedUniversalCredit.ts @@ -0,0 +1,15 @@ +import type { Request } from 'express'; + +/** + * Checks if the user selected Universal Credit in regular-income step. + * + * Checks if the universalCredit field exists in CCD case data. + * Returns true if universalCredit was selected, false otherwise. + */ +export const hasSelectedUniversalCredit = async (req: Request): Promise => { + const caseData = req.res?.locals?.validatedCase?.data; + const universalCredit = + caseData?.possessionClaimResponse?.defendantResponses?.householdCircumstances?.universalCredit; + + return universalCredit !== undefined; +}; diff --git a/src/main/steps/utils/index.ts b/src/main/steps/utils/index.ts index 741722f8e..6b786c1e8 100644 --- a/src/main/steps/utils/index.ts +++ b/src/main/steps/utils/index.ts @@ -4,8 +4,11 @@ export { isNoticeDateProvided } from './isNoticeDateProvided'; export { isRentArrearsClaim } from './isRentArrearsClaim'; export { isNoticeServed } from './isNoticeServed'; export { isTenancyStartDateKnown } from './isTenancyStartDateKnown'; +export { isFromIncomeAndExpenditure } from './isFromIncomeAndExpenditure'; +export { isFinanceDetailsProvided } from './isFinanceDetailsProvided'; +export { isUniversalCreditSelected } from './isUniversalCreditSelected'; +export { hasSelectedUniversalCredit } from './hasSelectedUniversalCredit'; export { getPreviousPageForArrears } from './journeyHelpers'; export { formatDatePartsToISODate } from './dateUtils'; export { toYesNoEnum, fromYesNoEnum } from './yesNoEnum'; export { penceToPounds, poundsToPence } from './currencyConversion'; -export { cameFromIncomeAndExpenditure } from './cameFromIncomeAndExpenditure'; diff --git a/src/main/steps/utils/isFinanceDetailsProvided.ts b/src/main/steps/utils/isFinanceDetailsProvided.ts new file mode 100644 index 000000000..60db45e51 --- /dev/null +++ b/src/main/steps/utils/isFinanceDetailsProvided.ts @@ -0,0 +1,13 @@ +import type { Request } from 'express'; + +/** + * Checks if the user agreed to provide finance details (income and expenses). + * + * Checks the current form submission (req.body) for the provideFinanceDetails field. + * Returns true if user selected 'yes', false otherwise. + */ +export const isFinanceDetailsProvided = async (req: Request): Promise => { + const provideFinanceDetails = req.body?.provideFinanceDetails; + + return provideFinanceDetails === 'yes'; +}; diff --git a/src/main/steps/utils/cameFromIncomeAndExpenditure.ts b/src/main/steps/utils/isFromIncomeAndExpenditure.ts similarity index 87% rename from src/main/steps/utils/cameFromIncomeAndExpenditure.ts rename to src/main/steps/utils/isFromIncomeAndExpenditure.ts index 82d7dd8a5..ff9411854 100644 --- a/src/main/steps/utils/cameFromIncomeAndExpenditure.ts +++ b/src/main/steps/utils/isFromIncomeAndExpenditure.ts @@ -7,7 +7,7 @@ import type { Request } from 'express'; * This field is only set when user completes the income-and-expenditure step. * Returns true if field exists, indicating journey came from income-and-expenditure. */ -export const cameFromIncomeAndExpenditure = async (req: Request): Promise => { +export const isFromIncomeAndExpenditure = async (req: Request): Promise => { const caseData = req.res?.locals?.validatedCase?.data; const shareIncomeExpenseDetails = caseData?.possessionClaimResponse?.defendantResponses?.householdCircumstances?.shareIncomeExpenseDetails; diff --git a/src/main/steps/utils/isUniversalCreditSelected.ts b/src/main/steps/utils/isUniversalCreditSelected.ts new file mode 100644 index 000000000..0920912c9 --- /dev/null +++ b/src/main/steps/utils/isUniversalCreditSelected.ts @@ -0,0 +1,13 @@ +import type { Request } from 'express'; + +/** + * Checks if the user selected Universal Credit in their regular income sources. + * + * Checks the current form submission (req.body) for the regularIncome field. + * Returns true if 'universalCredit' is included in the selected income sources, false otherwise. + */ +export const isUniversalCreditSelected = async (req: Request): Promise => { + const regularIncome = req.body?.regularIncome; + + return [regularIncome].flat().filter(Boolean).includes('universalCredit'); +}; From 77e39b9b98a5cf69d0de4ea4692a165381b42f8a Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 25 Mar 2026 21:03:25 +0000 Subject: [PATCH 15/20] HDPI-3764: Add end-now step to journey flow aligned with master --- src/main/steps/respond-to-claim/flow.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/steps/respond-to-claim/flow.config.ts b/src/main/steps/respond-to-claim/flow.config.ts index ac0123ef6..0d7fa1088 100644 --- a/src/main/steps/respond-to-claim/flow.config.ts +++ b/src/main/steps/respond-to-claim/flow.config.ts @@ -59,6 +59,7 @@ export const flowConfig: JourneyFlowConfig = { 'priority-debt-details', 'what-other-regular-expenses-do-you-have', 'upload-docs', + 'end-now', ], steps: { 'start-now': { @@ -426,6 +427,7 @@ export const flowConfig: JourneyFlowConfig = { nextStep: 'income-and-expenditure', }, ], + defaultNext: 'end-now', }, }, }; From 6df27abc1ce9afba7726e571503a473d4daa42f4 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 25 Mar 2026 21:11:14 +0000 Subject: [PATCH 16/20] HDPI-3764: Remove dependantChildren from HouseholdCircumstances interface --- src/main/interfaces/ccdCase.interface.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/interfaces/ccdCase.interface.ts b/src/main/interfaces/ccdCase.interface.ts index 2611cb845..5406f7122 100644 --- a/src/main/interfaces/ccdCase.interface.ts +++ b/src/main/interfaces/ccdCase.interface.ts @@ -37,7 +37,6 @@ export interface Address { } export interface HouseholdCircumstances { - dependantChildren?: YesNoValue; shareIncomeExpenseDetails?: YesNoValue; incomeFromJobs?: YesNoValue; incomeFromJobsAmount?: string; From 71be5507d8ff39e1bc9cbdf29531db44c11ded21 Mon Sep 17 00:00:00 2001 From: arun Date: Wed, 25 Mar 2026 21:13:51 +0000 Subject: [PATCH 17/20] HDPI-3764: Revert modules/index.ts to master version removing local OIDC code --- src/main/modules/index.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/main/modules/index.ts b/src/main/modules/index.ts index 6dc55692e..2004c5d33 100644 --- a/src/main/modules/index.ts +++ b/src/main/modules/index.ts @@ -1,24 +1,13 @@ -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; +export { Csrf } from './csrf'; // this is used to register the modules with the app in a certain order -export const modules = ['I18n', 'Nunjucks', 'Helmet', 'Session', 'S2S', 'OIDCModule', 'LaunchDarkly']; +export const modules = ['I18n', 'Nunjucks', 'Helmet', 'Session', 'S2S', 'OIDCModule', 'LaunchDarkly', 'Csrf']; From 5d07c1c9c94001875192c9119efb94045c86a8ad Mon Sep 17 00:00:00 2001 From: arun Date: Thu, 26 Mar 2026 15:28:53 +0000 Subject: [PATCH 18/20] HDPI-3764: Change frequency values from week/month to WEEKLY/MONTHLY --- .../respond-to-claim/regular-income/index.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/steps/respond-to-claim/regular-income/index.ts b/src/main/steps/respond-to-claim/regular-income/index.ts index 79ba9731d..3a9ca806e 100644 --- a/src/main/steps/respond-to-claim/regular-income/index.ts +++ b/src/main/steps/respond-to-claim/regular-income/index.ts @@ -74,7 +74,7 @@ export const step: StepDefinition = createFormStep({ formData['regularIncome.incomeFromJobsAmount'] = penceToPounds(hc.incomeFromJobsAmount as string); } if (hc.incomeFromJobsFrequency) { - formData['regularIncome.incomeFromJobsFrequency'] = (hc.incomeFromJobsFrequency as string).toLowerCase(); + formData['regularIncome.incomeFromJobsFrequency'] = hc.incomeFromJobsFrequency; } } @@ -85,7 +85,7 @@ export const step: StepDefinition = createFormStep({ formData['regularIncome.pensionAmount'] = penceToPounds(hc.pensionAmount as string); } if (hc.pensionFrequency) { - formData['regularIncome.pensionFrequency'] = (hc.pensionFrequency as string).toLowerCase(); + formData['regularIncome.pensionFrequency'] = hc.pensionFrequency; } } @@ -96,7 +96,7 @@ export const step: StepDefinition = createFormStep({ formData['regularIncome.universalCreditAmount'] = penceToPounds(hc.universalCreditAmount as string); } if (hc.universalCreditFrequency) { - formData['regularIncome.universalCreditFrequency'] = (hc.universalCreditFrequency as string).toLowerCase(); + formData['regularIncome.universalCreditFrequency'] = hc.universalCreditFrequency; } } @@ -107,7 +107,7 @@ export const step: StepDefinition = createFormStep({ formData['regularIncome.otherBenefitsAmount'] = penceToPounds(hc.otherBenefitsAmount as string); } if (hc.otherBenefitsFrequency) { - formData['regularIncome.otherBenefitsFrequency'] = (hc.otherBenefitsFrequency as string).toLowerCase(); + formData['regularIncome.otherBenefitsFrequency'] = hc.otherBenefitsFrequency; } } @@ -142,7 +142,7 @@ export const step: StepDefinition = createFormStep({ householdCircumstances.incomeFromJobsAmount = poundsToPence(amountRaw); } if (frequency) { - householdCircumstances.incomeFromJobsFrequency = frequency.toUpperCase(); + householdCircumstances.incomeFromJobsFrequency = frequency; } } @@ -156,7 +156,7 @@ export const step: StepDefinition = createFormStep({ householdCircumstances.pensionAmount = poundsToPence(amountRaw); } if (frequency) { - householdCircumstances.pensionFrequency = frequency.toUpperCase(); + householdCircumstances.pensionFrequency = frequency; } } @@ -170,7 +170,7 @@ export const step: StepDefinition = createFormStep({ householdCircumstances.universalCreditAmount = poundsToPence(amountRaw); } if (frequency) { - householdCircumstances.universalCreditFrequency = frequency.toUpperCase(); + householdCircumstances.universalCreditFrequency = frequency; } } @@ -184,7 +184,7 @@ export const step: StepDefinition = createFormStep({ householdCircumstances.otherBenefitsAmount = poundsToPence(amountRaw); } if (frequency) { - householdCircumstances.otherBenefitsFrequency = frequency.toUpperCase(); + householdCircumstances.otherBenefitsFrequency = frequency; } } @@ -255,8 +255,8 @@ export const step: StepDefinition = createFormStep({ label: 'subFields.frequency', }, options: [ - { value: 'week', translationKey: 'frequency.week' }, - { value: 'month', translationKey: 'frequency.month' }, + { value: 'WEEKLY', translationKey: 'frequency.week' }, + { value: 'MONTHLY', translationKey: 'frequency.month' }, ], }, }, @@ -293,8 +293,8 @@ export const step: StepDefinition = createFormStep({ label: 'subFields.frequency', }, options: [ - { value: 'week', translationKey: 'frequency.week' }, - { value: 'month', translationKey: 'frequency.month' }, + { value: 'WEEKLY', translationKey: 'frequency.week' }, + { value: 'MONTHLY', translationKey: 'frequency.month' }, ], }, }, @@ -331,8 +331,8 @@ export const step: StepDefinition = createFormStep({ label: 'subFields.frequency', }, options: [ - { value: 'week', translationKey: 'frequency.week' }, - { value: 'month', translationKey: 'frequency.month' }, + { value: 'WEEKLY', translationKey: 'frequency.week' }, + { value: 'MONTHLY', translationKey: 'frequency.month' }, ], }, }, @@ -369,8 +369,8 @@ export const step: StepDefinition = createFormStep({ label: 'subFields.frequency', }, options: [ - { value: 'week', translationKey: 'frequency.week' }, - { value: 'month', translationKey: 'frequency.month' }, + { value: 'WEEKLY', translationKey: 'frequency.week' }, + { value: 'MONTHLY', translationKey: 'frequency.month' }, ], }, }, From aeefb8e110945429e66546a50d93870709bf67f0 Mon Sep 17 00:00:00 2001 From: arun Date: Fri, 27 Mar 2026 16:35:57 +0000 Subject: [PATCH 19/20] HDPI-3764: Standardize Yes/No enum format and add type safety for monetary and frequency values --- src/main/constants/validation.ts | 10 ++++++ src/main/interfaces/ccdCase.interface.ts | 21 +++++++------ .../contact-preferences-telephone/index.ts | 3 +- .../contact-preferences-text-message/index.ts | 3 +- .../income-and-expenditure/index.ts | 1 + .../respond-to-claim/regular-income/index.ts | 27 +++++++--------- src/main/steps/utils/yesNoEnum.ts | 31 ++++++------------- 7 files changed, 49 insertions(+), 47 deletions(-) create mode 100644 src/main/constants/validation.ts diff --git a/src/main/constants/validation.ts b/src/main/constants/validation.ts new file mode 100644 index 000000000..bcd4d9933 --- /dev/null +++ b/src/main/constants/validation.ts @@ -0,0 +1,10 @@ +/** + * Validation constants for income and expenditure forms. + * Shared across all income/expense related steps. + */ + +/** Maximum income/expense amount: £1 billion in pence */ +export const MAX_INCOME_AMOUNT = 1_000_000_000; + +/** Amount format: up to 10 digits, exactly 2 decimal places */ +export const AMOUNT_FORMAT_REGEX = /^\d{1,10}\.\d{2}$/; diff --git a/src/main/interfaces/ccdCase.interface.ts b/src/main/interfaces/ccdCase.interface.ts index 05e2489b9..92eafa11a 100644 --- a/src/main/interfaces/ccdCase.interface.ts +++ b/src/main/interfaces/ccdCase.interface.ts @@ -3,11 +3,14 @@ export enum CaseState { SUBMITTED = 'Submitted', } -export type YesNoValue = 'YES' | 'NO' | null; +export type YesNoValue = 'Yes' | 'No' | null; export type ContactPreference = 'EMAIL' | 'POST' | null; export type YesNoNotSureValue = 'YES' | 'NO' | 'NOT_SURE'; +export type FrequencyValue = 'WEEKLY' | 'MONTHLY'; +export type PenceAmount = string; + export interface CcdUserCase { id: string; state: CaseState; @@ -39,18 +42,18 @@ export interface Address { export interface HouseholdCircumstances { shareIncomeExpenseDetails?: YesNoValue; incomeFromJobs?: YesNoValue; - incomeFromJobsAmount?: string; - incomeFromJobsFrequency?: string; + incomeFromJobsAmount?: PenceAmount; + incomeFromJobsFrequency?: FrequencyValue; pension?: YesNoValue; - pensionAmount?: string; - pensionFrequency?: string; + pensionAmount?: PenceAmount; + pensionFrequency?: FrequencyValue; universalCredit?: YesNoValue; - universalCreditAmount?: string; - universalCreditFrequency?: string; + universalCreditAmount?: PenceAmount; + universalCreditFrequency?: FrequencyValue; ucApplicationDate?: string; otherBenefits?: YesNoValue; - otherBenefitsAmount?: string; - otherBenefitsFrequency?: string; + otherBenefitsAmount?: PenceAmount; + otherBenefitsFrequency?: FrequencyValue; moneyFromElsewhere?: YesNoValue; moneyFromElsewhereDetails?: string; } diff --git a/src/main/steps/respond-to-claim/contact-preferences-telephone/index.ts b/src/main/steps/respond-to-claim/contact-preferences-telephone/index.ts index 49d9d5d8e..5541ea38c 100644 --- a/src/main/steps/respond-to-claim/contact-preferences-telephone/index.ts +++ b/src/main/steps/respond-to-claim/contact-preferences-telephone/index.ts @@ -1,6 +1,7 @@ import type { PossessionClaimResponse } from '../../../interfaces/ccdCase.interface'; import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; import { createFormStep } from '../../../modules/steps'; +import { toYesNoEnum } from '../../utils'; import { buildCcdCaseForPossessionClaimResponse as buildAndSubmitPossessionClaimResponse } from '../../utils/populateResponseToClaimPayloadmap'; import { flowConfig } from '../flow.config'; @@ -94,7 +95,7 @@ export const step: StepDefinition = createFormStep({ }, }, defendantResponses: { - contactByPhone: telephoneForm.contactByTelephone === 'yes' ? 'YES' : 'NO', + contactByPhone: toYesNoEnum(telephoneForm.contactByTelephone), }, }; diff --git a/src/main/steps/respond-to-claim/contact-preferences-text-message/index.ts b/src/main/steps/respond-to-claim/contact-preferences-text-message/index.ts index 8a76680b1..3844b8bef 100644 --- a/src/main/steps/respond-to-claim/contact-preferences-text-message/index.ts +++ b/src/main/steps/respond-to-claim/contact-preferences-text-message/index.ts @@ -1,6 +1,7 @@ import type { PossessionClaimResponse } from '../../../interfaces/ccdCase.interface'; import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; import { createFormStep } from '../../../modules/steps'; +import { toYesNoEnum } from '../../utils'; import { buildCcdCaseForPossessionClaimResponse as buildAndSubmitPossessionClaimResponse } from '../../utils/populateResponseToClaimPayloadmap'; import { flowConfig } from '../flow.config'; @@ -47,7 +48,7 @@ export const step: StepDefinition = createFormStep({ const possessionClaimResponse: PossessionClaimResponse = { defendantResponses: { - contactByText: textForm.contactByTextMessage === 'yes' ? 'YES' : 'NO', + contactByText: toYesNoEnum(textForm.contactByTextMessage), }, }; diff --git a/src/main/steps/respond-to-claim/income-and-expenditure/index.ts b/src/main/steps/respond-to-claim/income-and-expenditure/index.ts index 75b3579da..6088771ba 100644 --- a/src/main/steps/respond-to-claim/income-and-expenditure/index.ts +++ b/src/main/steps/respond-to-claim/income-and-expenditure/index.ts @@ -42,6 +42,7 @@ export const step: StepDefinition = createFormStep({ pageTitle: 'pageTitle', infoParagraph1: 'infoParagraph1', infoParagraph2: 'infoParagraph2', + infoParagraph3: 'infoParagraph3', question: 'question', }, diff --git a/src/main/steps/respond-to-claim/regular-income/index.ts b/src/main/steps/respond-to-claim/regular-income/index.ts index 3a9ca806e..f8b5bdf15 100644 --- a/src/main/steps/respond-to-claim/regular-income/index.ts +++ b/src/main/steps/respond-to-claim/regular-income/index.ts @@ -1,17 +1,14 @@ import type { Request } from 'express'; +import { AMOUNT_FORMAT_REGEX, MAX_INCOME_AMOUNT } from '../../../constants/validation'; import type { PossessionClaimResponse } from '../../../interfaces/ccdCase.interface'; import type { StepDefinition } from '../../../interfaces/stepFormData.interface'; -import { penceToPounds, poundsToPence, toYesNoEnum } from '../../utils'; +import { fromYesNoEnum, penceToPounds, poundsToPence, toYesNoEnum } from '../../utils'; import { buildCcdCaseForPossessionClaimResponse } from '../../utils/populateResponseToClaimPayloadmap'; import { flowConfig } from '../flow.config'; import { createFormStep } from '@modules/steps'; -// Validation constants (copied from rent-arrears-dispute) -const MAX_INCOME_AMOUNT = 1_000_000_000; // £1 billion maximum -const AMOUNT_FORMAT_REGEX = /^\d{1,10}\.\d{2}$/; // Up to 10 digits, exactly 2 decimal places - const createAmountValidator = (largeAmountErrorKey: string) => (value: unknown): boolean | string => { @@ -67,8 +64,8 @@ export const step: StepDefinition = createFormStep({ const formData: Record = {}; const selectedIncome: string[] = []; - // Income from jobs - Case-insensitive check to handle backend variations (YES/Yes/yes) - if (hc.incomeFromJobs && (hc.incomeFromJobs as string).toUpperCase() === 'YES') { + // Income from jobs + if (fromYesNoEnum(hc.incomeFromJobs) === 'yes') { selectedIncome.push('incomeFromJobs'); if (hc.incomeFromJobsAmount) { formData['regularIncome.incomeFromJobsAmount'] = penceToPounds(hc.incomeFromJobsAmount as string); @@ -78,8 +75,8 @@ export const step: StepDefinition = createFormStep({ } } - // Pension - Case-insensitive check to handle backend variations (YES/Yes/yes) - if (hc.pension && (hc.pension as string).toUpperCase() === 'YES') { + // Pension + if (fromYesNoEnum(hc.pension) === 'yes') { selectedIncome.push('pension'); if (hc.pensionAmount) { formData['regularIncome.pensionAmount'] = penceToPounds(hc.pensionAmount as string); @@ -89,8 +86,8 @@ export const step: StepDefinition = createFormStep({ } } - // Universal Credit - Case-insensitive check to handle backend variations (YES/Yes/yes) - if (hc.universalCredit && (hc.universalCredit as string).toUpperCase() === 'YES') { + // Universal Credit + if (fromYesNoEnum(hc.universalCredit) === 'yes') { selectedIncome.push('universalCredit'); if (hc.universalCreditAmount) { formData['regularIncome.universalCreditAmount'] = penceToPounds(hc.universalCreditAmount as string); @@ -100,8 +97,8 @@ export const step: StepDefinition = createFormStep({ } } - // Other benefits - Case-insensitive check to handle backend variations (YES/Yes/yes) - if (hc.otherBenefits && (hc.otherBenefits as string).toUpperCase() === 'YES') { + // Other benefits + if (fromYesNoEnum(hc.otherBenefits) === 'yes') { selectedIncome.push('otherBenefits'); if (hc.otherBenefitsAmount) { formData['regularIncome.otherBenefitsAmount'] = penceToPounds(hc.otherBenefitsAmount as string); @@ -111,8 +108,8 @@ export const step: StepDefinition = createFormStep({ } } - // Money from elsewhere - Case-insensitive check to handle backend variations (YES/Yes/yes) - if (hc.moneyFromElsewhere && (hc.moneyFromElsewhere as string).toUpperCase() === 'YES') { + // Money from elsewhere + if (fromYesNoEnum(hc.moneyFromElsewhere) === 'yes') { selectedIncome.push('moneyFromElsewhere'); if (hc.moneyFromElsewhereDetails) { formData['regularIncome.moneyFromElsewhereDetails'] = hc.moneyFromElsewhereDetails; diff --git a/src/main/steps/utils/yesNoEnum.ts b/src/main/steps/utils/yesNoEnum.ts index d2c300b36..bed5a2121 100644 --- a/src/main/steps/utils/yesNoEnum.ts +++ b/src/main/steps/utils/yesNoEnum.ts @@ -1,43 +1,32 @@ /** - * Utility functions for converting between frontend yes/no values and backend YES/NO enums. + * Utility functions for converting between frontend yes/no values and backend Yes/No enums. * Used for CCD API integration where boolean choices are represented as enum strings. */ import type { YesNoValue } from '../../interfaces/ccdCase.interface'; /** - * Converts frontend 'yes'/'no' string to backend CCD enum 'YES'/'NO' - * Case-insensitive conversion to handle any casing from user input - * @param value - Frontend radio button value ('yes' or 'no') - * @returns CCD enum value ('YES' or 'NO') - * @example - * toYesNoEnum('yes') // returns 'YES' - * toYesNoEnum('Yes') // returns 'YES' - * toYesNoEnum('no') // returns 'NO' - * toYesNoEnum('NO') // returns 'NO' + * Converts frontend 'yes'/'no' to backend 'Yes'/'No' enum. + * @example toYesNoEnum('yes') // returns 'Yes' */ export function toYesNoEnum(value: 'yes' | 'no'): YesNoValue { - return value.toLowerCase() === 'yes' ? 'YES' : 'NO'; + return value.toLowerCase() === 'yes' ? 'Yes' : 'No'; } /** - * Converts backend CCD enum 'YES'/'NO' to frontend 'yes'/'no' string - * @param value - CCD enum value ('YES' or 'NO') - * @returns Frontend radio button value ('yes' or 'no'), or undefined if value is null/invalid - * @example - * fromYesNoEnum('YES') // returns 'yes' - * fromYesNoEnum('NO') // returns 'no' - * fromYesNoEnum(null) // returns undefined + * Converts backend 'Yes'/'No' enum to frontend 'yes'/'no'. + * Case-insensitive for backward compatibility. + * @example fromYesNoEnum('Yes') // returns 'yes' */ export function fromYesNoEnum(value: YesNoValue | string | undefined): 'yes' | 'no' | undefined { if (!value) { return undefined; } - const upperValue = value.toUpperCase(); - if (upperValue === 'YES') { + const normalizedValue = value.charAt(0).toUpperCase() + value.slice(1).toLowerCase(); + if (normalizedValue === 'Yes') { return 'yes'; } - if (upperValue === 'NO') { + if (normalizedValue === 'No') { return 'no'; } return undefined; From f401783866e61f7ac3895584fcb09698972c9f5b Mon Sep 17 00:00:00 2001 From: arun Date: Fri, 27 Mar 2026 17:01:01 +0000 Subject: [PATCH 20/20] HDPI-3764: Update tenancy-type-details tests to use Yes/No standardized format --- .../respond-to-claim/tenancy-type-details.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/unit/steps/respond-to-claim/tenancy-type-details.test.ts b/src/test/unit/steps/respond-to-claim/tenancy-type-details.test.ts index 39fd5ec87..ed1064ea1 100644 --- a/src/test/unit/steps/respond-to-claim/tenancy-type-details.test.ts +++ b/src/test/unit/steps/respond-to-claim/tenancy-type-details.test.ts @@ -70,16 +70,16 @@ describe('respond-to-claim tenancy-type-details step', () => { }); it.each([ - ['YES', 'yes'], - ['NO', 'no'], + ['Yes', 'yes'], + ['No', 'no'], ['NOT_SURE', 'notSure'], ])('returns tenancyTypeConfirm=%s when CCD has %s', (ccdValue, formValue) => { const result = testedStep.getInitialFormData(makeReq(ccdValue)); expect(result).toMatchObject({ tenancyTypeConfirm: formValue }); }); - it('also returns correctType when CCD value is NO and tenancyType is set', () => { - const result = testedStep.getInitialFormData(makeReq('NO', 'Assured shorthold')); + it('also returns correctType when CCD value is No and tenancyType is set', () => { + const result = testedStep.getInitialFormData(makeReq('No', 'Assured shorthold')); expect(result).toEqual({ tenancyTypeConfirm: 'no', 'tenancyTypeConfirm.correctType': 'Assured shorthold', @@ -99,8 +99,8 @@ describe('respond-to-claim tenancy-type-details step', () => { describe('beforeRedirect', () => { it.each([ - ['yes', 'YES'], - ['no', 'NO'], + ['yes', 'Yes'], + ['no', 'No'], ['notSure', 'NOT_SURE'], ['maybe', undefined], [undefined, undefined],