diff --git a/README.md b/README.md index 80e7a30fd..6e266201c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ This service requires the following dependent services: ## Alerts -- Details of relevant alerts can be obtained from the `hmpps-prodiner-profile` repository [here](https://github.com/ministryofjustice/hmpps-prisoner-profile/blob/main/server/data/alertFlags/alertFlags.ts). - The icons for badges can be obtained from the `digital-prison-services` repository [here](https://github.com/ministryofjustice/digital-prison-services/tree/main/static/images). ## Running the application @@ -100,6 +99,7 @@ FRONTEND_COMPONENT_API_URL=https://frontend-components-dev.hmpps.service.justice MANAGE_USERS_API_URL=https://manage-users-api-dev.hmpps.service.justice.gov.uk BOOK_A_VIDEO_LINK_API_URL=https://book-a-video-link-api-dev.prison.service.justice.gov.uk NON_ASSOCIATIONS_API_URL=https://non-associations-api-dev.hmpps.service.justice.gov.uk +ALERTS_API_URL=https://alerts-api-dev.hmpps.service.justice.gov.uk LOCATIONS_INSIDE_PRISON_API_URL=https://locations-inside-prison-api-dev.hmpps.service.justice.gov.uk NOMIS_MAPPING_API_URL=https://nomis-sync-prisoner-mapping-dev.hmpps.service.justice.gov.uk VIDEO_CONFERENCE_SCHEDULE_URL=https://video-conference-schedule-dev.prison.service.justice.gov.uk diff --git a/feature.env b/feature.env index 10b9e6886..35f3b4bfa 100644 --- a/feature.env +++ b/feature.env @@ -11,6 +11,7 @@ MANAGE_USERS_API_URL=http://localhost:9091 CASE_NOTES_API_URL=http://localhost:9091 BOOK_A_VIDEO_LINK_API_URL=http://localhost:9091 NON_ASSOCIATIONS_API_URL=http://localhost:9091 +ALERTS_API_URL=http://localhost:9091 LOCATIONS_INSIDE_PRISON_API_URL=http://localhost:9091 NOMIS_MAPPING_API_URL=http://localhost:9091 TOKEN_VERIFICATION_ENABLED=true diff --git a/generate-alerts-types.sh b/generate-alerts-types.sh new file mode 100644 index 000000000..c6ccd83d5 --- /dev/null +++ b/generate-alerts-types.sh @@ -0,0 +1 @@ +npx openapi-typescript https://alerts-api-dev.hmpps.service.justice.gov.uk/v3/api-docs > server/@types/alertsApi/index.d.ts \ No newline at end of file diff --git a/helm_deploy/values-dev.yaml b/helm_deploy/values-dev.yaml index 909fd435f..1d181719a 100644 --- a/helm_deploy/values-dev.yaml +++ b/helm_deploy/values-dev.yaml @@ -24,6 +24,7 @@ generic-service: MANAGE_USERS_API_URL: "https://manage-users-api-dev.hmpps.service.justice.gov.uk" BOOK_A_VIDEO_LINK_API_URL: "https://book-a-video-link-api-dev.prison.service.justice.gov.uk" NON_ASSOCIATIONS_API_URL: "https://non-associations-api-dev.hmpps.service.justice.gov.uk" + ALERTS_API_URL: "https://alerts-api-dev.hmpps.service.justice.gov.uk" REPORTING_API_URL: "http://hmpps-digital-prison-reporting-mi-dev.hmpps-digital-prison-reporting-mi-dev.svc.cluster.local" LOCATIONS_INSIDE_PRISON_API_URL: "https://locations-inside-prison-api-dev.hmpps.service.justice.gov.uk" NOMIS_MAPPING_API_URL: "https://nomis-sync-prisoner-mapping-dev.hmpps.service.justice.gov.uk" diff --git a/helm_deploy/values-preprod.yaml b/helm_deploy/values-preprod.yaml index 1a3e84288..8cfb281f1 100644 --- a/helm_deploy/values-preprod.yaml +++ b/helm_deploy/values-preprod.yaml @@ -24,6 +24,7 @@ generic-service: MANAGE_USERS_API_URL: "https://manage-users-api-preprod.hmpps.service.justice.gov.uk" BOOK_A_VIDEO_LINK_API_URL: "https://book-a-video-link-api-preprod.prison.service.justice.gov.uk" NON_ASSOCIATIONS_API_URL: "https://non-associations-api-preprod.hmpps.service.justice.gov.uk" + ALERTS_API_URL: "https://alerts-api-preprod.hmpps.service.justice.gov.uk" REPORTING_API_URL: "http://hmpps-digital-prison-reporting-mi-preprod.hmpps-digital-prison-reporting-mi-preprod.svc.cluster.local" LOCATIONS_INSIDE_PRISON_API_URL: "https://locations-inside-prison-api-preprod.hmpps.service.justice.gov.uk" NOMIS_MAPPING_API_URL: "https://nomis-sync-prisoner-mapping-preprod.hmpps.service.justice.gov.uk" diff --git a/helm_deploy/values-prod.yaml b/helm_deploy/values-prod.yaml index 0075cf563..502d64a95 100644 --- a/helm_deploy/values-prod.yaml +++ b/helm_deploy/values-prod.yaml @@ -22,6 +22,7 @@ generic-service: MANAGE_USERS_API_URL: "https://manage-users-api.hmpps.service.justice.gov.uk" BOOK_A_VIDEO_LINK_API_URL: "https://book-a-video-link-api.prison.service.justice.gov.uk" NON_ASSOCIATIONS_API_URL: "https://non-associations-api.hmpps.service.justice.gov.uk" + ALERTS_API_URL: "https://alerts-api.hmpps.service.justice.gov.uk" REPORTING_API_URL: "http://hmpps-digital-prison-reporting-mi-prod.hmpps-digital-prison-reporting-mi-prod.svc.cluster.local" LOCATIONS_INSIDE_PRISON_API_URL: "https://locations-inside-prison-api.hmpps.service.justice.gov.uk" NOMIS_MAPPING_API_URL: "https://nomis-sync-prisoner-mapping.hmpps.service.justice.gov.uk" diff --git a/integration_tests/e2e/appointments/createAppointment.cy.ts b/integration_tests/e2e/appointments/createAppointment.cy.ts index 4988fccfa..0ba7005ef 100644 --- a/integration_tests/e2e/appointments/createAppointment.cy.ts +++ b/integration_tests/e2e/appointments/createAppointment.cy.ts @@ -14,7 +14,7 @@ import getScheduledEvents from '../../fixtures/activitiesApi/getScheduledEventsM import getAppointmentSeries from '../../fixtures/activitiesApi/getAppointmentSeries.json' import getGroupAppointmentSeriesDetails from '../../fixtures/activitiesApi/getGroupAppointmentSeriesDetails.json' import getGroupAppointmentDetails from '../../fixtures/activitiesApi/getGroupAppointmentDetails.json' -import getOffenderAlerts from '../../fixtures/activitiesApi/getOffenderAlerts.json' +import getPrisonerAlerts from '../../fixtures/alertsApi/getPrisonerAlerts.json' import getNonAssociationsBetweenA8644DYA1350DZ from '../../fixtures/nonAssociationsApi/getNonAssociationsBetweenA8644DYA1350DZ.json' import HowToAddPrisonersPage from '../../pages/appointments/create-and-edit/howToAddPrisonersPage' import ReviewPrisonersPage from '../../pages/appointments/create-and-edit/reviewPrisonersPage' @@ -80,7 +80,7 @@ context('Create group appointment', () => { '/users/jsmith', JSON.parse('{"name": "John Smith", "username": "jsmith", "authSource": "nomis"}'), ) - cy.stubEndpoint('POST', '/api/bookings/offenderNo/MDI/alerts', getOffenderAlerts) + cy.stubEndpoint('POST', '/search/alerts/prison-numbers\\?includeInactive=false', getPrisonerAlerts) cy.stubEndpoint('POST', '/non-associations/between', getNonAssociationsBetweenA8644DYA1350DZ) }) diff --git a/integration_tests/e2e/appointments/createAppointmentBackLinks.cy.ts b/integration_tests/e2e/appointments/createAppointmentBackLinks.cy.ts index 628d8331e..a779e9556 100644 --- a/integration_tests/e2e/appointments/createAppointmentBackLinks.cy.ts +++ b/integration_tests/e2e/appointments/createAppointmentBackLinks.cy.ts @@ -25,7 +25,7 @@ import TierPage from '../../pages/appointments/create-and-edit/tierPage' import HostPage from '../../pages/appointments/create-and-edit/hostPage' import HowToAddPrisonersPage from '../../pages/appointments/create-and-edit/howToAddPrisonersPage' import ReviewPrisonersPage from '../../pages/appointments/create-and-edit/reviewPrisonersPage' -import getOffenderAlerts from '../../fixtures/activitiesApi/getOffenderAlertsA8644DY.json' +import getPrisonerAlertsA8644DY from '../../fixtures/alertsApi/getPrisonerAlertsA8644DY.json' import ReviewPrisonerAlertsPage from '../../pages/appointments/create-and-edit/reviewPrisonerAlertsPage' context('Create group appointment - back links', () => { @@ -52,7 +52,7 @@ context('Create group appointment - back links', () => { cy.stubEndpoint('POST', '/appointment-series', getAppointmentSeries) cy.stubEndpoint('GET', '/appointment-series/10/details', getAppointmentSeriesDetails) cy.stubEndpoint('GET', '/appointments/11/details', getAppointmentDetails) - cy.stubEndpoint('POST', '/api/bookings/offenderNo/MDI/alerts', getOffenderAlerts) + cy.stubEndpoint('POST', '/search/alerts/prison-numbers\\?includeInactive=false', getPrisonerAlertsA8644DY) cy.stubEndpoint('POST', '/non-associations/between', []) }) diff --git a/integration_tests/e2e/appointments/createAppointmentCheckAnswers.cy.ts b/integration_tests/e2e/appointments/createAppointmentCheckAnswers.cy.ts index 912a7d451..910560940 100644 --- a/integration_tests/e2e/appointments/createAppointmentCheckAnswers.cy.ts +++ b/integration_tests/e2e/appointments/createAppointmentCheckAnswers.cy.ts @@ -27,7 +27,7 @@ import TierPage from '../../pages/appointments/create-and-edit/tierPage' import HostPage from '../../pages/appointments/create-and-edit/hostPage' import HowToAddPrisonersPage from '../../pages/appointments/create-and-edit/howToAddPrisonersPage' import ReviewPrisonersPage from '../../pages/appointments/create-and-edit/reviewPrisonersPage' -import getOffenderAlerts from '../../fixtures/activitiesApi/getOffenderAlertsA8644DY.json' +import getPrisonerAlertsA8644DY from '../../fixtures/alertsApi/getPrisonerAlertsA8644DY.json' import ReviewPrisonerAlertsPage from '../../pages/appointments/create-and-edit/reviewPrisonerAlertsPage' context('Create group appointment - check answers change links', () => { @@ -66,7 +66,7 @@ context('Create group appointment - check answers change links', () => { cy.stubEndpoint('POST', '/appointment-series', getAppointmentSeries) cy.stubEndpoint('GET', '/appointment-series/10/details', getAppointmentSeriesDetails) cy.stubEndpoint('GET', '/appointments/11/details', getAppointmentDetails) - cy.stubEndpoint('POST', '/api/bookings/offenderNo/MDI/alerts?', getOffenderAlerts) + cy.stubEndpoint('POST', '/search/alerts/prison-numbers\\?includeInactive=false', getPrisonerAlertsA8644DY) cy.stubEndpoint('POST', '/non-associations/between', []) }) diff --git a/integration_tests/e2e/appointments/createInCellAppointment.cy.ts b/integration_tests/e2e/appointments/createInCellAppointment.cy.ts index 699d0ea27..342f87336 100644 --- a/integration_tests/e2e/appointments/createInCellAppointment.cy.ts +++ b/integration_tests/e2e/appointments/createInCellAppointment.cy.ts @@ -26,7 +26,7 @@ import getPrisonerA8644DY from '../../fixtures/prisonerSearchApi/getPrisoner-MDI import getAppointmentSeriesDetails from '../../fixtures/activitiesApi/getAppointmentSeriesDetails.json' import getAppointmentDetails from '../../fixtures/activitiesApi/getAppointmentDetails.json' import ReviewPrisonerAlertsPage from '../../pages/appointments/create-and-edit/reviewPrisonerAlertsPage' -import getOffenderAlerts from '../../fixtures/activitiesApi/getOffenderAlertsA8644DY.json' +import getPrisonerAlerts from '../../fixtures/alertsApi/getPrisonerAlertsA8644DY.json' context('Create group appointment', () => { const tomorrow = addDays(new Date(), 1) @@ -58,7 +58,7 @@ context('Create group appointment', () => { '/users/jsmith', JSON.parse('{"name": "John Smith", "username": "jsmith", "authSource": "nomis"}'), ) - cy.stubEndpoint('POST', '/api/bookings/offenderNo/MDI/alerts', getOffenderAlerts) + cy.stubEndpoint('POST', '/search/alerts/prison-numbers\\?includeInactive=false', getPrisonerAlerts) cy.stubEndpoint('POST', '/non-associations/between', []) }) diff --git a/integration_tests/e2e/appointments/createRepeatAppointment.cy.ts b/integration_tests/e2e/appointments/createRepeatAppointment.cy.ts index 824b0a8fb..09a2e260d 100644 --- a/integration_tests/e2e/appointments/createRepeatAppointment.cy.ts +++ b/integration_tests/e2e/appointments/createRepeatAppointment.cy.ts @@ -29,7 +29,7 @@ import ExtraInformationPage from '../../pages/appointments/create-and-edit/extra import SchedulePage from '../../pages/appointments/create-and-edit/schedulePage' import TierPage from '../../pages/appointments/create-and-edit/tierPage' import HostPage from '../../pages/appointments/create-and-edit/hostPage' -import getOffenderAlerts from '../../fixtures/activitiesApi/getOffenderAlerts.json' +import getPrisonerAlerts from '../../fixtures/alertsApi/getPrisonerAlerts.json' import ReviewPrisonerAlertsPage, { arsonistBadge, catABadge, @@ -86,7 +86,7 @@ context('Create group appointment', () => { '/users/jsmith', JSON.parse('{"name": "John Smith", "username": "jsmith", "authSource": "nomis"}'), ) - cy.stubEndpoint('POST', '/api/bookings/offenderNo/MDI/alerts?', getOffenderAlerts) + cy.stubEndpoint('POST', '/search/alerts/prison-numbers\\?includeInactive=false', getPrisonerAlerts) cy.stubEndpoint('POST', '/non-associations/between', []) }) diff --git a/integration_tests/e2e/appointments/createRetrospectiveAppointment.cy.ts b/integration_tests/e2e/appointments/createRetrospectiveAppointment.cy.ts index bfcfaf8e5..2be91d9a0 100644 --- a/integration_tests/e2e/appointments/createRetrospectiveAppointment.cy.ts +++ b/integration_tests/e2e/appointments/createRetrospectiveAppointment.cy.ts @@ -14,7 +14,7 @@ import getScheduledEvents from '../../fixtures/activitiesApi/getScheduledEventsM import getAppointmentSeries from '../../fixtures/activitiesApi/getAppointmentSeries.json' import getGroupAppointmentSeriesDetails from '../../fixtures/activitiesApi/getGroupAppointmentSeriesDetails.json' import getGroupAppointmentDetails from '../../fixtures/activitiesApi/getGroupAppointmentDetails.json' -import getOffenderAlerts from '../../fixtures/activitiesApi/getOffenderAlerts.json' +import getPrisonerAlerts from '../../fixtures/alertsApi/getPrisonerAlerts.json' import HowToAddPrisonersPage from '../../pages/appointments/create-and-edit/howToAddPrisonersPage' import ReviewPrisonersPage from '../../pages/appointments/create-and-edit/reviewPrisonersPage' import ReviewPrisonerAlertsPage, { @@ -73,7 +73,7 @@ context('Create a retrospective appointment', () => { '/users/jsmith', JSON.parse('{"name": "John Smith", "username": "jsmith", "authSource": "nomis"}'), ) - cy.stubEndpoint('POST', '/api/bookings/offenderNo/MDI/alerts', getOffenderAlerts) + cy.stubEndpoint('POST', '/search/alerts/prison-numbers\\?includeInactive=false', getPrisonerAlerts) cy.stubEndpoint('POST', '/non-associations/between', []) }) diff --git a/integration_tests/e2e/appointments/duplicateAppointment.cy.ts b/integration_tests/e2e/appointments/duplicateAppointment.cy.ts index aa36d026a..071d952fd 100644 --- a/integration_tests/e2e/appointments/duplicateAppointment.cy.ts +++ b/integration_tests/e2e/appointments/duplicateAppointment.cy.ts @@ -15,7 +15,7 @@ import ReviewPrisonersPage from '../../pages/appointments/create-and-edit/review import DateAndTimePage from '../../pages/appointments/create-and-edit/dateAndTimePage' import SchedulePage from '../../pages/appointments/create-and-edit/schedulePage' import getGroupAppointmentSeriesDetails from '../../fixtures/activitiesApi/getGroupAppointmentSeriesDetails.json' -import getOffenderAlerts from '../../fixtures/activitiesApi/getOffenderAlerts.json' +import getPrisonerAlerts from '../../fixtures/alertsApi/getPrisonerAlerts.json' import getScheduledEvents from '../../fixtures/activitiesApi/getScheduledEventsMdi20230202.json' import { formatDate } from '../../../server/utils/utils' import CheckAnswersPage from '../../pages/appointments/create-and-edit/checkAnswersPage' @@ -56,7 +56,7 @@ context('Duplicate appointment', () => { JSON.parse('{"name": "John Smith", "username": "jsmith", "authSource": "nomis"}'), ) cy.stubEndpoint('GET', '/appointment-series/10/details', getGroupAppointmentSeriesDetails) - cy.stubEndpoint('POST', '/api/bookings/offenderNo/MDI/alerts', getOffenderAlerts) + cy.stubEndpoint('POST', '/search/alerts/prison-numbers\\?includeInactive=false', getPrisonerAlerts) cy.stubEndpoint('POST', `/scheduled-events/prison/MDI\\?date=${tomorrowFormatted}`, getScheduledEvents) cy.stubEndpoint('POST', '/appointment-series', getAppointmentSeries) }) diff --git a/integration_tests/e2e/appointments/editAppointments.cy.ts b/integration_tests/e2e/appointments/editAppointments.cy.ts index cd5aabc22..1702871c0 100644 --- a/integration_tests/e2e/appointments/editAppointments.cy.ts +++ b/integration_tests/e2e/appointments/editAppointments.cy.ts @@ -7,8 +7,8 @@ import getPrisonPrisonersG0995GW from '../../fixtures/prisonerSearchApi/getPriso import getPrisonPrisonersG6123VU from '../../fixtures/prisonerSearchApi/getPrisonPrisoners-MDI-G6123VU.json' import getPrisonerG0995GW from '../../fixtures/prisonerSearchApi/getPrisoner-MDI-G0995GW.json' import getPrisonerG6123VU from '../../fixtures/prisonerSearchApi/getPrisoner-MDI-G6123VU.json' -import getOffenderAlertsG0995GW from '../../fixtures/activitiesApi/getOffenderAlertsG0995GW.json' -import getOffenderAlertsG0995GWG6123VU from '../../fixtures/activitiesApi/getOffenderAlertsG0995GWG6123VU.json' +import getPrisonerAlertsG0995GW from '../../fixtures/alertsApi/getPrisonerAlertsG0995GW.json' +import getPrisonerAlertsG0995GWG6123VU from '../../fixtures/alertsApi/getPrisonerAlertsG0995GWG6123VU.json' import getNonAssociationsBetweenG0995GWA1350DZ from '../../fixtures/nonAssociationsApi/getNonAssociationsBetweenG0995GWA1350DZ.json' import getNonAssociationsBetweenG0995GWG6123VU from '../../fixtures/nonAssociationsApi/getNonAssociationsBetweenG0995GWG6123VU.json' import getPrisonPrisonersG0995GWA1351DZ from '../../fixtures/prisonerSearchApi/postPrisonerNumbers-A1350DZ-G0995GW.json' @@ -61,8 +61,8 @@ context('Edit appointment', () => { cy.stubEndpoint('GET', '/prison/MDI/prisoners\\?term=saunders&size=50', getPrisonPrisonersG6123VU) cy.stubEndpoint('GET', '/prisoner/G0995GW', getPrisonerG0995GW) cy.stubEndpoint('GET', '/prisoner/G6123VU', getPrisonerG6123VU) - cy.stubEndpoint('POST', '/api/bookings/offenderNo/MDI/alerts', getOffenderAlertsG0995GW) - cy.stubEndpoint('POST', '/api/bookings/offenderNo/MDI/alerts', getOffenderAlertsG0995GWG6123VU) + cy.stubEndpoint('POST', '/search/alerts/prison-numbers\\?includeInactive=false', getPrisonerAlertsG0995GW) + cy.stubEndpoint('POST', '/search/alerts/prison-numbers\\?includeInactive=false', getPrisonerAlertsG0995GWG6123VU) cy.stubEndpoint('POST', '/non-associations/between', getNonAssociationsBetweenG0995GWA1350DZ) cy.stubEndpoint('POST', '/prisoner-search/prisoner-numbers', getPrisonPrisonersG0995GWA1351DZ) diff --git a/integration_tests/e2e/appointments/reviewNonAssociationsPage.cy.ts b/integration_tests/e2e/appointments/reviewNonAssociationsPage.cy.ts index de8445fcd..ac0feb5c8 100644 --- a/integration_tests/e2e/appointments/reviewNonAssociationsPage.cy.ts +++ b/integration_tests/e2e/appointments/reviewNonAssociationsPage.cy.ts @@ -14,7 +14,7 @@ import getScheduledEvents from '../../fixtures/activitiesApi/getScheduledEventsM import getAppointmentSeries from '../../fixtures/activitiesApi/getAppointmentSeries.json' import getGroupAppointmentSeriesDetails from '../../fixtures/activitiesApi/getGroupAppointmentSeriesDetails.json' import getGroupAppointmentDetails from '../../fixtures/activitiesApi/getGroupAppointmentDetails.json' -import getOffenderAlerts from '../../fixtures/activitiesApi/getOffenderAlerts.json' +import getPrisonerAlerts from '../../fixtures/alertsApi/getPrisonerAlerts.json' import getNonAssociationsBetweenA8644DYA1350DZ from '../../fixtures/nonAssociationsApi/getNonAssociationsBetweenA8644DYA1350DZ.json' import HowToAddPrisonersPage from '../../pages/appointments/create-and-edit/howToAddPrisonersPage' import ReviewPrisonersPage from '../../pages/appointments/create-and-edit/reviewPrisonersPage' @@ -73,7 +73,7 @@ context('Create group appointment', () => { '/users/jsmith', JSON.parse('{"name": "John Smith", "username": "jsmith", "authSource": "nomis"}'), ) - cy.stubEndpoint('POST', '/api/bookings/offenderNo/MDI/alerts', getOffenderAlerts) + cy.stubEndpoint('POST', '/search/alerts/prison-numbers\\?includeInactive=false', getPrisonerAlerts) cy.stubEndpoint('POST', '/non-associations/between', getNonAssociationsBetweenA8644DYA1350DZ) }) diff --git a/integration_tests/e2e/health.cy.ts b/integration_tests/e2e/health.cy.ts index 5c395fd7e..ea5ea94ae 100644 --- a/integration_tests/e2e/health.cy.ts +++ b/integration_tests/e2e/health.cy.ts @@ -37,6 +37,7 @@ context('Healthcheck', () => { expect(response.body.checks.manageUsersApi).to.equal('OK') expect(response.body.checks.bookAVideoLinkApi).to.equal('OK') expect(response.body.checks.nonAssociationsApi).to.equal('OK') + expect(response.body.checks.alertsApi).to.equal('OK') expect(response.body.checks.locationsInsidePrisonApi).to.equal('OK') expect(response.body.checks.nomisMapping).to.equal('OK') expect(response.body.checks.tokenVerification).to.contain({ status: 500, retries: 2 }) diff --git a/integration_tests/fixtures/activitiesApi/getOffenderAlerts.json b/integration_tests/fixtures/activitiesApi/getOffenderAlerts.json deleted file mode 100644 index 59a5fb1d8..000000000 --- a/integration_tests/fixtures/activitiesApi/getOffenderAlerts.json +++ /dev/null @@ -1,72 +0,0 @@ -[ - { - "active": false, - "alertId": 1, - "bookingId": 830082, - "offenderNo": "A1351DZ", - "alertType": "X", - "alertTypeDescription": "Security", - "alertCode": "XNR", - "alertCodeDescription": "Not For Release", - "dateCreated": "2010-08-19", - "dateExpires": "2012-03-02", - "modifiedDateTime": "2017-05-09T21:50:35.311477", - "expired": true - }, - { - "active": true, - "alertId": 2, - "bookingId": 830082, - "offenderNo": "A1351DZ", - "alertType": "X", - "alertTypeDescription": "Security", - "alertCode": "XTACT", - "alertCodeDescription": "Terrorism Act or Related Offence", - "dateCreated": "2023-03-20", - "dateExpires": "2012-03-02", - "modifiedDateTime": "2017-05-09T21:57:04.212529", - "expired": false - }, - { - "active": true, - "alertId": 3, - "bookingId": 830082, - "offenderNo": "A1351DZ", - "alertType": "X", - "alertTypeDescription": "Security", - "alertCode": "XA", - "alertCodeDescription": "Arsonist", - "dateCreated": "2023-03-20", - "dateExpires": "2012-03-02", - "modifiedDateTime": "2023-03-20T21:57:04.212529", - "expired": false - }, - { - "active": true, - "alertId": 4, - "bookingId": 830082, - "offenderNo": "A1351DZ", - "alertType": "R", - "alertTypeDescription": "Risk", - "alertCode": "RNO121", - "alertCodeDescription": "No 1 to 1 with this prisoner", - "dateCreated": "2023-03-20", - "dateExpires": "2027-03-20", - "modifiedDateTime": "2023-03-20T21:57:04.212529", - "expired": false - }, - { - "active": true, - "alertId": 5, - "bookingId": 830082, - "offenderNo": "A1351DZ", - "alertType": "R", - "alertTypeDescription": "Risk", - "alertCode": "XCO", - "alertCodeDescription": "Corruptor", - "dateCreated": "2023-03-20", - "dateExpires": "2027-03-20", - "modifiedDateTime": "2023-03-20T21:57:04.212529", - "expired": false - } -] diff --git a/integration_tests/fixtures/activitiesApi/getOffenderAlertsA8644DY.json b/integration_tests/fixtures/activitiesApi/getOffenderAlertsA8644DY.json deleted file mode 100644 index cdd710aee..000000000 --- a/integration_tests/fixtures/activitiesApi/getOffenderAlertsA8644DY.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "active": true, - "alertId": 1, - "bookingId": 830082, - "offenderNo": "A8644DY", - "alertType": "X", - "alertTypeDescription": "Security", - "alertCode": "XNR", - "alertCodeDescription": "Not For Release", - "dateCreated": "2010-08-19", - "dateExpires": "2012-03-02", - "modifiedDateTime": "2017-05-09T21:50:35.311477", - "expired": false - } -] diff --git a/integration_tests/fixtures/activitiesApi/getOffenderAlertsG0995GW.json b/integration_tests/fixtures/activitiesApi/getOffenderAlertsG0995GW.json deleted file mode 100644 index 7b266074d..000000000 --- a/integration_tests/fixtures/activitiesApi/getOffenderAlertsG0995GW.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "active": true, - "alertId": 1, - "bookingId": 830082, - "offenderNo": "G0995GW", - "alertType": "X", - "alertTypeDescription": "Security", - "alertCode": "XNR", - "alertCodeDescription": "Not For Release", - "dateCreated": "2010-08-19", - "dateExpires": "2012-03-02", - "modifiedDateTime": "2017-05-09T21:50:35.311477", - "expired": false - } -] diff --git a/integration_tests/fixtures/activitiesApi/getOffenderAlertsG0995GWG6123VU.json b/integration_tests/fixtures/activitiesApi/getOffenderAlertsG0995GWG6123VU.json deleted file mode 100644 index 7b266074d..000000000 --- a/integration_tests/fixtures/activitiesApi/getOffenderAlertsG0995GWG6123VU.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "active": true, - "alertId": 1, - "bookingId": 830082, - "offenderNo": "G0995GW", - "alertType": "X", - "alertTypeDescription": "Security", - "alertCode": "XNR", - "alertCodeDescription": "Not For Release", - "dateCreated": "2010-08-19", - "dateExpires": "2012-03-02", - "modifiedDateTime": "2017-05-09T21:50:35.311477", - "expired": false - } -] diff --git a/integration_tests/fixtures/alertsApi/getPrisonerAlerts.json b/integration_tests/fixtures/alertsApi/getPrisonerAlerts.json new file mode 100644 index 000000000..7e224d4e4 --- /dev/null +++ b/integration_tests/fixtures/alertsApi/getPrisonerAlerts.json @@ -0,0 +1,100 @@ +{ + "content": [ + { + "alertUuid": "527b44f6-182e-4f5b-94c7-8f974bc77c64", + "prisonNumber": "A1351DZ", + "alertCode": { + "alertTypeCode": "X", + "alertTypeDescription": "Security", + "code": "XTACT", + "description": "Terrorism Act or Related Offence" + }, + "description": "Terrorism Act or Related Offence", + "authorisedBy": null, + "activeFrom": "2023-03-20", + "activeTo": null, + "isActive": true, + "createdAt": "2023-03-20T14:13:55", + "createdBy": "SCH_ACTIVITY", + "createdByDisplayName": "Schedule Activity", + "lastModifiedAt": null, + "lastModifiedBy": null, + "lastModifiedByDisplayName": null, + "activeToLastSetAt": null, + "activeToLastSetBy": null, + "activeToLastSetByDisplayName": null + }, + { + "alertUuid": "f32a8c22-4ed5-4039-a497-4db0dd1f2406", + "prisonNumber": "A1351DZ", + "alertCode": { + "alertTypeCode": "X", + "alertTypeDescription": "Security", + "code": "XA", + "description": "Arsonist" + }, + "description": "Arsonist", + "authorisedBy": null, + "activeFrom": "2023-03-20", + "activeTo": null, + "isActive": true, + "createdAt": "2023-03-20T14:13:26", + "createdBy": "SCH_ACTIVITY", + "createdByDisplayName": "Schedule Activity", + "lastModifiedAt": null, + "lastModifiedBy": null, + "lastModifiedByDisplayName": null, + "activeToLastSetAt": null, + "activeToLastSetBy": null, + "activeToLastSetByDisplayName": null + }, + { + "alertUuid": "b1e255ec-a39a-4ed6-aa81-3d6d9e99fa9e", + "prisonNumber": "A1351DZ", + "alertCode": { + "alertTypeCode": "R", + "alertTypeDescription": "Risk", + "code": "RNO121", + "description": "No 1 to 1 with this prisoner" + }, + "description": "1-1", + "authorisedBy": null, + "activeFrom": "2023-03-20", + "activeTo": null, + "isActive": true, + "createdAt": "2023-03-20T14:12:47", + "createdBy": "SCH_ACTIVITY", + "createdByDisplayName": "Schedule Activity", + "lastModifiedAt": null, + "lastModifiedBy": null, + "lastModifiedByDisplayName": null, + "activeToLastSetAt": null, + "activeToLastSetBy": null, + "activeToLastSetByDisplayName": null + }, + { + "alertUuid": "b6f8c9eb-2adf-41e5-9427-e9b5af237bfe", + "prisonNumber": "A1351DZ", + "alertCode": { + "alertTypeCode": "R", + "alertTypeDescription": "Risk", + "code": "XCO", + "description": "Corruptor" + }, + "description": "Corruptor", + "authorisedBy": null, + "activeFrom": "2023-03-20", + "activeTo": null, + "isActive": true, + "createdAt": "2023-03-20T14:11:30", + "createdBy": "SCH_ACTIVITY", + "createdByDisplayName": "Schedule Activity", + "lastModifiedAt": null, + "lastModifiedBy": null, + "lastModifiedByDisplayName": null, + "activeToLastSetAt": null, + "activeToLastSetBy": null, + "activeToLastSetByDisplayName": null + } + ] +} diff --git a/integration_tests/fixtures/alertsApi/getPrisonerAlertsA8644DY.json b/integration_tests/fixtures/alertsApi/getPrisonerAlertsA8644DY.json new file mode 100644 index 000000000..bdea6871f --- /dev/null +++ b/integration_tests/fixtures/alertsApi/getPrisonerAlertsA8644DY.json @@ -0,0 +1,28 @@ +{ + "content": [ + { + "alertUuid": "6cd3d3cb-fe76-4746-b2ea-a6968a63c2aa", + "prisonNumber": "A8644DY", + "alertCode": { + "alertTypeCode": "X", + "alertTypeDescription": "Security", + "code": "XNR", + "description": "Not For Release" + }, + "description": "Not For Release", + "authorisedBy": null, + "activeFrom": "2023-03-20", + "activeTo": null, + "isActive": true, + "createdAt": "2023-03-20T14:14:27", + "createdBy": "SCH_ACTIVITY", + "createdByDisplayName": "Schedule Activity", + "lastModifiedAt": null, + "lastModifiedBy": null, + "lastModifiedByDisplayName": null, + "activeToLastSetAt": null, + "activeToLastSetBy": null, + "activeToLastSetByDisplayName": null + } + ] +} diff --git a/integration_tests/fixtures/alertsApi/getPrisonerAlertsG0995GW.json b/integration_tests/fixtures/alertsApi/getPrisonerAlertsG0995GW.json new file mode 100644 index 000000000..c8fde8bc8 --- /dev/null +++ b/integration_tests/fixtures/alertsApi/getPrisonerAlertsG0995GW.json @@ -0,0 +1,28 @@ +{ + "content": [ + { + "alertUuid": "6cd3d3cb-fe76-4746-b2ea-a6968a63c2aa", + "prisonNumber": "G0995GW", + "alertCode": { + "alertTypeCode": "X", + "alertTypeDescription": "Security", + "code": "XNR", + "description": "Not For Release" + }, + "description": "Not For Release", + "authorisedBy": null, + "activeFrom": "2023-03-20", + "activeTo": null, + "isActive": true, + "createdAt": "2023-03-20T14:14:27", + "createdBy": "SCH_ACTIVITY", + "createdByDisplayName": "Schedule Activity", + "lastModifiedAt": null, + "lastModifiedBy": null, + "lastModifiedByDisplayName": null, + "activeToLastSetAt": null, + "activeToLastSetBy": null, + "activeToLastSetByDisplayName": null + } + ] +} diff --git a/integration_tests/fixtures/alertsApi/getPrisonerAlertsG0995GWG6123VU.json b/integration_tests/fixtures/alertsApi/getPrisonerAlertsG0995GWG6123VU.json new file mode 100644 index 000000000..c8fde8bc8 --- /dev/null +++ b/integration_tests/fixtures/alertsApi/getPrisonerAlertsG0995GWG6123VU.json @@ -0,0 +1,28 @@ +{ + "content": [ + { + "alertUuid": "6cd3d3cb-fe76-4746-b2ea-a6968a63c2aa", + "prisonNumber": "G0995GW", + "alertCode": { + "alertTypeCode": "X", + "alertTypeDescription": "Security", + "code": "XNR", + "description": "Not For Release" + }, + "description": "Not For Release", + "authorisedBy": null, + "activeFrom": "2023-03-20", + "activeTo": null, + "isActive": true, + "createdAt": "2023-03-20T14:14:27", + "createdBy": "SCH_ACTIVITY", + "createdByDisplayName": "Schedule Activity", + "lastModifiedAt": null, + "lastModifiedBy": null, + "lastModifiedByDisplayName": null, + "activeToLastSetAt": null, + "activeToLastSetBy": null, + "activeToLastSetByDisplayName": null + } + ] +} diff --git a/server/@types/alertsAPI/index.d.ts b/server/@types/alertsAPI/index.d.ts new file mode 100644 index 000000000..89ff3fd17 --- /dev/null +++ b/server/@types/alertsAPI/index.d.ts @@ -0,0 +1,2343 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + '/alerts/{alertUuid}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Get an alert by its unique identifier + * @description Returns the alert with the matching identifier. + * + * Requires one of the following roles: + * * ROLE_PRISONER_ALERTS__RO + * * ROLE_PRISONER_ALERTS__RW + */ + get: operations['retrieveAlert'] + /** + * Update an alert + * @description + * + * Requires one of the following roles: + * * ROLE_PRISONER_ALERTS__RW + */ + put: operations['updateAlert'] + post?: never + /** + * Delete an alert + * @description This endpoint fully removes the alert from the system. It is used when an alert has been created in error or should otherwise be removed from this service. This endpoint will return 200 OK if the alert was not found or already deleted. This is to prevent low value warnings being logged. + * + * Requires one of the following roles: + * * ROLE_PRISONER_ALERTS__RW + */ + delete: operations['deleteAlert'] + options?: never + head?: never + patch?: never + trace?: never + } + '/search/alerts/prison-numbers': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * Gets all the alerts for prisoners by their prison numbers + * @description Returns all the alerts for the supplied prison numbers. + * + * Requires one of the following roles: + * * ROLE_PRISONER_ALERTS__RO + * * ROLE_PRISONER_ALERTS__RW + */ + post: operations['retrievePrisonerAlerts'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/resync/{prisonNumber}/alerts': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * Resync all alerts for a prisoner from NOMIS + * @description + * + * Requires one of the following roles: + * * ROLE_NOMIS_ALERTS + */ + post: operations['resyncPrisonerAlerts'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/prisoners/{prisonNumber}/alerts': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Gets all the alerts for a prisoner by their prison number + * @description + * + * Requires one of the following roles: + * * ROLE_PRISONER_ALERTS__RO + * * ROLE_PRISONER_ALERTS__RW + */ + get: operations['retrievePrisonerAlerts_1'] + put?: never + /** + * Create an alert + * @description + * + * Requires one of the following roles: + * * ROLE_PRISONER_ALERTS__RW + */ + post: operations['createPrisonerAlert'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/bulk-alerts/plan': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * Create the plan for bulk alerts + * @description + * + * + */ + post: operations['createPlan'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/bulk-alerts/plan/{id}/start': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * Start the plan for bulk alerts + * @description + * + * + */ + post: operations['startPlan'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/alert-types': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Get all alert types + * @description Returns the full list of alert types and the alert codes within those types. By default this endpoint only returns active alert types and codes. The include inactive parameter can be used to return all alert types and codes. + * + * Requires one of the following roles: + * * ROLE_PRISONER_ALERTS__RO + * * ROLE_PRISONER_ALERTS__RW + */ + get: operations['retrieveAlertTypes'] + put?: never + /** + * Create an alert type + * @description Create a new alert type, typically from the Alerts UI + * + * + */ + post: operations['createAlertType'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/alert-codes': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Retrieve all alert codes + * @description Retrieve all alert codes, typically from the Alerts UI + * + * Requires one of the following roles: + * * ROLE_PRISONER_ALERTS__RO + * * ROLE_PRISONER_ALERTS__RW + */ + get: operations['retrieveAlertCodes'] + put?: never + /** + * Create an alert code + * @description Create a new alert code, typically from the Alerts UI + * + * + */ + post: operations['createAlertCode'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/bulk-alerts/plan/{id}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + delete?: never + options?: never + head?: never + /** + * Update the plan for bulk alerts + * @description + * + * + */ + patch: operations['updatePlan'] + trace?: never + } + '/alert-types/{alertType}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + delete?: never + options?: never + head?: never + /** + * Update alert type + * @description Set the properties of an alert type to the submitted value. + * + * + */ + patch: operations['updateAlertType'] + trace?: never + } + '/alert-types/{alertType}/reactivate': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + delete?: never + options?: never + head?: never + /** + * Reactivate an alert type + * @description Reactivate an alert type, typically from the Alerts UI + * + * + */ + patch: operations['reactivateAlertType'] + trace?: never + } + '/alert-types/{alertType}/deactivate': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + delete?: never + options?: never + head?: never + /** + * Deactivate an alert type + * @description Deactivate an alert type, typically from the Alerts UI + * + * + */ + patch: operations['deactivateAlertType'] + trace?: never + } + '/alert-codes/{alertCode}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Retrieve an alert code + * @description Retrieve an alert code, typically from the Alerts UI + * + * Requires one of the following roles: + * * ROLE_PRISONER_ALERTS__RO + * * ROLE_PRISONER_ALERTS__RW + */ + get: operations['retrieveAlertCode'] + put?: never + post?: never + delete?: never + options?: never + head?: never + /** + * Update alert code + * @description Set the properties of an alert code to the submitted value. + * + * + */ + patch: operations['updateAlertCode'] + trace?: never + } + '/alert-codes/{alertCode}/reactivate': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + delete?: never + options?: never + head?: never + /** + * Reactivate an alert code + * @description Reactivate an alert code, typically from the Alerts UI + * + * + */ + patch: operations['reactivateAlertCode'] + trace?: never + } + '/alert-codes/{alertCode}/deactivate': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + delete?: never + options?: never + head?: never + /** + * Deactivate an alert code + * @description Deactivate an alert code, typically from the Alerts UI + * + * + */ + patch: operations['deactivateAlertCode'] + trace?: never + } + '/bulk-alerts/plan/{id}/status': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Get the status of a plan + * @description + * + * + */ + get: operations['getPlanStatus'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/bulk-alerts/plan/{id}/prisoners': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Get prisoners associated with a plan + * @description + * + * + */ + get: operations['getPlanPrisoners'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/bulk-alerts/plan/{id}/affects': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Get counts associated with a plan + * @description + * + * + */ + get: operations['getPlanAffect'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/alert-types/{alertTypeCode}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Get an alert type + * @description Returns the specified alert type. + * + * Requires one of the following roles: + * * ROLE_PRISONER_ALERTS__RO + * * ROLE_PRISONER_ALERTS__RW + */ + get: operations['retrieveAlertType'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } +} +export type webhooks = Record +export interface components { + schemas: { + /** @description The alert data to use to update an alert in the service */ + UpdateAlert: { + /** + * @description The updated description of the alert. Will be ignored if null and will clear the description if empty. This is a free text field and can be used to provide additional information about the alert e.g. the reasons for adding it.It is limited to 4000 characters. + * @example Alert description + */ + description?: string + /** + * @description The updated user, staff member, approving person or organisation that authorised the alert to be added. Will be ignored if null and will clear the authorised by value if empty. This is a free text field and can be used to record the name of the person who authorised the alert. It is limited to 40 characters. + * @example A. Nurse, An Agency + */ + authorisedBy?: string + /** + * Format: date + * @description The date the alert should be active from. If set to null the field will be ignoredThe active from date can be in the past or the future, but must be before the active to date + * @example 2021-09-27 + */ + activeFrom?: string + /** + * Format: date + * @description The date the alert should be active until. If set to null i.e. cleared, the alert will be active indefinitely. The active to date can be in the past or the future, but must be after the active from date + * @example 2022-07-15 + */ + activeTo?: string + } + ErrorResponse: { + /** Format: int32 */ + status: number + errorCode?: string + userMessage?: string + developerMessage?: string + moreInfo?: string + } + /** @description An alert associated with a person */ + Alert: { + /** + * Format: uuid + * @description The unique identifier assigned to the alert + * @example 8cdadcf3-b003-4116-9956-c99bd8df6a00 + */ + alertUuid: string + /** + * @description The prison number of the person the alert is for. Also referred to as the offender number, offender id or NOMS id. + * @example A1234AA + */ + prisonNumber: string + /** @description The alert code for the alert. A person will only have one alert using each code active at any one time. */ + alertCode: components['schemas']['AlertCodeSummary'] + /** + * @description The description of the alert. It is a free text field and is used to provide additional information about the alert e.g. the reasons for adding it.It is limited to 4000 characters. + * @example Alert description + */ + description?: string + /** + * @description The user, staff member, approving person or organisation that authorised the alert to be added. It is a free text field and is used to record the name of the person who authorised the alert. It is limited to 40 characters. + * @example A. Nurse, An Agency + */ + authorisedBy?: string + /** + * Format: date + * @description The date the alert should be active from. If not provided, the alert will be active from the current date. The active from date can be in the past or the future, but must be before the active to date + * @example 2021-09-27 + */ + activeFrom: string + /** + * Format: date + * @description The date the alert should be active until. If not provided, the alert will be active indefinitely. The active to date can be in the past or the future, but must be after the active from date + * @example 2022-07-15 + */ + activeTo?: string + /** + * @description Indicates that the alert is active for the person. Alerts are active if their active from date is in the past and their active to date is either null or in the future. Note that this field is read only and cannot be set directly using the API. + * @example true + */ + isActive: boolean + /** + * Format: date-time + * @description The date and time the alert was created + * @example 2021-09-27T14:19:25 + */ + createdAt: string + /** + * @description The username of the user who created the alert + * @example USER1234 + */ + createdBy: string + /** + * @description The displayable name of the user who created the alert + * @example Firstname Lastname + */ + createdByDisplayName: string + /** + * Format: date-time + * @description The date and time the alert was last modified + * @example 2022-07-15T15:24:56 + */ + lastModifiedAt?: string + /** + * @description The username of the user who last modified the alert + * @example USER1234 + */ + lastModifiedBy?: string + /** + * @description The displayable name of the user who last modified the alert + * @example Firstname Lastname + */ + lastModifiedByDisplayName?: string + /** + * Format: date-time + * @description The date and time the alert was last set to expire. + * @example 2022-07-15T15:24:56 + */ + activeToLastSetAt?: string + /** + * @description The username of the user who last set the alert to expire. + * @example USER1234 + */ + activeToLastSetBy?: string + /** + * @description The displayable name of the user who last set the alert to expire. + * @example Firstname Lastname + */ + activeToLastSetByDisplayName?: string + } + /** @description The summary information of an alert code used to categorise alerts */ + AlertCodeSummary: { + /** + * @description The short code for the alert type + * @example A + */ + alertTypeCode: string + /** + * @description The description of the alert type + * @example Alert type description + */ + alertTypeDescription: string + /** + * @description The short code for the alert code. Usually starts with the alert type code + * @example ABC + */ + code: string + /** + * @description The description of the alert code + * @example Alert code description + */ + description: string + } + AlertsResponse: { + content: components['schemas']['Alert'][] + } + ResyncAlert: { + /** + * Format: int64 + * @description The internal NOMIS id for the offender booking. An alert in NOMIS is uniquely identified by the offender booking id and alert sequence.This is returned as part of the migrated alert response for mapping between NOMIS and DPS. + * @example 12345 + */ + offenderBookId: number + /** + * Format: int32 + * @description The NOMIS alert sequence. An alert in NOMIS is uniquely identified by the offender booking id and alert sequence.This is returned as part of the migrated alert response for mapping between NOMIS and DPS. + * @example 2 + */ + alertSeq: number + /** + * @description The alert code for the alert. A person should only have one alert using each code active at any one time however this is not enforced during migration. The alert code must exist but can be inactive. + * @example ABC + */ + alertCode: string + /** + * @description The description of the alert. This is a free text field and can be used to provide additional information about the alert e.g. the reasons for adding it.It is limited to 4000 characters when migrating an alert. + * @example Alert description + */ + description?: string + /** + * @description The user, staff member, approving person or organisation that authorised the alert to be added. This is a free text field and can be used to record the name of the person who authorised the alert. It is limited to 40 characters. + * @example A. Nurse, An Agency + */ + authorisedBy?: string + /** + * Format: date + * @description The date the alert should be active from. The active from date can be in the past or the future, but should be on or before the active to date + * @example 2021-09-27 + */ + activeFrom: string + /** + * Format: date + * @description The date the alert should be active until. If not provided, the alert will be active indefinitely. The active to date can be in the past or the future, but should be on or after the active from date + * @example 2022-07-15 + */ + activeTo?: string + /** + * Format: date-time + * @description The date and time the alert was created. + * @example 2022-07-15'H'23:03:01.123456 + */ + createdAt: string + /** + * @description The user id of the person who created the alert. + * @example AB11DZ + */ + createdBy: string + /** + * @description The display name of the person who created the alert. + * @example A Jones + */ + createdByDisplayName: string + /** + * Format: date-time + * @description The date and time the alert was last modified. Only provided if the alert has been modified since creation. + * @example 2022-07-15'H'23:03:01.123456 + */ + lastModifiedAt?: string + /** + * @description The user id of the person who last modified the alert. Required if lastModifiedAt has been supplied. + * @example AB11DZ + */ + lastModifiedBy?: string + /** + * @description The displayable name of the person who last modified the alert. Required if lastModifiedAt has been supplied. + * @example U Dated + */ + lastModifiedByDisplayName?: string + isActive: boolean + } + ResyncedAlert: { + /** + * Format: int64 + * @description The internal NOMIS id for the offender booking. An alert in NOMIS is uniquely identified by the offender booking id and alert sequence.This is returned as part of the resync alert response for mapping between NOMIS and DPS. + * @example 12345 + */ + offenderBookId: number + /** + * Format: int32 + * @description The NOMIS alert sequence. An alert in NOMIS is uniquely identified by the offender booking id and alert sequence.This is returned as part of the resync alert response for mapping between NOMIS and DPS. + * @example 2 + */ + alertSeq: number + /** + * Format: uuid + * @description The unique identifier assigned to the alert + * @example 8cdadcf3-b003-4116-9956-c99bd8df6a00 + */ + alertUuid: string + } + /** @description The alert data to use to create an alert in the service */ + CreateAlert: { + /** + * @description The alert code for the alert. A person can only have one alert using each code active at any one time. The alert code must exist and be active. + * @example ABC + */ + alertCode: string + /** + * @description The description of the alert. This is a free text field and can be used to provide additional information about the alert e.g. the reasons for adding it.It is limited to 4000 characters. + * @example Alert description + */ + description?: string + /** + * @description The user, staff member, approving person or organisation that authorised the alert to be added. This is a free text field and can be used to record the name of the person who authorised the alert. It is limited to 40 characters. + * @example A. Nurse, An Agency + */ + authorisedBy?: string + /** + * Format: date + * @description The date the alert should be active from. If not provided, the alert will be active from the current date. The active from date can be in the past or the future, but must be before the active to date + * @example 2021-09-27 + */ + activeFrom?: string + /** + * Format: date + * @description The date the alert should be active until. If not provided, the alert will be active indefinitely. The active to date can be in the past or the future, but must be after the active from date + * @example 2022-07-15 + */ + activeTo?: string + } + BulkPlan: { + /** Format: uuid */ + id: string + } + /** @description The request body for creating a new alert type */ + CreateAlertTypeRequest: { + /** + * @description The short code for the alert type + * @example A + */ + code: string + /** + * @description The description of the alert type + * @example Alert type description + */ + description: string + } + /** @description An alert code used to categorise alerts */ + AlertCode: { + /** + * @description The short code for the alert type + * @example A + */ + alertTypeCode: string + /** + * @description The short code for the alert code. Usually starts with the alert type code + * @example ABC + */ + code: string + /** + * @description The description of the alert code + * @example Alert code description + */ + description: string + /** + * Format: int32 + * @description The sequence number of the alert code within the alert type. Used for ordering alert codes correctly in lists and drop downs. A value of 0 indicates this is the default alert code for the alert type + * @example 3 + */ + listSequence: number + /** + * @description Indicates that the alert code is active and can be used. Inactive alert codes are not returned by default in the API + * @example true + */ + isActive: boolean + /** + * Format: date-time + * @description The date and time the alert code was created + * @example 2021-09-27T14:19:25 + */ + createdAt: string + /** + * @description The username of the user who created the alert code + * @example USER1234 + */ + createdBy: string + /** + * Format: date-time + * @description The date and time the alert code was last modified + * @example 2022-07-15T15:24:56 + */ + modifiedAt?: string + /** + * @description The username of the user who last modified the alert code + * @example USER1234 + */ + modifiedBy?: string + /** + * Format: date-time + * @description The date and time the alert code was deactivated + * @example 2023-11-08T09:53:34 + */ + deactivatedAt?: string + /** + * @description The username of the user who deactivated the alert code + * @example USER1234 + */ + deactivatedBy?: string + } + /** @description An alert type used to categorise alerts */ + AlertType: { + /** + * @description The short code for the alert type + * @example A + */ + code: string + /** + * @description The description of the alert type + * @example Alert type description + */ + description: string + /** + * Format: int32 + * @description The sequence number of the alert type. Used for ordering alert types correctly in lists and drop downs. A value of 0 indicates this is the default alert type + * @example 3 + */ + listSequence: number + /** + * @description Indicates that the alert type is active and can be used. Inactive alert types are not returned by default in the API + * @example true + */ + isActive: boolean + /** + * Format: date-time + * @description The date and time the alert type was created + * @example 2021-09-27T14:19:25 + */ + createdAt: string + /** + * @description The username of the user who created the alert type + * @example USER1234 + */ + createdBy: string + /** + * Format: date-time + * @description The date and time the alert type was last modified + * @example 2022-07-15T15:24:56 + */ + modifiedAt?: string + /** + * @description The username of the user who last modified the alert type + * @example USER1234 + */ + modifiedBy?: string + /** + * Format: date-time + * @description The date and time the alert type was deactivated + * @example 2023-11-08T09:53:34 + */ + deactivatedAt?: string + /** + * @description The username of the user who deactivated the alert type + * @example USER1234 + */ + deactivatedBy?: string + /** @description The alert codes associated with this alert type */ + alertCodes: components['schemas']['AlertCode'][] + } + /** @description The request body for creating a new alert code */ + CreateAlertCodeRequest: { + /** + * @description The short code for the alert code + * @example A + */ + code: string + /** + * @description The description of the alert code + * @example Alert code description + */ + description: string + /** + * @description The short code for the parent type + * @example A + */ + parent: string + } + /** @description The request body for updating the properties of an alert type */ + UpdateAlertTypeRequest: { + /** + * @description The new property value(s) to be updated onto an alert type + * @example New description value for an alert type + */ + description: string + } + /** @description The request body for updating the properties of an alert code */ + UpdateAlertCodeRequest: { + /** + * @description The new property value(s) to be updated onto an alert code + * @example New description value for an alert code + */ + description: string + } + PageAlert: { + /** Format: int64 */ + totalElements?: number + /** Format: int32 */ + totalPages?: number + first?: boolean + last?: boolean + /** Format: int32 */ + size?: number + content?: components['schemas']['Alert'][] + /** Format: int32 */ + number?: number + sort?: components['schemas']['Sortnull'] + /** Format: int32 */ + numberOfElements?: number + pageable?: components['schemas']['Pageablenull'] + empty?: boolean + } + Pageablenull: { + /** Format: int64 */ + offset?: number + sort?: components['schemas']['Sortnull'] + /** Format: int32 */ + pageSize?: number + paged?: boolean + /** Format: int32 */ + pageNumber?: number + unpaged?: boolean + } + Sortnull: { + empty?: boolean + sorted?: boolean + unsorted?: boolean + } + BulkPlanCounts: { + /** Format: int32 */ + existingAlerts: number + /** Format: int32 */ + created: number + /** Format: int32 */ + updated: number + /** Format: int32 */ + expired: number + } + BulkPlanStatus: { + /** Format: date-time */ + createdAt?: string + createdBy?: string + createdByDisplayName?: string + /** Format: date-time */ + startedAt?: string + startedBy?: string + startedByDisplayName?: string + /** Format: date-time */ + completedAt?: string + counts?: components['schemas']['BulkPlanCounts'] + } + BulkPlanPrisoners: { + prisoners: components['schemas']['PrisonerSummary'][] + } + PrisonerSummary: { + prisonNumber: string + firstName: string + lastName: string + prisonCode?: string + cellLocation?: string + } + BulkPlanAffect: { + counts: components['schemas']['BulkPlanCounts'] + } + } + responses: never + parameters: never + requestBodies: never + headers: never + pathItems: never +} +export type $defs = Record +export interface operations { + retrieveAlert: { + parameters: { + query?: never + header?: never + path: { + /** @description Alert unique identifier */ + alertUuid: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Alert found */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Alert'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description The alert associated with this identifier was not found. */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + updateAlert: { + parameters: { + query?: never + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + /** @description The source of the request. Will default to 'DPS' if not suppliedThis value will be assigned to the additionalInformation.source property in published domain events. A source value of 'NOMIS' will allow any username value that is less than 32 characters to be supplied. If this username is not found, its value will be used for the user display name property. A source value of 'NOMIS' will also allow no username value to be supplied and will use 'NOMIS' for both the username and display name properties. */ + Source?: string + } + path: { + /** @description Alert unique identifier */ + alertUuid: string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateAlert'] + } + } + responses: { + /** @description Alert updated successfully */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Alert'] + } + } + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + deleteAlert: { + parameters: { + query?: never + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + /** @description The source of the request. Will default to 'DPS' if not suppliedThis value will be assigned to the additionalInformation.source property in published domain events. A source value of 'NOMIS' will allow any username value that is less than 32 characters to be supplied. If this username is not found, its value will be used for the user display name property. A source value of 'NOMIS' will also allow no username value to be supplied and will use 'NOMIS' for both the username and display name properties. */ + Source?: string + } + path: { + /** @description Alert unique identifier */ + alertUuid: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Alert deleted */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Alert was not found or already deleted */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + retrievePrisonerAlerts: { + parameters: { + query?: { + includeInactive?: boolean + } + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': string[] + } + } + responses: { + /** @description Alerts found */ + 200: { + headers: { + [name: string]: unknown + } + content: { + '*/*': components['schemas']['AlertsResponse'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + '*/*': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + '*/*': components['schemas']['ErrorResponse'] + } + } + } + } + resyncPrisonerAlerts: { + parameters: { + query?: never + header?: never + path: { + /** + * @description Unique identifier of the prisoner. Aliases: offender number, prisoner number, offender id or NOMS id + * @example A1234AA + */ + prisonNumber: string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['ResyncAlert'][] + } + } + responses: { + /** @description Resync of alerts successful */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ResyncedAlert'][] + } + } + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + retrievePrisonerAlerts_1: { + parameters: { + query?: { + /** + * @description Return only active (true) or inactive (false) alerts. If not provided or a null value is supplied, all alerts are returned + * @example true + */ + isActive?: boolean + /** + * @description Filter by alert type code or codes. Supply a comma separated list of alert type codes to filter by more than one code + * @example M + */ + alertType?: string + /** + * @description Filter by alert code or codes. Supply a comma separated list of alert codes to filter by more than one code + * @example AS + */ + alertCode?: string + /** + * @description Filter alerts that have an active on date or after the supplied date + * @example 2023-09-27 + */ + activeFromStart?: string + /** + * @description Filter alerts that have an active on date up to or before the supplied date + * @example 2021-11-15 + */ + activeFromEnd?: string + /** + * @description Filter alerts that contain the search text in their description or authorised by. The search is case insensitive and will match any part of the description or authorised by text + * @example Search text + */ + search?: string + /** @description Zero-based page index (0..N) */ + page?: unknown + /** @description The size of the page to be returned */ + size?: unknown + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: unknown[] + } + header?: never + path: { + /** + * @description Prison number of the prisoner. Also referred to as the offender number, offender id or NOMS id + * @example A1234AA + */ + prisonNumber: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Alerts found */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['PageAlert'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + createPrisonerAlert: { + parameters: { + query?: { + /** @description Allows the creation of an alert using an inactive code. Intended only for use by the Alerts UI when the user has the ‘Manage Alerts in Bulk for Prison Estate’ role. Defaults to false */ + allowInactiveCode?: boolean + } + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + /** @description The source of the request. Will default to 'DPS' if not suppliedThis value will be assigned to the additionalInformation.source property in published domain events. A source value of 'NOMIS' will allow any username value that is less than 32 characters to be supplied. If this username is not found, its value will be used for the user display name property. A source value of 'NOMIS' will also allow no username value to be supplied and will use 'NOMIS' for both the username and display name properties. */ + Source?: string + } + path: { + /** + * @description Prison number of the prisoner. Also referred to as the offender number, offender id or NOMS id + * @example A1234AA + */ + prisonNumber: string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['CreateAlert'] + } + } + responses: { + /** @description Alert created successfully */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Alert'] + } + } + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Conflict, the person already has an active alert using the supplied alert code */ + 409: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + createPlan: { + parameters: { + query?: never + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + /** @description The source of the request. Will default to 'DPS' if not suppliedThis value will be assigned to the additionalInformation.source property in published domain events. A source value of 'NOMIS' will allow any username value that is less than 32 characters to be supplied. If this username is not found, its value will be used for the user display name property. A source value of 'NOMIS' will also allow no username value to be supplied and will use 'NOMIS' for both the username and display name properties. */ + Source?: string + } + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Alerts creation plan generated successfully */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['BulkPlan'] + } + } + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + startPlan: { + parameters: { + query?: never + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + /** @description The source of the request. Will default to 'DPS' if not suppliedThis value will be assigned to the additionalInformation.source property in published domain events. A source value of 'NOMIS' will allow any username value that is less than 32 characters to be supplied. If this username is not found, its value will be used for the user display name property. A source value of 'NOMIS' will also allow no username value to be supplied and will use 'NOMIS' for both the username and display name properties. */ + Source?: string + } + path: { + id: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Start plan accepted - will run asynchronously */ + 202: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + retrieveAlertTypes: { + parameters: { + query?: { + /** @description Include inactive alert types and codes. Defaults to false */ + includeInactive?: boolean + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Alert types and codes found */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AlertType'][] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + createAlertType: { + parameters: { + query?: never + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + } + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['CreateAlertTypeRequest'] + } + } + responses: { + /** @description Alert type created */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AlertType'][] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Conflict, the alert type code already exists */ + 409: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + retrieveAlertCodes: { + parameters: { + query?: { + /** @description Include inactive alert types and codes. Defaults to false */ + includeInactive?: boolean + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Alert code retrieved */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AlertCode'][] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + createAlertCode: { + parameters: { + query?: never + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + } + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['CreateAlertCodeRequest'] + } + } + responses: { + /** @description Alert code created */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AlertCode'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Not found, the parent alert type has not been found */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Conflict, the alert code already exists */ + 409: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + updatePlan: { + parameters: { + query?: never + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + /** @description The source of the request. Will default to 'DPS' if not suppliedThis value will be assigned to the additionalInformation.source property in published domain events. A source value of 'NOMIS' will allow any username value that is less than 32 characters to be supplied. If this username is not found, its value will be used for the user display name property. A source value of 'NOMIS' will also allow no username value to be supplied and will use 'NOMIS' for both the username and display name properties. */ + Source?: string + } + path: { + id: string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': unknown + } + } + responses: { + /** @description Alerts creation plan generated successfully */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['BulkPlan'] + } + } + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + updateAlertType: { + parameters: { + query?: never + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + } + path: { + alertType: string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateAlertTypeRequest'] + } + } + responses: { + /** @description Alert type updated */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AlertType'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Not found, the alert type was is not found */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + reactivateAlertType: { + parameters: { + query?: never + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + } + path: { + alertType: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Alert type reactivated */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AlertType'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Not found, the alert type was is not found */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + deactivateAlertType: { + parameters: { + query?: never + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + } + path: { + alertType: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Alert type deactivated */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AlertType'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Not found, the alert type was is not found */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + retrieveAlertCode: { + parameters: { + query?: never + header?: never + path: { + alertCode: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Alert code retrieved */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AlertCode'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Not found, the alert code was is not found */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + updateAlertCode: { + parameters: { + query?: never + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + } + path: { + alertCode: string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateAlertCodeRequest'] + } + } + responses: { + /** @description Alert code updated */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AlertCode'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Not found, the alert code was is not found */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + reactivateAlertCode: { + parameters: { + query?: never + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + } + path: { + alertCode: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Alert code reactivated */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AlertCode'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Not found, the alert code was is not found */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + deactivateAlertCode: { + parameters: { + query?: never + header?: { + /** @description The username of the user interacting with the client service. This can be used instead of the `user_name` or `username` token claim when the client service is acting on behalf of a user. The value passed in the username header will only be used if a `user_name` or `username` token claim is not present. */ + Username?: string + } + path: { + alertCode: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Alert code deactivated */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AlertCode'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Not found, the alert code was is not found */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + getPlanStatus: { + parameters: { + query?: never + header?: never + path: { + id: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully retrieved the status of a plan */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['BulkPlanStatus'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description No plan found with the provided identifier */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + getPlanPrisoners: { + parameters: { + query?: never + header?: never + path: { + id: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully retrieved prisoners associated with a plan */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['BulkPlanPrisoners'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description No plan found with the provided identifier */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + getPlanAffect: { + parameters: { + query?: never + header?: never + path: { + id: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully retrieved counts of affect of plan */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['BulkPlanAffect'] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description No plan found with the provided identifier */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + retrieveAlertType: { + parameters: { + query?: never + header?: never + path: { + alertTypeCode: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Alert type found */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AlertType'][] + } + } + /** @description Unauthorised, requires a valid Oauth2 token */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden, requires an appropriate role */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Not found, the alert type was is not found */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } +} diff --git a/server/@types/alertsAPI/types.ts b/server/@types/alertsAPI/types.ts new file mode 100644 index 000000000..b3c7542d6 --- /dev/null +++ b/server/@types/alertsAPI/types.ts @@ -0,0 +1,3 @@ +import { components } from '.' + +export type Alert = components['schemas']['Alert'] diff --git a/server/config.ts b/server/config.ts index cd1213af4..a13e121a3 100755 --- a/server/config.ts +++ b/server/config.ts @@ -169,6 +169,14 @@ export default { }, agent: new AgentConfig(Number(get('NON_ASSOCIATIONS_API_TIMEOUT_RESPONSE', 30000))), }, + alertsApi: { + url: get('ALERTS_API_URL', 'http://localhost:8097', requiredInProduction), + timeout: { + response: Number(get('ALERTS_API_TIMEOUT_RESPONSE', 30000)), + deadline: Number(get('ALERTS_API_TIMEOUT_DEADLINE', 30000)), + }, + agent: new AgentConfig(Number(get('ALERTS_API_TIMEOUT_RESPONSE', 30000))), + }, locationsInsidePrisonApi: { url: get('LOCATIONS_INSIDE_PRISON_API_URL', 'http://localhost:8082', requiredInProduction), timeout: { diff --git a/server/data/alertsApiClient.test.ts b/server/data/alertsApiClient.test.ts new file mode 100644 index 000000000..c2ddf530d --- /dev/null +++ b/server/data/alertsApiClient.test.ts @@ -0,0 +1,43 @@ +import nock from 'nock' +import config from '../config' +import TokenStore from './tokenStore' +import AlertsApiClient from './alertsApiClient' +import { ServiceUser } from '../@types/express' + +const user = {} as ServiceUser + +jest.mock('./tokenStore') + +describe('alertsApiClient', () => { + let fakeAlertsApi: nock.Scope + let alertsApiClient: AlertsApiClient + + beforeEach(() => { + fakeAlertsApi = nock(config.apis.alertsApi.url) + alertsApiClient = new AlertsApiClient() + + jest.spyOn(TokenStore.prototype, 'getToken').mockResolvedValue('accessToken') + }) + + afterEach(() => { + jest.resetAllMocks() + nock.cleanAll() + }) + + describe('getAlertsForPrisoners', () => { + it('returns data from api', async () => { + const response = { data: 'some data' } + + fakeAlertsApi + .post('/search/alerts/prison-numbers') + .query({ includeInactive: false }) + .matchHeader('authorization', `Bearer accessToken`) + .reply(200, response) + + const output = await alertsApiClient.getAlertsForPrisoners(['G4793VF', 'G0113GG'], user) + + expect(output).toEqual(response) + expect(nock.isDone()).toBe(true) + }) + }) +}) diff --git a/server/data/alertsApiClient.ts b/server/data/alertsApiClient.ts new file mode 100644 index 000000000..f6d7d7870 --- /dev/null +++ b/server/data/alertsApiClient.ts @@ -0,0 +1,21 @@ +import AbstractHmppsRestClient from './abstractHmppsRestClient' +import config, { ApiConfig } from '../config' +import { ServiceUser } from '../@types/express' +import { Alert } from '../@types/alertsAPI/types' + +export default class AlertsApiClient extends AbstractHmppsRestClient { + constructor() { + super('Alerts API', config.apis.alertsApi as ApiConfig) + } + + async getAlertsForPrisoners(prisonerNumbers: string[], user: ServiceUser): Promise<{ content: Alert[] }> { + return this.post( + { + path: `/search/alerts/prison-numbers`, + query: { includeInactive: false }, + data: prisonerNumbers, + }, + user, + ) + } +} diff --git a/server/data/index.test.ts b/server/data/index.test.ts index 62193d0be..675b67ac2 100644 --- a/server/data/index.test.ts +++ b/server/data/index.test.ts @@ -9,13 +9,14 @@ import FrontendComponentApiClient from './frontendComponentApiClient' import CaseNotesApiClient from './caseNotesApiClient' import BookAVideoLinkApiClient from './bookAVideoLinkApiClient' import NonAssociationsApiClient from './nonAssociationsApiClient' +import AlertsApiClient from './alertsApiClient' import LocationsInsidePrisonApiClient from './locationsInsidePrisonApiClient' import NomisMappingClient from './nomisMappingClient' describe('DataAccess', () => { test('The correct rest clients are instantiated', () => { const clients = dataAccess() - expect(Object.values(clients).length).toBe(13) + expect(Object.values(clients).length).toBe(14) expect(clients.manageUsersApiClient).toBeInstanceOf(ManageUsersApiClient) expect(clients.caseNotesApiClient).toBeInstanceOf(CaseNotesApiClient) expect(clients.prisonApiClient).toBeInstanceOf(PrisonApiClient) @@ -26,6 +27,7 @@ describe('DataAccess', () => { expect(clients.bookAVideoLinkApiClient).toBeInstanceOf(BookAVideoLinkApiClient) expect(clients.frontendComponentApiClient).toBeInstanceOf(FrontendComponentApiClient) expect(clients.nonAssociationsApiClient).toBeInstanceOf(NonAssociationsApiClient) + expect(clients.alertsApiClient).toBeInstanceOf(AlertsApiClient) expect(clients.locationsInsidePrisonApiClient).toBeInstanceOf(LocationsInsidePrisonApiClient) expect(clients.nomisMappingClient).toBeInstanceOf(NomisMappingClient) }) diff --git a/server/data/index.ts b/server/data/index.ts index 2d3c1a0c7..f825e58a2 100644 --- a/server/data/index.ts +++ b/server/data/index.ts @@ -18,6 +18,7 @@ import FrontendComponentApiClient from './frontendComponentApiClient' import CaseNotesApiClient from './caseNotesApiClient' import BookAVideoLinkApiClient from './bookAVideoLinkApiClient' import NonAssociationsApiClient from './nonAssociationsApiClient' +import AlertsApiClient from './alertsApiClient' import LocationsInsidePrisonApiClient from './locationsInsidePrisonApiClient' import NomisMappingClient from './nomisMappingClient' @@ -34,6 +35,7 @@ export default function dataAccess() { bookAVideoLinkApiClient: new BookAVideoLinkApiClient(), applicationInsightsClient: appInsightsClient, nonAssociationsApiClient: new NonAssociationsApiClient(), + alertsApiClient: new AlertsApiClient(), locationsInsidePrisonApiClient: new LocationsInsidePrisonApiClient(), nomisMappingClient: new NomisMappingClient(), } diff --git a/server/data/prisonApiClient.ts b/server/data/prisonApiClient.ts index 72547bcd7..7463a8be8 100644 --- a/server/data/prisonApiClient.ts +++ b/server/data/prisonApiClient.ts @@ -2,7 +2,7 @@ import { Readable } from 'stream' import config, { ApiConfig } from '../config' import AbstractHmppsRestClient from './abstractHmppsRestClient' -import { ReferenceCode, AgencyPrisonerPayProfile, Alert } from '../@types/prisonApiImport/types' +import { ReferenceCode, AgencyPrisonerPayProfile } from '../@types/prisonApiImport/types' import { ServiceUser } from '../@types/express' import { LocationLenient } from '../@types/prisonApiImportCustom' @@ -31,15 +31,6 @@ export default class PrisonApiClient extends AbstractHmppsRestClient { }) } - // TODO: Move to prisonSearchApiClient once alerts endpoint is available - async getPrisonersAlerts(offenderNumbers: string[], prisonCode: string, user: ServiceUser): Promise { - return this.post({ - path: `/api/bookings/offenderNo/${prisonCode}/alerts`, - data: offenderNumbers, - authToken: user.token, - }) - } - async getInternalLocationByKey(key: string, user: ServiceUser): Promise { return this.get({ path: `/api/locations/code/${key}`, authToken: user.token }) } diff --git a/server/routes/appointments/create-and-edit/createRoutes.ts b/server/routes/appointments/create-and-edit/createRoutes.ts index 7b7cff290..ff604dfab 100644 --- a/server/routes/appointments/create-and-edit/createRoutes.ts +++ b/server/routes/appointments/create-and-edit/createRoutes.ts @@ -35,7 +35,6 @@ import CopySeries, { HowToCopySeriesForm } from './handlers/copySeries' import NoAttendees from './handlers/noAttendees' import ReviewPrisonersAlertsRoutes from './handlers/reviewPrisonersAlerts' import ReviewNonAssociationsRoutes from './handlers/reviewNonAssociations' -import PrisonerAlertsService from '../../../services/prisonerAlertsService' import fetchAppointmentSeries from '../../../middleware/appointments/fetchAppointmentSeries' import AppointeeAttendeeService from '../../../services/appointeeAttendeeService' import ConfirmNonAssociationRoutes from './handlers/confirmNonAssociations' @@ -45,6 +44,7 @@ export default function Create({ activitiesService, metricsService, nonAssociationsService, + alertsService, }: Services): Router { const router = Router({ mergeParams: true }) @@ -54,7 +54,6 @@ export default function Create({ router.post(path, validationMiddleware(type), asyncMiddleware(handler)) const editAppointmentService = new EditAppointmentService(activitiesService, metricsService) - const prisonerAlertsService = new PrisonerAlertsService(prisonService) const appointeeAttendeeService = new AppointeeAttendeeService(prisonService) const startJourneyRoutes = new StartJourneyRoutes(prisonService, metricsService, appointeeAttendeeService) const selectPrisonerRoutes = new SelectPrisonerRoutes(prisonService) @@ -72,12 +71,12 @@ export default function Create({ const checkAnswersRoutes = new CheckAnswersRoutes(activitiesService) const confirmationRoutes = new ConfirmationRoutes(metricsService) const howToAddPrisonerRoutes = new HowToAddPrisonerRoutes() - const reviewPrisonerRoutes = new ReviewPrisonerRoutes(metricsService, prisonerAlertsService) + const reviewPrisonerRoutes = new ReviewPrisonerRoutes(metricsService, alertsService) const appointmentSetUploadRoutes = new AppointmentSetUploadRoutes(new PrisonerListCsvParser(), prisonService) const appointmentSetDateRoutes = new AppointmentSetDateRoutes() const appointmentSetTimesRoutes = new AppointmentSetTimesRoutes() const scheduleRoutes = new ScheduleRoutes(activitiesService, editAppointmentService, metricsService) - const reviewPrisonerAlerts = new ReviewPrisonersAlertsRoutes(prisonerAlertsService) + const reviewPrisonerAlerts = new ReviewPrisonersAlertsRoutes(alertsService) const reviewNonAssociations = new ReviewNonAssociationsRoutes(nonAssociationsService, prisonService) const confirmNonAssociations = new ConfirmNonAssociationRoutes(nonAssociationsService) const copySeriesRoutes = new CopySeries() diff --git a/server/routes/appointments/create-and-edit/editRoutes.ts b/server/routes/appointments/create-and-edit/editRoutes.ts index 44a3d8dfd..03e6fba13 100644 --- a/server/routes/appointments/create-and-edit/editRoutes.ts +++ b/server/routes/appointments/create-and-edit/editRoutes.ts @@ -23,7 +23,6 @@ import CancellationReasonRoutes, { CancellationReason } from './handlers/cancell import TierRoutes, { TierForm } from './handlers/tier' import HostRoutes, { HostForm } from './handlers/host' import ReviewPrisonersAlertsRoutes from './handlers/reviewPrisonersAlerts' -import PrisonerAlertsService from '../../../services/prisonerAlertsService' import AppointeeAttendeeService from '../../../services/appointeeAttendeeService' import UncancelRoutes from './handlers/uncancel' import ReviewNonAssociationRoutes from './handlers/reviewNonAssociations' @@ -33,6 +32,7 @@ export default function Edit({ activitiesService, metricsService, nonAssociationsService, + alertsService, }: Services): Router { const router = Router({ mergeParams: true }) @@ -44,7 +44,6 @@ export default function Edit({ const editAppointmentService = new EditAppointmentService(activitiesService, metricsService) const appointeeAttendeeService = new AppointeeAttendeeService(prisonService) const startJourneyRoutes = new StartJourneyRoutes(prisonService, metricsService, appointeeAttendeeService) - const prisonerAlertsService = new PrisonerAlertsService(prisonService) const tierRoutes = new TierRoutes(editAppointmentService) const hostRoutes = new HostRoutes(editAppointmentService) const locationRoutes = new LocationRoutes(activitiesService, editAppointmentService) @@ -124,8 +123,8 @@ export default function Edit({ const howToAddPrisoners = new HowToAddPrisoners() const selectPrisonerHandler = new SelectPrisonerRoutes(prisonService) const uploadPrisonerListRoutes = new UploadPrisonerListRoutes(new PrisonerListCsvParser(), prisonService) - const reviewPrisoners = new ReviewPrisoners(metricsService, prisonerAlertsService) - const reviewPrisonerAlerts = new ReviewPrisonersAlertsRoutes(prisonerAlertsService) + const reviewPrisoners = new ReviewPrisoners(metricsService, alertsService) + const reviewPrisonerAlerts = new ReviewPrisonersAlertsRoutes(alertsService) router.get( '/start/prisoners/add', diff --git a/server/routes/appointments/create-and-edit/handlers/reviewPrisoners.test.ts b/server/routes/appointments/create-and-edit/handlers/reviewPrisoners.test.ts index 5b9a0d7a7..bf4ec01c9 100644 --- a/server/routes/appointments/create-and-edit/handlers/reviewPrisoners.test.ts +++ b/server/routes/appointments/create-and-edit/handlers/reviewPrisoners.test.ts @@ -4,17 +4,17 @@ import ReviewPrisoners from './reviewPrisoners' import { AppointmentJourneyMode, AppointmentType } from '../appointmentJourney' import MetricsService from '../../../../services/metricsService' import MetricsEvent from '../../../../data/metricsEvent' -import PrisonerAlertsService, { PrisonerAlertResults } from '../../../../services/prisonerAlertsService' import { AppointmentPrisonerDetails } from '../appointmentPrisonerDetails' +import AlertsService, { PrisonerAlertResults } from '../../../../services/alertsService' jest.mock('../../../../services/metricsService') -jest.mock('../../../../services/prisonerAlertsService') +jest.mock('../../../../services/alertsService') const metricsService = new MetricsService(null) as jest.Mocked -const prisonerAlertsService = new PrisonerAlertsService(null) as jest.Mocked +const alertsService = new AlertsService(null) as jest.Mocked describe('Route Handlers - Create Appointment - Review Prisoners', () => { - const handler = new ReviewPrisoners(metricsService, prisonerAlertsService) + const handler = new ReviewPrisoners(metricsService, alertsService) let req: Request let res: Response const appointmentId = '2' @@ -185,8 +185,8 @@ describe('Route Handlers - Create Appointment - Review Prisoners', () => { async ({ mode }) => { req.session.appointmentJourney.mode = mode - when(prisonerAlertsService.getAlertDetails) - .calledWith(req.session.appointmentJourney.prisoners, res.locals.user.activeCaseLoadId, res.locals.user) + when(alertsService.getAlertDetails) + .calledWith(req.session.appointmentJourney.prisoners, res.locals.user) .mockReturnValue(Promise.resolve({ numPrisonersWithAlerts: 1 } as PrisonerAlertResults)) await handler.POST(req, res) @@ -195,8 +195,8 @@ describe('Route Handlers - Create Appointment - Review Prisoners', () => { ) it('should redirect or return to review alerts page there are no alerts when mode is CREATE', async () => { - when(prisonerAlertsService.getAlertDetails) - .calledWith(req.session.appointmentJourney.prisoners, res.locals.user.activeCaseLoadId, res.locals.user) + when(alertsService.getAlertDetails) + .calledWith(req.session.appointmentJourney.prisoners, res.locals.user) .mockReturnValue(Promise.resolve({ numPrisonersWithAlerts: 0 } as PrisonerAlertResults)) await handler.POST(req, res) @@ -206,8 +206,8 @@ describe('Route Handlers - Create Appointment - Review Prisoners', () => { it('should redirect or return to name page when there are no alerts when mode is COPY', async () => { req.session.appointmentJourney.mode = AppointmentJourneyMode.COPY - when(prisonerAlertsService.getAlertDetails) - .calledWith(req.session.appointmentJourney.prisoners, res.locals.user.activeCaseLoadId, res.locals.user) + when(alertsService.getAlertDetails) + .calledWith(req.session.appointmentJourney.prisoners, res.locals.user) .mockReturnValue(Promise.resolve({ numPrisonersWithAlerts: 0 } as PrisonerAlertResults)) await handler.POST(req, res) @@ -245,8 +245,8 @@ describe('Route Handlers - Create Appointment - Review Prisoners', () => { }) it('should redirect or return to review alerts page if there are any alerts', async () => { - when(prisonerAlertsService.getAlertDetails) - .calledWith(prisoners, res.locals.user.activeCaseLoadId, res.locals.user) + when(alertsService.getAlertDetails) + .calledWith(prisoners, res.locals.user) .mockReturnValue(Promise.resolve({ numPrisonersWithAlerts: 1 } as PrisonerAlertResults)) await handler.EDIT(req, res) @@ -255,8 +255,8 @@ describe('Route Handlers - Create Appointment - Review Prisoners', () => { }) it('should redirect to the alerts page if there are no alerts', async () => { - when(prisonerAlertsService.getAlertDetails) - .calledWith(prisoners, res.locals.user.activeCaseLoadId, res.locals.user) + when(alertsService.getAlertDetails) + .calledWith(prisoners, res.locals.user) .mockReturnValue(Promise.resolve({ numPrisonersWithAlerts: 0 } as PrisonerAlertResults)) await handler.EDIT(req, res) diff --git a/server/routes/appointments/create-and-edit/handlers/reviewPrisoners.ts b/server/routes/appointments/create-and-edit/handlers/reviewPrisoners.ts index ef070fb36..d6a509a20 100644 --- a/server/routes/appointments/create-and-edit/handlers/reviewPrisoners.ts +++ b/server/routes/appointments/create-and-edit/handlers/reviewPrisoners.ts @@ -3,13 +3,13 @@ import { AppointmentJourneyMode, AppointmentType } from '../appointmentJourney' import config from '../../../../config' import MetricsService from '../../../../services/metricsService' import MetricsEvent from '../../../../data/metricsEvent' -import PrisonerAlertsService from '../../../../services/prisonerAlertsService' import { AppointmentPrisonerDetails } from '../appointmentPrisonerDetails' +import AlertsService from '../../../../services/alertsService' export default class ReviewPrisonerRoutes { constructor( private readonly metricsService: MetricsService, - private readonly prisonerAlertsService: PrisonerAlertsService, + private readonly alertsService: AlertsService, ) {} GET = async (req: Request, res: Response): Promise => { @@ -60,15 +60,10 @@ export default class ReviewPrisonerRoutes { } private async getNextPageInJourney(req: Request, res: Response): Promise { - const prisonCode = res.locals.user.activeCaseLoadId const prisoners = ReviewPrisonerRoutes.getPrisoners(req) - const prisonerAlertsDetails = await this.prisonerAlertsService.getAlertDetails( - prisoners, - prisonCode, - res.locals.user, - ) + const alertsDetails = await this.alertsService.getAlertDetails(prisoners, res.locals.user) - if (prisonerAlertsDetails.numPrisonersWithAlerts === 0) { + if (alertsDetails.numPrisonersWithAlerts === 0) { if (req.session.appointmentJourney.mode === AppointmentJourneyMode.EDIT) { return '../../schedule' } diff --git a/server/routes/appointments/create-and-edit/handlers/reviewPrisonersAlerts.test.ts b/server/routes/appointments/create-and-edit/handlers/reviewPrisonersAlerts.test.ts index 1a298b79c..2e7185e2a 100644 --- a/server/routes/appointments/create-and-edit/handlers/reviewPrisonersAlerts.test.ts +++ b/server/routes/appointments/create-and-edit/handlers/reviewPrisonersAlerts.test.ts @@ -2,14 +2,14 @@ import { Request, Response } from 'express' import { when } from 'jest-when' import { AppointmentJourneyMode, AppointmentType } from '../appointmentJourney' import ReviewPrisonersAlertsRoutes from './reviewPrisonersAlerts' -import PrisonerAlertsService, { PrisonerAlertResults } from '../../../../services/prisonerAlertsService' +import AlertsService, { PrisonerAlertResults } from '../../../../services/alertsService' -jest.mock('../../../../services/prisonerAlertsService') +jest.mock('../../../../services/alertsService') -const prisonerAlertsService = new PrisonerAlertsService(null) as jest.Mocked +const alertsService = new AlertsService(null) as jest.Mocked describe('Route Handlers - Create Appointment - Review Prisoners Alerts', () => { - const handler = new ReviewPrisonersAlertsRoutes(prisonerAlertsService) + const handler = new ReviewPrisonersAlertsRoutes(alertsService) let req: Request let res: Response @@ -51,12 +51,12 @@ describe('Route Handlers - Create Appointment - Review Prisoners Alerts', () => }) describe('GET', () => { - const prisonerAlertsDetails = {} as PrisonerAlertResults + const alertsDetails = {} as PrisonerAlertResults beforeEach(() => { - when(prisonerAlertsService.getAlertDetails) - .calledWith(req.session.appointmentJourney.prisoners, res.locals.user.activeCaseLoadId, res.locals.user) - .mockReturnValue(Promise.resolve(prisonerAlertsDetails)) + when(alertsService.getAlertDetails) + .calledWith(req.session.appointmentJourney.prisoners, res.locals.user) + .mockReturnValue(Promise.resolve(alertsDetails)) }) it('should render the view for create appointment', async () => { @@ -66,7 +66,7 @@ describe('Route Handlers - Create Appointment - Review Prisoners Alerts', () => appointmentId, backLinkHref: 'how-to-add-prisoners', preserveHistory, - prisonerAlertsDetails, + alertsDetails, }) }) @@ -79,7 +79,7 @@ describe('Route Handlers - Create Appointment - Review Prisoners Alerts', () => appointmentId, backLinkHref: 'upload-appointment-set', preserveHistory, - prisonerAlertsDetails, + alertsDetails, }) }) @@ -92,7 +92,7 @@ describe('Route Handlers - Create Appointment - Review Prisoners Alerts', () => appointmentId, backLinkHref: 'how-to-add-prisoners', preserveHistory, - prisonerAlertsDetails, + alertsDetails, }) }) @@ -105,7 +105,7 @@ describe('Route Handlers - Create Appointment - Review Prisoners Alerts', () => appointmentId, backLinkHref: 'https://digital-dev.prison.service.justice.gov.uk/prisoner/A1234BC', preserveHistory, - prisonerAlertsDetails, + alertsDetails, }) }) @@ -118,7 +118,7 @@ describe('Route Handlers - Create Appointment - Review Prisoners Alerts', () => appointmentId, backLinkHref: 'how-to-add-prisoners', preserveHistory: 'true', - prisonerAlertsDetails, + alertsDetails, }) }) }) diff --git a/server/routes/appointments/create-and-edit/handlers/reviewPrisonersAlerts.ts b/server/routes/appointments/create-and-edit/handlers/reviewPrisonersAlerts.ts index 6ddf6d880..f1bc86b53 100644 --- a/server/routes/appointments/create-and-edit/handlers/reviewPrisonersAlerts.ts +++ b/server/routes/appointments/create-and-edit/handlers/reviewPrisonersAlerts.ts @@ -1,10 +1,10 @@ import { Request, Response } from 'express' import { AppointmentJourneyMode, AppointmentType } from '../appointmentJourney' import config from '../../../../config' -import PrisonerAlertsService from '../../../../services/prisonerAlertsService' +import AlertsService from '../../../../services/alertsService' export default class ReviewPrisonersAlertsRoutes { - constructor(private readonly prisonerAlertsService: PrisonerAlertsService) {} + constructor(private readonly alertsService: AlertsService) {} GET = async (req: Request, res: Response): Promise => { const { appointmentId } = req.params @@ -27,17 +27,13 @@ export default class ReviewPrisonersAlertsRoutes { prisoners = appointmentJourney.prisoners } - const prisonerAlertsDetails = await this.prisonerAlertsService.getAlertDetails( - prisoners, - res.locals.user.activeCaseLoadId, - res.locals.user, - ) + const alertsDetails = await this.alertsService.getAlertDetails(prisoners, res.locals.user) res.render('pages/appointments/create-and-edit/review-prisoners-alerts', { appointmentId, backLinkHref, preserveHistory, - prisonerAlertsDetails, + alertsDetails, }) } diff --git a/server/services/alertsService.test.ts b/server/services/alertsService.test.ts new file mode 100644 index 000000000..9b30500e8 --- /dev/null +++ b/server/services/alertsService.test.ts @@ -0,0 +1,356 @@ +import { when } from 'jest-when' +import atLeast from '../../jest.setup' + +import { ServiceUser } from '../@types/express' +import AlertsApiClient from '../data/alertsApiClient' +import AlertsService, { PrisonerAlertDetails, PrisonerAlertResults, PrisonerDetails } from './alertsService' +import { Alert } from '../@types/alertsAPI/types' + +jest.mock('../data/alertsApiClient') + +describe('Alerts service', () => { + let alertsApiClient: AlertsApiClient + let alertsService: AlertsService + + const user = { + activeCaseLoadId: 'MDI', + } as ServiceUser + + let prisonerMaggie: PrisonerDetails + let prisonerDave: PrisonerDetails + + let tectAlertForMaggie: Partial + let xcuAlertForMaggie: Partial + let peepAlertForDave: Partial + + let maggieResult: PrisonerAlertDetails + let daveResult: PrisonerAlertDetails + + beforeEach(() => { + alertsApiClient = new AlertsApiClient() + alertsService = new AlertsService(alertsApiClient) + + prisonerMaggie = { + number: 'B1111B', + name: 'Maggie Jones', + category: 'H', + } + + prisonerDave = { + number: 'A1111A', + name: 'Dave Smith', + category: 'A', + } + + tectAlertForMaggie = { + prisonNumber: 'B1111B', + isActive: true, + alertCode: { + alertTypeCode: '', + alertTypeDescription: '', + code: 'TECT', + description: 'Terrorism Act or Related Offence', + }, + } + + xcuAlertForMaggie = { + prisonNumber: 'B1111B', + isActive: true, + alertCode: { + alertTypeCode: '', + alertTypeDescription: '', + code: 'XCU', + description: 'Controlled Unlock', + }, + } + + peepAlertForDave = { + prisonNumber: 'A1111A', + isActive: true, + alertCode: { + alertTypeCode: '', + alertTypeDescription: '', + code: 'PEEP', + description: 'Personal Emergency Evacuation Plan', + }, + } + + maggieResult = { + number: prisonerMaggie.number, + name: prisonerMaggie.name, + category: prisonerMaggie.category, + alerts: [ + { + alertCode: tectAlertForMaggie.alertCode.code, + }, + { + alertCode: xcuAlertForMaggie.alertCode.code, + }, + ], + alertDescriptions: [xcuAlertForMaggie.alertCode.description, tectAlertForMaggie.alertCode.description], + hasBadgeAlerts: true, + hasRelevantCategories: true, + } + + daveResult = { + number: prisonerDave.number, + name: prisonerDave.name, + category: prisonerDave.category, + alerts: [ + { + alertCode: peepAlertForDave.alertCode.code, + }, + ], + alertDescriptions: [peepAlertForDave.alertCode.description], + hasBadgeAlerts: true, + hasRelevantCategories: true, + } + }) + + describe('getAlertsForPrisoners', () => { + it('should respond with the alerts for the prisoners given, from the alerts API', async () => { + const apiResponse: Alert[] = [ + { + alertUuid: '6cd3d3cb-fe76-4746-b2ea-a6968a63c2aa', + prisonNumber: 'G4793VF', + alertCode: { + alertTypeCode: 'X', + alertTypeDescription: 'Security', + code: 'XTACT', + description: 'Terrorism Act or Related Offence', + }, + description: 'TACT', + authorisedBy: 'TEST_GEN', + activeFrom: '2023-03-20', + activeTo: '2026-03-20', + isActive: true, + createdAt: '2023-03-20T14:14:27', + createdBy: 'SCH_ACTIVITY', + createdByDisplayName: 'Schedule Activity', + lastModifiedAt: '2023-07-20T14:14:27', + lastModifiedBy: 'TEST_GEN', + lastModifiedByDisplayName: 'TESTER', + activeToLastSetAt: '2023-07-20T14:14:27', + activeToLastSetBy: 'TEST_GEN', + activeToLastSetByDisplayName: 'TESTER', + }, + ] + + const prisonerNumbers = ['G4793VF', 'Q6755TY'] + + when(alertsApiClient.getAlertsForPrisoners) + .calledWith(atLeast(prisonerNumbers)) + .mockResolvedValue({ content: apiResponse }) + + const actualResult = await alertsService.getAlertsUsingPrisonerNumbers(prisonerNumbers, user) + + expect(actualResult).toEqual(apiResponse) + expect(alertsApiClient.getAlertsForPrisoners).toHaveBeenCalledWith(prisonerNumbers, user) + }) + }) + describe('getAlertDetails', () => { + it('should return alerts when all alerts from API are matched', async () => { + const prisoners = [prisonerDave, prisonerMaggie] + + const expectedOffenderNumbers = prisoners.map(prisoner => prisoner.number) + + const apiResponse = [peepAlertForDave, tectAlertForMaggie, xcuAlertForMaggie] as unknown as Alert[] + + when(alertsApiClient.getAlertsForPrisoners) + .calledWith(atLeast(expectedOffenderNumbers)) + .mockResolvedValue({ content: apiResponse }) + + const actualResults = await alertsService.getAlertDetails(prisoners, user) + + const expectedResults = { + numPrisonersWithAlerts: 2, + prisoners: [maggieResult, daveResult], + } + + expect(actualResults).toEqual(expectedResults) + }) + it('should return alerts where category is relevant', async () => { + const prisoners = [prisonerDave, prisonerMaggie] + + const expectedOffenderNumbers = prisoners.map(prisoner => prisoner.number) + + const apiResponse = [peepAlertForDave, xcuAlertForMaggie] as Alert[] + + when(alertsApiClient.getAlertsForPrisoners) + .calledWith(atLeast(expectedOffenderNumbers)) + .mockResolvedValue({ content: apiResponse }) + + const actualResults = await alertsService.getAlertDetails(prisoners, user) + + maggieResult.alerts = [{ alertCode: xcuAlertForMaggie.alertCode.code }] + maggieResult.alertDescriptions = [xcuAlertForMaggie.alertCode.description] + + const expectedResults = { + numPrisonersWithAlerts: 2, + prisoners: [daveResult, maggieResult], + } + + expect(actualResults).toEqual(expectedResults) + }) + it('should return alerts ignoring expired alerts and where category is irrelevant', async () => { + const prisoners = [prisonerDave, prisonerMaggie] + + const expectedOffenderNumbers = prisoners.map(prisoner => prisoner.number) + + prisonerMaggie.category = 'X' + + const apiResponse = [peepAlertForDave, xcuAlertForMaggie] as Alert[] + + when(alertsApiClient.getAlertsForPrisoners) + .calledWith(atLeast(expectedOffenderNumbers)) + .mockResolvedValue({ content: apiResponse }) + + const actualResults = await alertsService.getAlertDetails(prisoners, user) + + maggieResult.category = prisonerMaggie.category + maggieResult.alerts = [{ alertCode: xcuAlertForMaggie.alertCode.code }] + maggieResult.alertDescriptions = [xcuAlertForMaggie.alertCode.description] + maggieResult.hasBadgeAlerts = true + maggieResult.hasRelevantCategories = false + + const expectedResults = { + numPrisonersWithAlerts: 2, + prisoners: [daveResult, maggieResult], + } + + expect(actualResults).toEqual(expectedResults) + }) + it('should return alerts where an alert is irrelevant and category is irrelevant', async () => { + const prisoners = [prisonerDave, prisonerMaggie] + + const expectedOffenderNumbers = prisoners.map(prisoner => prisoner.number) + + prisonerMaggie.category = 'X' + + const apiResponse = [peepAlertForDave, tectAlertForMaggie] as unknown as Alert[] + + when(alertsApiClient.getAlertsForPrisoners) + .calledWith(atLeast(expectedOffenderNumbers)) + .mockResolvedValue({ content: apiResponse }) + + const actualResults = await alertsService.getAlertDetails(prisoners, user) + + maggieResult.category = prisonerMaggie.category + maggieResult.alerts = [{ alertCode: tectAlertForMaggie.alertCode.code }] + maggieResult.alertDescriptions = [tectAlertForMaggie.alertCode.description] + maggieResult.hasBadgeAlerts = false + maggieResult.hasRelevantCategories = false + + const expectedResults = { + numPrisonersWithAlerts: 2, + prisoners: [daveResult, maggieResult], + } + + expect(actualResults).toEqual(expectedResults) + }) + it('should return alerts when there are no alerts', async () => { + const prisoners = [prisonerDave, prisonerMaggie] + + const expectedOffenderNumbers = prisoners.map(prisoner => prisoner.number) + + prisonerDave.category = 'X' + prisonerMaggie.category = 'X' + + const apiResponse = [] as Alert[] + + when(alertsApiClient.getAlertsForPrisoners) + .calledWith(atLeast(expectedOffenderNumbers)) + .mockResolvedValue({ content: apiResponse }) + + const actualResults = await alertsService.getAlertDetails(prisoners, user) + + daveResult.category = prisonerMaggie.category + daveResult.alerts = [] + daveResult.alertDescriptions = [] + daveResult.hasBadgeAlerts = false + daveResult.hasRelevantCategories = false + maggieResult.category = prisonerMaggie.category + maggieResult.alerts = [] + maggieResult.alertDescriptions = [] + maggieResult.hasBadgeAlerts = false + maggieResult.hasRelevantCategories = false + + const expectedResults = { + numPrisonersWithAlerts: 0, + prisoners: [daveResult, maggieResult], + } + + expect(actualResults).toEqual(expectedResults) + }) + it('should sort alerts correctly', async () => { + const prisonerAlan = { + number: 'D1111D', + name: 'Alan Jones', + category: 'A', + } + + const prisonerAlice = { + number: 'E1111E', + name: 'Alice Robins', + } + + const ocgAlertForAlice = { + prisonNumber: 'E1111E', + isActive: true, + alertCode: { + alertTypeCode: '', + alertTypeDescription: '', + code: 'OCG', + description: 'OCG Nominal', + }, + } + + const alanResult = { + number: prisonerAlan.number, + name: prisonerAlan.name, + category: prisonerAlan.category, + alerts: [] as PrisonerAlertResults[], + alertDescriptions: [] as string[], + hasBadgeAlerts: true, + hasRelevantCategories: true, + } + + const aliceResult = { + number: prisonerAlice.number, + name: prisonerAlice.name, + alerts: [ + { + alertCode: ocgAlertForAlice.alertCode.code, + }, + ], + alertDescriptions: [ocgAlertForAlice.alertCode.description], + hasBadgeAlerts: false, + hasRelevantCategories: false, + } + + const prisoners = [prisonerAlice, prisonerDave, prisonerMaggie, prisonerAlan] + + const expectedOffenderNumbers = prisoners.map(prisoner => prisoner.number) + + const apiResponse = [ + peepAlertForDave, + tectAlertForMaggie, + xcuAlertForMaggie, + ocgAlertForAlice, + ] as unknown as Alert[] + + when(alertsApiClient.getAlertsForPrisoners) + .calledWith(atLeast(expectedOffenderNumbers)) + .mockResolvedValue({ content: apiResponse }) + + const actualResults = await alertsService.getAlertDetails(prisoners, user) + + const expectedResults = { + numPrisonersWithAlerts: 3, + prisoners: [maggieResult, daveResult, aliceResult, alanResult], + } + + expect(actualResults).toEqual(expectedResults) + }) + }) +}) diff --git a/server/services/prisonerAlertsService.ts b/server/services/alertsService.ts similarity index 67% rename from server/services/prisonerAlertsService.ts rename to server/services/alertsService.ts index 060e349c0..f4252bf17 100644 --- a/server/services/prisonerAlertsService.ts +++ b/server/services/alertsService.ts @@ -1,9 +1,9 @@ -import { Alert } from '../@types/prisonApiImport/types' +import { Alert } from '../@types/alertsAPI/types' import { ServiceUser } from '../@types/express' -import PrisonService from './prisonService' +import AlertsApiClient from '../data/alertsApiClient' -export default class PrisonerAlertsService { - constructor(private readonly prisonService: PrisonService) {} +export default class AlertsService { + constructor(private readonly alertsApiClient: AlertsApiClient) {} /* We need to know which prisoner has badged alerts to determine how to display this text list @@ -45,35 +45,25 @@ export default class PrisonerAlertsService { readonly categoriesWithBadges: Set = new Set(['A', 'E', 'H', 'P']) - private fetchRelevantPrisonerAlerts( - offenderNumbers: string[], - prisonCode: string, - user: ServiceUser, - ): Promise { - return this.prisonService - .getPrisonerAlerts(offenderNumbers, prisonCode, user) - .then(a => a.filter(alert => !alert.expired && alert.active)) + async getAlertsUsingPrisonerNumbers(prisonerNumbers: string[], user: ServiceUser): Promise { + return this.alertsApiClient.getAlertsForPrisoners(prisonerNumbers, user).then(alerts => alerts?.content) } - async getAlertDetails( - prisoners: PrisonerDetails[], - prisonCode: string, - user: ServiceUser, - ): Promise { - const offenderNumbers: string[] = prisoners.map(prisoner => prisoner.number) + async getAlertDetails(prisoners: PrisonerDetails[], user: ServiceUser): Promise { + const prisonerNumbers: string[] = prisoners.map(prisoner => prisoner.number) - const allAlertsForAllPrisoners: Alert[] = await this.fetchRelevantPrisonerAlerts(offenderNumbers, prisonCode, user) + const allAlertsForAllPrisoners: Alert[] = await this.getAlertsUsingPrisonerNumbers(prisonerNumbers, user) const prisonersWithAlerts: PrisonerAlertDetails[] = [] let numPrisonersWithAlerts = 0 prisoners.forEach(prisoner => { - const relevantAlerts = allAlertsForAllPrisoners.filter(alert => alert.offenderNo === prisoner.number) + const relevantAlerts = allAlertsForAllPrisoners.filter(alert => alert.prisonNumber === prisoner.number) - const prisonerAlerts = relevantAlerts.map(a => ({ alertCode: a.alertCode })) + const prisonerAlerts = relevantAlerts.map(a => ({ alertCode: a.alertCode.code })) const prisonerAlertDescriptions = relevantAlerts - .map(alert => alert.alertCodeDescription) + .map(alert => alert.alertCode.description) .sort((a, b) => a.localeCompare(b)) const isCategoryRelevant = this.categoriesWithBadges.has(prisoner.category) @@ -87,7 +77,8 @@ export default class PrisonerAlertsService { alerts: prisonerAlerts, alertDescriptions: prisonerAlertDescriptions, hasRelevantCategories: isCategoryRelevant, - hasBadgeAlerts: isCategoryRelevant || relevantAlerts.some(alert => this.alertsWithBadges.has(alert.alertCode)), + hasBadgeAlerts: + isCategoryRelevant || relevantAlerts.some(alert => this.alertsWithBadges.has(alert.alertCode.code)), }) }) diff --git a/server/services/healthCheck.ts b/server/services/healthCheck.ts index 272ecfc0b..a0ff9da8a 100644 --- a/server/services/healthCheck.ts +++ b/server/services/healthCheck.ts @@ -63,6 +63,7 @@ const apiChecks = [ `${config.apis.nonAssociationsApi.url}/health/ping`, config.apis.nonAssociationsApi.agent, ), + service('alertsApi', `${config.apis.alertsApi.url}/health/ping`, config.apis.alertsApi.agent), service('bookAVideoLinkApi', `${config.apis.bookAVideoLinkApi.url}/health/ping`, config.apis.bookAVideoLinkApi.agent), service( 'locationsInsidePrisonApi', diff --git a/server/services/index.test.ts b/server/services/index.test.ts index 6f33f7e78..0f59a7f7f 100644 --- a/server/services/index.test.ts +++ b/server/services/index.test.ts @@ -10,6 +10,7 @@ import AlertsFilterService from './alertsFilterService' import BookAVideoLinkService from './bookAVideoLinkService' import NonAssociationsService from './nonAssociationsService' import LocationMappingService from './locationMappingService' +import AlertsService from './alertsService' jest.mock('applicationinsights') @@ -17,7 +18,7 @@ describe('Services', () => { test('The correct services are instantiated', () => { const servicesList = services() - expect(Object.values(servicesList).length).toBe(11) + expect(Object.values(servicesList).length).toBe(12) expect(servicesList.userService).toBeInstanceOf(UserService) expect(servicesList.prisonService).toBeInstanceOf(PrisonService) expect(servicesList.ukBankHolidayService).toBeInstanceOf(BankHolidayService) @@ -28,6 +29,7 @@ describe('Services', () => { expect(servicesList.metricsService).toBeInstanceOf(MetricsService) expect(servicesList.bookAVideoLinkService).toBeInstanceOf(BookAVideoLinkService) expect(servicesList.nonAssociationsService).toBeInstanceOf(NonAssociationsService) + expect(servicesList.alertsService).toBeInstanceOf(AlertsService) expect(servicesList.locationMappingService).toBeInstanceOf(LocationMappingService) }) }) diff --git a/server/services/index.ts b/server/services/index.ts index 64bd898f3..3c7f30897 100644 --- a/server/services/index.ts +++ b/server/services/index.ts @@ -10,6 +10,7 @@ import AlertsFilterService from './alertsFilterService' import BookAVideoLinkService from './bookAVideoLinkService' import NonAssociationsService from './nonAssociationsService' import LocationMappingService from './locationMappingService' +import AlertsService from './alertsService' export default function services() { const { @@ -23,12 +24,14 @@ export default function services() { applicationInsightsClient, caseNotesApiClient, nonAssociationsApiClient, + alertsApiClient, locationsInsidePrisonApiClient, nomisMappingClient, } = dataAccess() const alertsFilterService = new AlertsFilterService() const prisonService = new PrisonService(prisonApiClient, prisonerSearchApiClient, incentivesApiClient) + const alertsService = new AlertsService(alertsApiClient) return { userService: new UserService(manageUsersApiClient, prisonRegisterApiClient, activitiesApiClient), @@ -41,6 +44,7 @@ export default function services() { unlockListService: new UnlockListService(prisonerSearchApiClient, activitiesApiClient, alertsFilterService), metricsService: new MetricsService(applicationInsightsClient), nonAssociationsService: new NonAssociationsService(nonAssociationsApiClient, prisonService), + alertsService, locationMappingService: new LocationMappingService(locationsInsidePrisonApiClient, nomisMappingClient), } } diff --git a/server/services/prisonService.ts b/server/services/prisonService.ts index 7c168772b..baee5feea 100644 --- a/server/services/prisonService.ts +++ b/server/services/prisonService.ts @@ -2,7 +2,7 @@ import _ from 'lodash' import { Readable } from 'stream' import PrisonApiClient from '../data/prisonApiClient' import PrisonerSearchApiClient from '../data/prisonerSearchApiClient' -import { ReferenceCode, AgencyPrisonerPayProfile, Alert } from '../@types/prisonApiImport/types' +import { ReferenceCode, AgencyPrisonerPayProfile } from '../@types/prisonApiImport/types' import { PagePrisoner, Prisoner } from '../@types/prisonerOffenderSearchImport/types' import { ServiceUser } from '../@types/express' @@ -66,10 +66,6 @@ export default class PrisonService { return this.prisonApiClient.getPayProfile(prisonCode) } - async getPrisonerAlerts(offenderNumbers: string[], prisonCode: string, user: ServiceUser): Promise { - return this.prisonApiClient.getPrisonersAlerts(offenderNumbers, prisonCode, user) - } - async getPrisonerImage(prisonerNumber: string, user: ServiceUser): Promise { return this.prisonApiClient.getPrisonerImage(prisonerNumber, user) } diff --git a/server/services/prisonerAlertsService.test.ts b/server/services/prisonerAlertsService.test.ts deleted file mode 100644 index 512db6e24..000000000 --- a/server/services/prisonerAlertsService.test.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { when } from 'jest-when' -import atLeast from '../../jest.setup' -import PrisonService from './prisonService' -import { ServiceUser } from '../@types/express' -import PrisonerAlertsService, { - PrisonerAlertDetails, - PrisonerAlertResults, - PrisonerDetails, -} from './prisonerAlertsService' -import { Alert } from '../@types/prisonApiImport/types' - -jest.mock('./prisonService') - -describe('Prisoner Alerts Service', () => { - let prisonService: PrisonService - let prisonerAlertsService: PrisonerAlertsService - - const user = { - activeCaseLoadId: 'MDI', - } as ServiceUser - - let prisonerMaggie: PrisonerDetails - let prisonerDave: PrisonerDetails - - let tectAlertForMaggie: Partial - let xcuAlertForMaggie: Partial - let peepAlertForDave: Partial - - let maggieResult: PrisonerAlertDetails - let daveResult: PrisonerAlertDetails - - beforeEach(() => { - prisonService = new PrisonService(null, null, null) as jest.Mocked - prisonerAlertsService = new PrisonerAlertsService(prisonService) - - prisonerMaggie = { - number: 'B1111B', - name: 'Maggie Jones', - category: 'H', - } - - prisonerDave = { - number: 'A1111A', - name: 'Dave Smith', - category: 'A', - } - - tectAlertForMaggie = { - offenderNo: 'B1111B', - active: true, - expired: false, - alertCode: 'TECT', - alertCodeDescription: 'Terrorism Act or Related Offence', - } - - xcuAlertForMaggie = { - offenderNo: 'B1111B', - active: true, - expired: false, - alertCode: 'XCU', - alertCodeDescription: 'Controlled Unlock', - } - - peepAlertForDave = { - offenderNo: 'A1111A', - active: true, - expired: false, - alertCode: 'PEEP', - alertCodeDescription: 'Personal Emergency Evacuation Plan', - } - - maggieResult = { - number: prisonerMaggie.number, - name: prisonerMaggie.name, - category: prisonerMaggie.category, - alerts: [ - { - alertCode: tectAlertForMaggie.alertCode, - }, - { - alertCode: xcuAlertForMaggie.alertCode, - }, - ], - alertDescriptions: [xcuAlertForMaggie.alertCodeDescription, tectAlertForMaggie.alertCodeDescription], - hasBadgeAlerts: true, - hasRelevantCategories: true, - } - - daveResult = { - number: prisonerDave.number, - name: prisonerDave.name, - category: prisonerDave.category, - alerts: [ - { - alertCode: peepAlertForDave.alertCode, - }, - ], - alertDescriptions: [peepAlertForDave.alertCodeDescription], - hasBadgeAlerts: true, - hasRelevantCategories: true, - } - }) - - it('should return alerts when all alerts from API are matched', async () => { - const prisoners = [prisonerDave, prisonerMaggie] - - const expectedOffenderNumbers = prisoners.map(prisoner => prisoner.number) - - const apiResponse = [peepAlertForDave, tectAlertForMaggie, xcuAlertForMaggie] as unknown as Alert[] - - when(prisonService.getPrisonerAlerts) - .calledWith(atLeast(expectedOffenderNumbers, 'MDI', user)) - .mockResolvedValue(apiResponse) - - const actualResults = await prisonerAlertsService.getAlertDetails(prisoners, 'MDI', user) - - const expectedResults = { - numPrisonersWithAlerts: 2, - prisoners: [maggieResult, daveResult], - } - - expect(actualResults).toEqual(expectedResults) - }) - - it('should return alerts ignoring expired alerts and where category is relevant', async () => { - const prisoners = [prisonerDave, prisonerMaggie] - - const expectedOffenderNumbers = prisoners.map(prisoner => prisoner.number) - - tectAlertForMaggie.expired = true - - const apiResponse = [peepAlertForDave, tectAlertForMaggie, xcuAlertForMaggie] as unknown as Alert[] - - when(prisonService.getPrisonerAlerts) - .calledWith(atLeast(expectedOffenderNumbers, 'MDI', user)) - .mockReturnValue(Promise.resolve(apiResponse)) - - const actualResults = await prisonerAlertsService.getAlertDetails(prisoners, 'MDI', user) - - maggieResult.alerts = [{ alertCode: xcuAlertForMaggie.alertCode }] - maggieResult.alertDescriptions = [xcuAlertForMaggie.alertCodeDescription] - - const expectedResults = { - numPrisonersWithAlerts: 2, - prisoners: [daveResult, maggieResult], - } - - expect(actualResults).toEqual(expectedResults) - }) - - it('should return alerts ignoring inactive alerts and where category is relevant', async () => { - const prisoners = [prisonerDave, prisonerMaggie] - - const expectedOffenderNumbers = prisoners.map(prisoner => prisoner.number) - - tectAlertForMaggie.active = false - - const apiResponse = [peepAlertForDave, tectAlertForMaggie, xcuAlertForMaggie] as unknown as Alert[] - - when(prisonService.getPrisonerAlerts) - .calledWith(atLeast(expectedOffenderNumbers, 'MDI', user)) - .mockResolvedValue(apiResponse) - - const actualResults = await prisonerAlertsService.getAlertDetails(prisoners, 'MDI', user) - - maggieResult.alerts = [{ alertCode: xcuAlertForMaggie.alertCode }] - maggieResult.alertDescriptions = [xcuAlertForMaggie.alertCodeDescription] - - const expectedResults = { - numPrisonersWithAlerts: 2, - prisoners: [daveResult, maggieResult], - } - - expect(actualResults).toEqual(expectedResults) - }) - - it('should return alerts ignoring expired alerts and where category is irrelevant', async () => { - const prisoners = [prisonerDave, prisonerMaggie] - - const expectedOffenderNumbers = prisoners.map(prisoner => prisoner.number) - - prisonerMaggie.category = 'X' - tectAlertForMaggie.expired = true - - const apiResponse = [peepAlertForDave, tectAlertForMaggie, xcuAlertForMaggie] as unknown as Alert[] - - when(prisonService.getPrisonerAlerts) - .calledWith(atLeast(expectedOffenderNumbers, 'MDI', user)) - .mockResolvedValue(apiResponse) - - const actualResults = await prisonerAlertsService.getAlertDetails(prisoners, 'MDI', user) - - maggieResult.category = prisonerMaggie.category - maggieResult.alerts = [{ alertCode: xcuAlertForMaggie.alertCode }] - maggieResult.alertDescriptions = [xcuAlertForMaggie.alertCodeDescription] - maggieResult.hasBadgeAlerts = true - maggieResult.hasRelevantCategories = false - - const expectedResults = { - numPrisonersWithAlerts: 2, - prisoners: [daveResult, maggieResult], - } - - expect(actualResults).toEqual(expectedResults) - }) - - it('should return alerts where an alert is irrelevant and category is irrelevant', async () => { - const prisoners = [prisonerDave, prisonerMaggie] - - const expectedOffenderNumbers = prisoners.map(prisoner => prisoner.number) - - prisonerMaggie.category = 'X' - - const apiResponse = [peepAlertForDave, tectAlertForMaggie] as unknown as Alert[] - - when(prisonService.getPrisonerAlerts) - .calledWith(atLeast(expectedOffenderNumbers, 'MDI', user)) - .mockResolvedValue(apiResponse) - - const actualResults = await prisonerAlertsService.getAlertDetails(prisoners, 'MDI', user) - - maggieResult.category = prisonerMaggie.category - maggieResult.alerts = [{ alertCode: tectAlertForMaggie.alertCode }] - maggieResult.alertDescriptions = [tectAlertForMaggie.alertCodeDescription] - maggieResult.hasBadgeAlerts = false - maggieResult.hasRelevantCategories = false - - const expectedResults = { - numPrisonersWithAlerts: 2, - prisoners: [daveResult, maggieResult], - } - - expect(actualResults).toEqual(expectedResults) - }) - - it('should return alerts when there are no alerts', async () => { - const prisoners = [prisonerDave, prisonerMaggie] - - const expectedOffenderNumbers = prisoners.map(prisoner => prisoner.number) - - prisonerDave.category = 'X' - prisonerMaggie.category = 'X' - peepAlertForDave.expired = true - tectAlertForMaggie.active = false - - const apiResponse = [peepAlertForDave, tectAlertForMaggie] as unknown as Alert[] - - when(prisonService.getPrisonerAlerts) - .calledWith(atLeast(expectedOffenderNumbers, 'MDI', user)) - .mockResolvedValue(apiResponse) - - const actualResults = await prisonerAlertsService.getAlertDetails(prisoners, 'MDI', user) - - daveResult.category = prisonerMaggie.category - daveResult.alerts = [] - daveResult.alertDescriptions = [] - daveResult.hasBadgeAlerts = false - daveResult.hasRelevantCategories = false - maggieResult.category = prisonerMaggie.category - maggieResult.alerts = [] - maggieResult.alertDescriptions = [] - maggieResult.hasBadgeAlerts = false - maggieResult.hasRelevantCategories = false - - const expectedResults = { - numPrisonersWithAlerts: 0, - prisoners: [daveResult, maggieResult], - } - - expect(actualResults).toEqual(expectedResults) - }) - - it('should sort alerts correctly', async () => { - const prisonerAlan = { - number: 'D1111D', - name: 'Alan Jones', - category: 'A', - } - - const prisonerAlice = { - number: 'E1111E', - name: 'Alice Robins', - } - - const ocgAlertForAlice = { - offenderNo: 'E1111E', - active: true, - expired: false, - alertCode: 'OCG', - alertCodeDescription: 'OCG Nominal', - } - - const alanResult = { - number: prisonerAlan.number, - name: prisonerAlan.name, - category: prisonerAlan.category, - alerts: [] as PrisonerAlertResults[], - alertDescriptions: [] as string[], - hasBadgeAlerts: true, - hasRelevantCategories: true, - } - - const aliceResult = { - number: prisonerAlice.number, - name: prisonerAlice.name, - alerts: [ - { - alertCode: ocgAlertForAlice.alertCode, - }, - ], - alertDescriptions: [ocgAlertForAlice.alertCodeDescription], - hasBadgeAlerts: false, - hasRelevantCategories: false, - } - - const prisoners = [prisonerAlice, prisonerDave, prisonerMaggie, prisonerAlan] - - const expectedOffenderNumbers = prisoners.map(prisoner => prisoner.number) - - const apiResponse = [ - peepAlertForDave, - tectAlertForMaggie, - xcuAlertForMaggie, - ocgAlertForAlice, - ] as unknown as Alert[] - - when(prisonService.getPrisonerAlerts) - .calledWith(atLeast(expectedOffenderNumbers, 'MDI', user)) - .mockResolvedValue(apiResponse) - - const actualResults = await prisonerAlertsService.getAlertDetails(prisoners, 'MDI', user) - - const expectedResults = { - numPrisonersWithAlerts: 3, - prisoners: [maggieResult, daveResult, aliceResult, alanResult], - } - - expect(actualResults).toEqual(expectedResults) - }) -}) diff --git a/server/views/pages/appointments/create-and-edit/review-prisoners-alerts.njk b/server/views/pages/appointments/create-and-edit/review-prisoners-alerts.njk index 466f0aef8..95d7b4f27 100644 --- a/server/views/pages/appointments/create-and-edit/review-prisoners-alerts.njk +++ b/server/views/pages/appointments/create-and-edit/review-prisoners-alerts.njk @@ -12,8 +12,8 @@ {% from "moj/components/badge/macro.njk" import mojBadge -%} {% from "govuk/components/details/macro.njk" import govukDetails %} -{% set prisoners = prisonerAlertsDetails.prisoners %} -{% set numPrisonersWithAlerts = prisonerAlertsDetails.numPrisonersWithAlerts %} +{% set prisoners = alertsDetails.prisoners %} +{% set numPrisonersWithAlerts = alertsDetails.numPrisonersWithAlerts %} {% if session.appointmentJourney.mode == AppointmentJourneyMode.EDIT %} {% set pageTitle = appointmentJourneyTitle("no attendees added" if prisoners.length == 0 else "Review additional attendee" + ("s" if prisoners.length > 1), session.appointmentJourney) %}