From 1daa12a117d6ce5d2b4e962b7b452e7daf7e5752 Mon Sep 17 00:00:00 2001 From: keshav Date: Fri, 31 Jul 2026 10:58:58 +0530 Subject: [PATCH] kyc risk and AML & Compliance Checks automate on client creation --- src/app/aml/aml.service.ts | 89 +++ .../client-general-step.component.ts | 55 +- .../client-kyc-step.component.html | 6 + .../client-kyc-step.component.ts | 71 +- .../authentication/authentication.service.ts | 16 + src/app/core/shell/shell.component.html | 16 +- src/app/core/shell/shell.component.ts | 17 - src/app/fiu/fiu.component.html | 223 +++++- src/app/fiu/fiu.component.scss | 117 +++ src/app/fiu/fiu.component.ts | 128 ++- .../ctr-str-dialog.component.html | 285 ------- .../ctr-str-dialog.component.scss | 750 ------------------ .../ctr-str-dialog.component.ts | 279 ------- .../aml-rules/aml-rules.component.html | 87 ++ .../aml-rules/aml-rules.component.scss | 228 ++++++ .../aml-rules/aml-rules.component.ts | 87 ++ .../rbi-circulars-routing.module.ts | 6 + 17 files changed, 1113 insertions(+), 1347 deletions(-) create mode 100644 src/app/aml/aml.service.ts delete mode 100644 src/app/home/dashboard/ctr-str-dialog/ctr-str-dialog.component.html delete mode 100644 src/app/home/dashboard/ctr-str-dialog/ctr-str-dialog.component.scss delete mode 100644 src/app/home/dashboard/ctr-str-dialog/ctr-str-dialog.component.ts create mode 100644 src/app/rbi-circulars/aml-rules/aml-rules.component.html create mode 100644 src/app/rbi-circulars/aml-rules/aml-rules.component.scss create mode 100644 src/app/rbi-circulars/aml-rules/aml-rules.component.ts diff --git a/src/app/aml/aml.service.ts b/src/app/aml/aml.service.ts new file mode 100644 index 000000000..ceb0993c8 --- /dev/null +++ b/src/app/aml/aml.service.ts @@ -0,0 +1,89 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +/** + * AML transaction-monitoring API: live alerts raised by the rule engine, the + * two-tier AML Officer / Principal Officer review workflow, STR cases (with + * their 7-working-day FIU filing deadline), monthly CTR reports, and a + * client's risk-category change history. + */ +@Injectable({ + providedIn: 'root' +}) +export class AmlService { + private http = inject(HttpClient); + + // ═══ Alerts (AML Officer) ═══ + + getAlerts(status?: string): Observable { + const params = status ? new HttpParams().set('status', status) : undefined; + return this.http.get('/aml/alerts', { params }); + } + + /** disposition: 'WHITELIST' | 'ESCALATE' */ + reviewAlert(alertId: number, disposition: 'WHITELIST' | 'ESCALATE', notes: string): Observable { + return this.http.post(`/aml/alerts/${alertId}/review`, { disposition, notes }); + } + + // ═══ STR cases (Principal Officer) ═══ + + getStrCases(): Observable { + return this.http.get('/aml/str-cases'); + } + + getStrCase(strCaseId: number): Observable { + return this.http.get(`/aml/str-cases/${strCaseId}`); + } + + getWeeklyDigest(): Observable { + return this.http.get('/aml/str-cases/weekly-digest'); + } + + assignPrincipalOfficer(strCaseId: number, principalOfficerUserId: number): Observable { + return this.http.post(`/aml/str-cases/${strCaseId}/assign`, { principalOfficerUserId }); + } + + /** conclusion: 'NOT_SUSPICIOUS' | 'SUSPICIOUS' */ + concludeStrCase( + strCaseId: number, + conclusion: 'NOT_SUSPICIOUS' | 'SUSPICIOUS', + investigationNotes: string, + sourceOfFundsNotes: string + ): Observable { + return this.http.post(`/aml/str-cases/${strCaseId}/conclude`, { + conclusion, + investigationNotes, + sourceOfFundsNotes + }); + } + + fileStr(strCaseId: number, referenceNo: string): Observable { + return this.http.post(`/aml/str-cases/${strCaseId}/file`, { referenceNo }); + } + + // ═══ CTR reports (mechanical, threshold-based) ═══ + + getCtrReports(): Observable { + return this.http.get('/aml/ctr-reports'); + } + + /** month format: yyyy-MM */ + generateCtrReport(month: string): Observable { + return this.http.post('/aml/ctr-reports/generate', { month }); + } + + // ═══ Client risk-category history ═══ + + getRiskCategoryHistory(clientId: number): Observable { + return this.http.get(`/clients/${clientId}/risk-category-history`); + } +} diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts index dba14f5e8..e6e642c66 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts @@ -2126,7 +2126,15 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges return generalDetails; } - get calculatedRisk(): { category: number; reason: string } { + get calculatedRisk(): { + category: number; + reason: string; + amlCheckStatus?: string; + bannedEntityMatch?: string; + willfulDefaulter?: string; + ecgcSalFlag?: string; + dormantFlag?: string; + } { const legalFormId = this.createClientForm.get('legalFormId')?.value || 1; // 1 = Person, 2 = Entity const occCatId = this.createClientForm.get('occupationCategoryCvId')?.value; const occId = this.createClientForm.get('occupationCvId')?.value; @@ -2143,6 +2151,21 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges // Evaluate rules from DB (ordered by orderPosition ASC) if (this.riskRules && this.riskRules.length > 0) { for (const rule of this.riskRules) { + // Skip transaction/AML rules that have no KYC criteria + if ( + rule.legalFormId == null && + rule.occupationCategoryCvId == null && + rule.occupationCvId == null && + rule.minMonthlyIncome == null && + rule.maxMonthlyIncome == null && + rule.minAnnualIncome == null && + rule.maxAnnualIncome == null && + rule.minAnnualTurnover == null && + rule.maxAnnualTurnover == null + ) { + continue; + } + // Match legal form if (rule.legalFormId != null && rule.legalFormId !== legalFormId) { continue; @@ -2183,15 +2206,39 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges } // All matched! - return { category: rule.riskCategory, reason: rule.reason }; + return { + category: rule.riskCategory, + reason: rule.reason, + amlCheckStatus: rule.amlCheckStatus || (rule.riskCategory === 3 ? 'FLAGGED' : 'CLEAR'), + bannedEntityMatch: rule.bannedEntityMatch || 'NO', + willfulDefaulter: rule.willfulDefaulter || 'NO', + ecgcSalFlag: rule.ecgcSalFlag || 'NO', + dormantFlag: rule.dormantFlag || 'ACTIVE' + }; } } // Default Fallbacks if (legalFormId !== LegalFormId.PERSON) { - return { category: 2, reason: 'Non-Individual / Entity profile (Default Medium Risk)' }; + return { + category: 2, + reason: 'Non-Individual / Entity profile (Default Medium Risk)', + amlCheckStatus: 'CLEAR', + bannedEntityMatch: 'NO', + willfulDefaulter: 'NO', + ecgcSalFlag: 'NO', + dormantFlag: 'ACTIVE' + }; } - return { category: 1, reason: 'Default Low Risk profile (Salary / Service / Agriculture / Others)' }; + return { + category: 1, + reason: 'Default Low Risk profile (Salary / Service / Agriculture / Others)', + amlCheckStatus: 'CLEAR', + bannedEntityMatch: 'NO', + willfulDefaulter: 'NO', + ecgcSalFlag: 'NO', + dormantFlag: 'ACTIVE' + }; } } diff --git a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html index 11b4e42c7..c5495da56 100644 --- a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html +++ b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html @@ -1251,6 +1251,12 @@
+
+ + Override Auto-calculated AML & Compliance Checks + +
+ AML Check Status diff --git a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts index 13bfad537..b44b19958 100644 --- a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts +++ b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts @@ -70,7 +70,15 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy { @Input() entityName: string = ''; /** Read-only mode — disables form editing until user clicks edit icon */ @Input() readOnly = false; - @Input() calculatedRisk: { category: number; reason: string } | null = null; + @Input() calculatedRisk: { + category: number; + reason: string; + amlCheckStatus?: string; + bannedEntityMatch?: string; + willfulDefaulter?: string; + ecgcSalFlag?: string; + dormantFlag?: string; + } | null = null; today = new Date(); kycId: string = ''; @@ -457,6 +465,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy { kycDocExpiryDate: [''], riskCategory: [1], allowRiskOverride: [false], + allowAmlOverride: [false], remarks: [''], amlCheckStatus: [ 'CLEAR', @@ -534,9 +543,35 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy { } }); + this.kycForm.get('allowAmlOverride')?.valueChanges.subscribe((allowed: boolean) => { + const amlControls = [ + this.kycForm.get('amlCheckStatus'), + this.kycForm.get('bannedEntityMatch'), + this.kycForm.get('willfulDefaulter'), + this.kycForm.get('ecgcSalFlag'), + this.kycForm.get('dormantFlag') + ]; + if (allowed) { + if (!this.readOnly) { + amlControls.forEach((ctrl) => ctrl?.enable()); + } + } else { + amlControls.forEach((ctrl) => ctrl?.disable()); + this.applyAutoCalculatedAmlFlags(); + } + }); + if (!this.kycForm.get('allowRiskOverride')?.value) { this.kycForm.get('riskCategory')?.disable(); } + + if (!this.kycForm.get('allowAmlOverride')?.value) { + this.kycForm.get('amlCheckStatus')?.disable(); + this.kycForm.get('bannedEntityMatch')?.disable(); + this.kycForm.get('willfulDefaulter')?.disable(); + this.kycForm.get('ecgcSalFlag')?.disable(); + this.kycForm.get('dormantFlag')?.disable(); + } } /** Seed the document-type picker from defaults (entity RBI docs vs. individual OVDs, @@ -644,6 +679,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy { if (!this.kycForm.get('allowRiskOverride')?.value) { this.kycForm.get('riskCategory')?.disable({ emitEvent: false }); } + if (!this.kycForm.get('allowAmlOverride')?.value) { + this.kycForm.get('amlCheckStatus')?.disable({ emitEvent: false }); + this.kycForm.get('bannedEntityMatch')?.disable({ emitEvent: false }); + this.kycForm.get('willfulDefaulter')?.disable({ emitEvent: false }); + this.kycForm.get('ecgcSalFlag')?.disable({ emitEvent: false }); + this.kycForm.get('dormantFlag')?.disable({ emitEvent: false }); + } } } if (changes['calculatedRisk']) { @@ -656,6 +698,11 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy { riskControl.setValue(newRisk.category); riskControl.markAsDirty(); } + + const allowAmlOverride = this.kycForm.get('allowAmlOverride')?.value; + if (!allowAmlOverride) { + this.applyAutoCalculatedAmlFlags(newRisk); + } } else { this.riskCalculationReason = ''; } @@ -870,6 +917,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy { // Risk & Remarks riskCategory: data.riskCategory || 1, allowRiskOverride: this.calculatedRisk ? this.calculatedRisk.category !== (data.riskCategory || 1) : false, + allowAmlOverride: this.calculatedRisk + ? this.calculatedRisk.amlCheckStatus !== (data.amlCheckStatus || 'CLEAR') || + this.calculatedRisk.bannedEntityMatch !== (data.bannedEntityMatch || 'NO') || + this.calculatedRisk.willfulDefaulter !== (data.willfulDefaulter || 'NO') || + this.calculatedRisk.ecgcSalFlag !== (data.ecgcSalFlag || 'NO') || + this.calculatedRisk.dormantFlag !== (data.dormantFlag || 'ACTIVE') + : false, remarks: data.remarks || '', hasPan: !!data.panNumber, // AML & Compliance @@ -1809,6 +1863,21 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy { v.form15Applicable ); } + + private applyAutoCalculatedAmlFlags(risk = this.calculatedRisk): void { + if (!risk) return; + this.kycForm.get('amlCheckStatus')?.setValue(risk.amlCheckStatus || 'CLEAR'); + this.kycForm.get('bannedEntityMatch')?.setValue(risk.bannedEntityMatch || 'NO'); + this.kycForm.get('willfulDefaulter')?.setValue(risk.willfulDefaulter || 'NO'); + this.kycForm.get('ecgcSalFlag')?.setValue(risk.ecgcSalFlag || 'NO'); + this.kycForm.get('dormantFlag')?.setValue(risk.dormantFlag || 'ACTIVE'); + + this.kycForm.get('amlCheckStatus')?.markAsDirty(); + this.kycForm.get('bannedEntityMatch')?.markAsDirty(); + this.kycForm.get('willfulDefaulter')?.markAsDirty(); + this.kycForm.get('ecgcSalFlag')?.markAsDirty(); + this.kycForm.get('dormantFlag')?.markAsDirty(); + } } /** diff --git a/src/app/core/authentication/authentication.service.ts b/src/app/core/authentication/authentication.service.ts index 9d1ae8204..0dea8be9a 100644 --- a/src/app/core/authentication/authentication.service.ts +++ b/src/app/core/authentication/authentication.service.ts @@ -392,6 +392,22 @@ export class AuthenticationService { return roleNames.some((n) => n === 'clerk'); } + /** Returns true when the logged-in user has the "Principal Officer" role (STR conclusion/filing authority). */ + isPrincipalOfficerUser(): boolean { + const creds = this.getCredentials(); + if (!creds || !creds.roles) return false; + const roleNames: string[] = (creds.roles as any[]).map((r) => (r.name || r.role || '').toLowerCase().trim()); + return roleNames.some((n) => n === 'principal officer'); + } + + /** Returns true when the logged-in user has the "AML Officer" role (first-level alert review). */ + isAmlOfficerUser(): boolean { + const creds = this.getCredentials(); + if (!creds || !creds.roles) return false; + const roleNames: string[] = (creds.roles as any[]).map((r) => (r.name || r.role || '').toLowerCase().trim()); + return roleNames.some((n) => n === 'aml officer'); + } + /** * Returns true ONLY for regular cashier role (not Head Cashier). * Regular cashier → full-screen transaction entry, no nav. diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html index bfb5c140c..c9f0b0494 100644 --- a/src/app/core/shell/shell.component.html +++ b/src/app/core/shell/shell.component.html @@ -1167,10 +1167,6 @@ -
@@ -1571,7 +1567,8 @@ CTR / STR @@ -1668,8 +1665,13 @@ > Daily Position Notice - - R/SLR + + AML
diff --git a/src/app/core/shell/shell.component.ts b/src/app/core/shell/shell.component.ts index cd6cb79e8..3d0ba7fa9 100644 --- a/src/app/core/shell/shell.component.ts +++ b/src/app/core/shell/shell.component.ts @@ -280,23 +280,6 @@ export class ShellComponent implements OnInit, OnDestroy { }); } - /** - * Opens the CTR & STR Compliance Center dialog. - */ - async openCtrStrDialog(): Promise { - const { CtrStrDialogComponent } = await import('../../home/dashboard/ctr-str-dialog/ctr-str-dialog.component'); - this.dialog.open(CtrStrDialogComponent, { - width: '95vw', - maxWidth: '1280px', - maxHeight: '92vh', - panelClass: 'crr-slr-dialog-panel', - autoFocus: false, - disableClose: false, - hasBackdrop: true, - backdropClass: 'ckyc-rpt-dlg-backdrop' - }); - } - /** * Opens the CIMS / XBRL Returns dialog. The initial form (OSS-2, PSA, RAL, RCP-UCB) * is selected based on the menu link the user clicked; user can switch between diff --git a/src/app/fiu/fiu.component.html b/src/app/fiu/fiu.component.html index 58ac6a00f..c6a1a6439 100644 --- a/src/app/fiu/fiu.component.html +++ b/src/app/fiu/fiu.component.html @@ -129,9 +129,130 @@ @if (activeTab === 'ctr') {
+
-

CTR — Cash Transaction Report

+

Live Cash Alerts

+

+ Raised by the transaction-monitoring rule engine — review and whitelist or escalate to Principal Officer +

+
+ +
+
+ + + + + + + + + + + + + + + + @for (alert of amlAlerts; track alert.id) { + + + + + + + + + + + + } @empty { + + + + } + +
IDClientTypeRuleAmountRaised OnStatusNotesAction
{{ alert.id }}{{ alert.clientDisplayName }}{{ alert.transactionType }}{{ alert.ruleCode }} — {{ alert.ruleDescription }}₹{{ alert.amount | number: '1.0-0' }}{{ alert.raisedOn | date: 'medium' }} + {{ alert.status }} + + @if (alert.status === 'RAISED' || alert.status === 'UNDER_REVIEW') { + + } @else { + {{ alert.reviewNotes }} + } + + @if (alert.status === 'RAISED' || alert.status === 'UNDER_REVIEW') { +
+ + +
+ } +
No live alerts.
+
+ + +
+
+

Monthly CTR Reports

+

+ Threshold: ₹{{ bank.cashThreshold | number }} per single txn / connected aggregate + per month +

+
+
+ + +
+
+
+ + + + + + + + + + + + @for (r of amlCtrReports; track r.id) { + + + + + + + + } @empty { + + + + } + +
MonthGenerated OnRecordsStatusSubmitted On
{{ r.reportMonth }}{{ r.generatedOn | date: 'medium' }}{{ r.recordCount }} + {{ r.status }} + {{ r.submittedOn ? (r.submittedOn | date: 'medium') : '—' }}
No CTR reports generated yet.
+
+ + +
+
+

FINnet TC2 Export Template

All cash transactions ≥ ₹{{ bank.cashThreshold | number }} (single or aggregate per month per account)

@@ -221,9 +342,107 @@ @if (activeTab === 'str') {
+
-

STR — Suspicious Transaction Report

+

Live STR Cases

+

+ Escalated by an AML Officer; Principal Officer investigates, concludes and files with FIU-IND within 7 + working days +

+
+
+
+ + + + + + + + + + + + + + + @for (sc of amlStrCases; track sc.id) { + + + + + + + + + + + } @empty { + + + + } + +
IDClientCumulative CasesAssigned POConclusionFIU Due DateStatusAction
{{ sc.id }}{{ sc.clientDisplayName }} + {{ sc.cumulativeStrCaseCountForClient }} + {{ sc.assignedPoUsername || '—' }}{{ sc.conclusion || '—' }}{{ sc.fiuDueDate || '—' }} + {{ sc.status }} +
No STR cases yet.
+
+ + @if (selectedStrCase) { +
+
+
+

STR Case #{{ selectedStrCase.id }} — {{ selectedStrCase.clientDisplayName }}

+

+ Alert #{{ selectedStrCase.alertId }} · + {{ selectedStrCase.cumulativeStrCaseCountForClient }} cumulative STR case(s) for this client + @if (selectedStrCase.cumulativeStrCaseCountForClient >= 3) { + Risk category auto-escalated + } +

+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+ + + @if (selectedStrCase.status === 'CONCLUDED_SUSPICIOUS') { +
+ + + +
+ } +
+
+ } + + +
+
+

FINnet STR XML Export Template

Detected by AML rules engine + branch compliance officers; filed within 7 days

-
- - -
- - -
- - -
- - @if (activeTab === 'dashboard') { -
- -
-
-
-

Cash Transaction Reports (CTR - Monthly Cash Registry)

-

- Scans and compiles cash transactions exceeding INR 10 Lakhs limit (Single or monthly cumulative cash - deposits/withdrawals). -

-
- -
- -
- -
- -
- - - - - - - - - - - - - - @for (report of filteredCtrReports; track report.id) { - - - - - - - - - - } @empty { - - - - } - -
Report Payload NameReporting PeriodRecord CountGenerated ByGenerated DateStatusActions
{{ report.name }}{{ report.month }} - {{ report.recordCount }} txns - {{ report.generatedBy }}{{ report.generatedAt | date: 'medium' }}Ready - -
No CTR reports found matching your search.
-
-
- - -
-
-
-

Suspicious Transaction Activity Pipeline (STR Management)

-

- Behavioral & structural AML alerts generated by KYC profile rules engine. Investigator actions update - state in real-time. -

-
- -
- -
- -
- -
- @for (alert of filteredStrAlerts; track alert.id) { -
-
-
-

{{ alert.clientName }}

- {{ alert.clientCif }} -
- {{ - alert.status - }} -
- -
-
- Amount Highlighted: - ₹{{ alert.amount | number: '1.2-2' }} -
-
- Alert Flag: - {{ alert.alertType }} -
-

{{ alert.description }}

- Flagged Date: {{ alert.date | date: 'mediumDate' }} -
- - -
- } @empty { -
No Suspicious Transaction alerts matching your criteria.
- } -
-
-
- } - - - @if (activeTab === 'guidelines') { -
-
- -
-
- CTR - Cash Transaction Report -
-
-

- As mandated by Section 12 of the **Prevention of Money Laundering Act (PMLA), 2002** and RBI KYC - guidelines, banks must record and report all high-value cash transactions. -

- -
-
1. Transaction Threshold
-

- All cash transactions (either deposits or withdrawals) of **value more than INR 10 Lakhs** (or its - equivalent in foreign currency) in a single transaction. -

-
- -
-
2. Monthly Aggregation Rule (Structuring check)
-

- A series of cash transactions in a calendar month that are **integrally connected** and accumulate to - a total value exceeding **INR 10 Lakhs** must be compiled and reported in the monthly CTR. -

-
- -
-
3. Reporting Timeline
-

- CTRs must be generated and uploaded to the FIU-IND gateway **by the 15th day of the succeeding month** - (e.g., June transactions must be uploaded by July 15th). -

-
- -
- Important Note: Transactions where the net cash balance does not change but high volume - is rotated (e.g., cash withdrawals immediately deposited back) must be reported under CTR as separate - debit and credit records. -
-
-
- - -
-
- STR - Suspicious Transaction Report -
-
-

- Every reporting entity must maintain vigilance over account transactions. If there are reasonable - grounds to suspect transactions involve proceeds of crime, an STR must be generated. -

- -
-
1. What Constitutes Suspicion?
-

- Transactions that are complex, show unusual patterns, mismatch the customer’s business/risk profile, - have no apparent economic rationale, or involve suspected proceeds of crime or terror funding. -

-
- -
-
2. No Minimum Threshold Limit
-

- **There is no minimum monetary limit** for STR. An alert should be generated for any transaction, - regardless of size, if suspicious behavior is flagged. -

-
- -
-
3. Reporting Timeline
-

- The STR must be filed to FIU-IND **within 7 days** of the Principal Compliance Officer confirming the - suspicion. -

-
- -
- Strict Tipping-off Restriction: It is illegal to disclose or "tip-off" to the client - that their account is being monitored, investigated, or reported under an STR. Complete confidentiality - must be maintained. -
-
-
-
-
- } -
-
diff --git a/src/app/home/dashboard/ctr-str-dialog/ctr-str-dialog.component.scss b/src/app/home/dashboard/ctr-str-dialog/ctr-str-dialog.component.scss deleted file mode 100644 index bddcb23cb..000000000 --- a/src/app/home/dashboard/ctr-str-dialog/ctr-str-dialog.component.scss +++ /dev/null @@ -1,750 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -.ctr-str-dlg { - display: flex; - flex-direction: column; - height: 90vh; - max-height: 820px; - background: #f8fafc; - color: #1e293b; - font-family: Inter, sans-serif; - overflow: hidden; -} - -// Header -.dlg-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 24px; - background: #fff; - border-bottom: 1px solid #e2e8f0; - - .dlg-title-area { - display: flex; - align-items: center; - gap: 14px; - - .header-icon { - font-size: 26px; - color: #4f46e5; - } - - .title { - margin: 0; - font-size: 18px; - font-weight: 700; - color: #0f172a; - } - - .subtitle { - margin: 2px 0 0; - font-size: 12px; - color: #64748b; - } - } - - .close-btn { - color: #64748b; - - &:hover { - color: #0f172a; - } - } -} - -// Navigation Tabs -.tab-navs { - display: flex; - gap: 6px; - background: #fff; - padding: 8px 24px; - border-bottom: 1px solid #e2e8f0; - - .tab-btn { - display: flex; - align-items: center; - gap: 8px; - background: transparent; - border: none; - padding: 10px 16px; - font-size: 13px; - font-weight: 600; - color: #64748b; - cursor: pointer; - border-radius: 6px; - transition: all 0.2s ease; - - .tab-icon { - font-size: 14px; - } - - &:hover { - background: #f1f5f9; - color: #0f172a; - } - - &.active { - background: #e0e7ff; - color: #4f46e5; - } - } -} - -// Scroll layout -.scroll-y { - overflow-y: auto; - flex: 1; -} - -// Dialog Body -.dlg-body { - flex: 1; - overflow: hidden; - padding: 24px; - background: #f1f5f9; - display: flex; - flex-direction: column; -} - -.tab-content { - height: 100%; - display: flex; - flex-direction: column; - - &.scroll-y { - overflow-y: auto; - padding-right: 6px; - - /* Custom scrollbar */ - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background: #cbd5e1; - border-radius: 3px; - - &:hover { - background: #94a3b8; - } - } - } -} - -// Dashboard Tab specific layout -.dashboard-tab { - overflow-y: auto; - padding-right: 6px; - - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background: #cbd5e1; - border-radius: 3px; - } -} - -// Card Style Container -.card-glass { - background: #fff; - border-radius: 12px; - box-shadow: - 0 4px 6px -1px rgb(0 0 0 / 5%), - 0 2px 4px -1px rgb(0 0 0 / 2%); - border: 1px solid #e2e8f0; - padding: 20px; -} - -.dashboard-section { - display: flex; - flex-direction: column; - - .section-hdr { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - - h3 { - margin: 0; - font-size: 15px; - font-weight: 700; - color: #0f172a; - } - - .section-sub { - margin: 4px 0 0; - font-size: 12px; - color: #64748b; - } - } -} - -.mt-20 { - margin-top: 20px; -} - -.mt-25 { - margin-top: 25px; -} - -// Search input -.table-actions { - margin-bottom: 14px; - - .search-input { - width: 100%; - max-width: 380px; - padding: 8px 12px; - font-size: 13px; - border: 1px solid #cbd5e1; - border-radius: 6px; - background: #fff; - color: #1e293b; - outline: none; - - &:focus { - border-color: #4f46e5; - box-shadow: 0 0 0 2px rgb(79 70 229 / 10%); - } - } -} - -// Button styles -.action-btn { - display: inline-flex; - align-items: center; - gap: 8px; - border: none; - border-radius: 6px; - padding: 10px 16px; - font-size: 12px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - - &.primary { - background: #4f46e5; - color: #fff; - - &:hover:not([disabled]) { - background: #4338ca; - } - - &:disabled { - background: #94a3b8; - cursor: not-allowed; - } - } - - &.secondary { - background: #0ea5e9; - color: #fff; - - &:hover { - background: #0284c7; - } - } -} - -// Spinner animation -.spinner { - width: 12px; - height: 12px; - border: 2px solid rgb(255 255 255 / 30%); - border-radius: 50%; - border-top-color: #fff; - animation: spin 0.8s linear infinite; - display: inline-block; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -// Tables styling -.table-container { - overflow-x: auto; - border: 1px solid #e2e8f0; - border-radius: 8px; -} - -.compliance-table { - width: 100%; - border-collapse: collapse; - font-size: 13px; - text-align: left; - - th { - background: #f8fafc; - color: #475569; - font-weight: 600; - padding: 12px 16px; - border-bottom: 1px solid #e2e8f0; - } - - td { - padding: 12px 16px; - border-bottom: 1px solid #f1f5f9; - color: #334155; - - &.bold-text { - font-weight: 600; - color: #0f172a; - } - - &.center-text { - text-align: center; - } - } - - tr:last-child td { - border-bottom: none; - } - - .cnt-badge { - background: #e2e8f0; - color: #475569; - padding: 3px 8px; - border-radius: 12px; - font-size: 11px; - font-weight: 500; - } - - .xml-icon { - color: #ea580c; - margin-right: 4px; - } - - .empty-state { - text-align: center; - color: #64748b; - padding: 24px; - font-style: italic; - } -} - -// Icon Action Button -.icon-action-btn { - background: #f1f5f9; - border: 1px solid #cbd5e1; - color: #475569; - border-radius: 4px; - padding: 6px 12px; - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - - &:hover { - background: #e2e8f0; - color: #0f172a; - } -} - -// Alert Cards Grid -.alerts-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 16px; -} - -.alert-card { - background: #fff; - border: 1px solid #e2e8f0; - border-radius: 8px; - padding: 16px; - display: flex; - flex-direction: column; - transition: box-shadow 0.2s ease; - - &:hover { - box-shadow: 0 4px 12px rgb(0 0 0 / 5%); - } - - // Left boundary color codes based on status - &.open { - border-left: 4px solid #dc2626; - } - - &.investigating { - border-left: 4px solid #ea580c; - } - - &.reported-str { - border-left: 4px solid #16a34a; - } - - &.closed { - border-left: 4px solid #64748b; - } - - .card-hdr { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 12px; - - h4 { - margin: 0; - font-size: 14px; - font-weight: 700; - color: #0f172a; - } - - .cif-number { - font-size: 11px; - color: #64748b; - } - } - - .card-body { - flex: 1; - font-size: 12px; - color: #475569; - - .amount-line, - .type-line { - display: flex; - justify-content: space-between; - margin-bottom: 4px; - - .label { - color: #64748b; - } - - .amount { - font-weight: 700; - color: #0f172a; - } - - .type-value { - font-weight: 600; - color: #4f46e5; - } - } - - .desc { - margin: 8px 0; - line-height: 1.4; - background: #f8fafc; - padding: 8px; - border-radius: 4px; - border: 1px dashed #e2e8f0; - font-style: italic; - } - - .date-stamp { - font-size: 11px; - color: #94a3b8; - display: block; - margin-top: 8px; - } - } - - .card-footer { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-top: 14px; - border-top: 1px solid #f1f5f9; - padding-top: 12px; - - .card-btn { - flex: 1; - min-width: 75px; - padding: 6px 8px; - font-size: 11px; - font-weight: 600; - border: none; - border-radius: 4px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - gap: 4px; - transition: all 0.2s ease; - - &.investigate { - background: #fef3c7; - color: #d97706; - - &:hover { - background: #fde68a; - } - } - - &.file-str { - background: #dcfce7; - color: #16a34a; - - &:hover { - background: #bbf7d0; - } - } - - &.close-alert { - background: #fee2e2; - color: #dc2626; - - &:hover { - background: #fecaca; - } - } - - &.reset-alert { - background: #f1f5f9; - color: #475569; - border: 1px solid #cbd5e1; - - &:hover { - background: #e2e8f0; - } - } - } - } -} - -// Badges -.status-badge { - padding: 3px 8px; - border-radius: 12px; - font-size: 10px; - font-weight: 600; - - &.ready { - background: #dcfce7; - color: #16a34a; - } - - &.open { - background: #fee2e2; - color: #dc2626; - } - - &.investigating { - background: #fef3c7; - color: #d97706; - } - - &.reported-str { - background: #dbeafe; - color: #2563eb; - } - - &.closed { - background: #f1f5f9; - color: #475569; - } -} - -.empty-alerts { - grid-column: 1 / -1; - text-align: center; - padding: 36px; - background: #fff; - border: 1px solid #e2e8f0; - border-radius: 8px; - color: #64748b; - font-style: italic; - font-size: 13px; -} - -// Tab 2: Guidelines layout -.info-layout { - display: grid; - grid-template-columns: 1fr; - gap: 20px; - - @media (width >= 900px) { - grid-template-columns: 1fr 1fr; - } -} - -.info-card { - display: flex; - flex-direction: column; - - .card-title-badge { - align-self: flex-start; - display: inline-flex; - align-items: center; - gap: 8px; - padding: 6px 14px; - border-radius: 20px; - font-size: 12px; - font-weight: 700; - margin-bottom: 16px; - text-transform: uppercase; - letter-spacing: 0.5px; - color: #fff; - } - - .ctr-badge-color { - background: #ea580c; - } - - .str-badge-color { - background: #be185d; - } - - .rules-body { - font-size: 13px; - line-height: 1.5; - color: #334155; - - .lead-desc { - font-size: 14px; - color: #0f172a; - margin-bottom: 20px; - } - - .rule-point { - margin-bottom: 18px; - - h5 { - margin: 0 0 6px; - font-size: 13px; - font-weight: 700; - color: #0f172a; - } - - p { - margin: 0; - color: #475569; - } - } - - .text-warning-border { - border-left: 3px solid #eab308; - background: #fef9c3; - padding: 10px 14px; - border-radius: 0 6px 6px 0; - color: #854d0e; - } - - .text-danger-border { - border-left: 3px solid #ef4444; - background: #fee2e2; - padding: 10px 14px; - border-radius: 0 6px 6px 0; - color: #991b1b; - } - } -} - -// Tab 3: Architecture & Schema -.arch-container { - display: flex; - flex-direction: column; - - h3 { - margin: 0; - font-size: 16px; - font-weight: 700; - color: #0f172a; - } - - .desc { - margin: 4px 0 20px; - font-size: 12px; - color: #64748b; - } -} - -.arch-section { - h4 { - margin: 0 0 4px; - font-size: 14px; - font-weight: 700; - color: #0f172a; - } - - .section-sub { - margin: 0 0 10px; - font-size: 12px; - color: #64748b; - } -} - -.code-snippet { - background: #0f172a; - color: #e2e8f0; - padding: 14px; - border-radius: 8px; - font-family: 'Courier New', Courier, monospace; - font-size: 12px; - overflow-x: auto; - line-height: 1.4; - margin: 0; - - &.plain { - background: #1e293b; - color: #94a3b8; - } -} - -// API Routes -.route-item { - display: flex; - align-items: center; - gap: 12px; - padding: 8px 12px; - background: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 6px; - margin-bottom: 8px; - font-size: 12px; - - .method { - padding: 3px 8px; - border-radius: 4px; - font-size: 10px; - font-weight: 700; - color: #fff; - min-width: 45px; - text-align: center; - - &.get { - background: #16a34a; - } - - &.put { - background: #ea580c; - } - - &.post { - background: #2563eb; - } - } - - .path { - font-family: 'Courier New', Courier, monospace; - font-weight: 700; - color: #0f172a; - } - - .route-desc { - color: #475569; - flex: 1; - } -} diff --git a/src/app/home/dashboard/ctr-str-dialog/ctr-str-dialog.component.ts b/src/app/home/dashboard/ctr-str-dialog/ctr-str-dialog.component.ts deleted file mode 100644 index 5af39f110..000000000 --- a/src/app/home/dashboard/ctr-str-dialog/ctr-str-dialog.component.ts +++ /dev/null @@ -1,279 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { Component, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { MatDialogRef, MatDialogModule } from '@angular/material/dialog'; -import { MatButtonModule } from '@angular/material/button'; -import { MatTabsModule } from '@angular/material/tabs'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatIconModule } from '@angular/material/icon'; -import { FaIconComponent, FaIconLibrary } from '@fortawesome/angular-fontawesome'; -import { - faShieldAlt, - faChartBar, - faUniversity, - faCode, - faSyncAlt, - faFileExcel, - faFileCode, - faDownload, - faClock, - faSearch, - faFlag, - faBan, - faRedoAlt, - faCoins -} from '@fortawesome/free-solid-svg-icons'; - -interface CtrReport { - id: number; - name: string; - month: string; - recordCount: number; - generatedBy: string; - generatedAt: Date; - status: 'Ready' | 'Generating'; -} - -interface StrAlert { - id: number; - clientName: string; - clientCif: string; - alertType: string; - amount: number; - status: 'Open' | 'Investigating' | 'Reported STR' | 'Closed'; - description: string; - date: Date; -} - -@Component({ - selector: 'mifosx-ctr-str-dialog', - standalone: true, - templateUrl: './ctr-str-dialog.component.html', - styleUrls: ['./ctr-str-dialog.component.scss'], - imports: [ - CommonModule, - FormsModule, - MatDialogModule, - MatButtonModule, - MatTabsModule, - MatTooltipModule, - MatIconModule, - FaIconComponent - ] -}) -export class CtrStrDialogComponent { - dialogRef = inject(MatDialogRef); - - constructor() { - const library = inject(FaIconLibrary); - library.addIcons( - faShieldAlt, - faChartBar, - faUniversity, - faCode, - faSyncAlt, - faFileExcel, - faFileCode, - faDownload, - faClock, - faSearch, - faFlag, - faBan, - faRedoAlt, - faCoins - ); - } - - activeTab: 'dashboard' | 'guidelines' = 'dashboard'; - - // Mock State - isGeneratingCtr = false; - ctrFilter = ''; - strFilter = ''; - - ctrReports: CtrReport[] = [ - { - id: 1, - name: 'CTR_REPORT_JUNE_2026.xml', - month: 'June 2026', - recordCount: 14, - generatedBy: 'Compliance_Officer_1', - generatedAt: new Date(2026, 6, 5, 10, 30), - status: 'Ready' - }, - { - id: 2, - name: 'CTR_REPORT_MAY_2026.xml', - month: 'May 2026', - recordCount: 22, - generatedBy: 'Compliance_Officer_1', - generatedAt: new Date(2026, 5, 4, 14, 15), - status: 'Ready' - }, - { - id: 3, - name: 'CTR_REPORT_APRIL_2026.xml', - month: 'April 2026', - recordCount: 9, - generatedBy: 'Compliance_Officer_2', - generatedAt: new Date(2026, 4, 3, 11, 45), - status: 'Ready' - } - ]; - - strAlerts: StrAlert[] = [ - { - id: 101, - clientName: 'Aarav Sharma', - clientCif: 'CIF982348', - alertType: 'Profile Mismatch', - amount: 450000, - status: 'Open', - description: - 'Student account received a large inward transfer (INR 4.5L) mismatching declared zero/student income category.', - date: new Date(2026, 6, 26) - }, - { - id: 102, - clientName: 'Karan Malhotra', - clientCif: 'CIF229482', - alertType: 'Structuring (Smurfing)', - amount: 980000, - status: 'Investigating', - description: - 'Multiple cash deposits of INR 2.45L, 2.45L, 2.45L, 2.45L within 10 days, totaling INR 9.8L cash (deliberately staying below the 10L single cash transaction CTR threshold).', - date: new Date(2026, 6, 24) - }, - { - id: 103, - clientName: 'Nexora Real Estate Ltd', - clientCif: 'CIF500481', - alertType: 'Turnover Outlier', - amount: 15000000, - status: 'Open', - description: - 'Entity account registered an outward wire of INR 1.5Cr when maximum monthly turnover limit is set to INR 50L.', - date: new Date(2026, 6, 20) - }, - { - id: 104, - clientName: 'Priyanka Patel', - clientCif: 'CIF774920', - alertType: 'Dormant Account Reactivation', - amount: 600000, - status: 'Reported STR', - description: - 'Account inactive for 18 months suddenly received a cash deposit of INR 6L, followed by immediate online transfers.', - date: new Date(2026, 6, 15) - } - ]; - - get filteredCtrReports() { - return this.ctrReports.filter( - (r) => - r.name.toLowerCase().includes(this.ctrFilter.toLowerCase()) || - r.month.toLowerCase().includes(this.ctrFilter.toLowerCase()) - ); - } - - get filteredStrAlerts() { - return this.strAlerts.filter( - (a) => - a.clientName.toLowerCase().includes(this.strFilter.toLowerCase()) || - a.clientCif.toLowerCase().includes(this.strFilter.toLowerCase()) || - a.alertType.toLowerCase().includes(this.strFilter.toLowerCase()) - ); - } - - // Actions - generateCtrReport() { - if (this.isGeneratingCtr) { - return; - } - this.isGeneratingCtr = true; - - // Simulate API call to Spring Boot batch job - setTimeout(() => { - const date = new Date(); - const newReport: CtrReport = { - id: this.ctrReports.length + 1, - name: `CTR_REPORT_JULY_2026_DRAFT.xml`, - month: 'July 2026 (Draft)', - recordCount: 8, - generatedBy: 'Current_Admin', - generatedAt: date, - status: 'Ready' - }; - this.ctrReports.unshift(newReport); - this.isGeneratingCtr = false; - }, 2000); - } - - downloadCtrFile(reportName: string) { - const dummyXml = ` - -
- CTR - CBS Nexora Bank - 2026-06 -
- - - TXN88924824 - 2026-06-15 - CASH - 1250000.00 - SAVINGS - Aarav Sharma - CIF982348 - - -
`; - - const blob = new Blob([dummyXml], { type: 'application/xml' }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = reportName; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - } - - changeAlertStatus(alertId: number, newStatus: 'Open' | 'Investigating' | 'Reported STR' | 'Closed') { - const alert = this.strAlerts.find((a) => a.id === alertId); - if (alert) { - alert.status = newStatus; - } - } - - downloadStrReportTemplate() { - const dummyCsv = - `AlertID,ClientName,CIF,AlertType,Amount,Status,Description\n` + - this.strAlerts - .map( - (a) => - `"${a.id}","${a.clientName}","${a.clientCif}","${a.alertType}","${a.amount}","${a.status}","${a.description.replace(/"/g, '""')}"` - ) - .join('\n'); - - const blob = new Blob([dummyCsv], { type: 'text/csv' }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `STR_AML_ALERTS_EXPORT.csv`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - } -} diff --git a/src/app/rbi-circulars/aml-rules/aml-rules.component.html b/src/app/rbi-circulars/aml-rules/aml-rules.component.html new file mode 100644 index 000000000..6726b0a4d --- /dev/null +++ b/src/app/rbi-circulars/aml-rules/aml-rules.component.html @@ -0,0 +1,87 @@ + + +
+ + +
+ shield +
+ AML Rules Master + + Database-driven rules for client risk categorization and AML compliance checks + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rule Srno{{ element.orderPosition }}Rule Description{{ element.ruleName }}Risk Category + + + {{ getRiskCategoryLabel(element.riskCategory) }} + + Rule Reason / Criteria{{ element.reason }}AML Check Status + + {{ element.amlCheckStatus || 'CLEAR' }} + + Banned Match + + {{ element.bannedEntityMatch || 'NO' }} + +
+ + + + +
+ info_outline + No AML rules found in the database. +
+
+
+
diff --git a/src/app/rbi-circulars/aml-rules/aml-rules.component.scss b/src/app/rbi-circulars/aml-rules/aml-rules.component.scss new file mode 100644 index 000000000..ca8f739c5 --- /dev/null +++ b/src/app/rbi-circulars/aml-rules/aml-rules.component.scss @@ -0,0 +1,228 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +.aml-rules-container { + padding: 24px; + background-color: var(--bg-neutral-light, #f8fafc); + min-height: calc(100vh - 64px); +} + +.aml-card { + border-radius: 12px; + box-shadow: + 0 4px 20px -2px rgb(0 0 0 / 5%), + 0 2px 8px -1px rgb(0 0 0 / 3%); + border: 1px solid rgb(226 232 240 / 80%); + background: #fff; + overflow: hidden; +} + +.aml-card-header { + background: linear-gradient(135deg, #1e293b, #0f172a); + color: #fff; + padding: 24px; +} + +.aml-header-content { + display: flex; + align-items: center; + gap: 16px; + + .header-icon { + font-size: 32px; + width: 32px; + height: 32px; + color: #38bdf8; + } + + .header-title { + font-size: 20px; + font-weight: 600; + letter-spacing: -0.02em; + color: #fff; + margin-bottom: 4px; + } + + .header-subtitle { + font-size: 13px; + color: #94a3b8; + } +} + +.aml-card-content { + padding: 24px; +} + +.aml-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + border-radius: 8px; + overflow: hidden; + border: 1px solid #e2e8f0; + + th { + background-color: #f1f5f9; + color: #475569; + font-weight: 600; + font-size: 13px; + padding: 16px; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid #cbd5e1; + } + + td { + padding: 16px; + font-size: 14px; + color: #334155; + border-bottom: 1px solid #e2e8f0; + } + + .rule-row { + transition: background-color 0.2s ease; + + &:hover { + background-color: #f8fafc; + } + + &:last-child td { + border-bottom: none; + } + } +} + +.col-center { + text-align: center; + justify-content: center; +} + +.rule-name-cell { + font-weight: 500; + color: #1e293b; + max-width: 300px; +} + +.reason-cell { + color: #64748b; + font-size: 13px; + max-width: 400px; +} + +/* Badges for Risk Category */ +.risk-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 20px; + font-size: 12px; + font-weight: 500; + + .dot { + width: 6px; + height: 6px; + border-radius: 50%; + } + + &.risk-high { + background-color: #fef2f2; + color: #ef4444; + border: 1px solid #fee2e2; + + .dot { + background-color: #ef4444; + } + } + + &.risk-medium { + background-color: #fffbeb; + color: #d97706; + border: 1px solid #fef3c7; + + .dot { + background-color: #d97706; + } + } + + &.risk-low { + background-color: #f0fdf4; + color: #16a34a; + border: 1px solid #dcfce7; + + .dot { + background-color: #16a34a; + } + } +} + +/* Badges for Compliance status */ +.compliance-badge { + display: inline-block; + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.05em; + + /* AML Statuses */ + &.status-clear { + background-color: #dcfce7; + color: #15803d; + } + + &.status-flagged { + background-color: #fef3c7; + color: #b45309; + } + + &.status-blocked { + background-color: #fee2e2; + color: #b91c1c; + } + + /* Banned Matches */ + &.match-no { + background-color: #f1f5f9; + color: #475569; + } + + &.match-yes { + background-color: #fee2e2; + color: #b91c1c; + } + + &.match-partial_match { + background-color: #ffedd5; + color: #c2410c; + } +} + +.no-rules-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px; + color: #64748b; + gap: 12px; + + mat-icon { + font-size: 48px; + width: 48px; + height: 48px; + } +} + +.aml-paginator { + background: #fff; + border-top: 1px solid #e2e8f0; + padding: 8px 0; + font-size: 13px; + color: #475569; +} diff --git a/src/app/rbi-circulars/aml-rules/aml-rules.component.ts b/src/app/rbi-circulars/aml-rules/aml-rules.component.ts new file mode 100644 index 000000000..913ca16c5 --- /dev/null +++ b/src/app/rbi-circulars/aml-rules/aml-rules.component.ts @@ -0,0 +1,87 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, OnInit, inject, ViewChild } from '@angular/core'; +import { MatTableModule, MatTableDataSource } from '@angular/material/table'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatPaginatorModule, MatPaginator } from '@angular/material/paginator'; +import { CommonModule } from '@angular/common'; + +/** Custom Services */ +import { ClientsService } from '../../clients/clients.service'; + +@Component({ + selector: 'mifosx-aml-rules', + templateUrl: './aml-rules.component.html', + styleUrls: ['./aml-rules.component.scss'], + standalone: true, + imports: [ + CommonModule, + MatTableModule, + MatCardModule, + MatIconModule, + MatButtonModule, + MatCheckboxModule, + MatPaginatorModule + ] +}) +export class AmlRulesComponent implements OnInit { + private clientsService = inject(ClientsService); + + rules = new MatTableDataSource([]); + displayedColumns: string[] = [ + 'orderPosition', + 'ruleName', + 'riskCategory', + 'reason', + 'amlCheckStatus', + 'bannedEntityMatch' + ]; + + @ViewChild(MatPaginator) paginator!: MatPaginator; + + ngOnInit(): void { + this.clientsService.getClientRiskRules().subscribe({ + next: (data) => { + this.rules.data = data || []; + this.rules.paginator = this.paginator; + }, + error: () => {} + }); + } + + getRiskCategoryLabel(category: number): string { + switch (category) { + case 1: + return 'Low Risk'; + case 2: + return 'Medium Risk'; + case 3: + return 'High Risk'; + default: + return 'Unknown'; + } + } + + getRiskColorClass(category: number): string { + switch (category) { + case 1: + return 'risk-low'; + case 2: + return 'risk-medium'; + case 3: + return 'risk-high'; + default: + return ''; + } + } +} diff --git a/src/app/rbi-circulars/rbi-circulars-routing.module.ts b/src/app/rbi-circulars/rbi-circulars-routing.module.ts index 32bbc0ce3..3cb6f2329 100644 --- a/src/app/rbi-circulars/rbi-circulars-routing.module.ts +++ b/src/app/rbi-circulars/rbi-circulars-routing.module.ts @@ -11,6 +11,7 @@ import { Routes, RouterModule } from '@angular/router'; import { Route } from '../core/route/route.service'; import { RbiCircularsComponent } from './rbi-circulars.component'; +import { AmlRulesComponent } from './aml-rules/aml-rules.component'; const routes: Routes = [ Route.withShell([ @@ -18,6 +19,11 @@ const routes: Routes = [ path: 'rbi-circulars', component: RbiCircularsComponent, data: { title: 'RBI Circulars', breadcrumb: 'RBI Circulars' } + }, + { + path: 'rbi/aml', + component: AmlRulesComponent, + data: { title: 'AML Rules Master', breadcrumb: 'AML Rules Master' } } ]) ];