use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegulatoryDriftEvent {
pub event_id: String,
pub regulation_type: RegulationType,
pub effective_date: NaiveDate,
#[serde(default)]
pub announcement_date: Option<NaiveDate>,
#[serde(default)]
pub transition_period_days: u32,
#[serde(default)]
pub affected_accounts: Vec<String>,
#[serde(default)]
pub impacts: Vec<RegulatoryImpact>,
#[serde(default)]
pub description: Option<String>,
}
impl RegulatoryDriftEvent {
pub fn new(
event_id: impl Into<String>,
regulation_type: RegulationType,
effective_date: NaiveDate,
) -> Self {
Self {
event_id: event_id.into(),
regulation_type,
effective_date,
announcement_date: None,
transition_period_days: 0,
affected_accounts: Vec::new(),
impacts: Vec::new(),
description: None,
}
}
pub fn is_active_at(&self, date: NaiveDate) -> bool {
date >= self.effective_date
}
pub fn is_in_transition_at(&self, date: NaiveDate) -> bool {
if date < self.effective_date {
return false;
}
let transition_end =
self.effective_date + chrono::Duration::days(self.transition_period_days as i64);
date < transition_end
}
pub fn transition_progress_at(&self, date: NaiveDate) -> f64 {
if date < self.effective_date {
return 0.0;
}
if self.transition_period_days == 0 {
return 1.0;
}
let days_elapsed = (date - self.effective_date).num_days() as f64;
(days_elapsed / self.transition_period_days as f64).min(1.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RegulationType {
AccountingStandardAdoption {
standard: String,
framework: AccountingFramework,
topic: String,
},
TaxRateChange {
tax_type: String,
#[serde(with = "crate::serde_decimal")]
old_rate: Decimal,
#[serde(with = "crate::serde_decimal")]
new_rate: Decimal,
jurisdiction: String,
},
NewComplianceRequirement {
requirement: String,
framework: String,
severity: ComplianceSeverity,
},
IndustryRegulation {
industry: String,
regulation: String,
regulatory_body: String,
},
}
impl RegulationType {
pub fn type_name(&self) -> &'static str {
match self {
Self::AccountingStandardAdoption { .. } => "accounting_standard_adoption",
Self::TaxRateChange { .. } => "tax_rate_change",
Self::NewComplianceRequirement { .. } => "new_compliance_requirement",
Self::IndustryRegulation { .. } => "industry_regulation",
}
}
pub fn name(&self) -> &str {
match self {
Self::AccountingStandardAdoption { standard, .. } => standard,
Self::TaxRateChange { tax_type, .. } => tax_type,
Self::NewComplianceRequirement { requirement, .. } => requirement,
Self::IndustryRegulation { regulation, .. } => regulation,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AccountingFramework {
#[default]
UsGaap,
Ifrs,
DualReporting,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ComplianceSeverity {
Minor,
#[default]
Standard,
Major,
Critical,
}
impl ComplianceSeverity {
pub fn effort_multiplier(&self) -> f64 {
match self {
Self::Minor => 1.1,
Self::Standard => 1.3,
Self::Major => 1.6,
Self::Critical => 2.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegulatoryImpact {
pub area: ImpactArea,
pub description: String,
pub magnitude: f64,
#[serde(default)]
pub affected_metrics: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ImpactArea {
BalanceSheet,
IncomeStatement,
CashFlow,
Disclosures,
InternalControls,
Systems,
Processes,
AuditProcedures,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditFocusEvent {
pub event_id: String,
pub focus_type: AuditFocusType,
pub effective_date: NaiveDate,
#[serde(default = "default_priority")]
pub priority_level: u8,
#[serde(default)]
pub risk_areas: Vec<String>,
#[serde(default)]
pub accounts_with_additional_procedures: Vec<String>,
#[serde(default)]
pub description: Option<String>,
}
fn default_priority() -> u8 {
3
}
impl AuditFocusEvent {
pub fn new(
event_id: impl Into<String>,
focus_type: AuditFocusType,
effective_date: NaiveDate,
) -> Self {
Self {
event_id: event_id.into(),
focus_type,
effective_date,
priority_level: 3,
risk_areas: Vec::new(),
accounts_with_additional_procedures: Vec::new(),
description: None,
}
}
pub fn is_active_at(&self, date: NaiveDate) -> bool {
date >= self.effective_date
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AuditFocusType {
RiskBasedShift {
trigger: String,
new_focus_areas: Vec<String>,
},
IndustryTrendResponse {
trend: String,
affected_procedures: Vec<String>,
},
PriorYearFindingFollowUp {
finding_ids: Vec<String>,
},
RegulatoryDrivenFocus {
regulation: String,
required_procedures: Vec<String>,
},
FraudRiskResponse {
risk_indicators: Vec<String>,
response_procedures: Vec<String>,
},
}
impl AuditFocusType {
pub fn type_name(&self) -> &'static str {
match self {
Self::RiskBasedShift { .. } => "risk_based_shift",
Self::IndustryTrendResponse { .. } => "industry_trend_response",
Self::PriorYearFindingFollowUp { .. } => "prior_year_finding_followup",
Self::RegulatoryDrivenFocus { .. } => "regulatory_driven_focus",
Self::FraudRiskResponse { .. } => "fraud_risk_response",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RegulatoryCalendar {
#[serde(default)]
pub regulatory_events: Vec<RegulatoryDriftEvent>,
#[serde(default)]
pub audit_focus_events: Vec<AuditFocusEvent>,
}
impl RegulatoryCalendar {
pub fn new() -> Self {
Self {
regulatory_events: Vec::new(),
audit_focus_events: Vec::new(),
}
}
pub fn us_gaap_2024() -> Self {
Self {
regulatory_events: vec![
RegulatoryDriftEvent {
event_id: "REG-842-2024".to_string(),
regulation_type: RegulationType::AccountingStandardAdoption {
standard: "ASC 842".to_string(),
framework: AccountingFramework::UsGaap,
topic: "leases".to_string(),
},
effective_date: NaiveDate::from_ymd_opt(2024, 1, 1)
.expect("valid date components"),
announcement_date: None,
transition_period_days: 0,
affected_accounts: vec![
"1600".to_string(), "2300".to_string(), ],
impacts: vec![RegulatoryImpact {
area: ImpactArea::BalanceSheet,
description: "Recognition of ROU assets and lease liabilities".to_string(),
magnitude: 0.3,
affected_metrics: vec![
"total_assets".to_string(),
"total_liabilities".to_string(),
],
}],
description: Some("ASC 842 Leases implementation".to_string()),
},
],
audit_focus_events: vec![AuditFocusEvent {
event_id: "AF-CYBER-2024".to_string(),
focus_type: AuditFocusType::IndustryTrendResponse {
trend: "Increased cybersecurity risks".to_string(),
affected_procedures: vec![
"IT general controls testing".to_string(),
"Cybersecurity disclosure review".to_string(),
],
},
effective_date: NaiveDate::from_ymd_opt(2024, 1, 1).expect("valid date components"),
priority_level: 2,
risk_areas: vec!["IT controls".to_string(), "Data security".to_string()],
accounts_with_additional_procedures: Vec::new(),
description: Some("Enhanced focus on cybersecurity".to_string()),
}],
}
}
pub fn ifrs_2024() -> Self {
Self {
regulatory_events: vec![RegulatoryDriftEvent {
event_id: "REG-IFRS16-2024".to_string(),
regulation_type: RegulationType::AccountingStandardAdoption {
standard: "IFRS 16".to_string(),
framework: AccountingFramework::Ifrs,
topic: "leases".to_string(),
},
effective_date: NaiveDate::from_ymd_opt(2024, 1, 1).expect("valid date components"),
announcement_date: None,
transition_period_days: 0,
affected_accounts: vec!["1600".to_string(), "2300".to_string()],
impacts: vec![],
description: Some("IFRS 16 Leases implementation".to_string()),
}],
audit_focus_events: Vec::new(),
}
}
pub fn add_regulatory_event(&mut self, event: RegulatoryDriftEvent) {
self.regulatory_events.push(event);
}
pub fn add_audit_focus_event(&mut self, event: AuditFocusEvent) {
self.audit_focus_events.push(event);
}
pub fn regulatory_events_on_date(&self, date: NaiveDate) -> Vec<&RegulatoryDriftEvent> {
self.regulatory_events
.iter()
.filter(|e| e.effective_date == date)
.collect()
}
pub fn active_regulatory_events_at(&self, date: NaiveDate) -> Vec<&RegulatoryDriftEvent> {
self.regulatory_events
.iter()
.filter(|e| e.is_active_at(date))
.collect()
}
pub fn active_audit_focus_at(&self, date: NaiveDate) -> Vec<&AuditFocusEvent> {
self.audit_focus_events
.iter()
.filter(|e| e.is_active_at(date))
.collect()
}
pub fn events_in_transition_at(&self, date: NaiveDate) -> Vec<&RegulatoryDriftEvent> {
self.regulatory_events
.iter()
.filter(|e| e.is_in_transition_at(date))
.collect()
}
pub fn affected_accounts_at(&self, date: NaiveDate) -> Vec<String> {
let mut accounts: Vec<String> = self
.regulatory_events
.iter()
.filter(|e| e.is_active_at(date))
.flat_map(|e| e.affected_accounts.iter().cloned())
.collect();
accounts.sort();
accounts.dedup();
accounts
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_regulatory_event_creation() {
let event = RegulatoryDriftEvent::new(
"REG-001",
RegulationType::AccountingStandardAdoption {
standard: "ASC 842".to_string(),
framework: AccountingFramework::UsGaap,
topic: "leases".to_string(),
},
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
);
assert_eq!(event.event_id, "REG-001");
assert_eq!(
event.regulation_type.type_name(),
"accounting_standard_adoption"
);
}
#[test]
fn test_regulatory_event_active() {
let event = RegulatoryDriftEvent {
event_id: "REG-001".to_string(),
regulation_type: RegulationType::TaxRateChange {
tax_type: "corporate_income".to_string(),
old_rate: Decimal::from_str_exact("0.21").unwrap(),
new_rate: Decimal::from_str_exact("0.25").unwrap(),
jurisdiction: "US".to_string(),
},
effective_date: NaiveDate::from_ymd_opt(2024, 6, 1).unwrap(),
announcement_date: Some(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
transition_period_days: 30,
affected_accounts: vec!["5100".to_string()],
impacts: Vec::new(),
description: None,
};
assert!(!event.is_active_at(NaiveDate::from_ymd_opt(2024, 5, 31).unwrap()));
assert!(event.is_active_at(NaiveDate::from_ymd_opt(2024, 6, 1).unwrap()));
assert!(event.is_in_transition_at(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap()));
assert!(!event.is_in_transition_at(NaiveDate::from_ymd_opt(2024, 7, 15).unwrap()));
}
#[test]
fn test_audit_focus_event() {
let event = AuditFocusEvent::new(
"AF-001",
AuditFocusType::FraudRiskResponse {
risk_indicators: vec!["unusual_transactions".to_string()],
response_procedures: vec!["extended_testing".to_string()],
},
NaiveDate::from_ymd_opt(2024, 3, 1).unwrap(),
);
assert!(!event.is_active_at(NaiveDate::from_ymd_opt(2024, 2, 28).unwrap()));
assert!(event.is_active_at(NaiveDate::from_ymd_opt(2024, 3, 1).unwrap()));
}
#[test]
fn test_regulatory_calendar_preset() {
let calendar = RegulatoryCalendar::us_gaap_2024();
assert!(!calendar.regulatory_events.is_empty());
assert!(!calendar.audit_focus_events.is_empty());
let active =
calendar.active_regulatory_events_at(NaiveDate::from_ymd_opt(2024, 6, 1).unwrap());
assert!(!active.is_empty());
}
}