From bfcd63d7eccabd2ba78a774409b6d6df45e76546 Mon Sep 17 00:00:00 2001 From: Josh <23316525+josh-uk@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:49:03 +0100 Subject: [PATCH 01/40] FACT-2618 Remove old service centre code (#271) Remove old service centre code --- .../controllers/AuthFunctionalTest.java | 3 - .../CourtControllerFunctionalTest.java | 14 --- .../SearchCourtControllerFunctionalTest.java | 5 +- ...chServiceAreaControllerFunctionalTest.java | 68 ---------- .../functional/helpers/TestDataHelper.java | 2 - .../fact/data/api/audit/AuditingTest.java | 1 - .../api/controllers/AuditControllerTest.java | 1 - .../api/controllers/CourtControllerTest.java | 2 - .../SearchServiceAreaControllerTest.java | 69 ---------- .../data/api/db/CourtLastUpdatedTimeTest.java | 1 - .../controllers/TestingSupportController.java | 4 - .../search/SearchServiceAreaController.java | 46 ------- .../api/entities/AbstractCourtEntity.java | 4 - .../data/api/entities/CourtServiceAreas.java | 91 -------------- .../api/entities/types/SearchStrategy.java | 2 - .../service/CourtMigrationHelper.java | 77 ++---------- .../migration/service/MigrationContext.java | 6 - .../migration/service/MigrationService.java | 6 +- .../repositories/CourtAddressRepository.java | 89 ------------- .../CourtServiceAreasRepository.java | 53 -------- .../fact/data/api/services/CourtService.java | 2 +- .../api/services/CourtServiceAreaService.java | 56 --------- .../data/api/services/ServiceAreaService.java | 19 +-- .../api/services/TestingSupportService.java | 53 ++------ .../services/search/SearchCourtService.java | 12 +- .../api/services/search/SearchExecuter.java | 48 ------- ...7__drop_service_centre_data_structures.sql | 9 ++ .../AuditableCourtEntityListenerTest.java | 1 - .../api/controllers/CourtControllerTest.java | 1 - .../TestingSupportControllerTest.java | 16 +-- .../SearchServiceAreaControllerTest.java | 42 ------- .../service/CourtMigrationHelperTest.java | 25 +--- .../service/MigrationServiceTest.java | 19 +-- .../services/CourtServiceAreaServiceTest.java | 119 ------------------ .../api/services/SearchCourtServiceTest.java | 31 +---- .../api/services/ServiceAreaServiceTest.java | 4 - .../services/TestingSupportServiceTest.java | 84 +++---------- .../services/search/SearchExecuterTest.java | 92 -------------- 38 files changed, 64 insertions(+), 1113 deletions(-) delete mode 100644 src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchServiceAreaControllerFunctionalTest.java delete mode 100644 src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaControllerTest.java delete mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaController.java delete mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/CourtServiceAreas.java delete mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtServiceAreasRepository.java delete mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceAreaService.java create mode 100644 src/main/resources/db/migration/V1.27__drop_service_centre_data_structures.sql delete mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaControllerTest.java delete mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceAreaServiceTest.java diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuthFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuthFunctionalTest.java index e9ab2ebbd..4373eebf7 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuthFunctionalTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuthFunctionalTest.java @@ -77,7 +77,6 @@ private static UUID createCourtAsAdmin(String name) { court.setName(TestDataHelper.appendRandomSuffixToCourtName(name)); court.setRegionId(UUID.fromString(getRegionId())); court.setOpen(Boolean.FALSE); - court.setIsServiceCentre(true); Response createResponse = http.doPost("/courts/v1", court, adminToken); assertThat(createResponse.statusCode()).isEqualTo(201); return UUID.fromString(createResponse.jsonPath().getString("id")); @@ -159,7 +158,6 @@ void courtControllerAuth() { testingCourt.setName(TestDataHelper.appendRandomSuffixToCourtName("Test Court Auth")); testingCourt.setOpen(Boolean.FALSE); testingCourt.setRegionId(UUID.fromString(getRegionId())); - testingCourt.setIsServiceCentre(true); // initially test that only the admin can create the court Response createViewer = http.doPost("/courts/v1", testingCourt, viewerToken); @@ -637,7 +635,6 @@ void searchEndpointsAuth() { "/search/courts/v1/postcode?postcode=SW1A1AA&limit=5", "courts by postcode", "/search/courts/v1/prefix?prefix=A", "courts by prefix", "/search/courts/v1/name?q=court", "courts by name", - "/search/service-area/v1/adoption", "service area", "/search/services/v1", "services", "/search/services/v1/divorce/service-areas", "service areas by service" ); diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/CourtControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/CourtControllerFunctionalTest.java index 3c949102a..e926d22ea 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/CourtControllerFunctionalTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/CourtControllerFunctionalTest.java @@ -44,7 +44,6 @@ void shouldCreateCourtWithValidPayload() throws Exception { final Court court = new Court(); court.setName(TestDataHelper.appendRandomSuffixToCourtName("Test Court Create Valid")); court.setRegionId(UUID.fromString(regionId)); - court.setIsServiceCentre(true); court.setOpen(false); final Response createResponse = http.doPost("/courts/v1", court); @@ -67,9 +66,6 @@ void shouldCreateCourtWithValidPayload() throws Exception { assertThat(fetchedCourt.getRegionId()) .as("Court region ID should match") .isEqualTo(UUID.fromString(regionId)); - assertThat(fetchedCourt.getIsServiceCentre()) - .as("Court should be a service centre") - .isTrue(); } @Test @@ -78,7 +74,6 @@ void shouldFailWithNonExistentRegionId() throws Exception { final Court court = new Court(); court.setName(TestDataHelper.appendRandomSuffixToCourtName("Test Court Invalid Region")); court.setRegionId(UUID.randomUUID()); - court.setIsServiceCentre(true); court.setOpen(false); final Response response = http.doPost("/courts/v1", court); @@ -95,7 +90,6 @@ void shouldFailWithNonExistentRegionId() throws Exception { void shouldFailWithMissingName() throws Exception { final Court court = new Court(); court.setRegionId(UUID.fromString(regionId)); - court.setIsServiceCentre(true); final Response response = http.doPost("/courts/v1", court); @@ -130,7 +124,6 @@ void shouldUpdateExistingCourt() throws Exception { final Court updatedCourt = new Court(); updatedCourt.setName(TestDataHelper.appendRandomSuffixToCourtName("Test Court Updated")); updatedCourt.setRegionId(UUID.fromString(regionId)); - updatedCourt.setIsServiceCentre(true); updatedCourt.setOpen(false); final Response updateResponse = http.doPut("/courts/" + courtId + "/v1", updatedCourt); @@ -145,10 +138,6 @@ void shouldUpdateExistingCourt() throws Exception { assertThat(fetchedCourt.getName()) .as("Court name should be updated") .startsWith("Test Court Updated"); - assertThat(fetchedCourt.getIsServiceCentre()) - .as("Court should remain a service centre") - .isTrue(); - final ZonedDateTime timestampAfterUpdate = AssertionHelper.getCourtLastUpdatedAt(http, courtId); assertThat(timestampAfterUpdate) .as("Court lastUpdatedAt should move forward after court update for court %s", courtId) @@ -163,7 +152,6 @@ void shouldFailToUpdateNonExistentCourt() throws Exception { final Court updatedCourt = new Court(); updatedCourt.setName(TestDataHelper.appendRandomSuffixToCourtName("Test Court Non-Existent court ID")); updatedCourt.setRegionId(UUID.fromString(regionId)); - updatedCourt.setIsServiceCentre(true); updatedCourt.setOpen(false); final Response response = http.doPut("/courts/" + nonExistentCourtId + "/v1", updatedCourt); @@ -183,7 +171,6 @@ void shouldFailToUpdateCourtWithNonExistentRegionId() throws Exception { final Court updatedCourt = new Court(); updatedCourt.setName(TestDataHelper.appendRandomSuffixToCourtName("Test Court Non-Existent Region ID Updated")); updatedCourt.setRegionId(UUID.randomUUID()); - updatedCourt.setIsServiceCentre(true); updatedCourt.setOpen(false); final Response response = http.doPut("/courts/" + courtId + "/v1", updatedCourt); @@ -231,7 +218,6 @@ void shouldReturnOnlyActiveCourts() throws Exception { final Court updatedCourt = new Court(); updatedCourt.setName(TestDataHelper.appendRandomSuffixToCourtName("Test Court Open")); updatedCourt.setRegionId(UUID.fromString(regionId)); - updatedCourt.setIsServiceCentre(true); updatedCourt.setOpen(true); final Response updateResponse = http.doPut("/courts/" + courtId + "/v1", updatedCourt); diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchCourtControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchCourtControllerFunctionalTest.java index 7cc472d3c..ca63ac9ec 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchCourtControllerFunctionalTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchCourtControllerFunctionalTest.java @@ -159,9 +159,7 @@ void shouldReturnNearestCourtsForPostcodeOnlySearch() throws Exception { * - Childcare SPOE special-case (single point of entry search) * *

- * The only search strategy not covered is FAMILY_REGIONAL, because there is currently no API endpoint to - * link courts to service areas (court_service_areas). Once an endpoint is available, a functional test - * should be added to cover the FAMILY_REGIONAL flow end-to-end. + * TODO: Add service-centre search coverage when that model is introduced. */ @Test @DisplayName("GET /search/courts/v1/postcode returns courts for OTHER type service area using DEFAULT_AOL_DISTANCE") @@ -652,7 +650,6 @@ private static UUID createOpenCourt(final String courtName) { final Court courtToUpdate = new Court(); courtToUpdate.setName(TestDataHelper.appendRandomSuffixToCourtName(courtName)); courtToUpdate.setRegionId(UUID.fromString(regionId)); - courtToUpdate.setIsServiceCentre(true); courtToUpdate.setOpen(true); final Response updateResponse = http.doPut("/courts/" + courtId + "/v1", courtToUpdate); diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchServiceAreaControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchServiceAreaControllerFunctionalTest.java deleted file mode 100644 index 7ad82c13b..000000000 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchServiceAreaControllerFunctionalTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package uk.gov.hmcts.reform.fact.functional.controllers.search; - -import tools.jackson.core.type.TypeReference; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.cfg.DateTimeFeature; -import tools.jackson.databind.json.JsonMapper; -import io.qameta.allure.Feature; -import io.restassured.response.Response; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import uk.gov.hmcts.reform.fact.data.api.entities.Court; -import uk.gov.hmcts.reform.fact.functional.http.HttpClient; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.http.HttpStatus.NOT_FOUND; -import static org.springframework.http.HttpStatus.OK; - -@Feature("Search Service Area Controller") -@DisplayName("Search Service Area Controller") -public final class SearchServiceAreaControllerFunctionalTest { - - private static final HttpClient http = new HttpClient(); - private static final ObjectMapper mapper = JsonMapper.builder() - .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) - .build(); - - /** - * This test asserts for an empty list at the moment because there is currently no API endpoint - * to associate courts with service areas. The only way to create this link is by - * directly updating the database manually or through a migration script, which is - * not recommended for functional tests. Once an endpoint to link courts to service - * areas is available, this test should be updated to create the association and - * assert that the court is returned. - */ - @Test - @DisplayName("GET /search/service-area/v1/{serviceAreaName} returns empty list when no courts are linked") - void shouldReturnEmptyListForServiceAreaWithNoCourts() { - final String serviceAreaName = "Tax"; - final Response response = http.doGet("/search/service-area/v1/" + serviceAreaName); - - assertThat(response.statusCode()) - .as("Expected 200 OK for valid service area name '%s'", serviceAreaName) - .isEqualTo(OK.value()); - - final List courts = mapper.readValue( - response.getBody().asString(), - new TypeReference>() {} - ); - - assertThat(courts) - .as("Expected empty list as no courts are currently linked to service area '%s'", - serviceAreaName) - .isEmpty(); - } - - @Test - @DisplayName("GET /search/service-area/v1/{serviceAreaName} returns 404 for non-existent service area") - void shouldReturn404ForNonExistentServiceArea() { - final String nonExistentServiceArea = "Non Existent Service Area"; - final Response response = http.doGet("/search/service-area/v1/" + nonExistentServiceArea); - - assertThat(response.statusCode()) - .as("Expected 404 Not Found for non-existent service area '%s'", nonExistentServiceArea) - .isEqualTo(NOT_FOUND.value()); - } -} diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/helpers/TestDataHelper.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/helpers/TestDataHelper.java index fc71da071..b3814b765 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/helpers/TestDataHelper.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/helpers/TestDataHelper.java @@ -131,7 +131,6 @@ public static UUID createCourt(final HttpClient http, final String courtName, bo final Court court = new Court(); court.setName(appendRandomSuffixToCourtName(courtName)); court.setRegionId(UUID.fromString(fetchFirstRegionId(http))); - court.setIsServiceCentre(true); court.setOpen(isOpen); final Response createResponse = http.doPost("/courts/v1", court); @@ -156,7 +155,6 @@ public static UUID createCourt( final Court court = new Court(); court.setName(appendRandomSuffixToCourtName(courtName)); court.setRegionId(UUID.fromString(fetchFirstRegionId(http))); - court.setIsServiceCentre(true); court.setOpen(isOpen); court.setMrdId(mrdId); court.setOpenOnCath(openOnCath); diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java index e6b1ee317..4ea5d0c1e 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java @@ -363,7 +363,6 @@ private Court createCourt(UUID regionId, String name) { .name(name) .open(Boolean.FALSE) .regionId(regionId) - .isServiceCentre(Boolean.FALSE) .build(); } } diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index a3feda42b..8bc737ebc 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -306,7 +306,6 @@ private Court buildCourt(UUID regionId, String name) { .name(name) .open(Boolean.FALSE) .regionId(regionId) - .isServiceCentre(Boolean.FALSE) .build(); } diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java index 9fcb438a0..6c5cab536 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java @@ -543,7 +543,6 @@ private Court buildCourt(UUID id) { .open(Boolean.TRUE) .warningNotice("Notice") .regionId(REGION_ID) - .isServiceCentre(Boolean.TRUE) .openOnCath(Boolean.TRUE) .mrdId("MRD123") .build(); @@ -557,7 +556,6 @@ private CourtDetails buildCourtDetails(UUID id, String name) { .open(Boolean.TRUE) .warningNotice("Notice") .regionId(REGION_ID) - .isServiceCentre(Boolean.TRUE) .openOnCath(Boolean.TRUE) .mrdId("MRD123") .build(); diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaControllerTest.java deleted file mode 100644 index 11e5ab250..000000000 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaControllerTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package uk.gov.hmcts.reform.fact.data.api.controllers.search; - -import io.qameta.allure.Feature; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.web.servlet.MockMvc; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtServiceAreas; -import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; -import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; -import uk.gov.hmcts.reform.fact.data.api.services.CourtServiceAreaService; - -import java.util.List; -import java.util.UUID; - -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@Feature("Search Service Area Controller") -@DisplayName("Search Service Area Controller") -@WebMvcTest(SearchServiceAreaController.class) -@AutoConfigureMockMvc(addFilters = false) -class SearchServiceAreaControllerTest { - - private static final UUID COURT_ID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); - private static final UUID SERVICE_AREA_ID = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); - - @Autowired - private MockMvc mockMvc; - - @MockitoBean - private CourtServiceAreaService courtServiceAreaService; - - @Test - @DisplayName("GET /search/service-area/v1/{serviceAreaName} returns court service areas") - void getServiceAreaReturnsOk() throws Exception { - CourtServiceAreas area = new CourtServiceAreas(); - area.setCourtId(COURT_ID); - area.setServiceAreaId(List.of(SERVICE_AREA_ID)); - area.setCatchmentType(CatchmentType.REGIONAL); - area.setCourtName("court name"); - area.setCourtSlug("court-name"); - - when(courtServiceAreaService.findByServiceAreaName("Family")) - .thenReturn(List.of(area)); - - mockMvc.perform(get("/search/service-area/v1/{serviceAreaName}", "Family")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].courtId").value(COURT_ID.toString())) - .andExpect(jsonPath("$[0].catchmentType").value("REGIONAL")) - .andExpect(jsonPath("$[0].courtName").value("court name")) - .andExpect(jsonPath("$[0].courtSlug").value("court-name")); - } - - @Test - @DisplayName("GET /search/service-area/v1/{serviceAreaName} returns 404 when missing") - void getServiceAreaReturnsNotFound() throws Exception { - when(courtServiceAreaService.findByServiceAreaName("Family")) - .thenThrow(new NotFoundException("Service area Family not found")); - - mockMvc.perform(get("/search/service-area/v1/{serviceAreaName}", "Family")) - .andExpect(status().isNotFound()); - } -} diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/db/CourtLastUpdatedTimeTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/db/CourtLastUpdatedTimeTest.java index 84c38fe26..6972d3e8c 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/db/CourtLastUpdatedTimeTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/db/CourtLastUpdatedTimeTest.java @@ -190,7 +190,6 @@ private Court buildCourt(UUID regionId, String name, String slug) { .open(Boolean.TRUE) .warningNotice("Notice") .regionId(regionId) - .isServiceCentre(Boolean.TRUE) .openOnCath(Boolean.TRUE) .mrdId("MRD123") .build(); diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java index 87cab59ff..4e605d112 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java @@ -83,23 +83,19 @@ public ResponseEntity createSampleCourt( @RequestParam(required = true) String courtName, @RequestParam(required = false) UUID regionId, @RequestParam(required = false) Long seed, - @RequestParam(required = false, defaultValue = "false") boolean serviceCenter, @RequestParam(required = false, defaultValue = "true") boolean open, @RequestParam(required = false, defaultValue = "false") boolean addWarningNotice, @RequestParam(required = false, defaultValue = "true") boolean withTranslations, @RequestParam(required = false, defaultValue = "true") boolean withEnquiriesContact, - @RequestParam(required = false, defaultValue = "false") boolean withServiceAreaAssociation, @RequestParam(required = false, defaultValue = "false") boolean forceFamilyCourt) { String courtSlug = testingSupportService.createCourt( courtName, regionId, seed, - serviceCenter, open, addWarningNotice, withTranslations, withEnquiriesContact, - withServiceAreaAssociation, forceFamilyCourt ); CourtDetails details = courtService.getCourtDetailsBySlug(courtSlug); diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaController.java deleted file mode 100644 index 651802364..000000000 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaController.java +++ /dev/null @@ -1,46 +0,0 @@ -package uk.gov.hmcts.reform.fact.data.api.controllers.search; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtServiceAreas; -import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; -import uk.gov.hmcts.reform.fact.data.api.services.CourtServiceAreaService; - -import java.util.List; - -@SecuredFactRestController( - name = "Search Service Area", - description = "Operations related to the searching of service areas" -) -@RequestMapping("/search/service-area") -public class SearchServiceAreaController { - - private final CourtServiceAreaService courtServiceAreaService; - - public SearchServiceAreaController(CourtServiceAreaService courtServiceAreaService) { - this.courtServiceAreaService = courtServiceAreaService; - } - - @GetMapping("/v1/{serviceAreaName}") - @Operation( - summary = "Search for service area by name.", - description = "Retrieve Service Area based on provided name." - ) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved service area based on provided name."), - @ApiResponse(responseCode = "400", description = "Service area name is missing or is not valid."), - @ApiResponse(responseCode = "404", description = "Information not found for provided service area name.") - }) - public ResponseEntity> getServiceAreaByName( - @Parameter(description = "Service area name", required = true) - @PathVariable String serviceAreaName - ) { - return ResponseEntity.ok(courtServiceAreaService.findByServiceAreaName(serviceAreaName)); - } -} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AbstractCourtEntity.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AbstractCourtEntity.java index 4b605af35..77dab9c10 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AbstractCourtEntity.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AbstractCourtEntity.java @@ -87,10 +87,6 @@ public abstract class AbstractCourtEntity { @Column(name = "region_id") private UUID regionId; - @Schema(description = "Indicates that this Court is Service Centre") - @NotNull - private Boolean isServiceCentre; - @Schema(description = "Indicates that this Court is declared open in the Court and Tribunal Hearing service") private Boolean openOnCath; diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/CourtServiceAreas.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/CourtServiceAreas.java deleted file mode 100644 index 547e0859e..000000000 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/CourtServiceAreas.java +++ /dev/null @@ -1,91 +0,0 @@ -package uk.gov.hmcts.reform.fact.data.api.entities; - -import uk.gov.hmcts.reform.fact.data.api.audit.AuditableCourtEntityListener; -import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; - -import java.util.List; -import java.util.UUID; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import jakarta.persistence.Transient; -import jakarta.validation.constraints.NotNull; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.JdbcTypeCode; -import org.hibernate.type.SqlTypes; - -@Data -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@NoArgsConstructor -@Builder -@Entity -@EntityListeners(AuditableCourtEntityListener.class) -@Table(name = "court_service_areas", schema = "public") -public class CourtServiceAreas implements AuditableCourtEntity { - - @Schema( - description = "The internal ID - assigned by the server during creation", - accessMode = Schema.AccessMode.READ_ONLY - ) - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private UUID id; - - @Schema(description = "The ID of the associated Court", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull - @Column(name = "court_id") - private UUID courtId; - - @JsonIgnore - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "court_id", insertable = false, updatable = false) - private Court court; - - @Schema(description = "The Service Area IDs") - @JdbcTypeCode(SqlTypes.ARRAY) - @Column(columnDefinition = "uuid[]") - private List serviceAreaId; - - @Schema(description = "The catchment type") - @Enumerated(EnumType.STRING) - private CatchmentType catchmentType; - - @Transient - private String courtName; - - @Transient - private String courtSlug; - - // jackson will ignore the above transient fields, so we need to - // explicitly add them as properties for the API response - - @Schema(description = "Has associated court with LOCAL catchment") - @JsonProperty("courtName") - public String getCourtName() { - return courtName; - } - - @Schema(description = "Has associated court with NATIONAL catchment") - @JsonProperty("courtSlug") - public String getCourtSlug() { - return courtSlug; - } - -} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SearchStrategy.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SearchStrategy.java index 36f1cccfb..df0139b8e 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SearchStrategy.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SearchStrategy.java @@ -4,12 +4,10 @@ * Enums for search strategies: * DEFAULT_AOL_DISTANCE is for where individuals are searching for nearest courts. * FAMILY_NON_REGIONAL family area of law search for one court within a local authority. - * FAMILY_REGIONAL same as non-regional, but for local-proximity and regional combinations. * CIVIL_POSTCODE_PREFERENCE civil jurisdictional search based on postcode supplied. */ public enum SearchStrategy { DEFAULT_AOL_DISTANCE, FAMILY_NON_REGIONAL, - FAMILY_REGIONAL, CIVIL_POSTCODE_PREFERENCE } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/CourtMigrationHelper.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/CourtMigrationHelper.java index f47f4f5d7..078b9648e 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/CourtMigrationHelper.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/CourtMigrationHelper.java @@ -21,7 +21,6 @@ import uk.gov.hmcts.reform.fact.data.api.entities.CourtFax; import uk.gov.hmcts.reform.fact.data.api.entities.CourtLocalAuthorities; import uk.gov.hmcts.reform.fact.data.api.entities.CourtProfessionalInformation; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtServiceAreas; import uk.gov.hmcts.reform.fact.data.api.entities.CourtSinglePointsOfEntry; import uk.gov.hmcts.reform.fact.data.api.entities.types.AllowedLocalAuthorityAreasOfLaw; import uk.gov.hmcts.reform.fact.data.api.entities.validation.ValidationConstants; @@ -34,7 +33,6 @@ import uk.gov.hmcts.reform.fact.data.api.migration.model.CourtFaxDto; import uk.gov.hmcts.reform.fact.data.api.migration.model.CourtLocalAuthorityDto; import uk.gov.hmcts.reform.fact.data.api.migration.model.CourtProfessionalInformationDto; -import uk.gov.hmcts.reform.fact.data.api.migration.model.CourtServiceAreaDto; import uk.gov.hmcts.reform.fact.data.api.migration.model.CourtSinglePointOfEntryDto; import uk.gov.hmcts.reform.fact.data.api.repositories.AreaOfLawTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtAreasOfLawRepository; @@ -43,9 +41,7 @@ import uk.gov.hmcts.reform.fact.data.api.repositories.CourtFaxRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtLocalAuthoritiesRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtProfessionalInformationRepository; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtServiceAreasRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtSinglePointsOfEntryRepository; -import uk.gov.hmcts.reform.fact.data.api.repositories.RegionRepository; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; import uk.gov.hmcts.reform.fact.data.api.migration.repository.LegacyCourtMappingRepository; @@ -58,8 +54,6 @@ class CourtMigrationHelper { private static final Pattern INVALID_GENERIC_DESCRIPTION_CHARACTERS = Pattern.compile("[^A-Za-z0-9 ()':,-]+"); - private final RegionRepository regionRepository; - private final CourtServiceAreasRepository courtServiceAreasRepository; private final CourtAreasOfLawRepository courtAreasOfLawRepository; private final CourtSinglePointsOfEntryRepository courtSinglePointsOfEntryRepository; private final CourtLocalAuthoritiesRepository courtLocalAuthoritiesRepository; @@ -72,8 +66,6 @@ class CourtMigrationHelper { private final CourtService courtService; CourtMigrationHelper( - RegionRepository regionRepository, - CourtServiceAreasRepository courtServiceAreasRepository, CourtAreasOfLawRepository courtAreasOfLawRepository, CourtSinglePointsOfEntryRepository courtSinglePointsOfEntryRepository, CourtLocalAuthoritiesRepository courtLocalAuthoritiesRepository, @@ -85,8 +77,6 @@ class CourtMigrationHelper { LegacyCourtMappingRepository legacyCourtMappingRepository, CourtService courtService ) { - this.regionRepository = regionRepository; - this.courtServiceAreasRepository = courtServiceAreasRepository; this.courtAreasOfLawRepository = courtAreasOfLawRepository; this.courtSinglePointsOfEntryRepository = courtSinglePointsOfEntryRepository; this.courtLocalAuthoritiesRepository = courtLocalAuthoritiesRepository; @@ -115,6 +105,12 @@ int migrateCourts(List courts, MigrationContext context) { int total = 0; for (CourtDto dto : courts) { + if (Boolean.TRUE.equals(dto.getIsServiceCentre())) { + //TODO: Only migrate courts. + LOG.info("Skipping service centre {}", dto.getSlug()); + continue; + } + Optional regionId = resolveRegionId(dto, context); if (regionId.isEmpty()) { LOG.warn("Skipping court {} because region {} was not migrated", dto.getSlug(), dto.getRegionId()); @@ -140,7 +136,6 @@ int migrateCourts(List courts, MigrationContext context) { .slug(dto.getSlug()) .open(dto.getOpen()) .regionId(regionId.get()) - .isServiceCentre(dto.getIsServiceCentre()) .build(); Court savedCourt; @@ -151,7 +146,6 @@ int migrateCourts(List courts, MigrationContext context) { throw ex; } UUID courtId = savedCourt.getId(); - persistCourtServiceAreas(dto.getCourtServiceAreas(), courtId, context); persistCourtAreasOfLaw(dto.getCourtAreasOfLaw(), courtId, context); persistCourtSinglePointsOfEntry(dto.getCourtSinglePointsOfEntry(), courtId, context); persistCourtLocalAuthorities(dto.getCourtLocalAuthorities(), courtId, context); @@ -165,34 +159,6 @@ int migrateCourts(List courts, MigrationContext context) { return total; } - /** - * Persists court-service-area joins for a single court. - * - * @param serviceAreas legacy service-area relationships. - * @param courtId identifier of the newly created court. - * @param context migration context containing service-area ID mappings. - */ - private void persistCourtServiceAreas( - List serviceAreas, - UUID courtId, - MigrationContext context - ) { - if (isEmpty(serviceAreas)) { - return; - } - - for (CourtServiceAreaDto dto : serviceAreas) { - CourtServiceAreas.CourtServiceAreasBuilder entityBuilder = CourtServiceAreas.builder() - .courtId(courtId) - .serviceAreaId(mapIds(dto.getServiceAreaIds(), context.getServiceAreaIds(), "court service area")) - ; - parseCatchmentType(dto.getCatchmentType()).ifPresent(entityBuilder::catchmentType); - CourtServiceAreas entity = entityBuilder.build(); - courtServiceAreasRepository.save(entity); - context.courtServiceAreasMigrated++; - } - } - /** * Persists the areas-of-law association for the supplied court. * @@ -475,8 +441,7 @@ private void persistLegacyCourtMapping(Long legacyCourtId, UUID courtId) { } /** - * Resolves the region ID for the supplied court. Service centres fall back to the dedicated - * "Service Centre" region when the legacy payload omits a region. + * Resolves the region ID for the supplied court. * * @param dto legacy court payload. * @param context migration context containing mapped region IDs. @@ -487,33 +452,7 @@ private Optional resolveRegionId(CourtDto dto, MigrationContext context) { if (regionId != null) { return Optional.of(regionId); } - if (!Boolean.TRUE.equals(dto.getIsServiceCentre())) { - return Optional.empty(); - } - UUID fallback = context.getServiceCentreRegionId(); - if (fallback == null) { - Optional loadedFallback = loadServiceCentreRegionId(); - if (loadedFallback.isEmpty()) { - LOG.warn( - "Unable to assign region for service centre '{}' because the fallback region was not found", - dto.getSlug() - ); - return Optional.empty(); - } - fallback = loadedFallback.get(); - context.setServiceCentreRegionId(fallback); - } - return Optional.of(fallback); - } - - /** - * Loads the fallback region for service centres. - * - * @return identifier of the service-centre region when present. - */ - private Optional loadServiceCentreRegionId() { - return regionRepository.findByNameAndCountry("Service Centre", "England") - .map(uk.gov.hmcts.reform.fact.data.api.entities.Region::getId); + return Optional.empty(); } /** diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationContext.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationContext.java index a77d24011..4a9fbf597 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationContext.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationContext.java @@ -12,17 +12,11 @@ class MigrationContext { private final Map areaOfLawIds = new HashMap<>(); private final Map serviceAreaIds = new HashMap<>(); private final Map> localAuthorityTypeIds = new HashMap<>(); - private UUID serviceCentreRegionId; int courtAreasOfLawMigrated; - int courtServiceAreasMigrated; int courtLocalAuthoritiesMigrated; int courtSinglePointsOfEntryMigrated; int courtProfessionalInformationMigrated; int courtCodesMigrated; int courtDxCodesMigrated; int courtFaxMigrated; - - void setServiceCentreRegionId(UUID id) { - this.serviceCentreRegionId = id; - } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationService.java index 16d858ccc..a892f3176 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationService.java @@ -24,7 +24,6 @@ import uk.gov.hmcts.reform.fact.data.api.repositories.CourtFaxRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtLocalAuthoritiesRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtProfessionalInformationRepository; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtServiceAreasRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtSinglePointsOfEntryRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.LocalAuthorityTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.OpeningHoursTypeRepository; @@ -53,7 +52,6 @@ public MigrationService( LocalAuthorityTypeRepository localAuthorityTypeRepository, ContactDescriptionTypeRepository contactDescriptionTypeRepository, OpeningHoursTypeRepository openingHourTypeRepository, - CourtServiceAreasRepository courtServiceAreasRepository, CourtAreasOfLawRepository courtAreasOfLawRepository, CourtSinglePointsOfEntryRepository courtSinglePointsOfEntryRepository, CourtLocalAuthoritiesRepository courtLocalAuthoritiesRepository, @@ -78,8 +76,6 @@ public MigrationService( openingHourTypeRepository ); this.courtMigrationHelper = new CourtMigrationHelper( - regionRepository, - courtServiceAreasRepository, courtAreasOfLawRepository, courtSinglePointsOfEntryRepository, courtLocalAuthoritiesRepository, @@ -118,7 +114,7 @@ MigrationSummary persistExport(LegacyExportResponse exportResponse) { MigrationResult result = new MigrationResult( courtsMigrated, context.getCourtAreasOfLawMigrated(), - context.getCourtServiceAreasMigrated(), + 0, context.getCourtLocalAuthoritiesMigrated(), context.getCourtSinglePointsOfEntryMigrated(), context.getCourtProfessionalInformationMigrated(), diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtAddressRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtAddressRepository.java index 42d4be566..6a9d364dd 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtAddressRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtAddressRepository.java @@ -235,95 +235,6 @@ List findFamilyNonRegionalByLocalAuthority( @Param("limit") int limit ); - /** - * Finds a regional family court for a local authority and service area. - * - * @param serviceAreaId the service area id - * @param lat the latitude to search from - * @param lon the longitude to search from - * @param aolId the area of law id - * @param localAuthorityId the local authority id - * @return matching courts with distance data - */ - @Query( - value = """ - SELECT DISTINCT ON (c.id) - c.id as courtId, - c.name as courtName, - c.slug as courtSlug, - ( - point(CAST(ca.lon AS float8), CAST(ca.lat AS float8)) - <@> - point(CAST(:lon AS float8), CAST(:lat AS float8)) - ) AS distance - FROM court c - JOIN court_address ca ON ca.court_id = c.id - JOIN court_local_authorities cla ON cla.court_id = c.id - JOIN court_service_areas csa ON csa.court_id = c.id - WHERE c.open = true - AND cla.area_of_law_id = CAST(:aolId AS uuid) - AND CAST(:localAuthorityId AS uuid) = ANY(cla.local_authority_ids) - AND CAST(:serviceAreaId AS uuid) = ANY(csa.service_area_id) - AND csa.catchment_type = 'REGIONAL' - AND ca.address_type IN ('VISIT_US', 'VISIT_OR_CONTACT_US') - AND ca.lat IS NOT NULL - AND ca.lon IS NOT NULL - ORDER BY c.id, distance - LIMIT 1 - """, - nativeQuery = true - ) - List findFamilyRegionalByLocalAuthority( - @Param("serviceAreaId") UUID serviceAreaId, - @Param("lat") double lat, - @Param("lon") double lon, - @Param("aolId") UUID aolId, - @Param("localAuthorityId") UUID localAuthorityId - ); - - /** - * Finds a regional family court for a service area and area of law. - * - * @param serviceAreaId the service area id - * @param lat the latitude to search from - * @param lon the longitude to search from - * @param aolId the area of law id - * @return matching courts with distance data - */ - @Query( - value = """ - SELECT DISTINCT ON (c.id) - c.id as courtId, - c.name as courtName, - c.slug as courtSlug, - ( - point(CAST(ca.lon AS float8), CAST(ca.lat AS float8)) - <@> - point(CAST(:lon AS float8), CAST(:lat AS float8)) - ) AS distance - FROM court c - JOIN court_address ca ON ca.court_id = c.id - JOIN court_service_areas csa ON csa.court_id = c.id - JOIN court_areas_of_law coa ON coa.court_id = c.id - WHERE c.open = true - AND CAST(:serviceAreaId AS uuid) = ANY(csa.service_area_id) - AND csa.catchment_type = 'REGIONAL' - AND CAST(:aolId AS uuid) = ANY(coa.areas_of_law) - AND ca.address_type IN ('VISIT_US', 'VISIT_OR_CONTACT_US') - AND ca.lat IS NOT NULL - AND ca.lon IS NOT NULL - ORDER BY c.id, distance - LIMIT 1 - """, - nativeQuery = true - ) - List findFamilyRegionalByAol( - @Param("serviceAreaId") UUID serviceAreaId, - @Param("lat") double lat, - @Param("lon") double lon, - @Param("aolId") UUID aolId - ); - List findByCourtId(UUID courtId); Optional findByIdAndCourtId(UUID addressId, UUID courtId); diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtServiceAreasRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtServiceAreasRepository.java deleted file mode 100644 index b2a49eff9..000000000 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtServiceAreasRepository.java +++ /dev/null @@ -1,53 +0,0 @@ -package uk.gov.hmcts.reform.fact.data.api.repositories; - -import org.springframework.data.jpa.repository.Query; - -import uk.gov.hmcts.reform.fact.data.api.entities.CourtServiceAreas; -import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; - -import java.util.List; -import java.util.UUID; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface CourtServiceAreasRepository extends JpaRepository { - /** - * Finds court service area links for a service area id. - * Note that this query still uses native SQL to match against the Postgres array column with ANY(...). - * - * @param id the service area id - * @return matching court service areas - */ - @Query( - value = "SELECT * FROM court_service_areas c WHERE :id = ANY(c.service_area_id)", - nativeQuery = true - ) - List findByServiceAreaId(UUID id); - - /** - * checks that a court service area link exists for a set of service area id and catchment types. - * - * @param id the service area id - * @param catchmentTypes the list of catchment types - * @return true if a matching court service area exists, false otherwise - */ - @Query( - value = """ - SELECT EXISTS ( - SELECT - csa.id - FROM - court_service_areas csa - WHERE - :id = ANY(csa.service_area_id) - AND - csa.catchment_type IN (:#{#catchmentTypes.![name()]}) - LIMIT 1 - ) - """, - nativeQuery = true - ) - boolean existsByServiceAreaIdAndCatchmentTypeIn(UUID id, List catchmentTypes); -} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtService.java index f8d92413e..3e69b8afd 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtService.java @@ -111,7 +111,7 @@ public Page getFilteredAndPaginatedCourts(int pageNumber, int pageSize, B } /** - * Creates a new court or service centre. + * Creates a new court. * * @param court The court to create. * @return The created court. diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceAreaService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceAreaService.java deleted file mode 100644 index b2f3c3f77..000000000 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceAreaService.java +++ /dev/null @@ -1,56 +0,0 @@ -package uk.gov.hmcts.reform.fact.data.api.services; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtServiceAreas; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtRepository; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtServiceAreasRepository; - -import java.util.List; -import java.util.UUID; - -@Service -@RequiredArgsConstructor -public class CourtServiceAreaService { - - private final CourtServiceAreasRepository courtServiceAreasRepository; - private final ServiceAreaService serviceAreaService; - private final CourtRepository courtRepository; - - /** - * Finds court service area links by service area id. - * - * @param id the service area id - * @return matching court service areas - */ - public List findByServiceAreaId(UUID id) { - return courtServiceAreasRepository.findByServiceAreaId(id).stream() - .map(this::enrichCourtServiceAreas) - .toList(); - } - - /** - * Return the court service area details which includes the court_id on the frontend. - * We can then call a service centre endpoint from the frontend if the - * catchment type is NATIONAL. - * - * @param serviceAreaName The name of the service area. - * @return A list of courts that link to the service area. - */ - public List findByServiceAreaName(String serviceAreaName) { - return courtServiceAreasRepository.findByServiceAreaId( - serviceAreaService.getServiceAreaByName(serviceAreaName).getId()).stream() - .map(this::enrichCourtServiceAreas) - .toList(); - } - - // adds the name and slug for the court into the response - private CourtServiceAreas enrichCourtServiceAreas(CourtServiceAreas courtServiceAreas) { - courtRepository.findNameAndSlugById(courtServiceAreas.getCourtId()) - .ifPresent(courtInfo -> { - courtServiceAreas.setCourtName(courtInfo.name()); - courtServiceAreas.setCourtSlug(courtInfo.slug()); - }); - return courtServiceAreas; - } -} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaService.java index 17092df93..da5d1e1ec 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaService.java @@ -1,9 +1,7 @@ package uk.gov.hmcts.reform.fact.data.api.services; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; -import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtServiceAreasRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; import java.text.MessageFormat; @@ -17,7 +15,6 @@ public class ServiceAreaService { private final ServiceAreaRepository serviceAreaRepository; - private final CourtServiceAreasRepository courtServiceAreasRepository; /** * Retrieves a service area by name. @@ -59,18 +56,10 @@ public List getAllServiceAreasForService(String serviceName) { * @return the service area with additional data */ private ServiceArea enrichServiceArea(ServiceArea serviceArea) { - serviceArea.setHasLocal( - courtServiceAreasRepository.existsByServiceAreaIdAndCatchmentTypeIn( - serviceArea.getId(), List.of(CatchmentType.LOCAL)) - ); - serviceArea.setHasNational( - courtServiceAreasRepository.existsByServiceAreaIdAndCatchmentTypeIn( - serviceArea.getId(), List.of(CatchmentType.NATIONAL)) - ); - serviceArea.setHasRegional( - courtServiceAreasRepository.existsByServiceAreaIdAndCatchmentTypeIn( - serviceArea.getId(), List.of(CatchmentType.REGIONAL)) - ); + //TODO: Re-populate catchment availability from service-centres when that model is introduced. + serviceArea.setHasLocal(false); + serviceArea.setHasNational(false); + serviceArea.setHasRegional(false); return serviceArea; } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java index 754b37d46..14d55acc7 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java @@ -17,17 +17,14 @@ import uk.gov.hmcts.reform.fact.data.api.entities.CourtFacilities; import uk.gov.hmcts.reform.fact.data.api.entities.CourtOpeningHours; import uk.gov.hmcts.reform.fact.data.api.entities.CourtPhoto; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtServiceAreas; import uk.gov.hmcts.reform.fact.data.api.entities.CourtTranslation; import uk.gov.hmcts.reform.fact.data.api.entities.CourtType; import uk.gov.hmcts.reform.fact.data.api.entities.LocalAuthorityType; import uk.gov.hmcts.reform.fact.data.api.entities.OpeningHourType; import uk.gov.hmcts.reform.fact.data.api.entities.Region; -import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.entities.types.AddressType; import uk.gov.hmcts.reform.fact.data.api.entities.types.AllowedLocalAuthorityAreasOfLaw; -import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.entities.types.DayOfTheWeek; import uk.gov.hmcts.reform.fact.data.api.entities.types.HearingEnhancementEquipment; import uk.gov.hmcts.reform.fact.data.api.entities.types.OpeningTimesDetail; @@ -39,12 +36,10 @@ import uk.gov.hmcts.reform.fact.data.api.repositories.AreaOfLawTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.ContactDescriptionTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtPhotoRepository; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtServiceAreasRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.LocalAuthorityTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.OpeningHoursTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.RegionRepository; -import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; import java.awt.AlphaComposite; import java.awt.Color; @@ -99,7 +94,6 @@ public class TestingSupportService { private static final List CONTACT_DESCRIPTION_IDS = new ArrayList<>(); private static final List LOCAL_AUTHORITY_TYPE_IDS = new ArrayList<>(); private static final List OPENING_HOUR_TYPE_IDS = new ArrayList<>(); - private static final List SERVICE_AREA_IDS = new ArrayList<>(); // ------------------------------------------------------------------------ // Data variations for randomisation @@ -182,7 +176,6 @@ public class TestingSupportService { private final CourtTranslationService courtTranslationService; private final CourtProfessionalInformationService courtProfessionalInformationService; private final CourtLocalAuthoritiesService courtLocalAuthoritiesService; - private final CourtServiceAreasRepository courtServiceAreasRepository; // for photos, we try the service first, then fall back to the repository if required private final CourtPhotoService courtPhotoService; private final CourtPhotoRepository courtPhotoRepository; @@ -198,7 +191,6 @@ public class TestingSupportService { private final LocalAuthorityTypeRepository localAuthorityTypeRepository; private final OpeningHoursTypeRepository openingHoursTypeRepository; private final RegionRepository regionRepository; - private final ServiceAreaRepository serviceAreaRepository; /** * Creates a court with the specified name and randomised data for all other fields. The randomisation is based on @@ -206,29 +198,26 @@ public class TestingSupportService { * * @param courtName the name of the court to create * @param seed the seed for randomisation - if not provided, a random seed will be used - * @param serviceCentre whether the court should be marked as a service centre (true/false) * @return the slug of the created court entry **/ public String createCourt( @NonNull String courtName, Long seed, - boolean serviceCentre, boolean open, boolean addWarningNotice ) { - return createCourt(courtName, null, seed, serviceCentre, open, addWarningNotice, true, true, false, false); + return createCourt(courtName, null, seed, open, addWarningNotice, true, true, false); } public String createCourt( @NonNull String courtName, Long seed, - boolean serviceCentre, boolean open, boolean addWarningNotice, boolean withTranslations ) { - return createCourt(courtName, null, seed, serviceCentre, open, addWarningNotice, withTranslations, true, - false, false); + return createCourt(courtName, null, seed, open, addWarningNotice, withTranslations, true, + false); } // Suppressing the "too many params" warning for now as this is a test setup @@ -238,15 +227,14 @@ public String createCourt( public String createCourt( @NonNull String courtName, Long seed, - boolean serviceCentre, boolean open, boolean addWarningNotice, boolean withTranslations, boolean withEnquiriesContact, - boolean associateServiceAreas + boolean forceFamilyCourt ) { - return createCourt(courtName, null, seed, serviceCentre, open, addWarningNotice, withTranslations, - withEnquiriesContact, associateServiceAreas, false); + return createCourt(courtName, null, seed, open, addWarningNotice, withTranslations, + withEnquiriesContact, forceFamilyCourt); } // Suppressing the "too many params" warning for now as this is a test setup @@ -257,12 +245,10 @@ public String createCourt( @NonNull String courtName, UUID regionId, Long seed, - boolean serviceCentre, boolean open, boolean addWarningNotice, boolean withTranslations, boolean withEnquiriesContact, - boolean associateServiceAreas, boolean forceFamilyCourt ) { try { @@ -271,7 +257,7 @@ public String createCourt( Random random = new Random(Optional.ofNullable(seed).orElse(System.currentTimeMillis())); - Court court = createCourt(courtName, regionId, serviceCentre, addWarningNotice, random); + Court court = createCourt(courtName, regionId, addWarningNotice, random); UUID courtId = court.getId(); List areasOfLaw = setAreasOfLaw(courtId, random); List courtTypes = COURT_TYPES.stream().filter(l -> random.nextBoolean()).toList(); @@ -290,9 +276,6 @@ public String createCourt( setLocalAuthorities(courtId, areasOfLaw, random); setOpeningHours(courtId, random); setProfessionalInformation(courtId, forceFamilyCourt, random); - if (associateServiceAreas) { - setServiceAreas(courtId, random); - } setSinglePointsOfEntry(courtId, areasOfLaw, random); setTranslations(courtId, random, withTranslations); setPhotos(courtId, courtName); @@ -307,12 +290,10 @@ public String createCourt( private Court createCourt(String name, UUID regionId, - boolean serviceCenter, boolean addWarningNotice, Random random) { Court court = Court.builder() .name(name) - .isServiceCentre(serviceCenter) .regionId(Optional.ofNullable(regionId).orElseGet(() -> REGION_IDS.get(random.nextInt(REGION_IDS.size())))) .mrdId(rndAlphaNumeric(random.nextInt(16, 32), random)) .open(false) @@ -358,11 +339,9 @@ private void initialiseCaches() { OPENING_HOUR_TYPE_IDS.addAll(openingHoursTypeRepository.findAll().stream().map( OpeningHourType::getId).toList() ); - SERVICE_AREA_IDS.addAll(serviceAreaRepository.findAll().stream().map(ServiceArea::getId).toList()); - if (REGION_IDS.isEmpty() || AREAS_OF_LAW.isEmpty() || COURT_TYPES.isEmpty() || CONTACT_DESCRIPTION_IDS.isEmpty() || LOCAL_AUTHORITY_TYPE_IDS.isEmpty() - || OPENING_HOUR_TYPE_IDS.isEmpty() || SERVICE_AREA_IDS.isEmpty()) { + || OPENING_HOUR_TYPE_IDS.isEmpty()) { throw new IllegalStateException("Unable to initialize testing support service caches " + "- one or more required reference data tables are empty"); } @@ -681,22 +660,6 @@ private List createFaxNumbers(final Random random) { return faxNumbers; } - private void setServiceAreas(final UUID courtId, final Random random) { - - List serviceAreaIds = SERVICE_AREA_IDS.stream().filter(id -> random.nextBoolean()).toList(); - if (serviceAreaIds.isEmpty()) { - serviceAreaIds = List.of(SERVICE_AREA_IDS.get(random.nextInt(SERVICE_AREA_IDS.size()))); - } - - CourtServiceAreas serviceAreas = CourtServiceAreas.builder() - .courtId(courtId) - .serviceAreaId(serviceAreaIds) - .catchmentType(CatchmentType.values()[random.nextInt(CatchmentType.values().length)]) - .build(); - - courtServiceAreasRepository.save(serviceAreas); - } - private void setSinglePointsOfEntry(final UUID courtId, final List areasOfLaw, final Random random) { if (random.nextBoolean()) { List allowedAols = Arrays.stream(AllowedLocalAuthorityAreasOfLaw.values()) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchCourtService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchCourtService.java index 8404c3270..d6fa7b5b1 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchCourtService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchCourtService.java @@ -12,7 +12,6 @@ import uk.gov.hmcts.reform.fact.data.api.os.OsLocationData; import uk.gov.hmcts.reform.fact.data.api.repositories.LocalAuthorityTypeRepository; import uk.gov.hmcts.reform.fact.data.api.services.CourtAddressService; -import uk.gov.hmcts.reform.fact.data.api.services.CourtServiceAreaService; import uk.gov.hmcts.reform.fact.data.api.services.CourtSinglePointOfEntryService; import uk.gov.hmcts.reform.fact.data.api.services.OsService; import uk.gov.hmcts.reform.fact.data.api.services.ServiceAreaService; @@ -22,12 +21,10 @@ import java.util.Optional; import static uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentMethod.LOCAL_AUTHORITY; -import static uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType.REGIONAL; import static uk.gov.hmcts.reform.fact.data.api.entities.types.SearchAction.NEAREST; import static uk.gov.hmcts.reform.fact.data.api.entities.types.SearchStrategy.CIVIL_POSTCODE_PREFERENCE; import static uk.gov.hmcts.reform.fact.data.api.entities.types.SearchStrategy.DEFAULT_AOL_DISTANCE; import static uk.gov.hmcts.reform.fact.data.api.entities.types.SearchStrategy.FAMILY_NON_REGIONAL; -import static uk.gov.hmcts.reform.fact.data.api.entities.types.SearchStrategy.FAMILY_REGIONAL; @Service @Slf4j @@ -36,7 +33,6 @@ public class SearchCourtService { private final OsService osService; private final ServiceAreaService serviceAreaService; private final CourtSinglePointOfEntryService courtSinglePointOfEntryService; - private final CourtServiceAreaService courtServiceAreaService; private final CourtAddressService courtAddressService; private final SearchExecuter searchExecuter; private final LocalAuthorityTypeRepository localAuthorityTypeRepository; @@ -46,14 +42,12 @@ public class SearchCourtService { public SearchCourtService(OsService osService, ServiceAreaService serviceAreaService, CourtSinglePointOfEntryService courtSinglePointOfEntryService, - CourtServiceAreaService courtServiceAreaService, CourtAddressService courtAddressService, SearchExecuter searchExecuter, LocalAuthorityTypeRepository localAuthorityTypeRepository) { this.osService = osService; this.serviceAreaService = serviceAreaService; this.courtSinglePointOfEntryService = courtSinglePointOfEntryService; - this.courtServiceAreaService = courtServiceAreaService; this.courtAddressService = courtAddressService; this.searchExecuter = searchExecuter; this.localAuthorityTypeRepository = localAuthorityTypeRepository; @@ -182,10 +176,8 @@ public SearchStrategy selectSearchStrategy(SearchAction action, private SearchStrategy getFamilyStrategy(SearchAction action, ServiceArea serviceArea, String authorityName) { if (LOCAL_AUTHORITY.equals(serviceArea.getCatchmentMethod()) && !authorityName.isEmpty()) { - return courtServiceAreaService.findByServiceAreaId(serviceArea.getId()) - .stream() - .anyMatch(courtService -> courtService.getCatchmentType().equals(REGIONAL)) - ? FAMILY_REGIONAL : FAMILY_NON_REGIONAL; + //TODO: Restore regional service-centre routing when service-centres are reintroduced. + return FAMILY_NON_REGIONAL; } log.debug("Setting search strategy to default for {}, {}, {}", action, serviceArea, authorityName); diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuter.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuter.java index f4a57ed50..af4a2a16f 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuter.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuter.java @@ -55,14 +55,6 @@ public List executeSearchStrategy(OsLocationData osLocationDa limit, aolId ); - case FAMILY_REGIONAL -> executeFamilyRegionalSearchStrategy( - serviceArea, - osLocationData, - lat, - lon, - limit, - aolId - ); case FAMILY_NON_REGIONAL -> { List results = executeFamilyNonRegionalSearchStrategy( osLocationData, @@ -121,46 +113,6 @@ private List executeCivilSearchStrategy(String postcode, doub : courtAddressRepository.findNearestByAreaOfLaw(lat, lon, aolId, limit); } - /** - * Execute the family regional search strategy according to the following rules: - * One of the court service areas need to match a regional service area by ID. - * The area of law needs to be FAMILY. - * The catchment also needs to be local-authority. - * - * @param serviceArea The service area - * @param osLocationData The ordnance survey location data - * @param lat the latitude - * @param lon the longitude - * @param limit the limit - * @param aolId the area of law id - * @return a list of court with distance objects - */ - private List executeFamilyRegionalSearchStrategy(ServiceArea serviceArea, - OsLocationData osLocationData, double lat, - double lon, int limit, UUID aolId) { - UUID serviceAreaId = serviceArea.getId(); - return getAuthorityID(osLocationData) - .map(LocalAuthorityType::getId) - .map(localAuthorityId -> courtAddressRepository.findFamilyRegionalByLocalAuthority( - serviceAreaId, - lat, - lon, - aolId, - localAuthorityId - )) - .filter(list -> !list.isEmpty()) - .orElseGet(() -> { - log.debug("Finding family regional for {}", serviceArea); - List byAol = - courtAddressRepository.findFamilyRegionalByAol(serviceAreaId, lat, lon, aolId); - if (!byAol.isEmpty()) { - return byAol; - } - log.debug("Finding family regional fallback for {}", serviceArea); - return courtAddressRepository.findNearestByAreaOfLaw(lat, lon, aolId, limit); - }); - } - /** * Execute the non-regional search strategy. * This will perform a search that requires the local authority to be present. diff --git a/src/main/resources/db/migration/V1.27__drop_service_centre_data_structures.sql b/src/main/resources/db/migration/V1.27__drop_service_centre_data_structures.sql new file mode 100644 index 000000000..73831ba19 --- /dev/null +++ b/src/main/resources/db/migration/V1.27__drop_service_centre_data_structures.sql @@ -0,0 +1,9 @@ +ALTER TABLE court DROP COLUMN IF EXISTS is_service_centre; + +DROP TRIGGER IF EXISTS court_service_areas_court_update_last_updated_at_trg ON court_service_areas; + +DROP TABLE IF EXISTS court_service_areas CASCADE; + +DELETE FROM region +WHERE name = 'Service Centre' + AND country = 'England'; diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListenerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListenerTest.java index e5ab44071..51c345156 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListenerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListenerTest.java @@ -147,7 +147,6 @@ private Court createCourt() { .slug("test-court") .open(Boolean.FALSE) .regionId(UUID.randomUUID()) - .isServiceCentre(Boolean.FALSE) .build(); } } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java index 225c38470..801018bb6 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java @@ -309,7 +309,6 @@ private static void populateCourt(final AbstractCourtEntity court) { court.setSlug("test-court"); court.setOpen(Boolean.TRUE); court.setRegionId(UUID.randomUUID()); - court.setIsServiceCentre(Boolean.TRUE); court.setOpenOnCath(Boolean.TRUE); court.setWarningNotice("Warning notice"); court.setMrdId("MRD123"); diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java index e8aa95b08..96fe5a554 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java @@ -83,17 +83,13 @@ void createSampleCourtPassesTranslationOverride() { anyBoolean(), anyBoolean(), anyBoolean(), - anyBoolean(), - anyBoolean(), anyBoolean() )) .thenReturn("test-court"); - testingSupportController.createSampleCourt("Test Court", null, 1L, false, true, false, false, false, - false, false); + testingSupportController.createSampleCourt("Test Court", null, 1L, true, false, false, false, false); - verify(testingSupportService).createCourt("Test Court", null, 1L, false, true, false, false, false, - false, false); + verify(testingSupportService).createCourt("Test Court", null, 1L, true, false, false, false, false); } @Test @@ -107,16 +103,12 @@ void createSampleCourtPassesRegionIdOverride() { anyBoolean(), anyBoolean(), anyBoolean(), - anyBoolean(), - anyBoolean(), anyBoolean() )) .thenReturn("test-court"); - testingSupportController.createSampleCourt("Test Court", regionId, 1L, false, true, false, false, false, - false, false); + testingSupportController.createSampleCourt("Test Court", regionId, 1L, true, false, false, false, false); - verify(testingSupportService).createCourt("Test Court", regionId, 1L, false, true, false, false, false, - false, false); + verify(testingSupportService).createCourt("Test Court", regionId, 1L, true, false, false, false, false); } } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaControllerTest.java deleted file mode 100644 index 8b333db0a..000000000 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaControllerTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package uk.gov.hmcts.reform.fact.data.api.controllers.search; - -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 org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtServiceAreas; -import uk.gov.hmcts.reform.fact.data.api.services.CourtServiceAreaService; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class SearchServiceAreaControllerTest { - - @Mock - private CourtServiceAreaService courtServiceAreaService; - - @InjectMocks - private SearchServiceAreaController controller; - - @Test - void getServiceAreaByNameShouldReturnOk() { - CourtServiceAreas courtServiceAreas = new CourtServiceAreas(); - List results = List.of(courtServiceAreas); - - when(courtServiceAreaService.findByServiceAreaName("Money Claims")) - .thenReturn(results); - - ResponseEntity> response = controller.getServiceAreaByName("Money Claims"); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isEqualTo(results); - verify(courtServiceAreaService).findByServiceAreaName("Money Claims"); - } -} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/CourtMigrationHelperTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/CourtMigrationHelperTest.java index 351f277a5..916764f9b 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/CourtMigrationHelperTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/CourtMigrationHelperTest.java @@ -7,7 +7,6 @@ import static org.mockito.Mockito.when; import java.util.List; -import java.util.Optional; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -21,7 +20,6 @@ import uk.gov.hmcts.reform.fact.data.api.entities.CourtDxCode; import uk.gov.hmcts.reform.fact.data.api.entities.CourtLocalAuthorities; import uk.gov.hmcts.reform.fact.data.api.entities.CourtProfessionalInformation; -import uk.gov.hmcts.reform.fact.data.api.entities.Region; import uk.gov.hmcts.reform.fact.data.api.entities.types.AllowedLocalAuthorityAreasOfLaw; import uk.gov.hmcts.reform.fact.data.api.migration.model.CourtAreasOfLawDto; import uk.gov.hmcts.reform.fact.data.api.migration.model.CourtDto; @@ -37,17 +35,13 @@ import uk.gov.hmcts.reform.fact.data.api.repositories.CourtFaxRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtLocalAuthoritiesRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtProfessionalInformationRepository; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtServiceAreasRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtSinglePointsOfEntryRepository; -import uk.gov.hmcts.reform.fact.data.api.repositories.RegionRepository; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; import uk.gov.hmcts.reform.fact.data.api.migration.repository.LegacyCourtMappingRepository; @ExtendWith(MockitoExtension.class) class CourtMigrationHelperTest { - @Mock private RegionRepository regionRepository; - @Mock private CourtServiceAreasRepository courtServiceAreasRepository; @Mock private CourtAreasOfLawRepository courtAreasOfLawRepository; @Mock private CourtSinglePointsOfEntryRepository courtSinglePointsOfEntryRepository; @Mock private CourtLocalAuthoritiesRepository courtLocalAuthoritiesRepository; @@ -65,8 +59,6 @@ class CourtMigrationHelperTest { @BeforeEach void setUp() { helper = new CourtMigrationHelper( - regionRepository, - courtServiceAreasRepository, courtAreasOfLawRepository, courtSinglePointsOfEntryRepository, courtLocalAuthoritiesRepository, @@ -98,7 +90,6 @@ void shouldPersistCourtRelationshipsAndIncrementCounters() { context.getRegionIds().put(1, regionId); context.getAreaOfLawIds().put(10, childrenAolID); - context.getServiceAreaIds().put(100, UUID.randomUUID()); context.getLocalAuthorityTypeIds().put(20, List.of(UUID.randomUUID())); when(courtService.createCourt(any(Court.class))).thenAnswer(invocation -> { Court court = invocation.getArgument(0); @@ -120,23 +111,13 @@ void shouldPersistCourtRelationshipsAndIncrementCounters() { int migrated = helper.migrateCourts(List.of(courtDto), context); assertThat(migrated).isEqualTo(1); - assertThat(context.getCourtServiceAreasMigrated()).isEqualTo(1); assertThat(context.getCourtAreasOfLawMigrated()).isEqualTo(1); assertThat(context.getCourtLocalAuthoritiesMigrated()).isEqualTo(1); assertThat(context.getCourtSinglePointsOfEntryMigrated()).isEqualTo(1); } @Test - void shouldUseServiceCentreFallbackRegion() { - UUID fallbackRegion = UUID.randomUUID(); - when(regionRepository.findByNameAndCountry("Service Centre", "England")) - .thenReturn(Optional.of(Region.builder().id(fallbackRegion).build())); - when(courtService.createCourt(any(Court.class))).thenAnswer(invocation -> { - Court court = invocation.getArgument(0); - court.setId(UUID.randomUUID()); - return court; - }); - + void shouldSkipServiceCentre() { CourtDto serviceCentre = new CourtDto(); serviceCentre.setId(600L); serviceCentre.setName("Service Centre Court"); @@ -145,8 +126,8 @@ void shouldUseServiceCentreFallbackRegion() { serviceCentre.setIsServiceCentre(true); int migrated = helper.migrateCourts(List.of(serviceCentre), context); - assertThat(migrated).isEqualTo(1); - assertThat(context.getServiceCentreRegionId()).isEqualTo(fallbackRegion); + assertThat(migrated).isZero(); + verify(courtService, never()).createCourt(any(Court.class)); } @Test diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationServiceTest.java index 0c134438d..46e33ce3e 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationServiceTest.java @@ -60,7 +60,6 @@ import uk.gov.hmcts.reform.fact.data.api.repositories.CourtLocalAuthoritiesRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtProfessionalInformationRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtRepository; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtServiceAreasRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtSinglePointsOfEntryRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.LocalAuthorityTypeRepository; @@ -110,8 +109,6 @@ class MigrationServiceTest { @Mock private CourtRepository courtRepository; @Mock - private CourtServiceAreasRepository courtServiceAreasRepository; - @Mock private CourtAreasOfLawRepository courtAreasOfLawRepository; @Mock private CourtSinglePointsOfEntryRepository courtSinglePointsOfEntryRepository; @@ -261,7 +258,7 @@ void shouldPersistLegacyDataAndReturnSummary() { assertThat(result.getCourtsMigrated()).isEqualTo(1); assertThat(result.getCourtAreasOfLawMigrated()).isEqualTo(1); - assertThat(result.getCourtServiceAreasMigrated()).isEqualTo(1); + assertThat(result.getCourtServiceAreasMigrated()).isZero(); assertThat(result.getCourtLocalAuthoritiesMigrated()).isEqualTo(1); assertThat(result.getCourtSinglePointsOfEntryMigrated()).isEqualTo(1); assertThat(result.getCourtProfessionalInformationMigrated()).isEqualTo(1); @@ -270,7 +267,6 @@ void shouldPersistLegacyDataAndReturnSummary() { assertThat(result.getCourtFaxMigrated()).isEqualTo(1); verify(legacyServiceRepository).save(any(LegacyService.class)); - verify(courtServiceAreasRepository).save(any()); verify(courtAreasOfLawRepository).save(any()); verify(courtSinglePointsOfEntryRepository).save(any()); verify(courtLocalAuthoritiesRepository).save(any()); @@ -310,7 +306,7 @@ void shouldSkipCourtDxCodesWhenDetailsMissing() { } @Test - void shouldAssignFallbackRegionToServiceCentresWithoutRegion() { + void shouldSkipServiceCentres() { CourtDto serviceCentre = new CourtDto( 999L, "Service Centre Court", @@ -347,11 +343,8 @@ void shouldAssignFallbackRegionToServiceCentresWithoutRegion() { MigrationSummary summary = migrationService.migrate(); - assertThat(summary.getResult().getCourtsMigrated()).isEqualTo(1); - ArgumentCaptor captor = ArgumentCaptor.forClass(Court.class); - verify(courtService).createCourt(captor.capture()); - assertThat(captor.getValue().getRegionId()).isNotNull(); - verify(regionRepository).findByNameAndCountry("Service Centre", "England"); + assertThat(summary.getResult().getCourtsMigrated()).isZero(); + verify(courtService, never()).createCourt(any(Court.class)); } private CourtDto courtDto() { @@ -370,7 +363,7 @@ private CourtDto courtDto() { List.of(new CourtDxCodeDto("dx", "123", "dx explanation")), List.of(new CourtFaxDto("fax", "01632960000")), null, - true + false ); } @@ -419,7 +412,7 @@ private CourtDto courtDtoWithEmptyDxCode() { List.of(new CourtDxCodeDto("dx", null, null)), List.of(new CourtFaxDto("fax", "01632960000")), null, - true + false ); } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceAreaServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceAreaServiceTest.java deleted file mode 100644 index b6b340f00..000000000 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceAreaServiceTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package uk.gov.hmcts.reform.fact.data.api.services; - -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.reform.fact.data.api.entities.CourtServiceAreas; -import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtRepository; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtServiceAreasRepository; - -import java.util.List; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class CourtServiceAreaServiceTest { - - @Mock - private CourtServiceAreasRepository courtServiceAreasRepository; - - @Mock - private CourtRepository courtRepository; - - @Mock - private ServiceAreaService serviceAreaService; - - @InjectMocks - private CourtServiceAreaService courtServiceAreaService; - - @Test - void findByServiceAreaIdShouldReturnResults() { - UUID serviceAreaId = UUID.randomUUID(); - List results = List.of(new CourtServiceAreas()); - when(courtServiceAreasRepository.findByServiceAreaId(serviceAreaId)).thenReturn(results); - - List response = courtServiceAreaService.findByServiceAreaId(serviceAreaId); - - assertThat(response).isEqualTo(results); - verify(courtServiceAreasRepository).findByServiceAreaId(serviceAreaId); - } - - @Test - void findByServiceAreaNameShouldResolveServiceAreaAndReturnResults() { - UUID serviceAreaId = UUID.randomUUID(); - ServiceArea area = new ServiceArea(); - area.setId(serviceAreaId); - List results = List.of(new CourtServiceAreas()); - - when(serviceAreaService.getServiceAreaByName("Money Claims")).thenReturn(area); - when(courtServiceAreasRepository.findByServiceAreaId(serviceAreaId)).thenReturn(results); - - List response = courtServiceAreaService.findByServiceAreaName("Money Claims"); - - assertThat(response).isEqualTo(results); - verify(serviceAreaService).getServiceAreaByName("Money Claims"); - verify(courtServiceAreasRepository).findByServiceAreaId(serviceAreaId); - } - - @Test - void findByServiceAreaIdShouldEnrichCourtServiceAreasWithNameAndSlug() { - UUID serviceAreaId = UUID.randomUUID(); - UUID courtId = UUID.randomUUID(); - CourtServiceAreas csa = new CourtServiceAreas(); - csa.setCourtId(courtId); - List results = List.of(csa); - when(courtServiceAreasRepository.findByServiceAreaId(serviceAreaId)).thenReturn(results); - when(courtRepository.findNameAndSlugById(courtId)) - .thenReturn(java.util.Optional.of(new CourtRepository.NameAndSlug("Court Name", "court-slug"))); - - List response = courtServiceAreaService.findByServiceAreaId(serviceAreaId); - - assertThat(response.getFirst().getCourtName()).isEqualTo("Court Name"); - assertThat(response.getFirst().getCourtSlug()).isEqualTo("court-slug"); - verify(courtRepository).findNameAndSlugById(courtId); - } - - @Test - void findByServiceAreaNameShouldEnrichCourtServiceAreasWithNameAndSlug() { - UUID serviceAreaId = UUID.randomUUID(); - UUID courtId = UUID.randomUUID(); - ServiceArea area = new ServiceArea(); - area.setId(serviceAreaId); - CourtServiceAreas csa = new CourtServiceAreas(); - csa.setCourtId(courtId); - List results = List.of(csa); - when(serviceAreaService.getServiceAreaByName("Money Claims")).thenReturn(area); - when(courtServiceAreasRepository.findByServiceAreaId(serviceAreaId)).thenReturn(results); - when(courtRepository.findNameAndSlugById(courtId)) - .thenReturn(java.util.Optional.of(new CourtRepository.NameAndSlug("Court Name", "court-slug"))); - - List response = courtServiceAreaService.findByServiceAreaName("Money Claims"); - - assertThat(response.getFirst().getCourtName()).isEqualTo("Court Name"); - assertThat(response.getFirst().getCourtSlug()).isEqualTo("court-slug"); - verify(courtRepository).findNameAndSlugById(courtId); - } - - @Test - void enrichmentShouldHandleMissingCourtNameAndSlug() { - UUID serviceAreaId = UUID.randomUUID(); - UUID courtId = UUID.randomUUID(); - CourtServiceAreas csa = new CourtServiceAreas(); - csa.setCourtId(courtId); - List results = List.of(csa); - when(courtServiceAreasRepository.findByServiceAreaId(serviceAreaId)).thenReturn(results); - when(courtRepository.findNameAndSlugById(courtId)).thenReturn(java.util.Optional.empty()); - - List response = courtServiceAreaService.findByServiceAreaId(serviceAreaId); - - assertThat(response.getFirst().getCourtName()).isNull(); - assertThat(response.getFirst().getCourtSlug()).isNull(); - verify(courtRepository).findNameAndSlugById(courtId); - } -} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/SearchCourtServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/SearchCourtServiceTest.java index 3bdc85eef..0a0ccd5be 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/SearchCourtServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/SearchCourtServiceTest.java @@ -8,11 +8,9 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import uk.gov.hmcts.reform.fact.data.api.dto.CourtWithDistance; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtServiceAreas; import uk.gov.hmcts.reform.fact.data.api.entities.LocalAuthorityType; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentMethod; -import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchAction; import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchStrategy; import uk.gov.hmcts.reform.fact.data.api.entities.types.ServiceAreaType; @@ -55,9 +53,6 @@ class SearchCourtServiceTest { @Mock private CourtSinglePointOfEntryService courtSinglePointOfEntryService; - @Mock - private CourtServiceAreaService courtServiceAreaService; - @Mock private CourtAddressService courtAddressService; @@ -177,33 +172,9 @@ void selectSearchStrategyShouldReturnCivilPreference() { } @Test - void selectSearchStrategyShouldReturnFamilyRegionalWhenRegionalCatchmentExists() { - ServiceArea area = serviceAreaWithType(ServiceAreaType.FAMILY); - area.setCatchmentMethod(CatchmentMethod.LOCAL_AUTHORITY); - CourtServiceAreas courtServiceAreas = new CourtServiceAreas(); - courtServiceAreas.setCatchmentType(CatchmentType.REGIONAL); - - when(courtServiceAreaService.findByServiceAreaId(area.getId())) - .thenReturn(List.of(courtServiceAreas)); - - SearchStrategy strategy = searchCourtService.selectSearchStrategy( - SearchAction.DOCUMENTS, - "Authority", - area - ); - - assertThat(strategy).isEqualTo(SearchStrategy.FAMILY_REGIONAL); - } - - @Test - void selectSearchStrategyShouldReturnFamilyNonRegionalWhenNoRegionalCatchment() { + void selectSearchStrategyShouldReturnFamilyNonRegionalForLocalAuthorityCatchment() { ServiceArea area = serviceAreaWithType(ServiceAreaType.FAMILY); area.setCatchmentMethod(CatchmentMethod.LOCAL_AUTHORITY); - CourtServiceAreas courtServiceAreas = new CourtServiceAreas(); - courtServiceAreas.setCatchmentType(CatchmentType.LOCAL); - - when(courtServiceAreaService.findByServiceAreaId(area.getId())) - .thenReturn(List.of(courtServiceAreas)); SearchStrategy strategy = searchCourtService.selectSearchStrategy( SearchAction.DOCUMENTS, diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaServiceTest.java index 1835b106e..8f5c7679f 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaServiceTest.java @@ -7,7 +7,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtServiceAreasRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; import java.util.List; @@ -24,9 +23,6 @@ class ServiceAreaServiceTest { @Mock private ServiceAreaRepository serviceAreaRepository; - @Mock - private CourtServiceAreasRepository courtServiceAreasRepository; - @InjectMocks private ServiceAreaService serviceAreaService; diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportServiceTest.java index 957b264d1..eef35fe82 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportServiceTest.java @@ -27,13 +27,11 @@ import uk.gov.hmcts.reform.fact.data.api.entities.CourtFacilities; import uk.gov.hmcts.reform.fact.data.api.entities.CourtOpeningHours; import uk.gov.hmcts.reform.fact.data.api.entities.CourtPhoto; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtServiceAreas; import uk.gov.hmcts.reform.fact.data.api.entities.CourtTranslation; import uk.gov.hmcts.reform.fact.data.api.entities.CourtType; import uk.gov.hmcts.reform.fact.data.api.entities.LocalAuthorityType; import uk.gov.hmcts.reform.fact.data.api.entities.OpeningHourType; import uk.gov.hmcts.reform.fact.data.api.entities.Region; -import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.entities.types.AllowedLocalAuthorityAreasOfLaw; import uk.gov.hmcts.reform.fact.data.api.models.AreaOfLawSelectionDto; @@ -41,12 +39,10 @@ import uk.gov.hmcts.reform.fact.data.api.repositories.AreaOfLawTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.ContactDescriptionTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtPhotoRepository; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtServiceAreasRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.LocalAuthorityTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.OpeningHoursTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.RegionRepository; -import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; import java.util.List; import java.util.Optional; @@ -89,8 +85,6 @@ class TestingSupportServiceTest { @Mock private CourtLocalAuthoritiesService courtLocalAuthoritiesService; @Mock - private CourtServiceAreasRepository courtServiceAreasRepository; - @Mock private CourtPhotoService courtPhotoService; @Mock private CourtPhotoRepository courtPhotoRepository; @@ -110,9 +104,6 @@ class TestingSupportServiceTest { private OpeningHoursTypeRepository openingHoursTypeRepository; @Mock private RegionRepository regionRepository; - @Mock - private ServiceAreaRepository serviceAreaRepository; - @InjectMocks private TestingSupportService testingSupportService; @@ -121,12 +112,10 @@ class TestingSupportServiceTest { private final List areasOfLaw = randomAreasOfLaw(); private final UUID auditUserId = UUID.randomUUID(); - private final List courtTypes = randomCourtTypes(); private final List contactDescIds = randomUuidList(); private final List localAuthorityIds = randomUuidList(); private final List openingHourTypeIds = randomUuidList(); - private final List serviceAreaIds = randomUuidList(); private static List randomAreasOfLaw() { Random random = new Random(); @@ -202,10 +191,6 @@ void setup() { lenient().when(openingHoursTypeRepository.findAll()).thenReturn( openingHourTypeIds.stream().map(id -> OpeningHourType.builder().id(id).build()).toList() ); - // ServiceAreaRepository - lenient().when(serviceAreaRepository.findAll()).thenReturn( - serviceAreaIds.stream().map(id -> ServiceArea.builder().id(id).build()).toList() - ); lenient().when(auditUserContext.getUserId()).thenReturn(Optional.of(auditUserId)); lenient().when(auditUserContext.requireUserId()).thenReturn(auditUserId); lenient().when(userService.createOrUpdateLastLoginUser(any(User.class))).thenAnswer(inv -> { @@ -221,10 +206,11 @@ void createCourtWithValidName() { when(courtService.createCourt(any())).thenAnswer(inv -> { Court c = Court.class.cast(inv.getArguments()[0]); c.setSlug("test-court"); + c.setId(UUID.randomUUID()); return c; }); - String result = testingSupportService.createCourt(courtName, null, false, false, true); + String result = testingSupportService.createCourt(courtName, null, false, true); assertNotNull(result); assertEquals("test-court", result); @@ -244,7 +230,6 @@ void createCourtWithValidName() { verify(courtAddressService, atLeast(1)).createAddress(any(), any()); // things that are optional, but have a ceiling - verify(courtServiceAreasRepository, atMost(1)).save(any()); verify(courtLocalAuthoritiesService, atMost(1)).setCourtLocalAuthorities(any(), any()); verify(courtTranslationService, times(1)).setTranslation(any(), any()); verify(courtPhotoRepository, atMost(1)).save(any()); @@ -258,10 +243,11 @@ void createCourtWithWarningNotice() { when(courtService.createCourt(any())).thenAnswer(inv -> { Court c = Court.class.cast(inv.getArguments()[0]); c.setSlug("test-court"); + c.setId(UUID.randomUUID()); return c; }); - String result = testingSupportService.createCourt(courtName, null, false, false, true); + String result = testingSupportService.createCourt(courtName, null, false, true); assertNotNull(result); assertEquals("test-court", result); @@ -277,10 +263,11 @@ void createCourtWithoutWarningNotice() { when(courtService.createCourt(any())).thenAnswer(inv -> { Court c = Court.class.cast(inv.getArguments()[0]); c.setSlug("test-court"); + c.setId(UUID.randomUUID()); return c; }); - String result = testingSupportService.createCourt(courtName, null, false, false, false); + String result = testingSupportService.createCourt(courtName, null, false, false); assertNotNull(result); assertEquals("test-court", result); @@ -297,11 +284,12 @@ void createCourtWithRegionIdUsesProvidedRegion() { when(courtService.createCourt(any())).thenAnswer(inv -> { Court c = Court.class.cast(inv.getArguments()[0]); c.setSlug("test-court"); + c.setId(UUID.randomUUID()); return c; }); - String result = testingSupportService.createCourt(courtName, regionId, null, false, false, true, - true, false, false, false); + String result = testingSupportService.createCourt(courtName, regionId, null, false, true, + true, false, false); assertNotNull(result); assertEquals("test-court", result); @@ -316,7 +304,7 @@ void createCourtWithEmptyNameThrowsException() { String courtName = ""; assertThrows( NullPointerException.class, - () -> testingSupportService.createCourt(courtName, null, false, false, true) + () -> testingSupportService.createCourt(courtName, null, false, true) ); } @@ -326,10 +314,11 @@ void createCourtWithoutTranslationsSkipsTranslationService() { when(courtService.createCourt(any())).thenAnswer(inv -> { Court c = Court.class.cast(inv.getArguments()[0]); c.setSlug("test-court"); + c.setId(UUID.randomUUID()); return c; }); - String result = testingSupportService.createCourt(courtName, null, false, false, true, false); + String result = testingSupportService.createCourt(courtName, null, false, true, false); assertNotNull(result); assertEquals("test-court", result); @@ -342,49 +331,17 @@ void createCourtWithoutEnquiriesContactSkipsContactCreation() { when(courtService.createCourt(any())).thenAnswer(inv -> { Court c = Court.class.cast(inv.getArguments()[0]); c.setSlug("test-court"); + c.setId(UUID.randomUUID()); return c; }); - String result = testingSupportService.createCourt(courtName, null, false, false, true, true, false, false); + String result = testingSupportService.createCourt(courtName, null, false, true, true, false, false); assertNotNull(result); assertEquals("test-court", result); verify(courtContactDetailsService, never()).createContactDetail(any(), any()); } - @Test - void createCourtWithoutAssociateServiceAreasSkipsServiceAreaAssociation() { - String courtName = "Test Court"; - when(courtService.createCourt(any())).thenAnswer(inv -> { - Court c = Court.class.cast(inv.getArguments()[0]); - c.setSlug("test-court"); - return c; - }); - - String result = testingSupportService.createCourt(courtName, null, false, false, true, true, false, false); - - assertNotNull(result); - assertEquals("test-court", result); - verify(courtServiceAreasRepository, never()).save(any()); - } - - @Test - void createCourtWithAssociateServiceAreasPerformsServiceAreaAssociation() { - String courtName = "Test Court"; - when(courtService.createCourt(any())).thenAnswer(inv -> { - Court c = Court.class.cast(inv.getArguments()[0]); - c.setSlug("test-court"); - return c; - }); - - String result = testingSupportService.createCourt(courtName, null, false, false, true, true, false, true); - - assertNotNull(result); - assertEquals("test-court", result); - verify(courtServiceAreasRepository, times(1)).save(any()); - } - - @Captor private ArgumentCaptor> aolSelectionDtoArgumentCaptor; @Captor @@ -404,7 +361,7 @@ void createCourtWithSeedGeneratesConsistentResults() { return c; }); - testingSupportService.createCourt(courtName, seed, false, false, true); + testingSupportService.createCourt(courtName, seed, false, true); // things that are always called once verify(courtService, times(1)).createCourt(any()); @@ -455,13 +412,8 @@ void createCourtWithSeedGeneratesConsistentResults() { verify(courtPhotoRepository, atMost(1)).save(courtPhotoArgumentCaptor.capture()); verify(courtSinglePointsOfEntryService, atMost(1)) .updateCourtSinglePointsOfEntry(eq(courtId), aolSelectionDtoArgumentCaptor.capture()); - ArgumentCaptor courtServiceAreasArgumentCaptor = - ArgumentCaptor.forClass(CourtServiceAreas.class); - verify(courtServiceAreasRepository, atMost(1)) - .save(courtServiceAreasArgumentCaptor.capture()); - // second call with same seed should generate same results - testingSupportService.createCourt(courtName, seed, false, false, true); + testingSupportService.createCourt(courtName, seed, false, true); verify(courtService, times(2)).createCourt(any()); @@ -513,9 +465,5 @@ void createCourtWithSeedGeneratesConsistentResults() { times(2) ).updateCourtSinglePointsOfEntry(courtId, v) ); - courtServiceAreasArgumentCaptor.getAllValues().forEach(v -> verify( - courtServiceAreasRepository, - times(2) - ).save(v)); } } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuterTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuterTest.java index 63875eb04..bd21c2487 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuterTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuterTest.java @@ -121,98 +121,6 @@ void executeSearchStrategyShouldFallbackToNearestWhenCivilTieredResultsEmpty() { verify(courtAddressRepository).findNearestByAreaOfLaw(51.5, -0.1, area.getAreaOfLawId(), 5); } - @Test - void executeSearchStrategyShouldReturnFamilyRegionalByLocalAuthorityWhenFound() { - ServiceArea area = serviceArea(ServiceAreaType.FAMILY); - OsLocationData locationData = osLocationData("Authority", "SW1A 1AA"); - LocalAuthorityType authorityType = localAuthorityType(UUID.randomUUID()); - List results = List.of(mock(CourtWithDistance.class)); - - when(localAuthorityTypeRepository.findIdByNameIgnoreCase("Authority")) - .thenReturn(Optional.of(authorityType)); - when(courtAddressRepository.findFamilyRegionalByLocalAuthority( - area.getId(), - 51.5, - -0.1, - area.getAreaOfLawId(), - authorityType.getId() - )).thenReturn(results); - - List response = searchExecuter.executeSearchStrategy( - locationData, - area, - SearchStrategy.FAMILY_REGIONAL, - SearchAction.DOCUMENTS, - 10 - ); - - assertThat(response).isEqualTo(results); - verify(courtAddressRepository, never()).findFamilyRegionalByAol(any(), anyDouble(), anyDouble(), any()); - } - - @Test - void executeSearchStrategyShouldFallbackToRegionalByAolWhenLocalAuthorityEmpty() { - ServiceArea area = serviceArea(ServiceAreaType.FAMILY); - OsLocationData locationData = osLocationData("Authority", "SW1A 1AA"); - LocalAuthorityType authorityType = localAuthorityType(UUID.randomUUID()); - List results = List.of(mock(CourtWithDistance.class)); - - when(localAuthorityTypeRepository.findIdByNameIgnoreCase("Authority")) - .thenReturn(Optional.of(authorityType)); - when(courtAddressRepository.findFamilyRegionalByLocalAuthority( - area.getId(), - 51.5, - -0.1, - area.getAreaOfLawId(), - authorityType.getId() - )).thenReturn(List.of()); - when(courtAddressRepository.findFamilyRegionalByAol(area.getId(), 51.5, -0.1, area.getAreaOfLawId())) - .thenReturn(results); - - List response = searchExecuter.executeSearchStrategy( - locationData, - area, - SearchStrategy.FAMILY_REGIONAL, - SearchAction.DOCUMENTS, - 10 - ); - - assertThat(response).isEqualTo(results); - verify(courtAddressRepository, never()).findNearestByAreaOfLaw(anyDouble(), anyDouble(), any(), anyInt()); - } - - @Test - void executeSearchStrategyShouldFallbackToNearestWhenRegionalResultsMissing() { - ServiceArea area = serviceArea(ServiceAreaType.FAMILY); - OsLocationData locationData = osLocationData("Authority", "SW1A 1AA"); - LocalAuthorityType authorityType = localAuthorityType(UUID.randomUUID()); - List results = List.of(mock(CourtWithDistance.class)); - - when(localAuthorityTypeRepository.findIdByNameIgnoreCase("Authority")) - .thenReturn(Optional.of(authorityType)); - when(courtAddressRepository.findFamilyRegionalByLocalAuthority( - area.getId(), - 51.5, - -0.1, - area.getAreaOfLawId(), - authorityType.getId() - )).thenReturn(List.of()); - when(courtAddressRepository.findFamilyRegionalByAol(area.getId(), 51.5, -0.1, area.getAreaOfLawId())) - .thenReturn(List.of()); - when(courtAddressRepository.findNearestByAreaOfLaw(51.5, -0.1, area.getAreaOfLawId(), 10)) - .thenReturn(results); - - List response = searchExecuter.executeSearchStrategy( - locationData, - area, - SearchStrategy.FAMILY_REGIONAL, - SearchAction.DOCUMENTS, - 10 - ); - - assertThat(response).isEqualTo(results); - } - @Test void executeSearchStrategyShouldReturnFamilyNonRegionalByLocalAuthorityWhenFound() { ServiceArea area = serviceArea(ServiceAreaType.FAMILY); From 1ddcff4c2d2fe2557d144ad9e92ba838c375db19 Mon Sep 17 00:00:00 2001 From: Josh <23316525+josh-uk@users.noreply.github.com> Date: Thu, 18 Jun 2026 09:29:40 +0100 Subject: [PATCH 02/40] FACT-2618 Add service centre entities and update audit (#273) * Add service centre entities and update audit * jsonignore --- .../fact/data/api/audit/AuditingTest.java | 38 ++++++ .../api/controllers/AuditControllerTest.java | 66 ++++++++++ .../audit/AuditableCourtEntityListener.java | 25 ++-- .../data/api/controllers/AuditController.java | 10 +- .../reform/fact/data/api/entities/Audit.java | 23 +++- .../api/entities/AuditableCourtEntity.java | 16 ++- .../data/api/entities/AuditableEntity.java | 16 +++ .../fact/data/api/entities/ServiceCentre.java | 119 ++++++++++++++++++ .../api/entities/ServiceCentreAddress.java | 110 ++++++++++++++++ .../api/entities/ServiceCentreAreasOfLaw.java | 86 +++++++++++++ .../entities/ServiceCentreContactDetails.java | 116 +++++++++++++++++ .../api/entities/types/AuditSubjectType.java | 6 + .../api/repositories/AuditRepository.java | 27 ++-- .../ServiceCentreAddressRepository.java | 10 ++ .../ServiceCentreAreasOfLawRepository.java | 10 ++ ...ServiceCentreContactDetailsRepository.java | 10 ++ .../repositories/ServiceCentreRepository.java | 10 ++ .../fact/data/api/services/AuditService.java | 51 +++++--- .../V1.28__create_service_centre_tables.sql | 64 ++++++++++ .../V1.29__generalise_audit_subject.sql | 12 ++ .../AuditableCourtEntityListenerTest.java | 3 + .../api/controllers/AuditControllerTest.java | 25 +++- .../data/api/services/AuditServiceTest.java | 89 ++++++++++--- 23 files changed, 881 insertions(+), 61 deletions(-) create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableEntity.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentre.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAddress.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreContactDetails.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubjectType.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAddressRepository.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAreasOfLawRepository.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreContactDetailsRepository.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java create mode 100644 src/main/resources/db/migration/V1.28__create_service_centre_tables.sql create mode 100644 src/main/resources/db/migration/V1.29__generalise_audit_subject.sql diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java index 4ea5d0c1e..3891496cb 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java @@ -8,14 +8,17 @@ import uk.gov.hmcts.reform.fact.data.api.entities.CourtPhoto; import uk.gov.hmcts.reform.fact.data.api.entities.CourtTranslation; import uk.gov.hmcts.reform.fact.data.api.entities.Region; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditActionType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.UserRole; import uk.gov.hmcts.reform.fact.data.api.repositories.AuditRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtPhotoRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtTranslationRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.RegionRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.UserRepository; import uk.gov.hmcts.reform.fact.data.api.services.AuditService; @@ -63,6 +66,9 @@ class AuditingTest { @Autowired CourtPhotoRepository courtPhotoRepository; + @Autowired + ServiceCentreRepository serviceCentreRepository; + @Autowired AuditRepository auditRepository; @@ -83,6 +89,7 @@ void setUp() { courtRepository.deleteAll(); courtPhotoRepository.deleteAll(); courtTranslationRepository.deleteAll(); + serviceCentreRepository.deleteAll(); // do this one last auditRepository.deleteAll(); @@ -120,6 +127,36 @@ void deletingExistingAuditRecordsViaServiceShouldWork() { assertEquals(0L, auditRepository.count()); } + @Test + @DisplayName("Creating and updating a ServiceCentre should create Audit records") + void creatingAndUpdatingAServiceCentreShouldCreateAuditRecords() { + ServiceCentre serviceCentre = ServiceCentre.builder() + .name("Test Service Centre") + .slug("test-service-centre") + .open(Boolean.FALSE) + .build(); + serviceCentreRepository.save(serviceCentre); + + serviceCentre.setOpen(Boolean.TRUE); + serviceCentreRepository.save(serviceCentre); + + Page audits = auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfter( + serviceCentre.getId(), + AuditSubjectType.SERVICE_CENTRE, + CREATED_AFTER.atStartOfDay().atZone(java.time.ZoneOffset.UTC), + org.springframework.data.domain.PageRequest.of(0, 1000) + ); + + List content = audits.getContent().stream().filter(audit -> Objects.equals( + audit.getActionEntity(), + ServiceCentre.class.getSimpleName() + )).toList(); + + assertEquals(2, content.size()); + assertEquals(1, content.stream().filter(audit -> AuditActionType.UPDATE.equals(audit.getActionType())).count()); + assertEquals(1, content.stream().filter(audit -> AuditActionType.INSERT.equals(audit.getActionType())).count()); + } + @Test @DisplayName("Creating and updating a CourtTranslation should create Audit records") void creatingAndUpdatingACourtTranslationShouldCreateAuditRecords() { @@ -330,6 +367,7 @@ private void assertAuditActionsForEntity(Court owningCourt, String testedEntity, CREATED_AFTER, null, owningCourt.getId().toString(), + null, null ); diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index 8bc737ebc..0e2fb6c0d 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -8,12 +8,14 @@ import uk.gov.hmcts.reform.fact.data.api.audit.AuditUserContext; import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.Region; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditActionType; import uk.gov.hmcts.reform.fact.data.api.entities.types.UserRole; import uk.gov.hmcts.reform.fact.data.api.repositories.AuditRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.RegionRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.UserRepository; import java.time.LocalDate; @@ -55,6 +57,9 @@ class AuditControllerTest { @Autowired AuditRepository auditRepository; + @Autowired + ServiceCentreRepository serviceCentreRepository; + @Autowired UserRepository userRepository; @@ -73,6 +78,7 @@ void setUp() { regions = regionRepository.findAll(); // and clear these down courtRepository.deleteAll(); + serviceCentreRepository.deleteAll(); auditRepository.deleteAll(); auditUserContext.clear(); @@ -181,6 +187,36 @@ void getFilteredAndPaginatedAuditsReturnsFilteredResults() throws Exception { .andExpect(jsonPath("$.page.totalElements").value(3)); } + @Test + @DisplayName("GET /audits/v1 returns results filtered by serviceCentreId") + void getFilteredAndPaginatedAuditsReturnsServiceCentreFilteredResults() throws Exception { + + ServiceCentre serviceCentre1 = createTestServiceCentre(); + for (int i = 0; i < 2; i++) { + serviceCentre1.setOpen(!Optional.ofNullable(serviceCentre1.getOpen()).orElse(false)); + serviceCentreRepository.save(serviceCentre1); + } + + ServiceCentre serviceCentre2 = createTestServiceCentre(); + serviceCentre2.setOpen(!Optional.ofNullable(serviceCentre2.getOpen()).orElse(false)); + serviceCentreRepository.save(serviceCentre2); + + mvc.perform(get("/audits/v1") + .param("pageNumber", "0") + .param("pageSize", "10") + .param("fromDate", LocalDate.now().toString()) + .param("serviceCentreId", serviceCentre1.getId().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content.length()").value(3)) + .andExpect(jsonPath("$.content[0]").exists()) + .andExpect(jsonPath("$.content[*].subjectId").value(allElementsEqual(serviceCentre1.getId().toString()))) + .andExpect(jsonPath("$.content[*].subjectType").value(allElementsEqual("SERVICE_CENTRE"))) + .andExpect(jsonPath("$.page.number").value(0)) + .andExpect(jsonPath("$.page.totalPages").value(1)) + .andExpect(jsonPath("$.page.totalElements").value(3)); + } + @Test @DisplayName("GET /audits/v1 returns correct action types") @@ -262,6 +298,29 @@ void getFilteredAndPaginatedAuditsReturnsBadRequestForInvalidCourtId() throws Ex .andExpect(status().isBadRequest()); } + @Test + @DisplayName("GET /audits/v1 returns returns 400 when serviceCentreId is invalid") + void getFilteredAndPaginatedAuditsReturnsBadRequestForInvalidServiceCentreId() throws Exception { + mvc.perform(get("/audits/v1") + .param("pageNumber", "0") + .param("pageSize", "5") + .param("fromDate", LocalDate.now().toString()) + .param("serviceCentreId", "this-is-not-a-valid-service-centre-id")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("GET /audits/v1 returns returns 400 when courtId and serviceCentreId are both provided") + void getFilteredAndPaginatedAuditsReturnsBadRequestForBothSubjectIds() throws Exception { + mvc.perform(get("/audits/v1") + .param("pageNumber", "0") + .param("pageSize", "5") + .param("fromDate", LocalDate.now().toString()) + .param("courtId", UUID.randomUUID().toString()) + .param("serviceCentreId", UUID.randomUUID().toString())) + .andExpect(status().isBadRequest()); + } + @Test @DisplayName("GET /audits/v1 returns returns 400 when page size is invalid") void getFilteredAndPaginatedAuditsReturnsBadRequestForInvalidPageSize() throws Exception { @@ -301,6 +360,13 @@ private List createTestCourts(int num) { return results; } + private ServiceCentre createTestServiceCentre() { + return serviceCentreRepository.save(ServiceCentre.builder() + .name("Service Centre " + RandomStringUtils.insecure().next(10, true, false)) + .open(Boolean.FALSE) + .build()); + } + private Court buildCourt(UUID regionId, String name) { return Court.builder() .name(name) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListener.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListener.java index d693be26e..6c8079fa9 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListener.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListener.java @@ -1,7 +1,7 @@ package uk.gov.hmcts.reform.fact.data.api.audit; import uk.gov.hmcts.reform.fact.data.api.entities.Audit; -import uk.gov.hmcts.reform.fact.data.api.entities.AuditableCourtEntity; +import uk.gov.hmcts.reform.fact.data.api.entities.AuditableEntity; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditActionType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Change; @@ -33,7 +33,7 @@ import org.springframework.stereotype.Component; /** - * Entity listener implementation for all {@link AuditableCourtEntity} derived entities. + * Entity listener implementation for all {@link AuditableEntity} derived entities. */ @Component @RequiredArgsConstructor @@ -60,17 +60,17 @@ public void setApplicationContext(final ApplicationContext applicationContext) t } @PrePersist - public void beforePersist(AuditableCourtEntity entity) { + public void beforePersist(AuditableEntity entity) { writeAudit(entity, AuditActionType.INSERT); } @PreUpdate - public void beforeUpdate(AuditableCourtEntity entity) { + public void beforeUpdate(AuditableEntity entity) { writeAudit(entity, AuditActionType.UPDATE); } @PreRemove - public void beforeRemove(AuditableCourtEntity entity) { + public void beforeRemove(AuditableEntity entity) { writeAudit(entity, AuditActionType.DELETE); } @@ -92,13 +92,13 @@ private boolean ensureAuditUserContext() { return auditUserContextRef.get() != null; } - private void writeAudit(AuditableCourtEntity entity, AuditActionType auditActionType) { + private void writeAudit(AuditableEntity entity, AuditActionType auditActionType) { if (ensureAuditUserContext() && auditUserContextRef.get().isAuditSuppressed()) { return; } if (ensureEntityManager()) { - AuditableCourtEntity previousEntity = auditActionType == AuditActionType.INSERT + AuditableEntity previousEntity = auditActionType == AuditActionType.INSERT ? null : findPreviousEntity(entity); writeAuditRecord(entity, previousEntity, auditActionType); @@ -108,14 +108,14 @@ private void writeAudit(AuditableCourtEntity entity, AuditActionType auditAction } } - private AuditableCourtEntity findPreviousEntity(final AuditableCourtEntity entity) { + private AuditableEntity findPreviousEntity(final AuditableEntity entity) { // Need to run our lookup for the previous entity on a different // thread. If we don't, JPA/Hibernate will intercept and return the // data that's about to be persisted as part of the event we're // listening to. - Future future = executor.submit( + Future future = executor.submit( () -> entityManagerRef.get().find( entity.getClass(), entity.getId() @@ -135,7 +135,7 @@ private AuditableCourtEntity findPreviousEntity(final AuditableCourtEntity entit } } - private void writeAuditRecord(AuditableCourtEntity entity, AuditableCourtEntity previous, + private void writeAuditRecord(AuditableEntity entity, AuditableEntity previous, AuditActionType operationType) { if (auditUserContextRef.get() == null && !ensureAuditUserContext()) { log.error("No audit user context available during an audit operation"); @@ -143,7 +143,8 @@ private void writeAuditRecord(AuditableCourtEntity entity, AuditableCourtEntity } Audit.AuditBuilder audit = Audit.builder() - .courtId(entity.getCourtId()) + .subjectId(entity.getAuditSubjectId()) + .subjectType(entity.getAuditSubjectType()) .actionType(operationType) .actionEntity(entity.getClass().getSimpleName()) .createdAt(ZonedDateTime.now()) @@ -154,7 +155,7 @@ private void writeAuditRecord(AuditableCourtEntity entity, AuditableCourtEntity entityManagerRef.get().persist(audit.build()); } - private List generateDiffs(AuditableCourtEntity previous, AuditableCourtEntity current) { + private List generateDiffs(AuditableEntity previous, AuditableEntity current) { List diffs = new ArrayList<>(); try { // convert the entities into maps diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java index 66f0e4169..b951e04b6 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java @@ -2,6 +2,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.Audit; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidDateRangeException; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; import uk.gov.hmcts.reform.fact.data.api.services.AuditService; import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidUUID; @@ -38,9 +39,9 @@ public class AuditController { @GetMapping("/v1") @Operation( - summary = "Get filtered and paginated list of audits for a given Court", + summary = "Get filtered and paginated list of audits", description = "Fetch a paginated and optionally filtered, list of audit records" - + " that relate to a specific Court" + + " that relate to a specific Court or Service Centre" ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved list of audits"), @@ -52,6 +53,7 @@ public ResponseEntity> getFilteredAndPaginatedAudits( @RequestParam(name = "pageSize", defaultValue = "25") @Positive(message = "pageSize must be greater than 0") int pageSize, @RequestParam(name = "courtId", required = false) @ValidUUID(allowNull = true) String courtId, + @RequestParam(name = "serviceCentreId", required = false) @ValidUUID(allowNull = true) String serviceCentreId, @RequestParam(name = "email", required = false) @Pattern( regexp = "^[A-Za-z0-9._+-]*(|@[A-Za-z0-9._+-]*)$", @@ -68,6 +70,9 @@ public ResponseEntity> getFilteredAndPaginatedAudits( if (toDate != null && toDate.isBefore(fromDate)) { throw new InvalidDateRangeException("toDate must not be before fromDate"); } + if (courtId != null && serviceCentreId != null) { + throw new InvalidParameterCombinationException("Only one of courtId or serviceCentreId can be provided"); + } return ResponseEntity.ok( auditService.getFilteredAndPaginatedAudits( @@ -76,6 +81,7 @@ public ResponseEntity> getFilteredAndPaginatedAudits( fromDate, toDate, courtId, + serviceCentreId, emailMatch ) ); diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java index e707a75db..abbfd73ee 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java @@ -1,12 +1,14 @@ package uk.gov.hmcts.reform.fact.data.api.entities; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditActionType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Change; import java.time.ZonedDateTime; import java.util.List; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -48,12 +50,16 @@ public class Audit { private UUID id; @NotNull - @Column(name = "court_id") - private UUID courtId; + @Column(name = "subject_id") + private UUID subjectId; + + @NotNull + @Enumerated(EnumType.STRING) + private AuditSubjectType subjectType; @Schema(description = "The associated Court") @ManyToOne - @JoinColumn(name = "court_id", insertable = false, updatable = false) + @JoinColumn(name = "subject_id", insertable = false, updatable = false) private Court court; @NotNull @@ -83,4 +89,15 @@ public class Audit { @Setter(AccessLevel.NONE) private ZonedDateTime createdAt; + @JsonProperty("courtId") + public UUID getCourtId() { + return AuditSubjectType.COURT.equals(subjectType) ? subjectId : null; + } + + @JsonProperty("courtId") + public void setCourtId(UUID courtId) { + this.subjectId = courtId; + this.subjectType = AuditSubjectType.COURT; + } + } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableCourtEntity.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableCourtEntity.java index 424b66d35..1cf6f5766 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableCourtEntity.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableCourtEntity.java @@ -1,9 +1,19 @@ package uk.gov.hmcts.reform.fact.data.api.entities; -import java.util.UUID; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; -public interface AuditableCourtEntity { - UUID getId(); +import java.util.UUID; +public interface AuditableCourtEntity extends AuditableEntity { UUID getCourtId(); + + @Override + default UUID getAuditSubjectId() { + return getCourtId(); + } + + @Override + default AuditSubjectType getAuditSubjectType() { + return AuditSubjectType.COURT; + } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableEntity.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableEntity.java new file mode 100644 index 000000000..d1438f40f --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableEntity.java @@ -0,0 +1,16 @@ +package uk.gov.hmcts.reform.fact.data.api.entities; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; + +import java.util.UUID; + +public interface AuditableEntity { + UUID getId(); + + @JsonIgnore + UUID getAuditSubjectId(); + + @JsonIgnore + AuditSubjectType getAuditSubjectType(); +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentre.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentre.java new file mode 100644 index 000000000..b938e8e33 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentre.java @@ -0,0 +1,119 @@ +package uk.gov.hmcts.reform.fact.data.api.entities; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.annotations.TimeZoneStorage; +import org.hibernate.annotations.TimeZoneStorageType; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.type.SqlTypes; +import uk.gov.hmcts.reform.fact.data.api.audit.AuditableCourtEntityListener; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidCourtSlug; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.UUID; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Entity +@EntityListeners(AuditableCourtEntityListener.class) +@Table(name = "service_centre") +public class ServiceCentre implements AuditableEntity { + + @Schema( + description = "The internal ID - assigned by the server during creation", + accessMode = Schema.AccessMode.READ_ONLY + ) + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private UUID id; + + @Schema(description = "The name of the Service Centre") + @NotBlank(message = "Service centre name must be specified") + @Size(min = 5, max = 200, message = "Service centre name should be between 5 and 200 chars") + @Pattern( + regexp = "^[A-Za-z&'()\\- ]+$", + message = "Service centre name may only contain letters, spaces, apostrophes, hyphens, ampersands, " + + "and parentheses" + ) + private String name; + + @Schema(description = "The Service Centre 'slug'") + @ValidCourtSlug + @Column(unique = true) + private String slug; + + @Schema(description = "The open status of the Service Centre") + @NotNull(message = "Service centre open status must be specified") + private Boolean open; + + @Schema(description = "Any warning notices attached to the Service Centre") + @Size(max = 500, message = "Warning notice must be less than 500 characters") + @Pattern( + regexp = "^[A-Za-z0-9.,!?:;'\"()\\-/&@+\\s]+$", + message = "Warning notice may only contain letters, numbers, spaces, and standard punctuation or symbols (@, +)" + ) + @Column(name = "warning_notice") + private String warningNotice; + + @Schema( + description = "The created date/time of the Service Centre record", + accessMode = Schema.AccessMode.READ_ONLY + ) + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC) + @CreationTimestamp + @Setter(AccessLevel.NONE) + private ZonedDateTime createdAt; + + @Schema( + description = "The last updated date/time of the Service Centre record", + accessMode = Schema.AccessMode.READ_ONLY + ) + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC) + @UpdateTimestamp + @Setter(AccessLevel.NONE) + private ZonedDateTime lastUpdatedAt; + + @Schema(description = "The list of associated Service Area IDs") + @JdbcTypeCode(SqlTypes.ARRAY) + @Column(columnDefinition = "uuid[]") + private List serviceAreaIds; + + @Schema(description = "The catchment type") + @Enumerated(EnumType.STRING) + private CatchmentType catchmentType; + + @Override + public UUID getAuditSubjectId() { + return id; + } + + @Override + public AuditSubjectType getAuditSubjectType() { + return AuditSubjectType.SERVICE_CENTRE; + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAddress.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAddress.java new file mode 100644 index 000000000..71afc8a87 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAddress.java @@ -0,0 +1,110 @@ +package uk.gov.hmcts.reform.fact.data.api.entities; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import uk.gov.hmcts.reform.fact.data.api.audit.AuditableCourtEntityListener; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AddressType; +import uk.gov.hmcts.reform.fact.data.api.entities.validation.ValidationConstants; +import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidPostcode; + +import java.math.BigDecimal; +import java.util.UUID; + +@Data +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor +@Builder +@Entity +@EntityListeners(AuditableCourtEntityListener.class) +@Table(name = "service_centre_address") +public class ServiceCentreAddress implements AuditableEntity { + + @Schema( + description = "The internal ID - assigned by the server during creation", + accessMode = Schema.AccessMode.READ_ONLY + ) + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private UUID id; + + @Schema(description = "The ID of the associated Service Centre", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @Column(name = "service_centre_id") + private UUID serviceCentreId; + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "service_centre_id", insertable = false, updatable = false) + private ServiceCentre serviceCentre; + + @Schema(description = "The first address line") + @Size(max = 255, message = "Address line should be 255 characters or less") + @Pattern(regexp = ValidationConstants.OPTIONAL_ADDRESS_LINE_REGEX, + message = "Address Line 1: " + ValidationConstants.ADDRESS_LINE_REGEX_MESSAGE) + @Column(name = "address_line_1") + private String addressLine1; + + @Schema(description = "The second address line") + @Size(max = 255, message = "Address line should be 255 characters or less") + @Pattern(regexp = ValidationConstants.OPTIONAL_ADDRESS_LINE_REGEX, + message = "Address Line 2: " + ValidationConstants.ADDRESS_LINE_REGEX_MESSAGE) + @Column(name = "address_line_2") + private String addressLine2; + + @Schema(description = "The town/city") + @Size(max = 100, message = "Town/City name should be 100 characters or less") + @Pattern(regexp = ValidationConstants.OPTIONAL_ADDRESS_LINE_REGEX, + message = "Town/City: " + ValidationConstants.ADDRESS_LINE_REGEX_MESSAGE) + private String townCity; + + @Schema(description = "The county") + @Size(max = 100, message = "County name should be 100 characters or less") + @Pattern(regexp = ValidationConstants.OPTIONAL_ADDRESS_LINE_REGEX, + message = "County: " + ValidationConstants.ADDRESS_LINE_REGEX_MESSAGE) + private String county; + + @Schema(description = "The postcode") + @ValidPostcode + private String postcode; + + @Schema(description = "The latitude coordinate") + private BigDecimal lat; + + @Schema(description = "The longitude coordinate") + private BigDecimal lon; + + @Schema(description = "The address type") + @Enumerated(EnumType.STRING) + private AddressType addressType; + + @Override + public UUID getAuditSubjectId() { + return serviceCentreId; + } + + @Override + public AuditSubjectType getAuditSubjectType() { + return AuditSubjectType.SERVICE_CENTRE; + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java new file mode 100644 index 000000000..7c04100c8 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java @@ -0,0 +1,86 @@ +package uk.gov.hmcts.reform.fact.data.api.entities; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; +import uk.gov.hmcts.reform.fact.data.api.audit.AuditableCourtEntityListener; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; + +import java.util.List; +import java.util.UUID; + +@Data +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor +@Builder +@Entity +@EntityListeners(AuditableCourtEntityListener.class) +@Table(name = "service_centre_areas_of_law") +public class ServiceCentreAreasOfLaw implements AuditableEntity { + + @Schema( + description = "The internal ID - assigned by the server during creation", + accessMode = Schema.AccessMode.READ_ONLY + ) + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private UUID id; + + @Schema(description = "The ID of the associated Service Centre", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @Column(name = "service_centre_id") + private UUID serviceCentreId; + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "service_centre_id", insertable = false, updatable = false) + private ServiceCentre serviceCentre; + + @Schema(description = "The list of associated Areas of Law") + @JdbcTypeCode(SqlTypes.ARRAY) + @Column(columnDefinition = "uuid[]") + private List areasOfLaw; + + @Transient + @JsonIgnore + private List areasOfLawDetails; + + @JsonProperty("areasOfLaw") + public List getAreasOfLawForView() { + return areasOfLawDetails != null ? areasOfLawDetails : areasOfLaw; + } + + @JsonIgnore + public List getAreasOfLaw() { + return areasOfLaw; + } + + @Override + public UUID getAuditSubjectId() { + return serviceCentreId; + } + + @Override + public AuditSubjectType getAuditSubjectType() { + return AuditSubjectType.SERVICE_CENTRE; + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreContactDetails.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreContactDetails.java new file mode 100644 index 000000000..43ac8f7e3 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreContactDetails.java @@ -0,0 +1,116 @@ +package uk.gov.hmcts.reform.fact.data.api.entities; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import uk.gov.hmcts.reform.fact.data.api.audit.AuditableCourtEntityListener; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.validation.ValidationConstants; + +import java.util.UUID; + +@Data +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor +@Builder +@Entity +@EntityListeners(AuditableCourtEntityListener.class) +@Table(name = "service_centre_contact_details") +public class ServiceCentreContactDetails implements AuditableEntity { + + @Schema( + description = "The internal ID - assigned by the server during creation", + accessMode = Schema.AccessMode.READ_ONLY + ) + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private UUID id; + + @Schema(description = "The ID of the associated Service Centre", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @Column(name = "service_centre_id") + private UUID serviceCentreId; + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "service_centre_id", insertable = false, updatable = false) + private ServiceCentre serviceCentre; + + @Schema(description = "The ID of the associated Contact Description Type") + @Column(name = "service_centre_contact_description_id") + private UUID serviceCentreContactDescriptionId; + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "service_centre_contact_description_id", insertable = false, updatable = false) + private ContactDescriptionType serviceCentreContactDescription; + + @Transient + @JsonIgnore + private ContactDescriptionType serviceCentreContactDescriptionDetails; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonProperty("serviceCentreContactDescription") + public ContactDescriptionType getServiceCentreContactDescriptionForView() { + return serviceCentreContactDescriptionDetails; + } + + @Schema(description = "The explanation") + @Column(name = "explanation", length = 250) + @Size(max = 250, message = "Explanation should be no more than {max} characters") + @Pattern( + regexp = "^[A-Za-z0-9 '\\-()&+]*$", + message = "Explanation contains invalid characters. Allowed: letters, numbers, spaces, apostrophes, - ( ) & +" + ) + private String explanation; + + @Schema(description = "The Welsh language explanation") + @Column(name = "explanation_cy", length = 250) + @Size(max = 250, message = "Explanation should be no more than {max} characters") + @Pattern( + regexp = "^[\\p{L}0-9 '\\-()&+]*$", + message = "Welsh explanation contains invalid characters. Allowed: letters (with accents), numbers, spaces, " + + "apostrophes, - ( ) & +" + ) + private String explanationCy; + + @Schema(description = "The associated email address") + @Size(max = ValidationConstants.EMAIL_MAX_LENGTH, message = ValidationConstants.EMAIL_MAX_LENGTH_MESSAGE) + @Pattern(regexp = ValidationConstants.EMAIL_REGEX, message = ValidationConstants.EMAIL_REGEX_MESSAGE) + private String email; + + @Schema(description = "The associated phone number") + @Size(max = ValidationConstants.PHONE_NO_MAX_LENGTH, message = ValidationConstants.PHONE_NO_MAX_LENGTH_MESSAGE) + @Pattern(regexp = ValidationConstants.PHONE_NO_REGEX, message = ValidationConstants.PHONE_NO_REGEX_MESSAGE) + private String phoneNumber; + + @Override + public UUID getAuditSubjectId() { + return serviceCentreId; + } + + @Override + public AuditSubjectType getAuditSubjectType() { + return AuditSubjectType.SERVICE_CENTRE; + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubjectType.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubjectType.java new file mode 100644 index 000000000..6d97135cf --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubjectType.java @@ -0,0 +1,6 @@ +package uk.gov.hmcts.reform.fact.data.api.entities.types; + +public enum AuditSubjectType { + COURT, + SERVICE_CENTRE +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java index 7f49759b2..daf2df1b7 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java @@ -1,6 +1,7 @@ package uk.gov.hmcts.reform.fact.data.api.repositories; import uk.gov.hmcts.reform.fact.data.api.entities.Audit; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import java.time.ZonedDateTime; import java.util.UUID; @@ -26,13 +27,15 @@ Page findByCreatedAtBetween( ZonedDateTime createdAtBefore, Pageable pageable); - Page findByCourtIdAndCreatedAtAfter( - UUID courtId, + Page findBySubjectIdAndSubjectTypeAndCreatedAtAfter( + UUID subjectId, + AuditSubjectType subjectType, ZonedDateTime createdAt, Pageable pageable); - Page findByCourtIdAndCreatedAtBetween( - UUID courtId, + Page findBySubjectIdAndSubjectTypeAndCreatedAtBetween( + UUID subjectId, + AuditSubjectType subjectType, ZonedDateTime createdAtAfter, ZonedDateTime createdAtBefore, Pageable pageable); @@ -61,7 +64,7 @@ Page findByCourtIdAndCreatedAtBetween( and u.email like %:email% """; - String AND_COURT_ID_EQUALS = " and a.courtId = :courtId"; + String AND_SUBJECT_EQUALS = " and a.subjectId = :subjectId and a.subjectType = :subjectType"; @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_AFTER) @@ -77,16 +80,18 @@ Page findByCreatedAtBetweenAndEmailAddressLike( String email, Pageable pageable); - @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_AFTER + AND_COURT_ID_EQUALS) - Page findByCourtIdAndCreatedAtAfterAndEmailAddressLike( - UUID courtId, + @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_AFTER + AND_SUBJECT_EQUALS) + Page findBySubjectIdAndSubjectTypeAndCreatedAtAfterAndEmailAddressLike( + UUID subjectId, + AuditSubjectType subjectType, ZonedDateTime createdAtAfter, String email, Pageable pageable); - @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_BETWEEN + AND_COURT_ID_EQUALS) - Page findByCourtIdAndCreatedAtBetweenAndEmailAddressLike( - UUID courtId, + @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_BETWEEN + AND_SUBJECT_EQUALS) + Page findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( + UUID subjectId, + AuditSubjectType subjectType, ZonedDateTime createdAtAfter, ZonedDateTime createdAtBefore, String email, diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAddressRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAddressRepository.java new file mode 100644 index 000000000..79886188d --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAddressRepository.java @@ -0,0 +1,10 @@ +package uk.gov.hmcts.reform.fact.data.api.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAddress; + +import java.util.UUID; + +@Repository +public interface ServiceCentreAddressRepository extends JpaRepository {} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAreasOfLawRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAreasOfLawRepository.java new file mode 100644 index 000000000..f7b79bc15 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAreasOfLawRepository.java @@ -0,0 +1,10 @@ +package uk.gov.hmcts.reform.fact.data.api.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; + +import java.util.UUID; + +@Repository +public interface ServiceCentreAreasOfLawRepository extends JpaRepository {} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreContactDetailsRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreContactDetailsRepository.java new file mode 100644 index 000000000..903ad4d1a --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreContactDetailsRepository.java @@ -0,0 +1,10 @@ +package uk.gov.hmcts.reform.fact.data.api.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; + +import java.util.UUID; + +@Repository +public interface ServiceCentreContactDetailsRepository extends JpaRepository {} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java new file mode 100644 index 000000000..67dfdf948 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java @@ -0,0 +1,10 @@ +package uk.gov.hmcts.reform.fact.data.api.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; + +import java.util.UUID; + +@Repository +public interface ServiceCentreRepository extends JpaRepository {} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java index 1c8fa71cb..459265771 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java @@ -2,6 +2,8 @@ import uk.gov.hmcts.reform.fact.data.api.config.properties.AuditConfigurationProperties; import uk.gov.hmcts.reform.fact.data.api.entities.Audit; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.repositories.AuditRepository; import java.time.LocalDate; @@ -28,7 +30,7 @@ public class AuditService { // bitfields for query column matching private static final int IGNORE = 0b0000; - private static final int INCLUDE_COURT_ID = 0b0001; + private static final int INCLUDE_SUBJECT_ID = 0b0001; private static final int INCLUDE_TO_DATE = 0b0010; private static final int INCLUDE_EMAIL = 0b0100; @@ -39,12 +41,14 @@ public class AuditService { * @param pageSize The size of the results page * @param fromDate The "from" date for filtering. Filtering assumes start of day. * @param courtId The id of the court. can be {@code null}. + * @param serviceCentreId The id of the service centre. can be {@code null}. * @param email The email, or partial email of the related user. can be {@code null}. * @param toDate The "to" date for auditing. Filtering assumes end of day. can e {@code null}. * @return a {@link Page} of {@link Audit} results. */ public Page getFilteredAndPaginatedAudits(int pageNumber, int pageSize, @NonNull LocalDate fromDate, - LocalDate toDate, String courtId, String email) { + LocalDate toDate, String courtId, String serviceCentreId, + String email) { Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending()); @@ -58,9 +62,9 @@ public Page getFilteredAndPaginatedAudits(int pageNumber, int pageSize, @ ZoneOffset.UTC )).orElse(null); - UUID courtUUID = Optional.ofNullable(courtId).map(UUID::fromString).orElse(null); + SubjectFilter subjectFilter = resolveSubjectFilter(courtId, serviceCentreId); - return performAuditQuery(fromDateTime, toDateTime, courtUUID, email, pageable); + return performAuditQuery(fromDateTime, toDateTime, subjectFilter, email, pageable); } /** @@ -77,36 +81,53 @@ public void removeExpiredAuditEntries() { } private Page performAuditQuery(ZonedDateTime fromDateTime, ZonedDateTime toDateTime, - UUID courtUUID, String email, Pageable pageable) { + SubjectFilter subjectFilter, String email, Pageable pageable) { // create a query bitfield that can be tested in the switch // below using boolean arithmetic matching. int query = 0; - query |= courtUUID != null ? INCLUDE_COURT_ID : IGNORE; + query |= subjectFilter != null ? INCLUDE_SUBJECT_ID : IGNORE; query |= toDateTime != null ? INCLUDE_TO_DATE : IGNORE; query |= email != null && !email.isBlank() ? INCLUDE_EMAIL : IGNORE; // perform the appropriate repository lookup based on matching // bits in the query param return switch (query) { - case INCLUDE_COURT_ID -> auditRepository.findByCourtIdAndCreatedAtAfter( - courtUUID, fromDateTime, pageable); + case INCLUDE_SUBJECT_ID -> auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfter( + subjectFilter.subjectId(), subjectFilter.subjectType(), fromDateTime, pageable); case INCLUDE_TO_DATE -> auditRepository.findByCreatedAtBetween( fromDateTime, toDateTime, pageable); - case INCLUDE_COURT_ID | INCLUDE_TO_DATE -> auditRepository.findByCourtIdAndCreatedAtBetween( - courtUUID, fromDateTime, toDateTime, pageable); + case INCLUDE_SUBJECT_ID | INCLUDE_TO_DATE -> + auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtBetween( + subjectFilter.subjectId(), subjectFilter.subjectType(), fromDateTime, toDateTime, pageable); case INCLUDE_EMAIL -> auditRepository.findByCreatedAtAfterAndEmailAddressLike( fromDateTime, email, pageable); - case INCLUDE_COURT_ID | INCLUDE_EMAIL -> auditRepository.findByCourtIdAndCreatedAtAfterAndEmailAddressLike( - courtUUID, fromDateTime, email, pageable); + case INCLUDE_SUBJECT_ID | INCLUDE_EMAIL -> + auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfterAndEmailAddressLike( + subjectFilter.subjectId(), subjectFilter.subjectType(), fromDateTime, email, pageable); case INCLUDE_TO_DATE | INCLUDE_EMAIL -> auditRepository.findByCreatedAtBetweenAndEmailAddressLike( fromDateTime, toDateTime, email, pageable); - case INCLUDE_COURT_ID | INCLUDE_TO_DATE | INCLUDE_EMAIL -> - auditRepository.findByCourtIdAndCreatedAtBetweenAndEmailAddressLike( - courtUUID, fromDateTime, toDateTime, email, pageable); + case INCLUDE_SUBJECT_ID | INCLUDE_TO_DATE | INCLUDE_EMAIL -> + auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( + subjectFilter.subjectId(), subjectFilter.subjectType(), fromDateTime, toDateTime, email, pageable); // if no field match bits are set then test only using the // mandatory fromDate. default -> auditRepository.findByCreatedAtAfter(fromDateTime, pageable); }; } + + private static SubjectFilter resolveSubjectFilter(String courtId, String serviceCentreId) { + if (courtId != null && serviceCentreId != null) { + throw new InvalidParameterCombinationException("Only one of courtId or serviceCentreId can be provided"); + } + if (courtId != null) { + return new SubjectFilter(UUID.fromString(courtId), AuditSubjectType.COURT); + } + if (serviceCentreId != null) { + return new SubjectFilter(UUID.fromString(serviceCentreId), AuditSubjectType.SERVICE_CENTRE); + } + return null; + } + + private record SubjectFilter(UUID subjectId, AuditSubjectType subjectType) {} } diff --git a/src/main/resources/db/migration/V1.28__create_service_centre_tables.sql b/src/main/resources/db/migration/V1.28__create_service_centre_tables.sql new file mode 100644 index 000000000..8a7e4c4c4 --- /dev/null +++ b/src/main/resources/db/migration/V1.28__create_service_centre_tables.sql @@ -0,0 +1,64 @@ +CREATE TABLE service_centre ( + id UUID PRIMARY KEY NOT NULL, + name VARCHAR NOT NULL, + slug VARCHAR, + open BOOLEAN, + warning_notice VARCHAR, + created_at TIMESTAMP NOT NULL, + last_updated_at TIMESTAMP NOT NULL, + service_area_ids UUID[], + catchment_type VARCHAR +); + +CREATE TABLE service_centre_address ( + id UUID PRIMARY KEY NOT NULL, + service_centre_id UUID NOT NULL, + address_line_1 VARCHAR, + address_line_2 VARCHAR, + town_city VARCHAR, + county VARCHAR, + postcode VARCHAR, + lat DECIMAL(9,6), + lon DECIMAL(9,6), + address_type VARCHAR +); + +CREATE TABLE service_centre_contact_details ( + id UUID PRIMARY KEY NOT NULL, + service_centre_id UUID NOT NULL, + service_centre_contact_description_id UUID, + explanation VARCHAR, + explanation_cy VARCHAR, + email VARCHAR, + phone_number VARCHAR +); + +CREATE TABLE service_centre_areas_of_law ( + id UUID PRIMARY KEY NOT NULL, + service_centre_id UUID NOT NULL, + areas_of_law UUID[] +); + +ALTER TABLE service_centre_address +ADD CONSTRAINT fk_service_centre_address_service_centre + FOREIGN KEY (service_centre_id) + REFERENCES service_centre(id) + ON DELETE CASCADE; + +ALTER TABLE service_centre_contact_details +ADD CONSTRAINT fk_service_centre_contact_details_service_centre + FOREIGN KEY (service_centre_id) + REFERENCES service_centre(id) + ON DELETE CASCADE; + +ALTER TABLE service_centre_contact_details +ADD CONSTRAINT fk_service_centre_contact_details_description + FOREIGN KEY (service_centre_contact_description_id) + REFERENCES contact_description_types(id) + ON DELETE SET NULL; + +ALTER TABLE service_centre_areas_of_law +ADD CONSTRAINT fk_service_centre_areas_of_law_service_centre + FOREIGN KEY (service_centre_id) + REFERENCES service_centre(id) + ON DELETE CASCADE; diff --git a/src/main/resources/db/migration/V1.29__generalise_audit_subject.sql b/src/main/resources/db/migration/V1.29__generalise_audit_subject.sql new file mode 100644 index 000000000..1e5b2c1f8 --- /dev/null +++ b/src/main/resources/db/migration/V1.29__generalise_audit_subject.sql @@ -0,0 +1,12 @@ +ALTER TABLE audit + RENAME COLUMN court_id TO subject_id; + +ALTER TABLE audit + ADD COLUMN subject_type VARCHAR; + +UPDATE audit +SET subject_type = 'COURT' +WHERE subject_type IS NULL; + +ALTER TABLE audit + ALTER COLUMN subject_type SET NOT NULL; diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListenerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListenerTest.java index 51c345156..ab915c3f2 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListenerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListenerTest.java @@ -10,6 +10,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.Audit; import uk.gov.hmcts.reform.fact.data.api.entities.Court; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import java.util.UUID; @@ -118,6 +119,8 @@ void generateDiffsShouldReturnChangesWhenValuesDiffer() throws Exception { verify(entityManager, times(1)).find(Court.class, courtCurrent.getId()); Audit audit = auditCaptor.getValue(); assertEquals(USER_ID, audit.getUserId()); + assertEquals(COURT_ID, audit.getSubjectId()); + assertEquals(AuditSubjectType.COURT, audit.getSubjectType()); assertEquals(1, audit.getActionDataDiff().size()); assertEquals("name", audit.getActionDataDiff().getFirst().field()); } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index b82facf62..4b7883c7a 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -9,8 +9,10 @@ import uk.gov.hmcts.reform.fact.data.api.entities.Audit; import uk.gov.hmcts.reform.fact.data.api.entities.Court; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Change; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidDateRangeException; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.services.AuditService; import java.time.LocalDate; @@ -33,6 +35,7 @@ class AuditControllerTest { private static final UUID AUDIT_ID = UUID.randomUUID(); private static final UUID COURT_ID = UUID.randomUUID(); + private static final UUID SERVICE_CENTRE_ID = UUID.randomUUID(); private static final UUID USER_ID = UUID.randomUUID(); private static final int PAGE_NUMBER = 0; @@ -58,6 +61,7 @@ void getFilteredAndPaginatedAuditsReturns200() { any(LocalDate.class), any(LocalDate.class), isNull(), + isNull(), isNull() )).thenReturn(auditPage); @@ -66,6 +70,7 @@ void getFilteredAndPaginatedAuditsReturns200() { PAGE_SIZE, null, null, + null, LocalDate.now().minusDays(1), LocalDate.now() ); @@ -85,16 +90,34 @@ void getFilteredAndPaginatedAuditsThrowsInvalidDateRangeExceptionForInvalidDateR PAGE_SIZE, null, null, + null, fromDate, toDate ) ); } + @Test + void getFilteredAndPaginatedAuditsThrowsInvalidParameterCombinationForBothSubjectIds() { + assertThrows( + InvalidParameterCombinationException.class, () -> + auditController.getFilteredAndPaginatedAudits( + PAGE_NUMBER, + PAGE_SIZE, + COURT_ID.toString(), + SERVICE_CENTRE_ID.toString(), + null, + LocalDate.now().minusDays(1), + LocalDate.now() + ) + ); + } + private Audit createAudit() { return Audit.builder() .id(AUDIT_ID) - .courtId(COURT_ID) + .subjectId(COURT_ID) + .subjectType(AuditSubjectType.COURT) .actionEntity(Court.class.getSimpleName()) .actionDataDiff(List.of( new Change("isOpen", Boolean.FALSE, Boolean.TRUE), diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java index f2e23344b..05f94eef3 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java @@ -12,7 +12,9 @@ import uk.gov.hmcts.reform.fact.data.api.config.properties.AuditConfigurationProperties; import uk.gov.hmcts.reform.fact.data.api.entities.Audit; import uk.gov.hmcts.reform.fact.data.api.entities.Court; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Change; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.repositories.AuditRepository; import java.time.LocalDate; @@ -35,6 +37,7 @@ class AuditServiceTest { private static final UUID AUDIT_ID = UUID.randomUUID(); private static final UUID COURT_ID = UUID.randomUUID(); + private static final UUID SERVICE_CENTRE_ID = UUID.randomUUID(); private static final UUID USER_ID = UUID.randomUUID(); private static final String EMAIL = "test.user@example.com"; @@ -79,6 +82,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCreatedAtAfter() { fromDate, null, null, + null, null ); @@ -105,6 +109,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCreatedAtBetween() { fromDate, toDate, null, + null, null ); @@ -117,8 +122,9 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCreatedAtBetween() { @Test void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtAfter() { - when(auditRepository.findByCourtIdAndCreatedAtAfter( + when(auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfter( eq(COURT_ID), + eq(AuditSubjectType.COURT), eq(fromDateTime), any(Pageable.class) ) @@ -131,20 +137,22 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtAfter() { fromDate, null, COURT_ID.toString(), + null, null ); assertThat(result.getContent()).hasSize(1); - verify(auditRepository).findByCourtIdAndCreatedAtAfter( - eq(COURT_ID), eq(fromDateTime), any(Pageable.class) + verify(auditRepository).findBySubjectIdAndSubjectTypeAndCreatedAtAfter( + eq(COURT_ID), eq(AuditSubjectType.COURT), eq(fromDateTime), any(Pageable.class) ); } @Test void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtBetween() { - when(auditRepository.findByCourtIdAndCreatedAtBetween( + when(auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtBetween( eq(COURT_ID), + eq(AuditSubjectType.COURT), eq(fromDateTime), eq(toDateTime), any(Pageable.class) @@ -158,12 +166,13 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtBetween() { fromDate, toDate, COURT_ID.toString(), + null, null ); assertThat(result.getContent()).hasSize(1); - verify(auditRepository).findByCourtIdAndCreatedAtBetween( - eq(COURT_ID), eq(fromDateTime), eq(toDateTime), any(Pageable.class) + verify(auditRepository).findBySubjectIdAndSubjectTypeAndCreatedAtBetween( + eq(COURT_ID), eq(AuditSubjectType.COURT), eq(fromDateTime), eq(toDateTime), any(Pageable.class) ); } @@ -186,6 +195,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCreatedAtAfterAndEmailAddressL fromDate, null, null, + null, EMAIL ); @@ -213,6 +223,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCreatedAtBetweenAndEmailAddres fromDate, toDate, null, + null, EMAIL ); @@ -225,8 +236,9 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCreatedAtBetweenAndEmailAddres @Test void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtAfterAndEmailAddressLike() { - when(auditRepository.findByCourtIdAndCreatedAtAfterAndEmailAddressLike( + when(auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfterAndEmailAddressLike( eq(COURT_ID), + eq(AuditSubjectType.COURT), eq(fromDateTime), eq(EMAIL), any(Pageable.class) @@ -240,20 +252,22 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtAfterAndEma fromDate, null, COURT_ID.toString(), + null, EMAIL ); assertThat(result.getContent()).hasSize(1); - verify(auditRepository).findByCourtIdAndCreatedAtAfterAndEmailAddressLike( - eq(COURT_ID), eq(fromDateTime), eq(EMAIL), any(Pageable.class) + verify(auditRepository).findBySubjectIdAndSubjectTypeAndCreatedAtAfterAndEmailAddressLike( + eq(COURT_ID), eq(AuditSubjectType.COURT), eq(fromDateTime), eq(EMAIL), any(Pageable.class) ); } @Test void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtBetweenAndEmailAddressLike() { - when(auditRepository.findByCourtIdAndCreatedAtBetweenAndEmailAddressLike( + when(auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( eq(COURT_ID), + eq(AuditSubjectType.COURT), eq(fromDateTime), eq(toDateTime), eq(EMAIL), @@ -268,12 +282,13 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtBetweenAndE fromDate, toDate, COURT_ID.toString(), + null, EMAIL ); assertThat(result.getContent()).hasSize(1); - verify(auditRepository).findByCourtIdAndCreatedAtBetweenAndEmailAddressLike( - eq(COURT_ID), eq(fromDateTime), eq(toDateTime), eq(EMAIL), any(Pageable.class) + verify(auditRepository).findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( + eq(COURT_ID), eq(AuditSubjectType.COURT), eq(fromDateTime), eq(toDateTime), eq(EMAIL), any(Pageable.class) ); } @@ -292,11 +307,54 @@ void removeExpiredAuditEntriesShouldUserPropertiesAndCallDeleteAllBeforeCreatedD // edge case tests + @Test + void getFilteredAndPaginatedAuditsShouldCallFindByServiceCentreIdAndCreatedAtAfter() { + + when(auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfter( + eq(SERVICE_CENTRE_ID), + eq(AuditSubjectType.SERVICE_CENTRE), + eq(fromDateTime), + any(Pageable.class) + ) + ).thenReturn(new PageImpl<>(List.of(createAudit()))); + + Page result = + auditService.getFilteredAndPaginatedAudits( + PAGE_NUMBER, + PAGE_SIZE, + fromDate, + null, + null, + SERVICE_CENTRE_ID.toString(), + null + ); + + assertThat(result.getContent()).hasSize(1); + verify(auditRepository).findBySubjectIdAndSubjectTypeAndCreatedAtAfter( + eq(SERVICE_CENTRE_ID), eq(AuditSubjectType.SERVICE_CENTRE), eq(fromDateTime), any(Pageable.class) + ); + } + + @Test + void shouldThrowInvalidParameterCombinationExceptionWhenBothSubjectIdsProvided() { + assertThrows( + InvalidParameterCombinationException.class, () -> auditService.getFilteredAndPaginatedAudits( + PAGE_NUMBER, + PAGE_SIZE, + fromDate, + null, + COURT_ID.toString(), + SERVICE_CENTRE_ID.toString(), + null + ) + ); + } + @Test void shouldThrowNullPointerExceptionWhenFromDateIsNull() { assertThrows( NullPointerException.class, () -> { - auditService.getFilteredAndPaginatedAudits(0, 1, null, null, null, null); + auditService.getFilteredAndPaginatedAudits(0, 1, null, null, null, null, null); } ); } @@ -317,6 +375,7 @@ void shouldCallSimpleQueryForBlankEmail() { fromDate, null, null, + null, "" ); assertThat(result.getContent()).hasSize(1); @@ -328,6 +387,7 @@ void shouldCallSimpleQueryForBlankEmail() { fromDate, null, null, + null, " " ); assertThat(result.getContent()).hasSize(1); @@ -340,7 +400,8 @@ void shouldCallSimpleQueryForBlankEmail() { private Audit createAudit() { return Audit.builder() .id(AUDIT_ID) - .courtId(COURT_ID) + .subjectId(COURT_ID) + .subjectType(AuditSubjectType.COURT) .actionEntity(Court.class.getSimpleName()) .actionDataDiff(List.of( new Change("isOpen", Boolean.FALSE, Boolean.TRUE), From e750efbd6b5a32023e64cdce5c31c59c087b0fae Mon Sep 17 00:00:00 2001 From: Josh <23316525+josh-uk@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:28:10 +0100 Subject: [PATCH 03/40] FACT-2618 Added new controllers/services for service centres (#275) * Added new controllers/services for service centres * Func test fixsxes * Clean up and splitting --- .../controllers/AuthFunctionalTest.java | 149 +++++++++++++++++ ...CentreAddressControllerFunctionalTest.java | 68 ++++++++ ...treAreasOfLawControllerFunctionalTest.java | 52 ++++++ ...ontactDetailsControllerFunctionalTest.java | 75 +++++++++ ...ServiceCentreControllerFunctionalTest.java | 69 ++++++++ .../functional/helpers/TestDataHelper.java | 64 +++++++ .../ServiceCentreAddressControllerTest.java | 142 ++++++++++++++++ ...ServiceCentreAreasOfLawControllerTest.java | 99 +++++++++++ ...iceCentreContactDetailsControllerTest.java | 144 ++++++++++++++++ .../ServiceCentreControllerTest.java | 130 +++++++++++++++ .../ServiceCentreAddressController.java | 149 +++++++++++++++++ .../ServiceCentreAreasOfLawController.java | 77 +++++++++ ...ServiceCentreContactDetailsController.java | 155 +++++++++++++++++ .../controllers/ServiceCentreController.java | 109 ++++++++++++ .../controllers/TestingSupportController.java | 46 +++++ .../api/entities/ServiceCentreAreasOfLaw.java | 11 ++ .../ServiceCentreAddressRepository.java | 11 +- .../ServiceCentreAreasOfLawRepository.java | 6 +- ...ServiceCentreContactDetailsRepository.java | 13 +- .../repositories/ServiceCentreRepository.java | 11 +- .../services/ServiceCentreAddressService.java | 127 ++++++++++++++ .../ServiceCentreAreasOfLawService.java | 83 +++++++++ .../ServiceCentreContactDetailsService.java | 137 +++++++++++++++ .../api/services/ServiceCentreService.java | 142 ++++++++++++++++ .../api/services/TestingSupportService.java | 134 ++++++++++++++- .../V1.30__refresh_service_area_mappings.sql | 43 +++++ .../ServiceCentreAddressControllerTest.java | 93 +++++++++++ ...ServiceCentreAreasOfLawControllerTest.java | 59 +++++++ ...iceCentreContactDetailsControllerTest.java | 98 +++++++++++ .../ServiceCentreControllerTest.java | 84 ++++++++++ .../TestingSupportControllerTest.java | 42 +++++ .../ServiceCentreAddressServiceTest.java | 143 ++++++++++++++++ .../ServiceCentreAreasOfLawServiceTest.java | 125 ++++++++++++++ ...erviceCentreContactDetailsServiceTest.java | 157 ++++++++++++++++++ .../services/ServiceCentreServiceTest.java | 141 ++++++++++++++++ .../services/TestingSupportServiceTest.java | 78 +++++++++ 36 files changed, 3261 insertions(+), 5 deletions(-) create mode 100644 src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreAddressControllerFunctionalTest.java create mode 100644 src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreAreasOfLawControllerFunctionalTest.java create mode 100644 src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreContactDetailsControllerFunctionalTest.java create mode 100644 src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreControllerFunctionalTest.java create mode 100644 src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAddressControllerTest.java create mode 100644 src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAreasOfLawControllerTest.java create mode 100644 src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreContactDetailsControllerTest.java create mode 100644 src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAddressController.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAreasOfLawController.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreContactDetailsController.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreController.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAddressService.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAreasOfLawService.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreContactDetailsService.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java create mode 100644 src/main/resources/db/migration/V1.30__refresh_service_area_mappings.sql create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAddressControllerTest.java create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAreasOfLawControllerTest.java create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreContactDetailsControllerTest.java create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAddressServiceTest.java create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAreasOfLawServiceTest.java create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreContactDetailsServiceTest.java create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuthFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuthFunctionalTest.java index 4373eebf7..ea4959ee4 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuthFunctionalTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuthFunctionalTest.java @@ -19,6 +19,10 @@ import uk.gov.hmcts.reform.fact.data.api.entities.CourtFacilities; import uk.gov.hmcts.reform.fact.data.api.entities.CourtOpeningHours; import uk.gov.hmcts.reform.fact.data.api.entities.CourtTranslation; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAddress; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.entities.types.AddressType; import uk.gov.hmcts.reform.fact.data.api.entities.types.DayOfTheWeek; @@ -82,6 +86,10 @@ private static UUID createCourtAsAdmin(String name) { return UUID.fromString(createResponse.jsonPath().getString("id")); } + private static UUID createServiceCentreAsAdmin(String name) { + return TestDataHelper.createServiceCentre(http, name, adminToken); + } + private static void assertViewerAllowed(Response adminResponse, Response viewerResponse, String endpoint) { assertThat(adminResponse.statusCode()) .as("Admin status should not be 401 for %s", endpoint) @@ -196,6 +204,140 @@ void courtControllerAuth() { assertUnauthenticated(http.doPost("/courts/v1", testingCourt, ""), "/courts/v1 [POST]"); } + @Test + @DisplayName("Service centre controller endpoints enforce admin-only writes") + void serviceCentreControllerAuth() { + ServiceCentre serviceCentre = TestDataHelper.buildServiceCentre( + http, + "Auth Test Service Centre", + adminToken + ); + + Response createViewer = http.doPost("/service-centres/v1", serviceCentre, viewerToken); + Response createAdmin = http.doPost("/service-centres/v1", serviceCentre, adminToken); + assertViewerForbidden(createViewer, "/service-centres/v1 [POST]"); + assertThat(createAdmin.statusCode()).isEqualTo(201); + + ServiceCentre createdServiceCentre = createAdmin.as(ServiceCentre.class); + UUID serviceCentreId = createdServiceCentre.getId(); + assertThat(createdServiceCentre.getCatchmentType().name()).isEqualTo("NATIONAL"); + + String[] readEndpoints = new String[]{ + "/service-centres/" + serviceCentreId + "/v1" + }; + + for (String endpoint : readEndpoints) { + Response admin = http.doGet(endpoint, adminToken); + Response viewer = http.doGet(endpoint, viewerToken); + assertViewerAllowed(admin, viewer, endpoint); + } + + serviceCentre.setName(TestDataHelper.appendRandomSuffixToCourtName("Auth Test Service Centre Updated")); + Response updateViewer = http.doPut("/service-centres/" + serviceCentreId + "/v1", serviceCentre, viewerToken); + Response updateAdmin = http.doPut("/service-centres/" + serviceCentreId + "/v1", serviceCentre, adminToken); + assertViewerForbidden(updateViewer, "/service-centres/{serviceCentreId}/v1 [PUT]"); + assertThat(updateAdmin.statusCode()).isEqualTo(200); + + assertUnauthenticated( + http.doGet("/service-centres/" + serviceCentreId + "/v1", ""), + "/service-centres/{serviceCentreId}/v1 [GET]" + ); + assertUnauthenticated(http.doPost("/service-centres/v1", serviceCentre, ""), "/service-centres/v1 [POST]"); + } + + @Test + @DisplayName("Service centre child endpoints enforce admin-only writes") + void serviceCentreChildControllerAuth() { + UUID serviceCentreId = createServiceCentreAsAdmin("Auth Test Service Centre Children"); + ServiceCentreAddress address = ServiceCentreAddress.builder() + .serviceCentreId(serviceCentreId) + .addressLine1("1 Auth Street") + .townCity("London") + .postcode("NW10 4DX") + .addressType(AddressType.VISIT_OR_CONTACT_US) + .build(); + + Response createAddressAdmin = + http.doPost("/service-centres/" + serviceCentreId + "/v1/address", address, adminToken); + assertThat(createAddressAdmin.statusCode()).isEqualTo(201); + UUID addressId = UUID.fromString(createAddressAdmin.jsonPath().getString("id")); + + assertViewerAllowed( + http.doGet("/service-centres/" + serviceCentreId + "/v1/address", adminToken), + http.doGet("/service-centres/" + serviceCentreId + "/v1/address", viewerToken), + "/service-centres/{serviceCentreId}/v1/address [GET]" + ); + assertViewerForbidden( + http.doPost("/service-centres/" + serviceCentreId + "/v1/address", address, viewerToken), + "/service-centres/{serviceCentreId}/v1/address [POST]" + ); + assertViewerForbidden( + http.doPut("/service-centres/" + serviceCentreId + "/v1/address/" + addressId, address, viewerToken), + "/service-centres/{serviceCentreId}/v1/address/{addressId} [PUT]" + ); + + UUID contactTypeId = UUID.fromString(http.doGet("/types/v1/contact-description-types", adminToken) + .jsonPath().getString("[0].id")); + ServiceCentreContactDetails contactDetails = ServiceCentreContactDetails.builder() + .serviceCentreId(serviceCentreId) + .serviceCentreContactDescriptionId(contactTypeId) + .explanation("Auth test") + .email("servicecentre-auth@test.gov.uk") + .phoneNumber("01234567890") + .build(); + Response createContactAdmin = http.doPost( + "/service-centres/" + serviceCentreId + "/v1/contact-details", + contactDetails, + adminToken + ); + assertThat(createContactAdmin.statusCode()).isEqualTo(201); + UUID contactId = UUID.fromString(createContactAdmin.jsonPath().getString("id")); + + assertViewerAllowed( + http.doGet("/service-centres/" + serviceCentreId + "/v1/contact-details", adminToken), + http.doGet("/service-centres/" + serviceCentreId + "/v1/contact-details", viewerToken), + "/service-centres/{serviceCentreId}/v1/contact-details [GET]" + ); + assertViewerForbidden( + http.doPut( + "/service-centres/" + serviceCentreId + "/v1/contact-details/" + contactId, + contactDetails, + viewerToken + ), + "/service-centres/{serviceCentreId}/v1/contact-details/{contactId} [PUT]" + ); + + UUID areaOfLawId = UUID.fromString(http.doGet("/types/v1/areas-of-law", adminToken) + .jsonPath().getString("[0].id")); + ServiceCentreAreasOfLaw areasOfLaw = ServiceCentreAreasOfLaw.builder() + .serviceCentreId(serviceCentreId) + .areasOfLaw(List.of(areaOfLawId)) + .build(); + assertThat(http.doPut( + "/service-centres/" + serviceCentreId + "/v1/areas-of-law", + areasOfLaw, + adminToken + ).statusCode()).isEqualTo(201); + assertViewerAllowed( + http.doGet("/service-centres/" + serviceCentreId + "/v1/areas-of-law", adminToken), + http.doGet("/service-centres/" + serviceCentreId + "/v1/areas-of-law", viewerToken), + "/service-centres/{serviceCentreId}/v1/areas-of-law [GET]" + ); + assertViewerForbidden( + http.doPut("/service-centres/" + serviceCentreId + "/v1/areas-of-law", areasOfLaw, viewerToken), + "/service-centres/{serviceCentreId}/v1/areas-of-law [PUT]" + ); + + assertViewerForbidden( + http.doDelete("/service-centres/" + serviceCentreId + "/v1/address/" + addressId, viewerToken), + "/service-centres/{serviceCentreId}/v1/address/{addressId} [DELETE]" + ); + assertViewerForbidden( + http.doDelete("/service-centres/" + serviceCentreId + "/v1/contact-details/" + contactId, viewerToken), + "/service-centres/{serviceCentreId}/v1/contact-details/{contactId} [DELETE]" + ); + } + @Test @DisplayName("Court address endpoints enforce admin-only writes") void courtAddressAuth() { @@ -657,11 +799,18 @@ void testingSupportEndpointAuth() { endpoint = "/testing-support/courts/name-prefix/Auth Test Cleanup"; noToken = http.doDelete(endpoint, ""); assertOpen(noToken, endpoint); + endpoint = "/testing-support/service-centres?serviceCentreName=Auth Test Service Centre Support"; + noToken = http.doGet(endpoint); + assertOpen(noToken, endpoint); + endpoint = "/testing-support/service-centres/name-prefix/Auth Test Service Centre Support"; + noToken = http.doDelete(endpoint, ""); + assertOpen(noToken, endpoint); } @AfterAll static void cleanUp() { http.doDelete("/testing-support/courts/name-prefix/Test Court", adminToken); http.doDelete("/testing-support/courts/name-prefix/Auth Test", adminToken); + http.doDelete("/testing-support/service-centres/name-prefix/Auth Test", adminToken); } } diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreAddressControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreAddressControllerFunctionalTest.java new file mode 100644 index 000000000..a3f11d344 --- /dev/null +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreAddressControllerFunctionalTest.java @@ -0,0 +1,68 @@ +package uk.gov.hmcts.reform.fact.functional.controllers; + +import io.qameta.allure.Feature; +import io.restassured.response.Response; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAddress; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AddressType; +import uk.gov.hmcts.reform.fact.functional.helpers.AssertionHelper; +import uk.gov.hmcts.reform.fact.functional.helpers.TestDataHelper; +import uk.gov.hmcts.reform.fact.functional.http.HttpClient; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static org.springframework.http.HttpStatus.OK; + +@Feature("Service Centre Address Controller") +@DisplayName("Service Centre Address Controller") +public final class ServiceCentreAddressControllerFunctionalTest { + + private static final HttpClient http = new HttpClient(); + private static final String TEST_PREFIX = "Test Service Centre Address"; + + @Test + @DisplayName("Service centre address endpoints support CRUD") + void shouldCreateReadUpdateAndDeleteServiceCentreAddress() { + UUID serviceCentreId = TestDataHelper.createServiceCentre(http, TEST_PREFIX); + ServiceCentreAddress address = ServiceCentreAddress.builder() + .serviceCentreId(serviceCentreId) + .addressLine1("1 Test Street") + .townCity("London") + .postcode("SW1A 1AA") + .addressType(AddressType.VISIT_OR_CONTACT_US) + .build(); + + Response createResponse = http.doPost("/service-centres/" + serviceCentreId + "/v1/address", address); + AssertionHelper.assertStatus(createResponse, CREATED); + UUID addressId = UUID.fromString(createResponse.jsonPath().getString("id")); + + AssertionHelper.assertStatus(http.doGet("/service-centres/" + serviceCentreId + "/v1/address"), OK); + + Response getResponse = http.doGet("/service-centres/" + serviceCentreId + "/v1/address/" + addressId); + AssertionHelper.assertStatus(getResponse, OK); + assertThat(getResponse.jsonPath().getString("addressLine1")).isEqualTo("1 Test Street"); + + address.setAddressLine1("2 Updated Street"); + Response updateResponse = http.doPut( + "/service-centres/" + serviceCentreId + "/v1/address/" + addressId, + address + ); + AssertionHelper.assertStatus(updateResponse, OK); + assertThat(updateResponse.jsonPath().getString("addressLine1")).isEqualTo("2 Updated Street"); + + AssertionHelper.assertStatus( + http.doDelete("/service-centres/" + serviceCentreId + "/v1/address/" + addressId), + NO_CONTENT + ); + } + + @AfterAll + static void cleanUpTestData() { + http.doDelete("/testing-support/service-centres/name-prefix/" + TEST_PREFIX); + } +} diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreAreasOfLawControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreAreasOfLawControllerFunctionalTest.java new file mode 100644 index 000000000..a0f1c6b48 --- /dev/null +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreAreasOfLawControllerFunctionalTest.java @@ -0,0 +1,52 @@ +package uk.gov.hmcts.reform.fact.functional.controllers; + +import io.qameta.allure.Feature; +import io.restassured.response.Response; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.functional.helpers.AssertionHelper; +import uk.gov.hmcts.reform.fact.functional.helpers.TestDataHelper; +import uk.gov.hmcts.reform.fact.functional.http.HttpClient; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.OK; + +@Feature("Service Centre Areas Of Law Controller") +@DisplayName("Service Centre Areas Of Law Controller") +public final class ServiceCentreAreasOfLawControllerFunctionalTest { + + private static final HttpClient http = new HttpClient(); + private static final String TEST_PREFIX = "Test Service Centre Areas Of Law"; + + @Test + @DisplayName("Service centre areas of law endpoints support get and update") + void shouldSetAndGetServiceCentreAreasOfLaw() { + UUID serviceCentreId = TestDataHelper.createServiceCentre(http, TEST_PREFIX); + UUID areaOfLawId = UUID.fromString(http.doGet("/types/v1/areas-of-law").jsonPath().getString("[0].id")); + ServiceCentreAreasOfLaw areasOfLaw = ServiceCentreAreasOfLaw.builder() + .serviceCentreId(serviceCentreId) + .areasOfLaw(List.of(areaOfLawId)) + .build(); + + Response updateResponse = http.doPut( + "/service-centres/" + serviceCentreId + "/v1/areas-of-law", + areasOfLaw + ); + AssertionHelper.assertStatus(updateResponse, CREATED); + + Response getResponse = http.doGet("/service-centres/" + serviceCentreId + "/v1/areas-of-law"); + AssertionHelper.assertStatus(getResponse, OK); + assertThat(getResponse.jsonPath().getMap("$")).isNotEmpty(); + } + + @AfterAll + static void cleanUpTestData() { + http.doDelete("/testing-support/service-centres/name-prefix/" + TEST_PREFIX); + } +} diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreContactDetailsControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreContactDetailsControllerFunctionalTest.java new file mode 100644 index 000000000..8ae1b0acf --- /dev/null +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreContactDetailsControllerFunctionalTest.java @@ -0,0 +1,75 @@ +package uk.gov.hmcts.reform.fact.functional.controllers; + +import io.qameta.allure.Feature; +import io.restassured.response.Response; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; +import uk.gov.hmcts.reform.fact.functional.helpers.AssertionHelper; +import uk.gov.hmcts.reform.fact.functional.helpers.TestDataHelper; +import uk.gov.hmcts.reform.fact.functional.http.HttpClient; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static org.springframework.http.HttpStatus.OK; + +@Feature("Service Centre Contact Details Controller") +@DisplayName("Service Centre Contact Details Controller") +public final class ServiceCentreContactDetailsControllerFunctionalTest { + + private static final HttpClient http = new HttpClient(); + private static final String TEST_PREFIX = "Test Service Centre Contact"; + + @Test + @DisplayName("Service centre contact details endpoints support CRUD") + void shouldCreateReadUpdateAndDeleteServiceCentreContactDetails() { + UUID serviceCentreId = TestDataHelper.createServiceCentre(http, TEST_PREFIX); + UUID contactDescriptionId = UUID.fromString( + http.doGet("/types/v1/contact-description-types").jsonPath().getString("[0].id") + ); + ServiceCentreContactDetails contactDetails = ServiceCentreContactDetails.builder() + .serviceCentreId(serviceCentreId) + .serviceCentreContactDescriptionId(contactDescriptionId) + .explanation("General enquiries") + .email("servicecentre@example.com") + .phoneNumber("01234567890") + .build(); + + Response createResponse = http.doPost( + "/service-centres/" + serviceCentreId + "/v1/contact-details", + contactDetails + ); + AssertionHelper.assertStatus(createResponse, CREATED); + UUID contactId = UUID.fromString(createResponse.jsonPath().getString("id")); + + AssertionHelper.assertStatus(http.doGet("/service-centres/" + serviceCentreId + "/v1/contact-details"), OK); + + Response getResponse = http.doGet( + "/service-centres/" + serviceCentreId + "/v1/contact-details/" + contactId + ); + AssertionHelper.assertStatus(getResponse, OK); + assertThat(getResponse.jsonPath().getString("email")).isEqualTo("servicecentre@example.com"); + + contactDetails.setEmail("updated-servicecentre@example.com"); + Response updateResponse = http.doPut( + "/service-centres/" + serviceCentreId + "/v1/contact-details/" + contactId, + contactDetails + ); + AssertionHelper.assertStatus(updateResponse, OK); + assertThat(updateResponse.jsonPath().getString("email")).isEqualTo("updated-servicecentre@example.com"); + + AssertionHelper.assertStatus( + http.doDelete("/service-centres/" + serviceCentreId + "/v1/contact-details/" + contactId), + NO_CONTENT + ); + } + + @AfterAll + static void cleanUpTestData() { + http.doDelete("/testing-support/service-centres/name-prefix/" + TEST_PREFIX); + } +} diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreControllerFunctionalTest.java new file mode 100644 index 000000000..83844d435 --- /dev/null +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreControllerFunctionalTest.java @@ -0,0 +1,69 @@ +package uk.gov.hmcts.reform.fact.functional.controllers; + +import io.qameta.allure.Feature; +import io.restassured.response.Response; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.functional.helpers.AssertionHelper; +import uk.gov.hmcts.reform.fact.functional.helpers.TestDataHelper; +import uk.gov.hmcts.reform.fact.functional.http.HttpClient; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.OK; + +@Feature("Service Centre Controller") +@DisplayName("Service Centre Controller") +public final class ServiceCentreControllerFunctionalTest { + + private static final HttpClient http = new HttpClient(); + private static final String TEST_PREFIX = "Test Service Centre"; + + @Test + @DisplayName("Service centre top-level endpoints create, read, update and name") + void shouldCreateReadAndUpdateServiceCentre() { + ServiceCentre serviceCentre = TestDataHelper.buildServiceCentre(http, TEST_PREFIX + " Top Level"); + + Response createResponse = http.doPost("/service-centres/v1", serviceCentre); + AssertionHelper.assertStatus(createResponse, CREATED); + + UUID serviceCentreId = UUID.fromString(createResponse.jsonPath().getString("id")); + assertThat(createResponse.jsonPath().getBoolean("open")).isFalse(); + assertThat(createResponse.jsonPath().getString("catchmentType")).isEqualTo(CatchmentType.NATIONAL.name()); + + Response getResponse = http.doGet("/service-centres/" + serviceCentreId + "/v1"); + AssertionHelper.assertStatus(getResponse, OK); + String createdName = createResponse.jsonPath().getString("name"); + assertThat(getResponse.jsonPath().getString("name")).isEqualTo(createdName); + + Response nameResponse = http.doGet("/service-centres/name/v1", Map.of("name", createdName)); + AssertionHelper.assertStatus(nameResponse, OK); + assertThat(UUID.fromString(nameResponse.jsonPath().getString("id"))).isEqualTo(serviceCentreId); + + serviceCentre.setName(TestDataHelper.appendRandomSuffixToCourtName(TEST_PREFIX + " Updated")); + serviceCentre.setOpen(true); + serviceCentre.setWarningNotice("Updated warning notice"); + serviceCentre.setCatchmentType(CatchmentType.NATIONAL); + + Response updateResponse = http.doPut("/service-centres/" + serviceCentreId + "/v1", serviceCentre); + AssertionHelper.assertStatus(updateResponse, OK); + assertThat(updateResponse.jsonPath().getBoolean("open")).isTrue(); + assertThat(updateResponse.jsonPath().getString("warningNotice")).isEqualTo("Updated warning notice"); + assertThat(updateResponse.jsonPath().getString("catchmentType")).isEqualTo(CatchmentType.NATIONAL.name()); + + Response missingResponse = http.doGet("/service-centres/" + UUID.randomUUID() + "/v1"); + AssertionHelper.assertStatus(missingResponse, NOT_FOUND); + } + + @AfterAll + static void cleanUpTestData() { + http.doDelete("/testing-support/service-centres/name-prefix/" + TEST_PREFIX); + } +} diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/helpers/TestDataHelper.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/helpers/TestDataHelper.java index b3814b765..cfc31a467 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/helpers/TestDataHelper.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/helpers/TestDataHelper.java @@ -7,7 +7,9 @@ import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.CourtFacilities; import uk.gov.hmcts.reform.fact.data.api.entities.CourtLock; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; import uk.gov.hmcts.reform.fact.data.api.entities.User; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; import uk.gov.hmcts.reform.fact.data.api.entities.types.UserRole; import uk.gov.hmcts.reform.fact.data.api.os.OsData; @@ -165,6 +167,68 @@ public static UUID createCourt( return UUID.fromString(createResponse.jsonPath().getString("id")); } + /** + * Builds a test service centre with the given name. + * + * @param http the HTTP client + * @param serviceCentreName the name for the test service centre + * @return the service centre body + */ + public static ServiceCentre buildServiceCentre(final HttpClient http, final String serviceCentreName) { + return buildServiceCentre(http, serviceCentreName, HttpClient.getAdminBearerToken()); + } + + /** + * Builds a test service centre with the given name. + * + * @param http the HTTP client + * @param serviceCentreName the name for the test service centre + * @param bearerToken the bearer token to use when fetching reference data + * @return the service centre body + */ + public static ServiceCentre buildServiceCentre(final HttpClient http, + final String serviceCentreName, + final String bearerToken) { + final ServiceCentre serviceCentre = new ServiceCentre(); + serviceCentre.setName(appendRandomSuffixToCourtName(serviceCentreName)); + serviceCentre.setOpen(false); + serviceCentre.setServiceAreaIds(List.of(UUID.fromString( + http.doGet("/types/v1/service-areas", bearerToken).jsonPath().getString("[0].id") + ))); + serviceCentre.setCatchmentType(CatchmentType.NATIONAL); + return serviceCentre; + } + + /** + * Creates a test service centre with the given name. + * + * @param http the HTTP client + * @param serviceCentreName the name for the test service centre + * @return the created service centre UUID + */ + public static UUID createServiceCentre(final HttpClient http, final String serviceCentreName) { + return createServiceCentre(http, serviceCentreName, HttpClient.getAdminBearerToken()); + } + + /** + * Creates a test service centre with the given name. + * + * @param http the HTTP client + * @param serviceCentreName the name for the test service centre + * @param bearerToken the bearer token to use when creating the service centre + * @return the created service centre UUID + */ + public static UUID createServiceCentre(final HttpClient http, + final String serviceCentreName, + final String bearerToken) { + final ServiceCentre serviceCentre = buildServiceCentre(http, serviceCentreName, bearerToken); + + final Response createResponse = http.doPost("/service-centres/v1", serviceCentre, bearerToken); + assertThat(createResponse.statusCode()).isEqualTo(CREATED.value()); + + return UUID.fromString(createResponse.jsonPath().getString("id")); + } + /** * Builds a CourtFacilities object with all fields set to true by default. * diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAddressControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAddressControllerTest.java new file mode 100644 index 000000000..1b3797dfc --- /dev/null +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAddressControllerTest.java @@ -0,0 +1,142 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers; + +import io.qameta.allure.Feature; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import tools.jackson.databind.ObjectMapper; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAddress; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AddressType; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreAddressService; + +import java.util.List; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Feature("Service Centre Address Controller") +@DisplayName("Service Centre Address Controller") +@WebMvcTest(ServiceCentreAddressController.class) +@AutoConfigureMockMvc(addFilters = false) +@ActiveProfiles("test") +class ServiceCentreAddressControllerTest { + + private static final UUID SERVICE_CENTRE_ID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + private static final UUID ADDRESS_ID = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private ServiceCentreAddressService serviceCentreAddressService; + + @Test + void getAddressesReturnsAddresses() throws Exception { + ServiceCentreAddress address = buildAddress(); + when(serviceCentreAddressService.getAddresses(SERVICE_CENTRE_ID)).thenReturn(List.of(address)); + + mockMvc.perform(get("/service-centres/{serviceCentreId}/v1/address", SERVICE_CENTRE_ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(ADDRESS_ID.toString())); + } + + @Test + void getAddressReturnsAddress() throws Exception { + ServiceCentreAddress address = buildAddress(); + when(serviceCentreAddressService.getAddress(SERVICE_CENTRE_ID, ADDRESS_ID)).thenReturn(address); + + mockMvc.perform(get("/service-centres/{serviceCentreId}/v1/address/{addressId}", + SERVICE_CENTRE_ID, + ADDRESS_ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(ADDRESS_ID.toString())); + } + + @Test + void getAddressReturnsBadRequestForInvalidUuid() throws Exception { + mockMvc.perform(get("/service-centres/{serviceCentreId}/v1/address/{addressId}", + SERVICE_CENTRE_ID, + "invalid-uuid")) + .andExpect(status().isBadRequest()); + } + + @Test + void postAddressCreatesAddress() throws Exception { + ServiceCentreAddress address = buildAddress(); + when(serviceCentreAddressService.createAddress(eq(SERVICE_CENTRE_ID), any(ServiceCentreAddress.class))) + .thenReturn(address); + + mockMvc.perform(post("/service-centres/{serviceCentreId}/v1/address", SERVICE_CENTRE_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(address))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(ADDRESS_ID.toString())); + } + + @Test + void putAddressUpdatesAddress() throws Exception { + ServiceCentreAddress address = buildAddress(); + when(serviceCentreAddressService.updateAddress( + eq(SERVICE_CENTRE_ID), + eq(ADDRESS_ID), + any(ServiceCentreAddress.class) + )).thenReturn(address); + + mockMvc.perform(put("/service-centres/{serviceCentreId}/v1/address/{addressId}", + SERVICE_CENTRE_ID, + ADDRESS_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(address))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(ADDRESS_ID.toString())); + } + + @Test + void deleteAddressReturnsNoContent() throws Exception { + mockMvc.perform(delete("/service-centres/{serviceCentreId}/v1/address/{addressId}", + SERVICE_CENTRE_ID, + ADDRESS_ID)) + .andExpect(status().isNoContent()); + } + + @Test + void deleteAddressReturnsNotFound() throws Exception { + doThrow(new NotFoundException("Missing")) + .when(serviceCentreAddressService).deleteAddress(SERVICE_CENTRE_ID, ADDRESS_ID); + + mockMvc.perform(delete("/service-centres/{serviceCentreId}/v1/address/{addressId}", + SERVICE_CENTRE_ID, + ADDRESS_ID)) + .andExpect(status().isNotFound()); + } + + private ServiceCentreAddress buildAddress() { + return ServiceCentreAddress.builder() + .id(ADDRESS_ID) + .serviceCentreId(SERVICE_CENTRE_ID) + .addressLine1("1 Test Street") + .townCity("London") + .postcode("SW1A 1AA") + .addressType(AddressType.VISIT_OR_CONTACT_US) + .build(); + } +} diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAreasOfLawControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAreasOfLawControllerTest.java new file mode 100644 index 000000000..bd7606622 --- /dev/null +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAreasOfLawControllerTest.java @@ -0,0 +1,99 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers; + +import io.qameta.allure.Feature; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import tools.jackson.databind.ObjectMapper; +import uk.gov.hmcts.reform.fact.data.api.entities.AreaOfLawType; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreAreasOfLawService; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Feature("Service Centre Areas Of Law Controller") +@DisplayName("Service Centre Areas Of Law Controller") +@WebMvcTest(ServiceCentreAreasOfLawController.class) +@AutoConfigureMockMvc(addFilters = false) +@ActiveProfiles("test") +class ServiceCentreAreasOfLawControllerTest { + + private static final UUID SERVICE_CENTRE_ID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + private static final UUID AREA_OF_LAW_ID = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private ServiceCentreAreasOfLawService serviceCentreAreasOfLawService; + + @Test + void getAreasOfLawReturnsMap() throws Exception { + AreaOfLawType areaOfLawType = AreaOfLawType.builder() + .id(AREA_OF_LAW_ID) + .name("Civil") + .displayName("Civil") + .build(); + + when(serviceCentreAreasOfLawService.getAreasOfLawStatusByServiceCentreId(SERVICE_CENTRE_ID)) + .thenReturn(Map.of(areaOfLawType, true)); + + mockMvc.perform(get("/service-centres/{serviceCentreId}/v1/areas-of-law", SERVICE_CENTRE_ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.*").exists()); + } + + @Test + void getAreasOfLawReturnsBadRequestForInvalidUuid() throws Exception { + mockMvc.perform(get("/service-centres/{serviceCentreId}/v1/areas-of-law", "invalid-uuid")) + .andExpect(status().isBadRequest()); + } + + @Test + void getAreasOfLawReturnsNotFoundWhenMissing() throws Exception { + when(serviceCentreAreasOfLawService.getAreasOfLawStatusByServiceCentreId(SERVICE_CENTRE_ID)) + .thenThrow(new NotFoundException("Missing")); + + mockMvc.perform(get("/service-centres/{serviceCentreId}/v1/areas-of-law", SERVICE_CENTRE_ID)) + .andExpect(status().isNotFound()); + } + + @Test + void putAreasOfLawReturnsCreated() throws Exception { + ServiceCentreAreasOfLaw areasOfLaw = ServiceCentreAreasOfLaw.builder() + .serviceCentreId(SERVICE_CENTRE_ID) + .areasOfLaw(List.of(AREA_OF_LAW_ID)) + .build(); + + when(serviceCentreAreasOfLawService.setServiceCentreAreasOfLaw( + eq(SERVICE_CENTRE_ID), + any(ServiceCentreAreasOfLaw.class) + )).thenReturn(areasOfLaw); + + mockMvc.perform(put("/service-centres/{serviceCentreId}/v1/areas-of-law", SERVICE_CENTRE_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(areasOfLaw))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.serviceCentreId").value(SERVICE_CENTRE_ID.toString())); + } +} diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreContactDetailsControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreContactDetailsControllerTest.java new file mode 100644 index 000000000..2baa81b39 --- /dev/null +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreContactDetailsControllerTest.java @@ -0,0 +1,144 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers; + +import io.qameta.allure.Feature; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import tools.jackson.databind.ObjectMapper; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreContactDetailsService; + +import java.util.List; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Feature("Service Centre Contact Details Controller") +@DisplayName("Service Centre Contact Details Controller") +@WebMvcTest(ServiceCentreContactDetailsController.class) +@AutoConfigureMockMvc(addFilters = false) +@ActiveProfiles("test") +class ServiceCentreContactDetailsControllerTest { + + private static final UUID SERVICE_CENTRE_ID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + private static final UUID CONTACT_ID = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private ServiceCentreContactDetailsService serviceCentreContactDetailsService; + + @Test + void getContactDetailsReturnsContactDetails() throws Exception { + ServiceCentreContactDetails contactDetails = buildContactDetails(); + when(serviceCentreContactDetailsService.getContactDetails(SERVICE_CENTRE_ID)) + .thenReturn(List.of(contactDetails)); + + mockMvc.perform(get("/service-centres/{serviceCentreId}/v1/contact-details", SERVICE_CENTRE_ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(CONTACT_ID.toString())); + } + + @Test + void getContactDetailReturnsContactDetail() throws Exception { + ServiceCentreContactDetails contactDetails = buildContactDetails(); + when(serviceCentreContactDetailsService.getContactDetail(SERVICE_CENTRE_ID, CONTACT_ID)) + .thenReturn(contactDetails); + + mockMvc.perform(get("/service-centres/{serviceCentreId}/v1/contact-details/{contactId}", + SERVICE_CENTRE_ID, + CONTACT_ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(CONTACT_ID.toString())); + } + + @Test + void getContactDetailReturnsBadRequestForInvalidUuid() throws Exception { + mockMvc.perform(get("/service-centres/{serviceCentreId}/v1/contact-details/{contactId}", + SERVICE_CENTRE_ID, + "invalid-uuid")) + .andExpect(status().isBadRequest()); + } + + @Test + void postContactDetailCreatesContactDetail() throws Exception { + ServiceCentreContactDetails contactDetails = buildContactDetails(); + when(serviceCentreContactDetailsService.createContactDetail( + eq(SERVICE_CENTRE_ID), + any(ServiceCentreContactDetails.class) + )).thenReturn(contactDetails); + + mockMvc.perform(post("/service-centres/{serviceCentreId}/v1/contact-details", SERVICE_CENTRE_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(contactDetails))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(CONTACT_ID.toString())); + } + + @Test + void putContactDetailUpdatesContactDetail() throws Exception { + ServiceCentreContactDetails contactDetails = buildContactDetails(); + when(serviceCentreContactDetailsService.updateContactDetail( + eq(SERVICE_CENTRE_ID), + eq(CONTACT_ID), + any(ServiceCentreContactDetails.class) + )).thenReturn(contactDetails); + + mockMvc.perform(put("/service-centres/{serviceCentreId}/v1/contact-details/{contactId}", + SERVICE_CENTRE_ID, + CONTACT_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(contactDetails))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(CONTACT_ID.toString())); + } + + @Test + void deleteContactDetailReturnsNoContent() throws Exception { + mockMvc.perform(delete("/service-centres/{serviceCentreId}/v1/contact-details/{contactId}", + SERVICE_CENTRE_ID, + CONTACT_ID)) + .andExpect(status().isNoContent()); + } + + @Test + void deleteContactDetailReturnsNotFound() throws Exception { + doThrow(new NotFoundException("Missing")) + .when(serviceCentreContactDetailsService).deleteContactDetail(SERVICE_CENTRE_ID, CONTACT_ID); + + mockMvc.perform(delete("/service-centres/{serviceCentreId}/v1/contact-details/{contactId}", + SERVICE_CENTRE_ID, + CONTACT_ID)) + .andExpect(status().isNotFound()); + } + + private ServiceCentreContactDetails buildContactDetails() { + return ServiceCentreContactDetails.builder() + .id(CONTACT_ID) + .serviceCentreId(SERVICE_CENTRE_ID) + .explanation("General enquiries") + .email("test@example.com") + .phoneNumber("020 123 456") + .build(); + } +} diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java new file mode 100644 index 000000000..4736e23d7 --- /dev/null +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java @@ -0,0 +1,130 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers; + +import io.qameta.allure.Feature; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import tools.jackson.databind.ObjectMapper; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreService; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Feature("Service Centre Controller") +@DisplayName("Service Centre Controller") +@WebMvcTest(ServiceCentreController.class) +@AutoConfigureMockMvc(addFilters = false) +@ActiveProfiles("test") +class ServiceCentreControllerTest { + + private static final UUID SERVICE_CENTRE_ID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + private static final String SERVICE_CENTRE_NAME = "Test Service Centre"; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private ServiceCentreService serviceCentreService; + + @Test + @DisplayName("GET /service-centres/{serviceCentreId}/v1 returns service centre") + void getServiceCentreByIdReturnsServiceCentre() throws Exception { + ServiceCentre serviceCentre = buildServiceCentre(); + + when(serviceCentreService.getServiceCentreById(SERVICE_CENTRE_ID)).thenReturn(serviceCentre); + + mockMvc.perform(get("/service-centres/{serviceCentreId}/v1", SERVICE_CENTRE_ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(SERVICE_CENTRE_ID.toString())) + .andExpect(jsonPath("$.name").value(SERVICE_CENTRE_NAME)); + } + + @Test + @DisplayName("GET /service-centres/{serviceCentreId}/v1 returns 400 for invalid UUID") + void getServiceCentreByIdReturnsBadRequestForInvalidUuid() throws Exception { + mockMvc.perform(get("/service-centres/{serviceCentreId}/v1", "invalid-uuid")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("GET /service-centres/{serviceCentreId}/v1 returns 404 when missing") + void getServiceCentreByIdReturnsNotFound() throws Exception { + when(serviceCentreService.getServiceCentreById(SERVICE_CENTRE_ID)) + .thenThrow(new NotFoundException("Service centre not found")); + + mockMvc.perform(get("/service-centres/{serviceCentreId}/v1", SERVICE_CENTRE_ID)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("GET /service-centres/name/v1 returns service centre for exact name") + void getServiceCentreByNameReturnsServiceCentre() throws Exception { + ServiceCentre serviceCentre = buildServiceCentre(); + + when(serviceCentreService.getServiceCentreByName(SERVICE_CENTRE_NAME)).thenReturn(serviceCentre); + + mockMvc.perform(get("/service-centres/name/v1").param("name", SERVICE_CENTRE_NAME)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(SERVICE_CENTRE_ID.toString())) + .andExpect(jsonPath("$.name").value(SERVICE_CENTRE_NAME)); + } + + @Test + @DisplayName("POST /service-centres/v1 creates service centre") + void createServiceCentreReturnsCreated() throws Exception { + ServiceCentre serviceCentre = buildServiceCentre(); + + when(serviceCentreService.createServiceCentre(any(ServiceCentre.class))).thenReturn(serviceCentre); + + mockMvc.perform(post("/service-centres/v1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(serviceCentre))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(SERVICE_CENTRE_ID.toString())); + } + + @Test + @DisplayName("PUT /service-centres/{serviceCentreId}/v1 updates service centre") + void updateServiceCentreReturnsUpdated() throws Exception { + ServiceCentre serviceCentre = buildServiceCentre(); + + when(serviceCentreService.updateServiceCentre(eq(SERVICE_CENTRE_ID), any(ServiceCentre.class))) + .thenReturn(serviceCentre); + + mockMvc.perform(put("/service-centres/{serviceCentreId}/v1", SERVICE_CENTRE_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(serviceCentre))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(SERVICE_CENTRE_ID.toString())); + } + + private ServiceCentre buildServiceCentre() { + return ServiceCentre.builder() + .id(SERVICE_CENTRE_ID) + .name(SERVICE_CENTRE_NAME) + .slug("test-service-centre") + .open(true) + .catchmentType(CatchmentType.REGIONAL) + .build(); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAddressController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAddressController.java new file mode 100644 index 000000000..88a096f62 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAddressController.java @@ -0,0 +1,149 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAddress; +import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreAddressService; +import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidUUID; + +import java.util.List; +import java.util.UUID; + +@SecuredFactRestController( + name = "Service Centre Addresses", + description = "Operations related to addresses available for service centres" +) +@RequestMapping("/service-centres/{serviceCentreId}") +@RequiredArgsConstructor +@SuppressWarnings("java:S4684") +public class ServiceCentreAddressController { + + private final ServiceCentreAddressService serviceCentreAddressService; + + @GetMapping("/v1/address") + @Operation( + summary = "Get addresses for a service centre", + description = "Fetch all addresses associated with the supplied service centre ID." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved addresses"), + @ApiResponse(responseCode = "400", description = "Invalid service centre ID supplied"), + @ApiResponse(responseCode = "404", description = "Service centre not found") + }) + public ResponseEntity> getAddresses( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId + ) { + return ResponseEntity.ok( + serviceCentreAddressService.getAddresses(UUID.fromString(serviceCentreId)) + ); + } + + @GetMapping("/v1/address/{addressId}") + @Operation( + summary = "Get a specific address for a service centre", + description = "Fetch a single address by service centre and address identifier." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved the address"), + @ApiResponse(responseCode = "400", description = "Invalid service centre ID or address ID supplied"), + @ApiResponse(responseCode = "404", description = "Address or service centre not found") + }) + public ResponseEntity getAddress( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId, + @Parameter(description = "UUID of the address", required = true) @ValidUUID @PathVariable String addressId + ) { + return ResponseEntity.ok( + serviceCentreAddressService.getAddress( + UUID.fromString(serviceCentreId), + UUID.fromString(addressId) + ) + ); + } + + @PostMapping("/v1/address") + @Operation( + summary = "Create address for a service centre", + description = "Create a new address entry for the supplied service centre." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully created the address"), + @ApiResponse(responseCode = "400", description = "Invalid service centre ID supplied or invalid request body"), + @ApiResponse(responseCode = "404", description = "Service centre not found") + }) + @PreAuthorize("@authService.isAdmin()") + public ResponseEntity createAddress( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId, + @Parameter(description = "Address to create", required = true) + @Valid @RequestBody ServiceCentreAddress serviceCentreAddress + ) { + return ResponseEntity + .status(HttpStatus.CREATED) + .body(serviceCentreAddressService.createAddress(UUID.fromString(serviceCentreId), serviceCentreAddress)); + } + + @PutMapping("/v1/address/{addressId}") + @Operation( + summary = "Update address for a service centre", + description = "Update an existing address entry for the supplied service centre." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully updated the address"), + @ApiResponse(responseCode = "400", + description = "Invalid service centre ID or address ID supplied, or invalid request body"), + @ApiResponse(responseCode = "404", description = "Address or service centre not found") + }) + @PreAuthorize("@authService.isAdmin()") + public ResponseEntity updateServiceCentreAddress( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId, + @Parameter(description = "UUID of the address", required = true) @ValidUUID @PathVariable String addressId, + @Parameter(description = "Updated address", required = true) + @Valid @RequestBody ServiceCentreAddress serviceCentreAddress + ) { + return ResponseEntity.ok( + serviceCentreAddressService.updateAddress( + UUID.fromString(serviceCentreId), + UUID.fromString(addressId), + serviceCentreAddress + ) + ); + } + + @DeleteMapping("/v1/address/{addressId}") + @Operation( + summary = "Delete address for a service centre", + description = "Remove an existing address entry for the supplied service centre." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Successfully deleted the address"), + @ApiResponse(responseCode = "400", description = "Invalid service centre ID or address ID supplied"), + @ApiResponse(responseCode = "404", description = "Address or service centre not found") + }) + @PreAuthorize("@authService.isAdmin()") + public ResponseEntity deleteServiceCentreAddress( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId, + @Parameter(description = "UUID of the address", required = true) @ValidUUID @PathVariable String addressId + ) { + serviceCentreAddressService.deleteAddress(UUID.fromString(serviceCentreId), UUID.fromString(addressId)); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAreasOfLawController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAreasOfLawController.java new file mode 100644 index 000000000..f1b7ddca9 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAreasOfLawController.java @@ -0,0 +1,77 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import uk.gov.hmcts.reform.fact.data.api.entities.AreaOfLawType; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreAreasOfLawService; +import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidUUID; + +import java.util.Map; +import java.util.UUID; + +@SecuredFactRestController( + name = "Service Centre Areas Of Law", + description = "Operations related to Service Centre Areas Of Law" +) +@RequestMapping("/service-centres/{serviceCentreId}") +@RequiredArgsConstructor +public class ServiceCentreAreasOfLawController { + + private final ServiceCentreAreasOfLawService serviceCentreAreasOfLawService; + + @GetMapping("/v1/areas-of-law") + @Operation( + summary = "Get Areas Of Law map with boolean by service centre ID", + description = "Fetch Areas Of Law with availability boolean for a given service centre." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved Areas Of Law"), + @ApiResponse(responseCode = "400", description = "Invalid service centre ID supplied"), + @ApiResponse(responseCode = "404", description = "Service centre not found") + }) + public ResponseEntity> getAreasOfLawByServiceCentreId( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId) { + return ResponseEntity.ok( + serviceCentreAreasOfLawService.getAreasOfLawStatusByServiceCentreId(UUID.fromString(serviceCentreId)) + ); + } + + @PutMapping("/v1/areas-of-law") + @Operation( + summary = "Create or update Service Centre Areas Of Law for a service centre", + description = "Creates new Service Centre Areas Of Law for a service centre or updates the existing one." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully created/updated Service Centre Areas Of Law"), + @ApiResponse(responseCode = "400", description = "Invalid service centre ID supplied or invalid request body"), + @ApiResponse(responseCode = "404", description = "Service centre not found") + }) + @PreAuthorize("@authService.isAdmin()") + public ResponseEntity setAreasOfLawServices( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId, + @Parameter(description = "AreasOfLaw object to create or update", required = true) + @Valid @RequestBody ServiceCentreAreasOfLaw serviceCentreAreasOfLaw) { + return ResponseEntity + .status(HttpStatus.CREATED) + .body(serviceCentreAreasOfLawService.setServiceCentreAreasOfLaw( + UUID.fromString(serviceCentreId), + serviceCentreAreasOfLaw + )); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreContactDetailsController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreContactDetailsController.java new file mode 100644 index 000000000..38eef9cd2 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreContactDetailsController.java @@ -0,0 +1,155 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; +import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreContactDetailsService; +import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidUUID; + +import java.util.List; +import java.util.UUID; + +@SecuredFactRestController( + name = "Service Centre Contact Details", + description = "Operations related to contact details available for service centres" +) +@RequestMapping("/service-centres/{serviceCentreId}") +@RequiredArgsConstructor +public class ServiceCentreContactDetailsController { + + private final ServiceCentreContactDetailsService serviceCentreContactDetailsService; + + @GetMapping("/v1/contact-details") + @Operation( + summary = "Get contact details for a service centre", + description = "Fetch all contact details associated with the supplied service centre ID." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved contact details"), + @ApiResponse(responseCode = "400", description = "Invalid service centre ID supplied"), + @ApiResponse(responseCode = "404", description = "Service centre not found") + }) + public ResponseEntity> getContactDetails( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId + ) { + return ResponseEntity.ok( + serviceCentreContactDetailsService.getContactDetails(UUID.fromString(serviceCentreId)) + ); + } + + @GetMapping("/v1/contact-details/{contactId}") + @Operation( + summary = "Get a specific contact detail for a service centre", + description = "Fetch a single contact detail by service centre and contact identifier." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved the contact detail"), + @ApiResponse(responseCode = "400", description = "Invalid service centre ID or contact ID supplied"), + @ApiResponse(responseCode = "404", description = "Contact detail or service centre not found") + }) + public ResponseEntity getContactDetail( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId, + @Parameter(description = "UUID of the contact", required = true) @ValidUUID @PathVariable String contactId + ) { + return ResponseEntity.ok( + serviceCentreContactDetailsService.getContactDetail( + UUID.fromString(serviceCentreId), + UUID.fromString(contactId) + ) + ); + } + + @PostMapping("/v1/contact-details") + @Operation( + summary = "Create contact details for a service centre", + description = "Create a new contact detail entry for the supplied service centre." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully created the contact detail"), + @ApiResponse(responseCode = "400", description = "Invalid service centre ID supplied or invalid request body"), + @ApiResponse(responseCode = "404", description = "Service centre or contact description type not found") + }) + @PreAuthorize("@authService.isAdmin()") + public ResponseEntity createContactDetail( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId, + @Parameter(description = "Contact detail to create", required = true) + @Valid @RequestBody ServiceCentreContactDetails serviceCentreContactDetails + ) { + return ResponseEntity + .status(HttpStatus.CREATED) + .body(serviceCentreContactDetailsService.createContactDetail( + UUID.fromString(serviceCentreId), + serviceCentreContactDetails + )); + } + + @PutMapping("/v1/contact-details/{contactId}") + @Operation( + summary = "Update contact details for a service centre", + description = "Update an existing contact detail entry for the supplied service centre." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully updated the contact detail"), + @ApiResponse(responseCode = "400", + description = "Invalid service centre ID or contact ID supplied, or invalid request body"), + @ApiResponse(responseCode = "404", + description = "Contact detail, service centre, or contact description type not found") + }) + @PreAuthorize("@authService.isAdmin()") + public ResponseEntity updateContactDetail( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId, + @Parameter(description = "UUID of the contact", required = true) @ValidUUID @PathVariable String contactId, + @Parameter(description = "Updated contact detail", required = true) + @Valid @RequestBody ServiceCentreContactDetails serviceCentreContactDetails + ) { + return ResponseEntity.ok( + serviceCentreContactDetailsService.updateContactDetail( + UUID.fromString(serviceCentreId), + UUID.fromString(contactId), + serviceCentreContactDetails + ) + ); + } + + @DeleteMapping("/v1/contact-details/{contactId}") + @Operation( + summary = "Delete contact details for a service centre", + description = "Remove an existing contact detail entry for the supplied service centre." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Successfully deleted the contact detail"), + @ApiResponse(responseCode = "400", description = "Invalid service centre ID or contact ID supplied"), + @ApiResponse(responseCode = "404", description = "Contact detail or service centre not found") + }) + @PreAuthorize("@authService.isAdmin()") + public ResponseEntity deleteContactDetail( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId, + @Parameter(description = "UUID of the contact", required = true) @ValidUUID @PathVariable String contactId + ) { + serviceCentreContactDetailsService.deleteContactDetail( + UUID.fromString(serviceCentreId), + UUID.fromString(contactId) + ); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreController.java new file mode 100644 index 000000000..f6d7b190d --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreController.java @@ -0,0 +1,109 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreService; +import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidUUID; + +import java.util.UUID; + +@SecuredFactRestController( + name = "Service Centre", + description = "Operations related to service centres" +) +@RequestMapping("/service-centres") +@RequiredArgsConstructor +@SuppressWarnings("java:S4684") +public class ServiceCentreController { + + private final ServiceCentreService serviceCentreService; + + @GetMapping("/{serviceCentreId}/v1") + @Operation( + summary = "Get service centre by ID", + description = "Fetch a service centre for a given service centre ID." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved service centre"), + @ApiResponse(responseCode = "400", description = "Invalid service centre ID supplied"), + @ApiResponse(responseCode = "404", description = "Service centre not found") + }) + public ResponseEntity getServiceCentreById( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId) { + return ResponseEntity.ok(serviceCentreService.getServiceCentreById(UUID.fromString(serviceCentreId))); + } + + @GetMapping("/name/v1") + @Operation( + summary = "Get service centre by exact name", + description = "Fetch the service centre for a given exact service centre name." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved service centre"), + @ApiResponse(responseCode = "400", description = "Invalid service centre name supplied"), + @ApiResponse(responseCode = "404", description = "Service centre not found") + }) + public ResponseEntity getServiceCentreByName( + @Parameter(description = "Exact name of the service centre", required = true) + @NotBlank(message = "name must not be blank") + @Size(max = 250, message = "name must be less than 250 characters") + @RequestParam(name = "name") String name) { + return ResponseEntity.ok(serviceCentreService.getServiceCentreByName(name)); + } + + @PostMapping(value = "/v1", + consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = "Create a new service centre", + description = "Creates a new service centre record." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully created service centre"), + @ApiResponse(responseCode = "400", description = "Invalid service centre data supplied") + }) + @PreAuthorize("@authService.isAdmin()") + public ResponseEntity createServiceCentre(@Valid @RequestBody ServiceCentre serviceCentre) { + return ResponseEntity.status(HttpStatus.CREATED) + .body(serviceCentreService.createServiceCentre(serviceCentre)); + } + + @PutMapping(value = "/{serviceCentreId}/v1", + consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = "Update an existing service centre", + description = "Updates the details of an existing service centre." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully updated service centre"), + @ApiResponse(responseCode = "400", description = "Invalid service centre data supplied"), + @ApiResponse(responseCode = "404", description = "Service centre not found") + }) + @PreAuthorize("@authService.isAdmin()") + public ResponseEntity updateServiceCentre( + @ValidUUID @PathVariable String serviceCentreId, + @Valid @RequestBody ServiceCentre serviceCentre) { + return ResponseEntity.ok( + serviceCentreService.updateServiceCentre(UUID.fromString(serviceCentreId), serviceCentre) + ); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java index 4e605d112..26af16392 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java @@ -2,8 +2,10 @@ import uk.gov.hmcts.reform.fact.data.api.entities.CourtDetails; import uk.gov.hmcts.reform.fact.data.api.entities.Region; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; import uk.gov.hmcts.reform.fact.data.api.services.RegionService; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreService; import uk.gov.hmcts.reform.fact.data.api.services.TestingSupportService; import java.util.List; @@ -38,6 +40,7 @@ public class TestingSupportController { private final CourtService courtService; private final RegionService regionService; + private final ServiceCentreService serviceCentreService; private final TestingSupportService testingSupportService; @DeleteMapping("/courts/name-prefix/{courtNamePrefix}") @@ -59,6 +62,25 @@ public ResponseEntity deleteCourtsByNamePrefix( + " court(s) with prefix " + courtNamePrefix + " deleted successfully"); } + @DeleteMapping("/service-centres/name-prefix/{serviceCentreNamePrefix}") + @Operation( + summary = "Bulk delete service centres by name prefix", + description = "Deletes all service centres whose names start with the supplied prefix (case-insensitive)." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully deleted matching service centres"), + @ApiResponse(responseCode = "400", description = "Invalid service centre name prefix supplied") + }) + public ResponseEntity deleteServiceCentresByNamePrefix( + @PathVariable + @Size(min = 1, max = 200, message = "Service centre name prefix must be between 1 and 200 characters") + String serviceCentreNamePrefix + ) { + return ResponseEntity.status(HttpStatus.OK) + .body(serviceCentreService.deleteServiceCentresByNamePrefix(serviceCentreNamePrefix) + + " service centre(s) with prefix " + serviceCentreNamePrefix + " deleted successfully"); + } + @GetMapping("/regions") @Operation( summary = "Get regions for testing support", @@ -102,4 +124,28 @@ public ResponseEntity createSampleCourt( return ResponseEntity.status(HttpStatus.CREATED) .body(details); } + + @GetMapping("/service-centres") + @Operation( + summary = "Create sample service centre", + description = "Creates a sample service centre with randomised data for testing purposes." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully created sample service centre"), + }) + public ResponseEntity createSampleServiceCentre( + @RequestParam(required = true) String serviceCentreName, + @RequestParam(required = false) Long seed, + @RequestParam(required = false, defaultValue = "true") boolean open, + @RequestParam(required = false, defaultValue = "false") boolean addWarningNotice, + @RequestParam(required = false, defaultValue = "true") boolean withContactDetails) { + return ResponseEntity.status(HttpStatus.CREATED) + .body(testingSupportService.createServiceCentre( + serviceCentreName, + seed, + open, + addWarningNotice, + withContactDetails + )); + } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java index 7c04100c8..e89ece795 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -74,6 +75,16 @@ public List getAreasOfLaw() { return areasOfLaw; } + @JsonSetter("serviceCentreId") + public void setServiceCentreId(UUID serviceCentreId) { + this.serviceCentreId = serviceCentreId; + } + + @JsonSetter("areasOfLaw") + public void setAreasOfLaw(List areasOfLaw) { + this.areasOfLaw = areasOfLaw; + } + @Override public UUID getAuditSubjectId() { return serviceCentreId; diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAddressRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAddressRepository.java index 79886188d..006603b04 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAddressRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAddressRepository.java @@ -4,7 +4,16 @@ import org.springframework.stereotype.Repository; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAddress; +import java.util.List; +import java.util.Optional; import java.util.UUID; @Repository -public interface ServiceCentreAddressRepository extends JpaRepository {} +public interface ServiceCentreAddressRepository extends JpaRepository { + + List findByServiceCentreId(UUID serviceCentreId); + + Optional findByIdAndServiceCentreId(UUID addressId, UUID serviceCentreId); + + void deleteByIdAndServiceCentreId(UUID addressId, UUID serviceCentreId); +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAreasOfLawRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAreasOfLawRepository.java index f7b79bc15..784dd68dc 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAreasOfLawRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreAreasOfLawRepository.java @@ -4,7 +4,11 @@ import org.springframework.stereotype.Repository; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import java.util.Optional; import java.util.UUID; @Repository -public interface ServiceCentreAreasOfLawRepository extends JpaRepository {} +public interface ServiceCentreAreasOfLawRepository extends JpaRepository { + + Optional findByServiceCentreId(UUID serviceCentreId); +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreContactDetailsRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreContactDetailsRepository.java index 903ad4d1a..5fe576c6e 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreContactDetailsRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreContactDetailsRepository.java @@ -4,7 +4,18 @@ import org.springframework.stereotype.Repository; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; +import java.util.List; +import java.util.Optional; import java.util.UUID; @Repository -public interface ServiceCentreContactDetailsRepository extends JpaRepository {} +public interface ServiceCentreContactDetailsRepository extends JpaRepository { + + List findByServiceCentreId(UUID serviceCentreId); + + Optional findByIdAndServiceCentreId(UUID contactId, UUID serviceCentreId); + + void deleteByIdAndServiceCentreId(UUID contactId, UUID serviceCentreId); + + boolean existsByIdAndServiceCentreId(UUID contactId, UUID serviceCentreId); +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java index 67dfdf948..d179829c7 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java @@ -4,7 +4,16 @@ import org.springframework.stereotype.Repository; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import java.util.List; +import java.util.Optional; import java.util.UUID; @Repository -public interface ServiceCentreRepository extends JpaRepository {} +public interface ServiceCentreRepository extends JpaRepository { + + Optional findByName(String name); + + boolean existsBySlug(String slug); + + List findByNameStartingWithIgnoreCase(String namePrefix); +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAddressService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAddressService.java new file mode 100644 index 000000000..6d1c8e86b --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAddressService.java @@ -0,0 +1,127 @@ +package uk.gov.hmcts.reform.fact.data.api.services; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAddress; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.os.OsData; +import uk.gov.hmcts.reform.fact.data.api.os.OsDpa; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreAddressRepository; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class ServiceCentreAddressService { + + private final ServiceCentreAddressRepository serviceCentreAddressRepository; + private final ServiceCentreService serviceCentreService; + private final OsService osService; + + /** + * Retrieve all addresses for a specific service centre. + * + * @param serviceCentreId The service centre identifier. + * @return List of address records for the service centre. + * @throws NotFoundException if the service centre does not exist. + */ + public List getAddresses(UUID serviceCentreId) { + serviceCentreService.getServiceCentreById(serviceCentreId); + return serviceCentreAddressRepository.findByServiceCentreId(serviceCentreId); + } + + /** + * Retrieve a single address entry by service centre and address identifiers. + * + * @param serviceCentreId The service centre identifier. + * @param addressId The address identifier. + * @return Matching address detail. + * @throws NotFoundException if the service centre or address does not exist. + */ + public ServiceCentreAddress getAddress(UUID serviceCentreId, UUID addressId) { + serviceCentreService.getServiceCentreById(serviceCentreId); + return serviceCentreAddressRepository.findByIdAndServiceCentreId(addressId, serviceCentreId).orElseThrow( + () -> new NotFoundException( + "Service centre address not found, addressId: " + addressId + ", serviceCentreId: " + serviceCentreId + ) + ); + } + + /** + * Persist a new address for a service centre. + * + * @param serviceCentreId The service centre identifier. + * @param serviceCentreAddress The address to create. + * @return Created address. + * @throws NotFoundException if the service centre does not exist. + */ + @Transactional + public ServiceCentreAddress createAddress(UUID serviceCentreId, ServiceCentreAddress serviceCentreAddress) { + ServiceCentre serviceCentre = serviceCentreService.getServiceCentreById(serviceCentreId); + + serviceCentreAddress.setId(null); + serviceCentreAddress.setServiceCentreId(serviceCentreId); + serviceCentreAddress.setServiceCentre(serviceCentre); + setLatLonFromPostcode(serviceCentreAddress); + + return serviceCentreAddressRepository.save(serviceCentreAddress); + } + + /** + * Update an existing address for a service centre. + * + * @param serviceCentreId The service centre identifier. + * @param addressId The address identifier. + * @param serviceCentreAddress Updated address values. + * @return Updated address. + * @throws NotFoundException if the service centre or address does not exist. + */ + @Transactional + public ServiceCentreAddress updateAddress(UUID serviceCentreId, + UUID addressId, + ServiceCentreAddress serviceCentreAddress) { + ServiceCentreAddress existing = getAddress(serviceCentreId, addressId); + setNewAddressFieldsOnExistingAddress(existing, serviceCentreAddress); + + return serviceCentreAddressRepository.save(existing); + } + + /** + * Remove an address for a service centre. + * + * @param serviceCentreId The service centre identifier. + * @param addressId The address identifier. + * @throws NotFoundException if the service centre or address does not exist. + */ + @Transactional + public void deleteAddress(UUID serviceCentreId, UUID addressId) { + serviceCentreAddressRepository.deleteByIdAndServiceCentreId( + getAddress(serviceCentreId, addressId).getId(), + serviceCentreId + ); + } + + private void setLatLonFromPostcode(ServiceCentreAddress address) { + OsData osData = osService.getOsAddressByFullPostcode(address.getPostcode()); + if (osData != null && osData.getResults() != null && !osData.getResults().isEmpty()) { + OsDpa dpa = osData.getResults().getFirst().getDpa(); + address.setLat(BigDecimal.valueOf(dpa.getLat())); + address.setLon(BigDecimal.valueOf(dpa.getLng())); + } + } + + private void setNewAddressFieldsOnExistingAddress(ServiceCentreAddress existing, ServiceCentreAddress newAddress) { + existing.setAddressLine1(newAddress.getAddressLine1()); + existing.setAddressLine2(newAddress.getAddressLine2()); + existing.setTownCity(newAddress.getTownCity()); + existing.setCounty(newAddress.getCounty()); + existing.setPostcode(newAddress.getPostcode()); + existing.setAddressType(newAddress.getAddressType()); + + setLatLonFromPostcode(existing); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAreasOfLawService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAreasOfLawService.java new file mode 100644 index 000000000..37a3042cc --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAreasOfLawService.java @@ -0,0 +1,83 @@ +package uk.gov.hmcts.reform.fact.data.api.services; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import uk.gov.hmcts.reform.fact.data.api.entities.AreaOfLawType; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreAreasOfLawRepository; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ServiceCentreAreasOfLawService { + + private final ServiceCentreAreasOfLawRepository serviceCentreAreasOfLawRepository; + private final ServiceCentreService serviceCentreService; + private final TypesService typesService; + + /** + * Get areas of law by service centre id. + * + * @param serviceCentreId The service centre id to find the areas of law for. + * @return Areas of law record for the service centre. + * @throws NotFoundException if no areas of law record exists for the service centre. + */ + public ServiceCentreAreasOfLaw getServiceCentreAreasOfLawByServiceCentreId(UUID serviceCentreId) { + return serviceCentreAreasOfLawRepository.findByServiceCentreId( + serviceCentreService.getServiceCentreById(serviceCentreId).getId() + ).orElseThrow(() -> new NotFoundException( + "No service centre areas of law found for service centre id: " + serviceCentreId + )); + } + + /** + * Get a map of areas of law types and their status for a service centre. + * + * @param serviceCentreId The service centre id to find the areas of law for. + * @return Map of area of law types with boolean values indicating if they are available at the service centre. + * @throws NotFoundException if no areas of law record exists for the service centre. + */ + public Map getAreasOfLawStatusByServiceCentreId(UUID serviceCentreId) { + List serviceCentreAreasOfLawIds = + getServiceCentreAreasOfLawByServiceCentreId(serviceCentreId).getAreasOfLaw(); + List allAreasOfLawTypes = typesService.getAreaOfLawTypes(); + + return allAreasOfLawTypes.stream() + .collect(Collectors.toMap( + areaOfLawType -> areaOfLawType, + areaOfLawType -> serviceCentreAreasOfLawIds.contains(areaOfLawType.getId()) + )); + } + + /** + * Set an areas of law record for a service centre. + * + * @param serviceCentreId The id of the service centre to set areas of law for. + * @param serviceCentreAreasOfLaw The service centre areas of law entity to create or update. + * @return The created or updated areas of law entity. + * @throws NotFoundException if the service centre or supplied areas of law do not exist. + */ + public ServiceCentreAreasOfLaw setServiceCentreAreasOfLaw(UUID serviceCentreId, + ServiceCentreAreasOfLaw serviceCentreAreasOfLaw) { + serviceCentreAreasOfLaw.setServiceCentre(serviceCentreService.getServiceCentreById(serviceCentreId)); + serviceCentreAreasOfLaw.setServiceCentreId(serviceCentreId); + + serviceCentreAreasOfLaw.setAreasOfLaw( + typesService.getAllAreasOfLawTypesByIds(serviceCentreAreasOfLaw.getAreasOfLaw()) + .stream() + .map(AreaOfLawType::getId) + .toList() + ); + + serviceCentreAreasOfLawRepository.findByServiceCentreId(serviceCentreId) + .map(ServiceCentreAreasOfLaw::getId) + .ifPresent(serviceCentreAreasOfLaw::setId); + + return serviceCentreAreasOfLawRepository.save(serviceCentreAreasOfLaw); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreContactDetailsService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreContactDetailsService.java new file mode 100644 index 000000000..4ea0c6915 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreContactDetailsService.java @@ -0,0 +1,137 @@ +package uk.gov.hmcts.reform.fact.data.api.services; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import uk.gov.hmcts.reform.fact.data.api.entities.ContactDescriptionType; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.repositories.ContactDescriptionTypeRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreContactDetailsRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ServiceCentreContactDetailsService { + + private final ServiceCentreContactDetailsRepository serviceCentreContactDetailsRepository; + private final ContactDescriptionTypeRepository contactDescriptionTypeRepository; + private final ServiceCentreService serviceCentreService; + + /** + * Retrieve all contact details for a specific service centre. + * + * @param serviceCentreId The service centre identifier. + * @return List of contact detail records for the service centre. + * @throws NotFoundException if the service centre does not exist. + */ + public List getContactDetails(UUID serviceCentreId) { + serviceCentreService.getServiceCentreById(serviceCentreId); + return serviceCentreContactDetailsRepository.findByServiceCentreId(serviceCentreId); + } + + /** + * Retrieve a single contact detail entry by service centre and contact identifiers. + * + * @param serviceCentreId The service centre identifier. + * @param contactId The contact identifier. + * @return Matching contact detail. + * @throws NotFoundException if the service centre or contact detail does not exist. + */ + public ServiceCentreContactDetails getContactDetail(UUID serviceCentreId, UUID contactId) { + serviceCentreService.getServiceCentreById(serviceCentreId); + return serviceCentreContactDetailsRepository.findByIdAndServiceCentreId(contactId, serviceCentreId) + .orElseThrow(() -> new NotFoundException( + "Service centre contact detail not found, contactId: " + contactId + + ", serviceCentreId: " + serviceCentreId + )); + } + + /** + * Persist a new contact detail for a service centre. + * + * @param serviceCentreId The service centre identifier. + * @param request The contact detail to create. + * @return Created contact detail. + * @throws NotFoundException if the service centre or supplied description type does not exist. + */ + @Transactional + public ServiceCentreContactDetails createContactDetail(UUID serviceCentreId, ServiceCentreContactDetails request) { + ServiceCentre serviceCentre = serviceCentreService.getServiceCentreById(serviceCentreId); + Optional description = + getValidatedContactDescription(request.getServiceCentreContactDescriptionId()); + + request.setId(null); + request.setServiceCentreId(serviceCentreId); + request.setServiceCentre(serviceCentre); + request.setServiceCentreContactDescription(description.orElse(null)); + + log.info("Creating contact detail for service centre {}", serviceCentreId); + return serviceCentreContactDetailsRepository.save(request); + } + + /** + * Update an existing contact detail for a service centre. + * + * @param serviceCentreId The service centre identifier. + * @param contactId The contact identifier. + * @param request Updated contact detail values. + * @return Updated contact detail. + * @throws NotFoundException if the service centre, contact detail, or supplied description type does not exist. + */ + @Transactional + public ServiceCentreContactDetails updateContactDetail(UUID serviceCentreId, + UUID contactId, + ServiceCentreContactDetails request) { + ServiceCentreContactDetails existing = getContactDetail(serviceCentreId, contactId); + Optional description = + getValidatedContactDescription(request.getServiceCentreContactDescriptionId()); + + existing.setServiceCentreContactDescriptionId(request.getServiceCentreContactDescriptionId()); + existing.setServiceCentreContactDescription(description.orElse(null)); + existing.setExplanation(request.getExplanation()); + existing.setExplanationCy(request.getExplanationCy()); + existing.setEmail(request.getEmail()); + existing.setPhoneNumber(request.getPhoneNumber()); + + log.info("Updating contact detail {} for service centre {}", contactId, serviceCentreId); + return serviceCentreContactDetailsRepository.save(existing); + } + + /** + * Remove a contact detail for a service centre. + * + * @param serviceCentreId The service centre identifier. + * @param contactId The contact identifier. + * @throws NotFoundException if the service centre or contact detail does not exist. + */ + @Transactional + public void deleteContactDetail(UUID serviceCentreId, UUID contactId) { + serviceCentreService.getServiceCentreById(serviceCentreId); + if (!serviceCentreContactDetailsRepository.existsByIdAndServiceCentreId(contactId, serviceCentreId)) { + throw new NotFoundException( + "Service centre contact detail not found, contactId: " + contactId + + ", serviceCentreId: " + serviceCentreId + ); + } + + log.info("Deleting contact detail {} for service centre {}", contactId, serviceCentreId); + serviceCentreContactDetailsRepository.deleteByIdAndServiceCentreId(contactId, serviceCentreId); + } + + private Optional getValidatedContactDescription(UUID contactDescriptionId) { + if (contactDescriptionId == null) { + return Optional.empty(); + } + + return Optional.of(contactDescriptionTypeRepository.findById(contactDescriptionId).orElseThrow( + () -> new NotFoundException("Contact description type not found, ID: " + contactDescriptionId) + )); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java new file mode 100644 index 000000000..ac3c757c3 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java @@ -0,0 +1,142 @@ +package uk.gov.hmcts.reform.fact.data.api.services; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class ServiceCentreService { + + private final ServiceCentreRepository serviceCentreRepository; + private final ServiceAreaRepository serviceAreaRepository; + + /** + * Get a service centre by id. + * + * @param serviceCentreId The ID of the service centre to get. + * @return The service centre entity. + * @throws NotFoundException if the service centre is not found. + */ + public ServiceCentre getServiceCentreById(UUID serviceCentreId) { + return serviceCentreRepository.findById(serviceCentreId) + .orElseThrow(() -> new NotFoundException("Service centre not found, ID: " + serviceCentreId)); + } + + /** + * Get a service centre by its exact name. + * + * @param serviceCentreName The exact name of the service centre to get. + * @return The service centre entity. + * @throws NotFoundException if the service centre is not found. + */ + public ServiceCentre getServiceCentreByName(String serviceCentreName) { + return serviceCentreRepository.findByName(serviceCentreName) + .orElseThrow(() -> new NotFoundException("Service centre not found, name: " + serviceCentreName)); + } + + /** + * Creates a new service centre. + * + * @param serviceCentre The service centre to create. + * @return The created service centre. + */ + public ServiceCentre createServiceCentre(ServiceCentre serviceCentre) { + serviceCentre.setId(null); + serviceCentre.setSlug(toUniqueSlug(serviceCentre.getName())); + serviceCentre.setOpen(false); + serviceCentre.setServiceAreaIds(getValidatedServiceAreaIds(serviceCentre.getServiceAreaIds())); + serviceCentre.setCatchmentType(CatchmentType.NATIONAL); + + return serviceCentreRepository.save(serviceCentre); + } + + /** + * Updates an existing service centre. + * + * @param serviceCentreId The id of the service centre to update. + * @param serviceCentre The service centre entity with updated values. + * @return The updated service centre. + * @throws NotFoundException if the service centre is not found. + */ + public ServiceCentre updateServiceCentre(UUID serviceCentreId, ServiceCentre serviceCentre) { + ServiceCentre existingServiceCentre = getServiceCentreById(serviceCentreId); + + if (!existingServiceCentre.getName().equalsIgnoreCase(serviceCentre.getName())) { + existingServiceCentre.setName(serviceCentre.getName()); + existingServiceCentre.setSlug(toUniqueSlug(serviceCentre.getName())); + } + + existingServiceCentre.setOpen(serviceCentre.getOpen()); + existingServiceCentre.setWarningNotice(serviceCentre.getWarningNotice()); + existingServiceCentre.setServiceAreaIds(getValidatedServiceAreaIds(serviceCentre.getServiceAreaIds())); + existingServiceCentre.setCatchmentType(CatchmentType.NATIONAL); + + return serviceCentreRepository.save(existingServiceCentre); + } + + /** + * Delete service centres matching the provided name prefix. + * + * @param serviceCentreNamePrefix The name prefix to match. + * @return The number of service centres deleted. + */ + @Transactional + public long deleteServiceCentresByNamePrefix(String serviceCentreNamePrefix) { + List serviceCentresToDelete = + serviceCentreRepository.findByNameStartingWithIgnoreCase(serviceCentreNamePrefix.trim()); + + if (serviceCentresToDelete.isEmpty()) { + return 0; + } + + serviceCentreRepository.deleteAllInBatch(serviceCentresToDelete); + return serviceCentresToDelete.size(); + } + + /** + * Convert a service centre name into slug format. + * + * @param name The service centre name. + * @return The slug-formatted name. + */ + public String toSlugFormat(String name) { + return name.toLowerCase() + .replaceAll("[^a-z\\s-]", "") + .replaceAll("[\\s-]+", "-") + .replaceAll("(^-)|(-$)", ""); + } + + private String toUniqueSlug(String name) { + String baseSlug = toSlugFormat(name); + String slug = baseSlug; + int counter = 1; + + while (serviceCentreRepository.existsBySlug(slug)) { + slug = baseSlug + "-" + counter++; + } + + return slug; + } + + private List getValidatedServiceAreaIds(List serviceAreaIds) { + if (serviceAreaIds == null) { + return List.of(); + } + + return serviceAreaRepository.findAllById(serviceAreaIds) + .stream() + .map(ServiceArea::getId) + .toList(); + } + +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java index 14d55acc7..2436f033e 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java @@ -22,9 +22,15 @@ import uk.gov.hmcts.reform.fact.data.api.entities.LocalAuthorityType; import uk.gov.hmcts.reform.fact.data.api.entities.OpeningHourType; import uk.gov.hmcts.reform.fact.data.api.entities.Region; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAddress; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.entities.types.AddressType; import uk.gov.hmcts.reform.fact.data.api.entities.types.AllowedLocalAuthorityAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.entities.types.DayOfTheWeek; import uk.gov.hmcts.reform.fact.data.api.entities.types.HearingEnhancementEquipment; import uk.gov.hmcts.reform.fact.data.api.entities.types.OpeningTimesDetail; @@ -40,6 +46,7 @@ import uk.gov.hmcts.reform.fact.data.api.repositories.LocalAuthorityTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.OpeningHoursTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.RegionRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; import java.awt.AlphaComposite; import java.awt.Color; @@ -94,6 +101,7 @@ public class TestingSupportService { private static final List CONTACT_DESCRIPTION_IDS = new ArrayList<>(); private static final List LOCAL_AUTHORITY_TYPE_IDS = new ArrayList<>(); private static final List OPENING_HOUR_TYPE_IDS = new ArrayList<>(); + private static final List SERVICE_AREA_IDS = new ArrayList<>(); // ------------------------------------------------------------------------ // Data variations for randomisation @@ -176,6 +184,10 @@ public class TestingSupportService { private final CourtTranslationService courtTranslationService; private final CourtProfessionalInformationService courtProfessionalInformationService; private final CourtLocalAuthoritiesService courtLocalAuthoritiesService; + private final ServiceCentreService serviceCentreService; + private final ServiceCentreAddressService serviceCentreAddressService; + private final ServiceCentreAreasOfLawService serviceCentreAreasOfLawService; + private final ServiceCentreContactDetailsService serviceCentreContactDetailsService; // for photos, we try the service first, then fall back to the repository if required private final CourtPhotoService courtPhotoService; private final CourtPhotoRepository courtPhotoRepository; @@ -191,6 +203,7 @@ public class TestingSupportService { private final LocalAuthorityTypeRepository localAuthorityTypeRepository; private final OpeningHoursTypeRepository openingHoursTypeRepository; private final RegionRepository regionRepository; + private final ServiceAreaRepository serviceAreaRepository; /** * Creates a court with the specified name and randomised data for all other fields. The randomisation is based on @@ -308,6 +321,60 @@ private Court createCourt(String name, return courtService.createCourt(court); } + public ServiceCentre createServiceCentre( + @NonNull String serviceCentreName, + Long seed, + boolean open, + boolean addWarningNotice, + boolean withContactDetails + ) { + try { + initialiseCaches(); + auditUserContext.setUserId(getTestingSupportUserId()); + + Random random = new Random(Optional.ofNullable(seed).orElse(System.currentTimeMillis())); + + ServiceCentre serviceCentre = createServiceCentre(serviceCentreName, addWarningNotice, random); + UUID serviceCentreId = serviceCentre.getId(); + setServiceCentreAreasOfLaw(serviceCentreId, random); + setServiceCentreAddress(serviceCentreId, random); + + if (withContactDetails) { + setServiceCentreContactDetails(serviceCentreId, random); + } + + if (open) { + serviceCentre = openServiceCentre(serviceCentre); + } + + return serviceCentre; + } catch (Exception e) { + log.error("error while creating service centre", e); + throw e; + } + } + + private ServiceCentre createServiceCentre(String name, boolean addWarningNotice, Random random) { + List serviceAreaIds = SERVICE_AREA_IDS.stream().filter(l -> random.nextBoolean()).toList(); + if (serviceAreaIds.isEmpty()) { + serviceAreaIds = List.of(SERVICE_AREA_IDS.get(random.nextInt(SERVICE_AREA_IDS.size()))); + } + + ServiceCentre serviceCentre = ServiceCentre.builder() + .name(name) + .open(false) + .warningNotice( + addWarningNotice + ? WARNING_NOTICE_VALUES.get(random.nextInt(WARNING_NOTICE_VALUES.size())) + : null + ) + .serviceAreaIds(serviceAreaIds) + .catchmentType(CatchmentType.NATIONAL) + .build(); + + return serviceCentreService.createServiceCentre(serviceCentre); + } + private UUID getTestingSupportUserId() { User user = User.builder() .email(TESTING_SUPPORT_USER_EMAIL) @@ -323,6 +390,11 @@ private void openCourt(final Court court) { courtService.updateCourt(court.getId(), court); } + private ServiceCentre openServiceCentre(final ServiceCentre serviceCentre) { + serviceCentre.setOpen(true); + return serviceCentreService.updateServiceCentre(serviceCentre.getId(), serviceCentre); + } + @Synchronized private void initialiseCaches() { // using region ids as the gate here @@ -339,9 +411,10 @@ private void initialiseCaches() { OPENING_HOUR_TYPE_IDS.addAll(openingHoursTypeRepository.findAll().stream().map( OpeningHourType::getId).toList() ); + SERVICE_AREA_IDS.addAll(serviceAreaRepository.findAll().stream().map(ServiceArea::getId).toList()); if (REGION_IDS.isEmpty() || AREAS_OF_LAW.isEmpty() || COURT_TYPES.isEmpty() || CONTACT_DESCRIPTION_IDS.isEmpty() || LOCAL_AUTHORITY_TYPE_IDS.isEmpty() - || OPENING_HOUR_TYPE_IDS.isEmpty()) { + || OPENING_HOUR_TYPE_IDS.isEmpty() || SERVICE_AREA_IDS.isEmpty()) { throw new IllegalStateException("Unable to initialize testing support service caches " + "- one or more required reference data tables are empty"); } @@ -421,6 +494,27 @@ private List setAreasOfLaw(final UUID courtId, final Random rando return areasOfLaw; } + private List setServiceCentreAreasOfLaw(final UUID serviceCentreId, final Random random) { + final List areasOfLaw = new ArrayList<>( + AREAS_OF_LAW.stream() + .filter(l -> random.nextInt(100) < 30) + .toList() + ); + + if (areasOfLaw.isEmpty()) { + areasOfLaw.add(AREAS_OF_LAW.get(random.nextInt(AREAS_OF_LAW.size()))); + } + + ServiceCentreAreasOfLaw serviceCentreAreasOfLaw = ServiceCentreAreasOfLaw.builder() + .serviceCentreId(serviceCentreId) + .areasOfLaw(areasOfLaw.stream().map(AreaOfLawType::getId).toList()) + .build(); + + serviceCentreAreasOfLawService.setServiceCentreAreasOfLaw(serviceCentreId, serviceCentreAreasOfLaw); + + return areasOfLaw; + } + private void setContactDetails(final UUID courtId, final Random random, final boolean withEnquiriesContact) { if (!withEnquiriesContact) { return; @@ -436,6 +530,26 @@ private void setContactDetails(final UUID courtId, final Random random, final bo courtContactDetailsService.createContactDetail(courtId, courtContactDetails); } + private void setServiceCentreAddress(final UUID serviceCentreId, final Random random) { + serviceCentreAddressService.createAddress( + serviceCentreId, + rndServiceCentreAddress(serviceCentreId, AddressType.VISIT_OR_CONTACT_US, random) + ); + } + + private void setServiceCentreContactDetails(final UUID serviceCentreId, final Random random) { + ServiceCentreContactDetails serviceCentreContactDetails = ServiceCentreContactDetails.builder() + .serviceCentreId(serviceCentreId) + .serviceCentreContactDescriptionId( + CONTACT_DESCRIPTION_IDS.get(random.nextInt(CONTACT_DESCRIPTION_IDS.size())) + ) + .explanation(CONTACT_DESCRIPTION_VALUES.get(random.nextInt(CONTACT_DESCRIPTION_VALUES.size()))) + .phoneNumber(rndPhoneNumber(random)) + .email(rndEmail(random)) + .build(); + serviceCentreContactDetailsService.createContactDetail(serviceCentreId, serviceCentreContactDetails); + } + private void setCounterServiceOpeningHours(final UUID courtId, List courtTypes, final Random random) { CourtCounterServiceOpeningHours openingHours = CourtCounterServiceOpeningHours.builder() @@ -773,6 +887,24 @@ private static CourtAddress rndAddress( .build(); } + private static ServiceCentreAddress rndServiceCentreAddress( + final UUID serviceCentreId, + final AddressType addressType, + final Random random) { + String city = LOCATION_MAP.keySet().stream().toList().get(random.nextInt(LOCATION_MAP.size())); + return ServiceCentreAddress.builder() + .serviceCentreId(serviceCentreId) + .addressLine1(ADDRESS_LINE_1.get(random.nextInt(ADDRESS_LINE_1.size()))) + .addressLine2(ADDRESS_LINE_2.get(random.nextInt(ADDRESS_LINE_2.size()))) + .townCity(city) + .county(LOCATION_MAP.get(city)) + .postcode(POST_CODES.get(random.nextInt(POST_CODES.size()))) + .addressType(addressType) + .lat(BigDecimal.valueOf(random.nextDouble())) + .lon(BigDecimal.valueOf(random.nextDouble())) + .build(); + } + private List rndOpeningTimesDetails(final Random random, final List days) { LocalTime openingTime = LocalTime.of(random.nextInt(6, 11), random.nextBoolean() ? 0 : 30); LocalTime closingTime = LocalTime.of(random.nextInt(12, 19), random.nextBoolean() ? 0 : 30); diff --git a/src/main/resources/db/migration/V1.30__refresh_service_area_mappings.sql b/src/main/resources/db/migration/V1.30__refresh_service_area_mappings.sql new file mode 100644 index 000000000..2aae2bbcc --- /dev/null +++ b/src/main/resources/db/migration/V1.30__refresh_service_area_mappings.sql @@ -0,0 +1,43 @@ +WITH mapping(service_name, area_name) AS (VALUES + ('Childcare and parenting', 'Adoption'), + ('Childcare and parenting', 'Childcare arrangements if you separate from your partner'), + ('Crime', 'Other criminal offences'), + ('Crime', 'Single Justice Procedure'), + ('Harm and abuse', 'Domestic abuse'), + ('Harm and abuse', 'Female Genital Mutilation Protection Orders'), + ('Harm and abuse', 'Forced marriage'), + ('High Court district registries', 'High Court district registry'), + ('Immigration and asylum', 'Immigration and asylum'), + ('Money', 'Bankruptcy'), + ('Money', 'Benefits'), + ('Money', 'Claims against employers'), + ('Money', 'Housing'), + ('Money', 'Money claims'), + ('Money', 'Probate'), + ('Money', 'Single Justice Procedure'), + ('Money', 'Tax'), + ('Probate, divorce or ending civil partnerships', 'Civil partnership'), + ('Probate, divorce or ending civil partnerships', 'Divorce'), + ('Probate, divorce or ending civil partnerships', 'Financial remedy'), + ('Probate, divorce or ending civil partnerships', 'Forced marriage'), + ('Probate, divorce or ending civil partnerships', 'Probate') +), +dedup AS ( + SELECT DISTINCT m.service_name, + sa.id AS service_area_id, + sa.sort_order, + sa.name + FROM mapping m + JOIN service_area sa + ON lower(sa.name) = lower(m.area_name) +), +agg AS ( + SELECT service_name, + array_agg(service_area_id ORDER BY sort_order nulls last, name) AS service_area_ids + FROM dedup + GROUP BY service_name +) +UPDATE service s +SET service_areas = agg.service_area_ids +FROM agg +WHERE lower(s.name) = lower(agg.service_name); diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAddressControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAddressControllerTest.java new file mode 100644 index 000000000..47dd85d87 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAddressControllerTest.java @@ -0,0 +1,93 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers; + +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 org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAddress; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreAddressService; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ServiceCentreAddressControllerTest { + + private static final UUID SERVICE_CENTRE_ID = UUID.randomUUID(); + private static final UUID ADDRESS_ID = UUID.randomUUID(); + + @Mock + private ServiceCentreAddressService serviceCentreAddressService; + + @InjectMocks + private ServiceCentreAddressController serviceCentreAddressController; + + @Test + void getAddressesReturns200() { + ServiceCentreAddress address = ServiceCentreAddress.builder().id(ADDRESS_ID).build(); + when(serviceCentreAddressService.getAddresses(SERVICE_CENTRE_ID)).thenReturn(List.of(address)); + + ResponseEntity> response = + serviceCentreAddressController.getAddresses(SERVICE_CENTRE_ID.toString()); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).containsExactly(address); + } + + @Test + void getAddressReturns200() { + ServiceCentreAddress address = ServiceCentreAddress.builder().id(ADDRESS_ID).build(); + when(serviceCentreAddressService.getAddress(SERVICE_CENTRE_ID, ADDRESS_ID)).thenReturn(address); + + ResponseEntity response = + serviceCentreAddressController.getAddress(SERVICE_CENTRE_ID.toString(), ADDRESS_ID.toString()); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(address); + } + + @Test + void createAddressReturns201() { + ServiceCentreAddress address = ServiceCentreAddress.builder().id(ADDRESS_ID).build(); + when(serviceCentreAddressService.createAddress(SERVICE_CENTRE_ID, address)).thenReturn(address); + + ResponseEntity response = + serviceCentreAddressController.createAddress(SERVICE_CENTRE_ID.toString(), address); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getBody()).isEqualTo(address); + } + + @Test + void updateAddressReturns200() { + ServiceCentreAddress address = ServiceCentreAddress.builder().id(ADDRESS_ID).build(); + when(serviceCentreAddressService.updateAddress(SERVICE_CENTRE_ID, ADDRESS_ID, address)).thenReturn(address); + + ResponseEntity response = serviceCentreAddressController.updateServiceCentreAddress( + SERVICE_CENTRE_ID.toString(), + ADDRESS_ID.toString(), + address + ); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(address); + } + + @Test + void deleteAddressReturns204() { + ResponseEntity response = serviceCentreAddressController.deleteServiceCentreAddress( + SERVICE_CENTRE_ID.toString(), + ADDRESS_ID.toString() + ); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + verify(serviceCentreAddressService).deleteAddress(SERVICE_CENTRE_ID, ADDRESS_ID); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAreasOfLawControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAreasOfLawControllerTest.java new file mode 100644 index 000000000..ad344c1d7 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreAreasOfLawControllerTest.java @@ -0,0 +1,59 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers; + +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 org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import uk.gov.hmcts.reform.fact.data.api.entities.AreaOfLawType; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreAreasOfLawService; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ServiceCentreAreasOfLawControllerTest { + + private static final UUID SERVICE_CENTRE_ID = UUID.randomUUID(); + + @Mock + private ServiceCentreAreasOfLawService serviceCentreAreasOfLawService; + + @InjectMocks + private ServiceCentreAreasOfLawController serviceCentreAreasOfLawController; + + @Test + void getAreasOfLawByServiceCentreIdReturns200() { + AreaOfLawType areaOfLawType = new AreaOfLawType(); + Map responseBody = Map.of(areaOfLawType, true); + + when(serviceCentreAreasOfLawService.getAreasOfLawStatusByServiceCentreId(SERVICE_CENTRE_ID)) + .thenReturn(responseBody); + + ResponseEntity> response = + serviceCentreAreasOfLawController.getAreasOfLawByServiceCentreId(SERVICE_CENTRE_ID.toString()); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(responseBody); + } + + @Test + void setAreasOfLawServicesReturns201() { + ServiceCentreAreasOfLaw areasOfLaw = ServiceCentreAreasOfLaw.builder().build(); + + when(serviceCentreAreasOfLawService.setServiceCentreAreasOfLaw(SERVICE_CENTRE_ID, areasOfLaw)) + .thenReturn(areasOfLaw); + + ResponseEntity response = + serviceCentreAreasOfLawController.setAreasOfLawServices(SERVICE_CENTRE_ID.toString(), areasOfLaw); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getBody()).isEqualTo(areasOfLaw); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreContactDetailsControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreContactDetailsControllerTest.java new file mode 100644 index 000000000..9b002ce44 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreContactDetailsControllerTest.java @@ -0,0 +1,98 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers; + +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 org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreContactDetailsService; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ServiceCentreContactDetailsControllerTest { + + private static final UUID SERVICE_CENTRE_ID = UUID.randomUUID(); + private static final UUID CONTACT_ID = UUID.randomUUID(); + + @Mock + private ServiceCentreContactDetailsService serviceCentreContactDetailsService; + + @InjectMocks + private ServiceCentreContactDetailsController serviceCentreContactDetailsController; + + @Test + void getContactDetailsReturns200() { + ServiceCentreContactDetails contactDetails = ServiceCentreContactDetails.builder().id(CONTACT_ID).build(); + when(serviceCentreContactDetailsService.getContactDetails(SERVICE_CENTRE_ID)) + .thenReturn(List.of(contactDetails)); + + ResponseEntity> response = + serviceCentreContactDetailsController.getContactDetails(SERVICE_CENTRE_ID.toString()); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).containsExactly(contactDetails); + } + + @Test + void getContactDetailReturns200() { + ServiceCentreContactDetails contactDetails = ServiceCentreContactDetails.builder().id(CONTACT_ID).build(); + when(serviceCentreContactDetailsService.getContactDetail(SERVICE_CENTRE_ID, CONTACT_ID)) + .thenReturn(contactDetails); + + ResponseEntity response = + serviceCentreContactDetailsController.getContactDetail(SERVICE_CENTRE_ID.toString(), CONTACT_ID.toString()); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(contactDetails); + } + + @Test + void createContactDetailReturns201() { + ServiceCentreContactDetails contactDetails = ServiceCentreContactDetails.builder().id(CONTACT_ID).build(); + when(serviceCentreContactDetailsService.createContactDetail(SERVICE_CENTRE_ID, contactDetails)) + .thenReturn(contactDetails); + + ResponseEntity response = + serviceCentreContactDetailsController.createContactDetail(SERVICE_CENTRE_ID.toString(), contactDetails); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getBody()).isEqualTo(contactDetails); + } + + @Test + void updateContactDetailReturns200() { + ServiceCentreContactDetails contactDetails = ServiceCentreContactDetails.builder().id(CONTACT_ID).build(); + when(serviceCentreContactDetailsService.updateContactDetail(SERVICE_CENTRE_ID, CONTACT_ID, contactDetails)) + .thenReturn(contactDetails); + + ResponseEntity response = + serviceCentreContactDetailsController.updateContactDetail( + SERVICE_CENTRE_ID.toString(), + CONTACT_ID.toString(), + contactDetails + ); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(contactDetails); + } + + @Test + void deleteContactDetailReturns204() { + ResponseEntity response = serviceCentreContactDetailsController.deleteContactDetail( + SERVICE_CENTRE_ID.toString(), + CONTACT_ID.toString() + ); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + verify(serviceCentreContactDetailsService).deleteContactDetail(SERVICE_CENTRE_ID, CONTACT_ID); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java new file mode 100644 index 000000000..07a4d950d --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java @@ -0,0 +1,84 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers; + +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 org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreService; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ServiceCentreControllerTest { + + private static final UUID SERVICE_CENTRE_ID = UUID.randomUUID(); + + @Mock + private ServiceCentreService serviceCentreService; + + @InjectMocks + private ServiceCentreController serviceCentreController; + + @Test + void getServiceCentreByIdReturns200() { + ServiceCentre serviceCentre = createServiceCentre(); + when(serviceCentreService.getServiceCentreById(SERVICE_CENTRE_ID)).thenReturn(serviceCentre); + + ResponseEntity response = + serviceCentreController.getServiceCentreById(SERVICE_CENTRE_ID.toString()); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(serviceCentre); + } + + @Test + void getServiceCentreByNameReturns200() { + ServiceCentre serviceCentre = createServiceCentre(); + when(serviceCentreService.getServiceCentreByName("Test Service Centre")).thenReturn(serviceCentre); + + ResponseEntity response = + serviceCentreController.getServiceCentreByName("Test Service Centre"); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(serviceCentre); + } + + @Test + void createServiceCentreReturns201() { + ServiceCentre serviceCentre = createServiceCentre(); + when(serviceCentreService.createServiceCentre(serviceCentre)).thenReturn(serviceCentre); + + ResponseEntity response = serviceCentreController.createServiceCentre(serviceCentre); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getBody()).isEqualTo(serviceCentre); + } + + @Test + void updateServiceCentreReturns200() { + ServiceCentre serviceCentre = createServiceCentre(); + when(serviceCentreService.updateServiceCentre(SERVICE_CENTRE_ID, serviceCentre)).thenReturn(serviceCentre); + + ResponseEntity response = + serviceCentreController.updateServiceCentre(SERVICE_CENTRE_ID.toString(), serviceCentre); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(serviceCentre); + } + + private ServiceCentre createServiceCentre() { + return ServiceCentre.builder() + .id(SERVICE_CENTRE_ID) + .name("Test Service Centre") + .slug("test-service-centre") + .open(true) + .build(); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java index 96fe5a554..e612f2cb4 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java @@ -8,8 +8,10 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import uk.gov.hmcts.reform.fact.data.api.entities.Region; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; import uk.gov.hmcts.reform.fact.data.api.services.RegionService; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreService; import uk.gov.hmcts.reform.fact.data.api.services.TestingSupportService; import java.util.List; @@ -28,6 +30,7 @@ class TestingSupportControllerTest { private static final String COURT_NAME_PREFIX = "TESTCOURT"; + private static final String SERVICE_CENTRE_NAME_PREFIX = "TESTSERVICECENTRE"; private static final String BLANK_PREFIX = " "; @Mock @@ -35,6 +38,8 @@ class TestingSupportControllerTest { @Mock private RegionService regionService; @Mock + private ServiceCentreService serviceCentreService; + @Mock private TestingSupportService testingSupportService; @InjectMocks @@ -60,6 +65,18 @@ void deleteCourtsByNamePrefixPropagatesIllegalArgumentExceptionForBlankPrefix() ); } + @Test + void deleteServiceCentresByNamePrefixReturns200() { + when(serviceCentreService.deleteServiceCentresByNamePrefix(SERVICE_CENTRE_NAME_PREFIX)).thenReturn(2L); + + ResponseEntity response = + testingSupportController.deleteServiceCentresByNamePrefix(SERVICE_CENTRE_NAME_PREFIX); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).contains("2 service centre(s)").contains(SERVICE_CENTRE_NAME_PREFIX); + verify(serviceCentreService).deleteServiceCentresByNamePrefix(SERVICE_CENTRE_NAME_PREFIX); + } + @Test void getRegionsReturns200() { UUID regionId = UUID.randomUUID(); @@ -111,4 +128,29 @@ void createSampleCourtPassesRegionIdOverride() { verify(testingSupportService).createCourt("Test Court", regionId, 1L, true, false, false, false, false); } + + @Test + void createSampleServiceCentreReturns201() { + ServiceCentre serviceCentre = ServiceCentre.builder() + .id(UUID.randomUUID()) + .name("Test Service Centre") + .slug("test-service-centre") + .open(true) + .build(); + + when(testingSupportService.createServiceCentre("Test Service Centre", 1L, true, false, true)) + .thenReturn(serviceCentre); + + ResponseEntity response = testingSupportController.createSampleServiceCentre( + "Test Service Centre", + 1L, + true, + false, + true + ); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getBody()).isEqualTo(serviceCentre); + verify(testingSupportService).createServiceCentre("Test Service Centre", 1L, true, false, true); + } } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAddressServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAddressServiceTest.java new file mode 100644 index 000000000..92cf3d753 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAddressServiceTest.java @@ -0,0 +1,143 @@ +package uk.gov.hmcts.reform.fact.data.api.services; + +import org.junit.jupiter.api.BeforeEach; +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.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAddress; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AddressType; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.os.OsData; +import uk.gov.hmcts.reform.fact.data.api.os.OsDpa; +import uk.gov.hmcts.reform.fact.data.api.os.OsResult; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreAddressRepository; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ServiceCentreAddressServiceTest { + + @Mock + private ServiceCentreAddressRepository serviceCentreAddressRepository; + + @Mock + private ServiceCentreService serviceCentreService; + + @Mock + private OsService osService; + + @InjectMocks + private ServiceCentreAddressService serviceCentreAddressService; + + private UUID serviceCentreId; + private UUID addressId; + private ServiceCentre serviceCentre; + private ServiceCentreAddress address; + + @BeforeEach + void setup() { + serviceCentreId = UUID.randomUUID(); + addressId = UUID.randomUUID(); + serviceCentre = ServiceCentre.builder().id(serviceCentreId).name("Test Service Centre").build(); + address = ServiceCentreAddress.builder() + .id(addressId) + .serviceCentreId(serviceCentreId) + .addressLine1("1 Test Street") + .postcode("TE1 1ST") + .addressType(AddressType.VISIT_US) + .build(); + + OsDpa osDpa = OsDpa.builder().lat(51.5).lng(-0.1).build(); + OsResult osResult = OsResult.builder().dpa(osDpa).build(); + OsData osData = OsData.builder().results(List.of(osResult)).build(); + lenient().when(osService.getOsAddressByFullPostcode(anyString())).thenReturn(osData); + } + + @Test + void getAddressesReturnsParentScopedRecords() { + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(serviceCentreAddressRepository.findByServiceCentreId(serviceCentreId)).thenReturn(List.of(address)); + + assertThat(serviceCentreAddressService.getAddresses(serviceCentreId)).containsExactly(address); + } + + @Test + void getAddressThrowsNotFoundWhenMissing() { + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(serviceCentreAddressRepository.findByIdAndServiceCentreId(addressId, serviceCentreId)) + .thenReturn(Optional.empty()); + + NotFoundException exception = assertThrows( + NotFoundException.class, + () -> serviceCentreAddressService.getAddress(serviceCentreId, addressId) + ); + + assertThat(exception.getMessage()).contains(addressId.toString()); + } + + @Test + void createAddressSetsParentAndGeocodesPostcode() { + ServiceCentreAddress request = ServiceCentreAddress.builder() + .addressLine1("2 Test Street") + .postcode("TE1 1ST") + .build(); + + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(serviceCentreAddressRepository.save(any(ServiceCentreAddress.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + ServiceCentreAddress result = serviceCentreAddressService.createAddress(serviceCentreId, request); + + assertThat(result.getServiceCentreId()).isEqualTo(serviceCentreId); + assertThat(result.getServiceCentre()).isEqualTo(serviceCentre); + assertThat(result.getLat()).isEqualByComparingTo(BigDecimal.valueOf(51.5)); + assertThat(result.getLon()).isEqualByComparingTo(BigDecimal.valueOf(-0.1)); + } + + @Test + void updateAddressAppliesNewValues() { + ServiceCentreAddress request = ServiceCentreAddress.builder() + .addressLine1("Updated Street") + .addressLine2("Suite 1") + .townCity("Updated City") + .county("Updated County") + .postcode("UP1 2ED") + .addressType(AddressType.VISIT_OR_CONTACT_US) + .build(); + + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(serviceCentreAddressRepository.findByIdAndServiceCentreId(addressId, serviceCentreId)) + .thenReturn(Optional.of(address)); + when(serviceCentreAddressRepository.save(address)).thenReturn(address); + + ServiceCentreAddress result = serviceCentreAddressService.updateAddress(serviceCentreId, addressId, request); + + assertThat(result.getAddressLine1()).isEqualTo("Updated Street"); + assertThat(result.getAddressType()).isEqualTo(AddressType.VISIT_OR_CONTACT_US); + } + + @Test + void deleteAddressDeletesParentScopedRecord() { + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(serviceCentreAddressRepository.findByIdAndServiceCentreId(addressId, serviceCentreId)) + .thenReturn(Optional.of(address)); + + serviceCentreAddressService.deleteAddress(serviceCentreId, addressId); + + verify(serviceCentreAddressRepository).deleteByIdAndServiceCentreId(addressId, serviceCentreId); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAreasOfLawServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAreasOfLawServiceTest.java new file mode 100644 index 000000000..a06940bc4 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreAreasOfLawServiceTest.java @@ -0,0 +1,125 @@ +package uk.gov.hmcts.reform.fact.data.api.services; + +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.reform.fact.data.api.entities.AreaOfLawType; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreAreasOfLawRepository; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ServiceCentreAreasOfLawServiceTest { + + @Mock + private ServiceCentreAreasOfLawRepository serviceCentreAreasOfLawRepository; + + @Mock + private ServiceCentreService serviceCentreService; + + @Mock + private TypesService typesService; + + @InjectMocks + private ServiceCentreAreasOfLawService serviceCentreAreasOfLawService; + + @Test + void getServiceCentreAreasOfLawByServiceCentreIdReturnsRecord() { + UUID serviceCentreId = UUID.randomUUID(); + ServiceCentre serviceCentre = ServiceCentre.builder().id(serviceCentreId).build(); + ServiceCentreAreasOfLaw areasOfLaw = ServiceCentreAreasOfLaw.builder() + .serviceCentreId(serviceCentreId) + .build(); + + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(serviceCentreAreasOfLawRepository.findByServiceCentreId(serviceCentreId)) + .thenReturn(Optional.of(areasOfLaw)); + + assertThat(serviceCentreAreasOfLawService.getServiceCentreAreasOfLawByServiceCentreId(serviceCentreId)) + .isEqualTo(areasOfLaw); + } + + @Test + void getServiceCentreAreasOfLawByServiceCentreIdThrowsNotFoundWhenMissing() { + UUID serviceCentreId = UUID.randomUUID(); + ServiceCentre serviceCentre = ServiceCentre.builder().id(serviceCentreId).build(); + + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(serviceCentreAreasOfLawRepository.findByServiceCentreId(serviceCentreId)).thenReturn(Optional.empty()); + + assertThrows( + NotFoundException.class, + () -> serviceCentreAreasOfLawService.getServiceCentreAreasOfLawByServiceCentreId(serviceCentreId) + ); + } + + @Test + void getAreasOfLawStatusReturnsAvailabilityMap() { + UUID serviceCentreId = UUID.randomUUID(); + UUID selectedAreaId = UUID.randomUUID(); + UUID unselectedAreaId = UUID.randomUUID(); + ServiceCentre serviceCentre = ServiceCentre.builder().id(serviceCentreId).build(); + AreaOfLawType selectedArea = new AreaOfLawType(); + selectedArea.setId(selectedAreaId); + AreaOfLawType unselectedArea = new AreaOfLawType(); + unselectedArea.setId(unselectedAreaId); + ServiceCentreAreasOfLaw areasOfLaw = ServiceCentreAreasOfLaw.builder() + .serviceCentreId(serviceCentreId) + .areasOfLaw(List.of(selectedAreaId)) + .build(); + + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(serviceCentreAreasOfLawRepository.findByServiceCentreId(serviceCentreId)) + .thenReturn(Optional.of(areasOfLaw)); + when(typesService.getAreaOfLawTypes()).thenReturn(List.of(selectedArea, unselectedArea)); + + Map result = + serviceCentreAreasOfLawService.getAreasOfLawStatusByServiceCentreId(serviceCentreId); + + assertThat(result).containsEntry(selectedArea, true); + assertThat(result).containsEntry(unselectedArea, false); + } + + @Test + void setServiceCentreAreasOfLawCreatesOrUpdatesRecordWithValidIds() { + UUID serviceCentreId = UUID.randomUUID(); + UUID existingRecordId = UUID.randomUUID(); + UUID suppliedAreaId = UUID.randomUUID(); + UUID validAreaId = UUID.randomUUID(); + ServiceCentre serviceCentre = ServiceCentre.builder().id(serviceCentreId).build(); + AreaOfLawType validArea = new AreaOfLawType(); + validArea.setId(validAreaId); + ServiceCentreAreasOfLaw request = ServiceCentreAreasOfLaw.builder() + .areasOfLaw(List.of(suppliedAreaId)) + .build(); + ServiceCentreAreasOfLaw existing = ServiceCentreAreasOfLaw.builder().id(existingRecordId).build(); + + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(typesService.getAllAreasOfLawTypesByIds(List.of(suppliedAreaId))).thenReturn(List.of(validArea)); + when(serviceCentreAreasOfLawRepository.findByServiceCentreId(serviceCentreId)) + .thenReturn(Optional.of(existing)); + when(serviceCentreAreasOfLawRepository.save(request)).thenReturn(request); + + ServiceCentreAreasOfLaw result = + serviceCentreAreasOfLawService.setServiceCentreAreasOfLaw(serviceCentreId, request); + + assertThat(result.getId()).isEqualTo(existingRecordId); + assertThat(result.getServiceCentreId()).isEqualTo(serviceCentreId); + assertThat(result.getServiceCentre()).isEqualTo(serviceCentre); + assertThat(result.getAreasOfLaw()).containsExactly(validAreaId); + verify(serviceCentreAreasOfLawRepository).save(request); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreContactDetailsServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreContactDetailsServiceTest.java new file mode 100644 index 000000000..bc9802d4e --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreContactDetailsServiceTest.java @@ -0,0 +1,157 @@ +package uk.gov.hmcts.reform.fact.data.api.services; + +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.reform.fact.data.api.entities.ContactDescriptionType; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.repositories.ContactDescriptionTypeRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreContactDetailsRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ServiceCentreContactDetailsServiceTest { + + @Mock + private ServiceCentreContactDetailsRepository serviceCentreContactDetailsRepository; + + @Mock + private ContactDescriptionTypeRepository contactDescriptionTypeRepository; + + @Mock + private ServiceCentreService serviceCentreService; + + @InjectMocks + private ServiceCentreContactDetailsService serviceCentreContactDetailsService; + + @Test + void getContactDetailsReturnsParentScopedRecords() { + UUID serviceCentreId = UUID.randomUUID(); + ServiceCentre serviceCentre = ServiceCentre.builder().id(serviceCentreId).build(); + ServiceCentreContactDetails contactDetails = ServiceCentreContactDetails.builder() + .serviceCentreId(serviceCentreId) + .build(); + + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(serviceCentreContactDetailsRepository.findByServiceCentreId(serviceCentreId)) + .thenReturn(List.of(contactDetails)); + + assertThat(serviceCentreContactDetailsService.getContactDetails(serviceCentreId)) + .containsExactly(contactDetails); + } + + @Test + void createContactDetailAllowsMissingDescriptionWhenNull() { + UUID serviceCentreId = UUID.randomUUID(); + ServiceCentre serviceCentre = ServiceCentre.builder().id(serviceCentreId).build(); + ServiceCentreContactDetails request = ServiceCentreContactDetails.builder() + .email("test@example.com") + .build(); + + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(serviceCentreContactDetailsRepository.save(any(ServiceCentreContactDetails.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + ServiceCentreContactDetails result = + serviceCentreContactDetailsService.createContactDetail(serviceCentreId, request); + + assertThat(result.getServiceCentreId()).isEqualTo(serviceCentreId); + assertThat(result.getServiceCentre()).isEqualTo(serviceCentre); + assertThat(result.getServiceCentreContactDescription()).isNull(); + } + + @Test + void createContactDetailThrowsNotFoundWhenDescriptionMissing() { + UUID serviceCentreId = UUID.randomUUID(); + UUID contactDescriptionId = UUID.randomUUID(); + ServiceCentre serviceCentre = ServiceCentre.builder().id(serviceCentreId).build(); + ServiceCentreContactDetails request = ServiceCentreContactDetails.builder() + .serviceCentreContactDescriptionId(contactDescriptionId) + .build(); + + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(contactDescriptionTypeRepository.findById(contactDescriptionId)).thenReturn(Optional.empty()); + + assertThrows( + NotFoundException.class, + () -> serviceCentreContactDetailsService.createContactDetail(serviceCentreId, request) + ); + } + + @Test + void updateContactDetailAppliesNewValuesAndDescription() { + UUID serviceCentreId = UUID.randomUUID(); + UUID contactId = UUID.randomUUID(); + UUID contactDescriptionId = UUID.randomUUID(); + ContactDescriptionType contactDescriptionType = new ContactDescriptionType(); + ServiceCentre serviceCentre = ServiceCentre.builder().id(serviceCentreId).build(); + ServiceCentreContactDetails existing = ServiceCentreContactDetails.builder() + .id(contactId) + .serviceCentreId(serviceCentreId) + .build(); + ServiceCentreContactDetails request = ServiceCentreContactDetails.builder() + .serviceCentreContactDescriptionId(contactDescriptionId) + .explanation("General") + .email("updated@example.com") + .phoneNumber("020 123 456") + .build(); + + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(serviceCentreContactDetailsRepository.findByIdAndServiceCentreId(contactId, serviceCentreId)) + .thenReturn(Optional.of(existing)); + when(contactDescriptionTypeRepository.findById(contactDescriptionId)) + .thenReturn(Optional.of(contactDescriptionType)); + when(serviceCentreContactDetailsRepository.save(existing)).thenReturn(existing); + + ServiceCentreContactDetails result = + serviceCentreContactDetailsService.updateContactDetail(serviceCentreId, contactId, request); + + assertThat(result.getServiceCentreContactDescriptionId()).isEqualTo(contactDescriptionId); + assertThat(result.getServiceCentreContactDescription()).isEqualTo(contactDescriptionType); + assertThat(result.getEmail()).isEqualTo("updated@example.com"); + } + + @Test + void deleteContactDetailThrowsNotFoundWhenChildDoesNotBelongToParent() { + UUID serviceCentreId = UUID.randomUUID(); + UUID contactId = UUID.randomUUID(); + ServiceCentre serviceCentre = ServiceCentre.builder().id(serviceCentreId).build(); + + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(serviceCentreContactDetailsRepository.existsByIdAndServiceCentreId(contactId, serviceCentreId)) + .thenReturn(false); + + assertThrows( + NotFoundException.class, + () -> serviceCentreContactDetailsService.deleteContactDetail(serviceCentreId, contactId) + ); + } + + @Test + void deleteContactDetailDeletesParentScopedRecord() { + UUID serviceCentreId = UUID.randomUUID(); + UUID contactId = UUID.randomUUID(); + ServiceCentre serviceCentre = ServiceCentre.builder().id(serviceCentreId).build(); + + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(serviceCentreContactDetailsRepository.existsByIdAndServiceCentreId(contactId, serviceCentreId)) + .thenReturn(true); + + serviceCentreContactDetailsService.deleteContactDetail(serviceCentreId, contactId); + + verify(serviceCentreContactDetailsRepository).deleteByIdAndServiceCentreId(contactId, serviceCentreId); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java new file mode 100644 index 000000000..05cf18173 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java @@ -0,0 +1,141 @@ +package uk.gov.hmcts.reform.fact.data.api.services; + +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.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ServiceCentreServiceTest { + + @Mock + private ServiceCentreRepository serviceCentreRepository; + + @Mock + private ServiceAreaRepository serviceAreaRepository; + + @InjectMocks + private ServiceCentreService serviceCentreService; + + @Test + void getServiceCentreByIdReturnsServiceCentreWhenFound() { + UUID serviceCentreId = UUID.randomUUID(); + ServiceCentre serviceCentre = ServiceCentre.builder().id(serviceCentreId).name("Test Service Centre").build(); + + when(serviceCentreRepository.findById(serviceCentreId)).thenReturn(Optional.of(serviceCentre)); + + ServiceCentre result = serviceCentreService.getServiceCentreById(serviceCentreId); + + assertThat(result).isEqualTo(serviceCentre); + } + + @Test + void getServiceCentreByIdThrowsNotFoundWhenMissing() { + UUID serviceCentreId = UUID.randomUUID(); + when(serviceCentreRepository.findById(serviceCentreId)).thenReturn(Optional.empty()); + + NotFoundException exception = assertThrows( + NotFoundException.class, + () -> serviceCentreService.getServiceCentreById(serviceCentreId) + ); + + assertThat(exception.getMessage()).isEqualTo("Service centre not found, ID: " + serviceCentreId); + } + + @Test + void getServiceCentreByNameReturnsServiceCentre() { + ServiceCentre serviceCentre = ServiceCentre.builder().name("Bulk Scan Centre").build(); + + when(serviceCentreRepository.findByName("Bulk Scan Centre")).thenReturn(Optional.of(serviceCentre)); + + assertThat(serviceCentreService.getServiceCentreByName("Bulk Scan Centre")).isEqualTo(serviceCentre); + } + + @Test + void createServiceCentreGeneratesUniqueSlugStartsClosedAndNormalisesServiceAreas() { + UUID suppliedServiceAreaId = UUID.randomUUID(); + UUID validServiceAreaId = UUID.randomUUID(); + ServiceArea serviceArea = ServiceArea.builder().id(validServiceAreaId).build(); + ServiceCentre request = ServiceCentre.builder() + .name("Bulk Scan Centre") + .open(true) + .serviceAreaIds(List.of(suppliedServiceAreaId)) + .catchmentType(CatchmentType.REGIONAL) + .build(); + + when(serviceAreaRepository.findAllById(List.of(suppliedServiceAreaId))).thenReturn(List.of(serviceArea)); + when(serviceCentreRepository.existsBySlug("bulk-scan-centre")).thenReturn(true); + when(serviceCentreRepository.existsBySlug("bulk-scan-centre-1")).thenReturn(false); + when(serviceCentreRepository.save(any(ServiceCentre.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + ServiceCentre result = serviceCentreService.createServiceCentre(request); + + assertThat(result.getSlug()).isEqualTo("bulk-scan-centre-1"); + assertThat(result.getOpen()).isFalse(); + assertThat(result.getServiceAreaIds()).containsExactly(validServiceAreaId); + assertThat(result.getCatchmentType()).isEqualTo(CatchmentType.NATIONAL); + } + + @Test + void updateServiceCentreRegeneratesSlugWhenNameChanges() { + UUID serviceCentreId = UUID.randomUUID(); + ServiceCentre existing = ServiceCentre.builder() + .id(serviceCentreId) + .name("Old Service Centre") + .slug("old-service-centre") + .open(false) + .build(); + ServiceCentre request = ServiceCentre.builder() + .name("New Service Centre") + .open(true) + .warningNotice("Warning") + .serviceAreaIds(List.of()) + .catchmentType(CatchmentType.LOCAL) + .build(); + + when(serviceCentreRepository.findById(serviceCentreId)).thenReturn(Optional.of(existing)); + when(serviceCentreRepository.existsBySlug("new-service-centre")).thenReturn(false); + when(serviceAreaRepository.findAllById(List.of())).thenReturn(List.of()); + when(serviceCentreRepository.save(existing)).thenReturn(existing); + + ServiceCentre result = serviceCentreService.updateServiceCentre(serviceCentreId, request); + + assertThat(result.getName()).isEqualTo("New Service Centre"); + assertThat(result.getSlug()).isEqualTo("new-service-centre"); + assertThat(result.getOpen()).isTrue(); + assertThat(result.getWarningNotice()).isEqualTo("Warning"); + assertThat(result.getCatchmentType()).isEqualTo(CatchmentType.NATIONAL); + } + + @Test + void deleteServiceCentresByNamePrefixDeletesMatchingRowsForTestingSupport() { + ServiceCentre first = ServiceCentre.builder().id(UUID.randomUUID()).name("SC Delete One").build(); + ServiceCentre second = ServiceCentre.builder().id(UUID.randomUUID()).name("SC Delete Two").build(); + + List serviceCentres = List.of(first, second); + when(serviceCentreRepository.findByNameStartingWithIgnoreCase("SC Delete")).thenReturn(serviceCentres); + + long deleted = serviceCentreService.deleteServiceCentresByNamePrefix("SC Delete"); + + assertThat(deleted).isEqualTo(2); + verify(serviceCentreRepository).deleteAllInBatch(serviceCentres); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportServiceTest.java index eef35fe82..374bac2be 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportServiceTest.java @@ -32,8 +32,14 @@ import uk.gov.hmcts.reform.fact.data.api.entities.LocalAuthorityType; import uk.gov.hmcts.reform.fact.data.api.entities.OpeningHourType; import uk.gov.hmcts.reform.fact.data.api.entities.Region; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAddress; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.entities.types.AllowedLocalAuthorityAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.models.AreaOfLawSelectionDto; import uk.gov.hmcts.reform.fact.data.api.models.CourtLocalAuthorityDto; import uk.gov.hmcts.reform.fact.data.api.repositories.AreaOfLawTypeRepository; @@ -43,6 +49,7 @@ import uk.gov.hmcts.reform.fact.data.api.repositories.LocalAuthorityTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.OpeningHoursTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.RegionRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; import java.util.List; import java.util.Optional; @@ -85,6 +92,14 @@ class TestingSupportServiceTest { @Mock private CourtLocalAuthoritiesService courtLocalAuthoritiesService; @Mock + private ServiceCentreService serviceCentreService; + @Mock + private ServiceCentreAddressService serviceCentreAddressService; + @Mock + private ServiceCentreAreasOfLawService serviceCentreAreasOfLawService; + @Mock + private ServiceCentreContactDetailsService serviceCentreContactDetailsService; + @Mock private CourtPhotoService courtPhotoService; @Mock private CourtPhotoRepository courtPhotoRepository; @@ -104,6 +119,8 @@ class TestingSupportServiceTest { private OpeningHoursTypeRepository openingHoursTypeRepository; @Mock private RegionRepository regionRepository; + @Mock + private ServiceAreaRepository serviceAreaRepository; @InjectMocks private TestingSupportService testingSupportService; @@ -116,6 +133,7 @@ class TestingSupportServiceTest { private final List contactDescIds = randomUuidList(); private final List localAuthorityIds = randomUuidList(); private final List openingHourTypeIds = randomUuidList(); + private final List serviceAreaIds = randomUuidList(); private static List randomAreasOfLaw() { Random random = new Random(); @@ -191,6 +209,9 @@ void setup() { lenient().when(openingHoursTypeRepository.findAll()).thenReturn( openingHourTypeIds.stream().map(id -> OpeningHourType.builder().id(id).build()).toList() ); + lenient().when(serviceAreaRepository.findAll()).thenReturn( + serviceAreaIds.stream().map(id -> ServiceArea.builder().id(id).build()).toList() + ); lenient().when(auditUserContext.getUserId()).thenReturn(Optional.of(auditUserId)); lenient().when(auditUserContext.requireUserId()).thenReturn(auditUserId); lenient().when(userService.createOrUpdateLastLoginUser(any(User.class))).thenAnswer(inv -> { @@ -342,6 +363,63 @@ void createCourtWithoutEnquiriesContactSkipsContactCreation() { verify(courtContactDetailsService, never()).createContactDetail(any(), any()); } + @Test + void createServiceCentreCreatesCoreServiceCentreData() { + String serviceCentreName = "Test Service Centre"; + UUID serviceCentreId = UUID.randomUUID(); + + when(serviceCentreService.createServiceCentre(any())).thenAnswer(inv -> { + ServiceCentre serviceCentre = ServiceCentre.class.cast(inv.getArguments()[0]); + serviceCentre.setSlug("test-service-centre"); + serviceCentre.setId(serviceCentreId); + return serviceCentre; + }); + when(serviceCentreAreasOfLawService.setServiceCentreAreasOfLaw(any(), any())) + .thenAnswer(inv -> inv.getArgument(1)); + when(serviceCentreAddressService.createAddress(any(), any())).thenAnswer(inv -> inv.getArgument(1)); + when(serviceCentreContactDetailsService.createContactDetail(any(), any())) + .thenAnswer(inv -> inv.getArgument(1)); + when(serviceCentreService.updateServiceCentre(eq(serviceCentreId), any(ServiceCentre.class))) + .thenAnswer(inv -> inv.getArgument(1)); + + ServiceCentre result = testingSupportService.createServiceCentre(serviceCentreName, 1L, true, true, true); + + assertThat(result.getSlug()).isEqualTo("test-service-centre"); + ArgumentCaptor serviceCentreArgumentCaptor = ArgumentCaptor.forClass(ServiceCentre.class); + verify(serviceCentreService).createServiceCentre(serviceCentreArgumentCaptor.capture()); + assertThat(serviceCentreArgumentCaptor.getValue().getName()).isEqualTo(serviceCentreName); + assertThat(serviceCentreArgumentCaptor.getValue().getServiceAreaIds()).isNotEmpty(); + assertThat(serviceCentreArgumentCaptor.getValue().getCatchmentType()).isEqualTo(CatchmentType.NATIONAL); + verify(serviceCentreService).updateServiceCentre(eq(serviceCentreId), any(ServiceCentre.class)); + verify(serviceCentreAreasOfLawService).setServiceCentreAreasOfLaw( + eq(serviceCentreId), + any(ServiceCentreAreasOfLaw.class) + ); + verify(serviceCentreAddressService).createAddress(eq(serviceCentreId), any(ServiceCentreAddress.class)); + verify(serviceCentreContactDetailsService).createContactDetail( + eq(serviceCentreId), + any(ServiceCentreContactDetails.class) + ); + } + + @Test + void createServiceCentreWithoutContactDetailsSkipsContactCreation() { + String serviceCentreName = "Test Service Centre"; + UUID serviceCentreId = UUID.randomUUID(); + + when(serviceCentreService.createServiceCentre(any())).thenAnswer(inv -> { + ServiceCentre serviceCentre = ServiceCentre.class.cast(inv.getArguments()[0]); + serviceCentre.setSlug("test-service-centre"); + serviceCentre.setId(serviceCentreId); + return serviceCentre; + }); + + ServiceCentre result = testingSupportService.createServiceCentre(serviceCentreName, 1L, false, false, false); + + assertThat(result.getSlug()).isEqualTo("test-service-centre"); + verify(serviceCentreContactDetailsService, never()).createContactDetail(any(), any()); + } + @Captor private ArgumentCaptor> aolSelectionDtoArgumentCaptor; @Captor From 1045a54c0546f7374eb4ee5f4c677cef0fdb5823 Mon Sep 17 00:00:00 2001 From: Josh <23316525+josh-uk@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:44:14 +0100 Subject: [PATCH 04/40] FACT-2618 Added details endpoints/view (#279) * Added details endpoints/view * fix func --- build.gradle | 1 + ...ServiceCentreControllerFunctionalTest.java | 36 +++ .../TestingSupportControllerTest.java | 29 +++ .../ServiceCentreControllerTest.java | 82 ++++++- .../controllers/ServiceCentreController.java | 37 ++- .../controllers/TestingSupportController.java | 25 ++- .../fact/data/api/entities/AreaOfLawType.java | 3 +- .../api/entities/ContactDescriptionType.java | 3 +- .../fact/data/api/entities/ServiceArea.java | 3 + .../api/entities/ServiceCentreAddress.java | 3 + .../api/entities/ServiceCentreAreasOfLaw.java | 28 ++- .../entities/ServiceCentreContactDetails.java | 16 +- .../api/entities/ServiceCentreDetails.java | 141 ++++++++++++ .../ServiceCentreDetailsRepository.java | 11 + .../ServiceCentreDetailsViewService.java | 135 +++++++++++ .../api/services/ServiceCentreService.java | 15 ++ .../ServiceCentreControllerTest.java | 29 ++- .../TestingSupportControllerTest.java | 21 +- .../entities/ServiceCentreAreasOfLawTest.java | 87 ++++++++ .../ServiceCentreDetailsViewServiceTest.java | 211 ++++++++++++++++++ .../services/ServiceCentreServiceTest.java | 33 +++ 21 files changed, 921 insertions(+), 28 deletions(-) create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreDetails.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreDetailsRepository.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreDetailsViewService.java create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLawTest.java create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreDetailsViewServiceTest.java diff --git a/build.gradle b/build.gradle index fa7fc5b0c..e2b50f4c2 100644 --- a/build.gradle +++ b/build.gradle @@ -83,6 +83,7 @@ tasks.withType(JavaExec).configureEach { tasks.withType(Test).configureEach { useJUnitPlatform() failFast = true + systemProperty 'org.aspectj.weaver.Dump.exception', 'false' testLogging { exceptionFormat = 'full' diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreControllerFunctionalTest.java index 83844d435..3a99df3c1 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreControllerFunctionalTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/ServiceCentreControllerFunctionalTest.java @@ -43,6 +43,10 @@ void shouldCreateReadAndUpdateServiceCentre() { String createdName = createResponse.jsonPath().getString("name"); assertThat(getResponse.jsonPath().getString("name")).isEqualTo(createdName); + Response entityResponse = http.doGet("/service-centres/" + serviceCentreId + "/entity/v1"); + AssertionHelper.assertStatus(entityResponse, OK); + assertThat(entityResponse.jsonPath().getString("name")).isEqualTo(createdName); + Response nameResponse = http.doGet("/service-centres/name/v1", Map.of("name", createdName)); AssertionHelper.assertStatus(nameResponse, OK); assertThat(UUID.fromString(nameResponse.jsonPath().getString("id"))).isEqualTo(serviceCentreId); @@ -62,6 +66,38 @@ void shouldCreateReadAndUpdateServiceCentre() { AssertionHelper.assertStatus(missingResponse, NOT_FOUND); } + @Test + @DisplayName("Service centre details endpoint returns child resources") + void shouldReturnServiceCentreDetailsWithChildResources() { + Response createResponse = http.doGet( + "/testing-support/service-centres", + Map.of( + "serviceCentreName", TestDataHelper.appendRandomSuffixToCourtName(TEST_PREFIX + " Details"), + "withContactDetails", "true" + ) + ); + AssertionHelper.assertStatus(createResponse, CREATED); + + UUID serviceCentreId = UUID.fromString(createResponse.jsonPath().getString("id")); + + Response detailsResponse = http.doGet("/service-centres/" + serviceCentreId + "/v1"); + AssertionHelper.assertStatus(detailsResponse, OK); + assertThat(detailsResponse.jsonPath().getList("serviceCentreAddresses")).isNotEmpty(); + assertThat(detailsResponse.jsonPath().getList("serviceAreas")).isNotEmpty(); + assertThat(detailsResponse.jsonPath().getList("serviceAreaIds")).isNull(); + assertThat(detailsResponse.jsonPath().getList("serviceCentreContactDetails")).isNotEmpty(); + assertThat(detailsResponse.jsonPath().getList("serviceCentreAreasOfLaw")).isNotEmpty(); + assertThat(detailsResponse.jsonPath().getString( + "serviceCentreContactDetails[0].serviceCentreContactDescription.id" + )).isNotBlank(); + assertThat(detailsResponse.jsonPath().getString( + "serviceCentreContactDetails[0].serviceCentreContactDescriptionId" + )).isNull(); + assertThat(detailsResponse.jsonPath().getString( + "serviceCentreAreasOfLaw[0].areasOfLaw[0].id" + )).isNotBlank(); + } + @AfterAll static void cleanUpTestData() { http.doDelete("/testing-support/service-centres/name-prefix/" + TEST_PREFIX); diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/TestingSupportControllerTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/TestingSupportControllerTest.java index 8f6f3853c..3105c344d 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/TestingSupportControllerTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/TestingSupportControllerTest.java @@ -3,9 +3,12 @@ import static org.assertj.core.api.Assertions.assertThat; import uk.gov.hmcts.reform.fact.data.api.entities.CourtDetails; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; import uk.gov.hmcts.reform.fact.functional.helpers.AssertionHelper; import uk.gov.hmcts.reform.fact.functional.http.HttpClient; +import java.util.Map; + import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.cfg.DateTimeFeature; import tools.jackson.databind.json.JsonMapper; @@ -65,8 +68,34 @@ void testingSupportEndpointAuth(String courtName) { assertThat(retrievedCourt.getCourtPhotos()).isNotEmpty(); } + @ParameterizedTest + @DisplayName("Test random service centre generation returns details") + @ValueSource(strings = {"TSC Service Centre A", "TSC Service Centre B"}) + void testingSupportServiceCentreEndpointReturnsDetails(String serviceCentreName) { + Response response = http.doGet( + "/testing-support/service-centres", + Map.of("serviceCentreName", serviceCentreName) + ); + + assertThat(response).isNotNull(); + AssertionHelper.assertStatus(response, HttpStatus.CREATED); + + ServiceCentreDetails createdServiceCentre = mapper.readValue( + response.getBody().asString(), + ServiceCentreDetails.class + ); + + assertThat(createdServiceCentre.getId()).isNotNull(); + assertThat(createdServiceCentre.getSlug()).isNotNull(); + assertThat(createdServiceCentre.getOpen()).isTrue(); + assertThat(createdServiceCentre.getServiceCentreAddresses()).isNotEmpty(); + assertThat(createdServiceCentre.getServiceCentreContactDetails()).isNotEmpty(); + assertThat(createdServiceCentre.getServiceCentreAreasOfLaw()).isNotEmpty(); + } + @AfterAll static void cleanUp() { http.doDelete("/testing-support/courts/name-prefix/TSC Auth Test"); + http.doDelete("/testing-support/service-centres/name-prefix/TSC Service Centre"); } } diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java index 4736e23d7..ecb100538 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java @@ -11,11 +11,19 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import tools.jackson.databind.ObjectMapper; +import uk.gov.hmcts.reform.fact.data.api.entities.AreaOfLawType; +import uk.gov.hmcts.reform.fact.data.api.entities.ContactDescriptionType; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreDetailsViewService; import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreService; +import java.util.List; import java.util.UUID; import static org.mockito.ArgumentMatchers.any; @@ -35,6 +43,9 @@ class ServiceCentreControllerTest { private static final UUID SERVICE_CENTRE_ID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + private static final UUID CONTACT_DESCRIPTION_ID = UUID.fromString("223e4567-e89b-12d3-a456-426614174000"); + private static final UUID AREA_OF_LAW_ID = UUID.fromString("323e4567-e89b-12d3-a456-426614174000"); + private static final UUID SERVICE_AREA_ID = UUID.fromString("423e4567-e89b-12d3-a456-426614174000"); private static final String SERVICE_CENTRE_NAME = "Test Service Centre"; @Autowired @@ -46,17 +57,31 @@ class ServiceCentreControllerTest { @MockitoBean private ServiceCentreService serviceCentreService; + @MockitoBean + private ServiceCentreDetailsViewService serviceCentreDetailsViewService; + @Test - @DisplayName("GET /service-centres/{serviceCentreId}/v1 returns service centre") - void getServiceCentreByIdReturnsServiceCentre() throws Exception { - ServiceCentre serviceCentre = buildServiceCentre(); + @DisplayName("GET /service-centres/{serviceCentreId}/v1 returns service centre details") + void getServiceCentreDetailsByIdReturnsServiceCentreDetails() throws Exception { + ServiceCentreDetails serviceCentreDetails = buildServiceCentreDetails(); - when(serviceCentreService.getServiceCentreById(SERVICE_CENTRE_ID)).thenReturn(serviceCentre); + when(serviceCentreService.getServiceCentreDetailsById(SERVICE_CENTRE_ID)).thenReturn(serviceCentreDetails); + when(serviceCentreDetailsViewService.prepareDetailsView(serviceCentreDetails)).thenReturn(serviceCentreDetails); mockMvc.perform(get("/service-centres/{serviceCentreId}/v1", SERVICE_CENTRE_ID)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(SERVICE_CENTRE_ID.toString())) - .andExpect(jsonPath("$.name").value(SERVICE_CENTRE_NAME)); + .andExpect(jsonPath("$.name").value(SERVICE_CENTRE_NAME)) + .andExpect(jsonPath("$.serviceAreaIds").doesNotExist()) + .andExpect(jsonPath("$.serviceAreas[0].id").value(SERVICE_AREA_ID.toString())) + .andExpect(jsonPath("$.serviceAreas[0].name").value("Family")) + .andExpect(jsonPath("$.serviceCentreContactDetails[0].serviceCentreContactDescriptionId").doesNotExist()) + .andExpect(jsonPath("$.serviceCentreContactDetails[0].serviceCentreContactDescription.id") + .value(CONTACT_DESCRIPTION_ID.toString())) + .andExpect(jsonPath("$.serviceCentreContactDetails[0].serviceCentreContactDescription.name") + .value("Enquiries")) + .andExpect(jsonPath("$.serviceCentreAreasOfLaw[0].areasOfLaw[0].id").value(AREA_OF_LAW_ID.toString())) + .andExpect(jsonPath("$.serviceCentreAreasOfLaw[0].areasOfLaw[0].name").value("Civil")); } @Test @@ -69,13 +94,26 @@ void getServiceCentreByIdReturnsBadRequestForInvalidUuid() throws Exception { @Test @DisplayName("GET /service-centres/{serviceCentreId}/v1 returns 404 when missing") void getServiceCentreByIdReturnsNotFound() throws Exception { - when(serviceCentreService.getServiceCentreById(SERVICE_CENTRE_ID)) + when(serviceCentreService.getServiceCentreDetailsById(SERVICE_CENTRE_ID)) .thenThrow(new NotFoundException("Service centre not found")); mockMvc.perform(get("/service-centres/{serviceCentreId}/v1", SERVICE_CENTRE_ID)) .andExpect(status().isNotFound()); } + @Test + @DisplayName("GET /service-centres/{serviceCentreId}/entity/v1 returns service centre entity") + void getServiceCentreEntityByIdReturnsServiceCentre() throws Exception { + ServiceCentre serviceCentre = buildServiceCentre(); + + when(serviceCentreService.getServiceCentreById(SERVICE_CENTRE_ID)).thenReturn(serviceCentre); + + mockMvc.perform(get("/service-centres/{serviceCentreId}/entity/v1", SERVICE_CENTRE_ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(SERVICE_CENTRE_ID.toString())) + .andExpect(jsonPath("$.name").value(SERVICE_CENTRE_NAME)); + } + @Test @DisplayName("GET /service-centres/name/v1 returns service centre for exact name") void getServiceCentreByNameReturnsServiceCentre() throws Exception { @@ -127,4 +165,36 @@ private ServiceCentre buildServiceCentre() { .catchmentType(CatchmentType.REGIONAL) .build(); } + + private ServiceCentreDetails buildServiceCentreDetails() { + ServiceCentreContactDetails contactDetails = ServiceCentreContactDetails.builder() + .serviceCentreContactDescriptionId(CONTACT_DESCRIPTION_ID) + .serviceCentreContactDescriptionDetails(ContactDescriptionType.builder() + .id(CONTACT_DESCRIPTION_ID) + .name("Enquiries") + .build()) + .build(); + ServiceCentreAreasOfLaw areasOfLaw = ServiceCentreAreasOfLaw.builder() + .areasOfLaw(List.of(AREA_OF_LAW_ID)) + .areasOfLawDetails(List.of(AreaOfLawType.builder() + .id(AREA_OF_LAW_ID) + .name("Civil") + .build())) + .build(); + + return ServiceCentreDetails.builder() + .id(SERVICE_CENTRE_ID) + .name(SERVICE_CENTRE_NAME) + .slug("test-service-centre") + .open(true) + .catchmentType(CatchmentType.NATIONAL) + .serviceAreaIds(List.of(SERVICE_AREA_ID)) + .serviceAreaDetails(List.of(ServiceArea.builder() + .id(SERVICE_AREA_ID) + .name("Family") + .build())) + .serviceCentreContactDetails(List.of(contactDetails)) + .serviceCentreAreasOfLaw(List.of(areasOfLaw)) + .build(); + } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreController.java index f6d7b190d..914d6a649 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreController.java @@ -1,5 +1,6 @@ package uk.gov.hmcts.reform.fact.data.api.controllers; +import com.fasterxml.jackson.annotation.JsonView; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -20,7 +21,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreDetailsViewService; import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreService; import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidUUID; @@ -35,15 +38,43 @@ @SuppressWarnings("java:S4684") public class ServiceCentreController { + /** + * JsonView marker for expanded service centre details responses. + */ + public interface ServiceCentreDetailsView { + } + private final ServiceCentreService serviceCentreService; + private final ServiceCentreDetailsViewService serviceCentreDetailsViewService; @GetMapping("/{serviceCentreId}/v1") + @JsonView(ServiceCentreDetailsView.class) @Operation( - summary = "Get service centre by ID", - description = "Fetch a service centre for a given service centre ID." + summary = "Get service centre details by ID", + description = "Fetch detailed service centre information for a given service centre ID." ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved service centre"), + @ApiResponse(responseCode = "200", description = "Successfully retrieved service centre details"), + @ApiResponse(responseCode = "400", description = "Invalid service centre ID supplied"), + @ApiResponse(responseCode = "404", description = "Service centre not found") + }) + public ResponseEntity getServiceCentreDetailsById( + @Parameter(description = "UUID of the service centre", required = true) + @ValidUUID @PathVariable String serviceCentreId) { + return ResponseEntity.ok( + serviceCentreDetailsViewService.prepareDetailsView( + serviceCentreService.getServiceCentreDetailsById(UUID.fromString(serviceCentreId)) + ) + ); + } + + @GetMapping("/{serviceCentreId}/entity/v1") + @Operation( + summary = "Get service centre entity by ID", + description = "Fetch the service centre entity for a given service centre ID." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved service centre entity"), @ApiResponse(responseCode = "400", description = "Invalid service centre ID supplied"), @ApiResponse(responseCode = "404", description = "Service centre not found") }) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java index 26af16392..245cfbdca 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java @@ -1,10 +1,13 @@ package uk.gov.hmcts.reform.fact.data.api.controllers; +import com.fasterxml.jackson.annotation.JsonView; import uk.gov.hmcts.reform.fact.data.api.entities.CourtDetails; import uk.gov.hmcts.reform.fact.data.api.entities.Region; -import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; +import uk.gov.hmcts.reform.fact.data.api.controllers.ServiceCentreController.ServiceCentreDetailsView; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; import uk.gov.hmcts.reform.fact.data.api.services.RegionService; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreDetailsViewService; import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreService; import uk.gov.hmcts.reform.fact.data.api.services.TestingSupportService; @@ -40,6 +43,7 @@ public class TestingSupportController { private final CourtService courtService; private final RegionService regionService; + private final ServiceCentreDetailsViewService serviceCentreDetailsViewService; private final ServiceCentreService serviceCentreService; private final TestingSupportService testingSupportService; @@ -126,6 +130,7 @@ public ResponseEntity createSampleCourt( } @GetMapping("/service-centres") + @JsonView(ServiceCentreDetailsView.class) @Operation( summary = "Create sample service centre", description = "Creates a sample service centre with randomised data for testing purposes." @@ -133,19 +138,21 @@ public ResponseEntity createSampleCourt( @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Successfully created sample service centre"), }) - public ResponseEntity createSampleServiceCentre( + public ResponseEntity createSampleServiceCentre( @RequestParam(required = true) String serviceCentreName, @RequestParam(required = false) Long seed, @RequestParam(required = false, defaultValue = "true") boolean open, @RequestParam(required = false, defaultValue = "false") boolean addWarningNotice, @RequestParam(required = false, defaultValue = "true") boolean withContactDetails) { + UUID serviceCentreId = testingSupportService.createServiceCentre( + serviceCentreName, + seed, + open, + addWarningNotice, + withContactDetails + ).getId(); + ServiceCentreDetails details = serviceCentreService.getServiceCentreDetailsById(serviceCentreId); return ResponseEntity.status(HttpStatus.CREATED) - .body(testingSupportService.createServiceCentre( - serviceCentreName, - seed, - open, - addWarningNotice, - withContactDetails - )); + .body(serviceCentreDetailsViewService.prepareDetailsView(details)); } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AreaOfLawType.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AreaOfLawType.java index e3004f63e..e1ad18b21 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AreaOfLawType.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AreaOfLawType.java @@ -17,6 +17,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import uk.gov.hmcts.reform.fact.data.api.controllers.CourtController.CourtDetailsView; +import uk.gov.hmcts.reform.fact.data.api.controllers.ServiceCentreController.ServiceCentreDetailsView; @Data @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -24,7 +25,7 @@ @Builder @Entity @Table(name = "area_of_law_types") -@JsonView(CourtDetailsView.class) +@JsonView({CourtDetailsView.class, ServiceCentreDetailsView.class}) public class AreaOfLawType { @Schema( diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ContactDescriptionType.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ContactDescriptionType.java index 01043b44b..d68b123f4 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ContactDescriptionType.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ContactDescriptionType.java @@ -16,13 +16,14 @@ import lombok.Data; import lombok.NoArgsConstructor; import uk.gov.hmcts.reform.fact.data.api.controllers.CourtController.CourtDetailsView; +import uk.gov.hmcts.reform.fact.data.api.controllers.ServiceCentreController.ServiceCentreDetailsView; @Data @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor @Builder @Entity -@JsonView(CourtDetailsView.class) +@JsonView({CourtDetailsView.class, ServiceCentreDetailsView.class}) @Table(name = "contact_description_types") public class ContactDescriptionType { diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceArea.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceArea.java index 2b62fd031..a011d0c11 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceArea.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceArea.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -27,12 +28,14 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import uk.gov.hmcts.reform.fact.data.api.controllers.ServiceCentreController.ServiceCentreDetailsView; @Data @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor @Builder @Entity +@JsonView(ServiceCentreDetailsView.class) @Table(name = "service_area", schema = "public") public class ServiceArea { diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAddress.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAddress.java index 71afc8a87..ad95c327c 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAddress.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAddress.java @@ -1,6 +1,7 @@ package uk.gov.hmcts.reform.fact.data.api.entities; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonView; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -23,6 +24,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import uk.gov.hmcts.reform.fact.data.api.audit.AuditableCourtEntityListener; +import uk.gov.hmcts.reform.fact.data.api.controllers.ServiceCentreController.ServiceCentreDetailsView; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.AddressType; import uk.gov.hmcts.reform.fact.data.api.entities.validation.ValidationConstants; @@ -37,6 +39,7 @@ @Builder @Entity @EntityListeners(AuditableCourtEntityListener.class) +@JsonView(ServiceCentreDetailsView.class) @Table(name = "service_centre_address") public class ServiceCentreAddress implements AuditableEntity { diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java index e89ece795..82783123f 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.JsonView; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -24,9 +25,11 @@ import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; import uk.gov.hmcts.reform.fact.data.api.audit.AuditableCourtEntityListener; +import uk.gov.hmcts.reform.fact.data.api.controllers.ServiceCentreController.ServiceCentreDetailsView; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import java.util.List; +import java.util.Map; import java.util.UUID; @Data @@ -35,6 +38,7 @@ @Builder @Entity @EntityListeners(AuditableCourtEntityListener.class) +@JsonView(ServiceCentreDetailsView.class) @Table(name = "service_centre_areas_of_law") public class ServiceCentreAreasOfLaw implements AuditableEntity { @@ -65,6 +69,7 @@ public class ServiceCentreAreasOfLaw implements AuditableEntity { @JsonIgnore private List areasOfLawDetails; + @JsonView(ServiceCentreDetailsView.class) @JsonProperty("areasOfLaw") public List getAreasOfLawForView() { return areasOfLawDetails != null ? areasOfLawDetails : areasOfLaw; @@ -81,8 +86,27 @@ public void setServiceCentreId(UUID serviceCentreId) { } @JsonSetter("areasOfLaw") - public void setAreasOfLaw(List areasOfLaw) { - this.areasOfLaw = areasOfLaw; + public void setAreasOfLaw(List areasOfLaw) { + this.areasOfLaw = areasOfLaw == null + ? null + : areasOfLaw.stream().map(ServiceCentreAreasOfLaw::extractAreaOfLawId).toList(); + } + + private static UUID extractAreaOfLawId(Object areaOfLaw) { + if (areaOfLaw instanceof UUID id) { + return id; + } + if (areaOfLaw instanceof String id) { + return UUID.fromString(id); + } + if (areaOfLaw instanceof AreaOfLawType areaOfLawType) { + return areaOfLawType.getId(); + } + if (areaOfLaw instanceof Map areaOfLawType) { + Object id = areaOfLawType.get("id"); + return id instanceof UUID uuid ? uuid : UUID.fromString(String.valueOf(id)); + } + return UUID.fromString(String.valueOf(areaOfLaw)); } @Override diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreContactDetails.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreContactDetails.java index 43ac8f7e3..667a3340c 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreContactDetails.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreContactDetails.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -24,6 +25,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import uk.gov.hmcts.reform.fact.data.api.audit.AuditableCourtEntityListener; +import uk.gov.hmcts.reform.fact.data.api.controllers.ServiceCentreController.ServiceCentreDetailsView; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.validation.ValidationConstants; @@ -44,11 +46,13 @@ public class ServiceCentreContactDetails implements AuditableEntity { ) @Id @GeneratedValue(strategy = GenerationType.AUTO) + @JsonView(ServiceCentreDetailsView.class) private UUID id; @Schema(description = "The ID of the associated Service Centre", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull @Column(name = "service_centre_id") + @JsonView(ServiceCentreDetailsView.class) private UUID serviceCentreId; @JsonIgnore @@ -58,10 +62,11 @@ public class ServiceCentreContactDetails implements AuditableEntity { @Schema(description = "The ID of the associated Contact Description Type") @Column(name = "service_centre_contact_description_id") + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private UUID serviceCentreContactDescriptionId; @JsonIgnore - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "service_centre_contact_description_id", insertable = false, updatable = false) private ContactDescriptionType serviceCentreContactDescription; @@ -69,10 +74,13 @@ public class ServiceCentreContactDetails implements AuditableEntity { @JsonIgnore private ContactDescriptionType serviceCentreContactDescriptionDetails; + @JsonView(ServiceCentreDetailsView.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonProperty("serviceCentreContactDescription") public ContactDescriptionType getServiceCentreContactDescriptionForView() { - return serviceCentreContactDescriptionDetails; + return serviceCentreContactDescriptionDetails != null + ? serviceCentreContactDescriptionDetails + : serviceCentreContactDescription; } @Schema(description = "The explanation") @@ -82,6 +90,7 @@ public ContactDescriptionType getServiceCentreContactDescriptionForView() { regexp = "^[A-Za-z0-9 '\\-()&+]*$", message = "Explanation contains invalid characters. Allowed: letters, numbers, spaces, apostrophes, - ( ) & +" ) + @JsonView(ServiceCentreDetailsView.class) private String explanation; @Schema(description = "The Welsh language explanation") @@ -92,16 +101,19 @@ public ContactDescriptionType getServiceCentreContactDescriptionForView() { message = "Welsh explanation contains invalid characters. Allowed: letters (with accents), numbers, spaces, " + "apostrophes, - ( ) & +" ) + @JsonView(ServiceCentreDetailsView.class) private String explanationCy; @Schema(description = "The associated email address") @Size(max = ValidationConstants.EMAIL_MAX_LENGTH, message = ValidationConstants.EMAIL_MAX_LENGTH_MESSAGE) @Pattern(regexp = ValidationConstants.EMAIL_REGEX, message = ValidationConstants.EMAIL_REGEX_MESSAGE) + @JsonView(ServiceCentreDetailsView.class) private String email; @Schema(description = "The associated phone number") @Size(max = ValidationConstants.PHONE_NO_MAX_LENGTH, message = ValidationConstants.PHONE_NO_MAX_LENGTH_MESSAGE) @Pattern(regexp = ValidationConstants.PHONE_NO_REGEX, message = ValidationConstants.PHONE_NO_REGEX_MESSAGE) + @JsonView(ServiceCentreDetailsView.class) private String phoneNumber; @Override diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreDetails.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreDetails.java new file mode 100644 index 000000000..cc1099815 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreDetails.java @@ -0,0 +1,141 @@ +package uk.gov.hmcts.reform.fact.data.api.entities; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.annotations.TimeZoneStorage; +import org.hibernate.annotations.TimeZoneStorageType; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.type.SqlTypes; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidCourtSlug; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.UUID; +import javax.annotation.concurrent.Immutable; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; + +import uk.gov.hmcts.reform.fact.data.api.controllers.ServiceCentreController.ServiceCentreDetailsView; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Entity +@Immutable +@JsonView(ServiceCentreDetailsView.class) +@Table(name = "service_centre") +public class ServiceCentreDetails { + + @Schema( + description = "The internal ID - assigned by the server during creation", + accessMode = Schema.AccessMode.READ_ONLY + ) + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private UUID id; + + @Schema(description = "The name of the Service Centre") + @NotBlank(message = "Service centre name must be specified") + @Size(min = 5, max = 200, message = "Service centre name should be between 5 and 200 chars") + @Pattern( + regexp = "^[A-Za-z&'()\\- ]+$", + message = "Service centre name may only contain letters, spaces, apostrophes, hyphens, ampersands, " + + "and parentheses" + ) + private String name; + + @Schema(description = "The Service Centre 'slug'") + @ValidCourtSlug + private String slug; + + @Schema(description = "The open status of the Service Centre") + private Boolean open; + + @Schema(description = "Any warning notices attached to the Service Centre") + @Size(max = 500, message = "Warning notice must be less than 500 characters") + @Pattern( + regexp = "^[A-Za-z0-9.,!?:;'\"()\\-/&@+\\s]+$", + message = "Warning notice may only contain letters, numbers, spaces, and standard punctuation or symbols (@, +)" + ) + @Column(name = "warning_notice") + private String warningNotice; + + @Schema( + description = "The created date/time of the Service Centre record", + accessMode = Schema.AccessMode.READ_ONLY + ) + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC) + @CreationTimestamp + @Setter(AccessLevel.NONE) + private ZonedDateTime createdAt; + + @Schema( + description = "The last updated date/time of the Service Centre record", + accessMode = Schema.AccessMode.READ_ONLY + ) + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC) + @UpdateTimestamp + @Setter(AccessLevel.NONE) + private ZonedDateTime lastUpdatedAt; + + @Schema(description = "The list of associated Service Area IDs") + @JdbcTypeCode(SqlTypes.ARRAY) + @Column(columnDefinition = "uuid[]") + @JsonIgnore + private List serviceAreaIds; + + @Transient + @JsonIgnore + private List serviceAreaDetails; + + @JsonView(ServiceCentreDetailsView.class) + @JsonProperty("serviceAreas") + public List getServiceAreasForView() { + return serviceAreaDetails != null ? serviceAreaDetails : serviceAreaIds; + } + + @Schema(description = "The catchment type") + @Enumerated(EnumType.STRING) + private CatchmentType catchmentType; + + @Schema(description = "The Addresses for the Service Centre") + @OneToMany(fetch = FetchType.EAGER) + @JoinColumn(name = "service_centre_id", insertable = false, updatable = false) + private List serviceCentreAddresses; + + @Schema(description = "The Contact Details for the Service Centre") + @OneToMany(fetch = FetchType.EAGER) + @JoinColumn(name = "service_centre_id", insertable = false, updatable = false) + private List serviceCentreContactDetails; + + @Schema(description = "The Areas of Law for the Service Centre") + @OneToMany(fetch = FetchType.EAGER) + @JoinColumn(name = "service_centre_id", insertable = false, updatable = false) + private List serviceCentreAreasOfLaw; +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreDetailsRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreDetailsRepository.java new file mode 100644 index 000000000..0e84dfb9e --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreDetailsRepository.java @@ -0,0 +1,11 @@ +package uk.gov.hmcts.reform.fact.data.api.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; + +import java.util.UUID; + +@Repository +public interface ServiceCentreDetailsRepository extends JpaRepository { +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreDetailsViewService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreDetailsViewService.java new file mode 100644 index 000000000..79862cd29 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreDetailsViewService.java @@ -0,0 +1,135 @@ +package uk.gov.hmcts.reform.fact.data.api.services; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import uk.gov.hmcts.reform.fact.data.api.entities.AreaOfLawType; +import uk.gov.hmcts.reform.fact.data.api.entities.ContactDescriptionType; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ServiceCentreDetailsViewService { + + private final TypesService typesService; + private final ServiceAreaRepository serviceAreaRepository; + + /** + * Prepares the service centre details response by expanding UUID-backed fields into reference objects. + * + * @param serviceCentreDetails the service centre details entity to prepare. + * @return the same entity after enrichment, or null if nothing was provided. + */ + public ServiceCentreDetails prepareDetailsView(ServiceCentreDetails serviceCentreDetails) { + if (serviceCentreDetails == null) { + return null; + } + + enrichServiceAreas(serviceCentreDetails); + enrichContactDescriptions(serviceCentreDetails.getServiceCentreContactDetails()); + enrichServiceCentreAreasOfLaw(serviceCentreDetails.getServiceCentreAreasOfLaw()); + return serviceCentreDetails; + } + + private void enrichServiceAreas(ServiceCentreDetails serviceCentreDetails) { + List serviceAreaIds = safeList(serviceCentreDetails.getServiceAreaIds()); + if (serviceAreaIds.isEmpty()) { + return; + } + + Map serviceAreasById = serviceAreaRepository.findAllById(serviceAreaIds).stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(ServiceArea::getId, Function.identity())); + + serviceCentreDetails.setServiceAreaDetails( + serviceAreaIds.stream() + .map(id -> serviceAreasById.getOrDefault(id, createServiceAreaStub(id))) + .toList() + ); + } + + private void enrichContactDescriptions(List contactDetails) { + if (contactDetails == null || contactDetails.isEmpty()) { + return; + } + + List descriptionTypeIds = contactDetails.stream() + .map(ServiceCentreContactDetails::getServiceCentreContactDescriptionId) + .filter(Objects::nonNull) + .distinct() + .toList(); + + Map descriptionTypesById = descriptionTypeIds.isEmpty() + ? Collections.emptyMap() + : typesService.getContactDescriptionTypesByIds(descriptionTypeIds).stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(ContactDescriptionType::getId, Function.identity())); + + contactDetails.forEach(contactDetail -> { + UUID descriptionTypeId = contactDetail.getServiceCentreContactDescriptionId(); + ContactDescriptionType descriptionType = descriptionTypesById.get(descriptionTypeId); + if (descriptionType == null && descriptionTypeId != null) { + ContactDescriptionType fallback = new ContactDescriptionType(); + fallback.setId(descriptionTypeId); + descriptionType = fallback; + } + contactDetail.setServiceCentreContactDescriptionDetails(descriptionType); + }); + } + + private void enrichServiceCentreAreasOfLaw(List serviceCentreAreasOfLaw) { + if (serviceCentreAreasOfLaw == null || serviceCentreAreasOfLaw.isEmpty()) { + return; + } + + List areaOfLawIds = serviceCentreAreasOfLaw.stream() + .flatMap(area -> safeList(area.getAreasOfLaw()).stream()) + .distinct() + .toList(); + + Map areasOfLawById = areaOfLawIds.isEmpty() + ? Collections.emptyMap() + : typesService.getAllAreasOfLawTypesByIds(areaOfLawIds).stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(AreaOfLawType::getId, Function.identity())); + + serviceCentreAreasOfLaw.forEach(area -> area.setAreasOfLawDetails( + safeList(area.getAreasOfLaw()).stream() + .map(id -> areasOfLawById.getOrDefault(id, createAreaOfLawStub(id))) + .toList() + )); + } + + private AreaOfLawType createAreaOfLawStub(UUID id) { + if (id == null) { + return null; + } + AreaOfLawType fallback = new AreaOfLawType(); + fallback.setId(id); + return fallback; + } + + private ServiceArea createServiceAreaStub(UUID id) { + if (id == null) { + return null; + } + ServiceArea fallback = new ServiceArea(); + fallback.setId(id); + return fallback; + } + + private List safeList(List list) { + return list == null ? Collections.emptyList() : list; + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java index ac3c757c3..e5fde6e76 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java @@ -5,9 +5,11 @@ import org.springframework.transaction.annotation.Transactional; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreDetailsRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; import java.util.List; @@ -18,6 +20,7 @@ public class ServiceCentreService { private final ServiceCentreRepository serviceCentreRepository; + private final ServiceCentreDetailsRepository serviceCentreDetailsRepository; private final ServiceAreaRepository serviceAreaRepository; /** @@ -32,6 +35,18 @@ public ServiceCentre getServiceCentreById(UUID serviceCentreId) { .orElseThrow(() -> new NotFoundException("Service centre not found, ID: " + serviceCentreId)); } + /** + * Get service centre details by id. + * + * @param serviceCentreId The ID of the service centre details to get. + * @return The service centre details entity. + * @throws NotFoundException if the service centre details are not found. + */ + public ServiceCentreDetails getServiceCentreDetailsById(UUID serviceCentreId) { + return serviceCentreDetailsRepository.findById(serviceCentreId) + .orElseThrow(() -> new NotFoundException("Service centre not found, ID: " + serviceCentreId)); + } + /** * Get a service centre by its exact name. * diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java index 07a4d950d..373d537cc 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java @@ -8,6 +8,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreDetailsViewService; import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreService; import java.util.UUID; @@ -23,11 +25,27 @@ class ServiceCentreControllerTest { @Mock private ServiceCentreService serviceCentreService; + @Mock + private ServiceCentreDetailsViewService serviceCentreDetailsViewService; + @InjectMocks private ServiceCentreController serviceCentreController; @Test - void getServiceCentreByIdReturns200() { + void getServiceCentreDetailsByIdReturns200() { + ServiceCentreDetails serviceCentreDetails = createServiceCentreDetails(); + when(serviceCentreService.getServiceCentreDetailsById(SERVICE_CENTRE_ID)).thenReturn(serviceCentreDetails); + when(serviceCentreDetailsViewService.prepareDetailsView(serviceCentreDetails)).thenReturn(serviceCentreDetails); + + ResponseEntity response = + serviceCentreController.getServiceCentreDetailsById(SERVICE_CENTRE_ID.toString()); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(serviceCentreDetails); + } + + @Test + void getServiceCentreEntityByIdReturns200() { ServiceCentre serviceCentre = createServiceCentre(); when(serviceCentreService.getServiceCentreById(SERVICE_CENTRE_ID)).thenReturn(serviceCentre); @@ -81,4 +99,13 @@ private ServiceCentre createServiceCentre() { .open(true) .build(); } + + private ServiceCentreDetails createServiceCentreDetails() { + return ServiceCentreDetails.builder() + .id(SERVICE_CENTRE_ID) + .name("Test Service Centre") + .slug("test-service-centre") + .open(true) + .build(); + } } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java index e612f2cb4..39d039224 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java @@ -9,8 +9,10 @@ import org.springframework.http.ResponseEntity; import uk.gov.hmcts.reform.fact.data.api.entities.Region; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; import uk.gov.hmcts.reform.fact.data.api.services.RegionService; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreDetailsViewService; import uk.gov.hmcts.reform.fact.data.api.services.ServiceCentreService; import uk.gov.hmcts.reform.fact.data.api.services.TestingSupportService; @@ -38,6 +40,8 @@ class TestingSupportControllerTest { @Mock private RegionService regionService; @Mock + private ServiceCentreDetailsViewService serviceCentreDetailsViewService; + @Mock private ServiceCentreService serviceCentreService; @Mock private TestingSupportService testingSupportService; @@ -131,8 +135,15 @@ void createSampleCourtPassesRegionIdOverride() { @Test void createSampleServiceCentreReturns201() { + UUID serviceCentreId = UUID.randomUUID(); ServiceCentre serviceCentre = ServiceCentre.builder() - .id(UUID.randomUUID()) + .id(serviceCentreId) + .name("Test Service Centre") + .slug("test-service-centre") + .open(true) + .build(); + ServiceCentreDetails serviceCentreDetails = ServiceCentreDetails.builder() + .id(serviceCentreId) .name("Test Service Centre") .slug("test-service-centre") .open(true) @@ -140,8 +151,10 @@ void createSampleServiceCentreReturns201() { when(testingSupportService.createServiceCentre("Test Service Centre", 1L, true, false, true)) .thenReturn(serviceCentre); + when(serviceCentreService.getServiceCentreDetailsById(serviceCentreId)).thenReturn(serviceCentreDetails); + when(serviceCentreDetailsViewService.prepareDetailsView(serviceCentreDetails)).thenReturn(serviceCentreDetails); - ResponseEntity response = testingSupportController.createSampleServiceCentre( + ResponseEntity response = testingSupportController.createSampleServiceCentre( "Test Service Centre", 1L, true, @@ -150,7 +163,9 @@ void createSampleServiceCentreReturns201() { ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); - assertThat(response.getBody()).isEqualTo(serviceCentre); + assertThat(response.getBody()).isEqualTo(serviceCentreDetails); verify(testingSupportService).createServiceCentre("Test Service Centre", 1L, true, false, true); + verify(serviceCentreService).getServiceCentreDetailsById(serviceCentreId); + verify(serviceCentreDetailsViewService).prepareDetailsView(serviceCentreDetails); } } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLawTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLawTest.java new file mode 100644 index 000000000..169cb8958 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLawTest.java @@ -0,0 +1,87 @@ +package uk.gov.hmcts.reform.fact.data.api.entities; + +import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class ServiceCentreAreasOfLawTest { + + private final ObjectMapper mapper = JsonMapper.builder().build(); + + @Test + void deserializesExpandedAreaOfLawDetailsToAreaIds() { + UUID areaOfLawId = UUID.randomUUID(); + String json = """ + { + "areasOfLaw": [ + { + "id": "%s", + "name": "Family", + "nameCy": "Teulu" + } + ] + } + """.formatted(areaOfLawId); + + ServiceCentreAreasOfLaw areasOfLaw = mapper.readValue(json, ServiceCentreAreasOfLaw.class); + + assertThat(areasOfLaw.getAreasOfLaw()).containsExactly(areaOfLawId); + } + + @Test + void deserializesAreaOfLawIds() { + UUID areaOfLawId = UUID.randomUUID(); + String json = """ + { + "areasOfLaw": ["%s"] + } + """.formatted(areaOfLawId); + + ServiceCentreAreasOfLaw areasOfLaw = mapper.readValue(json, ServiceCentreAreasOfLaw.class); + + assertThat(areasOfLaw.getAreasOfLaw()).containsExactly(areaOfLawId); + } + + @Test + void setsAreaOfLawIdsDirectly() { + UUID areaOfLawId = UUID.randomUUID(); + ServiceCentreAreasOfLaw areasOfLaw = new ServiceCentreAreasOfLaw(); + + areasOfLaw.setAreasOfLaw(List.of(areaOfLawId)); + + assertThat(areasOfLaw.getAreasOfLaw()).containsExactly(areaOfLawId); + } + + @Test + void serviceCentreDetailsDeserializesExpandedAreaOfLawDetailsToAreaIds() { + UUID serviceCentreAreasOfLawId = UUID.randomUUID(); + UUID areaOfLawId = UUID.randomUUID(); + String json = """ + { + "serviceCentreAreasOfLaw": [ + { + "id": "%s", + "areasOfLaw": [ + { + "id": "%s", + "name": "Family", + "nameCy": "Teulu" + } + ] + } + ] + } + """.formatted(serviceCentreAreasOfLawId, areaOfLawId); + + ServiceCentreDetails serviceCentreDetails = mapper.readValue(json, ServiceCentreDetails.class); + + assertThat(serviceCentreDetails.getServiceCentreAreasOfLaw()).hasSize(1); + assertThat(serviceCentreDetails.getServiceCentreAreasOfLaw().getFirst().getAreasOfLaw()) + .containsExactly(areaOfLawId); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreDetailsViewServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreDetailsViewServiceTest.java new file mode 100644 index 000000000..2bd47584d --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreDetailsViewServiceTest.java @@ -0,0 +1,211 @@ +package uk.gov.hmcts.reform.fact.data.api.services; + +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.reform.fact.data.api.entities.AreaOfLawType; +import uk.gov.hmcts.reform.fact.data.api.entities.ContactDescriptionType; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ServiceCentreDetailsViewServiceTest { + + @Mock + private TypesService typesService; + + @Mock + private ServiceAreaRepository serviceAreaRepository; + + @InjectMocks + private ServiceCentreDetailsViewService serviceCentreDetailsViewService; + + @Test + void prepareDetailsViewReturnsNullForNullInput() { + ServiceCentreDetails result = serviceCentreDetailsViewService.prepareDetailsView(null); + + assertThat(result).isNull(); + verifyNoInteractions(typesService); + verifyNoInteractions(serviceAreaRepository); + } + + @Test + void prepareDetailsViewDoesNotCallDependenciesWhenChildCollectionsAreEmpty() { + ServiceCentreDetails serviceCentreDetails = ServiceCentreDetails.builder() + .serviceCentreContactDetails(List.of()) + .serviceCentreAreasOfLaw(List.of()) + .build(); + + ServiceCentreDetails result = serviceCentreDetailsViewService.prepareDetailsView(serviceCentreDetails); + + assertThat(result).isSameAs(serviceCentreDetails); + verifyNoInteractions(typesService); + verifyNoInteractions(serviceAreaRepository); + } + + @Test + void prepareDetailsViewPopulatesServiceAreasAndStubsMissing() { + UUID serviceAreaId1 = UUID.randomUUID(); + UUID serviceAreaId2 = UUID.randomUUID(); + + ServiceArea serviceArea = ServiceArea.builder() + .id(serviceAreaId2) + .name("Family") + .nameCy("Teulu") + .build(); + + when(serviceAreaRepository.findAllById(List.of(serviceAreaId1, serviceAreaId2))) + .thenReturn(List.of(serviceArea)); + + ServiceCentreDetails serviceCentreDetails = ServiceCentreDetails.builder() + .serviceAreaIds(List.of(serviceAreaId1, serviceAreaId2)) + .build(); + + serviceCentreDetailsViewService.prepareDetailsView(serviceCentreDetails); + + assertThat(serviceCentreDetails.getServiceAreaDetails()).hasSize(2); + assertThat(serviceCentreDetails.getServiceAreaDetails().get(0).getId()).isEqualTo(serviceAreaId1); + assertThat(serviceCentreDetails.getServiceAreaDetails().get(0).getName()).isNull(); + assertThat(serviceCentreDetails.getServiceAreaDetails().get(1).getId()).isEqualTo(serviceAreaId2); + assertThat(serviceCentreDetails.getServiceAreaDetails().get(1).getName()).isEqualTo("Family"); + assertThat(serviceCentreDetails.getServiceAreaDetails().get(1).getNameCy()).isEqualTo("Teulu"); + } + + @Test + void prepareDetailsViewPopulatesContactDescriptions() { + UUID descriptionId = UUID.randomUUID(); + ServiceCentreContactDetails contactDetails = ServiceCentreContactDetails.builder() + .serviceCentreContactDescriptionId(descriptionId) + .build(); + + ContactDescriptionType descriptionType = ContactDescriptionType.builder() + .id(descriptionId) + .name("Enquiries") + .nameCy("Ymholiadau") + .build(); + + when(typesService.getContactDescriptionTypesByIds(List.of(descriptionId))) + .thenReturn(List.of(descriptionType)); + + ServiceCentreDetails serviceCentreDetails = ServiceCentreDetails.builder() + .serviceCentreContactDetails(List.of(contactDetails)) + .build(); + + serviceCentreDetailsViewService.prepareDetailsView(serviceCentreDetails); + + assertThat(contactDetails.getServiceCentreContactDescriptionDetails()).isNotNull(); + assertThat(contactDetails.getServiceCentreContactDescriptionDetails().getId()).isEqualTo(descriptionId); + assertThat(contactDetails.getServiceCentreContactDescriptionDetails().getName()).isEqualTo("Enquiries"); + assertThat(contactDetails.getServiceCentreContactDescriptionDetails().getNameCy()).isEqualTo("Ymholiadau"); + } + + @Test + void prepareDetailsViewFallsBackWhenContactDescriptionMissing() { + UUID descriptionId = UUID.randomUUID(); + ServiceCentreContactDetails contactDetails = ServiceCentreContactDetails.builder() + .serviceCentreContactDescriptionId(descriptionId) + .build(); + + when(typesService.getContactDescriptionTypesByIds(List.of(descriptionId))) + .thenReturn(List.of()); + + ServiceCentreDetails serviceCentreDetails = ServiceCentreDetails.builder() + .serviceCentreContactDetails(List.of(contactDetails)) + .build(); + + serviceCentreDetailsViewService.prepareDetailsView(serviceCentreDetails); + + assertThat(contactDetails.getServiceCentreContactDescriptionDetails()).isNotNull(); + assertThat(contactDetails.getServiceCentreContactDescriptionDetails().getId()).isEqualTo(descriptionId); + assertThat(contactDetails.getServiceCentreContactDescriptionDetails().getName()).isNull(); + assertThat(contactDetails.getServiceCentreContactDescriptionDetails().getNameCy()).isNull(); + } + + @Test + void prepareDetailsViewPopulatesAreasOfLawAndStubsMissing() { + UUID areaId1 = UUID.randomUUID(); + UUID areaId2 = UUID.randomUUID(); + ServiceCentreAreasOfLaw areasOfLaw = ServiceCentreAreasOfLaw.builder() + .areasOfLaw(List.of(areaId1, areaId2)) + .build(); + + AreaOfLawType areaOfLawType = AreaOfLawType.builder() + .id(areaId2) + .name("Civil") + .nameCy("Sifil") + .build(); + + when(typesService.getAllAreasOfLawTypesByIds(List.of(areaId1, areaId2))) + .thenReturn(List.of(areaOfLawType)); + + ServiceCentreDetails serviceCentreDetails = ServiceCentreDetails.builder() + .serviceCentreAreasOfLaw(List.of(areasOfLaw)) + .build(); + + serviceCentreDetailsViewService.prepareDetailsView(serviceCentreDetails); + + assertThat(areasOfLaw.getAreasOfLawDetails()).hasSize(2); + assertThat(areasOfLaw.getAreasOfLawDetails().get(0).getId()).isEqualTo(areaId1); + assertThat(areasOfLaw.getAreasOfLawDetails().get(0).getName()).isNull(); + assertThat(areasOfLaw.getAreasOfLawDetails().get(1).getId()).isEqualTo(areaId2); + assertThat(areasOfLaw.getAreasOfLawDetails().get(1).getName()).isEqualTo("Civil"); + assertThat(areasOfLaw.getAreasOfLawDetails().get(1).getNameCy()).isEqualTo("Sifil"); + } + + @Test + void prepareDetailsViewUsesDistinctTypeIdsAcrossChildCollections() { + UUID descriptionId = UUID.randomUUID(); + UUID areaId = UUID.randomUUID(); + + ServiceCentreContactDetails contactDetails1 = ServiceCentreContactDetails.builder() + .serviceCentreContactDescriptionId(descriptionId) + .build(); + ServiceCentreContactDetails contactDetails2 = ServiceCentreContactDetails.builder() + .serviceCentreContactDescriptionId(descriptionId) + .build(); + ServiceCentreAreasOfLaw areasOfLaw1 = ServiceCentreAreasOfLaw.builder() + .areasOfLaw(List.of(areaId)) + .build(); + ServiceCentreAreasOfLaw areasOfLaw2 = ServiceCentreAreasOfLaw.builder() + .areasOfLaw(List.of(areaId)) + .build(); + + ContactDescriptionType descriptionType = ContactDescriptionType.builder() + .id(descriptionId) + .name("Telephone") + .build(); + AreaOfLawType areaOfLawType = AreaOfLawType.builder() + .id(areaId) + .name("Family") + .build(); + + when(typesService.getContactDescriptionTypesByIds(List.of(descriptionId))) + .thenReturn(List.of(descriptionType)); + when(typesService.getAllAreasOfLawTypesByIds(List.of(areaId))) + .thenReturn(List.of(areaOfLawType)); + + ServiceCentreDetails serviceCentreDetails = ServiceCentreDetails.builder() + .serviceCentreContactDetails(List.of(contactDetails1, contactDetails2)) + .serviceCentreAreasOfLaw(List.of(areasOfLaw1, areasOfLaw2)) + .build(); + + serviceCentreDetailsViewService.prepareDetailsView(serviceCentreDetails); + + assertThat(contactDetails1.getServiceCentreContactDescriptionDetails()).isSameAs(descriptionType); + assertThat(contactDetails2.getServiceCentreContactDescriptionDetails()).isSameAs(descriptionType); + assertThat(areasOfLaw1.getAreasOfLawDetails()).containsExactly(areaOfLawType); + assertThat(areasOfLaw2.getAreasOfLawDetails()).containsExactly(areaOfLawType); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java index 05cf18173..c72c3e1f1 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java @@ -7,9 +7,11 @@ import org.mockito.junit.jupiter.MockitoExtension; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreDetailsRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; import java.util.List; @@ -28,6 +30,9 @@ class ServiceCentreServiceTest { @Mock private ServiceCentreRepository serviceCentreRepository; + @Mock + private ServiceCentreDetailsRepository serviceCentreDetailsRepository; + @Mock private ServiceAreaRepository serviceAreaRepository; @@ -59,6 +64,34 @@ void getServiceCentreByIdThrowsNotFoundWhenMissing() { assertThat(exception.getMessage()).isEqualTo("Service centre not found, ID: " + serviceCentreId); } + @Test + void getServiceCentreDetailsByIdReturnsDetailsWhenFound() { + UUID serviceCentreId = UUID.randomUUID(); + ServiceCentreDetails serviceCentreDetails = ServiceCentreDetails.builder() + .id(serviceCentreId) + .name("Test Service Centre") + .build(); + + when(serviceCentreDetailsRepository.findById(serviceCentreId)).thenReturn(Optional.of(serviceCentreDetails)); + + ServiceCentreDetails result = serviceCentreService.getServiceCentreDetailsById(serviceCentreId); + + assertThat(result).isEqualTo(serviceCentreDetails); + } + + @Test + void getServiceCentreDetailsByIdThrowsNotFoundWhenMissing() { + UUID serviceCentreId = UUID.randomUUID(); + when(serviceCentreDetailsRepository.findById(serviceCentreId)).thenReturn(Optional.empty()); + + NotFoundException exception = assertThrows( + NotFoundException.class, + () -> serviceCentreService.getServiceCentreDetailsById(serviceCentreId) + ); + + assertThat(exception.getMessage()).isEqualTo("Service centre not found, ID: " + serviceCentreId); + } + @Test void getServiceCentreByNameReturnsServiceCentre() { ServiceCentre serviceCentre = ServiceCentre.builder().name("Bulk Scan Centre").build(); From 89ea59c819b050075ecdedf80ed1ff0caa1849bf Mon Sep 17 00:00:00 2001 From: Josh <23316525+josh-uk@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:26:08 +0100 Subject: [PATCH 05/40] FACT-2618 search (#280) Search --- .../ServiceCentreControllerTest.java | 16 +++ .../search/SearchLocationControllerTest.java | 78 +++++++++++++ .../SearchServiceAreaControllerTest.java | 74 ++++++++++++ .../controllers/ServiceCentreController.java | 23 ++++ .../search/SearchLocationController.java | 70 +++++++++++ .../search/SearchServiceAreaController.java | 46 ++++++++ .../api/dto/CourtWithDistanceResponse.java | 27 +++-- .../fact/data/api/dto/SearchResult.java | 44 +++++++ .../data/api/dto/ServiceAreaSearchResult.java | 26 +++++ .../api/dto/ServiceCentreWithDistance.java | 14 +++ .../api/entities/types/SearchResultType.java | 6 + .../CourtLocalAuthoritiesRepository.java | 2 + .../ServiceCentreDetailsRepository.java | 2 + .../repositories/ServiceCentreRepository.java | 72 ++++++++++++ .../data/api/services/ServiceAreaService.java | 22 +++- .../api/services/ServiceCentreService.java | 20 +++- .../search/SearchLocationService.java | 50 ++++++++ .../search/SearchServiceAreaService.java | 45 +++++++ .../search/SearchServiceCentreService.java | 69 +++++++++++ ...__drop_service_centre_data_structures.sql} | 0 ...> V1.32__create_service_centre_tables.sql} | 0 ...ql => V1.33__generalise_audit_subject.sql} | 0 ... V1.34__refresh_service_area_mappings.sql} | 0 .../api/services/ServiceAreaServiceTest.java | 59 ++++++++++ .../services/ServiceCentreServiceTest.java | 31 ++++- .../search/SearchLocationServiceTest.java | 110 ++++++++++++++++++ .../search/SearchServiceAreaServiceTest.java | 59 ++++++++++ .../SearchServiceCentreServiceTest.java | 109 +++++++++++++++++ 28 files changed, 1055 insertions(+), 19 deletions(-) create mode 100644 src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchLocationControllerTest.java create mode 100644 src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaControllerTest.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchLocationController.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaController.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/SearchResult.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/ServiceAreaSearchResult.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/ServiceCentreWithDistance.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SearchResultType.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchLocationService.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceAreaService.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceCentreService.java rename src/main/resources/db/migration/{V1.27__drop_service_centre_data_structures.sql => V1.31__drop_service_centre_data_structures.sql} (100%) rename src/main/resources/db/migration/{V1.28__create_service_centre_tables.sql => V1.32__create_service_centre_tables.sql} (100%) rename src/main/resources/db/migration/{V1.29__generalise_audit_subject.sql => V1.33__generalise_audit_subject.sql} (100%) rename src/main/resources/db/migration/{V1.30__refresh_service_area_mappings.sql => V1.34__refresh_service_area_mappings.sql} (100%) create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchLocationServiceTest.java create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceAreaServiceTest.java create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceCentreServiceTest.java diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java index ecb100538..b7686745a 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreControllerTest.java @@ -84,6 +84,22 @@ void getServiceCentreDetailsByIdReturnsServiceCentreDetails() throws Exception { .andExpect(jsonPath("$.serviceCentreAreasOfLaw[0].areasOfLaw[0].name").value("Civil")); } + @Test + @DisplayName("GET /service-centres/slug/{serviceCentreSlug}/v1 returns service centre details") + void getServiceCentreDetailsBySlugReturnsServiceCentreDetails() throws Exception { + ServiceCentreDetails serviceCentreDetails = buildServiceCentreDetails(); + + when(serviceCentreService.getServiceCentreDetailsBySlug("test-service-centre")) + .thenReturn(serviceCentreDetails); + when(serviceCentreDetailsViewService.prepareDetailsView(serviceCentreDetails)).thenReturn(serviceCentreDetails); + + mockMvc.perform(get("/service-centres/slug/{serviceCentreSlug}/v1", "test-service-centre")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(SERVICE_CENTRE_ID.toString())) + .andExpect(jsonPath("$.slug").value("test-service-centre")) + .andExpect(jsonPath("$.serviceAreas[0].id").value(SERVICE_AREA_ID.toString())); + } + @Test @DisplayName("GET /service-centres/{serviceCentreId}/v1 returns 400 for invalid UUID") void getServiceCentreByIdReturnsBadRequestForInvalidUuid() throws Exception { diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchLocationControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchLocationControllerTest.java new file mode 100644 index 000000000..6528d918a --- /dev/null +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchLocationControllerTest.java @@ -0,0 +1,78 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers.search; + +import io.qameta.allure.Feature; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import uk.gov.hmcts.reform.fact.data.api.dto.SearchResult; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchAction; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchResultType; +import uk.gov.hmcts.reform.fact.data.api.services.search.SearchLocationService; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Feature("Search Location Controller") +@DisplayName("Search Location Controller") +@WebMvcTest(SearchLocationController.class) +@AutoConfigureMockMvc(addFilters = false) +class SearchLocationControllerTest { + + private static final UUID RESULT_ID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private SearchLocationService searchLocationService; + + @Test + @DisplayName("GET /search/locations/v1/postcode returns typed search results") + void getLocationsByPostcodeReturnsTypedResults() throws Exception { + List results = List.of( + SearchResult.builder() + .id(RESULT_ID) + .name("National Business Centre") + .slug("national-business-centre") + .distance(BigDecimal.valueOf(1.5)) + .type(SearchResultType.SERVICE_CENTRE) + .build() + ); + + when(searchLocationService.getLocationsBySearchParameters( + "SW1A 1AA", + "Money Claims", + SearchAction.DOCUMENTS, + 10 + )).thenReturn(results); + + mockMvc.perform(get("/search/locations/v1/postcode") + .param("postcode", "SW1A 1AA") + .param("serviceArea", "Money Claims") + .param("action", "documents")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(RESULT_ID.toString())) + .andExpect(jsonPath("$[0].name").value("National Business Centre")) + .andExpect(jsonPath("$[0].slug").value("national-business-centre")) + .andExpect(jsonPath("$[0].type").value("SERVICE_CENTRE")) + .andExpect(jsonPath("$[0].distance").value(1.5)); + + verify(searchLocationService).getLocationsBySearchParameters( + "SW1A 1AA", + "Money Claims", + SearchAction.DOCUMENTS, + 10 + ); + } +} diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaControllerTest.java new file mode 100644 index 000000000..21c47526b --- /dev/null +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaControllerTest.java @@ -0,0 +1,74 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers.search; + +import io.qameta.allure.Feature; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import uk.gov.hmcts.reform.fact.data.api.dto.ServiceAreaSearchResult; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchResultType; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.services.search.SearchServiceAreaService; + +import java.util.List; +import java.util.UUID; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Feature("Search Service Area Controller") +@DisplayName("Search Service Area Controller") +@WebMvcTest(SearchServiceAreaController.class) +@AutoConfigureMockMvc(addFilters = false) +class SearchServiceAreaControllerTest { + + private static final UUID SERVICE_CENTRE_ID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + private static final UUID SERVICE_AREA_ID = UUID.fromString("123e4567-e89b-12d3-a456-426614174111"); + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private SearchServiceAreaService searchServiceAreaService; + + @Test + @DisplayName("GET /search/service-area/v1/{serviceAreaName} returns service-centre results") + void getServiceAreaByNameReturnsServiceCentreResults() throws Exception { + when(searchServiceAreaService.findByServiceAreaName("Money Claims")) + .thenReturn(List.of(ServiceAreaSearchResult.builder() + .id(SERVICE_CENTRE_ID) + .serviceCentreId(SERVICE_CENTRE_ID) + .serviceCentreName("National Business Centre") + .serviceCentreSlug("national-business-centre") + .serviceAreaIds(List.of(SERVICE_AREA_ID)) + .catchmentType(CatchmentType.NATIONAL) + .type(SearchResultType.SERVICE_CENTRE) + .build())); + + mockMvc.perform(get("/search/service-area/v1/{serviceAreaName}", "Money Claims")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(SERVICE_CENTRE_ID.toString())) + .andExpect(jsonPath("$[0].serviceCentreId").value(SERVICE_CENTRE_ID.toString())) + .andExpect(jsonPath("$[0].serviceCentreName").value("National Business Centre")) + .andExpect(jsonPath("$[0].serviceCentreSlug").value("national-business-centre")) + .andExpect(jsonPath("$[0].serviceAreaIds[0]").value(SERVICE_AREA_ID.toString())) + .andExpect(jsonPath("$[0].catchmentType").value("NATIONAL")) + .andExpect(jsonPath("$[0].type").value("SERVICE_CENTRE")); + } + + @Test + @DisplayName("GET /search/service-area/v1/{serviceAreaName} returns 404 when missing") + void getServiceAreaByNameReturnsNotFound() throws Exception { + when(searchServiceAreaService.findByServiceAreaName("Missing")) + .thenThrow(new NotFoundException("Service area Missing not found")); + + mockMvc.perform(get("/search/service-area/v1/{serviceAreaName}", "Missing")) + .andExpect(status().isNotFound()); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreController.java index 914d6a649..fe7429d4a 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/ServiceCentreController.java @@ -68,6 +68,29 @@ public ResponseEntity getServiceCentreDetailsById( ); } + @GetMapping("/slug/{serviceCentreSlug}/v1") + @JsonView(ServiceCentreDetailsView.class) + @Operation( + summary = "Get service centre details by slug", + description = "Fetch detailed service centre information for a given service centre slug." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved service centre details"), + @ApiResponse(responseCode = "400", description = "Invalid service centre slug supplied"), + @ApiResponse(responseCode = "404", description = "Service centre not found") + }) + public ResponseEntity getServiceCentreDetailsBySlug( + @Parameter(description = "Slug of the service centre", required = true) + @NotBlank(message = "serviceCentreSlug must not be blank") + @Size(max = 250, message = "serviceCentreSlug must be less than 250 characters") + @PathVariable String serviceCentreSlug) { + return ResponseEntity.ok( + serviceCentreDetailsViewService.prepareDetailsView( + serviceCentreService.getServiceCentreDetailsBySlug(serviceCentreSlug) + ) + ); + } + @GetMapping("/{serviceCentreId}/entity/v1") @Operation( summary = "Get service centre entity by ID", diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchLocationController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchLocationController.java new file mode 100644 index 000000000..3d02f845b --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchLocationController.java @@ -0,0 +1,70 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers.search; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import uk.gov.hmcts.reform.fact.data.api.dto.SearchResult; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchAction; +import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; +import uk.gov.hmcts.reform.fact.data.api.services.search.SearchLocationService; +import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidPostcode; + +import java.util.List; + +@SecuredFactRestController( + name = "Search Location", + description = "Operations related to searching courts and service centres" +) +@RequestMapping("/search/locations") +public class SearchLocationController { + + private final SearchLocationService searchLocationService; + + public SearchLocationController(SearchLocationService searchLocationService) { + this.searchLocationService = searchLocationService; + } + + @GetMapping("/v1/postcode") + @Operation( + summary = "Search courts and service centres by postcode.", + description = "Retrieve court and service-centre results based on postcode, service area and action." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved matching locations."), + @ApiResponse(responseCode = "400", description = "Postcode is missing or is not valid."), + @ApiResponse(responseCode = "404", description = "Information not found.") + }) + public ResponseEntity> getLocationsByPostcode( + @Parameter(description = "Postcode") + @ValidPostcode + @NotBlank + @RequestParam(value = "postcode") + final String postcode, + + @Parameter(description = "Service area name") + @RequestParam(value = "serviceArea", required = false) + final String serviceArea, + + @Parameter(description = "Action to perform") + @RequestParam(value = "action", required = false) + final SearchAction action, + + @Parameter(description = "Maximum number of results (default 10)") + @RequestParam(value = "limit", required = false, defaultValue = "10") + @Min(1) + @Max(50) + final Integer limit) { + + return ResponseEntity.ok( + searchLocationService.getLocationsBySearchParameters(postcode, serviceArea, action, limit) + ); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaController.java new file mode 100644 index 000000000..48770df1c --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/search/SearchServiceAreaController.java @@ -0,0 +1,46 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers.search; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import uk.gov.hmcts.reform.fact.data.api.dto.ServiceAreaSearchResult; +import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; +import uk.gov.hmcts.reform.fact.data.api.services.search.SearchServiceAreaService; + +import java.util.List; + +@SecuredFactRestController( + name = "Search Service Area", + description = "Operations related to the searching of service areas" +) +@RequestMapping("/search/service-area") +public class SearchServiceAreaController { + + private final SearchServiceAreaService searchServiceAreaService; + + public SearchServiceAreaController(SearchServiceAreaService searchServiceAreaService) { + this.searchServiceAreaService = searchServiceAreaService; + } + + @GetMapping("/v1/{serviceAreaName}") + @Operation( + summary = "Search for service area by name.", + description = "Retrieve service-centre search results based on provided service area name." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved results for service area."), + @ApiResponse(responseCode = "400", description = "Service area name is missing or is not valid."), + @ApiResponse(responseCode = "404", description = "Information not found for provided service area name.") + }) + public ResponseEntity> getServiceAreaByName( + @Parameter(description = "Service area name", required = true) + @PathVariable String serviceAreaName + ) { + return ResponseEntity.ok(searchServiceAreaService.findByServiceAreaName(serviceAreaName)); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/CourtWithDistanceResponse.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/CourtWithDistanceResponse.java index 62d54a4a9..e9e4d9f3d 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/CourtWithDistanceResponse.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/CourtWithDistanceResponse.java @@ -1,22 +1,27 @@ package uk.gov.hmcts.reform.fact.data.api.dto; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + import java.math.BigDecimal; import java.util.UUID; /** * DTO for deserializing {@link CourtWithDistance} JSON responses. - * - * @param courtId the court ID - * @param courtName the court name - * @param courtSlug the court slug - * @param distance the distance from the searched point */ -public record CourtWithDistanceResponse( - UUID courtId, - String courtName, - String courtSlug, - BigDecimal distance -) implements CourtWithDistance { +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PUBLIC) +public class CourtWithDistanceResponse implements CourtWithDistance { + + private UUID courtId; + private String courtName; + private String courtSlug; + private BigDecimal distance; @Override public UUID getCourtId() { diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/SearchResult.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/SearchResult.java new file mode 100644 index 000000000..7a41f5297 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/SearchResult.java @@ -0,0 +1,44 @@ +package uk.gov.hmcts.reform.fact.data.api.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchResultType; + +import java.math.BigDecimal; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PUBLIC) +public class SearchResult { + + private UUID id; + private String name; + private String slug; + private BigDecimal distance; + private SearchResultType type; + + public static SearchResult fromCourt(CourtWithDistance court) { + return SearchResult.builder() + .id(court.getCourtId()) + .name(court.getCourtName()) + .slug(court.getCourtSlug()) + .distance(court.getDistance()) + .type(SearchResultType.COURT) + .build(); + } + + public static SearchResult fromServiceCentre(ServiceCentreWithDistance serviceCentre) { + return SearchResult.builder() + .id(serviceCentre.getServiceCentreId()) + .name(serviceCentre.getServiceCentreName()) + .slug(serviceCentre.getServiceCentreSlug()) + .distance(serviceCentre.getDistance()) + .type(SearchResultType.SERVICE_CENTRE) + .build(); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/ServiceAreaSearchResult.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/ServiceAreaSearchResult.java new file mode 100644 index 000000000..4d3e7447f --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/ServiceAreaSearchResult.java @@ -0,0 +1,26 @@ +package uk.gov.hmcts.reform.fact.data.api.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchResultType; + +import java.util.List; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PUBLIC) +public class ServiceAreaSearchResult { + private UUID id; + private UUID serviceCentreId; + private String serviceCentreName; + private String serviceCentreSlug; + private List serviceAreaIds; + private CatchmentType catchmentType; + private SearchResultType type; +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/ServiceCentreWithDistance.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/ServiceCentreWithDistance.java new file mode 100644 index 000000000..12a33cf8e --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/dto/ServiceCentreWithDistance.java @@ -0,0 +1,14 @@ +package uk.gov.hmcts.reform.fact.data.api.dto; + +import java.math.BigDecimal; +import java.util.UUID; + +public interface ServiceCentreWithDistance { + UUID getServiceCentreId(); + + String getServiceCentreName(); + + String getServiceCentreSlug(); + + BigDecimal getDistance(); +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SearchResultType.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SearchResultType.java new file mode 100644 index 000000000..1a54d5da7 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SearchResultType.java @@ -0,0 +1,6 @@ +package uk.gov.hmcts.reform.fact.data.api.entities.types; + +public enum SearchResultType { + COURT, + SERVICE_CENTRE +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtLocalAuthoritiesRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtLocalAuthoritiesRepository.java index da45e9418..366ce6eb3 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtLocalAuthoritiesRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtLocalAuthoritiesRepository.java @@ -14,6 +14,8 @@ public interface CourtLocalAuthoritiesRepository extends JpaRepository { Optional findByCourtIdAndAreaOfLawId(UUID courtId, UUID areaOfLawId); + boolean existsByAreaOfLawId(UUID areaOfLawId); + void deleteByCourtId(UUID courtId); /** diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreDetailsRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreDetailsRepository.java index 0e84dfb9e..aa5570602 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreDetailsRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreDetailsRepository.java @@ -4,8 +4,10 @@ import org.springframework.stereotype.Repository; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; +import java.util.Optional; import java.util.UUID; @Repository public interface ServiceCentreDetailsRepository extends JpaRepository { + Optional findBySlug(String slug); } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java index d179829c7..c054aad6f 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java @@ -1,8 +1,12 @@ package uk.gov.hmcts.reform.fact.data.api.repositories; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import uk.gov.hmcts.reform.fact.data.api.dto.ServiceCentreWithDistance; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import java.util.List; import java.util.Optional; @@ -16,4 +20,72 @@ public interface ServiceCentreRepository extends JpaRepository findByNameStartingWithIgnoreCase(String namePrefix); + + @Query( + value = "SELECT * FROM service_centre sc WHERE CAST(:serviceAreaId AS uuid) = ANY(sc.service_area_ids)", + nativeQuery = true + ) + List findByServiceAreaId(@Param("serviceAreaId") UUID serviceAreaId); + + @Query( + value = """ + SELECT EXISTS ( + SELECT + sc.id + FROM + service_centre sc + WHERE + CAST(:serviceAreaId AS uuid) = ANY(sc.service_area_ids) + AND + sc.catchment_type IN (:#{#catchmentTypes.![name()]}) + LIMIT 1 + ) + """, + nativeQuery = true + ) + boolean existsByServiceAreaIdAndCatchmentTypeIn( + @Param("serviceAreaId") UUID serviceAreaId, + @Param("catchmentTypes") List catchmentTypes + ); + + @Query( + value = """ + SELECT * + FROM ( + SELECT DISTINCT ON (sc.id) + sc.id AS serviceCentreId, + sc.name AS serviceCentreName, + sc.slug AS serviceCentreSlug, + ( + point(CAST(sca.lon AS float8), CAST(sca.lat AS float8)) + <@> + point(CAST(:lon AS float8), CAST(:lat AS float8)) + ) AS distance + FROM service_centre sc + JOIN service_centre_address sca + ON sca.service_centre_id = sc.id + JOIN service_centre_areas_of_law scaol + ON scaol.service_centre_id = sc.id + WHERE sc.open = true + AND CAST(:serviceAreaId AS uuid) = ANY(sc.service_area_ids) + AND CAST(:areaOfLawId AS uuid) = ANY(scaol.areas_of_law) + AND sc.catchment_type IN (:#{#catchmentTypes.![name()]}) + AND sca.address_type IN ('VISIT_US', 'VISIT_OR_CONTACT_US') + AND sca.lat IS NOT NULL + AND sca.lon IS NOT NULL + ORDER BY sc.id, distance + ) x + ORDER BY x.distance + LIMIT :limit + """, + nativeQuery = true + ) + List findNearestByServiceAreaAndAreaOfLawAndCatchmentTypeIn( + @Param("serviceAreaId") UUID serviceAreaId, + @Param("areaOfLawId") UUID areaOfLawId, + @Param("catchmentTypes") List catchmentTypes, + @Param("lat") double lat, + @Param("lon") double lon, + @Param("limit") int limit + ); } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaService.java index da5d1e1ec..9cb246574 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaService.java @@ -1,8 +1,11 @@ package uk.gov.hmcts.reform.fact.data.api.services; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.repositories.CourtLocalAuthoritiesRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; import java.text.MessageFormat; import java.util.List; @@ -15,6 +18,8 @@ public class ServiceAreaService { private final ServiceAreaRepository serviceAreaRepository; + private final ServiceCentreRepository serviceCentreRepository; + private final CourtLocalAuthoritiesRepository courtLocalAuthoritiesRepository; /** * Retrieves a service area by name. @@ -56,10 +61,19 @@ public List getAllServiceAreasForService(String serviceName) { * @return the service area with additional data */ private ServiceArea enrichServiceArea(ServiceArea serviceArea) { - //TODO: Re-populate catchment availability from service-centres when that model is introduced. - serviceArea.setHasLocal(false); - serviceArea.setHasNational(false); - serviceArea.setHasRegional(false); + serviceArea.setHasLocal( + serviceCentreRepository.existsByServiceAreaIdAndCatchmentTypeIn( + serviceArea.getId(), List.of(CatchmentType.LOCAL)) + || courtLocalAuthoritiesRepository.existsByAreaOfLawId(serviceArea.getAreaOfLawId()) + ); + serviceArea.setHasNational( + serviceCentreRepository.existsByServiceAreaIdAndCatchmentTypeIn( + serviceArea.getId(), List.of(CatchmentType.NATIONAL)) + ); + serviceArea.setHasRegional( + serviceCentreRepository.existsByServiceAreaIdAndCatchmentTypeIn( + serviceArea.getId(), List.of(CatchmentType.REGIONAL)) + ); return serviceArea; } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java index e5fde6e76..927174bb7 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java @@ -47,6 +47,18 @@ public ServiceCentreDetails getServiceCentreDetailsById(UUID serviceCentreId) { .orElseThrow(() -> new NotFoundException("Service centre not found, ID: " + serviceCentreId)); } + /** + * Get service centre details by slug. + * + * @param serviceCentreSlug The slug of the service centre details to get. + * @return The service centre details entity. + * @throws NotFoundException if the service centre details are not found. + */ + public ServiceCentreDetails getServiceCentreDetailsBySlug(String serviceCentreSlug) { + return serviceCentreDetailsRepository.findBySlug(serviceCentreSlug) + .orElseThrow(() -> new NotFoundException("Service centre not found, slug: " + serviceCentreSlug)); + } + /** * Get a service centre by its exact name. * @@ -70,7 +82,7 @@ public ServiceCentre createServiceCentre(ServiceCentre serviceCentre) { serviceCentre.setSlug(toUniqueSlug(serviceCentre.getName())); serviceCentre.setOpen(false); serviceCentre.setServiceAreaIds(getValidatedServiceAreaIds(serviceCentre.getServiceAreaIds())); - serviceCentre.setCatchmentType(CatchmentType.NATIONAL); + serviceCentre.setCatchmentType(getCatchmentTypeOrDefault(serviceCentre.getCatchmentType())); return serviceCentreRepository.save(serviceCentre); } @@ -94,7 +106,7 @@ public ServiceCentre updateServiceCentre(UUID serviceCentreId, ServiceCentre ser existingServiceCentre.setOpen(serviceCentre.getOpen()); existingServiceCentre.setWarningNotice(serviceCentre.getWarningNotice()); existingServiceCentre.setServiceAreaIds(getValidatedServiceAreaIds(serviceCentre.getServiceAreaIds())); - existingServiceCentre.setCatchmentType(CatchmentType.NATIONAL); + existingServiceCentre.setCatchmentType(getCatchmentTypeOrDefault(serviceCentre.getCatchmentType())); return serviceCentreRepository.save(existingServiceCentre); } @@ -154,4 +166,8 @@ private List getValidatedServiceAreaIds(List serviceAreaIds) { .toList(); } + private CatchmentType getCatchmentTypeOrDefault(CatchmentType catchmentType) { + return catchmentType == null ? CatchmentType.NATIONAL : catchmentType; + } + } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchLocationService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchLocationService.java new file mode 100644 index 000000000..694f856b6 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchLocationService.java @@ -0,0 +1,50 @@ +package uk.gov.hmcts.reform.fact.data.api.services.search; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import uk.gov.hmcts.reform.fact.data.api.dto.SearchResult; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchAction; + +import java.util.Comparator; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SearchLocationService { + + private final SearchCourtService searchCourtService; + private final SearchServiceCentreService searchServiceCentreService; + + /** + * Search locations by postcode. Court results preserve the existing search behaviour; + * service-centre results are included for service-area searches where the new service-centre + * catchment data applies. + * + * @param postcode the postcode to search from + * @param serviceArea optional service area name + * @param action optional search action + * @param limit maximum number of combined results + * @return court and service-centre search results + */ + public List getLocationsBySearchParameters(String postcode, + String serviceArea, + SearchAction action, + Integer limit) { + List courts = searchCourtService + .getCourtsBySearchParameters(postcode, serviceArea, action, limit) + .stream() + .map(SearchResult::fromCourt) + .toList(); + + List serviceCentres = searchServiceCentreService + .getServiceCentresBySearchParameters(postcode, serviceArea, action, limit) + .stream() + .map(SearchResult::fromServiceCentre) + .toList(); + + return java.util.stream.Stream.concat(courts.stream(), serviceCentres.stream()) + .sorted(Comparator.comparing(SearchResult::getDistance)) + .limit(limit) + .toList(); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceAreaService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceAreaService.java new file mode 100644 index 000000000..335d0fd2e --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceAreaService.java @@ -0,0 +1,45 @@ +package uk.gov.hmcts.reform.fact.data.api.services.search; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import uk.gov.hmcts.reform.fact.data.api.dto.ServiceAreaSearchResult; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchResultType; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceAreaService; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SearchServiceAreaService { + + private final ServiceAreaService serviceAreaService; + private final ServiceCentreRepository serviceCentreRepository; + + /** + * Finds service-centre search results by service area name. + * + * @param serviceAreaName the service area name + * @return service centres linked to that service area + */ + public List findByServiceAreaName(String serviceAreaName) { + ServiceArea serviceArea = serviceAreaService.getServiceAreaByName(serviceAreaName); + return serviceCentreRepository.findByServiceAreaId(serviceArea.getId()).stream() + .map(this::toSearchResult) + .toList(); + } + + private ServiceAreaSearchResult toSearchResult(ServiceCentre serviceCentre) { + return ServiceAreaSearchResult.builder() + .id(serviceCentre.getId()) + .serviceCentreId(serviceCentre.getId()) + .serviceCentreName(serviceCentre.getName()) + .serviceCentreSlug(serviceCentre.getSlug()) + .serviceAreaIds(serviceCentre.getServiceAreaIds()) + .catchmentType(serviceCentre.getCatchmentType()) + .type(SearchResultType.SERVICE_CENTRE) + .build(); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceCentreService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceCentreService.java new file mode 100644 index 000000000..8bd120f3a --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceCentreService.java @@ -0,0 +1,69 @@ +package uk.gov.hmcts.reform.fact.data.api.services.search; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import uk.gov.hmcts.reform.fact.data.api.dto.ServiceCentreWithDistance; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchAction; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; +import uk.gov.hmcts.reform.fact.data.api.os.OsLocationData; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; +import uk.gov.hmcts.reform.fact.data.api.services.OsService; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceAreaService; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SearchServiceCentreService { + + private static final List POSTCODE_SEARCH_CATCHMENTS = + List.of(CatchmentType.LOCAL, CatchmentType.REGIONAL); + + private final OsService osService; + private final ServiceAreaService serviceAreaService; + private final ServiceCentreRepository serviceCentreRepository; + + /** + * Search service centres by postcode, service area and action. + * + * @param postcode the postcode to search from + * @param serviceArea the service area name + * @param action the search action + * @param limit maximum number of results + * @return matching service centres with distance + */ + public List getServiceCentresBySearchParameters(String postcode, + String serviceArea, + SearchAction action, + Integer limit) { + boolean serviceAreaEmpty = serviceArea == null || serviceArea.isBlank(); + if (action == null ^ serviceAreaEmpty) { + throw new InvalidParameterCombinationException( + "Both 'serviceArea' and 'action' must be provided together if one is present." + ); + } + if (serviceAreaEmpty) { + return List.of(); + } + return searchWithServiceArea(postcode, serviceArea, action, limit); + } + + private List searchWithServiceArea(String postcode, + String serviceArea, + SearchAction action, + Integer limit) { + OsLocationData osLocationData = osService.getOsLonLatDistrictByPartial(postcode); + ServiceArea serviceAreaFound = serviceAreaService.getServiceAreaByName(serviceArea); + + return serviceCentreRepository.findNearestByServiceAreaAndAreaOfLawAndCatchmentTypeIn( + serviceAreaFound.getId(), + serviceAreaFound.getAreaOfLawId(), + POSTCODE_SEARCH_CATCHMENTS, + osLocationData.getLatitude(), + osLocationData.getLongitude(), + limit + ); + } +} diff --git a/src/main/resources/db/migration/V1.27__drop_service_centre_data_structures.sql b/src/main/resources/db/migration/V1.31__drop_service_centre_data_structures.sql similarity index 100% rename from src/main/resources/db/migration/V1.27__drop_service_centre_data_structures.sql rename to src/main/resources/db/migration/V1.31__drop_service_centre_data_structures.sql diff --git a/src/main/resources/db/migration/V1.28__create_service_centre_tables.sql b/src/main/resources/db/migration/V1.32__create_service_centre_tables.sql similarity index 100% rename from src/main/resources/db/migration/V1.28__create_service_centre_tables.sql rename to src/main/resources/db/migration/V1.32__create_service_centre_tables.sql diff --git a/src/main/resources/db/migration/V1.29__generalise_audit_subject.sql b/src/main/resources/db/migration/V1.33__generalise_audit_subject.sql similarity index 100% rename from src/main/resources/db/migration/V1.29__generalise_audit_subject.sql rename to src/main/resources/db/migration/V1.33__generalise_audit_subject.sql diff --git a/src/main/resources/db/migration/V1.30__refresh_service_area_mappings.sql b/src/main/resources/db/migration/V1.34__refresh_service_area_mappings.sql similarity index 100% rename from src/main/resources/db/migration/V1.30__refresh_service_area_mappings.sql rename to src/main/resources/db/migration/V1.34__refresh_service_area_mappings.sql diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaServiceTest.java index 8f5c7679f..f9ba1559b 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceAreaServiceTest.java @@ -6,11 +6,15 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.repositories.CourtLocalAuthoritiesRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; import java.util.List; import java.util.Optional; +import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -23,12 +27,19 @@ class ServiceAreaServiceTest { @Mock private ServiceAreaRepository serviceAreaRepository; + @Mock + private ServiceCentreRepository serviceCentreRepository; + + @Mock + private CourtLocalAuthoritiesRepository courtLocalAuthoritiesRepository; + @InjectMocks private ServiceAreaService serviceAreaService; @Test void getServiceAreaByNameShouldTrimAndReturnArea() { ServiceArea area = new ServiceArea(); + area.setId(UUID.randomUUID()); area.setName("Money Claims"); when(serviceAreaRepository.findByNameIgnoreCase("Money Claims")) .thenReturn(Optional.of(area)); @@ -52,6 +63,7 @@ void getServiceAreaByNameShouldThrowWhenMissing() { @Test void getAllServiceAreasForServiceShouldTrimAndReturnAreas() { ServiceArea area = new ServiceArea(); + area.setId(UUID.randomUUID()); area.setName("Civil"); List results = List.of(area); when(serviceAreaRepository.findAllByServiceName("Family")) @@ -63,6 +75,53 @@ void getAllServiceAreasForServiceShouldTrimAndReturnAreas() { verify(serviceAreaRepository).findAllByServiceName("Family"); } + @Test + void getServiceAreaByNameShouldPopulateCatchmentFlagsFromServiceCentres() { + UUID serviceAreaId = UUID.randomUUID(); + UUID areaOfLawId = UUID.randomUUID(); + ServiceArea area = new ServiceArea(); + area.setId(serviceAreaId); + area.setAreaOfLawId(areaOfLawId); + area.setName("Money Claims"); + when(serviceAreaRepository.findByNameIgnoreCase("Money Claims")) + .thenReturn(Optional.of(area)); + when(serviceCentreRepository.existsByServiceAreaIdAndCatchmentTypeIn( + serviceAreaId, List.of(CatchmentType.LOCAL)) + ).thenReturn(true); + when(serviceCentreRepository.existsByServiceAreaIdAndCatchmentTypeIn( + serviceAreaId, List.of(CatchmentType.NATIONAL)) + ).thenReturn(false); + when(serviceCentreRepository.existsByServiceAreaIdAndCatchmentTypeIn( + serviceAreaId, List.of(CatchmentType.REGIONAL)) + ).thenReturn(true); + + ServiceArea response = serviceAreaService.getServiceAreaByName("Money Claims"); + + assertThat(response.hasLocal()).isTrue(); + assertThat(response.hasNational()).isFalse(); + assertThat(response.hasRegional()).isTrue(); + } + + @Test + void getServiceAreaByNameShouldSetLocalWhenCourtLocalAuthoritiesExist() { + UUID serviceAreaId = UUID.randomUUID(); + UUID areaOfLawId = UUID.randomUUID(); + ServiceArea area = new ServiceArea(); + area.setId(serviceAreaId); + area.setAreaOfLawId(areaOfLawId); + area.setName("Family"); + when(serviceAreaRepository.findByNameIgnoreCase("Family")) + .thenReturn(Optional.of(area)); + when(serviceCentreRepository.existsByServiceAreaIdAndCatchmentTypeIn( + serviceAreaId, List.of(CatchmentType.LOCAL)) + ).thenReturn(false); + when(courtLocalAuthoritiesRepository.existsByAreaOfLawId(areaOfLawId)).thenReturn(true); + + ServiceArea response = serviceAreaService.getServiceAreaByName("Family"); + + assertThat(response.hasLocal()).isTrue(); + } + @Test void getAllServiceAreasForServiceShouldThrowWhenNoneFound() { when(serviceAreaRepository.findAllByServiceName("Family")) diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java index c72c3e1f1..398cc247b 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java @@ -92,6 +92,33 @@ void getServiceCentreDetailsByIdThrowsNotFoundWhenMissing() { assertThat(exception.getMessage()).isEqualTo("Service centre not found, ID: " + serviceCentreId); } + @Test + void getServiceCentreDetailsBySlugReturnsDetailsWhenFound() { + ServiceCentreDetails serviceCentreDetails = ServiceCentreDetails.builder() + .slug("test-service-centre") + .name("Test Service Centre") + .build(); + + when(serviceCentreDetailsRepository.findBySlug("test-service-centre")) + .thenReturn(Optional.of(serviceCentreDetails)); + + ServiceCentreDetails result = serviceCentreService.getServiceCentreDetailsBySlug("test-service-centre"); + + assertThat(result).isEqualTo(serviceCentreDetails); + } + + @Test + void getServiceCentreDetailsBySlugThrowsNotFoundWhenMissing() { + when(serviceCentreDetailsRepository.findBySlug("missing-service-centre")).thenReturn(Optional.empty()); + + NotFoundException exception = assertThrows( + NotFoundException.class, + () -> serviceCentreService.getServiceCentreDetailsBySlug("missing-service-centre") + ); + + assertThat(exception.getMessage()).isEqualTo("Service centre not found, slug: missing-service-centre"); + } + @Test void getServiceCentreByNameReturnsServiceCentre() { ServiceCentre serviceCentre = ServiceCentre.builder().name("Bulk Scan Centre").build(); @@ -124,7 +151,7 @@ void createServiceCentreGeneratesUniqueSlugStartsClosedAndNormalisesServiceAreas assertThat(result.getSlug()).isEqualTo("bulk-scan-centre-1"); assertThat(result.getOpen()).isFalse(); assertThat(result.getServiceAreaIds()).containsExactly(validServiceAreaId); - assertThat(result.getCatchmentType()).isEqualTo(CatchmentType.NATIONAL); + assertThat(result.getCatchmentType()).isEqualTo(CatchmentType.REGIONAL); } @Test @@ -155,7 +182,7 @@ void updateServiceCentreRegeneratesSlugWhenNameChanges() { assertThat(result.getSlug()).isEqualTo("new-service-centre"); assertThat(result.getOpen()).isTrue(); assertThat(result.getWarningNotice()).isEqualTo("Warning"); - assertThat(result.getCatchmentType()).isEqualTo(CatchmentType.NATIONAL); + assertThat(result.getCatchmentType()).isEqualTo(CatchmentType.LOCAL); } @Test diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchLocationServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchLocationServiceTest.java new file mode 100644 index 000000000..1fbd855c9 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchLocationServiceTest.java @@ -0,0 +1,110 @@ +package uk.gov.hmcts.reform.fact.data.api.services.search; + +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.reform.fact.data.api.dto.CourtWithDistance; +import uk.gov.hmcts.reform.fact.data.api.dto.SearchResult; +import uk.gov.hmcts.reform.fact.data.api.dto.ServiceCentreWithDistance; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchAction; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchResultType; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SearchLocationServiceTest { + + @Mock + private SearchCourtService searchCourtService; + + @Mock + private SearchServiceCentreService searchServiceCentreService; + + @InjectMocks + private SearchLocationService searchLocationService; + + @Test + void getLocationsBySearchParametersReturnsCourtsAndServiceCentresSortedByDistance() { + UUID courtId = UUID.randomUUID(); + UUID serviceCentreId = UUID.randomUUID(); + CourtWithDistance court = courtWithDistance(courtId, BigDecimal.valueOf(3)); + ServiceCentreWithDistance serviceCentre = + serviceCentreWithDistance(serviceCentreId, BigDecimal.valueOf(1)); + + when(searchCourtService.getCourtsBySearchParameters("SW1A 1AA", "Money Claims", SearchAction.DOCUMENTS, 10)) + .thenReturn(List.of(court)); + when(searchServiceCentreService.getServiceCentresBySearchParameters( + "SW1A 1AA", + "Money Claims", + SearchAction.DOCUMENTS, + 10 + )).thenReturn(List.of(serviceCentre)); + + List results = searchLocationService.getLocationsBySearchParameters( + "SW1A 1AA", + "Money Claims", + SearchAction.DOCUMENTS, + 10 + ); + + assertThat(results).extracting(SearchResult::getType) + .containsExactly(SearchResultType.SERVICE_CENTRE, SearchResultType.COURT); + assertThat(results).extracting(SearchResult::getId) + .containsExactly(serviceCentreId, courtId); + } + + private CourtWithDistance courtWithDistance(UUID courtId, BigDecimal distance) { + return new CourtWithDistance() { + @Override + public UUID getCourtId() { + return courtId; + } + + @Override + public String getCourtName() { + return "Test Court"; + } + + @Override + public String getCourtSlug() { + return "test-court"; + } + + @Override + public BigDecimal getDistance() { + return distance; + } + }; + } + + private ServiceCentreWithDistance serviceCentreWithDistance(UUID serviceCentreId, BigDecimal distance) { + return new ServiceCentreWithDistance() { + @Override + public UUID getServiceCentreId() { + return serviceCentreId; + } + + @Override + public String getServiceCentreName() { + return "Test Service Centre"; + } + + @Override + public String getServiceCentreSlug() { + return "test-service-centre"; + } + + @Override + public BigDecimal getDistance() { + return distance; + } + }; + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceAreaServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceAreaServiceTest.java new file mode 100644 index 000000000..99ecb4974 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceAreaServiceTest.java @@ -0,0 +1,59 @@ +package uk.gov.hmcts.reform.fact.data.api.services.search; + +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.reform.fact.data.api.dto.ServiceAreaSearchResult; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchResultType; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceAreaService; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SearchServiceAreaServiceTest { + + @Mock + private ServiceAreaService serviceAreaService; + + @Mock + private ServiceCentreRepository serviceCentreRepository; + + @InjectMocks + private SearchServiceAreaService searchServiceAreaService; + + @Test + void findByServiceAreaNameReturnsServiceCentreResults() { + UUID serviceAreaId = UUID.randomUUID(); + UUID serviceCentreId = UUID.randomUUID(); + ServiceArea serviceArea = new ServiceArea(); + serviceArea.setId(serviceAreaId); + ServiceCentre serviceCentre = ServiceCentre.builder() + .id(serviceCentreId) + .name("National Business Centre") + .slug("national-business-centre") + .serviceAreaIds(List.of(serviceAreaId)) + .catchmentType(CatchmentType.NATIONAL) + .build(); + + when(serviceAreaService.getServiceAreaByName("Money Claims")).thenReturn(serviceArea); + when(serviceCentreRepository.findByServiceAreaId(serviceAreaId)).thenReturn(List.of(serviceCentre)); + + List results = searchServiceAreaService.findByServiceAreaName("Money Claims"); + + assertThat(results).hasSize(1); + assertThat(results.getFirst().getId()).isEqualTo(serviceCentreId); + assertThat(results.getFirst().getServiceCentreName()).isEqualTo("National Business Centre"); + assertThat(results.getFirst().getCatchmentType()).isEqualTo(CatchmentType.NATIONAL); + assertThat(results.getFirst().getType()).isEqualTo(SearchResultType.SERVICE_CENTRE); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceCentreServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceCentreServiceTest.java new file mode 100644 index 000000000..76a11c657 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceCentreServiceTest.java @@ -0,0 +1,109 @@ +package uk.gov.hmcts.reform.fact.data.api.services.search; + +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.reform.fact.data.api.dto.ServiceCentreWithDistance; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchAction; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; +import uk.gov.hmcts.reform.fact.data.api.os.OsLocationData; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; +import uk.gov.hmcts.reform.fact.data.api.services.OsService; +import uk.gov.hmcts.reform.fact.data.api.services.ServiceAreaService; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SearchServiceCentreServiceTest { + + @Mock + private OsService osService; + + @Mock + private ServiceAreaService serviceAreaService; + + @Mock + private ServiceCentreRepository serviceCentreRepository; + + @InjectMocks + private SearchServiceCentreService searchServiceCentreService; + + @Test + void getServiceCentresBySearchParametersReturnsEmptyWhenSearchingPostcodeOnly() { + List result = searchServiceCentreService.getServiceCentresBySearchParameters( + "SW1A 1AA", + null, + null, + 10 + ); + + assertThat(result).isEmpty(); + verifyNoInteractions(osService); + verifyNoInteractions(serviceAreaService); + verifyNoInteractions(serviceCentreRepository); + } + + @Test + void getServiceCentresBySearchParametersThrowsWhenOnlyServiceAreaProvided() { + assertThatThrownBy(() -> searchServiceCentreService.getServiceCentresBySearchParameters( + "SW1A 1AA", + "Money Claims", + null, + 10 + )).isInstanceOf(InvalidParameterCombinationException.class); + } + + @Test + void getServiceCentresBySearchParametersReturnsMatchingServiceCentres() { + UUID serviceAreaId = UUID.randomUUID(); + UUID areaOfLawId = UUID.randomUUID(); + ServiceArea serviceArea = new ServiceArea(); + serviceArea.setId(serviceAreaId); + serviceArea.setAreaOfLawId(areaOfLawId); + OsLocationData osLocationData = OsLocationData.builder() + .latitude(51.5) + .longitude(-0.1) + .build(); + List expected = List.of(mock(ServiceCentreWithDistance.class)); + + when(osService.getOsLonLatDistrictByPartial("SW1A 1AA")).thenReturn(osLocationData); + when(serviceAreaService.getServiceAreaByName("Money Claims")).thenReturn(serviceArea); + when(serviceCentreRepository.findNearestByServiceAreaAndAreaOfLawAndCatchmentTypeIn( + serviceAreaId, + areaOfLawId, + List.of(CatchmentType.LOCAL, CatchmentType.REGIONAL), + 51.5, + -0.1, + 10 + )).thenReturn(expected); + + List result = searchServiceCentreService.getServiceCentresBySearchParameters( + "SW1A 1AA", + "Money Claims", + SearchAction.DOCUMENTS, + 10 + ); + + assertThat(result).isEqualTo(expected); + verify(serviceCentreRepository).findNearestByServiceAreaAndAreaOfLawAndCatchmentTypeIn( + serviceAreaId, + areaOfLawId, + List.of(CatchmentType.LOCAL, CatchmentType.REGIONAL), + 51.5, + -0.1, + 10 + ); + } +} From b0c99319ffc8a861ca09b6d43dabc17f2c6edcff Mon Sep 17 00:00:00 2001 From: Josh <23316525+josh-uk@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:52:13 +0100 Subject: [PATCH 06/40] Potential fix for pull request finding 'CodeQL / Cross-site scripting' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../fact/data/api/controllers/TestingSupportController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java index 245cfbdca..94dc92dda 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportController.java @@ -63,7 +63,7 @@ public ResponseEntity deleteCourtsByNamePrefix( ) { return ResponseEntity.status(HttpStatus.OK) .body(courtService.deleteCourtsByNamePrefix(courtNamePrefix) - + " court(s) with prefix " + courtNamePrefix + " deleted successfully"); + + " court(s) deleted successfully"); } @DeleteMapping("/service-centres/name-prefix/{serviceCentreNamePrefix}") @@ -82,7 +82,7 @@ public ResponseEntity deleteServiceCentresByNamePrefix( ) { return ResponseEntity.status(HttpStatus.OK) .body(serviceCentreService.deleteServiceCentresByNamePrefix(serviceCentreNamePrefix) - + " service centre(s) with prefix " + serviceCentreNamePrefix + " deleted successfully"); + + " service centre(s) deleted successfully"); } @GetMapping("/regions") From ee581397a181fbcc98cf44eb67fad9f9ff0ded35 Mon Sep 17 00:00:00 2001 From: josh-uk Date: Tue, 23 Jun 2026 11:58:39 +0100 Subject: [PATCH 07/40] Fix sonar issues --- .../fact/data/api/services/AuditService.java | 74 ++++++++++--------- .../TestingSupportControllerTest.java | 4 +- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java index 459265771..6137a7dbc 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java @@ -28,12 +28,6 @@ public class AuditService { private final AuditRepository auditRepository; private final AuditConfigurationProperties auditConfiguration; - // bitfields for query column matching - private static final int IGNORE = 0b0000; - private static final int INCLUDE_SUBJECT_ID = 0b0001; - private static final int INCLUDE_TO_DATE = 0b0010; - private static final int INCLUDE_EMAIL = 0b0100; - /** * Get a paginated list of {@link Audit}s with optional filters. * @@ -82,38 +76,46 @@ public void removeExpiredAuditEntries() { private Page performAuditQuery(ZonedDateTime fromDateTime, ZonedDateTime toDateTime, SubjectFilter subjectFilter, String email, Pageable pageable) { + boolean hasToDate = toDateTime != null; + boolean hasEmail = email != null && !email.isBlank(); - // create a query bitfield that can be tested in the switch - // below using boolean arithmetic matching. - int query = 0; - query |= subjectFilter != null ? INCLUDE_SUBJECT_ID : IGNORE; - query |= toDateTime != null ? INCLUDE_TO_DATE : IGNORE; - query |= email != null && !email.isBlank() ? INCLUDE_EMAIL : IGNORE; - - // perform the appropriate repository lookup based on matching - // bits in the query param - return switch (query) { - case INCLUDE_SUBJECT_ID -> auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfter( - subjectFilter.subjectId(), subjectFilter.subjectType(), fromDateTime, pageable); - case INCLUDE_TO_DATE -> auditRepository.findByCreatedAtBetween( - fromDateTime, toDateTime, pageable); - case INCLUDE_SUBJECT_ID | INCLUDE_TO_DATE -> - auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtBetween( - subjectFilter.subjectId(), subjectFilter.subjectType(), fromDateTime, toDateTime, pageable); - case INCLUDE_EMAIL -> auditRepository.findByCreatedAtAfterAndEmailAddressLike( - fromDateTime, email, pageable); - case INCLUDE_SUBJECT_ID | INCLUDE_EMAIL -> - auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfterAndEmailAddressLike( - subjectFilter.subjectId(), subjectFilter.subjectType(), fromDateTime, email, pageable); - case INCLUDE_TO_DATE | INCLUDE_EMAIL -> auditRepository.findByCreatedAtBetweenAndEmailAddressLike( + if (subjectFilter != null) { + return performSubjectAuditQuery(fromDateTime, toDateTime, subjectFilter, email, pageable); + } + if (hasToDate && hasEmail) { + return auditRepository.findByCreatedAtBetweenAndEmailAddressLike( fromDateTime, toDateTime, email, pageable); - case INCLUDE_SUBJECT_ID | INCLUDE_TO_DATE | INCLUDE_EMAIL -> - auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( - subjectFilter.subjectId(), subjectFilter.subjectType(), fromDateTime, toDateTime, email, pageable); - // if no field match bits are set then test only using the - // mandatory fromDate. - default -> auditRepository.findByCreatedAtAfter(fromDateTime, pageable); - }; + } + if (hasToDate) { + return auditRepository.findByCreatedAtBetween(fromDateTime, toDateTime, pageable); + } + if (hasEmail) { + return auditRepository.findByCreatedAtAfterAndEmailAddressLike(fromDateTime, email, pageable); + } + return auditRepository.findByCreatedAtAfter(fromDateTime, pageable); + } + + private Page performSubjectAuditQuery(ZonedDateTime fromDateTime, ZonedDateTime toDateTime, + SubjectFilter subjectFilter, String email, Pageable pageable) { + UUID subjectId = subjectFilter.subjectId(); + AuditSubjectType subjectType = subjectFilter.subjectType(); + boolean hasToDate = toDateTime != null; + boolean hasEmail = email != null && !email.isBlank(); + + if (hasToDate && hasEmail) { + return auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( + subjectId, subjectType, fromDateTime, toDateTime, email, pageable); + } + if (hasToDate) { + return auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtBetween( + subjectId, subjectType, fromDateTime, toDateTime, pageable); + } + if (hasEmail) { + return auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfterAndEmailAddressLike( + subjectId, subjectType, fromDateTime, email, pageable); + } + return auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfter( + subjectId, subjectType, fromDateTime, pageable); } private static SubjectFilter resolveSubjectFilter(String courtId, String serviceCentreId) { diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java index 39d039224..fe9b4c7e5 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/TestingSupportControllerTest.java @@ -56,7 +56,7 @@ void deleteCourtsByNamePrefixReturns200() { ResponseEntity response = testingSupportController.deleteCourtsByNamePrefix(COURT_NAME_PREFIX); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).contains("3 court(s)").contains(COURT_NAME_PREFIX); + assertThat(response.getBody()).contains("3 court(s)").doesNotContain(COURT_NAME_PREFIX); verify(courtService).deleteCourtsByNamePrefix(COURT_NAME_PREFIX); } @@ -77,7 +77,7 @@ void deleteServiceCentresByNamePrefixReturns200() { testingSupportController.deleteServiceCentresByNamePrefix(SERVICE_CENTRE_NAME_PREFIX); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).contains("2 service centre(s)").contains(SERVICE_CENTRE_NAME_PREFIX); + assertThat(response.getBody()).contains("2 service centre(s)").doesNotContain(SERVICE_CENTRE_NAME_PREFIX); verify(serviceCentreService).deleteServiceCentresByNamePrefix(SERVICE_CENTRE_NAME_PREFIX); } From 539b6316e590cf5dfd4be6989d18db06c8e4c280 Mon Sep 17 00:00:00 2001 From: Adam Stevenson Date: Tue, 23 Jun 2026 16:16:42 +0100 Subject: [PATCH 08/40] FACT-2821 Migration (#281) * migration code cleanup. SC tables mig * update to mig for sc * change open to closed by default * update to unit tests * Checkstyle fix --------- Co-authored-by: josh-uk --- .../api/migration/model/MigrationResult.java | 3 +- .../migration/service/MigrationContext.java | 1 + .../migration/service/MigrationService.java | 16 +- .../service/ServiceCentreMigrationHelper.java | 192 +++++++++ .../service/MigrationServiceTest.java | 30 +- .../ServiceCentreMigrationHelperTest.java | 404 ++++++++++++++++++ 6 files changed, 641 insertions(+), 5 deletions(-) create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/ServiceCentreMigrationHelper.java create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/ServiceCentreMigrationHelperTest.java diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/model/MigrationResult.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/model/MigrationResult.java index 80107e52f..af69381c7 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/model/MigrationResult.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/model/MigrationResult.java @@ -9,12 +9,13 @@ @AllArgsConstructor public class MigrationResult { private int courtsMigrated; + private int serviceCentresMigrated; private int courtAreasOfLawMigrated; - private int courtServiceAreasMigrated; private int courtLocalAuthoritiesMigrated; private int courtSinglePointsOfEntryMigrated; private int courtProfessionalInformationMigrated; private int courtCodesMigrated; private int courtDxCodesMigrated; private int courtFaxMigrated; + private int serviceCentreAreasOfLawMigrated; } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationContext.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationContext.java index 4a9fbf597..a9d0e2432 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationContext.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationContext.java @@ -19,4 +19,5 @@ class MigrationContext { int courtCodesMigrated; int courtDxCodesMigrated; int courtFaxMigrated; + int serviceCentreAreasOfLawMigrated; } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationService.java index a892f3176..b98fc41da 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationService.java @@ -29,6 +29,8 @@ import uk.gov.hmcts.reform.fact.data.api.repositories.OpeningHoursTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.RegionRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreAreasOfLawRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; @Service @@ -41,6 +43,7 @@ public class MigrationService { private final TransactionTemplate transactionTemplate; private final ReferenceDataImporter referenceDataImporter; private final CourtMigrationHelper courtMigrationHelper; + private final ServiceCentreMigrationHelper serviceCentreMigrationHelper; public MigrationService( LegacyFactClient legacyFactClient, @@ -59,6 +62,8 @@ public MigrationService( CourtCodesRepository courtCodesRepository, CourtDxCodeRepository courtDxCodeRepository, CourtFaxRepository courtFaxRepository, + ServiceCentreRepository serviceCentreRepository, + ServiceCentreAreasOfLawRepository serviceCentreAreasOfLawRepository, MigrationAuditRepository migrationAuditRepository, TransactionTemplate transactionTemplate, CourtService courtService @@ -87,6 +92,10 @@ public MigrationService( legacyCourtMappingRepository, courtService ); + this.serviceCentreMigrationHelper = new ServiceCentreMigrationHelper( + serviceCentreRepository, + serviceCentreAreasOfLawRepository + ); } public MigrationSummary migrate() { @@ -110,17 +119,20 @@ MigrationSummary persistExport(LegacyExportResponse exportResponse) { final MigrationContext context = new MigrationContext(); referenceDataImporter.importReferenceData(exportResponse, context); int courtsMigrated = courtMigrationHelper.migrateCourts(exportResponse.getCourts(), context); + int serviceCentresMigrated = + serviceCentreMigrationHelper.migrateServiceCentres(exportResponse.getCourts(), context); MigrationResult result = new MigrationResult( courtsMigrated, + serviceCentresMigrated, context.getCourtAreasOfLawMigrated(), - 0, context.getCourtLocalAuthoritiesMigrated(), context.getCourtSinglePointsOfEntryMigrated(), context.getCourtProfessionalInformationMigrated(), context.getCourtCodesMigrated(), context.getCourtDxCodesMigrated(), - context.getCourtFaxMigrated() + context.getCourtFaxMigrated(), + context.getServiceCentreAreasOfLawMigrated() ); return new MigrationSummary(result); diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/ServiceCentreMigrationHelper.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/ServiceCentreMigrationHelper.java new file mode 100644 index 000000000..ae583f26c --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/ServiceCentreMigrationHelper.java @@ -0,0 +1,192 @@ +package uk.gov.hmcts.reform.fact.data.api.migration.service; + +import jakarta.validation.ConstraintViolationException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.migration.model.CourtAreasOfLawDto; +import uk.gov.hmcts.reform.fact.data.api.migration.model.CourtDto; +import uk.gov.hmcts.reform.fact.data.api.migration.model.CourtServiceAreaDto; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreAreasOfLawRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; + +class ServiceCentreMigrationHelper { + + private static final Logger LOG = LoggerFactory.getLogger(ServiceCentreMigrationHelper.class); + private static final Pattern SERVICE_CENTRE_NAME_PATTERN = Pattern.compile("^[A-Za-z&'()\\- ]+$"); + + private final ServiceCentreRepository serviceCentreRepository; + private final ServiceCentreAreasOfLawRepository serviceCentreAreasOfLawRepository; + + ServiceCentreMigrationHelper( + ServiceCentreRepository serviceCentreRepository, + ServiceCentreAreasOfLawRepository serviceCentreAreasOfLawRepository + ) { + this.serviceCentreRepository = serviceCentreRepository; + this.serviceCentreAreasOfLawRepository = serviceCentreAreasOfLawRepository; + } + + int migrateServiceCentres(List courts, MigrationContext context) { + if (isEmpty(courts)) { + return 0; + } + + int total = 0; + for (CourtDto dto : courts) { + if (!Boolean.TRUE.equals(dto.getIsServiceCentre())) { + continue; + } + + String serviceCentreName = sanitiseServiceCentreName(dto.getName()); + if (StringUtils.isBlank(serviceCentreName)) { + LOG.warn("Skipping service centre {} because sanitised name was blank", dto.getSlug()); + continue; + } + if (!SERVICE_CENTRE_NAME_PATTERN.matcher(serviceCentreName).matches()) { + LOG.warn( + "Skipping service centre {} because sanitised name '{}' still fails validation regex", + dto.getSlug(), + serviceCentreName + ); + continue; + } + if (StringUtils.length(serviceCentreName) < 5 || StringUtils.length(serviceCentreName) > 200) { + LOG.warn("Skipping service centre {} because name length is outside 5-200 characters", dto.getSlug()); + continue; + } + + ServiceAreaSelection serviceAreaSelection = selectServiceAreas(dto.getCourtServiceAreas(), context); + ServiceCentre serviceCentre = ServiceCentre.builder() + .name(serviceCentreName) + .slug(dto.getSlug()) + .open(Boolean.FALSE) + .serviceAreaIds(serviceAreaSelection.serviceAreaIds()) + .catchmentType(serviceAreaSelection.catchmentType().orElse(null)) + .build(); + + ServiceCentre savedServiceCentre; + try { + savedServiceCentre = serviceCentreRepository.save(serviceCentre); + } catch (ConstraintViolationException ex) { + LOG.error("Validation failed while migrating service centre '{}': {}", dto.getName(), ex.getMessage()); + throw ex; + } + + UUID serviceCentreId = savedServiceCentre.getId(); + persistServiceCentreAreasOfLaw(dto.getCourtAreasOfLaw(), serviceCentreId, context); + total++; + } + return total; + } + + private void persistServiceCentreAreasOfLaw( + CourtAreasOfLawDto dto, + UUID serviceCentreId, + MigrationContext context + ) { + if (dto == null || isEmpty(dto.getAreaOfLawIds())) { + return; + } + + List areasOfLaw = mapIds( + dto.getAreaOfLawIds(), + context.getAreaOfLawIds(), + "service centre areas of law" + ); + if (areasOfLaw.isEmpty()) { + LOG.warn("Skipping service centre areas of law for {} because all area ids were unmapped", serviceCentreId); + return; + } + + serviceCentreAreasOfLawRepository.save(ServiceCentreAreasOfLaw.builder() + .serviceCentreId(serviceCentreId) + .areasOfLaw(areasOfLaw) + .build()); + context.serviceCentreAreasOfLawMigrated++; + } + + private ServiceAreaSelection selectServiceAreas(List dtos, MigrationContext context) { + if (isEmpty(dtos)) { + return ServiceAreaSelection.empty(); + } + + return dtos.stream() + .map(dto -> new ServiceAreaSelection( + mapIds(dto.getServiceAreaIds(), context.getServiceAreaIds(), "service centre service area"), + parseCatchmentType(dto.getCatchmentType()) + )) + .filter(selection -> !selection.serviceAreaIds().isEmpty() || selection.catchmentType().isPresent()) + .min(Comparator.comparing(ServiceCentreMigrationHelper::catchmentPriority)) + .orElseGet(ServiceAreaSelection::empty); + } + + private static int catchmentPriority(ServiceAreaSelection selection) { + if (selection.catchmentType().isEmpty()) { + return 3; + } + return switch (selection.catchmentType().get()) { + case NATIONAL -> 0; + case REGIONAL -> 1; + case LOCAL -> 2; + }; + } + + private static Optional parseCatchmentType(String value) { + if (StringUtils.isBlank(value)) { + return Optional.empty(); + } + try { + return Optional.of(CatchmentType.valueOf(StringUtils.upperCase(value).replace('-', '_'))); + } catch (IllegalArgumentException ex) { + LOG.warn("Unknown service centre catchment type '{}'", value); + return Optional.empty(); + } + } + + private static List mapIds(List sourceIds, Map lookup, String context) { + if (sourceIds == null || sourceIds.isEmpty()) { + return List.of(); + } + + List results = new ArrayList<>(); + for (Integer id : sourceIds) { + UUID mapped = lookup.get(id); + if (mapped == null) { + LOG.warn("Unable to map {} identifier '{}' in migration payload", context, id); + continue; + } + results.add(mapped); + } + + return results.isEmpty() ? List.of() : results; + } + + private static boolean isEmpty(Collection values) { + return values == null || values.isEmpty(); + } + + private static String sanitiseServiceCentreName(String name) { + if (StringUtils.isBlank(name)) { + return name; + } + String cleaned = name.replaceAll("[^A-Za-z&'()\\- ]", " "); + return cleaned.replaceAll("\\s+", " ").trim(); + } + + private record ServiceAreaSelection(List serviceAreaIds, Optional catchmentType) { + private static ServiceAreaSelection empty() { + return new ServiceAreaSelection(List.of(), Optional.empty()); + } + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationServiceTest.java index 46e33ce3e..922c8988f 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/MigrationServiceTest.java @@ -14,10 +14,12 @@ import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; import uk.gov.hmcts.reform.fact.data.api.entities.AreaOfLawType; +import uk.gov.hmcts.reform.fact.data.api.entities.ContactDescriptionType; import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.LocalAuthorityType; import uk.gov.hmcts.reform.fact.data.api.entities.Region; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; import uk.gov.hmcts.reform.fact.data.api.entities.types.AllowedLocalAuthorityAreasOfLaw; import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentMethod; import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; @@ -66,6 +68,8 @@ import uk.gov.hmcts.reform.fact.data.api.repositories.OpeningHoursTypeRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.RegionRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreAreasOfLawRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; import static org.assertj.core.api.Assertions.assertThat; @@ -86,6 +90,7 @@ class MigrationServiceTest { private static final UUID AREA_OF_LAW_ID = UUID.randomUUID(); private static final UUID SERVICE_AREA_ID = UUID.randomUUID(); private static final UUID COURT_ID = UUID.randomUUID(); + private static final UUID SERVICE_CENTRE_ID = UUID.randomUUID(); private static final UUID LOCAL_AUTHORITY_TYPE_ID = UUID.randomUUID(); @Mock @@ -125,6 +130,10 @@ class MigrationServiceTest { @Mock private LegacyCourtMappingRepository legacyCourtMappingRepository; @Mock + private ServiceCentreRepository serviceCentreRepository; + @Mock + private ServiceCentreAreasOfLawRepository serviceCentreAreasOfLawRepository; + @Mock private TransactionTemplate transactionTemplate; @Mock private CourtService courtService; @@ -174,12 +183,24 @@ void setUp() { return Optional.of(serviceArea); }); lenient().when(legacyServiceRepository.findByName(anyString())).thenReturn(Optional.empty()); + lenient().when(contactDescriptionTypeRepository.findAll()).thenReturn(List.of( + ContactDescriptionType.builder() + .id(UUID.randomUUID()) + .name("CDT") + .nameCy("CDT CY") + .build() + )); lenient().when(courtService.createCourt(any(Court.class))).thenAnswer(invocation -> { Court court = invocation.getArgument(0); court.setId(COURT_ID); return court; }); + lenient().when(serviceCentreRepository.save(any(ServiceCentre.class))).thenAnswer(invocation -> { + ServiceCentre serviceCentre = invocation.getArgument(0); + serviceCentre.setId(SERVICE_CENTRE_ID); + return serviceCentre; + }); lenient().when(transactionTemplate.execute(any())).thenAnswer(invocation -> { @SuppressWarnings("unchecked") @@ -257,14 +278,15 @@ void shouldPersistLegacyDataAndReturnSummary() { MigrationResult result = summary.getResult(); assertThat(result.getCourtsMigrated()).isEqualTo(1); + assertThat(result.getServiceCentresMigrated()).isZero(); assertThat(result.getCourtAreasOfLawMigrated()).isEqualTo(1); - assertThat(result.getCourtServiceAreasMigrated()).isZero(); assertThat(result.getCourtLocalAuthoritiesMigrated()).isEqualTo(1); assertThat(result.getCourtSinglePointsOfEntryMigrated()).isEqualTo(1); assertThat(result.getCourtProfessionalInformationMigrated()).isEqualTo(1); assertThat(result.getCourtCodesMigrated()).isEqualTo(1); assertThat(result.getCourtDxCodesMigrated()).isEqualTo(1); assertThat(result.getCourtFaxMigrated()).isEqualTo(1); + assertThat(result.getServiceCentreAreasOfLawMigrated()).isZero(); verify(legacyServiceRepository).save(any(LegacyService.class)); verify(courtAreasOfLawRepository).save(any()); @@ -306,7 +328,7 @@ void shouldSkipCourtDxCodesWhenDetailsMissing() { } @Test - void shouldSkipServiceCentres() { + void shouldMigrateServiceCentresWithoutCreatingCourts() { CourtDto serviceCentre = new CourtDto( 999L, "Service Centre Court", @@ -344,7 +366,11 @@ void shouldSkipServiceCentres() { MigrationSummary summary = migrationService.migrate(); assertThat(summary.getResult().getCourtsMigrated()).isZero(); + assertThat(summary.getResult().getServiceCentresMigrated()).isEqualTo(1); + assertThat(summary.getResult().getServiceCentreAreasOfLawMigrated()).isEqualTo(1); verify(courtService, never()).createCourt(any(Court.class)); + verify(serviceCentreRepository).save(any(ServiceCentre.class)); + verify(serviceCentreAreasOfLawRepository).save(any()); } private CourtDto courtDto() { diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/ServiceCentreMigrationHelperTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/ServiceCentreMigrationHelperTest.java new file mode 100644 index 000000000..43f37a58a --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/migration/service/ServiceCentreMigrationHelperTest.java @@ -0,0 +1,404 @@ +package uk.gov.hmcts.reform.fact.data.api.migration.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.validation.ConstraintViolationException; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.migration.model.CourtAreasOfLawDto; +import uk.gov.hmcts.reform.fact.data.api.migration.model.CourtDto; +import uk.gov.hmcts.reform.fact.data.api.migration.model.CourtServiceAreaDto; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreAreasOfLawRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; + +@ExtendWith(MockitoExtension.class) +class ServiceCentreMigrationHelperTest { + + @Mock private ServiceCentreRepository serviceCentreRepository; + @Mock private ServiceCentreAreasOfLawRepository serviceCentreAreasOfLawRepository; + + private ServiceCentreMigrationHelper helper; + private MigrationContext context; + + @BeforeEach + void setUp() { + helper = new ServiceCentreMigrationHelper( + serviceCentreRepository, + serviceCentreAreasOfLawRepository + ); + context = new MigrationContext(); + } + + @Test + void shouldPersistServiceCentreAndMappedChildRecords() { + UUID serviceCentreId = UUID.randomUUID(); + UUID nationalServiceAreaId = UUID.randomUUID(); + UUID localServiceAreaId = UUID.randomUUID(); + UUID areaOfLawId = UUID.randomUUID(); + + context.getServiceAreaIds().put(1, localServiceAreaId); + context.getServiceAreaIds().put(2, nationalServiceAreaId); + context.getAreaOfLawIds().put(10, areaOfLawId); + + when(serviceCentreRepository.save(any(ServiceCentre.class))).thenAnswer(invocation -> { + ServiceCentre serviceCentre = invocation.getArgument(0); + serviceCentre.setId(serviceCentreId); + return serviceCentre; + }); + + CourtDto serviceCentreDto = buildServiceCentreDto(); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isEqualTo(1); + assertThat(context.getServiceCentreAreasOfLawMigrated()).isEqualTo(1); + + ArgumentCaptor serviceCentreCaptor = ArgumentCaptor.forClass(ServiceCentre.class); + verify(serviceCentreRepository).save(serviceCentreCaptor.capture()); + assertThat(serviceCentreCaptor.getValue().getName()).isEqualTo("Legacy Service Centre"); + assertThat(serviceCentreCaptor.getValue().getSlug()).isEqualTo("legacy-service-centre"); + assertThat(serviceCentreCaptor.getValue().getOpen()).isFalse(); + assertThat(serviceCentreCaptor.getValue().getServiceAreaIds()).containsExactly(nationalServiceAreaId); + assertThat(serviceCentreCaptor.getValue().getCatchmentType()).isEqualTo(CatchmentType.NATIONAL); + + ArgumentCaptor areasCaptor = + ArgumentCaptor.forClass(ServiceCentreAreasOfLaw.class); + verify(serviceCentreAreasOfLawRepository).save(areasCaptor.capture()); + assertThat(areasCaptor.getValue().getServiceCentreId()).isEqualTo(serviceCentreId); + assertThat(areasCaptor.getValue().getAreasOfLaw()).containsExactly(areaOfLawId); + } + + @Test + void shouldIgnoreCourtRows() { + CourtDto courtDto = new CourtDto(); + courtDto.setIsServiceCentre(false); + + int migrated = helper.migrateServiceCentres(List.of(courtDto), context); + + assertThat(migrated).isZero(); + verify(serviceCentreRepository, never()).save(any(ServiceCentre.class)); + } + + @Test + void shouldReturnZeroWhenCourtListIsNull() { + int migrated = helper.migrateServiceCentres(null, context); + + assertThat(migrated).isZero(); + verify(serviceCentreRepository, never()).save(any(ServiceCentre.class)); + } + + @Test + void shouldSkipServiceCentreWhenSanitisedNameIsBlank() { + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("123 !!!"); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isZero(); + verify(serviceCentreRepository, never()).save(any(ServiceCentre.class)); + } + + @Test + void shouldSkipServiceCentreWhenNameIsNull() { + CourtDto serviceCentreDto = buildMinimalServiceCentreDto(null); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isZero(); + verify(serviceCentreRepository, never()).save(any(ServiceCentre.class)); + } + + @Test + void shouldSkipServiceCentreWhenNameIsTooShortAfterSanitising() { + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("Abcd"); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isZero(); + verify(serviceCentreRepository, never()).save(any(ServiceCentre.class)); + } + + @Test + void shouldSkipServiceCentreWhenNameIsTooLongAfterSanitising() { + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("A".repeat(201)); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isZero(); + verify(serviceCentreRepository, never()).save(any(ServiceCentre.class)); + } + + @Test + void shouldPropagateValidationFailureWhenServiceCentreCannotBeSaved() { + ConstraintViolationException exception = new ConstraintViolationException("invalid", Set.of()); + doThrow(exception).when(serviceCentreRepository).save(any(ServiceCentre.class)); + + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("Legacy Service Centre"); + + assertThatThrownBy(() -> helper.migrateServiceCentres(List.of(serviceCentreDto), context)) + .isSameAs(exception); + } + + @Test + void shouldPreserveCatchmentTypeWhenServiceAreaIdsAreUnmapped() { + mockServiceCentreSave(); + + CourtDto serviceCentreDto = new CourtDto(); + serviceCentreDto.setName("Legacy Service Centre"); + serviceCentreDto.setSlug("legacy-service-centre"); + serviceCentreDto.setOpen(true); + serviceCentreDto.setIsServiceCentre(true); + serviceCentreDto.setCourtServiceAreas(List.of( + new CourtServiceAreaDto(1, CatchmentType.LOCAL.name(), List.of(999)) + )); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isEqualTo(1); + + ArgumentCaptor serviceCentreCaptor = ArgumentCaptor.forClass(ServiceCentre.class); + verify(serviceCentreRepository).save(serviceCentreCaptor.capture()); + assertThat(serviceCentreCaptor.getValue().getServiceAreaIds()).isEmpty(); + assertThat(serviceCentreCaptor.getValue().getCatchmentType()).isEqualTo(CatchmentType.LOCAL); + } + + @Test + void shouldMigrateServiceCentreWithoutServiceAreasOrAreasOfLaw() { + mockServiceCentreSave(); + + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("Legacy Service Centre"); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isEqualTo(1); + assertThat(context.getServiceCentreAreasOfLawMigrated()).isZero(); + + ArgumentCaptor serviceCentreCaptor = ArgumentCaptor.forClass(ServiceCentre.class); + verify(serviceCentreRepository).save(serviceCentreCaptor.capture()); + assertThat(serviceCentreCaptor.getValue().getServiceAreaIds()).isEmpty(); + assertThat(serviceCentreCaptor.getValue().getCatchmentType()).isNull(); + verify(serviceCentreAreasOfLawRepository, never()).save(any(ServiceCentreAreasOfLaw.class)); + } + + @Test + void shouldSkipAreasOfLawWhenLegacyAreaIdsAreEmpty() { + when(serviceCentreRepository.save(any(ServiceCentre.class))).thenAnswer(invocation -> { + ServiceCentre serviceCentre = invocation.getArgument(0); + serviceCentre.setId(UUID.randomUUID()); + return serviceCentre; + }); + + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("Legacy Service Centre"); + serviceCentreDto.setCourtAreasOfLaw(new CourtAreasOfLawDto("areas", List.of())); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isEqualTo(1); + assertThat(context.getServiceCentreAreasOfLawMigrated()).isZero(); + verify(serviceCentreAreasOfLawRepository, never()).save(any(ServiceCentreAreasOfLaw.class)); + } + + @Test + void shouldSkipAreasOfLawWhenAllAreaIdsAreUnmapped() { + when(serviceCentreRepository.save(any(ServiceCentre.class))).thenAnswer(invocation -> { + ServiceCentre serviceCentre = invocation.getArgument(0); + serviceCentre.setId(UUID.randomUUID()); + return serviceCentre; + }); + + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("Legacy Service Centre"); + serviceCentreDto.setCourtAreasOfLaw(new CourtAreasOfLawDto("areas", List.of(999))); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isEqualTo(1); + assertThat(context.getServiceCentreAreasOfLawMigrated()).isZero(); + verify(serviceCentreAreasOfLawRepository, never()).save(any(ServiceCentreAreasOfLaw.class)); + } + + @Test + void shouldPreferRegionalCatchmentOverLocalWhenNationalIsAbsent() { + UUID regionalServiceAreaId = UUID.randomUUID(); + UUID localServiceAreaId = UUID.randomUUID(); + context.getServiceAreaIds().put(1, localServiceAreaId); + context.getServiceAreaIds().put(2, regionalServiceAreaId); + mockServiceCentreSave(); + + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("Legacy Service Centre"); + serviceCentreDto.setCourtServiceAreas(List.of( + new CourtServiceAreaDto(1, CatchmentType.LOCAL.name(), List.of(1)), + new CourtServiceAreaDto(2, CatchmentType.REGIONAL.name(), List.of(2)) + )); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isEqualTo(1); + + ArgumentCaptor serviceCentreCaptor = ArgumentCaptor.forClass(ServiceCentre.class); + verify(serviceCentreRepository).save(serviceCentreCaptor.capture()); + assertThat(serviceCentreCaptor.getValue().getServiceAreaIds()).containsExactly(regionalServiceAreaId); + assertThat(serviceCentreCaptor.getValue().getCatchmentType()).isEqualTo(CatchmentType.REGIONAL); + } + + @Test + void shouldChooseMappedServiceAreaWithoutCatchmentWhenOtherSelectionsAreUnmappedAndUnknown() { + UUID serviceAreaId = UUID.randomUUID(); + context.getServiceAreaIds().put(1, serviceAreaId); + mockServiceCentreSave(); + + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("Legacy Service Centre"); + serviceCentreDto.setCourtServiceAreas(List.of( + new CourtServiceAreaDto(1, null, List.of(1)), + new CourtServiceAreaDto(2, "unknown", List.of(999)) + )); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isEqualTo(1); + + ArgumentCaptor serviceCentreCaptor = ArgumentCaptor.forClass(ServiceCentre.class); + verify(serviceCentreRepository).save(serviceCentreCaptor.capture()); + assertThat(serviceCentreCaptor.getValue().getServiceAreaIds()).containsExactly(serviceAreaId); + assertThat(serviceCentreCaptor.getValue().getCatchmentType()).isNull(); + } + + @Test + void shouldMapServiceAreasWithoutCatchmentType() { + UUID serviceAreaId = UUID.randomUUID(); + context.getServiceAreaIds().put(1, serviceAreaId); + mockServiceCentreSave(); + + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("Legacy Service Centre"); + serviceCentreDto.setCourtServiceAreas(List.of(new CourtServiceAreaDto(1, null, List.of(1)))); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isEqualTo(1); + + ArgumentCaptor serviceCentreCaptor = ArgumentCaptor.forClass(ServiceCentre.class); + verify(serviceCentreRepository).save(serviceCentreCaptor.capture()); + assertThat(serviceCentreCaptor.getValue().getServiceAreaIds()).containsExactly(serviceAreaId); + assertThat(serviceCentreCaptor.getValue().getCatchmentType()).isNull(); + } + + @Test + void shouldIgnoreServiceAreaSelectionWhenIdsAreEmptyAndCatchmentIsBlank() { + mockServiceCentreSave(); + + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("Legacy Service Centre"); + serviceCentreDto.setCourtServiceAreas(List.of(new CourtServiceAreaDto(1, " ", List.of()))); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isEqualTo(1); + + ArgumentCaptor serviceCentreCaptor = ArgumentCaptor.forClass(ServiceCentre.class); + verify(serviceCentreRepository).save(serviceCentreCaptor.capture()); + assertThat(serviceCentreCaptor.getValue().getServiceAreaIds()).isEmpty(); + assertThat(serviceCentreCaptor.getValue().getCatchmentType()).isNull(); + } + + @Test + void shouldSelectFirstMappedServiceAreaWhenCatchmentTypesAreAbsent() { + UUID firstServiceAreaId = UUID.randomUUID(); + UUID secondServiceAreaId = UUID.randomUUID(); + context.getServiceAreaIds().put(1, firstServiceAreaId); + context.getServiceAreaIds().put(2, secondServiceAreaId); + mockServiceCentreSave(); + + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("Legacy Service Centre"); + serviceCentreDto.setCourtServiceAreas(List.of( + new CourtServiceAreaDto(1, null, List.of(1)), + new CourtServiceAreaDto(2, null, List.of(2)) + )); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isEqualTo(1); + + ArgumentCaptor serviceCentreCaptor = ArgumentCaptor.forClass(ServiceCentre.class); + verify(serviceCentreRepository).save(serviceCentreCaptor.capture()); + assertThat(serviceCentreCaptor.getValue().getServiceAreaIds()).containsExactly(firstServiceAreaId); + assertThat(serviceCentreCaptor.getValue().getCatchmentType()).isNull(); + } + + @Test + void shouldMapServiceAreaIdsWhenCatchmentTypeIsUnknown() { + UUID serviceAreaId = UUID.randomUUID(); + context.getServiceAreaIds().put(1, serviceAreaId); + mockServiceCentreSave(); + + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("Legacy Service Centre"); + serviceCentreDto.setCourtServiceAreas(List.of(new CourtServiceAreaDto(1, "unknown", List.of(1)))); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isEqualTo(1); + + ArgumentCaptor serviceCentreCaptor = ArgumentCaptor.forClass(ServiceCentre.class); + verify(serviceCentreRepository).save(serviceCentreCaptor.capture()); + assertThat(serviceCentreCaptor.getValue().getServiceAreaIds()).containsExactly(serviceAreaId); + assertThat(serviceCentreCaptor.getValue().getCatchmentType()).isNull(); + } + + @Test + void shouldPreserveCatchmentTypeWhenLegacyServiceAreaIdsAreNull() { + mockServiceCentreSave(); + + CourtDto serviceCentreDto = buildMinimalServiceCentreDto("Legacy Service Centre"); + serviceCentreDto.setCourtServiceAreas(List.of(new CourtServiceAreaDto(1, CatchmentType.LOCAL.name(), null))); + + int migrated = helper.migrateServiceCentres(List.of(serviceCentreDto), context); + + assertThat(migrated).isEqualTo(1); + + ArgumentCaptor serviceCentreCaptor = ArgumentCaptor.forClass(ServiceCentre.class); + verify(serviceCentreRepository).save(serviceCentreCaptor.capture()); + assertThat(serviceCentreCaptor.getValue().getServiceAreaIds()).isEmpty(); + assertThat(serviceCentreCaptor.getValue().getCatchmentType()).isEqualTo(CatchmentType.LOCAL); + } + + private CourtDto buildMinimalServiceCentreDto(String name) { + CourtDto serviceCentreDto = new CourtDto(); + serviceCentreDto.setName(name); + serviceCentreDto.setSlug("legacy-service-centre"); + serviceCentreDto.setOpen(true); + serviceCentreDto.setIsServiceCentre(true); + return serviceCentreDto; + } + + private CourtDto buildServiceCentreDto() { + CourtDto serviceCentreDto = new CourtDto(); + serviceCentreDto.setId(100L); + serviceCentreDto.setName("Legacy Service Centre"); + serviceCentreDto.setSlug("legacy-service-centre"); + serviceCentreDto.setOpen(true); + serviceCentreDto.setIsServiceCentre(true); + serviceCentreDto.setCourtServiceAreas(List.of( + new CourtServiceAreaDto(1, CatchmentType.LOCAL.name(), List.of(1)), + new CourtServiceAreaDto(2, CatchmentType.NATIONAL.name(), List.of(2)) + )); + serviceCentreDto.setCourtAreasOfLaw(new CourtAreasOfLawDto("areas", List.of(10))); + return serviceCentreDto; + } + + private void mockServiceCentreSave() { + when(serviceCentreRepository.save(any(ServiceCentre.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + } +} From 08396bbfd3f6999b23166e3ca6b798da930c3bcb Mon Sep 17 00:00:00 2001 From: josh-uk Date: Tue, 23 Jun 2026 16:31:01 +0100 Subject: [PATCH 09/40] Sonar fix --- .../service/ServiceCentreMigrationHelper.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/ServiceCentreMigrationHelper.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/ServiceCentreMigrationHelper.java index ae583f26c..4cf026f9e 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/ServiceCentreMigrationHelper.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/ServiceCentreMigrationHelper.java @@ -132,14 +132,13 @@ private ServiceAreaSelection selectServiceAreas(List dtos, } private static int catchmentPriority(ServiceAreaSelection selection) { - if (selection.catchmentType().isEmpty()) { - return 3; - } - return switch (selection.catchmentType().get()) { - case NATIONAL -> 0; - case REGIONAL -> 1; - case LOCAL -> 2; - }; + return selection.catchmentType() + .map(catchmentType -> switch (catchmentType) { + case NATIONAL -> 0; + case REGIONAL -> 1; + case LOCAL -> 2; + }) + .orElse(3); } private static Optional parseCatchmentType(String value) { From 5fcebe0dae7a9c0670cc9f4e1535d28d0c7efbb6 Mon Sep 17 00:00:00 2001 From: josh-uk Date: Tue, 23 Jun 2026 17:19:56 +0100 Subject: [PATCH 10/40] Some clean up --- .../SearchCourtControllerFunctionalTest.java | 137 +++++++++++++++++- .../api/entities/types/SearchStrategy.java | 2 + .../service/CourtMigrationHelper.java | 1 - .../services/search/SearchCourtService.java | 14 +- .../api/services/search/SearchExecuter.java | 1 + .../api/services/SearchCourtServiceTest.java | 26 +++- .../services/search/SearchExecuterTest.java | 17 +++ 7 files changed, 190 insertions(+), 8 deletions(-) diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchCourtControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchCourtControllerFunctionalTest.java index ca63ac9ec..25336a98c 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchCourtControllerFunctionalTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchCourtControllerFunctionalTest.java @@ -10,11 +10,17 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import uk.gov.hmcts.reform.fact.data.api.dto.CourtWithDistanceResponse; +import uk.gov.hmcts.reform.fact.data.api.dto.SearchResult; import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.CourtAddress; import uk.gov.hmcts.reform.fact.data.api.entities.CourtAreasOfLaw; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAddress; +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreAreasOfLaw; import uk.gov.hmcts.reform.fact.data.api.entities.types.AddressType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchResultType; import uk.gov.hmcts.reform.fact.data.api.entities.types.ServiceAreaType; import uk.gov.hmcts.reform.fact.data.api.models.AreaOfLawSelectionDto; import uk.gov.hmcts.reform.fact.data.api.models.CourtLocalAuthorityDto; @@ -45,6 +51,7 @@ public final class SearchCourtControllerFunctionalTest { private static final String regionId = TestDataHelper.fetchFirstRegionId(http); private static final String STABLE_ENGLAND_POSTCODE = "SW1A 0AA"; private static final String TEST_COURT_PREFIX = "Test Court"; + private static final String TEST_SERVICE_CENTRE_PREFIX = "Test Service Centre Search"; @Test @DisplayName("GET /search/courts/v1/name returns courts matching query") @@ -157,9 +164,6 @@ void shouldReturnNearestCourtsForPostcodeOnlySearch() throws Exception { * - DEFAULT_AOL_DISTANCE (other service areas) * - FAMILY_NON_REGIONAL (family service areas) * - Childcare SPOE special-case (single point of entry search) - * - *

- * TODO: Add service-centre search coverage when that model is introduced. */ @Test @DisplayName("GET /search/courts/v1/postcode returns courts for OTHER type service area using DEFAULT_AOL_DISTANCE") @@ -531,6 +535,77 @@ void shouldReturnCourtsForFamilyServiceAreaUsingFamilyNonRegionalStrategy() thro .doesNotContain(unmappedCourtId); } + @Test + @DisplayName("GET /search/locations/v1/postcode returns regional service centre for FAMILY service area") + void shouldReturnRegionalServiceCentreForFamilyServiceAreaLocationSearch() throws Exception { + final String familyServiceAreaName = "Forced marriage"; + + final Response serviceAreasResponse = http.doGet("/types/v1/service-areas"); + + assertThat(serviceAreasResponse.statusCode()) + .as("Expected 200 OK when fetching service areas") + .isEqualTo(OK.value()); + + final List serviceAreas = mapper.readValue( + serviceAreasResponse.getBody().asString(), + new TypeReference>() {} + ); + + final List matchingServiceAreas = serviceAreas.stream() + .filter(serviceArea -> familyServiceAreaName.equals(serviceArea.getName())) + .toList(); + + assertThat(matchingServiceAreas) + .as("Expected to find exactly one service area named '%s'", familyServiceAreaName) + .hasSize(1); + + final ServiceArea familyServiceArea = matchingServiceAreas.getFirst(); + + assertThat(familyServiceArea.getType()) + .as("Expected service area '%s' to have type FAMILY", familyServiceAreaName) + .isEqualTo(ServiceAreaType.FAMILY); + + assertThat(familyServiceArea.getAreaOfLawId()) + .as("Expected FAMILY service area '%s' to have an area of law ID", familyServiceAreaName) + .isNotNull(); + + final OsDpa dpa = TestDataHelper.fetchFirstDpaForPostcode(http, STABLE_ENGLAND_POSTCODE); + final String serviceCentreName = TEST_SERVICE_CENTRE_PREFIX + " Family Regional"; + final UUID serviceCentreId = createOpenRegionalServiceCentre( + serviceCentreName, + familyServiceArea, + dpa + ); + + final Response searchResponse = http.doGet( + "/search/locations/v1/postcode", + Map.of( + "postcode", STABLE_ENGLAND_POSTCODE, + "serviceArea", familyServiceAreaName, + "action", "DOCUMENTS" + ) + ); + + assertThat(searchResponse.statusCode()) + .as("Expected 200 OK for FAMILY regional service-centre postcode search (serviceArea=%s)", + familyServiceAreaName) + .isEqualTo(OK.value()); + + final List results = mapper.readValue( + searchResponse.getBody().asString(), + new TypeReference>() {} + ); + + assertThat(results) + .as("Expected regional service centre '%s' in location search results", serviceCentreName) + .anySatisfy(result -> { + assertThat(result.getId()).isEqualTo(serviceCentreId); + assertThat(result.getName()).startsWith(serviceCentreName); + assertThat(result.getType()).isEqualTo(SearchResultType.SERVICE_CENTRE); + assertThat(result.getDistance()).isNotNull(); + }); + } + @Test @DisplayName("GET /search/courts/v1/postcode returns nearest SPOE court for childcare arrangements") void shouldReturnNearestSpoeCourtForChildcareArrangementsServiceArea() throws Exception { @@ -679,8 +754,64 @@ private static CourtAddress.CourtAddressBuilder buildVisitUsAddressFromDpa(final .lon(BigDecimal.valueOf(dpa.getLng())); } + private static UUID createOpenRegionalServiceCentre( + final String serviceCentreName, + final ServiceArea serviceArea, + final OsDpa dpa + ) { + final ServiceCentre serviceCentre = new ServiceCentre(); + serviceCentre.setName(TestDataHelper.appendRandomSuffixToCourtName(serviceCentreName)); + serviceCentre.setOpen(true); + serviceCentre.setServiceAreaIds(List.of(serviceArea.getId())); + serviceCentre.setCatchmentType(CatchmentType.REGIONAL); + + final Response createResponse = http.doPost("/service-centres/v1", serviceCentre); + + assertThat(createResponse.statusCode()) + .as("Expected 201 CREATED when creating regional service centre") + .isEqualTo(CREATED.value()); + + final UUID serviceCentreId = UUID.fromString(createResponse.jsonPath().getString("id")); + + final ServiceCentreAddress address = ServiceCentreAddress.builder() + .serviceCentreId(serviceCentreId) + .addressLine1(dpa.getAddress()) + .townCity(dpa.getPostTown()) + .postcode(dpa.getPostcode()) + .addressType(AddressType.VISIT_US) + .lat(BigDecimal.valueOf(dpa.getLat())) + .lon(BigDecimal.valueOf(dpa.getLng())) + .build(); + + final Response addressResponse = http.doPost( + "/service-centres/" + serviceCentreId + "/v1/address", + address + ); + + assertThat(addressResponse.statusCode()) + .as("Expected 201 CREATED when creating address for service centre %s", serviceCentreId) + .isEqualTo(CREATED.value()); + + final ServiceCentreAreasOfLaw areasOfLaw = ServiceCentreAreasOfLaw.builder() + .serviceCentreId(serviceCentreId) + .areasOfLaw(List.of(serviceArea.getAreaOfLawId())) + .build(); + + final Response areasOfLawResponse = http.doPut( + "/service-centres/" + serviceCentreId + "/v1/areas-of-law", + areasOfLaw + ); + + assertThat(areasOfLawResponse.statusCode()) + .as("Expected 201 CREATED when setting areas of law for service centre %s", serviceCentreId) + .isEqualTo(CREATED.value()); + + return serviceCentreId; + } + @AfterAll static void cleanUpTestData() { http.doDelete("/testing-support/courts/name-prefix/" + TEST_COURT_PREFIX); + http.doDelete("/testing-support/service-centres/name-prefix/" + TEST_SERVICE_CENTRE_PREFIX); } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SearchStrategy.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SearchStrategy.java index df0139b8e..254420eeb 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SearchStrategy.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SearchStrategy.java @@ -4,10 +4,12 @@ * Enums for search strategies: * DEFAULT_AOL_DISTANCE is for where individuals are searching for nearest courts. * FAMILY_NON_REGIONAL family area of law search for one court within a local authority. + * FAMILY_REGIONAL family area of law search routed through regional service centres. * CIVIL_POSTCODE_PREFERENCE civil jurisdictional search based on postcode supplied. */ public enum SearchStrategy { DEFAULT_AOL_DISTANCE, FAMILY_NON_REGIONAL, + FAMILY_REGIONAL, CIVIL_POSTCODE_PREFERENCE } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/CourtMigrationHelper.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/CourtMigrationHelper.java index 078b9648e..f6f19bd91 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/CourtMigrationHelper.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/migration/service/CourtMigrationHelper.java @@ -106,7 +106,6 @@ int migrateCourts(List courts, MigrationContext context) { int total = 0; for (CourtDto dto : courts) { if (Boolean.TRUE.equals(dto.getIsServiceCentre())) { - //TODO: Only migrate courts. LOG.info("Skipping service centre {}", dto.getSlug()); continue; } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchCourtService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchCourtService.java index d6fa7b5b1..cea17481e 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchCourtService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchCourtService.java @@ -11,6 +11,7 @@ import uk.gov.hmcts.reform.fact.data.api.os.OsDpa; import uk.gov.hmcts.reform.fact.data.api.os.OsLocationData; import uk.gov.hmcts.reform.fact.data.api.repositories.LocalAuthorityTypeRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; import uk.gov.hmcts.reform.fact.data.api.services.CourtAddressService; import uk.gov.hmcts.reform.fact.data.api.services.CourtSinglePointOfEntryService; import uk.gov.hmcts.reform.fact.data.api.services.OsService; @@ -21,10 +22,12 @@ import java.util.Optional; import static uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentMethod.LOCAL_AUTHORITY; +import static uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType.REGIONAL; import static uk.gov.hmcts.reform.fact.data.api.entities.types.SearchAction.NEAREST; import static uk.gov.hmcts.reform.fact.data.api.entities.types.SearchStrategy.CIVIL_POSTCODE_PREFERENCE; import static uk.gov.hmcts.reform.fact.data.api.entities.types.SearchStrategy.DEFAULT_AOL_DISTANCE; import static uk.gov.hmcts.reform.fact.data.api.entities.types.SearchStrategy.FAMILY_NON_REGIONAL; +import static uk.gov.hmcts.reform.fact.data.api.entities.types.SearchStrategy.FAMILY_REGIONAL; @Service @Slf4j @@ -36,6 +39,7 @@ public class SearchCourtService { private final CourtAddressService courtAddressService; private final SearchExecuter searchExecuter; private final LocalAuthorityTypeRepository localAuthorityTypeRepository; + private final ServiceCentreRepository serviceCentreRepository; private static final String CHILDCARE_SERVICE_AREA = "Childcare arrangements if you separate from your partner"; private static final String CHILDCARE_AOL = "Children"; @@ -44,13 +48,15 @@ public SearchCourtService(OsService osService, CourtSinglePointOfEntryService courtSinglePointOfEntryService, CourtAddressService courtAddressService, SearchExecuter searchExecuter, - LocalAuthorityTypeRepository localAuthorityTypeRepository) { + LocalAuthorityTypeRepository localAuthorityTypeRepository, + ServiceCentreRepository serviceCentreRepository) { this.osService = osService; this.serviceAreaService = serviceAreaService; this.courtSinglePointOfEntryService = courtSinglePointOfEntryService; this.courtAddressService = courtAddressService; this.searchExecuter = searchExecuter; this.localAuthorityTypeRepository = localAuthorityTypeRepository; + this.serviceCentreRepository = serviceCentreRepository; } /** @@ -176,8 +182,10 @@ public SearchStrategy selectSearchStrategy(SearchAction action, private SearchStrategy getFamilyStrategy(SearchAction action, ServiceArea serviceArea, String authorityName) { if (LOCAL_AUTHORITY.equals(serviceArea.getCatchmentMethod()) && !authorityName.isEmpty()) { - //TODO: Restore regional service-centre routing when service-centres are reintroduced. - return FAMILY_NON_REGIONAL; + return serviceCentreRepository.existsByServiceAreaIdAndCatchmentTypeIn( + serviceArea.getId(), + List.of(REGIONAL) + ) ? FAMILY_REGIONAL : FAMILY_NON_REGIONAL; } log.debug("Setting search strategy to default for {}, {}, {}", action, serviceArea, authorityName); diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuter.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuter.java index af4a2a16f..44b65a9d6 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuter.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuter.java @@ -55,6 +55,7 @@ public List executeSearchStrategy(OsLocationData osLocationDa limit, aolId ); + case FAMILY_REGIONAL -> List.of(); case FAMILY_NON_REGIONAL -> { List results = executeFamilyNonRegionalSearchStrategy( osLocationData, diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/SearchCourtServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/SearchCourtServiceTest.java index 0a0ccd5be..4f43b9a06 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/SearchCourtServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/SearchCourtServiceTest.java @@ -11,6 +11,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.LocalAuthorityType; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceArea; import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentMethod; +import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchAction; import uk.gov.hmcts.reform.fact.data.api.entities.types.SearchStrategy; import uk.gov.hmcts.reform.fact.data.api.entities.types.ServiceAreaType; @@ -20,6 +21,7 @@ import uk.gov.hmcts.reform.fact.data.api.os.OsLocationData; import uk.gov.hmcts.reform.fact.data.api.os.OsResult; import uk.gov.hmcts.reform.fact.data.api.repositories.LocalAuthorityTypeRepository; +import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; import uk.gov.hmcts.reform.fact.data.api.services.search.SearchCourtService; import uk.gov.hmcts.reform.fact.data.api.services.search.SearchExecuter; @@ -62,6 +64,9 @@ class SearchCourtServiceTest { @Mock LocalAuthorityTypeRepository localAuthorityTypeRepository; + @Mock + private ServiceCentreRepository serviceCentreRepository; + @InjectMocks private SearchCourtService searchCourtService; @@ -172,7 +177,26 @@ void selectSearchStrategyShouldReturnCivilPreference() { } @Test - void selectSearchStrategyShouldReturnFamilyNonRegionalForLocalAuthorityCatchment() { + void selectSearchStrategyShouldReturnFamilyRegionalWhenRegionalServiceCentreExists() { + ServiceArea area = serviceAreaWithType(ServiceAreaType.FAMILY); + area.setCatchmentMethod(CatchmentMethod.LOCAL_AUTHORITY); + + when(serviceCentreRepository.existsByServiceAreaIdAndCatchmentTypeIn( + area.getId(), + List.of(CatchmentType.REGIONAL) + )).thenReturn(true); + + SearchStrategy strategy = searchCourtService.selectSearchStrategy( + SearchAction.DOCUMENTS, + "Authority", + area + ); + + assertThat(strategy).isEqualTo(SearchStrategy.FAMILY_REGIONAL); + } + + @Test + void selectSearchStrategyShouldReturnFamilyNonRegionalWhenRegionalServiceCentreDoesNotExist() { ServiceArea area = serviceAreaWithType(ServiceAreaType.FAMILY); area.setCatchmentMethod(CatchmentMethod.LOCAL_AUTHORITY); diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuterTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuterTest.java index bd21c2487..042024e72 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuterTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchExecuterTest.java @@ -121,6 +121,23 @@ void executeSearchStrategyShouldFallbackToNearestWhenCivilTieredResultsEmpty() { verify(courtAddressRepository).findNearestByAreaOfLaw(51.5, -0.1, area.getAreaOfLawId(), 5); } + @Test + void executeSearchStrategyShouldReturnNoCourtResultsForFamilyRegionalServiceCentreRouting() { + ServiceArea area = serviceArea(ServiceAreaType.FAMILY); + OsLocationData locationData = osLocationData("Authority", "SW1A 1AA"); + + List response = searchExecuter.executeSearchStrategy( + locationData, + area, + SearchStrategy.FAMILY_REGIONAL, + SearchAction.DOCUMENTS, + 10 + ); + + assertThat(response).isEmpty(); + verify(courtAddressRepository, never()).findNearestByAreaOfLaw(anyDouble(), anyDouble(), any(), anyInt()); + } + @Test void executeSearchStrategyShouldReturnFamilyNonRegionalByLocalAuthorityWhenFound() { ServiceArea area = serviceArea(ServiceAreaType.FAMILY); From 3792214c84ab089de3060803623133cbe788e79f Mon Sep 17 00:00:00 2001 From: josh-uk Date: Tue, 23 Jun 2026 17:25:32 +0100 Subject: [PATCH 11/40] Remove unused param --- .../data/api/services/search/SearchServiceCentreService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceCentreService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceCentreService.java index 8bd120f3a..c1f0bf468 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceCentreService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/search/SearchServiceCentreService.java @@ -47,12 +47,11 @@ public List getServiceCentresBySearchParameters(Strin if (serviceAreaEmpty) { return List.of(); } - return searchWithServiceArea(postcode, serviceArea, action, limit); + return searchWithServiceArea(postcode, serviceArea, limit); } private List searchWithServiceArea(String postcode, String serviceArea, - SearchAction action, Integer limit) { OsLocationData osLocationData = osService.getOsLonLatDistrictByPartial(postcode); ServiceArea serviceAreaFound = serviceAreaService.getServiceAreaByName(serviceArea); From 97afa5b30f67a66b52f3bc7b7ee2a471ca7f3d22 Mon Sep 17 00:00:00 2001 From: josh-uk Date: Tue, 23 Jun 2026 17:35:51 +0100 Subject: [PATCH 12/40] log fixes --- build.gradle | 4 +++- src/integrationTest/resources/META-INF/aop.xml | 6 ------ 2 files changed, 3 insertions(+), 7 deletions(-) delete mode 100644 src/integrationTest/resources/META-INF/aop.xml diff --git a/build.gradle b/build.gradle index b893c0206..6730969a9 100644 --- a/build.gradle +++ b/build.gradle @@ -83,7 +83,6 @@ tasks.withType(JavaExec).configureEach { tasks.withType(Test).configureEach { useJUnitPlatform() failFast = true - systemProperty 'org.aspectj.weaver.Dump.exception', 'false' testLogging { exceptionFormat = 'full' @@ -156,6 +155,9 @@ dependencyCheck { allure { version = '2.34.1' + adapter { + aspectjWeaver.set(false) + } } repositories { diff --git a/src/integrationTest/resources/META-INF/aop.xml b/src/integrationTest/resources/META-INF/aop.xml deleted file mode 100644 index c9136b90d..000000000 --- a/src/integrationTest/resources/META-INF/aop.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - From 0b74ab85b380a5225701769de134a7772c853d17 Mon Sep 17 00:00:00 2001 From: josh-uk Date: Tue, 23 Jun 2026 17:52:28 +0100 Subject: [PATCH 13/40] Fix functional test --- .../search/SearchCourtControllerFunctionalTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchCourtControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchCourtControllerFunctionalTest.java index 25336a98c..71b9ae57a 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchCourtControllerFunctionalTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/search/SearchCourtControllerFunctionalTest.java @@ -772,6 +772,13 @@ private static UUID createOpenRegionalServiceCentre( .isEqualTo(CREATED.value()); final UUID serviceCentreId = UUID.fromString(createResponse.jsonPath().getString("id")); + serviceCentre.setOpen(true); + + final Response updateResponse = http.doPut("/service-centres/" + serviceCentreId + "/v1", serviceCentre); + + assertThat(updateResponse.statusCode()) + .as("Expected 200 OK when updating regional service centre %s to open", serviceCentreId) + .isEqualTo(OK.value()); final ServiceCentreAddress address = ServiceCentreAddress.builder() .serviceCentreId(serviceCentreId) From 6910e1e5626b05a5f83deb801c32c72976c2da40 Mon Sep 17 00:00:00 2001 From: josh-uk Date: Wed, 24 Jun 2026 07:25:03 +0100 Subject: [PATCH 14/40] unique slug in db --- .../V1.35__add_unique_constraint_to_service_centre_slug.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/main/resources/db/migration/V1.35__add_unique_constraint_to_service_centre_slug.sql diff --git a/src/main/resources/db/migration/V1.35__add_unique_constraint_to_service_centre_slug.sql b/src/main/resources/db/migration/V1.35__add_unique_constraint_to_service_centre_slug.sql new file mode 100644 index 000000000..a273650ef --- /dev/null +++ b/src/main/resources/db/migration/V1.35__add_unique_constraint_to_service_centre_slug.sql @@ -0,0 +1,2 @@ +ALTER TABLE service_centre + ADD CONSTRAINT service_centre_slug_unique UNIQUE (slug); From 06e332b66cb95fcf52492eca01101ca9bd95d635 Mon Sep 17 00:00:00 2001 From: josh-uk Date: Wed, 24 Jun 2026 15:19:04 +0100 Subject: [PATCH 15/40] Drop court ID from audit --- .../AuditControllerFunctionalTest.java | 8 +++++++- .../api/controllers/AuditControllerTest.java | 8 ++++++-- .../reform/fact/data/api/entities/Audit.java | 17 ----------------- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuditControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuditControllerFunctionalTest.java index 149418f41..4bec96464 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuditControllerFunctionalTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuditControllerFunctionalTest.java @@ -71,12 +71,18 @@ void shouldReturnExpectedAuditActionsForCourtAndAddressFlow() { final Response auditsResponse = http.doGet(auditsPath); AssertionHelper.assertStatus(auditsResponse, OK); - final List auditedCourtIds = auditsResponse.jsonPath().getList("content.court.id", String.class); + final List auditedCourtIds = auditsResponse.jsonPath().getList("content.subjectId", String.class); assertThat(auditedCourtIds) .as("All audit records should be filtered to the requested court ID") .isNotEmpty() .allMatch(courtId.toString()::equals); + final List auditedSubjectTypes = auditsResponse.jsonPath().getList("content.subjectType", String.class); + assertThat(auditedSubjectTypes) + .as("All audit records should be court subject records") + .isNotEmpty() + .allMatch("COURT"::equals); + final List courtActionTypes = extractActionTypesForEntity(auditsResponse, "Court"); assertThat(courtActionTypes) .as("Expected at least one Court INSERT audit entry") diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index 0e2fb6c0d..f05102580 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -166,7 +166,10 @@ void getFilteredAndPaginatedAuditsReturnsFilteredResults() throws Exception { .andExpect(jsonPath("$.content").isArray()) .andExpect(jsonPath("$.content.length()").value(7)) .andExpect(jsonPath("$.content[0]").exists()) - .andExpect(jsonPath("$.content[*].court.id").value(allElementsEqual(court1.getCourtId().toString()))) + .andExpect(jsonPath("$.content[*].subjectId").value(allElementsEqual(court1.getId().toString()))) + .andExpect(jsonPath("$.content[*].subjectType").value(allElementsEqual("COURT"))) + .andExpect(jsonPath("$.content[0].court").doesNotExist()) + .andExpect(jsonPath("$.content[0].courtId").doesNotExist()) .andExpect(jsonPath("$.page.number").value(0)) .andExpect(jsonPath("$.page.totalPages").value(1)) .andExpect(jsonPath("$.page.totalElements").value(7)); @@ -181,7 +184,8 @@ void getFilteredAndPaginatedAuditsReturnsFilteredResults() throws Exception { .andExpect(jsonPath("$.content").isArray()) .andExpect(jsonPath("$.content.length()").value(3)) .andExpect(jsonPath("$.content[0]").exists()) - .andExpect(jsonPath("$.content[*].court.id").value(allElementsEqual(court2.getCourtId().toString()))) + .andExpect(jsonPath("$.content[*].subjectId").value(allElementsEqual(court2.getId().toString()))) + .andExpect(jsonPath("$.content[*].subjectType").value(allElementsEqual("COURT"))) .andExpect(jsonPath("$.page.number").value(0)) .andExpect(jsonPath("$.page.totalPages").value(1)) .andExpect(jsonPath("$.page.totalElements").value(3)); diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java index abbfd73ee..d4f60fc9e 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java @@ -8,7 +8,6 @@ import java.util.List; import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -57,11 +56,6 @@ public class Audit { @Enumerated(EnumType.STRING) private AuditSubjectType subjectType; - @Schema(description = "The associated Court") - @ManyToOne - @JoinColumn(name = "subject_id", insertable = false, updatable = false) - private Court court; - @NotNull @Column(name = "user_id") private UUID userId; @@ -89,15 +83,4 @@ public class Audit { @Setter(AccessLevel.NONE) private ZonedDateTime createdAt; - @JsonProperty("courtId") - public UUID getCourtId() { - return AuditSubjectType.COURT.equals(subjectType) ? subjectId : null; - } - - @JsonProperty("courtId") - public void setCourtId(UUID courtId) { - this.subjectId = courtId; - this.subjectType = AuditSubjectType.COURT; - } - } From 328181aa67af8cb9d07fc51d12a384025950071a Mon Sep 17 00:00:00 2001 From: josh-uk Date: Thu, 25 Jun 2026 09:14:23 +0100 Subject: [PATCH 16/40] Postgres 18 update - part 1 --- charts/fact-data-api/values.aat.template.yaml | 4 ++++ charts/fact-data-api/values.preview.template.yaml | 4 ++++ docker-compose.yml | 2 +- infrastructure/main.tf | 4 ++-- src/integrationTest/resources/application-test.yaml | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/charts/fact-data-api/values.aat.template.yaml b/charts/fact-data-api/values.aat.template.yaml index 4f835bceb..eb0f740af 100644 --- a/charts/fact-data-api/values.aat.template.yaml +++ b/charts/fact-data-api/values.aat.template.yaml @@ -13,6 +13,10 @@ java: TESTING_SUPPORT_ENABLE_API: true postgresql: enabled: true + image: + registry: hmctsprod.azurecr.io + repository: imported/postgres + tag: '18-alpine' primary: initdb: scripts: diff --git a/charts/fact-data-api/values.preview.template.yaml b/charts/fact-data-api/values.preview.template.yaml index 4f835bceb..eb0f740af 100644 --- a/charts/fact-data-api/values.preview.template.yaml +++ b/charts/fact-data-api/values.preview.template.yaml @@ -13,6 +13,10 @@ java: TESTING_SUPPORT_ENABLE_API: true postgresql: enabled: true + image: + registry: hmctsprod.azurecr.io + repository: imported/postgres + tag: '18-alpine' primary: initdb: scripts: diff --git a/docker-compose.yml b/docker-compose.yml index 4802e1e40..714d3b0d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: - TESTING_SUPPORT_ENABLE_API=${TESTING_SUPPORT_ENABLE_API} - OS_KEY=${OS_KEY} fact-database: - image: hmctsprod.azurecr.io/imported/postgres:16-alpine + image: hmctsprod.azurecr.io/imported/postgres:18-alpine environment: POSTGRES_DB: fact POSTGRES_USER: fact diff --git a/infrastructure/main.tf b/infrastructure/main.tf index 714ac0435..c6bbe6a49 100644 --- a/infrastructure/main.tf +++ b/infrastructure/main.tf @@ -20,7 +20,7 @@ data "azurerm_key_vault" "fact_kv" { resource_group_name = data.azurerm_resource_group.fact_rg.name } -# Postgres Database +# Postgres Database module "postgresql" { providers = { azurerm.postgres_network = azurerm.postgres_network @@ -38,7 +38,7 @@ module "postgresql" { ] common_tags = var.common_tags business_area = "cft" - pgsql_version = "16" + pgsql_version = "18" admin_user_object_id = var.jenkins_AAD_objectId pgsql_server_configuration = [{ name = "azure.extensions" diff --git a/src/integrationTest/resources/application-test.yaml b/src/integrationTest/resources/application-test.yaml index 68e8e1d6f..db7818511 100644 --- a/src/integrationTest/resources/application-test.yaml +++ b/src/integrationTest/resources/application-test.yaml @@ -1,7 +1,7 @@ spring: datasource: driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver - url: jdbc:tc:postgresql:16:///fact + url: jdbc:tc:postgresql:18:///fact cloud: azure: storage: From 7116693a767ca11c77ae40eae0f51af446a9be51 Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Thu, 18 Jun 2026 08:44:05 +0100 Subject: [PATCH 17/40] Add court name and id list retrieval endpoint --- .../api/controllers/CourtControllerTest.java | 35 +++++++++++++++++++ .../data/api/controllers/CourtController.java | 14 ++++++++ .../api/repositories/CourtRepository.java | 16 +++++++++ .../fact/data/api/services/CourtService.java | 9 +++++ .../api/controllers/CourtControllerTest.java | 29 +++++++++++++++ 5 files changed, 103 insertions(+) diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java index 6c5cab536..acdfcb81d 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java @@ -21,6 +21,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.validation.ValidationConstants; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.repositories.CourtRepository; import uk.gov.hmcts.reform.fact.data.api.services.CourtDetailsViewService; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; @@ -534,6 +535,40 @@ void getAllCourtsReturns200() throws Exception { Assertions.assertThat(responseList).isEqualTo(courtDetailsList); } + @Test + @DisplayName("GET /courts/nameoptions/v1 returns 200 with court name and id mappings") + void getCourtNameIdMapReturnsOkWithResults() throws Exception { + CourtRepository.NameAndId first = org.mockito.Mockito.mock(CourtRepository.NameAndId.class); + CourtRepository.NameAndId second = org.mockito.Mockito.mock(CourtRepository.NameAndId.class); + + UUID firstId = UUID.randomUUID(); + UUID secondId = UUID.randomUUID(); + + when(first.id()).thenReturn(firstId); + when(first.name()).thenReturn("Test Court A"); + when(second.id()).thenReturn(secondId); + when(second.name()).thenReturn("Test Court B"); + + when(courtService.getAllCourtNameAndIds()).thenReturn(List.of(first, second)); + + mockMvc.perform(get("/courts/nameoptions/v1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(firstId.toString())) + .andExpect(jsonPath("$[0].name").value("Test Court A")) + .andExpect(jsonPath("$[1].id").value(secondId.toString())) + .andExpect(jsonPath("$[1].name").value("Test Court B")); + } + + @Test + @DisplayName("GET /courts/nameoptions/v1 returns 200 with empty list when no mappings exist") + void getCourtNameIdMapReturnsOkWithEmptyList() throws Exception { + when(courtService.getAllCourtNameAndIds()).thenReturn(List.of()); + + mockMvc.perform(get("/courts/nameoptions/v1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$").isEmpty()); + } private Court buildCourt(UUID id) { return Court.builder() diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtController.java index 96821cf8e..dbc92287e 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtController.java @@ -4,6 +4,7 @@ import jakarta.validation.constraints.NotEmpty; import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.CourtDetails; +import uk.gov.hmcts.reform.fact.data.api.repositories.CourtRepository; import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; import uk.gov.hmcts.reform.fact.data.api.services.CourtDetailsViewService; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; @@ -247,4 +248,17 @@ public ResponseEntity handleCaTHCourtDeletion( courtService.handleCathCourtDeletion(mrdId); return ResponseEntity.noContent().build(); } + + @GetMapping("/nameoptions/v1") + @Operation( + summary = "Retrieve the complete set of name->id value pairs for all courts", + description = "Fetches a list of all court names with their corresponding ids." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved list of court name->id mappings") + }) + @PreAuthorize("@authService.isAdmin()") // only want admin using this endpoint + public ResponseEntity> getCourtNameIdMap() { + return ResponseEntity.ok(this.courtService.getAllCourtNameAndIds()); + } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtRepository.java index a0a00604a..f2061509c 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtRepository.java @@ -18,6 +18,8 @@ public interface CourtRepository extends JpaRepository { record NameAndSlug(String name, String slug) {} + record NameAndId(String name, UUID id) {} + /** * Finds courts by region ids and partial name, case-insensitive. * @@ -139,4 +141,18 @@ ILIKE REPLACE(CONCAT('%', :query, '%'), ' ', '') */ Optional findNameAndSlugById(UUID id); + /** + * Retrieve all court names mapped to their IDs. + * + * @return the {@link List} of {@link NameAndId} objects + */ + @Query( + value = """ + SELECT c.name, c.id + FROM court c + ORDER BY c.name + """, + nativeQuery = true + ) + List findAllNameAndId(); } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtService.java index 3e69b8afd..311aa13bc 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtService.java @@ -235,6 +235,15 @@ public List getAllCourtDetails() { return courtDetailsRepository.findAll(); } + /** + * get all court names mapped to their ids. + * + * @return a {@link List} of court names with their ids. + */ + public List getAllCourtNameAndIds() { + return courtRepository.findAllNameAndId(); + } + /** * Marks courts received from CaTH as open and records which MRD IDs could not be matched. * diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java index 801018bb6..04fd10220 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java @@ -14,6 +14,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.CourtDetails; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.repositories.CourtRepository; import uk.gov.hmcts.reform.fact.data.api.services.CourtDetailsViewService; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; @@ -291,6 +292,34 @@ void handleCaTHCourtDeletionReturns204() { verify(courtService).handleCathCourtDeletion("MRD123"); } + @Test + void getCourtNameIdMapReturns200WithAllNameAndIdPairs() { + CourtRepository.NameAndId first = org.mockito.Mockito.mock(CourtRepository.NameAndId.class); + CourtRepository.NameAndId second = org.mockito.Mockito.mock(CourtRepository.NameAndId.class); + List nameAndIds = List.of(first, second); + + when(courtService.getAllCourtNameAndIds()).thenReturn(nameAndIds); + + ResponseEntity> response = courtController.getCourtNameIdMap(); + + assertThat(response.getStatusCode()).as(RESPONSE_STATUS_MESSAGE).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).as(RESPONSE_BODY_MESSAGE).isEqualTo(nameAndIds); + verify(courtService).getAllCourtNameAndIds(); + } + + @Test + void getCourtNameIdMapReturns200WithEmptyListWhenNoCourtsExist() { + List nameAndIds = List.of(); + + when(courtService.getAllCourtNameAndIds()).thenReturn(nameAndIds); + + ResponseEntity> response = courtController.getCourtNameIdMap(); + + assertThat(response.getStatusCode()).as(RESPONSE_STATUS_MESSAGE).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).as(RESPONSE_BODY_MESSAGE).isEqualTo(nameAndIds); + verify(courtService).getAllCourtNameAndIds(); + } + private Court createCourt() { Court court = new Court(); populateCourt(court); From c90e3a1dffe5f79167a851d228a164f438745b0a Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Thu, 18 Jun 2026 08:44:49 +0100 Subject: [PATCH 18/40] Change UUID for test user to be a valid v4 UUID variant --- .../reform/fact/data/api/services/TestingSupportService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java index 2436f033e..6fd8ead2c 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java @@ -90,7 +90,7 @@ public class TestingSupportService { // used to generate alphanumeric strings private static final char[] ALPHA_NUMERICS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray(); private static final String TESTING_SUPPORT_USER_EMAIL = "testing-support-not-a-real-user@justice.gov.uk"; - private static final UUID TESTING_SUPPORT_USER_SSO_ID = UUID.fromString("00000000-0000-0000-0000-000000000002"); + private static final UUID TESTING_SUPPORT_USER_SSO_ID = UUID.fromString("00000000-0000-4000-a000-000000000002"); // ------------------------------------------------------------------------ // static data caches - we load these once and then reuse for all generated courts From 0025abb0b987de0f4f742ea048d05c9d4e9971ce Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Thu, 18 Jun 2026 09:04:35 +0100 Subject: [PATCH 19/40] Add missing service tests (coverage) --- .../data/api/services/CourtServiceTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceTest.java index d16d1dbc0..b172bf3bb 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceTest.java @@ -822,4 +822,28 @@ void getAllCourtDetailsReturnsCourtDetailsListWhenFound() { .isNotNull() .isEqualTo(courtDetailsList); } + + @Test + void getAllCourtNameAndIdsReturnsRepositoryResults() { + CourtRepository.NameAndId first = new CourtRepository.NameAndId("Court A", UUID.randomUUID()); + CourtRepository.NameAndId second = new CourtRepository.NameAndId("Court B", UUID.randomUUID()); + List expected = List.of(first, second); + + when(courtRepository.findAllNameAndId()).thenReturn(expected); + + List result = courtService.getAllCourtNameAndIds(); + + assertThat(result).isEqualTo(expected); + verify(courtRepository).findAllNameAndId(); + } + + @Test + void getAllCourtNameAndIdsReturnsEmptyListWhenNoCourtsExist() { + when(courtRepository.findAllNameAndId()).thenReturn(Collections.emptyList()); + + List result = courtService.getAllCourtNameAndIds(); + + assertThat(result).isEmpty(); + verify(courtRepository).findAllNameAndId(); + } } From f161a3014900574c0ad73ba04d192eba0ea65f83 Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Fri, 19 Jun 2026 11:50:02 +0100 Subject: [PATCH 20/40] Switch to subject-based retrieval of audit name options --- .../api/controllers/AuditControllerTest.java | 14 ++++++++ .../api/controllers/CourtControllerTest.java | 36 ------------------- .../data/api/controllers/AuditController.java | 17 +++++++++ .../data/api/controllers/CourtController.java | 14 -------- .../data/api/entities/types/AuditSubject.java | 9 +++++ .../data/api/entities/types/NameAndId.java | 11 ++++++ .../api/repositories/CourtRepository.java | 3 +- .../fact/data/api/services/AuditService.java | 22 ++++++++++++ .../fact/data/api/services/CourtService.java | 3 +- .../api/controllers/AuditControllerTest.java | 20 +++++++++++ .../api/controllers/CourtControllerTest.java | 29 --------------- .../data/api/services/AuditServiceTest.java | 32 +++++++++++++++++ .../data/api/services/CourtServiceTest.java | 11 +++--- 13 files changed, 134 insertions(+), 87 deletions(-) create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubject.java create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/NameAndId.java diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index f05102580..c74dcd531 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -351,6 +351,20 @@ void deleteAuditsRemovesExpiredAuditsAndReturns204() throws Exception { mvc.perform(delete("/audits/v1")).andExpect(status().isNoContent()); } + @Test + @DisplayName("GET /audits/subjectoptions/v1 returns subject options map") + void getSubjectNameAndIdMapReturnsResults() throws Exception { + // Seed at least one court so COURT options are present + createTestCourts(1); + + mvc.perform(get("/audits/subjectoptions/v1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.COURT").isArray()) + .andExpect(jsonPath("$.SERVICE_CENTRE").isArray()) + .andExpect(jsonPath("$.COURT[0].name").exists()) + .andExpect(jsonPath("$.COURT[0].id").exists()); + } + private List createTestCourts(int num) { List results = new ArrayList<>(); for (int i = 0; i < num; i++) { diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java index acdfcb81d..fc0785e3e 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java @@ -21,7 +21,6 @@ import uk.gov.hmcts.reform.fact.data.api.entities.validation.ValidationConstants; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtRepository; import uk.gov.hmcts.reform.fact.data.api.services.CourtDetailsViewService; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; @@ -535,41 +534,6 @@ void getAllCourtsReturns200() throws Exception { Assertions.assertThat(responseList).isEqualTo(courtDetailsList); } - @Test - @DisplayName("GET /courts/nameoptions/v1 returns 200 with court name and id mappings") - void getCourtNameIdMapReturnsOkWithResults() throws Exception { - CourtRepository.NameAndId first = org.mockito.Mockito.mock(CourtRepository.NameAndId.class); - CourtRepository.NameAndId second = org.mockito.Mockito.mock(CourtRepository.NameAndId.class); - - UUID firstId = UUID.randomUUID(); - UUID secondId = UUID.randomUUID(); - - when(first.id()).thenReturn(firstId); - when(first.name()).thenReturn("Test Court A"); - when(second.id()).thenReturn(secondId); - when(second.name()).thenReturn("Test Court B"); - - when(courtService.getAllCourtNameAndIds()).thenReturn(List.of(first, second)); - - mockMvc.perform(get("/courts/nameoptions/v1")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id").value(firstId.toString())) - .andExpect(jsonPath("$[0].name").value("Test Court A")) - .andExpect(jsonPath("$[1].id").value(secondId.toString())) - .andExpect(jsonPath("$[1].name").value("Test Court B")); - } - - @Test - @DisplayName("GET /courts/nameoptions/v1 returns 200 with empty list when no mappings exist") - void getCourtNameIdMapReturnsOkWithEmptyList() throws Exception { - when(courtService.getAllCourtNameAndIds()).thenReturn(List.of()); - - mockMvc.perform(get("/courts/nameoptions/v1")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()) - .andExpect(jsonPath("$").isEmpty()); - } - private Court buildCourt(UUID id) { return Court.builder() .id(id) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java index b951e04b6..4d113638e 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java @@ -1,6 +1,8 @@ package uk.gov.hmcts.reform.fact.data.api.controllers; import uk.gov.hmcts.reform.fact.data.api.entities.Audit; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubject; +import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidDateRangeException; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; @@ -8,6 +10,8 @@ import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidUUID; import java.time.LocalDate; +import java.util.List; +import java.util.Map; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -19,6 +23,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -100,4 +105,16 @@ public ResponseEntity removeExpiredAuditEntries() { return ResponseEntity.noContent().build(); } + @GetMapping("/subjectoptions/v1") + @Operation( + summary = "Retrieve the complete set of name->id value pairs for all supported audit subjects", + description = "Fetches a Map of all subject names with their corresponding ids, mapped to their subject type" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved map of subject-> name+id pairs") + }) + @PreAuthorize("@authService.isAdmin()") // only want admin using this endpoint + public ResponseEntity>> getSubjectNameAndIdMap() { + return ResponseEntity.ok(this.auditService.getSubjectNameAndIdMap()); + } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtController.java index dbc92287e..96821cf8e 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtController.java @@ -4,7 +4,6 @@ import jakarta.validation.constraints.NotEmpty; import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.CourtDetails; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtRepository; import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; import uk.gov.hmcts.reform.fact.data.api.services.CourtDetailsViewService; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; @@ -248,17 +247,4 @@ public ResponseEntity handleCaTHCourtDeletion( courtService.handleCathCourtDeletion(mrdId); return ResponseEntity.noContent().build(); } - - @GetMapping("/nameoptions/v1") - @Operation( - summary = "Retrieve the complete set of name->id value pairs for all courts", - description = "Fetches a list of all court names with their corresponding ids." - ) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved list of court name->id mappings") - }) - @PreAuthorize("@authService.isAdmin()") // only want admin using this endpoint - public ResponseEntity> getCourtNameIdMap() { - return ResponseEntity.ok(this.courtService.getAllCourtNameAndIds()); - } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubject.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubject.java new file mode 100644 index 000000000..09dde99bf --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubject.java @@ -0,0 +1,9 @@ +package uk.gov.hmcts.reform.fact.data.api.entities.types; + +/** + * Subject types that are applicable for auditing. + */ +public enum AuditSubject { + COURT, + SERVICE_CENTRE +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/NameAndId.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/NameAndId.java new file mode 100644 index 000000000..ff3084e5d --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/NameAndId.java @@ -0,0 +1,11 @@ +package uk.gov.hmcts.reform.fact.data.api.entities.types; + +import java.util.UUID; + +/** + * re-usable Name and Id pair object for use in repository results. + * + * @param name the entity Name + * @param id the entity Id + */ +public record NameAndId(String name, UUID id) {} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtRepository.java index f2061509c..2c66ffe21 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import uk.gov.hmcts.reform.fact.data.api.entities.Court; +import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import java.util.List; import java.util.Optional; @@ -18,8 +19,6 @@ public interface CourtRepository extends JpaRepository { record NameAndSlug(String name, String slug) {} - record NameAndId(String name, UUID id) {} - /** * Finds courts by region ids and partial name, case-insensitive. * diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java index 6137a7dbc..34a7e7ce1 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java @@ -3,12 +3,15 @@ import uk.gov.hmcts.reform.fact.data.api.config.properties.AuditConfigurationProperties; import uk.gov.hmcts.reform.fact.data.api.entities.Audit; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.repositories.AuditRepository; import java.time.LocalDate; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -26,6 +29,7 @@ public class AuditService { private final AuditRepository auditRepository; + private final CourtService courtService; private final AuditConfigurationProperties auditConfiguration; /** @@ -132,4 +136,22 @@ private static SubjectFilter resolveSubjectFilter(String courtId, String service } private record SubjectFilter(UUID subjectId, AuditSubjectType subjectType) {} + + /** + * Retrieves the complete set of supported audit subjects with their corresponding name and id pairs. + * + * @return a {@link Map} of {@link AuditSubjectType} to a list of {@link NameAndId} pairs + * representing the names and ids of the subjects. + */ + public Map> getSubjectNameAndIdMap() { + return Map.of( + AuditSubjectType.COURT, courtService.getAllCourtNameAndIds(), + // TODO: replace with real service centre once implemented + AuditSubjectType.SERVICE_CENTRE, List.of( + new NameAndId( + "Service Center", + UUID.fromString("00000000-0000-0000-0000-000000000000") + )) + ); + } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtService.java index 311aa13bc..2fbe81c3a 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtService.java @@ -6,6 +6,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.CourtDetails; import uk.gov.hmcts.reform.fact.data.api.entities.Region; +import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtDetailsRepository; @@ -240,7 +241,7 @@ public List getAllCourtDetails() { * * @return a {@link List} of court names with their ids. */ - public List getAllCourtNameAndIds() { + public List getAllCourtNameAndIds() { return courtRepository.findAllNameAndId(); } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index 4b7883c7a..46d0d0b47 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -5,12 +5,14 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import uk.gov.hmcts.reform.fact.data.api.entities.Audit; import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Change; +import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidDateRangeException; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.services.AuditService; @@ -18,6 +20,7 @@ import java.time.LocalDate; import java.time.ZonedDateTime; import java.util.List; +import java.util.Map; import java.util.UUID; import org.junit.jupiter.api.Test; @@ -113,6 +116,23 @@ void getFilteredAndPaginatedAuditsThrowsInvalidParameterCombinationForBothSubjec ); } + @Test + void getSubjectNameAndIdMapReturns200() { + Map> subjectMap = Map.of( + AuditSubjectType.COURT, List.of( + new NameAndId("Exeter Law Courts", UUID.randomUUID()) + ) + ); + + when(auditService.getSubjectNameAndIdMap()).thenReturn(subjectMap); + + ResponseEntity>> response = auditController.getSubjectNameAndIdMap(); + + assertEquals(HttpStatus.OK, response.getStatusCode(), RESPONSE_STATUS_MISMATCH); + assertEquals(subjectMap, response.getBody(), RESPONSE_BODY_MISMATCH); + verify(auditService).getSubjectNameAndIdMap(); + } + private Audit createAudit() { return Audit.builder() .id(AUDIT_ID) diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java index 04fd10220..801018bb6 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java @@ -14,7 +14,6 @@ import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.CourtDetails; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtRepository; import uk.gov.hmcts.reform.fact.data.api.services.CourtDetailsViewService; import uk.gov.hmcts.reform.fact.data.api.services.CourtService; @@ -292,34 +291,6 @@ void handleCaTHCourtDeletionReturns204() { verify(courtService).handleCathCourtDeletion("MRD123"); } - @Test - void getCourtNameIdMapReturns200WithAllNameAndIdPairs() { - CourtRepository.NameAndId first = org.mockito.Mockito.mock(CourtRepository.NameAndId.class); - CourtRepository.NameAndId second = org.mockito.Mockito.mock(CourtRepository.NameAndId.class); - List nameAndIds = List.of(first, second); - - when(courtService.getAllCourtNameAndIds()).thenReturn(nameAndIds); - - ResponseEntity> response = courtController.getCourtNameIdMap(); - - assertThat(response.getStatusCode()).as(RESPONSE_STATUS_MESSAGE).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).as(RESPONSE_BODY_MESSAGE).isEqualTo(nameAndIds); - verify(courtService).getAllCourtNameAndIds(); - } - - @Test - void getCourtNameIdMapReturns200WithEmptyListWhenNoCourtsExist() { - List nameAndIds = List.of(); - - when(courtService.getAllCourtNameAndIds()).thenReturn(nameAndIds); - - ResponseEntity> response = courtController.getCourtNameIdMap(); - - assertThat(response.getStatusCode()).as(RESPONSE_STATUS_MESSAGE).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).as(RESPONSE_BODY_MESSAGE).isEqualTo(nameAndIds); - verify(courtService).getAllCourtNameAndIds(); - } - private Court createCourt() { Court court = new Court(); populateCourt(court); diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java index 05f94eef3..7a718a1f9 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java @@ -14,6 +14,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Change; +import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.repositories.AuditRepository; @@ -21,6 +22,7 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.List; +import java.util.Map; import java.util.UUID; import org.junit.jupiter.api.Test; @@ -59,6 +61,8 @@ class AuditServiceTest { @Mock private AuditRepository auditRepository; @Mock + private CourtService courtService; + @Mock private AuditConfigurationProperties auditConfigurationProperties; @InjectMocks @@ -397,6 +401,34 @@ void shouldCallSimpleQueryForBlankEmail() { ); } + @Test + void getSubjectNameAndIdMapShouldReturnCourtAndServiceCentreSubjects() { + List courts = List.of( + new NameAndId("Birmingham Civil and Family Justice Centre", UUID.randomUUID()), + new NameAndId("Exeter Law Courts", UUID.randomUUID()) + ); + when(courtService.getAllCourtNameAndIds()).thenReturn(courts); + + Map> result = auditService.getSubjectNameAndIdMap(); + + assertThat(result).hasSize(2).containsKeys(AuditSubjectType.COURT, AuditSubjectType.SERVICE_CENTRE); + assertThat(result.get(AuditSubjectType.COURT)).containsExactlyElementsOf(courts); + // TODO: assertions for service centres + verify(courtService).getAllCourtNameAndIds(); + } + + @Test + void getSubjectNameAndIdMapShouldReturnEmptyCourtListWhenNoCourtsExist() { + when(courtService.getAllCourtNameAndIds()).thenReturn(List.of()); + + Map> result = auditService.getSubjectNameAndIdMap(); + + assertThat(result).containsKeys(AuditSubjectType.COURT, AuditSubjectType.SERVICE_CENTRE); + assertThat(result.get(AuditSubjectType.COURT)).isEmpty(); + // TODO: assertions for service centres + verify(courtService).getAllCourtNameAndIds(); + } + private Audit createAudit() { return Audit.builder() .id(AUDIT_ID) diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceTest.java index b172bf3bb..4e9061b58 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtServiceTest.java @@ -17,6 +17,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.CourtDetails; import uk.gov.hmcts.reform.fact.data.api.entities.Region; +import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtDetailsRepository; @@ -825,13 +826,13 @@ void getAllCourtDetailsReturnsCourtDetailsListWhenFound() { @Test void getAllCourtNameAndIdsReturnsRepositoryResults() { - CourtRepository.NameAndId first = new CourtRepository.NameAndId("Court A", UUID.randomUUID()); - CourtRepository.NameAndId second = new CourtRepository.NameAndId("Court B", UUID.randomUUID()); - List expected = List.of(first, second); + NameAndId first = new NameAndId("Court A", UUID.randomUUID()); + NameAndId second = new NameAndId("Court B", UUID.randomUUID()); + List expected = List.of(first, second); when(courtRepository.findAllNameAndId()).thenReturn(expected); - List result = courtService.getAllCourtNameAndIds(); + List result = courtService.getAllCourtNameAndIds(); assertThat(result).isEqualTo(expected); verify(courtRepository).findAllNameAndId(); @@ -841,7 +842,7 @@ void getAllCourtNameAndIdsReturnsRepositoryResults() { void getAllCourtNameAndIdsReturnsEmptyListWhenNoCourtsExist() { when(courtRepository.findAllNameAndId()).thenReturn(Collections.emptyList()); - List result = courtService.getAllCourtNameAndIds(); + List result = courtService.getAllCourtNameAndIds(); assertThat(result).isEmpty(); verify(courtRepository).findAllNameAndId(); From 9475ee3c887558f89d39bb688928c5a78880e646 Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Fri, 19 Jun 2026 12:17:39 +0100 Subject: [PATCH 21/40] naming --- .../hmcts/reform/fact/data/api/services/AuditServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java index 7a718a1f9..ebd20e614 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java @@ -402,7 +402,7 @@ void shouldCallSimpleQueryForBlankEmail() { } @Test - void getSubjectNameAndIdMapShouldReturnCourtAndServiceCentreSubjects() { + void getSubjectNameAndIdMapShouldAllSubjectData() { List courts = List.of( new NameAndId("Birmingham Civil and Family Justice Centre", UUID.randomUUID()), new NameAndId("Exeter Law Courts", UUID.randomUUID()) From 78328d8e035033b222a9e960bdd80a67e77cd7d1 Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Mon, 22 Jun 2026 16:20:32 +0100 Subject: [PATCH 22/40] Add audit record retrieval endpoint --- .../AuditControllerFunctionalTest.java | 37 ++++++++++++++++ .../api/controllers/AuditControllerTest.java | 42 +++++++++++++++++++ .../data/api/controllers/AuditController.java | 18 +++++++- .../fact/data/api/services/AuditService.java | 16 +++++++ .../api/controllers/AuditControllerTest.java | 40 ++++++++++++++++++ .../data/api/services/AuditServiceTest.java | 27 ++++++++++++ 6 files changed, 178 insertions(+), 2 deletions(-) diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuditControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuditControllerFunctionalTest.java index 4bec96464..9cd7a98b5 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuditControllerFunctionalTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuditControllerFunctionalTest.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.http.HttpStatus.CREATED; import static org.springframework.http.HttpStatus.NO_CONTENT; +import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.HttpStatus.OK; @Feature("Audit Controller") @@ -94,6 +95,42 @@ void shouldReturnExpectedAuditActionsForCourtAndAddressFlow() { .containsExactly("DELETE", "UPDATE", ACTION_INSERT); } + @Test + @DisplayName("GET /audits/{auditId}/v1 returns a single audit record for an existing id") + void shouldReturnSingleAuditRecordById() { + final UUID courtId = TestDataHelper.createCourt(http, "Test Court Audit By Id"); + final String auditsPath = String.format( + "/audits/v1?pageNumber=0&pageSize=%d&fromDate=%s&courtId=%s", + 50, + LocalDate.now().minusDays(1), + courtId + ); + + final Response auditsResponse = http.doGet(auditsPath); + AssertionHelper.assertStatus(auditsResponse, OK); + + final String auditId = auditsResponse.jsonPath().getString("content[0].id"); + assertThat(auditId).as("Expected at least one audit id to be present").isNotBlank(); + + final Response singleAuditResponse = http.doGet("/audits/" + auditId + "/v1"); + AssertionHelper.assertStatus(singleAuditResponse, OK); + + assertThat(singleAuditResponse.jsonPath().getString("id")) + .as("Returned audit id should match requested id") + .isEqualTo(auditId); + + assertThat(singleAuditResponse.jsonPath().getString("court.id")) + .as("Returned audit should belong to the created court") + .isEqualTo(courtId.toString()); + } + + @Test + @DisplayName("GET /audits/{auditId}/v1 returns not found for unknown id") + void shouldReturnNotFoundForUnknownAuditId() { + final Response response = http.doGet("/audits/" + UUID.randomUUID() + "/v1"); + AssertionHelper.assertStatus(response, NOT_FOUND); + } + private static List extractActionTypesForEntity(final Response response, final String actionEntity) { return response.jsonPath().getList( "content.findAll { it.actionEntity == '" + actionEntity + "' }.actionType", diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index c74dcd531..5cf5e7a2d 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -26,6 +26,7 @@ import java.util.Optional; import java.util.UUID; +import com.jayway.jsonpath.JsonPath; import io.qameta.allure.Feature; import org.apache.commons.lang3.RandomStringUtils; import org.hamcrest.Description; @@ -365,6 +366,47 @@ void getSubjectNameAndIdMapReturnsResults() throws Exception { .andExpect(jsonPath("$.COURT[0].id").exists()); } + @Test + @DisplayName("GET /audits/{auditId}/v1 returns audit when id is valid") + void getAuditByIdReturnsAuditWhenIdIsValid() throws Exception { + Court court = createTestCourts(1).getFirst(); + + String auditsResponse = mvc.perform(get("/audits/v1") + .param("pageNumber", "0") + .param("pageSize", "10") + .param("fromDate", LocalDate.now().toString()) + .param("courtId", court.getId().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content.length()").value(1)) + .andReturn() + .getResponse() + .getContentAsString(); + + String auditId = JsonPath.read(auditsResponse, "$.content[0].id"); + + mvc.perform(get("/audits/" + auditId + "/v1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(auditId)) + .andExpect(jsonPath("$.court.id").value(court.getCourtId().toString())) + .andExpect(jsonPath("$.actionType").exists()) + .andExpect(jsonPath("$.createdAt").exists()); + } + + @Test + @DisplayName("GET /audits/{auditId}/v1 returns 400 when auditId is not a UUID") + void getAuditByIdReturnsBadRequestWhenAuditIdIsInvalid() throws Exception { + mvc.perform(get("/audits/not-a-valid-uuid/v1")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("GET /audits/{auditId}/v1 returns 404 when audit does not exist") + void getAuditByIdReturnsNotFoundWhenAuditDoesNotExist() throws Exception { + mvc.perform(get("/audits/" + UUID.randomUUID() + "/v1")) + .andExpect(status().isNotFound()); + } + private List createTestCourts(int num) { List results = new ArrayList<>(); for (int i = 0; i < num; i++) { diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java index 4d113638e..0eb60b439 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java @@ -12,6 +12,7 @@ import java.time.LocalDate; import java.util.List; import java.util.Map; +import java.util.UUID; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -23,9 +24,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -92,6 +93,20 @@ public ResponseEntity> getFilteredAndPaginatedAudits( ); } + @GetMapping("/{auditId}/v1") + @Operation( + summary = "Retrieve a single audit record", + description = "Fetch the audit record that relates to the given id" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved Audit record"), + @ApiResponse(responseCode = "400", description = "Invalid request parameters supplied") + }) + public ResponseEntity getFilteredAndPaginatedAudits( + @ValidUUID @PathVariable String auditId) { + return ResponseEntity.ok(auditService.getAuditById(UUID.fromString(auditId))); + } + @DeleteMapping("/v1") @Operation( summary = "Remove expired audits", @@ -113,7 +128,6 @@ public ResponseEntity removeExpiredAuditEntries() { @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved map of subject-> name+id pairs") }) - @PreAuthorize("@authService.isAdmin()") // only want admin using this endpoint public ResponseEntity>> getSubjectNameAndIdMap() { return ResponseEntity.ok(this.auditService.getSubjectNameAndIdMap()); } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java index 34a7e7ce1..03c7e42e3 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java @@ -5,6 +5,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; import uk.gov.hmcts.reform.fact.data.api.repositories.AuditRepository; import java.time.LocalDate; @@ -154,4 +155,19 @@ public Map> getSubjectNameAndIdMap() { )) ); } + + /** + * Retrieves an {@link Audit} by its id. + * + * @param auditId the ID of the audit record. + * @return the {@link Audit} record with the specified ID. + * @throws NotFoundException if no audit record is found with the given ID. + */ + public Audit getAuditById(final UUID auditId) { + Audit audit = auditRepository.findById(auditId).orElseThrow(() -> + new NotFoundException("Audit not found, ID: " + auditId) + ); + audit.getCourt(); // Force loading of the court entity + return audit; + } } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index 46d0d0b47..59d54523d 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -14,6 +14,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.types.Change; import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidDateRangeException; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.services.AuditService; @@ -133,6 +134,45 @@ void getSubjectNameAndIdMapReturns200() { verify(auditService).getSubjectNameAndIdMap(); } + @Test + void getAuditByIdReturns200() { + Audit audit = createAudit(); + + when(auditService.getAuditById(AUDIT_ID)).thenReturn(audit); + + ResponseEntity response = auditController.getFilteredAndPaginatedAudits(AUDIT_ID.toString()); + + assertEquals(HttpStatus.OK, response.getStatusCode(), RESPONSE_STATUS_MISMATCH); + assertEquals(audit, response.getBody(), RESPONSE_BODY_MISMATCH); + verify(auditService).getAuditById(AUDIT_ID); + } + + @Test + void getAuditByIdPropagatesExceptionWhenAuditDoesNotExist() { + NotFoundException exception = new NotFoundException("Audit not found"); + UUID missingAuditId = UUID.randomUUID(); + + when(auditService.getAuditById(missingAuditId)).thenThrow(exception); + + String missingAuditIdString = missingAuditId.toString(); + + NotFoundException thrown = assertThrows( + NotFoundException.class, + () -> auditController.getFilteredAndPaginatedAudits(missingAuditIdString) + ); + + assertEquals("Audit not found", thrown.getMessage()); + verify(auditService).getAuditById(missingAuditId); + } + + @Test + void getAuditByIdThrowsIllegalArgumentExceptionForInvalidUuid() { + assertThrows( + IllegalArgumentException.class, + () -> auditController.getFilteredAndPaginatedAudits("not-a-valid-uuid") + ); + } + private Audit createAudit() { return Audit.builder() .id(AUDIT_ID) diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java index ebd20e614..d7d97400c 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java @@ -16,6 +16,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.types.Change; import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; import uk.gov.hmcts.reform.fact.data.api.repositories.AuditRepository; import java.time.LocalDate; @@ -23,6 +24,7 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import org.junit.jupiter.api.Test; @@ -429,6 +431,31 @@ void getSubjectNameAndIdMapShouldReturnEmptyCourtListWhenNoCourtsExist() { verify(courtService).getAllCourtNameAndIds(); } + @Test + void getAuditByIdShouldReturnAuditWhenRecordExists() { + Audit audit = createAudit(); + + when(auditRepository.findById(AUDIT_ID)).thenReturn(Optional.of(audit)); + + Audit result = auditService.getAuditById(AUDIT_ID); + + assertThat(result).isSameAs(audit); + verify(auditRepository).findById(AUDIT_ID); + } + + @Test + void getAuditByIdShouldThrowNotFoundExceptionWhenRecordDoesNotExist() { + when(auditRepository.findById(AUDIT_ID)).thenReturn(Optional.empty()); + + NotFoundException exception = assertThrows( + NotFoundException.class, + () -> auditService.getAuditById(AUDIT_ID) + ); + + assertThat(exception.getMessage()).isEqualTo("Audit not found, ID: " + AUDIT_ID); + verify(auditRepository).findById(AUDIT_ID); + } + private Audit createAudit() { return Audit.builder() .id(AUDIT_ID) From b608602efc5428c3c1b519b062c37ba657d8a68c Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Tue, 23 Jun 2026 12:33:06 +0100 Subject: [PATCH 23/40] Ensure user is part of response entity --- .../reform/fact/data/api/entities/Audit.java | 2 +- .../data/api/repositories/AuditRepository.java | 18 ++++++++++++------ .../fact/data/api/services/AuditService.java | 4 +--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java index d4f60fc9e..4b5fbc966 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java @@ -62,7 +62,7 @@ public class Audit { @Schema(description = "The associated User") @ManyToOne - @JoinColumn(name = "user_id", insertable = false, updatable = false) + @JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false) private User user; @Schema(description = "The action type") diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java index daf2df1b7..227538cc4 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java @@ -8,6 +8,7 @@ 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 org.springframework.stereotype.Repository; @@ -18,21 +19,25 @@ public interface AuditRepository extends JpaRepository { // ---------------------------------------------------- // Standard paged queries + @EntityGraph(attributePaths = {"user"}) Page findByCreatedAtAfter( ZonedDateTime createdAt, Pageable pageable); + @EntityGraph(attributePaths = {"user"}) Page findByCreatedAtBetween( ZonedDateTime createdAtAfter, ZonedDateTime createdAtBefore, Pageable pageable); + @EntityGraph(attributePaths = {"user"}) Page findBySubjectIdAndSubjectTypeAndCreatedAtAfter( UUID subjectId, AuditSubjectType subjectType, ZonedDateTime createdAt, Pageable pageable); + @EntityGraph(attributePaths = {"user"}) Page findBySubjectIdAndSubjectTypeAndCreatedAtBetween( UUID subjectId, AuditSubjectType subjectType, @@ -44,12 +49,9 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtBetween( // Complex paged queries String SELECT_WITH_USER_JOIN = """ - select - a - from - Audit a - join - User u on a.userId = u.id + select a + from Audit a + join User u on a.userId = u.id """; String WHERE_EMAIL_LIKE_AND_CREATED_AT_AFTER = """ @@ -67,12 +69,14 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtBetween( String AND_SUBJECT_EQUALS = " and a.subjectId = :subjectId and a.subjectType = :subjectType"; + @EntityGraph(attributePaths = {"user"}) @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_AFTER) Page findByCreatedAtAfterAndEmailAddressLike( ZonedDateTime createdAtAfter, String email, Pageable pageable); + @EntityGraph(attributePaths = {"user"}) @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_BETWEEN) Page findByCreatedAtBetweenAndEmailAddressLike( ZonedDateTime createdAtAfter, @@ -80,6 +84,7 @@ Page findByCreatedAtBetweenAndEmailAddressLike( String email, Pageable pageable); + @EntityGraph(attributePaths = {"user"}) @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_AFTER + AND_SUBJECT_EQUALS) Page findBySubjectIdAndSubjectTypeAndCreatedAtAfterAndEmailAddressLike( UUID subjectId, @@ -88,6 +93,7 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtAfterAndEmailAddressLike( String email, Pageable pageable); + @EntityGraph(attributePaths = {"user"}) @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_BETWEEN + AND_SUBJECT_EQUALS) Page findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( UUID subjectId, diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java index 03c7e42e3..7a44005f4 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java @@ -164,10 +164,8 @@ public Map> getSubjectNameAndIdMap() { * @throws NotFoundException if no audit record is found with the given ID. */ public Audit getAuditById(final UUID auditId) { - Audit audit = auditRepository.findById(auditId).orElseThrow(() -> + return auditRepository.findById(auditId).orElseThrow(() -> new NotFoundException("Audit not found, ID: " + auditId) ); - audit.getCourt(); // Force loading of the court entity - return audit; } } From 81eee034864fe579326dc1b45c1e3f15ab587935 Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Tue, 23 Jun 2026 15:49:20 +0100 Subject: [PATCH 24/40] Rename AuditSubject to AuditSubjectType to align with SC changes --- .../fact/data/api/controllers/AuditController.java | 4 ++-- .../fact/data/api/entities/types/AuditSubject.java | 9 --------- .../fact/data/api/entities/types/AuditSubjectType.java | 3 +++ 3 files changed, 5 insertions(+), 11 deletions(-) delete mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubject.java diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java index 0eb60b439..36bfee76f 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java @@ -1,7 +1,7 @@ package uk.gov.hmcts.reform.fact.data.api.controllers; import uk.gov.hmcts.reform.fact.data.api.entities.Audit; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubject; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidDateRangeException; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; @@ -128,7 +128,7 @@ public ResponseEntity removeExpiredAuditEntries() { @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved map of subject-> name+id pairs") }) - public ResponseEntity>> getSubjectNameAndIdMap() { + public ResponseEntity>> getSubjectNameAndIdMap() { return ResponseEntity.ok(this.auditService.getSubjectNameAndIdMap()); } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubject.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubject.java deleted file mode 100644 index 09dde99bf..000000000 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubject.java +++ /dev/null @@ -1,9 +0,0 @@ -package uk.gov.hmcts.reform.fact.data.api.entities.types; - -/** - * Subject types that are applicable for auditing. - */ -public enum AuditSubject { - COURT, - SERVICE_CENTRE -} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubjectType.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubjectType.java index 6d97135cf..4427dcf38 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubjectType.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubjectType.java @@ -1,5 +1,8 @@ package uk.gov.hmcts.reform.fact.data.api.entities.types; +/** + * Subject types that are applicable for auditing. + */ public enum AuditSubjectType { COURT, SERVICE_CENTRE From 1092bc69084ca9a1c79de56ff4800946a147eb48 Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Tue, 23 Jun 2026 16:18:15 +0100 Subject: [PATCH 25/40] Refactor audit retrieval to include court and user details, and update method naming for clarity --- .../reform/fact/data/api/controllers/AuditController.java | 5 +++-- .../fact/data/api/repositories/AuditRepository.java | 4 ++++ .../hmcts/reform/fact/data/api/services/AuditService.java | 2 +- .../fact/data/api/controllers/AuditControllerTest.java | 7 +++---- .../reform/fact/data/api/services/AuditServiceTest.java | 8 ++++---- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java index 36bfee76f..11a1a85dc 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java @@ -100,9 +100,10 @@ public ResponseEntity> getFilteredAndPaginatedAudits( ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved Audit record"), - @ApiResponse(responseCode = "400", description = "Invalid request parameters supplied") + @ApiResponse(responseCode = "400", description = "Invalid request parameters supplied"), + @ApiResponse(responseCode = "404", description = "Audit record with the given id was not found") }) - public ResponseEntity getFilteredAndPaginatedAudits( + public ResponseEntity getAuditById( @ValidUUID @PathVariable String auditId) { return ResponseEntity.ok(auditService.getAuditById(UUID.fromString(auditId))); } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java index 227538cc4..e8650b7f1 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java @@ -4,6 +4,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import java.time.ZonedDateTime; +import java.util.Optional; import java.util.UUID; import org.springframework.data.domain.Page; @@ -103,6 +104,9 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( String email, Pageable pageable); + @EntityGraph(attributePaths = {"court", "user"}) + Optional findWithCourtAndUserById(UUID id); + // ---------------------------------------------------- // Housekeeping queries diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java index 7a44005f4..c52fa3014 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java @@ -164,7 +164,7 @@ public Map> getSubjectNameAndIdMap() { * @throws NotFoundException if no audit record is found with the given ID. */ public Audit getAuditById(final UUID auditId) { - return auditRepository.findById(auditId).orElseThrow(() -> + return auditRepository.findWithCourtAndUserById(auditId).orElseThrow(() -> new NotFoundException("Audit not found, ID: " + auditId) ); } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index 59d54523d..752808594 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -140,7 +140,7 @@ void getAuditByIdReturns200() { when(auditService.getAuditById(AUDIT_ID)).thenReturn(audit); - ResponseEntity response = auditController.getFilteredAndPaginatedAudits(AUDIT_ID.toString()); + ResponseEntity response = auditController.getAuditById(AUDIT_ID.toString()); assertEquals(HttpStatus.OK, response.getStatusCode(), RESPONSE_STATUS_MISMATCH); assertEquals(audit, response.getBody(), RESPONSE_BODY_MISMATCH); @@ -158,8 +158,7 @@ void getAuditByIdPropagatesExceptionWhenAuditDoesNotExist() { NotFoundException thrown = assertThrows( NotFoundException.class, - () -> auditController.getFilteredAndPaginatedAudits(missingAuditIdString) - ); + () -> auditController.getAuditById(missingAuditIdString)); assertEquals("Audit not found", thrown.getMessage()); verify(auditService).getAuditById(missingAuditId); @@ -169,7 +168,7 @@ void getAuditByIdPropagatesExceptionWhenAuditDoesNotExist() { void getAuditByIdThrowsIllegalArgumentExceptionForInvalidUuid() { assertThrows( IllegalArgumentException.class, - () -> auditController.getFilteredAndPaginatedAudits("not-a-valid-uuid") + () -> auditController.getAuditById("not-a-valid-uuid") ); } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java index d7d97400c..06d25f429 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java @@ -435,17 +435,17 @@ void getSubjectNameAndIdMapShouldReturnEmptyCourtListWhenNoCourtsExist() { void getAuditByIdShouldReturnAuditWhenRecordExists() { Audit audit = createAudit(); - when(auditRepository.findById(AUDIT_ID)).thenReturn(Optional.of(audit)); + when(auditRepository.findWithCourtAndUserById(AUDIT_ID)).thenReturn(Optional.of(audit)); Audit result = auditService.getAuditById(AUDIT_ID); assertThat(result).isSameAs(audit); - verify(auditRepository).findById(AUDIT_ID); + verify(auditRepository).findWithCourtAndUserById(AUDIT_ID); } @Test void getAuditByIdShouldThrowNotFoundExceptionWhenRecordDoesNotExist() { - when(auditRepository.findById(AUDIT_ID)).thenReturn(Optional.empty()); + when(auditRepository.findWithCourtAndUserById(AUDIT_ID)).thenReturn(Optional.empty()); NotFoundException exception = assertThrows( NotFoundException.class, @@ -453,7 +453,7 @@ void getAuditByIdShouldThrowNotFoundExceptionWhenRecordDoesNotExist() { ); assertThat(exception.getMessage()).isEqualTo("Audit not found, ID: " + AUDIT_ID); - verify(auditRepository).findById(AUDIT_ID); + verify(auditRepository).findWithCourtAndUserById(AUDIT_ID); } private Audit createAudit() { From b15f44f7652f6c41a2033ef6f7648b155e93a087 Mon Sep 17 00:00:00 2001 From: josh-uk Date: Thu, 25 Jun 2026 10:06:39 +0100 Subject: [PATCH 26/40] Go to postgres 17 --- charts/fact-data-api/values.aat.template.yaml | 4 ++-- charts/fact-data-api/values.preview.template.yaml | 4 ++-- docker-compose.yml | 2 +- infrastructure/main.tf | 2 +- src/integrationTest/resources/application-test.yaml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/charts/fact-data-api/values.aat.template.yaml b/charts/fact-data-api/values.aat.template.yaml index eb0f740af..a4140a7d0 100644 --- a/charts/fact-data-api/values.aat.template.yaml +++ b/charts/fact-data-api/values.aat.template.yaml @@ -15,8 +15,8 @@ java: enabled: true image: registry: hmctsprod.azurecr.io - repository: imported/postgres - tag: '18-alpine' + repository: imported/bitnami/postgresql + tag: '17.5.0-debian-12-r20' primary: initdb: scripts: diff --git a/charts/fact-data-api/values.preview.template.yaml b/charts/fact-data-api/values.preview.template.yaml index eb0f740af..a4140a7d0 100644 --- a/charts/fact-data-api/values.preview.template.yaml +++ b/charts/fact-data-api/values.preview.template.yaml @@ -15,8 +15,8 @@ java: enabled: true image: registry: hmctsprod.azurecr.io - repository: imported/postgres - tag: '18-alpine' + repository: imported/bitnami/postgresql + tag: '17.5.0-debian-12-r20' primary: initdb: scripts: diff --git a/docker-compose.yml b/docker-compose.yml index 714d3b0d8..7155a6e59 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: - TESTING_SUPPORT_ENABLE_API=${TESTING_SUPPORT_ENABLE_API} - OS_KEY=${OS_KEY} fact-database: - image: hmctsprod.azurecr.io/imported/postgres:18-alpine + image: hmctsprod.azurecr.io/imported/postgres:17-alpine environment: POSTGRES_DB: fact POSTGRES_USER: fact diff --git a/infrastructure/main.tf b/infrastructure/main.tf index c6bbe6a49..301090776 100644 --- a/infrastructure/main.tf +++ b/infrastructure/main.tf @@ -38,7 +38,7 @@ module "postgresql" { ] common_tags = var.common_tags business_area = "cft" - pgsql_version = "18" + pgsql_version = "17" admin_user_object_id = var.jenkins_AAD_objectId pgsql_server_configuration = [{ name = "azure.extensions" diff --git a/src/integrationTest/resources/application-test.yaml b/src/integrationTest/resources/application-test.yaml index db7818511..c60c43239 100644 --- a/src/integrationTest/resources/application-test.yaml +++ b/src/integrationTest/resources/application-test.yaml @@ -1,7 +1,7 @@ spring: datasource: driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver - url: jdbc:tc:postgresql:18:///fact + url: jdbc:tc:postgresql:17:///fact cloud: azure: storage: From 6cadcfa636eb7d14493adec3a5a61414939de712 Mon Sep 17 00:00:00 2001 From: josh-uk Date: Thu, 25 Jun 2026 10:24:16 +0100 Subject: [PATCH 27/40] update to 17, force re-create --- infrastructure/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/infrastructure/main.tf b/infrastructure/main.tf index 301090776..dec9d4dc2 100644 --- a/infrastructure/main.tf +++ b/infrastructure/main.tf @@ -29,6 +29,7 @@ module "postgresql" { source = "git@github.com:hmcts/terraform-module-postgresql-flexible?ref=master" product = var.product component = var.component + name = "${var.product}-${var.component}-pg17" location = var.location env = var.env pgsql_databases = [ From a0faae0258049984b232be2411356f23a70aa47b Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Thu, 25 Jun 2026 10:26:56 +0100 Subject: [PATCH 28/40] Fixup tests after rebase --- .../controllers/AuditControllerFunctionalTest.java | 2 +- .../fact/data/api/controllers/AuditControllerTest.java | 2 +- .../fact/data/api/repositories/AuditRepository.java | 4 ++-- .../hmcts/reform/fact/data/api/services/AuditService.java | 2 +- .../reform/fact/data/api/services/AuditServiceTest.java | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuditControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuditControllerFunctionalTest.java index 9cd7a98b5..6b661d645 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuditControllerFunctionalTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuditControllerFunctionalTest.java @@ -119,7 +119,7 @@ void shouldReturnSingleAuditRecordById() { .as("Returned audit id should match requested id") .isEqualTo(auditId); - assertThat(singleAuditResponse.jsonPath().getString("court.id")) + assertThat(singleAuditResponse.jsonPath().getString("subjectId")) .as("Returned audit should belong to the created court") .isEqualTo(courtId.toString()); } diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index 5cf5e7a2d..b43e6f2e4 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -388,7 +388,7 @@ void getAuditByIdReturnsAuditWhenIdIsValid() throws Exception { mvc.perform(get("/audits/" + auditId + "/v1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(auditId)) - .andExpect(jsonPath("$.court.id").value(court.getCourtId().toString())) + .andExpect(jsonPath("$.subjectId").value(court.getCourtId().toString())) .andExpect(jsonPath("$.actionType").exists()) .andExpect(jsonPath("$.createdAt").exists()); } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java index e8650b7f1..80f943f10 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java @@ -104,8 +104,8 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( String email, Pageable pageable); - @EntityGraph(attributePaths = {"court", "user"}) - Optional findWithCourtAndUserById(UUID id); + @EntityGraph(attributePaths = {"user"}) + Optional findWithUserById(UUID id); // ---------------------------------------------------- // Housekeeping queries diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java index c52fa3014..518fd380d 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java @@ -164,7 +164,7 @@ public Map> getSubjectNameAndIdMap() { * @throws NotFoundException if no audit record is found with the given ID. */ public Audit getAuditById(final UUID auditId) { - return auditRepository.findWithCourtAndUserById(auditId).orElseThrow(() -> + return auditRepository.findWithUserById(auditId).orElseThrow(() -> new NotFoundException("Audit not found, ID: " + auditId) ); } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java index 06d25f429..6651d7362 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java @@ -435,17 +435,17 @@ void getSubjectNameAndIdMapShouldReturnEmptyCourtListWhenNoCourtsExist() { void getAuditByIdShouldReturnAuditWhenRecordExists() { Audit audit = createAudit(); - when(auditRepository.findWithCourtAndUserById(AUDIT_ID)).thenReturn(Optional.of(audit)); + when(auditRepository.findWithUserById(AUDIT_ID)).thenReturn(Optional.of(audit)); Audit result = auditService.getAuditById(AUDIT_ID); assertThat(result).isSameAs(audit); - verify(auditRepository).findWithCourtAndUserById(AUDIT_ID); + verify(auditRepository).findWithUserById(AUDIT_ID); } @Test void getAuditByIdShouldThrowNotFoundExceptionWhenRecordDoesNotExist() { - when(auditRepository.findWithCourtAndUserById(AUDIT_ID)).thenReturn(Optional.empty()); + when(auditRepository.findWithUserById(AUDIT_ID)).thenReturn(Optional.empty()); NotFoundException exception = assertThrows( NotFoundException.class, @@ -453,7 +453,7 @@ void getAuditByIdShouldThrowNotFoundExceptionWhenRecordDoesNotExist() { ); assertThat(exception.getMessage()).isEqualTo("Audit not found, ID: " + AUDIT_ID); - verify(auditRepository).findWithCourtAndUserById(AUDIT_ID); + verify(auditRepository).findWithUserById(AUDIT_ID); } private Audit createAudit() { From 5a7a31b8189becd8af86e3ce7d31761b65634516 Mon Sep 17 00:00:00 2001 From: josh-uk Date: Thu, 25 Jun 2026 10:52:43 +0100 Subject: [PATCH 29/40] change tag --- charts/fact-data-api/values.aat.template.yaml | 2 +- charts/fact-data-api/values.preview.template.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/fact-data-api/values.aat.template.yaml b/charts/fact-data-api/values.aat.template.yaml index a4140a7d0..69cadd2dc 100644 --- a/charts/fact-data-api/values.aat.template.yaml +++ b/charts/fact-data-api/values.aat.template.yaml @@ -16,7 +16,7 @@ java: image: registry: hmctsprod.azurecr.io repository: imported/bitnami/postgresql - tag: '17.5.0-debian-12-r20' + tag: '17.5.0' primary: initdb: scripts: diff --git a/charts/fact-data-api/values.preview.template.yaml b/charts/fact-data-api/values.preview.template.yaml index a4140a7d0..69cadd2dc 100644 --- a/charts/fact-data-api/values.preview.template.yaml +++ b/charts/fact-data-api/values.preview.template.yaml @@ -16,7 +16,7 @@ java: image: registry: hmctsprod.azurecr.io repository: imported/bitnami/postgresql - tag: '17.5.0-debian-12-r20' + tag: '17.5.0' primary: initdb: scripts: From 145522b46dcc0cdde82911a1d82a6bb98ac91721 Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Thu, 25 Jun 2026 11:18:11 +0100 Subject: [PATCH 30/40] Integrate service centres with audit subjectoptions endpoint --- .../api/controllers/AuditControllerTest.java | 5 ++++- .../repositories/ServiceCentreRepository.java | 16 ++++++++++++++++ .../fact/data/api/services/AuditService.java | 8 ++------ .../data/api/services/ServiceCentreService.java | 10 ++++++++++ .../api/controllers/AuditControllerTest.java | 3 +++ .../fact/data/api/services/AuditServiceTest.java | 15 ++++++++++++--- 6 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index b43e6f2e4..2ce553ce0 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -357,13 +357,16 @@ void deleteAuditsRemovesExpiredAuditsAndReturns204() throws Exception { void getSubjectNameAndIdMapReturnsResults() throws Exception { // Seed at least one court so COURT options are present createTestCourts(1); + createTestServiceCentre(); mvc.perform(get("/audits/subjectoptions/v1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.COURT").isArray()) .andExpect(jsonPath("$.SERVICE_CENTRE").isArray()) .andExpect(jsonPath("$.COURT[0].name").exists()) - .andExpect(jsonPath("$.COURT[0].id").exists()); + .andExpect(jsonPath("$.COURT[0].id").exists()) + .andExpect(jsonPath("$.SERVICE_CENTRE[0].name").exists()) + .andExpect(jsonPath("$.SERVICE_CENTRE[0].id").exists()); } @Test diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java index c054aad6f..826569588 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/ServiceCentreRepository.java @@ -7,6 +7,7 @@ import uk.gov.hmcts.reform.fact.data.api.dto.ServiceCentreWithDistance; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import java.util.List; import java.util.Optional; @@ -88,4 +89,19 @@ List findNearestByServiceAreaAndAreaOfLawAndCatchment @Param("lon") double lon, @Param("limit") int limit ); + + /** + * Retrieve all service centre names mapped to their IDs. + * + * @return the {@link List} of {@link NameAndId} objects + */ + @Query( + value = """ + SELECT sc.name, sc.id + FROM service_centre sc + ORDER BY sc.name + """, + nativeQuery = true + ) + List findAllNameAndId(); } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java index 518fd380d..100afa509 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java @@ -31,6 +31,7 @@ public class AuditService { private final AuditRepository auditRepository; private final CourtService courtService; + private final ServiceCentreService serviceCentreService; private final AuditConfigurationProperties auditConfiguration; /** @@ -147,12 +148,7 @@ private record SubjectFilter(UUID subjectId, AuditSubjectType subjectType) {} public Map> getSubjectNameAndIdMap() { return Map.of( AuditSubjectType.COURT, courtService.getAllCourtNameAndIds(), - // TODO: replace with real service centre once implemented - AuditSubjectType.SERVICE_CENTRE, List.of( - new NameAndId( - "Service Center", - UUID.fromString("00000000-0000-0000-0000-000000000000") - )) + AuditSubjectType.SERVICE_CENTRE, serviceCentreService.getAllServiceCentreNameAndIds() ); } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java index 927174bb7..fac819895 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreService.java @@ -7,6 +7,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreDetailsRepository; @@ -130,6 +131,15 @@ public long deleteServiceCentresByNamePrefix(String serviceCentreNamePrefix) { return serviceCentresToDelete.size(); } + /** + * get all service centre names mapped to their ids. + * + * @return a {@link List} of service centre names with their ids. + */ + public List getAllServiceCentreNameAndIds() { + return serviceCentreRepository.findAllNameAndId(); + } + /** * Convert a service centre name into slug format. * diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index 752808594..4585cb5d8 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -122,6 +122,9 @@ void getSubjectNameAndIdMapReturns200() { Map> subjectMap = Map.of( AuditSubjectType.COURT, List.of( new NameAndId("Exeter Law Courts", UUID.randomUUID()) + ), + AuditSubjectType.SERVICE_CENTRE, List.of( + new NameAndId("Traffic Enforcement Centre", UUID.randomUUID()) ) ); diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java index 6651d7362..eacae1eaa 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java @@ -65,6 +65,8 @@ class AuditServiceTest { @Mock private CourtService courtService; @Mock + private ServiceCentreService serviceCentreService; + @Mock private AuditConfigurationProperties auditConfigurationProperties; @InjectMocks @@ -409,26 +411,33 @@ void getSubjectNameAndIdMapShouldAllSubjectData() { new NameAndId("Birmingham Civil and Family Justice Centre", UUID.randomUUID()), new NameAndId("Exeter Law Courts", UUID.randomUUID()) ); + List serviceCentres = List.of( + new NameAndId("Traffic Enforcement Centre", UUID.randomUUID()) + ); when(courtService.getAllCourtNameAndIds()).thenReturn(courts); + when(serviceCentreService.getAllServiceCentreNameAndIds()).thenReturn(serviceCentres); Map> result = auditService.getSubjectNameAndIdMap(); assertThat(result).hasSize(2).containsKeys(AuditSubjectType.COURT, AuditSubjectType.SERVICE_CENTRE); assertThat(result.get(AuditSubjectType.COURT)).containsExactlyElementsOf(courts); - // TODO: assertions for service centres + assertThat(result.get(AuditSubjectType.SERVICE_CENTRE)).containsExactlyElementsOf(serviceCentres); verify(courtService).getAllCourtNameAndIds(); + verify(serviceCentreService).getAllServiceCentreNameAndIds(); } @Test - void getSubjectNameAndIdMapShouldReturnEmptyCourtListWhenNoCourtsExist() { + void getSubjectNameAndIdMapShouldReturnEmptyListsWhenNoSubjectsExist() { when(courtService.getAllCourtNameAndIds()).thenReturn(List.of()); + when(serviceCentreService.getAllServiceCentreNameAndIds()).thenReturn(List.of()); Map> result = auditService.getSubjectNameAndIdMap(); assertThat(result).containsKeys(AuditSubjectType.COURT, AuditSubjectType.SERVICE_CENTRE); assertThat(result.get(AuditSubjectType.COURT)).isEmpty(); - // TODO: assertions for service centres + assertThat(result.get(AuditSubjectType.SERVICE_CENTRE)).isEmpty(); verify(courtService).getAllCourtNameAndIds(); + verify(serviceCentreService).getAllServiceCentreNameAndIds(); } @Test From b7814f11d8082774ac7a11aefa5b8708a71bd579 Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Thu, 25 Jun 2026 15:43:07 +0100 Subject: [PATCH 31/40] Remove duplicate flyway scripts --- ...1__drop_service_centre_data_structures.sql | 9 --- .../V1.32__create_service_centre_tables.sql | 64 ------------------- .../V1.33__generalise_audit_subject.sql | 12 ---- .../V1.34__refresh_service_area_mappings.sql | 43 ------------- ...ique_constraint_to_service_centre_slug.sql | 2 - 5 files changed, 130 deletions(-) delete mode 100644 src/main/resources/db/migration/V1.31__drop_service_centre_data_structures.sql delete mode 100644 src/main/resources/db/migration/V1.32__create_service_centre_tables.sql delete mode 100644 src/main/resources/db/migration/V1.33__generalise_audit_subject.sql delete mode 100644 src/main/resources/db/migration/V1.34__refresh_service_area_mappings.sql delete mode 100644 src/main/resources/db/migration/V1.35__add_unique_constraint_to_service_centre_slug.sql diff --git a/src/main/resources/db/migration/V1.31__drop_service_centre_data_structures.sql b/src/main/resources/db/migration/V1.31__drop_service_centre_data_structures.sql deleted file mode 100644 index 73831ba19..000000000 --- a/src/main/resources/db/migration/V1.31__drop_service_centre_data_structures.sql +++ /dev/null @@ -1,9 +0,0 @@ -ALTER TABLE court DROP COLUMN IF EXISTS is_service_centre; - -DROP TRIGGER IF EXISTS court_service_areas_court_update_last_updated_at_trg ON court_service_areas; - -DROP TABLE IF EXISTS court_service_areas CASCADE; - -DELETE FROM region -WHERE name = 'Service Centre' - AND country = 'England'; diff --git a/src/main/resources/db/migration/V1.32__create_service_centre_tables.sql b/src/main/resources/db/migration/V1.32__create_service_centre_tables.sql deleted file mode 100644 index 8a7e4c4c4..000000000 --- a/src/main/resources/db/migration/V1.32__create_service_centre_tables.sql +++ /dev/null @@ -1,64 +0,0 @@ -CREATE TABLE service_centre ( - id UUID PRIMARY KEY NOT NULL, - name VARCHAR NOT NULL, - slug VARCHAR, - open BOOLEAN, - warning_notice VARCHAR, - created_at TIMESTAMP NOT NULL, - last_updated_at TIMESTAMP NOT NULL, - service_area_ids UUID[], - catchment_type VARCHAR -); - -CREATE TABLE service_centre_address ( - id UUID PRIMARY KEY NOT NULL, - service_centre_id UUID NOT NULL, - address_line_1 VARCHAR, - address_line_2 VARCHAR, - town_city VARCHAR, - county VARCHAR, - postcode VARCHAR, - lat DECIMAL(9,6), - lon DECIMAL(9,6), - address_type VARCHAR -); - -CREATE TABLE service_centre_contact_details ( - id UUID PRIMARY KEY NOT NULL, - service_centre_id UUID NOT NULL, - service_centre_contact_description_id UUID, - explanation VARCHAR, - explanation_cy VARCHAR, - email VARCHAR, - phone_number VARCHAR -); - -CREATE TABLE service_centre_areas_of_law ( - id UUID PRIMARY KEY NOT NULL, - service_centre_id UUID NOT NULL, - areas_of_law UUID[] -); - -ALTER TABLE service_centre_address -ADD CONSTRAINT fk_service_centre_address_service_centre - FOREIGN KEY (service_centre_id) - REFERENCES service_centre(id) - ON DELETE CASCADE; - -ALTER TABLE service_centre_contact_details -ADD CONSTRAINT fk_service_centre_contact_details_service_centre - FOREIGN KEY (service_centre_id) - REFERENCES service_centre(id) - ON DELETE CASCADE; - -ALTER TABLE service_centre_contact_details -ADD CONSTRAINT fk_service_centre_contact_details_description - FOREIGN KEY (service_centre_contact_description_id) - REFERENCES contact_description_types(id) - ON DELETE SET NULL; - -ALTER TABLE service_centre_areas_of_law -ADD CONSTRAINT fk_service_centre_areas_of_law_service_centre - FOREIGN KEY (service_centre_id) - REFERENCES service_centre(id) - ON DELETE CASCADE; diff --git a/src/main/resources/db/migration/V1.33__generalise_audit_subject.sql b/src/main/resources/db/migration/V1.33__generalise_audit_subject.sql deleted file mode 100644 index 1e5b2c1f8..000000000 --- a/src/main/resources/db/migration/V1.33__generalise_audit_subject.sql +++ /dev/null @@ -1,12 +0,0 @@ -ALTER TABLE audit - RENAME COLUMN court_id TO subject_id; - -ALTER TABLE audit - ADD COLUMN subject_type VARCHAR; - -UPDATE audit -SET subject_type = 'COURT' -WHERE subject_type IS NULL; - -ALTER TABLE audit - ALTER COLUMN subject_type SET NOT NULL; diff --git a/src/main/resources/db/migration/V1.34__refresh_service_area_mappings.sql b/src/main/resources/db/migration/V1.34__refresh_service_area_mappings.sql deleted file mode 100644 index 2aae2bbcc..000000000 --- a/src/main/resources/db/migration/V1.34__refresh_service_area_mappings.sql +++ /dev/null @@ -1,43 +0,0 @@ -WITH mapping(service_name, area_name) AS (VALUES - ('Childcare and parenting', 'Adoption'), - ('Childcare and parenting', 'Childcare arrangements if you separate from your partner'), - ('Crime', 'Other criminal offences'), - ('Crime', 'Single Justice Procedure'), - ('Harm and abuse', 'Domestic abuse'), - ('Harm and abuse', 'Female Genital Mutilation Protection Orders'), - ('Harm and abuse', 'Forced marriage'), - ('High Court district registries', 'High Court district registry'), - ('Immigration and asylum', 'Immigration and asylum'), - ('Money', 'Bankruptcy'), - ('Money', 'Benefits'), - ('Money', 'Claims against employers'), - ('Money', 'Housing'), - ('Money', 'Money claims'), - ('Money', 'Probate'), - ('Money', 'Single Justice Procedure'), - ('Money', 'Tax'), - ('Probate, divorce or ending civil partnerships', 'Civil partnership'), - ('Probate, divorce or ending civil partnerships', 'Divorce'), - ('Probate, divorce or ending civil partnerships', 'Financial remedy'), - ('Probate, divorce or ending civil partnerships', 'Forced marriage'), - ('Probate, divorce or ending civil partnerships', 'Probate') -), -dedup AS ( - SELECT DISTINCT m.service_name, - sa.id AS service_area_id, - sa.sort_order, - sa.name - FROM mapping m - JOIN service_area sa - ON lower(sa.name) = lower(m.area_name) -), -agg AS ( - SELECT service_name, - array_agg(service_area_id ORDER BY sort_order nulls last, name) AS service_area_ids - FROM dedup - GROUP BY service_name -) -UPDATE service s -SET service_areas = agg.service_area_ids -FROM agg -WHERE lower(s.name) = lower(agg.service_name); diff --git a/src/main/resources/db/migration/V1.35__add_unique_constraint_to_service_centre_slug.sql b/src/main/resources/db/migration/V1.35__add_unique_constraint_to_service_centre_slug.sql deleted file mode 100644 index a273650ef..000000000 --- a/src/main/resources/db/migration/V1.35__add_unique_constraint_to_service_centre_slug.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE service_centre - ADD CONSTRAINT service_centre_slug_unique UNIQUE (slug); From 3e246a2cbbf290790126157ca6e2145891996b2f Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Thu, 25 Jun 2026 15:43:25 +0100 Subject: [PATCH 32/40] Fix broken test --- .../reform/fact/data/api/controllers/AuditControllerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index c46d0aa14..4585cb5d8 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -95,7 +95,6 @@ void getFilteredAndPaginatedAuditsThrowsInvalidDateRangeExceptionForInvalidDateR null, null, null, - null, fromDate, toDate ) From 510b606454f4ce9f69c5afb6442ccf38ec934cea Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Thu, 25 Jun 2026 15:55:01 +0100 Subject: [PATCH 33/40] Fix rebase drift and align with master --- charts/fact-data-api/values.aat.template.yaml | 4 ---- charts/fact-data-api/values.preview.template.yaml | 4 ---- .../reform/fact/data/api/controllers/CourtControllerTest.java | 1 + 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/charts/fact-data-api/values.aat.template.yaml b/charts/fact-data-api/values.aat.template.yaml index 69cadd2dc..4f835bceb 100644 --- a/charts/fact-data-api/values.aat.template.yaml +++ b/charts/fact-data-api/values.aat.template.yaml @@ -13,10 +13,6 @@ java: TESTING_SUPPORT_ENABLE_API: true postgresql: enabled: true - image: - registry: hmctsprod.azurecr.io - repository: imported/bitnami/postgresql - tag: '17.5.0' primary: initdb: scripts: diff --git a/charts/fact-data-api/values.preview.template.yaml b/charts/fact-data-api/values.preview.template.yaml index 69cadd2dc..4f835bceb 100644 --- a/charts/fact-data-api/values.preview.template.yaml +++ b/charts/fact-data-api/values.preview.template.yaml @@ -13,10 +13,6 @@ java: TESTING_SUPPORT_ENABLE_API: true postgresql: enabled: true - image: - registry: hmctsprod.azurecr.io - repository: imported/bitnami/postgresql - tag: '17.5.0' primary: initdb: scripts: diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java index fc0785e3e..6c5cab536 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtControllerTest.java @@ -534,6 +534,7 @@ void getAllCourtsReturns200() throws Exception { Assertions.assertThat(responseList).isEqualTo(courtDetailsList); } + private Court buildCourt(UUID id) { return Court.builder() .id(id) From 07c907b6d051fd1bb502e35e87e6c30caec75c31 Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Thu, 25 Jun 2026 15:58:10 +0100 Subject: [PATCH 34/40] Add tests for service centre name and id retrieval --- .../services/ServiceCentreServiceTest.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java index 398cc247b..9f201ff99 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/ServiceCentreServiceTest.java @@ -9,11 +9,13 @@ import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreDetails; import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceAreaRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreDetailsRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.ServiceCentreRepository; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -198,4 +200,28 @@ void deleteServiceCentresByNamePrefixDeletesMatchingRowsForTestingSupport() { assertThat(deleted).isEqualTo(2); verify(serviceCentreRepository).deleteAllInBatch(serviceCentres); } + + @Test + void getAllServiceCentreNameAndIdsReturnsRepositoryResults() { + NameAndId first = new NameAndId("ServiceCentre A", UUID.randomUUID()); + NameAndId second = new NameAndId("ServiceCentre B", UUID.randomUUID()); + List expected = List.of(first, second); + + when(serviceCentreRepository.findAllNameAndId()).thenReturn(expected); + + List result = serviceCentreService.getAllServiceCentreNameAndIds(); + + assertThat(result).isEqualTo(expected); + verify(serviceCentreRepository).findAllNameAndId(); + } + + @Test + void getAllServiceCentreNameAndIdsReturnsEmptyListWhenNoServiceCentresExist() { + when(serviceCentreRepository.findAllNameAndId()).thenReturn(Collections.emptyList()); + + List result = serviceCentreService.getAllServiceCentreNameAndIds(); + + assertThat(result).isEmpty(); + verify(serviceCentreRepository).findAllNameAndId(); + } } From caf1a41c902b9f26e0b110c4c257b9f5632548c9 Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Thu, 25 Jun 2026 17:21:16 +0100 Subject: [PATCH 35/40] Add subject type filtering to the audit query code --- .../fact/data/api/audit/AuditingTest.java | 1 + .../api/controllers/AuditControllerTest.java | 32 +++ .../data/api/controllers/AuditController.java | 2 + .../api/repositories/AuditRepository.java | 33 ++- .../fact/data/api/services/AuditService.java | 41 +++- .../api/controllers/AuditControllerTest.java | 4 + .../data/api/services/AuditServiceTest.java | 194 +++++++++++++++++- 7 files changed, 300 insertions(+), 7 deletions(-) diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java index 3891496cb..e9a7db14e 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java @@ -366,6 +366,7 @@ private void assertAuditActionsForEntity(Court owningCourt, String testedEntity, 1000, CREATED_AFTER, null, + null, owningCourt.getId().toString(), null, null diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index 2ce553ce0..bbbd0373e 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -11,6 +11,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditActionType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.UserRole; import uk.gov.hmcts.reform.fact.data.api.repositories.AuditRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtRepository; @@ -346,6 +347,37 @@ void getFilteredAndPaginatedAuditsReturnsBadRequestForInvalidPageNumber() throws .andExpect(status().isBadRequest()); } + @Test + @DisplayName("GET /audits/v1 returns results when subjectType is provided") + void getFilteredAndPaginatedAuditsReturnsResultsWhenSubjectTypeProvided() throws Exception { + createTestCourts(2); + createTestServiceCentre(); + + mvc.perform(get("/audits/v1") + .param("pageNumber", "0") + .param("pageSize", "10") + .param("fromDate", LocalDate.now().toString()) + .param("subjectType", AuditSubjectType.COURT.name())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content.length()").value(2)) + .andExpect(jsonPath("$.content[*].subjectType") + .value(allElementsEqual(AuditSubjectType.COURT.name()))) + .andExpect(jsonPath("$.page.number").value(0)) + .andExpect(jsonPath("$.page.totalElements").value(2)); + } + + @Test + @DisplayName("GET /audits/v1 returns 400 when subjectType is invalid") + void getFilteredAndPaginatedAuditsReturnsBadRequestForInvalidSubjectType() throws Exception { + mvc.perform(get("/audits/v1") + .param("pageNumber", "0") + .param("pageSize", "5") + .param("fromDate", LocalDate.now().toString()) + .param("subjectType", "NOT_A_REAL_TYPE")) + .andExpect(status().isBadRequest()); + } + @Test @DisplayName("DELETE /audits/v1 returns 204") void deleteAuditsRemovesExpiredAuditsAndReturns204() throws Exception { diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java index 11a1a85dc..857047062 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java @@ -58,6 +58,7 @@ public ResponseEntity> getFilteredAndPaginatedAudits( @PositiveOrZero(message = "pageNumber must be greater than or equal to 0") int pageNumber, @RequestParam(name = "pageSize", defaultValue = "25") @Positive(message = "pageSize must be greater than 0") int pageSize, + @RequestParam(name = "subjectType", required = false) AuditSubjectType subjectType, @RequestParam(name = "courtId", required = false) @ValidUUID(allowNull = true) String courtId, @RequestParam(name = "serviceCentreId", required = false) @ValidUUID(allowNull = true) String serviceCentreId, @RequestParam(name = "email", required = false) @@ -86,6 +87,7 @@ public ResponseEntity> getFilteredAndPaginatedAudits( pageSize, fromDate, toDate, + subjectType, courtId, serviceCentreId, emailMatch diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java index 80f943f10..4949e25fb 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java @@ -31,6 +31,12 @@ Page findByCreatedAtBetween( ZonedDateTime createdAtBefore, Pageable pageable); + @EntityGraph(attributePaths = {"user"}) + Page findBySubjectTypeAndCreatedAtAfter( + AuditSubjectType subjectType, + ZonedDateTime fromDateTime, + Pageable pageable); + @EntityGraph(attributePaths = {"user"}) Page findBySubjectIdAndSubjectTypeAndCreatedAtAfter( UUID subjectId, @@ -38,6 +44,13 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtAfter( ZonedDateTime createdAt, Pageable pageable); + @EntityGraph(attributePaths = {"user"}) + Page findBySubjectTypeAndCreatedAtBetween( + AuditSubjectType subjectType, + ZonedDateTime fromDateTime, + ZonedDateTime toDateTime, + Pageable pageable); + @EntityGraph(attributePaths = {"user"}) Page findBySubjectIdAndSubjectTypeAndCreatedAtBetween( UUID subjectId, @@ -46,6 +59,7 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtBetween( ZonedDateTime createdAtBefore, Pageable pageable); + // ---------------------------------------------------- // Complex paged queries @@ -68,6 +82,7 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtBetween( """; String AND_SUBJECT_EQUALS = " and a.subjectId = :subjectId and a.subjectType = :subjectType"; + String AND_SUBJECT_TYPE_EQUALS = " and a.subjectType = :subjectType"; @EntityGraph(attributePaths = {"user"}) @@ -104,6 +119,23 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( String email, Pageable pageable); + @EntityGraph(attributePaths = {"user"}) + @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_AFTER + AND_SUBJECT_TYPE_EQUALS) + Page findBySubjectTypeAndCreatedAtAfterAndEmailAddressLike( + AuditSubjectType subjectType, + ZonedDateTime fromDateTime, + String email, + Pageable pageable); + + @EntityGraph(attributePaths = {"user"}) + @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_BETWEEN + AND_SUBJECT_TYPE_EQUALS) + Page findBySubjectTypeAndCreatedAtBetweenAndEmailAddressLike( + AuditSubjectType subjectType, + ZonedDateTime fromDateTime, + ZonedDateTime toDateTime, + String email, + Pageable pageable); + @EntityGraph(attributePaths = {"user"}) Optional findWithUserById(UUID id); @@ -111,5 +143,4 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( // Housekeeping queries void deleteAllByCreatedAtBefore(ZonedDateTime createdAtBefore); - } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java index 100afa509..680aed572 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java @@ -47,8 +47,8 @@ public class AuditService { * @return a {@link Page} of {@link Audit} results. */ public Page getFilteredAndPaginatedAudits(int pageNumber, int pageSize, @NonNull LocalDate fromDate, - LocalDate toDate, String courtId, String serviceCentreId, - String email) { + LocalDate toDate, AuditSubjectType subjectType, String courtId, + String serviceCentreId, String email) { Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending()); @@ -62,7 +62,7 @@ public Page getFilteredAndPaginatedAudits(int pageNumber, int pageSize, @ ZoneOffset.UTC )).orElse(null); - SubjectFilter subjectFilter = resolveSubjectFilter(courtId, serviceCentreId); + SubjectFilter subjectFilter = resolveSubjectFilter(courtId, serviceCentreId, subjectType); return performAuditQuery(fromDateTime, toDateTime, subjectFilter, email, pageable); } @@ -86,7 +86,12 @@ private Page performAuditQuery(ZonedDateTime fromDateTime, ZonedDateTime boolean hasEmail = email != null && !email.isBlank(); if (subjectFilter != null) { - return performSubjectAuditQuery(fromDateTime, toDateTime, subjectFilter, email, pageable); + if (subjectFilter.subjectId != null) { + return performSubjectAuditQuery(fromDateTime, toDateTime, subjectFilter, email, pageable); + } else { + return performSubjectTypeAuditQuery( + fromDateTime, toDateTime, subjectFilter.subjectType(), email, pageable); + } } if (hasToDate && hasEmail) { return auditRepository.findByCreatedAtBetweenAndEmailAddressLike( @@ -124,7 +129,29 @@ private Page performSubjectAuditQuery(ZonedDateTime fromDateTime, ZonedDa subjectId, subjectType, fromDateTime, pageable); } - private static SubjectFilter resolveSubjectFilter(String courtId, String serviceCentreId) { + private Page performSubjectTypeAuditQuery(ZonedDateTime fromDateTime, ZonedDateTime toDateTime, + AuditSubjectType subjectType, String email, Pageable pageable) { + boolean hasToDate = toDateTime != null; + boolean hasEmail = email != null && !email.isBlank(); + + if (hasToDate && hasEmail) { + return auditRepository.findBySubjectTypeAndCreatedAtBetweenAndEmailAddressLike( + subjectType, fromDateTime, toDateTime, email, pageable); + } + if (hasToDate) { + return auditRepository.findBySubjectTypeAndCreatedAtBetween( + subjectType, fromDateTime, toDateTime, pageable); + } + if (hasEmail) { + return auditRepository.findBySubjectTypeAndCreatedAtAfterAndEmailAddressLike( + subjectType, fromDateTime, email, pageable); + } + return auditRepository.findBySubjectTypeAndCreatedAtAfter( + subjectType, fromDateTime, pageable); + } + + private static SubjectFilter resolveSubjectFilter(String courtId, String serviceCentreId, + AuditSubjectType subjectType) { if (courtId != null && serviceCentreId != null) { throw new InvalidParameterCombinationException("Only one of courtId or serviceCentreId can be provided"); } @@ -134,6 +161,10 @@ private static SubjectFilter resolveSubjectFilter(String courtId, String service if (serviceCentreId != null) { return new SubjectFilter(UUID.fromString(serviceCentreId), AuditSubjectType.SERVICE_CENTRE); } + // if a subject type is provided without a specific subject id, we can still filter by subject type + if (subjectType != null) { + return new SubjectFilter(null, subjectType); + } return null; } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index 4585cb5d8..341d2741e 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -66,6 +66,7 @@ void getFilteredAndPaginatedAuditsReturns200() { any(LocalDate.class), isNull(), isNull(), + isNull(), isNull() )).thenReturn(auditPage); @@ -75,6 +76,7 @@ void getFilteredAndPaginatedAuditsReturns200() { null, null, null, + null, LocalDate.now().minusDays(1), LocalDate.now() ); @@ -95,6 +97,7 @@ void getFilteredAndPaginatedAuditsThrowsInvalidDateRangeExceptionForInvalidDateR null, null, null, + null, fromDate, toDate ) @@ -108,6 +111,7 @@ void getFilteredAndPaginatedAuditsThrowsInvalidParameterCombinationForBothSubjec auditController.getFilteredAndPaginatedAudits( PAGE_NUMBER, PAGE_SIZE, + null, COURT_ID.toString(), SERVICE_CENTRE_ID.toString(), null, diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java index 9e0a4176f..688908cab 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java @@ -90,6 +90,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCreatedAtAfter() { fromDate, null, null, + null, null, null ); @@ -118,6 +119,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCreatedAtBetween() { toDate, null, null, + null, null ); @@ -144,6 +146,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtAfter() { PAGE_SIZE, fromDate, null, + null, COURT_ID.toString(), null, null @@ -173,6 +176,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtBetween() { PAGE_SIZE, fromDate, toDate, + null, COURT_ID.toString(), null, null @@ -204,6 +208,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCreatedAtAfterAndEmailAddressL null, null, null, + null, EMAIL ); @@ -232,6 +237,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCreatedAtBetweenAndEmailAddres toDate, null, null, + null, EMAIL ); @@ -259,6 +265,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtAfterAndEma PAGE_SIZE, fromDate, null, + null, COURT_ID.toString(), null, EMAIL @@ -289,6 +296,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtBetweenAndE PAGE_SIZE, fromDate, toDate, + null, COURT_ID.toString(), null, EMAIL @@ -333,6 +341,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByServiceCentreIdAndCreatedAtAft fromDate, null, null, + null, SERVICE_CENTRE_ID.toString(), null ); @@ -351,6 +360,7 @@ void shouldThrowInvalidParameterCombinationExceptionWhenBothSubjectIdsProvided() PAGE_SIZE, fromDate, null, + null, COURT_ID.toString(), SERVICE_CENTRE_ID.toString(), null @@ -362,7 +372,7 @@ void shouldThrowInvalidParameterCombinationExceptionWhenBothSubjectIdsProvided() void shouldThrowNullPointerExceptionWhenFromDateIsNull() { assertThrows( NullPointerException.class, () -> { - auditService.getFilteredAndPaginatedAudits(0, 1, null, null, null, null, null); + auditService.getFilteredAndPaginatedAudits(0, 1, null, null, null,null, null, null); } ); } @@ -384,6 +394,7 @@ void shouldCallSimpleQueryForBlankEmail() { null, null, null, + null, "" ); assertThat(result.getContent()).hasSize(1); @@ -396,6 +407,7 @@ void shouldCallSimpleQueryForBlankEmail() { null, null, null, + null, " " ); assertThat(result.getContent()).hasSize(1); @@ -465,6 +477,186 @@ void getAuditByIdShouldThrowNotFoundExceptionWhenRecordDoesNotExist() { verify(auditRepository).findWithUserById(AUDIT_ID); } + @Test + void getFilteredAndPaginatedAuditsShouldCallFindBySubjectTypeAndCreatedAtAfterWhenOnlySubjectTypeProvided() { + when(auditRepository.findBySubjectTypeAndCreatedAtAfter( + eq(AuditSubjectType.COURT), + eq(fromDateTime), + any(Pageable.class) + ) + ).thenReturn(new PageImpl<>(List.of(createAudit()))); + + Page result = + auditService.getFilteredAndPaginatedAudits( + PAGE_NUMBER, + PAGE_SIZE, + fromDate, + null, + AuditSubjectType.COURT, + null, + null, + null + ); + + assertThat(result.getContent()).hasSize(1); + verify(auditRepository).findBySubjectTypeAndCreatedAtAfter( + eq(AuditSubjectType.COURT), eq(fromDateTime), any(Pageable.class) + ); + } + + @Test + void getFilteredAndPaginatedAuditsShouldCallFindBySubjectTypeAndCreatedAtBetweenWhenSubjectTypeAndToDateProvided() { + when(auditRepository.findBySubjectTypeAndCreatedAtBetween( + eq(AuditSubjectType.COURT), + eq(fromDateTime), + eq(toDateTime), + any(Pageable.class) + ) + ).thenReturn(new PageImpl<>(List.of(createAudit()))); + + Page result = + auditService.getFilteredAndPaginatedAudits( + PAGE_NUMBER, + PAGE_SIZE, + fromDate, + toDate, + AuditSubjectType.COURT, + null, + null, + null + ); + + assertThat(result.getContent()).hasSize(1); + verify(auditRepository).findBySubjectTypeAndCreatedAtBetween( + eq(AuditSubjectType.COURT), eq(fromDateTime), eq(toDateTime), any(Pageable.class) + ); + } + + @Test + void shouldCallFindBySubjectTypeAndCreatedAtAfterAndEmailAddressLikeWhenSubjectTypeAndEmailProvided() { + when(auditRepository.findBySubjectTypeAndCreatedAtAfterAndEmailAddressLike( + eq(AuditSubjectType.SERVICE_CENTRE), + eq(fromDateTime), + eq(EMAIL), + any(Pageable.class) + ) + ).thenReturn(new PageImpl<>(List.of(createAudit()))); + + Page result = + auditService.getFilteredAndPaginatedAudits( + PAGE_NUMBER, + PAGE_SIZE, + fromDate, + null, + AuditSubjectType.SERVICE_CENTRE, + null, + null, + EMAIL + ); + + assertThat(result.getContent()).hasSize(1); + verify(auditRepository).findBySubjectTypeAndCreatedAtAfterAndEmailAddressLike( + eq(AuditSubjectType.SERVICE_CENTRE), eq(fromDateTime), eq(EMAIL), any(Pageable.class) + ); + } + + @Test + void shouldCallFindBySubjectTypeAndCreatedAtBetweenAndEmailAddressLikeWhenAllFiltersProvided() { + when(auditRepository.findBySubjectTypeAndCreatedAtBetweenAndEmailAddressLike( + eq(AuditSubjectType.SERVICE_CENTRE), + eq(fromDateTime), + eq(toDateTime), + eq(EMAIL), + any(Pageable.class) + ) + ).thenReturn(new PageImpl<>(List.of(createAudit()))); + + Page result = + auditService.getFilteredAndPaginatedAudits( + PAGE_NUMBER, + PAGE_SIZE, + fromDate, + toDate, + AuditSubjectType.SERVICE_CENTRE, + null, + null, + EMAIL + ); + + assertThat(result.getContent()).hasSize(1); + verify(auditRepository).findBySubjectTypeAndCreatedAtBetweenAndEmailAddressLike( + eq(AuditSubjectType.SERVICE_CENTRE), eq(fromDateTime), eq(toDateTime), eq(EMAIL), any(Pageable.class) + ); + } + + @Test + void getFilteredAndPaginatedAuditsShouldIgnoreBlankEmailForSubjectTypeAndUseSimpleSubjectTypeQuery() { + when(auditRepository.findBySubjectTypeAndCreatedAtAfter( + eq(AuditSubjectType.COURT), + eq(fromDateTime), + any(Pageable.class) + ) + ).thenReturn(new PageImpl<>(List.of(createAudit()))); + + Page firstResult = + auditService.getFilteredAndPaginatedAudits( + PAGE_NUMBER, + PAGE_SIZE, + fromDate, + null, + AuditSubjectType.COURT, + null, + null, + "" + ); + + Page secondResult = + auditService.getFilteredAndPaginatedAudits( + PAGE_NUMBER, + PAGE_SIZE, + fromDate, + null, + AuditSubjectType.COURT, + null, + null, + " " + ); + + assertThat(firstResult.getContent()).hasSize(1); + assertThat(secondResult.getContent()).hasSize(1); + verify(auditRepository, times(2)).findBySubjectTypeAndCreatedAtAfter( + eq(AuditSubjectType.COURT), eq(fromDateTime), any(Pageable.class) + ); + } + + @Test + void getFilteredAndPaginatedAuditsShouldPrioritiseCourtIdOverSubjectTypeWhenBothProvided() { + when(auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfter( + eq(COURT_ID), + eq(AuditSubjectType.COURT), + eq(fromDateTime), + any(Pageable.class) + ) + ).thenReturn(new PageImpl<>(List.of(createAudit()))); + + Page result = + auditService.getFilteredAndPaginatedAudits( + PAGE_NUMBER, + PAGE_SIZE, + fromDate, + null, + AuditSubjectType.SERVICE_CENTRE, + COURT_ID.toString(), + null, + null + ); + + assertThat(result.getContent()).hasSize(1); + verify(auditRepository).findBySubjectIdAndSubjectTypeAndCreatedAtAfter( + eq(COURT_ID), eq(AuditSubjectType.COURT), eq(fromDateTime), any(Pageable.class) + ); + } + private Audit createAudit() { return Audit.builder() .id(AUDIT_ID) From 22ba74f241eae041a15171fc9fd415b9e517c3ca Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Fri, 26 Jun 2026 11:05:00 +0100 Subject: [PATCH 36/40] Made parameter names consistent with query fields and other methods --- .../fact/data/api/repositories/AuditRepository.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java index 4949e25fb..c3a250e28 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java @@ -34,7 +34,7 @@ Page findByCreatedAtBetween( @EntityGraph(attributePaths = {"user"}) Page findBySubjectTypeAndCreatedAtAfter( AuditSubjectType subjectType, - ZonedDateTime fromDateTime, + ZonedDateTime createdAtAfter, Pageable pageable); @EntityGraph(attributePaths = {"user"}) @@ -47,8 +47,8 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtAfter( @EntityGraph(attributePaths = {"user"}) Page findBySubjectTypeAndCreatedAtBetween( AuditSubjectType subjectType, - ZonedDateTime fromDateTime, - ZonedDateTime toDateTime, + ZonedDateTime createdAtAfter, + ZonedDateTime createdAtBefore, Pageable pageable); @EntityGraph(attributePaths = {"user"}) @@ -123,7 +123,7 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_AFTER + AND_SUBJECT_TYPE_EQUALS) Page findBySubjectTypeAndCreatedAtAfterAndEmailAddressLike( AuditSubjectType subjectType, - ZonedDateTime fromDateTime, + ZonedDateTime createdAtAfter, String email, Pageable pageable); @@ -131,8 +131,8 @@ Page findBySubjectTypeAndCreatedAtAfterAndEmailAddressLike( @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_BETWEEN + AND_SUBJECT_TYPE_EQUALS) Page findBySubjectTypeAndCreatedAtBetweenAndEmailAddressLike( AuditSubjectType subjectType, - ZonedDateTime fromDateTime, - ZonedDateTime toDateTime, + ZonedDateTime createdAtAfter, + ZonedDateTime createdAtBefore, String email, Pageable pageable); From d6bcd9dd2fd12d6de89f7c0b6cc1eaa50c006b07 Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Mon, 29 Jun 2026 10:42:37 +0100 Subject: [PATCH 37/40] whitespace --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 30494e45b..67b8fd620 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # FaCT Data API + [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=uk.gov.hmcts.reform%3Afact-data-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=uk.gov.hmcts.reform%3Afact-data-api) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=uk.gov.hmcts.reform%3Afact-data-api&metric=coverage)](https://sonarcloud.io/summary/new_code?id=uk.gov.hmcts.reform%3Afact-data-api) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=uk.gov.hmcts.reform%3Afact-data-api&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=uk.gov.hmcts.reform%3Afact-data-api) From 098ab4bf097779e2ac8a2229ec1508fb65484b6d Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Mon, 29 Jun 2026 15:54:00 +0100 Subject: [PATCH 38/40] whitespace --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 67b8fd620..30494e45b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # FaCT Data API - [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=uk.gov.hmcts.reform%3Afact-data-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=uk.gov.hmcts.reform%3Afact-data-api) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=uk.gov.hmcts.reform%3Afact-data-api&metric=coverage)](https://sonarcloud.io/summary/new_code?id=uk.gov.hmcts.reform%3Afact-data-api) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=uk.gov.hmcts.reform%3Afact-data-api&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=uk.gov.hmcts.reform%3Afact-data-api) From ffc91c86de1ac1b0fa169d9567940fac1b754456 Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Wed, 1 Jul 2026 11:44:33 +0100 Subject: [PATCH 39/40] whitespace --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 30494e45b..67b8fd620 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # FaCT Data API + [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=uk.gov.hmcts.reform%3Afact-data-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=uk.gov.hmcts.reform%3Afact-data-api) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=uk.gov.hmcts.reform%3Afact-data-api&metric=coverage)](https://sonarcloud.io/summary/new_code?id=uk.gov.hmcts.reform%3Afact-data-api) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=uk.gov.hmcts.reform%3Afact-data-api&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=uk.gov.hmcts.reform%3Afact-data-api) From a41dde7d618be4112efdb3cf9b88f37b2d36d7e2 Mon Sep 17 00:00:00 2001 From: hurricanepilot Date: Wed, 1 Jul 2026 12:00:19 +0100 Subject: [PATCH 40/40] Add @Synchronized around creation/update of test user --- .../reform/fact/data/api/services/TestingSupportService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java index b67d1a75d..748709702 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/TestingSupportService.java @@ -387,6 +387,7 @@ private ServiceCentre createServiceCentre(String name, UUID regionId, boolean ad return serviceCentreService.createServiceCentre(serviceCentre); } + @Synchronized private UUID getTestingSupportUserId() { User user = User.builder() .email(TESTING_SUPPORT_USER_EMAIL)