Files
cbsnexora/src/app/cfcfrms/i4c/i4c.service.ts
T
niteshandClaude Opus 5.5 96bebd339b feat(cfcfrms): I4C Live screens for SOP layer chain, risk, bank holds and restorations
- Risk screen and shared I4C date field
- Bank Master: IFSC to I4C bank code map and unmapped IFSC list
- Holds, restorations, grievances, documents, outbox and dashboard updates
  for the backend I4C SOP / outward ledger changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:25:40 +05:30

431 lines
15 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/.
*/
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<T> {
totalFilteredRecords: number;
pageItems: T[];
}
/** Query-string filters of a list; empty values are left out of the request. */
export type I4cListParams = Record<string, string | number | boolean | null | undefined>;
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<any> {
return this.http.get<any>(`${this.base}/dashboard`);
}
/** Filters: status, search, subCategory, layer, response, fromDate, toDate, offset, limit. */
requests(query: I4cListParams = {}): Observable<I4cPage<any>> {
return this.http.get<I4cPage<any>>(`${this.base}/requests`, { params: this.params(query) });
}
request(id: number): Observable<any> {
return this.http.get<any>(`${this.base}/requests/${id}`);
}
reprocess(id: number): Observable<any> {
return this.http.post<any>(`${this.base}/requests/${id}/reprocess`, {});
}
/** Filters: view, search, accountType, intimation, frozen, layer, fromDate, toDate, offset, limit. */
holds(query: I4cListParams = {}): Observable<I4cPage<I4cHold>> {
return this.http.get<I4cPage<I4cHold>>(`${this.base}/holds`, { params: this.params(query) });
}
hold(id: number): Observable<I4cHold> {
return this.http.get<I4cHold>(`${this.base}/holds/${id}`);
}
requestHoldAction(holdId: number, body: HoldActionRequest): Observable<any> {
return this.http.post<any>(`${this.base}/holds/${holdId}/actions`, body);
}
recordIntimation(holdId: number, mode: string, reference?: string): Observable<any> {
return this.http.post<any>(`${this.base}/holds/${holdId}/intimation`, { mode, reference });
}
/** Filters: status, action, reasonType, search, fromDate, toDate, offset, limit. */
holdActions(query: I4cListParams = {}): Observable<I4cPage<any>> {
return this.http.get<I4cPage<any>>(`${this.base}/hold-actions`, { params: this.params(query) });
}
approveHoldAction(id: number): Observable<any> {
return this.http.post<any>(`${this.base}/hold-actions/${id}/approve`, {});
}
rejectHoldAction(id: number, reason: string): Observable<any> {
return this.http.post<any>(`${this.base}/hold-actions/${id}/reject`, { reason });
}
/** Filters: status, type (ACK / RESPONSE), search, fromDate, toDate, offset, limit. */
outbox(query: I4cListParams = {}): Observable<I4cPage<any>> {
return this.http.get<I4cPage<any>>(`${this.base}/outbox`, { params: this.params(query) });
}
retryOutbox(id: number): Observable<any> {
return this.http.post<any>(`${this.base}/outbox/${id}/retry`, {});
}
/** Filters: direction, failures, httpStatus, responseCode, search, fromDate, toDate, offset, limit. */
apiLog(query: I4cListParams = {}): Observable<I4cPage<any>> {
return this.http.get<I4cPage<any>>(`${this.base}/api-log`, { params: this.params(query) });
}
apiLogEntry(id: number): Observable<any> {
return this.http.get<any>(`${this.base}/api-log/${id}`);
}
/** Send the hold notice now on the switched-on automatic channels. */
sendIntimation(holdId: number): Observable<IntimationResult> {
return this.http.post<IntimationResult>(`${this.base}/holds/${holdId}/intimation/send`, {});
}
intimationChannels(): Observable<IntimationSettings> {
return this.http.get<IntimationSettings>(`${this.base}/intimation-channels`);
}
updateIntimationChannel(
channel: string,
body: { enabled?: boolean; priority?: number; subject?: string; body?: string }
): Observable<IntimationChannel[]> {
return this.http.put<IntimationChannel[]>(`${this.base}/intimation-channels/${channel}`, body);
}
testIntimationChannel(channel: string, to: string): Observable<IntimationResult> {
return this.http.post<IntimationResult>(`${this.base}/intimation-channels/${channel}/test`, { to });
}
/** Filters: search, type, ifsc (KNOWN / UNKNOWN), offset, limit. */
bankMaster(query: I4cListParams = {}): Observable<I4cPage<any>> {
return this.http.get<I4cPage<any>>(`${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<I4cPage<any>> {
return this.http.get<I4cPage<any>>(`${this.cases}/grievances`, { params: this.params(query) });
}
grievance(id: number): Observable<any> {
return this.http.get<any>(`${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<string, any>): Observable<any> {
return this.http.post<any>(`${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<I4cPage<any>> {
return this.http.get<I4cPage<any>>(`${this.cases}/restorations`, { params: this.params(query) });
}
restoration(id: number): Observable<any> {
return this.http.get<any>(`${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<string, any>): Observable<any> {
return this.http.put<any>(`${this.cases}/restorations/${id}/request`, body);
}
/** step: bank-action, retry-outward, mrm-updated, receipt, victim-status */
restorationStep(id: number, step: string, body: Record<string, any> = {}): Observable<any> {
return this.http.post<any>(`${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<any> {
return this.http.get<any>(`${this.cases}/pro-rata`, { params: this.params({ accountNo, amount }) });
}
mrmBalanceReport(): Observable<any[]> {
return this.http.get<any[]>(`${this.cases}/mrm-balance-report`);
}
mrmBalanceReportCsv(): Observable<Blob> {
return this.http.get(`${this.cases}/mrm-balance-report.csv`, { responseType: 'blob' });
}
statementCsv(holdId: number, fromDate?: string, toDate?: string): Observable<Blob> {
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<string, any> = {}): Observable<any> {
return this.http.post<any>(`${this.cases}/${path}`, body);
}
casePut(path: string, body: Record<string, any> = {}): Observable<any> {
return this.http.put<any>(`${this.cases}/${path}`, body);
}
/** A paged register on /i4c/cases: mule-flags, fraud-reports, linked-alerts, channel-blocks. */
register(name: string, query: I4cListParams = {}): Observable<I4cPage<any>> {
return this.http.get<I4cPage<any>>(`${this.cases}/${name}`, { params: this.params(query) });
}
fraudReportsCsv(): Observable<Blob> {
return this.http.get(`${this.cases}/fraud-reports.csv`, { responseType: 'blob' });
}
documents(ownerType: I4cDocumentOwner, ownerId: number): Observable<I4cDocument[]> {
return this.http.get<I4cDocument[]>(`${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<Blob> {
return this.http.get(`${this.cases}/documents/${id}/content`, { responseType: 'blob' });
}
private params(values: Record<string, any>): 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'
);
}