Merge branch 'dev-latest' into feature/bugfix

This commit is contained in:
2026-09-25 11:06:16 +05:30
70 changed files with 5600 additions and 565 deletions
@@ -24,6 +24,13 @@
</button>
</div>
@if (loadError) {
<div class="tdsr__banner tdsr__banner--warn">
<fa-icon icon="exclamation-triangle"></fa-icon>
<div>{{ loadError }}</div>
</div>
}
<!-- Summary -->
<div class="tdsr__kpis">
<div class="tdsr__kpi">
@@ -15,7 +15,7 @@ import * as ExcelJS from 'exceljs';
import { BranchScopeService } from '../core/branch-scope.service';
import { GL_BRANCHES, glBranchName } from '../gl-reports/gl-reports-data';
import { FY_TAX_CONFIG, Quarter, QUARTER_LABEL } from '../letter-mgmt/certificates/form-15gh/tds-15gh.compute';
import { setTdsDeductees, TDS_FYS } from './tds-reports-data';
import { setTdsDeductees, tdsFys } from './tds-reports-data';
import { TdsReportsService } from './tds-reports.service';
import {
NEAR_THRESHOLD_BAND,
@@ -41,7 +41,8 @@ export class TdsInterestReportComponent implements OnInit {
private tdsService = inject(TdsReportsService);
readonly branches = GL_BRANCHES.filter((b) => b.code !== 'HO');
readonly financialYears = TDS_FYS;
/** Filled by the route resolver from Organization › TDS Configuration. */
readonly financialYears = tdsFys();
readonly quarterLabel = QUARTER_LABEL;
readonly nearBand = NEAR_THRESHOLD_BAND;
readonly quarters: Quarter[] = [
@@ -52,7 +53,7 @@ export class TdsInterestReportComponent implements OnInit {
];
filter: TdsReportFilter = {
fy: TDS_FYS[0],
fy: tdsFys()[0] ?? '',
branchCode: '',
quarter: 0,
band: '',
@@ -93,6 +94,10 @@ export class TdsInterestReportComponent implements OnInit {
loadError = '';
ngOnInit(): void {
if (!this.financialYears.length) {
this.loadError = 'No financial year is configured for TDS. Set one up under Organization › TDS Configuration.';
return;
}
// A branch login sees only its own book and cannot widen the scope.
const own = this.scope.ownBranchCode;
if (own) {
@@ -116,20 +121,29 @@ export class TdsInterestReportComponent implements OnInit {
next: (deductees) => {
this.loading = false;
setTdsDeductees(deductees);
this.summaryCacheKey = '';
this.cachedSummary = null;
this.invalidateCaches();
},
error: (err: any) => {
this.loading = false;
setTdsDeductees([]);
this.summaryCacheKey = '';
this.cachedSummary = null;
this.invalidateCaches();
this.loadError =
err?.error?.defaultUserMessage || 'Could not load deposit interest. The figures below cannot be shown.';
}
});
}
/**
* Both caches are keyed on the filter alone, so a fresh load must drop them together. Clearing
* only the summary left the KPIs counting the new data while the tables kept the empty list
* cached on the first render, before the load returned.
*/
private invalidateCaches(): void {
this.rowsCacheKey = '';
this.summaryCacheKey = '';
this.cachedSummary = null;
}
/**
* A different year needs a different set of interest credits, so the year re-reads from the
* platform rather than re-filtering the set already in hand.
@@ -14,7 +14,7 @@ import * as ExcelJS from 'exceljs';
import { BranchScopeService } from '../core/branch-scope.service';
import { FY_TAX_CONFIG, QUARTER_LABEL } from '../letter-mgmt/certificates/form-15gh/tds-15gh.compute';
import { TDS_AS_AT, TDS_FYS } from './tds-reports-data';
import { TDS_AS_AT, tdsFys } from './tds-reports-data';
import { Tds26qSummary, TdsRemittanceRow, tds26qSummary, tdsRemittanceRows } from './tds-reports.compute';
@Component({
@@ -30,11 +30,12 @@ import { Tds26qSummary, TdsRemittanceRow, tds26qSummary, tdsRemittanceRows } fro
export class TdsRemittanceReportComponent {
private scope = inject(BranchScopeService);
readonly financialYears = TDS_FYS;
/** Filled by the route resolver from Organization › TDS Configuration. */
readonly financialYears = tdsFys();
readonly quarterLabel = QUARTER_LABEL;
readonly asAt = TDS_AS_AT;
fy = TDS_FYS[0];
fy = tdsFys()[0] ?? '';
exporting = false;
/**
+6 -6
View File
@@ -20,17 +20,17 @@
import { GL_BRANCHES } from '../gl-reports/gl-reports-data';
import {
configuredFys,
Declaration,
Form15Kind,
InterestCredit,
TdsAccountInput
} from '../letter-mgmt/certificates/form-15gh/tds-15gh.compute';
/** The years the seed covers. 2025-26 is complete; 2026-27 is in progress. */
export const TDS_FYS = [
'2026-27',
'2025-26'
];
/** The years the reports can show: every year configured on Organization › TDS Configuration. */
export function tdsFys(): string[] {
return configuredFys();
}
/** Business date the book is drawn to — Q1 of 2026-27 is closed, Q2 is running. */
export const TDS_AS_AT = '2026-08-01';
@@ -205,7 +205,7 @@ export function tdsChallans(deductedByQuarter: Record<string, Record<number, num
}
const out: Record<string, TdsChallan[]> = {};
for (const fy of TDS_FYS) {
for (const fy of Object.keys(deductedByQuarter)) {
const y = Number(fy.slice(0, 4));
const months: Record<number, string> = {
1: `${y}-06`,
@@ -7,7 +7,8 @@
*/
import { isTdsApplicable, tdsDeducteeOf } from '../letter-mgmt/certificates/form-15gh/tds-15gh.compute';
import { TDS_AS_AT, TDS_FYS, depositDueDate, tdsDeductees } from './tds-reports-data';
import { installTdsConfigFixture } from '../letter-mgmt/certificates/form-15gh/tds-config.fixture';
import { TDS_AS_AT, depositDueDate, tdsDeductees, tdsFys } from './tds-reports-data';
import {
NEAR_THRESHOLD_BAND,
TdsReportFilter,
@@ -19,6 +20,8 @@ import {
tdsRemittanceRows
} from './tds-reports.compute';
installTdsConfigFixture();
/** The complete year in the seed — the one a return would be filed for. */
const CLOSED_FY = '2025-26';
@@ -75,7 +78,7 @@ describe('the reports follow the Act that governed each year', () => {
});
it('keeps the age-based threshold band in both years', () => {
for (const fy of TDS_FYS) {
for (const fy of tdsFys()) {
const rows = tdsInterestRowsForFy(fy);
expect(rows.some((r) => r.band === 'SENIOR')).toBe(true);
expect(rows.some((r) => r.band === 'GENERAL')).toBe(true);
@@ -240,7 +243,7 @@ describe('remittance schedule', () => {
});
describe('the 26Q reconciliation control', () => {
it.each(TDS_FYS)('balances for FY %s', (fy) => {
it.each(tdsFys())('balances for FY %s', (fy) => {
const s = tds26qSummary(fy, TDS_AS_AT);
// Deducted must equal deposited plus what is still within its due date.
// Anything else means tax has gone missing between the ledger and the
+3 -1
View File
@@ -27,6 +27,7 @@ import {
declarationFormFor,
formKindFor,
fyOf,
fyTaxConfigVersion,
isSeniorInFy,
isTdsApplicable,
isWellFormedPan,
@@ -118,7 +119,8 @@ let rowCache: TdsInterestRow[] = [];
export function tdsInterestRowsForFy(fy: string): TdsInterestRow[] {
// Keyed on the data set as well as the year: the rows are memoised, and a fresh load would
// otherwise keep serving the previous customer's figures under the same financial year.
const key = fy + '|' + tdsDeducteesVersion;
// The tax configuration too: an administrator's save must not be hidden behind rows built on the old rates.
const key = fy + '|' + tdsDeducteesVersion + '|' + fyTaxConfigVersion;
if (rowCacheKey === key) {
return rowCache;
}
+6 -8
View File
@@ -13,6 +13,7 @@ import { Observable, map } from 'rxjs';
/** Custom Helpers */
import { toIsoDate } from '../core/utils/api-date';
import { configuredFys } from '../letter-mgmt/certificates/form-15gh/tds-15gh.compute';
import { TdsDeductee } from './tds-reports-data';
/**
@@ -77,14 +78,11 @@ export class TdsReportsService {
return {};
}
const out: Record<string, any> = {};
// Applied to the years the reports can show. Without an acceptance date on the record the
// declaration is treated as operative from the start of each year rather than mid-year.
for (const year of [
2025,
2026
]) {
const fy = `FY ${year}-${String(year + 1).slice(2)}`;
out[fy] = { kind, acceptedOn: `${year}-04-01`, fy };
// Applied to every configured year. Without an acceptance date on the record the declaration is
// treated as operative from the start of each year rather than mid-year. Keyed '2026-27', the
// form the computation looks it up by - an 'FY 2026-27' key was never found.
for (const fy of configuredFys()) {
out[fy] = { kind, acceptedOn: `${fy.slice(0, 4)}-04-01`, fy };
}
return out;
}