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):
ArquivoDetectorclassifica o.xlsxem 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.- Cada tipo tem seu
{Tipo}Processordedicado, que sabe o layout daquela planilha e mapeia linhas → entidadesbi_*dedados. - 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). - A gravação passa pelos repositórios de
dados, com oBulkUpserterpara 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:
BulkUpserterusaPreparedStatement.executeBatch(), nãoCOPY: oCOPYnão suportaON CONFLICT DO UPDATE, e oexecuteBatch()devolve o rowCount por linha que alimenta a métricalinhas_efetivas. Por issoreWriteBatchedInserts=falseé obrigatório na URL JDBC — ligado, o pgjdbc reescreve o lote em multi-VALUES e devolveSUCCESS_NO_INFO(-2) (0009). Chamar fora de transação ativa lançaIllegalStateException.- Ordene
rowspela chave natural antes doexecuteBatch(). O executorprocessamentoroda até 4 arquivos em paralelo e as dimensões se sobrepõem entre arquivos; oON CONFLICT DO UPDATEtira row lock mesmo quando oWHERE IS DISTINCT FROMnão atualiza, e duas transações upsertando as mesmas linhas em ordens diferentes deadlockam (40P01). Cada repositório mantém umORDEM_CHAVE_NATURALe aplicasorted(...)antes de montar os parâmetros. - Coluna de conteúdo nova entra no
ColunasConteudo, a lista canônica que monta oWHERE … IS DISTINCT FROMe a comparação da conformidade Python↔Java. OWHEREcompara só as colunas presentes noSETdaquele upsert; coluna que não vem do arquivo (ex.:datadebi_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:
- import não-vazio — a guarda falha alto se o ArchUnit importar zero classe;
- sem ciclos entre as fatias;
comumnão depende de fatia;dadosé folha — não depende de nenhuma fatia;processamento.planilhanão depende do pacoteprocessamento(fila, lote, storage, controllers);- controller não acessa repository — passa sempre pelo service da fatia;
- nada depende de controller;
- SQL cru por conexão JDBC direta só nas classes de escrita em lote da allowlist (as de
dadose opowersync.PostgresReplicationAdmin); o resto do acesso a dado usa Micronaut Data; - 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 comoapplication/problem+json(RFC 7807), via o processor e oProblemDetailda libxadm-comum-web(0015). O código lança as exceções HTTP da lib ou acomum.exception.StorageIndisponivelException; não monta corpo de erro à mão. - Storage — o binário
.xlsxvai para o Garage (S3), coordenado peloprocessamento/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/dataem 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.csssã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-mapdoapplication.yml: a regra do YAML roda antes daViewSecurityRulee decide sozinha para usuário anônimo. Sem/js/**ali, o bundle responde401na tela de login (0025). OEstaticosDoKitAnonimosTestfaz GET anônimo em cadahref/srcdo 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 recebe415sem log útil. - Status visual por classe
.badge-{STATUS}(nãoswitchinline 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) |