Compare commits

...
Author SHA1 Message Date
Sonali Likhitkar f499a6b57e product creation changes and preview changes 2026-09-01 10:46:36 +05:30
Sonali LikhitkarandClaude Fable 5 f0b4e98e71 feat(loan-products): extend loan product stepper with legacy account-type fields
Add UI-only fields across the details, settings, charges, terms, and
accounting steps to mirror the legacy Account Type Creation screen
(classification, cheque book, interest, and additional settings).
These are stripped from the create/edit payload via
CUSTOM_UI_ONLY_FIELDS until a backend mechanism (e.g. a linked data
table) exists to persist them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 16:27:36 +05:30
niteshandClaude Sonnet 5 e58fd3c79a feat(deposits): sync linked account with maturity terms, add deposit certificate printing
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
Fixed and Recurring Deposit wizards now bidirectionally sync the Product tab's
linked-account selection with the new Terms tab's Transfer-to-Savings account,
disable the Withdraw Deposit maturity instruction end-to-end, and card-style
the Transfer to Savings picker to match the existing linked-account UI. Adds
a printable FD/RD deposit certificate from the account view page, following
the existing letter-mgmt certificate pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 11:05:25 +05:30
nitesh 1b2ef5126e Merge branch 'dev' of https://git.lincpay.tech/CBSNexora/cbsnexora into dev
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
2026-08-19 10:44:53 +05:30
nitesh b953afe2da fix(clients): remove leftover unresolved git conflict markers on Last Name field
An unresolved git stash conflict had been committed as-is, leaving
literal <<<<<<< Updated upstream / ======= / >>>>>>> Stashed changes
markers inside the template. Angular rendered them as plain text right
in the create-client form. Kept the blur-triggered capitalizeControl()
version (matches First/Middle Name) and dropped the now-unused
formatName() left behind by the other side of the conflict.
2026-08-19 10:43:00 +05:30
nitesh ff781e50e3 feat(savings,deposits,current-accounts): streamline account activation flow
Matches the backend change that stopped auto-depositing money at
activation: removes the opening-balance field from every Activate
screen (Savings, Current, FD, RD) since funding now happens as a
separate deposit afterward, and removes the Digital-Facility step from
every Create wizard since every facility starts disabled by default
and gets turned on later through Edit Nominee & Features.

Also merges Approve+Activate into a single action for Savings,
Current, FD and RD accounts -- one click takes the account straight to
Active instead of two separate screens.
2026-08-19 10:39:52 +05:30
85 changed files with 5209 additions and 924 deletions
+7
View File
@@ -69,3 +69,10 @@ src/environments/.env.ts
/playwright-report/
/blob-report/
/playwright/.cache/
# JVM crash / replay logs
hs_err_pid*.log
replay_pid*.log
# Microsoft Office temporary lock files
~$*.xlsx
@@ -263,7 +263,6 @@
<mat-form-field appearance="outline" class="name-field last-name">
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>person</mat-icon></span>
<<<<<<< Updated upstream
<input
matInput
required
@@ -272,9 +271,6 @@
(blur)="capitalizeControl('lastname')"
style="text-transform: capitalize"
/>
=======
<input matInput required formControlName="lastname" autocomplete="off" (input)="formatName('lastname')" />
>>>>>>> Stashed changes
@if (createClientForm.controls.lastname.hasError('required')) {
<mat-error>Last name is <strong>required</strong></mat-error>
} @else if (createClientForm.controls.lastname.hasError('pattern')) {
@@ -2590,28 +2590,6 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
dormantFlag: 'ACTIVE'
};
}
/**
* Automatically capitalizes the first letter of each word and lowercases the rest.
* e.g., "sri rAm" -> "Sri Ram"
*/
formatName(controlName: string): void {
const control = this.createClientForm.get(controlName);
if (!control || typeof control.value !== 'string') return;
const val = control.value;
const formatted = val
.split(' ')
.map((word: string) => {
if (!word) return '';
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
})
.join(' ');
if (formatted !== val) {
control.setValue(formatted, { emitEvent: false });
}
}
}
/**
@@ -12,9 +12,7 @@
<fa-icon icon="university" class="cca-header__icon"></fa-icon>
<div>
<h2 class="cca-header__title">Create Current Account</h2>
<div class="cca-header__sub">
Pick a product, confirm currency, set digital channels and review before submit.
</div>
<div class="cca-header__sub">Pick a product, confirm currency and review before submit.</div>
</div>
</div>
@@ -78,34 +76,7 @@
</div>
</mat-tab>
<!-- Tab 3: Digital -->
<mat-tab [disabled]="!currentAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="mobile-alt" class="cca-tab__icon"></fa-icon>
Digital
</ng-template>
<div class="cca-tab-body">
@if (currentAccountProductTemplate) {
<mifosx-current-account-digital-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
></mifosx-current-account-digital-step>
}
<div class="cca-actions">
<button mat-stroked-button (click)="prev()">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<span class="cca-actions__spacer"></span>
<button mat-raised-button color="primary" (click)="next()">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</div>
</mat-tab>
<!-- Tab 4: Preview -->
<!-- Tab 3: Preview -->
<mat-tab [disabled]="!currentAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="eye" class="cca-tab__icon"></fa-icon>
@@ -57,8 +57,8 @@ export class CreateCurrentAccountComponent {
currentAccountProductTemplate: any;
/** Currently visible wizard tab (0-indexed). */
selectedTabIndex = 0;
/** Total wizard tabs — Product, Currency, Digital, Preview. */
private readonly totalTabs = 4;
/** Total wizard tabs — Product, Currency, Preview. */
private readonly totalTabs = 3;
@ViewChild(CurrentAccountDetailsStepComponent, { static: true })
currentAccountDetailsStep: CurrentAccountDetailsStepComponent;
@@ -30,18 +30,6 @@
</mat-error>
}
</mat-form-field>
<mat-form-field class="flex-fill">
<mat-label>Opening Balance</mat-label>
<input matInput type="number" [min]="minRequiredOpeningBalance ?? 0" formControlName="openingBalance" />
<span matPrefix>₹&nbsp;</span>
@if (minRequiredOpeningBalance) {
<mat-hint>Minimum required: ₹{{ minRequiredOpeningBalance }}</mat-hint>
}
@if (activateCurrentAccountForm.controls.openingBalance.hasError('min')) {
<mat-error>Opening balance must be at least ₹{{ minRequiredOpeningBalance ?? 0 }}</mat-error>
}
</mat-form-field>
</div>
</mat-card-content>
@@ -7,7 +7,7 @@
*/
/** Angular Imports */
import { Component, OnInit, Input, inject } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Dates } from 'app/core/utils/dates';
@@ -36,8 +36,6 @@ export class ActivateCurrentAccountComponent implements OnInit {
private router = inject(Router);
private settingsService = inject(SettingsService);
@Input() minRequiredOpeningBalance: number | null = null;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
@@ -71,18 +69,10 @@ export class ActivateCurrentAccountComponent implements OnInit {
* Creates the activate current account form.
*/
createActivateCurrentAccountForm() {
const minBalance = this.minRequiredOpeningBalance ?? 0;
this.activateCurrentAccountForm = this.formBuilder.group({
activatedOnDate: [
'',
Validators.required
],
openingBalance: [
minBalance,
[
Validators.required,
Validators.min(minBalance)
]
]
});
}
@@ -10,6 +10,7 @@
<mat-card>
<form [formGroup]="approveCurrentAccountForm" (ngSubmit)="submit()">
<mat-card-content>
<p class="mat-body-2">This approves and activates the account in one step -- it goes straight to Active.</p>
<div class="layout-column">
<mat-form-field (click)="approvedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Approved On Date' | translate }}</mat-label>
@@ -81,8 +81,9 @@ export class ApproveCurrentAccountComponent implements OnInit {
}
/**
* Submits the form and approves the current account,
* if successful redirects to the current account.
* Submits the form, approves the current account, then immediately activates it on the same
* date -- Approve and Activate are still two separate Fineract commands underneath (each shows
* up in the audit trail), but the officer only sees and clicks through one step.
*/
submit() {
const approveCurrentAccountFormData = this.approveCurrentAccountForm.value;
@@ -98,7 +99,16 @@ export class ApproveCurrentAccountComponent implements OnInit {
locale
};
this.currentAccountsService.executeCurrentAccountCommand(this.accountId, 'approve', data).subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
const activationData = {
activatedOnDate: approveCurrentAccountFormData.approvedOnDate,
dateFormat,
locale
};
this.currentAccountsService
.executeCurrentAccountCommand(this.accountId, 'activate', activationData)
.subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
});
}
}
@@ -13,9 +13,7 @@
<mifosx-reject-current-account></mifosx-reject-current-account>
}
@if (actions['Activate']) {
<mifosx-activate-current-account
[minRequiredOpeningBalance]="accountData?.minRequiredOpeningBalance"
></mifosx-activate-current-account>
<mifosx-activate-current-account></mifosx-activate-current-account>
}
@if (actions['Undo Approval']) {
<mifosx-undo-approval-current-account></mifosx-undo-approval-current-account>
@@ -15,7 +15,6 @@
<h3 class="dig-header__title">Digital &amp; Account Features</h3>
<p class="dig-header__sub">Toggle the facilities available for this account (as per RBI guidelines)</p>
</div>
<span class="dig-header__badge">FROM PRODUCT</span>
</div>
<div class="dig-grid">
@@ -52,12 +52,11 @@ export class CurrentAccountDigitalStepComponent {
}
}
// Note: a new account always starts with every digital facility off, even if the product has
// its own defaults configured -- those are enabled deliberately, one at a time, after the
// account is created and active, not inherited automatically at opening.
@Input()
set currentAccountProductTemplate(value: any) {
if (!this.hasExistingAccount && value?.accountFacilityConfig) {
this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig };
}
}
currentAccountProductTemplate: any;
get currentAccountDigital() {
return { accountFacilityConfig: { ...this.facilityConfig } };
@@ -136,12 +136,11 @@ export class CurrentAccountPreviewStepComponent implements OnChanges {
return this.currentAccountSettingsForm?.getRawValue?.() || this.currentAccount || {};
}
/** Digital facility config as actually captured by a digital step in this wizard -- no fallback
* to the product's own defaults, since account creation no longer has a digital step at all and
* must not preview facilities the officer never actually chose. */
get digitalFacilityConfig(): any {
return (
this.currentAccountDigital?.accountFacilityConfig ??
this.currentAccountProductTemplate?.accountFacilityConfig ??
null
);
return this.currentAccountDigital?.accountFacilityConfig ?? null;
}
get hasDigitalSection(): boolean {
@@ -33,6 +33,7 @@
<mifosx-fixed-deposit-account-details-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
(fixedDepositsAccountProductTemplate)="setTemplate($event)"
[externalLinkedAccountId]="fixedDepositAccountMaturityTermsStep?.selectedTransferAccount?.id ?? null"
[hideButtons]="true"
>
</mifosx-fixed-deposit-account-details-step>
@@ -107,7 +108,40 @@
</div>
</mat-tab>
<!-- Tab 4: Preview -->
<!-- Tab 4: Terms -->
<mat-tab [disabled]="!fixedDepositsAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="file-alt" class="cfa-tab__icon"></fa-icon>
Terms
</ng-template>
<div class="cfa-tab-body">
@if (fixedDepositsAccountProductTemplate) {
<mifosx-fixed-deposit-account-maturity-terms-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
[linkedSavingsAccountId]="
fixedDepositsAccountDetailsStep?.selectedLinkedAccount?.accountType === 'SAVINGS'
? fixedDepositsAccountDetailsStep?.selectedLinkedAccount?.id
: null
"
></mifosx-fixed-deposit-account-maturity-terms-step>
}
<div class="cfa-actions">
<button mat-stroked-button (click)="prev()">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<span class="cfa-actions__spacer"></span>
<button mat-raised-button color="primary" (click)="next()">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</div>
</mat-tab>
<!-- Tab 5: Preview -->
<mat-tab [disabled]="!fixedDepositsAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="eye" class="cfa-tab__icon"></fa-icon>
@@ -121,6 +155,7 @@
[fixedDepositAccountData]="fixedDepositAccount"
[selectedTenure]="selectedTenure"
[fdNominees]="fixedDepositAccountCurrencyStep?.nominees ?? []"
[closureSettings]="fixedDepositAccountMaturityTermsStep?.closureSettings ?? {}"
[hideButtons]="true"
(submitEvent)="submit()"
></mifosx-fixed-deposit-account-preview-step>
@@ -24,6 +24,7 @@ import { MatTabsModule } from '@angular/material/tabs';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { FixedDepositAccountInterestRateChartStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component';
import { FixedDepositAccountPreviewStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component';
import { FixedDepositAccountMaturityTermsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -40,6 +41,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
FixedDepositAccountDetailsStepComponent,
FixedDepositAccountInterestRateChartStepComponent,
FixedDepositAccountCurrencyStepComponent,
FixedDepositAccountMaturityTermsStepComponent,
FixedDepositAccountPreviewStepComponent
]
})
@@ -56,6 +58,10 @@ export class CreateFixedDepositAccountComponent {
fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent;
@ViewChild(FixedDepositAccountCurrencyStepComponent)
fixedDepositAccountCurrencyStep: FixedDepositAccountCurrencyStepComponent;
@ViewChild(FixedDepositAccountPreviewStepComponent)
fixedDepositAccountPreviewStep: FixedDepositAccountPreviewStepComponent;
@ViewChild(FixedDepositAccountMaturityTermsStepComponent)
fixedDepositAccountMaturityTermsStep: FixedDepositAccountMaturityTermsStepComponent;
/** Fixed Deposits Account Template */
fixedDepositsAccountTemplate: any;
@@ -63,8 +69,8 @@ export class CreateFixedDepositAccountComponent {
fixedDepositsAccountProductTemplate: any;
/** Currently visible wizard tab (0-indexed). */
selectedTabIndex = 0;
/** Total wizard tabs — Product, Currency, Chart, Preview. */
private readonly totalTabs = 4;
/** Total wizard tabs — Product, Currency, Chart, Terms, Preview. */
private readonly totalTabs = 5;
/** Active CBS base interest rate versions for the selected product's FD type */
cbsBaseRates: any[] = [];
/** Tenure selected from the interest rate chart step */
@@ -243,6 +249,7 @@ export class CreateFixedDepositAccountComponent {
...this.fixedDepositAccount,
...terms,
...settings,
...(this.fixedDepositAccountMaturityTermsStep?.closureSettings ?? {}),
...(this.selectedTenure
? {
depositPeriod: this.selectedTenure.depositPeriod,
@@ -35,6 +35,7 @@
<mifosx-fixed-deposit-account-details-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountAndTemplate"
(fixedDepositsAccountProductTemplate)="setTemplate($event)"
[externalLinkedAccountId]="fixedDepositAccountMaturityTermsStep?.selectedTransferAccount?.id ?? null"
[hideButtons]="true"
>
</mifosx-fixed-deposit-account-details-step>
@@ -111,7 +112,40 @@
</div>
</mat-tab>
<!-- Tab 4: Preview -->
<!-- Tab 4: Terms -->
<mat-tab [disabled]="!fixedDepositsAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="file-alt" class="cfa-tab__icon"></fa-icon>
Terms
</ng-template>
<div class="cfa-tab-body">
@if (fixedDepositsAccountProductTemplate) {
<mifosx-fixed-deposit-account-maturity-terms-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountAndTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
[linkedSavingsAccountId]="
fixedDepositsAccountDetailsStep?.selectedLinkedAccount?.accountType === 'SAVINGS'
? fixedDepositsAccountDetailsStep?.selectedLinkedAccount?.id
: null
"
></mifosx-fixed-deposit-account-maturity-terms-step>
}
<div class="cfa-actions">
<button mat-stroked-button (click)="prev()">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<span class="cfa-actions__spacer"></span>
<button mat-raised-button color="primary" (click)="next()">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</div>
</mat-tab>
<!-- Tab 5: Preview -->
<mat-tab [disabled]="!fixedDepositsAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="eye" class="cfa-tab__icon"></fa-icon>
@@ -125,6 +159,7 @@
[fixedDepositAccountData]="fixedDepositAccount"
[selectedTenure]="selectedTenure"
[fdNominees]="fixedDepositAccountCurrencyStep?.nominees ?? []"
[closureSettings]="fixedDepositAccountMaturityTermsStep?.closureSettings ?? {}"
[hideButtons]="true"
(submitEvent)="submit()"
></mifosx-fixed-deposit-account-preview-step>
@@ -25,6 +25,7 @@ import { MatTabsModule } from '@angular/material/tabs';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { FixedDepositAccountInterestRateChartStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component';
import { FixedDepositAccountPreviewStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component';
import { FixedDepositAccountMaturityTermsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -41,6 +42,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
FixedDepositAccountDetailsStepComponent,
FixedDepositAccountInterestRateChartStepComponent,
FixedDepositAccountCurrencyStepComponent,
FixedDepositAccountMaturityTermsStepComponent,
FixedDepositAccountPreviewStepComponent
]
})
@@ -58,6 +60,10 @@ export class EditFixedDepositAccountComponent {
fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent;
@ViewChild(FixedDepositAccountCurrencyStepComponent)
fixedDepositAccountCurrencyStep: FixedDepositAccountCurrencyStepComponent;
@ViewChild(FixedDepositAccountPreviewStepComponent)
fixedDepositAccountPreviewStep: FixedDepositAccountPreviewStepComponent;
@ViewChild(FixedDepositAccountMaturityTermsStepComponent)
fixedDepositAccountMaturityTermsStep: FixedDepositAccountMaturityTermsStepComponent;
/** Full account data + template from resolver */
fixedDepositsAccountAndTemplate: any;
@@ -65,7 +71,7 @@ export class EditFixedDepositAccountComponent {
fixedDepositsAccountProductTemplate: any;
/** Currently visible wizard tab (0-indexed). */
selectedTabIndex = 0;
private readonly totalTabs = 4;
private readonly totalTabs = 5;
/** Active CBS base interest rate versions for the selected product's FD type */
cbsBaseRates: any[] = [];
/** Tenure selected from the interest rate chart step */
@@ -236,6 +242,7 @@ export class EditFixedDepositAccountComponent {
...this.fixedDepositAccount,
...terms,
...settings,
...(this.fixedDepositAccountMaturityTermsStep?.closureSettings ?? {}),
...(this.selectedTenure
? {
depositPeriod: this.selectedTenure.depositPeriod,
@@ -7,7 +7,17 @@
*/
/** Angular Imports */
import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core';
import {
Component,
OnInit,
OnChanges,
SimpleChanges,
Input,
Output,
EventEmitter,
inject,
ChangeDetectorRef
} from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, FormsModule } from '@angular/forms';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
@@ -43,7 +53,7 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc
IntroducerSearchComponent
]
})
export class FixedDepositAccountDetailsStepComponent implements OnInit {
export class FixedDepositAccountDetailsStepComponent implements OnInit, OnChanges {
private formBuilder = inject(UntypedFormBuilder);
private fixedDepositsService = inject(FixedDepositsService);
private settingsService = inject(SettingsService);
@@ -56,6 +66,9 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
/** Fixed Deposits Account Template */
@Input() fixedDepositsAccountTemplate: any;
@Input() hideButtons = false;
/** Linked-account id picked elsewhere in the wizard (e.g. the Terms step's Transfer to Savings
* account) -- pre-fills this card so the officer doesn't have to pick the same account twice. */
@Input() externalLinkedAccountId: number | null = null;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
@@ -222,6 +235,23 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
this.dalSearch = '';
}
ngOnChanges(changes: SimpleChanges): void {
if (!changes.externalLinkedAccountId || this.selectedLinkedAccount) {
return;
}
const id = this.externalLinkedAccountId;
if (!id) return;
const savings = this.dalAllSavings.find((a: any) => a.id === id);
const current = this.dalAllCurrent.find((a: any) => a.id === id);
if (savings) {
this.dalSelectedType = 'SAVINGS';
this.selectedLinkedAccount = savings;
} else if (current) {
this.dalSelectedType = 'CURRENT';
this.selectedLinkedAccount = current;
}
}
private loadClientAccounts(): void {
const clientId = String(this.fixedDepositsAccountTemplate?.clientId);
if (!clientId) return;
@@ -0,0 +1,108 @@
<!--
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/.
-->
<form [formGroup]="closureSettingsForm" class="layout-row-wrap gap-2px responsive-column">
<h4 class="mat-h4 flex-98">{{ 'labels.heading.For Pre-mature closure' | translate }}</h4>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Maturity Instructions' | translate }}</mat-label>
<mat-select formControlName="maturityInstructionId">
@for (maturityInstruction of maturityInstructionOptions; track maturityInstruction) {
<mat-option [value]="maturityInstruction.id">
{{ maturityInstruction.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
@if (closureSettingsForm.contains('transferToSavingsId')) {
<div class="dal-card flex-98">
<div class="dal-card__header">
<div class="dal-card__header-left">
<div class="dal-card__icon">
<mat-icon>savings</mat-icon>
</div>
<div>
<h3 class="dal-card__title">{{ 'labels.inputs.Transfer to savings account' | translate }}</h3>
<span class="dal-card__sub">Maturity proceeds will be credited to this account</span>
</div>
</div>
</div>
<div class="dal-card__body">
@if (selectedTransferAccount) {
<div class="dal-selected">
<div class="dal-selected__info">
<span class="dal-type-badge dal-type-badge--savings">
<mat-icon>savings</mat-icon>
Savings
</span>
<div class="dal-selected__details">
<span class="dal-selected__accno">{{ selectedTransferAccount.accountNo }}</span>
<span class="dal-selected__product">{{ selectedTransferAccount.savingsProductName }}</span>
</div>
@if (selectedTransferAccount.summary?.accountBalance != null) {
<div class="dal-selected__balance">
<span class="dal-selected__bal-label">Balance</span>
<span class="dal-selected__bal-amount">
{{ selectedTransferAccount.currency?.code }}
{{ selectedTransferAccount.summary.accountBalance | number: '1.2-2' }}
</span>
</div>
}
</div>
<button
mat-stroked-button
color="primary"
type="button"
class="dal-change-btn"
(click)="clearTransferAccount()"
>
<mat-icon>swap_horiz</mat-icon>
Change
</button>
</div>
} @else {
<mat-form-field appearance="outline" class="flex-98">
<mat-label>{{ 'labels.inputs.Transfer to savings account' | translate }}</mat-label>
<mat-select formControlName="transferToSavingsId" required>
@for (account of savingsAccountsData; track account) {
<mat-option [value]="account.id">
({{ account.accountNo }}) {{ account.savingsProductName }}
</mat-option>
}
</mat-select>
<mat-error>Transfer to savings account is required</mat-error>
</mat-form-field>
}
</div>
</div>
}
<mat-checkbox class="flex-98 margin-v" labelPosition="before" formControlName="preClosurePenalApplicable">
{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}
</mat-checkbox>
@if (closureSettingsForm.value.preClosurePenalApplicable) {
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Penal Interest' | translate }} (%)</mat-label>
<input type="number" matInput formControlName="preClosurePenalInterest" min="0" step="0.01" />
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Period' | translate }}</mat-label>
<mat-select formControlName="preClosurePenalInterestOnTypeId">
@for (preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; track preClosurePenalInterestOnType) {
<mat-option [value]="preClosurePenalInterestOnType.id">
{{ preClosurePenalInterestOnType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
}
</form>
@@ -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/.
*/
// ── Transfer-to-Savings account card (mirrors the Deposit Account Link card) ──
.dal-card {
margin-top: 16px;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: #fff;
overflow: hidden;
&__header {
display: flex;
align-items: center;
gap: 14px;
padding: 16px 24px;
border-bottom: 1px solid #f1f5f9;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
}
&__header-left {
display: flex;
align-items: center;
gap: 14px;
}
&__icon {
width: 44px;
height: 44px;
border-radius: 10px;
background: #0ea5e9;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
&__title {
margin: 0;
font-size: 15px;
font-weight: 700;
color: #0c4a6e;
}
&__sub {
display: block;
font-size: 12px;
color: #64748b;
margin-top: 2px;
}
&__body {
padding: 20px 24px;
}
}
.dal-selected {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 20px;
border: 2px solid #0ea5e9;
border-radius: 12px;
background: #f0f9ff;
&__info {
display: flex;
align-items: center;
gap: 14px;
flex: 1;
flex-wrap: wrap;
}
&__details {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
&__accno {
font-size: 15px;
font-weight: 700;
color: #0c4a6e;
font-family: monospace;
}
&__product {
font-size: 12px;
color: #64748b;
}
&__balance {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
&__bal-label {
font-size: 10px;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.5px;
}
&__bal-amount {
font-size: 15px;
font-weight: 700;
color: #0c4a6e;
}
}
.dal-change-btn {
white-space: nowrap;
}
.dal-type-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
background: #f1f5f9;
color: #475569;
border: 1px solid #e2e8f0;
mat-icon {
font-size: 14px;
width: 14px;
height: 14px;
}
&--savings {
background: #eff6ff;
color: #1d4ed8;
border-color: #bfdbfe;
mat-icon {
color: #2563eb;
}
}
}
@@ -0,0 +1,117 @@
/**
* 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/.
*/
/** Angular Imports */
import { Component, Input, OnChanges, SimpleChanges, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, UntypedFormControl, Validators } from '@angular/forms';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatIcon } from '@angular/material/icon';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Fixed Deposit Account Maturity Terms Step -- what happens to this account at maturity
* (Withdraw / Transfer to Savings / Reinvest) and the pre-mature-closure penalty, decided
* per account rather than silently inherited from the product.
*/
@Component({
selector: 'mifosx-fixed-deposit-account-maturity-terms-step',
templateUrl: './fixed-deposit-account-maturity-terms-step.component.html',
styleUrls: ['./fixed-deposit-account-maturity-terms-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox,
MatIcon
]
})
export class FixedDepositAccountMaturityTermsStepComponent implements OnChanges {
private formBuilder = inject(UntypedFormBuilder);
@Input() fixedDepositsAccountTemplate: any;
@Input() fixedDepositsAccountProductTemplate: any;
/** Account already linked elsewhere in the wizard (the Details step's Deposit Account Link) --
* pre-fills Transfer to Savings so the officer doesn't have to pick the same account twice. */
@Input() linkedSavingsAccountId: number | null = null;
closureSettingsForm: UntypedFormGroup;
maturityInstructionOptions: any[] = [];
savingsAccountsData: any[] = [];
preClosurePenalInterestOnTypeData: any[] = [];
constructor() {
this.closureSettingsForm = this.formBuilder.group({
maturityInstructionId: [''],
preClosurePenalApplicable: [false],
preClosurePenalInterest: [
'',
Validators.min(0)
],
preClosurePenalInterestOnTypeId: ['']
});
this.closureSettingsForm.get('maturityInstructionId').valueChanges.subscribe((value: number) => {
// 200 = Transfer to Savings -- Withdraw Deposit (100) needs no target account, and both
// Reinvest options (300/400) open a brand-new FD instead, so they ignore this field too.
if (value === 200) {
this.closureSettingsForm.addControl(
'transferToSavingsId',
new UntypedFormControl(this.linkedSavingsAccountId ?? '', Validators.required)
);
} else {
this.closureSettingsForm.removeControl('transferToSavingsId');
}
});
}
ngOnChanges(changes: SimpleChanges) {
if (changes.linkedSavingsAccountId) {
const control = this.closureSettingsForm.get('transferToSavingsId');
if (control && !control.value && this.linkedSavingsAccountId) {
control.setValue(this.linkedSavingsAccountId);
}
}
if (this.fixedDepositsAccountProductTemplate) {
// Withdraw Deposit (id 100) books the maturity proceeds out of the bank's cash/vault the moment
// the account matures, before the customer has actually collected it -- disabled everywhere.
this.maturityInstructionOptions = (
this.fixedDepositsAccountProductTemplate.maturityInstructionOptions ?? []
).filter((option: any) => option.id !== 100);
this.savingsAccountsData = this.fixedDepositsAccountProductTemplate.savingsAccounts ?? [];
this.preClosurePenalInterestOnTypeData =
this.fixedDepositsAccountProductTemplate.preClosurePenalInterestOnTypeOptions ?? [];
if (!this.closureSettingsForm.get('preClosurePenalApplicable').dirty) {
this.closureSettingsForm.patchValue({
preClosurePenalApplicable: this.fixedDepositsAccountProductTemplate.preClosurePenalApplicable ?? false,
preClosurePenalInterest: this.fixedDepositsAccountProductTemplate.preClosurePenalInterest ?? '',
preClosurePenalInterestOnTypeId:
this.fixedDepositsAccountProductTemplate.preClosurePenalInterestOnType?.id ?? ''
});
}
}
}
/** The linked-savings account for the transfer-to-savings maturity instruction, once picked. */
get selectedTransferAccount(): any {
const id = this.closureSettingsForm.value.transferToSavingsId;
if (!id) return null;
return this.savingsAccountsData.find((account: any) => account.id === id) ?? null;
}
clearTransferAccount(): void {
this.closureSettingsForm.get('transferToSavingsId')?.setValue('');
}
/** Chosen maturity/closure settings, ready to merge into the submit payload. */
get closureSettings() {
const settings = this.closureSettingsForm.getRawValue();
for (const key in settings) {
if (settings[key] === '') {
delete settings[key];
}
}
return settings;
}
}
@@ -683,6 +683,41 @@
</div>
</div>
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-flag-checkered preview-section__icon"></i>
<span class="preview-section__title">Maturity &amp; Pre-mature Closure</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Maturity Instructions' | translate }}</span>
<span class="preview-value">{{ closureMaturityInstructionLabel }}</span>
</div>
@if (closureSettings?.transferToSavingsId) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Transfer to savings account' | translate }}</span>
<span class="preview-value">{{ closureTransferToSavingsLabel }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}</span>
<span class="preview-value">{{ closureSettings?.preClosurePenalApplicable | yesNo }}</span>
</div>
@if (closureSettings?.preClosurePenalApplicable) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Penal Interest' | translate }} (%)</span>
<span class="preview-value">{{ closureSettings?.preClosurePenalInterest }}%</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Period' | translate }}</span>
<span class="preview-value">{{ closurePenalInterestOnTypeLabel }}</span>
</div>
}
</div>
</div>
</div>
@if (!hideButtons) {
<div class="layout-row responsive-column align-center gap-2px margin-t">
<button mat-raised-button matStepperPrevious>
@@ -164,7 +164,26 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges {
/** Form submission event */
@Output() submitEvent = new EventEmitter();
constructor() {}
/** Maturity/pre-mature-closure choices made in the Terms step -- shown here read-only. */
@Input() closureSettings: any = {};
get closureMaturityInstructionLabel(): string {
const options = this.fixedDepositsAccountProductTemplate?.maturityInstructionOptions ?? [];
const match = options.find((o: any) => o.id === this.closureSettings?.maturityInstructionId);
return match?.value ?? '—';
}
get closureTransferToSavingsLabel(): string {
const accounts = this.fixedDepositsAccountProductTemplate?.savingsAccounts ?? [];
const match = accounts.find((a: any) => a.id === this.closureSettings?.transferToSavingsId);
return match ? `(${match.accountNo}) ${match.savingsProductName}` : '—';
}
get closurePenalInterestOnTypeLabel(): string {
const options = this.fixedDepositsAccountProductTemplate?.preClosurePenalInterestOnTypeOptions ?? [];
const match = options.find((o: any) => o.id === this.closureSettings?.preClosurePenalInterestOnTypeId);
return match?.value ?? '—';
}
ngOnChanges() {
if (this.fixedDepositsAccountProductTemplate) {
@@ -79,6 +79,16 @@
<!-- Action menu (kebab) -->
<div class="fd-hero__actions">
@if (!fixedDepositsAccountData.status.rejected && !fixedDepositsAccountData.status.submittedAndPendingApproval) {
<button
mat-icon-button
matTooltip="Print Certificate"
(click)="printCertificate()"
aria-label="Print certificate"
>
<fa-icon icon="print"></fa-icon>
</button>
}
<button mat-icon-button [matMenuTriggerFor]="accountMenu" aria-label="Account actions" yPosition="below">
<fa-icon icon="ellipsis-v"></fa-icon>
</button>
@@ -16,6 +16,7 @@ import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/conf
import { CalculateInterestDialogComponent } from './custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component';
import { PostInterestDialogComponent } from './custom-dialogs/post-interest-dialog/post-interest-dialog.component';
import { ToggleWithholdTaxDialogComponent } from './custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component';
import { DepositCertificateDialogComponent } from 'app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component';
/** Custom Button Config. */
import { FixedDepositsButtonsConfiguration } from './fixed-deposits-buttons.config';
@@ -358,6 +359,20 @@ export class FixedDepositAccountViewComponent implements OnInit {
});
}
/**
* Opens a printable deposit certificate for this account.
*/
printCertificate() {
this.dialog.open(DepositCertificateDialogComponent, {
width: '900px',
data: {
accountType: 'FD',
accountData: this.fixedDepositsAccountData,
clientId: this.fixedDepositsAccountData.clientId
}
});
}
/**
* Disables withhold tax for fixed deposits account
* Fixed deposits endpoint is not supported so using Savings endpoint.
@@ -10,6 +10,7 @@
<mat-card>
<form [formGroup]="approveFixedDepositsAccountForm" (ngSubmit)="submit()">
<mat-card-content>
<p class="mat-body-2">This approves and activates the account in one step -- it goes straight to Active.</p>
<div class="layout-column">
<mat-form-field (click)="approvedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Approved On Date' | translate }}</mat-label>
@@ -81,8 +81,9 @@ export class ApproveFixedDepositsAccountComponent implements OnInit {
}
/**
* Submits the form and approves the fixed deposit account,
* if successful redirects to the fixed deposit account.
* Submits the form, approves the fixed deposit account, then immediately activates it on the
* same date -- Approve and Activate are still two separate Fineract commands underneath (each
* shows up in the audit trail), but the officer only sees and clicks through one step.
*/
submit() {
const approveFixedDepositsAccountFormData = this.approveFixedDepositsAccountForm.value;
@@ -98,7 +99,16 @@ export class ApproveFixedDepositsAccountComponent implements OnInit {
locale
};
this.fixedDepositsService.executeFixedDepositsAccountCommand(this.accountId, 'approve', data).subscribe(() => {
this.router.navigate(['../../'], { relativeTo: this.route });
const activationData = {
activatedOnDate: approveFixedDepositsAccountFormData.approvedOnDate,
dateFormat,
locale
};
this.fixedDepositsService
.executeFixedDepositsAccountCommand(this.accountId, 'activate', activationData)
.subscribe(() => {
this.router.navigate(['../../'], { relativeTo: this.route });
});
});
}
}
@@ -33,6 +33,7 @@
<mifosx-recurring-deposits-account-details-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
(recurringDepositsAccountProductTemplate)="setTemplate($event)"
[externalLinkedAccountId]="recurringDepositsAccountMaturityTermsStep?.selectedTransferAccount?.id ?? null"
>
</mifosx-recurring-deposits-account-details-step>
@@ -111,7 +112,40 @@
</div>
</mat-tab>
<!-- Tab 4: Preview -->
<!-- Tab 4: Terms -->
<mat-tab [disabled]="!recurringDepositsAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="file-alt" class="cra-tab__icon"></fa-icon>
Terms
</ng-template>
<div class="cra-tab-body">
@if (recurringDepositsAccountProductTemplate) {
<mifosx-recurring-deposits-account-maturity-terms-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[linkedSavingsAccountId]="
recurringDepositsAccountDetailsStep?.selectedLinkedAccount?.accountType === 'SAVINGS'
? recurringDepositsAccountDetailsStep?.selectedLinkedAccount?.id
: null
"
></mifosx-recurring-deposits-account-maturity-terms-step>
}
<div class="cra-actions">
<button mat-stroked-button (click)="prev()">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<span class="cra-actions__spacer"></span>
<button mat-raised-button color="primary" (click)="next()">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</div>
</mat-tab>
<!-- Tab 5: Preview -->
<mat-tab [disabled]="!recurringDepositsAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="eye" class="cra-tab__icon"></fa-icon>
@@ -132,6 +166,7 @@
[rdDaysInYear]="rdDaysInYear"
[rdAppliedRate]="rdAppliedRate"
[rdIsSeniorCitizen]="rdIsSeniorCitizen"
[closureSettings]="recurringDepositsAccountMaturityTermsStep?.closureSettings ?? {}"
[hideButtons]="true"
(submitEvent)="submit()"
>
@@ -24,6 +24,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { RecurringDepositsAccountCurrencyStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component';
import { RecurringDepositsAccountInterestRateChartStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component';
import { RecurringDepositsAccountPreviewStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component';
import { RecurringDepositsAccountMaturityTermsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -40,6 +41,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
RecurringDepositsAccountDetailsStepComponent,
RecurringDepositsAccountCurrencyStepComponent,
RecurringDepositsAccountInterestRateChartStepComponent,
RecurringDepositsAccountMaturityTermsStepComponent,
RecurringDepositsAccountPreviewStepComponent
]
})
@@ -61,6 +63,12 @@ export class CreateRecurringDepositsAccountComponent {
@ViewChild(RecurringDepositsAccountInterestRateChartStepComponent)
recurringDepositsAccountChartStep: RecurringDepositsAccountInterestRateChartStepComponent;
@ViewChild(RecurringDepositsAccountPreviewStepComponent)
recurringDepositsAccountPreviewStep: RecurringDepositsAccountPreviewStepComponent;
@ViewChild(RecurringDepositsAccountMaturityTermsStepComponent)
recurringDepositsAccountMaturityTermsStep: RecurringDepositsAccountMaturityTermsStepComponent;
/** Recurring Deposits Account Template */
recurringDepositsAccountTemplate: any;
/** Recurring Deposit Account Product Template */
@@ -68,7 +76,7 @@ export class CreateRecurringDepositsAccountComponent {
/** Currently visible wizard tab (0-indexed). */
selectedTabIndex = 0;
/** Total wizard tabs — Product, Currency, Chart, Preview. */
private readonly totalTabs = 4;
private readonly totalTabs = 5;
/** Active CBS base interest rate versions */
cbsBaseRates: any[] = [];
/** Selected tenure value and frequency from calculator */
@@ -258,6 +266,7 @@ export class CreateRecurringDepositsAccountComponent {
...details,
...terms,
...settings,
...(this.recurringDepositsAccountMaturityTermsStep?.closureSettings ?? {}),
...productDefaults,
clientId: this.recurringDepositsAccountTemplate.clientId,
mandatoryRecommendedDepositAmount: pt?.minDepositAmount ?? 0,
@@ -35,6 +35,7 @@
<mifosx-recurring-deposits-account-details-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
(recurringDepositsAccountProductTemplate)="setTemplate($event)"
[externalLinkedAccountId]="recurringDepositsAccountMaturityTermsStep?.selectedTransferAccount?.id ?? null"
>
</mifosx-recurring-deposits-account-details-step>
@@ -115,7 +116,40 @@
</div>
</mat-tab>
<!-- Tab 4: Preview -->
<!-- Tab 4: Terms -->
<mat-tab [disabled]="!recurringDepositsAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="file-alt" class="cra-tab__icon"></fa-icon>
Terms
</ng-template>
<div class="cra-tab-body">
@if (recurringDepositsAccountProductTemplate) {
<mifosx-recurring-deposits-account-maturity-terms-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[linkedSavingsAccountId]="
recurringDepositsAccountDetailsStep?.selectedLinkedAccount?.accountType === 'SAVINGS'
? recurringDepositsAccountDetailsStep?.selectedLinkedAccount?.id
: null
"
></mifosx-recurring-deposits-account-maturity-terms-step>
}
<div class="cra-actions">
<button mat-stroked-button (click)="prev()">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<span class="cra-actions__spacer"></span>
<button mat-raised-button color="primary" (click)="next()">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</div>
</mat-tab>
<!-- Tab 5: Preview -->
<mat-tab [disabled]="!recurringDepositsAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="eye" class="cra-tab__icon"></fa-icon>
@@ -136,6 +170,7 @@
[rdDaysInYear]="rdDaysInYear"
[rdAppliedRate]="rdAppliedRate"
[rdIsSeniorCitizen]="rdIsSeniorCitizen"
[closureSettings]="recurringDepositsAccountMaturityTermsStep?.closureSettings ?? {}"
[hideButtons]="true"
(submitEvent)="submit()"
>
@@ -25,6 +25,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { RecurringDepositsAccountCurrencyStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component';
import { RecurringDepositsAccountInterestRateChartStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component';
import { RecurringDepositsAccountPreviewStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component';
import { RecurringDepositsAccountMaturityTermsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -41,6 +42,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
RecurringDepositsAccountDetailsStepComponent,
RecurringDepositsAccountCurrencyStepComponent,
RecurringDepositsAccountInterestRateChartStepComponent,
RecurringDepositsAccountMaturityTermsStepComponent,
RecurringDepositsAccountPreviewStepComponent
]
})
@@ -63,13 +65,19 @@ export class EditRecurringDepositAccountComponent {
@ViewChild(RecurringDepositsAccountInterestRateChartStepComponent)
recurringDepositsAccountChartStep: RecurringDepositsAccountInterestRateChartStepComponent;
@ViewChild(RecurringDepositsAccountPreviewStepComponent)
recurringDepositsAccountPreviewStep: RecurringDepositsAccountPreviewStepComponent;
@ViewChild(RecurringDepositsAccountMaturityTermsStepComponent)
recurringDepositsAccountMaturityTermsStep: RecurringDepositsAccountMaturityTermsStepComponent;
/** Full account data + template from resolver */
recurringDepositsAccountAndTemplate: any;
/** Product template emitted by details step */
recurringDepositsAccountProductTemplate: any;
/** Currently visible wizard tab (0-indexed). */
selectedTabIndex = 0;
private readonly totalTabs = 4;
private readonly totalTabs = 5;
/** Active CBS base interest rate versions */
cbsBaseRates: any[] = [];
/** Selected tenure value and frequency from calculator */
@@ -277,6 +285,7 @@ export class EditRecurringDepositAccountComponent {
...details,
...terms,
...settings,
...(this.recurringDepositsAccountMaturityTermsStep?.closureSettings ?? {}),
...productDefaults,
clientId: this.recurringDepositsAccountAndTemplate.clientId,
mandatoryRecommendedDepositAmount: pt?.minDepositAmount ?? 0,
@@ -10,6 +10,7 @@
<mat-card>
<form [formGroup]="approveRecurringDepositsAccountForm" (ngSubmit)="submit()">
<mat-card-content>
<p class="mat-body-2">This approves and activates the account in one step -- it goes straight to Active.</p>
<div class="layout-column">
<mat-form-field (click)="approvedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Approved On Date' | translate }}</mat-label>
@@ -81,8 +81,9 @@ export class ApproveRecurringDepositsAccountComponent implements OnInit {
}
/**
* Submits the form and approves the recurring deposit account,
* if successful redirects to the recurring deposit account.
* Submits the form, approves the recurring deposit account, then immediately activates it on
* the same date -- Approve and Activate are still two separate Fineract commands underneath
* (each shows up in the audit trail), but the officer only sees and clicks through one step.
*/
submit() {
const approveRecurringDepositsAccountFormData = this.approveRecurringDepositsAccountForm.value;
@@ -103,7 +104,16 @@ export class ApproveRecurringDepositsAccountComponent implements OnInit {
this.recurringDepositsService
.executeRecurringDepositsAccountCommand(this.accountId, 'approve', data)
.subscribe(() => {
this.router.navigate(['../../'], { relativeTo: this.route });
const activationData = {
activatedOnDate: approveRecurringDepositsAccountFormData.approvedOnDate,
dateFormat,
locale
};
this.recurringDepositsService
.executeRecurringDepositsAccountCommand(this.accountId, 'activate', activationData)
.subscribe(() => {
this.router.navigate(['../../'], { relativeTo: this.route });
});
});
}
}
@@ -7,7 +7,17 @@
*/
/** Angular Imports */
import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core';
import {
Component,
OnInit,
OnChanges,
SimpleChanges,
Input,
Output,
EventEmitter,
inject,
ChangeDetectorRef
} from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, FormsModule } from '@angular/forms';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
@@ -43,7 +53,7 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc
IntroducerSearchComponent
]
})
export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
export class RecurringDepositsAccountDetailsStepComponent implements OnInit, OnChanges {
private formBuilder = inject(UntypedFormBuilder);
private recurringDepositsService = inject(RecurringDepositsService);
private settingsService = inject(SettingsService);
@@ -56,6 +66,10 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
/** Recurring Deposits Account Template */
@Input() recurringDepositsAccountTemplate: any;
/** Linked-account id picked elsewhere in the wizard (e.g. the Terms step's Transfer to Savings
* account) -- pre-fills this card so the officer doesn't have to pick the same account twice. */
@Input() externalLinkedAccountId: number | null = null;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
@@ -198,6 +212,23 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
this.dalSearch = '';
}
ngOnChanges(changes: SimpleChanges): void {
if (!changes.externalLinkedAccountId || this.selectedLinkedAccount) {
return;
}
const id = this.externalLinkedAccountId;
if (!id) return;
const savings = this.dalAllSavings.find((a: any) => a.id === id);
const current = this.dalAllCurrent.find((a: any) => a.id === id);
if (savings) {
this.dalSelectedType = 'SAVINGS';
this.selectedLinkedAccount = savings;
} else if (current) {
this.dalSelectedType = 'CURRENT';
this.selectedLinkedAccount = current;
}
}
private loadClientAccounts(): void {
const clientId = String(this.recurringDepositsAccountTemplate?.clientId);
if (!clientId) return;
@@ -0,0 +1,108 @@
<!--
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/.
-->
<form [formGroup]="closureSettingsForm" class="layout-row-wrap gap-2px responsive-column">
<h4 class="mat-h4 flex-98">{{ 'labels.heading.For Pre-mature closure' | translate }}</h4>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Maturity Instructions' | translate }}</mat-label>
<mat-select formControlName="maturityInstructionId">
@for (maturityInstruction of maturityInstructionOptions; track maturityInstruction) {
<mat-option [value]="maturityInstruction.id">
{{ maturityInstruction.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
@if (closureSettingsForm.contains('transferToSavingsId')) {
<div class="dal-card flex-98">
<div class="dal-card__header">
<div class="dal-card__header-left">
<div class="dal-card__icon">
<mat-icon>savings</mat-icon>
</div>
<div>
<h3 class="dal-card__title">{{ 'labels.inputs.Transfer to savings account' | translate }}</h3>
<span class="dal-card__sub">Maturity proceeds will be credited to this account</span>
</div>
</div>
</div>
<div class="dal-card__body">
@if (selectedTransferAccount) {
<div class="dal-selected">
<div class="dal-selected__info">
<span class="dal-type-badge dal-type-badge--savings">
<mat-icon>savings</mat-icon>
Savings
</span>
<div class="dal-selected__details">
<span class="dal-selected__accno">{{ selectedTransferAccount.accountNo }}</span>
<span class="dal-selected__product">{{ selectedTransferAccount.savingsProductName }}</span>
</div>
@if (selectedTransferAccount.summary?.accountBalance != null) {
<div class="dal-selected__balance">
<span class="dal-selected__bal-label">Balance</span>
<span class="dal-selected__bal-amount">
{{ selectedTransferAccount.currency?.code }}
{{ selectedTransferAccount.summary.accountBalance | number: '1.2-2' }}
</span>
</div>
}
</div>
<button
mat-stroked-button
color="primary"
type="button"
class="dal-change-btn"
(click)="clearTransferAccount()"
>
<mat-icon>swap_horiz</mat-icon>
Change
</button>
</div>
} @else {
<mat-form-field appearance="outline" class="flex-98">
<mat-label>{{ 'labels.inputs.Transfer to savings account' | translate }}</mat-label>
<mat-select formControlName="transferToSavingsId" required>
@for (account of savingsAccountsData; track account) {
<mat-option [value]="account.id">
({{ account.accountNo }}) {{ account.savingsProductName }}
</mat-option>
}
</mat-select>
<mat-error>Transfer to savings account is required</mat-error>
</mat-form-field>
}
</div>
</div>
}
<mat-checkbox class="flex-98 margin-v" labelPosition="before" formControlName="preClosurePenalApplicable">
{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}
</mat-checkbox>
@if (closureSettingsForm.value.preClosurePenalApplicable) {
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Penal Interest' | translate }} (%)</mat-label>
<input type="number" matInput formControlName="preClosurePenalInterest" min="0" step="0.01" />
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Period' | translate }}</mat-label>
<mat-select formControlName="preClosurePenalInterestOnTypeId">
@for (preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; track preClosurePenalInterestOnType) {
<mat-option [value]="preClosurePenalInterestOnType.id">
{{ preClosurePenalInterestOnType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
}
</form>
@@ -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/.
*/
// ── Transfer-to-Savings account card (mirrors the Deposit Account Link card) ──
.dal-card {
margin-top: 16px;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: #fff;
overflow: hidden;
&__header {
display: flex;
align-items: center;
gap: 14px;
padding: 16px 24px;
border-bottom: 1px solid #f1f5f9;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
}
&__header-left {
display: flex;
align-items: center;
gap: 14px;
}
&__icon {
width: 44px;
height: 44px;
border-radius: 10px;
background: #0ea5e9;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
&__title {
margin: 0;
font-size: 15px;
font-weight: 700;
color: #0c4a6e;
}
&__sub {
display: block;
font-size: 12px;
color: #64748b;
margin-top: 2px;
}
&__body {
padding: 20px 24px;
}
}
.dal-selected {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 20px;
border: 2px solid #0ea5e9;
border-radius: 12px;
background: #f0f9ff;
&__info {
display: flex;
align-items: center;
gap: 14px;
flex: 1;
flex-wrap: wrap;
}
&__details {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
&__accno {
font-size: 15px;
font-weight: 700;
color: #0c4a6e;
font-family: monospace;
}
&__product {
font-size: 12px;
color: #64748b;
}
&__balance {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
&__bal-label {
font-size: 10px;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.5px;
}
&__bal-amount {
font-size: 15px;
font-weight: 700;
color: #0c4a6e;
}
}
.dal-change-btn {
white-space: nowrap;
}
.dal-type-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
background: #f1f5f9;
color: #475569;
border: 1px solid #e2e8f0;
mat-icon {
font-size: 14px;
width: 14px;
height: 14px;
}
&--savings {
background: #eff6ff;
color: #1d4ed8;
border-color: #bfdbfe;
mat-icon {
color: #2563eb;
}
}
}
@@ -0,0 +1,116 @@
/**
* 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/.
*/
/** Angular Imports */
import { Component, Input, OnChanges, SimpleChanges, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, UntypedFormControl, Validators } from '@angular/forms';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatIcon } from '@angular/material/icon';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Recurring Deposit Account Maturity Terms Step -- what happens to this account at maturity
* (Withdraw / Transfer to Savings / Reinvest) and the pre-mature-closure penalty, decided
* per account rather than silently inherited from the product.
*/
@Component({
selector: 'mifosx-recurring-deposits-account-maturity-terms-step',
templateUrl: './recurring-deposits-account-maturity-terms-step.component.html',
styleUrls: ['./recurring-deposits-account-maturity-terms-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox,
MatIcon
]
})
export class RecurringDepositsAccountMaturityTermsStepComponent implements OnChanges {
private formBuilder = inject(UntypedFormBuilder);
@Input() recurringDepositsAccountTemplate: any;
@Input() recurringDepositsAccountProductTemplate: any;
/** Account already linked elsewhere in the wizard (the Details step's Deposit Account Link) --
* pre-fills Transfer to Savings so the officer doesn't have to pick the same account twice. */
@Input() linkedSavingsAccountId: number | null = null;
closureSettingsForm: UntypedFormGroup;
maturityInstructionOptions: any[] = [];
savingsAccountsData: any[] = [];
preClosurePenalInterestOnTypeData: any[] = [];
constructor() {
this.closureSettingsForm = this.formBuilder.group({
maturityInstructionId: [''],
preClosurePenalApplicable: [false],
preClosurePenalInterest: [
'',
Validators.min(0)
],
preClosurePenalInterestOnTypeId: ['']
});
this.closureSettingsForm.get('maturityInstructionId').valueChanges.subscribe((value: number) => {
// 200 = Transfer to Savings -- Withdraw Deposit (100) needs no target account, and both
// Reinvest options (300/400) open a brand-new RD instead, so they ignore this field too.
if (value === 200) {
this.closureSettingsForm.addControl(
'transferToSavingsId',
new UntypedFormControl(this.linkedSavingsAccountId ?? '', Validators.required)
);
} else {
this.closureSettingsForm.removeControl('transferToSavingsId');
}
});
}
ngOnChanges(changes: SimpleChanges) {
if (changes.linkedSavingsAccountId) {
const control = this.closureSettingsForm.get('transferToSavingsId');
if (control && !control.value && this.linkedSavingsAccountId) {
control.setValue(this.linkedSavingsAccountId);
}
}
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
if (template) {
// Withdraw Deposit (id 100) books the maturity proceeds out of the bank's cash/vault the moment
// the account matures, before the customer has actually collected it -- disabled everywhere.
this.maturityInstructionOptions = (template.maturityInstructionOptions ?? []).filter(
(option: any) => option.id !== 100
);
this.savingsAccountsData = template.savingsAccounts ?? [];
this.preClosurePenalInterestOnTypeData = template.preClosurePenalInterestOnTypeOptions ?? [];
if (!this.closureSettingsForm.get('preClosurePenalApplicable').dirty) {
this.closureSettingsForm.patchValue({
preClosurePenalApplicable: template.preClosurePenalApplicable ?? false,
preClosurePenalInterest: template.preClosurePenalInterest ?? '',
preClosurePenalInterestOnTypeId: template.preClosurePenalInterestOnType?.id ?? ''
});
}
}
}
/** The linked-savings account for the transfer-to-savings maturity instruction, once picked. */
get selectedTransferAccount(): any {
const id = this.closureSettingsForm.value.transferToSavingsId;
if (!id) return null;
return this.savingsAccountsData.find((account: any) => account.id === id) ?? null;
}
clearTransferAccount(): void {
this.closureSettingsForm.get('transferToSavingsId')?.setValue('');
}
/** Chosen maturity/closure settings, ready to merge into the submit payload. */
get closureSettings() {
const settings = this.closureSettingsForm.getRawValue();
for (const key in settings) {
if (settings[key] === '') {
delete settings[key];
}
}
return settings;
}
}
@@ -853,6 +853,41 @@
}
</div>
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-flag-checkered preview-section__icon"></i>
<span class="preview-section__title">Maturity &amp; Pre-mature Closure</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Maturity Instructions' | translate }}</span>
<span class="preview-value">{{ closureMaturityInstructionLabel }}</span>
</div>
@if (closureSettings?.transferToSavingsId) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Transfer to savings account' | translate }}</span>
<span class="preview-value">{{ closureTransferToSavingsLabel }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}</span>
<span class="preview-value">{{ closureSettings?.preClosurePenalApplicable | yesNo }}</span>
</div>
@if (closureSettings?.preClosurePenalApplicable) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Penal Interest' | translate }} (%)</span>
<span class="preview-value">{{ closureSettings?.preClosurePenalInterest }}%</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Period' | translate }}</span>
<span class="preview-value">{{ closurePenalInterestOnTypeLabel }}</span>
</div>
}
</div>
</div>
</div>
@if (!hideButtons) {
<div class="layout-row responsive-column align-center gap-2px margin-t">
<button mat-raised-button matStepperPrevious>
@@ -201,7 +201,29 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges {
/** Expand Chart Slab Index used in the view */
expandChartSlabIndex: number;
constructor() {}
/** Maturity/pre-mature-closure choices made in the Terms step -- shown here read-only. */
@Input() closureSettings: any = {};
get closureMaturityInstructionLabel(): string {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
const options = template?.maturityInstructionOptions ?? [];
const match = options.find((o: any) => o.id === this.closureSettings?.maturityInstructionId);
return match?.value ?? '—';
}
get closureTransferToSavingsLabel(): string {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
const accounts = template?.savingsAccounts ?? [];
const match = accounts.find((a: any) => a.id === this.closureSettings?.transferToSavingsId);
return match ? `(${match.accountNo}) ${match.savingsProductName}` : '—';
}
get closurePenalInterestOnTypeLabel(): string {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
const options = template?.preClosurePenalInterestOnTypeOptions ?? [];
const match = options.find((o: any) => o.id === this.closureSettings?.preClosurePenalInterestOnTypeId);
return match?.value ?? '—';
}
ngOnChanges() {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
@@ -17,6 +17,7 @@ import {
} from '@angular/forms';
import { SettingsService } from 'app/settings/settings.service';
import { Currency } from 'app/shared/models/general.model';
import { OptionData } from 'app/shared/models/option-data.model';
import { MatCheckbox } from '@angular/material/checkbox';
import { InputAmountComponent } from '../../../../shared/input-amount/input-amount.component';
import { MatDivider } from '@angular/material/divider';
@@ -61,6 +62,10 @@ export class RecurringDepositsAccountSettingsStepComponent implements OnInit, On
periodFrequencyTypeData: any[] = [];
/** Preclosure Penal Interest Type on Data */
preClosurePenalInterestOnTypeData: any[] = [];
/** Options for what happens to the account at maturity */
maturityInstructionOptions: OptionData[];
/** Savings accounts eligible as a maturity-proceeds transfer target */
savingsAccountsData: any;
/** Tax Group */
taxGroup: any;
currency: Currency | null = null;
@@ -91,6 +96,7 @@ export class RecurringDepositsAccountSettingsStepComponent implements OnInit, On
: '',
maturityInstructions: recurringDepositsAccount.maturityInstructions ?? false,
creditMaturityProceeds: recurringDepositsAccount.creditMaturityProceeds ?? false,
maturityInstructionId: recurringDepositsAccount.maturityInstructionId ?? '',
penaltyGracePeriodMissedInstallments: recurringDepositsAccount.penaltyGracePeriodMissedInstallments ?? false,
panelRequestApplicable: recurringDepositsAccount.panelRequestApplicable ?? false,
noPenaltyNomineeHeirClosure: recurringDepositsAccount.noPenaltyNomineeHeirClosure ?? false,
@@ -182,6 +188,7 @@ export class RecurringDepositsAccountSettingsStepComponent implements OnInit, On
lockinPeriodFrequencyType: [''],
maturityInstructions: [false],
creditMaturityProceeds: [false],
maturityInstructionId: [''],
penaltyGracePeriodMissedInstallments: [false],
panelRequestApplicable: [false],
noPenaltyNomineeHeirClosure: [false],
@@ -217,6 +224,8 @@ export class RecurringDepositsAccountSettingsStepComponent implements OnInit, On
this.lockinPeriodFrequencyTypeData = recurringDepositsAccount.lockinPeriodFrequencyTypeOptions;
this.periodFrequencyTypeData = recurringDepositsAccount.periodFrequencyTypeOptions;
this.preClosurePenalInterestOnTypeData = recurringDepositsAccount.preClosurePenalInterestOnTypeOptions;
this.maturityInstructionOptions = recurringDepositsAccount.maturityInstructionOptions;
this.savingsAccountsData = recurringDepositsAccount.savingsAccounts;
}
buildDependencies() {
@@ -233,6 +242,16 @@ export class RecurringDepositsAccountSettingsStepComponent implements OnInit, On
this.recurringDepositAccountSettingsForm.addControl('recurringFrequencyType', new UntypedFormControl(''));
}
});
this.recurringDepositAccountSettingsForm.get('maturityInstructionId').valueChanges.subscribe((value: number) => {
if (value > 100) {
this.recurringDepositAccountSettingsForm.addControl(
'transferToSavingsId',
new UntypedFormControl('', Validators.required)
);
} else {
this.recurringDepositAccountSettingsForm.removeControl('transferToSavingsId');
}
});
}
get recurringDepositAccountSettings() {
@@ -87,6 +87,19 @@
<!-- Action menu (kebab) -->
<div class="rd-hero__actions">
@if (
!recurringDepositsAccountData.status.rejected &&
!recurringDepositsAccountData.status.submittedAndPendingApproval
) {
<button
mat-icon-button
matTooltip="Print Certificate"
(click)="printCertificate()"
aria-label="Print certificate"
>
<fa-icon icon="print"></fa-icon>
</button>
}
<button mat-icon-button [matMenuTriggerFor]="accountMenu" aria-label="Account actions" yPosition="below">
<fa-icon icon="ellipsis-v"></fa-icon>
</button>
@@ -22,6 +22,7 @@ import { RecurringDepositsButtonsConfiguration } from './recurring-deposits-butt
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
import { RecurringDepositConfirmationDialogComponent } from './custom-dialogs/recurring-deposit-confirmation-dialog/recurring-deposit-confirmation-dialog.component';
import { DepositCertificateDialogComponent } from 'app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component';
import { Currency } from 'app/shared/models/general.model';
import { TranslateService } from '@ngx-translate/core';
import { AccountsService } from 'app/accounts/accounts.service';
@@ -426,6 +427,20 @@ export class RecurringDepositsAccountViewComponent implements OnInit {
});
}
/**
* Opens a printable deposit certificate for this account.
*/
printCertificate() {
this.dialog.open(DepositCertificateDialogComponent, {
width: '900px',
data: {
accountType: 'RD',
accountData: this.recurringDepositsAccountData,
clientId: this.recurringDepositsAccountData.clientId
}
});
}
/**
* Disables withhold tax for recurring deposits account
* Recurring deposits endpoint is not supported so using Savings endpoint.
@@ -21,6 +21,33 @@
<mat-checkbox formControlName="lienMarked">Lien Marked</mat-checkbox>
<mat-checkbox formControlName="withdrawalFeeForTransfers">Apply Withdrawal Fee for Transfers</mat-checkbox>
</div>
<mat-checkbox class="fdps-preclosure-toggle" formControlName="preClosurePenalApplicable">
{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}
</mat-checkbox>
@if (fixedDepositProductSettingsForm.value.preClosurePenalApplicable) {
<div class="fdps-lockin-fields">
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Penal Interest' | translate }} (%)</mat-label>
<input type="number" matInput formControlName="preClosurePenalInterest" min="0" step="0.01" />
</mat-form-field>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Period' | translate }}</mat-label>
<mat-select formControlName="preClosurePenalInterestOnTypeId">
@for (
preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData;
track preClosurePenalInterestOnType
) {
<mat-option [value]="preClosurePenalInterestOnType.id">
{{ preClosurePenalInterestOnType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
</div>
</div>
@@ -130,6 +130,15 @@ $gray-900: #202124;
}
}
.fdps-preclosure-toggle {
display: block;
margin: 4px 0 16px;
padding-bottom: 16px;
border-bottom: 1px solid $gray-200;
font-size: 14px;
color: $gray-900;
}
.fdps-layout {
display: grid;
gap: 16px;
@@ -30,6 +30,7 @@ export class FixedDepositProductSettingsStepComponent implements OnInit {
fixedDepositProductSettingsForm: UntypedFormGroup;
lockinPeriodFrequencyTypeData: any;
preClosurePenalInterestOnTypeData: any;
constructor() {
this.createFixedDepositProductSettingsForm();
@@ -37,6 +38,7 @@ export class FixedDepositProductSettingsStepComponent implements OnInit {
ngOnInit() {
this.lockinPeriodFrequencyTypeData = this.fixedDepositProductsTemplate.lockinPeriodFrequencyTypeOptions;
this.preClosurePenalInterestOnTypeData = this.fixedDepositProductsTemplate.preClosurePenalInterestOnTypeOptions;
if (this.fixedDepositProductsTemplate?.id) {
this.fixedDepositProductSettingsForm.patchValue({
@@ -48,6 +50,9 @@ export class FixedDepositProductSettingsStepComponent implements OnInit {
enableLockinPeriod: this.fixedDepositProductsTemplate.enableLockinPeriod ?? false,
lockinPeriodFrequency: this.fixedDepositProductsTemplate.lockinPeriodFrequency ?? '',
lockinPeriodFrequencyType: this.fixedDepositProductsTemplate.lockinPeriodFrequencyType?.id ?? '',
preClosurePenalApplicable: this.fixedDepositProductsTemplate.preClosurePenalApplicable ?? false,
preClosurePenalInterest: this.fixedDepositProductsTemplate.preClosurePenalInterest ?? '',
preClosurePenalInterestOnTypeId: this.fixedDepositProductsTemplate.preClosurePenalInterestOnType?.id ?? '',
maturityInstructions: this.fixedDepositProductsTemplate.maturityInstructions ?? false,
creditMaturityProceeds: this.fixedDepositProductsTemplate.creditMaturityProceeds ?? false,
penaltyGracePeriodMissedInstallments:
@@ -76,6 +81,12 @@ export class FixedDepositProductSettingsStepComponent implements OnInit {
Validators.min(0)
],
lockinPeriodFrequencyType: [''],
preClosurePenalApplicable: [false],
preClosurePenalInterest: [
'',
Validators.min(0)
],
preClosurePenalInterestOnTypeId: [''],
maturityInstructions: [false],
creditMaturityProceeds: [false],
penaltyGracePeriodMissedInstallments: [false],
@@ -33,6 +33,9 @@
<span class="flex-60">{{ loanProduct.description }}</span>
</div>
}
<ng-container
*ngTemplateOutlet="legacySections; context: { sections: customFieldSectionsByStep.details }"
></ng-container>
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Currency' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
@@ -298,6 +301,10 @@
<span class="flex-60">{{ loanProduct.minimumDaysBetweenDisbursalAndFirstRepayment | formatNumber }}</span>
</div>
}
<ng-container
*ngTemplateOutlet="legacySections; context: { sections: customFieldSectionsByStep.terms }"
></ng-container>
<ng-container *ngTemplateOutlet="legacyInterestRateDetail"></ng-container>
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Settings' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
@if (loanProduct.amortizationType?.value) {
@@ -741,6 +748,9 @@
</div>
</div>
}
<ng-container
*ngTemplateOutlet="legacySections; context: { sections: customFieldSectionsByStep.settings }"
></ng-container>
@if (loanProduct.charges.length) {
<div class="flex-fill layout-row-wrap responsive-column">
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Charges' | translate }}</h3>
@@ -813,6 +823,9 @@
</table>
</div>
}
<ng-container
*ngTemplateOutlet="legacySections; context: { sections: customFieldSectionsByStep.charges }"
></ng-container>
@if (isAdvancedPaymentAllocation) {
<div class="flex-100 layout-row-wrap responsive-column">
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Income capitalization' | translate }}</h3>
@@ -1271,5 +1284,73 @@
</div>
</div>
}
<ng-container
*ngTemplateOutlet="legacySections; context: { sections: customFieldSectionsByStep.accounting }"
></ng-container>
</div>
}
<!--
Legacy "Account Type Creation" tables. Rendered once here and pulled into each tab's own summary
above via ngTemplateOutlet, so a section sits with the tab it was entered on rather than in a
separate pile at the end of the preview.
-->
<ng-template #legacySections let-sections="sections">
@for (section of sections; track section.title) {
<div class="flex-100 layout-row-wrap responsive-column custom-field-section">
<h4 class="mat-h4 flex-100">{{ section.title }}</h4>
<table class="mat-elevation-z1 flex-100" mat-table [dataSource]="section.rows">
<ng-container matColumnDef="label">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Field' | translate }}</th>
<td mat-cell *matCellDef="let row">{{ row.label }}</td>
</ng-container>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Value' | translate }}</th>
<td mat-cell *matCellDef="let row">
@switch (row.kind) {
@case ('yesNo') {
{{ row.yesNo | yesNo }}
}
@case ('date') {
{{ row.date | dateFormat }}
}
@case ('number') {
{{ row.number | formatNumber }}
}
@default {
{{ row.text }}
}
}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="customFieldDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: customFieldDisplayedColumns"></tr>
</table>
</div>
}
</ng-template>
<!-- The legacy Interest Rate Detail grid, shown under Terms since that is where it is filled in. -->
<ng-template #legacyInterestRateDetail>
@if (interestRateDetailRows.length > 0) {
<div class="flex-100 layout-row-wrap responsive-column">
<h4 class="mat-h4 flex-100">{{ 'labels.heading.Interest Rate Detail' | translate }}</h4>
<table class="mat-elevation-z1 flex-100" mat-table [dataSource]="interestRateDetailRows">
@for (column of interestRateDetailColumns; track column.key) {
<ng-container [matColumnDef]="column.key">
<th mat-header-cell *matHeaderCellDef>{{ column.label }}</th>
<td mat-cell *matCellDef="let row">
@if (column.key === 'calculation') {
{{ row.calculation | yesNo }}
} @else {
{{ row[column.key] }}
}
</td>
</ng-container>
}
<tr mat-header-row *matHeaderRowDef="interestRateDetailDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: interestRateDetailDisplayedColumns"></tr>
</table>
</div>
}
</ng-template>
@@ -56,3 +56,13 @@ mat-divider {
.repayment-strategy {
color: $status-approved;
}
// Legacy "Account Type Creation" sections. Each is a two-column field/value table, and the widest
// of them (Interest Related Parameters) is long, so the label column is pinned to keep the value
// column from drifting between one section and the next.
.custom-field-section {
th:first-of-type,
td:first-of-type {
width: 45%;
}
}
@@ -26,6 +26,15 @@ import {
PaymentAllocation
} from '../../loan-product-stepper/loan-product-payment-strategy-step/payment-allocation-model';
import { LoanProducts } from '../../loan-products';
import {
CUSTOM_FIELD_LABELS,
CUSTOM_FIELD_SECTIONS,
CUSTOM_GL_ACCOUNT_FIELDS,
CustomFieldStep,
CustomGlAccountKind,
INTEREST_RATE_DETAIL_COLUMNS,
isCustomSectionApplicable
} from '../../loan-product-custom-fields';
import { CodeName, OptionData, StringEnumOptionData } from '../../../../shared/models/option-data.model';
import { Accounting } from 'app/core/utils/accounting';
import { DecimalPipe } from '@angular/common';
@@ -51,6 +60,38 @@ import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
import { YesnoPipe } from '../../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* One legacy field, ready to render. `kind` picks which pipe the template applies, so formatting
* (translation, dates, numbers) stays in the template rather than being baked into a string here.
*/
export interface CustomFieldRow {
label: string;
kind: 'text' | 'yesNo' | 'date' | 'number';
text?: string;
yesNo?: boolean;
date?: Date;
number?: number;
}
export interface CustomFieldSectionView {
title: string;
rows: CustomFieldRow[];
}
/** Legacy sections keyed by the tab they belong under, so each renders inside that tab's summary. */
export type CustomFieldSectionsByStep = Record<CustomFieldStep, CustomFieldSectionView[]>;
/** A row of the legacy Interest Rate Detail grid. */
export interface InterestRateDetailRow {
interestType: string;
calculation: boolean;
normalMinRate: number | null;
normalMaxRate: number | null;
specialMinRate: number | null;
specialMaxRate: number | null;
period: string;
}
@Component({
selector: 'mifosx-loan-product-summary',
templateUrl: './loan-product-summary.component.html',
@@ -113,6 +154,12 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
'chargeOffReasonCodeValueId',
'expenseAccountId'
];
customFieldDisplayedColumns: string[] = [
'label',
'value'
];
readonly interestRateDetailColumns = INTEREST_RATE_DETAIL_COLUMNS;
readonly interestRateDetailDisplayedColumns: string[] = INTEREST_RATE_DETAIL_COLUMNS.map((column) => column.key);
accountingRuleData: string[] = [];
isAdvancedPaymentAllocation = false;
@@ -128,6 +175,15 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
capitalizedIncomeClassificationToIncomeAccountMappings: ClassificationToIncomeAccountMapping[] = [];
writeOffReasonsToExpenseMappings: ChargeOffReasonToExpenseAccountMapping[] = [];
/** Legacy "Account Type Creation" sections, keyed by the tab each belongs under. */
customFieldSectionsByStep: CustomFieldSectionsByStep = LoanProductSummaryComponent.emptySectionsByStep();
/** The legacy Interest Rate Detail grid, shown as its own table since its value is a row array. */
interestRateDetailRows: InterestRateDetailRow[] = [];
private static emptySectionsByStep(): CustomFieldSectionsByStep {
return { details: [], settings: [], terms: [], charges: [], accounting: [] };
}
ngOnInit() {
this.accountingRuleData = this.accounting.getAccountingRulesForLoans();
this.setCurrentValues();
@@ -518,6 +574,8 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
this.loanProductsTemplate.advancedPaymentAllocationFutureInstallmentAllocationRules
};
}
this.setCustomFieldValues();
}
optionDataLookUp(itemId: any, optionsData: any[]): OptionData {
@@ -658,4 +716,119 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
mapHumanReadableValueStringEnumOptionDataList(incomingParameter: StringEnumOptionData[]): string[] {
return incomingParameter.map((v) => v.value);
}
/**
* Builds the legacy "Account Type Creation" tables, which only exist on the create/edit forms
* (see CUSTOM_FIELD_SECTIONS) — reviewed here because `LoanProducts.buildPayload()` drops them
* from the submit payload before it reaches Fineract.
*
* Sections are keyed by their originating tab so each renders inside that tab's own summary. A
* "Cheque Book Settings" table is only reviewable next to the rest of Settings; collected into
* one block at the bottom, it reads as an unrelated pile.
*
* Only genuinely unset values are hidden. A field the officer deliberately set to No or
* false is part of the configuration being reviewed, so it is shown like any other answer.
*/
private setCustomFieldValues(): void {
this.customFieldSectionsByStep = LoanProductSummaryComponent.emptySectionsByStep();
this.interestRateDetailRows = [];
if (!this.loanProduct) {
return;
}
this.interestRateDetailRows = this.buildInterestRateDetailRows((this.loanProduct as any)['interestRateDetail']);
const productSubGroup: string = (this.loanProduct as any)['productTypeSubGroup'];
for (const section of CUSTOM_FIELD_SECTIONS) {
// A section the Settings step hides for this sub-group is not part of the product's
// configuration, so listing its defaulted controls here would only invite the reader to
// review settings that do not apply -- a Gold Loan has no cheque book.
if (!isCustomSectionApplicable(section.title, productSubGroup)) {
continue;
}
const rows: CustomFieldRow[] = [];
for (const field of section.fields) {
// Rendered as its own table under the Terms summary.
if (field === 'interestRateDetail') {
continue;
}
const row = this.buildCustomFieldRow(field, (this.loanProduct as any)[field]);
if (row) {
rows.push(row);
}
}
if (rows.length > 0) {
this.customFieldSectionsByStep[section.step].push({ title: section.title, rows });
}
}
}
private buildCustomFieldRow(field: string, value: any): CustomFieldRow | null {
if (value === null || value === undefined || value === '') {
return null;
}
const label = CUSTOM_FIELD_LABELS[field] ?? field;
const glAccountKind = CUSTOM_GL_ACCOUNT_FIELDS[field];
if (glAccountKind) {
const account = this.glAccountLookUp(value, this.glAccountOptionsFor(glAccountKind));
return {
label,
kind: 'text',
// An id with no matching account means the option lists have not loaded yet, so show the
// raw id rather than a blank cell.
text: account ? '(' + account.glCode + ') ' + account.name : String(value)
};
}
if (typeof value === 'boolean') {
return { label, kind: 'yesNo', yesNo: value };
}
if (value === 'YES' || value === 'NO') {
return { label, kind: 'yesNo', yesNo: value === 'YES' };
}
// Only real Date instances go through the date pipe -- a value already formatted by a step's
// getter is left as text so it is not reformatted a second time.
if (value instanceof Date) {
return { label, kind: 'date', date: value };
}
// Numeric strings such as account or MICR codes stay text, so leading zeros survive.
if (typeof value === 'number') {
return { label, kind: 'number', number: value };
}
return { label, kind: 'text', text: String(value) };
}
/** Drops the untouched rows of the legacy Interest Rate Detail grid. */
private buildInterestRateDetailRows(value: any): InterestRateDetailRow[] {
if (!Array.isArray(value)) {
return [];
}
return value.filter((row: any) =>
this.interestRateDetailColumns.some(
(column) =>
column.key !== 'interestType' &&
row?.[column.key] !== null &&
row?.[column.key] !== undefined &&
row?.[column.key] !== '' &&
row?.[column.key] !== false
)
);
}
private glAccountOptionsFor(kind: CustomGlAccountKind): GLAccount[] {
const options = this.loanProductsTemplate?.accountingMappingOptions;
switch (kind) {
case 'asset':
return options?.assetAccountOptions || [];
case 'income':
return options?.incomeAccountOptions || [];
case 'expense':
return options?.expenseAccountOptions || [];
case 'liability':
return options?.liabilityAccountOptions || [];
}
}
}
@@ -26,6 +26,7 @@
<div class="lp-tab-content">
<mifosx-loan-product-details-step
[loanProductsTemplate]="loanProductsTemplate"
(productSubGroupChange)="setProductSubGroup($event)"
></mifosx-loan-product-details-step>
</div>
</mat-tab>
@@ -51,6 +52,7 @@
<mifosx-loan-product-settings-step
[loanProductsTemplate]="loanProductsTemplate"
[isLinkedToFloatingInterestRates]="loanProductTermsForm.get('isLinkedToFloatingInterestRates')"
[productSubGroup]="selectedProductSubGroup"
(advancePaymentStrategy)="advancePaymentStrategy($event)"
></mifosx-loan-product-settings-step>
</div>
@@ -88,6 +88,7 @@ export class CreateLoanProductComponent implements OnInit {
selectedTabIndex = 0;
isAdvancedPaymentStrategy = false;
selectedProductSubGroup = '';
paymentAllocation: PaymentAllocation[] = [];
creditAllocation: PaymentAllocation[] = [];
supportedInterestRefundTypes: StringEnumOptionData[] = [];
@@ -144,6 +145,10 @@ export class CreateLoanProductComponent implements OnInit {
return this.loanProductTermsStep.loanProductTermsForm;
}
setProductSubGroup(value: string) {
this.selectedProductSubGroup = value;
}
advancePaymentStrategy(value: string) {
this.isAdvancedPaymentStrategy = LoanProducts.isAdvancedPaymentAllocationStrategy(value);
if (this.isAdvancedPaymentStrategy) {
@@ -244,6 +249,53 @@ export class CreateLoanProductComponent implements OnInit {
}
get loanProductFormValid() {
// TEMP DEBUG — remove once the invalid-step diagnosis is done.
const forms: [string, any][] = [
[
'details',
this.loanProductDetailsForm
],
[
'currency',
this.loanProductCurrencyForm
],
[
'terms',
this.loanProductTermsForm
],
[
'settings',
this.loanProductSettingsForm
],
[
'accounting',
this.loanProductAccountingForm
]
];
if (this.isAdvancedPaymentStrategy) {
forms.push([
'incomeCapitalization',
this.loanIncomeCapitalizationForm
]);
}
const invalid = forms.filter(
([
,
form
]) => !form?.valid
);
if (invalid.length) {
invalid.forEach(
([
name,
form
]) => {
const invalidControls = Object.keys(form.controls || {}).filter((key) => form.get(key)?.invalid);
console.warn(`[loanProductFormValid] "${name}" form invalid. Invalid controls:`, invalidControls);
}
);
}
if (this.isAdvancedPaymentStrategy) {
return (
this.loanProductDetailsForm.valid &&
@@ -39,6 +39,7 @@
<mifosx-loan-product-details-step
[loanProductsTemplate]="loanProductAndTemplate"
(productSubGroupChange)="setProductSubGroup($event)"
></mifosx-loan-product-details-step>
</mat-step>
@@ -56,6 +57,7 @@
<mifosx-loan-product-settings-step
[loanProductsTemplate]="loanProductAndTemplate"
[isLinkedToFloatingInterestRates]="loanProductTermsForm.get('isLinkedToFloatingInterestRates')"
[productSubGroup]="selectedProductSubGroup"
(advancePaymentStrategy)="advancePaymentStrategy($event)"
>
</mifosx-loan-product-settings-step>
@@ -95,6 +95,7 @@ export class EditLoanProductComponent implements OnInit {
itemsByDefault: GlobalConfiguration[] = [];
isAdvancedPaymentStrategy = false;
selectedProductSubGroup = '';
wasPaymentAllocationChanged = false;
paymentAllocation: PaymentAllocation[] = [];
creditAllocation: CreditAllocation[] = [];
@@ -116,7 +117,9 @@ export class EditLoanProductComponent implements OnInit {
const loanProducts = this.loanProducts;
this.route.data.subscribe((data: { loanProductAndTemplate: any; configurations: any }) => {
this.loanProductAndTemplate = data.loanProductAndTemplate;
// Lift the saved legacy "Account Type Creation" fields to the top level before any step reads
// them, since the backend returns them nested under `cbsExtensions`.
this.loanProductAndTemplate = loanProducts.flattenCbsExtensions(data.loanProductAndTemplate);
const assetAccountData = this.loanProductAndTemplate.accountingMappingOptions.assetAccountOptions || [];
const liabilityAccountData = this.loanProductAndTemplate.accountingMappingOptions.liabilityAccountOptions || [];
this.loanProductAndTemplate.accountingMappingOptions.assetAndLiabilityAccountOptions =
@@ -218,6 +221,10 @@ export class EditLoanProductComponent implements OnInit {
this.isAdvancedPaymentStrategy = LoanProducts.isAdvancedPaymentAllocationStrategy(value);
}
setProductSubGroup(value: string): void {
this.selectedProductSubGroup = value;
}
buildAdvancedPaymentAllocation(): void {
this.advancedPaymentAllocations = this.advancedPaymentStrategy.buildAdvancedPaymentAllocationList(
this.loanProductAndTemplate
@@ -0,0 +1,820 @@
/**
* 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/.
*/
/**
* Metadata for the legacy "Account Type Creation" fields grafted onto the loan product form.
*
* These fields have no home in Fineract's loan product schema, so `LoanProducts.buildPayload()`
* strips them before submitting. They are still worth showing in full on the preview step, which is
* what this file exists for: the grouping and labels here mirror the create/edit form, so an
* officer reviews the same wording they typed against.
*
* Each section carries the `step` it was entered on, so the preview can show a section directly
* under that tab's own summary instead of collecting every legacy field into one block at the
* bottom -- reviewing "Cheque Book Settings" is only meaningful next to the rest of Settings.
*
* `CUSTOM_UI_ONLY_FIELDS` is derived from `CUSTOM_FIELD_SECTIONS` rather than kept alongside it, so
* the strip list and the preview can never disagree about which fields are custom.
*/
import { UntypedFormGroup } from '@angular/forms';
/** Which set of GL accounts a custom mapping field draws from, for resolving an id to an account. */
export type CustomGlAccountKind = 'asset' | 'income' | 'expense' | 'liability';
/** The create/edit tab a section's fields are entered on. */
export type CustomFieldStep = 'details' | 'settings' | 'terms' | 'charges' | 'accounting';
export interface CustomFieldSection {
/** Heading shown on the preview step. */
title: string;
/** Tab this section belongs under. */
step: CustomFieldStep;
fields: string[];
}
export const CUSTOM_FIELD_SECTIONS: CustomFieldSection[] = [
{
title: 'Classification & Legacy Header',
step: 'details',
fields: [
'productTypeGroup',
'productTypeSubGroup',
'debitIntCalculation',
'separatePenalInterest',
'creditIntCalculation',
'subAccountTypeBalance',
'finalIntRounding',
'roundingUpTo',
'accountTypeCode',
'micrTransactionCode',
'reservedEcsSerialNumber',
'reverseEcsNumber',
'lockChequeInfoEnabled',
'lockPageTwoInfoEnabled',
'computerizationDate',
'chequePrintDescription',
'posAccountTypeDescription',
'pfmsAccountType',
'renewalDateAddMonths',
'documentRenewalDateAddMonths',
'ppsLimit',
'isWriteOffAccountType',
'writeOffAccountTypeCode',
'renewalGraceDays'
]
},
{
title: 'Cheque Book Settings',
step: 'settings',
fields: [
'chequeBookIssueEnabled',
'chequeValidationRequired',
'isFirstChequeBookFree',
'chequePrintingAccountType',
'allowWithdrawalSlipUsage',
'maintainChequeBookStockRegistry',
'freeChequeBooksPerYear',
'allowCreditByChequeNumber',
'creditChequeValidationRequired',
'graceDaysForStockStatement'
]
},
{
title: 'Additional Product Settings',
step: 'settings',
fields: [
'accountTypeActiveForTransactions',
'subTypeActivityEnabled',
'accountOpeningCharges',
'maximumGoldLoanLimit',
'lastAccountNumberOpened',
'customerTerminationAllowed',
'chargesForCashCreditAboveLimit',
'deathInterestGLAccount',
'startingLedgerNumber',
'endingLedgerNumber',
'currentLedgerNumber',
'remittanceAccountNumber',
'isNonResidentIndianAccount',
'debitClearingGLAccount',
'missedCallAlertServiceActive',
'standingInstructionChargeGLAccount',
'standingInstructionDebitChargeSkipOption',
'openingBalanceMultiplierForRemittanceCharges',
'goldLoanPacketNumberWithYearEnabled',
'accountMoveToInoperativeEnabled',
'generateGlDiffEntryEnabled',
'subTypeWiseGlBalanceGenerationEnabled',
'validateBeforeDeletingDebitEntryEnabled',
'rcFileConceptEnabled',
'accountCloseZeroBalanceInOpenDayProcess',
'accountCloseZeroBalanceAtSubledgerTranEntry',
'limitEnhancementAgainstClearingScreenDisplayEnabled',
'advanceInterestParameterInLedgerEnabled',
'vehicleDetailInMasterEnabled',
'deletedChequeIssueEnabled',
'dpReduceInDocumentRenewalDateEnabled',
'dpReduceInRenewalDateEnabled',
'autoCloseAtDayEndEnabled',
'loanRateFromMasterEnabled',
'chequeShortName',
'statementPrintEnabled',
'neslCode'
]
},
{
title: 'Interest Related Parameters',
step: 'settings',
fields: [
'drawingPowerAndLimitGenerationProcedure',
'advancesInterestCalculationMethod',
'againstInterestCalculationEnabled',
'dayWiseAgainstClearingInterestRounding',
'interestCalculationOneFourthMethod',
'againstClearingInterestProcedureType',
'considerDebitClosingAmountForAgainstClearing',
'interestRoundingMethod',
'penaltyInterestCalculationEnabled',
'penaltyInterestCalculationMethod',
'penaltyInterestProcedureNumber',
'overdrawnInterestCalculationMethod',
'calculateOverdrawnInterestAfterRenewal',
'interestOnCreditBalance',
'penaltyInterestDateDifferentThanRegularInterest',
'fixedDepositOverdraftCertificateSortOrder',
'useDocumentSubmissionDateForInterest',
'loanManagementSystemRenewalPenaltyActive',
'minimumNormalInterestAmount',
'minimumInterestCalculationMethod',
'minimumAgainstClearingInterestAmount',
'limitIncreaseUptoInterestAmountInInterestCalculation',
'minimumPenaltyInterestAmount',
'daysForLimitIncreaseUptoInterestAmount',
'additionalInterestRateForFixedDepositOverdraft',
'considerDdPayorderInAgainstClearingInterestCalculation',
'graceDaysAfterRenewDate',
'applyUndebitedInterestConceptEnabled',
'interestRegisterPrintFormatType',
'normalGraceDaysAfterOverdue',
'allowAgainstClearingEntry',
'allowNoBalanceEntry',
'fixedDepositOverdraftPenaltyInterestCombinedWithNormalInterest',
'allowNoBalanceEntryWithPassword',
'undebitedSimpleInterestAccountWise',
'dailyInterestCalculationEnabled',
'dailyInterestCalculationStartDate',
'nonPerformingAssetPenalInterestRateUpdate',
'nonPerformingAssetPenalInterestRate',
'fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess',
'addPenalInterestRate',
'atyp14SimpleInterestEnabled',
'fixedDepositOverdraftMarginType',
'checkMasterDetailEditedPassingPendingEnabled',
'creditDebitInterestCalculationSeparateEnabled',
'onTimeInterestProvisionEnabled',
'checkAgainstClearingEnabled',
'gstOnPenaltyEnabled',
'penaltyInterestGraceDays',
'overdrawnInterestRate',
'calculateDailyLimitEnhancementTodEnabled',
'fixedDepositOverdraftMultipleInterestRateEnabled',
'graceDaysAfterOverdueForMoratorium'
]
},
{
title: 'Credit Bureau Settings (CIBIL)',
step: 'settings',
fields: [
'cibilAccountType',
'cibilCommercialAccountType',
'highmarkAccountType',
'highmarkCommercialAccountType',
'experianAccountType',
'experianCommercialAccountType',
'equifaxAccountType',
'equifaxCommercialAccountType'
]
},
{
title: 'RBI Compliance & Product Category',
step: 'settings',
fields: [
'loanProductCategory',
'penalChargeMethod',
'isFloatingRate',
'foreclosureChargesApplicable',
'keyFactStatementEnabled',
'annualPercentageRateCalculationEnabled'
]
},
{
title: 'Channel & Transaction Limits',
step: 'settings',
fields: [
'isMobileAppEnabled',
'isAtmEnabled',
'atmYearlyChargeEnabled',
'atmMinimumBalance',
'emailActiveEnabled',
'isImpsEnabled',
'isUpiEnabled',
'isPosEnabled',
'isBbpsEnabled',
'interBankLimitPerDay',
'interBankLimitPerTransaction',
'neftRtgsLimitPerDay',
'neftRtgsLimitPerTransaction',
'neftCutoffLimit',
'rtgsCutoffLimit',
'neftRtgsMinimumBalance',
'isChargeRequiredNeftRtgsCbs',
'isChargeRequiredNeftRtgsMobile',
'impsLimitPerDay',
'impsLimitPerTransaction',
'impsLimitPerMonth',
'impsMinLimit',
'impsMonthlyTransactionCount'
]
},
{
title: 'POS Facility & ATM CSV Settings',
step: 'settings',
fields: [
'maxPerTransactionLimitAtm',
'maxDayTransactionLimitAtm',
'maxPerTransactionLimitPos',
'maxDayTransactionLimitPos',
'maxPerTransactionLimitEcom',
'maxDayTransactionLimitEcom',
'maxPerTransactionLimitUpi',
'maxDayTransactionLimitUpi',
'atmCsvProductCode',
'atmCsvTypeCode'
]
},
{
title: 'Auto Interest Post Parameters',
step: 'settings',
fields: [
'autoInterestPostType',
'autoInterestPostWhen',
'autoInterestPostFixDay',
'autoLastInterestDate'
]
},
{
title: 'Foreclosure / Early Account Closure',
step: 'settings',
fields: [
'calculateEarlyAccountCloseCharge',
'earlyAccountCloseCharge',
'earlyAccountClosePeriodInMonths'
]
},
{
title: 'Legacy Terms',
step: 'terms',
fields: [
'debitBalanceAllow',
'allowDebitTransAfterDueDate',
'branchLevelAccountType',
'interestRateDetail'
]
},
{
title: 'Legacy Cheque, Standing Instruction & Minimum Balance Charges',
step: 'charges',
fields: [
'chequeBookChargeEnabled',
'chequeReturnChargeEnabled',
'icChargeDebitEnabled',
'cashBundleChargeEnabled',
'cashBundleChargeWithPanLimit',
'perChequeEntryCharge',
'perChequeEntryServiceCharge',
'perInwardChequeEntryCharge',
'perOutwardChequeEntryCharge',
'creditorStandingInstructionFailCharge',
'debtorStandingInstructionFailCharge',
'minimumBalanceCheckingType',
'autoMinimumBalanceCharge',
'calculateDailyMinimumBalanceCharges',
'minimumBalanceChargeEnabled',
'minimumBalanceChargeAmount',
'minimumBalanceChargeForChequeIssue',
'minimumBalanceChargeMethod',
'monthsAfterMinBalanceChargeCalculate',
'forChequeIssue',
'forChequeIssueAvg',
'forWithdrawalSlip',
'forWithdrawalSlipAvg',
'calculateDailyAgainstClearingCharge'
]
},
{
title: 'Custom GL Account Mappings',
step: 'accounting',
fields: [
'intReceivablesGlAccountId',
'intPayableGlAccountId',
'deafIntPayableGlAccountId',
'intOverdueReserveAccountId',
'undebitedInterestAccountId',
'penalIntCrGlAccountId',
'rdMaturedGlAccountId',
'glAccountId',
'icChargeGlAccountId',
'inoperativeGlAccountId',
'creditBalanceLiabilityGlAccountId',
'creditBalanceAssetGlAccountId',
'overdueAccountsGlAccountId',
'rebateGlAccountId'
]
}
];
/** Form labels, copied from the create/edit step templates so preview wording matches. */
export const CUSTOM_FIELD_LABELS: Record<string, string> = {
productTypeGroup: 'Account Type Group',
productTypeSubGroup: 'Product Type Sub Group',
debitIntCalculation: 'Debit Int. Calculation',
separatePenalInterest: 'Separate Penal Int.',
creditIntCalculation: 'Credit Int. Calculation',
subAccountTypeBalance: 'Sub A/c Type Balance',
finalIntRounding: 'Final Int. Rounding',
roundingUpTo: 'Rounding UpTo',
accountTypeCode: 'A/c Type Code',
micrTransactionCode: 'MICR Trn. Code',
reservedEcsSerialNumber: 'Reserved Code Srno For ECS',
reverseEcsNumber: 'Reverse ECS No.',
lockChequeInfoEnabled: 'Lock Cheque Info',
lockPageTwoInfoEnabled: 'Lock Page 2 Info',
computerizationDate: 'Computerization Date',
chequePrintDescription: 'Chq. Print Description',
posAccountTypeDescription: 'POS A/c Type Description',
pfmsAccountType: 'PFMS Account Type',
renewalDateAddMonths: 'Renewal Dt. Add Months',
documentRenewalDateAddMonths: 'Doc. Renewal Dt. Add Months',
ppsLimit: 'PPS Limit',
isWriteOffAccountType: 'Is this Actyp Write Off?',
writeOffAccountTypeCode: 'Write Off Actyp',
renewalGraceDays: 'Renewal Grace Days',
chequeBookIssueEnabled: 'Cheque Book Issue Enabled',
chequeValidationRequired: 'Cheque Validation Required',
isFirstChequeBookFree: 'Is First Cheque Book Free',
chequePrintingAccountType: 'Cheque Printing Account Type',
allowWithdrawalSlipUsage: 'Allow Withdrawal Slip Usage',
maintainChequeBookStockRegistry: 'Maintain Cheque Book Stock Registry',
freeChequeBooksPerYear: 'Free Cheque Books Per Year',
allowCreditByChequeNumber: 'Allow Credit By Cheque Number',
creditChequeValidationRequired: 'Credit Cheque Validation Required',
graceDaysForStockStatement: 'Grace Days For Stock Statement',
accountTypeActiveForTransactions: 'Account Type Active For Transactions',
subTypeActivityEnabled: 'Sub Type Activity Enabled',
accountOpeningCharges: 'Account Opening Charges',
maximumGoldLoanLimit: 'Maximum Gold Loan Limit',
lastAccountNumberOpened: 'Last Account Number Opened',
customerTerminationAllowed: 'Customer Termination Allowed',
chargesForCashCreditAboveLimit: 'Charges For Cash Credit Above Limit',
deathInterestGLAccount: 'Death Interest GL Account',
startingLedgerNumber: 'Starting Ledger Number',
endingLedgerNumber: 'Ending Ledger Number',
currentLedgerNumber: 'Current Ledger Number',
remittanceAccountNumber: 'Remittance Account Number',
isNonResidentIndianAccount: 'Is Non Resident Indian Account',
debitClearingGLAccount: 'Debit Clearing GL Account',
missedCallAlertServiceActive: 'Missed Call Alert Service Active',
standingInstructionChargeGLAccount: 'Standing Instruction Charge GL Account',
standingInstructionDebitChargeSkipOption: 'Standing Instruction Debit Charge Skip Option',
openingBalanceMultiplierForRemittanceCharges: 'Opening Balance Multiplier For Remittance Charges',
goldLoanPacketNumberWithYearEnabled: 'Gold Loan [Packet No.] With Year?',
accountMoveToInoperativeEnabled: 'A/c Move To InOperative A/c',
generateGlDiffEntryEnabled: 'Generate GL Diff Entry',
subTypeWiseGlBalanceGenerationEnabled: 'Sub Type Wise GL Balance Gen',
validateBeforeDeletingDebitEntryEnabled: 'Want To Validation While Deleting Debit Entry',
rcFileConceptEnabled: 'Want To RC File Concept',
accountCloseZeroBalanceInOpenDayProcess: 'A/c Close With Zero Bal. In Open Day Process',
accountCloseZeroBalanceAtSubledgerTranEntry: 'A/c Close When Bal. Zero At Subled Tran Entry',
limitEnhancementAgainstClearingScreenDisplayEnabled: 'Lmt.Enh/Ag.Clg Screen Display',
advanceInterestParameterInLedgerEnabled: 'Advance Int. Param In Ledger',
vehicleDetailInMasterEnabled: 'Vehicle Detail In Master',
deletedChequeIssueEnabled: 'Deleted Chq. Issue',
dpReduceInDocumentRenewalDateEnabled: 'DP Reduce In Doc. Renewal Date',
dpReduceInRenewalDateEnabled: 'DP Reduce In Renewal Date',
autoCloseAtDayEndEnabled: 'Auto Close Day-End',
loanRateFromMasterEnabled: 'Loan Rate From Master',
chequeShortName: 'Cheque Short Name',
statementPrintEnabled: 'Statement Print',
neslCode: 'NESL Code',
drawingPowerAndLimitGenerationProcedure: 'Drawing Power And Limit Generation Procedure',
advancesInterestCalculationMethod: 'Advances Interest Calculation Method',
againstInterestCalculationEnabled: 'Against Interest Calculation Enabled',
dayWiseAgainstClearingInterestRounding: 'Day Wise Against Clearing Interest Rounding',
interestCalculationOneFourthMethod: 'Interest Calculation One Fourth Method',
againstClearingInterestProcedureType: 'Against Clearing Interest Procedure Type',
considerDebitClosingAmountForAgainstClearing: 'Consider Debit Closing Amount For Against Clearing',
interestRoundingMethod: 'Interest Rounding Method',
penaltyInterestCalculationEnabled: 'Penalty Interest Calculation Enabled',
penaltyInterestCalculationMethod: 'Penalty Interest Calculation Method',
penaltyInterestProcedureNumber: 'Penalty Interest Procedure Number',
overdrawnInterestCalculationMethod: 'Overdrawn Interest Calculation Method',
calculateOverdrawnInterestAfterRenewal: 'Calculate Overdrawn Interest After Renewal',
interestOnCreditBalance: 'Interest On Credit Balance',
penaltyInterestDateDifferentThanRegularInterest: 'Penalty Interest Date Different Than Regular Interest',
fixedDepositOverdraftCertificateSortOrder: 'Fixed Deposit Overdraft Certificate Sort Order',
useDocumentSubmissionDateForInterest: 'Use Document Submission Date For Interest',
loanManagementSystemRenewalPenaltyActive: 'Loan Management System Renewal Penalty Active',
minimumNormalInterestAmount: 'Minimum Normal Interest Amount',
minimumInterestCalculationMethod: 'Minimum Interest Calculation Method',
minimumAgainstClearingInterestAmount: 'Minimum Against Clearing Interest Amount',
limitIncreaseUptoInterestAmountInInterestCalculation: 'Limit Increase Upto Interest Amount In Interest Calculation',
minimumPenaltyInterestAmount: 'Minimum Penalty Interest Amount',
daysForLimitIncreaseUptoInterestAmount: 'Days For Limit Increase Upto Interest Amount',
additionalInterestRateForFixedDepositOverdraft: 'Additional Interest Rate For Fixed Deposit Overdraft',
considerDdPayorderInAgainstClearingInterestCalculation:
'Consider DD/Payorder In Against Clearing Interest Calculation',
graceDaysAfterRenewDate: 'Grace Days After Renewal Date',
applyUndebitedInterestConceptEnabled: 'Apply Undebited Interest Concept Enabled',
interestRegisterPrintFormatType: 'Interest Register Print Format Type',
normalGraceDaysAfterOverdue: 'Normal Grace Days After Overdue',
allowAgainstClearingEntry: 'Allow Against Clearing Entry',
allowNoBalanceEntry: 'Allow No Balance Entry',
fixedDepositOverdraftPenaltyInterestCombinedWithNormalInterest:
'Fixed Deposit Overdraft Penalty Interest Combined With Normal Interest',
allowNoBalanceEntryWithPassword: 'Allow No Balance Entry With Password',
undebitedSimpleInterestAccountWise: 'Undebited Simple Interest Account Wise',
dailyInterestCalculationEnabled: 'Daily Interest Calculation Enabled',
dailyInterestCalculationStartDate: 'Daily Interest Calculation Start Date',
nonPerformingAssetPenalInterestRateUpdate: 'Non Performing Asset Penal Interest Rate Update',
nonPerformingAssetPenalInterestRate: 'Non Performing Asset Penal Interest Rate',
fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess:
'Fixed Deposit Overdraft Penal Interest Rate Update In Branch Process',
addPenalInterestRate: 'Add Penal Interest Rate',
atyp14SimpleInterestEnabled: 'Atyp14 Simple Int.',
fixedDepositOverdraftMarginType: 'FDOD Margin Type',
checkMasterDetailEditedPassingPendingEnabled: 'Check Master Detail Edited (Passing Pend.)',
creditDebitInterestCalculationSeparateEnabled: 'Credit/Debit Int. Calc. Separate',
onTimeInterestProvisionEnabled: 'On Time Interest Provision',
checkAgainstClearingEnabled: 'Check Against Clearing',
gstOnPenaltyEnabled: 'GST On Penalty',
penaltyInterestGraceDays: 'Penalty Int. Grace Days',
overdrawnInterestRate: 'Over Drawn Int. Rate',
calculateDailyLimitEnhancementTodEnabled: 'Calculate Daily Limit Enhancement (TOD)',
fixedDepositOverdraftMultipleInterestRateEnabled: 'FDOD Multiple Int. Rate',
graceDaysAfterOverdueForMoratorium: 'Grace Days After Overdue For Moratorium A/c',
cibilAccountType: 'CIBIL Account Type',
cibilCommercialAccountType: 'CIBIL Commercial Account Type',
highmarkAccountType: 'Highmark Account Type',
highmarkCommercialAccountType: 'Highmark Commercial Account Type',
experianAccountType: 'Experian Account Type',
experianCommercialAccountType: 'Experian Commercial Account Type',
equifaxAccountType: 'Equifax Account Type',
equifaxCommercialAccountType: 'Equifax Commercial Account Type',
loanProductCategory: 'Loan Product Category (RBI Reporting)',
penalChargeMethod: 'Penal Charge Method',
isFloatingRate: 'Penal Charge Method',
foreclosureChargesApplicable: 'Foreclosure Charges Applicable',
keyFactStatementEnabled: 'Foreclosure Charges Applicable',
annualPercentageRateCalculationEnabled: 'Foreclosure Charges Applicable',
isMobileAppEnabled: 'Is Mobile App?',
isAtmEnabled: 'Is ATM?',
atmYearlyChargeEnabled: 'ATM Yearly Charge?',
atmMinimumBalance: 'ATM Min. Balance',
emailActiveEnabled: 'Email Active?',
isImpsEnabled: 'Is IMPS?',
isUpiEnabled: 'Is UPI?',
isPosEnabled: 'Is POS?',
isBbpsEnabled: 'Is BBPS?',
interBankLimitPerDay: 'Inter Bank Limit (Per Day)',
interBankLimitPerTransaction: 'Inter Bank Limit (Per Transaction)',
neftRtgsLimitPerDay: 'NEFT/RTGS Limit (Per Day)',
neftRtgsLimitPerTransaction: 'NEFT/RTGS Limit (Per Transaction)',
neftCutoffLimit: 'NEFT Cutoff Limit',
rtgsCutoffLimit: 'RTGS Cutoff Limit',
neftRtgsMinimumBalance: 'NEFT/RTGS Min. Balance',
isChargeRequiredNeftRtgsCbs: 'Is Charge Req. NEFT/RTGS (CBS)?',
isChargeRequiredNeftRtgsMobile: 'Is Charge Req. NEFT/RTGS (Mobile)?',
impsLimitPerDay: 'IMPS Limit (Per Day)',
impsLimitPerTransaction: 'IMPS Limit (Per Transaction)',
impsLimitPerMonth: 'IMPS Limit (Per Month)',
impsMinLimit: 'IMPS Min. Limit',
impsMonthlyTransactionCount: 'IMPS Monthly Transaction Count',
maxPerTransactionLimitAtm: 'Max Per Transaction Limit (ATM)',
maxDayTransactionLimitAtm: 'Max Day Transaction Limit (ATM)',
maxPerTransactionLimitPos: 'Max Per Transaction Limit (POS)',
maxDayTransactionLimitPos: 'Max Day Transaction Limit (POS)',
maxPerTransactionLimitEcom: 'Max Per Transaction Limit (ECOM)',
maxDayTransactionLimitEcom: 'Max Day Transaction Limit (ECOM)',
maxPerTransactionLimitUpi: 'Max Per Transaction Limit (UPI)',
maxDayTransactionLimitUpi: 'Max Day Transaction Limit (UPI)',
atmCsvProductCode: 'ATM CSV Product Code',
atmCsvTypeCode: 'ATM CSV Type Code',
autoInterestPostType: 'Auto Int. Post Type',
autoInterestPostWhen: 'Auto Int. Post When',
autoInterestPostFixDay: 'Fix Day',
autoLastInterestDate: 'Auto Last Int. Date',
calculateEarlyAccountCloseCharge: 'Calculate Early A/c Close Charge?',
earlyAccountCloseCharge: 'Early A/c Close Charge',
earlyAccountClosePeriodInMonths: 'Early A/c Close Period (In Months)',
debitBalanceAllow: 'Debit Balance Allow',
allowDebitTransAfterDueDate: 'Allow Debit Trans After Due Date',
branchLevelAccountType: 'Branch Level A/c Type',
interestRateDetail: 'Interest Rate Detail',
chequeBookChargeEnabled: 'Chq. Book Charge',
chequeReturnChargeEnabled: 'Chq. Return Charge',
icChargeDebitEnabled: 'I.C. Charge Debit',
cashBundleChargeEnabled: 'Cash Bundle Charge',
cashBundleChargeWithPanLimit: 'Cash Bundle Charge With PAN Limit',
perChequeEntryCharge: 'Per Cheque Entry Charge',
perChequeEntryServiceCharge: 'Per Cheque Entry Service Charge',
perInwardChequeEntryCharge: 'Per Inward Cheque Entry Charge',
perOutwardChequeEntryCharge: 'Per Outward Cheque Entry Charge',
creditorStandingInstructionFailCharge: 'Creditor Standing Instr. Fail Charge',
debtorStandingInstructionFailCharge: 'Debtor Standing Instr. Fail Charge',
minimumBalanceCheckingType: 'Minimum Balance Checking',
autoMinimumBalanceCharge: 'Auto Minimum Balance Charge',
calculateDailyMinimumBalanceCharges: 'Calculate Daily Min. Balance Charges',
minimumBalanceChargeEnabled: 'Minimum Balance Charge',
minimumBalanceChargeAmount: 'Min. Balance Charge (Rs.)',
minimumBalanceChargeForChequeIssue: 'Min. Balance Charge For Cheque Issue (Rs.)',
minimumBalanceChargeMethod: 'Min. Balance Charge Method',
monthsAfterMinBalanceChargeCalculate: 'Months After Min. Balance Charge Calculate',
forChequeIssue: 'For Cheque Issue',
forChequeIssueAvg: 'For Cheque Issue (Avg.)',
forWithdrawalSlip: 'For Withdrawal Slip',
forWithdrawalSlipAvg: 'For Withdrawal Slip (Avg.)',
calculateDailyAgainstClearingCharge: 'Calculate Daily Against Clearing Charge',
intReceivablesGlAccountId: 'Int. Receivables GL A/c',
intPayableGlAccountId: 'Int. Payable GL A/c',
deafIntPayableGlAccountId: 'Deaf Int. Payble GL A/c',
intOverdueReserveAccountId: 'Int. Overdue Reserve A/c (AS)',
undebitedInterestAccountId: 'UnDebited Interest A/c (LI)',
penalIntCrGlAccountId: 'Penal Int. Cr. GL A/c',
rdMaturedGlAccountId: 'RD Matured GL A/c',
glAccountId: 'GL A/c No.',
icChargeGlAccountId: 'I.C. Charge GL A/c No.',
inoperativeGlAccountId: 'InOperative GL A/c',
creditBalanceLiabilityGlAccountId: 'Cr.Balance Liability GL A/c',
creditBalanceAssetGlAccountId: 'Cr.Balance Asset GL A/c',
overdueAccountsGlAccountId: 'Overdue Accounts GL A/c No.',
rebateGlAccountId: 'Rebate GL A/c No.'
};
/**
* Custom GL mapping fields hold an account id, so the preview resolves each to its account rather
* than printing a bare number. The kind picks which option list from the product template to
* search -- it matches the `options` the Accounting step's own field config uses.
*/
export const CUSTOM_GL_ACCOUNT_FIELDS: Record<string, CustomGlAccountKind> = {
intReceivablesGlAccountId: 'asset',
intPayableGlAccountId: 'liability',
deafIntPayableGlAccountId: 'asset',
intOverdueReserveAccountId: 'asset',
undebitedInterestAccountId: 'liability',
penalIntCrGlAccountId: 'income',
rdMaturedGlAccountId: 'asset',
glAccountId: 'asset',
icChargeGlAccountId: 'income',
inoperativeGlAccountId: 'asset',
creditBalanceLiabilityGlAccountId: 'liability',
creditBalanceAssetGlAccountId: 'asset',
overdueAccountsGlAccountId: 'asset',
rebateGlAccountId: 'expense'
};
/** Columns of the legacy Interest Rate Detail grid, whose value is a row array rather than a scalar. */
export const INTEREST_RATE_DETAIL_COLUMNS: { key: string; label: string }[] = [
{
key: 'interestType',
label: 'Interest Type'
},
{
key: 'calculation',
label: 'Calculation'
},
{
key: 'normalMinRate',
label: 'Normal Min. Rate'
},
{
key: 'normalMaxRate',
label: 'Normal Max. Rate'
},
{
key: 'specialMinRate',
label: 'Special Min. Rate'
},
{
key: 'specialMaxRate',
label: 'Special Max. Rate'
},
{
key: 'period',
label: 'Period'
}
];
/**
* Legacy fields whose form control is a dropdown holding the *strings* 'YES' and 'NO', not a boolean.
*
* They are stored as BOOLEAN, so a saved product comes back from the API as true/false. Patching that
* straight into the form would match no option and leave the dropdown blank, which is why
* `LoanProducts.flattenCbsExtensions()` maps them back to 'YES'/'NO' on the way in.
*/
export const CUSTOM_YES_NO_STRING_FIELDS: string[] = [
'lockChequeInfoEnabled',
'lockPageTwoInfoEnabled',
'isWriteOffAccountType',
'chequeBookIssueEnabled',
'chequeValidationRequired',
'isFirstChequeBookFree',
'allowWithdrawalSlipUsage',
'maintainChequeBookStockRegistry',
'allowCreditByChequeNumber',
'creditChequeValidationRequired',
'accountTypeActiveForTransactions',
'subTypeActivityEnabled',
'customerTerminationAllowed',
'isNonResidentIndianAccount',
'missedCallAlertServiceActive',
'goldLoanPacketNumberWithYearEnabled',
'accountMoveToInoperativeEnabled',
'generateGlDiffEntryEnabled',
'subTypeWiseGlBalanceGenerationEnabled',
'validateBeforeDeletingDebitEntryEnabled',
'rcFileConceptEnabled',
'accountCloseZeroBalanceInOpenDayProcess',
'accountCloseZeroBalanceAtSubledgerTranEntry',
'limitEnhancementAgainstClearingScreenDisplayEnabled',
'advanceInterestParameterInLedgerEnabled',
'vehicleDetailInMasterEnabled',
'deletedChequeIssueEnabled',
'dpReduceInDocumentRenewalDateEnabled',
'dpReduceInRenewalDateEnabled',
'autoCloseAtDayEndEnabled',
'loanRateFromMasterEnabled',
'statementPrintEnabled',
'againstInterestCalculationEnabled',
'dayWiseAgainstClearingInterestRounding',
'considerDebitClosingAmountForAgainstClearing',
'penaltyInterestCalculationEnabled',
'calculateOverdrawnInterestAfterRenewal',
'interestOnCreditBalance',
'penaltyInterestDateDifferentThanRegularInterest',
'useDocumentSubmissionDateForInterest',
'loanManagementSystemRenewalPenaltyActive',
'limitIncreaseUptoInterestAmountInInterestCalculation',
'considerDdPayorderInAgainstClearingInterestCalculation',
'applyUndebitedInterestConceptEnabled',
'allowAgainstClearingEntry',
'allowNoBalanceEntry',
'fixedDepositOverdraftPenaltyInterestCombinedWithNormalInterest',
'allowNoBalanceEntryWithPassword',
'undebitedSimpleInterestAccountWise',
'dailyInterestCalculationEnabled',
'nonPerformingAssetPenalInterestRateUpdate',
'fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess',
'atyp14SimpleInterestEnabled',
'checkMasterDetailEditedPassingPendingEnabled',
'creditDebitInterestCalculationSeparateEnabled',
'onTimeInterestProvisionEnabled',
'checkAgainstClearingEnabled',
'gstOnPenaltyEnabled',
'calculateDailyLimitEnhancementTodEnabled',
'fixedDepositOverdraftMultipleInterestRateEnabled',
'foreclosureChargesApplicable',
'isMobileAppEnabled',
'isAtmEnabled',
'atmYearlyChargeEnabled',
'emailActiveEnabled',
'isImpsEnabled',
'isUpiEnabled',
'isPosEnabled',
'isBbpsEnabled',
'isChargeRequiredNeftRtgsCbs',
'isChargeRequiredNeftRtgsMobile',
'calculateEarlyAccountCloseCharge',
'debitBalanceAllow',
'allowDebitTransAfterDueDate',
'branchLevelAccountType',
'chequeBookChargeEnabled',
'chequeReturnChargeEnabled',
'icChargeDebitEnabled',
'cashBundleChargeEnabled',
'autoMinimumBalanceCharge',
'calculateDailyMinimumBalanceCharges',
'minimumBalanceChargeEnabled',
'calculateDailyAgainstClearingCharge'
];
/**
* Sub-groups whose products are current-account-linked and therefore carry channel limits.
* Kept here rather than only on the Settings step so the preview can apply the same rule.
*/
export const CHANNEL_ENABLED_SUB_GROUPS: string[] = [
'cc',
'overdraft'
];
/** Sub-groups that are fixed-tenure EMI term loans, the only ones foreclosure applies to. */
export const TERM_LOAN_SUB_GROUPS: string[] = [
'term',
'personal',
'housing',
'vehicle',
'education',
'business',
'gold_emi'
];
/** Sub-groups issued against pledged gold, which have no cheque book. */
export const NO_CHEQUE_BOOK_SUB_GROUPS: string[] = [
'gold',
'gold_emi'
];
/**
* Whether a legacy section applies to a product, given its sub-group.
*
* The Settings step hides three of these sections outright for some sub-groups -- a Gold Loan has no
* cheque book, no channel limits and no foreclosure charges. The preview was still listing them,
* because the controls keep their defaults whether or not the section was ever rendered, so a Gold
* Loan showed a "Cheque Book Settings" table the officer never saw. Titles are matched against
* CUSTOM_FIELD_SECTIONS, so a rename has to happen in one place.
*
* An unknown or empty sub-group shows everything, matching how the form behaves before a sub-group
* is picked.
*/
export function isCustomSectionApplicable(sectionTitle: string, productSubGroup: string | null | undefined): boolean {
if (!productSubGroup) {
return true;
}
switch (sectionTitle) {
case 'Cheque Book Settings':
return !NO_CHEQUE_BOOK_SUB_GROUPS.includes(productSubGroup);
case 'Channel & Transaction Limits':
case 'POS Facility & ATM CSV Settings':
return CHANNEL_ENABLED_SUB_GROUPS.includes(productSubGroup);
case 'Foreclosure / Early Account Closure':
return TERM_LOAN_SUB_GROUPS.includes(productSubGroup);
default:
return true;
}
}
/** Legacy date fields. Fineract returns a date as [year, month, day], which needs a real Date for the picker. */
export const CUSTOM_DATE_FIELDS: string[] = [
'computerizationDate'
];
/** Every custom field, in form order. Derived from the sections above -- do not maintain separately. */
export const CUSTOM_UI_ONLY_FIELDS: string[] = CUSTOM_FIELD_SECTIONS.flatMap((section) => section.fields);
/**
* Copies a saved product's legacy field values into one step's form.
*
* Every step calls this with its own form and the same product: a field belonging to another step
* has no control here, so it is skipped. That works because the form control names and the field
* names in this file are the same strings -- which is also where the labels and sections above were
* read from -- so there is no per-step mapping to keep in step with the forms.
*
* Written as a loop rather than 220 lines of patchValue because a field added to a step's form later
* is picked up here for free, and a typo cannot silently leave one field unpopulated.
*
* Only real values are applied: a null or absent field is left at whatever default the form set, so
* this can run after a step's own patchValue without blanking anything it deliberately defaulted.
*
* Patched twice, deliberately. Some of these controls drive others through a valueChanges
* subscription -- picking a Product Category auto-fills the four credit bureau account types, and
* the Details sub-group decides which Settings sections are shown. The first pass lets those
* subscriptions run so that dependent state is set up; the second pass repeats the same values with
* events suppressed, so a value the officer actually saved wins over whatever an auto-fill just
* wrote over it. Without the second pass, reopening a Personal Loan product silently reset its
* saved CIBIL account type back to the category default.
*/
export function patchCustomFields(form: UntypedFormGroup, product: any): void {
if (!form || !product) {
return;
}
const apply = (emitEvent: boolean) => {
for (const field of CUSTOM_UI_ONLY_FIELDS) {
const control = form.get(field);
if (!control) {
continue;
}
const value = product[field];
if (value === undefined || value === null) {
continue;
}
control.patchValue(value, { emitEvent });
}
};
apply(true);
apply(false);
}
@@ -8,6 +8,7 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges, inject } from '@angular/core';
import { UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
import { patchCustomFields } from '../../loan-product-custom-fields';
import { MatDialog } from '@angular/material/dialog';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
@@ -210,7 +211,14 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
intOverdueReserveAccountId: accountingMappings.intOverdueReserveAccount?.id || '',
undebitedInterestAccountId: accountingMappings.undebitedInterestAccount?.id || '',
penalIntCrGlAccountId: accountingMappings.penalIntCrGlAccount?.id || '',
rdMaturedGlAccountId: accountingMappings.rdMaturedGlAccount?.id || ''
rdMaturedGlAccountId: accountingMappings.rdMaturedGlAccount?.id || '',
glAccountId: accountingMappings.glAccount?.id || '',
icChargeGlAccountId: accountingMappings.icChargeGlAccount?.id || '',
inoperativeGlAccountId: accountingMappings.inoperativeGlAccount?.id || '',
creditBalanceLiabilityGlAccountId: accountingMappings.creditBalanceLiabilityGlAccount?.id || '',
creditBalanceAssetGlAccountId: accountingMappings.creditBalanceAssetGlAccount?.id || '',
overdueAccountsGlAccountId: accountingMappings.overdueAccountsGlAccount?.id || '',
rebateGlAccountId: accountingMappings.rebateGlAccount?.id || ''
});
if (this.loanProductsTemplate.paymentChannelToFundSourceMappings) {
@@ -234,6 +242,10 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
this.setOtherGlMappings('incomeOtherGlMappings', glMappings.income || [], this.incomeAccountData);
}
}
// Repopulate the legacy "Account Type Creation" fields saved against this product.
// Last, so a value actually stored wins over any default set above.
patchCustomFields(this.loanProductAccountingForm, this.loanProductsTemplate);
}
public createLoanProductAccountingForm() {
@@ -276,16 +288,10 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
'incomeFromGoodwillCreditInterestAccountId',
'incomeFromGoodwillCreditFeesAccountId',
'incomeFromGoodwillCreditPenaltyAccountId',
'receivableInterestAccountId',
'receivableFeeAccountId',
'receivablePenaltyAccountId',
'intReceivablesGlAccountId',
'intPayableGlAccountId',
'deafIntPayableGlAccountId',
'intOverdueReserveAccountId',
'undebitedInterestAccountId',
'penalIntCrGlAccountId',
'rdMaturedGlAccountId'
'glAccountId',
'inoperativeGlAccountId'
];
commonFields.forEach((field) => {
if (!this.loanProductAccountingForm.contains(field)) {
@@ -293,6 +299,44 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
}
});
// Accrual-only receivable fields: their input (mifosx-gl-account-selector) only renders
// when isAccrualAccounting() is true, so only require them in that case — otherwise they'd
// be unconditionally required with no visible field to fill them, permanently invalidating
// the form (accountingRule is hard-locked to Cash in this build).
const accrualOnlyFields = [
'receivableInterestAccountId',
'receivableFeeAccountId',
'receivablePenaltyAccountId'
];
accrualOnlyFields.forEach((field) => {
if (!this.loanProductAccountingForm.contains(field)) {
this.loanProductAccountingForm.addControl(
field,
new UntypedFormControl('', this.isAccrualAccounting() ? Validators.required : null)
);
}
});
// Optional custom GL account fields (legacy screenshot fields not marked mandatory) —
// no Validators.required, so mat-form-field does not render a required asterisk for these.
const optionalCommonFields = [
'deafIntPayableGlAccountId',
'intOverdueReserveAccountId',
'undebitedInterestAccountId',
'penalIntCrGlAccountId',
'rdMaturedGlAccountId',
'icChargeGlAccountId',
'creditBalanceLiabilityGlAccountId',
'creditBalanceAssetGlAccountId',
'overdueAccountsGlAccountId',
'rebateGlAccountId'
];
optionalCommonFields.forEach((field) => {
if (!this.loanProductAccountingForm.contains(field)) {
this.loanProductAccountingForm.addControl(field, new UntypedFormControl(''));
}
});
if (!this.loanProductAccountingForm.contains('advancedAccountingRules')) {
this.loanProductAccountingForm.addControl('advancedAccountingRules', new UntypedFormControl(false));
}
@@ -620,7 +664,7 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
}
public isAccrualAccounting(): boolean {
const accountingRule = this.loanProductAccountingForm.value.accountingRule;
const accountingRule = this.loanProductAccountingForm.get('accountingRule').value;
return accountingRule === 3 || accountingRule === 4;
}
@@ -708,15 +752,15 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
{
key: 'intReceivablesGlAccountId',
label: 'Int. Receivables GL A/c',
inputLabel: 'Int. Receivables GL A/c',
inputLabel: 'Int Receivables GL A/c',
controlName: 'intReceivablesGlAccountId',
options: this.assetAccountData,
required: false
required: true
},
{
key: 'deafIntPayableGlAccountId',
label: 'Deaf Int. Payble GL A/c',
inputLabel: 'Deaf Int. Payble GL A/c',
inputLabel: 'Deaf Int Payble GL A/c',
controlName: 'deafIntPayableGlAccountId',
options: this.assetAccountData,
required: false
@@ -724,7 +768,7 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
{
key: 'intOverdueReserveAccountId',
label: 'Int. Overdue Reserve A/c (AS)',
inputLabel: 'Int. Overdue Reserve A/c',
inputLabel: 'Int Overdue Reserve A/c',
controlName: 'intOverdueReserveAccountId',
options: this.assetAccountData,
required: false
@@ -736,6 +780,38 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
controlName: 'rdMaturedGlAccountId',
options: this.assetAccountData,
required: false
},
{
key: 'glAccountId',
label: 'GL A/c No.',
inputLabel: 'GL A/c No',
controlName: 'glAccountId',
options: this.assetAccountData,
required: true
},
{
key: 'inoperativeGlAccountId',
label: 'InOperative GL A/c',
inputLabel: 'InOperative GL A/c',
controlName: 'inoperativeGlAccountId',
options: this.assetAccountData,
required: true
},
{
key: 'creditBalanceAssetGlAccountId',
label: 'Cr.Balance Asset GL A/c',
inputLabel: 'Cr Balance Asset GL A/c',
controlName: 'creditBalanceAssetGlAccountId',
options: this.assetAccountData,
required: false
},
{
key: 'overdueAccountsGlAccountId',
label: 'Overdue Accounts GL A/c No.',
inputLabel: 'Overdue Accounts GL A/c No',
controlName: 'overdueAccountsGlAccountId',
options: this.assetAccountData,
required: false
}
],
otherMappingsControlName: 'assetsOtherGlMappings',
@@ -848,10 +924,18 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
{
key: 'penalIntCrGlAccountId',
label: 'Penal Int. Cr. GL A/c',
inputLabel: 'Penal Int. Cr. GL A/c',
inputLabel: 'Penal Int Cr GL A/c',
controlName: 'penalIntCrGlAccountId',
options: this.incomeAccountData,
required: false
},
{
key: 'icChargeGlAccountId',
label: 'I.C. Charge GL A/c No.',
inputLabel: 'IC Charge GL A/c No',
controlName: 'icChargeGlAccountId',
options: this.incomeAccountData,
required: false
}
],
otherMappingsControlName: 'incomeOtherGlMappings',
@@ -903,6 +987,14 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
options: this.expenseAccountData,
required: true,
deferredBuyDownMerchantOnly: true
},
{
key: 'rebateGlAccountId',
label: 'Rebate GL A/c No.',
inputLabel: 'Rebate GL A/c No',
controlName: 'rebateGlAccountId',
options: this.expenseAccountData,
required: false
}
],
otherMappingsControlName: 'expensesOtherGlMappings',
@@ -934,10 +1026,10 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
{
key: 'intPayableGlAccountId',
label: 'Int. Payable GL A/c',
inputLabel: 'Int. Payable GL A/c',
inputLabel: 'Int Payable GL A/c',
controlName: 'intPayableGlAccountId',
options: this.liabilityAccountData,
required: false
required: true
},
{
key: 'undebitedInterestAccountId',
@@ -946,6 +1038,14 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
controlName: 'undebitedInterestAccountId',
options: this.liabilityAccountData,
required: false
},
{
key: 'creditBalanceLiabilityGlAccountId',
label: 'Cr.Balance Liability GL A/c',
inputLabel: 'Cr Balance Liability GL A/c',
controlName: 'creditBalanceLiabilityGlAccountId',
options: this.liabilityAccountData,
required: false
}
],
otherMappingsControlName: 'liabilitiesOtherGlMappings',
@@ -6,7 +6,11 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<!-- Charges -->
<!--
Charges & Overdue Charges (native Fineract charge-picker sections) are temporarily
disabled — not currently in use (no matching backend charge data / not part of the
active workflow). Re-enable by removing this comment block when needed.
<div class="lpch-section">
<div class="lpch-section__header">
<i class="fas fa-receipt"></i>
@@ -89,7 +93,6 @@
</div>
</div>
<!-- Overdue Charges -->
<div class="lpch-section">
<div class="lpch-section__header">
<i class="fas fa-exclamation-circle"></i>
@@ -171,3 +174,174 @@
}
</div>
</div>
-->
<!-- Additional Charges (legacy Page 2/Page 4 charge fields) -->
<form [formGroup]="legacyChargesForm">
<div class="lpch-section">
<div class="lpch-section__header">
<i class="fas fa-money-check"></i>
<h3>Cheque &amp; Standing Instruction Charges</h3>
</div>
<div class="lpch-section__body lpch-grid-4">
<mat-form-field appearance="outline">
<mat-label>Chq. Book Charge</mat-label>
<mat-select formControlName="chequeBookChargeEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Chq. Return Charge</mat-label>
<mat-select formControlName="chequeReturnChargeEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>I.C. Charge Debit</mat-label>
<mat-select formControlName="icChargeDebitEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Cash Bundle Charge</mat-label>
<mat-select formControlName="cashBundleChargeEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Cash Bundle Charge With PAN Limit</mat-label>
<input matInput type="number" formControlName="cashBundleChargeWithPanLimit" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Per Cheque Entry Charge</mat-label>
<input matInput type="number" formControlName="perChequeEntryCharge" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Per Cheque Entry Service Charge</mat-label>
<input matInput type="number" formControlName="perChequeEntryServiceCharge" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Per Inward Cheque Entry Charge</mat-label>
<input matInput type="number" formControlName="perInwardChequeEntryCharge" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Per Outward Cheque Entry Charge</mat-label>
<input matInput type="number" formControlName="perOutwardChequeEntryCharge" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Creditor Standing Instr. Fail Charge</mat-label>
<input matInput type="number" formControlName="creditorStandingInstructionFailCharge" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Debtor Standing Instr. Fail Charge</mat-label>
<input matInput type="number" formControlName="debtorStandingInstructionFailCharge" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Calculate Daily Against Clearing Charge</mat-label>
<mat-select formControlName="calculateDailyAgainstClearingCharge">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="lpch-section">
<div class="lpch-section__header">
<i class="fas fa-wallet"></i>
<h3>Minimum Balance Charges</h3>
</div>
<div class="lpch-section__body lpch-grid-4">
<mat-form-field appearance="outline">
<mat-label>Minimum Balance Checking</mat-label>
<mat-select formControlName="minimumBalanceCheckingType">
<mat-option value="STRICT">Strict</mat-option>
<mat-option value="AVERAGE">Average</mat-option>
<mat-option value="LENIENT">Lenient</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Auto Minimum Balance Charge</mat-label>
<mat-select formControlName="autoMinimumBalanceCharge">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Calculate Daily Min. Balance Charges</mat-label>
<mat-select formControlName="calculateDailyMinimumBalanceCharges">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Minimum Balance Charge</mat-label>
<mat-select formControlName="minimumBalanceChargeEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Min. Balance Charge (Rs.)</mat-label>
<input matInput type="number" formControlName="minimumBalanceChargeAmount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Min. Balance Charge For Cheque Issue (Rs.)</mat-label>
<input matInput type="number" formControlName="minimumBalanceChargeForChequeIssue" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Min. Balance Charge Method</mat-label>
<mat-select formControlName="minimumBalanceChargeMethod">
<mat-option value="FLAT">Flat</mat-option>
<mat-option value="SLAB">Slab</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Months After Min. Balance Charge Calculate</mat-label>
<input matInput type="number" formControlName="monthsAfterMinBalanceChargeCalculate" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>For Cheque Issue</mat-label>
<input matInput type="number" formControlName="forChequeIssue" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>For Cheque Issue (Avg.)</mat-label>
<input matInput type="number" formControlName="forChequeIssueAvg" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>For Withdrawal Slip</mat-label>
<input matInput type="number" formControlName="forWithdrawalSlip" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>For Withdrawal Slip (Avg.)</mat-label>
<input matInput type="number" formControlName="forWithdrawalSlipAvg" />
</mat-form-field>
</div>
</div>
</form>
@@ -88,3 +88,27 @@ $gray-900: #202124;
.lpch-table {
width: 100%;
}
.lpch-section__body {
display: grid;
grid-template-columns: repeat(2, 1fr);
align-items: start;
gap: 18px 16px;
padding: 20px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.lpch-section__body.lpch-grid-4 {
grid-template-columns: repeat(4, 1fr);
@media (width <= 1100px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
@@ -7,7 +7,8 @@
*/
import { Component, Input, OnInit, inject } from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { UntypedFormControl, UntypedFormBuilder, UntypedFormGroup, ReactiveFormsModule } from '@angular/forms';
import { patchCustomFields } from '../../loan-product-custom-fields';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
@@ -51,12 +52,16 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatRow,
ChargesFilterPipe,
ChargesPenaltyFilterPipe,
FormatNumberPipe
FormatNumberPipe,
ReactiveFormsModule
]
})
export class LoanProductChargesStepComponent implements OnInit {
dialog = inject(MatDialog);
private translateService = inject(TranslateService);
private formBuilder = inject(UntypedFormBuilder);
legacyChargesForm: UntypedFormGroup;
@Input() loanProductsTemplate: any;
@Input() currencyCode: UntypedFormControl;
@@ -76,6 +81,48 @@ export class LoanProductChargesStepComponent implements OnInit {
pristine = true;
constructor() {
this.createLegacyChargesForm();
}
createLegacyChargesForm() {
this.legacyChargesForm = this.formBuilder.group({
// Cheque-related charges (legacy "A/c Mas Scr2 Para")
chequeBookChargeEnabled: ['NO'],
chequeReturnChargeEnabled: ['NO'],
icChargeDebitEnabled: ['NO'],
cashBundleChargeEnabled: ['NO'],
cashBundleChargeWithPanLimit: [''],
// Cheque transaction charges (legacy Page 4)
perChequeEntryCharge: [''],
perChequeEntryServiceCharge: [''],
perInwardChequeEntryCharge: [''],
perOutwardChequeEntryCharge: [''],
// Standing Instruction fail charges (legacy Page 4)
creditorStandingInstructionFailCharge: [''],
debtorStandingInstructionFailCharge: [''],
// Minimum Balance Information (legacy Page 2 + Page 4)
minimumBalanceCheckingType: [''],
autoMinimumBalanceCharge: [''],
calculateDailyMinimumBalanceCharges: ['NO'],
minimumBalanceChargeEnabled: ['NO'],
minimumBalanceChargeAmount: [''],
minimumBalanceChargeForChequeIssue: [''],
minimumBalanceChargeMethod: [''],
monthsAfterMinBalanceChargeCalculate: [''],
forChequeIssue: [''],
forChequeIssueAvg: [''],
forWithdrawalSlip: [''],
forWithdrawalSlipAvg: [''],
// Against Clearing charge (legacy Page 4)
calculateDailyAgainstClearingCharge: ['NO']
});
}
ngOnInit() {
this.chargeData = this.loanProductsTemplate.chargeOptions;
this.overdueChargeData = this.loanProductsTemplate.penaltyOptions
@@ -89,6 +136,10 @@ export class LoanProductChargesStepComponent implements OnInit {
this.currencyCode.valueChanges.subscribe(() => (this.chargesDataSource = []));
this.multiDisburseLoan.valueChanges.subscribe(() => (this.chargesDataSource = []));
// Repopulate the legacy "Account Type Creation" fields saved against this product.
// Last, so a value actually stored wins over any default set above.
patchCustomFields(this.legacyChargesForm, this.loanProductsTemplate);
}
addCharge(charge: any) {
@@ -112,7 +163,8 @@ export class LoanProductChargesStepComponent implements OnInit {
get loanProductCharges() {
return {
charges: this.chargesDataSource
charges: this.chargesDataSource,
...this.legacyChargesForm.value
};
}
}
@@ -0,0 +1,11 @@
{
"folders": [
{
"path": "../../../../../.."
},
{
"path": "../../../../../../../cbsnexora-backend"
}
],
"settings": {}
}
@@ -7,40 +7,144 @@
-->
<form [formGroup]="loanProductDetailsForm">
<!-- Product Classification + General Information — merged single section, 2-column layout -->
<div class="lpd-section">
<div class="lpd-section__header">
<i class="fas fa-info-circle"></i>
<h3>{{ 'labels.inputs.Product Name' | translate }} &amp; {{ 'labels.inputs.Description' | translate }}</h3>
<h3>General Information</h3>
</div>
<div class="lpd-grid">
<mat-form-field appearance="outline" class="lpd-field lpd-field--full">
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label>
<input
matInput
formControlName="name"
matTooltip="{{ 'tooltips.Product name is a unique identifier' | translate }}"
required
/>
<mat-error>
{{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<div class="lpd-two-col">
<div class="lpd-two-col__column">
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Account Type Group</mat-label>
<mat-select formControlName="productTypeGroup">
@for (group of productTypeGroups; track group.id) {
<mat-option [value]="group.id">{{ group.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>{{ 'labels.inputs.Short Name' | translate }}</mat-label>
<input
matInput
formControlName="shortName"
maxlength="4"
matTooltip="{{ 'tooltips.Short name is a unique identifier' | translate }}"
required
/>
<mat-error>
{{ 'labels.inputs.Short Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
@if (availableSubGroups.length) {
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Product Type Sub Group</mat-label>
<mat-select formControlName="productTypeSubGroup">
@for (subGroup of availableSubGroups; track subGroup.id) {
<mat-option [value]="subGroup.id">{{ subGroup.label }}</mat-option>
}
</mat-select>
</mat-form-field>
}
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>A/c. Type Description</mat-label>
<input
matInput
formControlName="name"
matTooltip="{{ 'tooltips.Product name is a unique identifier' | translate }}"
required
/>
<mat-error>
{{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>{{ 'labels.inputs.Short Name' | translate }}</mat-label>
<input
matInput
formControlName="shortName"
maxlength="4"
matTooltip="{{ 'tooltips.Short name is a unique identifier' | translate }}"
required
/>
<mat-error>
{{ 'labels.inputs.Short Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>A/c Type Code</mat-label>
<input matInput formControlName="accountTypeCode" maxlength="2" />
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>MICR Trn. Code</mat-label>
<input matInput formControlName="micrTransactionCode" />
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Reserved Code Srno For ECS</mat-label>
<input matInput formControlName="reservedEcsSerialNumber" />
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Reverse ECS No.</mat-label>
<input matInput formControlName="reverseEcsNumber" />
</mat-form-field>
</div>
<div class="lpd-two-col__column">
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Lock Cheque Info</mat-label>
<mat-select formControlName="lockChequeInfoEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Lock Page 2 Info</mat-label>
<mat-select formControlName="lockPageTwoInfoEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field" (click)="computerizationDatePicker.open()">
<mat-label>Computerization Date</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="computerizationDatePicker"
formControlName="computerizationDate"
/>
<mat-datepicker-toggle matSuffix [for]="computerizationDatePicker"></mat-datepicker-toggle>
<mat-datepicker #computerizationDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>PFMS Account Type</mat-label>
<input matInput formControlName="pfmsAccountType" />
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Chq. Print Description</mat-label>
<input matInput formControlName="chequePrintDescription" />
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>POS A/c Type Description</mat-label>
<input matInput formControlName="posAccountTypeDescription" />
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Renewal Dt. Add Months</mat-label>
<input matInput type="number" formControlName="renewalDateAddMonths" />
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Renewal Grace Days</mat-label>
<input matInput type="number" formControlName="renewalGraceDays" />
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Doc. Renewal Dt. Add Months</mat-label>
<input matInput type="number" formControlName="documentRenewalDateAddMonths" />
</mat-form-field>
</div>
</div>
</div>
@@ -97,86 +201,94 @@
<i class="fas fa-percentage"></i>
<h3>Interest Information</h3>
</div>
<div class="lpd-grid">
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Debit Int. Calculation</mat-label>
<mat-select formControlName="debitIntCalculation">
@for (opt of yesNoOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Separate Penal Int.</mat-label>
<mat-select formControlName="separatePenalInterest">
@for (opt of yesNoOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Credit Int. Calculation</mat-label>
<mat-select formControlName="creditIntCalculation">
@for (opt of yesNoOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Sub A/c Type Balance</mat-label>
<mat-select formControlName="subAccountTypeBalance">
@for (opt of yesNoOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Final Int. Rounding</mat-label>
<mat-select formControlName="finalIntRounding">
@for (opt of yesNoOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
</mat-form-field>
@if (loanProductDetailsForm.get('finalIntRounding')?.value) {
<div class="lpd-two-col">
<div class="lpd-two-col__column">
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Rounding UpTo</mat-label>
<input matInput type="number" formControlName="roundingUpTo" min="0" />
<mat-label>Debit Int. Calculation</mat-label>
<mat-select formControlName="debitIntCalculation">
@for (opt of yesNoOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
</mat-form-field>
}
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Separate Penal Int.</mat-label>
<mat-select formControlName="separatePenalInterest">
@for (opt of yesNoOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Final Int. Rounding</mat-label>
<mat-select formControlName="finalIntRounding">
@for (opt of yesNoOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
</mat-form-field>
@if (loanProductDetailsForm.get('finalIntRounding')?.value) {
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Rounding UpTo</mat-label>
<input matInput type="number" formControlName="roundingUpTo" min="0" />
</mat-form-field>
}
</div>
<div class="lpd-two-col__column">
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Credit Int. Calculation</mat-label>
<mat-select formControlName="creditIntCalculation">
@for (opt of yesNoOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Sub A/c Type Balance</mat-label>
<mat-select formControlName="subAccountTypeBalance">
@for (opt of yesNoOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<div class="lpd-info-note">
<i class="fas fa-circle-info"></i>
GL Account mappings for these fields (Int. Receivables, Penal Int. Cr., RD Matured, Int. Payable, Deaf Int.
Payable, Int. Overdue Reserve, UnDebited Interest) are available in the <strong>Accounting</strong> tab.
</div>
</div>
<!-- Product Classification + GL Account — combined section -->
<!-- PPS Limit & Write Off Details (legacy Page 1 bottom fields) -->
<div class="lpd-section">
<div class="lpd-section__header">
<i class="fas fa-sitemap"></i>
<h3>Product Classification</h3>
<i class="fas fa-ban"></i>
<h3>PPS Limit &amp; Write Off Details</h3>
</div>
<div class="lpd-grid">
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Product Type Group</mat-label>
<mat-select formControlName="productTypeGroup">
@for (group of productTypeGroups; track group.id) {
<mat-option [value]="group.id">{{ group.label }}</mat-option>
}
<mat-label>PPS Limit</mat-label>
<input matInput type="number" formControlName="ppsLimit" />
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Is this Actyp Write Off?</mat-label>
<mat-select formControlName="isWriteOffAccountType">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
@if (loanProductDetailsForm.get('productTypeGroup')?.value) {
@if (loanProductDetailsForm.get('isWriteOffAccountType')?.value === 'YES') {
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>GL Account ({{ glClassificationLabel }})</mat-label>
<mat-select formControlName="glAccount">
@for (acc of availableGlAccounts; track acc.id) {
<mat-option [value]="acc.id"> ({{ acc.glCode }}) {{ acc.name }} </mat-option>
}
</mat-select>
<mat-error>GL Account is required</mat-error>
<mat-label>Write Off Actyp</mat-label>
<input matInput formControlName="writeOffAccountTypeCode" />
</mat-form-field>
}
</div>
@@ -46,9 +46,10 @@ $gray-900: #202124;
.lpd-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
align-items: start;
gap: 18px 16px;
padding: 20px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
@@ -57,6 +58,7 @@ $gray-900: #202124;
.lpd-field {
width: 100%;
min-width: 0;
&--full {
grid-column: 1 / -1;
@@ -69,6 +71,43 @@ $gray-900: #202124;
padding: 4px 0;
}
// Two independent vertical columns of fields, side by side
.lpd-two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 40px;
padding: 20px 24px;
align-items: start;
@media (width <= 700px) {
grid-template-columns: 1fr;
}
&__column {
display: flex;
flex-direction: column;
}
}
.lpd-info-note {
display: flex;
align-items: flex-start;
gap: 8px;
margin: 0 24px 16px;
padding: 10px 14px;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 8px;
font-size: 12.5px;
color: #1e3a8a;
line-height: 1.5;
i {
color: #2563eb;
margin-top: 2px;
}
}
// GL Section
.lpd-gl-section {
border-top: 2px solid #e0f2fe;
@@ -7,8 +7,9 @@
*/
/** Angular Imports */
import { Component, OnInit, Input, inject } from '@angular/core';
import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { CUSTOM_DATE_FIELDS, patchCustomFields } from '../../loan-product-custom-fields';
import { Dates } from 'app/core/utils/dates';
/** Custom Services */
@@ -33,6 +34,7 @@ export class LoanProductDetailsStepComponent implements OnInit {
private settingsService = inject(SettingsService);
@Input() loanProductsTemplate: any;
@Output() productSubGroupChange = new EventEmitter<string>();
loanProductDetailsForm: UntypedFormGroup;
@@ -49,6 +51,10 @@ export class LoanProductDetailsStepComponent implements OnInit {
this.createLoanProductDetailsForm();
}
assetAccountOptions: any[] = [];
incomeAccountOptions: any[] = [];
liabilityAccountOptions: any[] = [];
ngOnInit() {
this.loanProductDetailsForm.patchValue({
name: this.loanProductsTemplate.name,
@@ -58,10 +64,6 @@ export class LoanProductDetailsStepComponent implements OnInit {
closeDate: this.loanProductsTemplate.closeDate && new Date(this.loanProductsTemplate.closeDate)
});
const mappingOptions = this.loanProductsTemplate?.accountingMappingOptions ?? {};
this.assetAccountOptions = mappingOptions.assetAccountOptions ?? [];
this.liabilityAccountOptions = mappingOptions.liabilityAccountOptions ?? [];
this.loanProductDetailsForm.patchValue({
debitIntCalculation: this.loanProductsTemplate.debitIntCalculation ?? false,
separatePenalInterest: this.loanProductsTemplate.separatePenalInterest ?? false,
@@ -70,66 +72,63 @@ export class LoanProductDetailsStepComponent implements OnInit {
finalIntRounding: this.loanProductsTemplate.finalIntRounding ?? false,
roundingUpTo: this.loanProductsTemplate.roundingUpTo ?? null
});
const mappingOptions = this.loanProductsTemplate?.accountingMappingOptions ?? {};
this.assetAccountOptions = mappingOptions.assetAccountOptions ?? [];
this.incomeAccountOptions = mappingOptions.incomeAccountOptions ?? [];
this.liabilityAccountOptions = mappingOptions.liabilityAccountOptions ?? [];
const accountingMappings = this.loanProductsTemplate?.accountingMappings;
if (accountingMappings) {
this.loanProductDetailsForm.patchValue({
intReceivablesGlAccountId: accountingMappings.intReceivablesGlAccount?.id || '',
penalIntCrGlAccountId: accountingMappings.penalIntCrGlAccount?.id || '',
rdMaturedGlAccountId: accountingMappings.rdMaturedGlAccount?.id || '',
intPayableGlAccountId: accountingMappings.intPayableGlAccount?.id || '',
deafIntPayableGlAccountId: accountingMappings.deafIntPayableGlAccount?.id || '',
intOverdueReserveAccountId: accountingMappings.intOverdueReserveAccount?.id || '',
undebitedInterestAccountId: accountingMappings.undebitedInterestAccount?.id || ''
});
}
// Repopulate the legacy "Account Type Creation" fields saved against this product.
// Last, so a value actually stored wins over any default set above.
patchCustomFields(this.loanProductDetailsForm, this.loanProductsTemplate);
}
/** Product Classification data */
readonly productTypeGroups = [
{ id: 'loans', label: 'Loan' },
{ id: 'advanced', label: 'Advanced' }
{ id: 'advances', label: 'Advances A/C' },
{ id: 'loans', label: 'Loans A/C' }
];
readonly productTypeSubGroups: Record<string, { id: string; label: string }[]> = {
loans: [
{ id: 'emi', label: 'EMI' },
{ id: 'cc', label: 'CC (Cash Credit)' },
{ id: 'overdraft', label: 'Overdraft' },
{ id: 'warehouse', label: 'Warehouse' },
{ id: 'gold', label: 'Gold' }
// Renewable, Drawing-Power/limit-based revolving credit facilities
advances: [
{ id: 'cc', label: 'Cash Credit (CC)' },
{ id: 'overdraft', label: 'Overdraft (OD)' },
{ id: 'gold', label: 'Gold Loan' },
{ id: 'warehouse', label: 'Warehouse Receipt / Pledge Loan' },
{ id: 'kcc', label: 'Kisan Credit Card (KCC)' },
{ id: 'bills', label: 'Bills Purchased / Discounted' }
],
advanced: [
{ id: 'fd', label: 'FD (Fixed Deposit)' },
{ id: 'rd', label: 'RD (Recurring Deposit)' }
// Fixed disbursement, fixed tenure, EMI-based term credit facilities
loans: [
{ id: 'term', label: 'Term Loan / Long Term Loan' },
{ id: 'personal', label: 'Personal Loan' },
{ id: 'housing', label: 'Housing Loan' },
{ id: 'vehicle', label: 'Vehicle Loan' },
{ id: 'education', label: 'Education Loan' },
{ id: 'business', label: 'Business / MSME Term Loan' },
{ id: 'gold_emi', label: 'Gold EMI Loan' }
]
};
/** Default GL per product type group */
private readonly glDefaultMap: Record<string, string> = {
loans: 'asset',
advanced: 'asset'
};
get availableSubGroups() {
const group = this.loanProductDetailsForm.get('productTypeGroup')?.value;
return group ? (this.productTypeSubGroups[group] ?? []) : [];
}
get glOptions(): string[] {
return [
'asset',
'liability'
];
}
assetAccountOptions: any[] = [];
liabilityAccountOptions: any[] = [];
get availableGlAccounts(): any[] {
const type = this.loanProductDetailsForm.get('glClassification')?.value;
if (type === 'asset') return this.assetAccountOptions;
if (type === 'liability') return this.liabilityAccountOptions;
return [];
}
get glClassificationLabel(): string {
const type = this.loanProductDetailsForm.get('glClassification')?.value;
return type === 'liability' ? 'Liability' : 'Asset';
}
get selectedGroupLabel(): string {
const group = this.loanProductDetailsForm.get('productTypeGroup')?.value;
return this.productTypeGroups.find((g) => g.id === group)?.label ?? '';
}
readonly yesNoOptions = [
{ value: true, label: 'YES' },
{ value: false, label: 'NO' }
@@ -150,28 +149,46 @@ export class LoanProductDetailsStepComponent implements OnInit {
closeDate: [''],
productTypeGroup: [''],
productTypeSubGroup: [''],
glClassification: ['asset'],
glAccount: [''],
debitIntCalculation: [false],
separatePenalInterest: [false],
creditIntCalculation: [false],
subAccountTypeBalance: [false],
finalIntRounding: [false],
roundingUpTo: [null]
roundingUpTo: [null],
// General Information (legacy Page 1 header fields)
accountTypeCode: [''],
micrTransactionCode: [''],
reservedEcsSerialNumber: [''],
reverseEcsNumber: [''],
lockChequeInfoEnabled: ['NO'],
lockPageTwoInfoEnabled: ['NO'],
computerizationDate: [''],
chequePrintDescription: [''],
posAccountTypeDescription: [''],
pfmsAccountType: [''],
renewalDateAddMonths: [''],
renewalGraceDays: [''],
documentRenewalDateAddMonths: [''],
// PPS Limit & Write Off Details
ppsLimit: [''],
isWriteOffAccountType: ['NO'],
writeOffAccountTypeCode: ['']
});
// Auto-set GL based on product type group
this.loanProductDetailsForm.get('productTypeGroup')?.valueChanges.subscribe((group: string) => {
const defaultVal = this.glDefaultMap[group] ?? 'asset';
this.loanProductDetailsForm.get('glClassification')?.setValue(defaultVal);
this.loanProductDetailsForm.get('glAccount')?.setValue('');
this.loanProductDetailsForm.get('productTypeSubGroup')?.valueChanges.subscribe((subGroup: string) => {
this.productSubGroupChange.emit(subGroup);
});
}
get loanProductDetails() {
const loanProductDetailsFormData = this.loanProductDetailsForm.value;
const prevStartDate: Date = this.loanProductDetailsForm.value.startDate;
const prevCloseDate: Date = this.loanProductDetailsForm.value.closeDate;
// getRawValue() rather than value: it returns a fresh object, so the date reformatting below
// cannot write back into the form's own cached value, and any control disabled later still
// reaches the preview step instead of silently vanishing from it.
const loanProductDetailsFormData = this.loanProductDetailsForm.getRawValue();
const prevStartDate: Date = loanProductDetailsFormData.startDate;
const prevCloseDate: Date = loanProductDetailsFormData.closeDate;
const dateFormat = this.settingsService.dateFormat;
if (loanProductDetailsFormData.startDate instanceof Date) {
loanProductDetailsFormData.startDate = this.dateUtils.formatDate(prevStartDate, dateFormat) || '';
@@ -179,6 +196,17 @@ export class LoanProductDetailsStepComponent implements OnInit {
if (loanProductDetailsFormData.closeDate instanceof Date) {
loanProductDetailsFormData.closeDate = this.dateUtils.formatDate(prevCloseDate, dateFormat) || '';
}
// The legacy date fields need the same treatment: their control holds a Date, but the API parses
// them against the dateFormat sent alongside, and rejects the raw value outright. Driven off
// CUSTOM_DATE_FIELDS rather than named here, so a legacy date added later is covered too.
for (const field of CUSTOM_DATE_FIELDS) {
const value = loanProductDetailsFormData[field];
if (value instanceof Date) {
loanProductDetailsFormData[field] = this.dateUtils.formatDate(value, dateFormat) || '';
}
}
return loanProductDetailsFormData;
}
}
@@ -235,209 +235,361 @@
</div>
</div>
<!-- Cheque Book Settings -->
<div class="lps-section">
<div class="lps-section__header">
<i class="fas fa-money-check lps-section__icon"></i>
<h3 class="lps-section__title">Cheque Book Settings</h3>
</div>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline">
<mat-label>Cheque Book Issue Enabled</mat-label>
<mat-select formControlName="chequeBookIssueEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-accordion class="lps-accordion" multi="true">
@if (showChequeBookSettings) {
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<i class="fas fa-money-check lps-section__icon"></i>
Cheque Book Settings
</mat-panel-title>
</mat-expansion-panel-header>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline">
<mat-label>Cheque Book Issue Enabled</mat-label>
<mat-select formControlName="chequeBookIssueEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Cheque Validation Required</mat-label>
<mat-select formControlName="chequeValidationRequired">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Cheque Validation Required</mat-label>
<mat-select formControlName="chequeValidationRequired">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Is First Cheque Book Free</mat-label>
<mat-select formControlName="isFirstChequeBookFree">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Is First Cheque Book Free</mat-label>
<mat-select formControlName="isFirstChequeBookFree">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Cheque Printing Account Type</mat-label>
<mat-select formControlName="chequePrintingAccountType">
<mat-option value="SAVINGS_BANK">Savings Bank</mat-option>
<mat-option value="CURRENT_ACCOUNT">Current Account</mat-option>
<mat-option value="CASH_CREDIT">Cash Credit</mat-option>
<mat-option value="OVERDRAFT">Overdraft</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Cheque Printing Account Type</mat-label>
<mat-select formControlName="chequePrintingAccountType">
<mat-option value="SAVINGS_BANK">Savings Bank</mat-option>
<mat-option value="CURRENT_ACCOUNT">Current Account</mat-option>
<mat-option value="CASH_CREDIT">Cash Credit</mat-option>
<mat-option value="OVERDRAFT">Overdraft</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Allow Withdrawal Slip Usage</mat-label>
<mat-select formControlName="allowWithdrawalSlipUsage">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Allow Withdrawal Slip Usage</mat-label>
<mat-select formControlName="allowWithdrawalSlipUsage">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Maintain Cheque Book Stock Registry</mat-label>
<mat-select formControlName="maintainChequeBookStockRegistry">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Maintain Cheque Book Stock Registry</mat-label>
<mat-select formControlName="maintainChequeBookStockRegistry">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Free Cheque Books Per Year</mat-label>
<input type="number" matInput formControlName="freeChequeBooksPerYear" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Free Cheque Books Per Year</mat-label>
<input type="number" matInput formControlName="freeChequeBooksPerYear" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Allow Credit By Cheque Number</mat-label>
<mat-select formControlName="allowCreditByChequeNumber">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Allow Credit By Cheque Number</mat-label>
<mat-select formControlName="allowCreditByChequeNumber">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Credit Cheque Validation Required</mat-label>
<mat-select formControlName="creditChequeValidationRequired">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Credit Cheque Validation Required</mat-label>
<mat-select formControlName="creditChequeValidationRequired">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Grace Days For Stock Statement</mat-label>
<input type="number" matInput formControlName="graceDaysForStockStatement" />
</mat-form-field>
</div>
</div>
<mat-form-field appearance="outline">
<mat-label>Grace Days For Stock Statement</mat-label>
<input type="number" matInput formControlName="graceDaysForStockStatement" />
</mat-form-field>
</div>
</mat-expansion-panel>
}
<!-- Additional Product Settings -->
<div class="lps-section">
<div class="lps-section__header">
<i class="fas fa-cogs lps-section__icon"></i>
<h3 class="lps-section__title">Additional Product Settings</h3>
</div>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline">
<mat-label>Account Type Active For Transactions</mat-label>
<mat-select formControlName="accountTypeActiveForTransactions">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<i class="fas fa-cogs lps-section__icon"></i>
Additional Product Settings
</mat-panel-title>
</mat-expansion-panel-header>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline">
<mat-label>Account Type Active For Transactions</mat-label>
<mat-select formControlName="accountTypeActiveForTransactions">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Sub Type Activity Enabled</mat-label>
<mat-select formControlName="subTypeActivityEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Sub Type Activity Enabled</mat-label>
<mat-select formControlName="subTypeActivityEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Account Opening Charges</mat-label>
<input type="number" matInput formControlName="accountOpeningCharges" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Account Opening Charges</mat-label>
<input type="number" matInput formControlName="accountOpeningCharges" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Maximum Gold Loan Limit</mat-label>
<input type="number" matInput formControlName="maximumGoldLoanLimit" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Maximum Gold Loan Limit</mat-label>
<input type="number" matInput formControlName="maximumGoldLoanLimit" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Last Account Number Opened</mat-label>
<input type="text" matInput formControlName="lastAccountNumberOpened" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Gold Loan [Packet No.] With Year?</mat-label>
<mat-select formControlName="goldLoanPacketNumberWithYearEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Customer Termination Allowed</mat-label>
<mat-select formControlName="customerTerminationAllowed">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Last Account Number Opened</mat-label>
<input type="text" matInput formControlName="lastAccountNumberOpened" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Charges For Cash Credit Above Limit</mat-label>
<input type="number" matInput formControlName="chargesForCashCreditAboveLimit" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Customer Termination Allowed</mat-label>
<mat-select formControlName="customerTerminationAllowed">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Death Interest GL Account</mat-label>
<input type="text" matInput formControlName="deathInterestGLAccount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Charges For Cash Credit Above Limit</mat-label>
<input type="number" matInput formControlName="chargesForCashCreditAboveLimit" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Starting Ledger Number</mat-label>
<input type="number" matInput formControlName="startingLedgerNumber" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Death Interest GL Account</mat-label>
<input type="text" matInput formControlName="deathInterestGLAccount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Ending Ledger Number</mat-label>
<input type="number" matInput formControlName="endingLedgerNumber" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Starting Ledger Number</mat-label>
<input type="number" matInput formControlName="startingLedgerNumber" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Current Ledger Number</mat-label>
<input type="number" matInput formControlName="currentLedgerNumber" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Ending Ledger Number</mat-label>
<input type="number" matInput formControlName="endingLedgerNumber" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Remittance Account Number</mat-label>
<input type="text" matInput formControlName="remittanceAccountNumber" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Current Ledger Number</mat-label>
<input type="number" matInput formControlName="currentLedgerNumber" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Is Non Resident Indian Account</mat-label>
<mat-select formControlName="isNonResidentIndianAccount">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Remittance Account Number</mat-label>
<input type="text" matInput formControlName="remittanceAccountNumber" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Debit Clearing GL Account</mat-label>
<input type="text" matInput formControlName="debitClearingGLAccount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Is Non Resident Indian Account</mat-label>
<mat-select formControlName="isNonResidentIndianAccount">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Missed Call Alert Service Active</mat-label>
<mat-select formControlName="missedCallAlertServiceActive">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Debit Clearing GL Account</mat-label>
<input type="text" matInput formControlName="debitClearingGLAccount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Standing Instruction Charge GL Account</mat-label>
<input type="text" matInput formControlName="standingInstructionChargeGLAccount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Missed Call Alert Service Active</mat-label>
<mat-select formControlName="missedCallAlertServiceActive">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Standing Instruction Debit Charge Skip Option</mat-label>
<mat-select formControlName="standingInstructionDebitChargeSkipOption">
<mat-option value="SKIP_ALWAYS">Skip Always</mat-option>
<mat-option value="SKIP_ON_FIRST_FAILURE">Skip on First Failure</mat-option>
<mat-option value="NEVER_SKIP">Never Skip</mat-option>
<mat-option value="RETRY_AFTER_24_HOURS">Retry after 24 Hours</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Standing Instruction Charge GL Account</mat-label>
<input type="text" matInput formControlName="standingInstructionChargeGLAccount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Opening Balance Multiplier For Remittance Charges</mat-label>
<input type="number" matInput formControlName="openingBalanceMultiplierForRemittanceCharges" />
</mat-form-field>
</div>
</div>
<mat-form-field appearance="outline">
<mat-label>Standing Instruction Debit Charge Skip Option</mat-label>
<mat-select formControlName="standingInstructionDebitChargeSkipOption">
<mat-option value="SKIP_ALWAYS">Skip Always</mat-option>
<mat-option value="SKIP_ON_FIRST_FAILURE">Skip on First Failure</mat-option>
<mat-option value="NEVER_SKIP">Never Skip</mat-option>
<mat-option value="RETRY_AFTER_24_HOURS">Retry after 24 Hours</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Opening Balance Multiplier For Remittance Charges</mat-label>
<input type="number" matInput formControlName="openingBalanceMultiplierForRemittanceCharges" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>A/c Move To InOperative A/c</mat-label>
<mat-select formControlName="accountMoveToInoperativeEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Generate GL Diff Entry</mat-label>
<mat-select formControlName="generateGlDiffEntryEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Sub Type Wise GL Balance Gen</mat-label>
<mat-select formControlName="subTypeWiseGlBalanceGenerationEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Want To Validation While Deleting Debit Entry</mat-label>
<mat-select formControlName="validateBeforeDeletingDebitEntryEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Want To RC File Concept</mat-label>
<mat-select formControlName="rcFileConceptEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>A/c Close With Zero Bal. In Open Day Process</mat-label>
<mat-select formControlName="accountCloseZeroBalanceInOpenDayProcess">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>A/c Close When Bal. Zero At Subled Tran Entry</mat-label>
<mat-select formControlName="accountCloseZeroBalanceAtSubledgerTranEntry">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Lmt.Enh/Ag.Clg Screen Display</mat-label>
<mat-select formControlName="limitEnhancementAgainstClearingScreenDisplayEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Advance Int. Param In Ledger</mat-label>
<mat-select formControlName="advanceInterestParameterInLedgerEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Vehicle Detail In Master</mat-label>
<mat-select formControlName="vehicleDetailInMasterEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Deleted Chq. Issue</mat-label>
<mat-select formControlName="deletedChequeIssueEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>DP Reduce In Doc. Renewal Date</mat-label>
<mat-select formControlName="dpReduceInDocumentRenewalDateEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>DP Reduce In Renewal Date</mat-label>
<mat-select formControlName="dpReduceInRenewalDateEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Auto Close Day-End</mat-label>
<mat-select formControlName="autoCloseAtDayEndEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Loan Rate From Master</mat-label>
<mat-select formControlName="loanRateFromMasterEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Cheque Short Name</mat-label>
<input type="text" matInput formControlName="chequeShortName" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Statement Print</mat-label>
<mat-select formControlName="statementPrintEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>NESL Code</mat-label>
<input type="text" matInput formControlName="neslCode" />
</mat-form-field>
</div>
</mat-expansion-panel>
</mat-accordion>
<!-- Interest Related Parameters (Page 3) -->
<div class="lps-section">
@@ -754,6 +906,85 @@
<mat-label>Add Penal Interest Rate</mat-label>
<input type="number" matInput formControlName="addPenalInterestRate" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Atyp14 Simple Int.</mat-label>
<mat-select formControlName="atyp14SimpleInterestEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>FDOD Margin Type</mat-label>
<input type="text" matInput formControlName="fixedDepositOverdraftMarginType" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Check Master Detail Edited (Passing Pend.)</mat-label>
<mat-select formControlName="checkMasterDetailEditedPassingPendingEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Credit/Debit Int. Calc. Separate</mat-label>
<mat-select formControlName="creditDebitInterestCalculationSeparateEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>On Time Interest Provision</mat-label>
<mat-select formControlName="onTimeInterestProvisionEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Check Against Clearing</mat-label>
<mat-select formControlName="checkAgainstClearingEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>GST On Penalty</mat-label>
<mat-select formControlName="gstOnPenaltyEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Penalty Int. Grace Days</mat-label>
<input type="number" min="0" matInput formControlName="penaltyInterestGraceDays" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Over Drawn Int. Rate</mat-label>
<input type="number" matInput formControlName="overdrawnInterestRate" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Calculate Daily Limit Enhancement (TOD)</mat-label>
<mat-select formControlName="calculateDailyLimitEnhancementTodEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>FDOD Multiple Int. Rate</mat-label>
<mat-select formControlName="fixedDepositOverdraftMultipleInterestRateEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
@@ -801,6 +1032,306 @@
</div>
</div>
<mat-accordion class="lps-accordion" multi="true">
@if (showChannelLimits) {
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<i class="fas fa-network-wired lps-section__icon"></i>
Channel &amp; Transaction Limits
</mat-panel-title>
</mat-expansion-panel-header>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline">
<mat-label>Is Mobile App?</mat-label>
<mat-select formControlName="isMobileAppEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Is ATM?</mat-label>
<mat-select formControlName="isAtmEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
@if (loanProductSettingsForm.value.isAtmEnabled === 'YES') {
<mat-form-field appearance="outline">
<mat-label>ATM Yearly Charge?</mat-label>
<mat-select formControlName="atmYearlyChargeEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>ATM Min. Balance</mat-label>
<input type="number" matInput formControlName="atmMinimumBalance" />
</mat-form-field>
}
<mat-form-field appearance="outline">
<mat-label>Email Active?</mat-label>
<mat-select formControlName="emailActiveEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Is IMPS?</mat-label>
<mat-select formControlName="isImpsEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Is UPI?</mat-label>
<mat-select formControlName="isUpiEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Is POS?</mat-label>
<mat-select formControlName="isPosEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Is BBPS?</mat-label>
<mat-select formControlName="isBbpsEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Inter Bank Limit (Per Day)</mat-label>
<input type="number" matInput formControlName="interBankLimitPerDay" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Inter Bank Limit (Per Transaction)</mat-label>
<input type="number" matInput formControlName="interBankLimitPerTransaction" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>NEFT/RTGS Limit (Per Day)</mat-label>
<input type="number" matInput formControlName="neftRtgsLimitPerDay" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>NEFT/RTGS Limit (Per Transaction)</mat-label>
<input type="number" matInput formControlName="neftRtgsLimitPerTransaction" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>NEFT Cutoff Limit</mat-label>
<input type="number" matInput formControlName="neftCutoffLimit" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>RTGS Cutoff Limit</mat-label>
<input type="number" matInput formControlName="rtgsCutoffLimit" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>NEFT/RTGS Min. Balance</mat-label>
<input type="number" matInput formControlName="neftRtgsMinimumBalance" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Is Charge Req. NEFT/RTGS (CBS)?</mat-label>
<mat-select formControlName="isChargeRequiredNeftRtgsCbs">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Is Charge Req. NEFT/RTGS (Mobile)?</mat-label>
<mat-select formControlName="isChargeRequiredNeftRtgsMobile">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
@if (loanProductSettingsForm.value.isImpsEnabled === 'YES') {
<mat-form-field appearance="outline">
<mat-label>IMPS Limit (Per Day)</mat-label>
<input type="number" matInput formControlName="impsLimitPerDay" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>IMPS Limit (Per Transaction)</mat-label>
<input type="number" matInput formControlName="impsLimitPerTransaction" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>IMPS Limit (Per Month)</mat-label>
<input type="number" matInput formControlName="impsLimitPerMonth" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>IMPS Min. Limit</mat-label>
<input type="number" matInput formControlName="impsMinLimit" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>IMPS Monthly Transaction Count</mat-label>
<input type="number" matInput formControlName="impsMonthlyTransactionCount" />
</mat-form-field>
}
</div>
</mat-expansion-panel>
}
@if (
showChannelLimits &&
(loanProductSettingsForm.value.isPosEnabled === 'YES' || loanProductSettingsForm.value.isAtmEnabled === 'YES')
) {
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<i class="fas fa-credit-card lps-section__icon"></i>
POS Facility &amp; ATM CSV Settings
</mat-panel-title>
</mat-expansion-panel-header>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline">
<mat-label>Max Per Transaction Limit (ATM)</mat-label>
<input type="number" matInput formControlName="maxPerTransactionLimitAtm" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Day Transaction Limit (ATM)</mat-label>
<input type="number" matInput formControlName="maxDayTransactionLimitAtm" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Per Transaction Limit (POS)</mat-label>
<input type="number" matInput formControlName="maxPerTransactionLimitPos" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Day Transaction Limit (POS)</mat-label>
<input type="number" matInput formControlName="maxDayTransactionLimitPos" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Per Transaction Limit (ECOM)</mat-label>
<input type="number" matInput formControlName="maxPerTransactionLimitEcom" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Day Transaction Limit (ECOM)</mat-label>
<input type="number" matInput formControlName="maxDayTransactionLimitEcom" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Per Transaction Limit (UPI)</mat-label>
<input type="number" matInput formControlName="maxPerTransactionLimitUpi" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Day Transaction Limit (UPI)</mat-label>
<input type="number" matInput formControlName="maxDayTransactionLimitUpi" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>ATM CSV Product Code</mat-label>
<input type="text" matInput formControlName="atmCsvProductCode" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>ATM CSV Type Code</mat-label>
<input type="text" matInput formControlName="atmCsvTypeCode" />
</mat-form-field>
</div>
</mat-expansion-panel>
}
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<i class="fas fa-clock lps-section__icon"></i>
Auto Interest Post Parameters
</mat-panel-title>
</mat-expansion-panel-header>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline">
<mat-label>Auto Int. Post Type</mat-label>
<mat-select formControlName="autoInterestPostType">
<mat-option value="MONTHLY">Monthly</mat-option>
<mat-option value="QUARTERLY">Quarterly</mat-option>
<mat-option value="HALF_YEARLY">Half Yearly</mat-option>
<mat-option value="YEARLY">Yearly</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Auto Int. Post When</mat-label>
<mat-select formControlName="autoInterestPostWhen">
<mat-option value="MONTH_END">Month End</mat-option>
<mat-option value="FIX_DAY">Fix Day</mat-option>
</mat-select>
</mat-form-field>
@if (loanProductSettingsForm.value.autoInterestPostWhen === 'FIX_DAY') {
<mat-form-field appearance="outline">
<mat-label>Fix Day</mat-label>
<input type="number" min="1" max="31" matInput formControlName="autoInterestPostFixDay" />
</mat-form-field>
}
<mat-form-field appearance="outline">
<mat-label>Auto Last Int. Date</mat-label>
<input type="text" matInput formControlName="autoLastInterestDate" placeholder="DD/MM/YYYY" />
</mat-form-field>
</div>
</mat-expansion-panel>
@if (showForeclosureSettings) {
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<i class="fas fa-door-closed lps-section__icon"></i>
Foreclosure / Early Account Closure
</mat-panel-title>
</mat-expansion-panel-header>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline">
<mat-label>Calculate Early A/c Close Charge?</mat-label>
<mat-select formControlName="calculateEarlyAccountCloseCharge">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
@if (loanProductSettingsForm.value.calculateEarlyAccountCloseCharge === 'YES') {
<mat-form-field appearance="outline">
<mat-label>Early A/c Close Charge</mat-label>
<input type="number" matInput formControlName="earlyAccountCloseCharge" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Early A/c Close Period (In Months)</mat-label>
<input type="number" matInput formControlName="earlyAccountClosePeriodInMonths" />
</mat-form-field>
}
</div>
</mat-expansion-panel>
}
</mat-accordion>
<!-- Credit Bureau Settings (CIBIL) -->
<div class="lps-section">
<div class="lps-section__header">
@@ -883,6 +1414,11 @@
<input type="number" min="0" matInput formControlName="graceOnInterestPayment" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Grace Days After Overdue For Moratorium A/c</mat-label>
<input type="number" min="0" matInput formControlName="graceDaysAfterOverdueForMoratorium" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Delinquency Bucket' | translate }}</mat-label>
<mat-select formControlName="delinquencyBucketId">
@@ -50,8 +50,9 @@
&__body {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
align-items: start;
gap: 18px 16px;
padding: 20px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
@@ -83,6 +84,26 @@
grid-column: span 3;
}
.lps-narrow {
max-width: 160px;
}
.lps-accordion {
margin-top: 16px;
.lps-section__body {
padding: 16px 24px 20px;
}
mat-panel-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
font-weight: 600;
}
}
.lps-checkbox-row {
display: flex;
align-items: center;
@@ -15,12 +15,25 @@ import {
ReactiveFormsModule
} from '@angular/forms';
import { LoanProducts } from '../../loan-products';
import {
CHANNEL_ENABLED_SUB_GROUPS,
CUSTOM_UI_ONLY_FIELDS,
NO_CHEQUE_BOOK_SUB_GROUPS,
TERM_LOAN_SUB_GROUPS,
patchCustomFields
} from '../../loan-product-custom-fields';
import { GlobalConfiguration } from 'app/system/configurations/global-configurations-tab/configuration.model';
import { CodeName, OptionData, StringEnumOptionData } from 'app/shared/models/option-data.model';
import { ProcessingStrategyService } from '../../services/processing-strategy.service';
import { MatTooltip } from '@angular/material/tooltip';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatIconButton } from '@angular/material/button';
import {
MatAccordion,
MatExpansionPanel,
MatExpansionPanelHeader,
MatExpansionPanelTitle
} from '@angular/material/expansion';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -34,6 +47,10 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatTooltip,
MatCheckbox,
MatIconButton,
MatAccordion,
MatExpansionPanel,
MatExpansionPanelHeader,
MatExpansionPanelTitle,
FaIconComponent,
ReactiveFormsModule
]
@@ -45,8 +62,27 @@ export class LoanProductSettingsStepComponent implements OnInit {
@Input() toEdit: boolean;
@Input() loanProductsTemplate: any;
@Input() isLinkedToFloatingInterestRates: UntypedFormControl;
@Input() productSubGroup: string;
@Output() advancePaymentStrategy = new EventEmitter<string>();
// The sub-group lists live in loan-product-custom-fields so the preview step can apply the same
// rule: it renders these sections from the form's values, and without the shared list it listed
// sections this step had hidden.
readonly channelEnabledSubGroups = CHANNEL_ENABLED_SUB_GROUPS;
readonly termLoanSubGroups = TERM_LOAN_SUB_GROUPS;
get showChannelLimits(): boolean {
return !this.productSubGroup || CHANNEL_ENABLED_SUB_GROUPS.includes(this.productSubGroup);
}
get showChequeBookSettings(): boolean {
return !this.productSubGroup || !NO_CHEQUE_BOOK_SUB_GROUPS.includes(this.productSubGroup);
}
get showForeclosureSettings(): boolean {
return !this.productSubGroup || TERM_LOAN_SUB_GROUPS.includes(this.productSubGroup);
}
loanProductSettingsForm: UntypedFormGroup;
amortizationTypeData: any;
@@ -247,6 +283,10 @@ export class LoanProductSettingsStepComponent implements OnInit {
}
});
}
// Repopulate the legacy "Account Type Creation" fields saved against this product.
// Last, so a value actually stored wins over any default set above.
patchCustomFields(this.loanProductSettingsForm, this.loanProductsTemplate);
}
createLoanProductSettingsForm() {
@@ -366,6 +406,24 @@ export class LoanProductSettingsStepComponent implements OnInit {
standingInstructionChargeGLAccount: [''],
standingInstructionDebitChargeSkipOption: [''],
openingBalanceMultiplierForRemittanceCharges: [''],
accountMoveToInoperativeEnabled: ['NO'],
generateGlDiffEntryEnabled: ['NO'],
subTypeWiseGlBalanceGenerationEnabled: ['NO'],
validateBeforeDeletingDebitEntryEnabled: ['NO'],
rcFileConceptEnabled: ['NO'],
accountCloseZeroBalanceInOpenDayProcess: ['NO'],
accountCloseZeroBalanceAtSubledgerTranEntry: ['NO'],
limitEnhancementAgainstClearingScreenDisplayEnabled: ['NO'],
advanceInterestParameterInLedgerEnabled: ['NO'],
vehicleDetailInMasterEnabled: ['NO'],
deletedChequeIssueEnabled: ['NO'],
dpReduceInDocumentRenewalDateEnabled: ['NO'],
dpReduceInRenewalDateEnabled: ['NO'],
autoCloseAtDayEndEnabled: ['NO'],
loanRateFromMasterEnabled: ['NO'],
chequeShortName: [''],
statementPrintEnabled: ['YES'],
neslCode: [''],
// Interest Related Parameters
drawingPowerAndLimitGenerationProcedure: ['NORMAL D.P. ENTRY IS CONSIDER'],
@@ -385,7 +443,7 @@ export class LoanProductSettingsStepComponent implements OnInit {
penaltyInterestDateDifferentThanRegularInterest: ['YES'],
fixedDepositOverdraftCertificateSortOrder: ['ORDER ON INTEREST RATE'],
useDocumentSubmissionDateForInterest: ['NO'],
loanManagementSystemRenewalPenaltyActive: [''],
loanManagementSystemRenewalPenaltyActive: ['NO'],
minimumNormalInterestAmount: [''],
minimumInterestCalculationMethod: [''],
minimumAgainstClearingInterestAmount: [''],
@@ -409,6 +467,18 @@ export class LoanProductSettingsStepComponent implements OnInit {
nonPerformingAssetPenalInterestRate: [''],
fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess: ['YES'],
addPenalInterestRate: [''],
atyp14SimpleInterestEnabled: ['NO'],
fixedDepositOverdraftMarginType: [''],
checkMasterDetailEditedPassingPendingEnabled: ['NO'],
creditDebitInterestCalculationSeparateEnabled: ['NO'],
onTimeInterestProvisionEnabled: ['NO'],
checkAgainstClearingEnabled: ['NO'],
gstOnPenaltyEnabled: ['NO'],
penaltyInterestGraceDays: [''],
overdrawnInterestRate: [''],
calculateDailyLimitEnhancementTodEnabled: ['NO'],
fixedDepositOverdraftMultipleInterestRateEnabled: ['NO'],
graceDaysAfterOverdueForMoratorium: [''],
// Credit Bureau Settings (CIBIL)
cibilAccountType: [''],
@@ -426,7 +496,60 @@ export class LoanProductSettingsStepComponent implements OnInit {
isFloatingRate: [false],
foreclosureChargesApplicable: ['NO'],
keyFactStatementEnabled: [true],
annualPercentageRateCalculationEnabled: [true]
annualPercentageRateCalculationEnabled: [true],
// Channel & Transaction Limits
isMobileAppEnabled: ['NO'],
isAtmEnabled: ['NO'],
atmYearlyChargeEnabled: ['NO'],
atmMinimumBalance: [''],
emailActiveEnabled: ['NO'],
isImpsEnabled: ['NO'],
isUpiEnabled: ['NO'],
isPosEnabled: ['NO'],
isBbpsEnabled: ['NO'],
interBankLimitPerDay: [''],
interBankLimitPerTransaction: [''],
neftRtgsLimitPerDay: [''],
neftRtgsLimitPerTransaction: [''],
neftCutoffLimit: [''],
rtgsCutoffLimit: [''],
neftRtgsMinimumBalance: [''],
isChargeRequiredNeftRtgsCbs: [''],
isChargeRequiredNeftRtgsMobile: [''],
impsLimitPerDay: [''],
impsLimitPerTransaction: [''],
impsLimitPerMonth: [''],
impsMinLimit: [''],
impsMonthlyTransactionCount: [''],
// POS Facility
maxPerTransactionLimitAtm: [''],
maxDayTransactionLimitAtm: [''],
maxPerTransactionLimitPos: [''],
maxDayTransactionLimitPos: [''],
maxPerTransactionLimitEcom: [''],
maxDayTransactionLimitEcom: [''],
maxPerTransactionLimitUpi: [''],
maxDayTransactionLimitUpi: [''],
// ATM CSV File Related Other Details
atmCsvProductCode: [''],
atmCsvTypeCode: [''],
// Auto Interest Post Parameters
autoInterestPostType: [''],
autoInterestPostWhen: [''],
autoInterestPostFixDay: [''],
autoLastInterestDate: [''],
// Foreclosure / Early Account Closure
calculateEarlyAccountCloseCharge: ['NO'],
earlyAccountCloseCharge: [''],
earlyAccountClosePeriodInMonths: [''],
// Gold Loan Specific
goldLoanPacketNumberWithYearEnabled: ['NO']
});
}
@@ -829,10 +952,20 @@ export class LoanProductSettingsStepComponent implements OnInit {
}
get loanProductSettings() {
const productSettings = this.loanProductSettingsForm.value;
const productSettings = { ...this.loanProductSettingsForm.value };
if (productSettings['delinquencyBucketId'] === '') {
productSettings['delinquencyBucketId'] = null;
}
// `value` omits disabled controls, which would leave a legacy field blank on the preview step
// even though the officer filled it in. Only the custom fields are added back, never the
// standard Fineract ones -- those are disabled deliberately and must stay out of the payload.
// Carrying the custom ones is free: buildPayload() strips every one of them before submitting.
for (const field of CUSTOM_UI_ONLY_FIELDS) {
const control = this.loanProductSettingsForm.get(field);
if (control?.disabled) {
productSettings[field] = control.value;
}
}
return productSettings;
}
@@ -17,6 +17,69 @@
matTooltip="{{ 'tooltips.Fields are used to define the minimum, default, and maximum principal' | translate }}"
></i>
</div>
<div class="lpt-subsection">
<div class="lpt-grid lpt-grid-4">
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Minimum' | translate }}</mat-label>
<input type="number" matInput formControlName="minPrincipal" [min]="0" />
<mat-error *ngIf="loanProductTermsForm.get('minPrincipal')?.hasError('min')">
{{ 'labels.commons.Minimum Value must be' | translate }} <strong>0</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Default' | translate }}</mat-label>
<input type="number" matInput formControlName="principal" required [min]="0" />
<mat-error *ngIf="loanProductTermsForm.get('principal')?.hasError('required')">
{{ 'labels.inputs.Principal' | translate }}
<span class="text-danger">{{ 'labels.commons.is required' | translate }}</span>
</mat-error>
<mat-error *ngIf="loanProductTermsForm.get('principal')?.hasError('min')">
{{ 'labels.commons.Minimum Value must be' | translate }} <strong>0</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Maximum' | translate }}</mat-label>
<input type="number" matInput formControlName="maxPrincipal" [min]="0" />
<mat-error *ngIf="loanProductTermsForm.get('maxPrincipal')?.hasError('min')">
{{ 'labels.commons.Minimum Value must be' | translate }} <strong>0</strong>
</mat-error>
</mat-form-field>
</div>
</div>
<div class="lpt-subsection">
<h4 class="lpt-subsection-title">
{{ 'labels.inputs.Number of Repayments' | translate }}
</h4>
<div class="lpt-grid lpt-grid-4">
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Minimum' | translate }}</mat-label>
<input type="number" matInput formControlName="minNumberOfRepayments" [min]="1" />
<mat-error *ngIf="loanProductTermsForm.get('minNumberOfRepayments')?.hasError('min')">
{{ 'labels.commons.Minimum Value must be' | translate }} <strong>1</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Default' | translate }}</mat-label>
<input type="number" matInput formControlName="numberOfRepayments" required [min]="1" />
<mat-error *ngIf="loanProductTermsForm.get('numberOfRepayments')?.hasError('required')">
{{ 'labels.inputs.Number of Repayments' | translate }}
<span class="text-danger">{{ 'labels.commons.is required' | translate }}</span>
</mat-error>
<mat-error *ngIf="loanProductTermsForm.get('numberOfRepayments')?.hasError('min')">
{{ 'labels.commons.Minimum Value must be' | translate }} <strong>1</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Maximum' | translate }}</mat-label>
<input type="number" matInput formControlName="maxNumberOfRepayments" [min]="1" />
<mat-error *ngIf="loanProductTermsForm.get('maxNumberOfRepayments')?.hasError('min')">
{{ 'labels.commons.Minimum Value must be' | translate }} <strong>1</strong>
</mat-error>
</mat-form-field>
</div>
</div>
<div class="lpt-grid">
<div class="lpt-checkbox-row lpt-full-width">
<mat-checkbox labelPosition="before" formControlName="allowApprovedDisbursedAmountsOverApplied">
@@ -264,90 +327,56 @@
}
</div>
<!-- Interest Rate Slabs Section -->
<!-- Interest Rate Detail Section (legacy Page 1 table) -->
<div class="lpt-section">
<div class="lpt-section-header">
<i class="fas fa-table lpt-section-icon"></i>
<h3>Interest Rate Slabs</h3>
<h3>Interest Rate Detail</h3>
</div>
<div class="lpt-slab-c">
<!-- Particular Name -->
<div class="lpt-slab-c-particular">
<label class="lpt-slab-c-particular-label">Particular</label>
<input
type="text"
class="lpt-slab-c-name-input"
[formControl]="particularLabel"
placeholder="e.g. Consumer Loan"
/>
</div>
<!-- 2-column slab grid (same as before, General + Special inside each card) -->
<div class="lpt-slab-c-grid">
@for (genGroup of slabGroups; track $index) {
<div class="lpt-slab-c-item">
<span class="lpt-slab-c-range">{{ interestSlabDefinitions[$index].label }}</span>
<div class="lpt-slab-c-rate-pair">
<!-- General -->
<div class="lpt-slab-c-rate-field" [formGroup]="genGroup">
<span class="lpt-slab-c-rate-label">Gen</span>
<div class="lpt-slab-c-input-wrap">
<input
type="number"
class="lpt-slab-c-input"
formControlName="interestRate"
placeholder="0.00"
min="0"
max="100"
step="0.01"
/>
<span class="lpt-slab-c-suffix">%</span>
</div>
</div>
<!-- Special -->
<div class="lpt-slab-c-rate-field" [formGroup]="specialSlabGroups[$index]">
<span class="lpt-slab-c-rate-label lpt-slab-c-rate-label--special">Spl</span>
<div class="lpt-slab-c-input-wrap">
<input
type="number"
class="lpt-slab-c-input lpt-slab-c-input--special"
formControlName="interestRate"
placeholder="0.00"
min="0"
max="100"
step="0.01"
/>
<span class="lpt-slab-c-suffix">%</span>
</div>
</div>
</div>
</div>
}
</div>
<!-- Fixed Interest Rate row -->
<div class="lpt-slab-c-fixed" [formGroup]="fixedRateGroup">
<label class="lpt-slab-c-fixed-toggle">
<input type="checkbox" formControlName="hasFixed" class="lpt-slab-c-checkbox" />
<i class="fas fa-lock"></i>
<span>Fixed Interest Rate (%)</span>
</label>
@if (fixedRateGroup.get('hasFixed').value) {
<div class="lpt-slab-c-input-wrap">
<input
type="number"
class="lpt-slab-c-input lpt-slab-c-input--fixed"
formControlName="fixedAmount"
placeholder="0.00"
min="0"
max="100"
step="0.01"
/>
<span class="lpt-slab-c-suffix">%</span>
</div>
}
</div>
<div class="lpt-ird-table-wrap">
<table class="lpt-ird-table">
<thead>
<tr>
<th rowspan="2">Interest Type</th>
<th rowspan="2">Calculation</th>
<th colspan="2">Normal Category</th>
<th colspan="2">Special Category</th>
<th rowspan="2">Period</th>
</tr>
<tr>
<th>Mini. Rate</th>
<th>Maxi. Rate</th>
<th>Mini. Rate</th>
<th>Maxi. Rate</th>
</tr>
</thead>
<tbody>
@for (row of interestRateDetailGroups; track $index) {
<tr [formGroup]="row">
<td class="lpt-ird-type">{{ interestRateDetailTypes[$index] }}</td>
<td>
<select formControlName="calculation" class="lpt-ird-select">
<option [ngValue]="true">YES</option>
<option [ngValue]="false">NO</option>
</select>
</td>
<td><input type="number" formControlName="normalMinRate" class="lpt-ird-input" min="0" max="100" /></td>
<td><input type="number" formControlName="normalMaxRate" class="lpt-ird-input" min="0" max="100" /></td>
<td><input type="number" formControlName="specialMinRate" class="lpt-ird-input" min="0" max="100" /></td>
<td><input type="number" formControlName="specialMaxRate" class="lpt-ird-input" min="0" max="100" /></td>
<td>
<select formControlName="period" class="lpt-ird-select">
<option value=""></option>
@for (period of interestRateDetailPeriods; track period) {
<option [value]="period">{{ period }}</option>
}
</select>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
@@ -199,175 +199,42 @@ $shadow-hover: 0 4px 12px rgb(37 99 235 / 10%);
padding: 8px 4px;
}
// ─── Interest Rate Slabs — Option C (2-column grid) ──────────────────────────
.lpt-slab-c {
// ─── Interest Rate Detail table (legacy Page 1 grid) ─────────────────────────
.lpt-ird-table-wrap {
padding: 16px 24px 20px;
display: flex;
flex-direction: column;
gap: 0;
overflow-x: auto;
}
// Particular row
.lpt-slab-c-particular {
display: flex;
align-items: center;
gap: 14px;
padding-bottom: 14px;
border-bottom: 1px solid $border;
margin-bottom: 12px;
flex-wrap: wrap;
}
.lpt-slab-c-particular-label {
font-size: 12px;
font-weight: 700;
color: $text-secondary;
text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
}
.lpt-slab-c-name-input {
flex: 1;
max-width: 320px;
padding: 7px 12px;
border: 1px solid $border;
border-radius: 8px;
font-size: 13.5px;
font-weight: 600;
color: $text-primary;
background: $surface;
outline: none;
transition:
border-color 0.15s,
box-shadow 0.15s;
&::placeholder {
color: #b0b8c4;
font-weight: 400;
}
&:focus {
border-color: $primary;
box-shadow: 0 0 0 3px rgb(37 99 235 / 10%);
}
}
// General / Special tab toggle
.lpt-slab-tabs {
display: flex;
gap: 6px;
margin-left: auto;
}
.lpt-slab-tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 16px;
border: 1.5px solid $border;
border-radius: 20px;
background: $surface;
.lpt-ird-table {
width: 100%;
border-collapse: collapse;
font-size: 12.5px;
font-weight: 600;
color: $text-secondary;
cursor: pointer;
transition: all 0.15s;
i {
th,
td {
border: 1px solid $border;
padding: 6px 8px;
text-align: center;
}
thead th {
background: $surface-alt;
font-weight: 700;
color: $text-secondary;
text-transform: uppercase;
letter-spacing: 0.03em;
font-size: 11px;
}
&:hover {
border-color: $primary;
color: $primary;
}
&--active {
background: $primary;
border-color: $primary;
color: $surface;
box-shadow: 0 2px 8px rgb(37 99 235 / 25%);
}
}
// 2-column slab grid
.lpt-slab-c-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 16px;
@media (width <= 540px) {
grid-template-columns: 1fr;
}
}
.lpt-slab-c-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 8px 12px;
border: 1px solid $border;
border-radius: 8px;
background: $surface;
transition:
border-color 0.15s,
box-shadow 0.15s;
&:focus-within {
border-color: $primary;
box-shadow: 0 0 0 3px rgb(37 99 235 / 8%);
}
}
// General + Special pair inside each card
.lpt-slab-c-rate-pair {
display: flex;
gap: 8px;
align-items: center;
}
.lpt-slab-c-rate-field {
display: flex;
align-items: center;
gap: 4px;
}
.lpt-slab-c-rate-label {
font-size: 10px;
font-weight: 700;
color: $primary;
background: #eff6ff;
padding: 2px 5px;
border-radius: 4px;
.lpt-ird-type {
text-align: left !important;
font-weight: 600;
color: $text-primary;
white-space: nowrap;
&--special {
color: #92400e;
background: #fef9c3;
}
}
.lpt-slab-c-range {
font-size: 12.5px;
font-weight: 500;
color: $text-label;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// Input + suffix wrapper
.lpt-slab-c-input-wrap {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.lpt-slab-c-input {
.lpt-ird-input {
width: 70px;
padding: 4px 6px;
border: 1px solid $border;
@@ -379,79 +246,26 @@ $shadow-hover: 0 4px 12px rgb(37 99 235 / 10%);
background: $surface-alt;
outline: none;
transition: border-color 0.15s;
appearance: textfield;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
appearance: none;
}
&:focus {
border-color: $primary;
background: $primary-light;
}
&--fixed {
width: 90px;
}
}
.lpt-slab-c-suffix {
font-size: 12px;
font-weight: 700;
color: $text-secondary;
user-select: none;
&--special {
color: #92400e;
}
}
.lpt-slab-c-input--special {
&:focus {
border-color: #f59e0b;
background: #fef9c3;
}
}
.lpt-slab-c-error {
font-size: 10px;
color: #ef4444;
}
// Fixed Interest Rate row (below grid)
.lpt-slab-c-fixed {
display: flex;
align-items: center;
gap: 20px;
margin: 12px -24px -20px;
padding: 12px 16px;
border-top: 2px dashed $primary-border;
background: $primary-light;
border-radius: 0 0 $radius-card $radius-card;
padding-left: 24px;
padding-right: 24px;
}
.lpt-slab-c-fixed-toggle {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
.lpt-ird-select {
width: 100%;
min-width: 90px;
padding: 4px 6px;
border: 1px solid $border;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
color: $primary;
user-select: none;
color: $text-primary;
background: $surface;
outline: none;
i {
font-size: 12px;
opacity: 0.7;
&:focus {
border-color: $primary;
}
}
.lpt-slab-c-checkbox {
accent-color: $primary;
width: 15px;
height: 15px;
cursor: pointer;
}
@@ -17,6 +17,7 @@ import {
FormControl,
ReactiveFormsModule
} from '@angular/forms';
import { patchCustomFields } from '../../loan-product-custom-fields';
import { ProcessingStrategyService } from '../../services/processing-strategy.service';
import { TranslateService } from '@ngx-translate/core';
import { MatTooltip } from '@angular/material/tooltip';
@@ -57,24 +58,23 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
repaymentFrequencyTypeData: any;
repaymentStartDateTypeOptions: any;
readonly interestSlabDefinitions = [
{ label: 'Upto 10,000', maxAmount: 10000 },
{ label: 'Upto 25,000', maxAmount: 25000 },
{ label: 'Upto 1,00,000', maxAmount: 100000 },
{ label: 'Upto 2,00,000', maxAmount: 200000 },
{ label: 'Upto 5,00,000', maxAmount: 500000 },
{ label: 'Upto 10,00,000', maxAmount: 1000000 },
{ label: 'Upto 15,00,000', maxAmount: 1500000 },
{ label: 'Upto 20,00,000', maxAmount: 2000000 },
{ label: 'Upto 25,00,000', maxAmount: 2500000 },
{ label: 'More than 25,00,000', maxAmount: null }
// Legacy "Interest Rate Detail" table — 4 interest types x Normal/Special category x Min/Max Rate x Period
readonly interestRateDetailTypes = [
'Normal Debit',
'Normal Credit',
'Against Clearing',
'Penalty'
];
interestRateSlabsArray: FormArray;
specialSlabsArray: FormArray;
fixedRateGroup: FormGroup;
particularLabel = new FormControl('');
activeSlabTab: 'general' | 'special' = 'general';
readonly interestRateDetailPeriods = [
'DAILY',
'MONTHLY',
'QUARTERLY',
'HALF YEARLY',
'YEARLY'
];
interestRateDetailArray: FormArray;
constructor() {
this.createLoanProductTermsForm();
@@ -92,6 +92,12 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
];
this.loanProductTermsForm.patchValue({
minPrincipal: this.loanProductsTemplate.minPrincipal,
principal: this.loanProductsTemplate.principal,
maxPrincipal: this.loanProductsTemplate.maxPrincipal,
minNumberOfRepayments: this.loanProductsTemplate.minNumberOfRepayments,
numberOfRepayments: this.loanProductsTemplate.numberOfRepayments,
maxNumberOfRepayments: this.loanProductsTemplate.maxNumberOfRepayments,
isLinkedToFloatingInterestRates: this.loanProductsTemplate.isLinkedToFloatingInterestRates,
minInterestRatePerPeriod: this.loanProductsTemplate.minInterestRatePerPeriod,
interestRatePerPeriod: this.loanProductsTemplate.interestRatePerPeriod,
@@ -121,6 +127,11 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
});
}
// Repopulate the legacy "Account Type Creation" fields saved against this product.
// Last, so a value actually stored wins over any default set above.
patchCustomFields(this.loanProductTermsForm, this.loanProductsTemplate);
this.patchInterestRateDetail();
this.zeroInterest.patchValue(
this.loanProductsTemplate.minInterestRatePerPeriod === 0 &&
this.loanProductsTemplate.interestRatePerPeriod === 0 &&
@@ -134,36 +145,63 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
}
createLoanProductTermsForm() {
this.interestRateSlabsArray = new FormArray(
this.interestSlabDefinitions.map(
this.interestRateDetailArray = new FormArray(
this.interestRateDetailTypes.map(
() =>
new FormGroup({
interestRate: new FormControl(null, [
calculation: new FormControl(false),
normalMinRate: new FormControl(null, [
Validators.min(0),
Validators.max(100)
])
]),
normalMaxRate: new FormControl(null, [
Validators.min(0),
Validators.max(100)
]),
specialMinRate: new FormControl(null, [
Validators.min(0),
Validators.max(100)
]),
specialMaxRate: new FormControl(null, [
Validators.min(0),
Validators.max(100)
]),
period: new FormControl('')
})
)
);
this.specialSlabsArray = new FormArray(
this.interestSlabDefinitions.map(
() =>
new FormGroup({
interestRate: new FormControl(null, [
Validators.min(0),
Validators.max(100)
])
})
)
);
this.fixedRateGroup = new FormGroup({
hasFixed: new FormControl(false),
fixedAmount: new FormControl({ value: null, disabled: true }, [Validators.min(0)])
});
this.loanProductTermsForm = this.formBuilder.group({
minPrincipal: [
'',
[Validators.min(0)]
],
principal: [
'',
[
Validators.required,
Validators.min(0)
]
],
maxPrincipal: [
'',
[Validators.min(0)]
],
minNumberOfRepayments: [
'',
[Validators.min(1)]
],
numberOfRepayments: [
'',
[
Validators.required,
Validators.min(1)
]
],
maxNumberOfRepayments: [
'',
[Validators.min(1)]
],
isLinkedToFloatingInterestRates: [false],
allowApprovedDisbursedAmountsOverApplied: [false],
overAppliedCalculationType: [{ value: null, disabled: true }],
@@ -287,15 +325,6 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
}
});
this.fixedRateGroup.get('hasFixed').valueChanges.subscribe((hasFixed: boolean) => {
if (hasFixed) {
this.fixedRateGroup.get('fixedAmount').enable();
} else {
this.fixedRateGroup.get('fixedAmount').disable();
this.fixedRateGroup.get('fixedAmount').setValue(null);
}
});
this.zeroInterest.valueChanges.subscribe((zeroInterest) => {
if (zeroInterest) {
this.loanProductTermsForm.get('minInterestRatePerPeriod').patchValue(0);
@@ -322,16 +351,34 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
});
}
get slabGroups(): FormGroup[] {
return this.interestRateSlabsArray.controls as FormGroup[];
get interestRateDetailGroups(): FormGroup[] {
return this.interestRateDetailArray.controls as FormGroup[];
}
get activeSlabGroups(): FormGroup[] {
return this.interestRateSlabsArray.controls as FormGroup[];
}
get specialSlabGroups(): FormGroup[] {
return this.specialSlabsArray.controls as FormGroup[];
/**
* Fills the legacy Interest Rate Detail grid from a saved product. Rows are matched on
* `interestType` rather than by position, so a payload that comes back in a different order cannot
* put one interest type's rates onto another. A type with no saved row is left at its defaults.
*/
private patchInterestRateDetail(): void {
const saved: any[] = this.loanProductsTemplate?.interestRateDetail;
if (!Array.isArray(saved) || saved.length === 0) {
return;
}
this.interestRateDetailTypes.forEach((interestType, index) => {
const row = saved.find((r) => r?.interestType === interestType);
if (!row) {
return;
}
this.interestRateDetailArray.at(index).patchValue({
calculation: row.calculation ?? false,
normalMinRate: row.normalMinRate,
normalMaxRate: row.normalMaxRate,
specialMinRate: row.specialMinRate,
specialMaxRate: row.specialMaxRate,
period: row.period ?? ''
});
});
}
get loanProductTerms() {
@@ -349,15 +396,10 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
minInterestRatePerPeriod: normalizeDecimal(formValue.minInterestRatePerPeriod),
interestRatePerPeriod: normalizeDecimal(formValue.interestRatePerPeriod),
maxInterestRatePerPeriod: normalizeDecimal(formValue.maxInterestRatePerPeriod),
interestRateSlabs: this.interestRateSlabsArray.getRawValue().map((slab, i) => ({
maxAmount: this.interestSlabDefinitions[i].maxAmount,
label: this.interestSlabDefinitions[i].label,
interestRate: slab.interestRate
})),
fixedRate: (() => {
const fr = this.fixedRateGroup.getRawValue();
return fr.hasFixed ? fr.fixedAmount : null;
})()
interestRateDetail: this.interestRateDetailArray.getRawValue().map((row, i) => ({
interestType: this.interestRateDetailTypes[i],
...row
}))
};
}
@@ -9,6 +9,10 @@
import { Injectable, inject } from '@angular/core';
import { SettingsService } from 'app/settings/settings.service';
import { CUSTOM_DATE_FIELDS, CUSTOM_YES_NO_STRING_FIELDS } from './loan-product-custom-fields';
export { CUSTOM_UI_ONLY_FIELDS } from './loan-product-custom-fields';
@Injectable({
providedIn: 'root'
})
@@ -35,6 +39,42 @@ export class LoanProducts {
return loanProductTemplate;
}
/**
* The backend returns the legacy "Account Type Creation" fields nested under `cbsExtensions`, while
* every step form reads and writes them as flat properties on the product. Lifting them here keeps
* that difference in one place rather than teaching each step about the nesting.
*
* Two shapes also have to be converted back to what the form controls expect:
*
* - the yes/no dropdowns hold the strings 'YES'/'NO', but the column is BOOLEAN, so the API returns
* true/false -- patched as-is the control matches no option and shows blank;
* - a date arrives as Fineract's [year, month, day] array rather than a Date.
*
* Mutates and returns the product so it can be dropped into an existing resolver chain.
*/
public flattenCbsExtensions(loanProduct: any): any {
if (!loanProduct?.cbsExtensions) {
return loanProduct;
}
Object.assign(loanProduct, loanProduct.cbsExtensions);
for (const field of CUSTOM_YES_NO_STRING_FIELDS) {
const value = loanProduct[field];
if (typeof value === 'boolean') {
loanProduct[field] = value ? 'YES' : 'NO';
}
}
for (const field of CUSTOM_DATE_FIELDS) {
const value = loanProduct[field];
if (Array.isArray(value) && value.length >= 3) {
loanProduct[field] = new Date(value[0], value[1] - 1, value[2]);
}
}
return loanProduct;
}
public buildPayload(loanProductData: any, itemsByDefault: any) {
const dateFormat: string = this.settingsService.dateFormat;
const locale: string = this.settingsService.language.code;
@@ -49,6 +89,12 @@ export class LoanProducts {
delete loanProduct.allowAttributeConfiguration;
delete loanProduct.advancedAccountingRules;
// The legacy "Account Type Creation" fields used to be stripped here, because Fineract's loan
// product validator rejected any parameter it did not recognise. They are now persisted by
// LoanProductCbsExtService in a satellite table keyed on the product, so they are sent as-is:
//
// CUSTOM_UI_ONLY_FIELDS.forEach((field) => delete loanProduct[field]);
// In Fineract, the POST and PUT endpoints for /v1/loanproducts have a typo in the field
// allowPartialPeriodInterestCalculation. Until that is fixed, we need to replace the field name in the payload.
loanProduct.allowPartialPeriodInterestCalculation = loanProduct.allowPartialPeriodInterestCalculation;
@@ -21,6 +21,33 @@
<mat-checkbox formControlName="lienMarked">Lien Marked</mat-checkbox>
<mat-checkbox formControlName="withdrawalFeeForTransfers">Apply Withdrawal Fee for Transfers</mat-checkbox>
</div>
<mat-checkbox class="rdps-preclosure-toggle" formControlName="preClosurePenalApplicable">
{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}
</mat-checkbox>
@if (recurringDepositProductSettingsForm.value.preClosurePenalApplicable) {
<div class="rdps-lockin-fields">
<mat-form-field appearance="outline" class="rdps-field">
<mat-label>{{ 'labels.inputs.Penal Interest' | translate }} (%)</mat-label>
<input type="number" matInput formControlName="preClosurePenalInterest" min="0" step="0.01" />
</mat-form-field>
<mat-form-field appearance="outline" class="rdps-field">
<mat-label>{{ 'labels.inputs.Period' | translate }}</mat-label>
<mat-select formControlName="preClosurePenalInterestOnTypeId">
@for (
preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData;
track preClosurePenalInterestOnType
) {
<mat-option [value]="preClosurePenalInterestOnType.id">
{{ preClosurePenalInterestOnType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
</div>
</div>
@@ -162,3 +162,12 @@ $gray-900: #202124;
}
}
}
.rdps-preclosure-toggle {
display: block;
margin: 4px 0 16px;
padding-bottom: 16px;
border-bottom: 1px solid $gray-200;
font-size: 14px;
color: $gray-900;
}
@@ -28,6 +28,7 @@ export class RecurringDepositProductSettingsStepComponent implements OnInit {
recurringDepositProductSettingsForm: UntypedFormGroup;
lockinPeriodFrequencyTypeData: any;
preClosurePenalInterestOnTypeData: any;
constructor() {
this.createrecurringDepositProductSettingsForm();
@@ -35,6 +36,7 @@ export class RecurringDepositProductSettingsStepComponent implements OnInit {
ngOnInit() {
this.lockinPeriodFrequencyTypeData = this.recurringDepositProductsTemplate.lockinPeriodFrequencyTypeOptions;
this.preClosurePenalInterestOnTypeData = this.recurringDepositProductsTemplate.preClosurePenalInterestOnTypeOptions;
if (this.recurringDepositProductsTemplate?.id) {
this.recurringDepositProductSettingsForm.patchValue({
@@ -46,6 +48,9 @@ export class RecurringDepositProductSettingsStepComponent implements OnInit {
enableLockinPeriod: this.recurringDepositProductsTemplate.enableLockinPeriod ?? false,
lockinPeriodFrequency: this.recurringDepositProductsTemplate.lockinPeriodFrequency ?? '',
lockinPeriodFrequencyType: this.recurringDepositProductsTemplate.lockinPeriodFrequencyType?.id ?? '',
preClosurePenalApplicable: this.recurringDepositProductsTemplate.preClosurePenalApplicable ?? false,
preClosurePenalInterest: this.recurringDepositProductsTemplate.preClosurePenalInterest ?? '',
preClosurePenalInterestOnTypeId: this.recurringDepositProductsTemplate.preClosurePenalInterestOnType?.id ?? '',
maturityInstructions: this.recurringDepositProductsTemplate.maturityInstructions ?? false,
creditMaturityProceeds: this.recurringDepositProductsTemplate.creditMaturityProceeds ?? false,
penaltyGracePeriodMissedInstallments:
@@ -71,6 +76,9 @@ export class RecurringDepositProductSettingsStepComponent implements OnInit {
enableLockinPeriod: [false],
lockinPeriodFrequency: [''],
lockinPeriodFrequencyType: [''],
preClosurePenalApplicable: [false],
preClosurePenalInterest: [''],
preClosurePenalInterestOnTypeId: [''],
maturityInstructions: [false],
creditMaturityProceeds: [false],
penaltyGracePeriodMissedInstallments: [false],
@@ -12,9 +12,7 @@
<fa-icon icon="piggy-bank" class="csa-header__icon"></fa-icon>
<div>
<h2 class="csa-header__title">Create Savings Account</h2>
<div class="csa-header__sub">
Pick a product, confirm currency, set digital channels and review before submit.
</div>
<div class="csa-header__sub">Pick a product, confirm currency and review before submit.</div>
</div>
</div>
@@ -76,33 +74,7 @@
</div>
</mat-tab>
<!-- Tab 3: Digital -->
<mat-tab [disabled]="!savingsAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="mobile-alt" class="csa-tab__icon"></fa-icon>
Digital
</ng-template>
<div class="csa-tab-body">
@if (savingsAccountProductTemplate) {
<mifosx-savings-account-digital-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
</mifosx-savings-account-digital-step>
}
<div class="csa-actions">
<button mat-stroked-button (click)="prev()">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<span class="csa-actions__spacer"></span>
<button mat-raised-button color="primary" (click)="next()">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</div>
</mat-tab>
<!-- Tab 4: Preview / Submit -->
<!-- Tab 3: Preview / Submit -->
<mat-tab [disabled]="!savingsAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="eye" class="csa-tab__icon"></fa-icon>
@@ -62,8 +62,8 @@ export class CreateSavingsAccountComponent {
savingsAccountProductTemplate: any;
/** Currently visible wizard tab (0-indexed). */
selectedTabIndex = 0;
/** Total wizard tabs — Product, Currency, Digital, Preview. */
private readonly totalTabs = 4;
/** Total wizard tabs — Product, Currency, Preview. */
private readonly totalTabs = 3;
/** Guards against duplicate submissions from a double-click while the create request is in flight. */
submitting = false;
@@ -14,7 +14,9 @@
</div>
<div>
<h2 class="act-savings__title">Activate Savings Account</h2>
<p class="act-savings__sub">Set the activation date and opening balance to bring this account live.</p>
<p class="act-savings__sub">
Set the activation date to bring this account live -- funding happens afterwards, as a separate deposit.
</p>
</div>
</div>
@@ -45,18 +47,6 @@
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="act-savings__field">
<mat-label>Opening Balance</mat-label>
<input matInput type="number" [min]="minRequiredOpeningBalance ?? 0" formControlName="openingBalance" />
<span matPrefix class="act-savings__prefix">₹&nbsp;</span>
@if (minRequiredOpeningBalance) {
<mat-hint>Minimum required: ₹{{ minRequiredOpeningBalance }}</mat-hint>
}
@if (activateSavingsAccountForm.controls.openingBalance.hasError('min')) {
<mat-error>Opening balance must be at least ₹{{ minRequiredOpeningBalance ?? 0 }}</mat-error>
}
</mat-form-field>
</div>
<div class="act-savings__actions">
@@ -7,7 +7,7 @@
*/
/** Angular Imports */
import { Component, OnInit, Input, inject } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Dates } from 'app/core/utils/dates';
@@ -41,8 +41,6 @@ export class ActivateSavingsAccountComponent implements OnInit {
private router = inject(Router);
private settingsService = inject(SettingsService);
@Input() minRequiredOpeningBalance: number | null = null;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
@@ -76,18 +74,10 @@ export class ActivateSavingsAccountComponent implements OnInit {
* Creates the activate savings account form.
*/
createActivateSavingsAccountForm() {
const minBalance = this.minRequiredOpeningBalance ?? 0;
this.activateSavingsAccountForm = this.formBuilder.group({
activatedOnDate: [
'',
Validators.required
],
openingBalance: [
minBalance,
[
Validators.required,
Validators.min(minBalance)
]
]
});
}
@@ -109,9 +99,6 @@ export class ActivateSavingsAccountComponent implements OnInit {
dateFormat,
locale
};
if (data.openingBalance === 0 || data.openingBalance === null) {
delete data.openingBalance;
}
this.savingsService.executeSavingsAccountCommand(this.accountId, 'activate', data).subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
@@ -10,6 +10,7 @@
<mat-card>
<form [formGroup]="approveSavingsAccountForm" (ngSubmit)="submit()">
<mat-card-content>
<p class="mat-body-2">This approves and activates the account in one step -- it goes straight to Active.</p>
<div class="layout-column">
<mat-form-field (click)="approvedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Approved On Date' | translate }}</mat-label>
@@ -81,8 +81,9 @@ export class ApproveSavingsAccountComponent implements OnInit {
}
/**
* Submits the form and approves the saving account,
* if successful redirects to the saving account.
* Submits the form, approves the saving account, then immediately activates it on the same
* date -- Approve and Activate are still two separate Fineract commands underneath (each shows
* up in the audit trail), but the officer only sees and clicks through one step.
*/
submit() {
const approveSavingsAccountFormData = this.approveSavingsAccountForm.value;
@@ -98,7 +99,14 @@ export class ApproveSavingsAccountComponent implements OnInit {
locale
};
this.savingsService.executeSavingsAccountCommand(this.accountId, 'approve', data).subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
const activationData = {
activatedOnDate: approveSavingsAccountFormData.approvedOnDate,
dateFormat,
locale
};
this.savingsService.executeSavingsAccountCommand(this.accountId, 'activate', activationData).subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
});
}
}
@@ -13,9 +13,7 @@
<mifosx-reject-savings-account></mifosx-reject-savings-account>
}
@if (actions['Activate']) {
<mifosx-activate-savings-account
[minRequiredOpeningBalance]="accountData?.minRequiredOpeningBalance"
></mifosx-activate-savings-account>
<mifosx-activate-savings-account></mifosx-activate-savings-account>
}
@if (actions['Undo Approval']) {
<mifosx-undo-approval-savings-account></mifosx-undo-approval-savings-account>
@@ -15,7 +15,6 @@
<h3 class="dig-header__title">Digital &amp; Account Features</h3>
<p class="dig-header__sub">Toggle the facilities available for this account (as per RBI guidelines)</p>
</div>
<span class="dig-header__badge">FROM PRODUCT</span>
</div>
<div class="dig-grid">
@@ -50,12 +50,11 @@ export class SavingsAccountDigitalStepComponent {
}
}
// Note: a new account always starts with every digital facility off, even if the product has
// its own defaults configured -- those are enabled deliberately, one at a time, after the
// account is created and active, not inherited automatically at opening.
@Input()
set savingsAccountProductTemplate(value: any) {
if (!this.hasExistingAccount && value?.accountFacilityConfig) {
this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig };
}
}
savingsAccountProductTemplate: any;
get savingsAccountDigital() {
return { accountFacilityConfig: { ...this.facilityConfig } };
@@ -124,13 +124,11 @@ export class SavingsAccountPreviewStepComponent implements OnChanges {
}
}
/** Returns user-modified digital config (from digital step), falling back to product template */
/** Digital facility config as actually captured by a digital step in this wizard -- no fallback
* to the product's own defaults, since account creation no longer has a digital step at all and
* must not preview facilities the officer never actually chose. */
get digitalFacilityConfig(): any {
return (
this.savingsAccountDigital?.accountFacilityConfig ??
this.savingsAccountProductTemplate?.accountFacilityConfig ??
null
);
return this.savingsAccountDigital?.accountFacilityConfig ?? null;
}
get hasDigitalSection(): boolean {
@@ -0,0 +1,160 @@
<!--
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/.
-->
<h2 mat-dialog-title class="dialog-header no-print">
<span>{{ accountType === 'FD' ? 'Fixed Deposit' : 'Recurring Deposit' }} Certificate</span>
<span class="flex-auto"></span>
</h2>
<div mat-dialog-content class="dialog-content-container">
<!-- Certificate Page Frame -->
<div class="certificate-container" id="deposit-certificate-print">
<div class="certificate-border-outer">
<div class="certificate-border-inner">
<!-- Certificate Watermark Icon/Seal -->
<div class="certificate-watermark">
<mat-icon class="watermark-icon">{{ accountType === 'FD' ? 'savings' : 'event_repeat' }}</mat-icon>
</div>
<div class="certificate-header">
<div class="bank-name">Nexora Co-operative Bank Ltd.</div>
<div class="bank-sub">Mumbai, Maharashtra, India</div>
<div class="cert-title">
{{ accountType === 'FD' ? 'FIXED DEPOSIT CERTIFICATE' : 'RECURRING DEPOSIT CERTIFICATE' }}
</div>
<div class="cert-subtitle">Issued under the Common Seal of the Bank</div>
</div>
<div class="cert-meta-row">
<div class="meta-item">
<span class="meta-label">Certificate No.</span>
<span class="meta-value">{{ certificateNo }}</span>
</div>
<div class="meta-item">
<span class="meta-label">Account No.</span>
<span class="meta-value">{{ accountData.accountNo }}</span>
</div>
<div class="meta-item">
<span class="meta-label">Date of Issue</span>
<span class="meta-value">{{ issueDate | dateFormat }}</span>
</div>
</div>
<div class="cert-body-section">
<p class="cert-main-text">
This is to certify that
<strong class="highlight-text">{{ accountData.clientName || accountData.groupName }}</strong>
holds a {{ accountType === 'FD' ? 'Fixed Deposit' : 'Recurring Deposit' }} account bearing account number
<strong class="highlight-text">{{ accountData.accountNo }}</strong>
with <strong class="highlight-text">Nexora Co-operative Bank Ltd.</strong>, as per the particulars set out
below.
</p>
<div class="issued-to-details">
<div class="issued-to-title">ISSUED TO:</div>
<div class="client-name-details">{{ accountData.clientName || accountData.groupName }}</div>
<div class="client-ids">
<span *ngIf="accountData.clientAccountNo"
><strong>Customer ID:</strong> {{ accountData.clientAccountNo }}</span
>
<span *ngIf="panNumber && panNumber !== 'Not Available'">
&middot; <strong>PAN:</strong> {{ panNumber }}</span
>
</div>
<div class="client-address" *ngIf="clientAddress">
{{ clientAddress }}
</div>
</div>
</div>
<!-- Details Grid -->
<table class="certificate-details-table">
<tbody>
<tr>
<td class="lbl-col">Product</td>
<td class="val-col">{{ accountData.depositProductName }}</td>
<td class="lbl-col">{{ accountType === 'FD' ? 'Deposit Date' : 'First Deposit Date' }}</td>
<td class="val-col">{{ depositDate | dateFormat }}</td>
</tr>
<tr *ngIf="accountType === 'FD'">
<td class="lbl-col">Deposit Amount</td>
<td class="val-col">
{{ accountData.depositAmount | currency: accountData.currency.code : 'symbol-narrow' }}
</td>
<td class="lbl-col">Tenure</td>
<td class="val-col">{{ tenureLabel }}</td>
</tr>
<tr *ngIf="accountType === 'RD'">
<td class="lbl-col">Installment Amount</td>
<td class="val-col">
{{
accountData.mandatoryRecommendedDepositAmount | currency: accountData.currency.code : 'symbol-narrow'
}}
</td>
<td class="lbl-col">Installment Frequency</td>
<td class="val-col">
Every {{ accountData.recurringFrequency }} {{ accountData.recurringFrequencyType?.value }}
</td>
</tr>
<tr *ngIf="accountType === 'RD'">
<td class="lbl-col">Tenure</td>
<td class="val-col">{{ tenureLabel }}</td>
<td class="lbl-col">Interest Rate (p.a.)</td>
<td class="val-col">{{ accountData.nominalAnnualInterestRate }}%</td>
</tr>
<tr *ngIf="accountType === 'FD'">
<td class="lbl-col">Interest Rate (p.a.)</td>
<td class="val-col">{{ accountData.nominalAnnualInterestRate }}%</td>
<td class="lbl-col">Interest Compounding</td>
<td class="val-col">{{ accountData.interestCompoundingPeriodType?.value }}</td>
</tr>
<tr>
<td class="lbl-col">Maturity Date</td>
<td class="val-col">{{ accountData.maturityDate | dateFormat }}</td>
<td class="lbl-col">Current Balance</td>
<td class="val-col">
{{ accountData.summary?.accountBalance | currency: accountData.currency.code : 'symbol-narrow' }}
</td>
</tr>
<tr class="highlight-row">
<td colspan="2" class="lbl-col-highlight">MATURITY AMOUNT</td>
<td colspan="2" class="val-col-highlight">
{{ accountData.maturityAmount | currency: accountData.currency.code : 'symbol-narrow' }}
</td>
</tr>
</tbody>
</table>
<div class="cert-footer">
<div class="seal-container">
<div class="seal-ring">
<div class="seal-text">COMMON SEAL</div>
</div>
</div>
<div class="closing-text">
Given under the common seal of the bank this {{ issueDate | date: 'd' }} day of
{{ issueDate | date: 'MMMM yyyy' }}.
</div>
<div class="signatures-container">
<div class="signature-block">
<div class="signature-line"></div>
<div class="signatory-title">Authorised Signatory</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<mat-dialog-actions align="end" class="no-print">
<button mat-button mat-dialog-close>{{ 'labels.buttons.Close' | translate }}</button>
<button mat-raised-button color="primary" (click)="printCertificate()">
<mat-icon class="m-r-5">print</mat-icon>
Print Certificate
</button>
</mat-dialog-actions>
@@ -0,0 +1,512 @@
/**
* 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/.
*/
.dialog-header {
display: flex;
align-items: center;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 12px;
margin-bottom: 16px;
font-weight: 500;
color: #1a252c;
}
.dialog-content-container {
max-height: 80vh;
overflow-y: auto;
padding: 10px;
}
/* Certificate Outer Container */
.certificate-container {
background-color: #faf8f5;
padding: 20px;
font-family: Georgia, 'Times New Roman', Times, serif;
color: #2c2520;
box-shadow: 0 4px 20px rgb(0 0 0 / 8%);
border-radius: 8px;
max-width: 800px;
margin: 0 auto;
}
/* Classic Ornate Border System */
.certificate-border-outer {
border: 5px double #c5a059; /* Gold double border */
padding: 8px;
background-color: #fdfcf7; /* Textured light cream */
border-radius: 4px;
position: relative;
}
.certificate-border-inner {
border: 1px solid #dcd1bd;
padding: 30px;
border-radius: 2px;
position: relative;
overflow: hidden;
}
/* Watermark */
.certificate-watermark {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0.03;
pointer-events: none;
z-index: 1;
.watermark-icon {
font-size: 280px;
width: 280px;
height: 280px;
color: #c5a059;
}
}
.certificate-header,
.cert-meta-row,
.cert-body-section,
.certificate-details-table,
.cert-footer {
position: relative;
z-index: 2; /* Ensure content is above watermark */
}
/* Header styling */
.certificate-header {
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid #f2edd5;
padding-bottom: 20px;
.bank-name {
font-size: 26px;
font-weight: bold;
color: #0b3c5d; /* Dark Premium Blue */
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: 4px;
}
.bank-sub {
font-size: 13px;
color: #7b6f63;
font-style: italic;
letter-spacing: 1px;
margin-bottom: 20px;
}
.cert-title {
font-size: 28px;
font-weight: 400;
color: #c5a059; /* Classic Gold */
letter-spacing: 4px;
margin-bottom: 5px;
font-family: Georgia, serif;
}
.cert-subtitle {
font-size: 11px;
color: #7b6f63;
text-transform: uppercase;
letter-spacing: 2px;
}
}
/* Meta Data Grid (Top of cert) */
.cert-meta-row {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
border-bottom: 1px solid #f2edd5;
padding-bottom: 15px;
.meta-item {
display: flex;
flex-direction: column;
.meta-label {
font-size: 11px;
text-transform: uppercase;
color: #928374;
letter-spacing: 1px;
margin-bottom: 3px;
}
.meta-value {
font-size: 15px;
font-weight: bold;
color: #322f2b;
}
}
}
/* Main Body text */
.cert-body-section {
margin-bottom: 35px;
.cert-main-text {
font-size: 15px;
line-height: 1.8;
text-align: center;
color: #4a453f;
margin-bottom: 25px;
.highlight-text {
color: #1a202c;
font-weight: 600;
border-bottom: 1px dotted #c5a059;
padding-bottom: 1px;
}
}
}
/* Issued To Details Block */
.issued-to-details {
border: 1px solid #f0e6d2;
background-color: #fbfaf5;
padding: 15px 20px;
border-radius: 4px;
margin: 20px auto;
max-width: 90%;
text-align: left;
.issued-to-title {
font-size: 10px;
font-weight: bold;
color: #c5a059;
letter-spacing: 1.5px;
margin-bottom: 6px;
}
.client-name-details {
font-size: 18px;
font-weight: bold;
color: #0b3c5d;
margin-bottom: 4px;
}
.client-ids {
font-size: 12px;
color: #666;
margin-bottom: 6px;
}
.client-address {
font-size: 12px;
color: #7b6f63;
line-height: 1.4;
}
}
/* Certificate Data Table */
.certificate-details-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 40px;
td {
padding: 10px 12px;
border: 1px solid #ede8db;
font-size: 13px;
}
.lbl-col {
background-color: #f7f5ee;
font-weight: bold;
color: #7b6f63;
width: 25%;
}
.val-col {
color: #322f2b;
width: 25%;
}
.highlight-row {
background-color: #f5f2e9;
border-top: 2px solid #c5a059;
}
.lbl-col-highlight {
font-weight: bold;
color: #0b3c5d;
font-size: 14px;
letter-spacing: 1px;
text-align: right;
padding-right: 20px;
}
.val-col-highlight {
font-size: 16px;
font-weight: bold;
color: #b8860b; /* Dark Goldenrod */
}
}
/* Certificate Footer Seal / Signatures */
.cert-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
.seal-container {
width: 25%;
.seal-ring {
width: 90px;
height: 90px;
border: 2px dashed #b8860b;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
transform: rotate(-15deg);
.seal-text {
font-size: 10px;
font-weight: bold;
color: #b8860b;
text-align: center;
line-height: 1.2;
letter-spacing: 1px;
}
}
}
.closing-text {
width: 45%;
font-size: 12px;
color: #7b6f63;
font-style: italic;
line-height: 1.5;
text-align: center;
}
.signatures-container {
width: 30%;
text-align: center;
.signature-block {
display: inline-block;
width: 80%;
.signature-line {
border-bottom: 1px solid #7b6f63;
margin-bottom: 8px;
height: 40px; /* Space for signature */
}
.signatory-title {
font-size: 12px;
color: #7b6f63;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
}
}
/* Print CSS */
@media print {
/* Everything below `::ng-deep` reaches outside this component's own template -- html/body and
the whole Angular Material CDK overlay chain never carry this component's scoping attribute
under the default emulated encapsulation, so a plain (scoped) selector would silently never
match any of them. `::ng-deep` is deprecated but still the correct tool for a component that
genuinely needs to override its host application's shell during print. */
/* The app shell pins html/body to a fixed viewport height with overflow hidden (its inner
panels handle their own scrolling on screen) -- left in place, that caps every printed page
at one viewport's worth of content and silently cuts off the rest instead of paginating. */
::ng-deep html,
::ng-deep body {
height: auto !important;
overflow: visible !important;
}
/* Hide non-overlay contents during print */
::ng-deep body > :not(.cdk-overlay-container) {
display: none !important;
}
/* The dialog's own scroll region clips anything past 80vh -- printing only captures what a
scrollable/max-height box shows on screen, so both constraints must be lifted or the
certificate gets cut off after its first visible slice. */
.dialog-content-container {
max-height: none !important;
overflow: visible !important;
}
/* CDK's overlay wrapper/backdrop/pane are all `position: fixed` (needed to center the dialog
on screen) and the wrapper flex-centers its content -- during print, a fixed-position
ancestor is clipped to one page's height, so anything taller than that gets cut off and only
a thin cross-section (here, just the certificate's own border) survives. Every ancestor in
that chain has to become a normal, statically-flowing block for the full certificate to print. */
::ng-deep .cdk-overlay-backdrop {
display: none !important;
}
::ng-deep .cdk-overlay-container,
::ng-deep .cdk-global-overlay-wrapper,
::ng-deep .cdk-overlay-pane,
::ng-deep .mat-mdc-dialog-container,
::ng-deep .mdc-dialog__container,
::ng-deep .mdc-dialog__surface {
position: static !important;
display: block !important;
max-height: none !important;
max-width: none !important;
height: auto !important;
width: auto !important;
overflow: visible !important;
transform: none !important;
}
/* Make certificate fill page */
#deposit-certificate-print {
position: static !important;
width: 100% !important;
height: auto !important;
padding: 0 !important;
margin: 0 !important;
background-color: #fff !important;
box-shadow: none !important;
}
.certificate-border-outer {
border-width: 8px !important;
background-color: #fff !important;
}
.no-print {
display: none !important;
}
::ng-deep .mat-mdc-dialog-container {
padding: 0 !important;
}
/* Compact every spacing/font value so the whole certificate fits a single printed page --
the on-screen sizing is generous by design, but that same height doesn't fit one page. */
.certificate-container {
padding: 6px !important;
}
.certificate-border-inner {
padding: 14px !important;
}
.certificate-header {
margin-bottom: 10px !important;
padding-bottom: 10px !important;
.bank-name {
font-size: 19px !important;
margin-bottom: 2px !important;
}
.bank-sub {
font-size: 10px !important;
margin-bottom: 8px !important;
}
.cert-title {
font-size: 19px !important;
letter-spacing: 2px !important;
margin-bottom: 2px !important;
}
.cert-subtitle {
font-size: 9px !important;
}
}
.cert-meta-row {
margin-bottom: 10px !important;
padding-bottom: 8px !important;
.meta-label {
font-size: 9px !important;
margin-bottom: 1px !important;
}
.meta-value {
font-size: 12px !important;
}
}
.cert-body-section {
margin-bottom: 10px !important;
.cert-main-text {
font-size: 11px !important;
line-height: 1.4 !important;
margin-bottom: 10px !important;
}
}
.issued-to-details {
padding: 8px 14px !important;
margin: 8px auto !important;
.client-name-details {
font-size: 14px !important;
}
.client-ids,
.client-address {
font-size: 10px !important;
}
}
.certificate-details-table {
margin-bottom: 12px !important;
td {
padding: 5px 10px !important;
font-size: 10px !important;
}
.lbl-col-highlight {
font-size: 12px !important;
}
.val-col-highlight {
font-size: 13px !important;
}
}
.cert-footer {
margin-top: 8px !important;
.seal-ring {
width: 60px !important;
height: 60px !important;
}
.seal-text {
font-size: 8px !important;
}
.closing-text {
font-size: 10px !important;
}
.signature-line {
height: 22px !important;
}
.signatory-title {
font-size: 10px !important;
}
}
}
@@ -0,0 +1,132 @@
/**
* 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, OnInit, inject } from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogRef,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose
} from '@angular/material/dialog';
import { ClientsService } from 'app/clients/clients.service';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { DateFormatPipe } from '../../pipes/date-format.pipe';
import { CurrencyPipe } from '@angular/common';
import { MatButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
/**
* Printable deposit certificate for a Fixed Deposit or Recurring Deposit account -- handed to the
* customer as physical proof of their deposit. Shared between FD and RD since both account shapes
* carry the same core fields (depositAmount, nominalAnnualInterestRate, depositPeriod, maturityDate,
* maturityAmount); RD additionally shows its installment/frequency details.
*/
@Component({
selector: 'mifosx-deposit-certificate-dialog',
templateUrl: './deposit-certificate-dialog.component.html',
styleUrls: ['./deposit-certificate-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose,
DateFormatPipe,
CurrencyPipe,
MatButton,
MatIcon
]
})
export class DepositCertificateDialogComponent implements OnInit {
dialogRef = inject<MatDialogRef<DepositCertificateDialogComponent>>(MatDialogRef);
data = inject(MAT_DIALOG_DATA);
clientsService = inject(ClientsService);
/** 'FD' or 'RD' -- controls the title and which extra rows show. */
accountType: 'FD' | 'RD';
accountData: any;
clientData: any;
clientAddress = '';
panNumber = 'Not Available';
certificateNo = '';
issueDate: Date = new Date();
ngOnInit() {
this.accountType = this.data.accountType;
this.accountData = this.data.accountData;
const clientId = this.data.clientId != null ? String(this.data.clientId) : null;
this.certificateNo = `${this.accountType}-CERT-${this.accountData.accountNo}`;
if (!clientId) {
return;
}
this.clientsService.getClientData(clientId).subscribe({
next: (client: any) => {
this.clientData = client;
},
error: () => {}
});
this.clientsService.getClientAddressData(clientId).subscribe({
next: (addresses: any[]) => {
if (addresses && addresses.length > 0) {
const permanent = addresses.find((a) => /permanent/i.test(a?.addressType || '')) || addresses[0];
this.clientAddress = this.formatAddress(permanent);
}
},
error: () => {}
});
this.clientsService.getClientIdentifiers(clientId).subscribe({
next: (identifiers: any[]) => {
const idList = Array.isArray(identifiers) ? identifiers : (identifiers as any)?.pageItems || [];
const panId = idList.find((id: any) => /pan/i.test(id?.documentType?.name || ''));
if (panId) {
this.panNumber = panId.documentKey;
}
},
error: () => {}
});
}
formatAddress(addr: any): string {
if (!addr) return '';
return [
addr.addressLine1,
addr.addressLine2,
addr.townVillage,
addr.city,
addr.stateName,
addr.countryName,
addr.postalCode
]
.filter((p) => !!p && String(p).trim())
.join(', ');
}
get depositDate(): any {
return this.accountData?.timeline?.activatedOnDate ?? this.accountData?.timeline?.submittedOnDate ?? null;
}
get tenureLabel(): string {
const period = this.accountData?.depositPeriod;
const freq = this.accountData?.depositPeriodFrequency?.value;
if (period == null) return 'N/A';
return freq ? `${period} ${freq}` : String(period);
}
printCertificate() {
window.print();
}
}
+5 -2
View File
@@ -1469,7 +1469,9 @@
"General Information": "General Information",
"Recurrence Settings": "Recurrence Settings",
"To Account": "To Account",
"Transfer Settings": "Transfer Settings"
"Transfer Settings": "Transfer Settings",
"Additional Settings (Legacy Account Type)": "Additional Settings (Legacy Account Type)",
"Interest Rate Detail": "Interest Rate Detail"
},
"inputs": {
"# Remittance": "# Remittance",
@@ -3085,7 +3087,8 @@
"to": "to",
"valid": "valid",
"Valid From": "Valid From",
"Valid Till": "Valid Till"
"Valid Till": "Valid Till",
"Field": "Field"
},
"links": {
"Community": "Community",