diff --git a/.gitignore b/.gitignore index 1d420630c..368d2d443 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,10 @@ src/environments/.env.ts /playwright-report/ /blob-report/ /playwright/.cache/ + +# JVM crash / replay logs +hs_err_pid*.log +replay_pid*.log + +# Microsoft Office temporary lock files +~$*.xlsx diff --git a/src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.html b/src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.html index 48caed1c6..e34066fed 100644 --- a/src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.html +++ b/src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.html @@ -33,6 +33,9 @@ {{ loanProduct.description }} } +

{{ 'labels.inputs.Currency' | translate }}

@@ -298,6 +301,10 @@ {{ loanProduct.minimumDaysBetweenDisbursalAndFirstRepayment | formatNumber }}
} + +

{{ 'labels.heading.Settings' | translate }}

@if (loanProduct.amortizationType?.value) { @@ -741,6 +748,9 @@ } + @if (loanProduct.charges.length) {

{{ 'labels.heading.Charges' | translate }}

@@ -813,6 +823,9 @@
} + @if (isAdvancedPaymentAllocation) {

{{ 'labels.inputs.Income capitalization' | translate }}

@@ -1271,5 +1284,73 @@
} + } + + + + @for (section of sections; track section.title) { +
+

{{ section.title }}

+ + + + + + + + + + + +
{{ 'labels.inputs.Field' | translate }}{{ row.label }}{{ 'labels.inputs.Value' | translate }} + @switch (row.kind) { + @case ('yesNo') { + {{ row.yesNo | yesNo }} + } + @case ('date') { + {{ row.date | dateFormat }} + } + @case ('number') { + {{ row.number | formatNumber }} + } + @default { + {{ row.text }} + } + } +
+
+ } +
+ + + + @if (interestRateDetailRows.length > 0) { +
+

{{ 'labels.heading.Interest Rate Detail' | translate }}

+ + @for (column of interestRateDetailColumns; track column.key) { + + + + + } + + +
{{ column.label }} + @if (column.key === 'calculation') { + {{ row.calculation | yesNo }} + } @else { + {{ row[column.key] }} + } +
+
+ } +
diff --git a/src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.scss b/src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.scss index 9768995cc..3f394fbe0 100644 --- a/src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.scss +++ b/src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.scss @@ -56,3 +56,13 @@ mat-divider { .repayment-strategy { color: $status-approved; } + +// Legacy "Account Type Creation" sections. Each is a two-column field/value table, and the widest +// of them (Interest Related Parameters) is long, so the label column is pinned to keep the value +// column from drifting between one section and the next. +.custom-field-section { + th:first-of-type, + td:first-of-type { + width: 45%; + } +} diff --git a/src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.ts b/src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.ts index 04a46f611..492b7b2e7 100644 --- a/src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.ts +++ b/src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.ts @@ -26,6 +26,15 @@ import { PaymentAllocation } from '../../loan-product-stepper/loan-product-payment-strategy-step/payment-allocation-model'; import { LoanProducts } from '../../loan-products'; +import { + CUSTOM_FIELD_LABELS, + CUSTOM_FIELD_SECTIONS, + CUSTOM_GL_ACCOUNT_FIELDS, + CustomFieldStep, + CustomGlAccountKind, + INTEREST_RATE_DETAIL_COLUMNS, + isCustomSectionApplicable +} from '../../loan-product-custom-fields'; import { CodeName, OptionData, StringEnumOptionData } from '../../../../shared/models/option-data.model'; import { Accounting } from 'app/core/utils/accounting'; import { DecimalPipe } from '@angular/common'; @@ -51,6 +60,38 @@ import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; import { YesnoPipe } from '../../../../pipes/yesno.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +/** + * One legacy field, ready to render. `kind` picks which pipe the template applies, so formatting + * (translation, dates, numbers) stays in the template rather than being baked into a string here. + */ +export interface CustomFieldRow { + label: string; + kind: 'text' | 'yesNo' | 'date' | 'number'; + text?: string; + yesNo?: boolean; + date?: Date; + number?: number; +} + +export interface CustomFieldSectionView { + title: string; + rows: CustomFieldRow[]; +} + +/** Legacy sections keyed by the tab they belong under, so each renders inside that tab's summary. */ +export type CustomFieldSectionsByStep = Record; + +/** A row of the legacy Interest Rate Detail grid. */ +export interface InterestRateDetailRow { + interestType: string; + calculation: boolean; + normalMinRate: number | null; + normalMaxRate: number | null; + specialMinRate: number | null; + specialMaxRate: number | null; + period: string; +} + @Component({ selector: 'mifosx-loan-product-summary', templateUrl: './loan-product-summary.component.html', @@ -113,6 +154,12 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges { 'chargeOffReasonCodeValueId', 'expenseAccountId' ]; + customFieldDisplayedColumns: string[] = [ + 'label', + 'value' + ]; + readonly interestRateDetailColumns = INTEREST_RATE_DETAIL_COLUMNS; + readonly interestRateDetailDisplayedColumns: string[] = INTEREST_RATE_DETAIL_COLUMNS.map((column) => column.key); accountingRuleData: string[] = []; isAdvancedPaymentAllocation = false; @@ -128,6 +175,15 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges { capitalizedIncomeClassificationToIncomeAccountMappings: ClassificationToIncomeAccountMapping[] = []; writeOffReasonsToExpenseMappings: ChargeOffReasonToExpenseAccountMapping[] = []; + /** Legacy "Account Type Creation" sections, keyed by the tab each belongs under. */ + customFieldSectionsByStep: CustomFieldSectionsByStep = LoanProductSummaryComponent.emptySectionsByStep(); + /** The legacy Interest Rate Detail grid, shown as its own table since its value is a row array. */ + interestRateDetailRows: InterestRateDetailRow[] = []; + + private static emptySectionsByStep(): CustomFieldSectionsByStep { + return { details: [], settings: [], terms: [], charges: [], accounting: [] }; + } + ngOnInit() { this.accountingRuleData = this.accounting.getAccountingRulesForLoans(); this.setCurrentValues(); @@ -518,6 +574,8 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges { this.loanProductsTemplate.advancedPaymentAllocationFutureInstallmentAllocationRules }; } + + this.setCustomFieldValues(); } optionDataLookUp(itemId: any, optionsData: any[]): OptionData { @@ -658,4 +716,119 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges { mapHumanReadableValueStringEnumOptionDataList(incomingParameter: StringEnumOptionData[]): string[] { return incomingParameter.map((v) => v.value); } + + /** + * Builds the legacy "Account Type Creation" tables, which only exist on the create/edit forms + * (see CUSTOM_FIELD_SECTIONS) — reviewed here because `LoanProducts.buildPayload()` drops them + * from the submit payload before it reaches Fineract. + * + * Sections are keyed by their originating tab so each renders inside that tab's own summary. A + * "Cheque Book Settings" table is only reviewable next to the rest of Settings; collected into + * one block at the bottom, it reads as an unrelated pile. + * + * Only genuinely unset values are hidden. A field the officer deliberately set to No or + * false is part of the configuration being reviewed, so it is shown like any other answer. + */ + private setCustomFieldValues(): void { + this.customFieldSectionsByStep = LoanProductSummaryComponent.emptySectionsByStep(); + this.interestRateDetailRows = []; + + if (!this.loanProduct) { + return; + } + + this.interestRateDetailRows = this.buildInterestRateDetailRows((this.loanProduct as any)['interestRateDetail']); + + const productSubGroup: string = (this.loanProduct as any)['productTypeSubGroup']; + + for (const section of CUSTOM_FIELD_SECTIONS) { + // A section the Settings step hides for this sub-group is not part of the product's + // configuration, so listing its defaulted controls here would only invite the reader to + // review settings that do not apply -- a Gold Loan has no cheque book. + if (!isCustomSectionApplicable(section.title, productSubGroup)) { + continue; + } + const rows: CustomFieldRow[] = []; + for (const field of section.fields) { + // Rendered as its own table under the Terms summary. + if (field === 'interestRateDetail') { + continue; + } + const row = this.buildCustomFieldRow(field, (this.loanProduct as any)[field]); + if (row) { + rows.push(row); + } + } + if (rows.length > 0) { + this.customFieldSectionsByStep[section.step].push({ title: section.title, rows }); + } + } + } + + private buildCustomFieldRow(field: string, value: any): CustomFieldRow | null { + if (value === null || value === undefined || value === '') { + return null; + } + + const label = CUSTOM_FIELD_LABELS[field] ?? field; + const glAccountKind = CUSTOM_GL_ACCOUNT_FIELDS[field]; + if (glAccountKind) { + const account = this.glAccountLookUp(value, this.glAccountOptionsFor(glAccountKind)); + return { + label, + kind: 'text', + // An id with no matching account means the option lists have not loaded yet, so show the + // raw id rather than a blank cell. + text: account ? '(' + account.glCode + ') ' + account.name : String(value) + }; + } + + if (typeof value === 'boolean') { + return { label, kind: 'yesNo', yesNo: value }; + } + if (value === 'YES' || value === 'NO') { + return { label, kind: 'yesNo', yesNo: value === 'YES' }; + } + // Only real Date instances go through the date pipe -- a value already formatted by a step's + // getter is left as text so it is not reformatted a second time. + if (value instanceof Date) { + return { label, kind: 'date', date: value }; + } + // Numeric strings such as account or MICR codes stay text, so leading zeros survive. + if (typeof value === 'number') { + return { label, kind: 'number', number: value }; + } + return { label, kind: 'text', text: String(value) }; + } + + /** Drops the untouched rows of the legacy Interest Rate Detail grid. */ + private buildInterestRateDetailRows(value: any): InterestRateDetailRow[] { + if (!Array.isArray(value)) { + return []; + } + return value.filter((row: any) => + this.interestRateDetailColumns.some( + (column) => + column.key !== 'interestType' && + row?.[column.key] !== null && + row?.[column.key] !== undefined && + row?.[column.key] !== '' && + row?.[column.key] !== false + ) + ); + } + + private glAccountOptionsFor(kind: CustomGlAccountKind): GLAccount[] { + const options = this.loanProductsTemplate?.accountingMappingOptions; + switch (kind) { + case 'asset': + return options?.assetAccountOptions || []; + case 'income': + return options?.incomeAccountOptions || []; + case 'expense': + return options?.expenseAccountOptions || []; + case 'liability': + return options?.liabilityAccountOptions || []; + } + } } diff --git a/src/app/products/loan-products/create-loan-product/create-loan-product.component.ts b/src/app/products/loan-products/create-loan-product/create-loan-product.component.ts index 85b3eb1b0..f15b3e83e 100644 --- a/src/app/products/loan-products/create-loan-product/create-loan-product.component.ts +++ b/src/app/products/loan-products/create-loan-product/create-loan-product.component.ts @@ -249,6 +249,53 @@ export class CreateLoanProductComponent implements OnInit { } get loanProductFormValid() { + // TEMP DEBUG — remove once the invalid-step diagnosis is done. + const forms: [string, any][] = [ + [ + 'details', + this.loanProductDetailsForm + ], + [ + 'currency', + this.loanProductCurrencyForm + ], + [ + 'terms', + this.loanProductTermsForm + ], + [ + 'settings', + this.loanProductSettingsForm + ], + [ + 'accounting', + this.loanProductAccountingForm + ] + ]; + if (this.isAdvancedPaymentStrategy) { + forms.push([ + 'incomeCapitalization', + this.loanIncomeCapitalizationForm + ]); + } + const invalid = forms.filter( + ([ + , + form + ]) => !form?.valid + ); + if (invalid.length) { + invalid.forEach( + ([ + name, + form + ]) => { + const invalidControls = Object.keys(form.controls || {}).filter((key) => form.get(key)?.invalid); + console.warn(`[loanProductFormValid] "${name}" form invalid. Invalid controls:`, invalidControls); + } + ); + } + if (this.isAdvancedPaymentStrategy) { return ( this.loanProductDetailsForm.valid && diff --git a/src/app/products/loan-products/edit-loan-product/edit-loan-product.component.ts b/src/app/products/loan-products/edit-loan-product/edit-loan-product.component.ts index b4569cf8e..5c6648b4e 100644 --- a/src/app/products/loan-products/edit-loan-product/edit-loan-product.component.ts +++ b/src/app/products/loan-products/edit-loan-product/edit-loan-product.component.ts @@ -117,7 +117,9 @@ export class EditLoanProductComponent implements OnInit { const loanProducts = this.loanProducts; this.route.data.subscribe((data: { loanProductAndTemplate: any; configurations: any }) => { - this.loanProductAndTemplate = data.loanProductAndTemplate; + // Lift the saved legacy "Account Type Creation" fields to the top level before any step reads + // them, since the backend returns them nested under `cbsExtensions`. + this.loanProductAndTemplate = loanProducts.flattenCbsExtensions(data.loanProductAndTemplate); const assetAccountData = this.loanProductAndTemplate.accountingMappingOptions.assetAccountOptions || []; const liabilityAccountData = this.loanProductAndTemplate.accountingMappingOptions.liabilityAccountOptions || []; this.loanProductAndTemplate.accountingMappingOptions.assetAndLiabilityAccountOptions = diff --git a/src/app/products/loan-products/loan-product-custom-fields.ts b/src/app/products/loan-products/loan-product-custom-fields.ts new file mode 100644 index 000000000..4e273da5d --- /dev/null +++ b/src/app/products/loan-products/loan-product-custom-fields.ts @@ -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 = { + 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 = { + 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); +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.ts index 95fb2c7e6..33e5b7539 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.ts +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.ts @@ -8,6 +8,7 @@ import { Component, Input, OnChanges, OnInit, SimpleChanges, inject } from '@angular/core'; import { UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; +import { patchCustomFields } from '../../loan-product-custom-fields'; import { MatDialog } from '@angular/material/dialog'; import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; @@ -241,6 +242,10 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges { this.setOtherGlMappings('incomeOtherGlMappings', glMappings.income || [], this.incomeAccountData); } } + + // Repopulate the legacy "Account Type Creation" fields saved against this product. + // Last, so a value actually stored wins over any default set above. + patchCustomFields(this.loanProductAccountingForm, this.loanProductsTemplate); } public createLoanProductAccountingForm() { @@ -283,9 +288,6 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges { 'incomeFromGoodwillCreditInterestAccountId', 'incomeFromGoodwillCreditFeesAccountId', 'incomeFromGoodwillCreditPenaltyAccountId', - 'receivableInterestAccountId', - 'receivableFeeAccountId', - 'receivablePenaltyAccountId', 'intReceivablesGlAccountId', 'intPayableGlAccountId', 'glAccountId', @@ -297,6 +299,24 @@ 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 = [ @@ -644,7 +664,7 @@ export class LoanProductAccountingStepComponent implements OnInit, OnChanges { } public isAccrualAccounting(): boolean { - const accountingRule = this.loanProductAccountingForm.value.accountingRule; + const accountingRule = this.loanProductAccountingForm.get('accountingRule').value; return accountingRule === 3 || accountingRule === 4; } diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.ts index 09e230b43..e14b0e0da 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.ts +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.ts @@ -8,6 +8,7 @@ import { Component, Input, OnInit, inject } from '@angular/core'; import { UntypedFormControl, UntypedFormBuilder, UntypedFormGroup, ReactiveFormsModule } from '@angular/forms'; +import { patchCustomFields } from '../../loan-product-custom-fields'; import { MatDialog } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; @@ -135,6 +136,10 @@ export class LoanProductChargesStepComponent implements OnInit { this.currencyCode.valueChanges.subscribe(() => (this.chargesDataSource = [])); this.multiDisburseLoan.valueChanges.subscribe(() => (this.chargesDataSource = [])); + + // Repopulate the legacy "Account Type Creation" fields saved against this product. + // Last, so a value actually stored wins over any default set above. + patchCustomFields(this.legacyChargesForm, this.loanProductsTemplate); } addCharge(charge: any) { diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/cbsnexora.code-workspace b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/cbsnexora.code-workspace new file mode 100644 index 000000000..8cf8ae56c --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/cbsnexora.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "../../../../../.." + }, + { + "path": "../../../../../../../cbsnexora-backend" + } + ], + "settings": {} +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts index be0a33192..23d5ac3ef 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts @@ -9,6 +9,7 @@ /** Angular Imports */ import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { CUSTOM_DATE_FIELDS, patchCustomFields } from '../../loan-product-custom-fields'; import { Dates } from 'app/core/utils/dates'; /** Custom Services */ @@ -89,6 +90,10 @@ export class LoanProductDetailsStepComponent implements OnInit { 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 */ @@ -178,9 +183,12 @@ export class LoanProductDetailsStepComponent implements OnInit { } get loanProductDetails() { - const loanProductDetailsFormData = this.loanProductDetailsForm.value; - const prevStartDate: Date = this.loanProductDetailsForm.value.startDate; - const prevCloseDate: Date = this.loanProductDetailsForm.value.closeDate; + // getRawValue() rather than value: it returns a fresh object, so the date reformatting below + // cannot write back into the form's own cached value, and any control disabled later still + // reaches the preview step instead of silently vanishing from it. + const loanProductDetailsFormData = this.loanProductDetailsForm.getRawValue(); + const prevStartDate: Date = loanProductDetailsFormData.startDate; + const prevCloseDate: Date = loanProductDetailsFormData.closeDate; const dateFormat = this.settingsService.dateFormat; if (loanProductDetailsFormData.startDate instanceof Date) { loanProductDetailsFormData.startDate = this.dateUtils.formatDate(prevStartDate, dateFormat) || ''; @@ -188,6 +196,17 @@ export class LoanProductDetailsStepComponent implements OnInit { if (loanProductDetailsFormData.closeDate instanceof Date) { loanProductDetailsFormData.closeDate = this.dateUtils.formatDate(prevCloseDate, dateFormat) || ''; } + + // The legacy date fields need the same treatment: their control holds a Date, but the API parses + // them against the dateFormat sent alongside, and rejects the raw value outright. Driven off + // CUSTOM_DATE_FIELDS rather than named here, so a legacy date added later is covered too. + for (const field of CUSTOM_DATE_FIELDS) { + const value = loanProductDetailsFormData[field]; + if (value instanceof Date) { + loanProductDetailsFormData[field] = this.dateUtils.formatDate(value, dateFormat) || ''; + } + } + return loanProductDetailsFormData; } } diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.ts index da9fee4c7..079b6f0e8 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.ts +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.ts @@ -15,6 +15,13 @@ import { ReactiveFormsModule } from '@angular/forms'; import { LoanProducts } from '../../loan-products'; +import { + CHANNEL_ENABLED_SUB_GROUPS, + CUSTOM_UI_ONLY_FIELDS, + NO_CHEQUE_BOOK_SUB_GROUPS, + TERM_LOAN_SUB_GROUPS, + patchCustomFields +} from '../../loan-product-custom-fields'; import { GlobalConfiguration } from 'app/system/configurations/global-configurations-tab/configuration.model'; import { CodeName, OptionData, StringEnumOptionData } from 'app/shared/models/option-data.model'; import { ProcessingStrategyService } from '../../services/processing-strategy.service'; @@ -58,36 +65,22 @@ export class LoanProductSettingsStepComponent implements OnInit { @Input() productSubGroup: string; @Output() advancePaymentStrategy = new EventEmitter(); - /** Sub-groups (from Details step) that are current-account-linked, channel-heavy products */ - readonly channelEnabledSubGroups = [ - 'cc', - 'overdraft' - ]; - - /** Sub-groups that are fixed-tenure, EMI-based term loans (foreclosure applies to these) */ - readonly termLoanSubGroups = [ - 'term', - 'personal', - 'housing', - 'vehicle', - 'education', - 'business', - 'gold_emi' - ]; + // 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 || this.channelEnabledSubGroups.includes(this.productSubGroup); + return !this.productSubGroup || CHANNEL_ENABLED_SUB_GROUPS.includes(this.productSubGroup); } get showChequeBookSettings(): boolean { - return !this.productSubGroup || ![ - 'gold', - 'gold_emi' - ].includes(this.productSubGroup); + return !this.productSubGroup || !NO_CHEQUE_BOOK_SUB_GROUPS.includes(this.productSubGroup); } get showForeclosureSettings(): boolean { - return !this.productSubGroup || this.termLoanSubGroups.includes(this.productSubGroup); + return !this.productSubGroup || TERM_LOAN_SUB_GROUPS.includes(this.productSubGroup); } loanProductSettingsForm: UntypedFormGroup; @@ -290,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() { @@ -955,10 +952,20 @@ export class LoanProductSettingsStepComponent implements OnInit { } get loanProductSettings() { - const productSettings = this.loanProductSettingsForm.value; + const productSettings = { ...this.loanProductSettingsForm.value }; if (productSettings['delinquencyBucketId'] === '') { productSettings['delinquencyBucketId'] = null; } + // `value` omits disabled controls, which would leave a legacy field blank on the preview step + // even though the officer filled it in. Only the custom fields are added back, never the + // standard Fineract ones -- those are disabled deliberately and must stay out of the payload. + // Carrying the custom ones is free: buildPayload() strips every one of them before submitting. + for (const field of CUSTOM_UI_ONLY_FIELDS) { + const control = this.loanProductSettingsForm.get(field); + if (control?.disabled) { + productSettings[field] = control.value; + } + } return productSettings; } diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html index 7b1940a68..12fd62265 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html @@ -17,6 +17,69 @@ matTooltip="{{ 'tooltips.Fields are used to define the minimum, default, and maximum principal' | translate }}" > +
+
+ + {{ 'labels.inputs.Minimum' | translate }} + + + {{ 'labels.commons.Minimum Value must be' | translate }} 0 + + + + {{ 'labels.inputs.Default' | translate }} + + + {{ 'labels.inputs.Principal' | translate }} + {{ 'labels.commons.is required' | translate }} + + + {{ 'labels.commons.Minimum Value must be' | translate }} 0 + + + + {{ 'labels.inputs.Maximum' | translate }} + + + {{ 'labels.commons.Minimum Value must be' | translate }} 0 + + +
+
+ +
+

+ {{ 'labels.inputs.Number of Repayments' | translate }} +

+
+ + {{ 'labels.inputs.Minimum' | translate }} + + + {{ 'labels.commons.Minimum Value must be' | translate }} 1 + + + + {{ 'labels.inputs.Default' | translate }} + + + {{ 'labels.inputs.Number of Repayments' | translate }} + {{ 'labels.commons.is required' | translate }} + + + {{ 'labels.commons.Minimum Value must be' | translate }} 1 + + + + {{ 'labels.inputs.Maximum' | translate }} + + + {{ 'labels.commons.Minimum Value must be' | translate }} 1 + + +
+
+
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",