/** * 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/. */ import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; /** A hold the bank placed for an I4C complaint (m_i4c_hold). */ export interface I4cHold { id: number; request_id: number; incident_id: number; acknowledgement_no: string; account_type: 'SAVINGS' | 'CURRENT'; account_id: number; account_no: string; client_id?: number; client_name?: string; office_name?: string; layer: number; disputed_amount: number; amount_held: number; shortfall: number; status: 'ACTIVE' | 'RELEASED' | 'NOT_HOLDABLE'; debit_frozen: boolean; held_at: string; review_due_on: string; intimation_due_by?: string; customer_intimated_at?: string; intimation_mode?: string; intimation_reference?: string; intimated_by?: string; released_at?: string; release_reason_type?: string; release_reference?: string; pending_actions?: number; transactions?: any[]; actions?: any[]; /** Every notice sent or recorded for this hold, newest first. */ intimationLog?: IntimationLogEntry[]; /** Channels switched on for automatic intimation, in the order they are tried. */ automaticChannels?: string[]; grievances?: any[]; restorations?: any[]; documents?: I4cDocument[]; released_amount?: number; restored_amount?: number; digital_suspended?: boolean; lapse_on?: string; lea_notice_due_on?: string; lea_notice_sent_on?: string; lea_notice_ref?: string; extension_until?: string; extension_ref?: string; linkedAlerts?: any[]; relatedAccounts?: any[]; excludedKind?: { kind: string }[]; muleFlags?: any[]; fraudReports?: any[]; channelBlocks?: any[]; reportsOnAccount?: number; canLapseRelease?: boolean; auto_intimation_attempts?: number; } export interface IntimationLogEntry { id: number; channel: string; mode: 'AUTO' | 'MANUAL' | 'RESEND'; status: string; destination_masked?: string; provider?: string; provider_reference?: string; failure_reason?: string; actor?: string; created_at: string; } /** An automatic customer-intimation channel (m_i4c_intimation_channel). */ export interface IntimationChannel { channel: 'EMAIL' | 'SMS' | 'WHATSAPP'; enabled: boolean; priority: number; subject?: string; body: string; updated_by?: string; updated_at?: string; } export interface IntimationSettings { channels: IntimationChannel[]; placeholders: string[]; manualChannels: string[]; } export interface IntimationResult { outcome: 'SENT' | 'NO_CHANNEL_ENABLED' | 'NOT_DELIVERED' | 'ALREADY_INTIMATED' | 'NOTHING_HELD'; channel?: string; destination?: string; message: string; } export type I4cDocumentOwner = 'HOLD' | 'GRIEVANCE' | 'RESTORATION'; /** A document attached to a hold, grievance or restoration (content downloaded separately). */ export interface I4cDocument { id: number; doc_type: string; file_name: string; content_type: string; size_bytes: number; sha256: string; remarks?: string; uploaded_by: string; uploaded_at: string; } export const DOCUMENT_TYPES: { value: string; label: string }[] = [ { value: 'IO_ORDER', label: 'IO order' }, { value: 'GRM_ORDER', label: 'GRM order (DGRO / SGRO)' }, { value: 'COURT_ORDER', label: 'Court order' }, { value: 'BNSS_106_NOTICE', label: 'Notice u/s 106 BNSS' }, { value: 'INDEMNITY_BOND', label: 'Indemnity bond' }, { value: 'FIR', label: 'FIR copy' }, { value: 'KYC', label: 'KYC document' }, { value: 'APPLICATION', label: "Customer's application" }, { value: 'PHOTO', label: 'Photograph' }, { value: 'BANK_STATEMENT', label: 'Bank statement' }, { value: 'EDD_REPORT', label: 'EDD report' }, { value: 'PAN', label: 'PAN card' }, { value: 'OTHER', label: 'Other' } ]; /** Save a downloaded file (CSV, document) under the given name. */ export function saveBlob(blob: Blob, fileName: string): void { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); } /** One page of an officer list, as every /i4c/admin list endpoint returns it. */ export interface I4cPage { totalFilteredRecords: number; pageItems: T[]; } /** Query-string filters of a list; empty values are left out of the request. */ export type I4cListParams = Record; export interface HoldActionRequest { action: 'RELEASE' | 'FREEZE' | 'UNFREEZE' | 'UNFREEZE_AND_RELEASE' | 'SUSPEND_DIGITAL' | 'RESTORE_DIGITAL'; /** RELEASE only: how much to release; empty releases all. */ amount?: number | null; reasonType: string; orderReference?: string; orderDate?: string; authority?: string; remarks?: string; } export const HOLD_REASON_TYPES: { value: string; label: string }[] = [ { value: 'IO_ORDER', label: 'Investigating Officer order' }, { value: 'GRM_DECISION', label: 'GRM decision (IO / DGRO / SGRO)' }, { value: 'MRM_RESTORATION', label: 'MRM money restoration' }, { value: 'COURT_ORDER', label: 'Court order' }, { value: 'LEA_DIRECTION', label: 'LEA direction' }, { value: 'REVIEW_90_DAYS', label: '90-day review (no direction received)' }, { value: 'WRONG_ACCOUNT', label: 'Wrong account reported' }, { value: 'OTHER', label: 'Other' } ]; /** Officer API of the I4C (NCRP-CFCFRMS) integration: /v1/i4c/admin/* on the CBS backend. */ @Injectable({ providedIn: 'root' }) export class I4cService { private http = inject(HttpClient); private base = '/i4c/admin'; private cases = '/i4c/cases'; dashboard(): Observable { return this.http.get(`${this.base}/dashboard`); } /** Filters: status, search, subCategory, layer, response, fromDate, toDate, offset, limit. */ requests(query: I4cListParams = {}): Observable> { return this.http.get>(`${this.base}/requests`, { params: this.params(query) }); } request(id: number): Observable { return this.http.get(`${this.base}/requests/${id}`); } reprocess(id: number): Observable { return this.http.post(`${this.base}/requests/${id}/reprocess`, {}); } /** Filters: view, search, accountType, intimation, frozen, layer, fromDate, toDate, offset, limit. */ holds(query: I4cListParams = {}): Observable> { return this.http.get>(`${this.base}/holds`, { params: this.params(query) }); } hold(id: number): Observable { return this.http.get(`${this.base}/holds/${id}`); } requestHoldAction(holdId: number, body: HoldActionRequest): Observable { return this.http.post(`${this.base}/holds/${holdId}/actions`, body); } recordIntimation(holdId: number, mode: string, reference?: string): Observable { return this.http.post(`${this.base}/holds/${holdId}/intimation`, { mode, reference }); } /** Filters: status, action, reasonType, search, fromDate, toDate, offset, limit. */ holdActions(query: I4cListParams = {}): Observable> { return this.http.get>(`${this.base}/hold-actions`, { params: this.params(query) }); } approveHoldAction(id: number): Observable { return this.http.post(`${this.base}/hold-actions/${id}/approve`, {}); } rejectHoldAction(id: number, reason: string): Observable { return this.http.post(`${this.base}/hold-actions/${id}/reject`, { reason }); } /** Filters: status, type (ACK / RESPONSE), search, fromDate, toDate, offset, limit. */ outbox(query: I4cListParams = {}): Observable> { return this.http.get>(`${this.base}/outbox`, { params: this.params(query) }); } retryOutbox(id: number): Observable { return this.http.post(`${this.base}/outbox/${id}/retry`, {}); } /** Filters: direction, failures, httpStatus, responseCode, search, fromDate, toDate, offset, limit. */ apiLog(query: I4cListParams = {}): Observable> { return this.http.get>(`${this.base}/api-log`, { params: this.params(query) }); } apiLogEntry(id: number): Observable { return this.http.get(`${this.base}/api-log/${id}`); } /** Send the hold notice now on the switched-on automatic channels. */ sendIntimation(holdId: number): Observable { return this.http.post(`${this.base}/holds/${holdId}/intimation/send`, {}); } intimationChannels(): Observable { return this.http.get(`${this.base}/intimation-channels`); } updateIntimationChannel( channel: string, body: { enabled?: boolean; priority?: number; subject?: string; body?: string } ): Observable { return this.http.put(`${this.base}/intimation-channels/${channel}`, body); } testIntimationChannel(channel: string, to: string): Observable { return this.http.post(`${this.base}/intimation-channels/${channel}/test`, { to }); } /** Filters: search, type, ifsc (KNOWN / UNKNOWN), offset, limit. */ bankMaster(query: I4cListParams = {}): Observable> { return this.http.get>(`${this.base}/bank-master`, { params: this.params(query) }); } /** The bank's IFSC / prefix -> I4C Bank Code map, and reported IFSCs that had no code. */ ifscMap(): Observable<{ mappings: any[]; unknown: any[] }> { return this.http.get<{ mappings: any[]; unknown: any[] }>(`${this.cases}/ifsc-map`); } mapIfsc(body: { ifscKey: string; bankCode: number; note?: string }): Observable<{ resourceId: number }> { return this.http.post<{ resourceId: number }>(`${this.cases}/ifsc-map`, body); } unmapIfsc(id: number): Observable<{ resourceId: number }> { return this.http.post<{ resourceId: number }>(`${this.cases}/ifsc-map/${id}/remove`, {}); } // ── GRM / MRM (/v1/i4c/cases) ────────────────────────────────────────── grievances(query: I4cListParams = {}): Observable> { return this.http.get>(`${this.cases}/grievances`, { params: this.params(query) }); } grievance(id: number): Observable { return this.http.get(`${this.cases}/grievances/${id}`); } createGrievance( holdId: number, body: { customerRequest: string; customerReason?: string } ): Observable<{ resourceId: number }> { return this.http.post<{ resourceId: number }>(`${this.cases}/holds/${holdId}/grievances`, body); } /** step: edd, submit, nodal, events, investigation, decision, escalate, withdraw */ grievanceStep(id: number, step: string, body: Record): Observable { return this.http.post(`${this.cases}/grievances/${id}/${step}`, body); } raiseGrievanceAction(id: number): Observable<{ resourceId: number; actionRequestId: number | null }> { return this.http.post<{ resourceId: number; actionRequestId: number | null }>( `${this.cases}/grievances/${id}/action`, {} ); } restorations(query: I4cListParams = {}): Observable> { return this.http.get>(`${this.cases}/restorations`, { params: this.params(query) }); } restoration(id: number): Observable { return this.http.get(`${this.cases}/restorations/${id}`); } assessRestoration( holdId: number, body: { basis: string; restorableAmount?: number | null; remarks?: string } ): Observable<{ resourceId: number }> { return this.http.post<{ resourceId: number }>(`${this.cases}/holds/${holdId}/restorations`, body); } recordRestorationRequest(id: number, body: Record): Observable { return this.http.put(`${this.cases}/restorations/${id}/request`, body); } /** step: bank-action, retry-outward, mrm-updated, receipt, victim-status */ restorationStep(id: number, step: string, body: Record = {}): Observable { return this.http.post(`${this.cases}/restorations/${id}/${step}`, body); } openVictimCase(acknowledgementNo: string): Observable<{ resourceId: number }> { return this.http.post<{ resourceId: number }>(`${this.cases}/victim-cases`, { acknowledgementNo }); } proRata(accountNo: string, amount?: number | null): Observable { return this.http.get(`${this.cases}/pro-rata`, { params: this.params({ accountNo, amount }) }); } mrmBalanceReport(): Observable { return this.http.get(`${this.cases}/mrm-balance-report`); } mrmBalanceReportCsv(): Observable { return this.http.get(`${this.cases}/mrm-balance-report.csv`, { responseType: 'blob' }); } statementCsv(holdId: number, fromDate?: string, toDate?: string): Observable { return this.http.get(`${this.cases}/holds/${holdId}/statement.csv`, { params: this.params({ fromDate, toDate }), responseType: 'blob' }); } /** A POST on /i4c/cases/{path}, e.g. restorations/5/contest or holds/3/lea-notice. */ casePost(path: string, body: Record = {}): Observable { return this.http.post(`${this.cases}/${path}`, body); } casePut(path: string, body: Record = {}): Observable { return this.http.put(`${this.cases}/${path}`, body); } /** A paged register on /i4c/cases: mule-flags, fraud-reports, linked-alerts, channel-blocks. */ register(name: string, query: I4cListParams = {}): Observable> { return this.http.get>(`${this.cases}/${name}`, { params: this.params(query) }); } fraudReportsCsv(): Observable { return this.http.get(`${this.cases}/fraud-reports.csv`, { responseType: 'blob' }); } documents(ownerType: I4cDocumentOwner, ownerId: number): Observable { return this.http.get(`${this.cases}/documents`, { params: this.params({ ownerType, ownerId }) }); } uploadDocument(body: { ownerType: I4cDocumentOwner; ownerId: number; docType: string; fileName: string; contentType: string; content: string; remarks?: string; }): Observable<{ resourceId: number }> { return this.http.post<{ resourceId: number }>(`${this.cases}/documents`, body); } downloadDocument(id: number): Observable { return this.http.get(`${this.cases}/documents/${id}/content`, { responseType: 'blob' }); } private params(values: Record): HttpParams { let params = new HttpParams(); Object.entries(values).forEach( ([ key, value ]) => { if (value !== undefined && value !== null && value !== '') { params = params.set(key, String(value)); } } ); return params; } } /** Backend errors come as Fineract's { defaultUserMessage, errors[] }; this picks the sentence a user should read. */ export function i4cErrorMessage(error: any): string { const body = error?.error; return ( body?.errors?.[0]?.defaultUserMessage || body?.defaultUserMessage || body?.developerMessage || error?.message || 'Request failed' ); }