feat: update RBI reserve return form 1 & 9 UI and FD accounting mapping

This commit is contained in:
2026-09-28 13:36:29 +05:30
parent 6cbb31b5c6
commit eb278978bd
17 changed files with 1116 additions and 366 deletions
@@ -27,101 +27,104 @@
} @else {
<!-- ═══ Rates — only the reserve returns are charged at a percentage ═══ -->
@if (showRates) {
<div class="rrc-card">
<h3 class="rrc-card__title">Reserve rates</h3>
<p class="rrc-card__hint">
Rates are effective-dated rather than overwritten: a return filed for an earlier period keeps reporting the rate
that was actually in force then. Saving against a date that already has a rate edits it; a new date adds one.
</p>
<div class="rrc-card">
<h3 class="rrc-card__title">Reserve rates</h3>
<p class="rrc-card__hint">
Rates are effective-dated rather than overwritten: a return filed for an earlier period keeps reporting the
rate that was actually in force then. Saving against a date that already has a rate edits it; a new date adds
one.
</p>
<div class="rrc-rate-now">
@for (type of ['CRR', 'SLR', 'SAVINGS_DEMAND']; track type) {
<div class="rrc-rate-now__item">
<span class="rrc-rate-now__label">
{{ type === 'SAVINGS_DEMAND' ? 'Savings demand share' : type }} in force today
</span>
@if (currentRate(type); as rate) {
<span class="rrc-rate-now__value">{{ rate.percentage }}%</span>
@if (rate.seedDefault) {
<span class="rrc-rate-now__since rrc-rate-now__since--warn">
Placeholder — confirm the rate actually in force
</span>
} @else {
<span class="rrc-rate-now__since">since {{ rate.effectiveFrom }}</span>
}
} @else {
<span class="rrc-rate-now__value rrc-rate-now__value--none">Not set</span>
<span class="rrc-rate-now__since">{{ type === 'CRR' ? 'Form 1' : 'Form 9' }} cannot compute</span>
}
</div>
}
</div>
<div class="rrc-rate-form">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__type">
<mat-label>Rate</mat-label>
<mat-select [(ngModel)]="newRate.rateType">
<mat-option value="CRR">CRR — Form 1</mat-option>
<mat-option value="SLR">SLR — Form 9</mat-option>
<!-- Not a reserve ratio, but set by RBI and effective-dated the same way: the share of
savings balances that counts as a demand liability, the rest being time. -->
<mat-option value="SAVINGS_DEMAND">Savings — demand share</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__pct">
<mat-label>Percentage (%)</mat-label>
<input matInput type="number" step="0.0001" min="0" max="100" [(ngModel)]="newRate.percentage" />
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__date">
<mat-label>Effective from</mat-label>
<input matInput type="date" [(ngModel)]="newRate.effectiveFrom" />
</mat-form-field>
<button mat-raised-button color="primary" [disabled]="savingRate" (click)="saveRate()">
{{ savingRate ? 'Saving…' : 'Save rate' }}
</button>
</div>
<table class="rrc-table">
<thead>
<tr>
<th>Rate</th>
<th>Percentage</th>
<th>Effective from</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
@for (rate of rates; track rate.id) {
<tr>
<td><span class="rrc-key">{{ rate.rateType }}</span></td>
<td class="rrc-num">{{ rate.percentage }}%</td>
<td>{{ rate.effectiveFrom }}</td>
<td>
@if (isFuture(rate)) {
<span class="rrc-badge rrc-badge--future">Future-dated</span>
} @else if (currentRate(rate.rateType)?.id === rate.id) {
<span class="rrc-badge rrc-badge--active">In force</span>
} @else {
<span class="rrc-badge rrc-badge--past">Superseded</span>
}
<div class="rrc-rate-now">
@for (type of ['CRR', 'SLR', 'SAVINGS_DEMAND']; track type) {
<div class="rrc-rate-now__item">
<span class="rrc-rate-now__label">
{{ type === 'SAVINGS_DEMAND' ? 'Savings demand share' : type }} in force today
</span>
@if (currentRate(type); as rate) {
<span class="rrc-rate-now__value">{{ rate.percentage }}%</span>
@if (rate.seedDefault) {
<span class="rrc-badge rrc-badge--seed">Placeholder</span>
<span class="rrc-rate-now__since rrc-rate-now__since--warn">
Placeholder — confirm the rate actually in force
</span>
} @else {
<span class="rrc-rate-now__since">since {{ rate.effectiveFrom }}</span>
}
</td>
<td>
<button mat-button class="rrc-remove" (click)="deleteRate(rate)" title="Remove this rate">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
} @else {
<span class="rrc-rate-now__value rrc-rate-now__value--none">Not set</span>
<span class="rrc-rate-now__since">{{ type === 'CRR' ? 'Form 1' : 'Form 9' }} cannot compute</span>
}
</div>
}
</tbody>
</table>
</div>
</div>
<div class="rrc-rate-form">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__type">
<mat-label>Rate</mat-label>
<mat-select [(ngModel)]="newRate.rateType">
<mat-option value="CRR">CRR — Form 1</mat-option>
<mat-option value="SLR">SLR — Form 9</mat-option>
<!-- Not a reserve ratio, but set by RBI and effective-dated the same way: the share of
savings balances that counts as a demand liability, the rest being time. -->
<mat-option value="SAVINGS_DEMAND">Savings — demand share</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__pct">
<mat-label>Percentage (%)</mat-label>
<input matInput type="number" step="0.0001" min="0" max="100" [(ngModel)]="newRate.percentage" />
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__date">
<mat-label>Effective from</mat-label>
<input matInput type="date" [(ngModel)]="newRate.effectiveFrom" />
</mat-form-field>
<button mat-raised-button color="primary" [disabled]="savingRate" (click)="saveRate()">
{{ savingRate ? 'Saving…' : 'Save rate' }}
</button>
</div>
<table class="rrc-table">
<thead>
<tr>
<th>Rate</th>
<th>Percentage</th>
<th>Effective from</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
@for (rate of rates; track rate.id) {
<tr>
<td>
<span class="rrc-key">{{ rate.rateType }}</span>
</td>
<td class="rrc-num">{{ rate.percentage }}%</td>
<td>{{ rate.effectiveFrom }}</td>
<td>
@if (isFuture(rate)) {
<span class="rrc-badge rrc-badge--future">Future-dated</span>
} @else if (currentRate(rate.rateType)?.id === rate.id) {
<span class="rrc-badge rrc-badge--active">In force</span>
} @else {
<span class="rrc-badge rrc-badge--past">Superseded</span>
}
@if (rate.seedDefault) {
<span class="rrc-badge rrc-badge--seed">Placeholder</span>
}
</td>
<td>
<button mat-button class="rrc-remove" (click)="deleteRate(rate)" title="Remove this rate">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
<!-- ═══ GL accounts ═══ -->
@@ -324,6 +327,17 @@
<input matInput [(ngModel)]="newRole.description" placeholder="What this head covers" />
</mat-form-field>
<!--
The section prefixes are a naming convention rather than a field, so they have to be
stated somewhere: an administrator who wants a head on the Memorandum has no way to
discover that naming it so is what puts it there.
-->
<p class="rrc-card__hint rrc-add-role__note">
Naming a head <strong>Memorandum - …</strong>, <strong>Annexure A - …</strong> or
<strong>Annexure B - …</strong> prints it under that section of the return instead of in the main
statement.
</p>
<button mat-raised-button color="primary" [disabled]="addingRole" (click)="addRole()">
{{ addingRole ? 'Adding…' : 'Add head' }}
</button>
@@ -297,7 +297,7 @@ $red: #c5221f;
&--active {
border-color: $primary;
background: rgba(19, 83, 160, 0.06);
background: rgb(19 83 160 / 6%);
color: $primary;
}
@@ -306,13 +306,13 @@ $red: #c5221f;
font-weight: 500;
padding: 1px 7px;
border-radius: 10px;
background: rgba(176, 96, 0, 0.12);
background: rgb(176 96 0 / 12%);
color: $amber;
// A fully mapped return is worth stating as plainly as an incomplete one, so the tab row can
// be read at a glance instead of by elimination.
&--done {
background: rgba(19, 115, 51, 0.12);
background: rgb(19 115 51 / 12%);
color: $green;
}
}
@@ -379,6 +379,12 @@ $red: #c5221f;
}
}
// Spans the add-head form's columns rather than sitting in one of them.
.rrc-add-role__note {
flex-basis: 100%;
margin: 0 !important;
}
.rrc-remove {
min-width: 0 !important;
padding: 0 8px !important;
@@ -397,7 +403,7 @@ $red: #c5221f;
padding: 3px 10px;
}
@media (max-width: 720px) {
@media (width <= 720px) {
.rrc-rate-form__type,
.rrc-rate-form__pct,
.rrc-rate-form__date,
@@ -419,7 +425,7 @@ $red: #c5221f;
font-size: 0.72rem;
letter-spacing: 0.04em;
text-transform: uppercase;
color: rgba(0, 0, 0, 0.55);
color: rgb(0 0 0 / 55%);
font-weight: 600;
padding: 0 0.5rem 0.25rem 0;
}
@@ -447,7 +453,7 @@ $red: #c5221f;
/* An incomplete row is marked where it is, rather than only by the disabled Save button, so it is
clear which row is holding the save back. */
&__row--invalid td {
background: rgba(214, 106, 0, 0.06);
background: rgb(214 106 0 / 6%);
}
}
@@ -460,7 +466,7 @@ $red: #c5221f;
.rrc-add {
margin-top: 1.25rem;
padding-top: 1rem;
border-top: 1px dashed rgba(0, 0, 0, 0.12);
border-top: 1px dashed rgb(0 0 0 / 12%);
&__form {
display: flex;
@@ -91,12 +91,19 @@ export class RbiReserveConfigComponent implements OnInit {
{ value: 'TIME', label: 'Time' }
];
/** How a head an administrator adds is consumed by the return. */
/**
* How a head an administrator adds is consumed by the return.
*
* MAINTAINED and SLR_ASSET are separate because the two returns admit different assets: CRR is
* met in cash and balances with banks, SLR in cash, gold and approved securities. One option for
* both would let a head meant for Form 1 quietly satisfy Form 9.
*/
readonly roleUsages = [
{ value: 'LIABILITY', label: 'Liability — adds to the DTL base' },
{ value: 'ASSET_NET', label: 'Asset — netted off the base (NDTL)' },
{ value: 'MAINTAINED', label: 'Counts toward what is maintained' },
{ value: 'MEMO', label: 'Memo — shown but totals nothing' }
{ value: 'LIABILITY', label: 'Liability — adds to the DTL base (Form 1 & 9)' },
{ value: 'ASSET_NET', label: 'Asset — netted off the base to give NDTL (Form 1 & 9)' },
{ value: 'MAINTAINED', label: 'Counts toward CRR maintained (Form 1)' },
{ value: 'SLR_ASSET', label: 'Counts toward SLR maintained (Form 9)' },
{ value: 'MEMO', label: 'Memo — shown on the return but totals nothing' }
];
/** New-head form. */
@@ -168,10 +175,12 @@ export class RbiReserveConfigComponent implements OnInit {
}
byGroup.get(group)!.roles.push(role);
}
this.roleGroups = [...byGroup.entries()].map(([
name,
entry
]) => ({ name, label: entry.label, roles: entry.roles }));
this.roleGroups = [...byGroup.entries()].map(
([
name,
entry
]) => ({ name, label: entry.label, roles: entry.roles })
);
// Keep the open tab if it still exists, so saving a role does not throw the user back to the
// first tab with their place lost.
@@ -237,8 +246,7 @@ export class RbiReserveConfigComponent implements OnInit {
// a bank look compliant when it is not.
this.selection[role.configKey] = [
...(this.selection[role.configKey] || []),
{ glAccountId: null as any, weightPercentage: 100, liabilityClass: role.classified ? 'TIME' : null }
];
{ glAccountId: null as any, weightPercentage: 100, liabilityClass: role.classified ? 'TIME' : null }];
}
removeMapping(role: any, index: number): void {
@@ -252,9 +260,7 @@ export class RbiReserveConfigComponent implements OnInit {
*/
availableAccounts(role: any, index: number): any[] {
const rows = this.selection[role.configKey] || [];
const taken = new Set(
rows.filter((_, i) => i !== index).map((m) => `${m.glAccountId}/${m.liabilityClass ?? ''}`)
);
const taken = new Set(rows.filter((_, i) => i !== index).map((m) => `${m.glAccountId}/${m.liabilityClass ?? ''}`));
const current = rows[index];
return this.glAccountOptions.filter((o) => !taken.has(`${o.id}/${current?.liabilityClass ?? ''}`));
}