1use serde::{Deserialize, Serialize};
6use regex::Regex;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub enum TipoDocumentoFiscal {
11 #[serde(rename = "NFe")]
12 Nfe, #[serde(rename = "NFCe")]
14 Nfce, #[serde(rename = "NFSe")]
16 Nfse, #[serde(rename = "CTe")]
18 Cte, #[serde(rename = "MDFe")]
20 Mdfe, #[serde(rename = "NFAe")]
22 Nfae, #[serde(rename = "CFeSAT")]
24 CfeSat, #[serde(rename = "Desconhecido")]
26 Desconhecido,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct DanfeData {
32 pub tipo_documento: TipoDocumentoFiscal,
33
34 pub chave_acesso: Option<String>,
36 pub numero: Option<i32>,
37 pub serie: Option<i16>,
38 pub numero_rps: Option<i32>,
39 pub data_emissao: Option<String>,
40 pub codigo_verificacao: Option<String>,
41 pub competencia: Option<String>,
42
43 pub emit_cnpj: Option<String>,
45 pub emit_cpf: Option<String>,
46 pub emit_razao_social: Option<String>,
47 pub emit_nome_fantasia: Option<String>,
48 pub emit_inscricao_estadual: Option<String>,
49 pub emit_inscricao_municipal: Option<String>,
50 pub emit_endereco: Option<String>,
51 pub emit_complemento: Option<String>,
52 pub emit_bairro: Option<String>,
53 pub emit_municipio: Option<String>,
54 pub emit_uf: Option<String>,
55 pub emit_cep: Option<String>,
56 pub emit_telefone: Option<String>,
57 pub emit_email: Option<String>,
58
59 pub dest_cnpj: Option<String>,
61 pub dest_cpf: Option<String>,
62 pub dest_razao_social: Option<String>,
63 pub dest_endereco: Option<String>,
64 pub dest_complemento: Option<String>,
65 pub dest_bairro: Option<String>,
66 pub dest_municipio: Option<String>,
67 pub dest_uf: Option<String>,
68 pub dest_cep: Option<String>,
69
70 pub valor_total: Option<f64>,
72 pub valor_servicos: Option<f64>,
73 pub valor_produtos: Option<f64>,
74 pub valor_frete: Option<f64>,
75 pub valor_desconto: Option<f64>,
76 pub valor_liquido: Option<f64>,
77 pub base_calculo: Option<f64>,
78 pub aliquota: Option<f64>,
79 pub valor_iss: Option<f64>,
80 pub valor_icms: Option<f64>,
81 pub valor_pis: Option<f64>,
82 pub valor_cofins: Option<f64>,
83 pub valor_ir: Option<f64>,
84 pub valor_inss: Option<f64>,
85 pub valor_csll: Option<f64>,
86 pub valor_deducoes: Option<f64>,
87 pub valor_outras_retencoes: Option<f64>,
88 pub tributos_aproximados: Option<f64>,
89
90 pub codigo_servico: Option<String>,
92 pub descricao_servico: Option<String>,
93 pub discriminacao_servico: Option<String>,
94 pub natureza_operacao: Option<String>,
95 pub regime_tributacao: Option<String>,
96 pub municipio_prestacao: Option<String>,
97 pub optante_simples: Option<bool>,
98 pub incentivador_cultural: Option<bool>,
99 pub iss_retido: Option<bool>,
100
101 pub modal_transporte: Option<String>,
103 pub placa_veiculo: Option<String>,
104 pub uf_veiculo: Option<String>,
105 pub rntrc: Option<String>,
106
107 pub itens: Vec<ItemDanfe>,
109
110 pub texto_completo: String,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct ItemDanfe {
117 pub numero: Option<i32>,
118 pub codigo: Option<String>,
119 pub descricao: Option<String>,
120 pub ncm: Option<String>,
121 pub cfop: Option<String>,
122 pub unidade: Option<String>,
123 pub quantidade: Option<f64>,
124 pub valor_unitario: Option<f64>,
125 pub valor_total: Option<f64>,
126}
127
128impl Default for DanfeData {
129 fn default() -> Self {
130 Self {
131 tipo_documento: TipoDocumentoFiscal::Desconhecido,
132 chave_acesso: None,
133 numero: None,
134 serie: None,
135 numero_rps: None,
136 data_emissao: None,
137 codigo_verificacao: None,
138 competencia: None,
139 emit_cnpj: None,
140 emit_cpf: None,
141 emit_razao_social: None,
142 emit_nome_fantasia: None,
143 emit_inscricao_estadual: None,
144 emit_inscricao_municipal: None,
145 emit_endereco: None,
146 emit_complemento: None,
147 emit_bairro: None,
148 emit_municipio: None,
149 emit_uf: None,
150 emit_cep: None,
151 emit_telefone: None,
152 emit_email: None,
153 dest_cnpj: None,
154 dest_cpf: None,
155 dest_razao_social: None,
156 dest_endereco: None,
157 dest_complemento: None,
158 dest_bairro: None,
159 dest_municipio: None,
160 dest_uf: None,
161 dest_cep: None,
162 valor_total: None,
163 valor_servicos: None,
164 valor_produtos: None,
165 valor_frete: None,
166 valor_desconto: None,
167 valor_liquido: None,
168 base_calculo: None,
169 aliquota: None,
170 valor_iss: None,
171 valor_icms: None,
172 valor_pis: None,
173 valor_cofins: None,
174 valor_ir: None,
175 valor_inss: None,
176 valor_csll: None,
177 valor_deducoes: None,
178 valor_outras_retencoes: None,
179 tributos_aproximados: None,
180 codigo_servico: None,
181 descricao_servico: None,
182 discriminacao_servico: None,
183 natureza_operacao: None,
184 regime_tributacao: None,
185 municipio_prestacao: None,
186 optante_simples: None,
187 incentivador_cultural: None,
188 iss_retido: None,
189 modal_transporte: None,
190 placa_veiculo: None,
191 uf_veiculo: None,
192 rntrc: None,
193 itens: Vec::new(),
194 texto_completo: String::new(),
195 }
196 }
197}
198
199pub fn extract_danfe_data(pdf_bytes: &[u8]) -> Result<DanfeData, String> {
201 let text = pdf_extract::extract_text_from_mem(pdf_bytes)
202 .map_err(|e| format!("Erro ao extrair texto do PDF: {}", e))?;
203
204 let mut data = DanfeData::default();
205 data.texto_completo = text.clone();
206
207 data.tipo_documento = detect_document_type(&text);
209
210 match data.tipo_documento {
212 TipoDocumentoFiscal::Nfse => extract_nfse_data(&text, &mut data),
213 TipoDocumentoFiscal::Nfe | TipoDocumentoFiscal::Nfce => extract_nfe_data(&text, &mut data),
214 TipoDocumentoFiscal::Cte => extract_cte_data(&text, &mut data),
215 TipoDocumentoFiscal::Mdfe => extract_mdfe_data(&text, &mut data),
216 TipoDocumentoFiscal::CfeSat => extract_cfe_data(&text, &mut data),
217 _ => {
218 extract_generic_data(&text, &mut data);
220 }
221 }
222
223 Ok(data)
224}
225
226fn detect_document_type(text: &str) -> TipoDocumentoFiscal {
228 let text_upper = text.to_uppercase();
229
230 if text_upper.contains("NFS-E")
232 || text_upper.contains("NOTA FISCAL DE SERVIÇO")
233 || text_upper.contains("NOTA FISCAL ELETRÔNICA DE SERVIÇO")
234 || text_upper.contains("NOTA FISCAL ELETRONICA DE SERVICO")
235 || (text_upper.contains("PRESTADOR DE SERVIÇO") && text_upper.contains("TOMADOR DE SERVIÇO"))
236 || text_upper.contains("ISSQN") {
237 return TipoDocumentoFiscal::Nfse;
238 }
239
240 if text_upper.contains("CT-E")
242 || text_upper.contains("CONHECIMENTO DE TRANSPORTE")
243 || text_upper.contains("DACTE") {
244 return TipoDocumentoFiscal::Cte;
245 }
246
247 if text_upper.contains("MDF-E")
249 || text_upper.contains("MANIFESTO ELETRÔNICO")
250 || text_upper.contains("DAMDFE") {
251 return TipoDocumentoFiscal::Mdfe;
252 }
253
254 if text_upper.contains("CF-E")
256 || text_upper.contains("CUPOM FISCAL ELETRÔNICO")
257 || text_upper.contains("SAT") && text_upper.contains("CUPOM") {
258 return TipoDocumentoFiscal::CfeSat;
259 }
260
261 if text_upper.contains("NFC-E")
263 || text_upper.contains("NOTA FISCAL DE CONSUMIDOR")
264 || text_upper.contains("DANFE NFC-E") {
265 return TipoDocumentoFiscal::Nfce;
266 }
267
268 if text_upper.contains("NFA-E")
270 || text_upper.contains("NOTA FISCAL AVULSA") {
271 return TipoDocumentoFiscal::Nfae;
272 }
273
274 if text_upper.contains("DANFE")
276 || text_upper.contains("NF-E")
277 || text_upper.contains("NOTA FISCAL ELETRÔNICA")
278 || text_upper.contains("DOCUMENTO AUXILIAR") {
279 return TipoDocumentoFiscal::Nfe;
280 }
281
282 TipoDocumentoFiscal::Desconhecido
283}
284
285fn extract_nfse_data(text: &str, data: &mut DanfeData) {
290 data.numero = extract_numero_nfse(text);
292
293 data.numero_rps = extract_numero_rps(text);
295
296 data.codigo_verificacao = extract_codigo_verificacao(text);
298
299 data.data_emissao = extract_data_hora(text);
301
302 let cnpjs = extract_all_cnpjs(text);
304 if !cnpjs.is_empty() {
305 data.emit_cnpj = Some(cnpjs[0].clone());
306 }
307
308 let cpfs = extract_all_cpfs(text);
310
311 data.emit_inscricao_municipal = extract_inscricao_municipal(text);
313
314 data.emit_razao_social = extract_empresa_name(text);
317
318 data.emit_nome_fantasia = extract_nome_fantasia(text);
320
321 let enderecos = extract_all_enderecos(text);
323 if !enderecos.is_empty() {
324 data.emit_endereco = Some(enderecos[0].clone());
325 if enderecos.len() > 1 {
326 data.dest_endereco = Some(enderecos[1].clone());
327 }
328 }
329
330 let ceps = extract_all_ceps(text);
332 if !ceps.is_empty() {
333 data.emit_cep = Some(ceps[0].clone());
334 if ceps.len() > 1 {
335 data.dest_cep = Some(ceps[1].clone());
336 }
337 }
338
339 data.emit_telefone = extract_telefone(text);
341
342 data.emit_email = extract_email(text);
344
345 let municipios = extract_municipios_uf(text);
347 if !municipios.is_empty() {
348 data.emit_municipio = Some(municipios[0].0.clone());
349 data.emit_uf = Some(municipios[0].1.clone());
350 if municipios.len() > 1 {
351 data.dest_municipio = Some(municipios[1].0.clone());
352 data.dest_uf = Some(municipios[1].1.clone());
353 }
354 }
355
356 data.dest_razao_social = extract_pessoa_nome(text);
358
359 if !cpfs.is_empty() {
361 data.dest_cpf = Some(cpfs[0].clone());
362 }
363
364 data.codigo_servico = extract_codigo_servico(text);
366
367 data.descricao_servico = extract_descricao_servico(text);
369
370 data.discriminacao_servico = extract_discriminacao(text);
372
373 if let Some(v) = extract_valor_servico_nfse(text) {
375 data.valor_servicos = Some(v);
376 data.valor_total = Some(v);
377 }
378
379 if data.valor_servicos.is_none() || data.valor_servicos == Some(0.0) {
381 if let Some(v) = extract_valor_pattern(text, &["Valor do Serviço", "Valor Serviço"]) {
382 if v > 0.0 {
383 data.valor_servicos = Some(v);
384 data.valor_total = Some(v);
385 }
386 }
387 }
388
389 if let Some(v) = extract_valor_pattern(text, &["Valor Líquido", "Valor Liquido", "(=) Valor Líquido"]) {
391 data.valor_liquido = Some(v);
392 }
393
394 if let Some(v) = extract_valor_pattern(text, &["Base de Cálculo", "Base Cálculo", "Base de Calculo"]) {
396 data.base_calculo = Some(v);
397 }
398
399 data.aliquota = extract_aliquota(text);
401
402 if let Some(v) = extract_valor_pattern(text, &["Valor do ISSQN", "Valor ISSQN", "Valor do ISS", "Valor ISS"]) {
404 data.valor_iss = Some(v);
405 }
406
407 data.tributos_aproximados = extract_tributos_aproximados(text);
409
410 data.natureza_operacao = extract_natureza_operacao(text);
412
413 data.regime_tributacao = extract_regime_tributacao(text);
415
416 data.municipio_prestacao = extract_local_prestacao(text);
418
419 data.optante_simples = extract_sim_nao(text, "Simples Nacional");
421 data.incentivador_cultural = extract_sim_nao(text, "Incentivador Cultural");
422 data.iss_retido = extract_sim_nao(text, "ISS Retido");
423}
424
425fn extract_nfe_data(text: &str, data: &mut DanfeData) {
430 data.chave_acesso = extract_chave_acesso(text);
432
433 data.numero = extract_numero_nfe(text);
435 data.serie = extract_serie(text);
436
437 data.data_emissao = extract_data_hora(text);
439
440 let cnpjs = extract_all_cnpjs(text);
442 if !cnpjs.is_empty() {
443 data.emit_cnpj = Some(cnpjs[0].clone());
444 }
445
446 data.emit_razao_social = extract_empresa_name(text);
447 data.emit_inscricao_estadual = extract_inscricao_estadual(text);
448
449 let enderecos = extract_all_enderecos(text);
451 if !enderecos.is_empty() {
452 data.emit_endereco = Some(enderecos[0].clone());
453 }
454
455 if let Some(v) = extract_valor_pattern(text, &["Valor Total", "VALOR TOTAL"]) {
457 data.valor_total = Some(v);
458 }
459 if let Some(v) = extract_valor_pattern(text, &["Valor Produtos", "VALOR PRODUTOS"]) {
460 data.valor_produtos = Some(v);
461 }
462}
463
464fn extract_cte_data(text: &str, data: &mut DanfeData) {
469 data.chave_acesso = extract_chave_acesso(text);
470 data.numero = extract_numero_nfe(text);
471 data.data_emissao = extract_data_hora(text);
472
473 data.modal_transporte = extract_modal_transporte(text);
475
476 data.placa_veiculo = extract_placa(text);
478
479 data.rntrc = extract_rntrc(text);
481
482 let cnpjs = extract_all_cnpjs(text);
483 if !cnpjs.is_empty() {
484 data.emit_cnpj = Some(cnpjs[0].clone());
485 }
486
487 data.emit_razao_social = extract_empresa_name(text);
488}
489
490fn extract_mdfe_data(text: &str, data: &mut DanfeData) {
495 data.chave_acesso = extract_chave_acesso(text);
496 data.numero = extract_numero_nfe(text);
497 data.data_emissao = extract_data_hora(text);
498
499 data.modal_transporte = extract_modal_transporte(text);
500 data.placa_veiculo = extract_placa(text);
501
502 let cnpjs = extract_all_cnpjs(text);
503 if !cnpjs.is_empty() {
504 data.emit_cnpj = Some(cnpjs[0].clone());
505 }
506}
507
508fn extract_cfe_data(text: &str, data: &mut DanfeData) {
513 data.chave_acesso = extract_chave_acesso(text);
514 data.data_emissao = extract_data_hora(text);
515
516 let cnpjs = extract_all_cnpjs(text);
517 if !cnpjs.is_empty() {
518 data.emit_cnpj = Some(cnpjs[0].clone());
519 }
520
521 if let Some(v) = extract_valor_pattern(text, &["Valor Total", "TOTAL"]) {
522 data.valor_total = Some(v);
523 }
524}
525
526fn extract_generic_data(text: &str, data: &mut DanfeData) {
531 extract_nfe_data(text, data);
532 extract_nfse_data(text, data);
533}
534
535fn extract_numero_nfse(text: &str) -> Option<i32> {
540 if let Some(m) = regex_find(text, r"FAZENDA\s*\n?\s*(\d{5,9})") {
542 if let Ok(n) = m.parse() {
543 return Some(n);
544 }
545 }
546
547 if let Some(m) = regex_find(text, r"(?i)N[úu]mero\s*(?:da)?\s*\n?\s*NFS-?e\s*\n?\s*(\d+)") {
549 if let Ok(n) = m.parse() {
550 return Some(n);
551 }
552 }
553
554 if let Some(m) = regex_find(text, r"\n\s*(\d{6})\s*\n") {
556 if let Ok(n) = m.parse() {
557 if n > 100000 {
558 return Some(n);
559 }
560 }
561 }
562
563 None
564}
565
566fn extract_numero_rps(text: &str) -> Option<i32> {
567 if let Some(m) = regex_find(text, r"(?i)(?:N[úu]mero\s*(?:do)?\s*)?RPS\s*\n?\s*(\d+)") {
568 if let Ok(n) = m.parse() {
569 return Some(n);
570 }
571 }
572
573 if let Ok(re) = Regex::new(r"\n\s*(\d{6})\s*\n") {
575 let matches: Vec<_> = re.find_iter(text).collect();
576 if matches.len() >= 2 {
577 if let Ok(n) = matches[1].as_str().trim().parse() {
578 return Some(n);
579 }
580 }
581 }
582
583 None
584}
585
586fn extract_codigo_verificacao(text: &str) -> Option<String> {
587 if let Some(m) = regex_find(text, r"\d{1,2}/\d{1,2}/\d{4}\s+([A-Z]{9})\b") {
589 return Some(m);
590 }
591
592 if let Ok(re) = Regex::new(r"\b([A-Z0-9]{8,12})\b") {
594 for cap in re.captures_iter(text) {
595 if let Some(m) = cap.get(1) {
596 let code = m.as_str();
597 if !is_common_word(code) && code.chars().any(|c| c.is_alphabetic()) {
599 return Some(code.to_string());
600 }
601 }
602 }
603 }
604
605 None
606}
607
608fn extract_data_hora(text: &str) -> Option<String> {
609 if let Some(m) = regex_find(text, r"(\d{2}/\d{2}/\d{4}\s+\d{2}:\d{2}:\d{2})") {
611 return Some(m);
612 }
613
614 if let Some(m) = regex_find(text, r"(\d{2}/\d{2}/\d{4}\s+\d{2}:\d{2})") {
616 return Some(m);
617 }
618
619 if let Some(m) = regex_find(text, r"(\d{2}/\d{2}/\d{4})") {
621 return Some(m);
622 }
623
624 None
625}
626
627fn extract_all_cnpjs(text: &str) -> Vec<String> {
628 let mut cnpjs = Vec::new();
629
630 if let Ok(re) = Regex::new(r"(\d{2}[.\s]?\d{3}[.\s]?\d{3}[/\s]?\d{4}[-\s]?\d{2})") {
631 for cap in re.captures_iter(text) {
632 if let Some(m) = cap.get(1) {
633 let cnpj: String = m.as_str().chars().filter(|c| c.is_ascii_digit()).collect();
634 if cnpj.len() == 14 && !cnpjs.contains(&cnpj) {
635 cnpjs.push(cnpj);
636 }
637 }
638 }
639 }
640
641 cnpjs
642}
643
644fn extract_all_cpfs(text: &str) -> Vec<String> {
645 let mut cpfs = Vec::new();
646
647 if let Ok(re) = Regex::new(r"(\d{3}[.\s]?\d{3}[.\s]?\d{3}[-\s]?\d{2})") {
648 for cap in re.captures_iter(text) {
649 if let Some(m) = cap.get(1) {
650 let cpf: String = m.as_str().chars().filter(|c| c.is_ascii_digit()).collect();
651 if cpf.len() == 11 && !cpfs.contains(&cpf) {
652 cpfs.push(cpf);
653 }
654 }
655 }
656 }
657
658 cpfs
659}
660
661fn extract_inscricao_municipal(text: &str) -> Option<String> {
662 if let Some(m) = regex_find(text, r"\d{2}\.\d{3}\.\d{3}/\d{4}-\d{2}\s+(\d{5,10})") {
664 return Some(m);
665 }
666
667 if let Some(m) = regex_find(text, r"(?i)Inscri[çc][ãa]o\s*Municipal[:\s]*(\d+)") {
668 return Some(m);
669 }
670
671 None
672}
673
674fn extract_inscricao_estadual(text: &str) -> Option<String> {
675 if let Some(m) = regex_find(text, r"(?i)(?:IE|Inscri[çc][ãa]o\s*Estadual)[:\s]*(\d[\d./-]+)") {
676 let ie: String = m.chars().filter(|c| c.is_ascii_digit()).collect();
677 if ie.len() >= 8 {
678 return Some(ie);
679 }
680 }
681 None
682}
683
684fn extract_empresa_name(text: &str) -> Option<String> {
685 let patterns = [
688 r"\n([A-Z][A-Z0-9\s]+LTDA)\s*\n",
690 r"([A-Z][A-Z0-9\s]*(?:MEIOS DE PAGAMENTO|SERVICOS|COMERCIO)[A-Z0-9\s]*LTDA)",
692 r"([A-Z][A-Z0-9\s]{5,}LTDA)",
694 r"([A-Z][A-Z0-9\s]+(?:S/?A|EIRELI|EPP))\b",
696 ];
697
698 for pattern in patterns {
699 if let Ok(re) = Regex::new(pattern) {
700 for cap in re.captures_iter(text) {
701 if let Some(m) = cap.get(1) {
702 let nome = m.as_str().trim();
703 if nome.len() > 10
705 && !nome.contains("PREFEITURA")
706 && !nome.contains("SECRETARIA")
707 && !nome.contains("NOTA FISCAL")
708 && !nome.contains("MUNICIPAL")
709 && !nome.contains("FAZENDA") {
710 return Some(nome.to_string());
711 }
712 }
713 }
714 }
715 }
716
717 None
718}
719
720fn extract_pessoa_nome(text: &str) -> Option<String> {
721 if let Ok(re) = Regex::new(r"\n([A-Z][A-Z]+(?:\s+[A-Z][A-Z]+){1,4})\s*\n") {
723 for cap in re.captures_iter(text) {
724 if let Some(m) = cap.get(1) {
725 let nome = m.as_str().trim();
726 if nome.len() > 5
727 && !nome.contains("LTDA")
728 && !nome.contains("CNPJ")
729 && !nome.contains("PREFEITURA")
730 && !nome.contains("SECRETARIA")
731 && !nome.contains("MUNICIPAL")
732 && !nome.contains("FISCAL")
733 && !nome.contains("NOTA")
734 && !nome.contains("SERVIÇO")
735 && !nome.contains("SERVICO")
736 && !nome.contains("TOMADOR")
737 && !nome.contains("PRESTADOR")
738 && !nome.contains("ATIVIDADE")
739 && !nome.contains("TRIBUT")
740 && nome.split_whitespace().count() >= 2
741 && nome.split_whitespace().count() <= 5 {
742 return Some(nome.to_string());
743 }
744 }
745 }
746 }
747
748 None
749}
750
751fn extract_nome_fantasia(text: &str) -> Option<String> {
752 if let Some(m) = regex_find(text, r"(?i)Nome\s*Fantasia\s*([A-Z][A-Z\s]+)") {
753 let nome = m.trim();
754 if nome.len() > 2 {
755 return Some(nome.to_string());
756 }
757 }
758 None
759}
760
761fn extract_all_enderecos(text: &str) -> Vec<String> {
762 let mut enderecos = Vec::new();
763
764 let patterns = [
765 r"((?:RUA|AVENIDA|AV\.|R\.|ALAMEDA|AL\.|TRAVESSA|TV\.)\s+[^\n]+CEP[:\s]*\d{5}-?\d{3})",
766 r"((?:RUA|AVENIDA|AV\.|R\.)\s+[A-Z][^\n]{10,60})",
767 ];
768
769 for pattern in patterns {
770 if let Ok(re) = Regex::new(&format!("(?i){}", pattern)) {
771 for cap in re.captures_iter(text) {
772 if let Some(m) = cap.get(1) {
773 let end = m.as_str().trim().to_string();
774 if !enderecos.contains(&end) {
775 enderecos.push(end);
776 }
777 }
778 }
779 }
780 }
781
782 enderecos
783}
784
785fn extract_all_ceps(text: &str) -> Vec<String> {
786 let mut ceps = Vec::new();
787
788 if let Ok(re) = Regex::new(r"(?i)CEP[:\s]*(\d{5}-?\d{3})") {
789 for cap in re.captures_iter(text) {
790 if let Some(m) = cap.get(1) {
791 let cep: String = m.as_str().chars().filter(|c| c.is_ascii_digit()).collect();
792 if cep.len() == 8 && !ceps.contains(&cep) {
793 ceps.push(cep);
794 }
795 }
796 }
797 }
798
799 ceps
800}
801
802fn extract_telefone(text: &str) -> Option<String> {
803 if let Some(m) = regex_find(text, r"\(?\d{2}\)?\s*\d{4,5}[-\s]?\d{4}") {
804 let tel: String = m.chars().filter(|c| c.is_ascii_digit()).collect();
805 if tel.len() >= 10 {
806 return Some(tel);
807 }
808 }
809 None
810}
811
812fn extract_email(text: &str) -> Option<String> {
813 regex_find(text, r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
814}
815
816fn extract_municipios_uf(text: &str) -> Vec<(String, String)> {
817 let mut municipios = Vec::new();
818
819 if let Ok(re) = Regex::new(r"([A-Z][A-Za-zÀ-ú\s]+)\s*-\s*([A-Z]{2})\b") {
820 for cap in re.captures_iter(text) {
821 if let (Some(mun), Some(uf)) = (cap.get(1), cap.get(2)) {
822 let mut mun_str = mun.as_str().trim().to_string();
823 let uf_str = uf.as_str().to_string();
824
825 if let Some(pos) = mun_str.rfind('\n') {
827 mun_str = mun_str[pos+1..].trim().to_string();
828 }
829
830 if mun_str.len() > 3
832 && !mun_str.contains("NOTA")
833 && !mun_str.contains("FISCAL")
834 && !mun_str.contains("SERVIÇO")
835 && !mun_str.contains("SERVICO")
836 && !mun_str.contains("SILVESTRINI") && !mun_str.contains("DRESSANO")
838 && mun_str.chars().filter(|c| c.is_whitespace()).count() < 3 && !municipios.iter().any(|(m, _)| m == &mun_str) {
840 municipios.push((mun_str, uf_str));
841 }
842 }
843 }
844 }
845
846 municipios
847}
848
849fn extract_codigo_servico(text: &str) -> Option<String> {
850 if let Some(m) = regex_find(text, r"(\d{1,2}\.\d{2}\s*/\s*\d{5,8})") {
852 return Some(m);
853 }
854 None
855}
856
857fn extract_descricao_servico(text: &str) -> Option<String> {
858 if let Some(m) = regex_find(text, r"\d{1,2}\.\d{2}\s*/\s*\d+\s*-\s*([^\n]+)") {
860 return Some(m.trim().to_string());
861 }
862 None
863}
864
865fn extract_discriminacao(text: &str) -> Option<String> {
866 if let Some(m) = regex_find(text, r"((?:PEDIDO|SERVICO|SERVIÇO)[:\s][^\n]+)") {
868 return Some(m.trim().to_string());
869 }
870 None
871}
872
873fn extract_valores_monetarios(text: &str) -> Vec<(f64, usize)> {
874 let mut valores = Vec::new();
875
876 if let Ok(re) = Regex::new(r"(\d{1,3}(?:\.\d{3})*,\d{2})") {
877 for (i, cap) in re.captures_iter(text).enumerate() {
878 if let Some(m) = cap.get(1) {
879 if let Some(v) = parse_valor_br(m.as_str()) {
880 valores.push((v, i));
881 }
882 }
883 }
884 }
885
886 valores
887}
888
889fn extract_valor_pattern(text: &str, labels: &[&str]) -> Option<f64> {
890 for label in labels {
891 let pattern = format!(r"(?i){}\s*R?\$?\s*(\d{{1,3}}(?:\.\d{{3}})*,\d{{2}})", regex::escape(label));
893 if let Some(m) = regex_find(text, &pattern) {
894 if let Some(v) = parse_valor_br(&m) {
895 return Some(v);
896 }
897 }
898
899 let pattern2 = format!(r"(?is){}\s*R?\$?\s*\n(?:[^\d\n]*\n)*?(\d{{1,3}}(?:\.\d{{3}})*,\d{{2}})", regex::escape(label));
901 if let Ok(re) = Regex::new(&pattern2) {
902 if let Some(caps) = re.captures(text) {
903 if let Some(m) = caps.get(1) {
904 if let Some(v) = parse_valor_br(m.as_str()) {
905 return Some(v);
906 }
907 }
908 }
909 }
910 }
911 None
912}
913
914fn extract_valor_servico_nfse(text: &str) -> Option<f64> {
915 if let Ok(re) = Regex::new(r"(?i)SERVICO[A-Z\s]+(\d+\.\d{2})") {
918 if let Some(caps) = re.captures(text) {
919 if let Some(m) = caps.get(1) {
920 if let Ok(v) = m.as_str().parse::<f64>() {
921 if v > 0.0 && v < 1_000_000.0 {
922 return Some(v);
923 }
924 }
925 }
926 }
927 }
928
929 if let Ok(re) = Regex::new(r"SERVICO[^\n]+?(\d{1,5}\.\d{2})") {
931 if let Some(caps) = re.captures(text) {
932 if let Some(m) = caps.get(1) {
933 if let Ok(v) = m.as_str().parse::<f64>() {
934 if v > 0.0 && v < 1_000_000.0 {
935 return Some(v);
936 }
937 }
938 }
939 }
940 }
941
942 if let Some(m) = regex_find(text, r"(?i)SERVICO[^\n]*?(\d{1,3}(?:\.\d{3})*,\d{2})") {
944 if let Some(v) = parse_valor_br(&m) {
945 if v > 0.0 {
946 return Some(v);
947 }
948 }
949 }
950
951 let valores: Vec<f64> = extract_all_monetary_values(text);
953 for v in valores.iter().rev() {
954 if *v >= 5.0 && *v < 10000.0 {
955 return Some(*v);
956 }
957 }
958
959 None
960}
961
962fn extract_all_monetary_values(text: &str) -> Vec<f64> {
963 let mut valores = Vec::new();
964
965 if let Ok(re) = Regex::new(r"(\d{1,3}(?:\.\d{3})*,\d{2})") {
966 for cap in re.captures_iter(text) {
967 if let Some(m) = cap.get(1) {
968 if let Some(v) = parse_valor_br(m.as_str()) {
969 valores.push(v);
970 }
971 }
972 }
973 }
974
975 valores
976}
977
978fn extract_aliquota(text: &str) -> Option<f64> {
979 if let Ok(re) = Regex::new(r"(?is)(?:Al[íi]quota|ISSQN).*?(\d,\d{2})\n") {
982 for cap in re.captures_iter(text) {
983 if let Some(m) = cap.get(1) {
984 if let Some(v) = parse_valor_br(m.as_str()) {
985 if v >= 2.0 && v <= 5.0 {
987 return Some(v);
988 }
989 }
990 }
991 }
992 }
993
994 if let Some(m) = regex_find(text, r"(?i)Al[íi]quota\s*%?\s*\n?[^\d]*(\d{1,2}[,.]?\d{0,2})\s*%?") {
996 if let Some(v) = parse_valor_br(&m) {
997 if v > 0.0 && v <= 25.0 && v != 0.16 {
998 return Some(v);
999 }
1000 }
1001 }
1002
1003 if let Ok(re) = Regex::new(r"\n(\d,\d{2})\n") {
1005 for cap in re.captures_iter(text) {
1006 if let Some(m) = cap.get(1) {
1007 if let Some(v) = parse_valor_br(m.as_str()) {
1008 if v >= 2.0 && v <= 5.0 {
1009 return Some(v);
1010 }
1011 }
1012 }
1013 }
1014 }
1015
1016 None
1017}
1018
1019fn extract_tributos_aproximados(text: &str) -> Option<f64> {
1020 if let Some(m) = regex_find(text, r"(?i)TRIBUTOS\s*(\d{1,3}[,.]?\d{0,2})\s*%") {
1021 return parse_valor_br(&m);
1022 }
1023 None
1024}
1025
1026fn extract_natureza_operacao(text: &str) -> Option<String> {
1027 if let Some(m) = regex_find(text, r"(?i)Natureza\s*Opera[çc][ãa]o\s*\n?\s*(\d+-[^\n]+)") {
1028 return Some(m.trim().to_string());
1029 }
1030 None
1031}
1032
1033fn extract_regime_tributacao(text: &str) -> Option<String> {
1034 if let Some(m) = regex_find(text, r"(?i)Regime\s*(?:Especial\s*)?Tributa[çc][ãa]o\s*\n?\s*(\d+-[^\n]+)") {
1035 return Some(m.trim().to_string());
1036 }
1037 None
1038}
1039
1040fn extract_local_prestacao(text: &str) -> Option<String> {
1041 if let Some(m) = regex_find(text, r"(?i)Local\s*(?:da\s*)?Presta[çc][ãa]o\s*([A-Z][A-Za-zÀ-ú\s]+-\s*[A-Z]{2})") {
1042 return Some(m.trim().to_string());
1043 }
1044 None
1045}
1046
1047fn extract_sim_nao(text: &str, label: &str) -> Option<bool> {
1048 let pattern = format!(r"(?i){}\s*\n?\s*\(?([X\s])\)?\s*Sim\s*\(?([X\s])\)?\s*N[ãa]o", regex::escape(label));
1049 if let Ok(re) = Regex::new(&pattern) {
1050 if let Some(cap) = re.captures(text) {
1051 if let Some(sim) = cap.get(1) {
1052 return Some(sim.as_str().contains('X'));
1053 }
1054 }
1055 }
1056
1057 let alt_pattern = format!(r"(?i){}\s*\n?\s*(\d)\s*-\s*(?:Sim|N[ãa]o)", regex::escape(label));
1059 if let Some(m) = regex_find(text, &alt_pattern) {
1060 return Some(m == "1");
1061 }
1062
1063 None
1064}
1065
1066fn extract_chave_acesso(text: &str) -> Option<String> {
1067 let digits: String = text.chars().filter(|c| c.is_ascii_digit()).collect();
1068
1069 let valid_ufs = [11, 12, 13, 14, 15, 16, 17, 21, 22, 23, 24, 25, 26, 27, 28, 29,
1070 31, 32, 33, 35, 41, 42, 43, 50, 51, 52, 53];
1071
1072 for i in 0..digits.len().saturating_sub(43) {
1073 let candidate = &digits[i..i+44];
1074 if let Ok(uf) = candidate[0..2].parse::<u32>() {
1075 if valid_ufs.contains(&uf) {
1076 if let Ok(modelo) = candidate[20..22].parse::<u32>() {
1077 if modelo == 55 || modelo == 65 || modelo == 57 || modelo == 58 {
1078 return Some(candidate.to_string());
1079 }
1080 }
1081 }
1082 }
1083 }
1084
1085 None
1086}
1087
1088fn extract_numero_nfe(text: &str) -> Option<i32> {
1089 let patterns = [
1090 r"(?i)N[º°úu][:.\s]*(\d{1,9})",
1091 r"(?i)NUMERO[:.\s]*(\d{1,9})",
1092 ];
1093
1094 for pattern in patterns {
1095 if let Some(m) = regex_find(text, pattern) {
1096 if let Ok(n) = m.parse() {
1097 return Some(n);
1098 }
1099 }
1100 }
1101
1102 None
1103}
1104
1105fn extract_serie(text: &str) -> Option<i16> {
1106 if let Some(m) = regex_find(text, r"(?i)S[ée]rie[:.\s]*(\d{1,3})") {
1107 if let Ok(s) = m.parse() {
1108 return Some(s);
1109 }
1110 }
1111 None
1112}
1113
1114fn extract_modal_transporte(text: &str) -> Option<String> {
1115 let modais = ["RODOVIÁRIO", "RODOVIARIO", "AÉREO", "AEREO", "AQUAVIÁRIO", "AQUAVIARIO",
1116 "FERROVIÁRIO", "FERROVIARIO", "DUTOVIÁRIO", "DUTOVIARIO"];
1117
1118 let text_upper = text.to_uppercase();
1119 for modal in modais {
1120 if text_upper.contains(modal) {
1121 return Some(modal.to_string());
1122 }
1123 }
1124
1125 None
1126}
1127
1128fn extract_placa(text: &str) -> Option<String> {
1129 if let Some(m) = regex_find(text, r"([A-Z]{3}-?\d{4})") {
1131 return Some(m);
1132 }
1133
1134 if let Some(m) = regex_find(text, r"([A-Z]{3}\d[A-Z]\d{2})") {
1136 return Some(m);
1137 }
1138
1139 None
1140}
1141
1142fn extract_rntrc(text: &str) -> Option<String> {
1143 if let Some(m) = regex_find(text, r"(?i)RNTRC[:.\s]*(\d{8,14})") {
1144 return Some(m);
1145 }
1146 None
1147}
1148
1149fn regex_find(text: &str, pattern: &str) -> Option<String> {
1154 if let Ok(re) = Regex::new(pattern) {
1155 if let Some(caps) = re.captures(text) {
1156 if let Some(m) = caps.get(1) {
1157 return Some(m.as_str().to_string());
1158 }
1159 }
1160 }
1161 None
1162}
1163
1164fn parse_valor_br(valor: &str) -> Option<f64> {
1165 let valor = valor.trim();
1166 if valor.contains(',') {
1167 let valor = valor.replace(".", "").replace(",", ".");
1168 valor.parse().ok()
1169 } else {
1170 valor.parse().ok()
1171 }
1172}
1173
1174fn is_common_word(word: &str) -> bool {
1175 let common = [
1176 "MUNICIPAL", "PREFEITURA", "SECRETARIA", "ARARAQUARA", "PIRACICABA",
1177 "PRESTADOR", "ATIVIDADE", "INCENTIVA", "SERVICOSP", "NACIONAL",
1178 ];
1179 common.contains(&word)
1180}
1181
1182#[cfg(test)]
1183mod tests {
1184 use super::*;
1185
1186 #[test]
1187 fn test_parse_valor_br() {
1188 assert_eq!(parse_valor_br("1.234,56"), Some(1234.56));
1189 assert_eq!(parse_valor_br("8,00"), Some(8.0));
1190 }
1191
1192 #[test]
1193 fn test_detect_nfse() {
1194 let text = "NOTA FISCAL ELETRÔNICA DE SERVIÇO - NFS-e";
1195 assert_eq!(detect_document_type(text), TipoDocumentoFiscal::Nfse);
1196 }
1197
1198 #[test]
1199 fn test_detect_cte() {
1200 let text = "DACTE - Conhecimento de Transporte";
1201 assert_eq!(detect_document_type(text), TipoDocumentoFiscal::Cte);
1202 }
1203}