feat(loan-products): Gold, Warehouse Receipt and Loans (EMI) product creation, view and edit
Advances A/C - Loan against Gold: Security & Gold tab (valuation basis, appraiser, custody, revaluation on NPA, partial release, disbursement mode and cash limit, LTV alert, auction notice and reserve price), Maximum LTV cap, 1-12 month tenure band, lien/deposit fields hidden and not sent. - Warehouse Receipt: Security & Warehouse tab (receipt type, commodity groups, valuation price source, inspection, insurance, disposal rules), 1-12 month tenure band, Commodity collateral type. - Category follows the sub type; Priority Sector and CIBIL follow the category (officer's choice kept). - Edit keeps the saved charges (filling the currency used to empty the list). - View page shows the saved Advances settings; Review and the maker-checker diff carry the new fields. Loans A/C (EMI) - Edit opens in the same wizard and fills every tab from the saved record; View shows the saved settings (eligibility, security and guarantors, disbursement, repayment, interest, prepayment). - Security conditions follow the sub type (property, vehicle, gold, unsecured); the sub type is now passed to the Security step, so its per-type defaults apply. - Tenure band and age limits per sub type from the bank's loan policy (max 120 months); member, guarantor-member and income-multiple eligibility fields. - Repayment & EMI is common to all EMI loans; cheques (PDC) are the default mode, per the policy. - Floating-rate foreclosure for an individual is locked at nil (RBI); the duplicate "release on closure" and the static collateral rules table are removed. - Term loan no longer sends the deposit-cover rule or "out of order" (CC / OD only). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+13
-3
@@ -48,7 +48,13 @@
|
|||||||
<mat-tab [disabled]="!hasProductType">
|
<mat-tab [disabled]="!hasProductType">
|
||||||
<ng-template mat-tab-label>
|
<ng-template mat-tab-label>
|
||||||
<i class="fas fa-shield-alt lp-tab-icon"></i>
|
<i class="fas fa-shield-alt lp-tab-icon"></i>
|
||||||
SECURITY & LIEN
|
{{
|
||||||
|
advanceSubType === 'gold'
|
||||||
|
? 'SECURITY & GOLD'
|
||||||
|
: advanceSubType === 'warehouse'
|
||||||
|
? 'SECURITY & WAREHOUSE'
|
||||||
|
: 'SECURITY & LIEN'
|
||||||
|
}}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="lp-tab-content">
|
<div class="lp-tab-content">
|
||||||
<mifosx-loan-product-security-lien-step
|
<mifosx-loan-product-security-lien-step
|
||||||
@@ -114,7 +120,9 @@
|
|||||||
SECURITY & COLLATERAL
|
SECURITY & COLLATERAL
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="lp-tab-content">
|
<div class="lp-tab-content">
|
||||||
<mifosx-loan-product-loan-security-step></mifosx-loan-product-loan-security-step>
|
<mifosx-loan-product-loan-security-step
|
||||||
|
[productTypeSubGroup]="loanProductDetailsForm.get('productTypeSubGroup')"
|
||||||
|
></mifosx-loan-product-loan-security-step>
|
||||||
</div>
|
</div>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
|
||||||
@@ -124,7 +132,9 @@
|
|||||||
AMOUNT & TENURE
|
AMOUNT & TENURE
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="lp-tab-content">
|
<div class="lp-tab-content">
|
||||||
<mifosx-loan-product-loan-amount-tenure-step></mifosx-loan-product-loan-amount-tenure-step>
|
<mifosx-loan-product-loan-amount-tenure-step
|
||||||
|
[productSubType]="advanceSubType"
|
||||||
|
></mifosx-loan-product-loan-amount-tenure-step>
|
||||||
</div>
|
</div>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
|
||||||
|
|||||||
+271
-23
@@ -182,9 +182,10 @@ export class CreateLoanProductComponent implements OnInit, AfterViewInit {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.accountingRuleData = this.accounting.getAccountingRulesForLoans();
|
this.accountingRuleData = this.accounting.getAccountingRulesForLoans();
|
||||||
this.buildAdvancedPaymentAllocation();
|
this.buildAdvancedPaymentAllocation();
|
||||||
// Only Advances products are filled into the curated tabs so far. A product made before the type was saved, or a
|
// Advances and Loans A/C (EMI) products are filled into the curated tabs. A product made before the type was saved
|
||||||
// Loans A/C (EMI) product, uses the old editor instead of an empty form.
|
// uses the old editor instead of an empty form.
|
||||||
if (this.isEdit && this.loanProductsTemplate?.advanceExtensions?.productTypeGroup !== 'advances') {
|
const savedGroup = this.loanProductsTemplate?.advanceExtensions?.productTypeGroup;
|
||||||
|
if (this.isEdit && savedGroup !== 'advances' && savedGroup !== 'loans') {
|
||||||
this.router.navigate(['../edit-legacy'], { relativeTo: this.route });
|
this.router.navigate(['../edit-legacy'], { relativeTo: this.route });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,14 +209,163 @@ export class CreateLoanProductComponent implements OnInit, AfterViewInit {
|
|||||||
this.loanProductPenalNpaStep &&
|
this.loanProductPenalNpaStep &&
|
||||||
(!this.isOverdraftFacility || this.loanProductChequeBookStep)
|
(!this.isOverdraftFacility || this.loanProductChequeBookStep)
|
||||||
)
|
)
|
||||||
: false;
|
: group === 'loans'
|
||||||
|
? !!(
|
||||||
|
this.loanProductLoanSecurityStep &&
|
||||||
|
this.loanProductLoanAmountTenureStep &&
|
||||||
|
this.loanProductLoanRepaymentStep &&
|
||||||
|
this.loanProductLoanInterestStep &&
|
||||||
|
this.loanProductLoanPrepaymentStep &&
|
||||||
|
this.loanProductPenalNpaStep
|
||||||
|
)
|
||||||
|
: false;
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
if (attempt < 60) {
|
if (attempt < 60) {
|
||||||
setTimeout(() => this.populateWhenReady(attempt + 1), 150);
|
setTimeout(() => this.populateWhenReady(attempt + 1), 150);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.populateAdvance();
|
if (group === 'loans') {
|
||||||
|
this.populateLoans();
|
||||||
|
} else {
|
||||||
|
this.populateAdvance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reverse of assembleLoanProduct: the saved core fields and the extension record go back into the Loans tabs. */
|
||||||
|
private populateLoans(): void {
|
||||||
|
const p = this.loanProductsTemplate;
|
||||||
|
const e = p.advanceExtensions ?? {};
|
||||||
|
const repayment = this.loanProductLoanRepaymentStep;
|
||||||
|
const interest = this.loanProductLoanInterestStep;
|
||||||
|
const frequency = repayment.frequencyOptions.find((o) => o.fineractId === p.repaymentFrequencyType?.id)?.id;
|
||||||
|
const daysInYear = interest.daysInYearOptions.find((o) => o.fineractId === p.daysInYearType?.id)?.id;
|
||||||
|
const daysInMonth = interest.daysInMonthOptions.find((o) => o.fineractId === p.daysInMonthType?.id)?.id;
|
||||||
|
|
||||||
|
this.fill(this.loanProductCurrencyStep?.loanProductCurrencyForm, {
|
||||||
|
currencyCode: p.currency?.code,
|
||||||
|
digitsAfterDecimal: p.currency?.decimalPlaces
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fill(this.loanProductLoanSecurityStep.loanProductLoanSecurityForm, {
|
||||||
|
securityType: e.securityType,
|
||||||
|
securityMandatory: e.securityMandatory,
|
||||||
|
margin: e.margin,
|
||||||
|
guarantorsRequired: e.guarantorsRequired,
|
||||||
|
coApplicant: e.coApplicant,
|
||||||
|
thirdPartySecurity: e.thirdPartySecurity,
|
||||||
|
legalOpinionRequired: e.legalOpinionRequired,
|
||||||
|
propertyInsuranceRequired: e.propertyInsuranceRequired,
|
||||||
|
registerCharge: e.registerCharge,
|
||||||
|
autoReleaseOnClosure: e.releaseSecurityOnClosure,
|
||||||
|
allowMultipleSecurities: e.allowMultipleSecurities,
|
||||||
|
allowThirdPartyDeposit: e.allowThirdPartyDeposit
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fill(this.loanProductLoanAmountTenureStep.loanProductLoanAmountTenureForm, {
|
||||||
|
minAmount: p.minPrincipal,
|
||||||
|
defaultAmount: p.principal,
|
||||||
|
maxAmount: p.maxPrincipal,
|
||||||
|
minTenure: p.minNumberOfRepayments,
|
||||||
|
defaultTenure: p.numberOfRepayments,
|
||||||
|
maxTenure: p.maxNumberOfRepayments,
|
||||||
|
minAge: e.minAge,
|
||||||
|
maxAgeAtMaturity: e.maxAgeAtMaturity,
|
||||||
|
minCibil: e.minCibil,
|
||||||
|
minMonthlyIncome: e.minMonthlyIncome,
|
||||||
|
maxFoir: e.maxFoir,
|
||||||
|
maxLoansPerCustomer: e.maxLoansPerCustomer,
|
||||||
|
memberRequired: e.memberRequired,
|
||||||
|
guarantorMustBeMember: e.guarantorMustBeMember,
|
||||||
|
incomeMultiple: e.incomeMultiple,
|
||||||
|
disbursementType: e.disbursementType,
|
||||||
|
// The tranche count only applies to a multiple disbursement; a single one leaves the field at its own default.
|
||||||
|
maxTranches: e.disbursementType === 'multiple' ? p.maxTrancheCount : undefined,
|
||||||
|
disbursementMode: e.disbursementMode,
|
||||||
|
undisbursedInterest: e.undisbursedInterest,
|
||||||
|
preEmiInterest: e.preEmiInterest
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fill(repayment.loanProductLoanRepaymentForm, {
|
||||||
|
repaymentType: e.repaymentType,
|
||||||
|
repaidEvery: p.repaymentEvery,
|
||||||
|
frequency,
|
||||||
|
minDaysToFirstRepayment: p.minimumDaysBetweenDisbursalAndFirstRepayment,
|
||||||
|
emiDueDay: e.emiDueDay,
|
||||||
|
holidayRule: e.holidayRule,
|
||||||
|
moratoriumMonths: e.moratoriumMonths,
|
||||||
|
moratoriumInterest: e.moratoriumInterest,
|
||||||
|
variableInstallments: e.variableInstallments,
|
||||||
|
repaymentMode: e.repaymentMode,
|
||||||
|
linkedAccountType: e.linkedAccountType,
|
||||||
|
pdcCollection: e.pdcCollection,
|
||||||
|
mandateRequired: e.mandateRequired,
|
||||||
|
appropriationOrder: e.appropriationOrder,
|
||||||
|
advanceEmi: e.advanceEmi
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fill(interest.loanProductLoanInterestForm, {
|
||||||
|
interestType: e.rateType,
|
||||||
|
registerParticular: e.registerParticular,
|
||||||
|
applyCibilAdjustment: e.applyCibilAdjustment,
|
||||||
|
spread: e.spread,
|
||||||
|
rateResolvedAt: e.rateResolvedAt,
|
||||||
|
minRate: p.minInterestRatePerPeriod,
|
||||||
|
defaultRate: p.interestRatePerPeriod,
|
||||||
|
maxRate: p.maxInterestRatePerPeriod,
|
||||||
|
calcMethod: e.interestCalcMethod,
|
||||||
|
daysInYear,
|
||||||
|
daysInMonth,
|
||||||
|
calcPeriod: e.calcPeriod,
|
||||||
|
interestStartsFrom: e.interestStartsFrom,
|
||||||
|
compounding: e.compounding,
|
||||||
|
benchmark: e.benchmark,
|
||||||
|
resetFrequency: e.resetFrequency,
|
||||||
|
resetChange: e.resetChange
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fill(this.loanProductLoanPrepaymentStep.loanProductLoanPrepaymentForm, {
|
||||||
|
partPrepaymentAllowed: e.partPrepaymentAllowed,
|
||||||
|
minPartPayment: e.minPartPayment,
|
||||||
|
allowedOn: e.allowedOn,
|
||||||
|
partPaymentsPerYear: e.partPaymentsPerYear,
|
||||||
|
partPrepaymentMode: e.partPrepaymentMode,
|
||||||
|
foreclosureFloatingIndividual: e.foreclosureFloatingIndividual,
|
||||||
|
foreclosureFixedIndividual: e.foreclosureFixedIndividual,
|
||||||
|
foreclosureNonIndividual: e.foreclosureNonIndividual,
|
||||||
|
foreclosureTakeover: e.foreclosureTakeover,
|
||||||
|
lockInMonths: e.lockInMonths,
|
||||||
|
interestOnPreclosure: e.interestOnPreclosure,
|
||||||
|
foreclosureQuoteValidDays: e.foreclosureQuoteValidDays,
|
||||||
|
autoCloseOnZeroBalance: e.autoCloseOnZeroBalance,
|
||||||
|
nocLetter: e.nocLetter,
|
||||||
|
restructuring: e.restructuring,
|
||||||
|
topUpAllowed: e.topUpAllowed,
|
||||||
|
balanceTransferOutAllowed: e.balanceTransferOutAllowed
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fill(this.loanProductPenalNpaStep.loanProductPenalNpaForm, {
|
||||||
|
penalApplicable: e.penalApplicable,
|
||||||
|
penalRate: e.penalRate,
|
||||||
|
penalAppliedOn: e.penalAppliedOn,
|
||||||
|
penalGraceDays: e.penalGraceDays,
|
||||||
|
delinquencyBucketId: p.delinquencyBucket?.id,
|
||||||
|
npaDays: p.overdueDaysForNPA,
|
||||||
|
npaTrigger: e.npaTrigger,
|
||||||
|
interestAfterNpa: e.interestAfterNpa,
|
||||||
|
smaStartAfterMonths: e.smaStartAfterMonths,
|
||||||
|
inArrearsTolerance: p.inArrearsTolerance,
|
||||||
|
upgradeOnlyAllArrears: e.npaUpgradeOnlyAllArrears
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fill(this.loanProductAccountingStep?.loanProductAccountingForm, {
|
||||||
|
undebitedInterestAccountId: e.undebitedInterestGlId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filling the currency empties the Charges list; put the saved charges back, or saving would detach them.
|
||||||
|
if (this.loanProductChargesStep) {
|
||||||
|
this.loanProductChargesStep.chargesDataSource = [...(p.charges ?? [])];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the controls and marks them touched-by-user so a sub type default does not overwrite the saved value. */
|
/** Sets the controls and marks them touched-by-user so a sub type default does not overwrite the saved value. */
|
||||||
@@ -265,7 +415,39 @@ export class CreateLoanProductComponent implements OnInit, AfterViewInit {
|
|||||||
allowRenewalUnderLien: e.lienAllowRenewalUnderLien,
|
allowRenewalUnderLien: e.lienAllowRenewalUnderLien,
|
||||||
lienCertificateRequired: e.lienCertificateRequired,
|
lienCertificateRequired: e.lienCertificateRequired,
|
||||||
lienCertificatePrefix: e.lienCertificatePrefix,
|
lienCertificatePrefix: e.lienCertificatePrefix,
|
||||||
printOnDisbursement: e.printCertificateOnDisbursement
|
printOnDisbursement: e.printCertificateOnDisbursement,
|
||||||
|
goldMaxLtv: e.goldMaxLtv,
|
||||||
|
goldMinPurityCarat: e.goldMinPurityCarat,
|
||||||
|
goldRateSource: e.goldRateSource,
|
||||||
|
goldMaxPerGram: e.goldMaxPerGram,
|
||||||
|
goldMinNetWeight: e.goldMinNetWeight,
|
||||||
|
goldMaxNetWeightPerCustomer: e.goldMaxNetWeightPerCustomer,
|
||||||
|
goldAllowedItems: e.goldAllowedItems,
|
||||||
|
goldValuationCertificateRequired: e.goldValuationCertificateRequired,
|
||||||
|
goldMaxValuationPerAppraiser: e.goldMaxValuationPerAppraiser,
|
||||||
|
goldPacketPrefix: e.goldPacketPrefix,
|
||||||
|
goldRevalueOnNpa: e.goldRevalueOnNpa,
|
||||||
|
goldRevaluationChargeApplicable: e.goldRevaluationChargeApplicable,
|
||||||
|
insuranceRequired: e.insuranceRequired,
|
||||||
|
goldAuctionNoticeDays: e.goldAuctionNoticeDays,
|
||||||
|
goldPartialReleaseAllowed: e.goldPartialReleaseAllowed,
|
||||||
|
goldThirdPartyAllowed: e.goldThirdPartyAllowed,
|
||||||
|
goldDisbursementMode: e.goldDisbursementMode,
|
||||||
|
goldCashDisbursementLimit: e.goldCashDisbursementLimit,
|
||||||
|
goldLtvBreachAlert: e.goldLtvBreachAlert,
|
||||||
|
goldAuctionReservePct: e.goldAuctionReservePct,
|
||||||
|
wrMaxLtv: e.wrMaxLtv,
|
||||||
|
wrReceiptType: e.wrReceiptType,
|
||||||
|
wrTenureWithinReceiptValidity: e.wrTenureWithinReceiptValidity,
|
||||||
|
wrPledgeMarkedBeforeDisbursement: e.wrPledgeMarkedBeforeDisbursement,
|
||||||
|
wrCommodityGroups: split(e.wrCommodityGroups),
|
||||||
|
wrValuationPriceSource: e.wrValuationPriceSource,
|
||||||
|
wrMaxLoanPerQuintal: e.wrMaxLoanPerQuintal,
|
||||||
|
wrStockInspectionMonths: e.wrStockInspectionMonths,
|
||||||
|
wrLtvBreachAlert: e.wrLtvBreachAlert,
|
||||||
|
wrPartialReleaseAllowed: e.wrPartialReleaseAllowed,
|
||||||
|
wrDisposalNoticeDays: e.wrDisposalNoticeDays,
|
||||||
|
wrDisposalReservePct: e.wrDisposalReservePct
|
||||||
});
|
});
|
||||||
|
|
||||||
this.fill(this.loanProductAdvanceLimitTenureStep.loanProductAdvanceLimitTenureForm, {
|
this.fill(this.loanProductAdvanceLimitTenureStep.loanProductAdvanceLimitTenureForm, {
|
||||||
@@ -348,6 +530,12 @@ export class CreateLoanProductComponent implements OnInit, AfterViewInit {
|
|||||||
this.fill(this.loanProductAccountingStep?.loanProductAccountingForm, {
|
this.fill(this.loanProductAccountingStep?.loanProductAccountingForm, {
|
||||||
undebitedInterestAccountId: e.undebitedInterestGlId
|
undebitedInterestAccountId: e.undebitedInterestGlId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Filling the currency above empties the Charges list (a new currency invalidates it); put the saved charges back,
|
||||||
|
// or saving the edit would detach every charge from the product.
|
||||||
|
if (this.loanProductChargesStep) {
|
||||||
|
this.loanProductChargesStep.chargesDataSource = [...(p.charges ?? [])];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get loanProductDetailsForm() {
|
get loanProductDetailsForm() {
|
||||||
@@ -848,10 +1036,9 @@ export class CreateLoanProductComponent implements OnInit, AfterViewInit {
|
|||||||
smaStartAfterMonths: num(pn.smaStartAfterMonths),
|
smaStartAfterMonths: num(pn.smaStartAfterMonths),
|
||||||
interestAfterNpa: pn.interestAfterNpa,
|
interestAfterNpa: pn.interestAfterNpa,
|
||||||
npaUpgradeOnlyAllArrears: pn.upgradeOnlyAllArrears,
|
npaUpgradeOnlyAllArrears: pn.upgradeOnlyAllArrears,
|
||||||
npaKeepStandardIfDepositCovers: pn.keepStandardIfDepositCovers,
|
|
||||||
|
|
||||||
// The SICR & LGD (ECL) card is hidden for now, so its values are not sent.
|
// The SICR & LGD (ECL) card is hidden for now, so its values are not sent. "Out of order" (CC / OD only) and the
|
||||||
npaOutOfOrderBasis: pn.npaOutOfOrderBasis || undefined,
|
// deposit-cover rule do not apply to a term loan, so neither is sent.
|
||||||
cropDuration: pn.cropDuration || undefined,
|
cropDuration: pn.cropDuration || undefined,
|
||||||
|
|
||||||
securityMandatory: sec.securityMandatory,
|
securityMandatory: sec.securityMandatory,
|
||||||
@@ -870,6 +1057,9 @@ export class CreateLoanProductComponent implements OnInit, AfterViewInit {
|
|||||||
minMonthlyIncome: num(at.minMonthlyIncome),
|
minMonthlyIncome: num(at.minMonthlyIncome),
|
||||||
maxFoir: num(at.maxFoir),
|
maxFoir: num(at.maxFoir),
|
||||||
maxLoansPerCustomer: num(at.maxLoansPerCustomer),
|
maxLoansPerCustomer: num(at.maxLoansPerCustomer),
|
||||||
|
memberRequired: at.memberRequired,
|
||||||
|
guarantorMustBeMember: at.guarantorMustBeMember,
|
||||||
|
incomeMultiple: num(at.incomeMultiple),
|
||||||
disbursementType: at.disbursementType,
|
disbursementType: at.disbursementType,
|
||||||
disbursementMode: at.disbursementMode,
|
disbursementMode: at.disbursementMode,
|
||||||
undisbursedInterest: at.undisbursedInterest,
|
undisbursedInterest: at.undisbursedInterest,
|
||||||
@@ -907,7 +1097,6 @@ export class CreateLoanProductComponent implements OnInit, AfterViewInit {
|
|||||||
autoCloseOnZeroBalance: pp.autoCloseOnZeroBalance,
|
autoCloseOnZeroBalance: pp.autoCloseOnZeroBalance,
|
||||||
nocLetter: pp.nocLetter,
|
nocLetter: pp.nocLetter,
|
||||||
restructuring: pp.restructuring,
|
restructuring: pp.restructuring,
|
||||||
releaseCollateralOnClosure: pp.releaseCollateralOnClosure,
|
|
||||||
topUpAllowed: pp.topUpAllowed,
|
topUpAllowed: pp.topUpAllowed,
|
||||||
balanceTransferOutAllowed: pp.balanceTransferOutAllowed
|
balanceTransferOutAllowed: pp.balanceTransferOutAllowed
|
||||||
};
|
};
|
||||||
@@ -929,6 +1118,9 @@ export class CreateLoanProductComponent implements OnInit, AfterViewInit {
|
|||||||
const d = this.loanProductDetailsStep.loanProductDetails;
|
const d = this.loanProductDetailsStep.loanProductDetails;
|
||||||
const cur = this.loanProductCurrencyStep.loanProductCurrency;
|
const cur = this.loanProductCurrencyStep.loanProductCurrency;
|
||||||
const sec = this.loanProductSecurityLienStep.loanProductSecurityLien;
|
const sec = this.loanProductSecurityLienStep.loanProductSecurityLien;
|
||||||
|
const lien = this.loanProductSecurityLienStep.isLienSubType;
|
||||||
|
const gold = this.loanProductSecurityLienStep.isGold ? sec.gold : null;
|
||||||
|
const wr = this.loanProductSecurityLienStep.isWarehouse ? sec.warehouse : null;
|
||||||
const lt = this.loanProductAdvanceLimitTenureStep.loanProductAdvanceLimitTenure;
|
const lt = this.loanProductAdvanceLimitTenureStep.loanProductAdvanceLimitTenure;
|
||||||
const intr = this.loanProductAdvanceInterestStep.loanProductAdvanceInterest;
|
const intr = this.loanProductAdvanceInterestStep.loanProductAdvanceInterest;
|
||||||
const pn = this.loanProductPenalNpaStep.loanProductPenalNpa;
|
const pn = this.loanProductPenalNpaStep.loanProductPenalNpa;
|
||||||
@@ -1004,10 +1196,62 @@ export class CreateLoanProductComponent implements OnInit, AfterViewInit {
|
|||||||
ltv: sec.ltv,
|
ltv: sec.ltv,
|
||||||
dpMethod: 'security_value_x_ltv',
|
dpMethod: 'security_value_x_ltv',
|
||||||
lienRequired: this.loanProductSecurityLienStep.isLienSubType,
|
lienRequired: this.loanProductSecurityLienStep.isLienSubType,
|
||||||
lienMarkingStage: sec.lienMarkingStage,
|
// Lien settings exist only for a deposit / policy sub type; a pledged one (Gold ...) sends none of them.
|
||||||
lienCertificateRequired: sec.lienCertificateRequired,
|
...(lien
|
||||||
lienCertificatePrefix: sec.lienCertificatePrefix,
|
? {
|
||||||
printCertificateOnDisbursement: sec.printOnDisbursement,
|
lienMarkingStage: sec.lienMarkingStage,
|
||||||
|
lienCertificateRequired: sec.lienCertificateRequired,
|
||||||
|
lienCertificatePrefix: sec.lienCertificatePrefix,
|
||||||
|
printCertificateOnDisbursement: sec.printOnDisbursement
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(gold
|
||||||
|
? {
|
||||||
|
goldMaxLtv: num(gold.maxLtv),
|
||||||
|
goldMinPurityCarat: num(gold.minPurityCarat),
|
||||||
|
goldRateSource: gold.rateSource,
|
||||||
|
goldMaxPerGram: num(gold.maxPerGram),
|
||||||
|
goldMinNetWeight: num(gold.minNetWeight),
|
||||||
|
goldMaxNetWeightPerCustomer: num(gold.maxNetWeightPerCustomer),
|
||||||
|
goldAllowedItems: gold.allowedItems,
|
||||||
|
goldAppraiserRequired: true,
|
||||||
|
goldValuationCertificateRequired: gold.valuationCertificateRequired === 'YES',
|
||||||
|
goldMaxValuationPerAppraiser: num(gold.maxValuationPerAppraiser),
|
||||||
|
goldPacketPrefix: gold.packetPrefix || undefined,
|
||||||
|
goldRevalueOnNpa: gold.revalueOnNpa,
|
||||||
|
goldRevaluationChargeApplicable: gold.revaluationChargeApplicable,
|
||||||
|
insuranceRequired: gold.insuranceRequired === 'YES',
|
||||||
|
goldAuctionNoticeDays: num(gold.auctionNoticeDays),
|
||||||
|
goldPartialReleaseAllowed: gold.partialReleaseAllowed,
|
||||||
|
goldThirdPartyAllowed: gold.thirdPartyAllowed,
|
||||||
|
goldDisbursementMode: gold.disbursementMode,
|
||||||
|
goldCashDisbursementLimit:
|
||||||
|
gold.disbursementMode === 'account_credit_cash_limit' ? num(gold.cashDisbursementLimit) : undefined,
|
||||||
|
goldLtvBreachAlert: num(gold.ltvBreachAlert),
|
||||||
|
goldAuctionReservePct: num(gold.auctionReservePct),
|
||||||
|
goldRedeemBeforeAuction: true
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(wr
|
||||||
|
? {
|
||||||
|
wrMaxLtv: num(wr.maxLtv),
|
||||||
|
wrReceiptType: wr.receiptType,
|
||||||
|
wrWdraRegistered: true,
|
||||||
|
wrAssayCertificateRequired: true,
|
||||||
|
wrTenureWithinReceiptValidity: wr.tenureWithinReceiptValidity,
|
||||||
|
wrPledgeMarkedBeforeDisbursement: wr.pledgeMarkedBeforeDisbursement,
|
||||||
|
wrCommodityGroups: (wr.commodityGroups ?? []).join('|'),
|
||||||
|
wrValuationPriceSource: wr.valuationPriceSource,
|
||||||
|
wrMaxLoanPerQuintal: num(wr.maxLoanPerQuintal),
|
||||||
|
wrStockInspectionMonths: num(wr.stockInspectionMonths),
|
||||||
|
insuranceRequired: wr.insuranceRequired === 'YES',
|
||||||
|
wrLtvBreachAlert: num(wr.ltvBreachAlert),
|
||||||
|
wrPartialReleaseAllowed: wr.partialReleaseAllowed,
|
||||||
|
wrDisposalNoticeDays: num(wr.disposalNoticeDays),
|
||||||
|
wrDisposalReservePct: num(wr.disposalReservePct),
|
||||||
|
wrRedeemBeforeDisposal: true
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
interestBasis: intr.interestBasis,
|
interestBasis: intr.interestBasis,
|
||||||
registerParticular: intr.registerParticular || undefined,
|
registerParticular: intr.registerParticular || undefined,
|
||||||
spread: num(intr.spread),
|
spread: num(intr.spread),
|
||||||
@@ -1039,14 +1283,18 @@ export class CreateLoanProductComponent implements OnInit, AfterViewInit {
|
|||||||
ppsMandatoryAbove: num(cb?.ppsMandatoryAbove),
|
ppsMandatoryAbove: num(cb?.ppsMandatoryAbove),
|
||||||
|
|
||||||
// ---- remaining wizard fields ----
|
// ---- remaining wizard fields ----
|
||||||
// security & lien extras (deposit list joined; rule checkboxes)
|
// security & lien extras (deposit list joined; rule checkboxes), only for a deposit / policy sub type
|
||||||
eligibleDepositProducts: (sec.eligibleDepositProducts ?? []).join('|'),
|
...(lien
|
||||||
lienTenureNotExceedMaturity: sec.lienRules?.tenureNotExceedMaturity,
|
? {
|
||||||
lienAutoReleaseOnClosure: sec.lienRules?.autoReleaseOnClosure,
|
eligibleDepositProducts: (sec.eligibleDepositProducts ?? []).join('|'),
|
||||||
lienAllowMultipleDeposits: sec.lienRules?.allowMultipleDeposits,
|
lienTenureNotExceedMaturity: sec.lienRules?.tenureNotExceedMaturity,
|
||||||
lienAdjustFromMaturity: sec.lienRules?.adjustFromMaturity,
|
lienAutoReleaseOnClosure: sec.lienRules?.autoReleaseOnClosure,
|
||||||
lienAllowThirdParty: sec.lienRules?.allowThirdPartyDeposit,
|
lienAllowMultipleDeposits: sec.lienRules?.allowMultipleDeposits,
|
||||||
lienAllowRenewalUnderLien: sec.lienRules?.allowRenewalUnderLien,
|
lienAdjustFromMaturity: sec.lienRules?.adjustFromMaturity,
|
||||||
|
lienAllowThirdParty: sec.lienRules?.allowThirdPartyDeposit,
|
||||||
|
lienAllowRenewalUnderLien: sec.lienRules?.allowRenewalUnderLien
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
// limit & tenure extras
|
// limit & tenure extras
|
||||||
repaymentType: lt.repaymentType,
|
repaymentType: lt.repaymentType,
|
||||||
appropriationOrder: lt.appropriationOrder,
|
appropriationOrder: lt.appropriationOrder,
|
||||||
@@ -1059,7 +1307,7 @@ export class CreateLoanProductComponent implements OnInit, AfterViewInit {
|
|||||||
interestPostingDay: intr.postingDay,
|
interestPostingDay: intr.postingDay,
|
||||||
// NPA upgrade rules
|
// NPA upgrade rules
|
||||||
npaUpgradeOnlyAllArrears: pn.upgradeOnlyAllArrears,
|
npaUpgradeOnlyAllArrears: pn.upgradeOnlyAllArrears,
|
||||||
npaKeepStandardIfDepositCovers: pn.keepStandardIfDepositCovers,
|
...(lien ? { npaKeepStandardIfDepositCovers: pn.keepStandardIfDepositCovers } : {}),
|
||||||
// cheque book full config
|
// cheque book full config
|
||||||
chequeBookSizes: (cb?.bookSizes ?? []).join('|'),
|
chequeBookSizes: (cb?.bookSizes ?? []).join('|'),
|
||||||
chequeMaxBooksOutstanding: num(cb?.maxBooksOutstanding),
|
chequeMaxBooksOutstanding: num(cb?.maxBooksOutstanding),
|
||||||
|
|||||||
@@ -22,5 +22,58 @@ export function withExtensions(product: any): any {
|
|||||||
if (merged.inArrearsTolerance === undefined || merged.inArrearsTolerance === null) {
|
if (merged.inArrearsTolerance === undefined || merged.inArrearsTolerance === null) {
|
||||||
merged.inArrearsTolerance = 0;
|
merged.inArrearsTolerance = 0;
|
||||||
}
|
}
|
||||||
|
// Same for the arrears grace: a Loans A/C (EMI) product counts arrears from day one, and the read leaves a zero out.
|
||||||
|
if (merged.graceOnArrearsAgeing === undefined || merged.graceOnArrearsAgeing === null) {
|
||||||
|
merged.graceOnArrearsAgeing = 0;
|
||||||
|
}
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Core accounting mapping name on the product read -> the payload name the Review reader uses. */
|
||||||
|
const GL_MAPPING_KEYS: Record<string, string> = {
|
||||||
|
loanPortfolioAccount: 'loanPortfolioAccountId',
|
||||||
|
fundSourceAccount: 'fundSourceAccountId',
|
||||||
|
interestOnLoanAccount: 'interestOnLoanAccountId',
|
||||||
|
incomeFromFeeAccount: 'incomeFromFeeAccountId',
|
||||||
|
incomeFromPenaltyAccount: 'incomeFromPenaltyAccountId',
|
||||||
|
writeOffAccount: 'writeOffAccountId',
|
||||||
|
receivableInterestAccount: 'receivableInterestAccountId',
|
||||||
|
overpaymentLiabilityAccount: 'overpaymentLiabilityAccountId'
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A date the API sends as [2026, 10, 15] shown as 15-10-2026. */
|
||||||
|
function dateText(value: any): string | undefined {
|
||||||
|
if (!Array.isArray(value) || value.length < 3) {
|
||||||
|
return value ?? undefined;
|
||||||
|
}
|
||||||
|
const [
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day
|
||||||
|
] = value;
|
||||||
|
return `${String(day).padStart(2, '0')}-${String(month).padStart(2, '0')}-${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A saved Advances product as the read-only Review page needs it: the flat field names the create wizard submits
|
||||||
|
* (core fields plus the extension record), the GL mappings as ids with a matching option list so each shows by name,
|
||||||
|
* and the attached charges as one readable line. Pure; the product object is not changed.
|
||||||
|
*/
|
||||||
|
export function reviewProductOf(product: any): { product: any; template: any } {
|
||||||
|
const flat: any = { ...withExtensions(product) };
|
||||||
|
const accounts: any[] = [];
|
||||||
|
Object.keys(GL_MAPPING_KEYS).forEach((name) => {
|
||||||
|
const account = product?.accountingMappings?.[name];
|
||||||
|
if (account?.id !== undefined) {
|
||||||
|
flat[GL_MAPPING_KEYS[name]] = account.id;
|
||||||
|
accounts.push({ id: account.id, glCode: account.glCode, name: account.name });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
flat.currencyCode = product?.currency?.code;
|
||||||
|
flat.digitsAfterDecimal = product?.currency?.decimalPlaces;
|
||||||
|
flat.startDate = dateText(product?.startDate);
|
||||||
|
flat.closeDate = dateText(product?.closeDate);
|
||||||
|
flat.delinquencyBucketId = undefined;
|
||||||
|
flat.chargesSummary = (product?.charges ?? []).map((charge: any) => `${charge.name} (${charge.amount})`).join(', ');
|
||||||
|
return { product: flat, template: { accountingMappingOptions: { assetAccountOptions: accounts } } };
|
||||||
|
}
|
||||||
|
|||||||
+19
-12
@@ -61,12 +61,12 @@
|
|||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>Minimum Tenure (months)</mat-label>
|
<mat-label>Minimum Tenure (months)</mat-label>
|
||||||
<input matInput type="number" formControlName="minTenure" [min]="tenureMin" [max]="tenureMax" required />
|
<input matInput type="number" formControlName="minTenure" [min]="tenureMin" [max]="tenureMax" required />
|
||||||
@if (isFdTenureLocked) {
|
@if (tenureBand; as band) {
|
||||||
<mat-hint>FD policy: 12–15 months only</mat-hint>
|
<mat-hint>{{ band.label }} policy: {{ band.min }}–{{ band.max }} months only</mat-hint>
|
||||||
}
|
}
|
||||||
<mat-error>
|
<mat-error>
|
||||||
@if (isFdTenureLocked) {
|
@if (tenureBand; as band) {
|
||||||
Must be between <strong>12 and 15</strong> months
|
Must be between <strong>{{ band.min }} and {{ band.max }}</strong> months
|
||||||
} @else {
|
} @else {
|
||||||
Minimum Tenure is <strong>required</strong>
|
Minimum Tenure is <strong>required</strong>
|
||||||
}
|
}
|
||||||
@@ -75,24 +75,31 @@
|
|||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>Default Tenure (months)</mat-label>
|
<mat-label>Default Tenure (months)</mat-label>
|
||||||
<input matInput type="number" formControlName="defaultTenure" [min]="tenureMin" [max]="tenureMax" />
|
<input matInput type="number" formControlName="defaultTenure" [min]="tenureMin" [max]="tenureMax" />
|
||||||
@if (isFdTenureLocked) {
|
@if (tenureBand; as band) {
|
||||||
<mat-hint>Between 12 and 15</mat-hint>
|
<mat-hint>Between {{ band.min }} and {{ band.max }}</mat-hint>
|
||||||
}
|
}
|
||||||
@if (isFdTenureLocked) {
|
@if (tenureBand; as band) {
|
||||||
<mat-error>Must be between <strong>12 and 15</strong> months</mat-error>
|
<mat-error
|
||||||
|
>Must be between <strong>{{ band.min }} and {{ band.max }}</strong> months</mat-error
|
||||||
|
>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>Maximum Tenure (months)</mat-label>
|
<mat-label>Maximum Tenure (months)</mat-label>
|
||||||
<input matInput type="number" formControlName="maxTenure" [min]="tenureMin" [max]="tenureMax" required />
|
<input matInput type="number" formControlName="maxTenure" [min]="tenureMin" [max]="tenureMax" required />
|
||||||
@if (isFdTenureLocked) {
|
@if (tenureBand; as band) {
|
||||||
<mat-hint>FD policy: max 15 months (also capped at deposit maturity)</mat-hint>
|
<mat-hint>
|
||||||
|
{{ band.label }} policy: max {{ band.max }} months
|
||||||
|
@if (band.label === 'FD') {
|
||||||
|
(also capped at deposit maturity)
|
||||||
|
}
|
||||||
|
</mat-hint>
|
||||||
} @else {
|
} @else {
|
||||||
<mat-hint>Also capped at the deposit's maturity (Lien rule)</mat-hint>
|
<mat-hint>Also capped at the deposit's maturity (Lien rule)</mat-hint>
|
||||||
}
|
}
|
||||||
<mat-error>
|
<mat-error>
|
||||||
@if (isFdTenureLocked) {
|
@if (tenureBand; as band) {
|
||||||
Must be between <strong>12 and 15</strong> months
|
Must be between <strong>{{ band.min }} and {{ band.max }}</strong> months
|
||||||
} @else {
|
} @else {
|
||||||
Maximum Tenure is <strong>required</strong>
|
Maximum Tenure is <strong>required</strong>
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-12
@@ -110,21 +110,28 @@ export class LoanProductAdvanceLimitTenureStepComponent {
|
|||||||
return this.loanProductAdvanceLimitTenureForm.getRawValue();
|
return this.loanProductAdvanceLimitTenureForm.getRawValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True when the FD 12–15 month cap is in force (drives the hints/errors in the template). */
|
/** Bank-policy tenure band per sub type: FD 12 to 15 months, a bullet Gold loan 1 to 12. Others stay open. */
|
||||||
get isFdTenureLocked(): boolean {
|
private readonly tenureBands: Record<string, { label: string; min: number; max: number; defaultTenure: number }> = {
|
||||||
return this._productSubType === 'fd';
|
fd: { label: 'FD', min: 12, max: 15, defaultTenure: 12 },
|
||||||
|
gold: { label: 'Gold', min: 1, max: 12, defaultTenure: 12 },
|
||||||
|
warehouse: { label: 'Warehouse', min: 1, max: 12, defaultTenure: 6 }
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The policy band in force for the chosen sub type, or null when the sub type is open. */
|
||||||
|
get tenureBand(): { label: string; min: number; max: number; defaultTenure: number } | null {
|
||||||
|
return this.tenureBands[this._productSubType] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get tenureMin(): number {
|
get tenureMin(): number {
|
||||||
return this.isFdTenureLocked ? 12 : 1;
|
return this.tenureBand?.min ?? 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
get tenureMax(): number | null {
|
get tenureMax(): number | null {
|
||||||
return this.isFdTenureLocked ? 15 : null;
|
return this.tenureBand?.max ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FD advances are bank-policy capped to 12–15 months; every other sub type stays open (min 1).
|
* FD (12–15 months) and Gold (1–12 months) are bank-policy capped; every other sub type stays open (min 1).
|
||||||
* Called whenever the sub type changes so the validators — and thus the wizard's validity gate —
|
* Called whenever the sub type changes so the validators — and thus the wizard's validity gate —
|
||||||
* match the selected product.
|
* match the selected product.
|
||||||
*/
|
*/
|
||||||
@@ -136,10 +143,11 @@ export class LoanProductAdvanceLimitTenureStepComponent {
|
|||||||
const min = form.get('minTenure');
|
const min = form.get('minTenure');
|
||||||
const def = form.get('defaultTenure');
|
const def = form.get('defaultTenure');
|
||||||
const max = form.get('maxTenure');
|
const max = form.get('maxTenure');
|
||||||
if (this._productSubType === 'fd') {
|
const band = this.tenureBand;
|
||||||
|
if (band) {
|
||||||
const bounds = [
|
const bounds = [
|
||||||
Validators.min(12),
|
Validators.min(band.min),
|
||||||
Validators.max(15)
|
Validators.max(band.max)
|
||||||
];
|
];
|
||||||
min.setValidators([
|
min.setValidators([
|
||||||
Validators.required,
|
Validators.required,
|
||||||
@@ -150,9 +158,9 @@ export class LoanProductAdvanceLimitTenureStepComponent {
|
|||||||
Validators.required,
|
Validators.required,
|
||||||
...bounds
|
...bounds
|
||||||
]);
|
]);
|
||||||
min.setValue(12, { emitEvent: false });
|
min.setValue(band.min, { emitEvent: false });
|
||||||
def.setValue(12, { emitEvent: false });
|
def.setValue(band.defaultTenure, { emitEvent: false });
|
||||||
max.setValue(15, { emitEvent: false });
|
max.setValue(band.max, { emitEvent: false });
|
||||||
} else {
|
} else {
|
||||||
min.setValidators([
|
min.setValidators([
|
||||||
Validators.required,
|
Validators.required,
|
||||||
|
|||||||
+66
@@ -248,6 +248,72 @@ export class LoanProductDetailsStepComponent implements OnInit {
|
|||||||
if (subGroup) {
|
if (subGroup) {
|
||||||
this.loanProductDetailsForm.get('shortName').setValue(subGroup.shortName);
|
this.loanProductDetailsForm.get('shortName').setValue(subGroup.shortName);
|
||||||
}
|
}
|
||||||
|
this.applyCategoryForSubType(subGroupId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The category sets the Priority Sector and CIBIL defaults. A change the officer makes here reaches this
|
||||||
|
// subscription; the sub type's own default (above) calls applyCategoryDefaults directly.
|
||||||
|
this.loanProductDetailsForm.get('loanProductCategory')?.valueChanges.subscribe((category: string) => {
|
||||||
|
this.applyCategoryDefaults(category);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sub type -> the category that normally fits it. Sub types not listed leave the category as it is. */
|
||||||
|
private readonly categoryBySubType: Record<string, string> = {
|
||||||
|
fd: 'advances_deposits',
|
||||||
|
rd: 'advances_deposits',
|
||||||
|
dds: 'advances_deposits',
|
||||||
|
lic: 'advances_deposits',
|
||||||
|
gold: 'gold_loan',
|
||||||
|
warehouse: 'agriculture',
|
||||||
|
// Loans A/C (EMI)
|
||||||
|
term: 'other',
|
||||||
|
personal: 'personal',
|
||||||
|
housing: 'housing',
|
||||||
|
vehicle: 'personal',
|
||||||
|
education: 'personal',
|
||||||
|
business: 'msme',
|
||||||
|
gold_emi: 'gold_loan'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category -> Priority Sector and CIBIL account type that normally go with it (RBI sector classification).
|
||||||
|
* Defaults only: the officer can still pick another value for either.
|
||||||
|
*/
|
||||||
|
private readonly defaultsByCategory: Record<string, { prioritySector: string; cibilAccountType: string }> = {
|
||||||
|
agriculture: { prioritySector: 'yes', cibilAccountType: 'other' },
|
||||||
|
msme: { prioritySector: 'yes', cibilAccountType: 'business_loan' },
|
||||||
|
personal: { prioritySector: 'no', cibilAccountType: 'other' },
|
||||||
|
housing: { prioritySector: 'no', cibilAccountType: 'other' },
|
||||||
|
advances_deposits: { prioritySector: 'no', cibilAccountType: 'loan_bank_deposits' },
|
||||||
|
gold_loan: { prioritySector: 'no', cibilAccountType: 'gold_loan' },
|
||||||
|
other: { prioritySector: 'no', cibilAccountType: 'other' }
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Picking a sub type sets its usual category (unless the officer chose one), and the category's defaults follow. */
|
||||||
|
private applyCategoryForSubType(subGroupId: string): void {
|
||||||
|
const category = this.categoryBySubType[subGroupId];
|
||||||
|
if (!category) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const control = this.loanProductDetailsForm.get('loanProductCategory');
|
||||||
|
if (control && !control.dirty) {
|
||||||
|
control.setValue(category, { emitEvent: false });
|
||||||
|
this.applyCategoryDefaults(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets Priority Sector and CIBIL type for a category, leaving alone any the officer has already chosen. */
|
||||||
|
private applyCategoryDefaults(category: string): void {
|
||||||
|
const fit = this.defaultsByCategory[category];
|
||||||
|
if (!fit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(Object.keys(fit) as (keyof typeof fit)[]).forEach((name) => {
|
||||||
|
const control = this.loanProductDetailsForm.get(name);
|
||||||
|
if (control && !control.dirty) {
|
||||||
|
control.setValue(fit[name], { emitEvent: false });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+36
-5
@@ -44,18 +44,38 @@
|
|||||||
<div class="adv-grid adv-grid--3">
|
<div class="adv-grid adv-grid--3">
|
||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>Minimum Tenure (months)</mat-label>
|
<mat-label>Minimum Tenure (months)</mat-label>
|
||||||
<input matInput type="number" formControlName="minTenure" min="1" required />
|
<input matInput type="number" formControlName="minTenure" [min]="band?.minTenure ?? 1" required />
|
||||||
<mat-error>Minimum Tenure is <strong>required</strong></mat-error>
|
@if (band; as b) {
|
||||||
|
<mat-hint>{{ b.label }}: {{ b.minTenure }}–{{ b.maxTenure }} months</mat-hint>
|
||||||
|
}
|
||||||
|
<mat-error>
|
||||||
|
@if (band; as b) {
|
||||||
|
Must be between <strong>{{ b.minTenure }} and {{ b.maxTenure }}</strong> months
|
||||||
|
} @else {
|
||||||
|
Minimum Tenure is <strong>required</strong>
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>Default Tenure (months)</mat-label>
|
<mat-label>Default Tenure (months)</mat-label>
|
||||||
<input matInput type="number" formControlName="defaultTenure" min="1" />
|
<input matInput type="number" formControlName="defaultTenure" [min]="band?.minTenure ?? 1" />
|
||||||
|
@if (band; as b) {
|
||||||
|
<mat-error
|
||||||
|
>Must be between <strong>{{ b.minTenure }} and {{ b.maxTenure }}</strong> months</mat-error
|
||||||
|
>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>Maximum Tenure (months)</mat-label>
|
<mat-label>Maximum Tenure (months)</mat-label>
|
||||||
<input matInput type="number" formControlName="maxTenure" min="1" required />
|
<input matInput type="number" formControlName="maxTenure" [min]="band?.minTenure ?? 1" required />
|
||||||
<mat-hint>Age at maturity cap comes from Borrower Eligibility below</mat-hint>
|
<mat-hint>Age at maturity cap comes from Borrower Eligibility below</mat-hint>
|
||||||
<mat-error>Maximum Tenure is <strong>required</strong></mat-error>
|
<mat-error>
|
||||||
|
@if (band; as b) {
|
||||||
|
Must be between <strong>{{ b.minTenure }} and {{ b.maxTenure }}</strong> months
|
||||||
|
} @else {
|
||||||
|
Maximum Tenure is <strong>required</strong>
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,6 +110,17 @@
|
|||||||
<mat-label>Maximum Loans per Customer</mat-label>
|
<mat-label>Maximum Loans per Customer</mat-label>
|
||||||
<input matInput type="number" formControlName="maxLoansPerCustomer" min="1" />
|
<input matInput type="number" formControlName="maxLoansPerCustomer" min="1" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
|
<mat-label>Income Multiple (× net monthly salary)</mat-label>
|
||||||
|
<input matInput type="number" formControlName="incomeMultiple" min="0" step="0.5" />
|
||||||
|
<mat-hint>Loan up to this many times the net salary. Policy: personal loan 10×</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="adv-subsection">
|
||||||
|
<div class="adv-checks">
|
||||||
|
<mat-checkbox formControlName="memberRequired">Applicant must be a member of the bank</mat-checkbox>
|
||||||
|
<mat-checkbox formControlName="guarantorMustBeMember">Guarantors must be members of the bank</mat-checkbox>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+149
-1
@@ -6,12 +6,70 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, inject } from '@angular/core';
|
import { Component, Input, inject } from '@angular/core';
|
||||||
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
|
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
|
||||||
import { MatCheckbox } from '@angular/material/checkbox';
|
import { MatCheckbox } from '@angular/material/checkbox';
|
||||||
import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
|
import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tenure band (months) and borrower age limits for a Loans A/C sub type, taken from the bank's loan policy: short term
|
||||||
|
* up to 15 months, medium 15 to 84, long above 84, and never more than 120. Housing runs 7 to 10 years, education is
|
||||||
|
* repaid in 84 equal instalments, vehicle 3 to 7 years, personal 3 to 5 years.
|
||||||
|
*/
|
||||||
|
interface LoanBand {
|
||||||
|
label: string;
|
||||||
|
minTenure: number;
|
||||||
|
defaultTenure: number;
|
||||||
|
maxTenure: number;
|
||||||
|
minAge: number;
|
||||||
|
maxAgeAtMaturity?: number;
|
||||||
|
/** Loan = this many times the net monthly salary (policy: personal loan 10x). */
|
||||||
|
incomeMultiple?: number;
|
||||||
|
/** The guarantors must themselves be members of the bank (policy: personal loan). */
|
||||||
|
guarantorMustBeMember?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOAN_BANDS: Record<string, LoanBand> = {
|
||||||
|
personal: {
|
||||||
|
label: 'Personal Loan',
|
||||||
|
minTenure: 6,
|
||||||
|
defaultTenure: 36,
|
||||||
|
maxTenure: 60,
|
||||||
|
minAge: 21,
|
||||||
|
maxAgeAtMaturity: 60,
|
||||||
|
incomeMultiple: 10,
|
||||||
|
guarantorMustBeMember: true
|
||||||
|
},
|
||||||
|
housing: {
|
||||||
|
label: 'Housing Loan',
|
||||||
|
minTenure: 12,
|
||||||
|
defaultTenure: 84,
|
||||||
|
maxTenure: 120,
|
||||||
|
minAge: 21,
|
||||||
|
maxAgeAtMaturity: 70
|
||||||
|
},
|
||||||
|
vehicle: { label: 'Vehicle Loan', minTenure: 12, defaultTenure: 60, maxTenure: 84, minAge: 21, maxAgeAtMaturity: 65 },
|
||||||
|
education: { label: 'Education Loan', minTenure: 12, defaultTenure: 84, maxTenure: 84, minAge: 18 },
|
||||||
|
business: {
|
||||||
|
label: 'Business / MSME Loan',
|
||||||
|
minTenure: 12,
|
||||||
|
defaultTenure: 60,
|
||||||
|
maxTenure: 120,
|
||||||
|
minAge: 21,
|
||||||
|
maxAgeAtMaturity: 65
|
||||||
|
},
|
||||||
|
gold_emi: {
|
||||||
|
label: 'Gold EMI Loan',
|
||||||
|
minTenure: 6,
|
||||||
|
defaultTenure: 12,
|
||||||
|
maxTenure: 36,
|
||||||
|
minAge: 21,
|
||||||
|
maxAgeAtMaturity: 70
|
||||||
|
},
|
||||||
|
term: { label: 'Term Loan', minTenure: 12, defaultTenure: 60, maxTenure: 120, minAge: 21, maxAgeAtMaturity: 65 }
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Amount & Tenure step for a Loans A/C (EMI) product (step 4): fixed sanctioned amount, tenure,
|
* Amount & Tenure step for a Loans A/C (EMI) product (step 4): fixed sanctioned amount, tenure,
|
||||||
* borrower eligibility and disbursement. Amount, tenure and tranches feed Fineract's core product
|
* borrower eligibility and disbursement. Amount, tenure and tranches feed Fineract's core product
|
||||||
@@ -33,6 +91,27 @@ export class LoanProductLoanAmountTenureStepComponent {
|
|||||||
|
|
||||||
loanProductLoanAmountTenureForm: UntypedFormGroup;
|
loanProductLoanAmountTenureForm: UntypedFormGroup;
|
||||||
|
|
||||||
|
private _productSubType = '';
|
||||||
|
|
||||||
|
/** The sub type picked on the Details step: it sets the tenure band the three tenure fields must stay inside. */
|
||||||
|
@Input()
|
||||||
|
set productSubType(value: string) {
|
||||||
|
const next = value || '';
|
||||||
|
if (next === this._productSubType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._productSubType = next;
|
||||||
|
this.applyBand();
|
||||||
|
}
|
||||||
|
get productSubType(): string {
|
||||||
|
return this._productSubType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The band in force for the chosen sub type, or null for a sub type without one. */
|
||||||
|
get band(): LoanBand | null {
|
||||||
|
return LOAN_BANDS[this._productSubType] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
readonly disbursementModeOptions = [
|
readonly disbursementModeOptions = [
|
||||||
{ id: 'transfer_dd_vendor', label: 'Transfer to A/C, DD, Direct to vendor' },
|
{ id: 'transfer_dd_vendor', label: 'Transfer to A/C, DD, Direct to vendor' },
|
||||||
{ id: 'transfer', label: 'Transfer to A/C only' },
|
{ id: 'transfer', label: 'Transfer to A/C only' },
|
||||||
@@ -85,6 +164,13 @@ export class LoanProductLoanAmountTenureStepComponent {
|
|||||||
minMonthlyIncome: [''],
|
minMonthlyIncome: [''],
|
||||||
maxFoir: [50],
|
maxFoir: [50],
|
||||||
maxLoansPerCustomer: [1],
|
maxLoansPerCustomer: [1],
|
||||||
|
// Policy: loans go to members, so the applicant must be a member; a personal loan also needs member guarantors.
|
||||||
|
memberRequired: [true],
|
||||||
|
guarantorMustBeMember: [false],
|
||||||
|
incomeMultiple: [
|
||||||
|
'',
|
||||||
|
Validators.min(0)
|
||||||
|
],
|
||||||
disbursementType: ['single'],
|
disbursementType: ['single'],
|
||||||
maxTranches: [
|
maxTranches: [
|
||||||
2,
|
2,
|
||||||
@@ -96,6 +182,68 @@ export class LoanProductLoanAmountTenureStepComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sets the tenure limits and the age defaults for the sub type; a value the officer has typed is left alone. */
|
||||||
|
private applyBand(): void {
|
||||||
|
const form = this.loanProductLoanAmountTenureForm;
|
||||||
|
const band = this.band;
|
||||||
|
if (!form || !band) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tenureControls: [string, number][] = [
|
||||||
|
[
|
||||||
|
'minTenure',
|
||||||
|
band.minTenure
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'defaultTenure',
|
||||||
|
band.defaultTenure
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'maxTenure',
|
||||||
|
band.maxTenure
|
||||||
|
]
|
||||||
|
];
|
||||||
|
const bounds = [
|
||||||
|
Validators.min(band.minTenure),
|
||||||
|
Validators.max(band.maxTenure)
|
||||||
|
];
|
||||||
|
tenureControls.forEach(
|
||||||
|
([
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
]) => {
|
||||||
|
const control = form.get(name);
|
||||||
|
control.setValidators(
|
||||||
|
name === 'defaultTenure' ? bounds : [
|
||||||
|
Validators.required,
|
||||||
|
...bounds
|
||||||
|
]
|
||||||
|
);
|
||||||
|
if (!control.dirty) {
|
||||||
|
control.setValue(value, { emitEvent: false });
|
||||||
|
}
|
||||||
|
control.updateValueAndValidity({ emitEvent: false });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const incomeMultiple = form.get('incomeMultiple');
|
||||||
|
if (!incomeMultiple.dirty) {
|
||||||
|
incomeMultiple.setValue(band.incomeMultiple ?? '', { emitEvent: false });
|
||||||
|
}
|
||||||
|
const guarantorMember = form.get('guarantorMustBeMember');
|
||||||
|
if (!guarantorMember.dirty) {
|
||||||
|
guarantorMember.setValue(band.guarantorMustBeMember ?? false, { emitEvent: false });
|
||||||
|
}
|
||||||
|
const minAge = form.get('minAge');
|
||||||
|
if (!minAge.dirty) {
|
||||||
|
minAge.setValue(band.minAge, { emitEvent: false });
|
||||||
|
}
|
||||||
|
const maxAge = form.get('maxAgeAtMaturity');
|
||||||
|
// A sub type without its own limit goes back to the general 70, not the previous sub type's value.
|
||||||
|
if (!maxAge.dirty) {
|
||||||
|
maxAge.setValue(band.maxAgeAtMaturity ?? 70, { emitEvent: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get isMultiTranche(): boolean {
|
get isMultiTranche(): boolean {
|
||||||
return this.loanProductLoanAmountTenureForm.get('disbursementType').value === 'multiple';
|
return this.loanProductLoanAmountTenureForm.get('disbursementType').value === 'multiple';
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -63,8 +63,16 @@
|
|||||||
<div class="adv-grid">
|
<div class="adv-grid">
|
||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>Individual · floating rate (%)</mat-label>
|
<mat-label>Individual · floating rate (%)</mat-label>
|
||||||
<input matInput type="number" formControlName="foreclosureFloatingIndividual" min="0" max="100" step="0.01" />
|
<input
|
||||||
<mat-hint>Nil as per RBI</mat-hint>
|
matInput
|
||||||
|
type="number"
|
||||||
|
formControlName="foreclosureFloatingIndividual"
|
||||||
|
min="0"
|
||||||
|
max="0"
|
||||||
|
step="0.01"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<mat-hint>Nil as per RBI. Cannot be changed</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>Individual · fixed rate (%)</mat-label>
|
<mat-label>Individual · fixed rate (%)</mat-label>
|
||||||
@@ -132,7 +140,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="adv-subsection">
|
<div class="adv-subsection">
|
||||||
<div class="adv-checks">
|
<div class="adv-checks">
|
||||||
<mat-checkbox formControlName="releaseCollateralOnClosure">Release collateral and lien on closure</mat-checkbox>
|
|
||||||
<mat-checkbox formControlName="topUpAllowed">Top-up loan on the same account</mat-checkbox>
|
<mat-checkbox formControlName="topUpAllowed">Top-up loan on the same account</mat-checkbox>
|
||||||
<mat-checkbox formControlName="balanceTransferOutAllowed">Allow balance transfer (takeover) out</mat-checkbox>
|
<mat-checkbox formControlName="balanceTransferOutAllowed">Allow balance transfer (takeover) out</mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+5
-2
@@ -77,9 +77,13 @@ export class LoanProductLoanPrepaymentStepComponent {
|
|||||||
allowedOn: ['emi_date'],
|
allowedOn: ['emi_date'],
|
||||||
partPaymentsPerYear: ['unlimited'],
|
partPaymentsPerYear: ['unlimited'],
|
||||||
partPrepaymentMode: ['reduce_tenure'],
|
partPrepaymentMode: ['reduce_tenure'],
|
||||||
|
// RBI: no foreclosure / pre-payment charge on a floating-rate loan to an individual, so this stays at nil.
|
||||||
foreclosureFloatingIndividual: [
|
foreclosureFloatingIndividual: [
|
||||||
0,
|
0,
|
||||||
percent
|
[
|
||||||
|
Validators.min(0),
|
||||||
|
Validators.max(0)
|
||||||
|
]
|
||||||
],
|
],
|
||||||
foreclosureFixedIndividual: [
|
foreclosureFixedIndividual: [
|
||||||
2,
|
2,
|
||||||
@@ -105,7 +109,6 @@ export class LoanProductLoanPrepaymentStepComponent {
|
|||||||
autoCloseOnZeroBalance: ['YES'],
|
autoCloseOnZeroBalance: ['YES'],
|
||||||
nocLetter: ['auto'],
|
nocLetter: ['auto'],
|
||||||
restructuring: ['approval'],
|
restructuring: ['approval'],
|
||||||
releaseCollateralOnClosure: [true],
|
|
||||||
topUpAllowed: [false],
|
topUpAllowed: [false],
|
||||||
balanceTransferOutAllowed: [true]
|
balanceTransferOutAllowed: [true]
|
||||||
});
|
});
|
||||||
|
|||||||
+19
-13
@@ -49,6 +49,7 @@
|
|||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>Minimum Days to First Repayment</mat-label>
|
<mat-label>Minimum Days to First Repayment</mat-label>
|
||||||
<input matInput type="number" formControlName="minDaysToFirstRepayment" min="0" />
|
<input matInput type="number" formControlName="minDaysToFirstRepayment" min="0" />
|
||||||
|
<mat-hint>Policy: instalments start from the 2nd month after the loan month</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>EMI Due Day</mat-label>
|
<mat-label>EMI Due Day</mat-label>
|
||||||
@@ -69,6 +70,7 @@
|
|||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>Moratorium / Grace (months)</mat-label>
|
<mat-label>Moratorium / Grace (months)</mat-label>
|
||||||
<input matInput type="number" formControlName="moratoriumMonths" min="0" />
|
<input matInput type="number" formControlName="moratoriumMonths" min="0" />
|
||||||
|
<mat-hint>Interest and EMI fall due only after the moratorium ends (RBI)</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>Interest during Moratorium</mat-label>
|
<mat-label>Interest during Moratorium</mat-label>
|
||||||
@@ -98,14 +100,16 @@
|
|||||||
}
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="adv-field">
|
@if (isAutoDebit) {
|
||||||
<mat-label>Linked Account Type</mat-label>
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-select formControlName="linkedAccountType">
|
<mat-label>Linked Account Type</mat-label>
|
||||||
@for (opt of linkedAccountOptions; track opt.id) {
|
<mat-select formControlName="linkedAccountType">
|
||||||
<mat-option [value]="opt.id">{{ opt.label }}</mat-option>
|
@for (opt of linkedAccountOptions; track opt.id) {
|
||||||
}
|
<mat-option [value]="opt.id">{{ opt.label }}</mat-option>
|
||||||
</mat-select>
|
}
|
||||||
</mat-form-field>
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>PDC Collection</mat-label>
|
<mat-label>PDC Collection</mat-label>
|
||||||
<mat-select formControlName="pdcCollection">
|
<mat-select formControlName="pdcCollection">
|
||||||
@@ -115,11 +119,13 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="adv-subsection">
|
@if (isAutoDebit) {
|
||||||
<mat-checkbox formControlName="mandateRequired">
|
<div class="adv-subsection">
|
||||||
Mandate registration is required before the first disbursement
|
<mat-checkbox formControlName="mandateRequired">
|
||||||
</mat-checkbox>
|
Mandate registration is required before the first disbursement
|
||||||
</div>
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div style="padding: 0 24px 20px">
|
<div style="padding: 0 24px 20px">
|
||||||
<div class="adv-info">
|
<div class="adv-info">
|
||||||
Cheques are used here to repay the EMI as post-dated cheques, not to withdraw from the loan. A cheque book is
|
Cheques are used here to repay the EMI as post-dated cheques, not to withdraw from the loan. A cheque book is
|
||||||
|
|||||||
+42
-6
@@ -6,7 +6,7 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
|
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
|
||||||
import { MatCheckbox } from '@angular/material/checkbox';
|
import { MatCheckbox } from '@angular/material/checkbox';
|
||||||
import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
|
import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
|
||||||
@@ -28,7 +28,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|||||||
MatRadioButton
|
MatRadioButton
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class LoanProductLoanRepaymentStepComponent {
|
export class LoanProductLoanRepaymentStepComponent implements OnInit {
|
||||||
private formBuilder = inject(UntypedFormBuilder);
|
private formBuilder = inject(UntypedFormBuilder);
|
||||||
|
|
||||||
loanProductLoanRepaymentForm: UntypedFormGroup;
|
loanProductLoanRepaymentForm: UntypedFormGroup;
|
||||||
@@ -50,7 +50,7 @@ export class LoanProductLoanRepaymentStepComponent {
|
|||||||
|
|
||||||
readonly emiDueDayOptions = [
|
readonly emiDueDayOptions = [
|
||||||
{ id: 'disbursement_day', label: 'Same day as disbursement' },
|
{ id: 'disbursement_day', label: 'Same day as disbursement' },
|
||||||
{ id: 'fixed_day', label: 'Fixed day of month (5th)' }
|
{ id: 'fixed_day', label: 'Fixed day of the month (day set on each account)' }
|
||||||
];
|
];
|
||||||
|
|
||||||
readonly holidayRuleOptions = [
|
readonly holidayRuleOptions = [
|
||||||
@@ -114,16 +114,52 @@ export class LoanProductLoanRepaymentStepComponent {
|
|||||||
],
|
],
|
||||||
moratoriumInterest: ['pre_emi'],
|
moratoriumInterest: ['pre_emi'],
|
||||||
variableInstallments: [false],
|
variableInstallments: [false],
|
||||||
repaymentMode: ['nach'],
|
// Loan policy: cheques for the instalments are taken together in advance (quarterly), so cheques are the default.
|
||||||
|
repaymentMode: ['pdc'],
|
||||||
linkedAccountType: ['savings_current'],
|
linkedAccountType: ['savings_current'],
|
||||||
pdcCollection: ['mandatory'],
|
pdcCollection: ['mandatory'],
|
||||||
mandateRequired: [true],
|
mandateRequired: [false],
|
||||||
appropriationOrder: ['charges_penal_interest_principal'],
|
appropriationOrder: ['charges_penal_interest_principal'],
|
||||||
advanceEmi: ['adjust_next']
|
advanceEmi: ['adjust_next']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// The mode decides how the EMI is collected, so it sets the cheque and mandate defaults that go with it.
|
||||||
|
this.loanProductLoanRepaymentForm
|
||||||
|
.get('repaymentMode')
|
||||||
|
.valueChanges.subscribe((mode: string) => this.applyMode(mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyMode(mode: string): void {
|
||||||
|
const defaults: Record<string, { pdcCollection: string; mandateRequired: boolean }> = {
|
||||||
|
nach: { pdcCollection: 'optional', mandateRequired: true },
|
||||||
|
pdc: { pdcCollection: 'mandatory', mandateRequired: false },
|
||||||
|
cash: { pdcCollection: 'none', mandateRequired: false }
|
||||||
|
};
|
||||||
|
const fit = defaults[mode];
|
||||||
|
if (!fit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(Object.keys(fit) as (keyof typeof fit)[]).forEach((name) => {
|
||||||
|
const control = this.loanProductLoanRepaymentForm.get(name);
|
||||||
|
if (control && !control.dirty) {
|
||||||
|
control.setValue(fit[name], { emitEvent: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Auto-debit needs a linked account and a registered mandate; the other modes do not. */
|
||||||
|
get isAutoDebit(): boolean {
|
||||||
|
return this.loanProductLoanRepaymentForm.get('repaymentMode').value === 'nach';
|
||||||
|
}
|
||||||
|
|
||||||
get loanProductLoanRepayment() {
|
get loanProductLoanRepayment() {
|
||||||
return this.loanProductLoanRepaymentForm.getRawValue();
|
const value = this.loanProductLoanRepaymentForm.getRawValue();
|
||||||
|
// A mandate only exists for auto-debit; never save it as required for cheques or cash.
|
||||||
|
if (!this.isAutoDebit) {
|
||||||
|
value.mandateRequired = false;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-54
@@ -28,6 +28,9 @@
|
|||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-label>Margin (%)</mat-label>
|
<mat-label>Margin (%)</mat-label>
|
||||||
<input matInput type="number" formControlName="margin" min="0" max="100" step="0.01" required />
|
<input matInput type="number" formControlName="margin" min="0" max="100" step="0.01" required />
|
||||||
|
@if (marginHint) {
|
||||||
|
<mat-hint>{{ marginHint }}</mat-hint>
|
||||||
|
}
|
||||||
<mat-error>Margin must be between <strong>0 and 100</strong></mat-error>
|
<mat-error>Margin must be between <strong>0 and 100</strong></mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="adv-field">
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
@@ -38,40 +41,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="adv-section">
|
|
||||||
<div class="adv-section__header">
|
|
||||||
<i class="fas fa-table"></i>
|
|
||||||
<h3>Collateral Rules by Security Type</h3>
|
|
||||||
<span class="adv-section__hint">Which security this product may accept</span>
|
|
||||||
</div>
|
|
||||||
<table class="adv-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Security</th>
|
|
||||||
<th>Allowed</th>
|
|
||||||
<th>Max LTV %</th>
|
|
||||||
<th>Valuation</th>
|
|
||||||
<th>Insurance</th>
|
|
||||||
<th>Charge / lien</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (rule of collateralRules; track rule.security) {
|
|
||||||
<tr>
|
|
||||||
<td>{{ rule.security }}</td>
|
|
||||||
<td [class.adv-table__yes]="rule.allowed" [class.adv-table__no]="!rule.allowed">
|
|
||||||
{{ rule.allowed ? 'Yes' : 'No' }}
|
|
||||||
</td>
|
|
||||||
<td [class.adv-table__no]="!rule.allowed">{{ rule.ltv }}</td>
|
|
||||||
<td [class.adv-table__no]="!rule.allowed">{{ rule.valuation }}</td>
|
|
||||||
<td [class.adv-table__no]="!rule.allowed">{{ rule.insurance }}</td>
|
|
||||||
<td [class.adv-table__no]="!rule.allowed">{{ rule.charge }}</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="adv-section">
|
<div class="adv-section">
|
||||||
<div class="adv-section__header">
|
<div class="adv-section__header">
|
||||||
<i class="fas fa-user-plus"></i>
|
<i class="fas fa-user-plus"></i>
|
||||||
@@ -90,14 +59,16 @@
|
|||||||
}
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="adv-field">
|
@if (isSecured) {
|
||||||
<mat-label>Third-party Security</mat-label>
|
<mat-form-field appearance="outline" class="adv-field">
|
||||||
<mat-select formControlName="thirdPartySecurity">
|
<mat-label>Third-party Security</mat-label>
|
||||||
@for (opt of thirdPartyOptions; track opt.id) {
|
<mat-select formControlName="thirdPartySecurity">
|
||||||
<mat-option [value]="opt.id">{{ opt.label }}</mat-option>
|
@for (opt of thirdPartyOptions; track opt.id) {
|
||||||
}
|
<mat-option [value]="opt.id">{{ opt.label }}</mat-option>
|
||||||
</mat-select>
|
}
|
||||||
</mat-form-field>
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -107,18 +78,15 @@
|
|||||||
<h3>Conditions</h3>
|
<h3>Conditions</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="adv-subsection">
|
<div class="adv-subsection">
|
||||||
<div class="adv-checks">
|
@if (shownConditions.length > 0) {
|
||||||
<mat-checkbox formControlName="legalOpinionRequired">
|
<div class="adv-checks">
|
||||||
Legal opinion / title search mandatory before sanction
|
@for (rule of shownConditions; track rule.control) {
|
||||||
</mat-checkbox>
|
<mat-checkbox [formControlName]="rule.control">{{ rule.label }}</mat-checkbox>
|
||||||
<mat-checkbox formControlName="propertyInsuranceRequired">
|
}
|
||||||
Property insurance mandatory for the whole tenure
|
</div>
|
||||||
</mat-checkbox>
|
} @else {
|
||||||
<mat-checkbox formControlName="registerCharge">Register charge (CERSAI) before disbursement</mat-checkbox>
|
<div class="adv-info">This loan is unsecured, so it carries no security conditions.</div>
|
||||||
<mat-checkbox formControlName="autoReleaseOnClosure">Release security automatically on closure</mat-checkbox>
|
}
|
||||||
<mat-checkbox formControlName="allowMultipleSecurities">Allow multiple securities for one loan</mat-checkbox>
|
|
||||||
<mat-checkbox formControlName="allowThirdPartyDeposit">Allow third-party deposit as security</mat-checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 0 24px 20px">
|
<div style="padding: 0 24px 20px">
|
||||||
<div class="adv-info">
|
<div class="adv-info">
|
||||||
|
|||||||
+181
-46
@@ -16,27 +16,139 @@ interface SecurityProfile {
|
|||||||
securityMandatory: 'yes' | 'no';
|
securityMandatory: 'yes' | 'no';
|
||||||
margin: number;
|
margin: number;
|
||||||
guarantorsRequired: number;
|
guarantorsRequired: number;
|
||||||
|
coApplicant: string;
|
||||||
|
thirdPartySecurity: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One condition a sub type's security carries: the checkbox it controls, its wording, and whether it starts ticked. */
|
||||||
|
interface ConditionRule {
|
||||||
|
control: string;
|
||||||
|
label: string;
|
||||||
|
defaultOn: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every condition checkbox the form holds. One not listed for a sub type does not apply to it and is saved as off. */
|
||||||
|
const CONDITION_CONTROLS = [
|
||||||
|
'legalOpinionRequired',
|
||||||
|
'propertyInsuranceRequired',
|
||||||
|
'registerCharge',
|
||||||
|
'autoReleaseOnClosure',
|
||||||
|
'allowMultipleSecurities',
|
||||||
|
'allowThirdPartyDeposit'
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Immovable property (Housing, Business, Term): title check, property bima, CERSAI charge. */
|
||||||
|
const PROPERTY_CONDITIONS: ConditionRule[] = [
|
||||||
|
{ control: 'legalOpinionRequired', label: 'Legal opinion / title search mandatory before sanction', defaultOn: true },
|
||||||
|
{ control: 'propertyInsuranceRequired', label: 'Property insurance mandatory for the whole tenure', defaultOn: true },
|
||||||
|
{ control: 'registerCharge', label: 'Register charge (CERSAI) before disbursement', defaultOn: true },
|
||||||
|
{ control: 'autoReleaseOnClosure', label: 'Release the mortgage automatically on closure', defaultOn: true },
|
||||||
|
{ control: 'allowMultipleSecurities', label: 'Allow multiple securities for one loan', defaultOn: false }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Vehicle: the vehicle is hypothecated, insured, and the lien is noted on the RC book. */
|
||||||
|
const VEHICLE_CONDITIONS: ConditionRule[] = [
|
||||||
|
{ control: 'propertyInsuranceRequired', label: 'Vehicle insurance mandatory for the whole tenure', defaultOn: true },
|
||||||
|
{ control: 'registerCharge', label: 'Note the hypothecation on the RC book before disbursement', defaultOn: true },
|
||||||
|
{ control: 'autoReleaseOnClosure', label: 'Release the hypothecation automatically on closure', defaultOn: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Gold EMI: the gold is pledged and insured, and handed back on closure. No registered charge. */
|
||||||
|
const GOLD_CONDITIONS: ConditionRule[] = [
|
||||||
|
{ control: 'propertyInsuranceRequired', label: 'Gold insurance mandatory for the whole tenure', defaultOn: true },
|
||||||
|
{ control: 'autoReleaseOnClosure', label: 'Return the gold automatically on closure', defaultOn: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Wording used when a sub type has no list of its own but the officer made security mandatory. */
|
||||||
|
const GENERIC_CONDITIONS: ConditionRule[] = [
|
||||||
|
{
|
||||||
|
control: 'legalOpinionRequired',
|
||||||
|
label: 'Legal opinion / title search mandatory before sanction',
|
||||||
|
defaultOn: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: 'propertyInsuranceRequired',
|
||||||
|
label: 'Security insurance mandatory for the whole tenure',
|
||||||
|
defaultOn: false
|
||||||
|
},
|
||||||
|
{ control: 'registerCharge', label: 'Register the charge on the security before disbursement', defaultOn: false },
|
||||||
|
{ control: 'autoReleaseOnClosure', label: 'Release the security automatically on closure', defaultOn: false },
|
||||||
|
{ control: 'allowMultipleSecurities', label: 'Allow multiple securities for one loan', defaultOn: false },
|
||||||
|
{ control: 'allowThirdPartyDeposit', label: 'Allow third-party deposit as security', defaultOn: false }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Personal and Education are unsecured, so they carry no security conditions unless security is made mandatory. */
|
||||||
|
const CONDITIONS_BY_SUB_TYPE: Record<string, ConditionRule[]> = {
|
||||||
|
housing: PROPERTY_CONDITIONS,
|
||||||
|
business: PROPERTY_CONDITIONS,
|
||||||
|
term: PROPERTY_CONDITIONS,
|
||||||
|
vehicle: VEHICLE_CONDITIONS,
|
||||||
|
gold_emi: GOLD_CONDITIONS,
|
||||||
|
personal: [],
|
||||||
|
education: PROPERTY_CONDITIONS
|
||||||
|
};
|
||||||
|
|
||||||
/** Sensible defaults by Loans A/C sub type — the officer can still override every field. */
|
/** Sensible defaults by Loans A/C sub type — the officer can still override every field. */
|
||||||
const SECURITY_PROFILES: Record<string, SecurityProfile> = {
|
const SECURITY_PROFILES: Record<string, SecurityProfile> = {
|
||||||
personal: { securityType: 'None (unsecured)', securityMandatory: 'no', margin: 0, guarantorsRequired: 0 },
|
// Policy: a personal loan takes two guarantors who are bank members.
|
||||||
education: { securityType: 'None (unsecured)', securityMandatory: 'no', margin: 0, guarantorsRequired: 0 },
|
personal: {
|
||||||
|
securityType: 'None (unsecured)',
|
||||||
|
securityMandatory: 'no',
|
||||||
|
margin: 0,
|
||||||
|
guarantorsRequired: 2,
|
||||||
|
coApplicant: 'optional',
|
||||||
|
thirdPartySecurity: 'no'
|
||||||
|
},
|
||||||
|
// Policy: 90% of the expense is lent (the student bears 10%), against a house / plot / NSC worth double the loan,
|
||||||
|
// with the parent or guardian applying jointly.
|
||||||
|
education: {
|
||||||
|
securityType: 'Property / NSC (double the loan value)',
|
||||||
|
securityMandatory: 'yes',
|
||||||
|
margin: 10,
|
||||||
|
guarantorsRequired: 1,
|
||||||
|
coApplicant: 'always',
|
||||||
|
thirdPartySecurity: 'consent'
|
||||||
|
},
|
||||||
housing: {
|
housing: {
|
||||||
securityType: 'Immovable property (mortgage)',
|
securityType: 'Immovable property (mortgage)',
|
||||||
securityMandatory: 'yes',
|
securityMandatory: 'yes',
|
||||||
|
margin: 25,
|
||||||
|
guarantorsRequired: 1,
|
||||||
|
coApplicant: 'joint_property',
|
||||||
|
thirdPartySecurity: 'consent'
|
||||||
|
},
|
||||||
|
vehicle: {
|
||||||
|
securityType: 'Vehicle (hypothecation)',
|
||||||
|
securityMandatory: 'yes',
|
||||||
margin: 20,
|
margin: 20,
|
||||||
guarantorsRequired: 1
|
guarantorsRequired: 0,
|
||||||
|
coApplicant: 'optional',
|
||||||
|
thirdPartySecurity: 'no'
|
||||||
},
|
},
|
||||||
vehicle: { securityType: 'Vehicle (hypothecation)', securityMandatory: 'yes', margin: 15, guarantorsRequired: 0 },
|
|
||||||
business: {
|
business: {
|
||||||
securityType: 'Immovable property / business assets',
|
securityType: 'Immovable property / business assets',
|
||||||
securityMandatory: 'yes',
|
securityMandatory: 'yes',
|
||||||
margin: 25,
|
margin: 25,
|
||||||
guarantorsRequired: 1
|
guarantorsRequired: 1,
|
||||||
|
coApplicant: 'joint_property',
|
||||||
|
thirdPartySecurity: 'consent'
|
||||||
},
|
},
|
||||||
gold_emi: { securityType: 'Gold ornaments (pledge)', securityMandatory: 'yes', margin: 25, guarantorsRequired: 0 },
|
gold_emi: {
|
||||||
term: { securityType: 'Immovable property (mortgage)', securityMandatory: 'yes', margin: 20, guarantorsRequired: 1 }
|
securityType: 'Gold ornaments (pledge)',
|
||||||
|
securityMandatory: 'yes',
|
||||||
|
margin: 25,
|
||||||
|
guarantorsRequired: 0,
|
||||||
|
coApplicant: 'optional',
|
||||||
|
thirdPartySecurity: 'no'
|
||||||
|
},
|
||||||
|
term: {
|
||||||
|
securityType: 'Immovable property (mortgage)',
|
||||||
|
securityMandatory: 'yes',
|
||||||
|
margin: 20,
|
||||||
|
guarantorsRequired: 1,
|
||||||
|
coApplicant: 'joint_property',
|
||||||
|
thirdPartySecurity: 'consent'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,6 +174,39 @@ export class LoanProductLoanSecurityStepComponent implements OnInit {
|
|||||||
|
|
||||||
loanProductLoanSecurityForm: UntypedFormGroup;
|
loanProductLoanSecurityForm: UntypedFormGroup;
|
||||||
|
|
||||||
|
/** What the bank's loan policy says about the margin for a sub type, shown under the Margin field. */
|
||||||
|
private readonly marginHints: Record<string, string> = {
|
||||||
|
housing: 'Policy: house construction 25%, flat / house purchase 20%, repair or renovation 10%',
|
||||||
|
vehicle: 'Policy: new vehicle 20–25%, old vehicle 50%',
|
||||||
|
education: 'Policy: the student bears 10% of the expense',
|
||||||
|
business: 'Policy: machinery 25% (old machinery 40–50%)',
|
||||||
|
personal: 'Unsecured: no margin'
|
||||||
|
};
|
||||||
|
|
||||||
|
get marginHint(): string {
|
||||||
|
return this.marginHints[this.productTypeSubGroup?.value] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The conditions that apply to the chosen sub type; the template draws one checkbox for each. */
|
||||||
|
applicableConditions: ConditionRule[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The conditions drawn on screen: nothing while the loan is unsecured; the sub type's own list when it has one; and
|
||||||
|
* every condition (all off) when the officer makes security mandatory on a sub type that has no list of its own,
|
||||||
|
* so the officer can always set them.
|
||||||
|
*/
|
||||||
|
get shownConditions(): ConditionRule[] {
|
||||||
|
if (!this.isSecured) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.applicableConditions.length > 0 ? this.applicableConditions : GENERIC_CONDITIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when sanction needs security; an unsecured product shows no security conditions. */
|
||||||
|
get isSecured(): boolean {
|
||||||
|
return this.loanProductLoanSecurityForm.get('securityMandatory').value === 'yes';
|
||||||
|
}
|
||||||
|
|
||||||
readonly securityMandatoryOptions = [
|
readonly securityMandatoryOptions = [
|
||||||
{ id: 'yes', label: 'Yes — no sanction without security' },
|
{ id: 'yes', label: 'Yes — no sanction without security' },
|
||||||
{ id: 'no', label: 'No — unsecured' }
|
{ id: 'no', label: 'No — unsecured' }
|
||||||
@@ -78,44 +223,6 @@ export class LoanProductLoanSecurityStepComponent implements OnInit {
|
|||||||
{ id: 'no', label: 'Not allowed' }
|
{ id: 'no', label: 'Not allowed' }
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Reference table: which security a loan on this product may accept. */
|
|
||||||
readonly collateralRules = [
|
|
||||||
{
|
|
||||||
security: 'Residential property',
|
|
||||||
allowed: true,
|
|
||||||
ltv: '80',
|
|
||||||
valuation: 'Empanelled valuer',
|
|
||||||
insurance: 'Mandatory',
|
|
||||||
charge: 'Mortgage + CERSAI'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
security: 'Commercial property',
|
|
||||||
allowed: true,
|
|
||||||
ltv: '70',
|
|
||||||
valuation: 'Empanelled valuer',
|
|
||||||
insurance: 'Mandatory',
|
|
||||||
charge: 'Mortgage + CERSAI'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
security: 'Fixed / Recurring Deposit',
|
|
||||||
allowed: true,
|
|
||||||
ltv: '90',
|
|
||||||
valuation: 'Not needed',
|
|
||||||
insurance: 'Not needed',
|
|
||||||
charge: 'Lien marked'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
security: 'LIC policy',
|
|
||||||
allowed: true,
|
|
||||||
ltv: '90',
|
|
||||||
valuation: 'Surrender value',
|
|
||||||
insurance: 'Not needed',
|
|
||||||
charge: 'Assignment'
|
|
||||||
},
|
|
||||||
{ security: 'Vehicle', allowed: false, ltv: '—', valuation: '—', insurance: '—', charge: '—' },
|
|
||||||
{ security: 'Gold ornaments', allowed: false, ltv: '—', valuation: '—', insurance: '—', charge: '—' }
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.loanProductLoanSecurityForm = this.formBuilder.group({
|
this.loanProductLoanSecurityForm = this.formBuilder.group({
|
||||||
securityType: ['Immovable property (mortgage)'],
|
securityType: ['Immovable property (mortgage)'],
|
||||||
@@ -164,6 +271,26 @@ export class LoanProductLoanSecurityStepComponent implements OnInit {
|
|||||||
.get('guarantorsRequired')
|
.get('guarantorsRequired')
|
||||||
.setValue(profile.guarantorsRequired, { emitEvent: false });
|
.setValue(profile.guarantorsRequired, { emitEvent: false });
|
||||||
}
|
}
|
||||||
|
([
|
||||||
|
'coApplicant',
|
||||||
|
'thirdPartySecurity'
|
||||||
|
] as const).forEach((name) => {
|
||||||
|
const control = this.loanProductLoanSecurityForm.get(name);
|
||||||
|
if (!control.dirty) {
|
||||||
|
control.setValue(profile[name], { emitEvent: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only the conditions that fit this kind of security apply; the rest are off and not shown.
|
||||||
|
const rules = CONDITIONS_BY_SUB_TYPE[subGroup] ?? [];
|
||||||
|
this.applicableConditions = rules;
|
||||||
|
CONDITION_CONTROLS.forEach((name) => {
|
||||||
|
const control = this.loanProductLoanSecurityForm.get(name);
|
||||||
|
if (!control.dirty) {
|
||||||
|
const rule = rules.find((entry) => entry.control === name);
|
||||||
|
control.setValue(rule ? rule.defaultOn : false, { emitEvent: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Maximum LTV is derived from the margin (100 − margin) and shown read-only. */
|
/** Maximum LTV is derived from the margin (100 − margin) and shown read-only. */
|
||||||
@@ -173,8 +300,16 @@ export class LoanProductLoanSecurityStepComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get loanProductLoanSecurity() {
|
get loanProductLoanSecurity() {
|
||||||
|
const value = this.loanProductLoanSecurityForm.getRawValue();
|
||||||
|
// A condition that is not shown (unsecured, or not part of this security) is never saved as on.
|
||||||
|
const shown = new Set(this.shownConditions.map((rule) => rule.control));
|
||||||
|
CONDITION_CONTROLS.forEach((name) => {
|
||||||
|
if (!shown.has(name)) {
|
||||||
|
value[name] = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
...this.loanProductLoanSecurityForm.getRawValue(),
|
...value,
|
||||||
ltv: this.ltv
|
ltv: this.ltv
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -141,9 +141,11 @@
|
|||||||
<mat-checkbox formControlName="upgradeOnlyAllArrears">
|
<mat-checkbox formControlName="upgradeOnlyAllArrears">
|
||||||
Upgrade from NPA only when all arrears are cleared
|
Upgrade from NPA only when all arrears are cleared
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
<mat-checkbox formControlName="keepStandardIfDepositCovers">
|
@if (isDepositBacked) {
|
||||||
Keep standard while the pledged deposit fully covers the dues
|
<mat-checkbox formControlName="keepStandardIfDepositCovers">
|
||||||
</mat-checkbox>
|
Keep standard while the pledged deposit fully covers the dues
|
||||||
|
</mat-checkbox>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+12
@@ -50,6 +50,16 @@ export class LoanProductPenalNpaStepComponent implements OnInit {
|
|||||||
/** The chosen sub type (e.g. 'cc', 'overdraft', 'kcc', 'gold') within the current product type group. */
|
/** The chosen sub type (e.g. 'cc', 'overdraft', 'kcc', 'gold') within the current product type group. */
|
||||||
subType: string | null = null;
|
subType: string | null = null;
|
||||||
|
|
||||||
|
/** FD / RD / DDS / LIC are lent against a deposit or policy; Gold, CC and the rest are pledged or hypothecated. */
|
||||||
|
get isDepositBacked(): boolean {
|
||||||
|
return [
|
||||||
|
'fd',
|
||||||
|
'rd',
|
||||||
|
'dds',
|
||||||
|
'lic'
|
||||||
|
].includes(this.subType ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
readonly yesNoOptions = [
|
readonly yesNoOptions = [
|
||||||
'YES',
|
'YES',
|
||||||
'NO'
|
'NO'
|
||||||
@@ -94,6 +104,7 @@ export class LoanProductPenalNpaStepComponent implements OnInit {
|
|||||||
readonly eligibleCollateralTypeOptions = [
|
readonly eligibleCollateralTypeOptions = [
|
||||||
{ id: 'NONE', label: 'None' },
|
{ id: 'NONE', label: 'None' },
|
||||||
{ id: 'GOLD', label: 'Gold / gold jewellery' },
|
{ id: 'GOLD', label: 'Gold / gold jewellery' },
|
||||||
|
{ id: 'COMMODITY', label: 'Commodity (warehouse receipt)' },
|
||||||
{ id: 'GOVERNMENT_SECURITIES', label: 'Government securities' },
|
{ id: 'GOVERNMENT_SECURITIES', label: 'Government securities' },
|
||||||
{ id: 'BANK_DEPOSIT', label: 'Bank term deposit (lien-marked)' },
|
{ id: 'BANK_DEPOSIT', label: 'Bank term deposit (lien-marked)' },
|
||||||
{ id: 'LIFE_INSURANCE', label: 'Life insurance policy (surrender value)' }
|
{ id: 'LIFE_INSURANCE', label: 'Life insurance policy (surrender value)' }
|
||||||
@@ -230,6 +241,7 @@ export class LoanProductPenalNpaStepComponent implements OnInit {
|
|||||||
dds: 'BANK_DEPOSIT',
|
dds: 'BANK_DEPOSIT',
|
||||||
lic: 'LIFE_INSURANCE',
|
lic: 'LIFE_INSURANCE',
|
||||||
gold: 'GOLD',
|
gold: 'GOLD',
|
||||||
|
warehouse: 'COMMODITY',
|
||||||
gold_emi: 'GOLD'
|
gold_emi: 'GOLD'
|
||||||
};
|
};
|
||||||
const collateral = subType ? collateralBySubType[subType] : undefined;
|
const collateral = subType ? collateralBySubType[subType] : undefined;
|
||||||
|
|||||||
+5
-3
@@ -7,9 +7,11 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<div class="rv">
|
<div class="rv">
|
||||||
<p class="rv__intro">
|
@if (!readOnly) {
|
||||||
Check the details below. Use <strong>Submit</strong> at the bottom to send the product for approval.
|
<p class="rv__intro">
|
||||||
</p>
|
Check the details below. Use <strong>Submit</strong> at the bottom to send the product for approval.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
@for (section of sections; track section.title) {
|
@for (section of sections; track section.title) {
|
||||||
<div class="rv__card">
|
<div class="rv__card">
|
||||||
|
|||||||
+211
-5
@@ -35,6 +35,8 @@ export class LoanProductPreviewStepComponent {
|
|||||||
@Input() accountingRuleData: any;
|
@Input() accountingRuleData: any;
|
||||||
@Input() loanProduct: any;
|
@Input() loanProduct: any;
|
||||||
@Input() isValid = true;
|
@Input() isValid = true;
|
||||||
|
/** True on the product View page: the reader without the "submit for approval" wording. */
|
||||||
|
@Input() readOnly = false;
|
||||||
@Output() submitEvent = new EventEmitter();
|
@Output() submitEvent = new EventEmitter();
|
||||||
|
|
||||||
/** Keys shown in a section above; everything else non-empty goes to "Other settings". */
|
/** Keys shown in a section above; everything else non-empty goes to "Other settings". */
|
||||||
@@ -95,6 +97,11 @@ export class LoanProductPreviewStepComponent {
|
|||||||
const pct = (value: any) => `${value}%`;
|
const pct = (value: any) => `${value}%`;
|
||||||
const months = (value: any) => `${value} months`;
|
const months = (value: any) => `${value} months`;
|
||||||
const rupees = (value: any) => `₹ ${Number(value).toLocaleString('en-IN')}`;
|
const rupees = (value: any) => `₹ ${Number(value).toLocaleString('en-IN')}`;
|
||||||
|
// An option id such as "fixed_day" read as words ("Fixed day").
|
||||||
|
const words = (value: any) => {
|
||||||
|
const text = String(value).replace(/_/g, ' ');
|
||||||
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||||
|
};
|
||||||
|
|
||||||
const sections: ReviewSection[] = [
|
const sections: ReviewSection[] = [
|
||||||
{
|
{
|
||||||
@@ -123,8 +130,20 @@ export class LoanProductPreviewStepComponent {
|
|||||||
row('Minimum tenure', 'minNumberOfRepayments', months),
|
row('Minimum tenure', 'minNumberOfRepayments', months),
|
||||||
row('Default tenure', 'numberOfRepayments', months),
|
row('Default tenure', 'numberOfRepayments', months),
|
||||||
row('Maximum tenure', 'maxNumberOfRepayments', months),
|
row('Maximum tenure', 'maxNumberOfRepayments', months),
|
||||||
row('Repayment type', 'repaymentType'),
|
row('Repayment type', 'repaymentType', (v) =>
|
||||||
row('Appropriation order', 'appropriationOrder')
|
v === 'bullet'
|
||||||
|
? 'Bullet: principal + interest at maturity'
|
||||||
|
: v === 'running'
|
||||||
|
? 'Running: any time, clear by limit expiry'
|
||||||
|
: v
|
||||||
|
),
|
||||||
|
row('Appropriation order', 'appropriationOrder', (v) =>
|
||||||
|
v === 'charges_penal_interest_principal'
|
||||||
|
? 'Charges, then penal, then interest, then principal'
|
||||||
|
: v === 'principal_interest_penal_charges'
|
||||||
|
? 'Principal, then interest, then penal, then charges'
|
||||||
|
: v
|
||||||
|
)
|
||||||
].filter((r): r is ReviewRow => !!r)
|
].filter((r): r is ReviewRow => !!r)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -147,7 +166,20 @@ export class LoanProductPreviewStepComponent {
|
|||||||
icon: 'fa-shield-halved',
|
icon: 'fa-shield-halved',
|
||||||
rows: [
|
rows: [
|
||||||
row('Security type', 'securityType'),
|
row('Security type', 'securityType'),
|
||||||
row('Eligible amount basis', 'eligibleAmountBasis'),
|
row(
|
||||||
|
'Eligible amount basis',
|
||||||
|
'eligibleAmountBasis',
|
||||||
|
(v) =>
|
||||||
|
(
|
||||||
|
({
|
||||||
|
gold_value: 'Net weight × purity × bank gold rate',
|
||||||
|
balance_accrued: 'Deposit balance + accrued interest',
|
||||||
|
balance: 'Deposit balance only',
|
||||||
|
surrender: 'Surrender value (LIC)',
|
||||||
|
commodity_value: 'Quantity × market price of the commodity'
|
||||||
|
}) as Record<string, string>
|
||||||
|
)[v] ?? v
|
||||||
|
),
|
||||||
row('Margin', 'margin', pct),
|
row('Margin', 'margin', pct),
|
||||||
row('LTV', 'ltv', pct),
|
row('LTV', 'ltv', pct),
|
||||||
row('Lien required', 'lienRequired'),
|
row('Lien required', 'lienRequired'),
|
||||||
@@ -162,6 +194,173 @@ export class LoanProductPreviewStepComponent {
|
|||||||
row('Deposit renewal under lien', 'lienAllowRenewalUnderLien')
|
row('Deposit renewal under lien', 'lienAllowRenewalUnderLien')
|
||||||
].filter((r): r is ReviewRow => !!r)
|
].filter((r): r is ReviewRow => !!r)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Gold details',
|
||||||
|
icon: 'fa-ring',
|
||||||
|
rows: [
|
||||||
|
row('Maximum LTV cap', 'goldMaxLtv', pct),
|
||||||
|
row('Minimum purity', 'goldMinPurityCarat', (v) => `${v} carat`),
|
||||||
|
row('Valuation rate source', 'goldRateSource', (v) =>
|
||||||
|
v === 'ibja_30d_avg' ? 'IBJA 30-day average' : 'Bank daily gold rate'
|
||||||
|
),
|
||||||
|
row('Allowed items', 'goldAllowedItems', (v) =>
|
||||||
|
v === 'ornaments_coins_50g' ? 'Ornaments + coins (up to 50 g)' : 'Ornaments only'
|
||||||
|
),
|
||||||
|
row('Max loan per gram', 'goldMaxPerGram', rupees),
|
||||||
|
row('Minimum net weight', 'goldMinNetWeight', (v) => `${v} g`),
|
||||||
|
row('Max net weight per customer', 'goldMaxNetWeightPerCustomer', (v) => `${v} g`),
|
||||||
|
row('Appraiser required', 'goldAppraiserRequired'),
|
||||||
|
row('Valuation certificate required', 'goldValuationCertificateRequired'),
|
||||||
|
row('Max valuation per appraiser', 'goldMaxValuationPerAppraiser', rupees),
|
||||||
|
row('Packet number prefix', 'goldPacketPrefix'),
|
||||||
|
row('Revalue gold when loan turns NPA', 'goldRevalueOnNpa'),
|
||||||
|
row('Charge customer for revaluation', 'goldRevaluationChargeApplicable'),
|
||||||
|
row('Insurance required', 'insuranceRequired'),
|
||||||
|
row('Partial release of ornaments', 'goldPartialReleaseAllowed'),
|
||||||
|
row('Third-party gold allowed', 'goldThirdPartyAllowed'),
|
||||||
|
row('Disbursement mode', 'goldDisbursementMode', (v) =>
|
||||||
|
v === 'account_credit' ? 'Account credit only' : 'Account credit; cash up to the limit'
|
||||||
|
),
|
||||||
|
row('Cash disbursement limit', 'goldCashDisbursementLimit', rupees),
|
||||||
|
row('LTV breach alert', 'goldLtvBreachAlert', pct),
|
||||||
|
row('Auction notice', 'goldAuctionNoticeDays', (v) => `${v} days`),
|
||||||
|
row('Auction reserve price', 'goldAuctionReservePct', (v) => `${v}% of market value`),
|
||||||
|
row('Customer may redeem before auction', 'goldRedeemBeforeAuction')
|
||||||
|
].filter((r): r is ReviewRow => !!r)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Warehouse receipt details',
|
||||||
|
icon: 'fa-warehouse',
|
||||||
|
rows: [
|
||||||
|
row('Maximum LTV cap', 'wrMaxLtv', pct),
|
||||||
|
row('Receipt type allowed', 'wrReceiptType', (v) =>
|
||||||
|
v === 'e_nwr' ? 'e-NWR (electronic)' : v === 'paper_nwr' ? 'Paper NWR' : 'Both e-NWR and paper NWR'
|
||||||
|
),
|
||||||
|
row('WDRA-registered warehouse', 'wrWdraRegistered'),
|
||||||
|
row('Quality / assay certificate', 'wrAssayCertificateRequired'),
|
||||||
|
row('Tenure within receipt validity', 'wrTenureWithinReceiptValidity'),
|
||||||
|
row('Pledge marked in repository first', 'wrPledgeMarkedBeforeDisbursement'),
|
||||||
|
row('Eligible commodity groups', 'wrCommodityGroups', (v) =>
|
||||||
|
String(v)
|
||||||
|
.split('|')
|
||||||
|
.map((id) => id.charAt(0).toUpperCase() + id.slice(1))
|
||||||
|
.join(', ')
|
||||||
|
),
|
||||||
|
row(
|
||||||
|
'Valuation price source',
|
||||||
|
'wrValuationPriceSource',
|
||||||
|
(v) =>
|
||||||
|
(
|
||||||
|
({
|
||||||
|
mandi_apmc: 'Mandi / APMC price',
|
||||||
|
msp: 'MSP (government support price)',
|
||||||
|
exchange_ncdex: 'Exchange price (NCDEX)',
|
||||||
|
bank_assessed: 'Bank-assessed price'
|
||||||
|
}) as Record<string, string>
|
||||||
|
)[v] ?? v
|
||||||
|
),
|
||||||
|
row('Max loan per quintal', 'wrMaxLoanPerQuintal', rupees),
|
||||||
|
row('Insurance required', 'insuranceRequired'),
|
||||||
|
row('Stock inspection every', 'wrStockInspectionMonths', months),
|
||||||
|
row('LTV breach alert', 'wrLtvBreachAlert', pct),
|
||||||
|
row('Partial release of stock', 'wrPartialReleaseAllowed'),
|
||||||
|
row('Disposal notice', 'wrDisposalNoticeDays', (v) => `${v} days`),
|
||||||
|
row('Disposal reserve price', 'wrDisposalReservePct', (v) => `${v}% of market value`),
|
||||||
|
row('Customer may redeem before disposal', 'wrRedeemBeforeDisposal')
|
||||||
|
].filter((r): r is ReviewRow => !!r)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Borrower eligibility',
|
||||||
|
icon: 'fa-user',
|
||||||
|
rows: [
|
||||||
|
row('Applicant must be a bank member', 'memberRequired'),
|
||||||
|
row('Guarantors must be bank members', 'guarantorMustBeMember'),
|
||||||
|
row('Minimum age', 'minAge', (v) => `${v} years`),
|
||||||
|
row('Maximum age at maturity', 'maxAgeAtMaturity', (v) => `${v} years`),
|
||||||
|
row('Minimum CIBIL score', 'minCibil'),
|
||||||
|
row('Minimum monthly income', 'minMonthlyIncome', rupees),
|
||||||
|
row('Income multiple', 'incomeMultiple', (v) => `${v}× net monthly salary`),
|
||||||
|
row('Maximum FOIR / DBR', 'maxFoir', pct),
|
||||||
|
row('Maximum loans per customer', 'maxLoansPerCustomer')
|
||||||
|
].filter((r): r is ReviewRow => !!r)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Security and guarantors (EMI loan)',
|
||||||
|
icon: 'fa-shield-halved',
|
||||||
|
rows: [
|
||||||
|
row('Security mandatory', 'securityMandatory', words),
|
||||||
|
row('Guarantors required', 'guarantorsRequired'),
|
||||||
|
row('Co-applicant', 'coApplicant', words),
|
||||||
|
row('Third-party security', 'thirdPartySecurity', words),
|
||||||
|
row('Legal opinion / title search', 'legalOpinionRequired'),
|
||||||
|
row('Insurance of the security', 'propertyInsuranceRequired'),
|
||||||
|
row('Charge registered before disbursement', 'registerCharge'),
|
||||||
|
row('Release security on closure', 'releaseSecurityOnClosure'),
|
||||||
|
row('Multiple securities allowed', 'allowMultipleSecurities')
|
||||||
|
].filter((r): r is ReviewRow => !!r)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Disbursement',
|
||||||
|
icon: 'fa-arrow-right',
|
||||||
|
rows: [
|
||||||
|
row('Disbursement type', 'disbursementType', words),
|
||||||
|
row('Disbursement mode', 'disbursementMode', words),
|
||||||
|
row('Interest on undisbursed amount', 'undisbursedInterest', words),
|
||||||
|
row('Pre-EMI interest', 'preEmiInterest')
|
||||||
|
].filter((r): r is ReviewRow => !!r)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Repayment and EMI',
|
||||||
|
icon: 'fa-calendar-days',
|
||||||
|
rows: [
|
||||||
|
row('EMI due day', 'emiDueDay', words),
|
||||||
|
row('If the due date is a holiday', 'holidayRule', words),
|
||||||
|
row('Moratorium', 'moratoriumMonths', months),
|
||||||
|
row('Interest during moratorium', 'moratoriumInterest', words),
|
||||||
|
row('Variable instalments', 'variableInstallments'),
|
||||||
|
row('Repayment mode', 'repaymentMode', words),
|
||||||
|
row('Linked account type', 'linkedAccountType', words),
|
||||||
|
row('Cheque (PDC) collection', 'pdcCollection', words),
|
||||||
|
row('Mandate required', 'mandateRequired'),
|
||||||
|
row('Advance EMI', 'advanceEmi', words)
|
||||||
|
].filter((r): r is ReviewRow => !!r)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Interest rate settings',
|
||||||
|
icon: 'fa-percent',
|
||||||
|
rows: [
|
||||||
|
row('Rate type', 'rateType', words),
|
||||||
|
row('CIBIL adjustment applied', 'applyCibilAdjustment'),
|
||||||
|
row('Rate decided at', 'rateResolvedAt', words),
|
||||||
|
row('Calculation period', 'calcPeriod', words),
|
||||||
|
row('Interest starts from', 'interestStartsFrom', words),
|
||||||
|
row('Compounding', 'compounding', words),
|
||||||
|
row('Benchmark', 'benchmark', words),
|
||||||
|
row('Reset frequency', 'resetFrequency', words),
|
||||||
|
row('On reset, change', 'resetChange', words)
|
||||||
|
].filter((r): r is ReviewRow => !!r)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Prepayment and foreclosure',
|
||||||
|
icon: 'fa-flag-checkered',
|
||||||
|
rows: [
|
||||||
|
row('Part-prepayment allowed', 'partPrepaymentAllowed'),
|
||||||
|
row('Minimum part-payment', 'minPartPayment', rupees),
|
||||||
|
row('Part-payment allowed on', 'allowedOn', words),
|
||||||
|
row('Part-payments per year', 'partPaymentsPerYear', words),
|
||||||
|
row('Part-payment mode', 'partPrepaymentMode', words),
|
||||||
|
row('Foreclosure: individual, floating rate', 'foreclosureFloatingIndividual', pct),
|
||||||
|
row('Foreclosure: individual, fixed rate', 'foreclosureFixedIndividual', pct),
|
||||||
|
row('Foreclosure: non-individual', 'foreclosureNonIndividual', pct),
|
||||||
|
row('Foreclosure: takeover', 'foreclosureTakeover', pct),
|
||||||
|
row('Foreclosure quote valid for', 'foreclosureQuoteValidDays', (v) => `${v} days`),
|
||||||
|
row('Close automatically at zero balance', 'autoCloseOnZeroBalance'),
|
||||||
|
row('NOC letter', 'nocLetter', words),
|
||||||
|
row('Restructuring', 'restructuring', words),
|
||||||
|
row('Top-up allowed', 'topUpAllowed'),
|
||||||
|
row('Balance transfer out allowed', 'balanceTransferOutAllowed')
|
||||||
|
].filter((r): r is ReviewRow => !!r)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Penal, SMA and NPA',
|
title: 'Penal, SMA and NPA',
|
||||||
icon: 'fa-triangle-exclamation',
|
icon: 'fa-triangle-exclamation',
|
||||||
@@ -191,6 +390,11 @@ export class LoanProductPreviewStepComponent {
|
|||||||
row('Interest on pre-closure', 'interestOnPreclosure')
|
row('Interest on pre-closure', 'interestOnPreclosure')
|
||||||
].filter((r): r is ReviewRow => !!r)
|
].filter((r): r is ReviewRow => !!r)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Charges',
|
||||||
|
icon: 'fa-receipt',
|
||||||
|
rows: [row('Attached charges', 'chargesSummary')].filter((r): r is ReviewRow => !!r)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Accounting (GL accounts)',
|
title: 'Accounting (GL accounts)',
|
||||||
icon: 'fa-book',
|
icon: 'fa-book',
|
||||||
@@ -213,8 +417,10 @@ export class LoanProductPreviewStepComponent {
|
|||||||
this.shown.add('productTypeSubGroup');
|
this.shown.add('productTypeSubGroup');
|
||||||
|
|
||||||
this.cachedSections = sections;
|
this.cachedSections = sections;
|
||||||
this.cachedOther = Object.keys(p)
|
// On the View page the product also carries every raw core field; only the extension record is "other settings".
|
||||||
.filter((key) => !this.shown.has(key) && !this.hidden.has(key))
|
const candidates = this.readOnly ? Object.keys(p.advanceExtensions ?? {}) : Object.keys(p);
|
||||||
|
this.cachedOther = candidates
|
||||||
|
.filter((key) => key !== 'advanceExtensions' && !this.shown.has(key) && !this.hidden.has(key))
|
||||||
.filter(
|
.filter(
|
||||||
(key) => p[key] !== undefined && p[key] !== null && p[key] !== '' && !(Array.isArray(p[key]) && !p[key].length)
|
(key) => p[key] !== undefined && p[key] !== null && p[key] !== '' && !(Array.isArray(p[key]) && !p[key].length)
|
||||||
)
|
)
|
||||||
|
|||||||
+438
-9
@@ -27,15 +27,29 @@
|
|||||||
<mat-hint>From Sub Type — not chosen again</mat-hint>
|
<mat-hint>From Sub Type — not chosen again</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="lsl-field">
|
@if (isGold || isWarehouse) {
|
||||||
<mat-label>Eligible Amount Basis</mat-label>
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
<mat-select formControlName="eligibleAmountBasis" required>
|
<mat-label>Eligible Amount Basis</mat-label>
|
||||||
@for (opt of eligibleAmountBasisOptions; track opt.id) {
|
<input matInput [value]="isGold ? goldBasisLabel : warehouseBasisLabel" disabled />
|
||||||
<mat-option [value]="opt.id">{{ opt.label }}</mat-option>
|
<mat-hint>
|
||||||
}
|
@if (isGold) {
|
||||||
</mat-select>
|
Fixed for Gold: valued on weight, purity and the gold rate
|
||||||
<mat-error>Eligible Amount Basis is <strong>required</strong></mat-error>
|
} @else {
|
||||||
</mat-form-field>
|
Fixed for Warehouse Receipt: valued on quantity and the commodity's market price
|
||||||
|
}
|
||||||
|
</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
} @else {
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Eligible Amount Basis</mat-label>
|
||||||
|
<mat-select formControlName="eligibleAmountBasis" required>
|
||||||
|
@for (opt of eligibleAmountBasisOptions; track opt.id) {
|
||||||
|
<mat-option [value]="opt.id">{{ opt.label }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
<mat-error>Eligible Amount Basis is <strong>required</strong></mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="lsl-field">
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
<mat-label>Margin (%)</mat-label>
|
<mat-label>Margin (%)</mat-label>
|
||||||
@@ -53,9 +67,424 @@
|
|||||||
<input matInput [value]="ltv" disabled />
|
<input matInput [value]="ltv" disabled />
|
||||||
<mat-hint>= 100 − Margin</mat-hint>
|
<mat-hint>= 100 − Margin</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
@if (isGold) {
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Maximum LTV Cap (%)</mat-label>
|
||||||
|
<input matInput type="number" formControlName="goldMaxLtv" min="1" max="100" step="0.01" required />
|
||||||
|
<mat-hint>No account may be sanctioned above this LTV (RBI ceiling 75%)</mat-hint>
|
||||||
|
@if (loanProductSecurityLienForm.get('goldMaxLtv').hasError('required')) {
|
||||||
|
<mat-error>Maximum LTV Cap is <strong>required</strong></mat-error>
|
||||||
|
} @else {
|
||||||
|
<mat-error>Must be between 1 and 100</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (isWarehouse) {
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Maximum LTV Cap (%)</mat-label>
|
||||||
|
<input matInput type="number" formControlName="wrMaxLtv" min="1" max="100" step="0.01" required />
|
||||||
|
<mat-hint>No account may be sanctioned above this LTV</mat-hint>
|
||||||
|
@if (loanProductSecurityLienForm.get('wrMaxLtv').hasError('required')) {
|
||||||
|
<mat-error>Maximum LTV Cap is <strong>required</strong></mat-error>
|
||||||
|
} @else {
|
||||||
|
<mat-error>Must be between 1 and 100</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@if ((isGold || isWarehouse) && loanProductSecurityLienForm.hasError('ltvAboveCap')) {
|
||||||
|
<div class="lsl-error">
|
||||||
|
<i class="fas fa-circle-exclamation"></i>
|
||||||
|
LTV (100 − Margin) is above the Maximum LTV Cap. Raise the margin or the cap.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (isGold) {
|
||||||
|
<div class="lsl-section">
|
||||||
|
<div class="lsl-section__header">
|
||||||
|
<i class="fas fa-ring"></i>
|
||||||
|
<h3>Gold Details</h3>
|
||||||
|
<span class="lsl-section__hint">Pledged ornaments — no lien marking</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-subsection">
|
||||||
|
<p class="lsl-group">Valuation basis</p>
|
||||||
|
<div class="lsl-grid">
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Minimum Purity (carat)</mat-label>
|
||||||
|
<mat-select formControlName="goldMinPurityCarat" required>
|
||||||
|
@for (carat of goldPurityOptions; track carat) {
|
||||||
|
<mat-option [value]="carat">{{ carat }} carat</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
<mat-hint>Ornaments below this purity are not accepted</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Valuation Rate Source</mat-label>
|
||||||
|
<mat-select formControlName="goldRateSource" required>
|
||||||
|
@for (opt of goldRateSourceOptions; track opt.id) {
|
||||||
|
<mat-option [value]="opt.id">{{ opt.label }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Allowed Items</mat-label>
|
||||||
|
<mat-select formControlName="goldAllowedItems" required>
|
||||||
|
@for (opt of goldAllowedItemsOptions; track opt.id) {
|
||||||
|
<mat-option [value]="opt.id">{{ opt.label }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Max Loan per Gram (₹)</mat-label>
|
||||||
|
<input matInput type="number" formControlName="goldMaxPerGram" min="1" />
|
||||||
|
<mat-hint>Optional bank cap on the gold rate</mat-hint>
|
||||||
|
<mat-error>Must be above 0</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Min Net Weight (g)</mat-label>
|
||||||
|
<input matInput type="number" formControlName="goldMinNetWeight" min="0" step="0.01" />
|
||||||
|
<mat-hint>Optional</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Max Net Weight per Customer (g)</mat-label>
|
||||||
|
<input matInput type="number" formControlName="goldMaxNetWeightPerCustomer" min="0" step="0.01" />
|
||||||
|
<mat-hint>Optional</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
@if (loanProductSecurityLienForm.hasError('netWeightRange')) {
|
||||||
|
<div class="lsl-error">
|
||||||
|
<i class="fas fa-circle-exclamation"></i>
|
||||||
|
Minimum net weight cannot exceed the maximum per customer.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-subsection">
|
||||||
|
<p class="lsl-group">Valuation by appraiser</p>
|
||||||
|
<div class="lsl-grid">
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Appraiser Required</mat-label>
|
||||||
|
<input matInput value="YES (mandatory)" disabled />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Valuation Certificate Required</mat-label>
|
||||||
|
<mat-select formControlName="goldValuationCertificateRequired">
|
||||||
|
@for (opt of yesNoOptions; track opt) {
|
||||||
|
<mat-option [value]="opt">{{ opt }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Max Valuation per Appraiser (₹)</mat-label>
|
||||||
|
<input matInput type="number" formControlName="goldMaxValuationPerAppraiser" min="1" />
|
||||||
|
<mat-hint>Optional default limit for one appraiser</mat-hint>
|
||||||
|
<mat-error>Must be above 0</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-subsection">
|
||||||
|
<p class="lsl-group">Safe custody</p>
|
||||||
|
<div class="lsl-grid">
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Packet No. Prefix</mat-label>
|
||||||
|
<input matInput formControlName="goldPacketPrefix" maxlength="20" />
|
||||||
|
<mat-hint>The packet number itself is set per account</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Insurance Required</mat-label>
|
||||||
|
<mat-select formControlName="insuranceRequired">
|
||||||
|
@for (opt of yesNoOptions; track opt) {
|
||||||
|
<mat-option [value]="opt">{{ opt }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-subsection">
|
||||||
|
<p class="lsl-group">Revaluation</p>
|
||||||
|
<div class="lsl-checks">
|
||||||
|
<mat-checkbox formControlName="goldRevalueOnNpa">
|
||||||
|
Revalue the gold when the loan turns NPA (short-term loans are not revalued on a schedule)
|
||||||
|
</mat-checkbox>
|
||||||
|
<mat-checkbox formControlName="goldRevaluationChargeApplicable">
|
||||||
|
Charge the customer for revaluation (set the fee on the Charges step)
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-subsection">
|
||||||
|
<p class="lsl-group">Loan servicing rules</p>
|
||||||
|
<div class="lsl-checks">
|
||||||
|
<mat-checkbox formControlName="goldPartialReleaseAllowed">
|
||||||
|
Partial release of ornaments allowed on part payment (LTV must stay within the cap)
|
||||||
|
</mat-checkbox>
|
||||||
|
<mat-checkbox formControlName="goldThirdPartyAllowed">
|
||||||
|
Third-party gold allowed (relative's gold, consent letter required)
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-subsection">
|
||||||
|
<p class="lsl-group">Disbursement and monitoring</p>
|
||||||
|
<div class="lsl-grid">
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Disbursement Mode</mat-label>
|
||||||
|
<mat-select formControlName="goldDisbursementMode" required>
|
||||||
|
@for (opt of goldDisbursementModeOptions; track opt.id) {
|
||||||
|
<mat-option [value]="opt.id">{{ opt.label }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
@if (loanProductSecurityLienForm.get('goldDisbursementMode').value === 'account_credit_cash_limit') {
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Cash Disbursement Limit (₹)</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="number"
|
||||||
|
formControlName="goldCashDisbursementLimit"
|
||||||
|
min="0"
|
||||||
|
[max]="goldCashLimitMax"
|
||||||
|
/>
|
||||||
|
<mat-hint>Cash above this is not allowed (Income Tax Sec 269SS)</mat-hint>
|
||||||
|
<mat-error>Must be between 0 and {{ goldCashLimitMax }}</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>LTV Breach Alert (%)</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="number"
|
||||||
|
formControlName="goldLtvBreachAlert"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
step="0.01"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<mat-hint>Flag the account when the live LTV (after a gold rate fall) crosses this level</mat-hint>
|
||||||
|
<mat-error>Required, between 1 and 100</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
@if (loanProductSecurityLienForm.hasError('alertBelowCap')) {
|
||||||
|
<div class="lsl-error">
|
||||||
|
<i class="fas fa-circle-exclamation"></i>
|
||||||
|
LTV Breach Alert cannot be below the Maximum LTV Cap.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-subsection">
|
||||||
|
<p class="lsl-group">Auction and recovery</p>
|
||||||
|
<div class="lsl-grid">
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Auction Notice Period (days)</mat-label>
|
||||||
|
<input matInput type="number" formControlName="goldAuctionNoticeDays" min="0" required />
|
||||||
|
<mat-hint>Notice given before an auction after NPA</mat-hint>
|
||||||
|
<mat-error>Auction Notice Period is <strong>required</strong></mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Auction Reserve Price (% of market value)</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="number"
|
||||||
|
formControlName="goldAuctionReservePct"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
step="0.01"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<mat-hint>Gold is not sold below this share of the market value</mat-hint>
|
||||||
|
<mat-error>Required, between 1 and 100</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="lsl-checks lsl-checks--spaced">
|
||||||
|
<mat-checkbox [checked]="true" disabled>
|
||||||
|
Customer may redeem (clear dues and take the gold) any time before the auction. Always on
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-note">
|
||||||
|
<i class="fas fa-circle-info"></i>
|
||||||
|
Packet number, item list, weights, carat, appraiser and valuation date are captured per account at
|
||||||
|
<strong>Loan Account Creation</strong>, not on the product.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (isWarehouse) {
|
||||||
|
<div class="lsl-section">
|
||||||
|
<div class="lsl-section__header">
|
||||||
|
<i class="fas fa-warehouse"></i>
|
||||||
|
<h3>Warehouse Receipt Details</h3>
|
||||||
|
<span class="lsl-section__hint">Pledged commodity — no lien marking</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-subsection">
|
||||||
|
<p class="lsl-group">Receipt</p>
|
||||||
|
<div class="lsl-grid">
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Receipt Type Allowed</mat-label>
|
||||||
|
<mat-select formControlName="wrReceiptType" required>
|
||||||
|
@for (opt of wrReceiptTypeOptions; track opt.id) {
|
||||||
|
<mat-option [value]="opt.id">{{ opt.label }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
<mat-hint>e-NWR is pledge-marked online in the repository</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Warehouse WDRA-registered</mat-label>
|
||||||
|
<input matInput value="YES (mandatory)" disabled />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Quality / Assay Certificate</mat-label>
|
||||||
|
<input matInput value="YES (mandatory)" disabled />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="lsl-checks lsl-checks--spaced">
|
||||||
|
<mat-checkbox formControlName="wrTenureWithinReceiptValidity">
|
||||||
|
Tenure cannot exceed the receipt's validity date
|
||||||
|
</mat-checkbox>
|
||||||
|
<mat-checkbox formControlName="wrPledgeMarkedBeforeDisbursement">
|
||||||
|
Receipt must be pledge-marked in the repository before disbursement
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-subsection">
|
||||||
|
<p class="lsl-group">Commodity and valuation</p>
|
||||||
|
<div class="lsl-grid">
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field lsl-field--full">
|
||||||
|
<mat-label>Eligible Commodity Groups</mat-label>
|
||||||
|
<mat-select formControlName="wrCommodityGroups" multiple required>
|
||||||
|
@for (opt of wrCommodityGroupOptions; track opt.id) {
|
||||||
|
<mat-option [value]="opt.id">{{ opt.label }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
<mat-hint>Only receipts for these commodities can be pledged</mat-hint>
|
||||||
|
<mat-error>Pick at least one commodity group</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Valuation Price Source</mat-label>
|
||||||
|
<mat-select formControlName="wrValuationPriceSource" required>
|
||||||
|
@for (opt of wrValuationSourceOptions; track opt.id) {
|
||||||
|
<mat-option [value]="opt.id">{{ opt.label }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Max Loan per Quintal (₹)</mat-label>
|
||||||
|
<input matInput type="number" formControlName="wrMaxLoanPerQuintal" min="1" />
|
||||||
|
<mat-hint>Optional bank cap on the price</mat-hint>
|
||||||
|
<mat-error>Must be above 0</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-subsection">
|
||||||
|
<p class="lsl-group">Insurance and inspection</p>
|
||||||
|
<div class="lsl-grid">
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Insurance Required</mat-label>
|
||||||
|
<mat-select formControlName="insuranceRequired">
|
||||||
|
@for (opt of yesNoOptions; track opt) {
|
||||||
|
<mat-option [value]="opt">{{ opt }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Stock Inspection Every (months)</mat-label>
|
||||||
|
<input matInput type="number" formControlName="wrStockInspectionMonths" min="1" required />
|
||||||
|
<mat-hint>Physical check that the stock is in the warehouse</mat-hint>
|
||||||
|
<mat-error>Required, at least 1</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>LTV Breach Alert (%)</mat-label>
|
||||||
|
<input matInput type="number" formControlName="wrLtvBreachAlert" min="1" max="100" step="0.01" required />
|
||||||
|
<mat-hint>Flag the account when a price fall pushes the live LTV above this</mat-hint>
|
||||||
|
<mat-error>Required, between 1 and 100</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
@if (loanProductSecurityLienForm.hasError('alertBelowCap')) {
|
||||||
|
<div class="lsl-error">
|
||||||
|
<i class="fas fa-circle-exclamation"></i>
|
||||||
|
LTV Breach Alert cannot be below the Maximum LTV Cap.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-subsection">
|
||||||
|
<p class="lsl-group">Loan servicing rules</p>
|
||||||
|
<div class="lsl-checks">
|
||||||
|
<mat-checkbox formControlName="wrPartialReleaseAllowed">
|
||||||
|
Partial release of stock allowed on part payment (LTV must stay within the cap)
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-subsection">
|
||||||
|
<p class="lsl-group">Disposal and recovery</p>
|
||||||
|
<div class="lsl-grid">
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Disposal Notice Period (days)</mat-label>
|
||||||
|
<input matInput type="number" formControlName="wrDisposalNoticeDays" min="0" required />
|
||||||
|
<mat-hint>Notice given before the stock is sold after NPA</mat-hint>
|
||||||
|
<mat-error>Disposal Notice Period is <strong>required</strong></mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="lsl-field">
|
||||||
|
<mat-label>Disposal Reserve Price (% of market value)</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="number"
|
||||||
|
formControlName="wrDisposalReservePct"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
step="0.01"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<mat-hint>Stock is not sold below this share of the market value</mat-hint>
|
||||||
|
<mat-error>Required, between 1 and 100</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="lsl-checks lsl-checks--spaced">
|
||||||
|
<mat-checkbox [checked]="true" disabled>
|
||||||
|
Customer may redeem (clear dues and take the stock) any time before the disposal. Always on
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lsl-note">
|
||||||
|
<i class="fas fa-circle-info"></i>
|
||||||
|
Commodity, quantity, grade, warehouse, receipt number and validity, repository pledge reference and the market
|
||||||
|
price on the sanction date are captured per account at <strong>Loan Account Creation</strong>, not on the
|
||||||
|
product.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (isLienSubType) {
|
@if (isLienSubType) {
|
||||||
<div class="lsl-section">
|
<div class="lsl-section">
|
||||||
<div class="lsl-section__header">
|
<div class="lsl-section__header">
|
||||||
|
|||||||
+17
@@ -115,3 +115,20 @@ $gray-900: #202124;
|
|||||||
color: $accent;
|
color: $accent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lsl-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 8px 24px 16px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fef2f2;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lsl-checks--spaced {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|||||||
+325
-36
@@ -67,9 +67,65 @@ export class LoanProductSecurityLienStepComponent implements OnInit {
|
|||||||
fd: 10,
|
fd: 10,
|
||||||
rd: 10,
|
rd: 10,
|
||||||
dds: 10,
|
dds: 10,
|
||||||
lic: 40
|
lic: 40,
|
||||||
|
gold: 25,
|
||||||
|
warehouse: 25
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** RBI ceiling a gold loan may be sanctioned at; the product's own LTV cap defaults to it. */
|
||||||
|
private readonly goldDefaultLtvCap = 75;
|
||||||
|
|
||||||
|
readonly goldPurityOptions = [
|
||||||
|
18,
|
||||||
|
20,
|
||||||
|
22,
|
||||||
|
24
|
||||||
|
];
|
||||||
|
|
||||||
|
readonly goldRateSourceOptions = [
|
||||||
|
{ id: 'bank_daily', label: 'Bank daily gold rate' },
|
||||||
|
{ id: 'ibja_30d_avg', label: 'IBJA 30-day average' }
|
||||||
|
];
|
||||||
|
|
||||||
|
readonly goldAllowedItemsOptions = [
|
||||||
|
{ id: 'ornaments_only', label: 'Ornaments only' },
|
||||||
|
{ id: 'ornaments_coins_50g', label: 'Ornaments + coins (up to 50 g)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
readonly goldBasisLabel = 'Net weight × purity × bank gold rate';
|
||||||
|
|
||||||
|
// ---- Loan against Warehouse Receipt ----
|
||||||
|
readonly warehouseBasisLabel = 'Quantity × market price of the commodity';
|
||||||
|
|
||||||
|
readonly wrReceiptTypeOptions = [
|
||||||
|
{ id: 'e_nwr', label: 'e-NWR (electronic)' },
|
||||||
|
{ id: 'paper_nwr', label: 'Paper NWR' },
|
||||||
|
{ id: 'both', label: 'Both' }
|
||||||
|
];
|
||||||
|
|
||||||
|
readonly wrCommodityGroupOptions = [
|
||||||
|
{ id: 'cereals', label: 'Cereals' },
|
||||||
|
{ id: 'pulses', label: 'Pulses' },
|
||||||
|
{ id: 'oilseeds', label: 'Oilseeds' },
|
||||||
|
{ id: 'spices', label: 'Spices' },
|
||||||
|
{ id: 'others', label: 'Others' }
|
||||||
|
];
|
||||||
|
|
||||||
|
readonly wrValuationSourceOptions = [
|
||||||
|
{ id: 'mandi_apmc', label: 'Mandi / APMC price' },
|
||||||
|
{ id: 'msp', label: 'MSP (government support price)' },
|
||||||
|
{ id: 'exchange_ncdex', label: 'Exchange price (NCDEX)' },
|
||||||
|
{ id: 'bank_assessed', label: 'Bank-assessed price' }
|
||||||
|
];
|
||||||
|
|
||||||
|
readonly goldDisbursementModeOptions = [
|
||||||
|
{ id: 'account_credit', label: 'Account credit only' },
|
||||||
|
{ id: 'account_credit_cash_limit', label: 'Account credit; cash up to the limit' }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** The most that may be paid out in cash against one gold loan (Income Tax Sec 269SS). */
|
||||||
|
readonly goldCashLimitMax = 20000;
|
||||||
|
|
||||||
readonly eligibleAmountBasisOptions = [
|
readonly eligibleAmountBasisOptions = [
|
||||||
{ id: 'balance_accrued', label: 'Deposit balance + accrued interest' },
|
{ id: 'balance_accrued', label: 'Deposit balance + accrued interest' },
|
||||||
{ id: 'balance', label: 'Deposit balance only' },
|
{ id: 'balance', label: 'Deposit balance only' },
|
||||||
@@ -120,6 +176,16 @@ export class LoanProductSecurityLienStepComponent implements OnInit {
|
|||||||
if (margin != null && !this.loanProductSecurityLienForm.get('margin').dirty) {
|
if (margin != null && !this.loanProductSecurityLienForm.get('margin').dirty) {
|
||||||
this.loanProductSecurityLienForm.get('margin').setValue(margin, { emitEvent: false });
|
this.loanProductSecurityLienForm.get('margin').setValue(margin, { emitEvent: false });
|
||||||
}
|
}
|
||||||
|
// Gold is valued on weight, purity and the gold rate; the deposit bases apply to every other type.
|
||||||
|
const basis = this.loanProductSecurityLienForm.get('eligibleAmountBasis');
|
||||||
|
if (subGroup === 'gold') {
|
||||||
|
basis.setValue('gold_value', { emitEvent: false });
|
||||||
|
} else if (subGroup === 'warehouse') {
|
||||||
|
basis.setValue('commodity_value', { emitEvent: false });
|
||||||
|
} else if (basis.value === 'gold_value' || basis.value === 'commodity_value') {
|
||||||
|
basis.setValue('balance_accrued', { emitEvent: false });
|
||||||
|
}
|
||||||
|
this.loanProductSecurityLienForm.updateValueAndValidity({ emitEvent: false });
|
||||||
// The deposit list follows the sub type until the officer has chosen their own.
|
// 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.
|
// 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 deposit = this.defaultDepositBySubGroup[subGroup] ?? 'Fixed Deposit – Regular';
|
||||||
@@ -134,6 +200,16 @@ export class LoanProductSecurityLienStepComponent implements OnInit {
|
|||||||
return this.productTypeGroup?.value === 'advances';
|
return this.productTypeGroup?.value === 'advances';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Loan against Gold: pledged ornaments, valued by an appraiser; no lien or deposit list. */
|
||||||
|
get isGold(): boolean {
|
||||||
|
return this.productTypeSubGroup?.value === 'gold';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Loan against Warehouse Receipt: a pledged commodity, valued on quantity and market price; no lien or deposit list. */
|
||||||
|
get isWarehouse(): boolean {
|
||||||
|
return this.productTypeSubGroup?.value === 'warehouse';
|
||||||
|
}
|
||||||
|
|
||||||
/** The lien block shows only for deposit / policy sub types; gold, warehouse, CC etc. are pledged. */
|
/** The lien block shows only for deposit / policy sub types; gold, warehouse, CC etc. are pledged. */
|
||||||
get isLienSubType(): boolean {
|
get isLienSubType(): boolean {
|
||||||
return this.lienSubGroups.includes(this.productTypeSubGroup?.value);
|
return this.lienSubGroups.includes(this.productTypeSubGroup?.value);
|
||||||
@@ -149,41 +225,216 @@ export class LoanProductSecurityLienStepComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createForm() {
|
createForm() {
|
||||||
this.loanProductSecurityLienForm = this.formBuilder.group({
|
this.loanProductSecurityLienForm = this.formBuilder.group(
|
||||||
securityType: [{ value: '', disabled: true }],
|
{
|
||||||
eligibleAmountBasis: [
|
securityType: [{ value: '', disabled: true }],
|
||||||
'balance_accrued',
|
eligibleAmountBasis: [
|
||||||
Validators.required
|
'balance_accrued',
|
||||||
],
|
Validators.required
|
||||||
margin: [
|
],
|
||||||
10,
|
margin: [
|
||||||
[
|
10,
|
||||||
Validators.required,
|
[
|
||||||
Validators.min(0),
|
Validators.required,
|
||||||
Validators.max(100)
|
Validators.min(0),
|
||||||
|
Validators.max(100)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
lienMarkingStage: [
|
||||||
|
'disbursement',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
eligibleDepositProducts: [
|
||||||
|
['Fixed Deposit – Regular'],
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
tenureNotExceedMaturity: [true],
|
||||||
|
autoReleaseOnClosure: [true],
|
||||||
|
allowMultipleDeposits: [false],
|
||||||
|
adjustFromMaturity: [true],
|
||||||
|
allowThirdPartyDeposit: [false],
|
||||||
|
allowRenewalUnderLien: [true],
|
||||||
|
lienCertificateRequired: ['YES'],
|
||||||
|
lienCertificatePrefix: [
|
||||||
|
'LC/FD/',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
printOnDisbursement: ['YES'],
|
||||||
|
// ---- Loan against Gold (hidden and not sent for any other type) ----
|
||||||
|
goldMaxLtv: [
|
||||||
|
this.goldDefaultLtvCap,
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.min(1),
|
||||||
|
Validators.max(100)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
goldMinPurityCarat: [
|
||||||
|
18,
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
goldRateSource: [
|
||||||
|
'bank_daily',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
goldMaxPerGram: [
|
||||||
|
'',
|
||||||
|
Validators.min(1)
|
||||||
|
],
|
||||||
|
goldMinNetWeight: [
|
||||||
|
'',
|
||||||
|
Validators.min(0)
|
||||||
|
],
|
||||||
|
goldMaxNetWeightPerCustomer: [
|
||||||
|
'',
|
||||||
|
Validators.min(0)
|
||||||
|
],
|
||||||
|
goldAllowedItems: [
|
||||||
|
'ornaments_only',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
goldValuationCertificateRequired: ['YES'],
|
||||||
|
goldMaxValuationPerAppraiser: [
|
||||||
|
'',
|
||||||
|
Validators.min(1)
|
||||||
|
],
|
||||||
|
goldPacketPrefix: [
|
||||||
|
'GL/',
|
||||||
|
Validators.maxLength(20)
|
||||||
|
],
|
||||||
|
goldRevalueOnNpa: [true],
|
||||||
|
goldRevaluationChargeApplicable: [true],
|
||||||
|
insuranceRequired: ['YES'],
|
||||||
|
goldAuctionNoticeDays: [
|
||||||
|
15,
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.min(0)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
goldPartialReleaseAllowed: [true],
|
||||||
|
goldThirdPartyAllowed: [false],
|
||||||
|
goldDisbursementMode: [
|
||||||
|
'account_credit_cash_limit',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
goldCashDisbursementLimit: [
|
||||||
|
this.goldCashLimitMax,
|
||||||
|
[
|
||||||
|
Validators.min(0),
|
||||||
|
Validators.max(this.goldCashLimitMax)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
goldLtvBreachAlert: [
|
||||||
|
80,
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.min(1),
|
||||||
|
Validators.max(100)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
goldAuctionReservePct: [
|
||||||
|
90,
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.min(1),
|
||||||
|
Validators.max(100)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
// ---- Loan against Warehouse Receipt (hidden and not sent for any other type) ----
|
||||||
|
wrMaxLtv: [
|
||||||
|
75,
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.min(1),
|
||||||
|
Validators.max(100)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
wrReceiptType: [
|
||||||
|
'e_nwr',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
wrTenureWithinReceiptValidity: [true],
|
||||||
|
wrPledgeMarkedBeforeDisbursement: [true],
|
||||||
|
wrCommodityGroups: [
|
||||||
|
[
|
||||||
|
'cereals',
|
||||||
|
'pulses',
|
||||||
|
'oilseeds'
|
||||||
|
],
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
wrValuationPriceSource: [
|
||||||
|
'mandi_apmc',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
wrMaxLoanPerQuintal: [
|
||||||
|
'',
|
||||||
|
Validators.min(1)
|
||||||
|
],
|
||||||
|
wrStockInspectionMonths: [
|
||||||
|
3,
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.min(1)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
wrLtvBreachAlert: [
|
||||||
|
80,
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.min(1),
|
||||||
|
Validators.max(100)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
wrPartialReleaseAllowed: [true],
|
||||||
|
wrDisposalNoticeDays: [
|
||||||
|
15,
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.min(0)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
wrDisposalReservePct: [
|
||||||
|
90,
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.min(1),
|
||||||
|
Validators.max(100)
|
||||||
|
]
|
||||||
]
|
]
|
||||||
],
|
},
|
||||||
lienMarkingStage: [
|
{ validators: (group: AbstractControl) => this.pledgeRules(group) }
|
||||||
'disbursement',
|
);
|
||||||
Validators.required
|
}
|
||||||
],
|
|
||||||
eligibleDepositProducts: [
|
/**
|
||||||
['Fixed Deposit – Regular'],
|
* Pledged-security rules (Gold, Warehouse): the LTV the margin gives must fit under the product's LTV cap, the breach
|
||||||
Validators.required
|
* alert must not sit below the cap, and a gold net-weight range must run low to high.
|
||||||
],
|
*/
|
||||||
tenureNotExceedMaturity: [true],
|
private pledgeRules(group: AbstractControl): Record<string, boolean> | null {
|
||||||
autoReleaseOnClosure: [true],
|
const gold = this.isGold;
|
||||||
allowMultipleDeposits: [false],
|
if (!gold && !this.isWarehouse) {
|
||||||
adjustFromMaturity: [true],
|
return null;
|
||||||
allowThirdPartyDeposit: [false],
|
}
|
||||||
allowRenewalUnderLien: [true],
|
const errors: Record<string, boolean> = {};
|
||||||
lienCertificateRequired: ['YES'],
|
const margin = Number(group.get('margin')?.value);
|
||||||
lienCertificatePrefix: [
|
const cap = Number(group.get(gold ? 'goldMaxLtv' : 'wrMaxLtv')?.value);
|
||||||
'LC/FD/',
|
if (!isNaN(margin) && !isNaN(cap) && 100 - margin > cap) {
|
||||||
Validators.required
|
errors['ltvAboveCap'] = true;
|
||||||
],
|
}
|
||||||
printOnDisbursement: ['YES']
|
const alert = Number(group.get(gold ? 'goldLtvBreachAlert' : 'wrLtvBreachAlert')?.value);
|
||||||
});
|
if (!isNaN(alert) && !isNaN(cap) && alert < cap) {
|
||||||
|
errors['alertBelowCap'] = true;
|
||||||
|
}
|
||||||
|
if (gold) {
|
||||||
|
const min = group.get('goldMinNetWeight')?.value;
|
||||||
|
const max = group.get('goldMaxNetWeightPerCustomer')?.value;
|
||||||
|
if (min !== '' && max !== '' && min !== null && max !== null && Number(min) > Number(max)) {
|
||||||
|
errors['netWeightRange'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.keys(errors).length ? errors : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Flat values for the payload. Not wired into submit yet — see the class comment. */
|
/** Flat values for the payload. Not wired into submit yet — see the class comment. */
|
||||||
@@ -206,7 +457,45 @@ export class LoanProductSecurityLienStepComponent implements OnInit {
|
|||||||
},
|
},
|
||||||
lienCertificateRequired: value.lienCertificateRequired,
|
lienCertificateRequired: value.lienCertificateRequired,
|
||||||
lienCertificatePrefix: value.lienCertificatePrefix,
|
lienCertificatePrefix: value.lienCertificatePrefix,
|
||||||
printOnDisbursement: value.printOnDisbursement
|
printOnDisbursement: value.printOnDisbursement,
|
||||||
|
gold: {
|
||||||
|
maxLtv: value.goldMaxLtv,
|
||||||
|
minPurityCarat: value.goldMinPurityCarat,
|
||||||
|
rateSource: value.goldRateSource,
|
||||||
|
maxPerGram: value.goldMaxPerGram,
|
||||||
|
minNetWeight: value.goldMinNetWeight,
|
||||||
|
maxNetWeightPerCustomer: value.goldMaxNetWeightPerCustomer,
|
||||||
|
allowedItems: value.goldAllowedItems,
|
||||||
|
valuationCertificateRequired: value.goldValuationCertificateRequired,
|
||||||
|
maxValuationPerAppraiser: value.goldMaxValuationPerAppraiser,
|
||||||
|
packetPrefix: value.goldPacketPrefix,
|
||||||
|
revalueOnNpa: value.goldRevalueOnNpa,
|
||||||
|
revaluationChargeApplicable: value.goldRevaluationChargeApplicable,
|
||||||
|
insuranceRequired: value.insuranceRequired,
|
||||||
|
auctionNoticeDays: value.goldAuctionNoticeDays,
|
||||||
|
partialReleaseAllowed: value.goldPartialReleaseAllowed,
|
||||||
|
thirdPartyAllowed: value.goldThirdPartyAllowed,
|
||||||
|
disbursementMode: value.goldDisbursementMode,
|
||||||
|
cashDisbursementLimit: value.goldCashDisbursementLimit,
|
||||||
|
ltvBreachAlert: value.goldLtvBreachAlert,
|
||||||
|
auctionReservePct: value.goldAuctionReservePct,
|
||||||
|
redeemBeforeAuction: true
|
||||||
|
},
|
||||||
|
warehouse: {
|
||||||
|
maxLtv: value.wrMaxLtv,
|
||||||
|
receiptType: value.wrReceiptType,
|
||||||
|
tenureWithinReceiptValidity: value.wrTenureWithinReceiptValidity,
|
||||||
|
pledgeMarkedBeforeDisbursement: value.wrPledgeMarkedBeforeDisbursement,
|
||||||
|
commodityGroups: value.wrCommodityGroups,
|
||||||
|
valuationPriceSource: value.wrValuationPriceSource,
|
||||||
|
maxLoanPerQuintal: value.wrMaxLoanPerQuintal,
|
||||||
|
stockInspectionMonths: value.wrStockInspectionMonths,
|
||||||
|
insuranceRequired: value.insuranceRequired,
|
||||||
|
ltvBreachAlert: value.wrLtvBreachAlert,
|
||||||
|
partialReleaseAllowed: value.wrPartialReleaseAllowed,
|
||||||
|
disposalNoticeDays: value.wrDisposalNoticeDays,
|
||||||
|
disposalReservePct: value.wrDisposalReservePct
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -48,6 +48,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (advanceReview) {
|
||||||
|
<mifosx-loan-product-preview-step
|
||||||
|
[loanProduct]="advanceReview.product"
|
||||||
|
[loanProductsTemplate]="advanceReview.template"
|
||||||
|
[readOnly]="true"
|
||||||
|
></mifosx-loan-product-preview-step>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Summary content (rendered by shared component) -->
|
<!-- Summary content (rendered by shared component) -->
|
||||||
<div class="pv-legacy">
|
<div class="pv-legacy">
|
||||||
<mifosx-loan-product-summary
|
<mifosx-loan-product-summary
|
||||||
|
|||||||
+42
-1
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { LoanProduct } from '../../models/loan-product.model';
|
import { LoanProduct } from '../../models/loan-product.model';
|
||||||
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
|
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
|
||||||
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
|
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
|
||||||
@@ -19,6 +20,8 @@ import { SettingsService } from 'app/settings/settings.service';
|
|||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import { LoanProductSummaryComponent } from '../../common/loan-product-summary/loan-product-summary.component';
|
import { LoanProductSummaryComponent } from '../../common/loan-product-summary/loan-product-summary.component';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
import { LoanProductPreviewStepComponent } from '../../loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component';
|
||||||
|
import { reviewProductOf } from '../../loan-product-original.util';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-general-tab',
|
selector: 'mifosx-general-tab',
|
||||||
@@ -27,7 +30,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|||||||
imports: [
|
imports: [
|
||||||
...STANDALONE_SHARED_IMPORTS,
|
...STANDALONE_SHARED_IMPORTS,
|
||||||
FaIconComponent,
|
FaIconComponent,
|
||||||
LoanProductSummaryComponent
|
LoanProductSummaryComponent,
|
||||||
|
LoanProductPreviewStepComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class GeneralTabComponent implements OnInit {
|
export class GeneralTabComponent implements OnInit {
|
||||||
@@ -37,8 +41,11 @@ export class GeneralTabComponent implements OnInit {
|
|||||||
private productsService = inject(ProductsService);
|
private productsService = inject(ProductsService);
|
||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
private translateService = inject(TranslateService);
|
private translateService = inject(TranslateService);
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
|
||||||
loanProduct: LoanProduct;
|
loanProduct: LoanProduct;
|
||||||
|
/** An Advances product: its own settings live in the extension record the standard summary does not read. */
|
||||||
|
advanceReview: { product: any; template: any } | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.route.data.subscribe((data: { loanProduct: any }) => {
|
this.route.data.subscribe((data: { loanProduct: any }) => {
|
||||||
@@ -47,11 +54,45 @@ export class GeneralTabComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
const savedGroup = (this.loanProduct as any)?.advanceExtensions?.productTypeGroup;
|
||||||
|
if (savedGroup === 'advances' || savedGroup === 'loans') {
|
||||||
|
this.advanceReview = reviewProductOf(this.loanProduct);
|
||||||
|
this.nameSuspenseGl();
|
||||||
|
}
|
||||||
this.loanProduct.allowAttributeConfiguration = Object.values(this.loanProduct.allowAttributeOverrides).some(
|
this.loanProduct.allowAttributeConfiguration = Object.values(this.loanProduct.allowAttributeOverrides).some(
|
||||||
(attribute: boolean) => attribute
|
(attribute: boolean) => attribute
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The interest suspense GL lives on the extension record, not in the accounting mappings: look its name up. */
|
||||||
|
private nameSuspenseGl(): void {
|
||||||
|
const id = this.advanceReview?.product?.undebitedInterestGlId;
|
||||||
|
if (id === undefined || id === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.http.get(`/glaccounts/${id}`).subscribe({
|
||||||
|
next: (account: any) => {
|
||||||
|
const current = this.advanceReview;
|
||||||
|
const options = current.template.accountingMappingOptions;
|
||||||
|
// A new object, so the Review reader rebuilds with the name.
|
||||||
|
this.advanceReview = {
|
||||||
|
product: { ...current.product },
|
||||||
|
template: {
|
||||||
|
accountingMappingOptions: {
|
||||||
|
...options,
|
||||||
|
assetAccountOptions: [
|
||||||
|
...options.assetAccountOptions,
|
||||||
|
{ id: account.id, glCode: account.glCode, name: account.name }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
/* the id stays as the fallback text */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
exportDefinition(): void {
|
exportDefinition(): void {
|
||||||
const product = this.loanProduct;
|
const product = this.loanProduct;
|
||||||
delete product['id'];
|
delete product['id'];
|
||||||
|
|||||||
+38
-1
@@ -107,7 +107,44 @@ export class ProductEditDiffDialogComponent implements OnInit {
|
|||||||
writeOffAccountId: 'Write-off GL',
|
writeOffAccountId: 'Write-off GL',
|
||||||
overpaymentLiabilityAccountId: 'Overpayment GL',
|
overpaymentLiabilityAccountId: 'Overpayment GL',
|
||||||
receivableInterestAccountId: 'Interest receivable GL',
|
receivableInterestAccountId: 'Interest receivable GL',
|
||||||
undebitedInterestGlId: 'Interest suspense GL'
|
undebitedInterestGlId: 'Interest suspense GL',
|
||||||
|
goldMaxLtv: 'Gold: maximum LTV cap (%)',
|
||||||
|
goldMinPurityCarat: 'Gold: minimum purity (carat)',
|
||||||
|
goldRateSource: 'Gold: valuation rate source',
|
||||||
|
goldMaxPerGram: 'Gold: max loan per gram (₹)',
|
||||||
|
goldMinNetWeight: 'Gold: minimum net weight (g)',
|
||||||
|
goldMaxNetWeightPerCustomer: 'Gold: max net weight per customer (g)',
|
||||||
|
goldAllowedItems: 'Gold: allowed items',
|
||||||
|
goldAppraiserRequired: 'Gold: appraiser required',
|
||||||
|
goldValuationCertificateRequired: 'Gold: valuation certificate required',
|
||||||
|
goldMaxValuationPerAppraiser: 'Gold: max valuation per appraiser (₹)',
|
||||||
|
goldPacketPrefix: 'Gold: packet number prefix',
|
||||||
|
goldRevalueOnNpa: 'Gold: revalue when loan turns NPA',
|
||||||
|
goldRevaluationChargeApplicable: 'Gold: charge customer for revaluation',
|
||||||
|
insuranceRequired: 'Insurance required',
|
||||||
|
goldAuctionNoticeDays: 'Gold: auction notice (days)',
|
||||||
|
goldPartialReleaseAllowed: 'Gold: partial release of ornaments',
|
||||||
|
goldThirdPartyAllowed: 'Gold: third-party gold allowed',
|
||||||
|
goldDisbursementMode: 'Gold: disbursement mode',
|
||||||
|
goldCashDisbursementLimit: 'Gold: cash disbursement limit (₹)',
|
||||||
|
goldLtvBreachAlert: 'Gold: LTV breach alert (%)',
|
||||||
|
goldAuctionReservePct: 'Gold: auction reserve price (%)',
|
||||||
|
goldRedeemBeforeAuction: 'Gold: customer may redeem before auction',
|
||||||
|
wrMaxLtv: 'Warehouse: maximum LTV cap (%)',
|
||||||
|
wrReceiptType: 'Warehouse: receipt type allowed',
|
||||||
|
wrWdraRegistered: 'Warehouse: WDRA-registered warehouse',
|
||||||
|
wrAssayCertificateRequired: 'Warehouse: assay certificate required',
|
||||||
|
wrTenureWithinReceiptValidity: 'Warehouse: tenure within receipt validity',
|
||||||
|
wrPledgeMarkedBeforeDisbursement: 'Warehouse: pledge marked before disbursement',
|
||||||
|
wrCommodityGroups: 'Warehouse: eligible commodity groups',
|
||||||
|
wrValuationPriceSource: 'Warehouse: valuation price source',
|
||||||
|
wrMaxLoanPerQuintal: 'Warehouse: max loan per quintal (₹)',
|
||||||
|
wrStockInspectionMonths: 'Warehouse: stock inspection (months)',
|
||||||
|
wrLtvBreachAlert: 'Warehouse: LTV breach alert (%)',
|
||||||
|
wrPartialReleaseAllowed: 'Warehouse: partial release of stock',
|
||||||
|
wrDisposalNoticeDays: 'Warehouse: disposal notice (days)',
|
||||||
|
wrDisposalReservePct: 'Warehouse: disposal reserve price (%)',
|
||||||
|
wrRedeemBeforeDisposal: 'Warehouse: customer may redeem before disposal'
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The two reserved otherGlMappings entries a checker actually needs to see by name -- see
|
/** The two reserved otherGlMappings entries a checker actually needs to see by name -- see
|
||||||
|
|||||||
Reference in New Issue
Block a user