from __future__ import annotations import io import re import unicodedata as ud from pathlib import Path from typing import Dict, Union, Any import pandas as pd import pdfplumber ############################################################################### # Normalização # ############################################################################### _DASH_CHARS = "\u2010\u2011\u2012\u2013\u2014\u2212" _NBSP_CHARS = "\u00A0\u202F\u2007" _dash_trans = str.maketrans({c: "-" for c in _DASH_CHARS}) _nbsp_trans = str.maketrans({c: " " for c in _NBSP_CHARS}) def _normalize_line(s: str) -> str: s = s.translate(_dash_trans).translate(_nbsp_trans) s = ud.normalize("NFKC", s) return re.sub(r"\s{2,}", " ", s.strip()) def _parse_money(value: str) -> float | None: value = value.strip() if value == "-": return None return float(value.replace(".", "").replace(",", ".")) ############################################################################### # Regex – Cabeçalhos # ############################################################################### RX_MSISDN_HEADER = re.compile(r"Vantagens que seu plano oferece:\s*(?P\d{2}\s\d{5}-\d{4})", re.I) RX_MSISDN_SEU_NUM = re.compile(r"SEU\s+NÚMERO\s+TIM\s+(?P\d{2}\s\d{5}-\d{4})", re.I) RX_MSISDN_DETALHE = re.compile(r"Detalhamento de Serviços\s+N[°º]\s*(?P\d{2}\s\d{5}-\d{4})", re.I) SECTION_HEADERS: Dict[str, str] = { "IGNORE Ilimitados": r"^Detalhamento de Serviços Ilimitados", "IGNORE Detalhamento": r"^Detalhamento de Serviç[io]s\b", "Fatura Resumo": r"^FATURA\s+RESUMO", "DANFE-COM": r"^DANFE-COM\b", "Plano": r"^Plano\b", "Mensalidades Adicionais": r"^MENSALIDADES\s+ADICIONAIS", "Itens Eventuais": r"^ITENS\s+EVENTUAIS", "TIM Viagem": r"^TIM\s+VIAGEM", "SVA Detalhe Total": r"^Serviços\s+de\s+Valor\s+Adicionado\s+Total", "Desconto Franquia": r"^Desconto\(s\)\s+Franquia", "Desconto SVA": r"^Desconto\(s\)\s+Serviç[io]s", "Franquia": r"^Franquia\s*\(s\)", "SVA": r"^Serviços\s+de\s+valor\s+adicionado\(SVA\)", "Chamadas Rede TIM": r"^CHAMADAS\s+DENTRO\s+DA\s+REDE\s+TIM", "Chamadas Fora Rede TIM": r"^CHAMADAS\s+FORA\s+DA\s+REDE\s+TIM", "Outros Valores": r"^OUTROS\s+VALORES", "Deduções": r"^DEDUÇÕES", "Roaming Internacional": r"^ROAMING\s+INTERNACIONAL", "Cobranças de Terceiros": r"^COBRANÇAS\s+DE\s+TERCEIROS", "Débitos de outras operadoras": r"^DÉBITOS\s+DE\s+OUTRAS\s+OPERADORAS" } SECTION_REGEX = {k: re.compile(v, re.I) for k, v in SECTION_HEADERS.items()} RX_SECTION_TOTAL = re.compile(r"(?:R\$\s*)?(-?[\d\.]+,\d{2})") # Patterns que encerram a seção atual sem iniciar uma nova RX_SECTION_BREAK = re.compile( r"^(Nota Fiscal de Servi|SEUS\s+DADOS|ITEM\s+QTDE\s+ICMS|TOTAL\s+TIM|" r"Ficou\s+com\s+dúvidas|Bancos\s+Conveniados|Reservado\s+ao\s+fisco|" r"Tipo:\s+N\s+-\s+Normal)", re.I, ) ############################################################################### # Regex – Linhas # ############################################################################### RX_PERIOD = r"(?P(?:\d{2}/\d{2}\s+a\s+\d{2}/\d{2}|-))" RX_DAYS = r"(?P(?:\d+|-))" RX_VALUE = r"(?P-?\d+,\d{2}|Incluído)" RX_PARCEL = r"(?P(?:\d+/\d+|-))" # --- Plano / Desconto / Chamadas (layout padrão: QTY DESC PARCELA PERIOD DIAS VALOR) --- RX_SIMPLE = re.compile(rf"^\s*(?P\d+)\s+(?P.+?)\s+{RX_PARCEL}\s+{RX_PERIOD}\s+{RX_DAYS}\s+{RX_VALUE}\s*$") RX_CONSUMPTION = re.compile(rf"^\s*(?P\d+)\s+(?P.+?)\s+{RX_PARCEL}\s+(?PIlimitado|-)\s+(?P\d{{1,3}}m\d{{2}}s)\s+{RX_PERIOD}\s+{RX_DAYS}\s+{RX_VALUE}\s*$") RX_SUBTOTAL = re.compile(r"^Subtotal\s+(?P-?\d+,\d{2})\s*$", re.I) RX_DETAIL = re.compile(r"^\d+\s+.+?\s+(-?\d+,\d{2})\s*$") # Fatura Resumo RX_RESUMO = re.compile(r"^\s*(?P.+?)\s+R\$\s*(?P-?\d+,\d{2})\s*$") RX_TOTAL_GERAL = re.compile(r"^Total\s+geral\s+R\$\s*(?P-?\d+,\d{2})", re.I) RX_FATURA_METADATA = re.compile( r"FATURA\s+PER[ÍI]ODO\s+EMISS[ÃA]O\s+POSTAGEM\s+" r"(?P\S+)\s+" r"(?P\d{2}/\d{2}\s+a\s+\d{2}/\d{2})\s+" r"(?P\d{2}/\d{2}/\d{4})\s+" r"(?P\d{2}/\d{2}/\d{4})", re.I, ) RX_DANFE_TOTAL = re.compile(r"^Total\s+geral\s+R\$\s*(?P-?\d+(?:\.\d{3})*,\d{2})", re.I) RX_DANFE_ROW = re.compile( r"^(?P.+?)\s+" r"(?P[A-Z]{2})\s+" r"(?P\d+(?:,\d+)?)\s+" r"(?P-?\d+(?:\.\d{3})*,\d{2})\s+" r"(?P-?\d+(?:\.\d{3})*,\d{2}|-)\s+" r"(?P-?\d+(?:\.\d{3})*,\d{2}|-)\s+" r"(?P\d+(?:,\d+)?%|-)\s+" r"(?P-?\d+(?:\.\d{3})*,\d{2}|-)\s+" r"(?P-?\d+(?:\.\d{3})*,\d{2})$" ) # --- Itens Eventuais (layout: QTY DESC PARCELA FRANQUIA CONSUMO PERIODO DIAS VALOR) --- # Com consumo real (ex: 19,77GB) ou consumo zerado ("0") RX_EVENTUAIS = re.compile( r"^\s*(?P\d+)\s+(?P.+?)\s+-\s+-\s+" r"(?P(?:\S+(?:GB|MB|KB)|0))\s+-\s+-\s+" r"(?P-?\d+,\d{2})\s*$" ) # Sem consumo (todos "-") RX_EVENTUAIS_NO_CONS = re.compile( r"^\s*(?P\d+)\s+(?P.+?)" r"(?:\s+-){5}\s+" r"(?P-?\d+,\d{2})\s*$" ) # --- SVA Detalhe Total (layout: # DATA HORA ORIGEM DESC NUMERO TIPO PACOTE - - VALOR) --- # Exemplo: 1 09/04/25 - 02:54:18 RJ AREA 21 TIM Saude Mensal 00700001003511 N FP - - 14,99 RX_SVA_DETAIL = re.compile( r"^\s*(?P\d+)\s+" r"(?P\d{2}/\d{2}/\d{2})\s+-\s+(?P