Baixa de MDF-e por macro Sascar¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-30
O Integrador é a ponte entre quem detecta o fim da viagem (o int-sascar, via macro no rastreador Sascar) e quem encerra a MDF-e no ERP (o integrador-client, no ZIM). Ele espelha as tabelas de MDF-e do X-Adm, avisa o int-sascar quais MDF-e vigiar, recebe o callback de encerramento e grava um comando de baixa que o client executa e reporta de volta.
Tudo é aditivo ao ingest existente: mesmo endpoint PUT /api/v1/xadm, sem rota nova de entrada.
As escolhas de desenho e o preterido estão na decisão 0015.
Fluxo ponta a ponta¶
X-Adm ──MDFE_PUT──▶ Integrador ──watch aberto──▶ int-sascar
(espelho mdf) │
(motorista fecha macro)
│
Integrador ◀──POST /api/sascar/encerramento───────────┘
│ grava situacao_baixa=BAIXA, cod_retorno=000
▼
PowerSync ──▶ integrador-client (ZIM) executa ENCERRA_MDFE, escreve cod_retorno=006
│
▼ (write-back PowerSync)
X-Adm ──MDFE_PUT com NFEEVENTO(110112,A)──▶ Integrador ──watch encerrado──▶ int-sascar
Passo final (watch encerrado) é obrigatório e temporal: o int-sascar alarma se não vier em
~6h (o callback 2xx só marca enviado; o mdfe_vigiado só fecha ao receber o encerrado).
Ingest do MDFE_PUT¶
Origem:PMDFE. O CNPJ/Razao_Social do emitente vêm da raiz do payload (não são colunas da
mdf). O ingest é tipado por tabela (como o resto do XADM): cada tabela espelha por upsert de chave
literal do ZIM (TRIM só na consulta). Tabelas espelhadas: mdf, mdfcompl, nfeevento,
mdfitens, nfmdf, nfcomplmdf, fretes (municipio/propriedades reusadas). MOTORISTAS/
FRETESMOT não são espelhadas (fora de escopo).
Tabela ausente é tolerada: uma MDF-e sem NFEEVENTO(110112,'A') = aberta (situacao_baixa=ATIVO).
situacao_baixa (coluna própria do Integrador)¶
Na mdf, não vem do X-Adm. Valores:
| valor | significado |
|---|---|
ATIVO |
vigiada; sem baixa solicitada |
BAIXA |
encerramento solicitado (comando pendente) — cod_retorno=000 |
ERRO |
falha reportada pelo client |
Só a transição ATIVO→BAIXA grava (idempotência do callback). O NFEEVENTO do espelho é fiel ao
X-Adm e só leitura — o comando de baixa não o sobrecarrega. A confirmação do encerramento
(NFEEVENTO 110112/A) é marcada em encerrado_em (não re-empurra o watch encerrado — idempotência).
O watch encerrado dispara na transição independente da situacao_baixa: o fluxo comum é
baixar no X-Adm primeiro — a MDF-e é encerrada lá (fica ATIVO, sem passar pela macro Sascar) e o
único efeito necessário é o int-sascar parar de vigiar a placa. Como o watch aberto vigia a placa
de toda MDF-e aberta, todo encerramento (via Sascar ou direto no X-Adm) precisa avisá-lo a parar.
Tela /mdf (visualização)¶
A tela distingue dois eixos por MDF-e, em colunas próprias: Status — o documento no ERP,
ENCERRADA quando encerrado_em está setado, ABERTA caso contrário (derivado, sem coluna
nova) — e Situação baixa — o situacao_baixa (ATIVO/BAIXA/ERRO) do comando Sascar. Uma MDF-e
encerrada direto no X-Adm (sem macro) aparece ENCERRADA + Situação baixa=ATIVO — antes lia só
ATIVO e parecia aberta. Só apresentação (derivação inline na view JTE); o export XLSX segue
SELECT * (leva encerrado_em cru, não o rótulo). Ver .ia/011.
cod_retorno (mesmo contrato do V18)¶
000 pendente (callback) · 006 executado (write-back do client) · 1XX/9XX erro. É o sinal que
o fluxo ENCERRA_MDFE do integrador-client observa.
Watch de saída (Integrador → int-sascar)¶
POST {sascar.watch.url}/api/integrador/mdfe/watch, Bearer = inbound_token do cliente. Só dispara
quando o PUT tocou MDF-e; não empurra MDF-e já encerrada.
Entrega garantida (desde .ia/012 — decisão 0016). O watch deixou de ser best-effort: agora é enfileirado no outbox (
xadm-mensageria) — no caminho do ingest, dentro da transação do apply (rollback dropa a linha); no writeback powersync, pós-commit via oMdfeWatchListener. ORelayM2mda lib entrega (retry durável,PAUSADO+ alarme no teto); o operador reenvia/cancela em/admin/mensageria. O corpo destilado (abaixo) é salvo comopayload; oWatchEnviadorchama o@Client(transporte provisório até o contrato 006). Idempotente (o receptor deduplica porchave_nota).
Corpo destilado (o int-sascar nunca vê JSON cru do ZIM):
| campo | origem |
|---|---|
slug |
sascar.slug (slug canônico do cliente na central; o int-sascar dá 400 se divergir do inbound_token) |
chave_nota |
mdf.chave_nota (trimada) |
cnpj |
raiz do payload |
filial |
mdf.vinc_filial |
placa_cavalo |
mdfcompl.docto |
finalidade |
mdfitens.finalidade do 1º item (M se os itens divergirem) |
status |
aberto / encerrado |
destino_cidade / destino_uf |
discriminante NFe×CTe (abaixo) |
Destino por nfmdf.modelo: NFe (55) → nfcomplmdf.loc_entrega → propriedades.cod_mun →
municipio; CTe (57) → fretes.mun_dest → municipio.
Callback de encerramento (int-sascar → Integrador)¶
POST /api/sascar/encerramento (path literal, não /api/v1/). Auth pelo ApiBearerSecurityRule
(rota sob /api/**): o int-sascar envia o Bearer da casa — o outbound_token dele é o
INTEGRADOR_API_TOKEN deste app (a casa tem um Bearer único de /api). Bearer inválido → 401.
Corpo: chave_nota, cnpj, filial, placa, data_pacote (ISO-8601 local sem offset), id_pacote,
cidade, uf, motivo (macro|geo). Localiza a mdf por chave_nota (TRIM), grava
situacao_baixa=BAIXA + cod_retorno=000 + proveniência (id_pacote/data_pacote/motivo).
Sempre 2xx (o int-sascar é dono do retry e trata não-2xx como transitório):
| resultado | quando |
|---|---|
BAIXA |
comando gravado (transição ATIVO→BAIXA) |
NOOP |
idempotente — já em BAIXA/ERRO (reenvio não regride) |
NAO_ENCONTRADO |
chave não ingerida ainda (não 404 — 404 giraria o retry infinito) |
O callback recebido é auditado como ENTRADA/RECEBIDO no outbox (registrarEntrada, .ia/012) —
best-effort, não altera o sempre-2xx nem a idempotência de negócio; visível em /admin/mensageria.
Segredos e configuração (ENV)¶
Nunca versionados — só ENV no Coolify (constituição, Duas fontes da verdade):
| ENV | uso |
|---|---|
SASCAR_WATCH_ENABLED |
liga o watch (off por default) |
SASCAR_WATCH_URL |
base do int-sascar (ex. https://sascar.xadm.biz) |
SASCAR_INBOUND_TOKEN |
Bearer que o Integrador envia no watch (segredo) |
SASCAR_SLUG |
slug canônico do cliente (fallback CLIENTE) |
INTEGRADOR_API_TOKEN |
Bearer que o int-sascar envia no callback (= Bearer da casa /api) |
PowerSync do cliente (entrega do comando)¶
O comando de baixa chega ao integrador-client pelo canal PowerSync existente (bucket global, sem
bucket novo). Recorte mínimo: só a mdf — ela carrega chave_nota, vinc_filial,
situacao_baixa, cod_retorno, msg_retorno, tudo que o ENCERRA_MDFE lê para executar e reportar.
O SchemaTabelas do client segue este recorte (o espelho define o client).
No config.yaml do cliente, sob sync_rules.global.data:
- SELECT * FROM mdf WHERE deleted = false
Não sincronizar as demais tabelas MDF-e (contexto interno do espelho), MOTORISTAS/FRETESMOT,
nem criar bucket novo. A publicação Postgres powersync é FOR ALL TABLES no caminho padrão (a mdf
entra sozinha); se o cliente usa FOR TABLE ..., rodar ALTER PUBLICATION powersync ADD TABLE mdf;.
O write-back do cod_retorno/msg_retorno sobe pelo POST /api/v1/powersync já existente (sem canal
novo; client_auth inalterado). Ver atualizar-syncrules.
Cobertura de teste¶
Os 3 JSON reais (docs/anexos/privado/MDFE_PUT_2026072*.json) cobrem só o estado ATIVO
(16433017 sem NFEEVENTO; 17053300/17294274 com NFEEVENTO(110112,'I')). O ramo encerrado/
reconciliação exige o fixture sintético MDFE_PUT_SINTETICO-encerrado.json (SitEvento I→A).
Integração em Postgres real: MdfeEspelhoIngestIntegrationTest; destilação: MdfeWatchDistillerTest.