/** * 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 { let params = new HttpParams(); if (financialYear) { params = params.set('financialYear', String(financialYear)); } if (clientId) { params = params.set('clientId', String(clientId)); } return this.http .get('/tds/deposit-interest', { params }) .pipe(map((rows) => (rows || []).map((r) => this.toDeductee(r)))); } getDeclarations(clientId: number): Observable { return this.http.get('/tds/declarations', { params: new HttpParams().set('clientId', String(clientId)) }); } createDeclaration(declaration: NewTdsDeclaration): Observable { return this.http.post('/tds/declarations', declaration); } revokeDeclaration(id: number, reason: string): Observable { return this.http.post(`/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('/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 { return this.http .get('/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('/tds/challans/template'); } createChallan(challan: NewTdsChallan): Observable { return this.http .post('/tds/challans', challan) .pipe(map((rows) => (rows || []).map((r) => this.toChallan(r)))); } reverseChallan(id: number): Observable { return this.http .post(`/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 { const out: Record = {}; 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; } }