Skip to main content

datasynth_core/models/compliance/
temporal.rs

1//! Temporal versioning for compliance standards.
2
3use chrono::NaiveDate;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Impact level of a standard change on generated data.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ChangeImpact {
11    /// Cosmetic or disclosure-only changes
12    Low,
13    /// Changes to recognition or measurement
14    Medium,
15    /// Fundamental restructuring (e.g., IAS 39 → IFRS 9)
16    High,
17    /// Complete replacement of a standard
18    Replacement,
19}
20
21impl ChangeImpact {
22    /// Returns a numeric score for ML features.
23    pub fn score(&self) -> f64 {
24        match self {
25            Self::Low => 0.25,
26            Self::Medium => 0.50,
27            Self::High => 0.75,
28            Self::Replacement => 1.0,
29        }
30    }
31}
32
33/// A specific version of a standard with temporal bounds.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct TemporalVersion {
36    /// Version identifier (e.g., "2018", "2020-amended", "2023-revised")
37    pub version_id: String,
38    /// Date this version was issued/published
39    pub issued_date: Option<NaiveDate>,
40    /// Date this version becomes available for early adoption
41    pub early_adoption_from: Option<NaiveDate>,
42    /// Date this version becomes mandatory (global default)
43    pub effective_from: NaiveDate,
44    /// Date this version is superseded (None = currently active)
45    pub superseded_at: Option<NaiveDate>,
46    /// Per-jurisdiction effective date overrides
47    pub jurisdiction_overrides: HashMap<String, NaiveDate>,
48    /// Key changes from the previous version
49    pub change_summary: Vec<String>,
50    /// Impact level on generated data
51    pub impact: ChangeImpact,
52}
53
54impl TemporalVersion {
55    /// Creates a new temporal version with required fields.
56    pub fn new(
57        version_id: impl Into<String>,
58        effective_from: NaiveDate,
59        impact: ChangeImpact,
60    ) -> Self {
61        Self {
62            version_id: version_id.into(),
63            issued_date: None,
64            early_adoption_from: None,
65            effective_from,
66            superseded_at: None,
67            jurisdiction_overrides: HashMap::new(),
68            change_summary: Vec::new(),
69            impact,
70        }
71    }
72
73    /// Sets the issued date.
74    pub fn with_issued_date(mut self, date: NaiveDate) -> Self {
75        self.issued_date = Some(date);
76        self
77    }
78
79    /// Sets the early adoption date.
80    pub fn with_early_adoption(mut self, date: NaiveDate) -> Self {
81        self.early_adoption_from = Some(date);
82        self
83    }
84
85    /// Sets the superseded date.
86    pub fn superseded_at(mut self, date: NaiveDate) -> Self {
87        self.superseded_at = Some(date);
88        self
89    }
90
91    /// Adds a jurisdiction-specific effective date override.
92    pub fn with_jurisdiction_override(mut self, country: &str, date: NaiveDate) -> Self {
93        self.jurisdiction_overrides
94            .insert(country.to_string(), date);
95        self
96    }
97
98    /// Adds a change summary item.
99    pub fn with_change(mut self, summary: impl Into<String>) -> Self {
100        self.change_summary.push(summary.into());
101        self
102    }
103
104    /// Returns whether this version is active at a given date (global, ignoring jurisdiction overrides).
105    pub fn is_active_at(&self, date: NaiveDate) -> bool {
106        date >= self.effective_from && self.superseded_at.is_none_or(|sup| date < sup)
107    }
108
109    /// Returns whether this version is active at a given date for a specific jurisdiction.
110    pub fn is_active_at_in(&self, date: NaiveDate, country: &str) -> bool {
111        let effective = self
112            .jurisdiction_overrides
113            .get(country)
114            .copied()
115            .unwrap_or(self.effective_from);
116        date >= effective && self.superseded_at.is_none_or(|sup| date < sup)
117    }
118
119    /// Returns the effective date for a specific jurisdiction.
120    pub fn effective_date_for(&self, country: &str) -> NaiveDate {
121        self.jurisdiction_overrides
122            .get(country)
123            .copied()
124            .unwrap_or(self.effective_from)
125    }
126
127    /// Returns the number of days this version has been active as of a given date.
128    pub fn days_active_at(&self, date: NaiveDate) -> Option<i64> {
129        if self.is_active_at(date) {
130            Some((date - self.effective_from).num_days())
131        } else {
132            None
133        }
134    }
135}
136
137/// A resolved standard pinned to a specific version for generation.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct ResolvedStandard {
140    /// The standard identifier
141    pub id: super::StandardId,
142    /// The resolved version
143    pub version: TemporalVersion,
144    /// Local designation (e.g., "Ind AS 116" for IFRS 16 in India)
145    pub local_designation: Option<String>,
146    /// Entity codes this standard applies to
147    pub applicable_entities: Vec<String>,
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    fn date(y: i32, m: u32, d: u32) -> NaiveDate {
155        NaiveDate::from_ymd_opt(y, m, d).expect("valid date")
156    }
157
158    #[test]
159    fn test_version_active_at() {
160        let v = TemporalVersion::new("2019", date(2019, 1, 1), ChangeImpact::High);
161        assert!(!v.is_active_at(date(2018, 12, 31)));
162        assert!(v.is_active_at(date(2019, 1, 1)));
163        assert!(v.is_active_at(date(2025, 6, 30)));
164    }
165
166    #[test]
167    fn test_version_superseded() {
168        let v = TemporalVersion::new("2019", date(2019, 1, 1), ChangeImpact::High)
169            .superseded_at(date(2023, 1, 1));
170        assert!(v.is_active_at(date(2022, 12, 31)));
171        assert!(!v.is_active_at(date(2023, 1, 1)));
172    }
173
174    #[test]
175    fn test_jurisdiction_override() {
176        let v = TemporalVersion::new("2019", date(2019, 1, 1), ChangeImpact::High)
177            .with_jurisdiction_override("IN", date(2020, 4, 1));
178
179        // Globally active from 2019
180        assert!(v.is_active_at_in(date(2019, 6, 1), "US"));
181        // India delayed to April 2020
182        assert!(!v.is_active_at_in(date(2019, 6, 1), "IN"));
183        assert!(v.is_active_at_in(date(2020, 6, 1), "IN"));
184    }
185
186    #[test]
187    fn test_change_impact_score() {
188        assert!((ChangeImpact::Low.score() - 0.25).abs() < f64::EPSILON);
189        assert!((ChangeImpact::Replacement.score() - 1.0).abs() < f64::EPSILON);
190    }
191}