feat(ctsclearing): single-panel signature verification for inward cheques
Publish Image in Docker Hub / build (push) Canceled after 0s
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s

Adds a rich decide dialog for clerks verifying inward CTS cheques: cheque
images, all automatic validation results, and a mandatory signature-check
gate built from the account's mode of operation and joint holders (backed
by a new SignatoryService reusing the existing signature/document fetch
pattern from the cashier module).

Also adds a fullscreen compare mode: the cheque opens large with a shelf
of every mandatory signer's stored signature, which can be dragged onto
the cheque, resized, made semi-transparent, and zoomed, for a real
side-by-side signature match instead of squinting at a thumbnail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-07-22 12:25:20 +05:30
co-authored by Claude Sonnet 5
parent 6d205c0ed2
commit 0207cf85a3
8 changed files with 1170 additions and 17 deletions
@@ -0,0 +1,88 @@
/**
* 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[]) => {
const sigDoc = (docs || []).find(
(d: any) => d.name === 'clientSignature' || d.name?.toLowerCase().includes('signature')
);
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)));
}
}