Files
cbsnexora/src/app/tds-reports/tds-reports.service.ts
T

200 lines
6.9 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/.
*/
/** Angular Imports */
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, map } from 'rxjs';
/** Custom Helpers */
import { toIsoDate } from '../core/utils/api-date';
import { Declaration, formKindFor } from '../letter-mgmt/certificates/form-15gh/tds-15gh.compute';
import { TdsChallan, TdsDeductedMonth, TdsDeductee } from './tds-reports-data';
/** A declaration as recorded on the platform. */
export interface TdsDeclarationRecord {
id: number;
clientId: number;
clientName?: string;
fy: string;
formType: '15G' | '15H' | '121';
uin: string | null;
acceptedOn: string;
estimatedIncome: number | null;
revoked: boolean;
revokedOn: string | null;
revokeReason: string | null;
}
export interface NewTdsDeclaration {
clientId: number;
fy: string;
formType: string;
acceptedOn: string;
uin?: string;
estimatedIncome?: number | null;
}
export interface NewTdsChallan {
fy: string;
month: string;
amount: number;
challanNo: string;
bsrCode: string;
depositedOn: string;
paidFromGlAccountId: number;
remarks?: string;
}
/**
* Deposit interest, declarations and challans for the TDS screens.
*
* On an FD or RD the platform returns the interest that reached the customer's savings account and
* the TDS debited there for it — TDS is tracked and taken where the interest is received. A deposit
* is the customer's own money arriving and a fee is a charge, so neither is income and neither is
* sent. Working the figure out here from a balance and a rate would produce a number the passbook
* does not show.
*/
@Injectable({ providedIn: 'root' })
export class TdsReportsService {
private http = inject(HttpClient);
/**
* @param financialYear the opening calendar year — 2026 means 1 Apr 2026 to 31 Mar 2027.
* Omit to let the platform use the year the business date falls in.
*/
getDepositInterest(financialYear?: number, clientId?: number): Observable<TdsDeductee[]> {
let params = new HttpParams();
if (financialYear) {
params = params.set('financialYear', String(financialYear));
}
if (clientId) {
params = params.set('clientId', String(clientId));
}
return this.http
.get<any[]>('/tds/deposit-interest', { params })
.pipe(map((rows) => (rows || []).map((r) => this.toDeductee(r))));
}
getDeclarations(clientId: number): Observable<TdsDeclarationRecord[]> {
return this.http.get<TdsDeclarationRecord[]>('/tds/declarations', {
params: new HttpParams().set('clientId', String(clientId))
});
}
createDeclaration(declaration: NewTdsDeclaration): Observable<TdsDeclarationRecord[]> {
return this.http.post<TdsDeclarationRecord[]>('/tds/declarations', declaration);
}
revokeDeclaration(id: number, reason: string): Observable<TdsDeclarationRecord[]> {
return this.http.post<TdsDeclarationRecord[]>(`/tds/declarations/${id}/revoke`, { reason });
}
/** Tax actually withheld per month, and the business date the books are on. */
getDeducted(fy: string): Observable<{ asAt: string; months: TdsDeductedMonth[] }> {
return this.http.get<any>('/tds/deducted', { params: new HttpParams().set('fy', fy) }).pipe(
map((res) => ({
asAt: res?.asAt ?? '',
months: (res?.months || []).map((m: any) => ({
month: m.month,
amount: Number(m.amount) || 0,
postings: Number(m.postings) || 0
}))
}))
);
}
getChallans(fy: string): Observable<TdsChallan[]> {
return this.http
.get<any[]>('/tds/challans', { params: new HttpParams().set('fy', fy) })
.pipe(map((rows) => (rows || []).map((r) => this.toChallan(r))));
}
getChallanTemplate(): Observable<{ paidFromOptions: { id: number; glCode: string; name: string }[] }> {
return this.http.get<any>('/tds/challans/template');
}
createChallan(challan: NewTdsChallan): Observable<TdsChallan[]> {
return this.http
.post<any[]>('/tds/challans', challan)
.pipe(map((rows) => (rows || []).map((r) => this.toChallan(r))));
}
reverseChallan(id: number): Observable<TdsChallan[]> {
return this.http
.post<any[]>(`/tds/challans/${id}/reverse`, {})
.pipe(map((rows) => (rows || []).map((r) => this.toChallan(r))));
}
private toChallan(r: any): TdsChallan {
return {
id: r.id,
fy: r.fy,
month: r.month,
amount: Number(r.amount) || 0,
challanNo: r.challanNo,
bsrCode: r.bsrCode,
depositedOn: (r.depositedOn || '').slice(0, 10),
paidFrom: r.paidFrom || '',
remarks: r.remarks ?? null,
reversed: !!r.reversed
};
}
/**
* Shapes one platform row into what the TDS computation expects.
*
* Dates arrive as [y, M, d] arrays and are flattened to ISO here, because the computation
* compares them as strings when deciding which financial year a credit falls in. The platform
* has already grouped a minor's deposit under the guardian and a joint deposit under its primary
* holder, so every account here belongs to the deductee it is listed under.
*/
private toDeductee(row: any): TdsDeductee {
const dob = toIsoDate(row.dateOfBirth);
const credit = (a: any) => (c: any) => ({
date: toIsoDate(c.date),
amount: Number(c.amount) || 0,
accountNo: c.accountNo || a.accountNo || ''
});
return {
clientId: row.clientId,
customerId: row.customerId || '',
name: row.name || '',
pan: row.pan || '',
dob,
branchCode: row.branchName || '',
declarations: this.declarationsOf(row.declarations || [], dob),
accounts: (row.accounts || []).map((a: any) => ({
id: a.id,
accountNo: a.accountNo || '',
depositTypeCode: a.depositTypeCode || '',
// The deductee, not the holder: the grouping above already resolved whose PAN it is.
clientId: a.clientId ?? row.clientId,
holderClientId: a.holderClientId ?? undefined,
holderName: a.holderName ?? undefined,
credits: (a.credits || []).map(credit(a)),
deductions: (a.deductions || []).map(credit(a))
}))
};
}
/**
* Declarations keyed by year, the form the computation looks them up by ('2026-27'). A Form 121
* carries no G/H split, so the band follows the customer's age in that year.
*/
private declarationsOf(rows: any[], dob: string): Record<string, Declaration> {
const out: Record<string, Declaration> = {};
for (const d of rows) {
const fy = d.fy;
const kind = d.formType === '15H' ? '15H' : d.formType === '15G' ? '15G' : formKindFor(dob, fy);
out[fy] = { kind, acceptedOn: toIsoDate(d.acceptedOn), fy, uin: d.uin || undefined };
}
return out;
}
}