Files
eden/docs/progress.md
Matheus (Handix) 53c3031775 feat(nav): menu "Cadastro" agrupando Empresas/Produtos/Revendas/Clientes
Empresas, Produtos (com toda a árvore de submenus), Revendas e Clientes
saem do nível 1 do menu e passam a viver dentro de um novo item
"Cadastro". Isso exercita 3 níveis de submenu aninhado pela primeira
vez (Cadastro > Produtos > Grupos > Subgrupos) — a recursão do AppShell
já suportava isso estruturalmente, mas estava documentada como não
testada; verificado ao vivo via Playwright que funciona corretamente
(auto-expande só o caminho ativo em cada nível).

Suíte: 95/95. pnpm turbo run typecheck build: 28/28.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 12:20:24 -03:00

109 KiB
Raw Blame History

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 listao tentar rodarpnpm 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.


Módulo de Assinatura Eletrônica — completo (envelopes, signatários, OTP, hash-chain, finalização, verificação pública)

Data: 2026-09-04. Usuário pediu para seguir com a "questão da assinatura" — última peça do estudo do OrçaFácil. Dado o tamanho e a sensibilidade jurídica/criptográfica do módulo, os algoritmos de segurança (hash-chain, OTP, máquina de estados) foram re-verificados contra o código-fonte real do legado por um subagente fork dedicado, cruzado com eden.md módulo 4 (que já documentava o módulo com grande precisão, escrito numa leva anterior) — nenhuma linha de crypto foi escrita de memória.

Schema (packages/database/migrations/1788536022387_signatures.cjs)

5 tabelas: signature_envelopes (19 estados, company_id obrigatório — diferente de contracts.company_id, que é opcional — porque createEnvelope recusa criar sem representante legal cadastrado), signature_signers (snapshot, nunca referência viva), signature_documents (cópia congelada + SHA-256), signature_otp_challenges, signature_audit_events (hash-chain, append-only via a mesma GUC eden.allow_audit_mutation que já protege o audit_log genérico desde a Fase 1 — reaproveitada, não duplicada). packages/database/src/hash-chain.ts novo, compartilhado entre audit_log e signature_audit_events (a canonicalização recursiva já existia em audit.ts, só foi extraída).

Três adaptações deliberadas ao EDEN, documentadas no cabeçalho da migration (tudo o mais — os 19 estados, a fórmula do hash-chain, o algoritmo de OTP — é fidelidade exata ao legado):

  1. signature_documents.source_generation_id referencia document_generations (o pipeline de PDF já construído nesta sessão) em vez do source_attachment_id do legado — EDEN não tem um módulo de upload de anexo genérico ainda.
  2. reseller_id em vez de reseller_registration_id — EDEN modela revenda como uma tabela só.
  3. Só existe a versão 2 (correta) da canonicalização do hash — a v1 do legado existe lá só para tolerar hashes já gravados antes de uma correção de bug real; EDEN nunca teve essa v1.

Certificado de conclusão — server-side em vez de client-side

O legado desenha o certificado no navegador do backoffice (React → html2canvas → jsPDF) e recebe o PDF pronto via upload multipart em finalizeEnvelope. O EDEN já tem Chromium real server-side (construído para Documents/ANATEL) — apps/api/src/signatures/certificate-template.ts gera o HTML (conteúdo/texto legal portado verbatim, incluindo o aviso de que não é certificação ICP-Brasil) e renderiza no próprio servidor, eliminando o upload multipart inteiramente. Decisão deliberadamente limitada a essa troca técnica: os dois passos autenticados de finalização (seal-checkpoint depois finalize) foram mantidos exatamente como no legado — não virou automático mesmo sendo tecnicamente possível, porque esse checkpoint humano antes de uma finalização irreversível é comportamento de negócio, não um detalhe do desenho client-side do certificado.

Fluxo (apps/api/src/signatures/)

EnvelopeService (criação — congela documentos com cópia S3 + SHA-256 recalculado, nunca confia cegamente no hash já gravado —, envio de convite, cancelamento, exclusão física só antes de SIGNED, verificação da cadeia), PublicSignatureService (fluxo do signatário só por token — nunca por envelopeId/signerId do corpo da requisição —, cada ação exige o estado anterior correto: confirmar leitura de cada documento → confirmar dados → aceitar termos → solicitar/validar OTP → assinar), EnvelopeFinalizeService (os dois passos), PublicVerifyService (só para COMPLETED, recomputa envelope_hash ao vivo e compara, nomes mascarados). Nota de design importante preservada do legado: o envelope salta direto de SENT para SIGNED quando o último signatário termina — o progresso fino (VIEWED..OTP_VERIFIED) vive inteiramente em signature_signers.status, nunca é espelhado passo a passo no envelope.

Verificação

Suíte de testes nova e grande: signature-hash-chain.spec.ts (15 testes, puros, sem banco — canonicalização, determinismo do hash, máquina de estados, OTP, token) e signature-envelope-flow.spec.ts (13 testes, ciclo de vida completo via HTTP real: criação com signatário contratada automático, recusa sem representante legal, convite, os dois signatários passando pelo fluxo público inteiro incluindo código OTP errado/bloqueio/uso único, SENT→SIGNED no último signatário, cadeia de auditoria válida, seal-checkpoint, finalize com merge real via pdf-lib — confirmado contando páginas do PDF final —, public-verify com integridade válida e nomes mascarados, e proteção contra exclusão pós-assinatura). renderPdf/uploadObject/getObjectStream/sendMail mockados (S3 em memória); mergePdfs/getPdfPageCount NÃO mockados — o merge de PDF de verdade roda contra a saída (pequena, mas válida) do renderPdf mockado. Suíte completa: 70/70 verdes (42→70). pnpm turbo run typecheck build --force (28/28).

Pendências

Rate limiting nas rotas públicas (por IP e por token, eden.md §6.8/§10.3) — lacuna conhecida, mesmo padrão de outros endpoints públicos deste projeto que também não têm ainda (ex. forgot-password). UI de frontend para todo o módulo (criação/envio/acompanhamento de envelope no backoffice, tela pública de assinatura do signatário, página pública de verificação) — não construída nesta leva, só o backend. DECLINED (recusa de assinatura) é um estado reservado no schema mas sem rota que o produza — mesma lacuna do legado, nunca foi implementado lá também.


Rate limiting em todas as rotas públicas — lacuna fechada (eden-security)

Data: 2026-09-17. Essa lacuna estava registrada em pelo menos três lugares diferentes (docs/security/threat-model.md §2.1/§2.2, e as pendências de MFA/forgot-password/assinatura acima): nenhuma rota sem sessão tinha qualquer limite de requisições. Escopo: POST /v1/auth/{login,mfa/challenge,forgot-password,reset-password,refresh,logout} (logout não estava na lista original do pedido, mas também não tem JwtAuthGuard — incluído por completude), GET /v1/public-signature/:token, GET /v1/public-signature/:token/documents/:documentId/pdf, POST /v1/public-signature/:token/{documents/:documentId/confirm-read,confirm-data,consent,otp/request,otp/verify,sign} e GET /v1/public-verify/:verificationId.

Decisão: guard/decorator próprio, não @nestjs/throttler

Sem Redis no ambiente (compose.yaml: "eden-redis (quando houver job real)" — ainda não existe) e API roda como instância única (ADR-0002), então em memória é a escolha certa nos dois casos — @nestjs/throttler teria servido igual de bem nesse ponto. A razão real de ter escrito um guard próprio (apps/api/src/common/rate-limit/) foi precisar de chaves compostas por rota: public-signature/:token/* precisa de um balde por IP (pega uma fonte varrendo/testando muitos tokens diferentes) e um balde por token (pega um link vazado sendo atacado por qualquer número de IPs — é exatamente o caso que @nestjs/throttler não cobre bem sem sobrepor múltiplos throttlers nomeados com trackers customizados por controller, o que na prática vira quase tanto código próprio quanto escrever o guard direto). RateLimiterService é um Map com janela fixa (não sliding window/token bucket — trade-off deliberado de simplicidade, documentado no próprio arquivo), RateLimitGuard lê metadata de @RateLimit(...rules) e aplica todas; sem decorator no handler, o guard é um no-op (por isso pôde ser plugado a nível de controller sem afetar rotas autenticadas do mesmo controller, ex. mfa/enroll).

Limites

Todos em apps/api/src/common/rate-limit/rate-limit-rules.ts, cada um com a razão numa linha de comentário — resumo: login/mfa/challenge/forgot-password/reset-password a 10/min por IP (alvo de adivinhação de credencial — argon2/token de 256 bits já encarece o palpite, isso limita a taxa), refresh/logout a 30/min por IP (token de 256 bits, não é alvo de força bruta — limite mais alto porque sessão ativa com múltiplas abas atualiza sozinha com frequência), public-signature/* com um balde de IP compartilhado do controller inteiro (60/min) somado a um balde por token específico da ação (20/5min nas ações comuns, 5/15min em otp/request porque dispara e-mail de verdade, 15/5min em otp/verify — complementar ao lockout por tentativas erradas que já existe em public-signature.service.ts::verifyOtp, persistido no banco, esse sim já sobrevive a troca de IP), public-verify a 60/min por IP e 30/min por verificationId.

O que NÃO foi resolvido (residual, registrado em docs/security/threat-model.md)

Um atacante trocando de IP lentamente (abaixo do limite por IP, em baixo volume, por muito tempo) não é pego por isto — não existe lockout de conta a nível de aplicação para login hoje, só o rate limit por IP. E app.set('trust proxy', ...) nunca foi configurado nesta API; se algum dia um reverse proxy/LB externo ficar na frente do eden-api em produção, req.ip vai reportar o IP do proxy para todo mundo e essa mitigação (e todo log de IP do projeto) fica cega — não configurei isso adivinhando a topologia futura, porque confiar em X-Forwarded-For sem saber exatamente quantos hops confiáveis existem abre uma forma de o próprio cliente spoofar seu IP.

Verificação

apps/api/test/rate-limit.spec.ts: metade pura (sem banco/HTTP, no estilo de signature-hash-chain.spec.ts) provando que uma chave estoura exatamente no limite configurado, que chaves diferentes (duas IPs, ou dois tokens de assinatura) nunca competem pelo mesmo balde, e que a janela reseta (vi.useFakeTimers); metade HTTP real (app real, Postgres real) provando estouro de verdade em POST /v1/auth/login (11ª tentativa em 1 minuto vira 429 com Retry-After, não mais 401) e que esgotar o balde do login não afeta o balde de forgot-password na mesma requisição/IP. Suíte completa: 77/77 verdes (70→77). pnpm turbo run typecheck build 28/28. Testado ao vivo contra o container real eden-api (rebuild + restart): 10× 401 seguidos de 429 com corpo {"statusCode":429,"message":"Muitas requisições. Tente novamente mais tarde.","retryAfterSeconds":60} e header Retry-After: 60, log estruturado WARN [RateLimitGuard] rate limit exceeded: rule=auth-login ... no docker logs eden-api, e confirmado que forgot-password seguiu respondendo 200 no mesmo IP enquanto login estava bloqueado.


UI de Assinatura Eletrônica (backoffice + páginas públicas) — segunda pendência do módulo fechada

Data: 2026-09-17. Continuação direta, sem parar para autorização — retomando as duas pendências deixadas pela leva anterior (rate limiting, ver entrada acima, e a UI completa do módulo). Antes de delegar o frontend, um gap real de backend foi encontrado e corrigido diretamente (não delegado, por ser rota nova/sensível): PublicSignatureService.getPublicState só devolvia metadados dos documentos (título, versão, contagem de páginas) — não existia NENHUMA forma de o signatário efetivamente ver o conteúdo do PDF antes de "confirmar leitura", nem do backoffice baixar o documento congelado ou o PDF final consolidado. Endpoints novos, mesmo padrão de streaming já usado em documents.controller.ts (StreamableFile, nunca URL assinada — eden.md linha ~4442):

  • GET /v1/signature-envelopes/:id/documents/:documentId/pdf e GET /v1/signature-envelopes/:id/final-pdf (autenticados, escopo por revenda como o resto do módulo).
  • GET /v1/public-signature/:token/documents/:documentId/pdf (público, autorização inteiramente pelo token — mesma checagem de posse do documento que confirmDocumentRead já usava).

Cobertos em test/signature-envelope-flow.spec.ts (72/72 antes da leva de rate limiting subir para 77/77). pnpm turbo run typecheck build 28/28.

Frontend (agente eden-frontend, uma leva só — public+autenticado não foram paralelizados desta vez, para evitar a reconciliação manual de App.tsx que as levas anteriores precisaram fazer a cada par de agentes)

Backoffice (apps/core-web/src/routes/signatures/): lista de envelopes (enriquecida por chamada individual GET /:id, já que a listagem não traz signatários — mesmo idioma já usado em TicketsListPage para o SLA), criação (origem cliente XOR revenda, seleção de document_generations já final, form-array de signatários contratante — contratada nunca é pedida no formulário), detalhe com documentos/signatários/timeline de auditoria e todas as ações de transição de estado (enviar convite, cancelar, excluir, selar checkpoint, finalizar, baixar PDF final, verificar cadeia), gates de UI espelhando ENVELOPE_TRANSITIONS do repositório.

Público (apps/core-web/src/routes/, fora do AuthenticatedLayout, mesmo padrão de /login//forgot-password): PublicSignaturePage.tsx em /assinar/:token (path literal que o backend já usa no e-mail de convite) — wizard guiado inteiramente por signer.status, sempre re-buscando o estado a cada ação para permitir retomar de onde parou se o signatário fechar o navegador no meio; PublicVerifyPage.tsx em /verificar/:verificationId (idem, path literal do e-mail de conclusão).

Achado de infraestrutura do próprio agente: nenhuma tela existente no projeto tinha um padrão pronto de "esconder ação por permissão" (grep não encontrou nenhum uso de permissions em rotas) — o agente adicionou permissions ao AuthContext (buscado de /auth/me) e um hasPermission mínimo, usado só para UX (esconder/desabilitar botão), nunca como controle de acesso — a decisão real continua inteiramente no backend.

Divergências reais encontradas contra o brief, corrigidas antes de entregar: GET /signature-envelopes não inclui signatários/documentos (resolvido enriquecendo cada linha); verify-audit pode retornar failedAt/reason além de {valid, eventsChecked} quando a cadeia está corrompida (UI trata); o contador de tentativas do OTP não é exposto pela API (UI usa mensagem genérica); sendInvite não valida o status do envelope no backend (UI esconde o botão em estados terminais por UX, sem depender disso para segurança).

Verificação

pnpm turbo run typecheck build 28/28. Teste ao vivo de ponta a ponta contra eden-api/eden-postgres/eden-minio reais (o agente subiu um mail-catcher SMTP efêmero na rede docker para extrair tokens/OTP reais dos e-mails, revertido ao final — .env original confirmado intacto via docker inspect): empresa+representante legal → revenda → template→geração final → criar envelope → enviar convite → os dois signatários completando o fluxo público inteiro (ver PDF do documento, confirmar leitura, dados, consentimento, OTP errado então certo, assinar) → SENT→SIGNED automático no último signatário → seal-checkpoint → finalize (Chromium + merge de PDF reais) → COMPLETED → download do PDF final autenticado confirmado como PDF válido → verify-audit válido (38 eventos) → public-verify sem auth retornando o formato esperado com integrityValid: true. Depois containers eden-api/eden-core reconstruídos com o estado final combinado (endpoints de streaming + rate limiting + UI) e testados ao vivo: /signatures, /signatures/new, /assinar/:token, /verificar/:verificationId respondendo 200 via proxy same-origin (porta 3001), e o 11º login errado seguido confirmando 429 (rate limiting da leva anterior intacto após o rebuild combinado).

Pendências

DECLINED (recusa de assinatura) segue sem rota que o produza — reservado no schema, mesma lacuna do legado. contract_versions/contract_amendments (ADR-0006, adiados desde a leva de trava de edição de contrato) agora estão desbloqueados — o fluxo de assinatura já existe — mas ainda não iniciados. reseller-web/subscriber-web seguem só com o shell de login — nenhuma tela própria, incluindo nada do módulo de assinatura (fora de escopo desta leva).


Catálogos de Produto (Tipo/Grupo/Subgrupo/Marca/Unidade) — submenu, telas e dropdowns no cadastro de item

Data: 2026-09-17. Usuário pediu: "o menu produtos precisa ter sub menus, aonde cadastraremos os grupos subgrupos marcas e tipo e unidades e dentro do cadastro do item ai essas informações sera apenas dropdown dos cadastros feitos anteriormente." O próprio cabeçalho da migration original de products (Fase 2) já previa isso como trabalho futuro ("product_type is free text for now... tightening it to a CHECK/lookup table is future work"). Confirmado antes de começar: a tabela products só tinha 37 linhas, todas de teste/QA — backfill trivial, sem dado histórico real em risco.

Backend (agente eden-commercial)

Migration packages/database/migrations/1789694035000_product-catalogs.cjs: 5 tabelas novas no formato "catálogo simples" (mesmo shape de fiscal_ncm/fiscal_cfop, sem os campos de sincronização externa — são catálogos 100% manuais) — product_types, product_groups, product_subgroups (FK group_id → product_groups, ON DELETE RESTRICT), brands, units (seedada com 'Unidade'). products ganha product_type_id/product_group_id/product_subgroup_id/brand_id/unit_id, backfill a partir das colunas TEXT antigas (product_type/unit sempre presentes viram NOT NULL), colunas antigas dropadas. down reversível de verdade, testado (migrate → migrate:down → migrate de novo).

Endpoints novos (mesmo controller de products, padrão de agrupamento do FiscalController): GET/POST /v1/products/{types,groups,brands,units}, PATCH .../:id, GET/POST /v1/products/subgroups (groupId filtra a listagem, obrigatório no create), PATCH /v1/products/subgroups/:id. Leitura aberta a qualquer autenticado, escrita exige products:edit — mesma regra já documentada no ProductsController. Sem rota de exclusão física em nenhum dos 5 (mesmo padrão dos catálogos fiscais) — active=false é o "apagar". GET /v1/products/GET /v1/products/:id agora fazem JOIN e devolvem *_name resolvido ao lado de cada *_id. CreateProductDto/UpdateProductDto trocaram os campos de texto livre por productTypeId/productGroupId/productSubgroupId/brandId/unitId (UUID), com validação de existência e de consistência subgrupo↔grupo.

Achado real corrigido diretamente por mim (não delegado): o agente de frontend, ao integrar, encontrou e reportou com precisão um buraco de integridade de dados que o eden-commercial tinha deixado passar — PATCH /v1/products/:id usa COALESCE para os campos opcionais, então não existia forma de expressar "limpar este campo" distinta de "não mexer nele" (ambos colapsavam no mesmo parâmetro NULL do SQL). Consequência real: editar um produto trocando o Grupo sem tocar no Subgrupo deixava o product_subgroup_id antigo órfão, agora inconsistente com o novo grupo, sem nenhuma validação pegando isso (a checagem de consistência só rodava quando productSubgroupId vinha no mesmo PATCH). Corrigido em apps/api/src/products/products.service.ts/packages/database/src/repositories/products.ts: productSubgroupId: null agora é uma instrução explícita de "remover" (nunca tenta buscar um subgrupo com id null), a validação de update passou a checar o estado efetivo resultante (valor do DTO onde informado, senão o valor atual do produto) — então trocar de grupo sem resolver o subgrupo agora é bloqueado com 400 claro em vez de corromper o vínculo silenciosamente — e uma função de repositório dedicada (clearProductSubgroup) contorna a limitação do COALESCE para esse campo especificamente. Frontend (ProductFormPage.tsx) ajustado para mandar null explícito (não undefined) quando o campo é limpo, e o aviso que o agente tinha colocado como paliativo foi removido — o problema está resolvido de verdade, não só sinalizado. Não estendi o mesmo tratamento a productGroupId/brandId (limitação de "não dá para limpar de volta pra vazio via PATCH" pré-existente e mais ampla no código, sem risco de invariante quebrada — fora de escopo).

3 testes novos em apps/api/test/product-catalogs.spec.ts cobrindo exatamente esse ciclo (bloqueio, limpeza explícita, troca de grupo bem-sucedida depois). Suíte completa: 95/95 verdes (92→95, incluindo os 15 do agente + os 3 meus).

Frontend (agente eden-frontend)

packages/ui/src/components/AppShell.tsx: AppShellNavItem ganhou children? — item com filhos vira accordion (chevron, auto-expande quando a rota atual é de um filho), sem dependência nova. Verificado sem regressão nos 3 apps (core-web/reseller-web/subscriber-web usam o mesmo componente).

Menu "Produtos" ganhou submenu: Itens/Tipos/Grupos/Marcas/Unidades (Subgrupo deliberadamente sem entrada própria — sempre gerenciado a partir da tela de um Grupo específico, via painel "Ver subgrupos", já que subgrupo não existe fora do contexto de um grupo). 4 telas novas em apps/core-web/src/routes/products/catalogs/ reaproveitando o padrão já usado em fiscal/NcmListPage.tsx (lista + formulário inline de criação + toggle ativo/inativo). ProductFormPage.tsx: Tipo/Unidade/Grupo/Subgrupo/Marca viraram dropdowns (Subgrupo desabilitado até escolher um Grupo, refaz fetch por groupId a cada troca, limpa a seleção se deixar de pertencer ao grupo novo — e agora manda null explícito nesse caso, fechando o ciclo com a correção de backend acima).

Verificação

pnpm turbo run typecheck build 28/28 (verificado de forma independente por mim antes e depois da correção do bug de COALESCE). Suíte de testes 95/95. Testado ao vivo via curl contra eden-api/eden-core reais, através do proxy same-origin: criação de tipo/grupo/subgrupo/marca/unidade, criação de produto referenciando os 5, o 400 exato de "grupo incompatível" ao trocar de grupo sem resolver o subgrupo, productSubgroupId: null limpando de verdade (confirmado por re-GET), e a troca de grupo funcionando depois de limpo. Rotas /products/types, /products/groups, /products/brands, /products/units respondendo 200 pelo proxy (porta 3001). Linhas de teste (qa-test-*/smoke-test-*) desativadas nos 5 catálogos e no produto de teste ao final — estado ativo ficou limpo (service/telecom em Tipos, Unidade em Unidades, Grupos/Subgrupos/Marcas vazios, prontos para o usuário cadastrar os seus).

Pendências

Suítes de teste futuras vão continuar acrescentando linhas qa-test-* permanentes nesses 5 catálogos (sem rota de exclusão física, mesmo padrão aceito para os catálogos fiscais) — precisa de desativação manual periódica até (se algum dia for justificado) existir uma rota de exclusão física ou um processo de limpeza dedicado. "Não dá para limpar Grupo/Marca de volta para vazio via PATCH" continua uma limitação pré-existente e mais ampla do padrão COALESCE usado em updateProduct — não corrigida (sem risco de invariante quebrada, ao contrário do caso do subgrupo).


Submenu de Subgrupos dentro de Grupos — segundo nível de navegação

Data: 2026-09-18. Usuário pediu, sobre a leva anterior: "dentro da opção grupos tem que abrir um sub menu de sub grupos para que fique na mesma lógica" — ou seja, um segundo nível de submenu (Produtos > Grupos > Subgrupos), não só o painel "Ver subgrupos" já existente dentro da tela de Grupos. Implementado diretamente (mudança pequena e bem entendida, sem delegar).

packages/ui/src/components/AppShell.tsx: NavGroup passou a ser recursivo — um filho que também tem children agora renderiza como outro NavGroup aninhado (antes só 1 nível era suportado, documentado explicitamente como limitação). withActive() em AuthenticatedLayout.tsx já era recursivo (nenhuma mudança necessária ali) — só o item "Grupos" ganhou seu próprio children: [Grupos (lista), Subgrupos].

Nova tela apps/core-web/src/routes/products/catalogs/SubgroupsListPage.tsx (/products/subgroups) — todos os subgrupos, de todos os grupos, com coluna "Grupo" e formulário de criação que pede o Grupo explicitamente (dropdown), diferente do GroupSubgroupsPanel já existente (que fica dentro da tela de um Grupo específico e por isso nunca precisa perguntar "qual grupo" — o contexto já implica). As duas telas coexistem deliberadamente: uma é a visão geral alcançável pelo menu, a outra é o atalho contextual ao editar um grupo específico.

Pequeno complemento de backend necessário para a coluna "Grupo" da tela nova: listProductSubgroups (packages/database/src/repositories/product-catalogs.ts) passou a fazer JOIN com product_groups e devolver group_name — antes só existia group_id cru, suficiente para o painel escopado (que já sabe o nome do grupo pelo contexto) mas não para a listagem geral.

Verificação

pnpm turbo run typecheck build 28/28 (os 3 apps que usam @eden/ui — core-web/reseller-web/subscriber-web — confirmados sem regressão). Teste novo em apps/api/test/product-catalogs.spec.ts cobrindo group_name resolvido tanto na listagem filtrada por groupId quanto na geral. Suíte completa: 95/95 verdes. Testado ao vivo via curl contra eden-api/eden-core reconstruídos: rota /products/subgroups responde 200, criação de subgrupo segue funcionando, listagem geral resolve group_name corretamente. Interação de clique no accordion aninhado (abrir "Grupos" → ver "Subgrupos" → navegar) não foi possível verificar via browser automatizado (nenhuma ferramenta desse tipo disponível neste ambiente) — verificação limitada a build/tipos/API real, mesma limitação já registrada pelo agente de frontend na leva anterior.


Rótulo "Nome do X" nos formulários de catálogo de Produto + verificação real em navegador

Data: 2026-09-18. Usuário pediu duas coisas na sequência: (1) verificar no navegador se a leva anterior (submenu de Subgrupos) está funcionando de verdade, e (2) trocar o rótulo genérico "Nome" por "Nome do subgrupo" no formulário de criação — e, logo em seguida, "usar a mesma lógica para o Grupo", interpretado (e confirmado fazendo) como aplicar o mesmo padrão aos 5 catálogos: "Nome do tipo"/"Nome do grupo"/"Nome do subgrupo"/"Nome da marca"/"Nome da unidade" (sem dois-pontos no final — nenhum outro rótulo do app usa, mantido consistente).

Verificação real em navegador (não só API)

Nenhuma ferramenta de browser automation (claude-in-chrome) estava disponível nesta sessão. Em vez de reportar como não verificável (mesma limitação que os agentes de frontend já haviam registrado nas levas anteriores), usei o Playwright que já existe no monorepo (packages/integrations depende dele para renderização de PDF; o Chromium já estava em cache local de builds anteriores) para abrir o Chromium de verdade, logar como super_admin e navegar pelas telas reais. Isso capturou um bug real que a verificação só por API/build nunca pegaria: o menu aninhado Produtos → Grupos → Subgrupos realmente expande e navega corretamente (confirmado via dump do texto do DOM da sidebar), mas o rótulo do campo de criação continuava mostrando "Nome" em vez de "Nome do grupo"/"Nome do tipo"/etc — o prop nameLabel tinha sido adicionado à assinatura de CatalogCreateForm.tsx mas esquecido de usar de fato no JSX (<Input label="Nome" .../> continuava hardcoded). Corrigido, reconstruído, reverificado com o mesmo script Playwright — os 5 rótulos batem exatamente com o esperado.

Mudança

CatalogCreateForm.tsx/SimpleCatalogCard.tsx ganharam o prop nameLabel (default "Nome", sem quebrar nada que não o passe); os 5 pontos de uso (ProductTypesListPage, ProductGroupsListPage, GroupSubgroupsPanel, BrandsListPage, UnitsListPage) e o formulário manual de SubgroupsListPage.tsx passaram o rótulo específico de cada catálogo.

Verificação

pnpm turbo run typecheck build 28/28. Suíte 95/95 (uma corrida isolada apresentou uma falha pontual num afterAll de outro spec depois de várias execuções seguidas da suíte hoje contra o mesmo Postgres compartilhado — não reproduziu nas execuções seguintes, tratado como ambiental, não investigado a fundo). Verificação em navegador real via Playwright headless: dump de DOM confirmando os 5 rótulos corretos em /products/{types,groups,subgroups,brands,units}, screenshots confirmando visualmente o submenu aninhado "Grupos" expandido mostrando "Grupos"/"Subgrupos", a tela de Subgrupos com dropdown "Grupo" + campo "Nome do subgrupo" + coluna "Grupo" na tabela, e o painel "Ver subgrupos" dentro de um grupo específico também com "Nome do subgrupo". Linhas de teste acumuladas nos 5 catálogos ao longo do dia (múltiplas rodadas de suíte + smoke tests) desativadas ao final — só sobraram duas linhas de subgrupo não reconhecidas como teste automatizado ("teste"/"qqweqw", possivelmente exploração manual do próprio usuário) deliberadamente preservadas, não mexidas.


Menu "Cadastro" — agrupando Empresas/Produtos/Revendas/Clientes

Data: 2026-09-18. Usuário pediu para agrupar os cadastros de dados mestres num menu próprio: "vamos criar um menu cadastro, e dentro do menu cadastro vai estar o cadastro do produto com seus submenus, cadastro de revenda cadastro de clientes e cadastro da empresa". Também sinalizou que o próximo passo será ajustar a tela de Cadastro de Cliente (client-registrations) — ainda sem especificar o quê, fica para a próxima leva.

AuthenticatedLayout.tsx: novo item de nível 1 "Cadastro" (logo após "Painel") reunindo "Empresas", "Produtos" (com toda a árvore de submenus já existente — Itens/Tipos/Grupos{Grupos,Subgrupos}/Marcas/Unidades, agora um nível mais fundo), "Revendas" e "Clientes" — os 4 saíram do nível 1 do menu. withActive() já era recursivo (nenhuma mudança de lógica necessária); só a estrutura de dados (NAV_ITEM_DEFS) mudou.

Isso exercita profundidade 3 no AppShell (Cadastro > Produtos > Grupos > Subgrupos) pela primeira vez — o comentário do componente dizia explicitamente "depth beyond 2 levels is untested". Verificado ao vivo via Playwright que funciona corretamente (auto-expande só o caminho ativo em cada nível, sem abrir irmãos à toa) — comentário atualizado para refletir que a recursão é validada, não só "estruturalmente não impedida".

Verificação

pnpm turbo run typecheck build 28/28. Suíte 95/95. Verificado ao vivo com Playwright headless (login real, dump de DOM da sidebar em /companies, /products/groups, /products/subgroups, /resellers, /client-registrations, e no painel): "Cadastro" some fechado quando nenhum filho está ativo, abre exatamente o caminho certo nos outros casos, screenshot conferindo visualmente a indentação dos 3 níveis.

Pendências

Ajustes na tela de Cadastro de Cliente — usuário sinalizou que é o próximo passo, ainda sem especificar quais ajustes.