mod parser;
mod xml;
use crate::error::{DfeError, Result};
use crate::interno::cert::{DigestValue, RawPubKey, Sign};
use crate::interno::cleaner::Strings;
use crate::interno::cnpj_cpf::sanitize_cnpj;
use crate::interno::dates::get_current_year;
use crate::interno::transporte::{MtlsTransport, SoapTransport};
use crate::interno::ws::nfe_inutilizacao;
use crate::tipos::inutilizacao::Response;
pub struct InutilizacaoBuilder {
cert_path: Option<String>,
cert_pass: Option<String>,
tp_amb: Option<u8>,
uf: Option<String>,
cnpj: Option<String>,
mod_: Option<u32>,
serie: Option<u32>,
n_ini: Option<u64>,
n_fin: Option<u64>,
ano: Option<String>,
justificativa: Option<String>,
}
impl InutilizacaoBuilder {
pub fn new() -> Self {
Self {
cert_path: None,
cert_pass: None,
tp_amb: None,
uf: None,
cnpj: None,
mod_: None,
serie: None,
n_ini: None,
n_fin: None,
ano: None,
justificativa: None,
}
}
pub fn cert(mut self, path: &str, pass: &str) -> Self {
self.cert_path = Some(path.to_string());
self.cert_pass = Some(pass.to_string());
self
}
pub fn tp_amb(mut self, v: u8) -> Self {
self.tp_amb = Some(v);
self
}
pub fn uf(mut self, v: &str) -> Self {
self.uf = Some(v.to_string());
self
}
pub fn cnpj(mut self, v: &str) -> Self {
self.cnpj = Some(v.to_string());
self
}
pub fn mod_(mut self, v: u32) -> Self {
self.mod_ = Some(v);
self
}
pub fn serie(mut self, v: u32) -> Self {
self.serie = Some(v);
self
}
pub fn faixa(mut self, n_ini: u64, n_fin: u64) -> Self {
self.n_ini = Some(n_ini);
self.n_fin = Some(n_fin);
self
}
pub fn ano(mut self, v: &str) -> Self {
self.ano = Some(v.to_string());
self
}
pub fn justificativa(mut self, v: &str) -> Self {
self.justificativa = Some(v.to_string());
self
}
pub async fn send(self) -> Result<Response> {
let cert_path = self
.cert_path
.ok_or_else(|| DfeError::Configuracao("cert_path não informado".to_string()))?;
let cert_pass = self
.cert_pass
.ok_or_else(|| DfeError::Configuracao("cert_pass não informado".to_string()))?;
let tp_amb = self
.tp_amb
.ok_or_else(|| DfeError::Configuracao("tp_amb não informado".to_string()))?;
let uf = self
.uf
.ok_or_else(|| DfeError::Validacao("uf não informada".to_string()))?;
let cnpj = self
.cnpj
.ok_or_else(|| DfeError::Validacao("cnpj não informado".to_string()))?;
let mod_ = self
.mod_
.ok_or_else(|| DfeError::Validacao("mod_ não informado".to_string()))?;
let serie = self
.serie
.ok_or_else(|| DfeError::Validacao("serie não informada".to_string()))?;
let n_ini = self
.n_ini
.ok_or_else(|| DfeError::Validacao("faixa não informada".to_string()))?;
let n_fin = self
.n_fin
.ok_or_else(|| DfeError::Validacao("faixa não informada".to_string()))?;
let justificativa = self
.justificativa
.ok_or_else(|| DfeError::Validacao("justificativa não informada".to_string()))?;
let uf = uf.trim().to_uppercase();
let c_uf = crate::interno::uf::codigo_por_sigla(&uf)?;
if tp_amb != 1 && tp_amb != 2 {
return Err(DfeError::Validacao(
"tp_amb deve ser 1 (producao) ou 2 (homologacao)".to_string(),
));
}
if mod_ != 55 && mod_ != 65 {
return Err(DfeError::Validacao(
"mod_ deve ser 55 (NF-e) ou 65 (NFC-e)".to_string(),
));
}
let cnpj = sanitize_cnpj(&cnpj);
if cnpj.len() != 14 {
return Err(DfeError::Validacao(
"cnpj do emitente deve ter 14 posições".to_string(),
));
}
if serie > 999 {
return Err(DfeError::Validacao(
"serie deve estar entre 0 e 999".to_string(),
));
}
if n_ini == 0 || n_fin == 0 {
return Err(DfeError::Validacao(
"faixa de numeração deve começar em 1".to_string(),
));
}
if n_fin < n_ini {
return Err(DfeError::Validacao(
"número final da faixa não pode ser menor que o inicial".to_string(),
));
}
if n_fin > 999_999_999 {
return Err(DfeError::Validacao(
"número da faixa excede 9 dígitos".to_string(),
));
}
let justificativa = justificativa.trim().to_string();
let len = justificativa.chars().count();
if !(15..=255).contains(&len) {
return Err(DfeError::Validacao(
"justificativa deve ter entre 15 e 255 caracteres".to_string(),
));
}
let ano = match self.ano {
Some(a) => {
let a = a.trim().to_string();
if a.len() != 2 || !a.chars().all(|c| c.is_ascii_digit()) {
return Err(DfeError::Validacao(
"ano deve ter 2 dígitos (ex.: \"26\")".to_string(),
));
}
a
}
None => get_current_year(2),
};
inutilizar(
cert_path,
cert_pass,
xml::Pedido {
tp_amb,
c_uf,
ano: &ano,
cnpj: &cnpj,
mod_,
serie,
n_ini,
n_fin,
justificativa: &justificativa,
},
&uf,
)
.await
}
}
impl Default for InutilizacaoBuilder {
fn default() -> Self {
Self::new()
}
}
async fn inutilizar(
cert_path: String,
cert_pass: String,
pedido: xml::Pedido<'_>,
uf: &str,
) -> Result<Response> {
let url = nfe_inutilizacao(pedido.tp_amb, uf, pedido.mod_, false)?;
let id = xml::inf_inut_id(&pedido);
let inf_inut = Strings::clear_xml_string(&xml::inf_inut_xml(&pedido)?);
let digest = DigestValue::sha1(&inf_inut)?;
let signed_info = crate::interno::assinatura::signed_info_xml(&format!("#{id}"), &digest);
let signature_base64 = Sign::xml_string(&signed_info, &cert_path, &cert_pass).await?;
let x509_cert = RawPubKey::get_from_file(&cert_path, &cert_pass).await?;
let signature =
crate::interno::assinatura::signature_xml(&signed_info, &signature_base64, &x509_cert);
let envelope = Strings::clear_xml_string(&xml::inut_nfe_envelope(&inf_inut, &signature));
let send_xml = envelope.clone();
let receive_xml = MtlsTransport::new(&cert_path, &cert_pass)
.send_soap(url, envelope)
.await?;
let parsed = parser::parse_ret_inut_nfe(&receive_xml)?;
Ok(Response {
response: parsed,
send_xml,
receive_xml,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn builder_valido() -> InutilizacaoBuilder {
InutilizacaoBuilder::new()
.cert("inexistente.pfx", "x")
.tp_amb(2)
.uf("SP")
.cnpj("11222333000181")
.mod_(65)
.serie(1)
.faixa(325, 325)
.justificativa("Numeracao pulada por falha de comunicacao na emissao")
}
#[tokio::test]
async fn rejeita_cert_ausente() {
let err = InutilizacaoBuilder::new()
.tp_amb(2)
.uf("SP")
.cnpj("11222333000181")
.mod_(65)
.serie(1)
.faixa(1, 1)
.justificativa("Justificativa suficientemente longa")
.send()
.await
.unwrap_err();
assert!(matches!(err, DfeError::Configuracao(_)));
}
#[tokio::test]
async fn rejeita_uf_invalida() {
let err = builder_valido().uf("XX").send().await.unwrap_err();
assert!(matches!(err, DfeError::Validacao(_)));
}
#[tokio::test]
async fn rejeita_modelo_invalido() {
let err = builder_valido().mod_(57).send().await.unwrap_err();
assert!(matches!(err, DfeError::Validacao(_)));
}
#[tokio::test]
async fn rejeita_faixa_invertida() {
let err = builder_valido().faixa(330, 325).send().await.unwrap_err();
assert!(matches!(err, DfeError::Validacao(_)));
}
#[tokio::test]
async fn rejeita_faixa_comecando_em_zero() {
let err = builder_valido().faixa(0, 0).send().await.unwrap_err();
assert!(matches!(err, DfeError::Validacao(_)));
}
#[tokio::test]
async fn rejeita_justificativa_curta() {
let err = builder_valido()
.justificativa("Curta demais")
.send()
.await
.unwrap_err();
assert!(matches!(err, DfeError::Validacao(_)));
}
#[tokio::test]
async fn rejeita_cnpj_incompleto() {
let err = builder_valido().cnpj("1122233300").send().await.unwrap_err();
assert!(matches!(err, DfeError::Validacao(_)));
}
#[tokio::test]
async fn rejeita_ano_fora_de_2_digitos() {
let err = builder_valido().ano("2026").send().await.unwrap_err();
assert!(matches!(err, DfeError::Validacao(_)));
}
#[tokio::test]
async fn rejeita_tp_amb_invalido() {
let err = builder_valido().tp_amb(9).send().await.unwrap_err();
assert!(matches!(err, DfeError::Validacao(_)));
}
#[test]
fn todas_as_ufs_tem_endpoint_de_inutilizacao() {
for (_, sigla) in crate::interno::uf::UFS {
for amb in [1u8, 2] {
for modelo in [55u32, 65] {
nfe_inutilizacao(amb, sigla, modelo, false).unwrap_or_else(|e| {
panic!("sem endpoint de inutilização p/ {sigla} amb {amb} mod {modelo}: {e:?}")
});
}
}
}
}
}