# EDEN — Progresso ## Fase 0 — Descoberta e Arquitetura: CONCLUÍDA Data: 2026-09-03. ### Entregáveis - `docs/project-inventory.md`, `docs/glossary.md`, `docs/architecture.md`, `docs/assumptions.md`, `docs/security/threat-model.md`, `docs/implementation-plan.md`, `docs/progress.md` (este arquivo). - 12 ADRs em `docs/adr/` (0001–0012), cobrindo: modular monolith, topologia de containers (Postgres isolado + eden-core/parceiros/assinante em containers/portas distintas — decisão explícita do operador), auth/sessões, modelo de permissões, criptografia/segredos, contrato first-class, stock ledger, separação billing/finance/fiscal, outbox transacional, adapter SaperX, adapter Focus NFe, três apps com identidade compartilhada. - 14 subagentes em `.claude/agents/`: eden-architect, eden-database, eden-security, eden-commercial, eden-finance, eden-fiscal, eden-inventory, eden-telecom, eden-support, eden-hr-timeclock, eden-frontend, eden-api-integrations, eden-qa, eden-code-reviewer. - 7 skills em `.claude/skills/`: eden-domain, eden-security, eden-finance, eden-fiscal, eden-inventory, eden-telecom, eden-timeclock — cada uma com `SKILL.md` + `references/`. - Hooks em `.claude/settings.json` + `.claude/hooks/*`: `PreToolUse` (guarda de comandos Bash destrutivos/externos e de acesso a arquivo fora do projeto/`.ssh`/`/etc`/segredo), `PostToolUse` (lint/typecheck incremental, fail-open enquanto pnpm não existir), `Stop` (checagem advisória de segredo/TODO crítico no diff, silenciosa até existir repositório git). Testados via pipe com payloads sintéticos — todos os cenários (allow/ask/deny/fail-open) se comportaram como esperado. ### Revisão cruzada (self-review contra os critérios dos agentes eden-architect / eden-security / eden-database, criados nesta mesma fase) **eden-architect**: bounded contexts (`docs/architecture.md` §2) têm direção de dependência consistente (nenhum ciclo); HR/Timeclock corretamente isolado; candidatos a extração futura (Billing, Fiscal) identificados sem extração prematura. Nenhuma feature de negócio foi implementada antes deste gate — consistente com o Master Prompt §18. **eden-security**: threat model cobre as superfícies do legado (rotas públicas, auth, autorização, integrações, upload, PDF, auditoria, infraestrutura) mais os invariantes de teste obrigatórios (§15.3). Hooks `PreToolUse` já impõem, a nível de ferramenta, várias regras de §2.1/§2.2 (fora do projeto, `.ssh`/`/etc`, impressão de `.env`, SSH/SCP externo, remoção ampla de volume/prune). Pendência real registrada (não bloqueante): política de retenção/eliminação LGPD ainda não definida pela Handix (`docs/assumptions.md` #1). **eden-database**: princípios de modelagem (§11) já refletidos nas ADRs de contrato/estoque/billing (nunca saldo editável, nunca CASCADE indiscriminado, JSONB só para snapshot/metadata). Nenhum schema físico foi criado ainda nesta fase — corretamente adiado para a Fase 1, quando o Postgres 18 realmente subir. **Nota**: esta revisão foi feita pela própria sessão orquestradora aplicando os critérios definidos nos arquivos de agente recém-criados. Os subagentes dedicados (`eden-architect`, `eden-security`, `eden-database`) ficam disponíveis para revisões reais em sessões futuras assim que o Claude Code os carregar como subagent types. ### Pendências não bloqueantes (ver `docs/assumptions.md`) 1. Política de retenção/eliminação de dados pessoais (LGPD) — Handix ainda não definiu. 2. Docker/Node/pnpm precisam ser provisionados no ambiente antes da Fase 1. 3. Portas dos containers definidas via `.env` (não fixas) — ver ADR-0002. 4. Git ainda não iniciado no projeto — planejado para o início da Fase 1. 5. Inventário fino do `tema_do_Eden.zip` (extração de componentes) adiado para o início de `eden-frontend`. ## Gate de saída da Fase 0: VERDE Todos os itens do checklist da seção 22 do Master Prompt (“Primeira Execução”) estão completos. --- ## Fase 1 — Plataforma: EM ANDAMENTO Data de início: 2026-09-03. ### Ambiente provisionado - Docker CE 29.7.2 + Compose v5.5.0 (repositório oficial Docker, não o `docker.io` do Debian) instalados via apt. - Node.js 22.23.2 LTS (NodeSource) + pnpm 11.25.0 (via corepack). - Git 2.47.3. Repositório inicializado (`main`), remoto `origin` = `https://git.falehandix.com.br/Matheus/eden.git` (nenhum push feito ainda). ### Monorepo - pnpm workspaces + Turborepo. 5 apps (`api`, `worker`, `core-web`, `reseller-web`, `subscriber-web`) + 9 packages, todos com `package.json`/`tsconfig.json` válidos. - `pnpm install`, `pnpm turbo run typecheck` e `pnpm turbo run build` passam 100% (15/15 e 14/14 tarefas, respectivamente) em todo o monorepo. - `apps/api`: NestJS mínimo com `/health/live` e `/health/ready` (este último checando Postgres de verdade via `@eden/database`). - `apps/worker`: placeholder que confirma conectividade com o banco; sem BullMQ/Redis ainda (conforme ADR-0002 — só entra quando houver job real). - `apps/core-web`, `apps/reseller-web`, `apps/subscriber-web`: Vite + React + TS + Tailwind, cada um com favicon oficial do EDEN. - `packages/database`: migrations via `node-pg-migrate` (formato `.cjs`), client de query (`pg` Pool). ### Banco de dados - Migration baseline aplicada com sucesso contra Postgres 18.6 real (container `eden-postgres`): `roles`, `role_permissions`, `applications`, `users`, `user_applications`, `sessions`, `audit_log` — cobrindo Identity & Access (ADR-0003/ADR-0004) e um audit log append-only com hash-chain (testado: `UPDATE`/`DELETE` são efetivamente bloqueados pelo trigger, só liberados via `SET LOCAL eden.allow_audit_mutation`). - Seeds: 4 papéis do sistema (pesos idênticos ao legado) + as 3 aplicações (`core`/`reseller`/`subscriber`). - Organization (empresas/entidades legais), Commercial e demais bounded contexts **ainda não têm migration** — ficam para quando cada fase começar, por design (ver ADR-0001). ### Docker Compose — topologia ADR-0002 validada de ponta a ponta `docker compose --env-file .env up -d` sobe os 6 serviços com sucesso: | Serviço | Status observado | |---|---| | `eden-postgres` | healthy — porta dev 55432, nunca 5432 direto | | `eden-api` | healthy — único serviço com `DATABASE_URL`; `/health/ready` confirma Postgres via rede interna | | `eden-worker` | up — conecta ao Postgres, sem porta exposta | | `eden-core` (:3001) | healthy — HTTP 200 | | `eden-parceiros` (:3002) | healthy — HTTP 200 | | `eden-assinante` (:3003) | healthy — HTTP 200 | Bugs de bootstrap corrigidos nesta sessão (documentados aqui para não se repetirem): 1. Imagem oficial `postgres:18-alpine` mudou o layout do volume — precisa montar `/var/lib/postgresql` (não `.../data`). 2. `apps/api/package.json` tinha `"type": "module"` incompatível com o `tsconfig` (saída CommonJS) — Node falhava com `exports is not defined`. 3. `node-pg-migrate --envPath` não carregava o `.env` corretamente nesta versão — trocado por `bash -c 'source .env; ...'` nos scripts `migrate:up`/`migrate:down`. 4. Healthcheck dos 3 frontends usava `wget http://localhost/...`, que resolve para `::1` (IPv6) dentro do container — nginx só escuta IPv4; trocado para `127.0.0.1`. ### Auth, permissões, Companies e OpenAPI — implementados e testados ao vivo - **Auth** (`apps/api/src/auth/`): Argon2id (parâmetros versionados, ADR-0005), JWT de acesso de 15 min, refresh token opaco (256 bits) com rotação e **detecção de replay** (reuso de token já rotacionado revoga toda a cadeia de sessões do usuário). Endpoints `POST /api/v1/auth/{login,refresh,logout}` e `GET /api/v1/auth/me`. - **Permissões** (`apps/api/src/auth/permissions.ts`, `permission.guard.ts`): resource+action+scope (ADR-0004), `super_admin` com bypass hardcoded (nunca consulta `role_permissions`, espelhando a trava também existente no banco). Guard resolve permissões do banco a cada request (nunca cacheado no JWT) — mudança de papel tem efeito imediato, mesma filosofia do legado. - **Companies** (`apps/api/src/companies/` + migration `1788437478189_companies`): CRUD com leitura aberta a qualquer autenticado e escrita restrita a `companies:manage`; validação de CNPJ (dígito verificador, `packages/domain-shared`) testada com CNPJ válido/inválido conhecidos. - **Seed automático**: `SeedService` cria o primeiro `super_admin` a partir de `EDEN_SUPERADMIN_EMAIL`/`PASSWORD` só quando `users` está vazia — testado (log `super_admin inicial criado`). - **OpenAPI/Swagger**: `/api/docs` (UI) e `/api/docs-json`, cobrindo todos os endpoints atuais. - **Validação/segurança de request**: `ValidationPipe` global com `whitelist`+`forbidNonWhitelisted` (proteção contra mass-assignment) — testado (`id`/`isActive` extras num POST de company são rejeitados com 400). Testes manuais realizados contra a stack real (não só typecheck): login com senha errada → 401 genérico; refresh rotaciona e o token antigo some; reuso do token antigo → 401 + toda a cadeia revogada; logout revoga e é idempotente; audit log com a sequência completa de eventos (`auth.login`, `auth.refresh`, `auth.refresh_replay_detected`, `company.created`). ### CRUD de Usuários/Papéis, Products, Resellers, adapters S3/SMTP — implementados via 4 subagentes especializados em paralelo (worktrees isolados) + trabalho direto Estratégia: 4 subagentes (`general-purpose`, atuando com a persona `eden-commercial`/`eden-api-integrations` definida em `.claude/agents/`, já que os subagent types customizados ainda não carregaram nesta sessão) rodaram em paralelo, cada um em `git worktree` isolado, enquanto o CRUD de Usuários/Papéis foi feito diretamente (por tocar peso hierárquico e segurança). Cada agente foi instruído a não editar `app.module.ts` nem `packages/database/src/index.ts` — a integração final (merge das branches, wiring, resolução de conflitos, rebuild, teste ao vivo) foi feita manualmente. - **Users/Roles** (`apps/api/src/users/`, `apps/api/src/roles/`): convite com senha temporária, troca de senha self-service, reset administrativo, grant/revoke de aplicação — tudo com checagem de peso hierárquico (`assertCanAssignRole`/`assertCanActOnUser`, eden.md §1.6). CRUD de papéis e `role_permissions` exclusivo `super_admin` (`SuperAdminGuard`), deliberadamente não generalizado para o modelo de permissão (raiz do próprio sistema de autorização). - **Products** (`apps/api/src/products/` + migration `products`): catálogo com as 5 faixas de preço fixas, `product_code` sequencial, flags de rastreamento (estoque/serial/patrimônio/MAC). Leitura aberta, escrita via `products:edit`. - **Resellers** (`apps/api/src/resellers/` + migration `resellers`): CRUD operacional + vínculo usuário↔revenda (`users.reseller_id`). Leitura aberta, escrita via `resellers:edit` — desvio deliberado do legado (que travava em `requireAdmin`), documentado em comentário. - **Adapters S3/SMTP** (`packages/integrations/`): portados fielmente do legado — S3 falha alto (erro claro se não configurado), SMTP falha silencioso (nunca quebra um fluxo de negócio por causa de e-mail). - **CRM** (`apps/api/src/leads/`, `apps/api/src/opportunities/` + migration `crm`): leads e oportunidades com conversão transacional (`convertLeadToOpportunity`, `FOR UPDATE` + `withTransaction`). Diferente de Companies/Products, leitura também exige permissão (`leads:view`/`opportunities:view`) por conter PII de prospect. **4 bugs reais encontrados em teste manual e corrigidos antes do commit final:** 1. `assertCanAssignRole`/`assertCanActOnUser` pulavam a validação de existência do papel quando o ator era `super_admin` (o bypass de peso pulava a checagem inteira) — convidar usuário com `role_key` inexistente estourava a constraint FK do banco como 500 não tratado em vez de 400 claro. 2. `PATCH /v1/resellers/:id/assign-user` aceitava um `resellerId` diferente no corpo, que na prática era o valor usado — o `:id` da URL virava só uma checagem de existência, não o alvo real da atribuição. Corrigido para o `:id` da URL ser a única fonte de verdade; desatribuição virou endpoint próprio (`POST /v1/resellers/unassign-user`). 3. Os controllers de Leads/Opportunities liam `req.user?.id` em vez de usar `@CurrentUser()`/`request.edenUser` (o padrão real do `JwtAuthGuard`) — todo evento de auditoria de lead/oportunidade estaria gravando `actorId: null` sempre. O agente que os escreveu não tinha o módulo de auth disponível no worktree dele para testar isso de verdade (só stubou e reverteu antes de commitar). 4. As chamadas `appendAuditEvent()` de leads/opportunities omitiam o campo obrigatório `actorType` (6 ocorrências). Também precisou renomear o timestamp da migration de CRM, que o agente gerou com valor anterior ao da migration de `resellers` (já aplicada por outro agente/merge) — `node-pg-migrate` recusa migration fora de ordem. Todos os módulos testados ao vivo contra a stack real (não só typecheck): convite de usuário, papel inexistente (400 após o fix), CRUD de produto com preços NUMERIC corretos, CRUD de revenda, atribuição/desatribuição de usuário à revenda, lead→oportunidade transacional. ### Suíte de testes de integração (agente eden-qa) Vitest + supertest + `@nestjs/testing` (bootstrap real do Nest, sem mock) contra o Postgres real — 5 arquivos, 15 testes, todos verdes: `auth.spec.ts` (anti-enumeração, rotação de refresh, **detecção de replay fim-a-fim**, logout idempotente), `role-weight.spec.ts` (guarda a regressão do bug #1), `companies.spec.ts` (CNPJ), `mass-assignment.spec.ts` (whitelist), `reseller-assignment.spec.ts` (guarda a regressão do bug #2). Achado técnico do agente: o `esbuild` (transform padrão do Vitest) não implementa `emitDecoratorMetadata`, quebrando toda injeção de dependência do Nest silenciosamente sob teste — corrigido trocando para SWC (`unplugin-swc`), abordagem padrão documentada pelo próprio NestJS. Rodar com `pnpm --filter @eden/api test`. ### Resumo do lote de agentes paralelos 6 subagentes no total (4 + 1 QA + Users/Roles feito diretamente), rodando em `git worktree` isolados, revisados e mesclados manualmente um a um (nunca merge automático sem verificação). **4 bugs reais** encontrados só porque cada módulo foi testado contra a stack viva antes do commit — nenhum deles teria aparecido só com typecheck/build, que todos os agentes já reportavam verde. Padrão validado: delegar implementação para agentes especializados funciona bem para módulos CRUD bem especificados, mas a integração final (merge, wiring, teste ao vivo, revisão) precisa ficar com quem tem visão do sistema inteiro. ### Pendências da Fase 1 (não iniciadas) - ~~MFA/TOTP~~ — implementado na leva seguinte (ver Fase 4+ abaixo). - Extração do Design System a partir do tema DreamsERP (`packages/ui` ainda é placeholder). - Wiring dos adapters S3/SMTP em algum módulo consumidor real (hoje só existem como biblioteca em `packages/integrations`, nada os chama ainda). - Nota de dado de teste: a empresa "Handix Telecom LTDA" e a revenda "Handix (revenda própria)" criadas durante os testes usam dados de exemplo, não os dados reais da Handix — substituir/remover antes de qualquer ambiente que não seja dev local. --- ## Fase 2/3 — Ofertas (Quotes), Cadastro de Cliente (Customer 360) e esqueleto de Contratos Data: 2026-09-03. ### Ofertas / Quotes (`apps/api/src/quotes/` + migration `quotes`) — implementado diretamente Núcleo comercial mais sensível a erro de arredondamento/regra de negócio (eden.md §7/§8), por isso feito sem delegar a subagente: - `pricing.ts`: funções puras porteadas literalmente do legado — `monthlyTotal = Σitem.total`; `totalImpl = implFromProducts + implGeral`; `hasSpecialPrice/isDiscount/isMarkup` por comparação com `proposedMonthlyTotal`; `contractTotal = effectiveMonthly * (contractPeriod||1) + totalImpl`; `needsApproval = isDiscount || hasReducedFidelity`; fidelidade só tem efeito legal com `approval_status === 'approved'`. Faixas de condição de pagamento (`À vista` ≤1500; `+1+1` ≤3000; `+1+2` acima) replicadas do legado. - Preço sempre derivado server-side do produto vivo no banco (nunca confia em preço vindo do cliente). - Fluxo completo: criar oferta → decidir aprovação (`PATCH :id/approval`) → fechar negócio vinculando a um cadastro de cliente (`POST :id/close`, bloqueado por `client_registration_id IS NULL` — não fecha duas vezes). ### Customer 360 / Cadastro de Cliente (`apps/api/src/client-registrations/` + migration `client-registrations`) e Contratos (`apps/api/src/contracts/` + migration `contracts`) — subagentes + salvamento manual Lançados 2 subagentes em paralelo (worktrees isolados) para Cadastro de Cliente e esqueleto de Contratos. Ambos falharam duas vezes: primeiro com rate limit de sessão (retomado após o usuário habilitar cobrança de excedente), depois com **limite de gasto mensal da conta** — um bloqueio de nível de conta que não se resolve tentando de novo. Em vez de insistir, os dois worktrees foram inspecionados (`git status`, diff arquivo a arquivo); o trabalho não commitado encontrado em ambos era substancial e de boa qualidade — foi copiado diretamente para `main` (sem passar por branch/merge, já que nada tinha sido commitado) e o que faltava foi completado manualmente: - **Cadastro de Cliente**: repositórios e camada de API salvos do agente praticamente inalterados; só faltava o `client-registrations.module.ts`, escrito manualmente. MVP com escopo explícito (eden.md §3): só fluxo autenticado, sem token de autoatendimento público, sem upload de anexo, sem integração CNPJ/CEP — tudo documentado em comentário no código, não descartado silenciosamente. - **Contratos**: repositório salvo do agente (com um import não usado removido); toda a camada de API (DTOs, service, controller, module) escrita manualmente, pois o agente não tinha chegado lá. Máquina de estados (ADR-0006) `draft→{pending_signature,cancelled}`, `pending_signature→{active,cancelled}`, `active→{suspended,cancelled,terminated,expired}`, `suspended→{active,cancelled,terminated}` — imposta via `SELECT...FOR UPDATE` + tabela de transições, com `signed_at`/`activated_at` carimbados na transição correta e histórico completo em `contract_status_history`. Escopo: 4 das 11 tabelas do ADR-0006 (contracts, contract_items, contract_parties, contract_status_history); as outras 7 explicitamente adiadas. Wiring final feito manualmente: barrel (`packages/database/src/index.ts`) e `app.module.ts` atualizados; 3 migrations aplicadas ao Postgres real; `pnpm turbo run typecheck build` (28/28 tasks) e `pnpm --filter @eden/api test` (15/15 testes) verdes; container `eden-api` reconstruído e todo o fluxo testado ao vivo via curl contra a stack real: - Oferta sem preço especial (sem aprovação) e oferta com desconto + fidelidade reduzida (`approval_status: pending`) — valores conferidos manualmente contra a fórmula. - Aprovação da oferta, fechamento vinculando a um cadastro de cliente criado via `POST /client-registrations/direct`, e guarda de fechamento duplo (409 na segunda tentativa). - Contrato criado a partir da oferta fechada, transições de status válidas e inválidas (draft→active direto rejeitado com 409; draft→pending_signature→active aceito, `signed_at`/`activated_at` carimbados corretamente), adição de item/parte, FK de produto inexistente rejeitada com 400. - 401 sem token; trilha de auditoria (`audit_log`) conferida no Postgres com a sequência completa de eventos do lote (`quote.created`, `quote.approval_approved`, `quote.closed`, `client_registration.created`, `contract.created`, `contract.status_changed` ×2, `contract.party_added`, `client_registration.updated`). **2 bugs reais encontrados e corrigidos antes do commit (nenhum via subagente — achados na própria implementação/revisão):** 1. Primeiro rascunho do `updateQuote` (escrito por mim) usava `COALESCE` para os campos financeiros (`fidelity_period`, `proposed_monthly_total`, `approved_by_id`) — como `undefined` do JS também vira `NULL` no SQL, qualquer update parcial que omitisse esses campos os apagaria silenciosamente. Corrigido tornando esses campos **obrigatórios** em `UpdateQuoteInput` (não mais opcionais), forçando a camada de serviço a sempre fornecer um valor deliberado. 2. `packages/database/src/repositories/types.ts` — a interface `User` estava sem `reseller_id`/`can_approve_discount`, mesmo essas colunas já existindo no banco desde as migrations de Resellers/Quotes (ninguém tinha atualizado o tipo compartilhado). Descoberto ao ligar o uso desses campos em Quotes; corrigido no `types.ts` e propagado para `EdenAuthenticatedUser` em `jwt-auth.guard.ts`. ### Pendências deste lote - ~~FKs cruzadas entre as 3 tabelas~~ — fechadas na leva seguinte (ver abaixo). - Fluxo de autoatendimento público (token) de Cadastro de Cliente, upload de anexo, integração CNPJ/CEP — todos fora do escopo desta MVP. - 7 das 11 tabelas do ADR-0006 de Contratos (aditivos, faturas, documentos/e-signature, etc.) ainda não criadas. --- ## Fase 4/9 — MFA/TOTP, Estoque (Inventory), Suporte (Service Desk) e fundação de Ponto Eletrônico (HR/Timeclock) Data: 2026-09-03. Pedido do usuário: "pode seguir na criacao do app ao menos mais 20 etapas use subagentes". Estratégia: fechei a pendência de MFA/TOTP da Fase 1 diretamente (autenticação é sensível demais para delegar) e lancei 3 subagentes em paralelo (`eden-inventory`, `eden-support`, `eden-hr-timeclock`) em worktrees isolados, seguindo o mesmo padrão validado nas levas anteriores. Além das FKs cruzadas de Quotes/Contracts/ClientRegistrations, deixadas pendentes na leva anterior. ### FKs cruzadas (pendência da leva anterior) — fechada Migration `1788450538883_cross-reference-fks.cjs`: `quotes.client_registration_id → client_registrations(id)`, `client_registrations.quote_id → quotes(id)`, `contracts.quote_id → quotes(id)`, `contracts.client_registration_id → client_registrations(id)`, todas `ON DELETE RESTRICT` (nenhum desses agregados tem rota DELETE hoje; perder o vínculo silenciosamente numa exclusão não relacionada seria pior que bloquear a exclusão). Verificado zero órfãos antes de aplicar. ### MFA/TOTP (`apps/api/src/auth/mfa.service.ts`, `field-encryption.service.ts`) — implementado diretamente - `FieldEncryptionService` (`apps/api/src/crypto/`): criptografia reversível genérica AES-256-GCM versionada (ADR-0005), formato `v1:iv:tag:ciphertext` base64, chave de `EDEN_FIELD_ENCRYPTION_KEY` (hasheada para 32 bytes via SHA-256). Primeira implementação real desse padrão no projeto — vai ser reaproveitada por outros segredos reversíveis (ex.: credencial de equipamento Control iD, feito na mesma leva). - Fluxo de login em duas etapas quando `mfa_enabled=true`: `POST /auth/login` retorna `{mfaRequired: true, mfaChallenge}` (JWT de propósito único, 2 min, claim `purpose` distintivo) em vez de tokens; `POST /auth/mfa/challenge` troca o desafio + código TOTP pelos tokens reais. `POST /auth/mfa/enroll` (gera secret + `otpauthUrl`, não habilita ainda) → `POST /auth/mfa/confirm` (primeiro código correto habilita) → `POST /auth/mfa/disable` (exige código válido, não só sessão autenticada). - Biblioteca `otplib` v13 (API funcional `generateSecret`/`generateURI`/`verify`, não mais o antigo `authenticator` singleton de v12). **Bug de segurança real encontrado e corrigido em teste ao vivo (não hipotético — explorado de verdade contra a stack rodando):** o token de desafio MFA (`mfaChallenge`, emitido logo após a senha, antes do código TOTP) era aceito pelo `JwtAuthGuard` como um access token comum — `verifyAccessToken()` só validava assinatura/expiração, nunca checava a claim `purpose`. Resultado: um atacante que só tivesse a senha (sem o segundo fator) conseguia acesso total à API por 2 minutos usando o token de desafio como Bearer normal — MFA completamente contornável. Corrigido em `token.service.ts::verifyAccessToken` rejeitando qualquer token com claim `purpose` presente. Teste de regressão adicionado em `test/mfa.spec.ts` (`GET /auth/me` com o challenge token como Bearer → 401). Explorado e depois fechado ao vivo via curl contra o container real antes de seguir adiante. ### Estoque / Inventory (`apps/api/src/inventory/` + migration `inventory`) — subagente `eden-inventory` 5 tabelas (de 15 possíveis no Master Prompt §6.8, mesmo approach de MVP-coerente do módulo Contracts): `warehouses`, `stock_movements` (ledger append-only, saldo sempre derivado via SUM, nunca coluna editável — reforçado por trigger que bloqueia UPDATE/DELETE sem exceção), `serialized_assets`, `asset_assignments`, `stock_reservations`. Invariante "mesmo serial/patrimônio/MAC nunca duas vezes ativo" (Master Prompt §15.3) reforçada por **3 índices únicos parciais no banco**, não só validação de aplicação. Fluxo completo reserva→instalação(comodato)→devolução implementado com `SELECT...FOR UPDATE` + transação, erros tipados mapeados para 404/409. Subagente entregou trabalho de excelente qualidade na primeira tentativa — nenhum bug encontrado na revisão nem no teste ao vivo. ### Suporte / Service Desk (`apps/api/src/support/` + migration `support`) — subagente `eden-support` 4 tabelas: `support_tickets` (protocolo humano `TCK-2026-000001`, mesmo padrão de `quote_number`/`contract_number`), `ticket_comments` (com `is_internal`, nunca exposto fora do escopo `all`), `ticket_status_history`, `sla_timers` (1:1). Máquina de estados `new→triage→in_progress⇄{waiting_customer,waiting_third_party}→resolved→closed` (+`cancelled`), mesmo padrão de `CONTRACT_STATUS_TRANSITIONS`. Motor de SLA implementado como funções puras testáveis (`apps/api/src/support/sla.ts`) — pausa/despausa atomicamente em SQL na transição de status, cálculo de violação subtrai corretamente o tempo pausado (incluindo a pausa corrente). `root_cause`/`resolution_notes` só persistidos na transição para `closed`, não `resolved` (decisão documentada, confirmada em teste ao vivo). Também de excelente qualidade — nenhum bug encontrado. ### Fundação de Ponto Eletrônico / HR-Timeclock (`apps/api/src/hr-timeclock/` + migration `hr-timeclock`) — subagente `eden-hr-timeclock` Escopo desta leva deliberadamente limitado a fundação (modelo de dados + Control iD device + parser/import de AFD com CRC-16/KERMIT) — apuração, fechamento de período, ajustes, banco de horas e relatórios (eden.md §5-10) explicitamente adiados. 4 tabelas: `timeclock_devices` (senha do equipamento cifrada via `FieldEncryptionService`, nunca retornada em nenhum GET — só `hasCredential:boolean`), `timeclock_employees` (cadastro separado de `users`, decisão documentada: o Control iD identifica por CPF, e `users` não tem CPF), `afd_import_batches`, `afd_records`. `afd_records` é **imutável sem exceção, nem para super_admin** — trigger que bloqueia UPDATE/DELETE sem nenhuma válvula de escape (diferente de `audit_log`, que tem bypass controlado via `SET LOCAL`). Idempotência em duas camadas: hash SHA-256 do arquivo inteiro (retorna `alreadyImported:true` em reimportação) e `UNIQUE(device_id, nsr)` por registro. NSR ausente/não-numérico nunca vira 0 — fica `null`, preservando o dado bruto mesmo inválido. Parser CRC-16/KERMIT (poly 0x1021 refletido/0x8408, init 0x0000, sem XOR final) verificado contra o vetor de teste conhecido ("123456789" → 0x2189) — confirmado tanto no script ad-hoc do agente quanto na suíte permanente `test/afd-parser.spec.ts` (11 testes). **1 bug real encontrado e corrigido antes da integração:** a migration `PENDING_hr-timeclock.cjs` tinha uma crase (`` ` ``) solta dentro de um comentário SQL (`-- separate from \`users\``) — como a migration inteira é um template literal JS (`` pgm.sql(`...`) ``), essa crase fechava o literal prematuramente e quebrava o resto do SQL em código JS inválido, causando `SyntaxError: missing ) after argument list` ao tentar rodar `pnpm db:migrate`. Corrigido trocando a crase por aspas simples no comentário. Encontrado e corrigido antes de qualquer teste ao vivo, na primeira tentativa de aplicar a migration. ### Padrão de salvamento seletivo ao integrar (novo, deste lote) Diferente da leva anterior (onde os 2 agentes falharam por limite de gasto e o trabalho foi resgatado de worktrees não commitados), desta vez os 3 agentes **completaram normalmente**, mas todos os 3 worktrees foram criados a partir do commit único de bootstrap (antes de Auth/Companies/Products/etc. existirem nesse checkout isolado) — sem acesso ao código real de `apps/api/src/auth/`, `packages/database/src/repositories/{audit,users,roles,types}.ts` etc. Cada agente relatou isso com transparência e, para conseguir cumprir a etapa de verificação (`pnpm typecheck` limpo), recriou stubs/placeholders type-compatíveis desses arquivos dentro do próprio worktree — e dois deles (`eden-support`, `eden-hr-timeclock`) deixaram esses placeholders (`audit.ts`, `users.ts`, `roles.ts`, `types.ts`, config, guards de auth) como arquivos genuínos no diff final, não revertidos, por acreditarem (corretamente, do ponto de vista deles, isolados no worktree) que essa infraestrutura ainda não existia. Na integração, copiei seletivamente **só os arquivos de domínio real** de cada agente (migration, repositório específico do módulo, camada de API do módulo) e **descartei os placeholders duplicados de Auth/Config/Crypto/audit/users/roles**, mantendo as versões reais já existentes em `main`. Wiring do barrel (`packages/database/src/index.ts`) e `app.module.ts` feito manualmente para os 3 módulos, como nas levas anteriores. ### Verificação `pnpm turbo run typecheck build` verde em cada integração incremental; suíte de testes cresceu de 15 para **30 testes, todos verdes** (`auth.spec.ts`, `mfa.spec.ts` novo, `afd-parser.spec.ts` novo com 11 testes, `role-weight.spec.ts`, `reseller-assignment.spec.ts`, `companies.spec.ts`, `mass-assignment.spec.ts`). Container `eden-api` reconstruído e todo o lote testado ao vivo via curl contra a stack real: - MFA: enroll→confirm com código TOTP real gerado via `otplib`→login exige desafio→código errado rejeitado→código certo completa login→disable exige código válido; **exploração do bug de segurança repetida após o fix, confirmando 401**. - Inventory: armazém→entrada de ativo→serial duplicado rejeitado (409, não 500)→reserva→instalação (comodato)→instalação duplicada rejeitada (409)→devolução→ledger conferido linha a linha (`purchase_in`, `reservation`, `comodato_out`, `return_in`). - Support: ticket criado com SLA (+4h/+24h)→transições válidas→comentário público marca `first_response_met`→`waiting_customer` pausa o timer→retorno a `in_progress` acumula o tempo pausado→`resolved`→`closed` (root_cause só persiste aqui)→transição inválida rejeitada (409). - HR-Timeclock: device criado com senha cifrada (confirmado no Postgres, formato `v1:iv:tag:ciphertext`, nunca no GET)→import de arquivo AFD válido (2 linhas, CRC correto)→reimport do mesmo arquivo retorna `alreadyImported:true` sem duplicar→`UPDATE` direto em `afd_records` via SQL bloqueado pelo trigger mesmo fora da API. - Trilha de auditoria (`audit_log`) conferida no Postgres com a sequência completa de todos os eventos do lote. ### Pendências deste lote - Inventory: `warehouse_locations`, `stock_items` (cache de saldo), `stock_lots`, `inventory_counts`, `transfers`/`receipts`/`issues`/`returns` formais, `rma`, `asset_maintenance` — deferidos, tipos de movimento já modelados no vocabulário do ledger para não exigir nova migration. - Support: anexos reais (S3), CSAT, OS dedicada, filas com roteamento automático, calendário de atendimento configurável, SLA por categoria — hoje é uma política única fixa (4h/24h). - HR-Timeclock: apuração, fechamento de período, ajustes manuais, banco de horas, relatórios, sync ao vivo com o Control iD, upload multipart real (hoje aceita o conteúdo como string JSON no body). - MFA: sem rate-limiting/bloqueio de tentativas no `mfa/challenge` (mesma lacuna que já existe no login por senha hoje — nenhum endpoint de auth tem rate limit ainda, ficou registrado como débito técnico geral, não específico do MFA). --- ## Fase 5/6 + Design System — Billing, Fiscal e primeira tela real (login + shell autenticado) Data: 2026-09-03. Continuação do mesmo pedido do usuário ("pelo menos mais 20 etapas, use subagentes"). 3 subagentes em paralelo (`eden-finance`, `eden-fiscal`, `eden-frontend`), mesmo padrão de worktree isolado + integração seletiva manual. ### Billing / Faturamento recorrente (`apps/api/src/billing/` + migration `billing`) — subagente `eden-finance` MVP de 4 tabelas (ADR-0008 lista 9; billing_accounts/billing_cycles/charge_components/usage_charges/invoice_adjustments/billing_run_logs deferidos, documentado): `billing_subscriptions` (derivada de contrato, `monthly_value` é snapshot — nunca recalculado automaticamente), `billing_runs`, `invoices`, `invoice_items`. Fluxo `subscription→billing_run→invoice` completo e idempotente: rodar o mesmo run duas vezes em `draft` substitui as invoices (não duplica) — reforçado por índice único parcial `WHERE status='draft'` (no máximo um run em rascunho por período); depois de `consolidate`, run e invoices ficam imutáveis pela aplicação (409 em qualquer tentativa de gerar/alterar). ADR-0008 respeitado à risca: Billing só calcula o que é devido, nunca toca em AR/AP/boleto/cobrança (Finance) nem em documento fiscal (Fiscal) — bounded contexts continuam sendo desenvolvidos separadamente. ### Fiscal (`apps/api/src/fiscal/` + migration `fiscal`) — subagente `eden-fiscal` eden.md módulo 5 tem ~19 tabelas de catálogo + importadores automáticos (Siscomex/IBGE) + importador manual de arquivo — MVP portou 5 tabelas fielmente ao shape do legado: `fiscal_ncm`/`fiscal_cfop` (catálogo simples), `fiscal_municipality` (shape distinto, sem `valid_from`/`valid_until`), `fiscal_operation_nature` (seed fixo com as 11 naturezas do legado), `product_fiscal_profiles` (liga produto à classificação fiscal). Invariante do eden.md §2.5 implementada de verdade: **um produto tem no máximo um profile ativo por `billing_component`** — índice único parcial + camada de aplicação que inativa o profile anterior antes de criar o novo (nunca update destrutivo, histórico preservado). Todo o importador automático/manual e as ~14 outras tabelas de catálogo ficam deferidos (documentado com a seção exata do eden.md para cada item). ### Design System + login real (`packages/ui`, `apps/core-web`) — subagente `eden-frontend` Primeira leva de frontend do projeto. Achado que mudou o escopo para melhor: o `tema_do_Eden.zip` (comprado) não é só Angular — tem uma versão pronta em **React 18 + Vite + Tailwind** dentro (`react.zip`, 42.5MB), a mesma stack que o EDEN já usa. Em vez de portar de Angular, o agente extraiu os tokens visuais reais (cores, tipografia, sombras, border-radius, larguras de layout) desse React/Tailwind — confirmando inclusive uma divergência da nossa própria documentação: `docs/project-inventory.md` citava fonte Nunito/Poppins, a fonte real do tema é **Geist**. - `packages/ui` deixou de ser placeholder: preset Tailwind exportável (`@eden/ui/tailwind-preset`) com os tokens reais extraídos, mais componentes React reais (não stub) — `Button`, `Input`, `Card`, `Badge`, `Alert`, `PageHeader`, `AppShell` (sidebar+topbar+conteúdo). - `apps/core-web` (EDEN Core — prioridade, os outros 2 apps ficam para leva futura): trocou a tela estática "Bootstrap da Fase 1" por um fluxo real com `react-router-dom` — página de login chamando de verdade `POST /api/v1/auth/login` (com suporte ao fluxo `mfaRequired`/`POST /mfa/challenge` da leva de MFA), sessão em `localStorage`, rota protegida, e um shell autenticado (`AppShell` real, nome do usuário via `GET /api/v1/auth/me`, logout). **Achado real fora do escopo do agente, corrigido por mim em seguida**: a API não tinha CORS configurado (`app.enableCors` ausente em `main.ts`) — um browser real rodando `core-web` num domínio/porta diferente da API teria toda chamada de login bloqueada no preflight, mesmo com tudo implementado corretamente nos dois lados. Corrigido: novo `EDEN_CORS_ORIGINS` (env, lista de origens separadas por vírgula, nunca wildcard — a API emite bearer token) em `packages/config/src/env-schema.ts`, `app.enableCors({origin, credentials:true})` em `main.ts`, propagado em `compose.yaml`/`.env`/`.env.example` já com os 3 domínios públicos reais (`dev-edencore`/`dev-parceirohandix`/`dev-portalassinate.falehandix.com.br`) mais as portas de dev locais. Testado ao vivo: preflight de `https://dev-edencore.falehandix.com.br` aceito com os headers corretos; origem não autorizada (`evil.example.com`) corretamente sem `Access-Control-Allow-Origin` na resposta; login completo simulando o `Origin` real de browser, 200 com o header de CORS certo. **Pendência de infra identificada, não resolvida (decisão do operador, não técnica)**: os 3 domínios públicos hoje só apontam para os containers de frontend (3001/3002/3003) — não há um domínio público equivalente para a API. `VITE_API_BASE_URL` do build de produção dos 3 apps ainda aponta para `localhost:8080` (só funciona em dev local); falta decidir e implementar como o browser em produção alcança a API (domínio próprio para a API? proxy reverso roteando um path tipo `/api` para o container `eden-api` no mesmo domínio do frontend?) antes do login funcionar nos domínios públicos de verdade. ### Verificação `pnpm turbo run typecheck build` (28/28) e `pnpm --filter @eden/api test` (30/30, sem regressão) verdes após cada integração. Containers `eden-api`/`eden-core` reconstruídos; testado ao vivo via curl: - Fiscal: naturezas de operação semeadas (11), NCM criado, **invariante de profile único ativo por componente confirmada** (segundo profile RECURRING criado → primeiro automaticamente `active:false`). - Billing: subscription criada → billing run gerado (invoice de R$499,90, vencimento no `billing_day`) → regeneração do mesmo período reaproveita o mesmo run (idempotente, sem duplicar invoice) → consolidação → segunda consolidação rejeitada (409) → regeneração de período já consolidado rejeitada (409). - CORS: preflight aceito do domínio público real, rejeitado de origem não cadastrada, login completo com `Origin` de browser real. - `core-web`: build de produção servido pelo container real não contém mais o texto "Bootstrap da Fase 1" — contém o formulário de login real (`E-mail`/`Senha`/`Entrar`). - Trilha de auditoria conferida no Postgres com a sequência completa do lote. ### Pendências deste lote - Billing: `billing_accounts`, `billing_cycles` como tabela própria, consumo/telefonia (`charge_components`/`usage_charges`), `invoice_adjustments`, pró-rata — tudo Finance/AR-AP (títulos, boletos, dunning, conciliação) fica para quando esse bounded context for iniciado, por decisão da ADR-0008. - Fiscal: as ~14 tabelas de catálogo restantes, `fiscal_catalog_sync`, importador automático (Siscomex/IBGE) e manual (CSV/XLSX/JSON com proteção de inativação em massa), `fiscal_rules` (schema pronto mas sem consumo mesmo no legado). - Frontend: `reseller-web`/`subscriber-web` ainda no bootstrap estático; telas de negócio (Quotes/Contracts/Inventory/etc.) no `core-web`; rotação automática de refresh token; fluxo de ENROLL de MFA na UI (só login com MFA já habilitado está implementado). ### Decisão do operador sobre topologia de API pública + 3 bugs de infra encontrados e corrigidos na sequência Usuário decidiu: a API **nunca** tem domínio público próprio, só IP/nome interno — o browser sempre chama a API pelo mesmo domínio do frontend (caminho relativo), e o proxy externo (gerenciado pelo operador, fora deste repo) roteia esse caminho internamente para `eden-api` (por nome de serviço Docker, `/etc/hosts` ou IP interno — detalhe de infra do operador, não deste código). Implementado: `infra/docker/Dockerfile.web` agora aceita `ARG VITE_API_BASE_URL` (default `/api/v1`, Vite bake em build-time); `compose.yaml` passa esse build arg fixo (`/api/v1`) para os 3 serviços web; `EDEN_CORS_ORIGINS` simplificado para só as portas de dev local (o fluxo de produção é same-origin via proxy, não precisa de CORS). **3 bugs reais de infraestrutura encontrados e corrigidos nessa verificação (nenhum era o objetivo original, todos achados testando o resultado de ponta a ponta, não só lendo o código):** 1. **`turbo.json` não declarava `VITE_API_BASE_URL` como input da task `build`** — o Turborepo cacheava o build do Vite sem saber que essa env var afeta o output, então reaproveitava um bundle antigo (com a URL absoluta de dev) mesmo depois de mudar a env var. Corrigido adicionando `"env": ["VITE_*"]` à task `build` em `turbo.json`. 2. **`compose.yaml` usava `${VITE_API_BASE_URL:-/api/v1}`** para o build arg do Docker — parecia razoável, mas colidia com a variável de mesmo nome no `.env` (usada para `pnpm dev` local, valor absoluto `http://localhost:8080/api/v1`) que o `docker compose` carrega automaticamente; o "default" nunca era usado porque a variável já estava setada. Corrigido fixando o valor diretamente (`/api/v1`, sem interpolação de env) nos 3 serviços web, já que essa é uma decisão arquitetural fixa, não uma configuração por ambiente. 3. **Não existia `.dockerignore`** no projeto — todo `COPY . .` do `Dockerfile.web`/`Dockerfile.node` estava copiando `node_modules/` (315MB), `.git/` (5.8MB) e `.claude/worktrees/` (345MB, os worktrees de subagentes desta mesma sessão) para dentro do contexto de build a cada vez. Além do desperdício óbvio, isso é parte de por que o bug nº1 passou despercebido no primeiro teste: caches `.turbo/` de dentro de cada pacote também eram copiados para dentro da imagem. Corrigido criando `.dockerignore` (exclui `node_modules`, `.git`, `dist`, `.turbo`, `.claude/worktrees`, `.tmp`, `.env*`, o zip do tema). Cada correção foi testada isoladamente e a última confirmada abrindo o JS de produção servido pelo container real (`docker compose build --no-cache`) e conferindo que o literal `"/api/v1"` (não mais `"http://localhost:8080/api/v1"`) está no bundle. --- ## Fase 8 (parcial) — Portais Parceiros e Assinante replicam o shell autenticado do Core Data: 2026-09-03. Subagente `eden-frontend` replicou o mesmo padrão de login/sessão/shell do `core-web` (leva anterior) para `apps/reseller-web` e `apps/subscriber-web` — mesma estrutura de arquivos, mesmo cliente HTTP, mesmo `AuthContext`, só trocando `applicationKey` (`reseller`/`subscriber`), título, e a chave de `localStorage` da sessão (`eden.reseller.session`/`eden.subscriber.session`, isolamento explícito mesmo já sendo origens/portas diferentes). **Bug de integração (não do agente) encontrado e corrigido**: ao copiar `src/lib/` e `src/routes/` do worktree para `main`, o `cp -r` criou diretórios aninhados (`src/lib/lib/`, `src/routes/routes/`) porque os diretórios de destino já existiam vazios em `main` — mesmo padrão de erro já visto na integração do HR-Timeclock nesta sessão. Corrigido achatando a estrutura antes do typecheck. Verificado: `pnpm turbo run typecheck build` (28/28) após a correção; containers `eden-parceiros`/`eden-assinante` reconstruídos — títulos corretos (`EDEN Parceiros`/`EDEN Assinante`), texto antigo "Bootstrap da Fase 1" ausente do bundle de produção, formulário de login real presente; login de ponta a ponta testado contra a API real com `applicationKey: "reseller"` e `"subscriber"` — ambos aceitos e retornam tokens válidos. ### Pendências Idênticas às do `core-web` (rotação de refresh token, enroll de MFA na UI, telas de negócio) — nenhuma tela além do shell autenticado existe em nenhum dos 3 apps ainda. --- ## Fase 3 (parcial) — Fundação de Templates de Documento (sem Assinatura Eletrônica ainda) Data: 2026-09-03. Subagente construiu a metade "templates + geração" do eden.md módulo 4 (§1.1-1.3) — a metade de Assinatura Eletrônica (§1.4-1.9: envelopes, signatários, OTP, cadeia de auditoria hash-chain, 9 tabelas satélite) foi deliberadamente deixada de fora, por ser criptograficamente/legalmente sensível e grande demais para dividir espaço com esta leva — fica para uma leva própria e dedicada. `document_templates` + `document_template_versions` (ciclo `draft→publish`, invariante de no máximo 1 rascunho por template via índice único parcial, versão publicada é imutável) + `document_generations` (idempotente por `(template_version_id, origem)`, CHECK garantindo exatamente uma origem). Dependência circular (`document_templates.current_version_id` ↔ `document_template_versions`) resolvida com `ALTER TABLE` depois que as duas tabelas existem. Motor de merge de variáveis (`{{chave}}` → valor do contexto, nunca lança exceção, reporta variáveis ausentes) implementado como função pura testável — o parser Tiptap real e a renderização de PDF via Playwright/Chromium ficam para quando o editor visual e essa infra existirem (simplificação documentada: `editor_content.plainText` como stand-in). Verificado: `pnpm turbo run typecheck build` (28/28), `pnpm --filter @eden/api test` (30/30, sem regressão). Container `eden-api` reconstruído; testado ao vivo via curl: template criado → draft criado → segundo draft rejeitado (409, invariante confirmada) → draft editado com placeholders → publicado (`current_version_id` atualizado) → documento gerado com merge de variáveis funcionando (`{{empresa}}`/`{{cliente}}`/`{{valor}}` substituídos corretamente) → regeneração com os mesmos parâmetros reaproveita o registro existente (idempotência confirmada, mesmo `id`). Trilha de auditoria conferida no Postgres. ### Pendências Toda a Assinatura Eletrônica (eden.md §1.4-1.9/§5/§6/§7 — envelopes, signatários, OTP, hash-chain, verificação pública), geração real de PDF (Playwright/Chromium) + sanitização de HTML, editor visual Tiptap real (`renderToHtml`/`resolveDocument` com condicionais e repetição de tabela), seed dos 7 templates reais de produção da Handix, regra de idempotência completa com `force`/`extra_fields`. --- ## Fase 2/8 — Telas de negócio reais no EDEN Core (Empresas, Produtos, Revendas, CRM, Ofertas) Data: 2026-09-03. Usuário pediu explicitamente para priorizar a parte web ("ja faca a parte web para eu ja ver como esta ficando"). 2 subagentes `eden-frontend` em paralelo, mesma sessão, construindo sobre o shell autenticado + Design System já prontos. **Correção de instrução no meio da leva**: o usuário interrompeu para dizer "não esqueça de seguir o template" / "melhor usar ele" — as telas deviam seguir de perto o LAYOUT REAL do tema DreamsERP comprado, não só os tokens visuais abstratos já extraídos. Extraí do `tema_do_Eden.zip` as páginas React reais mais relevantes (`sales/salesQuotes*.tsx`, `crm/leads.tsx`/`deals*.tsx`, `inventory/products*.tsx`, `purchase/vendors.tsx`, `system/userManagement.tsx`, `ui-interface/{tables,forms}`) para dentro do worktree de cada agente (`.tmp/theme-reference/`, nunca versionado) e mandei mensagem de correção para os dois agentes já em execução — via `SendMessage`, não relançando a tarefa (no meio do processo cometi o erro de usar `Agent` em vez de `SendMessage` para a primeira correção, criando um agente duplicado; percebido e corrigido na hora com `TaskStop` antes de qualquer trabalho real ser feito por ele). Ambos incorporaram a referência do tema (estrutura de header/breadcrumb/tabela/formulário em cards) reescrita com `@eden/ui` + Tailwind puro — sem copiar JSX nem trazer dependências do tema (antd/Preline) para o projeto. ### Empresas / Produtos / Revendas (`apps/core-web/src/routes/{companies,products,resellers}/`) CRUD completo (lista + criar/editar) para os 3 módulos, layout modelado em `purchase/vendors.tsx` (empresas/revendas) e `inventory/products.tsx` (produtos, com máscara de moeda para as 5 faixas de preço + implantação). Novo componente `Table` 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 `` 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. --- ## Busca automática de CNPJ (dados da empresa, sócios, inscrição estadual) no Cadastro de Cliente Data: 2026-09-18. O ajuste pedido na leva anterior era este: usuário apontou `/home/handix/cadastro.md` (fora do repo), documentando como o legado OrçaFácil consulta CNPJ pra pré-preencher razão social/endereço/inscrição estadual e a lista de sócios (pra usar depois na confecção do contrato/assinatura), via duas APIs públicas gratuitas (cnpj.ws principal — única com inscrição estadual —, BrasilAPI como fallback). **Achado real antes de implementar**: `pj_cnpj` (e `pf_cpf`/`pj_rep_cpf`) **não tinham nenhuma forma de ser gravados** — `UpdateClientRegistrationDto` os exclui deliberadamente, com um comentário no próprio código: "Changing the document itself would need a dedicated re-verification flow — never a side effect of a generic admin PATCH." Era exatamente essa lacuna, já prevista mas nunca fechada, que esta feature resolve. ### Backend (agente `eden-commercial`) `packages/integrations/src/cnpj-lookup/lookup.ts` (novo adapter, `fetch` nativo do Node 22, sem dependência nova): `lookupCnpj(cnpj)` — cnpj.ws primário, BrasilAPI fallback só em erro de rede/5xx (404 explícito do primário = "não existe", não cai pro fallback), timeout de 10s. Endpoint novo `POST /v1/client-registrations/:id/cnpj-lookup` — uma ação só, dedicada (sem endpoint de "preview" separado, mais fiel ao "ao perder o foco já preenche" do legado): o servidor SEMPRE refaz a própria consulta autoritativa a partir do CNPJ recebido (nunca confia num payload do cliente dizendo o que "já encontrou"), grava `pj_cnpj` + todos os campos PJ + endereço via uma função de repositório nova e dedicada (`applyCnpjLookup`, o único lugar que pode tocar `pj_cnpj` — `COALESCE` em tudo mais pra nunca apagar dado já preenchido com um campo vazio da API), cria sócios novos com dedup por `document` (rodar de novo não duplica), devolve `{registration, partners}`. 404 se o CNPJ não for encontrado em nenhuma fonte (nenhum efeito colateral). 400 se o cadastro for PF. **Decisão registrada**: sócios vindos do lookup usam `source: 'cnpj_lookup'`, não o `'brasilapi'` hardcoded do legado — já que cnpj.ws é a fonte primária real nesta porta, o valor do legado seria uma atribuição enganosa. Verificado: `pnpm turbo run typecheck build` 28/28, suíte 100/100 (95→100, 5 testes novos com `lookupCnpj` mockado, sem dependência de rede). Testado ao vivo contra internet real: Petrobras (`33000167000101`, 10 sócios) e Banco do Brasil (`00000000000191`, 96 sócios) — dados corretos, inscrição estadual resolvida corretamente (ativa por UF, fallback pra primeira do array se nenhuma ativa); reexecução no mesmo cadastro não duplicou sócios; CNPJ inexistente devolveu 404 sem alterar nada. Achado ao vivo (não no plano original): cnpj.ws também devolve 429 (rate limit da própria API pública) além dos 5xx documentados — tratado igual, cai pro fallback normalmente. ### Frontend (agente `eden-frontend`) `ClientRegistrationDetailPage.tsx`: campo CNPJ (dentro de "Dados PJ") agora tem dois estados — sem `pj_cnpj` ainda: `Input` editável + botão "Buscar CNPJ" (desabilitado até 14 dígitos); já com `pj_cnpj`: valor formatado somente-leitura + botão "Buscar novamente" (permite atualizar situação cadastral/endereço/IE que podem mudar com o tempo, sem redigitar o CNPJ). Sucesso mostra quantos sócios ficaram no cadastro; 404 mostra a mensagem exata da API num `Alert` de aviso; a lista de Sócios (`PartnersSection.tsx`, componente independente) ganhou um prop `refreshKey` pra recarregar depois de um lookup bem-sucedido, sem precisar de F5. ### Verificação `pnpm turbo run typecheck build` 28/28. Suíte 100/100. Testado ao vivo em navegador real via Playwright: cadastro PJ novo → CNPJ da Petrobras → título da página, endereço, todos os campos PJ e os 10 sócios preenchidos automaticamente na tela, confirmado por screenshot, sem reload manual. Cadastro de teste desativado ao final. ### Pendências Lookup de CPF pra pessoa física — fora de escopo, não foi pedido. Refresh automático da situação cadastral/endereço (ex. um job periódico) não existe — "Buscar novamente" é manual, sob demanda. --- ## Reordenação do formulário de Cadastro de Cliente Data: 2026-09-18. Usuário pediu: "primeiro vem os dados PJ depois vem os sócios depois endereço e depois contatos". Implementado diretamente (mudança de layout, sem lógica nova). Ordem final: Status (não mencionado, mantido primeiro) → Dados PF/PJ → Sócios (só PJ) → Endereço → Contatos → Portabilidade/botão de salvar (não mencionados, mantidos no final). `ClientRegistrationDetailPage.tsx`: o `
` único virou dois — `PartnersSection` (Sócios) já renderiza seu próprio `` interno (form aninhado é HTML inválido), então não dá pra intercalar sem quebrar essa regra. Os dois `` chamam o mesmo `handleSubmit`, que sempre lê o estado React completo (`form`), nunca o DOM — testado ao vivo que salvar a partir do botão (agora depois de Contatos, no segundo ``) grava corretamente campos da seção Status (primeiro ``, antes de Sócios) junto com campos de Endereço (segundo ``), confirmando que a divisão é só visual. ### Verificação `pnpm turbo run typecheck build` 28/28. Suíte 100/100 (inalterada — mudança é só de frontend). Testado ao vivo em navegador real via Playwright: ordem das seções confirmada por leitura do DOM (`Status, Dados PJ, Sócios, Endereço, Contatos`) e por screenshot; ciclo completo com CNPJ real (Petrobras) mostrando os 10 sócios na nova posição; salvamento testado preenchendo um campo de cada `` e confirmando após reload que os dois persistiram. Cadastro de teste desativado ao final. --- ## Access token: 15min → 8h Data: 2026-09-18. Usuário pediu diretamente: "ajuste o tempo de acesso do token preciso que seja no mínimo 8 horas". Implementado diretamente (mudança pequena, uma constante). `ACCESS_TOKEN_TTL_SECONDS` em `apps/api/src/auth/token.service.ts`: `15 * 60` → `8 * 60 * 60`. Achado ao investigar antes de mudar: o TTL curto original era uma decisão deliberada e documentada (ADR-0003, motivada pelo Master Prompt §5.3 — reduzir a janela de uso de um token vazado), mas o frontend nunca implementou refresh silencioso (`AuthContext.tsx`/`session.ts` documentam isso explicitamente: "No refresh-token rotation in this leva") — na prática, 15min derrubava o usuário no meio do trabalho, sem nenhum mecanismo automático de renovação. 8h é um paliativo real pro problema real, não só "afrouxar por afrouxar": documentado como tal num adendo na própria ADR-0003, com a mitigação que já existe (revogação de sessão continua granular, não depende do TTL do JWT) e o trabalho futuro real que resolveria isso sem abrir mão de segurança (refresh silencioso no frontend). ### Verificação `pnpm turbo run typecheck build` 28/28. Suíte completa 106/106 (após a leva de Revenda abaixo, rodada em conjunto). Testado ao vivo: login real devolve `expiresIn: 28800` (exatamente 8h). --- ## Cadastro de Revenda — mesma expansão do Cadastro de Cliente (CNPJ, campos, reordenação) Data: 2026-09-18. Usuário pediu pra aplicar em Revenda a mesma coisa feita em Cliente: "fazer a busca automática de cnpj e expandir campos de cadastro e reordenar os campos". A tabela `resellers` até agora era minimalista (`name`/`document`/`contact_email`/`active`, sem endereço, sem sócios, sem os campos PJ que `client_registrations` já tinha). ### Backend (agente `eden-commercial`) Migration `packages/database/migrations/1789700000000_resellers-expansion.cjs`: `resellers` ganha `trade_name`, `state_registration`, `municipal_registration`, `opening_date`, `legal_nature`, `cnpj_situation`, `main_activity`, `address_*`, `contact_principal/financial/technical_*` — mesmo shape de `client_registrations`, sem o prefixo `pj_` (não existe ambiguidade PF/PJ aqui — decisão deliberada: revenda não ganhou um `person_type`, é sempre tratada como PJ). Tabela nova `reseller_partners`, mesmo shape de `client_registration_partners`. Endpoints novos em `resellers.controller.ts`: `GET/POST /v1/resellers/:id/partners`, `PATCH /v1/resellers/:id/partners/:partnerId`, e `POST /v1/resellers/:id/cnpj-lookup` — **reaproveitando o adapter `lookupCnpj` já existente** (`@eden/integrations`, construído pra Cliente), mesma mecânica exata (servidor sempre reconsulta a fonte autoritativa, `COALESCE` em tudo que a API pode não trazer, dedup de sócios por `document`, `source: 'cnpj_lookup'`). **Decisão deliberada, diferente de Cliente**: `document` continua livremente editável via `PATCH` genérico — ao contrário de `pj_cnpj` em `client_registrations` (que é bloqueado de propósito, só a ação dedicada de lookup grava). Aqui a busca de CNPJ é só uma conveniência adicional, não uma trava nova. ### Verificação `pnpm db:migrate`/`down`/`migrate` de novo — limpo. `pnpm turbo run typecheck build` 28/28. Suíte 106/106 (100→106, 6 testes novos). Testado ao vivo contra CNPJ real (Petrobras): nome/nome fantasia/inscrição estadual/endereço/10 sócios corretos, reexecução sem duplicar, `PATCH` manual em `document` confirmado ainda funcionando depois do lookup. ### Pendências UI ainda não construída — só o backend nesta leva (frontend é o próximo passo natural, mesmo padrão de Cliente: Dados PJ → Sócios → Endereço → Contatos, já direto na ordem certa desta vez, sem precisar de uma segunda leva de reordenação). --- ## UI de Cadastro de Revenda — CNPJ, sócios, endereço, contatos Data: 2026-09-18. Frontend da leva anterior (agente `eden-frontend`), construído direto na ordem pedida: Dados da revenda (com busca de CNPJ) → Sócios → Endereço → Contatos → Status. `ResellerFormPage.tsx` reescrita: ao criar uma revenda nova (sem `id` ainda), continua só com os campos mínimos (Nome/Documento/E-mail/Ativo) — CNPJ-lookup e Sócios precisam de um `id` existente. Diferença de UX deliberada em relação a Cliente: depois de criar, o usuário é redirecionado direto pra `/resellers/:id/edit` (antes voltava pra lista) — senão não teria como usar as novidades sem um clique extra. `PartnersSection.tsx` foi generalizado (`apps/core-web/src/components/PartnersSection.tsx`, funções de API via prop) em vez de duplicado — a versão de Cliente ficou como estava, pronta pra migrar depois se fizer sentido, sem necessidade agora. **Bug real encontrado e corrigido na verificação ao vivo (por mim, não pelo agente)**: o rótulo do botão ("Buscar CNPJ" vs. "Buscar novamente") estava amarrado à contagem de dígitos do campo `document` sendo DIGITADO (`form.document`, ainda não salvo) em vez de ao valor já persistido no servidor — resultado: digitar um CNPJ válido já trocava o botão para "Buscar novamente" antes de qualquer busca real ter acontecido, uma mensagem enganosa (funcionalmente a busca ainda funcionava, mas o texto mentia sobre o estado). Corrigido com um novo estado `savedDocument` (preenchido só a partir do que o servidor confirma — ao carregar, ao salvar manualmente, ao concluir uma busca), o rótulo agora reflete a persistência real, não a digitação. ### Verificação `pnpm turbo run typecheck build` 28/28 (antes e depois da correção). Suíte 106/106 (inalterada, mudança de frontend). Testado ao vivo em navegador real via Playwright, incluindo o cenário exato do bug (digitar 14 dígitos antes de qualquer busca — botão permanece "Buscar CNPJ" corretamente agora) e o ciclo completo com CNPJ real (Petrobras, nome fantasia e sócio confirmados). Revendas de teste desativadas ao final. --- ## Representante legal da Revenda — preparação pra contrato com revenda Data: 2026-09-18. Usuário: "o cadastro da revenda tem que ser completo pq depois vamos usar esses dados para fazer o contrato com a revenda tb por dentro do erp". Implementado diretamente (mudança pequena e bem entendida, mesmo padrão já estabelecido). Investigando antes de mudar: o módulo de Assinatura Eletrônica já aceita `resellerId` como origem de um envelope (`CreateEnvelopeDto`, alternativa a `clientRegistrationId`), mas os `contratante.signers` de um envelope hoje vêm sempre digitados manualmente no corpo da requisição — não existe ainda nenhuma derivação automática a partir de `client_registration_partners`/`reseller_partners` (achado que vale registrar para quando essa integração for construída de verdade, não resolvido aqui — fora do pedido desta leva). O que faltava de concreto e imediato: `resellers`, ao contrário de `client_registrations`, não tinha um bloco de **representante legal** dedicado (`pj_rep_*` em client_registrations) — só a lista geral de sócios. Um contrato precisa de um signatário nomeado e autorizado, não só uma lista de sócios. Migration `packages/database/migrations/1789700100000_resellers-legal-representative.cjs`: `resellers` ganha `rep_full_name`, `rep_cpf`, `rep_role`, `rep_email`, `rep_mobile_phone`, `rep_authorization_ack` — mesmo shape de `client_registrations.pj_rep_*`. **Divergência deliberada**: lá `pj_rep_cpf` é estruturalmente bloqueado do PATCH genérico (só seria setável pelo fluxo de submissão pública original do legado, que o EDEN ainda não construiu — hoje é uma lacuna conhecida, o campo nunca é preenchido de fato). Revenda não tem esse fluxo de submissão pública (é sempre cadastro de backoffice), então não existe "momento de submissão original" pra travar atrás — `rep_cpf` aqui é um campo comum, livremente editável via `PATCH`, sem repetir uma trava que só faria sentido lá. Backend (`CreateResellerDto`, `resellers.service.ts`, repositório) e frontend (`ResellerFormPage.tsx`, novo card "Representante legal" entre "Dados da revenda" e "Sócios") seguindo o mesmo padrão já usado no resto do módulo. ### Verificação `pnpm db:migrate`/`down`/`migrate` de novo — limpo. `pnpm turbo run typecheck build` 28/28. Suíte 107/107 (106→107, 1 teste novo cobrindo set+update dos campos). Testado ao vivo em navegador real: ordem confirmada (`Dados da revenda → Representante legal → Sócios → Endereço → Contatos → Status`), Nome/CPF do representante preenchidos e confirmados persistentes após reload da página. Revenda de teste desativada ao final. ### Pendências A derivação automática de signatários de contrato a partir de `client_registration_partners`/`reseller_partners`/representante legal (pra realmente "usar na confecção do contrato") ainda não existe — hoje `CreateEnvelopeDto.signers` continua sendo preenchido manualmente independente da origem. Esse é o trabalho real que fecha o ciclo que o usuário descreveu; os dados já ficam prontos e completos esperando por ele. --- ## Derivação automática de sócios/representante na criação do envelope de assinatura Data: 2026-09-18. Fecha exatamente a pendência registrada acima. Usuário: "vamos puxar sócios/representante automaticamente na criação do envelope de assinatura". Implementado diretamente (módulo de assinatura é sensível o suficiente pra não delegar, mesmo padrão já seguido desde a leva original do módulo). `CreateEnvelopeDto.signers` passou de obrigatório pra opcional. `EnvelopeService.create()`: se `signers` vier vazio/omitido, deriva automaticamente a partir da origem do envelope (`clientRegistrationId` XOR `resellerId`, já validado): - **Cliente PF**: o próprio titular (`pf_full_name`/`pf_cpf`/`pf_email`/`pf_mobile_phone`) — não tem sócios nem representante, é a única pessoa possível. - **Cliente PJ**: o representante legal (`pj_rep_*`) se `pj_rep_authorization_ack = true`, mais todo sócio de `client_registration_partners` marcado `will_sign = true`. - **Revenda**: mesma lógica, `rep_*`/`reseller_partners`. - **Dedup por CPF**: se o representante também aparecer como sócio com o mesmo CPF (possível em revenda, onde `rep_cpf` é editável — não em cliente, onde `pj_rep_cpf` nunca é setável, lacuna pré-existente já registrada antes), conta uma vez só. - **Nunca falha silenciosamente**: sócio marcado `will_sign` sem `signature_email` cadastrado → 400 nomeando o sócio, pedindo pra completar o cadastro; nada disponível pra derivar (sem representante autorizado, sem sócio marcado) → 400 claro. `signers` explícito no corpo da requisição continua funcionando exatamente como antes — override manual completo, ignora a derivação inteiramente. ### Verificação `pnpm turbo run typecheck build` 28/28. Suíte 113/113 (107→113, `test/signature-envelope-auto-signers.spec.ts` novo — 6 testes cobrindo PJ com rep+sócio, PF com titular, dedup por CPF em revenda, os dois casos de 400, e o override manual). Testado ao vivo contra a API real: criar revenda com representante autorizado + criar envelope sem informar `signers` → o `contratante` do envelope saiu preenchido automaticamente com os dados do representante, confirmado via `curl`. ### Achado adicional, resolvido no caminho (não pedido, mas real) Usuário reportou "testei o cadastro de revenda e continua só com nome/documento/e-mail" — investigado ao vivo: a tela de edição de uma revenda existente já mostrava todas as seções corretamente (confirmado abrindo a revenda real "Handix" pela lista). O que ele via era a tela de **criação** (`/resellers/new`), que por design mesmo só mostra os 3 campos mínimos até a revenda ser salva (CNPJ/sócios/representante precisam de um ID existente) — mas sem nenhuma explicação disso na tela, parecendo um cadastro incompleto. Adicionado um aviso claro no formulário de criação: "Salve com o nome pra continuar o cadastro — busca de CNPJ, representante legal, sócios, endereço e contatos ficam disponíveis logo em seguida, na tela de edição desta revenda." Confirmado ao vivo via screenshot. --- ## Nova oferta — catálogo navegável + resumo ao vivo, no padrão do legado OrçaFácil Data: 2026-09-18. Usuário mandou uma captura de tela de como o legado montava essa tela (produtos navegáveis à esquerda, resumo se montando ao lado conforme adicionava) e um link de uma página de exemplo do tema DreamsERP como "só uma ideia" — na prática essa página do tema acabou sendo só inspiração de acabamento solta (é uma tabela de fatura genérica, não tem o padrão de catálogo+carrinho do legado). Pedido: "pode fazer mais legal". `QuoteFormPage.tsx` (agente `eden-frontend`): a seção de itens, que era uma tabela chata de linhas com dropdown de produto (sem preço visível, sem busca, sem total em tempo real), virou duas colunas — **"Adicionar Produtos"** à esquerda (busca por código/nome, ordenar por Código/Nome, lista rolável com badge de código, preço da faixa do período de contrato selecionado, tag "plano Nm", clique adiciona ao carrinho) e **"Resumo da Oferta"** à direita, sticky (cartão por item com stepper de quantidade e remover, breakdown de implantação por item, os campos que já existiam — implantação geral, condição de pagamento, condição especial/valor mensal final, observações — todos migrados pra dentro deste painel, e totais calculados ao vivo com "Total do Contrato" em destaque). Fórmula do preview espelha `apps/api/src/quotes/pricing.ts::computeQuotePricing` exatamente — mesma disciplina já documentada no próprio arquivo desde que foi criado: todo total mostrado aqui é ilustrativo, o servidor recalcula tudo ao salvar, nada muda no backend. **Achado real do próprio agente**: produtos sem preço cadastrado pra faixa do período de contrato selecionado (existem de verdade nos dados seedados) precisavam de tratamento — aparecem na lista como "indisponível p/ este período", não clicáveis, em vez de quebrar ou mostrar "R$ null/mês". **Limpeza feita por mim depois**: a lista de produtos navegável tornou a poluição de dados de teste (`qa-test-*`, acumulada a sessão inteira nas suítes de `product-catalogs`) muito mais visível do que era como dropdown — 173 de 177 produtos ativos eram lixo de teste. Desativados (mesma disciplina de higiene já seguida o resto da sessão), junto com uma nova rodada de limpeza nos catálogos de Tipo/Grupo/Subgrupo/Marca/Unidade (mais rodadas de suíte desde a última limpeza tinham acumulado mais linhas). Sobrou 1 produto real ativo ("Link Dedicado 100Mbps"). ### Verificação `pnpm turbo run typecheck build` 28/28. Suíte 113/113 (inalterada — mudança é só de frontend). Testado ao vivo pelo agente: busca/ordenação filtrando corretamente, período de contrato trocando o preço mostrado e o badge de plano, stepper incrementando/decrementando (desabilita em 1), totais batendo à mão com a fórmula pra vários produtos/quantidades, duas ofertas completas criadas de ponta a ponta com o total final batendo exatamente com o que o servidor calculou (uma delas disparando aprovação por desconto, confirmado o banner "aguarda aprovação"). Confirmado por mim depois, com screenshot, com o catálogo já limpo. ### Achado não resolvido (fora de escopo, registrado pelo agente) Na tela de sucesso pós-criação, o campo "Implantação total" mostra só `implementation_fee` (implantação dos produtos), sem somar `impl_geral` — quando esse campo é preenchido, o rótulo sub-representa o valor que efetivamente compõe o "Total do contrato". Não é um bug introduzido por esta leva, é pré-existente; não corrigido por estar fora do escopo pedido (só a tela de montagem da oferta).