fiscal_core/complement/
b2b.rs1use crate::FiscalError;
4
5use super::helpers::{extract_tag, normalize_nfe_proc_attrs, strip_newlines};
6
7pub fn attach_b2b(
23 nfe_proc_xml: &str,
24 b2b_xml: &str,
25 tag_b2b: Option<&str>,
26) -> Result<String, FiscalError> {
27 let tag_name = tag_b2b.unwrap_or("NFeB2BFin");
28
29 if !nfe_proc_xml.contains("<nfeProc") {
30 return Err(FiscalError::XmlParsing(
31 "XML does not contain <nfeProc> — is this an authorized NFe?".into(),
32 ));
33 }
34
35 let open_check = format!("<{tag_name}");
36 if !b2b_xml.contains(&open_check) {
37 return Err(FiscalError::XmlParsing(format!(
38 "B2B XML does not contain <{tag_name}> tag"
39 )));
40 }
41
42 let nfe_proc_content = extract_tag(nfe_proc_xml, "nfeProc")
43 .ok_or_else(|| FiscalError::XmlParsing("Could not extract <nfeProc> from XML".into()))?;
44
45 let nfe_proc_normalized = normalize_nfe_proc_attrs(&nfe_proc_content);
48
49 let b2b_content = extract_tag(b2b_xml, tag_name).ok_or_else(|| {
50 FiscalError::XmlParsing(format!("Could not extract <{tag_name}> from B2B XML"))
51 })?;
52
53 let raw = format!(
54 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
55 <nfeProcB2B>{nfe_proc_normalized}{b2b_content}</nfeProcB2B>"
56 );
57
58 let cleaned = strip_newlines(&raw);
64 Ok(cleaned)
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
74 fn attach_b2b_no_nfe_proc() {
75 let err = attach_b2b("<NFe/>", "<NFeB2BFin>data</NFeB2BFin>", None).unwrap_err();
76 assert!(matches!(err, FiscalError::XmlParsing(_)));
77 }
78
79 #[test]
80 fn attach_b2b_no_b2b_tag() {
81 let err = attach_b2b("<nfeProc><NFe/></nfeProc>", "<other>data</other>", None).unwrap_err();
82 assert!(matches!(err, FiscalError::XmlParsing(_)));
83 }
84
85 #[test]
86 fn attach_b2b_extract_failure() {
87 let err = attach_b2b("<nfeProc><NFe/>", "<NFeB2BFin>data</NFeB2BFin>", None).unwrap_err();
89 assert!(matches!(err, FiscalError::XmlParsing(_)));
90 }
91
92 #[test]
93 fn attach_b2b_success() {
94 let result = attach_b2b(
95 "<nfeProc><NFe/><protNFe/></nfeProc>",
96 "<NFeB2BFin><tag>data</tag></NFeB2BFin>",
97 None,
98 )
99 .unwrap();
100 assert!(result.contains("<nfeProcB2B>"));
101 assert!(result.contains("<nfeProc>"));
102 assert!(result.contains("<NFeB2BFin>"));
103 }
104
105 #[test]
106 fn attach_b2b_custom_tag() {
107 let result = attach_b2b(
108 "<nfeProc><NFe/><protNFe/></nfeProc>",
109 "<CustomB2B><tag>data</tag></CustomB2B>",
110 Some("CustomB2B"),
111 )
112 .unwrap();
113 assert!(result.contains("<CustomB2B>"));
114 }
115
116 #[test]
119 fn attach_b2b_strips_newlines() {
120 let nfe_proc = "<nfeProc versao=\"4.00\">\n<NFe/>\n<protNFe/>\n</nfeProc>";
121 let b2b = "<NFeB2BFin>\n<data>test</data>\n</NFeB2BFin>";
122 let result = attach_b2b(nfe_proc, b2b, None).unwrap();
123 assert!(!result.contains('\n'), "Result should not contain newlines");
124 assert!(
125 !result.contains('\r'),
126 "Result should not contain carriage returns"
127 );
128 assert!(result.contains("<nfeProcB2B>"));
129 assert!(result.contains("<NFeB2BFin>"));
130 }
131
132 #[test]
133 fn attach_b2b_strips_carriage_returns() {
134 let nfe_proc = "<nfeProc versao=\"4.00\">\r\n<NFe/>\r\n</nfeProc>";
135 let b2b = "<NFeB2BFin><data>test</data></NFeB2BFin>";
136 let result = attach_b2b(nfe_proc, b2b, None).unwrap();
137 assert!(!result.contains('\r'));
138 assert!(!result.contains('\n'));
139 }
140
141 #[test]
144 fn attach_b2b_extract_tag_coverage() {
145 let nfe_proc = concat!(
146 r#"<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">"#,
147 r#"<NFe><infNFe/></NFe><protNFe><infProt/></protNFe>"#,
148 r#"</nfeProc>"#
149 );
150 let b2b = r#"<NFeB2BFin versao="1.00"><dados>value</dados></NFeB2BFin>"#;
151 let result = attach_b2b(nfe_proc, b2b, None).unwrap();
152 assert!(result.contains("<nfeProcB2B>"));
153 assert!(result.contains("<dados>value</dados>"));
154 }
155}