diff --git a/integration_tests/e2e/manualDatesPath.cy.ts b/integration_tests/e2e/manualDatesPath.cy.ts index 5c2781ac..97e5d8e0 100644 --- a/integration_tests/e2e/manualDatesPath.cy.ts +++ b/integration_tests/e2e/manualDatesPath.cy.ts @@ -29,6 +29,7 @@ context('End to end user journeys entering and modifying approved dates', () => cy.task('stubSaveManualEntry') cy.task('stubGetCalculationResults') cy.task('stubHasNoRecallSentences') + cy.task('stubManualEntryDateValidation') }) it('Can add some manual dates', () => { @@ -66,14 +67,14 @@ context('End to end user journeys entering and modifying approved dates', () => 'NPD', 'DPRRD', ]) - selectDatesPage.checkDate('SED') + selectDatesPage.checkDate('LED') selectDatesPage.checkDate('CRD') selectDatesPage.checkDate('MTD') selectDatesPage.continue().click() const enterSedPage = Page.verifyOnPage(ManualDatesEnterDatePage) - enterSedPage.checkIsFor('SED') - enterSedPage.enterDate('SED', '01', '06', '2026') + enterSedPage.checkIsFor('LED') + enterSedPage.enterDate('LED', '01', '06', '2026') enterSedPage.continue().click() const enterCRDPage = Page.verifyOnPage(ManualDatesEnterDatePage) @@ -87,7 +88,7 @@ context('End to end user journeys entering and modifying approved dates', () => enterMTDPage.continue().click() const manualDatesConfirmationPage = Page.verifyOnPage(ManualDatesConfirmationPage) - manualDatesConfirmationPage.dateShouldHaveValue('SED', '01 June 2026') + manualDatesConfirmationPage.dateShouldHaveValue('LED', '01 June 2026') manualDatesConfirmationPage.dateShouldHaveValue('CRD', '03 September 2027') manualDatesConfirmationPage.dateShouldHaveValue('MTD', '09 March 2028') // check unselected dates are not shown diff --git a/integration_tests/mockApis/calculateReleaseDatesApi.ts b/integration_tests/mockApis/calculateReleaseDatesApi.ts index 8cdf3a2c..c3fc25f8 100644 --- a/integration_tests/mockApis/calculateReleaseDatesApi.ts +++ b/integration_tests/mockApis/calculateReleaseDatesApi.ts @@ -501,6 +501,19 @@ export default { }, }) }, + stubManualEntryDateValidation: (): SuperAgentRequest => { + return stubFor({ + request: { + method: 'GET', + urlPattern: '/calculate-release-dates/validation/manual-entry-dates-validation\\?releaseDates=([A-Z|,]*)', + }, + response: { + status: 200, + headers: { 'Content-Type': 'application/json;charset=UTF-8' }, + jsonBody: [], + }, + }) + }, stubSupportedValidationNoMessages: (): SuperAgentRequest => { return stubFor({ request: { diff --git a/server/@types/calculateReleaseDates/calculateReleaseDatesClientTypes.ts b/server/@types/calculateReleaseDates/calculateReleaseDatesClientTypes.ts index ed85f071..c18a2c80 100644 --- a/server/@types/calculateReleaseDates/calculateReleaseDatesClientTypes.ts +++ b/server/@types/calculateReleaseDates/calculateReleaseDatesClientTypes.ts @@ -3,14 +3,11 @@ import { components } from './index' export type BookingCalculation = components['schemas']['CalculatedReleaseDates'] export type WorkingDay = components['schemas']['WorkingDay'] export type CalculationBreakdown = components['schemas']['CalculationBreakdown'] -export type DateBreakdown = components['schemas']['DateBreakdown'] export type ValidationMessage = components['schemas']['ValidationMessage'] export type ReleaseDateCalculationBreakdown = components['schemas']['ReleaseDateCalculationBreakdown'] -export type CalculationFragments = components['schemas']['CalculationFragments'] export type CalculationUserInputs = components['schemas']['CalculationUserInputs'] export type CalculationRequestModel = components['schemas']['CalculationRequestModel'] export type CalculationSentenceUserInput = components['schemas']['CalculationSentenceUserInput'] -export type CalculationResults = components['schemas']['CalculationResults'] export type SubmittedDate = components['schemas']['SubmittedDate'] export type ManualEntrySelectedDate = components['schemas']['ManualEntrySelectedDate'] export type ManualEntryRequest = components['schemas']['ManualEntryRequest'] @@ -18,8 +15,6 @@ export type SubmitCalculationRequest = components['schemas']['SubmitCalculationR export type GenuineOverrideRequest = components['schemas']['GenuineOverrideRequest'] export type GenuineOverrideDateRequest = components['schemas']['GenuineOverrideDateRequest'] export type GenuineOverrideDateResponse = components['schemas']['GenuineOverrideDateResponse'] -export type NonFridayReleaseDay = components['schemas']['NonFridayReleaseDay'] -export type ComparisonInput = components['schemas']['ComparisonInput'] export type Comparison = components['schemas']['Comparison'] export type ComparisonSummary = components['schemas']['ComparisonSummary'] export type ComparisonOverview = components['schemas']['ComparisonOverview'] @@ -34,7 +29,6 @@ export type ComparisonPersonDiscrepancyCause = components['schemas']['Discrepanc export type HistoricCalculation = components['schemas']['HistoricCalculation'] export type DetailedCalculationResults = components['schemas']['DetailedCalculationResults'] export type DetailedDate = components['schemas']['DetailedDate'] -export type CalculationPrisonerDetails = components['schemas']['PrisonerDetails'] export type LatestCalculation = components['schemas']['LatestCalculation'] export type DateTypeDefinition = components['schemas']['DateTypeDefinition'] export type NomisCalculationSummary = components['schemas']['NomisCalculationSummary'] diff --git a/server/@types/calculateReleaseDates/index.d.ts b/server/@types/calculateReleaseDates/index.d.ts index 1d04fdb1..d4435ecd 100644 --- a/server/@types/calculateReleaseDates/index.d.ts +++ b/server/@types/calculateReleaseDates/index.d.ts @@ -81,6 +81,26 @@ export interface paths { patch?: never trace?: never } + '/overall-sentence-length': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * Find overall sentence length comparison + * @description Compares the sentence durations to an overall sentence length + */ + post: operations['compareOverallSentenceLength'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/manual-calculation/{prisonerId}': { parameters: { query?: never @@ -377,6 +397,46 @@ export interface paths { patch?: never trace?: never } + '/validation/manual-entry-dates-validation': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Validates that requested calculation dates are valid + * @description Some dates cannot be calculated together, while some dates may require another date to be valid + */ + get: operations['validateForDatesManualEntry'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/things-to-do/prisoner/{prisonerId}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Retrieve things-to-do for a prisoner + * @description Provides a list of things-to-do for a specified prisoner based on their ID. + */ + get: operations['getThingsToDo'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/specialist-support/genuine-override/calculation/{calculationReference}': { parameters: { query?: never @@ -697,22 +757,6 @@ export interface paths { patch?: never trace?: never } - '/calculation/view-json/': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get: operations['getCalculationJson'] - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } '/calculation/sentence-and-offences/{calculationRequestId}': { parameters: { query?: never @@ -1004,6 +1048,7 @@ export interface components { | 'ADJUSTMENT_FUTURE_DATED_UAL' | 'A_FINE_SENTENCE_CONSECUTIVE' | 'A_FINE_SENTENCE_CONSECUTIVE_TO' + | 'DTO_RECALL' | 'A_FINE_SENTENCE_MISSING_FINE_AMOUNT' | 'A_FINE_SENTENCE_WITH_PAYMENTS' | 'CUSTODIAL_PERIOD_EXTINGUISHED_REMAND' @@ -1057,6 +1102,14 @@ export interface components { | 'UNSUPPORTED_OFFENCE_ENCOURAGING_OR_ASSISTING' | 'UNSUPPORTED_BREACH_97' | 'UNSUPPORTED_SUSPENDED_OFFENCE' + | 'FTR_NO_RETURN_TO_CUSTODY_DATE' + | 'NO_SENTENCES' + | 'UNABLE_TO_DETERMINE_SHPO_RELEASE_PROVISIONS' + | 'SE2020_INVALID_OFFENCE_DETAIL' + | 'SE2020_INVALID_OFFENCE_COURT_DETAIL' + | 'REMAND_ON_OR_AFTER_SENTENCE_DATE' + | 'DATES_MISSING_REQUIRED_TYPE' + | 'DATES_PAIRINGS_INVALID' arguments: string[] message: string /** @enum {string} */ @@ -1096,7 +1149,7 @@ export interface components { effectiveDays: number } UnusedDeductionCalculationResponse: { - /** Format: int32 */ + /** Format: int64 */ unusedDeductions?: number validationMessages: components['schemas']['ValidationMessage'][] } @@ -1164,6 +1217,33 @@ export interface components { calculationReference: string originalCalculationReference: string } + OverallSentenceLength: { + /** Format: int64 */ + years: number + /** Format: int64 */ + months: number + /** Format: int64 */ + weeks: number + /** Format: int64 */ + days: number + } + OverallSentenceLengthRequest: { + overallSentenceLength: components['schemas']['OverallSentenceLengthSentence'] + consecutiveSentences: components['schemas']['OverallSentenceLengthSentence'][] + concurrentSentences: components['schemas']['OverallSentenceLengthSentence'][] + /** Format: date */ + warrantDate: string + } + OverallSentenceLengthSentence: { + custodialDuration: components['schemas']['OverallSentenceLength'] + extensionDuration?: components['schemas']['OverallSentenceLength'] + } + OverallSentenceLengthComparison: { + custodialLength: components['schemas']['OverallSentenceLength'] + licenseLength?: components['schemas']['OverallSentenceLength'] + custodialLengthMatches: boolean + licenseLengthMatches?: boolean + } ManualCalculationResponse: { enteredDates?: { [key: string]: string @@ -1274,83 +1354,6 @@ export interface components { calculationReasonId: number otherReasonDescription?: string } - AFineSentence: { - type: 'AFineSentence' - } & (Omit< - WithRequired< - components['schemas']['AbstractSentence'], - 'consecutiveSentenceUUIDs' | 'identifier' | 'isSDSPlus' | 'offence' | 'sentencedAt' - >, - 'type' - > & { - duration: components['schemas']['Duration'] - fineAmount?: number - }) - AbstractSentence: { - offence: components['schemas']['Offence'] - /** Format: date */ - sentencedAt: string - /** Format: uuid */ - identifier: string - consecutiveSentenceUUIDs: string[] - /** Format: int32 */ - caseSequence?: number - /** Format: int32 */ - lineSequence?: number - caseReference?: string - /** @enum {string} */ - recallType?: 'STANDARD_RECALL' | 'FIXED_TERM_RECALL_14' | 'FIXED_TERM_RECALL_28' - isSDSPlus: boolean - type: string - } - Adjustment: { - /** Format: date */ - appliesToSentencesFrom: string - /** Format: int32 */ - numberOfDays: number - /** Format: date */ - fromDate?: string - /** Format: date */ - toDate?: string - } - Adjustments: { - adjustments?: { - [key: string]: components['schemas']['Adjustment'][] - } - } - Booking: { - offender: components['schemas']['Offender'] - sentences: ( - | components['schemas']['AFineSentence'] - | components['schemas']['BotusSentence'] - | components['schemas']['DetentionAndTrainingOrderSentence'] - | components['schemas']['ExtendedDeterminateSentence'] - | components['schemas']['SopcSentence'] - | components['schemas']['StandardDeterminateSentence'] - )[] - adjustments: components['schemas']['Adjustments'] - /** Format: date */ - returnToCustodyDate?: string - fixedTermRecallDetails?: components['schemas']['FixedTermRecallDetails'] - /** Format: int64 */ - bookingId: number - historicalTusedData?: components['schemas']['HistoricalTusedData'] - } - BotusSentence: { - type: 'BotusSentence' - } & (Omit< - WithRequired< - components['schemas']['AbstractSentence'], - 'consecutiveSentenceUUIDs' | 'identifier' | 'isSDSPlus' | 'offence' | 'sentencedAt' - >, - 'type' - > & { - duration: components['schemas']['Duration'] - /** Format: date */ - latestTusedDate?: string - /** @enum {string} */ - latestTusedSource?: 'CRDS' | 'CRDS_OVERRIDDEN' | 'NOMIS' | 'NOMIS_OVERRIDDEN' - }) CalculatedReleaseDates: { dates: { [key: string]: string @@ -1415,7 +1418,6 @@ export interface components { sdsEarlyReleaseAllocatedTranche?: 'TRANCHE_0' | 'TRANCHE_1' | 'TRANCHE_2' /** @enum {string} */ sdsEarlyReleaseTranche?: 'TRANCHE_0' | 'TRANCHE_1' | 'TRANCHE_2' - calculatedBooking?: components['schemas']['Booking'] } CalculationFragments: { breakdownHtml: string @@ -1426,86 +1428,6 @@ export interface components { isOther: boolean displayName: string } - DetentionAndTrainingOrderSentence: { - type: 'DetentionAndTrainingOrderSentence' - } & (Omit< - WithRequired< - components['schemas']['AbstractSentence'], - 'consecutiveSentenceUUIDs' | 'identifier' | 'isSDSPlus' | 'offence' | 'sentencedAt' - >, - 'type' - > & { - duration: components['schemas']['Duration'] - }) - Duration: { - durationElements: { - [key: string]: number - } - } - ExtendedDeterminateSentence: { - type: 'ExtendedDeterminateSentence' - } & (Omit< - WithRequired< - components['schemas']['AbstractSentence'], - 'consecutiveSentenceUUIDs' | 'identifier' | 'isSDSPlus' | 'offence' | 'sentencedAt' - >, - 'type' - > & { - custodialDuration: components['schemas']['Duration'] - extensionDuration: components['schemas']['Duration'] - automaticRelease: boolean - }) - FixedTermRecallDetails: { - /** Format: int64 */ - bookingId: number - /** Format: date */ - returnToCustodyDate: string - /** Format: int32 */ - recallLength: number - } - HistoricalTusedData: { - /** Format: date */ - tused?: string - /** @enum {string} */ - historicalTusedSource: 'CRDS' | 'CRDS_OVERRIDDEN' | 'NOMIS' | 'NOMIS_OVERRIDDEN' - } - Offence: { - /** Format: date */ - committedAt: string - offenceCode?: string - } - Offender: { - reference: string - /** Format: date */ - dateOfBirth: string - isActiveSexOffender: boolean - } - SopcSentence: { - type: 'SopcSentence' - } & (Omit< - WithRequired< - components['schemas']['AbstractSentence'], - 'consecutiveSentenceUUIDs' | 'identifier' | 'isSDSPlus' | 'offence' | 'sentencedAt' - >, - 'type' - > & { - custodialDuration: components['schemas']['Duration'] - extensionDuration: components['schemas']['Duration'] - sdopcu18: boolean - }) - StandardDeterminateSentence: { - type: 'StandardDeterminateSentence' - } & (Omit< - WithRequired< - components['schemas']['AbstractSentence'], - 'consecutiveSentenceUUIDs' | 'identifier' | 'isSDSPlus' | 'offence' | 'sentencedAt' - >, - 'type' - > & { - duration: components['schemas']['Duration'] - /** @enum {string} */ - hasAnSDSEarlyReleaseExclusion: 'SEXUAL' | 'VIOLENT' | 'DOMESTIC_ABUSE' | 'NATIONAL_SECURITY' | 'TERRORISM' | 'NO' - }) CalculationResults: { calculatedReleaseDates?: components['schemas']['CalculatedReleaseDates'] validationMessages: components['schemas']['ValidationMessage'][] @@ -1541,8 +1463,6 @@ export interface components { releaseDate?: string /** Format: date */ postRecallReleaseDate?: string - /** Format: int32 */ - unusedDeductions: number validationMessages: components['schemas']['ValidationMessage'][] } SubmitCalculationRequest: { @@ -1556,6 +1476,10 @@ export interface components { adjustedForWeekend: boolean adjustedForBankHoliday: boolean } + ThingsToDo: { + prisonerId: string + thingsToDo: 'CALCULATION_REQUIRED'[] + } AnalysedSentenceAndOffence: { /** Format: int64 */ bookingId: number @@ -1582,7 +1506,19 @@ export interface components { sentenceAndOffenceAnalysis: 'NEW' | 'UPDATED' | 'SAME' isSDSPlus: boolean /** @enum {string} */ - hasAnSDSEarlyReleaseExclusion: 'SEXUAL' | 'VIOLENT' | 'DOMESTIC_ABUSE' | 'NATIONAL_SECURITY' | 'TERRORISM' | 'NO' + hasAnSDSEarlyReleaseExclusion: + | 'SEXUAL' + | 'VIOLENT' + | 'DOMESTIC_ABUSE' + | 'NATIONAL_SECURITY' + | 'TERRORISM' + | 'SEXUAL_T3' + | 'VIOLENT_T3' + | 'DOMESTIC_ABUSE_T3' + | 'NATIONAL_SECURITY_T3' + | 'TERRORISM_T3' + | 'MURDER_T3' + | 'NO' } OffenderOffence: { /** Format: int64 */ @@ -1832,7 +1768,19 @@ export interface components { fineAmount?: number isSDSPlus: boolean /** @enum {string} */ - hasAnSDSEarlyReleaseExclusion: 'SEXUAL' | 'VIOLENT' | 'DOMESTIC_ABUSE' | 'NATIONAL_SECURITY' | 'TERRORISM' | 'NO' + hasAnSDSEarlyReleaseExclusion: + | 'SEXUAL' + | 'VIOLENT' + | 'DOMESTIC_ABUSE' + | 'NATIONAL_SECURITY' + | 'TERRORISM' + | 'SEXUAL_T3' + | 'VIOLENT_T3' + | 'DOMESTIC_ABUSE_T3' + | 'NATIONAL_SECURITY_T3' + | 'TERRORISM_T3' + | 'MURDER_T3' + | 'NO' } PersonComparisonJson: { inputData: components['schemas']['JsonNode'] @@ -2299,6 +2247,48 @@ export interface operations { } } } + compareOverallSentenceLength: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['OverallSentenceLengthRequest'] + } + } + responses: { + /** @description Returns the sentence length comparison */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['OverallSentenceLengthComparison'] + } + } + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['OverallSentenceLengthComparison'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['OverallSentenceLengthComparison'] + } + } + } + } storeManualCalculation: { parameters: { query?: never @@ -2757,6 +2747,15 @@ export interface operations { 'application/json': components['schemas']['CalculatedReleaseDates'] } } + /** @description Unable to perform calculation */ + 500: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['CalculatedReleaseDates'] + } + } } } testCalculation: { @@ -3146,6 +3145,90 @@ export interface operations { } } } + validateForDatesManualEntry: { + parameters: { + query: { + releaseDates: string[] + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Validation job has run successfully, the response indicates if there are any errors */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ValidationMessage'][] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ValidationMessage'][] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ValidationMessage'][] + } + } + } + } + getThingsToDo: { + parameters: { + query?: never + header?: never + path: { + /** + * @description Prisoner's ID (also known as nomsId) + * @example A1234AB + */ + prisonerId: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returns the things-to-do list */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ThingsToDo'] + } + } + /** @description Unauthorized - valid Oauth2 token required */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ThingsToDo'] + } + } + /** @description Forbidden - requires appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ThingsToDo'] + } + } + } + } getGenuineOverride: { parameters: { query?: never @@ -3880,24 +3963,6 @@ export interface operations { } } } - getCalculationJson: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } getSentencesAndOffence: { parameters: { query?: never @@ -4574,6 +4639,3 @@ export interface operations { } } } -type WithRequired = T & { - [P in K]-?: T[P] -} diff --git a/server/api/calculateReleaseDatesApiClient.ts b/server/api/calculateReleaseDatesApiClient.ts index ee471dce..5c0a9162 100644 --- a/server/api/calculateReleaseDatesApiClient.ts +++ b/server/api/calculateReleaseDatesApiClient.ts @@ -299,6 +299,12 @@ export default class CalculateReleaseDatesApiClient { }) as Promise } + getManualEntryDateValidation(dateTypes: string[]) { + return this.restClient.get({ + path: `/validation/manual-entry-dates-validation?releaseDates=${dateTypes.join(',')}`, + }) as Promise + } + getCalculationHistory(prisonerId: string): Promise { return this.restClient.get({ path: `/historicCalculations/${prisonerId}`, diff --git a/server/routes/approvedDatesRoutes.test.ts b/server/routes/approvedDatesRoutes.test.ts index 3744620f..011413b0 100644 --- a/server/routes/approvedDatesRoutes.test.ts +++ b/server/routes/approvedDatesRoutes.test.ts @@ -19,13 +19,17 @@ import { StorageResponseModel } from '../services/dateValidationService' import config from '../config' import { testDateTypeDefinitions } from '../testutils/createUserToken' import { FullPageError } from '../types/FullPageError' +import CalculateReleaseDatesService from '../services/calculateReleaseDatesService' + +jest.mock('../services/calculateReleaseDatesService') let app: Express let sessionSetup: SessionSetup const prisonerService = new PrisonerService(null) as jest.Mocked const dateTypeConfigurationService = new DateTypeConfigurationService() const approvedDatesService = new ApprovedDatesService(dateTypeConfigurationService) -const manualEntryService = new ManualEntryService(null, dateTypeConfigurationService, null) +const calculateReleaseDatesService = new CalculateReleaseDatesService() as jest.Mocked +const manualEntryService = new ManualEntryService(dateTypeConfigurationService, null, calculateReleaseDatesService) jest.mock('../services/prisonerService') @@ -74,6 +78,10 @@ beforeEach(() => { config.apis.calculateReleaseDates.url = 'http://localhost:8100' fakeApi = nock(config.apis.calculateReleaseDates.url) fakeApi.get('/reference-data/date-type', '').reply(200, testDateTypeDefinitions) + calculateReleaseDatesService.validateDatesForManualEntry.mockResolvedValue({ + messages: [], + messageType: null, + }) app = appWithAllRoutes({ services: { prisonerService, approvedDatesService, manualEntryService }, sessionSetup, diff --git a/server/routes/manualEntryRoutes.test.ts b/server/routes/manualEntryRoutes.test.ts index 1493e1a2..59ebe161 100644 --- a/server/routes/manualEntryRoutes.test.ts +++ b/server/routes/manualEntryRoutes.test.ts @@ -16,7 +16,6 @@ import { } from '../@types/calculateReleaseDates/calculateReleaseDatesClientTypes' import ManualCalculationService from '../services/manualCalculationService' import ManualEntryService from '../services/manualEntryService' -import ManualEntryValidationService from '../services/manualEntryValidationService' import DateTypeConfigurationService from '../services/dateTypeConfigurationService' import DateValidationService, { StorageResponseModel } from '../services/dateValidationService' import { expectMiniProfile } from './testutils/layoutExpectations' @@ -24,6 +23,7 @@ import SessionSetup from './testutils/sessionSetup' import config from '../config' import { testDateTypeDefinitions } from '../testutils/createUserToken' import { FullPageError } from '../types/FullPageError' +import { ErrorMessageType } from '../types/ErrorMessages' jest.mock('../services/prisonerService') jest.mock('../services/calculateReleaseDatesService') @@ -32,13 +32,12 @@ jest.mock('../services/manualCalculationService') const prisonerService = new PrisonerService(null) as jest.Mocked const calculateReleaseDatesService = new CalculateReleaseDatesService() as jest.Mocked const manualCalculationService = new ManualCalculationService() as jest.Mocked -const manualEntryValidationService = new ManualEntryValidationService() const dateTypeConfigurationService = new DateTypeConfigurationService() const dateValidationService = new DateValidationService() const manualEntryService = new ManualEntryService( - manualEntryValidationService, dateTypeConfigurationService, dateValidationService, + calculateReleaseDatesService, ) let app: Express let sessionSetup: SessionSetup @@ -91,6 +90,10 @@ beforeEach(() => { config.apis.calculateReleaseDates.url = 'http://localhost:8100' fakeApi = nock(config.apis.calculateReleaseDates.url) fakeApi.get('/reference-data/date-type', '').reply(200, testDateTypeDefinitions).persist() + calculateReleaseDatesService.validateDatesForManualEntry.mockResolvedValue({ + messages: [], + messageType: null, + }) app = appWithAllRoutes({ services: { calculateReleaseDatesService, @@ -272,6 +275,32 @@ describe('Tests for /calculation/:nomsId/manual-entry', () => { }) }) + it('POST where invalid date types are submitted displays errors messages', () => { + manualCalculationService.hasRecallSentences.mockResolvedValue(false) + calculateReleaseDatesService.getUnsupportedSentenceOrCalculationMessages.mockResolvedValue([ + { + type: 'UNSUPPORTED_SENTENCE', + } as ValidationMessage, + ]) + calculateReleaseDatesService.validateDatesForManualEntry.mockResolvedValue({ + messages: [{ text: 'CRD and ARD cannot be selected together' }], + messageType: ErrorMessageType.VALIDATION, + }) + prisonerService.getPrisonerDetail.mockResolvedValue(stubbedPrisonerData) + manualCalculationService.hasIndeterminateSentences.mockResolvedValue(true) + return request(app) + .post('/calculation/A1234AA/manual-entry/select-dates') + .send({ dateSelect: ['CRD', 'ARD'] }) + .expect(200) + .expect('Content-Type', /html/) + .expect(res => { + expect(res.text).toContain('Tariff') + expect(res.text).toContain('/calculation/A1234AA/manual-entry') + const $ = cheerio.load(res.text) + expect($('.govuk-error-message li').html()).toStrictEqual('CRD and ARD cannot be selected together') + }) + }) + it('GET if there are indeterminate sentences then should have correct content', () => { manualCalculationService.hasRecallSentences.mockResolvedValue(false) calculateReleaseDatesService.getUnsupportedSentenceOrCalculationMessages.mockResolvedValue([ diff --git a/server/services/calculateReleaseDatesService.ts b/server/services/calculateReleaseDatesService.ts index d7266532..f9e67ca7 100644 --- a/server/services/calculateReleaseDatesService.ts +++ b/server/services/calculateReleaseDatesService.ts @@ -371,6 +371,11 @@ export default class CalculateReleaseDatesService { return new CalculateReleaseDatesApiClient(token).getGenuineOverride(calculationReference) } + async validateDatesForManualEntry(token: string, dateTypes: string[]): Promise { + const validationMessages = await new CalculateReleaseDatesApiClient(token).getManualEntryDateValidation(dateTypes) + return validationMessages.length ? this.convertMessages(validationMessages) : { messages: [] } + } + async validateBookingForManualEntry(prisonerId: string, token: string): Promise { const validationMessages = await new CalculateReleaseDatesApiClient(token).getBookingManualEntryValidation( prisonerId, diff --git a/server/services/index.ts b/server/services/index.ts index 3bddaea8..e7bc8a50 100644 --- a/server/services/index.ts +++ b/server/services/index.ts @@ -7,7 +7,6 @@ import { dataAccess } from '../data' import ManualCalculationService from './manualCalculationService' import ManualEntryService from './manualEntryService' import UserPermissionsService from './userPermissionsService' -import ManualEntryValidationService from './manualEntryValidationService' import ApprovedDatesService from './approvedDatesService' import DateTypeConfigurationService from './dateTypeConfigurationService' import DateValidationService from './dateValidationService' @@ -24,13 +23,12 @@ export const services = () => { const viewReleaseDatesService = new ViewReleaseDatesService() const userInputService = new UserInputService() const manualCalculationService = new ManualCalculationService() - const manualEntryValidationService = new ManualEntryValidationService() const dateTypeConfigurationService = new DateTypeConfigurationService() const dateValidationService = new DateValidationService() const manualEntryService = new ManualEntryService( - manualEntryValidationService, dateTypeConfigurationService, dateValidationService, + calculateReleaseDatesService, ) const userPermissionsService = new UserPermissionsService() const approvedDatesService = new ApprovedDatesService(dateTypeConfigurationService) diff --git a/server/services/manualEntryService.ts b/server/services/manualEntryService.ts index 2525a216..908d8cd7 100644 --- a/server/services/manualEntryService.ts +++ b/server/services/manualEntryService.ts @@ -1,8 +1,8 @@ import { Request } from 'express' import { DateTime } from 'luxon' -import ManualEntryValidationService from './manualEntryValidationService' import DateTypeConfigurationService from './dateTypeConfigurationService' import DateValidationService, { DateInputItem, EnteredDate, StorageResponseModel } from './dateValidationService' +import CalculateReleaseDatesService from './calculateReleaseDatesService' import { ManualEntrySelectedDate, SubmittedDate, @@ -38,9 +38,9 @@ const errorMessage = { } export default class ManualEntryService { constructor( - private readonly manualEntryValidationService: ManualEntryValidationService, private readonly dateTypeConfigurationService: DateTypeConfigurationService, private readonly dateValidationService: DateValidationService, + private readonly calculateReleaseDatesService: CalculateReleaseDatesService, ) { // intentionally left blank } @@ -75,9 +75,15 @@ export default class ManualEntryService { return { error: true, config: mergedConfig } } const selectedDateTypes: string[] = Array.isArray(req.body.dateSelect) ? req.body.dateSelect : [req.body.dateSelect] - const validationMessage = this.manualEntryValidationService.validatePairs(selectedDateTypes) - if (validationMessage) { - const validationError = { errorMessage: { html: validationMessage } } + + const validationMessages = await this.calculateReleaseDatesService.validateDatesForManualEntry( + token, + selectedDateTypes, + ) + + if (validationMessages.messages.length > 0) { + const dateErrors = `
    ${validationMessages.messages.map(e => `
  • ${e.text}
  • `).join('\n')}
` + const validationError = { errorMessage: { html: dateErrors } } const mergedConfig = { ...config, ...validationError } // eslint-disable-next-line no-restricted-syntax this.enrichConfiguration(mergedConfig, req, nomsId)