Skip to content

Commit

Permalink
CDPS-764 add currently out establishment roll page (#139)
Browse files Browse the repository at this point in the history
  • Loading branch information
whitfield-mj authored May 28, 2024
1 parent 8816d0b commit eb23132
Show file tree
Hide file tree
Showing 24 changed files with 322 additions and 31 deletions.
46 changes: 46 additions & 0 deletions integration_tests/e2e/currentlyOut.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Page from '../pages/page'
import { Role } from '../../server/enums/role'
import CurrentlyOutPage from '../pages/currentlyOut'

context('En Route Page', () => {
beforeEach(() => {
cy.task('reset')
cy.setupUserAuth({
roles: [`ROLE_PRISON`, `ROLE_${Role.GlobalSearch}`],
caseLoads: [
{ caseloadFunction: '', caseLoadId: 'LEI', currentlyActive: true, description: 'Leeds (HMP)', type: '' },
],
})
cy.task('stubOutToday', '123')
cy.task('stubPostSearchPrisonersById')
cy.task('stubRecentMovements')
cy.task('stubGetLocation')
cy.signIn({ redirectPath: '/establishment-roll/123/currently-out' })
cy.visit('/establishment-roll/123/currently-out')
})

it('Page is visible', () => {
Page.verifyOnPage(CurrentlyOutPage)
})

it('should display a table row for each prisoner en-route', () => {
const page = Page.verifyOnPage(CurrentlyOutPage)
page.currentlyOutRows().should('have.length', 2)

page.currentlyOutRows().first().find('td').eq(1).should('contain.text', 'Shannon, Eddie')
page.currentlyOutRows().first().find('td').eq(2).should('contain.text', 'A1234AB')
page.currentlyOutRows().first().find('td').eq(3).should('contain.text', '01/01/1980')
page.currentlyOutRows().first().find('td').eq(4).should('contain.text', '1-1-1')
page.currentlyOutRows().first().find('td').eq(5).should('contain.text', '')
page.currentlyOutRows().first().find('td').eq(6).should('contain.text', 'Sheffield')
page.currentlyOutRows().first().find('td').eq(7).should('contain.text', 'Some Sheffield comment')
})

it('should display alerts and category if cat A', () => {
const page = Page.verifyOnPage(CurrentlyOutPage)
page.currentlyOutRows().should('have.length', 2)

page.currentlyOutRows().eq(1).find('td').eq(5).should('contain.text', 'Hidden disability')
page.currentlyOutRows().eq(1).find('td').eq(5).should('contain.text', 'CAT A')
})
})
16 changes: 16 additions & 0 deletions integration_tests/mockApis/prison.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ export default {
})
},

stubOutToday: (livingUnitId = 'abc') => {
return stubFor({
request: {
method: 'GET',
urlPattern: `/prison/api/movements/livingUnit/${livingUnitId}/currently-out`,
},
response: {
status: 200,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
jsonBody: movementsOutMock,
},
})
},

stubMovementsInReception: (prisonCode = 'LEI') => {
return stubFor({
request: {
Expand Down
9 changes: 9 additions & 0 deletions integration_tests/pages/currentlyOut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Page, { PageElement } from './page'

export default class CurrentlyOutPage extends Page {
constructor() {
super(`Currently out - A-wing`)
}

currentlyOutRows = (): PageElement => cy.get('table.currently-out-roll__table tbody tr')
}
25 changes: 22 additions & 3 deletions server/controllers/establishmentRollController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import EstablishmentRollService from '../services/establishmentRollService'
import MovementsService from '../services/movementsService'
import { userHasRoles } from '../utils/utils'
import { Role } from '../enums/role'
import LocationService from '../services/locationsService'

export default class EstablishmentRollController {
constructor(
private readonly establishmentRollService: EstablishmentRollService,
private readonly movementsService: MovementsService,
private readonly locationService: LocationService,
) {}

public getEstablishmentRoll(): RequestHandler {
Expand All @@ -32,9 +34,9 @@ export default class EstablishmentRollController {

const [landingRollCounts, wing, spur, landing] = await Promise.all([
this.establishmentRollService.getLandingRollCounts(clientToken, user.activeCaseLoadId, Number(landingId)),
this.establishmentRollService.getLocationInfo(clientToken, wingId),
spurId ? this.establishmentRollService.getLocationInfo(clientToken, spurId) : null,
this.establishmentRollService.getLocationInfo(clientToken, landingId),
this.locationService.getLocationInfo(clientToken, wingId),
spurId ? this.locationService.getLocationInfo(clientToken, spurId) : null,
this.locationService.getLocationInfo(clientToken, landingId),
])

res.render('pages/establishmentRollLanding', { landingRollCounts, wing, spur, landing })
Expand Down Expand Up @@ -101,4 +103,21 @@ export default class EstablishmentRollController {
})
}
}

public getCurrentlyOut(): RequestHandler {
return async (req: Request, res: Response) => {
const { livingUnitId } = req.params
const { clientToken } = req.middleware

const [prisonersCurrentlyOut, location] = await Promise.all([
this.movementsService.getOffendersCurrentlyOutOfLivingUnit(clientToken, livingUnitId),
this.locationService.getLocationInfo(clientToken, livingUnitId),
])

res.render('pages/currentlyOut', {
prisoners: prisonersCurrentlyOut,
location,
})
}
}
}
2 changes: 2 additions & 0 deletions server/data/interfaces/offenderMovement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export interface OffenderMovement {
fromAgencyDescription: string
toAgency: string
toAgencyDescription: string
toCity: string
commentText?: string
movementType: 'CRT' | 'ADM' | 'REL' | 'TAP' | 'TRN'
movementTypeDescription: string
movementReason: string
Expand Down
1 change: 1 addition & 0 deletions server/data/interfaces/prisonApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ export interface PrisonApiClient {
getPrisonerImage(offenderNumber: string, fullSizeImage: boolean): Promise<Readable>
getOffenderCellHistory(bookingId: number, params?: { page: number; size: number }): Promise<PagedList<BedAssignment>>
getUserDetailsList(usernames: string[]): Promise<UserDetail[]>
getPrisonersCurrentlyOutOfLivingUnit(livingUnitId: string): Promise<OffenderOut[]>
}
4 changes: 4 additions & 0 deletions server/data/prisonApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,8 @@ export default class PrisonApiRestClient implements PrisonApiClient {
getUserDetailsList(usernames: string[]): Promise<UserDetail[]> {
return this.post<UserDetail[]>({ path: '/api/users/list', data: usernames })
}

getPrisonersCurrentlyOutOfLivingUnit(livingUnitId: string): Promise<OffenderOut[]> {
return this.get<OffenderOut[]>({ path: `/api/movements/livingUnit/${livingUnitId}/currently-out` })
}
}
2 changes: 2 additions & 0 deletions server/routes/establishmentRollRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default function establishmentRollRouter(services: Services): Router {
const establishmentRollController = new EstablishmentRollController(
services.establishmentRollService,
services.movementsService,
services.locationsService,
)

get('/', establishmentRollController.getEstablishmentRoll())
Expand All @@ -32,6 +33,7 @@ export default function establishmentRollRouter(services: Services): Router {
get('/en-route', establishmentRollController.getEnRoute())
get('/in-reception', establishmentRollController.getInReception())
get('/no-cell-allocated', establishmentRollController.getUnallocated())
get('/:livingUnitId/currently-out', establishmentRollController.getCurrentlyOut())

return router
}
11 changes: 0 additions & 11 deletions server/services/establishmentRollService.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import EstablishmentRollService from './establishmentRollService'
import prisonApiClientMock from '../test/mocks/prisonApiClientMock'
import { assignedRollCountWithSpursMock, unassignedRollCountMock } from '../mocks/rollCountMock'
import { locationMock } from '../mocks/locationMock'

describe('establishmentRollService', () => {
let establishmentRollService: EstablishmentRollService
Expand Down Expand Up @@ -128,14 +127,4 @@ describe('establishmentRollService', () => {
expect(landingRollCounts).toEqual(assignedRollCountWithSpursMock)
})
})

describe('getLocationInfo', () => {
it('should call api and return response', async () => {
prisonApiClientMock.getLocation = jest.fn().mockResolvedValueOnce(locationMock)

const locationInfo = await establishmentRollService.getLocationInfo('token', 'loc')

expect(locationInfo).toEqual(locationMock)
})
})
})
7 changes: 0 additions & 7 deletions server/services/establishmentRollService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { PrisonApiClient } from '../data/interfaces/prisonApiClient'
import { BlockRollCount } from '../data/interfaces/blockRollCount'
import EstablishmentRollCount from './interfaces/establishmentRollService/EstablishmentRollCount'
import nestRollBlocks, { splitRollBlocks } from './utils/nestRollBlocks'
import { Location } from '../data/interfaces/location'

const getTotals = (array: BlockRollCount[], figure: keyof BlockRollCount): number =>
array.reduce<number>((accumulator, block) => accumulator + ((block[figure] as number) || 0), 0)
Expand Down Expand Up @@ -66,10 +65,4 @@ export default class EstablishmentRollService {
parentLocationId: landingId,
})
}

public getLocationInfo(clientToken: string, locationId: string): Promise<Location> {
const prisonApi = this.prisonApiClientBuilder(clientToken)

return prisonApi.getLocation(locationId)
}
}
3 changes: 3 additions & 0 deletions server/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ContentfulService from './contentfulService'
import ComponentService from './componentService'
import EstablishmentRollService from './establishmentRollService'
import MovementsService from './movementsService'
import LocationService from './locationsService'

export const services = () => {
const {
Expand All @@ -29,6 +30,7 @@ export const services = () => {
const componentService = new ComponentService(componentApiClientBuilder)
const establishmentRollService = new EstablishmentRollService(prisonApiClientBuilder)
const movementsService = new MovementsService(prisonApiClientBuilder, prisonerSearchApiClientBuilder)
const locationsService = new LocationService(prisonApiClientBuilder)

const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
Expand Down Expand Up @@ -56,6 +58,7 @@ export const services = () => {
componentService,
establishmentRollService,
movementsService,
locationsService,
}
}

Expand Down
21 changes: 21 additions & 0 deletions server/services/locationService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import prisonApiClientMock from '../test/mocks/prisonApiClientMock'
import LocationService from './locationsService'
import { locationMock } from '../mocks/locationMock'

describe('establishmentRollService', () => {
let locationRollService: LocationService

beforeEach(() => {
locationRollService = new LocationService(() => prisonApiClientMock)
})

describe('getLocationInfo', () => {
it('should call api and return response', async () => {
prisonApiClientMock.getLocation = jest.fn().mockResolvedValueOnce(locationMock)

const locationInfo = await locationRollService.getLocationInfo('token', 'loc')

expect(locationInfo).toEqual(locationMock)
})
})
})
13 changes: 13 additions & 0 deletions server/services/locationsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { RestClientBuilder } from '../data'
import { PrisonApiClient } from '../data/interfaces/prisonApiClient'
import { Location } from '../data/interfaces/location'

export default class LocationService {
constructor(private readonly prisonApiClientBuilder: RestClientBuilder<PrisonApiClient>) {}

public async getLocationInfo(clientToken: string, locationId: string): Promise<Location> {
const prisonApi = this.prisonApiClientBuilder(clientToken)

return prisonApi.getLocation(locationId)
}
}
75 changes: 75 additions & 0 deletions server/services/movementsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,79 @@ describe('movementsService', () => {
expect(result).toEqual([])
})
})

describe('getOffendersCurrentlyOutOfLivingUnit', () => {
beforeEach(() => {
prisonApiClientMock.getPrisonersCurrentlyOutOfLivingUnit = jest.fn().mockResolvedValue(movementsOutMock)
prisonerSearchApiClientMock.getPrisonersById = jest.fn().mockResolvedValue(prisonerSearchMock)
prisonApiClientMock.getRecentMovements = jest.fn().mockResolvedValue(movementsRecentMock)
})

it('should search for prisoners for prisoners from getPrisonersCurrentlyOutOfLivingUnit', async () => {
const result = await movementsService.getOffendersCurrentlyOutOfLivingUnit('token', 'MDI')
expect(prisonerSearchApiClientMock.getPrisonersById).toHaveBeenCalledWith(['A1234AA', 'A1234AB'])
expect(prisonApiClientMock.getRecentMovements).toHaveBeenCalledWith(['A1234AA', 'A1234AB'])

expect(result).toEqual([
expect.objectContaining(prisonerSearchMock[0]),
expect.objectContaining(prisonerSearchMock[1]),
])
})

it('should decorate the alertFlags for each prisoner', async () => {
const result = await movementsService.getOffendersCurrentlyOutOfLivingUnit('token', 'MDI')

expect(result).toEqual([
expect.objectContaining({
alertFlags: [],
}),
expect.objectContaining({
alertFlags: [
{
alertCodes: ['HID'],
alertIds: ['HID'],
classes: 'alert-status alert-status--medical',
label: 'Hidden disability',
},
],
}),
])
})

it('should add the currentLocation from latest movement toCity', async () => {
const result = await movementsService.getOffendersCurrentlyOutOfLivingUnit('token', 'MDI')

expect(result).toEqual([
expect.objectContaining({
currentLocation: 'Sheffield',
}),
expect.objectContaining({
currentLocation: 'Doncaster',
}),
])
})

it('should add the movementComment from latest movement commentText', async () => {
const result = await movementsService.getOffendersCurrentlyOutOfLivingUnit('token', 'MDI')

expect(result).toEqual([
expect.objectContaining({
movementComment: 'Some Sheffield comment',
}),
expect.objectContaining({
movementComment: 'Some Doncaster comment',
}),
])
})

it('should return empty api if no currently out prisoners', async () => {
prisonApiClientMock.getPrisonersCurrentlyOutOfLivingUnit = jest.fn().mockResolvedValue([])

const result = await movementsService.getOffendersCurrentlyOutOfLivingUnit('token', 'LEI')
expect(prisonerSearchApiClientMock.getPrisonersById).toBeCalledTimes(0)
expect(prisonApiClientMock.getRecentMovements).toBeCalledTimes(0)

expect(result).toEqual([])
})
})
})
30 changes: 30 additions & 0 deletions server/services/movementsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,34 @@ export default class MovementsService {
}
})
}

public async getOffendersCurrentlyOutOfLivingUnit(
clientToken: string,
livingUnitId: string,
): Promise<(PrisonerWithAlerts & { currentLocation: string; movementComment?: string })[]> {
const prisonApi = this.prisonApiClientBuilder(clientToken)
const prisonerSearchClient = this.prisonerSearchClientBuilder(clientToken)

const outPrisoners = await prisonApi.getPrisonersCurrentlyOutOfLivingUnit(livingUnitId)
const prisonerNumbers = outPrisoners.map(prisoner => prisoner.offenderNo)
if (!outPrisoners || !outPrisoners?.length) return []

const [prisoners, recentMovements] = await Promise.all([
prisonerSearchClient.getPrisonersById(prisonerNumbers),
prisonApi.getRecentMovements(prisonerNumbers),
])

return prisoners
.sort((a, b) => a.lastName.localeCompare(b.lastName, 'en', { ignorePunctuation: true }))
.map(prisoner => {
const recentMovement = recentMovements.find(movement => movement.offenderNo === prisoner.prisonerNumber)

return {
...prisoner,
alertFlags: mapAlerts(prisoner),
currentLocation: recentMovement?.toCity,
movementComment: recentMovement?.commentText,
}
})
}
}
Loading

0 comments on commit eb23132

Please sign in to comment.