Skip to content

Commit

Permalink
ADJUST-21 create new rada (#8)
Browse files Browse the repository at this point in the history
* ADJUST-21 create new rada

* ADJUST1-24 linting.

* ADJUST1-24 linting.
  • Loading branch information
ldlharper authored Jun 16, 2023
1 parent 96e802f commit e547cad
Show file tree
Hide file tree
Showing 17 changed files with 462 additions and 9 deletions.
3 changes: 3 additions & 0 deletions assets/scss/application-ie8.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ $path: "/assets/images/";

@import 'govuk/all-ie8';


@import './components/adjustment-card';
@import './components/review-list';
@import './components/header-bar';
@import 'assets/scss/local';
1 change: 1 addition & 0 deletions assets/scss/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ $govuk-page-width: $moj-page-width;
@import 'moj/all';

@import './components/adjustment-card';
@import './components/review-list';
@import './components/header-bar';
@import 'assets/scss/local';
37 changes: 37 additions & 0 deletions assets/scss/components/_review-list.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.review-list {
.review-item {
display:block;
background-color: #f8f8f8;
padding:20px;
//border-left: 4px solid #6f72af;
border-left: 4px solid #ccc;
border-bottom:1px solid #ccc;
margin-bottom: 2em;

&.ng {
border-left: 4px solid #ccc;
}
a {
padding-left:10px;
}

ul {
margin-left:0;
padding-left:10px;
}

.govuk-table__cell, .govuk-table__header {
border-bottom: none;
padding-bottom: 0;
}

.govuk-button--secondary:visited, .govuk-button--secondary:active, .govuk-button--secondary:hover {
color: #0b0c0c !important;
}

.govuk-summary-list__value {
width:66%;
}

}
}
3 changes: 3 additions & 0 deletions server/@types/express/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { AdjustmentDetails } from '../adjustments/adjustmentsTypes'

export default {}

declare module 'express-session' {
// Declare that the session will potentially contain these additional fields
interface SessionData {
returnTo: string
nowInMinutes: number
adjustments: { string?: AdjustmentDetails[] }
}
}

Expand Down
2 changes: 1 addition & 1 deletion server/model/adjustmentTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const adjustmentTypes: AdjustmentType[] = [
url: 'unlawfully-at-large',
} as AdjustmentType,
{
value: 'RESTORED_ADDITIONAL_DAYS_AWARDED',
value: 'RESTORATION_OF_ADDITIONAL_DAYS_AWARDED',
text: 'Restore additional days awarded (RADA)',
shortText: 'RADA',
url: 'restored-additional-days',
Expand Down
2 changes: 1 addition & 1 deletion server/model/adjustmentsListModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default class AdjustmentsListViewModel {
'REMAND',
'TAGGED_BAIL',
'LAWFULLY_AT_LARGE',
'RESTORED_ADDITIONAL_DAYS_AWARDED',
'RESTORATION_OF_ADDITIONAL_DAYS_AWARDED',
'SPECIAL_REMISSION',
'TIME_SPENT_IN_CUSTODY_ABROAD',
].includes(it.value),
Expand Down
46 changes: 46 additions & 0 deletions server/model/restoredAdditionalDaysForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import dayjs from 'dayjs'
import { AdjustmentDetails } from '../@types/adjustments/adjustmentsTypes'
import { dateItems } from '../utils/utils'

export default class RestoredAdditionalDaysForm {
constructor(params: Partial<RestoredAdditionalDaysForm>) {
Object.assign(this, params)
}

'from-day': string

'from-month': string

'from-year': string

days: string

fromItems() {
return dateItems(this['from-year'], this['from-month'], this['from-day'])
}

static fromAdjustment(adjustment: AdjustmentDetails): RestoredAdditionalDaysForm {
return new RestoredAdditionalDaysForm({
'from-day': dayjs(adjustment.fromDate).get('date').toString(),
'from-month': (dayjs(adjustment.fromDate).get('month') + 1).toString(),
'from-year': dayjs(adjustment.fromDate).get('year').toString(),
days: adjustment.days.toString(),
})
}

toAdjustmentDetails(bookingId: number, nomsId: string): AdjustmentDetails {
return {
adjustmentType: 'RESTORATION_OF_ADDITIONAL_DAYS_AWARDED',
bookingId,
fromDate: dayjs()
.set('date', Number(this['from-day']))
.set('month', Number(this['from-month']) - 1)
.set('year', Number(this['from-year']))
.format('YYYY-MM-DD'),
toDate: null,
person: nomsId,
days: Number(this.days),
sentenceSequence: null,
}
}
}
11 changes: 11 additions & 0 deletions server/model/reviewModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PrisonApiPrisoner } from '../@types/prisonApi/prisonClientTypes'
import { AdjustmentDetails } from '../@types/adjustments/adjustmentsTypes'
import adjustmentTypes, { AdjustmentType } from './adjustmentTypes'

export default class ReviewModel {
constructor(public prisonerDetail: PrisonApiPrisoner, public adjustments: AdjustmentDetails[]) {}

adjustmentType(adjustment: AdjustmentDetails): AdjustmentType {
return adjustmentTypes.find(it => it.value === adjustment.adjustmentType)
}
}
137 changes: 137 additions & 0 deletions server/routes/adjustmentRoutes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import type { Express } from 'express'
import request from 'supertest'
import { appWithAllRoutes } from './testutils/appSetup'
import PrisonerService from '../services/prisonerService'
import AdjustmentsService from '../services/adjustmentsService'
import { PrisonApiPrisoner } from '../@types/prisonApi/prisonClientTypes'
import { AdjustmentDetails } from '../@types/adjustments/adjustmentsTypes'
import IdentifyRemandPeriodsService from '../services/identifyRemandPeriodsService'
import { Remand, RemandResult } from '../@types/identifyRemandPeriods/identifyRemandPeriodsTypes'
import AdjustmentsStoreService from '../services/adjustmentsStoreService'

jest.mock('../services/adjustmentsService')
jest.mock('../services/prisonerService')
jest.mock('../services/identifyRemandPeriodsService')
jest.mock('../services/adjustmentsStoreService')

const prisonerService = new PrisonerService(null) as jest.Mocked<PrisonerService>
const adjustmentsService = new AdjustmentsService() as jest.Mocked<AdjustmentsService>
const identifyRemandPeriodsService = new IdentifyRemandPeriodsService() as jest.Mocked<IdentifyRemandPeriodsService>
const adjustmentsStoreService = new AdjustmentsStoreService() as jest.Mocked<AdjustmentsStoreService>

const NOMS_ID = 'ABC123'

const stubbedPrisonerData = {
offenderNo: NOMS_ID,
firstName: 'Anon',
lastName: 'Nobody',
dateOfBirth: '24/06/2000',
bookingId: 12345,
} as PrisonApiPrisoner

const remandResult = {
chargeRemand: [],
sentenceRemand: [
{
days: 20,
} as Remand,
],
} as RemandResult

const radaAdjustment = {
adjustmentType: 'RESTORATION_OF_ADDITIONAL_DAYS_AWARDED',
toDate: null,
fromDate: '2023-04-05',
person: 'ABC123',
days: 24,
bookingId: 12345,
sentenceSequence: null,
} as AdjustmentDetails

let app: Express

beforeEach(() => {
app = appWithAllRoutes({
services: { prisonerService, adjustmentsService, identifyRemandPeriodsService, adjustmentsStoreService },
})
})

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

describe('Adjustment routes tests', () => {
it('GET /{nomsId}', () => {
prisonerService.getPrisonerDetail.mockResolvedValue(stubbedPrisonerData)
adjustmentsService.findByPerson.mockResolvedValue([{ id: '1', adjustment: radaAdjustment }])
identifyRemandPeriodsService.calculateRelevantRemand.mockResolvedValue(remandResult)
return request(app)
.get(`/${NOMS_ID}`)
.expect('Content-Type', /html/)
.expect(res => {
expect(res.text).toContain('Anon')
expect(res.text).toContain('Nobody')
expect(res.text).toContain('Nobody may have 20 days remand')
expect(res.text).toContain('24')
})
})

it('GET /{nomsId}/restored-additional-days/add', () => {
prisonerService.getPrisonerDetail.mockResolvedValue(stubbedPrisonerData)
return request(app)
.get(`/${NOMS_ID}/restored-additional-days/add`)
.expect('Content-Type', /html/)
.expect(res => {
expect(res.text).toContain('Anon')
expect(res.text).toContain('Nobody')
expect(res.text).toContain('Date of days restored')
expect(res.text).toContain('Continue')
})
})

it('POST /{nomsId}/restored-additional-days/add', () => {
prisonerService.getPrisonerDetail.mockResolvedValue(stubbedPrisonerData)
return request(app)
.post(`/${NOMS_ID}/restored-additional-days/add`)
.send({ 'from-day': '5', 'from-month': '4', 'from-year': '2023', days: 24 })
.type('form')
.expect(302)
.expect('Location', `/${NOMS_ID}/review`)
.expect(res => {
expect(adjustmentsStoreService.store.mock.calls).toHaveLength(1)
expect(adjustmentsStoreService.store.mock.calls[0][2]).toStrictEqual(radaAdjustment)
})
})

it('GET /{nomsId}/review', () => {
prisonerService.getPrisonerDetail.mockResolvedValue(stubbedPrisonerData)
adjustmentsStoreService.get.mockReturnValue([radaAdjustment])

return request(app)
.get(`/${NOMS_ID}/review`)
.expect('Content-Type', /html/)
.expect(res => {
expect(res.text).toContain('Anon')
expect(res.text).toContain('Adjustment type')
expect(res.text).toContain('Restore additional days awarded (RADA)')
expect(res.text).toContain('Date of days restored')
expect(res.text).toContain('05 April 2023')
expect(res.text).toContain('Number of days')
expect(res.text).toContain('24')
expect(res.text).toContain('Accept and save')
})
})

it('POST /{nomsId}/review', () => {
prisonerService.getPrisonerDetail.mockResolvedValue(stubbedPrisonerData)
adjustmentsStoreService.get.mockReturnValue([radaAdjustment])
return request(app)
.post(`/${NOMS_ID}/review`)
.expect(302)
.expect('Location', `/${NOMS_ID}`)
.expect(res => {
expect(adjustmentsService.create.mock.calls).toHaveLength(1)
expect(adjustmentsService.create.mock.calls[0][0]).toStrictEqual(radaAdjustment)
})
})
})
59 changes: 57 additions & 2 deletions server/routes/adjustmentRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import AdjustmentsService from '../services/adjustmentsService'
import AdjustmentsListViewModel, { Message } from '../model/adjustmentsListModel'
import config from '../config'
import AdditionalDaysModel from '../model/additionalDaysModel'
import RestoredAdditionalDaysForm from '../model/restoredAdditionalDaysForm'
import ReviewModel from '../model/reviewModel'
import { AdjustmentDetails } from '../@types/adjustments/adjustmentsTypes'
import AdjustmentsStoreService from '../services/adjustmentsStoreService'

export default class AdjustmentRoutes {
constructor(
private readonly prisonerService: PrisonerService,
private readonly adjustmentsService: AdjustmentsService,
private readonly identifyRemandPeriodsService: IdentifyRemandPeriodsService,
private readonly adjustmentsStoreService: AdjustmentsStoreService,
) {}

public entry: RequestHandler = async (req, res): Promise<void> => {
Expand Down Expand Up @@ -40,15 +45,15 @@ export default class AdjustmentRoutes {
const { caseloads, token } = res.locals.user
const { nomsId } = req.params
const prisonerDetail = await this.prisonerService.getPrisonerDetail(nomsId, caseloads, token)
const adjustments = await this.adjustmentsService.findByPersonAndSource(nomsId, 'DPS', token)
const adjustments = await this.adjustmentsService.findByPerson(nomsId, token)
const relevantRemand = await this.identifyRemandPeriodsService.calculateRelevantRemand(nomsId, token)
const message = req.flash('message')
return res.render('pages/adjustments/list', {
model: new AdjustmentsListViewModel(
prisonerDetail,
adjustments,
relevantRemand.sentenceRemand,
message[0] && (JSON.parse(message[0]) as Message),
message && message[0] && (JSON.parse(message[0]) as Message),
),
})
}
Expand All @@ -73,4 +78,54 @@ export default class AdjustmentRoutes {
model: new AdditionalDaysModel(prisonerDetail, adjudications),
})
}

public restoredAdditionalDays: RequestHandler = async (req, res): Promise<void> => {
const { caseloads, token } = res.locals.user
const { nomsId } = req.params
const prisonerDetail = await this.prisonerService.getPrisonerDetail(nomsId, caseloads, token)

return res.render('pages/adjustments/restoredAdditionalDays', {
model: { prisonerDetail, form: new RestoredAdditionalDaysForm({}) },
})
}

public submitRestoredAdditionalDays: RequestHandler = async (req, res): Promise<void> => {
const { caseloads, token } = res.locals.user
const { nomsId } = req.params

const prisonerDetail = await this.prisonerService.getPrisonerDetail(nomsId, caseloads, token)
const adjustmentForm = new RestoredAdditionalDaysForm(req.body)
const adjustment = adjustmentForm.toAdjustmentDetails(prisonerDetail.bookingId, nomsId)

this.adjustmentsStoreService.store(req, nomsId, adjustment)

return res.redirect(`/${nomsId}/review`)
}

public review: RequestHandler = async (req, res): Promise<void> => {
const { caseloads, token } = res.locals.user
const { nomsId } = req.params
const prisonerDetail = await this.prisonerService.getPrisonerDetail(nomsId, caseloads, token)

if (this.adjustmentsStoreService.get(req, nomsId)?.length) {
return res.render('pages/adjustments/review', {
model: new ReviewModel(prisonerDetail, this.adjustmentsStoreService.get(req, nomsId)),
})
}
return res.redirect(`/${nomsId}`)
}

public submitReview: RequestHandler = async (req, res): Promise<void> => {
const { token } = res.locals.user
const { nomsId } = req.params

if (this.adjustmentsStoreService.get(req, nomsId)?.length) {
await Promise.all(
this.adjustmentsStoreService
.get(req, nomsId)
.map((it: AdjustmentDetails) => this.adjustmentsService.create(it, token)),
)
}
return res.redirect(`/${nomsId}`)
}
}
10 changes: 8 additions & 2 deletions server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default function routes(service: Services): Router {
service.prisonerService,
service.adjustmentsService,
service.identifyRemandPeriodsService,
service.adjustmentsStoreService,
)
const adjustmentTestRoutes = new AdjustmentTestRoutes(service.prisonerService, service.adjustmentsService)

Expand All @@ -25,9 +26,14 @@ export default function routes(service: Services): Router {
get('/', adjustmentRoutes.entry)
get('/:nomsId/start', adjustmentRoutes.start)
get('/:nomsId', adjustmentRoutes.list)
get('/:nomsId/remand', adjustmentRoutes.remand)
get('/:nomsId/success', adjustmentRoutes.success)
get('/:nomsId/additional-days', adjustmentRoutes.additionalDays)
get('/:nomsId/review', adjustmentRoutes.review)
post('/:nomsId/review', adjustmentRoutes.submitReview)

get('/:nomsId/remand', adjustmentRoutes.remand)
get('/:nomsId/additional-days/add', adjustmentRoutes.additionalDays)
get('/:nomsId/restored-additional-days/add', adjustmentRoutes.restoredAdditionalDays)
post('/:nomsId/restored-additional-days/add', adjustmentRoutes.submitRestoredAdditionalDays)

get('/test/:nomsId', adjustmentTestRoutes.list)
get('/test/:nomsId/create', adjustmentTestRoutes.create)
Expand Down
Loading

0 comments on commit e547cad

Please sign in to comment.