Merge branch 'dev-latest' into feature/advanceLoanProduct
This commit is contained in:
@@ -13,15 +13,52 @@ import { Observable, map } from 'rxjs';
|
||||
|
||||
/** Custom Helpers */
|
||||
import { toIsoDate } from '../core/utils/api-date';
|
||||
import { TdsDeductee } from './tds-reports-data';
|
||||
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 for the TDS reports.
|
||||
* Deposit interest, declarations and challans for the TDS screens.
|
||||
*
|
||||
* The platform returns the interest it actually credited — nothing else. 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.
|
||||
* 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 {
|
||||
@@ -31,60 +68,131 @@ export class TdsReportsService {
|
||||
* @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): Observable<TdsDeductee[]> {
|
||||
const params = financialYear ? new HttpParams().set('financialYear', String(financialYear)) : undefined;
|
||||
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 section 194A computation expects.
|
||||
* 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.
|
||||
* 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: toIsoDate(row.dateOfBirth),
|
||||
dob,
|
||||
branchCode: row.branchName || '',
|
||||
// A 15G/15H on the customer's KYC record covers the year it was accepted for. The platform
|
||||
// carries the kind but not the acceptance date, so the year's start stands in — a
|
||||
// declaration is operative for the year it was given, which is what the computation needs.
|
||||
declarations: this.declarationsOf(row),
|
||||
declarations: this.declarationsOf(row.declarations || [], dob),
|
||||
accounts: (row.accounts || []).map((a: any) => ({
|
||||
id: a.id,
|
||||
accountNo: a.accountNo || '',
|
||||
depositTypeCode: a.depositTypeCode || '',
|
||||
accountStatus: a.accountStatus || undefined,
|
||||
// The deductee, not the holder: the grouping above already resolved whose PAN it is.
|
||||
clientId: a.clientId ?? row.clientId,
|
||||
credits: (a.credits || []).map((c: any) => ({
|
||||
date: toIsoDate(c.date),
|
||||
amount: Number(c.amount) || 0,
|
||||
accountNo: c.accountNo || a.accountNo || ''
|
||||
}))
|
||||
holderClientId: a.holderClientId ?? undefined,
|
||||
holderName: a.holderName ?? undefined,
|
||||
credits: (a.credits || []).map(credit(a)),
|
||||
deductions: (a.deductions || []).map(credit(a))
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
private declarationsOf(row: any): Record<string, any> {
|
||||
const kind = (row.form15Type || '').trim().toUpperCase();
|
||||
if (kind !== '15G' && kind !== '15H') {
|
||||
return {};
|
||||
}
|
||||
const out: Record<string, any> = {};
|
||||
// Applied to the years the reports can show. Without an acceptance date on the record the
|
||||
// declaration is treated as operative from the start of each year rather than mid-year.
|
||||
for (const year of [
|
||||
2025,
|
||||
2026
|
||||
]) {
|
||||
const fy = `FY ${year}-${String(year + 1).slice(2)}`;
|
||||
out[fy] = { kind, acceptedOn: `${year}-04-01`, fy };
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user