Skip to content

Commit 4d89a08

Browse files
PRC-419: Adding sync components for organisation types (#12)
1 parent 84362b6 commit 4d89a08

File tree

15 files changed

+736
-2
lines changed

15 files changed

+736
-2
lines changed

helm_deploy/hmpps-organisations-api/values.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ generic-service:
3939
FEATURE_EVENT_ORGANISATIONS_API_ORGANISATION_ADDRESS_CREATED: true
4040
FEATURE_EVENT_ORGANISATIONS_API_ORGANISATION_ADDRESS_UPDATED: true
4141
FEATURE_EVENT_ORGANISATIONS_API_ORGANISATION_ADDRESS_DELETED: true
42+
FEATURE_EVENT_ORGANISATIONS_API_ORGANISATION_TYPES_UPDATED: true
4243

4344

4445
# Pre-existing kubernetes secrets to load as environment variables in the deployment.

src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/facade/SyncFacade.kt

+19
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncUpda
1010
import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncUpdateEmailRequest
1111
import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncUpdateOrganisationRequest
1212
import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncUpdatePhoneRequest
13+
import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncUpdateTypesRequest
1314
import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncUpdateWebRequest
1415
import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OutboundEvent
1516
import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OutboundEventsService
@@ -18,6 +19,7 @@ import uk.gov.justice.digital.hmpps.organisationsapi.service.sync.SyncAddressSer
1819
import uk.gov.justice.digital.hmpps.organisationsapi.service.sync.SyncEmailService
1920
import uk.gov.justice.digital.hmpps.organisationsapi.service.sync.SyncOrganisationService
2021
import uk.gov.justice.digital.hmpps.organisationsapi.service.sync.SyncPhoneService
22+
import uk.gov.justice.digital.hmpps.organisationsapi.service.sync.SyncTypesService
2123
import uk.gov.justice.digital.hmpps.organisationsapi.service.sync.SyncWebService
2224

2325
/**
@@ -45,6 +47,7 @@ class SyncFacade(
4547
private val syncEmailService: SyncEmailService,
4648
private val syncWebService: SyncWebService,
4749
private val syncAddressService: SyncAddressService,
50+
private val syncTypesService: SyncTypesService,
4851
private val outboundEventsService: OutboundEventsService,
4952
) {
5053
// ================================================================
@@ -226,4 +229,20 @@ class SyncFacade(
226229
source = Source.NOMIS,
227230
)
228231
}
232+
233+
// ================================================================
234+
// Organisation types
235+
// ================================================================
236+
237+
fun getTypesByOrganisationId(organisationId: Long) = syncTypesService.getTypesByOrganisationId(organisationId)
238+
239+
fun updateTypes(organisationId: Long, request: SyncUpdateTypesRequest) = syncTypesService.updateTypes(organisationId, request)
240+
.also {
241+
outboundEventsService.send(
242+
outboundEvent = OutboundEvent.ORGANISATION_TYPES_UPDATED,
243+
organisationId = it.organisationId,
244+
identifier = it.organisationId,
245+
source = Source.NOMIS,
246+
)
247+
}
229248
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package uk.gov.justice.digital.hmpps.organisationsapi.mapping.sync
2+
3+
import uk.gov.justice.digital.hmpps.organisationsapi.entity.OrganisationTypeEntity
4+
import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncOrganisationType
5+
import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncTypesResponse
6+
7+
fun List<OrganisationTypeEntity>.toModel(organisationId: Long) = SyncTypesResponse(
8+
organisationId = organisationId,
9+
types = this.map { each ->
10+
SyncOrganisationType(
11+
type = each.id.organisationType,
12+
createdBy = each.createdBy,
13+
createdTime = each.createdTime,
14+
updatedBy = each.updatedBy,
15+
updatedTime = each.updatedTime,
16+
)
17+
},
18+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync
2+
3+
import io.swagger.v3.oas.annotations.media.Schema
4+
import jakarta.validation.constraints.NotNull
5+
import java.time.LocalDateTime
6+
7+
@Schema(description = "Request to update the list of types assigned to an organisation")
8+
data class SyncUpdateTypesRequest(
9+
10+
@Schema(description = "The organisation ID to update", example = "1233323")
11+
@field:NotNull(message = "The organisation ID must be present in this request")
12+
val organisationId: Long,
13+
14+
@Schema(description = "The list of organisation types (will replace the current types)")
15+
val types: List<SyncOrganisationType> = emptyList(),
16+
)
17+
18+
@Schema(description = "An organisation type")
19+
data class SyncOrganisationType(
20+
@Schema(description = "Organisation type", example = "SUPPLIER")
21+
val type: String,
22+
23+
@Schema(description = "User who created the entry", example = "admin")
24+
val createdBy: String,
25+
26+
@Schema(description = "The created timestamp", example = "2024-01-01T00:00:00Z")
27+
val createdTime: LocalDateTime = LocalDateTime.now(),
28+
29+
@Schema(description = "User who updated the entry", example = "admin")
30+
val updatedBy: String? = null,
31+
32+
@Schema(description = "The updated timestamp", example = "2024-01-01T00:00:00Z")
33+
val updatedTime: LocalDateTime? = null,
34+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync
2+
3+
import io.swagger.v3.oas.annotations.media.Schema
4+
import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncOrganisationType
5+
6+
@Schema(description = "Response object for an organisation types update")
7+
data class SyncTypesResponse(
8+
@Schema(description = "The organisation ID the updates refer to", example = "1234")
9+
val organisationId: Long,
10+
11+
@Schema(description = "The list of types updated on the organisation")
12+
val types: List<SyncOrganisationType>,
13+
)

src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/resource/OrganisationController.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,11 @@ class OrganisationController(private val organisationFacade: OrganisationFacade)
9292
@PreAuthorize("hasAnyRole('ROLE_ORGANISATIONS__R', 'ROLE_ORGANISATIONS__RW')")
9393
fun getOrganisationSummaryById(@PathVariable organisationId: Long): OrganisationSummary = organisationFacade.getOrganisationSummaryById(organisationId)
9494

95+
@Deprecated(message = "This endpoint is deprecated and should not be used to create organisations. Changes made will not be synchronised to NOMIS.")
9596
@PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE])
9697
@Operation(
9798
summary = "Create new organisation",
98-
description = "Creates a new organisation",
99+
description = "Creates a new organisation in DPS but does not currently synchronise to NOMIS. Deprecated until this 2-way sync is in place.",
99100
security = [SecurityRequirement(name = "bearer")],
100101
)
101102
@ApiResponses(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package uk.gov.justice.digital.hmpps.organisationsapi.resource.sync
2+
3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.Parameter
5+
import io.swagger.v3.oas.annotations.media.Content
6+
import io.swagger.v3.oas.annotations.media.Schema
7+
import io.swagger.v3.oas.annotations.responses.ApiResponse
8+
import io.swagger.v3.oas.annotations.responses.ApiResponses
9+
import io.swagger.v3.oas.annotations.security.SecurityRequirement
10+
import io.swagger.v3.oas.annotations.tags.Tag
11+
import jakarta.validation.Valid
12+
import org.springframework.http.MediaType
13+
import org.springframework.security.access.prepost.PreAuthorize
14+
import org.springframework.web.bind.annotation.GetMapping
15+
import org.springframework.web.bind.annotation.PathVariable
16+
import org.springframework.web.bind.annotation.PutMapping
17+
import org.springframework.web.bind.annotation.RequestBody
18+
import org.springframework.web.bind.annotation.RequestMapping
19+
import org.springframework.web.bind.annotation.ResponseBody
20+
import org.springframework.web.bind.annotation.RestController
21+
import uk.gov.justice.digital.hmpps.organisationsapi.facade.SyncFacade
22+
import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncUpdateTypesRequest
23+
import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncTypesResponse
24+
import uk.gov.justice.digital.hmpps.organisationsapi.swagger.AuthApiResponses
25+
26+
@Tag(name = "Migration and synchronisation")
27+
@RestController
28+
@RequestMapping(value = ["sync"], produces = [MediaType.APPLICATION_JSON_VALUE])
29+
@AuthApiResponses
30+
class SyncTypesController(val syncFacade: SyncFacade) {
31+
32+
@GetMapping(path = ["/organisation-types/{organisationId}"], produces = [MediaType.APPLICATION_JSON_VALUE])
33+
@Operation(
34+
summary = "Returns the organisation types for an organisation by ID",
35+
description = """
36+
Requires role: ROLE_ORGANISATIONS_MIGRATION.
37+
Used to get the organisation types for this organisation.
38+
""",
39+
security = [SecurityRequirement(name = "bearer")],
40+
)
41+
@ApiResponses(
42+
value = [
43+
ApiResponse(
44+
responseCode = "200",
45+
description = "Returning the details of the organisation types",
46+
content = [
47+
Content(
48+
mediaType = "application/json",
49+
schema = Schema(implementation = SyncTypesResponse::class),
50+
),
51+
],
52+
),
53+
ApiResponse(
54+
responseCode = "404",
55+
description = "No organisation with the requested ID was found",
56+
),
57+
],
58+
)
59+
@PreAuthorize("hasAnyRole('ROLE_ORGANISATIONS_MIGRATION')")
60+
fun syncGetTypesByOrganisationId(
61+
@Parameter(description = "The internal organisation ID.", required = true)
62+
@PathVariable organisationId: Long,
63+
) = syncFacade.getTypesByOrganisationId(organisationId)
64+
65+
@PutMapping(path = ["/organisation-types/{organisationId}"], produces = [MediaType.APPLICATION_JSON_VALUE])
66+
@ResponseBody
67+
@Operation(
68+
summary = "Updates the organisation types for a given organisation ID",
69+
description = """
70+
Requires role: ROLE_ORGANISATIONS_MIGRATION.
71+
Used to update an organisation's types.
72+
""",
73+
security = [SecurityRequirement(name = "bearer")],
74+
)
75+
@ApiResponses(
76+
value = [
77+
ApiResponse(
78+
responseCode = "200",
79+
description = "Successfully updated the organisation's types",
80+
content = [
81+
Content(
82+
mediaType = "application/json",
83+
schema = Schema(implementation = SyncTypesResponse::class),
84+
),
85+
],
86+
),
87+
ApiResponse(
88+
responseCode = "404",
89+
description = "The organisation ID was not found",
90+
),
91+
ApiResponse(
92+
responseCode = "400",
93+
description = "Invalid request data",
94+
),
95+
],
96+
)
97+
@PreAuthorize("hasAnyRole('ROLE_ORGANISATIONS_MIGRATION')")
98+
fun syncUpdateWeb(
99+
@Parameter(description = "The internal organisation ID.", required = true)
100+
@PathVariable organisationId: Long,
101+
@Valid @RequestBody syncUpdateTypesRequest: SyncUpdateTypesRequest,
102+
) = syncFacade.updateTypes(organisationId, syncUpdateTypesRequest)
103+
}

src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEvents.kt

+7
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ enum class OutboundEvent(val eventType: String) {
112112
description = "An organisation address has been deleted",
113113
)
114114
},
115+
ORGANISATION_TYPES_UPDATED("organisations-api.organisation-types.updated") {
116+
override fun event(additionalInformation: AdditionalInformation) = OutboundHMPPSDomainEvent(
117+
eventType = eventType,
118+
additionalInformation = additionalInformation,
119+
description = "An organisation has had its types updated",
120+
)
121+
},
115122
;
116123

117124
abstract fun event(additionalInformation: AdditionalInformation): OutboundHMPPSDomainEvent

src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEventsService.kt

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class OutboundEventsService(
3939
OutboundEvent.ORGANISATION_ADDRESS_CREATED,
4040
OutboundEvent.ORGANISATION_ADDRESS_UPDATED,
4141
OutboundEvent.ORGANISATION_ADDRESS_DELETED,
42+
OutboundEvent.ORGANISATION_TYPES_UPDATED,
4243
-> {
4344
sendSafely(outboundEvent, OrganisationInfo(organisationId, identifier, source))
4445
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package uk.gov.justice.digital.hmpps.organisationsapi.service.sync
2+
3+
import jakarta.persistence.EntityNotFoundException
4+
import org.slf4j.LoggerFactory
5+
import org.springframework.stereotype.Service
6+
import org.springframework.transaction.annotation.Transactional
7+
import uk.gov.justice.digital.hmpps.organisationsapi.entity.OrganisationTypeEntity
8+
import uk.gov.justice.digital.hmpps.organisationsapi.entity.OrganisationTypeId
9+
import uk.gov.justice.digital.hmpps.organisationsapi.mapping.sync.toModel
10+
import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncUpdateTypesRequest
11+
import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncTypesResponse
12+
import uk.gov.justice.digital.hmpps.organisationsapi.repository.OrganisationTypeRepository
13+
import uk.gov.justice.digital.hmpps.organisationsapi.repository.OrganisationWithFixedIdRepository
14+
15+
@Service
16+
@Transactional
17+
class SyncTypesService(
18+
val organisationRepository: OrganisationWithFixedIdRepository,
19+
val organisationTypeRepository: OrganisationTypeRepository,
20+
) {
21+
companion object {
22+
private val logger = LoggerFactory.getLogger(this::class.java)
23+
}
24+
25+
@Transactional(readOnly = true)
26+
fun getTypesByOrganisationId(organisationId: Long): SyncTypesResponse {
27+
organisationRepository.findById(organisationId)
28+
.orElseThrow { EntityNotFoundException("Organisation with ID $organisationId not found") }
29+
return organisationTypeRepository.getByIdOrganisationId(organisationId).toModel(organisationId)
30+
}
31+
32+
fun updateTypes(organisationId: Long, request: SyncUpdateTypesRequest): SyncTypesResponse {
33+
organisationRepository.findById(organisationId)
34+
.orElseThrow { EntityNotFoundException("Organisation with ID $organisationId not found") }
35+
36+
val entitiesDeleted = organisationTypeRepository.deleteAllByOrganisationId(organisationId)
37+
logger.info("Organisation types for ID $organisationId: Removed $entitiesDeleted, Create ${request.types.size}")
38+
39+
return if (request.types.isEmpty()) {
40+
SyncTypesResponse(organisationId = organisationId, types = emptyList())
41+
} else {
42+
organisationTypeRepository.saveAll(
43+
request.types.map { each ->
44+
OrganisationTypeEntity(
45+
OrganisationTypeId(
46+
organisationId = organisationId,
47+
organisationType = each.type,
48+
),
49+
createdBy = each.createdBy,
50+
createdTime = each.createdTime,
51+
updatedBy = each.updatedBy,
52+
updatedTime = each.updatedTime,
53+
)
54+
},
55+
).toModel(organisationId)
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)