risk review report with on which aml rule
This commit is contained in:
+298
-36
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user