+
+
+
+
+
+
+
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-penal-npa-step/loan-product-penal-npa-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-penal-npa-step/loan-product-penal-npa-step.component.ts
index 0df4451de..c3da01979 100644
--- a/src/app/products/loan-products/loan-product-stepper/loan-product-penal-npa-step/loan-product-penal-npa-step.component.ts
+++ b/src/app/products/loan-products/loan-product-stepper/loan-product-penal-npa-step/loan-product-penal-npa-step.component.ts
@@ -6,14 +6,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import { Component, Input, inject } from '@angular/core';
-import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
+import { Component, Input, OnInit, inject } from '@angular/core';
+import { UntypedFormGroup, UntypedFormBuilder, UntypedFormControl, Validators } from '@angular/forms';
import { MatCheckbox } from '@angular/material/checkbox';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
- * Penal charges & asset-classification (SMA / NPA) step for an Advances A/C product (Figma step 6).
- * UI-only for now — not sent in the create payload.
+ * Penal charges & asset-classification (SMA / NPA / SICR / LGD) step for a loan product (Figma step 6).
+ * Shared by both Advances A/C and Loans A/C flows — fields are sent in the create/update payload via
+ * `loanProductPenalNpa`, mapped onto the core product fields and the LoanProductAdvanceExt satellite params
+ * in create-loan-product.component.ts.
*/
@Component({
selector: 'mifosx-loan-product-penal-npa-step',
@@ -24,13 +26,30 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatCheckbox
]
})
-export class LoanProductPenalNpaStepComponent {
+export class LoanProductPenalNpaStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
@Input() loanProductsTemplate: any;
+ @Input() productTypeGroup: UntypedFormControl;
+ @Input() productTypeSubGroup: UntypedFormControl;
loanProductPenalNpaForm: UntypedFormGroup;
+ /** True for Advances A/C (cc/overdraft/kcc style limit products); drives which SICR/LGD fields show. */
+ isAdvanceAccount = false;
+ /** The ECL card is optional and starts collapsed. */
+ eclOpen = false;
+
+ /** One line for the collapsed ECL card. */
+ get eclSummary(): string {
+ const v = this.loanProductPenalNpaForm.getRawValue();
+ const days = this.isRevolvingSubType ? v.sicrRevolvingDaysThreshold : v.sicrDaysThreshold;
+ return `Stage 2 after ${days} days late · loss ${v.lgdSecuredPct}% with security, ${v.lgdUnsecuredPct}% without`;
+ }
+
+ /** The chosen sub type (e.g. 'cc', 'overdraft', 'kcc', 'gold') within the current product type group. */
+ subType: string | null = null;
+
readonly yesNoOptions = [
'YES',
'NO'
@@ -43,7 +62,8 @@ export class LoanProductPenalNpaStepComponent {
readonly npaTriggerOptions = [
{ id: 'maturity', label: 'Dues overdue after maturity' },
- { id: 'out_of_order', label: 'Out of order 90 days (CC/OD)' }
+ { id: 'out_of_order', label: 'Out of order 90 days (CC/OD)' },
+ { id: 'crop_season', label: 'Overdue for crop season(s) (KCC)' }
];
readonly interestAfterNpaOptions = [
@@ -51,10 +71,47 @@ export class LoanProductPenalNpaStepComponent {
{ id: 'suspense', label: 'Move to Interest Suspense GL' }
];
+ readonly sicrAssessmentBasisOptions = [
+ { id: 'DPD', label: 'Days-Past-Due threshold only' },
+ { id: 'RATING_DOWNGRADE', label: 'Internal rating downgrade only' },
+ { id: 'PRICING_INCREASE', label: 'Risk-based repricing only' },
+ { id: 'DPD_AND_RATING', label: 'DPD + rating downgrade (either triggers)' }
+ ];
+
+ /** RBI 2026: CC/OD "out of order" basis (para on revolving facilities). */
+ readonly npaOutOfOrderBasisOptions = [
+ { id: 'LIMIT_EXCEEDED', label: 'Outstanding continuously exceeds limit/drawing power' },
+ { id: 'NO_CREDITS_90_DAYS', label: 'No credits for 90 days, or credits < interest debited' },
+ { id: 'DP_NOT_RENEWED', label: 'Drawing power not reassessed/renewed within 180 days' }
+ ];
+
+ readonly cropDurationOptions = [
+ { id: 'KHARIF', label: 'Kharif (short duration)' },
+ { id: 'RABI', label: 'Rabi (short duration)' },
+ { id: 'LONG_DURATION', label: 'Long duration crop' }
+ ];
+
+ readonly eligibleCollateralTypeOptions = [
+ { id: 'NONE', label: 'None' },
+ { id: 'GOLD', label: 'Gold / gold jewellery' },
+ { id: 'GOVERNMENT_SECURITIES', label: 'Government securities' },
+ { id: 'BANK_DEPOSIT', label: 'Bank term deposit (lien-marked)' },
+ { id: 'LIFE_INSURANCE', label: 'Life insurance policy (surrender value)' }
+ ];
+
get delinquencyBucketOptions(): any[] {
return this.loanProductsTemplate?.delinquencyBucketOptions ?? [];
}
+ /** CC/OD are revolving facilities — RBI's "out of order" NPA basis and 60-day SICR window apply to them. */
+ get isRevolvingSubType(): boolean {
+ return this.subType === 'cc' || this.subType === 'overdraft';
+ }
+
+ get isKccSubType(): boolean {
+ return this.subType === 'kcc';
+ }
+
constructor() {
this.loanProductPenalNpaForm = this.formBuilder.group({
penalApplicable: ['YES'],
@@ -78,13 +135,124 @@ export class LoanProductPenalNpaStepComponent {
npaTrigger: ['maturity'],
interestAfterNpa: ['undebited'],
graceOnArrearsAgeing: [0],
+ smaStartAfterMonths: [
+ 1,
+ [
+ Validators.required,
+ Validators.min(0)
+ ]
+ ],
+ stockStatementValidityDays: [
+ 90,
+ [Validators.min(0)]
+ ],
inArrearsTolerance: [0],
upgradeOnlyAllArrears: [true],
- keepStandardIfDepositCovers: [true]
+ keepStandardIfDepositCovers: [true],
+
+ // ---- SICR (Significant Increase in Credit Risk) — RBI 2026 ECL framework ----
+ sicrDaysThreshold: [
+ 30,
+ [Validators.min(0)]
+ ],
+ sicrRevolvingDaysThreshold: [
+ 60,
+ [Validators.min(0)]
+ ],
+ sicrRatingDowngradeNotches: [
+ 2,
+ [Validators.min(0)]
+ ],
+ sicrPricingIncreaseTriggerPct: [
+ 1.5,
+ [
+ Validators.min(0),
+ Validators.max(100)
+ ]
+ ],
+ sicrAssessmentBasis: ['DPD'],
+ sicrRebuttalAllowed: [false],
+ npaOutOfOrderBasis: ['LIMIT_EXCEEDED'],
+ cropDuration: [''],
+
+ // ---- LGD (Loss Given Default) — regulatory backstop vs bank's own model ----
+ useOwnLgdModel: [false],
+ lgdSecuredPct: [
+ 65,
+ [
+ Validators.min(0),
+ Validators.max(100)
+ ]
+ ],
+ lgdUnsecuredPct: [
+ 70,
+ [
+ Validators.min(0),
+ Validators.max(100)
+ ]
+ ],
+ eligibleCollateralType: ['NONE']
});
}
+ ngOnInit() {
+ if (this.productTypeGroup) {
+ this.applyProductTypeRules(this.productTypeGroup.value === 'advances', this.productTypeSubGroup?.value ?? null);
+ this.productTypeGroup.valueChanges.subscribe((group: string) => {
+ this.applyProductTypeRules(group === 'advances', this.productTypeSubGroup?.value ?? null);
+ });
+ }
+ if (this.productTypeSubGroup) {
+ this.productTypeSubGroup.valueChanges.subscribe((subGroup: string) => {
+ this.applyProductTypeRules(this.productTypeGroup?.value === 'advances', subGroup);
+ });
+ }
+ }
+
+ /**
+ * CC/OD (revolving) default to the 60-day SICR window and show the "out of order" NPA basis; KCC shows
+ * the crop-duration field instead. Term/EMI loans use the plain 30-day DPD-based SICR window and hide
+ * both revolving-only fields. Only defaults the value the first time a sub type becomes relevant, so it
+ * doesn't clobber a value the user already edited.
+ */
+ private applyProductTypeRules(isAdvance: boolean, subType: string | null): void {
+ this.isAdvanceAccount = isAdvance;
+ this.subType = isAdvance ? subType : null;
+
+ if (this.isRevolvingSubType && !this.loanProductPenalNpaForm.get('sicrAssessmentBasis')?.dirty) {
+ this.loanProductPenalNpaForm.patchValue({ sicrAssessmentBasis: 'DPD_AND_RATING' });
+ }
+
+ // The security that backs the product decides which collateral counts for ECL.
+ const collateralBySubType: Record
= {
+ fd: 'BANK_DEPOSIT',
+ rd: 'BANK_DEPOSIT',
+ dds: 'BANK_DEPOSIT',
+ lic: 'LIFE_INSURANCE',
+ gold: 'GOLD',
+ gold_emi: 'GOLD'
+ };
+ const collateral = subType ? collateralBySubType[subType] : undefined;
+ if (!this.loanProductPenalNpaForm.get('eligibleCollateralType')?.dirty) {
+ this.loanProductPenalNpaForm.patchValue({ eligibleCollateralType: collateral ?? 'NONE' });
+ }
+
+ // SMA counting starts 9 months after disbursement for Advances, 1 month for Loans (EMI).
+ if (!this.loanProductPenalNpaForm.get('smaStartAfterMonths')?.dirty) {
+ this.loanProductPenalNpaForm.patchValue({ smaStartAfterMonths: isAdvance ? 9 : 1 });
+ }
+ // NPA trigger follows the facility: CC/OD out-of-order, KCC crop season, everything else overdue days.
+ if (!this.loanProductPenalNpaForm.get('npaTrigger')?.dirty) {
+ const trigger = this.isRevolvingSubType ? 'out_of_order' : this.isKccSubType ? 'crop_season' : 'maturity';
+ this.loanProductPenalNpaForm.patchValue({ npaTrigger: trigger });
+ }
+ }
+
get loanProductPenalNpa() {
- return this.loanProductPenalNpaForm.getRawValue();
+ const value = this.loanProductPenalNpaForm.getRawValue();
+ // Advances have no instalments, so the 9-month start is carried as arrears-ageing grace (30-day months).
+ // Loans (EMI) need none: the first instalment falls due one month after disbursement already.
+ const months = Number(value.smaStartAfterMonths) || 0;
+ return { ...value, graceOnArrearsAgeing: this.isAdvanceAccount ? months * 30 : 0 };
}
}
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.html
index d86fe2322..e2b67b3eb 100644
--- a/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.html
+++ b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.html
@@ -6,12 +6,36 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
-
-
+
+
+ Check the details below. Use Submit at the bottom to send the product for approval.
+
+
+ @for (section of sections; track section.title) {
+
+
{{ section.title }}
+
+ @for (item of section.rows; track item.label) {
+
+ {{ item.label }}
+ {{ item.value }}
+
+ }
+
+
+ }
+
+ @if (otherSettings.length > 0) {
+
+ Other settings ({{ otherSettings.length }})
+
+ @for (item of otherSettings; track item.label) {
+
+ {{ item.label }}
+ {{ item.value }}
+
+ }
+
+
+ }
+
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.scss
index 679255b76..ae57937a1 100644
--- a/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.scss
+++ b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.scss
@@ -47,3 +47,74 @@ mat-divider {
.repayment-strategy {
color: $status-approved;
}
+
+/* Review step: sections of label / value rows. */
+.rv {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+
+ &__intro {
+ margin: 0;
+ color: #475569;
+ font-size: 13px;
+ }
+
+ &__card {
+ border: 1px solid #e2e8f0;
+ border-radius: 10px;
+ background: #fff;
+ overflow: hidden;
+ }
+
+ &__head {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 16px;
+ background: #f8fafc;
+ color: #1e293b;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: default;
+
+ i {
+ color: #6366f1;
+ }
+ }
+
+ &__other > &__head {
+ cursor: pointer;
+ }
+
+ &__grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0 24px;
+ padding: 6px 16px 10px;
+
+ @media (width <= 900px) {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ &__row {
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 7px 0;
+ border-bottom: 1px solid #f1f5f9;
+ font-size: 13px;
+ }
+
+ &__label {
+ color: #64748b;
+ }
+
+ &__value {
+ color: #1e293b;
+ font-weight: 500;
+ text-align: right;
+ word-break: break-word;
+ }
+}
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.ts
index 87b1a8914..fbbb353aa 100644
--- a/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.ts
+++ b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.ts
@@ -6,44 +6,246 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
-import { LoanProducts } from '../../loan-products';
-import { LoanProductSummaryComponent } from '../../common/loan-product-summary/loan-product-summary.component';
-import { FaIconComponent } from '@fortawesome/angular-fontawesome';
-import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { productCategoryLabel, productTypeLabel } from '../../loan-product-types';
+interface ReviewRow {
+ label: string;
+ value: string;
+}
+
+interface ReviewSection {
+ title: string;
+ icon: string;
+ rows: ReviewRow[];
+}
+
+/**
+ * Review step for the Advances and Loans wizards: a plain read-out of what is about to be submitted, in sections, with
+ * the GL accounts shown by name. (The standard Fineract summary expects option objects this wizard does not build.)
+ */
@Component({
selector: 'mifosx-loan-product-preview-step',
templateUrl: './loan-product-preview-step.component.html',
styleUrls: ['./loan-product-preview-step.component.scss'],
- imports: [
- ...STANDALONE_SHARED_IMPORTS,
- LoanProductSummaryComponent,
- FaIconComponent
- ]
+ imports: []
})
-export class LoanProductPreviewStepComponent implements OnInit, OnChanges {
+export class LoanProductPreviewStepComponent {
@Input() loanProductsTemplate: any;
@Input() accountingRuleData: any;
@Input() loanProduct: any;
- @Input() isValid: boolean = true;
+ @Input() isValid = true;
@Output() submitEvent = new EventEmitter();
- isAdvancedPaymentAllocation = false;
+ /** Keys shown in a section above; everything else non-empty goes to "Other settings". */
+ private readonly shown = new Set();
+ private cachedFor: any = null;
+ private cachedSections: ReviewSection[] = [];
+ private cachedOther: ReviewRow[] = [];
- constructor() {}
+ /** Keys that are technical or already covered, never shown. */
+ private readonly hidden = new Set([
+ 'locale',
+ 'dateFormat',
+ 'charges',
+ 'accountingRule',
+ 'inMultiplesOf',
+ 'repaymentEvery',
+ 'repaymentFrequencyType',
+ 'interestRateFrequencyType',
+ 'amortizationType',
+ 'interestType',
+ 'interestCalculationPeriodType',
+ 'transactionProcessingStrategyCode',
+ 'daysInYearType',
+ 'daysInMonthType',
+ 'isInterestRecalculationEnabled',
+ 'glAccount',
+ 'interestRatePerPeriod'
+ ]);
- ngOnInit() {
- this.advancedPaymentAllocation();
+ get sections(): ReviewSection[] {
+ this.build();
+ return this.cachedSections;
}
- ngOnChanges(changes: SimpleChanges): void {
- this.advancedPaymentAllocation();
+ get otherSettings(): ReviewRow[] {
+ this.build();
+ return this.cachedOther;
}
- advancedPaymentAllocation() {
- this.isAdvancedPaymentAllocation = LoanProducts.isAdvancedPaymentAllocationStrategy(
- this.loanProduct.transactionProcessingStrategyCode
- );
+ /** Rebuilt only when the product object changes (it is stable until a form value changes). */
+ private build(): void {
+ const p = this.loanProduct;
+ if (!p || p === this.cachedFor) {
+ return;
+ }
+ this.cachedFor = p;
+ this.shown.clear();
+
+ const row = (label: string, key: string, format?: (value: any) => string): ReviewRow | null => {
+ this.shown.add(key);
+ const value = p[key];
+ if (value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0)) {
+ return null;
+ }
+ return { label, value: format ? format(value) : this.format(value) };
+ };
+ const gl = (label: string, key: string) => row(label, key, (id) => this.glName(id));
+ const pct = (value: any) => `${value}%`;
+ const months = (value: any) => `${value} months`;
+ const rupees = (value: any) => `₹ ${Number(value).toLocaleString('en-IN')}`;
+
+ const sections: ReviewSection[] = [
+ {
+ title: 'Product',
+ icon: 'fa-hand-holding-usd',
+ rows: [
+ { label: 'Type', value: productCategoryLabel(p.productTypeGroup) },
+ { label: 'Sub type', value: productTypeLabel(p.productTypeGroup, p.productTypeSubGroup) },
+ row('Name', 'name'),
+ row('Short name', 'shortName'),
+ gl('Product GL (asset)', 'glAccount'),
+ row('Start date', 'startDate'),
+ row('Close date', 'closeDate'),
+ row('Currency', 'currencyCode'),
+ row('Decimal places', 'digitsAfterDecimal')
+ ].filter((r): r is ReviewRow => !!r)
+ },
+ {
+ title: 'Amount and tenure',
+ icon: 'fa-coins',
+ rows: [
+ row('Facility type', 'facilityType'),
+ row('Minimum amount / limit', 'minPrincipal', rupees),
+ row('Default amount / limit', 'principal', rupees),
+ row('Maximum amount / limit', 'maxPrincipal', rupees),
+ row('Minimum tenure', 'minNumberOfRepayments', months),
+ row('Default tenure', 'numberOfRepayments', months),
+ row('Maximum tenure', 'maxNumberOfRepayments', months),
+ row('Repayment type', 'repaymentType'),
+ row('Appropriation order', 'appropriationOrder')
+ ].filter((r): r is ReviewRow => !!r)
+ },
+ {
+ title: 'Interest',
+ icon: 'fa-percent',
+ rows: [
+ row('Rate register row', 'registerParticular'),
+ p.interestRatePerPeriod !== undefined
+ ? { label: 'Rate (default)', value: pct(p.interestRatePerPeriod) }
+ : null,
+ row('Spread', 'spread', pct),
+ row('Calculation method', 'interestCalcMethod'),
+ row('Interest charged on', 'interestChargedOn'),
+ row('Posting frequency', 'interestPostingFrequency'),
+ row('Posting day', 'interestPostingDay')
+ ].filter((r): r is ReviewRow => !!r)
+ },
+ {
+ title: 'Security and lien',
+ icon: 'fa-shield-halved',
+ rows: [
+ row('Security type', 'securityType'),
+ row('Eligible amount basis', 'eligibleAmountBasis'),
+ row('Margin', 'margin', pct),
+ row('LTV', 'ltv', pct),
+ row('Lien required', 'lienRequired'),
+ row('Lien marking stage', 'lienMarkingStage'),
+ row('Eligible deposit products', 'eligibleDepositProducts'),
+ row('Lien certificate prefix', 'lienCertificatePrefix'),
+ row('Tenure cannot exceed deposit maturity', 'lienTenureNotExceedMaturity'),
+ row('Auto-release lien on closure', 'lienAutoReleaseOnClosure'),
+ row('Multiple deposits for one advance', 'lienAllowMultipleDeposits'),
+ row('Adjust from deposit at maturity', 'lienAdjustFromMaturity'),
+ row('Third-party deposit allowed', 'lienAllowThirdParty'),
+ row('Deposit renewal under lien', 'lienAllowRenewalUnderLien')
+ ].filter((r): r is ReviewRow => !!r)
+ },
+ {
+ title: 'Penal, SMA and NPA',
+ icon: 'fa-triangle-exclamation',
+ rows: [
+ row('Penal charges', 'penalApplicable'),
+ row('Penal rate', 'penalRate', pct),
+ row('Penal applied on', 'penalAppliedOn'),
+ row('Penal grace days', 'penalGraceDays'),
+ row('SMA counting starts after', 'smaStartAfterMonths', months),
+ row('NPA after', 'overdueDaysForNPA', (v) => `${v} days overdue`),
+ row('NPA trigger', 'npaTrigger'),
+ row('Stock statement validity', 'stockStatementValidityDays', (v) => `${v} days`),
+ row('Interest after NPA', 'interestAfterNpa'),
+ row('Upgrade only when all arrears are cleared', 'npaUpgradeOnlyAllArrears'),
+ row('Keep standard while deposit covers dues', 'npaKeepStandardIfDepositCovers')
+ ].filter((r): r is ReviewRow => !!r)
+ },
+ {
+ title: 'Renewal and closure',
+ icon: 'fa-rotate',
+ rows: [
+ row('Renewal allowed', 'renewalAllowed'),
+ row('Renewal period', 'renewalPeriodMonths', months),
+ row('Renewal grace', 'renewalGraceDays', (v) => `${v} days`),
+ row('Pre-closure', 'preclosureAllowed'),
+ row('Lock-in', 'lockInMonths', months),
+ row('Interest on pre-closure', 'interestOnPreclosure')
+ ].filter((r): r is ReviewRow => !!r)
+ },
+ {
+ title: 'Accounting (GL accounts)',
+ icon: 'fa-book',
+ rows: [
+ gl('Loan portfolio', 'loanPortfolioAccountId'),
+ gl('Fund source', 'fundSourceAccountId'),
+ gl('Interest income', 'interestOnLoanAccountId'),
+ gl('Fee income', 'incomeFromFeeAccountId'),
+ gl('Penal income', 'incomeFromPenaltyAccountId'),
+ gl('Write-off', 'writeOffAccountId'),
+ gl('Interest receivable', 'receivableInterestAccountId'),
+ gl('Overpayment', 'overpaymentLiabilityAccountId'),
+ gl('Interest suspense', 'undebitedInterestGlId')
+ ].filter((r): r is ReviewRow => !!r)
+ }
+ ].filter((section) => section.rows.length > 0);
+
+ // The group and sub type are shown through the label row above.
+ this.shown.add('productTypeGroup');
+ this.shown.add('productTypeSubGroup');
+
+ this.cachedSections = sections;
+ this.cachedOther = Object.keys(p)
+ .filter((key) => !this.shown.has(key) && !this.hidden.has(key))
+ .filter(
+ (key) => p[key] !== undefined && p[key] !== null && p[key] !== '' && !(Array.isArray(p[key]) && !p[key].length)
+ )
+ .map((key) => ({ label: this.humanize(key), value: this.format(p[key]) }));
+ }
+
+ private glName(id: any): string {
+ const options = this.loanProductsTemplate?.accountingMappingOptions ?? {};
+ const all: any[] = [
+ ...(options.assetAccountOptions ?? []),
+ ...(options.incomeAccountOptions ?? []),
+ ...(options.expenseAccountOptions ?? []),
+ ...(options.liabilityAccountOptions ?? []),
+ ...(options.equityAccountOptions ?? [])
+ ];
+ const account = all.find((entry) => String(entry.id) === String(id));
+ return account ? `${account.glCode} ${account.name}` : String(id);
+ }
+
+ private humanize(key: string): string {
+ const spaced = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/_/g, ' ');
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
+ }
+
+ private format(value: any): string {
+ if (typeof value === 'boolean') {
+ return value ? 'Yes' : 'No';
+ }
+ if (Array.isArray(value)) {
+ return value.map((entry) => (typeof entry === 'object' ? JSON.stringify(entry) : entry)).join(', ');
+ }
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
}
}
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-security-lien-step/loan-product-security-lien-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-security-lien-step/loan-product-security-lien-step.component.ts
index d0327c94a..318cb7292 100644
--- a/src/app/products/loan-products/loan-product-stepper/loan-product-security-lien-step/loan-product-security-lien-step.component.ts
+++ b/src/app/products/loan-products/loan-product-stepper/loan-product-security-lien-step/loan-product-security-lien-step.component.ts
@@ -85,9 +85,18 @@ export class LoanProductSecurityLienStepComponent implements OnInit {
'Fixed Deposit – Regular',
'Fixed Deposit – Senior Citizen',
'Recurring Deposit – Regular',
- 'Daily Deposit Scheme'
+ 'Daily Deposit Scheme',
+ 'LIC Policy'
];
+ /** The deposit or policy a sub type is lent against; this is the default "Eligible Deposit Products". */
+ private readonly defaultDepositBySubGroup: Record = {
+ fd: 'Fixed Deposit – Regular',
+ rd: 'Recurring Deposit – Regular',
+ dds: 'Daily Deposit Scheme',
+ lic: 'LIC Policy'
+ };
+
readonly yesNoOptions = [
'YES',
'NO'
@@ -111,6 +120,13 @@ export class LoanProductSecurityLienStepComponent implements OnInit {
if (margin != null && !this.loanProductSecurityLienForm.get('margin').dirty) {
this.loanProductSecurityLienForm.get('margin').setValue(margin, { emitEvent: false });
}
+ // The deposit list follows the sub type until the officer has chosen their own.
+ // A sub type with no deposit of its own (gold, CC ...) keeps the plain FD default; the list is hidden for it.
+ const deposit = this.defaultDepositBySubGroup[subGroup] ?? 'Fixed Deposit – Regular';
+ const deposits = this.loanProductSecurityLienForm.get('eligibleDepositProducts');
+ if (deposit && deposits && !deposits.dirty) {
+ deposits.setValue([deposit], { emitEvent: false });
+ }
}
/** This whole step is only relevant to an Advances A/C product. */
diff --git a/src/app/products/loan-products/loan-product-stepper/register-rate-view/register-rate-view.component.ts b/src/app/products/loan-products/loan-product-stepper/register-rate-view/register-rate-view.component.ts
index 44d9a35cc..1d92c19cb 100644
--- a/src/app/products/loan-products/loan-product-stepper/register-rate-view/register-rate-view.component.ts
+++ b/src/app/products/loan-products/loan-product-stepper/register-rate-view/register-rate-view.component.ts
@@ -45,6 +45,11 @@ export interface RegisterRateSelection {
const PARTICULAR_BY_SUB_TYPE: Record = {
fd: 'against fdr',
rd: 'against rd',
+ dds: 'against dds',
+ lic: 'against lic',
+ warehouse: 'warehouse',
+ bills: 'bills purchased',
+ education: 'education loan',
gold: 'gold loan',
cc: 'cash credit',
overdraft: 'overdraft',
diff --git a/src/app/products/loan-products/loan-products.component.html b/src/app/products/loan-products/loan-products.component.html
index 676738f06..3f1726628 100644
--- a/src/app/products/loan-products/loan-products.component.html
+++ b/src/app/products/loan-products/loan-products.component.html
@@ -81,9 +81,33 @@
placeholder="Search by name or short name…"
/>