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
49 changes: 41 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ Mifos® X Web App is a modern single-page application (SPA) built on top of the
- [OIDC](#oidc-settings)
- [External National ID](#external-national-id-system-integration)
- [Interbank Transfers](#interbank-transfers-settings)
- [Mifos Copilot](#mifos-copilot-settings)
- [Remittance Module](#remittance-module-settings)
- [Client Data Masking](#client-data-masking-example)
- [Production Mode](#production-mode)
- [Interbank Transfer Menu](#interbank-transfer-menu)
- [Role-Based Access Control](#role-based-access-control-rbac)
- [Releases](#releases)
Expand Down Expand Up @@ -374,14 +376,14 @@ MIFOS_PASSWORD_REGEX=^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,50}$

#### UI Display Settings

| Variable | Description | Default Value |
| ---------------------------------- | -------------------------------------------------- | ------------- |
| MIFOS_DISPLAY_TENANT_SELECTOR | Display tenant selector in Login view | true |
| MIFOS_DISPLAY_BACKEND_INFO | Display backend info in footer | true |
| MIFOS_PRODUCTION_MODE | Show minimal production hero on login page | false |
| MIFOS_ALLOW_SERVER_SWITCH_SELECTOR | Display DNS server list | true |
| MIFOS_COMPLIANCE_HIDE_CLIENT_DATA | Hide client names in UI (mask with \*) | false |
| MIFOS_PRODUCTION_MODE_ENABLE_RBAC | Enable Role-Based Access Control for menus/buttons | false |
| Variable | Description | Default Value |
| ---------------------------------- | ------------------------------------------------------------------- | ------------- |
| MIFOS_DISPLAY_TENANT_SELECTOR | Display tenant selector in Login view | true |
| MIFOS_DISPLAY_BACKEND_INFO | Display backend info in footer and Login view | false |
| MIFOS_PRODUCTION_MODE | Enable production UI mode (see [Production Mode](#production-mode)) | false |
| MIFOS_ALLOW_SERVER_SWITCH_SELECTOR | Display DNS server list | true |
| MIFOS_COMPLIANCE_HIDE_CLIENT_DATA | Hide client names in UI (mask with \*) | false |
| MIFOS_PRODUCTION_MODE_ENABLE_RBAC | Enable Role-Based Access Control for menus/buttons | false |

#### OAUTH Settings

Expand Down Expand Up @@ -446,6 +448,17 @@ For more detailed configuration options, refer to the `env.sample` file in the r
| MIFOS_INTERBANK_TRANSFERS_API_VERSION | The Interbank server api version | /v1.0 |
| MIFOS_INTERBANK_TRANSFERS_ENABLED | If the Interbank feature is enabled | true |

#### Mifos Copilot Settings

These variables configure the Mifos Copilot, an AI assistant panel that lets officers operate Mifos X in natural language. Every action that writes data pauses for an explicit human confirmation.

| Variable | Description | Default Value |
| -------------------------- | --------------------------------------------------------- | ------------- |
| MIFOS_ENABLE_COPILOT | If the Copilot panel is enabled | false |
| MIFOS_COPILOT_MCP_BASE_URL | Base URL of the Copilot gateway (empty uses mock replies) | |

Set `MIFOS_ENABLE_COPILOT` to `true` to show the panel. With `MIFOS_COPILOT_MCP_BASE_URL` empty the panel answers from built-in mock responses and contacts no server, which is useful for demos and UI work without a gateway. Once a gateway URL is configured, the web app talks only to that gateway: it never holds an LLM key and never calls an LLM directly. The gateway keeps the key server-side and runs banking tools with the logged-in officer's own Fineract credential, so existing permissions and the audit trail still apply.

#### Remittance Module Settings

These variables configure the Remittance Module, which provides a 7-step wizard for processing remittance payouts (search, validate recipient, assign payout, confirm payment, and generate receipt).
Expand Down Expand Up @@ -483,6 +496,26 @@ M**\*** T\*\*\*

This applies to client name display, e.g. in Institution/Clients list.

## Production Mode

`MIFOS_PRODUCTION_MODE` switches the Web App into a leaner, deployment-oriented UI. It is **disabled by default** (`false`), so existing deployments keep the full development-friendly experience.

```bash
MIFOS_PRODUCTION_MODE=true
```

**What changes when `MIFOS_PRODUCTION_MODE=true`:**

| Area | Behaviour |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Login page | Shows the minimal production hero with branding only |
| Products → Loan Products → _Create_ menu | The **Create Loan Product (Convenient)** entry is hidden; **Classic** becomes the only loan product creation flow |
| Loan account → Screen Reports | The **Print** button is available on the screen report output |

The _Convenient_ flow is the guided wizard for the pre-configured loan profiles (Personal, Two Wheeler, Education, Agriculture, Custom/Advanced), which apply opinionated defaults suited to evaluation and quick setup. In a production tenant, loan products are expected to be created through the **Classic** form, where every field is set explicitly.

`MIFOS_PRODUCTION_MODE` and [`MIFOS_PRODUCTION_MODE_ENABLE_RBAC`](#role-based-access-control-rbac) are independent flags and can be enabled separately.

## Interbank Transfer Menu

By default, the “Interbank Transfer” menu will be displayed in the hamburger menu of the Savings account.
Expand Down
6 changes: 6 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ services:
- MIFOS_SESSION_IDLE_TIMEOUT=300000
- MIFOS_PRELOAD_CLIENTS=true
- MIFOS_DEFAULT_CHAR_DELIMITER=,
# Mifos Copilot: off by default. Both variables are needed for real AI responses,
# MIFOS_ENABLE_COPILOT=true to show the panel and MIFOS_COPILOT_MCP_BASE_URL
# pointing at a Copilot gateway. Enabling the panel with an empty URL uses the
# built-in mock replies.
- MIFOS_ENABLE_COPILOT=false
- MIFOS_COPILOT_MCP_BASE_URL=
6 changes: 4 additions & 2 deletions env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ MIFOS_HTTP_CACHE_ENABLED=true
# Hide client data information (set to true to mask client names)
MIFOS_COMPLIANCE_HIDE_CLIENT_DATA=false

# Production mode - when true, shows minimal hero with only branding at bottom
# Production mode - when true, shows minimal hero with only branding at bottom,
# hides the Convenient (guided) loan product creation flow so only Classic remains,
# and shows the Print button on loan screen reports
# Set to true for financial institutions that want a clean, professional look

MIFOS_PRODUCTION_MODE=false

# Enable Role-Based Access Control (RBAC) for menus and buttons
# When true: Menus/buttons are shown based on user permissions (production mode)
# When false (default): All menus/buttons are shown (backward compatibility)
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@
"@types/d3-shape": "^3.1.0",
"@types/d3-timer": "^3.0.0",
"@types/jest": "^29.5.14",
"@types/leaflet": "^1.9.12",
"@types/leaflet.markercluster": "^1.5.4",
"@types/leaflet": "^1.9.21",
"@types/lodash": "4.17.24",
"@types/node": "24.13.1",
"@types/vkbeautify": "^0.99.2",
Expand Down
5 changes: 4 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ export default defineConfig({
name: 'unit',
testMatch: [
/playwright\/utils\/.*\.spec\.ts/,
/playwright\/factories\/client\.spec\.ts/
/playwright\/pages\/.*\.spec\.ts/,
/playwright\/fixtures\/.*\.spec\.ts/,
/playwright\/factories\/client\.spec\.ts/,
/playwright\/factories\/_shared\.spec\.ts/
],
testDir: '.',
use: { storageState: { cookies: [], origins: [] } }
Expand Down
11 changes: 11 additions & 0 deletions playwright/auth-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ export async function authenticateRole(role: AuthRole, page: Page, browser: Brow
);
}

// Seed mifosXServerURL so SettingsService.server resolves to the
// local Fineract instance rather than falling through to
// environment.baseApiUrl (which defaults to demo.mifos.community).
// Without this, browser-side API calls like the group client
// autocomplete hit the wrong server and return empty results.
const fineractUrl = process.env.E2E_FINERACT_URL || 'https://localhost:8443';
await page.evaluate((url) => {
localStorage.setItem('mifosXServerURL', url);
}, fineractUrl);
console.log(`[auth:${role.id}] seeded mifosXServerURL = ${fineractUrl}`);

await page.context().storageState({ path: role.storageStateFile });
console.log(`[auth:${role.id}] storageState saved to ${role.storageStateFile}`);

Expand Down
92 changes: 92 additions & 0 deletions playwright/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,62 @@ export interface AppRoutes {
clientAction: (id: number, action: string) => string;
clientCharges: (id: number) => string;
clientChargesOverview: (id: number) => string;
/** Single client-charge detail view. */
clientChargeView: (id: number, chargeId: number) => string;
/** Pay form for a single client charge. */
clientChargePay: (id: number, chargeId: number) => string;

// ── KYC tabs (nested under a client) ────────────────────────
/** Family members list tab. */
clientFamilyMembers: (id: number) => string;
/** Add-family-member form (a route, not a dialog). */
clientFamilyMemberAdd: (id: number) => string;
/** Edit-family-member form. */
clientFamilyMemberEdit: (id: number, memberId: number) => string;
/** Identifiers tab — add/delete are dialog-driven. */
clientIdentities: (id: number) => string;
/** Documents tab — upload/delete are dialog-driven. */
clientDocuments: (id: number) => string;
/** Notes tab — inline add form, dialog edit, confirm delete. */
clientNotes: (id: number) => string;
/** Address tab — form is built from /fieldconfiguration/ADDRESS. */
clientAddress: (id: number) => string;

// ── Savings accounts (nested under a client) ──────────────────────
/** Create-savings-account stepper for a client. */
savingsAccountCreate: (clientId: number) => string;
/** Savings account general view. */
savingsAccountView: (clientId: number, savingsId: number) => string;
/** Savings account transactions tab. */
savingsAccountTransactions: (clientId: number, savingsId: number) => string;
/**
* Savings account action form (Approve, Activate, Reject,
* Undo Approval, Deposit, Withdrawal, ...).
*/
savingsAccountAction: (clientId: number, savingsId: number, action: string) => string;

// ── Loan accounts (nested under a client) ─────────────────────────
/** Create-loan-account stepper for a client. */
loanAccountCreate: (clientId: number) => string;
/** Loan account general view. */
loanAccountView: (clientId: number, loanId: number) => string;
/**
* Loan account action form (Approve, Disburse, Reject,
* Undo Approval, Undo Disbursal, ...).
*/
loanAccountAction: (clientId: number, loanId: number, action: string) => string;

groups: string;
groupCreate: string;
groupView: (id: number) => string;
/** Edit-group form. */
groupEdit: (id: number) => string;
/**
* Group action form (Activate, Manage Members, Close, Assign Staff,
* Transfer Clients, ...). All of them mount the same
* `GroupActionsComponent`, which switches on the `:action` param.
*/
groupAction: (id: number, action: string) => string;
users: string;
userCreate: string;
userView: (id: number) => string;
Expand All @@ -47,9 +100,48 @@ export const ROUTES: AppRoutes = {
clientAction: (id, action) => `/#/clients/${id}/actions/${encodeURIComponent(action)}`,
clientCharges: (id) => `/#/clients/${id}/charges`,
clientChargesOverview: (id) => `/#/clients/${id}/charges/overview`,
clientChargeView: (id, chargeId) => `/#/clients/${id}/charges/${chargeId}`,
clientChargePay: (id, chargeId) => `/#/clients/${id}/charges/${chargeId}/pay`,

// The KYC tabs are children of the client view, so they share its
// resolver and the client id stays in the path. Family members is
// the only one whose add/edit are full routes rather than dialogs.
clientFamilyMembers: (id) => `/#/clients/${id}/family-members`,
clientFamilyMemberAdd: (id) => `/#/clients/${id}/family-members/add`,
clientFamilyMemberEdit: (id, memberId) => `/#/clients/${id}/family-members/${memberId}/edit`,
clientIdentities: (id) => `/#/clients/${id}/identities`,
clientDocuments: (id) => `/#/clients/${id}/documents`,
clientNotes: (id) => `/#/clients/${id}/notes`,
clientAddress: (id) => `/#/clients/${id}/address`,

// Savings and loan accounts are lazy-loaded child modules mounted
// under the client route, so their URLs always carry the owning
// client id. Action names are interpolated into the path (Angular
// reads them as the `:name` / `:action` param) and are therefore
// encoded — "Undo Approval" and "Withdrawn by Client" both contain
// spaces.
savingsAccountCreate: (clientId) => `/#/clients/${clientId}/savings-accounts/create`,
savingsAccountView: (clientId, savingsId) => `/#/clients/${clientId}/savings-accounts/${savingsId}/general`,
savingsAccountTransactions: (clientId, savingsId) =>
`/#/clients/${clientId}/savings-accounts/${savingsId}/transactions`,
savingsAccountAction: (clientId, savingsId, action) =>
`/#/clients/${clientId}/savings-accounts/${savingsId}/actions/${encodeURIComponent(action)}`,

loanAccountCreate: (clientId) => `/#/clients/${clientId}/loans-accounts/create`,
loanAccountView: (clientId, loanId) => `/#/clients/${clientId}/loans-accounts/${loanId}/general`,
loanAccountAction: (clientId, loanId, action) =>
`/#/clients/${clientId}/loans-accounts/${loanId}/actions/${encodeURIComponent(action)}`,

groups: '/#/groups',
groupCreate: '/#/groups/create',
groupView: (id) => `/#/groups/${id}/general`,
groupEdit: (id) => `/#/groups/${id}/edit`,
// Encoded because every multi-word action name contains a space —
// "Manage Members", "Assign Staff", "Transfer Clients". Angular
// reads the decoded value back as the `:action` param and
// `GroupActionsComponent` matches it against a literal string map,
// so the encoding has to round-trip exactly.
groupAction: (id, action) => `/#/groups/${id}/actions/${encodeURIComponent(action)}`,
users: '/#/appusers',
userCreate: '/#/appusers/create',
userView: (id) => `/#/appusers/${id}`
Expand Down
Loading
Loading