/** * 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 { 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 { 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 { return this.http.get('/clients/template'); } getClientWithOfficeTemplate(officeId: number): Observable { 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 { 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 || '' }); } /** Fetches the exact email content (subject, body, sentAt, openedAt) for a given pending edit. * Only populated after the Checker has approved and the send succeeded. */ getClientEditEmailPreview(clientId: string | number, pendingEditId: string | number) { return this.http.get<{ emailSubject: string | null; emailBody: string | null; emailSent: boolean; emailSentAt: string | null; emailOpenedAt: string | null; emailFrom: string | null; emailTo: string | null; }>(`/clients/${clientId}/edit/${pendingEditId}/email-preview`); } 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 { 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 { 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 { 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 { 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 { return this.http.post(`/clients/${clientId}/familymembers/${familyMemberId}/documents`, formData); } // ═══ Nominee APIs ═══ getClientNominees(clientId: string, accountNo?: string, accountType?: string): Observable { 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 { return this.http.get(`/clients/${clientId}/nominees/template`); } addNominee(clientId: string, data: any): Observable { return this.http.post(`/clients/${clientId}/nominees`, data); } updateNominee(clientId: string, nomineeId: string, data: any): Observable { return this.http.put(`/clients/${clientId}/nominees/${nomineeId}`, data); } deleteNominee(clientId: string, nomineeId: string): Observable { return this.http.delete(`/clients/${clientId}/nominees/${nomineeId}`); } getClientIdentifiers(clientId: string) { return this.http.get(`/clients/${clientId}/identifiers`); } getKycProofTypes(type?: 'id' | 'address'): Observable { 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 { 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 { 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 { 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 { 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 { 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 { 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 { const httpParams = new HttpParams().set('command', command); return this.http.post(`/clients/${clientId}`, data, { params: httpParams }); } getClientCommandTemplate(command: string): Observable { const httpParams = new HttpParams().set('commandParam', command); return this.http.get(`/clients/template`, { params: httpParams }); } getClientTransferProposalDate(clientId: any): Observable { return this.http.get(`/clients/${clientId}/transferproposaldate`); } getClientChargeTemplate(clientId: any): Observable { return this.http.get(`/clients/${clientId}/charges/template`); } getChargeAndTemplate(chargeId: any): Observable { 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} Offices data */ getOffices(): Observable { 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 { 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 { 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 { return this.http.get(`/clients/${clientId}/kyc/template`); } getClientRiskRules(): Observable { return this.http.get('/kyc/risk-rules'); } /** * Adds a rule to the AML catalog. Amount/window details in the payload also create the * matching live-transaction rule server side; the response is the persisted rule in the same * shape as {@link getClientRiskRules}. */ createClientRiskRule(rule: any): Observable { return this.http.post('/kyc/risk-rules', rule); } /** Fetch code values by code ID */ getCodeValuesById(codeId: number): Observable { return this.http.get(`/codes/${codeId}/codevalues`).pipe(catchError(() => of([]))); } /** Fetch code values by code name (e.g. 'FORM_60_PURPOSE') */ getCodeValuesByName(codeName: string): Observable { return this.http.get('/codes').pipe( switchMap((codes: any[]) => { const code = codes.find((c: any) => c.name === codeName); if (code) { return this.http.get(`/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 { 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 { return this.http.post('/sanctions/screen', { name }); } /** * Records the reviewing officer's assessment of a flagged screening. * * A FALSE_POSITIVE does not unblock onboarding on its own — it only becomes effective once * the Principal Officer approves it from the Sanctions Logs screen. * * @param logId screening log id returned by screenSanctions() * @param disposition 'FALSE_POSITIVE' | 'CONFIRMED_MATCH' * @param reason which identifiers were compared and why they do/don't match */ recordSanctionsDisposition(logId: number, disposition: string, reason: string): Observable { return this.http.post(`/sanctions/alerts/${logId}/disposition`, { disposition, reason }); } /** * Records the officer's decision on a DIP/MNRL lookup that came back flagged or unverifiable. * Until PROCEEDED is stored, the client-create gate refuses to save the number. * * @param decision 'PROCEEDED' | 'REJECTED' */ recordDipDecision(logId: number, decision: string, reason: string): Observable { return this.http.post(`/lookup/logs/${logId}/decision`, { decision, reason }); } /** * 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 { 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 { 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 { 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 { 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 { 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 { return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=verify-gstin`, {}); } /** * Verifies CIN against MCA (reads the CIN from the stored KYC record, so it must be saved first). * @param clientId Client ID * @param kycId KYC record ID */ verifyCIN(clientId: string, kycId: string): Observable { return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=verify-cin`, {}); } /** * 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 { 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 { return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=complete-rekyc`, {}); } // ═══ Entity Classification APIs ═══ getEntityClassifications(clientId: string): Observable { return this.http.get(`/clients/${clientId}/entityclassification`); } saveEntityClassification(clientId: string, category: string, data: any): Observable { return this.http.post(`/clients/${clientId}/entityclassification/${category}`, data); } getClientCkyc(clientId: string): Observable { return this.http.get(`/clients/${clientId}/ckyc`); } uploadCkycDocument(clientId: string, formData: FormData): Observable { 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 { const httpParams = new HttpParams().set('mobileNumber', mobileNo); return this.http.get('/lookup/check', { params: httpParams }); } getClientForm60(clientId: string): Observable { return this.http.get(`/clients/${clientId}/form60`); } submitClientForm60(clientId: string, data: any): Observable { return this.http.post(`/clients/${clientId}/form60`, data); } getWelcomeKits(clientId: string | number): Observable { return this.http.get(`/clients/${clientId}/welcomekit`); } dispatchWelcomeKit(clientId: string | number, data: any): Observable { return this.http.post(`/clients/${clientId}/welcomekit`, data); } updateWelcomeKitStatus(welcomeKitId: number, data: any): Observable { return this.http.put(`/welcomekit/${welcomeKitId}`, data); } getKycExceptions(resolved = false): Observable { return this.http.get(`/kyc/exceptions?resolved=${resolved}`); } /** * Records a KYC-exception outreach attempt against a customer, and — for the SMS, EMAIL and * WHATSAPP channels — actually transmits the notice. * * The destination is resolved server-side from the client record; it is deliberately not part of * this payload, so a tampered request cannot redirect a KYC notice elsewhere. * * Resolves with HTTP 200 even when delivery fails — inspect `deliveryStatus` / `delivered` on the * response rather than assuming success, since the log row is written either way. */ saveKycContactLog( clientId: string | number, data: { channel: string; notes: string; officerName?: string } ): Observable { return this.http.post(`/kyc/exceptions/${clientId}/contact-log`, data); } getKycContactLog(clientId: string | number): Observable { return this.http.get(`/kyc/exceptions/${clientId}/contact-log`); } }