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

HMAI-270 - Get contacts controller + new PaginatedResponse changes #703

Merged
merged 9 commits into from
Mar 4, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import jakarta.validation.ValidationException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestAttribute
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
Expand All @@ -20,13 +21,17 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ImageMetada
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.OffenderSearchResponse
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Person
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.PersonName
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.PrisonerContact
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError.Type.ENTITY_NOT_FOUND
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.interfaces.toPaginatedResponse
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.roleconfig.ConsumerFilters
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetImageMetadataForPersonService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetNameForPersonService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonsService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPrisonerContactsService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.util.PaginatedResponse
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.util.paginateWith
Expand All @@ -41,6 +46,7 @@ class PersonController(
@Autowired val getPersonsService: GetPersonsService,
@Autowired val getNameForPersonService: GetNameForPersonService,
@Autowired val getImageMetadataForPersonService: GetImageMetadataForPersonService,
@Autowired val getPrisonerContactsService: GetPrisonerContactsService,
@Autowired val auditService: AuditService,
) {
@GetMapping
Expand Down Expand Up @@ -157,6 +163,36 @@ class PersonController(
return DataResponse(response.data)
}

@GetMapping("{hmppsId}/contacts")
@Operation(
summary = "Returns a prisoners contacts.",
responses = [
ApiResponse(responseCode = "200", useReturnTypeSchema = true, description = "Successfully found a prisoners contacts."),
ApiResponse(responseCode = "404", content = [Content(schema = Schema(ref = "#/components/schemas/PersonNotFound"))]),
ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]),
],
)
fun getPrisonersContacts(
@Parameter(description = "The HMPPS ID of the prisoner") @PathVariable hmppsId: String,
@Parameter(description = "The page number (starting from 1)", schema = Schema(minimum = "1")) @RequestParam(required = false, defaultValue = "1", name = "page") page: Int,
@Parameter(description = "The maximum number of results for a page", schema = Schema(minimum = "1")) @RequestParam(required = false, defaultValue = "10", name = "perPage") perPage: Int,
@RequestAttribute filters: ConsumerFilters?,
): PaginatedResponse<PrisonerContact> {
val response = getPrisonerContactsService.execute(hmppsId, page, perPage, filters)

if (response.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) {
throw EntityNotFoundException("Could not find person with id: $hmppsId")
}

if (response.hasError(UpstreamApiError.Type.BAD_REQUEST)) {
throw ValidationException("Invalid HMPPS ID: $hmppsId")
}

auditService.createEvent("GET_PRISONER_CONTACTS", mapOf("hmppsId" to hmppsId))

return response.data.toPaginatedResponse()
}

private fun isValidISODateFormat(dateString: String): Boolean =
try {
LocalDate.parse(dateString, DateTimeFormatter.ISO_DATE)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps

import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.interfaces.IPaginatedObject

data class PaginatedPrisonerContacts(
var contacts: List<PrisonerContact>,
val isLast: Boolean,
val numberOfElements: Int,
val number: Int,
val size: Int,
val totalElements: Long,
val totalPages: Int,
)
override val content: List<PrisonerContact>,
override val isLastPage: Boolean,
override val count: Int,
override val page: Int,
override val perPage: Int,
override val totalCount: Long,
override val totalPages: Int,
) : IPaginatedObject<PrisonerContact>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps

import com.fasterxml.jackson.annotation.JsonProperty
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.personalRelationships.interfaces.IRelationship

data class Relationship(
@JsonProperty("relationshipTypeCode")
override val relationshipTypeCode: String?,
@JsonProperty("relationshipTypeDescription")
override val relationshipTypeDescription: String?,
@JsonProperty("relationshipToPrisonerCode")
override val relationshipToPrisonerCode: String?,
@JsonProperty("relationshipToPrisonerDescription")
override val relationshipToPrisonerDescription: String?,
@JsonProperty("approvedVisitor")
val approvedPrisoner: Boolean?,
@JsonProperty("nextOfKin")
val nextOfKin: Boolean?,
@JsonProperty("emergencyContact")
val emergencyContact: Boolean?,
@JsonProperty("isRelationshipActive")
val isRelationshipActive: Boolean?,
@JsonProperty("currentTerm")
val currentTerm: Boolean?,
@JsonProperty("comments")
val comments: String?,
) : IRelationship
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.interfaces

import uk.gov.justice.digital.hmpps.hmppsintegrationapi.util.PaginatedResponse

interface IPaginatedObject<T> {
val content: List<T>
val isLastPage: Boolean
val count: Int
val page: Int
val perPage: Int
val totalCount: Long
val totalPages: Int
}

fun <T> IPaginatedObject<T>?.toPaginatedResponse(): PaginatedResponse<T> = PaginatedResponse.fromPaginatedObject(this)
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ data class PRPaginatedPrisonerContacts(
) {
fun toPaginatedPrisonerContacts(): PaginatedPrisonerContacts =
PaginatedPrisonerContacts(
contacts = this.contacts.map { it.toPrisonerContact() },
isLast = this.last,
numberOfElements = this.numberOfElements.toInt(),
number = this.number.toInt(),
size = this.size.toInt(),
totalElements = this.totalElements,
content = this.contacts.map { it.toPrisonerContact() },
isLastPage = this.last,
count = this.numberOfElements.toInt(),
page = this.number.toInt(),
perPage = this.size.toInt(),
totalCount = this.totalElements,
totalPages = this.totalPages.toInt(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12803,7 +12803,8 @@
"page": {
"type": "integer",
"format": "int32",
"minimum": 0
"minimum": 0,
"example": 1
},
"size": {
"type": "integer",
Expand All @@ -12830,7 +12831,8 @@
},
"pageSize": {
"type": "integer",
"format": "int32"
"format": "int32",
"example": 10
},
"unpaged": {
"type": "boolean"
Expand All @@ -12840,7 +12842,8 @@
},
"pageNumber": {
"type": "integer",
"format": "int32"
"format": "int32",
"example": 1
}
}
},
Expand Down Expand Up @@ -13061,29 +13064,34 @@
},
"totalElements": {
"type": "integer",
"format": "int64"
"format": "int64",
"example": 1
},
"totalPages": {
"type": "integer",
"format": "int32"
"format": "int32",
"example": 1
},
"first": {
"type": "boolean"
},
"size": {
"type": "integer",
"format": "int32"
"format": "int32",
"example": 10
},
"number": {
"type": "integer",
"format": "int32"
"format": "int32",
"example": 1
},
"sort": {
"$ref": "#/components/schemas/SortObject"
},
"numberOfElements": {
"type": "integer",
"format": "int32"
"format": "int32",
"example": 1
},
"empty": {
"type": "boolean"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ fun <T> List<T>.paginateWith(
val end = (start + paginationOptions.pageSize).coerceAtMost(this.size)

if (start > end) {
return PaginatedResponse(PageImpl(listOf<T>(), paginationOptions, this.size.toLong()))
return PaginatedResponse.fromPageableResponse(PageImpl(listOf<T>(), paginationOptions, this.size.toLong()))
}

return PaginatedResponse(PageImpl(this.subList(start, end), paginationOptions, this.count().toLong()))
return PaginatedResponse.fromPageableResponse(PageImpl(this.subList(start, end), paginationOptions, this.count().toLong()))
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,67 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.util

import io.swagger.v3.oas.annotations.media.Schema
import org.springframework.data.domain.Page
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.interfaces.IPaginatedObject

class PaginatedResponse<T>(
@Schema(hidden = true) pageableResponse: Page<T>,
val data: List<T>,
val pagination: Pagination,
) {
val data: List<T> = pageableResponse.content
val pagination: Pagination = Pagination(pageableResponse)
companion object {
fun <T> fromPageableResponse(pageableResponse: Page<T>): PaginatedResponse<T> {
val data: List<T> = pageableResponse.content
val pagination =
Pagination(
isLastPage = pageableResponse.isLast,
count = pageableResponse.numberOfElements,
page = pageableResponse.number + 1,
perPage = pageableResponse.size,
totalCount = pageableResponse.totalElements,
totalPages = pageableResponse.totalPages,
)
return PaginatedResponse(data, pagination)
}

inner class Pagination(
pageableResponse: Page<T>,
) {
@Schema(description = "Is the current page the last one?", example = "true")
val isLastPage: Boolean = pageableResponse.isLast

@Schema(description = "The number of results in `data` for the current page", example = "1")
val count: Int = pageableResponse.numberOfElements

@Schema(description = "The current page number", example = "1")
val page: Int = pageableResponse.number + 1

@Schema(description = "The maximum number of results in `data` for a page", example = "10")
val perPage: Int = pageableResponse.size

@Schema(description = "The total number of results in `data` across all pages", example = "1")
val totalCount: Long = pageableResponse.totalElements

@Schema(description = "The total number of pages", example = "1")
val totalPages: Int = pageableResponse.totalPages
fun <T> fromPaginatedObject(paginatedObject: IPaginatedObject<T>?): PaginatedResponse<T> {
if (paginatedObject == null) {
return PaginatedResponse(
emptyList(),
Pagination(
isLastPage = true,
count = 0,
page = 0,
perPage = 0,
totalCount = 0,
totalPages = 0,
),
)
}
return PaginatedResponse(
data = paginatedObject.content,
Pagination(
isLastPage = paginatedObject.isLastPage,
count = paginatedObject.count,
page = paginatedObject.page,
perPage = paginatedObject.perPage,
totalCount = paginatedObject.totalCount,
totalPages = paginatedObject.totalPages,
),
)
}
}
}

data class Pagination(
@Schema(description = "Is the current page the last one?", example = "true")
val isLastPage: Boolean,
@Schema(description = "The number of results in `data` for the current page", example = "1")
val count: Int,
@Schema(description = "The current page number", example = "1")
val page: Int,
@Schema(description = "The maximum number of results in `data` for a page", example = "10")
val perPage: Int,
@Schema(description = "The total number of results in `data` across all pages", example = "1")
val totalCount: Long,
@Schema(description = "The total number of pages", example = "1")
val totalPages: Int,
)
1 change: 1 addition & 0 deletions src/main/resources/application-integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ authorisation:
- "/v1/visit/[^/]*$"
- "/v1/prison/.*/visit/search[^/]*$"
- "/v1/contacts/[^/]*$"
- "/v1/persons/.*/contacts[^/]*$"
filters:
config-test:
include:
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ authorisation:
- "/v1/visit/[^/]*$"
- "/v1/prison/.*/visit/search[^/]*$"
- "/v1/contacts/[^/]*$"
- "/v1/persons/.*/contacts[^/]*$"
config-test:
include:
- "/v1/config/authorisation"
Expand Down
Loading