fiscal_core/complement/
inutilizacao.rs1use crate::FiscalError;
4use crate::status_codes::sefaz_status;
5use crate::xml_utils::extract_xml_tag_value;
6
7use super::helpers::{DEFAULT_VERSION, extract_attribute, extract_tag, join_xml};
8
9pub fn attach_inutilizacao(request_xml: &str, response_xml: &str) -> Result<String, FiscalError> {
25 if request_xml.is_empty() {
26 return Err(FiscalError::XmlParsing(
27 "Inutilizacao request XML is empty".into(),
28 ));
29 }
30 if response_xml.is_empty() {
31 return Err(FiscalError::XmlParsing(
32 "Inutilizacao response XML is empty".into(),
33 ));
34 }
35
36 let inut_content = extract_tag(request_xml, "inutNFe").ok_or_else(|| {
37 FiscalError::XmlParsing("Could not find <inutNFe> tag in request XML".into())
38 })?;
39
40 let ret_inut_content = extract_tag(response_xml, "retInutNFe").ok_or_else(|| {
41 FiscalError::XmlParsing("Could not find <retInutNFe> tag in response XML".into())
42 })?;
43
44 let c_stat = extract_xml_tag_value(&ret_inut_content, "cStat").unwrap_or_default();
46 if c_stat != sefaz_status::VOIDED {
47 let x_motivo = extract_xml_tag_value(&ret_inut_content, "xMotivo").unwrap_or_default();
48 return Err(FiscalError::SefazRejection {
49 code: c_stat,
50 message: x_motivo,
51 });
52 }
53
54 let version = extract_attribute(&inut_content, "inutNFe", "versao")
56 .unwrap_or_else(|| DEFAULT_VERSION.to_string());
57
58 let ret_version = extract_attribute(&ret_inut_content, "retInutNFe", "versao")
60 .unwrap_or_else(|| DEFAULT_VERSION.to_string());
61
62 let cpf_or_cnpj_tag = if extract_xml_tag_value(&inut_content, "CNPJ").is_some() {
64 "CNPJ"
65 } else {
66 "CPF"
67 };
68
69 let field_pairs: &[(&str, &str, &str)] = &[("versao", &version, &ret_version)];
70 for &(name, req_val, ret_val) in field_pairs {
71 if req_val != ret_val {
72 return Err(FiscalError::XmlParsing(format!(
73 "Inutilização: {name} diverge entre request ({req_val}) e response ({ret_val})"
74 )));
75 }
76 }
77
78 let tag_pairs: &[&str] = &[
79 "tpAmb",
80 "cUF",
81 "ano",
82 cpf_or_cnpj_tag,
83 "mod",
84 "serie",
85 "nNFIni",
86 "nNFFin",
87 ];
88 for tag_name in tag_pairs {
89 let req_val = extract_xml_tag_value(&inut_content, tag_name).unwrap_or_default();
90 let ret_val = extract_xml_tag_value(&ret_inut_content, tag_name).unwrap_or_default();
91 if req_val != ret_val {
92 return Err(FiscalError::XmlParsing(format!(
93 "Inutilização: <{tag_name}> diverge entre request ({req_val}) e response ({ret_val})"
94 )));
95 }
96 }
97
98 Ok(join_xml(
99 &inut_content,
100 &ret_inut_content,
101 "ProcInutNFe",
102 &version,
103 ))
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn attach_inutilizacao_empty_request() {
112 let err = attach_inutilizacao("", "<retInutNFe/>").unwrap_err();
113 assert!(matches!(err, FiscalError::XmlParsing(_)));
114 }
115
116 #[test]
117 fn attach_inutilizacao_empty_response() {
118 let err = attach_inutilizacao("<inutNFe/>", "").unwrap_err();
119 assert!(matches!(err, FiscalError::XmlParsing(_)));
120 }
121
122 #[test]
123 fn attach_inutilizacao_missing_inut_tag() {
124 let err = attach_inutilizacao("<other/>", "<retInutNFe><cStat>102</cStat></retInutNFe>")
125 .unwrap_err();
126 assert!(matches!(err, FiscalError::XmlParsing(_)));
127 }
128
129 #[test]
130 fn attach_inutilizacao_missing_ret_tag() {
131 let err = attach_inutilizacao(r#"<inutNFe versao="4.00"><data/></inutNFe>"#, "<other/>")
132 .unwrap_err();
133 assert!(matches!(err, FiscalError::XmlParsing(_)));
134 }
135
136 #[test]
137 fn attach_inutilizacao_rejected_status() {
138 let err = attach_inutilizacao(
139 r#"<inutNFe versao="4.00"><data/></inutNFe>"#,
140 r#"<retInutNFe><cStat>999</cStat><xMotivo>Erro</xMotivo></retInutNFe>"#,
141 )
142 .unwrap_err();
143 assert!(matches!(err, FiscalError::SefazRejection { .. }));
144 }
145
146 #[test]
147 fn attach_inutilizacao_success() {
148 let result = attach_inutilizacao(
149 r#"<inutNFe versao="4.00"><infInut/></inutNFe>"#,
150 r#"<retInutNFe><cStat>102</cStat><xMotivo>Inutilizacao de numero homologado</xMotivo></retInutNFe>"#,
151 )
152 .unwrap();
153 assert!(result.contains("<ProcInutNFe"));
154 assert!(result.contains("<inutNFe"));
155 assert!(result.contains("<retInutNFe>"));
156 }
157
158 #[test]
161 fn attach_inutilizacao_version_mismatch() {
162 let request = concat!(
163 r#"<inutNFe versao="4.00"><infInut>"#,
164 r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
165 r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>1</serie>"#,
166 r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
167 r#"</infInut></inutNFe>"#
168 );
169 let response = concat!(
170 r#"<retInutNFe versao="3.10"><infInut>"#,
171 r#"<cStat>102</cStat><xMotivo>Inutilizacao homologada</xMotivo>"#,
172 r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
173 r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>1</serie>"#,
174 r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
175 r#"</infInut></retInutNFe>"#
176 );
177 let err = attach_inutilizacao(request, response).unwrap_err();
178 match err {
179 FiscalError::XmlParsing(msg) => {
180 assert!(
181 msg.contains("versao"),
182 "Expected version mismatch error, got: {msg}"
183 );
184 }
185 other => panic!("Expected XmlParsing, got {:?}", other),
186 }
187 }
188
189 #[test]
192 fn attach_inutilizacao_tag_value_mismatch() {
193 let request = concat!(
194 r#"<inutNFe versao="4.00"><infInut>"#,
195 r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
196 r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>1</serie>"#,
197 r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
198 r#"</infInut></inutNFe>"#
199 );
200 let response = concat!(
201 r#"<retInutNFe versao="4.00"><infInut>"#,
202 r#"<cStat>102</cStat><xMotivo>Inutilizacao homologada</xMotivo>"#,
203 r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
204 r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>2</serie>"#,
205 r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
206 r#"</infInut></retInutNFe>"#
207 );
208 let err = attach_inutilizacao(request, response).unwrap_err();
209 match err {
210 FiscalError::XmlParsing(msg) => {
211 assert!(
212 msg.contains("serie"),
213 "Expected serie mismatch error, got: {msg}"
214 );
215 }
216 other => panic!("Expected XmlParsing, got {:?}", other),
217 }
218 }
219}