Skip to main content

datasynth_generators/industry/
common.rs

1//! Common traits and types for industry-specific modules.
2
3use chrono::NaiveDate;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Trait for industry-specific transactions.
9pub trait IndustryTransaction: std::fmt::Debug + Send + Sync {
10    /// Returns the transaction type name.
11    fn transaction_type(&self) -> &str;
12
13    /// Returns the transaction date.
14    fn date(&self) -> NaiveDate;
15
16    /// Returns the transaction amount (if applicable).
17    fn amount(&self) -> Option<Decimal>;
18
19    /// Returns the GL account(s) impacted.
20    fn accounts(&self) -> Vec<String>;
21
22    /// Converts to journal entry line items.
23    fn to_journal_lines(&self) -> Vec<IndustryJournalLine>;
24
25    /// Returns metadata for the transaction.
26    fn metadata(&self) -> HashMap<String, String>;
27}
28
29/// Journal line generated from industry transaction.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct IndustryJournalLine {
32    /// GL account number.
33    pub account: String,
34    /// Debit amount (zero if credit).
35    pub debit: Decimal,
36    /// Credit amount (zero if debit).
37    pub credit: Decimal,
38    /// Description.
39    pub description: String,
40    /// Cost center (if applicable).
41    pub cost_center: Option<String>,
42    /// Additional dimensions.
43    pub dimensions: HashMap<String, String>,
44}
45
46impl IndustryJournalLine {
47    /// Creates a debit line.
48    pub fn debit(
49        account: impl Into<String>,
50        amount: Decimal,
51        description: impl Into<String>,
52    ) -> Self {
53        Self {
54            account: account.into(),
55            debit: amount,
56            credit: Decimal::ZERO,
57            description: description.into(),
58            cost_center: None,
59            dimensions: HashMap::new(),
60        }
61    }
62
63    /// Creates a credit line.
64    pub fn credit(
65        account: impl Into<String>,
66        amount: Decimal,
67        description: impl Into<String>,
68    ) -> Self {
69        Self {
70            account: account.into(),
71            debit: Decimal::ZERO,
72            credit: amount,
73            description: description.into(),
74            cost_center: None,
75            dimensions: HashMap::new(),
76        }
77    }
78
79    /// Sets the cost center.
80    pub fn with_cost_center(mut self, cost_center: impl Into<String>) -> Self {
81        self.cost_center = Some(cost_center.into());
82        self
83    }
84
85    /// Adds a dimension.
86    pub fn with_dimension(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
87        self.dimensions.insert(key.into(), value.into());
88        self
89    }
90}
91
92/// Trait for industry-specific anomalies.
93pub trait IndustryAnomaly: std::fmt::Debug + Send + Sync {
94    /// Returns the anomaly type name.
95    fn anomaly_type(&self) -> &str;
96
97    /// Returns the severity (1-5).
98    fn severity(&self) -> u8;
99
100    /// Returns detection difficulty.
101    fn detection_difficulty(&self) -> &str;
102
103    /// Returns indicators that should trigger detection.
104    fn indicators(&self) -> Vec<String>;
105
106    /// Returns related regulatory concerns.
107    fn regulatory_concerns(&self) -> Vec<String>;
108}
109
110/// Trait for industry-specific transaction generators.
111///
112/// This is the intended future API for pluggable industry modules.
113/// Concrete implementations will be added as each industry vertical is built out.
114#[allow(unused)]
115pub trait IndustryTransactionGenerator: Send + Sync {
116    /// The transaction type produced by this generator.
117    type Transaction: IndustryTransaction;
118
119    /// The anomaly type produced by this generator.
120    type Anomaly: IndustryAnomaly;
121
122    /// Generates transactions for a period.
123    fn generate_transactions(
124        &self,
125        start_date: NaiveDate,
126        end_date: NaiveDate,
127        count: usize,
128    ) -> Vec<Self::Transaction>;
129
130    /// Generates industry-specific anomalies.
131    fn generate_anomalies(&self, transactions: &[Self::Transaction]) -> Vec<Self::Anomaly>;
132
133    /// Returns industry-specific GL accounts.
134    fn gl_accounts(&self) -> Vec<IndustryGlAccount>;
135}
136
137/// Industry-specific GL account definition.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct IndustryGlAccount {
140    /// Account number.
141    pub account_number: String,
142    /// Account name.
143    pub name: String,
144    /// Account type (Asset, Liability, Revenue, Expense, Equity).
145    pub account_type: String,
146    /// Industry-specific category.
147    pub category: String,
148    /// Whether this is a control account.
149    pub is_control: bool,
150    /// Normal balance (Debit or Credit).
151    pub normal_balance: String,
152}
153
154impl IndustryGlAccount {
155    /// Creates a new GL account.
156    pub fn new(
157        number: impl Into<String>,
158        name: impl Into<String>,
159        account_type: impl Into<String>,
160        category: impl Into<String>,
161    ) -> Self {
162        Self {
163            account_number: number.into(),
164            name: name.into(),
165            account_type: account_type.into(),
166            category: category.into(),
167            is_control: false,
168            normal_balance: "Debit".to_string(),
169        }
170    }
171
172    /// Marks as control account.
173    pub fn into_control(mut self) -> Self {
174        self.is_control = true;
175        self
176    }
177
178    /// Sets normal balance.
179    pub fn with_normal_balance(mut self, balance: impl Into<String>) -> Self {
180        self.normal_balance = balance.into();
181        self
182    }
183}
184
185#[cfg(test)]
186#[allow(clippy::unwrap_used)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_journal_line() {
192        let debit = IndustryJournalLine::debit("1000", Decimal::new(1000, 0), "Test debit")
193            .with_cost_center("CC001")
194            .with_dimension("project", "P001");
195
196        assert_eq!(debit.account, "1000");
197        assert_eq!(debit.debit, Decimal::new(1000, 0));
198        assert_eq!(debit.credit, Decimal::ZERO);
199        assert_eq!(debit.cost_center, Some("CC001".to_string()));
200        assert_eq!(debit.dimensions.get("project"), Some(&"P001".to_string()));
201
202        let credit = IndustryJournalLine::credit("2000", Decimal::new(1000, 0), "Test credit");
203        assert_eq!(credit.debit, Decimal::ZERO);
204        assert_eq!(credit.credit, Decimal::new(1000, 0));
205    }
206
207    #[test]
208    fn test_gl_account() {
209        let account =
210            IndustryGlAccount::new("5100", "Cost of Goods Sold", "Expense", "Manufacturing")
211                .into_control()
212                .with_normal_balance("Debit");
213
214        assert_eq!(account.account_number, "5100");
215        assert!(account.is_control);
216        assert_eq!(account.normal_balance, "Debit");
217    }
218}