Pular para conteúdo

Projeto — Integração PIED → X-Adm (Caminho Robusto)

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-11

Documento técnico da integração PIED → X-Adm da Maxsul (projeto MAXSUL2025/841). Sucede o Pré-projeto, cujo Caminho 2 (Robusto) foi aprovado pela diretoria em 2026-07-09 (Marcelo Garabeli) — ver decisão 0002.

Esta é a documentação-mãe do projeto como um todo. A solução se destila em vários repositórios (Integrador, este maxsul-pied, PowerSync, cliente Java). Aqui está a visão completa e canônica; cada repositório carrega a sua fatia destilada e aponta de volta para cá.


1. Estrutura inicial e suas partes

A integração é um pipeline entre dois sistemas que não se conhecem: a PIED (onde a venda de painéis solares acontece) e o X-Adm (o ERP que emite a nota fiscal). Entre eles, quatro peças, cada uma no seu repositório, ligadas por um único banco Postgres compartilhado (o banco de integração da Maxsul) e pelo PowerSync.

flowchart TB
    PIED["<b>PIED</b><br/>API REST + webhooks"]

    subgraph nuvem["Nuvem X-Adm · Coolify — Postgres Maxsul compartilhado"]
        direction TB
        PIEDAPP["<b>maxsul-pied</b> — pied.maxsul.xadm.biz<br/>webhook + poll + transform"]
        INT["<b>Integrador</b> — int.maxsul.xadm.biz<br/>tabelas espelho do X-Adm · conector PowerSync"]
        PS["<b>PowerSync</b> — ps.maxsul.xadm.biz<br/>Postgres ↔ SQLite"]
    end

    ERP["<b>ps-java</b> — cliente Java on-premises no ERP<br/>grava no X-Adm (ZIM)"]

    PIED -->|"webhook (tempo real)"| PIEDAPP
    PIEDAPP -->|"poll REST"| PIED
    PIEDAPP -->|"PUT / DELETE /api/v1/xadm"| INT
    INT -->|"replicação lógica (WAL)"| PS
    PS -->|"sync offline-first"| ERP
    ERP -->|"write-back de status"| INT

Quem é dono do quê (a regra que rege todo o resto)

Peça Subdomínio É dono de… Papel
Integrador (integracao/integrador) int.maxsul.xadm.biz tabelas espelho do X-Adm: contratos, propriedades, fones, itensped, estoque Plataforma central de integração. Recebe o estado desejado pelo mesmo contrato REST que o ERP já usa, persiste, e alimenta o PowerSync.
maxsul-pied (este repo) pied.maxsul.xadm.biz tabelas PIED: pied_webhook, pied_rest (raw), pied_produto, pied_cliente, pied_pedido (normalizadas) Ponte com a PIED: recebe webhook e faz poll REST, valida, transforma para o modelo X-Adm e empurra ao Integrador via REST.
maxsul-powersync (maxsul/powersync) ps.maxsul.xadm.biz — (infra) Replica as tabelas do Integrador (Postgres) para o SQLite do cliente Java, offline-first.
ps-java (maxsul/ps-java) on-premises (junto ao ERP) — (consumidor) Cliente Java de exemplo: observa as mudanças sincronizadas e (fase futura, pela equipe X-Adm) grava no ERP; devolve o status.

Dois princípios invioláveis que valem em todo o pipeline:

  1. Persistir o raw ANTES de processar — nada é transformado sem antes existir cru no banco; auditoria e reprocessamento ficam garantidos desde o dia 1.
  2. Nunca escrever síncrono no ERP — a entrega é desacoplada (Integrador → PowerSync → cliente). A nuvem nunca chama o ZIM direto.

Banco compartilhado, donos separados. Integrador e maxsul-pied conectam no mesmo Postgres, mas cada um só faz CREATE/ALTER (Flyway) no seu conjunto de tabelas. O maxsul-pied não escreve SQL direto nas tabelas do Integrador — ele usa a API REST existente do Integrador (PUT/DELETE /api/v1/xadm), o mesmo caminho que o agente on-premises do X-Adm já usa. Assim preserva-se a serialização por ReentrantLock, a auditoria em request e a semântica de soft-delete/revive do Integrador.

O plano é em fases (decisão 0001):

  • Fase 1 — captura raw (MVP, em produção primeiro): só pied_webhook + pied_rest. Sem transform, sem escrita no Integrador. Objetivo: acumular payloads reais da PIED para modelar a fase 2 com evidência, não com suposição.
  • Fase 2 — transform + entrega (implementada, 2026-07-13): normaliza (pied_produto/pied_cliente/pied_pedido), aplica o de-para X-Adm, faz o gate por dealStatus (transição de pagamento →received), empurra o estado desejado ao Integrador (PUT /api/v1/xadm) e reconcilia o retorno do X-Adm pela remessa do manifesto (GET /api/v1/integracao/remessas) — um pedido só fica CONFIRMADO quando a remessa inteira fechou, não quando o contrato voltou 006 (decisão 0021). MODO PRODUCAO (auto) × STAGING (fila manual). Operação e dry-run pelo console /console (§3.4). Nasce desligada por toggle (pied.integracao.habilitada, default false) — ligar é decisão de operação após confirmar de-para/gate com a Maxsul (configuração).

2. Integrador — o espelho do banco X-Adm

O Integrador já é um produto da casa: um servidor Micronaut/Java, uma instância por cliente, Postgres dedicado, publicado em int.<cliente>.xadm.biz. Ele nasceu para o fluxo de saída do X-Adm (o ERP faz push e o Integrador replica para os apps móveis via PowerSync). Para a Maxsul, ele ganha o papel de plataforma central desta integração: é onde vive o modelo X-Adm que a PIED precisa alimentar.

2.1 As tabelas espelho (o alvo da integração)

O contrato autoritativo é o documento interno MAXSUL2025_610110 (entidades, campos, tamanhos e regras de transformação). Cinco entidades:

Tabela Chave de negócio O que é
contratos xPed / ChaveCP Cabeçalho do pedido de venda (cliente, valor, natureza de operação, frete).
itensped xPed + item Itens do pedido (produto, quantidade, valor, frete).
propriedades CgcCpf + CodProp Cliente / endereço (razão social, endereço, cidade, estado).
fones CgcCpf Contato do cliente (nome, DDD, telefone, e-mail).
estoque CodigoAlt / ChaveEst Produto (código PIED, nome, valores de venda, EAN13).

Cada tabela carrega a convenção do Integrador (chave de negócio + id UUID v7 + soft-delete); as colunas Chave* e CodRetorno/MsgRetorno são preenchidas pelo lado X-Adm e voltam pelo write-back (§5). Schema completo (colunas, tipos, tamanhos) e relacionamentos: Modelagem §1. De-para e regras de transformação: Mapeamento.

O que muda no Integrador para a Maxsul — aditivo. O X-Adm tem um modelo único: contratos/propriedades/estoque/itensped (e fones) são as mesmas tabelas para todos os clientes — o Integrador as espelha. Uma mesma contratos serve compra e venda; cada fluxo preenche o subconjunto de colunas que usa. Para a Maxsul o trabalho é aditivo ao molde compartilhado: garantir as colunas do fluxo de venda (xPed, CgcCpfCliente, NatOp…) e a tabela fones (nova). Delta de colunas a confirmar com o dono do Integrador.

2.2 Como o Integrador recebe o estado desejado

O maxsul-pied entrega os dados pelo mesmo contrato REST que o ERP já usa para popular o Integrador: PUT /api/v1/xadm (upsert) e DELETE /api/v1/xadm (soft-delete), com Authorization: Bearer <token>. O Integrador faz o parse, serializa o apply (ReentrantLock(fair=true)), persiste e audita em request. Para JSON válido responde sempre HTTP 200, com o resultado no corpo. É o maxsul-pied no papel que o agente on-premises ocupa para os outros clientes — só que a fonte, aqui, é a PIED em vez do próprio ERP.

Comunicação tolerante a falhas. A entrega ao Integrador nunca se perde por indisponibilidade. Se o Integrador estiver fora do ar, responder erro ou a chamada expirar, o maxsul-pied retenta no futuro com espera progressiva — o estado desejado continua no banco (raw + tabelas normalizadas), então retentar é sempre seguro (idempotente por chave natural, §6.4). Falha que persista além das retentativas alerta a equipe de desenvolvimento via GlitchTip — decidido ter uma instância para este projeto (habilitar features.glitchtip no app.json ao configurar). Nenhum pedido é descartado por falha de rede.


3. maxsul-pied (este projeto) — a ponte com a PIED

Este repositório é o microserviço slim que conversa com a PIED. Ele capta por dois canais complementares (nenhum cobre tudo sozinho) e é dono das tabelas pied_*.

flowchart TB
    subgraph pied_app["maxsul-pied"]
        WH["Webhook<br/>POST /webhook/pied"] --> RAWW[("pied_webhook")]
        POLL["Poll REST<br/>@Scheduled"] --> RAWR[("pied_rest")]
        RAWW --> TF["Transform (fase 2)"]
        RAWR --> TF
        TF --> NORM[("pied_produto<br/>pied_cliente<br/>pied_pedido")]
        NORM --> PUSH["push → Integrador<br/>PUT/DELETE /api/v1/xadm"]
    end
    UI["UI simples<br/>(o que chegou × o que foi processado)"] -.lê.- RAWW
    UI -.lê.- RAWR
    UI -.lê.- NORM

3.1 Webhook (tempo real) — caminho quente

A PIED envia POST com corpo { "event": "order.created", "data": { ... } } (eventos budget.* e order.*; o data traz company e products embutidos — ver o payload de exemplo no MAXSUL2025_610110). Regras:

  1. Toggle desligado → 503; segredo compartilhado configurado e divergente → 401 (RFC 7807).
  2. Insert do corpo cru + headers (credenciais mascaradas) em pied_webhook.
  3. Responde 200 imediatamente — nenhum processamento pesado no request.
  4. Evento desconhecido não é rejeitado: é dado capturado (evita reentrega da PIED por erro nosso).

O cadastro do endpoint e do segredo é manual, no painel da PIED (Configuração).

Modo de falha do canal: a PIED desliga o webhook e não avisa. Quando uma entrega falha — 404 (app fora do ar) ou timeout (rede oscilando, app de pé) — a PIED desativa o webhook, para de enviar, não religa sozinha e não reenvia o que se perdeu. Só volta com recadastro manual no dashboard (runbook). Em 18/09/2026, cinco minutos de rede ruim custaram 2h44 de apagão no pico de uma sexta. Vigiado pelo watchdog em duas réguas — 45 min comerciais e 24h úteis (0016, 0023).

3.2 Poll REST — reconciliação e o que o webhook não traz

O webhook sozinho não cobre tudo: não há evento de produto, e alterações de frete/responsável não disparam evento. Por isso o poll REST é obrigatório, não rede de segurança. Contrato da API validado contra o ambiente real em 2026-06-17:

  • Produtos: GET /api/v2/equipments/{pagina}/{limite} — varredura completa (sem delta).
  • Clientes: GET /api/v1/companies/{pagina}/{limite} (v1) — varredura completa.
  • Pedidos: GET /api/v2/requests/order/{pagina}/{limite}[?lastUpdateAfter=AAAA-MM-DD] — incremental por cursor (?lastUpdateAfter só funciona em pedidos).

Envelope comum: { "error": null, "data": { "items": [...], "totalItems": N } }. Paginação 1-based, limite 50; página além do fim retorna HTTP 200 com items vazio — a condição de parada. O cursor de pedidos é persistido (pied_rest / tabela de cursor) e só avança ao fim de uma rodada bem-sucedida; a janela do último dia é reprocessada de propósito (o filtro tem granularidade de dia — dedup é problema do transform).

O poll também é o backstop do apagão de webhook — e por um tempo foi um backstop pela metade: recuperava o dado do pedido perdido, mas se o evento de pagamento caiu no apagão, o pedido chega aqui já pago, sem transição para o gate testemunhar, e encalhava em CAPTURADO. Fechado pelo resgate da 1ª captura já paga (0023), que usa o orderCreated do REST para separar pedido novo de backlog. Nasce desligado (PIED_GATE_PRIMEIRA_CAPTURA_DESDE vazio).

3.3 Transform (fase 2) — do modelo PIED para o modelo X-Adm

Com dados reais em mãos, o transform lê o raw, normaliza para pied_produto/pied_cliente/ pied_pedido e produz o estado desejado no formato do MAXSUL2025_610110, empurrando ao Integrador via REST (§2.2). É idempotente por content-hash: só reescreve quando o conteúdo de negócio muda de fato — evita sync inútil no PowerSync. As regras de de-para (incluindo a lógica Kit × não-Kit, a NatOp por estado, os valores fixos ChaveUnid/CodMat, a derivação de CodProp, a quebra DDD/telefone) estão em Mapeamento e regras.

Regra de negócio — só pedido finalizado vai ao X-Adm. O X-Adm faz apenas INSERT de pedido — nunca UPDATE. Logo, um pedido só é empurrado ao Integrador quando está finalizado na PIED; enquanto estiver em aberto/rascunho, fica retido no landing e não é enviado. Assim que finaliza, é inserido uma única vez. (Clientes e produtos não têm essa trava: podem ser atualizados normalmente.)

Como foi definido: o gate de envio é a transição de pagamento — o payment.status passa a received num update de um pedido que já tínhamos não-pago (Maxsul, 2026-08-07). Detectada por evento no upsert (payment_status/payment_status_anterior); é go-forward por natureza (backlog já-pago não re-transiciona) e o envio é uma vez (INSERT-único; a máquina de status não reenfileira). Pedido que aparece já-pago na 1ª captura não dispara sozinho (operador enfileira no console). O dealStatus deixou de ser o gate (pendências P1).

3.4 Console operador (/console)

Superfície única de operação (aberta, como o /captura) que dirige cada pedido pela máquina de estados real (pied_pedido.status) e absorveu o antigo /inspetor+/fila. Por pedido: o de-para PIED × X-Adm campo a campo (dry-run, sem tocar o Integrador), o stepper de estágios e as ações liberadas pelo estado — puxar da PIED, enfileirar (gate manual), enviar, atualizar o retorno, reenviar, re-normalizar — mais um sync global de clientes (InscEst). Em STAGING é interativo; em PRODUCAO, monitor. Rotas e envs em configuração.

3.5 UI de diagnóstico — home + browser do banco (/dados/*)

As telas são server-rendered (JTE, decisão 0025 — layout/menu no padrão do integrador) e abertas (sem auth, atrás da rede). Além do /console e do /captura, a home (/) é um painel de cards com a contagem por tabela, e o browser do banco (/dados/{pedidos,clientes,produtos,webhooks,rest,cursor,estado}) expõe read-only as 7 tabelas pied_* pelo browser — colunas + payload JSON copiável. Expõe PII (clientes). Detalhe das rotas em configuração.


4. PowerSync — do Postgres ao cliente

O PowerSync (ps.maxsul.xadm.biz, repositório maxsul/powersync) replica, por replicação lógica (WAL do Postgres), as tabelas do Integrador para um SQLite local ao lado do cliente Java. É self-hosted (serviço PowerSync + MongoDB de bucket), publicação Postgres nomeada powersync, auth por JWKS (auth.xadm.biz, audience maxsul).

As sync rules já estão escritas (bucket global):

data:
  - SELECT * FROM contratos    WHERE deleted = false
  - SELECT * FROM propriedades WHERE deleted = false
  - SELECT * FROM fones        WHERE deleted = false
  - SELECT * FROM itensped     WHERE deleted = false
  - SELECT * FROM estoque      WHERE deleted = false

Com a fase 2 em produção, essas tabelas são alimentadas: o EstadoDesejado deste app mapeia cliente→propriedades, pedido→contratos, item→itensped e produto/kit→estoque, e chega lá pelo PUT /api/v1/xadm do Integrador (§3). O log "tabela ausente" do PowerSync era o estado da fase 1 — se voltar a aparecer, é sintoma, não o esperado. Runbook e regra de "nunca down -v em produção" (apaga o bucket) estão no repositório do PowerSync.


5. Quais dados chegam no cliente Java (ps-java)

O ps-java (maxsul/ps-java) é um programa Java puro (Java 21+, sem Gradle/Maven para rodar — só java -jar), entregue à equipe do ERP como exemplo funcional. Ele não puxa API nem faz polling: consulta o SQLite local que o PowerSync mantém atualizado em tempo real.

Chegam ao cliente exatamente as cinco tabelas espelho: contratos, propriedades, fones, itensped, estoque (linhas com deleted = false) — ou seja, o pedido completo pronto para o X-Adm: cabeçalho + itens + cliente + contato + produtos.

Fluxo no cliente:

  1. Watch reativo no SQLite: linhas novas/alteradas (estado desejado).
  2. Para cada uma: marca "em processamento", grava no X-Adm — hoje um TODO X-Adm (simulaXadm); a gravação real no ZIM é desenvolvida depois, pela equipe X-Adm.
  3. Devolve o resultado: write-back por upload do PowerSync (o SDK envia a alteração local de volta ao endpoint de upload, que persiste CodRetorno/MsgRetorno e as Chave* nas tabelas do Integrador). Offline-first: se a rede cair, o upload fica enfileirado e reenvia.

Máquina de status (códigos de retorno X-Adm — MAXSUL2025_610110)

O status de cada registro segue os códigos de retorno do X-Adm (000 novo → 001/002/003 em processamento → 006 gravado; 1XX erro terminal; 9XX erro de processamento). Tabela completa em Mapeamento §6.

Nota — exemplo atual × modelo final. O ps-java atual sincroniza uma tabela única requisicoes com status RECEBIDO/EM_PROCESSAMENTO/CONCLUIDO_*, para provar o mecanismo de sync/write-back. O modelo final são as cinco tabelas acima com os códigos de retorno — alinhar o ps-java a esse modelo é trabalho da fase 2.


6. Parte técnica (referência)

6.1 Modelo de dados

O modelo completo — as duas famílias (espelho do X-Adm, dono Integrador; pied_*, dono maxsul-pied), com schema, tipos, chaves e relacionamentos — está em Modelagem. A transformação de uma família na outra (de-para, regras de negócio, códigos de retorno) está em Mapeamento e regras. Postgres 18 (uuidv7() nativo).

6.2 Contrato de entrada (PIED) — validado

Endpoints REST, envelope, paginação, incremental e webhook (payloads, eventos, lacunas, segurança) em API da PIED.

6.3 Contrato de saída (Integrador) — reaproveitado

PUT/DELETE /api/v1/xadm, Bearer, sempre HTTP 200 para JSON válido, resultado no corpo — o mesmo contrato do agente on-premises do X-Adm. Endpoints, semântica DELETE→PUT, código Origem e corpo por tabela em API do Integrador.

6.3.1 Gravação no ZIM (X-Adm)

O X-Adm roda sobre ZIM, cujo modelo de dados difere de um Postgres comum (Character de largura variável, VastInt escalado, máscara ≠ valor armazenado). Os campos viajam tipados no pipeline e a formatação para o ZIM é feita no cliente, na fronteira com o zimrtmu. Tipos, regras de formatação por campo e armadilhas em Tratamento de dados no ZIM — é o ponto de maior risco da integração.

6.4 Idempotência e ordem

  • Raw persistido antes de processar; reprocessar é seguro.
  • Transform idempotente por content-hash (só reescreve em mudança real → sem sync inútil).
  • Apply no Integrador serializado por ReentrantLock (ordem DELETE→PUT, sem deadlock 40P01).
  • Overlap webhook × poll é seguro por chave natural (código do pedido/produto, documento do cliente).

6.5 Segurança

  • Webhook: segredo compartilhado opcional (header X-Pied-Secret, a confirmar com a PIED) + possível IP allowlist no Traefik; HTTPS obrigatório; payload com PII mascarado em log.
  • REST ao Integrador: Bearer estático (env do Coolify).
  • PowerSync: JWKS auth.xadm.biz, audience maxsul; credencial do Postgres só por PS_PG_URI.

6.6 Deploy e operação

Coolify por subdomínio (pied. / int. / ps. maxsul.xadm.biz), imagens multi-stage, GET /health. Toggles e passo a passo em operacao/configuracao.md.

6.7 Qualidade

  • Cliente REST/paginação com WireMock (mock da API PIED).
  • Webhook ponta a ponta (@MicronautTest + Postgres via PostgresTestResource): segredo, toggles, corpo arbitrário.
  • Checkstyle X-Adm no gate (./gradlew check, CI Forgejo).

6.8 Riscos e pendências

Risco / lacuna Situação
A lista decisão-a-decisão (cada item com alternativas e recomendação) está em
Pendências. Resumo dos principais riscos:
Risco / lacuna Situação
Colunas de venda + fones no modelo X-Adm do Integrador Aditivo ao molde compartilhado; delta de colunas a confirmar com o Integrador (Modelagem §1).
Campo que marca "pedido finalizado" na PIED Bloqueia o quando enviar (INSERT-only); definido a partir do raw da Fase 1 (§3.3).
De-para restante (ClassForma, Prazo, EAN13, rateio de frete) Regra/confirmação — Pendências.
Header do segredo do webhook / retry da PIED A confirmar com a PIED (API PIED §3.2). Token de produção é variável do Coolify.
ps-java ainda no modelo de exemplo (requisicoes) Alinhar às 5 tabelas + códigos de retorno na fase 2.
Crescimento do landing pied_* Política de retenção decidida na fase 2, com volumes medidos.

7. Como esta doc se destila nos outros repositórios

A documentação completa vive aqui. Cada repositório carrega a fatia dele e aponta para cá:

Repositório O que documentar lá Aponta para
Integrador As colunas de venda + a tabela fones no modelo X-Adm compartilhado (P1, de Modelagem §1), a fonte via /api/v1/xadm, e o registro do origem INT-PIED. esta doc (§2), API Integrador, Pendências
maxsul-pied (aqui) Webhook, poll, transform, tabelas pied_*, UI; modelo em Modelagem, transform em Mapeamento. — (é a doc-mãe)
maxsul-powersync Sync rules, replicação, runbook (já existe). esta doc (§4)
ps-java Cliente de exemplo; simulaXadm; alvo = 5 tabelas + códigos de retorno; formatação para o ZIM (integracao-zim). esta doc (§5)

8. Apêndice — histórico

Decisões: