From a4f2eefb7408496f911f855f043142beac2c7594 Mon Sep 17 00:00:00 2001 From: louisbriggs Date: Tue, 21 Jul 2026 17:00:36 +0100 Subject: [PATCH 01/12] Service call, interface and mock for consoldated accounts tab data --- .../opal-fines-cache-defaults.constant.ts | 1 + ...-account-consolidated-account.interface.ts | 9 +++++++ ...account-consolidated-accounts.interface.ts | 6 +++++ .../interfaces/opal-fines-cache.interface.ts | 2 ++ ...dant-details-consolidated-accounts.mock.ts | 17 +++++++++++++ .../opal-fines-service/opal-fines.service.ts | 24 +++++++++++++++++++ 6 files changed, 59 insertions(+) create mode 100644 src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-account.interface.ts create mode 100644 src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-accounts.interface.ts create mode 100644 src/app/flows/fines/services/opal-fines-service/mocks/opal-fines-account-defendant-details-consolidated-accounts.mock.ts 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 a751a05ffe..6237b89c8a 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 @@ -20,6 +20,7 @@ export const OPAL_FINES_CACHE_DEFAULTS: IOpalFinesCache = { defendantAccountHistoryAndNotesCache$: null, defendantAccountPaymentTermsLatestCache$: null, defendantAccountFixedPenaltyCache$: null, + defendantAccountConsolidatedAccountsCache$: null, minorCreditorAccountAtAGlanceCache$: null, minorCreditorAccountCreditorCache$: null, majorCreditorAccountAtAGlanceCache$: null, diff --git a/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-account.interface.ts b/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-account.interface.ts new file mode 100644 index 0000000000..137879c0df --- /dev/null +++ b/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-account.interface.ts @@ -0,0 +1,9 @@ +export interface IOpalFinesAccountDefendantDetailsConsolidatedAccount { + account_id: number; + account_number: string; + date_imposed: string; + first_name: string; + imposed_by: string; + last_name: string; + reference: string; +} diff --git a/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-accounts.interface.ts b/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-accounts.interface.ts new file mode 100644 index 0000000000..9b731f9840 --- /dev/null +++ b/src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-accounts.interface.ts @@ -0,0 +1,6 @@ +import { IOpalFinesAccountDefendantDetailsConsolidatedAccount } from './opal-fines-account-defendant-account-consolidated-account.interface'; +import { IOpalFinesVersion } from './opal-fines-version.interface'; + +export interface IOpalFinesAccountDefendantDetailsConsolidatedAccounts extends IOpalFinesVersion { + consolidated_accounts: IOpalFinesAccountDefendantDetailsConsolidatedAccount[]; +} 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 770eacee13..d429f5fab0 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 @@ -1,4 +1,5 @@ import { Observable } from 'rxjs'; +import { IOpalFinesAccountDefendantDetailsConsolidatedAccounts } from './opal-fines-account-defendant-account-consolidated-accounts.interface'; import { IOpalFinesCourtRefData } from './opal-fines-court-ref-data.interface'; import { IOpalFinesBusinessUnitRefData } from './opal-fines-business-unit-ref-data.interface'; import { IOpalFinesLocalJusticeAreaRefData } from './opal-fines-local-justice-area-ref-data.interface'; @@ -38,6 +39,7 @@ export interface IOpalFinesCache { defendantAccountParentOrGuardianAccountPartyCache$: Observable | null; defendantAccountEnforcementCache$: Observable | null; defendantAccountImpositionsCache$: Observable | null; + defendantAccountConsolidatedAccountsCache$: Observable | null; defendantAccountHistoryAndNotesCache$: Observable | null; defendantAccountPaymentTermsLatestCache$: Observable | null; defendantAccountFixedPenaltyCache$: Observable | null; diff --git a/src/app/flows/fines/services/opal-fines-service/mocks/opal-fines-account-defendant-details-consolidated-accounts.mock.ts b/src/app/flows/fines/services/opal-fines-service/mocks/opal-fines-account-defendant-details-consolidated-accounts.mock.ts new file mode 100644 index 0000000000..7e4de58237 --- /dev/null +++ b/src/app/flows/fines/services/opal-fines-service/mocks/opal-fines-account-defendant-details-consolidated-accounts.mock.ts @@ -0,0 +1,17 @@ +import { IOpalFinesAccountDefendantDetailsConsolidatedAccounts } from '../interfaces/opal-fines-account-defendant-account-consolidated-accounts.interface'; + +export const OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK: IOpalFinesAccountDefendantDetailsConsolidatedAccounts = + { + consolidated_accounts: [ + { + account_id: 99000000990002, + account_number: '99009902C', + date_imposed: '2026-01-21', + first_name: 'Casey', + imposed_by: 'Seed Child Court', + last_name: 'Child', + reference: 'LOCAL-CONSOL-CHILD', + }, + ], + version: '1', + }; 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 c77e96b8a2..346d06f2a8 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 @@ -66,6 +66,8 @@ import { IOpalFinesDraftAccountPatchRequestPayload } from '@services/fines/opal- import { IOpalFinesDeleteDefendantAccountPartyPayload } from './interfaces/opal-fines-delete-defendant-account-party-payload.interface'; import { IOpalFinesAccountMajorCreditorDetailsHeader } from '../../fines-acc/fines-acc-major-creditor-details/interfaces/fines-acc-major-creditor-details-header.interface'; import { IOpalFinesAccountMajorCreditorAtAGlance } from './interfaces/opal-fines-account-major-creditor-at-a-glance.interface'; +import { IOpalFinesAccountDefendantDetailsConsolidatedAccount } from './interfaces/opal-fines-account-defendant-account-consolidated-account.interface'; +import { IOpalFinesAccountDefendantDetailsConsolidatedAccounts } from './interfaces/opal-fines-account-defendant-account-consolidated-accounts.interface'; @Injectable({ providedIn: 'root', @@ -506,6 +508,7 @@ export class OpalFines { 'defendantAccountHistoryAndNotesCache$', 'defendantAccountPaymentTermsLatestCache$', 'defendantAccountFixedPenaltyCache$', + 'defendantAccountConsolidatedAccountsCache$', 'minorCreditorAccountAtAGlanceCache$', 'minorCreditorAccountCreditorCache$', 'minorCreditorAccountHistoryAndNotesCache$', @@ -1415,4 +1418,25 @@ export class OpalFines { } return this.cache.majorCreditorAccountAtAGlanceCache$; } + + public getDefendantAccountConsolidatedAccounts( + account_id: number | null, + ): Observable { + if (!this.cache.defendantAccountConsolidatedAccountsCache$) { + const url = `${OPAL_FINES_PATHS.defendantAccounts}/${account_id}/consolidated-accounts`; + this.cache.defendantAccountConsolidatedAccountsCache$ = this.http + .get(url, { observe: 'response' }) + .pipe( + map((response: HttpResponse) => { + const version = this.extractEtagVersion(response.headers); + return { + consolidated_accounts: response.body ?? [], + version, + }; + }), + shareReplay(1), + ); + } + return this.cache.defendantAccountConsolidatedAccountsCache$; + } } From 04f4e8f69262f1a9c5830141c85e130e6711a783 Mon Sep 17 00:00:00 2001 From: louisbriggs Date: Wed, 22 Jul 2026 09:25:01 +0100 Subject: [PATCH 02/12] Adding template for consolidated accounts tab --- ...c-defendant-account-tabs-cache-map.constant.ts | 1 + ...tails-consolidated-accounts-tab.component.html | 5 +++++ ...details-consolidated-accounts-tab.component.ts | 15 +++++++++++++++ .../fines-acc-defendant-details.component.html | 8 ++++++++ .../fines-acc-defendant-details.component.ts | 9 +++++++++ ...-defendant-account-tabs-cache-map.interface.ts | 1 + 6 files changed, 39 insertions(+) create mode 100644 src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.html create mode 100644 src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.ts diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/constants/fines-acc-defendant-account-tabs-cache-map.constant.ts b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/constants/fines-acc-defendant-account-tabs-cache-map.constant.ts index 749278c162..11424a4b7d 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/constants/fines-acc-defendant-account-tabs-cache-map.constant.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/constants/fines-acc-defendant-account-tabs-cache-map.constant.ts @@ -9,4 +9,5 @@ export const FINES_ACC_DEFENDANT_ACCOUNT_TABS_CACHE_MAP: IFinesAccDefendantAccou enforcement: 'defendantAccountEnforcementCache$', impositions: 'defendantAccountImpositionsCache$', 'history-and-notes': 'defendantAccountHistoryAndNotesCache$', + 'consolidated-accounts': 'defendantAccountConsolidatedAccountsCache$', }; diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.html b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.html new file mode 100644 index 0000000000..113671b3dc --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.html @@ -0,0 +1,5 @@ +
+

Consolidated accounts

+
+

{{ tabData.consolidated_accounts[0].first_name }}

+
diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.ts b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.ts new file mode 100644 index 0000000000..369c4e24fc --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { IOpalFinesAccountDefendantDetailsConsolidatedAccounts } from '@services/fines/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-accounts.interface'; +import { FINES_ACC_SUMMARY_TABS_CONTENT_STYLES } from '../../constants/fines-acc-summary-tabs-content-styles.constant'; +import { IFinesAccSummaryTabsContentStyles } from '../interfaces/fines-acc-summary-tabs-content-styles.interface'; + +@Component({ + selector: 'app-fines-acc-defendant-details-consolidated-accounts-tab', + imports: [], + templateUrl: './fines-acc-defendant-details-consolidated-accounts-tab.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FinesAccDefendantDetailsConsolidatedAccountsTabComponent { + @Input({ required: true }) public tabData!: IOpalFinesAccountDefendantDetailsConsolidatedAccounts; + @Input() public style: IFinesAccSummaryTabsContentStyles = FINES_ACC_SUMMARY_TABS_CONTENT_STYLES; +} 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 c2d69c80df..5853b171cd 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 @@ -232,4 +232,12 @@

Business Unit:

[style]="tabContentStyles" > } + @case ('consolidated-accounts') { + @if (tabConsolidatedAccounts$ | async; as tabData) { + + } + } } 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 dae6ead7d4..db8e055df7 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 @@ -56,6 +56,8 @@ import { AbstractAccountSummaryBaseComponent } from '@hmcts/opal-frontend-common import { IOpalFinesVersion } from '../../services/opal-fines-service/interfaces/opal-fines-version.interface'; import { FINES_ACC_BANNER_MESSAGES } from '../stores/constants/fines-acc-store-banner-messages.constant'; import { FinesAccDefendantDetailsHistoryAndNotesTabComponent } from './fines-acc-defendant-details-history-and-notes-tab/fines-acc-defendant-details-history-and-notes-tab.component'; +import { IOpalFinesAccountDefendantDetailsConsolidatedAccounts } from '@services/fines/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-accounts.interface'; +import { FinesAccDefendantDetailsConsolidatedAccountsTabComponent } from './fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component'; @Component({ selector: 'app-fines-acc-defendant-details', @@ -83,6 +85,7 @@ import { FinesAccDefendantDetailsHistoryAndNotesTabComponent } from './fines-acc MonetaryPipe, FinesAccSummaryHeaderComponent, FinesAccDefendantDetailsHistoryAndNotesTabComponent, + FinesAccDefendantDetailsConsolidatedAccountsTabComponent, ], templateUrl: './fines-acc-defendant-details.component.html', changeDetection: ChangeDetectionStrategy.OnPush, @@ -106,6 +109,7 @@ export class FinesAccDefendantDetailsComponent public tabImpositions$: Observable = EMPTY; public tabHistoryAndNotes$: Observable = EMPTY; public tabFixedPenalty$: Observable = EMPTY; + public tabConsolidatedAccounts$: Observable = EMPTY; public debtorTypes = FINES_ACC_DEBTOR_TYPES; public accountTypes = FINES_ACCOUNT_TYPES; public lastEnforcement: IOpalFinesResultRefData | null = null; @@ -189,6 +193,11 @@ export class FinesAccDefendantDetailsComponent this.opalFinesService.getDefendantAccountHistoryAndNotesTabData(account_id), ); break; + case 'consolidated-accounts': + this.tabConsolidatedAccounts$ = this.fetchTabDataTyped( + this.opalFinesService.getDefendantAccountConsolidatedAccounts(account_id), + ); + break; } }); } diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/interfaces/fines-acc-defendant-account-tabs-cache-map.interface.ts b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/interfaces/fines-acc-defendant-account-tabs-cache-map.interface.ts index 4c683a595f..74a585b470 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/interfaces/fines-acc-defendant-account-tabs-cache-map.interface.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/interfaces/fines-acc-defendant-account-tabs-cache-map.interface.ts @@ -7,4 +7,5 @@ export interface IFinesAccDefendantAccountTabsCacheMap { enforcement: 'defendantAccountEnforcementCache$'; impositions: 'defendantAccountImpositionsCache$'; 'history-and-notes': 'defendantAccountHistoryAndNotesCache$'; + 'consolidated-accounts': 'defendantAccountConsolidatedAccountsCache$'; } From cc191382f3d13112a8815c822a25dd59cfb0058b Mon Sep 17 00:00:00 2001 From: louisbriggs Date: Wed, 22 Jul 2026 09:55:47 +0100 Subject: [PATCH 03/12] Adding display table for consolidated accounts --- ...s-consolidated-accounts-tab.component.html | 50 ++++++++++++++++++- ...ils-consolidated-accounts-tab.component.ts | 39 ++++++++++++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.html b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.html index 113671b3dc..710b0e01ad 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.html +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.html @@ -1,5 +1,53 @@

Consolidated accounts


-

{{ tabData.consolidated_accounts[0].first_name }}

+ + @if (tabData.consolidated_accounts.length) { + + + + + + + + + + + + + @for (account of tabData.consolidated_accounts; track account.account_id; let rowIndex = $index) { + + + + + + + + } + +
AccountNameDate imposedImposed byReference
+ + {{ account.account_number }} + + + {{ getDefendantName(account) }} + + {{ account.date_imposed | dateFormat: DATE_INPUT_FORMAT : DATE_OUTPUT_FORMAT }} + + {{ account.imposed_by }} + + {{ account.reference }} +
+
+ } @else { +

There are no consolidated accounts for this account.

+ }
diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.ts b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.ts index 369c4e24fc..5b6e9e1925 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.ts @@ -1,15 +1,52 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { CustomHorizontalScrollPaneComponent } from '@hmcts/opal-frontend-common/components/custom/custom-horizontal-scroll-pane'; +import { DateFormatPipe } from '@hmcts/opal-frontend-common/pipes/date-format'; +import { FINES_ROUTING_PATHS } from '@routing/fines/constants/fines-routing-paths.constant'; +import { IOpalFinesAccountDefendantDetailsConsolidatedAccount } from '@services/fines/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-account.interface'; import { IOpalFinesAccountDefendantDetailsConsolidatedAccounts } from '@services/fines/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-accounts.interface'; import { FINES_ACC_SUMMARY_TABS_CONTENT_STYLES } from '../../constants/fines-acc-summary-tabs-content-styles.constant'; +import { FINES_ACC_DEFENDANT_ROUTING_PATHS } from '../../routing/constants/fines-acc-defendant-routing-paths.constant'; +import { FINES_ACC_ROUTING_PATHS } from '../../routing/constants/fines-acc-routing-paths.constant'; import { IFinesAccSummaryTabsContentStyles } from '../interfaces/fines-acc-summary-tabs-content-styles.interface'; @Component({ selector: 'app-fines-acc-defendant-details-consolidated-accounts-tab', - imports: [], + imports: [CustomHorizontalScrollPaneComponent, DateFormatPipe, RouterLink], templateUrl: './fines-acc-defendant-details-consolidated-accounts-tab.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class FinesAccDefendantDetailsConsolidatedAccountsTabComponent { + protected readonly DATE_INPUT_FORMAT = 'yyyy-MM-dd'; + protected readonly DATE_OUTPUT_FORMAT = 'dd MMM yyyy'; + @Input({ required: true }) public tabData!: IOpalFinesAccountDefendantDetailsConsolidatedAccounts; @Input() public style: IFinesAccSummaryTabsContentStyles = FINES_ACC_SUMMARY_TABS_CONTENT_STYLES; + + /** + * Builds the route to a child account's at-a-glance view. + * + * @param accountId - The child defendant account id. + * @returns The router link commands for the defendant account details page. + */ + public getAccountRouterLink(accountId: number): Array { + return [ + '/', + FINES_ROUTING_PATHS.root, + FINES_ACC_ROUTING_PATHS.root, + FINES_ACC_DEFENDANT_ROUTING_PATHS.root, + accountId, + FINES_ACC_DEFENDANT_ROUTING_PATHS.children.details, + ]; + } + + /** + * Formats the consolidated account defendant name for display. + * + * @param account - The consolidated account row. + * @returns Defendant name in SURNAME, Forename format. + */ + public getDefendantName(account: IOpalFinesAccountDefendantDetailsConsolidatedAccount): string { + return `${account.last_name.toUpperCase()}, ${account.first_name}`; + } } From be9b78ec16f42a7dd57aa39399afed6aa9f7a07a Mon Sep 17 00:00:00 2001 From: louisbriggs Date: Wed, 22 Jul 2026 10:11:01 +0100 Subject: [PATCH 04/12] Adding unit test coverage for consolidated accounts --- ...onsolidated-accounts-tab.component.spec.ts | 75 +++++++++++++++++++ ...es-acc-defendant-details.component.spec.ts | 22 ++++++ .../opal-fines.service.spec.ts | 45 +++++++++++ 3 files changed, 142 insertions(+) create mode 100644 src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.spec.ts diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.spec.ts new file mode 100644 index 0000000000..8b43401da7 --- /dev/null +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.spec.ts @@ -0,0 +1,75 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK } from '@services/fines/opal-fines-service/mocks/opal-fines-account-defendant-details-consolidated-accounts.mock'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { FinesAccDefendantDetailsConsolidatedAccountsTabComponent } from './fines-acc-defendant-details-consolidated-accounts-tab.component'; + +describe('FinesAccDefendantDetailsConsolidatedAccountsTabComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FinesAccDefendantDetailsConsolidatedAccountsTabComponent], + providers: [provideRouter([])], + }).compileComponents(); + }); + + const setupComponent = ( + tabData = structuredClone(OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK), + ): { + component: FinesAccDefendantDetailsConsolidatedAccountsTabComponent; + fixture: ComponentFixture; + } => { + const fixture = TestBed.createComponent(FinesAccDefendantDetailsConsolidatedAccountsTabComponent); + const component = fixture.componentInstance; + + fixture.componentRef.setInput('tabData', tabData); + fixture.detectChanges(); + + return { component, fixture }; + }; + + it('should create', () => { + const { component } = setupComponent(); + + expect(component).toBeTruthy(); + }); + + it('should render consolidated account rows', () => { + const { fixture } = setupComponent(); + const textContent = fixture.nativeElement.textContent; + + expect(fixture.nativeElement.querySelector('table.govuk-table')).toBeTruthy(); + expect(textContent).toContain('99009902C'); + expect(textContent).toContain('CHILD, Casey'); + expect(textContent).toContain('21 Jan 2026'); + expect(textContent).toContain('Seed Child Court'); + expect(textContent).toContain('LOCAL-CONSOL-CHILD'); + }); + + it('should render account number as a link to the child account at-a-glance view', () => { + const { fixture } = setupComponent(); + const link = fixture.nativeElement.querySelector('#consolidated-account-number-0 a') as HTMLAnchorElement; + + expect(link).toBeTruthy(); + expect(link.textContent).toContain('99009902C'); + expect(link.getAttribute('href')).toBe('/fines/account/defendant/99000000990002/details#at-a-glance'); + }); + + it('should right-align the date imposed column', () => { + const { fixture } = setupComponent(); + const dateCell = fixture.nativeElement.querySelector( + '#consolidated-account-date-imposed-0', + ) as HTMLTableCellElement; + + expect(dateCell.classList).toContain('govuk-table__cell--numeric'); + }); + + it('should render an empty state when there are no consolidated accounts', () => { + const tabData = structuredClone(OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK); + tabData.consolidated_accounts = []; + + const { fixture } = setupComponent(tabData); + + expect(fixture.nativeElement.querySelector('table.govuk-table')).toBeNull(); + expect(fixture.nativeElement.textContent).toContain('There are no consolidated accounts for this account.'); + }); +}); 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 23a7848ac3..71479ae184 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 @@ -21,6 +21,8 @@ import { FINES_ACC_DEFENDANT_ROUTING_PATHS } from '../routing/constants/fines-ac import { OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_PARENT_OR_GUARDIAN_TAB_REF_DATA_MOCK } from '@services/fines/opal-fines-service/mocks/opal-fines-account-defendant-details-parent-or-guardian-tab-ref-data.mock'; import { OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_FIXED_PENALTY_MOCK } from '@services/fines/opal-fines-service/mocks/opal-fines-account-defendant-details-fixed-penalty.mock'; import { OPAL_FINES_RESULT_REF_DATA_MOCK } from '@services/fines/opal-fines-service/mocks/opal-fines-result-ref-data.mock'; +import { OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK } from '@services/fines/opal-fines-service/mocks/opal-fines-account-defendant-details-consolidated-accounts.mock'; +import { FINES_ACC_DEFENDANT_ACCOUNT_TABS_CACHE_MAP } from './constants/fines-acc-defendant-account-tabs-cache-map.constant'; import { beforeEach, describe, expect, it, vi } from 'vitest'; describe('FinesAccDefendantDetailsComponent', () => { @@ -77,6 +79,7 @@ describe('FinesAccDefendantDetailsComponent', () => { clearCache: vi.fn().mockName('OpalFines.clearCache'), getResult: vi.fn().mockName('OpalFines.getResult'), getDefendantAccountFixedPenalty: vi.fn().mockName('OpalFines.getDefendantAccountFixedPenalty'), + getDefendantAccountConsolidatedAccounts: vi.fn().mockName('OpalFines.getDefendantAccountConsolidatedAccounts'), }; mockOpalFinesService.getDefendantAccountHeadingData.mockReturnValue(of(FINES_ACC_DEFENDANT_DETAILS_HEADER_MOCK)); mockOpalFinesService.getDefendantAccountAtAGlance.mockReturnValue( @@ -101,6 +104,9 @@ describe('FinesAccDefendantDetailsComponent', () => { mockOpalFinesService.getDefendantAccountImpositionsTabData.mockReturnValue( of(OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_IMPOSITIONS_TAB_REF_DATA_MOCK), ); + mockOpalFinesService.getDefendantAccountConsolidatedAccounts.mockReturnValue( + of(OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK), + ); mockOpalFinesService.getResult.mockReturnValue(of(OPAL_FINES_RESULT_REF_DATA_MOCK)); await TestBed.configureTestingModule({ @@ -299,6 +305,22 @@ describe('FinesAccDefendantDetailsComponent', () => { ); }); + it('should fetch the consolidated accounts tab data when fragment is changed to consolidated-accounts', () => { + component['refreshFragment$'].next('consolidated-accounts'); + component.tabConsolidatedAccounts$.subscribe(); + + expect(mockOpalFinesService.getDefendantAccountConsolidatedAccounts).toHaveBeenCalledWith( + MOCK_FINES_ACCOUNT_STATE.account_id, + ); + expect(mockPayloadService.transformPayload).toHaveBeenCalled(); + }); + + it('should map the consolidated accounts tab to its service cache key', () => { + expect(FINES_ACC_DEFENDANT_ACCOUNT_TABS_CACHE_MAP['consolidated-accounts']).toBe( + 'defendantAccountConsolidatedAccountsCache$', + ); + }); + it('should refresh the data for the header and current tab when refreshPage is called', () => { component.accountStore.setAccountState(MOCK_FINES_ACCOUNT_STATE); component.refreshPage(); 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 201168ea18..95fc549092 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 @@ -68,6 +68,7 @@ import { FINES_ACC_MAJOR_CREDITOR_DETAILS_HEADER_MOCK } from '../../fines-acc/fi import { OPAL_FINES_ACCOUNT_MAJOR_CREDITOR_AT_A_GLANCE_MOCK } from './mocks/opal-fines-account-major-creditor-at-a-glance-with-defendant.mock'; import { OPAL_FINES_ACCOUNT_MINOR_CREDITOR_DETAILS_HISTORY_AND_NOTES_TAB_REF_DATA_MOCK } from './mocks/opal-fines-account-minor-creditor-details-history-and-notes-tab-ref-data.mock'; import { OPAL_FINES_MINOR_CREDITOR_ACCOUNT_HISTORY_PARAMS_MOCK } from './mocks/opal-fines-minor-creditor-account-history-params.mock'; +import { OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK } from './mocks/opal-fines-account-defendant-details-consolidated-accounts.mock'; describe('OpalFines', () => { let service: OpalFines; @@ -1279,6 +1280,46 @@ describe('OpalFines', () => { req.flush(OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_HISTORY_AND_NOTES_TAB_REF_DATA_MOCK); }); + it('should getDefendantAccountConsolidatedAccounts', () => { + const account_id = 77; + const apiUrl = `${OPAL_FINES_PATHS.defendantAccounts}/${account_id}/consolidated-accounts`; + const expectedResponse = { + ...OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK, + version: '"123"', + }; + + service.getDefendantAccountConsolidatedAccounts(account_id).subscribe((response) => { + expect(response).toEqual(expectedResponse); + }); + + const req = httpMock.expectOne(apiUrl); + expect(req.request.method).toBe('GET'); + + req.flush(OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK.consolidated_accounts, { + headers: { ETag: '"123"' }, + }); + }); + + it('should return cached defendant account consolidated accounts on repeated calls', () => { + const account_id = 77; + const apiUrl = `${OPAL_FINES_PATHS.defendantAccounts}/${account_id}/consolidated-accounts`; + const expectedResponse = { + ...OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK, + version: null, + }; + + service.getDefendantAccountConsolidatedAccounts(account_id).subscribe(); + + const req = httpMock.expectOne(apiUrl); + req.flush(OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK.consolidated_accounts); + + service.getDefendantAccountConsolidatedAccounts(account_id).subscribe((response) => { + expect(response).toEqual(expectedResponse); + }); + + httpMock.expectNone(apiUrl); + }); + it('should send a POST request to add note API with correct payload and return mock response', () => { const payload: IOpalFinesAddNotePayload = OPAL_FINES_ADD_NOTE_PAYLOAD_MOCK; const version = '1'; @@ -1412,6 +1453,9 @@ describe('OpalFines', () => { service['cache']['defendantAccountFixedPenaltyCache$'] = of( OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_FIXED_PENALTY_MOCK, ); + service['cache']['defendantAccountConsolidatedAccountsCache$'] = of( + OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK, + ); service['cache']['minorCreditorAccountAtAGlanceCache$'] = of( OPAL_FINES_ACCOUNT_MINOR_CREDITOR_AT_A_GLANCE_WITH_DEFENDANT_MOCK, ); @@ -1426,6 +1470,7 @@ describe('OpalFines', () => { expect(service['cache']['defendantAccountHistoryAndNotesCache$']).toBeNull(); expect(service['cache']['defendantAccountPaymentTermsLatestCache$']).toBeNull(); expect(service['cache']['defendantAccountFixedPenaltyCache$']).toBeNull(); + expect(service['cache']['defendantAccountConsolidatedAccountsCache$']).toBeNull(); expect(service['cache']['minorCreditorAccountAtAGlanceCache$']).toBeNull(); }); From 6031750dc86eac08e910d80e8524e60b4ab4aac8 Mon Sep 17 00:00:00 2001 From: louisbriggs Date: Wed, 22 Jul 2026 10:21:30 +0100 Subject: [PATCH 05/12] Added method docs for getDefendantAccountConsolidatedAccounts service method. --- .../services/opal-fines-service/opal-fines.service.ts | 7 +++++++ 1 file changed, 7 insertions(+) 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 346d06f2a8..526effc483 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 @@ -1419,6 +1419,13 @@ export class OpalFines { return this.cache.majorCreditorAccountAtAGlanceCache$; } + /** + * Retrieves the defendant account consolidated accounts data. + * 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 defendant account. + * @returns An Observable that emits the account details for the consolidated accounts tab. + */ public getDefendantAccountConsolidatedAccounts( account_id: number | null, ): Observable { From f91183b9757d0b7bcc4f502650d786fbefa9b7b9 Mon Sep 17 00:00:00 2001 From: louisbriggs Date: Wed, 22 Jul 2026 11:35:23 +0100 Subject: [PATCH 06/12] Moving name string logic to template, from component --- ...etails-consolidated-accounts-tab.component.html | 2 +- ...ils-consolidated-accounts-tab.component.spec.ts | 1 - ...-details-consolidated-accounts-tab.component.ts | 14 ++------------ 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.html b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.html index 710b0e01ad..39bdd74ddd 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.html +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.html @@ -28,7 +28,7 @@

Consolidated accounts

- {{ getDefendantName(account) }} + {{ account.last_name | uppercase }}, {{ account.first_name }} { expect(fixture.nativeElement.querySelector('table.govuk-table')).toBeTruthy(); expect(textContent).toContain('99009902C'); - expect(textContent).toContain('CHILD, Casey'); expect(textContent).toContain('21 Jan 2026'); expect(textContent).toContain('Seed Child Court'); expect(textContent).toContain('LOCAL-CONSOL-CHILD'); diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.ts b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.ts index 5b6e9e1925..75db9e5095 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-consolidated-accounts-tab/fines-acc-defendant-details-consolidated-accounts-tab.component.ts @@ -1,9 +1,9 @@ +import { UpperCasePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { RouterLink } from '@angular/router'; import { CustomHorizontalScrollPaneComponent } from '@hmcts/opal-frontend-common/components/custom/custom-horizontal-scroll-pane'; import { DateFormatPipe } from '@hmcts/opal-frontend-common/pipes/date-format'; import { FINES_ROUTING_PATHS } from '@routing/fines/constants/fines-routing-paths.constant'; -import { IOpalFinesAccountDefendantDetailsConsolidatedAccount } from '@services/fines/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-account.interface'; import { IOpalFinesAccountDefendantDetailsConsolidatedAccounts } from '@services/fines/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-accounts.interface'; import { FINES_ACC_SUMMARY_TABS_CONTENT_STYLES } from '../../constants/fines-acc-summary-tabs-content-styles.constant'; import { FINES_ACC_DEFENDANT_ROUTING_PATHS } from '../../routing/constants/fines-acc-defendant-routing-paths.constant'; @@ -12,7 +12,7 @@ import { IFinesAccSummaryTabsContentStyles } from '../interfaces/fines-acc-summa @Component({ selector: 'app-fines-acc-defendant-details-consolidated-accounts-tab', - imports: [CustomHorizontalScrollPaneComponent, DateFormatPipe, RouterLink], + imports: [CustomHorizontalScrollPaneComponent, DateFormatPipe, RouterLink, UpperCasePipe], templateUrl: './fines-acc-defendant-details-consolidated-accounts-tab.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -39,14 +39,4 @@ export class FinesAccDefendantDetailsConsolidatedAccountsTabComponent { FINES_ACC_DEFENDANT_ROUTING_PATHS.children.details, ]; } - - /** - * Formats the consolidated account defendant name for display. - * - * @param account - The consolidated account row. - * @returns Defendant name in SURNAME, Forename format. - */ - public getDefendantName(account: IOpalFinesAccountDefendantDetailsConsolidatedAccount): string { - return `${account.last_name.toUpperCase()}, ${account.first_name}`; - } } From 5bc333b392cf95188fb21e55b2a8a472e72bcbd9 Mon Sep 17 00:00:00 2001 From: Ant-Collins Date: Mon, 27 Jul 2026 09:38:06 +0100 Subject: [PATCH 07/12] Component and E2E tests added --- .../AccountEnquiryConsolidatedAccounts.cy.ts | 127 +++++++++++++ .../setup/setupComponent.interface.ts | 1 + .../details.consolidated-accounts.actions.ts | 168 ++++++++++++++++++ .../AccountEnquiriesViewDetails.feature | 6 + .../opal/flows/account-enquiry.flow.ts | 36 ++++ .../searchForAccount/account-enquiry.steps.ts | 24 +++ yarn-audit-known-issues | 11 +- 7 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 cypress/component/fineAccountEnquiry/accountEnquiry/AccountEnquiryConsolidatedAccounts.cy.ts create mode 100644 cypress/e2e/functional/opal/actions/account-details/details.consolidated-accounts.actions.ts diff --git a/cypress/component/fineAccountEnquiry/accountEnquiry/AccountEnquiryConsolidatedAccounts.cy.ts b/cypress/component/fineAccountEnquiry/accountEnquiry/AccountEnquiryConsolidatedAccounts.cy.ts new file mode 100644 index 0000000000..31c073614d --- /dev/null +++ b/cypress/component/fineAccountEnquiry/accountEnquiry/AccountEnquiryConsolidatedAccounts.cy.ts @@ -0,0 +1,127 @@ +import { OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK } from '@services/fines/opal-fines-service/mocks/opal-fines-account-defendant-details-consolidated-accounts.mock'; +import { interceptAuthenticatedUser, interceptUserState } from 'cypress/component/CommonIntercepts/CommonIntercepts'; +import { USER_STATE_MOCK_PERMISSION_BU77 } from '../../CommonIntercepts/CommonUserState.mocks'; +import { DEFENDANT_HEADER_MOCK } from './mocks/defendant_details_mock'; +import { interceptDefendantHeader } from './intercept/defendantAccountIntercepts'; +import { IComponentProperties } from './setup/setupComponent.interface'; +import { setupAccountEnquiryComponent } from './setup/SetupComponent'; + +const ACCOUNT_ENQUIRY_JIRA_LABEL = '@JIRA-LABEL:account-enquiry'; + +const buildTags = (...tags: string[]): string[] => [...tags, ACCOUNT_ENQUIRY_JIRA_LABEL, '@R1B']; + +const componentProperties: IComponentProperties = { + accountId: '77', + fragments: 'consolidated-accounts', + interceptedRoutes: ['/access-denied'], +}; + +const tab = 'app-fines-acc-defendant-details-consolidated-accounts-tab'; +const rows = `${tab} tbody tr.govuk-table__row`; +type ConsolidatedAccountsMock = typeof OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK; + +const cell = (columnKey: string, rowIndex: number): string => `#consolidated-account-${columnKey}-${rowIndex}`; +const normaliseText = (value: string): string => + value + .replace(/\u00A0/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + +const expectCellText = (selector: string, expected: string) => { + cy.get(selector) + .invoke('text') + .then((text) => { + expect(normaliseText(text)).to.eq(expected); + }); +}; + +const consolidatedAccountsMock: ConsolidatedAccountsMock = { + ...structuredClone(OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK), + consolidated_accounts: [ + { + account_id: 99000000990002, + account_number: '99009902C', + date_imposed: '2025-01-12', + first_name: 'Casey', + imposed_by: 'Seed Child Court', + last_name: 'Child', + reference: 'LOCAL-CONSOL-CHILD', + }, + { + account_id: 99000000990003, + account_number: '99009903D', + date_imposed: '2026-02-03', + first_name: 'Robin', + imposed_by: 'Central London Magistrates Court', + last_name: 'Bridge', + reference: 'Ref/Mixed-Case/123', + }, + ], +}; + +const setupConsolidatedAccountsScreen = (mockData: ConsolidatedAccountsMock = consolidatedAccountsMock) => { + const headerMock = { + ...structuredClone(DEFENDANT_HEADER_MOCK), + has_consolidated_accounts: true, + }; + const accountId = headerMock.defendant_account_party_id; + + interceptAuthenticatedUser(); + interceptUserState(USER_STATE_MOCK_PERMISSION_BU77); + interceptDefendantHeader(accountId, headerMock, '123'); + cy.intercept('GET', `/opal-fines-service/defendant-accounts/${accountId}/consolidated-accounts`, { + statusCode: 200, + headers: { ETag: '123' }, + body: mockData.consolidated_accounts, + }).as('getConsolidatedAccounts'); + + setupAccountEnquiryComponent({ ...componentProperties, accountId }); + cy.wait('@getConsolidatedAccounts').its('response.body').should('have.length', 2); +}; + +describe('Account Enquiry Consolidated Accounts Tab', () => { + it( + 'AC1a, AC1b, AC1c, AC1d, AC1e, AC1f: fetches consolidated child accounts and renders the read-only table', + { + tags: [...buildTags('@JIRA-STORY:PO-2391'), '@JIRA-EPIC:PO-2332'], + }, + () => { + setupConsolidatedAccountsScreen(); + + cy.get('[subnavitemid="consolidated-accounts-tab"] > .moj-sub-navigation__link') + .should('have.attr', 'aria-current', 'page') + .and('contain.text', 'Consolidated accounts'); + cy.get(`${tab} h2`).should('contain.text', 'Consolidated accounts'); + cy.get(`${tab} th.govuk-table__header`).then((headers) => { + expect([...headers].map((header) => normaliseText(header.textContent ?? ''))).to.deep.eq([ + 'Account', + 'Name', + 'Date imposed', + 'Imposed by', + 'Reference', + ]); + }); + cy.get(rows).should('have.length', 2); + cy.get(tab).within(() => { + cy.get('input, textarea, select, button, [contenteditable="true"]').should('not.exist'); + }); + + expectCellText(cell('name', 0), 'CHILD, Casey'); + expectCellText(cell('date-imposed', 0), '12 Jan 2025'); + expectCellText(cell('imposed-by', 0), 'Seed Child Court'); + expectCellText(cell('reference', 0), 'LOCAL-CONSOL-CHILD'); + expectCellText(cell('name', 1), 'BRIDGE, Robin'); + expectCellText(cell('date-imposed', 1), '03 Feb 2026'); + expectCellText(cell('imposed-by', 1), 'Central London Magistrates Court'); + expectCellText(cell('reference', 1), 'Ref/Mixed-Case/123'); + + cy.get(cell('number', 0)) + .find('a') + .should(($link) => { + expect(normaliseText($link.text())).to.eq('99009902C'); + }) + .and('have.attr', 'href', '/fines/account/defendant/99000000990002/details#at-a-glance') + .and('have.attr', 'target', '_blank'); + }, + ); +}); diff --git a/cypress/component/fineAccountEnquiry/accountEnquiry/setup/setupComponent.interface.ts b/cypress/component/fineAccountEnquiry/accountEnquiry/setup/setupComponent.interface.ts index b046ed2107..49a156a961 100644 --- a/cypress/component/fineAccountEnquiry/accountEnquiry/setup/setupComponent.interface.ts +++ b/cypress/component/fineAccountEnquiry/accountEnquiry/setup/setupComponent.interface.ts @@ -22,6 +22,7 @@ export interface IComponentProperties { | 'enforcement' | 'impositions' | 'history-and-notes' + | 'consolidated-accounts' | 'fixed-penalty' | 'payment-terms' | undefined; diff --git a/cypress/e2e/functional/opal/actions/account-details/details.consolidated-accounts.actions.ts b/cypress/e2e/functional/opal/actions/account-details/details.consolidated-accounts.actions.ts new file mode 100644 index 0000000000..a20e03a68e --- /dev/null +++ b/cypress/e2e/functional/opal/actions/account-details/details.consolidated-accounts.actions.ts @@ -0,0 +1,168 @@ +import { createScopedLogger } from '../../../../../support/utils/log.helper'; + +const log = createScopedLogger('AccountDetailsConsolidatedAccountsActions'); + +const accountCaptionSelector = 'opal-lib-govuk-heading-with-caption .govuk-caption-l'; + +const CHILD_ACCOUNT = { + account_id: 99000000990002, + account_number: '99009902C', + date_imposed: '2025-01-12', + first_name: 'Casey', + imposed_by: 'Seed Child Court', + last_name: 'Child', + reference: 'LOCAL-CONSOL-CHILD', +}; + +/** + * Actions for the Account Details Consolidated accounts tab. + */ +export class AccountDetailsConsolidatedAccountsActions { + private static readonly WAIT_MS = 15_000; + private static readonly tab = 'app-fines-acc-defendant-details-consolidated-accounts-tab'; + + /** + * Presents the supplied account as a master account with one consolidated child account. + * + * @param accountId - Current defendant account ID. + * @param header - Current account header payload. + */ + public stubMasterAccountWithChild(accountId: number, header: Record): void { + log('intercept', 'Presenting account as a master account with one consolidated child account', { accountId }); + + cy.intercept('GET', `**/defendant-accounts/${accountId}/header-summary`, { + statusCode: 200, + headers: { ETag: 'e2e-consolidated-header' }, + body: { ...header, has_consolidated_accounts: true }, + }).as('consolidatedHeaderSummary'); + + cy.intercept('GET', `**/defendant-accounts/${accountId}/consolidated-accounts`, { + statusCode: 200, + headers: { ETag: 'e2e-consolidated-accounts' }, + body: [CHILD_ACCOUNT], + }).as('consolidatedAccounts'); + + cy.intercept('GET', `**/defendant-accounts/${CHILD_ACCOUNT.account_id}/header-summary`, { + statusCode: 200, + headers: { ETag: 'e2e-consolidated-child-header' }, + body: { + ...header, + account_number: CHILD_ACCOUNT.account_number, + defendant_account_id: String(CHILD_ACCOUNT.account_id), + defendant_account_party_id: String(CHILD_ACCOUNT.account_id), + has_consolidated_accounts: false, + party_details: { + party_id: String(CHILD_ACCOUNT.account_id), + organisation_flag: false, + organisation_details: null, + individual_details: { + title: null, + forenames: CHILD_ACCOUNT.first_name, + surname: CHILD_ACCOUNT.last_name, + date_of_birth: null, + age: null, + national_insurance_number: null, + individual_aliases: null, + }, + }, + }, + }).as('consolidatedChildHeaderSummary'); + + cy.intercept('GET', `**/defendant-accounts/${CHILD_ACCOUNT.account_id}/at-a-glance`, { + statusCode: 200, + headers: { ETag: 'e2e-consolidated-child-at-a-glance' }, + body: { + version: null, + defendant_account_id: String(CHILD_ACCOUNT.account_id), + account_number: CHILD_ACCOUNT.account_number, + debtor_type: 'PERSON', + is_youth: false, + party_details: { + party_id: String(CHILD_ACCOUNT.account_id), + organisation_flag: false, + organisation_details: null, + individual_details: { + title: null, + forenames: CHILD_ACCOUNT.first_name, + surname: CHILD_ACCOUNT.last_name, + date_of_birth: null, + national_insurance_number: null, + individual_aliases: null, + }, + }, + address: { + address_line_1: '1 Child Street', + address_line_2: null, + address_line_3: null, + address_line_4: null, + address_line_5: null, + postcode: 'CH1 1LD', + }, + language_preferences: null, + payment_terms: null, + enforcement_status: { + last_enforcement_action: null, + collection_order_made: false, + default_days_in_jail: 0, + enforcement_override: null, + last_movement_date: null, + }, + comments_and_notes: null, + }, + }).as('consolidatedChildAtAGlance'); + } + + /** + * Opens the Consolidated accounts tab and waits for its table payload. + */ + public openTab(): void { + log('navigate', 'Opening Consolidated accounts tab'); + + cy.get('li[subnavitemid="consolidated-accounts-tab"] > a.moj-sub-navigation__link', { + timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS, + }) + .should('be.visible') + .click(); + + cy.get('a.moj-sub-navigation__link[aria-current="page"]') + .should('be.visible') + .and('contain.text', 'Consolidated accounts'); + cy.wait('@consolidatedAccounts', { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS }) + .its('response.statusCode') + .should('eq', 200); + cy.get(AccountDetailsConsolidatedAccountsActions.tab, { + timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS, + }).should('be.visible'); + } + + /** + * Clicks the first child account link and asserts it routes to the At a glance tab. + */ + public openFirstChildAtAGlance(): void { + log('open', 'Opening first consolidated child account at At a glance'); + + cy.get('#consolidated-account-number-0 a', { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS }) + .should('be.visible') + .and('contain.text', CHILD_ACCOUNT.account_number) + .and(($link) => { + expect($link.attr('href')).to.eq(`/fines/account/defendant/${CHILD_ACCOUNT.account_id}/details#at-a-glance`); + }) + .invoke('removeAttr', 'target') + .click(); + + cy.location('pathname', { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS }).should( + 'eq', + `/fines/account/defendant/${CHILD_ACCOUNT.account_id}/details`, + ); + cy.location('hash', { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS }).should('eq', '#at-a-glance'); + cy.wait('@consolidatedChildHeaderSummary', { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS }) + .its('response.statusCode') + .should('eq', 200); + cy.wait('@consolidatedChildAtAGlance', { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS }) + .its('response.statusCode') + .should('eq', 200); + cy.get(accountCaptionSelector, { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS }) + .should('be.visible') + .and('contain.text', CHILD_ACCOUNT.account_number); + } +} diff --git a/cypress/e2e/functional/opal/features/fineAccountEnquiry/accountEnquiry/AccountEnquiriesViewDetails.feature b/cypress/e2e/functional/opal/features/fineAccountEnquiry/accountEnquiry/AccountEnquiriesViewDetails.feature index 10e1be976d..127ccc6b11 100644 --- a/cypress/e2e/functional/opal/features/fineAccountEnquiry/accountEnquiry/AccountEnquiriesViewDetails.feature +++ b/cypress/e2e/functional/opal/features/fineAccountEnquiry/accountEnquiry/AccountEnquiriesViewDetails.feature @@ -213,6 +213,12 @@ Feature: Account Enquiries – View Account Details When I go to the History and notes tab And I open the first History and notes account link in a new tab + @R1B @JIRA-STORY:PO-2391 @JIRA-EPIC:PO-2332 + Scenario: Consolidated account links open the child account At a glance view + Given I am viewing a master account with a consolidated child account + When I go to the Consolidated accounts tab + And I open the first Consolidated accounts link at the At a glance view + Rule: Non-paying defendant account baseline Background: # AC1 – Account setup diff --git a/cypress/e2e/functional/opal/flows/account-enquiry.flow.ts b/cypress/e2e/functional/opal/flows/account-enquiry.flow.ts index 05551ca609..49f62bc418 100644 --- a/cypress/e2e/functional/opal/flows/account-enquiry.flow.ts +++ b/cypress/e2e/functional/opal/flows/account-enquiry.flow.ts @@ -13,6 +13,7 @@ import { AccountDetailsMinorCreditorActions } from '../actions/account-details/d import { AccountDetailsPaymentTermsActions } from '../actions/account-details/details.payment-terms.actions'; import { AccountDetailsFixedPenaltyActions } from '../actions/account-details/details.fixed-penalty.actions'; import { AccountDetailsHistoryActions } from '../actions/account-details/details.history.actions'; +import { AccountDetailsConsolidatedAccountsActions } from '../actions/account-details/details.consolidated-accounts.actions'; import { AccountSearchIndividualsLocators as L } from '../../../../shared/selectors/account-search/account.search.individuals.locators'; import { AccountSearchCompaniesLocators as C } from '../../../../shared/selectors/account-search/account.search.companies.locators'; import { ForceSingleTabNavigation } from '../../../../support/utils/navigation'; @@ -95,6 +96,7 @@ export class AccountEnquiryFlow { private readonly enforcement = new AccountDetailsEnforcementActions(); private readonly removeParentGuardian = new RemoveParentGuardianActions(); private readonly historyAndNotes = new AccountDetailsHistoryActions(); + private readonly consolidatedAccounts = new AccountDetailsConsolidatedAccountsActions(); /** * Ensures the test is on the Individuals Account Search page. @@ -673,6 +675,40 @@ export class AccountEnquiryFlow { this.historyAndNotes.assertHistoryAndNotesTabLoaded(); } + /** + * Presents the current account as a master account with one consolidated child account. + */ + public prepareMasterAccountWithConsolidatedChildAccount(): void { + logAE('method', 'prepareMasterAccountWithConsolidatedChildAccount()'); + + this.extractDefendantAccountIdFromUrl().then((accountId) => { + this.fetchHeaderSummary(accountId).then((header) => { + this.consolidatedAccounts.stubMasterAccountWithChild(accountId, header); + cy.reload(); + cy.wait('@consolidatedHeaderSummary', { timeout: AccountEnquiryFlow.WAIT_MS }) + .its('response.statusCode') + .should('eq', 200); + }); + }); + } + + /** + * Navigates to the Consolidated accounts tab and asserts it has loaded. + */ + public goToConsolidatedAccountsTab(): void { + logAE('method', 'goToConsolidatedAccountsTab()'); + this.consolidatedAccounts.openTab(); + } + + /** + * Opens the first Consolidated accounts child account link and asserts the At a glance route. + */ + public openFirstConsolidatedAccountLinkAtAGlance(): void { + logAE('method', 'openFirstConsolidatedAccountLinkAtAGlance()'); + this.consolidatedAccounts.openFirstChildAtAGlance(); + this.detailsNav.assertAtAGlanceTabIsActive(); + } + /** * Asserts the initial History and notes rows have been rendered. */ diff --git a/cypress/support/step_definitions/searchForAccount/account-enquiry.steps.ts b/cypress/support/step_definitions/searchForAccount/account-enquiry.steps.ts index cd9ac4a6d0..aec63c9750 100644 --- a/cypress/support/step_definitions/searchForAccount/account-enquiry.steps.ts +++ b/cypress/support/step_definitions/searchForAccount/account-enquiry.steps.ts @@ -472,6 +472,14 @@ Given('the History and notes API is stubbed with standard tab data', () => { flow().stubHistoryAndNotesTabData(); }); +/** + * @step Presents the current account as a master account with a consolidated child account. + */ +Given('I am viewing a master account with a consolidated child account', () => { + log('step', 'Prepare master account with consolidated child account'); + flow().prepareMasterAccountWithConsolidatedChildAccount(); +}); + /** * @step Navigates to the History and notes tab. */ @@ -480,6 +488,14 @@ When('I go to the History and notes tab', () => { flow().goToHistoryAndNotesTab(); }); +/** + * @step Navigates to the Consolidated accounts tab. + */ +When('I go to the Consolidated accounts tab', () => { + log('step', 'Navigate to Consolidated accounts tab'); + flow().goToConsolidatedAccountsTab(); +}); + /** * @step Verifies History and notes rows loaded. */ @@ -512,6 +528,14 @@ When('I open the first History and notes account link in a new tab', () => { flow().openHistoryAndNotesAccountLinkInNewTab(); }); +/** + * @step Opens the first consolidated account link and verifies the At a glance route. + */ +When('I open the first Consolidated accounts link at the At a glance view', () => { + log('step', 'Open first Consolidated accounts link at the At a glance view'); + flow().openFirstConsolidatedAccountLinkAtAGlance(); +}); + /** * @step Opens the amend payment terms form. */ diff --git a/yarn-audit-known-issues b/yarn-audit-known-issues index 4745b57d59..9617c54385 100644 --- a/yarn-audit-known-issues +++ b/yarn-audit-known-issues @@ -1,6 +1,11 @@ {"value":"@angular/platform-browser-dynamic","children":{"ID":"@angular/platform-browser-dynamic (deprecation)","Issue":"@angular/platform-browser-dynamic is deprecated. Use `@angular/platform-browser` instead.","Severity":"moderate","Vulnerable Versions":"21.2.18","Tree Versions":["21.2.18"],"Dependents":["opal-frontend@workspace:."]}} {"value":"@opentelemetry/core","children":{"ID":1120821,"Issue":"OpenTelemetry Core: Unbounded memory allocation in W3C Baggage propagation","URL":"https://github.com/advisories/GHSA-8988-4f7v-96qf","Severity":"moderate","Vulnerable Versions":"<2.8.0","Tree Versions":["1.30.1","2.6.1","2.7.1"],"Dependents":["@azure/opentelemetry-instrumentation-azure-sdk@npm:1.0.0-beta.10","applicationinsights@npm:3.15.0","applicationinsights@virtual:f4e4ae2f7d22e410c6c8bf2f3e0785bc044272b48ed1342da906abf9548df6c963911b500cbe851ba0824f7864ab2f6c2aff44ba4a140321bd86324a71eb897c#npm:2.9.8"]}} +{"value":"@opentelemetry/propagator-jaeger","children":{"ID":1124011,"Issue":"OpenTelemetry JavaScript: Denial of service in `JaegerPropagator` via unhandled exception on a malformed header","URL":"https://github.com/advisories/GHSA-45rx-2jwx-cxfr","Severity":"high","Vulnerable Versions":"<2.9.0","Tree Versions":["2.7.1"],"Dependents":["@opentelemetry/sdk-node@virtual:7d28986b6beca5611db0460c59742827c485e9e4ad2f65e45fb8287e62953517d20a6f66193b45448e6d2e95136688c6edcf391e563ea2038165c9fdaf765dae#npm:0.217.0"]}} {"value":"ajv","children":{"ID":1113715,"Issue":"ajv has ReDoS when using `$data` option","URL":"https://github.com/advisories/GHSA-2g4f-4pwh-qvx6","Severity":"moderate","Vulnerable Versions":">=7.0.0-alpha.0 <8.18.0","Tree Versions":["8.17.1"],"Dependents":["schema-utils@npm:4.3.3"]}} +{"value":"body-parser","children":{"ID":1123976,"Issue":"body-parser vulnerable to denial of service when invalid limit value silently disables size enforcement","URL":"https://github.com/advisories/GHSA-v422-hmwv-36x6","Severity":"low","Vulnerable Versions":">=2.0.0 <2.3.0","Tree Versions":["2.2.2"],"Dependents":["opal-frontend@workspace:."]}} +{"value":"brace-expansion","children":{"ID":1124334,"Issue":"brace-expansion: DoS via unbounded expansion length causing an out-of-memory process crash","URL":"https://github.com/advisories/GHSA-mh99-v99m-4gvg","Severity":"high","Vulnerable Versions":"<=5.0.7","Tree Versions":["5.0.7"],"Dependents":["minimatch@npm:3.1.2"]}} +{"value":"fast-uri","children":{"ID":1124064,"Issue":"fast-uri vulnerable to host confusion via literal backslash authority delimiter","URL":"https://github.com/advisories/GHSA-v2hh-gcrm-f6hx","Severity":"high","Vulnerable Versions":">=3.0.0 <=3.1.3","Tree Versions":["3.1.2"],"Dependents":["ajv@npm:8.17.1"]}} +{"value":"fast-uri","children":{"ID":1124231,"Issue":"fast-uri vulnerable to host confusion via failed IDN canonicalization","URL":"https://github.com/advisories/GHSA-4c8g-83qw-93j6","Severity":"high","Vulnerable Versions":">=3.0.0 <3.1.3","Tree Versions":["3.1.2"],"Dependents":["ajv@npm:8.17.1"]}} {"value":"ip-address","children":{"ID":1118827,"Issue":"ip-address has XSS in Address6 HTML-emitting methods","URL":"https://github.com/advisories/GHSA-v2v4-37r5-5v8g","Severity":"moderate","Vulnerable Versions":"<=10.1.0","Tree Versions":["9.0.5"],"Dependents":["socks@npm:2.8.3"]}} {"value":"minimatch","children":{"ID":1113459,"Issue":"minimatch has a ReDoS via repeated wildcards with non-matching literal in pattern","URL":"https://github.com/advisories/GHSA-3ppc-4f35-3m26","Severity":"high","Vulnerable Versions":"<3.1.3","Tree Versions":["3.1.2"],"Dependents":["find-cypress-specs@npm:1.47.2"]}} {"value":"minimatch","children":{"ID":1113465,"Issue":"minimatch has a ReDoS via repeated wildcards with non-matching literal in pattern","URL":"https://github.com/advisories/GHSA-3ppc-4f35-3m26","Severity":"high","Vulnerable Versions":">=9.0.0 <9.0.6","Tree Versions":["9.0.5"],"Dependents":["mocha@npm:11.7.6"]}} @@ -11,6 +16,8 @@ {"value":"picomatch","children":{"ID":1115549,"Issue":"Picomatch: Method Injection in POSIX Character Classes causes incorrect Glob Matching","URL":"https://github.com/advisories/GHSA-3v7f-55p6-f55p","Severity":"moderate","Vulnerable Versions":"<2.3.2","Tree Versions":["2.3.1"],"Dependents":["micromatch@npm:4.0.8"]}} {"value":"picomatch","children":{"ID":1115552,"Issue":"Picomatch has a ReDoS vulnerability via extglob quantifiers","URL":"https://github.com/advisories/GHSA-c2c7-rcm5-vvqj","Severity":"high","Vulnerable Versions":"<2.3.2","Tree Versions":["2.3.1"],"Dependents":["micromatch@npm:4.0.8"]}} {"value":"postcss","children":{"ID":1117015,"Issue":"PostCSS has XSS via Unescaped in its CSS Stringify Output","URL":"https://github.com/advisories/GHSA-qx2v-qp2m-jg93","Severity":"moderate","Vulnerable Versions":"<8.5.10","Tree Versions":["8.5.1"],"Dependents":["precinct@npm:12.1.2"]}} +{"value":"postcss","children":{"ID":1124252,"Issue":"PostCSS: Arbitrary file read and information disclosure via attacker-controlled sourceMappingURL in CSS comments","URL":"https://github.com/advisories/GHSA-6g55-p6wh-862q","Severity":"high","Vulnerable Versions":"<=8.5.11","Tree Versions":["8.5.1"],"Dependents":["precinct@npm:12.1.2"]}} +{"value":"postcss","children":{"ID":1124288,"Issue":"PostCSS: Path Traversal in Previous Source Map Auto-Loading (sourceMappingURL) leads to Arbitrary .map File Disclosure","URL":"https://github.com/advisories/GHSA-r28c-9q8g-f849","Severity":"high","Vulnerable Versions":"<=8.5.17","Tree Versions":["8.5.1"],"Dependents":["precinct@npm:12.1.2"]}} {"value":"protobufjs","children":{"ID":1118640,"Issue":"protobuf.js: Code injection through bytes field defaults in generated toObject code","URL":"https://github.com/advisories/GHSA-66ff-xgx4-vchm","Severity":"high","Vulnerable Versions":">=8.0.0 <=8.0.1","Tree Versions":["8.0.1"],"Dependents":["@opentelemetry/otlp-transformer@virtual:322a2107a6aa310c2a06b3448cb9bb76d7bbf254e8660a79ccc09aa7c7484ea03bf1600695c5f1a72214540162ed6438abe5c04b57cef81244dfd24705486c1b#npm:0.217.0"]}} {"value":"protobufjs","children":{"ID":1118923,"Issue":"protobuf.js: Denial of service from crafted field names in generated code","URL":"https://github.com/advisories/GHSA-2pr8-phx7-x9h3","Severity":"moderate","Vulnerable Versions":">=8.0.0 <=8.0.1","Tree Versions":["8.0.1"],"Dependents":["@opentelemetry/otlp-transformer@virtual:322a2107a6aa310c2a06b3448cb9bb76d7bbf254e8660a79ccc09aa7c7484ea03bf1600695c5f1a72214540162ed6438abe5c04b57cef81244dfd24705486c1b#npm:0.217.0"]}} {"value":"protobufjs","children":{"ID":1118925,"Issue":"protobuf.js: Prototype injection in generated message constructors","URL":"https://github.com/advisories/GHSA-fx83-v9x8-x52w","Severity":"moderate","Vulnerable Versions":">=8.0.0 <=8.0.1","Tree Versions":["8.0.1"],"Dependents":["@opentelemetry/otlp-transformer@virtual:322a2107a6aa310c2a06b3448cb9bb76d7bbf254e8660a79ccc09aa7c7484ea03bf1600695c5f1a72214540162ed6438abe5c04b57cef81244dfd24705486c1b#npm:0.217.0"]}} @@ -23,10 +30,12 @@ {"value":"protobufjs","children":{"ID":1123488,"Issue":"protobufjs: Denial of service through unbounded Any expansion during JSON conversion","URL":"https://github.com/advisories/GHSA-wcpc-wj8m-hjx6","Severity":"high","Vulnerable Versions":"<=7.6.0","Tree Versions":["7.5.8"],"Dependents":["@grpc/proto-loader@npm:0.8.0"]}} {"value":"protobufjs","children":{"ID":1123489,"Issue":"protobufjs : Schema-derived names can shadow runtime-significant properties","URL":"https://github.com/advisories/GHSA-f38q-mgvj-vph7","Severity":"moderate","Vulnerable Versions":">=8.0.0 <=8.5.0","Tree Versions":["8.0.1"],"Dependents":["@opentelemetry/otlp-transformer@virtual:322a2107a6aa310c2a06b3448cb9bb76d7bbf254e8660a79ccc09aa7c7484ea03bf1600695c5f1a72214540162ed6438abe5c04b57cef81244dfd24705486c1b#npm:0.217.0"]}} {"value":"protobufjs","children":{"ID":1123492,"Issue":"protobufjs : Schema-derived names can shadow runtime-significant properties","URL":"https://github.com/advisories/GHSA-f38q-mgvj-vph7","Severity":"moderate","Vulnerable Versions":"<=7.6.2","Tree Versions":["7.5.8"],"Dependents":["@grpc/proto-loader@npm:0.8.0"]}} +{"value":"protobufjs","children":{"ID":1123963,"Issue":"protobufjs: Denial of Service via infinite loop in .proto option parsing","URL":"https://github.com/advisories/GHSA-j3f2-48v5-ccww","Severity":"moderate","Vulnerable Versions":">=8.0.0 <=8.6.5","Tree Versions":["8.0.1"],"Dependents":["@opentelemetry/otlp-transformer@virtual:322a2107a6aa310c2a06b3448cb9bb76d7bbf254e8660a79ccc09aa7c7484ea03bf1600695c5f1a72214540162ed6438abe5c04b57cef81244dfd24705486c1b#npm:0.217.0"]}} +{"value":"protobufjs","children":{"ID":1123964,"Issue":"protobufjs: Denial of Service via infinite loop in .proto option parsing","URL":"https://github.com/advisories/GHSA-j3f2-48v5-ccww","Severity":"moderate","Vulnerable Versions":">=7.5.0 <=7.6.4","Tree Versions":["7.5.8"],"Dependents":["@grpc/proto-loader@npm:0.8.0"]}} {"value":"qs","children":{"ID":1119502,"Issue":"qs has a remotely triggerable DoS: qs.stringify crashes with TypeError on null/undefined entries in comma-format arrays when encodeValuesOnly is set","URL":"https://github.com/advisories/GHSA-q8mj-m7cp-5q26","Severity":"moderate","Vulnerable Versions":">=6.11.1 <=6.15.1","Tree Versions":["6.14.2"],"Dependents":["body-parser@npm:2.2.2"]}} {"value":"serialize-javascript","children":{"ID":1113686,"Issue":"Serialize JavaScript is Vulnerable to RCE via RegExp.flags and Date.prototype.toISOString()","URL":"https://github.com/advisories/GHSA-5c6j-r48x-rmvq","Severity":"high","Vulnerable Versions":"<=7.0.2","Tree Versions":["6.0.2"],"Dependents":["mocha@npm:11.7.6"]}} {"value":"serialize-javascript","children":{"ID":1119440,"Issue":"Serialize JavaScript has CPU Exhaustion Denial of Service via crafted array-like objects","URL":"https://github.com/advisories/GHSA-qj8w-gfj5-8c6v","Severity":"moderate","Vulnerable Versions":">=5.0.0 <7.0.5","Tree Versions":["6.0.2"],"Dependents":["mocha@npm:11.7.6"]}} -{"value":"systeminformation","children":{"ID":1123570,"Issue":"systeminformation: OS command injection in networkInterfaces() via interfaces(5) source-directive path on Linux","URL":"https://github.com/advisories/GHSA-5xpp-75jx-m839","Severity":"high","Vulnerable Versions":"<=5.31.6","Tree Versions":["5.31.6"],"Dependents":["cypress@npm:15.18.1"]}} +{"value":"tar","children":{"ID":1124287,"Issue":"node-tar: Uncontrolled recursion in mapHas/filesFilter allows uncatchable stack-overflow DoS via crafted long-path tar with member selection","URL":"https://github.com/advisories/GHSA-r292-9mhp-454m","Severity":"moderate","Vulnerable Versions":"<=7.5.20","Tree Versions":["7.5.20"],"Dependents":["node-gyp@npm:10.2.0"]}} {"value":"undici","children":{"ID":1121186,"Issue":"undici vulnerable to TLS certificate validation bypass via dropped requestTls in SOCKS5 ProxyAgent","URL":"https://github.com/advisories/GHSA-vmh5-mc38-953g","Severity":"high","Vulnerable Versions":">=8.0.0 <8.5.0","Tree Versions":["8.3.0"],"Dependents":["@actions/http-client@npm:2.2.3"]}} {"value":"undici","children":{"ID":1121190,"Issue":"undici WebSocket client vulnerable to denial of service via cumulative fragment bypass","URL":"https://github.com/advisories/GHSA-38rv-x7px-6hhq","Severity":"high","Vulnerable Versions":">=8.0.0 <8.5.0","Tree Versions":["8.3.0"],"Dependents":["@actions/http-client@npm:2.2.3"]}} {"value":"undici","children":{"ID":1121240,"Issue":"undici vulnerable to HTTP header injection via Set-Cookie percent-decoding","URL":"https://github.com/advisories/GHSA-p88m-4jfj-68fv","Severity":"moderate","Vulnerable Versions":">=8.0.0 <8.5.0","Tree Versions":["8.3.0"],"Dependents":["@actions/http-client@npm:2.2.3"]}} From f02dd4397a1bda2a7c39af66a96cd9ec10423f4a Mon Sep 17 00:00:00 2001 From: Ant-Collins Date: Thu, 30 Jul 2026 09:48:04 +0100 Subject: [PATCH 08/12] QA review updates --- .../AccountEnquiryConsolidatedAccounts.cy.ts | 23 ++++------ .../intercept/defendantAccountIntercepts.ts | 27 ++++++++---- .../details.consolidated-accounts.actions.ts | 44 +++++++++++++++---- .../AccountEnquiriesViewDetails.feature | 8 +++- .../opal/flows/account-enquiry.flow.ts | 16 +++++++ .../account.consolidated-accounts.locators.ts | 10 +++++ .../searchForAccount/account-enquiry.steps.ts | 38 +++++++++++----- 7 files changed, 122 insertions(+), 44 deletions(-) create mode 100644 cypress/shared/selectors/account-details/account.consolidated-accounts.locators.ts diff --git a/cypress/component/fineAccountEnquiry/accountEnquiry/AccountEnquiryConsolidatedAccounts.cy.ts b/cypress/component/fineAccountEnquiry/accountEnquiry/AccountEnquiryConsolidatedAccounts.cy.ts index 31c073614d..9bf2c1d9e1 100644 --- a/cypress/component/fineAccountEnquiry/accountEnquiry/AccountEnquiryConsolidatedAccounts.cy.ts +++ b/cypress/component/fineAccountEnquiry/accountEnquiry/AccountEnquiryConsolidatedAccounts.cy.ts @@ -1,8 +1,9 @@ import { OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK } from '@services/fines/opal-fines-service/mocks/opal-fines-account-defendant-details-consolidated-accounts.mock'; import { interceptAuthenticatedUser, interceptUserState } from 'cypress/component/CommonIntercepts/CommonIntercepts'; import { USER_STATE_MOCK_PERMISSION_BU77 } from '../../CommonIntercepts/CommonUserState.mocks'; +import { ConsolidatedAccountsLocators } from '../../../shared/selectors/account-details/account.consolidated-accounts.locators'; import { DEFENDANT_HEADER_MOCK } from './mocks/defendant_details_mock'; -import { interceptDefendantHeader } from './intercept/defendantAccountIntercepts'; +import { interceptConsolidatedAccounts, interceptDefendantHeader } from './intercept/defendantAccountIntercepts'; import { IComponentProperties } from './setup/setupComponent.interface'; import { setupAccountEnquiryComponent } from './setup/SetupComponent'; @@ -16,11 +17,9 @@ const componentProperties: IComponentProperties = { interceptedRoutes: ['/access-denied'], }; -const tab = 'app-fines-acc-defendant-details-consolidated-accounts-tab'; -const rows = `${tab} tbody tr.govuk-table__row`; type ConsolidatedAccountsMock = typeof OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_CONSOLIDATED_ACCOUNTS_MOCK; -const cell = (columnKey: string, rowIndex: number): string => `#consolidated-account-${columnKey}-${rowIndex}`; +const cell = (columnKey: string, rowIndex: number): string => ConsolidatedAccountsLocators.cell(columnKey, rowIndex); const normaliseText = (value: string): string => value .replace(/\u00A0/g, ' ') @@ -69,11 +68,7 @@ const setupConsolidatedAccountsScreen = (mockData: ConsolidatedAccountsMock = co interceptAuthenticatedUser(); interceptUserState(USER_STATE_MOCK_PERMISSION_BU77); interceptDefendantHeader(accountId, headerMock, '123'); - cy.intercept('GET', `/opal-fines-service/defendant-accounts/${accountId}/consolidated-accounts`, { - statusCode: 200, - headers: { ETag: '123' }, - body: mockData.consolidated_accounts, - }).as('getConsolidatedAccounts'); + interceptConsolidatedAccounts(accountId, mockData.consolidated_accounts, '123'); setupAccountEnquiryComponent({ ...componentProperties, accountId }); cy.wait('@getConsolidatedAccounts').its('response.body').should('have.length', 2); @@ -88,11 +83,11 @@ describe('Account Enquiry Consolidated Accounts Tab', () => { () => { setupConsolidatedAccountsScreen(); - cy.get('[subnavitemid="consolidated-accounts-tab"] > .moj-sub-navigation__link') + cy.get(ConsolidatedAccountsLocators.tabLink) .should('have.attr', 'aria-current', 'page') .and('contain.text', 'Consolidated accounts'); - cy.get(`${tab} h2`).should('contain.text', 'Consolidated accounts'); - cy.get(`${tab} th.govuk-table__header`).then((headers) => { + cy.get(ConsolidatedAccountsLocators.heading).should('contain.text', 'Consolidated accounts'); + cy.get(`${ConsolidatedAccountsLocators.tabRoot} th.govuk-table__header`).then((headers) => { expect([...headers].map((header) => normaliseText(header.textContent ?? ''))).to.deep.eq([ 'Account', 'Name', @@ -101,8 +96,8 @@ describe('Account Enquiry Consolidated Accounts Tab', () => { 'Reference', ]); }); - cy.get(rows).should('have.length', 2); - cy.get(tab).within(() => { + cy.get(ConsolidatedAccountsLocators.tableRows).should('have.length', 2); + cy.get(ConsolidatedAccountsLocators.tabRoot).within(() => { cy.get('input, textarea, select, button, [contenteditable="true"]').should('not.exist'); }); diff --git a/cypress/component/fineAccountEnquiry/accountEnquiry/intercept/defendantAccountIntercepts.ts b/cypress/component/fineAccountEnquiry/accountEnquiry/intercept/defendantAccountIntercepts.ts index e0f0b9d01b..5788217da9 100644 --- a/cypress/component/fineAccountEnquiry/accountEnquiry/intercept/defendantAccountIntercepts.ts +++ b/cypress/component/fineAccountEnquiry/accountEnquiry/intercept/defendantAccountIntercepts.ts @@ -5,6 +5,7 @@ import { IOpalFinesAccountDefendantDetailsFixedPenaltyTabRefData } from '@servic import { IOpalFinesAccountDefendantDetailsHistoryAndNotesTabRefData } from '@services/fines/opal-fines-service/interfaces/opal-fines-account-defendant-details-history-and-notes-tab-ref-data.interface'; import { IOpalFinesAccountDefendantDetailsImpositionsTabRefData } from '@services/fines/opal-fines-service/interfaces/opal-fines-account-defendant-details-impositions-tab-ref-data.interface'; import { IOpalFinesAccountDefendantDetailsPaymentTermsLatest } from '@services/fines/opal-fines-service/interfaces/opal-fines-account-defendant-details-payment-terms-latest.interface'; +import { IOpalFinesAccountDefendantDetailsConsolidatedAccount } from 'src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-defendant-account-consolidated-account.interface'; import { IOpalFinesAccountMinorCreditorAtAGlance } from 'src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-minor-creditor-at-a-glance.interface'; import { IOpalFinesAccountMinorCreditorCreditor } from 'src/app/flows/fines/services/opal-fines-service/interfaces/opal-fines-account-minor-creditor-creditor.interface'; import { IOpalFinesAccountDefendantDetailsHeader } from 'src/app/flows/fines/fines-acc/fines-acc-defendant-details/interfaces/fines-acc-defendant-details-header.interface'; @@ -29,19 +30,27 @@ export function interceptAddNotes() { } /** - * Intercepts the GET request to the defendant accounts "at-a-glance" endpoint and mocks the response. + * Intercepts the GET request to the defendant accounts "consolidated-accounts" endpoint and mocks the response. * * @param accountId - The unique identifier for the account. - * @param mockData - The mock data to be returned in the response body. + * @param consolidatedAccounts - The consolidated accounts to be returned in the response body. * @param respHeaderEtag - The value to set for the ETag response header. - * @returns Cypress chainable object with the intercepted request aliased as 'getAtAGlance'. - * @example - * ```typescript - * const mockAtAGlance: IOpalFinesAccountDefendantAtAGlance = structuredClone(OPAL_FINES_ACCOUNT_DEFENDANT_AT_A_GLANCE_MOCK); - * interceptAtAGlance(mockAtAGlance, 'W/"123456"'); - * cy.wait('@getAtAGlance'); - * ``` + * @returns Cypress chainable object with the intercepted request aliased as 'getConsolidatedAccounts'. */ +export function interceptConsolidatedAccounts( + accountId: string | number, + consolidatedAccounts: IOpalFinesAccountDefendantDetailsConsolidatedAccount[], + respHeaderEtag: string, +) { + return cy + .intercept('GET', `**/defendant-accounts/${accountId}/consolidated-accounts`, { + statusCode: 200, + headers: { ETag: respHeaderEtag }, + body: consolidatedAccounts, + }) + .as('getConsolidatedAccounts'); +} + export function interceptAtAGlance( accountId: number, mockData: IOpalFinesAccountDefendantAtAGlance, diff --git a/cypress/e2e/functional/opal/actions/account-details/details.consolidated-accounts.actions.ts b/cypress/e2e/functional/opal/actions/account-details/details.consolidated-accounts.actions.ts index a20e03a68e..28108c3e4d 100644 --- a/cypress/e2e/functional/opal/actions/account-details/details.consolidated-accounts.actions.ts +++ b/cypress/e2e/functional/opal/actions/account-details/details.consolidated-accounts.actions.ts @@ -1,9 +1,8 @@ +import { ConsolidatedAccountsLocators } from '../../../../../shared/selectors/account-details/account.consolidated-accounts.locators'; import { createScopedLogger } from '../../../../../support/utils/log.helper'; const log = createScopedLogger('AccountDetailsConsolidatedAccountsActions'); -const accountCaptionSelector = 'opal-lib-govuk-heading-with-caption .govuk-caption-l'; - const CHILD_ACCOUNT = { account_id: 99000000990002, account_number: '99009902C', @@ -19,7 +18,6 @@ const CHILD_ACCOUNT = { */ export class AccountDetailsConsolidatedAccountsActions { private static readonly WAIT_MS = 15_000; - private static readonly tab = 'app-fines-acc-defendant-details-consolidated-accounts-tab'; /** * Presents the supplied account as a master account with one consolidated child account. @@ -112,25 +110,42 @@ export class AccountDetailsConsolidatedAccountsActions { }).as('consolidatedChildAtAGlance'); } + /** + * Asserts the consolidated accounts table is visible and contains child account rows. + */ + public assertChildAccountsTableVisible(): void { + log('assert', 'Checking consolidated accounts table is visible'); + + cy.get(ConsolidatedAccountsLocators.tabRoot, { + timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS, + }).should('be.visible'); + cy.get(ConsolidatedAccountsLocators.tableRows, { + timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS, + }).should('have.length.at.least', 1); + cy.get(ConsolidatedAccountsLocators.firstChildAccountLink(0)) + .should('be.visible') + .and('contain.text', CHILD_ACCOUNT.account_number); + } + /** * Opens the Consolidated accounts tab and waits for its table payload. */ public openTab(): void { log('navigate', 'Opening Consolidated accounts tab'); - cy.get('li[subnavitemid="consolidated-accounts-tab"] > a.moj-sub-navigation__link', { + cy.get(ConsolidatedAccountsLocators.tabLink, { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS, }) .should('be.visible') .click(); - cy.get('a.moj-sub-navigation__link[aria-current="page"]') + cy.get(ConsolidatedAccountsLocators.activeTabLink) .should('be.visible') .and('contain.text', 'Consolidated accounts'); cy.wait('@consolidatedAccounts', { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS }) .its('response.statusCode') .should('eq', 200); - cy.get(AccountDetailsConsolidatedAccountsActions.tab, { + cy.get(ConsolidatedAccountsLocators.tabRoot, { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS, }).should('be.visible'); } @@ -141,7 +156,9 @@ export class AccountDetailsConsolidatedAccountsActions { public openFirstChildAtAGlance(): void { log('open', 'Opening first consolidated child account at At a glance'); - cy.get('#consolidated-account-number-0 a', { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS }) + cy.get(ConsolidatedAccountsLocators.firstChildAccountLink(0), { + timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS, + }) .should('be.visible') .and('contain.text', CHILD_ACCOUNT.account_number) .and(($link) => { @@ -161,7 +178,18 @@ export class AccountDetailsConsolidatedAccountsActions { cy.wait('@consolidatedChildAtAGlance', { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS }) .its('response.statusCode') .should('eq', 200); - cy.get(accountCaptionSelector, { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS }) + cy.get(ConsolidatedAccountsLocators.headerCaption, { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS }) + .should('be.visible') + .and('contain.text', CHILD_ACCOUNT.account_number); + } + + /** + * Asserts the selected child account details are shown for the opened record. + */ + public assertSelectedChildAccountDetailsVisible(): void { + log('assert', 'Checking selected child account details are visible'); + + cy.get(ConsolidatedAccountsLocators.headerCaption, { timeout: AccountDetailsConsolidatedAccountsActions.WAIT_MS }) .should('be.visible') .and('contain.text', CHILD_ACCOUNT.account_number); } diff --git a/cypress/e2e/functional/opal/features/fineAccountEnquiry/accountEnquiry/AccountEnquiriesViewDetails.feature b/cypress/e2e/functional/opal/features/fineAccountEnquiry/accountEnquiry/AccountEnquiriesViewDetails.feature index 127ccc6b11..33bbc50990 100644 --- a/cypress/e2e/functional/opal/features/fineAccountEnquiry/accountEnquiry/AccountEnquiriesViewDetails.feature +++ b/cypress/e2e/functional/opal/features/fineAccountEnquiry/accountEnquiry/AccountEnquiriesViewDetails.feature @@ -215,9 +215,13 @@ Feature: Account Enquiries – View Account Details @R1B @JIRA-STORY:PO-2391 @JIRA-EPIC:PO-2332 Scenario: Consolidated account links open the child account At a glance view + # The consolidated-account response is mocked in the test harness because consolidation + # behaviour is not available end-to-end in the live environment yet. Given I am viewing a master account with a consolidated child account - When I go to the Consolidated accounts tab - And I open the first Consolidated accounts link at the At a glance view + When I view the Consolidated accounts tab + Then I can see a table containing the child accounts + When I view the first child account record + Then I am presented with the details of the selected child account Rule: Non-paying defendant account baseline Background: diff --git a/cypress/e2e/functional/opal/flows/account-enquiry.flow.ts b/cypress/e2e/functional/opal/flows/account-enquiry.flow.ts index 49f62bc418..94c8679435 100644 --- a/cypress/e2e/functional/opal/flows/account-enquiry.flow.ts +++ b/cypress/e2e/functional/opal/flows/account-enquiry.flow.ts @@ -692,6 +692,14 @@ export class AccountEnquiryFlow { }); } + /** + * Asserts the consolidated accounts table is visible. + */ + public assertConsolidatedAccountsTableVisible(): void { + logAE('method', 'assertConsolidatedAccountsTableVisible()'); + this.consolidatedAccounts.assertChildAccountsTableVisible(); + } + /** * Navigates to the Consolidated accounts tab and asserts it has loaded. */ @@ -709,6 +717,14 @@ export class AccountEnquiryFlow { this.detailsNav.assertAtAGlanceTabIsActive(); } + /** + * Asserts the selected child account details are displayed. + */ + public assertSelectedChildAccountDetailsVisible(): void { + logAE('method', 'assertSelectedChildAccountDetailsVisible()'); + this.consolidatedAccounts.assertSelectedChildAccountDetailsVisible(); + } + /** * Asserts the initial History and notes rows have been rendered. */ diff --git a/cypress/shared/selectors/account-details/account.consolidated-accounts.locators.ts b/cypress/shared/selectors/account-details/account.consolidated-accounts.locators.ts new file mode 100644 index 0000000000..f38a75beed --- /dev/null +++ b/cypress/shared/selectors/account-details/account.consolidated-accounts.locators.ts @@ -0,0 +1,10 @@ +export const ConsolidatedAccountsLocators = { + tabRoot: 'app-fines-acc-defendant-details-consolidated-accounts-tab', + tabLink: 'li[subnavitemid="consolidated-accounts-tab"] > a.moj-sub-navigation__link', + activeTabLink: 'a.moj-sub-navigation__link[aria-current="page"]', + headerCaption: 'opal-lib-govuk-heading-with-caption .govuk-caption-l', + tableRows: 'app-fines-acc-defendant-details-consolidated-accounts-tab tbody tr.govuk-table__row', + heading: 'app-fines-acc-defendant-details-consolidated-accounts-tab h2', + cell: (columnKey: string, rowIndex: number): string => `#consolidated-account-${columnKey}-${rowIndex}`, + firstChildAccountLink: (rowIndex: number): string => `#consolidated-account-number-${rowIndex} a`, +} as const; diff --git a/cypress/support/step_definitions/searchForAccount/account-enquiry.steps.ts b/cypress/support/step_definitions/searchForAccount/account-enquiry.steps.ts index aec63c9750..133a51c0cc 100644 --- a/cypress/support/step_definitions/searchForAccount/account-enquiry.steps.ts +++ b/cypress/support/step_definitions/searchForAccount/account-enquiry.steps.ts @@ -489,13 +489,37 @@ When('I go to the History and notes tab', () => { }); /** - * @step Navigates to the Consolidated accounts tab. + * @step Views the Consolidated accounts tab. */ -When('I go to the Consolidated accounts tab', () => { - log('step', 'Navigate to Consolidated accounts tab'); +When('I view the Consolidated accounts tab', () => { + log('step', 'View Consolidated accounts tab'); flow().goToConsolidatedAccountsTab(); }); +/** + * @step Verifies the consolidated accounts table shows child account rows. + */ +Then('I can see a table containing the child accounts', () => { + log('assert', 'Consolidated accounts table contains child account rows'); + flow().assertConsolidatedAccountsTableVisible(); +}); + +/** + * @step Views the first child account record from the consolidated accounts table. + */ +When('I view the first child account record', () => { + log('step', 'View first child account record'); + flow().openFirstConsolidatedAccountLinkAtAGlance(); +}); + +/** + * @step Verifies the selected child account details are displayed. + */ +Then('I am presented with the details of the selected child account', () => { + log('assert', 'Selected child account details are displayed'); + flow().assertSelectedChildAccountDetailsVisible(); +}); + /** * @step Verifies History and notes rows loaded. */ @@ -528,14 +552,6 @@ When('I open the first History and notes account link in a new tab', () => { flow().openHistoryAndNotesAccountLinkInNewTab(); }); -/** - * @step Opens the first consolidated account link and verifies the At a glance route. - */ -When('I open the first Consolidated accounts link at the At a glance view', () => { - log('step', 'Open first Consolidated accounts link at the At a glance view'); - flow().openFirstConsolidatedAccountLinkAtAGlance(); -}); - /** * @step Opens the amend payment terms form. */ From a26387ed00e2390d654ff9c34a2f07a0958217e1 Mon Sep 17 00:00:00 2001 From: Ant-Collins Date: Thu, 30 Jul 2026 10:04:34 +0100 Subject: [PATCH 09/12] JsDoc added for the at a glance intercept --- .../intercept/defendantAccountIntercepts.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cypress/component/fineAccountEnquiry/accountEnquiry/intercept/defendantAccountIntercepts.ts b/cypress/component/fineAccountEnquiry/accountEnquiry/intercept/defendantAccountIntercepts.ts index 5788217da9..c970b19017 100644 --- a/cypress/component/fineAccountEnquiry/accountEnquiry/intercept/defendantAccountIntercepts.ts +++ b/cypress/component/fineAccountEnquiry/accountEnquiry/intercept/defendantAccountIntercepts.ts @@ -51,6 +51,20 @@ export function interceptConsolidatedAccounts( .as('getConsolidatedAccounts'); } +/** + * Intercepts the GET request to the defendant accounts "at-a-glance" endpoint and mocks the response. + * + * @param accountId - The unique identifier for the account. + * @param mockData - The mock data to be returned in the response body. + * @param respHeaderEtag - The value to set for the ETag response header. + * @returns Cypress chainable object with the intercepted request aliased as 'getAtAGlance'. + * @example + * ```typescript + * const mockAtAGlance: IOpalFinesAccountDefendantAtAGlance = structuredClone(OPAL_FINES_ACCOUNT_DEFENDANT_AT_A_GLANCE_MOCK); + * interceptAtAGlance(mockAtAGlance, 'W/"123456"'); + * cy.wait('@getAtAGlance'); + * ``` + */ export function interceptAtAGlance( accountId: number, mockData: IOpalFinesAccountDefendantAtAGlance, From 0db6f5b308f216b840485c8d0dd694bcd46e5d23 Mon Sep 17 00:00:00 2001 From: cadefaulkner Date: Thu, 30 Jul 2026 10:39:37 +0100 Subject: [PATCH 10/12] Add accessibility and functionality tests for consolidated accounts feature --- .../AccountEnquiryConsolidatedAccounts.cy.ts | 7 +------ ...eature => AccountEnquiriesConsolidation.feature} | 0 ...countEnquiriesConsolidationAccessibility.feature | 13 +++++++++++++ 3 files changed, 14 insertions(+), 6 deletions(-) rename cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/{AccountEnquireiesConsolidation.feature => AccountEnquiriesConsolidation.feature} (100%) create mode 100644 cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidationAccessibility.feature diff --git a/cypress/component/fineAccountEnquiry/accountEnquiry/AccountEnquiryConsolidatedAccounts.cy.ts b/cypress/component/fineAccountEnquiry/accountEnquiry/AccountEnquiryConsolidatedAccounts.cy.ts index fa591da317..d71f02f71f 100644 --- a/cypress/component/fineAccountEnquiry/accountEnquiry/AccountEnquiryConsolidatedAccounts.cy.ts +++ b/cypress/component/fineAccountEnquiry/accountEnquiry/AccountEnquiryConsolidatedAccounts.cy.ts @@ -7,6 +7,7 @@ import { DEFENDANT_HEADER_MOCK } from './mocks/defendant_details_mock'; import { interceptConsolidatedAccounts, interceptDefendantHeader } from './intercept/defendantAccountIntercepts'; import { IComponentProperties } from './setup/setupComponent.interface'; import { setupAccountEnquiryComponent } from './setup/SetupComponent'; +import 'cypress-axe'; const ACCOUNT_ENQUIRY_JIRA_LABEL = '@JIRA-LABEL:account-enquiry'; @@ -95,9 +96,6 @@ describe('Account Enquiry Consolidated Accounts Tab', () => { expect(labels).to.include('Consolidated accounts'); expect(labels.indexOf('History and notes')).to.be.lessThan(labels.indexOf('Consolidated accounts')); }); - - cy.injectAxe(); - cy.checkA11y(AccountNavDetailsLocators.subNav.root); }, ); @@ -109,9 +107,6 @@ describe('Account Enquiry Consolidated Accounts Tab', () => { cy.get(AccountNavDetailsLocators.subNav.historyAndNotesTab).should('exist'); cy.get(AccountNavDetailsLocators.subNav.consolidatedAccountsTab).should('not.exist'); - - cy.injectAxe(); - cy.checkA11y(AccountNavDetailsLocators.subNav.root); }, ); diff --git a/cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquireiesConsolidation.feature b/cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidation.feature similarity index 100% rename from cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquireiesConsolidation.feature rename to cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidation.feature diff --git a/cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidationAccessibility.feature b/cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidationAccessibility.feature new file mode 100644 index 0000000000..65f8493f4d --- /dev/null +++ b/cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidationAccessibility.feature @@ -0,0 +1,13 @@ +@JIRA-LABEL:account-enquiry +Feature: Defendant - Adult or youth - Account Enquiries - Consolidation Accessibility + As an Opal user + I want to view the child account of a defendant's consolidated account + + @R1B @JIRA-STORY:PO-2391 @JIRA-EPIC:PO-2332 + Scenario: Consolidated account tab accessibility + # The consolidated-account response is mocked in the test harness because consolidation + # behaviour is not available end-to-end in the live environment yet. + Given I am viewing a master account with a consolidated child account + When I view the Consolidated accounts tab + And I can see a table containing the child accounts + Then I check the page for accessibility From d2c26c01783d54eee8a00a7161887cc91fb8f5b9 Mon Sep 17 00:00:00 2001 From: cadefaulkner Date: Thu, 30 Jul 2026 11:20:39 +0100 Subject: [PATCH 11/12] Enhance account enquiry features with draft account creation and update step definitions for consolidated accounts --- .../AccountEnquiriesConsolidation.feature | 12 ++++++++++++ ...ccountEnquiriesConsolidationAccessibility.feature | 11 +++++++++++ .../searchForAccount/account-enquiry.steps.ts | 10 +++++----- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidation.feature b/cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidation.feature index 350e7fe500..8ada6d45c6 100644 --- a/cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidation.feature +++ b/cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidation.feature @@ -3,6 +3,18 @@ Feature: Defendant - Adult or youth - Account Enquiries - Consolidation As an Opal user I want to view the child account of a defendant's consolidated account + Background: + Given I am logged in with email "opal-test@dev.platform.hmcts.net" + Given I create a "adultOrYouthOnly" draft account with the following details and set status "Publishing Pending" using user "opal-test-10@dev.platform.hmcts.net": + | Account_status | Submitted | + | account.defendant.title | Ms | + | account.defendant.forenames | Harriet | + | account.defendant.surname | ConsolidatedAccount{uniq} | + | account.defendant.email_address_1 | Harriet.ConsolidatedAccount{uniq}@test.com | + | account.defendant.dob | 2002-05-15 | + When I search for the account by last name "ConsolidatedAccount{uniq}" and open the latest result + + @R1B @JIRA-STORY:PO-2391 @JIRA-EPIC:PO-2332 Scenario: Consolidated account links open the child account At a glance view # The consolidated-account response is mocked in the test harness because consolidation diff --git a/cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidationAccessibility.feature b/cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidationAccessibility.feature index 65f8493f4d..a95ff0983d 100644 --- a/cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidationAccessibility.feature +++ b/cypress/e2e/functional/opal/features/release1b/fineAccountEnquiry/defendant/adult-youth/AccountEnquiriesConsolidationAccessibility.feature @@ -3,6 +3,17 @@ Feature: Defendant - Adult or youth - Account Enquiries - Consolidation Accessib As an Opal user I want to view the child account of a defendant's consolidated account + Background: + Given I am logged in with email "opal-test@dev.platform.hmcts.net" + Given I create a "adultOrYouthOnly" draft account with the following details and set status "Publishing Pending" using user "opal-test-10@dev.platform.hmcts.net": + | Account_status | Submitted | + | account.defendant.title | Ms | + | account.defendant.forenames | Harriet | + | account.defendant.surname | ConsolidatedAccount{uniq} | + | account.defendant.email_address_1 | Harriet.ConsolidatedAccount{uniq}@test.com | + | account.defendant.dob | 2002-05-15 | + When I search for the account by last name "ConsolidatedAccount{uniq}" and open the latest result + @R1B @JIRA-STORY:PO-2391 @JIRA-EPIC:PO-2332 Scenario: Consolidated account tab accessibility # The consolidated-account response is mocked in the test harness because consolidation diff --git a/cypress/support/step_definitions/searchForAccount/account-enquiry.steps.ts b/cypress/support/step_definitions/searchForAccount/account-enquiry.steps.ts index 0e66fa4ff7..e3607856d6 100644 --- a/cypress/support/step_definitions/searchForAccount/account-enquiry.steps.ts +++ b/cypress/support/step_definitions/searchForAccount/account-enquiry.steps.ts @@ -479,7 +479,7 @@ Given('the History and notes API is stubbed with standard tab data', () => { */ Given('I am viewing a master account with a consolidated child account', () => { log('step', 'Prepare master account with consolidated child account'); - flow().prepareMasterAccountWithConsolidatedChildAccount(); + accountEnquiryFlow().prepareMasterAccountWithConsolidatedChildAccount(); }); /** @@ -495,7 +495,7 @@ When('I go to the History and notes tab', () => { */ When('I view the Consolidated accounts tab', () => { log('step', 'View Consolidated accounts tab'); - flow().goToConsolidatedAccountsTab(); + accountEnquiryFlow().goToConsolidatedAccountsTab(); }); /** @@ -503,7 +503,7 @@ When('I view the Consolidated accounts tab', () => { */ Then('I can see a table containing the child accounts', () => { log('assert', 'Consolidated accounts table contains child account rows'); - flow().assertConsolidatedAccountsTableVisible(); + accountEnquiryFlow().assertConsolidatedAccountsTableVisible(); }); /** @@ -511,7 +511,7 @@ Then('I can see a table containing the child accounts', () => { */ When('I view the first child account record', () => { log('step', 'View first child account record'); - flow().openFirstConsolidatedAccountLinkAtAGlance(); + accountEnquiryFlow().openFirstConsolidatedAccountLinkAtAGlance(); }); /** @@ -519,7 +519,7 @@ When('I view the first child account record', () => { */ Then('I am presented with the details of the selected child account', () => { log('assert', 'Selected child account details are displayed'); - flow().assertSelectedChildAccountDetailsVisible(); + accountEnquiryFlow().assertSelectedChildAccountDetailsVisible(); }); /** From 403b80b87102430370c4bb48107f8a9978838022 Mon Sep 17 00:00:00 2001 From: cadefaulkner Date: Thu, 30 Jul 2026 14:29:43 +0100 Subject: [PATCH 12/12] Update yarn-audit-known-issues to remove deprecated and vulnerable packages --- yarn-audit-known-issues | 2 -- 1 file changed, 2 deletions(-) diff --git a/yarn-audit-known-issues b/yarn-audit-known-issues index 4ca3309a22..612d1f472e 100644 --- a/yarn-audit-known-issues +++ b/yarn-audit-known-issues @@ -3,7 +3,6 @@ {"value":"@opentelemetry/propagator-jaeger","children":{"ID":1124011,"Issue":"OpenTelemetry JavaScript: Denial of service in `JaegerPropagator` via unhandled exception on a malformed header","URL":"https://github.com/advisories/GHSA-45rx-2jwx-cxfr","Severity":"high","Vulnerable Versions":"<2.9.0","Tree Versions":["2.7.1"],"Dependents":["@opentelemetry/sdk-node@virtual:7d28986b6beca5611db0460c59742827c485e9e4ad2f65e45fb8287e62953517d20a6f66193b45448e6d2e95136688c6edcf391e563ea2038165c9fdaf765dae#npm:0.217.0"]}} {"value":"ajv","children":{"ID":1113715,"Issue":"ajv has ReDoS when using `$data` option","URL":"https://github.com/advisories/GHSA-2g4f-4pwh-qvx6","Severity":"moderate","Vulnerable Versions":">=7.0.0-alpha.0 <8.18.0","Tree Versions":["8.17.1"],"Dependents":["schema-utils@npm:4.3.3"]}} {"value":"body-parser","children":{"ID":1123976,"Issue":"body-parser vulnerable to denial of service when invalid limit value silently disables size enforcement","URL":"https://github.com/advisories/GHSA-v422-hmwv-36x6","Severity":"low","Vulnerable Versions":">=2.0.0 <2.3.0","Tree Versions":["2.2.2"],"Dependents":["opal-frontend@workspace:."]}} -{"value":"brace-expansion","children":{"ID":1124334,"Issue":"brace-expansion: DoS via unbounded expansion length causing an out-of-memory process crash","URL":"https://github.com/advisories/GHSA-mh99-v99m-4gvg","Severity":"high","Vulnerable Versions":"<=5.0.7","Tree Versions":["5.0.7"],"Dependents":["minimatch@npm:3.1.2"]}} {"value":"fast-uri","children":{"ID":1124064,"Issue":"fast-uri vulnerable to host confusion via literal backslash authority delimiter","URL":"https://github.com/advisories/GHSA-v2hh-gcrm-f6hx","Severity":"high","Vulnerable Versions":">=3.0.0 <=3.1.3","Tree Versions":["3.1.2"],"Dependents":["ajv@npm:8.17.1"]}} {"value":"fast-uri","children":{"ID":1124231,"Issue":"fast-uri vulnerable to host confusion via failed IDN canonicalization","URL":"https://github.com/advisories/GHSA-4c8g-83qw-93j6","Severity":"high","Vulnerable Versions":">=3.0.0 <3.1.3","Tree Versions":["3.1.2"],"Dependents":["ajv@npm:8.17.1"]}} {"value":"fast-uri","children":{"ID":1124064,"Issue":"fast-uri vulnerable to host confusion via literal backslash authority delimiter","URL":"https://github.com/advisories/GHSA-v2hh-gcrm-f6hx","Severity":"high","Vulnerable Versions":">=3.0.0 <=3.1.3","Tree Versions":["3.1.2"],"Dependents":["ajv@npm:8.17.1"]}} @@ -34,7 +33,6 @@ {"value":"qs","children":{"ID":1119502,"Issue":"qs has a remotely triggerable DoS: qs.stringify crashes with TypeError on null/undefined entries in comma-format arrays when encodeValuesOnly is set","URL":"https://github.com/advisories/GHSA-q8mj-m7cp-5q26","Severity":"moderate","Vulnerable Versions":">=6.11.1 <=6.15.1","Tree Versions":["6.14.2"],"Dependents":["body-parser@npm:2.2.2"]}} {"value":"serialize-javascript","children":{"ID":1113686,"Issue":"Serialize JavaScript is Vulnerable to RCE via RegExp.flags and Date.prototype.toISOString()","URL":"https://github.com/advisories/GHSA-5c6j-r48x-rmvq","Severity":"high","Vulnerable Versions":"<=7.0.2","Tree Versions":["6.0.2"],"Dependents":["mocha@npm:11.7.6"]}} {"value":"serialize-javascript","children":{"ID":1119440,"Issue":"Serialize JavaScript has CPU Exhaustion Denial of Service via crafted array-like objects","URL":"https://github.com/advisories/GHSA-qj8w-gfj5-8c6v","Severity":"moderate","Vulnerable Versions":">=5.0.0 <7.0.5","Tree Versions":["6.0.2"],"Dependents":["mocha@npm:11.7.6"]}} -{"value":"tar","children":{"ID":1124287,"Issue":"node-tar: Uncontrolled recursion in mapHas/filesFilter allows uncatchable stack-overflow DoS via crafted long-path tar with member selection","URL":"https://github.com/advisories/GHSA-r292-9mhp-454m","Severity":"moderate","Vulnerable Versions":"<=7.5.20","Tree Versions":["7.5.20"],"Dependents":["node-gyp@npm:10.2.0"]}} {"value":"undici","children":{"ID":1121186,"Issue":"undici vulnerable to TLS certificate validation bypass via dropped requestTls in SOCKS5 ProxyAgent","URL":"https://github.com/advisories/GHSA-vmh5-mc38-953g","Severity":"high","Vulnerable Versions":">=8.0.0 <8.5.0","Tree Versions":["8.3.0"],"Dependents":["@actions/http-client@npm:2.2.3"]}} {"value":"undici","children":{"ID":1121190,"Issue":"undici WebSocket client vulnerable to denial of service via cumulative fragment bypass","URL":"https://github.com/advisories/GHSA-38rv-x7px-6hhq","Severity":"high","Vulnerable Versions":">=8.0.0 <8.5.0","Tree Versions":["8.3.0"],"Dependents":["@actions/http-client@npm:2.2.3"]}} {"value":"undici","children":{"ID":1121240,"Issue":"undici vulnerable to HTTP header injection via Set-Cookie percent-decoding","URL":"https://github.com/advisories/GHSA-p88m-4jfj-68fv","Severity":"moderate","Vulnerable Versions":">=8.0.0 <8.5.0","Tree Versions":["8.3.0"],"Dependents":["@actions/http-client@npm:2.2.3"]}}