251 lines
6.8 KiB
TypeScript
251 lines
6.8 KiB
TypeScript
/**
|
||
* 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<string, Declaration>;
|
||
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<T>(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<string, TdsChallan[]> | 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<string, Record<number, number>>): Record<string, TdsChallan[]> {
|
||
if (challanCache) {
|
||
return challanCache;
|
||
}
|
||
const out: Record<string, TdsChallan[]> = {};
|
||
|
||
for (const fy of Object.keys(deductedByQuarter)) {
|
||
const y = Number(fy.slice(0, 4));
|
||
const months: Record<number, string> = {
|
||
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;
|
||
}
|