diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.ts
index baf888790..6b9a5f6a7 100644
--- a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.ts
+++ b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.ts
@@ -17,6 +17,7 @@ import {
FormControl,
ReactiveFormsModule
} from '@angular/forms';
+import { patchCustomFields } from '../../loan-product-custom-fields';
import { ProcessingStrategyService } from '../../services/processing-strategy.service';
import { TranslateService } from '@ngx-translate/core';
import { MatTooltip } from '@angular/material/tooltip';
@@ -91,6 +92,12 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
];
this.loanProductTermsForm.patchValue({
+ minPrincipal: this.loanProductsTemplate.minPrincipal,
+ principal: this.loanProductsTemplate.principal,
+ maxPrincipal: this.loanProductsTemplate.maxPrincipal,
+ minNumberOfRepayments: this.loanProductsTemplate.minNumberOfRepayments,
+ numberOfRepayments: this.loanProductsTemplate.numberOfRepayments,
+ maxNumberOfRepayments: this.loanProductsTemplate.maxNumberOfRepayments,
isLinkedToFloatingInterestRates: this.loanProductsTemplate.isLinkedToFloatingInterestRates,
minInterestRatePerPeriod: this.loanProductsTemplate.minInterestRatePerPeriod,
interestRatePerPeriod: this.loanProductsTemplate.interestRatePerPeriod,
@@ -120,6 +127,11 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
});
}
+ // Repopulate the legacy "Account Type Creation" fields saved against this product.
+ // Last, so a value actually stored wins over any default set above.
+ patchCustomFields(this.loanProductTermsForm, this.loanProductsTemplate);
+ this.patchInterestRateDetail();
+
this.zeroInterest.patchValue(
this.loanProductsTemplate.minInterestRatePerPeriod === 0 &&
this.loanProductsTemplate.interestRatePerPeriod === 0 &&
@@ -160,6 +172,36 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
);
this.loanProductTermsForm = this.formBuilder.group({
+ minPrincipal: [
+ '',
+ [Validators.min(0)]
+ ],
+ principal: [
+ '',
+ [
+ Validators.required,
+ Validators.min(0)
+ ]
+ ],
+ maxPrincipal: [
+ '',
+ [Validators.min(0)]
+ ],
+ minNumberOfRepayments: [
+ '',
+ [Validators.min(1)]
+ ],
+ numberOfRepayments: [
+ '',
+ [
+ Validators.required,
+ Validators.min(1)
+ ]
+ ],
+ maxNumberOfRepayments: [
+ '',
+ [Validators.min(1)]
+ ],
isLinkedToFloatingInterestRates: [false],
allowApprovedDisbursedAmountsOverApplied: [false],
overAppliedCalculationType: [{ value: null, disabled: true }],
@@ -313,6 +355,32 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
return this.interestRateDetailArray.controls as FormGroup[];
}
+ /**
+ * Fills the legacy Interest Rate Detail grid from a saved product. Rows are matched on
+ * `interestType` rather than by position, so a payload that comes back in a different order cannot
+ * put one interest type's rates onto another. A type with no saved row is left at its defaults.
+ */
+ private patchInterestRateDetail(): void {
+ const saved: any[] = this.loanProductsTemplate?.interestRateDetail;
+ if (!Array.isArray(saved) || saved.length === 0) {
+ return;
+ }
+ this.interestRateDetailTypes.forEach((interestType, index) => {
+ const row = saved.find((r) => r?.interestType === interestType);
+ if (!row) {
+ return;
+ }
+ this.interestRateDetailArray.at(index).patchValue({
+ calculation: row.calculation ?? false,
+ normalMinRate: row.normalMinRate,
+ normalMaxRate: row.normalMaxRate,
+ specialMinRate: row.specialMinRate,
+ specialMaxRate: row.specialMaxRate,
+ period: row.period ?? ''
+ });
+ });
+ }
+
get loanProductTerms() {
const formValue = this.loanProductTermsForm.getRawValue();
// Normalize decimal separators: convert comma to dot for backend compatibility
diff --git a/src/app/products/loan-products/loan-products.ts b/src/app/products/loan-products/loan-products.ts
index 2d31b2b24..efd75381d 100644
--- a/src/app/products/loan-products/loan-products.ts
+++ b/src/app/products/loan-products/loan-products.ts
@@ -9,245 +9,9 @@
import { Injectable, inject } from '@angular/core';
import { SettingsService } from 'app/settings/settings.service';
-// Custom, UI-only fields grafted onto the loan product form to mirror the legacy
-// "Account Type Creation" screen. Fineract's LoanProductDataValidator rejects any
-// unrecognised parameter, so these are stripped from the payload until a backend
-// mechanism (e.g. a linked Data Table) exists to persist them.
-const CUSTOM_UI_ONLY_FIELDS: string[] = [
- // Details step — classification & legacy header 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',
- // Settings step — Cheque Book Settings
- 'chequeBookIssueEnabled',
- 'chequeValidationRequired',
- 'isFirstChequeBookFree',
- 'chequePrintingAccountType',
- 'allowWithdrawalSlipUsage',
- 'maintainChequeBookStockRegistry',
- 'freeChequeBooksPerYear',
- 'allowCreditByChequeNumber',
- 'creditChequeValidationRequired',
- 'graceDaysForStockStatement',
- // Settings step — Additional Product Settings
- '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',
- // Settings step — Interest Related Parameters
- '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',
- // Settings step — Credit Bureau Settings (CIBIL)
- 'cibilAccountType',
- 'cibilCommercialAccountType',
- 'highmarkAccountType',
- 'highmarkCommercialAccountType',
- 'experianAccountType',
- 'experianCommercialAccountType',
- 'equifaxAccountType',
- 'equifaxCommercialAccountType',
- // Settings step — RBI Compliance & Product Category
- 'loanProductCategory',
- 'penalChargeMethod',
- 'isFloatingRate',
- 'foreclosureChargesApplicable',
- 'keyFactStatementEnabled',
- 'annualPercentageRateCalculationEnabled',
- // Settings step — Channel & Transaction Limits
- 'isMobileAppEnabled',
- 'isAtmEnabled',
- 'atmYearlyChargeEnabled',
- 'atmMinimumBalance',
- 'emailActiveEnabled',
- 'isImpsEnabled',
- 'isUpiEnabled',
- 'isPosEnabled',
- 'isBbpsEnabled',
- 'interBankLimitPerDay',
- 'interBankLimitPerTransaction',
- 'neftRtgsLimitPerDay',
- 'neftRtgsLimitPerTransaction',
- 'neftCutoffLimit',
- 'rtgsCutoffLimit',
- 'neftRtgsMinimumBalance',
- 'isChargeRequiredNeftRtgsCbs',
- 'isChargeRequiredNeftRtgsMobile',
- 'impsLimitPerDay',
- 'impsLimitPerTransaction',
- 'impsLimitPerMonth',
- 'impsMinLimit',
- 'impsMonthlyTransactionCount',
- // Settings step — POS Facility & ATM CSV Settings
- 'maxPerTransactionLimitAtm',
- 'maxDayTransactionLimitAtm',
- 'maxPerTransactionLimitPos',
- 'maxDayTransactionLimitPos',
- 'maxPerTransactionLimitEcom',
- 'maxDayTransactionLimitEcom',
- 'maxPerTransactionLimitUpi',
- 'maxDayTransactionLimitUpi',
- 'atmCsvProductCode',
- 'atmCsvTypeCode',
- // Settings step — Auto Interest Post Parameters
- 'autoInterestPostType',
- 'autoInterestPostWhen',
- 'autoInterestPostFixDay',
- 'autoLastInterestDate',
- // Settings step — Foreclosure / Early Account Closure
- 'calculateEarlyAccountCloseCharge',
- 'earlyAccountCloseCharge',
- 'earlyAccountClosePeriodInMonths',
- // Terms step
- 'debitBalanceAllow',
- 'allowDebitTransAfterDueDate',
- 'branchLevelAccountType',
- 'interestRateDetail',
- // Charges step — legacy cheque / standing instruction / minimum balance charges
- 'chequeBookChargeEnabled',
- 'chequeReturnChargeEnabled',
- 'icChargeDebitEnabled',
- 'cashBundleChargeEnabled',
- 'cashBundleChargeWithPanLimit',
- 'perChequeEntryCharge',
- 'perChequeEntryServiceCharge',
- 'perInwardChequeEntryCharge',
- 'perOutwardChequeEntryCharge',
- 'creditorStandingInstructionFailCharge',
- 'debtorStandingInstructionFailCharge',
- 'minimumBalanceCheckingType',
- 'autoMinimumBalanceCharge',
- 'calculateDailyMinimumBalanceCharges',
- 'minimumBalanceChargeEnabled',
- 'minimumBalanceChargeAmount',
- 'minimumBalanceChargeForChequeIssue',
- 'minimumBalanceChargeMethod',
- 'monthsAfterMinBalanceChargeCalculate',
- 'forChequeIssue',
- 'forChequeIssueAvg',
- 'forWithdrawalSlip',
- 'forWithdrawalSlipAvg',
- 'calculateDailyAgainstClearingCharge',
- // Accounting step — custom GL account mappings
- 'intReceivablesGlAccountId',
- 'intPayableGlAccountId',
- 'deafIntPayableGlAccountId',
- 'intOverdueReserveAccountId',
- 'undebitedInterestAccountId',
- 'penalIntCrGlAccountId',
- 'rdMaturedGlAccountId',
- 'glAccountId',
- 'icChargeGlAccountId',
- 'inoperativeGlAccountId',
- 'creditBalanceLiabilityGlAccountId',
- 'creditBalanceAssetGlAccountId',
- 'overdueAccountsGlAccountId',
- 'rebateGlAccountId'
-];
+import { CUSTOM_DATE_FIELDS, CUSTOM_YES_NO_STRING_FIELDS } from './loan-product-custom-fields';
+
+export { CUSTOM_UI_ONLY_FIELDS } from './loan-product-custom-fields';
@Injectable({
providedIn: 'root'
@@ -275,6 +39,42 @@ export class LoanProducts {
return loanProductTemplate;
}
+ /**
+ * The backend returns the legacy "Account Type Creation" fields nested under `cbsExtensions`, while
+ * every step form reads and writes them as flat properties on the product. Lifting them here keeps
+ * that difference in one place rather than teaching each step about the nesting.
+ *
+ * Two shapes also have to be converted back to what the form controls expect:
+ *
+ * - the yes/no dropdowns hold the strings 'YES'/'NO', but the column is BOOLEAN, so the API returns
+ * true/false -- patched as-is the control matches no option and shows blank;
+ * - a date arrives as Fineract's [year, month, day] array rather than a Date.
+ *
+ * Mutates and returns the product so it can be dropped into an existing resolver chain.
+ */
+ public flattenCbsExtensions(loanProduct: any): any {
+ if (!loanProduct?.cbsExtensions) {
+ return loanProduct;
+ }
+ Object.assign(loanProduct, loanProduct.cbsExtensions);
+
+ for (const field of CUSTOM_YES_NO_STRING_FIELDS) {
+ const value = loanProduct[field];
+ if (typeof value === 'boolean') {
+ loanProduct[field] = value ? 'YES' : 'NO';
+ }
+ }
+
+ for (const field of CUSTOM_DATE_FIELDS) {
+ const value = loanProduct[field];
+ if (Array.isArray(value) && value.length >= 3) {
+ loanProduct[field] = new Date(value[0], value[1] - 1, value[2]);
+ }
+ }
+
+ return loanProduct;
+ }
+
public buildPayload(loanProductData: any, itemsByDefault: any) {
const dateFormat: string = this.settingsService.dateFormat;
const locale: string = this.settingsService.language.code;
@@ -289,8 +89,11 @@ export class LoanProducts {
delete loanProduct.allowAttributeConfiguration;
delete loanProduct.advancedAccountingRules;
- // Remove custom, UI-only fields not yet recognised by the Fineract backend
- CUSTOM_UI_ONLY_FIELDS.forEach((field) => delete loanProduct[field]);
+ // The legacy "Account Type Creation" fields used to be stripped here, because Fineract's loan
+ // product validator rejected any parameter it did not recognise. They are now persisted by
+ // LoanProductCbsExtService in a satellite table keyed on the product, so they are sent as-is:
+ //
+ // CUSTOM_UI_ONLY_FIELDS.forEach((field) => delete loanProduct[field]);
// In Fineract, the POST and PUT endpoints for /v1/loanproducts have a typo in the field
// allowPartialPeriodInterestCalculation. Until that is fixed, we need to replace the field name in the payload.
diff --git a/src/assets/translations/en-US.json b/src/assets/translations/en-US.json
index 41e5d752b..55c97e4a1 100644
--- a/src/assets/translations/en-US.json
+++ b/src/assets/translations/en-US.json
@@ -1469,7 +1469,9 @@
"General Information": "General Information",
"Recurrence Settings": "Recurrence Settings",
"To Account": "To Account",
- "Transfer Settings": "Transfer Settings"
+ "Transfer Settings": "Transfer Settings",
+ "Additional Settings (Legacy Account Type)": "Additional Settings (Legacy Account Type)",
+ "Interest Rate Detail": "Interest Rate Detail"
},
"inputs": {
"# Remittance": "# Remittance",
@@ -3085,7 +3087,8 @@
"to": "to",
"valid": "valid",
"Valid From": "Valid From",
- "Valid Till": "Valid Till"
+ "Valid Till": "Valid Till",
+ "Field": "Field"
},
"links": {
"Community": "Community",