From 29ac061e0231fd6539eba0787bebc836c11dc06e Mon Sep 17 00:00:00 2001 From: Tim Harrison Date: Fri, 28 Feb 2025 08:41:41 +0000 Subject: [PATCH 1/2] PRC-500: Reconciliation endpoint for sync checks --- .../organisationsapi/facade/SyncFacade.kt | 3 ++ .../mapping/sync/SyncOrganisationMappers.kt | 6 ++++ .../model/response/sync/SyncOrganisationId.kt | 9 ++++++ .../sync/SyncOrganisationController.kt | 27 ++++++++++++++++++ .../service/sync/SyncOrganisationService.kt | 7 +++++ .../integration/helper/TestAPIClient.kt | 26 +++++++++++++++++ .../OrganisationSearchIntegrationTest.kt | 2 +- .../sync/SyncOrganisationsIntegrationTest.kt | 28 +++++++++++++++++++ 8 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/response/sync/SyncOrganisationId.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/facade/SyncFacade.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/facade/SyncFacade.kt index e59b833..50ae164 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/facade/SyncFacade.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/facade/SyncFacade.kt @@ -1,5 +1,6 @@ package uk.gov.justice.digital.hmpps.organisationsapi.facade +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncCreateAddressPhoneRequest import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncCreateAddressRequest @@ -90,6 +91,8 @@ class SyncFacade( ) } + fun getIds(pageable: Pageable) = syncOrganisationService.getOrganisationIds(pageable) + // ================================================================ // Organisation phone numbers // ================================================================ diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/mapping/sync/SyncOrganisationMappers.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/mapping/sync/SyncOrganisationMappers.kt index 3f6a812..71c32e0 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/mapping/sync/SyncOrganisationMappers.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/mapping/sync/SyncOrganisationMappers.kt @@ -1,7 +1,9 @@ package uk.gov.justice.digital.hmpps.organisationsapi.mapping.sync +import org.springframework.data.domain.Page import uk.gov.justice.digital.hmpps.organisationsapi.entity.OrganisationWithFixedIdEntity import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncCreateOrganisationRequest +import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncOrganisationId import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncOrganisationResponse fun OrganisationWithFixedIdEntity.toModel(): SyncOrganisationResponse = SyncOrganisationResponse( @@ -35,3 +37,7 @@ fun SyncCreateOrganisationRequest.toEntity() = OrganisationWithFixedIdEntity( updatedBy = this.updatedBy, updatedTime = this.updatedTime, ) + +fun OrganisationWithFixedIdEntity.toModelIds(): SyncOrganisationId = SyncOrganisationId(organisationId = this.organisationId) + +fun Page.toModelIds(): Page = map { it.toModelIds() } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/response/sync/SyncOrganisationId.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/response/sync/SyncOrganisationId.kt new file mode 100644 index 0000000..8b337a0 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/response/sync/SyncOrganisationId.kt @@ -0,0 +1,9 @@ +package uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Response object for email address changes via sync") +data class SyncOrganisationId( + @Schema(description = "The ID for an organisation", example = "111111") + val organisationId: Long, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/resource/sync/SyncOrganisationController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/resource/sync/SyncOrganisationController.kt index af0a6aa..5d21722 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/resource/sync/SyncOrganisationController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/resource/sync/SyncOrganisationController.kt @@ -9,6 +9,11 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid +import org.springdoc.core.annotations.ParameterObject +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort.Direction +import org.springframework.data.web.PageableDefault import org.springframework.http.MediaType import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.DeleteMapping @@ -23,6 +28,7 @@ import org.springframework.web.bind.annotation.RestController import uk.gov.justice.digital.hmpps.organisationsapi.facade.SyncFacade import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncCreateOrganisationRequest import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncUpdateOrganisationRequest +import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncOrganisationId import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncOrganisationResponse import uk.gov.justice.digital.hmpps.organisationsapi.swagger.AuthApiResponses import uk.gov.justice.hmpps.kotlin.common.ErrorResponse @@ -174,4 +180,25 @@ class SyncOrganisationController(val syncFacade: SyncFacade) { @PathVariable organisationId: Long, @Valid @RequestBody updateOrganisationRequest: SyncUpdateOrganisationRequest, ) = syncFacade.updateOrganisation(organisationId, updateOrganisationRequest) + + @GetMapping("/organisations/reconcile") + @Operation( + summary = "Reconciliation endpoint", + description = "Get a paged list of existing organisation IDs to reconcile against", + security = [SecurityRequirement(name = "bearer")], + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "Pageable organisation IDs returned", + ), + ], + ) + @PreAuthorize("hasAnyRole('ROLE_ORGANISATIONS_MIGRATION')") + fun reconcileOrganisations( + @ParameterObject + @PageableDefault(sort = ["organisationId"], size = 100, direction = Direction.ASC) + pageable: Pageable, + ): Page = syncFacade.getIds(pageable) } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/sync/SyncOrganisationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/sync/SyncOrganisationService.kt index 99b4546..db19adc 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/sync/SyncOrganisationService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/sync/SyncOrganisationService.kt @@ -2,12 +2,16 @@ package uk.gov.justice.digital.hmpps.organisationsapi.service.sync import jakarta.persistence.EntityNotFoundException import org.slf4j.LoggerFactory +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import uk.gov.justice.digital.hmpps.organisationsapi.mapping.sync.toEntity import uk.gov.justice.digital.hmpps.organisationsapi.mapping.sync.toModel +import uk.gov.justice.digital.hmpps.organisationsapi.mapping.sync.toModelIds import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncCreateOrganisationRequest import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncUpdateOrganisationRequest +import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncOrganisationId import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncOrganisationResponse import uk.gov.justice.digital.hmpps.organisationsapi.repository.OrganisationWithFixedIdRepository @@ -78,4 +82,7 @@ class SyncOrganisationService( return organisationRepository.saveAndFlush(changedOrganisation).toModel() } + + fun getOrganisationIds(pageable: Pageable): Page = + organisationRepository.findAll(pageable).toModelIds() } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/integration/helper/TestAPIClient.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/integration/helper/TestAPIClient.kt index 9c20e7b..a515d33 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/integration/helper/TestAPIClient.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/integration/helper/TestAPIClient.kt @@ -19,6 +19,7 @@ import uk.gov.justice.digital.hmpps.organisationsapi.model.response.migrate.Migr import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncAddressPhoneResponse import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncAddressResponse import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncEmailResponse +import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncOrganisationId import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncOrganisationResponse import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncPhoneResponse import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncWebResponse @@ -162,6 +163,17 @@ class TestAPIClient(private val webTestClient: WebTestClient, private val jwtAut .expectBody(SyncWebResponse::class.java) .returnResult().responseBody!! + fun syncReconcileOrganisations(page: Long = 0, size: Long = 10) = webTestClient.get() + .uri("/sync/organisations/reconcile?page=$page&size=$size") + .accept(MediaType.APPLICATION_JSON) + .headers(setAuthorisation(roles = listOf("ROLE_ORGANISATIONS_MIGRATION"))) + .exchange() + .expectStatus() + .isOk + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectBody(OrganisationIdsResponse::class.java) + .returnResult().responseBody!! + fun getBadResponseErrors(uri: URI) = webTestClient.get() .uri(uri.toString()) .accept(MediaType.APPLICATION_JSON) @@ -225,4 +237,18 @@ class TestAPIClient(private val webTestClient: WebTestClient, private val jwtAut val unsorted: Boolean, val sorted: Boolean, ) + + data class OrganisationIdsResponse( + val content: List, + val pageable: ReturnedPageable, + val last: Boolean, + val totalPages: Int, + val totalElements: Int, + val first: Boolean, + val size: Int, + val number: Int, + val sort: ReturnedSort, + val numberOfElements: Int, + val empty: Boolean, + ) } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/integration/resource/OrganisationSearchIntegrationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/integration/resource/OrganisationSearchIntegrationTest.kt index ad8d67b..01b8741 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/integration/resource/OrganisationSearchIntegrationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/integration/resource/OrganisationSearchIntegrationTest.kt @@ -41,7 +41,7 @@ class OrganisationSearchIntegrationTest : SecureApiIntegrationTestBase() { } @ParameterizedTest - @ValueSource(strings = ["ROLE_ORGANISATIONS__R", "ROLE_ORGANISATONS__RW"]) + @ValueSource(strings = ["ROLE_ORGANISATIONS__R", "ROLE_ORGANISATIONS__RW"]) fun `should return empty list if no organisation found and work with all roles`(role: String) { val response = testAPIClient.searchOrganisations(OrganisationSearchRequest("ABC")) assertThat(response.empty).isTrue() diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/integration/resource/sync/SyncOrganisationsIntegrationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/integration/resource/sync/SyncOrganisationsIntegrationTest.kt index 188ac24..c299fe4 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/integration/resource/sync/SyncOrganisationsIntegrationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/integration/resource/sync/SyncOrganisationsIntegrationTest.kt @@ -8,6 +8,7 @@ import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.test.context.TestPropertySource import uk.gov.justice.digital.hmpps.organisationsapi.integration.PostgresIntegrationTestBase +import uk.gov.justice.digital.hmpps.organisationsapi.integration.helper.hasSize import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncCreateOrganisationRequest import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncUpdateOrganisationRequest import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncOrganisationResponse @@ -232,6 +233,33 @@ class SyncOrganisationsIntegrationTest : PostgresIntegrationTestBase() { stubEvents.assertHasNoEvents(OutboundEvent.ORGANISATION_CREATED) } + @Test + fun `should support pageable organisation IDs for reconciliation`() { + testAPIClient.syncCreateAnOrganisation(syncCreateOrganisationRequest(5005L)) + testAPIClient.syncCreateAnOrganisation(syncCreateOrganisationRequest(5006L)) + testAPIClient.syncCreateAnOrganisation(syncCreateOrganisationRequest(5007L)) + + val firstPage = testAPIClient.syncReconcileOrganisations(0, 2) + with(firstPage) { + assertThat(totalElements).isGreaterThanOrEqualTo(3) + assertThat(content.hasSize(2)) + assertThat(content).extracting("organisationId").hasSize(2) + } + + val secondPage = testAPIClient.syncReconcileOrganisations(1, 2) + with(secondPage) { + assertThat(totalElements).isGreaterThanOrEqualTo(3) + assertThat(content.size).isGreaterThanOrEqualTo(1) + } + + val bigPage = testAPIClient.syncReconcileOrganisations(0, 100) + with(bigPage) { + assertThat(totalElements).isGreaterThanOrEqualTo(3) + assertThat(content.size).isGreaterThanOrEqualTo(3) + assertThat(content).extracting("organisationId").containsAll(listOf(5005L, 5006L, 5007L)) + } + } + private fun syncUpdateOrganisationRequest(organisationId: Long) = SyncUpdateOrganisationRequest( organisationId = organisationId, organisationName = "Organisation321", From d6233b46a7848f40f9fc3cccf8c8224f2d0631db Mon Sep 17 00:00:00 2001 From: Tim Harrison Date: Fri, 28 Feb 2025 08:50:50 +0000 Subject: [PATCH 2/2] PRC-500: Correct typo --- .../organisationsapi/model/response/sync/SyncOrganisationId.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/response/sync/SyncOrganisationId.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/response/sync/SyncOrganisationId.kt index 8b337a0..ca25114 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/response/sync/SyncOrganisationId.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/response/sync/SyncOrganisationId.kt @@ -2,7 +2,7 @@ package uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync import io.swagger.v3.oas.annotations.media.Schema -@Schema(description = "Response object for email address changes via sync") +@Schema(description = "Response object for sync reconciliation") data class SyncOrganisationId( @Schema(description = "The ID for an organisation", example = "111111") val organisationId: Long,