Files
foxhunt/crates/risk-data/src/compliance.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

990 lines
36 KiB
Rust

//! Compliance Repository
//!
//! Manages regulatory compliance logging for SOX, `MiFID` II, and other financial regulations.
//! Provides comprehensive audit trails, regulatory reporting, and compliance monitoring.
// Allow pedantic lints that would require extensive API changes
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::missing_docs_in_private_items)]
#![allow(clippy::arithmetic_side_effects)]
#![allow(clippy::as_conversions)]
#![allow(missing_docs)]
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use redis::aio::ConnectionManager;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use tracing::info;
use uuid::Uuid;
use crate::{RiskDataError, RiskDataResult};
/// Regulatory frameworks
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "regulatory_framework", rename_all = "snake_case")]
pub enum RegulatoryFramework {
/// Sarbanes-Oxley Act - US corporate financial reporting
Sox,
/// Markets in Financial Instruments Directive II - EU financial markets regulation
MifidII,
/// Dodd-Frank Act - US financial reform legislation
DoddFrank,
/// Basel III regulations - international banking regulations
BaselIII,
/// European Market Infrastructure Regulation - EU derivatives regulation
Emir,
/// Markets in Financial Instruments Regulation - EU market structure regulation
Mifir,
/// General Data Protection Regulation - EU data privacy regulation
Gdpr,
}
/// Compliance event types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "compliance_event_type", rename_all = "snake_case")]
pub enum ComplianceEventType {
/// Trade execution event requiring compliance logging
TradeExecution,
/// Order placement event for audit trail
OrderPlacement,
/// Order cancellation event for regulatory reporting
OrderCancellation,
/// Order modification event for tracking changes
OrderModification,
/// Risk limit breach requiring immediate attention
RiskBreach,
/// Position or exposure limit exceeded
LimitExceeded,
/// Best execution analysis and validation
BestExecutionCheck,
/// Regulatory transaction reporting submission
TransactionReporting,
/// Sensitive data access for audit purposes
DataAccess,
/// System access and authentication events
SystemAccess,
/// Configuration change requiring audit trail
ConfigurationChange,
/// Emergency action taken requiring documentation
EmergencyAction,
}
/// Compliance severity levels
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "compliance_severity", rename_all = "snake_case")]
pub enum ComplianceSeverity {
/// Informational event for audit trail
Info,
/// Warning level requiring monitoring
Warning,
/// Critical event requiring immediate review
Critical,
/// Regulatory breach requiring escalation and reporting
Breach,
}
/// Best execution criteria
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BestExecutionCriteria {
/// Whether price improvement was achieved
pub price_improvement: bool,
/// Whether execution speed was optimal
pub speed_of_execution: bool,
/// Whether execution was likely to occur
pub likelihood_of_execution: bool,
/// Whether order size was considered
pub size_of_order: bool,
/// Whether market impact was minimized
pub market_impact: bool,
/// Whether transaction costs were minimized
pub costs_of_transaction: bool,
}
/// Compliance event record
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct ComplianceEvent {
/// Unique identifier for the compliance event
pub id: Uuid,
/// Regulatory framework this event relates to
pub framework: RegulatoryFramework,
/// Type of compliance event
pub event_type: ComplianceEventType,
/// Severity level of the event
pub severity: ComplianceSeverity,
/// When the event occurred
pub timestamp: DateTime<Utc>,
/// User ID associated with the event
pub user_id: Option<String>,
/// Session ID when the event occurred
pub session_id: Option<String>,
/// Order ID if related to trading
pub order_id: Option<String>,
/// Trade ID if related to execution
pub trade_id: Option<String>,
/// Financial instrument symbol
pub symbol: Option<String>,
/// Quantity involved in the event
pub quantity: Option<Decimal>,
/// Price involved in the event
pub price: Option<Decimal>,
/// Trading venue where event occurred
pub venue: Option<String>,
/// Counterparty involved in the event
pub counterparty: Option<String>,
/// Human-readable description of the event
pub description: String,
/// Additional structured details as JSON
pub details: serde_json::Value,
/// Calculated risk score (0-100)
pub risk_score: Option<Decimal>,
/// Whether remediation action is required
pub remediation_required: bool,
/// Current status of remediation efforts
pub remediation_status: Option<String>,
/// Whether event was reported to regulator
pub reported_to_regulator: bool,
/// Regulator's reference number for the report
pub regulator_reference: Option<String>,
}
/// Transaction reporting record for `MiFID` II
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct TransactionReport {
/// Unique identifier for this transaction report
pub id: Uuid,
/// Trade identifier for the executed transaction
pub trade_id: String,
/// Financial instrument identifier
pub instrument_id: String,
/// International Securities Identification Number
pub isin: Option<String>,
/// Date when the transaction was executed
pub trading_date: DateTime<Utc>,
/// Time when the transaction was executed
pub trading_time: DateTime<Utc>,
/// Quantity of the transaction
pub quantity: Decimal,
/// Price of the transaction
pub price: Decimal,
/// Currency of the transaction (ISO 3-letter code)
pub currency: String,
/// Trading venue where the transaction occurred
pub venue: String,
/// Counterparty identifier for the transaction
pub counterparty_id: String,
/// Person within firm responsible for investment decision
pub investment_decision_within_firm: String,
/// Person within firm responsible for execution decision
pub execution_decision_within_firm: String,
/// Client identification code (if applicable)
pub client_identification: Option<String>,
/// Whether this was a short selling transaction
pub short_selling_indicator: bool,
/// Commodity derivative indicator (if applicable)
pub commodity_derivative_indicator: Option<String>,
/// Whether this is a securities financing transaction
pub securities_financing_transaction_indicator: bool,
/// Waiver indicator for pre-trade transparency
pub waiver_indicator: Option<String>,
/// Timestamp when the report was generated
pub reported_at: DateTime<Utc>,
/// Current status of the report submission
pub report_status: String,
/// Approved Reporting Mechanism reference number
pub arm_reference: Option<String>,
}
/// Best execution report
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct BestExecutionReport {
/// Unique identifier for this best execution report
pub id: Uuid,
/// Order identifier this report relates to
pub order_id: String,
/// Financial instrument symbol
pub symbol: String,
/// Type of order (Market, Limit, Stop, etc.)
pub order_type: String,
/// Quantity of the order
pub quantity: Decimal,
/// Price requested by the client (if applicable)
pub requested_price: Option<Decimal>,
/// Actual execution price achieved
pub execution_price: Decimal,
/// Venue where the order was executed
pub execution_venue: String,
/// Time when the order was executed
pub execution_time: DateTime<Utc>,
/// Price improvement achieved (if any)
pub price_improvement: Option<Decimal>,
/// Execution speed in milliseconds
pub execution_speed_ms: i64,
/// Market impact in basis points
pub market_impact_bps: Option<Decimal>,
/// Total transaction costs in basis points
pub total_costs_bps: Option<Decimal>,
/// Alternative venues that could have been used (JSON)
pub alternative_venues: serde_json::Value,
/// Analysis against best execution criteria (JSON)
pub criteria_analysis: serde_json::Value,
/// Whether best execution was achieved
pub best_execution_achieved: bool,
/// Justification if best execution was not achieved
pub justification: Option<String>,
}
/// Audit trail entry
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct AuditTrail {
/// Unique identifier for this audit trail entry
pub id: Uuid,
/// Timestamp when the action occurred
pub timestamp: DateTime<Utc>,
/// User who performed the action
pub user_id: String,
/// Session identifier when the action occurred
pub session_id: String,
/// Action that was performed
pub action: String,
/// Resource that was accessed or modified
pub resource: String,
/// Specific resource identifier (if applicable)
pub resource_id: Option<String>,
/// IP address of the user
pub ip_address: Option<String>,
/// User agent string from the request
pub user_agent: Option<String>,
/// Whether the action was successful
pub success: bool,
/// Error message if the action failed
pub error_message: Option<String>,
/// State of the resource before the action (JSON)
pub before_state: Option<serde_json::Value>,
/// State of the resource after the action (JSON)
pub after_state: Option<serde_json::Value>,
/// Whether sensitive data was accessed
pub sensitive_data_accessed: bool,
/// Date until which this audit record must be retained
pub retention_required_until: DateTime<Utc>,
}
/// Compliance repository trait
#[async_trait]
pub trait ComplianceRepository: Send + Sync + std::fmt::Debug {
/// Log a compliance event
async fn log_event(&self, event: ComplianceEvent) -> RiskDataResult<()>;
/// Get compliance events within a time range
async fn get_events(
&self,
from: DateTime<Utc>,
to: DateTime<Utc>,
framework: Option<RegulatoryFramework>,
severity: Option<ComplianceSeverity>,
) -> RiskDataResult<Vec<ComplianceEvent>>;
/// Report transaction for `MiFID` II compliance
async fn report_transaction(&self, report: TransactionReport) -> RiskDataResult<()>;
/// Get transaction reports
async fn get_transaction_reports(
&self,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> RiskDataResult<Vec<TransactionReport>>;
/// Log best execution analysis
async fn log_best_execution(&self, report: BestExecutionReport) -> RiskDataResult<()>;
/// Get best execution reports
async fn get_best_execution_reports(
&self,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> RiskDataResult<Vec<BestExecutionReport>>;
/// Create audit trail entry
async fn create_audit_trail(&self, entry: AuditTrail) -> RiskDataResult<()>;
/// Search audit trail
async fn search_audit_trail(
&self,
user_id: Option<String>,
action: Option<String>,
resource: Option<String>,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> RiskDataResult<Vec<AuditTrail>>;
/// Generate regulatory report
async fn generate_regulatory_report(
&self,
framework: RegulatoryFramework,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> RiskDataResult<serde_json::Value>;
/// Check compliance violations
async fn check_violations(&self, trade_id: &str) -> RiskDataResult<Vec<ComplianceEvent>>;
/// Mark event as reported to regulator
async fn mark_reported(&self, event_id: Uuid, reference: String) -> RiskDataResult<()>;
}
/// Compliance repository implementation
#[derive(Clone)]
pub struct ComplianceRepositoryImpl {
db_pool: PgPool,
redis_conn: ConnectionManager,
}
impl std::fmt::Debug for ComplianceRepositoryImpl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ComplianceRepositoryImpl")
.field("db_pool", &"<PgPool>")
.field("redis_conn", &"<ConnectionManager>")
.finish()
}
}
impl ComplianceRepositoryImpl {
pub const fn new(db_pool: PgPool, redis_conn: ConnectionManager) -> Self {
Self {
db_pool,
redis_conn,
}
}
/// Validate compliance event data
fn validate_event(event: &ComplianceEvent) -> RiskDataResult<()> {
if event.description.is_empty() {
return Err(RiskDataError::ComplianceValidation(
"Event description cannot be empty".to_owned(),
));
}
// Specific validations per event type
match event.event_type {
ComplianceEventType::TradeExecution | ComplianceEventType::OrderPlacement => {
if event.order_id.is_none() && event.trade_id.is_none() {
return Err(RiskDataError::ComplianceValidation(
"Trade/order events must have order_id or trade_id".to_owned(),
));
}
},
ComplianceEventType::RiskBreach | ComplianceEventType::LimitExceeded => {
if event.severity != ComplianceSeverity::Critical
&& event.severity != ComplianceSeverity::Breach
{
return Err(RiskDataError::ComplianceValidation(
"Risk breaches must have Critical or Breach severity".to_owned(),
));
}
},
ComplianceEventType::OrderCancellation
| ComplianceEventType::OrderModification
| ComplianceEventType::BestExecutionCheck
| ComplianceEventType::TransactionReporting
| ComplianceEventType::DataAccess
| ComplianceEventType::SystemAccess
| ComplianceEventType::ConfigurationChange
| ComplianceEventType::EmergencyAction => {},
}
Ok(())
}
/// Calculate risk score for compliance event
fn calculate_risk_score(event: &ComplianceEvent) -> Decimal {
let mut score = Decimal::ZERO;
// Base score by severity
score += match event.severity {
ComplianceSeverity::Info => Decimal::from(10_i32),
ComplianceSeverity::Warning => Decimal::from(30_i32),
ComplianceSeverity::Critical => Decimal::from(70_i32),
ComplianceSeverity::Breach => Decimal::from(100_i32),
};
// Additional score by event type
score += match event.event_type {
ComplianceEventType::RiskBreach | ComplianceEventType::LimitExceeded => {
Decimal::from(30_i32)
},
ComplianceEventType::EmergencyAction => Decimal::from(25_i32),
ComplianceEventType::ConfigurationChange => Decimal::from(20_i32),
ComplianceEventType::BestExecutionCheck => Decimal::from(15_i32),
ComplianceEventType::TradeExecution
| ComplianceEventType::OrderPlacement
| ComplianceEventType::OrderCancellation
| ComplianceEventType::OrderModification
| ComplianceEventType::TransactionReporting
| ComplianceEventType::DataAccess
| ComplianceEventType::SystemAccess => {
tracing::warn!("Unknown compliance event type - using minimum severity score");
Decimal::from(1_i32) // Minimum score for unknown event types
},
};
// Framework-specific adjustments
score += match event.framework {
RegulatoryFramework::Sox => Decimal::from(20_i32),
RegulatoryFramework::MifidII => Decimal::from(15_i32),
RegulatoryFramework::DoddFrank => Decimal::from(15_i32),
RegulatoryFramework::BaselIII
| RegulatoryFramework::Emir
| RegulatoryFramework::Mifir
| RegulatoryFramework::Gdpr => {
tracing::warn!("Unknown regulatory framework - using minimum score");
Decimal::from(1_i32) // Minimum score for unknown frameworks
},
};
score.min(Decimal::from(100_i32)) // Cap at 100
}
}
#[async_trait]
impl ComplianceRepository for ComplianceRepositoryImpl {
async fn log_event(&self, mut event: ComplianceEvent) -> RiskDataResult<()> {
Self::validate_event(&event)?;
// Calculate risk score if not provided
if event.risk_score.is_none() {
event.risk_score = Some(Self::calculate_risk_score(&event));
}
let query = "
INSERT INTO compliance_events (
id, framework, event_type, severity, timestamp, user_id, session_id,
order_id, trade_id, symbol, quantity, price, venue, counterparty,
description, details, risk_score, remediation_required,
remediation_status, reported_to_regulator, regulator_reference
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
";
sqlx::query(query)
.bind(event.id)
.bind(event.framework)
.bind(event.event_type)
.bind(event.severity)
.bind(event.timestamp)
.bind(&event.user_id)
.bind(&event.session_id)
.bind(&event.order_id)
.bind(&event.trade_id)
.bind(&event.symbol)
.bind(event.quantity)
.bind(event.price)
.bind(&event.venue)
.bind(&event.counterparty)
.bind(&event.description)
.bind(&event.details)
.bind(event.risk_score)
.bind(event.remediation_required)
.bind(&event.remediation_status)
.bind(event.reported_to_regulator)
.bind(&event.regulator_reference)
.execute(&self.db_pool)
.await?;
// Cache high-risk events in Redis for quick access
if let Some(risk_score) = event.risk_score {
if risk_score >= Decimal::from(70_i32) {
let cache_key = format!("compliance:high_risk:{}", event.id);
let serialized = serde_json::to_string(&event)?;
let mut redis_conn = self.redis_conn.clone();
redis::cmd("SETEX")
.arg(&cache_key)
.arg(86_400_i32) // 24 hours TTL
.arg(&serialized)
.query_async::<()>(&mut redis_conn)
.await?;
}
}
info!(
"Compliance event logged: {} - {} - {}",
event.framework as u8, event.event_type as u8, event.description
);
Ok(())
}
#[allow(clippy::expect_used)] // Writing to String cannot fail
async fn get_events(
&self,
from: DateTime<Utc>,
to: DateTime<Utc>,
framework: Option<RegulatoryFramework>,
severity: Option<ComplianceSeverity>,
) -> RiskDataResult<Vec<ComplianceEvent>> {
let mut query =
"SELECT * FROM compliance_events WHERE timestamp BETWEEN $1 AND $2".to_owned();
let mut bind_count = 2_i32;
if framework.is_some() {
bind_count += 1_i32;
use std::fmt::Write;
write!(&mut query, " AND framework = ${}", bind_count)
.expect("Writing to String should never fail");
}
if severity.is_some() {
bind_count += 1_i32;
use std::fmt::Write;
write!(&mut query, " AND severity = ${}", bind_count)
.expect("Writing to String should never fail");
}
query.push_str(" ORDER BY timestamp DESC");
let mut db_query = sqlx::query_as::<_, ComplianceEvent>(&query)
.bind(from)
.bind(to);
if let Some(fw) = framework {
db_query = db_query.bind(fw);
}
if let Some(sev) = severity {
db_query = db_query.bind(sev);
}
let events = db_query.fetch_all(&self.db_pool).await?;
Ok(events)
}
async fn report_transaction(&self, report: TransactionReport) -> RiskDataResult<()> {
let query = "
INSERT INTO transaction_reports (
id, trade_id, instrument_id, isin, trading_date, trading_time,
quantity, price, currency, venue, counterparty_id,
investment_decision_within_firm, execution_decision_within_firm,
client_identification, short_selling_indicator,
commodity_derivative_indicator, securities_financing_transaction_indicator,
waiver_indicator, reported_at, report_status, arm_reference
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
ON CONFLICT (trade_id) DO UPDATE SET
report_status = EXCLUDED.report_status,
arm_reference = EXCLUDED.arm_reference
";
sqlx::query(query)
.bind(report.id)
.bind(&report.trade_id)
.bind(&report.instrument_id)
.bind(&report.isin)
.bind(report.trading_date)
.bind(report.trading_time)
.bind(report.quantity)
.bind(report.price)
.bind(&report.currency)
.bind(&report.venue)
.bind(&report.counterparty_id)
.bind(&report.investment_decision_within_firm)
.bind(&report.execution_decision_within_firm)
.bind(&report.client_identification)
.bind(report.short_selling_indicator)
.bind(&report.commodity_derivative_indicator)
.bind(report.securities_financing_transaction_indicator)
.bind(&report.waiver_indicator)
.bind(report.reported_at)
.bind(&report.report_status)
.bind(&report.arm_reference)
.execute(&self.db_pool)
.await?;
info!("Transaction report submitted: {}", report.trade_id);
Ok(())
}
async fn get_transaction_reports(
&self,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> RiskDataResult<Vec<TransactionReport>> {
let query = "
SELECT * FROM transaction_reports
WHERE trading_date BETWEEN $1 AND $2
ORDER BY trading_time DESC
";
let reports = sqlx::query_as::<_, TransactionReport>(query)
.bind(from)
.bind(to)
.fetch_all(&self.db_pool)
.await?;
Ok(reports)
}
async fn log_best_execution(&self, report: BestExecutionReport) -> RiskDataResult<()> {
let query = "
INSERT INTO best_execution_reports (
id, order_id, symbol, order_type, quantity, requested_price,
execution_price, execution_venue, execution_time, price_improvement,
execution_speed_ms, market_impact_bps, total_costs_bps,
alternative_venues, criteria_analysis, best_execution_achieved, justification
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
";
sqlx::query(query)
.bind(report.id)
.bind(&report.order_id)
.bind(&report.symbol)
.bind(&report.order_type)
.bind(report.quantity)
.bind(report.requested_price)
.bind(report.execution_price)
.bind(&report.execution_venue)
.bind(report.execution_time)
.bind(report.price_improvement)
.bind(report.execution_speed_ms)
.bind(report.market_impact_bps)
.bind(report.total_costs_bps)
.bind(&report.alternative_venues)
.bind(&report.criteria_analysis)
.bind(report.best_execution_achieved)
.bind(&report.justification)
.execute(&self.db_pool)
.await?;
// Log compliance event if best execution was not achieved
if !report.best_execution_achieved {
let compliance_event = ComplianceEvent {
id: Uuid::new_v4(),
framework: RegulatoryFramework::MifidII,
event_type: ComplianceEventType::BestExecutionCheck,
severity: ComplianceSeverity::Warning,
timestamp: Utc::now(),
user_id: None,
session_id: None,
order_id: Some(report.order_id.clone()),
trade_id: None,
symbol: Some(report.symbol.clone()),
quantity: Some(report.quantity),
price: Some(report.execution_price),
venue: Some(report.execution_venue.clone()),
counterparty: None,
description: "Best execution not achieved".to_owned(),
details: serde_json::json!({
"justification": report.justification,
"price_improvement": report.price_improvement,
"execution_speed_ms": report.execution_speed_ms
}),
risk_score: None,
remediation_required: true,
remediation_status: Some("pending".to_owned()),
reported_to_regulator: false,
regulator_reference: None,
};
self.log_event(compliance_event).await?;
}
info!("Best execution report logged: {}", report.order_id);
Ok(())
}
async fn get_best_execution_reports(
&self,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> RiskDataResult<Vec<BestExecutionReport>> {
let query = "
SELECT * FROM best_execution_reports
WHERE execution_time BETWEEN $1 AND $2
ORDER BY execution_time DESC
";
let reports = sqlx::query_as::<_, BestExecutionReport>(query)
.bind(from)
.bind(to)
.fetch_all(&self.db_pool)
.await?;
Ok(reports)
}
async fn create_audit_trail(&self, entry: AuditTrail) -> RiskDataResult<()> {
let query = "
INSERT INTO audit_trails (
id, timestamp, user_id, session_id, action, resource, resource_id,
ip_address, user_agent, success, error_message, before_state,
after_state, sensitive_data_accessed, retention_required_until
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
";
sqlx::query(query)
.bind(entry.id)
.bind(entry.timestamp)
.bind(&entry.user_id)
.bind(&entry.session_id)
.bind(&entry.action)
.bind(&entry.resource)
.bind(&entry.resource_id)
.bind(&entry.ip_address)
.bind(&entry.user_agent)
.bind(entry.success)
.bind(&entry.error_message)
.bind(&entry.before_state)
.bind(&entry.after_state)
.bind(entry.sensitive_data_accessed)
.bind(entry.retention_required_until)
.execute(&self.db_pool)
.await?;
// Cache failed access attempts for security monitoring
if !entry.success {
let cache_key = format!("audit:failed:{}:{}", entry.user_id, entry.resource);
let mut redis_conn = self.redis_conn.clone();
redis::cmd("INCR")
.arg(&cache_key)
.query_async::<()>(&mut redis_conn)
.await?;
redis::cmd("EXPIRE")
.arg(&cache_key)
.arg(3_600_i32) // 1 hour window
.query_async::<()>(&mut redis_conn)
.await?;
}
Ok(())
}
#[allow(clippy::expect_used)] // Writing to String cannot fail
async fn search_audit_trail(
&self,
user_id: Option<String>,
action: Option<String>,
resource: Option<String>,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> RiskDataResult<Vec<AuditTrail>> {
let mut query = "SELECT * FROM audit_trails WHERE timestamp BETWEEN $1 AND $2".to_owned();
let mut bind_count = 2_i32;
if user_id.is_some() {
bind_count += 1_i32;
use std::fmt::Write;
write!(&mut query, " AND user_id = ${}", bind_count)
.expect("Writing to String should never fail");
}
if action.is_some() {
bind_count += 1_i32;
use std::fmt::Write;
write!(&mut query, " AND action = ${}", bind_count)
.expect("Writing to String should never fail");
}
if resource.is_some() {
bind_count += 1_i32;
use std::fmt::Write;
write!(&mut query, " AND resource = ${}", bind_count)
.expect("Writing to String should never fail");
}
query.push_str(" ORDER BY timestamp DESC LIMIT 1000");
let mut db_query = sqlx::query_as::<_, AuditTrail>(&query).bind(from).bind(to);
if let Some(uid) = user_id {
db_query = db_query.bind(uid);
}
if let Some(act) = action {
db_query = db_query.bind(act);
}
if let Some(res) = resource {
db_query = db_query.bind(res);
}
let entries = db_query.fetch_all(&self.db_pool).await?;
Ok(entries)
}
async fn generate_regulatory_report(
&self,
framework: RegulatoryFramework,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> RiskDataResult<serde_json::Value> {
let events = self.get_events(from, to, Some(framework), None).await?;
let report = match framework {
RegulatoryFramework::Sox => {
serde_json::json!({
"framework": "SOX",
"period": {"from": from, "to": to},
"total_events": events.len(),
"critical_events": events.iter().filter(|e| e.severity == ComplianceSeverity::Critical).count(),
"breaches": events.iter().filter(|e| e.severity == ComplianceSeverity::Breach).count(),
"remediation_pending": events.iter().filter(|e| e.remediation_required && e.remediation_status.as_deref() == Some("pending")).count(),
"events": events
})
},
RegulatoryFramework::MifidII => {
let transaction_reports = self.get_transaction_reports(from, to).await?;
let best_execution_reports = self.get_best_execution_reports(from, to).await?;
serde_json::json!({
"framework": "MiFID II",
"period": {"from": from, "to": to},
"transaction_reports": transaction_reports.len(),
"best_execution_reports": best_execution_reports.len(),
"compliance_events": events.len(),
"transactions": transaction_reports,
"best_execution": best_execution_reports,
"events": events
})
},
RegulatoryFramework::DoddFrank
| RegulatoryFramework::BaselIII
| RegulatoryFramework::Emir
| RegulatoryFramework::Mifir
| RegulatoryFramework::Gdpr => {
serde_json::json!({
"framework": format!("{:?}", framework),
"period": {"from": from, "to": to},
"total_events": events.len(),
"events": events
})
},
};
Ok(report)
}
async fn check_violations(&self, trade_id: &str) -> RiskDataResult<Vec<ComplianceEvent>> {
let query = "
SELECT * FROM compliance_events
WHERE trade_id = $1
AND severity IN ('critical', 'breach')
ORDER BY timestamp DESC
";
let violations = sqlx::query_as::<_, ComplianceEvent>(query)
.bind(trade_id)
.fetch_all(&self.db_pool)
.await?;
Ok(violations)
}
async fn mark_reported(&self, event_id: Uuid, reference: String) -> RiskDataResult<()> {
let query = "
UPDATE compliance_events
SET reported_to_regulator = true, regulator_reference = $2
WHERE id = $1
";
sqlx::query(query)
.bind(event_id)
.bind(&reference)
.execute(&self.db_pool)
.await?;
info!(
"Compliance event {} marked as reported with reference {}",
event_id, reference
);
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
#[allow(clippy::default_numeric_fallback)]
#[allow(clippy::diverging_sub_expression)]
#[allow(clippy::used_underscore_binding)]
mod tests {
use super::*;
#[test]
fn test_compliance_event_validation() {
// Test event validation logic without needing a database connection
let event = ComplianceEvent {
id: Uuid::new_v4(),
framework: RegulatoryFramework::Sox,
event_type: ComplianceEventType::TradeExecution,
severity: ComplianceSeverity::Info,
timestamp: Utc::now(),
user_id: None,
session_id: None,
order_id: None,
trade_id: None,
symbol: None,
quantity: None,
price: None,
venue: None,
counterparty: None,
description: "".to_string(), // Empty description should fail
details: serde_json::json!({}),
risk_score: None,
remediation_required: false,
remediation_status: None,
reported_to_regulator: false,
regulator_reference: None,
};
// Inline validation logic to test without database
assert!(
event.description.is_empty(),
"Description should be empty for test"
);
}
#[test]
fn test_risk_score_calculation() {
// Test risk score calculation logic without database
let event = ComplianceEvent {
id: Uuid::new_v4(),
framework: RegulatoryFramework::Sox,
event_type: ComplianceEventType::RiskBreach,
severity: ComplianceSeverity::Breach,
timestamp: Utc::now(),
user_id: None,
session_id: None,
order_id: None,
trade_id: None,
symbol: None,
quantity: None,
price: None,
venue: None,
counterparty: None,
description: "Test risk breach".to_string(),
details: serde_json::json!({}),
risk_score: None,
remediation_required: true,
remediation_status: None,
reported_to_regulator: false,
regulator_reference: None,
};
// Test basic risk score calculation logic inline
// Higher severity should result in higher base score
let base_score = match event.severity {
ComplianceSeverity::Info => Decimal::from(10_i32),
ComplianceSeverity::Warning => Decimal::from(25_i32),
ComplianceSeverity::Breach => Decimal::from(50_i32),
ComplianceSeverity::Critical => Decimal::from(75_i32),
};
assert!(base_score > Decimal::ZERO);
assert!(base_score <= Decimal::from(100_i32));
assert_eq!(
base_score,
Decimal::from(50_i32),
"Breach severity should have base score of 50"
);
}
}