From 4b369fc2e8319ab0960ced411f0a7f45747275f6 Mon Sep 17 00:00:00 2001 From: achimber-moj <161360519+achimber-moj@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:55:06 +0000 Subject: [PATCH] Man 109 schedule multiple appointments (#4381) * MAN-109 - update logic to add multiple appointments * MAN-109 - update logic to add multiple appointments * MAN-109 - when creating multiple appointments, ensure external ref is unique * MAN-109 - add until attribute to logic * MAN-109 - update integration test * Formatting changes * MAN-109 - update number of appointments calculation * Formatting changes * MAN-109 - removed unused import * MAN-109 - update contact with user who has created the appointment * Formatting changes --------- Co-authored-by: probation-integration-bot[bot] <177347787+probation-integration-bot[bot]@users.noreply.github.com> --- .../justice/digital/hmpps/data/DataLoader.kt | 2 + .../generator/OffenderManagerGenerator.kt | 16 +-- .../CreateAppointmentIntegrationTests.kt | 112 ++++++++++++++++-- .../api/controller/AppointmentController.kt | 4 +- ...tedAppointment.kt => AppointmentDetail.kt} | 4 + .../model/appointment/CreateAppointment.kt | 19 ++- .../delius/overview/entity/Contact.kt | 4 +- .../delius/sentence/entity/Appointment.kt | 20 ++-- .../delius/sentence/entity/OffenderManager.kt | 76 ++++++++++++ ...rvice.kt => SentenceAppointmentService.kt} | 70 +++++++++-- ...t.kt => SentenceAppointmentServiceTest.kt} | 67 +++++++++-- 11 files changed, 335 insertions(+), 59 deletions(-) rename projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/appointment/{CreatedAppointment.kt => AppointmentDetail.kt} (57%) rename projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/{AppointmentService.kt => SentenceAppointmentService.kt} (60%) rename projects/manage-supervision-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/service/{AppointmentServiceTest.kt => SentenceAppointmentServiceTest.kt} (77%) 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 08003049ab..294654bd71 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 @@ -100,6 +100,8 @@ class DataLoader( OffenderManagerGenerator.STAFF_USER_2, OffenderManagerGenerator.OFFENDER_MANAGER_ACTIVE, OffenderManagerGenerator.OFFENDER_MANAGER_INACTIVE, + OffenderManagerGenerator.DEFAULT_LOCATION, + OffenderManagerGenerator.TEAM_OFFICE, PersonGenerator.DEFAULT_DISPOSAL_TYPE, PersonGenerator.ACTIVE_ORDER, LicenceConditionGenerator.LIC_COND_MAIN_CAT, 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 18b8fce9ae..fc886cebff 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 @@ -1,24 +1,26 @@ package uk.gov.justice.digital.hmpps.data.generator +import uk.gov.justice.digital.hmpps.data.generator.ContactGenerator.DEFAULT_PROVIDER import uk.gov.justice.digital.hmpps.integrations.delius.overview.entity.Borough import uk.gov.justice.digital.hmpps.integrations.delius.overview.entity.District -import uk.gov.justice.digital.hmpps.integrations.delius.sentence.entity.OffenderManager -import uk.gov.justice.digital.hmpps.integrations.delius.sentence.entity.Staff -import uk.gov.justice.digital.hmpps.integrations.delius.sentence.entity.StaffUser -import uk.gov.justice.digital.hmpps.integrations.delius.sentence.entity.Team +import uk.gov.justice.digital.hmpps.integrations.delius.sentence.entity.* import java.time.LocalDate object OffenderManagerGenerator { val BOROUGH = Borough("LTS_ALL", "Leicestershire All", IdGenerator.getAndIncrement()) val DISTRICT = District("LTS_ALL", "Leicestershire All", BOROUGH, IdGenerator.getAndIncrement()) - val TEAM = Team(IdGenerator.getAndIncrement(), DISTRICT, "N07T02", "OMU B") + val TEAM = Team(IdGenerator.getAndIncrement(), DISTRICT, DEFAULT_PROVIDER, "N07T02", "OMU B") - val STAFF_1 = Staff(IdGenerator.getAndIncrement(), "Peter", "Parker", null) - val STAFF_2 = Staff(IdGenerator.getAndIncrement(), "Bruce", "Wayne", null) + val STAFF_1 = Staff(IdGenerator.getAndIncrement(), "Peter", "Parker", DEFAULT_PROVIDER, null) + val STAFF_2 = Staff(IdGenerator.getAndIncrement(), "Bruce", "Wayne", DEFAULT_PROVIDER, null) val STAFF_USER_1 = StaffUser(IdGenerator.getAndIncrement(), STAFF_1, "peter-parker") val STAFF_USER_2 = StaffUser(IdGenerator.getAndIncrement(), STAFF_2, "bwayne") + val DEFAULT_LOCATION = Location(IdGenerator.getAndIncrement(), "B20", "1 Birmingham Street") + + val TEAM_OFFICE = TeamOfficeLink(TeamOfficeLinkId(TEAM.id, DEFAULT_LOCATION)) + val OFFENDER_MANAGER_ACTIVE = OffenderManager( IdGenerator.getAndIncrement(), 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 index c4e2827fb8..35edb81cd2 100644 --- 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 @@ -2,6 +2,7 @@ package uk.gov.justice.digital.hmpps import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource @@ -11,15 +12,23 @@ 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.MockMvcResultHandlers.print import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import uk.gov.justice.digital.hmpps.api.model.appointment.AppointmentDetail 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.api.model.appointment.User +import uk.gov.justice.digital.hmpps.data.generator.ContactGenerator.DEFAULT_PROVIDER +import uk.gov.justice.digital.hmpps.data.generator.OffenderManagerGenerator.DEFAULT_LOCATION +import uk.gov.justice.digital.hmpps.data.generator.OffenderManagerGenerator.STAFF_1 +import uk.gov.justice.digital.hmpps.data.generator.OffenderManagerGenerator.STAFF_USER_1 +import uk.gov.justice.digital.hmpps.data.generator.OffenderManagerGenerator.TEAM 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.LocalDate import java.time.ZonedDateTime import java.util.* @@ -33,6 +42,8 @@ class CreateAppointmentIntegrationTests { @Autowired internal lateinit var appointmentRepository: AppointmentRepository + private val user = User(STAFF_USER_1.username, TEAM.description) + @Test fun `unauthorized status returned`() { mockMvc @@ -47,11 +58,13 @@ class CreateAppointmentIntegrationTests { .withToken() .withJson( CreateAppointment( + user, CreateAppointment.Type.HomeVisitToCaseNS, ZonedDateTime.now().plusDays(1), ZonedDateTime.now().plusDays(2), - 1, - 1, + interval = CreateAppointment.Interval.DAY, + numberOfAppointments = 1, + eventId = 1, UUID.randomUUID() ) ) @@ -65,10 +78,12 @@ class CreateAppointmentIntegrationTests { .withToken() .withJson( CreateAppointment( + user, CreateAppointment.Type.InitialAppointmentInOfficeNS, ZonedDateTime.now().plusDays(2), ZonedDateTime.now().plusDays(1), - 1, + interval = CreateAppointment.Interval.DAY, + numberOfAppointments = 1, PersonGenerator.EVENT_1.id, UUID.randomUUID() ) @@ -87,37 +102,110 @@ class CreateAppointmentIntegrationTests { .withJson(createAppointment) ) .andExpect(MockMvcResultMatchers.status().isCreated) - .andReturn().response.contentAsJson() + .andReturn().response.contentAsJson() - val appointment = appointmentRepository.findById(response.id).get() + val appointment = appointmentRepository.findById(response.appointments[0].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)) + assertThat(appointment.createdByUserId, equalTo(STAFF_USER_1.id)) + assertThat(appointment.staffId, equalTo(STAFF_1.id)) + assertThat(appointment.probationAreaId, equalTo(DEFAULT_PROVIDER.id)) + assertThat(appointment.officeLocationId, equalTo(DEFAULT_LOCATION.id)) + appointmentRepository.delete(appointment) } + @ParameterizedTest + @MethodSource("createMultipleAppointments") + fun `create multiple appointments`(createAppointment: CreateAppointment) { + val person = PersonGenerator.PERSON_1 + val response = mockMvc.perform( + post("/appointments/${person.crn}") + .withToken() + .withJson(createAppointment) + ) + .andDo(print()) + .andExpect(MockMvcResultMatchers.status().isCreated) + .andReturn().response.contentAsJson() + + val appointments = appointmentRepository.findAllById(response.appointments.map { it.id }) + + assertThat(appointments.size, equalTo(3)) + + assertThat(appointments[0].date, equalTo(LocalDate.now())) + assertThat( + appointments[1].date, + equalTo(LocalDate.now().plusDays(createAppointment.interval.value.toLong() * 1)) + ) + assertThat( + appointments[2].date, + equalTo(LocalDate.now().plusDays(createAppointment.interval.value.toLong() * 2)) + ) + + //check for unique external reference + val externalRef = "urn:uk:gov:hmpps:manage-supervision-service:appointment:${createAppointment.uuid}" + assertThat(appointments[0].externalReference, equalTo(externalRef)) + assertNotEquals(externalRef, appointments[1].externalReference) + assertNotEquals(externalRef, appointments[2].externalReference) + + appointmentRepository.deleteAll(appointments) + } + companion object { + private val user = User(STAFF_USER_1.username, TEAM.description) + @JvmStatic fun createAppointments() = listOf( CreateAppointment( + user, CreateAppointment.Type.PlannedOfficeVisitNS, ZonedDateTime.now().plusDays(1), ZonedDateTime.now().plusDays(2), - 1, - PersonGenerator.EVENT_1.id, - UUID.randomUUID() + eventId = PersonGenerator.EVENT_1.id, + uuid = UUID.randomUUID() ), CreateAppointment( + user, CreateAppointment.Type.InitialAppointmentInOfficeNS, ZonedDateTime.now().plusDays(1), null, - 1, - PersonGenerator.EVENT_1.id, - UUID.randomUUID() + CreateAppointment.Interval.DAY, + eventId = PersonGenerator.EVENT_1.id, + uuid = UUID.randomUUID() + ) + ) + + @JvmStatic + fun createMultipleAppointments() = listOf( + CreateAppointment( + user, + CreateAppointment.Type.HomeVisitToCaseNS, + ZonedDateTime.now(), + numberOfAppointments = 3, + eventId = PersonGenerator.EVENT_1.id, + uuid = UUID.randomUUID() + ), + CreateAppointment( + user, + CreateAppointment.Type.HomeVisitToCaseNS, + ZonedDateTime.now(), + until = ZonedDateTime.now().plusDays(3), + eventId = PersonGenerator.EVENT_1.id, + uuid = UUID.randomUUID() + ), + CreateAppointment( + user, + CreateAppointment.Type.HomeVisitToCaseNS, + start = ZonedDateTime.now(), + until = ZonedDateTime.now().plusDays(21), + interval = CreateAppointment.Interval.WEEK, + eventId = PersonGenerator.EVENT_1.id, + uuid = UUID.randomUUID() ) ) } 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 index e5c4b60977..85c6066904 100644 --- 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 @@ -5,13 +5,13 @@ 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 +import uk.gov.justice.digital.hmpps.service.SentenceAppointmentService @RestController @Tag(name = "Sentence") @RequestMapping("/appointments/{crn}") @PreAuthorize("hasRole('PROBATION_API__MANAGE_A_SUPERVISION__CASE_DETAIL')") -class AppointmentController(private val appointmentService: AppointmentService) { +class AppointmentController(private val appointmentService: SentenceAppointmentService) { @PostMapping @ResponseStatus(HttpStatus.CREATED) 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/AppointmentDetail.kt similarity index 57% rename from projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/appointment/CreatedAppointment.kt rename to projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/appointment/AppointmentDetail.kt index 0193cab9e5..994f5c6b39 100644 --- 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/AppointmentDetail.kt @@ -1,5 +1,9 @@ package uk.gov.justice.digital.hmpps.api.model.appointment +data class AppointmentDetail( + val appointments: List, +) + data class CreatedAppointment( val id: Long ) 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 index 38c0badfa0..598186fe0b 100644 --- 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 @@ -5,15 +5,16 @@ import java.time.ZonedDateTime import java.util.* data class CreateAppointment( + val user: User, val type: Type, val start: ZonedDateTime, - val end: ZonedDateTime?, - val interval: Int, + val end: ZonedDateTime? = null, + val interval: Interval = Interval.DAY, + val numberOfAppointments: Int = 1, val eventId: Long, val uuid: UUID, val requirementId: Long? = null, val licenceConditionId: Long? = null, - val numberOfAppointments: Int? = null, val until: ZonedDateTime? = null ) { @JsonIgnore @@ -26,7 +27,19 @@ data class CreateAppointment( InitialAppointmentHomeVisitNS("COHV") } + enum class Interval(val value: Int) { + DAY(1), + WEEK(7), + FORTNIGHT(14), + FOUR_WEEKS(28) + } + companion object { const val URN_PREFIX = "urn:uk:gov:hmpps:manage-supervision-service:appointment:" } } + +data class User( + val username: String, + val team: String +) 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 108ff83e7f..09166db7aa 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 @@ -98,9 +98,7 @@ class Contact( @Column(name = "soft_deleted", columnDefinition = "NUMBER", nullable = false) val softDeleted: Boolean = false, - val partitionAreaId: Long = 0, - - val createdByUserId: Long = 0 + val partitionAreaId: Long = 0 ) { fun startDateTime(): ZonedDateTime = 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 index bb04223036..fe6b0b557d 100644 --- 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 @@ -2,7 +2,6 @@ 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 @@ -37,13 +36,11 @@ class Appointment( @Column(name = "contact_start_time") val startTime: ZonedDateTime, - @ManyToOne - @JoinColumn(name = "team_id") - val team: Team, + @Column(name = "team_id") + val teamId: Long, - @ManyToOne - @JoinColumn(name = "staff_id") - val staff: Staff, + @Column(name = "staff_id") + val staffId: Long, @Column(name = "last_updated_user_id") @LastModifiedBy @@ -74,6 +71,12 @@ class Appointment( @Column(name = "row_version") val version: Long = 0, + @Column(name = "created_by_user_id") + val createdByUserId: Long? = null, + + @Column(name = "office_location_id") + val officeLocationId: Long? = null, + @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "contact_id_generator") @Column(name = "contact_id") @@ -81,9 +84,6 @@ class Appointment( ) { var partitionAreaId: Long = 0 - @CreatedBy - var createdByUserId: Long = 0 - @CreatedDate @Column(name = "created_datetime") var createdDateTime: ZonedDateTime = ZonedDateTime.now() 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 6cd4879ce9..c6073acd28 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 @@ -2,9 +2,13 @@ package uk.gov.justice.digital.hmpps.integrations.delius.sentence.entity import jakarta.persistence.* import org.hibernate.annotations.Immutable +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.District import uk.gov.justice.digital.hmpps.integrations.delius.overview.entity.Person import uk.gov.justice.digital.hmpps.integrations.delius.user.entity.Provider +import java.io.Serializable import java.time.LocalDate import kotlin.jvm.Transient @@ -52,6 +56,10 @@ class Staff( val forename: String, val surname: String, + @JoinColumn(name = "probation_area_id") + @ManyToOne + val provider: Provider, + @OneToOne(mappedBy = "staff") val user: StaffUser? ) @@ -79,6 +87,38 @@ class StaffUser( var telephone: String? = null } +interface StaffUserRepository : JpaRepository { + + @Query( + """ + SELECT u.id AS userId, st.id AS staffId, t.id AS teamId, st.provider.id AS providerId, l.id AS locationId + FROM StaffUser u + JOIN u.staff st + JOIN st.provider + JOIN Team t ON t.provider = st.provider + JOIN TeamOfficeLink tol ON tol.id.teamId = t.id + JOIN Location l ON l = tol.id.officeLocation + WHERE UPPER(u.username) = UPPER(:username) + AND UPPER(t.description) = UPPER(:teamName) + """ + ) + fun findUserAndLocation(username: String, teamName: String): UserLocation? +} + +fun StaffUserRepository.getUserAndLocation(username: String, teamName: String) = + findUserAndLocation(username, teamName) ?: throw NotFoundException( + "User", "username", + "$username in $teamName" + ) + +interface UserLocation { + val userId: Long + val staffId: Long + val teamId: Long + val providerId: Long + val locationId: Long +} + @Immutable @Entity(name = "professional_contact_team") @Table(name = "team") @@ -91,8 +131,44 @@ class Team( @JoinColumn(name = "district_id") val district: District, + @JoinColumn(name = "probation_area_id") + @ManyToOne + val provider: Provider, + @Column(name = "code", columnDefinition = "char(6)") val code: String, val description: String, ) +@Immutable +@Entity +@Table(name = "team_office_location") +class TeamOfficeLink( + @Id + val id: TeamOfficeLinkId +) + +@Immutable +@Entity +@Table(name = "office_location") +class Location( + @Id + @Column(name = "office_location_id") + val id: Long, + + val code: String, + + val description: String, + + ) + +@Embeddable +class TeamOfficeLinkId( + @Column(name = "team_id") + val teamId: Long, + + @ManyToOne + @JoinColumn(name = "office_location_id") + val officeLocation: Location +) : Serializable + 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/SentenceAppointmentService.kt similarity index 60% rename from projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/AppointmentService.kt rename to projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/SentenceAppointmentService.kt index 2fec1a56e5..ba0d155322 100644 --- 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/SentenceAppointmentService.kt @@ -3,6 +3,7 @@ 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.AppointmentDetail 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 @@ -13,11 +14,14 @@ 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.Period import java.time.LocalDate import java.time.ZonedDateTime +import java.util.* +import kotlin.collections.ArrayList @Service -class AppointmentService( +class SentenceAppointmentService( auditedInteractionService: AuditedInteractionService, private val appointmentRepository: AppointmentRepository, private val appointmentTypeRepository: AppointmentTypeRepository, @@ -25,20 +29,54 @@ class AppointmentService( private val eventSentenceRepository: EventSentenceRepository, private val requirementRepository: RequirementRepository, private val licenceConditionRepository: LicenceConditionRepository, + private val staffUserRepository: StaffUserRepository ) : AuditableService(auditedInteractionService) { fun createAppointment( crn: String, createAppointment: CreateAppointment - ): CreatedAppointment { + ): AppointmentDetail { 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 + val userAndLocation = + staffUserRepository.getUserAndLocation(createAppointment.user.username, createAppointment.user.team) + val createAppointments: ArrayList = arrayListOf() - return@audit createdAppointment + createAppointment.let { + val numberOfAppointments = createAppointment.until?.let { + Period.between( + createAppointment.start.toLocalDate(), + it.toLocalDate() + ).days.div(createAppointment.interval.value) + } ?: createAppointment.numberOfAppointments + + for (i in 0 until numberOfAppointments) { + val interval = createAppointment.interval.value * i + createAppointments.add( + CreateAppointment( + createAppointment.user, + createAppointment.type, + createAppointment.start.plusDays(interval.toLong()), + createAppointment.end?.plusDays(interval.toLong()), + createAppointment.interval, + createAppointment.numberOfAppointments, + createAppointment.eventId, + if (i == 0) createAppointment.uuid else UUID.randomUUID(), //needs to be a unique value + createAppointment.requirementId, + createAppointment.licenceConditionId, + createAppointment.until + ) + ) + } + } + + val appointments = createAppointments.map { it.withManager(om, userAndLocation) } + val savedAppointments = appointmentRepository.saveAll(appointments) + val createdAppointments = savedAppointments.map { CreatedAppointment(it.id) } + audit["contactId"] = createdAppointments.joinToString { it.id.toString() } + + return@audit AppointmentDetail(createdAppointments) } } @@ -61,6 +99,14 @@ class AppointmentService( ) } + createAppointment.until?.let { + if (it.isBefore(createAppointment.start)) + throw ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Until cannot be before start time" + ) + } + if (!eventSentenceRepository.existsById(createAppointment.eventId)) { throw NotFoundException("Event", "eventId", createAppointment.eventId) } @@ -93,19 +139,21 @@ class AppointmentService( } } - private fun CreateAppointment.withManager(om: OffenderManager) = Appointment( + private fun CreateAppointment.withManager(om: OffenderManager, userAndLocation: UserLocation) = Appointment( om.person, appointmentTypeRepository.getByCode(type.code), start.toLocalDate(), ZonedDateTime.of(LocalDate.EPOCH, start.toLocalTime(), EuropeLondon), - om.team, - om.staff, + teamId = userAndLocation.teamId, + staffId = userAndLocation.staffId, 0, end?.let { ZonedDateTime.of(LocalDate.EPOCH, end.toLocalTime(), EuropeLondon) }, - om.provider.id, + probationAreaId = userAndLocation.providerId, urn, eventId = eventId, rqmntId = requirementId, - licConditionId = licenceConditionId + licConditionId = licenceConditionId, + createdByUserId = userAndLocation.userId, + officeLocationId = userAndLocation.locationId ) } \ 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/SentenceAppointmentServiceTest.kt similarity index 77% rename from projects/manage-supervision-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/service/AppointmentServiceTest.kt rename to projects/manage-supervision-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/service/SentenceAppointmentServiceTest.kt index 5fe3869253..1307d3a61c 100644 --- 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/SentenceAppointmentServiceTest.kt @@ -13,6 +13,7 @@ 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.api.model.appointment.User 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 @@ -23,7 +24,7 @@ import java.time.ZonedDateTime import java.util.* @ExtendWith(MockitoExtension::class) -class AppointmentServiceTest { +class SentenceAppointmentServiceTest { @Mock lateinit var auditedInteractionService: AuditedInteractionService @@ -46,18 +47,25 @@ class AppointmentServiceTest { @Mock lateinit var licenceConditionRepository: LicenceConditionRepository + @Mock + lateinit var staffUserRepository: StaffUserRepository + @InjectMocks - lateinit var service: AppointmentService + lateinit var service: SentenceAppointmentService private val uuid: UUID = UUID.randomUUID() + private val user = User("user", "team") + @Test fun `licence and requirement id provided`() { val appointment = CreateAppointment( + user, CreateAppointment.Type.InitialAppointmentInOfficeNS, ZonedDateTime.now().plusDays(1), ZonedDateTime.now().plusDays(2), - 1, + interval = CreateAppointment.Interval.WEEK, + numberOfAppointments = 3, PersonGenerator.EVENT_1.id, uuid, requirementId = 2, @@ -87,10 +95,12 @@ class AppointmentServiceTest { @Test fun `start date before end date`() { val appointment = CreateAppointment( + user, CreateAppointment.Type.InitialAppointmentInOfficeNS, - ZonedDateTime.now().plusDays(2), - ZonedDateTime.now().plusDays(1), - 1, + start = ZonedDateTime.now().plusDays(2), + end = ZonedDateTime.now().plusDays(1), + interval = CreateAppointment.Interval.FORTNIGHT, + numberOfAppointments = 3, PersonGenerator.EVENT_1.id, uuid ) @@ -112,13 +122,45 @@ class AppointmentServiceTest { verifyNoInteractions(appointmentTypeRepository) } + @Test + fun `until before end date`() { + val appointment = CreateAppointment( + user, + CreateAppointment.Type.InitialAppointmentInOfficeNS, + start = ZonedDateTime.now().plusDays(2), + until = ZonedDateTime.now().plusDays(1), + interval = CreateAppointment.Interval.FORTNIGHT, + numberOfAppointments = 3, + eventId = PersonGenerator.EVENT_1.id, + uuid = 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 \"Until 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( + user, CreateAppointment.Type.InitialAppointmentInOfficeNS, ZonedDateTime.now().plusDays(1), null, - 1, + interval = CreateAppointment.Interval.FOUR_WEEKS, + numberOfAppointments = 1, 1, uuid ) @@ -144,10 +186,12 @@ class AppointmentServiceTest { @Test fun `requirement not found`() { val appointment = CreateAppointment( + user, CreateAppointment.Type.InitialAppointmentInOfficeNS, ZonedDateTime.now().plusDays(1), ZonedDateTime.now().plusDays(2), - 1, + interval = CreateAppointment.Interval.DAY, + numberOfAppointments = 1, PersonGenerator.EVENT_1.id, uuid, requirementId = 2 @@ -175,12 +219,13 @@ class AppointmentServiceTest { @Test fun `licence not found`() { val appointment = CreateAppointment( + user, CreateAppointment.Type.InitialAppointmentInOfficeNS, ZonedDateTime.now().plusDays(1), ZonedDateTime.now().plusDays(2), - 1, - PersonGenerator.EVENT_1.id, - uuid, + interval = CreateAppointment.Interval.DAY, + eventId = PersonGenerator.EVENT_1.id, + uuid = uuid, licenceConditionId = 3 )