use crate::FiscalError;
use crate::status_codes::sefaz_status;
use crate::xml_utils::extract_xml_tag_value;
use super::helpers::{DEFAULT_VERSION, extract_attribute, extract_tag, join_xml};
pub fn attach_inutilizacao(request_xml: &str, response_xml: &str) -> Result<String, FiscalError> {
if request_xml.is_empty() {
return Err(FiscalError::XmlParsing(
"Inutilizacao request XML is empty".into(),
));
}
if response_xml.is_empty() {
return Err(FiscalError::XmlParsing(
"Inutilizacao response XML is empty".into(),
));
}
let inut_content = extract_tag(request_xml, "inutNFe").ok_or_else(|| {
FiscalError::XmlParsing("Could not find <inutNFe> tag in request XML".into())
})?;
let ret_inut_content = extract_tag(response_xml, "retInutNFe").ok_or_else(|| {
FiscalError::XmlParsing("Could not find <retInutNFe> tag in response XML".into())
})?;
let c_stat = extract_xml_tag_value(&ret_inut_content, "cStat").unwrap_or_default();
if c_stat != sefaz_status::VOIDED {
let x_motivo = extract_xml_tag_value(&ret_inut_content, "xMotivo").unwrap_or_default();
return Err(FiscalError::SefazRejection {
code: c_stat,
message: x_motivo,
});
}
let version = extract_attribute(&inut_content, "inutNFe", "versao")
.unwrap_or_else(|| DEFAULT_VERSION.to_string());
let ret_version = extract_attribute(&ret_inut_content, "retInutNFe", "versao")
.unwrap_or_else(|| DEFAULT_VERSION.to_string());
let cpf_or_cnpj_tag = if extract_xml_tag_value(&inut_content, "CNPJ").is_some() {
"CNPJ"
} else {
"CPF"
};
let field_pairs: &[(&str, &str, &str)] = &[("versao", &version, &ret_version)];
for &(name, req_val, ret_val) in field_pairs {
if req_val != ret_val {
return Err(FiscalError::XmlParsing(format!(
"Inutilização: {name} diverge entre request ({req_val}) e response ({ret_val})"
)));
}
}
let tag_pairs: &[&str] = &[
"tpAmb",
"cUF",
"ano",
cpf_or_cnpj_tag,
"mod",
"serie",
"nNFIni",
"nNFFin",
];
for tag_name in tag_pairs {
let req_val = extract_xml_tag_value(&inut_content, tag_name).unwrap_or_default();
let ret_val = extract_xml_tag_value(&ret_inut_content, tag_name).unwrap_or_default();
if req_val != ret_val {
return Err(FiscalError::XmlParsing(format!(
"Inutilização: <{tag_name}> diverge entre request ({req_val}) e response ({ret_val})"
)));
}
}
Ok(join_xml(
&inut_content,
&ret_inut_content,
"ProcInutNFe",
&version,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attach_inutilizacao_empty_request() {
let err = attach_inutilizacao("", "<retInutNFe/>").unwrap_err();
assert!(matches!(err, FiscalError::XmlParsing(_)));
}
#[test]
fn attach_inutilizacao_empty_response() {
let err = attach_inutilizacao("<inutNFe/>", "").unwrap_err();
assert!(matches!(err, FiscalError::XmlParsing(_)));
}
#[test]
fn attach_inutilizacao_missing_inut_tag() {
let err = attach_inutilizacao("<other/>", "<retInutNFe><cStat>102</cStat></retInutNFe>")
.unwrap_err();
assert!(matches!(err, FiscalError::XmlParsing(_)));
}
#[test]
fn attach_inutilizacao_missing_ret_tag() {
let err = attach_inutilizacao(r#"<inutNFe versao="4.00"><data/></inutNFe>"#, "<other/>")
.unwrap_err();
assert!(matches!(err, FiscalError::XmlParsing(_)));
}
#[test]
fn attach_inutilizacao_rejected_status() {
let err = attach_inutilizacao(
r#"<inutNFe versao="4.00"><data/></inutNFe>"#,
r#"<retInutNFe><cStat>999</cStat><xMotivo>Erro</xMotivo></retInutNFe>"#,
)
.unwrap_err();
assert!(matches!(err, FiscalError::SefazRejection { .. }));
}
#[test]
fn attach_inutilizacao_success() {
let result = attach_inutilizacao(
r#"<inutNFe versao="4.00"><infInut/></inutNFe>"#,
r#"<retInutNFe><cStat>102</cStat><xMotivo>Inutilizacao de numero homologado</xMotivo></retInutNFe>"#,
)
.unwrap();
assert!(result.contains("<ProcInutNFe"));
assert!(result.contains("<inutNFe"));
assert!(result.contains("<retInutNFe>"));
}
#[test]
fn attach_inutilizacao_version_mismatch() {
let request = concat!(
r#"<inutNFe versao="4.00"><infInut>"#,
r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>1</serie>"#,
r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
r#"</infInut></inutNFe>"#
);
let response = concat!(
r#"<retInutNFe versao="3.10"><infInut>"#,
r#"<cStat>102</cStat><xMotivo>Inutilizacao homologada</xMotivo>"#,
r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>1</serie>"#,
r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
r#"</infInut></retInutNFe>"#
);
let err = attach_inutilizacao(request, response).unwrap_err();
match err {
FiscalError::XmlParsing(msg) => {
assert!(
msg.contains("versao"),
"Expected version mismatch error, got: {msg}"
);
}
other => panic!("Expected XmlParsing, got {:?}", other),
}
}
#[test]
fn attach_inutilizacao_tag_value_mismatch() {
let request = concat!(
r#"<inutNFe versao="4.00"><infInut>"#,
r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>1</serie>"#,
r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
r#"</infInut></inutNFe>"#
);
let response = concat!(
r#"<retInutNFe versao="4.00"><infInut>"#,
r#"<cStat>102</cStat><xMotivo>Inutilizacao homologada</xMotivo>"#,
r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>2</serie>"#,
r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
r#"</infInut></retInutNFe>"#
);
let err = attach_inutilizacao(request, response).unwrap_err();
match err {
FiscalError::XmlParsing(msg) => {
assert!(
msg.contains("serie"),
"Expected serie mismatch error, got: {msg}"
);
}
other => panic!("Expected XmlParsing, got {:?}", other),
}
}
}