1use 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
18pub 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
79pub 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
87pub 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
123pub 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
170pub fn unbalanced_journal_entry() -> JournalEntry {
172 let mut entry = balanced_journal_entry(Decimal::new(1000, 2));
173 entry.lines[1].credit_amount = Decimal::new(500, 2);
175 entry.lines[1].local_amount = Decimal::new(-500, 2);
176 entry
177}
178
179pub 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
193pub 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}