Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ async function signIn(page: Page): Promise<void> {
await expect(page).toHaveURL('/dashboard');
}

async function mockClientWithoutAccounts(page: Page): Promise<void> {
async function mockClient(page: Page): Promise<void> {
await page.route('**/api/v1/clients/2001**', async (route) => {
await route.fulfill({
status: 200,
Expand All @@ -88,9 +88,44 @@ async function mockClientWithoutAccounts(page: Page): Promise<void> {
});
}

test('loan account rows show the principal returned by the accounts endpoint', async ({ page }) => {
Comment thread
Aman-Mittal marked this conversation as resolved.
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 });
Expand Down
2 changes: 1 addition & 1 deletion e2e/client-load-error.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
28 changes: 27 additions & 1 deletion src/app/features/clients/client-view.component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 [
{
Expand Down
15 changes: 12 additions & 3 deletions src/app/features/clients/client-view.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -764,8 +767,14 @@ export type ClientTab = (typeof CLIENT_TAB)[keyof typeof CLIENT_TAB];
{{ 'LOANS.PRINCIPAL' | appTranslate }}
</th>
<td cdk-cell *cdkCellDef="let account">
{{ 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 {
—
}
</td>
</ng-container>

Expand Down Expand Up @@ -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<GetClientsLoanAccounts[]>([]);
readonly loanAccounts = signal<ClientLoanAccountRow[]>([]);
readonly savingsAccounts = signal<GetClientsSavingsAccounts[]>([]);
readonly shareAccounts = signal<ShareAccountRow[]>([]);

Expand Down
Loading