diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.html b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.html index 79213afe10..932ac37121 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.html +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.html @@ -20,6 +20,7 @@

Enforcement status

? true : false " + (actionClick)="handleChangeCollectionOrder()" > Collection Order status diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.spec.ts index bcace27490..604e614ed9 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.spec.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.spec.ts @@ -31,4 +31,24 @@ describe('FinesAccDefendantDetailsEnforcementTab', () => { expect(event.preventDefault).toHaveBeenCalled(); expect(eventEmitterSpy).toHaveBeenCalled(); }); + + it('should emit changeCollectionOrder when handleChangeCollectionOrder is called', () => { + const eventEmitterSpy = vi.spyOn(component.changeCollectionOrder, 'emit'); + component.hasAccountMaintenancePermission = true; + + component.handleChangeCollectionOrder(); + + expect(eventEmitterSpy).toHaveBeenCalledWith( + component.tabData.enforcement_overview.collection_order?.collection_order_flag ?? false, + ); + }); + + it('should not emit changeCollectionOrder when the user lacks account maintenance permission', () => { + const eventEmitterSpy = vi.spyOn(component.changeCollectionOrder, 'emit'); + component.hasAccountMaintenancePermission = false; + + component.handleChangeCollectionOrder(); + + expect(eventEmitterSpy).not.toHaveBeenCalled(); + }); }); diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.ts b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.ts index 19ec837685..288dbbfda7 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.ts @@ -39,6 +39,7 @@ export class FinesAccDefendantDetailsEnforcementTab { @Input() hasAccountMaintenancePermission: boolean = false; @Input() hasEnterEnforcementPermission: boolean = false; @Output() addEnforcementOverride = new EventEmitter(); + @Output() changeCollectionOrder = new EventEmitter(); /** * Emits an event to add an enforcement override if the user has the necessary permissions and there is no existing enforcement override result. @@ -53,4 +54,15 @@ export class FinesAccDefendantDetailsEnforcementTab { this.addEnforcementOverride.emit(); } } + + /** + * Emits an event to change the collection order status. + */ + public handleChangeCollectionOrder(): void { + if (this.hasAccountMaintenancePermission) { + this.changeCollectionOrder.emit( + this.tabData.enforcement_overview.collection_order?.collection_order_flag ?? false, + ); + } + } } diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details.component.html b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details.component.html index 39396c237f..c15b602227 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details.component.html +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details.component.html @@ -206,6 +206,7 @@

Business Unit:

[hasAccountMaintenancePermission]="hasPermission('account-maintenance')" [hasEnterEnforcementPermission]="hasPermission('enter-enforcement')" (addEnforcementOverride)="navigateToAddEnforcementOverridePage()" + (changeCollectionOrder)="navigateToChangeCollectionOrderPage($event)" > } } diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details.component.spec.ts index feaa9c5f9b..725ef86051 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details.component.spec.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details.component.spec.ts @@ -168,6 +168,17 @@ describe('FinesAccDefendantDetailsComponent', () => { ); }); + it('should call router.navigate when navigateToChangeCollectionOrderPage is called', () => { + component.navigateToChangeCollectionOrderPage(true); + expect(routerSpy.navigate).toHaveBeenCalledWith( + [`../${FINES_ACC_DEFENDANT_ROUTING_PATHS.children.enforcement}/collection-order/change`], + { + relativeTo: component['activatedRoute'], + state: { currentCollectionOrderFlag: true }, + }, + ); + }); + it('should fetch the defendant tab data when fragment is changed to defendant', () => { component['refreshFragment$'].next('defendant'); // Subscribe to trigger the pipe execution diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details.component.ts b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details.component.ts index c8d7f1b673..0a3231aec7 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details.component.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details.component.ts @@ -458,4 +458,14 @@ export class FinesAccDefendantDetailsComponent extends AbstractTabData implement relativeTo: this.activatedRoute, }); } + + /** + * Navigates to the change collection order page. + */ + public navigateToChangeCollectionOrderPage(currentCollectionOrderFlag: boolean): void { + this['router'].navigate([`../${FINES_ACC_DEFENDANT_ROUTING_PATHS.children.enforcement}/collection-order/change`], { + relativeTo: this.activatedRoute, + state: { currentCollectionOrderFlag }, + }); + } } diff --git a/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/constants/fines-acc-enf-collo-change-field-errors.constant.ts b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/constants/fines-acc-enf-collo-change-field-errors.constant.ts new file mode 100644 index 0000000000..645cb940e2 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/constants/fines-acc-enf-collo-change-field-errors.constant.ts @@ -0,0 +1,10 @@ +import { IFinesAccEnfColloChangeFieldErrors } from '../interfaces/fines-acc-enf-collo-change-field-errors.interface'; + +export const FINES_ACC_ENF_COLLO_CHANGE_FIELD_ERRORS: IFinesAccEnfColloChangeFieldErrors = { + facc_enf_collection_order_made: { + required: { + message: 'Select whether the account is subject to a Collection Order', + priority: 1, + }, + }, +}; diff --git a/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/constants/fines-acc-enf-collo-change-routing-titles.constant.ts b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/constants/fines-acc-enf-collo-change-routing-titles.constant.ts new file mode 100644 index 0000000000..23e3be0453 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/constants/fines-acc-enf-collo-change-routing-titles.constant.ts @@ -0,0 +1,8 @@ +import { IFinesAccEnfColloChangeRoutingTitles } from '../interfaces/fines-acc-enf-collo-change-routing-titles.interface'; + +export const FINES_ACC_ENF_COLLO_CHANGE_ROUTING_TITLES: IFinesAccEnfColloChangeRoutingTitles = { + root: 'Collection Order', + children: { + change: 'Change Collection Order status', + }, +}; diff --git a/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/constants/fines-acc-enf-collo-change-success-message.constant.ts b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/constants/fines-acc-enf-collo-change-success-message.constant.ts new file mode 100644 index 0000000000..512d2013a6 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/constants/fines-acc-enf-collo-change-success-message.constant.ts @@ -0,0 +1 @@ +export const FINES_ACC_ENF_COLLO_CHANGE_SUCCESS_MESSAGE = 'Collection Order status changed'; diff --git a/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change-form/fines-acc-enf-collo-change-form.component.html b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change-form/fines-acc-enf-collo-change-form.component.html new file mode 100644 index 0000000000..da532d051d --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change-form/fines-acc-enf-collo-change-form.component.html @@ -0,0 +1,69 @@ + + + + + +

If the status of a Collection Order is incorrect you can change it.

+

This will not issue a new notice.

+

+ To add a new Collection Order and issue a notice you should + go back + and add an enforcement action. +

+ +
+ +
+
+
+ +
+
+
+ + Change + + +
+
+
+
diff --git a/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change-form/fines-acc-enf-collo-change-form.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change-form/fines-acc-enf-collo-change-form.component.spec.ts new file mode 100644 index 0000000000..345a29a01b --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change-form/fines-acc-enf-collo-change-form.component.spec.ts @@ -0,0 +1,36 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { FinesAccEnfColloChangeFormComponent } from './fines-acc-enf-collo-change-form.component'; + +describe('FinesAccEnfColloChangeFormComponent', () => { + let component: FinesAccEnfColloChangeFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FinesAccEnfColloChangeFormComponent], + providers: [{ provide: ActivatedRoute, useValue: { snapshot: { params: {}, data: {} } } }], + }).compileComponents(); + + fixture = TestBed.createComponent(FinesAccEnfColloChangeFormComponent); + component = fixture.componentInstance; + component.accountNumber = '177A'; + component.partyName = 'Mr Robert THOMSON'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize the collection order control', () => { + expect(component.form.get('facc_enf_collection_order_made')).toBeTruthy(); + expect(component.form.get('facc_enf_collection_order_made')?.value).toBeNull(); + }); + + it('should render the account caption in account-number-first format', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.textContent).toContain('177A - Mr Robert THOMSON'); + }); +}); diff --git a/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change-form/fines-acc-enf-collo-change-form.component.ts b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change-form/fines-acc-enf-collo-change-form.component.ts new file mode 100644 index 0000000000..d2312cbcbb --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change-form/fines-acc-enf-collo-change-form.component.ts @@ -0,0 +1,64 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit } from '@angular/core'; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { AbstractFormBaseComponent } from '@hmcts/opal-frontend-common/components/abstract/abstract-form-base'; +import { + IAbstractFormBaseFieldErrors, + IAbstractFormBaseForm, +} from '@hmcts/opal-frontend-common/components/abstract/abstract-form-base/interfaces'; +import { IAbstractFormControlErrorMessage } from '@hmcts/opal-frontend-common/components/abstract/interfaces'; +import { GovukButtonComponent } from '@hmcts/opal-frontend-common/components/govuk/govuk-button'; +import { GovukCancelLinkComponent } from '@hmcts/opal-frontend-common/components/govuk/govuk-cancel-link'; +import { GovukErrorSummaryComponent } from '@hmcts/opal-frontend-common/components/govuk/govuk-error-summary'; +import { GovukHeadingWithCaptionComponent } from '@hmcts/opal-frontend-common/components/govuk/govuk-heading-with-caption'; +import { + GovukRadioComponent, + GovukRadiosItemComponent, +} from '@hmcts/opal-frontend-common/components/govuk/govuk-radio'; +import { FINES_ACC_DEFENDANT_ROUTING_PATHS } from '../../routing/constants/fines-acc-defendant-routing-paths.constant'; +import { IFinesAccEnfColloChangeFormState } from '../interfaces/fines-acc-enf-collo-change-form-state.interface'; +import { FINES_ACC_ENF_COLLO_CHANGE_FIELD_ERRORS } from '../constants/fines-acc-enf-collo-change-field-errors.constant'; + +@Component({ + selector: 'app-fines-acc-enf-collo-change-form', + imports: [ + FormsModule, + ReactiveFormsModule, + GovukButtonComponent, + GovukCancelLinkComponent, + GovukErrorSummaryComponent, + GovukHeadingWithCaptionComponent, + GovukRadioComponent, + GovukRadiosItemComponent, + ], + templateUrl: './fines-acc-enf-collo-change-form.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class FinesAccEnfColloChangeFormComponent extends AbstractFormBaseComponent implements OnInit { + protected override fieldErrors: IAbstractFormBaseFieldErrors = { + ...FINES_ACC_ENF_COLLO_CHANGE_FIELD_ERRORS, + }; + protected override formSubmit = new EventEmitter>(); + public override formControlErrorMessages: IAbstractFormControlErrorMessage = {}; + public readonly defendantAccRoutingPaths = FINES_ACC_DEFENDANT_ROUTING_PATHS; + + @Input({ required: true }) partyName!: string; + @Input({ required: true }) accountNumber!: string; + + /** + * Creates the form group for changing the Collection Order status. + */ + private setupForm(): void { + this.form = new FormGroup({ + facc_enf_collection_order_made: new FormControl(null, Validators.required), + }); + } + + /** + * Initialises the form before running the shared abstract form setup. + */ + public override ngOnInit(): void { + this.setupForm(); + super.ngOnInit(); + } +} diff --git a/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change.component.html b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change.component.html new file mode 100644 index 0000000000..2a44580914 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change.component.html @@ -0,0 +1,8 @@ +
+ +
diff --git a/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change.component.spec.ts new file mode 100644 index 0000000000..3c47d61966 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change.component.spec.ts @@ -0,0 +1,226 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Navigation, Router } from '@angular/router'; +import { signal, type WritableSignal } from '@angular/core'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { of, throwError } from 'rxjs'; +import { FinesAccEnfColloChangeComponent } from './fines-acc-enf-collo-change.component'; +import { FinesAccountStore } from '../stores/fines-acc.store'; +import { FinesAccPayloadService } from '../services/fines-acc-payload.service'; +import { OpalFines } from '../../services/opal-fines-service/opal-fines.service'; +import { UtilsService } from '@hmcts/opal-frontend-common/services/utils-service'; +import { FINES_ACC_DEFENDANT_ROUTING_PATHS } from '../routing/constants/fines-acc-defendant-routing-paths.constant'; +import { FinesAccountStoreType } from '../types/fines-account-store.type'; +import { FINES_ACC_ENF_COLLO_CHANGE_SUCCESS_MESSAGE } from './constants/fines-acc-enf-collo-change-success-message.constant'; + +describe('FinesAccEnfColloChangeComponent', () => { + let component: FinesAccEnfColloChangeComponent; + let fixture: ComponentFixture; + let mockRoute: ActivatedRoute; + let mockRouter: Pick; + let mockCurrentNavigation: WritableSignal; + let mockAccountStore: Pick< + FinesAccountStoreType, + 'getAccountNumber' | 'party_name' | 'account_id' | 'base_version' | 'business_unit_id' | 'setSuccessMessage' + >; + let mockPayloadService: Pick; + let mockOpalFinesService: Pick; + let mockUtilsService: Pick; + + beforeEach(async () => { + mockRoute = { + snapshot: { + data: {}, + }, + } as ActivatedRoute; + + mockCurrentNavigation = signal({ + extras: { + state: {}, + }, + } as unknown as Navigation); + + mockRouter = { + currentNavigation: mockCurrentNavigation, + navigate: vi.fn(), + }; + + mockAccountStore = { + getAccountNumber: signal('177A'), + party_name: signal('Mr Robert THOMSON'), + account_id: signal(1001), + base_version: signal('1'), + business_unit_id: signal('2002'), + setSuccessMessage: vi.fn(), + }; + + mockPayloadService = { + buildCollectionOrderPayload: vi.fn().mockImplementation((form) => ({ + collection_order: { + collection_order_date: null, + collection_order_flag: form.formData.facc_enf_collection_order_made, + }, + })), + }; + + mockOpalFinesService = { + patchDefendantAccount: vi.fn().mockReturnValue(of({})), + clearCache: vi.fn(), + }; + + mockUtilsService = { + scrollToTop: vi.fn(), + }; + + await TestBed.configureTestingModule({ + imports: [FinesAccEnfColloChangeComponent], + providers: [ + { provide: ActivatedRoute, useValue: mockRoute }, + { provide: Router, useValue: mockRouter }, + { provide: FinesAccountStore, useValue: mockAccountStore }, + { provide: FinesAccPayloadService, useValue: mockPayloadService }, + { provide: OpalFines, useValue: mockOpalFinesService }, + { provide: UtilsService, useValue: mockUtilsService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(FinesAccEnfColloChangeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should expose page data from the route and store', () => { + expect(component.accountNumber).toBe('177A'); + expect(component.partyName).toBe('Mr Robert THOMSON'); + }); + + it('should patch and navigate on success when the selection changes', () => { + const routerNavigateSpy = vi.spyOn(component as never, 'routerNavigate'); + + component.handleSubmit({ + formData: { + facc_enf_collection_order_made: true, + }, + nestedFlow: false, + }); + + expect(mockPayloadService.buildCollectionOrderPayload).toHaveBeenCalledWith({ + formData: { + facc_enf_collection_order_made: true, + }, + nestedFlow: false, + }); + expect(mockOpalFinesService.patchDefendantAccount).toHaveBeenCalledWith( + 1001, + { + collection_order: { + collection_order_date: null, + collection_order_flag: true, + }, + }, + '1', + '2002', + ); + expect(mockOpalFinesService.clearCache).toHaveBeenCalledWith('defendantAccountEnforcementCache$'); + expect(mockAccountStore.setSuccessMessage).toHaveBeenCalledWith(FINES_ACC_ENF_COLLO_CHANGE_SUCCESS_MESSAGE); + expect(routerNavigateSpy).toHaveBeenCalledWith( + FINES_ACC_DEFENDANT_ROUTING_PATHS.children.details, + false, + undefined, + null, + 'enforcement', + ); + }); + + it('should scroll to top on submit error', () => { + mockOpalFinesService.patchDefendantAccount = vi.fn().mockReturnValue(throwError(() => new Error('fail'))); + const routerNavigateSpy = vi.spyOn(component as never, 'routerNavigate'); + + component.handleSubmit({ + formData: { + facc_enf_collection_order_made: true, + }, + nestedFlow: false, + }); + + expect(mockUtilsService.scrollToTop).toHaveBeenCalled(); + expect(routerNavigateSpy).not.toHaveBeenCalled(); + }); + + it('should navigate back without patching when the selected value matches the current collection order flag', () => { + mockCurrentNavigation.set({ + extras: { + state: { + currentCollectionOrderFlag: false, + }, + }, + } as unknown as Navigation); + + fixture = TestBed.createComponent(FinesAccEnfColloChangeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + const routerNavigateSpy = vi.spyOn(component as never, 'routerNavigate'); + + component.handleSubmit({ + formData: { + facc_enf_collection_order_made: false, + }, + nestedFlow: false, + }); + + expect(mockPayloadService.buildCollectionOrderPayload).not.toHaveBeenCalled(); + expect(mockOpalFinesService.patchDefendantAccount).not.toHaveBeenCalled(); + expect(mockAccountStore.setSuccessMessage).not.toHaveBeenCalled(); + expect(routerNavigateSpy).toHaveBeenCalledWith( + FINES_ACC_DEFENDANT_ROUTING_PATHS.children.details, + false, + undefined, + null, + 'enforcement', + ); + }); + + it('should send an empty collection order date when selecting yes', () => { + component.handleSubmit({ + formData: { + facc_enf_collection_order_made: true, + }, + nestedFlow: false, + }); + + expect(mockPayloadService.buildCollectionOrderPayload).toHaveBeenCalledWith({ + formData: { + facc_enf_collection_order_made: true, + }, + nestedFlow: false, + }); + }); + + it('should send an empty collection order date when selecting no', () => { + component.handleSubmit({ + formData: { + facc_enf_collection_order_made: false, + }, + nestedFlow: false, + }); + + expect(mockPayloadService.buildCollectionOrderPayload).toHaveBeenCalledWith({ + formData: { + facc_enf_collection_order_made: false, + }, + nestedFlow: false, + }); + }); + + it('should update unsaved changes state', () => { + component.handleUnsavedChanges(true); + expect(component.stateUnsavedChanges).toBe(true); + + component.handleUnsavedChanges(false); + expect(component.stateUnsavedChanges).toBe(false); + }); +}); diff --git a/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change.component.ts b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change.component.ts new file mode 100644 index 0000000000..0df8f1807d --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/fines-acc-enf-collo-change.component.ts @@ -0,0 +1,103 @@ +import { ChangeDetectionStrategy, Component, OnDestroy, inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { AbstractFormParentBaseComponent } from '@hmcts/opal-frontend-common/components/abstract/abstract-form-parent-base'; +import { catchError, EMPTY, Subject, takeUntil } from 'rxjs'; +import { UtilsService } from '@hmcts/opal-frontend-common/services/utils-service'; +import { IAbstractFormBaseForm } from '@hmcts/opal-frontend-common/components/abstract/abstract-form-base/interfaces'; +import { FinesAccountStore } from '../stores/fines-acc.store'; +import { FinesAccPayloadService } from '../services/fines-acc-payload.service'; +import { OpalFines } from '../../services/opal-fines-service/opal-fines.service'; +import { FINES_ACC_DEFENDANT_ROUTING_PATHS } from '../routing/constants/fines-acc-defendant-routing-paths.constant'; +import { IFinesAccEnfColloChangeFormState } from './interfaces/fines-acc-enf-collo-change-form-state.interface'; +import { FinesAccEnfColloChangeFormComponent } from './fines-acc-enf-collo-change-form/fines-acc-enf-collo-change-form.component'; +import { FINES_ACC_ENF_COLLO_CHANGE_SUCCESS_MESSAGE } from './constants/fines-acc-enf-collo-change-success-message.constant'; + +@Component({ + selector: 'app-fines-acc-enf-collo-change', + templateUrl: './fines-acc-enf-collo-change.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [FinesAccEnfColloChangeFormComponent], +}) +export class FinesAccEnfColloChangeComponent extends AbstractFormParentBaseComponent implements OnDestroy { + private readonly ngUnsubscribe = new Subject(); + private readonly finesAccStore = inject(FinesAccountStore); + private readonly finesAccPayloadService = inject(FinesAccPayloadService); + private readonly opalFinesService = inject(OpalFines); + private readonly appRouter = inject(Router); + private readonly utilsService = inject(UtilsService); + private readonly finesDefendantRoutingPaths = FINES_ACC_DEFENDANT_ROUTING_PATHS; + private readonly currentCollectionOrderFlag = this.appRouter.currentNavigation()?.extras.state?.[ + 'currentCollectionOrderFlag' + ] as boolean | undefined; + + public readonly accountNumber = this.finesAccStore.getAccountNumber() ?? ''; + public readonly partyName = this.finesAccStore.party_name() ?? ''; + + /** + * Navigates back to the enforcement tab and optionally sets a success message. + * + * @param setSuccessMessage Whether to set the success banner before navigation. + */ + private navigateToEnforcementTab(setSuccessMessage = false): void { + this.stateUnsavedChanges = false; + + if (setSuccessMessage) { + this.finesAccStore.setSuccessMessage(FINES_ACC_ENF_COLLO_CHANGE_SUCCESS_MESSAGE); + } + + this.routerNavigate(this.finesDefendantRoutingPaths.children.details, false, undefined, null, 'enforcement'); + } + + /** + * Submits the selected Collection Order status for the current account. + * + * @param form The form payload emitted by the child form component. + */ + public handleSubmit(form: IAbstractFormBaseForm): void { + const selectedCollectionOrderFlag = form.formData.facc_enf_collection_order_made; + + if (selectedCollectionOrderFlag === this.currentCollectionOrderFlag) { + this.navigateToEnforcementTab(); + return; + } + + const payload = this.finesAccPayloadService.buildCollectionOrderPayload(form); + + this.opalFinesService + .patchDefendantAccount( + this.finesAccStore.account_id()!, + payload, + this.finesAccStore.base_version()!, + this.finesAccStore.business_unit_id()!, + ) + .pipe( + catchError(() => { + this.utilsService.scrollToTop(); + return EMPTY; + }), + takeUntil(this.ngUnsubscribe), + ) + .subscribe(() => { + this.opalFinesService.clearCache('defendantAccountEnforcementCache$'); + this.navigateToEnforcementTab(true); + }); + } + + /** + * Updates the page-level unsaved changes state from the child form. + * + * @param unsavedChanges Whether the form currently has unsaved changes. + */ + public handleUnsavedChanges(unsavedChanges: boolean): void { + this.stateUnsavedChanges = unsavedChanges; + } + + /** + * Completes the teardown notifier used by active subscriptions. + */ + public ngOnDestroy(): void { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } +} diff --git a/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/interfaces/fines-acc-enf-collo-change-field-errors.interface.ts b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/interfaces/fines-acc-enf-collo-change-field-errors.interface.ts new file mode 100644 index 0000000000..2b8d8aaed3 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/interfaces/fines-acc-enf-collo-change-field-errors.interface.ts @@ -0,0 +1,8 @@ +import { + IAbstractFormBaseFieldError, + IAbstractFormBaseFieldErrors, +} from '@hmcts/opal-frontend-common/components/abstract/abstract-form-base/interfaces'; + +export interface IFinesAccEnfColloChangeFieldErrors extends IAbstractFormBaseFieldErrors { + facc_enf_collection_order_made: IAbstractFormBaseFieldError; +} diff --git a/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/interfaces/fines-acc-enf-collo-change-form-state.interface.ts b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/interfaces/fines-acc-enf-collo-change-form-state.interface.ts new file mode 100644 index 0000000000..6326f61f3d --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/interfaces/fines-acc-enf-collo-change-form-state.interface.ts @@ -0,0 +1,3 @@ +export interface IFinesAccEnfColloChangeFormState { + facc_enf_collection_order_made: boolean | null; +} diff --git a/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/interfaces/fines-acc-enf-collo-change-routing-titles.interface.ts b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/interfaces/fines-acc-enf-collo-change-routing-titles.interface.ts new file mode 100644 index 0000000000..cba7d32ba3 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-enf-collo-change/interfaces/fines-acc-enf-collo-change-routing-titles.interface.ts @@ -0,0 +1,8 @@ +import { IChildRoutingPaths } from '@hmcts/opal-frontend-common/pages/routing/interfaces'; + +export interface IFinesAccEnfColloChangeRoutingTitles extends IChildRoutingPaths { + root: string; + children: { + change: string; + }; +} diff --git a/src/app/flows/fines/fines-acc/routing/fines-acc.routes.ts b/src/app/flows/fines/fines-acc/routing/fines-acc.routes.ts index e8420f4f79..c88185e1fa 100644 --- a/src/app/flows/fines/fines-acc/routing/fines-acc.routes.ts +++ b/src/app/flows/fines/fines-acc/routing/fines-acc.routes.ts @@ -22,6 +22,7 @@ import { fetchLocalJusticeAreasResolver } from '../../routing/resolvers/fetch-re import { fetchEnforcersResolver } from '../../routing/resolvers/fetch-results-with-params-resolver/fetch-enforcers-resolver'; import { FINES_ACC_ENF_OVERRIDE_ADD_CHANGE_ROUTING_TITLES } from '../fines-acc-enf-override-add-change/constants/fines-acc-enf-override-add-change-routing-titles.constant'; import { FINES_ACC_ENF_OVERRIDE_ADD_CHANGE_ROUTING_PATHS } from '../fines-acc-enf-override-add-change/constants/fines-acc-enf-override-add-change-routing-paths.constant'; +import { FINES_ACC_ENF_COLLO_CHANGE_ROUTING_TITLES } from '../fines-acc-enf-collo-change/constants/fines-acc-enf-collo-change-routing-titles.constant'; const accRootPermissionIds = FINES_PERMISSIONS; @@ -194,6 +195,22 @@ export const routing: Routes = [ enforcersRefData: fetchEnforcersResolver, }, }, + { + path: `${FINES_ACC_DEFENDANT_ROUTING_PATHS.children.enforcement}/collection-order/change`, + loadComponent: () => + import('../fines-acc-enf-collo-change/fines-acc-enf-collo-change.component').then( + (c) => c.FinesAccEnfColloChangeComponent, + ), + canActivate: [routePermissionsGuard, finesAccStateGuard], + canDeactivate: [canDeactivateGuard], + data: { + routePermissionId: [accRootPermissionIds['account-maintenance']], + title: FINES_ACC_ENF_COLLO_CHANGE_ROUTING_TITLES.children.change, + }, + resolve: { + title: TitleResolver, + }, + }, ], }, { diff --git a/src/app/flows/fines/fines-acc/services/constants/fines-acc-collection-order-payload-defaults.constant.ts b/src/app/flows/fines/fines-acc/services/constants/fines-acc-collection-order-payload-defaults.constant.ts new file mode 100644 index 0000000000..a399aff422 --- /dev/null +++ b/src/app/flows/fines/fines-acc/services/constants/fines-acc-collection-order-payload-defaults.constant.ts @@ -0,0 +1,6 @@ +import { IOpalFinesUpdateDefendantAccountCollectionOrder } from '@services/fines/opal-fines-service/interfaces/opal-fines-update-defendant-account-collection-order.interface'; + +export const FINES_ACC_COLLECTION_ORDER_PAYLOAD_DEFAULTS: IOpalFinesUpdateDefendantAccountCollectionOrder = { + collection_order_date: null, + collection_order_flag: null, +}; diff --git a/src/app/flows/fines/fines-acc/services/fines-acc-payload.service.spec.ts b/src/app/flows/fines/fines-acc/services/fines-acc-payload.service.spec.ts index 74a28b2b30..046aa3afad 100644 --- a/src/app/flows/fines/fines-acc/services/fines-acc-payload.service.spec.ts +++ b/src/app/flows/fines/fines-acc/services/fines-acc-payload.service.spec.ts @@ -545,6 +545,7 @@ describe('FinesAccPayloadService', () => { free_text_note_2: 'Updated note 2', free_text_note_3: 'Updated note 3', }, + collection_order: null, enforcement_override: null, }); }); @@ -566,10 +567,27 @@ describe('FinesAccPayloadService', () => { free_text_note_2: null, free_text_note_3: null, }, + collection_order: null, enforcement_override: null, }); }); + it('should build collection order payload correctly', () => { + const result = service.buildCollectionOrderPayload({ + formData: { + facc_enf_collection_order_made: true, + }, + nestedFlow: false, + }); + + expect(result).toEqual({ + collection_order: { + collection_order_date: null, + collection_order_flag: true, + }, + }); + }); + it('should transform payload using the transformation service', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any vi.spyOn(service['transformationService'], 'transformObjectValues').mockImplementation( diff --git a/src/app/flows/fines/fines-acc/services/fines-acc-payload.service.ts b/src/app/flows/fines/fines-acc/services/fines-acc-payload.service.ts index 06959f87ef..32358d5e9a 100644 --- a/src/app/flows/fines/fines-acc/services/fines-acc-payload.service.ts +++ b/src/app/flows/fines/fines-acc/services/fines-acc-payload.service.ts @@ -27,6 +27,10 @@ import { buildAccountPartyFromFormState } from './utils/fines-acc-payload-build- import { IOpalFinesAccountMinorCreditorDetailsHeader } from '../fines-acc-minor-creditor-details/interfaces/fines-acc-minor-creditor-details-header.interface'; import { IFinesAccEnfOverrideAddChangeFormState } from '../fines-acc-enf-override-add-change/interfaces/fines-acc-enf-override-add-change-form-state.interface'; import { OPAL_FINES_DEFENDANT_ACCOUNT_PATCH_PAYLOAD_DEFAULTS } from '../../services/opal-fines-service/constants/opal-fines-defendant-account-patch-payload-defaults.constant'; +import { IOpalFinesUpdateDefendantAccountCollectionOrder } from '@services/fines/opal-fines-service/interfaces/opal-fines-update-defendant-account-collection-order.interface'; +import { IAbstractFormBaseForm } from '@hmcts/opal-frontend-common/components/abstract/abstract-form-base/interfaces'; +import { IFinesAccEnfColloChangeFormState } from '../fines-acc-enf-collo-change/interfaces/fines-acc-enf-collo-change-form-state.interface'; +import { FINES_ACC_COLLECTION_ORDER_PAYLOAD_DEFAULTS } from './constants/fines-acc-collection-order-payload-defaults.constant'; @Injectable({ providedIn: 'root', @@ -173,6 +177,28 @@ export class FinesAccPayloadService { }; } + /** + * Transforms the given collection order form into an update payload + * for the defendant account API. + * + * @param form - The submitted collection order form + * @returns The transformed payload for updating the defendant account + */ + public buildCollectionOrderPayload( + form: IAbstractFormBaseForm, + ): IOpalFinesUpdateDefendantAccountPayload { + const collectionOrderFlag = form.formData.facc_enf_collection_order_made as boolean; + + const collectionOrder: IOpalFinesUpdateDefendantAccountCollectionOrder = { + ...FINES_ACC_COLLECTION_ORDER_PAYLOAD_DEFAULTS, + collection_order_flag: collectionOrderFlag, + }; + + return { + collection_order: collectionOrder, + }; + } + /** * Transforms the given IFinesAccEnfOverrideAddChangeFormState into an update payload * for the defendant account API. diff --git a/src/app/flows/fines/services/opal-fines-service/constants/opal-fines-defendant-account-patch-payload-defaults.constant.ts b/src/app/flows/fines/services/opal-fines-service/constants/opal-fines-defendant-account-patch-payload-defaults.constant.ts index b9fb0ce6fd..51476bb4b1 100644 --- a/src/app/flows/fines/services/opal-fines-service/constants/opal-fines-defendant-account-patch-payload-defaults.constant.ts +++ b/src/app/flows/fines/services/opal-fines-service/constants/opal-fines-defendant-account-patch-payload-defaults.constant.ts @@ -2,5 +2,6 @@ import { IOpalFinesUpdateDefendantAccountPayload } from '../interfaces/opal-fine export const OPAL_FINES_DEFENDANT_ACCOUNT_PATCH_PAYLOAD_DEFAULTS: IOpalFinesUpdateDefendantAccountPayload = { comment_and_notes: null, + collection_order: null, enforcement_override: null, }; diff --git a/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-update-defendant-account-collection-order.interface.ts b/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-update-defendant-account-collection-order.interface.ts new file mode 100644 index 0000000000..cbb70f8594 --- /dev/null +++ b/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-update-defendant-account-collection-order.interface.ts @@ -0,0 +1,4 @@ +export interface IOpalFinesUpdateDefendantAccountCollectionOrder { + collection_order_date: null; + collection_order_flag: boolean | null; +} diff --git a/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-update-defendant-account.interface.ts b/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-update-defendant-account.interface.ts index 09d92d1f52..0b98d5f216 100644 --- a/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-update-defendant-account.interface.ts +++ b/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-update-defendant-account.interface.ts @@ -1,10 +1,12 @@ import { IOpalFinesUpdateDefendantAccountCommentsNotes } from './opal-fines-update-defendant-account-comments-notes.interface'; +import { IOpalFinesUpdateDefendantAccountCollectionOrder } from './opal-fines-update-defendant-account-collection-order.interface'; import { IOpalFinesUpdateDefendantAccountEnforcementOverride } from './opal-fines-update-defendant-account-enforcement-override.interface'; /** * Interface for the payload to update a defendant account *Subject to change */ export interface IOpalFinesUpdateDefendantAccountPayload { - comment_and_notes: IOpalFinesUpdateDefendantAccountCommentsNotes | null; - enforcement_override: IOpalFinesUpdateDefendantAccountEnforcementOverride | null; + comment_and_notes?: IOpalFinesUpdateDefendantAccountCommentsNotes | null; + collection_order?: IOpalFinesUpdateDefendantAccountCollectionOrder | null; + enforcement_override?: IOpalFinesUpdateDefendantAccountEnforcementOverride | null; }