1use crate::models::Comprobante;
2
3#[derive(Debug)]
4pub struct ValidationResult {
5 pub valid: bool,
6 pub errors: Vec<String>,
7 pub warnings: Vec<String>,
8}
9
10pub fn validate(cfdi: &Comprobante) -> ValidationResult {
11 let mut errors = Vec::new();
12 let mut warnings = Vec::new();
13
14 if cfdi.version != "4.0" && cfdi.version != "3.3" {
16 errors.push(format!("Version no soportada: {}", cfdi.version));
17 }
18
19 if cfdi.fecha.is_empty() {
21 errors.push("Fecha es requerida".to_string());
22 }
23 if cfdi.sub_total.is_empty() {
24 errors.push("SubTotal es requerido".to_string());
25 }
26 if cfdi.total.is_empty() {
27 errors.push("Total es requerido".to_string());
28 }
29 if cfdi.tipo_de_comprobante.is_empty() {
30 errors.push("TipoDeComprobante es requerido".to_string());
31 }
32 if cfdi.lugar_expedicion.is_empty() {
33 errors.push("LugarExpedicion es requerido".to_string());
34 }
35 if cfdi.sello.is_empty() {
36 errors.push("Sello digital es requerido".to_string());
37 }
38 if cfdi.certificado.is_empty() {
39 errors.push("Certificado es requerido".to_string());
40 }
41 if cfdi.no_certificado.is_empty() {
42 errors.push("NoCertificado es requerido".to_string());
43 }
44
45 if cfdi.emisor.rfc.is_empty() {
47 errors.push("Emisor RFC es requerido".to_string());
48 } else if !is_valid_rfc(&cfdi.emisor.rfc) {
49 errors.push(format!("Emisor RFC invalido: {}", cfdi.emisor.rfc));
50 }
51 if cfdi.emisor.regimen_fiscal.is_empty() {
52 errors.push("Emisor RegimenFiscal es requerido".to_string());
53 }
54
55 if cfdi.receptor.rfc.is_empty() {
57 errors.push("Receptor RFC es requerido".to_string());
58 } else if !is_valid_rfc(&cfdi.receptor.rfc) {
59 warnings.push(format!(
60 "Receptor RFC podria ser invalido: {}",
61 cfdi.receptor.rfc
62 ));
63 }
64 if cfdi.receptor.uso_cfdi.is_empty() {
65 errors.push("Receptor UsoCFDI es requerido".to_string());
66 }
67
68 if cfdi.conceptos.is_empty() {
70 errors.push("Debe tener al menos un Concepto".to_string());
71 }
72 for (i, concepto) in cfdi.conceptos.iter().enumerate() {
73 let prefix = format!("Concepto[{}]", i + 1);
74 if concepto.clave_prod_serv.is_empty() {
75 errors.push(format!("{prefix}: ClaveProdServ es requerido"));
76 }
77 if concepto.cantidad.is_empty() {
78 errors.push(format!("{prefix}: Cantidad es requerida"));
79 }
80 if concepto.descripcion.is_empty() {
81 errors.push(format!("{prefix}: Descripcion es requerida"));
82 }
83 if concepto.valor_unitario.is_empty() {
84 errors.push(format!("{prefix}: ValorUnitario es requerido"));
85 }
86 if concepto.importe.is_empty() {
87 errors.push(format!("{prefix}: Importe es requerido"));
88 }
89 }
90
91 if let (Ok(sub), Ok(total)) = (
93 cfdi.sub_total.parse::<f64>(),
94 cfdi.total.parse::<f64>(),
95 ) {
96 let descuento: f64 = cfdi
97 .descuento
98 .as_deref()
99 .and_then(|d| d.parse().ok())
100 .unwrap_or(0.0);
101 let trasladados: f64 = cfdi
102 .impuestos
103 .as_ref()
104 .and_then(|i| i.total_impuestos_trasladados.as_deref())
105 .and_then(|t| t.parse().ok())
106 .unwrap_or(0.0);
107 let retenidos: f64 = cfdi
108 .impuestos
109 .as_ref()
110 .and_then(|i| i.total_impuestos_retenidos.as_deref())
111 .and_then(|t| t.parse().ok())
112 .unwrap_or(0.0);
113
114 let expected = sub - descuento + trasladados - retenidos;
115 if (expected - total).abs() > 0.01 {
116 warnings.push(format!(
117 "Total ({total}) no coincide con calculo esperado ({expected:.2}): SubTotal({sub}) - Descuento({descuento}) + Trasladados({trasladados}) - Retenidos({retenidos})"
118 ));
119 }
120 }
121
122 if cfdi.uuid().is_none() {
124 warnings.push("No tiene TimbreFiscalDigital (UUID)".to_string());
125 }
126
127 if !["I", "E", "T", "P", "N"].contains(&cfdi.tipo_de_comprobante.as_str()) {
129 errors.push(format!(
130 "TipoDeComprobante invalido: {}",
131 cfdi.tipo_de_comprobante
132 ));
133 }
134
135 ValidationResult {
136 valid: errors.is_empty(),
137 errors,
138 warnings,
139 }
140}
141
142fn is_valid_rfc(rfc: &str) -> bool {
143 let len = rfc.len();
144 (len == 12 || len == 13) && rfc.chars().all(|c| c.is_ascii_alphanumeric())
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::parser::parse_str;
153
154 fn valid_cfdi_xml() -> &'static str {
155 r#"<?xml version="1.0" encoding="utf-8"?>
156<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/4" xmlns:tfd="http://www.sat.gob.mx/TimbreFiscalDigital" Version="4.0" Fecha="2024-01-15T10:30:00" Sello="abc123" NoCertificado="00001000000504465028" Certificado="CERT123" SubTotal="1000.00" Total="1160.00" TipoDeComprobante="I" FormaPago="03" Moneda="MXN" LugarExpedicion="44100">
157 <cfdi:Emisor Rfc="AAA010101AAA" Nombre="Test" RegimenFiscal="601"/>
158 <cfdi:Receptor Rfc="BBB020202BBB" Nombre="Cliente" UsoCFDI="G03"/>
159 <cfdi:Conceptos>
160 <cfdi:Concepto ClaveProdServ="84111506" Cantidad="1" ClaveUnidad="E48" Descripcion="Servicio" ValorUnitario="1000.00" Importe="1000.00" ObjetoImp="02"/>
161 </cfdi:Conceptos>
162 <cfdi:Impuestos TotalImpuestosTrasladados="160.00">
163 <cfdi:Traslados>
164 <cfdi:Traslado Base="1000.00" Impuesto="002" TipoFactor="Tasa" TasaOCuota="0.160000" Importe="160.00"/>
165 </cfdi:Traslados>
166 </cfdi:Impuestos>
167 <cfdi:Complemento>
168 <tfd:TimbreFiscalDigital UUID="6128396F-0000-1111-2222-333344445555" FechaTimbrado="2024-01-15T10:31:00" SelloCFD="abc123" NoCertificadoSAT="001" SelloSAT="xyz"/>
169 </cfdi:Complemento>
170</cfdi:Comprobante>"#
171 }
172
173 #[test]
174 fn test_valid_cfdi() {
175 let cfdi = parse_str(valid_cfdi_xml()).unwrap();
176 let result = validate(&cfdi);
177 assert!(result.valid, "Errors: {:?}", result.errors);
178 }
179
180 #[test]
181 fn test_rfc_validation() {
182 assert!(is_valid_rfc("AAA010101AAA")); assert!(is_valid_rfc("AABB010101CCC")); assert!(!is_valid_rfc("SHORT"));
185 assert!(!is_valid_rfc("AAA-010101-AAA")); }
187}