#!/usr/bin/env python3 """ Regulatory Reporting CLI Commands Generate and manage regulatory compliance reports """ import click import asyncio import json from typing import Optional, List, Dict, Any from datetime import datetime, timedelta # Import regulatory reporting system with robust path resolution import os import sys _services_path = os.environ.get('AITBC_SERVICES_PATH') if _services_path: if os.path.isdir(_services_path): if _services_path not in sys.path: sys.path.insert(0, _services_path) else: print(f"Warning: AITBC_SERVICES_PATH set but not a directory: {_services_path}", file=sys.stderr) else: _project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) _computed_services = os.path.join(_project_root, 'apps', 'coordinator-api', 'src', 'app', 'services') if os.path.isdir(_computed_services) and _computed_services not in sys.path: sys.path.insert(0, _computed_services) else: _fallback = '/home/oib/windsurf/aitbc/apps/coordinator-api/src/app/services' if os.path.isdir(_fallback) and _fallback not in sys.path: sys.path.insert(0, _fallback) try: from regulatory_reporting import ( generate_sar, generate_compliance_summary, list_reports, regulatory_reporter, ReportType, ReportStatus, RegulatoryBody ) _import_error = None except ImportError as e: _import_error = e def _missing(*args, **kwargs): raise ImportError( f"Required service module 'regulatory_reporting' could not be imported: {_import_error}. " "Ensure coordinator-api dependencies are installed or set AITBC_SERVICES_PATH." ) generate_sar = generate_compliance_summary = list_reports = regulatory_reporter = _missing class ReportType: pass class ReportStatus: pass class RegulatoryBody: pass @click.group() def regulatory(): """Regulatory reporting and compliance management commands""" pass @regulatory.command() @click.option("--user-id", required=True, help="User ID for suspicious activity") @click.option("--activity-type", required=True, help="Type of suspicious activity") @click.option("--amount", type=float, required=True, help="Amount involved in USD") @click.option("--description", required=True, help="Description of suspicious activity") @click.option("--risk-score", type=float, default=0.5, help="Risk score (0.0-1.0)") @click.option("--currency", default="USD", help="Currency code") @click.pass_context def generate_sar(ctx, user_id: str, activity_type: str, amount: float, description: str, risk_score: float, currency: str): """Generate Suspicious Activity Report (SAR)""" try: click.echo(f"๐Ÿ” Generating Suspicious Activity Report...") click.echo(f"๐Ÿ‘ค User ID: {user_id}") click.echo(f"๐Ÿ“Š Activity Type: {activity_type}") click.echo(f"๐Ÿ’ฐ Amount: ${amount:,.2f} {currency}") click.echo(f"โš ๏ธ Risk Score: {risk_score:.2f}") # Create suspicious activity data activity = { "id": f"sar_{user_id}_{int(datetime.now().timestamp())}", "timestamp": datetime.now().isoformat(), "user_id": user_id, "type": activity_type, "description": description, "amount": amount, "currency": currency, "risk_score": risk_score, "indicators": [activity_type, "high_risk"], "evidence": {"cli_generated": True} } # Generate SAR result = asyncio.run(generate_sar([activity])) click.echo(f"\nโœ… SAR Report Generated Successfully!") click.echo(f"๐Ÿ“‹ Report ID: {result['report_id']}") click.echo(f"๐Ÿ“„ Report Type: {result['report_type'].upper()}") click.echo(f"๐Ÿ“Š Status: {result['status'].title()}") click.echo(f"๐Ÿ“… Generated: {result['generated_at']}") # Show next steps click.echo(f"\n๐Ÿ“ Next Steps:") click.echo(f" 1. Review the generated report") click.echo(f" 2. Submit to regulatory body when ready") click.echo(f" 3. Maintain records for 5 years (BSA requirement)") except Exception as e: click.echo(f"โŒ SAR generation failed: {e}", err=True) @regulatory.command() @click.option("--period-start", required=True, help="Start date (YYYY-MM-DD)") @click.option("--period-end", required=True, help="End date (YYYY-MM-DD)") @click.pass_context def compliance_summary(ctx, period_start: str, period_end: str): """Generate comprehensive compliance summary report""" try: # Parse dates start_date = datetime.strptime(period_start, "%Y-%m-%d") end_date = datetime.strptime(period_end, "%Y-%m-%d") click.echo(f"๐Ÿ“Š Generating Compliance Summary...") click.echo(f"๐Ÿ“… Period: {period_start} to {period_end}") click.echo(f"๐Ÿ“ˆ Duration: {(end_date - start_date).days} days") # Generate compliance summary result = asyncio.run(generate_compliance_summary( start_date.isoformat(), end_date.isoformat() )) click.echo(f"\nโœ… Compliance Summary Generated!") click.echo(f"๐Ÿ“‹ Report ID: {result['report_id']}") click.echo(f"๐Ÿ“Š Overall Compliance Score: {result['overall_score']:.1%}") click.echo(f"๐Ÿ“… Generated: {result['generated_at']}") # Get detailed report content report = regulatory_reporter._find_report(result['report_id']) if report: content = report.content click.echo(f"\n๐Ÿ“ˆ Executive Summary:") exec_summary = content.get('executive_summary', {}) click.echo(f" Critical Issues: {exec_summary.get('critical_issues', 0)}") click.echo(f" Regulatory Filings: {exec_summary.get('regulatory_filings', 0)}") click.echo(f"\n๐Ÿ‘ฅ KYC Compliance:") kyc = content.get('kyc_compliance', {}) click.echo(f" Total Customers: {kyc.get('total_customers', 0):,}") click.echo(f" Verified Customers: {kyc.get('verified_customers', 0):,}") click.echo(f" Completion Rate: {kyc.get('completion_rate', 0):.1%}") click.echo(f"\n๐Ÿ” AML Compliance:") aml = content.get('aml_compliance', {}) click.echo(f" Transaction Monitoring: {'โœ… Active' if aml.get('transaction_monitoring') else 'โŒ Inactive'}") click.echo(f" SARs Filed: {aml.get('suspicious_activity_reports', 0)}") click.echo(f" CTRs Filed: {aml.get('currency_transaction_reports', 0)}") except Exception as e: click.echo(f"โŒ Compliance summary generation failed: {e}", err=True) @regulatory.command() @click.option("--report-type", type=click.Choice(['sar', 'ctr', 'aml_report', 'compliance_summary']), help="Filter by report type") @click.option("--status", type=click.Choice(['draft', 'pending_review', 'submitted', 'accepted', 'rejected']), help="Filter by status") @click.option("--limit", type=int, default=20, help="Maximum number of reports to show") @click.pass_context def list(ctx, report_type: str, status: str, limit: int): """List regulatory reports""" try: click.echo(f"๐Ÿ“‹ Regulatory Reports") reports = list_reports(report_type, status) if not reports: click.echo(f"โœ… No reports found") return click.echo(f"\n๐Ÿ“Š Total Reports: {len(reports)}") if report_type: click.echo(f"๐Ÿ” Filtered by type: {report_type.upper()}") if status: click.echo(f"๐Ÿ” Filtered by status: {status.title()}") # Display reports for i, report in enumerate(reports[:limit]): status_icon = { "draft": "๐Ÿ“", "pending_review": "โณ", "submitted": "๐Ÿ“ค", "accepted": "โœ…", "rejected": "โŒ" }.get(report['status'], "โ“") click.echo(f"\n{status_icon} Report #{i+1}") click.echo(f" ID: {report['report_id']}") click.echo(f" Type: {report['report_type'].upper()}") click.echo(f" Body: {report['regulatory_body'].upper()}") click.echo(f" Status: {report['status'].title()}") click.echo(f" Generated: {report['generated_at'][:19]}") if len(reports) > limit: click.echo(f"\n... and {len(reports) - limit} more reports") except Exception as e: click.echo(f"โŒ Failed to list reports: {e}", err=True) @regulatory.command() @click.option("--report-id", required=True, help="Report ID to export") @click.option("--format", type=click.Choice(['json', 'csv', 'xml']), default="json", help="Export format") @click.option("--output", help="Output file path (default: stdout)") @click.pass_context def export(ctx, report_id: str, format: str, output: str): """Export regulatory report""" try: click.echo(f"๐Ÿ“ค Exporting Report: {report_id}") click.echo(f"๐Ÿ“„ Format: {format.upper()}") # Export report content = regulatory_reporter.export_report(report_id, format) if output: with open(output, 'w') as f: f.write(content) click.echo(f"โœ… Report exported to: {output}") else: click.echo(f"\n๐Ÿ“„ Report Content:") click.echo("=" * 60) click.echo(content) click.echo("=" * 60) except Exception as e: click.echo(f"โŒ Export failed: {e}", err=True) @regulatory.command() @click.option("--report-id", required=True, help="Report ID to submit") @click.pass_context def submit(ctx, report_id: str): """Submit report to regulatory body""" try: click.echo(f"๐Ÿ“ค Submitting Report: {report_id}") # Get report details report = regulatory_reporter._find_report(report_id) if not report: click.echo(f"โŒ Report {report_id} not found") return click.echo(f"๐Ÿ“„ Type: {report.report_type.value.upper()}") click.echo(f"๐Ÿข Regulatory Body: {report.regulatory_body.value.upper()}") click.echo(f"๐Ÿ“Š Current Status: {report.status.value.title()}") if report.status != ReportStatus.DRAFT: click.echo(f"โš ๏ธ Report already submitted") return # Submit report success = asyncio.run(regulatory_reporter.submit_report(report_id)) if success: click.echo(f"โœ… Report submitted successfully!") click.echo(f"๐Ÿ“… Submitted: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") click.echo(f"๐Ÿข Submitted to: {report.regulatory_body.value.upper()}") # Show submission details click.echo(f"\n๐Ÿ“‹ Submission Details:") click.echo(f" Report ID: {report_id}") click.echo(f" Regulatory Body: {report.regulatory_body.value}") click.echo(f" Submission Method: Electronic Filing") click.echo(f" Confirmation: Pending") else: click.echo(f"โŒ Report submission failed") except Exception as e: click.echo(f"โŒ Submission failed: {e}", err=True) @regulatory.command() @click.option("--report-id", required=True, help="Report ID to check") @click.pass_context def status(ctx, report_id: str): """Check report status""" try: click.echo(f"๐Ÿ“Š Report Status: {report_id}") report_status = regulatory_reporter.get_report_status(report_id) if not report_status: click.echo(f"โŒ Report {report_id} not found") return status_icon = { "draft": "๐Ÿ“", "pending_review": "โณ", "submitted": "๐Ÿ“ค", "accepted": "โœ…", "rejected": "โŒ" }.get(report_status['status'], "โ“") click.echo(f"\n{status_icon} Report Details:") click.echo(f" ID: {report_status['report_id']}") click.echo(f" Type: {report_status['report_type'].upper()}") click.echo(f" Body: {report_status['regulatory_body'].upper()}") click.echo(f" Status: {report_status['status'].title()}") click.echo(f" Generated: {report_status['generated_at'][:19]}") if report_status['submitted_at']: click.echo(f" Submitted: {report_status['submitted_at'][:19]}") if report_status['expires_at']: click.echo(f" Expires: {report_status['expires_at'][:19]}") # Show next actions based on status click.echo(f"\n๐Ÿ“ Next Actions:") if report_status['status'] == 'draft': click.echo(f" โ€ข Review and edit report content") click.echo(f" โ€ข Submit to regulatory body when ready") elif report_status['status'] == 'submitted': click.echo(f" โ€ข Wait for regulatory body response") click.echo(f" โ€ข Monitor submission status") elif report_status['status'] == 'accepted': click.echo(f" โ€ข Store confirmation records") click.echo(f" โ€ข Update compliance documentation") elif report_status['status'] == 'rejected': click.echo(f" โ€ข Review rejection reasons") click.echo(f" โ€ข Resubmit corrected report") except Exception as e: click.echo(f"โŒ Status check failed: {e}", err=True) @regulatory.command() @click.pass_context def overview(ctx): """Show regulatory reporting overview""" try: click.echo(f"๐Ÿ“Š Regulatory Reporting Overview") all_reports = regulatory_reporter.reports if not all_reports: click.echo(f"๐Ÿ“ No reports generated yet") return # Statistics total_reports = len(all_reports) by_type = {} by_status = {} by_body = {} for report in all_reports: # By type rt = report.report_type.value by_type[rt] = by_type.get(rt, 0) + 1 # By status st = report.status.value by_status[st] = by_status.get(st, 0) + 1 # By regulatory body rb = report.regulatory_body.value by_body[rb] = by_body.get(rb, 0) + 1 click.echo(f"\n๐Ÿ“ˆ Overall Statistics:") click.echo(f" Total Reports: {total_reports}") click.echo(f" Report Types: {len(by_type)}") click.echo(f" Regulatory Bodies: {len(by_body)}") click.echo(f"\n๐Ÿ“‹ Reports by Type:") for report_type, count in sorted(by_type.items()): click.echo(f" {report_type.upper()}: {count}") click.echo(f"\n๐Ÿ“Š Reports by Status:") status_icons = {"draft": "๐Ÿ“", "pending_review": "โณ", "submitted": "๐Ÿ“ค", "accepted": "โœ…", "rejected": "โŒ"} for status, count in sorted(by_status.items()): icon = status_icons.get(status, "โ“") click.echo(f" {icon} {status.title()}: {count}") click.echo(f"\n๐Ÿข Reports by Regulatory Body:") for body, count in sorted(by_body.items()): click.echo(f" {body.upper()}: {count}") # Recent activity recent_reports = sorted(all_reports, key=lambda x: x.generated_at, reverse=True)[:5] click.echo(f"\n๐Ÿ“… Recent Activity:") for report in recent_reports: click.echo(f" {report.generated_at.strftime('%Y-%m-%d %H:%M')} - {report.report_type.value.upper()} ({report.status.value})") # Compliance reminders click.echo(f"\nโš ๏ธ Compliance Reminders:") click.echo(f" โ€ข SAR reports must be filed within 30 days of detection") click.echo(f" โ€ข CTR reports required for transactions over $10,000") click.echo(f" โ€ข Maintain records for minimum 5 years") click.echo(f" โ€ข Annual AML program review required") except Exception as e: click.echo(f"โŒ Overview failed: {e}", err=True) @regulatory.command() @click.pass_context def templates(ctx): """Show available report templates and requirements""" try: click.echo(f"๐Ÿ“‹ Regulatory Report Templates") templates = regulatory_reporter.templates for template_name, template_data in templates.items(): click.echo(f"\n๐Ÿ“„ {template_name.upper()}:") click.echo(f" Format: {template_data['format'].upper()}") click.echo(f" Schema: {template_data['schema']}") click.echo(f" Required Fields ({len(template_data['required_fields'])}):") for field in template_data['required_fields']: click.echo(f" โ€ข {field}") click.echo(f"\n๐Ÿข Regulatory Bodies:") bodies = { "FINCEN": "Financial Crimes Enforcement Network (US Treasury)", "SEC": "Securities and Exchange Commission", "FINRA": "Financial Industry Regulatory Authority", "CFTC": "Commodity Futures Trading Commission", "OFAC": "Office of Foreign Assets Control", "EU_REGULATOR": "European Union Regulatory Authorities" } for body, description in bodies.items(): click.echo(f"\n๐Ÿ›๏ธ {body}:") click.echo(f" {description}") click.echo(f"\n๐Ÿ“ Filing Requirements:") click.echo(f" โ€ข SAR: File within 30 days of suspicious activity detection") click.echo(f" โ€ข CTR: File for cash transactions over $10,000") click.echo(f" โ€ข AML Reports: Quarterly and annual requirements") click.echo(f" โ€ข Compliance Summary: Annual filing requirement") click.echo(f"\nโฐ Filing Deadlines:") click.echo(f" โ€ข SAR: 30 days from detection") click.echo(f" โ€ข CTR: 15 days from transaction") click.echo(f" โ€ข Quarterly AML: Within 30 days of quarter end") click.echo(f" โ€ข Annual Report: Within 90 days of year end") except Exception as e: click.echo(f"โŒ Template display failed: {e}", err=True) @regulatory.command() @click.option("--period-start", default="2026-01-01", help="Start date for test data (YYYY-MM-DD)") @click.option("--period-end", default="2026-01-31", help="End date for test data (YYYY-MM-DD)") @click.pass_context def test(ctx, period_start: str, period_end: str): """Run regulatory reporting test with sample data""" try: click.echo(f"๐Ÿงช Running Regulatory Reporting Test...") click.echo(f"๐Ÿ“… Test Period: {period_start} to {period_end}") # Test SAR generation click.echo(f"\n๐Ÿ“‹ Test 1: SAR Generation") result = asyncio.run(generate_sar([{ "id": "test_sar_001", "timestamp": datetime.now().isoformat(), "user_id": "test_user_123", "type": "unusual_volume", "description": "Test suspicious activity for SAR generation", "amount": 25000, "currency": "USD", "risk_score": 0.75, "indicators": ["volume_spike", "timing_anomaly"], "evidence": {"test": True} }])) click.echo(f" โœ… SAR Generated: {result['report_id']}") # Test compliance summary click.echo(f"\n๐Ÿ“Š Test 2: Compliance Summary") compliance_result = asyncio.run(generate_compliance_summary(period_start, period_end)) click.echo(f" โœ… Compliance Summary: {compliance_result['report_id']}") click.echo(f" ๐Ÿ“ˆ Overall Score: {compliance_result['overall_score']:.1%}") # Test report listing click.echo(f"\n๐Ÿ“‹ Test 3: Report Listing") reports = list_reports() click.echo(f" โœ… Total Reports: {len(reports)}") # Test export if reports: test_report_id = reports[0]['report_id'] click.echo(f"\n๐Ÿ“ค Test 4: Report Export") try: content = regulatory_reporter.export_report(test_report_id, "json") click.echo(f" โœ… Export successful: {len(content)} characters") except Exception as e: click.echo(f" โš ๏ธ Export test failed: {e}") click.echo(f"\n๐ŸŽ‰ Regulatory Reporting Test Complete!") click.echo(f"๐Ÿ“Š All systems operational") click.echo(f"๐Ÿ“ Ready for production use") except Exception as e: click.echo(f"โŒ Test failed: {e}", err=True) if __name__ == "__main__": regulatory()