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:
+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.'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user