risk review report with on which aml rule

This commit is contained in:
2026-08-02 21:40:34 +05:30
parent ebe67cc410
commit b43ed87771
13 changed files with 5098 additions and 327 deletions
+298 -36
View File
@@ -15,8 +15,10 @@ import { AuthenticationService } from 'app/core/authentication/authentication.se
import {
BRANCHES,
BranchMaster,
CardTransaction,
CtrTransaction,
CTR_TXNS,
TC1_TXNS,
FIU_BANK,
FIU_BRANCH_CODE_TO_CANONICAL,
KC1_INDIVIDUALS,
@@ -29,6 +31,90 @@ import {
STR_TXNS
} from './fiu-data';
/** A FINnet file rendered as a grid — the same rows that go into the CSV. */
export interface FinnetTable {
headers: string[];
rows: string[][];
}
export interface FinnetFile {
key: string;
/** File name without the extension, as FIU-IND names it. */
name: string;
/** Position in the prescribed submission order. */
step: number;
title: string;
purpose: string;
table: FinnetTable;
}
/**
* The five reports a reporting entity owes FIU-IND under the PMLA, with the
* trigger for each. All of them are filed electronically through FINnet 2.0.
*/
export interface FiuObligation {
code: string;
name: string;
trigger: string;
deadline: string;
icon: string;
/** Whether this module currently files the report or only tracks the rule. */
handled: boolean;
note: string;
}
export const FIU_OBLIGATIONS: FiuObligation[] = [
{
code: 'CTR',
name: 'Cash Transaction Report',
trigger:
'Cash transactions exceeding ₹10,00,000 in a calendar month — a single transaction or a series of connected ones.',
deadline: 'By the 15th of the following month',
icon: 'coins',
handled: true,
note: 'Filed from the CTR tab as the six-file FINnet bundle.'
},
{
code: 'STR',
name: 'Suspicious Transaction Report',
trigger:
'Any transaction that gives rise to a reasonable ground of suspicion of money laundering or a related offence — regardless of amount, and whether or not it was completed.',
deadline: 'Within 7 working days of forming the suspicion',
icon: 'exclamation-triangle',
handled: true,
note: 'Filed from the STR tab as FINnet STR XML.'
},
{
code: 'CBWTR',
name: 'Cross-Border Wire Transfer Report',
trigger:
'Cross-border wire transfers exceeding ₹5,00,000, where the origin or destination of the funds is outside India.',
deadline: 'By the 15th of the following month',
icon: 'globe',
handled: false,
note: 'Not applicable — this bank holds no AD licence and books no cross-border wires.'
},
{
code: 'CCR',
name: 'Counterfeit Currency Report',
trigger:
'Any forged or counterfeit currency note detected, at the counter or in a remittance. No monetary threshold applies.',
deadline: 'By the 15th of the following month',
icon: 'money-bill-wave',
handled: false,
note: 'Raised from the branch counterfeit-note register; not yet wired into this module.'
},
{
code: 'NTR',
name: 'Non-Profit Organisation Transaction Report',
trigger: 'Receipts by a non-profit organisation exceeding ₹10,00,000, or its equivalent in foreign currency.',
deadline: 'By the 15th of the following month',
icon: 'hand-holding-heart',
handled: false,
note: 'Depends on NPO flagging in the customer master; not yet wired into this module.'
}
];
@Component({
selector: 'mifosx-fiu',
templateUrl: './fiu.component.html',
@@ -48,8 +134,12 @@ export class FiuComponent implements OnInit {
/** Active tab */
activeTab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files' = 'dashboard';
/** The five PMLA reporting obligations, shown on the FIU landing tab. */
readonly obligations = FIU_OBLIGATIONS;
bank = FIU_BANK;
ctrTxns: CtrTransaction[] = CTR_TXNS;
tc1Txns: CardTransaction[] = TC1_TXNS;
strTxns: StrTransaction[] = STR_TXNS;
kc1: KycIndividual[] = KC1_INDIVIDUALS;
kc2: KycEntity[] = KC2_ENTITIES;
@@ -748,6 +838,17 @@ export class FiuComponent implements OnInit {
});
}
/** ATM / card legs under the same scope controls, so TC1 and TC2 stay in step. */
get filteredTc1(): CardTransaction[] {
const acc = this.ctrFilterAccount.trim();
return this.tc1Txns.filter((c) => {
if (this.ctrFilterMode && c.depositOrWithdrawal !== this.ctrFilterMode) return false;
if (acc && !c.accountNumber.includes(acc)) return false;
if (this.ctrFilterMinAmount > 0 && c.txnAmount < this.ctrFilterMinAmount) return false;
return true;
});
}
get filteredStr(): StrTransaction[] {
return this.scopedStrTxns.filter((s) => {
if (this.strFilterStatus && s.status !== this.strFilterStatus) return false;
@@ -797,9 +898,14 @@ export class FiuComponent implements OnInit {
return map[c];
}
// -------------------- Exports --------------------
/** TC2 — Branch / cash transaction CSV */
exportTc2(): void {
// -------------------- FINnet file builders --------------------
/**
* Each FINnet file is built once, here, and used for both the on-screen
* preview and the CSV download. Keeping a single builder per file is what
* guarantees the branch sees on screen exactly what is submitted — the two
* used to be written out separately, which let them drift.
*/
private buildTc2(): FinnetTable {
const headers = [
'Report Type',
'Report Reference Number',
@@ -818,7 +924,7 @@ export class FiuComponent implements OnInit {
'Third Party PAN (Non-Customer)',
'Declaration (If Third Party PAN (Non-Customer) is not available)'
];
const rows = [headers];
const rows: string[][] = [];
for (const t of this.filteredCtr) {
rows.push([
t.reportType,
@@ -839,12 +945,11 @@ export class FiuComponent implements OnInit {
t.declaration
]);
}
this.saveCsv(rows, 'TC2.csv');
this.toast(`TC2 exported (${this.filteredCtr.length} rows)`);
return { headers, rows };
}
/** TC1 header-only template (UI doesn't track ATM/card txns) */
exportTc1(): void {
/** TC1 — ATM / card transactions. Header-only: this bank books no card txns. */
private buildTc1(): FinnetTable {
const headers = [
'Relationship Flag',
'Transaction Status',
@@ -865,12 +970,34 @@ export class FiuComponent implements OnInit {
'Branch Code of Account',
'Report Type'
];
this.saveCsv([headers], 'TC1.csv');
this.toast('TC1 template exported (header only)');
const rows: string[][] = [];
for (const c of this.filteredTc1) {
rows.push([
c.relationshipFlag,
c.txnStatus,
c.txnId.toString(),
c.txnDate.replace(/-/g, '/'),
c.txnTime,
c.depositOrWithdrawal,
c.txnAmount.toString(),
c.accountNumber,
c.accountType,
c.cardType,
c.cardNumber,
c.cardIssuingCountry,
c.cardIssuingBank,
c.reportRefNo.toString(),
c.switchTxnId,
c.atmId,
c.branchCode,
c.reportType
]);
}
return { headers, rows };
}
/** KC1 — Individual customers */
exportKc1(): void {
private buildKc1(): FinnetTable {
const headers = [
'Report Reference Number',
'UCIC',
@@ -917,7 +1044,7 @@ export class FiuComponent implements OnInit {
'Other Identifier Number',
'Primary Entity'
];
const rows = [headers];
const rows: string[][] = [];
for (const k of this.kc1) {
rows.push([
k.reportRefNo.toString(),
@@ -966,12 +1093,11 @@ export class FiuComponent implements OnInit {
'Yes'
]);
}
this.saveCsv(rows, 'KC1.csv');
this.toast(`KC1 exported (${this.kc1.length} individuals)`);
return { headers, rows };
}
/** KC2 — Entity customers */
exportKc2(): void {
private buildKc2(): FinnetTable {
const headers = [
'Report Reference Number',
'UCIC',
@@ -1006,7 +1132,7 @@ export class FiuComponent implements OnInit {
'UCIC of Owner of Sole Proprietor Firm',
'Name of Owner of Sole Proprietor Firm'
];
const rows = [headers];
const rows: string[][] = [];
for (const k of this.kc2) {
rows.push([
k.reportRefNo.toString(),
@@ -1043,12 +1169,11 @@ export class FiuComponent implements OnInit {
k.ownerName
]);
}
this.saveCsv(rows, 'KC2.csv');
this.toast(`KC2 exported (${this.kc2.length} entities)`);
return { headers, rows };
}
/** Account_Detail.csv — per-report account aggregates (12-month) */
exportAccountDetail(): void {
/** Account_Detail — per-report account aggregates (12-month) */
private buildAccountDetail(): FinnetTable {
const headers = [
'Report Type',
'Report Reference Number',
@@ -1067,7 +1192,7 @@ export class FiuComponent implements OnInit {
'Total Cash Deposit (In last 12 months)',
'Total Cash Withdrawal (In last 12 months) Amount'
];
const rows = [headers];
const rows: string[][] = [];
// Build one aggregate row per (reportRefNo, accountNumber) pair
const seen = new Set<string>();
for (const t of this.filteredCtr) {
@@ -1105,12 +1230,11 @@ export class FiuComponent implements OnInit {
.toString()
]);
}
this.saveCsv(rows, 'Account_Detail.csv');
this.toast(`Account_Detail exported (${rows.length - 1} accounts)`);
return { headers, rows };
}
/** Account_Person_Relation.csv — account ↔ customer mapping */
exportAccountPersonRelation(): void {
/** Account_Person_Relation — account ↔ customer mapping */
private buildAccountPersonRelation(): FinnetTable {
const headers = [
'Report Type',
'Report Reference Number',
@@ -1121,7 +1245,7 @@ export class FiuComponent implements OnInit {
'Individual / Non-individual',
'Name of Non-customer'
];
const rows = [headers];
const rows: string[][] = [];
const seen = new Set<string>();
for (const t of this.filteredCtr) {
const key = `${t.reportRefNo}|${t.accountNumber}`;
@@ -1140,8 +1264,145 @@ export class FiuComponent implements OnInit {
t.relationshipFlag === 'Non-Customer' ? t.customerName || '' : ''
]);
}
this.saveCsv(rows, 'Account_Person_Relation.csv');
this.toast(`Account_Person_Relation exported (${rows.length - 1} rows)`);
return { headers, rows };
}
// -------------------- FINnet file sequence --------------------
/**
* The six CTR files in the order FIU-IND expects them: the account is
* described first, then who is behind it, then the transactions, and finally
* the customer cards those transactions belong to.
*/
private get allFinnetFiles(): FinnetFile[] {
return [
{
key: 'account-detail',
name: 'Account_Detail',
step: 1,
title: 'Account Detail',
purpose:
'One row per reported account — status, opening date and the 12-month debit, credit and cash aggregates.',
table: this.buildAccountDetail()
},
{
key: 'account-person-relation',
name: 'Account_Person_Relation',
step: 2,
title: 'Account ↔ Person Relation',
purpose:
'Links each account to the customers behind it, with the relationship type and whether they are an individual or an entity.',
table: this.buildAccountPersonRelation()
},
{
key: 'tc1',
name: 'TC1',
step: 3,
title: 'TC1 — ATM / Card Transactions',
purpose:
'The card-rail leg of the same CTR: ATM withdrawals, recycler deposits and declined attempts. An ATM cash withdrawal is still cash, so it counts towards the customer’s monthly aggregate alongside TC2.',
table: this.buildTc1()
},
{
key: 'tc2',
name: 'TC2',
step: 4,
title: 'TC2 — Branch / Cash Transactions',
purpose: 'The transaction detail itself — every cash transaction that put the account over the CTR threshold.',
table: this.buildTc2()
},
{
key: 'kc1',
name: 'KC1',
step: 5,
title: 'KC1 — Individual Customer Cards',
purpose: 'Full KYC card for every individual named in the report: identifiers, address, income, risk level.',
table: this.buildKc1()
},
{
key: 'kc2',
name: 'KC2',
step: 6,
title: 'KC2 — Entity Customer Cards',
purpose: 'The same for non-individuals: firms, HUFs and societies, with GSTIN and sole-proprietor ownership.',
table: this.buildKc2()
}
];
}
/**
* The files walked through in the CTR tab: account, then who is behind it,
* then the two transaction files. The customer cards are excluded here and
* live on the Customer Cards tab instead, though they still travel with the
* full bundle.
*/
get finnetSequence(): FinnetFile[] {
return this.allFinnetFiles.filter((f) => f.key !== 'kc1' && f.key !== 'kc2');
}
/** Which file in the sequence is open in the CTR tab. */
activeFinnetFile = 'account-detail';
get openFinnetFile(): FinnetFile {
return this.finnetSequence.find((f) => f.key === this.activeFinnetFile) ?? this.finnetSequence[0];
}
showFinnetFile(key: string): void {
this.activeFinnetFile = key;
}
/** Downloads whichever file is currently previewed, so the button matches the table. */
downloadOpenFinnetFile(): void {
const f = this.openFinnetFile;
this.saveCsv(
[
f.table.headers,
...f.table.rows
],
`${f.name}.csv`
);
this.toast(`${f.name} exported (${f.table.rows.length} rows)`);
}
// -------------------- Exports --------------------
private saveTable(file: FinnetFile): void {
this.saveCsv(
[
file.table.headers,
...file.table.rows
],
`${file.name}.csv`
);
this.toast(`${file.name} exported (${file.table.rows.length} rows)`);
}
private fileByKey(key: string): FinnetFile {
return this.allFinnetFiles.find((f) => f.key === key)!;
}
exportTc1(): void {
this.saveTable(this.fileByKey('tc1'));
}
exportTc2(): void {
this.saveTable(this.fileByKey('tc2'));
}
exportKc1(): void {
this.saveTable(this.fileByKey('kc1'));
}
exportKc2(): void {
this.saveTable(this.fileByKey('kc2'));
}
exportAccountDetail(): void {
this.saveTable(this.fileByKey('account-detail'));
}
exportAccountPersonRelation(): void {
this.saveTable(this.fileByKey('account-person-relation'));
}
/** STR XML stub (FINnet 2.0 STR XML) */
@@ -1187,15 +1448,16 @@ export class FiuComponent implements OnInit {
URL.revokeObjectURL(url);
}
/** Bundle: the 6 CSVs plus the STR XML — emitted as separate downloads sequentially */
/**
* Bundle: the 6 CSVs plus the STR XML, emitted as separate downloads in the
* order FIU-IND expects them — account, then relation, then TC, then KC.
*/
exportAll(): void {
setTimeout(() => this.exportTc1(), 0);
setTimeout(() => this.exportTc2(), 200);
setTimeout(() => this.exportKc1(), 400);
setTimeout(() => this.exportKc2(), 600);
setTimeout(() => this.exportAccountDetail(), 800);
setTimeout(() => this.exportAccountPersonRelation(), 1000);
setTimeout(() => this.exportStrXml(), 1200);
// All six travel together — the customer cards are not walked through in the
// CTR tab, but a bundle without them would be incomplete.
const files = this.allFinnetFiles;
files.forEach((f, i) => setTimeout(() => this.saveTable(f), i * 200));
setTimeout(() => this.exportStrXml(), files.length * 200);
}
private escapeXml(s: string): string {