Pular para conteúdo

API do Integrador — /api/v1/xadm

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-10

Como o maxsul-pied grava o estado desejado nas tabelas espelho do X-Adm (contratos/propriedades/fones/itensped/estoque): pelo mesmo contrato REST que o agente on-premises do X-Adm já usa para popular o Integrador. O maxsul-pied não escreve SQL direto — mesmo compartilhando o Postgres, respeita a propriedade do Integrador (Projeto §2.2). O schema autoritativo das tabelas vive no repositório do Integrador.

1. O contrato de ingestão

O contrato abaixo é fato do Integrador, que é o dono dele: chega do raw/ dele vendorado em docs/_importado/, byte a byte, fixado (pin) na versão que este app implementa — não re-digitado (constituição §1.9). Adotar versão nova do contrato é atualizar a cópia no mesmo PR que muda o código.

Por que isto não é uma cópia

Até 2026-07-17 esta página re-digitava o contrato à mão, e ele já havia envelhecido: dizia que nenhuma origem da PIED estava registrada no Integrador (a INT-PIED já estava), e não trazia a guarda de no-op nem os casos 401/500. Fato re-digitado longe do dono envelhece sozinho; fato importado, não.

Endpoint e autenticação

O integrador tem um endpoint de ingestão, com dois verbos sobre o mesmo corpo:

Verbo Efeito
PUT /api/v1/xadm upsert — cria ou atualiza; registro que existia deletado é revivido
DELETE /api/v1/xadm soft delete dos registros identificados pelas chaves de negócio do corpo

Toda rota /api/** exige Authorization: Bearer <token>. O token é estático e por instalação (há uma instalação por cliente), entregue por canal seguro — sem ele a resposta é 401.

Envelope

O corpo é um objeto JSON com o campo Origem (string) e uma chave por tabela, cada uma com um array de objetos:

{
  "Origem": "PNOTAI",
  "ESTOQUE": [ { "ChaveEst": "...", "Saldo": 10 } ],
  "MUNICIPIO": [ { "CodMun": "4314902", "Estado": "RS", "NomeMun": "Porto Alegre" } ]
}

As chaves de tabela reconhecidas são 15:

CONTRATOS · ITENSPED · ITENSPEDAMX · ITPEDAMXDI · LOTEEST · ESTOQUE · MUNICIPIO · PROPRIEDADES · FONES · VEICULOS · NOTACOMPL · ITENS · ITENSLOTE · TBPCOEST · FORMULAS

O nome da chave é case-sensitive: chave com caixa errada não casa tabela nenhuma e o payload é tratado como vazio (ver Resposta). Tabela ausente do corpo é ignorada — só se envia o que mudou.

Resposta

O status HTTP não diz se deu certo. Falha de processamento responde 200 com resultado: "ERRO" — o contrato foi mantido assim por compatibilidade com o integrador antigo. Quem consome tem de olhar o campo resultado, não o status HTTP; um cliente que trate 200 como sucesso perde toda falha em silêncio.

{ "requestId": 1234, "resultado": "SUCESSO", "mensagem": null }
Campo Valor
requestId id do registro Request criado para esta chamada (rastreia o processamento)
resultado SUCESSO, ERRO ou PENDENTE
mensagem null em sucesso; o detalhe do erro quando resultado é ERRO
HTTP Quando
200 o corpo era JSON válido — inclusive quando o processamento falhou (resultado: "ERRO")
400 JSON inválido ou malformado; corpo {"requestId": null, "resultado": null, "mensagem": "JSON inválido: …"}
401 Bearer ausente ou inválido
500 falha antes do processamento (ex.: o registro da chamada não gravou). Erro de processamento não é 500 — é 200 com ERRO

Payload que não casa nenhuma tabela conhecida responde 200 com resultado: "ERRO" e a mensagem "payload sem nenhuma tabela reconhecida (verifique as chaves de array por tabela)". É guarda deliberada contra o no-op silencioso: sem ela, chave com caixa errada voltaria SUCESSO sem gravar nada.

Origem

Origem identifica a rotina do X-Adm que originou a carga. Ela é gravada e usada para rastreio, nunca validada: origem desconhecida é aceita e processada normalmente.

O que a lista de origens conhecidas muda é só o roteamento de erro ao Sentry — falha de processamento numa origem conhecida vira evento com as tags origem/tipo/request_id; numa origem fora da lista, a falha não é reportada. Ou seja, origem errada não quebra a chamada, mas apaga o alarme: o erro fica invisível.

Origens conhecidas hoje: PPEDAMX · PPEDIDO · PBLDI · PNOTAI · PNOTAD · PCSTPROD · PREQUIS · INT-PIED.

FORMULAS — fórmula (BOM) do kit (fluxo de entrada PIED)

Tabela de entrada (Origem: INT-PIED): a fórmula de um pedido-kit — um objeto por componente do kit. O produtor manda só três campos de input (caixa exata):

Campo Tipo Conteúdo
xPed string(15) código do pedido do kit (casa com o xPed de CONTRATOS/ITENSPED)
CodProd string(6) código do produto componente
Qtde decimal quantidade do componente consumida na fórmula
{ "Origem": "INT-PIED",
  "FORMULAS": [ { "xPed": "260069986", "CodProd": "22162", "Qtde": 10 },
                { "xPed": "260069986", "CodProd": "16442", "Qtde": 2 } ] }

Chave natural (xPed, CodProd). É fluxo de entrada de mão única para o dado: o X-Adm cadastra e devolve só o status (CodRetorno/MsgRetorno) pelo write-back (006 = gravado) — nunca devolve chave, então a tabela não carrega colunas Chave*. Reenvio idempotente preserva o status já gravado. Sem DELETE (pedido pago é imutável).

Tabela ESTOQUE

Cada item do array ESTOQUE é um produto. Todos os campos são opcionais no envelope — o que não vier não é escrito.

Campo Tipo Nota
ChaveEst texto identidade do produto. É a chave de join do espelho
codProd texto identidade alternativa, usada onde ChaveEst chega vazio. Aceita CodProd (PascalCase)
NomeProd texto descrição. Aceita nomeProd
EAN13 texto código de barras. Aceita ean13
CodProdAlt texto código alternativo do produto no X-Adm
CodigoAlt texto código do produto na PIED — rastreabilidade de origem externa, gravado em pied_codigo_alt. Não é identidade nem critério de join
Venda decimal preço de venda. Aceita venda
VendaPz decimal preço a prazo. Aceita vendaPz
Saldo decimal saldo em estoque. Aceita saldo

Identidade. O produto é identificado por ChaveEst; onde ele chega vazio (o caso da instância Thoms), a identidade é o codProd. A chave do parceiro (CodigoAlt) nunca é identidade — entra prefixada no espelho (pied_codigo_alt) e serve só para rastrear de onde o dado veio. Promover chave de parceiro a identidade de primeira classe acopla o espelho ao vocabulário de um terceiro.

Write-back. CodRetorno e MsgRetorno não são lidos deste payload: são escritos pelo X-Adm no sentido inverso. Mandá-los aqui não tem efeito.

Caixa dos campos. Onde a tabela diz "aceita", o parser lê as duas grafias; nos demais campos a grafia é exata. Campo com caixa fora do previsto é ignorado em silêncio — o registro é gravado sem ele, e a chamada volta SUCESSO.

2. Como o maxsul-pied usa esse contrato

O que segue não é o contrato — é a decisão deste app sobre como consumi-lo.

  • Ordem DELETE → PUT: o padrão do ERP para limpar/reescrever estado. O maxsul-pied envia os pares na ordem certa e não paraleliza writes concorrentes na mesma chave. (Do outro lado, o Integrador serializa o apply num lock global, FIFO — mas depender disso seria depender de detalhe interno do dono; a ordem é responsabilidade de quem envia.)
  • Soft-delete/revive: combina com o content-hash do transform — só reescreve em mudança real.
  • Idempotência: a chave natural (código do pedido/produto, documento do cliente) é o que torna o reenvio do mesmo estado inofensivo.
  • Leitura da resposta: o IntegradorClient trata só resultado: "SUCESSO" como enviado; qualquer outro vira ERRO na máquina de status, sem retentar. O transporte (timeout/5xx/429) é que é retentado. Isto implementa o aviso do contrato acima — o status HTTP não diz se deu certo.
  • Reconciliação pela remessa: o status do pedido sai do manifesto (§5), não do retorno de uma tabela — CONFIRMADO só quando não há remessa viva e existe ao menos uma ENTREGUE.
  • Origem: o maxsul-pied envia INT-PIED em todo o payload da Maxsul (já registrada no Integrador — ver Origem no contrato acima; é o que faz a falha destes writes ser reportada).

3. Corpo por tabela (o mapeamento deste app)

Payload = Origem + um array por tabela, na ordem de dependência (propriedades → fones → estoque → contratos → itensped). Nomes de campo conforme Modelagem §1 (PascalCase do MAXSUL2025_610110). PUT faz upsert por chave natural; DELETE leva só as chaves.

PUT (upsert):

{
  "Origem": "INT-PIED",
  "propriedades": [ { "CgcCpf": "12345678000109", "CodProp": " 0", "NomeProp": "Solar Sul Ltda",
                      "Endereco": "Rua das Acácias", "Numero": "100", "Bairro": "Centro",
                      "Cidade": "Ponta Grossa", "Estado": " PR", "CEP": "84010000" } ],
  "fones":        [ { "CgcCpf": "12345678000109", "DDD": "42", "Telefone": "999990000",
                      "Nome": "João", "Email": "joao@solarsul.com" } ],
  "estoque":      [ { "CodigoAlt": "MOD-550", "NomeProd": "Painel Solar 550W", "Venda": 780.00000 } ],
  "contratos":    [ { "xPed": "6f3a9c12", "CgcCpfCliente": "12345678000109", "ChaveUnid": " 1 29",
                      "CodMat": " 1 0 134", "DtEm": "20260615", "VlTot": 7800.00, "NatOp": "6.102",
                      "TpVda": "C" } ],
  "itensped":     [ { "xPed": "6f3a9c12", "CodigoAlt": "MOD-550", "Qtde": 10.000, "Valor": 780.00000,
                      "Total": 7800.00 } ]
}

DELETE (por chave natural):

{ "Origem": "INT-PIED",
  "itensped":  [ { "xPed": "6f3a9c12", "CodigoAlt": "MOD-550" } ],
  "contratos": [ { "xPed": "6f3a9c12" } ] }

Formatação dos valores: no pipeline viajam tipados (NUMERIC/TEXT/DATE); a conversão para o formato de armazenamento do ZIM (Character com espaços significativos, VastInt com escala fixa, datas AAAAMMDD) é feita no cliente — ver Tratamento de dados no ZIM.

4. Write-back (status de volta)

O resultado da gravação no X-Adm (CodRetorno/MsgRetorno e as Chave*) volta pelo conector PowerSync do Integrador (POST /api/v1/powersync), que persiste nas tabelas espelho. Ver Projeto §5.

5. Manifesto de remessa — a consulta de retorno e o fechamento à mão

O aceite do PUT (§1) só diz que o Integrador persistiu o estado desejado — não o resultado do X-Adm. Quem fecha esse laço é o manifesto de remessa: uma consulta cobre o pedido inteiro, com o status da remessa e, nas TRAVADA, os itens rejeitados. O contrato é fato do dono, vendorado como os da §1 — pin: integrador-server 43c3048 (decisão 0025, com o 409 para linha pendente).

Consultar remessas

GET /api/v1/integracao/remessas?origem=<origem>[&chave=<chave>]...[&status=<status>]

Fora do namespace /xadm de propósito: remessa é dado de controle do Integrador, não espelho do ERP. Toda rota /api/** exige Authorization: Bearer <token>; sem ele, 401.

parâmetro efeito
origem obrigatória — hoje só INT-PIED tem manifesto
chave pied_x_ped do pedido ou cgc_cpf do cliente. Aceita repetição (&chave=A&chave=B) — uma chamada por rodada, não uma por pedido
status filtra por ABERTA, ENTREGUE, TRAVADA ou RESOLVIDA_MANUAL

Chave inexistente não é 404: é 200 com lista vazia. Quem consome decide pelo conteúdo.

[
  { "id": "0199…", "chaveNegocio": "260079421", "status": "TRAVADA", "totalItens": 5,
    "criadoEm": "2026-09-08T14:02:11Z",
    "itensRejeitados": [
      { "tabela": "propriedades", "linhaId": "0199…", "codRetorno": "120",
        "msgRetorno": "Cliente CPF 072.508.149-07 não cadastrado.", "bloqueado": false },
      { "tabela": "contratos", "linhaId": "0199…", "codRetorno": "120",
        "msgRetorno": "Pedido (10738.24) não cadastrado.", "bloqueado": true } ] },
  { "id": "0199…", "chaveNegocio": "260081234", "status": "RESOLVIDA_MANUAL", "totalItens": 4,
    "criadoEm": "2026-09-09T10:40:02Z", "itensRejeitados": [],
    "resolucaoManual": { "em": "2026-09-10T16:20:00Z",
      "observacao": "Lançado manualmente no X-Adm: kit sem produto cadastrado.",
      "operador": "fulano@maxsul.com.br" } }
]
campo valor
id id da remessa
chaveNegocio pied_x_ped ou cgc_cpf
status ver a tabela de estados abaixo
totalItens quantos itens a remessa declara — a coluna, não um COUNT(*); é o que detecta lista rasgada
criadoEm quando a remessa nasceu
itensRejeitados os itens em 1XX, só nas TRAVADA. Nas demais vem [] — nunca ausente
resolucaoManual só nas RESOLVIDA_MANUAL: em, observacao e operador (este some quando não foi informado). Nas demais, o campo some

Em cada item rejeitado vêm tabela, linhaId, codRetorno, msgRetorno e bloqueado. O bloqueado separa a causa do colateral: true quer dizer que um pai desta linha, na mesma remessa, também está em 1XX. Quem reporta ao operador nomeia a mensagem do item não bloqueado. No exemplo, a causa é o cliente; o "Pedido não cadastrado" é consequência.

Os estados

status significa viva?
ABERTA ainda não entregue, sem rejeição terminal sim
TRAVADA alguma linha em 1XX; só o re-arme a reabre sim
ENTREGUE todas as linhas aceitas (006) — histórico não
RESOLVIDA_MANUAL o operador lançou o pedido à mão no X-Adm e fechou a remessa por comando — histórico não

Uma remessa viva por (origem, chave): é invariante de banco. As não vivas acumulam. Os três primeiros estados são derivados dos cod_retorno. RESOLVIDA_MANUAL é o único que nasce de comando e que a derivação nunca reabre.

Um consumidor que encontre um status que não conhece deve tratá-lo como não travado e não enviável. É o que mantém a adição de um estado não quebrante.

Fechar à mão uma remessa travada

POST /api/v1/integracao/remessas/resolver-manual?origem=INT-PIED&chave=<xPed>
Content-Type: application/json

{ "observacao": "Lançado manualmente no X-Adm: kit sem produto cadastrado.",
  "operador": "fulano@maxsul.com.br" }

Serve para o pedido que não se resolve reenviando (ex.: o produto-kit que o ERP não criou) e que o operador lançou direto no X-Adm. Sem isto, a remessa seguiria TRAVADA para sempre. Quem chama é o botão "Importado manualmente" do painel do maxsul-pied.

corpo regra
observacao obrigatória, até 2000 caracteres — é o que sobra para reconstruir o caso
operador opcional, até 100 caracteres. O Bearer é estático e não identifica pessoa
{ "chave": "260079421", "resolvidas": 1 }
HTTP quando
200 fechou (resolvidas: 1), ou não havia remessa viva (resolvidas: 0: nunca existiu, já entregue ou já resolvida)
400 origem ausente ou diferente de INT-PIED; chave ausente; observacao ausente, branca ou longa demais; operador longo demais
401 Bearer ausente ou inválido
409 a remessa viva está ABERTA (ainda em entrega), está TRAVADA com linha ainda pendente (000/001/9XX), ou mudou de estado durante o comando — consulte e tente de novo

Só TRAVADA. Fechar uma ABERTA enquanto o cliente pode estar mandando as linhas arrisca duplicar no ERP.

E sem linha pendente. Numa TRAVADA o filho de um pai 1XX fica 000, bloqueado. Fechar a remessa a tira do manifesto, e o cliente passaria a mandar esse filho por presença — duplicando no ERP o que o operador lançou à mão. Nesse caso o caminho é corrigir o dado e re-armar.

Não mexe nas linhas. As 1XX continuam 1XX e o cliente não as coleta. Muda só a remessa.

Idempotente: a segunda chamada devolve resolvidas: 0, porque a remessa já não está viva.

Depois dele, o re-arme da chave responde 409. Re-armar reenviaria ao ERP o que o operador já lançou. E se a chave ganhar uma remessa viva nova, o re-arme dela não reabre as linhas de pedido (contratos/itensped/formulas) que também são itens da resolvida.

Como o maxsul-pied usa o manifesto

  • Reconciliação (ReconciliacaoJob): CONFIRMADO só quando não há remessa viva e existe ao menos uma ENTREGUE. RESOLVIDA_MANUAL não é viva, mas também não conta como entregue — o ERP recusou e quem resolveu foi gente; confirmar por ela diria que o X-Adm aceitou o que rejeitou. Status que este app não conhece conta como viva (conservador: nunca vira CONFIRMADO).
  • Botão "Importado manualmente" (PainelController, só em Falha): chama o resolver-manual antes de marcar o pedido aqui, e só o 200 (resolvidas 1, ou 0 = nada vivo lá) leva ERRO_XADM → IMPORTADO_MANUAL. O 409 (parte do pedido ainda pendente de entrega: remessa ABERTA, ex. depois de um re-arme, ou TRAVADA com filho 000 bloqueado por um pai 1XX) e a falha de transporte deixam o pedido em Falha, com flash dizendo por quê — no 409, não lançar à mão, que duplicaria: aguardar, ou corrigir o dado e Re-Importar (o detalhe volta com ?pendente=1, que põe o botão à mostra). Na ordem inversa, uma falha do comando deixaria o pedido fechado aqui e a remessa TRAVADA lá para sempre, sem ninguém que a reconcilie de novo.
  • operador: opcional na tela (o painel não tem login), lembrado no navegador; vai no corpo só quando preenchido. Localmente entra no fim da observação (importacao_manual_obs).
  • Fixtures do contrato: o IntegradorClientTest lê deste arquivo os exemplos json (listagem e resposta do resolver) e o de requisição do resolver (rota + corpo), e alimenta o servidor fake com eles (ContratoImportado). Atualizar o pin re-testa o parser contra o contrato novo.

O campo é itensRejeitados — não itens

O nome vem do RemessaResponse do dono. Este app lia itens e recebia sempre lista vazia: a reconciliação gravava "Remessa travada no X-Adm." sem cod_retorno, e sem ele o alerta 1XX não disparava e o reenvio não re-armava o espelho. Quinze pedidos-kit ficaram parados em silêncio (2026-09-10). As fixtures de teste também usavam itens e por isso passavam — daí elas saírem, agora, do exemplo do contrato.

A rota por tabela (GET /api/v1/xadm/retorno/{tabela}) não reconcilia — só alimenta o diagnóstico do painel

Desde 2026-09-10 o detalhe do pedido em Falha a consulta ao vivo (DiagnosticoService → IntegradorClient.consultarRetorno): a remessa só lista o que foi rejeitado, e o operador precisa ver o que o X-Adm gravou (e o código do contrato lá) para lançar o resto à mão. Na reconciliação ela segue fora, pelo motivo abaixo.

Ela responde uma linha por chave natural e cobre só contratos/estoque/itensped/propriedades/ fones — não formulas. Reconciliar por ela deixava item e fórmula rejeitados invisíveis daqui: o pedido 260079618 ficou CONFIRMADO com o itensped em 120, e 64 das 76 linhas travadas eram fórmulas. Além disso itensped exige xPed e codigoAlt (sem os dois é 400), então nem dá para varrer um pedido por ela. Ver decisão 0021.

Contrato do dono ainda não publicado — dívida do Integrador

Este endpoint é do Integrador (XadmRetornoController, criado justamente para a reconciliação da PIED). Hoje o dono já publica o fato (raw/xadm-retorno.md, que cobre também o re-arme da §6), mas este repo ainda não o importou — só o xadm-ingest, o xadm-ingest-estoque e o integracao-remessa. Por isso aqui há link, não cópia: re-digitar o contrato de retorno recriaria exatamente o apodrecimento que a §1 acabou de matar.

A dívida é do dono, não deste repo (constituição §2): o certo é um PR no integracao/integrador publicando o fato do retorno, e então importá-lo aqui como os outros dois. Enquanto isso, a fonte é o código do dono — XadmRetornoController — e o cliente deste lado é o IntegradorClient.consultarRetornoContrato.

6. Re-arme do espelho — POST /api/v1/xadm/retorno/pedido/reenfileirar

Rota que o maxsul-pied chama no PushEnviador, logo depois da entrega confirmada (IntegradorClient.reenfileirarPedido), e só para pedido que carrega rejeição terminal (cod_retorno 1XX gravado).

Não é mais o clique do botão — e a razão é a ordem obrigatória abaixo

Até 2026-09 quem chamava era o botão Reimportar do painel. Mas o botão só enfileira: o PUT sai até 2 min depois, pelo relay. Re-armar no clique reabria a linha no espelho antes de o dado novo chegar — exatamente o que a ordem obrigatória proíbe. Movendo a chamada para depois do marcarEnviado, a ordem passa a valer por construção, e vale igual para o caminho automático (dado novo da PIED reenfileira sozinho — desde 2026-09-10 só o ERRO de envio; o ERRO_XADM se resolve à mão, revisão da decisão 0006). Ver decisão 0021.

Por que existe. O write-back (§4) grava cod_retorno nas linhas do espelho, e o cliente do X-Adm só coleta WHERE cod_retorno = '000' OR LIKE '9%'. Linha em 1XX (rejeição de dado) é invisível para ele para sempre. O Integrador não zera esse campo sozinho — é a regra de mão única: sem ela, um re-push de rotina apagaria o retorno de um pedido já gravado e duplicaria pedido no ERP. Esta rota é a exceção sob comando: quem pede é o operador, depois de corrigir o dado.

Ordem obrigatória: push (§1) primeiro, re-arme depois. São transações separadas — re-armar antes deixaria o cliente do ERP coletar a linha com o dado velho. Com esta ordem, quando a linha volta a ser coletável já está atualizada.

POST /api/v1/xadm/retorno/pedido/reenfileirar?xPed=<code>
Authorization: Bearer $INTEGRADOR_API_TOKEN

Resposta = contagem de linhas re-armadas por tabela; este lado usa o total:

{ "xPed": "260079421", "reArmadas": { "contratos": 1, "itensped": 3, "formulas": 2 }, "total": 6 }

Leitura tolerante de propósito (IntegradorClient.contarReArmadas): usa total quando vier, senão soma os números do mapa (reArmadas, tabelas ou raiz). Corpo sem número nenhum é contrato quebrado → erro, nunca "zero linhas" — o painel não pode dizer que reenviou sem ter reenviado.

Zero é resposta legítima (não havia linha em 1XX) e o painel a mostra como tal; falha de transporte vira flash de erro explícito.

Mesma dívida da §5 — e um pino a mais

Este endpoint também é do Integrador e também não está publicado como fato no raw/ dele, daí o contrato acima estar re-digitado (o que a §1 combate). Pior: ele nasceu depois deste cliente — o formato do envelope foi acordado por handoff, não importado. Enquanto o dono não publicar, a verdade é o código dele (XadmRetornoController) e a tolerância do contarReArmadas é o para-choque. O certo segue sendo o PR no integracao/integrador publicando §5 e §6.