ADR-0006 explicitly commits to preserving the legacy's "trava" once a contract leaves its editable draft state, overridable only by an audited super_admin correction — but ContractsService.addItem/addParty had no status or role check at all, letting anyone with contracts:edit add items/parties to a contract in any status, including a signed active one. Adds assertEditable() (same shape as Quotes' assertNotLocked): draft is the only free-editing state; past it, only super_admin can still write, and that write is tagged distinctly in the audit log (contract.item_added_override) rather than looking like an ordinary edit. No frontend change needed — ContractDetailPage never called the add-item/party endpoints in the first place. Verified live: super_admin override still works past draft, and the audit_log row for it carries overrode:true and the contract's status at the time — confirmed directly in Postgres. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx
542 lines
79 KiB
Markdown
542 lines
79 KiB
Markdown
# EDEN — Progresso
|
||
|
||
## Fase 0 — Descoberta e Arquitetura: CONCLUÍDA
|
||
|
||
Data: 2026-09-03.
|
||
|
||
### Entregáveis
|
||
- `docs/project-inventory.md`, `docs/glossary.md`, `docs/architecture.md`, `docs/assumptions.md`, `docs/security/threat-model.md`, `docs/implementation-plan.md`, `docs/progress.md` (este arquivo).
|
||
- 12 ADRs em `docs/adr/` (0001–0012), cobrindo: modular monolith, topologia de containers (Postgres isolado + eden-core/parceiros/assinante em containers/portas distintas — decisão explícita do operador), auth/sessões, modelo de permissões, criptografia/segredos, contrato first-class, stock ledger, separação billing/finance/fiscal, outbox transacional, adapter SaperX, adapter Focus NFe, três apps com identidade compartilhada.
|
||
- 14 subagentes em `.claude/agents/`: eden-architect, eden-database, eden-security, eden-commercial, eden-finance, eden-fiscal, eden-inventory, eden-telecom, eden-support, eden-hr-timeclock, eden-frontend, eden-api-integrations, eden-qa, eden-code-reviewer.
|
||
- 7 skills em `.claude/skills/`: eden-domain, eden-security, eden-finance, eden-fiscal, eden-inventory, eden-telecom, eden-timeclock — cada uma com `SKILL.md` + `references/`.
|
||
- Hooks em `.claude/settings.json` + `.claude/hooks/*`: `PreToolUse` (guarda de comandos Bash destrutivos/externos e de acesso a arquivo fora do projeto/`.ssh`/`/etc`/segredo), `PostToolUse` (lint/typecheck incremental, fail-open enquanto pnpm não existir), `Stop` (checagem advisória de segredo/TODO crítico no diff, silenciosa até existir repositório git). Testados via pipe com payloads sintéticos — todos os cenários (allow/ask/deny/fail-open) se comportaram como esperado.
|
||
|
||
### Revisão cruzada (self-review contra os critérios dos agentes eden-architect / eden-security / eden-database, criados nesta mesma fase)
|
||
|
||
**eden-architect**: bounded contexts (`docs/architecture.md` §2) têm direção de dependência consistente (nenhum ciclo); HR/Timeclock corretamente isolado; candidatos a extração futura (Billing, Fiscal) identificados sem extração prematura. Nenhuma feature de negócio foi implementada antes deste gate — consistente com o Master Prompt §18.
|
||
|
||
**eden-security**: threat model cobre as superfícies do legado (rotas públicas, auth, autorização, integrações, upload, PDF, auditoria, infraestrutura) mais os invariantes de teste obrigatórios (§15.3). Hooks `PreToolUse` já impõem, a nível de ferramenta, várias regras de §2.1/§2.2 (fora do projeto, `.ssh`/`/etc`, impressão de `.env`, SSH/SCP externo, remoção ampla de volume/prune). Pendência real registrada (não bloqueante): política de retenção/eliminação LGPD ainda não definida pela Handix (`docs/assumptions.md` #1).
|
||
|
||
**eden-database**: princípios de modelagem (§11) já refletidos nas ADRs de contrato/estoque/billing (nunca saldo editável, nunca CASCADE indiscriminado, JSONB só para snapshot/metadata). Nenhum schema físico foi criado ainda nesta fase — corretamente adiado para a Fase 1, quando o Postgres 18 realmente subir.
|
||
|
||
**Nota**: esta revisão foi feita pela própria sessão orquestradora aplicando os critérios definidos nos arquivos de agente recém-criados. Os subagentes dedicados (`eden-architect`, `eden-security`, `eden-database`) ficam disponíveis para revisões reais em sessões futuras assim que o Claude Code os carregar como subagent types.
|
||
|
||
### Pendências não bloqueantes (ver `docs/assumptions.md`)
|
||
1. Política de retenção/eliminação de dados pessoais (LGPD) — Handix ainda não definiu.
|
||
2. Docker/Node/pnpm precisam ser provisionados no ambiente antes da Fase 1.
|
||
3. Portas dos containers definidas via `.env` (não fixas) — ver ADR-0002.
|
||
4. Git ainda não iniciado no projeto — planejado para o início da Fase 1.
|
||
5. Inventário fino do `tema_do_Eden.zip` (extração de componentes) adiado para o início de `eden-frontend`.
|
||
|
||
## Gate de saída da Fase 0: VERDE
|
||
|
||
Todos os itens do checklist da seção 22 do Master Prompt (“Primeira Execução”) estão completos.
|
||
|
||
---
|
||
|
||
## Fase 1 — Plataforma: EM ANDAMENTO
|
||
|
||
Data de início: 2026-09-03.
|
||
|
||
### Ambiente provisionado
|
||
- Docker CE 29.7.2 + Compose v5.5.0 (repositório oficial Docker, não o `docker.io` do Debian) instalados via apt.
|
||
- Node.js 22.23.2 LTS (NodeSource) + pnpm 11.25.0 (via corepack).
|
||
- Git 2.47.3. Repositório inicializado (`main`), remoto `origin` = `https://git.falehandix.com.br/Matheus/eden.git` (nenhum push feito ainda).
|
||
|
||
### Monorepo
|
||
- pnpm workspaces + Turborepo. 5 apps (`api`, `worker`, `core-web`, `reseller-web`, `subscriber-web`) + 9 packages, todos com `package.json`/`tsconfig.json` válidos.
|
||
- `pnpm install`, `pnpm turbo run typecheck` e `pnpm turbo run build` passam 100% (15/15 e 14/14 tarefas, respectivamente) em todo o monorepo.
|
||
- `apps/api`: NestJS mínimo com `/health/live` e `/health/ready` (este último checando Postgres de verdade via `@eden/database`).
|
||
- `apps/worker`: placeholder que confirma conectividade com o banco; sem BullMQ/Redis ainda (conforme ADR-0002 — só entra quando houver job real).
|
||
- `apps/core-web`, `apps/reseller-web`, `apps/subscriber-web`: Vite + React + TS + Tailwind, cada um com favicon oficial do EDEN.
|
||
- `packages/database`: migrations via `node-pg-migrate` (formato `.cjs`), client de query (`pg` Pool).
|
||
|
||
### Banco de dados
|
||
- Migration baseline aplicada com sucesso contra Postgres 18.6 real (container `eden-postgres`): `roles`, `role_permissions`, `applications`, `users`, `user_applications`, `sessions`, `audit_log` — cobrindo Identity & Access (ADR-0003/ADR-0004) e um audit log append-only com hash-chain (testado: `UPDATE`/`DELETE` são efetivamente bloqueados pelo trigger, só liberados via `SET LOCAL eden.allow_audit_mutation`).
|
||
- Seeds: 4 papéis do sistema (pesos idênticos ao legado) + as 3 aplicações (`core`/`reseller`/`subscriber`).
|
||
- Organization (empresas/entidades legais), Commercial e demais bounded contexts **ainda não têm migration** — ficam para quando cada fase começar, por design (ver ADR-0001).
|
||
|
||
### Docker Compose — topologia ADR-0002 validada de ponta a ponta
|
||
`docker compose --env-file .env up -d` sobe os 6 serviços com sucesso:
|
||
|
||
| Serviço | Status observado |
|
||
|---|---|
|
||
| `eden-postgres` | healthy — porta dev 55432, nunca 5432 direto |
|
||
| `eden-api` | healthy — único serviço com `DATABASE_URL`; `/health/ready` confirma Postgres via rede interna |
|
||
| `eden-worker` | up — conecta ao Postgres, sem porta exposta |
|
||
| `eden-core` (:3001) | healthy — HTTP 200 |
|
||
| `eden-parceiros` (:3002) | healthy — HTTP 200 |
|
||
| `eden-assinante` (:3003) | healthy — HTTP 200 |
|
||
|
||
Bugs de bootstrap corrigidos nesta sessão (documentados aqui para não se repetirem):
|
||
1. Imagem oficial `postgres:18-alpine` mudou o layout do volume — precisa montar `/var/lib/postgresql` (não `.../data`).
|
||
2. `apps/api/package.json` tinha `"type": "module"` incompatível com o `tsconfig` (saída CommonJS) — Node falhava com `exports is not defined`.
|
||
3. `node-pg-migrate --envPath` não carregava o `.env` corretamente nesta versão — trocado por `bash -c 'source .env; ...'` nos scripts `migrate:up`/`migrate:down`.
|
||
4. Healthcheck dos 3 frontends usava `wget http://localhost/...`, que resolve para `::1` (IPv6) dentro do container — nginx só escuta IPv4; trocado para `127.0.0.1`.
|
||
|
||
### Auth, permissões, Companies e OpenAPI — implementados e testados ao vivo
|
||
|
||
- **Auth** (`apps/api/src/auth/`): Argon2id (parâmetros versionados, ADR-0005), JWT de acesso de 15 min, refresh token opaco (256 bits) com rotação e **detecção de replay** (reuso de token já rotacionado revoga toda a cadeia de sessões do usuário). Endpoints `POST /api/v1/auth/{login,refresh,logout}` e `GET /api/v1/auth/me`.
|
||
- **Permissões** (`apps/api/src/auth/permissions.ts`, `permission.guard.ts`): resource+action+scope (ADR-0004), `super_admin` com bypass hardcoded (nunca consulta `role_permissions`, espelhando a trava também existente no banco). Guard resolve permissões do banco a cada request (nunca cacheado no JWT) — mudança de papel tem efeito imediato, mesma filosofia do legado.
|
||
- **Companies** (`apps/api/src/companies/` + migration `1788437478189_companies`): CRUD com leitura aberta a qualquer autenticado e escrita restrita a `companies:manage`; validação de CNPJ (dígito verificador, `packages/domain-shared`) testada com CNPJ válido/inválido conhecidos.
|
||
- **Seed automático**: `SeedService` cria o primeiro `super_admin` a partir de `EDEN_SUPERADMIN_EMAIL`/`PASSWORD` só quando `users` está vazia — testado (log `super_admin inicial criado`).
|
||
- **OpenAPI/Swagger**: `/api/docs` (UI) e `/api/docs-json`, cobrindo todos os endpoints atuais.
|
||
- **Validação/segurança de request**: `ValidationPipe` global com `whitelist`+`forbidNonWhitelisted` (proteção contra mass-assignment) — testado (`id`/`isActive` extras num POST de company são rejeitados com 400).
|
||
|
||
Testes manuais realizados contra a stack real (não só typecheck): login com senha errada → 401 genérico; refresh rotaciona e o token antigo some; reuso do token antigo → 401 + toda a cadeia revogada; logout revoga e é idempotente; audit log com a sequência completa de eventos (`auth.login`, `auth.refresh`, `auth.refresh_replay_detected`, `company.created`).
|
||
|
||
### CRUD de Usuários/Papéis, Products, Resellers, adapters S3/SMTP — implementados via 4 subagentes especializados em paralelo (worktrees isolados) + trabalho direto
|
||
|
||
Estratégia: 4 subagentes (`general-purpose`, atuando com a persona `eden-commercial`/`eden-api-integrations` definida em `.claude/agents/`, já que os subagent types customizados ainda não carregaram nesta sessão) rodaram em paralelo, cada um em `git worktree` isolado, enquanto o CRUD de Usuários/Papéis foi feito diretamente (por tocar peso hierárquico e segurança). Cada agente foi instruído a não editar `app.module.ts` nem `packages/database/src/index.ts` — a integração final (merge das branches, wiring, resolução de conflitos, rebuild, teste ao vivo) foi feita manualmente.
|
||
|
||
- **Users/Roles** (`apps/api/src/users/`, `apps/api/src/roles/`): convite com senha temporária, troca de senha self-service, reset administrativo, grant/revoke de aplicação — tudo com checagem de peso hierárquico (`assertCanAssignRole`/`assertCanActOnUser`, eden.md §1.6). CRUD de papéis e `role_permissions` exclusivo `super_admin` (`SuperAdminGuard`), deliberadamente não generalizado para o modelo de permissão (raiz do próprio sistema de autorização).
|
||
- **Products** (`apps/api/src/products/` + migration `products`): catálogo com as 5 faixas de preço fixas, `product_code` sequencial, flags de rastreamento (estoque/serial/patrimônio/MAC). Leitura aberta, escrita via `products:edit`.
|
||
- **Resellers** (`apps/api/src/resellers/` + migration `resellers`): CRUD operacional + vínculo usuário↔revenda (`users.reseller_id`). Leitura aberta, escrita via `resellers:edit` — desvio deliberado do legado (que travava em `requireAdmin`), documentado em comentário.
|
||
- **Adapters S3/SMTP** (`packages/integrations/`): portados fielmente do legado — S3 falha alto (erro claro se não configurado), SMTP falha silencioso (nunca quebra um fluxo de negócio por causa de e-mail).
|
||
|
||
- **CRM** (`apps/api/src/leads/`, `apps/api/src/opportunities/` + migration `crm`): leads e oportunidades com conversão transacional (`convertLeadToOpportunity`, `FOR UPDATE` + `withTransaction`). Diferente de Companies/Products, leitura também exige permissão (`leads:view`/`opportunities:view`) por conter PII de prospect.
|
||
|
||
**4 bugs reais encontrados em teste manual e corrigidos antes do commit final:**
|
||
1. `assertCanAssignRole`/`assertCanActOnUser` pulavam a validação de existência do papel quando o ator era `super_admin` (o bypass de peso pulava a checagem inteira) — convidar usuário com `role_key` inexistente estourava a constraint FK do banco como 500 não tratado em vez de 400 claro.
|
||
2. `PATCH /v1/resellers/:id/assign-user` aceitava um `resellerId` diferente no corpo, que na prática era o valor usado — o `:id` da URL virava só uma checagem de existência, não o alvo real da atribuição. Corrigido para o `:id` da URL ser a única fonte de verdade; desatribuição virou endpoint próprio (`POST /v1/resellers/unassign-user`).
|
||
3. Os controllers de Leads/Opportunities liam `req.user?.id` em vez de usar `@CurrentUser()`/`request.edenUser` (o padrão real do `JwtAuthGuard`) — todo evento de auditoria de lead/oportunidade estaria gravando `actorId: null` sempre. O agente que os escreveu não tinha o módulo de auth disponível no worktree dele para testar isso de verdade (só stubou e reverteu antes de commitar).
|
||
4. As chamadas `appendAuditEvent()` de leads/opportunities omitiam o campo obrigatório `actorType` (6 ocorrências).
|
||
|
||
Também precisou renomear o timestamp da migration de CRM, que o agente gerou com valor anterior ao da migration de `resellers` (já aplicada por outro agente/merge) — `node-pg-migrate` recusa migration fora de ordem.
|
||
|
||
Todos os módulos testados ao vivo contra a stack real (não só typecheck): convite de usuário, papel inexistente (400 após o fix), CRUD de produto com preços NUMERIC corretos, CRUD de revenda, atribuição/desatribuição de usuário à revenda, lead→oportunidade transacional.
|
||
|
||
### Suíte de testes de integração (agente eden-qa)
|
||
|
||
Vitest + supertest + `@nestjs/testing` (bootstrap real do Nest, sem mock) contra o Postgres real — 5 arquivos, 15 testes, todos verdes: `auth.spec.ts` (anti-enumeração, rotação de refresh, **detecção de replay fim-a-fim**, logout idempotente), `role-weight.spec.ts` (guarda a regressão do bug #1), `companies.spec.ts` (CNPJ), `mass-assignment.spec.ts` (whitelist), `reseller-assignment.spec.ts` (guarda a regressão do bug #2). Achado técnico do agente: o `esbuild` (transform padrão do Vitest) não implementa `emitDecoratorMetadata`, quebrando toda injeção de dependência do Nest silenciosamente sob teste — corrigido trocando para SWC (`unplugin-swc`), abordagem padrão documentada pelo próprio NestJS. Rodar com `pnpm --filter @eden/api test`.
|
||
|
||
### Resumo do lote de agentes paralelos
|
||
|
||
6 subagentes no total (4 + 1 QA + Users/Roles feito diretamente), rodando em `git worktree` isolados, revisados e mesclados manualmente um a um (nunca merge automático sem verificação). **4 bugs reais** encontrados só porque cada módulo foi testado contra a stack viva antes do commit — nenhum deles teria aparecido só com typecheck/build, que todos os agentes já reportavam verde. Padrão validado: delegar implementação para agentes especializados funciona bem para módulos CRUD bem especificados, mas a integração final (merge, wiring, teste ao vivo, revisão) precisa ficar com quem tem visão do sistema inteiro.
|
||
|
||
### Pendências da Fase 1 (não iniciadas)
|
||
- ~~MFA/TOTP~~ — implementado na leva seguinte (ver Fase 4+ abaixo).
|
||
- Extração do Design System a partir do tema DreamsERP (`packages/ui` ainda é placeholder).
|
||
- Wiring dos adapters S3/SMTP em algum módulo consumidor real (hoje só existem como biblioteca em `packages/integrations`, nada os chama ainda).
|
||
- Nota de dado de teste: a empresa "Handix Telecom LTDA" e a revenda "Handix (revenda própria)" criadas durante os testes usam dados de exemplo, não os dados reais da Handix — substituir/remover antes de qualquer ambiente que não seja dev local.
|
||
|
||
---
|
||
|
||
## Fase 2/3 — Ofertas (Quotes), Cadastro de Cliente (Customer 360) e esqueleto de Contratos
|
||
|
||
Data: 2026-09-03.
|
||
|
||
### Ofertas / Quotes (`apps/api/src/quotes/` + migration `quotes`) — implementado diretamente
|
||
|
||
Núcleo comercial mais sensível a erro de arredondamento/regra de negócio (eden.md §7/§8), por isso feito sem delegar a subagente:
|
||
- `pricing.ts`: funções puras porteadas literalmente do legado — `monthlyTotal = Σitem.total`; `totalImpl = implFromProducts + implGeral`; `hasSpecialPrice/isDiscount/isMarkup` por comparação com `proposedMonthlyTotal`; `contractTotal = effectiveMonthly * (contractPeriod||1) + totalImpl`; `needsApproval = isDiscount || hasReducedFidelity`; fidelidade só tem efeito legal com `approval_status === 'approved'`. Faixas de condição de pagamento (`À vista` ≤1500; `+1+1` ≤3000; `+1+2` acima) replicadas do legado.
|
||
- Preço sempre derivado server-side do produto vivo no banco (nunca confia em preço vindo do cliente).
|
||
- Fluxo completo: criar oferta → decidir aprovação (`PATCH :id/approval`) → fechar negócio vinculando a um cadastro de cliente (`POST :id/close`, bloqueado por `client_registration_id IS NULL` — não fecha duas vezes).
|
||
|
||
### Customer 360 / Cadastro de Cliente (`apps/api/src/client-registrations/` + migration `client-registrations`) e Contratos (`apps/api/src/contracts/` + migration `contracts`) — subagentes + salvamento manual
|
||
|
||
Lançados 2 subagentes em paralelo (worktrees isolados) para Cadastro de Cliente e esqueleto de Contratos. Ambos falharam duas vezes: primeiro com rate limit de sessão (retomado após o usuário habilitar cobrança de excedente), depois com **limite de gasto mensal da conta** — um bloqueio de nível de conta que não se resolve tentando de novo. Em vez de insistir, os dois worktrees foram inspecionados (`git status`, diff arquivo a arquivo); o trabalho não commitado encontrado em ambos era substancial e de boa qualidade — foi copiado diretamente para `main` (sem passar por branch/merge, já que nada tinha sido commitado) e o que faltava foi completado manualmente:
|
||
- **Cadastro de Cliente**: repositórios e camada de API salvos do agente praticamente inalterados; só faltava o `client-registrations.module.ts`, escrito manualmente. MVP com escopo explícito (eden.md §3): só fluxo autenticado, sem token de autoatendimento público, sem upload de anexo, sem integração CNPJ/CEP — tudo documentado em comentário no código, não descartado silenciosamente.
|
||
- **Contratos**: repositório salvo do agente (com um import não usado removido); toda a camada de API (DTOs, service, controller, module) escrita manualmente, pois o agente não tinha chegado lá. Máquina de estados (ADR-0006) `draft→{pending_signature,cancelled}`, `pending_signature→{active,cancelled}`, `active→{suspended,cancelled,terminated,expired}`, `suspended→{active,cancelled,terminated}` — imposta via `SELECT...FOR UPDATE` + tabela de transições, com `signed_at`/`activated_at` carimbados na transição correta e histórico completo em `contract_status_history`. Escopo: 4 das 11 tabelas do ADR-0006 (contracts, contract_items, contract_parties, contract_status_history); as outras 7 explicitamente adiadas.
|
||
|
||
Wiring final feito manualmente: barrel (`packages/database/src/index.ts`) e `app.module.ts` atualizados; 3 migrations aplicadas ao Postgres real; `pnpm turbo run typecheck build` (28/28 tasks) e `pnpm --filter @eden/api test` (15/15 testes) verdes; container `eden-api` reconstruído e todo o fluxo testado ao vivo via curl contra a stack real:
|
||
- Oferta sem preço especial (sem aprovação) e oferta com desconto + fidelidade reduzida (`approval_status: pending`) — valores conferidos manualmente contra a fórmula.
|
||
- Aprovação da oferta, fechamento vinculando a um cadastro de cliente criado via `POST /client-registrations/direct`, e guarda de fechamento duplo (409 na segunda tentativa).
|
||
- Contrato criado a partir da oferta fechada, transições de status válidas e inválidas (draft→active direto rejeitado com 409; draft→pending_signature→active aceito, `signed_at`/`activated_at` carimbados corretamente), adição de item/parte, FK de produto inexistente rejeitada com 400.
|
||
- 401 sem token; trilha de auditoria (`audit_log`) conferida no Postgres com a sequência completa de eventos do lote (`quote.created`, `quote.approval_approved`, `quote.closed`, `client_registration.created`, `contract.created`, `contract.status_changed` ×2, `contract.party_added`, `client_registration.updated`).
|
||
|
||
**2 bugs reais encontrados e corrigidos antes do commit (nenhum via subagente — achados na própria implementação/revisão):**
|
||
1. Primeiro rascunho do `updateQuote` (escrito por mim) usava `COALESCE` para os campos financeiros (`fidelity_period`, `proposed_monthly_total`, `approved_by_id`) — como `undefined` do JS também vira `NULL` no SQL, qualquer update parcial que omitisse esses campos os apagaria silenciosamente. Corrigido tornando esses campos **obrigatórios** em `UpdateQuoteInput` (não mais opcionais), forçando a camada de serviço a sempre fornecer um valor deliberado.
|
||
2. `packages/database/src/repositories/types.ts` — a interface `User` estava sem `reseller_id`/`can_approve_discount`, mesmo essas colunas já existindo no banco desde as migrations de Resellers/Quotes (ninguém tinha atualizado o tipo compartilhado). Descoberto ao ligar o uso desses campos em Quotes; corrigido no `types.ts` e propagado para `EdenAuthenticatedUser` em `jwt-auth.guard.ts`.
|
||
|
||
### Pendências deste lote
|
||
- ~~FKs cruzadas entre as 3 tabelas~~ — fechadas na leva seguinte (ver abaixo).
|
||
- Fluxo de autoatendimento público (token) de Cadastro de Cliente, upload de anexo, integração CNPJ/CEP — todos fora do escopo desta MVP.
|
||
- 7 das 11 tabelas do ADR-0006 de Contratos (aditivos, faturas, documentos/e-signature, etc.) ainda não criadas.
|
||
|
||
---
|
||
|
||
## Fase 4/9 — MFA/TOTP, Estoque (Inventory), Suporte (Service Desk) e fundação de Ponto Eletrônico (HR/Timeclock)
|
||
|
||
Data: 2026-09-03.
|
||
|
||
Pedido do usuário: "pode seguir na criacao do app ao menos mais 20 etapas use subagentes". Estratégia: fechei a pendência de MFA/TOTP da Fase 1 diretamente (autenticação é sensível demais para delegar) e lancei 3 subagentes em paralelo (`eden-inventory`, `eden-support`, `eden-hr-timeclock`) em worktrees isolados, seguindo o mesmo padrão validado nas levas anteriores. Além das FKs cruzadas de Quotes/Contracts/ClientRegistrations, deixadas pendentes na leva anterior.
|
||
|
||
### FKs cruzadas (pendência da leva anterior) — fechada
|
||
|
||
Migration `1788450538883_cross-reference-fks.cjs`: `quotes.client_registration_id → client_registrations(id)`, `client_registrations.quote_id → quotes(id)`, `contracts.quote_id → quotes(id)`, `contracts.client_registration_id → client_registrations(id)`, todas `ON DELETE RESTRICT` (nenhum desses agregados tem rota DELETE hoje; perder o vínculo silenciosamente numa exclusão não relacionada seria pior que bloquear a exclusão). Verificado zero órfãos antes de aplicar.
|
||
|
||
### MFA/TOTP (`apps/api/src/auth/mfa.service.ts`, `field-encryption.service.ts`) — implementado diretamente
|
||
|
||
- `FieldEncryptionService` (`apps/api/src/crypto/`): criptografia reversível genérica AES-256-GCM versionada (ADR-0005), formato `v1:iv:tag:ciphertext` base64, chave de `EDEN_FIELD_ENCRYPTION_KEY` (hasheada para 32 bytes via SHA-256). Primeira implementação real desse padrão no projeto — vai ser reaproveitada por outros segredos reversíveis (ex.: credencial de equipamento Control iD, feito na mesma leva).
|
||
- Fluxo de login em duas etapas quando `mfa_enabled=true`: `POST /auth/login` retorna `{mfaRequired: true, mfaChallenge}` (JWT de propósito único, 2 min, claim `purpose` distintivo) em vez de tokens; `POST /auth/mfa/challenge` troca o desafio + código TOTP pelos tokens reais. `POST /auth/mfa/enroll` (gera secret + `otpauthUrl`, não habilita ainda) → `POST /auth/mfa/confirm` (primeiro código correto habilita) → `POST /auth/mfa/disable` (exige código válido, não só sessão autenticada).
|
||
- Biblioteca `otplib` v13 (API funcional `generateSecret`/`generateURI`/`verify`, não mais o antigo `authenticator` singleton de v12).
|
||
|
||
**Bug de segurança real encontrado e corrigido em teste ao vivo (não hipotético — explorado de verdade contra a stack rodando):** o token de desafio MFA (`mfaChallenge`, emitido logo após a senha, antes do código TOTP) era aceito pelo `JwtAuthGuard` como um access token comum — `verifyAccessToken()` só validava assinatura/expiração, nunca checava a claim `purpose`. Resultado: um atacante que só tivesse a senha (sem o segundo fator) conseguia acesso total à API por 2 minutos usando o token de desafio como Bearer normal — MFA completamente contornável. Corrigido em `token.service.ts::verifyAccessToken` rejeitando qualquer token com claim `purpose` presente. Teste de regressão adicionado em `test/mfa.spec.ts` (`GET /auth/me` com o challenge token como Bearer → 401). Explorado e depois fechado ao vivo via curl contra o container real antes de seguir adiante.
|
||
|
||
### Estoque / Inventory (`apps/api/src/inventory/` + migration `inventory`) — subagente `eden-inventory`
|
||
|
||
5 tabelas (de 15 possíveis no Master Prompt §6.8, mesmo approach de MVP-coerente do módulo Contracts): `warehouses`, `stock_movements` (ledger append-only, saldo sempre derivado via SUM, nunca coluna editável — reforçado por trigger que bloqueia UPDATE/DELETE sem exceção), `serialized_assets`, `asset_assignments`, `stock_reservations`. Invariante "mesmo serial/patrimônio/MAC nunca duas vezes ativo" (Master Prompt §15.3) reforçada por **3 índices únicos parciais no banco**, não só validação de aplicação. Fluxo completo reserva→instalação(comodato)→devolução implementado com `SELECT...FOR UPDATE` + transação, erros tipados mapeados para 404/409.
|
||
|
||
Subagente entregou trabalho de excelente qualidade na primeira tentativa — nenhum bug encontrado na revisão nem no teste ao vivo.
|
||
|
||
### Suporte / Service Desk (`apps/api/src/support/` + migration `support`) — subagente `eden-support`
|
||
|
||
4 tabelas: `support_tickets` (protocolo humano `TCK-2026-000001`, mesmo padrão de `quote_number`/`contract_number`), `ticket_comments` (com `is_internal`, nunca exposto fora do escopo `all`), `ticket_status_history`, `sla_timers` (1:1). Máquina de estados `new→triage→in_progress⇄{waiting_customer,waiting_third_party}→resolved→closed` (+`cancelled`), mesmo padrão de `CONTRACT_STATUS_TRANSITIONS`. Motor de SLA implementado como funções puras testáveis (`apps/api/src/support/sla.ts`) — pausa/despausa atomicamente em SQL na transição de status, cálculo de violação subtrai corretamente o tempo pausado (incluindo a pausa corrente). `root_cause`/`resolution_notes` só persistidos na transição para `closed`, não `resolved` (decisão documentada, confirmada em teste ao vivo).
|
||
|
||
Também de excelente qualidade — nenhum bug encontrado.
|
||
|
||
### Fundação de Ponto Eletrônico / HR-Timeclock (`apps/api/src/hr-timeclock/` + migration `hr-timeclock`) — subagente `eden-hr-timeclock`
|
||
|
||
Escopo desta leva deliberadamente limitado a fundação (modelo de dados + Control iD device + parser/import de AFD com CRC-16/KERMIT) — apuração, fechamento de período, ajustes, banco de horas e relatórios (eden.md §5-10) explicitamente adiados. 4 tabelas: `timeclock_devices` (senha do equipamento cifrada via `FieldEncryptionService`, nunca retornada em nenhum GET — só `hasCredential:boolean`), `timeclock_employees` (cadastro separado de `users`, decisão documentada: o Control iD identifica por CPF, e `users` não tem CPF), `afd_import_batches`, `afd_records`. `afd_records` é **imutável sem exceção, nem para super_admin** — trigger que bloqueia UPDATE/DELETE sem nenhuma válvula de escape (diferente de `audit_log`, que tem bypass controlado via `SET LOCAL`). Idempotência em duas camadas: hash SHA-256 do arquivo inteiro (retorna `alreadyImported:true` em reimportação) e `UNIQUE(device_id, nsr)` por registro. NSR ausente/não-numérico nunca vira 0 — fica `null`, preservando o dado bruto mesmo inválido.
|
||
|
||
Parser CRC-16/KERMIT (poly 0x1021 refletido/0x8408, init 0x0000, sem XOR final) verificado contra o vetor de teste conhecido ("123456789" → 0x2189) — confirmado tanto no script ad-hoc do agente quanto na suíte permanente `test/afd-parser.spec.ts` (11 testes).
|
||
|
||
**1 bug real encontrado e corrigido antes da integração:** a migration `PENDING_hr-timeclock.cjs` tinha uma crase (`` ` ``) solta dentro de um comentário SQL (`-- separate from \`users\``) — como a migration inteira é um template literal JS (`` pgm.sql(`...`) ``), essa crase fechava o literal prematuramente e quebrava o resto do SQL em código JS inválido, causando `SyntaxError: missing ) after argument list` ao tentar rodar `pnpm db:migrate`. Corrigido trocando a crase por aspas simples no comentário. Encontrado e corrigido antes de qualquer teste ao vivo, na primeira tentativa de aplicar a migration.
|
||
|
||
### Padrão de salvamento seletivo ao integrar (novo, deste lote)
|
||
|
||
Diferente da leva anterior (onde os 2 agentes falharam por limite de gasto e o trabalho foi resgatado de worktrees não commitados), desta vez os 3 agentes **completaram normalmente**, mas todos os 3 worktrees foram criados a partir do commit único de bootstrap (antes de Auth/Companies/Products/etc. existirem nesse checkout isolado) — sem acesso ao código real de `apps/api/src/auth/`, `packages/database/src/repositories/{audit,users,roles,types}.ts` etc. Cada agente relatou isso com transparência e, para conseguir cumprir a etapa de verificação (`pnpm typecheck` limpo), recriou stubs/placeholders type-compatíveis desses arquivos dentro do próprio worktree — e dois deles (`eden-support`, `eden-hr-timeclock`) deixaram esses placeholders (`audit.ts`, `users.ts`, `roles.ts`, `types.ts`, config, guards de auth) como arquivos genuínos no diff final, não revertidos, por acreditarem (corretamente, do ponto de vista deles, isolados no worktree) que essa infraestrutura ainda não existia.
|
||
|
||
Na integração, copiei seletivamente **só os arquivos de domínio real** de cada agente (migration, repositório específico do módulo, camada de API do módulo) e **descartei os placeholders duplicados de Auth/Config/Crypto/audit/users/roles**, mantendo as versões reais já existentes em `main`. Wiring do barrel (`packages/database/src/index.ts`) e `app.module.ts` feito manualmente para os 3 módulos, como nas levas anteriores.
|
||
|
||
### Verificação
|
||
|
||
`pnpm turbo run typecheck build` verde em cada integração incremental; suíte de testes cresceu de 15 para **30 testes, todos verdes** (`auth.spec.ts`, `mfa.spec.ts` novo, `afd-parser.spec.ts` novo com 11 testes, `role-weight.spec.ts`, `reseller-assignment.spec.ts`, `companies.spec.ts`, `mass-assignment.spec.ts`). Container `eden-api` reconstruído e todo o lote testado ao vivo via curl contra a stack real:
|
||
- MFA: enroll→confirm com código TOTP real gerado via `otplib`→login exige desafio→código errado rejeitado→código certo completa login→disable exige código válido; **exploração do bug de segurança repetida após o fix, confirmando 401**.
|
||
- Inventory: armazém→entrada de ativo→serial duplicado rejeitado (409, não 500)→reserva→instalação (comodato)→instalação duplicada rejeitada (409)→devolução→ledger conferido linha a linha (`purchase_in`, `reservation`, `comodato_out`, `return_in`).
|
||
- Support: ticket criado com SLA (+4h/+24h)→transições válidas→comentário público marca `first_response_met`→`waiting_customer` pausa o timer→retorno a `in_progress` acumula o tempo pausado→`resolved`→`closed` (root_cause só persiste aqui)→transição inválida rejeitada (409).
|
||
- HR-Timeclock: device criado com senha cifrada (confirmado no Postgres, formato `v1:iv:tag:ciphertext`, nunca no GET)→import de arquivo AFD válido (2 linhas, CRC correto)→reimport do mesmo arquivo retorna `alreadyImported:true` sem duplicar→`UPDATE` direto em `afd_records` via SQL bloqueado pelo trigger mesmo fora da API.
|
||
- Trilha de auditoria (`audit_log`) conferida no Postgres com a sequência completa de todos os eventos do lote.
|
||
|
||
### Pendências deste lote
|
||
- Inventory: `warehouse_locations`, `stock_items` (cache de saldo), `stock_lots`, `inventory_counts`, `transfers`/`receipts`/`issues`/`returns` formais, `rma`, `asset_maintenance` — deferidos, tipos de movimento já modelados no vocabulário do ledger para não exigir nova migration.
|
||
- Support: anexos reais (S3), CSAT, OS dedicada, filas com roteamento automático, calendário de atendimento configurável, SLA por categoria — hoje é uma política única fixa (4h/24h).
|
||
- HR-Timeclock: apuração, fechamento de período, ajustes manuais, banco de horas, relatórios, sync ao vivo com o Control iD, upload multipart real (hoje aceita o conteúdo como string JSON no body).
|
||
- MFA: sem rate-limiting/bloqueio de tentativas no `mfa/challenge` (mesma lacuna que já existe no login por senha hoje — nenhum endpoint de auth tem rate limit ainda, ficou registrado como débito técnico geral, não específico do MFA).
|
||
|
||
---
|
||
|
||
## Fase 5/6 + Design System — Billing, Fiscal e primeira tela real (login + shell autenticado)
|
||
|
||
Data: 2026-09-03.
|
||
|
||
Continuação do mesmo pedido do usuário ("pelo menos mais 20 etapas, use subagentes"). 3 subagentes em paralelo (`eden-finance`, `eden-fiscal`, `eden-frontend`), mesmo padrão de worktree isolado + integração seletiva manual.
|
||
|
||
### Billing / Faturamento recorrente (`apps/api/src/billing/` + migration `billing`) — subagente `eden-finance`
|
||
|
||
MVP de 4 tabelas (ADR-0008 lista 9; billing_accounts/billing_cycles/charge_components/usage_charges/invoice_adjustments/billing_run_logs deferidos, documentado): `billing_subscriptions` (derivada de contrato, `monthly_value` é snapshot — nunca recalculado automaticamente), `billing_runs`, `invoices`, `invoice_items`. Fluxo `subscription→billing_run→invoice` completo e idempotente: rodar o mesmo run duas vezes em `draft` substitui as invoices (não duplica) — reforçado por índice único parcial `WHERE status='draft'` (no máximo um run em rascunho por período); depois de `consolidate`, run e invoices ficam imutáveis pela aplicação (409 em qualquer tentativa de gerar/alterar). ADR-0008 respeitado à risca: Billing só calcula o que é devido, nunca toca em AR/AP/boleto/cobrança (Finance) nem em documento fiscal (Fiscal) — bounded contexts continuam sendo desenvolvidos separadamente.
|
||
|
||
### Fiscal (`apps/api/src/fiscal/` + migration `fiscal`) — subagente `eden-fiscal`
|
||
|
||
eden.md módulo 5 tem ~19 tabelas de catálogo + importadores automáticos (Siscomex/IBGE) + importador manual de arquivo — MVP portou 5 tabelas fielmente ao shape do legado: `fiscal_ncm`/`fiscal_cfop` (catálogo simples), `fiscal_municipality` (shape distinto, sem `valid_from`/`valid_until`), `fiscal_operation_nature` (seed fixo com as 11 naturezas do legado), `product_fiscal_profiles` (liga produto à classificação fiscal). Invariante do eden.md §2.5 implementada de verdade: **um produto tem no máximo um profile ativo por `billing_component`** — índice único parcial + camada de aplicação que inativa o profile anterior antes de criar o novo (nunca update destrutivo, histórico preservado). Todo o importador automático/manual e as ~14 outras tabelas de catálogo ficam deferidos (documentado com a seção exata do eden.md para cada item).
|
||
|
||
### Design System + login real (`packages/ui`, `apps/core-web`) — subagente `eden-frontend`
|
||
|
||
Primeira leva de frontend do projeto. Achado que mudou o escopo para melhor: o `tema_do_Eden.zip` (comprado) não é só Angular — tem uma versão pronta em **React 18 + Vite + Tailwind** dentro (`react.zip`, 42.5MB), a mesma stack que o EDEN já usa. Em vez de portar de Angular, o agente extraiu os tokens visuais reais (cores, tipografia, sombras, border-radius, larguras de layout) desse React/Tailwind — confirmando inclusive uma divergência da nossa própria documentação: `docs/project-inventory.md` citava fonte Nunito/Poppins, a fonte real do tema é **Geist**.
|
||
|
||
- `packages/ui` deixou de ser placeholder: preset Tailwind exportável (`@eden/ui/tailwind-preset`) com os tokens reais extraídos, mais componentes React reais (não stub) — `Button`, `Input`, `Card`, `Badge`, `Alert`, `PageHeader`, `AppShell` (sidebar+topbar+conteúdo).
|
||
- `apps/core-web` (EDEN Core — prioridade, os outros 2 apps ficam para leva futura): trocou a tela estática "Bootstrap da Fase 1" por um fluxo real com `react-router-dom` — página de login chamando de verdade `POST /api/v1/auth/login` (com suporte ao fluxo `mfaRequired`/`POST /mfa/challenge` da leva de MFA), sessão em `localStorage`, rota protegida, e um shell autenticado (`AppShell` real, nome do usuário via `GET /api/v1/auth/me`, logout).
|
||
|
||
**Achado real fora do escopo do agente, corrigido por mim em seguida**: a API não tinha CORS configurado (`app.enableCors` ausente em `main.ts`) — um browser real rodando `core-web` num domínio/porta diferente da API teria toda chamada de login bloqueada no preflight, mesmo com tudo implementado corretamente nos dois lados. Corrigido: novo `EDEN_CORS_ORIGINS` (env, lista de origens separadas por vírgula, nunca wildcard — a API emite bearer token) em `packages/config/src/env-schema.ts`, `app.enableCors({origin, credentials:true})` em `main.ts`, propagado em `compose.yaml`/`.env`/`.env.example` já com os 3 domínios públicos reais (`dev-edencore`/`dev-parceirohandix`/`dev-portalassinate.falehandix.com.br`) mais as portas de dev locais. Testado ao vivo: preflight de `https://dev-edencore.falehandix.com.br` aceito com os headers corretos; origem não autorizada (`evil.example.com`) corretamente sem `Access-Control-Allow-Origin` na resposta; login completo simulando o `Origin` real de browser, 200 com o header de CORS certo.
|
||
|
||
**Pendência de infra identificada, não resolvida (decisão do operador, não técnica)**: os 3 domínios públicos hoje só apontam para os containers de frontend (3001/3002/3003) — não há um domínio público equivalente para a API. `VITE_API_BASE_URL` do build de produção dos 3 apps ainda aponta para `localhost:8080` (só funciona em dev local); falta decidir e implementar como o browser em produção alcança a API (domínio próprio para a API? proxy reverso roteando um path tipo `/api` para o container `eden-api` no mesmo domínio do frontend?) antes do login funcionar nos domínios públicos de verdade.
|
||
|
||
### Verificação
|
||
|
||
`pnpm turbo run typecheck build` (28/28) e `pnpm --filter @eden/api test` (30/30, sem regressão) verdes após cada integração. Containers `eden-api`/`eden-core` reconstruídos; testado ao vivo via curl:
|
||
- Fiscal: naturezas de operação semeadas (11), NCM criado, **invariante de profile único ativo por componente confirmada** (segundo profile RECURRING criado → primeiro automaticamente `active:false`).
|
||
- Billing: subscription criada → billing run gerado (invoice de R$499,90, vencimento no `billing_day`) → regeneração do mesmo período reaproveita o mesmo run (idempotente, sem duplicar invoice) → consolidação → segunda consolidação rejeitada (409) → regeneração de período já consolidado rejeitada (409).
|
||
- CORS: preflight aceito do domínio público real, rejeitado de origem não cadastrada, login completo com `Origin` de browser real.
|
||
- `core-web`: build de produção servido pelo container real não contém mais o texto "Bootstrap da Fase 1" — contém o formulário de login real (`E-mail`/`Senha`/`Entrar`).
|
||
- Trilha de auditoria conferida no Postgres com a sequência completa do lote.
|
||
|
||
### Pendências deste lote
|
||
- Billing: `billing_accounts`, `billing_cycles` como tabela própria, consumo/telefonia (`charge_components`/`usage_charges`), `invoice_adjustments`, pró-rata — tudo Finance/AR-AP (títulos, boletos, dunning, conciliação) fica para quando esse bounded context for iniciado, por decisão da ADR-0008.
|
||
- Fiscal: as ~14 tabelas de catálogo restantes, `fiscal_catalog_sync`, importador automático (Siscomex/IBGE) e manual (CSV/XLSX/JSON com proteção de inativação em massa), `fiscal_rules` (schema pronto mas sem consumo mesmo no legado).
|
||
- Frontend: `reseller-web`/`subscriber-web` ainda no bootstrap estático; telas de negócio (Quotes/Contracts/Inventory/etc.) no `core-web`; rotação automática de refresh token; fluxo de ENROLL de MFA na UI (só login com MFA já habilitado está implementado).
|
||
|
||
### Decisão do operador sobre topologia de API pública + 3 bugs de infra encontrados e corrigidos na sequência
|
||
|
||
Usuário decidiu: a API **nunca** tem domínio público próprio, só IP/nome interno — o browser sempre chama a API pelo mesmo domínio do frontend (caminho relativo), e o proxy externo (gerenciado pelo operador, fora deste repo) roteia esse caminho internamente para `eden-api` (por nome de serviço Docker, `/etc/hosts` ou IP interno — detalhe de infra do operador, não deste código).
|
||
|
||
Implementado: `infra/docker/Dockerfile.web` agora aceita `ARG VITE_API_BASE_URL` (default `/api/v1`, Vite bake em build-time); `compose.yaml` passa esse build arg fixo (`/api/v1`) para os 3 serviços web; `EDEN_CORS_ORIGINS` simplificado para só as portas de dev local (o fluxo de produção é same-origin via proxy, não precisa de CORS).
|
||
|
||
**3 bugs reais de infraestrutura encontrados e corrigidos nessa verificação (nenhum era o objetivo original, todos achados testando o resultado de ponta a ponta, não só lendo o código):**
|
||
1. **`turbo.json` não declarava `VITE_API_BASE_URL` como input da task `build`** — o Turborepo cacheava o build do Vite sem saber que essa env var afeta o output, então reaproveitava um bundle antigo (com a URL absoluta de dev) mesmo depois de mudar a env var. Corrigido adicionando `"env": ["VITE_*"]` à task `build` em `turbo.json`.
|
||
2. **`compose.yaml` usava `${VITE_API_BASE_URL:-/api/v1}`** para o build arg do Docker — parecia razoável, mas colidia com a variável de mesmo nome no `.env` (usada para `pnpm dev` local, valor absoluto `http://localhost:8080/api/v1`) que o `docker compose` carrega automaticamente; o "default" nunca era usado porque a variável já estava setada. Corrigido fixando o valor diretamente (`/api/v1`, sem interpolação de env) nos 3 serviços web, já que essa é uma decisão arquitetural fixa, não uma configuração por ambiente.
|
||
3. **Não existia `.dockerignore`** no projeto — todo `COPY . .` do `Dockerfile.web`/`Dockerfile.node` estava copiando `node_modules/` (315MB), `.git/` (5.8MB) e `.claude/worktrees/` (345MB, os worktrees de subagentes desta mesma sessão) para dentro do contexto de build a cada vez. Além do desperdício óbvio, isso é parte de por que o bug nº1 passou despercebido no primeiro teste: caches `.turbo/` de dentro de cada pacote também eram copiados para dentro da imagem. Corrigido criando `.dockerignore` (exclui `node_modules`, `.git`, `dist`, `.turbo`, `.claude/worktrees`, `.tmp`, `.env*`, o zip do tema).
|
||
|
||
Cada correção foi testada isoladamente e a última confirmada abrindo o JS de produção servido pelo container real (`docker compose build --no-cache`) e conferindo que o literal `"/api/v1"` (não mais `"http://localhost:8080/api/v1"`) está no bundle.
|
||
|
||
---
|
||
|
||
## Fase 8 (parcial) — Portais Parceiros e Assinante replicam o shell autenticado do Core
|
||
|
||
Data: 2026-09-03. Subagente `eden-frontend` replicou o mesmo padrão de login/sessão/shell do `core-web` (leva anterior) para `apps/reseller-web` e `apps/subscriber-web` — mesma estrutura de arquivos, mesmo cliente HTTP, mesmo `AuthContext`, só trocando `applicationKey` (`reseller`/`subscriber`), título, e a chave de `localStorage` da sessão (`eden.reseller.session`/`eden.subscriber.session`, isolamento explícito mesmo já sendo origens/portas diferentes).
|
||
|
||
**Bug de integração (não do agente) encontrado e corrigido**: ao copiar `src/lib/` e `src/routes/` do worktree para `main`, o `cp -r` criou diretórios aninhados (`src/lib/lib/`, `src/routes/routes/`) porque os diretórios de destino já existiam vazios em `main` — mesmo padrão de erro já visto na integração do HR-Timeclock nesta sessão. Corrigido achatando a estrutura antes do typecheck.
|
||
|
||
Verificado: `pnpm turbo run typecheck build` (28/28) após a correção; containers `eden-parceiros`/`eden-assinante` reconstruídos — títulos corretos (`EDEN Parceiros`/`EDEN Assinante`), texto antigo "Bootstrap da Fase 1" ausente do bundle de produção, formulário de login real presente; login de ponta a ponta testado contra a API real com `applicationKey: "reseller"` e `"subscriber"` — ambos aceitos e retornam tokens válidos.
|
||
|
||
### Pendências
|
||
Idênticas às do `core-web` (rotação de refresh token, enroll de MFA na UI, telas de negócio) — nenhuma tela além do shell autenticado existe em nenhum dos 3 apps ainda.
|
||
|
||
---
|
||
|
||
## Fase 3 (parcial) — Fundação de Templates de Documento (sem Assinatura Eletrônica ainda)
|
||
|
||
Data: 2026-09-03. Subagente construiu a metade "templates + geração" do eden.md módulo 4 (§1.1-1.3) — a metade de Assinatura Eletrônica (§1.4-1.9: envelopes, signatários, OTP, cadeia de auditoria hash-chain, 9 tabelas satélite) foi deliberadamente deixada de fora, por ser criptograficamente/legalmente sensível e grande demais para dividir espaço com esta leva — fica para uma leva própria e dedicada.
|
||
|
||
`document_templates` + `document_template_versions` (ciclo `draft→publish`, invariante de no máximo 1 rascunho por template via índice único parcial, versão publicada é imutável) + `document_generations` (idempotente por `(template_version_id, origem)`, CHECK garantindo exatamente uma origem). Dependência circular (`document_templates.current_version_id` ↔ `document_template_versions`) resolvida com `ALTER TABLE` depois que as duas tabelas existem. Motor de merge de variáveis (`{{chave}}` → valor do contexto, nunca lança exceção, reporta variáveis ausentes) implementado como função pura testável — o parser Tiptap real e a renderização de PDF via Playwright/Chromium ficam para quando o editor visual e essa infra existirem (simplificação documentada: `editor_content.plainText` como stand-in).
|
||
|
||
Verificado: `pnpm turbo run typecheck build` (28/28), `pnpm --filter @eden/api test` (30/30, sem regressão). Container `eden-api` reconstruído; testado ao vivo via curl: template criado → draft criado → segundo draft rejeitado (409, invariante confirmada) → draft editado com placeholders → publicado (`current_version_id` atualizado) → documento gerado com merge de variáveis funcionando (`{{empresa}}`/`{{cliente}}`/`{{valor}}` substituídos corretamente) → regeneração com os mesmos parâmetros reaproveita o registro existente (idempotência confirmada, mesmo `id`). Trilha de auditoria conferida no Postgres.
|
||
|
||
### Pendências
|
||
Toda a Assinatura Eletrônica (eden.md §1.4-1.9/§5/§6/§7 — envelopes, signatários, OTP, hash-chain, verificação pública), geração real de PDF (Playwright/Chromium) + sanitização de HTML, editor visual Tiptap real (`renderToHtml`/`resolveDocument` com condicionais e repetição de tabela), seed dos 7 templates reais de produção da Handix, regra de idempotência completa com `force`/`extra_fields`.
|
||
|
||
---
|
||
|
||
## Fase 2/8 — Telas de negócio reais no EDEN Core (Empresas, Produtos, Revendas, CRM, Ofertas)
|
||
|
||
Data: 2026-09-03. Usuário pediu explicitamente para priorizar a parte web ("ja faca a parte web para eu ja ver como esta ficando"). 2 subagentes `eden-frontend` em paralelo, mesma sessão, construindo sobre o shell autenticado + Design System já prontos.
|
||
|
||
**Correção de instrução no meio da leva**: o usuário interrompeu para dizer "não esqueça de seguir o template" / "melhor usar ele" — as telas deviam seguir de perto o LAYOUT REAL do tema DreamsERP comprado, não só os tokens visuais abstratos já extraídos. Extraí do `tema_do_Eden.zip` as páginas React reais mais relevantes (`sales/salesQuotes*.tsx`, `crm/leads.tsx`/`deals*.tsx`, `inventory/products*.tsx`, `purchase/vendors.tsx`, `system/userManagement.tsx`, `ui-interface/{tables,forms}`) para dentro do worktree de cada agente (`.tmp/theme-reference/`, nunca versionado) e mandei mensagem de correção para os dois agentes já em execução — via `SendMessage`, não relançando a tarefa (no meio do processo cometi o erro de usar `Agent` em vez de `SendMessage` para a primeira correção, criando um agente duplicado; percebido e corrigido na hora com `TaskStop` antes de qualquer trabalho real ser feito por ele). Ambos incorporaram a referência do tema (estrutura de header/breadcrumb/tabela/formulário em cards) reescrita com `@eden/ui` + Tailwind puro — sem copiar JSX nem trazer dependências do tema (antd/Preline) para o projeto.
|
||
|
||
### Empresas / Produtos / Revendas (`apps/core-web/src/routes/{companies,products,resellers}/`)
|
||
|
||
CRUD completo (lista + criar/editar) para os 3 módulos, layout modelado em `purchase/vendors.tsx` (empresas/revendas) e `inventory/products.tsx` (produtos, com máscara de moeda para as 5 faixas de preço + implantação). Novo componente `Table<T>` em `packages/ui` (tabela genérica reutilizável, sem dependência de terceiro).
|
||
|
||
### CRM e Ofertas/Quotes (`apps/core-web/src/routes/{crm,quotes}/`)
|
||
|
||
Leads (lista, criar/editar, converter em oportunidade) e Oportunidades (lista, criar/editar) modelados em `crm/leads.tsx`/`dealsDetails.tsx`. Ofertas — o módulo mais rico: `QuotesListPage`, `QuoteFormPage` (revenda + cliente + período de contrato + fidelidade reduzida opcional + lista dinâmica de itens produto/quantidade + desconto opcional + condição de pagamento, mostra os totais calculados pelo servidor antes de ir para o detalhe) e `QuoteDetailPage` (resumo financeiro completo, aprovar/rejeitar quando pendente, fechar negócio quando ainda é orçamento) — modelados em `sales/salesQuotes.tsx`/`addSalesQuotes.tsx`/`salesQuotesDetails.tsx`. Novo componente `Select` em `packages/ui`.
|
||
|
||
**Achado real do próprio agente, corrigindo meu brief**: eu tinha dito no brief que as respostas de Leads/Opportunities eram snake_case (copiando o padrão do resto da API) — o agente verificou contra o código-fonte real (`packages/database/src/repositories/leads.ts`) e descobriu que esse módulo específico mapeia para **camelCase** na resposta (`mapLead`), diferente de Quotes/Companies/Products/Resellers (snake_case direto da linha do banco). Confirmado ao vivo via curl antes de integrar.
|
||
|
||
### Integração manual (os 2 agentes trabalharam em paralelo no mesmo `core-web`)
|
||
|
||
Um dos agentes não conseguiu `git merge` dentro do próprio sandbox e reconstruiu manualmente (leitura+escrita) o estado já existente do shell autenticado — comparei arquivo por arquivo contra `main` (`diff -rq`) para confirmar que a restauração foi fiel (nenhum componente restaurado divergiu) antes de aceitar qualquer coisa como "nova". `App.tsx`, `AuthenticatedLayout.tsx` (nav unificada, lógica de "ativo" por prefixo de path com borda de `/` para nunca casar `/companies` com `/companies-x`) e `packages/ui/src/index.ts` foram reconciliados manualmente juntando as rotas/exports dos dois agentes; `lib/format.ts` teve as duas versões mescladas (`formatDocument` de um lado, `formatDateBR` do outro).
|
||
|
||
Verificado: `pnpm turbo run typecheck build` (28/28). Containers `eden-api`/`eden-core` reconstruídos (o build em background travou duas vezes por algum motivo de recurso do ambiente — refeito em primeiro plano, funcionou); bundle de produção confirmado contendo todas as novas telas; login simulando requisição real de browser (`Origin: http://localhost:3001`) funcionando; listagens de Empresas/Produtos/Revendas retornando dados reais; criação de lead confirmando o formato camelCase ao vivo.
|
||
|
||
### Pendências
|
||
Client Registration (Cadastro de Cliente) ainda sem tela — `QuoteDetailPage` hoje exige colar manualmente um UUID para fechar negócio. Nenhuma tela de Contracts/Inventory/Support/HR-Timeclock/Fiscal/Billing/Documents ainda. `reseller-web`/`subscriber-web` continuam só com o shell de login (sem telas de negócio).
|
||
|
||
---
|
||
|
||
## Proxy same-origin dentro do próprio nginx dos 3 frontends
|
||
|
||
Data: 2026-09-03. Usuário pediu para "configurar o proxy". Como a API não tem domínio público próprio (decisão do operador) e o proxy externo que termina SSL nos 3 domínios é gerenciado fora deste repositório (sem acesso daqui, e o guard de segurança do projeto bloqueia acesso a caminhos fora do projeto/`/etc`), a solução implementada foi dentro do que o repo já controla: `infra/docker/nginx-spa.conf` (usado pelos 3 containers de frontend) ganhou um `location /api/` que faz `proxy_pass` para `eden-api:8080` — os 2 containers já estão na mesma rede Docker (`eden_net`), então o roteamento é interno, sem tocar em nada externo. `resolver 127.0.0.11` (DNS embutido do Docker) usado com `proxy_pass` via variável para evitar o problema clássico de nginx cachear o IP do upstream para sempre.
|
||
|
||
Com isso, o browser batendo em `https://dev-edencore.falehandix.com.br/api/v1/...` (uma vez que o proxy externo do operador já roteia o domínio inteiro para a porta 3001, como já estava configurado) chega ao nginx do container, que repassa para a API interna — fluxo 100% same-origin do ponto de vista do browser, sem CORS.
|
||
|
||
Testado ao vivo: `POST /api/v1/auth/login` com credenciais reais funcionando através da porta 3001 (`eden-core`), 3002 (`eden-parceiros`) e 3003 (`eden-assinante`) — os 3 containers reconstruídos e confirmados.
|
||
|
||
---
|
||
|
||
## Cadastro de Cliente e Contratos no EDEN Core
|
||
|
||
Data: 2026-09-03. Continuação direta da leva anterior (sem parar para autorização, a pedido do usuário). 2 subagentes `eden-frontend` em paralelo, mesmo padrão de referência do tema DreamsERP (`customers.tsx`/`contacts.tsx` para Cadastro de Cliente; `salesOrders*.tsx` para Contratos — a mais próxima estruturalmente, já que "pedido de venda" no tema tem exatamente o mesmo shape de itens+partes+status+histórico de um Contrato).
|
||
|
||
### Cadastro de Cliente (`apps/core-web/src/routes/client-registrations/`)
|
||
|
||
Lista, criação (fluxo direto: tipo PF/PJ + portabilidade), e uma tela de detalhe rica com edição por seção (Status, Endereço, Contatos principal/financeiro/técnico, Dados PF ou PJ condicionalmente — CPF/CNPJ sempre somente-leitura, nunca editável por este endpoint —, Portabilidade quando aplicável) mais uma sub-seção de Sócios (só PJ: lista + adicionar + editar os 3 campos administrativos).
|
||
|
||
**Fechou a pendência da leva anterior**: `QuoteDetailPage` não pede mais para colar um UUID à mão para fechar negócio — agora é um `<select>` real populado por `GET /v1/client-registrations` (código + nome), com atalho para criar um cadastro novo em outra aba quando a lista está vazia.
|
||
|
||
### Contratos (`apps/core-web/src/routes/contracts/`)
|
||
|
||
Lista, criação (revenda/empresa opcionais, período, fidelidade reduzida opcional, itens produto+quantidade, partes contratada/contratante) e detalhe rico: resumo financeiro, aviso explícito quando `fidelity_period` está preenchido mas `fidelity_approved=false` ("sem efeito legal ainda"), tabela de itens, lista de partes, histórico de status, e botões de transição — **só os alvos válidos a partir do status atual** (mesma tabela de transições do backend replicada no frontend só para guiar a UI; o servidor segue sendo a única fonte de verdade, confirmado com 409 num teste de transição inválida). `QuoteDetailPage` ganhou um link "Criar contrato a partir desta oferta" quando `deal_status === "fechado"`, pré-preenchendo `quoteId`/`clientRegistrationId` via query string.
|
||
|
||
### Integração
|
||
|
||
Ambos os agentes fizeram fast-forward do próprio branch para `main` antes de começar (mesmo padrão da leva anterior), então os diffs vieram limpos direto sobre o commit mais recente. Único ponto de reconciliação manual: os dois (mais o de Cadastro de Cliente) tocaram `QuoteDetailPage.tsx`/`App.tsx`/`AuthenticatedLayout.tsx` — mesclados um de cada vez, sempre confirmando `typecheck`/`build` limpo entre cada mesclagem antes de aceitar a próxima.
|
||
|
||
Verificado: `pnpm turbo run typecheck build` (28/28); containers `eden-api`/`eden-core` reconstruídos; todo o fluxo testado ao vivo **através do proxy same-origin da porta 3001** (não direto na 8080, simulando exatamente o caminho real do browser): login, criação de cadastro de cliente, criação de contrato, e transição de status inválida corretamente rejeitada com 409.
|
||
|
||
### Pendências
|
||
`ContractFormPage` não tem edição pós-criação (só criação + transição de status); nenhuma tela ainda para Inventory/Support/HR-Timeclock/Fiscal/Billing/Documents; `reseller-web`/`subscriber-web` seguem só com o shell de login.
|
||
|
||
---
|
||
|
||
## Estoque e Faturamento no EDEN Core
|
||
|
||
Data: 2026-09-03. Continuação direta, sem parar para autorização (a pedido do usuário). 2 subagentes `eden-frontend` em paralelo, referência do tema DreamsERP (`inventory/warehouse.tsx`/`inventoryList.tsx`/`manageStock.tsx` para Estoque; `sales/recurringInvoices*.tsx` para Faturamento — fatura recorrente no tema mapeia quase 1:1 para assinatura+fatura de Billing).
|
||
|
||
### Estoque (`apps/core-web/src/routes/inventory/`)
|
||
|
||
Armazéns (CRUD), Ativos (lista com ações contextuais por status — só mostra Reservar/Instalar/Devolver quando fazem sentido para o status atual, mesma disciplina de `ContractDetailPage`), formulário de entrada de ativo, detalhe com histórico de movimentos, ledger de movimentos (só leitura) e reservas genéricas.
|
||
|
||
**Achado real do próprio agente, documentado no código**: `installAsset`/`returnAsset` gravam o `stock_movements` com `reference_type: 'contract'`, não `'serialized_asset'` — então filtrar o ledger por `referenceId=<assetId>` nunca traz o histórico de instalação/devolução daquele ativo, só `purchase_in`/`reservation`. O agente contornou buscando também os movimentos do `current_contract_id` do ativo quando existe, e documentou que isso é uma lacuna real do backend (fora do escopo desta tarefa de frontend, registrado como pendência).
|
||
|
||
### Faturamento (`apps/core-web/src/routes/billing/`)
|
||
|
||
Assinaturas (criar a partir de um contrato, valor mensal pré-preenchido do contrato mas editável com aviso), Fechamentos (criar por ano/mês/dia de cobrança, aviso de idempotência), detalhe do fechamento com tabela de faturas + total e botão consolidar com confirmação em duas etapas, lista de faturas (só leitura, filtro por status/período/revenda).
|
||
|
||
### Integração
|
||
|
||
Um dos dois agentes restaurou a base manualmente (sem `git merge`) — comparação byte a byte (`diff -rq`) contra `main` confirmou zero desvio na restauração antes de aceitar qualquer coisa como "novo". `App.tsx`/`AuthenticatedLayout.tsx` reconciliados manualmente mais uma vez, confirmando `typecheck` limpo a cada mesclagem.
|
||
|
||
Verificado: `pnpm turbo run typecheck build` (28/28); containers `eden-api`/`eden-core` reconstruídos; testado ao vivo através do proxy same-origin (porta 3001): criação de armazém confirmada, trilha de auditoria conferida no Postgres. Os próprios agentes já haviam confirmado ao vivo, contra a API real, antes de entregar: invariante de serial duplicado (409), ciclo reservar→instalar→devolver, e o fluxo completo de billing (gerar→regenerar idempotente→consolidar→409 em nova tentativa).
|
||
|
||
### Pendências
|
||
Nenhuma tela ainda para Support/HR-Timeclock/Fiscal/Documentos; `reseller-web`/`subscriber-web` seguem só com o shell de login; a lacuna de `reference_type` no ledger de movimentos de instalação/devolução (achado acima) precisa de correção no backend numa leva futura.
|
||
|
||
---
|
||
|
||
## Suporte e Fiscal no EDEN Core
|
||
|
||
Data: 2026-09-04. Continuação direta, sem parar para autorização. 2 subagentes `eden-frontend` em paralelo, referência do tema DreamsERP (`support/tickets.tsx`/`ticketDetails.tsx`/`slaManagement.tsx` para Suporte; `inventory/categories.tsx`/`units.tsx`/`brands.tsx` para os catálogos fiscais — padrão "catálogo simples" quase idêntico).
|
||
|
||
### Suporte (`apps/core-web/src/routes/support/`)
|
||
|
||
Lista de tickets, criação, e detalhe rico: painel de SLA (prazos de primeira resposta/resolução, indicador cumprido/violado/pendente, aviso "SLA pausado" quando aplicável), thread de comentários públicos/internos com formulário de resposta, histórico de status (timeline), transições limitadas aos alvos válidos com campos condicionais de causa raiz/resolução ao fechar. Tradeoff documentado pelo próprio agente: a listagem não traz o `slaTimer` (só o detalhe traz), então o indicador de SLA vencido na lista busca o detalhe em background só para tickets não-terminais.
|
||
|
||
### Fiscal (`apps/core-web/src/routes/fiscal/`)
|
||
|
||
NCM/CFOP/Municípios (CRUD simples com formulário inline) e Naturezas de Operação (só leitura, as 11 seedadas). Nova seção "Fiscal" dentro de `ProductFormPage` (só ao editar um produto existente): histórico de perfis fiscais do produto com aviso claro de que criar um novo perfil do mesmo componente substitui automaticamente o anterior — confirmado ao vivo pelo próprio agente antes de entregar.
|
||
|
||
### Integração
|
||
|
||
Os dois agentes restauraram a base manualmente (sem `git merge`) — `diff -rq` contra `main` confirmou zero desvio de restauração nos dois casos antes de aceitar qualquer arquivo como novo. Reconciliação manual de `App.tsx`/`AuthenticatedLayout.tsx`/`ProductFormPage.tsx`/`format.ts` (este último ganhou `formatDateTimeBR`, puramente aditivo).
|
||
|
||
Verificado: `pnpm turbo run typecheck build` (28/28); containers `eden-api`/`eden-core` reconstruídos; testado ao vivo através do proxy same-origin (porta 3001): criação de ticket e listagem de naturezas de operação; trilha de auditoria conferida no Postgres.
|
||
|
||
### Pendências
|
||
Nenhuma tela ainda para HR-Timeclock/Documentos; `reseller-web`/`subscriber-web` seguem só com o shell de login. Com esta leva, o EDEN Core tem UI real para praticamente todo o backend construído até agora, exceto HR-Timeclock e Documentos.
|
||
|
||
---
|
||
|
||
## Documentos e Ponto Eletrônico no EDEN Core — cobertura completa do backend construído até agora
|
||
|
||
Data: 2026-09-04. Última leva desta sequência, sem parar para autorização. 2 subagentes `eden-frontend` em paralelo, referência do tema (`documents/documentRepository.tsx`/`documentWorkflows.tsx` para Documentos; `system/integrationsSettings.tsx` para dispositivos de ponto, `hrm/attendance.tsx` para registros AFD).
|
||
|
||
### Documentos (`apps/core-web/src/routes/documents/`)
|
||
|
||
Templates com ciclo draft→publish (edição por textarea de texto com placeholders `{{variavel}}`, já que o editor visual Tiptap não existe nesta leva de backend), preview do HTML publicado, geração de documento com extração automática das variáveis do template via regex para gerar um campo de formulário por variável, mostrando `missingVariables` quando aplicável.
|
||
|
||
**Achado real do próprio agente, corrigindo meu brief**: a regra de validação de `key` do template aceita minúsculas+dígitos+underscore, não só minúsculas+underscore como eu tinha descrito — corrigido no frontend antes de entregar, confirmado contra a mensagem de erro real do backend.
|
||
|
||
### Ponto Eletrônico — fundação (`apps/core-web/src/routes/hr-timeclock/`)
|
||
|
||
Dispositivos Control iD (CRUD, senha nunca retornada — só `hasCredential`, campo de senha vazio na edição significa "manter atual"), import de arquivo AFD (decodificação ISO-8859-1 explícita, não UTF-8, documentada no código — detalhe real e não óbvio), lista só-leitura de registros AFD (sem nenhuma ação de edição, imutabilidade legal respeitada na UI também).
|
||
|
||
**2 discrepâncias reais encontradas pelo próprio agente entre meu brief e o comportamento real da API, corrigidas antes de entregar**: reimportar o mesmo arquivo retorna **409** com corpo estruturado (`{message, alreadyImported, importId, status}`), não 200 como eu tinha descrito; `GET /hr-timeclock/afd-records` devolve um envelope paginado (`{records, total, limit, offset}`), não um array simples.
|
||
|
||
### Integração
|
||
|
||
Ambos os agentes reconciliados manualmente em `App.tsx`/`AuthenticatedLayout.tsx` (Documents entrou primeiro, HR-Timeclock por cima). Verificado: `pnpm turbo run typecheck build` (28/28); containers `eden-api`/`eden-core` reconstruídos; testado ao vivo através do proxy same-origin (porta 3001): criação de dispositivo confirmando ausência de senha na resposta, criação de template; trilha de auditoria conferida no Postgres.
|
||
|
||
### Estado do EDEN Core ao final desta sequência
|
||
|
||
UI real para: Empresas, Produtos (+ perfil fiscal), Revendas, CRM (Leads/Oportunidades), Ofertas, Cadastro de Cliente, Contratos, Estoque, Faturamento, Suporte, Fiscal, Documentos (fundação), Ponto Eletrônico (fundação). Cobertura completa de todo o backend construído nesta sessão, exceto Assinatura Eletrônica (deliberadamente não construída — módulo próprio, ainda não existe no backend) e apuração/fechamento de ponto (idem). `reseller-web`/`subscriber-web` seguem só com o shell de login — nenhuma tela de negócio própria ainda.
|
||
|
||
---
|
||
|
||
## E-mail transacional: convite de usuário + recuperação de senha
|
||
|
||
Data: 2026-09-04. Usuário perguntou onde configurar as credenciais de e-mail (`noreply@handix.com.br`) — confirmado que é via `.env` (`SMTP_HOST/PORT/USER/PASS/FROM`, já existiam desde a Fase 1, sem consumidor real). Pedido em seguida: ligar o envio de verdade e construir recuperação de senha na tela de login.
|
||
|
||
### Backend (`apps/api/src/auth/`, `apps/api/src/users/`) — implementado diretamente (fluxo de autenticação, não delegado)
|
||
|
||
- `@eden/integrations` adicionado como dependência real de `apps/api` (só existia como biblioteca solta até agora).
|
||
- `UsersService.invite()` agora envia e-mail de verdade com a senha temporária (mantendo o retorno na resposta da API também — dupla entrega, eden.md §4.8) via o adapter SMTP existente (`packages/integrations/src/smtp/mailer.ts`, já era fail-soft: nunca quebra o fluxo de convite se o SMTP falhar ou não estiver configurado).
|
||
- Novo fluxo de recuperação de senha: `POST /v1/auth/forgot-password` (`{email}` → sempre a mesma resposta genérica, cadastrado ou não — mesma disciplina anti-enumeração do login) e `POST /v1/auth/reset-password` (`{token, newPassword}`). Token de 256 bits, só o hash SHA-256 é persistido (`users.reset_token_hash`/`reset_token_expires_at`, colunas que já existiam desde a migration baseline da Fase 1 mas nunca tinham sido usadas), validade de 1h, uso único (limpo após consumido), e **revoga todas as sessões ativas do usuário** ao completar a redefinição (senha mudou → toda sessão anterior morre).
|
||
|
||
### Frontend (`apps/core-web`)
|
||
|
||
Link "Esqueci minha senha" no `LoginPage`, nova `ForgotPasswordPage` (mensagem genérica de sucesso, nunca confirma se o e-mail existe) e `ResetPasswordPage` (token vem da query string do link do e-mail, confirmação de senha, redireciona pro login com banner de sucesso ao concluir).
|
||
|
||
### Verificação
|
||
|
||
Teste de integração novo (`test/password-reset.spec.ts`, 3 testes) — usa `vi.mock` para interceptar `sendMail` e extrair o token real do corpo do e-mail (a única forma de testar o caminho feliz de ponta a ponta, já que o token nunca é devolvido na resposta da API, só por e-mail): confirma resposta genérica idêntica para e-mail cadastrado/não-cadastrado, token bogus rejeitado, e o ciclo completo — sessão anterior revogada, senha antiga para de funcionar, nova senha funciona, token não pode ser reaproveitado. Suíte completa: 33/33 verdes (30→33). `pnpm turbo run typecheck build` (28/28). Testado ao vivo através do proxy same-origin: anti-enumeração confirmada com curl real, token gravado no Postgres, token bogus rejeitado (400), telas novas confirmadas no bundle de produção.
|
||
|
||
### Pendências
|
||
SMTP ainda não configurado no `.env` real (aguardando credenciais do usuário) — o fluxo todo já funciona corretamente sem SMTP configurado (fail-soft), só não envia e-mail de verdade ainda. Sem rate-limiting no `forgot-password` (mesma lacuna geral já registrada para outros endpoints de auth).
|
||
|
||
---
|
||
|
||
## Renderização real de PDF (Chromium) — geração de Documentos ligada de ponta a ponta
|
||
|
||
Data: 2026-09-04. Usuário pediu para clonar o legado OrçaFácil (referência read-only em `.tmp/orcafacil-reference/`, fora do git — caminho pedido originalmente (`/opt/orcafacil`) foi bloqueado pelo guard-hook de segurança do projeto por ficar fora de `/opt/eden`; substituído por um local equivalente dentro do projeto em vez de contornar o guard) para estudar como o legado implementa edição de contrato, assinatura eletrônica e o PDF de oferta compatível com ANATEL. Achado principal (via subagente `fork`, sem poluir o contexto principal com código bruto): o legado usa Chromium real (Playwright) server-side só para os templates ricos de `document_templates`; o PDF de oferta ANATEL e o certificado de assinatura usam um pipeline client-side separado (`html2canvas`+`jsPDF`) — não recomendado para portar. Direção confirmada pelo usuário: unificar tudo em Chromium server-side, começando pela geração de Documentos (o "PDF do backoffice") que ainda não produzia PDF real nenhum.
|
||
|
||
### `packages/integrations/src/pdf/renderer.ts` (novo)
|
||
|
||
Port do `chromiumPdfRenderer.js` do legado, mesma disciplina de segurança/confiabilidade:
|
||
- SSRF impossível por construção: conteúdo sempre via `page.setContent()` (nunca `page.goto()`, não existe URL para o chamador controlar) + `page.route('**/*', route => route.abort())` como reforço — todo asset (imagem, fonte) precisa já vir como `data:` URI, não há fallback de rede.
|
||
- Browser único reaparoveitado entre chamadas, com relançamento automático se cair (`isConnected()` checado a cada uso).
|
||
- Semáforo de concorrência (`PDF_MAX_CONCURRENCY`, default 3) e timeout duro (`PDF_RENDER_TIMEOUT_SECONDS`, default 15s) — novas variáveis em `packages/config/src/env-schema.ts`.
|
||
- `--no-sandbox` no lançamento do Chromium (necessário rodando como root no container, sem `USER` no Dockerfile) — risco residual mitigado pelo bloqueio de rede acima (conteúdo é sempre nosso, nunca de terceiro).
|
||
- Gotcha documentado no código (igual ao legado): `@page { margin: 0 }` no CSS do próprio documento sobrescreve silenciosamente a opção `margin` passada para `page.pdf()`.
|
||
|
||
### Infraestrutura
|
||
|
||
- `infra/docker/Dockerfile.node`: novo passo condicional (`if [ "$APP_NAME" = "api" ]`) que roda `playwright install --with-deps chromium` só na imagem da API — o worker não renderiza PDF, fica sem o peso extra.
|
||
- `compose.yaml`: novo serviço `eden-minio` (imagem oficial `minio/minio` — a variante Bitnami parou de publicar `latest` gratuita, tentativa inicial falhou no pull) + `eden-minio-init` (one-shot `minio/mc` que cria o bucket via `mc mb --ignore-existing`, já que a imagem oficial não tem criação automática de bucket por env var). Mesmo racional do Redis já documentado no compose: container próprio de storage de objeto só entra agora porque a geração de PDF é o primeiro consumidor real (S3 existia como adapter desde a Fase 1, nunca tinha um caller). Credenciais MinIO geradas com `openssl rand` e adicionadas ao `.env` local.
|
||
|
||
### `apps/api/src/documents/` — geração agora produz PDF real
|
||
|
||
`DocumentsService.createGeneration()`: mescla variáveis no `html_content` da versão publicada (antes mesclava só o texto puro de `editor_content.plainText`, sem gerar PDF nenhum — `pdf_s3_key`/`pdf_sha256`/`pdf_size_bytes` sempre nulos), renderiza via `renderPdf`, calcula SHA-256, sobe pro S3 (`documents/generations/<uuid>.pdf`) e persiste os três campos. Checagem de idempotência (`findFinalGeneration`, exportado do repositório) roda ANTES de renderizar — uma geração deduplicada nunca aciona Chromium/S3 à toa. Novo endpoint `GET /v1/documents/generations/:id/pdf` (`StreamableFile`, `Content-Type: application/pdf`) para baixar o PDF gerado.
|
||
|
||
### Verificação
|
||
|
||
Teste de integração novo (`test/documents-pdf.spec.ts`, 3 testes, `@eden/integrations` mockado como no `password-reset.spec.ts` — evita depender de um Chromium/MinIO real rodando na suíte): confirma que a geração chama o renderer com a variável já resolvida, que o hash/tamanho persistidos batem com o buffer enviado ao S3, que uma geração duplicada não re-renderiza, e que o download stream a chave certa. Suíte completa: 36/36 verdes (33→36). `pnpm turbo run typecheck build --force` (28/28, forçado por precaução após editar `env-schema.ts`/pacotes compartilhados). Testado ao vivo através do container `eden-api` real (não só via proxy): Chromium instalado de fato na imagem (build ~4min, download de ~300MB), bucket criado no MinIO, geração de PDF de ponta a ponta via curl — SHA-256 do arquivo baixado bate byte a byte com o `pdf_sha256` gravado no Postgres.
|
||
|
||
### Pendências
|
||
PDF de Oferta/Orçamento compatível com ANATEL (Resolução 765/2023) ainda não construído — próximo item natural desta sequência, usando o mesmo `renderPdf`. Módulo de Assinatura Eletrônica completo (envelopes/signers/OTP/hash-chain) ainda não iniciado — item maior, sensível o suficiente para merecer leva própria dedicada, não apressada. Regras de negócio do legado em torno de edição/trava de Orçamento-como-Contrato (lock ao vincular `client_registration_id`, override só por `super_admin`) ainda não avaliadas contra o modelo de Contrato do EDEN (ADR-0006).
|
||
|
||
---
|
||
|
||
## Anexo de conformidade ANATEL/RGC no PDF de Oferta (Quotes)
|
||
|
||
Data: 2026-09-04. Continuação direta da leva anterior — usuário confirmou que a parte de conteúdo regulatório precisava ser exata, não aproximada de memória. Dois subagentes `fork` extraíram o conteúdo verbatim do legado antes de qualquer código ser escrito: (1) `BackofficeAnatelPDFTemplate.jsx` (texto legal, fórmulas computadas, ordem dos campos), (2) o schema real de `service_type`/`metered`/`tariff_rates`/`has_ldi` em `products` (baseline-schema.js).
|
||
|
||
### Decisão de escopo confirmada com o usuário
|
||
|
||
O anexo do legado depende de `service_type`/`metered`/`tariff_rates`/`has_ldi` por item — campos que a migration `1788438907074_products.cjs` **deliberadamente** excluiu ("legacy telecom-billing specifics que belong to a later phase"). Perguntei ao usuário como proceder (gerar conteúdo regulatório sobre uma base incompleta era um risco real de compliance) — escolheu **expandir o schema primeiro**, com fidelidade total ao legado incluindo a seção de tarifação STFC.
|
||
|
||
### Schema (`packages/database/migrations/1788525270647_products-service-type-tariff.cjs`)
|
||
|
||
`products` ganha `service_type` (CHECK nos 4 valores do legado: STFC/Serviço PABX/SVA (locação)/Serviço software), `metered`, `minutes_allowance`, `tariff_rates` (jsonb, formato `{TIPO: {normal, reduced}}`), `has_ldi`. `quotes` ganha `sales_rep_name` (impresso no bloco de assinatura). Novo pacote compartilhado `packages/domain-shared/src/telecom/service-types.ts` (catálogo de tipos, legendas de tarifação verbatim do legado, `formatCPF`/`formatCNPJ` adicionados a `br-documents.ts`) — consumido por `@eden/database`, `apps/api` e `apps/core-web`.
|
||
|
||
**Snapshot, não referência viva**: `QuotesService.buildItems()` copia `service_type`/`metered`/`minutesAllowance`/`tariffRates`/`hasLdi` do produto para o item no momento da criação da oferta (mesmo comportamento do legado, confirmado pelo subagente) — editar o produto depois nunca muda retroativamente a disclosure regulatória de uma oferta já emitida.
|
||
|
||
### `apps/api/src/quotes/anatel-offer-template.ts` (novo)
|
||
|
||
Gera o HTML completo do anexo — todos os campos do legado portados com fidelidade: Prazo de Comercialização (10 dias corridos a partir de `quote.updated_at`, rótulo exato do art. 3º XI, confirmado que NÃO é "Prazo de Oferta"), Prazo de Vigência/Permanência, Multa por Rescisão Antecipada (usa `computeSavings().beneficioTotalFidelidade` — `pricing.ts` já tinha essa fórmula pronta desde a leva de Quotes, nunca usada até agora), Critério/Data-base de Reajuste, tabela de itens agrupada por `service_type`, seção de Tarifação (LC/LDN/VC1/VC2/VC3/LDI, só renderiza para itens `metered`, legenda verbatim), condição especial, resumo financeiro, assinaturas. Placeholder `[A DEFINIR]` para CPF/CNPJ do cliente quando a oferta ainda não tem `client_registration_id` vinculado (nunca inventa um documento).
|
||
|
||
**Simplificação deliberada e documentada**: sem o split automático software/não-software do legado (`splitQuoteByServiceType.js`) — esta leva sempre gera um único PDF cobrindo todos os itens da oferta. Renderizado sempre ao vivo (nunca cacheado como os Documents), porque o conteúdo regulatório deriva de `updated_at` — um PDF salvo ficaria defasado assim que a oferta fosse editada de novo.
|
||
|
||
Novo endpoint `GET /v1/quotes/:id/anatel-pdf` — 400 se a oferta não tiver nenhum item STFC (o documento inteiro existe para essa disclosure, art. 21 §3º do RGC).
|
||
|
||
### Frontend
|
||
|
||
`ProductFormPage`: novo card "Regulatório (ANATEL/RGC)" — tipo de serviço, franquia de minutos, tarifado por minuto, LDI, tabela de tarifas (só aparece se "tarifado por minuto" marcado). `QuoteFormPage`: campo "Responsável comercial". `QuoteDetailPage`: botão "Baixar PDF ANATEL" (só aparece se a oferta tiver item STFC, mesma regra do backend) + campo "Responsável comercial" no resumo.
|
||
|
||
### Verificação
|
||
|
||
Teste de integração novo (`test/anatel-pdf.spec.ts`, 3 testes, `renderPdf` mockado como nos outros specs de PDF — o que está sob teste é o conteúdo regulatório computado, não o Chromium em si): confirma snapshot correto dos campos do produto no item, todos os campos regulatórios computados corretos no HTML (datas, fórmula da multa, tabela de tarifação, texto VoIP, campos fixos), e 400 para oferta sem item STFC. Suíte completa: 39/39 verdes (36→39). `pnpm turbo run typecheck build --force` (28/28).
|
||
|
||
---
|
||
|
||
## Trava de edição de Contrato — lacuna real entre ADR-0006 e a implementação
|
||
|
||
Data: 2026-09-04. Usuário pediu para avaliar as regras de edição/trava de Orçamento-como-Contrato do legado contra o modelo de Contrato do EDEN (ADR-0006) antes de seguir para Assinatura Eletrônica.
|
||
|
||
### Achado
|
||
|
||
`docs/adr/0006-contract-first-class.md` promete explicitamente: "fluxo de fechamento de oferta → geração de contrato preserva a trava equivalente [...] transição de estado do contrato que também impede edição desconforme, exceto por papel com permissão de correção auditada equivalente ao super_admin do legado". Essa trava **nunca foi implementada**: `ContractsService.addItem`/`addParty` não tinham nenhuma checagem de status ou papel — qualquer ator com a permissão genérica `contracts:edit` podia adicionar item/parte a um contrato em **qualquer** status, incluindo `cancelled`/`terminated`/`expired`, ou um `active` já assinado. A infraestrutura para a regra correta já existia (`resolveEffectiveFidelity()`, a máquina de estados `CONTRACT_STATUS_TRANSITIONS`) — só a trava de edição em si nunca foi conectada.
|
||
|
||
### Correção (`apps/api/src/contracts/contracts.service.ts`)
|
||
|
||
Novo `assertEditable()`, mesmo formato do `assertNotLocked` de Quotes: `status === 'draft'` é o único estado livre para adicionar item/parte; qualquer outro status bloqueia (409) para quem não é `super_admin`; `super_admin` pode seguir editando, mas o evento de auditoria fica marcado (`contract.item_added_override`/`contract.party_added_override`, com `overrode: true` no metadata) em vez de ser indistinguível de uma edição normal — a "correção auditada" que o ADR pedia.
|
||
|
||
Sem mudança de frontend: `ContractDetailPage` nunca chamou `addContractItem`/`addContractParty` (a função existe no client mas não é usada em nenhuma tela) — a lacuna era puramente de backend.
|
||
|
||
### Verificação
|
||
|
||
Teste de integração novo (`test/contracts-edit-lock.spec.ts`, 3 testes, papel throwaway com `contracts:edit`+`view` mas sem `super_admin` — mesmo padrão do `role-weight.spec.ts`): confirma que adicionar item funciona em `draft`, que um ator não-super_admin é bloqueado (409) assim que o contrato sai de `draft`, e que `super_admin` continua conseguindo (override). Suíte completa: 42/42 verdes (39→42). `pnpm turbo run typecheck build --force` (28/28).
|
||
|
||
### Pendências
|
||
`contract_versions` (snapshot-on-signature, invariante nº4 do ADR-0006) segue deliberadamente adiado — precisa do desenho do fluxo de assinatura para não virar uma tabela vazia. `contract_amendments` (forma correta de alterar um contrato já assinado, em vez do override de super_admin) também segue adiado — depende de `contract_versions` primeiro.
|