1use crate::FiscalError;
2use crate::constants::NFE_NAMESPACE;
3use crate::status_codes::{VALID_EVENT_STATUSES, VALID_PROTOCOL_STATUSES, sefaz_status};
4use crate::xml_utils::extract_xml_tag_value;
5
6const DEFAULT_VERSION: &str = "4.00";
8
9pub fn attach_protocol(request_xml: &str, response_xml: &str) -> Result<String, FiscalError> {
32 if request_xml.is_empty() {
33 return Err(FiscalError::XmlParsing("Request XML (NFe) is empty".into()));
34 }
35 if response_xml.is_empty() {
36 return Err(FiscalError::XmlParsing(
37 "Response XML (protocol) is empty".into(),
38 ));
39 }
40
41 let nfe_content = extract_tag(request_xml, "NFe")
42 .ok_or_else(|| FiscalError::XmlParsing("Could not find <NFe> tag in request XML".into()))?;
43
44 let digest_nfe = extract_xml_tag_value(request_xml, "DigestValue");
46 let access_key = extract_inf_nfe_id(request_xml);
47
48 let mut matched_prot: Option<String> = None;
50
51 let prot_nodes = extract_all_tags(response_xml, "protNFe");
52
53 for prot in &prot_nodes {
54 let dig_val = extract_xml_tag_value(prot, "digVal");
55 let ch_nfe = extract_xml_tag_value(prot, "chNFe");
56
57 if let (Some(dn), Some(dv)) = (&digest_nfe, &dig_val) {
58 if let (Some(ak), Some(cn)) = (&access_key, &ch_nfe) {
59 if dn == dv && ak == cn {
60 let c_stat = extract_xml_tag_value(prot, "cStat").unwrap_or_default();
62 if !VALID_PROTOCOL_STATUSES.contains(&c_stat.as_str()) {
63 let x_motivo = extract_xml_tag_value(prot, "xMotivo").unwrap_or_default();
64 return Err(FiscalError::SefazRejection {
65 code: c_stat,
66 message: x_motivo,
67 });
68 }
69 matched_prot = Some(prot.clone());
70 break;
71 }
72 }
73 }
74 }
75
76 if matched_prot.is_none() {
77 let single_prot = extract_tag(response_xml, "protNFe").ok_or_else(|| {
79 FiscalError::XmlParsing("Could not find <protNFe> in response XML".into())
80 })?;
81
82 let c_stat = extract_xml_tag_value(&single_prot, "cStat").unwrap_or_default();
84 if !VALID_PROTOCOL_STATUSES.contains(&c_stat.as_str()) {
85 let x_motivo = extract_xml_tag_value(&single_prot, "xMotivo").unwrap_or_default();
86 return Err(FiscalError::SefazRejection {
87 code: c_stat,
88 message: x_motivo,
89 });
90 }
91 matched_prot = Some(single_prot);
92 }
93
94 let version = extract_attribute(&nfe_content, "infNFe", "versao")
95 .unwrap_or_else(|| DEFAULT_VERSION.to_string());
96
97 Ok(join_xml(
98 &nfe_content,
99 &matched_prot.unwrap(),
100 "nfeProc",
101 &version,
102 ))
103}
104
105pub fn attach_inutilizacao(request_xml: &str, response_xml: &str) -> Result<String, FiscalError> {
121 if request_xml.is_empty() {
122 return Err(FiscalError::XmlParsing(
123 "Inutilizacao request XML is empty".into(),
124 ));
125 }
126 if response_xml.is_empty() {
127 return Err(FiscalError::XmlParsing(
128 "Inutilizacao response XML is empty".into(),
129 ));
130 }
131
132 let inut_content = extract_tag(request_xml, "inutNFe").ok_or_else(|| {
133 FiscalError::XmlParsing("Could not find <inutNFe> tag in request XML".into())
134 })?;
135
136 let ret_inut_content = extract_tag(response_xml, "retInutNFe").ok_or_else(|| {
137 FiscalError::XmlParsing("Could not find <retInutNFe> tag in response XML".into())
138 })?;
139
140 let c_stat = extract_xml_tag_value(&ret_inut_content, "cStat").unwrap_or_default();
142 if c_stat != sefaz_status::VOIDED {
143 let x_motivo = extract_xml_tag_value(&ret_inut_content, "xMotivo").unwrap_or_default();
144 return Err(FiscalError::SefazRejection {
145 code: c_stat,
146 message: x_motivo,
147 });
148 }
149
150 let version = extract_attribute(&inut_content, "inutNFe", "versao")
152 .unwrap_or_else(|| DEFAULT_VERSION.to_string());
153
154 Ok(join_xml(
155 &inut_content,
156 &ret_inut_content,
157 "ProcInutNFe",
158 &version,
159 ))
160}
161
162pub fn attach_event_protocol(request_xml: &str, response_xml: &str) -> Result<String, FiscalError> {
179 if request_xml.is_empty() {
180 return Err(FiscalError::XmlParsing("Event request XML is empty".into()));
181 }
182 if response_xml.is_empty() {
183 return Err(FiscalError::XmlParsing(
184 "Event response XML is empty".into(),
185 ));
186 }
187
188 let evento_content = extract_tag(request_xml, "evento").ok_or_else(|| {
189 FiscalError::XmlParsing("Could not find <evento> tag in request XML".into())
190 })?;
191
192 let ret_evento_content = extract_tag(response_xml, "retEvento").ok_or_else(|| {
193 FiscalError::XmlParsing("Could not find <retEvento> tag in response XML".into())
194 })?;
195
196 let version = extract_attribute(&evento_content, "evento", "versao")
198 .unwrap_or_else(|| DEFAULT_VERSION.to_string());
199
200 let c_stat = extract_xml_tag_value(&ret_evento_content, "cStat").unwrap_or_default();
202 if !VALID_EVENT_STATUSES.contains(&c_stat.as_str()) {
203 let x_motivo = extract_xml_tag_value(&ret_evento_content, "xMotivo").unwrap_or_default();
204 return Err(FiscalError::SefazRejection {
205 code: c_stat,
206 message: x_motivo,
207 });
208 }
209
210 Ok(join_xml(
211 &evento_content,
212 &ret_evento_content,
213 "procEventoNFe",
214 &version,
215 ))
216}
217
218pub fn attach_b2b(
234 nfe_proc_xml: &str,
235 b2b_xml: &str,
236 tag_b2b: Option<&str>,
237) -> Result<String, FiscalError> {
238 let tag_name = tag_b2b.unwrap_or("NFeB2BFin");
239
240 if !nfe_proc_xml.contains("<nfeProc") {
241 return Err(FiscalError::XmlParsing(
242 "XML does not contain <nfeProc> — is this an authorized NFe?".into(),
243 ));
244 }
245
246 let open_check = format!("<{tag_name}");
247 if !b2b_xml.contains(&open_check) {
248 return Err(FiscalError::XmlParsing(format!(
249 "B2B XML does not contain <{tag_name}> tag"
250 )));
251 }
252
253 let nfe_proc_content = extract_tag(nfe_proc_xml, "nfeProc")
254 .ok_or_else(|| FiscalError::XmlParsing("Could not extract <nfeProc> from XML".into()))?;
255
256 let b2b_content = extract_tag(b2b_xml, tag_name).ok_or_else(|| {
257 FiscalError::XmlParsing(format!("Could not extract <{tag_name}> from B2B XML"))
258 })?;
259
260 Ok(format!(
261 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
262 <nfeProcB2B>{nfe_proc_content}{b2b_content}</nfeProcB2B>"
263 ))
264}
265
266fn join_xml(first: &str, second: &str, node_name: &str, version: &str) -> String {
278 format!(
279 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
280 <{node_name} versao=\"{version}\" xmlns=\"{NFE_NAMESPACE}\">\
281 {first}{second}</{node_name}>"
282 )
283}
284
285fn extract_tag(xml: &str, tag_name: &str) -> Option<String> {
291 let open_pattern = format!("<{tag_name}");
293 let start = xml.find(&open_pattern)?;
294
295 let after_open = start + open_pattern.len();
298 if after_open < xml.len() {
299 let next_char = xml.as_bytes()[after_open];
300 if next_char != b' '
301 && next_char != b'>'
302 && next_char != b'/'
303 && next_char != b'\n'
304 && next_char != b'\r'
305 && next_char != b'\t'
306 {
307 return None;
308 }
309 }
310
311 let close_tag = format!("</{tag_name}>");
312 let close_index = xml.rfind(&close_tag)?;
313
314 Some(xml[start..close_index + close_tag.len()].to_string())
315}
316
317fn extract_all_tags(xml: &str, tag_name: &str) -> Vec<String> {
320 let mut results = Vec::new();
321 let open_pattern = format!("<{tag_name}");
322 let close_tag = format!("</{tag_name}>");
323 let mut search_from = 0;
324
325 while search_from < xml.len() {
326 let start = match xml[search_from..].find(&open_pattern) {
327 Some(pos) => search_from + pos,
328 None => break,
329 };
330
331 let after_open = start + open_pattern.len();
333 if after_open < xml.len() {
334 let next_char = xml.as_bytes()[after_open];
335 if next_char != b' '
336 && next_char != b'>'
337 && next_char != b'/'
338 && next_char != b'\n'
339 && next_char != b'\r'
340 && next_char != b'\t'
341 {
342 search_from = after_open;
343 continue;
344 }
345 }
346
347 let end = match xml[start..].find(&close_tag) {
348 Some(pos) => start + pos + close_tag.len(),
349 None => break,
350 };
351
352 results.push(xml[start..end].to_string());
353 search_from = end;
354 }
355
356 results
357}
358
359fn extract_attribute(xml: &str, tag_name: &str, attr_name: &str) -> Option<String> {
362 let open = format!("<{tag_name}");
363 let start = xml.find(&open)?;
364
365 let tag_end = xml[start..].find('>')? + start;
367 let tag_header = &xml[start..tag_end];
368
369 let attr_pattern = format!("{attr_name}=\"");
371 let attr_start = tag_header.find(&attr_pattern)? + attr_pattern.len();
372 let attr_end = tag_header[attr_start..].find('"')? + attr_start;
373
374 Some(tag_header[attr_start..attr_end].to_string())
375}
376
377fn extract_inf_nfe_id(xml: &str) -> Option<String> {
380 let attr_val = extract_attribute(xml, "infNFe", "Id")?;
381 Some(
382 attr_val
383 .strip_prefix("NFe")
384 .unwrap_or(&attr_val)
385 .to_string(),
386 )
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 #[test]
394 fn extract_tag_finds_outermost_match() {
395 let xml = r#"<root><NFe versao="4.00"><inner/></NFe></root>"#;
396 let result = extract_tag(xml, "NFe").unwrap();
397 assert!(result.starts_with("<NFe"));
398 assert!(result.ends_with("</NFe>"));
399 assert!(result.contains("<inner/>"));
400 }
401
402 #[test]
403 fn extract_tag_returns_none_for_missing_tag() {
404 let xml = "<root><other/></root>";
405 assert!(extract_tag(xml, "NFe").is_none());
406 }
407
408 #[test]
409 fn extract_tag_does_not_match_prefix() {
410 let xml = "<root><NFeExtra>data</NFeExtra></root>";
411 assert!(extract_tag(xml, "NFe").is_none());
412 }
413
414 #[test]
415 fn extract_attribute_works() {
416 let xml = r#"<infNFe versao="4.00" Id="NFe12345">"#;
417 assert_eq!(
418 extract_attribute(xml, "infNFe", "versao"),
419 Some("4.00".to_string())
420 );
421 assert_eq!(
422 extract_attribute(xml, "infNFe", "Id"),
423 Some("NFe12345".to_string())
424 );
425 }
426
427 #[test]
428 fn extract_all_tags_finds_multiple() {
429 let xml = r#"<root><item>1</item><item>2</item><item>3</item></root>"#;
430 let items = extract_all_tags(xml, "item");
431 assert_eq!(items.len(), 3);
432 assert!(items[0].contains("1"));
433 assert!(items[2].contains("3"));
434 }
435
436 #[test]
437 fn join_xml_produces_correct_wrapper() {
438 let result = join_xml("<A/>", "<B/>", "wrapper", "4.00");
439 assert!(result.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"));
440 assert!(result.contains("<wrapper versao=\"4.00\""));
441 assert!(result.contains(&format!("xmlns=\"{NFE_NAMESPACE}\"")));
442 assert!(result.ends_with("</wrapper>"));
443 }
444
445 #[test]
446 fn extract_inf_nfe_id_strips_prefix() {
447 let xml = r#"<NFe><infNFe versao="4.00" Id="NFe35260112345678000199650010000000011123456780"></infNFe></NFe>"#;
448 let key = extract_inf_nfe_id(xml).unwrap();
449 assert_eq!(key, "35260112345678000199650010000000011123456780");
450 }
451}