Created At
Complaint Ack No.
+ Layer
Type
Delivery Status
Attempts
@@ -121,6 +132,12 @@
{{ o.acknowledgement_no }}
+
+ {{ layerLabel(o.layer) }}
+
{{
o.message_type === 'ACK' ? 'ACK' : 'RESPONSE'
@@ -158,7 +175,7 @@
} @empty {
-
+
No responses in the outbox queue matching current filter.
@@ -235,6 +252,16 @@
placeholder="e.g. 00, 11"
/>
+
+ Layer
+
+ All layers
+ L0 · Victim
+ L1 · First layer
+ L2
+ L3
+
+
HTTP Status
Response Code
Ack No.
+ Layer
Latency (ms)
Caller IP
Error Details
@@ -290,6 +318,16 @@
{{ l.acknowledgement_no || '-' }}
+
+ @if (l.layer !== null && l.layer !== undefined) {
+ {{ layerLabel(l.layer) }}
+ } @else {
+ -
+ }
+
{{ l.duration_ms }} ms
{{ l.client_ip || '-' }}
@@ -298,7 +336,7 @@
} @empty {
-
+
No API calls recorded.
diff --git a/src/app/cfcfrms/i4c/i4c-outbox/i4c-outbox.component.scss b/src/app/cfcfrms/i4c/i4c-outbox/i4c-outbox.component.scss
index c79acb2f9..796aa3608 100644
--- a/src/app/cfcfrms/i4c/i4c-outbox/i4c-outbox.component.scss
+++ b/src/app/cfcfrms/i4c/i4c-outbox/i4c-outbox.component.scss
@@ -189,3 +189,30 @@
color: #64748b;
font-size: 12px;
}
+
+/* Layer of the complaint a row belongs to: victim (L0), first layer (L1), onward (L2+). */
+.layer-pill {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 9999px;
+ font-size: 10.5px;
+ font-weight: 700;
+ white-space: nowrap;
+ background: #f1f5f9;
+ color: #475569;
+
+ &--victim {
+ background: #eff6ff;
+ color: #1d4ed8;
+ }
+
+ &--first {
+ background: #fef3c7;
+ color: #b45309;
+ }
+
+ &--onward {
+ background: #f3e8ff;
+ color: #7e22ce;
+ }
+}
diff --git a/src/app/cfcfrms/i4c/i4c-outbox/i4c-outbox.component.ts b/src/app/cfcfrms/i4c/i4c-outbox/i4c-outbox.component.ts
index 69bf63e85..a421cc8ec 100644
--- a/src/app/cfcfrms/i4c/i4c-outbox/i4c-outbox.component.ts
+++ b/src/app/cfcfrms/i4c/i4c-outbox/i4c-outbox.component.ts
@@ -66,6 +66,7 @@ export class I4cOutboxComponent implements OnInit {
outboxStatus = '';
outboxType = '';
+ outboxLayer = '';
outboxSearch = '';
outboxDates: I4cDateRange = { ...ALL_DATES };
outboxTotal = 0;
@@ -78,6 +79,7 @@ export class I4cOutboxComponent implements OnInit {
logSearch = '';
logHttpStatus = '';
logResponseCode = '';
+ logLayer = '';
logDates: I4cDateRange = { ...ALL_DATES };
logTotal = 0;
logPageIndex = 0;
@@ -123,12 +125,19 @@ export class I4cOutboxComponent implements OnInit {
}
get outboxHasFilters(): boolean {
- return !!(this.outboxStatus || this.outboxType || this.outboxSearch || this.outboxDates.preset !== 'ALL');
+ return !!(
+ this.outboxStatus ||
+ this.outboxType ||
+ this.outboxLayer ||
+ this.outboxSearch ||
+ this.outboxDates.preset !== 'ALL'
+ );
}
clearOutboxFilters(): void {
this.outboxStatus = '';
this.outboxType = '';
+ this.outboxLayer = '';
this.outboxSearch = '';
this.outboxDates = { ...ALL_DATES };
this.loadOutbox(true);
@@ -152,6 +161,7 @@ export class I4cOutboxComponent implements OnInit {
this.logSearch ||
this.logHttpStatus ||
this.logResponseCode ||
+ this.logLayer ||
this.logDates.preset !== 'ALL'
);
}
@@ -162,6 +172,7 @@ export class I4cOutboxComponent implements OnInit {
this.logSearch = '';
this.logHttpStatus = '';
this.logResponseCode = '';
+ this.logLayer = '';
this.logDates = { ...ALL_DATES };
this.loadLog(true);
}
@@ -177,6 +188,7 @@ export class I4cOutboxComponent implements OnInit {
.outbox({
status: this.outboxStatus,
type: this.outboxType,
+ layer: this.outboxLayer,
search: this.outboxSearch.trim(),
fromDate: this.outboxDates.fromDate,
toDate: this.outboxDates.toDate,
@@ -207,6 +219,7 @@ export class I4cOutboxComponent implements OnInit {
search: this.logSearch.trim(),
httpStatus: this.logHttpStatus,
responseCode: this.logResponseCode.trim(),
+ layer: this.logLayer,
fromDate: this.logDates.fromDate,
toDate: this.logDates.toDate,
offset: this.logPageIndex * this.logPageSize,
@@ -300,6 +313,14 @@ export class I4cOutboxComponent implements OnInit {
}
}
+ /** Layer 0 is the victim's own account, 1 the first-layer account, 2+ onward accounts in the trail. */
+ layerLabel(layer: number | null | undefined): string {
+ if (layer === null || layer === undefined) {
+ return '-';
+ }
+ return layer === 0 ? 'L0 · Victim' : layer === 1 ? 'L1 · First layer' : 'L' + layer + ' · Onward';
+ }
+
ok(status: number | null): boolean {
return status !== null && status !== undefined && status >= 200 && status < 300;
}
diff --git a/src/app/cfcfrms/i4c/i4c-request-detail/i4c-request-detail.component.ts b/src/app/cfcfrms/i4c/i4c-request-detail/i4c-request-detail.component.ts
index 8ac0ee4f2..90ffe565b 100644
--- a/src/app/cfcfrms/i4c/i4c-request-detail/i4c-request-detail.component.ts
+++ b/src/app/cfcfrms/i4c/i4c-request-detail/i4c-request-detail.component.ts
@@ -130,6 +130,7 @@ export class I4cRequestDetailComponent implements OnInit {
return 'i4c-badge--green';
case 'PARTIALLY_HELD':
case 'PROSPECTIVE_HOLD':
+ case 'NOT_HELD_MOVED_ON':
return 'i4c-badge--amber';
case 'NOT_FOUND':
case 'NO_MATCH':
diff --git a/src/app/cfcfrms/i4c/i4c-requests/i4c-requests.component.html b/src/app/cfcfrms/i4c/i4c-requests/i4c-requests.component.html
index 345ee675e..6b74ec863 100644
--- a/src/app/cfcfrms/i4c/i4c-requests/i4c-requests.component.html
+++ b/src/app/cfcfrms/i4c/i4c-requests/i4c-requests.component.html
@@ -205,15 +205,16 @@
[class.modal-tab--active]="modalTab === 'incidents'"
(click)="modalTab = 'incidents'"
>
- Incidents ({{
- selectedComplaint.incidents?.length || selectedComplaint.incidents || 0
- }})
+ Incidents
+ {{ selectedComplaint.incidents?.length || selectedComplaint.incidents || 0 }}
- Section 6 Findings ({{ selectedComplaint.findings?.length || 0 }})
+ Section 6 Findings
+ {{ selectedComplaint.findings?.length || 0 }}
- Outbox Responses ({{ selectedComplaint.outbox?.length || 0 }})
+ Outbox Responses
+ {{ selectedComplaint.outbox?.length || 0 }}
Raw JSON
diff --git a/src/app/cfcfrms/i4c/i4c-restorations/i4c-restorations.component.html b/src/app/cfcfrms/i4c/i4c-restorations/i4c-restorations.component.html
index b2f6ce3a6..d023718d0 100644
--- a/src/app/cfcfrms/i4c/i4c-restorations/i4c-restorations.component.html
+++ b/src/app/cfcfrms/i4c/i4c-restorations/i4c-restorations.component.html
@@ -58,6 +58,10 @@
}
+ @if (role === 'BENEFICIARY_BANK') {
+ Past 15-day deadline
+ Contested
+ }
Clear filters
@@ -132,6 +136,19 @@
@if (r.bank_action && r.bank_action !== 'PENDING') {
{{ r.bank_action }}
}
+ @if (
+ r.compliance_due_on &&
+ r.role === 'BENEFICIARY_BANK' &&
+ r.status !== 'RESTORED' &&
+ r.status !== 'CLOSED'
+ ) {
+
+ Credit by {{ r.compliance_due_on | date: 'dd MMM' }}
+
+ }
+ @if (r.contested) {
+ CONTESTED
+ }
@if (r.transfer_error) {
{{ r.transfer_error }}
}
@@ -349,10 +366,14 @@
BNSS 106 notice no.
-
- Notice date
-
-
+
+
Indemnity bond received (Rs 10 stamp paper)
@@ -362,18 +383,18 @@
FIR no. (needed above ₹{{ r.firThreshold }})
-
- FIR date
-
-
+
Court order no.
-
- Court order date
-
-
+
Court
@@ -425,12 +446,18 @@
₹{{ r.restorable_amount | number: '1.2-2' }} goes to {{ r.victim_name }} ({{
r.victim_account_no
}}{{ r.victim_ifsc ? ', ' + r.victim_ifsc : ', this bank' }}) once a second officer approves.
- @if (r.victim_ifsc && !r.poolConfigured) {
+ @if (r.victim_ifsc && !r.outwardReady) {
No restoration pool account is configured, so an outward restoration cannot be
- approved. Map the I4C_RESTORATION_PAYABLE and FUNDTRANSFER_OUTWARD_SUSPENSE GLs (Organisation >
+ Clearing GL Configuration) before an outward restoration can be approved.
}
+ @if (r.compliance_due_on) {
+
+ SOP: credit the victim by {{ r.compliance_due_on | date: 'dd MMM yyyy' }} (15 days from
+ the notice).
+
+ }
} @else if (action.action === 'NOT_COMPLIED' || action.action === 'REFER_BACK_LEA') {
@@ -462,13 +489,12 @@
@if (r.status === 'IN_POOL') {
- ₹{{ r.restored_amount | number: '1.2-2' }} is in the restoration pool
+ ₹{{ r.restored_amount | number: '1.2-2' }} is held in the restoration payable
- It left the suspect account on approval. The outward {{ r.transfer_channel }} to
- {{ r.victim_account_no }} / {{ r.victim_ifsc }} has not gone through yet{{
- r.transfer_error ? ': ' + r.transfer_error : '.'
- }}
+ It left the suspect account on approval (Dr suspect, Cr I4C Restoration Payable). The outward
+ {{ r.transfer_channel }} to {{ r.victim_account_no }} / {{ r.victim_ifsc }} has not gone through
+ yet{{ r.transfer_error ? ': ' + r.transfer_error : '.' }}
@@ -503,8 +529,214 @@
}
+
+ @if ((r.status === 'RESTORED' || r.status === 'CLOSED') && r.restored_at) {
+
+
+ Suspect's challenge (SOP: within 15 days of the refund, until
+ {{ r.challenge_until | date: 'dd MMM yyyy' }})
+
+ @if (r.contested) {
+
+ Contested on {{ r.contested_on | date: 'dd MMM yyyy' }}: {{ r.contest_remarks }}. The
+ investigation continues; the bank and the account holder cooperate with the IO.
+
+ } @else {
+
+
+
+ Details
+
+
+
+
+ Record challenge
+
+ }
+
+ }
} @else {
@if (r.status !== 'CLOSED') {
+
+
+ Customer's claim on the bank (RBI customer protection in fraudulent EBTs)
+
+ @if (r.liability_category) {
+
+ {{
+ r.liability_regime === 'RBI_2026'
+ ? 'RBI 2026 directions (RBI/2026-27/172)'
+ : 'RBI 2017 framework'
+ }}
+ · {{ label(liabilityCategories, r.liability_category) }} · transaction
+ {{ r.txn_date | date: 'dd MMM yyyy' }} · reported
+ {{ r.customer_reported_on | date: 'dd MMM yyyy'
+ }}{{ r.cross_border ? ' · cross-border' : '' }}
+
+
{{ r.liability_note }}
+
+ @if (r.shadow_credit_due_on) {
+ Shadow reversal due
+ {{ r.shadow_credit_due_on | date: 'dd MMM yyyy' }}
+ ·
+ }
+ Resolve by
+ {{
+ r.resolution_due_on | date: 'dd MMM yyyy'
+ }}
+ ({{
+ r.liability_regime === 'RBI_2026'
+ ? (r.cross_border ? '60' : '45') + ' calendar days'
+ : '90 days'
+ }})
+
+ }
+
+
+
+
+
+ Caused by
+
+ @for (c of liabilityCategories; track c.value) {
+ {{ c.label }}
+ }
+
+
+
+ Gross loss (₹)
+
+
+
+ Recovered so far (₹)
+
+
+ Cross-border
+
+
+ Assess liability
+
+
+ @if (r.liability_category === 'CUSTOMER_NEGLIGENCE') {
+
+ Compensation (RBI 2026, para 65.1T):
+ @if (r.comp_eligible) {
+ ₹{{ r.comp_amount | number: '1.2-2' }} ({{ r.comp_reason }}) · RBI ₹{{
+ r.comp_share_rbi | number: '1.2-2'
+ }}
+ · this bank ₹{{ r.comp_share_own | number: '1.2-2' }} · beneficiary bank ₹{{
+ r.comp_share_beneficiary | number: '1.2-2'
+ }}
+ } @else {
+ not eligible: {{ r.comp_reason }}
+ }
+
+ @if (r.comp_eligible && !r.comp_status) {
+
+ Customer applied (pay within 5 days)
+
+ } @else if (r.comp_status === 'APPLIED') {
+
+ Applied {{ r.comp_applied_on | date: 'dd MMM yyyy' }}; pay by
+ {{ r.comp_due_on | date: 'dd MMM yyyy' }}.
+
+
+
+ Pay compensation (checker)
+
+
+ Reject
+
+
+ } @else if (r.comp_status) {
+
+ Compensation {{ r.comp_status }} {{ r.comp_paid_on | date: 'dd MMM yyyy' }}
+
+ }
+ }
+
+ @if (r.liability_category && r.liability_category !== 'CUSTOMER_NEGLIGENCE') {
+ @if (!r.shadow_credit_status || r.shadow_credit_status === 'REJECTED') {
+
+
+ Shadow reversal (₹)
+
+
+
+ Request shadow reversal (maker)
+
+
+
+ A provisional credit the customer cannot use until the case is decided (a lien is placed on
+ it).
+
+ } @else if (r.shadow_credit_status === 'PENDING') {
+
+ Shadow reversal of ₹{{ r.shadow_credit_amount | number: '1.2-2' }} requested by
+ {{ r.shadow_credit_requested_by }}; a different officer approves it.
+
+
+
+ Approve and credit (checker)
+
+
+ Reject
+
+
+ } @else {
+
+ Shadow reversal ₹{{ r.shadow_credit_amount | number: '1.2-2' }} posted
+ {{ r.shadow_credited_at | date: 'dd MMM yyyy' }} by {{ r.shadow_credited_by }}
+ {{
+ r.shadow_finalised_at
+ ? '· final ' + (r.shadow_finalised_at | date: 'dd MMM yyyy')
+ : '· provisional (lien on it)'
+ }}
+ @if (r.regularised_at) {
+ · regularised ₹{{ r.regularised_amount | number: '1.2-2' }} on
+ {{ r.regularised_at | date: 'dd MMM yyyy' }}
+ }
+
+
+ @if (!r.shadow_finalised_at && !r.regularised_at) {
+
+ Zero liability established: make final
+
+ }
+ @if (!r.regularised_at && r.received_amount) {
+
+ Recover it from the restored money
+
+ }
+
+ }
+ }
+
Restored money received for our customer
@@ -516,10 +748,10 @@
UTR / transaction id *
-
- Credited on *
-
-
+
Credited to account
diff --git a/src/app/cfcfrms/i4c/i4c-restorations/i4c-restorations.component.ts b/src/app/cfcfrms/i4c/i4c-restorations/i4c-restorations.component.ts
index f60029726..e6e433575 100644
--- a/src/app/cfcfrms/i4c/i4c-restorations/i4c-restorations.component.ts
+++ b/src/app/cfcfrms/i4c/i4c-restorations/i4c-restorations.component.ts
@@ -20,6 +20,7 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSnackBar } from '@angular/material/snack-bar';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { I4cService, i4cErrorMessage } from '../i4c.service';
+import { I4cDateFieldComponent } from '../i4c-date-field/i4c-date-field.component';
import { ALL_DATES, I4cDateRange, I4cDateRangeComponent } from '../i4c-date-range/i4c-date-range.component';
import { I4cDocumentsComponent } from '../i4c-documents/i4c-documents.component';
@@ -35,7 +36,7 @@ export const MRM_STATUSES: { value: string; label: string }[] = [
{ value: 'ASSESSED', label: 'Assessed' },
{ value: 'REQUESTED', label: 'Victim request received' },
{ value: 'AWAITING_APPROVAL', label: 'Awaiting checker' },
- { value: 'IN_POOL', label: 'In restoration pool' },
+ { value: 'IN_POOL', label: 'Held in restoration payable' },
{ value: 'TRANSFER_PENDING', label: 'Outward transfer pending' },
{ value: 'RESTORED', label: 'Restored' },
{ value: 'AWAITED', label: 'Victim bank: awaited' },
@@ -89,7 +90,8 @@ const VICTIM_STATUSES = [
MatPaginatorModule,
FaIconComponent,
I4cDateRangeComponent,
- I4cDocumentsComponent
+ I4cDocumentsComponent,
+ I4cDateFieldComponent
]
})
export class I4cRestorationsComponent implements OnInit {
@@ -112,6 +114,8 @@ export class I4cRestorationsComponent implements OnInit {
role = 'BENEFICIARY_BANK';
status = 'OPEN';
bankAction = '';
+ overdueOnly = false;
+ contestedOnly = false;
search = '';
dates: I4cDateRange = { ...ALL_DATES };
rows: any[] = [];
@@ -151,6 +155,8 @@ export class I4cRestorationsComponent implements OnInit {
role: this.role,
status: this.status,
bankAction: this.bankAction,
+ overdue: this.overdueOnly || null,
+ contested: this.contestedOnly || null,
search: this.search.trim(),
fromDate: this.dates.fromDate,
toDate: this.dates.toDate,
@@ -187,12 +193,21 @@ export class I4cRestorationsComponent implements OnInit {
}
get hasFilters(): boolean {
- return !!(this.status !== 'OPEN' || this.bankAction || this.search || this.dates.preset !== 'ALL');
+ return !!(
+ this.status !== 'OPEN' ||
+ this.bankAction ||
+ this.overdueOnly ||
+ this.contestedOnly ||
+ this.search ||
+ this.dates.preset !== 'ALL'
+ );
}
clearFilters(): void {
this.status = 'OPEN';
this.bankAction = '';
+ this.overdueOnly = false;
+ this.contestedOnly = false;
this.search = '';
this.dates = { ...ALL_DATES };
this.load(true);
@@ -242,6 +257,7 @@ export class I4cRestorationsComponent implements OnInit {
courtName: r.court_name || '',
bnssNoticeNo: r.bnss_notice_no || '',
bnssNoticeDate: r.bnss_notice_date || '',
+ noticeReceivedOn: r.notice_received_on || '',
indemnityBondReceived: !!r.indemnity_bond_received
};
this.action = { action: 'COMPLIED', reason: '' };
@@ -362,6 +378,83 @@ export class I4cRestorationsComponent implements OnInit {
);
}
+ readonly liabilityCategories = [
+ { value: 'BANK_NEGLIGENCE', label: 'Bank negligence / deficiency' },
+ { value: 'THIRD_PARTY_BREACH', label: 'Third-party breach (neither bank nor customer)' },
+ { value: 'CUSTOMER_NEGLIGENCE', label: 'Customer negligence (shared OTP / PIN, coerced)' }
+ ];
+ contest = { contestedOn: '', remarks: '' };
+ liability = {
+ txnDate: '',
+ reportedOn: '',
+ ncrpReportedOn: '',
+ category: 'THIRD_PARTY_BREACH',
+ crossBorder: false,
+ grossLoss: null as number | null,
+ recoveredAmount: null as number | null
+ };
+ shadowAmount: number | null = null;
+
+ isPast(date: string | null | undefined): boolean {
+ return !!date && new Date(date).getTime() < Date.now();
+ }
+
+ saveContest(): void {
+ this.run(
+ this.i4c.restorationStep(this.selected.id, 'contest', this.contest),
+ 'Challenge recorded; the investigation continues.'
+ );
+ }
+
+ saveLiability(): void {
+ this.run(
+ this.i4c.restorationStep(this.selected.id, 'liability', this.liability),
+ 'Liability assessed under the RBI framework for that date.'
+ );
+ }
+
+ requestShadowCredit(): void {
+ this.run(
+ this.i4c.restorationStep(this.selected.id, 'shadow-credit', { amount: this.shadowAmount }),
+ 'Shadow credit sent for approval.'
+ );
+ }
+
+ decideShadowCredit(approve: boolean): void {
+ this.run(
+ this.i4c.restorationStep(this.selected.id, 'shadow-credit/decision', { approve }),
+ approve ? 'Shadow credit posted to the customer.' : 'Shadow credit rejected.'
+ );
+ }
+
+ finaliseShadow(): void {
+ this.run(
+ this.i4c.restorationStep(this.selected.id, 'shadow-credit/finalise'),
+ 'Shadow reversal made final; the customer can use it.'
+ );
+ }
+
+ applyCompensation(): void {
+ this.run(
+ this.i4c.restorationStep(this.selected.id, 'compensation/apply'),
+ 'Application recorded; pay within 5 calendar days.'
+ );
+ }
+
+ payCompensation(approve: boolean): void {
+ this.run(
+ this.i4c.restorationStep(this.selected.id, 'compensation/decision', { approve }),
+ approve ? 'Compensation credited to the customer.' : 'Compensation rejected.'
+ );
+ }
+
+ regularise(): void {
+ this.run(
+ this.i4c.restorationStep(this.selected.id, 'regularise'),
+ 'Shadow credit recovered from the restored money.'
+ );
+ }
+
loadProRata(): void {
this.i4c.proRata(this.selected.hold_account_no).subscribe({
next: (p) => {
diff --git a/src/app/cfcfrms/i4c/i4c-risk/i4c-risk.component.html b/src/app/cfcfrms/i4c/i4c-risk/i4c-risk.component.html
new file mode 100644
index 000000000..faba4c4e1
--- /dev/null
+++ b/src/app/cfcfrms/i4c/i4c-risk/i4c-risk.component.html
@@ -0,0 +1,616 @@
+
+
+
+
+
+
+
+ Account / customer / reference
+
+
+
+ Status
+
+ Any
+ @for (s of statuses; track s) {
+ {{ s }}
+ }
+
+
+ @if (tab === 'fraud-reports') {
+ FMR / STR overdue
+ }
+ @if (tab === 'bank-holds') {
+ Past a deadline
+
+ Place a hold
+
+ }
+
+
+ Clear filters
+
+
+
+ @if (loading) {
+
+ }
+ @if (error) {
+
+ {{ error }}
+
+ }
+
+ @if (tab === 'exclusions') {
+
+
+
+
+ Account no.
+
+
+
+ Kind
+
+ Nodal account
+ Pool account
+ Escrow account
+
+
+
+ What it is (merchant nodal, aggregator pool...)
+
+
+
+
+ Exclude from I4C holds
+
+
+ MHA SOP: holds are for first-layer mule accounts, not nodal, pool or escrow accounts. A complaint against an
+ excluded account is answered "hold not placed" and traced through the merchant settlement.
+
+
+
+ }
+ @if (tab === 'bank-holds' && showPlace) {
+
+
Hold a suspected mule transaction
+
+
+ }
+
+
+
+
+ @switch (tab) {
+ @case ('mule-flags') {
+ Mule-account register
+ Accounts in two or more complaints are flagged automatically; review each with EDD or the LEA.
+ }
+ @case ('fraud-reports') {
+ Fraud reporting
+ Police / LEA, RBI fraud monitoring return (14 days), FIU-IND STR (7 days) from classification.
+ }
+ @case ('linked-alerts') {
+ Fraud money that moved on inside this bank
+ Loan repayments (SOP 9.1(v): verify, hold, tell the linked account) and transfers to another account here
+ (the next layer is with us). Found by the I4C Hold Review job.
+ }
+ @case ('exclusions') {
+ Nodal / pool / escrow accounts
+ Disputed amount is held; no prospective hold on future credits and normally no suspension (MHA SOP
+ 9.1(iii)).
+ }
+ @case ('bank-holds') {
+ Bank-initiated debit holds
+ Suspected mule transactions the bank holds itself (RBI draft mule SOP).
+ }
+ @case ('channel-blocks') {
+ Digital-channel suspensions
+ Placed and lifted through hold requests (maker-checker). CBS fund transfers refuse them.
+ }
+ }
+
+
+
+
+
diff --git a/src/app/cfcfrms/i4c/i4c-risk/i4c-risk.component.scss b/src/app/cfcfrms/i4c/i4c-risk/i4c-risk.component.scss
new file mode 100644
index 000000000..e297f2fec
--- /dev/null
+++ b/src/app/cfcfrms/i4c/i4c-risk/i4c-risk.component.scss
@@ -0,0 +1,21 @@
+/**
+ * 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/.
+ */
+
+@use '../i4c' as *;
+@use '../i4c-cases' as *;
+
+@include i4c-skin;
+@include i4c-case-page;
+
+.err-text {
+ color: #dc2626;
+}
+
+.wide {
+ flex: 1 1 280px;
+}
diff --git a/src/app/cfcfrms/i4c/i4c-risk/i4c-risk.component.ts b/src/app/cfcfrms/i4c/i4c-risk/i4c-risk.component.ts
new file mode 100644
index 000000000..f36c246d2
--- /dev/null
+++ b/src/app/cfcfrms/i4c/i4c-risk/i4c-risk.component.ts
@@ -0,0 +1,313 @@
+/**
+ * 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 { Component, OnInit, inject } from '@angular/core';
+import { DatePipe, DecimalPipe } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatSelectModule } from '@angular/material/select';
+import { MatProgressBarModule } from '@angular/material/progress-bar';
+import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { I4cService, i4cErrorMessage, saveBlob } from '../i4c.service';
+import { I4cDateFieldComponent } from '../i4c-date-field/i4c-date-field.component';
+import { ALL_DATES, I4cDateRange, I4cDateRangeComponent } from '../i4c-date-range/i4c-date-range.component';
+
+type Register = 'mule-flags' | 'fraud-reports' | 'linked-alerts' | 'channel-blocks' | 'exclusions' | 'bank-holds';
+
+/**
+ * The bank's own duties around suspect accounts (MHA SOP, RBI fraud and AML rules): the mule-account register, fraud
+ * reporting to police / RBI (FMR) / FIU-IND (STR) with due dates, fraud money that repaid loans, and digital-channel
+ * suspensions.
+ */
+@Component({
+ selector: 'mifosx-i4c-risk',
+ templateUrl: './i4c-risk.component.html',
+ styleUrls: ['./i4c-risk.component.scss'],
+ imports: [
+ DatePipe,
+ DecimalPipe,
+ FormsModule,
+ MatButtonModule,
+ MatCheckboxModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatSelectModule,
+ MatProgressBarModule,
+ MatPaginatorModule,
+ FaIconComponent,
+ I4cDateRangeComponent,
+ I4cDateFieldComponent
+ ]
+})
+export class I4cRiskComponent implements OnInit {
+ private i4c = inject(I4cService);
+ private snackBar = inject(MatSnackBar);
+
+ readonly tabs: { value: Register; label: string; icon: string; statuses: string[] }[] = [
+ { value: 'mule-flags', label: 'Mule register', icon: 'user-shield', statuses: [
+ 'SUSPECTED',
+ 'CONFIRMED',
+ 'CLEARED'
+ ] },
+ { value: 'fraud-reports', label: 'Fraud reporting (FMR / STR)', icon: 'file-alt', statuses: [
+ 'SHOW_CAUSE',
+ 'OPEN',
+ 'CLOSED'
+ ] },
+ { value: 'linked-alerts', label: 'Loan repayments', icon: 'exchange-alt', statuses: [
+ 'OPEN',
+ 'ACTIONED',
+ 'DISMISSED'
+ ] },
+ { value: 'channel-blocks', label: 'Digital suspensions', icon: 'ban', statuses: [
+ 'ACTIVE',
+ 'LIFTED'
+ ] },
+ { value: 'bank-holds', label: 'Bank-initiated holds (RBI draft)', icon: 'lock', statuses: [
+ 'OPEN',
+ 'HELD',
+ 'EXPLAINED',
+ 'REFERRED',
+ 'CONTINUED',
+ 'LIFTED'
+ ] },
+ { value: 'exclusions', label: 'Nodal / pool / escrow', icon: 'university', statuses: [
+ 'ACTIVE',
+ 'REMOVED'
+ ] }
+ ];
+ exclusionForm = { accountNo: '', kind: 'NODAL', reason: '' };
+ bankHoldForm = {
+ accountNo: '',
+ scope: 'TRANSACTION',
+ trigger: 'PROFILE_MISMATCH',
+ txnRef: '',
+ amount: null as number | null,
+ reason: ''
+ };
+ showPlace = false;
+ step = '';
+ readonly bankSteps: { value: string; label: string }[] = [
+ { value: 'notice', label: 'Customer told (by hand)' },
+ { value: 'explanation', label: "Customer's explanation" },
+ { value: 'decision', label: 'Decision: lift or refer to police' },
+ { value: 'police', label: "Police's instruction" },
+ { value: 'grievance', label: 'Customer grievance' },
+ { value: 'grievance-resolution', label: 'Grievance resolved' }
+ ];
+ readonly pageSizes = [
+ 10,
+ 20,
+ 50,
+ 100
+ ];
+
+ tab: Register = 'mule-flags';
+ status = '';
+ overdueOnly = false;
+ search = '';
+ dates: I4cDateRange = { ...ALL_DATES };
+ rows: any[] = [];
+ total = 0;
+ pageIndex = 0;
+ pageSize = 20;
+ loading = false;
+ busy = false;
+ error = '';
+
+ editing: any = null;
+ form: Record = {};
+
+ ngOnInit(): void {
+ this.load();
+ }
+
+ get statuses(): string[] {
+ return this.tabs.find((t) => t.value === this.tab)?.statuses || [];
+ }
+
+ setTab(t: Register): void {
+ this.tab = t;
+ this.status = '';
+ this.overdueOnly = false;
+ this.editing = null;
+ this.load(true);
+ }
+
+ load(fromFirstPage = false): void {
+ if (fromFirstPage) {
+ this.pageIndex = 0;
+ }
+ this.loading = true;
+ this.error = '';
+ this.i4c
+ .register(this.tab, {
+ status: this.status,
+ overdue: (this.tab === 'fraud-reports' && this.overdueOnly) || null,
+ due: (this.tab === 'bank-holds' && this.overdueOnly) || null,
+ search: this.search.trim(),
+ fromDate: this.dates.fromDate,
+ toDate: this.dates.toDate,
+ offset: this.pageIndex * this.pageSize,
+ limit: this.pageSize
+ })
+ .subscribe({
+ next: (page) => {
+ this.rows = page.pageItems;
+ this.total = page.totalFilteredRecords;
+ this.loading = false;
+ },
+ error: (e) => {
+ this.error = i4cErrorMessage(e);
+ this.loading = false;
+ }
+ });
+ }
+
+ onPage(e: PageEvent): void {
+ this.pageIndex = e.pageIndex;
+ this.pageSize = e.pageSize;
+ this.load();
+ }
+
+ onDates(range: I4cDateRange): void {
+ this.dates = range;
+ this.load(true);
+ }
+
+ get hasFilters(): boolean {
+ return !!(this.status || this.overdueOnly || this.search || this.dates.preset !== 'ALL');
+ }
+
+ clearFilters(): void {
+ this.status = '';
+ this.overdueOnly = false;
+ this.search = '';
+ this.dates = { ...ALL_DATES };
+ this.load(true);
+ }
+
+ isPast(date: string | null | undefined): boolean {
+ return !!date && new Date(date).getTime() < Date.now();
+ }
+
+ edit(row: any): void {
+ this.editing = row;
+ if (this.tab === 'mule-flags') {
+ this.form = { outcome: 'CONFIRMED', remarks: '' };
+ } else if (this.tab === 'fraud-reports') {
+ this.form = {
+ policeRef: row.police_ref || '',
+ policeReportedOn: row.police_reported_on || '',
+ fmrRef: row.fmr_ref || '',
+ fmrFiledOn: row.fmr_filed_on || '',
+ strRef: row.str_ref || '',
+ strFiledOn: row.str_filed_on || '',
+ remarks: row.remarks || ''
+ };
+ } else {
+ this.form = { outcome: 'ACTIONED', remarks: '' };
+ }
+ }
+
+ save(): void {
+ const row = this.editing;
+ const call =
+ this.tab === 'mule-flags'
+ ? this.i4c.casePost('mule-flags/' + row.id + '/review', this.form)
+ : this.tab === 'fraud-reports'
+ ? this.i4c.casePut('fraud-reports/' + row.id, this.form)
+ : this.i4c.casePost('linked-alerts/' + row.id, this.form);
+ this.busy = true;
+ call.subscribe({
+ next: () => {
+ this.busy = false;
+ this.editing = null;
+ this.snackBar.open('Saved.', 'OK', { duration: 3000 });
+ this.load();
+ },
+ error: (e) => {
+ this.busy = false;
+ this.snackBar.open(i4cErrorMessage(e), 'Close', { duration: 7000 });
+ }
+ });
+ }
+
+ private post(path: string, body: Record, done: string, after?: () => void): void {
+ this.busy = true;
+ this.i4c.casePost(path, body).subscribe({
+ next: () => {
+ this.busy = false;
+ this.snackBar.open(done, 'OK', { duration: 4000 });
+ after?.();
+ this.load();
+ },
+ error: (e) => {
+ this.busy = false;
+ this.snackBar.open(i4cErrorMessage(e), 'Close', { duration: 7000 });
+ }
+ });
+ }
+
+ addExclusion(): void {
+ this.post('exclusions', this.exclusionForm, 'Account excluded from I4C holds.', () => {
+ this.exclusionForm = { accountNo: '', kind: 'NODAL', reason: '' };
+ });
+ }
+
+ removeExclusion(row: any): void {
+ this.post(
+ 'exclusions/' + row.id + '/remove',
+ {},
+ 'Exclusion removed; I4C complaints will hold this account again.'
+ );
+ }
+
+ placeBankHold(): void {
+ this.post('bank-holds', this.bankHoldForm, 'Hold placed; the customer is told on the automatic channels.', () => {
+ this.showPlace = false;
+ this.bankHoldForm = {
+ accountNo: '',
+ scope: 'TRANSACTION',
+ trigger: 'PROFILE_MISMATCH',
+ txnRef: '',
+ amount: null,
+ reason: ''
+ };
+ });
+ }
+
+ openStep(row: any): void {
+ this.editing = row;
+ this.step =
+ row.status === 'HELD'
+ ? 'explanation'
+ : row.status === 'EXPLAINED'
+ ? 'decision'
+ : row.status === 'REFERRED'
+ ? 'police'
+ : 'grievance';
+ this.form = {};
+ }
+
+ saveStep(): void {
+ this.post('bank-holds/' + this.editing.id + '/' + this.step, this.form, 'Recorded.', () => (this.editing = null));
+ }
+
+ downloadFraudCsv(): void {
+ this.i4c.fraudReportsCsv().subscribe({
+ next: (blob) => saveBlob(blob, 'i4c-fraud-reports-' + new Date().toISOString().slice(0, 10) + '.csv'),
+ error: (e) => this.snackBar.open(i4cErrorMessage(e), 'Close', { duration: 6000 })
+ });
+ }
+}
diff --git a/src/app/cfcfrms/i4c/i4c-shell/i4c-shell.component.scss b/src/app/cfcfrms/i4c/i4c-shell/i4c-shell.component.scss
index 3878f045f..6c03296de 100644
--- a/src/app/cfcfrms/i4c/i4c-shell/i4c-shell.component.scss
+++ b/src/app/cfcfrms/i4c/i4c-shell/i4c-shell.component.scss
@@ -124,23 +124,23 @@
border: 1px solid #e2e8f0;
border-radius: 12px;
box-shadow: 0 2px 8px rgb(15 23 42 / 3%);
- overflow-x: auto;
+ /* No horizontal scroll: tabs wrap onto a second row when the screen is narrow. */
&__links {
display: flex;
- flex-wrap: nowrap;
+ flex-wrap: wrap;
align-items: center;
- gap: 4px;
+ gap: 2px;
width: 100%;
}
&__link {
display: inline-flex;
align-items: center;
- gap: 6px;
- padding: 6px 12px;
+ gap: 5px;
+ padding: 5px 9px;
border-radius: 8px;
- font-size: 11.5px;
+ font-size: 11px;
font-weight: 600;
color: #475569;
text-decoration: none;
@@ -162,7 +162,8 @@
}
}
- &--active {
+ &--active,
+ &--active:hover {
background: #2563eb;
color: #fff !important;
box-shadow: 0 3px 10px rgb(37 99 235 / 28%);
diff --git a/src/app/cfcfrms/i4c/i4c-shell/i4c-shell.component.ts b/src/app/cfcfrms/i4c/i4c-shell/i4c-shell.component.ts
index 3920727dd..1868049da 100644
--- a/src/app/cfcfrms/i4c/i4c-shell/i4c-shell.component.ts
+++ b/src/app/cfcfrms/i4c/i4c-shell/i4c-shell.component.ts
@@ -43,6 +43,7 @@ export class I4cShellComponent implements OnInit {
{ path: 'grievances', label: 'GRM Grievances', icon: 'balance-scale' },
{ path: 'restorations', label: 'MRM Restorations', icon: 'hand-holding-usd' },
{ path: 'mrm-report', label: 'MRM Balance Report', icon: 'file-csv' },
+ { path: 'risk', label: 'Risk Registers', icon: 'user-shield' },
{ path: 'outbox', label: 'Responses & API Log', icon: 'paper-plane' },
{ path: 'bank-master', label: 'Bank Code Master', icon: 'university' },
{ path: 'intimation-settings', label: 'Intimation Settings', icon: 'cog' }
diff --git a/src/app/cfcfrms/i4c/i4c.service.ts b/src/app/cfcfrms/i4c/i4c.service.ts
index 400d6f518..126c2f1c3 100644
--- a/src/app/cfcfrms/i4c/i4c.service.ts
+++ b/src/app/cfcfrms/i4c/i4c.service.ts
@@ -50,6 +50,21 @@ export interface I4cHold {
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;
}
@@ -141,7 +156,7 @@ export interface I4cPage {
export type I4cListParams = Record;
export interface HoldActionRequest {
- action: 'RELEASE' | 'FREEZE' | 'UNFREEZE' | 'UNFREEZE_AND_RELEASE';
+ 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;
@@ -259,6 +274,19 @@ export class I4cService {
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> {
@@ -335,6 +363,24 @@ export class I4cService {
});
}
+ /** 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 }) });
}