use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum TipoConsultaDistribuicao {
ConsultaNsu,
ConsultaChave,
DistribuicaoNsu,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DadosConsultaDistribuicao {
pub documento: String,
pub tipo_consulta: TipoConsultaDistribuicao,
pub nsu: Option<String>,
pub ultimo_nsu: Option<String>,
pub chave_acesso: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResultadoDistribuicao {
pub sucesso: bool,
pub codigo_status: u16,
pub descricao_status: String,
pub ultimo_nsu: Option<String>,
pub max_nsu: Option<String>,
pub documentos: Vec<DocumentoDistribuido>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentoDistribuido {
pub nsu: String,
pub tipo: TipoDocumentoDistribuido,
pub schema: String,
pub conteudo: String,
pub chave_acesso: Option<String>,
pub cnpj_emitente: Option<String>,
pub data_emissao: Option<String>,
pub valor_total: Option<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum TipoDocumentoDistribuido {
ResumoNfe,
NfeCompleta,
ResumoEvento,
EventoCompleto,
}
impl DadosConsultaDistribuicao {
pub fn validar(&self) -> Result<(), Vec<String>> {
let mut erros = Vec::new();
let doc_len = self.documento.len();
if doc_len != 11 && doc_len != 14 {
erros.push(format!(
"Documento inválido: {} dígitos (esperado 11 para CPF ou 14 para CNPJ)",
doc_len
));
}
if !self.documento.chars().all(|c| c.is_ascii_digit()) {
erros.push("Documento deve conter apenas números".to_string());
}
match self.tipo_consulta {
TipoConsultaDistribuicao::ConsultaNsu => {
if self.nsu.is_none() {
erros.push("NSU é obrigatório para consulta por NSU".to_string());
}
}
TipoConsultaDistribuicao::ConsultaChave => {
match &self.chave_acesso {
None => {
erros.push("Chave de acesso é obrigatória para consulta por chave".to_string());
}
Some(chave) if chave.len() != 44 => {
erros.push(format!(
"Chave de acesso inválida: {} dígitos (esperado 44)",
chave.len()
));
}
_ => {}
}
}
TipoConsultaDistribuicao::DistribuicaoNsu => {
}
}
if erros.is_empty() {
Ok(())
} else {
Err(erros)
}
}
}
pub fn gerar_xml_distribuicao(dados: &DadosConsultaDistribuicao, codigo_uf: u8, ambiente: u8) -> String {
let is_cnpj = dados.documento.len() == 14;
let doc_tag = if is_cnpj { "CNPJ" } else { "CPF" };
let consulta = match dados.tipo_consulta {
TipoConsultaDistribuicao::ConsultaNsu => {
format!(
r#"<consNSU>
<NSU>{}</NSU>
</consNSU>"#,
dados.nsu.as_deref().unwrap_or("0")
)
}
TipoConsultaDistribuicao::ConsultaChave => {
format!(
r#"<consChNFe>
<chNFe>{}</chNFe>
</consChNFe>"#,
dados.chave_acesso.as_deref().unwrap_or("")
)
}
TipoConsultaDistribuicao::DistribuicaoNsu => {
format!(
r#"<distNSU>
<ultNSU>{}</ultNSU>
</distNSU>"#,
dados.ultimo_nsu.as_deref().unwrap_or("0").trim_start_matches('0')
)
}
};
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<distDFeInt xmlns="http://www.portalfiscal.inf.br/nfe" versao="1.01">
<tpAmb>{ambiente}</tpAmb>
<cUFAutor>{codigo_uf:02}</cUFAutor>
<{doc_tag}>{documento}</{doc_tag}>
{consulta}
</distDFeInt>"#,
ambiente = ambiente,
codigo_uf = codigo_uf,
doc_tag = doc_tag,
documento = dados.documento,
consulta = consulta,
)
}
pub fn parsear_resposta_distribuicao(xml: &str) -> ResultadoDistribuicao {
let codigo_status = extrair_tag(xml, "cStat")
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(0);
let descricao = extrair_tag(xml, "xMotivo")
.unwrap_or_else(|| "Erro desconhecido".to_string());
let ultimo_nsu = extrair_tag(xml, "ultNSU");
let max_nsu = extrair_tag(xml, "maxNSU");
let documentos = extrair_documentos(xml);
let sucesso = codigo_status == 137 || codigo_status == 138;
ResultadoDistribuicao {
sucesso,
codigo_status,
descricao_status: descricao,
ultimo_nsu,
max_nsu,
documentos,
}
}
fn extrair_documentos(xml: &str) -> Vec<DocumentoDistribuido> {
let mut docs = Vec::new();
let mut pos = 0;
while let Some(start) = xml[pos..].find("<docZip") {
let start = pos + start;
if let Some(end) = xml[start..].find("</docZip>") {
let doc_xml = &xml[start..start + end + 9];
let nsu = extrair_atributo_str(doc_xml, "NSU")
.unwrap_or_default();
let schema = extrair_atributo_str(doc_xml, "schema")
.unwrap_or_default();
let tipo = if schema.contains("resNFe") {
TipoDocumentoDistribuido::ResumoNfe
} else if schema.contains("procNFe") || schema.contains("nfeProc") {
TipoDocumentoDistribuido::NfeCompleta
} else if schema.contains("resEvento") {
TipoDocumentoDistribuido::ResumoEvento
} else {
TipoDocumentoDistribuido::EventoCompleto
};
let conteudo = extrair_conteudo_tag(doc_xml, "docZip")
.unwrap_or_default();
docs.push(DocumentoDistribuido {
nsu,
tipo,
schema,
conteudo,
chave_acesso: None, cnpj_emitente: None,
data_emissao: None,
valor_total: None,
});
pos = start + end + 9;
} else {
break;
}
}
docs
}
fn extrair_tag(xml: &str, tag: &str) -> Option<String> {
let inicio = format!("<{}>", tag);
let fim = format!("</{}>", tag);
if let Some(start) = xml.find(&inicio) {
let start = start + inicio.len();
if let Some(end) = xml[start..].find(&fim) {
return Some(xml[start..start + end].to_string());
}
}
None
}
fn extrair_atributo_str(xml: &str, attr: &str) -> Option<String> {
let pattern = format!("{}=\"", attr);
if let Some(start) = xml.find(&pattern) {
let start = start + pattern.len();
if let Some(end) = xml[start..].find('"') {
return Some(xml[start..start + end].to_string());
}
}
None
}
fn extrair_conteudo_tag(xml: &str, tag: &str) -> Option<String> {
if let Some(start) = xml.find('>') {
let start = start + 1;
let fim = format!("</{}>", tag);
if let Some(end) = xml[start..].find(&fim) {
return Some(xml[start..start + end].to_string());
}
}
None
}