Skip to main content

datasynth_core/models/compliance/
filing.rs

1//! Regulatory filing models.
2
3use std::collections::HashMap;
4
5use chrono::NaiveDate;
6use serde::{Deserialize, Serialize};
7
8use crate::models::graph_properties::{GraphPropertyValue, ToNodeProperties};
9
10/// Type of regulatory filing.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum FilingType {
14    // US filings
15    /// SEC Form 10-K (Annual Report)
16    Form10K,
17    /// SEC Form 10-Q (Quarterly Report)
18    Form10Q,
19    /// SEC Form 8-K (Current Report)
20    Form8K,
21    /// SEC Form 20-F (Annual Report for Foreign Private Issuers)
22    Form20F,
23
24    // EU / German filings
25    /// German annual financial statements (Jahresabschluss)
26    Jahresabschluss,
27    /// German electronic balance sheet (E-Bilanz)
28    EBilanz,
29
30    // French filings
31    /// French fiscal package (Liasse fiscale)
32    LiasseFiscale,
33
34    // UK filings
35    /// UK annual return to Companies House
36    UkAnnualReturn,
37    /// UK corporation tax return
38    Ct600,
39
40    // Japanese filings
41    /// Japanese securities report (有価証券報告書)
42    YukaShokenHokokusho,
43
44    // Generic
45    /// Annual financial statements (generic)
46    AnnualStatements,
47    /// Quarterly report (generic)
48    QuarterlyReport,
49    /// Tax return (generic)
50    TaxReturn,
51    /// Custom filing type
52    Custom(String),
53}
54
55impl std::fmt::Display for FilingType {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            Self::Form10K => write!(f, "10-K"),
59            Self::Form10Q => write!(f, "10-Q"),
60            Self::Form8K => write!(f, "8-K"),
61            Self::Form20F => write!(f, "20-F"),
62            Self::Jahresabschluss => write!(f, "Jahresabschluss"),
63            Self::EBilanz => write!(f, "E-Bilanz"),
64            Self::LiasseFiscale => write!(f, "Liasse fiscale"),
65            Self::UkAnnualReturn => write!(f, "Annual Return"),
66            Self::Ct600 => write!(f, "CT600"),
67            Self::YukaShokenHokokusho => write!(f, "有価証券報告書"),
68            Self::AnnualStatements => write!(f, "Annual Financial Statements"),
69            Self::QuarterlyReport => write!(f, "Quarterly Report"),
70            Self::TaxReturn => write!(f, "Tax Return"),
71            Self::Custom(s) => write!(f, "{s}"),
72        }
73    }
74}
75
76/// Filing frequency.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum FilingFrequency {
80    /// Filed annually
81    Annual,
82    /// Filed semi-annually
83    SemiAnnual,
84    /// Filed quarterly
85    Quarterly,
86    /// Filed monthly
87    Monthly,
88    /// Filed on occurrence of event
89    EventDriven,
90}
91
92/// Filing status.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum FilingStatus {
96    /// Not yet due
97    NotDue,
98    /// Pending preparation
99    Pending,
100    /// Filed on time
101    Filed,
102    /// Filed late
103    FiledLate,
104    /// Overdue
105    Overdue,
106}
107
108/// A regulatory filing requirement.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct FilingRequirement {
111    /// Filing type
112    pub filing_type: FilingType,
113    /// Filing frequency
114    pub frequency: FilingFrequency,
115    /// Regulator receiving the filing
116    pub regulator: String,
117    /// Jurisdiction (ISO 3166-1 alpha-2)
118    pub jurisdiction: String,
119    /// Days after period end by which filing is due
120    pub deadline_days: u32,
121    /// Whether electronic filing is required
122    pub electronic_filing: bool,
123    /// Whether XBRL tagging is required
124    pub xbrl_required: bool,
125}
126
127/// A specific regulatory filing instance.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct RegulatoryFiling {
130    /// Filing type
131    pub filing_type: FilingType,
132    /// Company code
133    pub company_code: String,
134    /// Jurisdiction
135    pub jurisdiction: String,
136    /// Period end date
137    pub period_end: NaiveDate,
138    /// Filing deadline
139    pub deadline: NaiveDate,
140    /// Actual filing date (if filed)
141    pub filing_date: Option<NaiveDate>,
142    /// Filing status
143    pub status: FilingStatus,
144    /// Regulator
145    pub regulator: String,
146    /// Electronic filing reference
147    pub filing_reference: Option<String>,
148}
149
150impl RegulatoryFiling {
151    /// Creates a new filing instance.
152    pub fn new(
153        filing_type: FilingType,
154        company_code: impl Into<String>,
155        jurisdiction: impl Into<String>,
156        period_end: NaiveDate,
157        deadline: NaiveDate,
158        regulator: impl Into<String>,
159    ) -> Self {
160        Self {
161            filing_type,
162            company_code: company_code.into(),
163            jurisdiction: jurisdiction.into(),
164            period_end,
165            deadline,
166            filing_date: None,
167            status: FilingStatus::Pending,
168            regulator: regulator.into(),
169            filing_reference: None,
170        }
171    }
172
173    /// Marks as filed.
174    pub fn filed_on(mut self, date: NaiveDate) -> Self {
175        self.filing_date = Some(date);
176        self.status = if date <= self.deadline {
177            FilingStatus::Filed
178        } else {
179            FilingStatus::FiledLate
180        };
181        self
182    }
183
184    /// Returns the number of days until/past the deadline from a given date.
185    pub fn days_to_deadline(&self, from: NaiveDate) -> i64 {
186        (self.deadline - from).num_days()
187    }
188}
189
190impl ToNodeProperties for RegulatoryFiling {
191    fn node_type_name(&self) -> &'static str {
192        "regulatory_filing"
193    }
194    fn node_type_code(&self) -> u16 {
195        512
196    }
197    fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
198        let mut p = HashMap::new();
199        p.insert(
200            "filingType".into(),
201            GraphPropertyValue::String(self.filing_type.to_string()),
202        );
203        p.insert(
204            "companyCode".into(),
205            GraphPropertyValue::String(self.company_code.clone()),
206        );
207        p.insert(
208            "jurisdiction".into(),
209            GraphPropertyValue::String(self.jurisdiction.clone()),
210        );
211        p.insert(
212            "periodEnd".into(),
213            GraphPropertyValue::Date(self.period_end),
214        );
215        p.insert("deadline".into(), GraphPropertyValue::Date(self.deadline));
216        p.insert(
217            "status".into(),
218            GraphPropertyValue::String(format!("{:?}", self.status)),
219        );
220        p.insert(
221            "regulator".into(),
222            GraphPropertyValue::String(self.regulator.clone()),
223        );
224        if let Some(fd) = self.filing_date {
225            p.insert("filingDate".into(), GraphPropertyValue::Date(fd));
226        }
227        if let Some(ref fref) = self.filing_reference {
228            p.insert(
229                "filingReference".into(),
230                GraphPropertyValue::String(fref.clone()),
231            );
232        }
233        p
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    fn date(y: i32, m: u32, d: u32) -> NaiveDate {
242        NaiveDate::from_ymd_opt(y, m, d).expect("valid date")
243    }
244
245    #[test]
246    fn test_filing_creation() {
247        let filing = RegulatoryFiling::new(
248            FilingType::Form10K,
249            "C001",
250            "US",
251            date(2024, 12, 31),
252            date(2025, 3, 1),
253            "SEC",
254        );
255        assert_eq!(filing.company_code, "C001");
256        assert_eq!(filing.jurisdiction, "US");
257        assert_eq!(filing.status, FilingStatus::Pending);
258        assert!(filing.filing_date.is_none());
259    }
260
261    #[test]
262    fn test_filing_on_time() {
263        let filing = RegulatoryFiling::new(
264            FilingType::Form10K,
265            "C001",
266            "US",
267            date(2024, 12, 31),
268            date(2025, 3, 1),
269            "SEC",
270        )
271        .filed_on(date(2025, 2, 15));
272        assert_eq!(filing.status, FilingStatus::Filed);
273        assert_eq!(filing.filing_date, Some(date(2025, 2, 15)));
274    }
275
276    #[test]
277    fn test_filing_late() {
278        let filing = RegulatoryFiling::new(
279            FilingType::Form10K,
280            "C001",
281            "US",
282            date(2024, 12, 31),
283            date(2025, 3, 1),
284            "SEC",
285        )
286        .filed_on(date(2025, 3, 15));
287        assert_eq!(filing.status, FilingStatus::FiledLate);
288    }
289
290    #[test]
291    fn test_days_to_deadline() {
292        let filing = RegulatoryFiling::new(
293            FilingType::Form10Q,
294            "C001",
295            "US",
296            date(2024, 9, 30),
297            date(2024, 11, 9),
298            "SEC",
299        );
300        assert_eq!(filing.days_to_deadline(date(2024, 10, 9)), 31);
301        assert_eq!(filing.days_to_deadline(date(2024, 11, 9)), 0);
302        assert_eq!(filing.days_to_deadline(date(2024, 11, 19)), -10);
303    }
304
305    #[test]
306    fn test_filing_type_display() {
307        assert_eq!(format!("{}", FilingType::Form10K), "10-K");
308        assert_eq!(
309            format!("{}", FilingType::Jahresabschluss),
310            "Jahresabschluss"
311        );
312        assert_eq!(
313            format!("{}", FilingType::Custom("CbCR".to_string())),
314            "CbCR"
315        );
316    }
317}