Table of Contents
integrador-client¶
Status: Rascunho · Responsável: Gustavo Madruga · Atualizado em: 2026-09-10
Pedidos, clientes e produtos criados na nuvem (plataforma de integração X-Adm) precisam entrar
no ERP — e hoje param na fronteira: as tabelas-espelho ficam pendentes no banco do cliente sem
ninguém que as grave no X-Adm. O integrador-client fecha esse último trecho: é um programa
que roda na máquina do cliente, junto ao ERP, recebe os dados da nuvem automaticamente
(sincronização PowerSync, offline-first) e os grava no X-Adm chamando o próprio runtime ZIM
(zimrtmu.exe), devolvendo para a nuvem o resultado de cada registro — gravado, erro de dado ou
tentar de novo.
Por que importa¶
- Fim da redigitação — o que nasce na nuvem entra no ERP sozinho, com rastro de status por registro.
- Feito para a equipe do ERP — código Java propositalmente simples e explícito, mantível por
quem desenvolve em ZIM; instala como um único
.jar+ arquivo de configuração, rodando no Java 8 que já existe na máquina do ERP. - Robusto por construção — se a rede cai, a sincronização continua de onde parou; se o ZIM está ocupado ou sem licença, o lote tenta de novo sozinho.
- Visível de longe — cada instalação avisa que está viva: a X-Adm vê a versão e os fluxos de cada cliente sem acesso remoto à máquina, e é alertada quando uma instalação para — antes de o cliente reclamar.
Estado atual e desenho completo: Documentação Completa.
Etapas¶
O que já foi entregue:
- O que nasce na nuvem entra no ERP sozinho, com status por registro.
- A mesma instalação atende várias origens de dado do ERP, não só uma.
- Falha transitória deixa de virar martelada no ERP, e a crítica do X-Adm chega a alguém.
- O X-Adm passa a receber os componentes do kit, não só o pedido do kit.
- Pedido não chega mais ao ERP sem o cliente — o que depende viaja junto ou não viaja.
O que está planejado:
- Instalação parada deixa de ser descoberta pelo cliente: a plataforma vê versão e fluxos de cada uma e alerta quando alguma some.
Projeto
Documentação Completa — integrador-client¶
Status: Rascunho · Responsável: Gustavo Madruga · Atualizado em: 2026-09-21
O sistema como ele é hoje, em ordem de raciocínio: o problema, o que entra, o que se guarda, como processa, e — no fim — os contratos que o outro lado precisa cumprir. As etapas contam como chegamos aqui; esta página conta o que existe.
O sistema numa tela¶
flowchart TB
subgraph NUVEM["Nuvem X-Adm (por cliente)"]
direction TB
PG[("Postgres — barramento<br/>tabelas-espelho + manifesto")]
PS["PowerSync — connector"]
INT["integrador-server<br/>POST /api/v1/powersync"]
PG --- PS
INT --- PG
end
subgraph CLIENTE["Máquina do cliente, junto ao ERP (JRE 8)"]
direction TB
SQLITE[("SQLite local<br/>espelho + manifesto")]
APP["integrador-client<br/>um processador por fluxo, em série"]
ARQ["dXpEnvio / dXpRetorno<br/>fixed-width"]
ZIM["zimrtmu.exe — programa pAbast"]
ERP[("X-Adm / ERP")]
SQLITE --> APP
APP --> ARQ
ARQ --> ZIM
ZIM --> ERP
ZIM --> ARQ
ARQ --> APP
APP --> SQLITE
end
PS -->|"sync: pendentes descem"| SQLITE
SQLITE -->|"write-back: status sobe"| INT
APP -->|"heartbeat: sinal de vida"| INT
INT -->|"repassa com o cliente"| CEN["central-backend<br/>alerta de silêncio"]
O dado desce da nuvem para um SQLite na máquina do ERP; o cliente monta um arquivo de texto de largura fixa, chama o runtime ZIM, lê o resultado linha a linha e devolve o status pela mesma via por onde o dado veio. Em paralelo, um heartbeat avisa pelo mesmo integrador-server que a instalação está viva.
Como acompanhar se rodou. Três lugares, nesta ordem: (1) o cod_retorno de cada linha na
nuvem — 006 gravou, 1XX recusou, 9XX vai retentar; (2) o log do cliente
(logs/integrador-client.log), que ecoa cada linha enviada e recebida entre |pipes|;
(3) o GlitchTip (bug.xadm.biz), que recebe as falhas e as críticas do ERP, com a tag do
cliente. Não há tela nem endpoint: é um daemon.
A plataforma também acompanha a instalação sem ninguém olhar: o heartbeat (na largada e a cada hora) põe a versão, os fluxos e o último sinal de cada instalação na aba Deploy do central-ui, e 6 horas úteis sem sinal viram alerta no GlitchTip do central (runbook do central).
1. Contexto e problema¶
Pedidos, cadastros e produtos criados na nuvem precisam entrar no ERP — e paravam na fronteira: as tabelas-espelho ficavam pendentes no banco do cliente sem ninguém que as gravasse no X-Adm. O integrador-client fecha esse trecho, rodando on-premise, junto ao ERP, no JRE 8 que já existe na máquina.
É o cliente coringa da plataforma: o mesmo programa envia dados de várias origens ao X-Adm, cada origem um fluxo, com suas tabelas e seu literal de handshake — o ZIM identifica o fluxo pelo literal, porque o nome do programa é compartilhado. Cada arquivo de envio carrega as tabelas de um fluxo só.
| Fluxo | Literal | Estado |
|---|---|---|
| pied | IMPORTA_PIED |
implementado, o único ativo hoje |
| abastecimento | Continuar |
implementado e pronto; inativo até o espelho existir no servidor |
| encerramento de MDF-e | ENCERRA_MDFE |
implementado; inativo até as sync rules exporem a tabela |
| sascar | — | previsto |
Fora de escopo: o cliente não decide o que deve ir ao ERP (quem declara é o servidor), não transforma dado de negócio e não fala com terceiros. Ele transporta, grava e devolve status.
2. Dados de entrada¶
A entrada não é arquivo nem requisição: é o estado do espelho local. O que o cliente
processa a cada ciclo é a linha que está pendente — cod_retorno em 000 (nova) ou
9XX (reprocessável) — nas tabelas do fluxo, na ordem dos fatos.
Duas coisas chegam pelo mesmo sync:
- as tabelas-espelho, o dado que vai ao ERP;
- o manifesto de entrega (
integracao_remessa+integracao_remessa_item), que não vai ao ERP: ele diz quais linhas precisam viajar juntas.
A ordem dentro de um lote vem do id (UUID v7), não do updated_at — que o servidor bumpa
até no write-back e por isso não serve de ordem.
3. Modelo de dados¶
Modelo de dados — o espelho local¶
O integrador-client não tem banco próprio: ele materializa, num SQLite local, um subconjunto das tabelas do barramento (o Postgres por cliente, cujo dono é o integrador-server). O modelo abaixo é o que o cliente enxerga — nem o schema completo do servidor, nem o schema do ERP.
Duas famílias, e a diferença governa todo o resto:
- espelho — o dado que vai ao ERP. Cada tabela pertence a um fluxo e vira linha no arquivo de envio;
- controle — o manifesto de entrega. Não vai ao ZIM, não tem linha no arquivo e não pertence a fluxo nenhum: só declara quais linhas do espelho precisam viajar juntas.
erDiagram
INTEGRACAO_REMESSA ||--o{ INTEGRACAO_REMESSA_ITEM : declara
INTEGRACAO_REMESSA_ITEM }o..o| ESPELHO : "aponta (tabela + linha_id)"
ESPELHO {
text id PK "UUID v7 — implícito, gerido pelo PowerSync"
text cod_retorno "máquina de status"
text msg_retorno
}
INTEGRACAO_REMESSA {
text id PK
text origem
text chave_negocio
text status
text total_itens
}
INTEGRACAO_REMESSA_ITEM {
text id PK
text remessa_id FK
text tabela
text linha_id
text bloqueado "calculado no servidor"
text resolvido "calculado no servidor"
}
O vínculo item→espelho é polimórfico (tabela + linha_id), sem integridade
referencial: uma remessa mistura linhas de tabelas diferentes.
Regras que valem para toda tabela do espelho¶
id(UUID v7) não é declarado — o PowerSync o gerencia. É ele que dá a ordem estável do lote:updated_atdo servidor bumpa até no write-back e por isso não serve de ordem.- Toda coluna é TEXT. Número viaja como texto e vira
BigDecimalno Java — nuncadouble. Booleano do Postgres chega como"1"/"0"(o SDK não tem tipo booleano). - Só se declara o que o cliente usa: os campos de entrada (que vão ao ZIM) mais
cod_retorno/msg_retorno. Coluna que o X-Adm gera (aschave_*, derivadas) e controle do servidor (deleted,updated_at, …) ficam de fora — coluna não declarada não sincroniza. - A ordem das tabelas dentro de um fluxo é a ordem de dependência referencial, e é a ordem em que elas entram no arquivo de envio.
Tabelas por fluxo¶
Fluxo pied — IMPORTA_PIED¶
Pedidos, cadastros e produtos vindos do int-pied. Ordem de envio (FK-safe):
| # | Tabela | Colunas de entrada |
|---|---|---|
| 1 | propriedades |
cgc_cpf, cod_prop, nome_prop, fantasia, endereco, cep, bairro, cidade, estado, numero, insc_est |
| 2 | fones |
cgc_cpf, ddd, telefone, nome, email |
| 3 | estoque |
pied_codigo_alt, ean13, cod_prod_alt, nome_prod, venda, venda_pz |
| 4 | contratos |
pied_x_ped, cgc_cpf_cliente, chave_unid, cod_mat, dt_em, data_inc, hora_inc, vl_tot, nat_op, compl, tp_vda, class_forma, prazo |
| 5 | itensped |
pied_x_ped, pied_codigo_alt, qtde, valor, total, taxa_frete |
| 6 | formulas |
pied_formula_id, pied_x_ped, pied_cod_prod, qtde |
formulas (a fórmula/BOM do kit) vai por último: referencia o produto-kit e o
componente, que precisam existir no X-Adm antes. As chaves que o X-Adm gera não voltam.
Fluxo abastecimento — Continuar¶
Uma tabela, abastecimento, réplica da entidade do integrador do xposto: identificação do
bico e do almoxarifado, encerrantes inicial e final, duração, volume, autorização, captura,
usuário/motorista/produto/empresa, placa e os odômetros com os limites de média. Implementado
e pronto; inativo até o espelho existir no servidor.
Fluxo encerra-mdfe — ENCERRA_MDFE¶
Uma tabela, mdf, com uma coluna de entrada: chave_nota (19 posições, já formatada
pelo servidor). O resto do espelho é do servidor e não sincroniza. Implementado; inativo até
as sync rules exporem a tabela.
Tabelas de controle — o manifesto¶
integracao_remessa (origem, chave_negocio, status, total_itens) e
integracao_remessa_item (remessa_id, tabela, linha_id, bloqueado, resolvido).
- Só a remessa viva desce para o cliente — as entregues viram histórico no servidor.
total_itensnão é cache de contagem: é o que deixa o cliente perceber que a própria lista de itens chegou pela metade (o PowerSync entrega linhas da mesma transação em checkpoints diferentes).bloqueadoeresolvidosão calculados pelo servidor; o cliente só lê.bloqueado= um pai desta linha, na mesma remessa, está em erro terminal.resolvido= nada mais a esperar por ela.
Máquina de status (o campo cod_retorno)¶
flowchart TB
P["000 — pendente"] --> E["001 — em processamento"]
E --> S["006 — gravado no X-Adm (fim)"]
E --> T["1XX — erro de dado (fim, não retenta)"]
E --> R["9XX — reprocessável (volta a pendente)"]
R --> P
O cliente processa o que está pendente (000 ou 9XX), marca 001 enquanto o lote roda e
grava o código que o ZIM devolveu. Um 001 que sobrou de uma queda no meio do lote volta a
000 na inicialização. O estado terminal 1XX é o que o manifesto propaga como bloqueado
para os filhos daquela linha.
4. Mapeamento — do espelho para a linha do ERP¶
Cada tabela do fluxo vira um bloco de linhas no arquivo de envio, na ordem de dependência
referencial, e cada linha é fixed-width: contador (3) + tipo (20) + os campos daquela
tabela, com largura e escala fechadas. Texto é truncado ou preenchido; número vai sem
separador, na escala do campo (HALF_UP); valor monetário é BigDecimal, nunca double.
O mapeamento campo a campo — largura, escala, offset e exemplo de cada coluna — é o contrato pAbast §3, que anda junto com o código do arquivo de envio: mudou uma largura, muda o contrato no mesmo PR.
5. Fluxos e processamento¶
O ciclo, para cada fluxo ativo:
- Sincroniza — o SDK do PowerSync mantém o SQLite local em dia, offline-first, com reconexão automática. Mudança na nuvem acorda o ciclo; um poll de segurança (5 min) cobre o resto.
- Coleta os pendentes por duas vias que convivem (decisão 0004): a linha coberta por uma remessa viva só sai com a remessa inteira no banco local; a linha sem remessa sai por presença, após 3 ciclos de carência. Por cima, a coleta lê duas vezes e confere a passada inteira — banco que não sossega adia o lote. O corte no teto de 999 registros respeita remessas inteiras.
- Grava no ERP — escreve o arquivo de envio, executa o
zimrtmu.execom o programapAbast, faz o handshake por arquivos (o ZIM escreveIniciou Zim; o cliente responde com o literal do fluxo) e lê o retorno ao final, com timeout. Cada linha escrita e cada linha lida vão para o log. - Absorve as críticas do ERP — se o X-Adm deixou avisos no
dRels3(o que, com tela, apareceria ao usuário), o cliente os registra no log e no GlitchTip e apaga o arquivo. - Devolve o status — atualiza a linha local; o SDK propaga sozinho ao integrador-server. Nuvem fora: a fila local segura, e um vigia reconecta se ela ficar presa mais de 20s.
Quando o lote deixa pendência, o agendador desacelera: descarta o gatilho do próprio eco e aplica backoff exponencial (1s dobrando, teto 15 min). Dado novo fura o backoff e o zera — o item preso entra no arquivo do novo.
Com mais de um fluxo ativo, os processadores rodam em série numa thread única, no par de arquivos único (decisão 0002) — nunca concorrem.
Máquina de status¶
000 → 001 → 006 | 1XX | 9XX. O 001 marca o lote em voo; um 001 órfão de uma queda volta
a 000 no startup. 1XX é terminal (erro de dado, não retenta) e é o que o manifesto
propaga como bloqueado para os filhos. O matching envio↔retorno é por posição de linha,
protegido por contagem e tipo ecoado — divergência invalida o lote inteiro, de propósito. Duas
tolerâncias, ambas para defeitos do pAbast em conflito (contrato
§4): os começos de tentativa abortada que ele deixa antes da rodada final, idênticos a ela
(descartados; vale a rodada final), e a resposta que ele escreve para a linha de continuação no fim
do retorno (descartada; valem os 999 dos registros).
No fluxo de MDF-e o retorno casa por contador: pode vir fora de ordem e parcial, e o que
não voltar retenta.
Modo --debug¶
Homologação: nada roda sozinho. O operador processa pelo console — item a item ou pedido a
pedido (fechamento relacional do contrato) — vendo envio e retorno na tela, com os arquivos
mantidos em disco, a máscara do layout e as versões .json. Com mais de um fluxo ativo, um
seletor escolhe o fluxo antes do menu.
Heartbeat¶
Na largada — depois da trava de instância e antes de conectar ao PowerSync — o cliente manda um
heartbeat (versão, fluxos ativos, nome da máquina) ao integrador-server do próprio cliente, e o
repete a cada heartbeat.intervaloMinutos (60 por padrão), na mesma thread do laço. É só sinal de
vida: nunca derruba o daemon (falha vira WARN) e não vai ao GlitchTip — o silêncio de uma
instalação quem enxerga é o central. O --debug não manda. Etapa:
06 — Heartbeat.
6. Conceitos transversais e configuração¶
- Uma instância por pasta de dados. Uma trava impede duas execuções na mesma pasta; a segunda sai como ocupado (exit 4), sem ter feito nada — seguro repetir.
- Autenticação: JWT auto-assinado (RS256) com a chave privada da instalação (
.pem, que nunca entra no Git). Nenhum segredo é logado. - Configuração: um
.propertiesao lado do jar. Chave faltando ou caminho inválido para na largada, nomeando a chave (exit 2).zim.fluxosé obrigatória e sem default: a instalação declara quais fluxos rodam. A guarda de startup confere fluxo × schema. As chavesheartbeat.*são opcionais: o endereço sai dopowersync.upload.urle o intervalo tem padrão (0desliga); URL que não presta também para na largada. - Exit codes:
0normal ·1falha inesperada ·2configuração/ambiente (properties inválido ou JVM 32 bits) ·4ocupado. A taxonomia é da casa; o app não inventa código. Detalhe no runbook. - Logs: stderr + arquivo rolling (30 dias), UTF-8 nos dois. stdout fica livre — é o contrato de CLI da casa.
- Observabilidade: GlitchTip com a tag do cliente (o DSN é compartilhado), para diagnosticar
de longe uma instalação que roda na máquina do cliente. Todo
WARN/ERRORdo programa vira incidente por um caminho só — o appender do Logback —, e oINFOvira breadcrumb: contexto que viaja dentro do próximo incidente, sem gerar nada quando o ciclo corre bem. Cada incidente carrega ainda o eco dodXpEnvio/dXpRetornodo lote e a configuração de startup (mascarada). Ficam fora do GlitchTip, de propósito: erro de configuração local e JVM de 32 bits (ação do operador na ponta, não falha do aplicativo) e o conflito de concorrência nodRels3(ruído sem ação possível — o lote retenta sozinho). O envio se desliga pela variável de ambienteSENTRY_DSNvazia, que é como o e2e e o desenvolvimento local rodam sem sujar o painel de produção (decisão 0006). - Runtime Java 8 (decisão 0001): a máquina do ERP só tem JRE 8. Compila-se com JDK moderno, alvo bytecode 52, e há teste que confere isso.
7. Contratos¶
A conclusão de tudo acima: o que o outro lado precisa cumprir.
| Contrato | Fluxo | O que fixa |
|---|---|---|
| pAbast | pied | layout fixed-width campo a campo, ordem das tabelas, handshake, formato do retorno e a máquina de status |
| abastecimento | abastecimento | as colunas do espelho do posto e a conversão de duração |
| ENCERRA_MDFE | encerra-mdfe | a chave_nota de 19 posições e o retorno casado por contador |
| heartbeat | todos | o corpo do sinal de vida e as respostas; o dono é o integrador-server |
Não há API HTTP exposta: este é um daemon on-premise. A superfície pública dele é o par de arquivos e o literal de handshake — e é por isso que os contratos acima são o capítulo final. O heartbeat é o único contrato HTTP que o próprio cliente chama (o write-back quem faz é o SDK); ele pertence ao integrador-server, e aqui é linkado, não copiado. O guia para quem vai mexer no código está em guia do código; o fio completo, provado localmente, em E2E local.
8. Apêndice — histórico¶
Etapas:
- O que nasce na nuvem entra no ERP sozinho, com status por registro.
- A mesma instalação atende várias origens de dado do ERP, não só uma.
- Falha transitória deixa de virar martelada no ERP, e a crítica do X-Adm chega a alguém.
- O X-Adm passa a receber os componentes do kit, não só o pedido do kit.
- Pedido não chega mais ao ERP sem o cliente — o que depende viaja junto ou não viaja.
- Instalação parada deixa de ser descoberta pelo cliente: a plataforma vê versão e fluxos de cada uma e alerta quando alguma some. (planejado)
Decisões:
- 0001 — Runtime Java 8 (exceção à constituição)
- 0002 — Multi-fluxo: N Processador em série num par de arquivos único
- 0003 — Coleta coerente: ler duas vezes e conferir
- 0004 — Coleta por remessa: a unidade de entrega é declarada na origem
- 0005 — Adota o CI 100% GitHub Actions, com a Release no Forgejo — ponteiro do ADR central 0027
- 0006 — Observabilidade remota: WARN/ERROR pelo appender, eco cru em breadcrumb e kill-switch por SENTRY_DSN
Pendências abertas:
- Layout
formulasprovisório — larguras e escala em contrato-pabast §3.6 valem até o import real de FORMULAS chegar da equipe ZIM; o golden test trava o provisório e vira o ponto de partida da reconciliação. MascaraDebugsem branchformulas(deferido, com precedente domdf): no rodapé do modo debug a linha de fórmula sai só com contador e tipo.- Validação do contrato pAbast pela equipe X-Adm (§8 do contrato) e implementação do lado ZIM.
- Smoke em máquina Windows x64 real (os nativos do SDK foram validados em Linux/WSL).
- Ativação dos fluxos
abastecimentoeencerra-mdfe, ambos à espera do lado servidor.
Glossário¶
Termos do domínio: Glossário.
Operação
Instalação e operação¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-21
Provisionar uma instalação nova (lado X-Adm — antes de ir ao cliente)¶
- Par de chaves da instalação (a privada é a credencial do cliente):
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out <cliente>-private.pem openssl rsa -in <cliente>-private.pem -pubout -out <cliente>-public.pem - Registrar a pública no PowerSync do cliente: converter para JWK (modulus base64url) e
colocar inline em
client_auth.jwks.keysdopowersync.yaml(repo powersync do cliente), comkidnovo (ex.:<cliente>-ps-v1) eaudiencedo cliente; rebuild + redeploy da imagem (config assada). - Token de write-back: o
INTEGRADOR_API_TOKENda instância do Integrador do cliente (env no Coolify) — é o que vai empowersync.upload.token. O mesmo token autentica o heartbeat. - Montar o pacote:
dist/com o fat jar (./gradlew shadowJar), ointegrador-client.propertiespreenchido (URLs,jwt.kid/issuer/audience/subjectcasando com o passo 2, caminhos do ZIM reais,cliente=<slug>para a observabilidade) e o<cliente>-private.pem. - Validar antes de entregar: rodar
--debugapontando para a instância real — conectar,[r]listar pendentes; processar só com aval do cliente (grava status na nuvem). O programapAbastprecisa estar instalado no ZIM (contrato).
Requisitos¶
- Máquina do ERP (Windows x64) com Java 8 ou superior (
java -version). - ERP X-Adm instalado, com o programa ZIM
pAbastdesta integração (contrato). - Acesso HTTPS de saída para
ps.<cliente>.xadm.bizeint.<cliente>.xadm.biz— só isso: o heartbeat usa o mesmoint.<cliente>do write-back, sem liberação nova de firewall.
Instalar¶
- Crie uma pasta (ex.:
C:\integrador-client) e coloque nela os arquivos entregues: integrador-client-<versão>.jarintegrador-client.properties(a partir dointegrador-client.properties.exemplo)<cliente>-private.pem— credencial da instalação; não copiar, não enviarrodar.bat- Preencha o
integrador-client.properties(o exemplo documenta cada chave; as principais: URLs da nuvem, token de upload, caminho dozimrtmu.exee da pasta do X-Adm).
Caminhos com espaços são aceitos. Use /, não coloque aspas e mantenha cada
propriedade em sua própria linha, por exemplo:
zim.executavel=C:/Program Files (x86)/Zim/7.11/zimrtmu.exe
zim.diretorio=D:/XADM/XADMUSR/xadmXApiMDFe1
java -jar integrador-client-<versão>.jar --version deve imprimir a versão.
4. Inicie com rodar.bat. A primeira linha do log é a versão; em seguida o programa
conecta, sincroniza e passa a processar pendentes a cada ciclo.
5. Para iniciar junto com o Windows: agende o rodar.bat no Agendador de Tarefas
("Ao iniciar o computador", conta local, "Executar estando o usuário conectado ou não").
Modo debug (homologação com a equipe ZIM)¶
java -jar integrador-client-<versão>.jar --debug
Nada roda sozinho: o operador comanda pelo console o que processar —
[i]tem (1 registro = 1 linha no arquivo de envio), [p]edido (o próximo contrato pendente
com cliente/fones/produtos/itens num arquivo só), [t]udo (um ciclo normal),
[r]ecarregar a lista de pendentes, [s]air. Cada comando gera o arquivo de envio, roda o
pAbast e mostra no console as linhas do envio e do retorno entre |pipes| (padding
visível) + o resultado; ao final, a lista de pendentes é recarregada sozinha. No debug os
arquivos dXpEnvio/dXpRetorno ficam no disco (na pasta do X-Adm) para inspeção, com a
máscara do layout anexada ao final e as versões dXpEnvio.json/dXpRetorno.json
(pretty print) ao lado — o próximo processamento apaga e reescreve; no modo normal os
arquivos são apagados após aplicar.
A sincronização com a nuvem continua normal em segundo plano.
Acompanhar¶
- Logs em
logs\integrador-client.log(o dia corrente); ao virar o dia, o anterior é zipado comologs\anomesdia.integrador-client.log.zip(30 dias guardados). - Linha de sucesso de um lote:
lote aplicado: N gravado(s), …seguida doswrite-back … aceito. - Saída do próprio ZIM:
pAbast-saida.logna pasta do X-Adm (sobrescrito a cada lote). Quando o lote falha, as últimas linhas dela vão também para o log do cliente (saída do pAbast …). - Sinal de vida:
heartbeat enviado (inicio)na largada eheartbeat enviado (periodico)a cada hora. A instalação aparece sozinha na aba Deploy do central-ui, com a versão e os fluxos.
Heartbeat (sinal de vida)¶
Na largada e a cada intervalo o programa avisa a plataforma que está vivo: versão, fluxos ativos e nome da máquina. O central acompanha e alerta quando uma instalação passa de 6 horas úteis sem sinal (runbook do central). Não há nada obrigatório a configurar; as duas chaves são opcionais:
| Chave | Padrão | O que faz |
|---|---|---|
heartbeat.url |
o powersync.upload.url com o caminho /api/v1/heartbeat |
endereço do heartbeat; URL malformada para na largada (exit 2) |
heartbeat.intervaloMinutos |
60 |
minutos entre heartbeats; 0 desliga (inclusive o da largada); negativo para na largada |
O heartbeat nunca derruba o programa: qualquer falha só vira WARN no log. O --debug não manda
heartbeat. O contrato é do integrador-server:
heartbeat.md.
Encerrar / reiniciar¶
Fechar a janela (ou finalizar o processo java.exe) é seguro em qualquer momento: lote em
andamento volta a pendente sozinho na próxima subida (recuperação automática), e a fila de
write-back fica guardada no banco local — nada se perde.
Exit codes¶
Seguem a taxonomia da casa (contrato canônico):
| Exit | Significado no integrador-client |
|---|---|
| 0 | encerramento normal (interrupção pedida) |
| 1 | falha inesperada no daemon (ver log) |
| 2 | configuração/ambiente inválido — properties com chave faltando/errada ou JVM 32 bits (a extensão nativa do PowerSync não carrega; rode com Java 64 bits). A mensagem diz qual dos dois |
| 4 | ocupado: outra instância já roda nesta pasta de dados (lock); nada foi feito, seguro tentar depois |
(3 = erro externo não é usado: falha de rede/nuvem não derruba o daemon — o SDK retenta sozinho.)
Problemas comuns¶
| Sintoma | Causa provável | O que fazer |
|---|---|---|
| Sai na hora com "erro de configuração: chave …" (exit 2) | properties incompleto ou caminho errado | corrigir a chave citada na mensagem |
| Sai na hora com "Necessário Java 64 bits …" (exit 2) | rodou com o java 32 bits do PATH |
apontar pra uma JRE 64 bits (o PowerSync só carrega em 64 bits) |
pAbast não terminou em Ns — matando o processo |
falta de licença ZIM ou ZIM travado | nada — o lote tenta de novo sozinho; se persistir, liberar licença |
pAbast terminou com exit code N |
falha geral do ZIM | ver pAbast-saida.log na pasta do X-Adm |
retorno inválido — lote volta a pendente |
pAbast escreveu retorno fora do contrato | conferir o contrato §4 |
retorno do pAbast com N linha(s) de tentativa abortada … descartadas |
o ZIM deu conflito, recomeçou o lote e não limpou o dXpRetorno |
nada — o cliente aproveitou a rodada final e aplicou o lote normalmente; é defeito do ZIM a corrigir (contrato §4 e §8) |
retorno do pAbast ecoou a linha de continuação 'IMPORTA_PIED' como registro — descartada |
o ZIM deu conflito e respondeu também à linha de continuação | nada — os 999 foram aplicados e o lote retenta sozinho; é defeito do ZIM a corrigir (contrato §4 e §8) |
pAbast falhou depois do handshake … Conferir no ERP: … |
o ZIM caiu no meio do lote (exit ≠ 0, travou ou retorno fora do contrato) e pode ter gravado parte | o lote é reenviado sozinho; conferir no ERP os registros citados (cadastro duplicado, item sem pedido). O dXpRetorno e a saída do ZIM estão no log logo acima |
write-back … HTTP 4xx/5xx repetido |
token errado ou nuvem fora | conferir powersync.upload.token; a fila re-tenta sozinha |
heartbeat recusado: HTTP 401 |
token errado (é o mesmo do write-back) | conferir powersync.upload.token |
heartbeat recusado: HTTP 404 |
o integrador-server do cliente ainda não tem a rota do heartbeat (versão antiga) | atualizar o integrador-server do cliente; o programa segue normal enquanto isso |
heartbeat recusado: HTTP 502 |
o integrador-server não conseguiu repassar ao central (central fora, ou o central recusou) | transitório se passar sozinho; se repetir, ver o log do integrador-server (heartbeat_recusado = config errada lá) |
heartbeat recusado: HTTP 503 |
falta CLIENTE_ID ou CENTRAL_API_TOKEN no integrador-server do cliente |
configurar as envs no Coolify do integrador-server |
heartbeat não entregue: … |
rede ou integrador-server fora do ar | nada — o próximo heartbeat tenta de novo |
401 ao conectar no PowerSync |
chave PEM/kid/audience não batem com a instância | conferir chaves jwt.* e o .pem |
Registro parado em 1XX |
erro de dado (terminal) | corrigir o dado na origem (nuvem); o registro não é retentado |
Erros de configuração (exit 2) ficam no console e em logs/integrador-client.log, com a
chave e o caminho a corrigir. Eles não abrem incidente no GlitchTip/Sentry porque exigem
correção local da instalação, não investigação de falha do aplicativo.
O que vai para o GlitchTip¶
O programa roda na máquina do cliente, então o que chega ao GlitchTip (bug.xadm.biz, projeto
integrador-client) é o que dá para diagnosticar de longe. Filtre por cliente:<slug> — o slug é
a chave cliente do .properties.
| Vira incidente | Não vira |
|---|---|
todo WARN e ERROR do programa, com a exceção anexada quando há |
erro de configuração e JVM de 32 bits (ação do operador, não falha do app) |
| remessa travada ou retida, uma vez por causa | conflito de concorrência no dRels3 — o lote retenta sozinho |
| erro terminal no encerramento de MDF-e | o eco do dXpEnvio/dXpRetorno, que viaja dentro do incidente |
Cada incidente carrega junto: as linhas cruas do dXpEnvio e do dXpRetorno do lote (em
blocos, entre |pipes|, como no log local), os marcos do ciclo, e a configuração com que o
processo subiu — com powersync.upload.token e a chave privada mascarados, como no log.
As linhas cruas levam dado de cliente
O layout do pAbast tem cgc_cpf, nome_prop e fantasia: CPF/CNPJ e nome aparecem no
incidente. É decisão consciente — o GlitchTip é hospedado na infraestrutura da X-Adm, não em
serviço de terceiro, e o evento é descartado pela retenção de 90 dias. Quem lê o projeto
é quem já tem acesso ao GlitchTip da casa.
Desligar o envio¶
Suba o programa com a variável de ambiente SENTRY_DSN vazia:
set SENTRY_DSN=
java -jar integrador-client-<versão>.jar
A variável, quando definida, vence o app.json embarcado no jar — vazia desliga, preenchida
manda para o projeto que ela apontar. É assim que o e2e local e o desenvolvimento rodam sem sujar
o painel de produção. Sem a variável definida, vale o DSN do jar.
Para conferir o caminho até o GlitchTip numa instalação nova:
java -jar integrador-client-<versão>.jar --sentry-teste
O evento de teste sai com environment=smoke e cliente=smoke — não se mistura com incidente de
cliente real.
Atualizar versão¶
Parar o programa, trocar o jar pelo novo (mesma pasta), iniciar de novo. O banco local e o properties são preservados.
Ao passar para a primeira versão com heartbeat, o integrador-server do cliente precisa já ter a rota do heartbeat. Na ordem inversa o programa só loga WARN 404 a cada hora, mas o integrador-server registra ERROR a cada chamada autenticada que não acha a rota.
Dev
Guia do código¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-21
Para quem vai mexer no código. O porquê das escolhas está nas decisões; os contratos com o ZIM estão em contrato pAbast. Aqui fica o mapa: onde cada coisa mora. Como rodar na sua máquina está em como rodar.
Princípio de leitura¶
O público deste código é a equipe que desenvolve em ZIM e sabe pouco Java. Por isso: sem framework, sem injeção de dependência, wiring manual e explícito, e duplicidade aceita em prol da leitura. Antes de extrair uma abstração "porque repete", pergunte se ela facilita a vida de quem lê — normalmente não facilita.
Exceção única de linguagem: uma classe Kotlin embrulha o SDK PowerSync (Kotlin
Multiplatform, com suspend/Flow, inviável de Java 8 puro). Nenhuma lógica de negócio
mora nela — só a ponte.
Mapa dos pacotes¶
Base: br.com.xadm.integradorclient.
| Pacote | Papel |
|---|---|
| (raiz) | ponto de entrada e trava de instância: parsing das flags, wiring manual de tudo, guarda de uma execução por pasta de dados |
config |
leitura e validação do .properties, com erro que nomeia a chave faltante |
auth |
JWT auto-assinado (RS256) que autentica o cliente no PowerSync da nuvem |
powersync |
o espelho local: declaração do schema das tabelas, o wrapper Kotlin do SDK, o write-back HTTP do status e o envio do heartbeat (EnvioHeartbeat) |
processo |
o miolo: um processador por fluxo, o agendador serial que os roda, a coleta coerente por remessa, a máquina de status, os avisos e o heartbeat periódico (BatimentoPeriodico) |
zim |
a fronteira com o ERP: os fluxos conhecidos, a escrita do arquivo de envio fixed-width, a leitura do retorno e a execução do zimrtmu.exe |
Quatro pontos que costumam surpreender quem chega:
- Um lote carrega as tabelas de UM fluxo só. O fluxo é que define quais tabelas viajam e qual literal de handshake o ZIM vai ler para saber o que chegou.
- O arquivo de envio e o contrato andam juntos. Mudou uma largura de campo no layout, muda o contrato no mesmo PR — o gate não compara os dois por você.
- O que roda a cada volta do laço não pode lançar. O
Agendadorchama o vigia do write-back e o heartbeat periódico sem try/catch; uma exceção ali derrubaria o daemon. Por issoEnvioHeartbeateBatimentoPeriodicoengolem atéRuntimeExceptione só logam WARN. OBatimentoPeriodicorecebe o envio comoConsumer—processonão conhece o HTTP, e o teste usa um consumidor fake. - O nível do log decide o que abre incidente. Um
LOG.warnouLOG.errornovo vira issue no GlitchTip sozinho, pelo appender do Logback — não há (nem se deve criar) chamada ao SDK no call-site. O que é ação do operador na ponta, e não falha do app, sai pelo logger…integradorclient.operador; o eco do dXp sai pelo…integradorclient.eco. Os dois ficam fora do appender, e oObservabilidadeLogbackTesttrava isso (decisão 0006).
Como compilar e testar localmente¶
Pré-requisitos, comandos do gate e recomendações de ambiente estão em como rodar.
Onde NÃO mexer sem ler antes¶
- Layout fixed-width (
zim): cada campo tem largura e escala fechadas com a equipe ZIM; os testes golden travam byte a byte. Mudança aqui é mudança de contrato. - Casamento envio↔retorno (
processo): por posição de linha no fluxo pied, por contador no encerramento de MDF-e. Divergência invalida o lote inteiro de propósito — nunca aplique resultado desalinhado. Duas tolerâncias, ambas para defeitos dopAbastem conflito: os começos de tentativa abortada que ele deixa antes da rodada final e o eco da linha de continuação no fim do retorno (contrato §4, salvaguardas do reinício e do eco) — não as alargue sem a equipe ZIM. - Coleta por remessa (
processo): o cliente lê as flags que o servidor calcula; ele não deduz dependência entre tabelas (decisão 0004).
Como rodar¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-21
O que basta para compilar, rodar o gate e subir o cliente na máquina do dev. O mapa dos pacotes está no guia do código; instalar na máquina do cliente é outro documento — instalação e operação.
Pré-requisitos¶
- Um JDK instalado (qualquer razoável — o 21 serve). O wrapper baixa o Gradle e o
resolver de toolchain baixa sozinho o JDK de compilação na primeira build: 1× por
máquina, precisa de internet, fica em
~/.gradle/jdks. - Nada de Docker, banco ou serviço de nuvem. Os testes usam SQLite de verdade em pasta
temporária e um
pAbastfalso (scripts emsrc/test/resources/pabast-fake/) — a suíte exercita a plataforma real, não um mock do SDK. - Para rodar o programa de verdade contra um ERP, aí sim:
zimrtmu.exeinstalado, o.propertiesda instalação e o.pemdo cliente (ver instalação e operação).
Passos¶
./gradlew check --no-daemon --console=plain # gate: compila + checkstyle + testes + cobertura
./gradlew test --tests '*ArquivoEnvioTest' # um teste só, quando estiver iterando
./gradlew shadowJar # fat jar em dist/ (o que se distribui)
O alvo de compilação é Java 8 (bytecode 52) mesmo com o build rodando num JDK moderno —
é o que faz o jar rodar no JRE do ERP. Há um teste que confere isso; se ele ficar vermelho,
alguém mexeu no --release.
Recomendações de ambiente¶
- NetBeans: File → Open Project na pasta do repo (projeto Gradle). A IDE pode estar num JDK diferente do de compilação — o toolchain do Gradle resolve, não force nada.
- Windows é o ambiente do dev; a CI é Linux. Alguns testes de integração usam scripts
.shdopAbastfalso e por isso são pulados no Windows — ochecklocal fica verde sem tê-los executado. Antes de taggar, confirme a run da CI verde (ou rode o gate num container Linux), senão a divergência aparece depois da tag. - Console em UTF-8 ao rodar o programa (
chcp 65001, o que orodar.batjá faz), senão o log acentuado sai torto no Windows. - O log vai para stderr + arquivo (
logs/), nunca para stdout: é o contrato de CLI da casa, e é o que deixa a saída padrão livre.
Subir o programa¶
java -jar dist/integrador-client-<versão>.jar # daemon; lê o .properties ao lado do jar
java -jar dist/integrador-client-<versão>.jar --debug # processamento MANUAL pelo console
java -jar dist/integrador-client-<versão>.jar --version # imprime a versão e sai
No modo --debug nada roda sozinho: o operador processa passo a passo e vê as linhas
fixed-width do envio e do retorno — é o modo de homologar o pAbast com a equipe ZIM.
Rodar contra um ERP de verdade exige o zimrtmu.exe, o .properties da instalação e o
.pem do cliente (instalação e operação).
A suíte ponta a ponta local, com o pAbast falso, está em E2E local.
Contrato pAbast¶
Status: Rascunho · Responsável: Gustavo Madruga · Atualizado em: 2026-09-11
Contrato entre o integrador-client (escreve o arquivo de envio, executa o runtime, lê o
retorno) e o programa ZIM pAbast (lê o envio, grava no X-Adm, escreve o retorno).
Este documento é a fonte única do contrato: os dois lados derivam daqui.
Nome do programa: por hora a integração roda sob o nome
pAbast(o mesmo do programa de abastecimento legado — decisão da equipe ZIM). O que identifica ESTA integração (pied-maxsul) na família é a linha de continuaçãoIMPORTA_PIEDdo handshake (§1 passo 5).Um contrato por fluxo: o integrador-client é o cliente coringa da plataforma — este documento cobre o fluxo pied (tabelas do §3) e serve de BASE comum (formato §2, retorno §4, códigos §5, exit codes §6) para os demais. O fluxo abastecimento já tem o seu: contrato-abastecimento.md. Um arquivo de envio carrega SOMENTE as tabelas do fluxo anunciado pelo literal de continuação.
Status: rascunho — validar com a equipe X-Adm antes de fechar: larguras campo a campo, formatos de data e as observações marcadas com ⚠.
1. Fluxo de uma execução (lote)¶
- O cliente apaga
dXpEnvioedXpRetornoremanescentes no diretório do X-Adm (nomes e diretório configuráveis). - O cliente escreve o arquivo de envio completo — todas as linhas do lote (máximo 999 registros; o excedente fica para o lote seguinte — quem garante o limite é o cliente) — ainda sem a linha de continuação do passo 5.
O corte no limite é feito por remessas inteiras, não por posição: uma remessa é o conjunto que o Integrador declarou ter de chegar junto ao ERP, e fatiá-la produziria filho sem pai — o defeito que a decisão 0004 veio matar. Remessa que não couber espera o próximo lote, inteira; o espaço restante é preenchido com linhas que não pertencem a remessa nenhuma, essas sim seguras de cortar. 3. O cliente executa
zimrtmu.exe pAbastcom working directory = diretório do X-Adm. 4. Handshake (restrição do ZIM: o processo não tem stdin/stdout utilizável — a sincronização é por arquivos, herdada da integração de abastecimento do xposto): o programa, assim que sobe, escreve o marcador de início (literalIniciou Zim) como primeira linha do arquivo de retorno e fica esperando. 5. O cliente verifica o arquivo de retorno a cada 100 ms; ao ver o marcador, anexa a linha de continuação (literalIMPORTA_PIED) ao final do arquivo de envio — é o sinal de "arquivo completo, pode processar". Marcador que não vem dentro do timeout de handshake (padrão 3 s, configzim.handshake.timeout) = cliente mata o processo e o lote falha (causa típica: falta de licença ZIM).
Os literais são fixos em código (ZimExecutor) — fazem parte do contrato, não da
instalação. A linha de continuação identifica a integração na família ZIM:
Continuar = abastecimento (xposto) · IMPORTA_PIED = pied-maxsul (este cliente)
· ENCERRA_MDFE = encerramento MDF-e.
6. O pAbast, ao ver a linha IMPORTA_PIED, lê os registros do envio (a linha de
continuação não é registro e é ignorada), grava cada um no X-Adm e escreve o
arquivo de retorno completo antes de encerrar — uma linha de retorno por linha
de envio, na MESMA ordem, inclusive para erros. Registro que falhou também ganha
linha (com o código de erro). O marcador Iniciou Zim era só o sinal de progresso do
passo 4: o ZIM LIMPA o dXpRetorno e regrava o retorno final SEM ele (comportamento de
toda a família ZIM, confirmado pela equipe X-Adm em 2026-08-04) — o retorno final começa
direto nos registros.
7. O cliente só lê o retorno depois de o processo encerrar (timeout de processamento
configurável, padrão 5 minutos); valida (§4) e aplica os códigos; ao final apaga envio e
retorno. Se o marcador Iniciou Zim aparecer na 1ª linha, é descartado; se não (o caso
normal do retorno final), o retorno já começa nos registros — a ausência não invalida.
Falha do lote — o cliente não aplica código nenhum, devolve todas as linhas ao código anterior (pendente) e tenta de novo depois: handshake ausente no prazo, timeout de processamento, exit code ≠ 0, retorno ausente, ou retorno inválido (§4).
O handshake é o ponto de não-retorno. Uma falha antes dele (o marcador não veio, ou o
processo morreu sem escrevê-lo) garante que o ZIM não leu o lote: o reenvio é seguro por
construção. Uma falha depois dele (exit ≠ 0, timeout de processamento, retorno ausente ou
inválido) não garante nada: o ZIM não é transacional e pode ter gravado parte do lote no X-Adm antes
de cair. O cliente reenvia mesmo assim, porque é o único caminho sem intervenção manual, e isso só é
seguro porque toda rotina do pAbast é idempotente (§5). Nesse caso o cliente também:
- registra um aviso (log e GlitchTip) com as chaves do lote, para alguém conferir no ERP — um aviso por lote e motivo, não um por retentativa;
- ecoa no log o
dXpRetornoe a saída do ZIM (pAbast-saida.log), que o lote seguinte sobrescreve.
(Incidente 260077821, 2026-09-09: o ZIM caiu depois de gravar o contrato; no reenvio o contrato
voltou "previamente" e o item, 120.)
2. Formato comum dos arquivos¶
- Texto fixed-width (posições fixas, sem separadores), 1 linha = 1 registro.
- Encoding windows-1252 (ANSI), quebra de linha CRLF.
- Todo registro começa com o contador da linha: inteiro 1..999, largura 3,
alinhado à direita (
1,42,999) — sequencial dentro do arquivo, ecoado no retorno. Em seguida vem o tipo de registro = nome da tabela, texto de largura 20. - Campo texto: alinhado à esquerda, completado com espaço à direita; espaços à esquerda e embutidos são preservados byte a byte; campo não informado = branco (espaços), nunca omitido.
- Campo numérico: largura 18, alinhado à direita (espaços à esquerda), ponto
como separador decimal, escala fixa por campo (arredondamento HALF_UP). Por que 18 para
todos: o
VASTINTdo ZIM é um inteiro escalado de até 15 dígitos significativos (capacidade fixa do tipo — só a escala varia por coluna; verintegracao-zim.mddo int-pied); 15 dígitos + ponto = 16, e 18 dá folga. Ex.:Qtde=2(escala 3) →2.000. - Data:
AAAAMMDD(8). Hora:HHMM(4). Datetime:AAAAMMDDHHMMSS(14) — usado quando o campo representa um instante (data+hora); campos que são só data seguem 8 chars. Segundos são preenchidos com00quando o dado de origem só tem HH:MM.
3. Arquivo de envio (dXpEnvio)¶
Linha = contador(3) + tipo_registro(20) + campos abaixo, na ordem. Offsets contam a
partir de 0 (contador ocupa 0–2, tipo 3–22; o campo 1 de dados começa no offset 23).
3.1 propriedades — cliente/endereço (largura total da linha: 261)¶
| # | Campo | Tipo | Largura | Offset | Observação |
|---|---|---|---|---|---|
| 1 | cgc_cpf | texto | 14 | 23 | CNPJ/CPF, só dígitos |
| 2 | cod_prop | texto | 2 | 37 | código do endereço |
| 3 | nome_prop | texto | 50 | 39 | razão social |
| 4 | fantasia | texto | 30 | 89 | |
| 5 | endereco | texto | 60 | 119 | |
| 6 | cep | texto | 8 | 179 | |
| 7 | bairro | texto | 20 | 187 | |
| 8 | cidade | texto | 30 | 207 | |
| 9 | estado | texto | 3 | 237 | ⚠ gravar com espaço à esquerda (PR) — preservado byte a byte |
| 10 | numero | texto | 6 | 240 | |
| 11 | insc_est | texto | 15 | 246 | IE ou RG |
3.2 fones — contato (largura total: 202)¶
| # | Campo | Tipo | Largura | Offset |
|---|---|---|---|---|
| 1 | cgc_cpf | texto | 14 | 23 |
| 2 | ddd | texto | 5 | 37 |
| 3 | telefone | texto | 10 | 42 |
| 4 | nome | texto | 50 | 52 |
| 5 | texto | 100 | 102 |
3.3 estoque — produto (largura total: 223)¶
| # | Campo | Tipo | Largura | Offset | Observação |
|---|---|---|---|---|---|
| 1 | pied_codigo_alt | texto | 15 | 23 | código do produto no parceiro |
| 2 | ean13 | texto | 13 | 38 | código de barras |
| 3 | cod_prod_alt | texto | 8 | 51 | produto base para o cadastro |
| 4 | nome_prod | texto | 128 | 59 | |
| 5 | venda | numérico 2 dec | 18 | 187 | preço à vista |
| 6 | venda_pz | numérico 2 dec | 18 | 205 | preço a prazo |
⚠ Saldo NÃO vai no envio (é dado que o X-Adm gera) — confirmar com a equipe.
3.4 contratos — cabeçalho do pedido (largura total: 125)¶
| # | Campo | Tipo | Largura | Offset | Observação |
|---|---|---|---|---|---|
| 1 | pied_x_ped | texto | 15 | 23 | código do pedido no parceiro |
| 2 | cgc_cpf_cliente | texto | 14 | 38 | |
| 3 | chave_unid | texto | 5 | 52 | centro de custos — ⚠ espaços à esquerda preservados |
| 4 | cod_mat | texto | 10 | 57 | funcionário — ⚠ espaços à esquerda preservados |
| 5 | dt_em | data | 8 | 67 | emissão (só data) |
| 6 | data_inc | datetime | 14 | 75 | inclusão (data+hora do sistema; junta data_inc e hora_inc do espelho) |
| 7 | vl_tot | numérico 2 dec | 18 | 89 | |
| 8 | nat_op | texto | 5 | 107 | natureza de operação |
| 9 | compl | texto | 2 | 112 | |
| 10 | tp_vda | texto | 1 | 114 | F/C |
| 11 | class_forma | texto | 2 | 115 | forma de pagamento |
| 12 | prazo | data | 8 | 117 | data de entrega |
3.5 itensped — item do pedido (largura total: 125)¶
| # | Campo | Tipo | Largura | Offset |
|---|---|---|---|---|
| 1 | pied_x_ped | texto | 15 | 23 |
| 2 | pied_codigo_alt | texto | 15 | 38 |
| 3 | qtde | numérico 3 dec | 18 | 53 |
| 4 | valor | numérico 2 dec | 18 | 71 |
| 5 | total | numérico 2 dec | 18 | 89 |
| 6 | taxa_frete | numérico 2 dec | 18 | 107 |
3.6 formulas — fórmula (BOM) do kit (largura total: 80) — PROVISÓRIO¶
⚠️ Layout provisório — a reconciliar com a equipe X-Adm (Tiago). O import de FORMULAS no ZIM ainda não teve o layout confirmado. As larguras abaixo herdam o padrão do contrato (numérico 18, §8) e a escala 3 do
qtde(espelhoNUMERIC(19,3), igual aoitensped); opied_formula_id(id sequencial gerado no X-Adm) vai como 1º campo. Sem colunaschave_*(o X-Adm gera as chaves; nunca voltam). Ao confirmar o layout real, revisar esta seção e oArquivoEnvio.linhaFormulasjuntos.
| # | Campo | Tipo | Largura | Offset |
|---|---|---|---|---|
| 1 | pied_formula_id | numérico 0 dec | 18 | 23 |
| 2 | pied_x_ped | texto | 15 | 41 |
| 3 | pied_cod_prod | texto | 6 | 56 |
| 4 | qtde | numérico 3 dec | 18 | 62 |
3.7 Ordem das linhas no lote¶
O cliente escreve o lote na ordem dos fatos: tabelas na sequência
propriedades → fones → estoque → contratos → itensped → formulas (dependência referencial —
formulas por último, pois referencia o produto-kit e o componente) e, dentro de cada tabela,
na ordem de criação (id UUID v7 do espelho — estável mesmo quando um registro falha e é
retentado). O pAbast DEVE processar na ordem do arquivo. O contador (1..999) segue exatamente
essa ordem.
Coerência do lote (não só a ordem). A ordem garante o ARQUIVO; o conteúdo é garantia separada.
O sync grava no banco local do cliente a qualquer momento, inclusive no meio de uma coleta feita
tabela a tabela — o lote sairia com o pedido sem o cliente, e o 1XX que o ZIM devolve é
terminal (incidente do pedido 260079421, 2026-09-08). Por isso o cliente lê os pendentes duas
vezes e só monta o arquivo quando as duas leituras trazem o mesmo conjunto; senão, adia o lote
para o próximo ciclo. Ver decisão 0003.
4. Arquivo de retorno (dXpRetorno)¶
A primeira linha pode ser o marcador de início do handshake (Iniciou Zim, §1 passo 4) —
mas no retorno FINAL o ZIM já o limpou (§1 passo 6), então o normal é o arquivo começar direto
nos registros. O marcador é opcional: se vier, é descartado (não é registro); se não vier,
não invalida. Os registros são uma linha por linha do envio, na MESMA ordem (o casamento é
por posição, conferido pelo contador ecoado). Layout único para todas as tabelas:
| # | Campo | Tipo | Largura | Offset | Conteúdo |
|---|---|---|---|---|---|
| 1 | contador | numérico 1..999 | 3 | 0 | ECO do contador da linha de envio (à direita) |
| 2 | tipo_registro | texto | 20 | 3 | ECO do tipo da linha de envio correspondente |
| 3 | cod_retorno | texto | 3 | 23 | código de retorno (§5) |
| 4 | msg_retorno | texto | até 128 | 26 | mensagem (resto da linha; pode ser branco) |
O pAbast escreve o retorno inteiro antes de encerrar e escreve linha para TODO
registro — sucesso ou erro. O ZIM não devolve chaves (Chave*): só código e mensagem.
Validação no cliente (qualquer falha invalida o lote inteiro — nada é aplicado):
1. se a 1ª linha for o marcador Iniciou Zim (trim), descarta-a; sua ausência não invalida;
2. número de linhas de registro = número de linhas do envio — com duas tolerâncias, as
salvaguardas do reinício e do eco da continuação (abaixo);
3. contador ecoado = posição da linha (1..N);
4. tipo_registro ecoado confere com a linha de envio na mesma posição;
5. linha com pelo menos 26 caracteres e cod_retorno não-branco.
O alinhamento é garantido pelo eco de contador+tipo linha a linha, não pelo marcador; e o arquivo-velho é coberto pelo delete de envio/retorno antes do lote (§1 passo 1) + o handshake, que trunca o
dXpRetornona largada do ZIM. Por isso o marcador pôde deixar de ser exigido.
Salvaguarda do reinício (fora do contrato, tolerada). Em "conflito de transação" o pAbast
recomeça o lote do zero e não limpa o dXpRetorno (confirmado pela equipe X-Adm, Tiago,
2026-09-11): o arquivo sai com o começo da tentativa abortada e, depois, a rodada final completa —
ex. contadores 1,2,1,2,3,…,13 para 13 enviadas. O cliente aceita esse retorno e aplica só as
N últimas linhas (a rodada final) quando, e somente quando, as linhas a mais antes dela:
- formam tentativas que começam no contador 1 e seguem de 1 em 1, sem nenhuma chegar ao fim do envio;
- são idênticas (desprezando espaços no fim) à linha da rodada final de mesmo contador.
Qualquer outro excesso (linha a mais no fim, resposta diferente para o mesmo registro, rodada
inteira repetida) continua invalidando o lote. A rodada final passa pela validação normal (itens 3
a 5). O cliente registra um WARN quando descarta. Por que tolerar: sem isso o lote era reenviado,
e o ERP, que já tinha gravado tudo, respondia 120 no item na segunda execução (a rotina não é
idempotente — §8). O certo continua sendo o pAbast limpar o retorno ao recomeçar.
Salvaguarda do eco da continuação (fora do contrato, tolerada). Em "conflito de transação" o
pAbast responde 999 em todos os registros e escreve também uma resposta para a linha de
continuação — ex. IMPORTA_PIED 999-Conflito de transação no fim —, que não é registro
(§1 passo 6). O cliente descarta a última linha do retorno quando ela é o literal de continuação
do próprio fluxo (sozinho ou seguido de espaço), registra um WARN e valida o resto
normalmente. Em qualquer outra posição, ou com outro literal, o lote segue inválido. Vale também no
retorno casa-por-contador do MDF-e (literal ENCERRA_MDFE). Por que
tolerar: o conflito não grava nada (a tentativa seguinte cadastra do zero), então o certo é aplicar
os 999 e retentar — invalidar deixava as linhas em 000 e disparava o aviso falso de "o ERP pode
ter gravado parte" (§1). O certo é o pAbast não responder à linha de continuação.
4.1 Avisos do processo (dRels3)¶
Quando o X-Adm tem uma crítica sobre o processo (algo que, com tela, apareceria ao usuário — ex.
"lançamento fora da data de fechamento contábil", "XML não copiado para averbação"), o pAbast grava
essas mensagens no arquivo dRels3 (sem extensão, no mesmo diretório do dXpEnvio/dXpRetorno).
O X-Adm o limpa a cada chamada do pAbast, então conteúdo na volta = houve crítica. (Existe também
um dRels sem o 3, menos importante — hoje o cliente não o lê.)
Após executar o pAbast — em qualquer desfecho (é onde a crítica costuma explicar uma falha do
lote) — o cliente:
- lê o
dRels3se existir e descarta linhas em branco; - loga cada linha (
WARN) e, no modo debug, espelha no console; - dispara um aviso no GlitchTip/Sentry (
WARNING) com o conteúdo (agrupado por mensagem) — exceto linhas de conflito (concorrência; o lote retenta sozinho, não é ruído de observabilidade): o filtro é por linha, então a crítica ainda vai ao log/console, mas só as linhas SEM "conflito" alarmam; se nenhuma sobrar, nada é disparado; - apaga o
dRels3(já ficou no log).
5. Códigos de retorno¶
| Código | Significado | Ação do cliente |
|---|---|---|
006 |
gravado com sucesso | encerra o registro |
1XX |
erro terminal de dado (ex. cliente inexistente) | encerra com erro; não retenta |
9XX |
erro de processamento reprocessável (ex. lock) | retenta no próximo ciclo |
(000 = pendente e 001 = em processamento são estados do espelho na nuvem — nunca aparecem
no arquivo de retorno.)
006 só descreve o registro da própria linha. 006 = ESTE registro (desta linha, desta tabela)
está no X-Adm, gravado agora ou já existente. Se o pAbast não gravou nem encontrou o registro da
linha, o código é 1XX, mesmo que outra entidade relacionada exista. Um 006 com a mensagem de
outra entidade esconde o erro para sempre: o re-arme do Integrador só reabre 1XX, e a linha nunca
mais é reprocessada.
Idempotência — obrigatória em toda rotina. O cliente reenvia um registro sempre que não recebeu
a resposta dele: falha depois do handshake (§1), recuperação de linha presa em 001 após queda do
cliente, re-arme de remessa pelo Integrador. Por isso:
- registro que já existe no X-Adm volta
006com a mensagem da própria entidade (ex. "Pedido (X) cadastrado previamente com o código …") — nunca um erro, nunca um segundo cadastro; - o registro que depende dele (item → pedido, fórmula → produto) encontra o pai já existente do mesmo jeito que encontraria o pai gravado na mesma execução.
6. Exit codes do processo¶
| Exit | Significado |
|---|---|
| 0 | pAbast rodou e escreveu o retorno (mesmo que com erros por registro) |
| ≠0 | falha geral (sem licença, erro de runtime) — lote inteiro é retentado |
7. Exemplo¶
Envio (2 registros — larguras encurtadas na exibição, · = espaço; a linha IMPORTA_PIED
é anexada pelo cliente durante o handshake, §1 passo 5):
··1propriedades········11222333000144·01Cliente·Exemplo·Ltda···…
··2contratos···········E2E-PED-1······11222333000144···SB·001······2026072020260720143000···········1234.56VDA··01C0120260725
IMPORTA_PIED
Retorno final correspondente (o ZIM já limpou o marcador Iniciou Zim — §1 passos 6-7 —, então
começa direto nos registros; se o marcador aparecer, o cliente o descarta):
··1propriedades········006Gravado
··2contratos···········112Cliente·nao·encontrado
8. Pontos em aberto (validar com a equipe X-Adm)¶
- Larguras/escalas campo a campo (derivadas de
int-pied/docs/projeto/modelagem.md§1). Numéricos: largura única 18 (VASTINT = até 15 dígitos significativos escalados) — a equipe só precisa confirmar as escalas por campo e que opAbastlê os 18 à direita. estoque.Saldofora do envio — confirmar.- Formato de
prazo(texto AAAAMMDD) e dehora_inc(HHMM). - Comportamento do
pAbastsem licença ZIM disponível: exit ≠ 0 imediato (preferido) ou espera? O cliente cobre os dois (timeout de handshake). - Marcador de início: assumido
Iniciou Zim(mesmo literal do pAbast) para toda a família — confirmar que opAbastusa o mesmo texto. A linha de continuação já está fechada:IMPORTA_PIED(fixa em código noZimExecutor, junto com as irmãsContinuar/abastecimento eENCERRA_MDFE/MDF-e). - Pedidos-kit de 2026-09-08/09 (diagnóstico do maxsul-pied, 2026-09-10) — lado ZIM:
estoquedo produto-kit (código = número do pedido) respondeu006com a mensagem do pedido ("-Pedido (X) cadastrado previamente …") quando já havia contrato com aquele número, e o produto não foi criado. Viola o §5 ("006só descreve o registro da própria linha").contratoscriou contrato novo para pedidos cujo contrato anterior a rotina deestoqueencontrou (8 pedidos, possível duplicidade no ERP): as duas rotinas procuram o pedido por chaves diferentes?itenspedcom o contrato já existente ("previamente") respondeu "-Pedido () não cadastrado": a rotina não acha o pedido pré-existente (viola a idempotência do §5), e a mensagem imprime o valordo item no lugar do número do pedido. O layout do envio (§3.5) é o mesmo dos kits que deram certo.- Retorno não limpo depois de conflito: o
pAbastrecomeça o lote e acrescenta a rodada nova aodXpRetornosem truncar (Tiago confirmou em 2026-09-11; pedidos 260080333, 260077821, 260079756 e 260079648). O cliente tolera o formato (§4, salvaguarda do reinício), mas a correção é do lado ZIM. - Em conflito de transação o
pAbastresponde também à linha de continuação (IMPORTA_PIED 999-Conflito de transaçãono fim do retorno; pedidos 260079567 e 260075187, 2026-09-09/10). O cliente descarta esse eco (§4, salvaguarda do eco da continuação), mas a correção é do lado ZIM. - O
zimrtmu.exeabre processo filho? No timeout o cliente mata só o processo que iniciou (o Java 8 não alcança a árvore); um filho vivo poderia processar o arquivo do lote seguinte. - O
pAbastescreve o retorno aos poucos ou só no fim? Se for aos poucos, o cliente poderia aplicar as linhas já respondidas quando o ZIM cai no meio, em vez de reenviar o lote todo. formulas(§3.6) segue provisório: confirmar o layout.
Contrato do fluxo abastecimento¶
Status: Rascunho · Responsável: Gustavo Madruga · Atualizado em: 2026-07-27
Contrato do fluxo abastecimento do integrador-client (cliente coringa): backport do
integrador do xposto — a tabela-espelho abastecimento replica a ABASTECIMENTO daquele
integrador (só os campos que vão ao X-Adm) e é gravada no ERP pelo mesmo programa ZIM.
Status: rascunho — validar com a equipe X-Adm. O fluxo está implementado no cliente mas inativo: o espelho
abastecimentoainda não existe no servidor/PowerSync, e oMainroda só o fluxo pied. Ativar = sincronizar o espelho e ligar o fluxo noMain.
1. O que vale daqui e o que vem do contrato base¶
Tudo que é comum — fluxo da execução (limpeza → envio → handshake → processamento →
retorno), formato dos arquivos (fixed-width, windows-1252, CRLF, contador+tipo), regras de
campo (texto/número/data), retorno com eco e cod_retorno, códigos 006/1XX/9XX, exit
codes — vem do contrato base §§1-2 e 4-6. Este documento só define o
que é específico do fluxo: a linha de continuação e o layout da tabela.
- Linha de continuação (handshake §1 passo 5):
Continuar— herdada do integrador do xposto; é como o ZIM sabe que o lote é de abastecimento. - Decisão de formato: o xposto usa um layout legado próprio (larguras irregulares,
retorno
S/N+ motivo). AQUI o fluxo converge ao contrato base: mesmos campos e mesma ordem doescreverXpEnviodo xposto, mas no formato comum (contador+tipo, números em 18 posições, datetime 14, retorno comcod_retorno). O lado ZIM precisa acompanhar.
2. Arquivo de envio — tabela abastecimento (largura total da linha: 275)¶
Linha = contador(3) + tipo_registro(20) = abastecimento + campos abaixo. Offsets a
partir de 0 (campo 1 começa no offset 23). Ordem = a do escreverXpEnvio do xposto.
| # | Campo | Tipo | Largura | Offset | Observação |
|---|---|---|---|---|---|
| 1 | abastec_bico_global | numérico 0 dec | 18 | 23 | contador global do bico |
| 2 | id_almoxarifado | texto | 2 | 41 | |
| 3 | abastec_bico_diario | numérico 0 dec | 18 | 43 | contador diário do bico |
| 4 | bico | texto | 3 | 61 | |
| 5 | data_hora_abastecimento | datetime | 14 | 64 | AAAAMMDDHHMMSS |
| 6 | encerrante_inicial | numérico 3 dec | 18 | 78 | |
| 7 | encerrante_final | numérico 3 dec | 18 | 96 | |
| 8 | duracao | texto | 6 | 114 | HHMMSS — o espelho traz duracao_abastec_segundos (segundos); o cliente converte |
| 9 | volume_abastecido | numérico 3 dec | 18 | 120 | |
| 10 | abastecimento_autorizado | texto | 1 | 138 | S/N |
| 11 | data_hora_captura | datetime | 14 | 139 | AAAAMMDDHHMMSS |
| 12 | id_usuario | texto | 6 | 153 | |
| 13 | id_motorista | texto | 6 | 159 | |
| 14 | id_produto | texto | 6 | 165 | |
| 15 | placa | texto | 10 | 171 | |
| 16 | odometro | numérico 0 dec | 18 | 181 | |
| 17 | id_empresa | texto | 4 | 199 | |
| 18 | odometro_anterior | numérico 0 dec | 18 | 203 | |
| 19 | limite_odometro | numérico 0 dec | 18 | 221 | |
| 20 | media_sugerida | numérico 3 dec | 18 | 239 | |
| 21 | tolerancia_media | numérico 3 dec | 18 | 257 |
3. Arquivo de retorno¶
Idêntico ao contrato base §4: Iniciou Zim na 1ª linha, depois uma
linha por registro com eco do contador/tipo + cod_retorno + msg_retorno. Os casos que o
xposto tratava por texto do motivo viram códigos:
| Situação (retorno legado do xposto) | Código no contrato novo |
|---|---|
S (importado) |
006 |
N + "ja foi importado no banco de dados" |
006 (idempotente — já está lá) |
N + "conflito" |
9XX (reprocessável) |
N + outro motivo |
1XX (erro terminal; motivo em msg_retorno) |
4. Pontos em aberto (validar com a equipe X-Adm)¶
- Escalas e larguras (herdadas do xposto: encerrantes/volume/média/tolerância 3 dec; contadores/odômetros inteiros) — confirmar leitura em 18 posições.
- Mapeamento dos motivos legados para
006/1XX/9XX(§3) — a equipe ZIM define os códigos concretos. - Convergência do lado ZIM ao formato do contrato base (o pAbast atual do xposto lê o layout legado).
Contrato do fluxo ENCERRA_MDFE¶
Status: Rascunho · Responsável: Gustavo Madruga · Atualizado em: 2026-07-30
Contrato do fluxo encerramento de MDF-e do integrador-client (cliente coringa): o laço de
baixa automática de MDF-e por macro Sascar termina no ERP aqui — o int-sascar detecta o fim da
viagem, o integrador-server grava um comando de baixa pendente no espelho, o cliente manda a
ChaveNota do MDF-e ao ZIM via pAbast (com o literal ENCERRA_MDFE) e o ZIM encerra o
MDF-e. A responsabilidade do cliente termina no sucesso (006); a confirmação I→A
(NFEEvento.SitEvento=A) é feita no servidor (fora do cliente).
Status: rascunho — a definição do dXpEnvio/dXpRetorno chegou da equipe X-Adm (2026-07-30, arquivos em
docs/anexos/privado/mdfe/+ msgs do Diego); este documento a formaliza no modelo do pied (divergências no retorno: casa por contador — fora de ordem/parcial — e alerta no erro terminal, §4). O retorno já rodou em produção (2026-08-04): o ZIM não escreveIniciou Zimno arquivo final (marcador virou opcional — §4/§5). Falta as sync rules (010) exporemmdfao cliente.
1. O que vale daqui e o que vem do contrato base¶
Este fluxo segue o MODELO COMUM (pied); o formato dos arquivos (fixed-width, windows-1252,
CRLF, contador(3) + tipo_registro(20)), handshake Iniciou Zim, retorno com eco +
cod_retorno, códigos 006/1XX/9XX e exit codes vêm do contrato base
§§1-2 e 4-6. Muda o quê vai na linha (tabela mdf, único campo ChaveNota) e o retorno
(§4).
- Linha de continuação (handshake §1 passo 5):
ENCERRA_MDFE— é como o ZIM sabe que o lote é de encerramento de MDF-e (anexada pelo cliente ao final do envio, como oIMPORTA_PIEDdo pied). - Tabela única
mdf, um campo (chave_nota). Otipo_registroémdf; o único campo de dados é a ChaveNota. Envio idêntico ao modelo do pied. - Divergências no retorno (§4): casa pelo valor do contador — pode vir fora de ordem
(o X-Adm reordena por filial) e parcial (o ZIM aborta num conflito; as linhas que não
voltarem retentam). E um
1XXdispara alerta (GlitchTip). O pied é estrito e sem alerta.
Decisão 2026-07-30: a amostra crua do ZIM (
docs/anexos/privado/mdfe/) vinha diferente (sem contador, sem tipo, retorno textualOK/ERROcom a chave ecoada e um cabeçalho de lote). Aceitou-se seguir o modelo do pied — o ZIM alinha o lado dele; os arquivos crus ficam como referência ao lado dos exemplos no padrão.
2. Arquivo de envio — tabela mdf (largura total da linha: 42)¶
Linha = contador(3) + tipo_registro(20) = mdf + chave(19). Offsets a partir de 0 (o campo
1 de dados começa no offset 23), como no contrato base §3.
| # | Campo | Tipo | Largura | Offset | Observação |
|---|---|---|---|---|---|
| 1 | chave_nota | texto | 19 | 23 | ChaveNota já formatada pelo espelho — o cliente copia os 19 caracteres verbatim (ex. 1 0R 34501$ U 1); espaços à esquerda/embutidos preservados byte a byte |
ChaveNota já formatada (decisão 2026-07-30): o espelho (integrador-server, tabela
mdf, colunachave_notaCHAR(19)sem trim na gravação) entrega a string de 19 posições pronta; o cliente não monta a chave a partir de campos — só a copia.
3. Espelho (schema PowerSync)¶
Tabela mdf do espelho (integrador-server 009, migration V24__mdfe_espelho.sql). O
fluxo declara em SchemaTabelas só o que usa: chave_nota (a ChaveNota que vai ao ZIM) +
cod_retorno/msg_retorno (status). As demais colunas do espelho (situacao_baixa — ATIVO/
BAIXA/ERRO, própria do Integrador — e o contexto do encerramento) ficam fora (padrão 001-R1) —
coluna não declarada não sincroniza. O comando pendente é cod_retorno='000' (009 item 12).
4. Arquivo de retorno¶
Mesmo layout do pied (contrato base §4): o marcador Iniciou Zim é
opcional (o ZIM o escreve no handshake e depois LIMPA o dXpRetorno, regravando o retorno
final SEM ele — o normal é o arquivo começar direto nos registros); se aparecer, é descartado.
Cada linha processada = contador(3) (eco) + tipo_registro(20) (eco = mdf) + cod_retorno(3)
+ msg_retorno (resto). Casamento por contador (§4 abaixo). Sem cabeçalho de lote e sem chave
ecoada — a chave_nota de cada linha vem do envio na posição do contador.
Incidente 2026-08-04: a 1ª entrega do encerra-mdfe rejeitava TODO retorno porque exigia
Iniciou Zimna 1ª linha do arquivo final — que o ZIM não escreve (ele limpa e regrava). O X-Adm dava baixa com006, mas o cliente marcava lote inválido e reenviava sem parar (loop apertado, sem backoff). Corrigido: marcador opcional na leitura + o {@code Agendador} desacelera quando um lote deixa pendência (não reprocessa no eco das próprias write-backs). VerCHANGELOG(Não lançado).
| # | Campo | Tipo | Largura | Offset | Conteúdo |
|---|---|---|---|---|---|
| 1 | contador | numérico 1..999 | 3 | 0 | ECO do contador da linha de envio (à direita) |
| 2 | tipo_registro | texto | 20 | 3 | ECO do tipo (mdf) |
| 3 | cod_retorno | texto | 3 | 23 | código de retorno (§5 abaixo) |
| 4 | msg_retorno | texto | resto | 26 | mensagem (motivo do sucesso ou do erro) |
Retorno CASA-POR-CONTADOR: fora de ordem e parcial (Diego/Gustavo, 2026-07-30)¶
Diferente do pied (uma linha por linha, na MESMA ordem), o retorno do MDF-e casa pelo valor do contador ecoado — cada linha aplica na posição do envio dada pelo seu contador, então pode vir:
- Fora de ordem — o X-Adm às vezes reordena (ex. por filial): os contadores chegam embaralhados
(
…13, 15, 16, 14, 17). O contador diz a posição; a ordem no arquivo não importa. - Parcial — num conflito o ZIM aborta o resto: só um subconjunto dos contadores volta. As linhas cujo contador NÃO voltou (ausência) são consideradas não processadas e voltam a pendente para retentar.
Exemplo do Diego: envio 10 linhas; o ZIM devolve
006nas 3 primeiras,999(conflito) na 4ª e para. Resultado: 1–3 encerram (006), a 4ª retenta (999), 5–10 sem resposta retentam. Inválido: sobrar linha, contador fora de 1..N, contador repetido, tipo ecoado ≠ o do envio naquela posição. Implementação:Fluxo.getRetornoCasaPorContador()(só o MDF-e) +Processadorrestaura as posições não cobertas. Retry por LOTE segue para falha geral (timeout/exit≠0).
Alerta no erro terminal (Diego/Gustavo, 2026-07-30)¶
Um 1XX (erro terminal) no encerramento dispara um evento GlitchTip/Sentry (nível ERROR) com a
empresa (tag cliente, já global) e o contexto (a chave_nota + a mensagem do ZIM) — o
alerta do GlitchTip manda o email para avaliação humana. Só o MDF-e
(Fluxo.getAlertaNoErroTerminal()); nos demais fluxos o 1XX fica só no cod_retorno/log.
Códigos de retorno (os mesmos do pied — contrato base §5)¶
O cod_retorno é numérico: o ZIM mapeia o resultado do encerramento (o OK/ERRO da
amostra crua) para o código, e o cliente aplica pela máquina de status, sem interpretar texto.
| Código | Significado | Ação do cliente |
|---|---|---|
000 |
pendente (comando de baixa gravado pelo servidor na mdf) |
entra no próximo lote |
001 |
em processamento (lote em voo neste cliente) | — (estado transitório) |
006 |
encerrado com sucesso | encerra o registro; o servidor marca NFEEvento.SitEvento=A (fora do cliente [L4]) |
1XX |
erro terminal (ex. MDF-e não autorizado) | encerra com erro; não retenta ("quando não deve mais nem tentar" — Diego) |
9XX |
reprocessável (transitório) | retenta no próximo ciclo ("quando não voltar nada daquela chave" — Diego) |
000e001são estados do espelho na nuvem — nunca aparecem no arquivo de retorno. Retry de falha GERAL (timeout/exit≠0/retorno malformado) é por LOTE (todos voltam a pendente); retry por AUSÊNCIA (linha não devolvida no retorno parcial) é por LINHA (só as que faltaram) — ver §4.
5. Pontos em aberto (validar com a equipe X-Adm / ZIM)¶
A equipe X-Adm segue o modelo do pied no lado ZIM. A amostra crua vinha diferente (sem
contador, sem tipo, retorno textual OK/ERRO com a chave ecoada e um cabeçalho de lote); tudo
isso foi alinhado ao padrão — a amostra é referência, não o formato-alvo. O ZIM escreve o
retorno como o pied: contador + tipo(mdf) + cod_retorno numérico + msg, sem cabeçalho de lote.
~~Marcador
Iniciou Zimna 1ª linha do retorno final~~ RESOLVIDA (Diego/X-Adm, 2026-08-04): o marcador é só o sinal de progresso do handshake — o ZIM o escreve ao subir e depois limpa odXpRetornoe regrava o final SEM ele, em toda a família ZIM (xposto/pied/ mdfe). O cliente não o exige mais no retorno final (é opcional): ver §4 e o incidente 2026-08-04.
- ~~Forma do comando/espelho~~ RESOLVIDA: o
009(migrationV24) criou a tabelamdfcomchave_nota/situacao_baixa/cod_retorno/msg_retorno; o comando pendente écod_retorno='000'namdf. §2/§3 usam os nomes reais. Falta só as sync rules (010) exporemmdfao PowerSync do cliente (ainda não nopowersync.yamlda maxsul). - ~~Granularidade do retry / ordem do retorno~~ RESOLVIDA (Diego/Gustavo): retorno casa
por contador — fora de ordem e parcial; as linhas que não voltarem retentam por linha (§4). Só o
MDF-e (
getRetornoCasaPorContador); pied/abastecimento seguem estritos.
6. Exemplo¶
Envio (3 registros — a linha ENCERRA_MDFE é anexada pelo cliente no handshake; · = espaço):
··1mdf·················1·0R·34501$·U····1
··2mdf·················1·0R·34525$·U····1
··3mdf·················1·4R··7808$·U····1
ENCERRA_MDFE
Retorno final correspondente (o ZIM já limpou o marcador Iniciou Zim, então começa direto nos
registros) — igual ao pied: contador + tipo(mdf) + cod_retorno + msg (a chave de cada linha
vem do envio na posição do contador):
··1mdf 006MDF-e 34501 já foi encerrado anteriormente. Processo Cancelado.
··2mdf 006Encerramento do MDFe realizado com sucesso.
··3mdf 106MDF-e não autorizado. Não permite ser encerrado
Exemplos completos (14 registros) em docs/anexos/privado/mdfe/:
dxpenvio-exemplo/dXpRetorno-exemplo— o padrão canônico (retorno na ordem do envio);dxpenvio/dXPRetorno— demo da equipe X-Adm com o retorno REORDENADO (os contadores chegam…13, 15, 16, 14, 17— a linha 14 depois da 15/16), provando o casamento por contador;codigos.txt— a tabela de códigos (§5).
O padrão-alvo é o que a equipe X-Adm segue; o cliente casa por contador (§4), então a ordem do arquivo não importa.
E2E local — nuvem → ERP → nuvem¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-20
Roteiro para provar o fluxo de entrada COMPLETO na máquina de desenvolvimento, usando o
harness xadm/etc/tests/e2e-pied-local (Postgres + Mongo + PowerSync + Integrador +
int-pied + integrador-client) e um pAbast fake (que faz o handshake e ecoa 006) no
lugar do zimrtmu.exe. O harness é Windows-nativo (scripts PowerShell + Docker Desktop);
os .sh/zimrtmu.exe POSIX que ainda estão na pasta são legado da fase WSL.
Subir e testar (Windows / PowerShell)¶
Dois modos. O simulado (recomendado p/ provar só integrador↔integrador-client↔ZIM) sobe a
stack SEM o int-pied e injeta o pedido direto no Integrador via seed-curl.ps1 — um comando
fecha o ponta a ponta:
cd C:\Work\Xadm\etc\tests\e2e-pied-local
.\startup.ps1 -SemIntPied # sobe tudo menos o int-pied (Docker + Integrador + client)
.\seed-curl.ps1 -Esperar # injeta um pedido (PUT /api/v1/xadm) e espera o cod_retorno voltar
# -> "ponta a ponta OK - contratos.E2E-SIM-1 cod_retorno=006"
.\shutdown.ps1 # ou -Wipe para zerar o banco
Java 25: o integrador-server (Micronaut 5.x) exige JVM 25 para buildar e rodar; o
_config.ps1aponta$JAVApara o JDK 25. O integrador-client é--release 8e roda em qualquer JDK. Ostartup.ps1builda os jars que faltarem.
O modo completo (com int-pied real + seed.sh/captura.json) segue no RUNBOOK.md do
harness. Injeção manual equivalente ao seed-curl.ps1, para referência:
$body = @{ Origem='INT-PIED'
propriedades=@(@{CgcCpf='55666777000188';CodProp='01';NomeProp='Cliente E2E';Cidade='Ponta Grossa';Estado=' PR'})
estoque=@(@{CodigoAlt='GER-5KW';NomeProd='Gerador 5kW';Venda=18990.00;VendaPz=19990.00})
contratos=@(@{xPed='E2E-F6-1';CgcCpfCliente='55666777000188';VlTot=18990.00;TpVda='C'})
itensped=@(@{xPed='E2E-F6-1';CodigoAlt='GER-5KW';Qtde=1;Valor=18990.00;Total=18990.00}) } | ConvertTo-Json -Depth 6
Invoke-RestMethod -Method Put http://localhost:8081/api/v1/xadm -Headers @{Authorization='Bearer dev-e2e-token'} -ContentType application/json -Body $body
docker compose exec -T postgres psql -U postgres -d xadm_e2e -c "SELECT pied_x_ped, cod_retorno, msg_retorno FROM contratos;"
pAbast fake handshake-aware: no Windows o
zim.executavelaponta parazim-fake/zimrtmu.cmd, que delega aozimrtmu-fake.ps1(preserva windows-1252/CRLF/espaços à esquerda). O fake faz a dança do handshake — escreveIniciou Zim, esperaIMPORTA_PIED, então ecoa006— igual aosrc/test/resources/pabast-fake/feliz.bat. Um fake sem handshake faz o cliente atual matar o processo e retentar o lote pra sempre.
O caminho percorrido: PUT /api/v1/xadm grava as tabelas-espelho com cod_retorno=000 →
PowerSync replica ao SQLite local do cliente → Processador coleta o lote na ordem dos fatos,
marca 001, escreve o dXpEnvio, roda o pAbast (fake), lê o dXpRetorno → aplica 006 →
o SDK faz o write-back (POST /api/v1/powersync) → o Postgres da nuvem termina com o status.
Com o manifesto ligado, o
PUTtambém cria a remessa e o cliente só coleta o que ela cobre inteiro (decisão 0004). Enquanto as sync rules domaxsul/powersyncnão publicaremintegracao_remessa/integracao_remessa_item, o e2e roda pelo caminho por presença — o mesmo de antes —, com o detalhe de que uma linha sem remessa cumpre 3 ciclos de carência se houver manifesto no banco local. Num e2e sem manifesto nenhum, não há carência e o comportamento é idêntico ao descrito acima.
Evidência¶
Windows, modo simulado (2026-07-28): .\startup.ps1 -SemIntPied + .\seed-curl.ps1 -Esperar
com o fake handshake-aware (zimrtmu.cmd→zimrtmu-fake.ps1):
Injetando pedido E2E-SIM-1 no Integrador (PUT /api/v1/xadm)... resposta: {"requestId":1,"resultado":"SUCESSO"}
Processador - lote com 5 registro(s) pendente(s)
EnvioRetornoHttp - write-back propriedades|fones|estoque|contratos|itensped aceito
Processador - lote aplicado: 5 gravado(s), 0 erro(s) terminal(is), 0 para retentar
ponta a ponta OK - contratos.E2E-SIM-1 cod_retorno=006 (todas as 5 tabelas: 006 Gravado pelo fake)
Linux/WSL, modo completo (execução histórica de 2026-07-20, ambiente zerado com --wipe):
Log do integrador-client:
16:47:11.664 INFO Processador - lote com 4 registro(s) pendente(s)
16:47:11.742 INFO Processador - lote aplicado: 4 gravado(s), 0 erro(s) terminal(is), 0 para retentar
16:47:12.123 INFO EnvioRetornoHttp - write-back propriedades id=2e53a8e3-… aceito (requestId=6)
16:47:12.169 INFO EnvioRetornoHttp - write-back estoque id=729b335c-… aceito (requestId=7)
16:47:12.205 INFO EnvioRetornoHttp - write-back contratos id=60a52038-… aceito (requestId=8)
16:47:12.244 INFO EnvioRetornoHttp - write-back itensped id=0173bab2-… aceito (requestId=9)
Postgres da nuvem ao final:
t | chave | cod_retorno | msg_retorno
--------------+-----------------+-------------+-------------------
contratos | E2E-F6-1 | 006 | Gravado pelo fake
itensped | E2E-F6-1 | 006 | Gravado pelo fake
propriedades | 55666777000188 | 006 | Gravado pelo fake
estoque | GER-5KW | 006 | Gravado pelo fake
Notas¶
- A chave dev do harness (
keys/dev-private.pem, kidpoc-key-v1) é gerada localmente e o JWK público correspondente vive inline nopowersync/config.yamldo harness — nada de produção. - Comportamento do SDK observado e coberto: enquanto houver CRUD local sem upload confirmado, checkpoints novos NÃO são aplicados (offline-first) — com o write-back real a fila anda e o download segue.
- Contra o ZIM real, basta trocar
zim.executavel/zim.diretoriono properties — o contrato é o dedev/contrato-pabast.md.
Backoff em erro transiente (baixa de MDF-e)¶
O harness etc/tests/local/e2e-sascar-mdfe prova ponta a ponta a desaceleração/backoff do cliente
quando o ZIM devolve 9XX (reprocessável) — o bug do retry frenético. O zim-fake responde 933
nas primeiras K tentativas (arquivo zim-fake/transiente.count) e 006 depois; o seed-backoff.ps1
dirige a baixa e assere no log que o item foi retentado poucas vezes com intervalo crescente
(piso 1s → ~2s → ~4s), não em loop apertado, terminando em 006. Cobre o comportamento do
Agendador/Processador (desaceleração no 9XX aplicado) num ambiente real.
O irmão seed-writeback-down.ps1 derruba o integrador-server durante a write-back. Achado do e2e:
o PowerSync Kotlin 1.13.x não re-tenta o upload que falhou quando a nuvem volta (nem o cliente
Kotlin nem o Rust; upgrade pra 1.14.1 não corrige e regride o teardown no Windows) — a operação fica
presa na ps_crud até reconectar. Por isso o cliente tem o VigiaWriteBack: se a fila fica presa
20s, reconecta e re-dispara o upload. O seed prova o ciclo completo: servidor fora → nuvem retida → servidor volta → vigia reconecta →
006reconcilia. Evidência no log:VigiaWriteBack - write-back preso: N operação(ões) na fila há ~20s — reconectando.
Etapas do Projeto
01 — Fundação: o fluxo de entrada ponta a ponta¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-09
Etapa reconstruída
Esta e as demais etapas foram escritas retroativamente na migração para a constituição 1.4.5, a partir do CHANGELOG, das decisões e do código. Descrevem o delta de cada momento; o estado consolidado é a Documentação Completa.
Entregue em v0.0.1–v0.0.3 (2026-07-21).
Escopo¶
Fechar o último trecho da plataforma de integração: as tabelas-espelho ficavam pendentes no banco do cliente sem ninguém que as gravasse no ERP. Esta etapa cria o programa que roda na máquina do cliente, recebe o dado da nuvem e grava no X-Adm chamando o runtime ZIM.
Fora de escopo: mais de um fluxo por instalação (etapa 02), observabilidade remota (etapa 02), qualquer garantia de conjunto entre linhas (etapa 05).
O que foi construído¶
- Sincronização PowerSync das tabelas-espelho para um SQLite local, offline-first, com JWT auto-assinado RS256 aceito pela instância real; schema enxuto (só o que o cliente usa) e uma única classe Kotlin embrulhando o SDK.
- Gateway ZIM: arquivo de envio fixed-width, execução do
zimrtmu.execom timeout e leitura validada do retorno, com matching por posição de linha. - Processador de lotes na ordem dos fatos, com a máquina de status
000 → 001 → 006 | 1XX | 9XX, restauração integral em falha e recuperação de001no startup. - Write-back do status ao Integrador, com fila offline-first quando a nuvem está fora.
- Modo
--debug: processamento manual pelo console, item a item ou pedido a pedido, para homologar o programa ZIM passo a passo com a equipe do ERP. - Distribuição: fat jar + properties de exemplo + scripts em
dist/, e o runbook de instalação e operação.
Contratos¶
O contrato pAbast nasce aqui, campo a campo — é a fonte única
dos dois lados. Ainda em v0.0.3 ele foi convergido ao que já rodava em produção: escalas
monetárias em 2 casas e data_inc+hora_inc fundidos num datetime de 14 caracteres.
Decisões¶
| Decisão | Assunto |
|---|---|
| 0001 | runtime Java 8 — exceção registrada à constituição |
02 — Cliente coringa: N fluxos por instalação¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-09
Entregue em v0.0.4–v0.0.5 (2026-07-28 a 2026-07-31).
Escopo¶
O cliente nasceu com um fluxo fixo em código. A plataforma precisa do contrário: o mesmo binário envia dados de várias origens ao X-Adm, e o lado ZIM identifica cada uma pelo literal de handshake — o nome do programa é compartilhado.
O que foi construído¶
- Handshake por arquivos com o runtime ZIM: o programa escreve
Iniciou Zim, o cliente responde com a linha de continuação do fluxo (IMPORTA_PIED,Continuar,ENCERRA_MDFE). - Multi-fluxo em série: N fluxos ativos por instalação, um processador por fluxo, todos
no mesmo par de arquivos, rodados por um agendador de thread única — nunca concorrem.
A chave
zim.fluxospassou a nomear os fluxos ativos (e, emv0.0.7, a ser obrigatória). - Fluxo abastecimento (
Continuar), backport do integrador do xposto: implementado e pronto, inativo até o espelho existir no servidor. - Fluxo ENCERRA_MDFE: baixa de MDF-e no ERP, com duas divergências próprias — o retorno casa por contador (pode vir fora de ordem e parcial) e o erro terminal dispara alerta.
- Observabilidade: erros sobem para o GlitchTip, com a chave
clientedo properties virando tag — o DSN é compartilhado, e sem a tag não se sabe qual instalação gerou o erro.
Contratos¶
Contrato do abastecimento e contrato do ENCERRA_MDFE — ambos seguem o formato comum do pied; o que muda é o conjunto de tabelas e o literal.
Decisões¶
| Decisão | Assunto |
|---|---|
| 0002 | N processadores em série num par de arquivos único |
03 — Resiliência: backoff, fila destravada e os avisos do ERP¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-09
Entregue em v0.0.6–v0.0.10 (agosto de 2026), com o ajuste do teto em v0.0.12.
Escopo¶
A fundação provou o fio; o que faltava era comportamento sob falha. Três defeitos de campo dão a medida: lote que falhava reprocessava ~1×/s martelando o ZIM; status ficava preso na fila local até alguém reiniciar o cliente; e as críticas que o X-Adm grava para o usuário sumiam, porque num daemon não há tela.
O que foi construído¶
- Backoff exponencial quando o ciclo deixa pendência (falha geral, retorno parcial ou
9XXreprocessável): piso 1s, dobrando, teto 15 min (era 60 min — longo demais para MDF-e, que é sensível a tempo). O gatilho do próprio eco é descartado: a write-back do status volta como mudança do PowerSync e reprocessaria o mesmo lote. Dado novo da nuvem fura o backoff e o zera — o item preso entra no mesmo arquivo do novo. - Vigia do write-back: se a fila local fica presa mais de 20s, reconecta. Contorna um bug do SDK que não re-tenta o upload ao voltar do offline — medido no e2e, vale para o cliente Kotlin e para o Rust.
- Avisos do X-Adm (
dRels3): após o programa ZIM, em qualquer desfecho, o cliente lê as críticas que o ERP gravou, registra no log e no GlitchTip, e apaga o arquivo. Conflito de concorrência foi depois excluído do alerta (o lote retenta sozinho; era ruído sem ação possível), mas segue no log. - Eco das linhas no log: cada linha escrita no envio e cada linha lida do retorno
saem no log entre
|pipes|, em nível INFO — dá para conferir o que o ZIM recebeu e devolveu sem entrar no modo debug. - Marcador de handshake opcional na leitura do retorno final: o ZIM escreve
Iniciou Zimno handshake e depois limpa o arquivo, regravando o final sem ele. Exigi-lo fazia o006de sucesso ser lido como lote inválido — e o MDF-e já baixado reprocessava sem parar. - Higiene de operador: caminhos do ZIM com espaço aceitos, erro de config local sem abrir
incidente, e log em UTF-8 (console e arquivo) com o
rodar.batpondo o console em UTF-8.
Riscos que ficam¶
O backoff protege o ERP, mas não conserta dado: 1XX é terminal por contrato. É o que a
etapa 05 ataca por outro caminho — impedindo que o lote saia incompleto.
04 — Fórmula (BOM) do kit no fluxo pied¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-09
Entregue em v0.0.11 (2026-08-12).
Escopo¶
O fluxo pied levava o pedido-kit ao X-Adm, mas não a fórmula — a lista de componentes.
Esta etapa acrescenta a tabela-espelho formulas ao fluxo existente. É o slice deste repo de
uma feature de quatro repos: o servidor cria o espelho, o int-pied emite as linhas, a sync
rule as publica.
Fora de escopo: as chaves de volta (o X-Adm as gera e elas não retornam).
O que foi construído¶
- Tabela
formulasno schema local, com as colunas de entrada (pied_formula_id,pied_x_ped,pied_cod_prod,qtde) e o par de status — sem colunaschave_*. - Posição por último na ordem de envio do fluxo pied: a fórmula referencia o produto-kit e o componente, que precisam existir no X-Adm antes (ordem FK-safe).
- Layout fixed-width no arquivo de envio, com
qtdeem escala 3, travado por golden test byte a byte. - Write-back reusa o caminho de status existente — zero código novo.
Estado do contrato¶
O layout é provisório: vale até o import real de FORMULAS chegar da equipe ZIM. Está
marcado como tal no contrato pAbast §3.6, e o golden test é o
ponto de partida da reconciliação. Enquanto isso, o rodapé do modo debug não tem branch
própria para formulas (sai só com contador e tipo) — deferido, com precedente no mdf.
05 — Coleta por remessa: o lote nunca mais sai pela metade¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-09
Entregue em v0.0.12–v0.0.13 (2026-09-08 e 2026-09-09).
Escopo¶
O defeito que originou a etapa é concreto: a coleta fazia um SELECT por tabela e o PowerSync
grava no SQLite local a qualquer momento — inclusive no meio da coleta. Os primeiros
SELECTs rodavam antes de um lote de sync aterrissar, os últimos depois; o arquivo saía com o
pedido sem o cliente, o ZIM devolvia 120 - Cliente não cadastrado, e 1XX é terminal:
o pedido ficava órfão para sempre (pedido 260079421, 2026-09-08).
O que foi construído¶
Duas vias que convivem:
- Por remessa — o Integrador declara, a cada push, o conjunto que precisa chegar junto ao
ERP (o manifesto, que desce pelo PowerSync). A linha coberta por uma remessa viva só sai
quando a remessa está inteira no banco local: falta um item, nada dela viaja. O cliente
lê as flags que o servidor calcula (
bloqueado,resolvido,total_itens) e não deduz dependência nenhuma. - Por presença — a linha que não pertence a remessa nenhuma sai como sempre saiu, depois de 3 ciclos de carência. O servidor só cria remessa onde há dependência, então cadastro avulso nunca terá uma; daqui, "linha cuja remessa ainda não chegou" e "linha que nunca terá remessa" são o mesmo estado observável.
Por cima disso, a coleta lê duas vezes e confere a passada inteira, manifesto incluído — banco que não sossega em três tentativas adia o lote. E o corte no teto de 999 registros respeita remessas inteiras, nunca pela metade.
Nada espera calado: remessa travada, remessa retida há muitos ciclos e item de tabela que o fluxo não conhece viram aviso no log e no GlitchTip, deduplicado por causa.
Decisões¶
| Decisão | Assunto |
|---|---|
| 0003 | leitura dupla com conferência (superada pela 0004) |
| 0004 | a unidade de entrega é declarada na origem |
06 — Heartbeat: a instalação avisa que está viva¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-10
Implementado em 2026-09-10; sai na próxima release. É a ponta de uma entrega que atravessa quatro repos — este, o integrador-server, o central-backend e o central-ui.
Escopo¶
Uma instalação que para — máquina desligada, jar travado, Java trocado, .properties quebrado —
ninguém via. O write-back simplesmente parava de chegar, e o sintoma aparecia horas ou dias
depois, relatado pelo cliente como "a integração parou". A casa também não sabia, sem acesso
remoto à máquina, qual versão roda em cada cliente nem quais fluxos estão ligados. E não há
cadastro de instalação: o cliente recebe o jar e começa a rodar, então o registro tinha de ser
automático.
O que foi construído¶
- Heartbeat na largada (
motivo=inicio): logo depois da trava de instância e antes de conectar ao PowerSync, para a instalação aparecer mesmo que a conexão falhe depois. Leva a versão do jar, os fluxos dezim.fluxos, o nome da máquina e o instante da largada. - Heartbeat periódico (
motivo=periodico) a cadaheartbeat.intervaloMinutos(60 por padrão), na mesma thread do laço. O primeiro conta a partir do heartbeat de início — não sai dobrado. - Nada a configurar no cliente: vai ao integrador-server do próprio cliente, no mesmo host e com
o mesmo token do write-back — nenhuma liberação nova de firewall. As chaves
heartbeat.*são opcionais;heartbeat.intervaloMinutos=0desliga tudo. - Nunca derruba o daemon: qualquer falha (rede, recusa, erro interno) vira WARN no log e só. Não vai ao GlitchTip: perder um heartbeat não importa, e quem enxerga o silêncio é o central.
- O
--debugnão manda heartbeat — sessão manual de homologação não é instalação.
O resto da cadeia mora nos outros repos: o integrador-server carimba de qual cliente é e repassa ao central; o central grava a instalação, conta as horas úteis sem sinal e, passadas 6, abre um alerta no GlitchTip; a aba Deploy do central-ui lista as instalações. O contrato do heartbeat é do integrador-server: heartbeat.md.
Decisões¶
| Decisão | Assunto |
|---|---|
| 0028 do central-backend | rota em dois saltos pelo integrador-server, horas úteis, alerta por cliente |
Decisões
0001 — Runtime Java 8 (exceção à constituição)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-20 · Decidido em: 2026-07-20
Contexto¶
A constituição X-Adm manda Java 25 (LTS) para apps Java. O integrador-client, porém, roda on-premise na máquina do ERP, onde só existe JRE 8 instalado — e a equipe que mantém o ambiente desenvolve em ZIM, sem autonomia para atualizar o Java da máquina de produção do ERP.
Decisão¶
O alvo de runtime é Java 8 (bytecode 52): compileJava com --release 8 e Kotlin com
jvmTarget 1.8. O build continua moderno: Gradle Kotlin DSL com toolchain JDK atual, CI no
JDK 25 do setup-java do pipeline.yml (GitHub Actions, decisão central 0027; até set/2026 era a
imagem ci-java:25 da fábrica, aposentada) — só o alvo é 8. O SDK PowerSync Kotlin JVM publica bytecode 1.8 e testa em
launcher Java 8 no próprio CI, então a cadeia inteira é compatível. Dependências pinadas em
linhas compatíveis com Java 8 (ex.: Logback 1.3.x, nunca 1.5.x).
Todo o resto da constituição permanece valendo (docs, gate ./gradlew check, checkstyle,
release por tag com asset, §6).
Consequências¶
- Código de produção sem APIs pós-8 (
List.of, records,java.net.http…); testes podem usar o toolchain moderno pois não são empacotados. - Gate inclui verificação do bytecode (major 52) para flagrar regressão de alvo.
- Quando o ERP migrar de Java, esta decisão cai e o repo volta ao padrão da casa.
Desvios do arquétipo (mesma raiz: legibilidade para a equipe ZIM)¶
Além do runtime, dois desvios conscientes do arquétipo "Java CLI/worker" da casa:
- Sem picocli: o app tem 2 flags (
--version,--debug) + um caminho opcional de properties — parsing manual de meia dúzia de linhas é mais legível para a equipe do que uma biblioteca de CLI;--versioncontinua lendo do manifesto do build (nunca literal). - Sem flags ⇒ roda o daemon (não imprime help): este app é um worker residente, não um job de linha de comando — rodar é o comportamento esperado do operador; a validação fail-fast da configuração (exit 2 com a chave errada) cumpre o papel do "não falha calado".
Alternativas consideradas¶
- Java 25 padrão — impossível: o jar não subiria no JRE 8 do cliente.
- Empacotar JRE moderno junto (jlink/instalador) — aumenta o artefato e a superfície de operação na máquina do ERP; a equipe local não tem como dar suporte.
- Port do SDK PowerSync para Java puro — inviável: o núcleo do protocolo vive na extensão
Rust
powersync-sqlite-core, sem spec pública (estimado 3–6 pessoa-meses).
0002 — Multi-fluxo: N Processador em série num par de arquivos único¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-30 · Decidido em: 2026-07-30
Contexto¶
O integrador-client é o cliente coringa da plataforma: o mesmo binário envia dados de
várias origens ao X-Adm, cada origem um fluxo (zim/Fluxo.java) com suas tabelas-espelho
e seu literal de handshake. Até esta decisão o Main fixava um único Fluxo.PIED em código —
não havia como uma instalação rodar mais de um fluxo (ex. onpetro e vantroba passarão a rodar
abastecimento + MDF-e juntos). Invariante de deploy: 1 integrador-server ↔ 1
integrador-client por cliente; um cliente multi-fluxo roda um fluxo por vez no par de
arquivos único (dXpEnvio/dXpRetorno) — o handshake por arquivos e a TravaInstancia (uma
trava por pasta de dados) proíbem concorrência e instâncias separadas na mesma pasta.
Decisão¶
N Processador (um por fluxo ativo) + um scheduler serial de thread única (processo/Agendador),
no par de arquivos único. Peças:
- Config
zim.fluxos— lista de chaves de fluxo ativas, obrigatória e sem default (ausente/ vazio = exit 2 com a lista de válidos; a instalação declara o que roda). Resolvida na largada via registryFluxo.porChave(constantes = fonte da verdade; a config só as nomeia); chave inválida = exit 2. Agendadordono do laço de vida (o laço saiu doProcessador, que virou só o miolo de UM fluxo): a cada tick — ou quando o gatilho do PowerSync acorda — rodaexecutarCiclo()de cada fluxo em série, na ordem da config. A falha de um fluxo é isolada (não afoga os demais).- Guarda fluxo×schema na largada — fluxo ativo cuja tabela não está no
SchemaTabelasderruba com exit 2 (impede subir "pela metade"). - Fluxos seguem o MODELO COMUM. Adicionar um fluxo é, no geral, uma constante + um layout no
ArquivoEnvio+ espelhos no schema. OENCERRA_MDFE(tabela únicamdf) foi implementado assim:case "mdf"noArquivoEnvio(contador + tipo +chave_nota), retorno com o mesmo layout do pied. Exceções (só no retorno): o MDF-e casa por contador (Fluxo.getRetornoCasaPorContador()) — tolera vir fora de ordem (o X-Adm reordena por filial) e parcial (o ZIM aborta num conflito; oProcessadorrestaura as posições não cobertas para retentar); e um1XXdispara alerta GlitchTip (getAlertaNoErroTerminal()). Ver contrato §4.
Consequências¶
- Um lote (um
dXpEnvio) carrega somente as tabelas de UM fluxo; a serialização garante que um lote é escrito, processado, lido e apagado por completo antes do próximo — sem colisão. - Preserva o invariante "uma thread só, sem concorrência" (legibilidade é a prioridade do repo).
- O modo debug ganhou um seletor de fluxo (só com >1 ativo); com um fluxo só, console intacto.
- Co-execução real de dois fluxos é provada em integração (SQLite temp + pAbast fake); o e2e
cobre no-regression do pied (o espelho
abastecimento/mdfainda não existe no servidor).
Alternativas consideradas¶
- Diretório/par de arquivos por fluxo — exigiria mudança no lado ZIM ("programa intacto" a proíbe); descartada.
- Multi-thread (um fluxo por thread) — quebra a legibilidade sem concorrência que é a prioridade do repo; descartada.
- Instâncias separadas por fluxo na mesma pasta — a
TravaInstancia(uma por pasta) e o par de arquivos único as impediriam de conviver; multi-fluxo numa instância é a única forma coerente. - Formato de arquivo próprio para o MDF-e (linha só com a ChaveNota, retorno textual
OK/ERRO, casamento por chave) — chegou a ser implementado a partir da amostra crua do ZIM, mas foi colapsado no modelo do pied: o ZIM alinha o lado dele e o cliente reusa a máquina existente (menos código, menos superfície de erro). Sobraram divergências mínimas só no retorno (casa por contador — fora de ordem/parcial — e alerta no1XX), flags noFluxo— não uma máquina paralela. A amostra crua fica como referência.
0003 — Coleta coerente: ler duas vezes e conferir¶
Decisão obsoleta — superada pela decisão 0004
Status: Obsoleto · Responsável: Gustavo Madruga · Atualizado em: 2026-09-08 · Decidido em: 2026-09-08
Obsoleta desde 2026-09-09, superada pela 0004. A premissa desta decisão estava errada: ela trata o pedido órfão como tendo UMA causa (o sync aterrissando entre os SELECTs) e conclui que ler duas vezes basta. Há um segundo modo — a linha que ainda não chegou ao banco local — em que as duas leituras concordam e o lote sai órfão do mesmo jeito. Foi o que três incidentes medidos mostraram (
260079421,260079618,260078864). A leitura dupla permanece no código, agora como rede do caminho por presença; o que caiu foi a premissa de que ela era suficiente.
Contexto¶
O contrato pAbast (§3.7) manda escrever o lote na ordem de dependência referencial
(propriedades → fones → estoque → contratos → itensped → formulas), e o Fluxo já garantia
essa ordem. Só que a ordem garante o arquivo, não o conteúdo: nada obrigava o cliente a
ter as linhas do pai no lote.
Incidente do pedido 260079421 (2026-09-08). O coletarPendentes fazia um SELECT por
tabela. O PowerSync grava no SQLite local a qualquer momento — inclusive no meio da coleta — e o
Agendador acorda justamente no gatilho de mudança, ou seja, coleta enquanto o dado aterrissa.
Medido no log:
12:14:39.018 push do pedido aplicado no servidor (6 tabelas, 1 transação)
12:14:39.073 coleta: SELECT propriedades/fones/estoque → o pedido ainda não chegou
...as linhas aterrissam no SQLite...
coleta: SELECT contratos/itensped/formulas → chegaram
12:14:43 próximo ciclo pega propriedades/fones/estoque
O arquivo saiu com o pedido sem o cliente. O ZIM cadastrou cliente e produto no lote seguinte,
mas o pedido já tinha levado 120 -Cliente CPF ... não cadastrado. — e 1XX é terminal, não
retenta. Pedido órfão, sem caminho de volta pelo cliente.
Do lado servidor não havia o que corrigir: o applyPut do integrador-server aplica o payload
inteiro em uma transação e o PowerSync tem um stream só com as 6 tabelas. A publicação já
era atômica; quem quebrava a atomicidade era a leitura.
Decisão¶
Ler duas vezes e só aceitar o lote quando as duas leituras batem (Processador.coletarPendentes).
Duas leituras iguais significam que nada aterrissou entre elas — logo a segunda é uma foto
coerente. Diferentes significam que aterrissou: vale a segunda e confere de novo, até três vezes.
Banco que não sossega devolve lote vazio e espera o próximo ciclo; o dado não some, só espera
um momento mais calmo.
A comparação é por id + cod_retorno na ordem — o par que decide quem entra no lote. Mudança só
de campo de dado (o servidor reescrevendo um valor de linha que já estava lá) não é detectada: ela
não cria pedido órfão, que é o que esta conferência existe para impedir.
O laço de leitura (lerTodasAsTabelas) é exatamente o de antes. O custo é uma passada extra de
SELECTs por ciclo, em tabelas locais pequenas — irrelevante perto de rodar o zimrtmu.exe.
O que foi descartado¶
- Transação de leitura no wrapper (
readTransactiondo SDK). Resolveria igual, mas oPowerSyncWrapperé ponte e ponte fica magra — regra da casa, no cabeçalho da própria classe. SELECTcomposto comUNION ALLdas 6 tabelas (um statement = um instante). Correto e sem tocar no wrapper, mas exige padding deNULLaté a maior largura e colunas de controle: SQL gerado bem mais difícil de ler que o laço de duas leituras, para um público que programa em ZIM.executar("BEGIN")…COMMIT")em volta dos SELECTs. Não funciona: o SDK avisa que statement no nível do banco pode cair em conexão diferente (pool de leitura) — o BEGIN abriria transação numa conexão e os SELECTs rodariam noutra, sem snapshot e deixando transação órfã.- Gate de dependência referencial (segurar a linha cujo pai não está gravado nem no lote).
Chegou a ser implementado e foi removido: resolvia o mesmo bug que a leitura dupla já
resolve, ao custo de ~100 linhas, cinco regras FK — uma delas apoiada numa suposição sobre o dado
(
formulas→ kit) — e de um modo de falha novo: filho de pai morto em1XXficava segurado indefinidamente, com só um INFO por ciclo. Duas mecânicas para um bug é uma a mais.
Consequências¶
- Um pedido cujo cliente ainda não chegou não sai — sai inteiro no ciclo seguinte.
- Ciclo com o banco recebendo sync sem parar é adiado, não processado pela metade. Fica no log
(
coleta não estabilizou em N leituras). - Não há defesa contra órfão de causa diferente desta (ex.: o servidor publicar um pedido sem o
cliente). Se aparecer, o
1XXdo ZIM denuncia — e aí a decisão se revisita com o caso na mão, não por antecipação.
Lacuna conhecida (fora deste repo)¶
Uma linha em 1XX não tem caminho de volta: o re-importar do painel PIED reenvia o payload, mas o
upsert do integrador-server preserva o write-back por regra ("mão única") e não toca o
cod_retorno — a linha segue 1XX e o cliente nunca a recoleta. O botão do painel, hoje, não
re-arma nada. Correção pertence ao par int-pied/integrador-server; enquanto não vier, o desempate é
UPDATE ... SET cod_retorno = '000' na mão.
0004 — Coleta por remessa: a unidade de entrega é declarada na origem¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-09 · Decidido em: 2026-09-09
Contexto¶
A decisão 0003 partiu de uma premissa errada: que o pedido órfão tinha uma causa — o sync aterrissando no meio da coleta — e que ler duas vezes bastava. Três incidentes medidos mostraram que há um segundo modo, que a leitura dupla não cobre, porque as duas leituras concordam:
| pai ausente | filho rejeitado | evidência | pedido |
|---|---|---|---|
propriedades (cliente) |
contrato → item → fórmulas | 120-Cliente CPF 072.508.149-07 não cadastrado. |
260079421, 08-set |
contratos (pedido) |
item, fórmulas | 120-Pedido (10738.24) não cadastrado. |
260079618, 09-set |
estoque (produto) |
item, fórmulas | 120-Produto para o pedido (260078864) não cadastrado. |
260078864, 09-set |
Às 11:24:52 do 260079618 a coleta levou todos os pendentes locais e ainda assim o contrato
não saiu: ele não estava no SQLite. O PowerSync entregou linhas da mesma transação Postgres em
checkpoints diferentes.
A cascata é 1:N e silenciosa — no 260079421, uma ausência produziu nove linhas terminais, e
53 no log inteiro. 1XX é terminal e a coleta lê só 000/9XX: a linha some para sempre. Estado no
dia da decisão: 11 pedidos, 76 linhas travadas.
A raiz não é dependência semântica, é ausência de unidade de trabalho. O cliente lê um banco
local e não distingue "a linha ainda não chegou" de "a linha não existe". Qualquer regra que ele
infira — gate de FK, releitura — é inferência sobre informação incompleta. O Integrador, no instante
do applyPut, já sabe exatamente quais linhas aquele pedido precisa entregar.
Decisão¶
O integrador-server passa a declarar a unidade de entrega: uma remessa por componente de
dependência de cada push (integracao_remessa + integracao_remessa_item), replicada pelo PowerSync
junto com as linhas. Este cliente monta o arquivo por duas vias que convivem por desenho:
- Via remessa — linha coberta por uma remessa viva só sai quando a remessa está inteira no banco local. Uma regra, independente de quantas tabelas o fluxo tenha.
- Via presença — linha que não pertence a remessa nenhuma continua coletável como sempre. O
servidor cria remessa só onde há dependência (grupo com 2+ linhas ligadas), então
propriedadesavulsa eestoqueavulso nunca terão uma. O manifesto acrescenta garantia, nunca remove — é o que torna o rollout não-quebrante e mantémabastecimentoeencerra-mdfevivos sem manifesto próprio.
O que o cliente lê, e não deduz¶
| campo | quem calcula | para quê |
|---|---|---|
total_itens |
servidor | detectar que a própria lista de itens chegou pela metade |
bloqueado |
servidor | um pai da linha, na mesma remessa, está em 1XX — não reenviar |
resolvido |
servidor | nada mais a esperar: 006, soft-deletada ou órfã |
status |
servidor | ABERTA/ENTREGUE/TRAVADA (só as vivas sincronizam) |
O grafo de dependência mora no servidor (registry.ordemPut()) e não é replicado aqui. O cliente
lê flags. É o que dá a precisão do gate de FK sem o acoplamento: pai morto retém os filhos, folha
morta não retém as irmãs.
Carência de 3 ciclos¶
Linha pendente sem remessa só sai depois de aparecer descoberta em 3 ciclos seguidos. Sem isso, a
linha de um pedido cuja remessa ainda não aterrissou é indistinguível da linha solta que nunca terá
remessa, sai por presença, e o incidente se repete — o manifesto atravessa a mesma fronteira que
quebrou as linhas. Em ciclos e não em relógio porque o ciclo acelera justamente na janela de
risco (logo após o push): no 260079618 os dois ciclos ficaram a 6,5 s um do outro. Contrato
declarado dos dois lados (integrador-server .ia/005 R16b).
Consequências no ciclo¶
- A leitura é uma passada só — pendentes, manifesto e presença — e a conferência das duas leituras cobre tudo. Conferir só o dado deixaria nua a decisão coberto × descoberto, que é a que o manifesto existe para tomar.
- O corte de
LIMITE_REGISTROS(999) passou a ser por remessas inteiras: cortar cego fatiaria uma remessa e produziria o próprio órfão. temPendenteNovo()lembra os ids retidos, senão remessa presa fura o backoff a cada tick.coletarPendentes()continua devolvendo tudo: o modo debug tem de mostrar a realidade ao operador, inclusive a linha retida.
O que foi descartado¶
- Gate de FK no cliente (o que o
0003já havia removido). Cinco regras hoje, quinze no fluxo completo, escritas por quem talvez não conheça a semântica — e regra errada trava produção em silêncio. Não resolve o caso da linha que ainda não chegou, que é o caso dos três incidentes. - Estado
002("aguardando pai") na linha — exigiria inventar código numa coluna compartilhada com o ERP, com homologação do dono do pAbast. O manifesto o torna desnecessário: a elegibilidade é propriedade da remessa, não da linha. - Snapshot de leitura (
readTransactionnuma transação só). Mata a leitura rasgada por construção e continua boa ideia, mas com o manifesto o pior efeito dela no caminho coberto passa a ser um ciclo de espera. Adiado, para não misturar duas mudanças no mesmo PR. Column.integerpara as flags booleanas. Não traria nada: oPowerSyncWrapper.listarlê toda coluna comgetString, e o SDK mapeiabooldo Postgres para inteiro — chega"1"/"0"de qualquer jeito. A coerção está travada por teste contra o SQLite real.
Consequências¶
- Nenhum arquivo sai com linha órfã entre as linhas cobertas; remessa incompleta vira espera.
- Remessa travada, remessa retida há muito tempo e item de tabela desconhecida aparecem no GlitchTip, com dedup por causa — reter em silêncio foi o que derrubou a primeira tentativa.
- Cadastro avulso legítimo espera 3 ciclos na primeira vez. É o preço de não distinguir o indistinguível, e é pago uma vez por linha.
- O valor depende de duas peças de fora: as sync rules do
maxsul/powersyncpublicando as duas tabelas, e o manifesto + backfill dointegrador-server. Sem elas o cliente coleta como hoje, 100% por presença — não quebra, mas o bug continua possível.
0005 — Adota o CI 100% GitHub Actions, com a Release no Forgejo¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-11 · Decidido em: 2026-08-31
Contexto¶
Este RD não decide nada novo: CI/CD dos apps num pipeline.yml único no GitHub Actions é norma
da casa — ADR central 0027. A mesma
0027 fixa a fronteira que importa aqui: é "0% Forgejo no CI", não "0% Forgejo". O Forgejo segue
hospedando a Release-artefato de app cliente offline, e a 0027 cita este app como o exemplo. O
que faltava era o rastro local.
Decisão¶
Em 2026-08-31 (commit 7b2eed3), o ci.yml, o docs.yml e o release.yml do Forgejo Actions
viraram um .github/workflows/pipeline.yml à la carte (decide → gate ∥ docs → release). Os
workflows .forgejo/ saíram no mesmo dia (commit 8cd44a3).
- Toolchain pelas actions (
setup-java25 +setup-gradle), sem a fábricaci-images. O alvo Java 8 segue no build (--release 8, decisão local 0001). - A Release continua no Forgejo. Numa tag
v*, o jobreleasebuilda o fat jar e cria a Release emfonte.xadm.bizpela API do Forgejo, com o jar como asset, autenticado porRELEASE_TOKEN(token Forgejo comwrite:repository, cadastrado no GitHub). O operador on-premise baixa da mesma URL de antes: a Release no Forgejo existe desde 2026-07-21 (commit1ed3816), e só mudou quem a cria. - Sem deploy. O app roda
java -jarna máquina do ERP de cada cliente, então não há recurso Coolify nem control-plane.
Consequências¶
- Quem publica o artefato é o runner GitHub, mas o destino é o Forgejo. Sem o
RELEASE_TOKEN, o jobreleasefalha: não publica pela metade. - O
pipeline.ymlé template rastreado da casa, aqui na variante sem deploy e com Release. Muda por re-derivação (/xadm-docs), não por edição solta. - Este RD é ponteiro: divergência de mérito sobre o modelo de CI se resolve no ADR central 0027, não aqui.
0006 — Observabilidade remota e kill-switch do GlitchTip¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-21 · Decidido em: 2026-09-21
Contexto¶
O programa roda na máquina do cliente, sem acesso remoto. Diagnosticar exigia pedir o arquivo
de log para a equipe do cliente, um a um: o que foi escrito no dXpEnvio, o que voltou no
dXpRetorno, os avisos e a configuração do processo só existiam lá.
Ao mesmo tempo, o GlitchTip que já existia não tinha desligamento. O DSN vem do app.json
embarcado no jar e o Sentry.init o passava explícito, então nenhuma variável de ambiente o
desligava e --debug só trocava a tag. Em 15/09/2026 a rodada local do e2e-maxsul-pied abriu
três incidentes com environment=production no projeto 18 — chaves E2E-REMESSA-INC,
E2E-FILHO-1XX e E2E-RESOLV-MAN, todos com "Rejeitado pelo fake" no texto. Não corrompeu dado,
mas queimou o sinal: alerta de remessa travada em produção ficou indistinguível de incidente real
de cliente. A suíte contornou com um app.json de override antes do jar no classpath — paliativo
que depende da ordem do classpath.
Decisão¶
- A variável de ambiente
SENTRY_DSNvence oapp.jsonsempre que estiver definida — inclusive vazia, que desliga o envio. É o mesmo desligamento dos apps Micronaut da casa, e dispensa o truque de classpath no e2e. - O DSN continua versionado no
app.json. O desenho de injetar o DSN no deploy é dos apps que o Coolify provisiona; este é on-premise, distribuído como jar, sem ninguém injetando env na máquina do cliente. Tirá-lo do repo moveria o problema para o empacotamento de cada cliente sem ganho: DSN de ingest é público por desenho. - Um caminho só para o WARN: o
SentryAppenderdo Logback (minimumEventLevel=WARN,minimumBreadcrumbLevel=INFO) é quem leva evento ao GlitchTip. As chamadas explícitas aSentry.captureMessagede nível WARNING saíram; a curadoria que elas faziam (dedup por causa nos avisos da coleta, filtro de conflito nodRels3) passou a decidir o nível do log. - Dois loggers ficam fora do appender, por filtro no
logback.xml: o do eco (…integradorclient.eco) e o do operador (…integradorclient.operador, para configuração quebrada e JVM de 32 bits). - O eco do dXp vai cru, 100%, como breadcrumb, em blocos de até 25 linhas ou 4 000 caracteres, com o teto de breadcrumbs em 200.
setSendDefaultPii(true)(padrão da casa) e o hook de encerramento do SDK ligado, comflushTimeoutMillisexplícito — o programa captura e chamaSystem.exitna linha seguinte.
Consequências¶
- Uma rodada de e2e ou de desenvolvimento com
SENTRY_DSN=''não cria issue nenhuma; o paliativo do classpath pode sair da suíte. - As linhas cruas carregam CPF/CNPJ, nome e fantasia (
cgc_cpf,nome_prop,fantasiano layout dopAbast). É decisão consciente do dono: o GlitchTip é auto-hospedado na infraestrutura da X-Adm, não em SaaS de terceiro, e a retenção medida no serviço é de 90 dias (GLITCHTIP_RETENTION_DAYSno default, sem env). Quem lê o projeto já tem acesso ao GlitchTip da casa. - Degradação repetitiva vira evento por ciclo (ex.:
pAbast falhou … volta a pendenteenquanto o ERP está fora). O GlitchTip agrupa por template numa issue só, mas o consumo de cota é real — vale olhar o painel na primeira semana. - O eco no evento depende de o ciclo ser mono-thread. O escopo do Sentry é por thread e a
thread nova recebe um clone do hub; hoje o daemon roda tudo numa thread só
(
Agendador.rodarParaSempre). Paralelizar o ciclo faria o eco sumir dos incidentes sem erro nenhum — quem mexer nisso reabre esta decisão. - O SDK fica preso em
6.34.0(core esentry-logback): a 7.x exige Java 11 e o alvo aqui é bytecode 8 (decisão 0001). Há teste que exercita o parsentry-logback×logback-classic 1.3.14de verdade.
Alternativas deliberadas¶
--debugdesligar o envio. Simples, mas deixaria o e2e refém de lembrar a flag e não alinharia com o resto da casa, que desliga porSENTRY_DSN.- Appender em
ERROR, como a norma de engenharia fixa para os apps Micronaut. Recusada: aquiWARNé justamente a degradação que o suporte precisa ver de longe (pAbast falhou, retorno sem handshake). A divergência é declarada, não silenciosa. - Uma breadcrumb por linha do eco. Um lote cheio são ~1 998 linhas: encheria a janela do SDK antes de o erro acontecer e o incidente chegaria com o fim do log, sem a cabeça.
- Anexo (
Attachment) com o arquivo inteiro. O GlitchTip não suporta anexo — verificado no banco dele, sem tabela de anexo.
Glossário — integrador-client¶
Status: ativo · Responsável: gustavo · Atualizado em: 2026-09-10
- Fluxo — uma origem de dados que o integrador-client (cliente coringa da
plataforma) envia ao X-Adm: conjunto de tabelas-espelho + literal de continuação do
handshake (
zim/Fluxo.java). O ZIM identifica o fluxo pelo literal; cada arquivo de envio carrega só as tabelas de um fluxo. Ativo: pied (IMPORTA_PIED); prontos mas inativos: abastecimento (Continuar) e encerramento MDF-e (ENCERRA_MDFE, tabelamdf, contrato — aguarda sync rules e confirmação ZIM); previsto: sascar. zim.fluxos— config (properties) que lista os fluxos ativos nesta instância, por chave separada por vírgula/espaço (zim.fluxos=pied,abastecimento). Obrigatória, sem default: ausente/vazio derruba com exit 2 listando os fluxos válidos e exemplos (a instalação declara o que roda em vez de herdar um fluxo implícito). Nome de fluxo nunca contém vírgula nem espaço — são os separadores da chave. Cada chave é resolvida porFluxo.porChavena largada; chave desconhecida ou fluxo com tabela fora do schema também derruba com exit 2. A ordem é a ordem dos lotes no tick.- Agendador (
processo/Agendador) — scheduler de thread única, dono do laço de vida do daemon: a cada tick (ou quando o gatilho do PowerSync acorda) roda o ciclo de cadaProcessadorativo em série, na ordem dezim.fluxos— nunca concorrente. O laço saiu doProcessador(que virou só o miolo de um fluxo) quando a instância passou a rodar N fluxos no par de arquivos único. - pAbast — programa ZIM chamado via
zimrtmu.exe pAbast, que importa no ERP os registros do arquivo de envio e escreve o arquivo de retorno. O nome é por hora o mesmo do programa de abastecimento legado (decisão da equipe ZIM); o que identifica esta integração (pied-maxsul) é a linhaIMPORTA_PIEDdo handshake. - Arquivo de envio — arquivo texto fixed-width (1 linha = 1 registro, campo 1 = tipo de
registro) que o integrador-client escreve para o
pAbastconsumir. Nome configurável (default históricodXpEnvio). - Arquivo de retorno — arquivo texto fixed-width que o
pAbastescreve antes de encerrar: 1 linha por linha do envio, na mesma ordem (matching por posição) —tipo_registroecoado +cod_retorno+msg_retorno. Nome configurável (default históricodXpRetorno). - Tabelas-espelho — as 5 tabelas do fluxo de entrada sincronizadas via PowerSync:
propriedades,fones,estoque,contratos,itensped. - Código de retorno — máquina de status da plataforma:
000pendente ·001em processamento ·006gravado (sucesso) ·1XXerro terminal de dado ·9XXerro de processamento (reprocessável). - Write-back — atualização da linha no SQLite local que o SDK PowerSync converte sozinho em
POST /api/v1/powersyncno integrador (upload queue offline-first). - Heartbeat — sinal de vida que a instalação manda na largada e a cada
heartbeat.intervaloMinutos(60) ao integrador-server do cliente, que o repassa ao central: versão, fluxos ativos e nome da máquina. Não carrega dado de negócio, e perder um não importa. Quem enxerga o silêncio (6 horas úteis sem sinal) é o central. Contrato: heartbeat. - Lote — conjunto de registros pendentes enviados numa única execução do
pAbast. Máximo de 999 registros (o contador de linha do arquivo tem 3 dígitos); o excedente fica para o ciclo seguinte. - Modo debug (
--debug) — modo de homologação em que o operador comanda o processamento pelo console:[i]tem(1 registro por execução),[p]edido(fechamento relacional do contrato num arquivo só) ou[t]udo; os arquivos de troca ficam no disco com a máscara do layout no rodapé e versões.jsonem pretty print.