use crate::FiscalError;
use crate::newtypes::IbgeCode;
use crate::types::{AccessKeyParams, InvoiceModel};
use crate::xml_builder::access_key::build_access_key;
use crate::xml_utils::extract_xml_tag_value;
use super::manager::Contingency;
pub fn adjust_nfe_contingency(xml: &str, contingency: &Contingency) -> Result<String, FiscalError> {
if contingency.contingency_type.is_none() {
return Ok(xml.to_string());
}
let mut xml = remove_signature(xml);
let model = extract_xml_tag_value(&xml, "mod").unwrap_or_default();
if model == "65" {
return Err(FiscalError::Contingency(
"The XML belongs to a model 65 document (NFC-e), incorrect for SVCAN or SVCRS contingency.".to_string(),
));
}
let current_tp_emis = extract_xml_tag_value(&xml, "tpEmis").unwrap_or_default();
if current_tp_emis != "1" {
return Ok(xml);
}
let c_uf = extract_xml_tag_value(&xml, "cUF").unwrap_or_default();
let c_nf = extract_xml_tag_value(&xml, "cNF").unwrap_or_default();
let n_nf = extract_xml_tag_value(&xml, "nNF").unwrap_or_default();
let serie = extract_xml_tag_value(&xml, "serie").unwrap_or_default();
let dh_emi = extract_xml_tag_value(&xml, "dhEmi").unwrap_or_default();
let emit_doc = extract_emitter_doc(&xml);
let (year, month) = parse_year_month(&dh_emi);
let tz_offset = extract_tz_offset(&dh_emi);
let dth_cont = format_timestamp_with_offset(contingency.timestamp, &tz_offset);
let reason = contingency.reason.as_deref().unwrap_or("").trim();
let tp_emis = contingency.emission_type();
xml = xml.replacen(
&format!("<tpEmis>{current_tp_emis}</tpEmis>"),
&format!("<tpEmis>{tp_emis}</tpEmis>"),
1,
);
if xml.contains("<dhCont>") {
let re_start = xml.find("<dhCont>").unwrap();
let re_end = xml.find("</dhCont>").unwrap() + "</dhCont>".len();
xml = format!(
"{}<dhCont>{dth_cont}</dhCont>{}",
&xml[..re_start],
&xml[re_end..]
);
} else if xml.contains("<NFref>") {
xml = xml.replacen("<NFref>", &format!("<dhCont>{dth_cont}</dhCont><NFref>"), 1);
} else {
xml = xml.replacen("</ide>", &format!("<dhCont>{dth_cont}</dhCont></ide>"), 1);
}
if xml.contains("<xJust>") {
let re_start = xml.find("<xJust>").unwrap();
let re_end = xml.find("</xJust>").unwrap() + "</xJust>".len();
xml = format!(
"{}<xJust>{reason}</xJust>{}",
&xml[..re_start],
&xml[re_end..]
);
} else if xml.contains("<NFref>") {
xml = xml.replacen("<NFref>", &format!("<xJust>{reason}</xJust><NFref>"), 1);
} else {
xml = xml.replacen("</ide>", &format!("<xJust>{reason}</xJust></ide>"), 1);
}
let model_enum = match model.as_str() {
"65" => InvoiceModel::Nfce,
_ => InvoiceModel::Nfe,
};
let emission_type_enum = contingency.emission_type_enum();
let new_key = build_access_key(&AccessKeyParams {
state_code: IbgeCode(c_uf),
year_month: format!("{year}{month}"),
tax_id: emit_doc,
model: model_enum,
series: serie.parse().unwrap_or(0),
number: n_nf.parse().unwrap_or(0),
emission_type: emission_type_enum,
numeric_code: c_nf,
})?;
let new_cdv = &new_key[new_key.len() - 1..];
if let Some(start) = xml.find("<cDV>") {
if let Some(end) = xml[start..].find("</cDV>") {
let full_end = start + end + "</cDV>".len();
xml = format!("{}<cDV>{new_cdv}</cDV>{}", &xml[..start], &xml[full_end..]);
}
}
if let Some(id_start) = xml.find("Id=\"NFe") {
let after_nfe = id_start + 7; if xml.len() >= after_nfe + 44 {
let id_end = after_nfe + 44;
xml = format!("{}NFe{new_key}{}", &xml[..after_nfe], &xml[id_end..]);
}
}
Ok(xml)
}
fn remove_signature(xml: &str) -> String {
if let Some(start) = xml.find("<Signature") {
if let Some(end) = xml.find("</Signature>") {
let full_end = end + "</Signature>".len();
return format!("{}{}", xml[..start].trim_end(), &xml[full_end..])
.trim()
.to_string();
}
}
xml.to_string()
}
pub(super) fn extract_emitter_doc(xml: &str) -> String {
if let Some(emit_start) = xml.find("<emit>") {
if let Some(emit_end) = xml.find("</emit>") {
let emit_block = &xml[emit_start..emit_end];
if let Some(cnpj) = extract_inner(emit_block, "CNPJ") {
return cnpj;
}
if let Some(cpf) = extract_inner(emit_block, "CPF") {
return cpf;
}
}
}
String::new()
}
fn extract_inner(xml: &str, tag: &str) -> Option<String> {
let open = format!("<{tag}>");
let close = format!("</{tag}>");
let start = xml.find(&open)? + open.len();
let end = xml[start..].find(&close)? + start;
Some(xml[start..end].to_string())
}
pub(super) fn parse_year_month(dh_emi: &str) -> (String, String) {
if dh_emi.len() >= 7 {
let year = &dh_emi[2..4]; let month = &dh_emi[5..7]; (year.to_string(), month.to_string())
} else {
("00".to_string(), "00".to_string())
}
}
pub(super) fn extract_tz_offset(dh_emi: &str) -> String {
if dh_emi.len() >= 6 {
let tail = &dh_emi[dh_emi.len() - 6..];
if (tail.starts_with('+') || tail.starts_with('-')) && tail.as_bytes()[3] == b':' {
return tail.to_string();
}
}
"-03:00".to_string()
}
pub(super) fn format_timestamp_with_offset(timestamp: u64, offset: &str) -> String {
let offset_seconds = parse_offset_seconds(offset);
if let Some(fo) = chrono::FixedOffset::east_opt(offset_seconds) {
if let Some(dt) = chrono::DateTime::from_timestamp(timestamp as i64, 0) {
let local = dt.with_timezone(&fo);
return local.format("%Y-%m-%dT%H:%M:%S").to_string() + offset;
}
}
format!("1970-01-01T00:00:00{offset}")
}
fn parse_offset_seconds(offset: &str) -> i32 {
if offset.len() < 6 {
return 0;
}
let sign: i32 = if offset.starts_with('-') { -1 } else { 1 };
let hours: i32 = offset[1..3].parse().unwrap_or(0);
let minutes: i32 = offset[4..6].parse().unwrap_or(0);
sign * (hours * 3600 + minutes * 60)
}