Qualidade, recall e doação

Transições de lote (quarentena, bloqueio, recall), campanha de impacto e doação com baixa rastreável — o que a flag liga e o que já funciona só com leitura.

Status de lote que não atendem ATP/FEFO/OP/venda: QUARANTINE, EXPIRED, BLOCKED, RECALLED. Só ACTIVE (e validade nula ou ≥ hoje) sai.

Qualidade

Não altere status com PATCH /product-lot/:id. O PATCH recusa. Use:

AçãoRotaBody
QuarentenaPOST /api/product-lot/:id/quarantine{ "reason": "mínimo 5 caracteres" }
LiberarPOST /api/product-lot/:id/releaseidem
BloquearPOST /api/product-lot/:id/blockidem

Kanban: /apps/stock/lots/qa e GET /api/product-lot/{filialId}/qa.

Motivo é obrigatório (≥ 5). Fica auditoria no lote (quarantine_reason, block_reason, released_at, ator).

Inbound automático só com industrial.quality.inbound-quarantine. Sem a flag, a NF cria ACTIVE; o QA é um passo explícito.

RBAC documentado: quality.lot.release / quality.lot.block. O controller hoje autentica JWT + menu de produtos — o nome da permissão ainda não é gate.

Recall

A simulação não precisa de flag:

GET /api/lot-recall/impact/{lotId}

Resposta: saldo, depósitos, lotes da genealogia (PA/MP), OPs, clientes, NFs de saída, transferências, devoluções, doações (CFOP 5910/6910).

Abrir campanha precisa de industrial.recall.enabled:

POST /api/lot-recall/{filialId}
x-company-id: <empresa>

{
  "lotId": "<lote semente, ex. farinha>",
  "reason": "Recall farinha FAR-260828-A",
  "notes": "opcional"
}

Efeito: campanha OPEN (REC-######), snapshot, lotes da cadeia em RECALLED. Saem do ATP. Exemplo HML: REC-000001 no lote da farinha congelou o pão.

POST /api/lot-recall/{id}/close
{ "reason": "Campanha encerrada — lote segue bloqueado" }

Encerrar não volta o lote para ACTIVE. Cada lote precisa de release com motivo.

Telas: /apps/stock/recall e /apps/stock/recall/:id. Não confundir com a fila Health.

Devolução de venda pode gravar lot_id em FAT071. Expedição FATEX01.lot_id ainda não é writer industrial.

Doação

Listagem funciona com a flag OFF. Postar exige industrial.donation.enabled.

POST /api/lot-donation/{filialId}
x-company-id: <empresa>

{
  "reason": "Doação padaria comunitária",
  "recipientName": "Padaria Comunitária",
  "recipientDocument": "opcional",
  "recipientUf": "GO",
  "emitFiscal": false,
  "items": [
    { "lotId": "<lote PA>", "quantity": 250, "locationCode": "001" }
  ]
}
CampoRegra
reason≥ 5 caracteres
recipientName≥ 3
itemspelo menos um; quantity > 0
emitFiscalse true, NF de bonificação CFOP 5910/6910 (BONIFICACAO)
cfopopcional; default do tipo de movimento de bonificação

Kardex: origem DONATION, documento DON-######. Sem FAAJ01. Contábil: D CMV / C estoque.

Cancelar: POST /api/lot-donation/:id/cancel { "reason": "…" }.

Telas: /apps/stock/donations.

Rastreio

GET /api/product-lot/trace/{lotId}/backward
GET /api/product-lot/trace/{lotId}/forward

Tela /apps/stock/lots/:id: origem (NF, OP, fornecedor) e destino (OP, PA, NF, cliente). Kardex paginado no mesmo lote.

Volta típica: cliente → NF de saída → lote PA → OP → lote MP → NF de entrada / fornecedor.

Recusas que devem aparecer

TentativaHTTPMensagem típica
Saída de lote RECALLED / quarentena400estoque insuficiente nos lotes / lote não permitido
POST recall com flag OFF400industrial.recall.enabled
POST doação com flag OFF400industrial.donation.enabled
POST recall lote inexistente404lote não encontrado
PATCH de status400use /quarantine, /release ou /block
Motivo curto400mínimo 5 caracteres