diff --git a/src/app/constants/fines-permissions.constant.ts b/src/app/constants/fines-permissions.constant.ts index 4b726736f9..2ff515f55c 100644 --- a/src/app/constants/fines-permissions.constant.ts +++ b/src/app/constants/fines-permissions.constant.ts @@ -11,5 +11,6 @@ export const FINES_PERMISSIONS: IFinesPermissions = { 'add-account-activity-notes': 8, 'amend-payment-terms': 9, 'enter-enforcement': 10, + 'add-remove-payment-hold': 12, consolidate: 13, }; 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 045f4d7578..2a0bcb3ffe 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 @@ -1,7 +1,7 @@ @@ -149,7 +149,7 @@

Business Unit:

@if (tabAtAGlance$ | async; as tabData) { @@ -159,7 +159,7 @@

Business Unit:

@if (tabDefendant$ | async; as tabData) { Business Unit: @if (tabParentOrGuardian$ | async; as tabData) { Business Unit: @if (tabPaymentTerms$ | async; as tabData) { Business Unit: } } 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 f6b136cc90..cdcc8d611a 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 @@ -1,14 +1,10 @@ import { ChangeDetectionStrategy, Component, inject, OnDestroy, OnInit } from '@angular/core'; -import { distinctUntilChanged, EMPTY, map, merge, Observable, Subject, takeUntil, tap } from 'rxjs'; +import { EMPTY, merge, Observable, takeUntil, tap } from 'rxjs'; // Services import { OpalFines } from '@services/fines/opal-fines-service/opal-fines.service'; -import { PermissionsService } from '@hmcts/opal-frontend-common/services/permissions-service'; - // Stores -import { GlobalStore } from '@hmcts/opal-frontend-common/stores/global'; import { FinesAccountStore } from '../stores/fines-acc.store'; // Components -import { AbstractTabData } from '@hmcts/opal-frontend-common/components/abstract/abstract-tab-data'; import { FinesAccDefendantDetailsAtAGlanceTabComponent } from './fines-acc-defendant-details-at-a-glance-tab/fines-acc-defendant-details-at-a-glance-tab.component'; import { MojSubNavigationComponent, @@ -55,6 +51,7 @@ import { FINES_ACCOUNT_TYPES } from '../../constants/fines-account-types.constan import { IOpalFinesResultRefData } from '@services/fines/opal-fines-service/interfaces/opal-fines-result-ref-data.interface'; import { FinesAccDefendantDetailsEnforcementTab } from './fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component'; import { FinesAccSummaryHeaderComponent } from '../fines-acc-summary-header/fines-acc-summary-header.component'; +import { AbstractAccountSummaryBaseComponent } from '@hmcts/opal-frontend-common/components/abstract/abstract-account-summary-base'; @Component({ selector: 'app-fines-acc-defendant-details', @@ -84,18 +81,15 @@ import { FinesAccSummaryHeaderComponent } from '../fines-acc-summary-header/fine templateUrl: './fines-acc-defendant-details.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class FinesAccDefendantDetailsComponent extends AbstractTabData implements OnInit, OnDestroy { +export class FinesAccDefendantDetailsComponent + extends AbstractAccountSummaryBaseComponent + implements OnInit, OnDestroy +{ private readonly opalFinesService = inject(OpalFines); - private readonly permissionsService = inject(PermissionsService); - private readonly globalStore = inject(GlobalStore); - private readonly userState = this.globalStore.userState(); private readonly payloadService = inject(FinesAccPayloadService); - private readonly destroy$ = new Subject(); - private readonly refreshFragment$ = new Subject(); public accountStore = inject(FinesAccountStore); public tabs: IFinesAccountDefendantDetailsTabs = FINES_ACC_DEFENDANT_DETAILS_TABS; - public accountData!: IOpalFinesAccountDefendantDetailsHeader; public accountId: number = Number(this.activatedRoute.snapshot.paramMap.get('accountId')); public tabContentStyles: IFinesAccSummaryTabsContentStyles = FINES_ACC_SUMMARY_TABS_CONTENT_STYLES; public tabAtAGlance$: Observable = EMPTY; @@ -109,20 +103,9 @@ export class FinesAccDefendantDetailsComponent extends AbstractTabData implement public debtorTypes = FINES_ACC_DEBTOR_TYPES; public accountTypes = FINES_ACCOUNT_TYPES; public lastEnforcement: IOpalFinesResultRefData | null = null; - - /** - * Fetches the defendant account heading data and current tab fragment from the route. - */ - private getHeaderDataFromRoute(): void { - this.accountData = this.payloadService.transformPayload( - this.activatedRoute.snapshot.data['defendantAccountHeadingData'], - FINES_ACC_MAP_TRANSFORM_ITEMS_CONFIG, - ); - this.accountStore.setAccountState( - this.payloadService.transformAccountHeaderForStore(this.accountId, this.accountData, 'defendant'), - ); - - this.activeTab = this.activatedRoute.snapshot.fragment || 'at-a-glance'; + public finesPermissions = FINES_PERMISSIONS; + private fetchTabDataTyped(serviceCall: Observable): Observable { + return this.fetchTabData(serviceCall, (version) => this.accountStore.compareVersion(version)) as Observable; } /** @@ -137,7 +120,7 @@ export class FinesAccDefendantDetailsComponent extends AbstractTabData implement return ( !this.lastEnforcement?.extend_ttp_disallow && !invalidCodes.includes(accountStatusCode) && - this.hasBusinessUnitPermission('amend-payment-terms') && + this.hasBusinessUnitPermissionKey('amend-payment-terms') && this.accountData.payment_state_summary.account_balance! > 0 ); } @@ -148,7 +131,7 @@ export class FinesAccDefendantDetailsComponent extends AbstractTabData implement * @returns boolean indicating if the user can request a payment card */ private canRequestPaymentCard(): boolean { - return !this.lastEnforcement?.prevent_payment_card && this.hasBusinessUnitPermission('amend-payment-terms'); + return !this.lastEnforcement?.prevent_payment_card && this.hasBusinessUnitPermissionKey('amend-payment-terms'); } /** @@ -158,7 +141,7 @@ export class FinesAccDefendantDetailsComponent extends AbstractTabData implement private getAmendPaymentTermsDeniedType(): string { if (this.lastEnforcement?.extend_ttp_disallow) { return 'enforcement'; - } else if (!this.hasBusinessUnitPermission('amend-payment-terms')) { + } else if (!this.hasBusinessUnitPermissionKey('amend-payment-terms')) { return 'permission'; } else if (this.accountData.payment_state_summary.account_balance! <= 0) { return 'balance'; @@ -179,6 +162,18 @@ export class FinesAccDefendantDetailsComponent extends AbstractTabData implement } } + /** + * Checks if the user has the specified permission within the business unit related to the account. + * @param permissionKey The key of the permission to check. + * @returns True if the user has the permission, false otherwise. + */ + private hasBusinessUnitPermissionKey(permissionKey: string): boolean { + return super.hasBusinessUnitPermission( + FINES_PERMISSIONS[permissionKey], + Number(this.accountStore.business_unit_id()!), + ); + } + /** * Initializes and sets up the observable data stream for the fines draft tab component. * @@ -188,7 +183,7 @@ export class FinesAccDefendantDetailsComponent extends AbstractTabData implement * and constructs the necessary parameters for fetching and populating the tab's table data. * */ - private setupTabDataStream(): void { + protected override setupTabDataStream(): void { const fragment$ = merge( this.clearCacheOnTabChange(this.getFragmentStream('at-a-glance', this.destroy$), () => this.opalFinesService.clearCache( @@ -204,23 +199,25 @@ export class FinesAccDefendantDetailsComponent extends AbstractTabData implement fragment$.pipe(takeUntil(this.destroy$)).subscribe((tab) => { switch (tab) { case 'at-a-glance': - this.tabAtAGlance$ = this.fetchTabData(this.opalFinesService.getDefendantAccountAtAGlance(account_id)); + this.tabAtAGlance$ = this.fetchTabDataTyped(this.opalFinesService.getDefendantAccountAtAGlance(account_id)); break; case 'defendant': - this.tabDefendant$ = this.fetchTabData( + this.tabDefendant$ = this.fetchTabDataTyped( this.opalFinesService.getDefendantAccountParty(account_id, defendant_account_party_id), ); break; case 'parent-or-guardian': - this.tabParentOrGuardian$ = this.fetchTabData( + this.tabParentOrGuardian$ = this.fetchTabDataTyped( this.opalFinesService.getParentOrGuardianAccountParty(account_id, parent_guardian_party_id), ); break; case 'fixed-penalty': - this.tabFixedPenalty$ = this.fetchTabData(this.opalFinesService.getDefendantAccountFixedPenalty(account_id)); + this.tabFixedPenalty$ = this.fetchTabDataTyped( + this.opalFinesService.getDefendantAccountFixedPenalty(account_id), + ); break; case 'payment-terms': - this.tabPaymentTerms$ = this.fetchTabData( + this.tabPaymentTerms$ = this.fetchTabDataTyped( this.opalFinesService.getDefendantAccountPaymentTermsLatest(account_id).pipe( tap((data) => { if (data.last_enforcement) { @@ -236,15 +233,15 @@ export class FinesAccDefendantDetailsComponent extends AbstractTabData implement ); break; case 'enforcement': - this.tabEnforcement$ = this.fetchTabData( + this.tabEnforcement$ = this.fetchTabDataTyped( this.opalFinesService.getDefendantAccountEnforcementTabData(account_id), ); break; case 'impositions': - this.tabImpositions$ = this.fetchTabData(this.opalFinesService.getDefendantAccountImpositionsTabData()); + this.tabImpositions$ = this.fetchTabDataTyped(this.opalFinesService.getDefendantAccountImpositionsTabData()); break; case 'history-and-notes': - this.tabHistoryAndNotes$ = this.fetchTabData( + this.tabHistoryAndNotes$ = this.fetchTabDataTyped( this.opalFinesService.getDefendantAccountHistoryAndNotesTabData(), ); break; @@ -253,59 +250,41 @@ export class FinesAccDefendantDetailsComponent extends AbstractTabData implement } /** - * Fetches the data for a specific tab by calling the provided service function. - * Compares the version of the fetched data with the current version in the store. - * @param serviceCall the service function that retrieves the tab data - * @returns an observable of the tab data + * Fetches the defendant account heading data and current tab fragment from the route. */ - private fetchTabData(serviceCall: Observable): Observable { - return serviceCall.pipe( - map((data) => this.payloadService.transformPayload(data, FINES_ACC_MAP_TRANSFORM_ITEMS_CONFIG)), - tap((data) => { - this.accountStore.compareVersion(data.version); - }), - distinctUntilChanged(), - takeUntil(this.destroy$), - ); + protected override getHeaderDataFromRoute(): void { + const headingData = this.activatedRoute.snapshot.data['defendantAccountHeadingData']; + this.accountData = this.transformHeaderForView(headingData); + this.transformHeaderForStore(this.accountId, this.accountData); + this.activeTab = this.activatedRoute.snapshot.fragment || 'at-a-glance'; } - /** - * Checks if the user has the specified permission in any of their roles. - * @param permissionKey The key of the permission to check. - * @returns True if the user has the permission, false otherwise. - */ - public hasPermission(permissionKey: string): boolean { - return this.permissionsService.hasPermissionAccess( - FINES_PERMISSIONS[permissionKey], - this.userState.business_unit_users, - ); + protected override getHeaderData(accountId: number): Observable { + return this.opalFinesService.getDefendantAccountHeadingData(accountId); } - /** - * Checks if the user has the specified permission within the business unit related to the account. - * @param permissionKey The key of the permission to check. - * @returns True if the user has the permission, false otherwise. - */ - public hasBusinessUnitPermission(permissionKey: string): boolean { - return this.permissionsService.hasBusinessUnitPermissionAccess( - FINES_PERMISSIONS[permissionKey], - Number(this.accountStore.business_unit_id()!), - this.userState.business_unit_users, + protected override transformHeaderForStore(accountId: number, header: IOpalFinesAccountDefendantDetailsHeader): void { + this.accountStore.setAccountState( + this.payloadService.transformAccountHeaderForStore(accountId, header, 'defendant'), ); } + protected override transformHeaderForView( + header: IOpalFinesAccountDefendantDetailsHeader, + ): IOpalFinesAccountDefendantDetailsHeader { + return this.payloadService.transformPayload(header, FINES_ACC_MAP_TRANSFORM_ITEMS_CONFIG); + } + + protected override transformTabData(data: T): T { + return this.payloadService.transformPayload(data, FINES_ACC_MAP_TRANSFORM_ITEMS_CONFIG); + } + /** * Navigates to the add account note page. * If the user lacks the required permission in this BU, navigates to the access-denied page instead. */ public navigateToAddAccountNotePage(): void { - if ( - this.permissionsService.hasBusinessUnitPermissionAccess( - FINES_PERMISSIONS['add-account-activity-notes'], - Number(this.accountStore.business_unit_id()!), - this.userState.business_unit_users, - ) - ) { + if (this.hasBusinessUnitPermissionKey('add-account-activity-notes')) { this['router'].navigate([`../${FINES_ACC_DEFENDANT_ROUTING_PATHS.children.note}/add`], { relativeTo: this.activatedRoute, }); @@ -321,13 +300,7 @@ export class FinesAccDefendantDetailsComponent extends AbstractTabData implement * If the user lacks the required permission in this BU, navigates to the access-denied page instead. */ public navigateToAddCommentsPage(): void { - if ( - this.permissionsService.hasBusinessUnitPermissionAccess( - FINES_PERMISSIONS['account-maintenance'], - Number(this.accountStore.business_unit_id()!), - this.userState.business_unit_users, - ) - ) { + if (this.hasBusinessUnitPermissionKey('account-maintenance')) { this['router'].navigate([`../${FINES_ACC_DEFENDANT_ROUTING_PATHS.children.comments}/add`], { relativeTo: this.activatedRoute, }); @@ -345,44 +318,19 @@ export class FinesAccDefendantDetailsComponent extends AbstractTabData implement * Refreshes the page. * @param event The user event that triggered the refresh action. */ - public refreshPage(): void { + public override refreshPage(): void { this.accountStore.setHasVersionMismatch(false); - this.opalFinesService - .getDefendantAccountHeadingData(Number(this.accountStore.account_id())) - .pipe( - tap((defendantHeadingData) => { - this.accountStore.setAccountState( - this.payloadService.transformAccountHeaderForStore( - Number(this.accountStore.account_id()), - defendantHeadingData, - 'defendant', - ), - ); - }), - map((defendantHeadingData) => - this.payloadService.transformPayload(defendantHeadingData, FINES_ACC_MAP_TRANSFORM_ITEMS_CONFIG), - ), - takeUntil(this.destroy$), - ) - .subscribe((res) => { - this.accountStore.setSuccessMessage('Information is up to date'); - this.accountData = res; - this.refreshFragment$.next(this.activeTab); - }); - } - - public ngOnInit(): void { - this.getHeaderDataFromRoute(); - this.setupTabDataStream(); + super.refreshPage(Number(this.accountStore.account_id()), (header) => { + this.accountStore.setSuccessMessage('Information is up to date'); + this.accountData = header; + }); } - public ngOnDestroy(): void { + public override ngOnDestroy(): void { this.accountStore.clearSuccessMessage(); this.accountStore.setHasVersionMismatch(false); - this.destroy$.next(); - this.destroy$.complete(); - this.refreshFragment$.complete(); + super.ngOnDestroy(); } /** @@ -391,13 +339,7 @@ export class FinesAccDefendantDetailsComponent extends AbstractTabData implement * @param partyType */ public navigateToAmendPartyDetailsPage(partyType: string): void { - if ( - this.permissionsService.hasBusinessUnitPermissionAccess( - FINES_PERMISSIONS['account-maintenance'], - Number(this.accountStore.business_unit_id()!), - this.userState.business_unit_users, - ) - ) { + if (this.hasBusinessUnitPermissionKey('account-maintenance')) { this['router'].navigate([`../party/${partyType}/amend`], { relativeTo: this.activatedRoute, }); diff --git a/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/constants/fines-acc-minor-creditor-account-tabs-cache-map.constant.ts b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/constants/fines-acc-minor-creditor-account-tabs-cache-map.constant.ts new file mode 100644 index 0000000000..cf89717737 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/constants/fines-acc-minor-creditor-account-tabs-cache-map.constant.ts @@ -0,0 +1,5 @@ +import { IFinesAccMinorCreditorAccountTabsCacheMap } from '../interfaces/fines-acc-minor-creditor-account-tabs-cache-map.interface'; + +export const FINES_ACC_MINOR_CREDITOR_ACCOUNT_TABS_CACHE_MAP: IFinesAccMinorCreditorAccountTabsCacheMap = { + 'at-a-glance': 'minorCreditorAccountAtAGlanceCache$', +}; diff --git a/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details-at-a-glance-tab/fines-acc-minor-creditor-details-at-a-glance-tab.component.html b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details-at-a-glance-tab/fines-acc-minor-creditor-details-at-a-glance-tab.component.html new file mode 100644 index 0000000000..9186cf4e59 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details-at-a-glance-tab/fines-acc-minor-creditor-details-at-a-glance-tab.component.html @@ -0,0 +1,139 @@ +
+
+
+

Minor creditor

+
+ + @if (tabData.party.organisation_flag) { +

Company Name

+

+ {{ tabData.party.organisation_details?.organisation_name }} +

+ + @if (tabData.party.organisation_details?.organisation_aliases) { +

Aliases

+

+ @for (alias of tabData.party.organisation_details?.organisation_aliases; track alias) { + {{ alias.organisation_name }}
+ } +

+ } + } @else { +

Name

+

+ {{ tabData.party.individual_details?.title }} + {{ tabData.party.individual_details?.forenames }} + {{ tabData.party.individual_details?.surname | uppercase }} +

+ + @if (tabData.party.individual_details?.individual_aliases) { +

Aliases

+

+ @for (alias of tabData.party.individual_details?.individual_aliases; track alias) { + {{ alias.forenames }} {{ alias.surname | uppercase }}
+ } +

+ } + + @if (tabData.party.individual_details && tabData.party.individual_details.date_of_birth) { +

Date of birth

+

+ {{ tabData.party.individual_details.date_of_birth | dateFormat: 'dd/MM/yyyy' : 'dd MMMM yyyy' }} +

+ } + } + +

Address

+

+ {{ tabData.address.address_line_1 }} + @if (tabData.address.address_line_2) { +
+ {{ tabData.address.address_line_2 }} + } + @if (tabData.address.address_line_3) { +
+ {{ tabData.address.address_line_3 }} + } + @if (tabData.address.address_line_4) { +
+ {{ tabData.address.address_line_4 }} + } + @if (tabData.address.address_line_5) { +
+ {{ tabData.address.address_line_5 }} + } +
+ {{ tabData.address.postcode | uppercase }} +

+ + @if (!tabData.party.organisation_flag) { +

National Insurance Number

+

+ @if (tabData.party.individual_details?.national_insurance_number) { + {{ tabData.party.individual_details?.national_insurance_number! | nationalInsurance }} + } @else { + + } +

+ } +
+ + @if (tabData.defendant) { +
+

Defendant account

+
+ +

Defendant account

+

{{ tabData.defendant.account_number }}

+ +

Defendant Name

+

{{ tabData.defendant.name }}

+ +

Hearing Date

+

{{ tabData.defendant.hearing_date | dateFormat: 'dd/MM/yyyy' : 'dd MMMM yyyy' }}

+
+ } + +
+

Payout status

+
+ @if (tabData.payment.is_bacs) { + Provided + } @else { + Not provided + } + + @if (hasAddRemovePaymentHoldPermission) { + @if (tabData.payment.hold_payment) { +

+ Remove payment hold +

+ } @else { +

+ Add payment hold +

+ } + } +
+
+
diff --git a/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details-at-a-glance-tab/fines-acc-minor-creditor-details-at-a-glance-tab.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details-at-a-glance-tab/fines-acc-minor-creditor-details-at-a-glance-tab.component.spec.ts new file mode 100644 index 0000000000..45cf15d22b --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details-at-a-glance-tab/fines-acc-minor-creditor-details-at-a-glance-tab.component.spec.ts @@ -0,0 +1,37 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FinesAccMinorCreditorDetailsAtAGlanceTabComponent } from './fines-acc-minor-creditor-details-at-a-glance-tab.component'; +import { OPAL_FINES_ACCOUNT_MINOR_CREDITOR_AT_A_GLANCE_WITH_DEFENDANT_MOCK } from '@app/flows/fines/services/opal-fines-service/mocks/opal-fines-account-minor-creditor-at-a-glance-with-defendant.mock'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +describe('FinesAccMinorCreditorDetailsAtAGlanceTabComponent', () => { + let component: FinesAccMinorCreditorDetailsAtAGlanceTabComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FinesAccMinorCreditorDetailsAtAGlanceTabComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FinesAccMinorCreditorDetailsAtAGlanceTabComponent); + component = fixture.componentInstance; + component.tabData = OPAL_FINES_ACCOUNT_MINOR_CREDITOR_AT_A_GLANCE_WITH_DEFENDANT_MOCK; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should emit addPaymentHold event when handleAddPaymentHold is called', () => { + vi.spyOn(component.addPaymentHold, 'emit'); + component.handleAddPaymentHold(); + expect(component.addPaymentHold.emit).toHaveBeenCalled(); + }); + + it('should emit removePaymentHold event when handleRemovePaymentHold is called', () => { + vi.spyOn(component.removePaymentHold, 'emit'); + component.handleRemovePaymentHold(); + expect(component.removePaymentHold.emit).toHaveBeenCalled(); + }); +}); diff --git a/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details-at-a-glance-tab/fines-acc-minor-creditor-details-at-a-glance-tab.component.ts b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details-at-a-glance-tab/fines-acc-minor-creditor-details-at-a-glance-tab.component.ts new file mode 100644 index 0000000000..71d822b0a9 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details-at-a-glance-tab/fines-acc-minor-creditor-details-at-a-glance-tab.component.ts @@ -0,0 +1,40 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { UpperCasePipe } from '@angular/common'; +import { MojBadgeComponent } from '@hmcts/opal-frontend-common/components/moj/moj-badge'; +import { FINES_MAC_LANGUAGE_PREFERENCES_OPTIONS } from '../../../fines-mac/fines-mac-language-preferences/constants/fines-mac-language-preferences-options'; +import { IFinesAccSummaryTabsContentStyles } from '../../fines-acc-defendant-details/interfaces/fines-acc-summary-tabs-content-styles.interface'; +import { FINES_ACC_SUMMARY_TABS_CONTENT_STYLES } from '../../constants/fines-acc-summary-tabs-content-styles.constant'; +import { FINES_ACC_DEBTOR_TYPES } from '../../constants/fines-acc-debtor-types.constant'; +import { NationalInsurancePipe } from '@hmcts/opal-frontend-common/pipes/national-insurance'; +import { FinesNotProvidedComponent } from '../../../components/fines-not-provided/fines-not-provided.component'; +import { DateFormatPipe } from '@hmcts/opal-frontend-common/pipes/date-format'; +import { IOpalFinesAccountMinorCreditorAtAGlance } from '@app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-minor-creditor-at-a-glance.interface'; +@Component({ + selector: 'app-fines-acc-minor-creditor-details-at-a-glance-tab', + imports: [UpperCasePipe, MojBadgeComponent, NationalInsurancePipe, FinesNotProvidedComponent, DateFormatPipe], + templateUrl: './fines-acc-minor-creditor-details-at-a-glance-tab.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FinesAccMinorCreditorDetailsAtAGlanceTabComponent { + @Input({ required: true }) tabData!: IOpalFinesAccountMinorCreditorAtAGlance; + @Input() hasAddRemovePaymentHoldPermission: boolean = false; + @Input() style: IFinesAccSummaryTabsContentStyles = FINES_ACC_SUMMARY_TABS_CONTENT_STYLES; + @Output() addPaymentHold = new EventEmitter(); + @Output() removePaymentHold = new EventEmitter(); + public readonly languages = FINES_MAC_LANGUAGE_PREFERENCES_OPTIONS; + public readonly debtorTypes = FINES_ACC_DEBTOR_TYPES; + + /** + * Emits an event to indicate that the user wants to add a payment hold. + */ + public handleAddPaymentHold(): void { + this.addPaymentHold.emit(); + } + + /** + * Emits an event to indicate that the user wants to remove a payment hold. + */ + public handleRemovePaymentHold(): void { + this.removePaymentHold.emit(); + } +} diff --git a/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details.component.html b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details.component.html index aee71a78ba..316abb7ce9 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details.component.html +++ b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details.component.html @@ -1,7 +1,7 @@ @@ -71,3 +71,17 @@

Business Unit:

> + +@switch (activeTab) { + @case ('at-a-glance') { + @if (tabAtAGlance$ | async; as tabData) { + + } + } +} diff --git a/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details.component.spec.ts index fe3bfb2252..ffc265b00a 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details.component.spec.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details.component.spec.ts @@ -12,13 +12,17 @@ import { FinesAccPayloadService } from '../services/fines-acc-payload.service'; import { MOCK_FINES_ACCOUNT_STATE } from '../mocks/fines-acc-state.mock'; import { FINES_ACC_MINOR_CREDITOR_ROUTING_PATHS } from '../routing/constants/fines-acc-minor-creditor-routing-paths.constant'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { OPAL_FINES_ACCOUNT_MINOR_CREDITOR_AT_A_GLANCE_WITH_DEFENDANT_MOCK } from '../../services/opal-fines-service/mocks/opal-fines-account-minor-creditor-at-a-glance-with-defendant.mock'; describe('FinesAccMinorCreditorDetailsComponent', () => { let component: FinesAccMinorCreditorDetailsComponent; let fixture: ComponentFixture; let routerSpy: Pick; let activatedRouteStub: Partial; - let mockOpalFinesService: Pick; + let mockOpalFinesService: Pick< + OpalFines, + 'getMinorCreditorAccountHeadingData' | 'getMinorCreditorAccountAtAGlance' | 'clearCache' | 'getResult' + >; let mockPayloadService: Pick; beforeEach(async () => { @@ -49,6 +53,9 @@ describe('FinesAccMinorCreditorDetailsComponent', () => { .mockReturnValue(of(structuredClone(FINES_ACC_MINOR_CREDITOR_DETAILS_HEADER_MOCK))), clearCache: vi.fn(), getResult: vi.fn(), + getMinorCreditorAccountAtAGlance: vi + .fn() + .mockReturnValue(of(structuredClone(OPAL_FINES_ACCOUNT_MINOR_CREDITOR_AT_A_GLANCE_WITH_DEFENDANT_MOCK))), }; await TestBed.configureTestingModule({ @@ -113,4 +120,26 @@ describe('FinesAccMinorCreditorDetailsComponent', () => { relativeTo: component['activatedRoute'], }); }); + + it('should call router.navigate when navigateToAddPaymentHoldPage is called', () => { + vi.spyOn(component['permissionsService'], 'hasBusinessUnitPermissionAccess').mockReturnValue(true); + component.navigateToAddPaymentHoldPage(); + expect(routerSpy.navigate).toHaveBeenCalledWith( + [`../${FINES_ACC_MINOR_CREDITOR_ROUTING_PATHS.children['payment-hold']}/add`], + { + relativeTo: component['activatedRoute'], + }, + ); + }); + + it('should call router.navigate when navigateToRemovePaymentHoldPage is called', () => { + vi.spyOn(component['permissionsService'], 'hasBusinessUnitPermissionAccess').mockReturnValue(true); + component.navigateToRemovePaymentHoldPage(); + expect(routerSpy.navigate).toHaveBeenCalledWith( + [`../${FINES_ACC_MINOR_CREDITOR_ROUTING_PATHS.children['payment-hold']}/remove`], + { + relativeTo: component['activatedRoute'], + }, + ); + }); }); diff --git a/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details.component.ts b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details.component.ts index fe146807cc..34abc45dde 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details.component.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/fines-acc-minor-creditor-details.component.ts @@ -1,14 +1,11 @@ import { ChangeDetectionStrategy, Component, inject, OnDestroy, OnInit } from '@angular/core'; -import { Subject } from 'rxjs'; -import { tap, map, takeUntil } from 'rxjs/operators'; +import { EMPTY, merge, Observable } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; // Services -import { PermissionsService } from '@hmcts/opal-frontend-common/services/permissions-service'; import { OpalFines } from '../../services/opal-fines-service/opal-fines.service'; // Stores import { FinesAccountStore } from '../stores/fines-acc.store'; -import { GlobalStore } from '@hmcts/opal-frontend-common/stores/global'; // Components -import { AbstractTabData } from '@hmcts/opal-frontend-common/components/abstract/abstract-tab-data'; import { MojSubNavigationComponent, MojSubNavigationItemComponent, @@ -38,6 +35,12 @@ import { IOpalFinesResultRefData } from '@services/fines/opal-fines-service/inte import { IFinesAccSummaryTabsContentStyles } from '../fines-acc-defendant-details/interfaces/fines-acc-summary-tabs-content-styles.interface'; import { FinesAccPayloadService } from '../services/fines-acc-payload.service'; import { FinesAccSummaryHeaderComponent } from '../fines-acc-summary-header/fines-acc-summary-header.component'; +import { FinesAccMinorCreditorDetailsAtAGlanceTabComponent } from './fines-acc-minor-creditor-details-at-a-glance-tab/fines-acc-minor-creditor-details-at-a-glance-tab.component'; +import { AsyncPipe } from '@angular/common'; +import { IOpalFinesAccountMinorCreditorAtAGlance } from '../../services/opal-fines-service/interfaces/opal-fines-account-minor-creditor-at-a-glance.interface'; +import { FINES_ACC_MINOR_CREDITOR_ACCOUNT_TABS_CACHE_MAP } from './constants/fines-acc-minor-creditor-account-tabs-cache-map.constant'; +import { IFinesAccMinorCreditorAccountTabsCacheMap } from './interfaces/fines-acc-minor-creditor-account-tabs-cache-map.interface'; +import { AbstractAccountSummaryBaseComponent } from '@hmcts/opal-frontend-common/components/abstract/abstract-account-summary-base'; @Component({ selector: 'app-fines-acc-minor-creditor-details', @@ -54,67 +57,118 @@ import { FinesAccSummaryHeaderComponent } from '../fines-acc-summary-header/fine CustomAccountInformationItemValueComponent, MonetaryPipe, FinesAccSummaryHeaderComponent, + FinesAccMinorCreditorDetailsAtAGlanceTabComponent, + AsyncPipe, ], templateUrl: './fines-acc-minor-creditor-details.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class FinesAccMinorCreditorDetailsComponent extends AbstractTabData implements OnInit, OnDestroy { - private readonly destroy$ = new Subject(); - private readonly refreshFragment$ = new Subject(); - private readonly permissionsService = inject(PermissionsService); - private readonly globalStore = inject(GlobalStore); - private readonly userState = this.globalStore.userState(); +export class FinesAccMinorCreditorDetailsComponent + extends AbstractAccountSummaryBaseComponent + implements OnInit, OnDestroy +{ private readonly opalFinesService = inject(OpalFines); private readonly payloadService = inject(FinesAccPayloadService); public accountStore = inject(FinesAccountStore); public tabs: IFinesAccountMinorCreditorDetailsTabs = FINES_ACC_MINOR_CREDITOR_DETAILS_TABS; - public accountData!: IOpalFinesAccountMinorCreditorDetailsHeader; public tabContentStyles: IFinesAccSummaryTabsContentStyles = FINES_ACC_SUMMARY_TABS_CONTENT_STYLES; + public tabAtAGlance$: Observable = EMPTY; public debtorTypes = FINES_ACC_DEBTOR_TYPES; public accountTypes = FINES_ACCOUNT_TYPES; public lastEnforcement: IOpalFinesResultRefData | null = null; + public finesPermissions = FINES_PERMISSIONS; /** - * Fetches the minor creditor account heading data and current tab fragment from the route. + * Fetches the tab data and ensures it is typed correctly. + * @param serviceCall The observable service call to fetch the tab data. + * @returns An observable of the typed tab data. */ - private getHeaderDataFromRoute(): void { - this.accountData = this.activatedRoute.snapshot.data['minorCreditorAccountHeadingData']; - this.activeTab = this.activatedRoute.snapshot.fragment || 'at-a-glance'; + private fetchTabDataTyped(serviceCall: Observable): Observable { + return this.fetchTabData(serviceCall, (version) => this.accountStore.compareVersion(version)) as Observable; } /** - * Checks if the user has the specified permission in any of their roles. + * Checks if the current user has the specified business unit permission. * @param permissionKey The key of the permission to check. - * @returns True if the user has the permission, false otherwise. + * @returns A boolean indicating whether the user has the permission. */ - public hasPermission(permissionKey: string): boolean { - return this.permissionsService.hasPermissionAccess( + private hasBusinessUnitPermissionKey(permissionKey: string): boolean { + return super.hasBusinessUnitPermission( FINES_PERMISSIONS[permissionKey], - this.userState.business_unit_users, + Number(this.accountStore.business_unit_id()!), ); } /** - * Navigates to the add account note page. - * If the user lacks the required permission in this BU, navigates to the access-denied page instead. + * Initializes and sets up the observable data stream for the fines draft tab component. + * + * This method listens to changes in either the route fragment (representing the active tab) + * or the refreshFragment (triggered when a user refreshes the current tab), + * and updates the tab data stream accordingly. It uses the provided initial tab, + * and constructs the necessary parameters for fetching and populating the tab's table data. + * */ - public navigateToAddAccountNotePage(): void { - if ( - this.permissionsService.hasBusinessUnitPermissionAccess( - FINES_PERMISSIONS['add-account-activity-notes'], - Number(this.accountStore.business_unit_id()!), - this.userState.business_unit_users, - ) - ) { - this['router'].navigate([`../${FINES_ACC_MINOR_CREDITOR_ROUTING_PATHS.children.note}/add`], { - relativeTo: this.activatedRoute, - }); - } else { - this['router'].navigate(['/access-denied'], { - relativeTo: this.activatedRoute, - }); - } + protected override setupTabDataStream(): void { + const fragment$ = merge( + this.clearCacheOnTabChange(this.getFragmentStream('at-a-glance', this.destroy$), () => + this.opalFinesService.clearCache( + FINES_ACC_MINOR_CREDITOR_ACCOUNT_TABS_CACHE_MAP[ + this.activeTab as keyof IFinesAccMinorCreditorAccountTabsCacheMap + ], + ), + ), + this.refreshFragment$, + ); + + const { account_id } = this.accountStore.getAccountState(); + + fragment$.pipe(takeUntil(this.destroy$)).subscribe((tab) => { + switch (tab) { + case 'at-a-glance': + this.tabAtAGlance$ = this.fetchTabDataTyped( + this.opalFinesService.getMinorCreditorAccountAtAGlance(account_id), + ); + break; + } + }); + } + + /** + * Fetches the minor creditor account heading data and current tab fragment from the route. + */ + protected override getHeaderDataFromRoute(): void { + const headingData = this.activatedRoute.snapshot.data['minorCreditorAccountHeadingData']; + this.accountData = this.transformHeaderForView(headingData); + this.activeTab = this.activatedRoute.snapshot.fragment || 'at-a-glance'; + } + + /** + * Fetches the minor creditor account heading data for the specified account ID. + * @param accountId The ID of the account to fetch the heading data for. + * @returns An observable of the minor creditor account heading data. + */ + protected override getHeaderData(accountId: number): Observable { + return this.opalFinesService.getMinorCreditorAccountHeadingData(accountId); + } + + protected override transformHeaderForStore( + accountId: number, + header: IOpalFinesAccountMinorCreditorDetailsHeader, + ): void { + this.accountStore.setAccountState( + this.payloadService.transformAccountHeaderForStore(accountId, header, 'minorCreditor'), + ); + } + + protected override transformHeaderForView( + header: IOpalFinesAccountMinorCreditorDetailsHeader, + ): IOpalFinesAccountMinorCreditorDetailsHeader { + return this.payloadService.transformPayload(header, FINES_ACC_MAP_TRANSFORM_ITEMS_CONFIG); + } + + protected override transformTabData(data: T): T { + return this.payloadService.transformPayload(data, FINES_ACC_MAP_TRANSFORM_ITEMS_CONFIG); } /** @@ -124,42 +178,46 @@ export class FinesAccMinorCreditorDetailsComponent extends AbstractTabData imple * Refreshes the page. * @param event The user event that triggered the refresh action. */ - public refreshPage(): void { + public override refreshPage(): void { this.accountStore.setHasVersionMismatch(false); - this.opalFinesService - .getMinorCreditorAccountHeadingData(Number(this.accountStore.account_id())) - .pipe( - tap((minorCreditorHeadingData) => { - this.accountStore.setAccountState( - this.payloadService.transformAccountHeaderForStore( - Number(this.accountStore.account_id()), - minorCreditorHeadingData, - 'minorCreditor', - ), - ); - }), - map((minorCreditorHeadingData) => - this.payloadService.transformPayload(minorCreditorHeadingData, FINES_ACC_MAP_TRANSFORM_ITEMS_CONFIG), - ), - takeUntil(this.destroy$), - ) - .subscribe((res) => { - this.accountStore.setSuccessMessage('Information is up to date'); - this.accountData = res; - this.refreshFragment$.next(this.activeTab); - }); + super.refreshPage(Number(this.accountStore.account_id()), (header) => { + this.accountStore.setSuccessMessage('Information is up to date'); + this.accountData = header; + }); + } + + public navigateToAddPaymentHoldPage(): void { + this['router'].navigate([`../${FINES_ACC_MINOR_CREDITOR_ROUTING_PATHS.children['payment-hold']}/add`], { + relativeTo: this.activatedRoute, + }); + } + + public navigateToRemovePaymentHoldPage(): void { + this['router'].navigate([`../${FINES_ACC_MINOR_CREDITOR_ROUTING_PATHS.children['payment-hold']}/remove`], { + relativeTo: this.activatedRoute, + }); } - public ngOnInit(): void { - this.getHeaderDataFromRoute(); + /** + * Navigates to the add account note page. + * If the user lacks the required permission in this BU, navigates to the access-denied page instead. + */ + public navigateToAddAccountNotePage(): void { + if (this.hasBusinessUnitPermissionKey('add-account-activity-notes')) { + this['router'].navigate([`../${FINES_ACC_MINOR_CREDITOR_ROUTING_PATHS.children.note}/add`], { + relativeTo: this.activatedRoute, + }); + } else { + this['router'].navigate(['/access-denied'], { + relativeTo: this.activatedRoute, + }); + } } - public ngOnDestroy(): void { + public override ngOnDestroy(): void { this.accountStore.clearSuccessMessage(); this.accountStore.setHasVersionMismatch(false); - this.destroy$.next(); - this.destroy$.complete(); - this.refreshFragment$.complete(); + super.ngOnDestroy(); } } diff --git a/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/interfaces/fines-acc-minor-creditor-account-tabs-cache-map.interface.ts b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/interfaces/fines-acc-minor-creditor-account-tabs-cache-map.interface.ts new file mode 100644 index 0000000000..7ada35de8f --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-minor-creditor-details/interfaces/fines-acc-minor-creditor-account-tabs-cache-map.interface.ts @@ -0,0 +1,3 @@ +export interface IFinesAccMinorCreditorAccountTabsCacheMap { + 'at-a-glance': 'minorCreditorAccountAtAGlanceCache$'; +} diff --git a/src/app/flows/fines/fines-acc/fines-acc-payment-hold-add/fines-acc-payment-hold-add.component.html b/src/app/flows/fines/fines-acc/fines-acc-payment-hold-add/fines-acc-payment-hold-add.component.html new file mode 100644 index 0000000000..682481e680 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-payment-hold-add/fines-acc-payment-hold-add.component.html @@ -0,0 +1 @@ +

Add Payment Hold

diff --git a/src/app/flows/fines/fines-acc/fines-acc-payment-hold-add/fines-acc-payment-hold-add.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-payment-hold-add/fines-acc-payment-hold-add.component.spec.ts new file mode 100644 index 0000000000..477aedb85e --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-payment-hold-add/fines-acc-payment-hold-add.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FinesAccPaymentHoldAddComponent } from './fines-acc-payment-hold-add.component'; +import { beforeEach, describe, expect, it } from 'vitest'; + +describe('FinesAccPaymentHoldAddComponent', () => { + let component: FinesAccPaymentHoldAddComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FinesAccPaymentHoldAddComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FinesAccPaymentHoldAddComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/flows/fines/fines-acc/fines-acc-payment-hold-add/fines-acc-payment-hold-add.component.ts b/src/app/flows/fines/fines-acc/fines-acc-payment-hold-add/fines-acc-payment-hold-add.component.ts new file mode 100644 index 0000000000..2937dbf7df --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-payment-hold-add/fines-acc-payment-hold-add.component.ts @@ -0,0 +1,8 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'app-acc-payment-hold-add', + templateUrl: './fines-acc-payment-hold-add.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FinesAccPaymentHoldAddComponent {} diff --git a/src/app/flows/fines/fines-acc/fines-acc-payment-hold-remove/fines-acc-payment-hold-remove.component.html b/src/app/flows/fines/fines-acc/fines-acc-payment-hold-remove/fines-acc-payment-hold-remove.component.html new file mode 100644 index 0000000000..166152fec6 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-payment-hold-remove/fines-acc-payment-hold-remove.component.html @@ -0,0 +1 @@ +

Remove Payment Hold

diff --git a/src/app/flows/fines/fines-acc/fines-acc-payment-hold-remove/fines-acc-payment-hold-remove.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-payment-hold-remove/fines-acc-payment-hold-remove.component.spec.ts new file mode 100644 index 0000000000..d9eb44f976 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-payment-hold-remove/fines-acc-payment-hold-remove.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FinesAccPaymentHoldRemoveComponent } from './fines-acc-payment-hold-remove.component'; +import { beforeEach, describe, expect, it } from 'vitest'; + +describe('FinesAccPaymentHoldRemoveComponent', () => { + let component: FinesAccPaymentHoldRemoveComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FinesAccPaymentHoldRemoveComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FinesAccPaymentHoldRemoveComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/flows/fines/fines-acc/fines-acc-payment-hold-remove/fines-acc-payment-hold-remove.component.ts b/src/app/flows/fines/fines-acc/fines-acc-payment-hold-remove/fines-acc-payment-hold-remove.component.ts new file mode 100644 index 0000000000..0e3cbea134 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-payment-hold-remove/fines-acc-payment-hold-remove.component.ts @@ -0,0 +1,8 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'app-acc-payment-hold-remove', + templateUrl: './fines-acc-payment-hold-remove.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FinesAccPaymentHoldRemoveComponent {} diff --git a/src/app/flows/fines/fines-acc/routing/constants/fines-acc-minor-creditor-routing-paths.constant.ts b/src/app/flows/fines/fines-acc/routing/constants/fines-acc-minor-creditor-routing-paths.constant.ts index ad04021678..b99f905a1f 100644 --- a/src/app/flows/fines/fines-acc/routing/constants/fines-acc-minor-creditor-routing-paths.constant.ts +++ b/src/app/flows/fines/fines-acc/routing/constants/fines-acc-minor-creditor-routing-paths.constant.ts @@ -5,5 +5,6 @@ export const FINES_ACC_MINOR_CREDITOR_ROUTING_PATHS: IFinesAccMinorCreditorRouti children: { details: 'details', note: 'note', + 'payment-hold': 'payment-hold', }, }; diff --git a/src/app/flows/fines/fines-acc/routing/constants/fines-acc-minor-creditor-routing-titles.constant.ts b/src/app/flows/fines/fines-acc/routing/constants/fines-acc-minor-creditor-routing-titles.constant.ts index 6420277464..be75880d66 100644 --- a/src/app/flows/fines/fines-acc/routing/constants/fines-acc-minor-creditor-routing-titles.constant.ts +++ b/src/app/flows/fines/fines-acc/routing/constants/fines-acc-minor-creditor-routing-titles.constant.ts @@ -5,5 +5,6 @@ export const FINES_ACC_MINOR_CREDITOR_ROUTING_TITLES: IFinesAccMinorCreditorRout children: { details: 'Account details', note: 'Add note', + 'payment-hold': 'Payment hold', }, }; 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 acf3c220ec..ec0d0dba22 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 @@ -202,6 +202,34 @@ export const routing: Routes = [ }, resolve: { title: TitleResolver }, }, + { + path: `${FINES_ACC_MINOR_CREDITOR_ROUTING_PATHS.children['payment-hold']}/add`, + + loadComponent: () => + import('../fines-acc-payment-hold-add/fines-acc-payment-hold-add.component').then( + (c) => c.FinesAccPaymentHoldAddComponent, + ), + canActivate: [authGuard, finesAccStateGuard], + canDeactivate: [canDeactivateGuard], + data: { + title: FINES_ACC_MINOR_CREDITOR_ROUTING_TITLES.children['payment-hold'], + }, + resolve: { title: TitleResolver }, + }, + { + path: `${FINES_ACC_MINOR_CREDITOR_ROUTING_PATHS.children['payment-hold']}/remove`, + + loadComponent: () => + import('../fines-acc-payment-hold-remove/fines-acc-payment-hold-remove.component').then( + (c) => c.FinesAccPaymentHoldRemoveComponent, + ), + canActivate: [authGuard, finesAccStateGuard], + canDeactivate: [canDeactivateGuard], + data: { + title: FINES_ACC_MINOR_CREDITOR_ROUTING_TITLES.children['payment-hold'], + }, + resolve: { title: TitleResolver }, + }, ], }, ]; diff --git a/src/app/flows/fines/fines-acc/routing/interfaces/fines-acc-minor-creditor-routing-paths.interface.ts b/src/app/flows/fines/fines-acc/routing/interfaces/fines-acc-minor-creditor-routing-paths.interface.ts index 6ffe6581c4..71b584ee23 100644 --- a/src/app/flows/fines/fines-acc/routing/interfaces/fines-acc-minor-creditor-routing-paths.interface.ts +++ b/src/app/flows/fines/fines-acc/routing/interfaces/fines-acc-minor-creditor-routing-paths.interface.ts @@ -5,5 +5,6 @@ export interface IFinesAccMinorCreditorRoutingPaths extends IChildRoutingPaths { children: { details: string; note: string; + 'payment-hold': string; }; } diff --git a/src/app/flows/fines/fines-acc/services/constants/fines-acc-map-transform-items-config.constant.ts b/src/app/flows/fines/fines-acc/services/constants/fines-acc-map-transform-items-config.constant.ts index 48050eab1e..e188fab5fb 100644 --- a/src/app/flows/fines/fines-acc/services/constants/fines-acc-map-transform-items-config.constant.ts +++ b/src/app/flows/fines/fines-acc/services/constants/fines-acc-map-transform-items-config.constant.ts @@ -19,4 +19,5 @@ export const FINES_ACC_MAP_TRANSFORM_ITEMS_CONFIG: ITransformItem[] = [ { key: 'posted_date', ...MAP_PAYLOAD_DATE_FORMAT }, { key: 'payment_card_last_requested', ...MAP_PAYLOAD_DATE_FORMAT }, { key: 'date_notice_issued', ...MAP_PAYLOAD_DATE_FORMAT }, + { key: 'hearing_date', ...MAP_PAYLOAD_DATE_FORMAT }, ]; diff --git a/src/app/flows/fines/services/opal-fines-service/constants/opal-fines-cache-defaults.constant.ts b/src/app/flows/fines/services/opal-fines-service/constants/opal-fines-cache-defaults.constant.ts index d8c9a83538..8a088d306d 100644 --- a/src/app/flows/fines/services/opal-fines-service/constants/opal-fines-cache-defaults.constant.ts +++ b/src/app/flows/fines/services/opal-fines-service/constants/opal-fines-cache-defaults.constant.ts @@ -19,4 +19,5 @@ export const OPAL_FINES_CACHE_DEFAULTS: IOpalFinesCache = { defendantAccountHistoryAndNotesCache$: null, defendantAccountPaymentTermsLatestCache$: null, defendantAccountFixedPenaltyCache$: null, + minorCreditorAccountAtAGlanceCache$: null, }; diff --git a/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-minor-creditor-at-a-glance.interface.ts b/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-minor-creditor-at-a-glance.interface.ts new file mode 100644 index 0000000000..738dbae4a7 --- /dev/null +++ b/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-minor-creditor-at-a-glance.interface.ts @@ -0,0 +1,19 @@ +import { IOpalFinesDefendantAccountAddress } from './opal-fines-defendant-account-address.interface'; +import { IOpalFinesDefendantAccountPartyDetails } from './opal-fines-defendant-account-party-details.interface'; + +export interface IOpalFinesAccountMinorCreditorAtAGlance { + version: string | null; + party: IOpalFinesDefendantAccountPartyDetails; + address: IOpalFinesDefendantAccountAddress; + creditor_account_id: string; + defendant?: { + account_number: string; + id: number; + name: string; + hearing_date: string; + }; + payment: { + is_bacs: boolean; + hold_payment: boolean; + }; +} diff --git a/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-cache.interface.ts b/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-cache.interface.ts index f6fc28668b..2cf075bb5b 100644 --- a/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-cache.interface.ts +++ b/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-cache.interface.ts @@ -15,6 +15,7 @@ import { IOpalFinesAccountDefendantDetailsHistoryAndNotesTabRefData } from './op import { IOpalFinesAccountDefendantDetailsPaymentTermsLatest } from './opal-fines-account-defendant-details-payment-terms-latest.interface'; import { IOpalFinesAccountDefendantDetailsFixedPenaltyTabRefData } from './opal-fines-account-defendant-details-fixed-penalty-tab-ref-data.interface'; import { IOpalFinesResultRefData } from './opal-fines-result-ref-data.interface'; +import { IOpalFinesAccountMinorCreditorAtAGlance } from './opal-fines-account-minor-creditor-at-a-glance.interface'; export interface IOpalFinesCache { courtRefDataCache$: { [key: string]: Observable }; @@ -35,4 +36,5 @@ export interface IOpalFinesCache { defendantAccountHistoryAndNotesCache$: Observable | null; defendantAccountPaymentTermsLatestCache$: Observable | null; defendantAccountFixedPenaltyCache$: Observable | null; + minorCreditorAccountAtAGlanceCache$: Observable | null; } diff --git a/src/app/flows/fines/services/opal-fines-service/mocks/opal-fines-account-minor-creditor-at-a-glance-with-defendant.mock.ts b/src/app/flows/fines/services/opal-fines-service/mocks/opal-fines-account-minor-creditor-at-a-glance-with-defendant.mock.ts new file mode 100644 index 0000000000..0e88d76c14 --- /dev/null +++ b/src/app/flows/fines/services/opal-fines-service/mocks/opal-fines-account-minor-creditor-at-a-glance-with-defendant.mock.ts @@ -0,0 +1,34 @@ +import { IOpalFinesAccountMinorCreditorAtAGlance } from '../interfaces/opal-fines-account-minor-creditor-at-a-glance.interface'; + +export const OPAL_FINES_ACCOUNT_MINOR_CREDITOR_AT_A_GLANCE_WITH_DEFENDANT_MOCK: IOpalFinesAccountMinorCreditorAtAGlance = + { + version: null, + creditor_account_id: 'ACC-123456', + party: { + party_id: 'PARTY-001', + organisation_flag: true, + organisation_details: { + organisation_name: 'Test Organisation', + organisation_aliases: null, + }, + individual_details: null, + }, + address: { + address_line_1: '123 Main Street', + address_line_2: 'Apt 4', + address_line_3: null, + address_line_4: null, + address_line_5: null, + postcode: 'AB12 3CD', + }, + defendant: { + account_number: 'ACC-654321', + id: 123456789, + name: 'John Doe', + hearing_date: '07/07/2026', + }, + payment: { + is_bacs: true, + hold_payment: false, + }, + }; diff --git a/src/app/flows/fines/services/opal-fines-service/mocks/opal-fines-account-minor-creditor-at-a-glance-without-defendant.mock.ts b/src/app/flows/fines/services/opal-fines-service/mocks/opal-fines-account-minor-creditor-at-a-glance-without-defendant.mock.ts new file mode 100644 index 0000000000..ae86291afe --- /dev/null +++ b/src/app/flows/fines/services/opal-fines-service/mocks/opal-fines-account-minor-creditor-at-a-glance-without-defendant.mock.ts @@ -0,0 +1,28 @@ +import { IOpalFinesAccountMinorCreditorAtAGlance } from '../interfaces/opal-fines-account-minor-creditor-at-a-glance.interface'; + +export const OPAL_FINES_ACCOUNT_MINOR_CREDITOR_AT_A_GLANCE_WITHOUT_DEFENDANT_MOCK: IOpalFinesAccountMinorCreditorAtAGlance = + { + version: null, + creditor_account_id: 'ACC-123456', + party: { + party_id: 'PARTY-001', + organisation_flag: true, + organisation_details: { + organisation_name: 'Test Organisation', + organisation_aliases: null, + }, + individual_details: null, + }, + address: { + address_line_1: '123 Main Street', + address_line_2: 'Apt 4', + address_line_3: null, + address_line_4: null, + address_line_5: null, + postcode: 'AB12 3CD', + }, + payment: { + is_bacs: false, + hold_payment: false, + }, + }; diff --git a/src/app/flows/fines/services/opal-fines-service/opal-fines.service.spec.ts b/src/app/flows/fines/services/opal-fines-service/opal-fines.service.spec.ts index b38cc9c8b1..4410a930e6 100644 --- a/src/app/flows/fines/services/opal-fines-service/opal-fines.service.spec.ts +++ b/src/app/flows/fines/services/opal-fines-service/opal-fines.service.spec.ts @@ -33,6 +33,7 @@ import { OPAL_FINES_DRAFT_ACCOUNTS_PATCH_PAYLOAD } from './mocks/opal-fines-draf import { OPAL_FINES_PROSECUTOR_REF_DATA_MOCK } from './mocks/opal-fines-prosecutor-ref-data.mock'; import { FINES_ACC_DEFENDANT_DETAILS_HEADER_MOCK } from '../../fines-acc/fines-acc-defendant-details/mocks/fines-acc-defendant-details-header.mock'; import { OPAL_FINES_ACCOUNT_DEFENDANT_AT_A_GLANCE_MOCK } from './mocks/opal-fines-account-defendant-at-a-glance.mock'; +import { OPAL_FINES_ACCOUNT_MINOR_CREDITOR_AT_A_GLANCE_WITH_DEFENDANT_MOCK } from './mocks/opal-fines-account-minor-creditor-at-a-glance-with-defendant.mock'; import { OPAL_FINES_ADD_NOTE_PAYLOAD_MOCK } from './mocks/opal-fines-add-note-payload.mock'; import { OPAL_FINES_ADD_NOTE_RESPONSE_MOCK } from './mocks/opal-fines-add-note-response.mock'; import { IOpalFinesAddNotePayload } from './interfaces/opal-fines-add-note.interface'; @@ -755,6 +756,18 @@ describe('OpalFines', () => { req.flush(expectedResponse); }); + it('should getMinorCreditorAccountAtAGlance data', () => { + const account_id: number = 77; + const expectedResponse = OPAL_FINES_ACCOUNT_MINOR_CREDITOR_AT_A_GLANCE_WITH_DEFENDANT_MOCK; + const apiUrl = `${OPAL_FINES_PATHS.minorCreditorAccounts}/${account_id}/at-a-glance`; + + service.getMinorCreditorAccountAtAGlance(account_id).subscribe((response) => { + expect(response).toEqual(expectedResponse); + }); + + httpMock.expectNone(apiUrl); + }); + it('should getDefendantAccountParty', () => { const account_id: number = 77; const apiUrl = `${OPAL_FINES_PATHS.defendantAccounts}/${account_id}/defendant-account-parties/${FINES_ACC_DEFENDANT_DETAILS_HEADER_MOCK.defendant_account_party_id}`; diff --git a/src/app/flows/fines/services/opal-fines-service/opal-fines.service.ts b/src/app/flows/fines/services/opal-fines-service/opal-fines.service.ts index 6ef0507124..8ea6f3a948 100644 --- a/src/app/flows/fines/services/opal-fines-service/opal-fines.service.ts +++ b/src/app/flows/fines/services/opal-fines-service/opal-fines.service.ts @@ -50,6 +50,8 @@ import { IOpalFinesAccountDefendantDetailsFixedPenaltyTabRefData } from './inter import { IOpalFinesResultRefData } from './interfaces/opal-fines-result-ref-data.interface'; import { IOpalFinesAccountMinorCreditorDetailsHeader } from '../../fines-acc/fines-acc-minor-creditor-details/interfaces/fines-acc-minor-creditor-details-header.interface'; import { IOpalFinesAccountRequestPaymentCardResponse } from './interfaces/opal-fines-account-request-payment-card-response.interface'; +import { IOpalFinesAccountMinorCreditorAtAGlance } from './interfaces/opal-fines-account-minor-creditor-at-a-glance.interface'; +import { OPAL_FINES_ACCOUNT_MINOR_CREDITOR_AT_A_GLANCE_WITH_DEFENDANT_MOCK } from './mocks/opal-fines-account-minor-creditor-at-a-glance-with-defendant.mock'; @Injectable({ providedIn: 'root', @@ -979,4 +981,34 @@ export class OpalFines { } return this.http.post(url, {}, { headers }); } + + /** + * Retrieves the minor creditor account details at a glance for a specific tab. + * If the account details for the specified tab are not already cached, it makes an HTTP request to fetch the data and caches it for future use. + * + * @param account_id - The ID of the minor creditor account. + * @returns An Observable that emits the account details for the at a glance tab. + */ + public getMinorCreditorAccountAtAGlance( + account_id: number | null, + ): Observable { + if (!this.cache.minorCreditorAccountAtAGlanceCache$) { + const url = `${OPAL_FINES_PATHS.minorCreditorAccounts}/${account_id}/at-a-glance`; + this.cache.minorCreditorAccountAtAGlanceCache$ = + of(OPAL_FINES_ACCOUNT_MINOR_CREDITOR_AT_A_GLANCE_WITH_DEFENDANT_MOCK) ?? + this.http.get(url, { observe: 'response' }).pipe( + map((response: HttpResponse) => { + const version = this.extractEtagVersion(response.headers); + const payload = response.body as IOpalFinesAccountMinorCreditorAtAGlance; + return { + ...payload, + version, + }; + }), + shareReplay(1), + ); + } + + return this.cache.minorCreditorAccountAtAGlanceCache$; + } }