1use super::types::*;
4use crate::sefaz;
5use crate::sefaz::webservice::{SefazClient, AmbienteNfe};
6use crate::certificado::{CertificadoA1, AssinadorXml};
7use async_graphql::{Context, Object, Result as GqlResult};
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11pub struct GraphQLState {
13 pub certificado: Option<CertificadoA1>,
14 pub sefaz_client: Option<SefazClient>,
15}
16
17impl Default for GraphQLState {
18 fn default() -> Self {
19 Self {
20 certificado: None,
21 sefaz_client: None,
22 }
23 }
24}
25
26pub struct QueryRoot;
28
29#[Object]
30impl QueryRoot {
31 async fn nfe(&self, chave_acesso: String) -> GqlResult<Option<NfeType>> {
33 let info = sefaz::validar_chave_acesso(&chave_acesso)
35 .map_err(|e| async_graphql::Error::new(e))?;
36
37 Ok(Some(NfeType {
39 id: chave_acesso.clone(),
40 chave_acesso: chave_acesso.clone(),
41 numero: info.numero as i32,
42 serie: info.serie as i32,
43 tipo: TipoDocumento::Nfe,
44 ambiente: Ambiente::Producao,
45 status: StatusNfe::Pendente,
46 data_emissao: format!("20{}-{}-01", &info.ano_mes[0..2], &info.ano_mes[2..4]),
47 data_autorizacao: None,
48 protocolo: None,
49 emitente: EmitenteType {
50 cnpj: info.cnpj,
51 razao_social: "Consulte SEFAZ para dados completos".to_string(),
52 nome_fantasia: None,
53 inscricao_estadual: None,
54 endereco: EnderecoType {
55 logradouro: String::new(),
56 numero: String::new(),
57 complemento: None,
58 bairro: String::new(),
59 municipio: String::new(),
60 uf: info.uf,
61 cep: String::new(),
62 pais: Some("Brasil".to_string()),
63 },
64 },
65 destinatario: None,
66 itens: vec![],
67 totais: TotaisType {
68 base_calculo_icms: 0.0,
69 valor_icms: 0.0,
70 valor_produtos: 0.0,
71 valor_frete: 0.0,
72 valor_desconto: 0.0,
73 valor_total: 0.0,
74 },
75 xml: None,
76 }))
77 }
78
79 async fn nfes(
81 &self,
82 filter: Option<NfeFilter>,
83 pagination: Option<Pagination>,
84 ) -> GqlResult<Vec<NfeType>> {
85 let _filter = filter.unwrap_or_default();
86 let _pagination = pagination.unwrap_or_default();
87
88 Ok(vec![])
90 }
91
92 async fn consultar_sefaz(&self, ctx: &Context<'_>, chave_acesso: String) -> GqlResult<ConsultaSefazResult> {
94 let info = sefaz::validar_chave_acesso(&chave_acesso)
95 .map_err(|e| async_graphql::Error::new(e))?;
96
97 if let Some(state) = ctx.data_opt::<Arc<RwLock<GraphQLState>>>() {
99 let state = state.read().await;
100 if let Some(ref client) = state.sefaz_client {
101 match client.consultar_nfe(&chave_acesso).await {
102 Ok(resultado) => {
103 return Ok(ConsultaSefazResult {
104 sucesso: resultado.sucesso,
105 codigo_status: resultado.codigo_status.unwrap_or_else(|| "000".to_string()),
106 motivo: resultado.motivo.unwrap_or_else(|| "Consulta realizada".to_string()),
107 chave_acesso: resultado.chave_acesso,
108 protocolo: resultado.protocolo,
109 data_recebimento: resultado.data_autorizacao,
110 situacao: resultado.situacao,
111 });
112 }
113 Err(e) => {
114 return Err(async_graphql::Error::new(format!("Erro SEFAZ: {}", e)));
115 }
116 }
117 }
118 }
119
120 let url = sefaz::gerar_url_consulta_portal(&chave_acesso);
122
123 Ok(ConsultaSefazResult {
124 sucesso: true,
125 codigo_status: "100".to_string(),
126 motivo: "Chave válida - use certificado para consulta completa".to_string(),
127 chave_acesso: Some(info.chave),
128 protocolo: None,
129 data_recebimento: None,
130 situacao: Some(format!("Portal: {}", url)),
131 })
132 }
133
134 async fn status_servico(&self, ctx: &Context<'_>) -> GqlResult<StatusServicoType> {
136 if let Some(state) = ctx.data_opt::<Arc<RwLock<GraphQLState>>>() {
137 let state = state.read().await;
138 if let Some(ref client) = state.sefaz_client {
139 match client.status_servico().await {
140 Ok(status) => {
141 return Ok(StatusServicoType {
142 codigo_status: status.codigo_status.clone(),
143 motivo: status.motivo,
144 tempo_medio: status.tempo_medio.map(|t| format!("{} ms", t)),
145 uf: None,
146 online: status.online,
147 });
148 }
149 Err(e) => {
150 return Err(async_graphql::Error::new(format!("Erro SEFAZ: {}", e)));
151 }
152 }
153 }
154 }
155
156 Err(async_graphql::Error::new("Certificado digital não configurado"))
157 }
158
159 async fn validar_chave(&self, chave: String) -> GqlResult<bool> {
161 match sefaz::validar_chave_acesso(&chave) {
162 Ok(_) => Ok(true),
163 Err(_) => Ok(false),
164 }
165 }
166
167 async fn certificado(&self, ctx: &Context<'_>) -> GqlResult<Option<CertificadoInfoType>> {
169 if let Some(state) = ctx.data_opt::<Arc<RwLock<GraphQLState>>>() {
170 let state = state.read().await;
171 if let Some(ref cert) = state.certificado {
172 return Ok(Some(CertificadoInfoType {
173 cnpj: cert.info.cnpj.clone(),
174 razao_social: cert.info.razao_social.clone(),
175 valido: cert.info.valido,
176 data_validade: cert.info.not_after.clone(),
177 dias_para_expirar: cert.info.dias_para_expirar as i32,
178 }));
179 }
180 }
181
182 if let Some(cert_info) = ctx.data_opt::<crate::certificado::CertificadoInfo>() {
184 Ok(Some(CertificadoInfoType {
185 cnpj: cert_info.cnpj.clone(),
186 razao_social: cert_info.razao_social.clone(),
187 valido: cert_info.valido,
188 data_validade: cert_info.not_after.clone(),
189 dias_para_expirar: cert_info.dias_para_expirar as i32,
190 }))
191 } else {
192 Ok(None)
193 }
194 }
195
196 async fn health(&self) -> GqlResult<String> {
198 Ok("OK".to_string())
199 }
200}
201
202#[derive(Debug, Clone, async_graphql::SimpleObject)]
204pub struct StatusServicoType {
205 pub codigo_status: String,
206 pub motivo: String,
207 pub tempo_medio: Option<String>,
208 pub uf: Option<String>,
209 pub online: bool,
210}
211
212pub struct MutationRoot;
214
215#[Object]
216impl MutationRoot {
217 async fn emitir_nfe(&self, ctx: &Context<'_>, input: NfeInput) -> GqlResult<EmissaoResult> {
219 if input.itens.is_empty() {
221 return Err(async_graphql::Error::new("NF-e deve ter pelo menos um item"));
222 }
223
224 if let Some(state) = ctx.data_opt::<Arc<RwLock<GraphQLState>>>() {
226 let state = state.read().await;
227
228 if let (Some(ref cert), Some(ref client)) = (&state.certificado, &state.sefaz_client) {
229 let xml_nfe = gerar_xml_nfe(&input)?;
231
232 let assinador = AssinadorXml::new(cert.clone());
234 let xml_assinado = assinador.assinar_nfe(&xml_nfe)
235 .map_err(|e| async_graphql::Error::new(format!("Erro ao assinar: {}", e)))?;
236
237 match client.autorizar_nfe(&xml_assinado).await {
239 Ok(resultado) => {
240 return Ok(EmissaoResult {
241 sucesso: resultado.sucesso,
242 codigo_status: resultado.codigo_status,
243 motivo: resultado.motivo,
244 chave_acesso: resultado.chave_acesso,
245 protocolo: resultado.protocolo,
246 xml_autorizado: resultado.xml_autorizado,
247 });
248 }
249 Err(e) => {
250 return Err(async_graphql::Error::new(format!("Erro SEFAZ: {}", e)));
251 }
252 }
253 }
254 }
255
256 Ok(EmissaoResult {
257 sucesso: false,
258 codigo_status: "999".to_string(),
259 motivo: "Certificado digital não configurado. Use carregar_certificado primeiro.".to_string(),
260 chave_acesso: None,
261 protocolo: None,
262 xml_autorizado: None,
263 })
264 }
265
266 async fn cancelar_nfe(&self, ctx: &Context<'_>, input: CancelamentoInput) -> GqlResult<CancelamentoResult> {
268 let _info = sefaz::validar_chave_acesso(&input.chave_acesso)
270 .map_err(|e| async_graphql::Error::new(e))?;
271
272 if input.justificativa.len() < 15 {
274 return Err(async_graphql::Error::new("Justificativa deve ter no mínimo 15 caracteres"));
275 }
276
277 if let Some(state) = ctx.data_opt::<Arc<RwLock<GraphQLState>>>() {
279 let state = state.read().await;
280
281 if let Some(ref client) = state.sefaz_client {
282 match client.cancelar_nfe(&input.chave_acesso, &input.protocolo_autorizacao, &input.justificativa).await {
283 Ok(resultado) => {
284 return Ok(CancelamentoResult {
285 sucesso: resultado.sucesso,
286 codigo_status: resultado.codigo_status,
287 motivo: resultado.motivo,
288 protocolo: resultado.protocolo,
289 data_cancelamento: resultado.data_evento,
290 });
291 }
292 Err(e) => {
293 return Err(async_graphql::Error::new(format!("Erro SEFAZ: {}", e)));
294 }
295 }
296 }
297 }
298
299 Ok(CancelamentoResult {
300 sucesso: false,
301 codigo_status: "999".to_string(),
302 motivo: "Certificado digital não configurado. Use carregar_certificado primeiro.".to_string(),
303 protocolo: None,
304 data_cancelamento: None,
305 })
306 }
307
308 async fn carta_correcao(&self, ctx: &Context<'_>, input: CartaCorrecaoInput) -> GqlResult<EmissaoResult> {
310 let _info = sefaz::validar_chave_acesso(&input.chave_acesso)
312 .map_err(|e| async_graphql::Error::new(e))?;
313
314 if input.correcao.len() < 15 {
316 return Err(async_graphql::Error::new("Correção deve ter no mínimo 15 caracteres"));
317 }
318
319 if let Some(state) = ctx.data_opt::<Arc<RwLock<GraphQLState>>>() {
321 let state = state.read().await;
322
323 if let Some(ref client) = state.sefaz_client {
324 match client.carta_correcao(&input.chave_acesso, input.sequencia as u32, &input.correcao).await {
325 Ok(resultado) => {
326 return Ok(EmissaoResult {
327 sucesso: resultado.sucesso,
328 codigo_status: resultado.codigo_status,
329 motivo: resultado.motivo,
330 chave_acesso: Some(input.chave_acesso),
331 protocolo: resultado.protocolo,
332 xml_autorizado: None,
333 });
334 }
335 Err(e) => {
336 return Err(async_graphql::Error::new(format!("Erro SEFAZ: {}", e)));
337 }
338 }
339 }
340 }
341
342 Ok(EmissaoResult {
343 sucesso: false,
344 codigo_status: "999".to_string(),
345 motivo: "Certificado digital não configurado. Use carregar_certificado primeiro.".to_string(),
346 chave_acesso: None,
347 protocolo: None,
348 xml_autorizado: None,
349 })
350 }
351
352 async fn carregar_certificado(
354 &self,
355 ctx: &Context<'_>,
356 pfx_base64: String,
357 senha: String,
358 uf: String,
359 ambiente: Option<String>,
360 ) -> GqlResult<CertificadoInfoType> {
361 use base64::Engine;
362
363 let pfx_bytes = base64::engine::general_purpose::STANDARD
364 .decode(&pfx_base64)
365 .map_err(|e| async_graphql::Error::new(format!("Erro ao decodificar certificado: {}", e)))?;
366
367 let cert = CertificadoA1::from_bytes(&pfx_bytes, &senha)
368 .map_err(|e| async_graphql::Error::new(e))?;
369
370 if !cert.is_valid() {
372 return Err(async_graphql::Error::new("Certificado expirado ou inválido"));
373 }
374
375 let info = CertificadoInfoType {
376 cnpj: cert.info.cnpj.clone(),
377 razao_social: cert.info.razao_social.clone(),
378 valido: cert.info.valido,
379 data_validade: cert.info.not_after.clone(),
380 dias_para_expirar: cert.info.dias_para_expirar as i32,
381 };
382
383 let amb = match ambiente.as_deref() {
385 Some("homologacao") | Some("2") => AmbienteNfe::Homologacao,
386 _ => AmbienteNfe::Producao,
387 };
388
389 let sefaz_client = SefazClient::new(cert.clone(), &uf, amb)
391 .map_err(|e| async_graphql::Error::new(format!("Erro ao criar cliente SEFAZ: {}", e)))?;
392
393 if let Some(state) = ctx.data_opt::<Arc<RwLock<GraphQLState>>>() {
395 let mut state = state.write().await;
396 state.certificado = Some(cert);
397 state.sefaz_client = Some(sefaz_client);
398 }
399
400 Ok(info)
401 }
402
403 async fn parse_xml(&self, xml: String) -> GqlResult<NfeType> {
405 let xml_clean = xml.replace("xmlns=\"http://www.portalfiscal.inf.br/nfe\"", "");
407
408 let nfe: nfe_parser::Nfe = xml_clean.parse()
409 .map_err(|e| async_graphql::Error::new(format!("Erro ao parsear XML: {}", e)))?;
410
411 Ok(NfeType {
412 id: nfe.chave_acesso.clone(),
413 chave_acesso: nfe.chave_acesso,
414 numero: nfe.ide.numero as i32,
415 serie: nfe.ide.serie as i32,
416 tipo: TipoDocumento::Nfe,
417 ambiente: if nfe.ide.ambiente == nfe_parser::TipoAmbiente::Producao {
418 Ambiente::Producao
419 } else {
420 Ambiente::Homologacao
421 },
422 status: StatusNfe::Pendente,
423 data_emissao: nfe.ide.emissao.horario.format("%Y-%m-%dT%H:%M:%S").to_string(),
424 data_autorizacao: None,
425 protocolo: None,
426 emitente: EmitenteType {
427 cnpj: nfe.emit.cnpj.clone().unwrap_or_default(),
428 razao_social: nfe.emit.razao_social.clone().unwrap_or_default(),
429 nome_fantasia: nfe.emit.nome_fantasia.clone(),
430 inscricao_estadual: nfe.emit.ie.clone(),
431 endereco: EnderecoType {
432 logradouro: nfe.emit.endereco.logradouro.clone(),
433 numero: nfe.emit.endereco.numero.clone(),
434 complemento: nfe.emit.endereco.complemento.clone(),
435 bairro: nfe.emit.endereco.bairro.clone(),
436 municipio: nfe.emit.endereco.nome_municipio.clone(),
437 uf: nfe.emit.endereco.sigla_uf.clone(),
438 cep: nfe.emit.endereco.cep.clone(),
439 pais: Some("Brasil".to_string()),
440 },
441 },
442 destinatario: nfe.dest.as_ref().map(|d| DestinatarioType {
443 cnpj: Some(d.cnpj.clone()),
444 cpf: None,
445 razao_social: d.razao_social.clone().unwrap_or_default(),
446 inscricao_estadual: None,
447 endereco: d.endereco.as_ref().map(|e| EnderecoType {
448 logradouro: e.logradouro.clone(),
449 numero: e.numero.clone(),
450 complemento: e.complemento.clone(),
451 bairro: e.bairro.clone(),
452 municipio: e.nome_municipio.clone(),
453 uf: e.sigla_uf.clone(),
454 cep: e.cep.clone(),
455 pais: Some("Brasil".to_string()),
456 }),
457 }),
458 itens: nfe.itens.iter().map(|item| ItemType {
459 numero: item.numero as i32,
460 codigo: item.produto.codigo.clone(),
461 descricao: item.produto.descricao.clone(),
462 ncm: item.produto.ncm.clone(),
463 cfop: item.produto.tributacao.cfop.clone(),
464 unidade: item.produto.unidade.clone(),
465 quantidade: item.produto.quantidade as f64,
466 valor_unitario: item.produto.valor_unitario as f64,
467 valor_total: item.produto.valor_bruto as f64,
468 }).collect(),
469 totais: TotaisType {
470 base_calculo_icms: nfe.totais.valor_base_calculo as f64,
471 valor_icms: nfe.totais.valor_icms as f64,
472 valor_produtos: nfe.totais.valor_produtos as f64,
473 valor_frete: nfe.totais.valor_frete as f64,
474 valor_desconto: nfe.totais.valor_desconto as f64,
475 valor_total: nfe.totais.valor_total as f64,
476 },
477 xml: Some(xml),
478 })
479 }
480}
481
482fn gerar_xml_nfe(input: &NfeInput) -> Result<String, async_graphql::Error> {
484 let mut xml = String::new();
485
486 xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
488 xml.push_str("<NFe xmlns=\"http://www.portalfiscal.inf.br/nfe\">");
489
490 let id_placeholder = format!("NFe{}", chrono::Utc::now().format("%Y%m%d%H%M%S%f"));
492 xml.push_str(&format!("<infNFe versao=\"4.00\" Id=\"{}\">", id_placeholder));
493
494 xml.push_str("<ide>");
496 xml.push_str(&format!("<cUF>{}</cUF>", get_codigo_uf(&input.emitente.endereco.uf)));
497 xml.push_str(&format!("<cNF>{:08}</cNF>", chrono::Utc::now().timestamp() % 100000000));
498 xml.push_str("<natOp>Venda de mercadoria</natOp>");
499 xml.push_str("<mod>55</mod>");
501 xml.push_str(&format!("<serie>{}</serie>", input.serie));
502 xml.push_str(&format!("<nNF>{}</nNF>", input.numero));
503 xml.push_str(&format!("<dhEmi>{}</dhEmi>", chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S-03:00")));
504 xml.push_str("<tpNF>1</tpNF>"); xml.push_str("<idDest>1</idDest>"); xml.push_str(&format!("<cMunFG>{}</cMunFG>", get_codigo_municipio(&input.emitente.endereco.municipio)));
507 xml.push_str("<tpImp>1</tpImp>");
508 xml.push_str("<tpEmis>1</tpEmis>");
509 xml.push_str("<tpAmb>2</tpAmb>"); xml.push_str("<finNFe>1</finNFe>");
511 xml.push_str("<indFinal>1</indFinal>");
512 xml.push_str("<indPres>1</indPres>");
513 xml.push_str("<procEmi>0</procEmi>");
514 xml.push_str("<verProc>NfeWeb1.0</verProc>");
515 xml.push_str("</ide>");
516
517 xml.push_str("<emit>");
519 xml.push_str(&format!("<CNPJ>{}</CNPJ>", input.emitente.cnpj));
520 xml.push_str(&format!("<xNome>{}</xNome>", input.emitente.razao_social));
521 if let Some(ref fantasia) = input.emitente.nome_fantasia {
522 xml.push_str(&format!("<xFant>{}</xFant>", fantasia));
523 }
524 xml.push_str("<enderEmit>");
525 xml.push_str(&format!("<xLgr>{}</xLgr>", input.emitente.endereco.logradouro));
526 xml.push_str(&format!("<nro>{}</nro>", input.emitente.endereco.numero));
527 xml.push_str(&format!("<xBairro>{}</xBairro>", input.emitente.endereco.bairro));
528 xml.push_str(&format!("<cMun>{}</cMun>", get_codigo_municipio(&input.emitente.endereco.municipio)));
529 xml.push_str(&format!("<xMun>{}</xMun>", input.emitente.endereco.municipio));
530 xml.push_str(&format!("<UF>{}</UF>", input.emitente.endereco.uf));
531 xml.push_str(&format!("<CEP>{}</CEP>", input.emitente.endereco.cep));
532 xml.push_str("<cPais>1058</cPais>");
533 xml.push_str("<xPais>Brasil</xPais>");
534 xml.push_str("</enderEmit>");
535 if let Some(ref ie) = input.emitente.inscricao_estadual {
536 xml.push_str(&format!("<IE>{}</IE>", ie));
537 }
538 xml.push_str("<CRT>1</CRT>"); xml.push_str("</emit>");
540
541 if let Some(ref dest) = input.destinatario {
543 xml.push_str("<dest>");
544 if let Some(ref cnpj) = dest.cnpj {
545 xml.push_str(&format!("<CNPJ>{}</CNPJ>", cnpj));
546 } else if let Some(ref cpf) = dest.cpf {
547 xml.push_str(&format!("<CPF>{}</CPF>", cpf));
548 }
549 xml.push_str(&format!("<xNome>{}</xNome>", dest.razao_social));
550 if let Some(ref end) = dest.endereco {
551 xml.push_str("<enderDest>");
552 xml.push_str(&format!("<xLgr>{}</xLgr>", end.logradouro));
553 xml.push_str(&format!("<nro>{}</nro>", end.numero));
554 xml.push_str(&format!("<xBairro>{}</xBairro>", end.bairro));
555 xml.push_str(&format!("<cMun>{}</cMun>", get_codigo_municipio(&end.municipio)));
556 xml.push_str(&format!("<xMun>{}</xMun>", end.municipio));
557 xml.push_str(&format!("<UF>{}</UF>", end.uf));
558 xml.push_str(&format!("<CEP>{}</CEP>", end.cep));
559 xml.push_str("<cPais>1058</cPais>");
560 xml.push_str("<xPais>Brasil</xPais>");
561 xml.push_str("</enderDest>");
562 }
563 xml.push_str("<indIEDest>9</indIEDest>");
564 xml.push_str("</dest>");
565 }
566
567 for (i, item) in input.itens.iter().enumerate() {
569 xml.push_str(&format!("<det nItem=\"{}\">", i + 1));
570 xml.push_str("<prod>");
571 xml.push_str(&format!("<cProd>{}</cProd>", item.codigo));
572 xml.push_str("<cEAN>SEM GTIN</cEAN>");
573 xml.push_str(&format!("<xProd>{}</xProd>", item.descricao));
574 xml.push_str(&format!("<NCM>{}</NCM>", item.ncm));
575 xml.push_str(&format!("<CFOP>{}</CFOP>", item.cfop));
576 xml.push_str(&format!("<uCom>{}</uCom>", item.unidade));
577 xml.push_str(&format!("<qCom>{:.4}</qCom>", item.quantidade));
578 xml.push_str(&format!("<vUnCom>{:.4}</vUnCom>", item.valor_unitario));
579 xml.push_str(&format!("<vProd>{:.2}</vProd>", item.quantidade * item.valor_unitario));
580 xml.push_str("<cEANTrib>SEM GTIN</cEANTrib>");
581 xml.push_str(&format!("<uTrib>{}</uTrib>", item.unidade));
582 xml.push_str(&format!("<qTrib>{:.4}</qTrib>", item.quantidade));
583 xml.push_str(&format!("<vUnTrib>{:.4}</vUnTrib>", item.valor_unitario));
584 xml.push_str("<indTot>1</indTot>");
585 xml.push_str("</prod>");
586 xml.push_str("<imposto>");
587 xml.push_str("<ICMS><ICMSSN102><orig>0</orig><CSOSN>102</CSOSN></ICMSSN102></ICMS>");
588 xml.push_str("<PIS><PISOutr><CST>99</CST><vBC>0.00</vBC><pPIS>0.00</pPIS><vPIS>0.00</vPIS></PISOutr></PIS>");
589 xml.push_str("<COFINS><COFINSOutr><CST>99</CST><vBC>0.00</vBC><pCOFINS>0.00</pCOFINS><vCOFINS>0.00</vCOFINS></COFINSOutr></COFINS>");
590 xml.push_str("</imposto>");
591 xml.push_str("</det>");
592 }
593
594 let total_produtos: f64 = input.itens.iter().map(|i| i.quantidade * i.valor_unitario).sum();
596 xml.push_str("<total>");
597 xml.push_str("<ICMSTot>");
598 xml.push_str("<vBC>0.00</vBC>");
599 xml.push_str("<vICMS>0.00</vICMS>");
600 xml.push_str("<vICMSDeson>0.00</vICMSDeson>");
601 xml.push_str("<vFCP>0.00</vFCP>");
602 xml.push_str("<vBCST>0.00</vBCST>");
603 xml.push_str("<vST>0.00</vST>");
604 xml.push_str("<vFCPST>0.00</vFCPST>");
605 xml.push_str("<vFCPSTRet>0.00</vFCPSTRet>");
606 xml.push_str(&format!("<vProd>{:.2}</vProd>", total_produtos));
607 xml.push_str("<vFrete>0.00</vFrete>");
608 xml.push_str("<vSeg>0.00</vSeg>");
609 xml.push_str("<vDesc>0.00</vDesc>");
610 xml.push_str("<vII>0.00</vII>");
611 xml.push_str("<vIPI>0.00</vIPI>");
612 xml.push_str("<vIPIDevol>0.00</vIPIDevol>");
613 xml.push_str("<vPIS>0.00</vPIS>");
614 xml.push_str("<vCOFINS>0.00</vCOFINS>");
615 xml.push_str("<vOutro>0.00</vOutro>");
616 xml.push_str(&format!("<vNF>{:.2}</vNF>", total_produtos));
617 xml.push_str("</ICMSTot>");
618 xml.push_str("</total>");
619
620 xml.push_str("<transp>");
622 xml.push_str("<modFrete>9</modFrete>");
623 xml.push_str("</transp>");
624
625 xml.push_str("<pag>");
627 xml.push_str("<detPag>");
628 xml.push_str("<tPag>01</tPag>");
629 xml.push_str(&format!("<vPag>{:.2}</vPag>", total_produtos));
630 xml.push_str("</detPag>");
631 xml.push_str("</pag>");
632
633 xml.push_str("</infNFe>");
634 xml.push_str("</NFe>");
635
636 Ok(xml)
637}
638
639fn get_codigo_uf(uf: &str) -> &str {
641 match uf {
642 "AC" => "12", "AL" => "27", "AP" => "16", "AM" => "13", "BA" => "29",
643 "CE" => "23", "DF" => "53", "ES" => "32", "GO" => "52", "MA" => "21",
644 "MT" => "51", "MS" => "50", "MG" => "31", "PA" => "15", "PB" => "25",
645 "PR" => "41", "PE" => "26", "PI" => "22", "RJ" => "33", "RN" => "24",
646 "RS" => "43", "RO" => "11", "RR" => "14", "SC" => "42", "SP" => "35",
647 "SE" => "28", "TO" => "17", _ => "35",
648 }
649}
650
651fn get_codigo_municipio(municipio: &str) -> String {
653 format!("{}0000", municipio.len())
655}