- Add GL account picker (gl-picker.component.ts) for mapping each product row to a GL account - Extract shared diff logic into rate-register-diff.util.ts + loan-rate-register-diff.util.ts - Add rate-register-diff component to render before/after changes for checker approval - Rewire checker-inbox and view-checker-inbox to show loan interest rate register diffs - Refactor rate-register.service.ts (mock-to-real HTTP wiring, GL-aware payload mapping) - Simplify preview-register component, moving diff rendering into the shared diff component - Add glAccountId field to RateChartRow model Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
170 lines
5.4 KiB
TypeScript
170 lines
5.4 KiB
TypeScript
/**
|
||
* 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<string, any>(
|
||
(active?.slabs ?? []).map((c: any): [string, any] => [
|
||
cellKey(c),
|
||
c
|
||
])
|
||
);
|
||
const newCells = new Map<string, any>(
|
||
(submitted?.slabs ?? []).map((c: any): [string, any] => [
|
||
cellKey(c),
|
||
c
|
||
])
|
||
);
|
||
const names = new Map<number, string>();
|
||
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<string>([
|
||
...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<string, any>(
|
||
(active?.cibilSlabs ?? []).map((b: any): [string, any] => [
|
||
bandKey(b),
|
||
b
|
||
])
|
||
);
|
||
const newBands = new Map<string, any>(
|
||
(submitted?.cibilSlabs ?? []).map((b: any): [string, any] => [
|
||
bandKey(b),
|
||
b
|
||
])
|
||
);
|
||
for (const k of new Set<string>([
|
||
...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;
|
||
}
|