/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { ChangeKind, CommandChange } from './command-diff.util'; /** Entity name the maker-checker queue uses for Loan Base Interest Rate submissions. */ export const LOAN_BASE_INTEREST_RATE_ENTITY = 'LOANBASEINTERESTRATE'; export function isLoanBaseInterestRateCreate(entityName?: string | null, actionName?: string | null): boolean { return ( (entityName ?? '').toUpperCase() === LOAN_BASE_INTEREST_RATE_ENTITY && (actionName ?? '').toUpperCase() === 'CREATE' ); } const HEADER_FIELDS: { key: string; label: string }[] = [ { key: 'name', label: 'Name' }, { key: 'effectiveFromDate', label: 'Effective From Date' }, { key: 'currencyCode', label: 'Currency Code' }, { key: 'fallbackNoCibilSlab', label: 'Fallback No CIBIL Slab' }, { key: 'fallbackBelowMin', label: 'Fallback Below Min' }, { key: 'note', label: 'Note' } ]; const NA = 'N.A.'; const EMPTY = '—'; /** One chart cell as the checker reads it: a % rate, a formula, or N.A. */ function cellText(cell: any): string { if (!cell || cell.isNa) { return NA; } if (cell.isFormula && cell.rateFormula) { return String(cell.rateFormula); } return cell.rateValue === null || cell.rateValue === undefined ? NA : `${Number(cell.rateValue)}%`; } function plain(value: any): string { return value === null || value === undefined || value === '' ? EMPTY : String(value); } function kindOf(oldText: string, newText: string, emptyTexts: string[]): ChangeKind { const oldEmpty = emptyTexts.includes(oldText); const newEmpty = emptyTexts.includes(newText); if (oldEmpty && !newEmpty) { return 'ADDED'; } if (!oldEmpty && newEmpty) { return 'REMOVED'; } return 'MODIFIED'; } /** * Compares a submitted Loan Base Interest Rate version against the version currently in force and * returns only the values that differ: header fields, individual rate cells (per loan type and * amount slab) and CIBIL adjustment bands. */ export function diffLoanRateRegister(submitted: any, active: any): CommandChange[] { const changes: CommandChange[] = []; for (const { key, label } of HEADER_FIELDS) { if (submitted?.[key] === undefined) { continue; } const oldText = plain(active?.[key]); const newText = plain(submitted[key]); if (oldText !== newText) { changes.push({ label, kind: kindOf(oldText, newText, [EMPTY]), oldValue: oldText, newValue: newText }); } } // Rate cells, keyed by loan type + slab order. const cellKey = (c: any) => `${c.productSrNo}|${c.slabOrder}`; const oldCells = new Map( (active?.slabs ?? []).map((c: any): [string, any] => [ cellKey(c), c ]) ); const newCells = new Map( (submitted?.slabs ?? []).map((c: any): [string, any] => [ cellKey(c), c ]) ); const names = new Map(); for (const c of active?.slabs ?? []) { if (c.productName) { names.set(c.productSrNo, c.productName); } } for (const p of submitted?.products ?? []) { names.set(p.srNo, p.name); } const allKeys = new Set([ ...newCells.keys(), ...oldCells.keys() ]); const cellChanges = [...allKeys] .map((k) => { const before = oldCells.get(k); const after = newCells.get(k); const ref = after ?? before; return { srNo: Number(ref.productSrNo), slabOrder: Number(ref.slabOrder), label: `${names.get(ref.productSrNo) ?? `Loan type ${ref.productSrNo}`} — ${ref.slabLabel ?? `Slab ${ref.slabOrder + 1}`}`, oldText: before ? cellText(before) : EMPTY, newText: after ? cellText(after) : EMPTY }; }) // A cell that is N.A. in the submission and was never in the old version is not a change. .filter((c) => c.oldText !== c.newText && !(c.oldText === EMPTY && c.newText === NA)) .sort((a, b) => a.srNo - b.srNo || a.slabOrder - b.slabOrder); for (const c of cellChanges) { changes.push({ label: c.label, kind: kindOf(c.oldText, c.newText, [ EMPTY, NA ]), oldValue: c.oldText, newValue: c.newText }); } // CIBIL bands, matched by their score range, so a re-ordered list is not reported as a change. const bandKey = (b: any) => `${b.scoreFrom}-${b.scoreTo}`; const bandText = (b: any) => `${Number(b.adjustmentPct)}% (${b.slabType ?? 'Flat'})`; const oldBands = new Map( (active?.cibilSlabs ?? []).map((b: any): [string, any] => [ bandKey(b), b ]) ); const newBands = new Map( (submitted?.cibilSlabs ?? []).map((b: any): [string, any] => [ bandKey(b), b ]) ); for (const k of new Set([ ...newBands.keys(), ...oldBands.keys() ])) { const before = oldBands.get(k); const after = newBands.get(k); const oldText = before ? bandText(before) : EMPTY; const newText = after ? bandText(after) : EMPTY; if (oldText !== newText) { const ref = after ?? before; changes.push({ label: `CIBIL ${ref.scoreFrom}–${ref.scoreTo}${ref.tier ? ` (${ref.tier})` : ''} — Adjustment`, kind: kindOf(oldText, newText, [EMPTY]), oldValue: oldText, newValue: newText }); } } return changes; }