mod parser;
mod xml;
use crate::error::{DfeError, Result};
use crate::interno::chave_acesso::ChaveAcesso;
use crate::interno::transporte::{MtlsTransport, SoapTransport};
use crate::interno::ws::nfe_consulta_protocolo;
use crate::tipos::consulta_situacao::Response;
pub struct ConsultaSituacaoBuilder {
cert_path: Option<String>,
cert_pass: Option<String>,
tp_amb: Option<u8>,
chave: Option<String>,
}
impl ConsultaSituacaoBuilder {
pub fn new() -> Self {
Self {
cert_path: None,
cert_pass: None,
tp_amb: None,
chave: 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 chave(mut self, v: &str) -> Self {
self.chave = 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 chave = self
.chave
.ok_or_else(|| DfeError::Validacao("chave não informada".to_string()))?;
if tp_amb != 1 && tp_amb != 2 {
return Err(DfeError::Validacao(
"tp_amb deve ser 1 (producao) ou 2 (homologacao)".to_string(),
));
}
if chave.len() != 44 {
return Err(DfeError::Validacao("chave deve ter 44 dígitos".to_string()));
}
consultar(cert_path, cert_pass, tp_amb, chave).await
}
}
impl Default for ConsultaSituacaoBuilder {
fn default() -> Self {
Self::new()
}
}
fn resolver_url(tp_amb: u8, chave: &str) -> Result<&'static str> {
let comp = ChaveAcesso::extract_composition(chave)?;
let uf = crate::interno::uf::sigla_por_codigo(&comp.uf_code)?;
let modelo: u32 = comp
.modelo
.parse()
.map_err(|_| DfeError::Xml(format!("modelo inválido na chave: {}", comp.modelo)))?;
nfe_consulta_protocolo(tp_amb, uf, modelo, false)
}
async fn consultar(cert_path: String, cert_pass: String, tp_amb: u8, chave: String) -> Result<Response> {
let url = resolver_url(tp_amb, &chave)?;
let request_xml =
xml::cons_sit_nfe_request_xml(tp_amb, &chave).map_err(DfeError::Validacao)?;
let send_xml = request_xml.clone();
let response = MtlsTransport::new(&cert_path, &cert_pass)
.send_soap(url, request_xml)
.await?;
let parsed = parser::parse_ret_cons_sit_nfe(&response)?;
Ok(Response {
response: parsed,
send_xml,
receive_xml: response,
})
}
#[cfg(test)]
mod tests {
use super::*;
const CHAVE: &str = "35000000000000000000550010000000001000000001";
fn chave_com(uf_code: &str, modelo: &str) -> String {
let c = format!("{uf_code}260811222333000181{modelo}0010000000011000000011");
assert_eq!(c.len(), 44, "chave sintética malformada: {c}");
c
}
fn builder_valido() -> ConsultaSituacaoBuilder {
ConsultaSituacaoBuilder::new()
.cert("inexistente.pfx", "x")
.tp_amb(2)
.chave(CHAVE)
}
#[tokio::test]
async fn rejeita_cert_path_ausente() {
let err = ConsultaSituacaoBuilder::new()
.tp_amb(2)
.chave(CHAVE)
.send()
.await
.unwrap_err();
assert!(matches!(err, DfeError::Configuracao(_)));
}
#[tokio::test]
async fn rejeita_tp_amb_ausente() {
let err = ConsultaSituacaoBuilder::new()
.cert("inexistente.pfx", "x")
.chave(CHAVE)
.send()
.await
.unwrap_err();
assert!(matches!(err, DfeError::Configuracao(_)));
}
#[tokio::test]
async fn rejeita_tp_amb_invalido() {
let err = builder_valido().tp_amb(9).send().await.unwrap_err();
assert!(matches!(err, DfeError::Validacao(_)));
}
#[tokio::test]
async fn rejeita_chave_ausente() {
let err = ConsultaSituacaoBuilder::new()
.cert("inexistente.pfx", "x")
.tp_amb(2)
.send()
.await
.unwrap_err();
assert!(matches!(err, DfeError::Validacao(_)));
}
#[tokio::test]
async fn rejeita_chave_tamanho_invalido() {
let err = builder_valido().chave("35065").send().await.unwrap_err();
assert!(matches!(err, DfeError::Validacao(_)));
}
#[test]
fn deriva_url_nfce_sp_nao_usa_host_da_nfe() {
let chave = chave_com("35", "65");
assert_eq!(
resolver_url(1, &chave).unwrap(),
"https://nfce.fazenda.sp.gov.br/ws/NFeConsultaProtocolo4.asmx"
);
assert_eq!(
resolver_url(2, &chave).unwrap(),
"https://homologacao.nfce.fazenda.sp.gov.br/ws/NFeConsultaProtocolo4.asmx"
);
}
#[test]
fn deriva_url_nfe_sp() {
let chave = chave_com("35", "55");
assert_eq!(
resolver_url(1, &chave).unwrap(),
"https://nfe.fazenda.sp.gov.br/ws/nfeconsultaprotocolo4.asmx"
);
assert_eq!(
resolver_url(2, &chave).unwrap(),
"https://homologacao.nfe.fazenda.sp.gov.br/ws/nfeconsultaprotocolo4.asmx"
);
}
#[test]
fn deriva_url_por_modelo_via_svrs_rj() {
assert_eq!(
resolver_url(1, &chave_com("33", "65")).unwrap(),
"https://nfce.svrs.rs.gov.br/ws/NfeConsulta/NfeConsulta4.asmx"
);
assert_eq!(
resolver_url(1, &chave_com("33", "55")).unwrap(),
"https://nfe.svrs.rs.gov.br/ws/NfeConsulta/NfeConsulta4.asmx"
);
}
#[test]
fn go_compartilha_url_entre_modelos() {
for amb in [1u8, 2] {
assert_eq!(
resolver_url(amb, &chave_com("52", "55")).unwrap(),
resolver_url(amb, &chave_com("52", "65")).unwrap(),
);
}
}
#[test]
fn todas_as_ufs_resolvem_nfce_com_url_propria() {
for (codigo, sigla) in crate::interno::uf::UFS {
for amb in [1u8, 2] {
let url_65 = resolver_url(amb, &chave_com(codigo, "65"))
.unwrap_or_else(|e| panic!("sem endpoint NFC-e p/ {sigla} amb {amb}: {e:?}"));
let url_55 = resolver_url(amb, &chave_com(codigo, "55"))
.unwrap_or_else(|e| panic!("sem endpoint NF-e p/ {sigla} amb {amb}: {e:?}"));
if *sigla != "GO" {
assert_ne!(url_65, url_55, "{sigla} amb {amb} usa a mesma URL p/ 55 e 65");
}
}
}
}
#[test]
fn rejeita_uf_desconhecida_na_chave() {
let err = resolver_url(2, &chave_com("99", "65")).unwrap_err();
assert!(matches!(err, DfeError::Validacao(_)));
}
#[test]
fn rejeita_modelo_sem_endpoint() {
let err = resolver_url(2, &chave_com("35", "57")).unwrap_err();
assert!(matches!(err, DfeError::Webservice(_)));
}
}