Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MAN-108 manage appointments record an outcome #4433

Merged
merged 15 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ class DataLoader(
entityManager.persist(BusinessInteraction(IdGenerator.getAndIncrement(), it.code, ZonedDateTime.now()))
}
entityManager.persistAll(
AppointmentGenerator.ATTENDED_COMPLIED,
*AppointmentGenerator.APPOINTMENT_TYPES.toTypedArray(),
*AppointmentGenerator.CONTACT_TYPE_OUTCOMES.toTypedArray(),
ContactGenerator.DEFAULT_PROVIDER,
ContactGenerator.DEFAULT_BOROUGH,
ContactGenerator.DEFAULT_DISTRICT,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package uk.gov.justice.digital.hmpps.data.generator

import uk.gov.justice.digital.hmpps.api.model.appointment.CreateAppointment
import uk.gov.justice.digital.hmpps.data.generator.ContactGenerator.generateOutcome
import uk.gov.justice.digital.hmpps.integrations.delius.overview.entity.ContactOutcome
import uk.gov.justice.digital.hmpps.integrations.delius.overview.entity.ContactType
import uk.gov.justice.digital.hmpps.integrations.delius.sentence.entity.ContactTypeOutcome
import uk.gov.justice.digital.hmpps.integrations.delius.sentence.entity.ContactTypeOutcomeId

object AppointmentGenerator {

Expand All @@ -13,4 +17,21 @@ object AppointmentGenerator {
attendanceType: Boolean,
id: Long = IdGenerator.getAndIncrement()
) = ContactType(IdGenerator.getAndIncrement(), code, true, description)

val ATTENDED_COMPLIED = generateOutcome("ATTC", "Attended - Complied", true, true)

val CONTACT_TYPE_OUTCOMES = APPOINTMENT_TYPES.map {
generateContactTypeOutcome(it.id, ATTENDED_COMPLIED.id, it, ATTENDED_COMPLIED)
}

fun generateContactTypeOutcome(
contactTypeId: Long,
contactOutcomeTypeId: Long,
contactType: ContactType,
outcome: ContactOutcome
) = ContactTypeOutcome(
ContactTypeOutcomeId(contactTypeId, contactOutcomeTypeId),
contactType,
outcome
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ object ContactGenerator {
outcome = outcome
)

private fun generateOutcome(code: String, description: String, attendance: Boolean, acceptable: Boolean) =
fun generateOutcome(code: String, description: String, attendance: Boolean, acceptable: Boolean) =
ContactOutcome(IdGenerator.getAndIncrement(), code, description, attendance, acceptable)

private fun generateContactType(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package uk.gov.justice.digital.hmpps

import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
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 org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
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.Outcome
import uk.gov.justice.digital.hmpps.api.model.appointment.User
import uk.gov.justice.digital.hmpps.data.generator.AppointmentGenerator.ATTENDED_COMPLIED
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.ZonedDateTime
import java.util.*

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class AppointmentOutcomeIntegrationTest {

@Autowired
internal lateinit var appointmentRepository: AppointmentRepository

@Autowired
internal lateinit var mockMvc: MockMvc

val outcome = Outcome(123, "ATTC", "N")

@Test
fun `unauthorized status returned`() {
mockMvc
.perform(
MockMvcRequestBuilders.patch("/appointment")
.withJson(outcome)
)
.andExpect(MockMvcResultMatchers.status().isUnauthorized)
}

@Test
fun `when an appointment does not exist returns a 404 response`() {
mockMvc
.perform(
MockMvcRequestBuilders.patch("/appointment")
.withToken()
.withJson(outcome)
)
.andExpect(MockMvcResultMatchers.status().isNotFound)
.andExpect(jsonPath("$.message", equalTo("Appointment with id of 123 not found")))
}

@Test
fun `when an appointment outcome does not exist returns a 404 response`() {
val response = createAppointment()

mockMvc
.perform(
MockMvcRequestBuilders.patch("/appointment")
.withToken()
.withJson(Outcome(response.appointments[0].id, "ABC", "Y"))
)
.andExpect(MockMvcResultMatchers.status().isNotFound)
.andExpect(
jsonPath(
"$.message",
equalTo("ContactTypeOutcome with contact_type_id 8 and outcome code of ABC not found")
)
)

appointmentRepository.deleteById(response.appointments[0].id)
}

@Test
fun `outcome updated`() {
val response = createAppointment()
val createdAppointment = appointmentRepository.findById(response.appointments[0].id).get()

assertNull(createdAppointment.attended)
assertNull(createdAppointment.notes)
assertNull(createdAppointment.outcomeId)
assertNull(createdAppointment.sensitive)

val request = Outcome(response.appointments[0].id, "ATTC", "Y", notes = "my notes")

mockMvc
.perform(
MockMvcRequestBuilders.patch("/appointment")
.withToken()
.withJson(request)
)
.andExpect(MockMvcResultMatchers.status().isOk)

val updatedAppointment = appointmentRepository.findById(response.appointments[0].id).get()

assertEquals("Y", updatedAppointment.attended)
assertEquals(request.notes, updatedAppointment.notes)
assertEquals(ATTENDED_COMPLIED.id, updatedAppointment.outcomeId)
assertFalse(updatedAppointment.sensitive!!)

assertThat(updatedAppointment.type.code, equalTo(createdAppointment.type.code))
assertThat(updatedAppointment.date, equalTo(createdAppointment.date))
assertThat(updatedAppointment.startTime, isCloseTo(createdAppointment.startTime))
assertThat(updatedAppointment.externalReference, equalTo(createdAppointment.externalReference))
assertThat(updatedAppointment.eventId, equalTo(createdAppointment.eventId))
assertThat(updatedAppointment.createdByUserId, equalTo(createdAppointment.createdByUserId))
assertThat(updatedAppointment.staffId, equalTo(createdAppointment.staffId))
assertThat(updatedAppointment.probationAreaId, equalTo(createdAppointment.probationAreaId))
assertThat(updatedAppointment.officeLocationId, equalTo(createdAppointment.officeLocationId))

appointmentRepository.delete(updatedAppointment)
}

private fun createAppointment() = mockMvc.perform(
post("/appointment/${PersonGenerator.PERSON_1.crn}")
.withToken()
.withJson(
CreateAppointment(
User(STAFF_USER_1.username, TEAM.description),
CreateAppointment.Type.PlannedOfficeVisitNS,
ZonedDateTime.now().plusDays(1),
ZonedDateTime.now().plusDays(2),
eventId = PersonGenerator.EVENT_1.id,
uuid = UUID.randomUUID()
)
)
).andReturn().response.contentAsJson<AppointmentDetail>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ 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.MockMvcResultHandlers.print
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
Expand Down Expand Up @@ -48,14 +47,14 @@ class CreateAppointmentIntegrationTests {
@Test
fun `unauthorized status returned`() {
mockMvc
.perform(MockMvcRequestBuilders.get("/appointments/D123456"))
.perform(post("/appointment/D123456"))
.andExpect(MockMvcResultMatchers.status().isUnauthorized)
}

@Test
fun `when offender does not exist retuns a 404 response`() {
fun `when offender does not exist returns a 404 response`() {
mockMvc.perform(
post("/appointments/D123456")
post("/appointment/D123456")
.withToken()
.withJson(
CreateAppointment(
Expand All @@ -75,7 +74,7 @@ class CreateAppointmentIntegrationTests {
@Test
fun `appointment end date before start returns bad request`() {
mockMvc.perform(
post("/appointments/${PersonGenerator.PERSON_1.crn}")
post("/appointment/${PersonGenerator.PERSON_1.crn}")
.withToken()
.withJson(
CreateAppointment(
Expand All @@ -95,12 +94,12 @@ class CreateAppointmentIntegrationTests {
}

@ParameterizedTest
@MethodSource("createAppointments")
@MethodSource("createAppointment")
fun `create a new appointment`(createAppointment: CreateAppointment) {
val person = PersonGenerator.PERSON_1

val response = mockMvc.perform(
post("/appointments/${person.crn}")
post("/appointment/${person.crn}")
.withToken()
.withJson(createAppointment)
)
Expand Down Expand Up @@ -128,7 +127,7 @@ class CreateAppointmentIntegrationTests {
fun `create multiple appointments`(createAppointment: CreateAppointment) {
val person = PersonGenerator.PERSON_1
val response = mockMvc.perform(
post("/appointments/${person.crn}")
post("/appointment/${person.crn}")
.withToken()
.withJson(createAppointment)
)
Expand Down Expand Up @@ -163,7 +162,7 @@ class CreateAppointmentIntegrationTests {
private val user = User(STAFF_USER_1.username, TEAM.description)

@JvmStatic
fun createAppointments() = listOf(
fun createAppointment() = listOf(
CreateAppointment(
user,
CreateAppointment.Type.PlannedOfficeVisitNS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@ 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.api.model.appointment.Outcome
import uk.gov.justice.digital.hmpps.service.AppointmentOutcomeService
import uk.gov.justice.digital.hmpps.service.SentenceAppointmentService

@RestController
@Tag(name = "Sentence")
@RequestMapping("/appointments/{crn}")
@RequestMapping("/appointment")
@PreAuthorize("hasRole('PROBATION_API__MANAGE_A_SUPERVISION__CASE_DETAIL')")
class AppointmentController(private val appointmentService: SentenceAppointmentService) {
class AppointmentController(
private val appointmentService: SentenceAppointmentService,
private val appointmentOutcomeService: AppointmentOutcomeService
) {

@PostMapping
@PostMapping("/{crn}")
@ResponseStatus(HttpStatus.CREATED)
fun createAppointment(@PathVariable crn: String, @RequestBody createAppointment: CreateAppointment) =
appointmentService.createAppointment(crn, createAppointment)

@PatchMapping
@ResponseStatus(HttpStatus.OK)
fun recordOutcome(@RequestBody outcome: Outcome) = appointmentOutcomeService.recordOutcome(outcome)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package uk.gov.justice.digital.hmpps.api.model.appointment

data class Outcome(
val id: Long,
val code: String,
val attended: String,
val sensitive: Boolean? = false,
val notes: String? = null,
)


Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ package uk.gov.justice.digital.hmpps.integrations.delius.sentence.entity

import jakarta.persistence.*
import org.hibernate.annotations.SQLRestriction
import org.hibernate.type.YesNoConverter
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.ContactOutcome
import uk.gov.justice.digital.hmpps.integrations.delius.overview.entity.ContactType
import uk.gov.justice.digital.hmpps.integrations.delius.overview.entity.Person
import java.io.Serializable
import java.time.LocalDate
import java.time.ZoneId
import java.time.ZonedDateTime
Expand Down Expand Up @@ -77,6 +80,19 @@ class Appointment(
@Column(name = "office_location_id")
val officeLocationId: Long? = null,

@Column(name = "attended", columnDefinition = "char(1)")
var attended: String? = null,

@Column(name = "contact_outcome_type_id")
var outcomeId: Long? = null,

@Lob
var notes: String? = null,

@Column(name = "sensitive")
@Convert(converter = YesNoConverter::class)
var sensitive: Boolean? = null,

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "contact_id_generator")
@Column(name = "contact_id")
Expand Down Expand Up @@ -133,3 +149,39 @@ interface AppointmentTypeRepository : JpaRepository<ContactType, Long> {

fun AppointmentTypeRepository.getByCode(code: String) =
findByCode(code) ?: throw NotFoundException("AppointmentType", "code", code)

@Entity
@Table(name = "r_contact_type_outcome")
class ContactTypeOutcome(

@EmbeddedId
val id: ContactTypeOutcomeId,

@ManyToOne
@JoinColumn(name = "contact_type_id", insertable = false, updatable = false)
val type: ContactType,

@ManyToOne
@JoinColumn(name = "contact_outcome_type_id", insertable = false, updatable = false)
val outcome: ContactOutcome,
)

interface ContactTypeOutcomeRepository : JpaRepository<ContactTypeOutcome, ContactTypeOutcomeId> {
fun findByIdContactTypeIdAndOutcomeCode(contactTypeId: Long, code: String): ContactTypeOutcome?
}

fun ContactTypeOutcomeRepository.getByTypeIdAndOutcomeCode(contactTypeId: Long, code: String) =
findByIdContactTypeIdAndOutcomeCode(
contactTypeId, code
) ?: throw NotFoundException("ContactTypeOutcome", "contact_type_id $contactTypeId and outcome code", code)

@Embeddable
class ContactTypeOutcomeId(
@Column(name = "contact_type_id")
val contactTypeId: Long,

@Column(name = "contact_outcome_type_id")
val contactOutcomeTypeId: Long,

) : Serializable

Loading
Loading