feat(loan-origination): product type, income/co-borrower affordability, bank statement & CIC analysis, richer Field Investigation, and meeting-scheduled Personal Discussion
- New Application: add Product Type (Term Loan/Overdraft) driven by product eligibility. - KYC: 3-year ITR capture with exceptions, product-specific document checklists with officer-added custom docs, Housing Loan co-owner Aadhaar/PAN capture+verify in a popup dialog. - Income & Affordability: editable income/amount/tenure with product bounds validation, 40% EMI rule, and a full co-borrower flow (relationship, salary slips, combined-income eligibility). - Bank Statement upload auto-opens a full CSV-driven cash-flow/FOIR/risk analysis dialog. - CIC report upload auto-opens a bureau-style credit report analysis (score gauge, accounts, DPD history, enquiries) generated from the application's own credit score. - Field Investigation reworked into per-address cards (Home/Office/Property/Shop) with officer assignment, 48-hour SLA tracking, GPS + multi-photo capture, verify/reject with remarks, a full-detail popup, real Fineract addresses for existing clients, and a calculated property valuation (market/guideline/realizable value, LTV). - Personal Discussion: meeting scheduler (date/time/mode/officer) gating the discussion report, plus a full Application Summary Report PDF (KYC, CIC, FI, valuation, stage history) for the meeting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -152,6 +152,16 @@ export interface VerificationCheck {
|
||||
verifiedBy: string;
|
||||
verifiedDate: string;
|
||||
remarks: string;
|
||||
/** Field Investigation only -- the officer assigned to physically visit this address, when they
|
||||
* were assigned (both a display date and a raw epoch-ms timestamp for the 48-hour SLA clock),
|
||||
* and what they captured on the visit (GPS + a photo count -- the actual File objects are kept
|
||||
* local-only in the component, same as every other document capture in this mock pipeline). */
|
||||
officerAssigned: string;
|
||||
assignedDate: string;
|
||||
assignedAt: number;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
photoCount: number;
|
||||
}
|
||||
|
||||
export interface ApprovalRecord {
|
||||
@@ -188,6 +198,9 @@ export interface LoanApplication {
|
||||
|
||||
productCode: string;
|
||||
productName: string;
|
||||
/** Term Loan (fixed tenure, EMI-based) vs Overdraft (revolving limit, renewed annually) --
|
||||
* see allowedProductTypes() for which products can be which. */
|
||||
productType: ProductType;
|
||||
secured: boolean;
|
||||
requestedAmount: number;
|
||||
tenureMonths: number;
|
||||
@@ -203,11 +216,11 @@ export interface LoanApplication {
|
||||
panVerified: boolean;
|
||||
incomeProofSubmitted: boolean;
|
||||
bankStatementSubmitted: boolean;
|
||||
propertyDetailsSubmitted: boolean;
|
||||
taxReceiptSubmitted: boolean;
|
||||
electricityBillSubmitted: boolean;
|
||||
/** Aadhaar e-KYC biometric authentication (UIDAI) — captured on top of the OVD paper check. */
|
||||
aadhaarFingerprintCaptured: boolean;
|
||||
/** Documents specific to this application's loan product (e.g. property papers for a Housing
|
||||
* Loan, land records for a Kisan Credit Card) -- seeded from PRODUCT_DOCUMENT_REQUIREMENTS at
|
||||
* creation time, plus whatever the officer adds on top with "Add Custom Document". */
|
||||
productDocuments: ProductDocumentEntry[];
|
||||
monthlyIncome: number;
|
||||
eligibleLoanAmount: number;
|
||||
coBorrowerRequired: boolean;
|
||||
@@ -248,12 +261,34 @@ export interface LoanApplication {
|
||||
addressVerification: VerificationCheck;
|
||||
officeVerification: VerificationCheck;
|
||||
propertyVerification: VerificationCheck;
|
||||
/** Whether this applicant runs a shop/business premises worth sending a field officer to --
|
||||
* officer-ticked at Field Investigation itself (not derivable from anything else on the
|
||||
* application), same conditional-row pattern propertyVerification already uses off `secured`. */
|
||||
hasShop: boolean;
|
||||
shopVerification: VerificationCheck;
|
||||
/** propertyValuation itself is always derived (builtUpAreaSqFt × marketRatePerSqFt) -- kept as
|
||||
* its own field since every other part of the app already reads it directly (stage-history
|
||||
* remarks, Risk Approval), rather than making every one of those call sites recompute it. */
|
||||
propertyType: string;
|
||||
builtUpAreaSqFt: number;
|
||||
marketRatePerSqFt: number;
|
||||
guidelineRatePerSqFt: number;
|
||||
valuerName: string;
|
||||
valuationDate: string;
|
||||
propertyValuation: number;
|
||||
agentReportUploaded: boolean;
|
||||
agentReportFileName: string;
|
||||
agentReportRemark: string;
|
||||
|
||||
// Personal Discussion / discrepancy handling
|
||||
discrepancyType: DiscrepancyType;
|
||||
/** Scheduled before the meeting happens -- date/time/mode/officer -- so there's a real
|
||||
* appointment on record instead of the discussion notes just appearing from nowhere. */
|
||||
meetingScheduled: boolean;
|
||||
meetingDate: string;
|
||||
meetingTime: string;
|
||||
meetingMode: 'ONLINE' | 'OFFLINE' | 'CALL' | '';
|
||||
meetingWith: string;
|
||||
personalDiscussionNotes: string;
|
||||
personalDiscussionDate: string;
|
||||
/** Customer's signed declaration taken after the personal discussion clears. */
|
||||
@@ -468,6 +503,80 @@ export const ORIGINATION_PRODUCTS: OriginationProduct[] = [
|
||||
}
|
||||
];
|
||||
|
||||
/** One product-specific document a loan officer has to collect for a given product's collateral/
|
||||
* eligibility type -- distinct from the generic KYC docs (Aadhaar, PAN, Income Proof, ...) every
|
||||
* application needs regardless of product. */
|
||||
export interface ProductDocumentEntry {
|
||||
key: string;
|
||||
label: string;
|
||||
hint?: string;
|
||||
/** True for a document the officer added on the spot with "Add Custom Document" -- these can
|
||||
* be removed again; the product's own required list cannot. */
|
||||
custom: boolean;
|
||||
submitted: boolean;
|
||||
}
|
||||
|
||||
interface ProductDocumentTemplate {
|
||||
key: string;
|
||||
label: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
/** What each product needs beyond the generic KYC checklist -- a Housing or Kisan Credit loan is
|
||||
* secured against a specific asset (property / agricultural land), so the paperwork proving and
|
||||
* encumbering that asset has to be collected before the loan can be sanctioned, on top of the
|
||||
* borrower's own identity/income proof. */
|
||||
export const PRODUCT_DOCUMENT_REQUIREMENTS: Record<string, ProductDocumentTemplate[]> = {
|
||||
HL: [
|
||||
{ key: 'propertyTaxReceipt', label: 'Property Tax Receipt' },
|
||||
{ key: 'propertyRegistration', label: 'Property Registration Document (Sale Deed)' },
|
||||
{ key: 'propertyElectricityBill', label: 'Electricity Bill (of the Property)' },
|
||||
{
|
||||
key: 'coOwnerKyc',
|
||||
label: 'KYC Documents of Co-Owners',
|
||||
hint: 'If the registry lists a second co-owner, use "Add More Doc" below for their KYC too'
|
||||
},
|
||||
{ key: 'mutationExtract', label: 'Property Mutation Extract (Namantaran)' },
|
||||
{ key: 'noDuesCertificate', label: 'No-Dues Certificate' },
|
||||
{ key: 'encumbranceCertificate', label: 'Encumbrance Certificate (EC)' },
|
||||
{ key: 'approvedBuildingPlan', label: 'Approved Building Plan / Sanctioned Layout' }
|
||||
],
|
||||
KCC: [
|
||||
{ key: 'landRegistry', label: 'Land Registry / Title Deed' },
|
||||
{ key: 'khasraKhatauni', label: 'Khasra-Khatauni (Land Revenue Record)' },
|
||||
{ key: 'girdawari', label: 'Girdawari (Crop Inspection Report)' },
|
||||
{ key: 'noDuesOtherBank', label: 'No-Dues Certificate from Other Banks/Societies' }
|
||||
],
|
||||
VL: [
|
||||
{ key: 'vehicleQuotation', label: 'Vehicle Quotation / Proforma Invoice' },
|
||||
{ key: 'drivingLicense', label: 'Driving License' },
|
||||
{ key: 'vehicleRC', label: 'RC Copy', hint: 'For a used vehicle' },
|
||||
{ key: 'vehicleInsurance', label: 'Insurance Copy' }
|
||||
],
|
||||
CC: [
|
||||
{ key: 'udyamRegistration', label: 'Udyam / Business Registration Certificate' },
|
||||
{ key: 'gstCertificate', label: 'GST Registration Certificate' },
|
||||
{ key: 'businessFinancials', label: "Last 2 Years' Business Financial Statements" },
|
||||
{ key: 'businessAddressProof', label: 'Business Address Proof' },
|
||||
{ key: 'partnershipDeed', label: 'Partnership Deed / MOA-AOA', hint: 'If applicable' }
|
||||
],
|
||||
EL: [
|
||||
{ key: 'admissionLetter', label: 'Admission Letter from Institution' },
|
||||
{ key: 'feeStructure', label: 'Fee Structure / Schedule' },
|
||||
{ key: 'marksheets', label: 'Previous Qualifying Exam Marksheets' },
|
||||
{ key: 'coBorrowerKyc', label: 'Co-Borrower (Parent/Guardian) KYC & Income Proof' }
|
||||
],
|
||||
GL: [
|
||||
{ key: 'goldValuation', label: 'Gold Appraisal / Valuation Certificate' },
|
||||
{ key: 'goldPurchaseInvoice', label: 'Purchase Invoice of Gold Ornaments', hint: 'If available' }
|
||||
],
|
||||
PL: [{ key: 'employmentProof', label: 'Employment Proof / Appointment Letter' }]
|
||||
};
|
||||
|
||||
export function buildProductDocuments(productCode: string): ProductDocumentEntry[] {
|
||||
return (PRODUCT_DOCUMENT_REQUIREMENTS[productCode] ?? []).map((doc) => ({ ...doc, custom: false, submitted: false }));
|
||||
}
|
||||
|
||||
const FIRST_NAMES = [
|
||||
'Ramesh',
|
||||
'Suresh',
|
||||
@@ -534,7 +643,7 @@ const BC_AGENTS = [
|
||||
'Digital Seva Kendra — Sangli'
|
||||
];
|
||||
|
||||
const MANAGERS = [
|
||||
export const MANAGERS = [
|
||||
'Anil Deshpande (Branch Manager)',
|
||||
'Sunil Kadam (Branch Manager)',
|
||||
'Vikas Pathak (Credit Manager)',
|
||||
@@ -558,6 +667,20 @@ const FI_AGENCIES = [
|
||||
'VerifyIndia Field Services'
|
||||
];
|
||||
|
||||
export const PROPERTY_TYPES = [
|
||||
'Residential',
|
||||
'Commercial',
|
||||
'Plot',
|
||||
'Under Construction'
|
||||
];
|
||||
|
||||
const VALUERS = [
|
||||
'Apex Valuers & Engineers',
|
||||
'Prime Property Valuation Services',
|
||||
'National Valuation Associates',
|
||||
'Metro Approved Valuers'
|
||||
];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Delegation of Powers — sanctioning authority by loan amount, exactly the kind
|
||||
// of amount-based approval matrix every Indian bank/co-operative society runs.
|
||||
@@ -607,7 +730,18 @@ export function stageBadgeClass(app: LoanApplication): string {
|
||||
}
|
||||
|
||||
function blankVerification(): VerificationCheck {
|
||||
return { status: 'PENDING', verifiedBy: '—', verifiedDate: '—', remarks: '—' };
|
||||
return {
|
||||
status: 'PENDING',
|
||||
verifiedBy: '—',
|
||||
verifiedDate: '—',
|
||||
remarks: '—',
|
||||
officerAssigned: '—',
|
||||
assignedDate: '—',
|
||||
assignedAt: 0,
|
||||
latitude: '',
|
||||
longitude: '',
|
||||
photoCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
function blankApproval(): ApprovalRecord {
|
||||
@@ -703,6 +837,14 @@ function generateApplications(count: number): LoanApplication[] {
|
||||
const legalOfficer = pick(LEGAL_OFFICERS);
|
||||
const fiAgency = pick(FI_AGENCIES);
|
||||
|
||||
// Reverse-engineered the same way monthlyIncome is -- area × rate lands close to a plausible
|
||||
// multiple of the requested amount, rather than picking propertyValuation directly and having
|
||||
// no consistent area/rate breakdown behind it.
|
||||
const propertyBuiltUpAreaSqFt = product.secured ? round(between(600, 2400)) : 0;
|
||||
const propertyMarketRatePerSqFt = product.secured
|
||||
? round((requestedAmount * between(1.1, 1.4)) / propertyBuiltUpAreaSqFt)
|
||||
: 0;
|
||||
|
||||
const app: LoanApplication = {
|
||||
applicationNo: `LN${branch.code.slice(2)}${product.code}${seq}`,
|
||||
applicationDate,
|
||||
@@ -714,6 +856,7 @@ function generateApplications(count: number): LoanApplication[] {
|
||||
customerType,
|
||||
productCode: product.code,
|
||||
productName: product.name,
|
||||
productType: pick(allowedProductTypes(product.code)),
|
||||
secured: product.secured,
|
||||
requestedAmount,
|
||||
tenureMonths,
|
||||
@@ -725,10 +868,8 @@ function generateApplications(count: number): LoanApplication[] {
|
||||
panVerified: false,
|
||||
incomeProofSubmitted: false,
|
||||
bankStatementSubmitted: false,
|
||||
propertyDetailsSubmitted: false,
|
||||
taxReceiptSubmitted: false,
|
||||
electricityBillSubmitted: false,
|
||||
aadhaarFingerprintCaptured: false,
|
||||
productDocuments: buildProductDocuments(product.code),
|
||||
monthlyIncome,
|
||||
eligibleLoanAmount,
|
||||
coBorrowerRequired: eligibleLoanAmount < requestedAmount,
|
||||
@@ -750,10 +891,24 @@ function generateApplications(count: number): LoanApplication[] {
|
||||
addressVerification: blankVerification(),
|
||||
officeVerification: blankVerification(),
|
||||
propertyVerification: blankVerification(),
|
||||
propertyValuation: product.secured ? round(requestedAmount * between(1.1, 1.4)) : 0,
|
||||
hasShop: rng() < 0.3,
|
||||
shopVerification: blankVerification(),
|
||||
propertyType: product.secured ? pick(PROPERTY_TYPES) : '—',
|
||||
builtUpAreaSqFt: propertyBuiltUpAreaSqFt,
|
||||
marketRatePerSqFt: propertyMarketRatePerSqFt,
|
||||
guidelineRatePerSqFt: product.secured ? round(propertyMarketRatePerSqFt * between(0.6, 0.85)) : 0,
|
||||
valuerName: product.secured ? pick(VALUERS) : '—',
|
||||
valuationDate: product.secured ? fmt(addDays(today, -round(between(5, 60)))) : '—',
|
||||
propertyValuation: product.secured ? propertyBuiltUpAreaSqFt * propertyMarketRatePerSqFt : 0,
|
||||
agentReportUploaded: false,
|
||||
agentReportFileName: '—',
|
||||
agentReportRemark: '—',
|
||||
discrepancyType: 'NONE',
|
||||
meetingScheduled: false,
|
||||
meetingDate: '',
|
||||
meetingTime: '',
|
||||
meetingMode: '',
|
||||
meetingWith: '',
|
||||
personalDiscussionNotes: '—',
|
||||
personalDiscussionDate: '—',
|
||||
declarationTaken: false,
|
||||
@@ -836,10 +991,8 @@ function generateApplications(count: number): LoanApplication[] {
|
||||
app.panVerified = true;
|
||||
app.incomeProofSubmitted = true;
|
||||
app.bankStatementSubmitted = true;
|
||||
app.propertyDetailsSubmitted = product.secured;
|
||||
app.taxReceiptSubmitted = product.secured;
|
||||
app.electricityBillSubmitted = true;
|
||||
app.aadhaarFingerprintCaptured = true;
|
||||
app.productDocuments.forEach((doc) => (doc.submitted = true));
|
||||
app.stageHistory.push({
|
||||
stage,
|
||||
date: fmt(cursor),
|
||||
@@ -876,24 +1029,57 @@ function generateApplications(count: number): LoanApplication[] {
|
||||
status: 'VERIFIED',
|
||||
verifiedBy: fiAgency,
|
||||
verifiedDate: fmt(cursor),
|
||||
remarks: 'Registered address confirmed on physical visit.'
|
||||
remarks: 'Registered address confirmed on physical visit.',
|
||||
officerAssigned: fiAgency,
|
||||
assignedDate: fmt(cursor),
|
||||
assignedAt: cursor.getTime(),
|
||||
latitude: (23.2 + rng() * 5).toFixed(6),
|
||||
longitude: (77.4 + rng() * 5).toFixed(6),
|
||||
photoCount: 2 + Math.floor(rng() * 3)
|
||||
};
|
||||
app.officeVerification = {
|
||||
status: 'VERIFIED',
|
||||
verifiedBy: fiAgency,
|
||||
verifiedDate: fmt(cursor),
|
||||
remarks: 'Employment/business confirmed — Agent Report filed.'
|
||||
remarks: 'Employment/business confirmed — Agent Report filed.',
|
||||
officerAssigned: fiAgency,
|
||||
assignedDate: fmt(cursor),
|
||||
assignedAt: cursor.getTime(),
|
||||
latitude: (23.2 + rng() * 5).toFixed(6),
|
||||
longitude: (77.4 + rng() * 5).toFixed(6),
|
||||
photoCount: 2 + Math.floor(rng() * 3)
|
||||
};
|
||||
if (product.secured) {
|
||||
app.propertyVerification = {
|
||||
status: 'VERIFIED',
|
||||
verifiedBy: fiAgency,
|
||||
verifiedDate: fmt(cursor),
|
||||
remarks: `Property inspected and valued at ₹${app.propertyValuation.toLocaleString('en-IN')}.`
|
||||
remarks: `Property inspected and valued at ₹${app.propertyValuation.toLocaleString('en-IN')}.`,
|
||||
officerAssigned: fiAgency,
|
||||
assignedDate: fmt(cursor),
|
||||
assignedAt: cursor.getTime(),
|
||||
latitude: (23.2 + rng() * 5).toFixed(6),
|
||||
longitude: (77.4 + rng() * 5).toFixed(6),
|
||||
photoCount: 2 + Math.floor(rng() * 3)
|
||||
};
|
||||
}
|
||||
if (app.hasShop) {
|
||||
app.shopVerification = {
|
||||
status: 'VERIFIED',
|
||||
verifiedBy: fiAgency,
|
||||
verifiedDate: fmt(cursor),
|
||||
remarks: 'Shop/business premises visited and confirmed operational.',
|
||||
officerAssigned: fiAgency,
|
||||
assignedDate: fmt(cursor),
|
||||
assignedAt: cursor.getTime(),
|
||||
latitude: (23.2 + rng() * 5).toFixed(6),
|
||||
longitude: (77.4 + rng() * 5).toFixed(6),
|
||||
photoCount: 2 + Math.floor(rng() * 3)
|
||||
};
|
||||
}
|
||||
app.agentReportUploaded = true;
|
||||
app.agentReportFileName = `AgentReport_${app.applicationNo}.pdf`;
|
||||
app.agentReportRemark = 'All addresses visited and cross-verified — no discrepancies noted.';
|
||||
app.stageHistory.push({
|
||||
stage,
|
||||
date: fmt(cursor),
|
||||
@@ -906,6 +1092,15 @@ function generateApplications(count: number): LoanApplication[] {
|
||||
break;
|
||||
|
||||
case 'PERSONAL_DISCUSSION':
|
||||
app.meetingScheduled = true;
|
||||
app.meetingDate = fmt(cursor);
|
||||
app.meetingTime = `${String(10 + Math.floor(rng() * 6)).padStart(2, '0')}:${rng() < 0.5 ? '00' : '30'}`;
|
||||
app.meetingMode = pick<'ONLINE' | 'OFFLINE' | 'CALL'>([
|
||||
'ONLINE',
|
||||
'OFFLINE',
|
||||
'CALL'
|
||||
]);
|
||||
app.meetingWith = manager;
|
||||
if (isHoldStage) {
|
||||
app.discrepancyType = pick<DiscrepancyType>([
|
||||
'DOCUMENT_DISCREPANCY',
|
||||
@@ -1174,6 +1369,30 @@ export function moveToStage(app: LoanApplication, stage: OriginationStage, actio
|
||||
app.currentStage = stage;
|
||||
}
|
||||
|
||||
/** Books the Personal Discussion appointment -- date, time, mode and which bank officer is
|
||||
* taking it -- before any discussion notes exist, so there's a real scheduled meeting on record
|
||||
* rather than a discussion report just appearing with no appointment behind it. */
|
||||
export function scheduleMeeting(
|
||||
app: LoanApplication,
|
||||
date: string,
|
||||
time: string,
|
||||
mode: 'ONLINE' | 'OFFLINE' | 'CALL',
|
||||
withWhom: string,
|
||||
actionBy: string
|
||||
): void {
|
||||
app.meetingScheduled = true;
|
||||
app.meetingDate = date;
|
||||
app.meetingTime = time;
|
||||
app.meetingMode = mode;
|
||||
app.meetingWith = withWhom;
|
||||
recordStageProgress(
|
||||
app,
|
||||
actionBy,
|
||||
'Meeting Scheduled',
|
||||
`Personal Discussion scheduled with ${withWhom} on ${date} ${time} (${mode}).`
|
||||
);
|
||||
}
|
||||
|
||||
export function flagDiscrepancy(app: LoanApplication, type: DiscrepancyType, actionBy: string, notes: string): void {
|
||||
app.discrepancyType = type;
|
||||
app.personalDiscussionDate = todayDDMMYYYY();
|
||||
@@ -1264,20 +1483,48 @@ export function pushFieldInquiry(app: LoanApplication, agencyName: string, actio
|
||||
recordStageProgress(app, actionBy, 'FI Pushed', `Field Inquiry request pushed to ${agencyName}.`);
|
||||
}
|
||||
|
||||
export function uploadAgentReport(app: LoanApplication, fileName: string, actionBy: string): void {
|
||||
export function uploadAgentReport(app: LoanApplication, fileName: string, remark: string, actionBy: string): void {
|
||||
app.agentReportUploaded = true;
|
||||
app.agentReportFileName = fileName;
|
||||
recordStageProgress(app, actionBy, 'Uploaded', `Agent Report "${fileName}" uploaded.`);
|
||||
app.agentReportRemark = remark || 'Agent Report filed.';
|
||||
recordStageProgress(app, actionBy, 'Uploaded', `Agent Report "${fileName}" uploaded — ${app.agentReportRemark}`);
|
||||
}
|
||||
|
||||
export type FiCheckKey = 'addressVerification' | 'officeVerification' | 'propertyVerification' | 'shopVerification';
|
||||
|
||||
export const FI_CHECK_LABELS: Record<FiCheckKey, string> = {
|
||||
addressVerification: 'Home Address',
|
||||
officeVerification: 'Office / Employment Address',
|
||||
propertyVerification: 'Property Address',
|
||||
shopVerification: 'Shop / Business Premises'
|
||||
};
|
||||
|
||||
/** Sends a field officer out to physically visit and verify one of the 3 FI addresses -- resets
|
||||
* that address's whole visit cycle (GPS, photo count, prior remarks) fresh, since a newly
|
||||
* assigned visit (including a retry after a FAILED one) starts the 48-hour SLA clock over. */
|
||||
export function assignFieldOfficer(app: LoanApplication, which: FiCheckKey, officerName: string): void {
|
||||
app[which] = {
|
||||
status: 'PENDING',
|
||||
verifiedBy: '—',
|
||||
verifiedDate: '—',
|
||||
remarks: '—',
|
||||
officerAssigned: officerName,
|
||||
assignedDate: todayDDMMYYYY(),
|
||||
assignedAt: Date.now(),
|
||||
latitude: '',
|
||||
longitude: '',
|
||||
photoCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
export function recordVerification(
|
||||
app: LoanApplication,
|
||||
which: 'addressVerification' | 'officeVerification' | 'propertyVerification',
|
||||
which: FiCheckKey,
|
||||
status: VerificationStatus,
|
||||
verifiedBy: string,
|
||||
remarks: string
|
||||
): void {
|
||||
app[which] = { status, verifiedBy, verifiedDate: todayDDMMYYYY(), remarks };
|
||||
app[which] = { ...app[which], status, verifiedBy, verifiedDate: todayDDMMYYYY(), remarks };
|
||||
}
|
||||
|
||||
/** Cooperative-bank membership can be allotted independently of the loan's own progress — a
|
||||
@@ -1348,14 +1595,45 @@ export function disburseApplication(app: LoanApplication, actionBy: string, mode
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// EMI Repayment Schedule — the standard reducing-balance amortisation table a real CBS shows
|
||||
// once a term loan is disbursed. Cash Credit / KCC are revolving limits renewed annually, not
|
||||
// repaid on a fixed instalment schedule, so they're excluded rather than shown a misleading one.
|
||||
// Product Type — Term Loan (fixed tenure, EMI-based) vs Overdraft (revolving limit, renewed
|
||||
// annually rather than repaid on a fixed instalment schedule).
|
||||
// -------------------------------------------------------------------------
|
||||
export function isRevolvingProduct(productCode: string): boolean {
|
||||
return productCode === 'CC' || productCode === 'KCC';
|
||||
export type ProductType = 'TERM_LOAN' | 'OVERDRAFT';
|
||||
|
||||
export const PRODUCT_TYPE_LABELS: Record<ProductType, string> = {
|
||||
TERM_LOAN: 'Term Loan',
|
||||
OVERDRAFT: 'Overdraft'
|
||||
};
|
||||
|
||||
/** Which Product Type(s) a given product can be sanctioned as. Cash Credit and Kisan Credit Card
|
||||
* are inherently revolving limits, so Overdraft is the only option. Gold Loan is commonly
|
||||
* written up either way at a UCB -- a straight EMI term loan, or a 1-year overdraft/cash-credit
|
||||
* limit renewed annually -- so both are offered, defaulting to Overdraft (the more common
|
||||
* cooperative-bank practice for gold loans). Every other product here is a straight term loan. */
|
||||
export function allowedProductTypes(productCode: string): ProductType[] {
|
||||
if (productCode === 'CC' || productCode === 'KCC') {
|
||||
return ['OVERDRAFT'];
|
||||
}
|
||||
if (productCode === 'GL') {
|
||||
return [
|
||||
'OVERDRAFT',
|
||||
'TERM_LOAN'
|
||||
];
|
||||
}
|
||||
return ['TERM_LOAN'];
|
||||
}
|
||||
|
||||
export function defaultProductType(productCode: string): ProductType {
|
||||
return allowedProductTypes(productCode)[0];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// EMI Repayment Schedule — the standard reducing-balance amortisation table a real CBS shows
|
||||
// once a term loan is disbursed. Overdraft-type products are revolving limits renewed annually,
|
||||
// not repaid on a fixed instalment schedule, so they're excluded rather than shown a misleading
|
||||
// one (see LoanApplication.productType).
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
export interface EmiScheduleRow {
|
||||
installmentNo: number;
|
||||
dueDate: string;
|
||||
@@ -1426,6 +1704,7 @@ export interface NewApplicationInput {
|
||||
branchName: string;
|
||||
productCode: string;
|
||||
productName: string;
|
||||
productType: ProductType;
|
||||
secured: boolean;
|
||||
requestedAmount: number;
|
||||
tenureMonths: number;
|
||||
@@ -1447,6 +1726,7 @@ export function createApplication(input: NewApplicationInput, seq: number): Loan
|
||||
clientId: input.clientId ?? null,
|
||||
productCode: input.productCode,
|
||||
productName: input.productName,
|
||||
productType: input.productType,
|
||||
secured: input.secured,
|
||||
requestedAmount: input.requestedAmount,
|
||||
tenureMonths: input.tenureMonths,
|
||||
@@ -1458,10 +1738,8 @@ export function createApplication(input: NewApplicationInput, seq: number): Loan
|
||||
panVerified: false,
|
||||
incomeProofSubmitted: false,
|
||||
bankStatementSubmitted: false,
|
||||
propertyDetailsSubmitted: false,
|
||||
taxReceiptSubmitted: false,
|
||||
electricityBillSubmitted: false,
|
||||
aadhaarFingerprintCaptured: false,
|
||||
productDocuments: buildProductDocuments(input.productCode),
|
||||
monthlyIncome: 0,
|
||||
eligibleLoanAmount: 0,
|
||||
coBorrowerRequired: false,
|
||||
@@ -1483,10 +1761,24 @@ export function createApplication(input: NewApplicationInput, seq: number): Loan
|
||||
addressVerification: blankVerification(),
|
||||
officeVerification: blankVerification(),
|
||||
propertyVerification: blankVerification(),
|
||||
hasShop: false,
|
||||
shopVerification: blankVerification(),
|
||||
propertyType: '—',
|
||||
builtUpAreaSqFt: 0,
|
||||
marketRatePerSqFt: 0,
|
||||
guidelineRatePerSqFt: 0,
|
||||
valuerName: '—',
|
||||
valuationDate: '—',
|
||||
propertyValuation: 0,
|
||||
agentReportUploaded: false,
|
||||
agentReportFileName: '—',
|
||||
agentReportRemark: '—',
|
||||
discrepancyType: 'NONE',
|
||||
meetingScheduled: false,
|
||||
meetingDate: '',
|
||||
meetingTime: '',
|
||||
meetingMode: '',
|
||||
meetingWith: '',
|
||||
personalDiscussionNotes: '—',
|
||||
personalDiscussionDate: '—',
|
||||
declarationTaken: false,
|
||||
|
||||
Reference in New Issue
Block a user