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