1use crate::FiscalError;
2use crate::constants::NFE_NAMESPACE;
3use crate::status_codes::{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 mut found_dig_val = false;
79 for prot in &prot_nodes {
80 if extract_xml_tag_value(prot, "digVal").is_some() {
81 found_dig_val = true;
82 break;
83 }
84 }
85
86 if !prot_nodes.is_empty() && !found_dig_val {
87 let first_prot = &prot_nodes[0];
89 let c_stat = extract_xml_tag_value(first_prot, "cStat").unwrap_or_default();
90 let x_motivo = extract_xml_tag_value(first_prot, "xMotivo").unwrap_or_default();
91 let msg = format!("digVal ausente na resposta SEFAZ: [{c_stat}] {x_motivo}");
92 return Err(FiscalError::SefazRejection {
93 code: c_stat,
94 message: msg,
95 });
96 }
97
98 if found_dig_val {
99 let key_info = access_key.as_deref().unwrap_or("unknown");
101 return Err(FiscalError::XmlParsing(format!(
102 "Os digest são diferentes [{key_info}]"
103 )));
104 }
105
106 let single_prot = extract_tag(response_xml, "protNFe").ok_or_else(|| {
108 FiscalError::XmlParsing("Could not find <protNFe> in response XML".into())
109 })?;
110
111 let c_stat = extract_xml_tag_value(&single_prot, "cStat").unwrap_or_default();
113 if !VALID_PROTOCOL_STATUSES.contains(&c_stat.as_str()) {
114 let x_motivo = extract_xml_tag_value(&single_prot, "xMotivo").unwrap_or_default();
115 return Err(FiscalError::SefazRejection {
116 code: c_stat,
117 message: x_motivo,
118 });
119 }
120 matched_prot = Some(single_prot);
121 }
122
123 let version = extract_attribute(&nfe_content, "infNFe", "versao")
124 .unwrap_or_else(|| DEFAULT_VERSION.to_string());
125
126 Ok(join_xml(
127 &nfe_content,
128 &matched_prot.unwrap(),
129 "nfeProc",
130 &version,
131 ))
132}
133
134pub fn attach_inutilizacao(request_xml: &str, response_xml: &str) -> Result<String, FiscalError> {
150 if request_xml.is_empty() {
151 return Err(FiscalError::XmlParsing(
152 "Inutilizacao request XML is empty".into(),
153 ));
154 }
155 if response_xml.is_empty() {
156 return Err(FiscalError::XmlParsing(
157 "Inutilizacao response XML is empty".into(),
158 ));
159 }
160
161 let inut_content = extract_tag(request_xml, "inutNFe").ok_or_else(|| {
162 FiscalError::XmlParsing("Could not find <inutNFe> tag in request XML".into())
163 })?;
164
165 let ret_inut_content = extract_tag(response_xml, "retInutNFe").ok_or_else(|| {
166 FiscalError::XmlParsing("Could not find <retInutNFe> tag in response XML".into())
167 })?;
168
169 let c_stat = extract_xml_tag_value(&ret_inut_content, "cStat").unwrap_or_default();
171 if c_stat != sefaz_status::VOIDED {
172 let x_motivo = extract_xml_tag_value(&ret_inut_content, "xMotivo").unwrap_or_default();
173 return Err(FiscalError::SefazRejection {
174 code: c_stat,
175 message: x_motivo,
176 });
177 }
178
179 let version = extract_attribute(&inut_content, "inutNFe", "versao")
181 .unwrap_or_else(|| DEFAULT_VERSION.to_string());
182
183 let ret_version = extract_attribute(&ret_inut_content, "retInutNFe", "versao")
185 .unwrap_or_else(|| DEFAULT_VERSION.to_string());
186
187 let cpf_or_cnpj_tag = if extract_xml_tag_value(&inut_content, "CNPJ").is_some() {
189 "CNPJ"
190 } else {
191 "CPF"
192 };
193
194 let field_pairs: &[(&str, &str, &str)] = &[("versao", &version, &ret_version)];
195 for &(name, req_val, ret_val) in field_pairs {
196 if req_val != ret_val {
197 return Err(FiscalError::XmlParsing(format!(
198 "Inutilização: {name} diverge entre request ({req_val}) e response ({ret_val})"
199 )));
200 }
201 }
202
203 let tag_pairs: &[&str] = &[
204 "tpAmb",
205 "cUF",
206 "ano",
207 cpf_or_cnpj_tag,
208 "mod",
209 "serie",
210 "nNFIni",
211 "nNFFin",
212 ];
213 for tag_name in tag_pairs {
214 let req_val = extract_xml_tag_value(&inut_content, tag_name).unwrap_or_default();
215 let ret_val = extract_xml_tag_value(&ret_inut_content, tag_name).unwrap_or_default();
216 if req_val != ret_val {
217 return Err(FiscalError::XmlParsing(format!(
218 "Inutilização: <{tag_name}> diverge entre request ({req_val}) e response ({ret_val})"
219 )));
220 }
221 }
222
223 Ok(join_xml(
224 &inut_content,
225 &ret_inut_content,
226 "ProcInutNFe",
227 &version,
228 ))
229}
230
231pub fn attach_event_protocol(request_xml: &str, response_xml: &str) -> Result<String, FiscalError> {
251 if request_xml.is_empty() {
252 return Err(FiscalError::XmlParsing("Event request XML is empty".into()));
253 }
254 if response_xml.is_empty() {
255 return Err(FiscalError::XmlParsing(
256 "Event response XML is empty".into(),
257 ));
258 }
259
260 let evento_content = extract_tag(request_xml, "evento").ok_or_else(|| {
261 FiscalError::XmlParsing("Could not find <evento> tag in request XML".into())
262 })?;
263
264 let ret_evento_content = extract_tag(response_xml, "retEvento").ok_or_else(|| {
265 FiscalError::XmlParsing("Could not find <retEvento> tag in response XML".into())
266 })?;
267
268 let version = extract_attribute(&evento_content, "evento", "versao")
270 .unwrap_or_else(|| DEFAULT_VERSION.to_string());
271
272 let req_id_lote = extract_xml_tag_value(request_xml, "idLote");
274 let ret_id_lote = extract_xml_tag_value(response_xml, "idLote");
275 if let (Some(req_lote), Some(ret_lote)) = (&req_id_lote, &ret_id_lote) {
276 if req_lote != ret_lote {
277 return Err(FiscalError::XmlParsing(
278 "Os números de lote dos documentos são diferentes".into(),
279 ));
280 }
281 }
282
283 let c_stat = extract_xml_tag_value(&ret_evento_content, "cStat").unwrap_or_default();
285 let tp_evento = extract_xml_tag_value(&ret_evento_content, "tpEvento").unwrap_or_default();
286
287 let mut valid_statuses: Vec<&str> = vec!["135", "136"];
289 if tp_evento == EVT_CANCELA {
290 valid_statuses.push("155");
291 }
292
293 if !valid_statuses.contains(&c_stat.as_str()) {
294 let x_motivo = extract_xml_tag_value(&ret_evento_content, "xMotivo").unwrap_or_default();
295 return Err(FiscalError::SefazRejection {
296 code: c_stat,
297 message: x_motivo,
298 });
299 }
300
301 Ok(join_xml(
302 &evento_content,
303 &ret_evento_content,
304 "procEventoNFe",
305 &version,
306 ))
307}
308
309pub fn attach_b2b(
325 nfe_proc_xml: &str,
326 b2b_xml: &str,
327 tag_b2b: Option<&str>,
328) -> Result<String, FiscalError> {
329 let tag_name = tag_b2b.unwrap_or("NFeB2BFin");
330
331 if !nfe_proc_xml.contains("<nfeProc") {
332 return Err(FiscalError::XmlParsing(
333 "XML does not contain <nfeProc> — is this an authorized NFe?".into(),
334 ));
335 }
336
337 let open_check = format!("<{tag_name}");
338 if !b2b_xml.contains(&open_check) {
339 return Err(FiscalError::XmlParsing(format!(
340 "B2B XML does not contain <{tag_name}> tag"
341 )));
342 }
343
344 let nfe_proc_content = extract_tag(nfe_proc_xml, "nfeProc")
345 .ok_or_else(|| FiscalError::XmlParsing("Could not extract <nfeProc> from XML".into()))?;
346
347 let b2b_content = extract_tag(b2b_xml, tag_name).ok_or_else(|| {
348 FiscalError::XmlParsing(format!("Could not extract <{tag_name}> from B2B XML"))
349 })?;
350
351 let raw = format!(
352 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
353 <nfeProcB2B>{nfe_proc_content}{b2b_content}</nfeProcB2B>"
354 );
355
356 let cleaned = strip_newlines(&raw);
362 Ok(cleaned)
363}
364
365fn strip_newlines(s: &str) -> String {
372 let mut result = String::with_capacity(s.len());
373 let mut chars = s.chars().peekable();
374 while let Some(c) = chars.next() {
375 if c == '\n' || c == '\r' {
376 continue;
377 }
378 if c == '\\' {
379 if let Some(&'s') = chars.peek() {
380 chars.next(); continue;
382 }
383 }
384 result.push(c);
385 }
386 result
387}
388
389pub fn to_authorize(request_xml: &str, response_xml: &str) -> Result<String, FiscalError> {
411 if request_xml.is_empty() {
412 return Err(FiscalError::XmlParsing(
413 "Erro ao protocolar: o XML a protocolar está vazio.".into(),
414 ));
415 }
416 if response_xml.is_empty() {
417 return Err(FiscalError::XmlParsing(
418 "Erro ao protocolar: o retorno da SEFAZ está vazio.".into(),
419 ));
420 }
421
422 if contains_xml_tag(request_xml, "NFe") {
428 attach_protocol(request_xml, response_xml)
429 } else if contains_xml_tag(request_xml, "envEvento") {
430 attach_event_protocol(request_xml, response_xml)
431 } else if contains_xml_tag(request_xml, "inutNFe") {
432 attach_inutilizacao(request_xml, response_xml)
433 } else {
434 Err(FiscalError::XmlParsing(
435 "Tipo de documento não reconhecido para protocolação".into(),
436 ))
437 }
438}
439
440fn contains_xml_tag(xml: &str, tag_name: &str) -> bool {
442 let pattern = format!("<{tag_name}");
443 for (i, _) in xml.match_indices(&pattern) {
444 let after = i + pattern.len();
445 if after >= xml.len() {
446 return true;
447 }
448 let next = xml.as_bytes()[after];
449 if next == b' '
450 || next == b'>'
451 || next == b'/'
452 || next == b'\n'
453 || next == b'\r'
454 || next == b'\t'
455 {
456 return true;
457 }
458 }
459 false
460}
461
462const EVT_CANCELA: &str = "110111";
464const EVT_CANCELA_SUBSTITUICAO: &str = "110112";
466
467const VALID_CANCEL_STATUSES: &[&str] = &["135", "136", "155"];
473
474pub fn attach_cancellation(
500 nfe_proc_xml: &str,
501 cancel_event_xml: &str,
502) -> Result<String, FiscalError> {
503 let prot_nfe = extract_tag(nfe_proc_xml, "protNFe").ok_or_else(|| {
505 FiscalError::XmlParsing(
506 "Could not find <protNFe> in NF-e XML — is this an authorized NF-e?".into(),
507 )
508 })?;
509
510 let ch_nfe = extract_xml_tag_value(&prot_nfe, "chNFe")
511 .ok_or_else(|| FiscalError::XmlParsing("Could not find <chNFe> inside <protNFe>".into()))?;
512
513 let ret_eventos = extract_all_tags(cancel_event_xml, "retEvento");
515
516 for ret_evento in &ret_eventos {
517 let c_stat = match extract_xml_tag_value(ret_evento, "cStat") {
518 Some(v) => v,
519 None => continue,
520 };
521 let tp_evento = match extract_xml_tag_value(ret_evento, "tpEvento") {
522 Some(v) => v,
523 None => continue,
524 };
525 let ch_nfe_evento = match extract_xml_tag_value(ret_evento, "chNFe") {
526 Some(v) => v,
527 None => continue,
528 };
529
530 if VALID_CANCEL_STATUSES.contains(&c_stat.as_str())
531 && (tp_evento == EVT_CANCELA || tp_evento == EVT_CANCELA_SUBSTITUICAO)
532 && ch_nfe_evento == ch_nfe
533 {
534 let close_tag = "</nfeProc>";
536 if let Some(pos) = nfe_proc_xml.rfind(close_tag) {
537 let mut result = String::with_capacity(nfe_proc_xml.len() + ret_evento.len());
538 result.push_str(&nfe_proc_xml[..pos]);
539 result.push_str(ret_evento);
540 result.push_str(close_tag);
541 return Ok(result);
542 }
543 break;
545 }
546 }
547
548 Ok(nfe_proc_xml.to_string())
550}
551
552fn join_xml(first: &str, second: &str, node_name: &str, version: &str) -> String {
564 format!(
565 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
566 <{node_name} versao=\"{version}\" xmlns=\"{NFE_NAMESPACE}\">\
567 {first}{second}</{node_name}>"
568 )
569}
570
571fn extract_tag(xml: &str, tag_name: &str) -> Option<String> {
577 let open_pattern = format!("<{tag_name}");
579 let start = xml.find(&open_pattern)?;
580
581 let after_open = start + open_pattern.len();
584 if after_open < xml.len() {
585 let next_char = xml.as_bytes()[after_open];
586 if next_char != b' '
587 && next_char != b'>'
588 && next_char != b'/'
589 && next_char != b'\n'
590 && next_char != b'\r'
591 && next_char != b'\t'
592 {
593 return None;
594 }
595 }
596
597 let close_tag = format!("</{tag_name}>");
598 let close_index = xml.rfind(&close_tag)?;
599
600 Some(xml[start..close_index + close_tag.len()].to_string())
601}
602
603fn extract_all_tags(xml: &str, tag_name: &str) -> Vec<String> {
606 let mut results = Vec::new();
607 let open_pattern = format!("<{tag_name}");
608 let close_tag = format!("</{tag_name}>");
609 let mut search_from = 0;
610
611 while search_from < xml.len() {
612 let start = match xml[search_from..].find(&open_pattern) {
613 Some(pos) => search_from + pos,
614 None => break,
615 };
616
617 let after_open = start + open_pattern.len();
619 if after_open < xml.len() {
620 let next_char = xml.as_bytes()[after_open];
621 if next_char != b' '
622 && next_char != b'>'
623 && next_char != b'/'
624 && next_char != b'\n'
625 && next_char != b'\r'
626 && next_char != b'\t'
627 {
628 search_from = after_open;
629 continue;
630 }
631 }
632
633 let end = match xml[start..].find(&close_tag) {
634 Some(pos) => start + pos + close_tag.len(),
635 None => break,
636 };
637
638 results.push(xml[start..end].to_string());
639 search_from = end;
640 }
641
642 results
643}
644
645fn extract_attribute(xml: &str, tag_name: &str, attr_name: &str) -> Option<String> {
648 let open = format!("<{tag_name}");
649 let start = xml.find(&open)?;
650
651 let tag_end = xml[start..].find('>')? + start;
653 let tag_header = &xml[start..tag_end];
654
655 let attr_pattern = format!("{attr_name}=\"");
657 let attr_start = tag_header.find(&attr_pattern)? + attr_pattern.len();
658 let attr_end = tag_header[attr_start..].find('"')? + attr_start;
659
660 Some(tag_header[attr_start..attr_end].to_string())
661}
662
663fn extract_inf_nfe_id(xml: &str) -> Option<String> {
666 let attr_val = extract_attribute(xml, "infNFe", "Id")?;
667 Some(
668 attr_val
669 .strip_prefix("NFe")
670 .unwrap_or(&attr_val)
671 .to_string(),
672 )
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678
679 #[test]
680 fn extract_tag_finds_outermost_match() {
681 let xml = r#"<root><NFe versao="4.00"><inner/></NFe></root>"#;
682 let result = extract_tag(xml, "NFe").unwrap();
683 assert!(result.starts_with("<NFe"));
684 assert!(result.ends_with("</NFe>"));
685 assert!(result.contains("<inner/>"));
686 }
687
688 #[test]
689 fn extract_tag_returns_none_for_missing_tag() {
690 let xml = "<root><other/></root>";
691 assert!(extract_tag(xml, "NFe").is_none());
692 }
693
694 #[test]
695 fn extract_tag_does_not_match_prefix() {
696 let xml = "<root><NFeExtra>data</NFeExtra></root>";
697 assert!(extract_tag(xml, "NFe").is_none());
698 }
699
700 #[test]
701 fn extract_attribute_works() {
702 let xml = r#"<infNFe versao="4.00" Id="NFe12345">"#;
703 assert_eq!(
704 extract_attribute(xml, "infNFe", "versao"),
705 Some("4.00".to_string())
706 );
707 assert_eq!(
708 extract_attribute(xml, "infNFe", "Id"),
709 Some("NFe12345".to_string())
710 );
711 }
712
713 #[test]
714 fn extract_all_tags_finds_multiple() {
715 let xml = r#"<root><item>1</item><item>2</item><item>3</item></root>"#;
716 let items = extract_all_tags(xml, "item");
717 assert_eq!(items.len(), 3);
718 assert!(items[0].contains("1"));
719 assert!(items[2].contains("3"));
720 }
721
722 #[test]
723 fn join_xml_produces_correct_wrapper() {
724 let result = join_xml("<A/>", "<B/>", "wrapper", "4.00");
725 assert!(result.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"));
726 assert!(result.contains("<wrapper versao=\"4.00\""));
727 assert!(result.contains(&format!("xmlns=\"{NFE_NAMESPACE}\"")));
728 assert!(result.ends_with("</wrapper>"));
729 }
730
731 #[test]
732 fn extract_inf_nfe_id_strips_prefix() {
733 let xml = r#"<NFe><infNFe versao="4.00" Id="NFe35260112345678000199650010000000011123456780"></infNFe></NFe>"#;
734 let key = extract_inf_nfe_id(xml).unwrap();
735 assert_eq!(key, "35260112345678000199650010000000011123456780");
736 }
737
738 #[test]
741 fn attach_cancellation_appends_matching_ret_evento() {
742 let nfe_proc = concat!(
743 r#"<?xml version="1.0" encoding="UTF-8"?>"#,
744 r#"<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">"#,
745 r#"<NFe><infNFe versao="4.00" Id="NFe35260112345678000199550010000000011123456780">"#,
746 r#"<ide/></infNFe></NFe>"#,
747 r#"<protNFe versao="4.00"><infProt>"#,
748 r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
749 r#"<cStat>100</cStat><nProt>135220000009921</nProt>"#,
750 r#"</infProt></protNFe>"#,
751 r#"</nfeProc>"#
752 );
753
754 let cancel_xml = concat!(
755 r#"<retEnvEvento><retEvento versao="1.00"><infEvento>"#,
756 r#"<cStat>135</cStat>"#,
757 r#"<xMotivo>Evento registrado e vinculado a NF-e</xMotivo>"#,
758 r#"<tpEvento>110111</tpEvento>"#,
759 r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
760 r#"<nProt>135220000009999</nProt>"#,
761 r#"</infEvento></retEvento></retEnvEvento>"#
762 );
763
764 let result = attach_cancellation(nfe_proc, cancel_xml).unwrap();
765
766 assert!(
768 result.contains("<retEvento"),
769 "Result should contain <retEvento>"
770 );
771 assert!(
772 result.contains("<tpEvento>110111</tpEvento>"),
773 "Result should contain cancellation event type"
774 );
775 let ret_pos = result.find("<retEvento").unwrap();
777 let close_pos = result.rfind("</nfeProc>").unwrap();
778 assert!(ret_pos < close_pos, "retEvento should be before </nfeProc>");
779 assert!(result.contains("<protNFe"));
781 assert!(result.contains("<NFe>"));
782 }
783
784 #[test]
785 fn attach_cancellation_ignores_non_matching_ch_nfe() {
786 let nfe_proc = concat!(
787 r#"<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">"#,
788 r#"<NFe/>"#,
789 r#"<protNFe versao="4.00"><infProt>"#,
790 r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
791 r#"<cStat>100</cStat>"#,
792 r#"</infProt></protNFe>"#,
793 r#"</nfeProc>"#
794 );
795
796 let cancel_xml = concat!(
797 r#"<retEvento versao="1.00"><infEvento>"#,
798 r#"<cStat>135</cStat>"#,
799 r#"<tpEvento>110111</tpEvento>"#,
800 r#"<chNFe>99999999999999999999999999999999999999999999</chNFe>"#,
801 r#"<nProt>135220000009999</nProt>"#,
802 r#"</infEvento></retEvento>"#
803 );
804
805 let result = attach_cancellation(nfe_proc, cancel_xml).unwrap();
806 assert_eq!(result, nfe_proc);
808 }
809
810 #[test]
811 fn attach_cancellation_ignores_wrong_tp_evento() {
812 let nfe_proc = concat!(
813 r#"<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">"#,
814 r#"<NFe/>"#,
815 r#"<protNFe versao="4.00"><infProt>"#,
816 r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
817 r#"<cStat>100</cStat>"#,
818 r#"</infProt></protNFe>"#,
819 r#"</nfeProc>"#
820 );
821
822 let cancel_xml = concat!(
823 r#"<retEvento versao="1.00"><infEvento>"#,
824 r#"<cStat>135</cStat>"#,
825 r#"<tpEvento>110110</tpEvento>"#, r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
827 r#"<nProt>135220000009999</nProt>"#,
828 r#"</infEvento></retEvento>"#
829 );
830
831 let result = attach_cancellation(nfe_proc, cancel_xml).unwrap();
832 assert_eq!(result, nfe_proc);
834 }
835
836 #[test]
837 fn attach_cancellation_ignores_rejected_status() {
838 let nfe_proc = concat!(
839 r#"<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">"#,
840 r#"<NFe/>"#,
841 r#"<protNFe versao="4.00"><infProt>"#,
842 r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
843 r#"<cStat>100</cStat>"#,
844 r#"</infProt></protNFe>"#,
845 r#"</nfeProc>"#
846 );
847
848 let cancel_xml = concat!(
849 r#"<retEvento versao="1.00"><infEvento>"#,
850 r#"<cStat>573</cStat>"#, r#"<tpEvento>110111</tpEvento>"#,
852 r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
853 r#"<nProt>135220000009999</nProt>"#,
854 r#"</infEvento></retEvento>"#
855 );
856
857 let result = attach_cancellation(nfe_proc, cancel_xml).unwrap();
858 assert_eq!(result, nfe_proc);
860 }
861
862 #[test]
863 fn attach_cancellation_accepts_status_155() {
864 let nfe_proc = concat!(
865 r#"<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">"#,
866 r#"<NFe/>"#,
867 r#"<protNFe versao="4.00"><infProt>"#,
868 r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
869 r#"<cStat>100</cStat>"#,
870 r#"</infProt></protNFe>"#,
871 r#"</nfeProc>"#
872 );
873
874 let cancel_xml = concat!(
875 r#"<retEvento versao="1.00"><infEvento>"#,
876 r#"<cStat>155</cStat>"#,
877 r#"<tpEvento>110111</tpEvento>"#,
878 r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
879 r#"<nProt>135220000009999</nProt>"#,
880 r#"</infEvento></retEvento>"#
881 );
882
883 let result = attach_cancellation(nfe_proc, cancel_xml).unwrap();
884 assert!(result.contains("<retEvento"));
885 }
886
887 #[test]
888 fn attach_cancellation_accepts_substituicao_110112() {
889 let nfe_proc = concat!(
890 r#"<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">"#,
891 r#"<NFe/>"#,
892 r#"<protNFe versao="4.00"><infProt>"#,
893 r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
894 r#"<cStat>100</cStat>"#,
895 r#"</infProt></protNFe>"#,
896 r#"</nfeProc>"#
897 );
898
899 let cancel_xml = concat!(
900 r#"<retEvento versao="1.00"><infEvento>"#,
901 r#"<cStat>135</cStat>"#,
902 r#"<tpEvento>110112</tpEvento>"#,
903 r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
904 r#"<nProt>135220000009999</nProt>"#,
905 r#"</infEvento></retEvento>"#
906 );
907
908 let result = attach_cancellation(nfe_proc, cancel_xml).unwrap();
909 assert!(
910 result.contains("<tpEvento>110112</tpEvento>"),
911 "Should accept cancellation by substitution"
912 );
913 }
914
915 #[test]
916 fn attach_cancellation_rejects_missing_prot_nfe() {
917 let nfe_xml = "<NFe><infNFe/></NFe>";
918 let cancel_xml = "<retEvento/>";
919 let err = attach_cancellation(nfe_xml, cancel_xml).unwrap_err();
920 assert!(matches!(err, FiscalError::XmlParsing(_)));
921 }
922
923 #[test]
924 fn attach_cancellation_rejects_missing_ch_nfe_in_prot() {
925 let nfe_proc = concat!(
926 r#"<nfeProc><protNFe versao="4.00"><infProt>"#,
927 r#"<cStat>100</cStat>"#,
928 r#"</infProt></protNFe></nfeProc>"#
929 );
930 let cancel_xml = "<retEvento/>";
931 let err = attach_cancellation(nfe_proc, cancel_xml).unwrap_err();
932 assert!(matches!(err, FiscalError::XmlParsing(_)));
933 }
934
935 #[test]
938 fn attach_protocol_empty_request_xml() {
939 let err = attach_protocol("", "<protNFe/>").unwrap_err();
940 assert!(matches!(err, FiscalError::XmlParsing(_)));
941 }
942
943 #[test]
944 fn attach_protocol_empty_response_xml() {
945 let err = attach_protocol("<NFe/>", "").unwrap_err();
946 assert!(matches!(err, FiscalError::XmlParsing(_)));
947 }
948
949 #[test]
950 fn attach_protocol_matching_digest_and_key() {
951 let request = concat!(
952 r#"<NFe><infNFe versao="4.00" Id="NFe35260112345678000199650010000000011123456780">"#,
953 r#"<ide/></infNFe>"#,
954 r#"<Signature><SignedInfo/><SignatureValue/>"#,
955 r#"<KeyInfo><DigestValue>abc123</DigestValue></KeyInfo></Signature>"#,
956 r#"</NFe>"#
957 );
958 let response = concat!(
959 r#"<protNFe versao="4.00"><infProt>"#,
960 r#"<digVal>abc123</digVal>"#,
961 r#"<chNFe>35260112345678000199650010000000011123456780</chNFe>"#,
962 r#"<cStat>100</cStat>"#,
963 r#"<xMotivo>Autorizado</xMotivo>"#,
964 r#"</infProt></protNFe>"#
965 );
966 let result = attach_protocol(request, response).unwrap();
967 assert!(result.contains("<nfeProc"));
968 assert!(result.contains("</nfeProc>"));
969 assert!(result.contains("<NFe>"));
970 assert!(result.contains("<protNFe"));
971 }
972
973 #[test]
974 fn attach_protocol_rejected_status_in_exact_match() {
975 let request = concat!(
976 r#"<NFe><infNFe versao="4.00" Id="NFe35260112345678000199650010000000011123456780">"#,
977 r#"<ide/></infNFe>"#,
978 r#"<Signature><SignedInfo/><SignatureValue/>"#,
979 r#"<KeyInfo><DigestValue>abc123</DigestValue></KeyInfo></Signature>"#,
980 r#"</NFe>"#
981 );
982 let response = concat!(
983 r#"<protNFe versao="4.00"><infProt>"#,
984 r#"<digVal>abc123</digVal>"#,
985 r#"<chNFe>35260112345678000199650010000000011123456780</chNFe>"#,
986 r#"<cStat>999</cStat>"#,
987 r#"<xMotivo>Rejeitada</xMotivo>"#,
988 r#"</infProt></protNFe>"#
989 );
990 let err = attach_protocol(request, response).unwrap_err();
991 assert!(matches!(err, FiscalError::SefazRejection { .. }));
992 }
993
994 #[test]
995 fn attach_protocol_fallback_rejected_status() {
996 let request = concat!(
998 r#"<NFe><infNFe versao="4.00" Id="NFe35260112345678000199650010000000011123456780">"#,
999 r#"<ide/></infNFe></NFe>"#
1000 );
1001 let response = concat!(
1002 r#"<protNFe versao="4.00"><infProt>"#,
1003 r#"<cStat>999</cStat>"#,
1004 r#"<xMotivo>Rejeitada</xMotivo>"#,
1005 r#"</infProt></protNFe>"#
1006 );
1007 let err = attach_protocol(request, response).unwrap_err();
1008 assert!(matches!(err, FiscalError::SefazRejection { .. }));
1009 }
1010
1011 #[test]
1014 fn attach_inutilizacao_empty_request() {
1015 let err = attach_inutilizacao("", "<retInutNFe/>").unwrap_err();
1016 assert!(matches!(err, FiscalError::XmlParsing(_)));
1017 }
1018
1019 #[test]
1020 fn attach_inutilizacao_empty_response() {
1021 let err = attach_inutilizacao("<inutNFe/>", "").unwrap_err();
1022 assert!(matches!(err, FiscalError::XmlParsing(_)));
1023 }
1024
1025 #[test]
1026 fn attach_inutilizacao_missing_inut_tag() {
1027 let err = attach_inutilizacao("<other/>", "<retInutNFe><cStat>102</cStat></retInutNFe>")
1028 .unwrap_err();
1029 assert!(matches!(err, FiscalError::XmlParsing(_)));
1030 }
1031
1032 #[test]
1033 fn attach_inutilizacao_missing_ret_tag() {
1034 let err = attach_inutilizacao(r#"<inutNFe versao="4.00"><data/></inutNFe>"#, "<other/>")
1035 .unwrap_err();
1036 assert!(matches!(err, FiscalError::XmlParsing(_)));
1037 }
1038
1039 #[test]
1040 fn attach_inutilizacao_rejected_status() {
1041 let err = attach_inutilizacao(
1042 r#"<inutNFe versao="4.00"><data/></inutNFe>"#,
1043 r#"<retInutNFe><cStat>999</cStat><xMotivo>Erro</xMotivo></retInutNFe>"#,
1044 )
1045 .unwrap_err();
1046 assert!(matches!(err, FiscalError::SefazRejection { .. }));
1047 }
1048
1049 #[test]
1050 fn attach_inutilizacao_success() {
1051 let result = attach_inutilizacao(
1052 r#"<inutNFe versao="4.00"><infInut/></inutNFe>"#,
1053 r#"<retInutNFe><cStat>102</cStat><xMotivo>Inutilizacao de numero homologado</xMotivo></retInutNFe>"#,
1054 )
1055 .unwrap();
1056 assert!(result.contains("<ProcInutNFe"));
1057 assert!(result.contains("<inutNFe"));
1058 assert!(result.contains("<retInutNFe>"));
1059 }
1060
1061 #[test]
1064 fn attach_event_protocol_empty_request() {
1065 let err = attach_event_protocol("", "<retEvento/>").unwrap_err();
1066 assert!(matches!(err, FiscalError::XmlParsing(_)));
1067 }
1068
1069 #[test]
1070 fn attach_event_protocol_empty_response() {
1071 let err = attach_event_protocol("<evento/>", "").unwrap_err();
1072 assert!(matches!(err, FiscalError::XmlParsing(_)));
1073 }
1074
1075 #[test]
1076 fn attach_event_protocol_missing_evento() {
1077 let err = attach_event_protocol(
1078 "<other/>",
1079 "<retEvento><infEvento><cStat>135</cStat></infEvento></retEvento>",
1080 )
1081 .unwrap_err();
1082 assert!(matches!(err, FiscalError::XmlParsing(_)));
1083 }
1084
1085 #[test]
1086 fn attach_event_protocol_missing_ret_evento() {
1087 let err =
1088 attach_event_protocol(r#"<evento versao="1.00"><infEvento/></evento>"#, "<other/>")
1089 .unwrap_err();
1090 assert!(matches!(err, FiscalError::XmlParsing(_)));
1091 }
1092
1093 #[test]
1094 fn attach_event_protocol_rejected_status() {
1095 let err = attach_event_protocol(
1096 r#"<evento versao="1.00"><infEvento/></evento>"#,
1097 r#"<retEvento><infEvento><cStat>999</cStat><xMotivo>Rejeitado</xMotivo></infEvento></retEvento>"#,
1098 )
1099 .unwrap_err();
1100 assert!(matches!(err, FiscalError::SefazRejection { .. }));
1101 }
1102
1103 #[test]
1104 fn attach_event_protocol_success() {
1105 let result = attach_event_protocol(
1106 r#"<evento versao="1.00"><infEvento Id="ID1234"/></evento>"#,
1107 r#"<retEvento><infEvento><cStat>135</cStat><xMotivo>Evento registrado</xMotivo></infEvento></retEvento>"#,
1108 )
1109 .unwrap();
1110 assert!(result.contains("<procEventoNFe"));
1111 assert!(result.contains("<evento"));
1112 assert!(result.contains("<retEvento>"));
1113 }
1114
1115 #[test]
1118 fn attach_b2b_no_nfe_proc() {
1119 let err = attach_b2b("<NFe/>", "<NFeB2BFin>data</NFeB2BFin>", None).unwrap_err();
1120 assert!(matches!(err, FiscalError::XmlParsing(_)));
1121 }
1122
1123 #[test]
1124 fn attach_b2b_no_b2b_tag() {
1125 let err = attach_b2b("<nfeProc><NFe/></nfeProc>", "<other>data</other>", None).unwrap_err();
1126 assert!(matches!(err, FiscalError::XmlParsing(_)));
1127 }
1128
1129 #[test]
1130 fn attach_b2b_extract_failure() {
1131 let err = attach_b2b("<nfeProc><NFe/>", "<NFeB2BFin>data</NFeB2BFin>", None).unwrap_err();
1133 assert!(matches!(err, FiscalError::XmlParsing(_)));
1134 }
1135
1136 #[test]
1137 fn attach_b2b_success() {
1138 let result = attach_b2b(
1139 "<nfeProc><NFe/><protNFe/></nfeProc>",
1140 "<NFeB2BFin><tag>data</tag></NFeB2BFin>",
1141 None,
1142 )
1143 .unwrap();
1144 assert!(result.contains("<nfeProcB2B>"));
1145 assert!(result.contains("<nfeProc>"));
1146 assert!(result.contains("<NFeB2BFin>"));
1147 }
1148
1149 #[test]
1150 fn attach_b2b_custom_tag() {
1151 let result = attach_b2b(
1152 "<nfeProc><NFe/><protNFe/></nfeProc>",
1153 "<CustomB2B><tag>data</tag></CustomB2B>",
1154 Some("CustomB2B"),
1155 )
1156 .unwrap();
1157 assert!(result.contains("<CustomB2B>"));
1158 }
1159
1160 #[test]
1163 fn extract_all_tags_skips_prefix_match() {
1164 let xml = "<root><protNFeExtra>bad</protNFeExtra><protNFe>good</protNFe></root>";
1166 let results = extract_all_tags(xml, "protNFe");
1167 assert_eq!(results.len(), 1);
1168 assert!(results[0].contains("good"));
1169 }
1170
1171 #[test]
1172 fn attach_cancellation_picks_first_matching_from_multiple_ret_eventos() {
1173 let nfe_proc = concat!(
1174 r#"<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">"#,
1175 r#"<NFe/>"#,
1176 r#"<protNFe versao="4.00"><infProt>"#,
1177 r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
1178 r#"<cStat>100</cStat>"#,
1179 r#"</infProt></protNFe>"#,
1180 r#"</nfeProc>"#
1181 );
1182
1183 let cancel_xml = concat!(
1184 r#"<retEnvEvento>"#,
1185 r#"<retEvento versao="1.00"><infEvento>"#,
1187 r#"<cStat>135</cStat><tpEvento>110111</tpEvento>"#,
1188 r#"<chNFe>99999999999999999999999999999999999999999999</chNFe>"#,
1189 r#"<nProt>111111111111111</nProt>"#,
1190 r#"</infEvento></retEvento>"#,
1191 r#"<retEvento versao="1.00"><infEvento>"#,
1193 r#"<cStat>135</cStat><tpEvento>110111</tpEvento>"#,
1194 r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
1195 r#"<nProt>222222222222222</nProt>"#,
1196 r#"</infEvento></retEvento>"#,
1197 r#"</retEnvEvento>"#
1198 );
1199
1200 let result = attach_cancellation(nfe_proc, cancel_xml).unwrap();
1201 assert!(result.contains("<nProt>222222222222222</nProt>"));
1202 assert_eq!(result.matches("<retEvento").count(), 1);
1204 }
1205
1206 #[test]
1209 fn to_authorize_empty_request_returns_error() {
1210 let err = to_authorize("", "<retEnviNFe/>").unwrap_err();
1211 assert!(matches!(err, FiscalError::XmlParsing(_)));
1212 }
1213
1214 #[test]
1215 fn to_authorize_empty_response_returns_error() {
1216 let err = to_authorize("<NFe/>", "").unwrap_err();
1217 assert!(matches!(err, FiscalError::XmlParsing(_)));
1218 }
1219
1220 #[test]
1221 fn to_authorize_unrecognized_document_returns_error() {
1222 let err = to_authorize("<other>data</other>", "<response/>").unwrap_err();
1223 let msg = format!("{err}");
1224 assert!(
1225 msg.contains("não reconhecido"),
1226 "should mention unrecognized type: {msg}"
1227 );
1228 }
1229
1230 #[test]
1231 fn contains_xml_tag_basic() {
1232 assert!(contains_xml_tag("<NFe versao=\"4.00\">", "NFe"));
1233 assert!(contains_xml_tag("<NFe>", "NFe"));
1234 assert!(contains_xml_tag("<NFe/>", "NFe"));
1235 assert!(!contains_xml_tag("<NFeExtra>", "NFe"));
1236 assert!(contains_xml_tag("<envEvento versao=\"1.00\">", "envEvento"));
1237 assert!(contains_xml_tag("<inutNFe versao=\"4.00\">", "inutNFe"));
1238 }
1239
1240 #[test]
1243 fn attach_b2b_strips_newlines() {
1244 let nfe_proc = "<nfeProc versao=\"4.00\">\n<NFe/>\n<protNFe/>\n</nfeProc>";
1245 let b2b = "<NFeB2BFin>\n<data>test</data>\n</NFeB2BFin>";
1246 let result = attach_b2b(nfe_proc, b2b, None).unwrap();
1247 assert!(!result.contains('\n'), "Result should not contain newlines");
1248 assert!(
1249 !result.contains('\r'),
1250 "Result should not contain carriage returns"
1251 );
1252 assert!(result.contains("<nfeProcB2B>"));
1253 assert!(result.contains("<NFeB2BFin>"));
1254 }
1255
1256 #[test]
1257 fn attach_b2b_strips_carriage_returns() {
1258 let nfe_proc = "<nfeProc versao=\"4.00\">\r\n<NFe/>\r\n</nfeProc>";
1259 let b2b = "<NFeB2BFin><data>test</data></NFeB2BFin>";
1260 let result = attach_b2b(nfe_proc, b2b, None).unwrap();
1261 assert!(!result.contains('\r'));
1262 assert!(!result.contains('\n'));
1263 }
1264
1265 #[test]
1268 fn attach_protocol_fallback_prot_invalid_status() {
1269 let request = concat!(
1271 r#"<NFe><infNFe versao="4.00" Id="NFe35260112345678000199550010000000011123456780">"#,
1272 r#"<DigestValue>abc123</DigestValue>"#,
1273 r#"</infNFe></NFe>"#
1274 );
1275 let response = concat!(
1278 r#"<protNFe versao="4.00"><infProt>"#,
1279 r#"<cStat>999</cStat>"#,
1280 r#"<xMotivo>Rejeitado</xMotivo>"#,
1281 r#"</infProt></protNFe>"#
1282 );
1283 let err = attach_protocol(request, response).unwrap_err();
1284 match err {
1285 FiscalError::SefazRejection { code, .. } => assert_eq!(code, "999"),
1286 other => panic!("Expected SefazRejection, got {:?}", other),
1287 }
1288 }
1289
1290 #[test]
1293 fn attach_inutilizacao_version_mismatch() {
1294 let request = concat!(
1295 r#"<inutNFe versao="4.00"><infInut>"#,
1296 r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
1297 r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>1</serie>"#,
1298 r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
1299 r#"</infInut></inutNFe>"#
1300 );
1301 let response = concat!(
1302 r#"<retInutNFe versao="3.10"><infInut>"#,
1303 r#"<cStat>102</cStat><xMotivo>Inutilizacao homologada</xMotivo>"#,
1304 r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
1305 r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>1</serie>"#,
1306 r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
1307 r#"</infInut></retInutNFe>"#
1308 );
1309 let err = attach_inutilizacao(request, response).unwrap_err();
1310 match err {
1311 FiscalError::XmlParsing(msg) => {
1312 assert!(
1313 msg.contains("versao"),
1314 "Expected version mismatch error, got: {msg}"
1315 );
1316 }
1317 other => panic!("Expected XmlParsing, got {:?}", other),
1318 }
1319 }
1320
1321 #[test]
1324 fn attach_inutilizacao_tag_value_mismatch() {
1325 let request = concat!(
1326 r#"<inutNFe versao="4.00"><infInut>"#,
1327 r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
1328 r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>1</serie>"#,
1329 r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
1330 r#"</infInut></inutNFe>"#
1331 );
1332 let response = concat!(
1333 r#"<retInutNFe versao="4.00"><infInut>"#,
1334 r#"<cStat>102</cStat><xMotivo>Inutilizacao homologada</xMotivo>"#,
1335 r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
1336 r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>2</serie>"#,
1337 r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
1338 r#"</infInut></retInutNFe>"#
1339 );
1340 let err = attach_inutilizacao(request, response).unwrap_err();
1341 match err {
1342 FiscalError::XmlParsing(msg) => {
1343 assert!(
1344 msg.contains("serie"),
1345 "Expected serie mismatch error, got: {msg}"
1346 );
1347 }
1348 other => panic!("Expected XmlParsing, got {:?}", other),
1349 }
1350 }
1351
1352 #[test]
1355 fn attach_event_protocol_id_lote_mismatch() {
1356 let request = concat!(
1357 r#"<envEvento><idLote>100</idLote>"#,
1358 r#"<evento versao="1.00"><infEvento>"#,
1359 r#"<tpEvento>110110</tpEvento>"#,
1360 r#"</infEvento></evento></envEvento>"#
1361 );
1362 let response = concat!(
1363 r#"<retEnvEvento><idLote>999</idLote>"#,
1364 r#"<retEvento versao="1.00"><infEvento>"#,
1365 r#"<cStat>135</cStat><xMotivo>OK</xMotivo>"#,
1366 r#"<tpEvento>110110</tpEvento>"#,
1367 r#"</infEvento></retEvento></retEnvEvento>"#
1368 );
1369 let err = attach_event_protocol(request, response).unwrap_err();
1370 match err {
1371 FiscalError::XmlParsing(msg) => {
1372 assert!(
1373 msg.contains("lote"),
1374 "Expected lote mismatch error, got: {msg}"
1375 );
1376 }
1377 other => panic!("Expected XmlParsing, got {:?}", other),
1378 }
1379 }
1380
1381 #[test]
1384 fn attach_b2b_extract_tag_coverage() {
1385 let nfe_proc = concat!(
1386 r#"<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">"#,
1387 r#"<NFe><infNFe/></NFe><protNFe><infProt/></protNFe>"#,
1388 r#"</nfeProc>"#
1389 );
1390 let b2b = r#"<NFeB2BFin versao="1.00"><dados>value</dados></NFeB2BFin>"#;
1391 let result = attach_b2b(nfe_proc, b2b, None).unwrap();
1392 assert!(result.contains("<nfeProcB2B>"));
1393 assert!(result.contains("<dados>value</dados>"));
1394 }
1395
1396 #[test]
1399 fn to_authorize_dispatches_nfe() {
1400 let request = concat!(
1401 r#"<NFe><infNFe versao="4.00" Id="NFe35260112345678000199550010000000011123456780">"#,
1402 r#"<DigestValue>abc</DigestValue>"#,
1403 r#"</infNFe></NFe>"#
1404 );
1405 let response = concat!(
1406 r#"<protNFe versao="4.00"><infProt>"#,
1407 r#"<cStat>100</cStat><xMotivo>OK</xMotivo>"#,
1408 r#"<digVal>abc</digVal>"#,
1409 r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
1410 r#"</infProt></protNFe>"#
1411 );
1412 let result = to_authorize(request, response).unwrap();
1413 assert!(result.contains("<nfeProc"));
1414 }
1415
1416 #[test]
1419 fn to_authorize_dispatches_env_evento() {
1420 let request = concat!(
1421 r#"<envEvento>"#,
1422 r#"<evento versao="1.00"><infEvento>"#,
1423 r#"<tpEvento>110110</tpEvento>"#,
1424 r#"</infEvento></evento></envEvento>"#
1425 );
1426 let response = concat!(
1427 r#"<retEvento versao="1.00"><infEvento>"#,
1428 r#"<cStat>135</cStat><xMotivo>OK</xMotivo>"#,
1429 r#"<tpEvento>110110</tpEvento>"#,
1430 r#"</infEvento></retEvento>"#
1431 );
1432 let result = to_authorize(request, response).unwrap();
1433 assert!(result.contains("<procEventoNFe"));
1434 }
1435
1436 #[test]
1439 fn to_authorize_dispatches_inut_nfe() {
1440 let request = concat!(
1441 r#"<inutNFe versao="4.00"><infInut>"#,
1442 r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
1443 r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>1</serie>"#,
1444 r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
1445 r#"</infInut></inutNFe>"#
1446 );
1447 let response = concat!(
1448 r#"<retInutNFe versao="4.00"><infInut>"#,
1449 r#"<cStat>102</cStat><xMotivo>Inutilizacao homologada</xMotivo>"#,
1450 r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
1451 r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>1</serie>"#,
1452 r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
1453 r#"</infInut></retInutNFe>"#
1454 );
1455 let result = to_authorize(request, response).unwrap();
1456 assert!(result.contains("<ProcInutNFe"));
1457 }
1458
1459 #[test]
1462 fn contains_xml_tag_at_end_of_string() {
1463 assert!(contains_xml_tag("<NFe", "NFe"));
1465 }
1466
1467 #[test]
1470 fn strip_newlines_removes_newlines_and_cr() {
1471 assert_eq!(strip_newlines("a\nb\rc\r\nd"), "abcd");
1472 }
1473
1474 #[test]
1475 fn strip_newlines_removes_literal_backslash_s() {
1476 assert_eq!(strip_newlines("abc\\sdef"), "abcdef");
1477 }
1478
1479 #[test]
1480 fn strip_newlines_preserves_normal_content() {
1481 assert_eq!(strip_newlines("<tag>value</tag>"), "<tag>value</tag>");
1482 }
1483}