Esteiras e processos

central.process.json declara workflow, transições, bloqueios por etapa, bindings cross-model, proteção de exclusão e invariantes atômicas — executados pelo backend.

O arquivo opcional backend/central.process.json declara capacidades de processo que pertencem ao Core, não à implementação local. Ele é carregado depois do domínio: toda model e campo citados precisam existir em central.domain.json.

O runtime não executa JavaScript, eval, nomes de funções ou módulos informados no JSON. Expressões usam a mesma AST fechada das regras de domínio.

Contrato mínimo

{
  "schemaVersion": 1,
  "models": {
    "Pagamento": {
      "workflow": {},
      "bindings": [],
      "deleteProtection": [],
      "atomicInvariants": []
    }
  }
}

Exemplo: esteira de tickets

{
  "schemaVersion": 1, "name": "Exemplo de tickets",
  "models": [{
    "name": "Ticket", "singular": "ticket", "basePath": "/tickets",
    "crud": { "enabled": true, "permissions": { "read": "tickets.read", "write": "tickets.write" } },
    "fields": {
      "titulo": { "kind": "string", "required": true },
      "etapa": { "kind": "enum", "values": ["Aberto", "Concluido"], "default": "Aberto" }
    }
  }]
}
{
  "schemaVersion": 1,
  "models": {
    "Ticket": { "workflow": {
      "stageField": "etapa", "initialStages": ["Aberto"], "defaultStage": "Aberto",
      "transitions": [{ "from": "Aberto", "to": "Concluido" }]
    } }
  }
}

Mover de Aberto para Concluído funciona; voltar para Aberto é recusado pelo backend — mesmo por PATCH direto. A UI correspondente está em Esteiras (CorePipeline).

Workflow e transições

{
  "workflow": {
    "stageField": "etapa",
    "initialStages": ["Solicitado"],
    "defaultStage": "Solicitado",
    "transitions": [
      { "from": "Solicitado", "to": "Aprovado" },
      {
        "from": "Aprovado",
        "to": "Aguardando NF",
        "when": { "op": "eq", "args": [{ "field": "aprovadoFinanceiro" }, { "value": true }] }
      }
    ],
    "lockedFieldsByStage": {
      "Enviado para pagamento": ["valor", "projetoId", "projetoItemId"],
      "Pagamento Ok": ["valor", "projetoId", "projetoItemId"]
    },
    "lockedMessage": "Os dados de negócio ficam bloqueados nesta etapa.",
    "onEnter": [
      { "stage": "Enviado para pagamento", "set": { "statusTrabalho": "Trabalhando", "statusPagamento": "Pendente" } }
    ],
    "automaticTransitions": [
      {
        "when": { "op": "eq", "args": [{ "field": "pagamentoLiquidado" }, { "value": true }] },
        "to": "Pagamento Ok",
        "set": { "statusTrabalho": "Trabalhando" }
      }
    ]
  }
}
  • O backend compara o valor anterior e o novo; reenviar a mesma etapa não cria transição.
  • Uma mudança manual precisa existir em transitions e satisfazer when.
  • lockedFieldsByStage é aplicado sobre a etapa anterior e impede alterações mesmo por PUT/PATCH diretos.
  • onEnter produz alterações confiáveis do Core.
  • automaticTransitions roda no servidor depois dos bindings, sem depender do frontend.

A UI pode declarar botões transition e setField, mas é apenas uma projeção: a autoridade é o backend.

Bindings cross-model

Bindings preenchem campos derivados a partir de outra model ou de registros relacionados, resolvidos em lote e avaliados na ordem declarada — uma expressão pode consumir lookups e agregações anteriores.

Lookup

{
  "field": "percentualFeeAplicado",
  "kind": "lookup",
  "sourceModel": "Projeto",
  "localField": "projetoId",
  "sourceField": "percentualFee",
  "watchFields": ["percentualFee"],
  "recalculate": "async",
  "default": 0
}

Quando Projeto.percentualFee muda, o Core encontra os dependentes e agenda um recálculo assíncrono com uma leitura por binding e um bulkWrite — em vez de save() item a item na requisição do usuário.

Agregação

{
  "field": "pagamentoTotalPlanejado",
  "kind": "aggregate",
  "sourceModel": "Pagamento",
  "foreignField": "projetoItemId",
  "operator": "sum",
  "sourceField": "valor",
  "match": { "canceladoNaCentral": { "neq": true } },
  "default": 0
}

Operadores: sum, count, min, max. Filtros aceitam igualdade direta ou { "eq" }, { "neq" }, { "in": [] } e { "nin": [] }.

Expressão derivada

{
  "field": "pagamentoValorPendente",
  "kind": "expression",
  "precision": 2,
  "expression": {
    "op": "max",
    "args": [{ "value": 0 }, { "op": "subtract", "args": [{ "field": "contratacaoTotal" }, { "field": "pagamentoTotalPago" }] }]
  }
}

Além dos operadores do domínio, processos aceitam if (condição, verdadeiro, falso), concat e formatCurrency (valor, moeda e locale opcionais).

Recálculo imediato e assíncrono

recalculate Comportamento Use quando
immediate (padrão) Dependentes atualizados no mesmo ciclo da mutação O pai precisa refletir a alteração antes da resposta
async Recálculo em fila persistida, em lote Um pai com muitos filhos (ex.: percentuais de um Projeto)

A fila async é persistida em oon_process_jobs, com lease de 60 s e retomada por outra réplica, trava por alvo, deduplicação e até 5 tentativas com backoff. drainProcessJobs() existe para testes determinísticos.

Proteção de exclusão

{
  "deleteProtection": [
    { "sourceModel": "Pagamento", "foreignField": "projetoItemId", "message": "Não é possível excluir o item porque existem pagamentos vinculados." }
  ]
}

A verificação ocorre no CRUD oficial antes da exclusão — sem sobrescrever métodos do Mongoose.

Invariante financeira atômica

{
  "atomicInvariants": [
    {
      "name": "pagamentos-limitados-ao-contratado",
      "kind": "relatedSumLteParentField",
      "parentModel": "ProjetoItem",
      "parentLocalField": "projetoItemId",
      "sourceField": "valor",
      "parentField": "contratacaoTotal",
      "match": { "canceladoNaCentral": { "neq": true } },
      "tolerance": 0.01,
      "code": "PAGAMENTO_ACIMA_CONTRATADO",
      "message": "A soma {total} não pode ultrapassar o valor contratado {limit}."
    }
  ]
}

O Core executa a mutação em transação MongoDB e incrementa uma versão interna no pai antes de somar. Duas inclusões simultâneas disputam a mesma escrita: uma conclui, a outra recebe conflito transitório, é repetida, refaz a soma e é aceita ou rejeitada.

Alterações reais

O contexto de defineValidation inclui changedFields: somente campos cujo valor final difere do anterior, incluindo derivados pelo Core. Isso evita efeitos colaterais disparados por round-trips sem alteração real.

Fronteira recomendada

Pertence ao Core (process manifest) Permanece na Central
Transições e bloqueios de etapa Fórmula comercial específica
Status operacionais recorrentes Tipos de responsáveis do negócio
Dependências e recálculos cross-model Regras fiscais
Agregações relacionadas Integrações e mapeamentos
Proteção de exclusão Mensagens e condições próprias
Invariantes concorrentes, lote/assíncrono —

APIs públicas

const {
  validateProcessManifest, registerProcessManifest, loadProcessManifest,
  prepareProcessMutation, resolveProcessBindings, assertReferencePolicies,
  assertAtomicInvariants, assertDeleteAllowed, recalculateDependents,
  drainProcessJobs, ProcessManifestError,
} = require("@oondemand/oon-core-back");