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:
+24
-2
@@ -20,7 +20,15 @@
|
||||
*/
|
||||
|
||||
export type ReportType = 'CTR' | 'STR' | 'CCR' | 'NTR' | 'CBWTR';
|
||||
export type StrStatus = 'draft' | 'po-review' | 'filed' | 'rejected';
|
||||
/**
|
||||
* Lifecycle of an STR case.
|
||||
* draft — raised, not yet with the Principal Officer
|
||||
* po-review — under Principal Officer investigation
|
||||
* awaiting-filing — concluded suspicious; STR XML must still be submitted to FIU-IND
|
||||
* filed — submitted and FIU-IND acknowledgement recorded
|
||||
* rejected — concluded clean; nothing filed
|
||||
*/
|
||||
export type StrStatus = 'draft' | 'po-review' | 'awaiting-filing' | 'filed' | 'rejected';
|
||||
|
||||
/** TC2 — Branch / cash transaction record */
|
||||
export interface CtrTransaction {
|
||||
@@ -42,7 +50,17 @@ export interface CtrTransaction {
|
||||
declaration: string;
|
||||
/** Customer name resolved from KC1 (UI convenience, not in actual TC2 schema) */
|
||||
customerName?: string;
|
||||
complianceStatus?: 'PENDING' | 'WHITELISTED' | 'SUSPICIOUS';
|
||||
/**
|
||||
* CTR does not judge suspicion. It records only whether the source of funds was
|
||||
* evidenced at the counter (CTR filed, nothing further) or could not be, in which
|
||||
* case the transaction is referred to STR. The suspicious/clean verdict itself
|
||||
* belongs to the Principal Officer on the STR side.
|
||||
*/
|
||||
complianceStatus?: 'PENDING' | 'EVIDENCED' | 'REFERRED';
|
||||
/** Document that evidenced the source of funds, when verified at the counter. */
|
||||
evidenceDocType?: string;
|
||||
evidenceReference?: string;
|
||||
evidencedOn?: string;
|
||||
}
|
||||
|
||||
/** Outcome of the Principal Officer's investigation into an STR case */
|
||||
@@ -64,6 +82,10 @@ export interface StrTransaction extends CtrTransaction {
|
||||
poRemarks?: string;
|
||||
/** Set once the Principal Officer concludes the investigation */
|
||||
poFinding?: PoFinding;
|
||||
/** FIU-IND acknowledgement number, recorded only after the STR is actually submitted */
|
||||
fiuReferenceNo?: string;
|
||||
/** Date the STR was submitted on the FINnet portal */
|
||||
filedOn?: string;
|
||||
}
|
||||
|
||||
/** KC1 — Individual customer card (subset of the 70+ fields, only what UI shows) */
|
||||
|
||||
+274
-47
@@ -141,7 +141,8 @@
|
||||
<div class="fiu-form-title">
|
||||
<h3>Live CTR Threshold Alerts</h3>
|
||||
<p>
|
||||
Mechanical cash transaction threshold breaches — review and whitelist or escalate to Principal Officer
|
||||
Mechanical cash threshold breaches. CTR is filed regardless of outcome — where the source of funds cannot be
|
||||
evidenced, send the transaction to STR for the Principal Officer to investigate.
|
||||
</p>
|
||||
</div>
|
||||
<select class="fiu-inp" [(ngModel)]="amlAlertStatusFilter" (ngModelChange)="loadAmlAlerts()">
|
||||
@@ -198,8 +199,7 @@
|
||||
<td>
|
||||
@if (alert.status === 'RAISED' || alert.status === 'UNDER_REVIEW') {
|
||||
<div class="fiu-action-group" *mifosxHasPermission="'REVIEW_AML_ALERT'">
|
||||
<button class="fiu-btn fiu-btn-g" (click)="whitelistAlert(alert)">Whitelist</button>
|
||||
<button class="fiu-btn fiu-btn-xml" (click)="escalateAlert(alert)">Escalate</button>
|
||||
<button class="fiu-btn fiu-btn-xml" (click)="escalateAlert(alert)">Send to STR</button>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
@@ -213,9 +213,9 @@
|
||||
</table>
|
||||
</div>
|
||||
<p class="fiu-tbl-legend">
|
||||
<span class="report-pill report-pending">Pending</span> awaiting Whitelist/Escalate decision ·
|
||||
<span class="report-pill report-ctr">CTR only</span> whitelisted — cleared, no STR needed ·
|
||||
<span class="report-pill report-str">→ STR</span> escalated to Principal Officer as an STR case
|
||||
<span class="report-pill report-pending">Pending</span> cash breach awaiting review ·
|
||||
<span class="report-pill report-str">Referred to STR</span> source of funds not evidenced — sent to the
|
||||
Principal Officer, who decides there whether to whitelist it or file an STR
|
||||
</p>
|
||||
|
||||
<!-- LIVE: monthly CTR reports (mechanical, threshold-based) -->
|
||||
@@ -347,32 +347,75 @@
|
||||
</td>
|
||||
<td class="ta-c">
|
||||
<span class="report-pill report-ctr" title="Always included in the monthly CTR / TC2 file">CTR</span>
|
||||
@if (t.complianceStatus === 'SUSPICIOUS') {
|
||||
<span class="report-pill report-str" title="Also escalated to an STR case for Principal Officer review">+ STR</span>
|
||||
@if (t.complianceStatus === 'REFERRED') {
|
||||
<span
|
||||
class="report-pill report-str"
|
||||
title="Also under investigation as an STR case — the transaction remains in the CTR file"
|
||||
>+ STR</span
|
||||
>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (!t.complianceStatus || t.complianceStatus === 'PENDING') {
|
||||
<div class="fiu-action-group" style="gap: 4px;">
|
||||
<button class="fiu-btn" style="padding: 4px 8px; font-size: 11px; color: var(--f-green); border-color: var(--f-green); background: var(--f-green-l);" (click)="whitelistCtrTxn(t)">
|
||||
Whitelist
|
||||
</button>
|
||||
<button class="fiu-btn" style="padding: 4px 8px; font-size: 11px; color: var(--f-red); border-color: var(--f-red); background: var(--f-red-l);" (click)="flagCtrTxnSuspicious(t)">
|
||||
Flag STR
|
||||
</button>
|
||||
@if (t.complianceStatus === 'REFERRED') {
|
||||
<span class="status-pill status-escalated">REFERRED TO STR</span>
|
||||
<button class="fiu-link-btn" (click)="openStrCase(t)">View in STR tab →</button>
|
||||
} @else if (t.complianceStatus === 'EVIDENCED') {
|
||||
<span class="status-pill status-whitelisted">EVIDENCE ON FILE</span>
|
||||
<div class="muted" style="margin-top: 3px">
|
||||
{{ t.evidenceDocType }}
|
||||
@if (t.evidenceReference) {
|
||||
<span> · {{ t.evidenceReference }}</span>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<span [class]="t.complianceStatus === 'WHITELISTED' ? 'status-pill status-whitelisted' : 'status-pill status-escalated'">
|
||||
{{ t.complianceStatus }}
|
||||
</span>
|
||||
@if (t.complianceStatus === 'WHITELISTED') {
|
||||
<div class="muted" style="margin-top: 3px;">Stays in CTR only</div>
|
||||
} @else {
|
||||
<button class="fiu-link-btn" (click)="openStrCase(t)">View in STR tab →</button>
|
||||
}
|
||||
<div class="fiu-action-group" style="gap: 4px" *mifosxHasPermission="'REVIEW_AML_ALERT'">
|
||||
<button class="fiu-btn fiu-btn-evidence" (click)="startCtrEvidence(t)">Record evidence</button>
|
||||
<button class="fiu-btn fiu-btn-refer" (click)="referCtrTxnToStr(t)">Send to STR</button>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Inline evidence capture for this transaction -->
|
||||
@if (evidencingTxn?.txnId === t.txnId) {
|
||||
<tr class="evidence-row">
|
||||
<td colspan="12">
|
||||
<div class="evidence-panel">
|
||||
<div class="evidence-t">
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
Record source-of-funds evidence for {{ t.customerName }} — ₹{{
|
||||
t.txnAmount | number: '1.0-0'
|
||||
}}
|
||||
</div>
|
||||
<div class="evidence-grid">
|
||||
<div class="form-row">
|
||||
<label for="ctrDoc{{ t.txnId }}">Document produced</label>
|
||||
<select id="ctrDoc{{ t.txnId }}" class="fiu-inp" [(ngModel)]="ctrEvidenceDocType">
|
||||
<option value="">Select document…</option>
|
||||
@for (d of evidenceDocTypes; track d) {
|
||||
<option [value]="d">{{ d }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="ctrRef{{ t.txnId }}">Document / reference number</label>
|
||||
<input id="ctrRef{{ t.txnId }}" class="fiu-inp" type="text" [(ngModel)]="ctrEvidenceRef" />
|
||||
</div>
|
||||
<div class="fiu-action-group">
|
||||
<button class="fiu-btn fiu-btn-evidence" (click)="confirmCtrEvidence()">
|
||||
Confirm — CTR only
|
||||
</button>
|
||||
<button class="fiu-btn" (click)="cancelCtrEvidence()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="evidence-note">
|
||||
Recording evidence is a documentation step, not a suspicion verdict. The transaction is reported
|
||||
in the CTR either way — this simply confirms no STR referral is needed.
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="11" class="fiu-empty">No transactions match the filters.</td>
|
||||
@@ -382,10 +425,11 @@
|
||||
</table>
|
||||
</div>
|
||||
<p class="fiu-tbl-legend">
|
||||
<span class="report-pill report-ctr">CTR</span> every cash txn ≥ threshold stays in the monthly CTR / TC2
|
||||
file ·
|
||||
<span class="report-pill report-str">+ STR</span> also flagged as suspicious and escalated to a separate STR
|
||||
case (see the STR tab)
|
||||
<span class="report-pill report-ctr">CTR</span> every cash txn ≥ threshold is reported here regardless of
|
||||
outcome. <strong>Record evidence</strong> when the customer produces valid documents — CTR only, no STR ·
|
||||
<span class="report-pill report-str">+ STR</span> <strong>Send to STR</strong> when the source of funds cannot
|
||||
be evidenced; the Principal Officer investigates and decides whether to file with FIU-IND. A referred
|
||||
transaction stays in the CTR file as well.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
@@ -397,9 +441,7 @@
|
||||
<div class="fiu-form-meta">
|
||||
<div class="fiu-form-title">
|
||||
<h3>Live AML Alerts (Awaiting Triage)</h3>
|
||||
<p>
|
||||
Flagged by the monitoring engine for suspicious behavioral patterns — triage and whitelist or escalate
|
||||
</p>
|
||||
<p>Flagged by the monitoring engine for suspicious behavioral patterns — triage and whitelist or escalate</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fiu-tbl-wrap">
|
||||
@@ -468,7 +510,7 @@
|
||||
<span class="report-pill report-str">→ STR</span> escalated to Principal Officer as an STR case
|
||||
</p>
|
||||
|
||||
<hr style="border: 0; border-top: 1px solid var(--f-g200); margin: 8px 0;" />
|
||||
<hr style="border: 0; border-top: 1px solid var(--f-g200); margin: 8px 0" />
|
||||
|
||||
<!-- LIVE: STR cases with full Principal Officer actions -->
|
||||
<div class="fiu-form-meta">
|
||||
@@ -627,23 +669,26 @@
|
||||
} @else if (derivedConclusion === 'NOT_SUSPICIOUS') {
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
<span>
|
||||
<strong>Not suspicious.</strong> All {{ strChecklist.length }} points found genuine — the case will
|
||||
be closed and nothing filed with FIU-IND.
|
||||
<strong>Not suspicious.</strong> All {{ strChecklist.length }} points found genuine — the case will be
|
||||
closed and nothing filed with FIU-IND.
|
||||
</span>
|
||||
} @else {
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
<span>
|
||||
<strong>Suspicious — STR must be filed.</strong>
|
||||
{{ failedFindings.length }} of {{ strChecklist.length }} points not satisfied:
|
||||
{{ failedFindings[0].label }}@if (failedFindings.length > 1) {
|
||||
{{ failedFindings[0].label }}
|
||||
@if (failedFindings.length > 1) {
|
||||
<span> and {{ failedFindings.length - 1 }} more</span>
|
||||
}.
|
||||
}
|
||||
.
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button class="fiu-btn fiu-btn-xml" [disabled]="!allAnswered" (click)="concludeStrCase()">
|
||||
Conclude as {{ allAnswered ? (derivedConclusion === 'SUSPICIOUS' ? 'Suspicious' : 'Not Suspicious') : '…' }}
|
||||
Conclude as
|
||||
{{ allAnswered ? (derivedConclusion === 'SUSPICIOUS' ? 'Suspicious' : 'Not Suspicious') : '…' }}
|
||||
</button>
|
||||
|
||||
@if (selectedStrCase.status === 'CONCLUDED_SUSPICIOUS') {
|
||||
@@ -675,8 +720,9 @@
|
||||
<option value="">All</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="po-review">PO Review</option>
|
||||
<option value="awaiting-filing">Awaiting Filing</option>
|
||||
<option value="filed">Filed</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
<option value="rejected">Closed — Clean</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fiu-filter">
|
||||
@@ -752,17 +798,198 @@
|
||||
</label>
|
||||
<p>{{ poFindingLabel(s.poFinding) }}</p>
|
||||
</div>
|
||||
} @else {
|
||||
}
|
||||
|
||||
<!-- Concluded suspicious but not yet submitted to FIU-IND -->
|
||||
@if (s.status === 'awaiting-filing') {
|
||||
<div class="str-filing" *mifosxHasPermission="'CONCLUDE_AML_STR'">
|
||||
<label>
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon> Awaiting submission to FIU-IND — due within 7 days
|
||||
of {{ s.detectedOn }}
|
||||
</label>
|
||||
<p class="filing-hint">
|
||||
Generate the FINnet STR XML, upload it on the FIU-IND portal under
|
||||
{{ bank.principalOfficer }}'s digital signature, then record the acknowledgement number here.
|
||||
</p>
|
||||
<div class="filing-row">
|
||||
<button class="fiu-btn" (click)="exportSingleStrXml(s)">
|
||||
<fa-icon icon="file-alt"></fa-icon> Generate STR XML
|
||||
</button>
|
||||
<input
|
||||
class="fiu-inp"
|
||||
type="text"
|
||||
placeholder="FIU-IND acknowledgement no."
|
||||
[(ngModel)]="localFileRefs[s.reportRefNo]"
|
||||
/>
|
||||
<button class="fiu-btn fiu-btn-xml" (click)="fileLocalStr(s)">Mark STR Filed</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (s.status === 'filed' && s.fiuReferenceNo) {
|
||||
<div class="str-filed-ack">
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
<span>
|
||||
Filed with FIU-IND on <strong>{{ s.filedOn }}</strong> · acknowledgement
|
||||
<strong class="mono">{{ s.fiuReferenceNo }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!s.poFinding && investigatingStr?.reportRefNo !== s.reportRefNo) {
|
||||
<div class="str-po-action" *mifosxHasPermission="'CONCLUDE_AML_STR'">
|
||||
<label>Principal Officer investigation — is the source of funds legitimate?</label>
|
||||
<div class="fiu-action-group">
|
||||
<button class="fiu-btn fiu-btn-clean" (click)="concludeLocalStr(s, 'CLEAN')">
|
||||
Clean — close case
|
||||
</button>
|
||||
<button class="fiu-btn fiu-btn-xml" (click)="concludeLocalStr(s, 'SUSPICIOUS')">
|
||||
Suspicious — file STR with FIU-IND
|
||||
<button class="fiu-btn fiu-btn-xml" (click)="investigateLocalStr(s)">
|
||||
<fa-icon icon="search"></fa-icon> Investigate this case
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Investigation workspace for this case -->
|
||||
@if (investigatingStr?.reportRefNo === s.reportRefNo) {
|
||||
<div class="str-investigation" *mifosxHasPermission="'CONCLUDE_AML_STR'">
|
||||
<div class="inv-head">
|
||||
<span>Principal Officer investigation — STR-{{ s.reportRefNo }}</span>
|
||||
<button class="fiu-link-btn fiu-link-btn--muted" (click)="closeLocalStrInvestigation()">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 1 — call for evidence -->
|
||||
<div class="inv-step">
|
||||
<div class="inv-step-t"><span class="inv-num">1</span> Documentary evidence called for</div>
|
||||
<div class="inv-grid">
|
||||
<div class="form-row">
|
||||
<label for="invDocType">Document requested from the customer</label>
|
||||
<select id="invDocType" class="fiu-inp" [(ngModel)]="evidenceDocType">
|
||||
<option value="">Select document…</option>
|
||||
@for (d of evidenceDocTypes; track d) {
|
||||
<option [value]="d">{{ d }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="invReceived">Was it produced?</label>
|
||||
<select id="invReceived" class="fiu-inp" [(ngModel)]="evidenceReceived">
|
||||
<option value="">Select…</option>
|
||||
<option value="YES">Yes — complete</option>
|
||||
<option value="PARTIAL">Partially — incomplete</option>
|
||||
<option value="NO">No — not produced</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="invRef">Document / reference number</label>
|
||||
<input id="invRef" class="fiu-inp" type="text" [(ngModel)]="evidenceReference" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 — customer's account of the funds -->
|
||||
<div class="inv-step">
|
||||
<div class="inv-step-t"><span class="inv-num">2</span> Customer’s explanation of the source</div>
|
||||
<textarea
|
||||
rows="2"
|
||||
[(ngModel)]="customerExplanation"
|
||||
placeholder="e.g. Proceeds of a flat sold in June; sale deed produced and matched to the deposit amount and date."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Step 3 — verification checklist -->
|
||||
<div class="inv-step">
|
||||
<div class="inv-step-t">
|
||||
<span class="inv-num">3</span> Verification checklist
|
||||
<span class="checklist-progress" [class.done]="allAnswered">
|
||||
{{ answeredCount }} / {{ strChecklist.length }} answered
|
||||
</span>
|
||||
</div>
|
||||
<table class="checklist-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Verification point</th>
|
||||
<th class="ta-c col-yes">Genuine</th>
|
||||
<th class="ta-c col-no">Not satisfied</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (item of strChecklist; track item.key) {
|
||||
<tr
|
||||
[class.row-yes]="strFindings[item.key] === true"
|
||||
[class.row-no]="strFindings[item.key] === false"
|
||||
[class.row-pending]="strFindings[item.key] === null"
|
||||
>
|
||||
<td>
|
||||
<div class="chk-label">{{ item.label }}</div>
|
||||
<div class="chk-hint">{{ item.hint }}</div>
|
||||
</td>
|
||||
<td class="ta-c">
|
||||
<button
|
||||
type="button"
|
||||
class="chk-box chk-box--yes"
|
||||
[class.on]="strFindings[item.key] === true"
|
||||
[attr.aria-label]="'Mark ' + item.label + ' as genuine'"
|
||||
(click)="setFinding(item.key, true)"
|
||||
>
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
</button>
|
||||
</td>
|
||||
<td class="ta-c">
|
||||
<button
|
||||
type="button"
|
||||
class="chk-box chk-box--no"
|
||||
[class.on]="strFindings[item.key] === false"
|
||||
[attr.aria-label]="'Mark ' + item.label + ' as not satisfied'"
|
||||
(click)="setFinding(item.key, false)"
|
||||
>
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Derived outcome -->
|
||||
<div
|
||||
class="verdict"
|
||||
[class.verdict--clear]="canConcludeLocal && derivedConclusion === 'NOT_SUSPICIOUS'"
|
||||
[class.verdict--suspicious]="canConcludeLocal && derivedConclusion === 'SUSPICIOUS'"
|
||||
>
|
||||
@if (!canConcludeLocal) {
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
<span>
|
||||
Complete all three steps to determine the outcome.
|
||||
@if (!evidenceRecorded) {
|
||||
<span> Evidence not yet recorded.</span>
|
||||
}
|
||||
@if (!allAnswered) {
|
||||
<span> {{ strChecklist.length - answeredCount }} verification point(s) pending.</span>
|
||||
}
|
||||
</span>
|
||||
} @else if (derivedConclusion === 'NOT_SUSPICIOUS') {
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
<span>
|
||||
<strong>Source of funds established.</strong> Case closes as clean — the transaction stays in
|
||||
the CTR report and no STR is filed with FIU-IND.
|
||||
</span>
|
||||
} @else {
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
<span>
|
||||
<strong>Source of funds not established.</strong> {{ failedFindings.length }} point(s)
|
||||
unsatisfied — an STR will be filed with FIU-IND in addition to the CTR.
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button class="fiu-btn fiu-btn-xml" [disabled]="!canConcludeLocal" (click)="concludeLocalStr()">
|
||||
{{
|
||||
canConcludeLocal
|
||||
? derivedConclusion === 'SUSPICIOUS'
|
||||
? 'Conclude — file STR with FIU-IND'
|
||||
: 'Conclude — close as clean (CTR only)'
|
||||
: 'Conclude'
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -871,8 +1098,8 @@
|
||||
<div class="fiu-form-title">
|
||||
<h3>FINnet 2.0 Submission Files</h3>
|
||||
<p>
|
||||
6 CSV files in FIU-IND prescribed schema, plus STR XML. Counts reflect any filters set on the CTR and
|
||||
STR tabs, so every file in the bundle covers the same transactions.
|
||||
6 CSV files in FIU-IND prescribed schema, plus STR XML. Counts reflect any filters set on the CTR and STR
|
||||
tabs, so every file in the bundle covers the same transactions.
|
||||
</p>
|
||||
</div>
|
||||
<button class="fiu-btn fiu-btn-xls" (click)="exportAll()">
|
||||
|
||||
@@ -386,6 +386,78 @@
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
/* CTR outcome 1: source of funds evidenced at the counter — CTR only. */
|
||||
.fiu-btn-evidence {
|
||||
padding: 5px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--f-green);
|
||||
border-color: var(--f-green);
|
||||
background: var(--f-green-l);
|
||||
|
||||
&:hover {
|
||||
background: var(--f-green);
|
||||
border-color: var(--f-green);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* Inline evidence capture row inside the TC2 table. */
|
||||
.evidence-row td {
|
||||
background: var(--f-green-l) !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.evidence-panel {
|
||||
padding: 12px 16px;
|
||||
border-left: 3px solid var(--f-green);
|
||||
}
|
||||
|
||||
.evidence-t {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #14532d;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.evidence-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr auto;
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.evidence-note {
|
||||
margin: 8px 0 0;
|
||||
font-size: 11px;
|
||||
color: var(--f-g700);
|
||||
}
|
||||
|
||||
@media (width <= 900px) {
|
||||
.evidence-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* CTR outcome 2: evidence unavailable — refer the transaction onward to STR. */
|
||||
.fiu-btn-refer {
|
||||
padding: 5px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--f-red);
|
||||
border-color: var(--f-red);
|
||||
background: var(--f-red-l);
|
||||
|
||||
&:hover {
|
||||
background: var(--f-red);
|
||||
border-color: var(--f-red);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.fiu-link-btn {
|
||||
display: inline-block;
|
||||
margin-top: 3px;
|
||||
@@ -708,6 +780,11 @@
|
||||
color: var(--f-amber);
|
||||
}
|
||||
|
||||
&.st-awaiting-filing {
|
||||
background: #ffedd5;
|
||||
color: #9a3412;
|
||||
}
|
||||
|
||||
&.st-filed {
|
||||
background: var(--f-green-l);
|
||||
color: var(--f-green);
|
||||
@@ -1306,6 +1383,128 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Concluded suspicious, still to be submitted on the FINnet portal. */
|
||||
.str-filing {
|
||||
background: #fff7ed;
|
||||
border: 1px solid #fdba74;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #9a3412;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
}
|
||||
|
||||
.filing-hint {
|
||||
margin: 4px 0 8px;
|
||||
font-size: 12px;
|
||||
color: var(--f-g700);
|
||||
}
|
||||
|
||||
.filing-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.fiu-inp {
|
||||
max-width: 260px;
|
||||
}
|
||||
}
|
||||
|
||||
.str-filed-ack {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--f-green-l);
|
||||
border: 1px solid #86efac;
|
||||
border-radius: 8px;
|
||||
padding: 9px 14px;
|
||||
font-size: 12px;
|
||||
color: #14532d;
|
||||
}
|
||||
|
||||
/* ── CTR-referred case: Principal Officer investigation workspace ───────── */
|
||||
.str-investigation {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--f-purple);
|
||||
border-radius: 10px;
|
||||
background: #fdfcff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.inv-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--f-purple);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.inv-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--f-g300);
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.inv-step-t {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--f-g900);
|
||||
}
|
||||
|
||||
.inv-num {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: var(--f-purple);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (width <= 900px) {
|
||||
.inv-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Derived verdict banner */
|
||||
.verdict {
|
||||
display: flex;
|
||||
|
||||
+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