diff --git a/projects/manage-supervision-and-delius/deploy/database/access.yml b/projects/manage-supervision-and-delius/deploy/database/access.yml index 797ad0d190..421684ad81 100644 --- a/projects/manage-supervision-and-delius/deploy/database/access.yml +++ b/projects/manage-supervision-and-delius/deploy/database/access.yml @@ -2,6 +2,8 @@ database: access: username_key: /manage-supervision-and-delius/db-username password_key: /manage-supervision-and-delius/db-password + tables: + - contact audit: username: ManageSupervisionAndDelius diff --git a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt index 186f058fff..08003049ab 100644 --- a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt +++ b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt @@ -7,10 +7,13 @@ import org.springframework.boot.context.event.ApplicationReadyEvent import org.springframework.context.ApplicationListener import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional +import uk.gov.justice.digital.hmpps.audit.BusinessInteraction import uk.gov.justice.digital.hmpps.data.generator.* import uk.gov.justice.digital.hmpps.data.generator.CourtAppearanceGenerator.COURT_APPEARANCE import uk.gov.justice.digital.hmpps.data.generator.personalDetails.PersonDetailsGenerator +import uk.gov.justice.digital.hmpps.integrations.delius.audit.BusinessInteractionCode import uk.gov.justice.digital.hmpps.user.AuditUserRepository +import java.time.ZonedDateTime @Component @ConditionalOnProperty("seed.database") @@ -27,7 +30,11 @@ class DataLoader( @Transactional override fun onApplicationEvent(are: ApplicationReadyEvent) { + BusinessInteractionCode.entries.forEach { + entityManager.persist(BusinessInteraction(IdGenerator.getAndIncrement(), it.code, ZonedDateTime.now())) + } entityManager.persistAll( + *AppointmentGenerator.APPOINTMENT_TYPES.toTypedArray(), ContactGenerator.DEFAULT_PROVIDER, ContactGenerator.DEFAULT_BOROUGH, ContactGenerator.DEFAULT_DISTRICT, diff --git a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/AppointmentGenerator.kt b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/AppointmentGenerator.kt new file mode 100644 index 0000000000..95d9d0839c --- /dev/null +++ b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/AppointmentGenerator.kt @@ -0,0 +1,16 @@ +package uk.gov.justice.digital.hmpps.data.generator + +import uk.gov.justice.digital.hmpps.api.model.appointment.CreateAppointment +import uk.gov.justice.digital.hmpps.integrations.delius.overview.entity.ContactType + +object AppointmentGenerator { + + val APPOINTMENT_TYPES = CreateAppointment.Type.entries.map { generateType(it.code, attendanceType = true) } + + fun generateType( + code: String, + description: String = "Description for $code", + attendanceType: Boolean, + id: Long = IdGenerator.getAndIncrement() + ) = ContactType(IdGenerator.getAndIncrement(), code, true, description) +} diff --git a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/OffenderManagerGenerator.kt b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/OffenderManagerGenerator.kt index f3a50a041d..18b8fce9ae 100644 --- a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/OffenderManagerGenerator.kt +++ b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/OffenderManagerGenerator.kt @@ -35,6 +35,7 @@ object OffenderManagerGenerator { ContactGenerator.DEFAULT_PROVIDER, TEAM, STAFF_2, - LocalDate.now() + LocalDate.now(), + active = false ) } \ No newline at end of file diff --git a/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/CreateAppointmentIntegrationTests.kt b/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/CreateAppointmentIntegrationTests.kt new file mode 100644 index 0000000000..c4e2827fb8 --- /dev/null +++ b/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/CreateAppointmentIntegrationTests.kt @@ -0,0 +1,124 @@ +package uk.gov.justice.digital.hmpps + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import uk.gov.justice.digital.hmpps.api.model.appointment.CreateAppointment +import uk.gov.justice.digital.hmpps.api.model.appointment.CreatedAppointment +import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator +import uk.gov.justice.digital.hmpps.integrations.delius.sentence.entity.AppointmentRepository +import uk.gov.justice.digital.hmpps.test.CustomMatchers.isCloseTo +import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.contentAsJson +import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withJson +import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withToken +import java.time.ZonedDateTime +import java.util.* + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class CreateAppointmentIntegrationTests { + + @Autowired + internal lateinit var mockMvc: MockMvc + + @Autowired + internal lateinit var appointmentRepository: AppointmentRepository + + @Test + fun `unauthorized status returned`() { + mockMvc + .perform(MockMvcRequestBuilders.get("/appointments/D123456")) + .andExpect(MockMvcResultMatchers.status().isUnauthorized) + } + + @Test + fun `when offender does not exist retuns a 404 response`() { + mockMvc.perform( + post("/appointments/D123456") + .withToken() + .withJson( + CreateAppointment( + CreateAppointment.Type.HomeVisitToCaseNS, + ZonedDateTime.now().plusDays(1), + ZonedDateTime.now().plusDays(2), + 1, + 1, + UUID.randomUUID() + ) + ) + ).andExpect(MockMvcResultMatchers.status().isNotFound) + } + + @Test + fun `appointment end date before start returns bad request`() { + mockMvc.perform( + post("/appointments/${PersonGenerator.PERSON_1.crn}") + .withToken() + .withJson( + CreateAppointment( + CreateAppointment.Type.InitialAppointmentInOfficeNS, + ZonedDateTime.now().plusDays(2), + ZonedDateTime.now().plusDays(1), + 1, + PersonGenerator.EVENT_1.id, + UUID.randomUUID() + ) + ) + ).andExpect(MockMvcResultMatchers.status().isBadRequest) + } + + @ParameterizedTest + @MethodSource("createAppointments") + fun `create a new appointment`(createAppointment: CreateAppointment) { + val person = PersonGenerator.PERSON_1 + + val response = mockMvc.perform( + post("/appointments/${person.crn}") + .withToken() + .withJson(createAppointment) + ) + .andExpect(MockMvcResultMatchers.status().isCreated) + .andReturn().response.contentAsJson() + + val appointment = appointmentRepository.findById(response.id).get() + + assertThat(appointment.type.code, equalTo(createAppointment.type.code)) + assertThat(appointment.date, equalTo(createAppointment.start.toLocalDate())) + assertThat(appointment.startTime, isCloseTo(createAppointment.start)) + assertThat(appointment.externalReference, equalTo(createAppointment.urn)) + assertThat(appointment.eventId, equalTo(createAppointment.eventId)) + + appointmentRepository.delete(appointment) + } + + companion object { + @JvmStatic + fun createAppointments() = listOf( + CreateAppointment( + CreateAppointment.Type.PlannedOfficeVisitNS, + ZonedDateTime.now().plusDays(1), + ZonedDateTime.now().plusDays(2), + 1, + PersonGenerator.EVENT_1.id, + UUID.randomUUID() + ), + CreateAppointment( + CreateAppointment.Type.InitialAppointmentInOfficeNS, + ZonedDateTime.now().plusDays(1), + null, + 1, + PersonGenerator.EVENT_1.id, + UUID.randomUUID() + ) + ) + } +} \ No newline at end of file diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/controller/AppointmentController.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/controller/AppointmentController.kt new file mode 100644 index 0000000000..e5c4b60977 --- /dev/null +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/controller/AppointmentController.kt @@ -0,0 +1,20 @@ +package uk.gov.justice.digital.hmpps.api.controller + +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.* +import uk.gov.justice.digital.hmpps.api.model.appointment.CreateAppointment +import uk.gov.justice.digital.hmpps.service.AppointmentService + +@RestController +@Tag(name = "Sentence") +@RequestMapping("/appointments/{crn}") +@PreAuthorize("hasRole('PROBATION_API__MANAGE_A_SUPERVISION__CASE_DETAIL')") +class AppointmentController(private val appointmentService: AppointmentService) { + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + fun createAppointment(@PathVariable crn: String, @RequestBody createAppointment: CreateAppointment) = + appointmentService.createAppointment(crn, createAppointment) +} \ No newline at end of file diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/appointment/CreateAppointment.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/appointment/CreateAppointment.kt new file mode 100644 index 0000000000..38c0badfa0 --- /dev/null +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/appointment/CreateAppointment.kt @@ -0,0 +1,32 @@ +package uk.gov.justice.digital.hmpps.api.model.appointment + +import com.fasterxml.jackson.annotation.JsonIgnore +import java.time.ZonedDateTime +import java.util.* + +data class CreateAppointment( + val type: Type, + val start: ZonedDateTime, + val end: ZonedDateTime?, + val interval: Int, + val eventId: Long, + val uuid: UUID, + val requirementId: Long? = null, + val licenceConditionId: Long? = null, + val numberOfAppointments: Int? = null, + val until: ZonedDateTime? = null +) { + @JsonIgnore + val urn = URN_PREFIX + uuid + + enum class Type(val code: String) { + HomeVisitToCaseNS("CHVS"), + InitialAppointmentInOfficeNS("COAI"), + PlannedOfficeVisitNS("COAP"), + InitialAppointmentHomeVisitNS("COHV") + } + + companion object { + const val URN_PREFIX = "urn:uk:gov:hmpps:manage-supervision-service:appointment:" + } +} diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/appointment/CreatedAppointment.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/appointment/CreatedAppointment.kt new file mode 100644 index 0000000000..0193cab9e5 --- /dev/null +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/appointment/CreatedAppointment.kt @@ -0,0 +1,5 @@ +package uk.gov.justice.digital.hmpps.api.model.appointment + +data class CreatedAppointment( + val id: Long +) diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/audit/BusinessInteractionCode.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/audit/BusinessInteractionCode.kt new file mode 100644 index 0000000000..6327e5e109 --- /dev/null +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/audit/BusinessInteractionCode.kt @@ -0,0 +1,7 @@ +package uk.gov.justice.digital.hmpps.integrations.delius.audit + +import uk.gov.justice.digital.hmpps.audit.InteractionCode + +enum class BusinessInteractionCode(override val code: String) : InteractionCode { + ADD_CONTACT("CLBI003") +} \ No newline at end of file diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/overview/entity/Contact.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/overview/entity/Contact.kt index b73e2db4fd..108ff83e7f 100644 --- a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/overview/entity/Contact.kt +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/overview/entity/Contact.kt @@ -96,8 +96,13 @@ class Contact( val lastUpdatedUser: User, @Column(name = "soft_deleted", columnDefinition = "NUMBER", nullable = false) - val softDeleted: Boolean = false + val softDeleted: Boolean = false, + + val partitionAreaId: Long = 0, + + val createdByUserId: Long = 0 ) { + fun startDateTime(): ZonedDateTime = if (startTime != null) ZonedDateTime.of(date, startTime.toLocalTime(), EuropeLondon) else ZonedDateTime.of(date, date.atStartOfDay().toLocalTime(), EuropeLondon) diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/sentence/entity/Appointment.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/sentence/entity/Appointment.kt new file mode 100644 index 0000000000..bb04223036 --- /dev/null +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/sentence/entity/Appointment.kt @@ -0,0 +1,135 @@ +package uk.gov.justice.digital.hmpps.integrations.delius.sentence.entity + +import jakarta.persistence.* +import org.hibernate.annotations.SQLRestriction +import org.springframework.data.annotation.CreatedBy +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.LastModifiedBy +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import uk.gov.justice.digital.hmpps.exception.NotFoundException +import uk.gov.justice.digital.hmpps.integrations.delius.overview.entity.ContactType +import uk.gov.justice.digital.hmpps.integrations.delius.overview.entity.Person +import java.time.LocalDate +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +@Entity +@EntityListeners(AuditingEntityListener::class) +@Table(name = "contact") +@SequenceGenerator(name = "contact_id_generator", sequenceName = "contact_id_seq", allocationSize = 1) +@SQLRestriction("soft_deleted = 0") +class Appointment( + @ManyToOne + @JoinColumn(name = "offender_id") + val person: Person, + + @ManyToOne + @JoinColumn(name = "contact_type_id") + val type: ContactType, + + @Column(name = "contact_date") + val date: LocalDate, + + @Column(name = "contact_start_time") + val startTime: ZonedDateTime, + + @ManyToOne + @JoinColumn(name = "team_id") + val team: Team, + + @ManyToOne + @JoinColumn(name = "staff_id") + val staff: Staff, + + @Column(name = "last_updated_user_id") + @LastModifiedBy + var lastUpdatedUserId: Long, + + @Column(name = "contact_end_time") + val endTime: ZonedDateTime?, + + val probationAreaId: Long? = null, + + val externalReference: String? = null, + + @Column(name = "description") + val description: String? = null, + + @Column(name = "event_id") + val eventId: Long? = null, + + @Column(name = "rqmnt_id") + val rqmntId: Long? = null, + + val licConditionId: Long? = null, + + @Column(name = "soft_deleted", columnDefinition = "NUMBER", nullable = false) + val softDeleted: Boolean = false, + + @Version + @Column(name = "row_version") + val version: Long = 0, + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "contact_id_generator") + @Column(name = "contact_id") + val id: Long = 0 +) { + var partitionAreaId: Long = 0 + + @CreatedBy + var createdByUserId: Long = 0 + + @CreatedDate + @Column(name = "created_datetime") + var createdDateTime: ZonedDateTime = ZonedDateTime.now() + + @LastModifiedDate + @Column(name = "last_updated_datetime") + var lastUpdatedDateTime: ZonedDateTime = ZonedDateTime.now() +} + +interface AppointmentRepository : JpaRepository { + @Query( + """ + select count(c.contact_id) + from contact c + join r_contact_type ct on c.contact_type_id = ct.contact_type_id + where c.offender_id = :personId and ct.attendance_contact = 'Y' + and to_char(c.contact_date, 'YYYY-MM-DD') = :date + and to_char(c.contact_start_time, 'HH24:MI') < :endTime + and to_char(c.contact_end_time, 'HH24:MI') > :startTime + and c.soft_deleted = 0 and c.contact_outcome_type_id is null + """, + nativeQuery = true + ) + fun getClashCount( + personId: Long, + date: String, + startTime: String, + endTime: String + ): Int +} + +fun AppointmentRepository.appointmentClashes( + personId: Long, + date: LocalDate, + startTime: ZonedDateTime, + endTime: ZonedDateTime, +): Boolean = getClashCount( + personId, + date.format(DateTimeFormatter.ISO_LOCAL_DATE), + startTime.format(DateTimeFormatter.ISO_LOCAL_TIME.withZone(ZoneId.systemDefault())), + endTime.format(DateTimeFormatter.ISO_LOCAL_TIME.withZone(ZoneId.systemDefault())) +) > 0 + +interface AppointmentTypeRepository : JpaRepository { + fun findByCode(code: String): ContactType? +} + +fun AppointmentTypeRepository.getByCode(code: String) = + findByCode(code) ?: throw NotFoundException("AppointmentType", "code", code) diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/sentence/entity/OffenderManager.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/sentence/entity/OffenderManager.kt index c636329163..6cd4879ce9 100644 --- a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/sentence/entity/OffenderManager.kt +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/sentence/entity/OffenderManager.kt @@ -31,7 +31,13 @@ class OffenderManager( @JoinColumn(name = "allocation_staff_id") val staff: Staff, - val endDate: LocalDate? + val endDate: LocalDate?, + + @Column(name = "soft_deleted", columnDefinition = "number") + val softDeleted: Boolean = false, + + @Column(name = "active_flag", columnDefinition = "number") + val active: Boolean = true, ) @Immutable diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/sentence/entity/SentenceRepository.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/sentence/entity/SentenceRepository.kt index 001f9cd25a..d5ef29ff91 100644 --- a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/sentence/entity/SentenceRepository.kt +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/sentence/entity/SentenceRepository.kt @@ -41,10 +41,14 @@ interface AdditionalSentenceRepository : JpaRepository interface OffenderManagerRepository : JpaRepository { + fun findByPersonCrnAndSoftDeletedIsFalseAndActiveIsTrue(crn: String): OffenderManager? + fun countOffenderManagersByPerson(person: Person): Long fun findOffenderManagersByPersonOrderByEndDateDesc(person: Person): List } +fun OffenderManagerRepository.getByCrn(crn: String) = + findByPersonCrnAndSoftDeletedIsFalseAndActiveIsTrue(crn) ?: throw NotFoundException("Person", "crn", crn) diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/AppointmentService.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/AppointmentService.kt new file mode 100644 index 0000000000..2fec1a56e5 --- /dev/null +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/AppointmentService.kt @@ -0,0 +1,111 @@ +package uk.gov.justice.digital.hmpps.service + +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.web.server.ResponseStatusException +import uk.gov.justice.digital.hmpps.api.model.appointment.CreateAppointment +import uk.gov.justice.digital.hmpps.api.model.appointment.CreatedAppointment +import uk.gov.justice.digital.hmpps.audit.service.AuditableService +import uk.gov.justice.digital.hmpps.audit.service.AuditedInteractionService +import uk.gov.justice.digital.hmpps.datetime.EuropeLondon +import uk.gov.justice.digital.hmpps.exception.ConflictException +import uk.gov.justice.digital.hmpps.exception.NotFoundException +import uk.gov.justice.digital.hmpps.integrations.delius.audit.BusinessInteractionCode +import uk.gov.justice.digital.hmpps.integrations.delius.overview.entity.RequirementRepository +import uk.gov.justice.digital.hmpps.integrations.delius.sentence.entity.* +import java.time.LocalDate +import java.time.ZonedDateTime + +@Service +class AppointmentService( + auditedInteractionService: AuditedInteractionService, + private val appointmentRepository: AppointmentRepository, + private val appointmentTypeRepository: AppointmentTypeRepository, + private val offenderManagerRepository: OffenderManagerRepository, + private val eventSentenceRepository: EventSentenceRepository, + private val requirementRepository: RequirementRepository, + private val licenceConditionRepository: LicenceConditionRepository, +) : AuditableService(auditedInteractionService) { + fun createAppointment( + crn: String, + createAppointment: CreateAppointment + ): CreatedAppointment { + return audit(BusinessInteractionCode.ADD_CONTACT) { audit -> + val om = offenderManagerRepository.getByCrn(crn) + audit["offenderId"] = om.person.id + checkForConflicts(om.person.id, createAppointment) + val appointment = appointmentRepository.save(createAppointment.withManager(om)) + val createdAppointment = CreatedAppointment(appointment.id) + audit["contactId"] = appointment.id + + return@audit createdAppointment + } + } + + private fun checkForConflicts( + personId: Long, + createAppointment: CreateAppointment + ) { + if (createAppointment.requirementId != null && createAppointment.licenceConditionId != null) { + throw ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Either licence id or requirement id can be provided, not both" + ) + } + + createAppointment.end?.let { + if (it.isBefore(createAppointment.start)) + throw ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Appointment end time cannot be before start time" + ) + } + + if (!eventSentenceRepository.existsById(createAppointment.eventId)) { + throw NotFoundException("Event", "eventId", createAppointment.eventId) + } + + if (createAppointment.requirementId != null && !requirementRepository.existsById(createAppointment.requirementId)) { + throw NotFoundException("Requirement", "requirementId", createAppointment.requirementId) + } + + if (createAppointment.licenceConditionId != null && !licenceConditionRepository.existsById(createAppointment.licenceConditionId)) { + throw NotFoundException("LicenceCondition", "licenceConditionId", createAppointment.licenceConditionId) + } + + if (createAppointment.start.isAfter(ZonedDateTime.now()) && appointmentRepository.appointmentClashes( + personId, + createAppointment.start.toLocalDate(), + createAppointment.start, + createAppointment.start + ) + ) { + throw ConflictException("Appointment conflicts with an existing future appointment") + } + + val licenceOrRequirement = listOfNotNull(createAppointment.licenceConditionId, createAppointment.requirementId) + + if (licenceOrRequirement.size > 1) { + throw ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Either licence id or requirement id can be provided, not both" + ) + } + } + + private fun CreateAppointment.withManager(om: OffenderManager) = Appointment( + om.person, + appointmentTypeRepository.getByCode(type.code), + start.toLocalDate(), + ZonedDateTime.of(LocalDate.EPOCH, start.toLocalTime(), EuropeLondon), + om.team, + om.staff, + 0, + end?.let { ZonedDateTime.of(LocalDate.EPOCH, end.toLocalTime(), EuropeLondon) }, + om.provider.id, + urn, + eventId = eventId, + rqmntId = requirementId, + licConditionId = licenceConditionId + ) +} \ No newline at end of file diff --git a/projects/manage-supervision-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/service/AppointmentServiceTest.kt b/projects/manage-supervision-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/service/AppointmentServiceTest.kt new file mode 100644 index 0000000000..5fe3869253 --- /dev/null +++ b/projects/manage-supervision-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/service/AppointmentServiceTest.kt @@ -0,0 +1,205 @@ +package uk.gov.justice.digital.hmpps.service + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.springframework.web.server.ResponseStatusException +import uk.gov.justice.digital.hmpps.api.model.appointment.CreateAppointment +import uk.gov.justice.digital.hmpps.audit.service.AuditedInteractionService +import uk.gov.justice.digital.hmpps.data.generator.OffenderManagerGenerator +import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator +import uk.gov.justice.digital.hmpps.exception.NotFoundException +import uk.gov.justice.digital.hmpps.integrations.delius.overview.entity.RequirementRepository +import uk.gov.justice.digital.hmpps.integrations.delius.sentence.entity.* +import java.time.ZonedDateTime +import java.util.* + +@ExtendWith(MockitoExtension::class) +class AppointmentServiceTest { + + @Mock + lateinit var auditedInteractionService: AuditedInteractionService + + @Mock + lateinit var appointmentRepository: AppointmentRepository + + @Mock + lateinit var appointmentTypeRepository: AppointmentTypeRepository + + @Mock + lateinit var offenderManagerRepository: OffenderManagerRepository + + @Mock + lateinit var eventSentenceRepository: EventSentenceRepository + + @Mock + lateinit var requirementRepository: RequirementRepository + + @Mock + lateinit var licenceConditionRepository: LicenceConditionRepository + + @InjectMocks + lateinit var service: AppointmentService + + private val uuid: UUID = UUID.randomUUID() + + @Test + fun `licence and requirement id provided`() { + val appointment = CreateAppointment( + CreateAppointment.Type.InitialAppointmentInOfficeNS, + ZonedDateTime.now().plusDays(1), + ZonedDateTime.now().plusDays(2), + 1, + PersonGenerator.EVENT_1.id, + uuid, + requirementId = 2, + licenceConditionId = 3 + ) + + whenever(offenderManagerRepository.findByPersonCrnAndSoftDeletedIsFalseAndActiveIsTrue(PersonGenerator.PERSON_1.crn)).thenReturn( + OffenderManagerGenerator.OFFENDER_MANAGER_ACTIVE + ) + val exception = assertThrows { + service.createAppointment(PersonGenerator.PERSON_1.crn, appointment) + } + + assertThat( + exception.message, + equalTo("400 BAD_REQUEST \"Either licence id or requirement id can be provided, not both\"") + ) + + verifyNoMoreInteractions(offenderManagerRepository) + verifyNoInteractions(eventSentenceRepository) + verifyNoInteractions(licenceConditionRepository) + verifyNoInteractions(requirementRepository) + verifyNoInteractions(appointmentRepository) + verifyNoInteractions(appointmentTypeRepository) + } + + @Test + fun `start date before end date`() { + val appointment = CreateAppointment( + CreateAppointment.Type.InitialAppointmentInOfficeNS, + ZonedDateTime.now().plusDays(2), + ZonedDateTime.now().plusDays(1), + 1, + PersonGenerator.EVENT_1.id, + uuid + ) + + whenever(offenderManagerRepository.findByPersonCrnAndSoftDeletedIsFalseAndActiveIsTrue(PersonGenerator.PERSON_1.crn)).thenReturn( + OffenderManagerGenerator.OFFENDER_MANAGER_ACTIVE + ) + val exception = assertThrows { + service.createAppointment(PersonGenerator.PERSON_1.crn, appointment) + } + + assertThat(exception.message, equalTo("400 BAD_REQUEST \"Appointment end time cannot be before start time\"")) + + verifyNoMoreInteractions(offenderManagerRepository) + verifyNoInteractions(eventSentenceRepository) + verifyNoInteractions(licenceConditionRepository) + verifyNoInteractions(requirementRepository) + verifyNoInteractions(appointmentRepository) + verifyNoInteractions(appointmentTypeRepository) + } + + @Test + fun `event not found`() { + val appointment = CreateAppointment( + CreateAppointment.Type.InitialAppointmentInOfficeNS, + ZonedDateTime.now().plusDays(1), + null, + 1, + 1, + uuid + ) + + whenever(offenderManagerRepository.findByPersonCrnAndSoftDeletedIsFalseAndActiveIsTrue(PersonGenerator.PERSON_1.crn)).thenReturn( + OffenderManagerGenerator.OFFENDER_MANAGER_ACTIVE + ) + whenever(eventSentenceRepository.existsById(appointment.eventId)).thenReturn(false) + val exception = assertThrows { + service.createAppointment(PersonGenerator.PERSON_1.crn, appointment) + } + + assertThat(exception.message, equalTo("Event with eventId of 1 not found")) + + verifyNoMoreInteractions(offenderManagerRepository) + verifyNoMoreInteractions(eventSentenceRepository) + verifyNoInteractions(licenceConditionRepository) + verifyNoInteractions(requirementRepository) + verifyNoInteractions(appointmentRepository) + verifyNoInteractions(appointmentTypeRepository) + } + + @Test + fun `requirement not found`() { + val appointment = CreateAppointment( + CreateAppointment.Type.InitialAppointmentInOfficeNS, + ZonedDateTime.now().plusDays(1), + ZonedDateTime.now().plusDays(2), + 1, + PersonGenerator.EVENT_1.id, + uuid, + requirementId = 2 + ) + + whenever(offenderManagerRepository.findByPersonCrnAndSoftDeletedIsFalseAndActiveIsTrue(PersonGenerator.PERSON_1.crn)).thenReturn( + OffenderManagerGenerator.OFFENDER_MANAGER_ACTIVE + ) + whenever(eventSentenceRepository.existsById(appointment.eventId)).thenReturn(true) + whenever(requirementRepository.existsById(appointment.requirementId!!)).thenReturn(false) + val exception = assertThrows { + service.createAppointment(PersonGenerator.PERSON_1.crn, appointment) + } + + assertThat(exception.message, equalTo("Requirement with requirementId of 2 not found")) + + verifyNoMoreInteractions(offenderManagerRepository) + verifyNoMoreInteractions(eventSentenceRepository) + verifyNoMoreInteractions(requirementRepository) + verifyNoInteractions(licenceConditionRepository) + verifyNoInteractions(appointmentRepository) + verifyNoInteractions(appointmentTypeRepository) + } + + @Test + fun `licence not found`() { + val appointment = CreateAppointment( + CreateAppointment.Type.InitialAppointmentInOfficeNS, + ZonedDateTime.now().plusDays(1), + ZonedDateTime.now().plusDays(2), + 1, + PersonGenerator.EVENT_1.id, + uuid, + licenceConditionId = 3 + ) + + whenever(offenderManagerRepository.findByPersonCrnAndSoftDeletedIsFalseAndActiveIsTrue(PersonGenerator.PERSON_1.crn)).thenReturn( + OffenderManagerGenerator.OFFENDER_MANAGER_ACTIVE + ) + whenever(eventSentenceRepository.existsById(appointment.eventId)).thenReturn(true) + whenever(licenceConditionRepository.existsById(appointment.licenceConditionId!!)).thenReturn(false) + val exception = assertThrows { + service.createAppointment(PersonGenerator.PERSON_1.crn, appointment) + } + + assertThat(exception.message, equalTo("LicenceCondition with licenceConditionId of 3 not found")) + + verifyNoMoreInteractions(offenderManagerRepository) + verifyNoMoreInteractions(eventSentenceRepository) + verifyNoMoreInteractions(licenceConditionRepository) + verifyNoInteractions(requirementRepository) + verifyNoInteractions(appointmentRepository) + verifyNoInteractions(appointmentTypeRepository) + } +} \ No newline at end of file diff --git a/projects/resettlement-passport-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Appointment.kt b/projects/resettlement-passport-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Appointment.kt index f49b1215c5..6c1425a5dc 100644 --- a/projects/resettlement-passport-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Appointment.kt +++ b/projects/resettlement-passport-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Appointment.kt @@ -80,7 +80,7 @@ class Appointment( @Column(name = "contact_id") val id: Long = 0 ) { - val partitionAreaId: Long = 0 + var partitionAreaId: Long = 0 @CreatedDate @Column(name = "created_datetime")