/** * 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, of } from 'rxjs'; import { catchError, map, switchMap } from 'rxjs/operators'; export interface JointHolder { clientId: number; cifNumber?: string; displayName: string; sharePercentage?: number; signingAuthority?: string; role?: string; isPrimary?: boolean; isMandatory?: boolean; } export interface AccountSignatoryInfo { modeOfOperation: string | null; primaryClientId: number | null; primaryName: string | null; jointHolders: JointHolder[]; availableBalance: number | null; onHoldAmount: number | null; } /** * Fetches account-holder/signatory data needed to verify a cheque's signature — reusing the exact APIs and * conventions already established in the cashier module's joint-holder detail view * (`transaction-entry.component.ts`), so this isn't a second, divergent implementation of the same lookup. */ @Injectable({ providedIn: 'root' }) export class SignatoryService { private http = inject(HttpClient); /** * Fetches the savings or current account (with joint holders and mode of operation embedded via * `associations=all` — the same Fineract convention the cashier module already relies on). */ fetchAccountSignatories(accountType: string, accountId: number | string): Observable { const path = accountType?.toUpperCase() === 'CURRENT' ? `/currentaccounts/${accountId}` : `/savingsaccounts/${accountId}`; return this.http.get(path, { params: new HttpParams().set('associations', 'all') }).pipe( map((acc: any) => ({ modeOfOperation: acc?.modeOfOperation ?? null, primaryClientId: acc?.clientId ?? null, primaryName: acc?.clientName ?? acc?.name ?? null, jointHolders: Array.isArray(acc?.jointHolders) ? acc.jointHolders : [], availableBalance: acc?.summary?.availableBalance ?? null, onHoldAmount: acc?.summary?.totalSavingsAmountOnHold ?? acc?.onHoldFundsDerived ?? null })) ); } /** The client's stored signature (a document tagged name === 'clientSignature'), as a blob object URL, or null if none uploaded. */ fetchClientSignature(clientId: number | string): Observable { return this.http.get(`/clients/${clientId}/documents`).pipe( switchMap((docs: any[]) => { // Multiple 'clientSignature' documents can exist for clients who re-cropped/replaced // their signature before uploads became single-slot, so pick the highest id (latest) // rather than the first unordered match. const sigDoc = (docs || []) .filter((d: any) => d.name === 'clientSignature' || d.name?.toLowerCase().includes('signature')) .sort((a: any, b: any) => b.id - a.id)[0]; if (!sigDoc) { return of(null); } return this.http.get(`/clients/${clientId}/documents/${sigDoc.id}/attachment`, { responseType: 'blob' }).pipe( map((blob: Blob) => URL.createObjectURL(blob)), catchError(() => of(null)) ); }), catchError(() => of(null)) ); } /** The client's photo (already base64/text from this API), or null if none uploaded. */ fetchClientPhoto(clientId: number | string): Observable { return this.http.get(`/clients/${clientId}/images`, { responseType: 'text' }).pipe(catchError(() => of(null))); } }