276 lines
12 KiB
TypeScript
276 lines
12 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 { Component, OnInit, inject } from '@angular/core';
|
|
import { CommonModule, DecimalPipe } from '@angular/common';
|
|
import { MatDialogRef, MatDialogModule } from '@angular/material/dialog';
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
import { MatTabsModule } from '@angular/material/tabs';
|
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
import { RbiCircularsService } from '../../../rbi-circulars/rbi-circulars.service';
|
|
|
|
/** One account under a head, at the base date and the reporting date. */
|
|
interface CrrSlrRow {
|
|
sl: string;
|
|
name: string;
|
|
sub: string;
|
|
code: string;
|
|
opening: string;
|
|
current: string;
|
|
/** The figures themselves, kept so totals add from values rather than from formatted text. */
|
|
openingValue: number | null;
|
|
currentValue: number | null;
|
|
change: string;
|
|
changeDir: 'up' | 'dn' | 'flat';
|
|
status: string;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'mifosx-crr-slr-dialog',
|
|
standalone: true,
|
|
templateUrl: './crr-slr-dialog.component.html',
|
|
styleUrls: ['./crr-slr-dialog.component.scss'],
|
|
imports: [
|
|
CommonModule,
|
|
DecimalPipe,
|
|
MatDialogModule,
|
|
MatButtonModule,
|
|
MatTabsModule,
|
|
MatTooltipModule,
|
|
FaIconComponent
|
|
]
|
|
})
|
|
export class CrrSlrDialogComponent implements OnInit {
|
|
private dialogRef = inject(MatDialogRef<CrrSlrDialogComponent>);
|
|
private rbi = inject(RbiCircularsService);
|
|
|
|
activeTab: 'demand' | 'time' | 'assets' | 'slr' = 'demand';
|
|
|
|
/** The date the figures were struck at, as the return itself reports it. */
|
|
reportingDateStr = '';
|
|
|
|
/**
|
|
* The close the ratios are charged on — the second preceding fortnight, not the one reported.
|
|
*
|
|
* Named on screen because the gap between the two is the whole of how the rules work: a bank
|
|
* cannot hold a reserve against liabilities it does not yet know, so the requirement is fixed on
|
|
* a position already struck and reported. A screen showing one date invites a reader to think the
|
|
* requirement was worked out on the day it is shown against.
|
|
*/
|
|
baseDateStr = '';
|
|
|
|
loading = true;
|
|
/** Set when the return could not be computed — and why, in the return's own words. */
|
|
unavailable = '';
|
|
|
|
/**
|
|
* The NDTL computation, read from the return rather than stated here.
|
|
*
|
|
* These were fixed numbers until now — a demonstration screen that showed ₹69,221 Cr whatever the
|
|
* ledger held. The figures are the bank's own regulatory position, so a plausible-looking number
|
|
* is worse than no number: it reads as a reported figure and cannot be told apart from one.
|
|
*
|
|
* Null until the return answers, and left null where it cannot compute. The template shows a dash
|
|
* for a null, which says "not reported" where a zero would say "reported as nil".
|
|
*/
|
|
totalDemand: number | null = null;
|
|
totalTime: number | null = null;
|
|
totalBankingAssets: number | null = null;
|
|
ndtl: number | null = null;
|
|
crrRate: number | null = null;
|
|
slrRate: number | null = null;
|
|
crrRequired: number | null = null;
|
|
slrRequired: number | null = null;
|
|
totalLiquidAssets: number | null = null;
|
|
slrActualPercent: number | null = null;
|
|
slrSurplus: number | null = null;
|
|
totalReserve: number | null = null;
|
|
|
|
/**
|
|
* A figure for display, or a dash where the return did not carry one.
|
|
*
|
|
* A dash and a zero say different things — "not reported" against "reported as nil" — and on a
|
|
* regulatory screen the difference decides whether anyone goes looking for the missing mapping.
|
|
*/
|
|
crore(value: number | null): string {
|
|
if (value === null) {
|
|
return '—';
|
|
}
|
|
return `₹${value.toLocaleString('en-IN', { maximumFractionDigits: 0 })}`;
|
|
}
|
|
|
|
ngOnInit(): void {
|
|
// The return is reported in rupees; this screen reads in crore, as a dashboard tile does.
|
|
const toCrore = (v: number | null | undefined): number | null =>
|
|
v === null || v === undefined ? null : +(v / 1e7).toFixed(2);
|
|
|
|
// The statutory Form 1, not the working report. Form 1 is filed twice a month — at the 15th and
|
|
// at the month end — and this screen should show the close the bank last filed, not a position
|
|
// struck 45 days back by a lag rule that belongs to the other report. Those two disagree by
|
|
// weeks, and the 45-day one lands mid-fortnight on a date the bank never reported.
|
|
this.rbi.getForm1Statutory().subscribe({
|
|
next: (data: any) => {
|
|
this.loading = false;
|
|
|
|
// The later of the two closes the return reports — the one the bank most recently filed.
|
|
const columns: string[] = data?.columns ?? [];
|
|
const latest = columns.length - 1;
|
|
this.reportingDateStr = columns[latest] ?? '';
|
|
this.baseDateStr = data?.baseColumns?.[latest] ?? '';
|
|
this.crrRate = data?.crrPercentage ?? null;
|
|
this.slrRate = data?.slrPercentage ?? null;
|
|
|
|
if (!columns.length) {
|
|
this.unavailable =
|
|
data?.unmetRequirements?.[0] ?? 'Form 1 could not be computed. Check the CRR/SLR configuration in Admin.';
|
|
return;
|
|
}
|
|
// The return computes but says something would make it wrong to file — a rate never set, a
|
|
// base date the ledger cannot answer. Shown rather than swallowed: the reason is the thing
|
|
// an administrator needs in order to fix it.
|
|
this.unavailable = data?.unmetRequirements?.[0] ?? '';
|
|
|
|
// Part A's own lines, read by the Roman numeral each carries. Read from the return rather
|
|
// than recomputed, so this screen and the filed form cannot come to disagree.
|
|
const lines: any[] = data?.partA?.lines ?? [];
|
|
const at = (startsWith: string): number | null => {
|
|
const line = lines.find((l: any) => String(l?.label ?? '').startsWith(startsWith));
|
|
return line?.values?.[latest] ?? null;
|
|
};
|
|
|
|
this.totalDemand = toCrore(at('(A) Demand liabilities'));
|
|
this.totalTime = toCrore(at('(B) Time liabilities'));
|
|
this.totalBankingAssets = toCrore(at('TOTAL OF III'));
|
|
// IV(b) is the base the ratios are actually charged on — the figure behind the requirement,
|
|
// not the bank's liabilities on the day being reported.
|
|
this.ndtl = toCrore(at('IV. b)'));
|
|
this.crrRequired = toCrore(data?.partB?.required?.[latest]);
|
|
this.slrRequired = toCrore(data?.partC?.required?.[latest]);
|
|
this.totalLiquidAssets = toCrore(data?.partC?.maintained?.[latest]);
|
|
|
|
if (this.slrRequired !== null && this.totalLiquidAssets !== null) {
|
|
this.slrSurplus = +(this.totalLiquidAssets - this.slrRequired).toFixed(2);
|
|
}
|
|
if (this.ndtl) {
|
|
this.slrActualPercent =
|
|
this.totalLiquidAssets === null ? null : +((this.totalLiquidAssets / this.ndtl) * 100).toFixed(2);
|
|
}
|
|
if (this.crrRequired !== null && this.slrRequired !== null) {
|
|
this.totalReserve = +(this.crrRequired + this.slrRequired).toFixed(2);
|
|
}
|
|
|
|
this.fillTabs(lines, latest);
|
|
},
|
|
error: () => {
|
|
this.loading = false;
|
|
this.unavailable = 'The CRR/SLR position could not be read. The return service did not respond.';
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The four tabs, filled from the return's own lines.
|
|
*
|
|
* These were four fixed lists until now — 435 lines of invented accounts with invented balances,
|
|
* which a reader had no way to tell apart from the bank's real position. They are now built from
|
|
* the accounts the return actually read, so a tab is empty exactly when the bank has nothing
|
|
* mapped under that head, and says so.
|
|
*/
|
|
demandLiabilities: CrrSlrRow[] = [];
|
|
timeLiabilities: CrrSlrRow[] = [];
|
|
bankingAssets: CrrSlrRow[] = [];
|
|
liquidAssets: CrrSlrRow[] = [];
|
|
/**
|
|
* Turns Part A's lines into the rows a tab shows.
|
|
*
|
|
* The form reports each head at both of the closes it carries, so the earlier column is the
|
|
* opening and the later one the current — the same pair these tables have always shown, now taken
|
|
* from the form rather than invented. Headings and totals are skipped: a heading carries no
|
|
* figure, and a total would be added a second time by the footer below.
|
|
*/
|
|
private rowsFrom(lines: any[], latest: number, match: (label: string) => boolean): CrrSlrRow[] {
|
|
const rows: CrrSlrRow[] = [];
|
|
for (const line of lines ?? []) {
|
|
const label = String(line?.label ?? '');
|
|
if (line?.heading || line?.total || !line?.values || !match(label)) {
|
|
continue;
|
|
}
|
|
const opening = latest > 0 ? (line.values[latest - 1] ?? null) : null;
|
|
const current = line.values[latest] ?? null;
|
|
const change = opening === null || current === null ? null : current - opening;
|
|
rows.push({
|
|
sl: String(rows.length + 1).padStart(2, '0'),
|
|
// The form's own wording, with its numbering trimmed off the front — the table has a column
|
|
// of its own for that.
|
|
name: label.replace(/^\s*\(?[a-zA-Z0-9]+\)?[.)]?\s*/, '').trim() || label.trim(),
|
|
sub: line.configKey ? String(line.configKey).replace(/_/g, ' ') : 'Derived from the form',
|
|
code: line.configKey ?? '—',
|
|
opening: this.crore(opening),
|
|
current: this.crore(current),
|
|
openingValue: opening,
|
|
currentValue: current,
|
|
change: change === null ? '—' : `${change >= 0 ? '+' : ''}${this.crore(change).replace('₹', '')}`,
|
|
changeDir: change === null ? 'flat' : change >= 0 ? 'up' : 'dn',
|
|
// A head with no GL mapped reports nothing, and the form says so rather than reporting nil.
|
|
status: line.configured === false ? 'Not mapped' : 'Active'
|
|
});
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
/**
|
|
* A tab's column total, added from the rows on screen.
|
|
*
|
|
* Added from the rows rather than stated separately, so the foot of a table always agrees with
|
|
* what is above it. The footers used to be fixed text and agreed with nothing.
|
|
*
|
|
* Summed from each row's own figure, not from the text it displays: a total parsed back out of
|
|
* formatted strings would quietly lose the paise the display rounds away.
|
|
*/
|
|
totalOf(rows: CrrSlrRow[], column: 'openingValue' | 'currentValue'): string {
|
|
const values = rows.map((r) => r[column]).filter((v): v is number => v !== null);
|
|
return values.length ? this.crore(values.reduce((a, b) => a + b, 0)) : '—';
|
|
}
|
|
|
|
/** The movement across a tab, between the two closes the form reports. */
|
|
totalChange(rows: CrrSlrRow[]): string {
|
|
const opening = rows.map((r) => r.openingValue).filter((v): v is number => v !== null);
|
|
const current = rows.map((r) => r.currentValue).filter((v): v is number => v !== null);
|
|
if (!opening.length || !current.length) {
|
|
return '—';
|
|
}
|
|
const change = current.reduce((a, b) => a + b, 0) - opening.reduce((a, b) => a + b, 0);
|
|
return `${change >= 0 ? '+' : ''}${this.crore(change).replace('₹', '')}`;
|
|
}
|
|
|
|
/** Fills the four tabs from one reading of the form. */
|
|
private fillTabs(lines: any[], latest: number): void {
|
|
const has = (label: string, ...words: string[]): boolean => {
|
|
const l = label.toLowerCase();
|
|
return words.some((w) => l.includes(w));
|
|
};
|
|
// Matched on how the form labels each head, not on a fixed list of account names: a bank that
|
|
// maps a new account to a head gets it on the right tab without this screen being changed.
|
|
this.demandLiabilities = this.rowsFrom(lines, latest, (l) => has(l, 'demand liabilit'));
|
|
this.timeLiabilities = this.rowsFrom(lines, latest, (l) => has(l, 'time liabilit') && !has(l, 'demand and time'));
|
|
// III — what the banking system owes this bank, which nets off the liability base.
|
|
this.bankingAssets = this.rowsFrom(lines, latest, (l) =>
|
|
has(l, 'current account balances with sbi', 'balances in all accounts', 'money at call', 'advances and any other')
|
|
);
|
|
// V, VI and VII — the cash, balances and securities the two ratios are met in.
|
|
this.liquidAssets = this.rowsFrom(lines, latest, (l) =>
|
|
has(l, 'cash on hand', 'reserve bank of india', 'co-operative bank', 'government securities')
|
|
);
|
|
}
|
|
|
|
close(): void {
|
|
this.dialogRef.close();
|
|
}
|
|
}
|