lixum.labs / field notes / gimi-auditoria
← voltar
Data 02 de agosto de 2025 10 min de leitura

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.

python
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.

40k+Contratos processados
94%Match rate automático
R$2.4MDuplicatas identificadas
100%Python — zero Power Query

"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."

lixum.labs

Seus dados têm esse mesmo problema de reconciliação?

falar com a equipe
plugins premium WordPress