Skip to content

Commit d28d2a7

Browse files
wcdkjemmalanigan
andauthored
HMAI-104 Prisons Transactions account code endpoint (#576)
* Init new endpoint, service, gateway route for prisoner transactions * Nomis gateway unit tests for transaction queries * Get transactions service layer unit tests * Expanded transaction controller tests * add test coverage of 404 and 400 errors on transactionController * Integration test for transaction controller * Correcting typos in transactions work * Adding transactions endpoint to more consumers configs * Moved transaction account code check to service layer, updated regex inc consumer config * Revised transactions accounts endpoint uri --------- Co-authored-by: emmalanigan <emma.lanigan@justice.gov.uk>
1 parent 2f6e84e commit d28d2a7

File tree

15 files changed

+814
-4
lines changed

15 files changed

+814
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1
2+
3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.Parameter
5+
import io.swagger.v3.oas.annotations.media.Content
6+
import io.swagger.v3.oas.annotations.responses.ApiResponse
7+
import jakarta.validation.ValidationException
8+
import org.springframework.beans.factory.annotation.Autowired
9+
import org.springframework.web.bind.annotation.GetMapping
10+
import org.springframework.web.bind.annotation.PathVariable
11+
import org.springframework.web.bind.annotation.RequestAttribute
12+
import org.springframework.web.bind.annotation.RequestMapping
13+
import org.springframework.web.bind.annotation.RequestParam
14+
import org.springframework.web.bind.annotation.RestController
15+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException
16+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.DataResponse
17+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Transactions
18+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
19+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.roleconfig.ConsumerFilters
20+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetTransactionsForPersonService
21+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
22+
import java.time.LocalDate
23+
24+
@RestController
25+
@RequestMapping("/v1/prison/{prisonId}/prisoners/{hmppsId}/accounts/{accountCode}/transactions")
26+
class TransactionsController(
27+
@Autowired val auditService: AuditService,
28+
@Autowired val getTransactionsForPersonService: GetTransactionsForPersonService,
29+
) {
30+
@Operation(
31+
summary = "Returns all transactions for a prisoner associated with an account code that they have at a prison.",
32+
description = "<b>Applicable filters</b>: <ul><li>prisons</li></ul>",
33+
responses = [
34+
ApiResponse(responseCode = "200", useReturnTypeSchema = true, description = "Successfully found a prisoner's transactions."),
35+
ApiResponse(
36+
responseCode = "400",
37+
description = "The request data has an invalid format or the prisoner does hot have transactions at the specified prison.",
38+
content = [
39+
Content(
40+
schema =
41+
io.swagger.v3.oas.annotations.media
42+
.Schema(ref = "#/components/schemas/BadRequest"),
43+
),
44+
],
45+
),
46+
ApiResponse(
47+
responseCode = "404",
48+
content = [
49+
Content(
50+
schema =
51+
io.swagger.v3.oas.annotations.media
52+
.Schema(ref = "#/components/schemas/PersonNotFound"),
53+
),
54+
],
55+
),
56+
ApiResponse(
57+
responseCode = "500",
58+
content = [
59+
Content(
60+
schema =
61+
io.swagger.v3.oas.annotations.media
62+
.Schema(ref = "#/components/schemas/InternalServerError"),
63+
),
64+
],
65+
),
66+
],
67+
)
68+
@GetMapping()
69+
fun getTransactionsByAccountCode(
70+
@PathVariable hmppsId: String,
71+
@PathVariable prisonId: String,
72+
@PathVariable accountCode: String,
73+
@RequestAttribute filters: ConsumerFilters?,
74+
@Parameter(description = "Start date for transactions (defaults to today if not supplied)") @RequestParam(required = false, name = "from_date") fromDate: String?,
75+
@Parameter(description = "To date for transactions (defaults to today if not supplied)") @RequestParam(required = false, name = "to_date") toDate: String?,
76+
): DataResponse<Transactions?> {
77+
var startDate = LocalDate.now().toString()
78+
var endDate = LocalDate.now().toString()
79+
80+
if (fromDate == null && toDate != null || toDate == null && fromDate != null) {
81+
throw ValidationException("Both fromDate and toDate must be supplied if one is populated")
82+
}
83+
84+
if (fromDate != null && toDate != null) {
85+
startDate = fromDate
86+
endDate = toDate
87+
}
88+
89+
val response = getTransactionsForPersonService.execute(hmppsId, prisonId, accountCode, startDate, endDate, filters)
90+
91+
if (response.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) {
92+
throw EntityNotFoundException("Could not find transactions with id: $hmppsId")
93+
}
94+
95+
if (response.hasError(UpstreamApiError.Type.BAD_REQUEST)) {
96+
throw ValidationException("Either invalid HMPPS ID: $hmppsId at or incorrect prison: $prisonId")
97+
}
98+
99+
auditService.createEvent("GET_TRANSACTIONS_FOR_PERSON", mapOf("hmppsId" to hmppsId, "prisonId" to prisonId, "fromDate" to fromDate, "toDate" to toDate))
100+
return DataResponse(response.data)
101+
}
102+
}

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/gateways/NomisGateway.kt

+34-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskCategor
1616
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Sentence
1717
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.SentenceAdjustment
1818
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.SentenceKeyDates
19+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Transactions
1920
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
2021
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisAccounts
2122
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisAddress
@@ -201,7 +202,11 @@ class NomisGateway(
201202

202203
return when (result) {
203204
is WebClientWrapperResponse.Success -> {
204-
Response(data = result.data.latestPrisonTerm.sentenceAdjustments.toSentenceAdjustment())
205+
Response(
206+
data =
207+
result.data.latestPrisonTerm.sentenceAdjustments
208+
.toSentenceAdjustment(),
209+
)
205210
}
206211

207212
is WebClientWrapperResponse.Error -> {
@@ -331,6 +336,34 @@ class NomisGateway(
331336
}
332337
}
333338

339+
fun getTransactionsForPerson(
340+
prisonId: String,
341+
nomisNumber: String,
342+
accountCode: String,
343+
fromDate: String,
344+
toDate: String,
345+
): Response<Transactions?> {
346+
val result =
347+
webClient.request<Transactions>(
348+
HttpMethod.GET,
349+
"/api/v1/prison/$prisonId/offenders/$nomisNumber/accounts/$accountCode/transactions?from_date=$fromDate&to_date=$toDate",
350+
authenticationHeader(),
351+
UpstreamApi.NOMIS,
352+
)
353+
return when (result) {
354+
is WebClientWrapperResponse.Success -> {
355+
Response(data = result.data)
356+
}
357+
358+
is WebClientWrapperResponse.Error -> {
359+
Response(
360+
data = null,
361+
errors = result.errors,
362+
)
363+
}
364+
}
365+
}
366+
334367
private fun authenticationHeader(): Map<String, String> {
335368
val token = hmppsAuthGateway.getClientToken("NOMIS")
336369

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps
2+
3+
data class Type(
4+
val code: String,
5+
val desc: String,
6+
)
7+
8+
data class Transaction(
9+
val id: String,
10+
val type: Type,
11+
val description: String,
12+
val amount: Int,
13+
val date: String,
14+
)
15+
16+
data class Transactions(
17+
val transactions: List<Transaction> = emptyList(),
18+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services
2+
3+
import jakarta.validation.ValidationException
4+
import org.springframework.beans.factory.annotation.Autowired
5+
import org.springframework.stereotype.Service
6+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.NomisGateway
7+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
8+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Transactions
9+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
10+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
11+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.roleconfig.ConsumerFilters
12+
13+
@Service
14+
class GetTransactionsForPersonService(
15+
@Autowired val nomisGateway: NomisGateway,
16+
@Autowired val getPersonService: GetPersonService,
17+
) {
18+
fun execute(
19+
hmppsId: String,
20+
prisonId: String,
21+
accountCode: String,
22+
startDate: String,
23+
endDate: String,
24+
filters: ConsumerFilters? = null,
25+
): Response<Transactions?> {
26+
if (
27+
filters != null && !filters.matchesPrison(prisonId)
28+
) {
29+
return Response(
30+
data = null,
31+
errors = listOf(UpstreamApiError(UpstreamApi.NOMIS, UpstreamApiError.Type.ENTITY_NOT_FOUND, "Not found")),
32+
)
33+
}
34+
35+
if (accountCode !in listOf("spends", "savings", "cash")) {
36+
throw ValidationException("Account code must either be 'spends', 'savings', or 'cash'")
37+
}
38+
39+
val personResponse = getPersonService.getNomisNumber(hmppsId = hmppsId)
40+
41+
if (personResponse == null) {
42+
return Response(
43+
data = null,
44+
errors = listOf(UpstreamApiError(UpstreamApi.NOMIS, UpstreamApiError.Type.ENTITY_NOT_FOUND, "Nomis number not found")),
45+
)
46+
}
47+
48+
val nomisNumber = personResponse.data?.nomisNumber
49+
50+
if (nomisNumber == null) {
51+
return Response(
52+
data = null,
53+
errors = personResponse.errors,
54+
)
55+
}
56+
57+
val nomisTransactions =
58+
nomisGateway.getTransactionsForPerson(
59+
prisonId,
60+
nomisNumber,
61+
accountCode,
62+
startDate,
63+
endDate,
64+
)
65+
66+
if (nomisTransactions.errors.isNotEmpty()) {
67+
return Response(
68+
data = null,
69+
errors = nomisTransactions.errors,
70+
)
71+
}
72+
73+
return Response(
74+
data = nomisTransactions.data,
75+
errors = emptyList(),
76+
)
77+
}
78+
}

src/main/resources/application-dev.yml

+3
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ authorisation:
130130
- "/v1/prison/prisoners"
131131
- "/v1/prison/.*/prisoners/.*/balances$"
132132
- "/v1/prison/.*/prisoners/.*/balances/[^/]*$"
133+
- "/v1/prison/.*/prisoners/.*/accounts/.*/transactions"
133134
filters:
134135
kilco:
135136
include:
@@ -139,6 +140,7 @@ authorisation:
139140
- "/v1/prison/prisoners/[^/]*$"
140141
- "/v1/prison/.*/prisoners/.*/balances$"
141142
- "/v1/prison/.*/prisoners/.*/balances/[^/]*$"
143+
- "/v1/prison/.*/prisoners/.*/accounts/.*/transactions"
142144
filters:
143145
meganexus:
144146
include:
@@ -156,6 +158,7 @@ authorisation:
156158
- "/v1/prison/prisoners"
157159
- "/v1/prison/.*/prisoners/.*/balances$"
158160
- "/v1/prison/.*/prisoners/.*/balances/[^/]*$"
161+
- "/v1/prison/.*/prisoners/.*/accounts/.*/transactions"
159162
filters:
160163
serco:
161164
include:

src/main/resources/application-integration-test.yml

+2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ authorisation:
8585
- "/v1/prison/prisoners/[^/]*$"
8686
- "/v1/prison/prisoners"
8787
- "/v1/prison/.*/prisoners/.*/balances$"
88+
- "/v1/prison/.*/prisoners/.*/accounts/.*/transactions"
8889
- "/v1/prison/.*/prisoners/.*/balances/[^/]*$"
8990
filters:
9091
config-test:
@@ -98,6 +99,7 @@ authorisation:
9899
limited-prisons:
99100
include:
100101
- "/v1/prison/prisoners/[^/]*$"
102+
- "/v1/prison/.*/prisoners/.*/accounts/.*/transactions"
101103
filters:
102104
prisons:
103105
- ABC

src/main/resources/application-local-docker.yml

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ authorisation:
5959
- "/v1/prison/prisoners/[^/]*$"
6060
- "/v1/prison/prisoners"
6161
- "/v1/prison/.*/prisoners/.*/balances$"
62+
- "/v1/prison/.*/prisoners/.*/accounts/.*/transactions"
6263
filters:
6364
config-test:
6465
include:

src/main/resources/application-local.yml

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ authorisation:
6666
- "/v1/prison/prisoners/[^/]*$"
6767
- "/v1/prison/prisoners"
6868
- "/v1/prison/.*/prisoners/.*/balances$"
69+
- "/v1/prison/.*/prisoners/.*/accounts/.*/transactions"
6970
filters:
7071
config-test:
7172
include:

src/main/resources/application-test.yml

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ authorisation:
8888
- "/v1/prison/prisoners"
8989
- "/v1/prison/.*/prisoners/.*/balances$"
9090
- "/v1/prison/.*/prisoners/.*/balances/[^/]*$"
91+
- "/v1/prison/.*/prisoners/.*/accounts/.*/transactions"
9192
config-test:
9293
include:
9394
- "/v1/config/authorisation"

0 commit comments

Comments
 (0)