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
25 changed files with 3045 additions and 727 deletions
+7
View File
@@ -69,3 +69,10 @@ src/environments/.env.ts
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
# JVM crash / replay logs
hs_err_pid*.log
replay_pid*.log
# Microsoft Office temporary lock files
~$*.xlsx
@@ -33,6 +33,9 @@
<span class="flex-60">{{ loanProduct.description }}</span> <span class="flex-60">{{ loanProduct.description }}</span>
</div> </div>
} }
<ng-container
*ngTemplateOutlet="legacySections; context: { sections: customFieldSectionsByStep.details }"
></ng-container>
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Currency' | translate }}</h3> <h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Currency' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider> <mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row"> <div class="flex-fill layout-row">
@@ -298,6 +301,10 @@
<span class="flex-60">{{ loanProduct.minimumDaysBetweenDisbursalAndFirstRepayment | formatNumber }}</span> <span class="flex-60">{{ loanProduct.minimumDaysBetweenDisbursalAndFirstRepayment | formatNumber }}</span>
</div> </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> <h3 class="mat-h3 flex-fill">{{ 'labels.heading.Settings' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider> <mat-divider [inset]="true"></mat-divider>
@if (loanProduct.amortizationType?.value) { @if (loanProduct.amortizationType?.value) {
@@ -741,6 +748,9 @@
</div> </div>
</div> </div>
} }
<ng-container
*ngTemplateOutlet="legacySections; context: { sections: customFieldSectionsByStep.settings }"
></ng-container>
@if (loanProduct.charges.length) { @if (loanProduct.charges.length) {
<div class="flex-fill layout-row-wrap responsive-column"> <div class="flex-fill layout-row-wrap responsive-column">
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Charges' | translate }}</h3> <h3 class="mat-h3 flex-fill">{{ 'labels.heading.Charges' | translate }}</h3>
@@ -813,6 +823,9 @@
</table> </table>
</div> </div>
} }
<ng-container
*ngTemplateOutlet="legacySections; context: { sections: customFieldSectionsByStep.charges }"
></ng-container>
@if (isAdvancedPaymentAllocation) { @if (isAdvancedPaymentAllocation) {
<div class="flex-100 layout-row-wrap responsive-column"> <div class="flex-100 layout-row-wrap responsive-column">
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Income capitalization' | translate }}</h3> <h3 class="mat-h3 flex-100">{{ 'labels.inputs.Income capitalization' | translate }}</h3>
@@ -1271,5 +1284,73 @@
</div> </div>
</div> </div>
} }
<ng-container
*ngTemplateOutlet="legacySections; context: { sections: customFieldSectionsByStep.accounting }"
></ng-container>
</div> </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 { .repayment-strategy {
color: $status-approved; 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 PaymentAllocation
} from '../../loan-product-stepper/loan-product-payment-strategy-step/payment-allocation-model'; } from '../../loan-product-stepper/loan-product-payment-strategy-step/payment-allocation-model';
import { LoanProducts } from '../../loan-products'; 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 { CodeName, OptionData, StringEnumOptionData } from '../../../../shared/models/option-data.model';
import { Accounting } from 'app/core/utils/accounting'; import { Accounting } from 'app/core/utils/accounting';
import { DecimalPipe } from '@angular/common'; import { DecimalPipe } from '@angular/common';
@@ -51,6 +60,38 @@ import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
import { YesnoPipe } from '../../../../pipes/yesno.pipe'; import { YesnoPipe } from '../../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; 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({ @Component({
selector: 'mifosx-loan-product-summary', selector: 'mifosx-loan-product-summary',
templateUrl: './loan-product-summary.component.html', templateUrl: './loan-product-summary.component.html',
@@ -113,6 +154,12 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
'chargeOffReasonCodeValueId', 'chargeOffReasonCodeValueId',
'expenseAccountId' 'expenseAccountId'
]; ];
customFieldDisplayedColumns: string[] = [
'label',
'value'
];
readonly interestRateDetailColumns = INTEREST_RATE_DETAIL_COLUMNS;
readonly interestRateDetailDisplayedColumns: string[] = INTEREST_RATE_DETAIL_COLUMNS.map((column) => column.key);
accountingRuleData: string[] = []; accountingRuleData: string[] = [];
isAdvancedPaymentAllocation = false; isAdvancedPaymentAllocation = false;
@@ -128,6 +175,15 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
capitalizedIncomeClassificationToIncomeAccountMappings: ClassificationToIncomeAccountMapping[] = []; capitalizedIncomeClassificationToIncomeAccountMappings: ClassificationToIncomeAccountMapping[] = [];
writeOffReasonsToExpenseMappings: ChargeOffReasonToExpenseAccountMapping[] = []; 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() { ngOnInit() {
this.accountingRuleData = this.accounting.getAccountingRulesForLoans(); this.accountingRuleData = this.accounting.getAccountingRulesForLoans();
this.setCurrentValues(); this.setCurrentValues();
@@ -518,6 +574,8 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
this.loanProductsTemplate.advancedPaymentAllocationFutureInstallmentAllocationRules this.loanProductsTemplate.advancedPaymentAllocationFutureInstallmentAllocationRules
}; };
} }
this.setCustomFieldValues();
} }
optionDataLookUp(itemId: any, optionsData: any[]): OptionData { optionDataLookUp(itemId: any, optionsData: any[]): OptionData {
@@ -658,4 +716,119 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
mapHumanReadableValueStringEnumOptionDataList(incomingParameter: StringEnumOptionData[]): string[] { mapHumanReadableValueStringEnumOptionDataList(incomingParameter: StringEnumOptionData[]): string[] {
return incomingParameter.map((v) => v.value); 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"> <div class="lp-tab-content">
<mifosx-loan-product-details-step <mifosx-loan-product-details-step
[loanProductsTemplate]="loanProductsTemplate" [loanProductsTemplate]="loanProductsTemplate"
(productSubGroupChange)="setProductSubGroup($event)"
></mifosx-loan-product-details-step> ></mifosx-loan-product-details-step>
</div> </div>
</mat-tab> </mat-tab>
@@ -51,6 +52,7 @@
<mifosx-loan-product-settings-step <mifosx-loan-product-settings-step
[loanProductsTemplate]="loanProductsTemplate" [loanProductsTemplate]="loanProductsTemplate"
[isLinkedToFloatingInterestRates]="loanProductTermsForm.get('isLinkedToFloatingInterestRates')" [isLinkedToFloatingInterestRates]="loanProductTermsForm.get('isLinkedToFloatingInterestRates')"
[productSubGroup]="selectedProductSubGroup"
(advancePaymentStrategy)="advancePaymentStrategy($event)" (advancePaymentStrategy)="advancePaymentStrategy($event)"
></mifosx-loan-product-settings-step> ></mifosx-loan-product-settings-step>
</div> </div>
@@ -88,6 +88,7 @@ export class CreateLoanProductComponent implements OnInit {
selectedTabIndex = 0; selectedTabIndex = 0;
isAdvancedPaymentStrategy = false; isAdvancedPaymentStrategy = false;
selectedProductSubGroup = '';
paymentAllocation: PaymentAllocation[] = []; paymentAllocation: PaymentAllocation[] = [];
creditAllocation: PaymentAllocation[] = []; creditAllocation: PaymentAllocation[] = [];
supportedInterestRefundTypes: StringEnumOptionData[] = []; supportedInterestRefundTypes: StringEnumOptionData[] = [];
@@ -144,6 +145,10 @@ export class CreateLoanProductComponent implements OnInit {
return this.loanProductTermsStep.loanProductTermsForm; return this.loanProductTermsStep.loanProductTermsForm;
} }
setProductSubGroup(value: string) {
this.selectedProductSubGroup = value;
}
advancePaymentStrategy(value: string) { advancePaymentStrategy(value: string) {
this.isAdvancedPaymentStrategy = LoanProducts.isAdvancedPaymentAllocationStrategy(value); this.isAdvancedPaymentStrategy = LoanProducts.isAdvancedPaymentAllocationStrategy(value);
if (this.isAdvancedPaymentStrategy) { if (this.isAdvancedPaymentStrategy) {
@@ -244,6 +249,53 @@ export class CreateLoanProductComponent implements OnInit {
} }
get loanProductFormValid() { 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) { if (this.isAdvancedPaymentStrategy) {
return ( return (
this.loanProductDetailsForm.valid && this.loanProductDetailsForm.valid &&
@@ -39,6 +39,7 @@
<mifosx-loan-product-details-step <mifosx-loan-product-details-step
[loanProductsTemplate]="loanProductAndTemplate" [loanProductsTemplate]="loanProductAndTemplate"
(productSubGroupChange)="setProductSubGroup($event)"
></mifosx-loan-product-details-step> ></mifosx-loan-product-details-step>
</mat-step> </mat-step>
@@ -56,6 +57,7 @@
<mifosx-loan-product-settings-step <mifosx-loan-product-settings-step
[loanProductsTemplate]="loanProductAndTemplate" [loanProductsTemplate]="loanProductAndTemplate"
[isLinkedToFloatingInterestRates]="loanProductTermsForm.get('isLinkedToFloatingInterestRates')" [isLinkedToFloatingInterestRates]="loanProductTermsForm.get('isLinkedToFloatingInterestRates')"
[productSubGroup]="selectedProductSubGroup"
(advancePaymentStrategy)="advancePaymentStrategy($event)" (advancePaymentStrategy)="advancePaymentStrategy($event)"
> >
</mifosx-loan-product-settings-step> </mifosx-loan-product-settings-step>
@@ -95,6 +95,7 @@ export class EditLoanProductComponent implements OnInit {
itemsByDefault: GlobalConfiguration[] = []; itemsByDefault: GlobalConfiguration[] = [];
isAdvancedPaymentStrategy = false; isAdvancedPaymentStrategy = false;
selectedProductSubGroup = '';
wasPaymentAllocationChanged = false; wasPaymentAllocationChanged = false;
paymentAllocation: PaymentAllocation[] = []; paymentAllocation: PaymentAllocation[] = [];
creditAllocation: CreditAllocation[] = []; creditAllocation: CreditAllocation[] = [];
@@ -116,7 +117,9 @@ export class EditLoanProductComponent implements OnInit {
const loanProducts = this.loanProducts; const loanProducts = this.loanProducts;
this.route.data.subscribe((data: { loanProductAndTemplate: any; configurations: any }) => { 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 assetAccountData = this.loanProductAndTemplate.accountingMappingOptions.assetAccountOptions || [];
const liabilityAccountData = this.loanProductAndTemplate.accountingMappingOptions.liabilityAccountOptions || []; const liabilityAccountData = this.loanProductAndTemplate.accountingMappingOptions.liabilityAccountOptions || [];
this.loanProductAndTemplate.accountingMappingOptions.assetAndLiabilityAccountOptions = this.loanProductAndTemplate.accountingMappingOptions.assetAndLiabilityAccountOptions =
@@ -218,6 +221,10 @@ export class EditLoanProductComponent implements OnInit {
this.isAdvancedPaymentStrategy = LoanProducts.isAdvancedPaymentAllocationStrategy(value); this.isAdvancedPaymentStrategy = LoanProducts.isAdvancedPaymentAllocationStrategy(value);
} }
setProductSubGroup(value: string): void {
this.selectedProductSubGroup = value;
}
buildAdvancedPaymentAllocation(): void { buildAdvancedPaymentAllocation(): void {
this.advancedPaymentAllocations = this.advancedPaymentStrategy.buildAdvancedPaymentAllocationList( this.advancedPaymentAllocations = this.advancedPaymentStrategy.buildAdvancedPaymentAllocationList(
this.loanProductAndTemplate 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 { Component, Input, OnChanges, OnInit, SimpleChanges, inject } from '@angular/core';
import { UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
import { patchCustomFields } from '../../loan-product-custom-fields';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
@@ -210,7 +211,14 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
intOverdueReserveAccountId: accountingMappings.intOverdueReserveAccount?.id || '', intOverdueReserveAccountId: accountingMappings.intOverdueReserveAccount?.id || '',
undebitedInterestAccountId: accountingMappings.undebitedInterestAccount?.id || '', undebitedInterestAccountId: accountingMappings.undebitedInterestAccount?.id || '',
penalIntCrGlAccountId: accountingMappings.penalIntCrGlAccount?.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) { if (this.loanProductsTemplate.paymentChannelToFundSourceMappings) {
@@ -234,6 +242,10 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
this.setOtherGlMappings('incomeOtherGlMappings', glMappings.income || [], this.incomeAccountData); 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() { public createLoanProductAccountingForm() {
@@ -276,16 +288,10 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
'incomeFromGoodwillCreditInterestAccountId', 'incomeFromGoodwillCreditInterestAccountId',
'incomeFromGoodwillCreditFeesAccountId', 'incomeFromGoodwillCreditFeesAccountId',
'incomeFromGoodwillCreditPenaltyAccountId', 'incomeFromGoodwillCreditPenaltyAccountId',
'receivableInterestAccountId',
'receivableFeeAccountId',
'receivablePenaltyAccountId',
'intReceivablesGlAccountId', 'intReceivablesGlAccountId',
'intPayableGlAccountId', 'intPayableGlAccountId',
'deafIntPayableGlAccountId', 'glAccountId',
'intOverdueReserveAccountId', 'inoperativeGlAccountId'
'undebitedInterestAccountId',
'penalIntCrGlAccountId',
'rdMaturedGlAccountId'
]; ];
commonFields.forEach((field) => { commonFields.forEach((field) => {
if (!this.loanProductAccountingForm.contains(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')) { if (!this.loanProductAccountingForm.contains('advancedAccountingRules')) {
this.loanProductAccountingForm.addControl('advancedAccountingRules', new UntypedFormControl(false)); this.loanProductAccountingForm.addControl('advancedAccountingRules', new UntypedFormControl(false));
} }
@@ -620,7 +664,7 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
} }
public isAccrualAccounting(): boolean { public isAccrualAccounting(): boolean {
const accountingRule = this.loanProductAccountingForm.value.accountingRule; const accountingRule = this.loanProductAccountingForm.get('accountingRule').value;
return accountingRule === 3 || accountingRule === 4; return accountingRule === 3 || accountingRule === 4;
} }
@@ -708,15 +752,15 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
{ {
key: 'intReceivablesGlAccountId', key: 'intReceivablesGlAccountId',
label: 'Int. Receivables GL A/c', label: 'Int. Receivables GL A/c',
inputLabel: 'Int. Receivables GL A/c', inputLabel: 'Int Receivables GL A/c',
controlName: 'intReceivablesGlAccountId', controlName: 'intReceivablesGlAccountId',
options: this.assetAccountData, options: this.assetAccountData,
required: false required: true
}, },
{ {
key: 'deafIntPayableGlAccountId', key: 'deafIntPayableGlAccountId',
label: 'Deaf Int. Payble GL A/c', label: 'Deaf Int. Payble GL A/c',
inputLabel: 'Deaf Int. Payble GL A/c', inputLabel: 'Deaf Int Payble GL A/c',
controlName: 'deafIntPayableGlAccountId', controlName: 'deafIntPayableGlAccountId',
options: this.assetAccountData, options: this.assetAccountData,
required: false required: false
@@ -724,7 +768,7 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
{ {
key: 'intOverdueReserveAccountId', key: 'intOverdueReserveAccountId',
label: 'Int. Overdue Reserve A/c (AS)', label: 'Int. Overdue Reserve A/c (AS)',
inputLabel: 'Int. Overdue Reserve A/c', inputLabel: 'Int Overdue Reserve A/c',
controlName: 'intOverdueReserveAccountId', controlName: 'intOverdueReserveAccountId',
options: this.assetAccountData, options: this.assetAccountData,
required: false required: false
@@ -736,6 +780,38 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
controlName: 'rdMaturedGlAccountId', controlName: 'rdMaturedGlAccountId',
options: this.assetAccountData, options: this.assetAccountData,
required: false 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', otherMappingsControlName: 'assetsOtherGlMappings',
@@ -848,10 +924,18 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
{ {
key: 'penalIntCrGlAccountId', key: 'penalIntCrGlAccountId',
label: 'Penal Int. Cr. GL A/c', label: 'Penal Int. Cr. GL A/c',
inputLabel: 'Penal Int. Cr. GL A/c', inputLabel: 'Penal Int Cr GL A/c',
controlName: 'penalIntCrGlAccountId', controlName: 'penalIntCrGlAccountId',
options: this.incomeAccountData, options: this.incomeAccountData,
required: false 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', otherMappingsControlName: 'incomeOtherGlMappings',
@@ -903,6 +987,14 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
options: this.expenseAccountData, options: this.expenseAccountData,
required: true, required: true,
deferredBuyDownMerchantOnly: 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', otherMappingsControlName: 'expensesOtherGlMappings',
@@ -934,10 +1026,10 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
{ {
key: 'intPayableGlAccountId', key: 'intPayableGlAccountId',
label: 'Int. Payable GL A/c', label: 'Int. Payable GL A/c',
inputLabel: 'Int. Payable GL A/c', inputLabel: 'Int Payable GL A/c',
controlName: 'intPayableGlAccountId', controlName: 'intPayableGlAccountId',
options: this.liabilityAccountData, options: this.liabilityAccountData,
required: false required: true
}, },
{ {
key: 'undebitedInterestAccountId', key: 'undebitedInterestAccountId',
@@ -946,6 +1038,14 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges {
controlName: 'undebitedInterestAccountId', controlName: 'undebitedInterestAccountId',
options: this.liabilityAccountData, options: this.liabilityAccountData,
required: false 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', otherMappingsControlName: 'liabilitiesOtherGlMappings',
@@ -6,7 +6,11 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/. 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">
<div class="lpch-section__header"> <div class="lpch-section__header">
<i class="fas fa-receipt"></i> <i class="fas fa-receipt"></i>
@@ -89,7 +93,6 @@
</div> </div>
</div> </div>
<!-- Overdue Charges -->
<div class="lpch-section"> <div class="lpch-section">
<div class="lpch-section__header"> <div class="lpch-section__header">
<i class="fas fa-exclamation-circle"></i> <i class="fas fa-exclamation-circle"></i>
@@ -171,3 +174,174 @@
} }
</div> </div>
</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 { .lpch-table {
width: 100%; 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 { 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 { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@@ -51,12 +52,16 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatRow, MatRow,
ChargesFilterPipe, ChargesFilterPipe,
ChargesPenaltyFilterPipe, ChargesPenaltyFilterPipe,
FormatNumberPipe FormatNumberPipe,
ReactiveFormsModule
] ]
}) })
export class LoanProductChargesStepComponent implements OnInit { export class LoanProductChargesStepComponent implements OnInit {
dialog = inject(MatDialog); dialog = inject(MatDialog);
private translateService = inject(TranslateService); private translateService = inject(TranslateService);
private formBuilder = inject(UntypedFormBuilder);
legacyChargesForm: UntypedFormGroup;
@Input() loanProductsTemplate: any; @Input() loanProductsTemplate: any;
@Input() currencyCode: UntypedFormControl; @Input() currencyCode: UntypedFormControl;
@@ -76,6 +81,48 @@ export class LoanProductChargesStepComponent implements OnInit {
pristine = true; 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() { ngOnInit() {
this.chargeData = this.loanProductsTemplate.chargeOptions; this.chargeData = this.loanProductsTemplate.chargeOptions;
this.overdueChargeData = this.loanProductsTemplate.penaltyOptions this.overdueChargeData = this.loanProductsTemplate.penaltyOptions
@@ -89,6 +136,10 @@ export class LoanProductChargesStepComponent implements OnInit {
this.currencyCode.valueChanges.subscribe(() => (this.chargesDataSource = [])); this.currencyCode.valueChanges.subscribe(() => (this.chargesDataSource = []));
this.multiDisburseLoan.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) { addCharge(charge: any) {
@@ -112,7 +163,8 @@ export class LoanProductChargesStepComponent implements OnInit {
get loanProductCharges() { get loanProductCharges() {
return { 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"> <form [formGroup]="loanProductDetailsForm">
<!-- Product Classification + General Information — merged single section, 2-column layout -->
<div class="lpd-section"> <div class="lpd-section">
<div class="lpd-section__header"> <div class="lpd-section__header">
<i class="fas fa-info-circle"></i> <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>
<div class="lpd-grid"> <div class="lpd-two-col">
<mat-form-field appearance="outline" class="lpd-field lpd-field--full"> <div class="lpd-two-col__column">
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label> <mat-form-field appearance="outline" class="lpd-field">
<input <mat-label>Account Type Group</mat-label>
matInput <mat-select formControlName="productTypeGroup">
formControlName="name" @for (group of productTypeGroups; track group.id) {
matTooltip="{{ 'tooltips.Product name is a unique identifier' | translate }}" <mat-option [value]="group.id">{{ group.label }}</mat-option>
required }
/> </mat-select>
<mat-error> </mat-form-field>
{{ '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"> @if (availableSubGroups.length) {
<mat-label>{{ 'labels.inputs.Short Name' | translate }}</mat-label> <mat-form-field appearance="outline" class="lpd-field">
<input <mat-label>Product Type Sub Group</mat-label>
matInput <mat-select formControlName="productTypeSubGroup">
formControlName="shortName" @for (subGroup of availableSubGroups; track subGroup.id) {
maxlength="4" <mat-option [value]="subGroup.id">{{ subGroup.label }}</mat-option>
matTooltip="{{ 'tooltips.Short name is a unique identifier' | translate }}" }
required </mat-select>
/> </mat-form-field>
<mat-error> }
{{ 'labels.inputs.Short Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong> <mat-form-field appearance="outline" class="lpd-field">
</mat-error> <mat-label>A/c. Type Description</mat-label>
</mat-form-field> <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>
</div> </div>
@@ -97,86 +201,94 @@
<i class="fas fa-percentage"></i> <i class="fas fa-percentage"></i>
<h3>Interest Information</h3> <h3>Interest Information</h3>
</div> </div>
<div class="lpd-grid"> <div class="lpd-two-col">
<mat-form-field appearance="outline" class="lpd-field"> <div class="lpd-two-col__column">
<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) {
<mat-form-field appearance="outline" class="lpd-field"> <mat-form-field appearance="outline" class="lpd-field">
<mat-label>Rounding UpTo</mat-label> <mat-label>Debit Int. Calculation</mat-label>
<input matInput type="number" formControlName="roundingUpTo" min="0" /> <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>
}
<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>
</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">
<div class="lpd-section__header"> <div class="lpd-section__header">
<i class="fas fa-sitemap"></i> <i class="fas fa-ban"></i>
<h3>Product Classification</h3> <h3>PPS Limit &amp; Write Off Details</h3>
</div> </div>
<div class="lpd-grid"> <div class="lpd-grid">
<mat-form-field appearance="outline" class="lpd-field"> <mat-form-field appearance="outline" class="lpd-field">
<mat-label>Product Type Group</mat-label> <mat-label>PPS Limit</mat-label>
<mat-select formControlName="productTypeGroup"> <input matInput type="number" formControlName="ppsLimit" />
@for (group of productTypeGroups; track group.id) { </mat-form-field>
<mat-option [value]="group.id">{{ group.label }}</mat-option>
} <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-select>
</mat-form-field> </mat-form-field>
@if (loanProductDetailsForm.get('productTypeGroup')?.value) { @if (loanProductDetailsForm.get('isWriteOffAccountType')?.value === 'YES') {
<mat-form-field appearance="outline" class="lpd-field"> <mat-form-field appearance="outline" class="lpd-field">
<mat-label>GL Account ({{ glClassificationLabel }})</mat-label> <mat-label>Write Off Actyp</mat-label>
<mat-select formControlName="glAccount"> <input matInput formControlName="writeOffAccountTypeCode" />
@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-form-field> </mat-form-field>
} }
</div> </div>
@@ -46,9 +46,10 @@ $gray-900: #202124;
.lpd-grid { .lpd-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 4px 16px; align-items: start;
padding: 16px 24px; gap: 18px 16px;
padding: 20px 24px;
@media (width <= 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -57,6 +58,7 @@ $gray-900: #202124;
.lpd-field { .lpd-field {
width: 100%; width: 100%;
min-width: 0;
&--full { &--full {
grid-column: 1 / -1; grid-column: 1 / -1;
@@ -69,6 +71,43 @@ $gray-900: #202124;
padding: 4px 0; 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 // GL Section
.lpd-gl-section { .lpd-gl-section {
border-top: 2px solid #e0f2fe; border-top: 2px solid #e0f2fe;
@@ -7,8 +7,9 @@
*/ */
/** Angular Imports */ /** 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 { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { CUSTOM_DATE_FIELDS, patchCustomFields } from '../../loan-product-custom-fields';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
/** Custom Services */ /** Custom Services */
@@ -33,6 +34,7 @@ export class LoanProductDetailsStepComponent implements OnInit {
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
@Input() loanProductsTemplate: any; @Input() loanProductsTemplate: any;
@Output() productSubGroupChange = new EventEmitter<string>();
loanProductDetailsForm: UntypedFormGroup; loanProductDetailsForm: UntypedFormGroup;
@@ -49,6 +51,10 @@ export class LoanProductDetailsStepComponent implements OnInit {
this.createLoanProductDetailsForm(); this.createLoanProductDetailsForm();
} }
assetAccountOptions: any[] = [];
incomeAccountOptions: any[] = [];
liabilityAccountOptions: any[] = [];
ngOnInit() { ngOnInit() {
this.loanProductDetailsForm.patchValue({ this.loanProductDetailsForm.patchValue({
name: this.loanProductsTemplate.name, name: this.loanProductsTemplate.name,
@@ -58,10 +64,6 @@ export class LoanProductDetailsStepComponent implements OnInit {
closeDate: this.loanProductsTemplate.closeDate && new Date(this.loanProductsTemplate.closeDate) 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({ this.loanProductDetailsForm.patchValue({
debitIntCalculation: this.loanProductsTemplate.debitIntCalculation ?? false, debitIntCalculation: this.loanProductsTemplate.debitIntCalculation ?? false,
separatePenalInterest: this.loanProductsTemplate.separatePenalInterest ?? false, separatePenalInterest: this.loanProductsTemplate.separatePenalInterest ?? false,
@@ -70,66 +72,63 @@ export class LoanProductDetailsStepComponent implements OnInit {
finalIntRounding: this.loanProductsTemplate.finalIntRounding ?? false, finalIntRounding: this.loanProductsTemplate.finalIntRounding ?? false,
roundingUpTo: this.loanProductsTemplate.roundingUpTo ?? null 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 */ /** Product Classification data */
readonly productTypeGroups = [ readonly productTypeGroups = [
{ id: 'loans', label: 'Loan' }, { id: 'advances', label: 'Advances A/C' },
{ id: 'advanced', label: 'Advanced' } { id: 'loans', label: 'Loans A/C' }
]; ];
readonly productTypeSubGroups: Record<string, { id: string; label: string }[]> = { readonly productTypeSubGroups: Record<string, { id: string; label: string }[]> = {
loans: [ // Renewable, Drawing-Power/limit-based revolving credit facilities
{ id: 'emi', label: 'EMI' }, advances: [
{ id: 'cc', label: 'CC (Cash Credit)' }, { id: 'cc', label: 'Cash Credit (CC)' },
{ id: 'overdraft', label: 'Overdraft' }, { id: 'overdraft', label: 'Overdraft (OD)' },
{ id: 'warehouse', label: 'Warehouse' }, { id: 'gold', label: 'Gold Loan' },
{ id: 'gold', label: 'Gold' } { id: 'warehouse', label: 'Warehouse Receipt / Pledge Loan' },
{ id: 'kcc', label: 'Kisan Credit Card (KCC)' },
{ id: 'bills', label: 'Bills Purchased / Discounted' }
], ],
advanced: [ // Fixed disbursement, fixed tenure, EMI-based term credit facilities
{ id: 'fd', label: 'FD (Fixed Deposit)' }, loans: [
{ id: 'rd', label: 'RD (Recurring Deposit)' } { 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() { get availableSubGroups() {
const group = this.loanProductDetailsForm.get('productTypeGroup')?.value; const group = this.loanProductDetailsForm.get('productTypeGroup')?.value;
return group ? (this.productTypeSubGroups[group] ?? []) : []; 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 = [ readonly yesNoOptions = [
{ value: true, label: 'YES' }, { value: true, label: 'YES' },
{ value: false, label: 'NO' } { value: false, label: 'NO' }
@@ -150,28 +149,46 @@ export class LoanProductDetailsStepComponent implements OnInit {
closeDate: [''], closeDate: [''],
productTypeGroup: [''], productTypeGroup: [''],
productTypeSubGroup: [''], productTypeSubGroup: [''],
glClassification: ['asset'],
glAccount: [''],
debitIntCalculation: [false], debitIntCalculation: [false],
separatePenalInterest: [false], separatePenalInterest: [false],
creditIntCalculation: [false], creditIntCalculation: [false],
subAccountTypeBalance: [false], subAccountTypeBalance: [false],
finalIntRounding: [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('productTypeSubGroup')?.valueChanges.subscribe((subGroup: string) => {
this.loanProductDetailsForm.get('productTypeGroup')?.valueChanges.subscribe((group: string) => { this.productSubGroupChange.emit(subGroup);
const defaultVal = this.glDefaultMap[group] ?? 'asset';
this.loanProductDetailsForm.get('glClassification')?.setValue(defaultVal);
this.loanProductDetailsForm.get('glAccount')?.setValue('');
}); });
} }
get loanProductDetails() { get loanProductDetails() {
const loanProductDetailsFormData = this.loanProductDetailsForm.value; // getRawValue() rather than value: it returns a fresh object, so the date reformatting below
const prevStartDate: Date = this.loanProductDetailsForm.value.startDate; // cannot write back into the form's own cached value, and any control disabled later still
const prevCloseDate: Date = this.loanProductDetailsForm.value.closeDate; // 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; const dateFormat = this.settingsService.dateFormat;
if (loanProductDetailsFormData.startDate instanceof Date) { if (loanProductDetailsFormData.startDate instanceof Date) {
loanProductDetailsFormData.startDate = this.dateUtils.formatDate(prevStartDate, dateFormat) || ''; loanProductDetailsFormData.startDate = this.dateUtils.formatDate(prevStartDate, dateFormat) || '';
@@ -179,6 +196,17 @@ export class LoanProductDetailsStepComponent implements OnInit {
if (loanProductDetailsFormData.closeDate instanceof Date) { if (loanProductDetailsFormData.closeDate instanceof Date) {
loanProductDetailsFormData.closeDate = this.dateUtils.formatDate(prevCloseDate, dateFormat) || ''; 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; return loanProductDetailsFormData;
} }
} }
@@ -235,209 +235,361 @@
</div> </div>
</div> </div>
<!-- Cheque Book Settings --> <mat-accordion class="lps-accordion" multi="true">
<div class="lps-section"> @if (showChequeBookSettings) {
<div class="lps-section__header"> <mat-expansion-panel [expanded]="true">
<i class="fas fa-money-check lps-section__icon"></i> <mat-expansion-panel-header>
<h3 class="lps-section__title">Cheque Book Settings</h3> <mat-panel-title>
</div> <i class="fas fa-money-check lps-section__icon"></i>
<div class="lps-section__body lps-grid-4"> Cheque Book Settings
<mat-form-field appearance="outline"> </mat-panel-title>
<mat-label>Cheque Book Issue Enabled</mat-label> </mat-expansion-panel-header>
<mat-select formControlName="chequeBookIssueEnabled"> <div class="lps-section__body lps-grid-4">
<mat-option value="YES">YES</mat-option> <mat-form-field appearance="outline">
<mat-option value="NO">NO</mat-option> <mat-label>Cheque Book Issue Enabled</mat-label>
</mat-select> <mat-select formControlName="chequeBookIssueEnabled">
</mat-form-field> <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-form-field appearance="outline">
<mat-label>Cheque Validation Required</mat-label> <mat-label>Cheque Validation Required</mat-label>
<mat-select formControlName="chequeValidationRequired"> <mat-select formControlName="chequeValidationRequired">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option> <mat-option value="NO">NO</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Is First Cheque Book Free</mat-label> <mat-label>Is First Cheque Book Free</mat-label>
<mat-select formControlName="isFirstChequeBookFree"> <mat-select formControlName="isFirstChequeBookFree">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option> <mat-option value="NO">NO</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Cheque Printing Account Type</mat-label> <mat-label>Cheque Printing Account Type</mat-label>
<mat-select formControlName="chequePrintingAccountType"> <mat-select formControlName="chequePrintingAccountType">
<mat-option value="SAVINGS_BANK">Savings Bank</mat-option> <mat-option value="SAVINGS_BANK">Savings Bank</mat-option>
<mat-option value="CURRENT_ACCOUNT">Current Account</mat-option> <mat-option value="CURRENT_ACCOUNT">Current Account</mat-option>
<mat-option value="CASH_CREDIT">Cash Credit</mat-option> <mat-option value="CASH_CREDIT">Cash Credit</mat-option>
<mat-option value="OVERDRAFT">Overdraft</mat-option> <mat-option value="OVERDRAFT">Overdraft</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Allow Withdrawal Slip Usage</mat-label> <mat-label>Allow Withdrawal Slip Usage</mat-label>
<mat-select formControlName="allowWithdrawalSlipUsage"> <mat-select formControlName="allowWithdrawalSlipUsage">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option> <mat-option value="NO">NO</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Maintain Cheque Book Stock Registry</mat-label> <mat-label>Maintain Cheque Book Stock Registry</mat-label>
<mat-select formControlName="maintainChequeBookStockRegistry"> <mat-select formControlName="maintainChequeBookStockRegistry">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option> <mat-option value="NO">NO</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Free Cheque Books Per Year</mat-label> <mat-label>Free Cheque Books Per Year</mat-label>
<input type="number" matInput formControlName="freeChequeBooksPerYear" /> <input type="number" matInput formControlName="freeChequeBooksPerYear" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Allow Credit By Cheque Number</mat-label> <mat-label>Allow Credit By Cheque Number</mat-label>
<mat-select formControlName="allowCreditByChequeNumber"> <mat-select formControlName="allowCreditByChequeNumber">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option> <mat-option value="NO">NO</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Credit Cheque Validation Required</mat-label> <mat-label>Credit Cheque Validation Required</mat-label>
<mat-select formControlName="creditChequeValidationRequired"> <mat-select formControlName="creditChequeValidationRequired">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option> <mat-option value="NO">NO</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Grace Days For Stock Statement</mat-label> <mat-label>Grace Days For Stock Statement</mat-label>
<input type="number" matInput formControlName="graceDaysForStockStatement" /> <input type="number" matInput formControlName="graceDaysForStockStatement" />
</mat-form-field> </mat-form-field>
</div> </div>
</div> </mat-expansion-panel>
}
<!-- Additional Product Settings --> <mat-expansion-panel [expanded]="true">
<div class="lps-section"> <mat-expansion-panel-header>
<div class="lps-section__header"> <mat-panel-title>
<i class="fas fa-cogs lps-section__icon"></i> <i class="fas fa-cogs lps-section__icon"></i>
<h3 class="lps-section__title">Additional Product Settings</h3> Additional Product Settings
</div> </mat-panel-title>
<div class="lps-section__body lps-grid-4"> </mat-expansion-panel-header>
<mat-form-field appearance="outline"> <div class="lps-section__body lps-grid-4">
<mat-label>Account Type Active For Transactions</mat-label> <mat-form-field appearance="outline">
<mat-select formControlName="accountTypeActiveForTransactions"> <mat-label>Account Type Active For Transactions</mat-label>
<mat-option value="YES">YES</mat-option> <mat-select formControlName="accountTypeActiveForTransactions">
<mat-option value="NO">NO</mat-option> <mat-option value="YES">YES</mat-option>
</mat-select> <mat-option value="NO">NO</mat-option>
</mat-form-field> </mat-select>
</mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Sub Type Activity Enabled</mat-label> <mat-label>Sub Type Activity Enabled</mat-label>
<mat-select formControlName="subTypeActivityEnabled"> <mat-select formControlName="subTypeActivityEnabled">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option> <mat-option value="NO">NO</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Account Opening Charges</mat-label> <mat-label>Account Opening Charges</mat-label>
<input type="number" matInput formControlName="accountOpeningCharges" /> <input type="number" matInput formControlName="accountOpeningCharges" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Maximum Gold Loan Limit</mat-label> <mat-label>Maximum Gold Loan Limit</mat-label>
<input type="number" matInput formControlName="maximumGoldLoanLimit" /> <input type="number" matInput formControlName="maximumGoldLoanLimit" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Last Account Number Opened</mat-label> <mat-label>Gold Loan [Packet No.] With Year?</mat-label>
<input type="text" matInput formControlName="lastAccountNumberOpened" /> <mat-select formControlName="goldLoanPacketNumberWithYearEnabled">
</mat-form-field> <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-form-field appearance="outline">
<mat-label>Customer Termination Allowed</mat-label> <mat-label>Last Account Number Opened</mat-label>
<mat-select formControlName="customerTerminationAllowed"> <input type="text" matInput formControlName="lastAccountNumberOpened" />
<mat-option value="YES">YES</mat-option> </mat-form-field>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Charges For Cash Credit Above Limit</mat-label> <mat-label>Customer Termination Allowed</mat-label>
<input type="number" matInput formControlName="chargesForCashCreditAboveLimit" /> <mat-select formControlName="customerTerminationAllowed">
</mat-form-field> <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-form-field appearance="outline">
<mat-label>Death Interest GL Account</mat-label> <mat-label>Charges For Cash Credit Above Limit</mat-label>
<input type="text" matInput formControlName="deathInterestGLAccount" /> <input type="number" matInput formControlName="chargesForCashCreditAboveLimit" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Starting Ledger Number</mat-label> <mat-label>Death Interest GL Account</mat-label>
<input type="number" matInput formControlName="startingLedgerNumber" /> <input type="text" matInput formControlName="deathInterestGLAccount" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Ending Ledger Number</mat-label> <mat-label>Starting Ledger Number</mat-label>
<input type="number" matInput formControlName="endingLedgerNumber" /> <input type="number" matInput formControlName="startingLedgerNumber" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Current Ledger Number</mat-label> <mat-label>Ending Ledger Number</mat-label>
<input type="number" matInput formControlName="currentLedgerNumber" /> <input type="number" matInput formControlName="endingLedgerNumber" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Remittance Account Number</mat-label> <mat-label>Current Ledger Number</mat-label>
<input type="text" matInput formControlName="remittanceAccountNumber" /> <input type="number" matInput formControlName="currentLedgerNumber" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Is Non Resident Indian Account</mat-label> <mat-label>Remittance Account Number</mat-label>
<mat-select formControlName="isNonResidentIndianAccount"> <input type="text" matInput formControlName="remittanceAccountNumber" />
<mat-option value="YES">YES</mat-option> </mat-form-field>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Debit Clearing GL Account</mat-label> <mat-label>Is Non Resident Indian Account</mat-label>
<input type="text" matInput formControlName="debitClearingGLAccount" /> <mat-select formControlName="isNonResidentIndianAccount">
</mat-form-field> <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-form-field appearance="outline">
<mat-label>Missed Call Alert Service Active</mat-label> <mat-label>Debit Clearing GL Account</mat-label>
<mat-select formControlName="missedCallAlertServiceActive"> <input type="text" matInput formControlName="debitClearingGLAccount" />
<mat-option value="YES">YES</mat-option> </mat-form-field>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Standing Instruction Charge GL Account</mat-label> <mat-label>Missed Call Alert Service Active</mat-label>
<input type="text" matInput formControlName="standingInstructionChargeGLAccount" /> <mat-select formControlName="missedCallAlertServiceActive">
</mat-form-field> <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-form-field appearance="outline">
<mat-label>Standing Instruction Debit Charge Skip Option</mat-label> <mat-label>Standing Instruction Charge GL Account</mat-label>
<mat-select formControlName="standingInstructionDebitChargeSkipOption"> <input type="text" matInput formControlName="standingInstructionChargeGLAccount" />
<mat-option value="SKIP_ALWAYS">Skip Always</mat-option> </mat-form-field>
<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-form-field appearance="outline">
<mat-label>Opening Balance Multiplier For Remittance Charges</mat-label> <mat-label>Standing Instruction Debit Charge Skip Option</mat-label>
<input type="number" matInput formControlName="openingBalanceMultiplierForRemittanceCharges" /> <mat-select formControlName="standingInstructionDebitChargeSkipOption">
</mat-form-field> <mat-option value="SKIP_ALWAYS">Skip Always</mat-option>
</div> <mat-option value="SKIP_ON_FIRST_FAILURE">Skip on First Failure</mat-option>
</div> <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) --> <!-- Interest Related Parameters (Page 3) -->
<div class="lps-section"> <div class="lps-section">
@@ -754,6 +906,85 @@
<mat-label>Add Penal Interest Rate</mat-label> <mat-label>Add Penal Interest Rate</mat-label>
<input type="number" matInput formControlName="addPenalInterestRate" /> <input type="number" matInput formControlName="addPenalInterestRate" />
</mat-form-field> </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>
</div> </div>
@@ -801,6 +1032,306 @@
</div> </div>
</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) --> <!-- Credit Bureau Settings (CIBIL) -->
<div class="lps-section"> <div class="lps-section">
<div class="lps-section__header"> <div class="lps-section__header">
@@ -883,6 +1414,11 @@
<input type="number" min="0" matInput formControlName="graceOnInterestPayment" /> <input type="number" min="0" matInput formControlName="graceOnInterestPayment" />
</mat-form-field> </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-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Delinquency Bucket' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Delinquency Bucket' | translate }}</mat-label>
<mat-select formControlName="delinquencyBucketId"> <mat-select formControlName="delinquencyBucketId">
@@ -50,8 +50,9 @@
&__body { &__body {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 4px 16px; align-items: start;
padding: 16px 24px; gap: 18px 16px;
padding: 20px 24px;
@media (width <= 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -83,6 +84,26 @@
grid-column: span 3; 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 { .lps-checkbox-row {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -15,12 +15,25 @@ import {
ReactiveFormsModule ReactiveFormsModule
} from '@angular/forms'; } from '@angular/forms';
import { LoanProducts } from '../../loan-products'; 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 { GlobalConfiguration } from 'app/system/configurations/global-configurations-tab/configuration.model';
import { CodeName, OptionData, StringEnumOptionData } from 'app/shared/models/option-data.model'; import { CodeName, OptionData, StringEnumOptionData } from 'app/shared/models/option-data.model';
import { ProcessingStrategyService } from '../../services/processing-strategy.service'; import { ProcessingStrategyService } from '../../services/processing-strategy.service';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { MatCheckbox } from '@angular/material/checkbox'; import { MatCheckbox } from '@angular/material/checkbox';
import { MatIconButton } from '@angular/material/button'; import { MatIconButton } from '@angular/material/button';
import {
MatAccordion,
MatExpansionPanel,
MatExpansionPanelHeader,
MatExpansionPanelTitle
} from '@angular/material/expansion';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -34,6 +47,10 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatTooltip, MatTooltip,
MatCheckbox, MatCheckbox,
MatIconButton, MatIconButton,
MatAccordion,
MatExpansionPanel,
MatExpansionPanelHeader,
MatExpansionPanelTitle,
FaIconComponent, FaIconComponent,
ReactiveFormsModule ReactiveFormsModule
] ]
@@ -45,8 +62,27 @@ export class LoanProductSettingsStepComponent implements OnInit {
@Input() toEdit: boolean; @Input() toEdit: boolean;
@Input() loanProductsTemplate: any; @Input() loanProductsTemplate: any;
@Input() isLinkedToFloatingInterestRates: UntypedFormControl; @Input() isLinkedToFloatingInterestRates: UntypedFormControl;
@Input() productSubGroup: string;
@Output() advancePaymentStrategy = new EventEmitter<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; loanProductSettingsForm: UntypedFormGroup;
amortizationTypeData: any; 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() { createLoanProductSettingsForm() {
@@ -366,6 +406,24 @@ export class LoanProductSettingsStepComponent implements OnInit {
standingInstructionChargeGLAccount: [''], standingInstructionChargeGLAccount: [''],
standingInstructionDebitChargeSkipOption: [''], standingInstructionDebitChargeSkipOption: [''],
openingBalanceMultiplierForRemittanceCharges: [''], 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 // Interest Related Parameters
drawingPowerAndLimitGenerationProcedure: ['NORMAL D.P. ENTRY IS CONSIDER'], drawingPowerAndLimitGenerationProcedure: ['NORMAL D.P. ENTRY IS CONSIDER'],
@@ -385,7 +443,7 @@ export class LoanProductSettingsStepComponent implements OnInit {
penaltyInterestDateDifferentThanRegularInterest: ['YES'], penaltyInterestDateDifferentThanRegularInterest: ['YES'],
fixedDepositOverdraftCertificateSortOrder: ['ORDER ON INTEREST RATE'], fixedDepositOverdraftCertificateSortOrder: ['ORDER ON INTEREST RATE'],
useDocumentSubmissionDateForInterest: ['NO'], useDocumentSubmissionDateForInterest: ['NO'],
loanManagementSystemRenewalPenaltyActive: [''], loanManagementSystemRenewalPenaltyActive: ['NO'],
minimumNormalInterestAmount: [''], minimumNormalInterestAmount: [''],
minimumInterestCalculationMethod: [''], minimumInterestCalculationMethod: [''],
minimumAgainstClearingInterestAmount: [''], minimumAgainstClearingInterestAmount: [''],
@@ -409,6 +467,18 @@ export class LoanProductSettingsStepComponent implements OnInit {
nonPerformingAssetPenalInterestRate: [''], nonPerformingAssetPenalInterestRate: [''],
fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess: ['YES'], fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess: ['YES'],
addPenalInterestRate: [''], 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) // Credit Bureau Settings (CIBIL)
cibilAccountType: [''], cibilAccountType: [''],
@@ -426,7 +496,60 @@ export class LoanProductSettingsStepComponent implements OnInit {
isFloatingRate: [false], isFloatingRate: [false],
foreclosureChargesApplicable: ['NO'], foreclosureChargesApplicable: ['NO'],
keyFactStatementEnabled: [true], 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() { get loanProductSettings() {
const productSettings = this.loanProductSettingsForm.value; const productSettings = { ...this.loanProductSettingsForm.value };
if (productSettings['delinquencyBucketId'] === '') { if (productSettings['delinquencyBucketId'] === '') {
productSettings['delinquencyBucketId'] = null; 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; return productSettings;
} }
@@ -17,6 +17,69 @@
matTooltip="{{ 'tooltips.Fields are used to define the minimum, default, and maximum principal' | translate }}" matTooltip="{{ 'tooltips.Fields are used to define the minimum, default, and maximum principal' | translate }}"
></i> ></i>
</div> </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-grid">
<div class="lpt-checkbox-row lpt-full-width"> <div class="lpt-checkbox-row lpt-full-width">
<mat-checkbox labelPosition="before" formControlName="allowApprovedDisbursedAmountsOverApplied"> <mat-checkbox labelPosition="before" formControlName="allowApprovedDisbursedAmountsOverApplied">
@@ -264,90 +327,56 @@
} }
</div> </div>
<!-- Interest Rate Slabs Section --> <!-- Interest Rate Detail Section (legacy Page 1 table) -->
<div class="lpt-section"> <div class="lpt-section">
<div class="lpt-section-header"> <div class="lpt-section-header">
<i class="fas fa-table lpt-section-icon"></i> <i class="fas fa-table lpt-section-icon"></i>
<h3>Interest Rate Slabs</h3> <h3>Interest Rate Detail</h3>
</div> </div>
<div class="lpt-slab-c"> <div class="lpt-ird-table-wrap">
<!-- Particular Name --> <table class="lpt-ird-table">
<div class="lpt-slab-c-particular"> <thead>
<label class="lpt-slab-c-particular-label">Particular</label> <tr>
<input <th rowspan="2">Interest Type</th>
type="text" <th rowspan="2">Calculation</th>
class="lpt-slab-c-name-input" <th colspan="2">Normal Category</th>
[formControl]="particularLabel" <th colspan="2">Special Category</th>
placeholder="e.g. Consumer Loan" <th rowspan="2">Period</th>
/> </tr>
</div> <tr>
<th>Mini. Rate</th>
<!-- 2-column slab grid (same as before, General + Special inside each card) --> <th>Maxi. Rate</th>
<div class="lpt-slab-c-grid"> <th>Mini. Rate</th>
@for (genGroup of slabGroups; track $index) { <th>Maxi. Rate</th>
<div class="lpt-slab-c-item"> </tr>
<span class="lpt-slab-c-range">{{ interestSlabDefinitions[$index].label }}</span> </thead>
<div class="lpt-slab-c-rate-pair"> <tbody>
<!-- General --> @for (row of interestRateDetailGroups; track $index) {
<div class="lpt-slab-c-rate-field" [formGroup]="genGroup"> <tr [formGroup]="row">
<span class="lpt-slab-c-rate-label">Gen</span> <td class="lpt-ird-type">{{ interestRateDetailTypes[$index] }}</td>
<div class="lpt-slab-c-input-wrap"> <td>
<input <select formControlName="calculation" class="lpt-ird-select">
type="number" <option [ngValue]="true">YES</option>
class="lpt-slab-c-input" <option [ngValue]="false">NO</option>
formControlName="interestRate" </select>
placeholder="0.00" </td>
min="0" <td><input type="number" formControlName="normalMinRate" class="lpt-ird-input" min="0" max="100" /></td>
max="100" <td><input type="number" formControlName="normalMaxRate" class="lpt-ird-input" min="0" max="100" /></td>
step="0.01" <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>
<span class="lpt-slab-c-suffix">%</span> <td>
</div> <select formControlName="period" class="lpt-ird-select">
</div> <option value=""></option>
<!-- Special --> @for (period of interestRateDetailPeriods; track period) {
<div class="lpt-slab-c-rate-field" [formGroup]="specialSlabGroups[$index]"> <option [value]="period">{{ period }}</option>
<span class="lpt-slab-c-rate-label lpt-slab-c-rate-label--special">Spl</span> }
<div class="lpt-slab-c-input-wrap"> </select>
<input </td>
type="number" </tr>
class="lpt-slab-c-input lpt-slab-c-input--special" }
formControlName="interestRate" </tbody>
placeholder="0.00" </table>
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> </div>
</div> </div>
@@ -199,175 +199,42 @@ $shadow-hover: 0 4px 12px rgb(37 99 235 / 10%);
padding: 8px 4px; padding: 8px 4px;
} }
// ─── Interest Rate Slabs — Option C (2-column grid) ────────────────────────── // ─── Interest Rate Detail table (legacy Page 1 grid) ─────────────────────────
.lpt-slab-c { .lpt-ird-table-wrap {
padding: 16px 24px 20px; padding: 16px 24px 20px;
display: flex; overflow-x: auto;
flex-direction: column;
gap: 0;
} }
// Particular row .lpt-ird-table {
.lpt-slab-c-particular { width: 100%;
display: flex; border-collapse: collapse;
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;
font-size: 12.5px; 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; 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-ird-type {
.lpt-slab-c-grid { text-align: left !important;
display: grid; font-weight: 600;
grid-template-columns: 1fr 1fr; color: $text-primary;
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;
white-space: nowrap; white-space: nowrap;
&--special {
color: #92400e;
background: #fef9c3;
}
} }
.lpt-slab-c-range { .lpt-ird-input {
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 {
width: 70px; width: 70px;
padding: 4px 6px; padding: 4px 6px;
border: 1px solid $border; border: 1px solid $border;
@@ -379,79 +246,26 @@ $shadow-hover: 0 4px 12px rgb(37 99 235 / 10%);
background: $surface-alt; background: $surface-alt;
outline: none; outline: none;
transition: border-color 0.15s; transition: border-color 0.15s;
appearance: textfield;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
appearance: none;
}
&:focus { &:focus {
border-color: $primary; border-color: $primary;
background: $primary-light; background: $primary-light;
} }
&--fixed {
width: 90px;
}
} }
.lpt-slab-c-suffix { .lpt-ird-select {
font-size: 12px; width: 100%;
font-weight: 700; min-width: 90px;
color: $text-secondary; padding: 4px 6px;
user-select: none; border: 1px solid $border;
border-radius: 6px;
&--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;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
color: $primary; color: $text-primary;
user-select: none; background: $surface;
outline: none;
i { &:focus {
font-size: 12px; border-color: $primary;
opacity: 0.7;
} }
} }
.lpt-slab-c-checkbox {
accent-color: $primary;
width: 15px;
height: 15px;
cursor: pointer;
}
@@ -17,6 +17,7 @@ import {
FormControl, FormControl,
ReactiveFormsModule ReactiveFormsModule
} from '@angular/forms'; } from '@angular/forms';
import { patchCustomFields } from '../../loan-product-custom-fields';
import { ProcessingStrategyService } from '../../services/processing-strategy.service'; import { ProcessingStrategyService } from '../../services/processing-strategy.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
@@ -57,24 +58,23 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
repaymentFrequencyTypeData: any; repaymentFrequencyTypeData: any;
repaymentStartDateTypeOptions: any; repaymentStartDateTypeOptions: any;
readonly interestSlabDefinitions = [ // Legacy "Interest Rate Detail" table — 4 interest types x Normal/Special category x Min/Max Rate x Period
{ label: 'Upto 10,000', maxAmount: 10000 }, readonly interestRateDetailTypes = [
{ label: 'Upto 25,000', maxAmount: 25000 }, 'Normal Debit',
{ label: 'Upto 1,00,000', maxAmount: 100000 }, 'Normal Credit',
{ label: 'Upto 2,00,000', maxAmount: 200000 }, 'Against Clearing',
{ label: 'Upto 5,00,000', maxAmount: 500000 }, 'Penalty'
{ 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 }
]; ];
interestRateSlabsArray: FormArray; readonly interestRateDetailPeriods = [
specialSlabsArray: FormArray; 'DAILY',
fixedRateGroup: FormGroup; 'MONTHLY',
particularLabel = new FormControl(''); 'QUARTERLY',
activeSlabTab: 'general' | 'special' = 'general'; 'HALF YEARLY',
'YEARLY'
];
interestRateDetailArray: FormArray;
constructor() { constructor() {
this.createLoanProductTermsForm(); this.createLoanProductTermsForm();
@@ -92,6 +92,12 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
]; ];
this.loanProductTermsForm.patchValue({ 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, isLinkedToFloatingInterestRates: this.loanProductsTemplate.isLinkedToFloatingInterestRates,
minInterestRatePerPeriod: this.loanProductsTemplate.minInterestRatePerPeriod, minInterestRatePerPeriod: this.loanProductsTemplate.minInterestRatePerPeriod,
interestRatePerPeriod: this.loanProductsTemplate.interestRatePerPeriod, 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.zeroInterest.patchValue(
this.loanProductsTemplate.minInterestRatePerPeriod === 0 && this.loanProductsTemplate.minInterestRatePerPeriod === 0 &&
this.loanProductsTemplate.interestRatePerPeriod === 0 && this.loanProductsTemplate.interestRatePerPeriod === 0 &&
@@ -134,36 +145,63 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
} }
createLoanProductTermsForm() { createLoanProductTermsForm() {
this.interestRateSlabsArray = new FormArray( this.interestRateDetailArray = new FormArray(
this.interestSlabDefinitions.map( this.interestRateDetailTypes.map(
() => () =>
new FormGroup({ new FormGroup({
interestRate: new FormControl(null, [ calculation: new FormControl(false),
normalMinRate: new FormControl(null, [
Validators.min(0), Validators.min(0),
Validators.max(100) 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({ 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], isLinkedToFloatingInterestRates: [false],
allowApprovedDisbursedAmountsOverApplied: [false], allowApprovedDisbursedAmountsOverApplied: [false],
overAppliedCalculationType: [{ value: null, disabled: true }], 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) => { this.zeroInterest.valueChanges.subscribe((zeroInterest) => {
if (zeroInterest) { if (zeroInterest) {
this.loanProductTermsForm.get('minInterestRatePerPeriod').patchValue(0); this.loanProductTermsForm.get('minInterestRatePerPeriod').patchValue(0);
@@ -322,16 +351,34 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
}); });
} }
get slabGroups(): FormGroup[] { get interestRateDetailGroups(): FormGroup[] {
return this.interestRateSlabsArray.controls as FormGroup[]; return this.interestRateDetailArray.controls as FormGroup[];
} }
get activeSlabGroups(): FormGroup[] { /**
return this.interestRateSlabsArray.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.
get specialSlabGroups(): FormGroup[] { */
return this.specialSlabsArray.controls as FormGroup[]; 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() { get loanProductTerms() {
@@ -349,15 +396,10 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
minInterestRatePerPeriod: normalizeDecimal(formValue.minInterestRatePerPeriod), minInterestRatePerPeriod: normalizeDecimal(formValue.minInterestRatePerPeriod),
interestRatePerPeriod: normalizeDecimal(formValue.interestRatePerPeriod), interestRatePerPeriod: normalizeDecimal(formValue.interestRatePerPeriod),
maxInterestRatePerPeriod: normalizeDecimal(formValue.maxInterestRatePerPeriod), maxInterestRatePerPeriod: normalizeDecimal(formValue.maxInterestRatePerPeriod),
interestRateSlabs: this.interestRateSlabsArray.getRawValue().map((slab, i) => ({ interestRateDetail: this.interestRateDetailArray.getRawValue().map((row, i) => ({
maxAmount: this.interestSlabDefinitions[i].maxAmount, interestType: this.interestRateDetailTypes[i],
label: this.interestSlabDefinitions[i].label, ...row
interestRate: slab.interestRate }))
})),
fixedRate: (() => {
const fr = this.fixedRateGroup.getRawValue();
return fr.hasFixed ? fr.fixedAmount : null;
})()
}; };
} }
@@ -9,6 +9,10 @@
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { SettingsService } from 'app/settings/settings.service'; 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
@@ -35,6 +39,42 @@ export class LoanProducts {
return loanProductTemplate; 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) { public buildPayload(loanProductData: any, itemsByDefault: any) {
const dateFormat: string = this.settingsService.dateFormat; const dateFormat: string = this.settingsService.dateFormat;
const locale: string = this.settingsService.language.code; const locale: string = this.settingsService.language.code;
@@ -49,6 +89,12 @@ export class LoanProducts {
delete loanProduct.allowAttributeConfiguration; delete loanProduct.allowAttributeConfiguration;
delete loanProduct.advancedAccountingRules; 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 // 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. // allowPartialPeriodInterestCalculation. Until that is fixed, we need to replace the field name in the payload.
loanProduct.allowPartialPeriodInterestCalculation = loanProduct.allowPartialPeriodInterestCalculation; loanProduct.allowPartialPeriodInterestCalculation = loanProduct.allowPartialPeriodInterestCalculation;
+5 -2
View File
@@ -1469,7 +1469,9 @@
"General Information": "General Information", "General Information": "General Information",
"Recurrence Settings": "Recurrence Settings", "Recurrence Settings": "Recurrence Settings",
"To Account": "To Account", "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": { "inputs": {
"# Remittance": "# Remittance", "# Remittance": "# Remittance",
@@ -3085,7 +3087,8 @@
"to": "to", "to": "to",
"valid": "valid", "valid": "valid",
"Valid From": "Valid From", "Valid From": "Valid From",
"Valid Till": "Valid Till" "Valid Till": "Valid Till",
"Field": "Field"
}, },
"links": { "links": {
"Community": "Community", "Community": "Community",