Commit Graph

16 Commits

Author SHA1 Message Date
1545666f17 feat(documents): editor de texto rico (Tiptap) para templates de documento
Substitui o textarea de texto puro por um editor visual real, seguindo o
print de referência do usuário do editor "Contrato de Software" do
OrçaFácil legado: toolbar completa (negrito/itálico/sublinhado/tachado,
alinhamento, listas, tabela, imagem, quebra de página), variáveis inseridas
como chip inline via catálogo fechado (nunca {{texto livre}}), painel
"Variáveis usadas neste modelo" (com badge obrigatório ao vivo) e
"Histórico de versões".

Backend: editor-content-renderer.ts e merge-engine.ts portados/reescritos
a partir do renderEditorContent.js/variableRegistry.js reais do legado
(achados em .tmp/orcafacil-reference) — resolve a árvore Tiptap
diretamente em vez de regex sobre HTML cacheado. Novo catálogo de
variáveis + GET /documents/variable-catalog.

Fora de escopo desta leva (documentado em docs/progress.md): nó
`conditional`, repetição de linha de tabela, bloqueio por variável
obrigatória ausente, e auto-resolução de dados reais do cadastro na hora
de gerar o documento.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 19:49:42 -03:00
553c0d9533 feat(documents): pré-visualização em PDF do rascunho via Chromium
Novo endpoint POST /documents/templates/:id/draft/preview: renderiza o
conteúdo atual (ainda não salvo) do editor com placeholders {{variavel}}
substituídos por [variavel], via Chromium real, sem persistir nada
(sem generation, sem upload S3) — permite validar visualmente o
resultado antes do "Publicar", que é uma ação permanente.

Corrige de passagem um bug real: css_content do template nunca chegava
ao renderPdf na geração real de documento (createGeneration), sendo
descartado silenciosamente.

No frontend, a pré-visualização aparece embutida num iframe na própria
tela (não em nova aba): abrir em nova aba se mostrou não-confiável ao
vivo — a navegação tardia de uma aba pré-aberta pode ser ignorada pelo
navegador quando o gesto do usuário que a autorizou já expirou no tempo
que o Chromium leva pra renderizar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 23:59:02 -03:00
57f83598b6 feat(signatures): deriva sócios/representante automaticamente na criação do envelope
signers no CreateEnvelopeDto passa a ser opcional. Quando omitido, o
contratante é derivado automaticamente da origem do envelope:
representante legal autorizado (pj_rep_*/rep_*) + sócios marcados
will_sign (client_registration_partners/reseller_partners), deduplicado
por CPF; para cliente PF, o próprio titular. signers explícito continua
funcionando como override manual completo, sem mudança de comportamento.

Nunca falha silenciosamente: sócio marcado pra assinar sem e-mail de
assinatura, ou nada disponível pra derivar, retorna 400 com mensagem
clara em vez de criar um envelope incompleto.

Suíte: 113/113 (107→113). pnpm turbo run typecheck build: 28/28.
Testado ao vivo contra a API real.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 16:37:35 -03:00
d81553fcfe feat(resellers): campos de representante legal (preparação p/ contrato)
Usuário: o cadastro de revenda precisa ficar completo porque essa
mesma base vai alimentar a criação de contrato com a revenda dentro
do ERP. resellers ganha rep_full_name/rep_cpf/rep_role/rep_email/
rep_mobile_phone/rep_authorization_ack, mesmo shape de
client_registrations.pj_rep_* — um contrato precisa de um signatário
nomeado e autorizado, não só a lista geral de sócios.

Divergência deliberada: ao contrário de pj_rep_cpf (bloqueado do PATCH
genérico em client_registrations, esperando um fluxo de submissão
pública que o EDEN ainda não construiu — hoje uma lacuna conhecida,
nunca preenchido de fato), rep_cpf aqui fica livremente editável —
revenda não tem esse fluxo de submissão pública pra travar atrás.

Nova tela: card "Representante legal" entre "Dados da revenda" e
"Sócios". Suíte: 107/107 (106→107). pnpm turbo run typecheck build:
28/28. Testado ao vivo em navegador real.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 15:41:16 -03:00
2ae61dba22 feat(resellers): expande cadastro (dados PJ, endereço, contatos, sócios) + busca de CNPJ
Mesma expansão feita no Cadastro de Cliente, aplicada a Revenda:
razão social/nome fantasia, inscrição estadual, natureza jurídica,
endereço, contatos e sócios — tabela reseller_partners nova. Endpoint
POST /v1/resellers/:id/cnpj-lookup reaproveita o adapter lookupCnpj já
existente (@eden/integrations), sem duplicar a lógica de consulta.

Decisão deliberada: revenda não ganhou person_type (sempre tratada
como PJ) e document continua livremente editável via PATCH genérico
(diferente de client_registrations, que bloqueia pj_cnpj de propósito)
— a busca de CNPJ aqui é só conveniência adicional, não uma trava nova.

Suíte: 106/106 (100→106). pnpm turbo run typecheck build: 28/28.
Testado ao vivo com CNPJ real (Petrobras, 10 sócios).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 14:47:23 -03:00
93cd42b22e feat(client-registrations): busca automática de CNPJ (dados, sócios, IE)
Ao digitar/já ter um CNPJ num cadastro de cliente PJ, busca automática
contra a Receita Federal (cnpj.ws primário, BrasilAPI fallback) e
preenche razão social, endereço e inscrição estadual, além de criar os
sócios encontrados (sem duplicar em buscas repetidas) — pra usar depois
na confecção do contrato/assinatura.

Fecha uma lacuna real que já estava prevista mas nunca implementada:
pj_cnpj não tinha nenhuma forma de ser gravado (UpdateClientRegistrationDto
o excluía deliberadamente, esperando por "a dedicated re-verification
flow" — exatamente o que este endpoint é). O servidor sempre refaz a
própria consulta autoritativa a partir do CNPJ recebido, nunca confia
em dado "já encontrado" vindo do cliente.

Suíte: 100/100 (95→100). pnpm turbo run typecheck build: 28/28.
Testado ao vivo contra CNPJs reais (Petrobras, Banco do Brasil) e em
navegador real via Playwright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 13:12:15 -03:00
96200dc2ba feat(products): submenu de Subgrupos aninhado dentro de Grupos
Mesma lógica do submenu "Produtos" já existente: "Grupos" agora expande
seu próprio submenu com "Subgrupos" (AppShell ganha suporte a um
segundo nível de accordion aninhado). Nova tela lista todos os
subgrupos de todos os grupos, com coluna Grupo e criação via dropdown
de grupo explícito — complementa (não substitui) o painel já existente
dentro da tela de um grupo específico.

listProductSubgroups passa a resolver group_name via JOIN.

Suíte: 95/95. pnpm turbo run typecheck build: 28/28 (core-web,
reseller-web e subscriber-web confirmados sem regressão no AppShell
compartilhado).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 08:52:19 -03:00
f38f2407ff feat(products): catálogos de Tipo/Grupo/Subgrupo/Marca/Unidade com submenu e dropdowns
Menu Produtos ganha submenu (AppShell agora suporta itens aninhados) para
cadastrar Tipos/Grupos/Marcas/Unidades; Subgrupo é gerenciado a partir da
tela do Grupo (sempre pertence a um). O cadastro de item passa a usar
dropdowns desses catálogos em vez de texto livre — exatamente o que o
comentário original da migration de products já previa como trabalho
futuro.

Backend: 5 tabelas de catálogo simples (mesmo padrão dos catálogos
fiscais), products ganha as FKs correspondentes com backfill, e
endpoints CRUD (sem exclusão física, active=false é o "apagar").

Corrige também um buraco de integridade real encontrado na integração:
PATCH /v1/products/:id usava COALESCE para os campos de catálogo
opcionais, então não dava para distinguir "não mexer" de "limpar" —
trocar o Grupo de um produto sem tocar no Subgrupo deixava o vínculo
antigo órfão e inconsistente, sem nenhuma validação pegando isso. Agora
a atualização valida o estado efetivo resultante (bloqueia com 400 em
vez de corromper o vínculo) e productSubgroupId: null limpa de verdade.

Suíte: 95/95 verdes (92→95). pnpm turbo run typecheck build: 28/28.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 23:46:14 -03:00
9c14671e19 feat(signatures): UI completa do módulo + rate limiting em rotas públicas
Fecha as duas pendências da leva anterior (módulo de Assinatura
Eletrônica): tela de backoffice para gerenciar envelopes (criação,
envio, cancelamento, checkpoint/finalização, auditoria) e as duas
páginas públicas (/assinar/:token, /verificar/:verificationId).

Backend: novos endpoints de streaming de PDF (documento congelado e
PDF final consolidado, autenticados e via token público) que faltavam
para a UI funcionar — o signatário não tinha como ver o próprio
documento antes de confirmar leitura. Rate limiting (guard próprio,
em memória, chaves por IP e por token) em todas as rotas sem sessão:
auth (login/mfa/forgot-password/reset-password/refresh/logout) e todo
o fluxo público de assinatura/verificação.

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:51:18 -03:00
20b114a0bf feat(signatures): módulo completo de assinatura eletrônica
Porta o motor de assinatura do legado com fidelidade algorítmica
exata — hash-chain (SHA256 encadeado, seq bigserial define a ordem,
não timestamp), OTP (6 dígitos via crypto.randomInt, HMAC-SHA256 com
SIGNATURE_OTP_SECRET, TTL 5min, 5 tentativas, único ativo por vez) e
a máquina de estados de 19 estados do envelope + 8 do signatário —
tudo re-verificado contra o código-fonte real do legado por um
subagente dedicado antes de qualquer linha ser escrita, cruzado com
eden.md módulo 4.

5 tabelas novas (signature_envelopes/signers/documents/
otp_challenges/audit_events, esta última append-only via a mesma GUC
eden.allow_audit_mutation que já protege audit_log desde a Fase 1 —
canonicalização de hash extraída para packages/database/src/
hash-chain.ts, compartilhada entre as duas). Três adaptações
deliberadas ao EDEN (documentadas na migration): documentos vêm de
document_generations em vez de um módulo de anexo genérico que não
existe ainda; reseller_id em vez de reseller_registration_id; só a
versão correta (v2) da canonicalização, sem a v1 do legado (existia
lá só para tolerar hashes pré-correção de bug, EDEN nunca teve isso).

Certificado de conclusão renderizado no servidor via Chromium (não
client-side como no legado) — elimina o upload multipart do
certificado pronto. Os dois passos autenticados de finalização
(seal-checkpoint, finalize) continuam explícitos e manuais, não
automáticos, mesmo sendo tecnicamente possível agora — esse
checkpoint humano antes de uma finalização irreversível é
comportamento de negócio herdado do legado, não um detalhe do
desenho client-side do certificado que deixou de existir.

Fluxo público (token do link, nunca envelopeId/signerId do corpo da
requisição) e verificação pública (só para COMPLETED, recomputa
envelope_hash ao vivo, nomes mascarados) inteiros.

Verificado: 28 testes novos (15 puros — hash-chain/máquina de
estados/OTP/token — e 13 de ciclo de vida completo via HTTP real,
incluindo merge de PDF de verdade via pdf-lib contra saída real do
Chromium mockado). Suíte completa: 70/70 verdes. Testado ao vivo
através do container real, ponta a ponta, sem nenhum mock: criação →
convite → fluxo público completo dos dois signatários (OTP
recuperado via HMAC com o segredo real, já que SMTP não está
configurado neste ambiente) → SIGNED → seal-checkpoint → finalize
(certificado real via Chromium, merge real, upload real no MinIO) →
public-verify — SHA-256 do PDF final baixado bate byte a byte com o
gravado no Postgres, integrityValid:true.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx
2026-09-04 13:44:43 -03:00
d46d44f01b fix(contracts): enforce the edit-lock ADR-0006 promises but never wired
ADR-0006 explicitly commits to preserving the legacy's "trava" once a
contract leaves its editable draft state, overridable only by an
audited super_admin correction — but ContractsService.addItem/addParty
had no status or role check at all, letting anyone with contracts:edit
add items/parties to a contract in any status, including a signed
active one.

Adds assertEditable() (same shape as Quotes' assertNotLocked): draft
is the only free-editing state; past it, only super_admin can still
write, and that write is tagged distinctly in the audit log
(contract.item_added_override) rather than looking like an ordinary
edit. No frontend change needed — ContractDetailPage never called the
add-item/party endpoints in the first place.

Verified live: super_admin override still works past draft, and the
audit_log row for it carries overrode:true and the contract's status
at the time — confirmed directly in Postgres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx
2026-09-04 12:21:47 -03:00
ccac936372 feat(quotes): anexo de conformidade ANATEL/RGC no PDF de oferta
Porta o anexo regulatório do legado (Resolução Anatel 765/2023, RGC,
recorte STFC) com fidelidade total ao conteúdo legal, incluindo a
seção de tarifação — em vez do html2canvas+jsPDF client-side do
legado, usa o Chromium server-side já construído para Documents.

Schema: products ganha service_type/metered/tariff_rates/has_ldi
(migration 1788525270647) — campos que a migration original de
products tinha deliberadamente deferido. Snapshot no item da oferta
na criação (nunca referência viva ao produto). Novo pacote
compartilhado packages/domain-shared/src/telecom para o catálogo de
tipos e legendas de tarifação.

Endpoint GET /v1/quotes/:id/anatel-pdf (400 sem item STFC), sempre
renderizado ao vivo (nunca cacheado, já que o conteúdo deriva de
updated_at). Frontend: campos de produto/oferta necessários +
"Baixar PDF ANATEL" na tela de detalhe da oferta. Inclui também o
botão de download de PDF em Documentos (endpoint já existia, faltava
o link na UI).

Verificado ao vivo através do container real: PDF gerado com
Chromium de verdade, texto extraído confere byte a byte com os
valores computados (multa, datas, tarifas, resumo financeiro).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx
2026-09-04 10:33:33 -03:00
29430d0d96 feat(documents): renderização real de PDF via Chromium headless
Substitui a geração de PDF simulada (pdf_s3_key/pdf_sha256/pdf_size_bytes
sempre nulos) por Chromium real (Playwright), portado do
chromiumPdfRenderer.js do legado: SSRF impossível por construção
(setContent + bloqueio de toda requisição de rede da página), browser
único reaproveitado com relançamento automático, semáforo de
concorrência + timeout duro.

Infra: MinIO (compose.yaml, primeiro consumidor real do adapter S3) e
instalação condicional do Chromium só na imagem da API
(Dockerfile.node). DocumentsService.createGeneration agora renderiza,
faz upload e persiste hash/tamanho de verdade; novo endpoint de
download GET /v1/documents/generations/:id/pdf.

Verificado ao vivo através do container real: PDF gerado, baixado, e
SHA-256 conferido byte a byte contra o valor persistido no Postgres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx
2026-09-04 09:26:31 -03:00
4d23af3b23 feat(auth): e-mail transacional real + recuperação de senha
Liga o adapter SMTP existente (packages/integrations, já era fail-soft)
a um consumidor de verdade e adiciona o fluxo completo de recuperação
de senha, faltante desde a Fase 1.

- UsersService.invite() agora envia e-mail com a senha temporária
  (mantém o retorno na resposta também — dupla entrega, eden.md §4.8).
- POST /v1/auth/forgot-password + POST /v1/auth/reset-password: token
  de 256 bits, só o hash SHA-256 persistido (users.reset_token_hash/
  reset_token_expires_at — colunas que já existiam desde a baseline da
  Fase 1, nunca usadas até agora), validade de 1h, uso único, revoga
  todas as sessões ativas do usuário ao concluir. Mesma disciplina
  anti-enumeração do login: resposta idêntica para e-mail cadastrado
  ou não.
- Frontend: link "Esqueci minha senha" no LoginPage, novas
  ForgotPasswordPage/ResetPasswordPage.

Teste de integração novo (password-reset.spec.ts, 3 testes) usa
vi.mock para interceptar sendMail e recuperar o token real do corpo do
e-mail — única forma de testar o caminho feliz de ponta a ponta, já
que o token nunca é devolvido na resposta da API. Suíte completa:
33/33 (30→33). pnpm turbo run typecheck build (28/28). Testado ao vivo
através do proxy same-origin: anti-enumeração confirmada com curl
real, token gravado no Postgres, token bogus rejeitado (400).

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 08:02:33 -03:00
eff515b70d 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>
2026-09-03 17:25:31 -03:00
cdafb7f853 test(api): add Vitest+supertest integration test suite (eden-qa)
Boots the real Nest app (Test.createTestingModule + AppModule) against a
real Postgres — no mocked DB or guards anywhere in this suite. Uses SWC
(unplugin-swc) instead of Vite's default esbuild transform because
esbuild doesn't implement emitDecoratorMetadata, which silently breaks
NestJS's constructor DI (every provider resolves to undefined).

Covers:
- auth.spec.ts: anti-enumeration login message, refresh-token rotation,
  replay-detection revoking the whole session chain, idempotent logout
  (ADR-0003).
- role-weight.spec.ts: invalid role_key on invite returns 400 not 500
  (regression guard), lower-weight actor forbidden from assigning a
  heavier role even when holding users:edit (ADR-0004).
- companies.spec.ts: CNPJ digit-check validation (reject/accept).
- mass-assignment.spec.ts: forbidNonWhitelisted actually rejects
  unexpected fields on companies/products.
- reseller-assignment.spec.ts: assign-user always targets the URL's
  :id, never a body resellerId (regression guard for commit 8d108e0);
  unassign-user clears the assignment.

All fixtures are qa-test-/qa_ tagged and cleaned up (deactivated where
no DELETE endpoint exists, deleted where one does) — verified no
pre-existing seed data (super_admin, Handix Telecom LTDA, Handix revenda
própria) was touched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 11:14:43 -03:00