Skip to main content

nfe_web/graphql/
resolvers.rs

1//! Resolvers GraphQL
2
3use super::types::*;
4use crate::sefaz;
5use async_graphql::{Context, Object, Result as GqlResult};
6
7/// Query root
8pub struct QueryRoot;
9
10#[Object]
11impl QueryRoot {
12    /// Consulta NF-e por chave de acesso
13    async fn nfe(&self, chave_acesso: String) -> GqlResult<Option<NfeType>> {
14        // Validar chave
15        let info = sefaz::validar_chave_acesso(&chave_acesso)
16            .map_err(|e| async_graphql::Error::new(e))?;
17
18        // Retornar dados básicos da chave
19        Ok(Some(NfeType {
20            id: chave_acesso.clone(),
21            chave_acesso: chave_acesso.clone(),
22            numero: info.numero as i32,
23            serie: info.serie as i32,
24            tipo: TipoDocumento::Nfe,
25            ambiente: Ambiente::Producao,
26            status: StatusNfe::Pendente,
27            data_emissao: format!("20{}-{}-01", &info.ano_mes[0..2], &info.ano_mes[2..4]),
28            data_autorizacao: None,
29            protocolo: None,
30            emitente: EmitenteType {
31                cnpj: info.cnpj,
32                razao_social: "Consulte SEFAZ para dados completos".to_string(),
33                nome_fantasia: None,
34                inscricao_estadual: None,
35                endereco: EnderecoType {
36                    logradouro: String::new(),
37                    numero: String::new(),
38                    complemento: None,
39                    bairro: String::new(),
40                    municipio: String::new(),
41                    uf: info.uf,
42                    cep: String::new(),
43                    pais: Some("Brasil".to_string()),
44                },
45            },
46            destinatario: None,
47            itens: vec![],
48            totais: TotaisType {
49                base_calculo_icms: 0.0,
50                valor_icms: 0.0,
51                valor_produtos: 0.0,
52                valor_frete: 0.0,
53                valor_desconto: 0.0,
54                valor_total: 0.0,
55            },
56            xml: None,
57        }))
58    }
59
60    /// Lista NF-e com filtros
61    async fn nfes(
62        &self,
63        filter: Option<NfeFilter>,
64        pagination: Option<Pagination>,
65    ) -> GqlResult<Vec<NfeType>> {
66        let _filter = filter.unwrap_or_default();
67        let _pagination = pagination.unwrap_or_default();
68
69        // Em produção, buscar do banco de dados
70        Ok(vec![])
71    }
72
73    /// Consulta status no SEFAZ
74    async fn consultar_sefaz(&self, chave_acesso: String) -> GqlResult<ConsultaSefazResult> {
75        let info = sefaz::validar_chave_acesso(&chave_acesso)
76            .map_err(|e| async_graphql::Error::new(e))?;
77
78        let url = sefaz::gerar_url_consulta_portal(&chave_acesso);
79
80        Ok(ConsultaSefazResult {
81            sucesso: true,
82            codigo_status: "100".to_string(),
83            motivo: "Chave válida - acesse URL para consulta completa".to_string(),
84            chave_acesso: Some(info.chave),
85            protocolo: None,
86            data_recebimento: None,
87            situacao: Some(format!("URL: {}", url)),
88        })
89    }
90
91    /// Valida chave de acesso
92    async fn validar_chave(&self, chave: String) -> GqlResult<bool> {
93        match sefaz::validar_chave_acesso(&chave) {
94            Ok(_) => Ok(true),
95            Err(_) => Ok(false),
96        }
97    }
98
99    /// Informações do certificado carregado
100    async fn certificado(&self, ctx: &Context<'_>) -> GqlResult<Option<CertificadoInfoType>> {
101        // Verificar se há certificado no contexto
102        if let Some(cert_info) = ctx.data_opt::<crate::certificado::CertificadoInfo>() {
103            Ok(Some(CertificadoInfoType {
104                cnpj: cert_info.cnpj.clone(),
105                razao_social: cert_info.razao_social.clone(),
106                valido: cert_info.valido,
107                data_validade: cert_info.not_after.clone(),
108                dias_para_expirar: cert_info.dias_para_expirar as i32,
109            }))
110        } else {
111            Ok(None)
112        }
113    }
114
115    /// Health check
116    async fn health(&self) -> GqlResult<String> {
117        Ok("OK".to_string())
118    }
119}
120
121/// Mutation root
122pub struct MutationRoot;
123
124#[Object]
125impl MutationRoot {
126    /// Emite NF-e no SEFAZ
127    async fn emitir_nfe(&self, input: NfeInput) -> GqlResult<EmissaoResult> {
128        // Validar dados
129        if input.itens.is_empty() {
130            return Err(async_graphql::Error::new("NF-e deve ter pelo menos um item"));
131        }
132
133        // Em produção:
134        // 1. Gerar XML
135        // 2. Assinar com certificado
136        // 3. Enviar para SEFAZ
137        // 4. Processar retorno
138
139        Ok(EmissaoResult {
140            sucesso: false,
141            codigo_status: "999".to_string(),
142            motivo: "Emissão requer certificado digital configurado".to_string(),
143            chave_acesso: None,
144            protocolo: None,
145            xml_autorizado: None,
146        })
147    }
148
149    /// Cancela NF-e no SEFAZ
150    async fn cancelar_nfe(&self, input: CancelamentoInput) -> GqlResult<CancelamentoResult> {
151        // Validar chave
152        let _info = sefaz::validar_chave_acesso(&input.chave_acesso)
153            .map_err(|e| async_graphql::Error::new(e))?;
154
155        // Validar justificativa (mínimo 15 caracteres)
156        if input.justificativa.len() < 15 {
157            return Err(async_graphql::Error::new("Justificativa deve ter no mínimo 15 caracteres"));
158        }
159
160        // Em produção:
161        // 1. Gerar XML do evento de cancelamento
162        // 2. Assinar com certificado
163        // 3. Enviar para SEFAZ
164        // 4. Processar retorno
165
166        Ok(CancelamentoResult {
167            sucesso: false,
168            codigo_status: "999".to_string(),
169            motivo: "Cancelamento requer certificado digital configurado".to_string(),
170            protocolo: None,
171            data_cancelamento: None,
172        })
173    }
174
175    /// Envia carta de correção
176    async fn carta_correcao(&self, input: CartaCorrecaoInput) -> GqlResult<EmissaoResult> {
177        // Validar chave
178        let _info = sefaz::validar_chave_acesso(&input.chave_acesso)
179            .map_err(|e| async_graphql::Error::new(e))?;
180
181        // Validar correção (mínimo 15 caracteres)
182        if input.correcao.len() < 15 {
183            return Err(async_graphql::Error::new("Correção deve ter no mínimo 15 caracteres"));
184        }
185
186        Ok(EmissaoResult {
187            sucesso: false,
188            codigo_status: "999".to_string(),
189            motivo: "Carta de correção requer certificado digital configurado".to_string(),
190            chave_acesso: None,
191            protocolo: None,
192            xml_autorizado: None,
193        })
194    }
195
196    /// Carrega certificado digital
197    async fn carregar_certificado(
198        &self,
199        pfx_base64: String,
200        senha: String,
201    ) -> GqlResult<CertificadoInfoType> {
202        use base64::Engine;
203
204        let pfx_bytes = base64::engine::general_purpose::STANDARD
205            .decode(&pfx_base64)
206            .map_err(|e| async_graphql::Error::new(format!("Erro ao decodificar certificado: {}", e)))?;
207
208        let cert = crate::certificado::CertificadoA1::from_bytes(&pfx_bytes, &senha)
209            .map_err(|e| async_graphql::Error::new(e))?;
210
211        Ok(CertificadoInfoType {
212            cnpj: cert.info.cnpj,
213            razao_social: cert.info.razao_social,
214            valido: cert.info.valido,
215            data_validade: cert.info.not_after,
216            dias_para_expirar: cert.info.dias_para_expirar as i32,
217        })
218    }
219
220    /// Parse XML de NF-e
221    async fn parse_xml(&self, xml: String) -> GqlResult<NfeType> {
222        // Usar parser existente
223        let xml_clean = xml.replace("xmlns=\"http://www.portalfiscal.inf.br/nfe\"", "");
224
225        let nfe: nfe_parser::Nfe = xml_clean.parse()
226            .map_err(|e| async_graphql::Error::new(format!("Erro ao parsear XML: {}", e)))?;
227
228        Ok(NfeType {
229            id: nfe.chave_acesso.clone(),
230            chave_acesso: nfe.chave_acesso,
231            numero: nfe.ide.numero as i32,
232            serie: nfe.ide.serie as i32,
233            tipo: TipoDocumento::Nfe,
234            ambiente: if nfe.ide.ambiente == nfe_parser::TipoAmbiente::Producao {
235                Ambiente::Producao
236            } else {
237                Ambiente::Homologacao
238            },
239            status: StatusNfe::Pendente,
240            data_emissao: nfe.ide.emissao.horario.format("%Y-%m-%dT%H:%M:%S").to_string(),
241            data_autorizacao: None,
242            protocolo: None,
243            emitente: EmitenteType {
244                cnpj: nfe.emit.cnpj.clone().unwrap_or_default(),
245                razao_social: nfe.emit.razao_social.clone().unwrap_or_default(),
246                nome_fantasia: nfe.emit.nome_fantasia.clone(),
247                inscricao_estadual: nfe.emit.ie.clone(),
248                endereco: EnderecoType {
249                    logradouro: nfe.emit.endereco.logradouro.clone(),
250                    numero: nfe.emit.endereco.numero.clone(),
251                    complemento: nfe.emit.endereco.complemento.clone(),
252                    bairro: nfe.emit.endereco.bairro.clone(),
253                    municipio: nfe.emit.endereco.nome_municipio.clone(),
254                    uf: nfe.emit.endereco.sigla_uf.clone(),
255                    cep: nfe.emit.endereco.cep.clone(),
256                    pais: Some("Brasil".to_string()),
257                },
258            },
259            destinatario: nfe.dest.as_ref().map(|d| DestinatarioType {
260                cnpj: Some(d.cnpj.clone()),
261                cpf: None,
262                razao_social: d.razao_social.clone().unwrap_or_default(),
263                inscricao_estadual: None,
264                endereco: d.endereco.as_ref().map(|e| EnderecoType {
265                    logradouro: e.logradouro.clone(),
266                    numero: e.numero.clone(),
267                    complemento: e.complemento.clone(),
268                    bairro: e.bairro.clone(),
269                    municipio: e.nome_municipio.clone(),
270                    uf: e.sigla_uf.clone(),
271                    cep: e.cep.clone(),
272                    pais: Some("Brasil".to_string()),
273                }),
274            }),
275            itens: nfe.itens.iter().map(|item| ItemType {
276                numero: item.numero as i32,
277                codigo: item.produto.codigo.clone(),
278                descricao: item.produto.descricao.clone(),
279                ncm: item.produto.ncm.clone(),
280                cfop: item.produto.tributacao.cfop.clone(),
281                unidade: item.produto.unidade.clone(),
282                quantidade: item.produto.quantidade as f64,
283                valor_unitario: item.produto.valor_unitario as f64,
284                valor_total: item.produto.valor_bruto as f64,
285            }).collect(),
286            totais: TotaisType {
287                base_calculo_icms: nfe.totais.valor_base_calculo as f64,
288                valor_icms: nfe.totais.valor_icms as f64,
289                valor_produtos: nfe.totais.valor_produtos as f64,
290                valor_frete: nfe.totais.valor_frete as f64,
291                valor_desconto: nfe.totais.valor_desconto as f64,
292                valor_total: nfe.totais.valor_total as f64,
293            },
294            xml: Some(xml),
295        })
296    }
297}