Skip to main content

nfe_web/pdf/
reader.rs

1//! Leitor de documentos fiscais em PDF
2//!
3//! Suporta: NF-e, NFC-e, NFS-e, CT-e, MDF-e, NFA-e, CF-e SAT
4
5use serde::{Deserialize, Serialize};
6use regex::Regex;
7
8/// Tipo de documento fiscal
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub enum TipoDocumentoFiscal {
11    #[serde(rename = "NFe")]
12    Nfe,           // Nota Fiscal Eletrônica (ICMS) - Modelo 55
13    #[serde(rename = "NFCe")]
14    Nfce,          // Nota Fiscal Consumidor Eletrônica - Modelo 65
15    #[serde(rename = "NFSe")]
16    Nfse,          // Nota Fiscal de Serviços Eletrônica (ISS)
17    #[serde(rename = "CTe")]
18    Cte,           // Conhecimento de Transporte Eletrônico
19    #[serde(rename = "MDFe")]
20    Mdfe,          // Manifesto Eletrônico de Documentos Fiscais
21    #[serde(rename = "NFAe")]
22    Nfae,          // Nota Fiscal Avulsa Eletrônica
23    #[serde(rename = "CFeSAT")]
24    CfeSat,        // Cupom Fiscal Eletrônico SAT
25    #[serde(rename = "Desconhecido")]
26    Desconhecido,
27}
28
29/// Dados extraídos do documento fiscal
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct DanfeData {
32    pub tipo_documento: TipoDocumentoFiscal,
33
34    // Identificação
35    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    // Emitente/Prestador
44    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    // Destinatário/Tomador
60    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    // Valores
71    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    // Serviço (NFS-e)
91    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    // Transporte (CT-e/MDF-e)
102    pub modal_transporte: Option<String>,
103    pub placa_veiculo: Option<String>,
104    pub uf_veiculo: Option<String>,
105    pub rntrc: Option<String>,
106
107    // Itens
108    pub itens: Vec<ItemDanfe>,
109
110    // Texto completo extraído
111    pub texto_completo: String,
112}
113
114/// Item extraído do documento
115#[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
199/// Extrai dados de um documento fiscal em PDF
200pub 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    // Detectar tipo de documento
208    data.tipo_documento = detect_document_type(&text);
209
210    // Extrair dados baseado no tipo
211    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            // Tentar extrair o máximo possível
219            extract_generic_data(&text, &mut data);
220        }
221    }
222
223    Ok(data)
224}
225
226/// Detecta o tipo de documento fiscal
227fn detect_document_type(text: &str) -> TipoDocumentoFiscal {
228    let text_upper = text.to_uppercase();
229
230    // NFS-e (Nota Fiscal de Serviços)
231    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    // CT-e (Conhecimento de Transporte)
241    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    // MDF-e (Manifesto de Documentos Fiscais)
248    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    // CF-e SAT
255    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    // NFC-e (Nota Fiscal Consumidor)
262    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    // NFA-e (Nota Fiscal Avulsa)
269    if text_upper.contains("NFA-E")
270        || text_upper.contains("NOTA FISCAL AVULSA") {
271        return TipoDocumentoFiscal::Nfae;
272    }
273
274    // NF-e (Nota Fiscal Eletrônica)
275    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
285// ============================================================================
286// Extração de NFS-e
287// ============================================================================
288
289fn extract_nfse_data(text: &str, data: &mut DanfeData) {
290    // Número da NFS-e - procurar após "FAZENDA" ou padrão de 6 dígitos isolado
291    data.numero = extract_numero_nfse(text);
292
293    // Número do RPS
294    data.numero_rps = extract_numero_rps(text);
295
296    // Código de verificação (9 caracteres alfanuméricos)
297    data.codigo_verificacao = extract_codigo_verificacao(text);
298
299    // Data de emissão
300    data.data_emissao = extract_data_hora(text);
301
302    // Extrair todos os CNPJs do texto
303    let cnpjs = extract_all_cnpjs(text);
304    if !cnpjs.is_empty() {
305        data.emit_cnpj = Some(cnpjs[0].clone());
306    }
307
308    // Extrair todos os CPFs do texto
309    let cpfs = extract_all_cpfs(text);
310
311    // Inscrição Municipal
312    data.emit_inscricao_municipal = extract_inscricao_municipal(text);
313
314    // Razão Social do Prestador (empresa com LTDA, S/A, etc)
315    // Procurar todas as empresas no texto e pegar a que tem LTDA
316    data.emit_razao_social = extract_empresa_name(text);
317
318    // Nome Fantasia
319    data.emit_nome_fantasia = extract_nome_fantasia(text);
320
321    // Endereços
322    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    // CEPs
331    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    // Telefone
340    data.emit_telefone = extract_telefone(text);
341
342    // Email
343    data.emit_email = extract_email(text);
344
345    // Município do Prestador
346    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    // Tomador - nome de pessoa (antes do CPF)
357    data.dest_razao_social = extract_pessoa_nome(text);
358
359    // CPF do Tomador
360    if !cpfs.is_empty() {
361        data.dest_cpf = Some(cpfs[0].clone());
362    }
363
364    // Código do Serviço
365    data.codigo_servico = extract_codigo_servico(text);
366
367    // Descrição do Serviço
368    data.descricao_servico = extract_descricao_servico(text);
369
370    // Discriminação do Serviço
371    data.discriminacao_servico = extract_discriminacao(text);
372
373    // Valor do serviço - tentar extração específica primeiro
374    if let Some(v) = extract_valor_servico_nfse(text) {
375        data.valor_servicos = Some(v);
376        data.valor_total = Some(v);
377    }
378
379    // Se não encontrou, tentar padrão genérico (ignorar valores zero)
380    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    // Valor líquido
390    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    // Base de cálculo
395    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    // Alíquota
400    data.aliquota = extract_aliquota(text);
401
402    // Valor do ISS
403    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    // Tributos aproximados
408    data.tributos_aproximados = extract_tributos_aproximados(text);
409
410    // Natureza da Operação
411    data.natureza_operacao = extract_natureza_operacao(text);
412
413    // Regime de Tributação
414    data.regime_tributacao = extract_regime_tributacao(text);
415
416    // Local da Prestação
417    data.municipio_prestacao = extract_local_prestacao(text);
418
419    // Opções booleanas
420    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
425// ============================================================================
426// Extração de NF-e / NFC-e
427// ============================================================================
428
429fn extract_nfe_data(text: &str, data: &mut DanfeData) {
430    // Chave de acesso (44 dígitos)
431    data.chave_acesso = extract_chave_acesso(text);
432
433    // Número e série
434    data.numero = extract_numero_nfe(text);
435    data.serie = extract_serie(text);
436
437    // Data de emissão
438    data.data_emissao = extract_data_hora(text);
439
440    // Dados do emitente
441    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    // Endereço
450    let enderecos = extract_all_enderecos(text);
451    if !enderecos.is_empty() {
452        data.emit_endereco = Some(enderecos[0].clone());
453    }
454
455    // Valores
456    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
464// ============================================================================
465// Extração de CT-e
466// ============================================================================
467
468fn 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    // Modal de transporte
474    data.modal_transporte = extract_modal_transporte(text);
475
476    // Placa do veículo
477    data.placa_veiculo = extract_placa(text);
478
479    // RNTRC
480    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
490// ============================================================================
491// Extração de MDF-e
492// ============================================================================
493
494fn 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
508// ============================================================================
509// Extração de CF-e SAT
510// ============================================================================
511
512fn 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
526// ============================================================================
527// Extração Genérica
528// ============================================================================
529
530fn extract_generic_data(text: &str, data: &mut DanfeData) {
531    extract_nfe_data(text, data);
532    extract_nfse_data(text, data);
533}
534
535// ============================================================================
536// Funções de Extração Específicas
537// ============================================================================
538
539fn extract_numero_nfse(text: &str) -> Option<i32> {
540    // Procurar número após "FAZENDA" (padrão comum em NFS-e municipal)
541    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    // Procurar "Número da NFS-e"
548    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    // Número isolado de 6 dígitos após quebra de linha
555    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    // Segundo número de 6 dígitos (geralmente RPS)
574    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    // Procurar código de 9 letras após data
588    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    // Código alfanumérico de 8-12 caracteres
593    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                // Filtrar palavras comuns
598                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    // Data com hora: DD/MM/YYYY HH:MM:SS
610    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    // Data com hora sem segundos
615    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    // Apenas data
620    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    // Procurar número de 7 dígitos após CNPJ
663    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    // Procurar nome de empresa com LTDA especificamente (mais confiável)
686    // Prioriza empresas com sufixo corporativo
687    let patterns = [
688        // Padrão específico para empresa com LTDA
689        r"\n([A-Z][A-Z0-9\s]+LTDA)\s*\n",
690        // Empresa com "MEIOS DE PAGAMENTO" ou similar + LTDA
691        r"([A-Z][A-Z0-9\s]*(?:MEIOS DE PAGAMENTO|SERVICOS|COMERCIO)[A-Z0-9\s]*LTDA)",
692        // Empresa genérica com LTDA
693        r"([A-Z][A-Z0-9\s]{5,}LTDA)",
694        // S/A, EIRELI, etc
695        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                    // Filtrar falsos positivos
704                    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    // Procurar nome de pessoa (2-4 palavras em maiúscula, sem termos técnicos)
722    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                // Remover texto antes de quebra de linha (ex: "NOME\n CIDADE" -> "CIDADE")
826                if let Some(pos) = mun_str.rfind('\n') {
827                    mun_str = mun_str[pos+1..].trim().to_string();
828                }
829
830                // Filtrar falsos positivos
831                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")  // Filtrar nomes próprios
837                    && !mun_str.contains("DRESSANO")
838                    && mun_str.chars().filter(|c| c.is_whitespace()).count() < 3  // Máx 2 palavras
839                    && !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    // Padrão: XX.XX / XXXXXXX
851    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    // Descrição após código do serviço
859    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    // Procurar texto com "PEDIDO:" ou similar
867    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        // Padrão 1: valor na mesma linha
892        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        // Padrão 2: valor em linhas posteriores (comum em NFS-e)
900        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    // Padrão 1: "SERVICO ... X.XX" (formato com ponto US - decimal separator)
916    // Procura número no formato X.XX após SERVICO
917    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    // Padrão 2: Procurar "X.XX" isolado após SERVICO (formato US)
930    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    // Padrão 3: "SERVICO" seguido de valor BR (8,00) na mesma linha
943    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    // Padrão 4: Último valor maior que 5 no documento
952    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    // Padrão 1: Procurar valor típico de alíquota (1-5%) após contexto de ISSQN
980    // Valores típicos: 2,00  3,00  5,00
981    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                    // Alíquota típica de ISS é entre 2% e 5%
986                    if v >= 2.0 && v <= 5.0 {
987                        return Some(v);
988                    }
989                }
990            }
991        }
992    }
993
994    // Padrão 2: Procurar valor entre 1 e 25 em contexto de porcentagem
995    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    // Padrão 3: Procurar "2,00" ou similar que é valor típico de alíquota
1004    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    // Padrão alternativo: "2 - Não"
1058    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    // Placa padrão antigo: ABC-1234
1130    if let Some(m) = regex_find(text, r"([A-Z]{3}-?\d{4})") {
1131        return Some(m);
1132    }
1133
1134    // Placa Mercosul: ABC1D23
1135    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
1149// ============================================================================
1150// Funções Auxiliares
1151// ============================================================================
1152
1153fn 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}