Skip to main content

nfe_web/sefaz/
webservice.rs

1//! WebServices SEFAZ
2//!
3//! Implementa comunicação SOAP com os WebServices da SEFAZ
4
5use super::consulta::ResultadoConsulta;
6use crate::certificado::{CertificadoA1, AssinadorXml};
7use reqwest::Client;
8use serde::{Deserialize, Serialize};
9
10/// Ambiente de operação
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum AmbienteNfe {
13    Producao = 1,
14    Homologacao = 2,
15}
16
17/// Serviços disponíveis
18#[derive(Debug, Clone, Copy)]
19pub enum ServicoNfe {
20    Autorizacao,
21    RetAutorizacao,
22    ConsultaProtocolo,
23    Inutilizacao,
24    RecepcaoEvento,
25    StatusServico,
26}
27
28/// URLs dos WebServices por UF
29pub struct WebServiceUrls {
30    pub uf: String,
31    pub ambiente: AmbienteNfe,
32}
33
34impl WebServiceUrls {
35    pub fn new(uf: &str, ambiente: AmbienteNfe) -> Self {
36        Self {
37            uf: uf.to_uppercase(),
38            ambiente,
39        }
40    }
41
42    pub fn get_url(&self, servico: ServicoNfe) -> String {
43        let base = self.get_base_url();
44        let service = match servico {
45            ServicoNfe::Autorizacao => "NFeAutorizacao4",
46            ServicoNfe::RetAutorizacao => "NFeRetAutorizacao4",
47            ServicoNfe::ConsultaProtocolo => "NFeConsultaProtocolo4",
48            ServicoNfe::Inutilizacao => "NFeInutilizacao4",
49            ServicoNfe::RecepcaoEvento => "NFeRecepcaoEvento4",
50            ServicoNfe::StatusServico => "NFeStatusServico4",
51        };
52        format!("{}/{}", base, service)
53    }
54
55    fn get_base_url(&self) -> String {
56        let is_prod = self.ambiente == AmbienteNfe::Producao;
57        match self.uf.as_str() {
58            "SP" => if is_prod { "https://nfe.fazenda.sp.gov.br/ws" } else { "https://homologacao.nfe.fazenda.sp.gov.br/ws" },
59            "RS" => if is_prod { "https://nfe.sefazrs.rs.gov.br/ws" } else { "https://nfe-homologacao.sefazrs.rs.gov.br/ws" },
60            "MG" => if is_prod { "https://nfe.fazenda.mg.gov.br/nfe2/services" } else { "https://hnfe.fazenda.mg.gov.br/nfe2/services" },
61            "PR" => if is_prod { "https://nfe.sefa.pr.gov.br/nfe/NFeServices" } else { "https://homologacao.nfe.sefa.pr.gov.br/nfe/NFeServices" },
62            _ => if is_prod { "https://nfe.svrs.rs.gov.br/ws" } else { "https://nfe-homologacao.svrs.rs.gov.br/ws" },
63        }.to_string()
64    }
65}
66
67/// Cliente SEFAZ
68pub struct SefazClient {
69    certificado: CertificadoA1,
70    http_client: Client,
71    ambiente: AmbienteNfe,
72    uf: String,
73}
74
75impl SefazClient {
76    /// Cria novo cliente SEFAZ
77    pub fn new(certificado: CertificadoA1, uf: &str, ambiente: AmbienteNfe) -> Result<Self, String> {
78        // Criar cliente HTTP com certificado PFX
79        let identity = reqwest::Identity::from_pkcs12_der(
80            certificado.pfx_bytes(),
81            certificado.senha()
82        ).map_err(|e| format!("Erro ao criar identidade: {}", e))?;
83
84        let http_client = Client::builder()
85            .identity(identity)
86            .danger_accept_invalid_certs(false)
87            .build()
88            .map_err(|e| format!("Erro ao criar cliente HTTP: {}", e))?;
89
90        Ok(Self {
91            certificado,
92            http_client,
93            ambiente,
94            uf: uf.to_uppercase(),
95        })
96    }
97
98    /// Consulta status do serviço SEFAZ
99    pub async fn status_servico(&self) -> Result<StatusServicoResult, String> {
100        let urls = WebServiceUrls::new(&self.uf, self.ambiente);
101        let url = urls.get_url(ServicoNfe::StatusServico);
102        let envelope = self.criar_envelope_status();
103
104        let response = self.enviar_soap(&url, &envelope, "nfeStatusServicoNF").await?;
105        self.parsear_status_servico(&response)
106    }
107
108    /// Consulta NF-e por chave de acesso
109    pub async fn consultar_nfe(&self, chave_acesso: &str) -> Result<ResultadoConsulta, String> {
110        let urls = WebServiceUrls::new(&self.uf, self.ambiente);
111        let url = urls.get_url(ServicoNfe::ConsultaProtocolo);
112        let envelope = self.criar_envelope_consulta(chave_acesso);
113
114        let response = self.enviar_soap(&url, &envelope, "nfeConsultaNF").await?;
115        self.parsear_consulta(&response)
116    }
117
118    /// Envia NF-e para autorização
119    pub async fn autorizar_nfe(&self, xml_nfe: &str) -> Result<AutorizacaoResult, String> {
120        let urls = WebServiceUrls::new(&self.uf, self.ambiente);
121        let url = urls.get_url(ServicoNfe::Autorizacao);
122
123        // Assinar XML
124        let assinador = AssinadorXml::new(self.certificado.clone());
125        let xml_assinado = assinador.assinar_nfe(xml_nfe)?;
126
127        // Criar lote
128        let lote_id = uuid::Uuid::new_v4().to_string().replace("-", "")[..15].to_string();
129        let xml_lote = self.criar_lote_nfe(&lote_id, &xml_assinado);
130        let envelope = self.criar_envelope_autorizacao(&xml_lote);
131
132        let response = self.enviar_soap(&url, &envelope, "nfeAutorizacaoLote").await?;
133        self.parsear_autorizacao(&response)
134    }
135
136    /// Cancela NF-e
137    pub async fn cancelar_nfe(
138        &self,
139        chave_acesso: &str,
140        protocolo: &str,
141        justificativa: &str,
142    ) -> Result<EventoResult, String> {
143        let urls = WebServiceUrls::new(&self.uf, self.ambiente);
144        let url = urls.get_url(ServicoNfe::RecepcaoEvento);
145
146        let xml_evento = self.criar_evento_cancelamento(chave_acesso, protocolo, justificativa)?;
147        let assinador = AssinadorXml::new(self.certificado.clone());
148        let xml_assinado = assinador.assinar_evento(&xml_evento)?;
149        let envelope = self.criar_envelope_evento(&xml_assinado);
150
151        let response = self.enviar_soap(&url, &envelope, "nfeRecepcaoEvento").await?;
152        self.parsear_evento(&response)
153    }
154
155    /// Envia carta de correção
156    pub async fn carta_correcao(
157        &self,
158        chave_acesso: &str,
159        sequencia: u32,
160        correcao: &str,
161    ) -> Result<EventoResult, String> {
162        let urls = WebServiceUrls::new(&self.uf, self.ambiente);
163        let url = urls.get_url(ServicoNfe::RecepcaoEvento);
164
165        let xml_evento = self.criar_evento_cce(chave_acesso, sequencia, correcao)?;
166        let assinador = AssinadorXml::new(self.certificado.clone());
167        let xml_assinado = assinador.assinar_evento(&xml_evento)?;
168        let envelope = self.criar_envelope_evento(&xml_assinado);
169
170        let response = self.enviar_soap(&url, &envelope, "nfeRecepcaoEvento").await?;
171        self.parsear_evento(&response)
172    }
173
174    async fn enviar_soap(&self, url: &str, envelope: &str, action: &str) -> Result<String, String> {
175        let response = self.http_client
176            .post(url)
177            .header("Content-Type", "application/soap+xml; charset=utf-8")
178            .header("SOAPAction", action)
179            .body(envelope.to_string())
180            .send()
181            .await
182            .map_err(|e| format!("Erro na requisição SOAP: {}", e))?;
183
184        if !response.status().is_success() {
185            return Err(format!("SEFAZ retornou erro HTTP: {}", response.status()));
186        }
187
188        response.text().await.map_err(|e| format!("Erro ao ler resposta: {}", e))
189    }
190
191    fn criar_envelope_status(&self) -> String {
192        let tp_amb = if self.ambiente == AmbienteNfe::Producao { "1" } else { "2" };
193        let c_uf = self.get_codigo_uf();
194        format!(r#"<?xml version="1.0" encoding="UTF-8"?>
195<soap12:Envelope xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
196  <soap12:Body>
197    <nfeDadosMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeStatusServico4">
198      <consStatServ versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">
199        <tpAmb>{tp_amb}</tpAmb>
200        <cUF>{c_uf}</cUF>
201        <xServ>STATUS</xServ>
202      </consStatServ>
203    </nfeDadosMsg>
204  </soap12:Body>
205</soap12:Envelope>"#)
206    }
207
208    fn criar_envelope_consulta(&self, chave_acesso: &str) -> String {
209        let tp_amb = if self.ambiente == AmbienteNfe::Producao { "1" } else { "2" };
210        format!(r#"<?xml version="1.0" encoding="UTF-8"?>
211<soap12:Envelope xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
212  <soap12:Body>
213    <nfeDadosMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeConsultaProtocolo4">
214      <consSitNFe versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">
215        <tpAmb>{tp_amb}</tpAmb>
216        <xServ>CONSULTAR</xServ>
217        <chNFe>{chave_acesso}</chNFe>
218      </consSitNFe>
219    </nfeDadosMsg>
220  </soap12:Body>
221</soap12:Envelope>"#)
222    }
223
224    fn criar_lote_nfe(&self, lote_id: &str, xml_nfe: &str) -> String {
225        format!(r#"<enviNFe versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">
226  <idLote>{lote_id}</idLote>
227  <indSinc>1</indSinc>
228  {xml_nfe}
229</enviNFe>"#)
230    }
231
232    fn criar_envelope_autorizacao(&self, xml_lote: &str) -> String {
233        format!(r#"<?xml version="1.0" encoding="UTF-8"?>
234<soap12:Envelope xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
235  <soap12:Body>
236    <nfeDadosMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeAutorizacao4">
237      {xml_lote}
238    </nfeDadosMsg>
239  </soap12:Body>
240</soap12:Envelope>"#)
241    }
242
243    fn criar_evento_cancelamento(&self, chave_acesso: &str, protocolo: &str, justificativa: &str) -> Result<String, String> {
244        let tp_amb = if self.ambiente == AmbienteNfe::Producao { "1" } else { "2" };
245        let cnpj = self.certificado.info.cnpj.clone().unwrap_or_default();
246        let dh_evento = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S-03:00").to_string();
247        let c_orgao = &chave_acesso[0..2];
248
249        Ok(format!(r#"<evento versao="1.00" xmlns="http://www.portalfiscal.inf.br/nfe">
250  <infEvento Id="ID110111{chave_acesso}01">
251    <cOrgao>{c_orgao}</cOrgao>
252    <tpAmb>{tp_amb}</tpAmb>
253    <CNPJ>{cnpj}</CNPJ>
254    <chNFe>{chave_acesso}</chNFe>
255    <dhEvento>{dh_evento}</dhEvento>
256    <tpEvento>110111</tpEvento>
257    <nSeqEvento>1</nSeqEvento>
258    <verEvento>1.00</verEvento>
259    <detEvento versao="1.00">
260      <descEvento>Cancelamento</descEvento>
261      <nProt>{protocolo}</nProt>
262      <xJust>{justificativa}</xJust>
263    </detEvento>
264  </infEvento>
265</evento>"#))
266    }
267
268    fn criar_evento_cce(&self, chave_acesso: &str, sequencia: u32, correcao: &str) -> Result<String, String> {
269        let tp_amb = if self.ambiente == AmbienteNfe::Producao { "1" } else { "2" };
270        let cnpj = self.certificado.info.cnpj.clone().unwrap_or_default();
271        let dh_evento = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S-03:00").to_string();
272        let c_orgao = &chave_acesso[0..2];
273
274        Ok(format!(r#"<evento versao="1.00" xmlns="http://www.portalfiscal.inf.br/nfe">
275  <infEvento Id="ID110110{chave_acesso}{sequencia:02}">
276    <cOrgao>{c_orgao}</cOrgao>
277    <tpAmb>{tp_amb}</tpAmb>
278    <CNPJ>{cnpj}</CNPJ>
279    <chNFe>{chave_acesso}</chNFe>
280    <dhEvento>{dh_evento}</dhEvento>
281    <tpEvento>110110</tpEvento>
282    <nSeqEvento>{sequencia}</nSeqEvento>
283    <verEvento>1.00</verEvento>
284    <detEvento versao="1.00">
285      <descEvento>Carta de Correcao</descEvento>
286      <xCorrecao>{correcao}</xCorrecao>
287      <xCondUso>A Carta de Correcao e disciplinada pelo paragrafo 1o-A do art. 7o do Convenio S/N, de 15 de dezembro de 1970...</xCondUso>
288    </detEvento>
289  </infEvento>
290</evento>"#))
291    }
292
293    fn criar_envelope_evento(&self, xml_evento: &str) -> String {
294        format!(r#"<?xml version="1.0" encoding="UTF-8"?>
295<soap12:Envelope xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
296  <soap12:Body>
297    <nfeDadosMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeRecepcaoEvento4">
298      <envEvento versao="1.00" xmlns="http://www.portalfiscal.inf.br/nfe">
299        <idLote>1</idLote>
300        {xml_evento}
301      </envEvento>
302    </nfeDadosMsg>
303  </soap12:Body>
304</soap12:Envelope>"#)
305    }
306
307    fn get_codigo_uf(&self) -> &str {
308        match self.uf.as_str() {
309            "AC" => "12", "AL" => "27", "AP" => "16", "AM" => "13", "BA" => "29",
310            "CE" => "23", "DF" => "53", "ES" => "32", "GO" => "52", "MA" => "21",
311            "MT" => "51", "MS" => "50", "MG" => "31", "PA" => "15", "PB" => "25",
312            "PR" => "41", "PE" => "26", "PI" => "22", "RJ" => "33", "RN" => "24",
313            "RS" => "43", "RO" => "11", "RR" => "14", "SC" => "42", "SP" => "35",
314            "SE" => "28", "TO" => "17",
315            _ => "35",
316        }
317    }
318
319    fn parsear_status_servico(&self, xml: &str) -> Result<StatusServicoResult, String> {
320        let c_stat = extract_xml_value(xml, "cStat").unwrap_or_default();
321        let x_motivo = extract_xml_value(xml, "xMotivo").unwrap_or_default();
322        let dh_recbto = extract_xml_value(xml, "dhRecbto");
323        let t_med = extract_xml_value(xml, "tMed").and_then(|s| s.parse().ok());
324
325        Ok(StatusServicoResult {
326            codigo_status: c_stat.clone(),
327            motivo: x_motivo,
328            data_hora: dh_recbto,
329            tempo_medio: t_med,
330            online: c_stat == "107",
331        })
332    }
333
334    fn parsear_consulta(&self, xml: &str) -> Result<ResultadoConsulta, String> {
335        let c_stat = extract_xml_value(xml, "cStat").unwrap_or_default();
336        let x_motivo = extract_xml_value(xml, "xMotivo").unwrap_or_default();
337        let ch_nfe = extract_xml_value(xml, "chNFe");
338        let n_prot = extract_xml_value(xml, "nProt");
339        let dh_recbto = extract_xml_value(xml, "dhRecbto");
340
341        let situacao = match c_stat.as_str() {
342            "100" => Some("Autorizada".to_string()),
343            "101" => Some("Cancelada".to_string()),
344            "110" => Some("Denegada".to_string()),
345            _ => Some(x_motivo.clone()),
346        };
347
348        Ok(ResultadoConsulta {
349            sucesso: c_stat == "100" || c_stat == "101",
350            codigo_status: Some(c_stat),
351            motivo: Some(x_motivo),
352            chave_acesso: ch_nfe,
353            situacao,
354            data_autorizacao: dh_recbto,
355            protocolo: n_prot,
356            numero: None,
357            serie: None,
358            emit_cnpj: None,
359            emit_razao_social: None,
360            valor_total: None,
361            url_consulta: None,
362        })
363    }
364
365    fn parsear_autorizacao(&self, xml: &str) -> Result<AutorizacaoResult, String> {
366        let c_stat = extract_xml_value(xml, "cStat").unwrap_or_default();
367        let x_motivo = extract_xml_value(xml, "xMotivo").unwrap_or_default();
368        let ch_nfe = extract_xml_value(xml, "chNFe");
369        let n_prot = extract_xml_value(xml, "nProt");
370
371        Ok(AutorizacaoResult {
372            sucesso: c_stat == "100" || c_stat == "104",
373            codigo_status: c_stat.clone(),
374            motivo: x_motivo,
375            chave_acesso: ch_nfe,
376            protocolo: n_prot,
377            xml_autorizado: if c_stat == "100" { Some(xml.to_string()) } else { None },
378        })
379    }
380
381    fn parsear_evento(&self, xml: &str) -> Result<EventoResult, String> {
382        let c_stat = extract_xml_value(xml, "cStat").unwrap_or_default();
383        let x_motivo = extract_xml_value(xml, "xMotivo").unwrap_or_default();
384        let n_prot = extract_xml_value(xml, "nProt");
385        let dh_reg = extract_xml_value(xml, "dhRegEvento");
386
387        Ok(EventoResult {
388            sucesso: c_stat == "135" || c_stat == "136",
389            codigo_status: c_stat,
390            motivo: x_motivo,
391            protocolo: n_prot,
392            data_evento: dh_reg,
393        })
394    }
395}
396
397// === Tipos de retorno ===
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct StatusServicoResult {
401    pub codigo_status: String,
402    pub motivo: String,
403    pub data_hora: Option<String>,
404    pub tempo_medio: Option<u32>,
405    pub online: bool,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct AutorizacaoResult {
410    pub sucesso: bool,
411    pub codigo_status: String,
412    pub motivo: String,
413    pub chave_acesso: Option<String>,
414    pub protocolo: Option<String>,
415    pub xml_autorizado: Option<String>,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct EventoResult {
420    pub sucesso: bool,
421    pub codigo_status: String,
422    pub motivo: String,
423    pub protocolo: Option<String>,
424    pub data_evento: Option<String>,
425}
426
427fn extract_xml_value(xml: &str, tag: &str) -> Option<String> {
428    let start_tag = format!("<{}>", tag);
429    let end_tag = format!("</{}>", tag);
430    let start = xml.find(&start_tag)?;
431    let value_start = start + start_tag.len();
432    let end = xml[value_start..].find(&end_tag)?;
433    Some(xml[value_start..value_start + end].to_string())
434}