From a46d18c0f498f6d47ff3b0bf686d7dd0936e4856 Mon Sep 17 00:00:00 2001 From: Phil Head - CGI <152269503+philhead1@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:08:11 +0100 Subject: [PATCH 01/11] Changed error message for constraint validations (CSV error output) --- .../service/BulkUploadAsyncLifecycle.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycle.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycle.java index 241db4ddd..5ac9f0633 100644 --- a/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycle.java +++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycle.java @@ -22,6 +22,7 @@ import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -499,7 +500,16 @@ private void writeErrorCSVLine( errors.stream().filter(e -> e.getRowNumber() == finalRowCount).toList(); builder.append(line); for (BulkUploadError error : rowErrors) { - builder.append("|").append(error.getMessage()); + if(Objects.nonNull(error.getRejectedValue()) && error.getRejectedValue().isBlank()) { + builder.append("|") + .append("%s - %s: %s".formatted(error.getLocation(), + error.getRejectedValue(), + "Field has been rejected as it's invalid")); + + } else { + builder.append("|") + .append("%s: %s".formatted(error.getLocation(), error.getMessage())); + } } builder.append("\n"); } else { From 96c6a7371bb3be41643c62f9c9a4c6ff192eca2f Mon Sep 17 00:00:00 2001 From: Phil Head - CGI <152269503+philhead1@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:27:35 +0100 Subject: [PATCH 02/11] Fixed issue with multiple row errors not showing up --- .../applicationentry/service/BulkUploadAsyncLifecycle.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycle.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycle.java index 5ac9f0633..e9c545a3e 100644 --- a/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycle.java +++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycle.java @@ -148,10 +148,8 @@ public void validating(AsyncJobLifecycleEvent event) throws IOExc rowErrors.addAll(validator.validateRow(rowNumber, row)); rowErrors.addAll(validateMappedDto(rowNumber, dto)); + rowErrors.addAll(validateBusinessRules(rowNumber, dto)); - if (rowErrors.isEmpty()) { - rowErrors.addAll(validateBusinessRules(rowNumber, dto)); - } allErrors.addAll(rowErrors); rowNumber++; @@ -500,7 +498,7 @@ private void writeErrorCSVLine( errors.stream().filter(e -> e.getRowNumber() == finalRowCount).toList(); builder.append(line); for (BulkUploadError error : rowErrors) { - if(Objects.nonNull(error.getRejectedValue()) && error.getRejectedValue().isBlank()) { + if(Objects.nonNull(error.getRejectedValue()) && !error.getRejectedValue().isBlank()) { builder.append("|") .append("%s - %s: %s".formatted(error.getLocation(), error.getRejectedValue(), From a2f47ce5b8450d3a0d8ad703deb4d00f806432f4 Mon Sep 17 00:00:00 2001 From: Phil Head - CGI <152269503+philhead1@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:58:15 +0100 Subject: [PATCH 03/11] Spotless --- .../service/BulkUploadAsyncLifecycle.java | 18 ++++--- .../service/BulkUploadAsyncLifecycleTest.java | 49 ++++++++++++++++++- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycle.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycle.java index e9c545a3e..8abde1d28 100644 --- a/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycle.java +++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycle.java @@ -150,7 +150,6 @@ public void validating(AsyncJobLifecycleEvent event) throws IOExc rowErrors.addAll(validateMappedDto(rowNumber, dto)); rowErrors.addAll(validateBusinessRules(rowNumber, dto)); - allErrors.addAll(rowErrors); rowNumber++; } @@ -498,15 +497,22 @@ private void writeErrorCSVLine( errors.stream().filter(e -> e.getRowNumber() == finalRowCount).toList(); builder.append(line); for (BulkUploadError error : rowErrors) { - if(Objects.nonNull(error.getRejectedValue()) && !error.getRejectedValue().isBlank()) { + if (Objects.nonNull(error.getRejectedValue()) + && !error.getRejectedValue().isBlank()) { builder.append("|") - .append("%s - %s: %s".formatted(error.getLocation(), - error.getRejectedValue(), - "Field has been rejected as it's invalid")); + .append( + "%s - %s: %s" + .formatted( + error.getLocation(), + error.getRejectedValue(), + "Field has been rejected")); } else { builder.append("|") - .append("%s: %s".formatted(error.getLocation(), error.getMessage())); + .append( + "%s: %s" + .formatted( + error.getLocation(), error.getMessage())); } } builder.append("\n"); diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java index cbb6409dd..1c185bbe5 100644 --- a/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java +++ b/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java @@ -698,7 +698,54 @@ void whenValidating_csvFileIsSet_thenWritesClobSuccessfully() throws IOException assertThat(writtenCsv.toString()) .contains("HEADER|") - .contains("row-two|must match") + .contains("row-two|") + .contains("respondent.organisation.contactDetails.postcode") + .contains(row.getRespondentPostcode()) + .contains("Field has been rejected") + .contains("row-three|"); + + verify(persistenceService, times(1)).writeClob(any(), any()); + } + + @Test + void whenValidating_csvFileIsSet_thenWritesClobSuccessfully_multipleRowErrorsForSingleRow() + throws IOException { + when(csvFile.getBytes()).thenReturn("HEADER\nrow-two\nrow-three\n".getBytes()); + + StringBuilder writtenCsv = new StringBuilder(); + doAnswer( + invocation -> { + ByteArrayInputStream inputStream = invocation.getArgument(1); + writtenCsv.append(new String(inputStream.readAllBytes())); + return null; + }) + .when(persistenceService) + .writeClob(any(), any()); + + BulkUploadRow row = validOrganisationRow(); + row.setRespondentPostcode("invalid"); + row.setRespondentEmail("testtest.com"); + + JobContext context = new JobContext(); + AsyncJobLifecycleEvent event = event(row, context); + + lifecycle.setCSVFile(csvFile); + + AppRegistryException exception = + assertThrows(AppRegistryException.class, () -> lifecycle.validating(event)); + + assertThat(exception.getCode()) + .isEqualTo(AppListEntryError.BULK_UPLOAD_ROW_VALIDATION_FAILED); + + assertThat(writtenCsv.toString()) + .contains("HEADER|") + .contains("row-two|") + .contains("respondent.organisation.contactDetails.email") + .contains("testtest.com") + .contains("Field has been rejected|") + .contains("respondent.organisation.contactDetails.postcode") + .contains(row.getRespondentPostcode()) + .contains("Field has been rejected") .contains("row-three|"); verify(persistenceService, times(1)).writeClob(any(), any()); From 79b28386cdc50e4bbb38ab5799089d7be96d2428 Mon Sep 17 00:00:00 2001 From: Phil Head - CGI <152269503+philhead1@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:35:51 +0100 Subject: [PATCH 04/11] Added trim to application code field to avoid issues with whitespace --- .../validator/BulkCreateApplicationEntryValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/validator/BulkCreateApplicationEntryValidator.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/validator/BulkCreateApplicationEntryValidator.java index 56fa043f9..2ca769700 100644 --- a/src/main/java/uk/gov/hmcts/appregister/applicationentry/validator/BulkCreateApplicationEntryValidator.java +++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/validator/BulkCreateApplicationEntryValidator.java @@ -182,7 +182,7 @@ private static List trimAndKeyWordingFields( } private static String normalise(String value) { - return StringUtils.lowerCase(value, Locale.ROOT); + return StringUtils.lowerCase(value, Locale.ROOT).trim() ; } /** Job-scoped validation state. A session is confined to one async lifecycle. */ From 129e5d8d2c0b3bc3b378d0aaaaf15ede30010b64 Mon Sep 17 00:00:00 2001 From: hmcts-jenkins-cnp <60659747+hmcts-jenkins-cnp[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:37:43 +0000 Subject: [PATCH 05/11] Bumping chart version/ fixing aliases --- charts/appreg-api/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/appreg-api/Chart.yaml b/charts/appreg-api/Chart.yaml index 9215b55f2..e08281c28 100644 --- a/charts/appreg-api/Chart.yaml +++ b/charts/appreg-api/Chart.yaml @@ -3,7 +3,7 @@ appVersion: "1.1" description: A Helm chart for AppReg API name: appreg-api home: https://github.com/hmcts/appreg-api -version: 0.0.8 +version: 0.0.9 maintainers: - name: HMCTS AppReg Team dependencies: From f35b5a1fbf2066e854eb71691a210acabc4ad819 Mon Sep 17 00:00:00 2001 From: Phil Head - CGI <152269503+philhead1@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:15:54 +0100 Subject: [PATCH 06/11] Update BulkCreateApplicationEntryValidator.java --- .../validator/BulkCreateApplicationEntryValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/validator/BulkCreateApplicationEntryValidator.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/validator/BulkCreateApplicationEntryValidator.java index 2ca769700..aecdb9c81 100644 --- a/src/main/java/uk/gov/hmcts/appregister/applicationentry/validator/BulkCreateApplicationEntryValidator.java +++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/validator/BulkCreateApplicationEntryValidator.java @@ -182,7 +182,7 @@ private static List trimAndKeyWordingFields( } private static String normalise(String value) { - return StringUtils.lowerCase(value, Locale.ROOT).trim() ; + return StringUtils.lowerCase(value, Locale.ROOT).trim(); } /** Job-scoped validation state. A session is confined to one async lifecycle. */ From d481b24e41a30c28f3d187b494673673f3818efe Mon Sep 17 00:00:00 2001 From: Phil Head - CGI <152269503+philhead1@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:43:08 +0100 Subject: [PATCH 07/11] Added test to verify potential breaking scenario --- .../model/BulkUploadError.java | 4 ++ .../service/BulkUploadAsyncLifecycleTest.java | 52 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/model/BulkUploadError.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/model/BulkUploadError.java index 38f7b7e7a..c58e5ab58 100644 --- a/src/main/java/uk/gov/hmcts/appregister/applicationentry/model/BulkUploadError.java +++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/model/BulkUploadError.java @@ -1,7 +1,10 @@ package uk.gov.hmcts.appregister.applicationentry.model; +import com.fasterxml.jackson.annotation.JsonProperty; + import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; /** * Represents a validation or processing failure for a specific row and location in a bulk upload @@ -9,6 +12,7 @@ */ @Data @AllArgsConstructor +@NoArgsConstructor public class BulkUploadError { private int rowNumber; private String location; diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java index 1c185bbe5..b76bb0318 100644 --- a/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java +++ b/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java @@ -371,6 +371,58 @@ void givenCodeAwareValidationFailure_whenValidating_thenLogsRowFailure() throws "DATA_ERROR")))); } + @Test + void givenMissingRequiredFields_whenValidatingBusinessRules_thenLogsRowFailure() throws IOException { + BulkUploadRow row = validOrganisationRow(); + row.setApplicationCode(null); + row.setApplicantCode(null); + JobContext context = new JobContext(); + AsyncJobLifecycleEvent event = event(row, context); + + lifecycle.setCSVFile(csvFile); + + AppRegistryException exception = + assertThrows(AppRegistryException.class, () -> lifecycle.validating(event)); + + verify(persistenceService, times(1)).writeClob(any(), any()); + + assertThat(exception.getCode()) + .isEqualTo(AppListEntryError.BULK_UPLOAD_ROW_VALIDATION_FAILED); + + ObjectMapper mapper = new ObjectMapper(); + BulkUploadError[] errors = mapper.readValue(context.getValidationFailureMessages().getFirst(), BulkUploadError[].class); + assertThat(errors).hasSize(3); + + assertThat(context.getValidationFailureMessages()) + .containsExactly( + createErrorDescription( + List.of( + new BulkUploadError( + 2, + "APPLICANT_CODE", + null, + "Applicant code is required", + row.getRespondentAddressLine1(), + row.getRespondentOrganisationName(), + "DATA_ERROR"), + new BulkUploadError( + 2, + "APPLICATION_CODE", + null, + "Application code is required", + row.getRespondentAddressLine1(), + row.getRespondentOrganisationName(), + "DATA_ERROR"), + new BulkUploadError( + 2, + "applicationCode", + null, + "must not be null", + row.getRespondentAddressLine1(), + row.getRespondentOrganisationName(), + "DATA_ERROR")))); + } + @Test void givenExistingValidationFailures_whenValidating_thenPrependsHeaderErrors() throws IOException { From e5a6a0ad5d8eb6e4d6072085dd0ae46f0a60afc3 Mon Sep 17 00:00:00 2001 From: Phil Head - CGI <152269503+philhead1@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:58:48 +0100 Subject: [PATCH 08/11] Added more detail to wording template validation --- .../common/template/wording/WordingTemplateSentence.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/uk/gov/hmcts/appregister/common/template/wording/WordingTemplateSentence.java b/src/main/java/uk/gov/hmcts/appregister/common/template/wording/WordingTemplateSentence.java index 97b3243de..a4bd4a94c 100644 --- a/src/main/java/uk/gov/hmcts/appregister/common/template/wording/WordingTemplateSentence.java +++ b/src/main/java/uk/gov/hmcts/appregister/common/template/wording/WordingTemplateSentence.java @@ -454,7 +454,9 @@ public void canValueBeSubstituted(String value) { if (value.length() > this.getDetail().getConstraint().getLength()) { throw new AppRegistryException( CommonAppError.WORDING_LENGTH_FAILURE, - "Invalid length type in template", + "Invalid length type in template: expected %d but got %d" + .formatted( + this.getDetail().getConstraint().getLength(), value.length()), Map.of(this.getDetail().getKey(), value)); } } From ef087ef7d1e7c472c63b1234775a87c50a565762 Mon Sep 17 00:00:00 2001 From: Phil Head - CGI <152269503+philhead1@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:46:26 +0100 Subject: [PATCH 09/11] Spotless --- .../applicationentry/model/BulkUploadError.java | 2 -- .../common/template/wording/WordingTemplateSentence.java | 3 ++- .../service/BulkUploadAsyncLifecycleTest.java | 9 ++++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/model/BulkUploadError.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/model/BulkUploadError.java index c58e5ab58..d342e534e 100644 --- a/src/main/java/uk/gov/hmcts/appregister/applicationentry/model/BulkUploadError.java +++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/model/BulkUploadError.java @@ -1,7 +1,5 @@ package uk.gov.hmcts.appregister.applicationentry.model; -import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/src/main/java/uk/gov/hmcts/appregister/common/template/wording/WordingTemplateSentence.java b/src/main/java/uk/gov/hmcts/appregister/common/template/wording/WordingTemplateSentence.java index a4bd4a94c..b38a86560 100644 --- a/src/main/java/uk/gov/hmcts/appregister/common/template/wording/WordingTemplateSentence.java +++ b/src/main/java/uk/gov/hmcts/appregister/common/template/wording/WordingTemplateSentence.java @@ -456,7 +456,8 @@ public void canValueBeSubstituted(String value) { CommonAppError.WORDING_LENGTH_FAILURE, "Invalid length type in template: expected %d but got %d" .formatted( - this.getDetail().getConstraint().getLength(), value.length()), + this.getDetail().getConstraint().getLength(), + value.length()), Map.of(this.getDetail().getKey(), value)); } } diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java index b76bb0318..069344027 100644 --- a/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java +++ b/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java @@ -372,7 +372,8 @@ void givenCodeAwareValidationFailure_whenValidating_thenLogsRowFailure() throws } @Test - void givenMissingRequiredFields_whenValidatingBusinessRules_thenLogsRowFailure() throws IOException { + void givenMissingRequiredFields_whenValidatingBusinessRules_thenLogsRowFailure() + throws IOException { BulkUploadRow row = validOrganisationRow(); row.setApplicationCode(null); row.setApplicantCode(null); @@ -390,8 +391,10 @@ void givenMissingRequiredFields_whenValidatingBusinessRules_thenLogsRowFailure() .isEqualTo(AppListEntryError.BULK_UPLOAD_ROW_VALIDATION_FAILED); ObjectMapper mapper = new ObjectMapper(); - BulkUploadError[] errors = mapper.readValue(context.getValidationFailureMessages().getFirst(), BulkUploadError[].class); - assertThat(errors).hasSize(3); + BulkUploadError[] errors = + mapper.readValue( + context.getValidationFailureMessages().getFirst(), BulkUploadError[].class); + assertThat(errors).hasSize(3); assertThat(context.getValidationFailureMessages()) .containsExactly( From e872adc5963d4be709857f7729784fa6787d96d5 Mon Sep 17 00:00:00 2001 From: Phil Head - CGI <152269503+philhead1@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:37:45 +0100 Subject: [PATCH 10/11] Fixed broken test --- .../applicationentry/service/BulkUploadAsyncLifecycleTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java index 7aa2fd2ec..e6f476017 100644 --- a/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java +++ b/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java @@ -839,7 +839,7 @@ void whenValidating_csvFileIsSet_containsHeaderErrors_thenWritesClobSuccessfully assertThat(writtenCsv.toString()) .contains("HEADER|HEADER_ERROR:4|HEADER_ERROR:3|HEADER_ERROR:2|HEADER_ERROR:1") - .contains("row-two|must match") + .contains("row-two|respondent.organisation.contactDetails.postcode - invalid: Field has been rejected") .contains("row-three|"); verify(persistenceService, times(1)).writeClob(any(), any()); From 30e3e3005ad6b788069b74d876d6cecb7a76cc81 Mon Sep 17 00:00:00 2001 From: Phil Head - CGI <152269503+philhead1@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:52:38 +0100 Subject: [PATCH 11/11] Spotless --- .../applicationentry/service/BulkUploadAsyncLifecycleTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java index e6f476017..495d0c7e9 100644 --- a/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java +++ b/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/BulkUploadAsyncLifecycleTest.java @@ -839,7 +839,8 @@ void whenValidating_csvFileIsSet_containsHeaderErrors_thenWritesClobSuccessfully assertThat(writtenCsv.toString()) .contains("HEADER|HEADER_ERROR:4|HEADER_ERROR:3|HEADER_ERROR:2|HEADER_ERROR:1") - .contains("row-two|respondent.organisation.contactDetails.postcode - invalid: Field has been rejected") + .contains( + "row-two|respondent.organisation.contactDetails.postcode - invalid: Field has been rejected") .contains("row-three|"); verify(persistenceService, times(1)).writeClob(any(), any());