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

@@ -0,0 +1,231 @@
/**
* Fiscal bounded context (Master Prompt §6.11, ADR-0011, eden.md módulo 5
* "Módulo Fiscal (NCM, CFOP, Municípios)"). The legacy OrçaFácil has a
* mature ~19-table catalog layer + automatic (Siscomex/IBGE fetch) and
* manual (CSV/XLSX/JSON upload) importers + a sync log (`fiscal_catalog_sync`)
* + `product_fiscal_profiles` linking `products` to fiscal classification.
*
* This MVP builds a coherent, faithfully-shaped SUBSET, end-to-end:
* - fiscal_ncm (this migration) — "simple catalog" shape, eden.md §2.2
* - fiscal_cfop (this migration) — simple catalog + direction/scope, eden.md §2.3/§4
* - fiscal_municipality (this migration) — distinct shape, eden.md §2.3
* - fiscal_operation_nature (this migration) — internal enum-like catalog, eden.md §2.3, seeded
* - product_fiscal_profiles (this migration) — eden.md §2.5, reduced to the
* 4 catalogs actually created here (ncm/cfop/municipality/operation_nature)
*
* Explicitly DEFERRED, not forgotten:
* - The other ~9 "simple catalog" tables (eden.md §2.2): fiscal_product_origin,
* fiscal_icms_cst, fiscal_icms_csosn, fiscal_pis_cst, fiscal_cofins_cst,
* fiscal_ipi_cst, fiscal_lc116_service, fiscal_nbs, fiscal_ibs_cbs_cst,
* fiscal_ibs_cbs_credit_class — same shape as fiscal_ncm/fiscal_cfop, can
* be added later without a redesign.
* - The "extended catalog" tables (eden.md §2.3): fiscal_cest, fiscal_cbenef,
* fiscal_nfcom_cclass, fiscal_nfse_trib_nacional, fiscal_nfse_trib_municipal,
* fiscal_ibs_cbs_classification, fiscal_ibs_cbs_standard_rate.
* - `fiscal_catalog_sync` (append-only sync log, eden.md §2.4) and the
* automatic importers (NCM/Siscomex, Municipality/IBGE — eden.md §3/§5)
* + the generic manual file importer with dry-run and the mass-
* inactivation safety threshold (eden.md §7) — substantial, isolated
* work (network fetch adapters, checksum/threshold logic, file parsing),
* deferred to its own leva.
* - `fiscal_rules` (eden.md §2.6) — schema-ready-but-unconsumed even in the
* legacy itself ("estrutura pronta, sem UI/consumo ainda"); not recreated
* here, stays future work until the fiscal resolution engine is built.
* - `products.ixc_product_code` / `products.product_code` (eden.md §6) —
* belong to the Products migration, not this one.
* - product_fiscal_profiles columns for catalogs not yet created here:
* nfcom_cclass_id, nfse_trib_nacional_id, nfse_trib_municipal_id,
* lc116_service_id, nbs_id, cest_id, product_origin_id, ibs_cbs_cst_id,
* ibs_cbs_classification_id, ibs_cbs_credit_class_id (eden.md §2.5) —
* added when their respective catalogs land.
*
* `product_fiscal_profiles.product_id` is deliberately nullable-FK-free:
* NO `REFERENCES products(id)`, same cross-module "FK cruzada adiada"
* pattern used by `contracts.quote_id`/`contracts.client_registration_id`
* (see 1788447802216_contracts.cjs header) and
* `support_tickets.client_registration_id`/`.contract_id`
* (see 1788451374900_support.cjs header) — this worktree may not always
* carry every other module's migration in the same merge order; wiring the
* FK is integration-step work at merge time, not this migration's.
*
* Naming convention: all fiscal tables use `created_at`/`updated_at`
* (not `created_date`/`updated_date`) — this is already EDEN's own
* repo-wide convention (see every other migration in this directory), so
* there is no deviation to reconcile here. eden.md §2.1 calls this "a
* deliberate deviation" only relative to the *legacy* OrçaFácil schema,
* not relative to EDEN.
*/
exports.shorthands = undefined;
exports.up = (pgm) => {
pgm.sql(`
-- =====================================================================
-- fiscal_ncm — Nomenclatura Comum do Mercosul. "Simple catalog" shape,
-- eden.md §2.2. Source: Receita Federal / Siscomex (import deferred).
-- =====================================================================
CREATE TABLE fiscal_ncm (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code TEXT NOT NULL,
description TEXT NOT NULL,
valid_from DATE,
valid_until DATE,
active BOOLEAN NOT NULL DEFAULT true,
source TEXT,
source_version TEXT,
source_updated_at TIMESTAMPTZ,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE fiscal_ncm IS
'Nomenclatura Comum do Mercosul (eden.md §2.2, §3). "Catálogo simples" — mesmo shape reutilizável para as ~9 outras tabelas de catálogo simples ainda não portadas (fiscal_product_origin, ICMS CST/CSOSN, PIS/COFINS/IPI CST, LC116, NBS, IBS-CBS CST/credit_class). Importação automática (Siscomex, checksum, trava de volume mínimo, upsert com inactivateMissing=true) é trabalho DEFERIDO — ver cabeçalho da migration.';
CREATE UNIQUE INDEX idx_fiscal_ncm_code ON fiscal_ncm(code);
CREATE INDEX idx_fiscal_ncm_active ON fiscal_ncm(active);
CREATE TRIGGER trg_fiscal_ncm_updated_at BEFORE UPDATE ON fiscal_ncm
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =====================================================================
-- fiscal_cfop — Código Fiscal de Operações e Prestações. "Simple
-- catalog" shape + direction/scope (eden.md §2.3, §4). Fonte MANUAL
-- (sem endpoint estruturado confiável testado no legado).
-- =====================================================================
CREATE TABLE fiscal_cfop (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code TEXT NOT NULL,
description TEXT NOT NULL,
direction TEXT,
scope TEXT,
valid_from DATE,
valid_until DATE,
active BOOLEAN NOT NULL DEFAULT true,
source TEXT,
source_version TEXT,
source_updated_at TIMESTAMPTZ,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE fiscal_cfop IS
'Código Fiscal de Operações e Prestações (eden.md §2.3, §4). "Catálogo simples" + direction/scope. Fonte declarada "Portal Nacional NF-e" mas tipo MANUAL no legado (sem endpoint estruturado confiável testado) — cadastro por upload de arquivo ou tela, ambos DEFERIDOS nesta leva (ver cabeçalho da migration).';
COMMENT ON COLUMN fiscal_cfop.direction IS
'Direção da operação (entrada/saída) — TEXT livre, sem CHECK/enum, exatamente como o legado (eden.md §4). O 1º dígito do código CFOP codifica direção+âmbito na prática brasileira, mas isso NUNCA é derivado automaticamente do código no legado nem aqui — direction/scope são só campos preenchidos manualmente/via importação.';
COMMENT ON COLUMN fiscal_cfop.scope IS
'Âmbito da operação (estadual/interestadual/exterior) — TEXT livre, sem CHECK/enum, mesma razão de direction (eden.md §4).';
CREATE UNIQUE INDEX idx_fiscal_cfop_code ON fiscal_cfop(code);
CREATE INDEX idx_fiscal_cfop_active ON fiscal_cfop(active);
CREATE TRIGGER trg_fiscal_cfop_updated_at BEFORE UPDATE ON fiscal_cfop
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =====================================================================
-- fiscal_municipality — shape DIFERENTE do catálogo simples: sem
-- valid_from/valid_until/metadata ("não faz sentido um município
-- expirar", eden.md §2.3). Fonte: IBGE (import deferido).
-- =====================================================================
CREATE TABLE fiscal_municipality (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code TEXT NOT NULL,
name TEXT NOT NULL,
state TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT true,
source TEXT,
source_version TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE fiscal_municipality IS
'Municípios/IBGE (eden.md §2.3, §5) — base para ISS/NFS-e. Shape deliberadamente distinto do catálogo simples: sem valid_from/valid_until/metadata ("não faz sentido um município expirar"). code = código IBGE, state = UF. Importação automática (IBGE, checksum, trava de volume mínimo de 5000 linhas) é DEFERIDA — ver cabeçalho da migration.';
CREATE UNIQUE INDEX idx_fiscal_municipality_code ON fiscal_municipality(code);
CREATE INDEX idx_fiscal_municipality_state ON fiscal_municipality(state);
CREATE TRIGGER trg_fiscal_municipality_updated_at BEFORE UPDATE ON fiscal_municipality
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =====================================================================
-- fiscal_operation_nature — cadastro interno, sem fonte externa
-- (eden.md §2.3). Seed fixo via ON CONFLICT DO NOTHING.
-- =====================================================================
CREATE TABLE fiscal_operation_nature (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
description TEXT,
active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE fiscal_operation_nature IS
'Naturezas de operação (eden.md §2.3) — cadastro interno de negócio, não uma fonte externa sincronizável. Seed fixo abaixo, único endpoint da API para este catálogo é leitura (GET /v1/fiscal/operation-natures) — sem criação via API nesta leva.';
CREATE TRIGGER trg_fiscal_operation_nature_updated_at BEFORE UPDATE ON fiscal_operation_nature
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
INSERT INTO fiscal_operation_nature (code, name) VALUES
('VENDA', 'Venda'),
('REMESSA_LOCACAO', 'Remessa em locação'),
('RETORNO_LOCACAO', 'Retorno de locação'),
('REMESSA_MANUTENCAO', 'Remessa para manutenção'),
('RETORNO_MANUTENCAO', 'Retorno de manutenção'),
('STFC_RECORRENTE', 'STFC recorrente'),
('SVA_RECORRENTE', 'SVA recorrente'),
('PABX_RECORRENTE', 'PABX recorrente'),
('PABX_IMPLANTACAO', 'PABX implantação'),
('SOFTWARE_RECORRENTE', 'Software recorrente'),
('SOFTWARE_IMPLANTACAO', 'Software implantação')
ON CONFLICT (code) DO NOTHING;
-- =====================================================================
-- product_fiscal_profiles — liga products à classificação fiscal
-- (eden.md §2.5). Reduzida aos 4 catálogos criados nesta leva.
-- =====================================================================
CREATE TABLE product_fiscal_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID NOT NULL,
billing_component TEXT NOT NULL CHECK (billing_component IN ('RECURRING', 'IMPLEMENTATION')),
document_type TEXT NOT NULL DEFAULT 'NONE' CHECK (document_type IN ('NFCOM', 'NFSE', 'NFE', 'NONE')),
ncm_id UUID REFERENCES fiscal_ncm(id),
cfop_id UUID REFERENCES fiscal_cfop(id),
municipality_id UUID REFERENCES fiscal_municipality(id),
operation_nature_id UUID REFERENCES fiscal_operation_nature(id),
valid_from DATE,
valid_until DATE,
active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID REFERENCES users(id),
updated_by UUID REFERENCES users(id)
);
COMMENT ON TABLE product_fiscal_profiles IS
'Configuração fiscal de produto (eden.md §2.5) — deliberadamente separada de products, nunca duplica campos comerciais (price_*, impl_unit_price, metered, tariff_rates, has_ldi continuam só em products). product_id SEM FK (products pode não existir neste worktree isolado — ver cabeçalho da migration, padrão "FK cruzada adiada" de contracts.cjs/support.cjs). Colunas de referência a catálogos ainda não portados (nfcom_cclass_id, nfse_trib_nacional_id, nfse_trib_municipal_id, lc116_service_id, nbs_id, cest_id, product_origin_id, ibs_cbs_cst_id, ibs_cbs_classification_id, ibs_cbs_credit_class_id) ficam para quando os catálogos correspondentes existirem. Sem backfill automático nesta leva (eden.md §2.5 backfill dependia de products.service_type/impl_unit_price já populados em produção real) — profiles são criados sob demanda via API.';
COMMENT ON COLUMN product_fiscal_profiles.product_id IS
'Sem REFERENCES products(id) nesta leva — products pode não existir neste worktree isolado. FK cruzada é trabalho de integração no merge (mesmo padrão de contracts.quote_id/client_registration_id e support_tickets.client_registration_id/contract_id).';
-- Invariante central (eden.md §2.5): um produto tem no máximo um
-- profile ATIVO por billing_component. Nunca DELETE/UPDATE destrutivo
-- de um profile antigo para "trocar" a classificação — cria-se um novo
-- profile ativo e inativa-se o anterior (active=false), preservando
-- histórico.
CREATE UNIQUE INDEX idx_product_fiscal_profiles_active_component
ON product_fiscal_profiles(product_id, billing_component) WHERE active = true;
CREATE INDEX idx_product_fiscal_profiles_product ON product_fiscal_profiles(product_id);
CREATE TRIGGER trg_product_fiscal_profiles_updated_at BEFORE UPDATE ON product_fiscal_profiles
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
`);
};
exports.down = (pgm) => {
pgm.sql(`
DROP TABLE IF EXISTS product_fiscal_profiles;
DROP TABLE IF EXISTS fiscal_operation_nature;
DROP TABLE IF EXISTS fiscal_municipality;
DROP TABLE IF EXISTS fiscal_cfop;
DROP TABLE IF EXISTS fiscal_ncm;
`);
};