# 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](/docs/backend/formulas-e-validacoes). ## Contrato mínimo ```json title="backend/central.process.json" { "schemaVersion": 1, "models": { "Pagamento": { "workflow": {}, "bindings": [], "deleteProtection": [], "atomicInvariants": [] } } } ``` ## Exemplo: esteira de tickets ```json title="backend/central.domain.json" { "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" } } }] } ``` ```json title="backend/central.process.json" { "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)](/docs/frontend/esteiras). ## Workflow e transições ```json { "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 ```json { "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 ```json { "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 ```json { "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 ```json { "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 ```json { "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. > [!WARNING] > Transações exigem MongoDB em **replica set**. Uma validação `find + sum + save` fora de transação não oferece essa garantia. ## 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 ```javascript const { validateProcessManifest, registerProcessManifest, loadProcessManifest, prepareProcessMutation, resolveProcessBindings, assertReferencePolicies, assertAtomicInvariants, assertDeleteAllowed, recalculateDependents, drainProcessJobs, ProcessManifestError, } = require("@oondemand/oon-core-back"); ``` Fonte: https://sdk.oonapps.com/docs/backend/esteiras-e-processos