From 4c8f5d83439d764206b4fca1bbf5118215c36db4 Mon Sep 17 00:00:00 2001 From: Shubham Kumar Date: Sun, 22 Feb 2026 11:22:37 +0530 Subject: [PATCH] WEB-506 feat: add Remittance Module with 7-step payout wizard (#3200) WEB-506 fix: address CodeRabbit review (security, validation) WEB-506 fix: make API gateway header name configurable (MIFOS_REMITTANCE_API_HEADER) WEB-506 fix: remove duplicate translation keys (Personal Information, Transaction Details) --- README.md | 23 ++ nginx.conf.template | 27 ++ proxy.conf.js | 42 +++- proxy.localhost.conf.js | 33 +++ src/app/app.module.ts | 2 + .../authentication.interceptor.ts | 4 + .../core/shell/sidenav/sidenav.component.html | 13 + .../core/shell/sidenav/sidenav.component.ts | 3 + src/app/home/activities.ts | 1 + .../remittances/models/beneficiary.model.ts | 66 +++++ .../models/remittance-status.enum.ts | 43 ++++ .../remittances/models/remittance.model.ts | 105 ++++++++ .../process-remittance.component.html | 103 ++++++++ .../process-remittance.component.scss | 20 ++ .../process-remittance.component.ts | 217 ++++++++++++++++ .../remittances/remittance-enabled.guard.ts | 26 ++ .../beneficiary-details-step.component.html | 234 ++++++++++++++++++ .../beneficiary-details-step.component.scss | 161 ++++++++++++ .../beneficiary-details-step.component.ts | 98 ++++++++ .../confirm-payment-step.component.html | 107 ++++++++ .../confirm-payment-step.component.scss | 99 ++++++++ .../confirm-payment-step.component.ts | 88 +++++++ .../payment-receipt-step.component.html | 103 ++++++++ .../payment-receipt-step.component.scss | 192 ++++++++++++++ .../payment-receipt-step.component.ts | 76 ++++++ .../remittance-details-step.component.html | 92 +++++++ .../remittance-details-step.component.scss | 104 ++++++++ .../remittance-details-step.component.ts | 65 +++++ .../search-beneficiary-step.component.html | 77 ++++++ .../search-beneficiary-step.component.scss | 47 ++++ .../search-beneficiary-step.component.ts | 125 ++++++++++ .../search-remittance-step.component.html | 81 ++++++ .../search-remittance-step.component.scss | 90 +++++++ .../search-remittance-step.component.ts | 114 +++++++++ .../transactional-profile-step.component.html | 145 +++++++++++ .../transactional-profile-step.component.scss | 150 +++++++++++ .../transactional-profile-step.component.ts | 167 +++++++++++++ src/app/remittances/remittance.config.ts | 19 ++ .../remittances/remittances-routing.module.ts | 42 ++++ src/app/remittances/remittances.module.ts | 41 +++ src/app/remittances/remittances.service.ts | 152 ++++++++++++ src/app/shared/icons.module.ts | 2 + src/assets/env.js | 8 + src/assets/env.template.js | 8 + src/assets/translations/en-US.json | 115 ++++++++- src/assets/translations/es-MX.json | 99 +++++++- src/environments/environment.prod.ts | 9 + src/environments/environment.ts | 8 + src/main.scss | 46 ++++ src/types/global.d.ts | 5 + 50 files changed, 3684 insertions(+), 13 deletions(-) create mode 100644 src/app/remittances/models/beneficiary.model.ts create mode 100644 src/app/remittances/models/remittance-status.enum.ts create mode 100644 src/app/remittances/models/remittance.model.ts create mode 100644 src/app/remittances/process-remittance/process-remittance.component.html create mode 100644 src/app/remittances/process-remittance/process-remittance.component.scss create mode 100644 src/app/remittances/process-remittance/process-remittance.component.ts create mode 100644 src/app/remittances/remittance-enabled.guard.ts create mode 100644 src/app/remittances/remittance-stepper/beneficiary-details-step/beneficiary-details-step.component.html create mode 100644 src/app/remittances/remittance-stepper/beneficiary-details-step/beneficiary-details-step.component.scss create mode 100644 src/app/remittances/remittance-stepper/beneficiary-details-step/beneficiary-details-step.component.ts create mode 100644 src/app/remittances/remittance-stepper/confirm-payment-step/confirm-payment-step.component.html create mode 100644 src/app/remittances/remittance-stepper/confirm-payment-step/confirm-payment-step.component.scss create mode 100644 src/app/remittances/remittance-stepper/confirm-payment-step/confirm-payment-step.component.ts create mode 100644 src/app/remittances/remittance-stepper/payment-receipt-step/payment-receipt-step.component.html create mode 100644 src/app/remittances/remittance-stepper/payment-receipt-step/payment-receipt-step.component.scss create mode 100644 src/app/remittances/remittance-stepper/payment-receipt-step/payment-receipt-step.component.ts create mode 100644 src/app/remittances/remittance-stepper/remittance-details-step/remittance-details-step.component.html create mode 100644 src/app/remittances/remittance-stepper/remittance-details-step/remittance-details-step.component.scss create mode 100644 src/app/remittances/remittance-stepper/remittance-details-step/remittance-details-step.component.ts create mode 100644 src/app/remittances/remittance-stepper/search-beneficiary-step/search-beneficiary-step.component.html create mode 100644 src/app/remittances/remittance-stepper/search-beneficiary-step/search-beneficiary-step.component.scss create mode 100644 src/app/remittances/remittance-stepper/search-beneficiary-step/search-beneficiary-step.component.ts create mode 100644 src/app/remittances/remittance-stepper/search-remittance-step/search-remittance-step.component.html create mode 100644 src/app/remittances/remittance-stepper/search-remittance-step/search-remittance-step.component.scss create mode 100644 src/app/remittances/remittance-stepper/search-remittance-step/search-remittance-step.component.ts create mode 100644 src/app/remittances/remittance-stepper/transactional-profile-step/transactional-profile-step.component.html create mode 100644 src/app/remittances/remittance-stepper/transactional-profile-step/transactional-profile-step.component.scss create mode 100644 src/app/remittances/remittance-stepper/transactional-profile-step/transactional-profile-step.component.ts create mode 100644 src/app/remittances/remittance.config.ts create mode 100644 src/app/remittances/remittances-routing.module.ts create mode 100644 src/app/remittances/remittances.module.ts create mode 100644 src/app/remittances/remittances.service.ts diff --git a/README.md b/README.md index 4f26095ba..486df9db5 100644 --- a/README.md +++ b/README.md @@ -387,6 +387,29 @@ 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 | +#### 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). + +| Variable | Description | Default Value | +| ----------------------------- | -------------------------------------- | ---------------------------- | +| MIFOS_REMITTANCE_API_URL | The Remittance Microservice server URL | https://apis.mifos.community | +| MIFOS_REMITTANCE_API_PROVIDER | The Remittance server endpoint | /1.0/remittance | +| MIFOS_REMITTANCE_API_VERSION | The Remittance server API version | /v1 | +| MIFOS_REMITTANCE_ENABLED | If the Remittance feature is enabled | false | +| MIFOS_REMITTANCE_API_HEADER | API gateway auth header name | X-Gravitee-Api-Key | +| MIFOS_REMITTANCE_API_KEY | API key for the Remittance gateway | | + +When `MIFOS_REMITTANCE_ENABLED` is set to `true`, a "Remittances" menu item appears in the sidenav and the `/remittances` route becomes accessible. The module supports the following workflow: + +1. **Search Remittance** — Look up a transaction by vendor and external reference ID +2. **Remittance Details** — Review transaction details and status +3. **Search Beneficiary** — Validate the recipient's identity documents +4. **Beneficiary Details** — Review validated recipient info and assign to a Mifos client +5. **Transactional Profile** — Register the transactional profile and assign payout +6. **Confirm Payment** — Final confirmation before payout +7. **Payment Receipt** — View and print the payment receipt + ### Client Data Masking Example When `MIFOS_COMPLIANCE_HIDE_CLIENT_DATA=false` (default): diff --git a/nginx.conf.template b/nginx.conf.template index 3a5a5c912..4b1232dda 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -1,6 +1,9 @@ # Rate limiting zone for external National ID API (2 requests/second per IP) limit_req_zone $binary_remote_addr zone=external_nationalid:10m rate=2r/s; +# Rate limiting zone for Remittance API (2 requests/second per IP) +limit_req_zone $binary_remote_addr zone=remittance_api:10m rate=2r/s; + server { listen 80; server_name _; @@ -46,6 +49,30 @@ server { # CORS is not needed since requests come from the same origin } + # Remittance API proxy + location /remittance-api { + resolver 127.0.0.11 valid=30s; + + # Rate limit to prevent abuse of the upstream Remittance API + limit_req zone=remittance_api burst=5 nodelay; + + set $remittance_target "${MIFOS_REMITTANCE_API_URL}"; + + # Rewrite path: /remittance-api/... -> /1.0/remittance/... + rewrite ^/remittance-api(.*)$ /1.0/remittance$1 break; + + proxy_pass $remittance_target; + proxy_set_header Host $proxy_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Inject API key server-side so it is never exposed to the browser + proxy_set_header ${MIFOS_REMITTANCE_API_HEADER} "${MIFOS_REMITTANCE_API_KEY}"; + + proxy_pass_request_headers on; + } + # Angular app - serve index.html for all routes location / { try_files $uri $uri/ /index.html; diff --git a/proxy.conf.js b/proxy.conf.js index 072f9c8d5..c138af69a 100644 --- a/proxy.conf.js +++ b/proxy.conf.js @@ -51,12 +51,12 @@ const proxyConfig = [ changeOrigin: true, secure: true, logLevel: 'debug', + headers: { + ...(process.env.EXTERNAL_NATIONAL_ID_SYSTEM_API_KEY + ? { 'X-Gravitee-Api-Key': process.env.EXTERNAL_NATIONAL_ID_SYSTEM_API_KEY } + : {}) + }, onProxyReq: function (proxyReq, req, res) { - // Inject API key server-side (same as nginx proxy_set_header in production) - const apiKey = process.env.EXTERNAL_NATIONAL_ID_SYSTEM_API_KEY || ''; - if (apiKey) { - proxyReq.setHeader('X-Gravitee-Api-Key', apiKey); - } const rewrittenPath = (req.url || '').replace(/^\/external-nationalid/, '/1.0/nationalid'); console.log('[Proxy] Proxying:', req.method, req.url, '->', this.target + rewrittenPath); }, @@ -75,6 +75,38 @@ const proxyConfig = [ res.end('Proxy error: ' + (err && err.message ? err.message : 'Unknown error')); } } + }, + { + context: ['/remittance-api'], + target: 'https://apis.mifos.community', + pathRewrite: { '^/remittance-api': '/1.0/remittance' }, + changeOrigin: true, + secure: true, + logLevel: 'debug', + headers: { + ...(process.env.MIFOS_REMITTANCE_API_KEY + ? { [process.env.MIFOS_REMITTANCE_API_HEADER || 'X-Gravitee-Api-Key']: process.env.MIFOS_REMITTANCE_API_KEY } + : {}) + }, + onProxyReq: function (proxyReq, req, res) { + const rewrittenPath = (req.url || '').replace(/^\/remittance-api/, '/1.0/remittance'); + console.log('[Proxy] Proxying:', req.method, req.url, '->', this.target + rewrittenPath); + }, + onError: function (err, req, res) { + console.error( + '[Proxy] Error while proxying request:', + req && req.method, + req && req.url, + '->', + this.target, + '-', + err && err.message + ); + if (res && !res.headersSent) { + res.writeHead(502, { 'Content-Type': 'text/plain' }); + res.end('Proxy error: ' + (err && err.message ? err.message : 'Unknown error')); + } + } } // For local development use `proxy.localhost.conf js` . ]; diff --git a/proxy.localhost.conf.js b/proxy.localhost.conf.js index 5f295e7e3..88d5d190f 100644 --- a/proxy.localhost.conf.js +++ b/proxy.localhost.conf.js @@ -72,5 +72,38 @@ module.exports = [ res.end('Proxy error: ' + (err && err.message ? err.message : 'Unknown error')); } } + }, + { + context: ['/remittance-api'], + target: 'http://54.225.231.146:8080', + pathRewrite: { '^/remittance-api': '/1.0/remittance' }, + changeOrigin: true, + secure: false, + logLevel: 'debug', + onProxyReq: function (proxyReq, req, res) { + // Inject API key server-side (same as nginx proxy_set_header in production) + const apiKey = process.env.MIFOS_REMITTANCE_API_KEY || ''; + const apiHeader = process.env.MIFOS_REMITTANCE_API_HEADER || 'X-Gravitee-Api-Key'; + if (apiKey) { + proxyReq.setHeader(apiHeader, apiKey); + } + const rewrittenPath = (req.url || '').replace(/^\/remittance-api/, '/1.0/remittance'); + console.log('[Proxy] Proxying:', req.method, req.url, '->', this.target + rewrittenPath); + }, + onError: function (err, req, res) { + console.error( + '[Proxy] Error while proxying request:', + req && req.method, + req && req.url, + '->', + this.target, + '-', + err && err.message + ); + if (res && !res.headersSent) { + res.writeHead(502, { 'Content-Type': 'text/plain' }); + res.end('Proxy error: ' + (err && err.message ? err.message : 'Unknown error')); + } + } } ]; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 09a73871d..cd95d8e33 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -48,6 +48,7 @@ import { CollectionsModule } from './collections/collections.module'; import { ProfileModule } from './profile/profile.module'; import { TasksModule } from './tasks/tasks.module'; import { ConfigurationWizardModule } from './configuration-wizard/configuration-wizard.module'; +import { RemittancesModule } from './remittances/remittances.module'; import { PortalModule } from '@angular/cdk/portal'; /** Main Routing Module */ @@ -127,6 +128,7 @@ export function HttpLoaderFactory(http: HttpClient) { CollectionsModule, TasksModule, ConfigurationWizardModule, + RemittancesModule, AppRoutingModule, NotFoundComponent, CallbackComponent, diff --git a/src/app/core/authentication/authentication.interceptor.ts b/src/app/core/authentication/authentication.interceptor.ts index 962a0bd23..7834d499a 100644 --- a/src/app/core/authentication/authentication.interceptor.ts +++ b/src/app/core/authentication/authentication.interceptor.ts @@ -41,6 +41,10 @@ export class AuthenticationInterceptor implements HttpInterceptor { * Intercepts a Http request and sets the request headers. */ intercept(request: HttpRequest, next: HttpHandler): Observable> { + // Skip Fineract auth headers for external API calls (e.g. remittance, national ID) + if (request.url.startsWith('http://') || request.url.startsWith('https://')) { + return next.handle(request); + } if (this.settingsService.tenantIdentifier) { httpOptions.headers['Fineract-Platform-TenantId'] = this.settingsService.tenantIdentifier; } diff --git a/src/app/core/shell/sidenav/sidenav.component.html b/src/app/core/shell/sidenav/sidenav.component.html index c7e4366c7..776d35ffa 100644 --- a/src/app/core/shell/sidenav/sidenav.component.html +++ b/src/app/core/shell/sidenav/sidenav.component.html @@ -132,6 +132,19 @@ {{ 'labels.menus.Individual Collection Sheet' | translate }} + + + + + {{ 'labels.menus.Remittances' | translate }} + ; diff --git a/src/app/home/activities.ts b/src/app/home/activities.ts index 305f96d86..67d2b9777 100644 --- a/src/app/home/activities.ts +++ b/src/app/home/activities.ts @@ -58,6 +58,7 @@ const activities: any[] = [ { activity: 'audit', path: '/products/audit-trails' }, { activity: 'create accounting closure', path: '' }, { activity: 'navigation', path: '/navigation' }, + { activity: 'remittances', path: '/remittances/process' }, { activity: '', path: 'home' } ]; diff --git a/src/app/remittances/models/beneficiary.model.ts b/src/app/remittances/models/beneficiary.model.ts new file mode 100644 index 000000000..d84393cf7 --- /dev/null +++ b/src/app/remittances/models/beneficiary.model.ts @@ -0,0 +1,66 @@ +/** + * Copyright since 2025 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/. + */ + +/** + * Recipient validation request body. + * POST /v1/remittances/:tenant/:vendor/transactions/:id/recipient + */ +export interface ValidateRecipientRequest { + givenName: string; + lastName: string; + motherMaidenName?: string; + dateOfBirth?: string; + phone?: string; + address?: { + country?: string; + state?: string; + city?: string; + postalCode?: string; + line1?: string; + }; + primaryDocument: { + documentType: string; + documentNumber: string; + issuingCountry?: string; + }; +} + +/** + * Validated recipient response from the API. + * Maps to all 17 beneficiary fields required by the spec. + */ +export interface ValidatedRecipient { + valid?: boolean; + givenName?: string; + firstName?: string; + lastName: string; + motherMaidenName?: string; + dateOfBirth?: string; + phone?: string; + identityCard?: string; + address?: { + country?: string; + state?: string; + city?: string; + postalCode?: string; + line1?: string; + line2?: string; + }; + primaryDocument?: { + documentType?: string; + documentNumber?: string; + issuingCountry?: string; + }; + occupation?: string; + economicActivity?: string; + /** Additional fields from spec (may be populated via additionalInfo or extended API) */ + municipality?: string; + locality?: string; + newLocality?: string; + entityMember?: string; +} diff --git a/src/app/remittances/models/remittance-status.enum.ts b/src/app/remittances/models/remittance-status.enum.ts new file mode 100644 index 000000000..c77e5eb2c --- /dev/null +++ b/src/app/remittances/models/remittance-status.enum.ts @@ -0,0 +1,43 @@ +/** + * Copyright since 2025 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/. + */ + +export enum RemittanceStatus { + PENDING = 'PENDING', + READY_FOR_PAYOUT = 'READY_FOR_PAYOUT', + PAID = 'PAID', + REJECTED = 'REJECTED', + CANCELED = 'CANCELED', + EXPIRED = 'EXPIRED', + UNKNOWN = 'UNKNOWN' +} + +/** + * Determines whether the given status represents a terminal (final) state. + */ +export function isTerminalStatus(status: RemittanceStatus): boolean { + return [ + RemittanceStatus.PAID, + RemittanceStatus.REJECTED, + RemittanceStatus.CANCELED, + RemittanceStatus.EXPIRED + ].includes(status); +} + +/** + * Maps a raw status string from the API to the RemittanceStatus enum. + * Handles both CANCELED (US) and CANCELLED (UK) spellings. + */ +export function parseRemittanceStatus(raw: string): RemittanceStatus { + if (!raw) return RemittanceStatus.UNKNOWN; + let normalized = raw.toUpperCase().replace(/-/g, '_').trim(); + if (normalized === 'CANCELLED') { + normalized = 'CANCELED'; + } + const match = Object.values(RemittanceStatus).find((s) => s === normalized); + return match ?? RemittanceStatus.UNKNOWN; +} diff --git a/src/app/remittances/models/remittance.model.ts b/src/app/remittances/models/remittance.model.ts new file mode 100644 index 000000000..0c2166af7 --- /dev/null +++ b/src/app/remittances/models/remittance.model.ts @@ -0,0 +1,105 @@ +/** + * Copyright since 2025 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/. + */ + +/** + * Represents a remittance transaction returned by the Find-Remittance endpoint. + * GET /v1/remittances/:tenant/:vendor/transactions/:id + */ +export interface RemittanceTransaction { + sendingPartner: string; + externalTransactionId: string; + pin: string; + created: string; + processedAt: string; + senderName: string; + recipientName: string; + receivingAmount: number; + receivingCurrency: string; + recipientAddress: string; + status: string; +} + +/** + * Payout assignment response (Endpoint 3). + * POST /v1/remittances/:tenant/:vendor/transactions/:id/payout-assignment + */ +export interface PayoutAssignmentResponse { + sendingPartner: string; + remittanceKey: string; + created: string; + receivingAmount: number; + receivingCurrency: string; + trackingNumber: string; + sender: string; + recipient: string; + status: string; +} + +/** + * Payout confirmation response (Endpoint 4). + * POST /v1/remittances/:tenant/:vendor/transactions/:id/payout-confirmation/:clientId + */ +export interface PayoutConfirmationResponse { + transaction: { + receivingAmount: number; + receivingCurrency: string; + status: string; + sender: PersonName; + recipient: RecipientDetails; + }; +} + +export interface PersonName { + firstName: string; + lastName: string; + middleName?: string; + mothersMaidenName?: string; +} + +export interface RecipientAddress { + country: string; + state: string; + city: string; + postalCode: string; + line1: string; + line2?: string; +} + +export interface RecipientDocument { + documentType: string; + documentNumber: string; + issuingCountry: string; +} + +export interface RecipientDetails extends PersonName { + phone?: string; + email?: string; + nationality?: string; + dateOfBirth?: string; + homeAddress?: RecipientAddress; + primaryDocument?: RecipientDocument; + occupation?: string; + economicActivity?: string; +} + +/** Vendor item from the GET /v1/remittances/:tenant/vendors endpoint */ +export interface RemittanceVendor { + name: string; + headerValue: string; +} + +/** Request body for payout assignment */ +export interface PayoutAssignmentRequest { + agent?: string; + additionalInfo?: Record; +} + +/** Request body for payout confirmation */ +export interface PayoutConfirmationRequest { + additionalInfo?: Record; +} diff --git a/src/app/remittances/process-remittance/process-remittance.component.html b/src/app/remittances/process-remittance/process-remittance.component.html new file mode 100644 index 000000000..3226b2e84 --- /dev/null +++ b/src/app/remittances/process-remittance/process-remittance.component.html @@ -0,0 +1,103 @@ + + +
+ + +
+

{{ 'labels.heading.Process Remittance' | translate }}

+
+ + + + + {{ 'labels.heading.Search Remittance' | translate }} + + + + + + {{ 'labels.heading.Remittance Details' | translate }} + + + + + + @if (showBeneficiarySteps) { + + {{ 'labels.heading.Search Beneficiary' | translate }} + + + + + + + {{ 'labels.heading.Beneficiary Details' | translate }} + + + + + + + {{ 'labels.heading.Transactional Profile' | translate }} + + + + } + + + @if (showConfirmStep) { + + {{ 'labels.heading.Confirm Payment' | translate }} + + + + } + + + + {{ 'labels.heading.Payment Receipt' | translate }} + + + + +
+
+
diff --git a/src/app/remittances/process-remittance/process-remittance.component.scss b/src/app/remittances/process-remittance/process-remittance.component.scss new file mode 100644 index 000000000..97b9150d5 --- /dev/null +++ b/src/app/remittances/process-remittance/process-remittance.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright since 2025 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/. + */ + +.container { + padding: 16px; +} + +.header { + margin-bottom: 16px; + + h2 { + margin: 0; + font-weight: 500; + } +} diff --git a/src/app/remittances/process-remittance/process-remittance.component.ts b/src/app/remittances/process-remittance/process-remittance.component.ts new file mode 100644 index 000000000..07390c39d --- /dev/null +++ b/src/app/remittances/process-remittance/process-remittance.component.ts @@ -0,0 +1,217 @@ +/** + * Copyright since 2025 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 { Component, ViewChild, ChangeDetectorRef, inject } from '@angular/core'; +import { MatStepper, MatStep, MatStepLabel } from '@angular/material/stepper'; +import { STANDALONE_SHARED_IMPORTS } from '../../standalone-shared.module'; + +import { + RemittanceTransaction, + PayoutAssignmentResponse, + PayoutConfirmationResponse, + RemittanceVendor +} from '../models/remittance.model'; +import { ValidatedRecipient } from '../models/beneficiary.model'; +import { RemittanceStatus, parseRemittanceStatus, isTerminalStatus } from '../models/remittance-status.enum'; + +import { SearchRemittanceStepComponent } from '../remittance-stepper/search-remittance-step/search-remittance-step.component'; +import { RemittanceDetailsStepComponent } from '../remittance-stepper/remittance-details-step/remittance-details-step.component'; +import { SearchBeneficiaryStepComponent } from '../remittance-stepper/search-beneficiary-step/search-beneficiary-step.component'; +import { BeneficiaryDetailsStepComponent } from '../remittance-stepper/beneficiary-details-step/beneficiary-details-step.component'; +import { TransactionalProfileStepComponent } from '../remittance-stepper/transactional-profile-step/transactional-profile-step.component'; +import { ConfirmPaymentStepComponent } from '../remittance-stepper/confirm-payment-step/confirm-payment-step.component'; +import { PaymentReceiptStepComponent } from '../remittance-stepper/payment-receipt-step/payment-receipt-step.component'; + +@Component({ + selector: 'mifosx-process-remittance', + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatStepper, + MatStep, + MatStepLabel, + SearchRemittanceStepComponent, + RemittanceDetailsStepComponent, + SearchBeneficiaryStepComponent, + BeneficiaryDetailsStepComponent, + TransactionalProfileStepComponent, + ConfirmPaymentStepComponent, + PaymentReceiptStepComponent + ], + templateUrl: './process-remittance.component.html', + styleUrls: ['./process-remittance.component.scss'] +}) +export class ProcessRemittanceComponent { + @ViewChild('stepper') stepper!: MatStepper; + + private cdr = inject(ChangeDetectorRef); + + /** Shared state across steps */ + remittanceData: RemittanceTransaction | null = null; + recipientData: ValidatedRecipient | null = null; + payoutAssignment: PayoutAssignmentResponse | null = null; + payoutConfirmation: PayoutConfirmationResponse | null = null; + selectedVendor: RemittanceVendor | null = null; + transactionId = ''; + parsedStatus: RemittanceStatus = RemittanceStatus.UNKNOWN; + selectedClientId: number | null = null; + + /** Step completion flags */ + detailsReviewed = false; + beneficiaryReviewed = false; + + /** Step visibility control - explicit properties for better change detection */ + showBeneficiarySteps = false; + showConfirmStep = false; + + private updateStepVisibility(): void { + // Beneficiary steps (3-5) only show for PENDING. + // Per spec: READY_FOR_PAYOUT skips directly to Confirm Payment. + // IMPORTANT: Once shown, never hide — prevents mid-flow step destruction + // when assignPayout changes status from PENDING → READY_FOR_PAYOUT. + this.showBeneficiarySteps = this.showBeneficiarySteps || this.parsedStatus === RemittanceStatus.PENDING; + + // Confirm step only shows for READY_FOR_PAYOUT (the only actionable confirm state). + // Terminal statuses (PAID, REJECTED, etc.) skip directly to Payment Receipt. + this.showConfirmStep = this.showConfirmStep || this.parsedStatus === RemittanceStatus.READY_FOR_PAYOUT; + } + + /** + * Called when Step 1 (Search) finds a remittance. + */ + onRemittanceFound(event: { + transaction: RemittanceTransaction; + transactionId: string; + vendor: RemittanceVendor; + }): void { + this.remittanceData = event.transaction; + this.transactionId = event.transactionId; + this.selectedVendor = event.vendor; + this.parsedStatus = parseRemittanceStatus(event.transaction.status); + + // Update step visibility based on new status + this.updateStepVisibility(); + this.cdr.detectChanges(); + + setTimeout(() => { + this.cdr.detectChanges(); + this.stepper.next(); + }); + } + + /** + * Called after Step 2 (Remittance Details) user clicks Next. + * Routes based on status. + */ + onDetailsNext(): void { + this.detailsReviewed = true; + this.cdr.detectChanges(); + + if (isTerminalStatus(this.parsedStatus)) { + this.goToLastStep(); + } else { + this.stepper.next(); + } + } + + /** + * Called when Step 3 (Search Beneficiary) validates a recipient. + */ + onRecipientValidated(recipient: ValidatedRecipient): void { + this.recipientData = recipient; + this.cdr.detectChanges(); + this.stepper.next(); + } + + /** + * Called after Step 4 (Beneficiary Details) user selects client and clicks Next. + */ + onBeneficiaryDetailsNext(clientId: number): void { + this.selectedClientId = clientId; + this.beneficiaryReviewed = true; + this.cdr.detectChanges(); + this.stepper.next(); + } + + /** + * Called when Step 5 (Transactional Profile) assigns payout. + * Status typically changes from PENDING → READY_FOR_PAYOUT here. + */ + onPayoutAssigned(assignment: PayoutAssignmentResponse): void { + this.payoutAssignment = assignment; + this.parsedStatus = parseRemittanceStatus(assignment.status); + + // Update step visibility — showConfirmStep will become true + // showBeneficiarySteps stays true (never contracts once expanded) + this.updateStepVisibility(); + + this.cdr.detectChanges(); + + // Navigate to confirm step (next visible step after transactional profile) + if (isTerminalStatus(this.parsedStatus)) { + this.goToLastStep(); + } else { + this.goToConfirmStep(); + } + } + + /** + * Called when Step 6 (Confirm Payment) confirms payout. + */ + onPayoutConfirmed(confirmation: PayoutConfirmationResponse): void { + this.payoutConfirmation = confirmation; + this.parsedStatus = RemittanceStatus.PAID; + this.cdr.detectChanges(); + this.stepper.next(); + } + + /** + * Reset the entire workflow. + */ + resetWorkflow(): void { + this.remittanceData = null; + this.recipientData = null; + this.payoutAssignment = null; + this.payoutConfirmation = null; + this.selectedVendor = null; + this.transactionId = ''; + this.parsedStatus = RemittanceStatus.UNKNOWN; + this.selectedClientId = null; + this.detailsReviewed = false; + this.beneficiaryReviewed = false; + + // Reset step visibility + this.showBeneficiarySteps = false; + this.showConfirmStep = false; + + this.stepper.reset(); + } + + private goToConfirmStep(): void { + // Use setTimeout to ensure the DOM is updated after status change + setTimeout(() => { + const steps = this.stepper.steps.toArray(); + + // Confirm step is always second to last (before Receipt) + const confirmIndex = steps.length - 2; + + if (confirmIndex >= 0 && confirmIndex < steps.length) { + this.stepper.selectedIndex = confirmIndex; + } else { + this.goToLastStep(); + } + }, 0); + } + + private goToLastStep(): void { + // Use setTimeout to ensure the DOM is updated + setTimeout(() => { + this.stepper.selectedIndex = this.stepper.steps.length - 1; + }, 0); + } +} diff --git a/src/app/remittances/remittance-enabled.guard.ts b/src/app/remittances/remittance-enabled.guard.ts new file mode 100644 index 000000000..e93e49490 --- /dev/null +++ b/src/app/remittances/remittance-enabled.guard.ts @@ -0,0 +1,26 @@ +/** + * Copyright since 2025 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 { inject } from '@angular/core'; +import { Router, CanActivateFn } from '@angular/router'; +import { remittanceConfig } from './remittance.config'; + +/** + * Guard to prevent access to the Remittance module if it is disabled in the config. + */ +export const remittanceEnabledGuard: CanActivateFn = () => { + const router = inject(Router); + + if (remittanceConfig.isRemittanceEnabled) { + return true; + } + + // If disabled, redirect to home or a 404 page + router.navigate(['/home'], { replaceUrl: true }); + return false; +}; diff --git a/src/app/remittances/remittance-stepper/beneficiary-details-step/beneficiary-details-step.component.html b/src/app/remittances/remittance-stepper/beneficiary-details-step/beneficiary-details-step.component.html new file mode 100644 index 000000000..8f82a75e8 --- /dev/null +++ b/src/app/remittances/remittance-stepper/beneficiary-details-step/beneficiary-details-step.component.html @@ -0,0 +1,234 @@ + + +@if (recipientData) { +
+

{{ 'labels.heading.Validated Recipient Information' | translate }}

+ + +
+ badge +

{{ 'labels.heading.Identification' | translate }}

+
+
+ @if (recipientData.primaryDocument?.documentType) { +
+ {{ 'labels.inputs.Document Type' | translate }} + {{ recipientData.primaryDocument?.documentType }} +
+ } + + @if (recipientData.primaryDocument?.documentNumber) { +
+ {{ 'labels.inputs.Document Number' | translate }} + {{ recipientData.primaryDocument?.documentNumber }} +
+ } + + @if (recipientData.identityCard) { +
+ {{ 'labels.inputs.National Identity Card Number' | translate }} + {{ recipientData.identityCard }} +
+ } +
+ + +
+ person +

{{ 'labels.heading.Personal Information' | translate }}

+
+
+
+ {{ 'labels.inputs.First Name' | translate }} + {{ recipientData.givenName }} +
+ +
+ {{ 'labels.inputs.Last Name' | translate }} + {{ recipientData.lastName }} +
+ + @if (recipientData.motherMaidenName) { +
+ {{ 'labels.inputs.Mothers Maiden Name' | translate }} + {{ recipientData.motherMaidenName }} +
+ } + + @if (recipientData.dateOfBirth) { +
+ {{ 'labels.inputs.Date of Birth' | translate }} + {{ recipientData.dateOfBirth | date: 'mediumDate' }} +
+ } + + @if (recipientData.phone) { +
+ {{ 'labels.inputs.Phone Number' | translate }} + {{ recipientData.phone }} +
+ } +
+ + +
+ location_on +

{{ 'labels.inputs.Address' | translate }}

+
+
+ @if (recipientData.address?.line1) { +
+ {{ 'labels.inputs.Address Line' | translate }} + {{ recipientData.address?.line1 }} +
+ } + + @if (recipientData.address?.postalCode) { +
+ {{ 'labels.inputs.Postal Code' | translate }} + {{ recipientData.address?.postalCode }} +
+ } + + @if (recipientData.address?.state) { +
+ {{ 'labels.inputs.State' | translate }} + {{ recipientData.address?.state }} +
+ } + + @if (recipientData.municipality || recipientData.address?.city) { +
+ {{ 'labels.inputs.Municipality' | translate }} + {{ recipientData.municipality || recipientData.address?.city }} +
+ } + + @if (recipientData.locality) { +
+ {{ 'labels.inputs.Locality' | translate }} + {{ recipientData.locality }} +
+ } + + @if (recipientData.newLocality) { +
+ {{ 'labels.inputs.New Locality' | translate }} + {{ recipientData.newLocality }} +
+ } + + @if (recipientData.address?.country) { +
+ {{ 'labels.inputs.Country' | translate }} + {{ recipientData.address?.country }} +
+ } +
+ + +
+ work +

{{ 'labels.heading.Professional Information' | translate }}

+
+
+ @if (recipientData.occupation) { +
+ {{ 'labels.inputs.Occupation' | translate }} + {{ recipientData.occupation }} +
+ } + + @if (recipientData.economicActivity) { +
+ {{ 'labels.inputs.Economic Activity' | translate }} + {{ recipientData.economicActivity }} +
+ } + + @if (recipientData.entityMember) { +
+ {{ 'labels.inputs.Entity Member' | translate }} + {{ recipientData.entityMember }} +
+ } +
+ + +
+ assignment_ind +

{{ 'labels.heading.Assign to Mifos Client' | translate }}

+
+
+

+ {{ 'labels.text.Search and assign this beneficiary to an existing Mifos client account' | translate }} +

+ + + {{ 'labels.inputs.Search Client' | translate }} + + search + + @for (client of filteredClients$ | async; track client.id) { + +
+ {{ client.displayName }} + + @if (client.accountNo) { + + } + @if (client.externalId) { + {{ 'labels.inputs.External ID' | translate }}: {{ client.externalId }} + } + +
+
+ } +
+
+ + @if (searchError) { +
+ error + {{ 'labels.text.Failed to search clients. Please try again.' | translate }} +
+ } + + @if (selectedClient) { +
+ check_circle +
+ {{ selectedClient.displayName }} + @if (selectedClient.accountNo) { + {{ 'labels.inputs.Account' | translate }}: {{ selectedClient.accountNo }} + } +
+
+ } +
+ +
+ +
+
+} diff --git a/src/app/remittances/remittance-stepper/beneficiary-details-step/beneficiary-details-step.component.scss b/src/app/remittances/remittance-stepper/beneficiary-details-step/beneficiary-details-step.component.scss new file mode 100644 index 000000000..2b3740116 --- /dev/null +++ b/src/app/remittances/remittance-stepper/beneficiary-details-step/beneficiary-details-step.component.scss @@ -0,0 +1,161 @@ +/** + * Copyright since 2025 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/. + */ + +.step-content { + padding: 16px 0; + + h3 { + margin: 0 0 16px; + font-weight: 500; + } +} + +.section-header { + display: flex; + align-items: center; + gap: 8px; + margin: 20px 0 12px; + padding-bottom: 8px; + border-bottom: 1px solid #e0e0e0; + + mat-icon { + font-size: 20px; + width: 20px; + height: 20px; + } + + h4 { + margin: 0; + font-weight: 500; + font-size: 14px; + } + + &:first-of-type { + margin-top: 0; + } +} + +.details-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 100%)); + gap: 16px; + margin-bottom: 16px; +} + +.detail-item { + display: flex; + flex-direction: column; + gap: 4px; + + .label { + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .value { + font-size: 14px; + } +} + +.step-actions { + margin-top: 16px; +} + +.client-search-section { + margin: 16px 0; + padding: 16px; + border-radius: 8px; + background-color: var(--mdc-theme-surface, rgb(0 0 0 / 4%)); + + .search-instruction { + margin: 0 0 12px; + font-size: 13px; + opacity: 0.7; + } + + .client-search-field { + width: 100%; + } + + .error-message { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; + padding: 8px 12px; + background-color: var(--mdc-theme-error-container, #ffebee); + border-radius: 4px; + color: var(--mdc-theme-on-error-container, #c62828); + font-size: 13px; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } + } + + .selected-client-info { + display: flex; + align-items: center; + gap: 12px; + margin-top: 12px; + padding: 12px; + background-color: var(--mdc-theme-primary-container, rgb(76 175 80 / 12%)); + border-radius: 4px; + border: 1px solid var(--mdc-theme-primary, rgb(76 175 80 / 30%)); + + mat-icon { + font-size: 24px; + width: 24px; + height: 24px; + color: var(--mdc-theme-primary, #4caf50); + } + + .client-info { + display: flex; + flex-direction: column; + gap: 4px; + + strong { + font-size: 14px; + } + + .subdued { + font-size: 12px; + opacity: 0.7; + } + } + } +} + +.client-option { + display: flex; + flex-direction: column; + gap: 4px; + padding: 4px 0; + + .client-name { + font-weight: 500; + } + + .client-details { + display: flex; + flex-wrap: wrap; + gap: 12px; + font-size: 12px; + color: #666; + + .account-no, + .external-id { + white-space: nowrap; + } + } +} diff --git a/src/app/remittances/remittance-stepper/beneficiary-details-step/beneficiary-details-step.component.ts b/src/app/remittances/remittance-stepper/beneficiary-details-step/beneficiary-details-step.component.ts new file mode 100644 index 000000000..765bb8ea2 --- /dev/null +++ b/src/app/remittances/remittance-stepper/beneficiary-details-step/beneficiary-details-step.component.ts @@ -0,0 +1,98 @@ +/** + * Copyright since 2025 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 { Component, EventEmitter, Input, Output, inject, OnInit } from '@angular/core'; +import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module'; +import { MatIcon } from '@angular/material/icon'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { Observable, of } from 'rxjs'; +import { debounceTime, distinctUntilChanged, switchMap, startWith, catchError, map } from 'rxjs/operators'; + +import { ValidatedRecipient } from '../../models/beneficiary.model'; +import { ClientsService } from '../../../clients/clients.service'; + +interface ClientSearchResult { + id: number; + displayName: string; + accountNo: string; + externalId: string; +} + +@Component({ + selector: 'mifosx-beneficiary-details-step', + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatIcon, + ReactiveFormsModule, + MatAutocompleteModule + ], + templateUrl: './beneficiary-details-step.component.html', + styleUrls: ['./beneficiary-details-step.component.scss'] +}) +export class BeneficiaryDetailsStepComponent implements OnInit { + @Input() recipientData: ValidatedRecipient | null = null; + @Output() beneficiaryNext = new EventEmitter(); + + private clientsService = inject(ClientsService); + + clientSearchControl = new FormControl(''); + filteredClients$!: Observable; + selectedClient: ClientSearchResult | null = null; + searchError = false; + + ngOnInit(): void { + this.filteredClients$ = this.clientSearchControl.valueChanges.pipe( + startWith(''), + debounceTime(300), + distinctUntilChanged(), + switchMap((searchValue) => { + const searchText = typeof searchValue === 'string' ? searchValue : ''; + if (!searchText || searchText.trim().length < 2) { + return of([]); + } + return this.searchClients(searchText.trim()); + }) + ); + } + + private searchClients(searchText: string): Observable { + this.searchError = false; + return this.clientsService.searchByText(searchText, 0, 20, '', '').pipe( + map((response: any) => { + const clients = response?.content || []; + return clients.map((client: any) => ({ + id: client.id, + displayName: client.displayName || '', + accountNo: client.accountNumber || '', + externalId: client.externalId || '' + })); + }), + catchError(() => { + this.searchError = true; + return of([]); + }) + ); + } + + onClientSelected(client: ClientSearchResult): void { + this.selectedClient = client; + this.clientSearchControl.setValue(client.displayName); + } + + displayFn = (value: string | ClientSearchResult): string => { + return typeof value === 'string' ? value : value?.displayName || ''; + }; + + onNext(): void { + if (this.selectedClient) { + this.beneficiaryNext.emit(this.selectedClient.id); + } + } +} diff --git a/src/app/remittances/remittance-stepper/confirm-payment-step/confirm-payment-step.component.html b/src/app/remittances/remittance-stepper/confirm-payment-step/confirm-payment-step.component.html new file mode 100644 index 000000000..10cbdc6c0 --- /dev/null +++ b/src/app/remittances/remittance-stepper/confirm-payment-step/confirm-payment-step.component.html @@ -0,0 +1,107 @@ + + +
+

{{ 'labels.heading.Confirm Payout' | translate }}

+

+ {{ 'labels.text.Please review the details carefully before confirming the payment.' | translate }} +

+ +
+ + @if (remittanceData) { +
+

{{ 'labels.heading.Transaction' | translate }}

+ @if (officeName) { +
+ {{ 'labels.inputs.Office' | translate }} + {{ officeName }} +
+ } +
+ {{ 'labels.inputs.Code' | translate }} + {{ remittanceData.pin }} +
+
+ {{ 'labels.inputs.Date' | translate }} + {{ remittanceData.created | date: 'medium' }} +
+
+ {{ 'labels.inputs.Payout Amount' | translate }} + {{ + remittanceData.receivingAmount | currency: remittanceData.receivingCurrency + }} +
+
+ {{ 'labels.inputs.Sender' | translate }} + {{ remittanceData.senderName }} +
+
+ } + + + + +
+

{{ 'labels.heading.Recipient' | translate }}

+
+ {{ 'labels.inputs.Name' | translate }} + @if (recipientData?.givenName) { + {{ recipientData.givenName }} {{ recipientData.lastName }} + } @else { + {{ remittanceData?.recipientName }} + } +
+ @if (recipientData?.primaryDocument?.documentType) { +
+ {{ 'labels.inputs.Document' | translate }} + {{ recipientData.primaryDocument?.documentType }} - + {{ recipientData.primaryDocument?.documentNumber }} +
+ } +
+ + @if (payoutAssignment) { + + + +
+

{{ 'labels.heading.Payout Assignment' | translate }}

+
+ {{ 'labels.inputs.Status' | translate }} + {{ payoutAssignment.status }} +
+ @if (payoutAssignment?.trackingNumber) { +
+ {{ 'labels.inputs.Reference Number' | translate }} + {{ payoutAssignment.trackingNumber }} +
+ } +
+ } +
+ + @if (errorMessage) { +
+ error_outline + {{ errorMessage }} +
+ } + +
+ +
+
diff --git a/src/app/remittances/remittance-stepper/confirm-payment-step/confirm-payment-step.component.scss b/src/app/remittances/remittance-stepper/confirm-payment-step/confirm-payment-step.component.scss new file mode 100644 index 000000000..2f6acacdd --- /dev/null +++ b/src/app/remittances/remittance-stepper/confirm-payment-step/confirm-payment-step.component.scss @@ -0,0 +1,99 @@ +/** + * Copyright since 2025 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/. + */ + +.step-content { + padding: 16px 0; + + h3 { + margin: 0 0 8px; + font-weight: 500; + } + + .description { + margin: 0 0 24px; + font-size: 14px; + } +} + +.confirmation-card { + border: 1px solid #e0e0e0; + border-radius: 8px; + overflow: hidden; + margin-bottom: 16px; +} + +.card-section { + padding: 16px; + + h4 { + margin: 0 0 12px; + font-weight: 500; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.5px; + } +} + +.info-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 0; + + .label { + font-size: 13px; + } + + .value { + font-size: 14px; + font-weight: 500; + + &.amount { + font-size: 18px; + font-weight: 700; + color: #2e7d32; + } + + &.mono { + font-family: monospace; + font-size: 12px; + background: #f5f5f5; + padding: 2px 8px; + border-radius: 4px; + } + } +} + +.error-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + margin: 8px 0; + background-color: #fdecea; + border-radius: 4px; + color: #611a15; + + mat-icon { + color: #f44336; + font-size: 20px; + width: 20px; + height: 20px; + } +} + +.step-actions { + margin-top: 16px; + + button { + mat-spinner { + display: inline-block; + margin-right: 8px; + } + } +} diff --git a/src/app/remittances/remittance-stepper/confirm-payment-step/confirm-payment-step.component.ts b/src/app/remittances/remittance-stepper/confirm-payment-step/confirm-payment-step.component.ts new file mode 100644 index 000000000..7a08365e8 --- /dev/null +++ b/src/app/remittances/remittance-stepper/confirm-payment-step/confirm-payment-step.component.ts @@ -0,0 +1,88 @@ +/** + * Copyright since 2025 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 { Component, EventEmitter, Input, Output, inject } from '@angular/core'; +import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatIcon } from '@angular/material/icon'; +import { MatDivider } from '@angular/material/divider'; +import { TranslateService } from '@ngx-translate/core'; + +import { RemittancesService } from '../../remittances.service'; +import { + RemittanceTransaction, + RemittanceVendor, + PayoutAssignmentResponse, + PayoutConfirmationResponse +} from '../../models/remittance.model'; +import { ValidatedRecipient } from '../../models/beneficiary.model'; +import { AuthenticationService } from '../../../core/authentication/authentication.service'; + +@Component({ + selector: 'mifosx-confirm-payment-step', + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatProgressSpinner, + MatIcon, + MatDivider + ], + templateUrl: './confirm-payment-step.component.html', + styleUrls: ['./confirm-payment-step.component.scss'] +}) +export class ConfirmPaymentStepComponent { + @Input() vendor: RemittanceVendor | null = null; + @Input() transactionId = ''; + @Input() remittanceData: RemittanceTransaction | null = null; + @Input() recipientData: ValidatedRecipient | null = null; + @Input() payoutAssignment: PayoutAssignmentResponse | null = null; + @Input() clientId: number | null = null; + @Output() payoutConfirmed = new EventEmitter(); + + private remittancesService = inject(RemittancesService); + private authService = inject(AuthenticationService); + private translateService = inject(TranslateService); + + isLoading = false; + errorMessage = ''; + + get officeName(): string { + return this.authService.getCredentials()?.officeName || ''; + } + + confirmPayout(): void { + if (!this.vendor || !this.recipientData || !this.clientId) { + this.errorMessage = this.translateService.instant('labels.text.Missing required information. Please try again.'); + return; + } + + this.isLoading = true; + this.errorMessage = ''; + + this.remittancesService + .confirmPayout(this.vendor.headerValue, this.transactionId, this.clientId, { + additionalInfo: { + comment: 'Payout confirmation from web-app' + } + }) + .subscribe({ + next: (response) => { + this.isLoading = false; + this.payoutConfirmed.emit(response); + }, + error: (err) => { + this.isLoading = false; + this.errorMessage = + err?.error?.message || + this.translateService.instant( + 'labels.text.Failed to confirm payment. Please try again or contact support.' + ); + } + }); + } +} diff --git a/src/app/remittances/remittance-stepper/payment-receipt-step/payment-receipt-step.component.html b/src/app/remittances/remittance-stepper/payment-receipt-step/payment-receipt-step.component.html new file mode 100644 index 000000000..53dd72f58 --- /dev/null +++ b/src/app/remittances/remittance-stepper/payment-receipt-step/payment-receipt-step.component.html @@ -0,0 +1,103 @@ + + +
+
+ {{ receiptIcon }} +

{{ receiptTitle }}

+
+ + @if (remittanceData) { +
+
+

{{ 'labels.heading.Transaction Details' | translate }}

+ @if (officeName) { +
+ {{ 'labels.inputs.Office' | translate }} + {{ officeName }} +
+ } +
+ {{ 'labels.inputs.External ID' | translate }} + {{ remittanceData.pin }} +
+
+ {{ 'labels.inputs.Status' | translate }} + {{ payoutConfirmation?.transaction?.status || remittanceData.status }} +
+
+ {{ 'labels.inputs.Payout Amount' | translate }} + {{ + remittanceData.receivingAmount | currency: remittanceData.receivingCurrency + }} +
+
+ {{ 'labels.inputs.Sender' | translate }} + {{ remittanceData.senderName }} +
+
+ +
+

{{ 'labels.heading.Recipient Details' | translate }}

+
+ {{ 'labels.inputs.Name' | translate }} + @if (recipientData?.givenName) { + {{ recipientData.givenName }} {{ recipientData.lastName }} + } @else { + {{ remittanceData?.recipientName }} + } +
+ @if (recipientData?.primaryDocument?.documentType) { +
+ {{ 'labels.inputs.Document' | translate }} + {{ recipientData.primaryDocument.documentType }} - + {{ recipientData.primaryDocument.documentNumber }} +
+ } +
+ + @if (payoutConfirmation) { +
+

{{ 'labels.heading.Confirmation Details' | translate }}

+ @if (payoutConfirmation?.transaction) { +
+ {{ 'labels.inputs.Status' | translate }} + {{ payoutConfirmation.transaction.status }} +
+ @if (payoutAssignment?.trackingNumber) { +
+ {{ 'labels.inputs.Reference Number' | translate }} + {{ payoutAssignment.trackingNumber }} +
+ } +
+ {{ 'labels.inputs.Payout Amount' | translate }} + {{ + payoutConfirmation.transaction.receivingAmount + | currency: payoutConfirmation.transaction.receivingCurrency + }} +
+ } +
+ } +
+ } + +
+ + +
+
diff --git a/src/app/remittances/remittance-stepper/payment-receipt-step/payment-receipt-step.component.scss b/src/app/remittances/remittance-stepper/payment-receipt-step/payment-receipt-step.component.scss new file mode 100644 index 000000000..0a9632348 --- /dev/null +++ b/src/app/remittances/remittance-stepper/payment-receipt-step/payment-receipt-step.component.scss @@ -0,0 +1,192 @@ +/** + * Copyright since 2025 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/. + */ + +.step-content { + padding: 16px 0; +} + +.receipt-header { + display: flex; + flex-direction: column; + align-items: center; + padding: 24px; + margin-bottom: 24px; + border-radius: 8px; + text-align: center; + + &.success { + background-color: #e8f5e9; + + .receipt-icon { + color: #2e7d32; + } + + h3 { + color: #2e7d32; + } + } + + &.terminal { + background-color: #fff3e0; + + .receipt-icon { + color: #e65100; + } + + h3 { + color: #e65100; + } + } + + .receipt-icon { + font-size: 48px; + width: 48px; + height: 48px; + margin-bottom: 8px; + } + + h3 { + margin: 0; + font-weight: 500; + } +} + +.receipt-body { + border: 1px solid #e0e0e0; + border-radius: 8px; + overflow: hidden; + margin-bottom: 24px; +} + +.receipt-section { + padding: 16px; + + &:not(:last-child) { + border-bottom: 1px solid #e0e0e0; + } + + h4 { + margin: 0 0 12px; + font-weight: 500; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.5px; + } +} + +.receipt-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 0; + + .label { + font-size: 13px; + } + + .value { + font-size: 14px; + font-weight: 500; + + &.amount { + font-size: 18px; + font-weight: 700; + color: #2e7d32; + } + + &.status-text { + text-transform: uppercase; + font-size: 12px; + letter-spacing: 0.5px; + } + + &.mono { + font-family: monospace; + font-size: 12px; + background: #f5f5f5; + padding: 2px 8px; + border-radius: 4px; + } + } +} + +.step-actions { + display: flex; + gap: 12px; + margin-top: 16px; +} + +@media print { + .step-actions { + display: none; + } + + .step-content { + padding: 0; + } + + .receipt-header { + border: none; + padding: 16px; + margin-bottom: 16px; + + &.success { + background-color: transparent; + border-bottom: 2px solid #000; + + .receipt-icon { + color: #000; + } + + h3 { + color: #000; + } + } + + &.terminal { + background-color: transparent; + border-bottom: 2px solid #000; + + .receipt-icon { + color: #000; + } + + h3 { + color: #000; + } + } + } + + .receipt-body { + border: 1px solid #000; + } + + .receipt-section { + border-color: #000 !important; + + h4 { + color: #000; + } + } + + .receipt-row { + .label, + .value { + color: #000; + } + + .value.amount { + color: #000; + } + + .value.mono { + background: transparent; + border: 1px solid #ccc; + } + } +} diff --git a/src/app/remittances/remittance-stepper/payment-receipt-step/payment-receipt-step.component.ts b/src/app/remittances/remittance-stepper/payment-receipt-step/payment-receipt-step.component.ts new file mode 100644 index 000000000..e675bd0ec --- /dev/null +++ b/src/app/remittances/remittance-stepper/payment-receipt-step/payment-receipt-step.component.ts @@ -0,0 +1,76 @@ +/** + * Copyright since 2025 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 { Component, EventEmitter, Input, Output, inject } from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; +import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module'; + +import { + RemittanceTransaction, + PayoutAssignmentResponse, + PayoutConfirmationResponse +} from '../../models/remittance.model'; +import { ValidatedRecipient } from '../../models/beneficiary.model'; +import { RemittanceStatus, isTerminalStatus } from '../../models/remittance-status.enum'; +import { AuthenticationService } from '../../../core/authentication/authentication.service'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'mifosx-payment-receipt-step', + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatIcon + ], + templateUrl: './payment-receipt-step.component.html', + styleUrls: ['./payment-receipt-step.component.scss'] +}) +export class PaymentReceiptStepComponent { + private authService = inject(AuthenticationService); + private translateService = inject(TranslateService); + + @Input() remittanceData: RemittanceTransaction | null = null; + @Input() recipientData: ValidatedRecipient | null = null; + @Input() payoutConfirmation: PayoutConfirmationResponse | null = null; + @Input() payoutAssignment: PayoutAssignmentResponse | null = null; + @Input() parsedStatus: RemittanceStatus = RemittanceStatus.UNKNOWN; + @Output() resetWorkflow = new EventEmitter(); + + get officeName(): string { + return this.authService.getCredentials()?.officeName || ''; + } + + get isPaid(): boolean { + return this.parsedStatus === RemittanceStatus.PAID; + } + + get isTerminal(): boolean { + return isTerminalStatus(this.parsedStatus) && !this.isPaid; + } + + get receiptIcon(): string { + if (this.isPaid) return 'check_circle'; + if (this.isTerminal) return 'cancel'; + return 'info'; + } + + get receiptTitle(): string { + if (this.isPaid) return this.translateService.instant('labels.heading.Payment Successful'); + if (this.isTerminal) + return this.translateService.instant('labels.heading.Transaction') + ' ' + (this.remittanceData?.status || ''); + return this.translateService.instant('labels.heading.Transaction Status'); + } + + onNewRemittance(): void { + this.resetWorkflow.emit(); + } + + printReceipt(): void { + window.print(); + } +} diff --git a/src/app/remittances/remittance-stepper/remittance-details-step/remittance-details-step.component.html b/src/app/remittances/remittance-stepper/remittance-details-step/remittance-details-step.component.html new file mode 100644 index 000000000..11e61259b --- /dev/null +++ b/src/app/remittances/remittance-stepper/remittance-details-step/remittance-details-step.component.html @@ -0,0 +1,92 @@ + + +@if (remittanceData) { +
+
+ {{ remittanceData.status }} +
+ +
+ @if (remittanceData.sendingPartner) { +
+ {{ 'labels.inputs.Sent By' | translate }} + {{ remittanceData.sendingPartner }} +
+ } + +
+ {{ 'labels.inputs.External ID' | translate }} + {{ remittanceData.pin }} +
+ +
+ {{ 'labels.inputs.Created At' | translate }} + @if (isValidDate(remittanceData.created)) { + {{ remittanceData.created | date: 'medium' }} + } @else { + {{ remittanceData.created }} + } +
+ + @if (remittanceData.processedAt && isValidDate(remittanceData.processedAt)) { +
+ {{ 'labels.inputs.Deposited At' | translate }} + {{ remittanceData.processedAt | date: 'medium' }} +
+ } + +
+ {{ 'labels.inputs.Sender Name' | translate }} + {{ remittanceData.senderName }} +
+ +
+ {{ 'labels.inputs.Payout Amount' | translate }} + {{ remittanceData.receivingAmount | currency: remittanceData.receivingCurrency }} +
+ +
+ {{ 'labels.inputs.Recipient Name' | translate }} + {{ remittanceData.recipientName }} +
+ + @if (remittanceData.recipientAddress) { +
+ {{ 'labels.inputs.Address' | translate }} + {{ remittanceData.recipientAddress }} +
+ } +
+ + @if (isPaid) { + + } @else if (isTerminal) { +
+ info + {{ + 'labels.text.This remittance has a terminal status and cannot be processed further.' | translate + }} +
+ } + +
+ +
+
+} diff --git a/src/app/remittances/remittance-stepper/remittance-details-step/remittance-details-step.component.scss b/src/app/remittances/remittance-stepper/remittance-details-step/remittance-details-step.component.scss new file mode 100644 index 000000000..91befdbe6 --- /dev/null +++ b/src/app/remittances/remittance-stepper/remittance-details-step/remittance-details-step.component.scss @@ -0,0 +1,104 @@ +/** + * Copyright since 2025 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/. + */ + +.step-content { + padding: 16px 0; +} + +.status-row { + margin-bottom: 16px; +} + +.status-badge { + display: inline-block; + padding: 4px 12px; + border-radius: 16px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.badge-pending { + background-color: #fff3e0; + color: #e65100; +} + +.badge-ready { + background-color: #e3f2fd; + color: #1565c0; +} + +.badge-paid { + background-color: #e8f5e9; + color: #2e7d32; +} + +.badge-terminal { + background-color: #ffebee; + color: #c62828; +} + +.badge-unknown { + background-color: #f5f5f5; + color: #616161; +} + +.details-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 100%)); + gap: 16px; + margin-bottom: 16px; +} + +.detail-item { + display: flex; + flex-direction: column; + gap: 4px; + + .label { + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .value { + font-size: 14px; + } +} + +.terminal-warning, +.terminal-info { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + margin: 16px 0; + border-radius: 4px; + + mat-icon { + font-size: 20px; + width: 20px; + height: 20px; + } +} + +.terminal-warning { + background-color: #fff3e0; + color: #e65100; +} + +.paid-info { + background-color: #e8f5e9; + color: #2e7d32; +} + +.step-actions { + margin-top: 16px; +} diff --git a/src/app/remittances/remittance-stepper/remittance-details-step/remittance-details-step.component.ts b/src/app/remittances/remittance-stepper/remittance-details-step/remittance-details-step.component.ts new file mode 100644 index 000000000..16cb26417 --- /dev/null +++ b/src/app/remittances/remittance-stepper/remittance-details-step/remittance-details-step.component.ts @@ -0,0 +1,65 @@ +/** + * Copyright since 2025 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 { Component, EventEmitter, Input, Output } from '@angular/core'; +import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module'; +import { MatIcon } from '@angular/material/icon'; + +import { RemittanceTransaction } from '../../models/remittance.model'; +import { RemittanceStatus, isTerminalStatus } from '../../models/remittance-status.enum'; + +@Component({ + selector: 'mifosx-remittance-details-step', + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatIcon + ], + templateUrl: './remittance-details-step.component.html', + styleUrls: ['./remittance-details-step.component.scss'] +}) +export class RemittanceDetailsStepComponent { + @Input() remittanceData: RemittanceTransaction | null = null; + @Input() parsedStatus: RemittanceStatus = RemittanceStatus.UNKNOWN; + @Output() detailsNext = new EventEmitter(); + + get isTerminal(): boolean { + return isTerminalStatus(this.parsedStatus); + } + + get isPaid(): boolean { + return this.parsedStatus === RemittanceStatus.PAID; + } + + get statusBadgeClass(): string { + switch (this.parsedStatus) { + case RemittanceStatus.PENDING: + return 'badge-pending'; + case RemittanceStatus.READY_FOR_PAYOUT: + return 'badge-ready'; + case RemittanceStatus.PAID: + return 'badge-paid'; + case RemittanceStatus.REJECTED: + case RemittanceStatus.CANCELED: + case RemittanceStatus.EXPIRED: + return 'badge-terminal'; + default: + return 'badge-unknown'; + } + } + + isValidDate(value: string | undefined): boolean { + if (!value) return false; + const timestamp = Date.parse(value); + return !isNaN(timestamp); + } + + onNext(): void { + this.detailsNext.emit(); + } +} diff --git a/src/app/remittances/remittance-stepper/search-beneficiary-step/search-beneficiary-step.component.html b/src/app/remittances/remittance-stepper/search-beneficiary-step/search-beneficiary-step.component.html new file mode 100644 index 000000000..d5522c15d --- /dev/null +++ b/src/app/remittances/remittance-stepper/search-beneficiary-step/search-beneficiary-step.component.html @@ -0,0 +1,77 @@ + + +
+
+
+ + {{ 'labels.inputs.First Name' | translate }} + + @if (beneficiaryForm.get('givenName')?.hasError('required') && beneficiaryForm.get('givenName')?.touched) { + {{ 'labels.inputs.First name is required' | translate }} + } + + + + {{ 'labels.inputs.Last Name' | translate }} + + @if (beneficiaryForm.get('lastName')?.hasError('required') && beneficiaryForm.get('lastName')?.touched) { + {{ 'labels.inputs.Last name is required' | translate }} + } + + + + {{ 'labels.inputs.Document Type' | translate }} + + @for (doc of documentTypes; track doc.value) { + {{ doc.label | translate }} + } + + + + + {{ 'labels.inputs.Document Number' | translate }} + + @if ( + beneficiaryForm.get('documentNumber')?.hasError('required') && beneficiaryForm.get('documentNumber')?.touched + ) { + {{ 'labels.inputs.Document number is required' | translate }} + } + +
+ + @if (errorMessage) { +
+ error_outline + {{ errorMessage }} +
+ } + +
+ +
+
+
diff --git a/src/app/remittances/remittance-stepper/search-beneficiary-step/search-beneficiary-step.component.scss b/src/app/remittances/remittance-stepper/search-beneficiary-step/search-beneficiary-step.component.scss new file mode 100644 index 000000000..e64a4563e --- /dev/null +++ b/src/app/remittances/remittance-stepper/search-beneficiary-step/search-beneficiary-step.component.scss @@ -0,0 +1,47 @@ +/** + * Copyright since 2025 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/. + */ + +.step-content { + padding: 16px 0; +} + +.form-fields { + display: flex; + flex-direction: column; + gap: 8px; + max-width: 480px; +} + +.error-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + margin: 8px 0; + background-color: #fdecea; + border-radius: 4px; + color: #611a15; + + mat-icon { + color: #f44336; + font-size: 20px; + width: 20px; + height: 20px; + } +} + +.step-actions { + margin-top: 16px; + + button { + mat-spinner { + display: inline-block; + margin-right: 8px; + } + } +} diff --git a/src/app/remittances/remittance-stepper/search-beneficiary-step/search-beneficiary-step.component.ts b/src/app/remittances/remittance-stepper/search-beneficiary-step/search-beneficiary-step.component.ts new file mode 100644 index 000000000..1b3cf854a --- /dev/null +++ b/src/app/remittances/remittance-stepper/search-beneficiary-step/search-beneficiary-step.component.ts @@ -0,0 +1,125 @@ +/** + * Copyright since 2025 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 { Component, EventEmitter, Input, OnInit, Output, inject } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatIcon } from '@angular/material/icon'; +import { TranslateService } from '@ngx-translate/core'; + +import { RemittancesService } from '../../remittances.service'; +import { RemittanceVendor } from '../../models/remittance.model'; +import { ValidatedRecipient } from '../../models/beneficiary.model'; + +@Component({ + selector: 'mifosx-search-beneficiary-step', + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatProgressSpinner, + MatIcon + ], + templateUrl: './search-beneficiary-step.component.html', + styleUrls: ['./search-beneficiary-step.component.scss'] +}) +export class SearchBeneficiaryStepComponent implements OnInit { + @Input() vendor: RemittanceVendor | null = null; + @Input() transactionId = ''; + @Output() recipientValidated = new EventEmitter(); + + private fb = inject(FormBuilder); + private remittancesService = inject(RemittancesService); + private translateService = inject(TranslateService); + + beneficiaryForm!: FormGroup; + isLoading = false; + errorMessage = ''; + + documentTypes = [ + { value: 'NATIONAL_ID', label: 'labels.inputs.National ID' }, + { value: 'PASSPORT', label: 'labels.inputs.Passport' }, + { value: 'DRIVERS_LICENSE', label: "labels.inputs.Driver's License" }, + { value: 'VOTER_ID', label: 'labels.inputs.Voter ID' } + ]; + + ngOnInit(): void { + this.beneficiaryForm = this.fb.group({ + givenName: [ + '', + Validators.required + ], + lastName: [ + '', + Validators.required + ], + documentType: [ + 'NATIONAL_ID', + Validators.required + ], + documentNumber: [ + '', + [ + Validators.required, + Validators.minLength(3) + ] + ] + }); + } + + validateRecipient(): void { + if (this.beneficiaryForm.invalid || !this.vendor) return; + + this.isLoading = true; + this.errorMessage = ''; + + const { givenName, lastName, documentType, documentNumber } = this.beneficiaryForm.value; + + this.remittancesService + .validateRecipient(this.vendor.headerValue, this.transactionId, { + givenName, + lastName, + primaryDocument: { + documentType, + documentNumber + } + }) + .subscribe({ + next: (recipient) => { + this.isLoading = false; + if (recipient && recipient.valid) { + // Merge form data into recipient so details show in Confirm step + const enrichedRecipient: ValidatedRecipient = { + ...recipient, + firstName: givenName, + givenName: givenName, + lastName: lastName, + primaryDocument: { + documentType, + documentNumber, + issuingCountry: '' + } + }; + this.recipientValidated.emit(enrichedRecipient); + } else { + this.errorMessage = this.translateService.instant( + 'labels.text.Recipient is not valid for this transaction. Please check the details.' + ); + } + }, + error: (err) => { + this.isLoading = false; + this.errorMessage = + err?.error?.message || + this.translateService.instant( + 'labels.text.Recipient validation failed. Please check the document details and try again.' + ); + } + }); + } +} diff --git a/src/app/remittances/remittance-stepper/search-remittance-step/search-remittance-step.component.html b/src/app/remittances/remittance-stepper/search-remittance-step/search-remittance-step.component.html new file mode 100644 index 000000000..48f3def5c --- /dev/null +++ b/src/app/remittances/remittance-stepper/search-remittance-step/search-remittance-step.component.html @@ -0,0 +1,81 @@ + + +
+
+
+ + {{ 'labels.inputs.Vendor' | translate }} + + @for (v of vendors; track v.name) { + {{ v.name }} + } + + @if (isLoadingVendors) { + {{ 'labels.inputs.Loading vendors...' | translate }} + } + @if (vendorLoadError) { + {{ 'labels.inputs.Failed to load vendors' | translate }} + } + + + + {{ 'labels.inputs.External Reference ID' | translate }} + + @if (searchForm.get('externalId')?.hasError('required') && searchForm.get('externalId')?.touched) { + {{ 'labels.inputs.Reference ID is required' | translate }} + } + @if (searchForm.get('externalId')?.hasError('minlength')) { + {{ 'labels.inputs.Minimum 3 characters' | translate }} + } + +
+ + @if (vendorLoadError) { +
+ warning + {{ 'labels.text.Unable to load vendors. Please check your connection and try again.' | translate }} + +
+ } + + @if (errorMessage) { +
+ error_outline + {{ errorMessage }} +
+ } + +
+ +
+
+
diff --git a/src/app/remittances/remittance-stepper/search-remittance-step/search-remittance-step.component.scss b/src/app/remittances/remittance-stepper/search-remittance-step/search-remittance-step.component.scss new file mode 100644 index 000000000..ec5cc09d1 --- /dev/null +++ b/src/app/remittances/remittance-stepper/search-remittance-step/search-remittance-step.component.scss @@ -0,0 +1,90 @@ +/** + * Copyright since 2025 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/. + */ + +.step-content { + padding: 16px 0; +} + +.form-fields { + display: flex; + flex-direction: column; + gap: 8px; + max-width: 480px; +} + +.error-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + margin: 8px 0; + background-color: #fdecea; + border-radius: 4px; + color: #611a15; + + mat-icon { + color: #f44336; + font-size: 20px; + width: 20px; + height: 20px; + } +} + +.vendor-error-banner { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + margin: 12px 0; + background-color: #fff3e0; + border-radius: 4px; + border-left: 4px solid #ff9800; + color: #e65100; + + mat-icon { + color: #ff9800; + font-size: 24px; + width: 24px; + height: 24px; + flex-shrink: 0; + } + + span { + flex: 1; + font-size: 14px; + } + + button { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 6px; + + mat-spinner { + display: inline-block; + } + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + margin: 0; + } + } +} + +.step-actions { + margin-top: 16px; + + button { + mat-spinner { + display: inline-block; + margin-right: 8px; + } + } +} diff --git a/src/app/remittances/remittance-stepper/search-remittance-step/search-remittance-step.component.ts b/src/app/remittances/remittance-stepper/search-remittance-step/search-remittance-step.component.ts new file mode 100644 index 000000000..3d8890e0f --- /dev/null +++ b/src/app/remittances/remittance-stepper/search-remittance-step/search-remittance-step.component.ts @@ -0,0 +1,114 @@ +/** + * Copyright since 2025 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 { Component, EventEmitter, OnInit, Output, inject } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatIcon } from '@angular/material/icon'; + +import { TranslateService } from '@ngx-translate/core'; +import { RemittancesService } from '../../remittances.service'; +import { RemittanceTransaction, RemittanceVendor } from '../../models/remittance.model'; + +@Component({ + selector: 'mifosx-search-remittance-step', + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatProgressSpinner, + MatIcon + ], + templateUrl: './search-remittance-step.component.html', + styleUrls: ['./search-remittance-step.component.scss'] +}) +export class SearchRemittanceStepComponent implements OnInit { + @Output() remittanceFound = new EventEmitter<{ + transaction: RemittanceTransaction; + transactionId: string; + vendor: RemittanceVendor; + }>(); + + private fb = inject(FormBuilder); + private remittancesService = inject(RemittancesService); + private translateService = inject(TranslateService); + + searchForm!: FormGroup; + vendors: RemittanceVendor[] = []; + isLoading = false; + isLoadingVendors = false; + vendorLoadError = false; + errorMessage = ''; + + ngOnInit(): void { + this.searchForm = this.fb.group({ + vendor: [ + '', + Validators.required + ], + externalId: [ + '', + [ + Validators.required, + Validators.minLength(3) + ] + ] + }); + + this.loadVendors(); + } + + loadVendors(): void { + this.isLoadingVendors = true; + this.vendorLoadError = false; + this.remittancesService.getVendors().subscribe({ + next: (response) => { + this.vendors = response.vendors; + this.isLoadingVendors = false; + this.vendorLoadError = false; + if (this.vendors.length === 1) { + this.searchForm.patchValue({ vendor: this.vendors[0].headerValue }); + } + }, + error: () => { + this.vendorLoadError = true; + this.isLoadingVendors = false; + } + }); + } + + searchRemittance(): void { + this.searchForm.markAllAsTouched(); + if (this.searchForm.invalid) return; + + this.isLoading = true; + this.errorMessage = ''; + + const { vendor, externalId } = this.searchForm.value; + const selectedVendor = this.vendors.find((v) => v.headerValue === vendor); + + this.remittancesService.findRemittance(vendor, externalId).subscribe({ + next: (transaction) => { + this.isLoading = false; + this.remittanceFound.emit({ + transaction, + transactionId: transaction.externalTransactionId || externalId, + vendor: selectedVendor || { name: vendor, headerValue: vendor } + }); + }, + error: (err) => { + this.isLoading = false; + this.errorMessage = + err?.error?.message || + this.translateService.instant( + 'labels.text.Remittance not found. Please check the reference number and try again.' + ); + } + }); + } +} diff --git a/src/app/remittances/remittance-stepper/transactional-profile-step/transactional-profile-step.component.html b/src/app/remittances/remittance-stepper/transactional-profile-step/transactional-profile-step.component.html new file mode 100644 index 000000000..10c1d03f2 --- /dev/null +++ b/src/app/remittances/remittance-stepper/transactional-profile-step/transactional-profile-step.component.html @@ -0,0 +1,145 @@ + + +
+

{{ 'labels.heading.Transactional Profile' | translate }}

+

+ {{ + 'labels.text.Register the transactional profile for the beneficiary. Add entries for individuals or legal entities.' + | translate + }} +

+ + +
+ + + person + {{ 'labels.inputs.Individual' | translate }} + + + business + {{ 'labels.inputs.Legal Entity' | translate }} + + +
+ + +
+ @if (profileData.length > 0) { + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.# Remittance' | translate }}{{ row.remittanceNumber }}{{ 'labels.inputs.Avg Amount/Transaction' | translate }}{{ row.averageAmount | currency: row.currency }}{{ 'labels.inputs.# Senders' | translate }}{{ row.senderCount }}{{ 'labels.inputs.Sender Registry' | translate }}{{ row.senderRegistry }}
+ } + + @if (profileData.length === 0) { +
+ table_chart +

{{ 'labels.text.No transactional profile entries yet. Add one below.' | translate }}

+
+ } +
+ + +
+

{{ 'labels.heading.Add Profile Entry' | translate }}

+
+ + {{ 'labels.inputs.# Remittance' | translate }} + + + + + {{ 'labels.inputs.Avg Amount/Transaction' | translate }} + + + + + {{ 'labels.inputs.# Senders' | translate }} + + + + + {{ 'labels.inputs.Sender Registry' | translate }} + + + + +
+
+ + @if (errorMessage) { +
+ error_outline + {{ errorMessage }} +
+ } + +
+ +
+
diff --git a/src/app/remittances/remittance-stepper/transactional-profile-step/transactional-profile-step.component.scss b/src/app/remittances/remittance-stepper/transactional-profile-step/transactional-profile-step.component.scss new file mode 100644 index 000000000..5206058d0 --- /dev/null +++ b/src/app/remittances/remittance-stepper/transactional-profile-step/transactional-profile-step.component.scss @@ -0,0 +1,150 @@ +/** + * Copyright since 2025 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/. + */ + +.step-content { + padding: 16px 0; + + h3 { + margin: 0 0 8px; + font-weight: 500; + } + + .description { + margin: 0 0 24px; + font-size: 14px; + } +} + +.entity-type-toggle { + margin-bottom: 20px; + + mat-button-toggle-group { + border-radius: 8px; + } + + mat-icon { + margin-right: 4px; + font-size: 18px; + width: 18px; + height: 18px; + vertical-align: middle; + } +} + +.table-section { + margin-bottom: 24px; + + .profile-table { + width: 100%; + border: 1px solid #e0e0e0; + border-radius: 8px; + overflow: hidden; + + th.mat-mdc-header-cell { + font-weight: 600; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + td.mat-mdc-cell { + font-size: 14px; + } + + .clickable-row { + cursor: pointer; + + &:hover { + background-color: #fff3f3; + } + } + } + + .empty-table { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 32px 16px; + background-color: #fafafa; + border: 1px dashed #e0e0e0; + border-radius: 8px; + + mat-icon { + font-size: 40px; + width: 40px; + height: 40px; + margin-bottom: 8px; + } + + p { + margin: 0; + font-size: 14px; + } + } +} + +.add-entry-section { + margin-bottom: 24px; + padding: 16px; + border-radius: 8px; + border: 1px solid #e0e0e0; + + h4 { + margin: 0 0 12px; + font-weight: 500; + font-size: 14px; + } + + .entry-form { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: flex-end; + + mat-form-field { + flex: 1 1 180px; + min-width: 160px; + } + + button { + margin-bottom: 3.5em; + margin-left: auto; + white-space: nowrap; + } + } +} + +.error-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + margin: 8px 0; + background-color: #fdecea; + border-radius: 4px; + color: #611a15; + + mat-icon { + color: #f44336; + font-size: 20px; + width: 20px; + height: 20px; + } +} + +.step-actions { + margin-top: 16px; + + button { + mat-spinner { + display: inline-block; + margin-right: 8px; + } + } +} diff --git a/src/app/remittances/remittance-stepper/transactional-profile-step/transactional-profile-step.component.ts b/src/app/remittances/remittance-stepper/transactional-profile-step/transactional-profile-step.component.ts new file mode 100644 index 000000000..9e4a55115 --- /dev/null +++ b/src/app/remittances/remittance-stepper/transactional-profile-step/transactional-profile-step.component.ts @@ -0,0 +1,167 @@ +/** + * Copyright since 2025 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 { Component, EventEmitter, Input, Output, inject, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators, FormsModule } from '@angular/forms'; +import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatIcon } from '@angular/material/icon'; +import { MatTableModule } from '@angular/material/table'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; + +import { TranslateService } from '@ngx-translate/core'; +import { RemittancesService } from '../../remittances.service'; +import { RemittanceTransaction, RemittanceVendor, PayoutAssignmentResponse } from '../../models/remittance.model'; +import { ValidatedRecipient } from '../../models/beneficiary.model'; + +/** + * Represents a single row in the Transactional Profile data table. + * As required by the spec: # Remittance, Avg Amount/Transaction, # Senders, Sender Registry. + */ +export interface TransactionalProfileRow { + remittanceNumber: string; + averageAmount: number; + currency: string; + senderCount: number; + senderRegistry: string; +} + +@Component({ + selector: 'mifosx-transactional-profile-step', + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatProgressSpinner, + MatIcon, + MatTableModule, + MatButtonToggleModule, + FormsModule + ], + templateUrl: './transactional-profile-step.component.html', + styleUrls: ['./transactional-profile-step.component.scss'] +}) +export class TransactionalProfileStepComponent implements OnInit { + @Input() vendor: RemittanceVendor | null = null; + @Input() transactionId = ''; + @Input() remittanceData: RemittanceTransaction | null = null; + @Input() recipientData: ValidatedRecipient | null = null; + @Output() payoutAssigned = new EventEmitter(); + + private remittancesService = inject(RemittancesService); + private fb = inject(FormBuilder); + private translateService = inject(TranslateService); + + isLoading = false; + errorMessage = ''; + + /** Transactional profile data table */ + displayedColumns: string[] = [ + 'remittanceNumber', + 'averageAmount', + 'senderCount', + 'senderRegistry' + ]; + profileData: TransactionalProfileRow[] = []; + + /** Form for adding new profile entries */ + profileForm!: FormGroup; + + /** Entity type toggle: 'individual' or 'legal' */ + entityType: 'individual' | 'legal' = 'individual'; + + ngOnInit(): void { + this.profileForm = this.fb.group({ + remittanceNumber: [ + '', + Validators.required + ], + averageAmount: [ + '', + [ + Validators.required, + Validators.min(0) + ] + ], + senderCount: [ + 0, + [ + Validators.required, + Validators.min(0) + ] + ], + senderRegistry: [ + '', + Validators.required + ] + }); + + // Pre-populate from the current transaction if available + if (this.remittanceData) { + this.profileData = [ + { + remittanceNumber: this.remittanceData.pin, + averageAmount: this.remittanceData.receivingAmount, + currency: this.remittanceData.receivingCurrency, + senderCount: 1, + senderRegistry: this.remittanceData.senderName + } + ]; + } + } + + addProfileEntry(): void { + if (this.profileForm.invalid) return; + const val = this.profileForm.value; + this.profileData = [ + ...this.profileData, + { + remittanceNumber: val.remittanceNumber, + averageAmount: val.averageAmount, + currency: this.remittanceData?.receivingCurrency || 'USD', + senderCount: Math.max(1, val.senderCount || 1), + senderRegistry: val.senderRegistry + } + ]; + this.profileForm.reset({ + senderCount: 1 + }); + } + + removeProfileEntry(index: number): void { + this.profileData = this.profileData.filter((_, i) => i !== index); + } + + assignPayout(): void { + if (!this.vendor || !this.recipientData) return; + + this.isLoading = true; + this.errorMessage = ''; + + this.remittancesService + .assignPayout(this.vendor.headerValue, this.transactionId, { + additionalInfo: { + comment: 'Payout assignment from web-app', + transactionalProfile: JSON.stringify(this.profileData), + entityType: this.entityType, + profileEntryCount: this.profileData.length.toString() + } + }) + .subscribe({ + next: (response) => { + this.isLoading = false; + this.payoutAssigned.emit(response); + }, + error: (err) => { + this.isLoading = false; + this.errorMessage = + err?.error?.message || + this.translateService.instant('labels.text.Failed to assign payout. Please try again.'); + } + }); + } +} diff --git a/src/app/remittances/remittance.config.ts b/src/app/remittances/remittance.config.ts new file mode 100644 index 000000000..fb0c83985 --- /dev/null +++ b/src/app/remittances/remittance.config.ts @@ -0,0 +1,19 @@ +/** + * Copyright since 2025 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 { environment } from '../../environments/environment'; + +/** + * Global configuration for the Remittance module. + * + * Reads from environment settings so the feature toggle is centralized. + * Set `mifosRemittanceEnabled` in the environment (or window.env) to control this. + */ +export const remittanceConfig = { + isRemittanceEnabled: environment.mifosRemittanceEnabled +}; diff --git a/src/app/remittances/remittances-routing.module.ts b/src/app/remittances/remittances-routing.module.ts new file mode 100644 index 000000000..5d35df3b4 --- /dev/null +++ b/src/app/remittances/remittances-routing.module.ts @@ -0,0 +1,42 @@ +/** + * Copyright since 2025 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 { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { Route } from '../core/route/route.service'; +import { ProcessRemittanceComponent } from './process-remittance/process-remittance.component'; +import { remittanceEnabledGuard } from './remittance-enabled.guard'; + +const routes: Routes = [ + Route.withShell([ + { + path: 'remittances', + canActivate: [remittanceEnabledGuard], + data: { title: 'Remittances', breadcrumb: 'Remittances' }, + children: [ + { + path: '', + redirectTo: 'process', + pathMatch: 'full' + }, + { + path: 'process', + component: ProcessRemittanceComponent, + data: { title: 'Process Remittance', breadcrumb: 'Process' } + } + ] + } + ]) +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class RemittancesRoutingModule {} diff --git a/src/app/remittances/remittances.module.ts b/src/app/remittances/remittances.module.ts new file mode 100644 index 000000000..05c2ea7e1 --- /dev/null +++ b/src/app/remittances/remittances.module.ts @@ -0,0 +1,41 @@ +/** + * Copyright since 2025 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 { NgModule } from '@angular/core'; +import { SharedModule } from 'app/shared/shared.module'; +import { PipesModule } from 'app/pipes/pipes.module'; +import { DirectivesModule } from 'app/directives/directives.module'; + +import { RemittancesRoutingModule } from './remittances-routing.module'; +import { ProcessRemittanceComponent } from './process-remittance/process-remittance.component'; +import { SearchRemittanceStepComponent } from './remittance-stepper/search-remittance-step/search-remittance-step.component'; +import { RemittanceDetailsStepComponent } from './remittance-stepper/remittance-details-step/remittance-details-step.component'; +import { SearchBeneficiaryStepComponent } from './remittance-stepper/search-beneficiary-step/search-beneficiary-step.component'; +import { BeneficiaryDetailsStepComponent } from './remittance-stepper/beneficiary-details-step/beneficiary-details-step.component'; +import { TransactionalProfileStepComponent } from './remittance-stepper/transactional-profile-step/transactional-profile-step.component'; +import { ConfirmPaymentStepComponent } from './remittance-stepper/confirm-payment-step/confirm-payment-step.component'; +import { PaymentReceiptStepComponent } from './remittance-stepper/payment-receipt-step/payment-receipt-step.component'; + +@NgModule({ + imports: [ + SharedModule, + PipesModule, + DirectivesModule, + RemittancesRoutingModule, + ProcessRemittanceComponent, + SearchRemittanceStepComponent, + RemittanceDetailsStepComponent, + SearchBeneficiaryStepComponent, + BeneficiaryDetailsStepComponent, + TransactionalProfileStepComponent, + ConfirmPaymentStepComponent, + PaymentReceiptStepComponent + ], + providers: [] +}) +export class RemittancesModule {} diff --git a/src/app/remittances/remittances.service.ts b/src/app/remittances/remittances.service.ts new file mode 100644 index 000000000..f62f3e602 --- /dev/null +++ b/src/app/remittances/remittances.service.ts @@ -0,0 +1,152 @@ +/** + * Copyright since 2025 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 { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpBackend } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { environment } from '../../environments/environment'; +import { + RemittanceTransaction, + RemittanceVendor, + PayoutAssignmentRequest, + PayoutAssignmentResponse, + PayoutConfirmationRequest, + PayoutConfirmationResponse +} from './models/remittance.model'; +import { ValidateRecipientRequest, ValidatedRecipient } from './models/beneficiary.model'; + +/** + * Remittances Service + * + * Handles all HTTP communication with the Mifos Remittance Microservice. + * Base URL pattern: {mifosRemittanceApiUrl}{mifosRemittanceApiProvider}{mifosRemittanceApiVersion}/remittances/:tenant/:vendor/... + * + * Auth: X-Gravitee-Api-Key header (injected server-side by the proxy) + */ +@Injectable({ + providedIn: 'root' +}) +export class RemittancesService { + private httpBackend = inject(HttpBackend); + + /** Separate HttpClient that bypasses interceptors (avoids Fineract API prefix) */ + private http = new HttpClient(this.httpBackend); + + private get baseUrl(): string { + return `${environment.mifosRemittanceApiUrl}${environment.mifosRemittanceApiProvider}${environment.mifosRemittanceApiVersion}`; + } + + private get tenant(): string { + return environment.fineractPlatformTenantId || 'default'; + } + + private getHeaders(): HttpHeaders { + return new HttpHeaders({ 'Content-Type': 'application/json' }); + } + + /** + * Endpoint 0: Get available remittance vendors for the tenant. + * GET /v1/remittances/:tenant/vendors + */ + getVendors(): Observable<{ vendors: RemittanceVendor[] }> { + return this.http.get<{ vendors: RemittanceVendor[] }>(`${this.baseUrl}/remittances/${this.tenant}/vendors`, { + headers: this.getHeaders() + }); + } + + /** + * Endpoint 1: Find remittance by external ID / PIN. + * GET /v1/remittances/:tenant/:vendor/transactions/:id + */ + findRemittance(vendor: string, externalId: string): Observable { + return this.http.get( + `${this.baseUrl}/remittances/${this.tenant}/${vendor}/transactions/${externalId}`, + { headers: this.getHeaders() } + ); + } + + /** + * Endpoint 2: Validate recipient for a transaction. + * POST /v1/remittances/:tenant/:vendor/transactions/:id/recipient + */ + validateRecipient( + vendor: string, + transactionId: string, + data: ValidateRecipientRequest + ): Observable { + return this.http.post( + `${this.baseUrl}/remittances/${this.tenant}/${vendor}/transactions/${transactionId}/recipient`, + data, + { headers: this.getHeaders() } + ); + } + + /** + * Endpoint 3: Assign transaction ready to payout (lock for payout). + * POST /v1/remittances/:tenant/:vendor/transactions/:id/payout-assignment + */ + assignPayout( + vendor: string, + transactionId: string, + body?: PayoutAssignmentRequest + ): Observable { + return this.http.post( + `${this.baseUrl}/remittances/${this.tenant}/${vendor}/transactions/${transactionId}/payout-assignment`, + body || {}, + { headers: this.getHeaders() } + ); + } + + /** + * Endpoint 4: Confirm transaction payout. + * POST /v1/remittances/:tenant/:vendor/transactions/:id/payout-confirmation/:clientId + */ + confirmPayout( + vendor: string, + transactionId: string, + clientId: number, + body?: PayoutConfirmationRequest + ): Observable { + return this.http.post( + `${this.baseUrl}/remittances/${this.tenant}/${vendor}/transactions/${transactionId}/payout-confirmation/${clientId}`, + body || {}, + { headers: this.getHeaders() } + ); + } + + /** + * Unassign transaction from payout (state management). + * DELETE /v1/remittances/:tenant/:vendor/transactions/:id/payout-assignment + */ + unassignPayout(vendor: string, transactionId: string, reason: string): Observable { + return this.http.request( + 'DELETE', + `${this.baseUrl}/remittances/${this.tenant}/${vendor}/transactions/${transactionId}/payout-assignment`, + { + headers: this.getHeaders(), + body: { reason } + } + ); + } + + /** + * Reverse payout confirmation (state management). + * DELETE /v1/remittances/:tenant/:vendor/transactions/:id/payout-confirmation + */ + reversePayoutConfirmation(vendor: string, transactionId: string, reason: string): Observable { + return this.http.request( + 'DELETE', + `${this.baseUrl}/remittances/${this.tenant}/${vendor}/transactions/${transactionId}/payout-confirmation`, + { + headers: this.getHeaders(), + body: { reason } + } + ); + } +} diff --git a/src/app/shared/icons.module.ts b/src/app/shared/icons.module.ts index 2f4814b62..88fdb5d48 100644 --- a/src/app/shared/icons.module.ts +++ b/src/app/shared/icons.module.ts @@ -64,6 +64,7 @@ import { faMinusCircle, faMinus, faMoneyBillAlt, + faMoneyBillWave, faMoneyCheck, faPencilAlt, faPlay, @@ -216,6 +217,7 @@ export class IconsModule { faMinus, faMinusCircle, faMoneyBillAlt, + faMoneyBillWave, faMoneyCheck, faPause, faPencilAlt, diff --git a/src/assets/env.js b/src/assets/env.js index e27da5a4b..07f7df643 100644 --- a/src/assets/env.js +++ b/src/assets/env.js @@ -90,6 +90,14 @@ window['env']['mifosInterbankTransfersApiVersion'] = ''; window['env']['mifosInterbankTransfersEnabled'] = 'true'; + // Remittance Module Environment variables + window['env']['mifosRemittanceApiUrl'] = ''; + window['env']['mifosRemittanceApiProvider'] = '/remittance-api'; + window['env']['mifosRemittanceApiVersion'] = '/v1'; + window['env']['mifosRemittanceEnabled'] = false; + window['env']['mifosRemittanceApiHeader'] = 'X-Gravitee-Api-Key'; + window['env']['mifosRemittanceApiKey'] = ''; + // Enable Role-Based Access Control (RBAC) for menu/button permissions // Set to true to enable RBAC, false (default) for backward compatibility window['env']['productionModeEnableRBAC'] = false; diff --git a/src/assets/env.template.js b/src/assets/env.template.js index ba01b2de0..98b4076b4 100644 --- a/src/assets/env.template.js +++ b/src/assets/env.template.js @@ -102,6 +102,14 @@ window['env']['mifosInterbankTransfersApiVersion'] = '$MIFOS_INTERBANK_TRANSFERS_API_VERSION'; window['env']['mifosInterbankTransfersEnabled'] = '$MIFOS_INTERBANK_TRANSFERS_ENABLED'; + // Remittance Module Environment variables + window['env']['mifosRemittanceApiUrl'] = '$MIFOS_REMITTANCE_API_URL'; + window['env']['mifosRemittanceApiProvider'] = '$MIFOS_REMITTANCE_API_PROVIDER'; + window['env']['mifosRemittanceApiVersion'] = '$MIFOS_REMITTANCE_API_VERSION'; + window['env']['mifosRemittanceEnabled'] = '$MIFOS_REMITTANCE_ENABLED'; + window['env']['mifosRemittanceApiHeader'] = '$MIFOS_REMITTANCE_API_HEADER'; + window['env']['mifosRemittanceApiKey'] = '$MIFOS_REMITTANCE_API_KEY'; + // Enable Role-Based Access Control (RBAC) for menu/button permissions // Set to 'true' to enable RBAC, 'false' (default) for backward compatibility window['env']['productionModeEnableRBAC'] = '$MIFOS_PRODUCTION_MODE_ENABLE_RBAC'; diff --git a/src/assets/translations/en-US.json b/src/assets/translations/en-US.json index 040532a6b..d75752da0 100644 --- a/src/assets/translations/en-US.json +++ b/src/assets/translations/en-US.json @@ -502,6 +502,7 @@ "Resend OTP": "Resend OTP", "Reset": "Reset", "Reset Password": "Reset Password", + "Retry": "Retry", "Retrieve": "Retrieve", "Revert Transaction": "Revert Transaction", "Run and Download Report": "Run and Download Report", @@ -561,7 +562,14 @@ "View Active Charges": "View Active Charges", "View Inactive Charges": "View Inactive Charges", "Yes": "Yes", - "Is Self Service": "Is Self Service" + "Is Self Service": "Is Self Service", + "Assign Payout": "Assign Payout", + "Confirm Payment": "Confirm Payment", + "New Remittance": "New Remittance", + "Print Receipt": "Print Receipt", + "Validate Recipient": "Validate Recipient", + "View Receipt": "View Receipt", + "Add Entry": "Add Entry" }, "catalogs": { "Interest payment waiver": "Interest Payment Waiver", @@ -1301,7 +1309,31 @@ "Date": "Date", "Secure & Reliable": "Secure & Reliable", "Community Driven": "Community Driven", - "Global Impact": "Global Impact" + "Global Impact": "Global Impact", + "Assign Payout": "Assign Payout", + "Assign to Mifos Client": "Assign to Mifos Client", + "Beneficiary Details": "Beneficiary Details", + "Confirm Payment": "Confirm Payment", + "Confirm Payout": "Confirm Payout", + "Confirmation Details": "Confirmation Details", + "Payment Receipt": "Payment Receipt", + "Payment Successful": "Payment Successful", + "Process Remittance": "Process Remittance", + "Recipient Details": "Recipient Details", + "Recipient Summary": "Recipient Summary", + "Remittance Details": "Remittance Details", + "Search Beneficiary": "Search Beneficiary", + "Search Remittance": "Search Remittance", + "Transaction": "Transaction", + "Transaction Status": "Transaction Status", + "Transaction Summary": "Transaction Summary", + "Transactional Profile": "Transactional Profile", + "Validated Recipient Information": "Validated Recipient Information", + "Recipient": "Recipient", + "Payout Assignment": "Payout Assignment", + "Identification": "Identification", + "Professional Information": "Professional Information", + "Add Profile Entry": "Add Profile Entry" }, "inputs": { "Floating rate": "Floating Rate", @@ -1519,6 +1551,7 @@ "Client Details": "Client Details", "Client ID": "Client ID", "Client Id": "Client Id", + "Confirmation Number": "Confirmation Number", "Client Member": "Client Member", "Client Members": "Client Members", "Client Name": "Client Name", @@ -1707,6 +1740,9 @@ "Do not calculate interest on past due principal balances": "Do not calculate interest on past due principal balances", "Documents": "Documents", "Document Key": "Document Key", + "Document": "Document", + "Document Number": "Document Number", + "Document number is required": "Document number is required", "Document Type": "Document Type", "Dormancy Tracking": "Dormancy Tracking", "Download": "Download", @@ -1767,6 +1803,8 @@ "Extend Repayment Period": "Extend Repayment Period", "External Asset Owner": "External Asset Owner", "External ID": "External ID", + "Failed to load vendors": "Failed to load vendors", + "External Reference ID": "External Reference ID", "External Id": "External Id", "External id": "External id", "External ID not found": "External ID not found", @@ -1795,6 +1833,7 @@ "Filter By Version": "Filter By Version", "Financial Activity": "Financial Activity", "First Name": "First Name", + "First name is required": "First name is required", "First repayment on": "First repayment on", "Fixed Account": "Fixed Account", "Fixed Deposit": "Fixed Deposit", @@ -1986,6 +2025,7 @@ "Last Active Transaction Date": "Last Active Transaction Date", "Last Loan Amount": "Last Loan Amount", "Last Name": "Last Name", + "Last name is required": "Last name is required", "Last Repayment": "Last Repayment", "Last Repayment Date": "Last Repayment Date", "Late": "Late", @@ -1995,6 +2035,7 @@ "Liability Account": "Liability Account", "Liability Account (CR)": "Liability Account (CR)", "Lifted On": "Lifted On", + "Loading vendors...": "Loading vendors...", "Link savings": "Link savings", "Linked Savings Account": "Linked Savings Account", "Linked Savings Account(Dividend Posting)": "Linked Savings Account(Dividend Posting)", @@ -2426,6 +2467,7 @@ "Schedule Date": "Schedule Date", "Score": "Score", "Screen Report": "Screen Report", + "Search Client": "Search Client", "Search Journal Entries": "Search Journal Entries", "Select Action": "Select Action", "Select Client Members for Transfer": "Select Client Members for Transfer", @@ -2570,6 +2612,7 @@ "Trigger Type": "Trigger Type", "Tuesday": "Tuesday", "Type": "Type", + "Type client name or account number": "Type client name or account number", "Type/Quality": "Type/Quality", "Unassigned": "Unassigned", "Unassignment Date": "Unassignment Date", @@ -2692,7 +2735,45 @@ "Overdue": "Overdue", "Overdue Fees": "Overdue Fees", "Select action": "Select action", - "Select entity": "Select entity" + "Select entity": "Select entity", + "Confirmed At": "Confirmed At", + "Full Name": "Full Name", + "Loading vendors...": "Loading vendors...", + "Minimum 3 characters": "Minimum 3 characters", + "National ID": "National ID", + "Nationality": "Nationality", + "Payout Amount": "Payout Amount", + "Payout Country": "Payout Country", + "Payout Token": "Payout Token", + "Recipient Name": "Recipient Name", + "Reference ID is required": "Reference ID is required", + "Send Amount": "Send Amount", + "Send Country": "Send Country", + "Sender": "Sender", + "Sender Name": "Sender Name", + "Sent By": "Sent By", + "Deposited At": "Deposited At", + "Vendor": "Vendor", + "Vendor is required": "Vendor is required", + "Voter ID": "Voter ID", + "Passport": "Passport", + "Driver's License": "Driver's License", + "Mothers Maiden Name": "Mother's Maiden Name", + "National Identity Card Number": "National Identity Card Number", + "State": "State", + "Municipality": "Municipality", + "Locality": "Locality", + "New Locality": "New Locality", + "Occupation": "Occupation", + "Economic Activity": "Economic Activity", + "Entity Member": "Entity Member", + "Country": "Country", + "# Remittance": "# Remittance", + "Avg Amount/Transaction": "Avg Amount/Transaction", + "# Senders": "# Senders", + "Sender Registry": "Sender Registry", + "Individual": "Individual", + "Legal Entity": "Legal Entity" }, "links": { "Community": "Community", @@ -2782,6 +2863,8 @@ "Interbank Transfer": "Interbank Transfer", "Interest Payment Waiver": "Interest Payment Waiver", "Payments": "Payments", + "Remittances": "Remittances", + "Process Remittance": "Process Remittance", "Payout Refund": "Payout Refund", "Post Interest": "Post Interest", "Post Interest As On": "Post Interest As On", @@ -2833,8 +2916,14 @@ "Reject Additional Shares": "Reject Additional Shares" }, "placeholders": { + "# Remittance": "# Remittance", "Add new server": "Add new server", + "Enter document number": "Enter document number", + "Enter first name (include middle name)": "Enter first name (include middle name)", + "Enter last name": "Enter last name", + "Enter remittance reference number": "Enter remittance reference number", "Search Activity": "Search Activity", + "Sender name": "Sender name", "Enter your username": "Enter your username", "Enter your password": "Enter your password" }, @@ -2949,6 +3038,7 @@ "Business Date functionality is not enabled": "The Business Date functionality is not enabled. If you need that, Go to Global Configurations and enable the configuration", "Button for accounting department": "This button opens the menu to access the system tools required by the accounting department.", "Button for individual account": "This button will be used for individual accounts, groups and centers", + "Failed to search clients. Please try again.": "Failed to search clients. Please try again.", "Cancel Pending Sale": "Cancel Pending Sale", "Cashier Transactions": "Cashier Transactions", "Cashiers": "Cashiers", @@ -3431,6 +3521,7 @@ "Preferences for generating account numbers for client": "Preferences for generating account numbers for client, loan and savings accounts", "Products": "Products", "Products Mix": "Products Mix", + "Process Remittance": "Process Remittance", "Profile": "Profile", "Provisioning Criteria": "Provisioning Criteria", "Provisioning Entries": "Provisioning Entries", @@ -3482,6 +3573,7 @@ "Scheduler Job History": "Scheduler Job History", "Scheduler Jobs can set batch jobs": "By using Scheduler Jobs, you can set batch jobs to be completed at regular intervals. From this page you can schedule, modify or delete jobs.", "Search": "Search", + "Search and assign this beneficiary to an existing Mifos client account": "Search and assign this beneficiary to an existing Mifos client account", "Search by Text": "Search by Text", "SearchByName": "Search by Name", "SearchByClient": "Search by client name, external Id, mobile", @@ -3661,7 +3753,21 @@ "“Maker-Checker” principle requires every tasks": "The “Maker-Checker” principle requires every tasks to be completed by two people to reduce the chance of errors and misuse. One person initiates the process and the second completes it.", "Buy Down Fees": "Buy Down Fees", "Undo Write-off Description": "This action will reverse the write-off transaction and restore the loan to its previous active status. Please provide a note explaining the reason for this action.", - "UploadDocumentHint": "Upload a PDF or image to generate a preview." + "UploadDocumentHint": "Upload a PDF or image to generate a preview.", + "This remittance has a terminal status and cannot be processed further.": "This remittance has a terminal status and cannot be processed further.", + "This remittance has already been paid successfully.": "This remittance has already been paid successfully.", + "Review the transaction and recipient details below, then assign the payout.": "Review the transaction and recipient details below, then assign the payout.", + "Please review the details carefully before confirming the payment.": "Please review the details carefully before confirming the payment.", + "Register the transactional profile for the beneficiary. Add entries for individuals or legal entities.": "Register the transactional profile for the beneficiary. Add entries for individuals or legal entities.", + "No transactional profile entries yet. Add one below.": "No transactional profile entries yet. Add one below.", + "Click to remove": "Click to remove", + "Recipient is not valid for this transaction. Please check the details.": "Recipient is not valid for this transaction. Please check the details.", + "Recipient validation failed. Please check the document details and try again.": "Recipient validation failed. Please check the document details and try again.", + "Missing required information. Please try again.": "Missing required information. Please try again.", + "Failed to confirm payment. Please try again or contact support.": "Failed to confirm payment. Please try again or contact support.", + "Failed to assign payout. Please try again.": "Failed to assign payout. Please try again.", + "Remittance not found. Please check the reference number and try again.": "Remittance not found. Please check the reference number and try again.", + "Unable to load vendors. Please check your connection and try again.": "Unable to load vendors. Please check your connection and try again." }, "auditTrail": { "actions": { @@ -3840,6 +3946,7 @@ "Indicates whether to enforce a minimum balance": "Indicates whether to enforce a minimum balance", "Indicates whether the dividends can be processed even share account of this share product is inactive": "Indicates whether the dividends can be processed even share account of this share product is inactive", "Individual Collection Sheet": "Individual Collection Sheet", + "Process Remittance": "Process Remittance", "Input to calculating the repayment schedule for a loan account": "These fields are input to calculating the repayment schedule for a loan account and are used to determine when payments are due.", "Interest effective from date": "Interest effective from date", "Interest rate is posted or credited to a saving account": "The period at which interest rate is posted or credited to a saving account", diff --git a/src/assets/translations/es-MX.json b/src/assets/translations/es-MX.json index 8c19bc911..c28d7fc74 100644 --- a/src/assets/translations/es-MX.json +++ b/src/assets/translations/es-MX.json @@ -560,7 +560,15 @@ "Is Self Service": "Es autoservicio", "Advanced Search": "Búsqueda avanzada", "Show advanced search": "Mostrar búsqueda avanzada", - "Hide advanced search": "Ocultar búsqueda avanzada" + "Hide advanced search": "Ocultar búsqueda avanzada", + "Validate Recipient": "Validar Beneficiario", + "Assign Payout": "Asignar Pago", + "Confirm Payment": "Confirmar Pago", + "Print Receipt": "Imprimir Recibo", + "New Remittance": "Nueva Remesa", + "View Receipt": "Ver Recibo", + "Add Entry": "Agregar Entrada", + "Retry": "Reintentar" }, "catalogs": { "Interest payment waiver": "Exención de pago de intereses", @@ -801,6 +809,9 @@ "Passport": "Pasaporte", "Id": "Identificación", "Drivers License": "Licencia de conducir", + "Driver's License": "Licencia de Conducir", + "National ID": "Identificación Nacional", + "Voter ID": "Credencial de Elector", "Any Other Id Type": "Cualquier otro tipo de identificación", "Check loan repayment overdue": "Verificar pagos vencidos de créditos", "Update loan arrears aging": "Actualizar antigüedad de atrasos en los créditos", @@ -1296,7 +1307,30 @@ "Contact Information": "Información de Contacto", "Classification": "Clasificación", "Status": "Estado", - "Pending Tasks": "Tareas pendientes" + "Pending Tasks": "Tareas pendientes", + "Process Remittance": "Procesar Remesa", + "Search Remittance": "Buscar Remesa", + "Remittance Details": "Detalles de la Remesa", + "Search Beneficiary": "Buscar Beneficiario", + "Beneficiary Details": "Detalles del Beneficiario", + "Transactional Profile": "Perfil Transaccional", + "Confirm Payout": "Confirmar Pago", + "Confirm Payment": "Confirmar Pago", + "Payment Receipt": "Recibo de Pago", + "Transaction": "Transacción", + "Recipient": "Beneficiario", + "Payout Assignment": "Asignación de Pago", + "Assign Payout": "Asignar Pago", + "Validated Recipient Information": "Información del Beneficiario Validada", + "Identification": "Identificación", + "Professional Information": "Información Profesional", + "Assign to Mifos Client": "Asignar a Cliente Mifos", + "Add Profile Entry": "Agregar Entrada de Perfil", + "Recipient Details": "Detalles del Beneficiario", + "Confirmation Details": "Detalles de Confirmación", + "Payment Successful": "Pago Exitoso", + "Transaction Status": "Estado de la Transacción", + "Transaction Summary": "Resumen de la Transacción" }, "inputs": { "Floating rate": "Tasa flotante", @@ -1986,6 +2020,7 @@ "Liability Account": "Cuenta de responsabilidad", "Liability Account (CR)": "Cuenta de Pasivo (CR)", "Lifted On": "levantado", + "Loading vendors...": "Cargando proveedores...", "Link savings": "Ahorro de enlaces", "Linked Savings Account": "Cuenta de Ahorros vinculada", "Linked Savings Account(Dividend Posting)": "Cuenta de Ahorros vinculada (publicación de dividendos)", @@ -2462,6 +2497,7 @@ "Start Time": "Hora de inicio", "Started On": "Iniciado en", "Starting date": "Fecha de inicio", + "State": "Estado", "State / Province": "Estado o Provincia", "Status": "Estado", "Step Name": "Nombre del paso", @@ -2821,11 +2857,50 @@ "Redeem Shares": "Canjear acciones", "Approve Additional Shares": "Aprobar acciones adicionales", "Reject Additional Shares": "Rechazar acciones adicionales", - "Tasks": "Tareas" + "Tasks": "Tareas", + "Vendor": "Proveedor", + "Loading vendors...": "Cargando proveedores...", + "Failed to load vendors": "Error al cargar proveedores", + "External Reference ID": "ID de Referencia Externa", + "Reference ID is required": "Se requiere ID de referencia", + "Minimum 3 characters": "Mínimo 3 caracteres", + "Sent By": "Enviado Por", + "Deposited At": "Depositado En", + "Sender Name": "Nombre del Remitente", + "Payout Amount": "Monto del Pago", + "Recipient Name": "Nombre del Beneficiario", + "Individual": "Individual", + "Legal Entity": "Persona Moral", + "# Remittance": "# Remesa", + "Avg Amount/Transaction": "Monto Promedio/Transacción", + "# Senders": "# Remitentes", + "Sender Registry": "Registro de Remitentes", + "Sender": "Remitente", + "Document": "Documento", + "Document Number": "Número de Documento", + "First name is required": "Se requiere nombre", + "Last name is required": "Se requiere apellido", + "Document number is required": "Se requiere número de documento", + "National Identity Card Number": "Número de Cédula de Identidad Nacional", + "Mothers Maiden Name": "Apellido de Soltera de la Madre", + "Municipality": "Municipio", + "Locality": "Localidad", + "New Locality": "Nueva Localidad", + "Occupation": "Ocupación", + "Economic Activity": "Actividad Económica", + "Entity Member": "Miembro de Entidad", + "Search Client": "Buscar Cliente", + "Type client name or account number": "Escriba nombre del cliente o número de cuenta" }, "placeholders": { + "# Remittance": "# Remesa", "Add new server": "Agregar nuevo servidor", + "Enter document number": "Ingrese número de documento", + "Enter first name (include middle name)": "Ingrese nombre (incluya segundo nombre)", + "Enter last name": "Ingrese apellido", + "Enter remittance reference number": "Ingrese número de referencia de remesa", "Search Activity": "Actividad de búsqueda", + "Sender name": "Nombre del remitente", "Enter your username": "Ingrese su nombre de usuario", "Enter your password": "Ingrese su contraseña" }, @@ -3852,7 +3927,23 @@ "Create Interest Refund": "Crear reembolso de intereses", "ACTIVE": "ACTIVO", "PENDING": "PENDIENTE", - "INACTIVE": "INACTIVO" + "INACTIVE": "INACTIVO", + "Unable to load vendors. Please check your connection and try again.": "No se pudieron cargar los proveedores. Verifique su conexión e intente de nuevo.", + "This remittance has a terminal status and cannot be processed further.": "Esta remesa tiene un estado terminal y no puede ser procesada.", + "This remittance has already been paid successfully.": "Esta remesa ya ha sido pagada exitosamente.", + "Register the transactional profile for the beneficiary. Add entries for individuals or legal entities.": "Registre el perfil transaccional del beneficiario. Agregue entradas para individuos o personas morales.", + "Click to remove": "Clic para eliminar", + "No transactional profile entries yet. Add one below.": "Aún no hay entradas en el perfil transaccional. Agregue una a continuación.", + "Please review the details carefully before confirming the payment.": "Revise los detalles cuidadosamente antes de confirmar el pago.", + "Search and assign this beneficiary to an existing Mifos client account": "Buscar y asignar este beneficiario a una cuenta de cliente Mifos existente", + "Failed to search clients. Please try again.": "Error al buscar clientes. Intente de nuevo.", + "Recipient is not valid for this transaction. Please check the details.": "El beneficiario no es válido para esta transacción. Verifique los detalles.", + "Recipient validation failed. Please check the document details and try again.": "La validación del beneficiario falló. Verifique los detalles del documento e intente de nuevo.", + "Missing required information. Please try again.": "Falta información requerida. Intente de nuevo.", + "Failed to confirm payment. Please try again or contact support.": "Error al confirmar el pago. Intente de nuevo o contacte al soporte.", + "Failed to assign payout. Please try again.": "Error al asignar el pago. Intente de nuevo.", + "Remittance not found. Please check the reference number and try again.": "Remesa no encontrada. Verifique el número de referencia e intente de nuevo.", + "Review the transaction and recipient details below, then assign the payout.": "Revise los detalles de la transacción y del beneficiario a continuación, luego asigne el pago." }, "countries": { "AF": "Afganistán", diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 0df9b90ed..d17ab78c9 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -85,6 +85,15 @@ export const environment = { mifosInterbankTransfersApiVersion: loadedEnv['mifosInterbankTransfersApiVersion'] || '/v1.0', mifosInterbankTransfersEnabled: loadedEnv['mifosInterbankTransfersEnabled'] ?? true, + /** Remittance Module Integration */ + mifosRemittanceApiUrl: loadedEnv['mifosRemittanceApiUrl'] || '', + mifosRemittanceApiProvider: loadedEnv['mifosRemittanceApiProvider'] || '/remittance-api', + mifosRemittanceApiVersion: loadedEnv['mifosRemittanceApiVersion'] || '/v1', + mifosRemittanceEnabled: + loadedEnv['mifosRemittanceEnabled'] === true || loadedEnv['mifosRemittanceEnabled'] === 'true', + mifosRemittanceApiHeader: loadedEnv['mifosRemittanceApiHeader'] || 'X-Gravitee-Api-Key', + mifosRemittanceApiKey: loadedEnv['mifosRemittanceApiKey'] || '', + minPasswordLength: loadedEnv['minPasswordLength'] || 12, /** External National ID System integration */ diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 983360342..dfb110ec5 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -87,6 +87,14 @@ export const environment = { mifosInterbankTransfersApiVersion: window.env?.mifosInterbankTransfersApiVersion || '/v1.0', mifosInterbankTransfersEnabled: window.env?.mifosInterbankTransfersEnabled ?? true, + /** Remittance Module Integration */ + mifosRemittanceApiUrl: window.env?.mifosRemittanceApiUrl || '', + mifosRemittanceApiProvider: window.env?.mifosRemittanceApiProvider || '/remittance-api', + mifosRemittanceApiVersion: window.env?.mifosRemittanceApiVersion || '/v1', + mifosRemittanceEnabled: window.env?.mifosRemittanceEnabled === true || window.env?.mifosRemittanceEnabled === 'true', + mifosRemittanceApiHeader: window.env?.mifosRemittanceApiHeader || 'X-Gravitee-Api-Key', + mifosRemittanceApiKey: window.env?.mifosRemittanceApiKey || '', + /** * External National ID System Integration * When enabled, client creation/editing will lookup external National ID diff --git a/src/main.scss b/src/main.scss index 96b76cfda..bc797c50e 100644 --- a/src/main.scss +++ b/src/main.scss @@ -719,6 +719,24 @@ mifosx-notifications-page td { color: inherit !important; } +// ===== Global Button Icon Alignment ===== +// Handles the span + mat-icon pattern used throughout the application +.mat-mdc-button, +.mat-mdc-raised-button, +.mat-mdc-outlined-button { + span { + display: inline-flex; + align-items: center; + gap: 8px; + + mat-icon { + display: inline-flex; + align-items: center; + justify-content: center; + } + } +} + .full-width-flex { display: flex; justify-content: flex-start; @@ -854,3 +872,31 @@ mat-card { } } } + +/* Print styles — hide app shell, show only receipt content */ +@media print { + mat-sidenav, + mifosx-toolbar, + mifosx-breadcrumb, + mifosx-footer, + .sidebar-panel, + .mat-mdc-tab-header, + .mat-stepper-vertical-line, + .mat-step-header { + display: none !important; + } + + mat-sidenav-content.sidenav { + margin-left: 0 !important; + } + + body, + html { + background: #fff !important; + } + + @page { + /* stylelint-disable-next-line unit-allowed-list */ + margin: 1.5cm; + } +} diff --git a/src/types/global.d.ts b/src/types/global.d.ts index aeacc5936..b7f57163a 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -24,6 +24,11 @@ interface Window { mifosInterbankTransfersApiProvider?: string; mifosInterbankTransfersApiVersion?: string; mifosInterbankTransfersEnabled?: boolean; + mifosRemittanceApiUrl?: string; + mifosRemittanceApiProvider?: string; + mifosRemittanceApiVersion?: string; + mifosRemittanceEnabled?: boolean; + mifosRemittanceApiKey?: string; minPasswordLength?: number; }; }