diff --git a/e2e/client-empty-account-cta.spec.ts b/e2e/client-accounts.spec.ts similarity index 74% rename from e2e/client-empty-account-cta.spec.ts rename to e2e/client-accounts.spec.ts index cdb61b9bb..485089c0a 100644 --- a/e2e/client-empty-account-cta.spec.ts +++ b/e2e/client-accounts.spec.ts @@ -63,7 +63,7 @@ async function signIn(page: Page): Promise { await expect(page).toHaveURL('/dashboard'); } -async function mockClientWithoutAccounts(page: Page): Promise { +async function mockClient(page: Page): Promise { await page.route('**/api/v1/clients/2001**', async (route) => { await route.fulfill({ status: 200, @@ -88,9 +88,44 @@ async function mockClientWithoutAccounts(page: Page): Promise { }); } +test('loan account rows show the principal returned by the accounts endpoint', async ({ page }) => { + await signIn(page); + await mockClient(page); + await page.route('**/api/v1/clients/2001/accounts**', (route) => + route.fulfill({ + json: { + loanAccounts: [ + { + id: 10, + accountNo: '000000010', + productName: 'Synthetic Loan', + originalLoan: 88_888, + currency: { code: 'USD', displaySymbol: '$' }, + status: { id: 300, value: 'Active' }, + }, + { id: 11, accountNo: '000000011', originalLoan: 0, currency: { displaySymbol: '$' } }, + { id: 12, accountNo: '000000012', currency: { displaySymbol: '$' } }, + ], + savingsAccounts: [], + }, + }), + ); + await page.goto(CLIENT_URL); + await page.getByRole('tab', { name: 'Loan Accounts', exact: true }).click({ force: true }); + + const loanRow = page.getByRole('row').filter({ hasText: '000000010' }); + await expect(loanRow.getByRole('cell', { name: '$ 88,888.00', exact: true })).toBeVisible(); + await expect( + page.getByRole('row').filter({ hasText: '000000011' }).locator('.cdk-column-principal'), + ).toHaveText('$ 0.00'); + await expect( + page.getByRole('row').filter({ hasText: '000000012' }).locator('.cdk-column-principal'), + ).toHaveText('—'); +}); + test('empty account tabs offer the matching creation action', async ({ page }, testInfo) => { await signIn(page); - await mockClientWithoutAccounts(page); + await mockClient(page); await page.goto(CLIENT_URL); await page.getByRole('tab', { name: 'Savings Accounts', exact: true }).click({ force: true }); diff --git a/e2e/client-load-error.spec.ts b/e2e/client-load-error.spec.ts index 22862af06..fc9795fd9 100644 --- a/e2e/client-load-error.spec.ts +++ b/e2e/client-load-error.spec.ts @@ -23,7 +23,7 @@ * A blank screen after an auto-dismissing toast is indistinguishable from the app doing nothing, * which is what these mocks are steered into: a 404/403 (the record cannot exist for this * caller) and a 500 (a transient failure worth retrying). Companion to - * `client-empty-account-cta.spec.ts`, which covers the same screen's empty-but-loaded states. + * `client-accounts.spec.ts`, which covers the same screen's empty-but-loaded states. * * npx playwright test e2e/client-load-error.spec.ts --project=mocked */ diff --git a/src/app/features/clients/client-view.component.test.ts b/src/app/features/clients/client-view.component.test.ts index 8a5214cf4..1b0ae6cdc 100644 --- a/src/app/features/clients/client-view.component.test.ts +++ b/src/app/features/clients/client-view.component.test.ts @@ -20,7 +20,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CLIENT_TAB, ClientViewComponent } from './client-view.component'; +import { + CLIENT_TAB, + ClientViewComponent, + type ClientLoanAccountRow, +} from './client-view.component'; import { ClientService, NotesService, @@ -218,6 +222,28 @@ describe('ClientViewComponent', () => { }); }); + it('renders loan principals from the client accounts response, including zero and missing values', () => { + const loanAccounts: ClientLoanAccountRow[] = [ + { id: 1, accountNo: 'L001', originalLoan: 88_888, currency: { displaySymbol: '$' } }, + { id: 2, accountNo: 'L002', originalLoan: 0, currency: { displaySymbol: '$' } }, + { id: 3, accountNo: 'L003', currency: { displaySymbol: '$' } }, + ]; + clientServiceSpy.getClientsClientIdAccounts.mockReturnValue( + // The generated spy selects the HttpEvent overload; the fixtures above remain typed. + of({ loanAccounts, savingsAccounts: [] }) as any, + ); + + component.loadClientAccounts(); + component.onTabChange(CLIENT_TAB.loans); + fixture.detectChanges(); + + const cells = Array.from( + fixture.nativeElement.querySelectorAll('td.cdk-column-principal'), + (cell: Element) => cell.textContent?.trim(), + ); + expect(cells).toEqual(['$ 88,888.00', '$ 0.00', '—']); + }); + describe('empty account tabs', () => { for (const { tab, testId, permission } of [ { diff --git a/src/app/features/clients/client-view.component.ts b/src/app/features/clients/client-view.component.ts index b8981fdd2..55219a436 100644 --- a/src/app/features/clients/client-view.component.ts +++ b/src/app/features/clients/client-view.component.ts @@ -90,6 +90,9 @@ import { IonSegmentButton, } from '@ionic/angular/standalone'; +// The client accounts response includes amounts omitted by the generated model. +export type ClientLoanAccountRow = GetClientsLoanAccounts & { originalLoan?: number }; + /** Shape the client action dialog resolves with. */ interface ClientActionResult { actionDate: Date; @@ -764,8 +767,14 @@ export type ClientTab = (typeof CLIENT_TAB)[keyof typeof CLIENT_TAB]; {{ 'LOANS.PRINCIPAL' | appTranslate }} - {{ account.currency?.displaySymbol }} - {{ account.originalPrincipal || 0 | number: '1.2-2' }} + @if ( + account.originalLoan !== undefined && account.originalLoan !== null + ) { + {{ account.currency?.displaySymbol }} + {{ account.originalLoan | number: '1.2-2' }} + } @else { + — + } @@ -1181,7 +1190,7 @@ export class ClientViewComponent implements OnInit, OnDestroy { * else (a 500, a dropped connection), where retrying the same request can succeed. */ readonly loadError = signal<'not-found' | 'failed' | null>(null); - readonly loanAccounts = signal([]); + readonly loanAccounts = signal([]); readonly savingsAccounts = signal([]); readonly shareAccounts = signal([]);