Pular para conteúdo

Guia do código

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

Audiência: dev que abriu o repositório e quer passar os olhos e entender como o código está organizado, sem ainda mergulhar no Javadoc classe a classe. Para o porquê das decisões, veja decisoes/; para rodar, Como rodar; para testar, Como rodar; para o contrato externo, API REST.

Como o código é organizado

O código segue package-by-feature sob br.com.onpetro.bi.comercial (decisão 0030): cada fatia carrega as próprias camadas, e o que é transversal fica na base comum. As travas do ArchUnit (abaixo) protegem as fronteiras.

Fatia Responsabilidade
processamento o pipeline XLS: a API de ingestão (POST /api/xls/processar) e de consulta (/api/comercial/processamentos), as telas /processamentos/**, a recepção do .zip, a fila assíncrona, o lote (ConjuntoLoteService), o storage do binário (ArmazenamentoArquivoService, backfill), as mensagens de Telegram do processamento e o recovery no boot; entidades xls_processamento/xls_conjunto_lote
processamento.planilha a leitura das planilhas: ArquivoDetector + TipoArquivo, os 9 {Tipo}Processor, ExcelSheetReader (FastExcel), ZipXlsExtrator, Segmento
dados as tabelas bi_* de negócio: entidades, repositórios Micronaut Data e a escrita em lote (BulkUpserter, ColunasConteudo, UpsertResultado). É folha
importacao a página aberta de import manual (/xls/importar, alias /compras/importar): ImportacaoXlsController
powersync o reset (nuke) da replicação: NukeReplicationService, a API REST e a tela /admin, Mongo, slot, Coolify, a auditoria xls_nuke_replication, as mensagens de Telegram do nuke e o recovery no boot
configuracao bi_configuracao: leitura com cache (ConfigService) e a tela /admin/configuracoes
diagnostico /test e /test/glitchtip
seguranca a policy de auth deste app: ViewSecurityRule, ViewWhitelist, ViewRejectionHandler, AuthExceptionHandler
comum base transversal, não é feature: comum.exception (StorageIndisponivelException, 503) e comum.notificacao (TelegramService, só o transporte)

Na raiz ficam Application e BearerTokenEnv (resolve a env do Bearer no main). As dependências entre fatias têm um sentido só:

%% lint-mermaid: LR-ok
flowchart LR
    I[importacao] --> P[processamento]
    P --> PL[processamento.planilha]
    PL --> D[dados]
    P --> D
    P --> C[comum]
    PS[powersync] --> CF[configuracao]
    PS --> C

O que é igual em todo app da casa vem das libs br.com.xadm, não de cópia local:

Lib O que entrega aqui
xadm-seguranca o motor de auth das views (sessão, validação do token Firebase, CSRF, settings) e a tela de login (xadm/login) — decisões 0018 e 0025
xadm-comum-web o erro RFC 7807 (ProblemDetail + processor), /health e /info, o init do Sentry e as exceções HTTP genéricas (BadRequest/Conflict/NotFound/Forbidden)
xadm-comum-storage o seam de object storage (ArquivoXlsStorage + impl S3/Garage), ArmazenamentoModo e a config S3
xadm-comum-util ChecksumSha256 e DataConverter

As camadas de uma requisição

Dentro de cada fatia o fluxo é Controller → Service → Repository:

  • Controller (@Controller): só HTTP. Em API retorna DTO (record + @Serdeable, sufixo *Request/*Response) → JSON; em view server-rendered alimenta o template JTE com um view-model (*View, ex.: NukeView, ConfiguracaoView). Zero regra de negócio.
  • Service (@Singleton): regra de negócio, orquestração, @Transactional, conversão entidade↔DTO. Injeção por construtor.
  • Repository (Micronaut Data JDBC, sem Hibernate — decisão 0007): só acesso a dado. Sem proxies lazy, sem sessão; SQL previsível.
  • A entidade (@MappedEntity) nunca cruza o controller. A borda HTTP fala DTO (API) ou view-model (view); a entidade fica da camada de service para dentro.

Notificação e recovery seguem o corte por feature: o TelegramService (em comum) só envia texto pronto, e cada feature monta o seu (ProcessamentoNotificador, NukeNotificador); o boot recupera cada feature no seu bean (ProcessamentoStartupRecovery, NukeStartupRecovery), depois do backfill do storage.

O pipeline de processamento (processamento.planilha)

Diferente do projeto irmão Transporte (que lê 3 abas fixas), o Comercial detecta automaticamente o tipo do arquivo — o cliente não informa tipoArquivo (decisão 0010):

  1. ArquivoDetector classifica o .xlsx em um dos 10 tipos por ordem de prioridade — sinais de conteúdo (abas e colunas-âncora) primeiro, nome do arquivo depois. Nomes começando com ~$ ou # (temporários do Excel) são rejeitados. A tabela de ordem e critérios está no livro, cap. 2.
  2. Cada tipo tem seu {Tipo}Processor dedicado, que sabe o layout daquela planilha e mapeia linhas → entidades bi_* de dados.
  3. A leitura das células usa o ExcelSheetReader (FastExcel-reader, StAX) compartilhado — streaming, não carrega a planilha inteira em memória. É o que viabiliza o native-image (decisão 0020).
  4. A gravação passa pelos repositórios de dados, com o BulkUpserter para os lotes grandes (seção seguinte).

A leitura não conhece o pipeline: quem a chama é o ProcessamentoTransactionalHelper, em processamento. O upload (POST /api/xls/processar, em processamento/XlsController) é uma porta única: um campo arquivo com o .zip do ciclo. Ele não exige período (vem do conteúdo) nem qualquer campo de coordenação — o zip é o lote. ProcessamentoService.receberZip deriva o conjuntoId ("zip-" + sha256(zip)[0..60], idempotente) e o total pelo número de entradas .xlsx, e delega cada entrada ao receber(...). A notificação Telegram agregada sai quando todas as entradas terminam (via ConjuntoLoteService, decisão 0011).

Persistência em lote

A escrita dos bi_* é diff-aware (desenho na etapa 02): INSERT … ON CONFLICT (chave_natural) DO UPDATE … WHERE … IS DISTINCT FROM …, seguido do DELETE seletivo do que sumiu no período. Só a linha que mudou troca de xmin, e as inalteradas não re-propagam pelo PowerSync. Regras para quem mexe num repositório:

  • BulkUpserter usa PreparedStatement.executeBatch(), não COPY: o COPY não suporta ON CONFLICT DO UPDATE, e o executeBatch() devolve o rowCount por linha que alimenta a métrica linhas_efetivas. Por isso reWriteBatchedInserts=false é obrigatório na URL JDBC — ligado, o pgjdbc reescreve o lote em multi-VALUES e devolve SUCCESS_NO_INFO (-2) (0009). Chamar fora de transação ativa lança IllegalStateException.
  • Ordene rows pela chave natural antes do executeBatch(). O executor processamento roda até 4 arquivos em paralelo e as dimensões se sobrepõem entre arquivos; o ON CONFLICT DO UPDATE tira row lock mesmo quando o WHERE IS DISTINCT FROM não atualiza, e duas transações upsertando as mesmas linhas em ordens diferentes deadlockam (40P01). Cada repositório mantém um ORDEM_CHAVE_NATURAL e aplica sorted(...) antes de montar os parâmetros.
  • Coluna de conteúdo nova entra no ColunasConteudo, a lista canônica que monta o WHERE … IS DISTINCT FROM e a comparação da conformidade Python↔Java. O WHERE compara só as colunas presentes no SET daquele upsert; coluna que não vem do arquivo (ex.: data de bi_posto/bi_trr) fica de fora, senão todo reupload vira UPDATE.
  • Tabela sem chave natural (bi_compra, bi_cota_petrobras) não faz upsert: é replace-por-período ou full-replace, em transação. A estratégia por tipo está no livro, cap. 4.

Travas de arquitetura (ArchUnit)

As regras vêm da fábrica RegrasArquitetura da lib xadm-comum-teste, aplicadas no ArchitectureTest — rodam no ./gradlew check, sem Docker:

  1. import não-vazio — a guarda falha alto se o ArchUnit importar zero classe;
  2. sem ciclos entre as fatias;
  3. comum não depende de fatia;
  4. dados é folha — não depende de nenhuma fatia;
  5. processamento.planilha não depende do pacote processamento (fila, lote, storage, controllers);
  6. controller não acessa repository — passa sempre pelo service da fatia;
  7. nada depende de controller;
  8. SQL cru por conexão JDBC direta só nas classes de escrita em lote da allowlist (as de dados e o powersync.PostgresReplicationAdmin); o resto do acesso a dado usa Micronaut Data;
  9. a entidade não depende de infra, e a escrita de controller de view declara @Consumes.

As regras de fronteira citam o nome completo da fatia (br.com.onpetro.bi.comercial.seguranca..): o padrão curto casaria também o pacote br.com.xadm.comum.seguranca da lib. Rename de pacote leva o literal junto e prova que a trava ainda morde, quebrando uma regra de propósito — com o literal para trás, a regra passa verde sobre zero classe. Mexeu na estrutura de pacotes → rode o gate antes de abrir o PR.

Contratos transversais

  • Auth — duas portas independentes: Bearer estático no /api/** (0002) e login Google nas views (0003); ver a etapa 06 e o runbook do login. As consultas de processamento (GET /api/comercial/processamentos, /{id} e /{id}/logs) exigem autenticação — Bearer ou a sessão da view. A exceção aberta é o /xls/importar, protegido por segredo compartilhado (API REST).
  • Erros da API — todo erro do /api/** sai como application/problem+json (RFC 7807), via o processor e o ProblemDetail da lib xadm-comum-web (0015). O código lança as exceções HTTP da lib ou a comum.exception.StorageIndisponivelException; não monta corpo de erro à mão.
  • Storage — o binário .xlsx vai para o Garage (S3), coordenado pelo processamento/ArmazenamentoArquivoService, o único ponto que conhece o modo; 3 modos (0005, etapa 04).
  • Trilha por processamento — o log de cada execução, exibido na tela de detalhe, é dado: mora em app.logs.dir (data/execucoes, volume /app/data em produção — deploy).

Views (JTE) e kit visual

As telas são templates JTE em src/main/jte/** (decisão 0022), compostas pelo kit/layout.jte. A tela recebe view-model ou DTO, nunca a entidade.

  • Dois CSS, dois donos: public/css/custom-theme.css é a identidade da casa (cópia verbatim do kit do central); public/css/app.css são os componentes deste app (nasce do scaffold e é editado aqui). O Bootstrap (public/css/bootstrap.min.css + public/js/bootstrap.bundle.min.js), o logo e o favicon também vêm do kit — bump é re-derivar pela /xadm-docs, nunca editar à mão.
  • Estático anônimo se libera no intercept-url-map do application.yml: a regra do YAML roda antes da ViewSecurityRule e decide sozinha para usuário anônimo. Sem /js/** ali, o bundle responde 401 na tela de login (0025). O EstaticosDoKitAnonimosTest faz GET anônimo em cada href/src do layout e do /login.
  • Write-endpoint de controller de view declara o media type consumido (consumes = …): o default do Micronaut é JSON, e o POST de um <form> sem a declaração recebe 415 sem log útil.
  • Status visual por classe .badge-{STATUS} (não switch inline no template); paginação com .pagination .pagination-sm; a tela de progresso do nuke tem <noscript> com meta refresh.

Base de teste

Os testes espelham as fatias (src/test/java/.../processamento/planilha, .../dados, .../powersync, …); ficam na raiz os transversais — ArchitectureTest, AbstractRepositoryIntegrationTest, IntegracaoDocker, TestXlsxFactory, /health, RFC 7807 e serde. O Postgres e o Garage de teste vêm da xadm-comum-teste; o AbstractRepositoryIntegrationTest limpa as tabelas do projeto entre testes. Camadas de teste, fixtures golden e as armadilhas de @Replaces e de cobertura estão em Como rodar.

Divergências deliberadas do bi-transporte

O projeto irmão bi-transporte-xls compartilha a stack e o layout package-by-feature. O corte das fatias difere onde o domínio pede: lá as bi_* moram em processamento e a leitura em processamento.processing; aqui as bi_* são a fatia folha dados e a leitura é processamento.planilha. No domínio, diverge onde o negócio pede: aqui o tipo é detectado entre 9 (lá, 3 abas fixas); a entrada é um .zip que é o lote (lá, data/hora no POST); os bi_* usam UUID v7 porque sincronizam (0008); as métricas de idempotência são agregadas por upload (etapa 02); e o upsert usa executeBatch() em vez de COPY + tabela temporária, pelo rowCount por linha.

Referência completa

O Javadoc gerado (árvore de classes navegável, interna) fica em dev/api/ — gerado pelo CI de docs a cada publish. É o inventário completo das classes; use-o quando precisar do detalhe de uma delas.

Armadilhas

Armadilhas de framework e plataforma que o código deste app contorna; o comentário no código aponta para cá. As armadilhas de framework que a casa já registrou estão em https://docs.xadm.biz/engenharia/java-micronaut/ (seção Armadilhas); aqui ficam só as deste app.

Sintoma Causa Cura
/health e /info respondem 400 os controllers da xadm-comum-web são incondicionais e colidem com os do micronaut-management (rota dupla) endpoints.health/endpoints.info com enabled: false; o /health fica liveness, sem checar o banco
Restart do PowerSync falha com PKIX path building failed a URL externa do Coolify passa por DNS público, hairpin NAT e Traefik com TLS, e o cert wildcard pode não estar na truststore da JVM (há ainda a allowlist de IP da API) COOLIFY_API_URL=http://coolify:8080, a URL interna da rede Docker (app na network coolify)