- 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>
431 lines
15 KiB
TypeScript
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'
|
|
);
|
|
}
|