Skip to content

Commit

Permalink
VB-4942 Filter visit notification events by configurable allow list (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
tpmcgowan authored Jan 30, 2025
1 parent f691c63 commit e71c8e1
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 54 deletions.
51 changes: 7 additions & 44 deletions integration_tests/e2e/reviewVisitsList.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ context('Bookings review listing page', () => {
const prettyDateFormat = 'd MMMM yyyy'

const notificationGroups = [
TestData.notificationGroup(),
TestData.notificationGroup({
reference: 'bc*de*fg*hi',
type: 'PRISONER_RELEASED_EVENT',
Expand All @@ -22,18 +21,6 @@ context('Bookings review listing page', () => {
}),
],
}),
TestData.notificationGroup({
reference: 'cd*ef*gh*ij',
type: 'PRISONER_RESTRICTION_CHANGE_EVENT',
affectedVisits: [
TestData.notificationVisitInfo({
bookedByUserName: 'user4',
bookedByName: 'User Four',
prisonerNumber: 'C1234DE',
visitDate: '2023-12-05',
}),
],
}),
TestData.notificationGroup({
reference: 'de*fg*hi*jk',
type: 'PRISON_VISITS_BLOCKED_FOR_DATE',
Expand Down Expand Up @@ -63,20 +50,18 @@ context('Bookings review listing page', () => {
homePage.needReviewTile().click()
const listingPage = Page.verifyOnPage(VisitsReviewListingPage)

// Non-association
// Prisoner released
listingPage.getPrisonerNumber(1).contains(notificationGroups[0].affectedVisits[0].prisonerNumber)
listingPage.getPrisonerNumber(1).contains(notificationGroups[0].affectedVisits[1].prisonerNumber)
listingPage
.getVisitDate(1)
.contains(format(new Date(notificationGroups[0].affectedVisits[0].visitDate), prettyDateFormat))
listingPage.getBookedBy(1).contains(notificationGroups[0].affectedVisits[0].bookedByName)
listingPage.getBookedBy(1).contains(notificationGroups[0].affectedVisits[1].bookedByName)
listingPage.getType(1).contains(notificationTypes[notificationGroups[0].type])
listingPage
.getActionLink(1)
.should('have.attr', 'href', `/review/non-association/${notificationGroups[0].reference}`)
.should('have.attr', 'href', `/visit/${notificationGroups[0].affectedVisits[0].bookingReference}?from=review`)

// Prisoner released
// Visits blocked for date
listingPage.getPrisonerNumber(2).contains(notificationGroups[1].affectedVisits[0].prisonerNumber)
listingPage
.getVisitDate(2)
Expand All @@ -86,28 +71,6 @@ context('Bookings review listing page', () => {
listingPage
.getActionLink(2)
.should('have.attr', 'href', `/visit/${notificationGroups[1].affectedVisits[0].bookingReference}?from=review`)

// Visit type changed
listingPage.getPrisonerNumber(3).contains(notificationGroups[2].affectedVisits[0].prisonerNumber)
listingPage
.getVisitDate(3)
.contains(format(new Date(notificationGroups[2].affectedVisits[0].visitDate), prettyDateFormat))
listingPage.getBookedBy(3).contains(notificationGroups[2].affectedVisits[0].bookedByName)
listingPage.getType(3).contains(notificationTypes[notificationGroups[2].type])
listingPage
.getActionLink(3)
.should('have.attr', 'href', `/visit/${notificationGroups[2].affectedVisits[0].bookingReference}?from=review`)

// Visits blocked for date
listingPage.getPrisonerNumber(4).contains(notificationGroups[3].affectedVisits[0].prisonerNumber)
listingPage
.getVisitDate(4)
.contains(format(new Date(notificationGroups[3].affectedVisits[0].visitDate), prettyDateFormat))
listingPage.getBookedBy(4).contains(notificationGroups[3].affectedVisits[0].bookedByName)
listingPage.getType(4).contains(notificationTypes[notificationGroups[3].type])
listingPage
.getActionLink(4)
.should('have.attr', 'href', `/visit/${notificationGroups[3].affectedVisits[0].bookingReference}?from=review`)
})

it('should filter bookings review listing', () => {
Expand All @@ -118,12 +81,12 @@ context('Bookings review listing page', () => {
const listingPage = Page.verifyOnPage(VisitsReviewListingPage)

// All rows show when no filter selected
listingPage.getBookingsRows().should('have.length', 4)
listingPage.getBookingsRows().should('have.length', 2)

// Filter by user
listingPage.filterByUser('User One')
listingPage.applyFilter()
listingPage.getBookingsRows().should('have.length', 2)
listingPage.getBookingsRows().should('have.length', 1)
listingPage.removeFilter('User One')

// Filter by reason
Expand All @@ -134,12 +97,12 @@ context('Bookings review listing page', () => {

// Filter by both
listingPage.filterByUser('User One')
listingPage.filterByReason('Visit type changed')
listingPage.filterByReason('Prisoner released')
listingPage.applyFilter()
listingPage.getBookingsRows().should('have.length', 0)

// Clear all filters
listingPage.clearFilters()
listingPage.getBookingsRows().should('have.length', 4)
listingPage.getBookingsRows().should('have.length', 2)
})
})
12 changes: 12 additions & 0 deletions server/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { NotificationType } from './data/orchestrationApiTypes'

const production = process.env.NODE_ENV === 'production'

function get<T>(name: string, fallback: T, options = { requireInProduction: false }): T | string {
Expand Down Expand Up @@ -130,6 +132,16 @@ export default {
agent: new AgentConfig(Number(get('ORCHESTRATION_API_TIMEOUT_RESPONSE', 10000))),
},
},
features: {
notificationTypes: {
enabledNotifications: <NotificationType[]>(
get(
'FEATURE_ENABLED_NOTIFICATION_TYPES',
'PRISONER_RELEASED_EVENT,PRISONER_RECEIVED_EVENT,PRISON_VISITS_BLOCKED_FOR_DATE',
).split(',')
),
},
},
domain: get('INGRESS_URL', 'http://localhost:3000', requiredInProduction),
environmentName: get('ENVIRONMENT_NAME', ''),
}
5 changes: 0 additions & 5 deletions server/constants/notificationEvents.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { NotificationType } from '../data/orchestrationApiTypes'

// export const notificationTypes: Readonly<Record<NotificationType, string>> = {
export const notificationTypes: Partial<Readonly<Record<NotificationType, string>>> = {
NON_ASSOCIATION_EVENT: 'Non-association',
PRISONER_RELEASED_EVENT: 'Prisoner released',
Expand All @@ -9,16 +8,12 @@ export const notificationTypes: Partial<Readonly<Record<NotificationType, string
PRISONER_RECEIVED_EVENT: 'Prisoner transferred',
}

// export const notificationTypeReasons: Readonly<Record<NotificationType, string>> = {
export const notificationTypeReasons: Partial<Readonly<Record<NotificationType, string>>> = {
// NON_ASSOCIATION_EVENT: 'there are non-associations',
PRISONER_RELEASED_EVENT: 'the prisoner is released',
// PRISONER_RESTRICTION_CHANGE_EVENT: 'the visit type has changed',
PRISON_VISITS_BLOCKED_FOR_DATE: 'the date is no longer available for social visits',
PRISONER_RECEIVED_EVENT: 'the prisoner has been transferred',
}

// export const notificationTypeWarnings: Readonly<Record<NotificationType, string>> = {
export const notificationTypeWarnings: Partial<Readonly<Record<NotificationType, string>>> = {
NON_ASSOCIATION_EVENT:
'A new visit time should be selected as the original time slot has a prisoner non-association.',
Expand Down
41 changes: 37 additions & 4 deletions server/data/orchestrationApiClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,17 +397,50 @@ describe('orchestrationApiClient', () => {
})

describe('getNotificationGroups', () => {
it('should return notification groups for given prison', async () => {
const notificationGroups = [TestData.notificationGroup()]
const rawNotificationGroups = [
TestData.notificationGroup({ type: 'NON_ASSOCIATION_EVENT' }),
TestData.notificationGroup({ type: 'PRISONER_RELEASED_EVENT' }),
TestData.notificationGroup({ type: 'PRISON_VISITS_BLOCKED_FOR_DATE' }),
]

afterEach(() => {
jest.resetAllMocks()
})

it('should return filtered notification groups for given prison - no types allowed', async () => {
jest.replaceProperty(config, 'features', {
notificationTypes: { enabledNotifications: [] },
})

fakeOrchestrationApi
.get(`/visits/notification/${prisonId}/groups`)
.matchHeader('authorization', `Bearer ${token}`)
.reply(200, rawNotificationGroups)

const output = await orchestrationApiClient.getNotificationGroups(prisonId)

expect(output).toEqual([])
})

it('should return filtered notification groups for given prison - some types allowed', async () => {
jest.replaceProperty(config, 'features', {
notificationTypes: {
enabledNotifications: [
'PRISONER_RELEASED_EVENT',
'PRISONER_RESTRICTION_CHANGE_EVENT',
'PRISON_VISITS_BLOCKED_FOR_DATE',
],
},
})

fakeOrchestrationApi
.get(`/visits/notification/${prisonId}/groups`)
.matchHeader('authorization', `Bearer ${token}`)
.reply(200, notificationGroups)
.reply(200, rawNotificationGroups)

const output = await orchestrationApiClient.getNotificationGroups(prisonId)

expect(output).toEqual(notificationGroups)
expect(output).toEqual([rawNotificationGroups[1], rawNotificationGroups[2]])
})
})

Expand Down
8 changes: 7 additions & 1 deletion server/data/orchestrationApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,13 @@ export default class OrchestrationApiClient {
}

async getNotificationGroups(prisonId: string): Promise<NotificationGroup[]> {
return this.restClient.get({ path: `/visits/notification/${prisonId}/groups` })
const { enabledNotifications } = config.features.notificationTypes

const notificationGroups = await this.restClient.get<NotificationGroup[]>({
path: `/visits/notification/${prisonId}/groups`,
})

return notificationGroups.filter(notification => enabledNotifications.includes(notification.type))
}

async getVisitNotifications(reference: string): Promise<NotificationType[]> {
Expand Down

0 comments on commit e71c8e1

Please sign in to comment.