diff --git a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts index 9973b63c7c54..cbab6e505062 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts @@ -39,16 +39,13 @@ import { } from '@island.is/judicial-system/formatters' import type { User } from '@island.is/judicial-system/types' import { - CaseAppealDecision, CaseAppealRulingDecision, - CaseAppealState, CaseDecision, CaseState, CaseTransition, CaseType, indictmentCases, investigationCases, - isIndictmentCase, isRestrictionCase, restrictionCases, UserRole, @@ -305,117 +302,7 @@ export class CaseController { ): Promise { this.logger.debug(`Transitioning case ${caseId}`) - let update: UpdateCase = transitionCase( - transition.transition, - theCase.type, - theCase.state, - theCase.appealState, - ) - - switch (transition.transition) { - case CaseTransition.DELETE: - update.parentCaseId = null - break - case CaseTransition.SUBMIT: - if (isIndictmentCase(theCase.type)) { - update.indictmentDeniedExplanation = null - } - break - case CaseTransition.ACCEPT: - case CaseTransition.REJECT: - case CaseTransition.DISMISS: - case CaseTransition.COMPLETE: - update.rulingDate = isIndictmentCase(theCase.type) - ? nowFactory() - : theCase.courtEndTime - - // Handle appealed in court - if ( - !theCase.appealState && // don't appeal twice - (theCase.prosecutorAppealDecision === CaseAppealDecision.APPEAL || - theCase.accusedAppealDecision === CaseAppealDecision.APPEAL) - ) { - if (theCase.prosecutorAppealDecision === CaseAppealDecision.APPEAL) { - update.prosecutorPostponedAppealDate = nowFactory() - } else { - update.accusedPostponedAppealDate = nowFactory() - } - - update = { - ...update, - ...transitionCase( - CaseTransition.APPEAL, - theCase.type, - update.state ?? theCase.state, - update.appealState ?? theCase.appealState, - ), - } - } - break - case CaseTransition.REOPEN: - update.rulingDate = null - update.courtRecordSignatoryId = null - update.courtRecordSignatureDate = null - break - // TODO: Consider changing the names of the postponed appeal date variables - // as they are now also used when the case is appealed in court - case CaseTransition.APPEAL: - // The only roles that can appeal a case here are prosecutor roles - update.prosecutorPostponedAppealDate = nowFactory() - break - case CaseTransition.RECEIVE_APPEAL: - update.appealReceivedByCourtDate = nowFactory() - break - case CaseTransition.COMPLETE_APPEAL: - if ( - isRestrictionCase(theCase.type) && - theCase.state === CaseState.ACCEPTED && - (theCase.decision === CaseDecision.ACCEPTING || - theCase.decision === CaseDecision.ACCEPTING_PARTIALLY) - ) { - if ( - theCase.appealRulingDecision === CaseAppealRulingDecision.CHANGED || - theCase.appealRulingDecision === - CaseAppealRulingDecision.CHANGED_SIGNIFICANTLY - ) { - // The court of appeals has modified the ruling of a restriction case - update.validToDate = theCase.appealValidToDate - update.isCustodyIsolation = theCase.isAppealCustodyIsolation - update.isolationToDate = theCase.appealIsolationToDate - } else if ( - theCase.appealRulingDecision === CaseAppealRulingDecision.REPEAL - ) { - // The court of appeals has repealed the ruling of a restriction case - update.validToDate = nowFactory() - } - } - break - case CaseTransition.WITHDRAW_APPEAL: - // We only want to set the appeal ruling decision if the - // case has already been received. - // Otherwise the court of appeals never knew of the appeal in - // the first place so it remains withdrawn without a decision. - if ( - !theCase.appealRulingDecision && - theCase.appealState === CaseAppealState.RECEIVED - ) { - update.appealRulingDecision = CaseAppealRulingDecision.DISCONTINUED - } - break - case CaseTransition.ASK_FOR_CONFIRMATION: - update.indictmentReturnedExplanation = null - break - case CaseTransition.RETURN_INDICTMENT: - update.courtCaseNumber = null - update.indictmentHash = null - break - case CaseTransition.ASK_FOR_CANCELLATION: - if (theCase.indictmentDecision) { - throw new ForbiddenException( - `Cannot ask for cancellation of an indictment that is already in progress at the district court`, - ) - } - } + const update = transitionCase(transition.transition, theCase, user) const updatedCase = await this.caseService.update( theCase, diff --git a/apps/judicial-system/backend/src/app/modules/case/case.service.ts b/apps/judicial-system/backend/src/app/modules/case/case.service.ts index 12f19b067dd1..f33789f515e9 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.service.ts @@ -1689,12 +1689,7 @@ export class CaseService { return this.sequelize .transaction(async (transaction) => { if (receivingCase) { - update.state = transitionCase( - CaseTransition.RECEIVE, - theCase.type, - theCase.state, - theCase.appealState, - ).state + update = transitionCase(CaseTransition.RECEIVE, theCase, user, update) } await this.handleDateUpdates(theCase, update, transaction) diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts index f4728247f295..c263e83f455d 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts @@ -28,10 +28,7 @@ import { } from '@island.is/judicial-system/auth' import type { User as TUser } from '@island.is/judicial-system/types' import { - CaseAppealRulingDecision, - CaseAppealState, CaseState, - CaseTransition, CaseType, indictmentCases, investigationCases, @@ -169,24 +166,7 @@ export class LimitedAccessCaseController { `Transitioning case ${caseId} to ${transition.transition}`, ) - const update: LimitedAccessUpdateCase = transitionCase( - transition.transition, - theCase.type, - theCase.state, - theCase.appealState, - ) - - if (update.appealState === CaseAppealState.APPEALED) { - update.accusedPostponedAppealDate = nowFactory() - } - - if ( - transition.transition === CaseTransition.WITHDRAW_APPEAL && - !theCase.appealRulingDecision && - theCase.appealState === CaseAppealState.RECEIVED - ) { - update.appealRulingDecision = CaseAppealRulingDecision.DISCONTINUED - } + const update = transitionCase(transition.transition, theCase, user) const updatedCase = await this.limitedAccessCaseService.update( theCase, diff --git a/apps/judicial-system/backend/src/app/modules/case/state/case.state.spec.ts b/apps/judicial-system/backend/src/app/modules/case/state/case.state.spec.ts index bf84c06d64e6..c92fc6bbdf8d 100644 --- a/apps/judicial-system/backend/src/app/modules/case/state/case.state.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/state/case.state.spec.ts @@ -1,3 +1,5 @@ +import { uuid } from 'uuidv4' + import { ForbiddenException } from '@nestjs/common' import { @@ -5,10 +7,14 @@ import { CaseState, CaseTransition, indictmentCases, + InstitutionType, investigationCases, restrictionCases, + User, + UserRole, } from '@island.is/judicial-system/types' +import { Case } from '../models/case.model' import { transitionCase } from './case.state' describe('Transition Case', () => { @@ -17,7 +23,16 @@ describe('Transition Case', () => { 'state %s - should not open', (fromState) => { // Arrange - const act = () => transitionCase(CaseTransition.OPEN, type, fromState) + const act = () => + transitionCase( + CaseTransition.OPEN, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -38,13 +53,21 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.OPEN, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ state: CaseState.DRAFT }) + expect(res).toMatchObject({ state: CaseState.DRAFT }) }, ) @@ -55,9 +78,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.OPEN, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -78,9 +109,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.OPEN, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -100,12 +139,16 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.ASK_FOR_CONFIRMATION, - type, - fromState, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ state: CaseState.WAITING_FOR_CONFIRMATION }) + expect(res).toMatchObject({ state: CaseState.WAITING_FOR_CONFIRMATION }) }, ) @@ -116,7 +159,15 @@ describe('Transition Case', () => { )('state %s - should not ask for confirmation', (fromState) => { // Arrange const act = () => - transitionCase(CaseTransition.ASK_FOR_CONFIRMATION, type, fromState) + transitionCase( + CaseTransition.ASK_FOR_CONFIRMATION, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -134,9 +185,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.ASK_FOR_CONFIRMATION, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -156,12 +215,16 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.DENY_INDICTMENT, - type, - fromState, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ state: CaseState.DRAFT }) + expect(res).toMatchObject({ state: CaseState.DRAFT }) }, ) @@ -172,7 +235,15 @@ describe('Transition Case', () => { )('state %s - should not deny indictment', (fromState) => { // Arrange const act = () => - transitionCase(CaseTransition.DENY_INDICTMENT, type, fromState) + transitionCase( + CaseTransition.DENY_INDICTMENT, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -190,9 +261,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.DENY_INDICTMENT, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -210,10 +289,18 @@ describe('Transition Case', () => { 'state %s - should submit', (fromState) => { // Act - const res = transitionCase(CaseTransition.SUBMIT, type, fromState) + const res = transitionCase( + CaseTransition.SUBMIT, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Assert - expect(res).toEqual({ state: CaseState.SUBMITTED }) + expect(res).toMatchObject({ state: CaseState.SUBMITTED }) }, ) @@ -223,7 +310,16 @@ describe('Transition Case', () => { ), )('state %s - should not submit', (fromState) => { // Arrange - const act = () => transitionCase(CaseTransition.SUBMIT, type, fromState) + const act = () => + transitionCase( + CaseTransition.SUBMIT, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -243,13 +339,21 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.SUBMIT, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ state: CaseState.SUBMITTED }) + expect(res).toMatchObject({ state: CaseState.SUBMITTED }) }, ) @@ -260,9 +364,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.SUBMIT, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -283,9 +395,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.SUBMIT, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -305,12 +425,16 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.ASK_FOR_CANCELLATION, - type, - fromState, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ state: CaseState.WAITING_FOR_CANCELLATION }) + expect(res).toMatchObject({ state: CaseState.WAITING_FOR_CANCELLATION }) }, ) @@ -321,7 +445,15 @@ describe('Transition Case', () => { )('state %s - should not ask for cancellation', (fromState) => { // Arrange const act = () => - transitionCase(CaseTransition.ASK_FOR_CANCELLATION, type, fromState) + transitionCase( + CaseTransition.ASK_FOR_CANCELLATION, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -339,9 +471,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.ASK_FOR_CANCELLATION, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -359,10 +499,18 @@ describe('Transition Case', () => { 'state %s - should receive', (fromState) => { // Act - const res = transitionCase(CaseTransition.RECEIVE, type, fromState) + const res = transitionCase( + CaseTransition.RECEIVE, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Assert - expect(res).toEqual({ state: CaseState.RECEIVED }) + expect(res).toMatchObject({ state: CaseState.RECEIVED }) }, ) @@ -372,7 +520,16 @@ describe('Transition Case', () => { ), )('state %s - should not receive', (fromState) => { // Arrange - const act = () => transitionCase(CaseTransition.RECEIVE, type, fromState) + const act = () => + transitionCase( + CaseTransition.RECEIVE, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -392,13 +549,21 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.RECEIVE, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ state: CaseState.RECEIVED }) + expect(res).toMatchObject({ state: CaseState.RECEIVED }) }, ) @@ -409,9 +574,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.RECEIVE, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -432,9 +605,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.RECEIVE, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -454,12 +635,16 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.RETURN_INDICTMENT, - type, - fromState, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ state: CaseState.DRAFT }) + expect(res).toMatchObject({ state: CaseState.DRAFT }) }, ) @@ -470,7 +655,15 @@ describe('Transition Case', () => { )('state %s - should not return indictment', (fromState) => { // Arrange const act = () => - transitionCase(CaseTransition.RETURN_INDICTMENT, type, fromState) + transitionCase( + CaseTransition.RETURN_INDICTMENT, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -488,9 +681,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.RETURN_INDICTMENT, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -511,10 +712,18 @@ describe('Transition Case', () => { 'state %s - should complete', (fromState) => { // Act - const res = transitionCase(CaseTransition.COMPLETE, type, fromState) + const res = transitionCase( + CaseTransition.COMPLETE, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Assert - expect(res).toEqual({ state: CaseState.COMPLETED }) + expect(res).toMatchObject({ state: CaseState.COMPLETED }) }, ) @@ -524,7 +733,16 @@ describe('Transition Case', () => { ), )('state %s - should not complete', (fromState) => { // Arrange - const act = () => transitionCase(CaseTransition.COMPLETE, type, fromState) + const act = () => + transitionCase( + CaseTransition.COMPLETE, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -542,9 +760,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.COMPLETE, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -560,7 +786,16 @@ describe('Transition Case', () => { 'state %s - should not accept', (fromState) => { // Arrange - const act = () => transitionCase(CaseTransition.ACCEPT, type, fromState) + const act = () => + transitionCase( + CaseTransition.ACCEPT, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -584,13 +819,21 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.ACCEPT, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ state: CaseState.ACCEPTED }) + expect(res).toMatchObject({ state: CaseState.ACCEPTED }) }, ) }) @@ -607,9 +850,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.ACCEPT, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -625,7 +876,16 @@ describe('Transition Case', () => { 'state %s - should not reject', (fromState) => { // Arrange - const act = () => transitionCase(CaseTransition.REJECT, type, fromState) + const act = () => + transitionCase( + CaseTransition.REJECT, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -649,13 +909,21 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.REJECT, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ state: CaseState.REJECTED }) + expect(res).toMatchObject({ state: CaseState.REJECTED }) }, ) }) @@ -672,9 +940,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.REJECT, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -691,7 +967,15 @@ describe('Transition Case', () => { (fromState) => { // Arrange const act = () => - transitionCase(CaseTransition.DISMISS, type, fromState) + transitionCase( + CaseTransition.DISMISS, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -715,13 +999,21 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.DISMISS, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ state: CaseState.DISMISSED }) + expect(res).toMatchObject({ state: CaseState.DISMISSED }) }, ) }) @@ -738,9 +1030,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.DISMISS, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -761,10 +1061,18 @@ describe('Transition Case', () => { 'state %s - should delete', (fromState) => { // Act - const res = transitionCase(CaseTransition.DELETE, type, fromState) + const res = transitionCase( + CaseTransition.DELETE, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Assert - expect(res).toEqual({ state: CaseState.DELETED }) + expect(res).toMatchObject({ state: CaseState.DELETED }) }, ) @@ -774,7 +1082,16 @@ describe('Transition Case', () => { ), )('state %s - should not delete', (fromState) => { // Arrange - const act = () => transitionCase(CaseTransition.DELETE, type, fromState) + const act = () => + transitionCase( + CaseTransition.DELETE, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -799,13 +1116,21 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.DELETE, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ state: CaseState.DELETED }) + expect(res).toMatchObject({ state: CaseState.DELETED }) }, ) @@ -816,9 +1141,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.DELETE, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -839,9 +1172,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.DELETE, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -857,7 +1198,16 @@ describe('Transition Case', () => { 'state %s - should not reopen', (fromState) => { // Arrange - const act = () => transitionCase(CaseTransition.REOPEN, type, fromState) + const act = () => + transitionCase( + CaseTransition.REOPEN, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -885,13 +1235,21 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.REOPEN, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ state: CaseState.RECEIVED }) + expect(res).toMatchObject({ state: CaseState.RECEIVED }) }, ) }) @@ -908,9 +1266,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.REOPEN, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -926,7 +1292,16 @@ describe('Transition Case', () => { 'state %s - should not appeal', (fromState) => { // Arrange - const act = () => transitionCase(CaseTransition.APPEAL, type, fromState) + const act = () => + transitionCase( + CaseTransition.APPEAL, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -951,13 +1326,21 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.APPEAL, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ appealState: CaseAppealState.APPEALED }) + expect(res).toMatchObject({ appealState: CaseAppealState.APPEALED }) }, ) @@ -968,9 +1351,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.APPEAL, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -991,9 +1382,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.APPEAL, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -1010,7 +1409,15 @@ describe('Transition Case', () => { (fromState) => { // Arrange const act = () => - transitionCase(CaseTransition.WITHDRAW_APPEAL, type, fromState) + transitionCase( + CaseTransition.WITHDRAW_APPEAL, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -1038,13 +1445,23 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.WITHDRAW_APPEAL, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ appealState: CaseAppealState.WITHDRAWN }) + expect(res).toMatchObject({ + appealState: CaseAppealState.WITHDRAWN, + }) }, ) @@ -1057,9 +1474,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.WITHDRAW_APPEAL, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -1079,9 +1504,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.WITHDRAW_APPEAL, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -1098,7 +1531,15 @@ describe('Transition Case', () => { (fromState) => { // Arrange const act = () => - transitionCase(CaseTransition.RECEIVE_APPEAL, type, fromState) + transitionCase( + CaseTransition.RECEIVE_APPEAL, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -1123,13 +1564,21 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.RECEIVE_APPEAL, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ appealState: CaseAppealState.RECEIVED }) + expect(res).toMatchObject({ appealState: CaseAppealState.RECEIVED }) }, ) @@ -1142,9 +1591,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.RECEIVE_APPEAL, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -1164,9 +1621,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.RECEIVE_APPEAL, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -1183,7 +1648,15 @@ describe('Transition Case', () => { (fromState) => { // Arrange const act = () => - transitionCase(CaseTransition.COMPLETE_APPEAL, type, fromState) + transitionCase( + CaseTransition.COMPLETE_APPEAL, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -1211,13 +1684,23 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.COMPLETE_APPEAL, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ appealState: CaseAppealState.COMPLETED }) + expect(res).toMatchObject({ + appealState: CaseAppealState.COMPLETED, + }) }, ) @@ -1230,9 +1713,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.COMPLETE_APPEAL, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -1252,9 +1743,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.COMPLETE_APPEAL, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -1271,7 +1770,15 @@ describe('Transition Case', () => { (fromState) => { // Arrange const act = () => - transitionCase(CaseTransition.REOPEN_APPEAL, type, fromState) + transitionCase( + CaseTransition.REOPEN_APPEAL, + { id: uuid(), state: fromState, type } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, + ) // Act and assert expect(act).toThrow(ForbiddenException) @@ -1296,14 +1803,21 @@ describe('Transition Case', () => { // Act const res = transitionCase( CaseTransition.REOPEN_APPEAL, - type, - fromState, - - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Assert - expect(res).toEqual({ appealState: CaseAppealState.RECEIVED }) + expect(res).toMatchObject({ appealState: CaseAppealState.RECEIVED }) }, ) @@ -1316,10 +1830,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.REOPEN_APPEAL, - type, - fromState, - - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert @@ -1339,9 +1860,17 @@ describe('Transition Case', () => { const act = () => transitionCase( CaseTransition.REOPEN_APPEAL, - type, - fromState, - fromAppealState, + { + id: uuid(), + state: fromState, + appealState: fromAppealState, + type, + } as Case, + { + id: uuid(), + role: UserRole.PROSECUTOR, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User, ) // Act and assert diff --git a/apps/judicial-system/backend/src/app/modules/case/state/case.state.ts b/apps/judicial-system/backend/src/app/modules/case/state/case.state.ts index 311f741c5cac..af95ebaff7c3 100644 --- a/apps/judicial-system/backend/src/app/modules/case/state/case.state.ts +++ b/apps/judicial-system/backend/src/app/modules/case/state/case.state.ts @@ -1,40 +1,49 @@ import { ForbiddenException } from '@nestjs/common' import { + CaseAppealDecision, + CaseAppealRulingDecision, CaseAppealState, + CaseDecision, CaseState, CaseTransition, - CaseType, IndictmentCaseState, IndictmentCaseTransition, + isDefenceUser, isIndictmentCase, isIndictmentCaseState, isIndictmentCaseTransition, + isProsecutionUser, isRequestCase, isRequestCaseState, isRequestCaseTransition, + isRestrictionCase, RequestCaseState, RequestCaseTransition, + User, } from '@island.is/judicial-system/types' -interface IndictmentCaseStates { - state?: IndictmentCaseState -} +import { nowFactory } from '../../../factories' +import { UpdateCase } from '../case.service' +import { Case } from '../models/case.model' -interface RequestCaseStates { - state?: RequestCaseState - appealState?: CaseAppealState -} +type Actor = 'Prosecution' | 'Defence' | 'Neutral' + +type Transition = ( + update: UpdateCase, + theCase: Case, + actor: Actor, +) => UpdateCase interface IndictmentCaseRule { fromStates: IndictmentCaseState[] - to: IndictmentCaseStates + transition: Transition } interface RequestCaseRule { fromStates: RequestCaseState[] - fromAppealStates: (undefined | CaseAppealState)[] - to: RequestCaseStates + fromAppealStates: (CaseAppealState | undefined)[] + transition: Transition } interface CaseStates { @@ -50,42 +59,69 @@ const indictmentCaseStateMachine: Map< IndictmentCaseTransition.ASK_FOR_CONFIRMATION, { fromStates: [IndictmentCaseState.DRAFT, IndictmentCaseState.SUBMITTED], - to: { state: IndictmentCaseState.WAITING_FOR_CONFIRMATION }, + transition: (update: UpdateCase) => ({ + ...update, + state: CaseState.WAITING_FOR_CONFIRMATION, + indictmentReturnedExplanation: null, + }), }, ], [ IndictmentCaseTransition.DENY_INDICTMENT, { fromStates: [IndictmentCaseState.WAITING_FOR_CONFIRMATION], - to: { state: IndictmentCaseState.DRAFT }, + transition: (update: UpdateCase) => ({ + ...update, + state: CaseState.DRAFT, + }), }, ], [ IndictmentCaseTransition.SUBMIT, { fromStates: [IndictmentCaseState.WAITING_FOR_CONFIRMATION], - to: { state: IndictmentCaseState.SUBMITTED }, + transition: (update: UpdateCase) => ({ + ...update, + state: CaseState.SUBMITTED, + indictmentDeniedExplanation: null, + }), }, ], [ IndictmentCaseTransition.ASK_FOR_CANCELLATION, { fromStates: [IndictmentCaseState.SUBMITTED, IndictmentCaseState.RECEIVED], - to: { state: IndictmentCaseState.WAITING_FOR_CANCELLATION }, + transition: (update: UpdateCase, theCase: Case) => { + if (update.indictmentDecision ?? theCase.indictmentDecision) { + throw new ForbiddenException( + 'Cannot ask for cancellation of an indictment that is already in progress at the district court', + ) + } + + return { ...update, state: CaseState.WAITING_FOR_CANCELLATION } + }, }, ], [ IndictmentCaseTransition.RECEIVE, { fromStates: [IndictmentCaseState.SUBMITTED], - to: { state: IndictmentCaseState.RECEIVED }, + transition: (update: UpdateCase) => ({ + ...update, + state: CaseState.RECEIVED, + }), }, ], [ IndictmentCaseTransition.RETURN_INDICTMENT, { fromStates: [IndictmentCaseState.RECEIVED], - to: { state: IndictmentCaseState.DRAFT }, + transition: (update: UpdateCase) => ({ + ...update, + state: CaseState.DRAFT, + courtCaseNumber: null, + indictmentHash: null, + }), }, ], [ @@ -95,7 +131,11 @@ const indictmentCaseStateMachine: Map< IndictmentCaseState.WAITING_FOR_CANCELLATION, IndictmentCaseState.RECEIVED, ], - to: { state: IndictmentCaseState.COMPLETED }, + transition: (update: UpdateCase) => ({ + ...update, + state: CaseState.COMPLETED, + rulingDate: nowFactory(), + }), }, ], [ @@ -105,11 +145,55 @@ const indictmentCaseStateMachine: Map< IndictmentCaseState.DRAFT, IndictmentCaseState.WAITING_FOR_CONFIRMATION, ], - to: { state: IndictmentCaseState.DELETED }, + transition: (update: UpdateCase) => ({ + ...update, + state: CaseState.DELETED, + }), }, ], ]) +const requestCaseCompletionSideEffect = + (state: CaseState) => (update: UpdateCase, theCase: Case) => { + const currentCourtEndTime = update.courtEndTime ?? theCase.courtEndTime + const newUpdate: UpdateCase = { + ...update, + state, + rulingDate: currentCourtEndTime, + } + + // Handle appealed in court + const hasBeenAppealed = update.appealState ?? theCase.appealState + const prosecutorAppealedInCourt = + (update.prosecutorAppealDecision ?? theCase.prosecutorAppealDecision) === + CaseAppealDecision.APPEAL + const accusedAppealedInCourt = + (update.accusedAppealDecision ?? theCase.accusedAppealDecision) === + CaseAppealDecision.APPEAL + + if ( + // TODO: Decide what to do if correcting case + !hasBeenAppealed && // don't appeal twice + (prosecutorAppealedInCourt || accusedAppealedInCourt) + ) { + // TODO: Decide if we should set both appeal dates if both appeal + if (prosecutorAppealedInCourt) { + newUpdate.prosecutorPostponedAppealDate = currentCourtEndTime + } else { + newUpdate.accusedPostponedAppealDate = currentCourtEndTime + } + + return transitionRequestCase( + CaseTransition.APPEAL, + theCase, + newUpdate, + prosecutorAppealedInCourt ? 'Prosecution' : 'Defence', + ) + } + + return newUpdate + } + const requestCaseStateMachine: Map = new Map([ [ @@ -117,7 +201,10 @@ const requestCaseStateMachine: Map = { fromStates: [RequestCaseState.NEW], fromAppealStates: [undefined], - to: { state: RequestCaseState.DRAFT }, + transition: (update: UpdateCase) => ({ + ...update, + state: CaseState.DRAFT, + }), }, ], [ @@ -125,7 +212,10 @@ const requestCaseStateMachine: Map = { fromStates: [RequestCaseState.DRAFT], fromAppealStates: [undefined], - to: { state: RequestCaseState.SUBMITTED }, + transition: (update: UpdateCase) => ({ + ...update, + state: CaseState.SUBMITTED, + }), }, ], [ @@ -133,7 +223,10 @@ const requestCaseStateMachine: Map = { fromStates: [RequestCaseState.SUBMITTED], fromAppealStates: [undefined], - to: { state: RequestCaseState.RECEIVED }, + transition: (update: UpdateCase) => ({ + ...update, + state: CaseState.RECEIVED, + }), }, ], [ @@ -147,7 +240,7 @@ const requestCaseStateMachine: Map = CaseAppealState.COMPLETED, CaseAppealState.WITHDRAWN, ], - to: { state: RequestCaseState.ACCEPTED }, + transition: requestCaseCompletionSideEffect(CaseState.ACCEPTED), }, ], [ @@ -161,7 +254,7 @@ const requestCaseStateMachine: Map = CaseAppealState.COMPLETED, CaseAppealState.WITHDRAWN, ], - to: { state: RequestCaseState.REJECTED }, + transition: requestCaseCompletionSideEffect(CaseState.REJECTED), }, ], [ @@ -175,7 +268,7 @@ const requestCaseStateMachine: Map = CaseAppealState.COMPLETED, CaseAppealState.WITHDRAWN, ], - to: { state: RequestCaseState.DISMISSED }, + transition: requestCaseCompletionSideEffect(CaseState.DISMISSED), }, ], [ @@ -188,7 +281,11 @@ const requestCaseStateMachine: Map = RequestCaseState.RECEIVED, ], fromAppealStates: [undefined], - to: { state: RequestCaseState.DELETED }, + transition: (update: UpdateCase) => ({ + ...update, + state: CaseState.DELETED, + parentCaseId: null, + }), }, ], [ @@ -206,7 +303,13 @@ const requestCaseStateMachine: Map = CaseAppealState.COMPLETED, CaseAppealState.WITHDRAWN, ], - to: { state: RequestCaseState.RECEIVED }, + transition: (update: UpdateCase) => ({ + ...update, + state: CaseState.RECEIVED, + rulingDate: null, + courtRecordSignatoryId: null, + courtRecordSignatureDate: null, + }), }, ], // APPEAL, RECEIVE_APPEAL and COMPLETE_APPEAL transitions do not affect the case state, @@ -220,7 +323,31 @@ const requestCaseStateMachine: Map = RequestCaseState.DISMISSED, ], fromAppealStates: [undefined], - to: { appealState: CaseAppealState.APPEALED }, + transition: (update: UpdateCase, theCase: Case, actor?: Actor) => { + if (actor === 'Prosecution') { + return { + ...update, + appealState: CaseAppealState.APPEALED, + // We don't want to overwrite an already set appeal date + prosecutorPostponedAppealDate: + update.prosecutorPostponedAppealDate ?? nowFactory(), + } + } + + if (actor === 'Defence') { + return { + ...update, + appealState: CaseAppealState.APPEALED, + // We don't want to overwrite an already set appeal date + accusedPostponedAppealDate: + update.accusedPostponedAppealDate ?? nowFactory(), + } + } + + throw new ForbiddenException( + `${actor} cannot appeal a ${theCase.type} case`, + ) + }, }, ], [ @@ -232,7 +359,11 @@ const requestCaseStateMachine: Map = RequestCaseState.DISMISSED, ], fromAppealStates: [CaseAppealState.APPEALED], - to: { appealState: CaseAppealState.RECEIVED }, + transition: (update: UpdateCase) => ({ + ...update, + appealState: CaseAppealState.RECEIVED, + appealReceivedByCourtDate: nowFactory(), + }), }, ], [ @@ -244,7 +375,49 @@ const requestCaseStateMachine: Map = RequestCaseState.DISMISSED, ], fromAppealStates: [CaseAppealState.RECEIVED, CaseAppealState.WITHDRAWN], - to: { appealState: CaseAppealState.COMPLETED }, + transition: (update: UpdateCase, theCase: Case) => { + const newUpdate = { + ...update, + appealState: CaseAppealState.COMPLETED, + } + + const currentState = update.state ?? theCase.state + const currentDecision = update.decision ?? theCase.decision + + if ( + isRestrictionCase(theCase.type) && + currentState === CaseState.ACCEPTED && + (currentDecision === CaseDecision.ACCEPTING || + currentDecision === CaseDecision.ACCEPTING_PARTIALLY) + ) { + // TODO: Decide what to do if correcting appeal + const currentAppealRulingDecision = + newUpdate.appealRulingDecision ?? theCase.appealRulingDecision + + if ( + currentAppealRulingDecision === + CaseAppealRulingDecision.CHANGED || + currentAppealRulingDecision === + CaseAppealRulingDecision.CHANGED_SIGNIFICANTLY + ) { + // The court of appeals has modified the ruling of a restriction case + newUpdate.validToDate = + update.appealValidToDate ?? theCase.appealValidToDate + newUpdate.isCustodyIsolation = + update.isAppealCustodyIsolation ?? + theCase.isAppealCustodyIsolation + newUpdate.isolationToDate = + update.appealIsolationToDate ?? theCase.appealIsolationToDate + } else if ( + currentAppealRulingDecision === CaseAppealRulingDecision.REPEAL + ) { + // The court of appeals has repealed the ruling of a restriction case + newUpdate.validToDate = nowFactory() + } + } + + return newUpdate + }, }, ], [ @@ -256,7 +429,10 @@ const requestCaseStateMachine: Map = RequestCaseState.DISMISSED, ], fromAppealStates: [CaseAppealState.COMPLETED], - to: { appealState: CaseAppealState.RECEIVED }, + transition: (update: UpdateCase) => ({ + ...update, + appealState: CaseAppealState.RECEIVED, + }), }, ], [ @@ -268,15 +444,37 @@ const requestCaseStateMachine: Map = RequestCaseState.DISMISSED, ], fromAppealStates: [CaseAppealState.APPEALED, CaseAppealState.RECEIVED], - to: { appealState: CaseAppealState.WITHDRAWN }, + transition: (update: UpdateCase, theCase: Case) => { + // We only want to set the appeal ruling decision if the + // case has already been received. + // Otherwise the court of appeals never knew of the appeal in + // the first place so it remains withdrawn without a decision. + if ( + !(update.appealRulingDecision ?? theCase.appealRulingDecision) && + (update.appealState ?? theCase.appealState) === + CaseAppealState.RECEIVED + ) { + return { + ...update, + appealState: CaseAppealState.WITHDRAWN, + appealRulingDecision: CaseAppealRulingDecision.DISCONTINUED, + } + } + + return { ...update, appealState: CaseAppealState.WITHDRAWN } + }, }, ], ]) const transitionIndictmentCase = ( transition: CaseTransition, - currentState: CaseState, -): CaseStates => { + theCase: Case, + update: UpdateCase, + actor: Actor, +): UpdateCase => { + const currentState = update.state ?? theCase.state + if ( !isIndictmentCaseTransition(transition) || !isIndictmentCaseState(currentState) @@ -294,15 +492,18 @@ const transitionIndictmentCase = ( ) } - // Since the state machine is a global constant, we spread the result before returning it to avoid accidental mutations - return { ...rule.to } as CaseStates + return rule.transition(update, theCase, actor) } const transitionRequestCase = ( transition: CaseTransition, - currentState: CaseState, - currentAppealState?: CaseAppealState | undefined, -): CaseStates => { + theCase: Case, + update: UpdateCase, + actor: Actor, +): UpdateCase => { + const currentState = update.state ?? theCase.state + const currentAppealState = update.appealState ?? theCase.appealState + if ( !isRequestCaseTransition(transition) || !isRequestCaseState(currentState) @@ -325,25 +526,33 @@ const transitionRequestCase = ( ) } - // Since the state machine is a global constant, we spread the result before returning it to avoid accidental mutations - return { ...rule.to } as CaseStates + return rule.transition(update, theCase, actor) as UpdateCase } export const transitionCase = function ( transition: CaseTransition, - caseType: CaseType, - currentState: CaseState, - currentAppealState?: CaseAppealState, -): CaseStates { - if (isIndictmentCase(caseType)) { - return transitionIndictmentCase(transition, currentState) + theCase: Case, + user: User, + update: UpdateCase = {}, +): UpdateCase { + let actor: Actor + if (isProsecutionUser(user)) { + actor = 'Prosecution' + } else if (isDefenceUser(user)) { + actor = 'Defence' + } else { + actor = 'Neutral' + } + + if (isIndictmentCase(theCase.type)) { + return transitionIndictmentCase(transition, theCase, update, actor) } - if (isRequestCase(caseType)) { - return transitionRequestCase(transition, currentState, currentAppealState) + if (isRequestCase(theCase.type)) { + return transitionRequestCase(transition, theCase, update, actor) } throw new ForbiddenException( - `The transition ${transition} cannot be applied to a ${caseType} case`, + `The transition ${transition} cannot be applied to a ${theCase.type} case`, ) } diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/transition.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/transition.spec.ts index 244783bcaaf2..e1d0c49e6887 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/transition.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/transition.spec.ts @@ -17,10 +17,13 @@ import { completedIndictmentCaseStates, completedRequestCaseStates, indictmentCases, + InstitutionType, investigationCases, isIndictmentCase, + isRequestCase, restrictionCases, User, + UserRole, } from '@island.is/judicial-system/types' import { createTestingCaseModule } from '../createTestingCaseModule' @@ -48,7 +51,12 @@ type GivenWhenThen = ( describe('CaseController - Transition', () => { const date = randomDate() const userId = uuid() - const defaultUser = { id: userId, canConfirmIndictment: false } as User + const defaultUser = { + id: userId, + role: UserRole.PROSECUTOR, + canConfirmIndictment: false, + institution: { type: InstitutionType.PROSECUTORS_OFFICE }, + } as User let mockMessageService: MessageService let transaction: Transaction @@ -353,7 +361,9 @@ describe('CaseController - Transition', () => { { state: newState, parentCaseId: - transition === CaseTransition.DELETE ? null : undefined, + isRequestCase(type) && transition === CaseTransition.DELETE + ? null + : undefined, courtCaseNumber: transition === CaseTransition.RETURN_INDICTMENT ? null diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/transition.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/transition.spec.ts index ecfcf7793341..4e4eebc30276 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/transition.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/transition.spec.ts @@ -12,6 +12,7 @@ import { CaseState, CaseTransition, CaseType, + UserRole, } from '@island.is/judicial-system/types' import { createTestingCaseModule } from '../createTestingCaseModule' @@ -35,7 +36,7 @@ type GivenWhenThen = ( describe('LimitedAccessCaseController - Transition', () => { const date = randomDate() - const user = { id: uuid() } as User + const user = { id: uuid(), role: UserRole.DEFENDER } as User const caseId = uuid() const defenderAppealBriefId = uuid() const defenderAppealBriefCaseFileId1 = uuid()