/** * 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/. */ /** * Deductees and their deposit interest, for the two TDS reports. * * The bank's deduction obligation runs on time deposits, so the seed * gives each customer a mix of fixed and recurring deposits — and some savings * accounts, which are outside the section and must stay out of the aggregate. * * Everything is generated from a fixed seed so the figures do not move between * loads, and the challan schedule is deliberately imperfect: one quarter is * deposited late so the default-interest column has something to show. */ 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 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'; export interface TdsDeductee { clientId: number; customerId: string; name: string; pan: string; dob: string; branchCode: string; /** Declarations on record, keyed by financial year. */ declarations: Record; accounts: TdsAccountInput[]; } /** One month's remittance of deducted tax to the Government. */ export interface TdsChallan { fy: string; /** Quarter the deduction belongs to. */ quarter: 1 | 2 | 3 | 4; /** Month the tax was deducted in, e.g. '2026-06'. */ month: string; amount: number; /** Statutory date by which it had to be deposited. */ dueDate: string; /** When it actually reached the Government — null while still pending. */ depositedOn: string | null; challanNo: string | null; bsrCode: string | null; } /** Deterministic linear congruential generator — same figures on every load. */ function lcg(seed: number): () => number { let s = seed >>> 0; return () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 4294967296; }; } function pick(rand: () => number, list: T[]): T { return list[Math.floor(rand() * list.length)]; } const FIRST_NAMES = [ 'Ramesh', 'Sunita', 'Vitthal', 'Kamala', 'Ganpat', 'Shobha', 'Dnyaneshwar', 'Mangala', 'Bhaskar', 'Anita', 'Tukaram', 'Sarika', 'Nivrutti', 'Rekha', 'Popat', 'Chhaya', 'Ashok', 'Vaishali', 'Sitaram', 'Jayshree', 'Madhukar', 'Pushpa', 'Eknath', 'Sulochana', 'Hanumant', 'Kalpana' ]; const SURNAMES = [ 'Jadhav', 'Pawar', 'Shinde', 'Gaikwad', 'More', 'Kadam', 'Bhosale', 'Patil', 'Sawant', 'Chavan', 'Salunkhe', 'Mane', 'Deshmukh', 'Thorat', 'Nikam', 'Wagh' ]; /** * Quarter-end dates for a financial year. Interest is credited on these, which * is what makes the deduction fall due. */ function quarterEnds(fy: string): string[] { const y = Number(fy.slice(0, 4)); return [ `${y}-06-30`, `${y}-09-30`, `${y}-12-31`, `${y + 1}-03-31` ]; } /** A well-formed individual PAN — fourth character 'P'. */ function makePan(rand: () => number, surname: string): string { const letter = () => String.fromCharCode(65 + Math.floor(rand() * 26)); const digits = `${1000 + Math.floor(rand() * 9000)}`; return `${letter()}${letter()}${letter()}P${surname[0].toUpperCase()}${digits}${letter()}`; } /** * The deductees the reports run on, loaded from the platform. * * This used to be sixty-four customers generated from a fixed seed — names, PANs, deposits and all. * The computation over them is the real section 194A calculation, so the figures looked like a * filing and were not one: not a rupee of it came from the bank's own books. * * It is set once by whoever loads the screen and read by the pure computation below, which is why * this is a module-level holder rather than a service the compute would have to inject. */ let liveDeductees: TdsDeductee[] = []; export function setTdsDeductees(deductees: TdsDeductee[]): void { liveDeductees = deductees || []; // The row cache in the compute is keyed on the financial year alone, so it has to be told that // the underlying set changed or it would serve rows built from the previous load. tdsDeducteesVersion++; } /** Bumped on every load, so memoised rows can tell one data set from the next. */ export let tdsDeducteesVersion = 0; export function tdsDeductees(): TdsDeductee[] { return liveDeductees; } /** * Statutory date by which tax deducted in a given month must be deposited. * * The seventh of the following month, except for March, which runs to 30 April. */ export function depositDueDate(month: string): string { const [ y, m ] = month.split('-').map(Number); if (m === 3) { return `${y}-04-30`; } const nextY = m === 12 ? y + 1 : y; const nextM = m === 12 ? 1 : m + 1; return `${nextY}-${`${nextM}`.padStart(2, '0')}-07`; } let challanCache: Record | null = null; /** * The remittance schedule, derived from what the engine says was deducted. * * Deductions land at each quarter end, so there is one challan per quarter. The * Q2 challan of the prior year is deliberately deposited a month and a bit late * so the late-deposit interest column is exercised, and the current year's latest * quarter is left pending, which is the ordinary state of a live book. */ export function tdsChallans(deductedByQuarter: Record>): Record { if (challanCache) { return challanCache; } const out: Record = {}; for (const fy of Object.keys(deductedByQuarter)) { const y = Number(fy.slice(0, 4)); const months: Record = { 1: `${y}-06`, 2: `${y}-09`, 3: `${y}-12`, 4: `${y + 1}-03` }; const rows: TdsChallan[] = []; for (const q of [ 1, 2, 3, 4 ] as (1 | 2 | 3 | 4)[]) { const amount = Math.round(deductedByQuarter[fy]?.[q] ?? 0); if (amount <= 0) { continue; } const month = months[q]; const dueDate = depositDueDate(month); // Anything falling after the business date has not been deposited yet. const pending = dueDate > TDS_AS_AT; const late = fy === '2025-26' && q === 2; rows.push({ fy, quarter: q, month, amount, dueDate, depositedOn: pending ? null : late ? `${y}-11-14` : dueDate, challanNo: pending ? null : `${y}${q}${`${amount % 10000}`.padStart(4, '0')}`, bsrCode: pending ? null : '0510308' }); } out[fy] = rows; } challanCache = out; return out; }