Skip to main content

datasynth_test_utils/
fixtures.rs

1//! Pre-built test fixtures and configurations.
2
3use chrono::{NaiveDate, Utc};
4use rust_decimal::Decimal;
5use uuid::Uuid;
6
7use datasynth_config::schema::{
8    AccountingStandardsConfig, AuditGenerationConfig, AuditStandardsConfig, ChartOfAccountsConfig,
9    CompanyConfig, DataQualitySchemaConfig, FraudConfig, GeneratorConfig, GlobalConfig,
10    GraphExportConfig, OcpmConfig, OutputConfig, RateLimitSchemaConfig, RelationshipSchemaConfig,
11    ScenarioConfig, StreamingSchemaConfig, TemporalAttributeSchemaConfig, TransactionVolume,
12};
13use datasynth_core::models::{
14    AccountSubType, AccountType, BusinessProcess, CoAComplexity, GLAccount, IndustrySector,
15    JournalEntry, JournalEntryHeader, JournalEntryLine, TransactionSource,
16};
17
18/// Create a minimal test configuration.
19pub fn minimal_config() -> GeneratorConfig {
20    GeneratorConfig {
21        global: GlobalConfig {
22            seed: Some(42),
23            industry: IndustrySector::Manufacturing,
24            start_date: "2024-01-01".to_string(),
25            period_months: 1,
26            group_currency: "USD".to_string(),
27            parallel: false,
28            worker_threads: 0,
29            memory_limit_mb: 0,
30        },
31        companies: vec![CompanyConfig {
32            code: "TEST".to_string(),
33            name: "Test Company".to_string(),
34            currency: "USD".to_string(),
35            country: "US".to_string(),
36            annual_transaction_volume: TransactionVolume::TenK,
37            volume_weight: 1.0,
38            fiscal_year_variant: "K4".to_string(),
39        }],
40        chart_of_accounts: ChartOfAccountsConfig {
41            complexity: CoAComplexity::Small,
42            industry_specific: false,
43            custom_accounts: None,
44            min_hierarchy_depth: 2,
45            max_hierarchy_depth: 3,
46        },
47        transactions: Default::default(),
48        output: OutputConfig::default(),
49        fraud: FraudConfig {
50            enabled: false,
51            ..Default::default()
52        },
53        internal_controls: Default::default(),
54        business_processes: Default::default(),
55        user_personas: Default::default(),
56        templates: Default::default(),
57        approval: Default::default(),
58        departments: Default::default(),
59        master_data: Default::default(),
60        document_flows: Default::default(),
61        intercompany: Default::default(),
62        balance: Default::default(),
63        ocpm: OcpmConfig::default(),
64        audit: AuditGenerationConfig::default(),
65        banking: datasynth_banking::BankingConfig::default(),
66        data_quality: DataQualitySchemaConfig::default(),
67        scenario: ScenarioConfig::default(),
68        temporal: Default::default(),
69        graph_export: GraphExportConfig::default(),
70        streaming: StreamingSchemaConfig::default(),
71        rate_limit: RateLimitSchemaConfig::default(),
72        temporal_attributes: TemporalAttributeSchemaConfig::default(),
73        relationships: RelationshipSchemaConfig::default(),
74        accounting_standards: AccountingStandardsConfig::default(),
75        audit_standards: AuditStandardsConfig::default(),
76    }
77}
78
79/// Create a test configuration with fraud enabled.
80pub fn fraud_enabled_config() -> GeneratorConfig {
81    let mut config = minimal_config();
82    config.fraud.enabled = true;
83    config.fraud.fraud_rate = 0.1;
84    config
85}
86
87/// Create a test configuration for multi-company scenarios.
88pub fn multi_company_config() -> GeneratorConfig {
89    let mut config = minimal_config();
90    config.companies = vec![
91        CompanyConfig {
92            code: "1000".to_string(),
93            name: "Parent Company".to_string(),
94            currency: "USD".to_string(),
95            country: "US".to_string(),
96            annual_transaction_volume: TransactionVolume::TenK,
97            volume_weight: 0.6,
98            fiscal_year_variant: "K4".to_string(),
99        },
100        CompanyConfig {
101            code: "2000".to_string(),
102            name: "Subsidiary EU".to_string(),
103            currency: "EUR".to_string(),
104            country: "DE".to_string(),
105            annual_transaction_volume: TransactionVolume::TenK,
106            volume_weight: 0.3,
107            fiscal_year_variant: "K4".to_string(),
108        },
109        CompanyConfig {
110            code: "3000".to_string(),
111            name: "Subsidiary Asia".to_string(),
112            currency: "JPY".to_string(),
113            country: "JP".to_string(),
114            annual_transaction_volume: TransactionVolume::TenK,
115            volume_weight: 0.1,
116            fiscal_year_variant: "K4".to_string(),
117        },
118    ];
119    config.global.period_months = 12;
120    config
121}
122
123/// Create a balanced test journal entry.
124pub fn balanced_journal_entry(amount: Decimal) -> JournalEntry {
125    let doc_id = Uuid::new_v4();
126    let posting_date = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
127
128    JournalEntry {
129        header: JournalEntryHeader {
130            document_id: doc_id,
131            company_code: "TEST".to_string(),
132            fiscal_year: 2024,
133            fiscal_period: 1,
134            posting_date,
135            document_date: posting_date,
136            created_at: Utc::now(),
137            document_type: "SA".to_string(),
138            currency: "USD".to_string(),
139            exchange_rate: Decimal::ONE,
140            reference: None,
141            header_text: Some("Test entry".to_string()),
142            created_by: "TESTUSER".to_string(),
143            user_persona: "test_user".to_string(),
144            source: TransactionSource::Manual,
145            business_process: Some(BusinessProcess::R2R),
146            ledger: "0L".to_string(),
147            is_fraud: false,
148            fraud_type: None,
149            batch_id: None,
150            control_ids: vec![],
151            sox_relevant: false,
152            control_status: Default::default(),
153            sod_violation: false,
154            sod_conflict_type: None,
155            approval_workflow: None,
156            ocpm_event_ids: vec![],
157            ocpm_object_ids: vec![],
158            ocpm_case_id: None,
159            is_anomaly: false,
160            anomaly_id: None,
161            anomaly_type: None,
162        },
163        lines: vec![
164            JournalEntryLine::debit(doc_id, 1, "100000".to_string(), amount),
165            JournalEntryLine::credit(doc_id, 2, "200000".to_string(), amount),
166        ],
167    }
168}
169
170/// Create an unbalanced journal entry (for testing error cases).
171pub fn unbalanced_journal_entry() -> JournalEntry {
172    let mut entry = balanced_journal_entry(Decimal::new(1000, 2));
173    // Make it unbalanced by changing the credit amount
174    entry.lines[1].credit_amount = Decimal::new(500, 2);
175    entry.lines[1].local_amount = Decimal::new(-500, 2);
176    entry
177}
178
179/// Create a test GL account.
180pub fn test_gl_account(
181    number: &str,
182    account_type: AccountType,
183    sub_type: AccountSubType,
184) -> GLAccount {
185    GLAccount::new(
186        number.to_string(),
187        format!("Test Account {}", number),
188        account_type,
189        sub_type,
190    )
191}
192
193/// Create test GL accounts for common account types.
194pub fn standard_test_accounts() -> Vec<GLAccount> {
195    vec![
196        test_gl_account("100000", AccountType::Asset, AccountSubType::Cash),
197        test_gl_account(
198            "110000",
199            AccountType::Asset,
200            AccountSubType::AccountsReceivable,
201        ),
202        test_gl_account("120000", AccountType::Asset, AccountSubType::Inventory),
203        test_gl_account("150000", AccountType::Asset, AccountSubType::FixedAssets),
204        test_gl_account(
205            "200000",
206            AccountType::Liability,
207            AccountSubType::AccountsPayable,
208        ),
209        test_gl_account(
210            "210000",
211            AccountType::Liability,
212            AccountSubType::AccruedLiabilities,
213        ),
214        test_gl_account(
215            "300000",
216            AccountType::Equity,
217            AccountSubType::RetainedEarnings,
218        ),
219        test_gl_account(
220            "400000",
221            AccountType::Revenue,
222            AccountSubType::ProductRevenue,
223        ),
224        test_gl_account(
225            "500000",
226            AccountType::Expense,
227            AccountSubType::CostOfGoodsSold,
228        ),
229        test_gl_account(
230            "600000",
231            AccountType::Expense,
232            AccountSubType::OperatingExpenses,
233        ),
234    ]
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn test_minimal_config_is_valid() {
243        let config = minimal_config();
244        assert_eq!(config.global.period_months, 1);
245        assert_eq!(config.companies.len(), 1);
246        assert_eq!(config.companies[0].code, "TEST");
247    }
248
249    #[test]
250    fn test_fraud_enabled_config() {
251        let config = fraud_enabled_config();
252        assert!(config.fraud.enabled);
253        assert!((config.fraud.fraud_rate - 0.1).abs() < f64::EPSILON);
254    }
255
256    #[test]
257    fn test_multi_company_config() {
258        let config = multi_company_config();
259        assert_eq!(config.companies.len(), 3);
260        assert_eq!(config.global.period_months, 12);
261    }
262
263    #[test]
264    fn test_balanced_entry_is_balanced() {
265        let entry = balanced_journal_entry(Decimal::new(10000, 2));
266        let total_debits: Decimal = entry.lines.iter().map(|l| l.debit_amount).sum();
267        let total_credits: Decimal = entry.lines.iter().map(|l| l.credit_amount).sum();
268        assert_eq!(total_debits, total_credits);
269    }
270
271    #[test]
272    fn test_unbalanced_entry_is_unbalanced() {
273        let entry = unbalanced_journal_entry();
274        let total_debits: Decimal = entry.lines.iter().map(|l| l.debit_amount).sum();
275        let total_credits: Decimal = entry.lines.iter().map(|l| l.credit_amount).sum();
276        assert_ne!(total_debits, total_credits);
277    }
278
279    #[test]
280    fn test_standard_accounts_cover_all_types() {
281        let accounts = standard_test_accounts();
282        assert!(accounts
283            .iter()
284            .any(|a| a.account_type == AccountType::Asset));
285        assert!(accounts
286            .iter()
287            .any(|a| a.account_type == AccountType::Liability));
288        assert!(accounts
289            .iter()
290            .any(|a| a.account_type == AccountType::Equity));
291        assert!(accounts
292            .iter()
293            .any(|a| a.account_type == AccountType::Revenue));
294        assert!(accounts
295            .iter()
296            .any(|a| a.account_type == AccountType::Expense));
297    }
298}