Files
cbsnexora/src/app/clients/clients.service.ts
T
nitesh 14c5f5070d Revert demo to pre-2026-08-06 state (rollback today's push)
Reverts all commits pushed to demo today (a5124ca3, 858c277a, a1a50ab8,
f72abb09, fcf66657) back to f8c532f7 as 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.
2026-08-06 13:39:40 +05:30

962 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}
}