Pular para conteúdo

Modelagem de dados — PIED e X-Adm

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

O modelo de dados consolidado da integração. São duas famílias de tabelas, no mesmo Postgres compartilhado, com donos distintos:

Família Dono (Flyway) Papel Sincroniza p/ o cliente?
Espelho do X-Adm (contratos, propriedades, fones, itensped, estoque) Integrador Estado desejado no formato do ERP — o alvo da integração ✅ sim (PowerSync)
PIED (pied_*) maxsul-pied Captura crua da PIED + normalização interna ❌ não (interno)

O maxsul-pied grava na família espelho pela API REST do Integrador (/api/v1/xadm), nunca por SQL direto (ver Projeto §2.2). A transformação de uma família na outra — de-para, regras de negócio e códigos de retorno — vive em Mapeamento e regras.


1. Família espelho do X-Adm (dono: Integrador)

Contrato autoritativo: documento interno MAXSUL2025_610110. Cinco entidades que reproduzem o modelo do ERP. Este schema é consolidado aqui e replicado no repositório do Integrador (que é o dono do Flyway).

Modelo único do X-Adm. Estas tabelas são as mesmas para todos os clientes do Integrador — não são específicas da Maxsul. Uma contratos (chave ChaveCP) serve compra e venda; cada fluxo preenche o subconjunto de colunas que usa. A integração da Maxsul usa as colunas de venda abaixo — algumas podem precisar entrar no molde compartilhado do Integrador, sem colidir com outros clientes.

Convenções (valem para as cinco tabelas)

  • Chave natural da fonte PIED (ex.: xPed, CodigoAlt, CgcCpf) + id UUID v7 surrogate (Postgres 18 uuidv7(), ordenável por tempo) + soft-delete (deleted BOOLEAN, deleted_at).

    Nota (identidade no X-Adm). xPed/CodigoAlt são códigos da PIED (transporte): servem de chave natural da entrada enquanto o X-Adm ainda não gerou a sua identidade (Chave* — ChaveCP/ChaveEst). No espelho do integrador eles são armazenados prefixados pied_* (rastreabilidade), e a identidade do produto é ChaveEst/CodProd — nunca o código do parceiro. O fio (JSON que este projeto posta) segue com CodigoAlt/xPed inalterado. Ver decisão 0013 do integrador.

  • Tipos ZIM → Postgres: VastInt(a-b dec) → NUMERIC; Date(8) (AAAAMMDD) → DATE; Char(n)/VarAlpha(n) → TEXT/VARCHAR.
  • Colunas preenchidas pelo lado X-Adm (chegam vazias, voltam pelo write-back, §5 do livro): Chave* (ChaveCP, ChaveProp, ChaveEst, ChaveFone, ChaveItem) e CodRetorno/MsgRetorno.
  • Colunas só do PowerSync (sequenciais gerados na gravação): id em propriedades e itensped (marcadas ⟳ abaixo).

1.1 contratos — cabeçalho do pedido

Campo Tipo Tam Conteúdo
xPed 🔑 Char 15 Código do pedido de venda no PIED.
CgcCpfCliente Char 14 CNPJ ou CPF do cliente.
ChaveUnid Char 5 Centro de custos do pedido.
CodMat Char 10 Chave do funcionário que cadastrou o pedido.
DtEm Date 8 Data de emissão.
DataInc Date 8 Data de inclusão.
HoraInc Char 4 Hora de inclusão.
VlTot VastInt 0-2 dec Valor do pedido.
NatOp Char 5 Filtro de contabilização (natureza de operação).
Compl Char 2 Complemento do filtro de contabilização.
TpVda Char 1 F = FOB, C = CIF.
ClassForma Char 2 Forma de pagamento.
Prazo Char 8 Data de entrega.
ChaveCP ⬅X-Adm Char 20 Chave unique do pedido no X-Adm (gerada na inclusão).
ChaveProp ⬅X-Adm Char 11 Chave unique do cliente no X-Adm.
CodRetorno ⬅X-Adm Char 3 Código de retorno do cadastro.
MsgRetorno ⬅X-Adm VarAlpha 128 Mensagem de retorno.

1.2 propriedades — cliente / endereço

Campo Tipo Tam Conteúdo
id ⟳🔑 VastInt — Sequencial unique (gerado no PowerSync).
CgcCpf Char 14 CNPJ ou CPF do cliente.
CodProp Char 2 Código do endereço do cliente.
NomeProp VarAlpha 50 Nome/razão social do cliente.
Fantasia VarAlpha 30 Nome fantasia.
Endereco VarAlpha 60 Endereço.
CEP Char 8 CEP.
Bairro VarAlpha 20 Bairro.
Cidade VarAlpha 30 Cidade (o X-Adm tem tabela de municípios).
Estado Char 3 Estado (gravar com um espaço antes, totalizando 3 chars).
Numero Char 6 Número do endereço.
InscEst Char 15 Inscrição Estadual ou RG.
ChaveProp ⬅X-Adm Char 11 Chave unique do cliente no X-Adm.
CodRetorno ⬅X-Adm Char 3 Código de retorno.
MsgRetorno ⬅X-Adm VarAlpha 128 Mensagem de retorno.

1.3 fones — contato do cliente

Campo Tipo Tam Conteúdo
CgcCpf 🔑 Char 14 CNPJ/CPF do cliente (no X-Adm relaciona por ChaveProp).
DDD Char 5 DDD do contato.
Telefone Char 10 Telefone do contato.
Nome VarAlpha 50 Nome do contato.
Email VarAlpha 100 E-mail do contato.
ChaveFone ⬅X-Adm Char 14 Chave unique do contato no X-Adm.
ChaveProp ⬅X-Adm Char 11 Chave unique do cliente no X-Adm.
CodRetorno ⬅X-Adm Char 3 Código de retorno.
MsgRetorno ⬅X-Adm VarAlpha 128 Mensagem de retorno.

Divergência decidida da doc-mãe (MAXSUL2025_610110): o fones agora carrega CodRetorno como as demais espelho — princípio universal (toda tabela espelho carrega seu código de retorno, para o GET /retorno/{tabela} responder uniforme, api-integrador §7). O livro original omitia CodRetorno em fones.

1.4 itensped — itens do pedido

Campo Tipo Tam Conteúdo
id ⟳🔑 VastInt — Sequencial unique (gerado no PowerSync).
xPed Char 15 Código do pedido no PIED.
CodigoAlt Char 15 Código do produto no PIED.
Qtde VastInt 0-3 dec Quantidade.
Valor VastInt 0-5 dec Preço unitário.
Total VastInt 0-2 dec Valor total do item.
TaxaFrete VastInt 0-5 dec Valor unitário do frete.
ChaveCp ⬅X-Adm Char 20 Chave unique do pedido no X-Adm.
ChaveEst ⬅X-Adm Char 11 Chave unique do produto no X-Adm.
ChaveItem ⬅X-Adm Char 27 Chave unique do item no X-Adm.
CodRetorno ⬅X-Adm Char 3 Código de retorno.
MsgRetorno ⬅X-Adm VarAlpha 128 Mensagem de retorno.

No caso Kit (§ Mapeamento), o item usa também DescCompl (Char) para classificar o gerador por potência — coluna auxiliar do transform, ver Mapeamento.

1.5 estoque — produto

Campo Tipo Tam Conteúdo
CodigoAlt 🔑 Char 15 Código do produto no PIED.
EAN13 Char 13 Código de barras.
CodProdAlt Char 8 Código do produto base a partir do qual o novo estoque é cadastrado.
NomeProd VarAlpha 128 Nome do produto.
Venda VastInt 0-5 dec Valor unitário de venda.
VendaPz VastInt 0-5 dec Valor unitário de venda a prazo.
Saldo VastInt 0-3 dec Saldo no X-Adm.
ChaveEst ⬅X-Adm Char 11 Chave unique do produto no X-Adm.
CodRetorno ⬅X-Adm Char 3 Código de retorno.
MsgRetorno ⬅X-Adm VarAlpha 128 Mensagem de retorno.

🔑 chave de negócio · ⟳ sequencial só do PowerSync · ⬅X-Adm preenchido pelo ERP (write-back).

1.6 Relacionamentos

Chave primária Chave estrangeira
propriedades.CgcCpf contratos.CgcCpfCliente
propriedades.CgcCpf fones.CgcCpf
propriedades.ChaveProp contratos.ChaveProp
propriedades.ChaveProp fones.ChaveProp
contratos.ChaveCP itensped.ChaveCP
contratos.xPed itensped.xPed
estoque.ChaveEst itensped.ChaveEst
estoque.CodigoAlt itensped.CodigoAlt
erDiagram
    PROPRIEDADES ||--o{ CONTRATOS : "CgcCpf / ChaveProp"
    PROPRIEDADES ||--o{ FONES : "CgcCpf / ChaveProp"
    CONTRATOS ||--o{ ITENSPED : "xPed / ChaveCP"
    ESTOQUE ||--o{ ITENSPED : "CodigoAlt / ChaveEst"

2. Família PIED (dono: maxsul-pied)

Tabelas internas do maxsul-pied, não sincronizadas. Dividem-se em landing (captura crua) e normalizadas (fase 2).

2.1 Landing — captura raw (fase 1, em produção)

Criadas pela migração Flyway V1__landing_pied.sql. Guardam o payload como chegou — o transform da fase 2 lê daqui. A granularidade é fiel e barata (a página/o POST inteiro, não o item explodido).

erDiagram
    PIED_WEBHOOK {
        uuid id PK "default uuidv7()"
        text evento "campo event do payload (nullable)"
        jsonb payload "corpo cru (NOT NULL)"
        jsonb headers "headers HTTP, credenciais mascaradas"
        timestamptz recebido_em "default now()"
    }
    PIED_REST {
        uuid id PK "default uuidv7()"
        text entidade "produtos | clientes | pedidos"
        int pagina "1-based"
        jsonb payload "envelope completo da página (NOT NULL)"
        int total_items "data.totalItems"
        timestamptz buscado_em "default now()"
    }
    PIED_CURSOR {
        text entidade PK "produtos | clientes | pedidos"
        date last_update_after "cursor do filtro incremental (só pedidos)"
        int ultima_pagina "checkpoint do backfill (0 = recomeça da pág 1)"
        timestamptz atualizado_em
    }
    PIED_CURSOR ||--o{ PIED_REST : "governa o delta de pedidos"
  • pied_webhook — uma linha por POST recebido. evento extraído do campo event quando presente (order.created, budget.updated…); corpo não-JSON é envelopado como string JSON (payload é NOT NULL, nada é rejeitado). headers guarda a requisição para diagnóstico, com X-Pied-Secret/Authorization mascarados.
  • pied_rest — uma linha por página buscada no poll; o payload é o envelope {error, data:{items, totalItems}} completo.
  • pied_cursor — estado do poll por entidade. last_update_after é a data usada como ?lastUpdateAfter na próxima rodada; só pedidos usam (produtos/clientes são sempre varridos por inteiro — a PIED ignora o filtro neles) e só passa a valer após o backfill completar. ultima_pagina é o checkpoint do backfill: como a cota horária da PIED (429) interrompe um scan grande no meio, guarda-se a última página capturada para retomar dali na próxima rodada em vez de re-escanear do início (V2; migração da premissa "cursor avança só ao completar", que gerava re-scan infinito — pendências P1, decisão do 429). Ao completar (página vazia), zera-se ultima_pagina e, em pedidos, liga-se o last_update_after incremental.
  • Índices: pied_webhook (recebido_em), pied_webhook (evento), pied_rest (entidade, buscado_em).

Nomenclatura (definida): as landing ficam como estão — pied_webhook, pied_rest, pied_cursor; a fase 2 apenas acrescenta as normalizadas (pied_produto/pied_cliente/pied_pedido), sem renomear.

Retenção (definida): o landing pied_* é mantido indefinidamente enquanto barato — é a trilha de auditoria e a fonte de reprocessamento da fase 2. Expurgo só se o volume medido no MVP exigir; nesse caso, arquivar/expurgar páginas antigas de pied_rest já processadas, preservando pied_webhook.

2.2 Normalizadas — negócio interno (materializadas na V3__normalizadas.sql)

O transform explode o raw em tabelas de negócio próprias, fonte estável e deduplicada do de-para. Criadas pela migração V3__normalizadas.sql (Postgres 18, uuidv7() surrogate + chave natural + soft-delete); o NormalizadorService faz upsert idempotente por chave natural a partir de pied_rest/pied_webhook. Schema exato: a migração é a fonte (não duplicado aqui).

Tabela Chave natural Origem Colunas típicas
pied_produto product_code poll equipments name, manufacturer, model, baseprice, payload
pied_cliente documento (CNPJ/CPF só dígitos) poll companies + company embutido no pedido company_name, fantasy_name, state_inscription (InscEst), payload
pied_pedido code poll requests/order + webhook order.* pied_id (→ xPed), documento_cliente, deal_status (+ _anterior), máquina de status, payment_status (+ _anterior), pago_em, parcial_desde, cancelado_pied_em, importacao_manual_obs/_em (V12, "Importado manualmente"), alerta_cod_retorno (V13, dedup do alerta de erro terminal), last_update, payload
  • pied_pedido é a estrutura central: carrega a máquina de status da entrega (CAPTURADO → NA_FILA → ENVIANDO → ENVIADO → CONFIRMADO | ERRO_XADM; ERRO = falha de push) e as colunas de retorno (cod_retorno/msg_retorno/chave_xadm, content_hash). Elas nascem na V3, mas só são dirigidas nas fatias de push/reconciliação (adiante na Fase 2). O alerta_cod_retorno guarda o último código 1XX já alertado por e-mail: a reconciliação só envia quando consegue gravar um código diferente, e o campo zera quando o pedido confirma ou é reenfileirado.
  • Colunas de pagamento (carimbos por transição no upsert): pago_em (→ received, gate de envio), parcial_desde (→ partial, aviso de parcial travado >5 dias) e cancelado_pied_em (set-once, CONFIRMADO → cancelled, dispara o e-mail de cancelamento pós-import). Ver decisão 0010.
  • pied_produto vem só do poll equipments; os products[] embutidos no pedido são consumidos inline pelo de-para, não materializados — evita sobrescrever os campos ricos do produto com o item esparso do pedido (o pedido cru fica em pied_pedido.payload).
  • Orçamento (budget) não vira tabela normalizada — não vai ao X-Adm.
  • Recência do evento (V11): o upsert de pied_pedido recusa payload cujo lastUpdate seja anterior ao já persistido — snapshot atrasado não reaplica um payment.status vencido nem dispara os carimbos de transição. Empate (>=) passa, porque é o enriquecimento rebuscando o mesmo snapshot pelo invoice. upsert devolve boolean e o alerta de cancelamento pós-import só sai quando a linha foi mesmo alterada. A comparação é no segundo cheio (date_trunc('second', …) dos dois lados): webhook e REST carimbam o mesmo instante com precisões diferentes (.512Z vs. segundo cheio), e comparar cru prendia o pedido em NA_FILA para sempre — ver 0018 A.1.
  • Cursor da normalização (pied_normalizacao_estado, V11): singleton id = 1 com o max(recebido_em/buscado_em) do raw já normalizado — a rodada lê só o raw novo (mais uma margem de 10 min, contra commit fora de ordem) em vez de reprocessar a história inteira a cada 5 min. Reprocesso completo = UPDATE pied_normalizacao_estado SET ate_ts = NULL. Motivação e trade-offs na decisão 0018.