Skip to main content

datasynth_generators/compliance/
regulation_generator.rs

1//! Regulation generator — produces compliance registry snapshot data for output.
2//!
3//! Resolves the active standards for each configured jurisdiction at the reference
4//! date and emits serializable records for CSV/JSON export.
5
6use chrono::NaiveDate;
7use serde::Serialize;
8
9use datasynth_standards::registry::StandardRegistry;
10
11/// A flattened compliance-standard record suitable for CSV/JSON output.
12#[derive(Debug, Clone, Serialize)]
13pub struct ComplianceStandardRecord {
14    pub standard_id: String,
15    pub body: String,
16    pub number: String,
17    pub title: String,
18    pub category: String,
19    pub domain: String,
20    pub jurisdiction: String,
21    pub effective_date: String,
22    pub version: String,
23    pub is_active: bool,
24    pub superseded_by: Option<String>,
25    /// GL account types this standard applies to.
26    pub applicable_account_types: Vec<String>,
27    /// Business processes this standard governs.
28    pub applicable_processes: Vec<String>,
29}
30
31/// A flattened cross-reference record.
32#[derive(Debug, Clone, Serialize)]
33pub struct CrossReferenceRecord {
34    pub from_standard: String,
35    pub to_standard: String,
36    pub relationship: String,
37    pub convergence_level: f64,
38    pub description: Option<String>,
39}
40
41/// A flattened jurisdiction profile record.
42#[derive(Debug, Clone, Serialize)]
43pub struct JurisdictionRecord {
44    pub country_code: String,
45    pub country_name: String,
46    pub accounting_framework: String,
47    pub audit_framework: String,
48    pub standards_body: String,
49    pub statutory_tax_rate: f64,
50    pub standard_count: usize,
51}
52
53/// Generator that produces compliance regulation output data from the registry.
54pub struct RegulationGenerator {
55    registry: StandardRegistry,
56}
57
58impl RegulationGenerator {
59    /// Creates a new generator with the built-in registry.
60    pub fn new() -> Self {
61        Self {
62            registry: StandardRegistry::with_built_in(),
63        }
64    }
65
66    /// Creates a generator with a custom registry.
67    pub fn with_registry(registry: StandardRegistry) -> Self {
68        Self { registry }
69    }
70
71    /// Returns a reference to the underlying registry.
72    pub fn registry(&self) -> &StandardRegistry {
73        &self.registry
74    }
75
76    /// Generates standard records for a set of jurisdictions at a reference date.
77    pub fn generate_standard_records(
78        &self,
79        jurisdictions: &[String],
80        reference_date: NaiveDate,
81    ) -> Vec<ComplianceStandardRecord> {
82        let mut records = Vec::new();
83
84        for jurisdiction in jurisdictions {
85            let standards = self
86                .registry
87                .standards_for_jurisdiction(jurisdiction, reference_date);
88
89            for std in standards {
90                let active_version =
91                    self.registry
92                        .active_version_in(&std.id, jurisdiction, reference_date);
93
94                let (effective_date, version) = if let Some(v) = active_version {
95                    (v.effective_from.to_string(), v.version_id.clone())
96                } else {
97                    ("unknown".to_string(), "unknown".to_string())
98                };
99
100                records.push(ComplianceStandardRecord {
101                    standard_id: std.id.as_str().to_string(),
102                    body: std.id.body().to_string(),
103                    number: std.id.number().to_string(),
104                    title: std.title.clone(),
105                    category: format!("{}", std.category),
106                    domain: format!("{}", std.domain),
107                    jurisdiction: jurisdiction.clone(),
108                    effective_date,
109                    version,
110                    is_active: true,
111                    superseded_by: std.superseded_by.as_ref().map(|s| s.as_str().to_string()),
112                    applicable_account_types: std.applicable_account_types.clone(),
113                    applicable_processes: std.applicable_processes.clone(),
114                });
115            }
116        }
117
118        records
119    }
120
121    /// Generates cross-reference records.
122    pub fn generate_cross_reference_records(&self) -> Vec<CrossReferenceRecord> {
123        self.registry
124            .cross_references()
125            .iter()
126            .map(|xr| CrossReferenceRecord {
127                from_standard: xr.from_standard.as_str().to_string(),
128                to_standard: xr.to_standard.as_str().to_string(),
129                relationship: format!("{}", xr.relationship),
130                convergence_level: xr.convergence_level,
131                description: xr.description.clone(),
132            })
133            .collect()
134    }
135
136    /// Generates jurisdiction profile records.
137    pub fn generate_jurisdiction_records(
138        &self,
139        jurisdictions: &[String],
140        reference_date: NaiveDate,
141    ) -> Vec<JurisdictionRecord> {
142        jurisdictions
143            .iter()
144            .filter_map(|code| {
145                self.registry.jurisdiction(code).map(|jp| {
146                    let standard_count = self
147                        .registry
148                        .standards_for_jurisdiction(code, reference_date)
149                        .len();
150
151                    JurisdictionRecord {
152                        country_code: jp.country_code.clone(),
153                        country_name: jp.country_name.clone(),
154                        accounting_framework: format!("{:?}", jp.accounting_framework),
155                        audit_framework: format!("{:?}", jp.audit_framework),
156                        standards_body: jp.accounting_standards_body.clone(),
157                        statutory_tax_rate: jp.corporate_tax_rate.unwrap_or(0.0),
158                        standard_count,
159                    }
160                })
161            })
162            .collect()
163    }
164}
165
166impl Default for RegulationGenerator {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172#[cfg(test)]
173#[allow(clippy::unwrap_used)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_generate_standard_records() {
179        let gen = RegulationGenerator::new();
180        let date = NaiveDate::from_ymd_opt(2025, 6, 30).unwrap();
181        let records = gen.generate_standard_records(&["US".to_string()], date);
182        assert!(!records.is_empty(), "Should have US standards");
183    }
184
185    #[test]
186    fn test_generate_cross_references() {
187        let gen = RegulationGenerator::new();
188        let records = gen.generate_cross_reference_records();
189        assert!(!records.is_empty(), "Should have cross-references");
190    }
191
192    #[test]
193    fn test_generate_jurisdiction_records() {
194        let gen = RegulationGenerator::new();
195        let date = NaiveDate::from_ymd_opt(2025, 6, 30).unwrap();
196        let records =
197            gen.generate_jurisdiction_records(&["US".to_string(), "DE".to_string()], date);
198        assert_eq!(records.len(), 2);
199    }
200}