Pular para conteúdo

0015 — Baixa de MDF-e por macro Sascar: espelho + watch + callback

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

Contexto. O X-Adm passou a emitir MDF-e e o negócio quer encerrá-la sozinha quando o motorista fecha uma macro na Sascar. Quem detecta a macro é o int-sascar; quem encerra a MDF-e no ERP é o integrador-client (ZIM). Faltava a ponte no Integrador: espelhar a MDF-e, dizer ao int-sascar quais vigiar, receber o encerramento e virar um comando que o client execute. O como está em baixa-mdfe-sascar; aqui ficam as escolhas e o preterido.

Decisão. Quatro capacidades aditivas ao espelho, sem quebrar o fluxo PIED.

  • Reusar PUT /api/v1/xadm (não rota nova) para o MDFE_PUT (Origem:PMDFE). Mesmo contrato de resposta (200 com resultado). Ingest tipado por tabela (como o resto): 7 tabelas novas (mdf/mdfcompl/nfeevento/mdfitens/nfmdf/nfcomplmdf/fretes), municipio/propriedades reusadas. Preterido: /api/v1/mdfe dedicado — duplicaria auth/erro/request sem ganho.
  • Comando de baixa em coluna própria na mdf, não tabela nova: situacao_baixa (ATIVO/BAIXA/ERRO) + cod_retorno/msg_retorno (contrato do V18) + proveniência (id_pacote/data_pacote/motivo). Preterido: tabela mdfe_comando (mais junção, sem ganho) e sobrecarregar NFEEVENTO (que fica fiel ao X-Adm, só leitura). O cod_retorno é o sinal que o ENCERRA_MDFE observa; a coluna própria dá visibilidade humana.
  • Watch de saída = @Client declarativo + @Retryable (padrão do poke 008), best-effort em virtual thread, idempotente. Corpo destilado (o int-sascar nunca vê JSON cru do ZIM): status já interpretado, destino resolvido por discriminante NFe(55)×CTe(57). status=encerrado, não DELETE: a chave_nota tem $/+ e não cabe em path. O encerrado só dispara na transição (marcador encerrado_em), independente da situacao_baixa — o watch aberto vigia a placa de toda MDF-e aberta, então todo encerramento precisa mandá-lo parar. Revisão 2026-07-31: o gate situacao=BAIXA caiu — a baixa comum é encerrar no X-Adm primeiro (fica ATIVO, sem macro Sascar), e aí basta parar de vigiar; receber encerrado sem baixa pendente não alarma o int-sascar (só remove do mdfe_vigiado).
  • Callback POST /api/sascar/encerramento (path literal, não /api/v1/). Sempre 2xx (o int-sascar é dono do retry): chave inexistente = no-op, não 404 (404 giraria o retry infinito); reenvio = no-op idempotente (só a transição ATIVO→BAIXA grava).
  • Auth do callback = Bearer único da casa. O outbound_token que o int-sascar envia é o INTEGRADOR_API_TOKEN (o Bearer de /api/**, via ApiBearerSecurityRule). Preterido: um 2º TokenValidator para um token separado — daria ROLE_API global ao token do int-sascar (rebaixaria a segurança de todas as rotas /api). Um Bearer, um validador.
  • Entrega PowerSync = recorte mínimo mdf no bucket global (sem bucket novo). A mdf carrega tudo que o ENCERRA_MDFE lê; o SchemaTabelas do client segue o espelho. Decisão do rollout (2026-07-30): não editar os config.yaml de cliente neste PR — só documentar o recorte; a edição por cliente é operação de rollout.

Consequências. MDF-e ativa aparece ATIVO, é vigiada, e — fechada a macro — vira BAIXA/000; o client executa e escreve 006; o re-PUT com NFEEVENTO(110112,A) dispara o watch encerrado. Chaves empacotadas do ZIM: literal na gravação, TRIM só na consulta (matching do callback e das junções depende disso). Segredos (SASCAR_INBOUND_TOKEN) só por ENV (§1.9).

Fora de escopo: detector de macro/registry (int-sascar); ENCERRA_MDFE no ZIM (integrador-client); geo/shadow; tela de baixas; espelhar MOTORISTAS/FRETESMOT.