diff --git a/server/model/adjustmentsHubViewModel.ts b/server/model/adjustmentsHubViewModel.ts index c28ceaa2..86c5821b 100644 --- a/server/model/adjustmentsHubViewModel.ts +++ b/server/model/adjustmentsHubViewModel.ts @@ -1,5 +1,7 @@ import { Adjustment } from '../@types/adjustments/adjustmentsTypes' import { IdentifyRemandDecision, RemandResult } from '../@types/identifyRemandPeriods/identifyRemandPeriodsTypes' +import config from '../config' +import { UnusedDeductionMessageType } from '../services/unusedDeductionsService' import { calculateReleaseDatesCheckInformationUrl } from '../utils/utils' import adjustmentTypes, { AdjustmentType } from './adjustmentTypes' @@ -13,6 +15,8 @@ export type Message = { export type MessageAction = 'CREATE' | 'REMOVE' | 'UPDATE' | 'REJECTED' | 'VALIDATION' export default class AdjustmentsHubViewModel { + public checkInformationLink: string + constructor( public prisonerNumber: string, public adjustments: Adjustment[], @@ -20,8 +24,10 @@ export default class AdjustmentsHubViewModel { public remandDecision: IdentifyRemandDecision, public roles: string[], public message: Message, - public serviceHasCalculatedUnusedDeductions: boolean, - ) {} + public unusedDeductionMessage: UnusedDeductionMessageType, + ) { + this.checkInformationLink = `${config.services.calculateReleaseDatesUI.url}/calculation/${this.prisonerNumber}/check-information?hasErrors=true` + } public deductions(): AdjustmentType[] { return adjustmentTypes.filter(it => diff --git a/server/routes/adjustmentRoutes.test.ts b/server/routes/adjustmentRoutes.test.ts index c9cb97a3..80fec2e2 100644 --- a/server/routes/adjustmentRoutes.test.ts +++ b/server/routes/adjustmentRoutes.test.ts @@ -129,7 +129,7 @@ describe('Adjustment routes tests', () => { unusedDeductions, ]) identifyRemandPeriodsService.calculateRelevantRemand.mockResolvedValue(remandResult) - unusedDeductionsService.serviceHasCalculatedUnusedDeductions.mockResolvedValue(true) + unusedDeductionsService.getCalculatedUnusedDeductionsMessage.mockResolvedValue('NONE') additionalDaysAwardedService.shouldIntercept.mockResolvedValue({ type: 'NONE', number: 0, @@ -147,14 +147,14 @@ describe('Adjustment routes tests', () => { expect(res.text).toContain('including 10 days unused') }) }) - it('GET /{nomsId} hub unused deductions cannot be calculated', () => { + it('GET /{nomsId} hub unused deductions cannot be calculated because of unsupported sentence type', () => { prisonerService.getStartOfSentenceEnvelope.mockResolvedValue({ earliestExcludingRecalls: new Date(), earliestSentence: new Date(), }) adjustmentsService.findByPerson.mockResolvedValue([remandAdjustment]) identifyRemandPeriodsService.calculateRelevantRemand.mockResolvedValue(remandResult) - unusedDeductionsService.serviceHasCalculatedUnusedDeductions.mockResolvedValue(false) + unusedDeductionsService.getCalculatedUnusedDeductionsMessage.mockResolvedValue('UNSUPPORTED') additionalDaysAwardedService.shouldIntercept.mockResolvedValue({ type: 'NONE', number: 0, @@ -164,7 +164,75 @@ describe('Adjustment routes tests', () => { .get(`/${NOMS_ID}`) .expect('Content-Type', /html/) .expect(res => { - expect(res.text).toContain('Unused remand/tagged bail time cannot be calculated') + expect(res.text).toContain( + 'Some of the details recorded in NOMIS cannot be used for a sentence calculation. This means unused deductions cannot be automatically calculated by this service. To add any unused remand, go to the sentence adjustments screen in NOMIS.', + ) + }) + }) + it('GET /{nomsId} hub unused deductions cannot be calculated because of validation error', () => { + prisonerService.getStartOfSentenceEnvelope.mockResolvedValue({ + earliestExcludingRecalls: new Date(), + earliestSentence: new Date(), + }) + adjustmentsService.findByPerson.mockResolvedValue([remandAdjustment]) + identifyRemandPeriodsService.calculateRelevantRemand.mockResolvedValue(remandResult) + unusedDeductionsService.getCalculatedUnusedDeductionsMessage.mockResolvedValue('VALIDATION') + additionalDaysAwardedService.shouldIntercept.mockResolvedValue({ + type: 'NONE', + number: 0, + anyProspective: false, + }) + return request(app) + .get(`/${NOMS_ID}`) + .expect('Content-Type', /html/) + .expect(res => { + expect(res.text).toContain( + 'Some of the data in NOMIS related to this person is incorrect. This means unused deductions cannot be automatically calculated.', + ) + }) + }) + it('GET /{nomsId} hub unused deductions cannot be calculated because its a nomis adjustment', () => { + prisonerService.getStartOfSentenceEnvelope.mockResolvedValue({ + earliestExcludingRecalls: new Date(), + earliestSentence: new Date(), + }) + adjustmentsService.findByPerson.mockResolvedValue([remandAdjustment]) + identifyRemandPeriodsService.calculateRelevantRemand.mockResolvedValue(remandResult) + unusedDeductionsService.getCalculatedUnusedDeductionsMessage.mockResolvedValue('NOMIS_ADJUSTMENT') + additionalDaysAwardedService.shouldIntercept.mockResolvedValue({ + type: 'NONE', + number: 0, + anyProspective: false, + }) + return request(app) + .get(`/${NOMS_ID}`) + .expect('Content-Type', /html/) + .expect(res => { + expect(res.text).toContain( + 'Existing deductions have been added on NOMIS. This means unused deductions cannot be automatically calculated. To add any unused remand, go to the sentence adjustments screen in NOMIS.', + ) + }) + }) + it('GET /{nomsId} hub unused deductions cannot be calculated because of an exception', () => { + prisonerService.getStartOfSentenceEnvelope.mockResolvedValue({ + earliestExcludingRecalls: new Date(), + earliestSentence: new Date(), + }) + adjustmentsService.findByPerson.mockResolvedValue([remandAdjustment]) + identifyRemandPeriodsService.calculateRelevantRemand.mockResolvedValue(remandResult) + unusedDeductionsService.getCalculatedUnusedDeductionsMessage.mockResolvedValue('UNKNOWN') + additionalDaysAwardedService.shouldIntercept.mockResolvedValue({ + type: 'NONE', + number: 0, + anyProspective: false, + }) + return request(app) + .get(`/${NOMS_ID}`) + .expect('Content-Type', /html/) + .expect(res => { + expect(res.text).toContain( + 'Unused remand/tagged bail time cannot be calculated. Any unused deductions must be entered in NOMIS.', + ) }) }) it('GET /{nomsId} with remand role', () => { @@ -178,7 +246,7 @@ describe('Adjustment routes tests', () => { }) identifyRemandPeriodsService.calculateRelevantRemand.mockResolvedValue(remandResult) additionalDaysAwardedService.shouldIntercept.mockResolvedValue({ type: 'NONE', number: 0, anyProspective: false }) - unusedDeductionsService.serviceHasCalculatedUnusedDeductions.mockResolvedValue(true) + unusedDeductionsService.getCalculatedUnusedDeductionsMessage.mockResolvedValue('NONE') return request(app) .get(`/${NOMS_ID}`) .expect('Content-Type', /html/) diff --git a/server/routes/adjustmentRoutes.ts b/server/routes/adjustmentRoutes.ts index f5109ea0..7aba1259 100644 --- a/server/routes/adjustmentRoutes.ts +++ b/server/routes/adjustmentRoutes.ts @@ -18,7 +18,7 @@ import FullPageError from '../model/FullPageError' import { daysBetween } from '../utils/utils' import RecallModel from '../model/recallModel' import RecallForm from '../model/recallForm' -import UnusedDeductionsService from '../services/unusedDeductionsService' +import UnusedDeductionsService, { UnusedDeductionMessageType } from '../services/unusedDeductionsService' import { Adjustment } from '../@types/adjustments/adjustmentsTypes' export default class AdjustmentRoutes { @@ -54,14 +54,11 @@ export default class AdjustmentRoutes { const message = req.flash('message') const messageExists = message && message[0] - let serviceHasCalculatedUnusedDeductions = true + let unusedDeductionMessage: UnusedDeductionMessageType = 'NONE' if (messageExists) { this.adjustmentsStoreService.clear(req, nomsId) - serviceHasCalculatedUnusedDeductions = await this.unusedDeductionsService.waitUntilUnusedRemandCreated( - nomsId, - token, - ) + unusedDeductionMessage = await this.unusedDeductionsService.waitUntilUnusedRemandCreated(nomsId, token) } const adjustments = await this.adjustmentsService.findByPerson( @@ -71,7 +68,7 @@ export default class AdjustmentRoutes { ) if (!messageExists) { - serviceHasCalculatedUnusedDeductions = await this.unusedDeductionsService.serviceHasCalculatedUnusedDeductions( + unusedDeductionMessage = await this.unusedDeductionsService.getCalculatedUnusedDeductionsMessage( nomsId, adjustments, token, @@ -107,7 +104,7 @@ export default class AdjustmentRoutes { remandDecision, roles, message && message[0] && (JSON.parse(message[0]) as Message), - serviceHasCalculatedUnusedDeductions, + unusedDeductionMessage, ), }) } diff --git a/server/services/unusedDeductionsService.ts b/server/services/unusedDeductionsService.ts index 22a7365b..acf4da8a 100644 --- a/server/services/unusedDeductionsService.ts +++ b/server/services/unusedDeductionsService.ts @@ -3,6 +3,8 @@ import { delay } from '../utils/utils' import AdjustmentsService from './adjustmentsService' import CalculateReleaseDatesService from './calculateReleaseDatesService' +export type UnusedDeductionMessageType = 'NOMIS_ADJUSTMENT' | 'VALIDATION' | 'UNSUPPORTED' | 'UNKNOWN' | 'NONE' + export default class UnusedDeductionsService { private maxTries = 6 // 3 seconds max wait @@ -18,18 +20,13 @@ export default class UnusedDeductionsService { } /* Wait until calclulated unused deductions matches with adjustments database. */ - async waitUntilUnusedRemandCreated(nomsId: string, token: string): Promise { + async waitUntilUnusedRemandCreated(nomsId: string, token: string): Promise { try { let adjustments = await this.adjustmentsService.findByPersonOutsideSentenceEnvelope(nomsId, token) - const deductions = adjustments.filter(it => it.adjustmentType === 'REMAND' || it.adjustmentType === 'TAGGED_BAIL') if (!deductions.length) { // If there are no deductions then unused deductions doesn't need to be calculated - return true - } - if (this.anyDeductionFromNomis(deductions)) { - // won't calculate unused deductions if adjusments are not from DPS. - return false + return 'NONE' } const unusedDeductionsResponse = await this.calculateReleaseDatesService.calculateUnusedDeductions( @@ -39,46 +36,58 @@ export default class UnusedDeductionsService { ) if (unusedDeductionsResponse.validationMessages?.length) { - return false + if ( + unusedDeductionsResponse.validationMessages.find( + it => it.type === 'UNSUPPORTED_CALCULATION' || it.type === 'UNSUPPORTED_SENTENCE', + ) + ) { + return 'UNSUPPORTED' + } + + return 'VALIDATION' + } + + if (this.anyDeductionFromNomis(deductions)) { + // won't calculate unused deductions if adjusments are not from DPS. + return 'NOMIS_ADJUSTMENT' } - const calculatedUnusedDeducions = unusedDeductionsResponse.unusedDeductions + const calculatedUnusedDeducions = unusedDeductionsResponse.unusedDeductions /* eslint-disable no-await-in-loop */ for (let i = 0; i < this.maxTries; i += 1) { if (calculatedUnusedDeducions || calculatedUnusedDeducions === 0) { const dbDeductions = this.getTotalUnusedRemand(adjustments) if (calculatedUnusedDeducions === dbDeductions) { - return true + return 'NONE' } await delay(this.waitBetweenTries) adjustments = await this.adjustmentsService.findByPersonOutsideSentenceEnvelope(nomsId, token) // Try again } else { // Unable to calculate unused deductions. - return false + return 'UNKNOWN' } } } catch { // Error couldn't calculate unused deductions. } - return false + + return 'UNKNOWN' /* eslint-enable no-await-in-loop */ } - async serviceHasCalculatedUnusedDeductions( + async getCalculatedUnusedDeductionsMessage( nomsId: string, adjustments: Adjustment[], token: string, - ): Promise { + ): Promise { try { const deductions = adjustments.filter(it => it.adjustmentType === 'REMAND' || it.adjustmentType === 'TAGGED_BAIL') if (!deductions.length) { // If there are no deductions then unused deductions doesn't need to be calculated - return true - } - if (this.anyDeductionFromNomis(deductions)) { - return false + return 'NONE' } + const unusedDeductionsResponse = await this.calculateReleaseDatesService.calculateUnusedDeductions( nomsId, adjustments, @@ -86,19 +95,32 @@ export default class UnusedDeductionsService { ) if (unusedDeductionsResponse.validationMessages?.length) { - return false + if ( + unusedDeductionsResponse.validationMessages.find( + it => it.type === 'UNSUPPORTED_CALCULATION' || it.type === 'UNSUPPORTED_SENTENCE', + ) + ) { + return 'UNSUPPORTED' + } + + return 'VALIDATION' + } + + if (this.anyDeductionFromNomis(deductions)) { + return 'NOMIS_ADJUSTMENT' } - const calculatedUnusedDeducions = unusedDeductionsResponse.unusedDeductions + const calculatedUnusedDeducions = unusedDeductionsResponse.unusedDeductions if (calculatedUnusedDeducions || calculatedUnusedDeducions === 0) { const dbDeductions = this.getTotalUnusedRemand(adjustments) if (calculatedUnusedDeducions === dbDeductions) { - return true + return 'NONE' } } - return false + + return 'UNKNOWN' } catch { - return false + return 'UNKNOWN' } } diff --git a/server/views/pages/adjustments/hub.njk b/server/views/pages/adjustments/hub.njk index 361b1be8..f237110c 100644 --- a/server/views/pages/adjustments/hub.njk +++ b/server/views/pages/adjustments/hub.njk @@ -66,23 +66,41 @@ NOMIS, {{ prisoner.firstName | title }} {{ prisoner.lastName | title }} may have {{ model.getTotalDaysRelevantRemand() }} days remand. Review the remand to make sure it is relevant.

- Review - - {% endif %} - - {% if not model.serviceHasCalculatedUnusedDeductions %} -

- Unused deductions -

-
- Unused remand/tagged bail time cannot be calculated. Any unused deductions must be entered in NOMIS. + Review
{% endif %}

Deductions

+ + {% if model.unusedDeductionMessage != 'NONE' %} +
+ {% if model.unusedDeductionMessage == 'UNSUPPORTED' %} + Some of the details recorded in NOMIS cannot be used for a sentence calculation. This means unused deductions cannot be automatically calculated by this service. To add any unused remand, go to the sentence adjustments screen in NOMIS. + {% elif model.unusedDeductionMessage == 'VALIDATION' %} + Some of the data in NOMIS related to this person is incorrect. This means unused deductions cannot be automatically calculated. +
+ To continue, you must: +
    +
  1. + Review the incorrect details +
  2. +
  3. + Update these details +
  4. +
  5. + Reload this page +
  6. +
+ {% elif model.unusedDeductionMessage == 'NOMIS_ADJUSTMENT' %} + Existing deductions have been added on NOMIS. This means unused deductions cannot be automatically calculated. To add any unused remand, go to the sentence adjustments screen in NOMIS. + {% elif model.unusedDeductionMessage == 'UNKNOWN' %} + Unused remand/tagged bail time cannot be calculated. Any unused deductions must be entered in NOMIS. + {% endif %} +
+ {% endif %} +
{% for adjustmentType in model.deductions() %} {{ adjustmentCard(adjustmentType, model, prisoner.prisonerNumber) }}