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
67 changes: 67 additions & 0 deletions playwright/tests/clients/lifecycle/activate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Copyright since 2026 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { test, expect } from '../../../fixtures/test-fixtures';
import { createTestClient } from '../../../factories/client.factory';
import { ClientViewPage } from '../../../pages/client-view.page';
import { ActivateClientPage } from '../../../pages/client-actions/activate-client.page';

/**
* WA-3.3 client lifecycle happy path: Pending → Active.
*
* Pattern:
* 1. Factory creates the predecessor-state client (pending).
* 2. UI drives the transition via `chooseAction('Activate')`.
* 3. Assert snackbar copy + `getClient().status.value` +
* timeline entry (`activatedOnDate`).
*/
const SUBMITTED_ON_DATE = '01 January 2024';
const ACTIVATION_DATE = '02 January 2024';

test.describe('Client lifecycle · Activate (Pending → Active)', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const creds = localStorage.getItem('mifosXCredentials');
if (creds) {
sessionStorage.setItem('mifosXCredentials', creds);
}
});
});

test('activates a pending client from the client actions flow', async ({
page,
fineractApi,
apiSetup,
cleanupGuard
}) => {
const client = await createTestClient(apiSetup, cleanupGuard, {
submittedOnDate: SUBMITTED_ON_DATE
});

const clientViewPage = new ClientViewPage(page, client.resourceId);
const activatePage = new ActivateClientPage(page, client.resourceId);

await clientViewPage.navigate();
await clientViewPage.waitForLoad();
await clientViewPage.chooseAction('Activate');

await activatePage.waitForLoad();
await activatePage.submitActivation({ activationDate: ACTIVATION_DATE });

await clientViewPage.waitForLoad();
await expect(clientViewPage.successSnackbar).toContainText('Client activated successfully.');

const clientDetails = await fineractApi.getClient(client.resourceId);
expect(clientDetails.status?.value).toBe('Active');
expect(clientDetails.timeline?.activatedOnDate).toEqual([
2024,
1,
2
]);
});
});
85 changes: 85 additions & 0 deletions playwright/tests/clients/lifecycle/reactivate-after-close.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Copyright since 2026 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { test, expect } from '../../../fixtures/test-fixtures';
import { ClientViewPage } from '../../../pages/client-view.page';
import { ReactivateClientPage } from '../../../pages/client-actions/reactivate-client.page';

/**
* WA-3.3 client lifecycle happy path: Closed → Pending (Reactivate).
*
* Pattern:
* 1. API creates an active client, then closes it, so the client
* enters the predecessor `Closed` state. This flow deliberately
* bypasses `createTestClient` because active/closed clients
* cannot be hard-deleted by the cleanup guard.
* 2. UI drives the transition via `chooseAction('Reactivate')`.
* 3. Assert snackbar copy + `getClient().status.value` returns to
* `Pending` (Fineract's documented reactivate destination — a
* subsequent Activate is required to reach `Active`) + the
* `closedOnDate` timeline entry is cleared.
*/
const SUBMITTED_ON_DATE = '01 January 2024';
const ACTIVATION_DATE = '02 January 2024';
const CLOSURE_DATE = '03 January 2024';
const REACTIVATION_DATE = '04 January 2024';
const CLOSURE_REASON_NAME = 'E2E Close Client Reason';

test.describe('Client lifecycle · Reactivate (Closed → Pending)', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const creds = localStorage.getItem('mifosXCredentials');
if (creds) {
sessionStorage.setItem('mifosXCredentials', creds);
}
});
});

test('reactivates a closed client from the client actions flow', async ({ page, fineractApi }) => {
const closureReason = await fineractApi.ensureClientClosureReason(CLOSURE_REASON_NAME);
const officeId = await fineractApi.getFirstOfficeId();

const uniqueSuffix = Date.now();
const createClientResponse = await fineractApi.createActiveClient(officeId, {
firstname: `Reactivate${uniqueSuffix}`,
lastname: 'Client',
submittedOnDate: SUBMITTED_ON_DATE,
activationDate: ACTIVATION_DATE
});

const clientId = createClientResponse.resourceId;
await fineractApi.closeClient(clientId, closureReason.id, CLOSURE_DATE);

const clientViewPage = new ClientViewPage(page, clientId);
const reactivatePage = new ReactivateClientPage(page, clientId);

await clientViewPage.navigate();
await clientViewPage.waitForLoad();
await clientViewPage.chooseAction('Reactivate');

await reactivatePage.waitForLoad();
await reactivatePage.submitReactivation({ reactivationDate: REACTIVATION_DATE });

await clientViewPage.waitForLoad();
await expect(clientViewPage.successSnackbar).toContainText('Client reactivated successfully.');

const clientDetails = await fineractApi.getClient(clientId);
// In Fineract, reactivating a closed client moves it back to
// `Pending` state — a subsequent Activate action is required to
// return to `Active`. The Pending status combined with the cleared
// `closedOnDate` timeline entry is the definitive evidence that
// the reactivate command was recorded server-side.
expect(clientDetails.status?.value).toBe('Pending');
expect(clientDetails.timeline?.closedOnDate).toBeFalsy();
expect(clientDetails.timeline?.submittedOnDate).toEqual([
2024,
1,
1
]);
});
});
79 changes: 79 additions & 0 deletions playwright/tests/clients/lifecycle/reject.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Copyright since 2026 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { test, expect } from '../../../fixtures/test-fixtures';
import { createTestClient } from '../../../factories/client.factory';
import { ClientViewPage } from '../../../pages/client-view.page';
import { RejectClientPage } from '../../../pages/client-actions/reject-client.page';

/**
* WA-3.3 client lifecycle happy path: Pending → Rejected.
*
* Pattern:
* 1. Factory creates a pending client.
* 2. UI drives the transition via `chooseAction('Reject')`.
* 3. Assert snackbar copy + `getClient().status.value` +
* timeline entry (`rejectedOnDate`).
Comment thread
IOhacker marked this conversation as resolved.
*/
const SUBMITTED_ON_DATE = '01 January 2024';
const REJECTION_DATE = '02 January 2024';
const REJECTION_REASON_NAME = 'E2E Reject Client Reason';

test.describe('Client lifecycle · Reject (Pending → Rejected)', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const creds = localStorage.getItem('mifosXCredentials');
if (creds) {
sessionStorage.setItem('mifosXCredentials', creds);
}
});
});

test('rejects a pending client from the client actions flow', async ({
page,
fineractApi,
apiSetup,
cleanupGuard
}) => {
await fineractApi.ensureClientRejectionReason(REJECTION_REASON_NAME);

const client = await createTestClient(apiSetup, cleanupGuard, {
submittedOnDate: SUBMITTED_ON_DATE
});

const clientViewPage = new ClientViewPage(page, client.resourceId);
const rejectPage = new RejectClientPage(page, client.resourceId);

await clientViewPage.navigate();
await clientViewPage.waitForLoad();
await clientViewPage.chooseAction('Reject');

await rejectPage.waitForLoad();
await rejectPage.submitRejection({
rejectionDate: REJECTION_DATE,
reasonName: REJECTION_REASON_NAME
});

await clientViewPage.waitForLoad();
await expect(clientViewPage.successSnackbar).toContainText('Client rejected successfully.');

const clientDetails = await fineractApi.getClient(client.resourceId);
expect(clientDetails.status?.value).toBe('Rejected');
// Fineract's GET /clients/{id} response does not surface `rejectedOnDate`
// in the timeline payload for rejected clients (verified against the
// running backend on 2026-07-15). The status transition itself is the
// definitive evidence of the reject action; we still assert that the
// pre-existing `submittedOnDate` timeline entry is preserved so any
// future regression that wipes the timeline object is caught.
expect(clientDetails.timeline?.submittedOnDate).toEqual([
2024,
1,
1
]);
});
});
77 changes: 77 additions & 0 deletions playwright/tests/clients/lifecycle/undo-rejection.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Copyright since 2026 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { test, expect } from '../../../fixtures/test-fixtures';
import { createTestClient } from '../../../factories/client.factory';
import { ClientViewPage } from '../../../pages/client-view.page';
import { ROUTES } from '../../../config/routes';

/**
* WA-3.3 client lifecycle happy path: Rejected → Pending (Undo Rejection).
*
* Pattern:
* 1. Factory creates a pending client; API immediately rejects it so
* the client enters the predecessor `Rejected` state.
* 2. UI drives the transition via `chooseAction('Undo Rejection')`.
* Undo Rejection carries a single `reopenedDate` control and has
* no dedicated page object yet, so the field is driven inline.
* 3. Assert snackbar copy + `getClient().status.value` returns to
* `Pending` + `timeline.rejectedOnDate` is cleared.
*/
const SUBMITTED_ON_DATE = '01 January 2024';
const REJECTION_DATE = '02 January 2024';
const REOPENED_DATE = '03 January 2024';
const REJECTION_REASON_NAME = 'E2E Reject Client Reason';

test.describe('Client lifecycle · Undo Rejection (Rejected → Pending)', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const creds = localStorage.getItem('mifosXCredentials');
if (creds) {
sessionStorage.setItem('mifosXCredentials', creds);
}
});
});

test('undoes a rejection from the client actions flow', async ({ page, fineractApi, apiSetup, cleanupGuard }) => {
const rejectionReason = await fineractApi.ensureClientRejectionReason(REJECTION_REASON_NAME);

const client = await createTestClient(apiSetup, cleanupGuard, {
submittedOnDate: SUBMITTED_ON_DATE
});

// Predecessor state: reject through the API so the UI transition
// under test is Undo Rejection, not the initial rejection.
await fineractApi.rejectClient(client.resourceId, rejectionReason.id, REJECTION_DATE);

const clientViewPage = new ClientViewPage(page, client.resourceId);

await clientViewPage.navigate();
await clientViewPage.waitForLoad();
await clientViewPage.chooseAction('Undo Rejection');

await expect(page).toHaveURL(new RegExp(`${ROUTES.clientAction(client.resourceId, 'Undo Rejection')}$`));

const reopenedDateInput = page.locator('input[formcontrolname="reopenedDate"]');
await reopenedDateInput.waitFor({ state: 'visible', timeout: 30000 });
await reopenedDateInput.fill(REOPENED_DATE);
await reopenedDateInput.blur();

const confirmButton = page.getByRole('button', { name: 'Confirm' });
await expect(confirmButton).toBeEnabled();
await confirmButton.click();

await clientViewPage.waitForLoad();
await expect(clientViewPage.successSnackbar).toContainText('Client rejection undone successfully.');

const clientDetails = await fineractApi.getClient(client.resourceId);
expect(clientDetails.status?.value).toBe('Pending');
// Fineract clears the rejection timestamp when a rejection is undone.
expect(clientDetails.timeline?.rejectedOnDate).toBeFalsy();
});
});
79 changes: 79 additions & 0 deletions playwright/tests/clients/lifecycle/withdraw.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Copyright since 2026 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { test, expect } from '../../../fixtures/test-fixtures';
import { createTestClient } from '../../../factories/client.factory';
import { ClientViewPage } from '../../../pages/client-view.page';
import { WithdrawClientPage } from '../../../pages/client-actions/withdraw-client.page';

/**
* WA-3.3 client lifecycle happy path: Pending → Withdrawn.
*
* Pattern:
* 1. Factory creates a pending client.
* 2. UI drives the transition via `chooseAction('Withdraw')`.
* 3. Assert snackbar copy + `getClient().status.value` +
* timeline entry (`withdrawnOnDate`).
*/
const SUBMITTED_ON_DATE = '01 January 2024';
const WITHDRAWAL_DATE = '02 January 2024';
const WITHDRAWAL_REASON_NAME = 'E2E Withdraw Client Reason';

test.describe('Client lifecycle · Withdraw (Pending → Withdrawn)', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const creds = localStorage.getItem('mifosXCredentials');
if (creds) {
sessionStorage.setItem('mifosXCredentials', creds);
}
});
});

test('withdraws a pending client from the client actions flow', async ({
page,
fineractApi,
apiSetup,
cleanupGuard
}) => {
await fineractApi.ensureClientWithdrawalReason(WITHDRAWAL_REASON_NAME);

const client = await createTestClient(apiSetup, cleanupGuard, {
submittedOnDate: SUBMITTED_ON_DATE
});

const clientViewPage = new ClientViewPage(page, client.resourceId);
const withdrawPage = new WithdrawClientPage(page, client.resourceId);

await clientViewPage.navigate();
await clientViewPage.waitForLoad();
await clientViewPage.chooseAction('Withdraw');

await withdrawPage.waitForLoad();
await withdrawPage.submitWithdrawal({
withdrawalDate: WITHDRAWAL_DATE,
reasonName: WITHDRAWAL_REASON_NAME
});

await clientViewPage.waitForLoad();
await expect(clientViewPage.successSnackbar).toContainText('Client withdrawn successfully.');

const clientDetails = await fineractApi.getClient(client.resourceId);
expect(clientDetails.status?.value).toBe('Withdrawn');
// Fineract's GET /clients/{id} response does not surface `withdrawnOnDate`
// in the timeline payload for withdrawn clients (verified against the
// running backend on 2026-07-15). The status transition itself is the
// definitive evidence of the withdraw action; we still assert that the
// pre-existing `submittedOnDate` timeline entry is preserved so any
// future regression that wipes the timeline object is caught.
expect(clientDetails.timeline?.submittedOnDate).toEqual([
2024,
1,
1
]);
});
});
Loading