datasynth_core/models/compliance/
temporal.rs1use chrono::NaiveDate;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ChangeImpact {
11 Low,
13 Medium,
15 High,
17 Replacement,
19}
20
21impl ChangeImpact {
22 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#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct TemporalVersion {
36 pub version_id: String,
38 pub issued_date: Option<NaiveDate>,
40 pub early_adoption_from: Option<NaiveDate>,
42 pub effective_from: NaiveDate,
44 pub superseded_at: Option<NaiveDate>,
46 pub jurisdiction_overrides: HashMap<String, NaiveDate>,
48 pub change_summary: Vec<String>,
50 pub impact: ChangeImpact,
52}
53
54impl TemporalVersion {
55 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 pub fn with_issued_date(mut self, date: NaiveDate) -> Self {
75 self.issued_date = Some(date);
76 self
77 }
78
79 pub fn with_early_adoption(mut self, date: NaiveDate) -> Self {
81 self.early_adoption_from = Some(date);
82 self
83 }
84
85 pub fn superseded_at(mut self, date: NaiveDate) -> Self {
87 self.superseded_at = Some(date);
88 self
89 }
90
91 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 pub fn with_change(mut self, summary: impl Into<String>) -> Self {
100 self.change_summary.push(summary.into());
101 self
102 }
103
104 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct ResolvedStandard {
140 pub id: super::StandardId,
142 pub version: TemporalVersion,
144 pub local_designation: Option<String>,
146 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 assert!(v.is_active_at_in(date(2019, 6, 1), "US"));
181 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}