feat: update TDS config, RBI reserve returns, current products, and dormancy reports
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
ageInFy,
|
||||
buildSchedule,
|
||||
checkEligibility,
|
||||
configuredFys,
|
||||
declarationFormFor,
|
||||
formKindFor,
|
||||
isSeniorInFy,
|
||||
@@ -134,7 +135,10 @@ export class Form15ghComponent implements OnInit {
|
||||
// Derived once. The template reads these many times per change-detection
|
||||
// pass, and re-running the schedules on every read is what made the other
|
||||
// register screens stall.
|
||||
this.rows = seedForm15gh().map((record) => this.buildRow(record));
|
||||
// Only years that are configured can be computed; the engine throws on any other.
|
||||
this.rows = seedForm15gh()
|
||||
.filter((record) => !!FY_TAX_CONFIG[normaliseFy(record.fy)])
|
||||
.map((record) => this.buildRow(record));
|
||||
}
|
||||
|
||||
private buildRow(record: Form15ghRecord): Form15ghRow {
|
||||
@@ -219,13 +223,13 @@ export class Form15ghComponent implements OnInit {
|
||||
* than written into the template.
|
||||
*/
|
||||
get statute(): { form: string; section: string; statement: string; frequency: string } {
|
||||
const fy = normaliseFy(this.rows[0]?.record.fy ?? '2026-27');
|
||||
const fy = normaliseFy(this.rows[0]?.record.fy ?? configuredFys()[0] ?? '');
|
||||
const cfg = FY_TAX_CONFIG[fy];
|
||||
return {
|
||||
form: cfg.declarationForm === '121' ? 'Form 121' : 'Form 15G / 15H',
|
||||
section: cfg.interestTdsSection,
|
||||
statement: cfg.tdsStatementForm,
|
||||
frequency: cfg.declarationStatementFrequency === 'MONTHLY' ? 'monthly' : 'quarterly'
|
||||
form: cfg?.declarationForm === '121' ? 'Form 121' : 'Form 15G / 15H',
|
||||
section: cfg?.interestTdsSection ?? '—',
|
||||
statement: cfg?.tdsStatementForm ?? '—',
|
||||
frequency: cfg?.declarationStatementFrequency === 'MONTHLY' ? 'monthly' : 'quarterly'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+10
-6
@@ -41,6 +41,7 @@ import {
|
||||
buildSchedule,
|
||||
checkEligibility,
|
||||
collectTdsCredits,
|
||||
configuredFys,
|
||||
declarationFormFor,
|
||||
formKindFor,
|
||||
isSeniorInFy,
|
||||
@@ -115,18 +116,15 @@ export class RealForm15ghViewComponent implements OnInit, OnDestroy {
|
||||
|
||||
/** Which row's quarter-wise breakdown is open. */
|
||||
expandedId: string | null = null;
|
||||
selectedFy = 'FY 2026-27';
|
||||
/** Every year configured on Organization › TDS Configuration, loaded by the route resolver. */
|
||||
readonly availableFys = configuredFys().map((fy) => `FY ${fy}`);
|
||||
selectedFy = this.availableFys[0] ?? '';
|
||||
|
||||
// Per-client caches
|
||||
private clientDataCache = new Map<string, Observable<any>>();
|
||||
private clientKycCache = new Map<string, Observable<any>>();
|
||||
private clientAccountsCache = new Map<string, Observable<any>>();
|
||||
|
||||
readonly availableFys = [
|
||||
'FY 2026-27',
|
||||
'FY 2025-26'
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadRealData();
|
||||
}
|
||||
@@ -137,6 +135,12 @@ export class RealForm15ghViewComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
loadRealData(): void {
|
||||
// No configured year means no rates to compute with; report it rather than throw mid-build.
|
||||
if (!this.selectedFy) {
|
||||
this.loading = false;
|
||||
this.loadError = true;
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.loadError = false;
|
||||
this.clientDataCache.clear();
|
||||
|
||||
@@ -30,6 +30,9 @@ import {
|
||||
tdsDeducteeOf,
|
||||
tdsModeFor
|
||||
} from './tds-15gh.compute';
|
||||
import { installTdsConfigFixture } from './tds-config.fixture';
|
||||
|
||||
installTdsConfigFixture();
|
||||
|
||||
const FY = 'FY 2026-27';
|
||||
|
||||
|
||||
@@ -129,73 +129,37 @@ export interface FyTaxConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Statutory figures for the years this book covers, on the new tax regime.
|
||||
* Statutory figures per financial year, keyed '2026-27'.
|
||||
*
|
||||
* Nothing is compiled in. The values live in m_tds_fy_config, are edited on
|
||||
* Organization › TDS Configuration, and are loaded into this registry by
|
||||
* `TdsConfigService` before any TDS screen opens (see `tdsConfigResolver`). A
|
||||
* year with no entry makes the computation throw rather than guess.
|
||||
*
|
||||
* For a customer who has opted out onto the old regime, `basicExemption` and
|
||||
* `nilTaxCeiling` are lower and vary by age; pass an override through
|
||||
* `TdsContext.config`.
|
||||
*
|
||||
* Thresholds and rates did not change at the 2026 changeover — ₹50,000,
|
||||
* ₹1,00,000 for senior citizens, 10%, and 20% without PAN carried straight
|
||||
* across. What changed is the declaration and the return it is reported in.
|
||||
*/
|
||||
export const FY_TAX_CONFIG: Record<string, FyTaxConfig> = {
|
||||
'2025-26': {
|
||||
fy: '2025-26',
|
||||
regime: 'ITA_1961',
|
||||
interestTdsSection: 'section 194A',
|
||||
thresholdSection: 'section 194A(3)(i)',
|
||||
noPanSection: 'section 206AA',
|
||||
defaultSection: 'section 201(1A)',
|
||||
declarationForm: '15G',
|
||||
declarationRule: 'Rule 29C',
|
||||
declarationSection: 'section 197A',
|
||||
tdsStatementForm: 'Form 26Q',
|
||||
tdsStatementRule: 'Rule 31A',
|
||||
certificateForm: 'Form 16A',
|
||||
declarationStatementFrequency: 'QUARTERLY',
|
||||
declarationStatementDueDay: 15,
|
||||
thresholdGeneral: 50_000,
|
||||
thresholdSenior: 100_000,
|
||||
rate: 0.1,
|
||||
rateNoPan: 0.2,
|
||||
basicExemption: 400_000,
|
||||
nilTaxCeiling: 1_200_000,
|
||||
roundTo: 1,
|
||||
lateDeductionRate: 0.01,
|
||||
lateDepositRate: 0.015
|
||||
},
|
||||
'2026-27': {
|
||||
fy: '2026-27',
|
||||
regime: 'ITA_2025',
|
||||
// Old section 194A was folded into the omnibus TDS section of the 2025 Act.
|
||||
interestTdsSection: 'section 393(1) [Table Sl. 5(ii)/(iii)]',
|
||||
thresholdSection: 'section 393(4) [Table Sl. 7]',
|
||||
// 206AA and 206CC merged here. The reduced 5% limb applies only to Sl. 8(ii)
|
||||
// and 8(v), so interest stays at 20% without PAN.
|
||||
noPanSection: 'section 397(2)',
|
||||
defaultSection: 'section 398',
|
||||
declarationForm: '121',
|
||||
declarationRule: 'Rule 211 of the Income-tax Rules, 2026',
|
||||
declarationSection: 'section 393(6)',
|
||||
tdsStatementForm: 'Form 140',
|
||||
tdsStatementRule: 'Rule 219 of the Income-tax Rules, 2026',
|
||||
certificateForm: 'Form 131',
|
||||
// The consolidated statement of declarations received moved from a quarterly
|
||||
// upload to a monthly one, due on the 7th of the following month.
|
||||
declarationStatementFrequency: 'MONTHLY',
|
||||
declarationStatementDueDay: 7,
|
||||
thresholdGeneral: 50_000,
|
||||
thresholdSenior: 100_000,
|
||||
rate: 0.1,
|
||||
rateNoPan: 0.2,
|
||||
basicExemption: 400_000,
|
||||
nilTaxCeiling: 1_200_000,
|
||||
roundTo: 1,
|
||||
lateDeductionRate: 0.01,
|
||||
lateDepositRate: 0.015
|
||||
export const FY_TAX_CONFIG: Record<string, FyTaxConfig> = {};
|
||||
|
||||
/** Bumped on every load, so memoised computations can tell one configuration from the next. */
|
||||
export let fyTaxConfigVersion = 0;
|
||||
|
||||
/** Replaces the whole registry with what the platform holds. */
|
||||
export function setFyTaxConfigs(configs: FyTaxConfig[]): void {
|
||||
for (const key of Object.keys(FY_TAX_CONFIG)) {
|
||||
delete FY_TAX_CONFIG[key];
|
||||
}
|
||||
};
|
||||
for (const cfg of configs || []) {
|
||||
FY_TAX_CONFIG[normaliseFy(cfg.fy)] = cfg;
|
||||
}
|
||||
fyTaxConfigVersion++;
|
||||
}
|
||||
|
||||
/** The configured years, latest first. */
|
||||
export function configuredFys(): string[] {
|
||||
return Object.keys(FY_TAX_CONFIG).sort().reverse();
|
||||
}
|
||||
|
||||
// ─────────────────────── Unique Identification Number ───────────────────────
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Test-only TDS configuration. The application reads these figures from Organization › TDS
|
||||
* Configuration; the specs install this copy of the seeded values so they run without a backend.
|
||||
*/
|
||||
|
||||
import { FyTaxConfig, setFyTaxConfigs } from './tds-15gh.compute';
|
||||
|
||||
export const TDS_CONFIG_FIXTURE: FyTaxConfig[] = [
|
||||
{
|
||||
fy: '2025-26',
|
||||
regime: 'ITA_1961',
|
||||
interestTdsSection: 'section 194A',
|
||||
thresholdSection: 'section 194A(3)(i)',
|
||||
noPanSection: 'section 206AA',
|
||||
defaultSection: 'section 201(1A)',
|
||||
declarationForm: '15G',
|
||||
declarationRule: 'Rule 29C',
|
||||
declarationSection: 'section 197A',
|
||||
tdsStatementForm: 'Form 26Q',
|
||||
tdsStatementRule: 'Rule 31A',
|
||||
certificateForm: 'Form 16A',
|
||||
declarationStatementFrequency: 'QUARTERLY',
|
||||
declarationStatementDueDay: 15,
|
||||
thresholdGeneral: 50_000,
|
||||
thresholdSenior: 100_000,
|
||||
rate: 0.1,
|
||||
rateNoPan: 0.2,
|
||||
basicExemption: 400_000,
|
||||
nilTaxCeiling: 1_200_000,
|
||||
roundTo: 1,
|
||||
lateDeductionRate: 0.01,
|
||||
lateDepositRate: 0.015
|
||||
},
|
||||
{
|
||||
fy: '2026-27',
|
||||
regime: 'ITA_2025',
|
||||
interestTdsSection: 'section 393(1) [Table Sl. 5(ii)/(iii)]',
|
||||
thresholdSection: 'section 393(4) [Table Sl. 7]',
|
||||
noPanSection: 'section 397(2)',
|
||||
defaultSection: 'section 398',
|
||||
declarationForm: '121',
|
||||
declarationRule: 'Rule 211 of the Income-tax Rules, 2026',
|
||||
declarationSection: 'section 393(6)',
|
||||
tdsStatementForm: 'Form 140',
|
||||
tdsStatementRule: 'Rule 219 of the Income-tax Rules, 2026',
|
||||
certificateForm: 'Form 131',
|
||||
declarationStatementFrequency: 'MONTHLY',
|
||||
declarationStatementDueDay: 7,
|
||||
thresholdGeneral: 50_000,
|
||||
thresholdSenior: 100_000,
|
||||
rate: 0.1,
|
||||
rateNoPan: 0.2,
|
||||
basicExemption: 400_000,
|
||||
nilTaxCeiling: 1_200_000,
|
||||
roundTo: 1,
|
||||
lateDeductionRate: 0.01,
|
||||
lateDepositRate: 0.015
|
||||
}
|
||||
];
|
||||
|
||||
export function installTdsConfigFixture(): void {
|
||||
setFyTaxConfigs(TDS_CONFIG_FIXTURE);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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 { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
import { Observable, catchError, map, of, tap } from 'rxjs';
|
||||
|
||||
import { FyTaxConfig, setFyTaxConfigs } from './tds-15gh.compute';
|
||||
|
||||
/** One financial year as the platform stores it — percentages as entered, e.g. 10 for 10%. */
|
||||
export interface TdsFyConfigRecord {
|
||||
id?: number;
|
||||
fy: string;
|
||||
regime: string;
|
||||
interestTdsSection: string | null;
|
||||
thresholdSection: string | null;
|
||||
noPanSection: string | null;
|
||||
defaultSection: string | null;
|
||||
declarationForm: string;
|
||||
declarationRule: string | null;
|
||||
declarationSection: string | null;
|
||||
tdsStatementForm: string | null;
|
||||
tdsStatementRule: string | null;
|
||||
certificateForm: string | null;
|
||||
declarationStatementFrequency: string;
|
||||
declarationStatementDueDay: number;
|
||||
thresholdGeneral: number;
|
||||
thresholdSenior: number;
|
||||
ratePanPercentage: number;
|
||||
rateNoPanPercentage: number;
|
||||
basicExemption: number;
|
||||
nilTaxCeiling: number;
|
||||
roundTo: number;
|
||||
lateDeductionPercentage: number;
|
||||
lateDepositPercentage: number;
|
||||
seedDefault?: boolean;
|
||||
updatedOn?: string | null;
|
||||
}
|
||||
|
||||
export interface TdsGlRole {
|
||||
configKey: string;
|
||||
label: string;
|
||||
description: string;
|
||||
glAccountId: number | null;
|
||||
glCode: string | null;
|
||||
glAccountName: string | null;
|
||||
}
|
||||
|
||||
export interface TdsConfigResponse {
|
||||
years: TdsFyConfigRecord[];
|
||||
glRoles: TdsGlRole[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The single source of TDS thresholds, rates and statutory references.
|
||||
*
|
||||
* Every TDS screen computes from `FY_TAX_CONFIG`, and this service is the only thing that fills it.
|
||||
* Saves go through here too, so the registry is refreshed the moment an administrator changes a
|
||||
* figure and no open screen keeps computing on the old one.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TdsConfigService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
private last: TdsConfigResponse | null = null;
|
||||
|
||||
/** The GL roles as last loaded. */
|
||||
get glRoles(): TdsGlRole[] {
|
||||
return this.last?.glRoles ?? [];
|
||||
}
|
||||
|
||||
/** Reads the configuration and installs it into the engine. */
|
||||
load(): Observable<TdsConfigResponse> {
|
||||
return this.http.get<TdsConfigResponse>('/tdsconfig').pipe(tap((res) => this.install(res)));
|
||||
}
|
||||
|
||||
/** For route resolvers: never blocks navigation, the screen reports a missing year itself. */
|
||||
ensureLoaded(): Observable<boolean> {
|
||||
return this.load().pipe(
|
||||
map(() => true),
|
||||
catchError(() => {
|
||||
setFyTaxConfigs([]);
|
||||
return of(false);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
template(): Observable<any> {
|
||||
return this.http.get('/tdsconfig/template');
|
||||
}
|
||||
|
||||
saveYear(record: TdsFyConfigRecord): Observable<TdsConfigResponse> {
|
||||
return this.http
|
||||
.put<TdsConfigResponse>(`/tdsconfig/year/${record.fy}`, record)
|
||||
.pipe(tap((res) => this.install(res)));
|
||||
}
|
||||
|
||||
deleteYear(fy: string): Observable<TdsConfigResponse> {
|
||||
return this.http.delete<TdsConfigResponse>(`/tdsconfig/year/${fy}`).pipe(tap((res) => this.install(res)));
|
||||
}
|
||||
|
||||
saveGl(configKey: string, glAccountId: number | null): Observable<TdsConfigResponse> {
|
||||
return this.http
|
||||
.put<TdsConfigResponse>(`/tdsconfig/gl/${configKey}`, { glAccountId })
|
||||
.pipe(tap((res) => this.install(res)));
|
||||
}
|
||||
|
||||
private install(res: TdsConfigResponse): void {
|
||||
this.last = res;
|
||||
setFyTaxConfigs((res?.years || []).map(toFyTaxConfig));
|
||||
}
|
||||
}
|
||||
|
||||
/** Stored percentages become the fractions the engine multiplies by. */
|
||||
export function toFyTaxConfig(r: TdsFyConfigRecord): FyTaxConfig {
|
||||
return {
|
||||
fy: r.fy,
|
||||
regime: r.regime as FyTaxConfig['regime'],
|
||||
interestTdsSection: r.interestTdsSection ?? '—',
|
||||
thresholdSection: r.thresholdSection ?? '—',
|
||||
noPanSection: r.noPanSection ?? '—',
|
||||
defaultSection: r.defaultSection ?? '—',
|
||||
declarationForm: r.declarationForm as FyTaxConfig['declarationForm'],
|
||||
declarationRule: r.declarationRule ?? '—',
|
||||
declarationSection: r.declarationSection ?? '—',
|
||||
tdsStatementForm: r.tdsStatementForm ?? '—',
|
||||
tdsStatementRule: r.tdsStatementRule ?? '—',
|
||||
certificateForm: r.certificateForm ?? '—',
|
||||
declarationStatementFrequency: r.declarationStatementFrequency === 'MONTHLY' ? 'MONTHLY' : 'QUARTERLY',
|
||||
declarationStatementDueDay: Number(r.declarationStatementDueDay),
|
||||
thresholdGeneral: Number(r.thresholdGeneral),
|
||||
thresholdSenior: Number(r.thresholdSenior),
|
||||
rate: Number(r.ratePanPercentage) / 100,
|
||||
rateNoPan: Number(r.rateNoPanPercentage) / 100,
|
||||
basicExemption: Number(r.basicExemption),
|
||||
nilTaxCeiling: Number(r.nilTaxCeiling),
|
||||
roundTo: Number(r.roundTo) || 1,
|
||||
lateDeductionRate: Number(r.lateDeductionPercentage) / 100,
|
||||
lateDepositRate: Number(r.lateDepositPercentage) / 100
|
||||
};
|
||||
}
|
||||
|
||||
/** Put on every route whose screen computes TDS, so the figures are in place before it builds. */
|
||||
export const tdsConfigResolver: ResolveFn<boolean> = () => inject(TdsConfigService).ensureLoaded();
|
||||
@@ -16,6 +16,7 @@ import { ShareCertificateComponent } from './certificates/share-certificate/shar
|
||||
import { BalanceCertificateComponent } from './certificates/balance-certificate/balance-certificate.component';
|
||||
import { Form60Component } from './certificates/form-60/form-60.component';
|
||||
import { Form15ghComponent } from './certificates/form-15gh/form-15gh.component';
|
||||
import { tdsConfigResolver } from './certificates/form-15gh/tds-config.service';
|
||||
import { HypothecationCertificateComponent } from './certificates/hypothecation/hypothecation.component';
|
||||
import { NocCertificateComponent } from './certificates/noc/noc.component';
|
||||
import { DatabaseExportComponent } from './database-export/database-export.component';
|
||||
@@ -68,6 +69,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'letter-mgmt/form-15gh',
|
||||
component: Form15ghComponent,
|
||||
resolve: { tdsConfig: tdsConfigResolver },
|
||||
data: { title: 'Form 121', breadcrumb: 'Form 121' }
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user