0013 — Fluxo de entrada PIED no espelho do X-Adm (chave dupla + write-back)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-15 · Decidido em: 2026-07-15
Atualização 2026-07-15 — identidade do produto. A identidade do produto no espelho é
ChaveEst(eCodProd, versão virtual/substring doChaveEst, usada na instância Thoms ondeChaveEstchega null) — confirmado com a equipe ERP. Ocodigo_alt(código do produto na PIED), adotado abaixo como chave da entrada, foi reenquadrado como transporte e renomeado parapied_codigo_alt(migração V19) — identidade nunca é vocabulário do parceiro. Oestoqueda Thoms vem do PUT de saída do X-Adm (não do fluxo de entrada PIED); o apply resolve a saída porChaveEstou, quando null, porCodProd. Onde o texto abaixo dissercodigo_altcomo chave, leiapied_codigo_alt(transporte) + identidadeChaveEst/CodProd. Fase 2 (V20) fará o mesmo rename emx_ped/itensped.codigo_alt. Ver.ia/007-identidade-produto-espelho-spec.md.Atualização 2026-07-15 — sinal de mudança de estoque (feature 008). O tradutor WebStorm-ecom da Thoms (
clientes/thoms/int-produtos-ecom) consome oestoquedeste espelho. Duas entregas do integrador destravam o v1 dele (migração V21): (1) colunaestoque.updated_at(TIMESTAMPTZ) como chave-versão, mantida no Java por Micronaut Data (@DateUpdated) na entidadeEstoque— auto-populada em toda escrita de entidade (save/update), por qualquer escritor que passe pelo repositório (apply XADM, tela adminEstoqueViewController, API REST, write-back PowerSync). Cobre os 4 escritores porque todos usam Micronaut Data; o@Querycru que só mexe emnome_prod_curtonão passa por entidade e (de propósito) não bumpa. Sem dirty-check: bumpa também em reenvio idêntico do X-Adm e no write-back decod_retorno/chave_est→ o tradutor reenvia à WebStorm com mais frequência (inofensivo — WebStorm é idempotente, o tradutor coalesce). Alternativa preterida: trigger Postgres comIS DISTINCT FROM(dava o dirty-check, mas o time preferiu manter a lógica em Java, sem trigger). (2) Webhook de saída (poke):EstoqueMudouEvent(publicado pós-commit peloXadmServicequando o payload tocou estoque) →EstoqueWebhookListener(virtual thread fire-and-forget) →@ClientPOSTvazio + Bearer ao tradutor, config-gated off (webstorm.webhook.enabled, só o deploy Thoms liga — env vars do Coolify emdev/application-config.md). Best-effort: falha do poke não derruba o XADM — o@Scheduleddo tradutor é a rede de segurança (oupdated_atjá está persistido). Escritas via admin/API não pokam, mas bumpamupdated_at→ chegam pelo sweep. Ver.ia/008-sinal-estoque-tradutor-spec.md. Evolução aberta (novo prompt.ia/009): estenderupdated_ata todas as tabelas espelho do X-Adm (não sóestoque).Atualização 2026-08-11 — FORMULAS (fórmula/BOM do kit). Nova tabela-espelho de entrada (
.ia/013): a fórmula de um pedido-kit PIED — uma linha por componente (pied_x_ped,pied_cod_prod,qtde). Distinta das demais tabelas de entrada em três pontos: (1) o id do X-Adm (pied_formula_id, sequencial) é gerado por nós (GENERATED ALWAYS AS IDENTITY, coluna DB não-mapeada na entidade — a réplica WAL do PowerSync a carrega), não pelo X-Adm — as outras tabelas recebem a identidadeChave*do X-Adm; (2) sem colunasChave*— o X-Adm nunca devolve chave neste fluxo (confirmado no staging: 0/62 itensped PIED têm chave), sócod_retorno/msg_retorno(006); (3) sem DELETE — a PIED só envia pedido pago, imutável. Upsert single-key (sem dual-key). Contrato público em xadm-ingest; migraçãoV25__formulas.sql.
Contexto. O Integrador é um espelho do ERP X-Adm — todas as tabelas são tabelas do X-Adm,
num modelo único. Até aqui só havia o fluxo de saída: X-Adm → Integrador → PowerSync → apps
mobile (projetos Sul Plata / OnPetro). O X-Adm é o produtor e empurra por /api/v1/xadm com a
Chave* (identidade do ERP) já preenchida.
A integração PIED (Maxsul) introduz o fluxo de entrada: PIED → INT-PIED → Integrador →
PowerSync → X-Adm. Agora uma fonte externa (PIED, pedidos de venda) é o produtor. As mesmas
tabelas do espelho passam a receber mais colunas (as mesmas tabelas do ERP, com o subconjunto
de colunas que a Sul Plata não usava). A Chave* chega vazia — quem a gera é o X-Adm, que
a devolve com o status pelo write-back. Schema autoritativo: documento MAXSUL2025_610110 (repo
clientes/maxsul/int-pied); o dono do Flyway é este repo.
Decisão. Trabalho aditivo ao espelho compartilhado (mesmo modelo, sem schema por cliente, sem tabelas paralelas). Quatro escolhas centrais:
- Chave dupla no upsert (rastro L12). O
upsert*/softDelete*resolvem a linha pelaChave*do X-Adm quando o produtor a fornece (fluxo de saída, comportamento intacto) e caem para a chave natural da fonte externa quando ela chega vazia (fluxo de entrada) —contratos→pied_x_ped,estoque→pied_codigo_alt(oucod_prodna saída Thoms),itensped→(pied_x_ped,pied_codigo_alt),propriedades/fones→cgc_cpf(nomespied_*após V19/V20 — ver nota de atualização no topo). Materializado nofinderde cada método (oEscritaPorChaveNaturalnão muda). Alternativa preterida: métodos de upsert paralelos por fluxo — mais código, mesma lógica. - Colunas de retorno são preenchidas só pelo write-back (L12). No fluxo de entrada, a fonte
(PIED) é dona das colunas de dado; o X-Adm é dono das colunas de retorno (
Chave*,CodRetorno,MsgRetorno). Elas são read-only no caminho de entrada e chegam pelo write-back (POST /api/v1/powersync). NocopiaCamposdo ramo de entrada elas são preservadas (não sobrescritas com o branco que a fonte envia) — senão o reenvio idempotente apagaria o retorno recém-gravado. Mesmo padrão donomePropCurto. As telas admin mostram essas colunas read-only. - NOT NULL relaxado + crítica em runtime (L15). O fluxo de entrada deixa vazias tanto colunas de
dado do fluxo de saída (
seq_veic,cod_mun,nome_prod…) quanto as colunas-chave (chave_cp/chave_est/chave_prop/chave_item,NOT NULLantes — o X-Adm só as gera depois). A migração V18 relaxa todas para nullable; a garantia de cada fluxo passa à crítica na camada de apply (rejeita registro sem chave natural resolvível). Alternativa preterida:CHECKcondicional no schema — acopla o banco à noção de fluxo. - Write-back reusa o PowerSync (L16), sem endpoint novo. O transporte (insert/update local →
uploadData→POST /api/v1/powersync) já existia. Só a aplicação de campos doPowersyncControllerfoi estendida: somarcontratos/itensped/fonesao switch e cadapatchX(inclusivepatchPropriedades/patchEstoque, que só aplicavam nome-curto) gravarcod_retorno/msg_retorno/chave_*.fonesresolve a linha só porid(tabela nova, semold_id).
Princípio arquitetural — colunas de retorno do X-Adm¶
Toda tabela que volta ao X-Adm (fluxo de entrada) carrega CodRetorno + MsgRetorno — princípio
universal, sem exceção. O X-Adm atualiza essas duas colunas com o status do processamento de cada
linha que recebe (junto com as Chave* que ele gera). Códigos (doc-mãe MAXSUL2025_610110, ver
clientes/maxsul/int-pied/docs/projeto/mapeamento.md): 006 = gravado no X-Adm (sucesso);
000/00X = novo/em processamento (pendente, ainda não cadastrado); 1XX = erro terminal;
9XX = erro de processamento. Consequências:
- Fluxo de saída (ex. Sul Plata): as tabelas não voltam ao X-Adm —
CodRetorno/MsgRetornoexistem no schema compartilhado mas ficam semprenull. - Fluxo de entrada (ex. PIED):
CodRetorno/MsgRetornosão preenchidas pelo write-back e seguem a especificação do fluxo (clientes/maxsul/int-pied).fonestambém carregaCodRetorno(o princípio é universal) — isso diverge da doc-mãe MAXSUL2025_610110, que especificoufonessó comMsgRetorno; a divergência é intencional e deve ser reportada à equipe X-Adm / maxsul-pied.
Leitura do retorno — reconciliação (fecha o loop)¶
O int-pied precisa saber o resultado real do X-Adm para reconciliar o próprio estado. O Integrador
expõe GET /api/v1/xadm/retorno/{tabela} (por chave natural do PIED: contratos?xPed,
estoque?codigoAlt, itensped?xPed+codigoAlt, propriedades/fones?cgcCpf; {tabela}
case-insensitive; ?origem= opcional/ignorado). Devolve um DTO enxuto { tabela, status, codRetorno,
msgRetorno, chaveXadm } — HTTP 200 sempre (poll não toma 404; 400 só para tabela/chave
inválida; Bearer herdado do ApiBearerSecurityRule). O status é derivado dos códigos do X-Adm:
006→CONFIRMADO, 000/00X(≠006) ou branco→PENDENTE, 1XX/9XX→REJEITADO, linha ausente
(ou soft-deleted)→NAO_ENCONTRADO; o codRetorno cru também volta (para distinguir 1XX×9XX).
Read-only — não grava em request (poll de alta frequência). Mantém o acoplamento na API (o
int-pied não lê o Postgres direto). Código: ingest/XadmRetornoController + StatusRetorno.derive.
Tipos/nomes (L13/L14/L17): convenção da casa (JSON PascalCase — com o pedido literalmente
xPed, x minúsculo — → DB snake_case → Java camelCase); tipos casando com o repo
(CHAR/VARCHAR/NUMERIC/DATE). fones é 1 contato por cliente (cgc_cpf UNIQUE). chave_prop
ampliado a CHAR(11). Índices unique das chaves da fonte são parciais (WHERE … IS NOT NULL) —
não colidem com as linhas do fluxo de saída. INT-PIED entra em XadmService.KNOWN_ORIGENS.
Conformidade à largura fixa do espelho (o espelho é refém do X-Adm)¶
As colunas do espelho têm a largura fixa do X-Adm real — nunca são alargadas (o valor não caberia no X-Adm no sync). Então a fonte se ajusta ao espelho, não o contrário (achados do e2e local):
- Chave = código, não ObjectId. A chave natural da entrada (
xPed,CodigoAlt) é odata.codedo pedido PIED (número, ~9 chars, cabe noCHAR(15)), não odata.id(ObjectId de 24, que estoura). O de-para do int-pied e a reconciliação (GET /retorno) usam ocodedos dois lados. No kit, oitensped.CodigoAlt= o mesmodata.codedoestoque(o item referencia o produto; o apply resolveitenspedpor(xPed, CodigoAlt)— vazio não resolve). - Texto trunca no ingest; chave erra. O
EscritaPorChaveNaturaltrunca cadaStringao@Size(max)declarado na própria entidade (fonte única = o domain, lido por introspection) antes de persistir, comlog.warn. As chaves cabem por construção (code≤15,cgcCpf=14) e, se estourassem, erram no persist — chave que não cabe é violação de contrato, não texto a cortar.
Consequências / trade-offs.
- O fluxo de saída segue idêntico (o ramo
Chave*do upsert não mudou); a garantia de chave saiu do schema (NOT NULL) e virou crítica em runtime — inclusive o CRUD admin perdeu a validação de chave em branco (o admin não passa pela crítica do ingest). Aceito: admin é operador confiável. - Cobertura de teste: o round-trip real-DB (idempotência, preservação do retorno, write-back)
roda via
fones(repo real); a chave dupla das demais tabelas fica em unit (Mockito). Motivo: os@Factory @Replacessem@RequiresemPushContextBuilderIntegrationTest/PowersyncControllerTestmockam esses repositórios globalmente em todo@MicronautTest. Guardá-los com@Requiresé um follow-up de test-infra. - V17-fantasma (resolvido): a migração nova é V18 (pula V17). O
V17__request_identity_sequence_syncfoi adicionado e removido do classpath; comvalidate-on-migrate: true, se aplicado em produção o boot do Flyway falharia. Endereçado porflyway...ignore-migration-patterns: ["*:missing"]noapplication.yml(tolera migração aplicada-mas-ausente; no-op em banco novo). V18 vingou e as migrações seguiram por cima até V25 em produção — o número está consolidado. chave_prop=11 (confirmado): validado com o PIED rodando em staging sem erro de persist de chave — a largura 11 cabe a chave real da fonte. Pendência de aval encerrada.- Fora de escopo: a fase 2 do
maxsul-pied(produtor real dos campos); a config de down-sync do PowerSync (carregar as colunas novas ao cliente/X-Adm); múltiplos endereços por cliente (assumido 1).
Detalhe de mecânica vive no código (ingest/XadmApplyService, ingest/XadmPayloadParser,
powersync/PowersyncController, migração V18__fluxo_entrada_pied.sql) — aqui fica só o durável.