1use std::cell::Cell;
8use std::io::Write;
9use std::path::Path;
10
11use crate::enhanced_orchestrator::EnhancedGenerationResult;
12use datasynth_core::documents::PaymentType;
13use datasynth_output::OutputRootConfig;
14use tracing::{info, warn};
15
16thread_local! {
17 static FLAT_LAYOUT_ACTIVE: Cell<bool> = const { Cell::new(false) };
22
23 static SKIP_JSON: Cell<bool> = const { Cell::new(false) };
27}
28
29fn write_json<T: serde::Serialize>(
42 data: &[T],
43 path: &Path,
44 label: &str,
45) -> Result<(), Box<dyn std::error::Error>> {
46 use std::io::Write;
47
48 if data.is_empty() {
49 return Ok(());
50 }
51
52 let file = std::fs::File::create(path)?;
53 let mut writer = std::io::BufWriter::with_capacity(512 * 1024, file);
54
55 writer.write_all(b"[\n")?;
57 for (i, item) in data.iter().enumerate() {
58 if i > 0 {
59 writer.write_all(b",\n")?;
60 }
61 serde_json::to_writer_pretty(&mut writer, item)?;
62 }
63 writer.write_all(b"\n]\n")?;
64 writer.flush()?;
65
66 info!(
67 " {} written: {} records -> {}",
68 label,
69 data.len(),
70 path.display()
71 );
72 Ok(())
73}
74
75fn write_journal_entries_csv(
80 result: &EnhancedGenerationResult,
81 output_dir: &Path,
82) -> Result<(), Box<dyn std::error::Error>> {
83 if result.journal_entries.is_empty() {
84 return Ok(());
85 }
86
87 let path = output_dir.join("journal_entries.csv");
88 let file = std::fs::File::create(&path)?;
89 let mut w = std::io::BufWriter::with_capacity(256 * 1024, file);
90
91 writeln!(
106 w,
107 "document_id,company_code,fiscal_year,fiscal_period,posting_date,document_date,\
108 document_type,currency,exchange_rate,reference,header_text,created_by,source,\
109 business_process,ledger,is_fraud,is_anomaly,\
110 line_number,gl_account,debit_amount,credit_amount,local_amount,\
111 cost_center,profit_center,line_text,\
112 auxiliary_account_number,auxiliary_account_label,lettrage,lettrage_date,\
113 is_manual,is_post_close,source_system,\
114 account_description,financial_statement_category,\
115 assignment,value_date,tax_code,transaction_id,\
116 account_class,account_class_name,account_sub_class,account_sub_class_name"
117 )?;
118
119 let coa_index: std::collections::HashMap<&str, (&str, &str, &str, &str, &str)> = result
124 .chart_of_accounts
125 .accounts
126 .iter()
127 .map(|a| {
128 (
129 a.account_number.as_str(),
130 (
131 a.short_description.as_str(),
132 a.account_class.as_str(),
133 a.account_class_name.as_str(),
134 a.account_sub_class.as_str(),
135 a.account_sub_class_name.as_str(),
136 ),
137 )
138 })
139 .collect();
140
141 for je in &result.journal_entries {
142 let h = &je.header;
143 for line in &je.lines {
144 let lettrage_date_str = line
145 .lettrage_date
146 .map(|d| d.to_string())
147 .unwrap_or_default();
148 let value_date_str = line.value_date.map(|d| d.to_string()).unwrap_or_default();
149 let coa_hit = coa_index.get(line.gl_account.as_str()).copied();
151 let coa_short_desc = coa_hit.map(|t| t.0).unwrap_or("");
152 let coa_class = coa_hit.map(|t| t.1).unwrap_or("");
153 let coa_class_name = coa_hit.map(|t| t.2).unwrap_or("");
154 let coa_sub_class = coa_hit.map(|t| t.3).unwrap_or("");
155 let coa_sub_class_name = coa_hit.map(|t| t.4).unwrap_or("");
156 let account_description: &str = line
160 .account_description
161 .as_deref()
162 .filter(|s| !s.is_empty())
163 .unwrap_or(coa_short_desc);
164 let fsa_category =
167 datasynth_core::accounts::AccountCategory::from_account(line.gl_account.as_str())
168 .as_label();
169 let transaction_id = line.transaction_id.clone().unwrap_or_else(|| {
171 datasynth_core::models::JournalEntryLine::derive_transaction_id(
172 line.document_id,
173 line.line_number,
174 )
175 });
176 writeln!(
177 w,
178 "{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
179 h.document_id,
180 csv_escape(&h.company_code),
181 h.fiscal_year,
182 h.fiscal_period,
183 h.posting_date,
184 h.document_date,
185 csv_escape(&h.document_type),
186 csv_escape(&h.currency),
187 h.exchange_rate,
188 csv_opt_str(&h.reference),
189 csv_opt_str(&h.header_text),
190 csv_escape(&h.created_by),
191 h.source,
192 h.business_process
193 .map(|bp| format!("{bp:?}"))
194 .unwrap_or_default(),
195 csv_escape(&h.ledger),
196 h.is_fraud,
197 h.is_anomaly,
198 line.line_number,
199 csv_escape(&line.gl_account),
200 line.debit_amount,
201 line.credit_amount,
202 line.local_amount,
203 csv_opt_str(&line.cost_center),
204 csv_opt_str(&line.profit_center),
205 csv_opt_str(&line.line_text),
206 csv_opt_str(&line.auxiliary_account_number),
207 csv_opt_str(&line.auxiliary_account_label),
208 csv_opt_str(&line.lettrage),
209 lettrage_date_str,
210 h.is_manual,
211 h.is_post_close,
212 csv_escape(&h.source_system),
213 csv_escape(account_description),
214 fsa_category,
215 csv_opt_str(&line.assignment),
216 value_date_str,
217 csv_opt_str(&line.tax_code),
218 csv_escape(&transaction_id),
219 csv_escape(coa_class),
220 csv_escape(coa_class_name),
221 csv_escape(coa_sub_class),
222 csv_escape(coa_sub_class_name),
223 )?;
224 }
225 }
226
227 w.flush()?;
228 let total_lines: usize = result.journal_entries.iter().map(|je| je.lines.len()).sum();
229 info!(
230 " Journal entries CSV written: {} entries, {} line items -> {}",
231 result.journal_entries.len(),
232 total_lines,
233 path.display()
234 );
235 Ok(())
236}
237
238fn write_journal_entries_flat_json(
243 result: &EnhancedGenerationResult,
244 output_dir: &Path,
245) -> Result<(), Box<dyn std::error::Error>> {
246 if result.journal_entries.is_empty() {
247 return Ok(());
248 }
249
250 let path = output_dir.join("journal_entries.json");
251 let file = std::fs::File::create(&path)?;
252 let mut writer = std::io::BufWriter::with_capacity(256 * 1024, file);
253
254 writer.write_all(b"[\n")?;
256
257 let mut first = true;
258 let mut total_lines = 0usize;
259 for je in &result.journal_entries {
260 let header_value = serde_json::to_value(&je.header)?;
262
263 for line in &je.lines {
264 if !first {
265 writer.write_all(b",\n")?;
266 }
267 first = false;
268 total_lines += 1;
269
270 let mut line_value = serde_json::to_value(line)?;
272
273 if let serde_json::Value::Object(ref header_map) = header_value {
274 if let serde_json::Value::Object(ref mut line_map) = line_value {
275 for (key, val) in header_map {
276 if !line_map.contains_key(key) {
278 line_map.insert(key.clone(), val.clone());
279 }
280 }
281 }
282 }
283
284 serde_json::to_writer_pretty(&mut writer, &line_value)?;
285 }
286 }
287
288 writer.write_all(b"\n]\n")?;
289 writer.flush()?;
290 info!(
291 " Journal entries (flat JSON) written: {} line items -> {}",
292 total_lines,
293 path.display()
294 );
295 Ok(())
296}
297
298fn add_ocel_object_type_alias(value: &mut serde_json::Value) {
306 if let Some(events) = value.get_mut("events").and_then(|v| v.as_array_mut()) {
307 for event in events.iter_mut() {
308 if let Some(refs) = event.get_mut("object_refs").and_then(|r| r.as_array_mut()) {
309 for oref in refs.iter_mut() {
310 if let Some(obj) = oref.as_object_mut() {
311 if let Some(oti) = obj.get("object_type_id").cloned() {
312 obj.entry("object_type").or_insert(oti);
313 }
314 }
315 }
316 }
317 }
318 }
319}
320
321fn csv_escape(s: &str) -> String {
323 if s.contains(',') || s.contains('"') || s.contains('\n') {
324 format!("\"{}\"", s.replace('"', "\"\""))
325 } else {
326 s.to_string()
327 }
328}
329
330fn csv_opt_str(opt: &Option<String>) -> String {
332 match opt {
333 Some(s) => csv_escape(s),
334 None => String::new(),
335 }
336}
337
338#[allow(dead_code)]
345pub fn write_all_output(
346 result: &EnhancedGenerationResult,
347 output_dir: &Path,
348) -> Result<(), Box<dyn std::error::Error>> {
349 write_all_output_with_layout(
350 result,
351 output_dir,
352 datasynth_config::ExportLayout::Nested,
353 &[
354 datasynth_config::FileFormat::Csv,
355 datasynth_config::FileFormat::Json,
356 ],
357 )
358}
359
360#[allow(dead_code)]
374pub fn write_all_output_with_root(
375 result: &EnhancedGenerationResult,
376 root: &OutputRootConfig,
377 export_layout: datasynth_config::ExportLayout,
378 formats: &[datasynth_config::FileFormat],
379) -> Result<(), Box<dyn std::error::Error>> {
380 let effective = root.effective_dir();
381 write_all_output_with_layout(result, &effective, export_layout, formats)
382}
383
384pub fn write_all_output_with_layout(
390 result: &EnhancedGenerationResult,
391 output_dir: &Path,
392 export_layout: datasynth_config::ExportLayout,
393 formats: &[datasynth_config::FileFormat],
394) -> Result<(), Box<dyn std::error::Error>> {
395 let csv_enabled = formats.is_empty()
396 || formats.contains(&datasynth_config::FileFormat::Csv)
397 || formats.contains(&datasynth_config::FileFormat::Parquet);
398 let json_enabled = formats.is_empty()
399 || formats.contains(&datasynth_config::FileFormat::Json)
400 || formats.contains(&datasynth_config::FileFormat::JsonLines);
401 std::fs::create_dir_all(output_dir)?;
402 info!("Writing comprehensive output to: {}", output_dir.display());
403
404 struct FlatLayoutGuard;
407 impl Drop for FlatLayoutGuard {
408 fn drop(&mut self) {
409 FLAT_LAYOUT_ACTIVE.with(|c| c.set(false));
410 }
411 }
412 let _flat_guard = if export_layout == datasynth_config::ExportLayout::Flat {
413 FLAT_LAYOUT_ACTIVE.with(|c| c.set(true));
414 Some(FlatLayoutGuard)
415 } else {
416 None
417 };
418
419 struct SkipJsonGuard;
421 impl Drop for SkipJsonGuard {
422 fn drop(&mut self) {
423 SKIP_JSON.with(|c| c.set(false));
424 }
425 }
426 let _skip_json_guard = if !json_enabled {
427 SKIP_JSON.with(|c| c.set(true));
428 info!("JSON output skipped (not in requested formats)");
429 Some(SkipJsonGuard)
430 } else {
431 None
432 };
433
434 if !result.journal_entries.is_empty() {
438 let do_csv = csv_enabled;
439 let do_json = json_enabled;
440 let is_flat = export_layout == datasynth_config::ExportLayout::Flat;
441
442 std::thread::scope(|s| {
443 if do_csv {
444 s.spawn(|| {
445 if let Err(e) = write_journal_entries_csv(result, output_dir) {
446 warn!("Failed to write journal_entries.csv: {}", e);
447 }
448 });
449 }
450 if do_json {
451 s.spawn(|| {
452 if is_flat {
453 if let Err(e) = write_journal_entries_flat_json(result, output_dir) {
454 warn!("Failed to write flat journal_entries.json: {}", e);
455 }
456 } else if let Err(e) = write_json(
457 &result.journal_entries,
458 &output_dir.join("journal_entries.json"),
459 "Journal entries (JSON)",
460 ) {
461 warn!("Failed to write journal_entries.json: {}", e);
462 }
463 });
464 }
465 });
466 }
467
468 let md_dir = output_dir.join("master_data");
472 if !result.master_data.vendors.is_empty()
473 || !result.master_data.customers.is_empty()
474 || !result.master_data.materials.is_empty()
475 || !result.master_data.assets.is_empty()
476 || !result.master_data.employees.is_empty()
477 || !result.master_data.cost_centers.is_empty()
478 || !result.master_data.profit_centers.is_empty()
479 {
480 std::fs::create_dir_all(&md_dir)?;
481 info!("Writing master data...");
482
483 write_json_safe(
484 &result.master_data.vendors,
485 &md_dir.join("vendors.json"),
486 "Vendors",
487 );
488 write_json_safe(
489 &result.master_data.customers,
490 &md_dir.join("customers.json"),
491 "Customers",
492 );
493 write_json_safe(
494 &result.master_data.materials,
495 &md_dir.join("materials.json"),
496 "Materials",
497 );
498 write_json_safe(
499 &result.master_data.assets,
500 &md_dir.join("fixed_assets.json"),
501 "Fixed assets",
502 );
503 write_json_safe(
504 &result.master_data.employees,
505 &md_dir.join("employees.json"),
506 "Employees",
507 );
508 write_json_safe(
509 &result.master_data.cost_centers,
510 &md_dir.join("cost_centers.json"),
511 "Cost centers",
512 );
513 write_json_safe(
515 &result.master_data.profit_centers,
516 &md_dir.join("profit_centers.json"),
517 "Profit centres",
518 );
519 write_json_safe(
521 &result.master_data.organizational_profiles,
522 &md_dir.join("organizational_profiles.json"),
523 "Organizational profiles (v3.3.0)",
524 );
525 }
526
527 let df_dir = output_dir.join("document_flows");
531 let flat_mode = export_layout == datasynth_config::ExportLayout::Flat;
532 if !result.document_flows.purchase_orders.is_empty()
533 || !result.document_flows.sales_orders.is_empty()
534 {
535 std::fs::create_dir_all(&df_dir)?;
536 info!("Writing document flows...");
537
538 write_json_auto(
539 &result.document_flows.purchase_orders,
540 &df_dir.join("purchase_orders.json"),
541 "Purchase orders",
542 flat_mode,
543 );
544 write_json_auto(
545 &result.document_flows.goods_receipts,
546 &df_dir.join("goods_receipts.json"),
547 "Goods receipts",
548 flat_mode,
549 );
550 write_json_auto(
551 &result.document_flows.vendor_invoices,
552 &df_dir.join("vendor_invoices.json"),
553 "Vendor invoices",
554 flat_mode,
555 );
556 write_json_auto(
557 &result.document_flows.payments,
558 &df_dir.join("payments.json"),
559 "Payments",
560 flat_mode,
561 );
562 let customer_receipts: Vec<_> = result
563 .document_flows
564 .payments
565 .iter()
566 .filter(|p| p.payment_type == PaymentType::ArReceipt)
567 .collect();
568 write_json_auto(
569 &customer_receipts,
570 &df_dir.join("customer_receipts.json"),
571 "Customer receipts",
572 flat_mode,
573 );
574 write_json_auto(
575 &result.document_flows.sales_orders,
576 &df_dir.join("sales_orders.json"),
577 "Sales orders",
578 flat_mode,
579 );
580 write_json_auto(
581 &result.document_flows.deliveries,
582 &df_dir.join("deliveries.json"),
583 "Deliveries",
584 flat_mode,
585 );
586 write_json_auto(
587 &result.document_flows.customer_invoices,
588 &df_dir.join("customer_invoices.json"),
589 "Customer invoices",
590 flat_mode,
591 );
592
593 match serde_json::to_value(&result.document_flows.document_references) {
599 Ok(mut v) => {
600 if let Some(arr) = v.as_array_mut() {
601 for r in arr.iter_mut() {
602 if let Some(obj) = r.as_object_mut() {
603 if let Some(st) = obj.get("source_doc_type").cloned() {
604 obj.entry("from_type").or_insert(st);
605 }
606 if let Some(si) = obj.get("source_doc_id").cloned() {
607 obj.entry("from_id").or_insert(si);
608 }
609 if let Some(tt) = obj.get("target_doc_type").cloned() {
610 obj.entry("to_type").or_insert(tt);
611 }
612 if let Some(ti) = obj.get("target_doc_id").cloned() {
613 obj.entry("to_id").or_insert(ti);
614 }
615 }
616 }
617 }
618 match serde_json::to_string_pretty(&v) {
619 Ok(json) => {
620 let path = df_dir.join("document_references.json");
621 if let Err(e) = std::fs::write(&path, json) {
622 warn!("Failed to write document references: {}", e);
623 } else {
624 info!(
625 " Document references written: {} records -> {}",
626 result.document_flows.document_references.len(),
627 path.display()
628 );
629 }
630 }
631 Err(e) => warn!("Failed to serialize document references: {}", e),
632 }
633 }
634 Err(e) => warn!("Failed to build document references Value: {}", e),
635 }
636
637 if !result.document_flows.p2p_chains.is_empty() {
640 info!(
641 " P2P chains: {} (data exported via individual document files)",
642 result.document_flows.p2p_chains.len()
643 );
644 }
645 if !result.document_flows.o2c_chains.is_empty() {
646 info!(
647 " O2C chains: {} (data exported via individual document files)",
648 result.document_flows.o2c_chains.len()
649 );
650 }
651 }
652
653 let sl_dir = output_dir.join("subledger");
657 if !result.subledger.ap_invoices.is_empty()
658 || !result.subledger.ar_invoices.is_empty()
659 || !result.subledger.fa_records.is_empty()
660 || !result.subledger.inventory_positions.is_empty()
661 {
662 std::fs::create_dir_all(&sl_dir)?;
663 info!("Writing subledger data...");
664
665 write_json_safe(
666 &result.subledger.ap_invoices,
667 &sl_dir.join("ap_invoices.json"),
668 "AP invoices",
669 );
670 write_json_safe(
671 &result.subledger.ar_invoices,
672 &sl_dir.join("ar_invoices.json"),
673 "AR invoices",
674 );
675 write_json_safe(
676 &result.subledger.fa_records,
677 &sl_dir.join("fa_records.json"),
678 "FA records",
679 );
680 write_json_safe(
681 &result.subledger.inventory_positions,
682 &sl_dir.join("inventory_positions.json"),
683 "Inventory positions",
684 );
685 write_json_safe(
686 &result.subledger.inventory_movements,
687 &sl_dir.join("inventory_movements.json"),
688 "Inventory movements",
689 );
690 write_json_safe(
691 &result.subledger.ar_aging_reports,
692 &sl_dir.join("ar_aging.json"),
693 "AR aging reports",
694 );
695 write_json_safe(
696 &result.subledger.ap_aging_reports,
697 &sl_dir.join("ap_aging.json"),
698 "AP aging reports",
699 );
700 write_json_safe(
701 &result.subledger.depreciation_runs,
702 &sl_dir.join("depreciation_runs.json"),
703 "Depreciation runs",
704 );
705 write_json_safe(
706 &result.subledger.inventory_valuations,
707 &sl_dir.join("inventory_valuation.json"),
708 "Inventory valuations",
709 );
710 write_json_safe(
712 &result.subledger.dunning_runs,
713 &sl_dir.join("dunning_runs.json"),
714 "Dunning runs",
715 );
716 write_json_safe(
717 &result.subledger.dunning_letters,
718 &sl_dir.join("dunning_letters.json"),
719 "Dunning letters",
720 );
721 }
722
723 let audit_dir = output_dir.join("audit");
727 if !result.audit.engagements.is_empty() {
728 std::fs::create_dir_all(&audit_dir)?;
729 info!("Writing audit data...");
730
731 write_json_safe(
732 &result.audit.engagements,
733 &audit_dir.join("audit_engagements.json"),
734 "Audit engagements",
735 );
736 write_json_safe(
737 &result.audit.audit_scopes,
738 &audit_dir.join("audit_scopes.json"),
739 "Audit scopes (ISA 220 / ISA 300)",
740 );
741 write_json_safe(
742 &result.audit.workpapers,
743 &audit_dir.join("audit_workpapers.json"),
744 "Audit workpapers",
745 );
746 write_json_safe(
747 &result.audit.evidence,
748 &audit_dir.join("audit_evidence.json"),
749 "Audit evidence",
750 );
751 write_json_safe(
752 &result.audit.risk_assessments,
753 &audit_dir.join("audit_risk_assessments.json"),
754 "Audit risk assessments",
755 );
756 write_json_safe(
757 &result.audit.findings,
758 &audit_dir.join("audit_findings.json"),
759 "Audit findings",
760 );
761 write_json_safe(
762 &result.audit.judgments,
763 &audit_dir.join("audit_judgments.json"),
764 "Audit judgments",
765 );
766 write_json_safe(
767 &result.audit.confirmations,
768 &audit_dir.join("audit_confirmations.json"),
769 "Audit confirmations",
770 );
771 write_json_safe(
772 &result.audit.confirmation_responses,
773 &audit_dir.join("audit_confirmation_responses.json"),
774 "Audit confirmation responses",
775 );
776 write_json_safe(
777 &result.audit.procedure_steps,
778 &audit_dir.join("audit_procedure_steps.json"),
779 "Audit procedure steps",
780 );
781 write_json_safe(
782 &result.audit.samples,
783 &audit_dir.join("audit_samples.json"),
784 "Audit samples",
785 );
786 write_json_safe(
787 &result.audit.analytical_results,
788 &audit_dir.join("audit_analytical_results.json"),
789 "Audit analytical results",
790 );
791 write_json_safe(
792 &result.audit.ia_functions,
793 &audit_dir.join("audit_ia_functions.json"),
794 "Audit IA functions",
795 );
796 write_json_safe(
797 &result.audit.ia_reports,
798 &audit_dir.join("audit_ia_reports.json"),
799 "Audit IA reports",
800 );
801 write_json_safe(
802 &result.audit.related_parties,
803 &audit_dir.join("audit_related_parties.json"),
804 "Audit related parties",
805 );
806 write_json_safe(
807 &result.audit.related_party_transactions,
808 &audit_dir.join("audit_related_party_transactions.json"),
809 "Audit related party transactions",
810 );
811 if !result.audit.component_auditors.is_empty() {
813 write_json_safe(
814 &result.audit.component_auditors,
815 &audit_dir.join("component_auditors.json"),
816 "Component auditors (ISA 600)",
817 );
818 if let Some(plan) = &result.audit.group_audit_plan {
819 write_json_single_safe(
820 plan,
821 &audit_dir.join("group_audit_plan.json"),
822 "Group audit plan (ISA 600)",
823 );
824 }
825 write_json_safe(
826 &result.audit.component_instructions,
827 &audit_dir.join("component_instructions.json"),
828 "Component instructions (ISA 600)",
829 );
830 write_json_safe(
831 &result.audit.component_reports,
832 &audit_dir.join("component_reports.json"),
833 "Component auditor reports (ISA 600)",
834 );
835 }
836 write_json_safe(
838 &result.audit.engagement_letters,
839 &audit_dir.join("engagement_letters.json"),
840 "Engagement letters (ISA 210)",
841 );
842 write_json_safe(
844 &result.audit.subsequent_events,
845 &audit_dir.join("subsequent_events.json"),
846 "Subsequent events (ISA 560 / IAS 10)",
847 );
848 write_json_safe(
850 &result.audit.service_organizations,
851 &audit_dir.join("service_organizations.json"),
852 "Service organizations (ISA 402)",
853 );
854 write_json_safe(
855 &result.audit.soc_reports,
856 &audit_dir.join("soc_reports.json"),
857 "SOC reports (ISA 402)",
858 );
859 write_json_safe(
860 &result.audit.user_entity_controls,
861 &audit_dir.join("user_entity_controls.json"),
862 "User entity controls (ISA 402)",
863 );
864
865 write_json_safe(
867 &result.audit.going_concern_assessments,
868 &audit_dir.join("going_concern_assessments.json"),
869 "Going concern assessments (ISA 570)",
870 );
871
872 write_json_safe(
874 &result.audit.accounting_estimates,
875 &audit_dir.join("accounting_estimates.json"),
876 "Accounting estimates (ISA 540)",
877 );
878
879 write_json_always(
885 &result.audit.audit_opinions,
886 &audit_dir.join("audit_opinions.json"),
887 "Audit opinions (ISA 700/705/706)",
888 );
889 write_json_always(
890 &result.audit.key_audit_matters,
891 &audit_dir.join("key_audit_matters.json"),
892 "Key Audit Matters (ISA 701)",
893 );
894
895 if !result.audit.sox_302_certifications.is_empty() {
897 write_json_safe(
898 &result.audit.sox_302_certifications,
899 &audit_dir.join("sox_302_certifications.json"),
900 "SOX 302 certifications",
901 );
902 write_json_safe(
903 &result.audit.sox_404_assessments,
904 &audit_dir.join("sox_404_assessments.json"),
905 "SOX 404 ICFR assessments",
906 );
907 }
908
909 if !result.audit.materiality_calculations.is_empty() {
911 write_json_safe(
912 &result.audit.materiality_calculations,
913 &audit_dir.join("materiality_calculations.json"),
914 "Materiality calculations (ISA 320)",
915 );
916 }
917
918 if !result.audit.combined_risk_assessments.is_empty() {
920 write_json_safe(
921 &result.audit.combined_risk_assessments,
922 &audit_dir.join("combined_risk_assessments.json"),
923 "Combined Risk Assessments (ISA 315)",
924 );
925 }
926
927 if !result.audit.sampling_plans.is_empty() {
929 write_json_safe(
930 &result.audit.sampling_plans,
931 &audit_dir.join("sampling_plans.json"),
932 "Sampling plans (ISA 530)",
933 );
934 write_json_safe(
935 &result.audit.sampled_items,
936 &audit_dir.join("sampled_items.json"),
937 "Sampled items (ISA 530)",
938 );
939 }
940
941 if !result.audit.significant_transaction_classes.is_empty() {
943 write_json_safe(
944 &result.audit.significant_transaction_classes,
945 &audit_dir.join("significant_transaction_classes.json"),
946 "Significant Classes of Transactions / SCOTS (ISA 315)",
947 );
948 }
949
950 if !result.audit.unusual_items.is_empty() {
952 write_json_safe(
953 &result.audit.unusual_items,
954 &audit_dir.join("unusual_items.json"),
955 "Unusual item flags (ISA 520)",
956 );
957 }
958
959 if !result.audit.analytical_relationships.is_empty() {
961 write_json_safe(
962 &result.audit.analytical_relationships,
963 &audit_dir.join("analytical_relationships.json"),
964 "Analytical relationships (ISA 520)",
965 );
966 }
967
968 if !result.audit.isa_pcaob_mappings.is_empty() {
970 write_json_safe(
971 &result.audit.isa_pcaob_mappings,
972 &audit_dir.join("isa_pcaob_mappings.json"),
973 "PCAOB-ISA standard mappings",
974 );
975 }
976
977 if !result.audit.isa_mappings.is_empty() {
979 write_json_safe(
980 &result.audit.isa_mappings,
981 &audit_dir.join("isa_mappings.json"),
982 "ISA standard reference mappings",
983 );
984 }
985
986 if let Some(ref event_trail) = result.audit.fsm_event_trail {
988 if !event_trail.is_empty() {
989 write_json_safe(
990 event_trail,
991 &audit_dir.join("fsm_event_trail.json"),
992 "FSM audit event trail",
993 );
994 }
995 }
996
997 write_json_safe(
999 &result.audit.legal_documents,
1000 &audit_dir.join("legal_documents.json"),
1001 "Legal documents (v3.3.0)",
1002 );
1003
1004 write_json_safe(
1006 &result.audit.it_controls_access_logs,
1007 &audit_dir.join("it_controls_access_logs.json"),
1008 "IT general controls — access logs (v3.3.0)",
1009 );
1010 write_json_safe(
1011 &result.audit.it_controls_change_records,
1012 &audit_dir.join("it_controls_change_records.json"),
1013 "IT general controls — change management records (v3.3.0)",
1014 );
1015 } else {
1016 std::fs::create_dir_all(&audit_dir)?;
1022 write_json_always(
1023 &result.audit.audit_opinions,
1024 &audit_dir.join("audit_opinions.json"),
1025 "Audit opinions (ISA 700/705/706) — empty (audit phase disabled)",
1026 );
1027 write_json_always(
1028 &result.audit.key_audit_matters,
1029 &audit_dir.join("key_audit_matters.json"),
1030 "Key Audit Matters (ISA 701) — empty (audit phase disabled)",
1031 );
1032 }
1033
1034 let banking_dir = output_dir.join("banking");
1038 if !result.banking.customers.is_empty() {
1039 std::fs::create_dir_all(&banking_dir)?;
1040 info!("Writing banking data...");
1041
1042 match serde_json::to_value(&result.banking.customers) {
1048 Ok(mut v) => {
1049 if let Some(arr) = v.as_array_mut() {
1050 for c in arr.iter_mut() {
1051 if let Some(obj) = c.as_object_mut() {
1052 if let Some(rt) = obj.get("risk_tier").cloned() {
1053 obj.entry("risk_level").or_insert(rt);
1054 }
1055 }
1056 }
1057 }
1058 match serde_json::to_string_pretty(&v) {
1059 Ok(json) => {
1060 let path = banking_dir.join("banking_customers.json");
1061 if let Err(e) = std::fs::write(&path, json) {
1062 warn!("Failed to write banking_customers.json: {}", e);
1063 } else {
1064 info!(
1065 " Banking customers written: {} records -> {}",
1066 result.banking.customers.len(),
1067 path.display()
1068 );
1069 }
1070 }
1071 Err(e) => warn!("Failed to serialize banking customers: {}", e),
1072 }
1073 }
1074 Err(e) => warn!("Failed to build banking customers Value: {}", e),
1075 }
1076 write_json_safe(
1077 &result.banking.accounts,
1078 &banking_dir.join("banking_accounts.json"),
1079 "Banking accounts",
1080 );
1081 write_json_safe(
1082 &result.banking.transactions,
1083 &banking_dir.join("banking_transactions.json"),
1084 "Banking transactions",
1085 );
1086 write_json_safe(
1087 &result.banking.transaction_labels,
1088 &banking_dir.join("aml_transaction_labels.json"),
1089 "AML transaction labels",
1090 );
1091 write_json_safe(
1092 &result.banking.customer_labels,
1093 &banking_dir.join("aml_customer_labels.json"),
1094 "AML customer labels",
1095 );
1096 write_json_safe(
1097 &result.banking.account_labels,
1098 &banking_dir.join("aml_account_labels.json"),
1099 "AML account labels",
1100 );
1101 write_json_safe(
1102 &result.banking.relationship_labels,
1103 &banking_dir.join("aml_relationship_labels.json"),
1104 "AML relationship labels",
1105 );
1106 write_json_safe(
1107 &result.banking.narratives,
1108 &banking_dir.join("aml_narratives.json"),
1109 "AML narratives",
1110 );
1111 }
1112
1113 let s2c_dir = output_dir.join("sourcing");
1117 if !result.sourcing.spend_analyses.is_empty() || !result.sourcing.sourcing_projects.is_empty() {
1118 std::fs::create_dir_all(&s2c_dir)?;
1119 info!("Writing sourcing (S2C) data...");
1120
1121 write_json_safe(
1122 &result.sourcing.spend_analyses,
1123 &s2c_dir.join("spend_analyses.json"),
1124 "Spend analyses",
1125 );
1126 write_json_safe(
1127 &result.sourcing.sourcing_projects,
1128 &s2c_dir.join("sourcing_projects.json"),
1129 "Sourcing projects",
1130 );
1131 write_json_safe(
1132 &result.sourcing.qualifications,
1133 &s2c_dir.join("supplier_qualifications.json"),
1134 "Supplier qualifications",
1135 );
1136 write_json_safe(
1137 &result.sourcing.rfx_events,
1138 &s2c_dir.join("rfx_events.json"),
1139 "RFx events",
1140 );
1141 write_json_safe(
1142 &result.sourcing.bids,
1143 &s2c_dir.join("supplier_bids.json"),
1144 "Supplier bids",
1145 );
1146 write_json_safe(
1147 &result.sourcing.bid_evaluations,
1148 &s2c_dir.join("bid_evaluations.json"),
1149 "Bid evaluations",
1150 );
1151 write_json_safe(
1152 &result.sourcing.contracts,
1153 &s2c_dir.join("procurement_contracts.json"),
1154 "Procurement contracts",
1155 );
1156 write_json_safe(
1157 &result.sourcing.catalog_items,
1158 &s2c_dir.join("catalog_items.json"),
1159 "Catalog items",
1160 );
1161 write_json_safe(
1162 &result.sourcing.scorecards,
1163 &s2c_dir.join("supplier_scorecards.json"),
1164 "Supplier scorecards",
1165 );
1166 }
1167
1168 let ic_dir = output_dir.join("intercompany");
1172 if result.intercompany.group_structure.is_some()
1173 || !result.intercompany.matched_pairs.is_empty()
1174 {
1175 std::fs::create_dir_all(&ic_dir)?;
1176 info!("Writing intercompany data...");
1177
1178 if let Some(gs) = &result.intercompany.group_structure {
1180 write_json_single_safe(gs, &ic_dir.join("group_structure.json"), "Group structure");
1181 }
1182
1183 write_json_safe(
1184 &result.intercompany.matched_pairs,
1185 &ic_dir.join("ic_matched_pairs.json"),
1186 "IC matched pairs",
1187 );
1188 write_json_safe(
1189 &result.intercompany.seller_journal_entries,
1190 &ic_dir.join("ic_seller_journal_entries.json"),
1191 "IC seller journal entries",
1192 );
1193 write_json_safe(
1194 &result.intercompany.buyer_journal_entries,
1195 &ic_dir.join("ic_buyer_journal_entries.json"),
1196 "IC buyer journal entries",
1197 );
1198 write_json_safe(
1199 &result.intercompany.elimination_entries,
1200 &ic_dir.join("ic_elimination_entries.json"),
1201 "IC elimination entries",
1202 );
1203
1204 if !result.intercompany.nci_measurements.is_empty() {
1206 write_json_safe(
1207 &result.intercompany.nci_measurements,
1208 &ic_dir.join("nci_measurements.json"),
1209 "NCI measurements",
1210 );
1211 }
1212 }
1213
1214 let fin_dir = output_dir.join("financial_reporting");
1218 if !result.financial_reporting.financial_statements.is_empty()
1219 || !result.financial_reporting.bank_reconciliations.is_empty()
1220 || !result
1221 .financial_reporting
1222 .consolidated_statements
1223 .is_empty()
1224 {
1225 std::fs::create_dir_all(&fin_dir)?;
1226 info!("Writing financial reporting data...");
1227
1228 write_json_safe(
1230 &result.financial_reporting.financial_statements,
1231 &fin_dir.join("financial_statements.json"),
1232 "Financial statements",
1233 );
1234
1235 if !result.financial_reporting.standalone_statements.is_empty() {
1237 let standalone_dir = fin_dir.join("standalone");
1238 std::fs::create_dir_all(&standalone_dir)?;
1239 for (entity_code, stmts) in &result.financial_reporting.standalone_statements {
1240 let file_name = format!("{}_financial_statements.json", entity_code);
1241 write_json_safe(
1242 stmts,
1243 &standalone_dir.join(&file_name),
1244 &format!("Standalone statements for {}", entity_code),
1245 );
1246 }
1247 }
1248
1249 if !result
1251 .financial_reporting
1252 .consolidated_statements
1253 .is_empty()
1254 || !result
1255 .financial_reporting
1256 .consolidation_schedules
1257 .is_empty()
1258 {
1259 let consolidated_dir = fin_dir.join("consolidated");
1260 std::fs::create_dir_all(&consolidated_dir)?;
1261 write_json_safe(
1262 &result.financial_reporting.consolidated_statements,
1263 &consolidated_dir.join("consolidated_financial_statements.json"),
1264 "Consolidated financial statements",
1265 );
1266 write_json_safe(
1267 &result.financial_reporting.consolidation_schedules,
1268 &consolidated_dir.join("consolidation_schedule.json"),
1269 "Consolidation schedule",
1270 );
1271 }
1272
1273 write_json_safe(
1274 &result.financial_reporting.bank_reconciliations,
1275 &fin_dir.join("bank_reconciliations.json"),
1276 "Bank reconciliations",
1277 );
1278
1279 if !result.financial_reporting.segment_reports.is_empty()
1281 || !result
1282 .financial_reporting
1283 .segment_reconciliations
1284 .is_empty()
1285 {
1286 let seg_dir = fin_dir.join("segment_reporting");
1287 std::fs::create_dir_all(&seg_dir)?;
1288 write_json_safe(
1289 &result.financial_reporting.segment_reports,
1290 &seg_dir.join("segment_reports.json"),
1291 "Segment reports",
1292 );
1293 write_json_safe(
1294 &result.financial_reporting.segment_reconciliations,
1295 &seg_dir.join("segment_reconciliations.json"),
1296 "Segment reconciliations",
1297 );
1298 }
1299
1300 write_json_safe(
1302 &result.financial_reporting.notes_to_financial_statements,
1303 &fin_dir.join("notes_to_financial_statements.json"),
1304 "Notes to financial statements",
1305 );
1306 }
1307
1308 if !result.financial_reporting.trial_balances.is_empty() {
1319 let pc_dir = output_dir.join("period_close");
1320 std::fs::create_dir_all(&pc_dir)?;
1321 info!(
1322 "Writing {} period-close trial balances...",
1323 result.financial_reporting.trial_balances.len()
1324 );
1325 let (company_code, currency) = result
1332 .journal_entries
1333 .first()
1334 .map(|je| (je.header.company_code.as_str(), je.header.currency.as_str()))
1335 .unwrap_or(("UNKNOWN", "USD"));
1336 let canonical: Vec<datasynth_core::models::balance::TrialBalance> = result
1337 .financial_reporting
1338 .trial_balances
1339 .iter()
1340 .cloned()
1341 .map(|tb| tb.into_canonical(company_code, currency))
1342 .collect();
1343 write_json_safe(
1344 &canonical,
1345 &pc_dir.join("trial_balances.json"),
1346 "Period-close trial balances (canonical)",
1347 );
1348 }
1349
1350 if !result.opening_balances.is_empty() || !result.subledger_reconciliation.is_empty() {
1354 let balance_dir = output_dir.join("balance");
1355 std::fs::create_dir_all(&balance_dir)?;
1356 info!("Writing balance data...");
1357
1358 write_json_safe(
1359 &result.opening_balances,
1360 &balance_dir.join("opening_balances.json"),
1361 "Opening balances",
1362 );
1363 write_json_safe(
1364 &result.subledger_reconciliation,
1365 &balance_dir.join("subledger_reconciliation.json"),
1366 "Subledger reconciliation",
1367 );
1368 }
1369
1370 let hr_dir = output_dir.join("hr");
1374 if !result.hr.payroll_runs.is_empty()
1375 || !result.hr.time_entries.is_empty()
1376 || !result.hr.expense_reports.is_empty()
1377 || !result.hr.benefit_enrollments.is_empty()
1378 || !result.hr.pension_plans.is_empty()
1379 || !result.hr.stock_grants.is_empty()
1380 || !result.master_data.employee_change_history.is_empty()
1381 {
1382 std::fs::create_dir_all(&hr_dir)?;
1383 info!("Writing HR data...");
1384
1385 write_json_safe(
1386 &result.hr.payroll_runs,
1387 &hr_dir.join("payroll_runs.json"),
1388 "Payroll runs",
1389 );
1390 write_json_safe(
1391 &result.hr.payroll_line_items,
1392 &hr_dir.join("payroll_line_items.json"),
1393 "Payroll line items",
1394 );
1395 write_json_safe(
1396 &result.hr.time_entries,
1397 &hr_dir.join("time_entries.json"),
1398 "Time entries",
1399 );
1400 write_json_safe(
1401 &result.hr.expense_reports,
1402 &hr_dir.join("expense_reports.json"),
1403 "Expense reports",
1404 );
1405 write_json_safe(
1406 &result.hr.benefit_enrollments,
1407 &hr_dir.join("benefit_enrollments.json"),
1408 "Benefit enrollments",
1409 );
1410 write_json_safe(
1411 &result.hr.pension_plans,
1412 &hr_dir.join("pension_plans.json"),
1413 "Pension plans",
1414 );
1415 write_json_safe(
1416 &result.hr.pension_obligations,
1417 &hr_dir.join("pension_obligations.json"),
1418 "Pension obligations",
1419 );
1420 write_json_safe(
1421 &result.hr.pension_plan_assets,
1422 &hr_dir.join("plan_assets.json"),
1423 "Plan assets",
1424 );
1425 write_json_safe(
1426 &result.hr.pension_disclosures,
1427 &hr_dir.join("pension_disclosures.json"),
1428 "Pension disclosures",
1429 );
1430 write_json_safe(
1431 &result.hr.stock_grants,
1432 &hr_dir.join("stock_grants.json"),
1433 "Stock grants",
1434 );
1435 write_json_safe(
1436 &result.hr.stock_comp_expenses,
1437 &hr_dir.join("stock_comp_expense.json"),
1438 "Stock comp expense",
1439 );
1440 write_json_safe(
1441 &result.master_data.employee_change_history,
1442 &hr_dir.join("employee_change_history.json"),
1443 "Employee change history",
1444 );
1445 }
1446
1447 let mfg_dir = output_dir.join("manufacturing");
1451 if !result.manufacturing.production_orders.is_empty()
1452 || !result.manufacturing.quality_inspections.is_empty()
1453 || !result.manufacturing.cycle_counts.is_empty()
1454 || !result.manufacturing.bom_components.is_empty()
1455 || !result.manufacturing.inventory_movements.is_empty()
1456 {
1457 std::fs::create_dir_all(&mfg_dir)?;
1458 info!("Writing manufacturing data...");
1459
1460 write_json_safe(
1461 &result.manufacturing.production_orders,
1462 &mfg_dir.join("production_orders.json"),
1463 "Production orders",
1464 );
1465 write_json_safe(
1466 &result.manufacturing.quality_inspections,
1467 &mfg_dir.join("quality_inspections.json"),
1468 "Quality inspections",
1469 );
1470 write_json_safe(
1471 &result.manufacturing.cycle_counts,
1472 &mfg_dir.join("cycle_counts.json"),
1473 "Cycle counts",
1474 );
1475 write_json_safe(
1476 &result.manufacturing.bom_components,
1477 &mfg_dir.join("bom_components.json"),
1478 "BOM components",
1479 );
1480 write_json_safe(
1481 &result.manufacturing.inventory_movements,
1482 &mfg_dir.join("inventory_movements.json"),
1483 "Inventory movements",
1484 );
1485 }
1486
1487 let sales_dir = output_dir.join("sales_kpi_budgets");
1491 if !result.sales_kpi_budgets.sales_quotes.is_empty()
1492 || !result.sales_kpi_budgets.kpis.is_empty()
1493 || !result.sales_kpi_budgets.budgets.is_empty()
1494 {
1495 std::fs::create_dir_all(&sales_dir)?;
1496 info!("Writing sales, KPI, and budget data...");
1497
1498 write_json_safe(
1499 &result.sales_kpi_budgets.sales_quotes,
1500 &sales_dir.join("sales_quotes.json"),
1501 "Sales quotes",
1502 );
1503 write_json_safe(
1504 &result.sales_kpi_budgets.kpis,
1505 &sales_dir.join("management_kpis.json"),
1506 "Management KPIs",
1507 );
1508 write_json_safe(
1509 &result.sales_kpi_budgets.budgets,
1510 &sales_dir.join("budgets.json"),
1511 "Budgets",
1512 );
1513 }
1514
1515 let tax_dir = output_dir.join("tax");
1519 if !result.tax.jurisdictions.is_empty()
1520 || !result.tax.codes.is_empty()
1521 || !result.tax.tax_provisions.is_empty()
1522 {
1523 std::fs::create_dir_all(&tax_dir)?;
1524 info!("Writing tax data...");
1525
1526 write_json_safe(
1527 &result.tax.jurisdictions,
1528 &tax_dir.join("tax_jurisdictions.json"),
1529 "Tax jurisdictions",
1530 );
1531 write_json_safe(
1532 &result.tax.codes,
1533 &tax_dir.join("tax_codes.json"),
1534 "Tax codes",
1535 );
1536 write_json_safe(
1537 &result.tax.tax_provisions,
1538 &tax_dir.join("tax_provisions.json"),
1539 "Tax provisions",
1540 );
1541 write_json_safe(
1542 &result.tax.tax_lines,
1543 &tax_dir.join("tax_lines.json"),
1544 "Tax lines",
1545 );
1546 write_json_safe(
1547 &result.tax.tax_returns,
1548 &tax_dir.join("tax_returns.json"),
1549 "Tax returns",
1550 );
1551 write_json_safe(
1552 &result.tax.withholding_records,
1553 &tax_dir.join("withholding_records.json"),
1554 "Withholding tax records",
1555 );
1556 if !result.tax.tax_anomaly_labels.is_empty() {
1557 write_json_safe(
1558 &result.tax.tax_anomaly_labels,
1559 &tax_dir.join("tax_anomaly_labels.json"),
1560 "Tax anomaly labels",
1561 );
1562 }
1563 if !result.tax.deferred_tax.temporary_differences.is_empty() {
1565 write_json_safe(
1566 &result.tax.deferred_tax.temporary_differences,
1567 &tax_dir.join("temporary_differences.json"),
1568 "Temporary differences",
1569 );
1570 write_json_safe(
1571 &result.tax.deferred_tax.etr_reconciliations,
1572 &tax_dir.join("etr_reconciliation.json"),
1573 "ETR reconciliation",
1574 );
1575 write_json_safe(
1576 &result.tax.deferred_tax.rollforwards,
1577 &tax_dir.join("deferred_tax_rollforward.json"),
1578 "Deferred tax rollforward",
1579 );
1580 write_json_safe(
1581 &result.tax.deferred_tax.journal_entries,
1582 &tax_dir.join("deferred_tax_journal_entries.json"),
1583 "Deferred tax journal entries",
1584 );
1585 }
1586 }
1587
1588 let esg_dir = output_dir.join("esg");
1592 if !result.esg.emissions.is_empty()
1593 || !result.esg.energy.is_empty()
1594 || !result.esg.diversity.is_empty()
1595 || !result.esg.governance.is_empty()
1596 {
1597 std::fs::create_dir_all(&esg_dir)?;
1598 info!("Writing ESG data...");
1599
1600 write_json_safe(
1601 &result.esg.emissions,
1602 &esg_dir.join("emission_records.json"),
1603 "Emission records",
1604 );
1605 write_json_safe(
1606 &result.esg.energy,
1607 &esg_dir.join("energy_consumption.json"),
1608 "Energy consumption",
1609 );
1610 write_json_safe(
1611 &result.esg.water,
1612 &esg_dir.join("water_usage.json"),
1613 "Water usage",
1614 );
1615 write_json_safe(
1616 &result.esg.waste,
1617 &esg_dir.join("waste_records.json"),
1618 "Waste records",
1619 );
1620 write_json_safe(
1621 &result.esg.diversity,
1622 &esg_dir.join("workforce_diversity.json"),
1623 "Workforce diversity",
1624 );
1625 write_json_safe(
1626 &result.esg.pay_equity,
1627 &esg_dir.join("pay_equity.json"),
1628 "Pay equity",
1629 );
1630 write_json_safe(
1631 &result.esg.safety_incidents,
1632 &esg_dir.join("safety_incidents.json"),
1633 "Safety incidents",
1634 );
1635 write_json_safe(
1636 &result.esg.safety_metrics,
1637 &esg_dir.join("safety_metrics.json"),
1638 "Safety metrics",
1639 );
1640 write_json_safe(
1641 &result.esg.governance,
1642 &esg_dir.join("governance_metrics.json"),
1643 "Governance metrics",
1644 );
1645 write_json_safe(
1646 &result.esg.supplier_assessments,
1647 &esg_dir.join("supplier_esg_assessments.json"),
1648 "Supplier ESG assessments",
1649 );
1650 write_json_safe(
1651 &result.esg.materiality,
1652 &esg_dir.join("materiality_assessments.json"),
1653 "Materiality assessments",
1654 );
1655 write_json_safe(
1656 &result.esg.disclosures,
1657 &esg_dir.join("esg_disclosures.json"),
1658 "ESG disclosures",
1659 );
1660 write_json_safe(
1661 &result.esg.climate_scenarios,
1662 &esg_dir.join("climate_scenarios.json"),
1663 "Climate scenarios",
1664 );
1665 write_json_safe(
1666 &result.esg.anomaly_labels,
1667 &esg_dir.join("esg_anomaly_labels.json"),
1668 "ESG anomaly labels",
1669 );
1670 }
1671
1672 if let Some(ref event_log) = result.ocpm.event_log {
1676 if !event_log.events.is_empty() || !event_log.objects.is_empty() {
1677 let pm_dir = output_dir.join("process_mining");
1678 std::fs::create_dir_all(&pm_dir)?;
1679 info!("Writing process mining (OCPM) data...");
1680
1681 match serde_json::to_value(event_log) {
1687 Ok(mut v) => {
1688 add_ocel_object_type_alias(&mut v);
1689 match serde_json::to_string_pretty(&v) {
1690 Ok(json) => {
1691 if let Err(e) = std::fs::write(pm_dir.join("event_log.json"), json) {
1692 warn!("Failed to write OCPM event log: {}", e);
1693 } else {
1694 info!(
1695 " Event log written: {} events, {} objects",
1696 result.ocpm.event_count, result.ocpm.object_count
1697 );
1698 }
1699 }
1700 Err(e) => warn!("Failed to serialize OCPM event log: {}", e),
1701 }
1702 }
1703 Err(e) => warn!("Failed to build OCPM event log Value: {}", e),
1704 }
1705
1706 if !event_log.events.is_empty() {
1708 match serde_json::to_string_pretty(&event_log.events) {
1709 Ok(json) => {
1710 if let Err(e) = std::fs::write(pm_dir.join("events.json"), json) {
1711 warn!("Failed to write OCPM events: {}", e);
1712 } else {
1713 info!(" Events written: {} records", event_log.events.len());
1714 }
1715 }
1716 Err(e) => warn!("Failed to serialize OCPM events: {}", e),
1717 }
1718 }
1719
1720 if !event_log.objects.is_empty() {
1722 let objects: Vec<&_> = event_log.objects.iter().collect();
1723 match serde_json::to_string_pretty(&objects) {
1724 Ok(json) => {
1725 if let Err(e) = std::fs::write(pm_dir.join("objects.json"), json) {
1726 warn!("Failed to write OCPM objects: {}", e);
1727 } else {
1728 info!(" Objects written: {} records", event_log.objects.len());
1729 }
1730 }
1731 Err(e) => warn!("Failed to serialize OCPM objects: {}", e),
1732 }
1733 }
1734
1735 if !event_log.variants.is_empty() {
1737 let variants: Vec<&_> = event_log.variants.values().collect();
1738 match serde_json::to_string_pretty(&variants) {
1739 Ok(json) => {
1740 if let Err(e) = std::fs::write(pm_dir.join("process_variants.json"), json) {
1741 warn!("Failed to write process variants: {}", e);
1742 } else {
1743 info!(
1744 " Process variants written: {} variants",
1745 event_log.variants.len()
1746 );
1747 }
1748 }
1749 Err(e) => warn!("Failed to serialize process variants: {}", e),
1750 }
1751 }
1752 }
1753 }
1754
1755 match serde_json::to_string_pretty(&result.chart_of_accounts.accounts) {
1761 Ok(json) => {
1762 if let Err(e) = std::fs::write(output_dir.join("chart_of_accounts.json"), json) {
1763 warn!("Failed to write chart of accounts: {}", e);
1764 } else {
1765 info!(" Chart of accounts written");
1766 }
1767 }
1768 Err(e) => warn!("Failed to serialize chart of accounts: {}", e),
1769 }
1770 let coa_meta = serde_json::json!({
1776 "coa_id": result.chart_of_accounts.coa_id,
1777 "name": result.chart_of_accounts.name,
1778 "country": result.chart_of_accounts.country,
1779 "industry": result.chart_of_accounts.industry,
1780 "complexity": result.chart_of_accounts.complexity,
1781 "account_format": result.chart_of_accounts.account_format,
1782 "accounting_framework": result.chart_of_accounts.accounting_framework,
1783 "account_count": result.chart_of_accounts.accounts.len(),
1784 });
1785 match serde_json::to_string_pretty(&coa_meta) {
1786 Ok(json) => {
1787 if let Err(e) = std::fs::write(output_dir.join("chart_of_accounts_meta.json"), json) {
1788 warn!("Failed to write CoA metadata: {}", e);
1789 } else {
1790 info!(
1791 " Chart of accounts metadata written (accounting_framework: {:?})",
1792 result.chart_of_accounts.accounting_framework
1793 );
1794 }
1795 }
1796 Err(e) => warn!("Failed to serialize CoA metadata: {}", e),
1797 }
1798
1799 if result.balance_validation.validated {
1803 match serde_json::to_string_pretty(&BalanceValidationSummary::from(
1804 &result.balance_validation,
1805 )) {
1806 Ok(json) => {
1807 if let Err(e) = std::fs::write(output_dir.join("balance_validation.json"), json) {
1808 warn!("Failed to write balance validation: {}", e);
1809 } else {
1810 info!(" Balance validation summary written");
1811 }
1812 }
1813 Err(e) => warn!("Failed to serialize balance validation: {}", e),
1814 }
1815 }
1816
1817 {
1821 match serde_json::to_string_pretty(&result.data_quality_stats) {
1822 Ok(json) => {
1823 if let Err(e) = std::fs::write(output_dir.join("data_quality_stats.json"), json) {
1824 warn!("Failed to write data quality stats: {}", e);
1825 } else {
1826 info!(" Data quality stats written (full detail)");
1827 }
1828 }
1829 Err(e) => warn!("Failed to serialize data quality stats: {}", e),
1830 }
1831 }
1832
1833 {
1838 let am = &result.analytics_metadata;
1839 if !am.prior_year_comparatives.is_empty()
1840 || !am.industry_benchmarks.is_empty()
1841 || !am.management_reports.is_empty()
1842 || !am.drift_events.is_empty()
1843 {
1844 let analytics_dir = output_dir.join("analytics");
1845 std::fs::create_dir_all(&analytics_dir)?;
1846 write_json_safe(
1847 &am.prior_year_comparatives,
1848 &analytics_dir.join("prior_year_comparatives.json"),
1849 "Prior-year comparatives (v3.3.0)",
1850 );
1851 write_json_safe(
1852 &am.industry_benchmarks,
1853 &analytics_dir.join("industry_benchmarks.json"),
1854 "Industry benchmarks (v3.3.0)",
1855 );
1856 write_json_safe(
1857 &am.management_reports,
1858 &analytics_dir.join("management_reports.json"),
1859 "Management reports (v3.3.0)",
1860 );
1861 write_json_safe(
1862 &am.drift_events,
1863 &analytics_dir.join("drift_events.json"),
1864 "Drift event labels (v3.3.0)",
1865 );
1866 }
1867 }
1868
1869 {
1873 let analytics_dir = output_dir.join("analytics");
1874
1875 let amounts: Vec<_> = result
1877 .journal_entries
1878 .iter()
1879 .flat_map(|je| je.lines.iter())
1880 .flat_map(|line| {
1881 let d = (!line.debit_amount.is_zero()).then_some(line.debit_amount);
1882 let c = (!line.credit_amount.is_zero()).then_some(line.credit_amount);
1883 d.into_iter().chain(c)
1884 })
1885 .collect();
1886
1887 if amounts.len() >= 10 {
1888 std::fs::create_dir_all(&analytics_dir)?;
1889 info!("Writing pre-built analytics ({} amounts)...", amounts.len());
1890
1891 let benford_analyzer = datasynth_eval::BenfordAnalyzer::default();
1893 match benford_analyzer.analyze(&amounts) {
1894 Ok(ref benford_result) => {
1895 if let Ok(json) = serde_json::to_string_pretty(benford_result) {
1896 if let Err(e) =
1897 std::fs::write(analytics_dir.join("benford_analysis.json"), json)
1898 {
1899 warn!("Failed to write Benford analysis: {}", e);
1900 } else {
1901 info!(
1902 " Benford analysis written (conformity: {:?}, MAD: {:.4})",
1903 benford_result.conformity, benford_result.mad
1904 );
1905 }
1906 }
1907 }
1908 Err(e) => warn!("Benford analysis skipped: {}", e),
1909 }
1910
1911 let amount_analyzer = datasynth_eval::AmountDistributionAnalyzer::new();
1913 match amount_analyzer.analyze(&amounts) {
1914 Ok(ref dist_result) => {
1915 if let Ok(json) = serde_json::to_string_pretty(dist_result) {
1916 if let Err(e) =
1917 std::fs::write(analytics_dir.join("amount_distribution.json"), json)
1918 {
1919 warn!("Failed to write amount distribution: {}", e);
1920 } else {
1921 info!(
1922 " Amount distribution written (skewness: {:.2}, kurtosis: {:.2})",
1923 dist_result.skewness, dist_result.kurtosis
1924 );
1925 }
1926 }
1927 }
1928 Err(e) => warn!("Amount distribution analysis skipped: {}", e),
1929 }
1930 }
1931
1932 if let Some(ref event_log) = result.ocpm.event_log {
1942 std::fs::create_dir_all(&analytics_dir)?;
1943 let variant_data: Vec<datasynth_eval::VariantData> = if !event_log.variants.is_empty() {
1944 event_log
1945 .variants
1946 .values()
1947 .map(|v| datasynth_eval::VariantData {
1948 variant_id: v.variant_id.clone(),
1949 case_count: v.frequency as usize,
1950 is_happy_path: v.is_happy_path,
1951 })
1952 .collect()
1953 } else {
1954 use std::collections::HashMap;
1960 let mut per_case: HashMap<String, Vec<String>> = HashMap::new();
1963 for ev in &event_log.events {
1964 if let Some(case_id) = ev.case_id {
1965 per_case
1966 .entry(case_id.to_string())
1967 .or_default()
1968 .push(ev.activity_id.clone());
1969 }
1970 }
1971 let mut variant_counts: HashMap<Vec<String>, usize> = HashMap::new();
1972 for activities in per_case.into_values() {
1973 *variant_counts.entry(activities).or_insert(0) += 1;
1974 }
1975 let max_count = variant_counts.values().copied().max().unwrap_or(0);
1977 variant_counts
1978 .into_iter()
1979 .enumerate()
1980 .map(|(i, (seq, count))| datasynth_eval::VariantData {
1981 variant_id: format!("V{i:04}:{}", seq.join("->")),
1982 case_count: count,
1983 is_happy_path: count == max_count && max_count > 0,
1984 })
1985 .collect()
1986 };
1987
1988 let variant_analyzer = datasynth_eval::VariantAnalyzer::new();
1989 match variant_analyzer.analyze(&variant_data) {
1990 Ok(ref variant_result) => {
1991 if let Ok(json) = serde_json::to_string_pretty(variant_result) {
1992 if let Err(e) =
1993 std::fs::write(analytics_dir.join("process_variant_summary.json"), json)
1994 {
1995 warn!("Failed to write variant summary: {}", e);
1996 } else {
1997 info!(
1998 " Process variant summary written ({} variants, entropy: {:.2})",
1999 variant_result.variant_count, variant_result.variant_entropy
2000 );
2001 }
2002 }
2003 }
2004 Err(e) => {
2005 warn!("Variant analysis failed: {}; emitting empty summary", e);
2008 let placeholder = serde_json::json!({
2009 "variant_count": 0,
2010 "variant_entropy": null,
2011 "happy_path_concentration": null,
2012 "top_variants": [],
2013 "passes": false,
2014 "issues": [format!("analyzer error: {e}")],
2015 });
2016 if let Ok(json) = serde_json::to_string_pretty(&placeholder) {
2017 let _ = std::fs::write(
2018 analytics_dir.join("process_variant_summary.json"),
2019 json,
2020 );
2021 }
2022 }
2023 }
2024 }
2025
2026 if !result.banking.customers.is_empty() {
2030 use datasynth_core::models::banking::BankingCustomerType;
2031 use datasynth_eval::banking::{
2032 AmlDetectabilityAnalyzer, AmlTransactionData, BankingEvaluation,
2033 KycCompletenessAnalyzer, KycProfileData, TypologyData,
2034 };
2035 use std::collections::HashMap;
2036 std::fs::create_dir_all(&analytics_dir)?;
2037
2038 let kyc_data: Vec<KycProfileData> = result
2039 .banking
2040 .customers
2041 .iter()
2042 .map(|c| KycProfileData {
2043 profile_id: c.customer_id.to_string(),
2044 has_name: true,
2045 has_dob: c.date_of_birth.is_some(),
2046 has_address: c.address_line1.is_some(),
2047 has_id_document: c.national_id.is_some() || c.passport_number.is_some(),
2048 has_risk_rating: true,
2049 has_beneficial_owner: !c.beneficial_owners.is_empty(),
2050 is_entity: c.customer_type == BankingCustomerType::Business,
2051 is_verified: c.kyc_truthful,
2052 })
2053 .collect();
2054
2055 let mut banking_eval = BankingEvaluation::new();
2056 if let Ok(kyc_res) = KycCompletenessAnalyzer::new().analyze(&kyc_data) {
2057 banking_eval.kyc = Some(kyc_res);
2058 }
2059
2060 let suspicious: Vec<&_> = result
2061 .banking
2062 .transactions
2063 .iter()
2064 .filter(|t| t.is_suspicious)
2065 .collect();
2066 if !suspicious.is_empty() {
2067 let aml_data: Vec<AmlTransactionData> = suspicious
2074 .iter()
2075 .map(|t| AmlTransactionData {
2076 transaction_id: t.transaction_id.to_string(),
2077 typology: t
2078 .suspicion_reason
2079 .as_ref()
2080 .map(|r| r.canonical_name().to_string())
2081 .unwrap_or_default(),
2082 case_id: t.case_id.clone().unwrap_or_default(),
2083 amount: t.amount.try_into().unwrap_or(0.0),
2084 is_flagged: t.is_suspicious,
2085 })
2086 .collect();
2087
2088 let mut typology_map: HashMap<String, (usize, HashMap<String, bool>)> =
2089 HashMap::new();
2090 for txn in &aml_data {
2091 if !txn.typology.is_empty() {
2092 let entry = typology_map
2093 .entry(txn.typology.clone())
2094 .or_insert_with(|| (0, HashMap::new()));
2095 entry.0 += 1;
2096 entry.1.insert(txn.case_id.clone(), true);
2097 }
2098 }
2099 let typology_data: Vec<TypologyData> = typology_map
2100 .iter()
2101 .map(|(name, (count, cases))| TypologyData {
2102 name: name.clone(),
2103 scenario_count: *count,
2104 case_ids_consistent: cases.len() <= *count,
2105 })
2106 .collect();
2107
2108 if let Ok(aml_res) =
2109 AmlDetectabilityAnalyzer::new().analyze(&aml_data, &typology_data)
2110 {
2111 banking_eval.aml = Some(aml_res);
2112 }
2113 }
2114 banking_eval.check_thresholds();
2115
2116 match serde_json::to_string_pretty(&banking_eval) {
2117 Ok(json) => {
2118 if let Err(e) =
2119 std::fs::write(analytics_dir.join("banking_evaluation.json"), json)
2120 {
2121 warn!("Failed to write banking evaluation: {}", e);
2122 } else {
2123 info!(
2124 " Banking evaluation written ({} profiles, {} issues, passes={})",
2125 result.banking.customers.len(),
2126 banking_eval.issues.len(),
2127 banking_eval.passes
2128 );
2129 }
2130 }
2131 Err(e) => warn!("Failed to serialize banking evaluation: {}", e),
2132 }
2133 }
2134 }
2135
2136 if !result.quality_issues.is_empty() {
2140 let labels_dir = output_dir.join("labels");
2141 std::fs::create_dir_all(&labels_dir)?;
2142 info!("Writing data quality issue records...");
2143 write_json_safe(
2144 &result.quality_issues,
2145 &labels_dir.join("quality_issues.json"),
2146 "Data quality issues",
2147 );
2148
2149 use datasynth_generators::{
2152 LabeledIssueType, QualityIssueLabel, QualityIssueType, QualityLabels,
2153 };
2154 let mut quality_labels = QualityLabels::with_capacity(result.quality_issues.len());
2155 for issue in &result.quality_issues {
2156 let labeled_type = match issue.issue_type {
2157 QualityIssueType::MissingValue => LabeledIssueType::MissingValue,
2158 QualityIssueType::Typo => LabeledIssueType::Typo,
2159 QualityIssueType::DateFormatVariation
2160 | QualityIssueType::AmountFormatVariation
2161 | QualityIssueType::IdentifierFormatVariation
2162 | QualityIssueType::TextFormatVariation => LabeledIssueType::FormatVariation,
2163 QualityIssueType::ExactDuplicate
2164 | QualityIssueType::NearDuplicate
2165 | QualityIssueType::FuzzyDuplicate => LabeledIssueType::Duplicate,
2166 QualityIssueType::EncodingIssue => LabeledIssueType::EncodingIssue,
2167 };
2168 let mut label = QualityIssueLabel::new(
2169 labeled_type,
2170 issue.record_id.clone(),
2171 issue.field.clone().unwrap_or_else(|| "_record".to_string()),
2172 "data_quality_injector",
2173 );
2174 if let Some(ref orig) = issue.original_value {
2175 label = label.with_original(orig.clone());
2176 }
2177 if let Some(ref modified) = issue.modified_value {
2178 label = label.with_modified(modified.clone());
2179 }
2180 quality_labels.add(label);
2181 }
2182 if let Ok(json) = serde_json::to_string_pretty(&quality_labels) {
2183 if let Err(e) = std::fs::write(labels_dir.join("quality_labels.json"), json.as_bytes())
2184 {
2185 warn!("Failed to write quality labels: {}", e);
2186 } else {
2187 info!(
2188 " Quality labels written: {} labels -> labels/quality_labels.json",
2189 quality_labels.len()
2190 );
2191 }
2192 }
2193 }
2194
2195 if !result.internal_controls.is_empty() || !result.sod_violations.is_empty() {
2199 let ctrl_dir = output_dir.join("internal_controls");
2200 std::fs::create_dir_all(&ctrl_dir)?;
2201 info!("Writing internal controls data...");
2202
2203 write_json_safe(
2204 &result.internal_controls,
2205 &ctrl_dir.join("internal_controls.json"),
2206 "Internal controls",
2207 );
2208 write_json_safe(
2210 &result.sod_violations,
2211 &ctrl_dir.join("sod_violations.json"),
2212 "SoD violations",
2213 );
2214
2215 let exporter = datasynth_output::ControlExporter::new(&ctrl_dir);
2219 match exporter.export_standard() {
2220 Ok(summary) => {
2221 info!(
2222 " Control master data written: {} controls, {} SoD conflicts, {} SoD rules, {} COSO mappings, {} account mappings",
2223 summary.controls_count,
2224 summary.sod_conflicts_count,
2225 summary.sod_rules_count,
2226 summary.coso_mappings_count,
2227 summary.account_mappings_count,
2228 );
2229 }
2230 Err(e) => warn!("Failed to write control master data: {}", e),
2231 }
2232 }
2233
2234 if !result.accounting_standards.contracts.is_empty()
2238 || !result.accounting_standards.impairment_tests.is_empty()
2239 || !result.accounting_standards.business_combinations.is_empty()
2240 || !result.accounting_standards.ecl_models.is_empty()
2241 || !result.accounting_standards.provisions.is_empty()
2242 || !result
2243 .accounting_standards
2244 .currency_translation_results
2245 .is_empty()
2246 {
2247 let acct_dir = output_dir.join("accounting_standards");
2248 std::fs::create_dir_all(&acct_dir)?;
2249 info!("Writing accounting standards data...");
2250
2251 write_json_safe(
2252 &result.accounting_standards.contracts,
2253 &acct_dir.join("customer_contracts.json"),
2254 "Customer contracts",
2255 );
2256 write_json_safe(
2257 &result.accounting_standards.impairment_tests,
2258 &acct_dir.join("impairment_tests.json"),
2259 "Impairment tests",
2260 );
2261 write_json_safe(
2262 &result.accounting_standards.business_combinations,
2263 &acct_dir.join("business_combinations.json"),
2264 "Business combinations",
2265 );
2266 write_json_safe(
2267 &result
2268 .accounting_standards
2269 .business_combination_journal_entries,
2270 &acct_dir.join("business_combination_journal_entries.json"),
2271 "Business combination journal entries",
2272 );
2273 write_json_safe(
2274 &result.accounting_standards.ecl_models,
2275 &acct_dir.join("ecl_models.json"),
2276 "ECL models",
2277 );
2278 write_json_safe(
2279 &result.accounting_standards.ecl_provision_movements,
2280 &acct_dir.join("ecl_provision_movements.json"),
2281 "ECL provision movements",
2282 );
2283 write_json_safe(
2284 &result.accounting_standards.ecl_journal_entries,
2285 &acct_dir.join("ecl_journal_entries.json"),
2286 "ECL journal entries",
2287 );
2288 write_json_safe(
2289 &result.accounting_standards.provisions,
2290 &acct_dir.join("provisions.json"),
2291 "Provisions (IAS 37 / ASC 450)",
2292 );
2293 write_json_safe(
2294 &result.accounting_standards.provision_movements,
2295 &acct_dir.join("provision_movements.json"),
2296 "Provision movements",
2297 );
2298 write_json_safe(
2299 &result.accounting_standards.contingent_liabilities,
2300 &acct_dir.join("contingent_liabilities.json"),
2301 "Contingent liabilities",
2302 );
2303 write_json_safe(
2304 &result.accounting_standards.provision_journal_entries,
2305 &acct_dir.join("provision_journal_entries.json"),
2306 "Provision journal entries",
2307 );
2308
2309 if !result
2311 .accounting_standards
2312 .currency_translation_results
2313 .is_empty()
2314 {
2315 let fx_dir = acct_dir.join("fx");
2316 std::fs::create_dir_all(&fx_dir)?;
2317 write_json_safe(
2318 &result.accounting_standards.currency_translation_results,
2319 &fx_dir.join("currency_translation_results.json"),
2320 "IAS 21 currency translation results",
2321 );
2322 }
2323
2324 if !result.accounting_standards.leases.is_empty() {
2326 let leases_dir = acct_dir.join("leases");
2327 std::fs::create_dir_all(&leases_dir)?;
2328 write_json_safe(
2329 &result.accounting_standards.leases,
2330 &leases_dir.join("leases.json"),
2331 "Leases (IFRS 16 / ASC 842) — v3.3.1",
2332 );
2333 }
2334
2335 if !result
2337 .accounting_standards
2338 .fair_value_measurements
2339 .is_empty()
2340 {
2341 let fv_dir = acct_dir.join("fair_value");
2342 std::fs::create_dir_all(&fv_dir)?;
2343 write_json_safe(
2344 &result.accounting_standards.fair_value_measurements,
2345 &fv_dir.join("fair_value_measurements.json"),
2346 "Fair value measurements (IFRS 13 / ASC 820) — v3.3.1",
2347 );
2348 }
2349
2350 if !result.accounting_standards.framework_differences.is_empty() {
2352 let diff_dir = acct_dir.join("framework_differences");
2353 std::fs::create_dir_all(&diff_dir)?;
2354 write_json_safe(
2355 &result.accounting_standards.framework_differences,
2356 &diff_dir.join("framework_differences.json"),
2357 "Framework differences (US GAAP vs IFRS) — v3.3.1",
2358 );
2359 write_json_safe(
2360 &result.accounting_standards.framework_reconciliations,
2361 &diff_dir.join("framework_reconciliations.json"),
2362 "Per-entity framework reconciliation — v3.3.1",
2363 );
2364 }
2365 }
2366
2367 if let Some(ref gate_result) = result.gate_result {
2371 match serde_json::to_string_pretty(gate_result) {
2372 Ok(json) => {
2373 if let Err(e) = std::fs::write(output_dir.join("quality_gate_result.json"), json) {
2374 warn!("Failed to write quality gate result: {}", e);
2375 } else {
2376 info!(
2377 " Quality gate result written (passed={})",
2378 gate_result.passed
2379 );
2380 }
2381 }
2382 Err(e) => warn!("Failed to serialize quality gate result: {}", e),
2383 }
2384 }
2385
2386 if !result.treasury.debt_instruments.is_empty()
2390 || !result.treasury.cash_positions.is_empty()
2391 || !result.treasury.hedging_instruments.is_empty()
2392 {
2393 let treasury_dir = output_dir.join("treasury");
2394 std::fs::create_dir_all(&treasury_dir)?;
2395 info!("Writing treasury data...");
2396
2397 write_json_safe(
2398 &result.treasury.debt_instruments,
2399 &treasury_dir.join("debt_instruments.json"),
2400 "Debt instruments",
2401 );
2402 write_json_safe(
2403 &result.treasury.hedging_instruments,
2404 &treasury_dir.join("hedging_instruments.json"),
2405 "Hedging instruments",
2406 );
2407 write_json_safe(
2408 &result.treasury.hedge_relationships,
2409 &treasury_dir.join("hedge_relationships.json"),
2410 "Hedge relationships",
2411 );
2412 write_json_safe(
2413 &result.treasury.cash_positions,
2414 &treasury_dir.join("cash_positions.json"),
2415 "Cash positions",
2416 );
2417 write_json_safe(
2418 &result.treasury.cash_forecasts,
2419 &treasury_dir.join("cash_forecasts.json"),
2420 "Cash forecasts",
2421 );
2422 write_json_safe(
2423 &result.treasury.cash_pools,
2424 &treasury_dir.join("cash_pools.json"),
2425 "Cash pools",
2426 );
2427 write_json_safe(
2428 &result.treasury.cash_pool_sweeps,
2429 &treasury_dir.join("cash_pool_sweeps.json"),
2430 "Cash pool sweeps",
2431 );
2432 write_json_safe(
2433 &result.treasury.bank_guarantees,
2434 &treasury_dir.join("bank_guarantees.json"),
2435 "Bank guarantees",
2436 );
2437 write_json_safe(
2438 &result.treasury.netting_runs,
2439 &treasury_dir.join("netting_runs.json"),
2440 "Netting runs",
2441 );
2442 if !result.treasury.treasury_anomaly_labels.is_empty() {
2443 write_json_safe(
2444 &result.treasury.treasury_anomaly_labels,
2445 &treasury_dir.join("treasury_anomaly_labels.json"),
2446 "Treasury anomaly labels",
2447 );
2448 }
2449 }
2450
2451 if !result.project_accounting.projects.is_empty() {
2455 let pa_dir = output_dir.join("project_accounting");
2456 std::fs::create_dir_all(&pa_dir)?;
2457 info!("Writing project accounting data...");
2458
2459 write_json_safe(
2460 &result.project_accounting.projects,
2461 &pa_dir.join("projects.json"),
2462 "Projects",
2463 );
2464 write_json_safe(
2465 &result.project_accounting.cost_lines,
2466 &pa_dir.join("cost_lines.json"),
2467 "Project cost lines",
2468 );
2469 write_json_safe(
2470 &result.project_accounting.revenue_records,
2471 &pa_dir.join("revenue_records.json"),
2472 "Project revenue records",
2473 );
2474 write_json_safe(
2475 &result.project_accounting.earned_value_metrics,
2476 &pa_dir.join("earned_value_metrics.json"),
2477 "Earned value metrics",
2478 );
2479 write_json_safe(
2480 &result.project_accounting.change_orders,
2481 &pa_dir.join("change_orders.json"),
2482 "Change orders",
2483 );
2484 write_json_safe(
2485 &result.project_accounting.milestones,
2486 &pa_dir.join("milestones.json"),
2487 "Project milestones",
2488 );
2489 }
2490
2491 if !result.process_evolution.is_empty()
2495 || !result.organizational_events.is_empty()
2496 || !result.disruption_events.is_empty()
2497 {
2498 let events_dir = output_dir.join("events");
2499 std::fs::create_dir_all(&events_dir)?;
2500 info!("Writing evolution events...");
2501
2502 write_json_safe(
2503 &result.process_evolution,
2504 &events_dir.join("process_evolution_events.json"),
2505 "Process evolution events",
2506 );
2507 write_json_safe(
2508 &result.organizational_events,
2509 &events_dir.join("organizational_events.json"),
2510 "Organizational events",
2511 );
2512 write_json_safe(
2513 &result.disruption_events,
2514 &events_dir.join("disruption_events.json"),
2515 "Disruption events",
2516 );
2517 }
2518
2519 if !result.counterfactual_pairs.is_empty() {
2523 let ml_dir = output_dir.join("ml_training");
2524 std::fs::create_dir_all(&ml_dir)?;
2525 info!("Writing ML training data...");
2526
2527 write_json_safe(
2528 &result.counterfactual_pairs,
2529 &ml_dir.join("counterfactual_pairs.json"),
2530 "Counterfactual pairs",
2531 );
2532 }
2533
2534 if !result.red_flags.is_empty() {
2538 let labels_dir = output_dir.join("labels");
2539 std::fs::create_dir_all(&labels_dir)?;
2540 info!("Writing fraud red-flag indicators...");
2541
2542 write_json_safe(
2543 &result.red_flags,
2544 &labels_dir.join("fraud_red_flags.json"),
2545 "Fraud red flags",
2546 );
2547 }
2548
2549 if !result.collusion_rings.is_empty() {
2553 let labels_dir = output_dir.join("labels");
2554 std::fs::create_dir_all(&labels_dir)?;
2555 info!("Writing collusion rings...");
2556
2557 write_json_safe(
2558 &result.collusion_rings,
2559 &labels_dir.join("collusion_rings.json"),
2560 "Collusion rings",
2561 );
2562 }
2563
2564 if !result.temporal_vendor_chains.is_empty() {
2568 let temporal_dir = output_dir.join("temporal");
2569 std::fs::create_dir_all(&temporal_dir)?;
2570 info!("Writing temporal vendor version chains...");
2571
2572 write_json_safe(
2573 &result.temporal_vendor_chains,
2574 &temporal_dir.join("vendor_version_chains.json"),
2575 "Vendor version chains",
2576 );
2577 }
2578
2579 if result.entity_relationship_graph.is_some() || !result.cross_process_links.is_empty() {
2583 let rel_dir = output_dir.join("relationships");
2584 std::fs::create_dir_all(&rel_dir)?;
2585 info!("Writing entity relationship data...");
2586
2587 if let Some(ref graph) = result.entity_relationship_graph {
2588 match serde_json::to_string_pretty(graph) {
2589 Ok(json) => {
2590 let path = rel_dir.join("entity_relationship_graph.json");
2591 if let Err(e) = std::fs::write(&path, json) {
2592 warn!("Failed to write entity relationship graph: {}", e);
2593 } else {
2594 info!(
2595 " Entity relationship graph written: {} nodes, {} edges -> {}",
2596 graph.nodes.len(),
2597 graph.edges.len(),
2598 path.display()
2599 );
2600 }
2601 }
2602 Err(e) => warn!("Failed to serialize entity relationship graph: {}", e),
2603 }
2604 }
2605
2606 write_json_safe(
2607 &result.cross_process_links,
2608 &rel_dir.join("cross_process_links.json"),
2609 "Cross-process links",
2610 );
2611 }
2612
2613 if let Some(ref industry_output) = result.industry_output {
2617 if !industry_output.gl_accounts.is_empty() {
2618 let industry_dir = output_dir.join("industry");
2619 std::fs::create_dir_all(&industry_dir).ok();
2620 info!("Writing industry-specific data...");
2621 match serde_json::to_string_pretty(industry_output) {
2622 Ok(json) => {
2623 if let Err(e) = std::fs::write(industry_dir.join("industry_data.json"), json) {
2624 warn!("Failed to write industry data: {}", e);
2625 } else {
2626 info!(
2627 " Industry data written: {} GL accounts for {}",
2628 industry_output.gl_accounts.len(),
2629 industry_output.industry
2630 );
2631 }
2632 }
2633 Err(e) => warn!("Failed to serialize industry data: {}", e),
2634 }
2635 }
2636 }
2637
2638 if result.graph_export.exported {
2642 let graph_dir = output_dir.join("graph_export");
2643 std::fs::create_dir_all(&graph_dir).ok();
2644 match serde_json::to_string_pretty(&result.graph_export) {
2645 Ok(json) => {
2646 if let Err(e) = std::fs::write(graph_dir.join("graph_export_summary.json"), json) {
2647 warn!("Failed to write graph export summary: {}", e);
2648 } else {
2649 info!(" Graph export summary written");
2650 }
2651 }
2652 Err(e) => warn!("Failed to serialize graph export summary: {}", e),
2653 }
2654 }
2655
2656 let cr = &result.compliance_regulations;
2660 let has_compliance_data = !cr.standard_records.is_empty()
2661 || !cr.audit_procedures.is_empty()
2662 || !cr.findings.is_empty()
2663 || !cr.filings.is_empty();
2664 if has_compliance_data {
2665 let cr_dir = output_dir.join("compliance_regulations");
2666 std::fs::create_dir_all(&cr_dir)?;
2667 info!("Writing compliance regulations data...");
2668
2669 write_json_safe(
2670 &cr.standard_records,
2671 &cr_dir.join("compliance_standards.json"),
2672 "Compliance standards",
2673 );
2674 write_json_safe(
2675 &cr.cross_reference_records,
2676 &cr_dir.join("cross_references.json"),
2677 "Cross-references",
2678 );
2679 write_json_safe(
2680 &cr.jurisdiction_records,
2681 &cr_dir.join("jurisdiction_profiles.json"),
2682 "Jurisdiction profiles",
2683 );
2684 write_json_safe(
2685 &cr.audit_procedures,
2686 &cr_dir.join("audit_procedures.json"),
2687 "Audit procedures",
2688 );
2689 write_json_safe(
2690 &cr.findings,
2691 &cr_dir.join("compliance_findings.json"),
2692 "Compliance findings",
2693 );
2694 write_json_safe(
2695 &cr.filings,
2696 &cr_dir.join("regulatory_filings.json"),
2697 "Regulatory filings",
2698 );
2699
2700 if let Some(ref graph) = cr.compliance_graph {
2701 match serde_json::to_string_pretty(graph) {
2702 Ok(json) => {
2703 if let Err(e) = std::fs::write(cr_dir.join("compliance_graph.json"), json) {
2704 warn!("Failed to write compliance graph: {}", e);
2705 } else {
2706 info!(
2707 " Compliance graph written: {} nodes, {} edges",
2708 graph.nodes.len(),
2709 graph.edges.len()
2710 );
2711 }
2712 }
2713 Err(e) => warn!("Failed to serialize compliance graph: {}", e),
2714 }
2715 }
2716 }
2717
2718 match serde_json::to_string_pretty(&result.statistics) {
2722 Ok(json) => {
2723 if let Err(e) = std::fs::write(output_dir.join("generation_statistics.json"), json) {
2724 warn!("Failed to write generation statistics: {}", e);
2725 } else {
2726 info!(" Generation statistics written");
2727 }
2728 }
2729 Err(e) => warn!("Failed to serialize generation statistics: {}", e),
2730 }
2731
2732 info!("Output writing complete.");
2733 Ok(())
2734}
2735
2736fn write_json_safe<T: serde::Serialize>(data: &[T], path: &Path, label: &str) {
2744 if SKIP_JSON.with(|c| c.get()) {
2746 return;
2747 }
2748 if FLAT_LAYOUT_ACTIVE.with(|c| c.get()) {
2749 write_json_flat(data, path, label);
2750 } else if let Err(e) = write_json(data, path, label) {
2751 warn!("Failed to write {}: {}", label, e);
2752 }
2753}
2754
2755fn write_json_auto<T: serde::Serialize>(data: &[T], path: &Path, label: &str, flat: bool) {
2757 if flat {
2758 write_json_flat(data, path, label);
2759 } else {
2760 write_json_safe(data, path, label);
2761 }
2762}
2763
2764fn write_json_always<T: serde::Serialize>(data: &[T], path: &Path, label: &str) {
2772 if SKIP_JSON.with(|c| c.get()) {
2773 return;
2774 }
2775 match std::fs::File::create(path) {
2776 Ok(file) => {
2777 let mut writer = std::io::BufWriter::with_capacity(64 * 1024, file);
2778 if let Err(e) = (|| -> Result<(), Box<dyn std::error::Error>> {
2779 writer.write_all(b"[\n")?;
2780 for (i, item) in data.iter().enumerate() {
2781 if i > 0 {
2782 writer.write_all(b",\n")?;
2783 }
2784 serde_json::to_writer_pretty(&mut writer, item)?;
2785 }
2786 if !data.is_empty() {
2787 writer.write_all(b"\n")?;
2788 }
2789 writer.write_all(b"]\n")?;
2790 writer.flush()?;
2791 Ok(())
2792 })() {
2793 warn!("Failed to write {}: {}", label, e);
2794 } else {
2795 info!(
2796 " {} written: {} records -> {}",
2797 label,
2798 data.len(),
2799 path.display()
2800 );
2801 }
2802 }
2803 Err(e) => {
2804 warn!("Failed to create {}: {}", path.display(), e);
2805 }
2806 }
2807}
2808
2809fn write_json_flat<T: serde::Serialize>(data: &[T], path: &Path, label: &str) {
2828 if data.is_empty() {
2829 return;
2830 }
2831
2832 let mut flat: Vec<serde_json::Value> = Vec::with_capacity(data.len());
2834
2835 for item in data {
2836 let val = match serde_json::to_value(item) {
2837 Ok(v) => v,
2838 Err(e) => {
2839 warn!("Failed to serialize record for flat export: {}", e);
2840 continue;
2841 }
2842 };
2843
2844 let serde_json::Value::Object(map) = val else {
2845 flat.push(val);
2846 continue;
2847 };
2848
2849 let items_key = ["items", "lines", "allocations", "line_items"]
2851 .iter()
2852 .find(|k| map.contains_key(**k))
2853 .copied();
2854
2855 let header_map = match map.get("header") {
2857 Some(serde_json::Value::Object(h)) => Some(h),
2858 _ => None,
2859 };
2860
2861 let Some(items_key) = items_key else {
2862 if let Some(header_map) = header_map {
2867 let mut merged = map.clone();
2868 merged.remove("header");
2869 for (k, v) in header_map {
2870 merged.entry(k.clone()).or_insert_with(|| v.clone());
2871 }
2872 flat.push(serde_json::Value::Object(merged));
2873 } else {
2874 flat.push(serde_json::Value::Object(map));
2875 }
2876 continue;
2877 };
2878
2879 let Some(serde_json::Value::Array(items)) = map.get(items_key) else {
2880 flat.push(serde_json::Value::Object(map));
2882 continue;
2883 };
2884
2885 if items.is_empty() {
2889 let mut merged = map.clone();
2890 merged.remove(items_key);
2891 if let Some(header_map) = header_map {
2892 merged.remove("header");
2893 for (k, v) in header_map {
2894 merged.entry(k.clone()).or_insert_with(|| v.clone());
2895 }
2896 }
2897 flat.push(serde_json::Value::Object(merged));
2898 continue;
2899 }
2900
2901 let top_fields: Vec<(&String, &serde_json::Value)> = map
2907 .iter()
2908 .filter(|(k, _)| k.as_str() != "header" && k.as_str() != items_key)
2909 .collect();
2910
2911 flat.reserve(items.len());
2912 for item_val in items {
2913 let mut merged = serde_json::Map::new();
2914 if let serde_json::Value::Object(m) = item_val {
2916 merged.extend(m.iter().map(|(k, v)| (k.clone(), v.clone())));
2917 }
2918 if let Some(header_map) = header_map {
2920 for (k, v) in header_map {
2921 merged.entry(k.clone()).or_insert_with(|| v.clone());
2922 }
2923 }
2924 for &(k, v) in &top_fields {
2926 merged.entry(k.clone()).or_insert_with(|| v.clone());
2927 }
2928 flat.push(serde_json::Value::Object(merged));
2929 }
2930 }
2931
2932 if flat.is_empty() {
2933 return;
2934 }
2935
2936 let count = flat.len();
2938 match std::fs::File::create(path) {
2939 Ok(file) => {
2940 use std::io::Write;
2941 let mut writer = std::io::BufWriter::with_capacity(512 * 1024, file);
2942 if let Err(e) = (|| -> Result<(), Box<dyn std::error::Error>> {
2943 writer.write_all(b"[\n")?;
2944 for (i, item) in flat.iter().enumerate() {
2945 if i > 0 {
2946 writer.write_all(b",\n")?;
2947 }
2948 serde_json::to_writer_pretty(&mut writer, item)?;
2949 }
2950 writer.write_all(b"\n]\n")?;
2951 writer.flush()?;
2952 Ok(())
2953 })() {
2954 warn!("Failed to write {}: {}", label, e);
2955 } else {
2956 info!(
2957 " {} written (flat): {} records -> {}",
2958 label,
2959 count,
2960 path.display()
2961 );
2962 }
2963 }
2964 Err(e) => warn!("Failed to create {}: {}", label, e),
2965 }
2966}
2967
2968fn write_json_single<T: serde::Serialize>(
2970 data: &T,
2971 path: &Path,
2972 label: &str,
2973) -> Result<(), Box<dyn std::error::Error>> {
2974 let file = std::fs::File::create(path)?;
2975 let writer = std::io::BufWriter::with_capacity(256 * 1024, file);
2976 serde_json::to_writer_pretty(writer, data)?;
2977 info!(" {} written -> {}", label, path.display());
2978 Ok(())
2979}
2980
2981fn write_json_single_safe<T: serde::Serialize>(data: &T, path: &Path, label: &str) {
2983 if SKIP_JSON.with(|c| c.get()) {
2984 return;
2985 }
2986 if let Err(e) = write_json_single(data, path, label) {
2987 warn!("Failed to write {}: {}", label, e);
2988 }
2989}
2990
2991#[derive(serde::Serialize)]
2994struct BalanceValidationSummary {
2995 validated: bool,
2996 is_balanced: bool,
2997 entries_processed: u64,
2998 total_debits: String,
2999 total_credits: String,
3000 accounts_tracked: usize,
3001 companies_tracked: usize,
3002 has_unbalanced_entries: bool,
3003 validation_error_count: usize,
3004}
3005
3006impl BalanceValidationSummary {
3007 fn from(v: &crate::enhanced_orchestrator::BalanceValidationResult) -> Self {
3008 Self {
3009 validated: v.validated,
3010 is_balanced: v.is_balanced,
3011 entries_processed: v.entries_processed,
3012 total_debits: v.total_debits.to_string(),
3013 total_credits: v.total_credits.to_string(),
3014 accounts_tracked: v.accounts_tracked,
3015 companies_tracked: v.companies_tracked,
3016 has_unbalanced_entries: v.has_unbalanced_entries,
3017 validation_error_count: v.validation_errors.len(),
3018 }
3019 }
3020}