diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts index a8ebfa8de..666f126d5 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts @@ -334,17 +334,18 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan const frequencyId = this.resolveFrequencyId(this.tenurePeriodUnit); if (this.principalAmount && this.principalAmount > 0) { - // Convert tenure to months for backend API. A Days tenure under 30 days rounds down to 0 - // months, which the backend rejects -- send it as tenureInDays instead in that case. + // Years and Months convert exactly; a Days tenure is sent as days and never converted. let tenureInMonths: number | undefined; let tenureInDays: number | undefined; if (this.tenurePeriodUnit === 'Years') { tenureInMonths = this.tenureValue * 12; } else if (this.tenurePeriodUnit === 'Months') { tenureInMonths = this.tenureValue; - } else if (enteredDays >= 30) { - tenureInMonths = Math.floor(enteredDays / 30); } else { + // Sent as days, whatever the count. Converting to whole months silently dropped the + // remainder — a 200-day deposit quoted as six months lost 20 days of interest, and the + // screen still said "200 Days" above the figure. The backend takes days directly and + // works the calendar out itself. tenureInDays = enteredDays; } @@ -423,10 +424,17 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan } else { const n = 12 / compMonths; const effectiveMonths = tenureInMonths ?? enteredDays / 30; - const totalCompoundings = Math.floor(effectiveMonths / compMonths); const rPerCompounding = rate / (n * 100); - result.maturityAmount = - Math.round(this.principalAmount * Math.pow(1 + rPerCompounding, totalCompoundings) * 100) / 100; + // Whole compounding periods, then simple interest on the days left over. Flooring the + // periods and stopping there dropped the remainder entirely: a 200-day deposit + // compounding monthly quoted only six months of interest and lost the other twenty + // days, which is the depositor's money. + const wholePeriods = Math.floor(effectiveMonths / compMonths); + const compounded = this.principalAmount * Math.pow(1 + rPerCompounding, wholePeriods); + const leftoverDays = enteredDays - wholePeriods * compMonths * 30; + const leftoverInterest = + leftoverDays > 0 ? (compounded * rate * leftoverDays) / (100 * daysInYearVal) : 0; + result.maturityAmount = Math.round((compounded + leftoverInterest) * 100) / 100; } result.interestEarned = Math.round((result.maturityAmount - this.principalAmount) * 100) / 100; this.matchResult = { ...result }; diff --git a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.html b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.html index 0e5f05cb9..9c8f007c2 100644 --- a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.html +++ b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.html @@ -27,101 +27,104 @@ } @else { @if (showRates) { -
- 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. -
++ 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. +
-| Rate | -Percentage | -Effective from | -Status | -- |
|---|---|---|---|---|
| {{ rate.rateType }} | -{{ rate.percentage }}% | -{{ rate.effectiveFrom }} | -
- @if (isFuture(rate)) {
- Future-dated
- } @else if (currentRate(rate.rateType)?.id === rate.id) {
- In force
- } @else {
- Superseded
- }
+
+ @for (type of ['CRR', 'SLR', 'SAVINGS_DEMAND']; track type) {
+
+
+ {{ type === 'SAVINGS_DEMAND' ? 'Savings demand share' : type }} in force today
+
+ @if (currentRate(type); as rate) {
+ {{ rate.percentage }}%
@if (rate.seedDefault) {
- Placeholder
+
+ Placeholder — confirm the rate actually in force
+
+ } @else {
+ since {{ rate.effectiveFrom }}
}
- |
- - - | -
| Rate | +Percentage | +Effective from | +Status | ++ |
|---|---|---|---|---|
| + {{ rate.rateType }} + | +{{ rate.percentage }}% | +{{ rate.effectiveFrom }} | ++ @if (isFuture(rate)) { + Future-dated + } @else if (currentRate(rate.rateType)?.id === rate.id) { + In force + } @else { + Superseded + } + @if (rate.seedDefault) { + Placeholder + } + | ++ + | +
+ Naming a head Memorandum - …, Annexure A - … or + Annexure B - … prints it under that section of the return instead of in the main + statement. +
+ diff --git a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.scss b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.scss index 4b597578c..2660489ab 100644 --- a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.scss +++ b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.scss @@ -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; diff --git a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.ts b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.ts index 3499691c4..73a9c7de2 100644 --- a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.ts +++ b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.ts @@ -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 ?? ''}`)); } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts index 921f9773e..0f6bb338f 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts @@ -308,6 +308,11 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha setRequired('incomeFromPenaltyAccountId', cashOrAccrual); setRequired('feesReceivableAccountId', accrualOnly); setRequired('penaltiesReceivableAccountId', accrualOnly); + // Required only on accrual, where there is no posting without it. On cash it stays optional but + // is offered, because interest posting routes through it when the product names one (see + // AccountingProcessorHelper.createInterestPostingJournalEntriesForSavings): the payable carries + // interest declared but not yet credited, and a product that names none collapses straight to + // Savings Control instead. setRequired('interestPayableAccountId', accrualOnly); setRequired('escheatLiabilityId', cashOrAccrual && this.isDormancyTrackingEnabled()); @@ -326,8 +331,8 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha rows: [ { key: 'savingsReferenceAccountId', - label: 'Savings Reference Account', - inputLabel: 'Savings reference account', + label: 'Fixed Deposit Reference Account', + inputLabel: 'Fixed deposit reference account', controlName: 'savingsReferenceAccountId', options: this.assetAccountOptions, required: true @@ -360,8 +365,8 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha rows: [ { key: 'savingsControlAccountId', - label: 'Savings Control Account', - inputLabel: 'Savings control account', + label: 'Fixed Deposit Control Account', + inputLabel: 'Fixed deposit control account', controlName: 'savingsControlAccountId', options: this.liabilityAccountOptions, required: true @@ -375,12 +380,16 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha required: true }, { + // Offered on cash accounting too, not only accrual: interest posting routes through + // this account whichever basis the product uses, so that interest declared but not yet + // credited to the member reads as the bank's own liability rather than appearing only + // when it is paid. Optional on cash - a product naming none posts straight to Savings + // Control as before. key: 'interestPayableAccountId', label: 'Interest Payable Account', inputLabel: 'Interest payable account', controlName: 'interestPayableAccountId', - options: this.liabilityAccountOptions, - accrualOnly: true + options: this.liabilityAccountOptions }, { key: 'escheatLiabilityId', @@ -402,8 +411,8 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha rows: [ { key: 'interestOnSavingsAccountId', - label: 'Interest On Savings Account', - inputLabel: 'Interest on savings account', + label: 'Interest On Fixed Deposits', + inputLabel: 'Interest on fixed deposits', controlName: 'interestOnSavingsAccountId', options: this.expenseAccountOptions, required: true diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html index e92b052fd..8a6431b90 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html @@ -481,6 +481,7 @@