40 mil contratos, 3 fontes e um pipeline em Python
A GIMI Consultoria tinha um problema que boa parte das empresas financeiras tem mas poucas assumem: ninguém sabia ao certo quantos contratos existiam. A API interna dizia um número. O bucket S3 com os PDFs dizia outro. A planilha do gestor dizia um terceiro. Qual era a fonte da verdade?

[ as três fontes ]
Cada fonte tinha sido criada em um momento diferente da empresa, por equipes diferentes, com convenções diferentes.
A API interna era o sistema operacional — registrava contratos ativos. Mas não registrava contratos encerrados antes de 2021, que tinham sido migrados para um sistema legado e depois "arquivados".
O bucket S3 tinha os PDFs de todos os contratos, incluindo os legados. Mas o nome do arquivo seguia uma convenção que mudou três vezes. E alguns PDFs foram renomeados manualmente por alguém que saiu da empresa.
A planilha era a tentativa humana de reconciliar as duas fontes anteriores. Incompleta, com filtros salvos que excluíam linhas sem o operador perceber.
[ a abordagem: reconciliação por hash ]
A tentativa inicial foi cruzar por número de contrato. Não funcionou — a API usava um formato, o S3 usava outro, e a planilha tinha uma coluna de "número legado" que não batia com nenhum dos dois.
A solução foi mais bruta e mais robusta: extraímos os metadados de cada PDF via pdfplumber, normalizamos CPF/CNPJ do tomador, data de emissão e valor do contrato, e geramos um hash composto. O mesmo hash foi gerado para cada linha da API e cada linha da planilha.
Qualquer registro que compartilhasse o mesmo hash era o mesmo contrato. 94% dos registros bateram. Os 6% restantes foram auditados manualmente.
import hashlib
from pdfplumber import open as open_pdf
def extract_contract_hash(pdf_path: str) -> str | None:
try:
with open_pdf(pdf_path) as pdf:
text = pdf.pages[0].extract_text() or ""
cpf_cnpj = normalize_document(extract_field(text, r"CPF/CNPJ[:\s]+([\d\.\-\/]+)"))
date = normalize_date(extract_field(text, r"Data de Emissão[:\s]+(\d{2}/\d{2}/\d{4})"))
value = normalize_currency(extract_field(text, r"Valor[:\s]+R\$\s?([\d\.\,]+)"))
if not all([cpf_cnpj, date, value]):
return None # metadados insuficientes — vai para revisão manual
raw = f"{cpf_cnpj}|{date}|{value}"
return hashlib.sha256(raw.encode()).hexdigest()[:16]
except Exception:
return None
Extração e normalização de metadados do PDF para geração do hash de reconciliação
[ o que os 6% revelaram ]
Os contratos que não bateram não eram todos erros. Três categorias distintas:
1. Contratos renegociados — existiam como dois registros diferentes (contrato original + aditivo), mas o sistema os tratava como um só. 2. PDFs corrompidos — alguns arquivos no S3 tinham os primeiros 4 bytes danificados, o que quebrava o parser mas não impedia a leitura humana. 3. Contratos genuinamente duplicados — o mesmo contrato foi digitado duas vezes na planilha por pessoas diferentes, em datas diferentes, com pequenas variações de valor (provavelmente erro de arredondamento na digitação manual).
Essa terceira categoria foi a descoberta mais importante para o cliente. Havia R$ 2.4M em contratos duplicados no sistema.
"Dado "completo" é uma ilusão operacional. Toda empresa que tem mais de uma fonte de dados tem inconsistências — a questão é se alguém já mediu o tamanho do problema. A maioria não mediu porque medir dói."
Seus dados têm esse mesmo problema de reconciliação?