diff --git a/build.gradle b/build.gradle
index 421d833f2..c2fa01e26 100644
--- a/build.gradle
+++ b/build.gradle
@@ -242,7 +242,7 @@ tasks.register('bootRunLocalDev', BootRun) {
def activeProfiles = providers.gradleProperty('springProfiles')
.orElse(providers.systemProperty('spring.profiles.active'))
- .orElse('nosecurity')
+ .orElse('nosecurity,p6spy')
systemProperty 'spring.profiles.active', activeProfiles.get()
systemProperty 'logging.level.root', 'INFO'
@@ -454,9 +454,9 @@ dependencies {
// Future Jackson 2 overrides for CVE-2026-54515.
// Leave commented until the FasterXML 2.x fixes are actually published to Maven Central.
- // implementation 'com.fasterxml.jackson.core:jackson-core:2.21.5'
- // implementation 'com.fasterxml.jackson.core:jackson-databind:2.21.5'
- // implementation 'com.fasterxml.jackson.core:jackson-annotations:2.21.5'
+ implementation 'com.fasterxml.jackson.core:jackson-core:2.21.5'
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.21.5'
+ implementation 'com.fasterxml.jackson.core:jackson-annotations:2.21.5'
implementation 'org.flywaydb:flyway-database-postgresql:12.11.0'
diff --git a/config/owasp/suppressions.xml b/config/owasp/suppressions.xml
index 12ae69b55..0dc3d3597 100644
--- a/config/owasp/suppressions.xml
+++ b/config/owasp/suppressions.xml
@@ -8,23 +8,13 @@
^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-databind@.*$
CVE-2023-35116
+
We do not expose a SMTP interface
CVE-2025-7962
-
-
- Temporary suppression: CVE-2026-54515 affects Jackson 2.21.4, but the advisory's fixed
- FasterXML 2.x versions were not published to Maven Central when investigated on 2026-07-01.
- Review when a published Jackson 2.x fix is available to replace this suppression with a real
- upgrade.
-
- ^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-databind@2\.21\.4$
- CVE-2026-54515
-
-
diff --git a/openapi.yaml b/openapi.yaml
index 3e1b69ba5..2da8c0fef 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -3473,6 +3473,100 @@ paths:
tags:
- application-list-entry-results
/application-lists/entries/results:
+ delete:
+ description: |
+ Atomically deletes the specified Application List Entry Results. Each item is validated using the same rules as the single delete endpoint. If any item cannot be deleted, no Results are removed.
+ operationId: bulkDeleteResultEntries
+ requestBody:
+ content:
+ application/vnd.hmcts.appreg.v1+json:
+ schema:
+ $ref: "#/components/schemas/bulk-delete-results-dto"
+ required: true
+ responses:
+ "204":
+ description: Results deleted for all supplied items.
+ headers:
+ Vary:
+ description: Response varies by Accept for media-type versioning.
+ schema:
+ example: Accept
+ type: string
+ "400":
+ content:
+ application/problem+json:
+ schema:
+ $ref: "#/components/schemas/problem"
+ description: Invalid request parameters.
+ "401":
+ content:
+ application/problem+json:
+ examples:
+ unauthenticated:
+ value:
+ type: https://errors.hmcts.net/common/unauthorized
+ title: Unauthorized
+ status: 401
+ detail: Missing or invalid credentials
+ schema:
+ $ref: "#/components/schemas/problem"
+ description: Authentication required or token invalid.
+ "403":
+ content:
+ application/problem+json:
+ examples:
+ forbidden:
+ value:
+ type: https://errors.hmcts.net/common/forbidden
+ title: Forbidden
+ status: 403
+ detail: You do not have permission to access this resource
+ schema:
+ $ref: "#/components/schemas/problem"
+ description: Authenticated but not permitted to perform this action.
+ "404":
+ content:
+ application/problem+json:
+ examples:
+ missing:
+ value:
+ type: https://errors.hmcts.net/appreg/not-found
+ title: Not Found
+ status: 404
+ detail: Result code with id=123 was not found
+ schema:
+ $ref: "#/components/schemas/problem"
+ description: The requested resource was not found.
+ "409":
+ content:
+ application/problem+json:
+ examples:
+ generic:
+ value:
+ type: https://errors.hmcts.net/common/conflict
+ title: Conflict
+ status: 409
+ detail: The Application List could not be modified due to a conflict
+ with its current state
+ schema:
+ $ref: "#/components/schemas/problem"
+ description: Conflict with the current state of the resource.
+ "500":
+ content:
+ application/problem+json:
+ examples:
+ generic:
+ value:
+ type: https://errors.hmcts.net/common/internal-error
+ title: Internal Server Error
+ status: 500
+ detail: An unexpected error occurred
+ schema:
+ $ref: "#/components/schemas/problem"
+ description: Unexpected server error.
+ summary: Delete multiple Application List Entry Results.
+ tags:
+ - application-list-entry-results
post:
description: |
Atomically creates a **separate Result** for each specified entry in the **source** list (path `listId`), using the same Result payload for all entries. If any entry cannot be resulted, **no Results are created**.
@@ -6156,6 +6250,7 @@ components:
feeReference:
description: A short reference code that identifies a Fee.
example: CO5.2
+ maxLength: 12
nullable: true
type: string
feeAmount:
@@ -6167,6 +6262,7 @@ components:
offsiteFeeReference:
description: A short reference code that identifies a offsite Fee.
example: CO1.1
+ maxLength: 12
nullable: true
type: string
offsiteFeeAmount:
@@ -6305,6 +6401,7 @@ components:
feeReference:
description: A short reference code that identifies a Fee.
example: CO5.2
+ maxLength: 12
nullable: true
type: string
feeAmount:
@@ -6317,6 +6414,7 @@ components:
offsiteFeeReference:
description: A short reference code that identifies a offsite Fee.
example: CO5.2
+ maxLength: 12
nullable: true
type: string
offsiteFeeAmount:
@@ -7384,6 +7482,20 @@ components:
- id
- resultCode
type: object
+ bulk-delete-results-dto:
+ additionalProperties: false
+ description: Delete multiple Application List Entry Results atomically.
+ properties:
+ results:
+ description: |
+ Result delete requests to execute. Each item is validated using the same rules as the single-result delete endpoint. If any item is invalid, no results are deleted.
+ items:
+ $ref: "#/components/schemas/bulk-delete-result-item-dto"
+ minItems: 1
+ type: array
+ required:
+ - results
+ type: object
result-page:
allOf:
- $ref: "#/components/schemas/page"
@@ -8234,7 +8346,7 @@ components:
paymentReference:
description: Optional reference used to identify the payment.
example: PAY-001
- maxLength: 100
+ maxLength: 15
minLength: 1
type: string
required:
@@ -8264,6 +8376,29 @@ components:
- COMPLETED
example: RECEIVED
type: string
+ bulk-delete-result-item-dto:
+ additionalProperties: false
+ properties:
+ listId:
+ description: Public identifier of the Application List.
+ example: 123e4567-e89b-12d3-a456-426655440000
+ format: uuid
+ type: string
+ entryId:
+ description: Public identifier of the Application List Entry.
+ example: 123e4567-e89b-12d3-a456-426655440100
+ format: uuid
+ type: string
+ resultId:
+ description: Public identifier of the Application List Entry Result.
+ example: 123e4567-e89b-12d3-a456-426655440200
+ format: uuid
+ type: string
+ required:
+ - entryId
+ - listId
+ - resultId
+ type: object
court-location-get-summary-dto:
description: "Lightweight DTO for Court Locations, used in list/search views."
properties:
diff --git a/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentry/ApplicationEntryControllerBulkFeesTest.java b/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentry/ApplicationEntryControllerBulkFeesTest.java
index 5a991c968..77072176d 100644
--- a/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentry/ApplicationEntryControllerBulkFeesTest.java
+++ b/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentry/ApplicationEntryControllerBulkFeesTest.java
@@ -515,20 +515,20 @@ private void assertSuccessfulBulkUpdateAudited() {
differenceLogAsserter.assertNoErrors();
differenceLogAsserter.assertDataAuditChange(
DataAuditLogAsserter.getDataAuditAssertion(
- TableNames.APPLICATION_LISTS_FEE_STATUS,
- "alefs_fee_status",
- null,
- "REMITTED",
- AppListEntryAuditOperation.CREATE_FEE_STATUS_ENTRY.getType().name(),
- AppListEntryAuditOperation.CREATE_FEE_STATUS_ENTRY.getEventName()));
+ TableNames.APPLICATION_LISTS_ENTRY,
+ "bulk_fees_fee_details",
+ "",
+ "",
+ AppListEntryAuditOperation.BULK_UPDATE_FEES.getType().name(),
+ AppListEntryAuditOperation.BULK_UPDATE_FEES.getEventName()));
differenceLogAsserter.assertDataAuditChange(
DataAuditLogAsserter.getDataAuditAssertion(
- TableNames.APPLCATION_LISTS_ENTRY_FEE_ID,
- "fee_fee_id",
- null,
+ TableNames.APPLICATION_LISTS_ENTRY,
+ "bulk_fees_offsite_entry_ids",
+ "",
"",
- AppListEntryAuditOperation.CREATE_FEE_ENTRY.getType().name(),
- AppListEntryAuditOperation.CREATE_FEE_ENTRY.getEventName()));
+ AppListEntryAuditOperation.BULK_UPDATE_FEES.getType().name(),
+ AppListEntryAuditOperation.BULK_UPDATE_FEES.getEventName()));
}
private void assertNoBulkFeeAuditWritten() {
diff --git a/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentry/ApplicationEntryControllerBulkOfficialsTest.java b/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentry/ApplicationEntryControllerBulkOfficialsTest.java
index dcd0e251e..4e09de991 100644
--- a/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentry/ApplicationEntryControllerBulkOfficialsTest.java
+++ b/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentry/ApplicationEntryControllerBulkOfficialsTest.java
@@ -2,13 +2,23 @@
import static org.assertj.core.api.Assertions.assertThat;
+import com.nimbusds.jose.JOSEException;
import io.restassured.response.Response;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
import java.util.List;
+import java.util.Optional;
import java.util.UUID;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ProblemDetail;
+import uk.gov.hmcts.appregister.applicationentry.audit.AppListEntryAuditOperation;
import uk.gov.hmcts.appregister.applicationentry.exception.AppListEntryError;
import uk.gov.hmcts.appregister.applicationlist.exception.ApplicationListError;
+import uk.gov.hmcts.appregister.common.entity.DataAudit;
+import uk.gov.hmcts.appregister.common.entity.TableNames;
+import uk.gov.hmcts.appregister.common.entity.repository.DataAuditRepository;
import uk.gov.hmcts.appregister.generated.model.BulkOfficialsUpdateDto;
import uk.gov.hmcts.appregister.generated.model.EntryGetDetailDto;
import uk.gov.hmcts.appregister.generated.model.Official;
@@ -18,6 +28,8 @@
class ApplicationEntryControllerBulkOfficialsTest extends AbstractApplicationEntryCrudTest {
+ @Autowired private DataAuditRepository dataAuditRepository;
+
@Test
void givenValidEntries_whenReplaceOfficials_thenOfficialsAreReplacedForEveryEntry()
throws Exception {
@@ -49,6 +61,81 @@ void givenValidEntries_whenReplaceOfficials_thenOfficialsAreReplacedForEveryEntr
.containsExactlyElementsOf(replacementOfficials);
}
+ @Test
+ void givenValidEntries_whenReplaceOfficials_thenBulkAuditRowIsPersisted() throws Exception {
+ TokenGenerator tokenGenerator = createAdminToken();
+ EntryGetDetailDto firstEntry =
+ createEntry(List.of(official("Mr", "Original", "One", OfficialType.CLERK)));
+ EntryGetDetailDto secondEntry =
+ createEntry(List.of(official("Mrs", "Original", "Two", OfficialType.MAGISTRATE)));
+
+ replaceOfficials(
+ tokenGenerator,
+ firstEntry.getListId(),
+ new BulkOfficialsUpdateDto()
+ .entryIds(List.of(firstEntry.getId(), secondEntry.getId()))
+ .officials(
+ List.of(
+ official(
+ "Ms",
+ "Ada",
+ "Bench",
+ OfficialType.MAGISTRATE),
+ official(
+ "Mr",
+ "Clive",
+ "Court",
+ OfficialType.CLERK))))
+ .then()
+ .statusCode(204);
+
+ var bulkAuditRow = awaitBulkOfficialsAuditRow();
+ Assertions.assertEquals(
+ AppListEntryAuditOperation.BULK_UPDATE_OFFICIALS.getEventName(),
+ bulkAuditRow.getEventName());
+ assertThat(bulkAuditRow.getOldValue()).contains(firstEntry.getId().toString(), "Original");
+ assertThat(bulkAuditRow.getOldValue()).contains(secondEntry.getId().toString(), "Two");
+ assertThat(bulkAuditRow.getNewValue()).contains("Ada", "Bench", "Clive", "Court");
+ assertThat(
+ dataAuditRepository.findDataAuditForTableAndColumnAndOldValue(
+ TableNames.APPLCATION_LISTS_ENTRY_OFFICIAL, "surname", "One"))
+ .isEmpty();
+ assertThat(
+ dataAuditRepository.findDataAuditForTableAndColumnAndNewValue(
+ TableNames.APPLCATION_LISTS_ENTRY_OFFICIAL, "surname", "Bench"))
+ .isEmpty();
+ }
+
+ private DataAudit awaitBulkOfficialsAuditRow() {
+ for (int attempt = 0; attempt < 10; attempt++) {
+ Optional auditRow =
+ dataAuditRepository.findAll().stream()
+ .filter(
+ audit ->
+ TableNames.APPLICATION_LISTS_ENTRY.equals(
+ audit.getTableName())
+ && "bulk_officials_officials"
+ .equals(audit.getColumnName())
+ && AppListEntryAuditOperation
+ .BULK_UPDATE_OFFICIALS
+ .getEventName()
+ .equals(audit.getEventName()))
+ .findFirst();
+ if (auditRow.isPresent()) {
+ return auditRow.get();
+ }
+
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException interruptedException) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+
+ throw new AssertionError("Expected a bulk officials audit row with old and new values");
+ }
+
@Test
void givenMissingEntry_whenReplaceOfficials_thenReturns400AndDoesNotReplaceAnyOfficials()
throws Exception {
@@ -85,7 +172,8 @@ void givenMissingEntry_whenReplaceOfficials_thenReturns400AndDoesNotReplaceAnyOf
}
@Test
- void givenMissingApplicationList_whenReplaceOfficials_thenReturns404() throws Exception {
+ void givenMissingApplicationList_whenReplaceOfficials_thenReturns404()
+ throws MalformedURLException, JOSEException {
TokenGenerator tokenGenerator = createAdminToken();
UUID missingListId = UUID.randomUUID();
@@ -101,7 +189,8 @@ void givenMissingApplicationList_whenReplaceOfficials_thenReturns404() throws Ex
}
@Test
- void givenClosedApplicationList_whenReplaceOfficials_thenReturns409() throws Exception {
+ void givenClosedApplicationList_whenReplaceOfficials_thenReturns409()
+ throws MalformedURLException, JOSEException {
TokenGenerator tokenGenerator = createAdminToken();
UUID closedListId = getClosedApplicationListId();
@@ -117,7 +206,8 @@ void givenClosedApplicationList_whenReplaceOfficials_thenReturns409() throws Exc
}
@Test
- void givenEmptyEntryIds_whenReplaceOfficials_thenReturns400() throws Exception {
+ void givenEmptyEntryIds_whenReplaceOfficials_thenReturns400()
+ throws MalformedURLException, JOSEException {
TokenGenerator tokenGenerator = createAdminToken();
Response response =
@@ -306,7 +396,7 @@ private EntryGetDetailDto createEntry(List officials) throws Exception
private Response replaceOfficials(
TokenGenerator tokenGenerator, UUID listId, BulkOfficialsUpdateDto dto)
- throws Exception {
+ throws MalformedURLException, JOSEException {
return restAssuredClient.executePostRequest(
getLocalUrl(CREATE_ENTRY_CONTEXT + "/" + listId + "/entries/officials"),
tokenGenerator.fetchTokenForRole(),
@@ -314,7 +404,7 @@ private Response replaceOfficials(
}
private EntryGetDetailDto getEntry(TokenGenerator tokenGenerator, UUID listId, UUID entryId)
- throws Exception {
+ throws URISyntaxException, MalformedURLException, JOSEException {
Response response =
restAssuredClient.executeGetRequest(
getLocalUrl(CREATE_ENTRY_CONTEXT + "/" + listId + "/entries/" + entryId),
diff --git a/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentry/ApplicationEntryControllerMoveTest.java b/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentry/ApplicationEntryControllerMoveTest.java
index 8f2d30487..ff8fd32ec 100644
--- a/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentry/ApplicationEntryControllerMoveTest.java
+++ b/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentry/ApplicationEntryControllerMoveTest.java
@@ -119,14 +119,8 @@ void givenValidRequest_whenMove_thenDataAuditRowsPersisted() throws Exception {
val sourceList = createOpenListWithEntry(sourceEntry);
val targetList = createOpenTargetList();
- // Re-read the persisted entry so we can compare the version before and after the move.
- val persistedBeforeMove =
- applicationListEntryRepository.findByUuid(sourceEntry.getUuid()).orElseThrow();
-
// Clear earlier audit rows so these assertions only inspect the move request.
dataAuditRepository.deleteAll();
-
- val originalVersion = persistedBeforeMove.getVersion();
val resp = moveEntries(sourceList, targetList, Set.of(sourceEntry.getUuid()));
resp.then().statusCode(HttpStatus.OK.value());
@@ -134,56 +128,65 @@ void givenValidRequest_whenMove_thenDataAuditRowsPersisted() throws Exception {
val persistedAfterMove =
applicationListEntryRepository.findByUuid(sourceEntry.getUuid()).orElseThrow();
- // The entry should now belong to the target list and have an incremented version.
+ // The entry should now belong to the target list.
Assertions.assertEquals(
targetList.getId(), persistedAfterMove.getApplicationList().getId());
- Assertions.assertTrue(persistedAfterMove.getVersion() > originalVersion);
-
- // The move audit should capture the old and new list identifiers for the moved entry.
- val listIdAuditRow =
- dataAuditRepository.findAll().stream()
- .filter(row -> TableNames.APPLICATION_LISTS.equals(row.getTableName()))
- .filter(row -> "al_id".equals(row.getColumnName()))
- .filter(row -> sourceList.getId().toString().equals(row.getOldValue()))
- .filter(row -> targetList.getId().toString().equals(row.getNewValue()))
- .findFirst()
- .orElseThrow(
- () ->
- new AssertionError(
- "Expected an application_lists.al_id move audit row"));
-
- Assertions.assertEquals(
- AppListEntryAuditOperation.MOVE_APP_ENTRY.getEventName(),
- listIdAuditRow.getEventName());
- Assertions.assertEquals(
- AppListEntryAuditOperation.MOVE_APP_ENTRY.getType(),
- listIdAuditRow.getUpdateType());
- // The same move request should also audit the version increment on the entry row itself.
- val versionAuditRow =
+ // The bulk move audit should capture source and target list UUIDs plus the moved entry.
+ val moveAuditRows =
dataAuditRepository.findAll().stream()
.filter(
+ row ->
+ AppListEntryAuditOperation.BULK_MOVE_APP_ENTRIES
+ .getEventName()
+ .equals(row.getEventName()))
+ .toList();
+
+ Assertions.assertFalse(moveAuditRows.isEmpty());
+ Assertions.assertTrue(
+ moveAuditRows.stream()
+ .anyMatch(
row ->
TableNames.APPLICATION_LISTS_ENTRY.equals(
- row.getTableName()))
- .filter(row -> "version".equals(row.getColumnName()))
- .filter(row -> Long.toString(originalVersion).equals(row.getOldValue()))
- .filter(
+ row.getTableName())
+ && "bulk_move_source_list_uuid"
+ .equals(row.getColumnName())
+ && sourceList
+ .getUuid()
+ .toString()
+ .equals(row.getOldValue())
+ && sourceList
+ .getUuid()
+ .toString()
+ .equals(row.getNewValue())));
+ Assertions.assertTrue(
+ moveAuditRows.stream()
+ .anyMatch(
row ->
- Long.toString(persistedAfterMove.getVersion())
- .equals(row.getNewValue()))
- .findFirst()
- .orElseThrow(
- () ->
- new AssertionError(
- "Expected an application_list_entries.version move audit row"));
-
- Assertions.assertEquals(
- AppListEntryAuditOperation.MOVE_APP_ENTRY.getEventName(),
- versionAuditRow.getEventName());
- Assertions.assertEquals(
- AppListEntryAuditOperation.MOVE_APP_ENTRY.getType(),
- versionAuditRow.getUpdateType());
+ TableNames.APPLICATION_LISTS_ENTRY.equals(
+ row.getTableName())
+ && "bulk_move_target_list_uuid"
+ .equals(row.getColumnName())
+ && targetList
+ .getUuid()
+ .toString()
+ .equals(row.getOldValue())
+ && targetList
+ .getUuid()
+ .toString()
+ .equals(row.getNewValue())));
+ Assertions.assertTrue(
+ moveAuditRows.stream()
+ .anyMatch(
+ row ->
+ TableNames.APPLICATION_LISTS_ENTRY.equals(
+ row.getTableName())
+ && "bulk_move_entries".equals(row.getColumnName())
+ && row.getOldValue()
+ .contains(sourceEntry.getUuid().toString())
+ && row.getNewValue()
+ .contains(
+ sourceEntry.getUuid().toString())));
}
@Test
@@ -201,9 +204,8 @@ void givenSecondMoveSaveFails_whenMove_thenAllEntriesRollBack() throws Exception
// Clear earlier audit rows so we only inspect what this failing move attempted to write.
dataAuditRepository.deleteAll();
- // Fail on the second save call inside the move loop so we can prove the first move rolls
- // back as part of the same transaction.
- moveEntryFailureSwitch.failOnSecondSave();
+ // Fail during the batched saveAll call so we can prove the move still rolls back.
+ moveEntryFailureSwitch.failOnMoveSave();
try {
val resp =
@@ -228,39 +230,17 @@ void givenSecondMoveSaveFails_whenMove_thenAllEntriesRollBack() throws Exception
Assertions.assertEquals(sourceList.getId(), persistedFirst.getApplicationList().getId());
Assertions.assertEquals(sourceList.getId(), persistedSecond.getApplicationList().getId());
- // The business move rolls back, but the attempted move audit remains persisted in line
- // with the legacy autonomous-audit behaviour.
+ // The business move rolls back and no completed bulk-move audit rows are persisted.
val moveAuditRows =
dataAuditRepository.findAll().stream()
.filter(
row ->
- AppListEntryAuditOperation.MOVE_APP_ENTRY
+ AppListEntryAuditOperation.BULK_MOVE_APP_ENTRIES
.getEventName()
.equals(row.getEventName()))
.toList();
- Assertions.assertFalse(moveAuditRows.isEmpty());
- Assertions.assertTrue(
- moveAuditRows.stream()
- .anyMatch(
- row ->
- TableNames.APPLICATION_LISTS.equals(row.getTableName())
- && "al_id".equals(row.getColumnName())
- && sourceList
- .getId()
- .toString()
- .equals(row.getOldValue())
- && targetList
- .getId()
- .toString()
- .equals(row.getNewValue())));
- Assertions.assertTrue(
- moveAuditRows.stream()
- .anyMatch(
- row ->
- TableNames.APPLICATION_LISTS_ENTRY.equals(
- row.getTableName())
- && "version".equals(row.getColumnName())));
+ Assertions.assertTrue(moveAuditRows.isEmpty());
}
@Test
@@ -591,10 +571,9 @@ public Object postProcessAfterInitialization(Object bean, String beanName)
bean.getClass().getClassLoader(),
bean.getClass().getInterfaces(),
(proxy, method, args) -> {
- if ("save".equals(method.getName())
- && args != null
+ if (args != null
&& args.length == 1
- && switcher.shouldFail(args[0])) {
+ && switcher.shouldFail(method.getName(), args[0])) {
throw new IllegalStateException(
"Simulated move save failure for rollback test");
}
@@ -614,7 +593,7 @@ static class MoveEntryFailureSwitch {
private final AtomicBoolean enabled = new AtomicBoolean(false);
private final AtomicInteger saveCount = new AtomicInteger(0);
- void failOnSecondSave() {
+ void failOnMoveSave() {
saveCount.set(0);
enabled.set(true);
}
@@ -624,10 +603,12 @@ void reset() {
saveCount.set(0);
}
- boolean shouldFail(Object candidate) {
+ boolean shouldFail(String methodName, Object candidate) {
return enabled.get()
- && candidate instanceof ApplicationListEntry
- && saveCount.incrementAndGet() == 2;
+ && (("saveAll".equals(methodName) && candidate instanceof Iterable>)
+ || ("save".equals(methodName)
+ && candidate instanceof ApplicationListEntry))
+ && saveCount.incrementAndGet() == 1;
}
}
}
diff --git a/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentryresult/AbstractApplicationEntryResultCrudTest.java b/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentryresult/AbstractApplicationEntryResultCrudTest.java
index 91c5631ae..9bf7b68b8 100644
--- a/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentryresult/AbstractApplicationEntryResultCrudTest.java
+++ b/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentryresult/AbstractApplicationEntryResultCrudTest.java
@@ -35,6 +35,7 @@
import uk.gov.hmcts.appregister.data.ResolutionCodeTestData;
import uk.gov.hmcts.appregister.generated.model.ApplicationListCreateDto;
import uk.gov.hmcts.appregister.generated.model.ApplicationListStatus;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultsDto;
import uk.gov.hmcts.appregister.generated.model.EntryCreateDto;
import uk.gov.hmcts.appregister.generated.model.EntryGetDetailDto;
import uk.gov.hmcts.appregister.generated.model.ResultCreateDto;
@@ -130,6 +131,12 @@ protected Response createBulkResult(TokenAndJwksKey token, Object body)
getLocalUrl(WEB_CONTEXT + "/entries/results"), token, body);
}
+ protected Response deleteBulkResult(TokenAndJwksKey token, BulkDeleteResultsDto body)
+ throws MalformedURLException {
+ return restAssuredClient.executeDeleteRequest(
+ getLocalUrl(WEB_CONTEXT + "/entries/results"), token, body);
+ }
+
protected Response updateResult(
UUID listId,
UUID entryId,
diff --git a/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentryresult/ApplicationEntryResultControllerBulkDeleteTest.java b/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentryresult/ApplicationEntryResultControllerBulkDeleteTest.java
new file mode 100644
index 000000000..0edda55b1
--- /dev/null
+++ b/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentryresult/ApplicationEntryResultControllerBulkDeleteTest.java
@@ -0,0 +1,161 @@
+package uk.gov.hmcts.appregister.controller.applicationentryresult;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static uk.gov.hmcts.appregister.common.enumeration.Status.OPEN;
+import static uk.gov.hmcts.appregister.testutils.util.ProblemAssertUtil.assertEquals;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpStatus;
+import uk.gov.hmcts.appregister.applicationentryresult.audit.AppListEntryResultAuditOperation;
+import uk.gov.hmcts.appregister.applicationentryresult.exception.ApplicationListEntryResultError;
+import uk.gov.hmcts.appregister.common.entity.DataAudit;
+import uk.gov.hmcts.appregister.common.entity.TableNames;
+import uk.gov.hmcts.appregister.data.ResolutionCodeTestData;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultItemDto;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultsDto;
+
+class ApplicationEntryResultControllerBulkDeleteTest
+ extends AbstractApplicationEntryResultCrudTest {
+
+ @Test
+ void givenValidRequest_whenBulkDelete_thenDeletesAllResultsAndWritesSingleBulkAudit()
+ throws Exception {
+ var list = createAndSaveList(OPEN);
+ var firstEntry = createEntry(list);
+ var secondEntry = createEntry(list);
+ persistance.save(firstEntry);
+ persistance.save(secondEntry);
+
+ var firstResult =
+ createAndSaveResolution(firstEntry, new ResolutionCodeTestData().someComplete());
+ var secondResult =
+ createAndSaveResolution(secondEntry, new ResolutionCodeTestData().someComplete());
+
+ dataAuditRepository.deleteAll();
+
+ deleteBulkResult(
+ getToken(),
+ new BulkDeleteResultsDto()
+ .results(
+ List.of(
+ requestItem(
+ list.getUuid(),
+ firstEntry.getUuid(),
+ firstResult.getUuid()),
+ requestItem(
+ list.getUuid(),
+ secondEntry.getUuid(),
+ secondResult.getUuid()))))
+ .then()
+ .statusCode(HttpStatus.NO_CONTENT.value());
+
+ assertThat(appListEntryResolutionRepository.findByUuid(firstResult.getUuid())).isEmpty();
+ assertThat(appListEntryResolutionRepository.findByUuid(secondResult.getUuid())).isEmpty();
+
+ var bulkAuditRow =
+ awaitBulkDeleteAuditRow(
+ firstResult.getUuid(), secondResult.getUuid(), firstEntry.getUuid());
+ assertThat(bulkAuditRow.getEventName())
+ .isEqualTo(
+ AppListEntryResultAuditOperation.BULK_DELETE_APP_LIST_ENTRY_RESULT
+ .getEventName());
+ assertThat(valueOrClob(bulkAuditRow.getNewValue(), bulkAuditRow.getNewClobValue()))
+ .contains(firstResult.getUuid().toString(), secondResult.getUuid().toString());
+ assertThat(noPerResultDeleteAuditRows()).isTrue();
+ }
+
+ @Test
+ void givenInvalidRequest_whenBulkDelete_thenRollsBackEverything() throws Exception {
+ var list = createAndSaveList(OPEN);
+ var entry = createEntry(list);
+ persistance.save(entry);
+
+ var result = createAndSaveResolution(entry, new ResolutionCodeTestData().someComplete());
+
+ var response =
+ deleteBulkResult(
+ getToken(),
+ new BulkDeleteResultsDto()
+ .results(
+ List.of(
+ requestItem(
+ list.getUuid(),
+ entry.getUuid(),
+ result.getUuid()),
+ requestItem(
+ list.getUuid(),
+ entry.getUuid(),
+ UUID.randomUUID()))));
+
+ response.then().statusCode(HttpStatus.NOT_FOUND.value());
+ assertEquals(
+ ApplicationListEntryResultError.APPLICATION_ENTRY_RESULT_DOES_NOT_EXIST.getCode(),
+ response);
+
+ assertThat(appListEntryResolutionRepository.findByUuid(result.getUuid())).isPresent();
+ assertThat(
+ dataAuditRepository.findAll().stream()
+ .noneMatch(
+ row ->
+ AppListEntryResultAuditOperation
+ .BULK_DELETE_APP_LIST_ENTRY_RESULT
+ .getEventName()
+ .equals(row.getEventName())))
+ .isTrue();
+ }
+
+ private BulkDeleteResultItemDto requestItem(UUID listId, UUID entryId, UUID resultId) {
+ return new BulkDeleteResultItemDto().listId(listId).entryId(entryId).resultId(resultId);
+ }
+
+ private DataAudit awaitBulkDeleteAuditRow(UUID resultId, UUID resultId1, UUID entryId) {
+ for (int attempt = 0; attempt < 10; attempt++) {
+ Optional auditRow =
+ dataAuditRepository.findAll().stream()
+ .filter(
+ audit ->
+ TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS.equals(
+ audit.getTableName())
+ && "bulk_delete_results"
+ .equals(audit.getColumnName())
+ && AppListEntryResultAuditOperation
+ .BULK_DELETE_APP_LIST_ENTRY_RESULT
+ .getEventName()
+ .equals(audit.getEventName()))
+ .filter(
+ audit -> {
+ var auditValue =
+ valueOrClob(
+ audit.getNewValue(),
+ audit.getNewClobValue());
+ return auditValue.contains(resultId.toString())
+ && auditValue.contains(resultId1.toString())
+ && auditValue.contains(entryId.toString());
+ })
+ .findFirst();
+ if (auditRow.isPresent()) {
+ return auditRow.get();
+ }
+ }
+
+ throw new AssertionError("Expected a bulk delete audit row with deleted result details");
+ }
+
+ private boolean noPerResultDeleteAuditRows() {
+ return dataAuditRepository.findAll().stream()
+ .noneMatch(
+ auditRow ->
+ AppListEntryResultAuditOperation.DELETE_APP_LIST_ENTRY_RESULT
+ .getEventName()
+ .equals(auditRow.getEventName())
+ && TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS.equals(
+ auditRow.getTableName()));
+ }
+
+ private String valueOrClob(String value, String clobValue) {
+ return value != null ? value : clobValue;
+ }
+}
diff --git a/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentryresult/ApplicationEntryResultControllerCreateTest.java b/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentryresult/ApplicationEntryResultControllerCreateTest.java
index 08f577893..78298aafd 100644
--- a/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentryresult/ApplicationEntryResultControllerCreateTest.java
+++ b/src/integrationTest/java/uk/gov/hmcts/appregister/controller/applicationentryresult/ApplicationEntryResultControllerCreateTest.java
@@ -21,6 +21,7 @@
import uk.gov.hmcts.appregister.applicationentryresult.exception.ApplicationListEntryResultError;
import uk.gov.hmcts.appregister.applicationlist.exception.ApplicationListError;
import uk.gov.hmcts.appregister.common.entity.AppListEntryResolution;
+import uk.gov.hmcts.appregister.common.entity.DataAudit;
import uk.gov.hmcts.appregister.common.entity.TableNames;
import uk.gov.hmcts.appregister.common.exception.CommonAppError;
import uk.gov.hmcts.appregister.generated.model.BulkResultDto;
@@ -369,6 +370,7 @@ void givenAValidBulkResultRequest_whenACallIsMadeWithAListAndTwoEntries_thenSucc
new TemplateSubstitution("Date", "Date"),
new TemplateSubstitution("Courthouse", "ch")));
bulkResultDto.setResult(createDto);
+ dataAuditRepository.deleteAll();
val token = getToken();
@@ -462,127 +464,21 @@ void givenAValidBulkResultRequest_whenACallIsMadeWithAListAndTwoEntries_thenSucc
new AssertionError(
"Created AppListEntryResolution could not be reloaded"));
- // The resolution row itself should record its generated identifier on create.
- val resultIdAuditRow =
- dataAuditRepository
- .findDataAuditForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "aler_id",
- createdResolution.getId().toString())
- .orElseThrow(
- () ->
- new AssertionError(
- "Expected an app_list_entry_resolutions.aler_id create audit row"));
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- resultIdAuditRow.getEventName());
-
- dataAuditRepository
- .findDataAuditForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "aler_id",
- createdResolution1.getId().toString())
- .orElseThrow(
- () ->
- new AssertionError(
- "Expected an app_list_entry_resolutions.aler_id create audit row"));
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- resultIdAuditRow.getEventName());
-
- // The owning entry id is stored through the foreign key column ale_ale_id.
- val entryIdAuditRow =
- dataAuditRepository
- .findDataAuditForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "ale_ale_id",
- createdResolution.getApplicationList().getId().toString())
- .orElseThrow(
- () ->
- new AssertionError(
- "Expected an app_list_entry_resolutions.ale_ale_id create audit row"));
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- entryIdAuditRow.getEventName());
-
- // The owning entry id is stored through the foreign key column ale_ale_id.
- dataAuditRepository
- .findDataAuditForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "ale_ale_id",
- createdResolution1.getApplicationList().getId().toString())
- .orElseThrow(
- () ->
- new AssertionError(
- "Expected an app_list_entry_resolutions.ale_ale_id create audit row"));
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- entryIdAuditRow.getEventName());
-
- // The selected resolution code should be recorded via the rc_rc_id join column.
- val resolutionCodeAuditRow =
- dataAuditRepository.findDataAuditListForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "rc_rc_id",
- createdResolution.getResolutionCode().getId().toString());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- resolutionCodeAuditRow.get(0).getEventName());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- resolutionCodeAuditRow.get(1).getEventName());
-
- val resolutionCodeAuditRow1 =
- dataAuditRepository.findDataAuditListForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "rc_rc_id",
- createdResolution1.getResolutionCode().getId().toString());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- resolutionCodeAuditRow1.get(0).getEventName());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- resolutionCodeAuditRow1.get(1).getEventName());
-
- // The substituted wording is stored directly on the resolution row.
- val wordingAuditRow =
- dataAuditRepository.findDataAuditListForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "al_entry_resolution_wording",
- createdResolution.getResolutionWording());
-
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- wordingAuditRow.get(0).getEventName());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- wordingAuditRow.get(1).getEventName());
-
- // The service stamps the acting user into the officer column on create.
- val officerAuditRow =
- dataAuditRepository.findDataAuditListForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "al_entry_resolution_officer",
- "email");
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- officerAuditRow.get(0).getEventName());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- officerAuditRow.get(1).getEventName());
-
- // Version is database-backed and should be written alongside the other create audit rows.
- val versionAuditRow =
- dataAuditRepository.findDataAuditListForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "version",
- createdResolution.getVersion().toString());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- versionAuditRow.get(0).getEventName());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- versionAuditRow.get(1).getEventName());
+ val bulkAuditRow =
+ awaitBulkResultAuditRow(
+ entry.getUuid(),
+ entry2.getUuid(),
+ createdResolution.getUuid(),
+ createdResolution1.getUuid());
+ Assertions.assertEquals(
+ AppListEntryResultAuditOperation.BULK_CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
+ bulkAuditRow.getEventName());
+ Assertions.assertTrue(
+ valueOrClob(bulkAuditRow.getNewValue(), bulkAuditRow.getNewClobValue())
+ .contains(createdResolution.getResolutionWording()));
+ Assertions.assertTrue(
+ noPerResultCreateAuditRows(),
+ "Expected the bulk path to avoid per-result create audit rows");
}
@Test
@@ -604,6 +500,7 @@ void givenAValidBulkResultRequest_whenACallIsMadeWithAListAndTwoEntries_thenSucc
new TemplateSubstitution("Date", "Date"),
new TemplateSubstitution("Courthouse", "ch")));
bulkResultDto.setResult(createDto);
+ dataAuditRepository.deleteAll();
Response resp = createBulkResult(list.getUuid(), getToken(), bulkResultDto);
@@ -861,127 +758,64 @@ void givenAValidBulkResultRequest_whenACallIsMadeWithAListAndTwoEntries_thenSucc
new AssertionError(
"Created AppListEntryResolution could not be reloaded"));
- // The resolution row itself should record its generated identifier on create.
- val resultIdAuditRow =
- dataAuditRepository
- .findDataAuditForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "aler_id",
- createdResolution.getId().toString())
- .orElseThrow(
- () ->
- new AssertionError(
- "Expected an app_list_entry_resolutions.aler_id create audit row"));
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- resultIdAuditRow.getEventName());
-
- dataAuditRepository
- .findDataAuditForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "aler_id",
- createdResolution1.getId().toString())
- .orElseThrow(
- () ->
- new AssertionError(
- "Expected an app_list_entry_resolutions.aler_id create audit row"));
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- resultIdAuditRow.getEventName());
-
- // The owning entry id is stored through the foreign key column ale_ale_id.
- val entryIdAuditRow =
- dataAuditRepository
- .findDataAuditForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "ale_ale_id",
- createdResolution.getApplicationList().getId().toString())
- .orElseThrow(
- () ->
- new AssertionError(
- "Expected an app_list_entry_resolutions.ale_ale_id create audit row"));
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- entryIdAuditRow.getEventName());
-
- // The owning entry id is stored through the foreign key column ale_ale_id.
- dataAuditRepository
- .findDataAuditForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "ale_ale_id",
- createdResolution1.getApplicationList().getId().toString())
- .orElseThrow(
- () ->
- new AssertionError(
- "Expected an app_list_entry_resolutions.ale_ale_id create audit row"));
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- entryIdAuditRow.getEventName());
-
- // The selected resolution code should be recorded via the rc_rc_id join column.
- val resolutionCodeAuditRow =
- dataAuditRepository.findDataAuditListForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "rc_rc_id",
- createdResolution.getResolutionCode().getId().toString());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- resolutionCodeAuditRow.get(0).getEventName());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- resolutionCodeAuditRow.get(1).getEventName());
-
- val resolutionCodeAuditRow1 =
- dataAuditRepository.findDataAuditListForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "rc_rc_id",
- createdResolution1.getResolutionCode().getId().toString());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- resolutionCodeAuditRow1.get(0).getEventName());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- resolutionCodeAuditRow1.get(1).getEventName());
-
- // The substituted wording is stored directly on the resolution row.
- val wordingAuditRow =
- dataAuditRepository.findDataAuditListForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "al_entry_resolution_wording",
- createdResolution.getResolutionWording());
+ val bulkAuditRow =
+ awaitBulkResultAuditRow(
+ entry.getUuid(),
+ entry2.getUuid(),
+ createdResolution.getUuid(),
+ createdResolution1.getUuid());
+ Assertions.assertEquals(
+ AppListEntryResultAuditOperation.BULK_CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
+ bulkAuditRow.getEventName());
+ Assertions.assertTrue(
+ valueOrClob(bulkAuditRow.getNewValue(), bulkAuditRow.getNewClobValue())
+ .contains(createdResolution.getResolutionWording()));
+ Assertions.assertTrue(
+ noPerResultCreateAuditRows(),
+ "Expected the bulk path to avoid per-result create audit rows");
+ }
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- wordingAuditRow.get(0).getEventName());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- wordingAuditRow.get(1).getEventName());
+ private DataAudit awaitBulkResultAuditRow(
+ UUID entryId, UUID entryId1, UUID resultId, UUID resultId1) {
+ for (int attempt = 0; attempt < 20; attempt++) {
+ for (var auditRow : dataAuditRepository.findAll()) {
+ if (!AppListEntryResultAuditOperation.BULK_CREATE_APP_LIST_ENTRY_RESULT
+ .getEventName()
+ .equals(auditRow.getEventName())) {
+ continue;
+ }
+
+ if (!TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS.equals(auditRow.getTableName())
+ || !"bulk_results_created".equals(auditRow.getColumnName())) {
+ continue;
+ }
+
+ var auditValue = valueOrClob(auditRow.getNewValue(), auditRow.getNewClobValue());
+ if (auditValue.contains(entryId.toString())
+ && auditValue.contains(entryId1.toString())
+ && auditValue.contains(resultId.toString())
+ && auditValue.contains(resultId1.toString())) {
+ return auditRow;
+ }
+ }
+ }
+
+ throw new AssertionError("Expected a bulk result audit row with created result details");
+ }
- // The service stamps the acting user into the officer column on create.
- val officerAuditRow =
- dataAuditRepository.findDataAuditListForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "al_entry_resolution_officer",
- "email");
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- officerAuditRow.get(0).getEventName());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- officerAuditRow.get(1).getEventName());
+ private boolean noPerResultCreateAuditRows() {
+ return dataAuditRepository.findAll().stream()
+ .noneMatch(
+ auditRow ->
+ AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT
+ .getEventName()
+ .equals(auditRow.getEventName())
+ && TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS.equals(
+ auditRow.getTableName()));
+ }
- // Version is database-backed and should be written alongside the other create audit rows.
- val versionAuditRow =
- dataAuditRepository.findDataAuditListForTableAndColumnAndNewValue(
- TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS,
- "version",
- createdResolution.getVersion().toString());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- versionAuditRow.get(0).getEventName());
- Assertions.assertEquals(
- AppListEntryResultAuditOperation.CREATE_APP_LIST_ENTRY_RESULT.getEventName(),
- versionAuditRow.get(1).getEventName());
+ private String valueOrClob(String value, String clobValue) {
+ return value != null ? value : clobValue;
}
@Test
diff --git a/src/integrationTest/java/uk/gov/hmcts/appregister/testutils/client/RestAssuredClient.java b/src/integrationTest/java/uk/gov/hmcts/appregister/testutils/client/RestAssuredClient.java
index 230c11a87..049943299 100644
--- a/src/integrationTest/java/uk/gov/hmcts/appregister/testutils/client/RestAssuredClient.java
+++ b/src/integrationTest/java/uk/gov/hmcts/appregister/testutils/client/RestAssuredClient.java
@@ -366,6 +366,15 @@ public Response executeDeleteRequest(URL url, TokenAndJwksKey token) {
.andReturn();
}
+ public Response executeDeleteRequest(URL url, TokenAndJwksKey token, Object object) {
+ return given().body(object)
+ .header("Authorization", "Bearer " + token.getToken())
+ .header("Content-Type", "application/vnd.hmcts.appreg.v1+json")
+ .header("traceparent", DEFAULT_TRACE_ID)
+ .delete(url)
+ .andReturn();
+ }
+
/**
* deletes a request builder that can be used to make requests against the application with an
* If-Match header.
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/audit/AppListEntryAuditOperation.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/audit/AppListEntryAuditOperation.java
index 8b77b2df8..f1f2d3a0a 100644
--- a/src/main/java/uk/gov/hmcts/appregister/applicationentry/audit/AppListEntryAuditOperation.java
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/audit/AppListEntryAuditOperation.java
@@ -10,6 +10,9 @@
public enum AppListEntryAuditOperation implements AuditOperation {
CREATE_APP_ENTRY_LIST("Create Entry Application List", CrudEnum.CREATE),
BULK_IMPORT_APP_ENTRIES("Bulk Import Application List Entries", CrudEnum.CREATE),
+ BULK_MOVE_APP_ENTRIES("Bulk Move Application List Entries", CrudEnum.UPDATE),
+ BULK_UPDATE_OFFICIALS("Bulk Update Officials", CrudEnum.UPDATE),
+ BULK_UPDATE_FEES("Bulk Update Fees", CrudEnum.UPDATE),
UPDATE_APP_ENTRY_LIST("Update Entry Application List", CrudEnum.UPDATE),
UPDATE_CLOSED_APP_ENTRY_LIST("Update Closed Entry Application List", CrudEnum.UPDATE),
CREATE_OFFICIAL_ENTRY("Create Official", CrudEnum.CREATE),
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkMoveApplicationListEntriesAudit.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkMoveApplicationListEntriesAudit.java
new file mode 100644
index 000000000..d1e7545b0
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkMoveApplicationListEntriesAudit.java
@@ -0,0 +1,75 @@
+package uk.gov.hmcts.appregister.applicationentry.audit;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import uk.gov.hmcts.appregister.audit.listener.diff.Auditable;
+import uk.gov.hmcts.appregister.audit.listener.diff.AuditableData;
+import uk.gov.hmcts.appregister.common.entity.ApplicationListEntry;
+import uk.gov.hmcts.appregister.common.entity.TableNames;
+import uk.gov.hmcts.appregister.common.enumeration.CrudEnum;
+
+public record BulkMoveApplicationListEntriesAudit(
+ Long sourceListId,
+ UUID sourceListUuid,
+ Long targetListId,
+ UUID targetListUuid,
+ int entryCount,
+ String entries)
+ implements Auditable {
+
+ private static final String TABLE_NAME = TableNames.APPLICATION_LISTS_ENTRY;
+ private static final String SOURCE_LIST_UUID_FIELD = "bulk_move_source_list_uuid";
+ private static final String TARGET_LIST_UUID_FIELD = "bulk_move_target_list_uuid";
+ private static final String ENTRY_COUNT_FIELD = "bulk_move_entry_count";
+ private static final String ENTRIES_FIELD = "bulk_move_entries";
+
+ @Override
+ public Long getId() {
+ return sourceListId;
+ }
+
+ @Override
+ public List extractAuditData(CrudEnum crudEnum) {
+ return List.of(
+ new AuditableData(TABLE_NAME, SOURCE_LIST_UUID_FIELD, sourceListUuid.toString()),
+ new AuditableData(TABLE_NAME, TARGET_LIST_UUID_FIELD, targetListUuid.toString()),
+ new AuditableData(TABLE_NAME, ENTRY_COUNT_FIELD, Integer.toString(entryCount)),
+ new AuditableData(TABLE_NAME, ENTRIES_FIELD, entries));
+ }
+
+ public static BulkMoveApplicationListEntriesAudit forState(
+ Long sourceListId,
+ UUID sourceListUuid,
+ Long targetListId,
+ UUID targetListUuid,
+ List entries) {
+ var sortedEntries =
+ entries.stream()
+ .sorted(
+ Comparator.comparing(ApplicationListEntry::getSequenceNumber)
+ .thenComparing(ApplicationListEntry::getId))
+ .toList();
+
+ return new BulkMoveApplicationListEntriesAudit(
+ sourceListId,
+ sourceListUuid,
+ targetListId,
+ targetListUuid,
+ sortedEntries.size(),
+ formatEntries(sortedEntries));
+ }
+
+ private static String formatEntries(List entries) {
+ return entries.stream()
+ .map(
+ entry ->
+ "{\"entryId\":\"%s\",\"sequenceNumber\":%s,\"version\":%s}"
+ .formatted(
+ entry.getUuid(),
+ entry.getSequenceNumber(),
+ entry.getVersion()))
+ .collect(Collectors.joining(",", "[", "]"));
+ }
+}
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkUpdateFeesAudit.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkUpdateFeesAudit.java
new file mode 100644
index 000000000..a89c8ddd2
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkUpdateFeesAudit.java
@@ -0,0 +1,97 @@
+package uk.gov.hmcts.appregister.applicationentry.audit;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import uk.gov.hmcts.appregister.audit.listener.diff.Auditable;
+import uk.gov.hmcts.appregister.audit.listener.diff.AuditableData;
+import uk.gov.hmcts.appregister.audit.listener.diff.BulkAuditFormatting;
+import uk.gov.hmcts.appregister.common.entity.AppListEntryFeeStatus;
+import uk.gov.hmcts.appregister.common.entity.TableNames;
+import uk.gov.hmcts.appregister.common.enumeration.CrudEnum;
+import uk.gov.hmcts.appregister.generated.model.BulkFeeDetailsDto;
+
+public record BulkUpdateFeesAudit(
+ Long listId,
+ UUID listUuid,
+ List entryIds,
+ int entryCount,
+ String feeDetails,
+ String offsiteEntryIds)
+ implements Auditable {
+
+ private static final String TABLE_NAME = TableNames.APPLICATION_LISTS_ENTRY;
+ private static final String LIST_UUID_FIELD = "bulk_fees_list_uuid";
+ private static final String ENTRY_IDS_FIELD = "bulk_fees_entry_ids";
+ private static final String ENTRY_COUNT_FIELD = "bulk_fees_entry_count";
+ private static final String FEE_DETAILS_FIELD = "bulk_fees_fee_details";
+ private static final String OFFSITE_ENTRY_IDS_FIELD = "bulk_fees_offsite_entry_ids";
+
+ @Override
+ public Long getId() {
+ return listId;
+ }
+
+ @Override
+ public List extractAuditData(CrudEnum crudEnum) {
+ return List.of(
+ new AuditableData(TABLE_NAME, LIST_UUID_FIELD, listUuid.toString()),
+ new AuditableData(
+ TABLE_NAME,
+ ENTRY_IDS_FIELD,
+ BulkAuditFormatting.formatSortedUuidArray(entryIds)),
+ new AuditableData(TABLE_NAME, ENTRY_COUNT_FIELD, Integer.toString(entryCount)),
+ new AuditableData(TABLE_NAME, FEE_DETAILS_FIELD, feeDetails),
+ new AuditableData(TABLE_NAME, OFFSITE_ENTRY_IDS_FIELD, offsiteEntryIds));
+ }
+
+ public static String formatExistingFeeStatuses(List feeStatuses) {
+ return feeStatuses.stream()
+ .sorted(
+ Comparator.comparing(
+ (AppListEntryFeeStatus status) ->
+ status.getAppListEntry().getSequenceNumber())
+ .thenComparing(
+ status -> status.getId(),
+ Comparator.nullsLast(Long::compareTo)))
+ .map(
+ status ->
+ ("{\"entryId\":\"%s\",\"status\":\"%s\",\"statusDate\":\"%s\","
+ + "\"paymentReference\":\"%s\"}")
+ .formatted(
+ status.getAppListEntry().getUuid(),
+ status.getAlefsFeeStatus(),
+ status.getAlefsFeeStatusDate(),
+ BulkAuditFormatting.escape(
+ status.getAlefsPaymentReference())))
+ .collect(Collectors.joining(",", "[", "]"));
+ }
+
+ public static String formatRequestedFeeDetails(
+ List feeDetails, Boolean hasOffsiteFee) {
+ return feeDetails.stream()
+ .map(
+ feeDetail ->
+ ("{\"paymentStatus\":\"%s\",\"statusDate\":\"%s\","
+ + "\"paymentReference\":\"%s\",\"hasOffsiteFee\":%s}")
+ .formatted(
+ feeDetail.getPaymentStatus(),
+ feeDetail.getStatusDate(),
+ BulkAuditFormatting.escape(
+ feeDetail.getPaymentReference()),
+ Boolean.TRUE.equals(hasOffsiteFee)))
+ .collect(Collectors.joining(",", "[", "]"));
+ }
+
+ public static String formatOffsiteEntryIds(
+ Collection entryIds, Map entryUuidsById) {
+ return entryIds.stream()
+ .map(entryUuidsById::get)
+ .collect(
+ Collectors.collectingAndThen(
+ Collectors.toList(), BulkAuditFormatting::formatSortedUuidArray));
+ }
+}
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkUpdateOfficialsAudit.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkUpdateOfficialsAudit.java
new file mode 100644
index 000000000..c7e2318a9
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkUpdateOfficialsAudit.java
@@ -0,0 +1,78 @@
+package uk.gov.hmcts.appregister.applicationentry.audit;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import uk.gov.hmcts.appregister.audit.listener.diff.Auditable;
+import uk.gov.hmcts.appregister.audit.listener.diff.AuditableData;
+import uk.gov.hmcts.appregister.audit.listener.diff.BulkAuditFormatting;
+import uk.gov.hmcts.appregister.common.entity.AppListEntryOfficial;
+import uk.gov.hmcts.appregister.common.entity.TableNames;
+import uk.gov.hmcts.appregister.common.enumeration.CrudEnum;
+import uk.gov.hmcts.appregister.generated.model.Official;
+
+public record BulkUpdateOfficialsAudit(
+ Long listId, UUID listUuid, List entryIds, int entryCount, String officials)
+ implements Auditable {
+
+ private static final String TABLE_NAME = TableNames.APPLICATION_LISTS_ENTRY;
+ private static final String LIST_UUID_FIELD = "bulk_officials_list_uuid";
+ private static final String ENTRY_IDS_FIELD = "bulk_officials_entry_ids";
+ private static final String ENTRY_COUNT_FIELD = "bulk_officials_entry_count";
+ private static final String OFFICIALS_FIELD = "bulk_officials_officials";
+
+ @Override
+ public Long getId() {
+ return listId;
+ }
+
+ @Override
+ public List extractAuditData(CrudEnum crudEnum) {
+ return List.of(
+ new AuditableData(TABLE_NAME, LIST_UUID_FIELD, listUuid.toString()),
+ new AuditableData(
+ TABLE_NAME,
+ ENTRY_IDS_FIELD,
+ BulkAuditFormatting.formatSortedUuidArray(entryIds)),
+ new AuditableData(TABLE_NAME, ENTRY_COUNT_FIELD, Integer.toString(entryCount)),
+ new AuditableData(TABLE_NAME, OFFICIALS_FIELD, officials));
+ }
+
+ public static String formatDeletedOfficials(List deletedOfficials) {
+ return deletedOfficials.stream()
+ .sorted(
+ Comparator.comparing(
+ (AppListEntryOfficial official) ->
+ official.getAppListEntry().getSequenceNumber())
+ .thenComparing(
+ AppListEntryOfficial::getId,
+ Comparator.nullsLast(Long::compareTo)))
+ .map(
+ official ->
+ ("{\"entryId\":\"%s\",\"sequenceNumber\":%s,\"type\":\"%s\","
+ + "\"title\":\"%s\",\"forename\":\"%s\","
+ + "\"surname\":\"%s\"}")
+ .formatted(
+ official.getAppListEntry().getUuid(),
+ official.getAppListEntry().getSequenceNumber(),
+ official.getOfficialType(),
+ BulkAuditFormatting.escape(official.getTitle()),
+ BulkAuditFormatting.escape(official.getForename()),
+ BulkAuditFormatting.escape(official.getSurname())))
+ .collect(Collectors.joining(",", "[", "]"));
+ }
+
+ public static String formatReplacementOfficials(List replacementOfficials) {
+ return replacementOfficials.stream()
+ .map(
+ official ->
+ "{\"type\":\"%s\",\"title\":\"%s\",\"forename\":\"%s\",\"surname\":\"%s\"}"
+ .formatted(
+ official.getType(),
+ BulkAuditFormatting.escape(official.getTitle()),
+ BulkAuditFormatting.escape(official.getForename()),
+ BulkAuditFormatting.escape(official.getSurname())))
+ .collect(Collectors.joining(",", "[", "]"));
+ }
+}
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/exception/AppListEntryError.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/exception/AppListEntryError.java
index 6abf81053..50de541f3 100644
--- a/src/main/java/uk/gov/hmcts/appregister/applicationentry/exception/AppListEntryError.java
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/exception/AppListEntryError.java
@@ -199,7 +199,7 @@ public enum AppListEntryError implements ErrorCodeEnum {
PAYMENT_REFERENCE_TOO_LONG(
DefaultErrorDetail.create(
HttpStatus.BAD_REQUEST,
- "paymentReference must not be longer than 100 characters",
+ "paymentReference must not be longer than 15 characters",
"ALE-40")),
OFFICIAL_TITLE_REQUIRED(
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/ApplicationEntryServiceImpl.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/ApplicationEntryServiceImpl.java
index a6f3e282b..ac16fb9f6 100644
--- a/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/ApplicationEntryServiceImpl.java
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/service/ApplicationEntryServiceImpl.java
@@ -27,9 +27,11 @@
import org.springframework.transaction.annotation.Transactional;
import uk.gov.hmcts.appregister.applicationentry.api.ApplicationEntrySortConfig;
import uk.gov.hmcts.appregister.applicationentry.audit.AppListEntryAuditOperation;
-import uk.gov.hmcts.appregister.applicationentry.audit.ApplicationListEntryMoveAudit;
import uk.gov.hmcts.appregister.applicationentry.audit.ApplicationListEntryReadAudit;
import uk.gov.hmcts.appregister.applicationentry.audit.BulkApplicationListEntriesReadAudit;
+import uk.gov.hmcts.appregister.applicationentry.audit.BulkMoveApplicationListEntriesAudit;
+import uk.gov.hmcts.appregister.applicationentry.audit.BulkUpdateFeesAudit;
+import uk.gov.hmcts.appregister.applicationentry.audit.BulkUpdateOfficialsAudit;
import uk.gov.hmcts.appregister.applicationentry.audit.model.DeleteAuditable;
import uk.gov.hmcts.appregister.applicationentry.exception.AppListEntryError;
import uk.gov.hmcts.appregister.applicationentry.mapper.ApplicationListEntryEntityMapper;
@@ -940,11 +942,63 @@ public void replaceOfficials(UUID listId, BulkOfficialsUpdateDto bulkOfficialsUp
(req, success) -> {
List entries = new ArrayList<>(success.getEntries());
entries.sort(Comparator.comparing(ApplicationListEntry::getSequenceNumber));
+ List replacementOfficials = req.data().getOfficials();
+ var applicationList =
+ entries.isEmpty() ? null : entries.getFirst().getApplicationList();
+
+ List entryUuids =
+ entries.stream().map(ApplicationListEntry::getUuid).toList();
+ Map> existingOfficialsByEntryUuid =
+ appListEntryOfficialRepository
+ .findByAppListEntry_UuidIn(entryUuids)
+ .stream()
+ .collect(
+ Collectors.groupingBy(
+ official ->
+ official.getAppListEntry().getUuid()));
+
+ List entryIds =
+ entries.stream().map(ApplicationListEntry::getId).toList();
+ List officialsToCreate =
+ new ArrayList<>(entries.size() * replacementOfficials.size());
+ List deletedOfficials =
+ existingOfficialsByEntryUuid.values().stream()
+ .flatMap(List::stream)
+ .toList();
+ var deletedOfficialsAudit =
+ new BulkUpdateOfficialsAudit(
+ applicationList != null ? applicationList.getId() : null,
+ listId,
+ entryUuids,
+ entries.size(),
+ BulkUpdateOfficialsAudit.formatDeletedOfficials(
+ deletedOfficials));
+ var replacementOfficialsAudit =
+ new BulkUpdateOfficialsAudit(
+ applicationList != null ? applicationList.getId() : null,
+ listId,
+ entryUuids,
+ entries.size(),
+ BulkUpdateOfficialsAudit.formatReplacementOfficials(
+ replacementOfficials));
for (ApplicationListEntry entry : entries) {
- replaceOfficialsForEntry(entry, req.data().getOfficials());
+ addOfficialsForEntry(officialsToCreate, entry, replacementOfficials);
}
+ auditService.processAudit(
+ deletedOfficialsAudit,
+ AppListEntryAuditOperation.BULK_UPDATE_OFFICIALS,
+ ignored -> {
+ if (!entryIds.isEmpty()) {
+ appListEntryOfficialRepository.deleteAllForEntryIds(entryIds);
+ }
+
+ appListEntryOfficialRepository.saveAll(officialsToCreate);
+ return Optional.of(
+ new AuditableResult<>(null, replacementOfficialsAudit));
+ });
+
log.info(
"Completed bulk officials replacement for {} entries in list {}",
entries.size(),
@@ -954,6 +1008,7 @@ public void replaceOfficials(UUID listId, BulkOfficialsUpdateDto bulkOfficialsUp
}
@Override
+ @NestedAudit
@Transactional
public BulkUpdateResponseDto bulkUpdateFees(UUID listId, BulkFeesUpdateDto bulkFeesUpdateDto) {
var payload = new BulkUpdateFeesPayload(listId, bulkFeesUpdateDto);
@@ -981,10 +1036,27 @@ public BulkUpdateResponseDto bulkUpdateFees(UUID listId, BulkFeesUpdateDto bulkF
.orElse(Boolean.FALSE));
Supplier offsiteFeeSupplier =
offsiteFeeSupplier(hasOffsiteFee);
+ List entryUuids =
+ entries.stream()
+ .map(ApplicationListEntry::getUuid)
+ .toList();
+ Map entryUuidsById =
+ entries.stream()
+ .collect(
+ Collectors.toMap(
+ ApplicationListEntry::getId,
+ ApplicationListEntry::getUuid));
+ List existingFeeStatuses =
+ appListEntryFeeStatusRepository.findByAppListEntry_UuidIn(
+ entryUuids);
Set entryIdsWithOffsiteMapping =
hasOffsiteFee
? getEntryIdsWithOffsiteMapping(entries)
: Set.of();
+ List feeStatusesToCreate =
+ new ArrayList<>(entries.size() * feeDetails.size());
+ List offsiteFeeMappingsToCreate =
+ new ArrayList<>();
if (!feeDetails.isEmpty()) {
for (ApplicationListEntry entry : entries) {
@@ -993,14 +1065,17 @@ public BulkUpdateResponseDto bulkUpdateFees(UUID listId, BulkFeesUpdateDto bulkF
feeDetails,
hasOffsiteFee,
offsiteFeeSupplier,
- entryIdsWithOffsiteMapping);
+ entryIdsWithOffsiteMapping,
+ feeStatusesToCreate,
+ offsiteFeeMappingsToCreate);
}
} else if (hasOffsiteFee) {
for (ApplicationListEntry entry : entries) {
ensureOffsiteFeeMapping(
entry,
offsiteFeeSupplier,
- entryIdsWithOffsiteMapping);
+ entryIdsWithOffsiteMapping,
+ offsiteFeeMappingsToCreate);
}
} else if (!hasOffsiteFee) {
for (ApplicationListEntry entry : entries) {
@@ -1008,12 +1083,65 @@ public BulkUpdateResponseDto bulkUpdateFees(UUID listId, BulkFeesUpdateDto bulkF
}
}
+ var oldAudit =
+ new BulkUpdateFeesAudit(
+ applicationListId(entries),
+ listId,
+ entryUuids,
+ entries.size(),
+ BulkUpdateFeesAudit.formatExistingFeeStatuses(
+ existingFeeStatuses),
+ BulkUpdateFeesAudit.formatOffsiteEntryIds(
+ entryIdsWithOffsiteMapping,
+ entryUuidsById));
+
int updatedCount = entries.size();
+ Set updatedOffsiteEntryIds =
+ new HashSet<>(entryIdsWithOffsiteMapping);
+ offsiteFeeMappingsToCreate.stream()
+ .map(AppListEntryFeeId::getAppListEntryId)
+ .forEach(updatedOffsiteEntryIds::add);
+ var newAudit =
+ new BulkUpdateFeesAudit(
+ applicationListId(entries),
+ listId,
+ entryUuids,
+ entries.size(),
+ BulkUpdateFeesAudit.formatRequestedFeeDetails(
+ feeDetails,
+ req.data()
+ .getHasOffsiteFee()
+ .orElse(Boolean.FALSE)),
+ BulkUpdateFeesAudit.formatOffsiteEntryIds(
+ updatedOffsiteEntryIds, entryUuidsById));
+
+ return auditService.processAudit(
+ oldAudit,
+ AppListEntryAuditOperation.BULK_UPDATE_FEES,
+ ignored -> {
+ if (!feeStatusesToCreate.isEmpty()) {
+ appListEntryFeeStatusRepository.saveAll(
+ feeStatusesToCreate);
+ }
+
+ if (!offsiteFeeMappingsToCreate.isEmpty()) {
+ appListEntryFeeRepository.saveAll(
+ offsiteFeeMappingsToCreate);
+ }
+
+ var bulkUpdateResponse =
+ new BulkUpdateResponseDto()
+ .totalCount(
+ req.data().getEntryIds().size())
+ .updatedCount(updatedCount)
+ .status(
+ BulkUpdateResponseDto.StatusEnum
+ .SUCCEEDED);
- return new BulkUpdateResponseDto()
- .totalCount(req.data().getEntryIds().size())
- .updatedCount(updatedCount)
- .status(BulkUpdateResponseDto.StatusEnum.SUCCEEDED);
+ return Optional.of(
+ new AuditableResult<>(
+ bulkUpdateResponse, newAudit));
+ });
});
long durationNanos = System.nanoTime() - startNanos;
@@ -1088,13 +1216,19 @@ private void appendFeeDetailsForEntry(
List feeDetails,
boolean hasOffsiteFee,
Supplier offsiteFeeSupplier,
- Set entryIdsWithOffsiteMapping) {
+ Set entryIdsWithOffsiteMapping,
+ List feeStatusesToCreate,
+ List offsiteFeeMappingsToCreate) {
for (BulkFeeDetailsDto feeDetail : feeDetails) {
- saveFeeStatus(createBulkFeeStatus(entry, feeDetail), new ArrayList<>());
+ feeStatusesToCreate.add(createBulkFeeStatus(entry, feeDetail));
}
if (hasOffsiteFee) {
- ensureOffsiteFeeMapping(entry, offsiteFeeSupplier, entryIdsWithOffsiteMapping);
+ ensureOffsiteFeeMapping(
+ entry,
+ offsiteFeeSupplier,
+ entryIdsWithOffsiteMapping,
+ offsiteFeeMappingsToCreate);
}
}
@@ -1301,30 +1435,25 @@ private Set getEntryIdsWithOffsiteMapping(List entri
private void ensureOffsiteFeeMapping(
ApplicationListEntry entry,
Supplier offsiteFeeSupplier,
- Set entryIdsWithOffsiteMapping) {
+ Set entryIdsWithOffsiteMapping,
+ List offsiteFeeMappingsToCreate) {
if (entryIdsWithOffsiteMapping.contains(entry.getId())) {
return;
}
Fee offsiteFee = offsiteFeeSupplier.get();
+ AppListEntryFeeId offsiteEntryFee = new AppListEntryFeeId();
+ offsiteEntryFee.setAppListEntryId(entry.getId());
+ offsiteEntryFee.setFeeId(offsiteFee.getId());
+ offsiteFeeMappingsToCreate.add(offsiteEntryFee);
+ log.debug(
+ CREATED_OFFSITE_FEE_LOG,
+ offsiteEntryFee.getFeeId(),
+ offsiteEntryFee.getAppListEntryId());
+ }
- auditService.processAudit(
- AppListEntryAuditOperation.CREATE_FEE_ENTRY,
- req -> {
- AppListEntryFeeId offsiteEntryFee = new AppListEntryFeeId();
- offsiteEntryFee.setAppListEntryId(entry.getId());
- offsiteEntryFee.setFeeId(offsiteFee.getId());
-
- AppListEntryFeeId savedOffsiteEntryFee =
- appListEntryFeeRepository.save(offsiteEntryFee);
-
- log.debug(
- CREATED_OFFSITE_FEE_LOG,
- savedOffsiteEntryFee.getFeeId(),
- savedOffsiteEntryFee.getAppListEntryId());
-
- return Optional.of(new AuditableResult<>(null, savedOffsiteEntryFee));
- });
+ private static Long applicationListId(List entries) {
+ return entries.isEmpty() ? null : entries.getFirst().getApplicationList().getId();
}
/**
@@ -1717,31 +1846,12 @@ private List updateOfficials(
return officialList;
}
- private void replaceOfficialsForEntry(
- ApplicationListEntry entry, List replacementOfficials) {
- List existingOfficials =
- appListEntryOfficialRepository.getOfficialByEntryUuid(entry.getUuid());
-
- for (AppListEntryOfficial existingOfficial : existingOfficials) {
- auditService.processAudit(
- existingOfficial,
- AppListEntryAuditOperation.DELETE_OFFICIAL_ENTRY,
- req -> {
- appListEntryOfficialRepository.delete(existingOfficial);
- return Optional.empty();
- });
- }
-
+ private void addOfficialsForEntry(
+ List officialsToCreate,
+ ApplicationListEntry entry,
+ List replacementOfficials) {
for (Official official : replacementOfficials) {
- auditService.processAudit(
- AppListEntryAuditOperation.CREATE_OFFICIAL_ENTRY,
- req -> {
- AppListEntryOfficial createdOfficial =
- appListEntryOfficialRepository.save(
- applicationListEntryEntityMapper.toOfficial(
- official, entry));
- return Optional.of(new AuditableResult<>(null, createdOfficial));
- });
+ officialsToCreate.add(applicationListEntryEntityMapper.toOfficial(official, entry));
}
}
@@ -1977,24 +2087,41 @@ public void move(UUID sourceListId, MoveEntriesDto moveEntriesDto) {
List orderedEntriesToMove = new ArrayList<>(entriesToMove);
orderedEntriesToMove.sort(Comparator.comparing(ApplicationListEntry::getSequenceNumber));
+ var oldAudit =
+ BulkMoveApplicationListEntriesAudit.forState(
+ orderedEntriesToMove.getFirst().getApplicationList().getId(),
+ sourceListId,
+ targetList.getId(),
+ targetList.getUuid(),
+ orderedEntriesToMove);
- for (ApplicationListEntry entryToMove : orderedEntriesToMove) {
- auditService.processAudit(
- ApplicationListEntryMoveAudit.from(entryToMove),
- AppListEntryAuditOperation.MOVE_APP_ENTRY,
- req -> {
+ auditService.processAudit(
+ oldAudit,
+ AppListEntryAuditOperation.BULK_MOVE_APP_ENTRIES,
+ req -> {
+ short nextSequence =
+ allocateNextSequence(targetList.getId(), orderedEntriesToMove.size());
+ short startingSequence =
+ (short) (nextSequence - orderedEntriesToMove.size() + 1);
+
+ for (int i = 0; i < orderedEntriesToMove.size(); i++) {
+ var entryToMove = orderedEntriesToMove.get(i);
entryToMove.setApplicationList(targetList);
- entryToMove.setSequenceNumber(allocateNextSequence(targetList.getId()));
+ entryToMove.setSequenceNumber((short) (startingSequence + i));
+ }
- var movedEntry =
- refreshEntity(applicationListEntryRepository.save(entryToMove));
+ applicationListEntryRepository.saveAll(orderedEntriesToMove);
- return Optional.of(
- new AuditableResult<>(
- movedEntry.getUuid(),
- ApplicationListEntryMoveAudit.from(movedEntry)));
- });
- }
+ var newAudit =
+ BulkMoveApplicationListEntriesAudit.forState(
+ oldAudit.sourceListId(),
+ sourceListId,
+ targetList.getId(),
+ targetList.getUuid(),
+ orderedEntriesToMove);
+
+ return Optional.of(new AuditableResult<>(null, newAudit));
+ });
log.info(
"Completed bulk move for {} entries from list {}",
@@ -2098,18 +2225,26 @@ private List getKeyablesForCreateUpdateEtag(ApplicationListEntry update
}
private short allocateNextSequence(Long alId) {
+ return allocateNextSequence(alId, 1);
+ }
+
+ private short allocateNextSequence(Long alId, int requiredCount) {
AppListEntrySequenceMapping mapping =
appListEntrySequenceMappingRepository.findByAlIdForUpdate(alId).orElse(null);
if (mapping == null) {
- mapping = AppListEntrySequenceMapping.builder().alId(alId).aleLastSequence(1).build();
+ mapping =
+ AppListEntrySequenceMapping.builder()
+ .alId(alId)
+ .aleLastSequence(requiredCount)
+ .build();
appListEntrySequenceMappingRepository.save(mapping);
- return (short) 1;
+ return (short) requiredCount;
}
- int next = mapping.getAleLastSequence() + 1;
+ int next = mapping.getAleLastSequence() + requiredCount;
mapping.setAleLastSequence(next);
return (short) next;
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentry/validator/BulkUpdateFeesValidator.java b/src/main/java/uk/gov/hmcts/appregister/applicationentry/validator/BulkUpdateFeesValidator.java
index db63142a8..7c7e47b59 100644
--- a/src/main/java/uk/gov/hmcts/appregister/applicationentry/validator/BulkUpdateFeesValidator.java
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentry/validator/BulkUpdateFeesValidator.java
@@ -32,7 +32,7 @@
public class BulkUpdateFeesValidator
implements Validator {
- private static final int PAYMENT_REFERENCE_MAX_LENGTH = 100;
+ private static final int PAYMENT_REFERENCE_MAX_LENGTH = 15;
private final ApplicationListRepository applicationListRepository;
private final ApplicationListEntryRepository applicationListEntryRepository;
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/audit/AppListEntryResultAuditOperation.java b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/audit/AppListEntryResultAuditOperation.java
index 8f8ac6e9f..152776be6 100644
--- a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/audit/AppListEntryResultAuditOperation.java
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/audit/AppListEntryResultAuditOperation.java
@@ -9,7 +9,10 @@
@Getter
public enum AppListEntryResultAuditOperation implements AuditOperation {
DELETE_APP_LIST_ENTRY_RESULT("Delete Application List Entry Result", CrudEnum.DELETE),
+ BULK_DELETE_APP_LIST_ENTRY_RESULT(
+ "Delete Multiple Application List Entry Results", CrudEnum.DELETE),
CREATE_APP_LIST_ENTRY_RESULT("Create Application List Entry Result", CrudEnum.CREATE),
+ BULK_CREATE_APP_LIST_ENTRY_RESULT("Result Multiple Applications", CrudEnum.CREATE),
UPDATE_APP_LIST_ENTRY_RESULT("Update Application List Entry Result", CrudEnum.UPDATE),
GET_APP_LIST_ENTRY_RESULT("Get Application List Entry Result", CrudEnum.READ);
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/audit/BulkCreateApplicationEntryResultAudit.java b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/audit/BulkCreateApplicationEntryResultAudit.java
new file mode 100644
index 000000000..30ddbeb0f
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/audit/BulkCreateApplicationEntryResultAudit.java
@@ -0,0 +1,110 @@
+package uk.gov.hmcts.appregister.applicationentryresult.audit;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import uk.gov.hmcts.appregister.audit.listener.diff.Auditable;
+import uk.gov.hmcts.appregister.audit.listener.diff.AuditableData;
+import uk.gov.hmcts.appregister.audit.listener.diff.BulkAuditFormatting;
+import uk.gov.hmcts.appregister.common.entity.AppListEntryResolution;
+import uk.gov.hmcts.appregister.common.entity.TableNames;
+import uk.gov.hmcts.appregister.common.enumeration.CrudEnum;
+import uk.gov.hmcts.appregister.generated.model.TemplateSubstitution;
+
+public record BulkCreateApplicationEntryResultAudit(
+ Long anchorId,
+ UUID listUuid,
+ List entryIds,
+ int entryCount,
+ String resultCode,
+ String wordingFields,
+ String createdResults)
+ implements Auditable {
+
+ private static final String TABLE_NAME = TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS;
+ private static final String LIST_UUID_FIELD = "bulk_results_list_uuid";
+ private static final String ENTRY_IDS_FIELD = "bulk_results_entry_ids";
+ private static final String ENTRY_COUNT_FIELD = "bulk_results_entry_count";
+ private static final String RESULT_CODE_FIELD = "bulk_results_result_code";
+ private static final String WORDING_FIELDS_FIELD = "bulk_results_wording_fields";
+ private static final String CREATED_RESULTS_FIELD = "bulk_results_created";
+
+ @Override
+ public Long getId() {
+ return anchorId;
+ }
+
+ @Override
+ public List extractAuditData(CrudEnum crudEnum) {
+ var auditData =
+ new ArrayList<>(
+ List.of(
+ new AuditableData(
+ TABLE_NAME,
+ ENTRY_IDS_FIELD,
+ BulkAuditFormatting.formatSortedUuidArray(entryIds)),
+ new AuditableData(
+ TABLE_NAME,
+ ENTRY_COUNT_FIELD,
+ Integer.toString(entryCount)),
+ new AuditableData(TABLE_NAME, RESULT_CODE_FIELD, resultCode),
+ new AuditableData(TABLE_NAME, WORDING_FIELDS_FIELD, wordingFields),
+ new AuditableData(
+ TABLE_NAME, CREATED_RESULTS_FIELD, createdResults)));
+
+ if (listUuid != null) {
+ auditData.add(new AuditableData(TABLE_NAME, LIST_UUID_FIELD, listUuid.toString()));
+ }
+
+ return auditData;
+ }
+
+ public static String formatWordingFields(List wordingFields) {
+ if (wordingFields == null || wordingFields.isEmpty()) {
+ return "[]";
+ }
+
+ return wordingFields.stream()
+ .map(
+ substitution ->
+ "{\"key\":\"%s\",\"value\":\"%s\"}"
+ .formatted(
+ BulkAuditFormatting.escape(substitution.getKey()),
+ BulkAuditFormatting.escape(
+ substitution.getValue())))
+ .collect(Collectors.joining(",", "[", "]"));
+ }
+
+ public static String formatCreatedResults(List createdResults) {
+ return createdResults.stream()
+ .sorted(
+ Comparator.comparing(
+ (AppListEntryResolution resolution) ->
+ resolution.getApplicationList().getSequenceNumber())
+ .thenComparing(
+ AppListEntryResolution::getId,
+ Comparator.nullsLast(Long::compareTo)))
+ .map(
+ resolution ->
+ """
+ {"resultId":"%s","entryId":"%s","sequenceNumber":%s,
+ "resultCode":"%s","wording":"%s","officer":"%s"}
+ """
+ .strip()
+ .formatted(
+ resolution.getUuid(),
+ resolution.getApplicationList().getUuid(),
+ resolution.getApplicationList().getSequenceNumber(),
+ BulkAuditFormatting.escape(
+ resolution
+ .getResolutionCode()
+ .getResultCode()),
+ BulkAuditFormatting.escape(
+ resolution.getResolutionWording()),
+ BulkAuditFormatting.escape(
+ resolution.getResolutionOfficer())))
+ .collect(Collectors.joining(",", "[", "]"));
+ }
+}
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/audit/BulkDeleteApplicationEntryResultAudit.java b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/audit/BulkDeleteApplicationEntryResultAudit.java
new file mode 100644
index 000000000..906fa3692
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/audit/BulkDeleteApplicationEntryResultAudit.java
@@ -0,0 +1,92 @@
+package uk.gov.hmcts.appregister.applicationentryresult.audit;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import uk.gov.hmcts.appregister.audit.listener.diff.Auditable;
+import uk.gov.hmcts.appregister.audit.listener.diff.AuditableData;
+import uk.gov.hmcts.appregister.audit.listener.diff.BulkAuditFormatting;
+import uk.gov.hmcts.appregister.common.entity.AppListEntryResolution;
+import uk.gov.hmcts.appregister.common.entity.TableNames;
+import uk.gov.hmcts.appregister.common.enumeration.CrudEnum;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultItemDto;
+
+public record BulkDeleteApplicationEntryResultAudit(
+ Long resultId, List listIds, List entryIds, int resultCount, String results)
+ implements Auditable {
+
+ private static final String TABLE_NAME = TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS;
+ private static final String LIST_IDS_FIELD = "bulk_delete_results_list_ids";
+ private static final String ENTRY_IDS_FIELD = "bulk_delete_results_entry_ids";
+ private static final String RESULT_COUNT_FIELD = "bulk_delete_results_count";
+ private static final String RESULTS_FIELD = "bulk_delete_results";
+
+ @Override
+ public Long getId() {
+ return resultId;
+ }
+
+ @Override
+ public List extractAuditData(CrudEnum crudEnum) {
+ return List.of(
+ new AuditableData(
+ TABLE_NAME,
+ LIST_IDS_FIELD,
+ BulkAuditFormatting.formatSortedUuidArray(listIds)),
+ new AuditableData(
+ TABLE_NAME,
+ ENTRY_IDS_FIELD,
+ BulkAuditFormatting.formatSortedUuidArray(entryIds)),
+ new AuditableData(TABLE_NAME, RESULT_COUNT_FIELD, Integer.toString(resultCount)),
+ new AuditableData(TABLE_NAME, RESULTS_FIELD, results));
+ }
+
+ public static String formatRequestedResults(List requestedResults) {
+ return requestedResults.stream()
+ .map(
+ requestedResult ->
+ "{\"listId\":\"%s\",\"entryId\":\"%s\",\"resultId\":\"%s\"}"
+ .formatted(
+ requestedResult.getListId(),
+ requestedResult.getEntryId(),
+ requestedResult.getResultId()))
+ .collect(Collectors.joining(",", "[", "]"));
+ }
+
+ public static String formatDeletedResults(List deletedResults) {
+ return deletedResults.stream()
+ .sorted(
+ Comparator.comparing(
+ (AppListEntryResolution resolution) ->
+ resolution
+ .getApplicationList()
+ .getApplicationList()
+ .getId())
+ .thenComparing(
+ resolution -> resolution.getApplicationList().getId())
+ .thenComparing(AppListEntryResolution::getId))
+ .map(
+ resolution ->
+ ("{\"listId\":\"%s\",\"entryId\":\"%s\",\"resultId\":\"%s\","
+ + "\"sequenceNumber\":%s,\"resultCode\":\"%s\","
+ + "\"wording\":\"%s\",\"officer\":\"%s\"}")
+ .formatted(
+ resolution
+ .getApplicationList()
+ .getApplicationList()
+ .getUuid(),
+ resolution.getApplicationList().getUuid(),
+ resolution.getUuid(),
+ resolution.getApplicationList().getSequenceNumber(),
+ BulkAuditFormatting.escape(
+ resolution
+ .getResolutionCode()
+ .getResultCode()),
+ BulkAuditFormatting.escape(
+ resolution.getResolutionWording()),
+ BulkAuditFormatting.escape(
+ resolution.getResolutionOfficer())))
+ .collect(Collectors.joining(",", "[", "]"));
+ }
+}
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/controller/ApplicationEntryResultController.java b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/controller/ApplicationEntryResultController.java
index b59338e7c..9858ab7da 100644
--- a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/controller/ApplicationEntryResultController.java
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/controller/ApplicationEntryResultController.java
@@ -25,6 +25,7 @@
import uk.gov.hmcts.appregister.common.security.RoleNames;
import uk.gov.hmcts.appregister.common.util.PagingWrapper;
import uk.gov.hmcts.appregister.generated.api.ApplicationListEntryResultsApi;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultsDto;
import uk.gov.hmcts.appregister.generated.model.BulkResultDto;
import uk.gov.hmcts.appregister.generated.model.ResultCreateDto;
import uk.gov.hmcts.appregister.generated.model.ResultGetDto;
@@ -78,6 +79,13 @@ public ResponseEntity> bulkResultEntries(BulkResultDto bulkRe
.body(createdResults);
}
+ @Override
+ @PreAuthorize(RoleNames.USER_ROLE_OR_ADMIN_ROLE_RESTRICTION)
+ public ResponseEntity bulkDeleteResultEntries(BulkDeleteResultsDto bulkDeleteResultsDto) {
+ service.bulkDelete(bulkDeleteResultsDto);
+ return ResponseEntity.noContent().build();
+ }
+
/**
* Deletes an Application List Entry Result.
*
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/service/ApplicationEntryResultService.java b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/service/ApplicationEntryResultService.java
index c8b8ac2a3..8cf0c57c2 100644
--- a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/service/ApplicationEntryResultService.java
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/service/ApplicationEntryResultService.java
@@ -8,6 +8,7 @@
import uk.gov.hmcts.appregister.applicationentryresult.model.PayloadGetEntryResultInList;
import uk.gov.hmcts.appregister.common.concurrency.MatchResponse;
import uk.gov.hmcts.appregister.common.util.PagingWrapper;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultsDto;
import uk.gov.hmcts.appregister.generated.model.BulkResultDto;
import uk.gov.hmcts.appregister.generated.model.ResultCreateDto;
import uk.gov.hmcts.appregister.generated.model.ResultGetDto;
@@ -19,6 +20,8 @@
public interface ApplicationEntryResultService {
void delete(ListEntryResultDeleteArgs args);
+ void bulkDelete(BulkDeleteResultsDto bulkDeleteResultsDto);
+
/**
* Creates a new application list entry result.
*
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/service/ApplicationEntryResultServiceImpl.java b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/service/ApplicationEntryResultServiceImpl.java
index 79a46fe77..4bb941d1d 100644
--- a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/service/ApplicationEntryResultServiceImpl.java
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/service/ApplicationEntryResultServiceImpl.java
@@ -2,6 +2,7 @@
import jakarta.persistence.EntityManager;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
@@ -12,6 +13,8 @@
import org.springframework.transaction.annotation.Transactional;
import uk.gov.hmcts.appregister.applicationentryresult.audit.AppListEntryResultAuditOperation;
import uk.gov.hmcts.appregister.applicationentryresult.audit.ApplicationListEntryResultAudit;
+import uk.gov.hmcts.appregister.applicationentryresult.audit.BulkCreateApplicationEntryResultAudit;
+import uk.gov.hmcts.appregister.applicationentryresult.audit.BulkDeleteApplicationEntryResultAudit;
import uk.gov.hmcts.appregister.applicationentryresult.mapper.ApplicationListEntryResultEntityMapper;
import uk.gov.hmcts.appregister.applicationentryresult.mapper.ApplicationListEntryResultMapper;
import uk.gov.hmcts.appregister.applicationentryresult.model.ListEntryResultDeleteArgs;
@@ -24,6 +27,7 @@
import uk.gov.hmcts.appregister.applicationentryresult.validator.ApplicationEntryResultGetValidator;
import uk.gov.hmcts.appregister.applicationentryresult.validator.ApplicationEntryResultUpdateValidator;
import uk.gov.hmcts.appregister.applicationentryresult.validator.BulkApplicationEntryResultCreationValidator;
+import uk.gov.hmcts.appregister.applicationentryresult.validator.BulkApplicationEntryResultDeletionValidator;
import uk.gov.hmcts.appregister.audit.model.AuditableResult;
import uk.gov.hmcts.appregister.audit.service.AuditOperationService;
import uk.gov.hmcts.appregister.common.concurrency.MatchResponse;
@@ -38,6 +42,7 @@
import uk.gov.hmcts.appregister.common.security.UserProvider;
import uk.gov.hmcts.appregister.common.util.BeanUtil;
import uk.gov.hmcts.appregister.common.util.PagingWrapper;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultsDto;
import uk.gov.hmcts.appregister.generated.model.BulkResultDto;
import uk.gov.hmcts.appregister.generated.model.ResultCreateDto;
import uk.gov.hmcts.appregister.generated.model.ResultGetDto;
@@ -58,6 +63,8 @@ public class ApplicationEntryResultServiceImpl implements ApplicationEntryResult
private final ApplicationEntryResultCreationValidator creationValidator;
private final ApplicationEntryResultUpdateValidator updateValidator;
private final ApplicationEntryResultGetValidator applicationListGetValidator;
+ private final BulkApplicationEntryResultDeletionValidator
+ bulkApplicationEntryResultDeletionValidator;
private final BulkApplicationEntryResultCreationValidator
bulkApplicationEntryResultCreationValidator;
@@ -112,6 +119,67 @@ public void delete(ListEntryResultDeleteArgs args) {
});
}
+ @Override
+ @Transactional
+ public void bulkDelete(BulkDeleteResultsDto bulkDeleteResultsDto) {
+ bulkApplicationEntryResultDeletionValidator.validate(
+ bulkDeleteResultsDto,
+ (request, success) -> {
+ var deletedResults =
+ success.getResults().stream()
+ .map(
+ item ->
+ BeanUtil.copyBean(
+ item.validationSuccess()
+ .getAppListEntryResult()))
+ .toList();
+ var bulkAuditRequest =
+ new BulkDeleteApplicationEntryResultAudit(
+ deletedResults.isEmpty()
+ ? null
+ : deletedResults.getFirst().getId(),
+ success.getResults().stream()
+ .map(item -> item.args().listId())
+ .toList(),
+ success.getResults().stream()
+ .map(item -> item.args().entryId())
+ .toList(),
+ deletedResults.size(),
+ BulkDeleteApplicationEntryResultAudit.formatRequestedResults(
+ request.getResults()));
+ var bulkAuditResult =
+ new BulkDeleteApplicationEntryResultAudit(
+ deletedResults.isEmpty()
+ ? null
+ : deletedResults.getFirst().getId(),
+ success.getResults().stream()
+ .map(item -> item.args().listId())
+ .toList(),
+ success.getResults().stream()
+ .map(item -> item.args().entryId())
+ .toList(),
+ deletedResults.size(),
+ BulkDeleteApplicationEntryResultAudit.formatDeletedResults(
+ deletedResults));
+
+ auditService.processAudit(
+ bulkAuditRequest,
+ AppListEntryResultAuditOperation.BULK_DELETE_APP_LIST_ENTRY_RESULT,
+ ignored -> {
+ repository.deleteAllInBatch(
+ success.getResults().stream()
+ .map(
+ item ->
+ item.validationSuccess()
+ .getAppListEntryResult())
+ .toList());
+ entityManager.flush();
+ return Optional.of(new AuditableResult<>(null, bulkAuditResult));
+ });
+ return null;
+ });
+ }
+
@Override
@Transactional
public MatchResponse create(
@@ -319,16 +387,77 @@ public List bulkCreate(PayloadForCreateResults bulk
return bulkApplicationEntryResultCreationValidator.validate(
bulkResultDto,
(validate, success) -> {
- List createdResults = new ArrayList<>();
-
- // loop through each successful validation result and create the entry result
- for (PayloadForCreateEntryResult createValidationSuccesses :
- success.getResults()) {
- createdResults.add(
- getSelfService().create(createValidationSuccesses).getPayload());
+ var resultPayload = validate.getPayload().getResult();
+ var userEmail = userProvider.getEmail();
+ var entitiesToCreate =
+ new ArrayList(success.getResults().size());
+
+ for (var validatedItem : success.getResults()) {
+ var payload = validatedItem.payload();
+ var createSuccess = validatedItem.validationSuccess();
+ var entity =
+ applicationListEntryResultEntityMapper.toApplicationListEntryResult(
+ payload.getData(),
+ createSuccess
+ .getWordingSentence()
+ .substitute(payload.getData().getWordingFields())
+ .getSubstitutedString(),
+ createSuccess.getResolutionCode(),
+ createSuccess.getApplicationListEntry(),
+ userEmail);
+ entity.setResolutionOfficer(userEmail);
+ entitiesToCreate.add(entity);
}
- return createdResults;
+ return auditService
+ .processAudit(
+ AppListEntryResultAuditOperation
+ .BULK_CREATE_APP_LIST_ENTRY_RESULT,
+ ignored -> {
+ var persistedEntities =
+ new ArrayList<>(
+ repository.saveAll(entitiesToCreate));
+ entityManager.flush();
+ persistedEntities.sort(
+ Comparator.comparing(
+ AppListEntryResolution::getId));
+
+ var bulkAudit =
+ new BulkCreateApplicationEntryResultAudit(
+ persistedEntities.isEmpty()
+ ? null
+ : persistedEntities
+ .getFirst()
+ .getId(),
+ validate.getListId(),
+ success.getResults().stream()
+ .map(
+ item ->
+ item.payload()
+ .getEntryId())
+ .toList(),
+ persistedEntities.size(),
+ resultPayload.getResultCode(),
+ BulkCreateApplicationEntryResultAudit
+ .formatWordingFields(
+ resultPayload
+ .getWordingFields()),
+ BulkCreateApplicationEntryResultAudit
+ .formatCreatedResults(
+ persistedEntities));
+
+ var resultDtos =
+ persistedEntities.stream()
+ .map(
+ applicationListEntryResultMapper
+ ::toResultGetDto)
+ .toList();
+
+ return Optional.of(
+ new AuditableResult<>(resultDtos, bulkAudit));
+ })
+ .stream()
+ .toList();
});
}
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultCreationSuccess.java b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultCreationSuccess.java
index 831254853..21b83829d 100644
--- a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultCreationSuccess.java
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultCreationSuccess.java
@@ -4,11 +4,9 @@
import java.util.List;
import lombok.Getter;
import lombok.Setter;
-import uk.gov.hmcts.appregister.applicationentryresult.model.PayloadForCreateEntryResult;
-import uk.gov.hmcts.appregister.generated.model.ResultCreateDto;
@Getter
@Setter
public class BulkApplicationEntryResultCreationSuccess {
- final List> results = new ArrayList<>();
+ final List results = new ArrayList<>();
}
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultCreationValidator.java b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultCreationValidator.java
index e7e539e12..f549a598a 100644
--- a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultCreationValidator.java
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultCreationValidator.java
@@ -1,8 +1,10 @@
package uk.gov.hmcts.appregister.applicationentryresult.validator;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@@ -15,10 +17,15 @@
import uk.gov.hmcts.appregister.applicationentryresult.model.PayloadForCreateResults;
import uk.gov.hmcts.appregister.applicationlist.exception.ApplicationListError;
import uk.gov.hmcts.appregister.common.entity.ApplicationList;
-import uk.gov.hmcts.appregister.common.entity.model.EntryToList;
+import uk.gov.hmcts.appregister.common.entity.ApplicationListEntry;
+import uk.gov.hmcts.appregister.common.entity.ResolutionCode;
import uk.gov.hmcts.appregister.common.entity.repository.ApplicationListEntryRepository;
import uk.gov.hmcts.appregister.common.entity.repository.ApplicationListRepository;
+import uk.gov.hmcts.appregister.common.entity.repository.ResolutionCodeRepository;
import uk.gov.hmcts.appregister.common.exception.AppRegistryException;
+import uk.gov.hmcts.appregister.common.service.BusinessDateProvider;
+import uk.gov.hmcts.appregister.common.template.wording.WordingTemplateSentence;
+import uk.gov.hmcts.appregister.common.util.ReferenceDataSelectionUtil;
import uk.gov.hmcts.appregister.common.validator.Validator;
import uk.gov.hmcts.appregister.generated.model.BulkResultDto;
import uk.gov.hmcts.appregister.generated.model.ResultCreateDto;
@@ -34,6 +41,10 @@ public class BulkApplicationEntryResultCreationValidator
private final ApplicationListEntryRepository applicationListEntryRepository;
+ private final ResolutionCodeRepository resolutionCodeRepository;
+
+ private final BusinessDateProvider businessDateProvider;
+
@Override
public void validate(PayloadForCreateResults validatable) {
validate(validatable, (v, r) -> null);
@@ -48,77 +59,118 @@ public R validate(
R>
validateSuccess) {
- Optional applicationListOptional;
var entryIds = getEntryIds(validatable);
-
if (!entryIds.isEmpty()) {
validateNoDuplicateEntryIds(entryIds);
}
- // lets cope with the situation where we may not have a list but if
- // we do the entries should relate to those entries
+ ApplicationList validatedList = null;
if (validatable.getListId() != null) {
-
log.debug("Validating bulk result entries for list {}", validatable.getListId());
- applicationListOptional = applicationListRepository.findByUuid(validatable.getListId());
-
- if (applicationListOptional.isEmpty()) {
- throw new AppRegistryException(
- ApplicationListEntryResultError.APPLICATION_LIST_DOES_NOT_EXIST,
- "The list does not exist %s".formatted(validatable.getListId()));
- }
-
- validateEntryIdsProvided(entryIds);
+ validatedList =
+ applicationListRepository
+ .findByUuid(validatable.getListId())
+ .orElseThrow(
+ () ->
+ new AppRegistryException(
+ ApplicationListEntryResultError
+ .APPLICATION_LIST_DOES_NOT_EXIST,
+ "The list does not exist %s"
+ .formatted(validatable.getListId())));
+ validateParentApplicationListIsOpen(validatedList);
+ }
- // now validate the entries belong to the list
- boolean validated =
- applicationListEntryRepository.doesApplicationEntryBelongToApplicationList(
- entryIds, validatable.getListId());
+ validateEntryIdsProvided(entryIds);
- // if this is not valid then error
- if (!validated) {
+ List entries;
+ if (validatable.getListId() != null) {
+ entries =
+ applicationListEntryRepository.findByUuidsInSourceList(
+ validatable.getListId(), new HashSet<>(entryIds));
+ if (entries.size() < entryIds.size()) {
+ var activeEntries = applicationListEntryRepository.findActiveByUuids(entryIds);
+ if (activeEntries.size() < entryIds.size()) {
+ throw new AppRegistryException(
+ ApplicationListEntryResultError.APPLICATION_ENTRY_DOES_NOT_EXIST,
+ "One or more application list entries do not exist");
+ }
throw new AppRegistryException(
ApplicationListEntryResultError
.APPLICATION_ENTRY_RESULT_ENTRIES_NOT_IN_LIST,
"The bulk result entries are not in the same application list for %s"
.formatted(validatable.getListId()));
}
- }
-
- BulkApplicationEntryResultCreationSuccess bulkSuccess =
- new BulkApplicationEntryResultCreationSuccess();
-
- // process the validator according to whether we have the list ids or not
- if (validatable.getListId() != null) {
- List entryToListLst = getEntryForList(validatable.getListId(), entryIds);
-
- for (EntryToList entryToList : entryToListLst) {
- // validate the entries
- validateApplicationListEntryForResultCreation(
- entryToList, validatable.getPayload(), bulkSuccess);
- }
} else {
- // get all of the entry to list mappings
- List entryToListMapping;
- validateEntryIdsProvided(entryIds);
- entryToListMapping =
- applicationListEntryRepository.findApplicationListForAllEntries(entryIds);
-
- if (entryToListMapping.size() < entryIds.size()) {
+ entries = applicationListEntryRepository.findActiveByUuids(entryIds);
+ if (entries.size() < entryIds.size()) {
throw new AppRegistryException(
ApplicationListEntryResultError.APPLICATION_ENTRIES_NOT_ALL_EXIST,
"The entries are not all present");
}
- // now process all of the list id entries
- for (EntryToList entryToList : entryToListMapping) {
- log.debug(
- "Validating bulk result entries acquiring list as not present {}",
- entryToList.listId());
+ entries.forEach(
+ entry -> validateParentApplicationListIsOpen(entry.getApplicationList()));
+ }
+
+ ResolutionCode resolutionCode = null;
+ String wordingTemplate = null;
+ var createDto =
+ Optional.ofNullable(validatable.getPayload())
+ .map(BulkResultDto::getResult)
+ .orElse(null);
+ if (createDto != null && createDto.getResultCode() != null) {
+ var todayUk = businessDateProvider.currentUkDate();
+ var matchingCodes =
+ resolutionCodeRepository.findPrioritisingNullEndDate(
+ createDto.getResultCode(), todayUk);
+ if (matchingCodes.isEmpty()) {
+ throw new AppRegistryException(
+ ApplicationListEntryResultError.RESOLUTION_CODE_DOES_NOT_EXIST,
+ "No valid resolution code could be found %s"
+ .formatted(createDto.getResultCode()));
+ }
+ resolutionCode =
+ ReferenceDataSelectionUtil.selectFirstOrderedActiveRecord(
+ matchingCodes,
+ "result code",
+ createDto.getResultCode(),
+ todayUk,
+ ResolutionCode::getEndDate);
+ wordingTemplate = resolutionCode.getWording();
+ }
- validateApplicationListEntryForResultCreation(
- entryToList, validatable.getPayload(), bulkSuccess);
+ BulkApplicationEntryResultCreationSuccess bulkSuccess =
+ new BulkApplicationEntryResultCreationSuccess();
+ Map entriesByUuid = new HashMap<>();
+ for (var entry : entries) {
+ entriesByUuid.put(entry.getUuid(), entry);
+ }
+ for (var entryId : entryIds) {
+ var entry = entriesByUuid.get(entryId);
+ if (entry == null) {
+ throw new AppRegistryException(
+ ApplicationListEntryResultError.APPLICATION_ENTRY_DOES_NOT_EXIST,
+ "No application list entry exists %s".formatted(entryId));
}
+ bulkSuccess
+ .getResults()
+ .add(
+ new BulkApplicationEntryResultValidatedItem(
+ PayloadForCreateEntryResult.builder()
+ .listId(entry.getApplicationList().getUuid())
+ .entryId(entry.getUuid())
+ .data(createDto)
+ .build(),
+ ListEntryResultCreateValidationSuccess.builder()
+ .applicationList(entry.getApplicationList())
+ .applicationListEntry(entry)
+ .resolutionCode(resolutionCode)
+ .wordingSentence(
+ wordingTemplate == null
+ ? null
+ : WordingTemplateSentence.with(
+ wordingTemplate))
+ .build()));
}
// now pass the success details through to the callback so the logic can take place
@@ -149,40 +201,12 @@ private void validateEntryIdsProvided(List entryIds) {
}
}
- private List getEntryForList(UUID listId, List entryIds) {
- List entryToLists = new ArrayList<>();
-
- for (UUID entryId : entryIds) {
- entryToLists.add(new EntryToList(entryId, listId));
+ private void validateParentApplicationListIsOpen(ApplicationList validatable) {
+ if (!validatable.isOpen()) {
+ throw new AppRegistryException(
+ ApplicationListEntryResultError.APPLICATION_LIST_STATE_IS_INCORRECT,
+ "The application list id %s is not in the correct state or the application list is deleted %s"
+ .formatted(validatable.getUuid(), validatable.getStatus()));
}
-
- return entryToLists;
- }
-
- /**
- * validates the application list entry for result creation.
- *
- * @param entryToList The entry to list mapping
- * @param bulkSuccess The success object to populate on each successful validation
- */
- private void validateApplicationListEntryForResultCreation(
- EntryToList entryToList,
- BulkResultDto resultDto,
- BulkApplicationEntryResultCreationSuccess bulkSuccess) {
-
- // now validate all associated details using the preexisting create validator
- ResultCreateDto createDto = new ResultCreateDto();
- createDto.setResultCode(resultDto.getResult().getResultCode());
- createDto.setWordingFields(resultDto.getResult().getWordingFields());
-
- // now execute the validation to ensure all resulted data is relevant
- PayloadForCreateEntryResult result =
- PayloadForCreateEntryResult.builder()
- .listId(entryToList.listId())
- .entryId(entryToList.entryId())
- .data(createDto)
- .build();
-
- bulkSuccess.getResults().add(result);
}
}
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultDeletionSuccess.java b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultDeletionSuccess.java
new file mode 100644
index 000000000..54b536315
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultDeletionSuccess.java
@@ -0,0 +1,10 @@
+package uk.gov.hmcts.appregister.applicationentryresult.validator;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+
+@Getter
+public class BulkApplicationEntryResultDeletionSuccess {
+ private final List results = new ArrayList<>();
+}
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultDeletionValidatedItem.java b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultDeletionValidatedItem.java
new file mode 100644
index 000000000..890c999c8
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultDeletionValidatedItem.java
@@ -0,0 +1,6 @@
+package uk.gov.hmcts.appregister.applicationentryresult.validator;
+
+import uk.gov.hmcts.appregister.applicationentryresult.model.ListEntryResultDeleteArgs;
+
+public record BulkApplicationEntryResultDeletionValidatedItem(
+ ListEntryResultDeleteArgs args, ListEntryResultDeleteValidationSuccess validationSuccess) {}
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultDeletionValidator.java b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultDeletionValidator.java
new file mode 100644
index 000000000..6d823a2c8
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultDeletionValidator.java
@@ -0,0 +1,173 @@
+package uk.gov.hmcts.appregister.applicationentryresult.validator;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import uk.gov.hmcts.appregister.applicationentryresult.exception.ApplicationListEntryResultError;
+import uk.gov.hmcts.appregister.applicationentryresult.model.ListEntryResultDeleteArgs;
+import uk.gov.hmcts.appregister.applicationlist.exception.ApplicationListError;
+import uk.gov.hmcts.appregister.common.entity.AppListEntryResolution;
+import uk.gov.hmcts.appregister.common.entity.ApplicationList;
+import uk.gov.hmcts.appregister.common.entity.ApplicationListEntry;
+import uk.gov.hmcts.appregister.common.entity.model.EntryToList;
+import uk.gov.hmcts.appregister.common.entity.repository.AppListEntryResolutionRepository;
+import uk.gov.hmcts.appregister.common.entity.repository.ApplicationListEntryRepository;
+import uk.gov.hmcts.appregister.common.entity.repository.ApplicationListRepository;
+import uk.gov.hmcts.appregister.common.exception.AppRegistryException;
+import uk.gov.hmcts.appregister.common.validator.Validator;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultItemDto;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultsDto;
+
+@Component
+@RequiredArgsConstructor
+public class BulkApplicationEntryResultDeletionValidator
+ implements Validator {
+
+ private final ApplicationListRepository applicationListRepository;
+ private final ApplicationListEntryRepository applicationListEntryRepository;
+ private final AppListEntryResolutionRepository appListEntryResolutionRepository;
+
+ @Override
+ public void validate(BulkDeleteResultsDto validatable) {
+ validate(validatable, (request, success) -> null);
+ }
+
+ @Override
+ public R validate(
+ BulkDeleteResultsDto validatable,
+ BiFunction
+ validateSuccess) {
+ var results =
+ Optional.ofNullable(validatable)
+ .map(BulkDeleteResultsDto::getResults)
+ .map(ArrayList::new)
+ .orElseGet(ArrayList::new);
+
+ if (results.isEmpty()) {
+ throw new AppRegistryException(
+ ApplicationListError.ENTRY_NOT_PROVIDED, "No result delete items provided");
+ }
+
+ validateNoDuplicateItems(results);
+
+ var listIds = collectDistinct(results, BulkDeleteResultItemDto::getListId);
+ var entryIds = collectDistinct(results, BulkDeleteResultItemDto::getEntryId);
+ var resultIds = collectDistinct(results, BulkDeleteResultItemDto::getResultId);
+
+ var listsByUuid =
+ applicationListRepository.findByUuidIncludingDeleteIn(listIds).stream()
+ .collect(Collectors.toMap(ApplicationList::getUuid, Function.identity()));
+ var entriesByUuid =
+ applicationListEntryRepository.findByUuidIncludingDeleteIn(entryIds).stream()
+ .collect(
+ Collectors.toMap(
+ ApplicationListEntry::getUuid, Function.identity()));
+ var activeEntriesByKey =
+ applicationListEntryRepository
+ .findActiveByUuidsAndApplicationListUuids(entryIds, listIds)
+ .stream()
+ .collect(
+ Collectors.toMap(
+ entry ->
+ new EntryToList(
+ entry.getUuid(),
+ entry.getApplicationList().getUuid()),
+ Function.identity()));
+ var resultsByUuid =
+ appListEntryResolutionRepository.findByUuidIn(resultIds).stream()
+ .collect(
+ Collectors.toMap(
+ AppListEntryResolution::getUuid, Function.identity()));
+
+ var success = new BulkApplicationEntryResultDeletionSuccess();
+ for (var result : results) {
+ var args =
+ new ListEntryResultDeleteArgs(
+ result.getListId(), result.getEntryId(), result.getResultId());
+ var applicationList = listsByUuid.get(args.listId());
+ if (applicationList == null) {
+ throw new AppRegistryException(
+ ApplicationListEntryResultError.APPLICATION_LIST_DOES_NOT_EXIST,
+ "The application list does not exist %s".formatted(args.listId()));
+ }
+ if (applicationList.isDeleted() || !applicationList.isOpen()) {
+ throw new AppRegistryException(
+ ApplicationListEntryResultError.APPLICATION_LIST_STATE_IS_INCORRECT,
+ "The application list id %s is not in the correct state or the application list is deleted %s"
+ .formatted(args.listId(), applicationList.getStatus()));
+ }
+
+ if (!entriesByUuid.containsKey(args.entryId())) {
+ throw new AppRegistryException(
+ ApplicationListEntryResultError.APPLICATION_ENTRY_DOES_NOT_EXIST,
+ "No application list entry exists %s".formatted(args.entryId()));
+ }
+
+ var activeEntryKey = new EntryToList(args.entryId(), args.listId());
+ var applicationListEntry = activeEntriesByKey.get(activeEntryKey);
+ if (applicationListEntry == null) {
+ throw new AppRegistryException(
+ ApplicationListEntryResultError.APPLICATION_ENTRY_NOT_WITHIN_LIST,
+ "The application list entry %s does not belong to list %s"
+ .formatted(args.entryId(), args.listId()));
+ }
+
+ var appListEntryResult = resultsByUuid.get(args.resultId());
+ if (appListEntryResult == null) {
+ throw new AppRegistryException(
+ ApplicationListEntryResultError.APPLICATION_ENTRY_RESULT_DOES_NOT_EXIST,
+ "No application list entry result was found for UUID '%s'"
+ .formatted(args.resultId()));
+ }
+
+ if (!args.entryId().equals(appListEntryResult.getApplicationList().getUuid())) {
+ throw new AppRegistryException(
+ ApplicationListEntryResultError.APPLICATION_ENTRY_RESULT_NOT_WITHIN_ENTRY,
+ ("No application list entry result was found for UUID '%s' that"
+ + " belongs to the specified entry")
+ .formatted(args.resultId()));
+ }
+
+ success.getResults()
+ .add(
+ new BulkApplicationEntryResultDeletionValidatedItem(
+ args,
+ new ListEntryResultDeleteValidationSuccess(
+ null,
+ null,
+ applicationList,
+ applicationListEntry,
+ appListEntryResult)));
+ }
+
+ return validateSuccess.apply(validatable, success);
+ }
+
+ private List collectDistinct(
+ List results, Function mapper) {
+ return results.stream().map(mapper).distinct().toList();
+ }
+
+ private void validateNoDuplicateItems(List results) {
+ Set uniqueKeys = new HashSet<>();
+ for (var result : results) {
+ var key =
+ "%s|%s|%s"
+ .formatted(
+ result.getListId(), result.getEntryId(), result.getResultId());
+ if (!uniqueKeys.add(key)) {
+ throw new AppRegistryException(
+ ApplicationListError.ENTRY_IDS_MUST_BE_UNIQUE,
+ "Duplicate result delete items are not allowed");
+ }
+ }
+ }
+}
diff --git a/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultValidatedItem.java b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultValidatedItem.java
new file mode 100644
index 000000000..ed5c18fc6
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultValidatedItem.java
@@ -0,0 +1,8 @@
+package uk.gov.hmcts.appregister.applicationentryresult.validator;
+
+import uk.gov.hmcts.appregister.applicationentryresult.model.PayloadForCreateEntryResult;
+import uk.gov.hmcts.appregister.generated.model.ResultCreateDto;
+
+public record BulkApplicationEntryResultValidatedItem(
+ PayloadForCreateEntryResult payload,
+ ListEntryResultCreateValidationSuccess validationSuccess) {}
diff --git a/src/main/java/uk/gov/hmcts/appregister/audit/listener/DataAuditLogger.java b/src/main/java/uk/gov/hmcts/appregister/audit/listener/DataAuditLogger.java
index a296c2a10..18d829523 100644
--- a/src/main/java/uk/gov/hmcts/appregister/audit/listener/DataAuditLogger.java
+++ b/src/main/java/uk/gov/hmcts/appregister/audit/listener/DataAuditLogger.java
@@ -36,6 +36,8 @@ public class DataAuditLogger extends AuditOperationLifecycleListenerAdapter {
/** Represents a null value. We default to a null string. */
public static final String EMPTY_VALUE = "";
+ private static final int AUDIT_VARCHAR_LIMIT = 4000;
+
@Value("${spring.jpa.properties.hibernate.default_schema}")
private String schemaName;
@@ -278,7 +280,7 @@ private static void setNewAndOldAuditValues(
}
private static void setOldAuditValue(DataAudit audit, String value) {
- if (value != null && value.length() > VARCHAR_AUDIT_LIMIT) {
+ if (shouldUseClob(value)) {
audit.setOldValue(null);
audit.setOldClobValue(value);
return;
@@ -289,7 +291,7 @@ private static void setOldAuditValue(DataAudit audit, String value) {
}
private static void setNewAuditValue(DataAudit audit, String value) {
- if (value != null && value.length() > VARCHAR_AUDIT_LIMIT) {
+ if (shouldUseClob(value)) {
audit.setNewValue(null);
audit.setNewClobValue(value);
return;
@@ -299,6 +301,10 @@ private static void setNewAuditValue(DataAudit audit, String value) {
audit.setNewClobValue(null);
}
+ private static boolean shouldUseClob(String value) {
+ return value != null && value.length() > AUDIT_VARCHAR_LIMIT;
+ }
+
private static void logAuditValues(
AuditableData primaryDiff, AuditableData secondaryDiff, boolean primaryOld) {
if (primaryOld && secondaryDiff == null) {
diff --git a/src/main/java/uk/gov/hmcts/appregister/audit/listener/diff/BulkAuditFormatting.java b/src/main/java/uk/gov/hmcts/appregister/audit/listener/diff/BulkAuditFormatting.java
new file mode 100644
index 000000000..f810967ef
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/appregister/audit/listener/diff/BulkAuditFormatting.java
@@ -0,0 +1,27 @@
+package uk.gov.hmcts.appregister.audit.listener.diff;
+
+import java.util.Collection;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+public final class BulkAuditFormatting {
+
+ private BulkAuditFormatting() {
+ // Utility class.
+ }
+
+ public static String formatSortedUuidArray(Collection entryIds) {
+ return entryIds.stream()
+ .sorted()
+ .map("\"%s\""::formatted)
+ .collect(Collectors.joining(",", "[", "]"));
+ }
+
+ public static String escape(String value) {
+ if (value == null) {
+ return "";
+ }
+
+ return value.replace("\\", "\\\\").replace("\"", "\\\"");
+ }
+}
diff --git a/src/main/java/uk/gov/hmcts/appregister/common/entity/AppListEntryFeeStatus.java b/src/main/java/uk/gov/hmcts/appregister/common/entity/AppListEntryFeeStatus.java
index b48978643..fcb0249b2 100644
--- a/src/main/java/uk/gov/hmcts/appregister/common/entity/AppListEntryFeeStatus.java
+++ b/src/main/java/uk/gov/hmcts/appregister/common/entity/AppListEntryFeeStatus.java
@@ -58,7 +58,7 @@ public class AppListEntryFeeStatus implements Changeable, Accountable, Versionab
@JoinColumn(name = "alefs_ale_id", nullable = false)
private ApplicationListEntry appListEntry;
- @Column(name = "alefs_payment_reference")
+ @Column(name = "alefs_payment_reference", length = 15)
@Audit(action = {CrudEnum.CREATE, CrudEnum.DELETE})
private String alefsPaymentReference;
diff --git a/src/main/java/uk/gov/hmcts/appregister/common/entity/DataAudit.java b/src/main/java/uk/gov/hmcts/appregister/common/entity/DataAudit.java
index cb4b9a47e..fd30b7858 100644
--- a/src/main/java/uk/gov/hmcts/appregister/common/entity/DataAudit.java
+++ b/src/main/java/uk/gov/hmcts/appregister/common/entity/DataAudit.java
@@ -69,9 +69,6 @@ public class DataAudit implements Changeable, Accountable {
@Size(max = 4000)
private String newValue;
- @Column(name = "new_clob_value")
- private String newClobValue;
-
@Column(name = "user_id")
@Size(max = 32)
private String createdUser;
@@ -86,6 +83,9 @@ public class DataAudit implements Changeable, Accountable {
@Column(name = "old_clob_value")
private String oldClobValue;
+ @Column(name = "new_clob_value")
+ private String newClobValue;
+
@Column(name = "related_key")
private Long relatedKey;
diff --git a/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/AppListEntryOfficialRepository.java b/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/AppListEntryOfficialRepository.java
index 62a5a3b8d..dc69c1721 100644
--- a/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/AppListEntryOfficialRepository.java
+++ b/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/AppListEntryOfficialRepository.java
@@ -43,4 +43,18 @@ public interface AppListEntryOfficialRepository extends JpaRepository entryIds);
}
diff --git a/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/AppListEntryResolutionRepository.java b/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/AppListEntryResolutionRepository.java
index 2e46ca32e..a4b1c8785 100644
--- a/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/AppListEntryResolutionRepository.java
+++ b/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/AppListEntryResolutionRepository.java
@@ -6,6 +6,7 @@
import java.util.UUID;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import uk.gov.hmcts.appregister.common.entity.AppListEntryResolution;
@@ -66,6 +67,14 @@ Optional findByUuidAndApplicationList_Uuid(
Optional findByUuid(UUID resolutionUuid);
+ @EntityGraph(
+ attributePaths = {
+ "applicationList",
+ "applicationList.applicationList",
+ "resolutionCode"
+ })
+ List findByUuidIn(Collection resolutionUuids);
+
/**
* Finds an AppListEntryResolution by the UUID of the associated application list entry.
*
diff --git a/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/ApplicationListEntryRepository.java b/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/ApplicationListEntryRepository.java
index b29a72ecd..083462004 100644
--- a/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/ApplicationListEntryRepository.java
+++ b/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/ApplicationListEntryRepository.java
@@ -868,6 +868,15 @@ List findByApplicationListIdsForPrinting(
""")
Optional findByUuidIncludingDelete(UUID entryId);
+ @Query(
+ """
+ SELECT ale
+ FROM ApplicationListEntry ale
+ WHERE ale.uuid IN :entryIds
+ """)
+ List findByUuidIncludingDeleteIn(
+ @Param("entryIds") Collection entryIds);
+
/**
* Finds all entities with the given IDs, within the associated list.
*
@@ -901,6 +910,23 @@ List findByApplicationListIdsForPrinting(
""")
List findByUuidsInSourceList(UUID sourceListUuid, Set requestedIds);
+ /**
+ * Finds all non-deleted entries matching the supplied UUIDs, excluding rows whose parent lists
+ * are deleted.
+ *
+ * @param entryIds the entry UUIDs
+ * @return matching entries
+ */
+ @Query(
+ """
+ SELECT ale
+ FROM ApplicationListEntry ale
+ WHERE ale.uuid IN :entryIds
+ AND (ale.deleted IS NULL OR ale.deleted <> 'Y')
+ AND (ale.applicationList.deleted IS NULL OR ale.applicationList.deleted <> 'Y')
+ """)
+ List findActiveByUuids(List entryIds);
+
/**
* Bulk-move entries to a new application list using a single JPQL UPDATE. Returns number of
* rows updated.
@@ -949,6 +975,19 @@ int bulkMoveByUuidAndSourceList(
Optional findActiveByUuidAndApplicationListUuid(
@Param("entryUuid") UUID entryUuid, @Param("listUuid") UUID listUuid);
+ @Query(
+ """
+ SELECT ale
+ FROM ApplicationListEntry ale
+ WHERE ale.uuid IN :entryUuids
+ AND ale.applicationList.uuid IN :listUuids
+ AND (ale.deleted IS NULL OR ale.deleted <> 'Y')
+ AND (ale.applicationList.deleted IS NULL OR ale.applicationList.deleted <> 'Y')
+ """)
+ List findActiveByUuidsAndApplicationListUuids(
+ @Param("entryUuids") Collection entryUuids,
+ @Param("listUuids") Collection listUuids);
+
/**
* Soft-deletes an application list entry by UUID.
*
diff --git a/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/ApplicationListRepository.java b/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/ApplicationListRepository.java
index 5be74d46f..abb483f4c 100644
--- a/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/ApplicationListRepository.java
+++ b/src/main/java/uk/gov/hmcts/appregister/common/entity/repository/ApplicationListRepository.java
@@ -69,6 +69,14 @@ public interface ApplicationListRepository extends JpaRepository findByUuidIncludingDelete(UUID id);
+ @Query(
+ """
+ SELECT al
+ FROM ApplicationList al
+ WHERE al.uuid IN :ids
+ """)
+ List findByUuidIncludingDeleteIn(@Param("ids") Collection ids);
+
@Query(
"""
SELECT al
diff --git a/src/main/java/uk/gov/hmcts/appregister/config/StartupSecretDiagnosticsLogger.java b/src/main/java/uk/gov/hmcts/appregister/config/StartupSecretDiagnosticsLogger.java
new file mode 100644
index 000000000..de6d37a4d
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/appregister/config/StartupSecretDiagnosticsLogger.java
@@ -0,0 +1,70 @@
+package uk.gov.hmcts.appregister.config;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+
+/**
+ * Logs a narrow startup diagnostic for secret-backed configuration values that are useful when
+ * debugging config tree mounts in non-production environments.
+ */
+@Component
+@Slf4j
+public class StartupSecretDiagnosticsLogger implements ApplicationRunner {
+ static final String POSTGRES_DATABASE = "POSTGRES_DATABASE";
+ static final String POSTGRES_DATABASE_DEFAULT = "appreg-db";
+ static final String CSDS_BASE_URL = "CSDS_BASE_URL";
+ static final String CSDS_BASE_URL_DEFAULT = "http://noop";
+
+ private final Environment environment;
+ private final Path mountedSecretsPath;
+
+ public StartupSecretDiagnosticsLogger(
+ Environment environment,
+ @Value("${appreg.startup-secret-diagnostics.mount-path:/mnt/secrets/appreg}")
+ String mountedSecretsPath) {
+ this.environment = environment;
+ this.mountedSecretsPath = Path.of(mountedSecretsPath);
+ }
+
+ @Override
+ public void run(ApplicationArguments args) {
+ logDiagnostic(inspect(POSTGRES_DATABASE, POSTGRES_DATABASE_DEFAULT));
+ logDiagnostic(inspect(CSDS_BASE_URL, CSDS_BASE_URL_DEFAULT));
+ }
+
+ StartupSecretDiagnostic inspect(String propertyName, String defaultValue) {
+ var resolvedValue = environment.getProperty(propertyName, defaultValue);
+ var mountedSecretPath = mountedSecretsPath.resolve(propertyName);
+
+ return new StartupSecretDiagnostic(
+ propertyName,
+ resolvedValue,
+ defaultValue.equals(resolvedValue),
+ Files.exists(mountedSecretPath),
+ Files.isReadable(mountedSecretPath));
+ }
+
+ private void logDiagnostic(StartupSecretDiagnostic diagnostic) {
+ log.info(
+ "Startup secret diagnostic: property={} resolvedValue={} usingDefault={} mountedFilePresent={}"
+ + " mountedFileReadable={}",
+ diagnostic.propertyName(),
+ diagnostic.resolvedValue(),
+ diagnostic.usingDefault(),
+ diagnostic.mountedFilePresent(),
+ diagnostic.mountedFileReadable());
+ }
+
+ record StartupSecretDiagnostic(
+ String propertyName,
+ String resolvedValue,
+ boolean usingDefault,
+ boolean mountedFilePresent,
+ boolean mountedFileReadable) {}
+}
diff --git a/src/main/resources/openapi/components/schemas/application-codes/application-code-get-detail-dto.yaml b/src/main/resources/openapi/components/schemas/application-codes/application-code-get-detail-dto.yaml
index df805e8e5..48127567f 100644
--- a/src/main/resources/openapi/components/schemas/application-codes/application-code-get-detail-dto.yaml
+++ b/src/main/resources/openapi/components/schemas/application-codes/application-code-get-detail-dto.yaml
@@ -38,6 +38,7 @@ properties:
respondents to be created and linked.
feeReference:
type: string
+ maxLength: 12
nullable: true
description: A short reference code that identifies a Fee.
example: "CO5.2"
@@ -70,6 +71,7 @@ properties:
offsiteFeeReference:
type: string
+ maxLength: 12
nullable: true
description: A short reference code that identifies a offsite Fee.
example: "CO5.2"
diff --git a/src/main/resources/openapi/components/schemas/application-codes/application-code-get-summary-dto.yaml b/src/main/resources/openapi/components/schemas/application-codes/application-code-get-summary-dto.yaml
index d343b149c..6af99fbe2 100644
--- a/src/main/resources/openapi/components/schemas/application-codes/application-code-get-summary-dto.yaml
+++ b/src/main/resources/openapi/components/schemas/application-codes/application-code-get-summary-dto.yaml
@@ -35,6 +35,7 @@ properties:
respondents to be created and linked.
feeReference:
type: string
+ maxLength: 12
nullable: true
description: A short reference code that identifies a Fee.
example: "CO5.2"
@@ -65,6 +66,7 @@ properties:
description: Descriptive text for the fee.
offsiteFeeReference:
type: string
+ maxLength: 12
nullable: true
description: A short reference code that identifies a offsite Fee.
example: "CO1.1"
diff --git a/src/main/resources/openapi/components/schemas/application-list-entries/bulk-fee-details-dto.yaml b/src/main/resources/openapi/components/schemas/application-list-entries/bulk-fee-details-dto.yaml
index c90a92742..4efe66d1c 100644
--- a/src/main/resources/openapi/components/schemas/application-list-entries/bulk-fee-details-dto.yaml
+++ b/src/main/resources/openapi/components/schemas/application-list-entries/bulk-fee-details-dto.yaml
@@ -20,5 +20,5 @@ properties:
type: string
description: Optional reference used to identify the payment.
example: "PAY-001"
- maxLength: 100
+ maxLength: 15
minLength: 1
diff --git a/src/main/resources/openapi/components/schemas/application-list-entry-results/bulk-delete-result-item-dto.yaml b/src/main/resources/openapi/components/schemas/application-list-entry-results/bulk-delete-result-item-dto.yaml
new file mode 100644
index 000000000..a2dce31b0
--- /dev/null
+++ b/src/main/resources/openapi/components/schemas/application-list-entry-results/bulk-delete-result-item-dto.yaml
@@ -0,0 +1,22 @@
+type: object
+additionalProperties: false
+required:
+ - listId
+ - entryId
+ - resultId
+properties:
+ listId:
+ type: string
+ format: uuid
+ description: Public identifier of the Application List.
+ example: "123e4567-e89b-12d3-a456-426655440000"
+ entryId:
+ type: string
+ format: uuid
+ description: Public identifier of the Application List Entry.
+ example: "123e4567-e89b-12d3-a456-426655440100"
+ resultId:
+ type: string
+ format: uuid
+ description: Public identifier of the Application List Entry Result.
+ example: "123e4567-e89b-12d3-a456-426655440200"
diff --git a/src/main/resources/openapi/components/schemas/application-list-entry-results/bulk-delete-results-dto.yaml b/src/main/resources/openapi/components/schemas/application-list-entry-results/bulk-delete-results-dto.yaml
new file mode 100644
index 000000000..36d26cd62
--- /dev/null
+++ b/src/main/resources/openapi/components/schemas/application-list-entry-results/bulk-delete-results-dto.yaml
@@ -0,0 +1,14 @@
+type: object
+description: Delete multiple Application List Entry Results atomically.
+additionalProperties: false
+required:
+ - results
+properties:
+ results:
+ type: array
+ minItems: 1
+ description: >
+ Result delete requests to execute. Each item is validated using the same rules as the
+ single-result delete endpoint. If any item is invalid, no results are deleted.
+ items:
+ $ref: './bulk-delete-result-item-dto.yaml'
diff --git a/src/main/resources/openapi/paths/application-list-entry-results/entries-bulk-result.yaml b/src/main/resources/openapi/paths/application-list-entry-results/entries-bulk-result.yaml
index 8341d73b1..026ba490a 100644
--- a/src/main/resources/openapi/paths/application-list-entry-results/entries-bulk-result.yaml
+++ b/src/main/resources/openapi/paths/application-list-entry-results/entries-bulk-result.yaml
@@ -33,3 +33,33 @@ post:
"404": { $ref: "../../components/responses/not-found-error.yaml" }
"409": { $ref: "../../components/responses/conflict-error.yaml" }
"500": { $ref: "../../components/responses/internal-server-error.yaml" }
+
+delete:
+ operationId: bulkDeleteResultEntries
+ tags: [ application-list-entry-results ]
+ summary: Delete multiple Application List Entry Results.
+ description: >
+ Atomically deletes the specified Application List Entry Results. Each item is validated using
+ the same rules as the single delete endpoint. If any item cannot be deleted, no Results are
+ removed.
+ requestBody:
+ required: true
+ content:
+ application/vnd.hmcts.appreg.v1+json:
+ schema:
+ $ref: "../../components/schemas/application-list-entry-results/bulk-delete-results-dto.yaml"
+ responses:
+ "204":
+ description: Results deleted for all supplied items.
+ headers:
+ Vary:
+ description: Response varies by Accept for media-type versioning.
+ schema:
+ type: string
+ example: Accept
+ "400": { $ref: "../../components/responses/bad-request-error.yaml" }
+ "401": { $ref: "../../components/responses/unauthorized-error.yaml" }
+ "403": { $ref: "../../components/responses/forbidden-error.yaml" }
+ "404": { $ref: "../../components/responses/not-found-error.yaml" }
+ "409": { $ref: "../../components/responses/conflict-error.yaml" }
+ "500": { $ref: "../../components/responses/internal-server-error.yaml" }
diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkMoveApplicationListEntriesAuditTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkMoveApplicationListEntriesAuditTest.java
new file mode 100644
index 000000000..8293d36e9
--- /dev/null
+++ b/src/test/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkMoveApplicationListEntriesAuditTest.java
@@ -0,0 +1,92 @@
+package uk.gov.hmcts.appregister.applicationentry.audit;
+
+import java.util.List;
+import java.util.UUID;
+import lombok.val;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import uk.gov.hmcts.appregister.audit.listener.diff.AuditableData;
+import uk.gov.hmcts.appregister.common.entity.ApplicationListEntry;
+import uk.gov.hmcts.appregister.common.entity.TableNames;
+import uk.gov.hmcts.appregister.common.enumeration.CrudEnum;
+
+class BulkMoveApplicationListEntriesAuditTest {
+
+ @Test
+ void extractAuditData_includesSourceTargetAndMovedEntries() {
+ val sourceListId = 10L;
+ val targetListId = 20L;
+ val sourceListUuid = UUID.randomUUID();
+ val targetListUuid = UUID.randomUUID();
+ val firstEntry = entry(101L, UUID.randomUUID(), (short) 2, 7L);
+ val secondEntry = entry(102L, UUID.randomUUID(), (short) 4, 8L);
+
+ val audit =
+ BulkMoveApplicationListEntriesAudit.forState(
+ sourceListId,
+ sourceListUuid,
+ targetListId,
+ targetListUuid,
+ List.of(firstEntry, secondEntry));
+
+ val auditData = audit.extractAuditData(CrudEnum.UPDATE);
+
+ Assertions.assertEquals(sourceListId, audit.getId());
+ Assertions.assertTrue(
+ containsAuditRow(
+ auditData,
+ TableNames.APPLICATION_LISTS_ENTRY,
+ "bulk_move_source_list_uuid",
+ sourceListUuid.toString()));
+ Assertions.assertTrue(
+ containsAuditRow(
+ auditData,
+ TableNames.APPLICATION_LISTS_ENTRY,
+ "bulk_move_target_list_uuid",
+ targetListUuid.toString()));
+ Assertions.assertTrue(
+ containsAuditRow(
+ auditData,
+ TableNames.APPLICATION_LISTS_ENTRY,
+ "bulk_move_entry_count",
+ "2"));
+ val movedEntriesValue = fieldValue(auditData, "bulk_move_entries");
+ Assertions.assertNotNull(movedEntriesValue);
+ Assertions.assertTrue(movedEntriesValue.contains(firstEntry.getUuid().toString()));
+ Assertions.assertTrue(movedEntriesValue.contains(secondEntry.getUuid().toString()));
+ Assertions.assertTrue(movedEntriesValue.contains("\"sequenceNumber\":2"));
+ Assertions.assertTrue(movedEntriesValue.contains("\"sequenceNumber\":4"));
+ Assertions.assertTrue(movedEntriesValue.contains("\"version\":7"));
+ Assertions.assertTrue(movedEntriesValue.contains("\"version\":8"));
+ }
+
+ private ApplicationListEntry entry(Long id, UUID uuid, short sequenceNumber, Long version) {
+ val entry = new ApplicationListEntry();
+ entry.setId(id);
+ entry.setUuid(uuid);
+ entry.setSequenceNumber(sequenceNumber);
+ entry.setVersion(version);
+ return entry;
+ }
+
+ private boolean containsAuditRow(
+ List auditData, String tableName, String fieldName, String value) {
+ return auditData.stream()
+ .anyMatch(
+ row ->
+ tableName.equals(row.getTableName())
+ && fieldName.equals(row.getFieldName())
+ && value.equals(row.getValue()));
+ }
+
+ private String fieldValue(List auditData, String fieldName) {
+ return auditData.stream()
+ .filter(
+ row ->
+ TableNames.APPLICATION_LISTS_ENTRY.equals(row.getTableName())
+ && fieldName.equals(row.getFieldName()))
+ .map(AuditableData::getValue)
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkUpdateFeesAuditTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkUpdateFeesAuditTest.java
new file mode 100644
index 000000000..df7a69c2c
--- /dev/null
+++ b/src/test/java/uk/gov/hmcts/appregister/applicationentry/audit/BulkUpdateFeesAuditTest.java
@@ -0,0 +1,107 @@
+package uk.gov.hmcts.appregister.applicationentry.audit;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import lombok.val;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import uk.gov.hmcts.appregister.audit.listener.diff.AuditableData;
+import uk.gov.hmcts.appregister.common.entity.AppListEntryFeeStatus;
+import uk.gov.hmcts.appregister.common.entity.ApplicationList;
+import uk.gov.hmcts.appregister.common.entity.ApplicationListEntry;
+import uk.gov.hmcts.appregister.common.entity.TableNames;
+import uk.gov.hmcts.appregister.common.enumeration.CrudEnum;
+import uk.gov.hmcts.appregister.common.enumeration.FeeStatusType;
+import uk.gov.hmcts.appregister.generated.model.BulkFeeDetailsDto;
+import uk.gov.hmcts.appregister.generated.model.PaymentStatus;
+
+class BulkUpdateFeesAuditTest {
+
+ @Test
+ void extractAuditData_includesEntryMetadataFeeDetailsAndOffsiteEntries() {
+ val listUuid = UUID.randomUUID();
+ val entryId = UUID.randomUUID();
+ val audit =
+ new BulkUpdateFeesAudit(
+ 10L,
+ listUuid,
+ List.of(entryId),
+ 1,
+ "[{\"paymentStatus\":\"PAID\"}]",
+ "[\"" + entryId + "\"]");
+
+ val auditData = audit.extractAuditData(CrudEnum.UPDATE);
+
+ Assertions.assertTrue(
+ containsAuditRow(
+ auditData,
+ TableNames.APPLICATION_LISTS_ENTRY,
+ "bulk_fees_list_uuid",
+ listUuid.toString()));
+ Assertions.assertTrue(
+ containsAuditRow(
+ auditData,
+ TableNames.APPLICATION_LISTS_ENTRY,
+ "bulk_fees_fee_details",
+ "[{\"paymentStatus\":\"PAID\"}]"));
+ Assertions.assertTrue(
+ containsAuditRow(
+ auditData,
+ TableNames.APPLICATION_LISTS_ENTRY,
+ "bulk_fees_offsite_entry_ids",
+ "[\"" + entryId + "\"]"));
+ }
+
+ @Test
+ void formatHelpers_renderExistingStatusesRequestedFeeDetailsAndOffsiteEntries() {
+ val applicationList = new ApplicationList();
+ applicationList.setId(10L);
+ applicationList.setUuid(UUID.randomUUID());
+
+ val entry = new ApplicationListEntry();
+ entry.setId(100L);
+ entry.setUuid(UUID.randomUUID());
+ entry.setSequenceNumber((short) 2);
+ entry.setApplicationList(applicationList);
+
+ val existingStatus = new AppListEntryFeeStatus();
+ existingStatus.setId(200L);
+ existingStatus.setAppListEntry(entry);
+ existingStatus.setAlefsFeeStatus(FeeStatusType.REMITTED);
+ existingStatus.setAlefsFeeStatusDate(LocalDate.of(2026, 7, 20));
+ existingStatus.setAlefsPaymentReference("PAY-001");
+
+ val requestedFeeDetails =
+ new BulkFeeDetailsDto()
+ .paymentStatus(PaymentStatus.PAID)
+ .statusDate(LocalDate.of(2026, 7, 20))
+ .paymentReference("PAY-002");
+
+ Assertions.assertEquals(
+ ("[{\"entryId\":\"%s\",\"status\":\"REMITTED\",\"statusDate\":\"2026-07-20\","
+ + "\"paymentReference\":\"PAY-001\"}]")
+ .formatted(entry.getUuid()),
+ BulkUpdateFeesAudit.formatExistingFeeStatuses(List.of(existingStatus)));
+ Assertions.assertEquals(
+ "[{\"paymentStatus\":\"PAID\",\"statusDate\":\"2026-07-20\","
+ + "\"paymentReference\":\"PAY-002\",\"hasOffsiteFee\":true}]",
+ BulkUpdateFeesAudit.formatRequestedFeeDetails(List.of(requestedFeeDetails), true));
+ Assertions.assertEquals(
+ "[\"" + entry.getUuid() + "\"]",
+ BulkUpdateFeesAudit.formatOffsiteEntryIds(
+ Set.of(entry.getId()), Map.of(entry.getId(), entry.getUuid())));
+ }
+
+ private boolean containsAuditRow(
+ List auditData, String tableName, String fieldName, String value) {
+ return auditData.stream()
+ .anyMatch(
+ row ->
+ tableName.equals(row.getTableName())
+ && fieldName.equals(row.getFieldName())
+ && value.equals(row.getValue()));
+ }
+}
diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/ApplicationEntryServiceImplTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/ApplicationEntryServiceImplTest.java
index c87a6560e..27e6ae335 100644
--- a/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/ApplicationEntryServiceImplTest.java
+++ b/src/test/java/uk/gov/hmcts/appregister/applicationentry/service/ApplicationEntryServiceImplTest.java
@@ -6,6 +6,7 @@
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anySet;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.notNull;
@@ -36,6 +37,7 @@
import java.util.stream.IntStream;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
+import org.assertj.core.util.Lists;
import org.instancio.Instancio;
import org.instancio.settings.Keys;
import org.instancio.settings.Settings;
@@ -55,6 +57,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.test.util.ReflectionTestUtils;
+import uk.gov.hmcts.appregister.applicationentry.audit.AppListEntryAuditOperation;
import uk.gov.hmcts.appregister.applicationentry.exception.AppListEntryError;
import uk.gov.hmcts.appregister.applicationentry.mapper.ApplicationListEntryEntityMapper;
import uk.gov.hmcts.appregister.applicationentry.mapper.ApplicationListEntryEntityMapperImpl;
@@ -455,8 +458,7 @@ void testSearchForGetSummary() {
when(mockPage.getPageNumber()).thenReturn(1);
Page page =
- new PageImpl(
- List.of(applicationListEntryGetSummaryProjection), mockPage, 1);
+ new PageImpl<>(List.of(applicationListEntryGetSummaryProjection), mockPage, 1L);
when(applicationListEntryMapStructMapper.toStatus(entryGetFilterDto.getStatus()))
.thenReturn(Status.OPEN);
@@ -1627,25 +1629,32 @@ void move_resequencesEntries_whenValidRequest() {
when(appListEntrySequenceMappingRepository.findByAlIdForUpdate(targetList.getId()))
.thenReturn(Optional.of(mapping));
- when(applicationListEntryRepository.save(any(ApplicationListEntry.class)))
- .thenAnswer(invocation -> invocation.getArgument(0, ApplicationListEntry.class));
-
- ArgumentCaptor savedEntryCaptor =
- ArgumentCaptor.forClass(ApplicationListEntry.class);
+ when(applicationListEntryRepository.saveAll(anyList()))
+ .thenAnswer(invocation -> invocation.getArgument(0, List.class));
service.move(sourceListId, dto);
verify(applicationListEntryRepository).findByUuidsInSourceList(eq(sourceListId), anySet());
- verify(applicationListEntryRepository, times(2)).save(savedEntryCaptor.capture());
-
- List savedEntries = savedEntryCaptor.getAllValues();
- Assertions.assertEquals(2, savedEntries.size());
- Assertions.assertSame(entry2, savedEntries.get(0));
- Assertions.assertSame(entry1, savedEntries.get(1));
- Assertions.assertSame(targetList, savedEntries.get(0).getApplicationList());
- Assertions.assertSame(targetList, savedEntries.get(1).getApplicationList());
- Assertions.assertEquals((short) 3, savedEntries.get(0).getSequenceNumber());
- Assertions.assertEquals((short) 4, savedEntries.get(1).getSequenceNumber());
+ verify(applicationListEntryRepository)
+ .saveAll(
+ argThat(
+ savedEntries -> {
+ var savedEntriesList = Lists.newArrayList(savedEntries);
+ Assertions.assertEquals(2, savedEntriesList.size());
+ Assertions.assertSame(entry2, savedEntriesList.get(0));
+ Assertions.assertSame(entry1, savedEntriesList.get(1));
+ Assertions.assertSame(
+ targetList,
+ savedEntriesList.get(0).getApplicationList());
+ Assertions.assertSame(
+ targetList,
+ savedEntriesList.get(1).getApplicationList());
+ Assertions.assertEquals(
+ (short) 3, savedEntriesList.get(0).getSequenceNumber());
+ Assertions.assertEquals(
+ (short) 4, savedEntriesList.get(1).getSequenceNumber());
+ return true;
+ }));
Assertions.assertEquals(4, mapping.getAleLastSequence());
}
@@ -1690,7 +1699,7 @@ void move_throws_whenSomeRequestedEntriesAreMissingFromSourceList() {
});
verify(applicationListEntryRepository).findByUuidsInSourceList(eq(sourceListId), anySet());
- verify(applicationListEntryRepository, never()).save(any(ApplicationListEntry.class));
+ verify(applicationListEntryRepository, never()).saveAll(anyList());
}
@Test
@@ -1702,11 +1711,13 @@ void bulkUpdateFees_appendsStatusesForValidatedEntries() {
final var entry1 = applicationListEntry(applicationList, entryId1, 101L, (short) 2);
val existingStatus1 = new AppListEntryFeeStatus();
existingStatus1.setId(201L);
+ existingStatus1.setAppListEntry(entry1);
val entryId2 = UUID.randomUUID();
val entry2 = applicationListEntry(applicationList, entryId2, 102L, (short) 1);
val existingStatus2 = new AppListEntryFeeStatus();
existingStatus2.setId(202L);
+ existingStatus2.setAppListEntry(entry2);
final var dto =
bulkFeesUpdateDto(
@@ -1716,21 +1727,25 @@ void bulkUpdateFees_appendsStatusesForValidatedEntries() {
.thenReturn(Optional.of(applicationList));
when(applicationListEntryRepository.findByUuidsInSourceList(eq(listId), anySet()))
.thenReturn(List.of(entry1, entry2));
- when(appListEntryFeeStatusRepository.getFeeStatusByEntryUuid(entryId1))
- .thenReturn(List.of(existingStatus1));
- when(appListEntryFeeStatusRepository.getFeeStatusByEntryUuid(entryId2))
- .thenReturn(List.of(existingStatus2));
- stubFeeStatusSave();
+ when(appListEntryFeeStatusRepository.findByAppListEntry_UuidIn(List.of(entryId2, entryId1)))
+ .thenReturn(List.of(existingStatus1, existingStatus2));
final BulkUpdateResponseDto response = service.bulkUpdateFees(listId, dto);
- ArgumentCaptor statusCaptor =
- ArgumentCaptor.forClass(AppListEntryFeeStatus.class);
verify(appListEntryFeeStatusRepository, never()).delete(existingStatus1);
verify(appListEntryFeeStatusRepository, never()).delete(existingStatus2);
- verify(appListEntryFeeStatusRepository, times(2)).save(statusCaptor.capture());
-
- List savedStatuses = statusCaptor.getAllValues();
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> statusCaptor =
+ ArgumentCaptor.forClass(List.class);
+ verify(auditOperationService)
+ .processAudit(any(), eq(AppListEntryAuditOperation.BULK_UPDATE_FEES), any());
+ verify(auditOperationService, never())
+ .processAudit(eq(AppListEntryAuditOperation.CREATE_FEE_STATUS_ENTRY), any());
+ verify(auditOperationService, never())
+ .processAudit(eq(AppListEntryAuditOperation.CREATE_FEE_ENTRY), any());
+ verify(appListEntryFeeStatusRepository).saveAll(statusCaptor.capture());
+
+ List savedStatuses = statusCaptor.getValue();
Assertions.assertEquals(
List.of(102L, 101L),
savedStatuses.stream().map(status -> status.getAppListEntry().getId()).toList());
@@ -1812,14 +1827,14 @@ void bulkUpdateFees_setOffsiteToAllFees() {
when(appListEntryFeeRepository.getOffsiteEntryFeesForEntry(entry2.getId()))
.thenReturn(List.of());
when(feeRepository.findOffsite(any())).thenReturn(List.of(new Fee()));
- when(appListEntryFeeRepository.save(any()))
- .thenReturn(new AppListEntryFeeId(1L, 1L, 1L, "test"));
+ when(appListEntryFeeRepository.saveAll(anyList()))
+ .thenAnswer(invocation -> invocation.getArgument(0));
service.bulkUpdateFees(listId, dto);
verify(appListEntryFeeStatusRepository, never()).delete(existingStatus1);
verify(appListEntryFeeStatusRepository, never()).delete(existingStatus2);
- verify(appListEntryFeeRepository, times(2)).save(any());
+ verify(appListEntryFeeRepository).saveAll(anyList());
verify(appListEntryFeeStatusRepository, times(0)).save(any());
}
@@ -1881,20 +1896,21 @@ void bulkUpdateFees_appendsAllProvidedFeeDetails() {
when(applicationListRepository.findByUuidIncludingDelete(listId))
.thenReturn(Optional.of(applicationList));
val entry = applicationListEntry(applicationList, entryId, 101L, (short) 1);
+ existingStatus.setAppListEntry(entry);
when(applicationListEntryRepository.findByUuidsInSourceList(eq(listId), anySet()))
.thenReturn(List.of(entry));
- when(appListEntryFeeStatusRepository.getFeeStatusByEntryUuid(entryId))
+ when(appListEntryFeeStatusRepository.findByAppListEntry_UuidIn(List.of(entryId)))
.thenReturn(List.of(existingStatus));
- stubFeeStatusSave();
service.bulkUpdateFees(listId, dto);
- ArgumentCaptor statusCaptor =
- ArgumentCaptor.forClass(AppListEntryFeeStatus.class);
verify(appListEntryFeeStatusRepository, never()).delete(existingStatus);
- verify(appListEntryFeeStatusRepository, times(2)).save(statusCaptor.capture());
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> statusCaptor =
+ ArgumentCaptor.forClass(List.class);
+ verify(appListEntryFeeStatusRepository).saveAll(statusCaptor.capture());
- List savedStatuses = statusCaptor.getAllValues();
+ List savedStatuses = statusCaptor.getValue();
Assertions.assertEquals(
List.of(FeeStatusType.PAID, FeeStatusType.REMITTED),
savedStatuses.stream().map(AppListEntryFeeStatus::getAlefsFeeStatus).toList());
@@ -1939,7 +1955,7 @@ void bulkUpdateFees_recordsFailedMetricWhenValidationFails() {
.find(BULK_FEE_UPDATE_ENTRIES_METRIC)
.tag(METRIC_STATUS_TAG, "failed")
.summary());
- verify(appListEntryFeeStatusRepository, never()).save(any(AppListEntryFeeStatus.class));
+ verify(appListEntryFeeStatusRepository, never()).saveAll(anyList());
}
@Test
@@ -1964,9 +1980,8 @@ void bulkUpdateFees_updatesEntriesAtOperationalLimit() {
.thenReturn(Optional.of(applicationList));
when(applicationListEntryRepository.findByUuidsInSourceList(eq(listId), anySet()))
.thenReturn(entries);
- when(appListEntryFeeStatusRepository.getFeeStatusByEntryUuid(any(UUID.class)))
+ when(appListEntryFeeStatusRepository.findByAppListEntry_UuidIn(anyList()))
.thenReturn(List.of());
- stubFeeStatusSave();
BulkUpdateResponseDto response = service.bulkUpdateFees(listId, dto);
@@ -1974,7 +1989,7 @@ void bulkUpdateFees_updatesEntriesAtOperationalLimit() {
Assertions.assertEquals(1050, response.getUpdatedCount());
Assertions.assertEquals(BulkUpdateResponseDto.StatusEnum.SUCCEEDED, response.getStatus());
verify(applicationListEntryRepository).findByUuidsInSourceList(eq(listId), anySet());
- verify(appListEntryFeeStatusRepository, times(1050)).save(any(AppListEntryFeeStatus.class));
+ verify(appListEntryFeeStatusRepository).saveAll(anyList());
verify(appListEntryFeeRepository, never()).getOffsiteEntryFeesForEntries(anyList());
verify(feeRepository, never()).findOffsite(any(LocalDate.class));
}
@@ -1995,23 +2010,25 @@ void bulkUpdateFees_createsOffsiteFeeMappingWhenRequestedAndMissing() {
.thenReturn(Optional.of(applicationList));
when(applicationListEntryRepository.findByUuidsInSourceList(eq(listId), anySet()))
.thenReturn(List.of(entry));
- when(appListEntryFeeStatusRepository.getFeeStatusByEntryUuid(entryId))
+ when(appListEntryFeeStatusRepository.findByAppListEntry_UuidIn(List.of(entryId)))
.thenReturn(List.of());
when(appListEntryFeeRepository.getOffsiteEntryFeesForEntries(anyList()))
.thenReturn(List.of());
when(feeRepository.findOffsite(LocalDate.of(2025, Month.OCTOBER, 7)))
.thenReturn(List.of(offsiteFee));
- when(appListEntryFeeRepository.save(any(AppListEntryFeeId.class)))
+ when(appListEntryFeeRepository.saveAll(anyList()))
.thenAnswer(invocation -> invocation.getArgument(0));
- stubFeeStatusSave();
service.bulkUpdateFees(listId, dto);
- ArgumentCaptor feeMappingCaptor =
- ArgumentCaptor.forClass(AppListEntryFeeId.class);
- verify(appListEntryFeeRepository).save(feeMappingCaptor.capture());
- Assertions.assertEquals(entry.getId(), feeMappingCaptor.getValue().getAppListEntryId());
- Assertions.assertEquals(offsiteFee.getId(), feeMappingCaptor.getValue().getFeeId());
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> feeMappingCaptor =
+ ArgumentCaptor.forClass(List.class);
+ verify(appListEntryFeeRepository).saveAll(feeMappingCaptor.capture());
+ Assertions.assertEquals(
+ entry.getId(), feeMappingCaptor.getValue().getFirst().getAppListEntryId());
+ Assertions.assertEquals(
+ offsiteFee.getId(), feeMappingCaptor.getValue().getFirst().getFeeId());
}
@Test
@@ -2033,29 +2050,29 @@ void bulkUpdateFees_reusesActiveOffsiteFeeWhenMultipleMappingsAreMissing() {
.thenReturn(Optional.of(applicationList));
when(applicationListEntryRepository.findByUuidsInSourceList(eq(listId), anySet()))
.thenReturn(List.of(entry1, entry2));
- when(appListEntryFeeStatusRepository.getFeeStatusByEntryUuid(any(UUID.class)))
+ when(appListEntryFeeStatusRepository.findByAppListEntry_UuidIn(anyList()))
.thenReturn(List.of());
when(appListEntryFeeRepository.getOffsiteEntryFeesForEntries(anyList()))
.thenReturn(List.of());
when(feeRepository.findOffsite(LocalDate.of(2025, Month.OCTOBER, 7)))
.thenReturn(List.of(offsiteFee));
- when(appListEntryFeeRepository.save(any(AppListEntryFeeId.class)))
+ when(appListEntryFeeRepository.saveAll(anyList()))
.thenAnswer(invocation -> invocation.getArgument(0));
- stubFeeStatusSave();
service.bulkUpdateFees(listId, dto);
- ArgumentCaptor feeMappingCaptor =
- ArgumentCaptor.forClass(AppListEntryFeeId.class);
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> feeMappingCaptor =
+ ArgumentCaptor.forClass(List.class);
verify(feeRepository).findOffsite(LocalDate.of(2025, Month.OCTOBER, 7));
- verify(appListEntryFeeRepository, times(2)).save(feeMappingCaptor.capture());
+ verify(appListEntryFeeRepository).saveAll(feeMappingCaptor.capture());
Assertions.assertEquals(
Set.of(entry1.getId(), entry2.getId()),
- feeMappingCaptor.getAllValues().stream()
+ feeMappingCaptor.getValue().stream()
.map(AppListEntryFeeId::getAppListEntryId)
.collect(Collectors.toSet()));
Assertions.assertTrue(
- feeMappingCaptor.getAllValues().stream()
+ feeMappingCaptor.getValue().stream()
.allMatch(mapping -> offsiteFee.getId().equals(mapping.getFeeId())));
}
@@ -2075,16 +2092,15 @@ void bulkUpdateFees_preservesExistingOffsiteFeeMappingWhenNotRequested() {
.thenReturn(Optional.of(applicationList));
when(applicationListEntryRepository.findByUuidsInSourceList(eq(listId), anySet()))
.thenReturn(List.of(entry));
- when(appListEntryFeeStatusRepository.getFeeStatusByEntryUuid(entryId))
+ when(appListEntryFeeStatusRepository.findByAppListEntry_UuidIn(List.of(entryId)))
.thenReturn(List.of());
- stubFeeStatusSave();
service.bulkUpdateFees(listId, dto);
verify(appListEntryFeeRepository, never()).delete(existingOffsiteMapping);
verify(appListEntryFeeRepository, never()).flush();
verify(appListEntryFeeRepository, never()).getOffsiteEntryFeesForEntries(anyList());
- verify(appListEntryFeeRepository, never()).save(any(AppListEntryFeeId.class));
+ verify(appListEntryFeeRepository, never()).saveAll(anyList());
}
private ApplicationList openApplicationList(UUID listId) {
@@ -2179,10 +2195,8 @@ void replaceOfficials_replacesOfficialsForAllEntries() {
.thenReturn(Optional.of(applicationList));
when(applicationListEntryRepository.findByUuidsInSourceList(eq(listId), anySet()))
.thenReturn(List.of(entry1, entry2));
- when(appListEntryOfficialRepository.getOfficialByEntryUuid(entryId1))
- .thenReturn(List.of(existingOfficial1));
- when(appListEntryOfficialRepository.getOfficialByEntryUuid(entryId2))
- .thenReturn(List.of(existingOfficial2));
+ when(appListEntryOfficialRepository.findByAppListEntry_UuidIn(List.of(entryId2, entryId1)))
+ .thenReturn(List.of(existingOfficial1, existingOfficial2));
when(applicationListEntryEntityMapper.toOfficial(any(Official.class), any()))
.thenAnswer(
invocation -> {
@@ -2190,12 +2204,22 @@ void replaceOfficials_replacesOfficialsForAllEntries() {
entity.setAppListEntry(invocation.getArgument(1));
return entity;
});
+ when(appListEntryOfficialRepository.saveAll(anyList()))
+ .thenAnswer(invocation -> invocation.getArgument(0));
service.replaceOfficials(listId, dto);
- verify(appListEntryOfficialRepository).delete(existingOfficial1);
- verify(appListEntryOfficialRepository).delete(existingOfficial2);
- verify(appListEntryOfficialRepository, times(2)).save(any(AppListEntryOfficial.class));
+ verify(auditOperationService)
+ .processAudit(any(), eq(AppListEntryAuditOperation.BULK_UPDATE_OFFICIALS), any());
+ verify(auditOperationService, never())
+ .processAudit(eq(AppListEntryAuditOperation.CREATE_OFFICIAL_ENTRY), any());
+ verify(auditOperationService, never())
+ .processAudit(
+ any(AppListEntryOfficial.class),
+ eq(AppListEntryAuditOperation.DELETE_OFFICIAL_ENTRY),
+ any());
+ verify(appListEntryOfficialRepository).deleteAllForEntryIds(List.of(102L, 101L));
+ verify(appListEntryOfficialRepository).saveAll(anyList());
}
@Test
@@ -2239,8 +2263,8 @@ void replaceOfficials_throwsBeforeWriting_whenSomeRequestedEntriesAreMissingFrom
ApplicationListError.ENTRY_NOT_IN_SOURCE_LIST, appEx.getCode());
});
- verify(appListEntryOfficialRepository, never()).delete(any(AppListEntryOfficial.class));
- verify(appListEntryOfficialRepository, never()).save(any(AppListEntryOfficial.class));
+ verify(appListEntryOfficialRepository, never()).deleteAllForEntryIds(anyList());
+ verify(appListEntryOfficialRepository, never()).saveAll(anyList());
}
@Test
diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/audit/BulkCreateApplicationEntryResultAuditTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/audit/BulkCreateApplicationEntryResultAuditTest.java
new file mode 100644
index 000000000..7c2862eee
--- /dev/null
+++ b/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/audit/BulkCreateApplicationEntryResultAuditTest.java
@@ -0,0 +1,46 @@
+package uk.gov.hmcts.appregister.applicationentryresult.audit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.UUID;
+import org.junit.jupiter.api.Test;
+import uk.gov.hmcts.appregister.common.entity.AppListEntryResolution;
+import uk.gov.hmcts.appregister.common.entity.ApplicationListEntry;
+import uk.gov.hmcts.appregister.common.entity.ResolutionCode;
+import uk.gov.hmcts.appregister.generated.model.TemplateSubstitution;
+
+class BulkCreateApplicationEntryResultAuditTest {
+
+ @Test
+ void formatHelpers_renderExpectedEscapedValues() {
+ var entry = new ApplicationListEntry();
+ entry.setId(22L);
+ entry.setUuid(UUID.fromString("22222222-2222-2222-2222-222222222222"));
+ entry.setSequenceNumber((short) 7);
+
+ var resolutionCode = new ResolutionCode();
+ resolutionCode.setResultCode("CODE\\\"1");
+
+ var resolution = new AppListEntryResolution();
+ resolution.setId(11L);
+ resolution.setUuid(UUID.fromString("11111111-1111-1111-1111-111111111111"));
+ resolution.setApplicationList(entry);
+ resolution.setResolutionCode(resolutionCode);
+ resolution.setResolutionWording("Wording\\\"1");
+ resolution.setResolutionOfficer("Officer\\\"1");
+
+ assertThat(
+ BulkCreateApplicationEntryResultAudit.formatWordingFields(
+ List.of(new TemplateSubstitution().key("k\\\"1").value("v\\\"1"))))
+ .isEqualTo("[{\"key\":\"k\\\\\\\"1\",\"value\":\"v\\\\\\\"1\"}]");
+ assertThat(BulkCreateApplicationEntryResultAudit.formatCreatedResults(List.of(resolution)))
+ .isEqualTo(
+ "[{\"resultId\":\"11111111-1111-1111-1111-111111111111\","
+ + "\"entryId\":\"22222222-2222-2222-2222-222222222222\","
+ + "\"sequenceNumber\":7,\n"
+ + "\"resultCode\":\"CODE\\\\\\\"1\","
+ + "\"wording\":\"Wording\\\\\\\"1\","
+ + "\"officer\":\"Officer\\\\\\\"1\"}]");
+ }
+}
diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/audit/BulkDeleteApplicationEntryResultAuditTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/audit/BulkDeleteApplicationEntryResultAuditTest.java
new file mode 100644
index 000000000..b6ed42233
--- /dev/null
+++ b/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/audit/BulkDeleteApplicationEntryResultAuditTest.java
@@ -0,0 +1,77 @@
+package uk.gov.hmcts.appregister.applicationentryresult.audit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.UUID;
+import org.junit.jupiter.api.Test;
+import uk.gov.hmcts.appregister.common.entity.AppListEntryResolution;
+import uk.gov.hmcts.appregister.common.entity.ApplicationList;
+import uk.gov.hmcts.appregister.common.entity.ApplicationListEntry;
+import uk.gov.hmcts.appregister.common.entity.ResolutionCode;
+import uk.gov.hmcts.appregister.common.entity.TableNames;
+import uk.gov.hmcts.appregister.common.enumeration.CrudEnum;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultItemDto;
+
+class BulkDeleteApplicationEntryResultAuditTest {
+
+ @Test
+ void extractAuditData_includesSummaryFields() {
+ var listId = UUID.randomUUID();
+ var entryId = UUID.randomUUID();
+ var audit =
+ new BulkDeleteApplicationEntryResultAudit(
+ 10L, List.of(listId), List.of(entryId), 1, "[{\"resultId\":\"x\"}]");
+
+ var auditData = audit.extractAuditData(CrudEnum.DELETE);
+
+ assertThat(auditData)
+ .anyMatch(
+ row ->
+ TableNames.APPLICATION_LIST_ENTRY_RESOLUTIONS.equals(
+ row.getTableName())
+ && "bulk_delete_results_count".equals(row.getFieldName())
+ && "1".equals(row.getValue()));
+ }
+
+ @Test
+ void formatHelpers_renderExpectedValues() {
+ var requestedItem =
+ new BulkDeleteResultItemDto()
+ .listId(UUID.fromString("11111111-1111-1111-1111-111111111111"))
+ .entryId(UUID.fromString("22222222-2222-2222-2222-222222222222"))
+ .resultId(UUID.fromString("33333333-3333-3333-3333-333333333333"));
+
+ assertThat(
+ BulkDeleteApplicationEntryResultAudit.formatRequestedResults(
+ List.of(requestedItem)))
+ .contains(
+ requestedItem.getListId().toString(),
+ requestedItem.getResultId().toString());
+
+ var list = new ApplicationList();
+ list.setId(1L);
+ list.setUuid(requestedItem.getListId());
+ var entry = new ApplicationListEntry();
+ entry.setId(2L);
+ entry.setUuid(requestedItem.getEntryId());
+ entry.setSequenceNumber((short) 7);
+ entry.setApplicationList(list);
+ var code = new ResolutionCode();
+ code.setResultCode("CODE\"1");
+ var resolution = new AppListEntryResolution();
+ resolution.setId(3L);
+ resolution.setUuid(requestedItem.getResultId());
+ resolution.setApplicationList(entry);
+ resolution.setResolutionCode(code);
+ resolution.setResolutionWording("Wording\\\"1");
+ resolution.setResolutionOfficer("Officer\\\"1");
+
+ assertThat(BulkDeleteApplicationEntryResultAudit.formatDeletedResults(List.of(resolution)))
+ .contains(
+ "\"sequenceNumber\":7",
+ "CODE\\\"1",
+ "Wording\\\\\\\"1",
+ "Officer\\\\\\\"1");
+ }
+}
diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/controller/ApplicationEntryResultControllerTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/controller/ApplicationEntryResultControllerTest.java
index a3aa82828..35153c97e 100644
--- a/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/controller/ApplicationEntryResultControllerTest.java
+++ b/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/controller/ApplicationEntryResultControllerTest.java
@@ -23,6 +23,7 @@
import uk.gov.hmcts.appregister.common.concurrency.MatchResponse;
import uk.gov.hmcts.appregister.common.mapper.PageableMapper;
import uk.gov.hmcts.appregister.common.util.PagingWrapper;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultsDto;
import uk.gov.hmcts.appregister.generated.model.BulkResultDto;
import uk.gov.hmcts.appregister.generated.model.ResultCreateDto;
import uk.gov.hmcts.appregister.generated.model.ResultGetDto;
@@ -74,6 +75,17 @@ void bulkResultEntries_delegatesAndReturnsOk() {
assertThat(actual.getBody()).isSameAs(body);
}
+ @Test
+ void bulkDeleteResultEntries_delegatesAndReturnsNoContent() {
+ var request = new BulkDeleteResultsDto();
+ doNothing().when(service).bulkDelete(any());
+
+ ResponseEntity actual = controller.bulkDeleteResultEntries(request);
+
+ verify(service).bulkDelete(any());
+ assertThat(actual.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);
+ }
+
@Test
void deleteApplicationListEntryResult_delegatesAndReturnsNoContent() {
UUID listId = UUID.randomUUID();
diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/service/ApplicationEntryResultServiceImplTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/service/ApplicationEntryResultServiceImplTest.java
index 51304f949..007ba54f4 100644
--- a/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/service/ApplicationEntryResultServiceImplTest.java
+++ b/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/service/ApplicationEntryResultServiceImplTest.java
@@ -1,9 +1,12 @@
package uk.gov.hmcts.appregister.applicationentryresult.service;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static uk.gov.hmcts.appregister.common.template.wording.WordingTemplateSentence.with;
@@ -29,6 +32,7 @@
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
+import uk.gov.hmcts.appregister.applicationentryresult.audit.AppListEntryResultAuditOperation;
import uk.gov.hmcts.appregister.applicationentryresult.mapper.ApplicationListEntryResultEntityMapper;
import uk.gov.hmcts.appregister.applicationentryresult.mapper.ApplicationListEntryResultMapper;
import uk.gov.hmcts.appregister.applicationentryresult.model.ListEntryResultDeleteArgs;
@@ -42,6 +46,10 @@
import uk.gov.hmcts.appregister.applicationentryresult.validator.ApplicationEntryResultUpdateValidator;
import uk.gov.hmcts.appregister.applicationentryresult.validator.BulkApplicationEntryResultCreationSuccess;
import uk.gov.hmcts.appregister.applicationentryresult.validator.BulkApplicationEntryResultCreationValidator;
+import uk.gov.hmcts.appregister.applicationentryresult.validator.BulkApplicationEntryResultDeletionSuccess;
+import uk.gov.hmcts.appregister.applicationentryresult.validator.BulkApplicationEntryResultDeletionValidatedItem;
+import uk.gov.hmcts.appregister.applicationentryresult.validator.BulkApplicationEntryResultDeletionValidator;
+import uk.gov.hmcts.appregister.applicationentryresult.validator.BulkApplicationEntryResultValidatedItem;
import uk.gov.hmcts.appregister.applicationentryresult.validator.ListEntryResultCreateValidationSuccess;
import uk.gov.hmcts.appregister.applicationentryresult.validator.ListEntryResultDeleteValidationSuccess;
import uk.gov.hmcts.appregister.applicationentryresult.validator.ListEntryResultGetValidationSuccess;
@@ -72,6 +80,8 @@
import uk.gov.hmcts.appregister.common.service.BusinessDateProvider;
import uk.gov.hmcts.appregister.common.template.wording.WordingTemplateSentence;
import uk.gov.hmcts.appregister.common.util.PagingWrapper;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultItemDto;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultsDto;
import uk.gov.hmcts.appregister.generated.model.BulkResultDto;
import uk.gov.hmcts.appregister.generated.model.ResultCreateDto;
import uk.gov.hmcts.appregister.generated.model.ResultGetDto;
@@ -128,10 +138,15 @@ class ApplicationEntryResultServiceImplTest {
resolutionCodeRepository,
businessDateProvider);
+ @Mock private BulkApplicationEntryResultDeletionValidator bulkDeleteResultEntry;
+
@Spy
private DummyBulkApplicationEntryResultGetValidator bulkResultEntry =
new DummyBulkApplicationEntryResultGetValidator(
- applicationListRepository, applicationListEntryRepository);
+ applicationListRepository,
+ applicationListEntryRepository,
+ resolutionCodeRepository,
+ businessDateProvider);
@Spy
private final AuditOperationService auditOperationService = new DummyAuditOperationService();
@@ -152,6 +167,7 @@ void setUp() {
creationValidator,
updateValidator,
getValidator,
+ bulkDeleteResultEntry,
bulkResultEntry,
matchService,
auditOperationService,
@@ -200,6 +216,11 @@ void createAnEntryResult() {
AppListEntryResolution entryToSave = new AppListEntryResolution();
entryToSave.setId(23232L);
entryToSave.setVersion(2L);
+ entryToSave.setUuid(UUID.randomUUID());
+ entryToSave.setApplicationList(applicationListEntry);
+ entryToSave.setResolutionCode(resolutionCode);
+ entryToSave.setResolutionWording("This is a template {My Substituted Value}");
+ entryToSave.setResolutionOfficer("myemail@domain.com");
when(applicationListEntryResultEntityMapper.toApplicationListEntryResult(
resultCreateDto,
@@ -213,7 +234,8 @@ void createAnEntryResult() {
new PayloadForCreateEntryResult<>(
UUID.randomUUID(), UUID.randomUUID(), resultCreateDto);
- when(appListEntryResolutionRepository.save(entryToSave)).thenReturn(entryToSave);
+ when(appListEntryResolutionRepository.save(any(AppListEntryResolution.class)))
+ .thenReturn(entryToSave);
// setup the response of the call
ResultGetDto resultGetDto =
@@ -260,6 +282,93 @@ void delete_validArgs_deletesEntryResult() {
verify(appListEntryResolutionRepository).delete(any(AppListEntryResolution.class));
}
+ @Test
+ void bulkDelete_validArgs_deletesEntryResultsInBatch() {
+ var firstResult = createResolution(11L, 101L, 1001L, "CODE1", "wording1", "officer1");
+ var secondResult = createResolution(12L, 102L, 1002L, "CODE2", "wording2", "officer2");
+
+ var bulkDeleteSuccess = new BulkApplicationEntryResultDeletionSuccess();
+ bulkDeleteSuccess
+ .getResults()
+ .add(
+ new BulkApplicationEntryResultDeletionValidatedItem(
+ new ListEntryResultDeleteArgs(
+ firstResult
+ .getApplicationList()
+ .getApplicationList()
+ .getUuid(),
+ firstResult.getApplicationList().getUuid(),
+ firstResult.getUuid()),
+ new ListEntryResultDeleteValidationSuccess(
+ with(""),
+ firstResult.getResolutionCode(),
+ firstResult.getApplicationList().getApplicationList(),
+ firstResult.getApplicationList(),
+ firstResult)));
+ bulkDeleteSuccess
+ .getResults()
+ .add(
+ new BulkApplicationEntryResultDeletionValidatedItem(
+ new ListEntryResultDeleteArgs(
+ secondResult
+ .getApplicationList()
+ .getApplicationList()
+ .getUuid(),
+ secondResult.getApplicationList().getUuid(),
+ secondResult.getUuid()),
+ new ListEntryResultDeleteValidationSuccess(
+ with(""),
+ secondResult.getResolutionCode(),
+ secondResult.getApplicationList().getApplicationList(),
+ secondResult.getApplicationList(),
+ secondResult)));
+ doAnswer(
+ invocation -> {
+ BiFunction<
+ BulkDeleteResultsDto,
+ BulkApplicationEntryResultDeletionSuccess,
+ Object>
+ validateSuccess = invocation.getArgument(1);
+ return validateSuccess.apply(
+ invocation.getArgument(0), bulkDeleteSuccess);
+ })
+ .when(bulkDeleteResultEntry)
+ .validate(any(BulkDeleteResultsDto.class), any());
+
+ var request =
+ new BulkDeleteResultsDto()
+ .results(
+ List.of(
+ new BulkDeleteResultItemDto()
+ .listId(
+ firstResult
+ .getApplicationList()
+ .getApplicationList()
+ .getUuid())
+ .entryId(firstResult.getApplicationList().getUuid())
+ .resultId(firstResult.getUuid()),
+ new BulkDeleteResultItemDto()
+ .listId(
+ secondResult
+ .getApplicationList()
+ .getApplicationList()
+ .getUuid())
+ .entryId(
+ secondResult.getApplicationList().getUuid())
+ .resultId(secondResult.getUuid())));
+
+ service.bulkDelete(request);
+
+ verify(appListEntryResolutionRepository)
+ .deleteAllInBatch(List.of(firstResult, secondResult));
+ verify(entityManager).flush();
+ verify(auditOperationService)
+ .processAudit(
+ any(),
+ eq(AppListEntryResultAuditOperation.BULK_DELETE_APP_LIST_ENTRY_RESULT),
+ any());
+ }
+
@Test
void search_validArgs_returnEntryResult() {
PayloadGetEntryResultInList payloadGetEntryResultInList =
@@ -382,6 +491,11 @@ void bulkProcessEntries() {
AppListEntryResolution entryToSave = new AppListEntryResolution();
entryToSave.setId(23232L);
entryToSave.setVersion(2L);
+ entryToSave.setUuid(UUID.randomUUID());
+ entryToSave.setApplicationList(applicationListEntry);
+ entryToSave.setResolutionCode(resolutionCode);
+ entryToSave.setResolutionWording("This is a template {My Substituted Value}");
+ entryToSave.setResolutionOfficer("myemail@domain.com");
when(applicationListEntryResultEntityMapper.toApplicationListEntryResult(
resultCreateDto,
@@ -395,7 +509,8 @@ void bulkProcessEntries() {
new PayloadForCreateEntryResult<>(
UUID.randomUUID(), UUID.randomUUID(), resultCreateDto);
- when(appListEntryResolutionRepository.save(entryToSave)).thenReturn(entryToSave);
+ when(appListEntryResolutionRepository.saveAll(List.of(entryToSave)))
+ .thenReturn(List.of(entryToSave));
// setup the response of the call
ResultGetDto resultGetDto =
@@ -404,15 +519,28 @@ void bulkProcessEntries() {
BulkApplicationEntryResultCreationSuccess bulkApplicationEntryResultCreationSuccess =
new BulkApplicationEntryResultCreationSuccess();
- bulkApplicationEntryResultCreationSuccess.getResults().add(payload);
+ bulkApplicationEntryResultCreationSuccess
+ .getResults()
+ .add(new BulkApplicationEntryResultValidatedItem(payload, success));
bulkResultEntry.setSuccess(bulkApplicationEntryResultCreationSuccess);
// make the call
List createdResults =
- service.bulkCreate(PayloadForCreateResults.builder().build());
+ service.bulkCreate(
+ PayloadForCreateResults.builder()
+ .payload(new BulkResultDto().result(resultCreateDto))
+ .build());
Assertions.assertEquals(List.of(resultGetDto), createdResults);
+ verify(appListEntryResolutionRepository).saveAll(List.of(entryToSave));
+ verify(appListEntryResolutionRepository, never()).findAllById(any());
+ verify(entityManager).flush();
+ verify(auditOperationService)
+ .processAudit(
+ eq(AppListEntryResultAuditOperation.BULK_CREATE_APP_LIST_ENTRY_RESULT),
+ any());
+ verify(appListEntryResolutionRepository, never()).save(any(AppListEntryResolution.class));
}
@Setter
@@ -562,9 +690,15 @@ static class DummyBulkApplicationEntryResultGetValidator
public DummyBulkApplicationEntryResultGetValidator(
ApplicationListRepository applicationListRepository,
- ApplicationListEntryRepository applicationListEntryRepository) {
+ ApplicationListEntryRepository applicationListEntryRepository,
+ ResolutionCodeRepository resolutionCodeRepository,
+ BusinessDateProvider businessDateProvider) {
- super(applicationListRepository, applicationListEntryRepository);
+ super(
+ applicationListRepository,
+ applicationListEntryRepository,
+ resolutionCodeRepository,
+ businessDateProvider);
}
@Override
@@ -639,4 +773,35 @@ void update_validArgs_returnsUpdatedEntryResult() {
verify(entityManager).flush();
verify(entityManager).refresh(existing);
}
+
+ private AppListEntryResolution createResolution(
+ Long resultId,
+ Long entryId,
+ Long listId,
+ String resultCode,
+ String wording,
+ String officer) {
+ var list = new ApplicationList();
+ list.setId(listId);
+ list.setUuid(UUID.randomUUID());
+
+ var entry = new ApplicationListEntry();
+ entry.setId(entryId);
+ entry.setUuid(UUID.randomUUID());
+ entry.setSequenceNumber(entryId.shortValue());
+ entry.setApplicationList(list);
+
+ var code = new ResolutionCode();
+ code.setResultCode(resultCode);
+
+ var resolution = new AppListEntryResolution();
+ resolution.setId(resultId);
+ resolution.setVersion(1L);
+ resolution.setUuid(UUID.randomUUID());
+ resolution.setApplicationList(entry);
+ resolution.setResolutionCode(code);
+ resolution.setResolutionWording(wording);
+ resolution.setResolutionOfficer(officer);
+ return resolution;
+ }
}
diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultCreationValidatorTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultCreationValidatorTest.java
index 7754a1c1e..174358395 100644
--- a/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultCreationValidatorTest.java
+++ b/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultCreationValidatorTest.java
@@ -4,7 +4,6 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
-import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -23,10 +22,13 @@
import uk.gov.hmcts.appregister.applicationentryresult.model.PayloadForCreateResults;
import uk.gov.hmcts.appregister.applicationlist.exception.ApplicationListError;
import uk.gov.hmcts.appregister.common.entity.ApplicationList;
-import uk.gov.hmcts.appregister.common.entity.model.EntryToList;
+import uk.gov.hmcts.appregister.common.entity.ApplicationListEntry;
import uk.gov.hmcts.appregister.common.entity.repository.ApplicationListEntryRepository;
import uk.gov.hmcts.appregister.common.entity.repository.ApplicationListRepository;
+import uk.gov.hmcts.appregister.common.entity.repository.ResolutionCodeRepository;
+import uk.gov.hmcts.appregister.common.enumeration.Status;
import uk.gov.hmcts.appregister.common.exception.AppRegistryException;
+import uk.gov.hmcts.appregister.common.service.BusinessDateProvider;
import uk.gov.hmcts.appregister.generated.model.BulkResultDto;
import uk.gov.hmcts.appregister.generated.model.ResultCreateDto;
import uk.gov.hmcts.appregister.generated.model.TemplateSubstitution;
@@ -38,6 +40,10 @@ class BulkApplicationEntryResultCreationValidatorTest {
@Mock private ApplicationListEntryRepository applicationListEntryRepository;
+ @Mock private ResolutionCodeRepository resolutionCodeRepository;
+
+ @Mock private BusinessDateProvider businessDateProvider;
+
@InjectMocks private BulkApplicationEntryResultCreationValidator validator;
@Test
@@ -45,28 +51,30 @@ void testSuccessWithListProvided() {
UUID appList = UUID.randomUUID();
// setup 3 of the entries to belong to the list
- UUID appEntry = UUID.randomUUID();
- UUID appEntry2 = UUID.randomUUID();
- UUID appEntry3 = UUID.randomUUID();
-
- when(applicationListRepository.findByUuid(appList))
- .thenReturn(Optional.of(new ApplicationList()));
+ final UUID appEntry = UUID.randomUUID();
+ final UUID appEntry2 = UUID.randomUUID();
+ final UUID appEntry3 = UUID.randomUUID();
- OrderIndependantUuidMatcher uuidMatcher =
- new OrderIndependantUuidMatcher(List.of(appEntry, appEntry2, appEntry3));
- when(applicationListEntryRepository.doesApplicationEntryBelongToApplicationList(
- argThat(uuidMatcher), eq(appList)))
- .thenReturn(true);
+ var list = new ApplicationList();
+ list.setUuid(appList);
+ list.setStatus(Status.OPEN);
+ when(applicationListRepository.findByUuid(appList)).thenReturn(Optional.of(list));
ResultCreateDto resultCreateDto = new ResultCreateDto();
- resultCreateDto.setResultCode("code");
resultCreateDto.setWordingFields(List.of(new TemplateSubstitution()));
BulkResultDto bulkResultDto = new BulkResultDto();
+ bulkResultDto.setEntryIds(List.of(appEntry, appEntry2, appEntry3));
bulkResultDto.setResult(resultCreateDto);
- bulkResultDto.setEntryIds(List.of(appEntry, appEntry2, appEntry3));
- bulkResultDto.setResult(new ResultCreateDto());
+ when(applicationListEntryRepository.findByUuidsInSourceList(
+ eq(appList),
+ argThat(set -> set.containsAll(List.of(appEntry, appEntry2, appEntry3)))))
+ .thenReturn(
+ List.of(
+ entry(appEntry, list),
+ entry(appEntry2, list),
+ entry(appEntry3, list)));
PayloadForCreateResults payloadForCreateEntryResult =
PayloadForCreateResults.builder()
@@ -80,20 +88,26 @@ void testSuccessWithListProvided() {
payloadForCreateEntryResult,
(v, r) -> {
Assertions.assertEquals(3, r.getResults().size());
- Assertions.assertEquals(appList, r.getResults().get(0).getListId());
- Assertions.assertEquals(appList, r.getResults().get(1).getListId());
- Assertions.assertEquals(appList, r.getResults().get(2).getListId());
+ Assertions.assertEquals(
+ appList, r.getResults().get(0).payload().getListId());
+ Assertions.assertEquals(
+ appList, r.getResults().get(1).payload().getListId());
+ Assertions.assertEquals(
+ appList, r.getResults().get(2).payload().getListId());
// ensure that the entries are one of the ones we expect
Assertions.assertTrue(
Set.of(appEntry, appEntry2, appEntry3)
- .contains(r.getResults().get(0).getEntryId()));
+ .contains(
+ r.getResults().get(0).payload().getEntryId()));
Assertions.assertTrue(
Set.of(appEntry, appEntry2, appEntry3)
- .contains(r.getResults().get(1).getEntryId()));
+ .contains(
+ r.getResults().get(1).payload().getEntryId()));
Assertions.assertTrue(
Set.of(appEntry, appEntry2, appEntry3)
- .contains(r.getResults().get(2).getEntryId()));
+ .contains(
+ r.getResults().get(2).payload().getEntryId()));
return true;
});
@@ -106,36 +120,32 @@ void testSuccessWithNoListProvided() {
UUID appList2 = UUID.randomUUID();
// setup 3 of the entries to belong to the list
- UUID appEntry = UUID.randomUUID();
- UUID appEntry2 = UUID.randomUUID();
- UUID appEntry3 = UUID.randomUUID();
+ final UUID appEntry = UUID.randomUUID();
+ final UUID appEntry2 = UUID.randomUUID();
+ final UUID appEntry3 = UUID.randomUUID();
- List entryToLists = new ArrayList<>();
-
- // these are the entry to lists that we expect to be returned from the db
- EntryToList entryToList = new EntryToList(appEntry, appList);
- EntryToList entryToList2 = new EntryToList(appEntry2, appList2);
- EntryToList entryToList3 = new EntryToList(appEntry3, appList2);
-
- entryToLists.add(entryToList);
- entryToLists.add(entryToList2);
- entryToLists.add(entryToList3);
-
- OrderIndependantUuidMatcher uuidMatcher =
- new OrderIndependantUuidMatcher(List.of(appEntry, appEntry2, appEntry3));
-
- when(applicationListEntryRepository.findApplicationListForAllEntries(argThat(uuidMatcher)))
- .thenReturn(entryToLists);
+ var list = new ApplicationList();
+ list.setUuid(appList);
+ list.setStatus(Status.OPEN);
+ var list2 = new ApplicationList();
+ list2.setUuid(appList2);
+ list2.setStatus(Status.OPEN);
ResultCreateDto resultCreateDto = new ResultCreateDto();
- resultCreateDto.setResultCode("code");
resultCreateDto.setWordingFields(List.of(new TemplateSubstitution()));
BulkResultDto bulkResultDto = new BulkResultDto();
+ bulkResultDto.setEntryIds(List.of(appEntry, appEntry2, appEntry3));
bulkResultDto.setResult(resultCreateDto);
- bulkResultDto.setEntryIds(List.of(appEntry, appEntry2, appEntry3));
- bulkResultDto.setResult(new ResultCreateDto());
+ OrderIndependantUuidMatcher uuidMatcher =
+ new OrderIndependantUuidMatcher(List.of(appEntry, appEntry2, appEntry3));
+ when(applicationListEntryRepository.findActiveByUuids(argThat(uuidMatcher)))
+ .thenReturn(
+ List.of(
+ entry(appEntry, list),
+ entry(appEntry2, list2),
+ entry(appEntry3, list2)));
PayloadForCreateResults payloadForCreateEntryResult =
PayloadForCreateResults.builder().payload(bulkResultDto).build();
@@ -146,12 +156,18 @@ void testSuccessWithNoListProvided() {
payloadForCreateEntryResult,
(v, r) -> {
Assertions.assertEquals(3, r.getResults().size());
- Assertions.assertEquals(appList, r.getResults().get(0).getListId());
- Assertions.assertEquals(appList2, r.getResults().get(1).getListId());
- Assertions.assertEquals(appList2, r.getResults().get(2).getListId());
- Assertions.assertEquals(appEntry, r.getResults().get(0).getEntryId());
- Assertions.assertEquals(appEntry2, r.getResults().get(1).getEntryId());
- Assertions.assertEquals(appEntry3, r.getResults().get(2).getEntryId());
+ Assertions.assertEquals(
+ appList, r.getResults().get(0).payload().getListId());
+ Assertions.assertEquals(
+ appList2, r.getResults().get(1).payload().getListId());
+ Assertions.assertEquals(
+ appList2, r.getResults().get(2).payload().getListId());
+ Assertions.assertEquals(
+ appEntry, r.getResults().get(0).payload().getEntryId());
+ Assertions.assertEquals(
+ appEntry2, r.getResults().get(1).payload().getEntryId());
+ Assertions.assertEquals(
+ appEntry3, r.getResults().get(2).payload().getEntryId());
return true;
});
@@ -196,14 +212,17 @@ void testEntriesNotFoundForList() {
payloadForCreateEntryResult.getPayload().setEntryIds(List.of(appEntry, appEntry2));
- when(applicationListRepository.findByUuid(appList))
- .thenReturn(Optional.of(new ApplicationList()));
-
+ var list = new ApplicationList();
+ list.setUuid(appList);
+ list.setStatus(Status.OPEN);
+ when(applicationListRepository.findByUuid(appList)).thenReturn(Optional.of(list));
+ when(applicationListEntryRepository.findByUuidsInSourceList(
+ eq(appList), argThat(set -> set.containsAll(List.of(appEntry, appEntry2)))))
+ .thenReturn(List.of(entry(appEntry, list)));
OrderIndependantUuidMatcher uuidMatcher =
new OrderIndependantUuidMatcher(List.of(appEntry, appEntry2));
- when(applicationListEntryRepository.doesApplicationEntryBelongToApplicationList(
- argThat(uuidMatcher), eq(appList)))
- .thenReturn(false);
+ when(applicationListEntryRepository.findActiveByUuids(argThat(uuidMatcher)))
+ .thenReturn(List.of(entry(appEntry, list), entry(appEntry2, list)));
// run the validation
AppRegistryException exception =
@@ -223,6 +242,49 @@ void testEntriesNotFoundForList() {
exception.getCode().getCode());
}
+ @Test
+ void testMissingEntryForListReturnsEntryDoesNotExist() {
+ UUID appList = UUID.randomUUID();
+
+ var payloadForCreateEntryResult =
+ PayloadForCreateResults.builder()
+ .payload(new BulkResultDto())
+ .listId(appList)
+ .build();
+
+ UUID appEntry = UUID.randomUUID();
+ UUID missingEntry = UUID.randomUUID();
+
+ payloadForCreateEntryResult.getPayload().setEntryIds(List.of(appEntry, missingEntry));
+
+ var list = new ApplicationList();
+ list.setUuid(appList);
+ list.setStatus(Status.OPEN);
+ when(applicationListRepository.findByUuid(appList)).thenReturn(Optional.of(list));
+ when(applicationListEntryRepository.findByUuidsInSourceList(
+ eq(appList),
+ argThat(set -> set.containsAll(List.of(appEntry, missingEntry)))))
+ .thenReturn(List.of(entry(appEntry, list)));
+ OrderIndependantUuidMatcher uuidMatcher =
+ new OrderIndependantUuidMatcher(List.of(appEntry, missingEntry));
+ when(applicationListEntryRepository.findActiveByUuids(argThat(uuidMatcher)))
+ .thenReturn(List.of(entry(appEntry, list)));
+
+ var exception =
+ Assertions.assertThrows(
+ AppRegistryException.class,
+ () ->
+ validator.validate(
+ payloadForCreateEntryResult,
+ (v, r) -> {
+ return true;
+ }));
+
+ Assertions.assertEquals(
+ ApplicationListEntryResultError.APPLICATION_ENTRY_DOES_NOT_EXIST.getCode(),
+ exception.getCode().getCode());
+ }
+
@Test
void testNoListFoundForAllEntries() {
PayloadForCreateResults payloadForCreateEntryResult =
@@ -237,9 +299,11 @@ void testNoListFoundForAllEntries() {
OrderIndependantUuidMatcher uuidMatcher =
new OrderIndependantUuidMatcher(List.of(appEntry, appEntry2));
- // return only one entry so we error
- when(applicationListEntryRepository.findApplicationListForAllEntries(argThat(uuidMatcher)))
- .thenReturn(List.of(new EntryToList(UUID.randomUUID(), UUID.randomUUID())));
+ var list = new ApplicationList();
+ list.setUuid(UUID.randomUUID());
+ list.setStatus(Status.OPEN);
+ when(applicationListEntryRepository.findActiveByUuids(argThat(uuidMatcher)))
+ .thenReturn(List.of(entry(UUID.randomUUID(), list)));
// run the validation
AppRegistryException exception =
@@ -327,4 +391,11 @@ public boolean matches(List uuid) {
return true;
}
}
+
+ private ApplicationListEntry entry(UUID entryId, ApplicationList list) {
+ var entry = new ApplicationListEntry();
+ entry.setUuid(entryId);
+ entry.setApplicationList(list);
+ return entry;
+ }
}
diff --git a/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultDeletionValidatorTest.java b/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultDeletionValidatorTest.java
new file mode 100644
index 000000000..87b850906
--- /dev/null
+++ b/src/test/java/uk/gov/hmcts/appregister/applicationentryresult/validator/BulkApplicationEntryResultDeletionValidatorTest.java
@@ -0,0 +1,170 @@
+package uk.gov.hmcts.appregister.applicationentryresult.validator;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.UUID;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import uk.gov.hmcts.appregister.applicationentryresult.exception.ApplicationListEntryResultError;
+import uk.gov.hmcts.appregister.applicationlist.exception.ApplicationListError;
+import uk.gov.hmcts.appregister.common.entity.AppListEntryResolution;
+import uk.gov.hmcts.appregister.common.entity.ApplicationList;
+import uk.gov.hmcts.appregister.common.entity.ApplicationListEntry;
+import uk.gov.hmcts.appregister.common.entity.ResolutionCode;
+import uk.gov.hmcts.appregister.common.entity.repository.AppListEntryResolutionRepository;
+import uk.gov.hmcts.appregister.common.entity.repository.ApplicationListEntryRepository;
+import uk.gov.hmcts.appregister.common.entity.repository.ApplicationListRepository;
+import uk.gov.hmcts.appregister.common.enumeration.YesOrNo;
+import uk.gov.hmcts.appregister.common.exception.AppRegistryException;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultItemDto;
+import uk.gov.hmcts.appregister.generated.model.BulkDeleteResultsDto;
+
+@ExtendWith(MockitoExtension.class)
+class BulkApplicationEntryResultDeletionValidatorTest {
+
+ @Mock private ApplicationListRepository applicationListRepository;
+ @Mock private ApplicationListEntryRepository applicationListEntryRepository;
+ @Mock private AppListEntryResolutionRepository appListEntryResolutionRepository;
+
+ @InjectMocks private BulkApplicationEntryResultDeletionValidator validator;
+
+ @Test
+ void validate_whenResultsMissing_thenThrows() {
+ var exception =
+ assertThrows(
+ AppRegistryException.class,
+ () -> validator.validate(new BulkDeleteResultsDto()));
+
+ assertThat(exception.getCode()).isEqualTo(ApplicationListError.ENTRY_NOT_PROVIDED);
+ }
+
+ @Test
+ void validate_whenDuplicateItems_thenThrows() {
+ var item =
+ new BulkDeleteResultItemDto()
+ .listId(UUID.randomUUID())
+ .entryId(UUID.randomUUID())
+ .resultId(UUID.randomUUID());
+
+ var request = new BulkDeleteResultsDto().results(List.of(item, item));
+
+ var exception = assertThrows(AppRegistryException.class, () -> validator.validate(request));
+
+ assertThat(exception.getCode()).isEqualTo(ApplicationListError.ENTRY_IDS_MUST_BE_UNIQUE);
+ }
+
+ @Test
+ void validate_whenListMissing_thenThrows() {
+ var item =
+ new BulkDeleteResultItemDto()
+ .listId(UUID.randomUUID())
+ .entryId(UUID.randomUUID())
+ .resultId(UUID.randomUUID());
+
+ var exception =
+ assertThrows(
+ AppRegistryException.class,
+ () ->
+ validator.validate(
+ new BulkDeleteResultsDto().results(List.of(item))));
+
+ assertThat(exception.getCode())
+ .isEqualTo(ApplicationListEntryResultError.APPLICATION_LIST_DOES_NOT_EXIST);
+ }
+
+ @Test
+ void validate_whenValid_thenReturnsValidatedItems() {
+ final UUID listId = UUID.randomUUID();
+ final UUID entryId = UUID.randomUUID();
+ final UUID resultId = UUID.randomUUID();
+
+ var list = new ApplicationList();
+ list.setUuid(listId);
+ list.setDeleted(YesOrNo.NO);
+ list.setStatus(uk.gov.hmcts.appregister.common.enumeration.Status.OPEN);
+
+ var entry = new ApplicationListEntry();
+ entry.setUuid(entryId);
+ entry.setApplicationList(list);
+
+ var code = new ResolutionCode();
+ code.setResultCode("RTC");
+
+ var result = new AppListEntryResolution();
+ result.setUuid(resultId);
+ result.setApplicationList(entry);
+ result.setResolutionCode(code);
+
+ when(applicationListRepository.findByUuidIncludingDeleteIn(List.of(listId)))
+ .thenReturn(List.of(list));
+ when(applicationListEntryRepository.findByUuidIncludingDeleteIn(List.of(entryId)))
+ .thenReturn(List.of(entry));
+ when(applicationListEntryRepository.findActiveByUuidsAndApplicationListUuids(
+ List.of(entryId), List.of(listId)))
+ .thenReturn(List.of(entry));
+ when(appListEntryResolutionRepository.findByUuidIn(List.of(resultId)))
+ .thenReturn(List.of(result));
+
+ var item = new BulkDeleteResultItemDto().listId(listId).entryId(entryId).resultId(resultId);
+ var success =
+ validator.validate(
+ new BulkDeleteResultsDto().results(List.of(item)),
+ (request, validated) -> validated);
+
+ assertThat(success.getResults()).hasSize(1);
+ assertThat(success.getResults().getFirst().args().listId()).isEqualTo(listId);
+ }
+
+ @Test
+ void validate_whenResultNotInEntry_thenThrows() {
+ final UUID listId = UUID.randomUUID();
+ final UUID entryId = UUID.randomUUID();
+ final UUID resultId = UUID.randomUUID();
+
+ var list = new ApplicationList();
+ list.setUuid(listId);
+ list.setDeleted(YesOrNo.NO);
+ list.setStatus(uk.gov.hmcts.appregister.common.enumeration.Status.OPEN);
+
+ var activeEntry = new ApplicationListEntry();
+ activeEntry.setUuid(entryId);
+ activeEntry.setApplicationList(list);
+
+ final UUID otherEntryId = UUID.randomUUID();
+ var otherEntry = new ApplicationListEntry();
+ otherEntry.setUuid(otherEntryId);
+ otherEntry.setApplicationList(list);
+
+ var result = new AppListEntryResolution();
+ result.setUuid(resultId);
+ result.setApplicationList(otherEntry);
+
+ when(applicationListRepository.findByUuidIncludingDeleteIn(List.of(listId)))
+ .thenReturn(List.of(list));
+ when(applicationListEntryRepository.findByUuidIncludingDeleteIn(List.of(entryId)))
+ .thenReturn(List.of(activeEntry));
+ when(applicationListEntryRepository.findActiveByUuidsAndApplicationListUuids(
+ List.of(entryId), List.of(listId)))
+ .thenReturn(List.of(activeEntry));
+ when(appListEntryResolutionRepository.findByUuidIn(List.of(resultId)))
+ .thenReturn(List.of(result));
+
+ var item = new BulkDeleteResultItemDto().listId(listId).entryId(entryId).resultId(resultId);
+ var exception =
+ assertThrows(
+ AppRegistryException.class,
+ () ->
+ validator.validate(
+ new BulkDeleteResultsDto().results(List.of(item))));
+
+ assertThat(exception.getCode())
+ .isEqualTo(
+ ApplicationListEntryResultError.APPLICATION_ENTRY_RESULT_NOT_WITHIN_ENTRY);
+ }
+}
diff --git a/src/test/java/uk/gov/hmcts/appregister/audit/DataAuditLoggerTest.java b/src/test/java/uk/gov/hmcts/appregister/audit/DataAuditLoggerTest.java
index 813b6b778..2f4353226 100644
--- a/src/test/java/uk/gov/hmcts/appregister/audit/DataAuditLoggerTest.java
+++ b/src/test/java/uk/gov/hmcts/appregister/audit/DataAuditLoggerTest.java
@@ -289,6 +289,41 @@ void testCreateAuditStillPersistsEmptyStringNewValue() {
CrudEnum.CREATE, auditListCaptor.getValue().getFirst().getUpdateType());
}
+ @Test
+ void testUpdateAuditUsesClobColumnsWhenValuesExceedLimit() {
+ val oldValue = "a".repeat(4001);
+ val newValue = "b".repeat(4001);
+ var oldEntity = Mockito.mock(Keyable.class);
+ var newEntity = Mockito.mock(Keyable.class);
+ when(oldEntity.getId()).thenReturn(123L);
+ when(newEntity.getId()).thenReturn(123L);
+
+ StartEvent startEvent =
+ new StartEvent(AppListAuditOperation.UPDATE_APP_LIST, "ID", oldEntity);
+ CompleteEvent auditRequest = new CompleteEvent(startEvent, null, newEntity);
+
+ when(auditDifferentiator.extractAuditData(CrudEnum.UPDATE, oldEntity))
+ .thenReturn(
+ List.of(
+ new AuditableData(
+ TableNames.APPLICATION_LISTS, "field", oldValue)));
+ when(auditDifferentiator.extractAuditData(CrudEnum.UPDATE, newEntity))
+ .thenReturn(
+ List.of(
+ new AuditableData(
+ TableNames.APPLICATION_LISTS, "field", newValue)));
+
+ new DataAuditLogger(auditDifferentiator, nestedAuditPersistenceManager)
+ .eventPerformed(auditRequest);
+
+ verify(nestedAuditPersistenceManager).persistOrBuffer(auditListCaptor.capture());
+ var audit = auditListCaptor.getValue().getFirst();
+ Assertions.assertNull(audit.getOldValue());
+ Assertions.assertNull(audit.getNewValue());
+ Assertions.assertEquals(oldValue, audit.getOldClobValue());
+ Assertions.assertEquals(newValue, audit.getNewClobValue());
+ }
+
@Test
void testAuditSaveFailureDoesNotEscapeOnCompleteEvent() {
val testData = new ApplicationCodeTestData();
diff --git a/src/test/java/uk/gov/hmcts/appregister/audit/listener/diff/BulkAuditFormattingTest.java b/src/test/java/uk/gov/hmcts/appregister/audit/listener/diff/BulkAuditFormattingTest.java
new file mode 100644
index 000000000..d891537fe
--- /dev/null
+++ b/src/test/java/uk/gov/hmcts/appregister/audit/listener/diff/BulkAuditFormattingTest.java
@@ -0,0 +1,25 @@
+package uk.gov.hmcts.appregister.audit.listener.diff;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.UUID;
+import org.junit.jupiter.api.Test;
+
+class BulkAuditFormattingTest {
+
+ @Test
+ void formatSortedUuidArray_sortsAndFormatsValues() {
+ var first = UUID.fromString("11111111-1111-1111-1111-111111111111");
+ var second = UUID.fromString("22222222-2222-2222-2222-222222222222");
+
+ assertThat(BulkAuditFormatting.formatSortedUuidArray(List.of(second, first)))
+ .isEqualTo("[\"%s\",\"%s\"]".formatted(first, second));
+ }
+
+ @Test
+ void escape_returnsEmptyStringForNullAndEscapesQuotesAndBackslashes() {
+ assertThat(BulkAuditFormatting.escape(null)).isEmpty();
+ assertThat(BulkAuditFormatting.escape("a\\b\"c")).isEqualTo("a\\\\b\\\"c");
+ }
+}
diff --git a/src/test/java/uk/gov/hmcts/appregister/config/StartupSecretDiagnosticsLoggerTest.java b/src/test/java/uk/gov/hmcts/appregister/config/StartupSecretDiagnosticsLoggerTest.java
new file mode 100644
index 000000000..e354e03a9
--- /dev/null
+++ b/src/test/java/uk/gov/hmcts/appregister/config/StartupSecretDiagnosticsLoggerTest.java
@@ -0,0 +1,63 @@
+package uk.gov.hmcts.appregister.config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import nl.altindag.log.LogCaptor;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.springframework.mock.env.MockEnvironment;
+
+class StartupSecretDiagnosticsLoggerTest {
+ @TempDir Path tempDir;
+
+ @Test
+ void givenMountedSecretsAndResolvedValues_whenRun_thenLogsMountedAndResolvedState()
+ throws Exception {
+ Files.writeString(
+ tempDir.resolve(StartupSecretDiagnosticsLogger.POSTGRES_DATABASE), "appreg-db");
+ Files.writeString(
+ tempDir.resolve(StartupSecretDiagnosticsLogger.CSDS_BASE_URL),
+ "https://csds.dev.apps.hmcts.net/api/rest");
+
+ var environment =
+ new MockEnvironment()
+ .withProperty(StartupSecretDiagnosticsLogger.POSTGRES_DATABASE, "appreg-db")
+ .withProperty(
+ StartupSecretDiagnosticsLogger.CSDS_BASE_URL,
+ "https://csds.dev.apps.hmcts.net/api/rest");
+ var logger = new StartupSecretDiagnosticsLogger(environment, tempDir.toString());
+ var logCaptor = LogCaptor.forClass(StartupSecretDiagnosticsLogger.class);
+ logCaptor.clearLogs();
+
+ logger.run(null);
+
+ assertThat(logCaptor.getInfoLogs())
+ .contains(
+ "Startup secret diagnostic: property=POSTGRES_DATABASE resolvedValue=appreg-db "
+ + "usingDefault=true mountedFilePresent=true mountedFileReadable=true",
+ "Startup secret diagnostic: property=CSDS_BASE_URL "
+ + "resolvedValue=https://csds.dev.apps.hmcts.net/api/rest "
+ + "usingDefault=false mountedFilePresent=true mountedFileReadable=true");
+ }
+
+ @Test
+ void givenMissingCsdsFileAndFallbackValue_whenRun_thenLogsDefaultAndMissingFile()
+ throws Exception {
+ Files.writeString(
+ tempDir.resolve(StartupSecretDiagnosticsLogger.POSTGRES_DATABASE), "appreg-db");
+
+ var environment = new MockEnvironment();
+ var logger = new StartupSecretDiagnosticsLogger(environment, tempDir.toString());
+ var logCaptor = LogCaptor.forClass(StartupSecretDiagnosticsLogger.class);
+ logCaptor.clearLogs();
+
+ logger.run(null);
+
+ assertThat(logCaptor.getInfoLogs())
+ .contains(
+ "Startup secret diagnostic: property=CSDS_BASE_URL resolvedValue=http://noop "
+ + "usingDefault=true mountedFilePresent=false mountedFileReadable=false");
+ }
+}