/** * 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 { FY_TAX_CONFIG, InterestCredit, TdsAccountInput, TdsContext, ageInFy, declarationFormFor, annualSchedule, bucketByQuarter, checkEligibility, collectTdsCredits, cumulativeLiability, formKindFor, fyOf, isTdsApplicable, isWellFormedPan, lateDeductionInterest, monthsOrPartBetween, projectQuarterlyCredits, quarterlySchedule, reconcile, tdsDeducteeOf, tdsModeFor } from './tds-15gh.compute'; import { installTdsConfigFixture } from './tds-config.fixture'; installTdsConfigFixture(); const FY = 'FY 2026-27'; /** Under 60 for FY 2026-27, valid individual PAN. */ function general(overrides: Partial = {}): TdsContext { return { fy: FY, dateOfBirth: '1985-02-14', pan: 'ABCPM1234E', resident: true, ...overrides }; } /** 60+ for FY 2026-27. */ function senior(overrides: Partial = {}): TdsContext { return { fy: FY, dateOfBirth: '1958-06-22', pan: 'ABCPM1234E', resident: true, ...overrides }; } /** Four dated credits, one per quarter. */ function credits(q1: number, q2: number, q3: number, q4: number): InterestCredit[] { return [ { date: '2026-06-30', amount: q1 }, { date: '2026-09-30', amount: q2 }, { date: '2026-12-31', amount: q3 }, { date: '2027-03-31', amount: q4 } ]; } describe('194A threshold behaviour', () => { it('deducts nothing while the aggregate stays under the trigger', () => { expect(quarterlySchedule(credits(12_000, 12_000, 12_000, 12_000), general()).totalTds).toBe(0); }); it('deducts nothing when the aggregate lands exactly on the trigger', () => { // The section bites only where the amount *exceeds* the limit. expect(quarterlySchedule(credits(12_500, 12_500, 12_500, 12_500), general()).totalTds).toBe(0); }); it('treats the threshold as a trigger, not an allowance', () => { // ₹52,000 of interest: 10% of the whole ₹52,000, not 10% of the ₹2,000 above // the ₹50,000 line. const s = annualSchedule(credits(13_000, 13_000, 13_000, 13_000), general()); expect(s.totalTds).toBe(5_200); expect(s.totalTds).not.toBe(200); }); it('applies the senior citizen trigger of ₹1,00,000', () => { const c = credits(20_000, 20_000, 20_000, 20_000); // ₹80,000 expect(quarterlySchedule(c, general()).totalTds).toBe(8_000); expect(quarterlySchedule(c, senior()).totalTds).toBe(0); }); it('falls back to the 206AA rate of 20% where PAN is not on record', () => { const c = credits(25_000, 25_000, 25_000, 25_000); // ₹1,00,000 expect(annualSchedule(c, general()).totalTds).toBe(10_000); expect(annualSchedule(c, general({ pan: '' })).totalTds).toBe(20_000); }); }); describe('the quarterly schedule catches up in the quarter the trigger is crossed', () => { it('loads the whole year-to-date liability into that quarter', () => { // ₹20,000 a quarter. The aggregate crosses ₹50,000 during Q3. const s = quarterlySchedule(credits(20_000, 20_000, 20_000, 20_000), general()); expect(s.lines.map((l) => l.tds)).toEqual([ 0, 0, 6_000, 2_000 ]); expect(s.lines[2].thresholdCrossedHere).toBe(true); // Q3 deducts ₹6,000 against ₹20,000 of that quarter's own interest. That is // correct: the earlier quarters became liable the moment the line was // crossed, and the branch collects the arrears now. expect(s.totalTds).toBe(8_000); }); it('never deducts a negative amount on a rising book', () => { const s = quarterlySchedule(credits(5_000, 60_000, 1_000, 400), general()); for (const line of s.lines) { expect(line.tds).toBeGreaterThanOrEqual(0); } }); }); describe('quarterly and annual settlement agree to the rupee', () => { const distributions: [number, number, number, number][] = [ [ 12_000, 12_000, 12_000, 12_000 ], [ 20_000, 20_000, 20_000, 20_000 ], [ 60_000, 0, 0, 0 ], [ 0, 0, 0, 75_000 ], [ 49_999, 1, 0, 0 ], [ 13_333, 13_333, 13_333, 13_334 ], [ 1, 2, 3, 99_999 ], [ 250_000, 250_000, 250_000, 250_000 ] ]; it.each(distributions)('holds for %i / %i / %i / %i', (q1, q2, q3, q4) => { const c = credits(q1, q2, q3, q4); for (const ctx of [ general(), senior(), general({ pan: '' }) ]) { const q = quarterlySchedule(c, ctx); const a = annualSchedule(c, ctx); expect(q.totalTds).toBe(a.totalTds); expect(q.lines.reduce((s, l) => s + l.tds, 0)).toBe(a.totalTds); } }); it('holds under section 288B rounding to the nearest ₹10', () => { const ctx = general({ config: { roundTo: 10 } }); const c = credits(13_333, 13_333, 13_333, 13_334); expect(quarterlySchedule(c, ctx).totalTds).toBe(annualSchedule(c, ctx).totalTds); expect(annualSchedule(c, ctx).totalTds % 10).toBe(0); }); /** * Property test. Rounding drift only shows up on awkward numbers, so sweep a * deterministic spread of them rather than trusting hand-picked cases. */ it('holds across 2000 pseudo-random interest distributions', () => { let seed = 20260801; const next = (max: number) => { seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; return Math.floor((seed / 4294967296) * max); }; for (let i = 0; i < 2000; i++) { const c = credits(next(90_000), next(90_000), next(90_000), next(90_000)); const ctx = general({ config: { roundTo: i % 2 === 0 ? 1 : 10 } }); const q = quarterlySchedule(c, ctx); const a = annualSchedule(c, ctx); if (q.totalTds !== a.totalTds) { throw new Error( `Mismatch on ${c.map((x) => x.amount).join('/')}: quarterly ${q.totalTds} vs annual ${a.totalTds}` ); } expect(q.lines.reduce((s, l) => s + l.tds, 0)).toBe(q.totalTds); } }); it('holds across 2000 distributions with a declaration at every acceptance point', () => { let seed = 19980731; const next = (max: number) => { seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; return Math.floor((seed / 4294967296) * max); }; const dates = [ '2026-04-01', '2026-05-15', '2026-08-01', '2026-10-01', '2027-01-10', '2027-03-31' ]; for (let i = 0; i < 2000; i++) { const c = credits(next(90_000), next(90_000), next(90_000), next(90_000)); const ctx = general({ declaration: { kind: '15G', acceptedOn: dates[i % dates.length], fy: '2026-27' }, config: { roundTo: i % 2 === 0 ? 1 : 10 } }); const q = quarterlySchedule(c, ctx); const a = annualSchedule(c, ctx); if (q.totalTds !== a.totalTds) { throw new Error( `Mismatch on ${c.map((x) => x.amount).join('/')} declared ${dates[i % dates.length]}: ` + `quarterly ${q.totalTds} vs annual ${a.totalTds}` ); } } }); it('adjusts within the year when interest is reversed', () => { // Q2 took the aggregate to ₹80,000 and ₹8,000 was deducted. The reversal // brings the year back to ₹50,000, which does not exceed the trigger at all, // so the whole ₹8,000 is given back in Q3. const c = credits(40_000, 40_000, -30_000, 0); const q = quarterlySchedule(c, general()); expect(q.lines.map((l) => l.tds)).toEqual([ 0, 8_000, -8_000, 0 ]); expect(q.totalTds).toBe(0); expect(annualSchedule(c, general()).totalTds).toBe(0); }); }); describe('a Form 121 declaration', () => { const fromApril = general({ declaration: { kind: '15G', acceptedOn: '2026-04-01', fy: '2026-27', uin: 'U0001' } }); const fromOctober = general({ declaration: { kind: '15G', acceptedOn: '2026-10-01', fy: '2026-27' } }); it('stops deduction for the whole year when on record from April', () => { const c = credits(30_000, 30_000, 30_000, 30_000); expect(quarterlySchedule(c, fromApril).totalTds).toBe(0); expect(annualSchedule(c, fromApril).totalTds).toBe(0); expect(reconcile(c, fromApril).matched).toBe(true); }); it('shields only the interest credited on or after it was accepted', () => { const s = quarterlySchedule(credits(30_000, 30_000, 30_000, 30_000), fromOctober); expect(s.liableInterest).toBe(60_000); expect(s.shieldedInterest).toBe(60_000); }); it('does not claw back tax already remitted before it arrived', () => { const q = quarterlySchedule(credits(30_000, 30_000, 30_000, 30_000), fromOctober); expect(q.lines.map((l) => l.tds)).toEqual([ 0, 6_000, 0, 0 ]); expect(q.totalTds).toBe(6_000); expect(q.notes[0]).toContain('cannot reverse it'); }); it('leaves the two modes in agreement even when taken mid-year', () => { // A declaration is prospective, so the year-end figure still sees the // pre-declaration interest as liable. const r = reconcile(credits(30_000, 30_000, 30_000, 30_000), fromOctober); expect(r.quarterlyTotal).toBe(6_000); expect(r.annualTotal).toBe(6_000); expect(r.matched).toBe(true); }); it('reports the divergence when liability falls after deductions were frozen', () => { // Q1 crossed the trigger and ₹6,000 went to the Government. A reversal in // Q3, after the declaration was on record, drags the year back under // ₹50,000 — the liability is now nil but the branch cannot refund. const c: InterestCredit[] = [ { date: '2026-06-30', amount: 60_000 }, { date: '2026-12-31', amount: -15_000 } ]; const r = reconcile(c, fromOctober); expect(r.quarterlyTotal).toBe(6_000); expect(r.annualTotal).toBe(0); expect(r.matched).toBe(false); expect(quarterlySchedule(c, fromOctober).lines[2].tds).toBe(0); }); }); describe('age is measured against the financial year, not today', () => { it('gives the senior threshold to a customer who turns 60 in March', () => { expect(ageInFy('1967-03-20', FY)).toBe(60); expect(formKindFor('1967-03-20', FY)).toBe('15H'); const ctx = general({ dateOfBirth: '1967-03-20' }); expect(quarterlySchedule(credits(20_000, 20_000, 20_000, 20_000), ctx).threshold).toBe(100_000); }); it('keeps the general threshold for a customer who turns 60 the next April', () => { expect(ageInFy('1967-04-20', FY)).toBe(59); expect(formKindFor('1967-04-20', FY)).toBe('15G'); }); }); describe('savings interest is outside section 194A', () => { it.each([ [ 'depositAccountType.fixedDeposit', true ], [ 'depositAccountType.recurringDeposit', true ], [ 'depositAccountType.savingsDeposit', false ], [ 'depositAccountType.currentDeposit', false ], [ '', false ] ])('%s -> %s', (code, expected) => { expect(isTdsApplicable({ depositTypeCode: code })).toBe(expected); }); it('keeps savings interest out of the aggregate entirely', () => { const accounts: TdsAccountInput[] = [ { accountNo: 'FD001', depositTypeCode: 'depositAccountType.fixedDeposit', accountStatus: 'SINGLE', clientId: 1, credits: credits(10_000, 10_000, 10_000, 10_000) }, { accountNo: 'SB001', depositTypeCode: 'depositAccountType.savingsDeposit', accountStatus: 'SINGLE', clientId: 1, credits: credits(5_000, 5_000, 5_000, 5_000) } ]; const collected = collectTdsCredits(1, accounts, FY); // ₹40,000 of FD interest, not ₹60,000 — so the trigger is never crossed and // nothing is deducted. Folding the savings interest in would have crossed it. expect(collected.reduce((s, c) => s + c.amount, 0)).toBe(40_000); expect(quarterlySchedule(collected, general()).totalTds).toBe(0); }); }); describe('whose PAN the tax is deducted against', () => { it('uses the account holder for a single-name account', () => { expect(tdsDeducteeOf({ accountStatus: 'SINGLE', clientId: 7 })).toBe(7); expect(tdsDeducteeOf({ accountStatus: 'INDIVIDUAL_MALE', clientId: 7 })).toBe(7); }); it('uses the first-named holder on a joint account', () => { expect( tdsDeducteeOf({ accountStatus: 'JOINT', primaryClientId: 7, jointHolders: [{ clientId: 9 }] }) ).toBe(7); }); it('uses the guardian on a minor account', () => { expect(tdsDeducteeOf({ accountStatus: 'MINOR', clientId: 11, guardianClientId: 4 })).toBe(4); }); it('does not count a joint deposit twice across the two holders', () => { const jointFd: TdsAccountInput = { accountNo: 'FD-JOINT', depositTypeCode: 'depositAccountType.fixedDeposit', accountStatus: 'JOINT', primaryClientId: 7, jointHolders: [{ clientId: 9 }], credits: credits(15_000, 15_000, 15_000, 15_000) // ₹60,000 }; // Primary holder carries the whole ₹60,000 and the whole ₹6,000 of tax. const primary = collectTdsCredits(7, [jointFd], FY); expect(primary.reduce((s, c) => s + c.amount, 0)).toBe(60_000); expect(quarterlySchedule(primary, general()).totalTds).toBe(6_000); // The second holder carries none of it — no double deduction on one deposit. const secondHolder = collectTdsCredits(9, [jointFd], FY); expect(secondHolder).toEqual([]); expect(quarterlySchedule(secondHolder, general()).totalTds).toBe(0); }); it('ignores any share percentage when attributing interest', () => { // A 50/50 joint account still reports the full amount against the primary // holder; splitting would produce two deductees for one deposit. const jointFd: TdsAccountInput = { accountNo: 'FD-JOINT', depositTypeCode: 'depositAccountType.fixedDeposit', accountStatus: 'JOINT', primaryClientId: 7, jointHolders: [{ clientId: 9 }], credits: credits(15_000, 15_000, 15_000, 15_000) }; expect(collectTdsCredits(7, [jointFd], FY).reduce((s, c) => s + c.amount, 0)).toBe(60_000); }); it('aggregates a customer across every branch', () => { // Three deposits in three branches, each under the trigger on its own. const accounts: TdsAccountInput[] = [ 'FD-MUM', 'FD-PUN', 'FD-NAS' ].map((accountNo, i) => ({ accountNo, depositTypeCode: 'depositAccountType.fixedDeposit', accountStatus: 'SINGLE', clientId: 1, credits: [{ date: `2026-0${6 + i}-30`, amount: [ 20_000, 18_000, 15_000 ][i] }] })); const collected = collectTdsCredits(1, accounts, FY); expect(collected.reduce((s, c) => s + c.amount, 0)).toBe(53_000); // Branch by branch none would have crossed ₹50,000. Bank-wide, it does. expect(quarterlySchedule(collected, general()).totalTds).toBe(5_300); }); it('keeps other financial years out of the selected year', () => { const account: TdsAccountInput = { accountNo: 'FD001', depositTypeCode: 'depositAccountType.fixedDeposit', accountStatus: 'SINGLE', clientId: 1, credits: [ { date: '2026-03-31', amount: 99_000 }, // FY 2025-26 { date: '2026-06-30', amount: 10_000 }, // FY 2026-27 { date: '2027-04-01', amount: 99_000 } // FY 2027-28 ] }; expect(collectTdsCredits(1, [account], FY).map((c) => c.amount)).toEqual([10_000]); }); }); describe('settlement mode follows the product, not the customer', () => { it.each([ [ 'savingsPostingInterestPeriodType.monthly', 'QUARTERLY' ], [ 'savingsPostingInterestPeriodType.quarterly', 'QUARTERLY' ], [ 'savingsPostingInterestPeriodType.annual', 'ANNUAL' ], [ 'savingsPostingInterestPeriodType.annually', 'ANNUAL' ] ])('%s -> %s', (code, expected) => { expect(tdsModeFor({ interestPostingPeriodType: { code } })).toBe(expected); }); it('does not mistake a half-yearly product for an annual one', () => { // 'biannual' contains 'annual' — a naive substring test settles half-yearly // products once a year, which is a deduction default. expect(tdsModeFor({ interestPostingPeriodType: { code: 'savingsPostingInterestPeriodType.biannual' } })).toBe( 'QUARTERLY' ); expect(tdsModeFor({ interestPostingPeriodType: { value: 'Bi-Annual' } })).toBe('QUARTERLY'); }); it('reads the value when no code is present, and defaults safely', () => { expect(tdsModeFor({ interestPostingPeriodType: { value: 'Annually' } })).toBe('ANNUAL'); // Unknown or missing configuration settles on the quarterly cycle, which is // the conservative choice — deducting sooner is never a default. expect(tdsModeFor({})).toBe('QUARTERLY'); expect(tdsModeFor({ interestPostingPeriodType: null })).toBe('QUARTERLY'); }); }); describe('PAN validation', () => { it.each([ [ 'ABCPM1234E', true ], [ 'EFGHV5678A', true ], [ 'PQRSR2233Z', false ], [ 'TUVWS3344K', false ], [ 'ABCP1234E', false ], [ '', false ] ])('%s -> %s', (pan, expected) => { expect(isWellFormedPan(pan)).toBe(expected); }); }); const income = (estimatedInterest: number, estimatedIncome: number) => ({ estimatedInterest, estimatedIncome, incomeOnRecord: true }); /** FY 2025-26 — the last year the 1961 Act and the 15G/15H split applied. */ const OLD_FY = 'FY 2025-26'; describe('which declaration form is in force', () => { it('splits by age up to FY 2025-26', () => { expect(declarationFormFor(OLD_FY, '1985-02-14')).toBe('15G'); expect(declarationFormFor(OLD_FY, '1958-06-22')).toBe('15H'); }); it('is a single Form 121 from FY 2026-27, whatever the age', () => { expect(declarationFormFor(FY, '1985-02-14')).toBe('121'); expect(declarationFormFor(FY, '1958-06-22')).toBe('121'); }); it('keeps the senior threshold even though the forms merged', () => { // The two forms became one, but the deduction trigger is still age-based. expect(quarterlySchedule(credits(20_000, 20_000, 20_000, 20_000), general()).threshold).toBe(50_000); expect(quarterlySchedule(credits(20_000, 20_000, 20_000, 20_000), senior()).threshold).toBe(100_000); }); }); describe('eligibility up to FY 2025-26 — the 15G / 15H tests', () => { const old = (o: Partial = {}) => general({ fy: OLD_FY, ...o }); const oldSenior = (o: Partial = {}) => senior({ fy: OLD_FY, ...o }); it('accepts a 15G within both the income and the interest ceiling', () => { const r = checkEligibility(old(), income(90_000, 320_000)); expect(r.form).toBe('15G'); expect(r.eligible).toBe(true); }); it('rejects a 15G whose estimated income exceeds the basic exemption', () => { const r = checkEligibility(old(), income(38_500, 850_000)); expect(r.eligible).toBe(false); expect(r.reasons.join(' ')).toContain('exceeds the basic exemption'); }); it('rejects a 15G whose interest alone exceeds the basic exemption', () => { // The second limb: interest had to clear the limit as well as income. expect(checkEligibility(old(), income(450_000, 380_000)).eligible).toBe(false); }); it('places no interest ceiling on a 15H', () => { // A senior citizen with ₹8,00,000 of interest could still declare, provided // deductions and rebate left the liability at nil. const r = checkEligibility(oldSenior(), income(800_000, 900_000)); expect(r.form).toBe('15H'); expect(r.eligible).toBe(true); }); it('rejects a 15H that leaves a liability', () => { const r = checkEligibility(oldSenior(), income(800_000, 1_500_000)); expect(r.eligible).toBe(false); expect(r.reasons.join(' ')).toContain('nil'); }); it('cites the rule that applied to that year', () => { const r = checkEligibility(old({ pan: '' }), income(10_000, 100_000)); expect(r.eligible).toBe(false); expect(r.reasons.join(' ')).toContain('Rule 29C'); }); it('still checks the interest limb when income is not on record', () => { // Income has to be declared by the payee, but the bank knows the interest // figure, so that limb remains assessable. expect( checkEligibility(old(), { estimatedInterest: 450_000, estimatedIncome: 0, incomeOnRecord: false }).eligible ).toBe(false); }); }); describe('eligibility from FY 2026-27 — the single Form 121 test', () => { it('drops the interest limb entirely', () => { // Under a 15G this failed: interest of ₹4,50,000 was over the basic // exemption limit. Form 121 asks only whether the tax liability is nil, so // the same payee is now eligible. expect(checkEligibility(general({ fy: OLD_FY }), income(450_000, 380_000)).eligible).toBe(false); const r = checkEligibility(general(), income(450_000, 380_000)); expect(r.form).toBe('121'); expect(r.eligible).toBe(true); }); it('applies one nil-liability ceiling to every age', () => { for (const ctx of [ general(), senior() ]) { expect(checkEligibility(ctx, income(800_000, 900_000)).eligible).toBe(true); const over = checkEligibility(ctx, income(800_000, 1_500_000)); expect(over.eligible).toBe(false); expect(over.reasons.join(' ')).toContain('nil'); } }); it('takes PAN only — Aadhaar is not a substitute', () => { const r = checkEligibility(general({ pan: '' }), income(10_000, 100_000)); expect(r.eligible).toBe(false); expect(r.reasons.join(' ')).toContain('Rule 211'); expect(r.reasons.join(' ')).toContain('Aadhaar is not a substitute'); }); it('refuses an entity PAN', () => { const r = checkEligibility(general({ pan: 'ABCCM1234E' }), income(10_000, 100_000)); expect(r.eligible).toBe(false); expect(r.reasons.join(' ')).toContain('entity'); }); it('refuses a non-resident', () => { expect(checkEligibility(general({ resident: false }), income(10_000, 100_000)).eligible).toBe(false); }); it('reports the nil-liability test as unassessed rather than passing it silently', () => { // The branch cannot compute income earned outside the bank, so a missing // figure must not read as a pass. const r = checkEligibility(general(), { estimatedInterest: 90_000, estimatedIncome: 0, incomeOnRecord: false }); expect(r.unassessed.length).toBe(1); expect(r.unassessed[0]).toContain('not on record'); // With no interest limb left, nothing else can fail it — so the branch must // read the unassessed note rather than treat the row as cleared. expect(r.eligible).toBe(true); }); }); describe('statutory references travel with the year', () => { it('quotes the 1961 Act for FY 2025-26 and the 2025 Act for FY 2026-27', () => { expect(FY_TAX_CONFIG['2025-26'].regime).toBe('ITA_1961'); expect(FY_TAX_CONFIG['2025-26'].interestTdsSection).toContain('194A'); expect(FY_TAX_CONFIG['2025-26'].tdsStatementForm).toBe('Form 26Q'); expect(FY_TAX_CONFIG['2025-26'].declarationStatementFrequency).toBe('QUARTERLY'); expect(FY_TAX_CONFIG['2026-27'].regime).toBe('ITA_2025'); expect(FY_TAX_CONFIG['2026-27'].interestTdsSection).toContain('393'); expect(FY_TAX_CONFIG['2026-27'].tdsStatementForm).toBe('Form 140'); expect(FY_TAX_CONFIG['2026-27'].declarationStatementFrequency).toBe('MONTHLY'); }); it('carries the same thresholds and rates across the changeover', () => { const a = FY_TAX_CONFIG['2025-26']; const b = FY_TAX_CONFIG['2026-27']; expect(b.thresholdGeneral).toBe(a.thresholdGeneral); expect(b.thresholdSenior).toBe(a.thresholdSenior); expect(b.rate).toBe(a.rate); expect(b.rateNoPan).toBe(a.rateNoPan); }); }); describe('default interest under section 201(1A)', () => { it('counts any part of a month as a full month', () => { expect(monthsOrPartBetween('2026-07-07', '2026-07-07')).toBe(0); expect(monthsOrPartBetween('2026-07-07', '2026-07-08')).toBe(1); expect(monthsOrPartBetween('2026-07-07', '2026-08-07')).toBe(1); expect(monthsOrPartBetween('2026-07-07', '2026-08-08')).toBe(2); expect(monthsOrPartBetween('2026-07-07', '2027-01-07')).toBe(6); }); it('charges 1% a month for deducting late and 1.5% for depositing late', () => { expect(lateDeductionInterest(6_000, '2026-07-07', '2027-01-07', 'DEDUCTION', FY)).toEqual({ months: 6, rate: 0.01, interest: 360 }); expect(lateDeductionInterest(6_000, '2026-07-07', '2027-01-07', 'DEPOSIT', FY).interest).toBe(540); }); it('charges nothing when settled on time', () => { expect(lateDeductionInterest(6_000, '2026-07-07', '2026-07-07', 'DEPOSIT', FY).interest).toBe(0); }); }); describe('helpers', () => { it('resolves the financial year of a date', () => { expect(fyOf('2026-04-01')).toBe('2026-27'); expect(fyOf('2027-03-31')).toBe('2026-27'); expect(fyOf('2026-03-31')).toBe('2025-26'); expect(fyOf('2027-04-01')).toBe('2027-28'); }); it('buckets dated credits into the right quarters and drops out-of-year dates', () => { const b = bucketByQuarter( [ { date: '2026-04-05', amount: 100 }, { date: '2026-08-01', amount: 200 }, { date: '2026-11-30', amount: 300 }, { date: '2027-01-15', amount: 400 }, { date: '2026-03-31', amount: 999 }, { date: '2027-04-01', amount: 999 } ], FY ); expect(b).toEqual({ 1: 100, 2: 200, 3: 300, 4: 400 }); }); it('spreads an annual estimate without losing rupees to division', () => { for (const annual of [ 38_500, 52_801, 1, 999_999 ]) { expect(projectQuarterlyCredits(annual, FY).reduce((s, x) => s + x.amount, 0)).toBe(annual); } }); it('exposes the cumulative liability directly for ad-hoc checks', () => { expect(cumulativeLiability(50_000, 50_000, general())).toBe(0); expect(cumulativeLiability(50_001, 50_001, general())).toBe(5_000); }); });