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.