Skip to content

Commit 005b807

Browse files
authored
HMAI 99 - Implement GET /v1/prison/{prisonId}/prisoners/{hmppsId}/balances/{accountCode} (#572)
* Add concept of accountCodes to GetBalancesForPersonService so we can bring back a single balance * add logic to handle 3 possible account codes otherwise throw a bad request error * add test to check prison filter is still respected when account code provided * add new endpoint on BalancesController which calls getBalancesForPersonService; add endpoint on relevant yml files * getBalanceForPerson in balance controller handles 404 responses * getBalanceForPerson in balance controller handles 400 responses * getBalanceForPerson in balance controller handles 500 responses * amend logic in GetBalancesForPersonService to throw an exception within the getBalance function so that exception is only thrown if relevant account code is null * add integration test * Rename balance to balances * Change flow to call getBalance from controller * Remove unused argument. * Replace confusing it.equals with == * Make account code non optional.
1 parent ef8e520 commit 005b807

File tree

8 files changed

+259
-26
lines changed

8 files changed

+259
-26
lines changed

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/BalancesController.kt

+37-2
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,52 @@ class BalancesController(
4545
@PathVariable prisonId: String,
4646
@RequestAttribute filters: ConsumerFilters?,
4747
): DataResponse<Balances?> {
48-
val response = getBalancesForPersonService.execute(prisonId, hmppsId, filters)
48+
val response = getBalancesForPersonService.execute(prisonId, hmppsId, filters = filters)
4949

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

5454
if (response.hasError(UpstreamApiError.Type.BAD_REQUEST)) {
55-
throw ValidationException("Either invalid HMPPS ID: $hmppsId at or incorrect prison: $prisonId")
55+
throw ValidationException("Either invalid HMPPS ID: $hmppsId or incorrect prison: $prisonId")
5656
}
5757

5858
auditService.createEvent("GET_BALANCES_FOR_PERSON", mapOf("hmppsId" to hmppsId, "prisonId" to prisonId))
5959
return DataResponse(response.data)
6060
}
61+
62+
@GetMapping("/{accountCode}")
63+
@Operation(
64+
summary = "Returns a specific account for a prisoner that they have at a prison, based on the account code provided.",
65+
description = "<b>Applicable filters</b>: <ul><li>prisons</li></ul>",
66+
responses = [
67+
ApiResponse(responseCode = "200", useReturnTypeSchema = true, description = "Successfully found a prisoner's account."),
68+
ApiResponse(
69+
responseCode = "400",
70+
description = "The HMPPS ID provided has an invalid format, the account code is not one of the allowable accounts or the prisoner does hot have accounts at the specified prison.",
71+
content = [Content(schema = Schema(ref = "#/components/schemas/BadRequest"))],
72+
),
73+
ApiResponse(responseCode = "404", content = [Content(schema = Schema(ref = "#/components/schemas/PersonNotFound"))]),
74+
ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]),
75+
],
76+
)
77+
fun getBalanceForPerson(
78+
@PathVariable hmppsId: String,
79+
@PathVariable prisonId: String,
80+
@PathVariable accountCode: String,
81+
@RequestAttribute filters: ConsumerFilters?,
82+
): DataResponse<Balances?> {
83+
val response = getBalancesForPersonService.getBalance(prisonId, hmppsId, accountCode, filters = filters)
84+
85+
if (response.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) {
86+
throw EntityNotFoundException("Could not find person with id: $hmppsId")
87+
}
88+
89+
if (response.hasError(UpstreamApiError.Type.BAD_REQUEST)) {
90+
throw ValidationException("Either invalid HMPPS ID: $hmppsId, invalid Account Code: $accountCode or incorrect prison: $prisonId")
91+
}
92+
93+
auditService.createEvent("GET_BALANCE_FOR_PERSON", mapOf("hmppsId" to hmppsId, "accountCode" to accountCode, "prisonId" to prisonId))
94+
return DataResponse(response.data)
95+
}
6196
}

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/GetBalancesForPersonService.kt

+32
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,36 @@ class GetBalancesForPersonService(
7171
errors = emptyList(),
7272
)
7373
}
74+
75+
fun getBalance(
76+
prisonId: String,
77+
hmppsId: String,
78+
accountCode: String,
79+
filters: ConsumerFilters? = null,
80+
): Response<Balances?> {
81+
if (!listOf("spends", "savings", "cash").any { it == accountCode }) {
82+
return Response(
83+
data = null,
84+
errors = listOf(UpstreamApiError(type = UpstreamApiError.Type.BAD_REQUEST, causedBy = UpstreamApi.NOMIS)),
85+
)
86+
}
87+
88+
val response = execute(prisonId, hmppsId, filters)
89+
90+
if (response.errors.isNotEmpty()) {
91+
return Response(
92+
data = null,
93+
errors = response.errors,
94+
)
95+
}
96+
97+
val accountBalance = response.data?.balances?.filter { it.accountCode == accountCode }?.firstOrNull()
98+
99+
if (accountBalance == null) {
100+
throw IllegalStateException("Error occurred while trying to get accounts for person with id: $hmppsId")
101+
}
102+
103+
val balance = Balances(balances = listOf(accountBalance))
104+
return Response(data = balance, errors = emptyList())
105+
}
74106
}

src/main/resources/application-dev.yml

+3
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ authorisation:
129129
- "/v1/prison/prisoners/[^/]*$"
130130
- "/v1/prison/prisoners"
131131
- "/v1/prison/.*/prisoners/.*/balances$"
132+
- "/v1/prison/.*/prisoners/.*/balances/[^/]*$"
132133
filters:
133134
kilco:
134135
include:
@@ -137,6 +138,7 @@ authorisation:
137138
- "/v1/prison/prisoners"
138139
- "/v1/prison/prisoners/[^/]*$"
139140
- "/v1/prison/.*/prisoners/.*/balances$"
141+
- "/v1/prison/.*/prisoners/.*/balances/[^/]*$"
140142
filters:
141143
meganexus:
142144
include:
@@ -153,6 +155,7 @@ authorisation:
153155
- "/v1/prison/prisoners/[^/]*$"
154156
- "/v1/prison/prisoners"
155157
- "/v1/prison/.*/prisoners/.*/balances$"
158+
- "/v1/prison/.*/prisoners/.*/balances/[^/]*$"
156159
filters:
157160
serco:
158161
include:

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

+1
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/.*/balances/[^/]*$"
8889
filters:
8990
config-test:
9091
include:

src/main/resources/application-test.yml

+2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ authorisation:
8787
- "/v1/prison/prisoners/[^/]*$"
8888
- "/v1/prison/prisoners"
8989
- "/v1/prison/.*/prisoners/.*/balances$"
90+
- "/v1/prison/.*/prisoners/.*/balances/[^/]*$"
9091
config-test:
9192
include:
9293
- "/v1/config/authorisation"
@@ -96,6 +97,7 @@ authorisation:
9697
limited-prisons:
9798
include:
9899
- "/v1/prison/.*/prisoners/.*/balances$"
100+
- "/v1/prison/.*/prisoners/.*/balances/[^/]*$"
99101
filters:
100102
prisons:
101103
- XYZ

src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/BalancesControllerTest.kt

+122-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1
22

3-
import com.fasterxml.jackson.databind.ObjectMapper
43
import io.kotest.core.spec.style.DescribeSpec
54
import io.kotest.matchers.shouldBe
65
import io.kotest.matchers.string.shouldContain
@@ -36,10 +35,11 @@ class BalancesControllerTest(
3635
) : DescribeSpec({
3736
val hmppsId = "200313116M"
3837
val prisonId = "ABC"
38+
val accountCode = "spends"
3939

40-
val basePath = "/v1/prison/$prisonId/prisoners/$hmppsId/balances"
40+
val balancesPath = "/v1/prison/$prisonId/prisoners/$hmppsId/balances"
41+
val accountCodePath = "/v1/prison/$prisonId/prisoners/$hmppsId/balances/$accountCode"
4142
val mockMvc = IntegrationAPIMockMvc(springMockMvc)
42-
val objectMapper = ObjectMapper()
4343
val balance =
4444
Balances(
4545
balances =
@@ -49,20 +49,27 @@ class BalancesControllerTest(
4949
AccountBalance(accountCode = "cash", amount = 103),
5050
),
5151
)
52+
val singleBalance =
53+
Balances(
54+
balances =
55+
listOf(
56+
AccountBalance(accountCode = "spends", amount = 201),
57+
),
58+
)
5259

5360
it("gets the balances for a person with the matching ID") {
54-
mockMvc.performAuthorised(basePath)
61+
mockMvc.performAuthorised(balancesPath)
5562

56-
verify(getBalancesForPersonService, VerificationModeFactory.times(1)).execute(prisonId, hmppsId, null)
63+
verify(getBalancesForPersonService, VerificationModeFactory.times(1)).execute(prisonId, hmppsId, filters = null)
5764
}
5865

5966
it("returns the correct balances data") {
60-
whenever(getBalancesForPersonService.execute(prisonId, hmppsId, null)).thenReturn(
67+
whenever(getBalancesForPersonService.execute(prisonId, hmppsId, filters = null)).thenReturn(
6168
Response(
6269
data = balance,
6370
),
6471
)
65-
val result = mockMvc.performAuthorised(basePath)
72+
val result = mockMvc.performAuthorised(balancesPath)
6673
result.response.contentAsString.shouldContain(
6774
"""
6875
"data": {
@@ -86,8 +93,8 @@ class BalancesControllerTest(
8693
}
8794

8895
it("calls the API with the correct filters") {
89-
mockMvc.performAuthorisedWithCN(basePath, "limited-prisons")
90-
verify(getBalancesForPersonService, times(1)).execute(prisonId, hmppsId, ConsumerFilters(prisons = listOf("XYZ")))
96+
mockMvc.performAuthorisedWithCN(balancesPath, "limited-prisons")
97+
verify(getBalancesForPersonService, times(1)).execute(prisonId, hmppsId, filters = ConsumerFilters(prisons = listOf("XYZ")))
9198
}
9299

93100
it("returns a 404 NOT FOUND status code when person isn't found in probation offender search") {
@@ -104,13 +111,13 @@ class BalancesControllerTest(
104111
),
105112
)
106113

107-
val result = mockMvc.performAuthorised(basePath)
114+
val result = mockMvc.performAuthorised(balancesPath)
108115

109116
result.response.status.shouldBe(HttpStatus.NOT_FOUND.value())
110117
}
111118

112119
it("returns a 404 NOT FOUND status code when person isn't found in Nomis") {
113-
whenever(getBalancesForPersonService.execute(prisonId, hmppsId, null)).thenReturn(
120+
whenever(getBalancesForPersonService.execute(prisonId, hmppsId, filters = null)).thenReturn(
114121
Response(
115122
data = null,
116123
errors =
@@ -123,13 +130,112 @@ class BalancesControllerTest(
123130
),
124131
)
125132

126-
val result = mockMvc.performAuthorised(basePath)
133+
val result = mockMvc.performAuthorised(balancesPath)
127134

128135
result.response.status.shouldBe(HttpStatus.NOT_FOUND.value())
129136
}
130137

131138
it("returns a 400 BAD REQUEST status code when account isn't found in the upstream API") {
132-
whenever(getBalancesForPersonService.execute(prisonId, hmppsId, null)).thenReturn(
139+
whenever(getBalancesForPersonService.execute(prisonId, hmppsId, filters = null)).thenReturn(
140+
Response(
141+
data = null,
142+
errors =
143+
listOf(
144+
UpstreamApiError(
145+
type = UpstreamApiError.Type.BAD_REQUEST,
146+
causedBy = UpstreamApi.NOMIS,
147+
),
148+
),
149+
),
150+
)
151+
152+
val result = mockMvc.performAuthorised(balancesPath)
153+
154+
result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value())
155+
}
156+
157+
it("returns a 500 INTERNAL SERVER ERROR status code when balance isn't found in the upstream API") {
158+
whenever(getBalancesForPersonService.execute(prisonId, hmppsId, filters = null)).thenThrow(
159+
IllegalStateException("Error occurred while trying to get accounts for person with id: $hmppsId"),
160+
)
161+
162+
val result = mockMvc.performAuthorised(balancesPath)
163+
164+
result.response.status.shouldBe(HttpStatus.INTERNAL_SERVER_ERROR.value())
165+
}
166+
167+
it("gets the balance for the relevant account code for a person with the matching ID") {
168+
mockMvc.performAuthorised(accountCodePath)
169+
170+
verify(getBalancesForPersonService, VerificationModeFactory.times(1)).getBalance(prisonId, hmppsId, accountCode, null)
171+
}
172+
173+
it("returns the correct balance data when given an account code") {
174+
whenever(getBalancesForPersonService.getBalance(prisonId, hmppsId, accountCode, filters = null)).thenReturn(
175+
Response(
176+
data = singleBalance,
177+
),
178+
)
179+
val result = mockMvc.performAuthorised(accountCodePath)
180+
result.response.contentAsString.shouldContain(
181+
"""
182+
"data": {
183+
"balances": [
184+
{
185+
"accountCode": "spends",
186+
"amount": 201
187+
}
188+
]
189+
}
190+
""".removeWhitespaceAndNewlines(),
191+
)
192+
}
193+
194+
it("calls the API with the correct filters") {
195+
mockMvc.performAuthorisedWithCN(accountCodePath, "limited-prisons")
196+
verify(getBalancesForPersonService, times(1)).getBalance(prisonId, hmppsId, accountCode, filters = ConsumerFilters(prisons = listOf("XYZ")))
197+
}
198+
199+
it("returns a 404 NOT FOUND status code when person isn't found in probation offender search") {
200+
whenever(getBalancesForPersonService.getBalance(prisonId, hmppsId, accountCode)).thenReturn(
201+
Response(
202+
data = null,
203+
errors =
204+
listOf(
205+
UpstreamApiError(
206+
causedBy = UpstreamApi.PROBATION_OFFENDER_SEARCH,
207+
type = UpstreamApiError.Type.ENTITY_NOT_FOUND,
208+
),
209+
),
210+
),
211+
)
212+
213+
val result = mockMvc.performAuthorised(accountCodePath)
214+
215+
result.response.status.shouldBe(HttpStatus.NOT_FOUND.value())
216+
}
217+
218+
it("returns a 404 NOT FOUND status code when person isn't found in Nomis") {
219+
whenever(getBalancesForPersonService.getBalance(prisonId, hmppsId, accountCode, filters = null)).thenReturn(
220+
Response(
221+
data = null,
222+
errors =
223+
listOf(
224+
UpstreamApiError(
225+
causedBy = UpstreamApi.NOMIS,
226+
type = UpstreamApiError.Type.ENTITY_NOT_FOUND,
227+
),
228+
),
229+
),
230+
)
231+
232+
val result = mockMvc.performAuthorised(accountCodePath)
233+
234+
result.response.status.shouldBe(HttpStatus.NOT_FOUND.value())
235+
}
236+
237+
it("returns a 400 BAD REQUEST status code when account isn't found in the upstream API") {
238+
whenever(getBalancesForPersonService.getBalance(prisonId, hmppsId, accountCode, filters = null)).thenReturn(
133239
Response(
134240
data = null,
135241
errors =
@@ -142,17 +248,17 @@ class BalancesControllerTest(
142248
),
143249
)
144250

145-
val result = mockMvc.performAuthorised(basePath)
251+
val result = mockMvc.performAuthorised(accountCodePath)
146252

147253
result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value())
148254
}
149255

150256
it("returns a 500 INTERNAL SERVER ERROR status code when balance isn't found in the upstream API") {
151-
whenever(getBalancesForPersonService.execute(prisonId, hmppsId, null)).thenThrow(
257+
whenever(getBalancesForPersonService.getBalance(prisonId, hmppsId, accountCode, filters = null)).thenThrow(
152258
IllegalStateException("Error occurred while trying to get accounts for person with id: $hmppsId"),
153259
)
154260

155-
val result = mockMvc.performAuthorised(basePath)
261+
val result = mockMvc.performAuthorised(accountCodePath)
156262

157263
result.response.status.shouldBe(HttpStatus.INTERNAL_SERVER_ERROR.value())
158264
}

0 commit comments

Comments
 (0)