1use std::fs;
2use std::io::{self, Write};
3use std::path::Path;
4
5use colored::Colorize;
6
7use crate::formatter::{format_csv_header, format_csv_row, format_table, OutputFormat};
8use crate::models::Comprobante;
9use crate::parser;
10
11pub struct BulkResult {
12 pub total: usize,
13 pub success: usize,
14 pub failed: usize,
15 pub errors: Vec<(String, String)>,
16}
17
18pub fn process_directory(
19 dir: &Path,
20 format: OutputFormat,
21 output: Option<&Path>,
22) -> io::Result<BulkResult> {
23 let xml_files = collect_xml_files(dir)?;
24
25 let mut result = BulkResult {
26 total: xml_files.len(),
27 success: 0,
28 failed: 0,
29 errors: Vec::new(),
30 };
31
32 let cfdis: Vec<(String, Comprobante)> = xml_files
33 .iter()
34 .filter_map(|path| {
35 let filename = path.display().to_string();
36 match parser::parse_file(path) {
37 Ok(cfdi) => {
38 result.success += 1;
39 Some((filename, cfdi))
40 }
41 Err(e) => {
42 result.failed += 1;
43 result.errors.push((filename, e.to_string()));
44 None
45 }
46 }
47 })
48 .collect();
49
50 let mut writer: Box<dyn Write> = match output {
51 Some(path) => Box::new(fs::File::create(path)?),
52 None => Box::new(io::stdout().lock()),
53 };
54
55 match format {
56 OutputFormat::Json => {
57 let all: Vec<&Comprobante> = cfdis.iter().map(|(_, c)| c).collect();
58 let json = serde_json::to_string_pretty(&all)
59 .map_err(io::Error::other)?;
60 writeln!(writer, "{json}")?;
61 }
62 OutputFormat::Csv => {
63 let mut csv_wtr = csv::Writer::from_writer(writer);
64 format_csv_header(&mut csv_wtr)?;
65 for (_, cfdi) in &cfdis {
66 format_csv_row(cfdi, &mut csv_wtr)?;
67 }
68 csv_wtr.flush()?;
69 }
70 OutputFormat::Table => {
71 for (filename, cfdi) in &cfdis {
72 writeln!(writer, " {} {}", "Archivo:".bold(), filename)?;
73 format_table(cfdi, &mut writer)?;
74 writeln!(writer)?;
75 }
76 }
77 }
78
79 Ok(result)
80}
81
82pub fn summary(dir: &Path) -> io::Result<()> {
83 let xml_files = collect_xml_files(dir)?;
84 let mut total_ingresos: f64 = 0.0;
85 let mut total_egresos: f64 = 0.0;
86 let mut total_iva: f64 = 0.0;
87 let mut count_by_type: std::collections::HashMap<String, usize> =
88 std::collections::HashMap::new();
89 let mut by_rfc_emisor: std::collections::HashMap<String, f64> =
90 std::collections::HashMap::new();
91 let mut success = 0usize;
92 let mut failed = 0usize;
93
94 for path in &xml_files {
95 match parser::parse_file(path) {
96 Ok(cfdi) => {
97 success += 1;
98 let total: f64 = cfdi.total.parse().unwrap_or(0.0);
99 let tipo = cfdi.tipo_de_comprobante.clone();
100
101 *count_by_type.entry(tipo.clone()).or_insert(0) += 1;
102
103 match tipo.as_str() {
104 "I" => total_ingresos += total,
105 "E" => total_egresos += total,
106 _ => {}
107 }
108
109 if let Some(imp) = &cfdi.impuestos {
110 if let Some(ref t) = imp.total_impuestos_trasladados {
111 total_iva += t.parse::<f64>().unwrap_or(0.0);
112 }
113 }
114
115 *by_rfc_emisor.entry(cfdi.emisor.rfc.clone()).or_insert(0.0) += total;
116 }
117 Err(_) => {
118 failed += 1;
119 }
120 }
121 }
122
123 let stdout = io::stdout();
124 let mut out = stdout.lock();
125
126 writeln!(out, "\n{}", "RESUMEN DE CFDIS".bold().cyan())?;
127 writeln!(out, "{}", "=".repeat(50).dimmed())?;
128 writeln!(out, " Archivos procesados: {success}/{}", xml_files.len())?;
129 if failed > 0 {
130 writeln!(out, " {} {failed}", "Errores:".red())?;
131 }
132 writeln!(out)?;
133
134 writeln!(out, " {}", "Por tipo:".bold())?;
135 for (tipo, count) in &count_by_type {
136 let label = match tipo.as_str() {
137 "I" => "Ingreso",
138 "E" => "Egreso",
139 "T" => "Traslado",
140 "P" => "Pago",
141 "N" => "Nomina",
142 other => other,
143 };
144 writeln!(out, " {label}: {count}")?;
145 }
146 writeln!(out)?;
147
148 writeln!(out, " {}", "Totales:".bold())?;
149 writeln!(out, " {} ${total_ingresos:.2}", "Ingresos:".green())?;
150 writeln!(out, " {} ${total_egresos:.2}", "Egresos:".red())?;
151 writeln!(out, " {} ${total_iva:.2}", "IVA Trasladado:".yellow())?;
152 writeln!(
153 out,
154 " {} ${:.2}",
155 "Neto:".bold(),
156 total_ingresos - total_egresos
157 )?;
158 writeln!(out)?;
159
160 if !by_rfc_emisor.is_empty() {
161 writeln!(out, " {}", "Por RFC Emisor:".bold())?;
162 let mut rfcs: Vec<_> = by_rfc_emisor.into_iter().collect();
163 rfcs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
164 for (rfc, total) in rfcs {
165 writeln!(out, " {rfc}: ${total:.2}")?;
166 }
167 }
168
169 writeln!(out, "{}", "=".repeat(50).dimmed())?;
170 Ok(())
171}
172
173fn collect_xml_files(dir: &Path) -> io::Result<Vec<std::path::PathBuf>> {
174 let mut files = Vec::new();
175 if !dir.is_dir() {
176 return Err(io::Error::new(
177 io::ErrorKind::NotFound,
178 format!("{} is not a directory", dir.display()),
179 ));
180 }
181 for entry in fs::read_dir(dir)? {
182 let entry = entry?;
183 let path = entry.path();
184 if path.is_file() {
185 if let Some(ext) = path.extension() {
186 if ext.eq_ignore_ascii_case("xml") {
187 files.push(path);
188 }
189 }
190 }
191 }
192 files.sort();
193 Ok(files)
194}