diff --git a/server/testutils/factories/index.ts b/server/testutils/factories/index.ts index 80a253816..843d1e078 100644 --- a/server/testutils/factories/index.ts +++ b/server/testutils/factories/index.ts @@ -55,6 +55,7 @@ import premisesSummaryFactory from './premisesSummary' import prisonCaseNotesFactory from './prisonCaseNotes' import probationRegionFactory from './probationRegion' import referenceDataFactory from './referenceData' +import referralHistoryDomainEventNoteFactory from './referralHistoryDomainEventNote' import referralHistorySystemNoteFactory from './referralHistorySystemNote' import referralHistoryUserNoteFactory from './referralHistoryUserNote' import risksFactory from './risks' @@ -126,6 +127,7 @@ export { prisonCaseNotesFactory, probationRegionFactory, referenceDataFactory, + referralHistoryDomainEventNoteFactory, referralHistorySystemNoteFactory, referralHistoryUserNoteFactory, restrictedPersonFactory, diff --git a/server/testutils/factories/referralHistoryDomainEventNote.ts b/server/testutils/factories/referralHistoryDomainEventNote.ts new file mode 100644 index 000000000..ceeb8b8ac --- /dev/null +++ b/server/testutils/factories/referralHistoryDomainEventNote.ts @@ -0,0 +1,26 @@ +import type { ReferralHistoryDomainEventNote, ReferralHistoryNoteMessageDetails } from '@approved-premises/api' +import { fakerEN_GB as faker } from '@faker-js/faker' +import { Factory } from 'fishery' +import { DateFormats } from '../../utils/dateUtils' + +export default Factory.define(() => ({ + id: faker.string.uuid(), + createdByUserName: faker.person.fullName(), + createdAt: DateFormats.dateObjToIsoDate(faker.date.past()), + message: '', + messageDetails: { + domainEvent: { + id: faker.string.uuid(), + eventType: 'accommodation.cas3.assessment.updated', + timestamp: DateFormats.dateObjToIsoDate(faker.date.past()), + updatedFields: [ + { + fieldName: 'releaseDate', + updatedTo: '2025-09-02', + updatedFrom: '2123-09-02', + }, + ], + }, + } as ReferralHistoryNoteMessageDetails, + type: 'domainEvent', +})) diff --git a/server/utils/assessmentUtils.test.ts b/server/utils/assessmentUtils.test.ts index cea87411b..b10277d7e 100644 --- a/server/utils/assessmentUtils.test.ts +++ b/server/utils/assessmentUtils.test.ts @@ -1,5 +1,9 @@ import { AssessmentSearchApiStatus } from '@approved-premises/ui' -import type { ReferralHistoryNoteMessageDetails } from '@approved-premises/api' +import { + ReferralHistoryDomainEventNote as DomainEventNote, + ReferralHistoryNoteMessageDetails, +} from '@approved-premises/api' +import { fakerEN_GB as faker } from '@faker-js/faker' import paths from '../paths/temporary-accommodation/manage' import { applicationFactory, @@ -8,6 +12,7 @@ import { personFactory, placeContextFactory, referenceDataFactory, + referralHistoryDomainEventNoteFactory, referralHistorySystemNoteFactory, referralHistoryUserNoteFactory, restrictedPersonFactory, @@ -22,6 +27,7 @@ import { insertUpdateDateError, pathFromStatus, referralRejectionReasonIsOther, + renderDomainEventNote, renderNote, renderSystemNote, statusChangeMessage, @@ -30,6 +36,7 @@ import { import * as assessmentUtils from './assessmentUtils' import * as viewUtils from './viewUtils' import { addPlaceContext, addPlaceContextFromAssessmentId, createPlaceContext } from './placeUtils' +import { DateFormats } from './dateUtils' jest.mock('./userUtils') jest.mock('./placeUtils') @@ -295,6 +302,25 @@ describe('assessmentUtils', () => { expect(assessmentUtils.renderSystemNote).toHaveBeenCalledWith(note) }) + it('renders the contents of a domain event note with details', () => { + jest.spyOn(assessmentUtils, 'renderDomainEventNote').mockReturnValue('formatted message') + const note: DomainEventNote = { + id: faker.string.uuid(), + createdByUserName: faker.person.fullName(), + createdAt: DateFormats.dateObjToIsoDate(faker.date.past()), + type: 'domainEvent', + message: '', + messageDetails: { + domainEvent: { foo: 'bar' }, + } as ReferralHistoryNoteMessageDetails, + } + + const result = renderNote(note) + + expect(result).toEqual('formatted message') + expect(assessmentUtils.renderDomainEventNote).toHaveBeenCalledWith(note.messageDetails) + }) + it('returns undefined for a system note with no message details', () => { const note = referralHistorySystemNoteFactory.build({ message: '', @@ -344,6 +370,52 @@ describe('assessmentUtils', () => { }) }) + describe('renderDomainEventDetails', () => { + describe('when "Accommodation required from date" has been updated', () => { + it('returns HTML for a standard rejection reason', () => { + const messageDetails: DomainEventNote['messageDetails'] = { + domainEvent: { + eventType: 'accommodation.cas3.assessment.updated', + updatedFields: [ + { + fieldName: 'accommodationRequiredFromDate', + updatedTo: '2125-11-01', + updatedFrom: '2125-01-31', + }, + ], + }, + } + + const result = renderDomainEventNote(messageDetails) + + expect(result).toEqual( + '

Accommodation required from date was changed from 31 January 2125 to 1 November 2125

', + ) + }) + }) + + describe('when "Release date" has been updated', () => { + it('returns HTML for a standard rejection reason', () => { + const messageDetails: DomainEventNote['messageDetails'] = { + domainEvent: { + eventType: 'accommodation.cas3.assessment.updated', + updatedFields: [ + { + fieldName: 'releaseDate', + updatedTo: '2125-11-01', + updatedFrom: '2125-01-31', + }, + ], + }, + } + + const result = renderDomainEventNote(messageDetails) + + expect(result).toEqual('

Release date was changed from 31 January 2125 to 1 November 2125

') + }) + }) + }) + describe('timelineItems', () => { it('returns a notes in a format compatible with the MoJ timeline component', () => { const userNote1 = referralHistoryUserNoteFactory.build({ @@ -365,7 +437,43 @@ describe('assessmentUtils', () => { category: 'ready_to_place', }) - const notes = [systemNote1, systemNote2, userNote2, userNote1] + const domainEventNote1 = referralHistoryDomainEventNoteFactory.build({ + createdByUserName: 'SOME USER', + createdAt: '2024-06-02', + messageDetails: { + domainEvent: { + eventType: 'accommodation.cas3.assessment.updated', + timestamp: DateFormats.dateObjToIsoDate(faker.date.past()), + updatedFields: [ + { + fieldName: 'accommodationRequiredFromDate', + updatedTo: '2025-09-02', + updatedFrom: '2123-09-02', + }, + ], + }, + }, + }) + + const domainEventNote2 = referralHistoryDomainEventNoteFactory.build({ + createdByUserName: 'SOME USER', + createdAt: '2024-06-01', + messageDetails: { + domainEvent: { + eventType: 'accommodation.cas3.assessment.updated', + timestamp: DateFormats.dateObjToIsoDate(faker.date.past()), + updatedFields: [ + { + fieldName: 'releaseDate', + updatedTo: '2025-09-02', + updatedFrom: '2123-09-02', + }, + ], + }, + }, + }) + + const notes = [systemNote1, systemNote2, userNote2, userNote1, domainEventNote1, domainEventNote2] const assessment = assessmentFactory.build({ referralHistoryNotes: notes }) const userNoteHtml = 'some formatted html' @@ -373,6 +481,32 @@ describe('assessmentUtils', () => { const result = timelineItems(assessment) expect(result).toEqual([ + { + label: { + text: 'Accommodation required from date updated', + }, + html: '

Accommodation required from date was changed from 2 September 2123 to 2 September 2025

', + datetime: { + timestamp: domainEventNote1.createdAt, + type: 'datetime', + }, + byline: { + text: 'Some User', + }, + }, + { + label: { + text: 'Release date updated', + }, + html: '

Release date was changed from 2 September 2123 to 2 September 2025

', + datetime: { + timestamp: domainEventNote2.createdAt, + type: 'datetime', + }, + byline: { + text: 'Some User', + }, + }, { label: { text: 'Referral marked as ready to place', diff --git a/server/utils/assessmentUtils.ts b/server/utils/assessmentUtils.ts index 4103e4dfd..4f64b9edd 100644 --- a/server/utils/assessmentUtils.ts +++ b/server/utils/assessmentUtils.ts @@ -3,6 +3,7 @@ import { AssessmentSortField, TemporaryAccommodationAssessmentStatus as AssessmentStatus, TemporaryAccommodationAssessmentSummary as AssessmentSummary, + ReferralHistoryDomainEventNote as DomainEventNote, SortDirection, ReferralHistorySystemNote as SystemNote, ReferralHistoryUserNote as UserNote, @@ -135,12 +136,14 @@ export const assessmentActions = (assessment: Assessment) => { return items } -const timeLineLabelText = (note: UserNote | SystemNote): string => { +const timeLineLabelText = (note: UserNote | SystemNote | DomainEventNote): string => { switch (note.type) { + case 'domainEvent': + return domainEventLabelText(note.messageDetails as DomainEventNote['messageDetails']) case 'user': return 'Note' case 'system': - return systemNoteLabelText(note) + return systemNoteLabelText(note as SystemNote) default: throw new Error(`Unknown type of timeline item - ${note.type}`) } @@ -210,6 +213,10 @@ export const statusChangeMessage = (assessmentId: string, status: AssessmentStat } } +const isDomainEventNote = (note: UserNote | SystemNote | DomainEventNote) => { + return Boolean(note.type === 'domainEvent') +} + const isUserNote = (note: UserNote | SystemNote): note is UserNote => { return Boolean(note.type === 'user') } @@ -222,6 +229,18 @@ const isSystemNoteWithDetails = (note: UserNote | SystemNote): note is SystemNot return Boolean(isSystemNote(note) && note.messageDetails) } +export const renderDomainEventNote = (note: DomainEventNote['messageDetails']): TimelineItem['html'] | never => { + const updatedField = note.domainEvent.updatedFields[0] + switch (updatedField.fieldName) { + case 'accommodationRequiredFromDate': + return `

Accommodation required from date was changed from ${DateFormats.isoDateToUIDate(updatedField.updatedFrom)} to ${DateFormats.isoDateToUIDate(updatedField.updatedTo)}

` + case 'releaseDate': + return `

Release date was changed from ${DateFormats.isoDateToUIDate(updatedField.updatedFrom)} to ${DateFormats.isoDateToUIDate(updatedField.updatedTo)}

` + default: + return assertUnreachable(updatedField.fieldName as never) + } +} + export const renderSystemNote = (note: SystemNote): TimelineItem['html'] => { const reason = note.messageDetails.rejectionReasonDetails || note.messageDetails.rejectionReason const isWithdrawn = note.messageDetails.isWithdrawn ? 'Yes' : 'No' @@ -231,7 +250,7 @@ export const renderSystemNote = (note: SystemNote): TimelineItem['html'] => { return formatLines(lines.join('\n\n')) } -export const renderNote = (note: UserNote | SystemNote): TimelineItem['html'] => { +export const renderNote = (note: UserNote | SystemNote | DomainEventNote): TimelineItem['html'] => { if (isSystemNoteWithDetails(note)) { return renderSystemNote(note) } @@ -240,6 +259,10 @@ export const renderNote = (note: UserNote | SystemNote): TimelineItem['html'] => return formatLines(note.message) } + if (isDomainEventNote(note)) { + return renderDomainEventNote((note as DomainEventNote).messageDetails) + } + return undefined } @@ -261,6 +284,18 @@ const systemNoteLabelText = (note: SystemNote): TimelineItem['label']['text'] => return assertUnreachable(note.category) } } + +const domainEventLabelText = (note: DomainEventNote['messageDetails']): TimelineItem['label']['text'] => { + switch (note.domainEvent.updatedFields[0].fieldName) { + case 'accommodationRequiredFromDate': + return 'Accommodation required from date updated' + case 'releaseDate': + return 'Release date updated' + default: + return assertUnreachable(note.domainEvent.updatedFields[0].fieldName as never) + } +} + export const createTableHeadings = ( currentSortBy: AssessmentSortField, sortIsAscending: boolean,