feat: interest posting
This commit is contained in:
@@ -56,6 +56,25 @@ export interface TdsReportFilter {
|
||||
nearThresholdOnly: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* One of the customer's accounts as the report sees it: whether its interest is being counted
|
||||
* toward the deduction, and if not, why not. A customer can hold savings, fixed and recurring
|
||||
* accounts together, and the branch has to see which of them the deduction is actually watching.
|
||||
*/
|
||||
export interface TdsAccountLine {
|
||||
accountNo: string;
|
||||
type: 'FD' | 'RD' | 'SB' | 'OTHER';
|
||||
/** True when the account's interest counts toward this customer's threshold. */
|
||||
tracked: boolean;
|
||||
/** Plain-language reason, shown beside the account. */
|
||||
reason: string;
|
||||
/** Interest credited in the year. */
|
||||
interest: number;
|
||||
/** How many interest postings made up that figure. */
|
||||
postings: number;
|
||||
lastPostedOn: string | null;
|
||||
}
|
||||
|
||||
/** How close to the trigger counts as "worth chasing a declaration for". */
|
||||
export const NEAR_THRESHOLD_BAND = 6_000;
|
||||
|
||||
@@ -101,6 +120,10 @@ export interface TdsInterestRow {
|
||||
paidWithoutDeduction: number;
|
||||
panOnRecord: boolean;
|
||||
rateApplied: number;
|
||||
/** Every account the customer holds, tracked or not. */
|
||||
accounts: TdsAccountLine[];
|
||||
/** How many of them count toward the deduction. */
|
||||
trackedAccounts: number;
|
||||
}
|
||||
|
||||
function emptyQuarters(): Record<Quarter, number> {
|
||||
@@ -154,14 +177,35 @@ export function tdsInterestRowsForFy(fy: string): TdsInterestRow[] {
|
||||
|
||||
let exemptInterest = 0;
|
||||
let jointHeldInterest = 0;
|
||||
const accountLines: TdsAccountLine[] = [];
|
||||
for (const account of deductee.accounts) {
|
||||
const inYear = account.credits.filter((c) => fyOf(c.date) === fy).reduce((s, c) => s + c.amount, 0);
|
||||
const yearCredits = account.credits.filter((c) => fyOf(c.date) === fy);
|
||||
const inYear = yearCredits.reduce((s, c) => s + c.amount, 0);
|
||||
const type = accountTypeOf(account.depositTypeCode);
|
||||
let tracked = false;
|
||||
let reason: string;
|
||||
if (!isTdsApplicable(account)) {
|
||||
exemptInterest += inYear;
|
||||
reason = type === 'SB' ? 'Savings account — no TDS on savings interest' : 'Not a time deposit — no TDS';
|
||||
} else if (tdsDeducteeOf(account) !== deductee.clientId) {
|
||||
jointHeldInterest += inYear;
|
||||
reason = 'Joint deposit — counted under the first-named holder';
|
||||
} else {
|
||||
tracked = true;
|
||||
reason = 'Counted toward the TDS limit';
|
||||
}
|
||||
accountLines.push({
|
||||
accountNo: account.accountNo || '',
|
||||
type,
|
||||
tracked,
|
||||
reason,
|
||||
interest: inYear,
|
||||
postings: yearCredits.length,
|
||||
lastPostedOn: yearCredits.reduce<string | null>((last, c) => (!last || c.date > last ? c.date : last), null)
|
||||
});
|
||||
}
|
||||
// Tracked accounts first, so the deposits that drive the figure lead the list.
|
||||
accountLines.sort((a, b) => Number(b.tracked) - Number(a.tracked));
|
||||
|
||||
const crossedLine = schedule.lines.find((l) => l.thresholdCrossedHere);
|
||||
const headroom = Math.max(0, schedule.threshold - schedule.totalInterest);
|
||||
@@ -190,7 +234,9 @@ export function tdsInterestRowsForFy(fy: string): TdsInterestRow[] {
|
||||
tdsSaved: declaration ? Math.max(0, withoutDeclaration.totalTds - schedule.totalTds) : 0,
|
||||
paidWithoutDeduction: schedule.shieldedInterest,
|
||||
panOnRecord: isWellFormedPan(deductee.pan),
|
||||
rateApplied: schedule.rateApplied
|
||||
rateApplied: schedule.rateApplied,
|
||||
accounts: accountLines,
|
||||
trackedAccounts: accountLines.filter((a) => a.tracked).length
|
||||
});
|
||||
}
|
||||
|
||||
@@ -210,10 +256,29 @@ function overallModeFor(deductee: TdsDeductee): TdsMode {
|
||||
return applicable.some((a) => tdsModeFor(a) === 'QUARTERLY') ? 'QUARTERLY' : 'ANNUAL';
|
||||
}
|
||||
|
||||
function accountTypeOf(code: string | undefined): TdsAccountLine['type'] {
|
||||
const c = (code || '').toLowerCase();
|
||||
if (c.includes('fixeddeposit')) {
|
||||
return 'FD';
|
||||
}
|
||||
if (c.includes('recurringdeposit')) {
|
||||
return 'RD';
|
||||
}
|
||||
if (c.includes('savings')) {
|
||||
return 'SB';
|
||||
}
|
||||
return 'OTHER';
|
||||
}
|
||||
|
||||
/** Branches are compared by office name, case and spacing aside — the platform and the session both carry the name. */
|
||||
function sameBranch(a: string, b: string): boolean {
|
||||
return (a || '').trim().toLowerCase() === (b || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function tdsInterestRows(filter: TdsReportFilter): TdsInterestRow[] {
|
||||
const needle = filter.search.trim().toLowerCase();
|
||||
return tdsInterestRowsForFy(filter.fy).filter((row) => {
|
||||
if (filter.branchCode && row.branchCode !== filter.branchCode) {
|
||||
if (filter.branchCode && !sameBranch(row.branchCode, filter.branchCode)) {
|
||||
return false;
|
||||
}
|
||||
if (filter.band && row.band !== filter.band) {
|
||||
@@ -254,6 +319,8 @@ export interface TdsInterestSummary {
|
||||
tdsSaved: number;
|
||||
paidWithoutDeduction: number;
|
||||
noPan: number;
|
||||
/** Customers holding at least one account whose interest counts toward the deduction. */
|
||||
trackedCustomers: number;
|
||||
byQuarter: { quarter: Quarter; interest: number; tds: number }[];
|
||||
}
|
||||
|
||||
@@ -280,7 +347,10 @@ export function tdsInterestSummary(filter: TdsReportFilter): TdsInterestSummary
|
||||
declarations: rows.filter((r) => r.hasDeclaration).length,
|
||||
tdsSaved: rows.reduce((s, r) => s + r.tdsSaved, 0),
|
||||
paidWithoutDeduction: rows.reduce((s, r) => s + r.paidWithoutDeduction, 0),
|
||||
noPan: rows.filter((r) => !r.panOnRecord).length,
|
||||
// Only customers with a deposit the deduction watches: a savings-only customer without a PAN
|
||||
// is never deducted at the higher rate, so counting them overstated the problem.
|
||||
noPan: rows.filter((r) => !r.panOnRecord && r.trackedAccounts > 0).length,
|
||||
trackedCustomers: rows.filter((r) => r.trackedAccounts > 0).length,
|
||||
byQuarter
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user