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)
This commit is contained in:
@@ -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_API_VERSION | The Interbank server api version | /v1.0 |
|
||||||
| MIFOS_INTERBANK_TRANSFERS_ENABLED | If the Interbank feature is enabled | true |
|
| 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
|
### Client Data Masking Example
|
||||||
|
|
||||||
When `MIFOS_COMPLIANCE_HIDE_CLIENT_DATA=false` (default):
|
When `MIFOS_COMPLIANCE_HIDE_CLIENT_DATA=false` (default):
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# Rate limiting zone for external National ID API (2 requests/second per IP)
|
# 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;
|
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 {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
@@ -46,6 +49,30 @@ server {
|
|||||||
# CORS is not needed since requests come from the same origin
|
# 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
|
# Angular app - serve index.html for all routes
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
|
|||||||
+37
-5
@@ -51,12 +51,12 @@ const proxyConfig = [
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: true,
|
secure: true,
|
||||||
logLevel: 'debug',
|
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) {
|
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');
|
const rewrittenPath = (req.url || '').replace(/^\/external-nationalid/, '/1.0/nationalid');
|
||||||
console.log('[Proxy] Proxying:', req.method, req.url, '->', this.target + rewrittenPath);
|
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'));
|
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` .
|
// For local development use `proxy.localhost.conf js` .
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -72,5 +72,38 @@ module.exports = [
|
|||||||
res.end('Proxy error: ' + (err && err.message ? err.message : 'Unknown error'));
|
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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import { CollectionsModule } from './collections/collections.module';
|
|||||||
import { ProfileModule } from './profile/profile.module';
|
import { ProfileModule } from './profile/profile.module';
|
||||||
import { TasksModule } from './tasks/tasks.module';
|
import { TasksModule } from './tasks/tasks.module';
|
||||||
import { ConfigurationWizardModule } from './configuration-wizard/configuration-wizard.module';
|
import { ConfigurationWizardModule } from './configuration-wizard/configuration-wizard.module';
|
||||||
|
import { RemittancesModule } from './remittances/remittances.module';
|
||||||
import { PortalModule } from '@angular/cdk/portal';
|
import { PortalModule } from '@angular/cdk/portal';
|
||||||
|
|
||||||
/** Main Routing Module */
|
/** Main Routing Module */
|
||||||
@@ -127,6 +128,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||||||
CollectionsModule,
|
CollectionsModule,
|
||||||
TasksModule,
|
TasksModule,
|
||||||
ConfigurationWizardModule,
|
ConfigurationWizardModule,
|
||||||
|
RemittancesModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
NotFoundComponent,
|
NotFoundComponent,
|
||||||
CallbackComponent,
|
CallbackComponent,
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export class AuthenticationInterceptor implements HttpInterceptor {
|
|||||||
* Intercepts a Http request and sets the request headers.
|
* Intercepts a Http request and sets the request headers.
|
||||||
*/
|
*/
|
||||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
// 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) {
|
if (this.settingsService.tenantIdentifier) {
|
||||||
httpOptions.headers['Fineract-Platform-TenantId'] = this.settingsService.tenantIdentifier;
|
httpOptions.headers['Fineract-Platform-TenantId'] = this.settingsService.tenantIdentifier;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,19 @@
|
|||||||
</mat-icon>
|
</mat-icon>
|
||||||
<a matLine>{{ 'labels.menus.Individual Collection Sheet' | translate }}</a>
|
<a matLine>{{ 'labels.menus.Individual Collection Sheet' | translate }}</a>
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
|
<mat-list-item
|
||||||
|
*ngIf="mifosRemittanceEnabled"
|
||||||
|
[routerLink]="['/remittances/process']"
|
||||||
|
[matTooltipPosition]="tooltipPosition"
|
||||||
|
matTooltip="{{ 'tooltips.Process Remittance' | translate }}"
|
||||||
|
routerLinkActive="active-menu"
|
||||||
|
[routerLinkActiveOptions]="{ exact: false }"
|
||||||
|
>
|
||||||
|
<mat-icon matListIcon>
|
||||||
|
<fa-icon icon="money-bill-wave" size="sm"></fa-icon>
|
||||||
|
</mat-icon>
|
||||||
|
<a matLine>{{ 'labels.menus.Remittances' | translate }}</a>
|
||||||
|
</mat-list-item>
|
||||||
<mat-list-item
|
<mat-list-item
|
||||||
[routerLink]="['/notifications']"
|
[routerLink]="['/notifications']"
|
||||||
[matTooltipPosition]="tooltipPosition"
|
[matTooltipPosition]="tooltipPosition"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { MatNavList, MatListItem } from '@angular/material/list';
|
|||||||
import { MatIcon } from '@angular/material/icon';
|
import { MatIcon } from '@angular/material/icon';
|
||||||
import { MatLine } from '@angular/material/grid-list';
|
import { MatLine } from '@angular/material/grid-list';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
import { remittanceConfig } from '../../../remittances/remittance.config';
|
||||||
|
|
||||||
import { catchError, finalize, of, take } from 'rxjs';
|
import { catchError, finalize, of, take } from 'rxjs';
|
||||||
|
|
||||||
@@ -77,6 +78,8 @@ export class SidenavComponent implements OnInit, AfterViewInit {
|
|||||||
mappedActivities: any[] = [];
|
mappedActivities: any[] = [];
|
||||||
/** Collection of possible frequent activities */
|
/** Collection of possible frequent activities */
|
||||||
frequentActivities: any[] = frequentActivities;
|
frequentActivities: any[] = frequentActivities;
|
||||||
|
/** Whether remittance feature is enabled */
|
||||||
|
mifosRemittanceEnabled = remittanceConfig.isRemittanceEnabled;
|
||||||
|
|
||||||
/* Refernce of logo */
|
/* Refernce of logo */
|
||||||
@ViewChild('logo') logo: ElementRef<any>;
|
@ViewChild('logo') logo: ElementRef<any>;
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ const activities: any[] = [
|
|||||||
{ activity: 'audit', path: '/products/audit-trails' },
|
{ activity: 'audit', path: '/products/audit-trails' },
|
||||||
{ activity: 'create accounting closure', path: '' },
|
{ activity: 'create accounting closure', path: '' },
|
||||||
{ activity: 'navigation', path: '/navigation' },
|
{ activity: 'navigation', path: '/navigation' },
|
||||||
|
{ activity: 'remittances', path: '/remittances/process' },
|
||||||
{ activity: '', path: 'home' }
|
{ activity: '', path: 'home' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Request body for payout confirmation */
|
||||||
|
export interface PayoutConfirmationRequest {
|
||||||
|
additionalInfo?: Record<string, string>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<!--
|
||||||
|
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/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-content>
|
||||||
|
<div class="header">
|
||||||
|
<h2>{{ 'labels.heading.Process Remittance' | translate }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-stepper #stepper orientation="vertical" [linear]="true">
|
||||||
|
<!-- Step 1: Search Remittance -->
|
||||||
|
<mat-step [editable]="!remittanceData" [completed]="!!remittanceData">
|
||||||
|
<ng-template matStepLabel>{{ 'labels.heading.Search Remittance' | translate }}</ng-template>
|
||||||
|
<mifosx-search-remittance-step (remittanceFound)="onRemittanceFound($event)"> </mifosx-search-remittance-step>
|
||||||
|
</mat-step>
|
||||||
|
|
||||||
|
<!-- Step 2: Remittance Details -->
|
||||||
|
<mat-step [editable]="false" [completed]="detailsReviewed">
|
||||||
|
<ng-template matStepLabel>{{ 'labels.heading.Remittance Details' | translate }}</ng-template>
|
||||||
|
<mifosx-remittance-details-step
|
||||||
|
[remittanceData]="remittanceData"
|
||||||
|
[parsedStatus]="parsedStatus"
|
||||||
|
(detailsNext)="onDetailsNext()"
|
||||||
|
>
|
||||||
|
</mifosx-remittance-details-step>
|
||||||
|
</mat-step>
|
||||||
|
|
||||||
|
<!-- Step 3: Search Beneficiary (only for PENDING) -->
|
||||||
|
@if (showBeneficiarySteps) {
|
||||||
|
<mat-step [editable]="false" [completed]="!!recipientData">
|
||||||
|
<ng-template matStepLabel>{{ 'labels.heading.Search Beneficiary' | translate }}</ng-template>
|
||||||
|
<mifosx-search-beneficiary-step
|
||||||
|
[vendor]="selectedVendor"
|
||||||
|
[transactionId]="transactionId"
|
||||||
|
(recipientValidated)="onRecipientValidated($event)"
|
||||||
|
>
|
||||||
|
</mifosx-search-beneficiary-step>
|
||||||
|
</mat-step>
|
||||||
|
|
||||||
|
<!-- Step 4: Beneficiary Details -->
|
||||||
|
<mat-step [editable]="false" [completed]="beneficiaryReviewed">
|
||||||
|
<ng-template matStepLabel>{{ 'labels.heading.Beneficiary Details' | translate }}</ng-template>
|
||||||
|
<mifosx-beneficiary-details-step
|
||||||
|
[recipientData]="recipientData"
|
||||||
|
(beneficiaryNext)="onBeneficiaryDetailsNext($event)"
|
||||||
|
>
|
||||||
|
</mifosx-beneficiary-details-step>
|
||||||
|
</mat-step>
|
||||||
|
|
||||||
|
<!-- Step 5: Transactional Profile / Assign Payout -->
|
||||||
|
<mat-step [editable]="false" [completed]="!!payoutAssignment">
|
||||||
|
<ng-template matStepLabel>{{ 'labels.heading.Transactional Profile' | translate }}</ng-template>
|
||||||
|
<mifosx-transactional-profile-step
|
||||||
|
[vendor]="selectedVendor"
|
||||||
|
[transactionId]="transactionId"
|
||||||
|
[remittanceData]="remittanceData"
|
||||||
|
[recipientData]="recipientData"
|
||||||
|
(payoutAssigned)="onPayoutAssigned($event)"
|
||||||
|
>
|
||||||
|
</mifosx-transactional-profile-step>
|
||||||
|
</mat-step>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Step 6: Confirm Payment -->
|
||||||
|
@if (showConfirmStep) {
|
||||||
|
<mat-step [editable]="false" [completed]="!!payoutConfirmation">
|
||||||
|
<ng-template matStepLabel>{{ 'labels.heading.Confirm Payment' | translate }}</ng-template>
|
||||||
|
<mifosx-confirm-payment-step
|
||||||
|
[vendor]="selectedVendor"
|
||||||
|
[transactionId]="transactionId"
|
||||||
|
[remittanceData]="remittanceData"
|
||||||
|
[recipientData]="recipientData"
|
||||||
|
[payoutAssignment]="payoutAssignment"
|
||||||
|
[clientId]="selectedClientId"
|
||||||
|
(payoutConfirmed)="onPayoutConfirmed($event)"
|
||||||
|
>
|
||||||
|
</mifosx-confirm-payment-step>
|
||||||
|
</mat-step>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Step 7: Payment Receipt (always shown) -->
|
||||||
|
<mat-step [editable]="false">
|
||||||
|
<ng-template matStepLabel>{{ 'labels.heading.Payment Receipt' | translate }}</ng-template>
|
||||||
|
<mifosx-payment-receipt-step
|
||||||
|
[remittanceData]="remittanceData"
|
||||||
|
[recipientData]="recipientData"
|
||||||
|
[payoutConfirmation]="payoutConfirmation"
|
||||||
|
[payoutAssignment]="payoutAssignment"
|
||||||
|
[parsedStatus]="parsedStatus"
|
||||||
|
(resetWorkflow)="resetWorkflow()"
|
||||||
|
>
|
||||||
|
</mifosx-payment-receipt-step>
|
||||||
|
</mat-step>
|
||||||
|
</mat-stepper>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
+234
@@ -0,0 +1,234 @@
|
|||||||
|
<!--
|
||||||
|
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/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
@if (recipientData) {
|
||||||
|
<div class="step-content">
|
||||||
|
<h3>{{ 'labels.heading.Validated Recipient Information' | translate }}</h3>
|
||||||
|
|
||||||
|
<!-- Section: Identification -->
|
||||||
|
<div class="section-header">
|
||||||
|
<mat-icon>badge</mat-icon>
|
||||||
|
<h4>{{ 'labels.heading.Identification' | translate }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="details-grid">
|
||||||
|
@if (recipientData.primaryDocument?.documentType) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Document Type' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.primaryDocument?.documentType }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (recipientData.primaryDocument?.documentNumber) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Document Number' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.primaryDocument?.documentNumber }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (recipientData.identityCard) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.National Identity Card Number' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.identityCard }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section: Personal Information -->
|
||||||
|
<div class="section-header">
|
||||||
|
<mat-icon>person</mat-icon>
|
||||||
|
<h4>{{ 'labels.heading.Personal Information' | translate }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="details-grid">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.First Name' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.givenName }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Last Name' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.lastName }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (recipientData.motherMaidenName) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Mothers Maiden Name' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.motherMaidenName }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (recipientData.dateOfBirth) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Date of Birth' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.dateOfBirth | date: 'mediumDate' }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (recipientData.phone) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Phone Number' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.phone }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section: Address -->
|
||||||
|
<div class="section-header">
|
||||||
|
<mat-icon>location_on</mat-icon>
|
||||||
|
<h4>{{ 'labels.inputs.Address' | translate }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="details-grid">
|
||||||
|
@if (recipientData.address?.line1) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Address Line' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.address?.line1 }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (recipientData.address?.postalCode) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Postal Code' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.address?.postalCode }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (recipientData.address?.state) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.State' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.address?.state }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (recipientData.municipality || recipientData.address?.city) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Municipality' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.municipality || recipientData.address?.city }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (recipientData.locality) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Locality' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.locality }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (recipientData.newLocality) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.New Locality' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.newLocality }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (recipientData.address?.country) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Country' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.address?.country }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section: Professional Information -->
|
||||||
|
<div class="section-header">
|
||||||
|
<mat-icon>work</mat-icon>
|
||||||
|
<h4>{{ 'labels.heading.Professional Information' | translate }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="details-grid">
|
||||||
|
@if (recipientData.occupation) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Occupation' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.occupation }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (recipientData.economicActivity) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Economic Activity' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.economicActivity }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (recipientData.entityMember) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Entity Member' | translate }}</span>
|
||||||
|
<span class="value">{{ recipientData.entityMember }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section: Assign to Mifos Client -->
|
||||||
|
<div class="section-header">
|
||||||
|
<mat-icon>assignment_ind</mat-icon>
|
||||||
|
<h4>{{ 'labels.heading.Assign to Mifos Client' | translate }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="client-search-section">
|
||||||
|
<p class="search-instruction">
|
||||||
|
{{ 'labels.text.Search and assign this beneficiary to an existing Mifos client account' | translate }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="client-search-field">
|
||||||
|
<mat-label>{{ 'labels.inputs.Search Client' | translate }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[formControl]="clientSearchControl"
|
||||||
|
[matAutocomplete]="clientAuto"
|
||||||
|
placeholder="{{ 'labels.inputs.Type client name or account number' | translate }}"
|
||||||
|
/>
|
||||||
|
<mat-icon matIconPrefix>search</mat-icon>
|
||||||
|
<mat-autocomplete
|
||||||
|
#clientAuto="matAutocomplete"
|
||||||
|
[displayWith]="displayFn"
|
||||||
|
(optionSelected)="onClientSelected($event.option.value)"
|
||||||
|
>
|
||||||
|
@for (client of filteredClients$ | async; track client.id) {
|
||||||
|
<mat-option [value]="client">
|
||||||
|
<div class="client-option">
|
||||||
|
<span class="client-name">{{ client.displayName }}</span>
|
||||||
|
<span class="client-details">
|
||||||
|
@if (client.accountNo) {
|
||||||
|
<span class="account-no">{{ 'labels.inputs.Account' | translate }}: {{ client.accountNo }}</span>
|
||||||
|
}
|
||||||
|
@if (client.externalId) {
|
||||||
|
<span class="external-id"
|
||||||
|
>{{ 'labels.inputs.External ID' | translate }}: {{ client.externalId }}</span
|
||||||
|
>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</mat-option>
|
||||||
|
}
|
||||||
|
</mat-autocomplete>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
@if (searchError) {
|
||||||
|
<div class="error-message">
|
||||||
|
<mat-icon>error</mat-icon>
|
||||||
|
<span>{{ 'labels.text.Failed to search clients. Please try again.' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (selectedClient) {
|
||||||
|
<div class="selected-client-info">
|
||||||
|
<mat-icon color="primary">check_circle</mat-icon>
|
||||||
|
<div class="client-info">
|
||||||
|
<strong>{{ selectedClient.displayName }}</strong>
|
||||||
|
@if (selectedClient.accountNo) {
|
||||||
|
<span class="subdued">{{ 'labels.inputs.Account' | translate }}: {{ selectedClient.accountNo }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step-actions">
|
||||||
|
<button mat-raised-button color="primary" (click)="onNext()" [disabled]="!selectedClient">
|
||||||
|
{{ 'labels.buttons.Next' | translate }}
|
||||||
|
<mat-icon>arrow_forward</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
+161
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
@@ -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<number>();
|
||||||
|
|
||||||
|
private clientsService = inject(ClientsService);
|
||||||
|
|
||||||
|
clientSearchControl = new FormControl<string | ClientSearchResult>('');
|
||||||
|
filteredClients$!: Observable<ClientSearchResult[]>;
|
||||||
|
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<ClientSearchResult[]> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
<!--
|
||||||
|
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/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="step-content">
|
||||||
|
<h3>{{ 'labels.heading.Confirm Payout' | translate }}</h3>
|
||||||
|
<p class="description">
|
||||||
|
{{ 'labels.text.Please review the details carefully before confirming the payment.' | translate }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="confirmation-card">
|
||||||
|
<!-- Transaction info -->
|
||||||
|
@if (remittanceData) {
|
||||||
|
<div class="card-section">
|
||||||
|
<h4>{{ 'labels.heading.Transaction' | translate }}</h4>
|
||||||
|
@if (officeName) {
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Office' | translate }}</span>
|
||||||
|
<span class="value">{{ officeName }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Code' | translate }}</span>
|
||||||
|
<span class="value">{{ remittanceData.pin }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Date' | translate }}</span>
|
||||||
|
<span class="value">{{ remittanceData.created | date: 'medium' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Payout Amount' | translate }}</span>
|
||||||
|
<span class="value amount">{{
|
||||||
|
remittanceData.receivingAmount | currency: remittanceData.receivingCurrency
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Sender' | translate }}</span>
|
||||||
|
<span class="value">{{ remittanceData.senderName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
|
<!-- Recipient info -->
|
||||||
|
<div class="card-section">
|
||||||
|
<h4>{{ 'labels.heading.Recipient' | translate }}</h4>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Name' | translate }}</span>
|
||||||
|
@if (recipientData?.givenName) {
|
||||||
|
<span class="value">{{ recipientData.givenName }} {{ recipientData.lastName }}</span>
|
||||||
|
} @else {
|
||||||
|
<span class="value">{{ remittanceData?.recipientName }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (recipientData?.primaryDocument?.documentType) {
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Document' | translate }}</span>
|
||||||
|
<span class="value"
|
||||||
|
>{{ recipientData.primaryDocument?.documentType }} -
|
||||||
|
{{ recipientData.primaryDocument?.documentNumber }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (payoutAssignment) {
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
|
<!-- Assignment info -->
|
||||||
|
<div class="card-section">
|
||||||
|
<h4>{{ 'labels.heading.Payout Assignment' | translate }}</h4>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Status' | translate }}</span>
|
||||||
|
<span class="value">{{ payoutAssignment.status }}</span>
|
||||||
|
</div>
|
||||||
|
@if (payoutAssignment?.trackingNumber) {
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Reference Number' | translate }}</span>
|
||||||
|
<span class="value mono">{{ payoutAssignment.trackingNumber }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (errorMessage) {
|
||||||
|
<div class="error-banner">
|
||||||
|
<mat-icon>error_outline</mat-icon>
|
||||||
|
<span>{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="step-actions">
|
||||||
|
<button mat-raised-button color="warn" (click)="confirmPayout()" [disabled]="isLoading || !vendor">
|
||||||
|
@if (isLoading) {
|
||||||
|
<mat-spinner diameter="20"></mat-spinner>
|
||||||
|
} @else {
|
||||||
|
<span><mat-icon>check_circle</mat-icon> {{ 'labels.buttons.Confirm Payment' | translate }}</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+99
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+88
@@ -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<PayoutConfirmationResponse>();
|
||||||
|
|
||||||
|
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.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
<!--
|
||||||
|
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/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="receipt-header" [ngClass]="{ success: isPaid, terminal: isTerminal }">
|
||||||
|
<mat-icon class="receipt-icon">{{ receiptIcon }}</mat-icon>
|
||||||
|
<h3>{{ receiptTitle }}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (remittanceData) {
|
||||||
|
<div class="receipt-body">
|
||||||
|
<div class="receipt-section">
|
||||||
|
<h4>{{ 'labels.heading.Transaction Details' | translate }}</h4>
|
||||||
|
@if (officeName) {
|
||||||
|
<div class="receipt-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Office' | translate }}</span>
|
||||||
|
<span class="value">{{ officeName }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="receipt-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.External ID' | translate }}</span>
|
||||||
|
<span class="value">{{ remittanceData.pin }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="receipt-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Status' | translate }}</span>
|
||||||
|
<span class="value status-text">{{ payoutConfirmation?.transaction?.status || remittanceData.status }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="receipt-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Payout Amount' | translate }}</span>
|
||||||
|
<span class="value amount">{{
|
||||||
|
remittanceData.receivingAmount | currency: remittanceData.receivingCurrency
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="receipt-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Sender' | translate }}</span>
|
||||||
|
<span class="value">{{ remittanceData.senderName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="receipt-section">
|
||||||
|
<h4>{{ 'labels.heading.Recipient Details' | translate }}</h4>
|
||||||
|
<div class="receipt-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Name' | translate }}</span>
|
||||||
|
@if (recipientData?.givenName) {
|
||||||
|
<span class="value">{{ recipientData.givenName }} {{ recipientData.lastName }}</span>
|
||||||
|
} @else {
|
||||||
|
<span class="value">{{ remittanceData?.recipientName }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (recipientData?.primaryDocument?.documentType) {
|
||||||
|
<div class="receipt-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Document' | translate }}</span>
|
||||||
|
<span class="value"
|
||||||
|
>{{ recipientData.primaryDocument.documentType }} -
|
||||||
|
{{ recipientData.primaryDocument.documentNumber }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (payoutConfirmation) {
|
||||||
|
<div class="receipt-section">
|
||||||
|
<h4>{{ 'labels.heading.Confirmation Details' | translate }}</h4>
|
||||||
|
@if (payoutConfirmation?.transaction) {
|
||||||
|
<div class="receipt-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Status' | translate }}</span>
|
||||||
|
<span class="value">{{ payoutConfirmation.transaction.status }}</span>
|
||||||
|
</div>
|
||||||
|
@if (payoutAssignment?.trackingNumber) {
|
||||||
|
<div class="receipt-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Reference Number' | translate }}</span>
|
||||||
|
<span class="value mono">{{ payoutAssignment.trackingNumber }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="receipt-row">
|
||||||
|
<span class="label">{{ 'labels.inputs.Payout Amount' | translate }}</span>
|
||||||
|
<span class="value">{{
|
||||||
|
payoutConfirmation.transaction.receivingAmount
|
||||||
|
| currency: payoutConfirmation.transaction.receivingCurrency
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="step-actions">
|
||||||
|
<button mat-raised-button (click)="printReceipt()">
|
||||||
|
<mat-icon>print</mat-icon>
|
||||||
|
{{ 'labels.buttons.Print Receipt' | translate }}
|
||||||
|
</button>
|
||||||
|
<button mat-raised-button color="primary" (click)="onNewRemittance()">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
{{ 'labels.buttons.New Remittance' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+192
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
@@ -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<void>();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
<!--
|
||||||
|
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/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
@if (remittanceData) {
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="status-row">
|
||||||
|
<span class="status-badge" [ngClass]="statusBadgeClass">{{ remittanceData.status }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details-grid">
|
||||||
|
@if (remittanceData.sendingPartner) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Sent By' | translate }}</span>
|
||||||
|
<span class="value">{{ remittanceData.sendingPartner }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.External ID' | translate }}</span>
|
||||||
|
<span class="value">{{ remittanceData.pin }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Created At' | translate }}</span>
|
||||||
|
@if (isValidDate(remittanceData.created)) {
|
||||||
|
<span class="value">{{ remittanceData.created | date: 'medium' }}</span>
|
||||||
|
} @else {
|
||||||
|
<span class="value">{{ remittanceData.created }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (remittanceData.processedAt && isValidDate(remittanceData.processedAt)) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Deposited At' | translate }}</span>
|
||||||
|
<span class="value">{{ remittanceData.processedAt | date: 'medium' }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Sender Name' | translate }}</span>
|
||||||
|
<span class="value">{{ remittanceData.senderName }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Payout Amount' | translate }}</span>
|
||||||
|
<span class="value">{{ remittanceData.receivingAmount | currency: remittanceData.receivingCurrency }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Recipient Name' | translate }}</span>
|
||||||
|
<span class="value">{{ remittanceData.recipientName }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (remittanceData.recipientAddress) {
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ 'labels.inputs.Address' | translate }}</span>
|
||||||
|
<span class="value">{{ remittanceData.recipientAddress }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (isPaid) {
|
||||||
|
<div class="terminal-info paid-info">
|
||||||
|
<mat-icon>check_circle</mat-icon>
|
||||||
|
<span>{{ 'labels.text.This remittance has already been paid successfully.' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
} @else if (isTerminal) {
|
||||||
|
<div class="terminal-warning">
|
||||||
|
<mat-icon>info</mat-icon>
|
||||||
|
<span>{{
|
||||||
|
'labels.text.This remittance has a terminal status and cannot be processed further.' | translate
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="step-actions">
|
||||||
|
<button mat-raised-button color="primary" (click)="onNext()">
|
||||||
|
@if (isTerminal) {
|
||||||
|
{{ 'labels.buttons.View Receipt' | translate }}
|
||||||
|
} @else {
|
||||||
|
{{ 'labels.buttons.Next' | translate }}
|
||||||
|
}
|
||||||
|
<mat-icon>arrow_forward</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
+104
@@ -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;
|
||||||
|
}
|
||||||
+65
@@ -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<void>();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
<!--
|
||||||
|
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/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="step-content">
|
||||||
|
<form [formGroup]="beneficiaryForm" (ngSubmit)="validateRecipient()">
|
||||||
|
<div class="form-fields">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.First Name' | translate }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
formControlName="givenName"
|
||||||
|
placeholder="{{ 'labels.placeholders.Enter first name (include middle name)' | translate }}"
|
||||||
|
/>
|
||||||
|
@if (beneficiaryForm.get('givenName')?.hasError('required') && beneficiaryForm.get('givenName')?.touched) {
|
||||||
|
<mat-error>{{ 'labels.inputs.First name is required' | translate }}</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
formControlName="lastName"
|
||||||
|
placeholder="{{ 'labels.placeholders.Enter last name' | translate }}"
|
||||||
|
/>
|
||||||
|
@if (beneficiaryForm.get('lastName')?.hasError('required') && beneficiaryForm.get('lastName')?.touched) {
|
||||||
|
<mat-error>{{ 'labels.inputs.Last name is required' | translate }}</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.Document Type' | translate }}</mat-label>
|
||||||
|
<mat-select formControlName="documentType">
|
||||||
|
@for (doc of documentTypes; track doc.value) {
|
||||||
|
<mat-option [value]="doc.value">{{ doc.label | translate }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.Document Number' | translate }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
formControlName="documentNumber"
|
||||||
|
placeholder="{{ 'labels.placeholders.Enter document number' | translate }}"
|
||||||
|
/>
|
||||||
|
@if (
|
||||||
|
beneficiaryForm.get('documentNumber')?.hasError('required') && beneficiaryForm.get('documentNumber')?.touched
|
||||||
|
) {
|
||||||
|
<mat-error>{{ 'labels.inputs.Document number is required' | translate }}</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (errorMessage) {
|
||||||
|
<div class="error-banner">
|
||||||
|
<mat-icon>error_outline</mat-icon>
|
||||||
|
<span>{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="step-actions">
|
||||||
|
<button mat-raised-button color="primary" type="submit" [disabled]="beneficiaryForm.invalid || isLoading">
|
||||||
|
@if (isLoading) {
|
||||||
|
<mat-spinner diameter="20"></mat-spinner>
|
||||||
|
} @else {
|
||||||
|
<span><mat-icon>person_search</mat-icon> {{ 'labels.buttons.Validate Recipient' | translate }}</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
+47
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+125
@@ -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<ValidatedRecipient>();
|
||||||
|
|
||||||
|
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.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
<!--
|
||||||
|
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/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="step-content">
|
||||||
|
<form [formGroup]="searchForm" (ngSubmit)="searchRemittance()">
|
||||||
|
<div class="form-fields">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.Vendor' | translate }}</mat-label>
|
||||||
|
<mat-select formControlName="vendor" [disabled]="vendorLoadError">
|
||||||
|
@for (v of vendors; track v.name) {
|
||||||
|
<mat-option [value]="v.headerValue">{{ v.name }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
@if (isLoadingVendors) {
|
||||||
|
<mat-hint>{{ 'labels.inputs.Loading vendors...' | translate }}</mat-hint>
|
||||||
|
}
|
||||||
|
@if (vendorLoadError) {
|
||||||
|
<mat-error>{{ 'labels.inputs.Failed to load vendors' | translate }}</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.External Reference ID' | translate }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
formControlName="externalId"
|
||||||
|
placeholder="{{ 'labels.placeholders.Enter remittance reference number' | translate }}"
|
||||||
|
/>
|
||||||
|
@if (searchForm.get('externalId')?.hasError('required') && searchForm.get('externalId')?.touched) {
|
||||||
|
<mat-error>{{ 'labels.inputs.Reference ID is required' | translate }}</mat-error>
|
||||||
|
}
|
||||||
|
@if (searchForm.get('externalId')?.hasError('minlength')) {
|
||||||
|
<mat-error>{{ 'labels.inputs.Minimum 3 characters' | translate }}</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (vendorLoadError) {
|
||||||
|
<div class="vendor-error-banner">
|
||||||
|
<mat-icon>warning</mat-icon>
|
||||||
|
<span>{{ 'labels.text.Unable to load vendors. Please check your connection and try again.' | translate }}</span>
|
||||||
|
<button mat-stroked-button color="primary" type="button" (click)="loadVendors()" [disabled]="isLoadingVendors">
|
||||||
|
@if (isLoadingVendors) {
|
||||||
|
<mat-spinner diameter="16"></mat-spinner>
|
||||||
|
} @else {
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
<span>{{ 'labels.buttons.Retry' | translate }}</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (errorMessage) {
|
||||||
|
<div class="error-banner">
|
||||||
|
<mat-icon>error_outline</mat-icon>
|
||||||
|
<span>{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="step-actions">
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
(click)="searchRemittance()"
|
||||||
|
[disabled]="isLoading || vendorLoadError"
|
||||||
|
>
|
||||||
|
@if (isLoading) {
|
||||||
|
<mat-spinner diameter="20"></mat-spinner>
|
||||||
|
} @else {
|
||||||
|
<span><mat-icon>search</mat-icon> {{ 'labels.buttons.Search' | translate }}</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
+90
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+114
@@ -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.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
<!--
|
||||||
|
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/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="step-content">
|
||||||
|
<h3>{{ 'labels.heading.Transactional Profile' | translate }}</h3>
|
||||||
|
<p class="description">
|
||||||
|
{{
|
||||||
|
'labels.text.Register the transactional profile for the beneficiary. Add entries for individuals or legal entities.'
|
||||||
|
| translate
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Entity Type Toggle -->
|
||||||
|
<div class="entity-type-toggle">
|
||||||
|
<mat-button-toggle-group [(ngModel)]="entityType" aria-label="Entity type">
|
||||||
|
<mat-button-toggle value="individual">
|
||||||
|
<mat-icon>person</mat-icon>
|
||||||
|
{{ 'labels.inputs.Individual' | translate }}
|
||||||
|
</mat-button-toggle>
|
||||||
|
<mat-button-toggle value="legal">
|
||||||
|
<mat-icon>business</mat-icon>
|
||||||
|
{{ 'labels.inputs.Legal Entity' | translate }}
|
||||||
|
</mat-button-toggle>
|
||||||
|
</mat-button-toggle-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transactional Profile Data Table -->
|
||||||
|
<div class="table-section">
|
||||||
|
@if (profileData.length > 0) {
|
||||||
|
<table mat-table [dataSource]="profileData" class="profile-table">
|
||||||
|
<!-- # Remittance Column -->
|
||||||
|
<ng-container matColumnDef="remittanceNumber">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.# Remittance' | translate }}</th>
|
||||||
|
<td mat-cell *matCellDef="let row">{{ row.remittanceNumber }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Avg Amount/Transaction Column -->
|
||||||
|
<ng-container matColumnDef="averageAmount">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Avg Amount/Transaction' | translate }}</th>
|
||||||
|
<td mat-cell *matCellDef="let row">{{ row.averageAmount | currency: row.currency }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- # Senders Column -->
|
||||||
|
<ng-container matColumnDef="senderCount">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.# Senders' | translate }}</th>
|
||||||
|
<td mat-cell *matCellDef="let row">{{ row.senderCount }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Sender Registry Column -->
|
||||||
|
<ng-container matColumnDef="senderRegistry">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Sender Registry' | translate }}</th>
|
||||||
|
<td mat-cell *matCellDef="let row">{{ row.senderRegistry }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr
|
||||||
|
mat-row
|
||||||
|
*matRowDef="let row; columns: displayedColumns; let i = index"
|
||||||
|
(click)="removeProfileEntry(i)"
|
||||||
|
matTooltip="{{ 'labels.text.Click to remove' | translate }}"
|
||||||
|
class="clickable-row"
|
||||||
|
></tr>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (profileData.length === 0) {
|
||||||
|
<div class="empty-table">
|
||||||
|
<mat-icon>table_chart</mat-icon>
|
||||||
|
<p>{{ 'labels.text.No transactional profile entries yet. Add one below.' | translate }}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Profile Entry Form -->
|
||||||
|
<div class="add-entry-section">
|
||||||
|
<h4>{{ 'labels.heading.Add Profile Entry' | translate }}</h4>
|
||||||
|
<form [formGroup]="profileForm" class="entry-form">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.# Remittance' | translate }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
formControlName="remittanceNumber"
|
||||||
|
placeholder="{{ 'labels.placeholders.# Remittance' | translate }}"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.Avg Amount/Transaction' | translate }}</mat-label>
|
||||||
|
<input matInput type="number" formControlName="averageAmount" placeholder="0.00" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.# Senders' | translate }}</mat-label>
|
||||||
|
<input matInput type="number" formControlName="senderCount" placeholder="0" min="0" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.Sender Registry' | translate }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
formControlName="senderRegistry"
|
||||||
|
placeholder="{{ 'labels.placeholders.Sender name' | translate }}"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<button
|
||||||
|
mat-stroked-button
|
||||||
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
(click)="addProfileEntry()"
|
||||||
|
[disabled]="profileForm.invalid"
|
||||||
|
>
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
{{ 'labels.buttons.Add Entry' | translate }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (errorMessage) {
|
||||||
|
<div class="error-banner">
|
||||||
|
<mat-icon>error_outline</mat-icon>
|
||||||
|
<span>{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="step-actions">
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
color="primary"
|
||||||
|
(click)="assignPayout()"
|
||||||
|
[disabled]="isLoading || !vendor || !recipientData || profileData.length === 0"
|
||||||
|
>
|
||||||
|
@if (isLoading) {
|
||||||
|
<mat-spinner diameter="20"></mat-spinner>
|
||||||
|
} @else {
|
||||||
|
<span><mat-icon>assignment_turned_in</mat-icon> {{ 'labels.buttons.Assign Payout' | translate }}</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+150
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+167
@@ -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<PayoutAssignmentResponse>();
|
||||||
|
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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 {}
|
||||||
@@ -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 {}
|
||||||
@@ -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<RemittanceTransaction> {
|
||||||
|
return this.http.get<RemittanceTransaction>(
|
||||||
|
`${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<ValidatedRecipient> {
|
||||||
|
return this.http.post<ValidatedRecipient>(
|
||||||
|
`${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<PayoutAssignmentResponse> {
|
||||||
|
return this.http.post<PayoutAssignmentResponse>(
|
||||||
|
`${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<PayoutConfirmationResponse> {
|
||||||
|
return this.http.post<PayoutConfirmationResponse>(
|
||||||
|
`${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<any> {
|
||||||
|
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<any> {
|
||||||
|
return this.http.request(
|
||||||
|
'DELETE',
|
||||||
|
`${this.baseUrl}/remittances/${this.tenant}/${vendor}/transactions/${transactionId}/payout-confirmation`,
|
||||||
|
{
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
body: { reason }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,6 +64,7 @@ import {
|
|||||||
faMinusCircle,
|
faMinusCircle,
|
||||||
faMinus,
|
faMinus,
|
||||||
faMoneyBillAlt,
|
faMoneyBillAlt,
|
||||||
|
faMoneyBillWave,
|
||||||
faMoneyCheck,
|
faMoneyCheck,
|
||||||
faPencilAlt,
|
faPencilAlt,
|
||||||
faPlay,
|
faPlay,
|
||||||
@@ -216,6 +217,7 @@ export class IconsModule {
|
|||||||
faMinus,
|
faMinus,
|
||||||
faMinusCircle,
|
faMinusCircle,
|
||||||
faMoneyBillAlt,
|
faMoneyBillAlt,
|
||||||
|
faMoneyBillWave,
|
||||||
faMoneyCheck,
|
faMoneyCheck,
|
||||||
faPause,
|
faPause,
|
||||||
faPencilAlt,
|
faPencilAlt,
|
||||||
|
|||||||
@@ -90,6 +90,14 @@
|
|||||||
window['env']['mifosInterbankTransfersApiVersion'] = '';
|
window['env']['mifosInterbankTransfersApiVersion'] = '';
|
||||||
window['env']['mifosInterbankTransfersEnabled'] = 'true';
|
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
|
// Enable Role-Based Access Control (RBAC) for menu/button permissions
|
||||||
// Set to true to enable RBAC, false (default) for backward compatibility
|
// Set to true to enable RBAC, false (default) for backward compatibility
|
||||||
window['env']['productionModeEnableRBAC'] = false;
|
window['env']['productionModeEnableRBAC'] = false;
|
||||||
|
|||||||
@@ -102,6 +102,14 @@
|
|||||||
window['env']['mifosInterbankTransfersApiVersion'] = '$MIFOS_INTERBANK_TRANSFERS_API_VERSION';
|
window['env']['mifosInterbankTransfersApiVersion'] = '$MIFOS_INTERBANK_TRANSFERS_API_VERSION';
|
||||||
window['env']['mifosInterbankTransfersEnabled'] = '$MIFOS_INTERBANK_TRANSFERS_ENABLED';
|
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
|
// Enable Role-Based Access Control (RBAC) for menu/button permissions
|
||||||
// Set to 'true' to enable RBAC, 'false' (default) for backward compatibility
|
// Set to 'true' to enable RBAC, 'false' (default) for backward compatibility
|
||||||
window['env']['productionModeEnableRBAC'] = '$MIFOS_PRODUCTION_MODE_ENABLE_RBAC';
|
window['env']['productionModeEnableRBAC'] = '$MIFOS_PRODUCTION_MODE_ENABLE_RBAC';
|
||||||
|
|||||||
@@ -502,6 +502,7 @@
|
|||||||
"Resend OTP": "Resend OTP",
|
"Resend OTP": "Resend OTP",
|
||||||
"Reset": "Reset",
|
"Reset": "Reset",
|
||||||
"Reset Password": "Reset Password",
|
"Reset Password": "Reset Password",
|
||||||
|
"Retry": "Retry",
|
||||||
"Retrieve": "Retrieve",
|
"Retrieve": "Retrieve",
|
||||||
"Revert Transaction": "Revert Transaction",
|
"Revert Transaction": "Revert Transaction",
|
||||||
"Run and Download Report": "Run and Download Report",
|
"Run and Download Report": "Run and Download Report",
|
||||||
@@ -561,7 +562,14 @@
|
|||||||
"View Active Charges": "View Active Charges",
|
"View Active Charges": "View Active Charges",
|
||||||
"View Inactive Charges": "View Inactive Charges",
|
"View Inactive Charges": "View Inactive Charges",
|
||||||
"Yes": "Yes",
|
"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": {
|
"catalogs": {
|
||||||
"Interest payment waiver": "Interest Payment Waiver",
|
"Interest payment waiver": "Interest Payment Waiver",
|
||||||
@@ -1301,7 +1309,31 @@
|
|||||||
"Date": "Date",
|
"Date": "Date",
|
||||||
"Secure & Reliable": "Secure & Reliable",
|
"Secure & Reliable": "Secure & Reliable",
|
||||||
"Community Driven": "Community Driven",
|
"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": {
|
"inputs": {
|
||||||
"Floating rate": "Floating Rate",
|
"Floating rate": "Floating Rate",
|
||||||
@@ -1519,6 +1551,7 @@
|
|||||||
"Client Details": "Client Details",
|
"Client Details": "Client Details",
|
||||||
"Client ID": "Client ID",
|
"Client ID": "Client ID",
|
||||||
"Client Id": "Client Id",
|
"Client Id": "Client Id",
|
||||||
|
"Confirmation Number": "Confirmation Number",
|
||||||
"Client Member": "Client Member",
|
"Client Member": "Client Member",
|
||||||
"Client Members": "Client Members",
|
"Client Members": "Client Members",
|
||||||
"Client Name": "Client Name",
|
"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",
|
"Do not calculate interest on past due principal balances": "Do not calculate interest on past due principal balances",
|
||||||
"Documents": "Documents",
|
"Documents": "Documents",
|
||||||
"Document Key": "Document Key",
|
"Document Key": "Document Key",
|
||||||
|
"Document": "Document",
|
||||||
|
"Document Number": "Document Number",
|
||||||
|
"Document number is required": "Document number is required",
|
||||||
"Document Type": "Document Type",
|
"Document Type": "Document Type",
|
||||||
"Dormancy Tracking": "Dormancy Tracking",
|
"Dormancy Tracking": "Dormancy Tracking",
|
||||||
"Download": "Download",
|
"Download": "Download",
|
||||||
@@ -1767,6 +1803,8 @@
|
|||||||
"Extend Repayment Period": "Extend Repayment Period",
|
"Extend Repayment Period": "Extend Repayment Period",
|
||||||
"External Asset Owner": "External Asset Owner",
|
"External Asset Owner": "External Asset Owner",
|
||||||
"External ID": "External ID",
|
"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": "External id",
|
"External id": "External id",
|
||||||
"External ID not found": "External ID not found",
|
"External ID not found": "External ID not found",
|
||||||
@@ -1795,6 +1833,7 @@
|
|||||||
"Filter By Version": "Filter By Version",
|
"Filter By Version": "Filter By Version",
|
||||||
"Financial Activity": "Financial Activity",
|
"Financial Activity": "Financial Activity",
|
||||||
"First Name": "First Name",
|
"First Name": "First Name",
|
||||||
|
"First name is required": "First name is required",
|
||||||
"First repayment on": "First repayment on",
|
"First repayment on": "First repayment on",
|
||||||
"Fixed Account": "Fixed Account",
|
"Fixed Account": "Fixed Account",
|
||||||
"Fixed Deposit": "Fixed Deposit",
|
"Fixed Deposit": "Fixed Deposit",
|
||||||
@@ -1986,6 +2025,7 @@
|
|||||||
"Last Active Transaction Date": "Last Active Transaction Date",
|
"Last Active Transaction Date": "Last Active Transaction Date",
|
||||||
"Last Loan Amount": "Last Loan Amount",
|
"Last Loan Amount": "Last Loan Amount",
|
||||||
"Last Name": "Last Name",
|
"Last Name": "Last Name",
|
||||||
|
"Last name is required": "Last name is required",
|
||||||
"Last Repayment": "Last Repayment",
|
"Last Repayment": "Last Repayment",
|
||||||
"Last Repayment Date": "Last Repayment Date",
|
"Last Repayment Date": "Last Repayment Date",
|
||||||
"Late": "Late",
|
"Late": "Late",
|
||||||
@@ -1995,6 +2035,7 @@
|
|||||||
"Liability Account": "Liability Account",
|
"Liability Account": "Liability Account",
|
||||||
"Liability Account (CR)": "Liability Account (CR)",
|
"Liability Account (CR)": "Liability Account (CR)",
|
||||||
"Lifted On": "Lifted On",
|
"Lifted On": "Lifted On",
|
||||||
|
"Loading vendors...": "Loading vendors...",
|
||||||
"Link savings": "Link savings",
|
"Link savings": "Link savings",
|
||||||
"Linked Savings Account": "Linked Savings Account",
|
"Linked Savings Account": "Linked Savings Account",
|
||||||
"Linked Savings Account(Dividend Posting)": "Linked Savings Account(Dividend Posting)",
|
"Linked Savings Account(Dividend Posting)": "Linked Savings Account(Dividend Posting)",
|
||||||
@@ -2426,6 +2467,7 @@
|
|||||||
"Schedule Date": "Schedule Date",
|
"Schedule Date": "Schedule Date",
|
||||||
"Score": "Score",
|
"Score": "Score",
|
||||||
"Screen Report": "Screen Report",
|
"Screen Report": "Screen Report",
|
||||||
|
"Search Client": "Search Client",
|
||||||
"Search Journal Entries": "Search Journal Entries",
|
"Search Journal Entries": "Search Journal Entries",
|
||||||
"Select Action": "Select Action",
|
"Select Action": "Select Action",
|
||||||
"Select Client Members for Transfer": "Select Client Members for Transfer",
|
"Select Client Members for Transfer": "Select Client Members for Transfer",
|
||||||
@@ -2570,6 +2612,7 @@
|
|||||||
"Trigger Type": "Trigger Type",
|
"Trigger Type": "Trigger Type",
|
||||||
"Tuesday": "Tuesday",
|
"Tuesday": "Tuesday",
|
||||||
"Type": "Type",
|
"Type": "Type",
|
||||||
|
"Type client name or account number": "Type client name or account number",
|
||||||
"Type/Quality": "Type/Quality",
|
"Type/Quality": "Type/Quality",
|
||||||
"Unassigned": "Unassigned",
|
"Unassigned": "Unassigned",
|
||||||
"Unassignment Date": "Unassignment Date",
|
"Unassignment Date": "Unassignment Date",
|
||||||
@@ -2692,7 +2735,45 @@
|
|||||||
"Overdue": "Overdue",
|
"Overdue": "Overdue",
|
||||||
"Overdue Fees": "Overdue Fees",
|
"Overdue Fees": "Overdue Fees",
|
||||||
"Select action": "Select action",
|
"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": {
|
"links": {
|
||||||
"Community": "Community",
|
"Community": "Community",
|
||||||
@@ -2782,6 +2863,8 @@
|
|||||||
"Interbank Transfer": "Interbank Transfer",
|
"Interbank Transfer": "Interbank Transfer",
|
||||||
"Interest Payment Waiver": "Interest Payment Waiver",
|
"Interest Payment Waiver": "Interest Payment Waiver",
|
||||||
"Payments": "Payments",
|
"Payments": "Payments",
|
||||||
|
"Remittances": "Remittances",
|
||||||
|
"Process Remittance": "Process Remittance",
|
||||||
"Payout Refund": "Payout Refund",
|
"Payout Refund": "Payout Refund",
|
||||||
"Post Interest": "Post Interest",
|
"Post Interest": "Post Interest",
|
||||||
"Post Interest As On": "Post Interest As On",
|
"Post Interest As On": "Post Interest As On",
|
||||||
@@ -2833,8 +2916,14 @@
|
|||||||
"Reject Additional Shares": "Reject Additional Shares"
|
"Reject Additional Shares": "Reject Additional Shares"
|
||||||
},
|
},
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
"# Remittance": "# Remittance",
|
||||||
"Add new server": "Add new server",
|
"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",
|
"Search Activity": "Search Activity",
|
||||||
|
"Sender name": "Sender name",
|
||||||
"Enter your username": "Enter your username",
|
"Enter your username": "Enter your username",
|
||||||
"Enter your password": "Enter your password"
|
"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",
|
"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 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",
|
"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",
|
"Cancel Pending Sale": "Cancel Pending Sale",
|
||||||
"Cashier Transactions": "Cashier Transactions",
|
"Cashier Transactions": "Cashier Transactions",
|
||||||
"Cashiers": "Cashiers",
|
"Cashiers": "Cashiers",
|
||||||
@@ -3431,6 +3521,7 @@
|
|||||||
"Preferences for generating account numbers for client": "Preferences for generating account numbers for client, loan and savings accounts",
|
"Preferences for generating account numbers for client": "Preferences for generating account numbers for client, loan and savings accounts",
|
||||||
"Products": "Products",
|
"Products": "Products",
|
||||||
"Products Mix": "Products Mix",
|
"Products Mix": "Products Mix",
|
||||||
|
"Process Remittance": "Process Remittance",
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
"Provisioning Criteria": "Provisioning Criteria",
|
"Provisioning Criteria": "Provisioning Criteria",
|
||||||
"Provisioning Entries": "Provisioning Entries",
|
"Provisioning Entries": "Provisioning Entries",
|
||||||
@@ -3482,6 +3573,7 @@
|
|||||||
"Scheduler Job History": "Scheduler Job History",
|
"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.",
|
"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": "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",
|
"Search by Text": "Search by Text",
|
||||||
"SearchByName": "Search by Name",
|
"SearchByName": "Search by Name",
|
||||||
"SearchByClient": "Search by client name, external Id, mobile",
|
"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.",
|
"“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",
|
"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.",
|
"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": {
|
"auditTrail": {
|
||||||
"actions": {
|
"actions": {
|
||||||
@@ -3840,6 +3946,7 @@
|
|||||||
"Indicates whether to enforce a minimum balance": "Indicates whether to enforce a minimum balance",
|
"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",
|
"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",
|
"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.",
|
"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 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",
|
"Interest rate is posted or credited to a saving account": "The period at which interest rate is posted or credited to a saving account",
|
||||||
|
|||||||
@@ -560,7 +560,15 @@
|
|||||||
"Is Self Service": "Es autoservicio",
|
"Is Self Service": "Es autoservicio",
|
||||||
"Advanced Search": "Búsqueda avanzada",
|
"Advanced Search": "Búsqueda avanzada",
|
||||||
"Show advanced search": "Mostrar 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": {
|
"catalogs": {
|
||||||
"Interest payment waiver": "Exención de pago de intereses",
|
"Interest payment waiver": "Exención de pago de intereses",
|
||||||
@@ -801,6 +809,9 @@
|
|||||||
"Passport": "Pasaporte",
|
"Passport": "Pasaporte",
|
||||||
"Id": "Identificación",
|
"Id": "Identificación",
|
||||||
"Drivers License": "Licencia de conducir",
|
"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",
|
"Any Other Id Type": "Cualquier otro tipo de identificación",
|
||||||
"Check loan repayment overdue": "Verificar pagos vencidos de créditos",
|
"Check loan repayment overdue": "Verificar pagos vencidos de créditos",
|
||||||
"Update loan arrears aging": "Actualizar antigüedad de atrasos en los 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",
|
"Contact Information": "Información de Contacto",
|
||||||
"Classification": "Clasificación",
|
"Classification": "Clasificación",
|
||||||
"Status": "Estado",
|
"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": {
|
"inputs": {
|
||||||
"Floating rate": "Tasa flotante",
|
"Floating rate": "Tasa flotante",
|
||||||
@@ -1986,6 +2020,7 @@
|
|||||||
"Liability Account": "Cuenta de responsabilidad",
|
"Liability Account": "Cuenta de responsabilidad",
|
||||||
"Liability Account (CR)": "Cuenta de Pasivo (CR)",
|
"Liability Account (CR)": "Cuenta de Pasivo (CR)",
|
||||||
"Lifted On": "levantado",
|
"Lifted On": "levantado",
|
||||||
|
"Loading vendors...": "Cargando proveedores...",
|
||||||
"Link savings": "Ahorro de enlaces",
|
"Link savings": "Ahorro de enlaces",
|
||||||
"Linked Savings Account": "Cuenta de Ahorros vinculada",
|
"Linked Savings Account": "Cuenta de Ahorros vinculada",
|
||||||
"Linked Savings Account(Dividend Posting)": "Cuenta de Ahorros vinculada (publicación de dividendos)",
|
"Linked Savings Account(Dividend Posting)": "Cuenta de Ahorros vinculada (publicación de dividendos)",
|
||||||
@@ -2462,6 +2497,7 @@
|
|||||||
"Start Time": "Hora de inicio",
|
"Start Time": "Hora de inicio",
|
||||||
"Started On": "Iniciado en",
|
"Started On": "Iniciado en",
|
||||||
"Starting date": "Fecha de inicio",
|
"Starting date": "Fecha de inicio",
|
||||||
|
"State": "Estado",
|
||||||
"State / Province": "Estado o Provincia",
|
"State / Province": "Estado o Provincia",
|
||||||
"Status": "Estado",
|
"Status": "Estado",
|
||||||
"Step Name": "Nombre del paso",
|
"Step Name": "Nombre del paso",
|
||||||
@@ -2821,11 +2857,50 @@
|
|||||||
"Redeem Shares": "Canjear acciones",
|
"Redeem Shares": "Canjear acciones",
|
||||||
"Approve Additional Shares": "Aprobar acciones adicionales",
|
"Approve Additional Shares": "Aprobar acciones adicionales",
|
||||||
"Reject Additional Shares": "Rechazar 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": {
|
"placeholders": {
|
||||||
|
"# Remittance": "# Remesa",
|
||||||
"Add new server": "Agregar nuevo servidor",
|
"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",
|
"Search Activity": "Actividad de búsqueda",
|
||||||
|
"Sender name": "Nombre del remitente",
|
||||||
"Enter your username": "Ingrese su nombre de usuario",
|
"Enter your username": "Ingrese su nombre de usuario",
|
||||||
"Enter your password": "Ingrese su contraseña"
|
"Enter your password": "Ingrese su contraseña"
|
||||||
},
|
},
|
||||||
@@ -3852,7 +3927,23 @@
|
|||||||
"Create Interest Refund": "Crear reembolso de intereses",
|
"Create Interest Refund": "Crear reembolso de intereses",
|
||||||
"ACTIVE": "ACTIVO",
|
"ACTIVE": "ACTIVO",
|
||||||
"PENDING": "PENDIENTE",
|
"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": {
|
"countries": {
|
||||||
"AF": "Afganistán",
|
"AF": "Afganistán",
|
||||||
|
|||||||
@@ -85,6 +85,15 @@ export const environment = {
|
|||||||
mifosInterbankTransfersApiVersion: loadedEnv['mifosInterbankTransfersApiVersion'] || '/v1.0',
|
mifosInterbankTransfersApiVersion: loadedEnv['mifosInterbankTransfersApiVersion'] || '/v1.0',
|
||||||
mifosInterbankTransfersEnabled: loadedEnv['mifosInterbankTransfersEnabled'] ?? true,
|
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,
|
minPasswordLength: loadedEnv['minPasswordLength'] || 12,
|
||||||
|
|
||||||
/** External National ID System integration */
|
/** External National ID System integration */
|
||||||
|
|||||||
@@ -87,6 +87,14 @@ export const environment = {
|
|||||||
mifosInterbankTransfersApiVersion: window.env?.mifosInterbankTransfersApiVersion || '/v1.0',
|
mifosInterbankTransfersApiVersion: window.env?.mifosInterbankTransfersApiVersion || '/v1.0',
|
||||||
mifosInterbankTransfersEnabled: window.env?.mifosInterbankTransfersEnabled ?? true,
|
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
|
* External National ID System Integration
|
||||||
* When enabled, client creation/editing will lookup external National ID
|
* When enabled, client creation/editing will lookup external National ID
|
||||||
|
|||||||
@@ -719,6 +719,24 @@ mifosx-notifications-page td {
|
|||||||
color: inherit !important;
|
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 {
|
.full-width-flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+5
@@ -24,6 +24,11 @@ interface Window {
|
|||||||
mifosInterbankTransfersApiProvider?: string;
|
mifosInterbankTransfersApiProvider?: string;
|
||||||
mifosInterbankTransfersApiVersion?: string;
|
mifosInterbankTransfersApiVersion?: string;
|
||||||
mifosInterbankTransfersEnabled?: boolean;
|
mifosInterbankTransfersEnabled?: boolean;
|
||||||
|
mifosRemittanceApiUrl?: string;
|
||||||
|
mifosRemittanceApiProvider?: string;
|
||||||
|
mifosRemittanceApiVersion?: string;
|
||||||
|
mifosRemittanceEnabled?: boolean;
|
||||||
|
mifosRemittanceApiKey?: string;
|
||||||
minPasswordLength?: number;
|
minPasswordLength?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user