Reverts all commits pushed to demo today (a5124ca3,858c277a,a1a50ab8,f72abb09,fcf66657) back tof8c532f7as a single forward commit, without rewriting history. All reverted content (Share Withdrawal/Closure feature, TDS reports, KYC exception tracking, client-edit-email-dialog, form121 tab, search-select) remains intact on dev and was verified identical before this revert.
962 lines
33 KiB
TypeScript
962 lines
33 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, HttpBackend, HttpHeaders, HttpContext } from '@angular/common/http';
|
||
import { SILENT_HTTP_ERROR } from 'app/core/http/silent-error.token';
|
||
|
||
/** rxjs Imports */
|
||
import { Observable, of, throwError } from 'rxjs';
|
||
import { switchMap } from 'rxjs/operators';
|
||
import { map, catchError } from 'rxjs/operators';
|
||
|
||
import { environment } from 'environments/environment';
|
||
import { dataUrlToFile } from 'app/shared/utils/data-url.util';
|
||
|
||
/**
|
||
* Clients service.
|
||
*/
|
||
@Injectable({
|
||
providedIn: 'root'
|
||
})
|
||
export class ClientsService {
|
||
private http = inject(HttpClient);
|
||
private httpBackend = inject(HttpBackend);
|
||
|
||
/** Separate HttpClient that bypasses interceptors (for external API calls) */
|
||
private externalHttp = new HttpClient(this.httpBackend);
|
||
|
||
getFilteredClients(
|
||
orderBy: string,
|
||
sortOrder: string,
|
||
orphansOnly: boolean,
|
||
displayName: string,
|
||
officeId?: any
|
||
): Observable<any> {
|
||
let httpParams = new HttpParams()
|
||
.set('displayName', displayName)
|
||
.set('orphansOnly', orphansOnly.toString())
|
||
.set('sortOrder', sortOrder)
|
||
.set('orderBy', orderBy);
|
||
if (officeId) {
|
||
httpParams = httpParams.set('officeId', officeId);
|
||
}
|
||
return this.http.get('/clients', { params: httpParams });
|
||
}
|
||
|
||
getClients(orderBy: string, sortOrder: string, offset: number, limit: number): Observable<any> {
|
||
const httpParams = new HttpParams()
|
||
.set('offset', offset.toString())
|
||
.set('limit', limit.toString())
|
||
.set('sortOrder', sortOrder)
|
||
.set('orderBy', orderBy);
|
||
return this.http.get('/clients', { params: httpParams });
|
||
}
|
||
|
||
getClientTemplate(): Observable<any> {
|
||
return this.http.get('/clients/template');
|
||
}
|
||
|
||
getClientWithOfficeTemplate(officeId: number): Observable<any> {
|
||
return this.http.get(`/clients/template?officeId=${officeId}&staffInSelectedOfficeOnly=true`);
|
||
}
|
||
|
||
getClientData(clientId: string, silent = false) {
|
||
return this.http.get(
|
||
`/clients/${clientId}`,
|
||
silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {}
|
||
);
|
||
}
|
||
|
||
createClient(client: any) {
|
||
return this.http.post(`/clients`, client);
|
||
}
|
||
|
||
updateClient(clientId: string, client: any) {
|
||
return this.http.put(`/clients/${clientId}`, client);
|
||
}
|
||
|
||
deleteClient(clientId: string) {
|
||
return this.http.delete(`/clients/${clientId}`);
|
||
}
|
||
|
||
/**
|
||
* Maker-checker for Client edits — stages the change in m_client_pending_edit instead of
|
||
* applying it immediately; a different user must call approveClientEdit() before it takes
|
||
* effect. See ClientEditApiResource on the backend.
|
||
*/
|
||
submitClientEdit(clientId: string, client: any) {
|
||
return this.http.post(`/clients/${clientId}/edit`, client);
|
||
}
|
||
|
||
getPendingClientEdit(clientId: string) {
|
||
return this.http.get(`/clients/${clientId}/edit/pending`);
|
||
}
|
||
|
||
getAllPendingClientEdits(
|
||
status?: string,
|
||
clientName?: string,
|
||
fromDate?: string,
|
||
toDate?: string,
|
||
offset = 0,
|
||
limit = 20
|
||
): Observable<any> {
|
||
let params = new HttpParams().set('offset', offset).set('limit', limit);
|
||
if (status) params = params.set('status', status);
|
||
if (clientName) params = params.set('clientName', clientName);
|
||
if (fromDate) params = params.set('fromDate', fromDate);
|
||
if (toDate) params = params.set('toDate', toDate);
|
||
return this.http.get(`/clients/edit/pending`, { params });
|
||
}
|
||
|
||
approveClientEdit(clientId: string) {
|
||
return this.http.post(`/clients/${clientId}/edit/approve`, {});
|
||
}
|
||
|
||
rejectClientEdit(clientId: string, rejectReason?: string) {
|
||
return this.http.post(`/clients/${clientId}/edit/reject`, { rejectReason: rejectReason || '' });
|
||
}
|
||
|
||
getClientDataAndTemplate(clientId: string) {
|
||
const httpParams = new HttpParams().set('template', 'true').set('staffInSelectedOfficeOnly', 'true');
|
||
return this.http.get(`/clients/${clientId}`, { params: httpParams });
|
||
}
|
||
|
||
getClientDatatables() {
|
||
const httpParams = new HttpParams().set('apptable', 'm_client');
|
||
return this.http.get(`/datatables`, { params: httpParams });
|
||
}
|
||
|
||
getClientDatatable(clientId: string, datatableName: string) {
|
||
const httpParams = new HttpParams().set('genericResultSet', 'true');
|
||
return this.http.get(`/datatables/${datatableName}/${clientId}`, { params: httpParams });
|
||
}
|
||
|
||
addClientDatatableEntry(clientId: string, datatableName: string, data: any) {
|
||
const httpParams = new HttpParams().set('genericResultSet', 'true');
|
||
return this.http.post(`/datatables/${datatableName}/${clientId}`, data, { params: httpParams });
|
||
}
|
||
|
||
editClientDatatableEntry(clientId: string, datatableName: string, data: any) {
|
||
const httpParams = new HttpParams().set('genericResultSet', 'true');
|
||
return this.http.put(`/datatables/${datatableName}/${clientId}`, data, { params: httpParams });
|
||
}
|
||
|
||
deleteDatatableContent(clientId: string, datatableName: string) {
|
||
const httpParams = new HttpParams().set('genericResultSet', 'true');
|
||
return this.http.delete(`/datatables/${datatableName}/${clientId}`, { params: httpParams });
|
||
}
|
||
|
||
getClientAccountData(clientId: string) {
|
||
return this.http.get(`/clients/${clientId}/accounts`);
|
||
}
|
||
|
||
getClientAddressData(clientId: string, silent = false) {
|
||
return this.http.get(
|
||
`/client/${clientId}/addresses`,
|
||
silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {}
|
||
);
|
||
}
|
||
|
||
getClientChargesData(clientId: string) {
|
||
const httpParams = new HttpParams().set('pendingPayment', 'true');
|
||
return this.http.get(`/clients/${clientId}/charges`, { params: httpParams });
|
||
}
|
||
|
||
getSelectedChargeData(clientId: string, chargeId: string) {
|
||
const httpParams = new HttpParams().set('associations', 'all');
|
||
return this.http.get(`/clients/${clientId}/charges/${chargeId}`, { params: httpParams });
|
||
}
|
||
|
||
/**
|
||
* @param chargeData Charge Data to be waived.
|
||
*/
|
||
waiveClientCharge(chargeData: any) {
|
||
const httpParams = new HttpParams().set('command', 'waive');
|
||
return this.http.post(`/clients/${chargeData.clientId}/charges/${chargeData.resourceType}`, chargeData, {
|
||
params: httpParams
|
||
});
|
||
}
|
||
|
||
getAllClientCharges(clientId: string) {
|
||
return this.http.get(`/clients/${clientId}/charges`);
|
||
}
|
||
|
||
/**
|
||
* @param transactionData Transaction Data to be undone.
|
||
*/
|
||
undoTransaction(transactionData: any) {
|
||
return this.http.post(
|
||
`/clients/${transactionData.clientId}/transactions/${transactionData.transactionId}?command=undo`,
|
||
transactionData
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @param clientId Client Id of the relevant charge.
|
||
* @param chargeId Charge Id to be deleted.
|
||
*/
|
||
deleteCharge(clientId: string, chargeId: string) {
|
||
return this.http.delete(`/clients/${clientId}/charges/${chargeId}?associations=all`);
|
||
}
|
||
|
||
/*
|
||
* @param clientId Client Id of payer.
|
||
* @param chargeId Charge Id of the charge to be paid.
|
||
*/
|
||
getClientTransactionPay(clientId: string, chargeId: string) {
|
||
return this.http.get(`/clients/${clientId}/charges/${chargeId}`);
|
||
}
|
||
|
||
/**
|
||
* @param clientId Client Id of the payment.
|
||
* @param chargeId Charge Id of the payment.
|
||
* @param payment Client Payment data.
|
||
*/
|
||
payClientCharge(clientId: string, chargeId: string, payment: any) {
|
||
const httpParams = new HttpParams().set('command', 'paycharge');
|
||
return this.http.post(`/clients/${clientId}/charges/${chargeId}?command=paycharge`, payment, {
|
||
params: httpParams
|
||
});
|
||
}
|
||
|
||
getClientSummary(clientId: string) {
|
||
const httpParams = new HttpParams().set('R_clientId', clientId).set('genericResultSet', 'false');
|
||
return this.http.get(`/runreports/ClientSummary`, { params: httpParams });
|
||
}
|
||
|
||
getClientProfileImage(clientId: string) {
|
||
const httpParams = new HttpParams().set('maxHeight', '150');
|
||
// Keep it simple since our interceptor will handle the 404 errors
|
||
return this.http
|
||
.get(`/clients/${clientId}/images`, {
|
||
params: httpParams,
|
||
responseType: 'text'
|
||
})
|
||
.pipe(
|
||
// Handle the error here and return null when no image is found (404)
|
||
catchError(() => {
|
||
// Client has no profile image (404/403) - return null
|
||
return of(null);
|
||
})
|
||
);
|
||
}
|
||
|
||
uploadClientProfileImage(clientId: string, image: File) {
|
||
const formData = new FormData();
|
||
formData.append('file', image);
|
||
formData.append('filename', 'file');
|
||
return this.http.post(`/clients/${clientId}/images`, formData);
|
||
}
|
||
|
||
uploadCapturedClientProfileImage(clientId: string, imageURL: string) {
|
||
return this.http.post(`/clients/${clientId}/images`, imageURL);
|
||
}
|
||
|
||
deleteClientProfileImage(clientId: string) {
|
||
return this.http.delete(`/clients/${clientId}/images`);
|
||
}
|
||
|
||
uploadClientSignatureImage(clientId: string, signature: File) {
|
||
const formData = new FormData();
|
||
formData.append('file', signature);
|
||
formData.append('name', 'clientSignature');
|
||
formData.append('description', 'Client signature');
|
||
return this.http.post(`/clients/${clientId}/documents`, formData);
|
||
}
|
||
|
||
getClientSignatureImage(clientId: string, documentId: string) {
|
||
return this.http.get(`/clients/${clientId}/documents/${documentId}/attachment`, { responseType: 'blob' });
|
||
}
|
||
|
||
/**
|
||
* Uploads a webcam-captured signature (photograph of a paper signature).
|
||
* Mirrors `uploadClientSignatureImage()`'s multipart shape (name=clientSignature)
|
||
* exactly, but the source is a captured `data:` URL rather than a picked `File`.
|
||
*/
|
||
uploadCapturedClientSignature(clientId: string, imageDataUrl: string) {
|
||
const formData = new FormData();
|
||
formData.append('file', dataUrlToFile(imageDataUrl, 'clientSignature.jpg'));
|
||
formData.append('name', 'clientSignature');
|
||
formData.append('description', 'Client signature (captured via webcam)');
|
||
return this.http.post(`/clients/${clientId}/documents`, formData);
|
||
}
|
||
|
||
/**
|
||
* Generates an OTP against a client's OLD or NEW mobile/email destination, ahead of a
|
||
* sensitive contact-detail change. Dual-OTP verification (old + new) is required before
|
||
* the mobileNo/emailAddress change is accepted by the backend — see `verifyClientContactOtp()`.
|
||
* @param clientId Client ID
|
||
* @param channel 'MOBILE' | 'EMAIL'
|
||
* @param destination Raw mobile number or email address to send the OTP to
|
||
*/
|
||
sendClientContactOtp(clientId: string, channel: 'MOBILE' | 'EMAIL', destination: string): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/contact-verification/otp`, { channel, destination });
|
||
}
|
||
|
||
/**
|
||
* Verifies an OTP previously generated via `sendClientContactOtp()`.
|
||
* @param clientId Client ID
|
||
* @param otpId ID returned by the generate call
|
||
* @param otp 6-digit code entered by staff on the customer's behalf
|
||
*/
|
||
verifyClientContactOtp(clientId: string, otpId: number, otp: string): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/contact-verification/otp/verify`, { otpId, otp });
|
||
}
|
||
|
||
/**
|
||
* Generates an OTP against a NOT-YET-CREATED client's mobile/email, ahead of the initial
|
||
* `POST /clients` call. There is no clientId yet, so this hits the clientId-less sibling
|
||
* endpoint (`ClientCreationContactVerificationApiResource`) rather than
|
||
* `sendClientContactOtp()`'s `/clients/{clientId}/...` shape. The returned otpId is sent
|
||
* back as `mobileNewOtpId`/`emailNewOtpId` on `createClient()`.
|
||
* @param channel 'MOBILE' | 'EMAIL'
|
||
* @param destination Raw mobile number or email address to send the OTP to
|
||
*/
|
||
sendClientCreationContactOtp(channel: 'MOBILE' | 'EMAIL', destination: string): Observable<any> {
|
||
return this.http.post(`/clients/contact-verification/otp`, { channel, destination });
|
||
}
|
||
|
||
/**
|
||
* Verifies an OTP previously generated via `sendClientCreationContactOtp()`.
|
||
* @param otpId ID returned by the generate call
|
||
* @param otp 6-digit code entered by staff on the customer's behalf
|
||
*/
|
||
verifyClientCreationContactOtp(otpId: number, otp: string): Observable<any> {
|
||
return this.http.post(`/clients/contact-verification/otp/verify`, { otpId, otp });
|
||
}
|
||
|
||
getClientFamilyMembers(clientId: string) {
|
||
return this.http.get(`/clients/${clientId}/familymembers`);
|
||
}
|
||
|
||
/**
|
||
* Enrich search results with DOB and father/spouse name.
|
||
* Fetches client data (for DOB) and family members (for father/spouse).
|
||
*/
|
||
enrichSearchResults(results: any[]): void {
|
||
const getRelName = (m: any): string => {
|
||
if (typeof m.relationshipId === 'object' && m.relationshipId?.name) {
|
||
return m.relationshipId.name.toUpperCase();
|
||
}
|
||
return '';
|
||
};
|
||
for (const client of results) {
|
||
// Fetch DOB from client data
|
||
this.getClientData(client.id.toString())
|
||
.pipe(catchError(() => of(null)))
|
||
.subscribe((data: any) => {
|
||
if (data?.dateOfBirth) {
|
||
client.dateOfBirth = data.dateOfBirth;
|
||
}
|
||
});
|
||
|
||
// Fetch father/spouse from family members
|
||
this.getClientFamilyMembers(client.id.toString())
|
||
.pipe(catchError(() => of([])))
|
||
.subscribe((members: any) => {
|
||
const list = Array.isArray(members) ? members : [];
|
||
const spouse = list.find((m: any) => getRelName(m) === 'SPOUSE');
|
||
const father = list.find((m: any) => getRelName(m) === 'FATHER');
|
||
if (spouse) {
|
||
client.fatherSpouseName = `S/o: ${spouse.firstName}`;
|
||
} else if (father) {
|
||
client.fatherSpouseName = `F/o: ${father.firstName}`;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
getClientFamilyMember(clientId: string, familyMemberId: string) {
|
||
return this.http.get(`/clients/${clientId}/familymembers/${familyMemberId}`);
|
||
}
|
||
|
||
addFamilyMember(clientId: string, familyMemberData: any) {
|
||
return this.http.post(`/clients/${clientId}/familymembers`, familyMemberData);
|
||
}
|
||
|
||
editFamilyMember(clientId: string, familyMemberId: any, familyMemberData: any) {
|
||
return this.http.put(`/clients/${clientId}/familymembers/${familyMemberId}`, familyMemberData);
|
||
}
|
||
|
||
deleteFamilyMember(clientId: string, familyMemberId: string) {
|
||
return this.http.delete(`/clients/${clientId}/familymembers/${familyMemberId}`);
|
||
}
|
||
|
||
uploadFamilyMemberDocument(clientId: string, familyMemberId: string, formData: FormData): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/familymembers/${familyMemberId}/documents`, formData);
|
||
}
|
||
|
||
// ═══ Nominee APIs ═══
|
||
|
||
getClientNominees(clientId: string, accountNo?: string, accountType?: string): Observable<any> {
|
||
let params = new HttpParams();
|
||
if (accountNo) {
|
||
params = params.set('accountNo', accountNo);
|
||
}
|
||
if (accountType) {
|
||
params = params.set('accountType', accountType);
|
||
}
|
||
return this.http.get(`/clients/${clientId}/nominees`, { params });
|
||
}
|
||
|
||
getClientNomineeTemplate(clientId: string): Observable<any> {
|
||
return this.http.get(`/clients/${clientId}/nominees/template`);
|
||
}
|
||
|
||
addNominee(clientId: string, data: any): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/nominees`, data);
|
||
}
|
||
|
||
updateNominee(clientId: string, nomineeId: string, data: any): Observable<any> {
|
||
return this.http.put(`/clients/${clientId}/nominees/${nomineeId}`, data);
|
||
}
|
||
|
||
deleteNominee(clientId: string, nomineeId: string): Observable<any> {
|
||
return this.http.delete(`/clients/${clientId}/nominees/${nomineeId}`);
|
||
}
|
||
|
||
getClientIdentifiers(clientId: string) {
|
||
return this.http.get(`/clients/${clientId}/identifiers`);
|
||
}
|
||
|
||
getKycProofTypes(type?: 'id' | 'address'): Observable<any> {
|
||
const options = type ? { params: new HttpParams().set('type', type) } : {};
|
||
return this.http.get(`/kyc/proof-types`, options);
|
||
}
|
||
|
||
/**
|
||
* Phase 1 – Record Master: fetch clients missing CKYC registration
|
||
* or artefacts. Backed by GET /ckyc/record-master.
|
||
*/
|
||
getCkycRecordMaster(
|
||
filters: {
|
||
officeId?: number | string;
|
||
productId?: number | string;
|
||
fromDate?: string;
|
||
toDate?: string;
|
||
missing?: 'photo' | 'signature' | 'id' | 'address' | 'any';
|
||
limit?: number;
|
||
offset?: number;
|
||
} = {}
|
||
): Observable<any> {
|
||
let params = new HttpParams();
|
||
Object.entries(filters).forEach(
|
||
([
|
||
key,
|
||
value
|
||
]) => {
|
||
if (value !== null && value !== undefined && value !== '') {
|
||
params = params.set(key, String(value));
|
||
}
|
||
}
|
||
);
|
||
return this.http.get(`/ckyc/record-master`, { params });
|
||
}
|
||
|
||
/**
|
||
* Phase 2 – Auto ZIP Creation: bundle selected clients into a CERSAI
|
||
* ZIP on the server. Backed by POST /ckyc/batches.
|
||
*/
|
||
createCkycBatch(body: { clientIds: (number | string)[]; remarks?: string }): Observable<any> {
|
||
return this.http.post(`/ckyc/batches`, body);
|
||
}
|
||
|
||
/**
|
||
* Phase 2/3 – List CKYC batches. Supports filters and an optional
|
||
* groupByDate=true to bucket results by created_on for the UI.
|
||
*/
|
||
getCkycBatches(
|
||
filters: {
|
||
fromDate?: string;
|
||
toDate?: string;
|
||
uploadedToCersai?: boolean;
|
||
groupByDate?: boolean;
|
||
} = {}
|
||
): Observable<any> {
|
||
let params = new HttpParams();
|
||
Object.entries(filters).forEach(
|
||
([
|
||
key,
|
||
value
|
||
]) => {
|
||
if (value !== null && value !== undefined && value !== '') {
|
||
params = params.set(key, String(value));
|
||
}
|
||
}
|
||
);
|
||
return this.http.get(`/ckyc/batches`, { params });
|
||
}
|
||
|
||
/** Fetch a single CKYC batch with parsed manifest. */
|
||
getCkycBatchById(id: number | string): Observable<any> {
|
||
return this.http.get(`/ckyc/batches/${id}`);
|
||
}
|
||
|
||
/** Download the ZIP attached to a batch (via generic document endpoint). */
|
||
downloadCkycBatchZip(parentEntityId: number | string, documentId: number | string): Observable<Blob> {
|
||
return this.http.get(`/ckyc_batch/${parentEntityId}/documents/${documentId}/attachment`, {
|
||
responseType: 'blob'
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Phase 3 – SFTP upload to CERSAI. Uploads the batch ZIP over the
|
||
* SFTP channel and returns the ACK + per-client CKYC numbers.
|
||
*/
|
||
uploadCkycBatchToSftp(batchId: number | string): Observable<any> {
|
||
return this.http.post(`/ckyc/batches/${batchId}/sftp-upload`, {});
|
||
}
|
||
|
||
getClientIdentifierTemplate(clientId: string) {
|
||
return this.http.get(`/clients/${clientId}/identifiers/template`);
|
||
}
|
||
|
||
addClientIdentifier(clientId: string, identifierData: any) {
|
||
return this.http.post(`/clients/${clientId}/identifiers`, identifierData);
|
||
}
|
||
|
||
deleteClientIdentifier(clientId: string, identifierId: string) {
|
||
return this.http.delete(`/clients/${clientId}/identifiers/${identifierId}`);
|
||
}
|
||
|
||
getClientIdentificationDocuments(documentId: string) {
|
||
return this.http.get(`/client_identifiers/${documentId}/documents`);
|
||
}
|
||
|
||
downloadClientIdentificationDocument(parentEntityId: string, documentId: string) {
|
||
return this.http.get(`/client_identifiers/${parentEntityId}/documents/${documentId}/attachment`, {
|
||
responseType: 'blob'
|
||
});
|
||
}
|
||
|
||
uploadClientIdentifierDocument(identifierId: string, documentData: any) {
|
||
return this.http.post(`/client_identifiers/${identifierId}/documents`, documentData);
|
||
}
|
||
|
||
getClientDocuments(clientId: string) {
|
||
return this.http.get(`/clients/${clientId}/documents`);
|
||
}
|
||
|
||
downloadClientDocument(parentEntityId: string, documentId: string) {
|
||
return this.http.get(`/clients/${parentEntityId}/documents/${documentId}/attachment`, { responseType: 'blob' });
|
||
}
|
||
|
||
uploadClientDocument(clientId: string, documentData: any) {
|
||
return this.http.post(`/clients/${clientId}/documents`, documentData);
|
||
}
|
||
|
||
deleteClientDocument(parentEntityId: string, documentId: string) {
|
||
return this.http.delete(`/clients/${parentEntityId}/documents/${documentId}`);
|
||
}
|
||
|
||
getClientNotes(clientId: string) {
|
||
return this.http.get(`/clients/${clientId}/notes`);
|
||
}
|
||
|
||
createClientNote(clientId: string, noteData: any) {
|
||
return this.http.post(`/clients/${clientId}/notes`, noteData);
|
||
}
|
||
|
||
editClientNote(clientId: string, noteId: string, noteData: any) {
|
||
return this.http.put(`/clients/${clientId}/notes/${noteId}`, noteData);
|
||
}
|
||
|
||
deleteClientNote(clientId: string, noteId: string) {
|
||
return this.http.delete(`/clients/${clientId}/notes/${noteId}`);
|
||
}
|
||
|
||
getAddressFieldConfiguration() {
|
||
return this.http.get(`/fieldconfiguration/ADDRESS`);
|
||
}
|
||
|
||
/**
|
||
* Documents attached to an m_client_address row. The generic document API takes
|
||
* `client_addresses` as the parent-entity type — see DocumentApiResource.
|
||
* Used by the address-step component to render a thumbnail of the proof file the user
|
||
* previously uploaded so it isn't invisible on the edit screen.
|
||
*/
|
||
getClientAddressDocuments(clientAddressId: string) {
|
||
return this.http.get(`/client_addresses/${clientAddressId}/documents`);
|
||
}
|
||
|
||
downloadClientAddressDocument(clientAddressId: string, documentId: string) {
|
||
return this.http.get(`/client_addresses/${clientAddressId}/documents/${documentId}/attachment`, {
|
||
responseType: 'blob'
|
||
});
|
||
}
|
||
|
||
uploadClientAddressDocument(clientAddressId: string, documentData: any) {
|
||
return this.http.post(`/client_addresses/${clientAddressId}/documents`, documentData);
|
||
}
|
||
|
||
getClientAddressTemplate() {
|
||
return this.http.get(`/client/addresses/template`);
|
||
}
|
||
|
||
createClientAddress(clientId: string, addressTypeId: string, addressData: any) {
|
||
return this.http.post(`/client/${clientId}/addresses?type=${addressTypeId}`, addressData);
|
||
}
|
||
|
||
editClientAddress(clientId: string, addressTypeId: string, addressData: any) {
|
||
return this.http.put(`/client/${clientId}/addresses?type=${addressTypeId}`, addressData);
|
||
}
|
||
|
||
executeClientCommand(clientId: string, command: string, data: any): Observable<any> {
|
||
const httpParams = new HttpParams().set('command', command);
|
||
return this.http.post(`/clients/${clientId}`, data, { params: httpParams });
|
||
}
|
||
|
||
getClientCommandTemplate(command: string): Observable<any> {
|
||
const httpParams = new HttpParams().set('commandParam', command);
|
||
return this.http.get(`/clients/template`, { params: httpParams });
|
||
}
|
||
|
||
getClientTransferProposalDate(clientId: any): Observable<any> {
|
||
return this.http.get(`/clients/${clientId}/transferproposaldate`);
|
||
}
|
||
|
||
getClientChargeTemplate(clientId: any): Observable<any> {
|
||
return this.http.get(`/clients/${clientId}/charges/template`);
|
||
}
|
||
|
||
getChargeAndTemplate(chargeId: any): Observable<any> {
|
||
const httpParams = new HttpParams().set('template', 'true');
|
||
return this.http.get(`/charges/${chargeId}`, { params: httpParams });
|
||
}
|
||
|
||
createClientCharge(clientId: any, charge: any) {
|
||
return this.http.post(`/clients/${clientId}/charges`, charge);
|
||
}
|
||
|
||
getClientReportTemplates() {
|
||
const httpParams = new HttpParams().set('entityId', '0').set('typeId', '0');
|
||
return this.http.get('/templates', { params: httpParams });
|
||
}
|
||
|
||
retrieveClientReportTemplate(templateId: string, clientId: string) {
|
||
const httpParams = new HttpParams().set('clientId', clientId);
|
||
return this.http.get(`/templates/${templateId}`, { params: httpParams, responseType: 'text' });
|
||
}
|
||
|
||
/**
|
||
* @returns {Observable<any>} Offices data
|
||
*/
|
||
getOffices(): Observable<any> {
|
||
return this.http.get('/offices');
|
||
}
|
||
|
||
/**
|
||
* returns the list of survey data of the particular Client
|
||
* @param clientId
|
||
*/
|
||
getSurveys(clientId: string) {
|
||
return this.http.get(`/surveys/scorecards/clients/${clientId}`);
|
||
}
|
||
|
||
/**
|
||
* returns the list of survey types and questions
|
||
*/
|
||
getAllSurveysType() {
|
||
return this.http.get('/surveys');
|
||
}
|
||
|
||
/**
|
||
* returns the response from the post request for that survey
|
||
* @param surveyId
|
||
* @param surveyData Survey Data submitted by client
|
||
*/
|
||
createNewSurvey(surveyId: Number, surveyData: any) {
|
||
return this.http.post(`/surveys/scorecards/${surveyId}`, surveyData);
|
||
}
|
||
|
||
/**
|
||
* @param userData User Data.
|
||
*/
|
||
createSelfServiceUser(userData: any) {
|
||
return this.http.post(`/users`, userData);
|
||
}
|
||
|
||
/**
|
||
* @param clientId Client ID.
|
||
* @param collateralData Collateral Data
|
||
*/
|
||
createClientCollateral(clientId: any, collateralData: any) {
|
||
return this.http.post(`/clients/${clientId}/collaterals`, collateralData);
|
||
}
|
||
|
||
/**
|
||
* @param clientId Client ID.
|
||
*/
|
||
getCollateralTemplate(clientId: any) {
|
||
return this.http.get(`/clients/${clientId}/collaterals/template`);
|
||
}
|
||
|
||
searchByText(
|
||
text: string,
|
||
page: number,
|
||
pageSize: number,
|
||
sortAttribute: string = '',
|
||
sortDirection: string = '',
|
||
activeOnly: boolean = false,
|
||
legalFormType: string = '',
|
||
silent: boolean = false
|
||
) {
|
||
const innerRequest: any = { text };
|
||
if (activeOnly) {
|
||
innerRequest.activeOnly = true;
|
||
}
|
||
// Restrict to one legal form when caller asks for it. Used by guardian/nominee/family-member
|
||
// pickers — non-individual entities (companies, partnerships) make no sense as a "guardian"
|
||
// or "spouse", so the SPA passes 'INDIVIDUAL' there. Backend treats this as authoritative;
|
||
// omitting the param still returns all forms (backwards-compatible for the general client list).
|
||
if (legalFormType) {
|
||
innerRequest.legalFormType = legalFormType;
|
||
}
|
||
let request: any = {
|
||
request: innerRequest,
|
||
page,
|
||
size: pageSize
|
||
};
|
||
if (sortAttribute !== '' && sortDirection !== '') {
|
||
request = {
|
||
...request,
|
||
sorts: [
|
||
{
|
||
direction: sortDirection,
|
||
property: sortAttribute
|
||
}
|
||
]
|
||
};
|
||
}
|
||
return this.http.post(
|
||
`/v2/clients/search`,
|
||
request,
|
||
silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {}
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Lookup external National ID from the configured external system.
|
||
* Uses a separate HttpClient (via HttpBackend) to bypass Angular interceptors
|
||
* so that Fineract auth headers are not sent to the external API.
|
||
*
|
||
* In development, requests go through the dev proxy (/external-nationalid).
|
||
* In production, requests go through the nginx reverse proxy.
|
||
*
|
||
* @param externalId The National ID string (e.g. CURP)
|
||
*/
|
||
lookupExternalNationalId(externalId: string): Observable<any> {
|
||
const apiUrl = environment.externalNationalIdSystemUrl;
|
||
if (!apiUrl) {
|
||
return throwError(() => new Error('External National ID System URL is not configured'));
|
||
}
|
||
|
||
let headers = new HttpHeaders({ 'Content-Type': 'application/json' });
|
||
const apiHeader = environment.externalNationalIdSystemApiHeader;
|
||
const apiKey = environment.externalNationalIdSystemApiKey;
|
||
if (apiHeader && apiKey) {
|
||
// Validate header name to prevent Angular from throwing on invalid header
|
||
if (!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(apiHeader)) {
|
||
return throwError(() => new Error(`Invalid API header name: '${apiHeader}'`));
|
||
}
|
||
headers = headers.set(apiHeader, apiKey);
|
||
}
|
||
|
||
return this.externalHttp.post(apiUrl, { externalId }, { headers });
|
||
}
|
||
|
||
/**
|
||
* Gets KYC details for a client.
|
||
* @param clientId Client ID
|
||
*/
|
||
getClientKyc(clientId: string, silent = false): Observable<any> {
|
||
return this.http.get(
|
||
`/clients/${clientId}/kyc`,
|
||
silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {}
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Gets KYC template for a client — returns required documents based on client type.
|
||
* @param clientId Client ID
|
||
*/
|
||
getClientKycTemplate(clientId: string): Observable<any> {
|
||
return this.http.get(`/clients/${clientId}/kyc/template`);
|
||
}
|
||
|
||
getClientRiskRules(): Observable<any[]> {
|
||
return this.http.get<any[]>('/kyc/risk-rules');
|
||
}
|
||
|
||
/** Fetch code values by code ID */
|
||
getCodeValuesById(codeId: number): Observable<any[]> {
|
||
return this.http.get<any[]>(`/codes/${codeId}/codevalues`).pipe(catchError(() => of([])));
|
||
}
|
||
|
||
/** Fetch code values by code name (e.g. 'FORM_60_PURPOSE') */
|
||
getCodeValuesByName(codeName: string): Observable<any[]> {
|
||
return this.http.get<any[]>('/codes').pipe(
|
||
switchMap((codes: any[]) => {
|
||
const code = codes.find((c: any) => c.name === codeName);
|
||
if (code) {
|
||
return this.http.get<any[]>(`/codes/${code.id}/codevalues`);
|
||
}
|
||
return of([]);
|
||
}),
|
||
catchError(() => of([]))
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Saves KYC details for a client.
|
||
* @param clientId Client ID
|
||
* @param kycData KYC payload
|
||
*/
|
||
saveClientKyc(clientId: string, kycData: any): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/kyc`, kycData);
|
||
}
|
||
|
||
/**
|
||
* Screen a client name against the UN Security Council consolidated sanctions list.
|
||
* @param name The name to screen
|
||
* @returns Observable with screening result (status: FLAGGED or CLEAR)
|
||
*/
|
||
screenSanctions(name: string): Observable<any> {
|
||
return this.http.post('/sanctions/screen', { name });
|
||
}
|
||
|
||
/**
|
||
* Updates existing KYC details for a client.
|
||
* @param clientId Client ID
|
||
* @param kycId KYC record ID
|
||
* @param kycData KYC payload
|
||
*/
|
||
updateClientKyc(clientId: string, kycId: string, kycData: any): Observable<any> {
|
||
return this.http.put(`/clients/${clientId}/kyc/${kycId}`, kycData);
|
||
}
|
||
|
||
/**
|
||
* Re-edit a VERIFIED KYC record. Transitions status VERIFIED → IN_PROGRESS.
|
||
* Remarks are mandatory in the payload.
|
||
*/
|
||
reEditClientKyc(clientId: string, kycId: string, kycData: any): Observable<any> {
|
||
return this.http.put(`/clients/${clientId}/kyc/${kycId}?command=re-edit`, kycData);
|
||
}
|
||
|
||
/**
|
||
* Verifies PAN number via LincPay (reads PAN from the stored KYC record).
|
||
* @param clientId Client ID
|
||
* @param kycId KYC record ID
|
||
*/
|
||
verifyPAN(clientId: string, kycId: string): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=verify-pan`, {});
|
||
}
|
||
|
||
/**
|
||
* Step 1 of Aadhaar e-KYC: sends OTP to the Aadhaar-linked mobile via LincPay.
|
||
* @param clientId Client ID
|
||
* @param kycId KYC record ID
|
||
* @param aadhaarNumber Full 12-digit Aadhaar number (never stored)
|
||
*/
|
||
sendAadhaarOtp(clientId: string, kycId: string, aadhaarNumber: string): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=aadhaar-generate-otp`, { aadhaarNumber });
|
||
}
|
||
|
||
/**
|
||
* Step 2 of Aadhaar e-KYC: verifies the OTP entered by the client via LincPay.
|
||
* @param clientId Client ID
|
||
* @param kycId KYC record ID
|
||
* @param otp OTP received on Aadhaar-linked mobile
|
||
* @param aadhaarRefNumber Reference number returned by sendAadhaarOtp (changes.aadhaarRefNumber)
|
||
*/
|
||
verifyAadhaarOtp(clientId: string, kycId: string, otp: string, aadhaarRefNumber: string): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=verify-aadhaar`, {
|
||
aadhaarRefNumber,
|
||
aadhaarOtp: otp
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Verifies GSTIN via LincPay (reads GSTIN from the stored KYC record).
|
||
* @param clientId Client ID
|
||
* @param kycId KYC record ID
|
||
*/
|
||
verifyGSTIN(clientId: string, kycId: string): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=verify-gstin`, {});
|
||
}
|
||
|
||
/**
|
||
* Verifies DIN (Director Identification Number) for an authorized signatory.
|
||
* Sends signatory clientId, DIN number and the non-individual entity name.
|
||
* Backend saves DIN & verification status in KYC table and returns company list.
|
||
* @param clientId Signatory's Client ID
|
||
* @param kycId KYC record ID
|
||
* @param dinNumber DIN number to verify
|
||
* @param entityName Non-individual entity name (for company match)
|
||
*/
|
||
verifyDIN(clientId: string, kycId: string, dinNumber: string, entityName: string): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=verify-din`, { dinNumber, entityName });
|
||
}
|
||
|
||
/**
|
||
* Completes Re-KYC after uploading signed self-declaration form.
|
||
* Updates Last Verified date to today and sets new Expiry Date (today + 10 years for low risk).
|
||
* @param clientId Client ID
|
||
* @param kycId KYC record ID
|
||
*/
|
||
completeReKyc(clientId: string, kycId: string): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=complete-rekyc`, {});
|
||
}
|
||
|
||
// ═══ Entity Classification APIs ═══
|
||
|
||
getEntityClassifications(clientId: string): Observable<any> {
|
||
return this.http.get(`/clients/${clientId}/entityclassification`);
|
||
}
|
||
|
||
saveEntityClassification(clientId: string, category: string, data: any): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/entityclassification/${category}`, data);
|
||
}
|
||
|
||
getClientCkyc(clientId: string): Observable<any> {
|
||
return this.http.get(`/clients/${clientId}/ckyc`);
|
||
}
|
||
|
||
uploadCkycDocument(clientId: string, formData: FormData): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/ckyc/documents`, formData);
|
||
}
|
||
|
||
/**
|
||
* Check mobile number against the DIP (Digital Intelligence Platform) MNRL.
|
||
* Calls an external service directly, bypassing Fineract interceptors.
|
||
* @param mobileNo Mobile number to verify
|
||
*/
|
||
checkDipLookup(mobileNo: string): Observable<any> {
|
||
const httpParams = new HttpParams().set('mobileNumber', mobileNo);
|
||
return this.http.get('/lookup/check', { params: httpParams });
|
||
}
|
||
|
||
getClientForm60(clientId: string): Observable<any> {
|
||
return this.http.get(`/clients/${clientId}/form60`);
|
||
}
|
||
|
||
submitClientForm60(clientId: string, data: any): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/form60`, data);
|
||
}
|
||
|
||
getWelcomeKits(clientId: string | number): Observable<any> {
|
||
return this.http.get(`/clients/${clientId}/welcomekit`);
|
||
}
|
||
|
||
dispatchWelcomeKit(clientId: string | number, data: any): Observable<any> {
|
||
return this.http.post(`/clients/${clientId}/welcomekit`, data);
|
||
}
|
||
}
|