feat(cfcfrms): I4C Live screens for the I4C-MHA integration
New CFCFRMS > I4C Live section backed by /v1/i4c/admin and /v1/i4c/cases (the demo mock interceptor does not match these paths): - Dashboard, complaints from I4C, holds register, approvals, responses and API log, bank code master, intimation settings. - Hold popup: overview, hold transactions, customer intimation (automatic and manual), maker request (full / partial release, freeze, unfreeze), GRM / MRM and documents, request history. - GRM grievances, MRM restorations and MRM balance report pages; shared date range filter (today, yesterday, week, month, custom), server-side paging and filters on every list; document upload / download. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* 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;
|
||||
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';
|
||||
/** 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) });
|
||||
}
|
||||
|
||||
// ── 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'
|
||||
});
|
||||
}
|
||||
|
||||
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'
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user