Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PI-2656 Add section for tier history #208

Merged
merged 1 commit into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ npm install

### Run the service

To run the service locally, with an in-memory session store and local user account, run:
To run the service locally with an in-memory session store, a local user account, and dependencies running in WireMock:

```shell
npm run start:dev
Expand Down Expand Up @@ -102,4 +102,4 @@ npm run int-test-ui
### Dependency Checks

The template project has implemented some scheduled checks to ensure that key dependencies are kept up to date.
If these are not desired in the cloned project, remove references to `check_outdated` job from `.circleci/config.yml`
If these are not desired in the cloned project, remove references to `check_outdated` job from `.circleci/config.yml`
1 change: 1 addition & 0 deletions helm_deploy/hmpps-tier-ui/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ generic-service:
HMPPS_AUTH_ENABLED: "true"
TOKEN_VERIFICATION_ENABLED: "true"
APPLICATIONINSIGHTS_CONNECTION_STRING: "InstrumentationKey=$(APPINSIGHTS_INSTRUMENTATIONKEY);IngestionEndpoint=https://northeurope-0.in.applicationinsights.azure.com/;LiveEndpoint=https://northeurope.livediagnostics.monitor.azure.com/"
AUDIT_ENABLED: "true"
AUDIT_SERVICE_NAME: "hmpps-tier-ui"
AUDIT_SQS_REGION: "eu-west-2"

Expand Down
45 changes: 38 additions & 7 deletions integration_tests/e2e/case.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ import CasePage from '../pages/case'
import Page from '../pages/page'

context('Case view screen', () => {
it('basic case', () => {
cy.visit('/case/X000001')
it('displays case details header', () => {
cy.visit('/case/A000001')
const page = Page.verifyOnPage(CasePage)
page.headerCrn().should('contain.text', 'X000001')
page.headerCrn().should('contain.text', 'A000001')
page.headerSex().should('contain.text', 'Female')
page.headerTier().should('contain.text', 'B2')
page.warnings().should('not.exist')
})

it('displays protect section', () => {
cy.visit('/case/A000001')
const page = Page.verifyOnPage(CasePage)
cy.get(page.protectTableRow(1))
.should('contain.text', 'Medium RoSH')
.should('contain.text', '+10')
Expand All @@ -31,7 +36,11 @@ context('Case view screen', () => {
.should('contain.text', 'Impulsivity and temper control')
.should('contain.text', '+2')
cy.get(page.protectTableRow(10)).should('contain.text', 'Total').should('contain.text', '33')
})

it('displays change section', () => {
cy.visit('/case/A000001')
const page = Page.verifyOnPage(CasePage)
cy.get(page.changeTableRow(1))
.should('contain.text', 'OGRS')
.should('contain.text', '56.7%')
Expand All @@ -52,16 +61,38 @@ context('Case view screen', () => {
cy.get(page.changeTableRow(8)).should('contain.text', 'Total').should('contain.text', '14')
})

it('displays history', () => {
cy.visit('/case/A000001')
const page = Page.verifyOnPage(CasePage)
cy.get(page.timelineItem(1))
.should('contain.text', 'B2')
.should('contain.text', '7 December 2023 at 12:05 PM')
.should('contain.text', 'A breach was raised')
cy.get(page.timelineItem(2))
.should('contain.text', 'C2')
.should('contain.text', '13 October 2023 at 11:10 AM')
.should('contain.text', "A registration of type 'MAPPA' was added")
cy.get(page.timelineItem(3))
.should('contain.text', 'D2')
.should('contain.text', '28 July 2023 at 5:09 PM')
.should('contain.text', 'An OASys assessment was produced')
cy.get(page.timelineItem(4)).should('contain.text', 'C0').should('contain.text', '7 October 2023 at 12:12 PM')
cy.get(page.timelineItem(5))
.should('contain.text', 'D0')
.should('contain.text', '7 October 2023 at 9:34 AM')
.should('contain.text', 'The case was created')
})

it('handles multiple rosh registrations', () => {
cy.visit('/case/X000002')
cy.visit('/case/A000002')
const page = Page.verifyOnPage(CasePage)
page.warnings().should('contain.text', 'Multiple RoSH registrations were found in Delius')
page.protectTable().should('not.contain.text', 'High RoSH')
cy.get(page.protectTableRow(1)).should('contain.text', 'Medium RoSH')
})

it('handles case with no mandate for change', () => {
cy.visit('/case/X000003')
cy.visit('/case/A000003')
const page = Page.verifyOnPage(CasePage)
page.warnings().should('not.exist')
page.changeTable().should('not.exist')
Expand All @@ -72,15 +103,15 @@ context('Case view screen', () => {
})

it('handles case with no assessment', () => {
cy.visit('/case/X000004')
cy.visit('/case/A000004')
const page = Page.verifyOnPage(CasePage)
page.warnings().should('not.exist')
page.changeTable().should('not.exist')
cy.get('body').should('contain.text', 'This case has not had an OASys assessment in the last 55 weeks')
})

it('handles case with limited access', () => {
cy.visit('/case/X000005')
cy.visit('/case/A000005')
cy.get('body').should('contain.text', 'You are not authorised to view this case')
})
})
2 changes: 2 additions & 0 deletions integration_tests/pages/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ export default class CasePage extends Page {
changeTable = (): PageElement => cy.get('[data-qa=change-table]')

changeTableRow = (row: number): string => `[data-qa=change-table] tbody tr:nth-child(${row})`

timelineItem = (row: number): string => `.moj-timeline .moj-timeline__item:nth-child(${row})`
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"watch-sass": "npm run compile-sass -- --watch",
"build": "npm run compile-sass && tsc && npm run copy-views",
"start": "node $NODE_OPTIONS dist/server.js | bunyan -o short",
"start:dev": "concurrently -k -p \"[{name}]\" -n \"Views,TypeScript,Node,Sass\" -c \"yellow.bold,cyan.bold,green.bold,blue.bold\" \"npm run watch-views\" \"npm run watch-ts\" \"npm run watch-node\" \"npm run watch-sass\"",
"start:dev": "concurrently -k -p \"[{name}]\" -n \"WireMock,Views,TypeScript,Node,Sass\" -c \"magenta.bold,yellow.bold,cyan.bold,green.bold,blue.bold\" \"npm run wiremock\" \"npm run watch-views\" \"npm run watch-ts\" \"npm run watch-node\" \"npm run watch-sass\"",
"start-feature": "concurrently -k -p \"[{name}]\" -n \"WireMock,Node\" -c \"magenta.bold,green.bold\" \"npm run wiremock\" \"npm run start-node-feature\"",
"start-feature:dev": "concurrently -k -p \"[{name}]\" -n \"WireMock,Views,TypeScript,Node,Sass\" -c \"magenta.bold,yellow.bold,cyan.bold,green.bold,blue.bold\" \"npm run wiremock\" \"npm run watch-views\" \"npm run watch-ts\" \"npm run watch-node-feature\" \"npm run watch-sass\"",
"start-node-feature": "export $(cat feature.env) && node $NODE_DEBUG_OPTION dist/server.js | bunyan -o short",
Expand Down
17 changes: 10 additions & 7 deletions server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,14 @@ export default {
secret: get('SESSION_SECRET', 'app-insecure-default-session', requiredInProduction),
expiryMinutes: Number(get('WEB_SESSION_TIMEOUT_IN_MINUTES', 120)),
},
audit: {
enabled: get('AUDIT_ENABLED', 'false') === 'true',
},
apis: {
hmppsAuth: {
enabled: get('HMPPS_AUTH_ENABLED', 'false', requiredInProduction) === 'true',
url: get('HMPPS_AUTH_URL', 'http://localhost:9090/auth', requiredInProduction),
externalUrl: get('HMPPS_AUTH_EXTERNAL_URL', get('HMPPS_AUTH_URL', 'http://localhost:9090/auth')),
url: get('HMPPS_AUTH_URL', 'http://localhost:9091/auth', requiredInProduction),
externalUrl: get('HMPPS_AUTH_EXTERNAL_URL', get('HMPPS_AUTH_URL', 'http://localhost:9091/auth')),
timeout: {
response: Number(get('HMPPS_AUTH_TIMEOUT_RESPONSE', 10000)),
deadline: Number(get('HMPPS_AUTH_TIMEOUT_DEADLINE', 10000)),
Expand All @@ -65,15 +68,15 @@ export default {
systemClientSecret: get('SYSTEM_CLIENT_SECRET', 'clientsecret', requiredInProduction),
},
manageUsersApi: {
url: get('MANAGE_USERS_API_URL', 'http://localhost:9091', requiredInProduction),
url: get('MANAGE_USERS_API_URL', 'http://localhost:9091/manage-users-api', requiredInProduction),
timeout: {
response: Number(get('MANAGE_USERS_API_TIMEOUT_RESPONSE', 10000)),
deadline: Number(get('MANAGE_USERS_API_TIMEOUT_DEADLINE', 10000)),
},
agent: new AgentConfig(Number(get('MANAGE_USERS_API_TIMEOUT_RESPONSE', 10000))),
},
tokenVerification: {
url: get('TOKEN_VERIFICATION_API_URL', 'http://localhost:8100', requiredInProduction),
url: get('TOKEN_VERIFICATION_API_URL', 'http://localhost:9091/token-verification-api', requiredInProduction),
timeout: {
response: Number(get('TOKEN_VERIFICATION_API_TIMEOUT_RESPONSE', 5000)),
deadline: Number(get('TOKEN_VERIFICATION_API_TIMEOUT_DEADLINE', 5000)),
Expand All @@ -82,23 +85,23 @@ export default {
enabled: get('TOKEN_VERIFICATION_ENABLED', 'false') === 'true',
},
deliusIntegration: {
url: get('DELIUS_INTEGRATION_URL', 'http://localhost:8100', requiredInProduction),
url: get('DELIUS_INTEGRATION_URL', 'http://localhost:9091/delius', requiredInProduction),
timeout: {
response: Number(get('DELIUS_INTEGRATION_TIMEOUT_RESPONSE', 5000)),
deadline: Number(get('DELIUS_INTEGRATION_TIMEOUT_DEADLINE', 5000)),
},
agent: new AgentConfig(Number(get('DELIUS_INTEGRATION_TIMEOUT_RESPONSE', 5000))),
},
tierApi: {
url: get('TIER_API_URL', 'http://localhost:8100', requiredInProduction),
url: get('TIER_API_URL', 'http://localhost:9091/tier', requiredInProduction),
timeout: {
response: Number(get('TIER_API_TIMEOUT_RESPONSE', 5000)),
deadline: Number(get('TIER_API_TIMEOUT_DEADLINE', 5000)),
},
agent: new AgentConfig(Number(get('TIER_API_TIMEOUT_RESPONSE', 5000))),
},
arnsApi: {
url: get('ARNS_API_URL', 'http://localhost:8100', requiredInProduction),
url: get('ARNS_API_URL', 'http://localhost:9091/arns', requiredInProduction),
timeout: {
response: Number(get('ARNS_API_TIMEOUT_RESPONSE', 10000)),
deadline: Number(get('ARNS_API_TIMEOUT_DEADLINE', 10000)),
Expand Down
5 changes: 5 additions & 0 deletions server/data/tierApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export default class TierApiClient extends RestClient {
return this.get({ path: `/crn/${crn}/tier/details` })
}

async getHistory(crn: string): Promise<TierCalculation[]> {
return (await this.get({ path: `/crn/${crn}/tier/history`, handle404: true })) ?? []
}

async getTierCounts(): Promise<TierCount[]> {
return this.get({ path: '/tier-counts' })
}
Expand All @@ -36,6 +40,7 @@ export interface TierCalculation {
change: TierLevel
calculationVersion: string
}
changeReason?: string
}

export interface TierLevel {
Expand Down
25 changes: 16 additions & 9 deletions server/routes/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ArnsApiClient, { OASysTierInputs } from '../data/arnsApiClient'
import TierApiClient, { TierLevel } from '../data/tierApiClient'
import { needsRow, row, Table } from '../utils/table'
import { Abbreviations, ComplexityFactors, mappaDescription } from '../utils/mappings'
import config from '../config'

export default function caseRoutes(router: Router, { hmppsAuthClient }: Services) {
const get = (path: string | string[], handler: RequestHandler) => router.get(path, asyncMiddleware(handler))
Expand All @@ -25,20 +26,23 @@ export default function caseRoutes(router: Router, { hmppsAuthClient }: Services
const tierClient = new TierApiClient(token)
const arnsClient = new ArnsApiClient(token)

await auditService.sendAuditMessage({
action: 'VIEW_TIER_INFORMATION',
who: res.locals.user.username,
subjectId: crn,
subjectType: 'CRN',
correlationId: v4(),
service: 'hmpps-tier-ui',
})
if (config.audit.enabled) {
await auditService.sendAuditMessage({
action: 'VIEW_TIER_INFORMATION',
who: res.locals.user.username,
subjectId: crn,
subjectType: 'CRN',
correlationId: v4(),
service: 'hmpps-tier-ui',
})
}

const [personalDetails, deliusInputs, oasysInputs, tierCalculation] = await Promise.all([
const [personalDetails, deliusInputs, oasysInputs, tierCalculation, history] = await Promise.all([
deliusClient.getPersonalDetails(crn),
deliusClient.getTierDetails(crn),
arnsClient.getTierAssessmentInfo(crn),
tierClient.getCalculationDetails(crn),
tierClient.getHistory(crn),
])

const warnings: string[] = []
Expand All @@ -52,6 +56,9 @@ export default function caseRoutes(router: Router, { hmppsAuthClient }: Services
tierCalculation,
protectTable,
changeTable,
history: history.filter(
(item, index) => index === history.length - 1 || item.tierScore !== history[index + 1].tierScore,
),
warnings: warnings.map(warning => ({ text: warning })),
})
})
Expand Down
3 changes: 2 additions & 1 deletion server/utils/nunjucksSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import path from 'path'
import nunjucks from 'nunjucks'
import express from 'express'
import { formatDistance, parseISO } from 'date-fns'
import { format, formatDistance, parseISO } from 'date-fns'
import { initialiseName } from './utils'
import { ApplicationInfo } from '../applicationInfo'
import config from '../config'
Expand Down Expand Up @@ -46,5 +46,6 @@ export default function nunjucksSetup(app: express.Express, applicationInfo: App

njkEnv.addFilter('initialiseName', initialiseName)
njkEnv.addFilter('formatNumber', (num: number) => num.toLocaleString('en-GB'))
njkEnv.addFilter('formatDate', (date: string) => format(parseISO(date), "d MMMM yyyy' at 'h:mm a"))
njkEnv.addFilter('ago', (date: string) => formatDistance(parseISO(date), new Date(), { addSuffix: true }))
}
Loading