Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0ce4321
HDPI-3451: Add rent arrears dispute step with framework validation pa…
arun-hmcts Mar 8, 2026
5f85467
HDPI-3451: Fix grammar in rent arrears translation - change 'received…
arun-hmcts Mar 9, 2026
c9b3b2b
HDPI-3451: Fix rent-arrears-dispute subField data persistence using d…
arun-hmcts Mar 9, 2026
26f4b2e
HDPI-3451: Merge defendant-name-dob-persistence branch with rent arre…
arun-hmcts Mar 11, 2026
ee882f4
HDPI-3451: Add forward routing logic to rent-arrears-dispute step for…
arun-hmcts Mar 11, 2026
e8bdc5e
HDPI-3451: Merge latest updates from defendant-name-dob-persistence b…
arun-hmcts Mar 11, 2026
f4274fc
HDPI-3451: Refactor test mocks to use reusable createMockT factory fo…
arun-hmcts Mar 11, 2026
457892e
HDPI-3495: Remove getClaimantName utility and use inline pattern cons…
arun-hmcts Mar 11, 2026
5105286
HDPI-3495: Fix logger issues - remove PII logging and reduce producti…
arun-hmcts Mar 11, 2026
eb8b631
HDPI-3549: Implement non-rent arrears dispute screen with CCD persist…
arun-hmcts Mar 11, 2026
5a8b292
HDPI-3549: Remove rows property from character-count field config
arun-hmcts Mar 11, 2026
ce3d607
HDPI-3549: Fix template button text to use variables instead of trans…
arun-hmcts Mar 11, 2026
e8820af
HDPI-3549: Implement non-rent-arrears-dispute page with GOV.UK Design…
arun-hmcts Mar 12, 2026
9179eaa
HDPI-3549: Revert OIDC local development changes (unrelated to feature)
arun-hmcts Mar 12, 2026
9db7ed8
HDPI-3549: Revert index.ts to production version (remove local dev OI…
arun-hmcts Mar 12, 2026
5c1dbac
Merge branch 'HDPI-3451-defendant-name-dob-persistence' into HDPI-349…
arun-hmcts Mar 12, 2026
f31bdb6
HDPI-3495: Fix rent arrears routing and back button navigation for mi…
arun-hmcts Mar 12, 2026
700432d
HDPI-3495: Rename helper functions for better readability
arun-hmcts Mar 13, 2026
7932d16
HDPI-3495: Simplify hasOnlyRentArrearsGrounds tests by removing redun…
arun-hmcts Mar 13, 2026
a059ef2
HDPI-3495: Merge HDPI-3451 defendant-name-dob-persistence branch - pr…
arun-hmcts Mar 13, 2026
ad8d3d7
HDPI-3495: Update journeyHelpers tests to match master's new tenancy …
arun-hmcts Mar 13, 2026
063a79b
HDPI-3495: Replace isRentArrearsClaim with hasAnyRentArrearsGround in…
arun-hmcts Mar 13, 2026
3fadc30
Merge branch 'HDPI-3451-defendant-name-dob-persistence' into HDPI-349…
AshaJayaprakash Mar 13, 2026
2ffa360
Automation HDPI-2495
AshaJayaprakash Mar 13, 2026
a7eb4b1
HDPI-3495: Make jest testPathIgnorePatterns more specific to avoid hi…
arun-hmcts Mar 13, 2026
e8592b9
HDPI-3495: Use specific filename pattern instead of directory to avoi…
arun-hmcts Mar 13, 2026
8726319
HDPI-3495: Merge HDPI-3549 non-rent-arrears-dispute implementation wi…
arun-hmcts Mar 13, 2026
2823eac
HDPI-3495: Merge HDPI-3451 defendant-name-dob-persistence base branch…
arun-hmcts Mar 13, 2026
e810591
HDPI-3495: Revert non-rent-arrears-dispute to placeholder version
arun-hmcts Mar 13, 2026
48cf8ea
Merge branch 'HDPI-3495-rent-arrears-frontend' into TestAutomation-HD…
AshaJayaprakash Mar 16, 2026
86bd3d2
Automation HDPI-2495
AshaJayaprakash Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports = {
'^jose$': '<rootDir>/src/test/unit/modules/s2s/__mocks__/jose.ts',
'^uuid$': '<rootDir>/src/test/unit/__mocks__/uuid.ts',
},
testPathIgnorePatterns: ['/__mocks__/'],
testPathIgnorePatterns: ['/__mocks__/', '/mockTranslation.ts$'],
coverageProvider: 'v8',
transformIgnorePatterns: ['node_modules/(?!(jose|@panva|oidc-token-hash)/)'],
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
{
"caption": "cyRespond to a property possession claim",
"pageTitle": "cyRent Arrears Dispute(Placeholder)"
"title": "cyRent arrears",
"captionHeading": "cyRespond to a property possession claim",
"pageTitle": "cyRent arrears",
"insetIntroText": "cyRent arrears are money you owe in rent payments.",
"insetDetailsText": "cyWhen making their claim, {{claimantName}} had to provide a copy of the rent statement for your property, showing the total rent arrears you owe.",
"insetConditionalYesText": "cyThe rent statement will have been included in the pack you received in the post letting you know a claim had been made against you, and is also available to view from 'View the claim' on your case dashboard.",
"amountOwedHeading": "cyAmount you owe in rent arrears given by {{claimantName}}:",
"rentArrearsQuestion": "cyDo you owe this amount in rent arrears?",
"rentArrearsOptions": {
"yes": "cyYes",
"no": "cyNo",
"or": "cyor",
"notSure": "cyI'm not sure"
},
"rentArrearsAmountCorrection": {
"label": "cyWhat amount do you believe you owe?",
"hint": "cyEnter the amount in pounds and pence, for example 123.45"
},
"errors": {
"rentArrears": {
"required": "cySelect yes if you owe this amount in rent arrears"
},
"rentArrearsAmountCorrection": {
"required": "cyEnter the amount you believe you owe in rent arrears",
"negativeAmount": "cyThe amount you believe you owe in rent arrears must be £0.00 or above",
"largeAmount": "cyEnter how much you believe you owe in rent arrears, in the correct format, up to £1,000,000,000.00",
"invalidFormat": "cyEnter how much you believe you owe in rent arrears, in the correct format (e.g. if you owe £148, please write £148.00)"
},
"rentArrears.rentArrearsAmountCorrection": "cyEnter the amount you believe you owe in rent arrears"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
{
"caption": "Respond to a property possession claim",
"pageTitle": "Rent Arrears Dispute(Placeholder)"
"title": "Rent arrears",
"captionHeading": "Respond to a property possession claim",
"pageTitle": "Rent arrears",
"insetIntroText": "Rent arrears are money you owe in rent payments.",
"insetDetailsText": "When making their claim, {{claimantName}} had to provide a copy of the rent statement for your property, showing the total rent arrears you owe.",
"insetConditionalYesText": "The rent statement will have been included in the pack you received in the post letting you know a claim had been made against you, and is also available to view from 'View the claim' on your case dashboard.",
"amountOwedHeading": "Amount you owe in rent arrears given by {{claimantName}}:",
"rentArrearsQuestion": "Do you owe this amount in rent arrears?",
"rentArrearsOptions": {
"yes": "Yes",
"no": "No",
"or": "or",
"notSure": "I'm not sure"
},
"rentArrearsAmountCorrection": {
"label": "What amount do you believe you owe?",
"hint": "Enter the amount in pounds and pence, for example 123.45"
},
"errors": {
"rentArrears": {
"required": "Select yes if you owe this amount in rent arrears"
},
"rentArrearsAmountCorrection": {
"required": "Enter the amount you believe you owe in rent arrears",
"negativeAmount": "The amount you believe you owe in rent arrears must be £0.00 or above",
"largeAmount": "Enter how much you believe you owe in rent arrears, in the correct format, up to £1,000,000,000.00",
"invalidFormat": "Enter how much you believe you owe in rent arrears, in the correct format (e.g. if you owe £148, please write £148.00)"
},
"rentArrears.rentArrearsAmountCorrection": "Enter the amount you believe you owe in rent arrears"
}
}
4 changes: 4 additions & 0 deletions src/main/interfaces/formFieldConfig.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export interface FormFieldConfig {
options?: FormFieldOption[];
classes?: string;
attributes?: Record<string, unknown>;
// Optional prefix for input fields (e.g. currency symbol)
prefix?: {
text: string;
};
legendClasses?: string;
// Pre-built component config for Nunjucks template rendering
component?: Record<string, unknown>;
Expand Down
3 changes: 2 additions & 1 deletion src/main/middleware/autoSaveDraftToCCD.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ async function saveToCCD(
}

try {
logger.debug(`[${stepName}] Auto-saving to CCD draft`);
logger.debug(`[${stepName}] Starting auto-save with ${Object.keys(formData).length} fields`);

let relevantData: string | string[] | Record<string, unknown>;
if (ccdMapping.frontendField) {
Expand Down Expand Up @@ -246,6 +246,7 @@ async function saveToCCD(
const ccdPayload = {
...nestedData,
};
logger.debug(`[${stepName}] Sending CCD payload for case ${validatedCase.id}`);

await ccdCaseService.updateDraftRespondToClaim(accessToken, validatedCase.id, ccdPayload);

Expand Down
47 changes: 35 additions & 12 deletions src/main/modules/steps/formBuilder/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,10 @@ export function normalizeCheckboxFields(req: Request, fields: FormFieldConfig[])
/**
* Processes all field data (checkbox normalization + date field consolidation)
* This should run AFTER validation because date field validation expects individual day/month/year keys
* Now also handles subFields within radio/checkbox options
*/
export function processFieldData(req: Request, fields: FormFieldConfig[]): void {
for (const field of fields) {
const processField = (field: FormFieldConfig): void => {
if (field.type === 'checkbox') {
// Normalize checkbox values (in case they weren't normalized before validation)
req.body[field.name] = normalizeCheckboxValue(req.body[field.name]);
Expand All @@ -95,6 +96,22 @@ export function processFieldData(req: Request, fields: FormFieldConfig[]): void
delete req.body[`${field.name}-month`];
delete req.body[`${field.name}-year`];
}

// Process subFields if this is a radio or checkbox with options
if ((field.type === 'radio' || field.type === 'checkbox') && field.options) {
for (const option of field.options) {
if (option.subFields) {
for (const subField of Object.values(option.subFields)) {
// Recursively process subFields (they might be dates or checkboxes too)
processField(subField);
}
}
}
}
};

for (const field of fields) {
processField(field);
}
}

Expand Down Expand Up @@ -173,7 +190,6 @@ export function getTranslationErrors(
export function getCustomErrorTranslations(t: TFunction, fields: FormFieldConfig[]): Record<string, string> {
const stepSpecificErrors: Record<string, string> = {};

const nestedKeys = ['required', 'custom', 'missingOne', 'missingTwo', 'futureDate'];
const commonErrorKeys = ['defaultRequired', 'defaultInvalid', 'defaultMaxLength'];

for (const key of commonErrorKeys) {
Expand All @@ -196,19 +212,26 @@ export function getCustomErrorTranslations(t: TFunction, fields: FormFieldConfig
const visitField = (field: FormFieldConfig): void => {
addMaxLengthTranslation(field.name);

// Keep existing nested error support for non-nested names (e.g., date fields)
// Auto-discover all error translations for this field from the step's translation file
// This eliminates the need for a hardcoded nestedKeys list
if (!field.name.includes('.')) {
for (const nestedKey of nestedKeys) {
const nestedErrorKey = `errors.${field.name}.${nestedKey}`;
const nestedError = t(nestedErrorKey);
if (nestedError && nestedError !== nestedErrorKey) {
if (field.type === 'date') {
const dateKey = getDateTranslationKey(nestedKey);
if (dateKey) {
stepSpecificErrors[dateKey] = nestedError;
const errorNamespace = `errors.${field.name}`;
const allFieldErrors = t(errorNamespace, { returnObjects: true });

// If the translation exists and is an object (not a string), it contains error keys
if (allFieldErrors && typeof allFieldErrors === 'object' && !Array.isArray(allFieldErrors)) {
for (const [errorKey, errorMessage] of Object.entries(allFieldErrors)) {
if (typeof errorMessage === 'string') {
// Special handling for date fields - map to date-specific keys
if (field.type === 'date') {
const dateKey = getDateTranslationKey(errorKey);
if (dateKey) {
stepSpecificErrors[dateKey] = errorMessage;
}
}
// Add the standard field.errorKey mapping
stepSpecificErrors[`${field.name}.${errorKey}`] = errorMessage;
}
stepSpecificErrors[`${field.name}.${nestedKey}`] = nestedError;
}
}
}
Expand Down
10 changes: 6 additions & 4 deletions src/main/services/ccdCaseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ interface EventTokenResponse {
}

function getBaseUrl(): string {
return config.get('ccd.url');
return config.get<string>('ccd.url');
}

function getApiUrl(): string {
Expand Down Expand Up @@ -163,21 +163,23 @@ async function submitEvent(

export const ccdCaseService = {
async getCaseById(accessToken: string, caseId: string, eventId: string = 'respondPossessionClaim'): Promise<CcdCase> {
const eventUrl = `${getBaseUrl()}/cases/${caseId}/event-triggers/${eventId}?ignore-warning=false`;
const ccdUrl = getBaseUrl();
const eventUrl = `${ccdUrl}/cases/${caseId}/event-triggers/${eventId}?ignore-warning=false`;

try {
logger.info(`[ccdCaseService] Validating case access for caseId: ${caseId}, eventId: ${eventId}`);
logger.debug(`[ccdCaseService] Validating case ${caseId} for event ${eventId}`);

const response = await http.get<{ case_details?: { case_data?: Record<string, unknown> } }>(
eventUrl,
getCaseHeaders(accessToken)
);
logger.info(`[ccdCaseService] Case access validated successfully for caseId: ${caseId}`);

return {
id: caseId,
data: response.data.case_details?.case_data || {},
};
} catch (error) {
logger.error('[ccdCaseService] getCaseById failed:', error);
throw convertAxiosErrorToHttpError(error, 'getCaseById');
}
},
Expand Down
52 changes: 33 additions & 19 deletions src/main/steps/respond-to-claim/flow.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { type Request } from 'express';

import type { JourneyFlowConfig } from '../../interfaces/stepFlow.interface';
import {
getPreviousPageForArrears,
getPreviousNoticeStep,
hasAnyRentArrearsGround,
hasOnlyRentArrearsGrounds,
isDefendantNameKnown,
isNoticeDateProvided,
isNoticeServed,
isRentArrearsClaim,
isTenancyStartDateKnown,
isWelshProperty,
} from '../utils';
Expand Down Expand Up @@ -162,14 +163,14 @@ export const flowConfig: JourneyFlowConfig = {
},
{
condition: async (req: Request): Promise<boolean> => {
const rentArrears = await isRentArrearsClaim(req);
const rentArrears = await hasAnyRentArrearsGround(req);
return rentArrears;
},
nextStep: 'rent-arrears-dispute',
},
{
condition: async (req: Request): Promise<boolean> => {
const rentArrears = await isRentArrearsClaim(req);
const rentArrears = await hasAnyRentArrearsGround(req);
return !rentArrears;
},
nextStep: 'non-rent-arrears-dispute',
Expand All @@ -185,14 +186,14 @@ export const flowConfig: JourneyFlowConfig = {
},
{
condition: async (req: Request): Promise<boolean> => {
const rentArrears = await isRentArrearsClaim(req);
const rentArrears = await hasAnyRentArrearsGround(req);
return rentArrears;
},
nextStep: 'rent-arrears-dispute',
},
{
condition: async (req: Request): Promise<boolean> => {
const rentArrears = await isRentArrearsClaim(req);
const rentArrears = await hasAnyRentArrearsGround(req);
return !rentArrears;
},
nextStep: 'non-rent-arrears-dispute',
Expand Down Expand Up @@ -230,7 +231,7 @@ export const flowConfig: JourneyFlowConfig = {
if (confirmNoticeGiven !== 'no' && confirmNoticeGiven !== 'imNotSure') {
return false;
}
const rentArrears = await isRentArrearsClaim(req);
const rentArrears = await hasAnyRentArrearsGround(req);
return rentArrears;
},
nextStep: 'rent-arrears-dispute',
Expand All @@ -241,7 +242,7 @@ export const flowConfig: JourneyFlowConfig = {
if (confirmNoticeGiven !== 'no' && confirmNoticeGiven !== 'imNotSure') {
return false;
}
const rentArrears = await isRentArrearsClaim(req);
const rentArrears = await hasAnyRentArrearsGround(req);
return !rentArrears;
},
nextStep: 'non-rent-arrears-dispute',
Expand All @@ -257,14 +258,14 @@ export const flowConfig: JourneyFlowConfig = {
routes: [
{
condition: async (req: Request): Promise<boolean> => {
const rentArrears = await isRentArrearsClaim(req);
const rentArrears = await hasAnyRentArrearsGround(req);
return rentArrears;
},
nextStep: 'rent-arrears-dispute',
},
{
condition: async (req: Request): Promise<boolean> => {
const rentArrears = await isRentArrearsClaim(req);
const rentArrears = await hasAnyRentArrearsGround(req);
return !rentArrears;
},
nextStep: 'non-rent-arrears-dispute',
Expand All @@ -276,14 +277,14 @@ export const flowConfig: JourneyFlowConfig = {
routes: [
{
condition: async (req: Request): Promise<boolean> => {
const rentArrears = await isRentArrearsClaim(req);
const rentArrears = await hasAnyRentArrearsGround(req);
return rentArrears;
},
nextStep: 'rent-arrears-dispute',
},
{
condition: async (req: Request): Promise<boolean> => {
const rentArrears = await isRentArrearsClaim(req);
const rentArrears = await hasAnyRentArrearsGround(req);
return !rentArrears;
},
nextStep: 'non-rent-arrears-dispute',
Expand All @@ -293,20 +294,33 @@ export const flowConfig: JourneyFlowConfig = {
},
'rent-arrears-dispute': {
defaultNext: 'counter-claim',
previousStep: req => getPreviousPageForArrears(req),
previousStep: (req: Request, _formData: Record<string, unknown>) => getPreviousNoticeStep(req),
routes: [
{
condition: (req: Request): Promise<boolean> => hasOnlyRentArrearsGrounds(req),
nextStep: 'counter-claim',
},
{
condition: async (req: Request): Promise<boolean> => !(await hasOnlyRentArrearsGrounds(req)),
nextStep: 'non-rent-arrears-dispute',
},
],
},
'non-rent-arrears-dispute': {
defaultNext: 'counter-claim',
previousStep: req => getPreviousPageForArrears(req),
},
'counter-claim': {
defaultNext: 'payment-interstitial',
previousStep: async (req: Request) => {
const rentArrearsClaim = await isRentArrearsClaim(req);
const rentArrearsClaim = await hasAnyRentArrearsGround(req);
if (rentArrearsClaim) {
return 'rent-arrears-dispute';
}
return 'non-rent-arrears-dispute';
return getPreviousNoticeStep(req);
},
},
'counter-claim': {
defaultNext: 'payment-interstitial',
previousStep: async (req: Request) => {
const onlyRentArrears = await hasOnlyRentArrearsGrounds(req);
return onlyRentArrears ? 'rent-arrears-dispute' : 'non-rent-arrears-dispute';
},
},
'payment-interstitial': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@
<form method="post" action="{{ url }}" novalidate>

<div class="govuk-button-group">
{{ govukButton({
{{ govukButton({
text: continue,
attributes: { type: 'submit', name: 'action', value: 'continue' }
}) }}
{{ govukButton({
{{ govukButton({
text: saveForLater,
classes: 'govuk-button--secondary',
attributes: { type: 'submit', name: 'action', value: 'saveForLater' }
}) }}
</div>
</form>

{% endblock %}
{% endblock %}
Loading