diff --git a/src/app/clients/clients-view/form121-tab/form121-tab.component.ts b/src/app/clients/clients-view/form121-tab/form121-tab.component.ts index 161f74dbf..eb5078526 100644 --- a/src/app/clients/clients-view/form121-tab/form121-tab.component.ts +++ b/src/app/clients/clients-view/form121-tab/form121-tab.component.ts @@ -321,7 +321,8 @@ export class Form121TabComponent implements OnInit, OnDestroy { this.availableFys = [...fys].sort().reverse(); if (!this.availableFys.includes(this.fy)) { - this.fy = this.availableFys[0] ?? this.declarations[0]?.fy ?? Object.keys(FY_TAX_CONFIG).sort().reverse()[0] ?? ''; + this.fy = + this.availableFys[0] ?? this.declarations[0]?.fy ?? Object.keys(FY_TAX_CONFIG).sort().reverse()[0] ?? ''; } } @@ -367,9 +368,7 @@ export class Form121TabComponent implements OnInit, OnDestroy { : 0; } // What was actually debited for this customer as deductee, wherever the savings account sits. - this.actualTds = this.accountInputs - .filter((a) => a.meta.tdsApplicable) - .reduce((s, a) => s + a.meta.tdsDeducted, 0); + this.actualTds = this.accountInputs.filter((a) => a.meta.tdsApplicable).reduce((s, a) => s + a.meta.tdsDeducted, 0); if (!this.dateOfBirth || this.configMissing) { return; diff --git a/src/app/home/dashboard/crr-slr-dialog/crr-slr-dialog.component.html b/src/app/home/dashboard/crr-slr-dialog/crr-slr-dialog.component.html index 20d63ccac..e331d938c 100644 --- a/src/app/home/dashboard/crr-slr-dialog/crr-slr-dialog.component.html +++ b/src/app/home/dashboard/crr-slr-dialog/crr-slr-dialog.component.html @@ -74,9 +74,7 @@ (3%)" while Admin holds 4% states a rate the bank is not on. -->
|
{{ row.srNo }}{{ row.particulars }}
+
+ {{ row.glAccountId == null ? 'GL not mapped' : glLabel(row) }}
+
+ |
{{ canEdit ? 'Cancel' : 'Back to list' }}
diff --git a/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.scss b/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.scss
index 78c8ccc5b..25e958ccc 100644
--- a/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.scss
+++ b/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.scss
@@ -70,6 +70,85 @@
}
}
+ &__gl {
+ margin: 3px 0 0 30px;
+ }
+
+ &__gl-tag {
+ display: inline-block;
+ font-size: 10px;
+ font-weight: 600;
+ color: #475569;
+ background: #f1f5f9;
+ border-radius: 999px;
+ padding: 1px 8px;
+ white-space: nowrap;
+
+ &--unmapped {
+ color: #92400e;
+ background: #fef3c7;
+ }
+
+ &--editable {
+ cursor: pointer;
+
+ &:hover {
+ outline: 1px solid #94a3b8;
+ }
+ }
+ }
+
+ &__gl-select {
+ display: block;
+ margin: 4px 0 0 30px;
+ max-width: 340px;
+ }
+
+ &__gl-warn {
+ color: #b45309;
+ font-weight: 600;
+ }
+
+ &__add-panel {
+ padding: 14px 18px;
+ border-bottom: 1px solid #f1f5f9;
+ background: #f8fafc;
+ }
+
+ &__add-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: #0f172a;
+ margin-bottom: 10px;
+ }
+
+ &__add-fields {
+ display: flex;
+ align-items: flex-end;
+ gap: 12px;
+ flex-wrap: wrap;
+
+ .er__field {
+ min-width: 240px;
+ }
+
+ .er__field--gl {
+ min-width: 320px;
+ }
+ }
+
+ &__add-hint {
+ margin-top: 8px;
+ font-size: 12px;
+ color: #64748b;
+ }
+
+ &__add-error {
+ margin-top: 8px;
+ font-size: 12px;
+ color: #b91c1c;
+ }
+
&__readonly-banner {
display: flex;
align-items: center;
diff --git a/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.ts b/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.ts
index 329cdd8f7..1900d114c 100644
--- a/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.ts
+++ b/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.ts
@@ -6,7 +6,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import { Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
+import { Component, ElementRef, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
+import { MatSnackBar } from '@angular/material/snack-bar';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
@@ -18,7 +19,8 @@ import { MatIconModule } from '@angular/material/icon';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from '@angular/material/core';
import { RateRegisterService } from '../rate-register.service';
-import { RateCell, RateChartRow, RateRegister, SlabColumn } from '../models/rate-register.model';
+import { GlOption, RateCell, RateChartRow, RateRegister, SlabColumn } from '../models/rate-register.model';
+import { GlPickerComponent } from '../gl-picker/gl-picker.component';
/**
* Grid editor for one Loan Interest Rate Register — Excel-shaped: 34 products as rows and 10
@@ -39,10 +41,13 @@ import { RateCell, RateChartRow, RateRegister, SlabColumn } from '../models/rate
MatSelectModule,
MatIconModule,
MatDatepickerModule,
- MatNativeDateModule
+ MatNativeDateModule,
+ GlPickerComponent
]
})
-export class EditRegisterComponent implements OnInit {
+export class EditRegisterComponent implements OnInit, OnDestroy {
+ private readonly snack = inject(MatSnackBar);
+
private readonly service = inject(RateRegisterService);
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
@@ -58,11 +63,42 @@ export class EditRegisterComponent implements OnInit {
@ViewChild('cellInput') cellInput?: ElementRef No rate register versions yet
- Create the first version to publish rates for all loan products. The template comes pre-filled from the bank's
- approved Excel chart (34 products × 10 amount slabs + 5 CIBIL bands) — you can edit everything before
- submitting for approval.
+ Create the first version to publish rates for all loan products. It starts with the standard loan types (rates
+ blank) and 5 CIBIL bands — fill in the rates, add any bank-specific products, and submit for approval.
{{ row.srNo }}{{ row.particulars }}
+ {{
+ row.glAccountId == null ? 'GL not mapped' : glLabel(row.glAccountId)
+ }}
|
{{ display(cell) }} |
diff --git a/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.scss b/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.scss
index 0756a126f..13cad796d 100644
--- a/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.scss
+++ b/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.scss
@@ -424,6 +424,22 @@
}
}
+ &__gl-tag {
+ display: inline-block;
+ margin-left: 8px;
+ font-size: 10px;
+ font-weight: 600;
+ color: #475569;
+ background: #f1f5f9;
+ border-radius: 999px;
+ padding: 1px 8px;
+
+ &--unmapped {
+ color: #92400e;
+ background: #fef3c7;
+ }
+ }
+
&__history-foot {
padding: 10px 20px;
border-top: 1px solid #f1f5f9;
diff --git a/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.ts b/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.ts
index 2f4265b80..2fc9f859b 100644
--- a/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.ts
+++ b/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.ts
@@ -38,9 +38,15 @@ export class LoanInterestRateRegisterComponent implements OnInit {
active?: RateRegister;
ngOnInit(): void {
+ // Load the GL list once so mapped particulars show a readable label instead of "GL #id".
+ this.service.getGlOptions().subscribe();
this.reload();
}
+ glLabel(id: number | null | undefined): string | null {
+ return this.service.glLabel(id);
+ }
+
private reload(): void {
this.service.list().subscribe((rows) => {
this.registers = rows;
@@ -168,10 +174,11 @@ export class LoanInterestRateRegisterComponent implements OnInit {
/** Scaffold the very first version — used only when the register has no versions at all. */
createFirstVersion(): void {
- const draft = this.service.createFirstVersion();
- this.snack.open('Blank draft created — fill in rates and submit for approval', 'OK', { duration: 3000 });
- this.reload();
- this.edit(draft);
+ this.service.createFirstVersion().subscribe((draft) => {
+ this.snack.open('Draft created — fill in rates and submit for approval', 'OK', { duration: 3000 });
+ this.reload();
+ this.edit(draft);
+ });
}
/** Open the Compare screen for a pending register (checker view). */
diff --git a/src/app/products/loan-interest-rate-register/models/rate-register.model.ts b/src/app/products/loan-interest-rate-register/models/rate-register.model.ts
index 943cc6029..8f25e359f 100644
--- a/src/app/products/loan-interest-rate-register/models/rate-register.model.ts
+++ b/src/app/products/loan-interest-rate-register/models/rate-register.model.ts
@@ -21,9 +21,17 @@ export interface RateChartRow {
srNo: number;
particulars: string;
isFormula?: boolean;
+ /** GL account this particular is mapped to. null/undefined = "GL not mapped". */
+ glAccountId?: number | null;
rates: RateCell[];
}
+/** One selectable GL account for the particular-to-GL mapping. */
+export interface GlOption {
+ id: number;
+ label: string;
+}
+
export type RiskTier = 'V. Poor' | 'Poor' | 'Fair' | 'Good' | 'Excellent';
export interface CibilSlab {
diff --git a/src/app/products/loan-interest-rate-register/preview-register/preview-register.component.html b/src/app/products/loan-interest-rate-register/preview-register/preview-register.component.html
index a7262b2a9..8f258ebdf 100644
--- a/src/app/products/loan-interest-rate-register/preview-register/preview-register.component.html
+++ b/src/app/products/loan-interest-rate-register/preview-register/preview-register.component.html
@@ -48,121 +48,23 @@
-
-
-
-
-
-
-
-
-
-
- Rate chart — inline diff
- Changed cells highlighted · unchanged cells show single value
-
-
-
-
- No changes detected
-
- Draft is identical to the current Active register. Edit the Draft first to see diffs here.
-
- 0">
-
-
0">
-
+
-
-
-
- CIBIL slabs — inline diff
- Changed fields shown as OLD → NEW · unchanged fields render once
-
Rate+ Taken from the Loan Interest Rate Register
-
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-advance-interest-step/loan-product-advance-interest-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-advance-interest-step/loan-product-advance-interest-step.component.ts
index 89dbb9956..4aa3d2dda 100644
--- a/src/app/products/loan-products/loan-product-stepper/loan-product-advance-interest-step/loan-product-advance-interest-step.component.ts
+++ b/src/app/products/loan-products/loan-product-stepper/loan-product-advance-interest-step/loan-product-advance-interest-step.component.ts
@@ -6,9 +6,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import { Component, inject } from '@angular/core';
+import { Component, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+import { RegisterRateSelection, RegisterRateViewComponent } from '../register-rate-view/register-rate-view.component';
/**
* Interest step for an Advances A/C product (Figma step 5). Interest basis + spread, rate bounds,
@@ -18,13 +19,23 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
selector: 'mifosx-loan-product-advance-interest-step',
templateUrl: './loan-product-advance-interest-step.component.html',
styleUrls: ['../advance-steps.scss'],
- imports: [...STANDALONE_SHARED_IMPORTS]
+ imports: [
+ ...STANDALONE_SHARED_IMPORTS,
+ RegisterRateViewComponent
+ ]
})
export class LoanProductAdvanceInterestStepComponent {
private formBuilder = inject(UntypedFormBuilder);
loanProductAdvanceInterestForm: UntypedFormGroup;
+ /** Sub type and GL account picked on the Details step; they choose the Loan Interest Rate Register row. */
+ @Input() productSubType = '';
+ @Input() glAccountId: number | string | null = null;
+
+ /** The register row the product takes its rate from; null while the register has no row for it. */
+ registerSelection: RegisterRateSelection | null = null;
+
readonly interestBasisOptions = [
{ id: 'deposit_spread', label: 'Deposit Rate + Spread' },
{ id: 'fixed', label: 'Fixed rate' },
@@ -77,11 +88,12 @@ export class LoanProductAdvanceInterestStepComponent {
Validators.max(20)
]
],
- minRate: [
- '',
- Validators.required
- ],
- maxRate: [
+ // Filled from the register row; never typed in.
+ minRate: [''],
+ maxRate: [''],
+ defaultRate: [''],
+ // The rate comes from the Loan Interest Rate Register, so a product with no register row cannot be created.
+ registerParticular: [
'',
Validators.required
],
@@ -95,6 +107,33 @@ export class LoanProductAdvanceInterestStepComponent {
});
}
+ /**
+ * The register row decides the rate. A row of plain rates fixes the minimum and maximum; a deposit row
+ * ("1% + FD") is the deposit rate plus a spread, which has no plain minimum or maximum. With no row for the
+ * product the register particular stays empty, which keeps this step, and so the whole product, invalid until
+ * the product has been added to the register.
+ */
+ onRegisterSelection(selection: RegisterRateSelection | null): void {
+ this.registerSelection = selection;
+ const form = this.loanProductAdvanceInterestForm;
+ if (selection) {
+ form.patchValue(
+ {
+ interestBasis: selection.isFormula ? 'deposit_spread' : 'fixed',
+ spread: selection.isFormula ? selection.minSpread : 0,
+ minRate: selection.minRate ?? '',
+ maxRate: selection.maxRate ?? '',
+ defaultRate: selection.defaultRate ?? '',
+ registerParticular: selection.particular
+ },
+ { emitEvent: false }
+ );
+ } else {
+ form.patchValue({ minRate: '', maxRate: '', defaultRate: '', registerParticular: '' }, { emitEvent: false });
+ }
+ form.updateValueAndValidity({ emitEvent: false });
+ }
+
get showSpread(): boolean {
return this.loanProductAdvanceInterestForm.get('interestBasis').value === 'deposit_spread';
}
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts
index 57a5f6dc4..6eb65abd2 100644
--- a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts
+++ b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts
@@ -17,6 +17,7 @@ import { MatTooltip } from '@angular/material/tooltip';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+import { PRODUCT_SUB_TYPES, PRODUCT_TYPE_GROUPS } from '../../loan-product-types';
@Component({
selector: 'mifosx-loan-product-details-step',
@@ -110,35 +111,10 @@ export class LoanProductDetailsStepComponent implements OnInit {
isExistingProduct = false;
/** Product Type -- picked first, since it decides which sections the rest of the wizard shows. */
- readonly productTypeGroups = [
- { id: 'advances', label: 'Advances A/C', hint: 'No EMI. Limit / drawing power against security.' },
- { id: 'loans', label: 'Loans A/C', hint: 'Fixed disbursement, fixed tenure, EMI repayment.' }
- ];
+ /** Category and type lists are shared with the product list and the loan account product picker. */
+ readonly productTypeGroups = PRODUCT_TYPE_GROUPS;
- /** `shortName` is the two-letter code the product's Short Name is pre-filled with when the sub type is picked. */
- readonly productTypeSubGroups: Record
+
Reading the Loan Interest Rate Register…
+ }
+ @case ('none') {
+
+ There is no active Loan Interest Rate Register yet.
+
+ A product takes its interest rate from the register, so it cannot be created until a register is created and
+ approved.
+
+ Open the register →
+
+ }
+ @case ('ready') {
+ @if (selectedRow) {
+
+
+ Interest comes from the active register {{ register?.name }}, effective {{ register?.effectiveFrom }}.
+
+ Open register →
+
+ }
+
+ @if (!selectedRow) {
+
+ This product has no interest rate in the Loan Interest Rate Register yet.
+ {{ blockReason }}
+ Add it in the register →
+
+ }
+
+ @if (selectedRow) {
+
+ Register row
+ {{ selectedRow.srNo }}. {{ selectedRow.particulars }}
+ @if (glLabel(selectedRow); as gl) {
+ {{ gl }}
+ }
+ {{ matchNote }}
+
+
+
+
+ @if (selectedRow.isFormula || slabRates[0]?.formula) {
+
+ This row is the deposit's own rate plus a spread, so the final rate is worked out on each account from its
+ deposit.
+
+ }
+ }
+ }
+}
diff --git a/src/app/products/loan-products/loan-product-stepper/register-rate-view/register-rate-view.component.scss b/src/app/products/loan-products/loan-product-stepper/register-rate-view/register-rate-view.component.scss
new file mode 100644
index 000000000..4347cf852
--- /dev/null
+++ b/src/app/products/loan-products/loan-product-stepper/register-rate-view/register-rate-view.component.scss
@@ -0,0 +1,106 @@
+/**
+ * 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/.
+ */
+
+:host {
+ display: block;
+}
+
+.rrv__source {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+
+ a {
+ font-weight: 600;
+ white-space: nowrap;
+ }
+}
+
+.rrv__block {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px 16px;
+ margin: 16px 24px 0;
+ padding: 12px 16px;
+ border: 1px solid #fecaca;
+ border-radius: 8px;
+ background: #fef2f2;
+ color: #991b1b;
+ font-size: 13px;
+ line-height: 1.5;
+
+ strong {
+ flex: 1 1 100%;
+ font-size: 14px;
+ }
+
+ span {
+ flex: 1 1 360px;
+ }
+}
+
+.rrv__block-btn {
+ font-weight: 600;
+ color: #991b1b;
+ white-space: nowrap;
+}
+
+.rrv__row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px 14px;
+ padding: 14px 24px 4px;
+ font-size: 14px;
+ color: #1e293b;
+}
+
+.rrv__row-label {
+ font-size: 12px;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ color: #5f6368;
+}
+
+.rrv__row-gl {
+ padding: 1px 10px;
+ border-radius: 999px;
+ background: #e0f2fe;
+ color: #075985;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.rrv__row-note {
+ font-size: 12px;
+ color: #5f6368;
+}
+
+.rrv__tables {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 16px;
+ padding: 8px 24px 16px;
+
+ @media (width <= 900px) {
+ grid-template-columns: 1fr;
+ }
+}
+
+.rrv__formula {
+ color: #075985;
+ font-weight: 600;
+}
+
+.rrv__note {
+ padding: 0 24px 16px;
+ font-size: 13px;
+ color: #5f6368;
+}
diff --git a/src/app/products/loan-products/loan-product-stepper/register-rate-view/register-rate-view.component.ts b/src/app/products/loan-products/loan-product-stepper/register-rate-view/register-rate-view.component.ts
new file mode 100644
index 000000000..44d9a35cc
--- /dev/null
+++ b/src/app/products/loan-products/loan-product-stepper/register-rate-view/register-rate-view.component.ts
@@ -0,0 +1,220 @@
+/**
+ * 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, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, inject } from '@angular/core';
+import { RouterLink } from '@angular/router';
+import { RateRegisterService } from 'app/products/loan-interest-rate-register/rate-register.service';
+import {
+ CibilSlab,
+ RateCell,
+ RateChartRow,
+ RateRegister
+} from 'app/products/loan-interest-rate-register/models/rate-register.model';
+
+/** How the register row was found for the product. */
+export type RegisterMatch = 'gl' | 'type';
+
+/** What the product takes from the register row, handed to the step that owns the rate fields. */
+export interface RegisterRateSelection {
+ particular: string;
+ srNo: number;
+ glAccountId: number | null;
+ /** True when the row holds formulas such as "1% + FD": the rate is the deposit's own rate plus a spread. */
+ isFormula: boolean;
+ /** Lowest and highest plain rate in the row, the top one including the largest CIBIL adjustment. Null for a formula row. */
+ minRate: number | null;
+ maxRate: number | null;
+ /** The rate the product is created with: the lowest plain rate, or the lowest spread for a formula row. */
+ defaultRate: number | null;
+ /** Lowest and highest spread over the deposit rate; only for a formula row. */
+ minSpread: number | null;
+ maxSpread: number | null;
+ matchedBy: RegisterMatch;
+ registerName: string;
+}
+
+/**
+ * Product sub type id -> a word of the register particular that goes with it. This only covers the register's
+ * default rows. Any other product is found through its GL account, which every register row carries.
+ */
+const PARTICULAR_BY_SUB_TYPE: Record
+
diff --git a/src/app/products/manage-delinquency-buckets/delinquency-bucket/delinquency-bucket.component.ts b/src/app/products/manage-delinquency-buckets/delinquency-bucket/delinquency-bucket.component.ts
index 00e11022e..24059b71f 100644
--- a/src/app/products/manage-delinquency-buckets/delinquency-bucket/delinquency-bucket.component.ts
+++ b/src/app/products/manage-delinquency-buckets/delinquency-bucket/delinquency-bucket.component.ts
@@ -24,6 +24,7 @@ import {
} from '@angular/material/table';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { DelinquencyPendingRequestsComponent } from '../pending-requests/delinquency-pending-requests.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
@@ -33,6 +34,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
+ DelinquencyPendingRequestsComponent,
MatTable,
MatSort,
MatColumnDef,
diff --git a/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.ts b/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.ts
index 7272967f4..716396d55 100644
--- a/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.ts
+++ b/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.ts
@@ -12,6 +12,7 @@ import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { ProductsService } from 'app/products/products.service';
+import { AlertService } from 'app/core/alert/alert.service';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
@@ -59,6 +60,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
export class EditBucketComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private productsService = inject(ProductsService);
+ private alertService = inject(AlertService);
private router = inject(Router);
private route = inject(ActivatedRoute);
dialog = inject(MatDialog);
@@ -191,7 +193,14 @@ export class EditBucketComponent implements OnInit {
ranges: ranges
};
- this.productsService.updateDelinquencyBucket(this.delinquencyBucketId, data).subscribe(() => {
+ this.productsService.updateDelinquencyBucket(this.delinquencyBucketId, data).subscribe((response: any) => {
+ // Maker-checker: the change is applied only after a checker approves it.
+ if (response?.rollbackTransaction) {
+ this.alertService.alert({
+ type: 'Success',
+ message: 'Delinquency bucket change submitted for approval.'
+ });
+ }
this.router.navigate(['../'], { relativeTo: this.route });
});
}
diff --git a/src/app/products/manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component.ts b/src/app/products/manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component.ts
index ae086e225..08abc1ad2 100644
--- a/src/app/products/manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component.ts
+++ b/src/app/products/manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component.ts
@@ -10,6 +10,7 @@ import { Component, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ProductsService } from 'app/products/products.service';
+import { AlertService } from 'app/core/alert/alert.service';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -28,6 +29,7 @@ export class ViewBucketComponent {
private router = inject(Router);
private dialog = inject(MatDialog);
private productsService = inject(ProductsService);
+ private alertService = inject(AlertService);
/** Delinquency Bucket Data. */
delinquencyBucketData: any;
@@ -47,7 +49,14 @@ export class ViewBucketComponent {
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
- this.productsService.deleteDelinquencyBucket(this.delinquencyBucketData.id).subscribe(() => {
+ this.productsService.deleteDelinquencyBucket(this.delinquencyBucketData.id).subscribe((result: any) => {
+ // Maker-checker: the bucket is deleted only after a checker approves.
+ if (result?.rollbackTransaction) {
+ this.alertService.alert({
+ type: 'Success',
+ message: 'Delinquency bucket delete request submitted for approval.'
+ });
+ }
this.router.navigate(['../'], { relativeTo: this.route });
});
}
diff --git a/src/app/products/manage-delinquency-buckets/delinquency-range/create-range/create-range.component.ts b/src/app/products/manage-delinquency-buckets/delinquency-range/create-range/create-range.component.ts
index 8c0e5ce92..a58a558da 100644
--- a/src/app/products/manage-delinquency-buckets/delinquency-range/create-range/create-range.component.ts
+++ b/src/app/products/manage-delinquency-buckets/delinquency-range/create-range/create-range.component.ts
@@ -10,6 +10,7 @@ import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ProductsService } from 'app/products/products.service';
+import { AlertService } from 'app/core/alert/alert.service';
import { SettingsService } from 'app/settings/settings.service';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -24,6 +25,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
export class CreateRangeComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private productsService = inject(ProductsService);
+ private alertService = inject(AlertService);
private route = inject(ActivatedRoute);
private router = inject(Router);
private settingsService = inject(SettingsService);
@@ -71,6 +73,15 @@ export class CreateRangeComponent implements OnInit {
locale
};
this.productsService.createDelinquencyRange(data).subscribe((response: any) => {
+ // Maker-checker: the request waits in the Checker Inbox, so there is no range to open yet.
+ if (response?.rollbackTransaction) {
+ this.alertService.alert({
+ type: 'Success',
+ message: 'Delinquency range request submitted for approval.'
+ });
+ this.router.navigate(['../'], { relativeTo: this.route });
+ return;
+ }
this.router.navigate(
[
'../',
diff --git a/src/app/products/manage-delinquency-buckets/delinquency-range/delinquency-range.component.html b/src/app/products/manage-delinquency-buckets/delinquency-range/delinquency-range.component.html
index 4e56b3d84..3e2cdf8a5 100644
--- a/src/app/products/manage-delinquency-buckets/delinquency-range/delinquency-range.component.html
+++ b/src/app/products/manage-delinquency-buckets/delinquency-range/delinquency-range.component.html
@@ -7,6 +7,8 @@
-->
diff --git a/src/app/products/manage-delinquency-buckets/delinquency-range/delinquency-range.component.ts b/src/app/products/manage-delinquency-buckets/delinquency-range/delinquency-range.component.ts
index 41d218989..706f2c846 100644
--- a/src/app/products/manage-delinquency-buckets/delinquency-range/delinquency-range.component.ts
+++ b/src/app/products/manage-delinquency-buckets/delinquency-range/delinquency-range.component.ts
@@ -24,6 +24,7 @@ import {
} from '@angular/material/table';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { DelinquencyPendingRequestsComponent } from '../pending-requests/delinquency-pending-requests.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
@@ -33,6 +34,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
+ DelinquencyPendingRequestsComponent,
MatTable,
MatSort,
MatColumnDef,
diff --git a/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.ts b/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.ts
index a83a58af3..2aacfa487 100644
--- a/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.ts
+++ b/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.ts
@@ -10,6 +10,7 @@ import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ProductsService } from 'app/products/products.service';
+import { AlertService } from 'app/core/alert/alert.service';
import { SettingsService } from 'app/settings/settings.service';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -24,6 +25,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
export class EditRangeComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private productsService = inject(ProductsService);
+ private alertService = inject(AlertService);
private route = inject(ActivatedRoute);
private router = inject(Router);
private settingsService = inject(SettingsService);
@@ -78,6 +80,15 @@ export class EditRangeComponent implements OnInit {
locale
};
this.productsService.updateDelinquencyRange(this.delinquencyRangeData.id, data).subscribe((response: any) => {
+ // Maker-checker: the change is applied only after a checker approves it.
+ if (response?.rollbackTransaction) {
+ this.alertService.alert({
+ type: 'Success',
+ message: 'Delinquency range change submitted for approval.'
+ });
+ this.router.navigate(['../../'], { relativeTo: this.route });
+ return;
+ }
this.router.navigate(
[
'../../',
diff --git a/src/app/products/manage-delinquency-buckets/delinquency-range/view-range/view-range.component.ts b/src/app/products/manage-delinquency-buckets/delinquency-range/view-range/view-range.component.ts
index 85a6a1e6c..810b80da9 100644
--- a/src/app/products/manage-delinquency-buckets/delinquency-range/view-range/view-range.component.ts
+++ b/src/app/products/manage-delinquency-buckets/delinquency-range/view-range/view-range.component.ts
@@ -10,6 +10,7 @@ import { Component, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ProductsService } from 'app/products/products.service';
+import { AlertService } from 'app/core/alert/alert.service';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -28,6 +29,7 @@ export class ViewRangeComponent {
private router = inject(Router);
private dialog = inject(MatDialog);
private productsService = inject(ProductsService);
+ private alertService = inject(AlertService);
/** Delinquency Range Data. */
delinquencyRangeData: any;
@@ -44,7 +46,14 @@ export class ViewRangeComponent {
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
- this.productsService.deleteDelinquencyRange(this.delinquencyRangeData.id).subscribe(() => {
+ this.productsService.deleteDelinquencyRange(this.delinquencyRangeData.id).subscribe((result: any) => {
+ // Maker-checker: the range is deleted only after a checker approves.
+ if (result?.rollbackTransaction) {
+ this.alertService.alert({
+ type: 'Success',
+ message: 'Delinquency range delete request submitted for approval.'
+ });
+ }
this.router.navigate(['../'], { relativeTo: this.route });
});
}
diff --git a/src/app/products/manage-delinquency-buckets/pending-requests/delinquency-pending-requests.component.html b/src/app/products/manage-delinquency-buckets/pending-requests/delinquency-pending-requests.component.html
new file mode 100644
index 000000000..537a2e6a2
--- /dev/null
+++ b/src/app/products/manage-delinquency-buckets/pending-requests/delinquency-pending-requests.component.html
@@ -0,0 +1,55 @@
+
+
+@if (rows.length > 0) {
+
+
+ {{ rows.length }} request{{ rows.length > 1 ? 's' : '' }} awaiting approval
+ These changes take effect only after a checker approves them.
+ Open Approvals →
+
+
Made up to {{ statutorySecondColumn }}. The base dates above are the matching close one month earlier — change either - one if the bank files on a different base. + >. The base dates above are the matching close one month earlier — change either one if the bank files on + a different base. } @else {Enter both fortnight closing dates — the base dates fill in from them. @@ -715,7 +715,7 @@{{ amount(v) }} |
}
-
-
+
{{ headingTitle }}
@@ -114,10 +114,30 @@
}
+
+ @if (rateDiff) {
+
+
+ }
+
@if (showChanges) {
Rate Chart Comparison
+ @if (changesBaseline) {
+ {{ changesBaseline }} + } +Changes Requested ({{ changes.length }})
+ @if (changesBaseline) {
+ {{ changesBaseline }} + } @if (changes.length === 0) {This request does not change any value. } @else { @@ -143,7 +163,7 @@ } - @if (!showChanges && createSections.length > 0) { + @if (!showChanges && !rateDiff && !loanRateLoading && createSections.length > 0) { @for (section of createSections; track section.title) {{{ section.title }}
@@ -157,7 +177,7 @@
}
- @if (!showChanges && createSections.length === 0 && displayJSONData) {
+ @if (!showChanges && !rateDiff && !loanRateLoading && createSections.length === 0 && displayJSONData) {
{{ deletingEntity ? 'Record To Be Deleted' : ('labels.heading.Command' | translate) }}
diff --git a/src/app/tasks/view-checker-inbox/view-checker-inbox.component.scss b/src/app/tasks/view-checker-inbox/view-checker-inbox.component.scss
index 9bb961c1c..2dce81707 100644
--- a/src/app/tasks/view-checker-inbox/view-checker-inbox.component.scss
+++ b/src/app/tasks/view-checker-inbox/view-checker-inbox.component.scss
@@ -32,6 +32,11 @@ $bg-page: #f8fafc;
border-radius: 10px;
}
+ /* A wide payload (the Loan Base Interest Rate chart) fills the dialog the caller sized for it. */
+ &--dialog#{&}--wide {
+ width: 100%;
+ }
+
&:not(&--dialog) {
max-width: 900px;
margin: 20px auto;
diff --git a/src/app/tasks/view-checker-inbox/view-checker-inbox.component.ts b/src/app/tasks/view-checker-inbox/view-checker-inbox.component.ts
index ade448188..22a4a07b5 100644
--- a/src/app/tasks/view-checker-inbox/view-checker-inbox.component.ts
+++ b/src/app/tasks/view-checker-inbox/view-checker-inbox.component.ts
@@ -10,6 +10,7 @@
import { Component, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import * as _ from 'lodash';
+import { forkJoin } from 'rxjs';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
/** Custom Services */
@@ -31,6 +32,7 @@ import {
groupCreateFields,
humanizeKey
} from '../command-diff.util';
+import { diffLoanRateRegister, isLoanBaseInterestRateCreate } from '../loan-rate-register-diff.util';
import { deleteSummaryKeys, EntityFetchServices, fetchCurrentEntity } from '../maker-checker-entity-fetch';
/** Services the entity lookup table needs; assembled once and handed to the shared fetcher. */
@@ -42,6 +44,22 @@ import { UsersService } from 'app/users/users.service';
import { FixedDepositsService } from 'app/deposits/fixed-deposits/fixed-deposits.service';
import { RecurringDepositsService } from 'app/deposits/recurring-deposits/recurring-deposits.service';
import { OrganizationService } from 'app/organization/organization.service';
+import { RateRegisterService } from 'app/products/loan-interest-rate-register/rate-register.service';
+import { RateRegisterDiffComponent } from 'app/products/loan-interest-rate-register/rate-register-diff/rate-register-diff.component';
+import {
+ CibilSlab,
+ RateChartRow,
+ SlabColumn
+} from 'app/products/loan-interest-rate-register/models/rate-register.model';
+
+/** Inputs of the shared rate-register comparison view (see RateRegisterDiffComponent). */
+interface RateDiffView {
+ slabs: SlabColumn[];
+ draftRows: RateChartRow[];
+ baselineRows: RateChartRow[];
+ draftCibil: CibilSlab[];
+ baselineCibil: CibilSlab[];
+}
/**
* One row of the rendered Command payload. The dialog turns the parsed command JSON into a tree of
@@ -132,7 +150,8 @@ const ENTRY_LABEL_KEYS = [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
NgTemplateOutlet,
- DateFormatPipe
+ DateFormatPipe,
+ RateRegisterDiffComponent
]
})
export class ViewCheckerInboxComponent {
@@ -150,6 +169,7 @@ export class ViewCheckerInboxComponent {
private fixedDepositsService = inject(FixedDepositsService);
private recurringDepositsService = inject(RecurringDepositsService);
private organizationService = inject(OrganizationService);
+ private rateRegisterService = inject(RateRegisterService);
/** Bundle handed to the shared entity lookup, so that table stays free of Angular injection. */
private get entityFetchServices(): EntityFetchServices {
@@ -196,10 +216,22 @@ export class ViewCheckerInboxComponent {
/** UPDATE review: the fields this command actually moves, as OLD -> NEW. */
changes: CommandChange[] = [];
+ /** Note naming the version a Loan Base Interest Rate request is compared against. */
+ changesBaseline = '';
+ /** Loan Base Interest Rate request: the chart-style comparison (same view as the Compare screen). */
+ rateDiff: RateDiffView | null = null;
/** True once the current entity was read and a real comparison is on screen. */
showChanges = false;
/** True while the current entity is being fetched for comparison. */
comparing = false;
+
+ /** A Loan Base Interest Rate request still waiting for its chart: hides the raw payload meanwhile. */
+ get loanRateLoading(): boolean {
+ return (
+ this.comparing &&
+ isLoanBaseInterestRateCreate(this.checkerInboxDetail?.entityName, this.checkerInboxDetail?.actionName)
+ );
+ }
/** True when the screen is describing a record that is about to be deleted. */
deletingEntity = false;
/** CREATE review: submitted data bucketed into readable sections. */
@@ -354,6 +386,8 @@ export class ViewCheckerInboxComponent {
this.comparing = false;
this.deletingEntity = false;
this.createSections = [];
+ this.changesBaseline = '';
+ this.rateDiff = null;
// commandAsJson is absent on some entries (a DELETE carries no payload), and JSON.parse(undefined)
// throws — which as a route only broke the page, but in a dialog would leave an empty shell open.
try {
@@ -377,10 +411,16 @@ export class ViewCheckerInboxComponent {
} else if (action === 'DELETE') {
this.loadDeleteSummary(detail);
} else if (action === 'CREATE') {
+ if (isLoanBaseInterestRateCreate(detail?.entityName, detail?.actionName)) {
+ this.loadLoanRateRegisterChanges(detail);
+ }
this.createSections = this.buildCreateSections(this.jsondata);
if ((detail?.entityName ?? '').toUpperCase() === 'GLACCOUNT') {
this.resolveGlAccountCreateLabels(detail, this.jsondata);
}
+ if ((detail?.entityName ?? '').toUpperCase() === 'DELINQUENCY_BUCKET') {
+ this.resolveDelinquencyBucketCreateRanges(detail);
+ }
}
}
@@ -393,6 +433,10 @@ export class ViewCheckerInboxComponent {
* entity showed before.
*/
private loadUpdateChanges(detail: any): void {
+ if ((detail?.entityName ?? '').toUpperCase() === 'DELINQUENCY_BUCKET') {
+ this.loadDelinquencyBucketChanges(detail);
+ return;
+ }
const request$ = fetchCurrentEntity(detail?.entityName, detail?.resourceId, this.entityFetchServices);
if (!request$) {
return;
@@ -415,6 +459,131 @@ export class ViewCheckerInboxComponent {
});
}
+ /** One line per range for a delinquency bucket, e.g. 'SMA-1 (31 - 60 days)', oldest range first. */
+ private delinquencyRangeNames(ids: any[], rangesById: Map
- ₹{{ summary.totalTds | number: '1.0-0' }}
TDS due by the rules
+ ₹{{ summary.actualTds | number: '1.0-0' }}
Actually deducted from accounts
@@ -332,8 +336,11 @@
} @else if (row.schedule.totalTds || row.actualTds) {
₹{{ row.schedule.totalTds | number: '1.0-0' }}
due @ {{ row.rateApplied * 100 | number: '1.0-0' }}%
-
+
@@ -144,9 +146,10 @@
}
@@ -198,12 +201,16 @@
@for (row of rows; track trackRow($index, row)) {
₹{{ row.actualTds | number: '1.0-0' }} deducted
} @else if (row.tdsSaved) {
@@ -499,8 +506,8 @@
} @else if (selectedTab === 'paying') {
No customer needs TDS deducted for these filters. Check the "No TDS" tab.
} @else if (selectedTab === 'declared') {
- No customer has a Form 121 / 15G / 15H on record for this year. Record one from the customer's
- Form 121 tab.
+ No customer has a Form 121 / 15G / 15H on record for this year. Record one from the customer's Form
+ 121 tab.
} @else {
No customers match these filters.
}
diff --git a/src/app/tds-reports/tds-interest-report.component.ts b/src/app/tds-reports/tds-interest-report.component.ts
index 618066c16..ca781d191 100644
--- a/src/app/tds-reports/tds-interest-report.component.ts
+++ b/src/app/tds-reports/tds-interest-report.component.ts
@@ -226,7 +226,10 @@ export class TdsInterestReportComponent implements OnInit {
if (row.hasDeclaration) {
const beforeForm = Math.round(row.schedule.totalTds - row.actualTds);
if (beforeForm > 0) {
- return { label: `Form ${row.form} — ₹${beforeForm.toLocaleString('en-IN')} due from before the form, not yet deducted`, tone: 'warn' };
+ return {
+ label: `Form ${row.form} — ₹${beforeForm.toLocaleString('en-IN')} due from before the form, not yet deducted`,
+ tone: 'warn'
+ };
}
if (row.tdsSaved > 0) {
return { label: `Form ${row.form} — TDS not deducted`, tone: 'ok' };
@@ -244,7 +247,10 @@ export class TdsInterestReportComponent implements OnInit {
return { label: 'Over the limit — TDS deducted', tone: 'ok' };
}
if (row.actualTds > 0) {
- return { label: `₹${Math.round(row.actualTds).toLocaleString('en-IN')} deducted but none due — check`, tone: 'warn' };
+ return {
+ label: `₹${Math.round(row.actualTds).toLocaleString('en-IN')} deducted but none due — check`,
+ tone: 'warn'
+ };
}
if (row.hasDeclaration && row.crossed) {
return { label: 'Over the limit — 15G/15H, no TDS', tone: 'ok' };
diff --git a/src/app/tds-reports/tds-remittance-report.component.html b/src/app/tds-reports/tds-remittance-report.component.html
index 48dd34e94..27c8cda5b 100644
--- a/src/app/tds-reports/tds-remittance-report.component.html
+++ b/src/app/tds-reports/tds-remittance-report.component.html
@@ -45,8 +45,8 @@
@@ -123,7 +123,9 @@
Reconciled.
- ₹{{ summary.totalDeducted | number: '1.0-0' }} deducted, ₹{{ summary.totalDeposited | number: '1.0-0' }}
+ ₹{{ summary.totalDeducted | number: '1.0-0' }} deducted, ₹{{
+ summary.totalDeposited | number: '1.0-0'
+ }}
paid to the Government, ₹{{ summary.pendingDeposit | number: '1.0-0' }} still within its due date.
{{ quarterLabel[row.quarter] }} |
+
+ {{ quarterLabel[row.quarter] }}
+ |
{{ row.month + '-01' | date: 'MMM y' }}
|
- {{ row.postings }} deduction{{ row.postings === 1 ? '' : 's' }}
₹{{ row.deducted | number: '1.0-2' }} |
+
+ ₹{{ row.deducted | number: '1.0-2' }}
+ |
{{ row.dueDate | date: 'd MMM y' }} |
@for (c of row.challans; track c.id) {
@@ -330,9 +337,9 @@
}
|
- The "without deduction" line is not optional — interest paid gross against a declaration must be reported in the
- quarterly {{ statute.statement }} return with the appropriate remark code, and the consolidated statement of
- declarations received is uploaded {{ statute.frequency }}. Confirm the remark code against the current
+ The "without deduction" line is not optional — interest paid gross against a declaration must be reported in
+ the quarterly {{ statute.statement }} return with the appropriate remark code, and the consolidated statement
+ of declarations received is uploaded {{ statute.frequency }}. Confirm the remark code against the current
specification before generating the file.
diff --git a/src/app/tds-reports/tds-remittance-report.component.ts b/src/app/tds-reports/tds-remittance-report.component.ts
index d24ffc6be..ee29ee6b5 100644
--- a/src/app/tds-reports/tds-remittance-report.component.ts
+++ b/src/app/tds-reports/tds-remittance-report.component.ts
@@ -153,7 +153,8 @@ export class TdsRemittanceReportComponent implements OnInit {
},
error: (err: any) => {
this.saving = false;
- this.formError = err?.error?.errors?.[0]?.defaultUserMessage || err?.error?.defaultUserMessage || 'Could not save.';
+ this.formError =
+ err?.error?.errors?.[0]?.defaultUserMessage || err?.error?.defaultUserMessage || 'Could not save.';
}
});
}
@@ -172,7 +173,16 @@ export class TdsRemittanceReportComponent implements OnInit {
}
private blankForm(): NewTdsChallan {
- return { fy: this.fy, month: '', amount: 0, challanNo: '', bsrCode: '', depositedOn: '', paidFromGlAccountId: 0, remarks: '' };
+ return {
+ fy: this.fy,
+ month: '',
+ amount: 0,
+ challanNo: '',
+ bsrCode: '',
+ depositedOn: '',
+ paidFromGlAccountId: 0,
+ remarks: ''
+ };
}
trackRow(_: number, row: TdsRemittanceRow): string {
@@ -228,19 +238,58 @@ export class TdsRemittanceReportComponent implements OnInit {
recon.addRow([`${this.statute.statement} year-end position — FY ${this.fy}`]);
recon.addRow([]);
const lines: [string, number | string][] = [
- ['Deductees', s.deductees],
- ['Total interest paid on time deposits', s.totalInterestPaid],
- [' — with tax deducted', s.interestWithDeduction],
- [' — without deduction (declaration on record)', s.interestWithoutDeduction],
- ['TDS at standard rate (PAN on record)', s.tdsAtStandardRate],
- ['TDS at higher rate (no PAN)', s.tdsAtHigherRate],
- ['TDS due by the rules', s.calculatedTds],
- ['Total TDS actually deducted', s.totalDeducted],
- ['Total deposited with Government', s.totalDeposited],
- ['Still within due date', s.pendingDeposit],
- ['Overdue', s.overdueAmount],
- ['DIFFERENCE (must be zero)', s.difference],
- ['Interest payable for late deposit', s.defaultInterest]
+ [
+ 'Deductees',
+ s.deductees
+ ],
+ [
+ 'Total interest paid on time deposits',
+ s.totalInterestPaid
+ ],
+ [
+ ' — with tax deducted',
+ s.interestWithDeduction
+ ],
+ [
+ ' — without deduction (declaration on record)',
+ s.interestWithoutDeduction
+ ],
+ [
+ 'TDS at standard rate (PAN on record)',
+ s.tdsAtStandardRate
+ ],
+ [
+ 'TDS at higher rate (no PAN)',
+ s.tdsAtHigherRate
+ ],
+ [
+ 'TDS due by the rules',
+ s.calculatedTds
+ ],
+ [
+ 'Total TDS actually deducted',
+ s.totalDeducted
+ ],
+ [
+ 'Total deposited with Government',
+ s.totalDeposited
+ ],
+ [
+ 'Still within due date',
+ s.pendingDeposit
+ ],
+ [
+ 'Overdue',
+ s.overdueAmount
+ ],
+ [
+ 'DIFFERENCE (must be zero)',
+ s.difference
+ ],
+ [
+ 'Interest payable for late deposit',
+ s.defaultInterest
+ ]
];
for (const line of lines) {
recon.addRow(line);
diff --git a/src/app/tds-reports/tds-reports.compute.spec.ts b/src/app/tds-reports/tds-reports.compute.spec.ts
index ee0aea6e5..fd8fd919d 100644
--- a/src/app/tds-reports/tds-reports.compute.spec.ts
+++ b/src/app/tds-reports/tds-reports.compute.spec.ts
@@ -25,7 +25,12 @@ installTdsConfigFixture();
* shapes ("some row crossed") because nobody could know the numbers.
*/
const FY = '2025-26';
-const Q = ['2025-06-30', '2025-09-30', '2025-12-31', '2026-03-31'];
+const Q = [
+ '2025-06-30',
+ '2025-09-30',
+ '2025-12-31',
+ '2026-03-31'
+];
const fd = (id: number, amounts: number[], extra: object = {}) => ({
id,
@@ -68,23 +73,70 @@ function person(
const BOOK: TdsDeductee[] = [
// Over the limit in Q4, plus a savings account that must stay outside the figure.
person(1, 'Ramesh', '1980-05-01', 'ABCPR1234K', 'Head Office', [
- { ...fd(11, [15_000, 15_000, 15_000, 15_000]), deductions: [{ date: Q[3], amount: 6_000 }] },
+ {
+ ...fd(
+ 11,
+ [
+ 15_000,
+ 15_000,
+ 15_000,
+ 15_000
+ ]
+ ),
+ deductions: [{ date: Q[3], amount: 6_000 }]
+ },
sb(12, 1_200)
]),
// Senior citizen, no PAN: higher threshold, higher rate - and the platform deducted too little.
person(2, 'Sunita', '1955-01-01', '', 'bhopal', [
- { ...fd(21, [30_000, 30_000, 30_000, 30_000]), deductions: [{ date: Q[3], amount: 20_000 }] }
+ {
+ ...fd(
+ 21,
+ [
+ 30_000,
+ 30_000,
+ 30_000,
+ 30_000
+ ]
+ ),
+ deductions: [{ date: Q[3], amount: 20_000 }]
+ }
]),
// ₹6,000 short of the limit: the customer to ask for a declaration.
- person(3, 'Vitthal', '1985-03-03', 'ABCPV5678L', 'bhopal', [fd(31, [22_000, 22_000])]),
+ person(3, 'Vitthal', '1985-03-03', 'ABCPV5678L', 'bhopal', [
+ fd(
+ 31,
+ [
+ 22_000,
+ 22_000
+ ]
+ )
+ ]),
// Declaration accepted mid-year: interest before it stays liable, interest after it is paid gross.
- person(4, 'Kamala', '1979-07-07', 'ABCPK9012M', 'Head Office', [fd(41, [20_000, 20_000, 20_000, 20_000])], {
- [FY]: { kind: '15G', acceptedOn: '2025-10-15', fy: FY, uin: 'UIN0001' }
- }),
+ person(
+ 4,
+ 'Kamala',
+ '1979-07-07',
+ 'ABCPK9012M',
+ 'Head Office',
+ [
+ fd(
+ 41,
+ [
+ 20_000,
+ 20_000,
+ 20_000,
+ 20_000
+ ]
+ )
+ ],
+ {
+ [FY]: { kind: '15G', acceptedOn: '2025-10-15', fy: FY, uin: 'UIN0001' }
+ }
+ ),
// A minor's deposit, already listed under the guardian by the platform.
person(5, 'Ganpat', '1975-02-02', 'ABCPG3456N', 'Head Office', [
- { ...fd(51, [5_000]), holderClientId: 99, holderName: 'Baby Ganpat' }
- ]),
+ { ...fd(51, [5_000]), holderClientId: 99, holderName: 'Baby Ganpat' }]),
// Savings only, no PAN: never deducted, so never counted as a no-PAN problem.
person(6, 'Shobha', '1990-09-09', '', 'Head Office', [sb(61, 900)])
];
@@ -201,7 +253,10 @@ describe('the summary', () => {
describe('filters', () => {
it('narrows to a branch by office name, whatever its case, without re-aggregating', () => {
const scoped = tdsInterestRows(filter({ branchCode: 'BHOPAL' }));
- expect(scoped.map((r) => r.deductee.name).sort()).toEqual(['Sunita', 'Vitthal']);
+ expect(scoped.map((r) => r.deductee.name).sort()).toEqual([
+ 'Sunita',
+ 'Vitthal'
+ ]);
expect(scoped.find((r) => r.deductee.name === 'Sunita')!.schedule.totalTds).toBe(24_000);
});
@@ -211,7 +266,11 @@ describe('filters', () => {
tdsInterestRows(filter({ crossedOnly: true }))
.map((r) => r.deductee.name)
.sort()
- ).toEqual(['Kamala', 'Ramesh', 'Sunita']);
+ ).toEqual([
+ 'Kamala',
+ 'Ramesh',
+ 'Sunita'
+ ]);
});
it('searches on name, customer ID and PAN', () => {
@@ -242,7 +301,12 @@ describe('remittance', () => {
});
it('marks a month paid in full and on time', () => {
- const [r] = tdsRemittanceRows(FY, [{ month: '2026-03', amount: 26_000, postings: 2 }], [challan({ amount: 26_000 })], '2026-06-01');
+ const [r] = tdsRemittanceRows(
+ FY,
+ [{ month: '2026-03', amount: 26_000, postings: 2 }],
+ [challan({ amount: 26_000 })],
+ '2026-06-01'
+ );
expect(r.quarter).toBe(4);
expect(r.outstanding).toBe(0);
expect(r.late).toBe(false);
diff --git a/src/app/tds-reports/tds-reports.compute.ts b/src/app/tds-reports/tds-reports.compute.ts
index de0e0e568..547d0a933 100644
--- a/src/app/tds-reports/tds-reports.compute.ts
+++ b/src/app/tds-reports/tds-reports.compute.ts
@@ -446,7 +446,12 @@ export function tdsRemittanceRows(
asAt: string
): TdsRemittanceRow[] {
const live = challans.filter((c) => !c.reversed && c.fy === fy);
- const months = [...new Set([...deducted.map((d) => d.month), ...live.map((c) => c.month)])].sort();
+ const months = [
+ ...new Set([
+ ...deducted.map((d) => d.month),
+ ...live.map((c) => c.month)
+ ])
+ ].sort();
return months.map((month) => {
const d = deducted.find((x) => x.month === month);
@@ -533,9 +538,7 @@ export function tds26qSummary(fy: string, remittance: TdsRemittanceRow[], asAt:
const higher = rows.filter((r) => !r.panOnRecord);
const totalDeducted = round2(remittance.reduce((s, r) => s + r.deducted, 0));
const totalDeposited = round2(remittance.reduce((s, r) => s + r.deposited, 0));
- const pendingDeposit = round2(
- remittance.filter((r) => r.dueDate >= asAt).reduce((s, r) => s + r.outstanding, 0)
- );
+ const pendingDeposit = round2(remittance.filter((r) => r.dueDate >= asAt).reduce((s, r) => s + r.outstanding, 0));
const overdueAmount = round2(remittance.filter((r) => r.overdue).reduce((s, r) => s + r.outstanding, 0));
return {
diff --git a/src/app/tds-reports/tds-reports.service.ts b/src/app/tds-reports/tds-reports.service.ts
index 89d50c912..27b7730d4 100644
--- a/src/app/tds-reports/tds-reports.service.ts
+++ b/src/app/tds-reports/tds-reports.service.ts
@@ -120,7 +120,9 @@ export class TdsReportsService {
}
createChallan(challan: NewTdsChallan): Observable |