feat: add Current Account module, DIP verification, charges integration, and translations

- Current Account module with full CRUD, transactions, charges, hold/release, block/unblock
- Current Products management under Products module
- DIP mobile number verification with real API call to /lookup/check
- Sanctions verification dialog
- Charges support for Current Account (chargeAppliesTo=5) in create/edit
- Fixed permissions from SAVINGSACCOUNT to CURRENTACCOUNT across all current account components
- Added all missing translation keys for Current Account in en-US.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-03-17 18:23:37 +05:30
co-authored by Claude Opus 4.6
parent 73aa69ee02
commit 0a32d7135e
210 changed files with 15048 additions and 155 deletions
@@ -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="container mat-elevation-z8">
<mat-card>
@if (transactionType.holdamount) {
<mat-card-title>{{ 'labels.heading.Hold Amount' | translate }}</mat-card-title>
}
@if (transactionType.blockaccount) {
<mat-card-title>{{ 'labels.heading.Block Current Account' | translate }}</mat-card-title>
}
<form [formGroup]="manageCurrentAccountForm" (ngSubmit)="submit()">
<mat-card-content class="layout-row">
<mat-form-field class="flex-100">
<mat-label>{{ 'labels.inputs.Reason' | translate }}</mat-label>
<mat-select required formControlName="reasonForBlock">
@for (codeValue of reasonOptions; track codeValue) {
<mat-option [value]="codeValue.id">
{{ codeValue.name }}
</mat-option>
}
</mat-select>
@if (manageCurrentAccountForm.controls.reasonForBlock.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Reason' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
@if (transactionType.holdamount) {
<mat-form-field class="flex-100" (click)="transactionDatePicker.open()">
<mat-label>{{ 'labels.inputs.Transaction Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="transactionDatePicker"
required
formControlName="transactionDate"
/>
<mat-datepicker-toggle matSuffix [for]="transactionDatePicker"></mat-datepicker-toggle>
<mat-datepicker #transactionDatePicker></mat-datepicker>
@if (manageCurrentAccountForm.controls.transactionDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
}
@if (transactionType.holdamount) {
<mifosx-input-amount
class="flex-100"
[currency]="currency"
[isRequired]="true"
[inputFormControl]="manageCurrentAccountForm.controls.transactionAmount"
[inputLabel]="'Transaction Amount'"
>
</mifosx-input-amount>
}
</mat-card-content>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../transactions']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!manageCurrentAccountForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</form>
</mat-card>
</div>
@@ -0,0 +1,11 @@
/**
* 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 {
width: 37rem;
}
@@ -0,0 +1,194 @@
/**
* 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, Input, OnInit, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Dates } from 'app/core/utils/dates';
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
import { Currency } from 'app/shared/models/general.model';
import { SystemService } from 'app/system/system.service';
import { MatCard, MatCardTitle, MatCardContent, MatCardActions } from '@angular/material/card';
import { InputAmountComponent } from '../../../shared/input-amount/input-amount.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
type TransactionCommandType = 'holdamount' | 'blockaccount' | 'blockdeposit' | 'blockwithdrawal';
interface TransactionType {
holdamount: boolean;
blockaccount: boolean;
blockdeposit: boolean;
blockwithdrawal: boolean;
}
@Component({
selector: 'mifosx-manage-current-account',
templateUrl: './manage-current-account.component.html',
styleUrls: ['./manage-current-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCardTitle,
InputAmountComponent
]
})
export class ManageCurrentAccountComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private dateUtils = inject(Dates);
private route = inject(ActivatedRoute);
private router = inject(Router);
private systemService = inject(SystemService);
private settingsService = inject(SettingsService);
@Input() currency: Currency;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Manage Current Account form. */
manageCurrentAccountForm: UntypedFormGroup;
/** Current Account Id */
currentAccountId: string;
transactionCommand: TransactionCommandType;
reasonOptions: any = [];
transactionType: TransactionType = {
holdamount: false,
blockaccount: false,
blockdeposit: false,
blockwithdrawal: false
};
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {Dates} dateUtils Date Utils
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.transactionCommand = this.route.snapshot.params['name'].toLowerCase().replaceAll(' ', '');
this.transactionType[this.transactionCommand] = true;
this.currentAccountId = this.route.snapshot.params['currentAccountId'];
}
/**
* Creates the post interest current account form.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createManageCurrentAccountForm();
if (
this.transactionType.holdamount ||
this.transactionType.blockaccount ||
this.transactionType.blockdeposit ||
this.transactionType.blockwithdrawal
) {
this.getCodeValues();
}
}
getCodeValues() {
let codeName = 'CurrentAccountTransactionFreezeReasons'; // Default Hold Amount
if (this.transactionType.blockaccount) {
codeName = 'CurrentAccountBlockReasons';
} else if (this.transactionType.blockdeposit) {
codeName = 'CreditTransactionFreezeReasons';
} else if (this.transactionType.blockwithdrawal) {
codeName = 'DebitTransactionFreezeReasons';
}
this.systemService.getCodes().subscribe((codes: any) => {
codes.some((code: any) => {
if (code.name === codeName) {
this.systemService.getCodeValues(code.id).subscribe((codeValues: any) => {
this.reasonOptions = codeValues;
return true;
});
}
return false;
});
});
}
/**
* Creates the manage current account form.
*/
createManageCurrentAccountForm() {
if (this.transactionType.holdamount) {
this.manageCurrentAccountForm = this.formBuilder.group({
reasonForBlock: [
'',
Validators.required
],
transactionDate: [
'',
Validators.required
],
transactionAmount: [
0.0,
Validators.required
]
});
} else {
this.manageCurrentAccountForm = this.formBuilder.group({
reasonForBlock: [
'',
Validators.required
]
});
}
}
submit() {
let command = '';
let payload: { transactionAmount?: number; [key: string]: any } = {};
if (this.transactionType.holdamount) {
const manageCurrentAccountFormData = this.manageCurrentAccountForm.value;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const prevTransactionDate: Date = this.manageCurrentAccountForm.value.transactionDate;
if (manageCurrentAccountFormData.transactionDate instanceof Date) {
manageCurrentAccountFormData.transactionDate = this.dateUtils.formatDate(prevTransactionDate, dateFormat);
}
payload = {
...manageCurrentAccountFormData,
dateFormat,
locale
};
command = 'holdAmount';
payload['transactionAmount'] = payload['transactionAmount'] * 1;
this.currentAccountsService
.executeCurrentAccountTransactionsCommand(this.currentAccountId, command, payload)
.subscribe((response: any) => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
} else {
payload = {
...this.manageCurrentAccountForm.value
};
command = 'block';
if (this.transactionType.blockdeposit) {
command = 'blockCredit';
} else if (this.transactionType.blockwithdrawal) {
command = 'blockDebit';
}
this.currentAccountsService
.executeCurrentAccountCommand(this.currentAccountId, command, payload)
.subscribe((response: any) => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
}
}
}