- Replace hardcoded absolute paths with project-relative resolution - Add AITBC_SERVICES_PATH environment variable override - Wrap service imports in try/except with graceful degradation - Add aiohttp to CLI requirements - Create missing README.md for aitbc-agent-sdk to fix poetry build - Make run_all_tests.sh portable with PROJECT_ROOT calculation - Fix enterprise_integration.py path handling CLI now loads successfully even when coordinator-api services are not installed. Advanced commands fail with helpful hints instead of crashing the entire CLI.
502 lines
21 KiB
Python
Executable File
502 lines
21 KiB
Python
Executable File
#!/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()
|