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:
2026-08-02 11:46:40 +05:30
parent 2a9df828ca
commit 3030e2f34e
37 changed files with 9292 additions and 620 deletions
+218 -37
View File
@@ -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 */