Pular para conteúdo

Documentação Completa — BI Transporte

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-10

O sistema como ele é hoje, em ordem de raciocínio: o problema, o que entra, como os dados são guardados, como o processamento funciona e, no fim, os contratos que outros sistemas consomem. Quem só integra pode pular direto para o cap. 7. O histórico ("como chegamos aqui") está no apêndice; o passo a passo de cada feature, nas Etapas do Projeto.

1. Contexto e problema

A Vantroba extrai do X-Adm, periodicamente, planilhas Excel com o faturamento e o movimento de frota do seu transporte. Antes, transformar esses arquivos exportados em dados consultáveis dependia de alguém rodar um script Python à mão — frágil, sem histórico, sem rastreabilidade de "qual arquivo gerou qual dado".

O BI Transporte substitui esse processo por um serviço contínuo (Java/Micronaut): recebe as planilhas por uma API autenticada — um .zip com 1..N .xlsx do ciclo —, valida, processa de forma assíncrona e persiste os fatos no PostgreSQL, de onde os clientes móveis leem (via PowerSync) e a equipe consulta (telas web e API). Cada planilha fica registrada com checksum, status e métricas — auditável.

Fora do escopo: editar os dados pela web (a origem é sempre a planilha) e enviar a planilha por tela (o envio é só por API). Relatórios e dashboards são de quem consome os dados, não deste serviço.

2. Dados de entrada

A entrada é um .zip com 1..N arquivos .xlsx exportados do X-Adm — o zip é o lote (decisão 0032). Cada .xlsx tem três abas de nome fixo — "10", "20" e "30" — e traz o próprio período de referência, na aba 10 (decisão 0016); o cliente não informa período no envio. Cada arquivo é validado por magic bytes ZIP antes de qualquer leitura; abas com outro nome são ignoradas. A leitura é FastExcel (StAX, streaming) — arquivos grandes não estouram a memória; escolhida no lugar do POI para compilar em native-image (decisão 0026). O layout coluna-a-coluna é contrato com o X-Adm (decisão 0003), fixado no parser e em fixtures golden.

📎 Arquivo de referência: RESULTADO_TRANSPORTE_0100_072025.xlsx — um arquivo real, com as 3 abas no formato abaixo, em docs/anexos/privado/ no Forgejo (docs/anexos/privado/RESULTADO_TRANSPORTE_0100_072025.xlsx). Arquivo confidencial — acesso restrito à equipe (só com login no Forgejo).

Aba 10 — período de referência

Só a primeira linha importa: célula E1 (data inicial) e F1 (data final), formato dd/MM/yyyy. É o período que delimita o diff-aware (decisão 0016).

Célula Conteúdo Formato
E1 data inicial do período dd/MM/yyyy
F1 data final do período dd/MM/yyyy

Aba 20 — faturamento (uma linha por lançamento)

Colunas por letra do Excel (índice 0-based entre parênteses). Linha descartada se dt_frete ou valor vier vazia.

Coluna Campo Formato / regra
B (1) classif_ctb texto (3)
C (2) ident_ger texto (2)
D (3) nro_frete texto (6)
E (4) dt_frete data dd/MM/yyyy — obrigatória
F (5) nro_docto texto (6)
G (6) serie texto (3)
H (7) dt_em data dd/MM/yyyy
I (8) db_cr texto (1)
J (9) tanque texto (1)
K (10) placa texto (10)
L (11) placa_comp texto (10)
M (12) segmento texto (3)
N (13) desc_seg texto (30)
O (14) seq_proj texto (6)
P (15) cod_proj texto (10)
Q (16) filial texto (2)
R (17) valor decimal — obrigatório
S (18) fator_fat decimal
T (19) distancia decimal
U (20) espec_veic texto (10)
V (21) mun_orig texto (30)
W (22) mun_dest texto (30)
X (23) cod_mot texto (6)
Y (24) nome_mot texto (30)
Z (25) cpf_mot texto (11)
AA (26) gestor texto (6)
AB (27) nome_gestor texto (30)
AC (28) desc_classif texto (70)
AH (33) tipo_frota texto (20)
AI (34) marca_veiculo texto (80)
AJ (35) modelo_veiculo texto (80)
AK (36) ano AAAA/AAAA → ano_construcao / ano_modelo

Aba 30 — movimento de frota (uma linha por lançamento)

Linha descartada se data_lcto ou valor vier vazia.

Coluna Campo Formato / regra
B (1) classif_ctb texto (3)
C (2) ident_ger texto (2)
D (3) ident_custo texto (1)
E (4) atrib_custo texto (1)
F (5) ident_rat texto (1)
G (6) db_cr texto (1)
H (7) tanque texto (1)
I (8) placa texto (10)
J (9) placa_comp texto (10)
K (10) segmento texto (3)
L (11) desc_seg texto (30)
M (12) seq_proj texto (6)
N (13) cod_proj texto (10)
O (14) filial texto (2)
P (15) valor decimal — obrigatório
Q (16) data_lcto data dd/MM/yyyy — obrigatória
R (17) espec_veic texto (10)
S (18) cod_conta texto (5)
T (19) classif_conta texto (18)
U (20) desc_conta texto (70)
V (21) desc_classif texto (70)
AA (26) processo texto (6)
AB (27) tipo_frota texto (20)
AC (28) marca_veiculo texto (80)
AD (29) modelo_veiculo texto (80)
AE (30) ano AAAA/AAAA → ano_construcao / ano_modelo

Entrada veicular malformada (marca/modelo/ano/placa) não derruba o lote — vira métrica de defesa (etapa 04).

3. Modelo de dados

O schema abaixo é a fonte da verdade — toda migration que evolui o banco atualiza este modelo no mesmo PR.

O schema vive em PostgreSQL, versionado por Flyway (V1..VN, congeladas por checksum — nunca editar uma aplicada, sempre uma nova). Duas famílias de tabela, por prefixo, que é contrato com o PowerSync:

  • bi_* — fatos e configuração sincronizados para os clientes móveis via PowerSync (exigem GRANT SELECT ao powersync_role, decisão 0013).
  • xls_* — controle e auditoria locais ao backend, nunca sincronizados.

Tabelas e relacionamentos

erDiagram
    xls_processamento ||--o{ bi_faturamento : "mescla por período"
    xls_processamento ||--o{ bi_movimento : "mescla por período"
    xls_conjunto_lote ||--o{ xls_processamento : "agrupa o lote .zip"
    xls_conjunto_lote {
        varchar conjunto_id PK
    }
    bi_faturamento {
        bigint id PK
    }
    bi_movimento {
        bigint id PK
    }
    xls_processamento {
        bigint id PK
        varchar checksum_sha256 UK
    }
    bi_configuracao {
        text id PK
    }
    xls_resetar_powersync {
        bigint id PK
    }

Sem FK físicas. A relação xls_processamento → bi_* é por período (periodo_inicial/periodo_final, lidos da aba 10), não por chave estrangeira: um upload mescla os fatos do seu período (UPSERT diff-aware), não os "possui". placa liga fato e dados veiculares de forma desnormalizada — não há tabela mestre de frota. As tabelas xls_* são controle/auditoria; xls_processamento.conjunto_id aponta o lote em xls_conjunto_lote (V16), também sem FK física.

Fluxo de dados

flowchart TD
    xadm[X-Adm] -->|exporta .xlsx → .zip| up["POST /api/xls/processar (.zip)"]
    up -->|binário| store[("Garage / arquivo_bytes")]
    up -->|registra upload| ctrl[("xls_processamento")]
    up --> sax["ExcelProcessor (FastExcel/StAX)"]
    sax -->|aba 20| fat[/faturamento/]
    sax -->|aba 30| mov[/movimento/]
    fat --> upsert["UPSERT diff-aware + DELETE seletivo"]
    mov --> upsert
    upsert --> bif[("bi_faturamento")]
    upsert --> bim[("bi_movimento")]
    upsert -->|métricas| ctrl
    bif -->|PowerSync| cli[Clientes móveis]
    bim -->|PowerSync| cli
    cfg[("bi_configuracao")] -->|PowerSync| cli

Detalhe de cada tabela

Dicionário por tabela (coluna, tipo, chave, nulo?, desde, nota). A chave natural composta, os índices e os CHECK vêm em bullets abaixo de cada tabela — é onde a chave de várias colunas fica legível (o ER acima só dá a topologia). Desde vazio = coluna original da tabela.

bi_faturamento — fatos de faturamento (aba 20) · sincronizada

Coluna Tipo Chave Nulo? Desde Nota
id bigint PK NOT NULL
classif_ctb varchar
ident_ger varchar
nro_frete varchar parte da chave natural
dt_frete date parte da chave natural
nro_docto varchar parte da chave natural
serie varchar
dt_em date parte da chave natural
db_cr varchar
tanque varchar
placa varchar parte da chave natural
placa_comp varchar
segmento varchar parte da chave natural
desc_seg varchar
seq_proj varchar parte da chave natural
cod_proj varchar parte da chave natural
filial varchar
valor decimal
fator_fat decimal
distancia decimal
espec_veic varchar parte da chave natural
mun_orig varchar
mun_dest varchar
cod_mot varchar parte da chave natural
nome_mot varchar
cpf_mot varchar
gestor varchar
nome_gestor varchar
desc_classif varchar
tipo_frota varchar V3 veicular
marca_veiculo varchar V3 veicular
modelo_veiculo varchar V3 veicular
ano_construcao smallint V3 veicular
ano_modelo smallint V3 veicular
  • Chave natural (UK) uq_bi_faturamento_natural — 10 colunas (NULLS NOT DISTINCT, V6): nro_frete, dt_frete, nro_docto, dt_em, placa, segmento, seq_proj, cod_proj, espec_veic, cod_mot. É a chave do UPSERT diff-aware.
  • Índices (V2): dt_frete, dt_em, placa, filial, cod_proj.
  • Veiculares (tipo_frota…ano_modelo) adicionadas em V3; demais em V1.

bi_movimento — movimento de frota (aba 30) · sincronizada

Coluna Tipo Chave Nulo? Desde Nota
id bigint PK NOT NULL
classif_ctb varchar parte da chave natural
ident_ger varchar parte da chave natural
ident_custo varchar parte da chave natural
atrib_custo varchar parte da chave natural
ident_rat varchar parte da chave natural
db_cr varchar parte da chave natural
tanque varchar
placa varchar parte da chave natural
placa_comp varchar
segmento varchar parte da chave natural
desc_seg varchar
seq_proj varchar parte da chave natural
cod_proj varchar parte da chave natural
filial varchar parte da chave natural
valor decimal parte da chave natural
data_lcto date parte da chave natural
espec_veic varchar parte da chave natural
cod_conta varchar parte da chave natural
classif_conta varchar parte da chave natural
desc_conta varchar
desc_classif varchar
processo varchar
tipo_frota varchar V3 veicular
marca_veiculo varchar V3 veicular
modelo_veiculo varchar V3 veicular
ano_construcao smallint V3 veicular
ano_modelo smallint V3 veicular
seq_dentro_grupo smallint NOT NULL V6 desambiguador da chave natural
  • Chave natural (UK) uq_bi_movimento_natural — 17 colunas (NULLS NOT DISTINCT, V6): as 16 contábeis (classif_ctb, ident_ger, ident_custo, atrib_custo, ident_rat, db_cr, placa, segmento, seq_proj, cod_proj, filial, data_lcto, espec_veic, cod_conta, classif_conta, valor) + seq_dentro_grupo (desambiguador — colisões nas 16 são lançamentos legítimos, não duplicatas).
  • Índices (V2/V6): data_lcto, placa, filial, cod_proj, cod_conta.

bi_configuracao — KV global sincronizada · sincronizada

Coluna Tipo Chave Nulo? Desde Nota
id text PK NOT NULL a chave É o id (semântica)
valor text NOT NULL
tipo varchar NOT NULL CHECK (ver abaixo)
descricao text
sistema boolean NOT NULL default FALSE
atualizado_em timestamptz NOT NULL default NOW()
  • CHECK chk_bi_configuracao_tipo: tipo ∈ {STRING, INT, BOOL, TIMESTAMP, JSON}.
  • Seed (V12): ultimo_nuke (TIMESTAMP, sistema=TRUE — sinal de reset, contrato com o cliente Flutter) e slow_query_timeout_ms (INT, 10000, editável).
  • id é a própria chave semântica (não surrogate; renomear = delete+insert).

xls_processamento — controle do pipeline · local

Coluna Tipo Chave Nulo? Desde Nota
id bigint PK NOT NULL
checksum_sha256 varchar UK NOT NULL SHA-256 do binário (dedup)
nome_arquivo varchar
recebido_em timestamp default NOW()
data_referencia date legado — era o período do upload; nulo desde o lote .zip (decisão 0016)
hora_referencia time legado — idem data_referencia
periodo_inicial date período real, lido da aba 10
periodo_final date período real, lido da aba 10
status varchar default PENDENTE
iniciado_em timestamp
concluido_em timestamp
tempo_ms bigint
erro_mensagem text
log_arquivo varchar caminho relativo do histórico que a UI exibe, data/execucoes/processamento-{id}.log (a V17 reescreveu os de logs/execucoes)
registros_faturamento integer
registros_movimento integer
arquivo_bytes bytea object storage (etapa 03)
objeto_bucket varchar V8 object storage
objeto_chave varchar V8 object storage
objeto_tamanho_bytes bigint V8 object storage
faturamento_* integer V7 ×4: inseridas/atualizadas/inalteradas/removidas
movimento_* integer V7 ×4: idem
defesa_*_qtd integer V11 ×5: defesa veicular (etapa 04)
xls_legado boolean V11
conjunto_id varchar(64) V16 lote .zip de origem → xls_conjunto_lote (sem FK); índice ix_xls_processamento_conjunto
  • UNIQUE uq_xls_processamento_checksum (checksum_sha256) — dedup de upload.
  • Object storage (V8): objeto_* + arquivo_bytes (etapa 03).
  • Métricas de idempotência (V7): faturamento_*/movimento_* × {inseridas, atualizadas, inalteradas, removidas} (8 colunas, etapa 02).
  • Defesa veicular (V11): 5 colunas defesa_*_qtd + xls_legado (etapa 04).
  • Lote .zip (V16): conjunto_id agrupa as entradas de um mesmo envio (decisão 0032).

xls_conjunto_lote — lote de envio .zip · local

Coluna Tipo Chave Nulo? Desde Nota
conjunto_id varchar(64) PK NOT NULL V16 gerado no servidor: zip- + 60 primeiros hex do SHA-256 do .zip
total integer NOT NULL V16 nº de entradas do .zip
duplicados integer NOT NULL V16 default 0
nao_processados integer NOT NULL V16 default 0; entradas que não viraram processamento nem duplicata-sucesso (tipo inválido, já em andamento) — contam para o lote fechar
criado_em timestamp NOT NULL V16 default NOW()
notificado_em timestamp V16 quando saiu o resumo único no Telegram
  • Um lote fecha quando todas as entradas terminam; aí sai um resumo no Telegram (decisão 0032). Espelha o modelo do bi-comercial-xls.

xls_resetar_powersync — auditoria do reset · local

Coluna Tipo Chave Nulo? Desde Nota
id bigint PK NOT NULL
iniciado_em timestamp NOT NULL default NOW()
concluido_em timestamp
status varchar NOT NULL default PENDENTE; CHECK (ver abaixo)
motivo text
iniciado_por varchar
tempo_ms bigint
erro_mensagem text
step_atual varchar
steps_completados jsonb NOT NULL default []
detalhes jsonb
atividade_atual text V9 escrita ao vivo p/ polling
  • CHECK chk_resetar_powersync_status: status ∈ {PENDENTE, EM_ANDAMENTO, SUCESSO, ERRO}.
  • Unique parcial uq_xls_resetar_powersync_em_andamento WHERE status = 'EM_ANDAMENTO': no máximo um reset em andamento (2º disparo → 409).
  • Índice iniciado_em DESC. Ex-xls_nuke_replication (renomeada V14).

4. Mapeamento entrada↔dados

Cada aba alimenta uma tabela do banco (o ExcelProcessor lê por índice de coluna):

Aba da planilha → Tabela do banco Linha válida exige
10 (E1/F1) xls_processamento.periodo_inicial / periodo_final — (1ª linha)
20 faturamento bi_faturamento dt_frete (E) e valor (R)
30 movimento bi_movimento data_lcto (Q) e valor (P)

Cada coluna grava na coluna homônima da tabela (nomes na seção 2); não é mapeamento posicional no banco, é por nome. Transformações na leitura:

  • datas dd/MM/yyyy → DATE; valores → DECIMAL; texto truncado ao tamanho da coluna (ver Modelo de dados).
  • ano AAAA/AAAA (coluna AK/AE) → divide em ano_construcao (esquerda) e ano_modelo (direita); formato inválido → ambos nulos.
  • o período da aba 10 não vira coluna de fato — delimita quais linhas o processamento mescla naquele upload (cap. 5).

A identidade de cada fato no banco não é a posição na planilha, e sim a chave natural (cap. 3) — é o que permite reprocessar o mesmo período sem duplicar.

5. Fluxos e processamento

Arquitetura interna

O código é package-by-feature (decisão 0022): cada funcionalidade — processamento, powersync, configuracao, seguranca — carrega suas próprias camadas Controller → Service → Repository. A fundação transversal — erro RFC 7807, health, Sentry, versão, motor de auth (Firebase/sessão/Bearer/CSRF), object storage Garage, utilitários e infra de teste — vem das libs do xadm-commons (xadm-seguranca, xadm-comum-web, -storage, -util, -teste; decisões 0023/0024/0025). O pacote local comum guarda só o resto app-específico (config typed, StorageIndisponivelException, dto, AnosVeiculoParser, notificação Telegram). Dentro de uma feature o fluxo é Controller → Service → Repository: o controller só fala com service, nunca com repositório direto, e a entidade não cruza o controller (a borda HTTP fala DTO/view-model). A leitura do Excel está em processamento.processing (parser FastExcel/StAX); a persistência é Micronaut Data JDBC, sem Hibernate (decisão 0001). O ArchUnit guarda três invariantes: sem ciclos entre as features de topo, nenhum subpacote de comum depende de feature e processamento.processing não depende da camada HTTP (a leitura do Excel não conhece HTTP).

Recepção e processamento assíncrono

O envio responde 202 Accepted na hora; o trabalho pesado roda num pool dedicado (processamento, 4 threads). Cada .xlsx do lote vira um processamento próprio; o lote (xls_conjunto_lote) fecha quando todos terminam e manda um resumo único no Telegram (decisão 0032). O status em xls_processamento é a máquina de estados de cada planilha:

stateDiagram-v2
    [*] --> PENDENTE: receber (checksum, grava binário, insere linha)
    PENDENTE --> PROCESSANDO: job inicia
    PROCESSANDO --> SUCESSO: grava métricas
    PROCESSANDO --> ERRO: catch → erro_mensagem
    ERRO --> PENDENTE: reprocessar
    SUCESSO --> [*]

Reenviar o mesmo arquivo (mesmo checksum) já concluído com sucesso responde 200 sem reprocessar; já em fila, 409. Jobs presos por um restart são retomados no boot (StartupRecoveryService).

Persistência idempotente (diff-aware)

O núcleo é o UPSERT diff-aware (etapa 02, decisão 0006): por período, COPY → TEMP TABLE → INSERT ... ON CONFLICT (chave natural) DO UPDATE WHERE ... IS DISTINCT FROM, seguido de DELETE seletivo do que sumiu no período. Só linhas realmente alteradas trocam de xmin — as inalteradas não re-propagam pelo PowerSync (sem churn de bateria/banda nos celulares). Cada upload grava 8 métricas finas (inseridas/atualizadas/inalteradas/removidas, por fato) em xls_processamento. Como os arquivos de um lote mesclam em paralelo, a mescla tem duas defesas contra deadlock: o INSERT … SELECT ordena pela chave natural e a fase de banco é serializada por um advisory lock (decisão 0031); o parse do Excel segue paralelo.

O binário do .xlsx vai para object storage (Garage), não para o banco — ver a configuração de storage no cap. 6.

6. Conceitos transversais e configuração

Idempotência por checksum + chave natural (caps. 3 e 5) é o conceito central: identidade de upload (checksum_sha256) separada da identidade de fato (chave natural). Preservação de cadastro veicular (decisão 0012): correção feita direto no banco sobrevive ao reprocessamento (entrada vazia não sobrescreve). Sinal de reset (ultimo_nuke em bi_configuracao) avisa os clientes a reconectar após um reset da replicação (etapas 05/06).

Prefixos de tabela são contrato com o PowerSync: bi_* sincroniza (exige GRANT SELECT ao powersync_role — decisão 0013), xls_* é local.

Configuração (variáveis do projeto):

Tema Variáveis principais
Banco DATASOURCES_DEFAULT_URL, DB_USER, DB_PASSWORD
API VANTROBA_XLS_API_TOKEN (Bearer do /api/**; antes API_BEARER_TOKEN)
Object storage S3_FILE_STORAGE (PSQL/PSQL_GARAGE/GARAGE), GARAGE_ENDPOINT, GARAGE_ACCESS_KEY, GARAGE_SECRET_KEY, GARAGE_BUCKET (antigas S3_* via fallback)
Auth das views AUTH_FIREBASE_*, AUTH_SESSION_SECRET (login Google @xadm.com.br)
Reset / PowerSync POWERSYNC_MONGODB_URI, POWERSYNC_NUKE_CONFIRMATION, COOLIFY_API_TOKEN
Notificação / erros TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, SENTRY_DSN

Defaults, semântica de cada modo de storage e o fluxo de auth/reset estão nas etapas 03, 05 e 07; o runbook exaustivo de envs em operacao/deploy. O deploy é procedimento — fica no runbook, não neste livro.

7. Contratos públicos / API

A conclusão de tudo acima: as assinaturas que outros sistemas consomem.

Método / rota Auth Resultado
POST /api/xls/processar Bearer (ROLE_API) 202 lote recebido (conjuntoId/total/itens[], status por arquivo) · 400 zip inválido/vazio/sem .xlsx
GET /api/transporte/processamentos anônimo lista paginada (page/size/status)
GET /api/transporte/processamentos/{id} anônimo detalhe (status, métricas)
GET …/{id}/logs · …/{id}/arquivo anônimo log (text/plain) · XLS original
POST /api/transporte/admin/resetar-powersync Bearer + X-Confirm-Nuke 202 (reset da replicação)
GET /api/transporte/admin/resetar-powersync Bearer histórico de resets: o Page do micronaut-data (page/size)

Auth em duas camadas (decisão 0011): /api/** por Bearer estático; as telas server-rendered por login Google. O contrato narrado (exemplos curl, formato do arquivo, respostas) está em dev/api-rest; a referência interativa gerada do código (OpenAPI/Swagger) em docs.xadm.biz/.../public/api/.

8. Apêndice — Histórico (changelog)

Como o sistema chegou ao estado atual — as etapas entregues e as decisões que as fundamentaram. Cada etapa descreve o seu delta; este livro é o consolidado.

Etapas:

  • Recebimento e processamento automático das planilhas de transporte, com histórico rastreável de cada envio.
  • Reprocessar um período sem gerar tráfego nem churn desnecessário para os aplicativos móveis.
  • Arquivamento dos arquivos recebidos em storage dedicado, fora do banco de dados.
  • Identificação de veículo e frota nos lançamentos, preservando correções manuais de cadastro.
  • Reset seguro e auditável da replicação quando o estado replicado precisa ser zerado.
  • Configurações compartilhadas entre servidor e aplicativo, ajustáveis sem novo deploy.
  • Acesso às telas internas restrito a contas @xadm.com.br.
  • Stack uma major atrás modernizada e código alinhado às práticas do framework, sem mudar comportamento observável.

Decisões: