200 lines
6.9 KiB
TypeScript
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;
|
|
}
|
|
}
|