feat(api,ui): MFA/TOTP, Inventory, Support, HR-Timeclock, Fiscal, Billing e Design System

Leva grande, pedida como "pelo menos mais 20 etapas, use subagentes":
fecha a pendência de MFA/TOTP da Fase 1 (feito diretamente, autenticação é
sensível demais para delegar) e lança 5 subagentes em paralelo em 2 ondas
(eden-inventory, eden-support, eden-hr-timeclock; depois eden-finance,
eden-fiscal, eden-frontend), cada um em worktree isolado, com integração
seletiva manual (barrel, app.module.ts, migrations renomeadas/aplicadas,
build/typecheck/teste ao vivo antes de cada merge).

## Backend

- MFA/TOTP (`apps/api/src/auth/mfa.service.ts`, `crypto/field-encryption.service.ts`):
  login em duas etapas quando `mfa_enabled=true`, `FieldEncryptionService`
  genérico (AES-256-GCM versionado, ADR-0005) reaproveitado por outros
  segredos reversíveis (ex. credencial de equipamento Control iD).
- Inventory: warehouses, stock_movements (ledger append-only), serialized_assets,
  asset_assignments, stock_reservations — invariante "serial/patrimônio/MAC
  nunca duplicado entre ativos ativos" reforçada por índices únicos parciais
  no banco (Master Prompt §15.3).
- Support/Service Desk: tickets com máquina de estados, SLA com pausa/retomada
  em `waiting_customer`/`waiting_third_party` (funções puras testáveis).
- HR-Timeclock (fundação): dispositivos Control iD (senha cifrada), import de
  AFD com parser + CRC-16/KERMIT, `afd_records` imutável sem exceção nem
  para super_admin (sem válvula de escape, diferente de audit_log).
- Fiscal: NCM/CFOP/municípios/naturezas de operação (eden.md módulo 5, MVP de
  5 das ~19 tabelas), product_fiscal_profiles com invariante de único
  profile ativo por billing_component.
- Billing: subscription→billing_run→invoice idempotente (regenerar em draft
  substitui, nunca duplica) e imutável após consolidar (ADR-0008: nunca
  confunde faturamento/documento fiscal/recebimento).
- FKs cruzadas fechadas: quotes/client_registrations/contracts.

## Frontend — primeira leva

- `packages/ui`: Design System real extraído do tema DreamsERP comprado —
  achado que mudou o escopo: o tema já vem em React+Tailwind (não só
  Angular), tokens visuais reais portados (cores, Geist como fonte real,
  divergindo da doc antiga que citava Nunito/Poppins).
- `apps/core-web`: login real (com suporte a MFA), sessão, shell autenticado
  via `AppShell` — substitui a tela estática "Bootstrap da Fase 1".

## Bugs reais encontrados e corrigidos (todos via teste ao vivo, não leitura)

1. **Segurança**: o token de desafio MFA era aceito como access token normal
   pelo `JwtAuthGuard` (mesma assinatura, nenhuma checagem da claim
   `purpose`) — bypass total do segundo fator. Explorado e fechado ao vivo
   contra o container real; teste de regressão em `test/mfa.spec.ts`.
2. Crase solta dentro de comentário SQL na migration de hr-timeclock quebrava
   o template literal JS (`SyntaxError` ao rodar a migration).
3. CORS ausente por completo na API.
4. `turbo.json` não declarava `VITE_API_BASE_URL` como input de build — o
   Turborepo cacheava o bundle do frontend ignorando mudança na env var.
5. `compose.yaml` usava `${VITE_API_BASE_URL:-/api/v1}`, que colidia com a
   variável de mesmo nome do `.env` local — o default nunca era aplicado.
6. Não existia `.dockerignore` — todo build copiava node_modules/.git/
   worktrees de subagentes (665MB) para dentro do contexto Docker.

Decisão do operador incorporada: a API nunca tem domínio público próprio
(só IP interno) — os 3 frontends chamam por caminho relativo `/api/v1`,
proxy externo roteia same-origin; CORS só é relevante para dev local.

Verificado: `pnpm turbo run typecheck build` (28/28), `pnpm --filter
@eden/api test` (30/30, cresceu de 15), todo o lote testado ao vivo via
curl contra os containers reais reconstruídos, incluindo os 6 bugs acima
já fechados e confirmados, e a trilha de auditoria conferida no Postgres.

Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 17:25:31 -03:00
parent 18838a9031
commit eff515b70d
105 changed files with 8875 additions and 21 deletions

View File

@@ -115,7 +115,7 @@ Vitest + supertest + `@nestjs/testing` (bootstrap real do Nest, sem mock) contra
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 (schema já existe — `users.mfa_*` — endpoint ainda não implementado).
- ~~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.
@@ -150,6 +150,121 @@ Wiring final feito manualmente: barrel (`packages/database/src/index.ts`) e `app
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 ainda não existem (ficaram como UUID nullable simples, documentado em comentário nas migrations): `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)`.
- ~~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.