Add CC accounts tracking, AML rule trial and branch approval fixes
This commit is contained in:
@@ -8,12 +8,14 @@
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule, DecimalPipe } from '@angular/common';
|
||||
import { MatDialogRef, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatDialog, 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';
|
||||
import { AuthenticationService } from '../../../core/authentication/authentication.service';
|
||||
import { ReserveTransferDialogComponent } from './reserve-transfer-dialog.component';
|
||||
|
||||
/** One account under a head, at the base date and the reporting date. */
|
||||
interface CrrSlrRow {
|
||||
@@ -31,6 +33,47 @@ interface CrrSlrRow {
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** One reserve account, with what it holds on the business date. */
|
||||
interface ReserveAccount {
|
||||
glAccountId: number;
|
||||
glCode: string;
|
||||
name: string;
|
||||
balance: number;
|
||||
/** The Form 1 item the account answers — V, VI(a), VI(b), VI(c). */
|
||||
item: string;
|
||||
head: string;
|
||||
/**
|
||||
* Whether the filed return reads this balance.
|
||||
*
|
||||
* An account the bank holds but has not mapped is still somewhere money can be moved, so it is
|
||||
* listed — but counting it toward what is maintained would have this screen claim a compliance
|
||||
* the return does not.
|
||||
*/
|
||||
countsTowardMaintained: boolean;
|
||||
}
|
||||
|
||||
/** One ratio's standing: what it needs, what it has, and which way the gap runs. */
|
||||
interface ReserveRatio {
|
||||
required: number;
|
||||
maintained: number;
|
||||
surplus: number;
|
||||
/** Positive — a shortfall is an amount to cover, not a negative surplus. */
|
||||
shortfall: number;
|
||||
met: boolean;
|
||||
}
|
||||
|
||||
/** The reserve position as it stands on the business date. */
|
||||
interface ReservePosition {
|
||||
onDate: string;
|
||||
/** The close the requirement is charged on — a fortnight back, by RBI's lag rule. */
|
||||
baseDate: string;
|
||||
crrPercentage: number | null;
|
||||
slrPercentage: number | null;
|
||||
crr: ReserveRatio;
|
||||
slr: ReserveRatio;
|
||||
accounts: ReserveAccount[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-crr-slr-dialog',
|
||||
standalone: true,
|
||||
@@ -49,8 +92,18 @@ interface CrrSlrRow {
|
||||
export class CrrSlrDialogComponent implements OnInit {
|
||||
private dialogRef = inject(MatDialogRef<CrrSlrDialogComponent>);
|
||||
private rbi = inject(RbiCircularsService);
|
||||
private dialog = inject(MatDialog);
|
||||
private auth = inject(AuthenticationService);
|
||||
|
||||
activeTab: 'demand' | 'time' | 'assets' | 'slr' = 'demand';
|
||||
activeTab: 'demand' | 'time' | 'assets' | 'slr' | 'held' = 'demand';
|
||||
|
||||
/**
|
||||
* The reserve position on the business date — null until it loads, or if it cannot be read.
|
||||
*
|
||||
* Separate from the return the other tabs show: those report a fortnight close, this reports
|
||||
* where the money is now.
|
||||
*/
|
||||
position: ReservePosition | null = null;
|
||||
|
||||
/** The date the figures were struck at, as the return itself reports it. */
|
||||
reportingDateStr = '';
|
||||
@@ -82,7 +135,23 @@ export class CrrSlrDialogComponent implements OnInit {
|
||||
totalDemand: number | null = null;
|
||||
totalTime: number | null = null;
|
||||
totalBankingAssets: number | null = null;
|
||||
/**
|
||||
* Whether item III was actually deducted from the base.
|
||||
*
|
||||
* It nets against item I rather than against the whole base, and the netting floors at zero — so
|
||||
* a bank holding more with other banks than it owes them has nothing deducted at all.
|
||||
*/
|
||||
bankingAssetsNet = false;
|
||||
/** Item IV(a) — the bank's own net liabilities on the date being reported. */
|
||||
ndtl: number | null = null;
|
||||
/**
|
||||
* Item IV(b) — the base a fortnight back, which both ratios are actually charged on.
|
||||
*
|
||||
* A separate figure from `ndtl` above and not interchangeable with it: the requirement is struck
|
||||
* on this one, so a ratio or a tile that quotes the other states a percentage of something the
|
||||
* requirement was never computed from.
|
||||
*/
|
||||
baseNdtl: number | null = null;
|
||||
crrRate: number | null = null;
|
||||
slrRate: number | null = null;
|
||||
crrRequired: number | null = null;
|
||||
@@ -98,7 +167,7 @@ export class CrrSlrDialogComponent implements OnInit {
|
||||
* 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 {
|
||||
rupees(value: number | null): string {
|
||||
if (value === null) {
|
||||
return '—';
|
||||
}
|
||||
@@ -106,9 +175,19 @@ export class CrrSlrDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
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);
|
||||
// Today's position, alongside the return below. The two answer different questions and are
|
||||
// fetched separately: the return reports a fortnight close, this reports the balances as they
|
||||
// stand on the business date, which is what a desk can still act on.
|
||||
this.rbi.getReservePosition().subscribe({
|
||||
next: (data: ReservePosition) => (this.position = data),
|
||||
// A position that cannot be read leaves the other four tabs working. The tab simply shows
|
||||
// nothing rather than taking the whole dialog down with it.
|
||||
error: () => (this.position = null)
|
||||
});
|
||||
|
||||
// Figures stay in rupees. They were rounded to crore until now, which on a bank this size threw
|
||||
// away everything that tells one fortnight from the next — a seventy-crore NDTL and the one a
|
||||
// fortnight later both read "₹70 Cr".
|
||||
|
||||
// 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
|
||||
@@ -139,27 +218,66 @@ export class CrrSlrDialogComponent implements OnInit {
|
||||
// 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 ?? [];
|
||||
// Matched without regard to case: the form prints its heads in capitals — "(A) DEMAND
|
||||
// LIABILITIES :" — and matching them as written left every tile reading a dash while the
|
||||
// figures sat in the response.
|
||||
// Matched without regard to case, and only against lines that actually carry figures. The
|
||||
// form heads item I with "(a) Demand Liabilities:" — a caption with no values — directly
|
||||
// above item II's "(A) DEMAND LIABILITIES :", which holds the figure. Matching on text
|
||||
// alone found the caption first and read a dash off it.
|
||||
const at = (startsWith: string): number | null => {
|
||||
const line = lines.find((l: any) => String(l?.label ?? '').startsWith(startsWith));
|
||||
const want = startsWith.toUpperCase();
|
||||
const line = lines.find(
|
||||
(l: any) =>
|
||||
Array.isArray(l?.values) &&
|
||||
String(l?.label ?? '')
|
||||
.toUpperCase()
|
||||
.startsWith(want)
|
||||
);
|
||||
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]);
|
||||
// Item II's own two lines, matched including the trailing colon the form prints. Item I
|
||||
// carries a "(b) Time Liabilities to the banking system" of its own — a different figure,
|
||||
// and the one a looser match found first.
|
||||
this.totalDemand = at('(A) DEMAND LIABILITIES :');
|
||||
this.totalTime = at('(B) TIME LIABILITIES :');
|
||||
this.totalBankingAssets = at('TOTAL OF III');
|
||||
// Whether item III actually came off the base. The form nets it against item I — the
|
||||
// liabilities OWED TO the banking system — and floors that netting at zero: where the
|
||||
// bank's assets with other banks exceed what it owes them, the excess is NOT deducted and
|
||||
// the base is item II alone. Stating "deducted from NDTL" in that case names a subtraction
|
||||
// the return did not make, and a reader checking the arithmetic finds it does not work.
|
||||
const liabilitiesToBanks = at('TOTAL OF I');
|
||||
this.bankingAssetsNet =
|
||||
liabilitiesToBanks !== null &&
|
||||
this.totalBankingAssets !== null &&
|
||||
liabilitiesToBanks > this.totalBankingAssets;
|
||||
// Two different figures, and the screen needs both.
|
||||
//
|
||||
// IV(a) is the bank's OWN net liabilities on the reporting date — what the demand, time and
|
||||
// banking-asset tiles add up to, so it is the one the formula row must end at. IV(b) is the
|
||||
// base a fortnight back, which is what the requirement is charged on.
|
||||
//
|
||||
// Showing IV(b) as the result of the formula was arithmetic that did not work: three terms
|
||||
// struck on one date reaching a figure struck on another, off by whatever the liabilities
|
||||
// had moved in between.
|
||||
this.ndtl = at('IV. a)');
|
||||
this.baseNdtl = at('IV. b)');
|
||||
this.crrRequired = data?.partB?.required?.[latest] ?? null;
|
||||
this.slrRequired = data?.partC?.required?.[latest] ?? null;
|
||||
this.totalLiquidAssets = data?.partC?.maintained?.[latest] ?? null;
|
||||
|
||||
if (this.slrRequired !== null && this.totalLiquidAssets !== null) {
|
||||
this.slrSurplus = +(this.totalLiquidAssets - this.slrRequired).toFixed(2);
|
||||
}
|
||||
if (this.ndtl) {
|
||||
// Struck on the base, not on today's own liabilities: the requirement below it is charged
|
||||
// on the base, and a ratio measured against a different denominator from the one the
|
||||
// requirement uses cannot be compared with it — "33.62% against a minimum of 18%" would be
|
||||
// two percentages of two different things.
|
||||
if (this.baseNdtl) {
|
||||
this.slrActualPercent =
|
||||
this.totalLiquidAssets === null ? null : +((this.totalLiquidAssets / this.ndtl) * 100).toFixed(2);
|
||||
this.totalLiquidAssets === null ? null : +((this.totalLiquidAssets / this.baseNdtl) * 100).toFixed(2);
|
||||
}
|
||||
if (this.crrRequired !== null && this.slrRequired !== null) {
|
||||
this.totalReserve = +(this.crrRequired + this.slrRequired).toFixed(2);
|
||||
@@ -209,13 +327,20 @@ export class CrrSlrDialogComponent implements OnInit {
|
||||
// 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),
|
||||
// The form's own numbering — I(a)(ii), II(A), VII(c) — rather than the configuration key
|
||||
// behind it. A reader checking this screen against the filed return looks for the item
|
||||
// number; FORM1_I_A_II_OTHER_DEMAND_BS is the name of a database row and means nothing to
|
||||
// them.
|
||||
sub: '',
|
||||
code: (label.match(/^\s*\(?([IVXivx]+|[A-Za-z0-9]+)\)?[.)]?/) ?? [
|
||||
,
|
||||
'—'
|
||||
])[1] ?? '—',
|
||||
opening: this.rupees(opening),
|
||||
current: this.rupees(current),
|
||||
openingValue: opening,
|
||||
currentValue: current,
|
||||
change: change === null ? '—' : `${change >= 0 ? '+' : ''}${this.crore(change).replace('₹', '')}`,
|
||||
change: change === null ? '—' : `${change >= 0 ? '+' : ''}${this.rupees(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'
|
||||
@@ -235,7 +360,7 @@ export class CrrSlrDialogComponent implements OnInit {
|
||||
*/
|
||||
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)) : '—';
|
||||
return values.length ? this.rupees(values.reduce((a, b) => a + b, 0)) : '—';
|
||||
}
|
||||
|
||||
/** The movement across a tab, between the two closes the form reports. */
|
||||
@@ -246,7 +371,7 @@ export class CrrSlrDialogComponent implements OnInit {
|
||||
return '—';
|
||||
}
|
||||
const change = current.reduce((a, b) => a + b, 0) - opening.reduce((a, b) => a + b, 0);
|
||||
return `${change >= 0 ? '+' : ''}${this.crore(change).replace('₹', '')}`;
|
||||
return `${change >= 0 ? '+' : ''}${this.rupees(change).replace('₹', '')}`;
|
||||
}
|
||||
|
||||
/** Fills the four tabs from one reading of the form. */
|
||||
@@ -257,16 +382,148 @@ export class CrrSlrDialogComponent implements OnInit {
|
||||
};
|
||||
// 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')
|
||||
// Item II only — what the bank owes its own depositors. Item I's lines are worded the same way
|
||||
// but are owed to the banking system, and the form nets them against item III rather than
|
||||
// adding them here, so a tab headed "payable on demand" that carried both would double-count
|
||||
// the one and misstate the other.
|
||||
const toBankingSystem = (l: string): boolean => has(l, 'to the banking system', 'banking system');
|
||||
this.demandLiabilities = this.rowsFrom(lines, latest, (l) => has(l, 'demand liabilit') && !toBankingSystem(l));
|
||||
this.timeLiabilities = this.rowsFrom(
|
||||
lines,
|
||||
latest,
|
||||
(l) => has(l, 'time liabilit') && !has(l, 'demand and time') && !toBankingSystem(l)
|
||||
);
|
||||
// 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')
|
||||
// III — what the banking system owes this bank, which nets off the liability base. Matched on
|
||||
// the words the form actually prints: its item III(a) reads "TOTAL OF CREDIT BALANCES IN
|
||||
// CURRENT ACCOUNT maintained with S.B.I...", which the shorter phrases this looked for never
|
||||
// appeared in, so the tab came out empty while its total showed a figure.
|
||||
this.bankingAssets = this.rowsFrom(
|
||||
lines,
|
||||
latest,
|
||||
(l) =>
|
||||
has(
|
||||
l,
|
||||
'credit balances in current account',
|
||||
'other assets with the banking system',
|
||||
'balances in all accounts',
|
||||
'money at call',
|
||||
'advances & any other'
|
||||
) &&
|
||||
// Item I(a)(i) is worded almost identically but is the opposite side of the ledger: what
|
||||
// the banking system holds WITH this bank, a liability. On a tab headed "deducted from
|
||||
// gross liabilities" it would be read as an asset and netted the wrong way.
|
||||
!has(l, 'maintained with the co-operative bank by state')
|
||||
);
|
||||
// V, VI and VII — the cash, balances and securities the two ratios are met in. Item I's own
|
||||
// SBI line is excluded: it is a liability TO the banking system, and listing it among the
|
||||
// assets held against a ratio reads as something the bank can count toward it.
|
||||
this.liquidAssets = this.rowsFrom(
|
||||
lines,
|
||||
latest,
|
||||
(l) =>
|
||||
has(l, 'cash on hand', 'reserve bank of india', 'co.op.bank', 'co-operative bank', 'govt. security') &&
|
||||
!has(l, 'maintained with the co-operative bank by state')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The two ratios as rows, so the template states them once rather than twice over.
|
||||
*/
|
||||
positionRatios(): {
|
||||
name: string;
|
||||
rate: number | null;
|
||||
required: number;
|
||||
maintained: number;
|
||||
surplus: number;
|
||||
shortfall: number;
|
||||
met: boolean;
|
||||
}[] {
|
||||
if (!this.position) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{ name: 'CRR', rate: this.position.crrPercentage, ...this.position.crr },
|
||||
{ name: 'SLR', rate: this.position.slrPercentage, ...this.position.slr }
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A reserve figure in rupees, to the paisa.
|
||||
*
|
||||
* Not rounded like the table cells above: a desk covering a shortfall transfers an exact amount, and
|
||||
* "₹0.37 Cr" is not an amount anyone can send.
|
||||
*/
|
||||
money(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) {
|
||||
return '—';
|
||||
}
|
||||
return `₹${value.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* A figure for a summary tile, scaled so it fits however large the bank grows.
|
||||
*
|
||||
* Tiles are read for order of magnitude, so they carry whole rupees up to a crore and switch to
|
||||
* crore above it: ₹70,10,45,062 is nineteen characters and overflows its card, where ₹70.10 Cr
|
||||
* is seven and says the same thing to anyone glancing at it. The exact amount, to the paise, is
|
||||
* on the tables below and on the reserve-accounts tab — which is where anyone acting on a figure
|
||||
* rather than scanning it goes.
|
||||
*
|
||||
* Scaled rather than merely shrunk, because a fixed format only ever fits the balances the bank
|
||||
* happens to hold today. This one holds at four or five characters whether the figure is lakhs
|
||||
* or lakh-crore, so a bank that grows ten-fold does not come back to a row of clipped tiles.
|
||||
*/
|
||||
tile(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) {
|
||||
return '—';
|
||||
}
|
||||
const abs = Math.abs(value);
|
||||
const sign = value < 0 ? '-' : '';
|
||||
// Indian scale: a crore is 1e7, a lakh 1e5. Below a lakh the whole rupee figure is already
|
||||
// short enough that scaling it would hide detail for nothing.
|
||||
if (abs >= 1e7) {
|
||||
return `${sign}₹${(abs / 1e7).toLocaleString('en-IN', { maximumFractionDigits: 2 })} Cr`;
|
||||
}
|
||||
if (abs >= 1e5) {
|
||||
return `${sign}₹${(abs / 1e5).toLocaleString('en-IN', { maximumFractionDigits: 2 })} L`;
|
||||
}
|
||||
return `${sign}₹${Math.round(abs).toLocaleString('en-IN')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move money into or out of a reserve account.
|
||||
*
|
||||
* The entry posts through the platform's own journal command, so it goes to maker-checker like
|
||||
* any other entry against the ledger — the money is real, and a transfer that settles a statutory
|
||||
* requirement is not a thing one person should be able to post alone.
|
||||
*/
|
||||
openTransfer(account: ReserveAccount): void {
|
||||
const ref = this.dialog.open(ReserveTransferDialogComponent, {
|
||||
data: {
|
||||
from: account,
|
||||
// Only the reserve accounts. A transfer out of the cash reserve into something that does
|
||||
// not count toward it would quietly reduce what is maintained, and this screen exists to
|
||||
// keep that figure up.
|
||||
accounts: this.position?.accounts ?? [],
|
||||
// The entry posts against the user's own office — the one whose books hold these accounts.
|
||||
// Falls back to head office, which Fineract seeds as 1, rather than failing to open.
|
||||
officeId: this.auth.getCredentials()?.officeId ?? 1,
|
||||
currencyCode: 'INR'
|
||||
},
|
||||
autoFocus: false
|
||||
});
|
||||
|
||||
ref.afterClosed().subscribe((posted: boolean) => {
|
||||
if (posted) {
|
||||
// The balances on screen are now a step behind the ledger. Read again rather than adjusting
|
||||
// them here: the entry may have gone to maker-checker and not yet moved anything, and a
|
||||
// screen that assumed it had would show a position the bank is not in.
|
||||
this.rbi.getReservePosition().subscribe({
|
||||
next: (data: ReservePosition) => (this.position = data),
|
||||
error: () => undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
|
||||
Reference in New Issue
Block a user