/** * 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/. */ import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; export type InterestPostingReportStatus = 'GENERATED' | 'APPROVED' | 'POSTED' | 'SUPERSEDED'; /** Which branches a list covers: the user's own, or (head office only) every other branch. */ export type ReportScope = 'own' | 'others'; export interface InterestPostingReport { id: number; productId: number; productName: string; officeId: number; officeName: string; postingDate: string; status: InterestPostingReportStatus; totalInterest: number; accountCount: number; generationMode: 'AUTO' | 'MANUAL'; generatedAt: string; generatedBy: string | null; approvedAt: string | null; approvedBy: string | null; approvedOnBehalf: boolean; postedAt: string | null; failureReason: string | null; /** Whether the signed-in user may regenerate or approve it: their own branch, or the head office for a branch that did not open. */ canAct: boolean; /** Why not, where canAct is false. */ blockedReason: string | null; } export interface InterestPostingReportLine { savingsAccountId: number; accountNo: string; clientName: string; interestAmount: number; balanceBefore: number; balanceAfter: number; savingsTransactionId: number | null; } export interface InterestPostingReportDetail extends InterestPostingReport { payableGlCode: string | null; payableGlName: string | null; paidGlCode: string | null; paidGlName: string | null; expenseGlCode: string | null; expenseGlName: string | null; lines: InterestPostingReportLine[]; } export interface InterestPostingDue { productId: number; productName: string; officeId: number; officeName: string; postingDate: string; canAct: boolean; blockedReason: string | null; } export interface InterestPostingSummaryRow { id: number; postingDate: string; officeId: number; officeName: string; productId: number; productName: string; accountCount: number; totalInterest: number; status: InterestPostingReportStatus; approvedBy: string | null; approvedOnBehalf: boolean; approvedAt: string | null; postedAt: string | null; } export interface InterestPreviewRow { officeId: number; officeName: string; productId: number; productName: string; savingsAccountId: number; accountNo: string; clientName: string; balance: number; rate: number; cycleStart: string; postingDate: string; asOf: string; interest: number; } /** * The savings interest posting reports, one per branch and product: generated at noon on a posting * date, approved by the branch, posted when the branch closes the day. */ @Injectable({ providedIn: 'root' }) export class InterestPostingReportService { private http = inject(HttpClient); private readonly base = '/savingsinterestpostingreports'; /** scope 'own' is the user's own branch; 'others' (head office only) every other branch. */ list( postingDate?: string, status?: string, officeId?: number, scope?: ReportScope ): Observable { let params = new HttpParams(); if (scope) params = params.set('scope', scope); if (postingDate) params = params.set('postingDate', postingDate); if (status) params = params.set('status', status); if (officeId) params = params.set('officeId', String(officeId)); return this.http.get(this.base, { params }); } /** Branch and product pairs with interest due by the date and no report yet, that this user may generate. */ dueWithoutReport(postingDate?: string, scope?: ReportScope): Observable { let params = new HttpParams(); if (scope) params = params.set('scope', scope); if (postingDate) params = params.set('postingDate', postingDate); return this.http.get(`${this.base}/due-without-report`, { params }); } /** The signed-in user's branch, and whether it is the head office. */ context(): Observable<{ officeId: number; officeName: string; headOffice: boolean }> { return this.http.get<{ officeId: number; officeName: string; headOffice: boolean }>(`${this.base}/context`); } detail(reportId: number): Observable { return this.http.get(`${this.base}/${reportId}`); } regenerate( productId: number, officeId: number, postingDate: string ): Observable<{ reportId: number; status: string }> { return this.http.post<{ reportId: number; status: string }>( this.base, { productId, officeId, postingDate }, { params: new HttpParams().set('command', 'regenerate') } ); } approve(reportId: number): Observable<{ reportId: number; status: string }> { return this.http.post<{ reportId: number; status: string }>( `${this.base}/${reportId}`, {}, { params: new HttpParams().set('command', 'approve') } ); } /** Interest each account has earned so far this cycle, as if posted today. View only. */ preview(officeId?: number): Observable { let params = new HttpParams(); if (officeId) params = params.set('officeId', String(officeId)); return this.http.get(`${this.base}/preview`, { params }); } /** The head office's branch-wise view over a date range. */ summary(fromDate?: string, toDate?: string, productId?: number): Observable { let params = new HttpParams(); if (fromDate) params = params.set('fromDate', fromDate); if (toDate) params = params.set('toDate', toDate); if (productId) params = params.set('productId', String(productId)); return this.http.get(`${this.base}/summary`, { params }); } }