(() => {
+ const message = this.addToListValidationMessageSignal();
+ return message ? [{ fieldId: 'defendants-select-all-checkbox', message }] : [];
+ });
public readonly selectedAccountsHintComputed = computed(() => {
return `${this.selectedAccountsCountComputed()} to ${this.totalAccountsCountComputed()} accounts selected`;
});
@@ -335,9 +344,30 @@ export class FinesConSearchResultDefendantTableWrapperComponent extends Abstract
.map((row) => row['Account ID'])
.filter((accountId): accountId is number => accountId !== null);
+ if (selectedAccountIds.length === 0) {
+ this.addToListValidationMessageSignal.set(this.ADD_TO_LIST_VALIDATION_MESSAGE);
+ return;
+ }
+
+ this.addToListValidationMessageSignal.set(null);
this.addToList.emit(Array.from(new Set(selectedAccountIds)));
}
+ /**
+ * Scrolls/focuses the requested target from the error summary.
+ *
+ * @param fieldId - DOM id to scroll to.
+ */
+ public scrollTo(fieldId: string): void {
+ const targetElement = document.getElementById(fieldId);
+ if (!targetElement) {
+ return;
+ }
+
+ targetElement.scrollIntoView({ block: 'center' });
+ targetElement.focus?.();
+ }
+
/**
* Resolves whether a row id maps to a selectable row.
*
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html
index bdfd464aed..a0dbdaeb28 100644
--- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html
@@ -5,6 +5,7 @@
[existingSortState]="defendantsSort"
[defendantType]="defendantType"
(accountIdClicked)="onAccountIdClick($event)"
+ (addToList)="onAddToList($event)"
>
} @else {
There are no matching results
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.spec.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.spec.ts
index 5015ea1a31..34c3f655e5 100644
--- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.spec.ts
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.spec.ts
@@ -282,4 +282,24 @@ describe('FinesConSearchResultComponent', () => {
}),
]);
});
+
+ it('should log selection alert navigation when at least one selected account is already in list', () => {
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+ const addSelectedAccountIdsSpy = vi.spyOn(finesConStore, 'addSelectedAccountIds');
+
+ component.alreadyAddedAccountIds = [11];
+ component.onAddToList([11, 12]);
+
+ expect(logSpy).toHaveBeenCalledWith('PO-2422: navigate to Selection alert screen');
+ expect(addSelectedAccountIdsSpy).not.toHaveBeenCalled();
+ });
+
+ it('should patch selected account ids to store when all selected ids are new', () => {
+ const addSelectedAccountIdsSpy = vi.spyOn(finesConStore, 'addSelectedAccountIds');
+
+ component.alreadyAddedAccountIds = [99];
+ component.onAddToList([11, 12]);
+
+ expect(addSelectedAccountIdsSpy).toHaveBeenCalledWith([11, 12]);
+ });
});
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts
index 524da68819..78c4cab4db 100644
--- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts
@@ -38,6 +38,7 @@ export class FinesConSearchResultComponent implements OnDestroy {
@Input({ required: true })
public defendantType: FinesConDefendant = 'individual';
+ @Input({ required: false }) public alreadyAddedAccountIds: number[] = [];
@Input({ required: false })
public set searchPayload(searchPayload: IOpalFinesDefendantAccountSearchParams | null) {
@@ -126,6 +127,27 @@ export class FinesConSearchResultComponent implements OnDestroy {
window.open(url, '_blank');
}
+ /**
+ * Handles Add to list selections.
+ * AC6c temporary behaviour: log when selected accounts already exist in consolidation list.
+ *
+ * @param selectedAccountIds - Selected account ids emitted from the table wrapper.
+ */
+ public onAddToList(selectedAccountIds: number[]): void {
+ const hasAlreadyAddedAccount = selectedAccountIds.some((accountId) =>
+ this.alreadyAddedAccountIds.includes(accountId),
+ );
+
+ if (hasAlreadyAddedAccount) {
+ // eslint-disable-next-line no-console
+ console.log('PO-2422: navigate to Selection alert screen');
+ return;
+ }
+
+ this.finesConStore.addSelectedAccountIds(selectedAccountIds);
+ // Navigation to "For consolidation" tab is implemented in a separate ticket.
+ }
+
public ngOnDestroy(): void {
this.defendantAccountsSearchSubscription?.unsubscribe();
this.defendantAccountsSearchSubscription = null;
diff --git a/src/app/flows/fines/fines-con/stores/fines-con.store.spec.ts b/src/app/flows/fines/fines-con/stores/fines-con.store.spec.ts
index 6baec01967..e474c7c611 100644
--- a/src/app/flows/fines/fines-con/stores/fines-con.store.spec.ts
+++ b/src/app/flows/fines/fines-con/stores/fines-con.store.spec.ts
@@ -25,6 +25,7 @@ describe('FinesConStore', () => {
expect(store.selectBuForm().formData.fcon_select_bu_business_unit_id).toBeNull();
expect(store.selectBuForm().formData.fcon_select_bu_defendant_type).toBe('individual');
expect(store.selectBuForm().nestedFlow).toBe(false);
+ expect(store.selectedAccountIds()).toEqual([]);
});
it('should update business unit and defendant type', () => {
@@ -58,6 +59,7 @@ describe('FinesConStore', () => {
it('should reset entire consolidation state', () => {
store.updateSelectBuForm(FINES_CON_SELECT_BU_FORM_INDIVIDUAL_MOCK.formData);
+ store.addSelectedAccountIds([11, 12]);
store.resetConsolidationState();
expect(store.selectBuForm().formData.fcon_select_bu_business_unit_id).toBe(
@@ -67,6 +69,7 @@ describe('FinesConStore', () => {
FINES_CON_SELECT_BU_FORM.formData.fcon_select_bu_defendant_type,
);
expect(store.selectBuForm().nestedFlow).toBe(FINES_CON_SELECT_BU_FORM.nestedFlow);
+ expect(store.selectedAccountIds()).toEqual([]);
});
it('should compute business unit id correctly', () => {
@@ -122,6 +125,7 @@ describe('FinesConStore', () => {
const testData = FINES_CON_SEARCH_ACCOUNT_FORM_ACCOUNT_NUMBER_MOCK.formData;
store.updateSearchAccountFormTemporary(testData);
+ store.addSelectedAccountIds([11]);
expect(store.searchAccountForm().fcon_search_account_number).toBe(
FINES_CON_SEARCH_ACCOUNT_FORM_ACCOUNT_NUMBER_MOCK.formData.fcon_search_account_number,
);
@@ -130,6 +134,14 @@ describe('FinesConStore', () => {
expect(store.searchAccountForm()).toEqual(FINES_CON_SEARCH_ACCOUNT_STATE);
expect(store.searchAccountForm().fcon_search_account_number).toBeNull();
+ expect(store.selectedAccountIds()).toEqual([]);
+ });
+
+ it('should add selected account ids uniquely', () => {
+ store.addSelectedAccountIds([11, 12, 12]);
+ store.addSelectedAccountIds([12, 13]);
+
+ expect(store.selectedAccountIds()).toEqual([11, 12, 13]);
});
it('should preserve search account form data when updating', () => {
diff --git a/src/app/flows/fines/fines-con/stores/fines-con.store.ts b/src/app/flows/fines/fines-con/stores/fines-con.store.ts
index 3a0566f4fb..0ad33aafb0 100644
--- a/src/app/flows/fines/fines-con/stores/fines-con.store.ts
+++ b/src/app/flows/fines/fines-con/stores/fines-con.store.ts
@@ -14,6 +14,7 @@ export const FinesConStore = signalStore(
searchAccountForm: FINES_CON_SEARCH_ACCOUNT_STATE,
individualResults: [] as IFinesConSearchResultDefendantAccount[],
companyResults: [] as IFinesConSearchResultDefendantAccount[],
+ selectedAccountIds: [] as number[],
activeTab: 'search',
stateChanges: false,
unsavedChanges: false,
@@ -26,6 +27,7 @@ export const FinesConStore = signalStore(
searchAccountForm: FINES_CON_SEARCH_ACCOUNT_STATE,
individualResults: [],
companyResults: [],
+ selectedAccountIds: [],
activeTab: 'search',
stateChanges: false,
unsavedChanges: false,
@@ -63,6 +65,7 @@ export const FinesConStore = signalStore(
searchAccountForm: FINES_CON_SEARCH_ACCOUNT_STATE,
individualResults: [],
companyResults: [],
+ selectedAccountIds: [],
stateChanges: true,
});
},
@@ -96,6 +99,7 @@ export const FinesConStore = signalStore(
searchAccountForm: FINES_CON_SEARCH_ACCOUNT_STATE,
individualResults: [],
companyResults: [],
+ selectedAccountIds: [],
});
},
@@ -112,6 +116,15 @@ export const FinesConStore = signalStore(
});
},
+ /**
+ * Adds selected account ids for consolidation, ensuring uniqueness.
+ */
+ addSelectedAccountIds(selectedAccountIds: number[]): void {
+ patchState(store, {
+ selectedAccountIds: Array.from(new Set([...store.selectedAccountIds(), ...selectedAccountIds])),
+ });
+ },
+
/**
* Resets the entire consolidation form state
*/
@@ -121,6 +134,7 @@ export const FinesConStore = signalStore(
searchAccountForm: FINES_CON_SEARCH_ACCOUNT_STATE,
individualResults: [],
companyResults: [],
+ selectedAccountIds: [],
stateChanges: false,
activeTab: 'search',
unsavedChanges: false,
From 266a2d9e6a874a35fad48c1acae05a1405bd2a3c Mon Sep 17 00:00:00 2001
From: Arnab subedi <147511052+Arnabsubedi233@users.noreply.github.com>
Date: Wed, 18 Mar 2026 17:23:38 +0000
Subject: [PATCH 2/5] fix merge conflict issue
---
...n-search-result-defendant-table-wrapper.component.html | 8 ++++----
...con-search-result-defendant-table-wrapper.component.ts | 7 ++++---
2 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html
index 450eea6f5a..80b50507f4 100644
--- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html
@@ -168,8 +168,8 @@ Select accounts to consolidate
opal-lib-govuk-checkboxes-item
opalLibMojMultiSelectBody
[control]="getRowControl(row, $index)"
- [inputId]="getRowCheckboxId(row, $index)"
- [inputName]="getRowCheckboxId(row, $index)"
+ [inputId]="getRowDomId(row, $index, 'defendant-select')"
+ [inputName]="getRowDomId(row, $index, 'defendant-select')"
labelText=""
labelClasses="govuk-!-padding-0"
[rowId]="getRowIdentifier(row, $index)"
@@ -187,7 +187,7 @@ Select accounts to consolidate
{{ row['Account']! }}Select accounts to consolidate
|
@for (severity of checkSeverities; track severity) {
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.ts
index c05ea29a20..77ca3797e6 100644
--- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.ts
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.ts
@@ -174,15 +174,16 @@ export class FinesConSearchResultDefendantTableWrapperComponent extends Abstract
}
/**
- * Builds checkbox DOM id for a row.
+ * Builds a sanitised DOM id for a row using the supplied prefix.
*
* @param row - Table row data.
* @param index - Row index fallback.
- * @returns Sanitised checkbox id.
+ * @param prefix - Id prefix used by the caller context.
+ * @returns Sanitised row id with prefix.
*/
public getRowDomId(row: IFinesConSearchResultDefendantTableWrapperTableData, index: number, prefix: string): string {
const raw = String(this.getRowIdentifier(row, index));
- return `defendant-checks-${raw.replaceAll(/[^a-zA-Z0-9_-]/g, '-')}`;
+ return `${prefix}-${raw.replaceAll(/[^a-zA-Z0-9_-]/g, '-')}`;
}
/**
From a050dcbaa2ca23c93fac6fc976a9c676deaf74c2 Mon Sep 17 00:00:00 2001
From: Arnab subedi <147511052+Arnabsubedi233@users.noreply.github.com>
Date: Tue, 24 Mar 2026 10:18:25 +0000
Subject: [PATCH 3/5] fixing merge conflicts
---
src/app/flows/fines/fines-con/stores/fines-con.store.spec.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/app/flows/fines/fines-con/stores/fines-con.store.spec.ts b/src/app/flows/fines/fines-con/stores/fines-con.store.spec.ts
index b2e25fa59d..f5b0d8f3a6 100644
--- a/src/app/flows/fines/fines-con/stores/fines-con.store.spec.ts
+++ b/src/app/flows/fines/fines-con/stores/fines-con.store.spec.ts
@@ -135,7 +135,6 @@ describe('FinesConStore', () => {
expect(store.searchAccountForm()).toEqual(FINES_CON_SEARCH_ACCOUNT_STATE);
expect(store.searchAccountForm().fcon_search_account_number).toBeNull();
- expect(store.selectedAccountIds()).toEqual([]);
});
it('should add selected account ids uniquely', () => {
From c5ad856a0c779ecbb8c904ec531a78ae1d27bafc Mon Sep 17 00:00:00 2001
From: Arnab subedi <147511052+Arnabsubedi233@users.noreply.github.com>
Date: Tue, 24 Mar 2026 15:12:23 +0000
Subject: [PATCH 4/5] small spelling fix
---
...fines-con-search-result-defendant-table-wrapper.component.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.ts
index 77ca3797e6..a253140699 100644
--- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.ts
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.ts
@@ -81,7 +81,7 @@ export class FinesConSearchResultDefendantTableWrapperComponent extends Abstract
return message ? [{ fieldId: 'defendants-select-all-checkbox', message }] : [];
});
public readonly selectedAccountsHintComputed = computed(() => {
- return `${this.selectedAccountsCountComputed()} to ${this.totalAccountsCountComputed()} accounts selected`;
+ return `${this.selectedAccountsCountComputed()} of ${this.totalAccountsCountComputed()} accounts selected`;
});
/**
From ec63f44a7b903ebe5b7ad46e7acc075c007b9cdc Mon Sep 17 00:00:00 2001
From: Arnab subedi <147511052+Arnabsubedi233@users.noreply.github.com>
Date: Mon, 30 Mar 2026 15:57:08 +0100
Subject: [PATCH 5/5] PO-2417 (#2349)
Co-authored-by: hmcts-jenkins-cnp <60659747+hmcts-jenkins-cnp[bot]@users.noreply.github.com>
Co-authored-by: jonathanDuffy
---
.../fines/consolidation/AccountResult.cy.ts | 777 ++++++++++++++++++
.../fines/consolidation/AccountSearch.cy.ts | 311 +++++--
.../fines/consolidation/ErrorPage.cy.ts | 64 ++
.../mocks/account_results_mock.ts | 183 +++++
.../consolidation/setup/SetupComponent.ts | 6 +
.../setup/setupComponent.interface.ts | 2 +
.../consolidation/consolidation.actions.ts | 336 +++++++-
.../FineAccountConsolidation.feature | 179 +++-
...sAccountConsolidationAccessibility.feature | 73 +-
.../opal/flows/consolidation.flow.ts | 97 +++
.../consolidation/AccountResults.locators.ts | 43 +
.../consolidation/AccountSearch.locators.ts | 1 +
.../consolidation/ErrorPage.locators.ts | 8 +
.../consolidation/consolidation.steps.ts | 75 ++
.../fines-con-consolidate-acc.component.html | 11 +-
...ines-con-consolidate-acc.component.spec.ts | 20 +-
...-con-search-account-form.component.spec.ts | 62 +-
...fines-con-search-account-form.component.ts | 39 +
.../fines-con-search-error.component.html | 15 +
.../fines-con-search-error.component.spec.ts | 59 ++
.../fines-con-search-error.component.ts | 37 +
...ult-defendant-table-wrapper.component.html | 27 +-
...-defendant-table-wrapper.component.spec.ts | 32 +
...esult-defendant-table-wrapper.component.ts | 21 +
.../fines-con-search-result.component.html | 28 +-
.../fines-con-search-result.component.spec.ts | 29 +-
.../fines-con-search-result.component.ts | 38 +-
.../fines-con-routing-paths.constant.ts | 1 +
.../fines-con-routing-titles.constant.ts | 1 +
.../fines-con/routing/fines-con.routes.ts | 14 +
.../fines-con-routing-paths.interface.ts | 1 +
31 files changed, 2466 insertions(+), 124 deletions(-)
create mode 100644 cypress/component/fines/consolidation/AccountResult.cy.ts
create mode 100644 cypress/component/fines/consolidation/ErrorPage.cy.ts
create mode 100644 cypress/component/fines/consolidation/mocks/account_results_mock.ts
create mode 100644 cypress/shared/selectors/consolidation/AccountResults.locators.ts
create mode 100644 cypress/shared/selectors/consolidation/ErrorPage.locators.ts
create mode 100644 src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component.html
create mode 100644 src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component.spec.ts
create mode 100644 src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component.ts
diff --git a/cypress/component/fines/consolidation/AccountResult.cy.ts b/cypress/component/fines/consolidation/AccountResult.cy.ts
new file mode 100644
index 0000000000..ff59bfe565
--- /dev/null
+++ b/cypress/component/fines/consolidation/AccountResult.cy.ts
@@ -0,0 +1,777 @@
+import { AccountSearchLocators } from '../../../shared/selectors/consolidation/AccountSearch.locators';
+import { AccountResultsLocators } from '../../../shared/selectors/consolidation/AccountResults.locators';
+import { FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/mocks/fines-con-search-result-defendant-accounts-company-formatting.mock';
+import { FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/mocks/fines-con-search-result-defendant-accounts-formatting.mock';
+import { IFinesConSearchResultDefendantAccount } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/interfaces/fines-con-search-result-defendant-account.interface';
+import { setupConsolidationComponent as mountConsolidationComponent } from './setup/SetupComponent';
+import { IComponentProperties } from './setup/setupComponent.interface';
+import {
+ createCompanyFalseyResult,
+ createCompanyMaxResultsMock,
+ createCompanyTooManyResultsMock,
+ createCompanyMultipleErrorsAndWarningsResult,
+ createCompanyMultipleWarningsResult,
+ createFalseyResult,
+ createMaxResultsMock,
+ createTooManyResultsMock,
+ createMultipleErrorsAndWarningsResult,
+ createMultipleWarningsResult,
+} from './mocks/account_results_mock';
+
+const CONSOLIDATION_JIRA_LABEL = '@JIRA-LABEL:consolidation';
+const CONSOLIDATION_EPIC_TAG = '@JIRA-STORY:PO-2294';
+const INDIVIDUAL_STORY_TAG = '@JIRA-STORY:PO-2415';
+const COMPANY_STORY_TAG = '@JIRA-STORY:PO-2421';
+const RESULTS_TAB_FUNCTIONALITY_STORY_TAG = '@JIRA-STORY:PO-2416';
+const INVALID_RESULTS_STORY_TAG = '@JIRA-STORY:PO-2420';
+const EM_DASH = '—';
+const individualResultsTableHeaders = [
+ 'Account',
+ 'Name',
+ 'Aliases',
+ 'Date of birth',
+ 'Address line 1',
+ 'Postcode',
+ 'CO',
+ 'ENF',
+ 'Balance',
+ 'P/G',
+ 'NI number',
+ 'Ref',
+];
+const companyResultsTableHeaders = [
+ 'Account',
+ 'Name',
+ 'Aliases',
+ 'Address line 1',
+ 'Postcode',
+ 'CO',
+ 'ENF',
+ 'Balance',
+ 'Ref',
+];
+
+const buildTags = (...tags: string[]): string[] => [...tags, CONSOLIDATION_JIRA_LABEL];
+const buildIndividualTags = (...tags: string[]): string[] =>
+ buildTags(CONSOLIDATION_EPIC_TAG, INDIVIDUAL_STORY_TAG, ...tags);
+const buildCompanyTags = (...tags: string[]): string[] => buildTags(CONSOLIDATION_EPIC_TAG, COMPANY_STORY_TAG, ...tags);
+const buildResultsTabFunctionalityTags = (...tags: string[]): string[] =>
+ buildTags(CONSOLIDATION_EPIC_TAG, RESULTS_TAB_FUNCTIONALITY_STORY_TAG, ...tags);
+const buildInvalidResultsTags = (...tags: string[]): string[] =>
+ buildTags(CONSOLIDATION_EPIC_TAG, INVALID_RESULTS_STORY_TAG, ...tags);
+const normaliseText = (value: string): string => value.replace(/\s+/g, ' ').trim();
+type ExpectedResultsOrderRow = {
+ account: string;
+ name: string;
+ dateOfBirth?: string;
+};
+
+describe('FinesConConsolidateAccComponent - Account Results', () => {
+ let defendantAccountResults: IFinesConSearchResultDefendantAccount[] = structuredClone(
+ FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK,
+ );
+
+ const defaultComponentProperties: IComponentProperties = {
+ defendantType: 'individual',
+ fragments: 'results',
+ };
+
+ const setupComponent = (componentProperties: IComponentProperties = {}) => {
+ return mountConsolidationComponent({
+ ...defaultComponentProperties,
+ ...componentProperties,
+ initialResults: defendantAccountResults,
+ });
+ };
+
+ const assertResultsTabSummary = (defendantType: 'Individual' | 'Company' = 'Individual') => {
+ cy.get(AccountSearchLocators.heading).should('contain', 'Consolidate accounts');
+ cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business unit');
+ cy.get(AccountSearchLocators.businessUnitValue).should('contain', 'Historical Debt');
+ cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant type');
+ cy.get(AccountSearchLocators.defendantTypeValue).should('contain', defendantType);
+ cy.get(AccountSearchLocators.resultsTab).should('have.attr', 'aria-current', 'page');
+ };
+
+ const assertResultsSummary = (defendantType: 'Individual' | 'Company' = 'Individual') => {
+ assertResultsTabSummary(defendantType);
+ cy.get(AccountResultsLocators.resultsTable).should('be.visible');
+ };
+
+ const assertNoMatchingResultsState = (defendantType: 'Individual' | 'Company' = 'Individual') => {
+ assertResultsTabSummary(defendantType);
+ cy.get(AccountResultsLocators.resultsTable).should('not.exist');
+ cy.get(AccountResultsLocators.invalidResultsHeading).should('contain', 'There are no matching results.');
+ cy.get(AccountResultsLocators.invalidResultsBody)
+ .invoke('text')
+ .then((text) => {
+ expect(normaliseText(text)).to.equal('Check your search and try again.');
+ });
+ cy.get(AccountResultsLocators.invalidResultsLink).should('contain', 'Check your search');
+ };
+
+ const assertTooManyResultsState = (defendantType: 'Individual' | 'Company' = 'Individual') => {
+ assertResultsTabSummary(defendantType);
+ cy.get(AccountResultsLocators.resultsTable).should('not.exist');
+ cy.get(AccountResultsLocators.invalidResultsHeading).should('contain', 'There are more than 100 results.');
+ cy.get(AccountResultsLocators.invalidResultsBody)
+ .invoke('text')
+ .then((text) => {
+ expect(normaliseText(text)).to.equal('Try adding more information to your search.');
+ });
+ cy.get(AccountResultsLocators.invalidResultsLink).should('contain', 'Try adding more information');
+ };
+
+ const assertRowCellText = (accountNumber: string, cellSelector: string, expectedText: string) => {
+ cy.get(AccountResultsLocators.resultRowWithAccount(accountNumber))
+ .find(cellSelector)
+ .should(($cell) => {
+ expect(normaliseText($cell.text())).to.equal(expectedText);
+ });
+ };
+
+ const assertDisplayedResultsOrder = (expectedRows: ExpectedResultsOrderRow[]) => {
+ cy.get(AccountResultsLocators.resultAccountLink)
+ .should('have.length', expectedRows.length)
+ .then(($accountLinks) => {
+ const actualRows = [...$accountLinks].map((accountLink) => {
+ const row = Cypress.$(accountLink).closest('tr');
+ const actualRow: ExpectedResultsOrderRow = {
+ account: normaliseText(accountLink.textContent ?? ''),
+ name: normaliseText(row.find(AccountResultsLocators.resultNameCell).text()),
+ };
+
+ if ('dateOfBirth' in expectedRows[0]) {
+ actualRow.dateOfBirth = normaliseText(row.find(AccountResultsLocators.resultDateOfBirthCell).text());
+ }
+
+ return actualRow;
+ });
+
+ expect(actualRows).to.deep.equal(expectedRows);
+ });
+ };
+
+ const buildIndividualResult = (
+ overrides: Partial,
+ ): IFinesConSearchResultDefendantAccount => ({
+ ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK[0]),
+ aliases: null,
+ checks: {
+ errors: [],
+ warnings: [],
+ },
+ ...overrides,
+ });
+
+ const buildCompanyResult = (
+ overrides: Partial,
+ ): IFinesConSearchResultDefendantAccount => ({
+ ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK[0]),
+ aliases: null,
+ checks: {
+ errors: [],
+ warnings: [],
+ },
+ ...overrides,
+ });
+
+ describe('Individual tests', () => {
+ beforeEach(() => {
+ defendantAccountResults = structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK);
+ });
+
+ it(
+ 'AC1, AC1a, AC1b. should render the individual account results tab with populated mock data',
+ { tags: buildIndividualTags() },
+ () => {
+ setupComponent();
+
+ // AC1, AC1a, AC1b. Results tab renders with the selected business unit and defendant type.
+ cy.get(AccountSearchLocators.heading).should('contain', 'Consolidate accounts');
+ cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business unit');
+ cy.get(AccountSearchLocators.businessUnitValue).should('contain', 'Historical Debt');
+ cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant type');
+ cy.get(AccountSearchLocators.defendantTypeValue).should('contain', 'Individual');
+ cy.get(AccountSearchLocators.resultsTab).should('have.attr', 'aria-current', 'page');
+
+ // AC1. Results tab content renders with the results table and actions.
+ cy.get(AccountResultsLocators.resultsHeading).should('contain', 'Select accounts to consolidate');
+ cy.get(AccountResultsLocators.addToListButton).should('contain', 'Add to list');
+ cy.get(AccountResultsLocators.selectedAccountsHint).should('be.visible');
+ cy.get(AccountResultsLocators.resultsTable).should('be.visible');
+ cy.get(AccountResultsLocators.resultAccountLinkByNumber('ACC001')).should('be.visible');
+ cy.get(AccountResultsLocators.resultRowWithAccount('ACC001'))
+ .find(AccountResultsLocators.resultNameCell)
+ .should('contain', 'SMITH, John James');
+ },
+ );
+
+ it(
+ 'AC2, AC2a, AC5a, AC5b, AC5c, AC5d, AC5e, AC5f, AC5g, AC5h, AC5i. should display the individual results columns in the AC order and format populated data',
+ { tags: buildIndividualTags() },
+ () => {
+ defendantAccountResults[0].has_paying_parent_guardian = true; // Set to true to confirm Y is displayed in the relevant cell
+ defendantAccountResults[0].checks = { errors: [], warnings: [] }; //checks should be empty for check boxes to appear
+ setupComponent();
+
+ assertResultsSummary();
+ cy.get(AccountResultsLocators.resultSelectAllCheckbox).should('exist');
+ // AC2a. Results table displays the named columns in the required order.
+ cy.get(AccountResultsLocators.resultsTableNamedHeaders).then(($headers) => {
+ const headers = [...$headers].map((header) => normaliseText(header.textContent ?? ''));
+ expect(headers).to.deep.equal(individualResultsTableHeaders);
+ });
+
+ cy.get(AccountResultsLocators.resultAccountLinkByNumber('ACC001')).should('be.visible');
+ // AC5a. Name displays SURNAME, Forename.
+ assertRowCellText('ACC001', AccountResultsLocators.resultNameCell, 'SMITH, John James');
+ // AC5b. Aliases display in ascending alias order when one or more aliases exist.
+ assertRowCellText('ACC001', AccountResultsLocators.resultAliasesCell, 'ADAMS, Amy BAKER, Ben');
+ // AC5c. Date of birth displays as DD Mon YYYY.
+ assertRowCellText('ACC001', AccountResultsLocators.resultDateOfBirthCell, '03 Jan 1990');
+ assertRowCellText('ACC001', AccountResultsLocators.resultAddressLine1Cell, '1 Main Street');
+ assertRowCellText('ACC001', AccountResultsLocators.resultPostcodeCell, 'AB1 2CD');
+ // AC5d. CO displays Y when collection order is true.
+ assertRowCellText('ACC001', AccountResultsLocators.resultCollectionOrderCell, 'Y');
+ // AC5e. ENF displays the most recent enforcement action code.
+ assertRowCellText('ACC001', AccountResultsLocators.resultEnforcementCell, 'DISTRESS');
+ // AC5f. Balance displays with a pound sign and currency formatting.
+ assertRowCellText('ACC001', AccountResultsLocators.resultBalanceCell, '£120.50');
+ // AC5g. P/G displays Y when a paying parent or guardian exists.
+ assertRowCellText('ACC001', AccountResultsLocators.resultPayingParentGuardianCell, 'Y');
+ // AC5h. NI number displays in the standard formatted layout.
+ assertRowCellText('ACC001', AccountResultsLocators.resultNationalInsuranceNumberCell, 'QQ 12 34 56 C');
+ // AC5i. Ref displays the prosecutor case reference when present.
+ assertRowCellText('ACC001', AccountResultsLocators.resultRefCell, 'REF-1');
+ },
+ );
+
+ it(
+ 'AC2b, AC2c, AC5b, AC5d, AC5fi, AC5g. should display an em dash for optional or unavailable account data',
+ { tags: buildIndividualTags() },
+ () => {
+ defendantAccountResults.push(createFalseyResult());
+
+ setupComponent();
+
+ assertResultsSummary();
+ cy.get(AccountResultsLocators.resultAccountLinkByNumber('ACC002')).should('be.visible');
+ cy.get(AccountResultsLocators.resultRowWithAccount('ACC002'))
+ .find(AccountResultsLocators.resultNameCell)
+ .should('contain', EM_DASH);
+ // AC5b. Aliases display only when aliases exist; otherwise the no-data marker is shown.
+ assertRowCellText('ACC002', AccountResultsLocators.resultAliasesCell, EM_DASH);
+ assertRowCellText('ACC002', AccountResultsLocators.resultDateOfBirthCell, EM_DASH);
+ assertRowCellText('ACC002', AccountResultsLocators.resultAddressLine1Cell, EM_DASH);
+ assertRowCellText('ACC002', AccountResultsLocators.resultPostcodeCell, EM_DASH);
+ // AC5d. CO displays an '-' when collection order is false.
+ assertRowCellText('ACC002', AccountResultsLocators.resultCollectionOrderCell, '-');
+ assertRowCellText('ACC002', AccountResultsLocators.resultEnforcementCell, EM_DASH);
+ assertRowCellText('ACC002', AccountResultsLocators.resultBalanceCell, EM_DASH);
+ // AC5g. P/G displays an '-' when there is no paying parent or guardian.
+ assertRowCellText('ACC002', AccountResultsLocators.resultPayingParentGuardianCell, '-');
+ assertRowCellText('ACC002', AccountResultsLocators.resultNationalInsuranceNumberCell, EM_DASH);
+ assertRowCellText('ACC002', AccountResultsLocators.resultRefCell, EM_DASH);
+ },
+ );
+
+ it(
+ 'AC2d, AC2e. should display a maximum of 100 accounts on a single scrollable page with no pagination',
+ { tags: buildIndividualTags() },
+ () => {
+ defendantAccountResults = createMaxResultsMock();
+
+ setupComponent();
+
+ assertResultsSummary();
+ // AC2e. Results are displayed on a single scrollable page.
+ cy.get(AccountResultsLocators.resultsScrollPane).should('exist');
+ // AC2e. No pagination is displayed.
+ cy.get(AccountResultsLocators.resultsPagination).should('not.exist');
+ // AC2d. A maximum of 100 accounts are displayed per search.
+ cy.get(AccountResultsLocators.resultAccountLink).should('have.length', 100);
+ cy.get(AccountResultsLocators.resultAccountLinkByNumber('ACC100')).should('be.visible');
+ },
+ );
+
+ it(
+ 'AC3. should display individual results in Name, Date of birth, then Account number ascending order',
+ { tags: buildIndividualTags() },
+ () => {
+ defendantAccountResults = [
+ buildIndividualResult({
+ defendant_account_id: 14,
+ account_number: 'ACC003',
+ defendant_surname: 'Resultlink',
+ defendant_firstnames: 'Aaron',
+ birth_date: '2003-05-15',
+ }),
+ buildIndividualResult({
+ defendant_account_id: 11,
+ account_number: 'ACC001',
+ defendant_surname: 'Resultlink',
+ defendant_firstnames: 'Consolidation',
+ birth_date: '2001-05-15',
+ }),
+ buildIndividualResult({
+ defendant_account_id: 12,
+ account_number: 'ACC002',
+ defendant_surname: 'Resultlink',
+ defendant_firstnames: 'Consolidation',
+ birth_date: '2001-05-15',
+ }),
+ buildIndividualResult({
+ defendant_account_id: 13,
+ account_number: 'ACC004',
+ defendant_surname: 'Resultlink',
+ defendant_firstnames: 'Consolidation',
+ birth_date: '2002-05-15',
+ }),
+ ];
+
+ setupComponent();
+
+ assertResultsSummary();
+ assertDisplayedResultsOrder([
+ { account: 'ACC003', name: 'RESULTLINK, Aaron', dateOfBirth: '15 May 2003' },
+ { account: 'ACC001', name: 'RESULTLINK, Consolidation', dateOfBirth: '15 May 2001' },
+ { account: 'ACC002', name: 'RESULTLINK, Consolidation', dateOfBirth: '15 May 2001' },
+ { account: 'ACC004', name: 'RESULTLINK, Consolidation', dateOfBirth: '15 May 2002' },
+ ]);
+ },
+ );
+
+ it(
+ 'AC1a, AC1b, AC3, AC3a, AC3b, AC3c. should display the individual over-100 results state with the try adding more information link',
+ { tags: buildInvalidResultsTags() },
+ () => {
+ defendantAccountResults = createTooManyResultsMock();
+
+ setupComponent();
+ // AC1a, AC1b. The Business unit row displays the Business Unit used in the search The Defendant type row displays the defendant type used in the search.
+ assertResultsTabSummary();
+
+ assertTooManyResultsState();
+ },
+ );
+
+ it(
+ 'AC1a, AC1b, AC2, AC2a, AC2b, AC2c. should display the individual no-results state with the check your search link',
+ { tags: buildInvalidResultsTags() },
+ () => {
+ defendantAccountResults = [];
+
+ setupComponent();
+ // AC1a, AC1b. The Business unit row displays the Business Unit used in the search The Defendant type row displays the defendant type used in the search.
+ assertResultsTabSummary();
+
+ assertNoMatchingResultsState();
+ },
+ );
+
+ it(
+ 'AC7. should display warning and error checks beneath the relevant account row',
+ { tags: buildIndividualTags() },
+ () => {
+ setupComponent();
+
+ assertResultsSummary();
+ // AC7. Checks are displayed beneath the relevant account row.
+ cy.get(AccountResultsLocators.resultRowWithAccount('ACC001'))
+ .next(AccountResultsLocators.resultTableRow)
+ .find(AccountResultsLocators.resultChecksCellByAccountId(11))
+ .should('be.visible')
+ .and('contain', 'Account has days in default');
+ },
+ );
+
+ it(
+ 'AC7a, AC7b. should show only errors when both errors and warnings exist, listing multiple errors as bullets',
+ { tags: buildIndividualTags() },
+ () => {
+ defendantAccountResults = [createMultipleErrorsAndWarningsResult()];
+
+ setupComponent();
+
+ assertResultsSummary();
+ // AC7a. Only errors are displayed when both errors and warnings exist.
+ cy.get(AccountResultsLocators.resultRowWithAccount('ACC005'))
+ .next(AccountResultsLocators.resultTableRow)
+ .find(AccountResultsLocators.resultChecksCellByAccountId(15))
+ .should('contain', 'Account status is CS')
+ .and('contain', 'Account is blocked for consolidation')
+ .and('not.contain', 'Account has uncleared cheque payments')
+ .and('not.contain', 'Account has linked cases');
+ // AC7b. Multiple errors are displayed as bullet points.
+ cy.get(AccountResultsLocators.resultChecksCellByAccountId(15))
+ .find(AccountResultsLocators.resultChecksBulletItems)
+ .should('have.length', 2);
+ },
+ );
+
+ it(
+ 'AC7c. should display all warnings when multiple warnings apply and no errors exist',
+ { tags: buildIndividualTags() },
+ () => {
+ defendantAccountResults = [createMultipleWarningsResult()];
+
+ setupComponent();
+
+ assertResultsSummary();
+ // AC7c. Multiple warnings are displayed when no errors apply.
+ cy.get(AccountResultsLocators.resultRowWithAccount('ACC006'))
+ .next(AccountResultsLocators.resultTableRow)
+ .find(AccountResultsLocators.resultChecksCellByAccountId(16))
+ .should('contain', 'Account has uncleared cheque payments')
+ .and('contain', 'Account has linked cases');
+ cy.get(AccountResultsLocators.resultChecksCellByAccountId(16))
+ .find(AccountResultsLocators.resultChecksBulletItems)
+ .should('have.length', 2);
+ },
+ );
+ });
+
+ describe('Company tests', () => {
+ beforeEach(() => {
+ defendantAccountResults = structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK);
+ });
+
+ it(
+ 'AC1, AC1a, AC1b. should render the company account results tab with populated mock data',
+ { tags: buildCompanyTags() },
+ () => {
+ setupComponent({ defendantType: 'company' });
+
+ cy.get(AccountSearchLocators.heading).should('contain', 'Consolidate accounts');
+ cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business unit');
+ cy.get(AccountSearchLocators.businessUnitValue).should('contain', 'Historical Debt');
+ cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant type');
+ cy.get(AccountSearchLocators.defendantTypeValue).should('contain', 'Company');
+ cy.get(AccountSearchLocators.resultsTab).should('have.attr', 'aria-current', 'page');
+
+ cy.get(AccountResultsLocators.resultsHeading).should('contain', 'Select accounts to consolidate');
+ cy.get(AccountResultsLocators.addToListButton).should('contain', 'Add to list');
+ cy.get(AccountResultsLocators.selectedAccountsHint).should('be.visible');
+ cy.get(AccountResultsLocators.resultsTable).should('be.visible');
+ cy.get(AccountResultsLocators.resultAccountLinkByNumber('COMP001')).should('be.visible');
+ cy.get(AccountResultsLocators.resultRowWithAccount('COMP001'))
+ .find(AccountResultsLocators.resultNameCell)
+ .should('contain', 'Acme Corporation');
+ },
+ );
+
+ it(
+ 'AC2, AC2a, AC5a, AC5b, AC5d, AC5e, AC5f, AC5i. should display the company results columns in the AC order and format populated data',
+ { tags: buildCompanyTags() },
+ () => {
+ setupComponent({ defendantType: 'company' });
+
+ assertResultsSummary('Company');
+ cy.get(AccountResultsLocators.resultSelectAllCheckbox).should('exist');
+ cy.get(AccountResultsLocators.resultsTableNamedHeaders).then(($headers) => {
+ const headers = [...$headers].map((header) => normaliseText(header.textContent ?? ''));
+ expect(headers).to.deep.equal(companyResultsTableHeaders);
+ });
+
+ cy.get(AccountResultsLocators.resultAccountLinkByNumber('COMP001')).should('be.visible');
+ assertRowCellText('COMP001', AccountResultsLocators.resultNameCell, 'Acme Corporation');
+ assertRowCellText('COMP001', AccountResultsLocators.resultAliasesCell, 'Alpha Ltd Bravo Ltd');
+ assertRowCellText('COMP001', AccountResultsLocators.resultAddressLine1Cell, '21 Company Street');
+ assertRowCellText('COMP001', AccountResultsLocators.resultPostcodeCell, 'CO1 2MP');
+ assertRowCellText('COMP001', AccountResultsLocators.resultCollectionOrderCell, 'Y');
+ assertRowCellText('COMP001', AccountResultsLocators.resultEnforcementCell, 'DISTRESS');
+ assertRowCellText('COMP001', AccountResultsLocators.resultBalanceCell, '£520.50');
+ assertRowCellText('COMP001', AccountResultsLocators.resultRefCell, 'COMP-REF-1');
+ cy.get(AccountResultsLocators.resultRowWithAccount('COMP001'))
+ .find(AccountResultsLocators.resultDateOfBirthCell)
+ .should('not.exist');
+ cy.get(AccountResultsLocators.resultRowWithAccount('COMP001'))
+ .find(AccountResultsLocators.resultPayingParentGuardianCell)
+ .should('not.exist');
+ cy.get(AccountResultsLocators.resultRowWithAccount('COMP001'))
+ .find(AccountResultsLocators.resultNationalInsuranceNumberCell)
+ .should('not.exist');
+ },
+ );
+
+ it(
+ 'AC2b, AC2c, AC5b, AC5d, AC5fi. should display an em dash for unavailable company account data',
+ { tags: buildCompanyTags() },
+ () => {
+ defendantAccountResults.push(createCompanyFalseyResult());
+
+ setupComponent({ defendantType: 'company' });
+
+ assertResultsSummary('Company');
+ cy.get(AccountResultsLocators.resultAccountLinkByNumber('COMP002')).should('be.visible');
+ cy.get(AccountResultsLocators.resultRowWithAccount('COMP002'))
+ .find(AccountResultsLocators.resultNameCell)
+ .should('contain', EM_DASH);
+ assertRowCellText('COMP002', AccountResultsLocators.resultAliasesCell, EM_DASH);
+ assertRowCellText('COMP002', AccountResultsLocators.resultAddressLine1Cell, EM_DASH);
+ assertRowCellText('COMP002', AccountResultsLocators.resultPostcodeCell, EM_DASH);
+ assertRowCellText('COMP002', AccountResultsLocators.resultCollectionOrderCell, '-');
+ assertRowCellText('COMP002', AccountResultsLocators.resultEnforcementCell, EM_DASH);
+ assertRowCellText('COMP002', AccountResultsLocators.resultBalanceCell, EM_DASH);
+ assertRowCellText('COMP002', AccountResultsLocators.resultRefCell, EM_DASH);
+ },
+ );
+
+ it(
+ 'AC2d, AC2e. should display a maximum of 100 company accounts on a single scrollable page with no pagination',
+ { tags: buildCompanyTags() },
+ () => {
+ defendantAccountResults = createCompanyMaxResultsMock();
+
+ setupComponent({ defendantType: 'company' });
+
+ assertResultsSummary('Company');
+ cy.get(AccountResultsLocators.resultsScrollPane).should('exist');
+ cy.get(AccountResultsLocators.resultsPagination).should('not.exist');
+ cy.get(AccountResultsLocators.resultAccountLink).should('have.length', 100);
+ cy.get(AccountResultsLocators.resultAccountLinkByNumber('COMP100')).should('be.visible');
+ },
+ );
+
+ it(
+ 'AC3. should display company results in Name, then Account number ascending order',
+ { tags: buildCompanyTags() },
+ () => {
+ defendantAccountResults = [
+ buildCompanyResult({
+ defendant_account_id: 24,
+ account_number: 'COMP003',
+ organisation_name: 'Alpha Holdings',
+ }),
+ buildCompanyResult({
+ defendant_account_id: 21,
+ account_number: 'COMP001',
+ organisation_name: 'Beta Holdings',
+ }),
+ buildCompanyResult({
+ defendant_account_id: 22,
+ account_number: 'COMP002',
+ organisation_name: 'Beta Holdings',
+ }),
+ buildCompanyResult({
+ defendant_account_id: 23,
+ account_number: 'COMP004',
+ organisation_name: 'Gamma Holdings',
+ }),
+ ];
+
+ setupComponent({ defendantType: 'company' });
+
+ assertResultsSummary('Company');
+ assertDisplayedResultsOrder([
+ { account: 'COMP003', name: 'Alpha Holdings' },
+ { account: 'COMP001', name: 'Beta Holdings' },
+ { account: 'COMP002', name: 'Beta Holdings' },
+ { account: 'COMP004', name: 'Gamma Holdings' },
+ ]);
+ },
+ );
+
+ it(
+ 'AC1a, AC1b, AC3, AC3a, AC3b, AC3c. should display the company over-100 results state with the try adding more information link',
+ { tags: buildInvalidResultsTags() },
+ () => {
+ defendantAccountResults = createCompanyTooManyResultsMock();
+
+ setupComponent({ defendantType: 'company' });
+
+ // AC1a, AC1b. The Business unit row displays the Business Unit used in the search The Defendant type row displays the defendant type used in the search.
+ assertResultsTabSummary('Company');
+ assertTooManyResultsState('Company');
+ },
+ );
+
+ it(
+ 'AC1a, AC1b, AC2, AC2a, AC2b, AC2c. should display the company no-results state with the check your search link',
+ { tags: buildInvalidResultsTags() },
+ () => {
+ defendantAccountResults = [];
+
+ setupComponent({ defendantType: 'company' });
+
+ // AC1a, AC1b. The Business unit row displays the Business Unit used in the search The Defendant type row displays the defendant type used in the search.
+ assertResultsTabSummary('Company');
+ assertNoMatchingResultsState('Company');
+ },
+ );
+
+ it(
+ 'AC7. should display warning and error checks beneath the relevant company account row',
+ { tags: buildCompanyTags() },
+ () => {
+ defendantAccountResults[0].checks = {
+ errors: [{ reference: 'CON.ER.4', message: 'Account has days in default' }],
+ warnings: [],
+ };
+
+ setupComponent({ defendantType: 'company' });
+
+ assertResultsSummary('Company');
+ cy.get(AccountResultsLocators.resultRowWithAccount('COMP001'))
+ .next(AccountResultsLocators.resultTableRow)
+ .find(AccountResultsLocators.resultChecksCellByAccountId(21))
+ .should('be.visible')
+ .and('contain', 'Account has days in default');
+ },
+ );
+
+ it(
+ 'AC7a, AC7b. should show only errors for company results when both errors and warnings exist, listing multiple errors as bullets',
+ { tags: buildCompanyTags() },
+ () => {
+ defendantAccountResults = [createCompanyMultipleErrorsAndWarningsResult()];
+
+ setupComponent({ defendantType: 'company' });
+
+ assertResultsSummary('Company');
+ cy.get(AccountResultsLocators.resultRowWithAccount('COMP005'))
+ .next(AccountResultsLocators.resultTableRow)
+ .find(AccountResultsLocators.resultChecksCellByAccountId(25))
+ .should('contain', 'Account status is CS')
+ .and('contain', 'Account is blocked for consolidation')
+ .and('not.contain', 'Account has uncleared cheque payments')
+ .and('not.contain', 'Account has linked cases');
+ cy.get(AccountResultsLocators.resultChecksCellByAccountId(25))
+ .find(AccountResultsLocators.resultChecksBulletItems)
+ .should('have.length', 2);
+ },
+ );
+
+ it(
+ 'AC7c. should display all warnings for company results when multiple warnings apply and no errors exist',
+ { tags: buildCompanyTags() },
+ () => {
+ defendantAccountResults = [createCompanyMultipleWarningsResult()];
+
+ setupComponent({ defendantType: 'company' });
+
+ assertResultsSummary('Company');
+ cy.get(AccountResultsLocators.resultRowWithAccount('COMP006'))
+ .next(AccountResultsLocators.resultTableRow)
+ .find(AccountResultsLocators.resultChecksCellByAccountId(26))
+ .should('contain', 'Account has uncleared cheque payments')
+ .and('contain', 'Account has linked cases');
+ cy.get(AccountResultsLocators.resultChecksCellByAccountId(26))
+ .find(AccountResultsLocators.resultChecksBulletItems)
+ .should('have.length', 2);
+ },
+ );
+ });
+
+ describe('Results tab functionality tests', () => {
+ beforeEach(() => {
+ defendantAccountResults = structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK);
+ });
+
+ it(
+ 'AC3, AC3a, AC3b. should show row checkboxes for selectable accounts, hide them for errors, and keep warning rows enabled',
+ { tags: buildResultsTabFunctionalityTags() },
+ () => {
+ defendantAccountResults[0].checks = { errors: [], warnings: [] };
+ defendantAccountResults.push(createMultipleErrorsAndWarningsResult(), createMultipleWarningsResult());
+
+ setupComponent();
+
+ assertResultsSummary();
+
+ // AC3. Each row includes a checkbox for selecting or unselecting the account.
+ cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(11))
+ .should('exist')
+ .and('be.enabled')
+ .and('not.be.checked')
+ .check({ force: true })
+ .should('be.checked');
+ cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(11))
+ .uncheck({ force: true })
+ .should('not.be.checked');
+
+ // AC3a. Accounts that contain one or more errors have their checkbox hidden.
+ cy.get(AccountResultsLocators.resultRowWithAccount('ACC005'))
+ .find(AccountResultsLocators.resultRowCheckboxByAccountId(15))
+ .should('not.exist');
+
+ // AC3b. Accounts that contain warnings keep their checkbox enabled.
+ cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(16)).should('exist').and('be.enabled');
+ },
+ );
+
+ it(
+ 'AC4, AC4a, AC4b, AC4c, AC5a, AC5b, AC5c. should bulk select and deselect all enabled accounts while excluding accounts with errors',
+ { tags: buildResultsTabFunctionalityTags() },
+ () => {
+ defendantAccountResults[0].checks = { errors: [], warnings: [] };
+ defendantAccountResults.push(createMultipleErrorsAndWarningsResult(), createMultipleWarningsResult());
+
+ setupComponent();
+
+ assertResultsSummary();
+
+ // AC4. A top-level checkbox is displayed above the table to allow bulk selection.
+ // AC5a, AC5b. The dynamic counter shows selected accounts against the total returned results.
+ cy.get(AccountResultsLocators.selectedAccountsHint).should('contain', '0 of 3 accounts selected');
+
+ // AC4a. Selecting the top-level checkbox selects all enabled accounts in the results.
+ cy.get(AccountResultsLocators.resultSelectAllCheckbox)
+ .should('exist')
+ .and('not.be.checked')
+ .check({ force: true })
+ .should('be.checked');
+
+ // AC4b. Accounts with one or more errors are not selected.
+ // AC5c. The counter updates automatically as accounts are selected.
+ cy.get(AccountResultsLocators.selectedAccountsHint).should('contain', '2 of 3 accounts selected');
+ cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(11)).should('be.checked');
+ cy.get(AccountResultsLocators.resultRowWithAccount('ACC005'))
+ .find(AccountResultsLocators.resultRowCheckboxByAccountId(15))
+ .should('not.exist');
+ cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(16)).should('be.checked');
+
+ // AC4c. Deselecting the top-level checkbox deselects all currently selected accounts.
+ // AC5c. The counter updates automatically as accounts are deselected.
+ cy.get(AccountResultsLocators.resultSelectAllCheckbox).uncheck({ force: true }).should('not.be.checked');
+
+ cy.get(AccountResultsLocators.selectedAccountsHint).should('contain', '0 of 3 accounts selected');
+ cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(11)).should('not.be.checked');
+ cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(16)).should('not.be.checked');
+ },
+ );
+
+ it(
+ 'AC6, AC6a, AC6b. should display Add to list above the counter and show a validation error when no accounts are selected',
+ { tags: buildResultsTabFunctionalityTags() },
+ () => {
+ defendantAccountResults[0].checks = { errors: [], warnings: [] };
+
+ setupComponent();
+
+ assertResultsSummary();
+
+ // AC6. An Add to list button is displayed above the counter.
+ cy.get(AccountResultsLocators.addToListButton)
+ .should('be.visible')
+ .and('contain', 'Add to list')
+ .then(($button) => {
+ cy.get(AccountResultsLocators.selectedAccountsHint).then(($hint) => {
+ expect($button[0].compareDocumentPosition($hint[0]) & Node.DOCUMENT_POSITION_FOLLOWING).to.not.equal(0);
+ });
+ });
+
+ // AC6a, AC6b. Selecting Add to list validates the selected accounts and shows an error when none are selected.
+ cy.get(AccountResultsLocators.addToListButton).click();
+
+ cy.get(AccountSearchLocators.errorSummary)
+ .should('be.visible')
+ .and('contain', 'Select 1 or more accounts to consolidate.');
+ cy.get(AccountResultsLocators.addToListErrorMessage)
+ .should('be.visible')
+ .and('contain', 'Select 1 or more accounts to consolidate.');
+ },
+ );
+ });
+});
diff --git a/cypress/component/fines/consolidation/AccountSearch.cy.ts b/cypress/component/fines/consolidation/AccountSearch.cy.ts
index 206f21ed3d..76d204c307 100644
--- a/cypress/component/fines/consolidation/AccountSearch.cy.ts
+++ b/cypress/component/fines/consolidation/AccountSearch.cy.ts
@@ -1,6 +1,7 @@
import { AccountSearchLocators } from '../../../shared/selectors/consolidation/AccountSearch.locators';
import { FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/mocks/fines-con-search-account-form-empty.mock';
import { IFinesConSearchAccountState } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/interfaces/fines-con-search-account-state.interface';
+import { FINES_CON_ROUTING_PATHS } from 'src/app/flows/fines/fines-con/routing/constants/fines-con-routing-paths.constant';
import { setupConsolidationComponent as mountConsolidationComponent } from './setup/SetupComponent';
import { ConsolidationTabFragment, IComponentProperties } from './setup/setupComponent.interface';
@@ -35,6 +36,36 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
cy.get(inlineSelector).should('be.visible').and('contain', message);
};
+ const assertSearchErrorRedirect = (expectedFormData: Partial) => {
+ cy.get('@routerNavigate').should('have.been.calledWithMatch', [FINES_CON_ROUTING_PATHS.children.searchError], {
+ relativeTo: {},
+ });
+
+ cy.get('@finesConStore').then((store: any) => {
+ const searchAccountForm = store.searchAccountForm();
+ expect(searchAccountForm).to.deep.include(expectedFormData);
+ });
+ };
+
+ const assertNoSearchUpdate = (updateSearchSpy: sinon.SinonSpy) => {
+ cy.then(() => {
+ expect(updateSearchSpy).to.not.have.been.called;
+ });
+ };
+
+ const findSubmittedFormData = (
+ updateSearchSpy: sinon.SinonSpy,
+ predicate: (formData: IFinesConSearchAccountState) => boolean,
+ ): IFinesConSearchAccountState => {
+ const matchingCall = updateSearchSpy
+ .getCalls()
+ .map((call) => call.args[0] as IFinesConSearchAccountState)
+ .find(predicate);
+
+ expect(matchingCall, 'matching search payload').to.exist;
+ return matchingCall!;
+ };
+
const switchToTab = (tab: ConsolidationTabFragment) => {
cy.get('@finesConStore').then((store: any) => {
store.setActiveTab(tab);
@@ -69,18 +100,18 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
//AC1a. Business unit displays the selected BU and is read-only'
- cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business Unit');
+ cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business unit');
cy.get(AccountSearchLocators.businessUnitValue).should('contain', 'Historical Debt');
//AC1b. Defendant type displays 'Individual'
- cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant Type');
+ cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant type');
cy.get(AccountSearchLocators.defendantTypeValue).should('contain', 'Individual');
//AC1c. Search screen mirrors expected field types, headings and actions
cy.get(AccountSearchLocators.tabsNav).should('be.visible');
cy.get(AccountSearchLocators.searchTab).should('contain', 'Search');
cy.get(AccountSearchLocators.resultsTab).should('contain', 'Results');
- cy.get(AccountSearchLocators.forConsolidationTab).should('contain', 'For Consolidation');
+ cy.get(AccountSearchLocators.forConsolidationTab).should('contain', 'For consolidation');
cy.get(AccountSearchLocators.quickSearchHeading).should('contain', 'Quick search');
cy.contains(AccountSearchLocators.advancedSearchHeading, 'Advanced Search').should('be.visible');
@@ -127,14 +158,63 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
);
it(
- 'AC3. Invalid search criteria display the expected errors and no search update occurs',
+ 'AC3a. Invalid account number format displays the expected error and no search update occurs',
+ { tags: buildTags('@JIRA-KEY:POT-3869') },
+ () => {
+ const updateSearchSpy = Cypress.sinon.spy();
+ finesConSearchAccountFormData.fcon_search_account_number = '1234567';
+
+ setupConsolidationComponent({ updateSearchSpy });
+ cy.get(AccountSearchLocators.searchButton).click();
+
+ assertValidationError(
+ 'Enter account number in the correct format such as 12345678 or 12345678A',
+ AccountSearchLocators.accountNumberError,
+ );
+ assertNoSearchUpdate(updateSearchSpy);
+ },
+ );
+
+ it(
+ 'AC3b. Invalid National Insurance number format displays the expected error and no search update occurs',
+ { tags: buildTags('@JIRA-KEY:POT-3869') },
+ () => {
+ const updateSearchSpy = Cypress.sinon.spy();
+ finesConSearchAccountFormData.fcon_search_account_national_insurance_number = 'AB12345$C';
+
+ setupConsolidationComponent({ updateSearchSpy });
+ cy.get(AccountSearchLocators.searchButton).click();
+
+ assertValidationError(
+ 'Enter a National Insurance number in the format AANNNNNNA',
+ AccountSearchLocators.nationalInsuranceNumberError,
+ );
+ assertNoSearchUpdate(updateSearchSpy);
+ },
+ );
+
+ it(
+ 'AC4a. Account number max length displays the expected error and no search update occurs',
+ { tags: buildTags('@JIRA-KEY:POT-3870') },
+ () => {
+ const updateSearchSpy = Cypress.sinon.spy();
+ finesConSearchAccountFormData.fcon_search_account_number = '123456789A';
+
+ setupConsolidationComponent({ updateSearchSpy });
+ cy.get(AccountSearchLocators.searchButton).click();
+
+ assertValidationError('Account number must be 9 characters or fewer', AccountSearchLocators.accountNumberError);
+ assertNoSearchUpdate(updateSearchSpy);
+ },
+ );
+
+ it(
+ 'AC3. Invalid advanced search criteria display the expected errors and no search update occurs',
{ tags: buildTags('@JIRA-KEY:POT-3869') },
() => {
const updateSearchSpy = Cypress.sinon.spy();
finesConSearchAccountFormData = {
...structuredClone(FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK.formData),
- fcon_search_account_number: '1234567',
- fcon_search_account_national_insurance_number: 'AB12345$C',
fcon_search_account_individuals_search_criteria: {
fcon_search_account_individuals_last_name: 'Smith',
fcon_search_account_individuals_last_name_exact_match: false,
@@ -150,14 +230,6 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
cy.get(AccountSearchLocators.searchButton).click();
const expectedValidationErrors = [
- {
- message: 'Enter account number in the correct format such as 12345678 or 12345678A',
- selector: AccountSearchLocators.accountNumberError,
- },
- {
- message: 'Enter a National Insurance number in the format AANNNNNNA',
- selector: AccountSearchLocators.nationalInsuranceNumberError,
- },
{
message: 'Date of birth must be in the format DD/MM/YYYY',
selector: AccountSearchLocators.dateOfBirthError,
@@ -176,21 +248,17 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
assertValidationError(message, selector);
});
- cy.then(() => {
- expect(updateSearchSpy).to.not.have.been.called;
- });
+ assertNoSearchUpdate(updateSearchSpy);
},
);
it(
- 'AC4. Max length validation errors display expected messages and no search update occurs',
+ 'AC4. Advanced search max length validation errors display expected messages and no search update occurs',
{ tags: buildTags('@JIRA-KEY:POT-3870') },
() => {
const updateSearchSpy = Cypress.sinon.spy();
finesConSearchAccountFormData = {
...structuredClone(FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK.formData),
- fcon_search_account_number: '123456789A',
- fcon_search_account_national_insurance_number: 'AB1234567C',
fcon_search_account_individuals_search_criteria: {
fcon_search_account_individuals_last_name: 'A'.repeat(31),
fcon_search_account_individuals_last_name_exact_match: false,
@@ -206,10 +274,6 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
cy.get(AccountSearchLocators.searchButton).click();
const expectedValidationErrors = [
- {
- message: 'Account number must be 9 characters or fewer',
- selector: AccountSearchLocators.accountNumberError,
- },
{
message: 'Last name must be 30 characters or fewer',
selector: AccountSearchLocators.lastNameError,
@@ -218,10 +282,6 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
message: 'First names must be 20 characters or fewer',
selector: AccountSearchLocators.firstNamesError,
},
- {
- message: 'Enter a National Insurance number in the format AANNNNNNA',
- selector: AccountSearchLocators.nationalInsuranceNumberError,
- },
{
message: 'Address line 1 must be 30 characters or fewer',
selector: AccountSearchLocators.addressLine1Error,
@@ -236,9 +296,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
assertValidationError(message, selector);
});
- cy.then(() => {
- expect(updateSearchSpy).to.not.have.been.called;
- });
+ assertNoSearchUpdate(updateSearchSpy);
},
);
@@ -254,9 +312,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
cy.get(AccountSearchLocators.searchButton).click();
assertValidationError('Enter last name', AccountSearchLocators.lastNameError);
- cy.then(() => {
- expect(updateSearchSpy).to.not.have.been.called;
- });
+ assertNoSearchUpdate(updateSearchSpy);
},
);
@@ -271,9 +327,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
cy.get(AccountSearchLocators.searchButton).click();
assertValidationError('Enter last name', AccountSearchLocators.lastNameError);
- cy.then(() => {
- expect(updateSearchSpy).to.not.have.been.called;
- });
+ assertNoSearchUpdate(updateSearchSpy);
},
);
@@ -287,9 +341,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
cy.get(AccountSearchLocators.searchButton).click();
assertValidationError('Enter last name', AccountSearchLocators.lastNameError);
- cy.then(() => {
- expect(updateSearchSpy).to.not.have.been.called;
- });
+ assertNoSearchUpdate(updateSearchSpy);
},
);
@@ -304,9 +356,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
cy.get(AccountSearchLocators.searchButton).click();
assertValidationError('Enter last name', AccountSearchLocators.lastNameError);
- cy.then(() => {
- expect(updateSearchSpy).to.not.have.been.called;
- });
+ assertNoSearchUpdate(updateSearchSpy);
},
);
@@ -321,8 +371,10 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
cy.get(AccountSearchLocators.searchButton).click();
cy.then(() => {
- expect(updateSearchSpy).to.have.been.calledOnce;
- const submittedFormData = updateSearchSpy.firstCall.args[0] as IFinesConSearchAccountState;
+ const submittedFormData = findSubmittedFormData(
+ updateSearchSpy,
+ (formData) => formData.fcon_search_account_number === '12345678',
+ );
expect(submittedFormData.fcon_search_account_number).to.equal('12345678');
expect(submittedFormData.fcon_search_account_national_insurance_number).to.be.null;
@@ -351,8 +403,10 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
cy.get(AccountSearchLocators.searchButton).click();
cy.then(() => {
- expect(updateSearchSpy).to.have.been.calledOnce;
- const submittedFormData = updateSearchSpy.firstCall.args[0] as IFinesConSearchAccountState;
+ const submittedFormData = findSubmittedFormData(
+ updateSearchSpy,
+ (formData) => formData.fcon_search_account_national_insurance_number === 'AB123456C',
+ );
expect(submittedFormData.fcon_search_account_national_insurance_number).to.equal('AB123456C');
expect(submittedFormData.fcon_search_account_number).to.be.null;
@@ -449,14 +503,14 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
//AC1a. Business unit displays the selected BU and is read-only'
- cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business Unit');
+ cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business unit');
cy.get(AccountSearchLocators.businessUnitValue)
.should('contain', 'Historical Debt')
.find('input, select, textarea')
.should('not.exist');
//AC1b. Defendant type displays 'Company'
- cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant Type');
+ cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant type');
cy.get(AccountSearchLocators.defendantTypeValue)
.should('contain', 'Company')
.find('input, select, textarea')
@@ -466,7 +520,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
cy.get(AccountSearchLocators.tabsNav).should('be.visible');
cy.get(AccountSearchLocators.searchTab).should('contain', 'Search');
cy.get(AccountSearchLocators.resultsTab).should('contain', 'Results');
- cy.get(AccountSearchLocators.forConsolidationTab).should('contain', 'For Consolidation');
+ cy.get(AccountSearchLocators.forConsolidationTab).should('contain', 'For consolidation');
cy.get(AccountSearchLocators.quickSearchHeading).should('contain', 'Quick search');
cy.contains(AccountSearchLocators.advancedSearchHeading, 'Advanced Search').should('be.visible');
@@ -509,13 +563,45 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
);
it(
- 'AC3. Invalid search criteria display the expected errors and no search update occurs',
+ 'AC3a. Invalid company account number format displays the expected error and no search update occurs',
+ { tags: buildTags('@JIRA-KEY:POT-3869') },
+ () => {
+ const updateSearchSpy = Cypress.sinon.spy();
+ finesConSearchAccountFormData.fcon_search_account_number = '1234567';
+
+ setupConsolidationComponent({ updateSearchSpy, defendantType: 'company' });
+ cy.get(AccountSearchLocators.searchButton).click();
+
+ assertValidationError(
+ 'Enter account number in the correct format such as 12345678 or 12345678A',
+ AccountSearchLocators.accountNumberError,
+ );
+ assertNoSearchUpdate(updateSearchSpy);
+ },
+ );
+
+ it(
+ 'AC4a. Company account number max length displays the expected error and no search update occurs',
+ { tags: buildTags('@JIRA-KEY:POT-3879') },
+ () => {
+ const updateSearchSpy = Cypress.sinon.spy();
+ finesConSearchAccountFormData.fcon_search_account_number = '123456789A';
+
+ setupConsolidationComponent({ updateSearchSpy, defendantType: 'company' });
+ cy.get(AccountSearchLocators.searchButton).click();
+
+ assertValidationError('Account number must be 9 characters or fewer', AccountSearchLocators.accountNumberError);
+ assertNoSearchUpdate(updateSearchSpy);
+ },
+ );
+
+ it(
+ 'AC3. Invalid advanced search criteria display the expected errors and no search update occurs',
{ tags: buildTags('@JIRA-KEY:POT-3869') },
() => {
const updateSearchSpy = Cypress.sinon.spy();
finesConSearchAccountFormData = {
...structuredClone(FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK.formData),
- fcon_search_account_number: '1234567',
fcon_search_account_companies_search_criteria: {
fcon_search_account_companies_company_name: 'Testing!!!',
fcon_search_account_companies_company_name_exact_match: false,
@@ -528,11 +614,6 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
cy.get(AccountSearchLocators.searchButton).click();
const expectedValidationErrors = [
- {
- //AC3a. User enters value that is not in correct format and the following error is produced
- message: 'Enter account number in the correct format such as 12345678 or 12345678A',
- selector: AccountSearchLocators.accountNumberError,
- },
{
//AC3b. User enters non-alphabetical or special characters and the following error is produced
message: 'Company name must only include letters a to z, hyphens, spaces and apostrophes',
@@ -555,20 +636,17 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
});
//AC3 Following selecting 'search' the system remains on the same screen
- cy.then(() => {
- expect(updateSearchSpy).to.not.have.been.called;
- });
+ assertNoSearchUpdate(updateSearchSpy);
},
);
it(
- 'AC4. Max length search validation displays the expected errors and no search update occurs',
+ 'AC4. Advanced search max length validation displays the expected errors and no search update occurs',
{ tags: buildTags('@JIRA-KEY:POT-3879') },
() => {
const updateSearchSpy = Cypress.sinon.spy();
finesConSearchAccountFormData = {
...structuredClone(FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK.formData),
- fcon_search_account_number: '1234567890',
fcon_search_account_companies_search_criteria: {
fcon_search_account_companies_company_name: 'QwertyuiopQwertyuiopQwertyuiopQwertyuiopQwertyuiopQ',
fcon_search_account_companies_company_name_exact_match: false,
@@ -581,11 +659,6 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
cy.get(AccountSearchLocators.searchButton).click();
const expectedValidationErrors = [
- {
- //AC4a. User enters value exceeding the max characters. Error isnt in line with others/conflicts this one first. Confirmed in ..field-errors.constant that it should be 'Account number must be 9 characters or fewer',. Pri 3. How to reach?
- message: 'Account number must be 9 characters or fewer',
- selector: AccountSearchLocators.accountNumberError,
- },
{
//AC4b. User enters value exceeding the max characters.
message: 'Company name must be 50 characters or fewer',
@@ -608,9 +681,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
});
//AC4 Following selecting 'search' the system remains on the same screen
- cy.then(() => {
- expect(updateSearchSpy).to.not.have.been.called;
- });
+ assertNoSearchUpdate(updateSearchSpy);
},
);
@@ -646,9 +717,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
});
//AC5 Following selecting 'search' the system remains on the same screen
- cy.then(() => {
- expect(updateSearchSpy).to.not.have.been.called;
- });
+ assertNoSearchUpdate(updateSearchSpy);
},
);
@@ -663,8 +732,10 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
cy.get(AccountSearchLocators.searchButton).click();
cy.then(() => {
- expect(updateSearchSpy).to.have.been.calledOnce;
- const submittedFormData = updateSearchSpy.firstCall.args[0] as IFinesConSearchAccountState;
+ const submittedFormData = findSubmittedFormData(
+ updateSearchSpy,
+ (formData) => formData.fcon_search_account_number === '12345678',
+ );
expect(submittedFormData.fcon_search_account_number).to.equal('12345678');
expect(submittedFormData.fcon_search_account_companies_search_criteria).to.deep.equal({
@@ -724,4 +795,94 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => {
// cy.get(AccountSearchLocators.forConsolidationTab).should('have.attr', 'aria-current', 'page');
},
);
+
+ it(
+ 'AC1a. Individual searches route to Search error when quick search and other account details are combined',
+ { tags: buildTags('@JIRA-KEY:POT-3882') },
+ () => {
+ const updateSearchSpy = Cypress.sinon.spy();
+ finesConSearchAccountFormData = {
+ ...structuredClone(FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK.formData),
+ fcon_search_account_number: '12345678',
+ fcon_search_account_individuals_search_criteria: {
+ fcon_search_account_individuals_last_name: 'Smith',
+ fcon_search_account_individuals_last_name_exact_match: false,
+ fcon_search_account_individuals_first_names: null,
+ fcon_search_account_individuals_first_names_exact_match: false,
+ fcon_search_account_individuals_include_aliases: false,
+ fcon_search_account_individuals_date_of_birth: null,
+ fcon_search_account_individuals_address_line_1: null,
+ fcon_search_account_individuals_post_code: null,
+ },
+ };
+
+ setupConsolidationComponent({ updateSearchSpy, setupRouterSpy: true });
+ cy.get(AccountSearchLocators.searchButton).click();
+
+ cy.then(() => {
+ findSubmittedFormData(
+ updateSearchSpy,
+ (formData) =>
+ formData.fcon_search_account_number === '12345678' &&
+ formData.fcon_search_account_individuals_search_criteria?.fcon_search_account_individuals_last_name ===
+ 'Smith',
+ );
+ });
+ assertSearchErrorRedirect({
+ fcon_search_account_number: '12345678',
+ fcon_search_account_individuals_search_criteria: {
+ fcon_search_account_individuals_last_name: 'Smith',
+ fcon_search_account_individuals_last_name_exact_match: false,
+ fcon_search_account_individuals_first_names: null,
+ fcon_search_account_individuals_first_names_exact_match: false,
+ fcon_search_account_individuals_include_aliases: false,
+ fcon_search_account_individuals_date_of_birth: null,
+ fcon_search_account_individuals_address_line_1: null,
+ fcon_search_account_individuals_post_code: null,
+ },
+ });
+ },
+ );
+
+ it(
+ 'AC1b. Company searches route to Search error when account number and other account details are combined',
+ { tags: buildTags('@JIRA-KEY:POT-3883') },
+ () => {
+ const updateSearchSpy = Cypress.sinon.spy();
+ finesConSearchAccountFormData = {
+ ...structuredClone(FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK.formData),
+ fcon_search_account_number: '12345678',
+ fcon_search_account_companies_search_criteria: {
+ fcon_search_account_companies_company_name: 'Test Company',
+ fcon_search_account_companies_company_name_exact_match: false,
+ fcon_search_account_companies_include_aliases: false,
+ fcon_search_account_companies_address_line_1: null,
+ fcon_search_account_companies_post_code: null,
+ },
+ };
+
+ setupConsolidationComponent({ updateSearchSpy, defendantType: 'company', setupRouterSpy: true });
+ cy.get(AccountSearchLocators.searchButton).click();
+
+ cy.then(() => {
+ findSubmittedFormData(
+ updateSearchSpy,
+ (formData) =>
+ formData.fcon_search_account_number === '12345678' &&
+ formData.fcon_search_account_companies_search_criteria?.fcon_search_account_companies_company_name ===
+ 'Test Company',
+ );
+ });
+ assertSearchErrorRedirect({
+ fcon_search_account_number: '12345678',
+ fcon_search_account_companies_search_criteria: {
+ fcon_search_account_companies_company_name: 'Test Company',
+ fcon_search_account_companies_company_name_exact_match: false,
+ fcon_search_account_companies_include_aliases: false,
+ fcon_search_account_companies_address_line_1: null,
+ fcon_search_account_companies_post_code: null,
+ },
+ });
+ },
+ );
});
diff --git a/cypress/component/fines/consolidation/ErrorPage.cy.ts b/cypress/component/fines/consolidation/ErrorPage.cy.ts
new file mode 100644
index 0000000000..f33b3cd290
--- /dev/null
+++ b/cypress/component/fines/consolidation/ErrorPage.cy.ts
@@ -0,0 +1,64 @@
+import { mount } from 'cypress/angular';
+import { ActivatedRoute, provideRouter } from '@angular/router';
+import { FinesConSearchErrorComponent } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component';
+import { FinesConStore } from 'src/app/flows/fines/fines-con/stores/fines-con.store';
+import { ErrorPageLocators } from 'cypress/shared/selectors/consolidation/ErrorPage.locators';
+
+const CONSOLIDATION_JIRA_LABEL = '@JIRA-LABEL:consolidation';
+const CONSOLIDATION_STORY_LABEL = '@JIRA-STORY:PO-2417';
+
+const buildTags = (...tags: string[]): string[] => [...tags, CONSOLIDATION_JIRA_LABEL, CONSOLIDATION_STORY_LABEL];
+
+describe('FinesConSearchErrorComponent', () => {
+ const setupComponent = (defendantType: 'individual' | 'company') => {
+ return mount(FinesConSearchErrorComponent, {
+ providers: [
+ provideRouter([]),
+ FinesConStore,
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ parent: {},
+ },
+ },
+ ],
+ }).then(({ fixture }) => {
+ const store = fixture.componentRef.injector.get(FinesConStore);
+ cy.stub(store, 'getDefendantType').returns(defendantType);
+ expect(store.getDefendantType()).to.equal(defendantType);
+ fixture.detectChanges();
+ });
+ };
+
+ const assertHeadingAndIntro = () => {
+ cy.get(ErrorPageLocators.heading).should('contain', 'There is a problem');
+ cy.get(ErrorPageLocators.message)
+ .invoke('text')
+ .then((text) => {
+ const normalisedText = text.replace(/\s+/g, ' ').trim();
+ expect(normalisedText).to.equal(
+ 'Reference data and account information cannot be entered together when searching for an account. Search using either:',
+ );
+ });
+ };
+
+ it('AC2a. displays the individual search error heading and message text', { tags: buildTags() }, () => {
+ setupComponent('individual');
+
+ assertHeadingAndIntro();
+ cy.get(ErrorPageLocators.bulletItems).then(($items) => {
+ const items = [...$items].map((item) => item.textContent?.replace(/\s+/g, ' ').trim());
+ expect(items).to.deep.equal(['account number, or', 'National Insurance number, or', 'advanced search']);
+ });
+ });
+
+ it('AC2b. displays the company search error heading and message text', { tags: buildTags() }, () => {
+ setupComponent('company');
+
+ assertHeadingAndIntro();
+ cy.get(ErrorPageLocators.bulletItems).then(($items) => {
+ const items = [...$items].map((item) => item.textContent?.replace(/\s+/g, ' ').trim());
+ expect(items).to.deep.equal(['account number, or', 'advanced search']);
+ });
+ });
+});
diff --git a/cypress/component/fines/consolidation/mocks/account_results_mock.ts b/cypress/component/fines/consolidation/mocks/account_results_mock.ts
new file mode 100644
index 0000000000..d91ebf2ac6
--- /dev/null
+++ b/cypress/component/fines/consolidation/mocks/account_results_mock.ts
@@ -0,0 +1,183 @@
+import { IFinesConSearchResultDefendantAccount } from '@app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/interfaces/fines-con-search-result-defendant-account.interface';
+import { FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK } from '@app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/mocks/fines-con-search-result-defendant-accounts-company-formatting.mock';
+import { FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK } from '@app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/mocks/fines-con-search-result-defendant-accounts-formatting.mock';
+
+export const createFalseyResult = (): IFinesConSearchResultDefendantAccount => ({
+ ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK[0]),
+ defendant_account_id: 12,
+ account_number: 'ACC002',
+ aliases: null,
+ address_line_1: null,
+ postcode: null,
+ prosecutor_case_reference: null,
+ last_enforcement_action: null,
+ account_balance: null,
+ defendant_firstnames: null,
+ defendant_surname: null,
+ birth_date: null,
+ national_insurance_number: null,
+ collection_order: false,
+ last_enforcement: null,
+ has_paying_parent_guardian: false,
+ checks: {
+ errors: [],
+ warnings: [],
+ },
+});
+
+export const createCompanyFalseyResult = (): IFinesConSearchResultDefendantAccount => ({
+ ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK[0]),
+ defendant_account_id: 22,
+ account_number: 'COMP002',
+ aliases: null,
+ address_line_1: null,
+ postcode: null,
+ prosecutor_case_reference: null,
+ last_enforcement_action: null,
+ account_balance: null,
+ organisation_name: null,
+ collection_order: false,
+ last_enforcement: null,
+ checks: {
+ errors: [],
+ warnings: [],
+ },
+});
+
+export const createMaxResultsMock = (): IFinesConSearchResultDefendantAccount[] =>
+ Array.from({ length: 100 }, (_, index) => ({
+ ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK[0]),
+ defendant_account_id: index + 1,
+ account_number: `ACC${String(index + 1).padStart(3, '0')}`,
+ aliases: null,
+ prosecutor_case_reference: `REF-${index + 1}`,
+ defendant_firstnames: `First${index + 1}`,
+ defendant_surname: `Surname${index + 1}`,
+ birth_date: '1990-01-03',
+ account_balance: 50 + index,
+ has_paying_parent_guardian: false,
+ checks: {
+ errors: [],
+ warnings: [],
+ },
+ }));
+
+export const createTooManyResultsMock = (): IFinesConSearchResultDefendantAccount[] => [
+ ...createMaxResultsMock(),
+ {
+ ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK[0]),
+ defendant_account_id: 101,
+ account_number: 'ACC101',
+ aliases: null,
+ prosecutor_case_reference: 'REF-101',
+ defendant_firstnames: 'First101',
+ defendant_surname: 'Surname101',
+ birth_date: '1990-01-03',
+ account_balance: 150,
+ has_paying_parent_guardian: false,
+ checks: {
+ errors: [],
+ warnings: [],
+ },
+ },
+];
+
+export const createCompanyMaxResultsMock = (): IFinesConSearchResultDefendantAccount[] =>
+ Array.from({ length: 100 }, (_, index) => ({
+ ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK[0]),
+ defendant_account_id: index + 1,
+ account_number: `COMP${String(index + 1).padStart(3, '0')}`,
+ aliases: null,
+ prosecutor_case_reference: `COMP-REF-${index + 1}`,
+ organisation_name: `Company ${index + 1}`,
+ account_balance: 500 + index,
+ checks: {
+ errors: [],
+ warnings: [],
+ },
+ }));
+
+export const createCompanyTooManyResultsMock = (): IFinesConSearchResultDefendantAccount[] => [
+ ...createCompanyMaxResultsMock(),
+ {
+ ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK[0]),
+ defendant_account_id: 101,
+ account_number: 'COMP101',
+ aliases: null,
+ prosecutor_case_reference: 'COMP-REF-101',
+ organisation_name: 'Company 101',
+ account_balance: 600,
+ checks: {
+ errors: [],
+ warnings: [],
+ },
+ },
+];
+
+export const createMultipleErrorsAndWarningsResult = (): IFinesConSearchResultDefendantAccount => ({
+ ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK[0]),
+ defendant_account_id: 15,
+ account_number: 'ACC005',
+ prosecutor_case_reference: 'REF-5',
+ defendant_firstnames: 'Erin',
+ defendant_surname: 'Example',
+ checks: {
+ errors: [
+ { reference: 'CON.ER.1', message: 'Account status is CS' },
+ { reference: 'CON.ER.2', message: 'Account is blocked for consolidation' },
+ ],
+ warnings: [
+ { reference: 'CON.WN.1', message: 'Account has uncleared cheque payments' },
+ { reference: 'CON.WN.2', message: 'Account has linked cases' },
+ ],
+ },
+});
+
+export const createMultipleWarningsResult = (): IFinesConSearchResultDefendantAccount => ({
+ ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK[0]),
+ defendant_account_id: 16,
+ account_number: 'ACC006',
+ prosecutor_case_reference: 'REF-6',
+ defendant_firstnames: 'Wendy',
+ defendant_surname: 'Warning',
+ checks: {
+ errors: [],
+ warnings: [
+ { reference: 'CON.WN.1', message: 'Account has uncleared cheque payments' },
+ { reference: 'CON.WN.2', message: 'Account has linked cases' },
+ ],
+ },
+});
+
+export const createCompanyMultipleErrorsAndWarningsResult = (): IFinesConSearchResultDefendantAccount => ({
+ ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK[0]),
+ defendant_account_id: 25,
+ account_number: 'COMP005',
+ prosecutor_case_reference: 'COMP-REF-5',
+ organisation_name: 'Errors & Warnings Ltd',
+ checks: {
+ errors: [
+ { reference: 'CON.ER.1', message: 'Account status is CS' },
+ { reference: 'CON.ER.2', message: 'Account is blocked for consolidation' },
+ ],
+ warnings: [
+ { reference: 'CON.WN.1', message: 'Account has uncleared cheque payments' },
+ { reference: 'CON.WN.2', message: 'Account has linked cases' },
+ ],
+ },
+});
+
+export const createCompanyMultipleWarningsResult = (): IFinesConSearchResultDefendantAccount => ({
+ ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK[0]),
+ defendant_account_id: 26,
+ account_number: 'COMP006',
+ prosecutor_case_reference: 'COMP-REF-6',
+ organisation_name: 'Warnings Only Ltd',
+ checks: {
+ errors: [],
+ warnings: [
+ { reference: 'CON.WN.1', message: 'Account has uncleared cheque payments' },
+ { reference: 'CON.WN.2', message: 'Account has linked cases' },
+ ],
+ },
+});
diff --git a/cypress/component/fines/consolidation/setup/SetupComponent.ts b/cypress/component/fines/consolidation/setup/SetupComponent.ts
index 242a3d23c7..79f3ebe5ad 100644
--- a/cypress/component/fines/consolidation/setup/SetupComponent.ts
+++ b/cypress/component/fines/consolidation/setup/SetupComponent.ts
@@ -13,6 +13,7 @@ import { IComponentProperties } from './setupComponent.interface';
export const setupConsolidationComponent = (componentProperties: IComponentProperties = {}) => {
const fragment = componentProperties.fragments ?? 'search';
const defendantType = componentProperties.defendantType ?? 'individual';
+ const initialResults = structuredClone(componentProperties.initialResults ?? []);
const finesConSelectBuFormData =
defendantType === 'company'
@@ -32,6 +33,11 @@ export const setupConsolidationComponent = (componentProperties: IComponentPrope
const store = new FinesConStore();
store.updateSelectBuForm(finesConSelectBuFormData);
store.updateSearchAccountFormTemporary(searchAccountFormData);
+ if (defendantType === 'company') {
+ store.updateDefendantResults([], initialResults);
+ } else {
+ store.updateDefendantResults(initialResults, []);
+ }
store.setActiveTab(fragment);
if (componentProperties.updateSearchSpy) {
diff --git a/cypress/component/fines/consolidation/setup/setupComponent.interface.ts b/cypress/component/fines/consolidation/setup/setupComponent.interface.ts
index 0a9fdc87ea..f8a4975ed2 100644
--- a/cypress/component/fines/consolidation/setup/setupComponent.interface.ts
+++ b/cypress/component/fines/consolidation/setup/setupComponent.interface.ts
@@ -1,4 +1,5 @@
import { IFinesConSearchAccountState } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/interfaces/fines-con-search-account-state.interface';
+import { IFinesConSearchResultDefendantAccount } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/interfaces/fines-con-search-result-defendant-account.interface';
export type ConsolidationTabFragment = 'search' | 'results' | 'for-consolidation';
export type DefendantType = 'individual' | 'company';
@@ -7,6 +8,7 @@ export interface IComponentProperties {
defendantType?: DefendantType;
fragments?: ConsolidationTabFragment;
searchAccountFormData?: IFinesConSearchAccountState;
+ initialResults?: IFinesConSearchResultDefendantAccount[];
setupRouterSpy?: boolean;
updateSearchSpy?: (formData: IFinesConSearchAccountState) => void;
}
diff --git a/cypress/e2e/functional/opal/actions/consolidation/consolidation.actions.ts b/cypress/e2e/functional/opal/actions/consolidation/consolidation.actions.ts
index d916a56863..d8db948e2b 100644
--- a/cypress/e2e/functional/opal/actions/consolidation/consolidation.actions.ts
+++ b/cypress/e2e/functional/opal/actions/consolidation/consolidation.actions.ts
@@ -5,12 +5,21 @@
import { SelectBusinessUnitLocators } from '../../../../../shared/selectors/consolidation/SelectBusinessUnit.locators';
import { AccountSearchLocators } from '../../../../../shared/selectors/consolidation/AccountSearch.locators';
+import { AccountResultsLocators } from '../../../../../shared/selectors/consolidation/AccountResults.locators';
+import { ErrorPageLocators } from '../../../../../shared/selectors/consolidation/ErrorPage.locators';
import { createScopedLogger } from '../../../../../support/utils/log.helper';
+import { applyUniqPlaceholder } from '../../../../../support/utils/stringUtils';
const log = createScopedLogger('ConsolidationActions');
+const SINGLE_BUSINESS_UNIT_MESSAGE_PREFIX = 'The consolidation will be processed in';
export type ConsolidationDefendantType = 'Individual' | 'Company';
type SearchDetails = Record;
+type CreatedAccountAlias = {
+ accountId?: number | string | null;
+ accountNumber?: string | null;
+};
+const SELECTED_BUSINESS_UNIT_ALIAS = 'selectedConsolidationBusinessUnit';
/** Actions and assertions for the Consolidation flow screens. */
export class ConsolidationActions {
@@ -54,15 +63,98 @@ export class ConsolidationActions {
throw new Error(`Unsupported checkbox value "${value}". Use true/false (or yes/no).`);
}
+ /**
+ * Resolves the last created account id/number stored on the shared @etagUpdate alias.
+ * This alias is set by the draft account creation helpers.
+ * @returns Chainable yielding the created account id and account number.
+ */
+ private getCreatedAccountAlias(): Cypress.Chainable<{ accountId: number; accountNumber: string }> {
+ return cy.get('@etagUpdate').then((etagUpdate) => {
+ const accountId = Number(etagUpdate?.accountId);
+ const accountNumber = String(etagUpdate?.accountNumber ?? '').trim();
+
+ if (!Number.isFinite(accountId) || accountId <= 0) {
+ throw new Error('Expected @etagUpdate to contain a valid accountId for consolidation result assertions.');
+ }
+
+ if (!accountNumber) {
+ throw new Error('Expected @etagUpdate to contain a valid accountNumber for consolidation result assertions.');
+ }
+
+ return { accountId, accountNumber };
+ });
+ }
+
+ /**
+ * Stores the selected consolidation business unit so later assertions can verify the actual chosen value.
+ * @param businessUnitName - Business unit label captured from the UI.
+ */
+ private setSelectedBusinessUnitAlias(businessUnitName: string): void {
+ cy.wrap(String(businessUnitName).trim(), { log: false }).as(SELECTED_BUSINESS_UNIT_ALIAS);
+ }
+
+ /**
+ * Resolves the selected consolidation business unit captured during the select BU step.
+ * @returns Chainable yielding the trimmed business unit name.
+ */
+ private getSelectedBusinessUnitAlias(): Cypress.Chainable {
+ return cy
+ .get(`@${SELECTED_BUSINESS_UNIT_ALIAS}`)
+ .then((businessUnitName) => String(businessUnitName).trim());
+ }
+
+ /**
+ * Waits until the select business unit screen has rendered either the autocomplete
+ * input or the single-business-unit informational message.
+ * @returns Chainable yielding the rendered business unit selection mode.
+ */
+ private waitForBusinessUnitSelectionMode(): Cypress.Chainable<'single' | 'multiple'> {
+ return cy
+ .get('body', { timeout: 10_000 })
+ .should(($body) => {
+ const hasBusinessUnitInput = $body.find(SelectBusinessUnitLocators.businessUnitInput).length > 0;
+ const hasSingleBusinessUnitMessage = $body
+ .find(SelectBusinessUnitLocators.singleBusinessUnitMessage)
+ .toArray()
+ .some((element) => Cypress.$(element).text().includes(SINGLE_BUSINESS_UNIT_MESSAGE_PREFIX));
+
+ expect(
+ hasBusinessUnitInput || hasSingleBusinessUnitMessage,
+ 'business unit autocomplete or single business unit message',
+ ).to.be.true;
+ })
+ .then(($body) => {
+ const hasBusinessUnitInput = $body.find(SelectBusinessUnitLocators.businessUnitInput).length > 0;
+ return hasBusinessUnitInput ? 'multiple' : 'single';
+ });
+ }
+
/**
* Selects a business unit when the selector is present.
* If a single business unit is auto-selected, verifies the informational message instead.
*/
public selectBusinessUnitIfRequired(): void {
- cy.get('body').then(($body) => {
- if ($body.find(SelectBusinessUnitLocators.businessUnitInput).length === 0) {
+ // Wait for the select business unit form and its business unit branch to finish rendering
+ // before deciding whether we are in the single-BU or autocomplete path.
+ cy.get(SelectBusinessUnitLocators.heading, { timeout: 10_000 }).should('contain.text', 'Consolidate accounts');
+ cy.get(SelectBusinessUnitLocators.defendantTypeHeading, { timeout: 10_000 }).should(
+ 'contain.text',
+ 'Defendant type',
+ );
+ cy.get(SelectBusinessUnitLocators.continueButton, { timeout: 10_000 }).should('be.visible');
+
+ this.waitForBusinessUnitSelectionMode().then((mode) => {
+ if (mode === 'single') {
log('info', 'Business unit input not shown; using auto-selected single business unit');
- cy.get(SelectBusinessUnitLocators.singleBusinessUnitMessage, { timeout: 10_000 }).should('be.visible');
+ cy.contains(SelectBusinessUnitLocators.singleBusinessUnitMessage, SINGLE_BUSINESS_UNIT_MESSAGE_PREFIX, {
+ timeout: 10_000,
+ })
+ .should('be.visible')
+ .invoke('text')
+ .then((text) => {
+ const businessUnitName = text.replace(SINGLE_BUSINESS_UNIT_MESSAGE_PREFIX, '').trim();
+ this.setSelectedBusinessUnitAlias(businessUnitName);
+ });
return;
}
@@ -72,7 +164,11 @@ export class ConsolidationActions {
.should('be.visible')
.find('li')
.first()
- .click();
+ .then(($item) => {
+ const businessUnitName = $item.text().trim();
+ this.setSelectedBusinessUnitAlias(businessUnitName);
+ cy.wrap($item).click();
+ });
});
}
@@ -84,17 +180,53 @@ export class ConsolidationActions {
log('select', `Selecting defendant type: ${defendantType}`);
if (defendantType === 'Individual') {
- cy.get(SelectBusinessUnitLocators.individualInput).check({ force: true });
+ cy.get(SelectBusinessUnitLocators.individualInput, { timeout: 10_000 })
+ .should('exist')
+ .and('not.be.disabled')
+ .check({ force: true });
return;
}
- cy.get(SelectBusinessUnitLocators.companyInput).check({ force: true });
+ cy.get(SelectBusinessUnitLocators.companyInput, { timeout: 10_000 })
+ .should('exist')
+ .and('not.be.disabled')
+ .check({ force: true });
}
/** Clicks Continue on the Select Business Unit screen. */
public continueFromSelectBusinessUnit(): void {
log('click', 'Clicking Continue on consolidation select business unit page');
- cy.get(SelectBusinessUnitLocators.continueButton, { timeout: 10_000 }).should('be.visible').click();
+ cy.get(SelectBusinessUnitLocators.continueButton, { timeout: 10_000 })
+ .should('be.visible')
+ .and('not.be.disabled')
+ .click();
+ }
+
+ /**
+ * Waits for the consolidation account search screen to finish rendering after continuing
+ * from the select business unit page.
+ * @param defendantType - Expected defendant type shown on the search summary.
+ */
+ public waitForAccountSearchScreen(defendantType: ConsolidationDefendantType): void {
+ cy.location('pathname', { timeout: 10_000 }).should('include', '/fines/consolidation/consolidate-accounts');
+ cy.get(AccountSearchLocators.heading, { timeout: 10_000 }).should('contain.text', 'Consolidate accounts');
+ cy.get(AccountSearchLocators.summaryList, { timeout: 10_000 }).should('be.visible');
+ cy.get(AccountSearchLocators.searchTabLink, { timeout: 10_000 }).should('have.attr', 'aria-current', 'page');
+ cy.get(AccountSearchLocators.defendantTypeValue, { timeout: 10_000 }).should('contain', defendantType);
+ cy.get(AccountSearchLocators.accountNumberInput, { timeout: 10_000 }).should('be.visible');
+
+ if (defendantType === 'Company') {
+ cy.get(AccountSearchLocators.companyNameInput, { timeout: 10_000 }).should('be.visible');
+ }
+ }
+
+ /** Asserts the user is on the consolidation business unit and defendant type selection screen. */
+ public assertOnSelectBusinessUnitScreen(): void {
+ log('assert', 'Verifying user is on consolidation select business unit screen');
+
+ cy.location('pathname', { timeout: 10_000 }).should('include', '/fines/consolidation/select-business-unit');
+ cy.get(SelectBusinessUnitLocators.heading, { timeout: 10_000 }).should('contain.text', 'Consolidate accounts');
+ cy.get(SelectBusinessUnitLocators.defendantTypeHeading).should('contain.text', 'Defendant type');
}
/** Clicks Search on the consolidation Search tab. */
@@ -103,6 +235,14 @@ export class ConsolidationActions {
cy.get(AccountSearchLocators.searchButton, { timeout: 10_000 }).should('be.visible').click();
}
+ /** Clicks the Clear search link on the consolidation Search tab. */
+ public clearSearch(): void {
+ log('click', 'Clearing consolidation account search form');
+ cy.contains(AccountSearchLocators.clearSearchLink, 'Clear search', { timeout: 10_000 })
+ .should('be.visible')
+ .click();
+ }
+
/** Asserts the user is on Consolidation account search for Individuals, Search tab active. */
public assertOnSearchTabForIndividuals(): void {
log('assert', 'Verifying user is on consolidation Search tab for Individuals');
@@ -124,6 +264,178 @@ export class ConsolidationActions {
cy.get(AccountSearchLocators.companyNameInput).should('be.visible');
}
+ /** Asserts the page-header back link is displayed on the consolidation shell. */
+ public assertBackLinkIsDisplayed(): void {
+ log('assert', 'Verifying consolidation page-header back link is displayed');
+ cy.get(AccountSearchLocators.backLink, { timeout: 10_000 }).should('be.visible').and('contain.text', 'Back');
+ }
+
+ /** Clicks the page-header back link on the consolidation shell. */
+ public clickBackLink(): void {
+ log('click', 'Clicking consolidation page-header back link');
+ cy.get(AccountSearchLocators.backLink, { timeout: 10_000 }).should('be.visible').click({ force: true });
+ }
+
+ /** Clicks the Results tab from the consolidation flow. */
+ public openResultsTab(): void {
+ log('navigate', 'Opening consolidation Results tab');
+ cy.get(AccountSearchLocators.resultsTab, { timeout: 10_000 }).should('be.visible').click();
+ }
+
+ /** Asserts the user is on the consolidation Results tab. */
+ public assertOnResultsTab(): void {
+ log('assert', 'Verifying user is on consolidation Results tab');
+
+ cy.location('pathname', { timeout: 10_000 }).should('include', '/fines/consolidation/consolidate-accounts');
+ cy.get(AccountSearchLocators.resultsTab, { timeout: 10_000 }).should('have.attr', 'aria-current', 'page');
+ cy.get(AccountSearchLocators.searchButton).should('not.exist');
+ cy.get(AccountResultsLocators.resultsTable, { timeout: 10_000 }).should('be.visible');
+ }
+
+ /**
+ * Asserts the user is on the consolidation Results tab with the expected summary values.
+ * Covers the active tab plus the displayed business unit and defendant type rows.
+ * @param defendantType - Expected defendant type shown in the summary.
+ */
+ public assertOnResultsTabForDefendantType(defendantType: ConsolidationDefendantType): void {
+ this.assertOnResultsTab();
+ this.getSelectedBusinessUnitAlias().then((businessUnitName) => {
+ cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business unit');
+ cy.get(AccountSearchLocators.businessUnitValue).should('contain', businessUnitName);
+ cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant type');
+ cy.get(AccountSearchLocators.defendantTypeValue).should('contain', defendantType);
+ });
+ }
+
+ /** Asserts the no matching results state is shown with the Check your search hyperlink. */
+ public assertNoMatchingResultsState(): void {
+ log('assert', 'Verifying consolidation no matching results state');
+
+ cy.get(AccountResultsLocators.resultsTable).should('not.exist');
+ cy.get(AccountResultsLocators.invalidResultsHeading, { timeout: 10_000 }).should(
+ 'contain',
+ 'There are no matching results.',
+ );
+ cy.get(AccountResultsLocators.invalidResultsBody)
+ .invoke('text')
+ .then((text) => {
+ const normalisedText = text.replace(/\s+/g, ' ').trim();
+ expect(normalisedText).to.equal('Check your search and try again.');
+ });
+ cy.contains(AccountResultsLocators.invalidResultsLink, 'Check your search', { timeout: 10_000 }).should(
+ 'be.visible',
+ );
+ }
+
+ /**
+ * Asserts no consolidation result row displays the supplied balance.
+ * @param balance - Forbidden rendered balance value, e.g. "£0.00"
+ */
+ public assertResultsExcludeBalance(balance: string): void {
+ const forbiddenBalance = balance.trim();
+
+ log('assert', 'Verifying consolidation results exclude balance', { forbiddenBalance });
+
+ cy.get(AccountResultsLocators.resultsRows, { timeout: 10_000 }).its('length').should('be.gte', 1);
+ cy.get(AccountResultsLocators.resultBalanceCell, { timeout: 10_000 }).each(($cell) => {
+ const renderedBalance = $cell.text().replace(/\s+/g, ' ').trim();
+ expect(renderedBalance).to.not.equal(forbiddenBalance);
+ });
+ }
+
+ /** Clicks the Check your search hyperlink from the no matching results state. */
+ public clickCheckYourSearchFromNoMatchingResults(): void {
+ log('click', 'Clicking Check your search from consolidation no matching results state');
+ cy.contains(AccountResultsLocators.invalidResultsLink, 'Check your search', { timeout: 10_000 })
+ .should('be.visible')
+ .click();
+ }
+
+ /** Asserts the newly created account number is displayed as a hyperlink in the results table. */
+ public assertCreatedAccountLinkIsDisplayed(): void {
+ this.getCreatedAccountAlias().then(({ accountNumber }) => {
+ log('assert', 'Verifying created consolidation result account is displayed as a hyperlink', { accountNumber });
+
+ cy.get(AccountResultsLocators.resultAccountLinkByNumber(accountNumber), { timeout: 10_000 })
+ .should('be.visible')
+ .and('have.class', 'govuk-link')
+ .then(($link) => {
+ expect($link.prop('tagName')).to.equal('A');
+ });
+ });
+ }
+
+ /** Opens the created consolidation result account and asserts it is opened in a new tab to the FAE details route. */
+ public openCreatedAccountFromResultsInNewTab(): void {
+ cy.intercept('GET', '**/defendant-accounts/**/header-summary').as('consolidationHeaderSummary');
+
+ this.getCreatedAccountAlias().then(({ accountNumber }) => {
+ log('open', 'Opening created consolidation result account from results', { accountNumber });
+
+ cy.window().then((win) => {
+ cy.stub(win, 'open')
+ .callsFake((url?: string | URL, target?: string) => {
+ expect(target).to.equal('_blank');
+ win.location.href = String(url);
+ })
+ .as('consolidationWindowOpen');
+ });
+
+ cy.get(AccountResultsLocators.resultAccountLinkByNumber(accountNumber), { timeout: 10_000 })
+ .should('be.visible')
+ .click();
+
+ cy.get('@consolidationWindowOpen').then((windowOpenStub) => {
+ const stub = windowOpenStub as any;
+ expect(stub.calledOnce).to.equal(true);
+
+ const [openedUrl, target] = stub.getCall(0).args as [string, string];
+ expect(target).to.equal('_blank');
+ expect(String(openedUrl)).to.match(/\/fines\/account\/defendant\/\d+\/details$/);
+ });
+
+ cy.wait('@consolidationHeaderSummary', { timeout: 15_000 }).its('response.statusCode').should('eq', 200);
+ });
+ }
+
+ /**
+ * Asserts the consolidation search error page content for the given defendant type.
+ * @param defendantType - "Individual" or "Company"
+ */
+ public assertSearchErrorPage(defendantType: ConsolidationDefendantType): void {
+ const expectedBulletItems =
+ defendantType === 'Individual'
+ ? ['account number, or', 'national insurance number, or', 'advanced search']
+ : ['account number, or', 'advanced search'];
+
+ log('assert', 'Verifying consolidation search error page', { defendantType, expectedBulletItems });
+
+ cy.get(ErrorPageLocators.root, { timeout: 10_000 }).should('be.visible');
+ cy.get(`${ErrorPageLocators.root} ${ErrorPageLocators.heading}`).should('have.text', 'There is a problem');
+ cy.get(`${ErrorPageLocators.root} ${ErrorPageLocators.message}`)
+ .should('be.visible')
+ .invoke('text')
+ .then((text) => {
+ const normalisedText = text.replace(/\s+/g, ' ').trim();
+ expect(normalisedText).to.equal(
+ 'Reference data and account information cannot be entered together when searching for an account. Search using either:',
+ );
+ });
+
+ cy.get(`${ErrorPageLocators.root} ${ErrorPageLocators.bulletItems}`).then(($items) => {
+ const items = [...$items].map((item) => item.textContent?.replace(/\s+/g, ' ').trim().toLowerCase());
+ expect(items).to.deep.equal(expectedBulletItems);
+ });
+ }
+
+ /** Clicks the Go back link on the consolidation search error page. */
+ public goBackFromSearchError(): void {
+ log('click', 'Going back from consolidation search error page');
+ cy.contains(`${ErrorPageLocators.root} ${ErrorPageLocators.backLink}`, 'Go back', { timeout: 10_000 })
+ .should('be.visible')
+ .click();
+ }
+
/**
* Populates fields on the consolidation Search tab from key/value details.
* @param details - Search details keyed by user-facing field labels.
@@ -136,16 +448,17 @@ export class ConsolidationActions {
Object.entries(details).forEach(([rawKey, value]) => {
const key = rawKey.trim().toLowerCase();
+ const resolvedValue = applyUniqPlaceholder(value);
if (activeTextMap[key]) {
const selector = activeTextMap[key];
- cy.get(selector).clear().type(value);
+ cy.get(selector).clear().type(resolvedValue);
return;
}
if (activeCheckboxMap[key]) {
const selector = activeCheckboxMap[key];
- const shouldCheck = this.parseCheckboxValue(value);
+ const shouldCheck = this.parseCheckboxValue(resolvedValue);
if (shouldCheck) {
cy.get(selector).check({ force: true });
} else {
@@ -200,16 +513,17 @@ export class ConsolidationActions {
Object.entries(details).forEach(([rawKey, value]) => {
const key = rawKey.trim().toLowerCase();
+ const resolvedValue = applyUniqPlaceholder(value);
if (activeTextMap[key]) {
const selector = activeTextMap[key];
- cy.get(selector).should('have.value', value);
+ cy.get(selector).should('have.value', resolvedValue);
return;
}
if (activeCheckboxMap[key]) {
const selector = activeCheckboxMap[key];
- const shouldCheck = this.parseCheckboxValue(value);
+ const shouldCheck = this.parseCheckboxValue(resolvedValue);
if (shouldCheck) {
cy.get(selector).should('be.checked');
} else {
diff --git a/cypress/e2e/functional/opal/features/consolidation/FineAccountConsolidation.feature b/cypress/e2e/functional/opal/features/consolidation/FineAccountConsolidation.feature
index 136266539f..c6b09ff2ef 100644
--- a/cypress/e2e/functional/opal/features/consolidation/FineAccountConsolidation.feature
+++ b/cypress/e2e/functional/opal/features/consolidation/FineAccountConsolidation.feature
@@ -39,7 +39,11 @@ Feature: Fines Account Consolidation
| first names exact match | true |
| include aliases | true |
Then I click Search on consolidation account search
- Then the consolidation search details are retained:
+ Then I see the consolidation search error page for "Individual"
+ # AC3b All data previously entered on the Search page is retained when a user clicks back from the consolidation search error page
+ When I go back from the consolidation search error page
+ Then I am on the consolidation Search tab for Individuals
+ And the consolidation search details are retained:
| account number | 12345678 |
| national insurance number | AB123456C |
| last name | Smith |
@@ -50,6 +54,94 @@ Feature: Fines Account Consolidation
| last name exact match | true |
| first names exact match | true |
| include aliases | true |
+ When I clear the consolidation search
+ And I enter the following consolidation search details:
+ | last name | NoMatch |
+ | last name exact match | true |
+ And I click Search on consolidation account search
+ # AC2d - If no matching results are returned, Check your search is a hyperlink that returns the user to Search with all previously entered data retained
+ Then I see the consolidation no matching results state
+ When I click Check your search on consolidation no matching results
+ Then I am on the consolidation Search tab for Individuals
+ And the consolidation search details are retained:
+ | last name | NoMatch |
+ | last name exact match | true |
+
+ And I click Search on consolidation account search
+ Then I see the consolidation no matching results state
+ # AC4 - A Back button will be displayed in the page header
+ Then the consolidation page header back link is displayed
+ # AC4 - Selecting Back will return the user to the BU and defendant type selection screen
+ When I click the consolidation page header back link
+ Then I am on the consolidation business unit and defendant type selection screen
+
+ @JIRA-STORY:PO-2413 @JIRA-KEY:POT-3328
+ Scenario: Consolidation Successful account search for Individuals
+ 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.forenames | Consolidation |
+ | account.defendant.surname | ResultLink{uniq} |
+ | account.defendant.email_address_1 | consolidation.result{uniq}@test.com |
+ | account.defendant.telephone_number_home | 02078259314 |
+ | account.account_type | Fine |
+ | account.prosecutor_case_reference | CONS-RESULT-IND-{uniq} |
+ | account.collection_order_made | false |
+ | account.collection_order_made_today | false |
+ | account.payment_card_request | false |
+ | account.defendant.dob | 2002-05-15 |
+ When I open Consolidate accounts
+ When I continue to the consolidation account search as an "Individual" defendant
+ Then I am on the consolidation Search tab for Individuals
+ And I enter the following consolidation search details:
+ | last name | ResultLink{uniq} |
+ | last name exact match | true |
+ And I click Search on consolidation account search
+ # AC1 - A user is navigated to the Results tab for Individuals when a valid search has been performed from the Search tab within the Consolidate accounts journey
+ # AC1a - The Business unit row displays the Business Unit used in the search
+ # AC1b - The Defendant type row displays Individual
+ Then I am on the consolidation Results tab for Individuals
+ # AC4 - The Account column displays the account number as a hyperlink. Selecting it will open the relevant FAE – At a glance page in a new tab
+ And the created consolidation result account number is displayed as a hyperlink
+ When I open the created consolidation result account in a new tab
+ Then I should see the account header contains "Mr Consolidation RESULTLINK{uniqUpper}"
+
+ @JIRA-STORY:PO-2413 @JIRA-KEY:POT-3328
+ Scenario: Consolidation search excludes zero balance accounts for Individuals
+ 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.forenames | Zero |
+ | account.defendant.surname | ConsolidationZeroBalance{uniq} |
+ | account.defendant.email_address_1 | consolidation.zero.balance{uniq}@test.com |
+ | account.defendant.telephone_number_home | 02078259310 |
+ | account.account_type | Fine |
+ | account.prosecutor_case_reference | CONS-ZERO-BAL-IND-A-{uniq} |
+ | account.collection_order_made | false |
+ | account.collection_order_made_today | false |
+ | account.payment_card_request | false |
+ | account.defendant.dob | 2001-05-15 |
+ | account.offences.0.impositions.0.amount_paid | 125 |
+ And 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.forenames | Visible |
+ | account.defendant.surname | ConsolidationZeroBalance{uniq} |
+ | account.defendant.email_address_1 | consolidation.visible.balance{uniq}@test.com |
+ | account.defendant.telephone_number_home | 02078259311 |
+ | account.account_type | Fine |
+ | account.prosecutor_case_reference | CONS-ZERO-BAL-IND-B-{uniq} |
+ | account.collection_order_made | false |
+ | account.collection_order_made_today | false |
+ | account.payment_card_request | false |
+ | account.defendant.dob | 2002-05-15 |
+ When I open Consolidate accounts
+ And I continue to the consolidation account search as an "Individual" defendant
+ Then I am on the consolidation Search tab for Individuals
+ And I enter the following consolidation search details:
+ | last name | ConsolidationZero |
+
+ And I click Search on consolidation account search
+ Then I am on the consolidation Results tab for Individuals
+ And the created consolidation result account number is displayed as a hyperlink
+ And the consolidation results exclude accounts with a balance of "£0.00"
@JIRA-STORY:PO-2414 @JIRA-KEY:POT-3329
Scenario: Consolidation account search for Companies
@@ -74,10 +166,93 @@ Feature: Fines Account Consolidation
| Search exact match | true |
| include aliases | true |
Then I click Search on consolidation account search
- Then the consolidation search details are retained:
+ Then I see the consolidation search error page for "Company"
+ # AC3b All data previously entered on the Search page is retained when a user clicks back from the consolidation search error page
+ When I go back from the consolidation search error page
+ Then I am on the consolidation Search tab for Companies
+ And the consolidation search details are retained:
| account number | 12345678 |
| company name | Company Name |
| address line 1 | 1 High St |
| postcode | SW1A 1AA |
| Search exact match | true |
| include aliases | true |
+ When I clear the consolidation search
+ And I enter the following consolidation search details:
+ | company name | No Match Co |
+ | Search exact match | true |
+ And I click Search on consolidation account search
+ # AC2d - If no matching results are returned, Check your search is a hyperlink that returns the user to Search with all previously entered data retained
+ Then I see the consolidation no matching results state
+ When I click Check your search on consolidation no matching results
+ Then I am on the consolidation Search tab for Companies
+ And the consolidation search details are retained:
+ | company name | No Match Co |
+ | Search exact match | true |
+ And I click Search on consolidation account search
+ Then I see the consolidation no matching results state
+ # AC4 - A Back button will be displayed in the page header
+ Then the consolidation page header back link is displayed
+ # AC4 - Selecting Back will return the user to the BU and defendant type selection screen
+ When I click the consolidation page header back link
+ Then I am on the consolidation business unit and defendant type selection screen
+
+
+ @JIRA-STORY:PO-2413 @JIRA-KEY:POT-3328
+ Scenario: Consolidation Successful account search for Company
+ Given I create a "company" 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.company_name | Consolidation Result Co {uniq} |
+ | account.defendant.email_address | consolidation.result.co{uniq}@test.com |
+ | account.defendant.post_code | AB23 4RN |
+ | account.prosecutor_case_reference | CONS-RESULT-COMP-{uniq} |
+ | account.account_type | Fine |
+ When I open Consolidate accounts
+ When I continue to the consolidation account search as an "Company" defendant
+ Then I am on the consolidation Search tab for Companies
+ And I enter the following consolidation search details:
+ | company name | Consolidation Result Co {uniq} |
+ | Search exact match | true |
+ And I click Search on consolidation account search
+ # AC1 - A user is navigated to the Results tab for Companies when a valid search has been performed from the Search tab within the Consolidate accounts journey
+ # AC1a - The Business unit row displays the Business Unit used in the search
+ # AC1b - The Defendant type row displays Company
+ Then I am on the consolidation Results tab for Companies
+ # AC4 - The Account column displays the account number as a hyperlink. Selecting it will open the relevant FAE – At a glance page in a new tab
+ And the created consolidation result account number is displayed as a hyperlink
+ When I open the created consolidation result account in a new tab
+ Then I should see the account header contains "Consolidation Result Co {uniqUpper}"
+
+ @JIRA-STORY:PO-2414 @JIRA-KEY:POT-3329
+ Scenario: Consolidation search excludes zero balance accounts for Company
+ Given I create a "company" 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.company_name | Consolidation Zero Balance Co {uniq} |
+ | account.defendant.email_address_1 | consolidation.zero.balance.co{uniq}@test.com |
+ | account.defendant.post_code | AB23 4RN |
+ | account.prosecutor_case_reference | CONS-ZERO-BAL-COMP-A-{uniq} |
+ | account.account_type | Fine |
+ | account.collection_order_made | false |
+ | account.collection_order_made_today | false |
+ | account.payment_card_request | false |
+ | account.offences.0.impositions.0.amount_paid | 125 |
+ And I create a "company" 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.company_name | Consolidation Zero Balance Co {uniq} |
+ | account.defendant.email_address_1 | consolidation.visible.balance.co{uniq}@test.com |
+ | account.defendant.post_code | AB23 4RN |
+ | account.prosecutor_case_reference | CONS-ZERO-BAL-COMP-B-{uniq} |
+ | account.account_type | Fine |
+ | account.collection_order_made | false |
+ | account.collection_order_made_today | false |
+ | account.payment_card_request | false |
+ When I open Consolidate accounts
+ And I continue to the consolidation account search as an "Company" defendant
+ Then I am on the consolidation Search tab for Companies
+ And I enter the following consolidation search details:
+ | company name | Consolidation Zero Balance Co {uniq} |
+ | Search exact match | true |
+ And I click Search on consolidation account search
+ Then I am on the consolidation Results tab for Companies
+ And the created consolidation result account number is displayed as a hyperlink
+ And the consolidation results exclude accounts with a balance of "£0.00"
diff --git a/cypress/e2e/functional/opal/features/consolidation/FinesAccountConsolidationAccessibility.feature b/cypress/e2e/functional/opal/features/consolidation/FinesAccountConsolidationAccessibility.feature
index 697966cace..e4beab898d 100644
--- a/cypress/e2e/functional/opal/features/consolidation/FinesAccountConsolidationAccessibility.feature
+++ b/cypress/e2e/functional/opal/features/consolidation/FinesAccountConsolidationAccessibility.feature
@@ -6,18 +6,87 @@ Feature: Accessibility Tests for Fines Consolidation
Given I am logged in with email "opal-test@dev.platform.hmcts.net"
Then I should be on the dashboard
- @JIRA-STORY:PO-2413 @JIRA-KEY:POT-3210
+ @JIRA-STORY:PO-2413 @JIRA-STORY:PO-2415 @JIRA-STORY:PO-2417 @JIRA-KEY:POT-3210
Scenario: Consolidate Accessibility for Individuals
+ 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.forenames | Consolidation |
+ | account.defendant.surname | Accessibility{uniq} |
+ | account.defendant.email_address_1 | consolidation.accessibility{uniq}@test.com |
+ | account.defendant.telephone_number_home | 02078259314 |
+ | account.account_type | Fine |
+ | account.prosecutor_case_reference | CONS-A11Y-IND-{uniq} |
+ | account.collection_order_made | false |
+ | account.collection_order_made_today | false |
+ | account.payment_card_request | false |
+ | account.defendant.dob | 2002-05-15 |
When I open Consolidate accounts
Then I check the page for accessibility
And I continue to the consolidation account search as an "Individual" defendant
Then I am on the consolidation Search tab for Individuals
And I check the page for accessibility
+ And I enter the following consolidation search details:
+ | account number | 12345678 |
+ | last name | Accessibility{uniq} |
+ When I click Search on consolidation account search
+ Then I see the consolidation search error page for "Individual"
+ And I check the page for accessibility
+ When I go back from the consolidation search error page
+ Then I am on the consolidation Search tab for Individuals
+ When I clear the consolidation search
+ And I enter the following consolidation search details:
+ | last name | Nobody |
+ | last name exact match | true |
+ When I click Search on consolidation account search
+ Then I see the consolidation no matching results state
+ Then I check the page for accessibility
+ When I click Check your search on consolidation no matching results
+ Then I am on the consolidation Search tab for Individuals
+ And I enter the following consolidation search details:
+ | last name | Accessibility{uniq} |
+ | last name exact match | true |
+ When I click Search on consolidation account search
+ Then I am on the consolidation Results tab
+ And I check the page for accessibility
- @JIRA-STORY:PO-2414 @JIRA-KEY:POT-3211
+ @JIRA-STORY:PO-2414 @JIRA-STORY:PO-2421 @JIRA-STORY:PO-2417 @JIRA-KEY:POT-3211
Scenario: Consolidate Accessibility for Companies
+ Given I create a "company" 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.company_name | Consolidation Access Comp {uniq} |
+ | account.defendant.email_address_1 | consolidation.access.comp{uniq}@test.com |
+ | account.defendant.post_code | AB23 4RN |
+ | account.account_type | Fine |
+ | account.prosecutor_case_reference | CONS-A11Y-COMP-{uniq} |
+ | account.collection_order_made | false |
+ | account.collection_order_made_today | false |
+ | account.payment_card_request | false |
When I open Consolidate accounts
Then I check the page for accessibility
And I continue to the consolidation account search as an "Company" defendant
Then I am on the consolidation Search tab for Companies
And I check the page for accessibility
+ And I enter the following consolidation search details:
+ | account number | 12345678 |
+ | company name | Consolidation Access Comp {uniq} |
+ When I click Search on consolidation account search
+ Then I see the consolidation search error page for "Company"
+ And I check the page for accessibility
+ When I go back from the consolidation search error page
+ Then I am on the consolidation Search tab for Companies
+ When I clear the consolidation search
+ And I enter the following consolidation search details:
+ | company name | Nobody |
+ | Search exact match | true |
+ When I click Search on consolidation account search
+ Then I see the consolidation no matching results state
+ Then I check the page for accessibility
+ When I click Check your search on consolidation no matching results
+ Then I am on the consolidation Search tab for Companies
+ When I clear the consolidation search
+ And I enter the following consolidation search details:
+ | company name | Consolidation Access Comp {uniq} |
+ | Search exact match | true |
+ When I click Search on consolidation account search
+ Then I am on the consolidation Results tab
+ And I check the page for accessibility
diff --git a/cypress/e2e/functional/opal/flows/consolidation.flow.ts b/cypress/e2e/functional/opal/flows/consolidation.flow.ts
index 4ab89ee09f..6411bec796 100644
--- a/cypress/e2e/functional/opal/flows/consolidation.flow.ts
+++ b/cypress/e2e/functional/opal/flows/consolidation.flow.ts
@@ -31,6 +31,7 @@ export class ConsolidationFlow {
this.consolidation.selectBusinessUnitIfRequired();
this.consolidation.selectDefendantType(defendantType);
this.consolidation.continueFromSelectBusinessUnit();
+ this.consolidation.waitForAccountSearchScreen(defendantType);
}
/**
@@ -41,18 +42,114 @@ export class ConsolidationFlow {
this.consolidation.clickSearch();
}
+ /** Clears the consolidation account-search form. */
+ public clearConsolidationSearch(): void {
+ log('flow', 'Clearing consolidation account-search form');
+ this.consolidation.clearSearch();
+ }
+
/** Asserts consolidation account search lands on Search tab for Individuals. */
public assertSearchTabForIndividuals(): void {
log('flow', 'Asserting consolidation account search is on Search tab for Individuals');
this.consolidation.assertOnSearchTabForIndividuals();
}
+ /** Asserts the user is on the consolidation business unit and defendant type selection screen. */
+ public assertSelectBusinessUnitScreen(): void {
+ log('flow', 'Asserting consolidation select business unit screen is displayed');
+ this.consolidation.assertOnSelectBusinessUnitScreen();
+ }
+
/** Asserts consolidation account search lands on Search tab for Companies. */
public assertSearchTabForCompanies(): void {
log('flow', 'Asserting consolidation account search is on Search tab for Companies');
this.consolidation.assertOnSearchTabForCompanies();
}
+ /** Asserts the page-header back link is displayed on the consolidation shell. */
+ public assertBackLinkIsDisplayed(): void {
+ log('flow', 'Asserting consolidation page-header back link is displayed');
+ this.consolidation.assertBackLinkIsDisplayed();
+ }
+
+ /** Clicks the page-header back link on the consolidation shell. */
+ public clickBackLink(): void {
+ log('flow', 'Clicking consolidation page-header back link');
+ this.consolidation.clickBackLink();
+ }
+
+ /** Opens the consolidation Results tab. */
+ public openResultsTab(): void {
+ log('flow', 'Opening consolidation Results tab');
+ this.consolidation.openResultsTab();
+ }
+
+ /** Asserts consolidation account search lands on the Results tab. */
+ public assertResultsTab(): void {
+ log('flow', 'Asserting consolidation account search is on the Results tab');
+ this.consolidation.assertOnResultsTab();
+ }
+
+ /** Asserts consolidation account search lands on the Results tab for Individuals with the correct summary values. */
+ public assertResultsTabForIndividuals(): void {
+ log('flow', 'Asserting consolidation account search is on the Results tab for Individuals');
+ this.consolidation.assertOnResultsTabForDefendantType('Individual');
+ }
+
+ /** Asserts consolidation account search lands on the Results tab for Companies with the correct summary values. */
+ public assertResultsTabForCompanies(): void {
+ log('flow', 'Asserting consolidation account search is on the Results tab for Companies');
+ this.consolidation.assertOnResultsTabForDefendantType('Company');
+ }
+
+ /** Asserts the created account number is rendered as a hyperlink in consolidation results. */
+ public assertCreatedAccountLinkIsDisplayed(): void {
+ log('flow', 'Asserting created consolidation result account is displayed as a hyperlink');
+ this.consolidation.assertCreatedAccountLinkIsDisplayed();
+ }
+
+ /** Asserts the consolidation no matching results state is displayed. */
+ public assertNoMatchingResultsState(): void {
+ log('flow', 'Asserting consolidation no matching results state');
+ this.consolidation.assertNoMatchingResultsState();
+ }
+
+ /**
+ * Asserts the consolidation results do not contain the supplied balance.
+ * @param balance - Forbidden rendered balance value.
+ */
+ public assertResultsExcludeBalance(balance: string): void {
+ log('flow', 'Asserting consolidation results exclude balance', { balance });
+ this.consolidation.assertResultsExcludeBalance(balance);
+ }
+
+ /** Clicks the Check your search hyperlink from the consolidation no matching results state. */
+ public clickCheckYourSearchFromNoMatchingResults(): void {
+ log('flow', 'Clicking Check your search from consolidation no matching results state');
+ this.consolidation.clickCheckYourSearchFromNoMatchingResults();
+ }
+
+ /** Opens the created consolidation result account and verifies the new-tab FAE details navigation. */
+ public openCreatedAccountFromResultsInNewTab(): void {
+ log('flow', 'Opening created consolidation result account in a new tab');
+ this.consolidation.openCreatedAccountFromResultsInNewTab();
+ }
+
+ /**
+ * Asserts the consolidation search error page for the given defendant type.
+ * @param defendantType - "Individual" or "Company"
+ */
+ public assertSearchErrorPage(defendantType: ConsolidationDefendantType): void {
+ log('flow', 'Asserting consolidation search error page', { defendantType });
+ this.consolidation.assertSearchErrorPage(defendantType);
+ }
+
+ /** Clicks the back link on the consolidation search error page. */
+ public goBackFromSearchError(): void {
+ log('flow', 'Going back from consolidation search error page');
+ this.consolidation.goBackFromSearchError();
+ }
+
/**
* Enters consolidation account-search details from a two-column data table.
* @param table - Data table in key/value form.
diff --git a/cypress/shared/selectors/consolidation/AccountResults.locators.ts b/cypress/shared/selectors/consolidation/AccountResults.locators.ts
new file mode 100644
index 0000000000..7bef78a735
--- /dev/null
+++ b/cypress/shared/selectors/consolidation/AccountResults.locators.ts
@@ -0,0 +1,43 @@
+export const AccountResultsLocators = {
+ // Results headings and actions
+ resultsHeading: 'h2.govuk-heading-m',
+ addToListButton: 'button.govuk-button[type="button"]',
+ selectedAccountsHint: 'p.govuk-hint',
+ invalidResultsHeading: 'h2.govuk-heading-m',
+ invalidResultsBody: 'p.govuk-body-m',
+ invalidResultsLink: 'p.govuk-body-m a.govuk-link',
+
+ // Results table
+ resultsTable: 'table.govuk-table',
+ resultsTableHeaders: 'table.govuk-table thead th',
+ resultsTableNamedHeaders: 'table.govuk-table thead th[opal-lib-moj-sortable-table-header]',
+ resultsScrollPane: 'opal-lib-custom-horizontal-scroll-pane',
+ resultsPagination: 'opal-lib-moj-pagination, .govuk-pagination, nav.govuk-pagination',
+ resultsTableBody: 'table.govuk-table tbody',
+ resultsRows: 'table.govuk-table tbody > tr.govuk-table__row',
+ resultSelectionCheckboxes: 'table.govuk-table input[type="checkbox"]',
+ resultSelectAllCheckbox: '#defendants-select-all-checkbox',
+ resultAccountLink: 'td#defendantAccountNumber a.govuk-link',
+ resultNameCell: 'td#defendantName',
+ resultAliasesCell: 'td#defendantAliases',
+ resultDateOfBirthCell: 'td#defendantDateOfBirth',
+ resultAddressLine1Cell: 'td#defendantAddressLine1',
+ resultPostcodeCell: 'td#defendantPostcode',
+ resultCollectionOrderCell: 'td#defendantCollectionOrder',
+ resultEnforcementCell: 'td#defendantEnforcement',
+ resultBalanceCell: 'td#defendantBalance',
+ resultPayingParentGuardianCell: 'td#defendantPayingParentGuardian',
+ resultNationalInsuranceNumberCell: 'td#defendantNationalInsuranceNumber',
+ resultRefCell: 'td#defendantRef',
+ resultChecksBulletItems: 'ul.defendant-check-message-list > li',
+ resultTableRow: 'tr.govuk-table__row',
+ addToListErrorMessage: '#defendants-select-all-error-message',
+
+ // Results row helpers
+ resultRowWithAccount: (accountNumber: string) =>
+ `tr.govuk-table__row:has(td#defendantAccountNumber a:contains("${accountNumber}"))`,
+ resultAccountLinkByNumber: (accountNumber: string) =>
+ `td#defendantAccountNumber a.govuk-link:contains("${accountNumber}")`,
+ resultChecksCellByAccountId: (accountId: number | string) => `#defendant-checks-${accountId}`,
+ resultRowCheckboxByAccountId: (accountId: number | string) => `#defendant-select-${accountId}`,
+};
diff --git a/cypress/shared/selectors/consolidation/AccountSearch.locators.ts b/cypress/shared/selectors/consolidation/AccountSearch.locators.ts
index 344ee2a9dd..05e5d2ea81 100644
--- a/cypress/shared/selectors/consolidation/AccountSearch.locators.ts
+++ b/cypress/shared/selectors/consolidation/AccountSearch.locators.ts
@@ -1,5 +1,6 @@
export const AccountSearchLocators = {
heading: 'h1.govuk-heading-l',
+ backLink: 'a.govuk-back-link',
// Summary rows
summaryList: '#consolidateAccSummary',
diff --git a/cypress/shared/selectors/consolidation/ErrorPage.locators.ts b/cypress/shared/selectors/consolidation/ErrorPage.locators.ts
new file mode 100644
index 0000000000..f525e0442f
--- /dev/null
+++ b/cypress/shared/selectors/consolidation/ErrorPage.locators.ts
@@ -0,0 +1,8 @@
+export const ErrorPageLocators = {
+ root: 'app-fines-con-search-error',
+ heading: 'h1.govuk-heading-l',
+ message: 'p.govuk-body',
+ bulletList: 'ul.govuk-list.govuk-list--bullet',
+ bulletItems: 'ul.govuk-list.govuk-list--bullet li',
+ backLink: 'a.govuk-link',
+};
diff --git a/cypress/support/step_definitions/consolidation/consolidation.steps.ts b/cypress/support/step_definitions/consolidation/consolidation.steps.ts
index 684548df21..4b3cca9b27 100644
--- a/cypress/support/step_definitions/consolidation/consolidation.steps.ts
+++ b/cypress/support/step_definitions/consolidation/consolidation.steps.ts
@@ -24,16 +24,91 @@ When('I click Search on consolidation account search', () => {
consolidationFlow().clickConsolidationSearch();
});
+When('I clear the consolidation search', () => {
+ log('step', 'Clearing the consolidation search form');
+ consolidationFlow().clearConsolidationSearch();
+});
+
Then('I am on the consolidation Search tab for Individuals', () => {
log('step', 'Verifying consolidation account search defaults for Individuals');
consolidationFlow().assertSearchTabForIndividuals();
});
+Then('I am on the consolidation business unit and defendant type selection screen', () => {
+ log('step', 'Verifying consolidation business unit and defendant type selection screen');
+ consolidationFlow().assertSelectBusinessUnitScreen();
+});
+
Then('I am on the consolidation Search tab for Companies', () => {
log('step', 'Verifying consolidation account search defaults for Companies');
consolidationFlow().assertSearchTabForCompanies();
});
+Then('the consolidation page header back link is displayed', () => {
+ log('step', 'Verifying consolidation page header back link is displayed');
+ consolidationFlow().assertBackLinkIsDisplayed();
+});
+
+When('I click the consolidation page header back link', () => {
+ log('step', 'Clicking the consolidation page header back link');
+ consolidationFlow().clickBackLink();
+});
+
+When('I open the consolidation Results tab', () => {
+ log('step', 'Opening the consolidation Results tab');
+ consolidationFlow().openResultsTab();
+});
+
+Then('I am on the consolidation Results tab', () => {
+ log('step', 'Verifying consolidation Results tab is active');
+ consolidationFlow().assertResultsTab();
+});
+
+Then('I am on the consolidation Results tab for Individuals', () => {
+ log('step', 'Verifying consolidation Results tab summary for Individuals');
+ consolidationFlow().assertResultsTabForIndividuals();
+});
+
+Then('I am on the consolidation Results tab for Companies', () => {
+ log('step', 'Verifying consolidation Results tab summary for Companies');
+ consolidationFlow().assertResultsTabForCompanies();
+});
+
+Then('the created consolidation result account number is displayed as a hyperlink', () => {
+ log('step', 'Verifying created consolidation result account number is displayed as a hyperlink');
+ consolidationFlow().assertCreatedAccountLinkIsDisplayed();
+});
+
+Then('I see the consolidation no matching results state', () => {
+ log('step', 'Verifying consolidation no matching results state');
+ consolidationFlow().assertNoMatchingResultsState();
+});
+
+Then('the consolidation results exclude accounts with a balance of {string}', (balance: string) => {
+ log('step', 'Verifying consolidation results exclude accounts with balance', { balance });
+ consolidationFlow().assertResultsExcludeBalance(balance);
+});
+
+When('I click Check your search on consolidation no matching results', () => {
+ log('step', 'Clicking Check your search on consolidation no matching results');
+ consolidationFlow().clickCheckYourSearchFromNoMatchingResults();
+});
+
+When('I open the created consolidation result account in a new tab', () => {
+ log('step', 'Opening created consolidation result account in a new tab');
+ consolidationFlow().openCreatedAccountFromResultsInNewTab();
+});
+
+Then('I see the consolidation search error page for {string}', (defendantType: ConsolidationDefendantType) => {
+ log('step', 'Verifying consolidation search error page', { defendantType });
+ consolidationFlow().assertSearchErrorPage(defendantType);
+});
+
+When('I go back from the consolidation search error page', () => {
+ log('step', 'Going back from the consolidation search error page');
+ consolidationFlow().goBackFromSearchError();
+});
+
When('I enter the following consolidation search details:', (table: DataTable) => {
log('step', 'Entering consolidation search details');
consolidationFlow().enterConsolidationSearchDetails(table);
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.html
index a4acdada83..1c79ed1bf8 100644
--- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.html
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.html
@@ -62,11 +62,12 @@ Consolidate accounts
>
}
@case ('results') {
-
+
}
@case ('for-consolidation') {
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.spec.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.spec.ts
index 3478f5484d..a81788e388 100644
--- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.spec.ts
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.spec.ts
@@ -6,6 +6,7 @@ import { FinesConStore } from '../../stores/fines-con.store';
import { FinesConStoreType } from '../../stores/types/fines-con-store.type';
import { OPAL_FINES_BUSINESS_UNIT_REF_DATA_MOCK } from '@services/fines/opal-fines-service/mocks/opal-fines-business-unit-ref-data.mock';
import { OPAL_FINES_DEFENDANT_ACCOUNT_SEARCH_PARAMS_DEFAULTS } from '@services/fines/opal-fines-service/constants/opal-fines-defendant-account-search-params-defaults.constant';
+import { Observable, of } from 'rxjs';
describe('FinesConConsolidateAccComponent', () => {
let component: FinesConConsolidateAccComponent;
@@ -13,7 +14,8 @@ describe('FinesConConsolidateAccComponent', () => {
let mockRouter: { navigate: ReturnType };
let mockActivatedRoute: {
parent: Record;
- snapshot: { data: Record };
+ fragment: Observable;
+ snapshot: { data: Record; fragment?: string | null };
};
let finesConStore: InstanceType;
@@ -23,7 +25,9 @@ describe('FinesConConsolidateAccComponent', () => {
const parentActivatedRoute = {};
mockActivatedRoute = {
parent: parentActivatedRoute,
+ fragment: of('search'),
snapshot: {
+ fragment: null,
data: {
businessUnits: OPAL_FINES_BUSINESS_UNIT_REF_DATA_MOCK,
},
@@ -56,6 +60,20 @@ describe('FinesConConsolidateAccComponent', () => {
expect(finesConStore.activeTab()).toBe('results');
});
+ it('should not navigate to update fragment when selected tab matches current fragment', () => {
+ mockActivatedRoute.snapshot.fragment = 'results';
+
+ component.handleTabSwitch('results');
+
+ expect(finesConStore.activeTab()).toBe('results');
+ expect(mockRouter.navigate).not.toHaveBeenCalledWith([], {
+ relativeTo: mockActivatedRoute,
+ fragment: 'results',
+ queryParamsHandling: 'preserve',
+ replaceUrl: true,
+ });
+ });
+
it('should store transformed search payload for results tab', () => {
const activeTabSpy = vi.spyOn(finesConStore, 'setActiveTab');
const payload = {
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.spec.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.spec.ts
index 0a59855450..b912dc2bf2 100644
--- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.spec.ts
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.spec.ts
@@ -2,10 +2,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
+import { Router } from '@angular/router';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { FinesConSearchAccountFormComponent } from './fines-con-search-account-form.component';
import { FinesConStore } from '../../../stores/fines-con.store';
import { FinesConStoreType } from '../../../stores/types/fines-con-store.type';
+import { FINES_CON_ROUTING_PATHS } from '../../../routing/constants/fines-con-routing-paths.constant';
+import { AbstractFormBaseComponent } from '@hmcts/opal-frontend-common/components/abstract/abstract-form-base';
describe('FinesConSearchAccountFormComponent', () => {
let component: FinesConSearchAccountFormComponent;
@@ -16,11 +19,18 @@ describe('FinesConSearchAccountFormComponent', () => {
const activatedRouteSpy = {
params: { subscribe: () => {} },
queryParams: { subscribe: () => {} },
+ parent: {},
+ };
+ const routerSpy = {
+ navigate: vi.fn().mockName('Router.navigate'),
};
await TestBed.configureTestingModule({
imports: [ReactiveFormsModule, FinesConSearchAccountFormComponent],
- providers: [{ provide: ActivatedRoute, useValue: activatedRouteSpy }],
+ providers: [
+ { provide: ActivatedRoute, useValue: activatedRouteSpy },
+ { provide: Router, useValue: routerSpy },
+ ],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
@@ -87,4 +97,54 @@ describe('FinesConSearchAccountFormComponent', () => {
expect(component.form.get('fcon_search_account_number')?.value).toBeNull();
});
+
+ it('should persist form and navigate to search error page when conflicting criteria are submitted', () => {
+ const router = TestBed.inject(Router);
+ const updateTemporarySpy = vi.spyOn(finesConStore, 'updateSearchAccountFormTemporary');
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const submitEmitSpy = vi.spyOn(component['formSubmit'], 'emit');
+
+ component.form.patchValue({
+ fcon_search_account_number: '12345678',
+ fcon_search_account_national_insurance_number: 'AB123456C',
+ });
+
+ component.handleFormSubmit(new SubmitEvent('submit'));
+
+ expect(updateTemporarySpy).toHaveBeenCalledWith(component.form.value);
+ expect(router.navigate).toHaveBeenCalledWith([FINES_CON_ROUTING_PATHS.children.searchError], {
+ relativeTo: component['activatedRoute'].parent,
+ });
+ expect(submitEmitSpy).not.toHaveBeenCalled();
+ });
+
+ it('should not call super.handleFormSubmit when form is empty (formEmpty)', () => {
+ const superSubmitSpy = vi.spyOn(AbstractFormBaseComponent.prototype, 'handleFormSubmit');
+
+ component.form.reset();
+ component.form.updateValueAndValidity({ emitEvent: false });
+ expect(component.form.errors?.['formEmpty']).toBe(true);
+
+ component.handleFormSubmit(new SubmitEvent('submit'));
+
+ expect(superSubmitSpy).not.toHaveBeenCalled();
+ });
+
+ it('should call super.handleFormSubmit when form submission is valid', () => {
+ const superSubmitSpy = vi.spyOn(AbstractFormBaseComponent.prototype, 'handleFormSubmit');
+ const router = TestBed.inject(Router);
+
+ component.form.patchValue({
+ fcon_search_account_number: '12345678',
+ });
+ component.form.updateValueAndValidity({ emitEvent: false });
+ expect(component.form.errors).toBeNull();
+
+ component.handleFormSubmit(new SubmitEvent('submit'));
+
+ expect(superSubmitSpy).toHaveBeenCalled();
+ expect(router.navigate).not.toHaveBeenCalledWith([FINES_CON_ROUTING_PATHS.children.searchError], {
+ relativeTo: component['activatedRoute'].parent,
+ });
+ });
});
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.ts
index 9c8643ff30..8cb07a59e3 100644
--- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.ts
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.ts
@@ -16,6 +16,7 @@ import { consolidateSearchAccountFormValidator } from './validators/fines-con-se
import { FinesConStore } from '../../../stores/fines-con.store';
import { FINES_CON_SEARCH_ACCOUNT_FORM_INDIVIDUALS_FIELD_ERRORS } from './fines-con-search-account-form-individuals/constants/fines-con-search-account-form-individuals-field-errors.constant';
import { FINES_CON_SEARCH_ACCOUNT_FORM_COMPANIES_FIELD_ERRORS } from './fines-con-search-account-form-companies/constants/fines-con-search-account-form-companies-field-errors.constant';
+import { FINES_CON_ROUTING_PATHS } from '../../../routing/constants/fines-con-routing-paths.constant';
import {
ALPHANUMERIC_WITH_HYPHENS_SPACES_APOSTROPHES_DOT_PATTERN,
ACCOUNT_NUMBER_PATTERN,
@@ -57,6 +58,7 @@ const ALPHANUMERIC_WITH_HYPHENS_SPACES_APOSTROPHES_DOT_VALIDATOR = patternValida
})
export class FinesConSearchAccountFormComponent extends AbstractFormBaseComponent {
private readonly finesConStore = inject(FinesConStore);
+ private readonly finesConRoutingPaths = FINES_CON_ROUTING_PATHS;
@Output() protected override formSubmit = new EventEmitter();
override fieldErrors: IFinesConSearchAccountFieldErrors = {
@@ -66,6 +68,30 @@ export class FinesConSearchAccountFormComponent extends AbstractFormBaseComponen
};
@Input({ required: true }) defendantType: FinesConDefendant = 'individual';
+ /**
+ * Pre-submit guard for mixed quick + advanced criteria.
+ * Navigates to the search error screen when mutually exclusive criteria are combined.
+ *
+ * @returns `true` when submission should continue, otherwise `false`.
+ */
+ private handleFormSubmission(): boolean {
+ this.form.updateValueAndValidity({ emitEvent: false });
+
+ if (this.form.errors?.['atLeastOneCriteriaRequired']) {
+ this.setSearchAccountTemporary();
+ this['router'].navigate([this.finesConRoutingPaths.children.searchError], {
+ relativeTo: this['activatedRoute'].parent,
+ });
+ return false;
+ }
+
+ if (this.form.errors?.['formEmpty']) {
+ return false;
+ }
+
+ return true;
+ }
+
/**
* Creates the form with quick search fields and detail search fields.
*/
@@ -136,6 +162,19 @@ export class FinesConSearchAccountFormComponent extends AbstractFormBaseComponen
super.ngOnInit();
}
+ /**
+ * Runs pre-submit guards, then delegates to the base submit flow.
+ *
+ * @param event - Native submit event.
+ */
+ public override handleFormSubmit(event: SubmitEvent): void {
+ if (!this.handleFormSubmission()) {
+ return;
+ }
+
+ super.handleFormSubmit(event);
+ }
+
/**
* Persists in-progress search criteria when the search tab is unmounted
* (for example when switching to another tab).
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component.html
new file mode 100644
index 0000000000..14970a4aaa
--- /dev/null
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component.html
@@ -0,0 +1,15 @@
+
+ There is a problem
+
+ Reference data and account information cannot be entered together when searching for an account. Search using
+ either:
+
+
+ - account number, or
+ @if (defendantType === 'individual') {
+ - National Insurance number, or
+ }
+ - advanced search
+
+ Go back
+
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component.spec.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component.spec.ts
new file mode 100644
index 0000000000..8d1b1dd4ae
--- /dev/null
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component.spec.ts
@@ -0,0 +1,59 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { FinesConSearchErrorComponent } from './fines-con-search-error.component';
+import { ActivatedRoute, Router } from '@angular/router';
+import { FinesConStore } from '../../stores/fines-con.store';
+import { FINES_CON_ROUTING_PATHS } from '../../routing/constants/fines-con-routing-paths.constant';
+
+describe('FinesConSearchErrorComponent', () => {
+ let component: FinesConSearchErrorComponent;
+ let fixture: ComponentFixture;
+ let routerSpy: { navigate: ReturnType };
+ let finesConStore: InstanceType;
+
+ beforeEach(async () => {
+ routerSpy = {
+ navigate: vi.fn().mockName('Router.navigate'),
+ };
+
+ await TestBed.configureTestingModule({
+ imports: [FinesConSearchErrorComponent],
+ providers: [
+ { provide: Router, useValue: routerSpy },
+ { provide: ActivatedRoute, useValue: { parent: {} } },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(FinesConSearchErrorComponent);
+ component = fixture.componentInstance;
+ finesConStore = TestBed.inject(FinesConStore);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should show National Insurance number guidance for individual defendant type', () => {
+ vi.spyOn(finesConStore, 'getDefendantType').mockReturnValue('individual');
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.textContent).toContain('National Insurance number, or');
+ });
+
+ it('should not show National Insurance number guidance for company defendant type', () => {
+ vi.spyOn(finesConStore, 'getDefendantType').mockReturnValue('company');
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.textContent).not.toContain('National Insurance number, or');
+ });
+
+ it('should navigate back to consolidate accounts search when goBack is called', () => {
+ const event = new Event('click');
+ component.goBack(event);
+
+ expect(routerSpy.navigate).toHaveBeenCalledWith([FINES_CON_ROUTING_PATHS.children.consolidateAcc], {
+ relativeTo: component['activatedRoute'].parent,
+ fragment: 'search',
+ });
+ });
+});
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component.ts
new file mode 100644
index 0000000000..75089cfd88
--- /dev/null
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component.ts
@@ -0,0 +1,37 @@
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FinesConDefendant } from '../../types/fines-con-defendant.type';
+import { FinesConStore } from '../../stores/fines-con.store';
+import { Router, ActivatedRoute } from '@angular/router';
+import { FINES_CON_ROUTING_PATHS } from '../../routing/constants/fines-con-routing-paths.constant';
+
+@Component({
+ selector: 'app-fines-con-search-error',
+ standalone: true,
+ imports: [CommonModule],
+ templateUrl: './fines-con-search-error.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class FinesConSearchErrorComponent {
+ private readonly router = inject(Router);
+ private readonly activatedRoute = inject(ActivatedRoute);
+ private readonly finesConStore = inject(FinesConStore);
+
+ /**
+ * Gets the current defendant type from the consolidation store.
+ */
+ public get defendantType(): FinesConDefendant {
+ return this.finesConStore.getDefendantType() as FinesConDefendant;
+ }
+
+ /**
+ * Navigates back to the consolidate accounts page (Search tab).
+ */
+ public goBack(event: Event): void {
+ event.preventDefault();
+ this.router.navigate([FINES_CON_ROUTING_PATHS.children.consolidateAcc], {
+ relativeTo: this.activatedRoute.parent,
+ fragment: 'search',
+ });
+ }
+}
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html
index 1b5a133618..289afa5c5e 100644
--- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html
+++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html
@@ -21,6 +21,7 @@ Select accounts to consolidate
|