- Fix signature-document reads picking the oldest upload instead of the latest across 7 call sites (CKYC tab, view/delete-signature dialogs, edit-wizard preview, cashier transaction entry, signatory service, record-master) by sorting on document id instead of an unsorted find(). - Add Monthly Turnover and Annual Income fields alongside the renamed Yearly Turnover field. - Add Senior Citizen (auto-detected from DOB) and Person with Disability classification, with a required disability-certificate upload gate before client creation can proceed. - Add Graduate and Diploma as distinct Education Levels, with diploma-specific degree options. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
92 lines
3.7 KiB
TypeScript
92 lines
3.7 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, 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<AccountSignatoryInfo> {
|
|
const path =
|
|
accountType?.toUpperCase() === 'CURRENT' ? `/currentaccounts/${accountId}` : `/savingsaccounts/${accountId}`;
|
|
return this.http.get<any>(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<string | null> {
|
|
return this.http.get<any[]>(`/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<string | null> {
|
|
return this.http.get(`/clients/${clientId}/images`, { responseType: 'text' }).pipe(catchError(() => of(null)));
|
|
}
|
|
}
|