Files
cbsnexora/src/app/tds-reports/tds-reports.compute.spec.ts
T
keshav 569f17ef46
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
kyc exception at client creation time and some tds work
2026-08-05 17:23:56 +05:30

302 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 { isTdsApplicable, tdsDeducteeOf } from '../letter-mgmt/certificates/form-15gh/tds-15gh.compute';
import { TDS_AS_AT, TDS_FYS, depositDueDate, tdsDeductees } from './tds-reports-data';
import {
NEAR_THRESHOLD_BAND,
TdsReportFilter,
deductedByQuarter,
tds26qSummary,
tdsInterestRows,
tdsInterestRowsForFy,
tdsInterestSummary,
tdsRemittanceRows
} from './tds-reports.compute';
/** The complete year in the seed — the one a return would be filed for. */
const CLOSED_FY = '2025-26';
function filter(overrides: Partial<TdsReportFilter> = {}): TdsReportFilter {
return {
fy: CLOSED_FY,
branchCode: '',
quarter: 0,
band: '',
search: '',
crossedOnly: false,
nearThresholdOnly: false,
...overrides
};
}
describe('the seed has every case the reports need to show', () => {
it('produces deductees on both sides of the threshold', () => {
const rows = tdsInterestRowsForFy(CLOSED_FY);
expect(rows.length).toBeGreaterThan(20);
expect(rows.some((r) => r.crossed)).toBe(true);
expect(rows.some((r) => !r.crossed)).toBe(true);
expect(rows.some((r) => r.nearThreshold)).toBe(true);
expect(rows.some((r) => r.band === 'GENERAL')).toBe(true);
expect(rows.some((r) => r.band === 'SENIOR')).toBe(true);
expect(rows.some((r) => !r.panOnRecord)).toBe(true);
expect(rows.some((r) => r.hasDeclaration)).toBe(true);
});
it('gives senior citizens the higher threshold', () => {
const rows = tdsInterestRowsForFy(CLOSED_FY);
for (const row of rows) {
expect(row.threshold).toBe(row.band === 'SENIOR' ? 100_000 : 50_000);
}
});
it('deducts at the higher rate wherever PAN is missing', () => {
for (const row of tdsInterestRowsForFy(CLOSED_FY)) {
expect(row.rateApplied).toBe(row.panOnRecord ? 0.1 : 0.2);
}
});
});
describe('the reports follow the Act that governed each year', () => {
it('shows 15G / 15H for FY 2025-26 and Form 121 for FY 2026-27', () => {
const old = tdsInterestRowsForFy('2025-26');
expect(old.some((r) => r.form === '15G')).toBe(true);
expect(old.some((r) => r.form === '15H')).toBe(true);
expect(old.some((r) => r.form === '121')).toBe(false);
// The two forms merged on 01 April 2026.
const current = tdsInterestRowsForFy('2026-27');
expect(current.every((r) => r.form === '121')).toBe(true);
});
it('keeps the age-based threshold band in both years', () => {
for (const fy of TDS_FYS) {
const rows = tdsInterestRowsForFy(fy);
expect(rows.some((r) => r.band === 'SENIOR')).toBe(true);
expect(rows.some((r) => r.band === 'GENERAL')).toBe(true);
for (const row of rows) {
expect(row.threshold).toBe(row.band === 'SENIOR' ? 100_000 : 50_000);
}
}
});
});
describe('savings interest stays out of the 194A aggregate', () => {
it('reports it separately and never inside the applicable interest', () => {
const rows = tdsInterestRowsForFy(CLOSED_FY);
const withSavings = rows.filter((r) => r.exemptInterest > 0);
expect(withSavings.length).toBeGreaterThan(0);
for (const row of withSavings) {
// The applicable figure must equal only the time-deposit interest where
// this customer is the deductee.
const expected = row.deductee.accounts
.filter((a) => isTdsApplicable(a) && tdsDeducteeOf(a) === row.deductee.clientId)
.flatMap((a) => a.credits)
.filter((c) => c.date >= '2025-04-01' && c.date <= '2026-03-31')
.reduce((s, c) => s + c.amount, 0);
expect(row.tdsApplicableInterest).toBe(expected);
}
});
it('leaves the savings totals out of the summary base', () => {
const s = tdsInterestSummary(filter());
expect(s.exemptInterest).toBeGreaterThan(0);
expect(s.applicableInterest).toBe(tdsInterestRows(filter()).reduce((acc, r) => acc + r.tdsApplicableInterest, 0));
});
});
describe('joint deposits are attributed to the first-named holder only', () => {
it('excludes second-holder interest from the aggregate and shows it apart', () => {
const rows = tdsInterestRowsForFy(CLOSED_FY);
const secondHolders = rows.filter((r) => r.jointHeldInterest > 0);
expect(secondHolders.length).toBeGreaterThan(0);
for (const row of secondHolders) {
const jointAccounts = row.deductee.accounts.filter(
(a) => isTdsApplicable(a) && tdsDeducteeOf(a) !== row.deductee.clientId
);
expect(jointAccounts.length).toBeGreaterThan(0);
// None of that interest may appear in the figure the threshold is tested
// against, or one deposit would be taxed against two people.
const jointTotal = jointAccounts
.flatMap((a) => a.credits)
.filter((c) => c.date >= '2025-04-01' && c.date <= '2026-03-31')
.reduce((s, c) => s + c.amount, 0);
expect(row.jointHeldInterest).toBe(jointTotal);
expect(row.tdsApplicableInterest).toBeLessThan(row.tdsApplicableInterest + jointTotal);
}
});
});
describe('quarter figures add up to the year', () => {
it('per row', () => {
for (const row of tdsInterestRowsForFy(CLOSED_FY)) {
const quarterInterest = row.quarters[1] + row.quarters[2] + row.quarters[3] + row.quarters[4];
expect(quarterInterest).toBe(row.tdsApplicableInterest);
expect(row.schedule.lines.reduce((s, l) => s + l.tds, 0)).toBe(row.schedule.totalTds);
}
});
it('and across the summary', () => {
const s = tdsInterestSummary(filter());
expect(s.byQuarter.reduce((acc, q) => acc + q.interest, 0)).toBe(s.applicableInterest);
expect(s.byQuarter.reduce((acc, q) => acc + q.tds, 0)).toBe(s.totalTds);
});
});
describe('filters', () => {
it("narrows to a branch without changing anyone's figures", () => {
const all = tdsInterestRows(filter());
const branch = all[0].branchCode;
const scoped = tdsInterestRows(filter({ branchCode: branch }));
expect(scoped.length).toBeGreaterThan(0);
expect(scoped.every((r) => r.branchCode === branch)).toBe(true);
// A branch view must not re-aggregate — the threshold is computed bank-wide.
const sample = scoped[0];
const sameRow = all.find((r) => r.deductee.customerId === sample.deductee.customerId);
expect(sample.tdsApplicableInterest).toBe(sameRow?.tdsApplicableInterest);
});
it('finds the customers worth chasing a declaration from', () => {
const near = tdsInterestRows(filter({ nearThresholdOnly: true }));
expect(near.length).toBeGreaterThan(0);
for (const row of near) {
expect(row.crossed).toBe(false);
expect(row.headroom).toBeGreaterThan(0);
expect(row.headroom).toBeLessThanOrEqual(NEAR_THRESHOLD_BAND);
}
});
it('narrows to customers who crossed', () => {
const crossed = tdsInterestRows(filter({ crossedOnly: true }));
expect(crossed.length).toBeGreaterThan(0);
expect(crossed.every((r) => r.crossed && r.tdsApplicableInterest > r.threshold)).toBe(true);
});
it('searches on name, CIF and PAN', () => {
const target = tdsInterestRowsForFy(CLOSED_FY).find((r) => r.panOnRecord)!;
expect(tdsInterestRows(filter({ search: target.deductee.customerId })).length).toBe(1);
expect(tdsInterestRows(filter({ search: target.deductee.pan })).length).toBeGreaterThanOrEqual(1);
});
});
describe('declarations', () => {
it('save tax and record the interest paid gross', () => {
const declared = tdsInterestRowsForFy(CLOSED_FY).filter((r) => r.hasDeclaration);
expect(declared.length).toBeGreaterThan(0);
expect(declared.some((r) => r.tdsSaved > 0)).toBe(true);
// Interest paid without deduction is a mandatory 26Q disclosure, so it must
// be carried even though no tax came off it.
expect(declared.some((r) => r.paidWithoutDeduction > 0)).toBe(true);
for (const row of declared) {
expect(row.declarationUin).toBeTruthy();
}
});
it('leaves tax deducted before a mid-year declaration in place', () => {
// Some declarations in the seed were accepted in October. Tax already taken
// in Q1/Q2 cannot be reversed, so those rows still carry a deduction.
const midYear = tdsInterestRowsForFy(CLOSED_FY).filter(
(r) => r.hasDeclaration && r.declarationAcceptedOn && r.declarationAcceptedOn.slice(5, 7) === '10'
);
expect(midYear.length).toBeGreaterThan(0);
expect(midYear.some((r) => r.schedule.totalTds > 0)).toBe(true);
});
});
describe('remittance schedule', () => {
it('deposits by the seventh of the following month, and 30 April for March', () => {
expect(depositDueDate('2025-06')).toBe('2025-07-07');
expect(depositDueDate('2025-12')).toBe('2026-01-07');
expect(depositDueDate('2026-03')).toBe('2026-04-30');
});
it('remits exactly what the engine says was deducted', () => {
const deducted = deductedByQuarter(CLOSED_FY);
const rows = tdsRemittanceRows(CLOSED_FY, TDS_AS_AT);
for (const row of rows) {
expect(row.amount).toBe(Math.round(deducted[row.quarter]));
}
});
it('charges section 201(1A) interest on a late deposit and nothing on a timely one', () => {
const rows = tdsRemittanceRows(CLOSED_FY, TDS_AS_AT);
const late = rows.filter((r) => r.late);
expect(late.length).toBeGreaterThan(0);
for (const row of late) {
expect(row.delayMonths).toBeGreaterThan(0);
expect(row.defaultInterest).toBe(Math.round(row.amount * 0.015 * row.delayMonths));
}
for (const row of rows.filter((r) => !r.late && !r.overdue)) {
expect(row.defaultInterest).toBe(0);
}
});
});
describe('the 26Q reconciliation control', () => {
it.each(TDS_FYS)('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
// Government, and the return must not be filed until it is found.
expect(s.difference).toBe(0);
expect(s.totalDeducted).toBe(s.totalDeposited + s.pendingDeposit);
});
it('splits the tax between the standard and the no-PAN rate without loss', () => {
const s = tds26qSummary(CLOSED_FY, TDS_AS_AT);
expect(s.tdsAtStandardRate + s.tdsAtHigherRate).toBe(s.totalDeducted);
expect(s.tdsAtHigherRate).toBeGreaterThan(0);
});
it('splits interest into the deducted and the paid-gross halves', () => {
const s = tds26qSummary(CLOSED_FY, TDS_AS_AT);
expect(s.interestWithDeduction + s.interestWithoutDeduction).toBe(s.totalInterestPaid);
// The gross-paid half is the Form 121 disclosure and must not be zero here,
// or the report would never exercise that line.
expect(s.interestWithoutDeduction).toBeGreaterThan(0);
});
it('agrees with the interest report on the same year', () => {
const summary = tdsInterestSummary(filter());
expect(tds26qSummary(CLOSED_FY, TDS_AS_AT).totalDeducted).toBe(summary.totalTds);
});
});
describe('the part-run current year', () => {
it('has postings only up to the business date', () => {
for (const deductee of tdsDeductees()) {
for (const account of deductee.accounts) {
for (const credit of account.credits) {
expect(credit.date <= TDS_AS_AT).toBe(true);
}
}
}
});
it('shows the closed quarter as settled and future obligations as not yet due', () => {
const rows = tdsRemittanceRows('2026-27', TDS_AS_AT);
// Q1 (Apr–Jun) has closed, so its tax was deducted and is due by 07 Jul.
expect(rows.some((r) => r.quarter === 1)).toBe(true);
// Q3 cannot appear: no interest has posted there and nothing settles there.
expect(rows.some((r) => r.quarter === 3)).toBe(false);
// Q4 *can* appear even in a part-run year — deposits on annual interest
// posting settle once, at year end — but that is a future obligation, so it
// must read as pending and must never be flagged overdue.
for (const row of rows.filter((r) => r.quarter === 4)) {
expect(row.depositedOn).toBeNull();
expect(row.overdue).toBe(false);
expect(row.defaultInterest).toBe(0);
}
});
});