form 91/98 report, min max value update in AML rule and whihc rule apply day, week, month and some more updates
This commit is contained in:
+218
-37
@@ -135,6 +135,54 @@ export class FiuComponent implements OnInit {
|
||||
/** null = not yet answered, true = genuine, false = not satisfied. */
|
||||
strFindings: Record<string, boolean | null> = {};
|
||||
|
||||
// ── Investigation of a CTR-referred STR case ───────────────────────────────
|
||||
/** The local STR card currently under investigation, if any. */
|
||||
investigatingStr: StrTransaction | null = null;
|
||||
|
||||
/** Documents the Principal Officer can call for to evidence the source of funds. */
|
||||
readonly evidenceDocTypes: string[] = [
|
||||
'Sale deed / agreement for immovable property',
|
||||
'Salary slips and Form 16',
|
||||
'Audited financials / ITR of the business',
|
||||
'Loan sanction or disbursement letter',
|
||||
'Gift deed with donor PAN and relationship proof',
|
||||
'Maturity advice of a deposit or insurance policy',
|
||||
'Sale bill for gold, vehicle or other asset',
|
||||
'Agricultural income certificate / mandi receipts',
|
||||
'No document produced by the customer'
|
||||
];
|
||||
|
||||
evidenceDocType = '';
|
||||
evidenceReceived: 'YES' | 'PARTIAL' | 'NO' | '' = '';
|
||||
evidenceReference = '';
|
||||
customerExplanation = '';
|
||||
|
||||
/** Opens the investigation panel for a CTR-referred case with a clean slate. */
|
||||
investigateLocalStr(rec: StrTransaction): void {
|
||||
this.investigatingStr = rec;
|
||||
this.selectedStrCase = null;
|
||||
this.resetChecklist();
|
||||
this.evidenceDocType = '';
|
||||
this.evidenceReceived = '';
|
||||
this.evidenceReference = '';
|
||||
this.customerExplanation = '';
|
||||
this.concludeInvestigationNotes = '';
|
||||
this.concludeSourceOfFundsNotes = '';
|
||||
}
|
||||
|
||||
closeLocalStrInvestigation(): void {
|
||||
this.investigatingStr = null;
|
||||
}
|
||||
|
||||
/** Documentary evidence is a precondition for clearing a case, not an optional note. */
|
||||
get evidenceRecorded(): boolean {
|
||||
return !!this.evidenceDocType && !!this.evidenceReceived;
|
||||
}
|
||||
|
||||
get canConcludeLocal(): boolean {
|
||||
return this.evidenceRecorded && this.allAnswered && !!this.customerExplanation.trim();
|
||||
}
|
||||
|
||||
private resetChecklist(): void {
|
||||
this.strFindings = {};
|
||||
for (const item of this.strChecklist) {
|
||||
@@ -202,14 +250,51 @@ export class FiuComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
whitelistCtrTxn(txn: CtrTransaction): void {
|
||||
txn.complianceStatus = 'WHITELISTED';
|
||||
localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns));
|
||||
this.toast(`Transaction for ${txn.customerName} whitelisted — stays in the CTR report only`);
|
||||
// ── CTR-side evidence capture ──────────────────────────────────────────────
|
||||
/** Transaction whose source-of-funds evidence is being recorded, if any. */
|
||||
evidencingTxn: CtrTransaction | null = null;
|
||||
ctrEvidenceDocType = '';
|
||||
ctrEvidenceRef = '';
|
||||
|
||||
startCtrEvidence(txn: CtrTransaction): void {
|
||||
this.evidencingTxn = txn;
|
||||
this.ctrEvidenceDocType = '';
|
||||
this.ctrEvidenceRef = '';
|
||||
}
|
||||
|
||||
flagCtrTxnSuspicious(txn: CtrTransaction): void {
|
||||
txn.complianceStatus = 'SUSPICIOUS';
|
||||
cancelCtrEvidence(): void {
|
||||
this.evidencingTxn = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records that the source of funds was evidenced at the counter. This is a
|
||||
* documentation step, not a suspicion verdict — the transaction is reported in
|
||||
* the CTR either way; recording evidence simply means no STR referral is needed.
|
||||
*/
|
||||
confirmCtrEvidence(): void {
|
||||
const txn = this.evidencingTxn;
|
||||
if (!txn) {
|
||||
return;
|
||||
}
|
||||
if (!this.ctrEvidenceDocType) {
|
||||
this.toast('Select the document that evidences the source of funds');
|
||||
return;
|
||||
}
|
||||
txn.complianceStatus = 'EVIDENCED';
|
||||
txn.evidenceDocType = this.ctrEvidenceDocType;
|
||||
txn.evidenceReference = this.ctrEvidenceRef.trim();
|
||||
txn.evidencedOn = new Date().toLocaleDateString('en-GB').replace(/\//g, '-');
|
||||
localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns));
|
||||
this.evidencingTxn = null;
|
||||
this.toast(`${txn.customerName} — evidence recorded. Reported in CTR only, no STR referral.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Referral to STR. Used when the source of funds could not be evidenced; the
|
||||
* Principal Officer then investigates and decides whether to file with FIU-IND.
|
||||
*/
|
||||
referCtrTxnToStr(txn: CtrTransaction): void {
|
||||
txn.complianceStatus = 'REFERRED';
|
||||
localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns));
|
||||
|
||||
const newRef = Math.max(100, ...this.strTxns.map((s) => s.reportRefNo)) + 1;
|
||||
@@ -222,34 +307,112 @@ export class FiuComponent implements OnInit {
|
||||
riskCategory: 'unusual-pattern',
|
||||
narrative:
|
||||
`Cash ${txn.depositOrWithdrawal.toLowerCase()} of ₹${txn.txnAmount.toLocaleString('en-IN')} breached the ` +
|
||||
`₹${this.bank.cashThreshold.toLocaleString('en-IN')} CTR limit and was flagged as suspicious by the AML Officer. ` +
|
||||
`Referred to the Principal Officer to establish whether the source of funds is legitimate or linked to terror financing.`,
|
||||
`₹${this.bank.cashThreshold.toLocaleString('en-IN')} CTR limit and genuine evidence of the source of funds ` +
|
||||
`could not be established at the branch. Referred to the Principal Officer for investigation.`,
|
||||
status: 'po-review',
|
||||
poFinding: undefined
|
||||
};
|
||||
|
||||
this.strTxns.unshift(newStrCase);
|
||||
localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns));
|
||||
this.toast(`STR-${newRef} opened for ${txn.customerName} — awaiting Principal Officer investigation`);
|
||||
this.toast(`${txn.customerName} referred to STR-${newRef} — awaiting Principal Officer investigation`);
|
||||
}
|
||||
|
||||
/** Principal Officer closes an STR case as legitimate, or confirms it and files an STR. */
|
||||
concludeLocalStr(rec: StrTransaction, finding: PoFinding): void {
|
||||
/**
|
||||
* Concludes a CTR-referred case. The verdict follows the evidence and the
|
||||
* verification checklist rather than a free choice, and the full trail is written
|
||||
* into the case remarks so the filing carries its own justification.
|
||||
*/
|
||||
concludeLocalStr(): void {
|
||||
const rec = this.investigatingStr;
|
||||
if (!rec) {
|
||||
return;
|
||||
}
|
||||
if (!this.evidenceRecorded) {
|
||||
this.toast('Record which document was called for and whether it was produced');
|
||||
return;
|
||||
}
|
||||
if (!this.customerExplanation.trim()) {
|
||||
this.toast('Record the explanation the customer gave for the source of funds');
|
||||
return;
|
||||
}
|
||||
if (!this.allAnswered) {
|
||||
this.toast(`Answer all verification points — ${this.strChecklist.length - this.answeredCount} still pending`);
|
||||
return;
|
||||
}
|
||||
|
||||
const finding: PoFinding = this.derivedConclusion === 'SUSPICIOUS' ? 'SUSPICIOUS' : 'CLEAN';
|
||||
const failed = this.failedFindings;
|
||||
const evidenceLine =
|
||||
`Document called for: ${this.evidenceDocType}. Produced: ${this.evidenceReceived}` +
|
||||
(this.evidenceReference.trim() ? ` (ref ${this.evidenceReference.trim()})` : '') +
|
||||
'.';
|
||||
const explanationLine = `Customer's explanation: ${this.customerExplanation.trim()}`;
|
||||
|
||||
rec.poFinding = finding;
|
||||
if (finding === 'CLEAN') {
|
||||
rec.status = 'rejected';
|
||||
rec.poRemarks =
|
||||
'Source-of-funds documentation reviewed and verified by the Principal Officer. No link to terror financing or ' +
|
||||
'any other predicate offence. Case closed — no STR filed with FIU-IND.';
|
||||
this.toast(`STR-${rec.reportRefNo} closed as clean — no STR filed`);
|
||||
`${evidenceLine} ${explanationLine} All ${this.strChecklist.length} verification points found genuine, ` +
|
||||
`so the source of funds is established. Case closed — the transaction remains in the CTR report and no STR ` +
|
||||
`is filed with FIU-IND.`;
|
||||
// The originating CTR row must stop advertising a live STR once cleared.
|
||||
const origin = this.ctrTxns.find((t) => t.txnId === rec.txnId);
|
||||
if (origin) {
|
||||
origin.complianceStatus = 'EVIDENCED';
|
||||
origin.evidenceDocType = this.evidenceDocType;
|
||||
origin.evidenceReference = this.evidenceReference.trim();
|
||||
origin.evidencedOn = new Date().toLocaleDateString('en-GB').replace(/\//g, '-');
|
||||
localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns));
|
||||
}
|
||||
this.toast(`STR-${rec.reportRefNo} closed as clean — CTR only, no STR filed`);
|
||||
} else {
|
||||
rec.status = 'filed';
|
||||
// Concluding suspicious does not file anything — the STR still has to be
|
||||
// submitted on the FINnet portal and its acknowledgement recorded.
|
||||
rec.status = 'awaiting-filing';
|
||||
rec.poRemarks =
|
||||
'Principal Officer investigation could not establish a legitimate source of funds. STR filed with FIU-IND under ' +
|
||||
'PMLA s.12 within the 7-day window; account placed under enhanced monitoring and reported to the Nodal Officer.';
|
||||
this.toast(`STR-${rec.reportRefNo} confirmed suspicious — filed with FIU-IND`);
|
||||
`${evidenceLine} ${explanationLine} ${failed.length} of ${this.strChecklist.length} verification points not ` +
|
||||
`satisfied: ${failed.map((f) => f.label).join('; ')}. Source of funds not established — STR to be filed with ` +
|
||||
`FIU-IND under PMLA s.12 within 7 days and the account placed under enhanced monitoring.`;
|
||||
this.toast(`STR-${rec.reportRefNo} concluded suspicious — now awaiting submission to FIU-IND`);
|
||||
}
|
||||
localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns));
|
||||
this.investigatingStr = null;
|
||||
}
|
||||
|
||||
/** Acknowledgement number captured against a case awaiting submission. */
|
||||
localFileRefs: Record<number, string> = {};
|
||||
|
||||
/**
|
||||
* Records that the STR was actually submitted on the FINnet portal. Only this
|
||||
* step moves a case to "filed" and counts it in the filed KPI.
|
||||
*/
|
||||
fileLocalStr(rec: StrTransaction): void {
|
||||
const ref = (this.localFileRefs[rec.reportRefNo] || '').trim();
|
||||
if (!ref) {
|
||||
this.toast('Enter the FIU-IND acknowledgement number received on the FINnet portal');
|
||||
return;
|
||||
}
|
||||
rec.status = 'filed';
|
||||
rec.fiuReferenceNo = ref;
|
||||
rec.filedOn = new Date().toLocaleDateString('en-GB').replace(/\//g, '-');
|
||||
rec.poRemarks = `${rec.poRemarks ?? ''} Submitted to FIU-IND on ${rec.filedOn}; acknowledgement ${ref}.`.trim();
|
||||
localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns));
|
||||
this.toast(`STR-${rec.reportRefNo} filed with FIU-IND — acknowledgement ${ref}`);
|
||||
}
|
||||
|
||||
/** Generates the FINnet STR XML for one case, ready to upload to the portal. */
|
||||
exportSingleStrXml(rec: StrTransaction): void {
|
||||
const saved = this.strFilterStatus;
|
||||
const savedRisk = this.strFilterRisk;
|
||||
// Temporarily narrow the export to this case only, then restore the filters.
|
||||
this.strFilterStatus = '';
|
||||
this.strFilterRisk = '';
|
||||
const only = [rec];
|
||||
this.writeStrXml(only, `STR-${rec.reportRefNo}-${rec.customerName ?? 'case'}.xml`);
|
||||
this.strFilterStatus = saved;
|
||||
this.strFilterRisk = savedRisk;
|
||||
this.toast(`STR-${rec.reportRefNo} XML generated — upload it on the FINnet portal`);
|
||||
}
|
||||
|
||||
poFindingLabel(finding: PoFinding): string {
|
||||
@@ -363,12 +526,7 @@ export class FiuComponent implements OnInit {
|
||||
: `${this.concludeInvestigationNotes.trim()}\n\nAll ${this.strChecklist.length} verification points found genuine.`;
|
||||
|
||||
this.amlService
|
||||
.concludeStrCase(
|
||||
this.selectedStrCase.id,
|
||||
this.concludeConclusion,
|
||||
notes,
|
||||
this.concludeSourceOfFundsNotes
|
||||
)
|
||||
.concludeStrCase(this.selectedStrCase.id, this.concludeConclusion, notes, this.concludeSourceOfFundsNotes)
|
||||
.subscribe({
|
||||
next: (updated) => {
|
||||
this.toast(
|
||||
@@ -406,16 +564,16 @@ export class FiuComponent implements OnInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a live AML alert will end up once triaged — drives the "Routes To" column.
|
||||
* `isCtrRule` distinguishes mechanical CTR-threshold alerts (whitelisting just keeps them
|
||||
* in the CTR file) from behavioral alerts (whitelisting clears them with no report at all).
|
||||
* Where a live AML alert stands — drives the "Routes To" column.
|
||||
* Whitelisting is an STR-side decision, so a CTR alert can only be pending or
|
||||
* referred onward. Historic WHITELISTED rows are still rendered for the record.
|
||||
*/
|
||||
alertRouteLabel(status: string, isCtrRule: boolean = true): string {
|
||||
switch (status) {
|
||||
case 'WHITELISTED':
|
||||
return isCtrRule ? 'CTR only' : 'Cleared';
|
||||
case 'ESCALATED':
|
||||
return '→ STR';
|
||||
return isCtrRule ? 'Referred to STR' : '→ STR';
|
||||
default:
|
||||
return 'Pending';
|
||||
}
|
||||
@@ -450,9 +608,7 @@ export class FiuComponent implements OnInit {
|
||||
this.resetStrFilters();
|
||||
this.highlightedStrRef = match.reportRefNo;
|
||||
setTimeout(() => {
|
||||
document
|
||||
.getElementById(`str-card-${match.reportRefNo}`)
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
document.getElementById(`str-card-${match.reportRefNo}`)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -467,7 +623,9 @@ export class FiuComponent implements OnInit {
|
||||
return this.ctrTxns.reduce((s, t) => s + t.txnAmount, 0);
|
||||
}
|
||||
get pendingStr(): number {
|
||||
return this.strTxns.filter((s) => s.status === 'draft' || s.status === 'po-review').length;
|
||||
return this.strTxns.filter(
|
||||
(s) => s.status === 'draft' || s.status === 'po-review' || s.status === 'awaiting-filing'
|
||||
).length;
|
||||
}
|
||||
get filedStr(): number {
|
||||
return this.strTxns.filter((s) => s.status === 'filed').length;
|
||||
@@ -496,12 +654,20 @@ export class FiuComponent implements OnInit {
|
||||
}
|
||||
|
||||
get ctrAlerts(): any[] {
|
||||
const ctrRuleCodes = ['CTR_THRESHOLD_10L', 'STRUCTURED_CTR_10L_MONTHLY', 'LARGE_CASH_TXN_25L'];
|
||||
const ctrRuleCodes = [
|
||||
'CTR_THRESHOLD_10L',
|
||||
'STRUCTURED_CTR_10L_MONTHLY',
|
||||
'LARGE_CASH_TXN_25L'
|
||||
];
|
||||
return this.amlAlerts.filter((a) => ctrRuleCodes.includes(a.ruleCode));
|
||||
}
|
||||
|
||||
get amlSuspiciousAlerts(): any[] {
|
||||
const ctrRuleCodes = ['CTR_THRESHOLD_10L', 'STRUCTURED_CTR_10L_MONTHLY', 'LARGE_CASH_TXN_25L'];
|
||||
const ctrRuleCodes = [
|
||||
'CTR_THRESHOLD_10L',
|
||||
'STRUCTURED_CTR_10L_MONTHLY',
|
||||
'LARGE_CASH_TXN_25L'
|
||||
];
|
||||
return this.amlAlerts.filter((a) => !ctrRuleCodes.includes(a.ruleCode));
|
||||
}
|
||||
|
||||
@@ -544,7 +710,14 @@ export class FiuComponent implements OnInit {
|
||||
|
||||
// -------------------- Helpers --------------------
|
||||
statusLabel(s: StrStatus): string {
|
||||
return s === 'draft' ? 'Draft' : s === 'po-review' ? 'PO Review' : s === 'filed' ? 'Filed' : 'Rejected';
|
||||
const map: Record<StrStatus, string> = {
|
||||
draft: 'Draft',
|
||||
'po-review': 'PO Review',
|
||||
'awaiting-filing': 'Awaiting Filing',
|
||||
filed: 'Filed',
|
||||
rejected: 'Closed — Clean'
|
||||
};
|
||||
return map[s] ?? s;
|
||||
}
|
||||
riskLabel(c: StrTransaction['riskCategory']): string {
|
||||
const map: Record<StrTransaction['riskCategory'], string> = {
|
||||
@@ -912,6 +1085,12 @@ export class FiuComponent implements OnInit {
|
||||
this.toast('No suspect cases to export — nothing matches the current filters');
|
||||
return;
|
||||
}
|
||||
this.writeStrXml(suspects, `STR-${this.bank.reportingMonth.replace(/\s+/g, '-')}.xml`);
|
||||
this.toast(`STR XML exported (${suspects.length} suspects)`);
|
||||
}
|
||||
|
||||
/** Serialises a set of suspect cases as FINnet 2.0 STR XML and downloads it. */
|
||||
private writeStrXml(suspects: StrTransaction[], filename: string): void {
|
||||
const xml: string[] = [];
|
||||
xml.push('<?xml version="1.0" encoding="UTF-8"?>');
|
||||
xml.push(`<STR xmlns="http://fiuind.gov.in/finnet2/str" reportingMonth="${this.bank.reportingMonth}">`);
|
||||
@@ -927,6 +1106,9 @@ export class FiuComponent implements OnInit {
|
||||
xml.push(` <Detection date="${s.detectedOn}" by="${this.escapeXml(s.flaggedBy)}" />`);
|
||||
xml.push(` <Narrative>${this.escapeXml(s.narrative)}</Narrative>`);
|
||||
if (s.poRemarks) xml.push(` <PORemarks>${this.escapeXml(s.poRemarks)}</PORemarks>`);
|
||||
if (s.fiuReferenceNo) {
|
||||
xml.push(` <Acknowledgement ref="${this.escapeXml(s.fiuReferenceNo)}" filedOn="${s.filedOn ?? ''}" />`);
|
||||
}
|
||||
xml.push(' </Suspect>');
|
||||
}
|
||||
xml.push('</STR>');
|
||||
@@ -934,10 +1116,9 @@ export class FiuComponent implements OnInit {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `STR-${this.bank.reportingMonth.replace(/\s+/g, '-')}.xml`;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
this.toast(`STR XML exported (${suspects.length} suspects)`);
|
||||
}
|
||||
|
||||
/** Bundle: the 6 CSVs plus the STR XML — emitted as separate downloads sequentially */
|
||||
|
||||
Reference in New Issue
Block a user