Skip to content
ERP-Bem-ComumPublic

About

Modular Monolith do ERP Bem Comum (Fase 1: módulo Contratos). Node 24 + TS 6 + ESM/NodeNext + Drizzle/MySQL. CLI primária em PT-BR. Domínio puro funcional (zero throw, zero class, Result<T,E>, branded types). Source of truth: handbook/.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1,872 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

core-api — ERP Bem Comum

Backend do ERP Bem Comum, modelado como modular monolith. Vários módulos de negócio coabitam o mesmo processo Node, isolados por pasta + prefixo de tabela (ctr_*, fin_*, …) e comunicando-se por eventos via Outbox (ADR-0014/0015). Cada módulo é desenhado para poder ser extraído como serviço independente no futuro, sem refactor traumático.

Stack: Node.js 24 LTS · TypeScript 6.0 (roadmap TS 7 / compilador nativo — ADR-0009) · ESM ("type": "module", NodeNext) · pnpm 11 · Drizzle ORM 0.45 + mysql2 3.22 (MySQL 8.4 — ADR-0020) · borda HTTP Fastify 5 com Zod contract-first + OpenAPI 3.1 (ADR-0025/0027) · storage S3/MinIO (ADR-0019) · auth próprio com JWT ES256 via jose (ADR-0024).

Source of Truth: handbook/ (handbook/architecture/adr/ vence tudo). Contexto canônico em CLAUDE.md. Orquestrador, agentes e skills em ./.claude/.

Regras invariantes: sempre pnpm, nunca npm (ADR-0012) · borda HTTP é a UX primária; a CLI embutida foi retirada (ADR-0037) — validação E2E é feita em Hurl (ADR-0074; substituiu o Bruno em 2026-10-06).


🧩 Módulos

Os Bounded Contexts vivem sob src/modules/, cada um com a mesma anatomia (domain/ → application/ → adapters/ → public-api/). A comunicação cross-módulo passa exclusivamente por public-api/ + eventos no Outbox (ADR-0006/0014).

Módulo Responsabilidade Borda HTTP
auth Identidade própria + RBAC por permissão, login/refresh (JWT ES256), usuários, papéis, reset de senha, foto de perfil — ADR-0024. /api/v2/auth, /api/v1/{users,me,approvers}
contracts Gestão de Contratos — agregados Contract/Amendment/Document, ciclo de vida (pending/active/cancelled — ADR-0023/0039), export CSV. Módulo inaugural. /api/v2/contracts
financial Financeiro — títulos/payables, baixa manual, conciliação bancária, remessa CNAB240, extrato/timeline, read-model de fornecedor (ADR-0045). /api/v2/financial
partners Registry de parceiros — colaboradores, fornecedores, financiadores, geografia/território (soft-delete ADR-0035), ACT (ADR-0036) — ADR-0031. /api/v1/{collaborators,suppliers,financiers,…}
programs Gestão de programas + logo storage S3/MinIO — spec 008, ADR-0033. /api/v1/programs
notifications E-mail transacional — templates + EmailSender (Nodemailer/Resend), consumidor de eventos de domínio (ADR-0010/0047). worker email-dispatch
budget-plans Planos orçamentários — plano, resultado realizado e estrutura de custo; taxonomia do planejável (ADR-0048/0051). /api/v2/budget-plans
reports Leitura agregada sobre o que os outros módulos escreveram. Não tem domain/ — é consulta, não modelo, e por isso não tem regra própria a proteger. /api/v2/reports

🏗️ Estrutura

Identificadores em EN (regra invariante de CLAUDE.md §"Idioma"). Strings ao humano e mensagens de erro formatadas em PT.

src/
├── server.ts                            Entrypoint HTTP — compõe os plugins de cada módulo, graceful shutdown
│
├── shared/                              Cross-módulo, puro (sem regra de negócio)
│   ├── primitives/                      Result<T,E>, Brand<T,Tag>, exhaustive, immutable
│   ├── kernel/                          VOs de domínio compartilhados: cpf, cnpj, money, period, plain-date, user-ref
│   ├── ports/  adapters/                Cross-cutting (Clock + SystemClock/FixedClock)
│   ├── http/                            Shell da borda: app.ts, config.ts, errors.ts, reply.ts (ADR-0028)
│   ├── outbox/                          Worker genérico de Outbox + tipos (ADR-0015)
│   ├── observability/                   correlation id
│   ├── runtime/                         last-resort handlers (uncaught/unhandled)
│   └── utils/                           csv, date, hash, id, string
│
├── modules/<módulo>/                    Bounded Context (auth · contracts · financial · partners · programs · notifications)
│   ├── domain/                          PURO — Result<T,E>, branded, Readonly, sem infra
│   ├── application/
│   │   ├── ports/                       type contracts (Repository, EventBus, Storage, …)
│   │   └── use-cases/                   factory functions (deps) => (input) => Promise<Result>
│   ├── adapters/                        Implementações concretas
│   │   ├── http/                        plugin Fastify, controllers/DTOs, schemas Zod (composition.ts)
│   │   ├── persistence/                 Drizzle + mysql2 (schemas, mappers c/ Result, repos, drivers, migrations)
│   │   ├── outbox/                      append transacional de eventos
│   │   └── storage/, export/, …         conforme o módulo
│   └── public-api/                      Fronteira cross-módulo: http.ts, events.ts, index.ts, permissions.ts, read.ts, migrate.ts
│
├── workers/                             Composition roots de processos longos (ADR-0022/0041)
│   ├── supplier-view-projection/        fin_supplier_view ← eventos do partners (ADR-0045)
│   ├── contract-count-projection/       par_contract_count_view ← eventos do contracts (ADR-0046)
│   └── email-dispatch/                  notifications consome eventos de domínio → EmailSender (ADR-0047)
│   (+ relays de outbox por módulo: contracts/worker, partners/worker)
│
└── jobs/                                Oneshot jobs (ADR-0041)
    ├── contracts/sweeper/               varredura de ciclo de vida de contratos por tempo
    ├── financial/supplier-view-backfill/  backfill do read-model de fornecedor
    └── migrate/                         aplica migrations Drizzle

tests/                                   modules (mirror de src/) · cleanup (invariantes estruturais) · e2e · etl · infra · workers · jobs …
db/drizzle/                              configs do drizzle-kit por módulo (contracts, auth, partners, programs, financial, notifications)
scripts/                                 ci · claude · e2e (smoke HTTP) · etl · financial · handbook · seed · setup

handbook/                                Source of Truth — interno ao repo
├── architecture/adr/                    ADRs aceitos (IMUTÁVEIS)
├── reference/<tech>/                    typescript · nodejs · drizzle · mysql · mysql2 · docker · pnpm · fastify
│                                        · fastify-plugins · nodemailer · zod · bruno · magalu-cloud · claude-code
├── domain_questions/, inquiries/, interviews/, reviews/, specs/, runbooks/, …

llms.txt                                 Mapa do acervo em markdown plano, para leitura por IA
.claude/
├── agents/                              Especialistas por tecnologia
├── skills/                              Técnicas e disciplinas aplicadas
├── rules/                               Regras path-scoped (carregam quando o path casa)
├── agent-memory/                        Memória entre sessões
└── hooks/                               pre-commit-typecheck.sh, block-npm.sh, prettier-write.sh, …

Imports cross-pasta

package.json#imports declara subpath imports nativos (Node, sem transpiler):

"imports": {
  "#src/*": "./src/*",
  "#scripts/*": "./scripts/*"
}

Imports relativos e via #src/* carregam a extensão .ts (requisito de NodeNext + allowImportingTsExtensions):

import { Money } from '#src/shared/kernel/money.ts';

🌐 Borda HTTP

A UX primária é HTTP (ADR-0037). src/server.ts compõe um plugin Fastify por módulo e expõe a API versionada (ADR-0033):

  • /api/v2/… — modelo greenfield (auth, contracts, financial): plugin direto.
  • /api/v1/… — espelho do legado (partners, programs, gestão de usuários/acessos): superfície compatível com o sistema antigo durante a estratégia strangler-fig (ADR-0001).

Características da borda:

  • Contract-first com Zod 4 + OpenAPI 3.1 (fastify-zod-openapi / zod-openapi, ADR-0027): o schema valida a entrada e gera o contrato. Swagger UI via @fastify/swagger/@fastify/swagger-ui.
  • Hardening com @fastify/helmet, @fastify/cors, @fastify/rate-limit (limite dedicado para login/refresh).
  • Auth/RBAC cross-módulo: requireAuth + authorize/hasPermission exportados pelo auth protegem as rotas dos demais módulos.
  • Read/Write split (ADR-0026): cada módulo aceita *_DATABASE_URL (writer) e *_READER_URL (réplica; ausente → reusa o writer).
  • Drivers memory | mysql por módulo via env (<MÓDULO>_DRIVER), resolvidos por uma única guarda no boot (src/shared/persistence/module-driver-config.ts, #456). Em produção, driver ausente/inválido ou mysql sem *_DATABASE_URL derruba o boot com exit 78 (EX_CONFIG), listando todos os problemas de uma vez — nunca degrada em silêncio (foi o que deixou #374 e #444 mudos). Fora de produção, degrada para in-memory com aviso por módulo. memory explícito é sempre aceito; em produção, avisa.

🚀 Scripts

Sempre pnpm, nunca npm (ADR-0012). Versão pinada via packageManager + corepack; há hook PreToolUse(Bash) que bloqueia npm.

pnpm install                           # respeita pnpm-lock.yaml
pnpm install --frozen-lockfile         # em CI

# Gate de qualidade — os quatro, nesta ordem
pnpm run typecheck                     # tsc --noEmit (TypeScript 7, strict completo)
pnpm run format:check                  # prettier --check .
pnpm run lint                          # oxlint (.oxlintrc.json, type-aware via oxlint-tsgolint)
pnpm test                              # tests/**/*.test.ts via node:test (executa .ts direto)

# Servidor + processos de background
pnpm run serve                         # sobe a borda HTTP (node src/server.ts); config via env
pnpm run dev                           # overmind start — HTTP + workers juntos (Procfile)
pnpm run job:migrate                   # aplica migrations
pnpm run job:contracts:sweep           # oneshot: varredura de ciclo de vida de contratos

# ⚠️ DEBUG de um worker isolado — NÃO é a stack. São entrypoints anteriores à consolidação
# #407, e não existe `worker:projections` nem `worker:van`: quem os usa como stack sobe
# topologia diferente da de produção, sem worker da VAN. Ver a nota após este bloco.
pnpm run worker:outbox                 # relay do outbox de contracts (idem :partners)
pnpm run worker:email-dispatch         # consumidor de e-mail (notifications)
pnpm run worker:supplier-projection    # projeção fin_supplier_view

# Testes de integração (sobem MySQL/S3 via Docker compose --wait) e smoke E2E HTTP
pnpm run test:integration:financial    # idem :contracts :auth :partners :programs :notifications :storage :etl …
pnpm run test:e2e:auth                 # smoke bash contra server real (idem :contracts :collaborators)

# Suíte da borda em Hurl (ADR-0074) — NÃO versionada (api-collections-hurl/ está no .gitignore),
# não é gate, e um clone novo não a tem. Contrato em .claude/rules/api-collections.md
hurl --test --jobs 1 --variables-file api-collections-hurl/_vars.env api-collections-hurl/casos/**/*.hurl

# Migrations (Drizzle Kit) — uma config por módulo em db/drizzle/
pnpm run db:generate                   # contracts  (idem :auth :partners :programs :financial :notifications)

# Secrets locais p/ docker-compose
pnpm run secrets:setup                 # gera ./secrets/*.txt

# Diário de bordo das sessões (quem caiu sem emitir SessionEnd)
pnpm run logbook                       # idem --dead

Detalhes completos: CLAUDE.md §Gotchas que não se descobre lendo o código.

⚠️ pnpm run dev exige o overmind instalado (brew install overmind) — ele não é dependência do projeto e não há gate que o cobre. Sem ele o comando falha com command not found, sem pista de origem.

O Procfile sobe http e os quatro grupos de worker.

⚠️ VAN_S3_REGION e VAN_S3_BUCKET não são opcionais, e não são só do grupo van: sem elas o http também não sobe. buildVanStorage (src/modules/financial/adapters/http/composition.ts:782) faz throw quando a config não resolve, e é chamado de forma eager na composição do financial sob FINANCIAL_DRIVER=mysql (:949) — fail-fast deliberado do ADR-0068. O manifesto declara as duas no env obrigatório da unidade backend (src/deploy/workloads.ts:232-233). Logo não existe subconjunto que dispense as VAN_S3_* — nem via OVERMIND_PROCESSES, nem via pnpm run serve.

⚠️ Um processo que morre leva os outros. O overmind interrompe o grupo inteiro quando um processo sai, a menos que ele esteja em OVERMIND_CAN_DIE. Em config incompleta, isso aparece como "o pnpm run dev não sobe", sem apontar qual processo falhou primeiro — ler o stderr do que morreu antes de culpar os demais.

⚠️ A porta é a do seu .env — medido em 14/10/2026: com PORT=3000 declarada, overmind start subiu a borda em 127.0.0.1:3000, e GET /health respondeu 200. O overmind atribui porta por processo apenas quando a variável não está declarada; o .env.example gerado a declara, então esse caminho não aparece no uso normal. overmind status continua sendo a forma de conferir.

Sem o overmind, a mesma topologia se reproduz à mão — um terminal por grupo, porque nenhum script pnpm invoca o runner. A ordem importa: o manifesto declara dependsOn: ['migrate', 'sync-permissions'] para o backend (src/deploy/workloads.ts:211), e subir a borda sem semear o catálogo devolve 403 mudo em permissão nova (#462):

pnpm run job:migrate                                                # 1. schema
node --enable-source-maps src/jobs/auth/sync-permissions/run.ts     # 2. catálogo de permissões
pnpm run serve                                                      # 3. a borda HTTP
WORKER_GROUP=outbox      node --enable-source-maps src/workers/runner/run.ts
WORKER_GROUP=projections node --enable-source-maps src/workers/runner/run.ts
WORKER_GROUP=email       node --enable-source-maps src/workers/runner/run.ts
WORKER_GROUP=van         node --enable-source-maps src/workers/runner/run.ts

⚠️ Os pnpm run worker:* não substituem isso: são os seis entrypoints standalone anteriores à consolidação #407, mantidos para debug de um worker isolado (ver .claude/rules/jobs-and-workers.md). Não existe worker:projections nem worker:van, então quem os usa como stack sobe uma topologia diferente da que roda em produção — e sem nenhum worker da VAN.


🐳 Ambiente local (Docker Compose)

compose.yaml sobe o stack local com hardening (read_only, cap_drop, security_opt), e só vale para ambiente local — homologação e produção são operadas por terceiro e provisionadas por taskdef, com as variáveis de ambiente postas à mão na console da AWS (ADR-0068:28). ⚠️ Se existe gerador de taskdef, ele não está nesta árvore e ninguém verificou onde vive — ver inquiry-0038 §4.

⚠️ Todo serviço de aplicação está atrás de um profile, então docker compose up sobe só a infra — sem erro algum, o que é o modo de falha fácil de confundir com "subiu".

  • Infra (sem profile, sobe sempre): MySQL 8.4 e MinIO (storage S3-compat dev, ADR-0019). Mailpit (captura de e-mail dev) exige --profile mail.
  • Aplicação: o serviço http (--profile app) e os workers por grupo — worker-outbox, worker-projections, worker-email (--profile workers) e worker-van (--profile van).
  • One-shot: migrate e sync-permissions (em app, workers e jobs, porque os três cenários dependem deles); contracts-sweeper e supplier-view-backfill (--profile jobs).

⚠️ Um worker por grupo, não um por responsabilidade: os processos sobem src/workers/runner/run.ts com WORKER_GROUP, consolidados na #407. O Procfile usado por pnpm run dev segue a mesma topologia. Variantes: compose.ci.yaml (CI, só remove o expose de portas) e compose.etl.yaml (ETL).


🌊 Como contribuir

Trabalho novo não abre ticket de processo: faz a mudança, roda o gate (typecheck + format:check + lint + test), commita. Decisão nova vira ADR; achado fora de escopo vira issue. Não existe pipeline W0→W3, STATE.json nem wave — foram removidos em 2026-08-06.

Achado fora do escopo do ticket atual? Não conserte na hora (scope-creep): registre via skill issue-report (ADR-0040).


🤖 Painel de agentes especialistas

Cada agente é ancorado num subdir de handbook/reference/ + ADRs vinculantes, invocado pelo contratos-orchestrator — um agente ou uma skill por turno.

Agente Tecnologia Status
contratos-orchestrator Roteamento (agente ou skill) ✅ ativo
typescript-language-expert TypeScript 6 / type system ✅ ativo
nodejs-runtime-expert Node 24 / ESM / node:test ✅ ativo
drizzle-orm-expert Drizzle ORM + Drizzle Kit ✅ ativo
mysql-database-expert MySQL 8.4 (SQL, índices, locks) ✅ ativo
mysql2-driver-expert Driver mysql2 (pool, auth, TLS) ✅ ativo
docker-compose-expert Docker / Compose / BuildKit ✅ ativo
pnpm-workspace-expert pnpm 11 / supply-chain ✅ ativo
fastify-server-expert Fastify 5 + plugins ✅ ativo (ADR-0025)
zod-expert Zod 4 (schemas de borda, ADR-0027) ✅ ativo
nodemailer-email-expert Nodemailer SMTP adapter ✅ ativo
security-backend-expert Segurança backend (Node/TS/Fastify) ✅ ativo
security-frontend-expert Segurança frontend (TanStack/React) ✅ ativo

As skills cobrem disciplinas aplicadas — domínio, ports & adapters, schema, testes, revisão, segurança, git local, registro de achado — em .claude/skills/, descobertas nativamente pelo Claude Code, cada uma declarando quando usar. A lista é o diretório: nem tabela nem contagem a manter aqui.


📐 Regras transversais (resumo)

  • throw proibido em domain/ e application/. Result<T, E> em vez disso (src/shared/primitives/result.ts).
  • Sem class, sem this — Readonly<> types + funções puras + factory functions com deps injetadas.
  • Branded types para IDs e VOs (ContractId, Money, Period, Cpf, Cnpj, …).
  • Discriminated unions + switch exaustivo com const _: never = x no default (sem throw).
  • Imutabilidade absoluta — mudança por cópia ({ ...prev, status: 'Cancelled' }).
  • import type + extensões .ts em imports relativos (NodeNext + verbatimModuleSyntax).
  • Erros são string literal unions EN kebab-case ('contract-not-active' | 'amendment-pending'), não classes.
  • MySQL 8.4 único em dev/CI/prod via Docker compose (ADR-0020); lista normativa de features SQL permitidas/proibidas.
  • Isolamento de módulo: importar de outro módulo só via <module>/public-api/ (nunca domain//application/ alheios) — ADR-0006.
  • Idioma: código em EN; documentação (handbook, ADRs, .claude/) em PT; strings ao humano em PT.

A sintaxe é enforced pelo tsconfig.json — strict completo, verbatimModuleSyntax (exige import type), NodeNext + allowImportingTsExtensions (exige extensão .ts). O resto vive nas regras path-scoped de .claude/rules/.


📚 Documentação canônica

About

Modular Monolith do ERP Bem Comum (Fase 1: módulo Contratos). Node 24 + TS 6 + ESM/NodeNext + Drizzle/MySQL. CLI primária em PT-BR. Domínio puro funcional (zero throw, zero class, Result<T,E>, branded types). Source of truth: handbook/.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages