diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/facade/OrganisationFacade.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/facade/OrganisationFacade.kt index 37928f0..4f35857 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/facade/OrganisationFacade.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/facade/OrganisationFacade.kt @@ -19,6 +19,7 @@ class OrganisationFacade( fun create(request: CreateOrganisationRequest): OrganisationDetails = organisationService.create(request).also { outboundEventsService.send( outboundEvent = OutboundEvent.ORGANISATION_CREATED, + organisationId = it.organisationId, identifier = it.organisationId, ) } 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 new file mode 100644 index 0000000..8c276ad --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/facade/SyncFacade.kt @@ -0,0 +1,69 @@ +package uk.gov.justice.digital.hmpps.organisationsapi.facade + +import org.springframework.stereotype.Service +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.service.events.OutboundEvent +import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OutboundEventsService +import uk.gov.justice.digital.hmpps.organisationsapi.service.events.Source +import uk.gov.justice.digital.hmpps.organisationsapi.service.sync.SyncOrganisationService + +/** + * This class is a facade over the sync services as a thin layer + * which is called by the sync controllers and in-turn calls the sync + * service methods. + * + * Each method provides two purposes: + * - To call the underlying sync services and apply the changes in a transactional method. + * - To generate a domain event to inform subscribed services what has happened. + * + * All events generated as a result of a sync operation should generate domain events with the + * additionalInformation.source = "NOMIS" to indicate that the actual source of the change + * was NOMIS. + * + * This is important as the Syscon sync service will ignore domain events with + * a source of NOMIS, but will action those with a source of DPS for changes which + * originate within this service via the UI or API clients. + */ + +@Service +class SyncFacade( + private val syncService: SyncOrganisationService, + private val outboundEventsService: OutboundEventsService, +) { + // ================================================================ + // Organisations + // ================================================================ + + fun getOrganisationById(organisationId: Long) = syncService.getOrganisationById(organisationId) + + fun createOrganisation(request: SyncCreateOrganisationRequest) = syncService.createOrganisation(request) + .also { + outboundEventsService.send( + outboundEvent = OutboundEvent.ORGANISATION_CREATED, + organisationId = it.organisationId, + identifier = it.organisationId, + source = Source.NOMIS, + ) + } + + fun updateOrganisation(organisationId: Long, request: SyncUpdateOrganisationRequest) = syncService.updateOrganisation(organisationId, request) + .also { + outboundEventsService.send( + outboundEvent = OutboundEvent.ORGANISATION_UPDATED, + organisationId = organisationId, + identifier = organisationId, + source = Source.NOMIS, + ) + } + + fun deleteOrganisation(organisationId: Long) = syncService.deleteOrganisation(organisationId) + .also { + outboundEventsService.send( + outboundEvent = OutboundEvent.ORGANISATION_DELETED, + organisationId = organisationId, + identifier = organisationId, + source = Source.NOMIS, + ) + } +} 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 new file mode 100644 index 0000000..3f6a812 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/mapping/sync/SyncOrganisationMappers.kt @@ -0,0 +1,37 @@ +package uk.gov.justice.digital.hmpps.organisationsapi.mapping.sync + +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.SyncOrganisationResponse + +fun OrganisationWithFixedIdEntity.toModel(): SyncOrganisationResponse = SyncOrganisationResponse( + organisationId = this.id(), + organisationName = this.organisationName, + programmeNumber = this.programmeNumber, + vatNumber = this.vatNumber, + caseloadId = this.caseloadId, + comments = this.comments, + active = this.active, + deactivatedDate = this.deactivatedDate, + createdBy = this.createdBy, + createdTime = this.createdTime, + updatedBy = this.updatedBy, + updatedTime = this.updatedTime, +) + +fun List.toModel(): List = map { it.toModel() } + +fun SyncCreateOrganisationRequest.toEntity() = OrganisationWithFixedIdEntity( + organisationId = this.organisationId, + organisationName = this.organisationName, + programmeNumber = this.programmeNumber, + vatNumber = this.vatNumber, + caseloadId = this.caseloadId, + comments = this.comments, + active = this.active, + deactivatedDate = this.deactivatedDate, + createdBy = this.createdBy, + createdTime = this.createdTime, + updatedBy = this.updatedBy, + updatedTime = this.updatedTime, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/request/sync/SyncCreateOrganisationRequest.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/request/sync/SyncCreateOrganisationRequest.kt new file mode 100644 index 0000000..82d70bb --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/request/sync/SyncCreateOrganisationRequest.kt @@ -0,0 +1,73 @@ +package uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size +import java.time.LocalDate +import java.time.LocalDateTime + +/** + The difference between a SyncCreateOrganisation and a CreateOrganisationRequest is only + that the sync version accepts an Organisation ID field and has less stringent validation, so + it accepts more of the range of values that the sync service provides from NOMIS. + + The standard CreateOrganisationRequest is used in CRUD endpoints intended for use by the DPS + UI service (and other DPS clients) and does not specify an organisation ID because this will be + generated from a DPS range, and also has a higher standard of validation to ensure it captures + more accurate values. + */ +@Schema(description = "Request to create a new organisation") +data class SyncCreateOrganisationRequest( + + @Schema(description = "The organisation ID AKA the corporate ID from NOMIS", example = "1233323") + @field:NotNull(message = "The organisation ID must be present in this request") + val organisationId: Long, + + @Schema(description = "The name of the organisation", example = "Example Limited", maxLength = 40) + @field:Size(max = 40, message = "organisationName must be <= 40 characters") + val organisationName: String, + + @Schema( + description = "The programme number for the organisation, stored as FEI_NUMBER in NOMIS", + example = "1", + maxLength = 40, + nullable = true, + ) + @field:Size(max = 40, message = "programmeNumber must be <= 40 characters") + val programmeNumber: String? = null, + + @Schema(description = "The VAT number for the organisation, if known", example = "123456", maxLength = 12, nullable = true) + @field:Size(max = 12, message = "vatNumber must be <= 12 characters") + val vatNumber: String? = null, + + @Schema( + description = "The id of the caseload for this organisation, this is an agency id in NOMIS", + example = "BXI", + maxLength = 6, + nullable = true, + ) + @field:Size(max = 6, message = "caseloadId must be <= 6 characters") + val caseloadId: String? = null, + + @Schema(description = "Any comments on the organisation", example = "Some additional info", maxLength = 240, nullable = true) + @field:Size(max = 240, message = "comments must be <= 240 characters") + val comments: String? = null, + + @Schema(description = "Whether the organisation is active or not", example = "true") + val active: Boolean = false, + + @Schema(description = "The date the organisation was deactivated, EXPIRY_DATE in NOMIS", example = "2010-12-30", nullable = true) + val deactivatedDate: LocalDate? = null, + + @Schema(description = "User who created the entry", example = "admin") + val createdBy: String, + + @Schema(description = "Timestamp when the entry was created", example = "2023-09-23T10:15:30") + val createdTime: LocalDateTime, + + @Schema(description = "User who updated the entry", example = "admin2") + val updatedBy: String? = null, + + @Schema(description = "Timestamp when the entry was updated", example = "2023-09-24T12:00:00") + val updatedTime: LocalDateTime? = null, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/request/sync/SyncUpdateOrganisationRequest.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/request/sync/SyncUpdateOrganisationRequest.kt new file mode 100644 index 0000000..32eef8c --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/request/sync/SyncUpdateOrganisationRequest.kt @@ -0,0 +1,55 @@ +package uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size +import java.time.LocalDate +import java.time.LocalDateTime + +data class SyncUpdateOrganisationRequest( + @Schema(description = "The organisation ID AKA the corporate ID from NOMIS", example = "1233323") + @field:NotNull(message = "The organisation ID must be present in this request") + val organisationId: Long, + + @Schema(description = "The name of the organisation", example = "Example Limited", maxLength = 40) + @field:Size(max = 40, message = "organisationName must be <= 40 characters") + val organisationName: String, + + @Schema( + description = "The programme number for the organisation, stored as FEI_NUMBER in NOMIS", + example = "1", + maxLength = 40, + nullable = true, + ) + @field:Size(max = 40, message = "programmeNumber must be <= 40 characters") + val programmeNumber: String? = null, + + @Schema(description = "The VAT number for the organisation, if known", example = "123456", maxLength = 12, nullable = true) + @field:Size(max = 12, message = "vatNumber must be <= 12 characters") + val vatNumber: String? = null, + + @Schema( + description = "The id of the caseload for this organisation, this is an agency id in NOMIS", + example = "BXI", + maxLength = 6, + nullable = true, + ) + @field:Size(max = 6, message = "caseloadId must be <= 6 characters") + val caseloadId: String? = null, + + @Schema(description = "Any comments on the organisation", example = "Some additional info", maxLength = 240, nullable = true) + @field:Size(max = 240, message = "comments must be <= 240 characters") + val comments: String? = null, + + @Schema(description = "Whether the organisation is active or not", example = "true") + val active: Boolean = false, + + @Schema(description = "The date the organisation was deactivated, EXPIRY_DATE in NOMIS", example = "2010-12-30", nullable = true) + val deactivatedDate: LocalDate? = null, + + @Schema(description = "User who updated the entry", example = "admin2") + val updatedBy: String, + + @Schema(description = "Timestamp when the entry was updated", example = "2023-09-24T12:00:00") + val updatedTime: LocalDateTime, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/response/sync/SyncOrganisationResponse.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/response/sync/SyncOrganisationResponse.kt new file mode 100644 index 0000000..a3f511b --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/model/response/sync/SyncOrganisationResponse.kt @@ -0,0 +1,43 @@ +package uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate +import java.time.LocalDateTime + +data class SyncOrganisationResponse( + @Schema(description = "The id of the organisation", example = "123456") + val organisationId: Long, + + @Schema(description = "Organisation name", example = "Supplier Services plc", nullable = true) + val organisationName: String? = null, + + @Schema(description = "Programme number", example = "8765", nullable = true) + val programmeNumber: String? = null, + + @Schema(description = "VAT number", example = "GB 55 55 55 55", nullable = true) + val vatNumber: String? = null, + + @Schema(description = "Caseload ID (a specific prison)", example = "HEI", nullable = true) + val caseloadId: String? = null, + + @Schema(description = "Comments related to this organisation", example = "Notes", nullable = true) + val comments: String? = null, + + @Schema(description = "Active flag", example = "true") + var active: Boolean = false, + + @Schema(description = "The date this organisation was deactivated", example = "2019-01-01", nullable = true) + val deactivatedDate: LocalDate? = null, + + @Schema(description = "User who created the organisation", example = "admin") + val createdBy: String, + + @Schema(description = "Timestamp when the organisation was created", example = "2023-09-23T10:15:30") + val createdTime: LocalDateTime, + + @Schema(description = "User who updated the organisation", example = "admin2") + val updatedBy: String? = null, + + @Schema(description = "Timestamp when the organisation was updated", example = "2023-09-24T12:00:00") + val updatedTime: LocalDateTime? = null, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/resource/migrate/MigrateOrganisationController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/resource/migrate/MigrateOrganisationController.kt index 75ba62a..ff4c832 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/resource/migrate/MigrateOrganisationController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/resource/migrate/MigrateOrganisationController.kt @@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import uk.gov.justice.digital.hmpps.organisationsapi.model.request.migrate.MigrateOrganisationRequest import uk.gov.justice.digital.hmpps.organisationsapi.model.response.migrate.MigrateOrganisationResponse -import uk.gov.justice.digital.hmpps.organisationsapi.service.migrate.OrganisationMigrationService +import uk.gov.justice.digital.hmpps.organisationsapi.service.migrate.MigrateOrganisationService import uk.gov.justice.digital.hmpps.organisationsapi.swagger.AuthApiResponses import uk.gov.justice.hmpps.kotlin.common.ErrorResponse @@ -24,7 +24,7 @@ import uk.gov.justice.hmpps.kotlin.common.ErrorResponse @RestController @RequestMapping(value = ["migrate/organisation"], produces = [MediaType.APPLICATION_JSON_VALUE]) @AuthApiResponses -class MigrateOrganisationController(val migrationService: OrganisationMigrationService) { +class MigrateOrganisationController(val migrationService: MigrateOrganisationService) { @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE]) @Operation( 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 new file mode 100644 index 0000000..6660098 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/resource/sync/SyncOrganisationController.kt @@ -0,0 +1,173 @@ +package uk.gov.justice.digital.hmpps.organisationsapi.resource.sync + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +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.springframework.http.MediaType +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 org.springframework.web.bind.annotation.ResponseBody +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.SyncOrganisationResponse +import uk.gov.justice.digital.hmpps.organisationsapi.swagger.AuthApiResponses +import uk.gov.justice.hmpps.kotlin.common.ErrorResponse + +@Tag(name = "Sync & Migrate") +@RestController +@RequestMapping(value = ["sync"], produces = [MediaType.APPLICATION_JSON_VALUE]) +@AuthApiResponses +class SyncOrganisationController(val syncFacade: SyncFacade) { + + @GetMapping(path = ["/organisation/{organisationId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + @Operation( + summary = "Returns the data for one organisation by organisationId", + description = """ + Requires role: ROLE_ORGANISATIONS_MIGRATION. + Used to get the details for one organisation. + """, + security = [SecurityRequirement(name = "bearer")], + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "Returning the details of the organisation", + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = SyncOrganisationResponse::class), + ), + ], + ), + ApiResponse( + responseCode = "404", + description = "No organisation with the requested ID was found", + ), + ], + ) + @PreAuthorize("hasAnyRole('ROLE_ORGANISATIONS_MIGRATION')") + fun syncGetOrganisationById( + @Parameter(description = "The internal ID for an organisation.", required = true) + @PathVariable organisationId: Long, + ) = syncFacade.getOrganisationById(organisationId) + + @DeleteMapping(path = ["/organisation/{organisationId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + @Operation( + summary = "Deletes one organisation by internal ID", + description = """ + Requires role: ROLE_ORGANISATIONS_MIGRATION. + Used to delete an organisation. + """, + security = [SecurityRequirement(name = "bearer")], + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "204", + description = "Successfully deleted the organisation", + ), + ApiResponse( + responseCode = "404", + description = "No organisation with the requested ID was found", + ), + ], + ) + @PreAuthorize("hasAnyRole('ROLE_ORGANISATIONS_MIGRATION')") + fun syncDeleteOrganisationById( + @Parameter(description = "The internal ID for the organisation.", required = true) + @PathVariable organisationId: Long, + ) = syncFacade.deleteOrganisation(organisationId) + + @PostMapping(path = ["/organisation"], produces = [MediaType.APPLICATION_JSON_VALUE]) + @ResponseBody + @Operation( + summary = "Creates a new organisation", + description = """ + Requires role: ROLE_ORGANISATIONS_MIGRATION. + Used to create a new organisation. + """, + security = [SecurityRequirement(name = "bearer")], + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "201", + description = "Successfully created the organisation", + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = SyncOrganisationResponse::class), + ), + ], + ), + ApiResponse( + responseCode = "400", + description = "The request has invalid or missing fields", + content = [Content(schema = Schema(implementation = ErrorResponse::class))], + ), + ApiResponse( + responseCode = "409", + description = "Conflict. The organisation ID provided in the request already exists", + content = [Content(schema = Schema(implementation = ErrorResponse::class))], + ), + ], + ) + @PreAuthorize("hasAnyRole('ROLE_ORGANISATIONS_MIGRATION')") + fun syncCreateOrganisation( + @Valid @RequestBody createOrganisationRequest: SyncCreateOrganisationRequest, + ) = syncFacade.createOrganisation(createOrganisationRequest) + + @PutMapping(path = ["/organisation/{organisationId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + @ResponseBody + @Operation( + summary = "Updates an organisation with altereed or additional details", + description = """ + Requires role: ROLE_ORGANISATIONS_MIGRATION. + Used to update an organisation. + """, + security = [SecurityRequirement(name = "bearer")], + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "Successfully updated the organisation", + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = SyncOrganisationResponse::class), + ), + ], + ), + ApiResponse( + responseCode = "404", + description = "The organisation was not found", + ), + ApiResponse( + responseCode = "400", + description = "Invalid request data", + ), + ], + ) + @PreAuthorize("hasAnyRole('ROLE_ORGANISATIONS_MIGRATION')") + fun syncUpdateOrganisation( + @Parameter(description = "The internal ID for the organisation.", required = true) + @PathVariable organisationId: Long, + @Valid @RequestBody updateOrganisationRequest: SyncUpdateOrganisationRequest, + ) = syncFacade.updateOrganisation(organisationId, updateOrganisationRequest) +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEvents.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEvents.kt index 6747787..a67e6fc 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEvents.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEvents.kt @@ -56,7 +56,7 @@ data class OutboundHMPPSDomainEvent( * All inherit the base class AdditionalInformation and extend it to contain the required fields. * The additional information is mapped into JSON by the ObjectMapper as part of the event body. */ -data class OrganisationInfo(val organisationId: Long, override val source: Source = Source.DPS) : AdditionalInformation(source) +data class OrganisationInfo(val organisationId: Long, val identifier: Long, override val source: Source = Source.DPS) : AdditionalInformation(source) /** * The event source. diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEventsService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEventsService.kt index 9550efd..f665a5d 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEventsService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEventsService.kt @@ -16,6 +16,7 @@ class OutboundEventsService( fun send( outboundEvent: OutboundEvent, + organisationId: Long, identifier: Long, source: Source = Source.DPS, ) { @@ -27,7 +28,7 @@ class OutboundEventsService( OutboundEvent.ORGANISATION_UPDATED, OutboundEvent.ORGANISATION_DELETED, -> { - sendSafely(outboundEvent, OrganisationInfo(identifier, source)) + sendSafely(outboundEvent, OrganisationInfo(organisationId, identifier, source)) } } } else { diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/migrate/OrganisationMigrationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/migrate/MigrateOrganisationService.kt similarity index 99% rename from src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/migrate/OrganisationMigrationService.kt rename to src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/migrate/MigrateOrganisationService.kt index c2ba8ae..358957d 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/migrate/OrganisationMigrationService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/migrate/MigrateOrganisationService.kt @@ -30,7 +30,7 @@ import uk.gov.justice.digital.hmpps.organisationsapi.repository.OrganisationWith import java.time.LocalDateTime @Service -class OrganisationMigrationService( +class MigrateOrganisationService( private val organisationRepository: OrganisationWithFixedIdRepository, private val organisationTypeRepository: OrganisationTypeRepository, private val organisationPhoneRepository: OrganisationPhoneRepository, diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/sync/DuplicateOrganisationException.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/sync/DuplicateOrganisationException.kt new file mode 100644 index 0000000..3d8cb63 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/sync/DuplicateOrganisationException.kt @@ -0,0 +1,3 @@ +package uk.gov.justice.digital.hmpps.organisationsapi.service.sync + +class DuplicateOrganisationException(message: String) : RuntimeException(message) 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 new file mode 100644 index 0000000..99b4546 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/sync/SyncOrganisationService.kt @@ -0,0 +1,81 @@ +package uk.gov.justice.digital.hmpps.organisationsapi.service.sync + +import jakarta.persistence.EntityNotFoundException +import org.slf4j.LoggerFactory +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.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 +import uk.gov.justice.digital.hmpps.organisationsapi.repository.OrganisationWithFixedIdRepository + +@Service +@Transactional +class SyncOrganisationService( + val organisationRepository: OrganisationWithFixedIdRepository, +) { + companion object { + private val logger = LoggerFactory.getLogger(this::class.java) + } + + @Transactional(readOnly = true) + fun getOrganisationById(organisationId: Long): SyncOrganisationResponse { + val orgWithFixedIdEntity = organisationRepository.findById(organisationId) + .orElseThrow { EntityNotFoundException("Organisation with ID $organisationId not found") } + return orgWithFixedIdEntity.toModel() + } + + /** + * A delete via sync will attempt to remove the organisation only. + * If there are still sub-elements like addresses, types, phones or emails + * this operation will fail. + */ + + fun deleteOrganisation(organisationId: Long): SyncOrganisationResponse { + val orgWithFixedIdEntity = organisationRepository.findById(organisationId) + .orElseThrow { EntityNotFoundException("Organisation with ID $organisationId not found") } + organisationRepository.deleteById(organisationId) + return orgWithFixedIdEntity.toModel() + } + + /** + * Creation of an organisation via sync will accept the NOMIS corporate_id and use this as + * the primary key (organisation_id) in the organisation database. There are two different sequence + * ranges for organisation_id - one for those created in NOMIS and another for those created + * in DPS. The ranges cannot overlap. + */ + fun createOrganisation(request: SyncCreateOrganisationRequest): SyncOrganisationResponse { + if (organisationRepository.existsById(request.organisationId)) { + val message = "Sync: Duplicate organisation ID received ${request.organisationId}" + logger.error(message) + throw DuplicateOrganisationException(message) + } + return organisationRepository.saveAndFlush(request.toEntity()).toModel() + } + + /** + * Updates via sync will receive the whole details again from NOMIS, so update + * all columns with the values provided. This is not a PATCH type update. + */ + + fun updateOrganisation(organisationId: Long, request: SyncUpdateOrganisationRequest): SyncOrganisationResponse { + val orgWithFixedIdEntity = organisationRepository.findById(organisationId) + .orElseThrow { EntityNotFoundException("Organisation with ID $organisationId not found") } + + val changedOrganisation = orgWithFixedIdEntity.copy( + organisationName = request.organisationName, + programmeNumber = request.programmeNumber, + vatNumber = request.vatNumber, + caseloadId = request.caseloadId, + comments = request.comments, + active = request.active, + deactivatedDate = request.deactivatedDate, + updatedBy = request.updatedBy, + updatedTime = request.updatedTime, + ) + + return organisationRepository.saveAndFlush(changedOrganisation).toModel() + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/facade/SyncFacadeTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/facade/SyncFacadeTest.kt new file mode 100644 index 0000000..06ab9b4 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/facade/SyncFacadeTest.kt @@ -0,0 +1,169 @@ +package uk.gov.justice.digital.hmpps.organisationsapi.facade + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever +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 +import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OutboundEvent +import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OutboundEventsService +import uk.gov.justice.digital.hmpps.organisationsapi.service.events.Source +import uk.gov.justice.digital.hmpps.organisationsapi.service.sync.SyncOrganisationService +import java.time.LocalDateTime + +class SyncFacadeTest { + private val syncOrganisationService: SyncOrganisationService = mock() + private val outboundEventsService: OutboundEventsService = mock() + + private val facade = SyncFacade( + syncOrganisationService, + outboundEventsService, + ) + + @Nested + inner class SyncOrganisationFacadeEvents { + @Test + fun `should send ORGANISATION_CREATED domain event on create success`() { + val request = syncCreateOrganisationRequest(organisationId = 1L) + val response = syncOrganisationResponse(1L) + + whenever(syncOrganisationService.createOrganisation(any())).thenReturn(response) + whenever(outboundEventsService.send(any(), any(), any(), any())).then {} + + val result = facade.createOrganisation(request) + + assertThat(result.organisationId).isEqualTo(request.organisationId) + + verify(syncOrganisationService).createOrganisation(request) + verify(outboundEventsService).send( + outboundEvent = OutboundEvent.ORGANISATION_CREATED, + identifier = result.organisationId, + organisationId = result.organisationId, + source = Source.NOMIS, + ) + } + + @Test + fun `should not send ORGANISATION_CREATED domain event on create failure`() { + val request = syncCreateOrganisationRequest(organisationId = 2L) + val expectedException = RuntimeException("Bang!") + + whenever(syncOrganisationService.createOrganisation(any())).thenThrow(expectedException) + whenever(outboundEventsService.send(any(), any(), any(), any())).then {} + + val exception = assertThrows { + facade.createOrganisation(request) + } + + assertThat(exception.message).isEqualTo(expectedException.message) + + verify(syncOrganisationService).createOrganisation(request) + verify(outboundEventsService, never()).send(any(), any(), any(), any()) + } + + @Test + fun `should send ORGANISATION_UPDATED domain event on update success`() { + val request = syncUpdateOrganisationRequest(organisationId = 3L) + val response = syncOrganisationResponse(3L) + + whenever(syncOrganisationService.updateOrganisation(any(), any())).thenReturn(response) + whenever(outboundEventsService.send(any(), any(), any(), any())).then {} + + val result = facade.updateOrganisation(3L, request) + + verify(syncOrganisationService).updateOrganisation(3L, request) + verify(outboundEventsService).send( + outboundEvent = OutboundEvent.ORGANISATION_UPDATED, + identifier = result.organisationId, + organisationId = result.organisationId, + source = Source.NOMIS, + ) + } + + @Test + fun `should not send ORGANISATION_UPDATED domain event on update failure`() { + val request = syncUpdateOrganisationRequest(organisationId = 4L) + val expectedException = RuntimeException("Bang!") + + whenever(syncOrganisationService.updateOrganisation(any(), any())).thenThrow(expectedException) + whenever(outboundEventsService.send(any(), any(), any(), any())).then {} + + val exception = assertThrows { + facade.updateOrganisation(4L, request) + } + + assertThat(exception.message).isEqualTo(expectedException.message) + + verify(syncOrganisationService).updateOrganisation(4L, request) + verify(outboundEventsService, never()).send(any(), any(), any(), any()) + } + + @Test + fun `should send ORGANISATION_DELETED domain event on delete success`() { + val response = syncOrganisationResponse(5L) + whenever(syncOrganisationService.deleteOrganisation(any())).thenReturn(response) + whenever(outboundEventsService.send(any(), any(), any(), any())).then {} + + val result = facade.deleteOrganisation(5L) + + verify(syncOrganisationService).deleteOrganisation(5L) + + verify(outboundEventsService).send( + outboundEvent = OutboundEvent.ORGANISATION_DELETED, + identifier = result.organisationId, + organisationId = result.organisationId, + source = Source.NOMIS, + ) + } + + @Test + fun `should not send ORGANISATION_DELETED on delete failure`() { + val expectedException = RuntimeException("Bang!") + + whenever(syncOrganisationService.deleteOrganisation(any())).thenThrow(expectedException) + whenever(outboundEventsService.send(any(), any(), any(), any())).then {} + + val exception = assertThrows { + facade.deleteOrganisation(6L) + } + + assertThat(exception.message).isEqualTo(expectedException.message) + verify(syncOrganisationService).deleteOrganisation(6L) + verify(outboundEventsService, never()).send(any(), any(), any(), any()) + } + + private fun syncCreateOrganisationRequest(organisationId: Long) = SyncCreateOrganisationRequest( + organisationId = organisationId, + organisationName = "Some Organisation", + active = true, + createdTime = LocalDateTime.now(), + createdBy = "CREATOR", + ) + + private fun syncOrganisationResponse(organisationId: Long) = SyncOrganisationResponse( + organisationId = organisationId, + organisationName = "Some Organisation", + active = true, + createdBy = "CREATOR", + createdTime = LocalDateTime.now(), + updatedBy = null, + updatedTime = null, + ) + + private fun syncUpdateOrganisationRequest(organisationId: Long) = SyncUpdateOrganisationRequest( + organisationId = organisationId, + organisationName = "Some Organisation", + vatNumber = "GB11111111", + active = true, + updatedBy = "UPDATER", + updatedTime = LocalDateTime.now(), + ) + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEventsServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEventsServiceTest.kt index c3f61a3..fae22d2 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEventsServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/events/OutboundEventsServiceTest.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.organisationsapi.integration.service.events +package uk.gov.justice.digital.hmpps.organisationsapi.service.events import org.assertj.core.api.Assertions.within import org.assertj.core.api.AssertionsForClassTypes.assertThat @@ -14,13 +14,6 @@ import org.mockito.kotlin.stub import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import uk.gov.justice.digital.hmpps.organisationsapi.config.FeatureSwitches -import uk.gov.justice.digital.hmpps.organisationsapi.service.events.AdditionalInformation -import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OrganisationInfo -import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OutboundEvent -import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OutboundEventsPublisher -import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OutboundEventsService -import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OutboundHMPPSDomainEvent -import uk.gov.justice.digital.hmpps.organisationsapi.service.events.Source import java.time.LocalDateTime import java.time.temporal.ChronoUnit @@ -33,10 +26,14 @@ class OutboundEventsServiceTest { @Test fun `organisation created event with id 1 is sent to the events publisher`() { featureSwitches.stub { on { isEnabled(OutboundEvent.ORGANISATION_CREATED) } doReturn true } - outboundEventsService.send(OutboundEvent.ORGANISATION_CREATED, 1L) + outboundEventsService.send(OutboundEvent.ORGANISATION_CREATED, 1L, 1L) verify( expectedEventType = "organisations-api.organisation.created", - expectedAdditionalInformation = OrganisationInfo(organisationId = 1L, source = Source.DPS), + expectedAdditionalInformation = OrganisationInfo( + organisationId = 1L, + identifier = 1L, + source = Source.DPS, + ), expectedDescription = "An organisation has been created", ) } @@ -44,10 +41,14 @@ class OutboundEventsServiceTest { @Test fun `organisation updated event with id 1 is sent to the events publisher`() { featureSwitches.stub { on { isEnabled(OutboundEvent.ORGANISATION_UPDATED) } doReturn true } - outboundEventsService.send(OutboundEvent.ORGANISATION_UPDATED, 1L) + outboundEventsService.send(OutboundEvent.ORGANISATION_UPDATED, 1L, 1L) verify( expectedEventType = "organisations-api.organisation.updated", - expectedAdditionalInformation = OrganisationInfo(organisationId = 1L, source = Source.DPS), + expectedAdditionalInformation = OrganisationInfo( + organisationId = 1L, + identifier = 1L, + source = Source.DPS, + ), expectedDescription = "An organisation has been updated", ) } @@ -55,10 +56,14 @@ class OutboundEventsServiceTest { @Test fun `organisation deleted event with id 1 is sent to the events publisher`() { featureSwitches.stub { on { isEnabled(OutboundEvent.ORGANISATION_DELETED) } doReturn true } - outboundEventsService.send(OutboundEvent.ORGANISATION_DELETED, 1L) + outboundEventsService.send(OutboundEvent.ORGANISATION_DELETED, 1L, 1L) verify( expectedEventType = "organisations-api.organisation.deleted", - expectedAdditionalInformation = OrganisationInfo(organisationId = 1L, source = Source.DPS), + expectedAdditionalInformation = OrganisationInfo( + organisationId = 1L, + identifier = 1L, + source = Source.DPS, + ), expectedDescription = "An organisation has been deleted", ) } @@ -68,7 +73,7 @@ class OutboundEventsServiceTest { fun `should trap exception sending event`(event: OutboundEvent) { featureSwitches.stub { on { isEnabled(event) } doReturn true } whenever(eventsPublisher.send(any())).thenThrow(RuntimeException("Boom!")) - outboundEventsService.send(event, 1L) + outboundEventsService.send(event, 1L, 1L) verify(eventsPublisher).send(any()) } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/sync/SyncOrganisationServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/sync/SyncOrganisationServiceTest.kt new file mode 100644 index 0000000..016318f --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/service/sync/SyncOrganisationServiceTest.kt @@ -0,0 +1,209 @@ +package uk.gov.justice.digital.hmpps.organisationsapi.service.sync + +import jakarta.persistence.EntityNotFoundException +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import uk.gov.justice.digital.hmpps.organisationsapi.entity.OrganisationWithFixedIdEntity +import uk.gov.justice.digital.hmpps.organisationsapi.mapping.sync.toEntity +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.repository.OrganisationWithFixedIdRepository +import java.time.LocalDateTime +import java.util.Optional + +class SyncOrganisationServiceTest { + private val orgWithFixedIdRepository: OrganisationWithFixedIdRepository = mock() + private val syncService = SyncOrganisationService(orgWithFixedIdRepository) + + @Nested + inner class SyncOrganisationTests { + @Test + fun `should get an organisation by ID`() { + whenever(orgWithFixedIdRepository.findById(1L)).thenReturn(Optional.of(orgWithFixedIdEntity(1L))) + val organisation = syncService.getOrganisationById(1L) + with(organisation) { + assertThat(organisationId).isEqualTo(1L) + assertThat(organisationName).isEqualTo("Some Organisation") + assertThat(programmeNumber).isEqualTo("1234") + assertThat(vatNumber).isEqualTo("GB11111111") + assertThat(comments).isEqualTo("comment") + assertThat(active).isTrue + assertThat(createdBy).isEqualTo("CREATOR") + } + verify(orgWithFixedIdRepository).findById(1L) + } + + @Test + fun `should throw EntityNotFoundException if the ID is not found`() { + whenever(orgWithFixedIdRepository.findById(1L)).thenReturn(Optional.empty()) + assertThrows { + syncService.getOrganisationById(1L) + } + verify(orgWithFixedIdRepository).findById(1L) + } + + @Test + fun `should create a contact`() { + val request = syncCreateOrganisationRequest(2L) + + whenever(orgWithFixedIdRepository.existsById(2L)).thenReturn(false) + whenever(orgWithFixedIdRepository.saveAndFlush(request.toEntity())).thenReturn(request.toEntity()) + + val organisation = syncService.createOrganisation(request) + + val captor = argumentCaptor() + verify(orgWithFixedIdRepository).saveAndFlush(captor.capture()) + + with(captor.firstValue) { + assertThat(organisationId).isEqualTo(request.organisationId) + assertThat(organisationName).isEqualTo(request.organisationName) + assertThat(vatNumber).isEqualTo(request.vatNumber) + assertThat(active).isEqualTo(request.active) + assertThat(createdBy).isEqualTo(request.createdBy) + } + + with(organisation) { + assertThat(organisationId).isEqualTo(request.organisationId) + assertThat(organisationName).isEqualTo(request.organisationName) + assertThat(vatNumber).isEqualTo(request.vatNumber) + assertThat(active).isEqualTo(request.active) + assertThat(createdBy).isEqualTo(request.createdBy) + } + } + + @Test + fun `should fail to create an organisation if the ID already exists`() { + whenever(orgWithFixedIdRepository.existsById(3L)).thenReturn(true) + val exceptionExpected = DuplicateOrganisationException("Sync: Duplicate organisation ID received 3") + val request = syncCreateOrganisationRequest(3L) + val exceptionThrown = assertThrows { + syncService.createOrganisation(request) + } + assertThat(exceptionThrown.message).isEqualTo(exceptionExpected.message) + assertThat(exceptionThrown.javaClass).isEqualTo(exceptionExpected.javaClass) + verify(orgWithFixedIdRepository, never()).saveAndFlush(any()) + } + + @Test + fun `should delete an organisation by ID`() { + whenever(orgWithFixedIdRepository.findById(4L)).thenReturn(Optional.of(orgWithFixedIdEntity(4L))) + val deleted = syncService.deleteOrganisation(4L) + with(deleted) { + assertThat(organisationId).isEqualTo(4L) + assertThat(organisationName).isEqualTo("Some Organisation") + } + verify(orgWithFixedIdRepository).deleteById(4L) + } + + @Test + fun `should fail to delete an organisation when it was not found`() { + whenever(orgWithFixedIdRepository.findById(5L)).thenReturn(Optional.empty()) + assertThrows { + syncService.deleteOrganisation(5L) + } + verify(orgWithFixedIdRepository).findById(5L) + } + + @Test + fun `should update an organisation`() { + val request = syncUpdateOrganisationRequest(6L) + + whenever(orgWithFixedIdRepository.findById(6L)).thenReturn(Optional.of(orgWithFixedIdEntity(6L))) + whenever(orgWithFixedIdRepository.saveAndFlush(any())).thenReturn( + request.toEntity(createdBy = "CREATOR", createdTime = LocalDateTime.now().minusHours(1)), + ) + + val updated = syncService.updateOrganisation(6L, request) + + val captor = argumentCaptor() + verify(orgWithFixedIdRepository).saveAndFlush(captor.capture()) + + with(captor.firstValue) { + assertThat(organisationId).isEqualTo(request.organisationId) + assertThat(organisationName).isEqualTo(request.organisationName) + assertThat(vatNumber).isEqualTo(request.vatNumber) + assertThat(active).isEqualTo(request.active) + assertThat(createdBy).isEqualTo("CREATOR") + assertThat(updatedBy).isEqualTo(request.updatedBy) + assertThat(updatedTime).isEqualTo(request.updatedTime) + } + + with(updated) { + assertThat(organisationId).isEqualTo(request.organisationId) + assertThat(organisationName).isEqualTo(request.organisationName) + assertThat(vatNumber).isEqualTo(request.vatNumber) + assertThat(active).isEqualTo(request.active) + assertThat(createdBy).isEqualTo("CREATOR") + assertThat(updatedBy).isEqualTo(request.updatedBy) + assertThat(updatedTime).isEqualTo(request.updatedTime) + } + } + + @Test + fun `should fail to update a contact when not found`() { + val updateRequest = syncUpdateOrganisationRequest(7L) + whenever(orgWithFixedIdRepository.findById(7L)).thenReturn(Optional.empty()) + assertThrows { + syncService.updateOrganisation(7L, updateRequest) + } + verify(orgWithFixedIdRepository).findById(7L) + } + } + + private fun syncUpdateOrganisationRequest(organisationId: Long) = SyncUpdateOrganisationRequest( + organisationId = organisationId, + organisationName = "Some Organisation", + programmeNumber = "1234", + vatNumber = "GB11111111", + comments = "comment", + active = true, + updatedBy = "UPDATER", + updatedTime = LocalDateTime.now(), + ) + + private fun syncCreateOrganisationRequest(organisationId: Long) = SyncCreateOrganisationRequest( + organisationId = organisationId, + organisationName = "Some Organisation", + active = true, + createdTime = LocalDateTime.now(), + createdBy = "CREATOR", + ) + + private fun orgWithFixedIdEntity(organisationId: Long) = OrganisationWithFixedIdEntity( + organisationId = organisationId, + organisationName = "Some Organisation", + programmeNumber = "1234", + vatNumber = "GB11111111", + comments = "comment", + caseloadId = null, + active = true, + deactivatedDate = null, + createdBy = "CREATOR", + createdTime = LocalDateTime.now(), + updatedBy = null, + updatedTime = null, + ) + + fun SyncUpdateOrganisationRequest.toEntity(createdBy: String, createdTime: LocalDateTime) = OrganisationWithFixedIdEntity( + organisationId = this.organisationId, + organisationName = this.organisationName, + programmeNumber = this.programmeNumber, + vatNumber = this.vatNumber, + caseloadId = this.caseloadId, + comments = this.comments, + active = this.active, + deactivatedDate = this.deactivatedDate, + updatedBy = this.updatedBy, + updatedTime = this.updatedTime, + createdBy = createdBy, + createdTime = createdTime, + ) +}