use crate::FiscalError;
use super::helpers::{extract_tag, normalize_nfe_proc_attrs, strip_newlines};
pub fn attach_b2b(
nfe_proc_xml: &str,
b2b_xml: &str,
tag_b2b: Option<&str>,
) -> Result<String, FiscalError> {
let tag_name = tag_b2b.unwrap_or("NFeB2BFin");
if !nfe_proc_xml.contains("<nfeProc") {
return Err(FiscalError::XmlParsing(
"XML does not contain <nfeProc> — is this an authorized NFe?".into(),
));
}
let open_check = format!("<{tag_name}");
if !b2b_xml.contains(&open_check) {
return Err(FiscalError::XmlParsing(format!(
"B2B XML does not contain <{tag_name}> tag"
)));
}
let nfe_proc_content = extract_tag(nfe_proc_xml, "nfeProc")
.ok_or_else(|| FiscalError::XmlParsing("Could not extract <nfeProc> from XML".into()))?;
let nfe_proc_normalized = normalize_nfe_proc_attrs(&nfe_proc_content);
let b2b_content = extract_tag(b2b_xml, tag_name).ok_or_else(|| {
FiscalError::XmlParsing(format!("Could not extract <{tag_name}> from B2B XML"))
})?;
let raw = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<nfeProcB2B>{nfe_proc_normalized}{b2b_content}</nfeProcB2B>"
);
let cleaned = strip_newlines(&raw);
Ok(cleaned)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attach_b2b_no_nfe_proc() {
let err = attach_b2b("<NFe/>", "<NFeB2BFin>data</NFeB2BFin>", None).unwrap_err();
assert!(matches!(err, FiscalError::XmlParsing(_)));
}
#[test]
fn attach_b2b_no_b2b_tag() {
let err = attach_b2b("<nfeProc><NFe/></nfeProc>", "<other>data</other>", None).unwrap_err();
assert!(matches!(err, FiscalError::XmlParsing(_)));
}
#[test]
fn attach_b2b_extract_failure() {
let err = attach_b2b("<nfeProc><NFe/>", "<NFeB2BFin>data</NFeB2BFin>", None).unwrap_err();
assert!(matches!(err, FiscalError::XmlParsing(_)));
}
#[test]
fn attach_b2b_success() {
let result = attach_b2b(
"<nfeProc><NFe/><protNFe/></nfeProc>",
"<NFeB2BFin><tag>data</tag></NFeB2BFin>",
None,
)
.unwrap();
assert!(result.contains("<nfeProcB2B>"));
assert!(result.contains("<nfeProc>"));
assert!(result.contains("<NFeB2BFin>"));
}
#[test]
fn attach_b2b_custom_tag() {
let result = attach_b2b(
"<nfeProc><NFe/><protNFe/></nfeProc>",
"<CustomB2B><tag>data</tag></CustomB2B>",
Some("CustomB2B"),
)
.unwrap();
assert!(result.contains("<CustomB2B>"));
}
#[test]
fn attach_b2b_strips_newlines() {
let nfe_proc = "<nfeProc versao=\"4.00\">\n<NFe/>\n<protNFe/>\n</nfeProc>";
let b2b = "<NFeB2BFin>\n<data>test</data>\n</NFeB2BFin>";
let result = attach_b2b(nfe_proc, b2b, None).unwrap();
assert!(!result.contains('\n'), "Result should not contain newlines");
assert!(
!result.contains('\r'),
"Result should not contain carriage returns"
);
assert!(result.contains("<nfeProcB2B>"));
assert!(result.contains("<NFeB2BFin>"));
}
#[test]
fn attach_b2b_strips_carriage_returns() {
let nfe_proc = "<nfeProc versao=\"4.00\">\r\n<NFe/>\r\n</nfeProc>";
let b2b = "<NFeB2BFin><data>test</data></NFeB2BFin>";
let result = attach_b2b(nfe_proc, b2b, None).unwrap();
assert!(!result.contains('\r'));
assert!(!result.contains('\n'));
}
#[test]
fn attach_b2b_extract_tag_coverage() {
let nfe_proc = concat!(
r#"<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">"#,
r#"<NFe><infNFe/></NFe><protNFe><infProt/></protNFe>"#,
r#"</nfeProc>"#
);
let b2b = r#"<NFeB2BFin versao="1.00"><dados>value</dados></NFeB2BFin>"#;
let result = attach_b2b(nfe_proc, b2b, None).unwrap();
assert!(result.contains("<nfeProcB2B>"));
assert!(result.contains("<dados>value</dados>"));
}
}