From fb826cfd5e506a945eaa390c4099d3434d4dd920 Mon Sep 17 00:00:00 2001 From: Yasin Mustafa Date: Tue, 19 Sep 2023 19:05:58 +0100 Subject: [PATCH] ADJUST1-47 Determine consecutive / concurrent ADAs and label accordingly for ADA review screen (#57) * ADJUST1-47 Determine consecutive ADAs and label accordingly for search screen * ADJUST1-47 Code tidy * ADJUST1-47 More refactoring of code and adding unt tests for the consecutive / concurrent / forthwith labelling * ADJUST1-47 lint fix and code tidy * ADJUST1-47 lint fix and code tidy * ADJUST1-47 Add edge case scenario (invalid nomis data) where an ada is consecutive to a non-ada --- server/@types/AdaTypes.ts | 2 + .../additionalDaysAwardedService.test.ts | 321 +++++++++++++++--- .../services/additionalDaysAwardedService.ts | 65 +++- 3 files changed, 347 insertions(+), 41 deletions(-) diff --git a/server/@types/AdaTypes.ts b/server/@types/AdaTypes.ts index 94efd959..12693ec9 100644 --- a/server/@types/AdaTypes.ts +++ b/server/@types/AdaTypes.ts @@ -4,6 +4,8 @@ type ChargeDetails = { heardAt: string status: string days: number + sequence: number + consecutiveToSequence: number } type AdasByDateCharged = { diff --git a/server/services/additionalDaysAwardedService.test.ts b/server/services/additionalDaysAwardedService.test.ts index 66872839..1c1ea375 100644 --- a/server/services/additionalDaysAwardedService.test.ts +++ b/server/services/additionalDaysAwardedService.test.ts @@ -14,34 +14,48 @@ const adaService = new AdditionalDaysAwardedService(hmppsAuthClient) const token = 'token' -const threeAdjudicationSummary = JSON.parse( - '{"results":' + - ' {"content":' + - ' [' + - ' {' + - ' "adjudicationNumber":1525916,"reportTime":"2023-08-02T09:09:00","agencyIncidentId":1503215,"agencyId":"MDI","partySeq":1,' + - ' "adjudicationCharges":' + - ' [' + - ' {"oicChargeId":"1525916/1","offenceCode":"51:16","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","findingCode":"PROVED"}' + - ' ]' + - ' },' + - ' {' + - ' "adjudicationNumber":1525917,"reportTime":"2023-08-02T09:09:00","agencyIncidentId":1503215,"agencyId":"MDI","partySeq":1,' + - ' "adjudicationCharges":' + - ' [' + - ' {"oicChargeId":"1525917/1","offenceCode":"51:16","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","findingCode":"PROVED"}' + - ' ]' + - ' },' + - ' {' + - ' "adjudicationNumber":1525918,"reportTime":"2023-08-02T09:09:00","agencyIncidentId":1503215,"agencyId":"MDI","partySeq":1,' + - ' "adjudicationCharges":' + - ' [' + - ' {"oicChargeId":"1525917/1","offenceCode":"51:16","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","findingCode":"PROVED"}' + - ' ]' + - ' }' + - ' ]' + - ' }' + - ' }', +const adjudication1SearchResponse = + ' {' + + ' "adjudicationNumber":1525916,"reportTime":"2023-08-02T09:09:00","agencyIncidentId":1503215,"agencyId":"MDI","partySeq":1,' + + ' "adjudicationCharges":' + + ' [' + + ' {"oicChargeId":"1525916/1","offenceCode":"51:16","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","findingCode":"PROVED"}' + + ' ]' + + ' }' +const adjudication2SearchResponse = + ' {' + + ` "adjudicationNumber":1525917,"reportTime":"2023-08-02T09:09:00","agencyIncidentId":1503215,"agencyId":"MDI","partySeq":1,` + + ` "adjudicationCharges":` + + ` [` + + ` {"oicChargeId":"1525917/1","offenceCode":"51:16","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","findingCode":"PROVED"}` + + ` ]` + + ` }` +const adjudication3SearchResponse = + ' {' + + ` "adjudicationNumber":1525918,"reportTime":"2023-08-02T09:09:00","agencyIncidentId":1503215,"agencyId":"MDI","partySeq":1,` + + ` "adjudicationCharges":` + + ` [` + + ` {"oicChargeId":"1525917/1","offenceCode":"51:16","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","findingCode":"PROVED"}` + + ` ]` + + ` }` +const threeAdjudicationSearchResponse = JSON.parse( + `{"results":` + + ` {"content":` + + ` [${adjudication1SearchResponse}, ${adjudication2SearchResponse}, ${adjudication3SearchResponse}]` + + ` }` + + ` }`, +) as AdjudicationSearchResponse + +const oneAdjudicationSearchResponse = JSON.parse( + `{"results": {"content": [${adjudication1SearchResponse}] } }`, +) as AdjudicationSearchResponse + +const twoAdjudicationsSearchResponse = JSON.parse( + `{"results": {"content": [${adjudication1SearchResponse}, ${adjudication2SearchResponse}] } }`, +) as AdjudicationSearchResponse + +const threeAdjudicationsSearchResponse = JSON.parse( + `{"results": {"content": [${adjudication1SearchResponse}, ${adjudication2SearchResponse}, ${adjudication3SearchResponse}] } }`, ) as AdjudicationSearchResponse const adjudicationOne = JSON.parse( @@ -49,11 +63,27 @@ const adjudicationOne = JSON.parse( ) as IndividualAdjudication const adjudicationTwo = JSON.parse( - '{"adjudicationNumber":1525917,"incidentTime":"2023-08-01T09:00:00","establishment":"Moorland (HMP & YOI)","interiorLocation":"Circuit","incidentDetails":"some details","reportNumber":1503215,"reportType":"Governor\'s Report","reporterFirstName":"TIM","reporterLastName":"WRIGHT","reportTime":"2023-08-02T09:09:00","hearings":[{"oicHearingId":2012687,"hearingType":"Governor\'s Hearing Adult","hearingTime":"2023-08-03T16:45:00","establishment":"Moorland (HMP & YOI)","location":"Adj","heardByFirstName":"JOHN","heardByLastName":"FERGUSON","results":[{"oicOffenceCode":"51:16","offenceType":"Prison Rule 51","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","plea":"Guilty","finding":"Charge Proved","sanctions":[{"sanctionType":"Additional Days Added","sanctionDays":5,"effectiveDate":"2023-08-09T00:00:00","status":"Immediate","sanctionSeq":15}]}]}]}', + '{"adjudicationNumber":1525917,"incidentTime":"2023-08-01T09:00:00","establishment":"Moorland (HMP & YOI)","interiorLocation":"Circuit","incidentDetails":"some details","reportNumber":1503215,"reportType":"Governor\'s Report","reporterFirstName":"TIM","reporterLastName":"WRIGHT","reportTime":"2023-08-02T09:09:00","hearings":[{"oicHearingId":2012687,"hearingType":"Governor\'s Hearing Adult","hearingTime":"2023-08-03T16:45:00","establishment":"Moorland (HMP & YOI)","location":"Adj","heardByFirstName":"JOHN","heardByLastName":"FERGUSON","results":[{"oicOffenceCode":"51:16","offenceType":"Prison Rule 51","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","plea":"Guilty","finding":"Charge Proved","sanctions":[{"sanctionType":"Additional Days Added","sanctionDays":5,"effectiveDate":"2023-08-09T00:00:00","status":"Immediate","sanctionSeq":16}]}]}]}', ) as IndividualAdjudication const adjudicationThreeWithTwoSanctions = JSON.parse( - '{"adjudicationNumber":1525918,"incidentTime":"2023-08-01T09:00:00","establishment":"Moorland (HMP & YOI)","interiorLocation":"Circuit","incidentDetails":"some details","reportNumber":1503215,"reportType":"Governor\'s Report","reporterFirstName":"TIM","reporterLastName":"WRIGHT","reportTime":"2023-08-02T09:09:00","hearings":[{"oicHearingId":2012687,"hearingType":"Governor\'s Hearing Adult","hearingTime":"2023-08-04T16:45:00","establishment":"Moorland (HMP & YOI)","location":"Adj","heardByFirstName":"JOHN","heardByLastName":"FERGUSON","results":[{"oicOffenceCode":"51:16","offenceType":"Prison Rule 51","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","plea":"Guilty","finding":"Charge Proved","sanctions":[{"sanctionType":"Additional Days Added","sanctionDays":5,"effectiveDate":"2023-08-09T00:00:00","status":"Immediate","sanctionSeq":15}, {"sanctionType":"Additional Days Added","sanctionDays":99,"effectiveDate":"2023-08-09T00:00:00","status":"Immediate","sanctionSeq":16}]}]}]}', + '{"adjudicationNumber":1525918,"incidentTime":"2023-08-01T09:00:00","establishment":"Moorland (HMP & YOI)","interiorLocation":"Circuit","incidentDetails":"some details","reportNumber":1503215,"reportType":"Governor\'s Report","reporterFirstName":"TIM","reporterLastName":"WRIGHT","reportTime":"2023-08-02T09:09:00","hearings":[{"oicHearingId":2012687,"hearingType":"Governor\'s Hearing Adult","hearingTime":"2023-08-04T16:45:00","establishment":"Moorland (HMP & YOI)","location":"Adj","heardByFirstName":"JOHN","heardByLastName":"FERGUSON","results":[{"oicOffenceCode":"51:16","offenceType":"Prison Rule 51","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","plea":"Guilty","finding":"Charge Proved","sanctions":[{"sanctionType":"Additional Days Added","sanctionDays":5,"effectiveDate":"2023-08-09T00:00:00","status":"Immediate","sanctionSeq":17}, {"sanctionType":"Additional Days Added","sanctionDays":99,"effectiveDate":"2023-08-09T00:00:00","status":"Immediate","sanctionSeq":18}]}]}]}', +) as IndividualAdjudication + +const adjudicationTwoConsecutiveToOne = JSON.parse( + '{"adjudicationNumber":1525917,"incidentTime":"2023-08-01T09:00:00","establishment":"Moorland (HMP & YOI)","interiorLocation":"Circuit","incidentDetails":"some details","reportNumber":1503215,"reportType":"Governor\'s Report","reporterFirstName":"TIM","reporterLastName":"WRIGHT","reportTime":"2023-08-02T09:09:00","hearings":[{"oicHearingId":2012687,"hearingType":"Governor\'s Hearing Adult","hearingTime":"2023-08-03T16:45:00","establishment":"Moorland (HMP & YOI)","location":"Adj","heardByFirstName":"JOHN","heardByLastName":"FERGUSON","results":[{"oicOffenceCode":"51:16","offenceType":"Prison Rule 51","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","plea":"Guilty","finding":"Charge Proved","sanctions":[{"sanctionType":"Additional Days Added","sanctionDays":5,"effectiveDate":"2023-08-09T00:00:00","status":"Immediate","sanctionSeq":16, "consecutiveSanctionSeq": 15}]}]}]}', +) as IndividualAdjudication + +const adjudicationThreeConcurrentToOne = JSON.parse( + '{"adjudicationNumber":1525918,"incidentTime":"2023-08-01T09:00:00","establishment":"Moorland (HMP & YOI)","interiorLocation":"Circuit","incidentDetails":"some details","reportNumber":1503215,"reportType":"Governor\'s Report","reporterFirstName":"TIM","reporterLastName":"WRIGHT","reportTime":"2023-08-02T09:09:00","hearings":[{"oicHearingId":2012687,"hearingType":"Governor\'s Hearing Adult","hearingTime":"2023-08-03T16:45:00","establishment":"Moorland (HMP & YOI)","location":"Adj","heardByFirstName":"JOHN","heardByLastName":"FERGUSON","results":[{"oicOffenceCode":"51:16","offenceType":"Prison Rule 51","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","plea":"Guilty","finding":"Charge Proved","sanctions":[{"sanctionType":"Additional Days Added","sanctionDays":5,"effectiveDate":"2023-08-09T00:00:00","status":"Immediate","sanctionSeq":17}]}]}]}', +) as IndividualAdjudication + +const adjudicationTwoConsecToNonAda = JSON.parse( + '{"adjudicationNumber":1525917,"incidentTime":"2023-08-01T09:00:00","establishment":"Moorland (HMP & YOI)","interiorLocation":"Circuit","incidentDetails":"some details","reportNumber":1503215,"reportType":"Governor\'s Report","reporterFirstName":"TIM","reporterLastName":"WRIGHT","reportTime":"2023-08-02T09:09:00","hearings":[{"oicHearingId":2012687,"hearingType":"Governor\'s Hearing Adult","hearingTime":"2023-08-03T16:45:00","establishment":"Moorland (HMP & YOI)","location":"Adj","heardByFirstName":"JOHN","heardByLastName":"FERGUSON","results":[{"oicOffenceCode":"51:16","offenceType":"Prison Rule 51","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","plea":"Guilty","finding":"Charge Proved","sanctions":[{"sanctionType":"Additional Days Added","sanctionDays":5,"effectiveDate":"2023-08-09T00:00:00","status":"Immediate","sanctionSeq":16, "consecutiveSanctionSeq": 17}]}]}]}', +) as IndividualAdjudication + +const adjudicationThreeNonAda = JSON.parse( + '{"adjudicationNumber":1525918,"incidentTime":"2023-08-01T09:00:00","establishment":"Moorland (HMP & YOI)","interiorLocation":"Circuit","incidentDetails":"some details","reportNumber":1503215,"reportType":"Governor\'s Report","reporterFirstName":"TIM","reporterLastName":"WRIGHT","reportTime":"2023-08-02T09:09:00","hearings":[{"oicHearingId":2012687,"hearingType":"Governor\'s Hearing Adult","hearingTime":"2023-08-03T16:45:00","establishment":"Moorland (HMP & YOI)","location":"Adj","heardByFirstName":"JOHN","heardByLastName":"FERGUSON","results":[{"oicOffenceCode":"51:16","offenceType":"Prison Rule 51","offenceDescription":"Intentionally or recklessly sets fire to any part of a prison or any other property, whether or not his own","plea":"Guilty","finding":"Charge Proved","sanctions":[{"sanctionType":"non-ADA","sanctionDays":5,"effectiveDate":"2023-08-09T00:00:00","status":"Immediate","sanctionSeq":17}]}]}]}', ) as IndividualAdjudication const adjudicationOneAdjustment = { @@ -69,7 +99,7 @@ const adjudicationThreeAdjustment = { additionalDaysAwarded: { adjudicationId: 1525918 }, } as Adjustment -const adjustmentRTesponsesWithChargeNumber = [ +const adjustmentResponsesWithChargeNumber = [ adjudicationOneAdjustment, adjudicationTwoAdjustment, adjudicationThreeAdjustment, @@ -90,13 +120,15 @@ describe('Additional Days Added Service', () => { nock.cleanAll() }) describe('ADA Review screen', () => { - it('Get adjudications ', async () => { + it('Get concurrent adjudications ', async () => { const nomsId = 'AA1234A' - adjudicationsApi.get('/adjudications/AA1234A/adjudications?size=1000', '').reply(200, threeAdjudicationSummary) + adjudicationsApi + .get('/adjudications/AA1234A/adjudications?size=1000', '') + .reply(200, threeAdjudicationSearchResponse) adjudicationsApi.get('/adjudications/AA1234A/charge/1525916', '').reply(200, adjudicationOne) adjudicationsApi.get('/adjudications/AA1234A/charge/1525917', '').reply(200, adjudicationTwo) adjudicationsApi.get('/adjudications/AA1234A/charge/1525918', '').reply(200, adjudicationThreeWithTwoSanctions) - adjustmentApi.get(`/adjustments?person=${nomsId}`).reply(200, adjustmentRTesponsesWithChargeNumber) + adjustmentApi.get(`/adjustments?person=${nomsId}`).reply(200, adjustmentResponsesWithChargeNumber) const startOfSentenceEnvelope = new Date('2023-01-01') const adaToReview: AdasToReview = await adaService.getAdasToReview( @@ -105,6 +137,7 @@ describe('Additional Days Added Service', () => { 'username', token, ) + expect(adaToReview).toEqual({ adas: [ { @@ -116,7 +149,8 @@ describe('Additional Days Added Service', () => { days: 5, heardAt: 'Moorland (HMP & YOI)', status: 'AWARDED', - toBeServed: 'TODO', + toBeServed: 'Concurrent', + sequence: 15, }, { chargeNumber: 1525917, @@ -124,7 +158,8 @@ describe('Additional Days Added Service', () => { days: 5, heardAt: 'Moorland (HMP & YOI)', status: 'AWARDED', - toBeServed: 'TODO', + toBeServed: 'Concurrent', + sequence: 16, }, ], }, @@ -137,7 +172,8 @@ describe('Additional Days Added Service', () => { days: 5, heardAt: 'Moorland (HMP & YOI)', status: 'AWARDED', - toBeServed: 'TODO', + toBeServed: 'Concurrent', + sequence: 17, }, { chargeNumber: 1525918, @@ -145,7 +181,8 @@ describe('Additional Days Added Service', () => { days: 99, heardAt: 'Moorland (HMP & YOI)', status: 'AWARDED', - toBeServed: 'TODO', + toBeServed: 'Concurrent', + sequence: 18, }, ], }, @@ -157,5 +194,213 @@ describe('Additional Days Added Service', () => { totalSuspended: 0, } as AdasToReview) }) + + it('Get adjudication where only one charge exists', async () => { + const nomsId = 'AA1234A' + adjudicationsApi + .get('/adjudications/AA1234A/adjudications?size=1000', '') + .reply(200, oneAdjudicationSearchResponse) + adjudicationsApi.get('/adjudications/AA1234A/charge/1525916', '').reply(200, adjudicationOne) + adjustmentApi.get(`/adjustments?person=${nomsId}`).reply(200, adjustmentResponsesWithChargeNumber) + const startOfSentenceEnvelope = new Date('2023-01-01') + + const adaToReview: AdasToReview = await adaService.getAdasToReview( + nomsId, + startOfSentenceEnvelope, + 'username', + token, + ) + + expect(adaToReview).toEqual({ + adas: [ + { + dateChargeProved: new Date('2023-08-03'), + charges: [ + { + chargeNumber: 1525916, + dateChargeProved: new Date('2023-08-03'), + days: 5, + heardAt: 'Moorland (HMP & YOI)', + status: 'AWARDED', + toBeServed: 'Forthwith', + sequence: 15, + }, + ], + }, + ], + suspended: [], + awaitingApproval: [], + totalAdas: 5, + totalAwaitingApproval: 0, + totalSuspended: 0, + } as AdasToReview) + }) + + it('Get adjudication where consecutive charges exist', async () => { + const nomsId = 'AA1234A' + adjudicationsApi + .get('/adjudications/AA1234A/adjudications?size=1000', '') + .reply(200, twoAdjudicationsSearchResponse) + adjudicationsApi.get('/adjudications/AA1234A/charge/1525916', '').reply(200, adjudicationOne) + adjudicationsApi.get('/adjudications/AA1234A/charge/1525917', '').reply(200, adjudicationTwoConsecutiveToOne) + adjustmentApi.get(`/adjustments?person=${nomsId}`).reply(200, adjustmentResponsesWithChargeNumber) + const startOfSentenceEnvelope = new Date('2023-01-01') + + const adaToReview: AdasToReview = await adaService.getAdasToReview( + nomsId, + startOfSentenceEnvelope, + 'username', + token, + ) + + expect(adaToReview).toEqual({ + adas: [ + { + dateChargeProved: new Date('2023-08-03'), + charges: [ + { + chargeNumber: 1525916, + dateChargeProved: new Date('2023-08-03'), + days: 5, + heardAt: 'Moorland (HMP & YOI)', + status: 'AWARDED', + toBeServed: 'Forthwith', + sequence: 15, + }, + { + chargeNumber: 1525917, + dateChargeProved: new Date('2023-08-03'), + days: 5, + heardAt: 'Moorland (HMP & YOI)', + status: 'AWARDED', + toBeServed: 'Consecutive to 1525916', + sequence: 16, + consecutiveToSequence: 15, + }, + ], + }, + ], + suspended: [], + awaitingApproval: [], + totalAdas: 10, + totalAwaitingApproval: 0, + totalSuspended: 0, + } as AdasToReview) + }) + + it('Get adjudication where a mix of consecutive and concurrent charges exist', async () => { + const nomsId = 'AA1234A' + adjudicationsApi + .get('/adjudications/AA1234A/adjudications?size=1000', '') + .reply(200, threeAdjudicationsSearchResponse) + adjudicationsApi.get('/adjudications/AA1234A/charge/1525916', '').reply(200, adjudicationOne) + adjudicationsApi.get('/adjudications/AA1234A/charge/1525917', '').reply(200, adjudicationTwoConsecutiveToOne) + adjudicationsApi.get('/adjudications/AA1234A/charge/1525918', '').reply(200, adjudicationThreeConcurrentToOne) + adjustmentApi.get(`/adjustments?person=${nomsId}`).reply(200, adjustmentResponsesWithChargeNumber) + const startOfSentenceEnvelope = new Date('2023-01-01') + + const adaToReview: AdasToReview = await adaService.getAdasToReview( + nomsId, + startOfSentenceEnvelope, + 'username', + token, + ) + + expect(adaToReview).toEqual({ + adas: [ + { + dateChargeProved: new Date('2023-08-03'), + charges: [ + { + chargeNumber: 1525916, + dateChargeProved: new Date('2023-08-03'), + days: 5, + heardAt: 'Moorland (HMP & YOI)', + status: 'AWARDED', + toBeServed: 'Forthwith', + sequence: 15, + }, + { + chargeNumber: 1525917, + dateChargeProved: new Date('2023-08-03'), + days: 5, + heardAt: 'Moorland (HMP & YOI)', + status: 'AWARDED', + toBeServed: 'Consecutive to 1525916', + sequence: 16, + consecutiveToSequence: 15, + }, + { + chargeNumber: 1525918, + dateChargeProved: new Date('2023-08-03'), + days: 5, + heardAt: 'Moorland (HMP & YOI)', + status: 'AWARDED', + toBeServed: 'Concurrent', + sequence: 17, + }, + ], + }, + ], + suspended: [], + awaitingApproval: [], + totalAdas: 15, + totalAwaitingApproval: 0, + totalSuspended: 0, + } as AdasToReview) + }) + + it('Get adjudication where ada is consecutive to a non-ada - edge case, this really stems from bad data in nomis ', async () => { + const nomsId = 'AA1234A' + adjudicationsApi + .get('/adjudications/AA1234A/adjudications?size=1000', '') + .reply(200, threeAdjudicationsSearchResponse) + adjudicationsApi.get('/adjudications/AA1234A/charge/1525916', '').reply(200, adjudicationOne) + adjudicationsApi.get('/adjudications/AA1234A/charge/1525917', '').reply(200, adjudicationTwoConsecToNonAda) + adjudicationsApi.get('/adjudications/AA1234A/charge/1525918', '').reply(200, adjudicationThreeNonAda) + adjustmentApi.get(`/adjustments?person=${nomsId}`).reply(200, adjustmentResponsesWithChargeNumber) + const startOfSentenceEnvelope = new Date('2023-01-01') + + const adaToReview: AdasToReview = await adaService.getAdasToReview( + nomsId, + startOfSentenceEnvelope, + 'username', + token, + ) + + expect(adaToReview).toEqual({ + adas: [ + { + dateChargeProved: new Date('2023-08-03'), + charges: [ + { + chargeNumber: 1525916, + dateChargeProved: new Date('2023-08-03'), + days: 5, + heardAt: 'Moorland (HMP & YOI)', + status: 'AWARDED', + toBeServed: 'Concurrent', + sequence: 15, + }, + { + chargeNumber: 1525917, + dateChargeProved: new Date('2023-08-03'), + days: 5, + heardAt: 'Moorland (HMP & YOI)', + status: 'AWARDED', + toBeServed: 'Concurrent', + sequence: 16, + consecutiveToSequence: 17, + }, + ], + }, + ], + suspended: [], + awaitingApproval: [], + totalAdas: 10, + totalAwaitingApproval: 0, + totalSuspended: 0, + } as AdasToReview) + }) }) }) diff --git a/server/services/additionalDaysAwardedService.ts b/server/services/additionalDaysAwardedService.ts index 0fb61a47..890d5657 100644 --- a/server/services/additionalDaysAwardedService.ts +++ b/server/services/additionalDaysAwardedService.ts @@ -26,6 +26,8 @@ const isSanctionedAda = (s: Sanction, hearingDate: Date, startOfSentenceEnvelope hearingDate.getTime() >= startOfSentenceEnvelope.getTime() const isProspectiveAda = (s: Sanction) => sanctionIsAda(s) && sanctionIsProspective(s) +const adaHasSequence = (sequence: number, ada: Ada) => sequence === ada.sequence + function isSuspended(sanction: Sanction) { return ( sanction.status === 'Suspended' || @@ -93,8 +95,8 @@ export default class AdditionalDaysAwardedService { return allAdas.filter(it => it.status === status).reduce((acc, cur) => acc + cur.days, 0) } - private getAdasByDateCharged(adas: Ada[], filterStatus: string) { - return adas + private getAdasByDateCharged(adas: Ada[], filterStatus: string): AdasByDateCharged[] { + const adasByDateCharged = adas .filter(it => it.status === filterStatus) .reduce((acc: AdasByDateCharged[], cur) => { if (acc.some(it => it.dateChargeProved.getTime() === cur.dateChargeProved.getTime())) { @@ -106,6 +108,52 @@ export default class AdditionalDaysAwardedService { return acc }, []) .sort((a, b) => a.dateChargeProved.getTime() - b.dateChargeProved.getTime()) + + return this.associateConsecutiveAdas(adasByDateCharged, adas) + } + + /* + * Sets the toBeServed of the groupedAdas for the review screen, can be either Consecutive, Concurrent or Forthwith + */ + private associateConsecutiveAdas(adasByDateCharged: AdasByDateCharged[], adas: Ada[]) { + const consecutiveSourceAdas = this.getSourceAdaForConsecutive(adas) + return adasByDateCharged.map(it => { + const { charges } = it + // Only one charge in group + if (charges.length === 1) { + return { ...it, charges: [{ ...charges[0], toBeServed: 'Forthwith' } as Ada] } + } + + // Label consecutive or concurrent adas + const consecutiveAndConcurrentCharges = charges.map(charge => { + if (this.validConsecutiveSequence(charge, consecutiveSourceAdas)) { + const consecutiveAda = consecutiveSourceAdas.find(c => adaHasSequence(charge.consecutiveToSequence, c)) + return { ...charge, toBeServed: `Consecutive to ${consecutiveAda.chargeNumber}` } as Ada + } + + if ( + !this.validConsecutiveSequence(charge, consecutiveSourceAdas) && + !this.isSourceForConsecutiveChain(consecutiveSourceAdas, charge) + ) { + return { ...charge, toBeServed: 'Concurrent' } as Ada + } + + return { ...charge, toBeServed: 'Forthwith' } as Ada + }) + + return { ...it, charges: consecutiveAndConcurrentCharges } + }) + } + + private isSourceForConsecutiveChain(consecutiveSourceAdas: Ada[], charge: Ada) { + return consecutiveSourceAdas.some(consecutiveAda => adaHasSequence(charge.sequence, consecutiveAda)) + } + + private validConsecutiveSequence(charge: Ada, consecutiveSourceAdas: Ada[]) { + return ( + charge.consecutiveToSequence && + consecutiveSourceAdas.some(consecutiveAda => adaHasSequence(charge.consecutiveToSequence, consecutiveAda)) + ) } private getAdas( @@ -121,6 +169,8 @@ export default class AdditionalDaysAwardedService { ) }), ) + + adasToTransform.map(a => a.hearings) // chech reliability of consecutive to sequence.. for a given set of adjudocations, where does the consec seq sit? return adasToTransform.reduce((acc: Ada[], cur) => { cur.hearings .filter(h => { @@ -140,10 +190,11 @@ export default class AdditionalDaysAwardedService { const ada = { dateChargeProved: new Date(hearing.hearingTime.substring(0, 10)), chargeNumber: cur.adjudicationNumber, - toBeServed: 'TODO', // TODO this field to be populated in a subsequent story heardAt: hearing.establishment, status: deriveStatus(cur.adjudicationNumber, sanction, existingAdaChargeIds), days: sanction.sanctionDays, + sequence: sanction.sanctionSeq, + consecutiveToSequence: sanction.consecutiveSanctionSeq, } as Ada acc.push(ada) }) @@ -151,4 +202,12 @@ export default class AdditionalDaysAwardedService { return acc }, []) } + + private getSourceAdaForConsecutive(allAdas: Ada[]): Ada[] { + return allAdas + .filter( + ada => ada.consecutiveToSequence && allAdas.some(sourceAda => sourceAda.sequence === ada.consecutiveToSequence), + ) + .map(consecutiveAda => allAdas.find(sourceAda => sourceAda.sequence === consecutiveAda.consecutiveToSequence)) + } }