From eff515b70dc3cb1d5d3e3172cbb121829b0ccf48 Mon Sep 17 00:00:00 2001 From: "Matheus (Handix)" Date: Thu, 3 Sep 2026 17:25:31 -0300 Subject: [PATCH] feat(api,ui): MFA/TOTP, Inventory, Support, HR-Timeclock, Fiscal, Billing e Design System MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .dockerignore | 13 + .env.example | 23 + apps/api/package.json | 1 + apps/api/src/app.module.ts | 10 + apps/api/src/auth/auth.controller.ts | 37 +- apps/api/src/auth/auth.module.ts | 13 +- apps/api/src/auth/auth.service.ts | 67 +- apps/api/src/auth/dto/mfa-challenge.dto.ts | 11 + apps/api/src/auth/dto/mfa-code.dto.ts | 7 + apps/api/src/auth/mfa.service.ts | 49 ++ apps/api/src/auth/token.service.ts | 44 +- apps/api/src/billing/billing.controller.ts | 104 +++ apps/api/src/billing/billing.module.ts | 12 + apps/api/src/billing/billing.service.ts | 262 ++++++ .../src/billing/dto/create-billing-run.dto.ts | 18 + .../dto/create-billing-subscription.dto.ts | 30 + .../src/crypto/field-encryption.service.ts | 61 ++ apps/api/src/fiscal/dto/create-cfop.dto.ts | 21 + .../src/fiscal/dto/create-municipality.dto.ts | 12 + apps/api/src/fiscal/dto/create-ncm.dto.ts | 14 + .../dto/create-product-fiscal-profile.dto.ts | 23 + apps/api/src/fiscal/dto/update-cfop.dto.ts | 15 + .../src/fiscal/dto/update-municipality.dto.ts | 10 + apps/api/src/fiscal/dto/update-ncm.dto.ts | 14 + apps/api/src/fiscal/fiscal.controller.ts | 142 ++++ apps/api/src/fiscal/fiscal.module.ts | 12 + apps/api/src/fiscal/fiscal.service.ts | 219 +++++ .../hr-timeclock/afd-imports.controller.ts | 35 + .../src/hr-timeclock/afd-imports.service.ts | 202 +++++ apps/api/src/hr-timeclock/afd-parser.ts | 495 ++++++++++++ .../hr-timeclock/afd-records.controller.ts | 37 + .../src/hr-timeclock/afd-records.service.ts | 45 ++ .../hr-timeclock/dto/create-afd-import.dto.ts | 20 + .../dto/create-timeclock-device.dto.ts | 20 + .../dto/list-afd-records-query.dto.ts | 18 + .../dto/update-timeclock-device.dto.ts | 18 + .../src/hr-timeclock/hr-timeclock.module.ts | 45 ++ .../timeclock-devices.controller.ts | 57 ++ .../hr-timeclock/timeclock-devices.service.ts | 95 +++ .../api/src/inventory/dto/create-asset.dto.ts | 21 + .../inventory/dto/create-reservation.dto.ts | 22 + .../src/inventory/dto/create-warehouse.dto.ts | 16 + .../src/inventory/dto/install-asset.dto.ts | 11 + .../src/inventory/dto/reserve-asset.dto.ts | 7 + .../api/src/inventory/dto/return-asset.dto.ts | 9 + .../src/inventory/dto/update-warehouse.dto.ts | 7 + .../api/src/inventory/inventory.controller.ts | 120 +++ apps/api/src/inventory/inventory.module.ts | 15 + apps/api/src/inventory/inventory.service.ts | 268 +++++++ .../src/inventory/warehouses.controller.ts | 54 ++ apps/api/src/inventory/warehouses.service.ts | 64 ++ apps/api/src/main.ts | 12 + .../support/dto/create-ticket-comment.dto.ts | 10 + apps/api/src/support/dto/create-ticket.dto.ts | 24 + .../support/dto/update-ticket-status.dto.ts | 31 + apps/api/src/support/sla.ts | 76 ++ apps/api/src/support/support.controller.ts | 64 ++ apps/api/src/support/support.module.ts | 12 + apps/api/src/support/support.service.ts | 176 +++++ apps/api/test/afd-parser.spec.ts | 117 +++ apps/api/test/mfa.spec.ts | 148 ++++ apps/core-web/.env.example | 4 + apps/core-web/package.json | 4 +- apps/core-web/public/eden-logo-horizontal.png | Bin 0 -> 454269 bytes apps/core-web/src/App.tsx | 27 +- apps/core-web/src/lib/api/auth.ts | 67 ++ apps/core-web/src/lib/api/http.ts | 73 ++ apps/core-web/src/lib/auth/AuthContext.tsx | 109 +++ apps/core-web/src/lib/auth/session.ts | 34 + .../src/routes/AuthenticatedLayout.tsx | 35 + apps/core-web/src/routes/DashboardPage.tsx | 23 + apps/core-web/src/routes/LoginPage.tsx | 132 ++++ apps/core-web/src/routes/ProtectedRoute.tsx | 24 + apps/core-web/src/vite-env.d.ts | 9 + apps/core-web/tailwind.config.js | 12 +- compose.yaml | 19 + docs/progress.md | 119 ++- infra/docker/Dockerfile.web | 9 + packages/config/src/env-schema.ts | 13 + .../1788450538883_cross-reference-fks.cjs | 55 ++ .../migrations/1788451164762_inventory.cjs | 264 +++++++ .../migrations/1788451374900_support.cjs | 164 ++++ .../migrations/1788452136875_hr-timeclock.cjs | 245 ++++++ .../migrations/1788457368744_fiscal.cjs | 231 ++++++ .../migrations/1788457629203_billing.cjs | 185 +++++ packages/database/src/index.ts | 89 +++ packages/database/src/repositories/billing.ts | 471 +++++++++++ packages/database/src/repositories/fiscal.ts | 484 ++++++++++++ .../database/src/repositories/hr-timeclock.ts | 468 +++++++++++ .../database/src/repositories/inventory.ts | 747 ++++++++++++++++++ .../src/repositories/support-tickets.ts | 423 ++++++++++ packages/database/src/repositories/users.ts | 22 + packages/ui/package.json | 13 +- packages/ui/src/components/Alert.tsx | 29 + packages/ui/src/components/AppShell.tsx | 120 +++ packages/ui/src/components/Badge.tsx | 33 + packages/ui/src/components/Button.tsx | 54 ++ packages/ui/src/components/Card.tsx | 22 + packages/ui/src/components/Input.tsx | 50 ++ packages/ui/src/components/PageHeader.tsx | 20 + packages/ui/src/index.ts | 30 +- packages/ui/src/tailwind-preset.ts | 231 ++++++ packages/ui/src/utils/cn.ts | 4 + pnpm-lock.yaml | 111 +++ turbo.json | 3 +- 105 files changed, 8875 insertions(+), 21 deletions(-) create mode 100644 .dockerignore create mode 100644 apps/api/src/auth/dto/mfa-challenge.dto.ts create mode 100644 apps/api/src/auth/dto/mfa-code.dto.ts create mode 100644 apps/api/src/auth/mfa.service.ts create mode 100644 apps/api/src/billing/billing.controller.ts create mode 100644 apps/api/src/billing/billing.module.ts create mode 100644 apps/api/src/billing/billing.service.ts create mode 100644 apps/api/src/billing/dto/create-billing-run.dto.ts create mode 100644 apps/api/src/billing/dto/create-billing-subscription.dto.ts create mode 100644 apps/api/src/crypto/field-encryption.service.ts create mode 100644 apps/api/src/fiscal/dto/create-cfop.dto.ts create mode 100644 apps/api/src/fiscal/dto/create-municipality.dto.ts create mode 100644 apps/api/src/fiscal/dto/create-ncm.dto.ts create mode 100644 apps/api/src/fiscal/dto/create-product-fiscal-profile.dto.ts create mode 100644 apps/api/src/fiscal/dto/update-cfop.dto.ts create mode 100644 apps/api/src/fiscal/dto/update-municipality.dto.ts create mode 100644 apps/api/src/fiscal/dto/update-ncm.dto.ts create mode 100644 apps/api/src/fiscal/fiscal.controller.ts create mode 100644 apps/api/src/fiscal/fiscal.module.ts create mode 100644 apps/api/src/fiscal/fiscal.service.ts create mode 100644 apps/api/src/hr-timeclock/afd-imports.controller.ts create mode 100644 apps/api/src/hr-timeclock/afd-imports.service.ts create mode 100644 apps/api/src/hr-timeclock/afd-parser.ts create mode 100644 apps/api/src/hr-timeclock/afd-records.controller.ts create mode 100644 apps/api/src/hr-timeclock/afd-records.service.ts create mode 100644 apps/api/src/hr-timeclock/dto/create-afd-import.dto.ts create mode 100644 apps/api/src/hr-timeclock/dto/create-timeclock-device.dto.ts create mode 100644 apps/api/src/hr-timeclock/dto/list-afd-records-query.dto.ts create mode 100644 apps/api/src/hr-timeclock/dto/update-timeclock-device.dto.ts create mode 100644 apps/api/src/hr-timeclock/hr-timeclock.module.ts create mode 100644 apps/api/src/hr-timeclock/timeclock-devices.controller.ts create mode 100644 apps/api/src/hr-timeclock/timeclock-devices.service.ts create mode 100644 apps/api/src/inventory/dto/create-asset.dto.ts create mode 100644 apps/api/src/inventory/dto/create-reservation.dto.ts create mode 100644 apps/api/src/inventory/dto/create-warehouse.dto.ts create mode 100644 apps/api/src/inventory/dto/install-asset.dto.ts create mode 100644 apps/api/src/inventory/dto/reserve-asset.dto.ts create mode 100644 apps/api/src/inventory/dto/return-asset.dto.ts create mode 100644 apps/api/src/inventory/dto/update-warehouse.dto.ts create mode 100644 apps/api/src/inventory/inventory.controller.ts create mode 100644 apps/api/src/inventory/inventory.module.ts create mode 100644 apps/api/src/inventory/inventory.service.ts create mode 100644 apps/api/src/inventory/warehouses.controller.ts create mode 100644 apps/api/src/inventory/warehouses.service.ts create mode 100644 apps/api/src/support/dto/create-ticket-comment.dto.ts create mode 100644 apps/api/src/support/dto/create-ticket.dto.ts create mode 100644 apps/api/src/support/dto/update-ticket-status.dto.ts create mode 100644 apps/api/src/support/sla.ts create mode 100644 apps/api/src/support/support.controller.ts create mode 100644 apps/api/src/support/support.module.ts create mode 100644 apps/api/src/support/support.service.ts create mode 100644 apps/api/test/afd-parser.spec.ts create mode 100644 apps/api/test/mfa.spec.ts create mode 100644 apps/core-web/.env.example create mode 100644 apps/core-web/public/eden-logo-horizontal.png create mode 100644 apps/core-web/src/lib/api/auth.ts create mode 100644 apps/core-web/src/lib/api/http.ts create mode 100644 apps/core-web/src/lib/auth/AuthContext.tsx create mode 100644 apps/core-web/src/lib/auth/session.ts create mode 100644 apps/core-web/src/routes/AuthenticatedLayout.tsx create mode 100644 apps/core-web/src/routes/DashboardPage.tsx create mode 100644 apps/core-web/src/routes/LoginPage.tsx create mode 100644 apps/core-web/src/routes/ProtectedRoute.tsx create mode 100644 apps/core-web/src/vite-env.d.ts create mode 100644 packages/database/migrations/1788450538883_cross-reference-fks.cjs create mode 100644 packages/database/migrations/1788451164762_inventory.cjs create mode 100644 packages/database/migrations/1788451374900_support.cjs create mode 100644 packages/database/migrations/1788452136875_hr-timeclock.cjs create mode 100644 packages/database/migrations/1788457368744_fiscal.cjs create mode 100644 packages/database/migrations/1788457629203_billing.cjs create mode 100644 packages/database/src/repositories/billing.ts create mode 100644 packages/database/src/repositories/fiscal.ts create mode 100644 packages/database/src/repositories/hr-timeclock.ts create mode 100644 packages/database/src/repositories/inventory.ts create mode 100644 packages/database/src/repositories/support-tickets.ts create mode 100644 packages/ui/src/components/Alert.tsx create mode 100644 packages/ui/src/components/AppShell.tsx create mode 100644 packages/ui/src/components/Badge.tsx create mode 100644 packages/ui/src/components/Button.tsx create mode 100644 packages/ui/src/components/Card.tsx create mode 100644 packages/ui/src/components/Input.tsx create mode 100644 packages/ui/src/components/PageHeader.tsx create mode 100644 packages/ui/src/tailwind-preset.ts create mode 100644 packages/ui/src/utils/cn.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ec2cc25 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.gitignore +node_modules +**/node_modules +**/dist +**/.turbo +.claude/worktrees +.tmp +*.log +.env +.env.local +.env.*.local +tema_do_Eden.zip diff --git a/.env.example b/.env.example index ab81ec0..7aa75f3 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,29 @@ EDEN_CORE_PORT=3001 EDEN_PARCEIROS_PORT=3002 EDEN_ASSINANTE_PORT=3003 +# --- Frontend: URL base da API consumida pelo browser (Vite, build-time) --- +# Lida via import.meta.env.VITE_API_BASE_URL. Dev local (`pnpm dev`, fora de +# Docker): a API roda em localhost:8080, origem DIFERENTE da porta do Vite — +# precisa de URL absoluta + CORS (ver EDEN_CORS_ORIGINS abaixo). Copiar para +# o .env de cada app (apps/core-web/.env etc.) — Vite não lê o .env da raiz. +VITE_API_BASE_URL=http://localhost:8080/api/v1 + +# Build de produção via Docker (compose.yaml `VITE_API_BASE_URL` de build +# arg, default /api/v1): a API NÃO tem domínio público próprio (decisão do +# operador — só acessível por IP interno). O browser chama a API pelo MESMO +# domínio do frontend, caminho relativo /api/v1 — o proxy reverso na frente +# de cada um dos 3 domínios públicos precisa rotear internamente esse path +# para o container eden-api (rede interna, nunca exposto publicamente). +# Nesse fluxo (mesma origem) o navegador não faz preflight de CORS. + +# --- CORS (origens de browser permitidas a chamar a API cross-origin) --- +# Só é relevante para dev local (core-web em localhost:3001+, API em +# localhost:8080 — origens diferentes). Nunca usar "*" — a API emite bearer +# token (ver packages/config/src/env-schema.ts). Os 3 domínios públicos NÃO +# entram aqui: eles chamam a API por caminho relativo via proxy (acima), +# nunca cross-origin diretamente na API. +EDEN_CORS_ORIGINS=http://localhost:3001,http://localhost:3002,http://localhost:3003 + # --- Autenticação / sessão (ADR-0003) --- # Gerar com: openssl rand -hex 48 JWT_SIGNING_SECRET= diff --git a/apps/api/package.json b/apps/api/package.json index a2fd9bc..a29801a 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -25,6 +25,7 @@ "class-validator": "^0.14.1", "express": "^4.21.2", "jsonwebtoken": "^9.0.2", + "otplib": "^13.5.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index a318f55..a443039 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -14,6 +14,11 @@ import { OpportunitiesModule } from "./opportunities/opportunities.module.js"; import { QuotesModule } from "./quotes/quotes.module.js"; import { ClientRegistrationsModule } from "./client-registrations/client-registrations.module.js"; import { ContractsModule } from "./contracts/contracts.module.js"; +import { InventoryModule } from "./inventory/inventory.module.js"; +import { SupportModule } from "./support/support.module.js"; +import { HrTimeclockModule } from "./hr-timeclock/hr-timeclock.module.js"; +import { FiscalModule } from "./fiscal/fiscal.module.js"; +import { BillingModule } from "./billing/billing.module.js"; @Module({ imports: [ @@ -31,6 +36,11 @@ import { ContractsModule } from "./contracts/contracts.module.js"; QuotesModule, ClientRegistrationsModule, ContractsModule, + InventoryModule, + SupportModule, + HrTimeclockModule, + FiscalModule, + BillingModule, ], controllers: [HealthController], }) diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts index eb7ecba..4547447 100644 --- a/apps/api/src/auth/auth.controller.ts +++ b/apps/api/src/auth/auth.controller.ts @@ -2,8 +2,11 @@ import { Body, Controller, Get, HttpCode, Post, Req, UseGuards } from "@nestjs/c import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import type { Request } from "express"; import { AuthService } from "./auth.service.js"; +import { MfaService } from "./mfa.service.js"; import { LoginDto } from "./dto/login.dto.js"; import { RefreshDto } from "./dto/refresh.dto.js"; +import { MfaChallengeDto } from "./dto/mfa-challenge.dto.js"; +import { MfaCodeDto } from "./dto/mfa-code.dto.js"; import { JwtAuthGuard } from "./jwt-auth.guard.js"; import { CurrentUser } from "./decorators/current-user.decorator.js"; import type { EdenAuthenticatedUser } from "./jwt-auth.guard.js"; @@ -18,7 +21,10 @@ function requestContext(req: Request) { @ApiTags("auth") @Controller("v1/auth") export class AuthController { - constructor(private readonly auth: AuthService) {} + constructor( + private readonly auth: AuthService, + private readonly mfaService: MfaService, + ) {} @Post("login") @HttpCode(200) @@ -26,6 +32,35 @@ export class AuthController { return this.auth.login(dto.email, dto.password, dto.applicationKey, requestContext(req)); } + @Post("mfa/challenge") + @HttpCode(200) + async mfaChallenge(@Body() dto: MfaChallengeDto, @Req() req: Request) { + return this.auth.completeMfaLogin(dto.mfaChallenge, dto.code, requestContext(req)); + } + + @Post("mfa/enroll") + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + async mfaEnroll(@CurrentUser() user: EdenAuthenticatedUser) { + return this.mfaService.startEnrollment(user.id, user.email); + } + + @Post("mfa/confirm") + @HttpCode(204) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + async mfaConfirm(@Body() dto: MfaCodeDto, @CurrentUser() user: EdenAuthenticatedUser) { + await this.mfaService.confirmEnrollment(user.id, dto.code); + } + + @Post("mfa/disable") + @HttpCode(204) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + async mfaDisable(@Body() dto: MfaCodeDto, @CurrentUser() user: EdenAuthenticatedUser) { + await this.mfaService.disable(user.id, dto.code); + } + @Post("refresh") @HttpCode(200) async refresh(@Body() dto: RefreshDto, @Req() req: Request) { diff --git a/apps/api/src/auth/auth.module.ts b/apps/api/src/auth/auth.module.ts index 17b156d..c5b4be9 100644 --- a/apps/api/src/auth/auth.module.ts +++ b/apps/api/src/auth/auth.module.ts @@ -3,13 +3,24 @@ import { AuthController } from "./auth.controller.js"; import { AuthService } from "./auth.service.js"; import { PasswordService } from "./password.service.js"; import { TokenService } from "./token.service.js"; +import { MfaService } from "./mfa.service.js"; import { JwtAuthGuard } from "./jwt-auth.guard.js"; import { PermissionGuard } from "./permission.guard.js"; import { SuperAdminGuard } from "./super-admin.guard.js"; +import { FieldEncryptionService } from "../crypto/field-encryption.service.js"; @Module({ controllers: [AuthController], - providers: [AuthService, PasswordService, TokenService, JwtAuthGuard, PermissionGuard, SuperAdminGuard], + providers: [ + AuthService, + PasswordService, + TokenService, + MfaService, + FieldEncryptionService, + JwtAuthGuard, + PermissionGuard, + SuperAdminGuard, + ], exports: [PasswordService, TokenService, JwtAuthGuard, PermissionGuard, SuperAdminGuard], }) export class AuthModule {} diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index 1d6eadc..a525441 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -2,6 +2,7 @@ import { ForbiddenException, Injectable, UnauthorizedException } from "@nestjs/c import { usersRepo, sessionsRepo, auditRepo, type User } from "@eden/database"; import { PasswordService } from "./password.service.js"; import { TokenService } from "./token.service.js"; +import { MfaService } from "./mfa.service.js"; import { resolvePermissions } from "./permissions.js"; export interface RequestContext { @@ -16,6 +17,14 @@ export interface AuthResult { user: PublicUser; } +export interface MfaRequiredResult { + mfaRequired: true; + mfaChallenge: string; + expiresIn: number; +} + +export type LoginResult = AuthResult | MfaRequiredResult; + export interface PublicUser { id: string; email: string; @@ -34,6 +43,7 @@ export class AuthService { constructor( private readonly passwords: PasswordService, private readonly tokens: TokenService, + private readonly mfa: MfaService, ) {} async login( @@ -41,7 +51,7 @@ export class AuthService { password: string, applicationKey: "core" | "reseller" | "subscriber", ctx: RequestContext, - ): Promise { + ): Promise { const user = await usersRepo.findUserByEmail(email); // Same generic error whether the account doesn't exist or the password @@ -65,6 +75,20 @@ export class AuthService { throw new ForbiddenException("Usuário sem acesso a esta aplicação"); } + if (user.mfa_enabled) { + const challenge = this.tokens.signMfaChallenge({ sub: user.id, app: applicationKey }); + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "auth.mfa_challenge_issued", + resourceType: "user", + resourceId: user.id, + metadata: { applicationKey }, + ipAddress: ctx.ipAddress, + }); + return { mfaRequired: true, mfaChallenge: challenge.token, expiresIn: challenge.expiresIn }; + } + const result = await this.issueTokens(user, applicationKey, ctx); await auditRepo.appendAuditEvent({ @@ -80,6 +104,47 @@ export class AuthService { return result; } + /** Second step of login when the account has MFA enabled — exchanges a password-verified challenge + TOTP code for real tokens. */ + async completeMfaLogin(challengeToken: string, code: string, ctx: RequestContext): Promise { + let payload; + try { + payload = this.tokens.verifyMfaChallenge(challengeToken); + } catch { + throw new UnauthorizedException("Desafio MFA inválido ou expirado"); + } + + const user = await usersRepo.findUserById(payload.sub); + if (!user || !user.is_active || !user.mfa_enabled || !user.mfa_secret_encrypted) { + throw new UnauthorizedException("Desafio MFA inválido ou expirado"); + } + + if (!(await this.mfa.verifyCode(user.mfa_secret_encrypted, code))) { + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "auth.mfa_challenge_failed", + resourceType: "user", + resourceId: user.id, + ipAddress: ctx.ipAddress, + }); + throw new UnauthorizedException("Código inválido"); + } + + const result = await this.issueTokens(user, payload.app, ctx); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "auth.login", + resourceType: "user", + resourceId: user.id, + metadata: { applicationKey: payload.app, viaMfa: true }, + ipAddress: ctx.ipAddress, + }); + + return result; + } + async refresh(refreshTokenPlaintext: string, ctx: RequestContext): Promise { const hash = this.tokens.hashRefreshToken(refreshTokenPlaintext); const active = await sessionsRepo.findActiveSessionByRefreshTokenHash(hash); diff --git a/apps/api/src/auth/dto/mfa-challenge.dto.ts b/apps/api/src/auth/dto/mfa-challenge.dto.ts new file mode 100644 index 0000000..3ab1eca --- /dev/null +++ b/apps/api/src/auth/dto/mfa-challenge.dto.ts @@ -0,0 +1,11 @@ +import { IsString, Length, MinLength } from "class-validator"; + +export class MfaChallengeDto { + @IsString() + @MinLength(1) + mfaChallenge!: string; + + @IsString() + @Length(6, 6) + code!: string; +} diff --git a/apps/api/src/auth/dto/mfa-code.dto.ts b/apps/api/src/auth/dto/mfa-code.dto.ts new file mode 100644 index 0000000..fd973e3 --- /dev/null +++ b/apps/api/src/auth/dto/mfa-code.dto.ts @@ -0,0 +1,7 @@ +import { IsString, Length } from "class-validator"; + +export class MfaCodeDto { + @IsString() + @Length(6, 6) + code!: string; +} diff --git a/apps/api/src/auth/mfa.service.ts b/apps/api/src/auth/mfa.service.ts new file mode 100644 index 0000000..aebbca9 --- /dev/null +++ b/apps/api/src/auth/mfa.service.ts @@ -0,0 +1,49 @@ +import { BadRequestException, Injectable, UnauthorizedException } from "@nestjs/common"; +import { generateSecret, generateURI, verify } from "otplib"; +import { usersRepo } from "@eden/database"; +import { FieldEncryptionService } from "../crypto/field-encryption.service.js"; + +const ISSUER = "EDEN"; + +@Injectable() +export class MfaService { + constructor(private readonly fieldEncryption: FieldEncryptionService) {} + + /** Stages a new TOTP secret (not yet active — see confirmEnrollment). */ + async startEnrollment(userId: string, email: string): Promise<{ secret: string; otpauthUrl: string }> { + const secret = generateSecret(); + await usersRepo.setMfaSecret(userId, this.fieldEncryption.encrypt(secret)); + const otpauthUrl = generateURI({ issuer: ISSUER, label: email, secret }); + return { secret, otpauthUrl }; + } + + /** Verifies the first code against the staged secret and flips mfa_enabled on. */ + async confirmEnrollment(userId: string, code: string): Promise { + const user = await usersRepo.findUserById(userId); + if (!user?.mfa_secret_encrypted) { + throw new BadRequestException("Nenhum cadastro de MFA pendente para este usuário"); + } + if (!(await this.verifyCode(user.mfa_secret_encrypted, code))) { + throw new BadRequestException("Código inválido"); + } + await usersRepo.confirmMfaEnrollment(userId); + } + + /** Requires a currently-valid code — proves the factor, not just an authenticated session. */ + async disable(userId: string, code: string): Promise { + const user = await usersRepo.findUserById(userId); + if (!user?.mfa_enabled || !user.mfa_secret_encrypted) { + throw new BadRequestException("MFA não está habilitado para este usuário"); + } + if (!(await this.verifyCode(user.mfa_secret_encrypted, code))) { + throw new UnauthorizedException("Código inválido"); + } + await usersRepo.disableMfa(userId); + } + + async verifyCode(encryptedSecret: string, code: string): Promise { + const secret = this.fieldEncryption.decrypt(encryptedSecret); + const result = await verify({ token: code, secret }); + return result.valid; + } +} diff --git a/apps/api/src/auth/token.service.ts b/apps/api/src/auth/token.service.ts index 3a86517..48f1b0d 100644 --- a/apps/api/src/auth/token.service.ts +++ b/apps/api/src/auth/token.service.ts @@ -11,6 +11,14 @@ export interface AccessTokenPayload { const ACCESS_TOKEN_TTL_SECONDS = 15 * 60; // 15 min — ADR-0003 const REFRESH_TOKEN_TTL_DAYS = 30; +const MFA_CHALLENGE_TTL_SECONDS = 2 * 60; +const MFA_CHALLENGE_PURPOSE = "mfa_challenge" as const; + +export interface MfaChallengePayload { + sub: string; + app: "core" | "reseller" | "subscriber"; + purpose: typeof MFA_CHALLENGE_PURPOSE; +} @Injectable() export class TokenService { @@ -26,7 +34,18 @@ export class TokenService { } verifyAccessToken(token: string): AccessTokenPayload { - return jwt.verify(token, this.secret) as AccessTokenPayload; + const payload = jwt.verify(token, this.secret) as AccessTokenPayload & { purpose?: unknown }; + // Both token kinds are signed with the same secret, so signature + // verification alone doesn't distinguish them — a real access token + // must never carry a `purpose` claim (only single-purpose tokens like + // the MFA challenge do). Without this check, a password-verified MFA + // challenge token would be indistinguishable from a full access token + // and grant complete API access without ever checking the second + // factor — this was caught live (a real bug, not hypothetical). + if (payload.purpose !== undefined) { + throw new Error("Token não é um access token válido"); + } + return payload; } /** Opaque, high-entropy refresh token — never a JWT (ADR-0003). Returns the @@ -42,4 +61,27 @@ export class TokenService { hashRefreshToken(plaintext: string): string { return createHash("sha256").update(plaintext).digest("hex"); } + + /** + * Short-lived, single-purpose token issued after password verification + * when the account has MFA enabled — proves "password already checked" + * without granting API access. The `purpose` claim is what keeps this + * out of JwtAuthGuard (see verifyAccessToken's explicit rejection of any + * `purpose` claim) — without it this token would be a fully valid + * access token, defeating MFA entirely. + */ + signMfaChallenge(payload: Omit): { token: string; expiresIn: number } { + const token = jwt.sign({ ...payload, purpose: MFA_CHALLENGE_PURPOSE }, this.secret, { + expiresIn: MFA_CHALLENGE_TTL_SECONDS, + }); + return { token, expiresIn: MFA_CHALLENGE_TTL_SECONDS }; + } + + verifyMfaChallenge(token: string): MfaChallengePayload { + const payload = jwt.verify(token, this.secret) as Partial; + if (payload.purpose !== MFA_CHALLENGE_PURPOSE || !payload.sub || !payload.app) { + throw new Error("Token de desafio MFA inválido"); + } + return payload as MfaChallengePayload; + } } diff --git a/apps/api/src/billing/billing.controller.ts b/apps/api/src/billing/billing.controller.ts new file mode 100644 index 0000000..3f2f4b2 --- /dev/null +++ b/apps/api/src/billing/billing.controller.ts @@ -0,0 +1,104 @@ +import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { BillingService } from "./billing.service.js"; +import { CreateBillingSubscriptionDto } from "./dto/create-billing-subscription.dto.js"; +import { CreateBillingRunDto } from "./dto/create-billing-run.dto.js"; +import { JwtAuthGuard } from "../auth/jwt-auth.guard.js"; +import { PermissionGuard } from "../auth/permission.guard.js"; +import { RequirePermission } from "../auth/decorators/require-permission.decorator.js"; +import { CurrentUser } from "../auth/decorators/current-user.decorator.js"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; +import type { + BillingRunStatus, + BillingSubscriptionStatus, + InvoiceStatus, +} from "@eden/database"; + +/** + * Billing — Master Prompt §6.10, ADR-0008. Recurring subscription -> + * billing_run -> invoice MVP. Explicitly NOT Finance/AR-AP (títulos, + * boletos, dunning) or Fiscal (documento fiscal) — see ADR-0008. See + * billing.service.ts for the authorization rules (why run + * creation/consolidation requires 'all' scope). + */ +@ApiTags("billing") +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("v1/billing") +export class BillingController { + constructor(private readonly billing: BillingService) {} + + @Get("subscriptions") + @RequirePermission("billing", "view") + listSubscriptions( + @CurrentUser() user: EdenAuthenticatedUser, + @Query("status") status?: BillingSubscriptionStatus, + ) { + return this.billing.listSubscriptions(user, status); + } + + @Post("subscriptions") + @RequirePermission("billing", "edit") + createSubscription( + @Body() dto: CreateBillingSubscriptionDto, + @CurrentUser() user: EdenAuthenticatedUser, + ) { + return this.billing.createSubscription(dto, user); + } + + @Patch("subscriptions/:id/cancel") + @RequirePermission("billing", "edit") + cancelSubscription(@Param("id") id: string, @CurrentUser() user: EdenAuthenticatedUser) { + return this.billing.cancelSubscription(id, user); + } + + @Post("runs") + @RequirePermission("billing", "edit") + createRun(@Body() dto: CreateBillingRunDto, @CurrentUser() user: EdenAuthenticatedUser) { + return this.billing.createOrRegenerateRun(dto, user); + } + + @Get("runs") + @RequirePermission("billing", "view") + listRuns( + @CurrentUser() user: EdenAuthenticatedUser, + @Query("referenceYear") referenceYear?: string, + @Query("referenceMonth") referenceMonth?: string, + @Query("status") status?: BillingRunStatus, + ) { + return this.billing.listRuns(user, { + referenceYear: referenceYear != null ? Number(referenceYear) : undefined, + referenceMonth: referenceMonth != null ? Number(referenceMonth) : undefined, + status, + }); + } + + @Get("runs/:id") + @RequirePermission("billing", "view") + getRun(@Param("id") id: string, @CurrentUser() user: EdenAuthenticatedUser) { + return this.billing.getRun(id, user); + } + + @Post("runs/:id/consolidate") + @RequirePermission("billing", "edit") + consolidateRun(@Param("id") id: string, @CurrentUser() user: EdenAuthenticatedUser) { + return this.billing.consolidateRun(id, user); + } + + @Get("invoices") + @RequirePermission("billing", "view") + listInvoices( + @CurrentUser() user: EdenAuthenticatedUser, + @Query("status") status?: InvoiceStatus, + @Query("referenceYear") referenceYear?: string, + @Query("referenceMonth") referenceMonth?: string, + @Query("resellerId") resellerId?: string, + ) { + return this.billing.listInvoices(user, { + status, + referenceYear: referenceYear != null ? Number(referenceYear) : undefined, + referenceMonth: referenceMonth != null ? Number(referenceMonth) : undefined, + resellerId, + }); + } +} diff --git a/apps/api/src/billing/billing.module.ts b/apps/api/src/billing/billing.module.ts new file mode 100644 index 0000000..beeced0 --- /dev/null +++ b/apps/api/src/billing/billing.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; +import { AuthModule } from "../auth/auth.module.js"; +import { BillingController } from "./billing.controller.js"; +import { BillingService } from "./billing.service.js"; + +@Module({ + imports: [AuthModule], + controllers: [BillingController], + providers: [BillingService], + exports: [BillingService], +}) +export class BillingModule {} diff --git a/apps/api/src/billing/billing.service.ts b/apps/api/src/billing/billing.service.ts new file mode 100644 index 0000000..c6ba442 --- /dev/null +++ b/apps/api/src/billing/billing.service.ts @@ -0,0 +1,262 @@ +import { + ConflictException, + ForbiddenException, + Injectable, + NotFoundException, +} from "@nestjs/common"; +import { + auditRepo, + billingRepo, + type BillingRun, + type BillingRunWithInvoices, + type BillingSubscription, + type Invoice, +} from "@eden/database"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; +import { maxGrantedScope } from "../auth/permissions.js"; +import type { CreateBillingSubscriptionDto } from "./dto/create-billing-subscription.dto.js"; +import type { CreateBillingRunDto } from "./dto/create-billing-run.dto.js"; + +const { + BillingSubscriptionNotFoundError, + BillingRunNotFoundError, + BillingRunConsolidatedError, + BillingPeriodAlreadyConsolidatedError, +} = billingRepo; + +const RESOURCE = "billing"; + +/** + * Billing (Master Prompt §6.10, ADR-0008) — recurring subscription -> + * billing_run -> invoice flow. Strictly distinct from Finance/AR-AP + * (títulos/boletos/cobrança) and Fiscal (documento fiscal) — neither is + * touched here. + * + * Authorization: creating/regenerating and consolidating a billing_run + * always requires 'all' scope for 'billing'/'edit' — a run spans every + * reseller with a matching billing_day, so only central back-office + * operators (not a single reseller) can trigger a billing close. Reading + * and per-subscription/invoice mutations respect the actor's scope + * ('all' vs 'reseller'), same pattern as contracts.service.ts. + */ +@Injectable() +export class BillingService { + // ------------------------------------------------------------------- + // billing_subscriptions + // ------------------------------------------------------------------- + + async listSubscriptions( + user: EdenAuthenticatedUser, + status?: BillingSubscription["status"], + ): Promise { + const scope = maxGrantedScope(user.permissions, RESOURCE, "view"); + if (!scope) throw new ForbiddenException(`Sem permissão para 'view' em '${RESOURCE}'`); + if (scope === "all") return billingRepo.listBillingSubscriptions({ scope: "all", status }); + return billingRepo.listBillingSubscriptions({ + scope: "reseller", + resellerId: user.resellerId, + status, + }); + } + + async createSubscription( + dto: CreateBillingSubscriptionDto, + user: EdenAuthenticatedUser, + ): Promise { + const scope = maxGrantedScope(user.permissions, RESOURCE, "edit"); + if (!scope) throw new ForbiddenException(`Sem permissão para 'edit' em '${RESOURCE}'`); + + // Never trust a client-supplied resellerId to place a subscription + // under an arbitrary reseller (CLAUDE.md: no authz decision trusts a + // client-supplied reseller_id). A 'reseller'-scoped actor always gets + // their own session-resolved reseller; only 'all' scope may target an + // explicit resellerId from the body. + let resellerId: string | null; + if (scope === "all") { + resellerId = dto.resellerId ?? null; + } else { + resellerId = user.resellerId; + if (!resellerId) { + throw new ForbiddenException("Usuário não está vinculado a nenhuma revenda"); + } + } + + const subscription = await billingRepo.createBillingSubscription({ + contractId: dto.contractId, + resellerId, + monthlyValue: dto.monthlyValue, + billingDay: dto.billingDay, + createdBy: user.id, + }); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "billing_subscription.created", + resourceType: "billing_subscription", + resourceId: subscription.id, + metadata: { contractId: dto.contractId, billingDay: dto.billingDay }, + }); + + return subscription; + } + + async cancelSubscription(id: string, user: EdenAuthenticatedUser): Promise { + const scope = maxGrantedScope(user.permissions, RESOURCE, "edit"); + if (!scope) throw new ForbiddenException(`Sem permissão para 'edit' em '${RESOURCE}'`); + + const existing = await billingRepo.getBillingSubscriptionById(id); + if (!existing) throw new NotFoundException("Subscription não encontrada"); + if (scope !== "all" && existing.reseller_id !== user.resellerId) { + throw new NotFoundException("Subscription não encontrada"); + } + + const cancelled = await billingRepo.cancelBillingSubscription(id, user.id); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "billing_subscription.cancelled", + resourceType: "billing_subscription", + resourceId: id, + metadata: {}, + }); + + return cancelled; + } + + // ------------------------------------------------------------------- + // billing_runs / invoices + // ------------------------------------------------------------------- + + /** + * Creates a new draft run, or — if a draft already exists for the same + * (referenceYear, referenceMonth, billingDay) — regenerates its + * invoices in place (idempotent: never duplicates). 409s if a run for + * that exact period is already consolidated (controlled re-billing is + * deferred, see PENDING_billing.cjs header). + */ + async createOrRegenerateRun( + dto: CreateBillingRunDto, + user: EdenAuthenticatedUser, + ): Promise { + this.requireAllScope(user, "edit"); + + try { + const run = await billingRepo.generateOrRegenerateBillingRun({ + referenceYear: dto.referenceYear, + referenceMonth: dto.referenceMonth, + billingDay: dto.billingDay, + triggeredBy: user.id, + }); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "billing_run.generated", + resourceType: "billing_run", + resourceId: run.id, + metadata: { + referenceYear: dto.referenceYear, + referenceMonth: dto.referenceMonth, + billingDay: dto.billingDay, + invoiceCount: run.invoices.length, + totalAmount: run.totalAmount, + }, + }); + + return run; + } catch (err) { + if (err instanceof BillingPeriodAlreadyConsolidatedError) { + throw new ConflictException(err.message); + } + throw err; + } + } + + async listRuns( + user: EdenAuthenticatedUser, + filter: { referenceYear?: number; referenceMonth?: number; status?: BillingRun["status"] }, + ): Promise { + const scope = maxGrantedScope(user.permissions, RESOURCE, "view"); + if (!scope) throw new ForbiddenException(`Sem permissão para 'view' em '${RESOURCE}'`); + return billingRepo.listBillingRuns(filter); + } + + async getRun(id: string, user: EdenAuthenticatedUser): Promise { + const scope = maxGrantedScope(user.permissions, RESOURCE, "view"); + if (!scope) throw new ForbiddenException(`Sem permissão para 'view' em '${RESOURCE}'`); + + const run = await billingRepo.getBillingRunById(id); + if (!run) throw new NotFoundException("Billing run não encontrado"); + + if (scope !== "all") { + // Reseller-scoped actors only ever see their own reseller's slice + // of a run's invoices — never another reseller's amounts. + run.invoices = run.invoices.filter((inv: Invoice) => inv.reseller_id === user.resellerId); + } + return run; + } + + async consolidateRun(id: string, user: EdenAuthenticatedUser): Promise { + this.requireAllScope(user, "edit"); + + try { + const run = await billingRepo.consolidateBillingRun(id, user.id); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "billing_run.consolidated", + resourceType: "billing_run", + resourceId: id, + metadata: { invoiceCount: run.invoices.length, totalAmount: run.totalAmount }, + }); + + return run; + } catch (err) { + if (err instanceof BillingRunNotFoundError) throw new NotFoundException("Billing run não encontrado"); + if (err instanceof BillingRunConsolidatedError) { + throw new ConflictException("Billing run já está consolidado"); + } + throw err; + } + } + + async listInvoices( + user: EdenAuthenticatedUser, + filter: { + status?: Invoice["status"]; + referenceYear?: number; + referenceMonth?: number; + /** Only honored when the actor holds 'all' scope — see below. */ + resellerId?: string; + }, + ): Promise { + const scope = maxGrantedScope(user.permissions, RESOURCE, "view"); + if (!scope) throw new ForbiddenException(`Sem permissão para 'view' em '${RESOURCE}'`); + const { resellerId: requestedResellerId, ...rest } = filter; + + if (scope === "all") { + // A client-supplied resellerId is only ever honored once the + // actor's own server-resolved scope already grants full ('all') + // visibility — it narrows an already-unrestricted view, it never + // grants access beyond what the session allows. + return billingRepo.listInvoices( + requestedResellerId + ? { scope: "reseller", resellerId: requestedResellerId, ...rest } + : { scope: "all", ...rest }, + ); + } + return billingRepo.listInvoices({ scope: "reseller", resellerId: user.resellerId, ...rest }); + } + + private requireAllScope(user: EdenAuthenticatedUser, action: "view" | "edit"): void { + const scope = maxGrantedScope(user.permissions, RESOURCE, action); + if (scope !== "all") { + throw new ForbiddenException( + `Fechamento de billing exige escopo 'all' para '${RESOURCE}:${action}'`, + ); + } + } +} diff --git a/apps/api/src/billing/dto/create-billing-run.dto.ts b/apps/api/src/billing/dto/create-billing-run.dto.ts new file mode 100644 index 0000000..7ace1e5 --- /dev/null +++ b/apps/api/src/billing/dto/create-billing-run.dto.ts @@ -0,0 +1,18 @@ +import { IsInt, Max, Min } from "class-validator"; + +export class CreateBillingRunDto { + @IsInt() + @Min(2000) + @Max(2100) + referenceYear!: number; + + @IsInt() + @Min(1) + @Max(12) + referenceMonth!: number; + + @IsInt() + @Min(1) + @Max(28) + billingDay!: number; +} diff --git a/apps/api/src/billing/dto/create-billing-subscription.dto.ts b/apps/api/src/billing/dto/create-billing-subscription.dto.ts new file mode 100644 index 0000000..5e0277c --- /dev/null +++ b/apps/api/src/billing/dto/create-billing-subscription.dto.ts @@ -0,0 +1,30 @@ +import { IsInt, IsNumber, IsOptional, IsUUID, Max, Min } from "class-validator"; + +/** + * `monthlyValue` is accepted directly in the body (never recalculated + * from a contract) — this worktree/leva has no guaranteed access to the + * real `contracts` module to look the value up server-side, so the + * caller (typically a Contracts-side workflow) supplies the snapshot + * explicitly. `resellerId` is only actually trusted from the client when + * the caller holds 'all' scope; for 'reseller' scope it is always + * overridden server-side with the actor's own reseller (see + * billing.service.ts#create) — no authorization decision ever trusts a + * client-supplied resellerId (CLAUDE.md). + */ +export class CreateBillingSubscriptionDto { + @IsUUID() + contractId!: string; + + @IsOptional() + @IsUUID() + resellerId?: string; + + @IsNumber() + @Min(0) + monthlyValue!: number; + + @IsInt() + @Min(1) + @Max(28) + billingDay!: number; +} diff --git a/apps/api/src/crypto/field-encryption.service.ts b/apps/api/src/crypto/field-encryption.service.ts new file mode 100644 index 0000000..2c15b02 --- /dev/null +++ b/apps/api/src/crypto/field-encryption.service.ts @@ -0,0 +1,61 @@ +import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto"; +import { Inject, Injectable } from "@nestjs/common"; +import type { EdenEnv } from "@eden/config"; +import { EDEN_ENV } from "../config/config.constants.js"; + +const KEY_VERSION = "v1"; +const IV_LENGTH_BYTES = 12; // 96 bits — AES-GCM recommended nonce size + +/** + * Generic reversible field encryption (ADR-0005) — AES-256-GCM, random IV + * per operation, authenticated (tampering makes decrypt() throw). Stored + * format mirrors the legacy's `iv:tag:ciphertext` scheme with an added key + * version prefix so a future root-key rotation can be applied incrementally + * instead of re-encrypting everything at once: `v1:iv:tag:ciphertext` (all + * three payload segments base64). + * + * Root key comes from EDEN_FIELD_ENCRYPTION_KEY (env-only, never in the + * database) and is hashed to exactly 32 bytes via SHA-256 so any + * sufficiently-long passphrase works as AES-256 key material. + */ +@Injectable() +export class FieldEncryptionService { + private readonly key: Buffer | null; + + constructor(@Inject(EDEN_ENV) env: EdenEnv) { + this.key = env.EDEN_FIELD_ENCRYPTION_KEY + ? createHash("sha256").update(env.EDEN_FIELD_ENCRYPTION_KEY).digest() + : null; + } + + encrypt(plaintext: string): string { + const key = this.requireKey(); + const iv = randomBytes(IV_LENGTH_BYTES); + const cipher = createCipheriv("aes-256-gcm", key, iv); + const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); + const tag = cipher.getAuthTag(); + return [KEY_VERSION, iv.toString("base64"), tag.toString("base64"), ciphertext.toString("base64")].join(":"); + } + + decrypt(stored: string): string { + const key = this.requireKey(); + const [version, ivB64, tagB64, ciphertextB64] = stored.split(":"); + if (version !== KEY_VERSION || !ivB64 || !tagB64 || !ciphertextB64) { + throw new Error("Formato de campo cifrado inválido ou versão de chave desconhecida"); + } + const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivB64, "base64")); + decipher.setAuthTag(Buffer.from(tagB64, "base64")); + const plaintext = Buffer.concat([ + decipher.update(Buffer.from(ciphertextB64, "base64")), + decipher.final(), + ]); + return plaintext.toString("utf8"); + } + + private requireKey(): Buffer { + if (!this.key) { + throw new Error("EDEN_FIELD_ENCRYPTION_KEY não configurada — obrigatória para criptografia de campo reversível"); + } + return this.key; + } +} diff --git a/apps/api/src/fiscal/dto/create-cfop.dto.ts b/apps/api/src/fiscal/dto/create-cfop.dto.ts new file mode 100644 index 0000000..1e6a1da --- /dev/null +++ b/apps/api/src/fiscal/dto/create-cfop.dto.ts @@ -0,0 +1,21 @@ +import { IsBoolean, IsDateString, IsObject, IsOptional, IsString } from "class-validator"; + +/** + * direction/scope are free-text, no CHECK/enum — exactly as the legacy + * (eden.md §4): the CFOP code's 1st digit encodes direction+scope in + * practice, but neither this DTO nor the schema derives it automatically. + */ +export class CreateCfopDto { + @IsString() code!: string; + @IsString() description!: string; + + @IsOptional() @IsString() direction?: string; + @IsOptional() @IsString() scope?: string; + @IsOptional() @IsDateString() validFrom?: string; + @IsOptional() @IsDateString() validUntil?: string; + @IsOptional() @IsBoolean() active?: boolean; + @IsOptional() @IsString() source?: string; + @IsOptional() @IsString() sourceVersion?: string; + @IsOptional() @IsDateString() sourceUpdatedAt?: string; + @IsOptional() @IsObject() metadata?: Record; +} diff --git a/apps/api/src/fiscal/dto/create-municipality.dto.ts b/apps/api/src/fiscal/dto/create-municipality.dto.ts new file mode 100644 index 0000000..9382a73 --- /dev/null +++ b/apps/api/src/fiscal/dto/create-municipality.dto.ts @@ -0,0 +1,12 @@ +import { IsBoolean, IsOptional, IsString, Length } from "class-validator"; + +/** fiscal_municipality has NO valid_from/valid_until/metadata — eden.md §2.3 ("não faz sentido um município expirar"). */ +export class CreateMunicipalityDto { + @IsString() code!: string; + @IsString() name!: string; + @IsString() @Length(2, 2) state!: string; + + @IsOptional() @IsBoolean() active?: boolean; + @IsOptional() @IsString() source?: string; + @IsOptional() @IsString() sourceVersion?: string; +} diff --git a/apps/api/src/fiscal/dto/create-ncm.dto.ts b/apps/api/src/fiscal/dto/create-ncm.dto.ts new file mode 100644 index 0000000..6a319ed --- /dev/null +++ b/apps/api/src/fiscal/dto/create-ncm.dto.ts @@ -0,0 +1,14 @@ +import { IsBoolean, IsDateString, IsObject, IsOptional, IsString } from "class-validator"; + +export class CreateNcmDto { + @IsString() code!: string; + @IsString() description!: string; + + @IsOptional() @IsDateString() validFrom?: string; + @IsOptional() @IsDateString() validUntil?: string; + @IsOptional() @IsBoolean() active?: boolean; + @IsOptional() @IsString() source?: string; + @IsOptional() @IsString() sourceVersion?: string; + @IsOptional() @IsDateString() sourceUpdatedAt?: string; + @IsOptional() @IsObject() metadata?: Record; +} diff --git a/apps/api/src/fiscal/dto/create-product-fiscal-profile.dto.ts b/apps/api/src/fiscal/dto/create-product-fiscal-profile.dto.ts new file mode 100644 index 0000000..00b1e46 --- /dev/null +++ b/apps/api/src/fiscal/dto/create-product-fiscal-profile.dto.ts @@ -0,0 +1,23 @@ +import { IsDateString, IsIn, IsOptional, IsUUID } from "class-validator"; + +/** + * Creates a new ACTIVE profile for a (productId, billingComponent) pair. + * If one is already active, the service inactivates it and creates this + * one in the same transaction (eden.md §2.5's central invariant) — never a + * destructive update of the old profile. + */ +export class CreateProductFiscalProfileDto { + @IsIn(["RECURRING", "IMPLEMENTATION"]) + billingComponent!: "RECURRING" | "IMPLEMENTATION"; + + @IsOptional() + @IsIn(["NFCOM", "NFSE", "NFE", "NONE"]) + documentType?: "NFCOM" | "NFSE" | "NFE" | "NONE"; + + @IsOptional() @IsUUID() ncmId?: string; + @IsOptional() @IsUUID() cfopId?: string; + @IsOptional() @IsUUID() municipalityId?: string; + @IsOptional() @IsUUID() operationNatureId?: string; + @IsOptional() @IsDateString() validFrom?: string; + @IsOptional() @IsDateString() validUntil?: string; +} diff --git a/apps/api/src/fiscal/dto/update-cfop.dto.ts b/apps/api/src/fiscal/dto/update-cfop.dto.ts new file mode 100644 index 0000000..8b98073 --- /dev/null +++ b/apps/api/src/fiscal/dto/update-cfop.dto.ts @@ -0,0 +1,15 @@ +import { IsBoolean, IsDateString, IsObject, IsOptional, IsString } from "class-validator"; + +export class UpdateCfopDto { + @IsOptional() @IsString() code?: string; + @IsOptional() @IsString() description?: string; + @IsOptional() @IsString() direction?: string; + @IsOptional() @IsString() scope?: string; + @IsOptional() @IsDateString() validFrom?: string; + @IsOptional() @IsDateString() validUntil?: string; + @IsOptional() @IsBoolean() active?: boolean; + @IsOptional() @IsString() source?: string; + @IsOptional() @IsString() sourceVersion?: string; + @IsOptional() @IsDateString() sourceUpdatedAt?: string; + @IsOptional() @IsObject() metadata?: Record; +} diff --git a/apps/api/src/fiscal/dto/update-municipality.dto.ts b/apps/api/src/fiscal/dto/update-municipality.dto.ts new file mode 100644 index 0000000..7085e6c --- /dev/null +++ b/apps/api/src/fiscal/dto/update-municipality.dto.ts @@ -0,0 +1,10 @@ +import { IsBoolean, IsOptional, IsString, Length } from "class-validator"; + +export class UpdateMunicipalityDto { + @IsOptional() @IsString() code?: string; + @IsOptional() @IsString() name?: string; + @IsOptional() @IsString() @Length(2, 2) state?: string; + @IsOptional() @IsBoolean() active?: boolean; + @IsOptional() @IsString() source?: string; + @IsOptional() @IsString() sourceVersion?: string; +} diff --git a/apps/api/src/fiscal/dto/update-ncm.dto.ts b/apps/api/src/fiscal/dto/update-ncm.dto.ts new file mode 100644 index 0000000..bee8d10 --- /dev/null +++ b/apps/api/src/fiscal/dto/update-ncm.dto.ts @@ -0,0 +1,14 @@ +import { IsBoolean, IsDateString, IsObject, IsOptional, IsString } from "class-validator"; + +/** All fields optional — PATCH is a partial update, including flipping `active` alone. */ +export class UpdateNcmDto { + @IsOptional() @IsString() code?: string; + @IsOptional() @IsString() description?: string; + @IsOptional() @IsDateString() validFrom?: string; + @IsOptional() @IsDateString() validUntil?: string; + @IsOptional() @IsBoolean() active?: boolean; + @IsOptional() @IsString() source?: string; + @IsOptional() @IsString() sourceVersion?: string; + @IsOptional() @IsDateString() sourceUpdatedAt?: string; + @IsOptional() @IsObject() metadata?: Record; +} diff --git a/apps/api/src/fiscal/fiscal.controller.ts b/apps/api/src/fiscal/fiscal.controller.ts new file mode 100644 index 0000000..4970841 --- /dev/null +++ b/apps/api/src/fiscal/fiscal.controller.ts @@ -0,0 +1,142 @@ +import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { FiscalService } from "./fiscal.service.js"; +import { CreateNcmDto } from "./dto/create-ncm.dto.js"; +import { UpdateNcmDto } from "./dto/update-ncm.dto.js"; +import { CreateCfopDto } from "./dto/create-cfop.dto.js"; +import { UpdateCfopDto } from "./dto/update-cfop.dto.js"; +import { CreateMunicipalityDto } from "./dto/create-municipality.dto.js"; +import { UpdateMunicipalityDto } from "./dto/update-municipality.dto.js"; +import { CreateProductFiscalProfileDto } from "./dto/create-product-fiscal-profile.dto.js"; +import { JwtAuthGuard } from "../auth/jwt-auth.guard.js"; +import { PermissionGuard } from "../auth/permission.guard.js"; +import { RequirePermission } from "../auth/decorators/require-permission.decorator.js"; +import { CurrentUser } from "../auth/decorators/current-user.decorator.js"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; + +/** + * Fiscal catalogs + product fiscal profiles (eden.md módulo 5). Uses the + * same `fiscal` permission feature as the legacy, with `view`/`edit` + * levels (eden.md §8). No bulk/mass operations in this MVP — writes always + * touch a single catalog row, never a whole table. + */ +@ApiTags("fiscal") +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("v1/fiscal") +export class FiscalController { + constructor(private readonly fiscal: FiscalService) {} + + // -- fiscal_ncm --------------------------------------------------------- + + @Get("ncm") + @RequirePermission("fiscal", "view") + listNcm(@Query("q") q?: string, @Query("active") active?: string) { + return this.fiscal.listNcm(q, parseActive(active)); + } + + @Get("ncm/:id") + @RequirePermission("fiscal", "view") + getNcm(@Param("id") id: string) { + return this.fiscal.getNcm(id); + } + + @Post("ncm") + @RequirePermission("fiscal", "edit") + createNcm(@Body() dto: CreateNcmDto, @CurrentUser() user: EdenAuthenticatedUser) { + return this.fiscal.createNcm(dto, user); + } + + @Patch("ncm/:id") + @RequirePermission("fiscal", "edit") + updateNcm(@Param("id") id: string, @Body() dto: UpdateNcmDto, @CurrentUser() user: EdenAuthenticatedUser) { + return this.fiscal.updateNcm(id, dto, user); + } + + // -- fiscal_cfop ---------------------------------------------------------- + + @Get("cfop") + @RequirePermission("fiscal", "view") + listCfop(@Query("q") q?: string, @Query("active") active?: string) { + return this.fiscal.listCfop(q, parseActive(active)); + } + + @Get("cfop/:id") + @RequirePermission("fiscal", "view") + getCfop(@Param("id") id: string) { + return this.fiscal.getCfop(id); + } + + @Post("cfop") + @RequirePermission("fiscal", "edit") + createCfop(@Body() dto: CreateCfopDto, @CurrentUser() user: EdenAuthenticatedUser) { + return this.fiscal.createCfop(dto, user); + } + + @Patch("cfop/:id") + @RequirePermission("fiscal", "edit") + updateCfop(@Param("id") id: string, @Body() dto: UpdateCfopDto, @CurrentUser() user: EdenAuthenticatedUser) { + return this.fiscal.updateCfop(id, dto, user); + } + + // -- fiscal_municipality -------------------------------------------------- + + @Get("municipalities") + @RequirePermission("fiscal", "view") + listMunicipalities(@Query("q") q?: string, @Query("active") active?: string) { + return this.fiscal.listMunicipalities(q, parseActive(active)); + } + + @Get("municipalities/:id") + @RequirePermission("fiscal", "view") + getMunicipality(@Param("id") id: string) { + return this.fiscal.getMunicipality(id); + } + + @Post("municipalities") + @RequirePermission("fiscal", "edit") + createMunicipality(@Body() dto: CreateMunicipalityDto, @CurrentUser() user: EdenAuthenticatedUser) { + return this.fiscal.createMunicipality(dto, user); + } + + @Patch("municipalities/:id") + @RequirePermission("fiscal", "edit") + updateMunicipality( + @Param("id") id: string, + @Body() dto: UpdateMunicipalityDto, + @CurrentUser() user: EdenAuthenticatedUser, + ) { + return this.fiscal.updateMunicipality(id, dto, user); + } + + // -- fiscal_operation_nature — read-only in this MVP ----------------------- + + @Get("operation-natures") + @RequirePermission("fiscal", "view") + listOperationNatures() { + return this.fiscal.listOperationNatures(); + } + + // -- product_fiscal_profiles ---------------------------------------------- + + @Get("products/:productId/profiles") + @RequirePermission("fiscal", "view") + listProfiles(@Param("productId") productId: string) { + return this.fiscal.listProfiles(productId); + } + + @Post("products/:productId/profiles") + @RequirePermission("fiscal", "edit") + createProfile( + @Param("productId") productId: string, + @Body() dto: CreateProductFiscalProfileDto, + @CurrentUser() user: EdenAuthenticatedUser, + ) { + return this.fiscal.createProfile(productId, dto, user); + } +} + +function parseActive(value?: string): boolean | undefined { + if (value === undefined) return undefined; + return value === "true"; +} diff --git a/apps/api/src/fiscal/fiscal.module.ts b/apps/api/src/fiscal/fiscal.module.ts new file mode 100644 index 0000000..7070c57 --- /dev/null +++ b/apps/api/src/fiscal/fiscal.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; +import { AuthModule } from "../auth/auth.module.js"; +import { FiscalController } from "./fiscal.controller.js"; +import { FiscalService } from "./fiscal.service.js"; + +@Module({ + imports: [AuthModule], + controllers: [FiscalController], + providers: [FiscalService], + exports: [FiscalService], +}) +export class FiscalModule {} diff --git a/apps/api/src/fiscal/fiscal.service.ts b/apps/api/src/fiscal/fiscal.service.ts new file mode 100644 index 0000000..182c3cb --- /dev/null +++ b/apps/api/src/fiscal/fiscal.service.ts @@ -0,0 +1,219 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { + auditRepo, + fiscalRepo, + productsRepo, + type FiscalCfop, + type FiscalMunicipality, + type FiscalNcm, + type FiscalOperationNature, + type ProductFiscalProfile, +} from "@eden/database"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; +import type { CreateNcmDto } from "./dto/create-ncm.dto.js"; +import type { UpdateNcmDto } from "./dto/update-ncm.dto.js"; +import type { CreateCfopDto } from "./dto/create-cfop.dto.js"; +import type { UpdateCfopDto } from "./dto/update-cfop.dto.js"; +import type { CreateMunicipalityDto } from "./dto/create-municipality.dto.js"; +import type { UpdateMunicipalityDto } from "./dto/update-municipality.dto.js"; +import type { CreateProductFiscalProfileDto } from "./dto/create-product-fiscal-profile.dto.js"; + +/** + * Fiscal catalogs (eden.md módulo 5) — see PENDING_fiscal.cjs migration + * header for the MVP scope. Read (`fiscal:view`) is open to any + * authenticated user holding that grant; write (`fiscal:edit`) creates or + * PATCHes a single catalog row — no bulk/mass operations in this MVP. + */ +@Injectable() +export class FiscalService { + // --------------------------------------------------------------------- + // fiscal_ncm + // --------------------------------------------------------------------- + + async listNcm(q?: string, active?: boolean): Promise { + return fiscalRepo.listNcm({ q, active }); + } + + async getNcm(id: string): Promise { + const row = await fiscalRepo.getNcmById(id); + if (!row) throw new NotFoundException("NCM não encontrado"); + return row; + } + + async createNcm(dto: CreateNcmDto, user: EdenAuthenticatedUser): Promise { + const row = await fiscalRepo.createNcm(dto); + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "fiscal.ncm.created", + resourceType: "fiscal_ncm", + resourceId: row.id, + metadata: { code: row.code }, + }); + return row; + } + + async updateNcm(id: string, dto: UpdateNcmDto, user: EdenAuthenticatedUser): Promise { + const row = await fiscalRepo.updateNcm(id, dto); + if (!row) throw new NotFoundException("NCM não encontrado"); + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "fiscal.ncm.updated", + resourceType: "fiscal_ncm", + resourceId: row.id, + metadata: { ...dto }, + }); + return row; + } + + // --------------------------------------------------------------------- + // fiscal_cfop + // --------------------------------------------------------------------- + + async listCfop(q?: string, active?: boolean): Promise { + return fiscalRepo.listCfop({ q, active }); + } + + async getCfop(id: string): Promise { + const row = await fiscalRepo.getCfopById(id); + if (!row) throw new NotFoundException("CFOP não encontrado"); + return row; + } + + async createCfop(dto: CreateCfopDto, user: EdenAuthenticatedUser): Promise { + const row = await fiscalRepo.createCfop(dto); + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "fiscal.cfop.created", + resourceType: "fiscal_cfop", + resourceId: row.id, + metadata: { code: row.code }, + }); + return row; + } + + async updateCfop(id: string, dto: UpdateCfopDto, user: EdenAuthenticatedUser): Promise { + const row = await fiscalRepo.updateCfop(id, dto); + if (!row) throw new NotFoundException("CFOP não encontrado"); + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "fiscal.cfop.updated", + resourceType: "fiscal_cfop", + resourceId: row.id, + metadata: { ...dto }, + }); + return row; + } + + // --------------------------------------------------------------------- + // fiscal_municipality + // --------------------------------------------------------------------- + + async listMunicipalities(q?: string, active?: boolean): Promise { + return fiscalRepo.listMunicipalities({ q, active }); + } + + async getMunicipality(id: string): Promise { + const row = await fiscalRepo.getMunicipalityById(id); + if (!row) throw new NotFoundException("Município não encontrado"); + return row; + } + + async createMunicipality( + dto: CreateMunicipalityDto, + user: EdenAuthenticatedUser, + ): Promise { + const row = await fiscalRepo.createMunicipality(dto); + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "fiscal.municipality.created", + resourceType: "fiscal_municipality", + resourceId: row.id, + metadata: { code: row.code }, + }); + return row; + } + + async updateMunicipality( + id: string, + dto: UpdateMunicipalityDto, + user: EdenAuthenticatedUser, + ): Promise { + const row = await fiscalRepo.updateMunicipality(id, dto); + if (!row) throw new NotFoundException("Município não encontrado"); + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "fiscal.municipality.updated", + resourceType: "fiscal_municipality", + resourceId: row.id, + metadata: { ...dto }, + }); + return row; + } + + // --------------------------------------------------------------------- + // fiscal_operation_nature — read-only in this MVP (fixed business enum, + // seeded by the migration) + // --------------------------------------------------------------------- + + async listOperationNatures(): Promise { + return fiscalRepo.listOperationNatures(); + } + + // --------------------------------------------------------------------- + // product_fiscal_profiles + // --------------------------------------------------------------------- + + async listProfiles(productId: string): Promise { + await this.assertProductExists(productId); + return fiscalRepo.listProductFiscalProfiles(productId); + } + + /** + * Creates a new ACTIVE profile for (productId, billingComponent). If one + * is already active, the repository layer inactivates it and creates the + * new one in a single transaction — never a destructive update of the + * old profile (eden.md §2.5). + */ + async createProfile( + productId: string, + dto: CreateProductFiscalProfileDto, + user: EdenAuthenticatedUser, + ): Promise { + await this.assertProductExists(productId); + + const created = await fiscalRepo.createActiveProductFiscalProfile({ + productId, + billingComponent: dto.billingComponent, + documentType: dto.documentType, + ncmId: dto.ncmId ?? null, + cfopId: dto.cfopId ?? null, + municipalityId: dto.municipalityId ?? null, + operationNatureId: dto.operationNatureId ?? null, + validFrom: dto.validFrom ?? null, + validUntil: dto.validUntil ?? null, + createdBy: user.id, + }); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "fiscal.product_profile.created", + resourceType: "product_fiscal_profile", + resourceId: created.id, + metadata: { productId, billingComponent: dto.billingComponent, documentType: created.document_type }, + }); + + return created; + } + + private async assertProductExists(productId: string): Promise { + const product = await productsRepo.getProductById(productId); + if (!product) throw new NotFoundException("Produto não encontrado"); + } +} diff --git a/apps/api/src/hr-timeclock/afd-imports.controller.ts b/apps/api/src/hr-timeclock/afd-imports.controller.ts new file mode 100644 index 0000000..59e745c --- /dev/null +++ b/apps/api/src/hr-timeclock/afd-imports.controller.ts @@ -0,0 +1,35 @@ +import { Body, Controller, Get, Param, Post, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { AfdImportsService } from "./afd-imports.service.js"; +import { CreateAfdImportDto } from "./dto/create-afd-import.dto.js"; +import { JwtAuthGuard } from "../auth/jwt-auth.guard.js"; +import { PermissionGuard } from "../auth/permission.guard.js"; +import { RequirePermission } from "../auth/decorators/require-permission.decorator.js"; +import { CurrentUser } from "../auth/decorators/current-user.decorator.js"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; + +/** + * eden.md módulo 6 §4.4/§4.5 — AFD import. See afd-imports.service.ts and + * CreateAfdImportDto for this leva's transport simplification (JSON + * string body, not multipart) and its documented encoding caveat. + */ +@ApiTags("hr-timeclock-afd-imports") +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, PermissionGuard) +@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })) +@Controller("v1/hr-timeclock/afd-imports") +export class AfdImportsController { + constructor(private readonly afdImports: AfdImportsService) {} + + @Post() + @RequirePermission("afd-import", "edit") + import(@Body() dto: CreateAfdImportDto, @CurrentUser() user: EdenAuthenticatedUser) { + return this.afdImports.import(dto, user); + } + + @Get(":id") + @RequirePermission("afd-import", "view") + get(@Param("id") id: string) { + return this.afdImports.getById(id); + } +} diff --git a/apps/api/src/hr-timeclock/afd-imports.service.ts b/apps/api/src/hr-timeclock/afd-imports.service.ts new file mode 100644 index 0000000..cf8ebe3 --- /dev/null +++ b/apps/api/src/hr-timeclock/afd-imports.service.ts @@ -0,0 +1,202 @@ +import { createHash } from "node:crypto"; +import { ConflictException, Injectable, NotFoundException } from "@nestjs/common"; +import { + auditRepo, + hrTimeclockRepo, + type AfdImportBatch, + type AfdImportBatchStatus, + type TimeclockEmployee, +} from "@eden/database"; +import { parseAfdFile, normalizeAfdCpf, type AfdParseSummary } from "./afd-parser.js"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; +import type { CreateAfdImportDto } from "./dto/create-afd-import.dto.js"; + +export interface ImportAfdFileResult { + alreadyImported: boolean; + batch: AfdImportBatch; + summary?: AfdParseSummary; +} + +function isUniqueViolation(err: unknown): boolean { + return typeof err === "object" && err !== null && (err as { code?: unknown }).code === "23505"; +} + +@Injectable() +export class AfdImportsService { + /** + * The whole AFD import orchestration: whole-file idempotency (SHA-256), + * pure parsing (afd-parser.ts — never touches the DB), best-effort + * employee resolution by CPF (never guesses — ambiguous/unmatched stays + * null), then the transactional write (hrTimeclockRepo.importAfdBatch, + * which does the per-device locking + per-record idempotency). + * + * ENCODING CAVEAT (see afd-parser.ts's header): `dto.fileContent` is + * expected to already be decoded as ISO-8859-1 by the caller — this + * leva's transport is a JSON string, not a raw multipart upload. + */ + async import(dto: CreateAfdImportDto, user: EdenAuthenticatedUser): Promise { + const device = await hrTimeclockRepo.getTimeclockDeviceById(dto.deviceId); + if (!device) throw new NotFoundException("Equipamento de ponto não encontrado"); + + // Whole-file idempotency layer (eden.md módulo 6 §12) — hash is over + // the content AS RECEIVED by this endpoint (see migration comment on + // afd_import_batches.file_hash for why this isn't necessarily + // byte-identical to a raw .afd file's own hash in this leva's + // string-transport simplification). + const fileHash = createHash("sha256").update(dto.fileContent, "utf8").digest("hex"); + const fileSize = Buffer.byteLength(dto.fileContent, "utf8"); + + const existing = await hrTimeclockRepo.findAfdImportBatchByFileHash(fileHash); + if (existing) { + throw new ConflictException({ + message: "Este arquivo já foi importado anteriormente (idempotência por SHA-256)", + alreadyImported: true, + importId: existing.id, + status: existing.status, + }); + } + + const parseResult = parseAfdFile(dto.fileContent); + + const employeeIdByCpf = await this.resolveEmployeeIds(device.company_id, parseResult.records.map((r) => r.employeeCpfRaw)); + + const byStatus = parseResult.summary.byValidationStatus; + const validCount = byStatus.VALID ?? 0; + const warningCount = byStatus.WARNING ?? 0; + const invalidCount = byStatus.INVALID ?? 0; + const unsupportedCount = byStatus.UNSUPPORTED_LAYOUT ?? 0; + const parseFailureCount = parseResult.failures.length; + + // Batch-level status: INVALID when nothing usable came out of the + // file at all; WARNING when the file is usable but has some + // invalid/unsupported/failed lines; VALID only when everything parsed + // clean. Never silently treated as fully VALID when problems exist. + const status: AfdImportBatchStatus = + validCount === 0 ? "INVALID" : invalidCount + unsupportedCount + parseFailureCount > 0 ? "WARNING" : "VALID"; + + let result; + try { + result = await hrTimeclockRepo.importAfdBatch({ + batch: { + deviceId: dto.deviceId, + companyId: device.company_id, + fileHash, + fileSize, + recordCount: parseResult.summary.parsedCount, + validCount, + warningCount, + invalidCount, + unsupportedCount, + parseFailureCount, + status, + firstNsr: parseResult.summary.firstNsr, + lastNsr: parseResult.summary.lastNsr, + summary: parseResult.summary as unknown as Record, + importedBy: user.id, + }, + records: parseResult.records.map((r) => ({ + nsr: r.nsr, + recordType: r.recordType, + eventDatetime: r.eventDatetime, + eventDatetimeRaw: r.eventDatetimeRaw, + employeeCpfRaw: r.employeeCpfRaw, + employeeId: employeeIdByCpf.get(r.employeeCpfRaw ?? "") ?? null, + rawLine: r.rawLine, + crcReceived: r.crcReceived, + crcCalculated: r.crcCalculated, + crcValid: r.crcValid, + parsedData: r.parsedData, + validationStatus: r.validationStatus, + validationNotes: r.validationNotes, + })), + }); + } catch (err) { + if (err instanceof hrTimeclockRepo.TimeclockDeviceNotFoundError) { + throw new NotFoundException("Equipamento de ponto não encontrado"); + } + if (isUniqueViolation(err)) { + // Race: another request imported the same file_hash between our + // check above and this INSERT. Never duplicate — report the + // existing batch exactly like the up-front check would have. + const raced = await hrTimeclockRepo.findAfdImportBatchByFileHash(fileHash); + if (raced) { + throw new ConflictException({ + message: "Este arquivo já foi importado anteriormente (idempotência por SHA-256)", + alreadyImported: true, + importId: raced.id, + status: raced.status, + }); + } + } + throw err; + } + + // Import itself is a mutation — audited like any other, per the task + // brief's explicit instruction ("obrigatório em toda mutação incluindo + // o próprio import"). + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "afd_import.imported", + resourceType: "afd_import_batch", + resourceId: result.batch.id, + metadata: { + deviceId: dto.deviceId, + fileHash, + recordCount: parseResult.summary.parsedCount, + insertedCount: result.insertedCount, + status, + }, + }); + + return { alreadyImported: false, batch: result.batch, summary: parseResult.summary }; + } + + async getById(id: string): Promise { + const batch = await hrTimeclockRepo.getAfdImportBatchById(id); + if (!batch) throw new NotFoundException("Importação de AFD não encontrada"); + return batch; + } + + /** + * Best-effort CPF -> timeclock_employees.id resolution. NEVER guesses: + * a CPF matched in more than one company when the device has no + * company (or vice-versa, ambiguous within the same company — can't + * actually happen given the (company_id, cpf) UNIQUE constraint, but + * the code doesn't rely on that) is left unresolved (null) rather than + * picking arbitrarily. Returns a map keyed by the RAW (unnormalized) + * cpf field, since that's what afd_records.employee_cpf_raw stores and + * what callers look records up by. + */ + private async resolveEmployeeIds( + deviceCompanyId: string | null, + rawCpfs: (string | null)[], + ): Promise> { + const normalizedByRaw = new Map(); + for (const raw of rawCpfs) { + if (!raw) continue; + const normalized = normalizeAfdCpf(raw); + if (normalized && normalized.length === 11) normalizedByRaw.set(raw, normalized); + } + + const distinctNormalized = [...new Set(normalizedByRaw.values())]; + const candidates = distinctNormalized.length > 0 + ? await hrTimeclockRepo.findTimeclockEmployeesByCpfs(distinctNormalized) + : []; + + const candidatesByCpf = new Map(); + for (const employee of candidates) { + const list = candidatesByCpf.get(employee.cpf) ?? []; + list.push(employee); + candidatesByCpf.set(employee.cpf, list); + } + + const result = new Map(); + for (const [raw, normalized] of normalizedByRaw) { + const matches = candidatesByCpf.get(normalized) ?? []; + const scoped = deviceCompanyId ? matches.filter((m) => m.company_id === deviceCompanyId) : matches; + result.set(raw, scoped.length === 1 ? scoped[0]!.id : null); + } + return result; + } +} diff --git a/apps/api/src/hr-timeclock/afd-parser.ts b/apps/api/src/hr-timeclock/afd-parser.ts new file mode 100644 index 0000000..440a377 --- /dev/null +++ b/apps/api/src/hr-timeclock/afd-parser.ts @@ -0,0 +1,495 @@ +/** + * AFD (Arquivo Fonte de Dados — Portaria MTP 671/2021, leiaute versão 004) + * parser/validator. Ported faithfully from the legacy OrçaFácil + * `parser.js`/`crc16.js` — see `eden.md` módulo 6 §4 (linhas ~3660-3771). + * + * Pure functions only — no I/O, no DB access. This module intentionally + * knows nothing about how the caller obtained the file content or what it + * will do with the parsed records (persist, preview, whatever). + * + * SCOPE OF THIS LEVA: parsing + CRC/structural validation only. Nothing + * here computes apuração (eden.md §5), fechamento de período (§6), ajustes + * (§7), banco de horas (§9) or relatórios (§10) — those are a future leva. + * + * ── Encoding note (real limitation of this leva's transport, documented + * per instruction) ────────────────────────────────────────────────────── + * The real AFD file is ISO-8859-1 (latin1), never UTF-8. This leva's HTTP + * endpoint accepts the file content as a plain JSON string (multipart + * upload is deferred — see afd-imports.controller.ts), which means the + * CALLER is responsible for decoding the original file bytes as latin1 + * into a JS string *before* putting it in the JSON body. If the caller + * instead decodes as UTF-8 (or lets some HTTP client guess), any byte + * ≥ 0x80 (accented characters in type-2 `razao_social`/`local` fields — + * type-3 punch records, the ones that matter for attendance, are plain + * ASCII/digits and unaffected) will be corrupted and CRC validation for + * that line will legitimately fail (which is the CORRECT behavior for + * corrupted input — the parser never silently "fixes" a CRC mismatch). + * This module reconstructs bytes for CRC purposes via + * `Buffer.from(str, "latin1")`, which round-trips correctly only if the + * string was decoded as latin1 to begin with. + * + * ── Field positions ───────────────────────────────────────────────────── + * eden.md documents positions as 1-indexed, inclusive ranges (e.g. + * "011-034" = characters 11 through 34, 24 chars wide). Converted here to + * JS's 0-indexed, end-exclusive `slice(a-1, b)`. Every record starts with + * a 9-digit NSR (positions 001-009) + 1-digit record type (position 010), + * i.e. `slice(0, 9)` / `slice(9, 10)` — so every type-specific field list + * below picks up at `slice(10, ...)`, matching "011" in the source doc. + * + * Principle carried over verbatim from the legacy: only fields with an + * exact, documented position are decoded. Everything else — undocumented + * trailer bytes, unsupported record types, truncated lines — is captured + * as `raw_tail` inside `parsedData`, never guessed/invented. + */ + +// ───────────────────────────────────────────────────────────────────────── +// CRC-16/KERMIT +// ───────────────────────────────────────────────────────────────────────── + +/** + * CRC-16/KERMIT: polynomial 0x1021 reflected (used in the bit-reversed + * form 0x8408), init 0x0000, no final XOR. Applies to AFD record types + * 2/3/4 only — type 7 uses its own hash chain (never recomputed here) and + * types 1/5/6/9 have no CRC field at all. + * + * Known test vector (verified in afd-parser.spec.ts and via an ad-hoc + * script before this was wired into the import path): CRC-16/KERMIT of + * the ASCII string "123456789" is 0x2189. + */ +export function crc16Kermit(data: string | Buffer): number { + const buffer = typeof data === "string" ? Buffer.from(data, "latin1") : data; + let crc = 0x0000; + for (let i = 0; i < buffer.length; i++) { + crc ^= buffer[i]!; + for (let bit = 0; bit < 8; bit++) { + if (crc & 0x0001) { + crc = (crc >>> 1) ^ 0x8408; + } else { + crc = crc >>> 1; + } + } + } + return crc & 0xffff; +} + +/** Zero-padded 4-hex-digit representation, uppercase, matching AFD's own CRC field format. */ +export function crc16Hex(data: string | Buffer): string { + return crc16Kermit(data).toString(16).padStart(4, "0").toUpperCase(); +} + +// ───────────────────────────────────────────────────────────────────────── +// CPF normalization +// ───────────────────────────────────────────────────────────────────────── + +/** + * The CPF field in AFD types 3/7 is 12 characters wide in the documented + * position, but a real CPF is 11 digits — eden.md §4.3: take the LAST 11 + * numeric digits of the raw field, never assume exact formatting. If + * fewer than 11 digits are present (corrupt/short data), returns exactly + * what was found — never left-pads with invented zeros, since that would + * fabricate digits that were never actually in the file. + */ +export function normalizeAfdCpf(raw: string | null | undefined): string | null { + if (!raw) return null; + const digits = raw.replace(/\D/g, ""); + if (digits.length === 0) return null; + return digits.length >= 11 ? digits.slice(-11) : digits; +} + +// ───────────────────────────────────────────────────────────────────────── +// Types +// ───────────────────────────────────────────────────────────────────────── + +export type AfdCrcValidity = "VALID" | "INVALID" | "NOT_APPLICABLE" | "NOT_VALIDATED"; +export type AfdValidationStatus = "VALID" | "WARNING" | "INVALID" | "UNSUPPORTED_LAYOUT"; + +export interface ParsedAfdRecord { + lineNumber: number; + nsr: number | null; + nsrRaw: string; + recordType: string; + eventDatetime: Date | null; + eventDatetimeRaw: string | null; + employeeCpfRaw: string | null; + rawLine: string; + crcReceived: string | null; + crcCalculated: string | null; + crcValid: AfdCrcValidity; + /** Type-specific decoded fields (only those with a documented position) + `raw_tail` for the rest. Never guessed. */ + parsedData: Record; + validationStatus: AfdValidationStatus; + validationNotes: string | null; +} + +export interface AfdParseFailure { + lineNumber: number; + rawLine: string; + error: string; +} + +export interface AfdParseSummary { + totalLines: number; + parsedCount: number; + failureCount: number; + byRecordType: Record; + byValidationStatus: Record; + /** Includes the sentinel trailer NSR (999999999); records whose NSR could not be parsed as a number are excluded. */ + firstNsr: number | null; + lastNsr: number | null; +} + +export interface AfdParseResult { + records: ParsedAfdRecord[]; + failures: AfdParseFailure[]; + summary: AfdParseSummary; +} + +// ───────────────────────────────────────────────────────────────────────── +// Datetime parsing — AAAA-MM-ddThh:mm:00ZZZZZ (24 chars), e.g. "2026-08-06T08:30:00-0300" +// ───────────────────────────────────────────────────────────────────────── + +const AFD_DATETIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-]\d{4})$/; + +/** + * Parses the fixed-width AFD datetime field. The UTC offset is always + * taken verbatim from the text (never assumed/injected from the server's + * own timezone) — in practice this is always -0300 (America/Sao_Paulo, + * no DST in Brazil since 2019), but this function doesn't hardcode that; + * it trusts whatever offset the equipment actually wrote, exactly per the + * "never invent" principle. Returns null (never throws) if the 24-char + * field doesn't match the documented format — the caller still keeps the + * raw text in `eventDatetimeRaw` regardless. + */ +function parseAfdDatetime(raw: string): Date | null { + const match = AFD_DATETIME_RE.exec(raw.trim()); + if (!match) return null; + const [, y, mo, d, h, mi, s, offset] = match; + const offsetWithColon = `${offset!.slice(0, 3)}:${offset!.slice(3)}`; + const iso = `${y}-${mo}-${d}T${h}:${mi}:${s}${offsetWithColon}`; + const date = new Date(iso); + return Number.isNaN(date.getTime()) ? null : date; +} + +// ───────────────────────────────────────────────────────────────────────── +// Per-line parsing +// ───────────────────────────────────────────────────────────────────────── + +const HEADER_LENGTH = 10; // 9-digit NSR + 1-digit record type + +function parseNsr(nsrRaw: string): number | null { + // Never default to 0/autoincrement — only a well-formed 9-digit numeric + // field becomes a number; anything else stays null (eden.md módulo 6 §12). + return /^\d{9}$/.test(nsrRaw) ? Number(nsrRaw) : null; +} + +interface CrcCheck { + crcReceived: string | null; + crcCalculated: string | null; + crcValid: AfdCrcValidity; + note: string | null; +} + +/** CRC is computed over the line bytes up to (not including) the CRC field itself, per eden.md §4.2. */ +function checkCrc(line: string, dataEnd: number, crcFieldStart: number, crcFieldEnd: number): CrcCheck { + if (line.length < crcFieldEnd) { + return { crcReceived: null, crcCalculated: null, crcValid: "INVALID", note: "linha mais curta que o esperado para conter o campo de CRC" }; + } + const crcReceived = line.slice(crcFieldStart, crcFieldEnd).trim(); + const crcCalculated = crc16Hex(line.slice(0, dataEnd)); + const valid = crcReceived.toUpperCase() === crcCalculated; + return { + crcReceived, + crcCalculated, + crcValid: valid ? "VALID" : "INVALID", + note: valid ? null : `CRC divergente: recebido=${crcReceived || "(vazio)"} calculado=${crcCalculated}`, + }; +} + +/** + * Parses a single AFD line. Never throws under normal malformed input — + * a line that doesn't fit any documented shape still produces a record + * (validationStatus INVALID/UNSUPPORTED_LAYOUT, nsr possibly null, + * rawLine always preserved) rather than being silently dropped, per + * eden.md's "preservar mesmo se inválido" invariant. `parseAfdFile` wraps + * this in a try/catch as a second line of defense for truly unexpected + * exceptions (defensive programming, not the normal path). + */ +export function parseAfdLine(rawLine: string, lineNumber: number): ParsedAfdRecord { + const notes: string[] = []; + + if (rawLine.length < HEADER_LENGTH) { + return { + lineNumber, + nsr: null, + nsrRaw: rawLine, + recordType: "", + eventDatetime: null, + eventDatetimeRaw: null, + employeeCpfRaw: null, + rawLine, + crcReceived: null, + crcCalculated: null, + crcValid: "NOT_VALIDATED", + parsedData: { raw_tail: rawLine }, + validationStatus: "INVALID", + validationNotes: "linha menor que o cabeçalho mínimo (NSR de 9 dígitos + 1 dígito de tipo, 10 caracteres)", + }; + } + + const nsrRaw = rawLine.slice(0, 9); + const recordType = rawLine.slice(9, 10); + const nsr = parseNsr(nsrRaw); + if (nsr === null) { + notes.push("NSR ausente ou não numérico — preservado como null, nunca substituído por 0/autoincrement"); + } + + let eventDatetimeRaw: string | null = null; + let employeeCpfRaw: string | null = null; + let parsedData: Record = {}; + let crcValid: AfdCrcValidity = "NOT_VALIDATED"; + let crcReceived: string | null = null; + let crcCalculated: string | null = null; + let validationStatus: AfdValidationStatus = "VALID"; + + switch (recordType) { + case "1": { + // Cabeçalho do arquivo — só o NSR fixo (000000000) + tipo são documentados. + parsedData = { raw_tail: rawLine.slice(HEADER_LENGTH) }; + if (nsrRaw !== "000000000") notes.push("NSR do cabeçalho difere do valor fixo esperado (000000000)"); + break; + } + case "2": { + // Inclusão/alteração do empregador no REP + const dh = rawLine.slice(10, 34); + const cpfResp = rawLine.slice(34, 48); + const tipoIdent = rawLine.slice(48, 49); + const cnpjEmpregador = rawLine.slice(49, 63); + const cnoCaepf = rawLine.slice(63, 77); + const razaoSocial = rawLine.slice(77, 227); + const local = rawLine.slice(227, 327); + eventDatetimeRaw = dh; + parsedData = { dh, cpf_resp: cpfResp, tipo_ident: tipoIdent, cnpj_empregador: cnpjEmpregador, cno_caepf: cnoCaepf, razao_social: razaoSocial, local }; + if (rawLine.length < 331) { + validationStatus = "INVALID"; + notes.push("linha mais curta que o esperado para o tipo 2 (esperado >= 331 caracteres)"); + crcValid = "INVALID"; + } else { + const crc = checkCrc(rawLine, 327, 327, 331); + crcReceived = crc.crcReceived; + crcCalculated = crc.crcCalculated; + crcValid = crc.crcValid; + if (crc.note) { notes.push(crc.note); validationStatus = "INVALID"; } + } + break; + } + case "3": { + // Marcação REP-C/REP-A — a principal (batida de ponto convencional) + const dh = rawLine.slice(10, 34); + const cpf = rawLine.slice(34, 46); + eventDatetimeRaw = dh; + employeeCpfRaw = cpf; + parsedData = { dh, cpf }; + if (rawLine.length < 50) { + validationStatus = "INVALID"; + notes.push("linha mais curta que o esperado para o tipo 3 (esperado 50 caracteres)"); + crcValid = "INVALID"; + } else { + const crc = checkCrc(rawLine, 46, 46, 50); + crcReceived = crc.crcReceived; + crcCalculated = crc.crcCalculated; + crcValid = crc.crcValid; + if (crc.note) { notes.push(crc.note); validationStatus = "INVALID"; } + } + break; + } + case "4": { + // Ajuste do relógio do REP + const dhAntes = rawLine.slice(10, 34); + const dhAjustada = rawLine.slice(34, 58); + const cpfResp = rawLine.slice(58, 69); + eventDatetimeRaw = dhAjustada; + // cpf_resp here is the CPF of whoever adjusted the clock, not + // necessarily a timeclock_employees row — stored in the same + // generic employee_cpf_raw column; resolution against + // timeclock_employees simply won't match if they aren't one + // (harmless — never fabricates a match). + employeeCpfRaw = cpfResp; + parsedData = { dh_antes: dhAntes, dh_ajustada: dhAjustada, cpf_resp: cpfResp }; + if (rawLine.length < 73) { + validationStatus = "INVALID"; + notes.push("linha mais curta que o esperado para o tipo 4 (esperado 73 caracteres)"); + crcValid = "INVALID"; + } else { + const crc = checkCrc(rawLine, 69, 69, 73); + crcReceived = crc.crcReceived; + crcCalculated = crc.crcCalculated; + crcValid = crc.crcValid; + if (crc.note) { notes.push(crc.note); validationStatus = "INVALID"; } + } + break; + } + case "5": { + // Inclusão/alteração/exclusão de empregado no REP — só dh + operação documentados + const dh = rawLine.slice(10, 34); + const operacao = rawLine.slice(34, 35); + eventDatetimeRaw = dh; + parsedData = { dh, operacao, raw_tail: rawLine.slice(35) }; + if (!["I", "A", "E"].includes(operacao)) { + notes.push(`código de operação não documentado: '${operacao}'`); + validationStatus = "WARNING"; + } + break; + } + case "6": { + // Evento sensível do REP — só dh documentado + const dh = rawLine.slice(10, 34); + eventDatetimeRaw = dh; + parsedData = { dh, raw_tail: rawLine.slice(34) }; + break; + } + case "7": { + // Marcação REP-P — usa hash próprio de encadeamento, NUNCA recalculado aqui + const dhMarcacao = rawLine.slice(10, 34); + const cpf = rawLine.slice(34, 46); + const dhGravacao = rawLine.slice(46, 70); + const coletor = rawLine.slice(70, 72); + const onlineOffline = rawLine.slice(72, 73); + const hash = rawLine.slice(73, 137); + eventDatetimeRaw = dhMarcacao; + employeeCpfRaw = cpf; + parsedData = { dh_marcacao: dhMarcacao, cpf, dh_gravacao: dhGravacao, coletor, online_offline: onlineOffline, hash }; + crcValid = "NOT_APPLICABLE"; + if (rawLine.length < 137) { + validationStatus = "INVALID"; + notes.push("linha mais curta que o esperado para o tipo 7 (esperado 137 caracteres, hash de 64 posições incompleto)"); + } + break; + } + case "9": { + // Trailer — só o NSR fixo (999999999) é documentado; contagens internas não têm posição documentada. + parsedData = { raw_tail: rawLine.slice(HEADER_LENGTH), trailer_cross_check: "not_implemented_no_documented_positions" }; + if (nsrRaw !== "999999999") notes.push("NSR do trailer difere do valor fixo esperado (999999999)"); + break; + } + default: { + parsedData = { raw_tail: rawLine.slice(HEADER_LENGTH) }; + validationStatus = "UNSUPPORTED_LAYOUT"; + notes.push(`tipo de registro não reconhecido: '${recordType}'`); + break; + } + } + + const eventDatetime = eventDatetimeRaw ? parseAfdDatetime(eventDatetimeRaw) : null; + if (eventDatetimeRaw && eventDatetime === null) { + notes.push(`campo de data/hora fora do formato esperado (AAAA-MM-ddThh:mm:00ZZZZZ): '${eventDatetimeRaw}'`); + if (validationStatus === "VALID") validationStatus = "WARNING"; + } + + if (nsr === null && validationStatus === "VALID") { + validationStatus = "INVALID"; + } + + return { + lineNumber, + nsr, + nsrRaw, + recordType, + eventDatetime, + eventDatetimeRaw, + employeeCpfRaw, + rawLine, + crcReceived, + crcCalculated, + crcValid, + parsedData, + validationStatus, + validationNotes: notes.length > 0 ? notes.join("; ") : null, + }; +} + +// ───────────────────────────────────────────────────────────────────────── +// File-level parsing +// ───────────────────────────────────────────────────────────────────────── + +/** + * Splits AFD file content into lines (CRLF per spec; also tolerates bare + * LF defensively) and parses each one. Never throws for a bad line and + * never stops processing because of one — see module doc for the + * "records vs. failures" partial-error policy: + * + * - A line that is merely malformed AFD data (wrong type, bad CRC, too + * short, unsupported record type, etc.) still produces a `records` + * entry with an INVALID/WARNING/UNSUPPORTED_LAYOUT status — it is + * NEVER dropped, because raw_line is legally-relevant raw data that + * must always be preserved (eden.md módulo 6 §12). + * - A line that causes an actual unexpected exception in `parseAfdLine` + * (a defensive safety net, not the normal path) still produces a + * fallback INVALID record so nothing is silently lost, AND is also + * recorded in `failures` for import-summary visibility. + * + * Fully blank lines (an artifact of a trailing CRLF at EOF, or stray + * blank lines in a hand-edited file) are skipped without generating a + * record — they carry no data to preserve. + */ +export function parseAfdFile(content: string): AfdParseResult { + const rawLines = content.split(/\r\n|\n\r|\n/); + const records: ParsedAfdRecord[] = []; + const failures: AfdParseFailure[] = []; + + let lineNumber = 0; + for (const line of rawLines) { + lineNumber += 1; + if (line.length === 0) continue; // trailing EOF newline / stray blank line — no data to preserve + + try { + records.push(parseAfdLine(line, lineNumber)); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + failures.push({ lineNumber, rawLine: line, error: message }); + records.push({ + lineNumber, + nsr: null, + nsrRaw: line.slice(0, 9), + recordType: line.slice(9, 10) || "", + eventDatetime: null, + eventDatetimeRaw: null, + employeeCpfRaw: null, + rawLine: line, + crcReceived: null, + crcCalculated: null, + crcValid: "NOT_VALIDATED", + parsedData: { raw_tail: line }, + validationStatus: "INVALID", + validationNotes: `Erro inesperado ao parsear linha: ${message}`, + }); + } + } + + const byRecordType: Record = {}; + const byValidationStatus: Record = {}; + let firstNsr: number | null = null; + let lastNsr: number | null = null; + + for (const record of records) { + byRecordType[record.recordType] = (byRecordType[record.recordType] ?? 0) + 1; + byValidationStatus[record.validationStatus] = (byValidationStatus[record.validationStatus] ?? 0) + 1; + if (record.nsr !== null) { + if (firstNsr === null) firstNsr = record.nsr; + lastNsr = record.nsr; + } + } + + return { + records, + failures, + summary: { + totalLines: lineNumber, + parsedCount: records.length, + failureCount: failures.length, + byRecordType, + byValidationStatus, + firstNsr, + lastNsr, + }, + }; +} diff --git a/apps/api/src/hr-timeclock/afd-records.controller.ts b/apps/api/src/hr-timeclock/afd-records.controller.ts new file mode 100644 index 0000000..0389d59 --- /dev/null +++ b/apps/api/src/hr-timeclock/afd-records.controller.ts @@ -0,0 +1,37 @@ +import { Controller, Get, Param, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { AfdRecordsService } from "./afd-records.service.js"; +import { ListAfdRecordsQueryDto } from "./dto/list-afd-records-query.dto.js"; +import { JwtAuthGuard } from "../auth/jwt-auth.guard.js"; +import { PermissionGuard } from "../auth/permission.guard.js"; +import { RequirePermission } from "../auth/decorators/require-permission.decorator.js"; + +/** + * READ-ONLY — eden.md módulo 6 §12: afd_records (dado bruto de ponto) é + * imutável. This controller intentionally has no POST/PATCH/PUT/DELETE + * handler, at any permission level — the only way a row is ever created + * is AfdImportsController's import endpoint (via + * hrTimeclockRepo.importAfdBatch's INSERT), and the database itself + * additionally rejects UPDATE/DELETE via a trigger with no bypass (see + * the migration) — so this is defense in depth, not the only guard. + */ +@ApiTags("hr-timeclock-afd-records") +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, PermissionGuard) +@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })) +@Controller("v1/hr-timeclock/afd-records") +export class AfdRecordsController { + constructor(private readonly afdRecords: AfdRecordsService) {} + + @Get() + @RequirePermission("afd-import", "view") + list(@Query() query: ListAfdRecordsQueryDto) { + return this.afdRecords.list(query); + } + + @Get(":id") + @RequirePermission("afd-import", "view") + get(@Param("id") id: string) { + return this.afdRecords.get(id); + } +} diff --git a/apps/api/src/hr-timeclock/afd-records.service.ts b/apps/api/src/hr-timeclock/afd-records.service.ts new file mode 100644 index 0000000..a25cac0 --- /dev/null +++ b/apps/api/src/hr-timeclock/afd-records.service.ts @@ -0,0 +1,45 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { hrTimeclockRepo, type AfdRecord } from "@eden/database"; +import type { ListAfdRecordsQueryDto } from "./dto/list-afd-records-query.dto.js"; + +/** + * Read-only, matching eden.md §4.5 ("nunca há rota de escrita para esta + * tabela"). There is deliberately no create/update/delete method here — + * see hr-timeclock/afd-records.controller.ts, which exposes GET only. + */ +@Injectable() +export class AfdRecordsService { + async list(query: ListAfdRecordsQueryDto): Promise<{ records: AfdRecord[]; total: number; limit: number; offset: number }> { + const limit = query.limit ?? 50; + const offset = query.offset ?? 0; + + // dateTo is inclusive at the day level per eden.md §4.5 ("date_to usa + // limite exclusivo do dia seguinte para incluir o dia inteiro") — the + // query DTO takes a plain date/datetime string; if it looks like a + // bare date (no time component), advance to the start of the next + // day so the whole end day is included. + let dateTo: Date | undefined; + if (query.dateTo) { + const parsed = new Date(query.dateTo); + dateTo = /^\d{4}-\d{2}-\d{2}$/.test(query.dateTo) ? new Date(parsed.getTime() + 24 * 60 * 60 * 1000) : parsed; + } + + const { records, total } = await hrTimeclockRepo.listAfdRecords({ + deviceId: query.deviceId, + employeeId: query.employeeId, + recordType: query.recordType, + dateFrom: query.dateFrom ? new Date(query.dateFrom) : undefined, + dateTo, + limit, + offset, + }); + + return { records, total, limit, offset }; + } + + async get(id: string): Promise { + const record = await hrTimeclockRepo.getAfdRecordById(id); + if (!record) throw new NotFoundException("Registro de AFD não encontrado"); + return record; + } +} diff --git a/apps/api/src/hr-timeclock/dto/create-afd-import.dto.ts b/apps/api/src/hr-timeclock/dto/create-afd-import.dto.ts new file mode 100644 index 0000000..62e4954 --- /dev/null +++ b/apps/api/src/hr-timeclock/dto/create-afd-import.dto.ts @@ -0,0 +1,20 @@ +import { IsString, IsUUID, MinLength } from "class-validator"; + +/** + * SIMPLIFICATION OF THIS LEVA: the AFD file is accepted as a plain string + * in the JSON body, not a real multipart upload — see + * afd-imports.controller.ts / afd-parser.ts header comments for why (and + * the encoding caveat this implies). Real multipart upload (matching + * eden.md §4.5's `POST /afd-imports/import`) is future work. + */ +export class CreateAfdImportDto { + @IsUUID() deviceId!: string; + + /** + * The AFD file's full text content, decoded by the CALLER as + * ISO-8859-1 (latin1) — see afd-parser.ts's encoding note. Sending + * UTF-8-decoded content here will cause CRC validation to legitimately + * fail for any line with a byte ≥ 0x80. + */ + @IsString() @MinLength(1) fileContent!: string; +} diff --git a/apps/api/src/hr-timeclock/dto/create-timeclock-device.dto.ts b/apps/api/src/hr-timeclock/dto/create-timeclock-device.dto.ts new file mode 100644 index 0000000..a087b7a --- /dev/null +++ b/apps/api/src/hr-timeclock/dto/create-timeclock-device.dto.ts @@ -0,0 +1,20 @@ +import { IsBoolean, IsInt, IsOptional, IsString, IsUUID, Max, MaxLength, Min, MinLength } from "class-validator"; + +export class CreateTimeclockDeviceDto { + @IsOptional() @IsUUID() companyId?: string; + + @IsString() @MinLength(1) @MaxLength(200) name!: string; + @IsOptional() @IsString() @MaxLength(2000) description?: string; + @IsOptional() @IsString() @MaxLength(200) branch?: string; + + @IsString() @MinLength(1) @MaxLength(255) host!: string; + @IsOptional() @IsInt() @Min(1) @Max(65535) port?: number; + @IsOptional() @IsBoolean() useHttps?: boolean; + + @IsString() @MinLength(1) @MaxLength(200) username!: string; + + /** Plaintext in — encrypted server-side via FieldEncryptionService before storage, never persisted or logged as-is. */ + @IsString() @MinLength(1) @MaxLength(500) password!: string; + + @IsOptional() @IsString() @MaxLength(100) timezone?: string; +} diff --git a/apps/api/src/hr-timeclock/dto/list-afd-records-query.dto.ts b/apps/api/src/hr-timeclock/dto/list-afd-records-query.dto.ts new file mode 100644 index 0000000..e87c606 --- /dev/null +++ b/apps/api/src/hr-timeclock/dto/list-afd-records-query.dto.ts @@ -0,0 +1,18 @@ +import { Type } from "class-transformer"; +import { IsDateString, IsIn, IsInt, IsOptional, IsUUID, Max, Min } from "class-validator"; + +const AFD_RECORD_TYPES = ["1", "2", "3", "4", "5", "6", "7", "9"] as const; + +export class ListAfdRecordsQueryDto { + @IsOptional() @IsUUID() deviceId?: string; + @IsOptional() @IsUUID() employeeId?: string; + @IsOptional() @IsIn(AFD_RECORD_TYPES) recordType?: string; + + /** Inclusive lower bound (ISO date/datetime string). */ + @IsOptional() @IsDateString() dateFrom?: string; + /** Inclusive upper bound at the DAY level — the service applies the next-day-exclusive convention (eden.md §4.5). */ + @IsOptional() @IsDateString() dateTo?: string; + + @IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(200) limit?: number; + @IsOptional() @Type(() => Number) @IsInt() @Min(0) offset?: number; +} diff --git a/apps/api/src/hr-timeclock/dto/update-timeclock-device.dto.ts b/apps/api/src/hr-timeclock/dto/update-timeclock-device.dto.ts new file mode 100644 index 0000000..9219ccd --- /dev/null +++ b/apps/api/src/hr-timeclock/dto/update-timeclock-device.dto.ts @@ -0,0 +1,18 @@ +import { IsBoolean, IsInt, IsOptional, IsString, Max, MaxLength, Min, MinLength } from "class-validator"; + +export class UpdateTimeclockDeviceDto { + @IsOptional() @IsString() @MinLength(1) @MaxLength(200) name?: string; + @IsOptional() @IsString() @MaxLength(2000) description?: string; + @IsOptional() @IsString() @MaxLength(200) branch?: string; + + @IsOptional() @IsString() @MinLength(1) @MaxLength(255) host?: string; + @IsOptional() @IsInt() @Min(1) @Max(65535) port?: number; + @IsOptional() @IsBoolean() useHttps?: boolean; + + @IsOptional() @IsString() @MinLength(1) @MaxLength(200) username?: string; + + /** Only re-encrypted/stored when present — omit to leave the current credential untouched. */ + @IsOptional() @IsString() @MinLength(1) @MaxLength(500) password?: string; + + @IsOptional() @IsBoolean() active?: boolean; +} diff --git a/apps/api/src/hr-timeclock/hr-timeclock.module.ts b/apps/api/src/hr-timeclock/hr-timeclock.module.ts new file mode 100644 index 0000000..c373ae4 --- /dev/null +++ b/apps/api/src/hr-timeclock/hr-timeclock.module.ts @@ -0,0 +1,45 @@ +import { Module } from "@nestjs/common"; +import { ConfigModule } from "../config/config.module.js"; +import { FieldEncryptionService } from "../crypto/field-encryption.service.js"; +import { JwtAuthGuard } from "../auth/jwt-auth.guard.js"; +import { PermissionGuard } from "../auth/permission.guard.js"; +import { TokenService } from "../auth/token.service.js"; +import { TimeclockDevicesController } from "./timeclock-devices.controller.js"; +import { TimeclockDevicesService } from "./timeclock-devices.service.js"; +import { AfdImportsController } from "./afd-imports.controller.js"; +import { AfdImportsService } from "./afd-imports.service.js"; +import { AfdRecordsController } from "./afd-records.controller.js"; +import { AfdRecordsService } from "./afd-records.service.js"; + +/** + * HR/Timeclock (eden.md módulo 6) — foundation leva. See + * afd-parser.ts and packages/database/migrations/PENDING_hr-timeclock.cjs + * for the full scope/deferral notes. + * + * INTEGRATION NOTE for whoever merges this: `ConfigModule`, + * `FieldEncryptionService`, `JwtAuthGuard`, `PermissionGuard` and + * `TokenService` are provided directly here (rather than imported from a + * shared `AuthModule`) because this worktree was isolated at a bootstrap + * commit with no Identity/Auth module yet — see apps/api/src/auth/*'s + * header comments. Once the real Identity/Auth module lands with its own + * `AuthModule` (exporting these same classes) and a global `ConfigModule` + * wired into `AppModule`, this module's `providers` list should shrink to + * just the HR-specific services and `imports: [AuthModule]` should + * replace the local guard/token/config providers below — no controller or + * service code needs to change, since they all import from the same + * `../auth/...`/`../crypto/...` paths either way. + */ +@Module({ + imports: [ConfigModule], + controllers: [TimeclockDevicesController, AfdImportsController, AfdRecordsController], + providers: [ + FieldEncryptionService, + TokenService, + JwtAuthGuard, + PermissionGuard, + TimeclockDevicesService, + AfdImportsService, + AfdRecordsService, + ], +}) +export class HrTimeclockModule {} diff --git a/apps/api/src/hr-timeclock/timeclock-devices.controller.ts b/apps/api/src/hr-timeclock/timeclock-devices.controller.ts new file mode 100644 index 0000000..008f766 --- /dev/null +++ b/apps/api/src/hr-timeclock/timeclock-devices.controller.ts @@ -0,0 +1,57 @@ +import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { TimeclockDevicesService } from "./timeclock-devices.service.js"; +import { CreateTimeclockDeviceDto } from "./dto/create-timeclock-device.dto.js"; +import { UpdateTimeclockDeviceDto } from "./dto/update-timeclock-device.dto.js"; +import { JwtAuthGuard } from "../auth/jwt-auth.guard.js"; +import { PermissionGuard } from "../auth/permission.guard.js"; +import { RequirePermission } from "../auth/decorators/require-permission.decorator.js"; +import { CurrentUser } from "../auth/decorators/current-user.decorator.js"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; + +/** + * eden.md módulo 6 §1.1/§2 — Control iD REP iDClass device registry. + * password_encrypted is never returned in plaintext by any route here — + * see TimeclockDevicesService's toView (hasCredential flag only). + * + * NOTE: `@UsePipes(ValidationPipe)` is applied locally (rather than + * relying on a global pipe in main.ts) because this bootstrap-stage + * worktree's main.ts doesn't wire one up yet — see this task's final + * summary. Harmless/redundant once the real global pipe lands. + */ +@ApiTags("hr-timeclock-devices") +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, PermissionGuard) +@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })) +@Controller("v1/hr-timeclock/devices") +export class TimeclockDevicesController { + constructor(private readonly devices: TimeclockDevicesService) {} + + @Get() + @RequirePermission("timeclock-devices", "view") + list(@Query("includeInactive") includeInactive?: string) { + return this.devices.list(includeInactive === "true"); + } + + @Get(":id") + @RequirePermission("timeclock-devices", "view") + get(@Param("id") id: string) { + return this.devices.get(id); + } + + @Post() + @RequirePermission("timeclock-devices", "edit") + create(@Body() dto: CreateTimeclockDeviceDto, @CurrentUser() user: EdenAuthenticatedUser) { + return this.devices.create(dto, user); + } + + @Patch(":id") + @RequirePermission("timeclock-devices", "edit") + update( + @Param("id") id: string, + @Body() dto: UpdateTimeclockDeviceDto, + @CurrentUser() user: EdenAuthenticatedUser, + ) { + return this.devices.update(id, dto, user); + } +} diff --git a/apps/api/src/hr-timeclock/timeclock-devices.service.ts b/apps/api/src/hr-timeclock/timeclock-devices.service.ts new file mode 100644 index 0000000..adec54c --- /dev/null +++ b/apps/api/src/hr-timeclock/timeclock-devices.service.ts @@ -0,0 +1,95 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { auditRepo, hrTimeclockRepo, type TimeclockDevice } from "@eden/database"; +import { FieldEncryptionService } from "../crypto/field-encryption.service.js"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; +import type { CreateTimeclockDeviceDto } from "./dto/create-timeclock-device.dto.js"; +import type { UpdateTimeclockDeviceDto } from "./dto/update-timeclock-device.dto.js"; + +/** Never expose password_encrypted — GET responses only ever carry hasCredential. */ +export type TimeclockDeviceView = Omit & { hasCredential: boolean }; + +function toView(device: TimeclockDevice): TimeclockDeviceView { + const { password_encrypted, ...rest } = device; + return { ...rest, hasCredential: password_encrypted.length > 0 }; +} + +@Injectable() +export class TimeclockDevicesService { + constructor(private readonly fieldEncryption: FieldEncryptionService) {} + + async list(includeInactive: boolean): Promise { + const devices = await hrTimeclockRepo.listTimeclockDevices(includeInactive); + return devices.map(toView); + } + + async get(id: string): Promise { + const device = await hrTimeclockRepo.getTimeclockDeviceById(id); + if (!device) throw new NotFoundException("Equipamento de ponto não encontrado"); + return toView(device); + } + + async create(dto: CreateTimeclockDeviceDto, user: EdenAuthenticatedUser): Promise { + const passwordEncrypted = this.fieldEncryption.encrypt(dto.password); + + const device = await hrTimeclockRepo.createTimeclockDevice({ + companyId: dto.companyId ?? null, + name: dto.name, + description: dto.description ?? null, + branch: dto.branch ?? null, + host: dto.host, + port: dto.port, + useHttps: dto.useHttps, + username: dto.username, + passwordEncrypted, + timezone: dto.timezone, + createdBy: user.id, + }); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "timeclock_device.created", + resourceType: "timeclock_device", + resourceId: device.id, + metadata: { name: device.name, host: device.host }, + }); + + return toView(device); + } + + async update( + id: string, + dto: UpdateTimeclockDeviceDto, + user: EdenAuthenticatedUser, + ): Promise { + await this.get(id); // 404 before attempting the update + + const passwordEncrypted = dto.password ? this.fieldEncryption.encrypt(dto.password) : undefined; + + const device = await hrTimeclockRepo.updateTimeclockDevice(id, { + name: dto.name, + description: dto.description, + branch: dto.branch, + host: dto.host, + port: dto.port, + useHttps: dto.useHttps, + username: dto.username, + passwordEncrypted, + active: dto.active, + updatedBy: user.id, + }); + if (!device) throw new NotFoundException("Equipamento de ponto não encontrado"); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "timeclock_device.updated", + resourceType: "timeclock_device", + resourceId: id, + // Never log the password itself, even encrypted — just whether it changed. + metadata: { fields: Object.keys(dto).filter((f) => f !== "password"), credentialChanged: passwordEncrypted !== undefined }, + }); + + return toView(device); + } +} diff --git a/apps/api/src/inventory/dto/create-asset.dto.ts b/apps/api/src/inventory/dto/create-asset.dto.ts new file mode 100644 index 0000000..ec99582 --- /dev/null +++ b/apps/api/src/inventory/dto/create-asset.dto.ts @@ -0,0 +1,21 @@ +import { IsDateString, IsOptional, IsString, IsUUID, MinLength } from "class-validator"; + +/** + * At least one of serial/patrimonyTag is required — enforced in the + * service (not here, class-validator doesn't do cross-field rules + * cleanly) so the 400 message can name both fields together. Mirrors the + * DB-level chk_serialized_assets_has_identifier constraint. + */ +export class CreateAssetDto { + @IsUUID() + productId!: string; + + @IsUUID() + warehouseId!: string; + + @IsOptional() @IsString() @MinLength(1) serial?: string; + @IsOptional() @IsString() @MinLength(1) patrimonyTag?: string; + @IsOptional() @IsString() @MinLength(1) macAddress?: string; + @IsOptional() @IsDateString() warrantyExpiresAt?: string; + @IsOptional() @IsString() notes?: string; +} diff --git a/apps/api/src/inventory/dto/create-reservation.dto.ts b/apps/api/src/inventory/dto/create-reservation.dto.ts new file mode 100644 index 0000000..0abf2a4 --- /dev/null +++ b/apps/api/src/inventory/dto/create-reservation.dto.ts @@ -0,0 +1,22 @@ +import { IsNumber, IsOptional, IsString, IsUUID, Min } from "class-validator"; + +/** + * Generic reservation — product+quantity, no serial selected yet. The + * first step of the Master Prompt §6.8 flow (reserve → later pick the + * specific serialized unit → install). + */ +export class CreateReservationDto { + @IsUUID() + warehouseId!: string; + + @IsUUID() + productId!: string; + + @IsNumber() + @Min(0.001) + quantity!: number; + + @IsOptional() @IsString() referenceType?: string; + @IsOptional() @IsUUID() referenceId?: string; + @IsOptional() @IsString() notes?: string; +} diff --git a/apps/api/src/inventory/dto/create-warehouse.dto.ts b/apps/api/src/inventory/dto/create-warehouse.dto.ts new file mode 100644 index 0000000..2472610 --- /dev/null +++ b/apps/api/src/inventory/dto/create-warehouse.dto.ts @@ -0,0 +1,16 @@ +import { IsBoolean, IsOptional, IsString, IsUUID, MinLength } from "class-validator"; + +export class CreateWarehouseDto { + @IsUUID() + companyId!: string; + + @IsString() + @MinLength(1) + name!: string; + + @IsString() + @MinLength(1) + code!: string; + + @IsOptional() @IsBoolean() isActive?: boolean; +} diff --git a/apps/api/src/inventory/dto/install-asset.dto.ts b/apps/api/src/inventory/dto/install-asset.dto.ts new file mode 100644 index 0000000..6df4d22 --- /dev/null +++ b/apps/api/src/inventory/dto/install-asset.dto.ts @@ -0,0 +1,11 @@ +import { IsIn, IsOptional, IsString, IsUUID } from "class-validator"; + +export class InstallAssetDto { + @IsUUID() + contractId!: string; + + @IsIn(["installed", "comodato"]) + assignmentType!: "installed" | "comodato"; + + @IsOptional() @IsString() notes?: string; +} diff --git a/apps/api/src/inventory/dto/reserve-asset.dto.ts b/apps/api/src/inventory/dto/reserve-asset.dto.ts new file mode 100644 index 0000000..496e4ba --- /dev/null +++ b/apps/api/src/inventory/dto/reserve-asset.dto.ts @@ -0,0 +1,7 @@ +import { IsOptional, IsString, IsUUID } from "class-validator"; + +export class ReserveAssetDto { + @IsOptional() @IsString() referenceType?: string; + @IsOptional() @IsUUID() referenceId?: string; + @IsOptional() @IsString() notes?: string; +} diff --git a/apps/api/src/inventory/dto/return-asset.dto.ts b/apps/api/src/inventory/dto/return-asset.dto.ts new file mode 100644 index 0000000..6d77d67 --- /dev/null +++ b/apps/api/src/inventory/dto/return-asset.dto.ts @@ -0,0 +1,9 @@ +import { IsOptional, IsString, IsUUID } from "class-validator"; + +export class ReturnAssetDto { + /** Warehouse receiving the physical unit back — see serialized_assets.warehouse_id comment in the migration for why this can't default to "the warehouse it left from" (that location was cleared on install). */ + @IsUUID() + warehouseId!: string; + + @IsOptional() @IsString() notes?: string; +} diff --git a/apps/api/src/inventory/dto/update-warehouse.dto.ts b/apps/api/src/inventory/dto/update-warehouse.dto.ts new file mode 100644 index 0000000..35d3896 --- /dev/null +++ b/apps/api/src/inventory/dto/update-warehouse.dto.ts @@ -0,0 +1,7 @@ +import { IsBoolean, IsOptional, IsString, MinLength } from "class-validator"; + +export class UpdateWarehouseDto { + @IsOptional() @IsString() @MinLength(1) name?: string; + @IsOptional() @IsString() @MinLength(1) code?: string; + @IsOptional() @IsBoolean() isActive?: boolean; +} diff --git a/apps/api/src/inventory/inventory.controller.ts b/apps/api/src/inventory/inventory.controller.ts new file mode 100644 index 0000000..e02b2c8 --- /dev/null +++ b/apps/api/src/inventory/inventory.controller.ts @@ -0,0 +1,120 @@ +import { Body, Controller, Get, Param, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { InventoryService } from "./inventory.service.js"; +import { CreateAssetDto } from "./dto/create-asset.dto.js"; +import { ReserveAssetDto } from "./dto/reserve-asset.dto.js"; +import { InstallAssetDto } from "./dto/install-asset.dto.js"; +import { ReturnAssetDto } from "./dto/return-asset.dto.js"; +import { CreateReservationDto } from "./dto/create-reservation.dto.js"; +import { JwtAuthGuard } from "../auth/jwt-auth.guard.js"; +import { PermissionGuard } from "../auth/permission.guard.js"; +import { RequirePermission } from "../auth/decorators/require-permission.decorator.js"; +import { CurrentUser } from "../auth/decorators/current-user.decorator.js"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; + +/** + * Serialized assets, the stock ledger, and reservations — Master Prompt + * §6.8, ADR-0007. Full cycle modeled here: reserve (generic or a specific + * serial) → install/comodato → return. Every mutation appends a + * stock_movement (ledger, never an editable balance column — see + * PENDING_inventory.cjs's header comment) and an audit_log event. + */ +@ApiTags("inventory") +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("v1/inventory") +export class InventoryController { + constructor(private readonly inventory: InventoryService) {} + + @Get("assets") + @RequirePermission("inventory", "view") + listAssets( + @Query("status") status?: string, + @Query("warehouseId") warehouseId?: string, + @Query("productId") productId?: string, + ) { + return this.inventory.listAssets(status, warehouseId, productId); + } + + @Get("assets/:id") + @RequirePermission("inventory", "view") + getAsset(@Param("id") id: string) { + return this.inventory.getAsset(id); + } + + @Post("assets") + @RequirePermission("inventory", "edit") + createAsset(@Body() dto: CreateAssetDto, @CurrentUser() user: EdenAuthenticatedUser) { + return this.inventory.createAsset(dto, user); + } + + /** + * Reserves this SPECIFIC serialized asset (quantity is always 1 — + * the serial has already been picked). For the generic product+quantity + * reservation described in the Master Prompt as the step BEFORE a + * serial is picked, see POST /v1/inventory/reservations instead. + */ + @Post("assets/:id/reserve") + @RequirePermission("inventory", "edit") + reserveAsset( + @Param("id") id: string, + @Body() dto: ReserveAssetDto, + @CurrentUser() user: EdenAuthenticatedUser, + ) { + return this.inventory.reserveAsset(id, dto, user); + } + + @Post("assets/:id/install") + @RequirePermission("inventory", "edit") + installAsset( + @Param("id") id: string, + @Body() dto: InstallAssetDto, + @CurrentUser() user: EdenAuthenticatedUser, + ) { + return this.inventory.installAsset(id, dto, user); + } + + @Post("assets/:id/return") + @RequirePermission("inventory", "edit") + returnAsset( + @Param("id") id: string, + @Body() dto: ReturnAssetDto, + @CurrentUser() user: EdenAuthenticatedUser, + ) { + return this.inventory.returnAsset(id, dto, user); + } + + @Get("movements") + @RequirePermission("inventory", "view") + listMovements( + @Query("warehouseId") warehouseId?: string, + @Query("productId") productId?: string, + @Query("referenceType") referenceType?: string, + @Query("referenceId") referenceId?: string, + ) { + return this.inventory.listMovements(warehouseId, productId, referenceType, referenceId); + } + + /** Generic quantity reservation (product+warehouse, no serial picked yet) — the first step of the Master Prompt §6.8 flow. */ + @Get("reservations") + @RequirePermission("inventory", "view") + listReservations( + @Query("status") status?: string, + @Query("warehouseId") warehouseId?: string, + @Query("productId") productId?: string, + ) { + return this.inventory.listReservations(status, warehouseId, productId); + } + + @Post("reservations") + @RequirePermission("inventory", "edit") + createReservation(@Body() dto: CreateReservationDto, @CurrentUser() user: EdenAuthenticatedUser) { + return this.inventory.createReservation(dto, user); + } + + @Post("reservations/:id/release") + @RequirePermission("inventory", "edit") + releaseReservation(@Param("id") id: string, @CurrentUser() user: EdenAuthenticatedUser) { + return this.inventory.releaseReservation(id, user); + } +} diff --git a/apps/api/src/inventory/inventory.module.ts b/apps/api/src/inventory/inventory.module.ts new file mode 100644 index 0000000..68ea57e --- /dev/null +++ b/apps/api/src/inventory/inventory.module.ts @@ -0,0 +1,15 @@ +import { Module } from "@nestjs/common"; +import { AuthModule } from "../auth/auth.module.js"; +import { WarehousesController } from "./warehouses.controller.js"; +import { WarehousesService } from "./warehouses.service.js"; +import { InventoryController } from "./inventory.controller.js"; +import { InventoryService } from "./inventory.service.js"; + +/** Warehouses/Assets/Stock — Master Prompt §6.8, ADR-0007. Not yet imported by app.module.ts (integration step left to the merge, see .claude/agents/eden-inventory.md). */ +@Module({ + imports: [AuthModule], + controllers: [WarehousesController, InventoryController], + providers: [WarehousesService, InventoryService], + exports: [WarehousesService, InventoryService], +}) +export class InventoryModule {} diff --git a/apps/api/src/inventory/inventory.service.ts b/apps/api/src/inventory/inventory.service.ts new file mode 100644 index 0000000..4a684a0 --- /dev/null +++ b/apps/api/src/inventory/inventory.service.ts @@ -0,0 +1,268 @@ +import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common"; +import { + auditRepo, + inventoryRepo, + productsRepo, + type SerializedAsset, + type StockMovement, + type StockReservation, +} from "@eden/database"; +import type { CreateAssetDto } from "./dto/create-asset.dto.js"; +import type { ReserveAssetDto } from "./dto/reserve-asset.dto.js"; +import type { InstallAssetDto } from "./dto/install-asset.dto.js"; +import type { ReturnAssetDto } from "./dto/return-asset.dto.js"; +import type { CreateReservationDto } from "./dto/create-reservation.dto.js"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; + +const { + AssetNotFoundError, + InvalidAssetTransitionError, + NoOpenAssignmentError, + ReservationNotFoundError, + InvalidReservationTransitionError, +} = inventoryRepo; + +@Injectable() +export class InventoryService { + // --------------------------------------------------------------------- + // serialized_assets + // --------------------------------------------------------------------- + + listAssets(status?: string, warehouseId?: string, productId?: string): Promise { + return inventoryRepo.listSerializedAssets({ + status: status as SerializedAsset["status"] | undefined, + warehouseId, + productId, + }); + } + + async getAsset(id: string): Promise { + const asset = await inventoryRepo.getSerializedAssetById(id); + if (!asset) throw new NotFoundException("Ativo não encontrado"); + return asset; + } + + async createAsset(dto: CreateAssetDto, user: EdenAuthenticatedUser): Promise { + if (!dto.serial && !dto.patrimonyTag) { + throw new BadRequestException("Informe ao menos serial ou patrimonyTag"); + } + + const product = await productsRepo.getProductById(dto.productId); + if (!product) throw new BadRequestException(`Produto ${dto.productId} não encontrado`); + + const warehouse = await inventoryRepo.getWarehouseById(dto.warehouseId); + if (!warehouse) throw new BadRequestException(`Armazém ${dto.warehouseId} não encontrado`); + + let result: Awaited>; + try { + result = await inventoryRepo.createAssetWithPurchaseMovement({ + productId: dto.productId, + warehouseId: dto.warehouseId, + serial: dto.serial ?? null, + patrimonyTag: dto.patrimonyTag ?? null, + macAddress: dto.macAddress ?? null, + warrantyExpiresAt: dto.warrantyExpiresAt ?? null, + notes: dto.notes ?? null, + createdBy: user.id, + }); + } catch (err) { + // 23505 = unique_violation — the DB-enforced "serial/patrimony/MAC + // never active twice" invariant (see migration). + if (isUniqueViolation(err)) { + throw new ConflictException( + "Serial, patrimônio ou MAC já está em uso por outro ativo ativo", + ); + } + throw err; + } + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "inventory.asset_created", + resourceType: "serialized_asset", + resourceId: result.asset.id, + metadata: { productId: dto.productId, warehouseId: dto.warehouseId, serial: dto.serial ?? null }, + }); + + return result.asset; + } + + async reserveAsset(id: string, dto: ReserveAssetDto, user: EdenAuthenticatedUser): Promise { + try { + const result = await inventoryRepo.reserveAsset(id, { + referenceType: dto.referenceType ?? null, + referenceId: dto.referenceId ?? null, + notes: dto.notes ?? null, + createdBy: user.id, + }); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "inventory.asset_reserved", + resourceType: "serialized_asset", + resourceId: id, + metadata: { reservationId: result.reservation.id }, + }); + + return result.asset; + } catch (err) { + if (err instanceof AssetNotFoundError) throw new NotFoundException("Ativo não encontrado"); + if (err instanceof InvalidAssetTransitionError) { + throw new ConflictException( + `Ativo com status '${err.fromStatus}' não pode ser reservado (esperado: in_stock)`, + ); + } + throw err; + } + } + + async installAsset(id: string, dto: InstallAssetDto, user: EdenAuthenticatedUser): Promise { + try { + const result = await inventoryRepo.installAsset(id, { + contractId: dto.contractId, + assignmentType: dto.assignmentType, + notes: dto.notes ?? null, + createdBy: user.id, + }); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "inventory.asset_installed", + resourceType: "serialized_asset", + resourceId: id, + metadata: { contractId: dto.contractId, assignmentType: dto.assignmentType }, + }); + + return result.asset; + } catch (err) { + if (err instanceof AssetNotFoundError) throw new NotFoundException("Ativo não encontrado"); + if (err instanceof InvalidAssetTransitionError) { + throw new ConflictException( + `Ativo com status '${err.fromStatus}' não pode ser instalado/comodato (esperado: in_stock ou reserved)`, + ); + } + throw err; + } + } + + async returnAsset(id: string, dto: ReturnAssetDto, user: EdenAuthenticatedUser): Promise { + const warehouse = await inventoryRepo.getWarehouseById(dto.warehouseId); + if (!warehouse) throw new BadRequestException(`Armazém ${dto.warehouseId} não encontrado`); + + try { + const result = await inventoryRepo.returnAsset(id, { + warehouseId: dto.warehouseId, + notes: dto.notes ?? null, + createdBy: user.id, + }); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "inventory.asset_returned", + resourceType: "serialized_asset", + resourceId: id, + metadata: { warehouseId: dto.warehouseId }, + }); + + return result.asset; + } catch (err) { + if (err instanceof AssetNotFoundError) throw new NotFoundException("Ativo não encontrado"); + if (err instanceof InvalidAssetTransitionError) { + throw new ConflictException( + `Ativo com status '${err.fromStatus}' não pode ser devolvido (esperado: installed ou comodato)`, + ); + } + if (err instanceof NoOpenAssignmentError) { + throw new ConflictException("Ativo não tem vínculo aberto para encerrar"); + } + throw err; + } + } + + // --------------------------------------------------------------------- + // stock_movements + // --------------------------------------------------------------------- + + listMovements( + warehouseId?: string, + productId?: string, + referenceType?: string, + referenceId?: string, + ): Promise { + return inventoryRepo.listStockMovements({ warehouseId, productId, referenceType, referenceId }); + } + + // --------------------------------------------------------------------- + // stock_reservations + // --------------------------------------------------------------------- + + listReservations(status?: string, warehouseId?: string, productId?: string): Promise { + return inventoryRepo.listStockReservations({ + status: status as StockReservation["status"] | undefined, + warehouseId, + productId, + }); + } + + async createReservation(dto: CreateReservationDto, user: EdenAuthenticatedUser): Promise { + const product = await productsRepo.getProductById(dto.productId); + if (!product) throw new BadRequestException(`Produto ${dto.productId} não encontrado`); + + const warehouse = await inventoryRepo.getWarehouseById(dto.warehouseId); + if (!warehouse) throw new BadRequestException(`Armazém ${dto.warehouseId} não encontrado`); + + const result = await inventoryRepo.createGenericReservation({ + warehouseId: dto.warehouseId, + productId: dto.productId, + quantity: dto.quantity, + referenceType: dto.referenceType ?? null, + referenceId: dto.referenceId ?? null, + notes: dto.notes ?? null, + createdBy: user.id, + }); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "inventory.reservation_created", + resourceType: "stock_reservation", + resourceId: result.reservation.id, + metadata: { productId: dto.productId, warehouseId: dto.warehouseId, quantity: dto.quantity }, + }); + + return result.reservation; + } + + async releaseReservation(id: string, user: EdenAuthenticatedUser): Promise { + try { + const result = await inventoryRepo.releaseReservation(id, user.id); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "inventory.reservation_released", + resourceType: "stock_reservation", + resourceId: id, + metadata: {}, + }); + + return result.reservation; + } catch (err) { + if (err instanceof ReservationNotFoundError) throw new NotFoundException("Reserva não encontrada"); + if (err instanceof InvalidReservationTransitionError) { + throw new ConflictException( + `Reserva com status '${err.fromStatus}' não pode ser liberada (esperado: active)`, + ); + } + throw err; + } + } +} + +function isUniqueViolation(err: unknown): boolean { + return typeof err === "object" && err !== null && (err as { code?: string }).code === "23505"; +} diff --git a/apps/api/src/inventory/warehouses.controller.ts b/apps/api/src/inventory/warehouses.controller.ts new file mode 100644 index 0000000..7fc2c02 --- /dev/null +++ b/apps/api/src/inventory/warehouses.controller.ts @@ -0,0 +1,54 @@ +import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { WarehousesService } from "./warehouses.service.js"; +import { CreateWarehouseDto } from "./dto/create-warehouse.dto.js"; +import { UpdateWarehouseDto } from "./dto/update-warehouse.dto.js"; +import { JwtAuthGuard } from "../auth/jwt-auth.guard.js"; +import { PermissionGuard } from "../auth/permission.guard.js"; +import { RequirePermission } from "../auth/decorators/require-permission.decorator.js"; +import { CurrentUser } from "../auth/decorators/current-user.decorator.js"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; + +/** + * Warehouses/Assets/Stock — Master Prompt §6.8, ADR-0007. Read is gated by + * inventory:view / write by inventory:edit for everything in this module + * (warehouses included) — no reseller-style scope filtering here, mirrors + * the simpler products/companies pattern rather than contracts' scoped + * one: warehouses/stock are an internal operational concern, not + * reseller-partitioned data. + */ +@ApiTags("inventory") +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("v1/warehouses") +export class WarehousesController { + constructor(private readonly warehouses: WarehousesService) {} + + @Get() + @RequirePermission("inventory", "view") + list(@Query("includeInactive") includeInactive?: string) { + return this.warehouses.list(includeInactive === "true"); + } + + @Get(":id") + @RequirePermission("inventory", "view") + get(@Param("id") id: string) { + return this.warehouses.get(id); + } + + @Post() + @RequirePermission("inventory", "edit") + create(@Body() dto: CreateWarehouseDto, @CurrentUser() user: EdenAuthenticatedUser) { + return this.warehouses.create(dto, user); + } + + @Patch(":id") + @RequirePermission("inventory", "edit") + update( + @Param("id") id: string, + @Body() dto: UpdateWarehouseDto, + @CurrentUser() user: EdenAuthenticatedUser, + ) { + return this.warehouses.update(id, dto, user); + } +} diff --git a/apps/api/src/inventory/warehouses.service.ts b/apps/api/src/inventory/warehouses.service.ts new file mode 100644 index 0000000..7561a49 --- /dev/null +++ b/apps/api/src/inventory/warehouses.service.ts @@ -0,0 +1,64 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { auditRepo, companiesRepo, inventoryRepo, type Warehouse } from "@eden/database"; +import type { CreateWarehouseDto } from "./dto/create-warehouse.dto.js"; +import type { UpdateWarehouseDto } from "./dto/update-warehouse.dto.js"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; + +@Injectable() +export class WarehousesService { + list(includeInactive: boolean): Promise { + return inventoryRepo.listWarehouses(includeInactive); + } + + async get(id: string): Promise { + const warehouse = await inventoryRepo.getWarehouseById(id); + if (!warehouse) throw new NotFoundException("Armazém não encontrado"); + return warehouse; + } + + async create(dto: CreateWarehouseDto, user: EdenAuthenticatedUser): Promise { + const company = await companiesRepo.getCompanyById(dto.companyId); + if (!company) throw new NotFoundException("Empresa (legal entity) não encontrada"); + + const warehouse = await inventoryRepo.createWarehouse({ + companyId: dto.companyId, + name: dto.name, + code: dto.code, + isActive: dto.isActive, + createdBy: user.id, + }); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "warehouse.created", + resourceType: "warehouse", + resourceId: warehouse.id, + metadata: { code: warehouse.code, companyId: warehouse.company_id }, + }); + + return warehouse; + } + + async update(id: string, dto: UpdateWarehouseDto, user: EdenAuthenticatedUser): Promise { + await this.get(id); // 404 before attempting the update + + const warehouse = await inventoryRepo.updateWarehouse(id, { + name: dto.name, + code: dto.code, + isActive: dto.isActive, + }); + if (!warehouse) throw new NotFoundException("Armazém não encontrado"); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "warehouse.updated", + resourceType: "warehouse", + resourceId: warehouse.id, + metadata: { name: dto.name, code: dto.code, isActive: dto.isActive }, + }); + + return warehouse; + } +} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 2f95223..410cf68 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -2,6 +2,7 @@ import "reflect-metadata"; import { NestFactory } from "@nestjs/core"; import { ValidationPipe } from "@nestjs/common"; import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; +import { loadEnv } from "@eden/config"; import { AppModule } from "./app.module"; async function bootstrap() { @@ -9,6 +10,17 @@ async function bootstrap() { app.setGlobalPrefix("api", { exclude: ["health/live", "health/ready"] }); + // The 3 frontends (core-web/reseller-web/subscriber-web) each run on a + // distinct origin/port (ADR-0002) and call this API cross-origin — never + // a wildcard, since requests carry bearer tokens (see env-schema.ts). + const corsOrigins = (loadEnv().EDEN_CORS_ORIGINS ?? "") + .split(",") + .map((origin) => origin.trim()) + .filter(Boolean); + if (corsOrigins.length > 0) { + app.enableCors({ origin: corsOrigins, credentials: true }); + } + // Server-side validation is the authority — never trust that a client // already validated (Master Prompt §12). Strips unknown fields instead // of accepting them silently (mass-assignment protection). diff --git a/apps/api/src/support/dto/create-ticket-comment.dto.ts b/apps/api/src/support/dto/create-ticket-comment.dto.ts new file mode 100644 index 0000000..8368937 --- /dev/null +++ b/apps/api/src/support/dto/create-ticket-comment.dto.ts @@ -0,0 +1,10 @@ +import { IsBoolean, IsOptional, IsString, MinLength } from "class-validator"; + +export class CreateTicketCommentDto { + @IsString() @MinLength(1) body!: string; + + // Defaults to false (public comment) — is_internal=true is never + // visible to the subscriber/reseller portals (enforced in + // support.service.ts::get, not just by UI convention). + @IsOptional() @IsBoolean() isInternal?: boolean; +} diff --git a/apps/api/src/support/dto/create-ticket.dto.ts b/apps/api/src/support/dto/create-ticket.dto.ts new file mode 100644 index 0000000..6222c11 --- /dev/null +++ b/apps/api/src/support/dto/create-ticket.dto.ts @@ -0,0 +1,24 @@ +import { IsIn, IsOptional, IsString, IsUUID, MinLength } from "class-validator"; + +export class CreateTicketDto { + @IsString() @MinLength(3) title!: string; + @IsString() @MinLength(3) description!: string; + @IsString() category!: string; + + @IsOptional() @IsString() subcategory?: string; + + @IsOptional() + @IsIn(["low", "medium", "high", "critical"]) + priority?: "low" | "medium" | "high" | "critical"; + + // resellerId is only honored for an actor with 'all' scope on + // support-tickets:edit (core staff) — a reseller-scoped actor always + // gets their own resellerId forced server-side in support.service.ts, + // never trusting this field for authorization (CLAUDE.md: never trust + // a client-supplied reseller_id). + @IsOptional() @IsUUID() resellerId?: string; + + @IsOptional() @IsUUID() clientRegistrationId?: string; + @IsOptional() @IsUUID() contractId?: string; + @IsOptional() @IsUUID() ownerUserId?: string; +} diff --git a/apps/api/src/support/dto/update-ticket-status.dto.ts b/apps/api/src/support/dto/update-ticket-status.dto.ts new file mode 100644 index 0000000..6210467 --- /dev/null +++ b/apps/api/src/support/dto/update-ticket-status.dto.ts @@ -0,0 +1,31 @@ +import { IsIn, IsOptional, IsString } from "class-validator"; + +export class UpdateTicketStatusDto { + @IsIn([ + "new", + "triage", + "in_progress", + "waiting_customer", + "waiting_third_party", + "resolved", + "closed", + "cancelled", + ]) + toStatus!: + | "new" + | "triage" + | "in_progress" + | "waiting_customer" + | "waiting_third_party" + | "resolved" + | "closed" + | "cancelled"; + + @IsOptional() @IsString() reason?: string; + + // Only persisted by the service layer on a transition into 'closed' — + // see support.service.ts::updateStatus and the root_cause column + // comment in PENDING_support.cjs. + @IsOptional() @IsString() rootCause?: string; + @IsOptional() @IsString() resolutionNotes?: string; +} diff --git a/apps/api/src/support/sla.ts b/apps/api/src/support/sla.ts new file mode 100644 index 0000000..53c6687 --- /dev/null +++ b/apps/api/src/support/sla.ts @@ -0,0 +1,76 @@ +/** + * Support/Service Desk SLA engine — Master Prompt §6.13. Pure, DB-free + * functions (mirrors apps/api/src/quotes/pricing.ts: business logic lives + * here, not scattered across the repository/service layers, and is + * trivially unit-testable without a database). + * + * SLA policy for this MVP is deliberately a single flat default + * (first response in 4h, resolution in 24h) applied to every ticket + * regardless of category/priority. A configurable, per-category SLA + * policy (and a calendar-aware "business hours only" due-date engine, + * instead of naive wall-clock offsets) is explicitly DEFERRED — see the + * migration header comment in PENDING_support.cjs. + */ + +/** The subset of a sla_timers row this module needs — duck-typed so callers can pass the full DB row. */ +export interface SlaTimerPauseState { + paused_since: Date | null; + accumulated_pause_seconds: number; +} + +export interface DefaultSlaDueDates { + firstResponseDueAt: Date; + resolutionDueAt: Date; +} + +const DEFAULT_FIRST_RESPONSE_HOURS = 4; +const DEFAULT_RESOLUTION_HOURS = 24; + +/** + * The example SLA policy this MVP ships with: first_response_due_at = + * now + 4h, resolution_due_at = now + 24h, for every ticket regardless of + * priority/category. Called once at ticket creation + * (support.service.ts::create) — never recomputed afterwards. + */ +export function computeDefaultSlaDueDates(now: Date): DefaultSlaDueDates { + return { + firstResponseDueAt: new Date(now.getTime() + DEFAULT_FIRST_RESPONSE_HOURS * 3_600_000), + resolutionDueAt: new Date(now.getTime() + DEFAULT_RESOLUTION_HOURS * 3_600_000), + }; +} + +/** + * Total time (in seconds) the timer has spent paused, up to `now` — + * accumulated_pause_seconds from past pause/resume cycles, PLUS the + * duration of the current pause if the timer is still paused right now + * (paused_since is non-null). This is exactly the amount that must be + * subtracted from wall-clock elapsed time to get the SLA-relevant + * "elapsed" time — a ticket sitting in waiting_customer does not burn + * SLA time by default (eden-support persona spec). + * + * Never negative even if `now` is (pathologically) earlier than + * paused_since — clamped at 0 for that slice. + */ +export function computeElapsedSeconds(timer: SlaTimerPauseState, now: Date): number { + let pausedSeconds = timer.accumulated_pause_seconds; + if (timer.paused_since) { + const currentPauseSeconds = Math.floor((now.getTime() - timer.paused_since.getTime()) / 1000); + pausedSeconds += Math.max(0, currentPauseSeconds); + } + return pausedSeconds; +} + +/** + * Whether the given SLA due date has been breached as of `now`, once the + * paused time (computeElapsedSeconds) is excluded from the elapsed + * wall-clock time. Works for either due date (first_response_due_at or + * resolution_due_at) — pass whichever is being checked. + * + * adjustedNow = now - totalPausedSeconds + * breached = adjustedNow > dueAt + */ +export function isSlaBreached(timer: SlaTimerPauseState, dueAt: Date, now: Date): boolean { + const pausedSeconds = computeElapsedSeconds(timer, now); + const adjustedNowMs = now.getTime() - pausedSeconds * 1000; + return adjustedNowMs > dueAt.getTime(); +} diff --git a/apps/api/src/support/support.controller.ts b/apps/api/src/support/support.controller.ts new file mode 100644 index 0000000..249e40f --- /dev/null +++ b/apps/api/src/support/support.controller.ts @@ -0,0 +1,64 @@ +import { Body, Controller, Get, Param, Patch, Post, UseGuards } from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { SupportService } from "./support.service.js"; +import { CreateTicketDto } from "./dto/create-ticket.dto.js"; +import { UpdateTicketStatusDto } from "./dto/update-ticket-status.dto.js"; +import { CreateTicketCommentDto } from "./dto/create-ticket-comment.dto.js"; +import { JwtAuthGuard } from "../auth/jwt-auth.guard.js"; +import { PermissionGuard } from "../auth/permission.guard.js"; +import { RequirePermission } from "../auth/decorators/require-permission.decorator.js"; +import { CurrentUser } from "../auth/decorators/current-user.decorator.js"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; + +/** + * Support/Service Desk — Master Prompt §6.13. Greenfield MVP: tickets, + * status machine + SLA timers (see apps/api/src/support/sla.ts), + * public/internal comments. DEFERRED (see PENDING_support.cjs header): + * real S3 attachments, CSAT, dedicated work-order entity, routing + * queues, configurable attendance calendar. + */ +@ApiTags("support") +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("v1/support/tickets") +export class SupportController { + constructor(private readonly support: SupportService) {} + + @Get() + @RequirePermission("support-tickets", "view") + list(@CurrentUser() user: EdenAuthenticatedUser) { + return this.support.list(user); + } + + @Get(":id") + @RequirePermission("support-tickets", "view") + get(@Param("id") id: string, @CurrentUser() user: EdenAuthenticatedUser) { + return this.support.get(id, user); + } + + @Post() + @RequirePermission("support-tickets", "edit") + create(@Body() dto: CreateTicketDto, @CurrentUser() user: EdenAuthenticatedUser) { + return this.support.create(dto, user); + } + + @Patch(":id/status") + @RequirePermission("support-tickets", "edit") + updateStatus( + @Param("id") id: string, + @Body() dto: UpdateTicketStatusDto, + @CurrentUser() user: EdenAuthenticatedUser, + ) { + return this.support.updateStatus(id, dto, user); + } + + @Post(":id/comments") + @RequirePermission("support-tickets", "edit") + addComment( + @Param("id") id: string, + @Body() dto: CreateTicketCommentDto, + @CurrentUser() user: EdenAuthenticatedUser, + ) { + return this.support.addComment(id, dto, user); + } +} diff --git a/apps/api/src/support/support.module.ts b/apps/api/src/support/support.module.ts new file mode 100644 index 0000000..17e9ad8 --- /dev/null +++ b/apps/api/src/support/support.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; +import { AuthModule } from "../auth/auth.module.js"; +import { SupportController } from "./support.controller.js"; +import { SupportService } from "./support.service.js"; + +@Module({ + imports: [AuthModule], + controllers: [SupportController], + providers: [SupportService], + exports: [SupportService], +}) +export class SupportModule {} diff --git a/apps/api/src/support/support.service.ts b/apps/api/src/support/support.service.ts new file mode 100644 index 0000000..b42717d --- /dev/null +++ b/apps/api/src/support/support.service.ts @@ -0,0 +1,176 @@ +import { + ConflictException, + ForbiddenException, + Injectable, + NotFoundException, +} from "@nestjs/common"; +import { + auditRepo, + supportTicketsRepo, + type SupportTicket, + type TicketComment, + type TicketWithDetails, +} from "@eden/database"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; +import { maxGrantedScope } from "../auth/permissions.js"; +import { computeDefaultSlaDueDates, isSlaBreached } from "./sla.js"; +import type { CreateTicketDto } from "./dto/create-ticket.dto.js"; +import type { UpdateTicketStatusDto } from "./dto/update-ticket-status.dto.js"; +import type { CreateTicketCommentDto } from "./dto/create-ticket-comment.dto.js"; + +const { TicketNotFoundError, InvalidTicketTransitionError } = supportTicketsRepo; + +const RESOURCE = "support-tickets"; + +@Injectable() +export class SupportService { + async list(user: EdenAuthenticatedUser): Promise { + const scope = maxGrantedScope(user.permissions, RESOURCE, "view"); + if (!scope) throw new ForbiddenException("Sem permissão para 'view' em 'support-tickets'"); + if (scope === "all") return supportTicketsRepo.listTickets({ scope: "all" }); + return supportTicketsRepo.listTickets({ scope: "reseller", resellerId: user.resellerId }); + } + + async get(id: string, user: EdenAuthenticatedUser): Promise { + const scope = maxGrantedScope(user.permissions, RESOURCE, "view"); + const ticket = await supportTicketsRepo.getTicketById(id); + if (!ticket || !scope) throw new NotFoundException("Ticket não encontrado"); + if (scope !== "all" && ticket.reseller_id !== user.resellerId) { + throw new NotFoundException("Ticket não encontrado"); + } + + // is_internal comments are never visible outside 'all' scope (core + // staff) — enforced here, in the query layer, not left to the UI. + // Reinforces the eden-support persona invariant: "comentário interno + // nunca visível ao cliente/revenda". A dedicated 'own' scope for the + // subscriber portal isn't wired up in this MVP (mirrors contracts.ts, + // which also only distinguishes all/reseller) — when it lands, it + // must fall into this same filtered branch. + if (scope !== "all") { + return { ...ticket, comments: ticket.comments.filter((c) => !c.is_internal) }; + } + return ticket; + } + + async create(dto: CreateTicketDto, user: EdenAuthenticatedUser): Promise { + const editScope = maxGrantedScope(user.permissions, RESOURCE, "edit"); + if (!editScope) throw new ForbiddenException("Sem permissão para 'edit' em 'support-tickets'"); + + // Never trust a client-supplied reseller_id for a reseller-scoped + // actor (CLAUDE.md: authorization never trusts a client field) — it's + // always forced to the caller's own reseller. Only an 'all'-scope + // actor (core staff) may open a ticket on behalf of an arbitrary + // reseller. + const resellerId = editScope === "all" ? (dto.resellerId ?? null) : user.resellerId; + + const now = new Date(); + const { firstResponseDueAt, resolutionDueAt } = computeDefaultSlaDueDates(now); + + const ticket = await supportTicketsRepo.createTicket({ + title: dto.title, + description: dto.description, + category: dto.category, + subcategory: dto.subcategory ?? null, + priority: dto.priority ?? "medium", + resellerId, + clientRegistrationId: dto.clientRegistrationId ?? null, + contractId: dto.contractId ?? null, + ownerUserId: dto.ownerUserId ?? null, + openedByUserId: user.id, + createdBy: user.id, + firstResponseDueAt, + resolutionDueAt, + }); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "support_ticket.created", + resourceType: "support_ticket", + resourceId: ticket.id, + metadata: { protocolNumber: ticket.protocol_number, priority: ticket.priority, category: ticket.category }, + }); + + return ticket; + } + + async updateStatus( + id: string, + dto: UpdateTicketStatusDto, + user: EdenAuthenticatedUser, + ): Promise { + await this.get(id, user); // scope check — 404 before attempting the transition + + try { + const { ticket, slaTimer } = await supportTicketsRepo.updateTicketStatus( + id, + dto.toStatus, + dto.reason ?? null, + user.id, + { rootCause: dto.rootCause ?? null, resolutionNotes: dto.resolutionNotes ?? null }, + ); + + // Only reachable when entering 'resolved' from 'in_progress' (see + // TICKET_STATUS_TRANSITIONS), so the timer returned here is + // guaranteed unpaused — safe to evaluate the breach directly + // against resolution_due_at. + if (dto.toStatus === "resolved") { + const now = new Date(); + const met = !isSlaBreached(slaTimer, slaTimer.resolution_due_at, now); + await supportTicketsRepo.recordResolutionMet(id, met); + } + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "support_ticket.status_changed", + resourceType: "support_ticket", + resourceId: id, + metadata: { toStatus: dto.toStatus, reason: dto.reason ?? null }, + }); + + return ticket; + } catch (err) { + if (err instanceof TicketNotFoundError) throw new NotFoundException("Ticket não encontrado"); + if (err instanceof InvalidTicketTransitionError) { + throw new ConflictException(err.message); + } + throw err; + } + } + + async addComment( + id: string, + dto: CreateTicketCommentDto, + user: EdenAuthenticatedUser, + ): Promise { + const ticket = await this.get(id, user); + const isInternal = dto.isInternal ?? false; + + const comment = await supportTicketsRepo.addTicketComment(id, { + authorUserId: user.id, + body: dto.body, + isInternal, + }); + + // First public response starts/settles first_response_at + the SLA + // first_response_met flag — only once, never recomputed afterwards + // (see recordFirstResponse's idempotent WHERE ... IS NULL guards). + if (!isInternal && ticket.first_response_at === null && ticket.slaTimer) { + const now = new Date(); + const met = !isSlaBreached(ticket.slaTimer, ticket.slaTimer.first_response_due_at, now); + await supportTicketsRepo.recordFirstResponse(id, met, now); + } + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId: user.id, + action: "support_ticket.comment_added", + resourceType: "support_ticket", + resourceId: id, + metadata: { isInternal }, + }); + + return comment; + } +} diff --git a/apps/api/test/afd-parser.spec.ts b/apps/api/test/afd-parser.spec.ts new file mode 100644 index 0000000..ac3af62 --- /dev/null +++ b/apps/api/test/afd-parser.spec.ts @@ -0,0 +1,117 @@ +import { describe, expect, it } from "vitest"; +import { crc16Hex, crc16Kermit, normalizeAfdCpf, parseAfdFile, parseAfdLine } from "../src/hr-timeclock/afd-parser.js"; + +/** + * Pure-function unit tests for the AFD parser — no DB, no Nest app, unlike + * every other spec in this directory (which are DB-backed integration + * tests per vitest.config.ts's own doc comment). Placed here anyway + * because `include: ["test/**\/*.spec.ts"]` is the only pattern vitest + * picks up for @eden/api; these tests just happen not to need Postgres. + */ +describe("crc16Kermit", () => { + it("matches the known CRC-16/KERMIT test vector for ASCII '123456789'", () => { + expect(crc16Kermit("123456789")).toBe(0x2189); + expect(crc16Hex("123456789")).toBe("2189"); + }); +}); + +describe("normalizeAfdCpf", () => { + it("takes the last 11 digits of a wider raw field", () => { + expect(normalizeAfdCpf("01234567890X")).toBe("01234567890"); + expect(normalizeAfdCpf(" 123.456.789-01 ")).toBe("12345678901"); + }); + + it("never invents padding for a short field", () => { + expect(normalizeAfdCpf("123")).toBe("123"); + expect(normalizeAfdCpf(null)).toBeNull(); + expect(normalizeAfdCpf("")).toBeNull(); + }); +}); + +function buildType3Line(nsr: string, dh: string, cpf: string): string { + const dataPart = `${nsr}3${dh}${cpf}`; + return dataPart + crc16Hex(dataPart); +} + +describe("parseAfdLine — type 3 (marcação REP-C/REP-A)", () => { + it("parses a well-formed, CRC-valid punch line", () => { + const line = buildType3Line("000000001", "2026-08-06T08:30:00-0300", "01234567890X"); + const record = parseAfdLine(line, 1); + + expect(record.nsr).toBe(1); + expect(record.recordType).toBe("3"); + expect(record.crcValid).toBe("VALID"); + expect(record.validationStatus).toBe("VALID"); + expect(record.employeeCpfRaw).toBe("01234567890X"); + expect(record.eventDatetime?.toISOString()).toBe(new Date("2026-08-06T08:30:00-03:00").toISOString()); + }); + + it("flags a tampered CRC as INVALID but still preserves the raw line", () => { + const line = buildType3Line("000000002", "2026-08-06T08:30:00-0300", "01234567890X"); + const tampered = line.slice(0, -1) + (line.at(-1) === "0" ? "1" : "0"); + const record = parseAfdLine(tampered, 2); + + expect(record.crcValid).toBe("INVALID"); + expect(record.validationStatus).toBe("INVALID"); + expect(record.rawLine).toBe(tampered); + expect(record.validationNotes).toMatch(/CRC divergente/); + }); + + it("never invents an NSR — a non-numeric NSR field stays null", () => { + const line = buildType3Line("ABCDEFGHI", "2026-08-06T08:30:00-0300", "01234567890X"); + const record = parseAfdLine(line, 3); + + expect(record.nsr).toBeNull(); + expect(record.nsrRaw).toBe("ABCDEFGHI"); + expect(record.validationStatus).toBe("INVALID"); + }); +}); + +describe("parseAfdLine — type 7 (marcação REP-P)", () => { + it("never recomputes the type-7 hash chain — crcValid is always NOT_APPLICABLE", () => { + const dh = "2026-08-06T08:30:00-0300"; + const line = "0000000037" + dh + "01234567890X" + dh + "01" + "O" + "a".repeat(64); + const record = parseAfdLine(line, 1); + + expect(record.recordType).toBe("7"); + expect(record.crcValid).toBe("NOT_APPLICABLE"); + expect(record.crcCalculated).toBeNull(); + }); +}); + +describe("parseAfdLine — unsupported/malformed", () => { + it("captures an unrecognized record type as UNSUPPORTED_LAYOUT, never dropped", () => { + const record = parseAfdLine("000000005X" + "resto-nao-documentado", 5); + expect(record.validationStatus).toBe("UNSUPPORTED_LAYOUT"); + expect(record.parsedData.raw_tail).toBe("resto-nao-documentado"); + }); + + it("preserves a too-short line instead of throwing", () => { + const record = parseAfdLine("12345", 6); + expect(record.validationStatus).toBe("INVALID"); + expect(record.rawLine).toBe("12345"); + expect(record.nsr).toBeNull(); + }); +}); + +describe("parseAfdFile — partial-error policy", () => { + it("keeps parsing all lines even when some are malformed, never dropping raw data", () => { + const good = buildType3Line("000000001", "2026-08-06T08:30:00-0300", "01234567890X"); + const bad = "short"; + const content = [good, bad].join("\r\n"); + + const result = parseAfdFile(content); + expect(result.records).toHaveLength(2); + expect(result.failures).toHaveLength(0); // "bad" is malformed-but-handled, not an unexpected exception + expect(result.summary.byValidationStatus.VALID).toBe(1); + expect(result.summary.byValidationStatus.INVALID).toBe(1); + expect(result.summary.firstNsr).toBe(1); + expect(result.summary.lastNsr).toBe(1); + }); + + it("skips blank trailing lines without generating a phantom record", () => { + const good = buildType3Line("000000001", "2026-08-06T08:30:00-0300", "01234567890X"); + const result = parseAfdFile(good + "\r\n"); + expect(result.records).toHaveLength(1); + }); +}); diff --git a/apps/api/test/mfa.spec.ts b/apps/api/test/mfa.spec.ts new file mode 100644 index 0000000..2461967 --- /dev/null +++ b/apps/api/test/mfa.spec.ts @@ -0,0 +1,148 @@ +import { INestApplication } from "@nestjs/common"; +import { generate } from "otplib"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { + agent, + bearer, + closeTestApp, + createTestApp, + loginSuperAdmin, + qaEmail, + type AuthedTokens, +} from "./setup.js"; + +/** + * Protects the MFA/TOTP invariants added in apps/api/src/auth/mfa.service.ts + * and the login-flow branch in auth.service.ts. Boots the real app against + * the real Postgres (same pattern as auth.spec.ts) — codes are generated + * for real via `otplib.generate` against the plaintext secret returned by + * the real enroll endpoint, never faked. + */ +describe("auth — MFA/TOTP (login step-up)", () => { + let app: INestApplication; + let admin: AuthedTokens; + let fixtureEmail: string; + let fixtureUserId: string; + let fixturePassword: string; + let fixtureAccessToken: string; + let secret: string; + + beforeAll(async () => { + app = await createTestApp(); + admin = await loginSuperAdmin(app); + + fixtureEmail = qaEmail("mfa"); + const invite = await agent(app) + .post("/api/v1/users/invite") + .set(...bearer(admin.accessToken)) + .send({ email: fixtureEmail, roleKey: "user", applications: ["core"] }) + .expect(201); + fixtureUserId = invite.body.user.id; + fixturePassword = invite.body.tempPassword; + + const login = await agent(app) + .post("/api/v1/auth/login") + .send({ email: fixtureEmail, password: fixturePassword, applicationKey: "core" }) + .expect(200); + fixtureAccessToken = login.body.accessToken as string; + }); + + afterAll(async () => { + await agent(app) + .patch(`/api/v1/users/${fixtureUserId}`) + .set(...bearer(admin.accessToken)) + .send({ isActive: false }) + .expect(200); + await closeTestApp(app); + }); + + it("login before enrollment returns tokens directly (no mfaRequired)", async () => { + const res = await agent(app) + .post("/api/v1/auth/login") + .send({ email: fixtureEmail, password: fixturePassword, applicationKey: "core" }) + .expect(200); + expect(res.body.accessToken).toBeTypeOf("string"); + expect(res.body.mfaRequired).toBeUndefined(); + }); + + it("confirming enrollment with a wrong code is rejected and does not enable MFA", async () => { + const enroll = await agent(app) + .post("/api/v1/auth/mfa/enroll") + .set(...bearer(fixtureAccessToken)) + .expect(201); + secret = enroll.body.secret as string; + expect(enroll.body.otpauthUrl).toContain("otpauth://totp/"); + + await agent(app) + .post("/api/v1/auth/mfa/confirm") + .set(...bearer(fixtureAccessToken)) + .send({ code: "000000" }) + .expect(400); + + // MFA must still be off — login without a code still returns tokens directly. + const res = await agent(app) + .post("/api/v1/auth/login") + .send({ email: fixtureEmail, password: fixturePassword, applicationKey: "core" }) + .expect(200); + expect(res.body.mfaRequired).toBeUndefined(); + }); + + it("confirming enrollment with the real current code enables MFA and step-up login works end to end", async () => { + await agent(app) + .post("/api/v1/auth/mfa/confirm") + .set(...bearer(fixtureAccessToken)) + .send({ code: await generate({ secret }) }) + .expect(204); + + // Password alone is no longer enough — login returns a challenge, not tokens. + const challenge = await agent(app) + .post("/api/v1/auth/login") + .send({ email: fixtureEmail, password: fixturePassword, applicationKey: "core" }) + .expect(200); + expect(challenge.body.mfaRequired).toBe(true); + expect(challenge.body.accessToken).toBeUndefined(); + const mfaChallenge = challenge.body.mfaChallenge as string; + + // Regression: the challenge token must NEVER work as a bearer access + // token — it's signed with the same secret as a real access token, so + // without an explicit `purpose` check it would grant full API access + // without ever verifying the second factor (caught live against the + // real running stack, not a hypothetical). + await agent(app).get("/api/v1/auth/me").set(...bearer(mfaChallenge)).expect(401); + + // Wrong TOTP code is rejected. + await agent(app) + .post("/api/v1/auth/mfa/challenge") + .send({ mfaChallenge, code: "000000" }) + .expect(401); + + // Correct TOTP code completes login with real tokens. + const done = await agent(app) + .post("/api/v1/auth/mfa/challenge") + .send({ mfaChallenge, code: await generate({ secret }) }) + .expect(200); + expect(done.body.accessToken).toBeTypeOf("string"); + expect(done.body.refreshToken).toBeTypeOf("string"); + }); + + it("disabling MFA requires a valid current code, then login goes back to returning tokens directly", async () => { + await agent(app) + .post("/api/v1/auth/mfa/disable") + .set(...bearer(fixtureAccessToken)) + .send({ code: "000000" }) + .expect(401); + + await agent(app) + .post("/api/v1/auth/mfa/disable") + .set(...bearer(fixtureAccessToken)) + .send({ code: await generate({ secret }) }) + .expect(204); + + const res = await agent(app) + .post("/api/v1/auth/login") + .send({ email: fixtureEmail, password: fixturePassword, applicationKey: "core" }) + .expect(200); + expect(res.body.mfaRequired).toBeUndefined(); + expect(res.body.accessToken).toBeTypeOf("string"); + }); +}); diff --git a/apps/core-web/.env.example b/apps/core-web/.env.example new file mode 100644 index 0000000..0c0928b --- /dev/null +++ b/apps/core-web/.env.example @@ -0,0 +1,4 @@ +# Copiar para `.env` neste diretório (apps/core-web/.env) para desenvolvimento +# local com `pnpm --filter @eden/core-web dev` — Vite só lê .env do root do +# próprio app, não do root do monorepo (ver .env.example na raiz do repo). +VITE_API_BASE_URL=http://localhost:8080/api/v1 diff --git a/apps/core-web/package.json b/apps/core-web/package.json index 15c3096..dd2bdc1 100644 --- a/apps/core-web/package.json +++ b/apps/core-web/package.json @@ -11,8 +11,10 @@ "test": "echo 'no tests yet'" }, "dependencies": { + "@eden/ui": "workspace:*", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.1" }, "devDependencies": { "@types/react": "^18.3.18", diff --git a/apps/core-web/public/eden-logo-horizontal.png b/apps/core-web/public/eden-logo-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..43f6bdd444c8e30ba537bf976baceead7887d831 GIT binary patch literal 454269 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

w5)VY05x>g+gmTHV>1ZOvR4U(MBX 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

GuE_j7(9iy2 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�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 Da!8S@vaKdBrw_&_{u<2=*+G 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

%yM*TR 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%0DgaS;+Py1p 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

G%S@dRYYxT79cHLlL!4l&TQqIN&JX40EJM6 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 Ai2wiq literal 0 HcmV?d00001 diff --git a/apps/core-web/src/App.tsx b/apps/core-web/src/App.tsx index b0e6d97..4e315dd 100644 --- a/apps/core-web/src/App.tsx +++ b/apps/core-web/src/App.tsx @@ -1,12 +1,23 @@ +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { AuthProvider } from "./lib/auth/AuthContext.js"; +import { AuthenticatedLayout } from "./routes/AuthenticatedLayout.js"; +import { DashboardPage } from "./routes/DashboardPage.js"; +import { LoginPage } from "./routes/LoginPage.js"; +import { ProtectedRoute } from "./routes/ProtectedRoute.js"; + export function App() { return ( -

-
-

EDEN Core

-

- Bootstrap da Fase 1 — Design System ainda não portado do tema DreamsERP. -

-
-
+ + + + } /> + }> + }> + } /> + + + + + ); } diff --git a/apps/core-web/src/lib/api/auth.ts b/apps/core-web/src/lib/api/auth.ts new file mode 100644 index 0000000..5423d87 --- /dev/null +++ b/apps/core-web/src/lib/api/auth.ts @@ -0,0 +1,67 @@ +/** + * Client for `apps/api` `/v1/auth/*` (see apps/api/src/auth/auth.controller.ts + * and auth.service.ts for the source of truth — shapes below are mirrored, + * not invented, and were smoke-tested against the real running API). + */ +import { apiRequest } from "./http.js"; + +export interface PublicUser { + id: string; + email: string; + fullName: string | null; + roleKey: string; +} + +export interface AuthResult { + accessToken: string; + expiresIn: number; + refreshToken: string; + user: PublicUser; +} + +export interface MfaRequiredResult { + mfaRequired: true; + mfaChallenge: string; + expiresIn: number; +} + +export type LoginResult = AuthResult | MfaRequiredResult; + +export function isMfaRequired(result: LoginResult): result is MfaRequiredResult { + return "mfaRequired" in result && result.mfaRequired === true; +} + +export type PermissionGrant = { resource: string; action: string; scope: string }; + +export interface MeResult { + user: PublicUser; + permissions: "ALL" | PermissionGrant[]; + applications: Array<"core" | "reseller" | "subscriber">; +} + +const APPLICATION_KEY = "core" as const; + +export function login(email: string, password: string): Promise { + return apiRequest("/auth/login", { + method: "POST", + body: JSON.stringify({ email, password, applicationKey: APPLICATION_KEY }), + }); +} + +export function completeMfaLogin(mfaChallenge: string, code: string): Promise { + return apiRequest("/auth/mfa/challenge", { + method: "POST", + body: JSON.stringify({ mfaChallenge, code }), + }); +} + +export function fetchMe(accessToken: string): Promise { + return apiRequest("/auth/me", { method: "GET" }, { accessToken }); +} + +export async function logout(refreshToken: string): Promise { + await apiRequest("/auth/logout", { + method: "POST", + body: JSON.stringify({ refreshToken }), + }); +} diff --git a/apps/core-web/src/lib/api/http.ts b/apps/core-web/src/lib/api/http.ts new file mode 100644 index 0000000..a6b74f7 --- /dev/null +++ b/apps/core-web/src/lib/api/http.ts @@ -0,0 +1,73 @@ +/** + * Minimal fetch wrapper for the EDEN API. Base URL comes from + * `VITE_API_BASE_URL` (set in apps/core-web/.env — see .env.example), + * falling back to the documented local dev default so `pnpm dev` works + * out of the box against `docker compose up eden-postgres` + a locally + * run `apps/api`. + */ +const DEFAULT_BASE_URL = "http://localhost:8080/api/v1"; + +export const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? DEFAULT_BASE_URL; + +export class ApiError extends Error { + readonly status: number; + + constructor(status: number, message: string) { + super(message); + this.name = "ApiError"; + this.status = status; + } +} + +interface ApiErrorBody { + message?: string | string[]; + error?: string; + statusCode?: number; +} + +function extractErrorMessage(body: unknown, fallback: string): string { + const parsed = body as ApiErrorBody | null; + if (parsed && typeof parsed === "object") { + if (Array.isArray(parsed.message)) return parsed.message.join(" "); + if (typeof parsed.message === "string") return parsed.message; + if (typeof parsed.error === "string") return parsed.error; + } + return fallback; +} + +export interface RequestOptions { + accessToken?: string; +} + +export async function apiRequest( + path: string, + init?: RequestInit, + options?: RequestOptions, +): Promise { + let response: Response; + try { + response = await fetch(`${API_BASE_URL}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + ...(options?.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {}), + ...init?.headers, + }, + }); + } catch { + throw new ApiError(0, "Não foi possível conectar à API. Verifique sua conexão e tente novamente."); + } + + if (response.status === 204) { + return undefined as TResponse; + } + + const text = await response.text(); + const body = text ? JSON.parse(text) : undefined; + + if (!response.ok) { + throw new ApiError(response.status, extractErrorMessage(body, "Ocorreu um erro inesperado.")); + } + + return body as TResponse; +} diff --git a/apps/core-web/src/lib/auth/AuthContext.tsx b/apps/core-web/src/lib/auth/AuthContext.tsx new file mode 100644 index 0000000..fc41fbe --- /dev/null +++ b/apps/core-web/src/lib/auth/AuthContext.tsx @@ -0,0 +1,109 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import type { ReactNode } from "react"; +import { + completeMfaLogin, + fetchMe, + isMfaRequired, + login as apiLogin, + logout as apiLogout, +} from "../api/auth.js"; +import type { PublicUser } from "../api/auth.js"; +import { clearSession, readSession, writeSession } from "./session.js"; + +export type AuthStatus = "loading" | "authenticated" | "anonymous"; + +/** Result of `login()` when the account has MFA enabled — caller must render a code field and call `submitMfa`. */ +export interface MfaPending { + mfaRequired: true; + mfaChallenge: string; +} + +interface AuthContextValue { + status: AuthStatus; + user: PublicUser | null; + accessToken: string | null; + /** Resolves to `{ mfaRequired: true, mfaChallenge }` when a TOTP code is needed next, otherwise logs in fully. */ + login: (email: string, password: string) => Promise; + submitMfa: (mfaChallenge: string, code: string) => Promise; + logout: () => Promise; +} + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [status, setStatus] = useState("loading"); + const [user, setUser] = useState(null); + const [accessToken, setAccessToken] = useState(null); + const [refreshToken, setRefreshToken] = useState(null); + + useEffect(() => { + const stored = readSession(); + if (!stored) { + setStatus("anonymous"); + return; + } + // Validate the persisted token against the API rather than trusting + // localStorage blindly — it may have expired since the last visit. + // No refresh-token rotation in this leva: an expired access token here + // just sends the user back to /login. + fetchMe(stored.accessToken) + .then((me) => { + setUser(me.user); + setAccessToken(stored.accessToken); + setRefreshToken(stored.refreshToken); + setStatus("authenticated"); + }) + .catch(() => { + clearSession(); + setStatus("anonymous"); + }); + }, []); + + const login = useCallback(async (email: string, password: string) => { + const result = await apiLogin(email, password); + if (isMfaRequired(result)) { + return { mfaRequired: true as const, mfaChallenge: result.mfaChallenge }; + } + writeSession({ accessToken: result.accessToken, refreshToken: result.refreshToken, user: result.user }); + setUser(result.user); + setAccessToken(result.accessToken); + setRefreshToken(result.refreshToken); + setStatus("authenticated"); + return { mfaRequired: false as const }; + }, []); + + const submitMfa = useCallback(async (mfaChallenge: string, code: string) => { + const result = await completeMfaLogin(mfaChallenge, code); + writeSession({ accessToken: result.accessToken, refreshToken: result.refreshToken, user: result.user }); + setUser(result.user); + setAccessToken(result.accessToken); + setRefreshToken(result.refreshToken); + setStatus("authenticated"); + }, []); + + const logout = useCallback(async () => { + if (refreshToken) { + // Best-effort — even if the API call fails (e.g. session already + // expired server-side), the local session must still be cleared. + await apiLogout(refreshToken).catch(() => undefined); + } + clearSession(); + setUser(null); + setAccessToken(null); + setRefreshToken(null); + setStatus("anonymous"); + }, [refreshToken]); + + const value = useMemo( + () => ({ status, user, accessToken, login, submitMfa, logout }), + [status, user, accessToken, login, submitMfa, logout], + ); + + return {children}; +} + +export function useAuth(): AuthContextValue { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuth must be used within an AuthProvider"); + return ctx; +} diff --git a/apps/core-web/src/lib/auth/session.ts b/apps/core-web/src/lib/auth/session.ts new file mode 100644 index 0000000..eded868 --- /dev/null +++ b/apps/core-web/src/lib/auth/session.ts @@ -0,0 +1,34 @@ +import type { PublicUser } from "../api/auth.js"; + +const STORAGE_KEY = "eden.core.session"; + +export interface StoredSession { + accessToken: string; + refreshToken: string; + user: PublicUser; +} + +/** + * Basic session persistence (localStorage) so a page refresh doesn't log + * the user out. Deliberately NOT rotating the refresh token automatically + * on expiry in this leva — see apps/core-web/src/lib/auth/AuthContext.tsx. + */ +export function readSession(): StoredSession | null { + try { + const raw = window.localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw) as StoredSession; + if (!parsed.accessToken || !parsed.refreshToken || !parsed.user) return null; + return parsed; + } catch { + return null; + } +} + +export function writeSession(session: StoredSession): void { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(session)); +} + +export function clearSession(): void { + window.localStorage.removeItem(STORAGE_KEY); +} diff --git a/apps/core-web/src/routes/AuthenticatedLayout.tsx b/apps/core-web/src/routes/AuthenticatedLayout.tsx new file mode 100644 index 0000000..c2d385b --- /dev/null +++ b/apps/core-web/src/routes/AuthenticatedLayout.tsx @@ -0,0 +1,35 @@ +import { useNavigate } from "react-router-dom"; +import { Link, Outlet } from "react-router-dom"; +import { AppShell } from "@eden/ui"; +import type { AppShellNavItem } from "@eden/ui"; +import { useAuth } from "../lib/auth/AuthContext.js"; + +const NAV_ITEMS: AppShellNavItem[] = [{ key: "dashboard", label: "Painel", href: "/", active: true }]; + +export function AuthenticatedLayout() { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + + async function handleLogout() { + await logout(); + navigate("/login", { replace: true }); + } + + return ( + ( + + {children} + + )} + > + + + ); +} diff --git a/apps/core-web/src/routes/DashboardPage.tsx b/apps/core-web/src/routes/DashboardPage.tsx new file mode 100644 index 0000000..4e11d76 --- /dev/null +++ b/apps/core-web/src/routes/DashboardPage.tsx @@ -0,0 +1,23 @@ +import { Card, PageHeader } from "@eden/ui"; +import { useAuth } from "../lib/auth/AuthContext.js"; + +/** + * Placeholder landing page — out of scope for this leva to build real + * business screens (Quotes/Contracts/etc.). This just proves the + * authenticated shell renders with real session data end to end. + */ +export function DashboardPage() { + const { user } = useAuth(); + + return ( +
+ + +

+ O shell autenticado está funcionando. As telas de negócio (orçamentos, contratos, etc.) chegam nas + próximas levas. +

+
+
+ ); +} diff --git a/apps/core-web/src/routes/LoginPage.tsx b/apps/core-web/src/routes/LoginPage.tsx new file mode 100644 index 0000000..aa32dca --- /dev/null +++ b/apps/core-web/src/routes/LoginPage.tsx @@ -0,0 +1,132 @@ +import { useState } from "react"; +import type { FormEvent } from "react"; +import { Navigate, useLocation, useNavigate } from "react-router-dom"; +import { Alert, Button, Card, Input } from "@eden/ui"; +import { ApiError } from "../lib/api/http.js"; +import { useAuth } from "../lib/auth/AuthContext.js"; + +export function LoginPage() { + const { status, login, submitMfa } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [mfaChallenge, setMfaChallenge] = useState(null); + const [mfaCode, setMfaCode] = useState(""); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + if (status === "authenticated") { + const redirectTo = (location.state as { from?: string } | null)?.from ?? "/"; + return ; + } + + async function handleCredentialsSubmit(event: FormEvent) { + event.preventDefault(); + setError(null); + setSubmitting(true); + try { + const result = await login(email, password); + if (result.mfaRequired) { + setMfaChallenge(result.mfaChallenge); + } else { + navigate("/", { replace: true }); + } + } catch (err) { + setError(err instanceof ApiError ? err.message : "Não foi possível entrar. Tente novamente."); + } finally { + setSubmitting(false); + } + } + + async function handleMfaSubmit(event: FormEvent) { + event.preventDefault(); + if (!mfaChallenge) return; + setError(null); + setSubmitting(true); + try { + await submitMfa(mfaChallenge, mfaCode); + navigate("/", { replace: true }); + } catch (err) { + setError(err instanceof ApiError ? err.message : "Código inválido. Tente novamente."); + } finally { + setSubmitting(false); + } + } + + return ( +
+ +
+ EDEN +

+ {mfaChallenge ? "Verificação em duas etapas" : "Bem-vindo de volta"} +

+

+ {mfaChallenge ? "Informe o código do seu aplicativo autenticador." : "Entre com sua conta EDEN Core."} +

+
+ + {error ? ( + + {error} + + ) : null} + + {mfaChallenge ? ( +
+ setMfaCode(e.target.value)} + required + autoFocus + /> + + +
+ ) : ( +
+ setEmail(e.target.value)} + required + autoFocus + /> + setPassword(e.target.value)} + required + /> + +
+ )} +
+
+ ); +} diff --git a/apps/core-web/src/routes/ProtectedRoute.tsx b/apps/core-web/src/routes/ProtectedRoute.tsx new file mode 100644 index 0000000..a7e083e --- /dev/null +++ b/apps/core-web/src/routes/ProtectedRoute.tsx @@ -0,0 +1,24 @@ +import { Navigate, Outlet, useLocation } from "react-router-dom"; +import { useAuth } from "../lib/auth/AuthContext.js"; + +export function ProtectedRoute() { + const { status } = useAuth(); + const location = useLocation(); + + if (status === "loading") { + return ( +
+ +
+ ); + } + + if (status === "anonymous") { + return ; + } + + return ; +} diff --git a/apps/core-web/src/vite-env.d.ts b/apps/core-web/src/vite-env.d.ts new file mode 100644 index 0000000..d43868c --- /dev/null +++ b/apps/core-web/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/apps/core-web/tailwind.config.js b/apps/core-web/tailwind.config.js index 93aa364..f474a79 100644 --- a/apps/core-web/tailwind.config.js +++ b/apps/core-web/tailwind.config.js @@ -1,6 +1,16 @@ +import edenPreset from "@eden/ui/tailwind-preset"; + /** @type {import('tailwindcss').Config} */ export default { - content: ["./index.html", "./src/**/*.{ts,tsx}"], + presets: [edenPreset], + content: [ + "./index.html", + "./src/**/*.{ts,tsx}", + // Design-system components live in packages/ui and ship as source (not + // pre-built CSS) — Tailwind's JIT scanner needs to see them too, or + // classes only used inside @eden/ui get purged from core-web's build. + "../../packages/ui/src/**/*.{ts,tsx}", + ], theme: { extend: {}, }, diff --git a/compose.yaml b/compose.yaml index d2048b7..92d6637 100644 --- a/compose.yaml +++ b/compose.yaml @@ -50,6 +50,7 @@ services: EDEN_FIELD_ENCRYPTION_KEY: ${EDEN_FIELD_ENCRYPTION_KEY} EDEN_SUPERADMIN_EMAIL: ${EDEN_SUPERADMIN_EMAIL} EDEN_SUPERADMIN_PASSWORD: ${EDEN_SUPERADMIN_PASSWORD} + EDEN_CORS_ORIGINS: ${EDEN_CORS_ORIGINS} SMTP_HOST: ${SMTP_HOST} SMTP_PORT: ${SMTP_PORT} SMTP_USER: ${SMTP_USER} @@ -95,6 +96,12 @@ services: dockerfile: infra/docker/Dockerfile.web args: APP_NAME: core-web + # Sempre relativo aqui (não ${VITE_API_BASE_URL} do .env — esse é + # para `pnpm dev` local fora do Docker, valor diferente e não deve + # vazar para este build arg). A API nunca tem domínio público + # próprio; o proxy externo roteia este path para eden-api na rede + # interna — ver comentário em .env.example. + VITE_API_BASE_URL: /api/v1 container_name: eden-core restart: unless-stopped ports: @@ -115,6 +122,12 @@ services: dockerfile: infra/docker/Dockerfile.web args: APP_NAME: reseller-web + # Sempre relativo aqui (não ${VITE_API_BASE_URL} do .env — esse é + # para `pnpm dev` local fora do Docker, valor diferente e não deve + # vazar para este build arg). A API nunca tem domínio público + # próprio; o proxy externo roteia este path para eden-api na rede + # interna — ver comentário em .env.example. + VITE_API_BASE_URL: /api/v1 container_name: eden-parceiros restart: unless-stopped ports: @@ -135,6 +148,12 @@ services: dockerfile: infra/docker/Dockerfile.web args: APP_NAME: subscriber-web + # Sempre relativo aqui (não ${VITE_API_BASE_URL} do .env — esse é + # para `pnpm dev` local fora do Docker, valor diferente e não deve + # vazar para este build arg). A API nunca tem domínio público + # próprio; o proxy externo roteia este path para eden-api na rede + # interna — ver comentário em .env.example. + VITE_API_BASE_URL: /api/v1 container_name: eden-assinante restart: unless-stopped ports: diff --git a/docs/progress.md b/docs/progress.md index b366bd7..65f259a 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -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. diff --git a/infra/docker/Dockerfile.web b/infra/docker/Dockerfile.web index f20ec5d..0147c17 100644 --- a/infra/docker/Dockerfile.web +++ b/infra/docker/Dockerfile.web @@ -8,6 +8,15 @@ WORKDIR /repo COPY . . RUN pnpm install --frozen-lockfile ARG APP_NAME +# Vite bakes VITE_-prefixed vars into the bundle at build time (no runtime +# env in a static nginx-served SPA) — see docs/adr/0002 + compose.yaml's +# build.args for each of the 3 frontends. Default is same-origin relative +# path: the API is never exposed on its own public domain (operator +# decision), so in production the reverse proxy in front of each public +# frontend domain must forward this path (e.g. /api/*) to eden-api on the +# internal network — see the comment on EDEN_CORS_ORIGINS in .env.example. +ARG VITE_API_BASE_URL=/api/v1 +ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} RUN pnpm turbo run build --filter=@eden/${APP_NAME}... FROM nginx:1.27-alpine AS runtime diff --git a/packages/config/src/env-schema.ts b/packages/config/src/env-schema.ts index f1e84d5..685aa14 100644 --- a/packages/config/src/env-schema.ts +++ b/packages/config/src/env-schema.ts @@ -36,6 +36,19 @@ export const envSchema = z.object({ S3_BUCKET: z.string().optional(), PUBLIC_BASE_URL: z.string().optional(), + + /** + * Comma-separated list of allowed browser origins for CORS. The API has + * no public domain of its own (operator decision) — production traffic + * reaches it same-origin through each frontend's reverse proxy, so this + * only matters for local dev, where the Vite dev server (localhost:3001+) + * and the API (localhost:8080) are different origins. Optional — when + * unset, CORS stays effectively disabled (no browser origin is allowed + * cross-origin). Never a wildcard: the API issues bearer tokens, and a + * wildcard origin combined with credentialed requests is a known + * CSRF-adjacent misconfiguration. + */ + EDEN_CORS_ORIGINS: z.string().optional(), }); export type EdenEnv = z.infer; diff --git a/packages/database/migrations/1788450538883_cross-reference-fks.cjs b/packages/database/migrations/1788450538883_cross-reference-fks.cjs new file mode 100644 index 0000000..9f467a7 --- /dev/null +++ b/packages/database/migrations/1788450538883_cross-reference-fks.cjs @@ -0,0 +1,55 @@ +/** + * Adds the cross-reference foreign keys between Quotes, Client + * Registrations and Contracts that were deliberately left as plain + * nullable UUIDs when each table was created in parallel (see the header + * comments in 1788445280742_quotes.cjs, 1788447739229_client-registrations.cjs + * and 1788447802216_contracts.cjs). All three tables now exist on `main`, + * so this closes that gap. + * + * ON DELETE RESTRICT everywhere: none of these aggregates has a DELETE + * route today (quotes/client_registrations/contracts are only ever + * status-transitioned, never removed), and a business document silently + * losing its link to another business document on an unrelated delete + * would be a worse failure mode than the delete itself being blocked. + */ + +exports.up = (pgm) => { + pgm.addConstraint("quotes", "fk_quotes_client_registration", { + foreignKeys: { + columns: "client_registration_id", + references: "client_registrations(id)", + onDelete: "RESTRICT", + }, + }); + + pgm.addConstraint("client_registrations", "fk_client_registrations_quote", { + foreignKeys: { + columns: "quote_id", + references: "quotes(id)", + onDelete: "RESTRICT", + }, + }); + + pgm.addConstraint("contracts", "fk_contracts_quote", { + foreignKeys: { + columns: "quote_id", + references: "quotes(id)", + onDelete: "RESTRICT", + }, + }); + + pgm.addConstraint("contracts", "fk_contracts_client_registration", { + foreignKeys: { + columns: "client_registration_id", + references: "client_registrations(id)", + onDelete: "RESTRICT", + }, + }); +}; + +exports.down = (pgm) => { + pgm.dropConstraint("contracts", "fk_contracts_client_registration"); + pgm.dropConstraint("contracts", "fk_contracts_quote"); + pgm.dropConstraint("client_registrations", "fk_client_registrations_quote"); + pgm.dropConstraint("quotes", "fk_quotes_client_registration"); +}; diff --git a/packages/database/migrations/1788451164762_inventory.cjs b/packages/database/migrations/1788451164762_inventory.cjs new file mode 100644 index 0000000..602190a --- /dev/null +++ b/packages/database/migrations/1788451164762_inventory.cjs @@ -0,0 +1,264 @@ +/** + * Inventory/Warehouse/Assets bounded context — Master Prompt §6.8, ADR-0007. + * Greenfield: the legacy OrçaFácil has no real stock module, only the + * requisições cited in the Master Prompt, so there is no eden.md section + * to port faithfully here — this schema is designed from the Master + * Prompt §6.8 requirements directly. + * + * Master Prompt §6.8 lists 15 possible entities. This migration builds + * the 5 that form one coherent, end-to-end-testable slice (reserve → + * select serial → install/comodato → return), mirroring the + * 4-of-11-tables MVP approach taken by the Contracts module + * (docs/adr/0006, see 1788447802216_contracts.cjs's header comment): + * + * - warehouses (this migration) + * - stock_movements (this migration) + * - serialized_assets (this migration) + * - asset_assignments (this migration) + * - stock_reservations (this migration) + * + * Explicitly DEFERRED, not forgotten — each needs a module/workflow that + * hasn't landed yet: + * - warehouse_locations — bin/shelf-level location inside a warehouse; + * needs a UX for put-away/picking before a bare table is useful. + * - stock_items — a per-(warehouse,product) balance cache row; + * deliberately NOT built — this MVP's invariant is that on-hand + * balance is ALWAYS derived from stock_movements (SUM by sign), never + * a stored/editable column (see stock_movements' comment). A + * materialized cache can be added later purely as a performance + * optimization, refreshed FROM the ledger, never the other way round. + * - stock_lots — batch/lot tracking (expiry, supplier batch); + * no product in the current catalog needs it yet. + * - inventory_counts — physical count / cycle-count workflow with + * its own reconciliation UX; a count's variance would itself need to + * post `adjustment_in`/`adjustment_out` movements once built. + * - transfers — a formal multi-step inter-warehouse transfer + * (request → in-transit → receive) with its own approval workflow; + * `transfer_out`/`transfer_in` movement types are already modeled in + * the ledger vocabulary below so this can be layered on top later + * without a ledger migration. + * - receipts / issues — formal goods-receipt-note / issue-note + * documents (e.g. tied to a purchase order); `purchase_in` already + * covers the ledger side for this MVP without the PO workflow. + * - returns — a formal customer-return workflow distinct + * from the simple `/assets/:id/return` endpoint this MVP ships + * (which only closes the assignment and posts `return_in`); needed + * later for e.g. partial/quantity-based non-serialized returns. + * - rma — formal vendor RMA case tracking (tracking + * number, vendor, resolution); `rma_out` is already modeled in the + * ledger vocabulary below so the case-tracking table can be added + * later without a ledger migration. + * - asset_maintenance — service/repair history for a serialized + * asset; needs its own workflow design, not just a bare log table. + * + * No FK to `contracts` yet: `serialized_assets.current_contract_id` and + * `asset_assignments.contract_id` are deliberately nullable/plain UUID + * with NO REFERENCES clause — same adopted pattern as Quotes/Contracts/ + * ClientRegistrations' cross-module FKs while modules land in parallel + * worktrees. Wiring those FKs is integration work for the merge step. + * + * ON DELETE: no table in this migration ever CASCADEs a delete from + * warehouses/products/serialized_assets — an asset or warehouse with + * movement/assignment history is never hard-deleted, only inactivated + * (warehouses.is_active) or status-transitioned (serialized_assets.status + * = 'written_off'). This is a deliberate invariant per the Inventory + * agent's mandate, not an oversight. + */ + +exports.shorthands = undefined; + +exports.up = (pgm) => { + pgm.sql(` + -- ===================================================================== + -- warehouses — armazém/depósito físico, pertence a uma legal entity + -- ===================================================================== + CREATE TABLE warehouses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + created_by UUID REFERENCES users(id), + company_id UUID NOT NULL REFERENCES companies(id), + name TEXT NOT NULL, + code TEXT NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT true, + UNIQUE (code) + ); + COMMENT ON TABLE warehouses IS + 'Armazém/depósito físico — Master Prompt §6.8. Pertence a uma company (legal entity, companies.id). code é único globalmente (não só por company) para servir como identificador humano curto e inequívoco em toda a UI/relatórios. Nunca deletado fisicamente — um armazém desativado é is_active=false, preservando o histórico de stock_movements/serialized_assets que apontam para ele (ver comentário de topo da migration).'; + + CREATE TRIGGER trg_warehouses_updated_at BEFORE UPDATE ON warehouses + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + + CREATE INDEX idx_warehouses_company ON warehouses(company_id); + + -- ===================================================================== + -- stock_movements — ledger append-only. Saldo de estoque é SEMPRE + -- derivado daqui (SUM com sinal implícito pelo tipo), nunca uma coluna + -- de saldo editável em nenhuma outra tabela. + -- ===================================================================== + CREATE TABLE stock_movements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + created_by UUID REFERENCES users(id), + warehouse_id UUID NOT NULL REFERENCES warehouses(id), + product_id UUID NOT NULL REFERENCES products(id), + movement_type TEXT NOT NULL CHECK (movement_type IN ( + 'purchase_in', 'adjustment_in', 'adjustment_out', + 'transfer_out', 'transfer_in', + 'reservation', 'reservation_release', + 'sale_out', 'install_out', 'comodato_out', + 'return_in', 'rma_out', 'write_off' + )), + quantity NUMERIC(14,3) NOT NULL CHECK (quantity > 0), + reference_type TEXT, + reference_id UUID, + notes TEXT + ); + COMMENT ON TABLE stock_movements IS + 'Ledger append-only de estoque — Master Prompt §6.8. quantity é SEMPRE positivo; o sinal/direção é implícito pelo movement_type (ver COMMENT de movement_type). Uma correção de um movimento passado é sempre um NOVO movimento de estorno (ex.: um purchase_in lançado errado é corrigido com um adjustment_out equivalente), nunca um UPDATE/DELETE da linha original — daí os triggers trg_stock_movements_no_update/no_delete abaixo, mesmo padrão de audit_log (ver baseline-schema). reference_type/reference_id são polimórficos e opcionais (sem FK, por natureza) — ex.: reference_type=''contract''/reference_id= num install_out, ou reference_type=''serialized_asset''/reference_id= numa reservation de um ativo específico.'; + COMMENT ON COLUMN stock_movements.movement_type IS + 'Direção implícita por tipo (usada para calcular o saldo on-hand de um warehouse+product via SUM): + ENTRADA (+ saldo on-hand): purchase_in, adjustment_in, transfer_in, return_in + SAÍDA (- saldo on-hand): adjustment_out, transfer_out, sale_out, install_out, comodato_out, rma_out, write_off + NEUTRO para saldo on-hand, afeta apenas "disponível para prometer" + (on_hand - reservas ativas), não o saldo físico: reservation, reservation_release + Saldo on-hand de um (warehouse_id, product_id) = SUM(quantity) das entradas − SUM(quantity) das saídas, sempre calculado em tempo de consulta a partir desta tabela — nunca cacheado numa coluna editável (ver header da migration, item stock_items deferido).'; + + CREATE OR REPLACE FUNCTION prevent_stock_movement_mutation() RETURNS TRIGGER AS $$ + BEGIN + IF current_setting('eden.allow_stock_movement_mutation', true) IS DISTINCT FROM 'true' THEN + RAISE EXCEPTION 'stock_movements é append-only — correções são novos movimentos de estorno, nunca UPDATE/DELETE (ver Master Prompt §6.8 / .claude/agents/eden-inventory.md)'; + END IF; + IF TG_OP = 'DELETE' THEN + RETURN OLD; + END IF; + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER trg_stock_movements_no_update BEFORE UPDATE ON stock_movements + FOR EACH ROW EXECUTE FUNCTION prevent_stock_movement_mutation(); + CREATE TRIGGER trg_stock_movements_no_delete BEFORE DELETE ON stock_movements + FOR EACH ROW EXECUTE FUNCTION prevent_stock_movement_mutation(); + + CREATE INDEX idx_stock_movements_warehouse_product ON stock_movements(warehouse_id, product_id); + CREATE INDEX idx_stock_movements_reference ON stock_movements(reference_type, reference_id); + CREATE INDEX idx_stock_movements_created_at ON stock_movements(created_at); + + -- ===================================================================== + -- serialized_assets — um ativo físico rastreado individualmente + -- ===================================================================== + CREATE TABLE serialized_assets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + 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), + product_id UUID NOT NULL REFERENCES products(id), + serial TEXT, + patrimony_tag TEXT, + mac_address TEXT, + warehouse_id UUID REFERENCES warehouses(id), + status TEXT NOT NULL DEFAULT 'in_stock' CHECK (status IN ( + 'in_stock', 'reserved', 'installed', 'comodato', 'returned', 'written_off' + )), + current_contract_id UUID, + warranty_expires_at TIMESTAMPTZ, + CONSTRAINT chk_serialized_assets_has_identifier CHECK (serial IS NOT NULL OR patrimony_tag IS NOT NULL) + ); + COMMENT ON TABLE serialized_assets IS + 'Ativo físico rastreado individualmente (serial/patrimônio/MAC) — Master Prompt §6.8. Ao menos um identificador (serial OU patrimony_tag) é obrigatório; produtos que só rastreiam patrimônio (ex.: mobiliário) podem não ter serial de fabricante. warehouse_id é a localização atual e é NULL quando o ativo está instalado/comodato no cliente (status IN (''installed'',''comodato'')) — current_contract_id aponta pra onde ele está nesse caso. current_contract_id NÃO tem FK para contracts ainda (contracts está sendo construído em paralelo por outro agente neste worktree) — mesmo padrão de FK cruzada adiada de Quotes/Contracts/ClientRegistrations; a FK real é trabalho de integração no merge. Nunca deletado fisicamente: um ativo dado de baixa vira status=''written_off'' (ver header da migration).'; + COMMENT ON COLUMN serialized_assets.mac_address IS + 'MVP: uma única coluna de MAC primário (nullable), não um array nem uma tabela filha. Decisão documentada: o índice único parcial que garante "MAC nunca duplicado entre ativos simultaneamente ativos" (ver abaixo) precisa colocalizar mac_address com status na MESMA tabela — um índice parcial não pode referenciar uma condição de uma tabela filha via JOIN. Se um produto real precisar rastrear múltiplos MACs (ex.: CPE dual-band com MAC de LAN e de WLAN), promover para uma tabela filha então, com a invariante reforçada por trigger em vez de índice parcial — deferido conscientemente, não esquecido.'; + COMMENT ON COLUMN serialized_assets.status IS + 'in_stock/reserved/installed/comodato contam como "ativo" para a invariante de não-duplicação de identificador (ver índices únicos parciais abaixo). returned/written_off NÃO contam — liberam o serial/patrimônio/MAC para reuso por uma linha nova, ex.: uma correção de cadastro. Nota: o endpoint POST /assets/:id/return desta MVP leva o ativo direto de volta a ''in_stock'' (mesma linha) — o status ''returned'' fica reservado para um fluxo futuro de RMA/inspeção pós-devolução (ainda não implementado, ver header da migration) em que o ativo chega mas ainda não foi conferido/restocado.'; + COMMENT ON COLUMN serialized_assets.current_contract_id IS + 'Sem FK para contracts.id ainda — deliberado, ver COMMENT da tabela. Preenchido em /assets/:id/install, limpo em /assets/:id/return.'; + + CREATE TRIGGER trg_serialized_assets_updated_at BEFORE UPDATE ON serialized_assets + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + + -- Invariante obrigatória (Master Prompt §15.3 / §6.8: "nunca reutilizar + -- o mesmo serial/MAC/patrimônio simultaneamente em dois ativos + -- ativos") — reforçada em DB, não só em validação de aplicação. + CREATE UNIQUE INDEX idx_serialized_assets_serial_active ON serialized_assets(serial) + WHERE serial IS NOT NULL AND status IN ('in_stock', 'reserved', 'installed', 'comodato'); + CREATE UNIQUE INDEX idx_serialized_assets_patrimony_active ON serialized_assets(patrimony_tag) + WHERE patrimony_tag IS NOT NULL AND status IN ('in_stock', 'reserved', 'installed', 'comodato'); + CREATE UNIQUE INDEX idx_serialized_assets_mac_active ON serialized_assets(mac_address) + WHERE mac_address IS NOT NULL AND status IN ('in_stock', 'reserved', 'installed', 'comodato'); + + CREATE INDEX idx_serialized_assets_product ON serialized_assets(product_id); + CREATE INDEX idx_serialized_assets_warehouse ON serialized_assets(warehouse_id); + CREATE INDEX idx_serialized_assets_status ON serialized_assets(status); + CREATE INDEX idx_serialized_assets_contract ON serialized_assets(current_contract_id); + + -- ===================================================================== + -- asset_assignments — histórico de vínculo de um serialized_asset a um + -- contrato/cliente ao longo do tempo + -- ===================================================================== + CREATE TABLE asset_assignments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + created_by UUID REFERENCES users(id), + asset_id UUID NOT NULL REFERENCES serialized_assets(id), + contract_id UUID NOT NULL, + assignment_type TEXT NOT NULL CHECK (assignment_type IN ('installed', 'comodato')), + assigned_at TIMESTAMPTZ NOT NULL DEFAULT now(), + unassigned_at TIMESTAMPTZ + ); + COMMENT ON TABLE asset_assignments IS + 'Histórico de vínculo de um ativo serializado a um contrato/cliente — Master Prompt §6.8. contract_id sem FK ainda (mesmo motivo/padrão de serialized_assets.current_contract_id). Nunca UPDATE de um assignment fechado (unassigned_at já preenchido): encerrar um vínculo é preencher unassigned_at UMA vez (camada de repositório só permite esse UPDATE específico, guardado pelo índice único parcial abaixo que impede reabrir/duplicar um vínculo aberto); um novo vínculo é sempre uma linha nova.'; + + -- Reforça em DB que um ativo nunca tem mais de um vínculo ABERTO + -- simultâneo (unassigned_at IS NULL) — mesma invariante de + -- "não alocado duas vezes" do Master Prompt §15.3, vista pelo lado do + -- histórico de vínculos em vez do status do ativo. + CREATE UNIQUE INDEX idx_asset_assignments_open ON asset_assignments(asset_id) + WHERE unassigned_at IS NULL; + + CREATE INDEX idx_asset_assignments_asset ON asset_assignments(asset_id); + CREATE INDEX idx_asset_assignments_contract ON asset_assignments(contract_id); + + -- ===================================================================== + -- stock_reservations — reserva de quantidade de um produto num + -- warehouse, com ou sem um serial específico já selecionado + -- ===================================================================== + CREATE TABLE stock_reservations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + created_by UUID REFERENCES users(id), + warehouse_id UUID NOT NULL REFERENCES warehouses(id), + product_id UUID NOT NULL REFERENCES products(id), + quantity NUMERIC(14,3) NOT NULL CHECK (quantity > 0), + reference_type TEXT, + reference_id UUID, + status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'released', 'fulfilled')), + notes TEXT + ); + COMMENT ON TABLE stock_reservations IS + 'Reserva de quantidade de um produto num warehouse, ANTES (ou no lugar) da seleção de um serial específico — Master Prompt §6.8, primeiro passo do fluxo reservar→selecionar serial→instalar. Duas formas de uso, ambas suportadas por esta mesma tabela (sem coluna asset_id — decisão documentada): + 1. Reserva GENÉRICA (produto+quantidade, ainda sem serial escolhido): reference_type/reference_id apontam, por exemplo, para a quote_id ou contract_id que originou a necessidade. Caso descrito no Master Prompt como o primeiro passo do fluxo. + 2. Reserva de um ATIVO ESPECÍFICO (POST /assets/:id/reserve): quantity=1, reference_type=''serialized_asset'', reference_id= — rastreável ao ativo exato sem precisar de uma coluna dedicada. + status=''fulfilled'' quando a reserva efetivamente vira uma saída (ex.: o ativo reservado é instalado); ''released'' quando cancelada sem uso. Sem FK em reference_id (polimórfico).'; + + CREATE TRIGGER trg_stock_reservations_updated_at BEFORE UPDATE ON stock_reservations + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + + CREATE INDEX idx_stock_reservations_warehouse_product ON stock_reservations(warehouse_id, product_id); + CREATE INDEX idx_stock_reservations_status ON stock_reservations(status); + CREATE INDEX idx_stock_reservations_reference ON stock_reservations(reference_type, reference_id); + `); +}; + +exports.down = (pgm) => { + pgm.sql(` + DROP TABLE IF EXISTS stock_reservations; + DROP TABLE IF EXISTS asset_assignments; + DROP TABLE IF EXISTS serialized_assets; + DROP TABLE IF EXISTS stock_movements; + DROP FUNCTION IF EXISTS prevent_stock_movement_mutation(); + DROP TABLE IF EXISTS warehouses; + `); +}; diff --git a/packages/database/migrations/1788451374900_support.cjs b/packages/database/migrations/1788451374900_support.cjs new file mode 100644 index 0000000..e424a37 --- /dev/null +++ b/packages/database/migrations/1788451374900_support.cjs @@ -0,0 +1,164 @@ +/** + * Support/Service Desk bounded context (Master Prompt §6.13). Greenfield — + * no legacy OrçaFácil module to port, unlike most other EDEN modules. + * + * This MVP builds the 4 tables needed for a working ticket lifecycle with + * real SLA timers: + * - support_tickets (this migration) + * - ticket_comments (this migration) + * - ticket_status_history (this migration) + * - sla_timers (this migration, 1:1 with support_tickets) + * + * Explicitly DEFERRED, not forgotten: + * - Real attachments (S3) — ticket_comments/support_tickets have no + * attachment table yet; needs the Documents/S3 integration adapter + * (packages/integrations) to land first, same reasoning as + * contract_documents in the Contracts migration. + * - CSAT (customer satisfaction survey after closing) — needs a survey + * delivery mechanism (email/portal) that doesn't exist yet. + * - Dedicated work order (OS) entity distinct from the ticket itself — + * this MVP treats the ticket as also carrying the OS-equivalent + * fields (root_cause/resolution_notes); a first-class `work_orders` + * table with its own scheduling/dispatch workflow is future work. + * - Queue model with automatic routing rules — there is no `queues` + * table yet; tickets are assigned via owner_user_id only (manual). + * - Configurable attendance calendar (business hours/holidays) for SLA + * due-date math — `sla_timers` due dates are computed with naive + * wall-clock offsets (see apps/api/src/support/sla.ts), not a + * calendar-aware engine. Category-specific SLA policy is also + * deferred — this MVP uses one flat default policy for every ticket + * regardless of category/priority (see sla.ts comment). + * + * `support_tickets.client_registration_id` and `.contract_id` are + * deliberately nullable with NO foreign key, same adopted pattern as + * `contracts.client_registration_id`/`contracts.quote_id`: those modules + * are being built in parallel by other agents. Wiring the FKs is + * integration-step work, not this migration's. + */ + +exports.shorthands = undefined; + +exports.up = (pgm) => { + pgm.sql(` + -- ===================================================================== + -- support_tickets — the core aggregate + -- ===================================================================== + CREATE SEQUENCE support_tickets_protocol_number_seq START 1; + + CREATE TABLE support_tickets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + 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), + protocol_number TEXT NOT NULL UNIQUE + DEFAULT ('TCK-' || to_char(now(), 'YYYY') || '-' || lpad(nextval('support_tickets_protocol_number_seq')::text, 6, '0')), + title TEXT NOT NULL, + description TEXT NOT NULL, + category TEXT NOT NULL, + subcategory TEXT, + priority TEXT NOT NULL DEFAULT 'medium' + CHECK (priority IN ('low', 'medium', 'high', 'critical')), + status TEXT NOT NULL DEFAULT 'new' + CHECK (status IN ( + 'new', 'triage', 'in_progress', 'waiting_customer', + 'waiting_third_party', 'resolved', 'closed', 'cancelled' + )), + reseller_id UUID REFERENCES resellers(id), + client_registration_id UUID, + contract_id UUID, + owner_user_id UUID REFERENCES users(id), + opened_by_user_id UUID NOT NULL REFERENCES users(id), + root_cause TEXT, + resolution_notes TEXT, + first_response_at TIMESTAMPTZ, + resolved_at TIMESTAMPTZ, + closed_at TIMESTAMPTZ + ); + COMMENT ON TABLE support_tickets IS + 'Ticket de atendimento (Service Desk, Master Prompt §6.13) — módulo greenfield, sem equivalente no legado OrçaFácil. protocol_number é gerado inline via support_tickets_protocol_number_seq + to_char, mesmo padrão de quotes.quote_number (ver migration 1788445280742_quotes.cjs), formato TCK-{ano}-{sequência de 6 dígitos}. client_registration_id/contract_id são nullable SEM FK — client_registrations e contracts existem em outras migrations mas a FK cruzada é trabalho de integração, não desta migration (mesmo padrão adotado por contracts.client_registration_id/contracts.quote_id).'; + COMMENT ON COLUMN support_tickets.root_cause IS + 'Causa raiz — só tem sentido preenchido no fechamento do ticket (transição para ''closed''), mas não há CHECK de banco forçando isso: a camada de aplicação (support.service.ts) só aceita esse campo no payload da transição para ''closed''.'; + COMMENT ON COLUMN support_tickets.first_response_at IS + 'Quando a primeira resposta pública (não-interna) ao ticket ocorreu. Setado uma única vez (nunca sobrescrito) pela camada de aplicação ao adicionar o primeiro ticket_comments.is_internal=false — ver apps/api/src/support/support.service.ts e sla_timers.first_response_met.'; + COMMENT ON COLUMN support_tickets.resolved_at IS + 'Quando o status se tornou ''resolved'' pela primeira vez — nunca sobrescrito depois (semântica COALESCE, mesmo padrão de contracts.activated_at).'; + COMMENT ON COLUMN support_tickets.closed_at IS + 'Quando o status se tornou ''closed'' pela primeira vez — nunca sobrescrito depois (semântica COALESCE, mesmo padrão de contracts.activated_at).'; + + CREATE TRIGGER trg_support_tickets_updated_at BEFORE UPDATE ON support_tickets + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + + CREATE INDEX idx_support_tickets_status ON support_tickets(status); + CREATE INDEX idx_support_tickets_reseller ON support_tickets(reseller_id); + CREATE INDEX idx_support_tickets_owner ON support_tickets(owner_user_id); + CREATE INDEX idx_support_tickets_client_registration ON support_tickets(client_registration_id); + CREATE INDEX idx_support_tickets_contract ON support_tickets(contract_id); + + ALTER SEQUENCE support_tickets_protocol_number_seq OWNED BY support_tickets.protocol_number; + + -- ===================================================================== + -- ticket_comments — public/internal comment thread + -- ===================================================================== + CREATE TABLE ticket_comments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + ticket_id UUID NOT NULL REFERENCES support_tickets(id) ON DELETE CASCADE, + author_user_id UUID NOT NULL REFERENCES users(id), + body TEXT NOT NULL, + is_internal BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + COMMENT ON TABLE ticket_comments IS + 'Thread de comentários do ticket. is_internal=true nunca pode ser exposto ao portal do assinante ou da revenda — reforçado na camada de query/serviço (apps/api/src/support/support.service.ts), não apenas por convenção de UI, mesma disciplina de isolamento cross-tenant do módulo comercial (Master Prompt, eden-support persona).'; + CREATE INDEX idx_ticket_comments_ticket ON ticket_comments(ticket_id); + + -- ===================================================================== + -- ticket_status_history — append-only UX-facing lifecycle log, + -- distinct from (and complementary to) the generic audit_log + -- ===================================================================== + CREATE TABLE ticket_status_history ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + ticket_id UUID NOT NULL REFERENCES support_tickets(id) ON DELETE CASCADE, + from_status TEXT, + to_status TEXT NOT NULL, + reason TEXT, + changed_by UUID REFERENCES users(id), + changed_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + COMMENT ON TABLE ticket_status_history IS + 'Log de transições de status do ticket — append-only por convenção da CAMADA DE APLICAÇÃO (nenhuma rota de UPDATE/DELETE é exposta), mesmo padrão e mesma justificativa de coexistência com audit_log de contract_status_history (ver migration 1788447802216_contracts.cjs).'; + CREATE INDEX idx_ticket_status_history_ticket ON ticket_status_history(ticket_id); + + -- ===================================================================== + -- sla_timers — 1:1 with support_tickets (ticket_id is both PK and FK) + -- ===================================================================== + CREATE TABLE sla_timers ( + ticket_id UUID PRIMARY KEY REFERENCES support_tickets(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + first_response_due_at TIMESTAMPTZ NOT NULL, + resolution_due_at TIMESTAMPTZ NOT NULL, + first_response_met BOOLEAN, + resolution_met BOOLEAN, + paused_since TIMESTAMPTZ, + accumulated_pause_seconds INTEGER NOT NULL DEFAULT 0 + ); + COMMENT ON TABLE sla_timers IS + 'Timer de SLA, uma linha por ticket (ticket_id é PK e FK ao mesmo tempo — força o 1:1). Regra de pausa: ao entrar em waiting_customer/waiting_third_party, paused_since = now(); ao voltar para in_progress, accumulated_pause_seconds soma o intervalo pausado e paused_since volta a NULL — implementado atomicamente em packages/database/src/repositories/support-tickets.ts::updateTicketStatus (mesmo padrão SELECT...FOR UPDATE + withTransaction de contracts.ts::updateContractStatus). O cálculo de violação de SLA (subtraindo o tempo pausado, incluindo a pausa corrente se ainda pausado) é feito pelas funções puras computeElapsedSeconds/isSlaBreached em apps/api/src/support/sla.ts — nunca reimplementado ad-hoc. first_response_met/resolution_met só são preenchidos no momento em que o evento correspondente (primeira resposta pública / entrada em resolved) de fato ocorre — permanecem NULL até lá, não são recalculados retroativamente.'; + COMMENT ON COLUMN sla_timers.paused_since IS + 'Quando não-nulo, o timer está pausado desde este instante — todo o tempo entre paused_since e now() ainda NÃO está refletido em accumulated_pause_seconds e deve ser somado no cálculo de "tempo decorrido" (ver computeElapsedSeconds em sla.ts).'; + + CREATE TRIGGER trg_sla_timers_updated_at BEFORE UPDATE ON sla_timers + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + `); +}; + +exports.down = (pgm) => { + pgm.sql(` + DROP TABLE IF EXISTS sla_timers; + DROP TABLE IF EXISTS ticket_status_history; + DROP TABLE IF EXISTS ticket_comments; + DROP TABLE IF EXISTS support_tickets; + DROP SEQUENCE IF EXISTS support_tickets_protocol_number_seq; + `); +}; diff --git a/packages/database/migrations/1788452136875_hr-timeclock.cjs b/packages/database/migrations/1788452136875_hr-timeclock.cjs new file mode 100644 index 0000000..1ae2ca1 --- /dev/null +++ b/packages/database/migrations/1788452136875_hr-timeclock.cjs @@ -0,0 +1,245 @@ +/** + * HR/Timeclock (Ponto Eletrônico) — eden.md módulo 6, foundation leva. + * Master Prompt §6.14. Ported faithfully from the legacy OrçaFácil + * (`eden.md` módulo 6 §1 for the data model, §2 for the Control iD + * integration shape, §4 for the AFD format) — this is dense legal/ + * compliance logic already validated in production, not redesigned here. + * + * SCOPE OF THIS LEVA (deliberately narrow — see the task brief): + * - timeclock_devices — Control iD REP iDClass device registry + * (data model only; no live sync yet) + * - timeclock_employees — separate cadastro from `users` (see note + * below on why) + * - afd_import_batches — one row per AFD file import (manual upload) + * - afd_records — one row per AFD file LINE, IMMUTABLE + * + * Explicitly DEFERRED to a future leva, not forgotten (eden.md §5-§10): + * - timeclock_employee_devices — per-device sync/biometry status link + * table (eden.md §1.3); needs the live-sync workflow to be meaningful. + * - timeclock_work_schedules / timeclock_work_schedule_days (§1.6-1.7) + * - timeclock_holidays (§1.8) + * - timeclock_punch_adjustments (§1.9) — administrative treatments + * - timeclock_daily_calculations (§1.10) — apuração engine output + * - timeclock_time_bank_entries (§1.11) — banco de horas ledger + * - timeclock_period_closures (§1.12) — fechamento de período + * - time_audit_logs (§1.13) — this leva uses the shared `audit_log` + * hash-chain (via auditRepo.appendAuditEvent) instead; the module- + * specific structured log is deferred with the workflows that need it + * (approvals, reopenings) to render nicely. + * + * ── employees vs. users (explicit decision, per task brief) ───────────── + * eden.md §1.2 is unambiguous: `timeclock_employees` is a cadastro + * SEPARATE from `users` (with an optional `user_id` link), because (a) a + * punch-clock employee often has no ERP login at all, and (b) the + * Control iD device identifies a person BY CPF (eden.md §1.2's "Importante + * — identidade no relógio": "no modo 671 da API Control iD, o CPF é o + * próprio identificador do usuário no equipamento"), and `users` has no + * CPF column. Reusing `users` would mean inventing a CPF column on a + * table owned by Identity/Auth, and forcing every punch-clock employee to + * have an ERP login they may not need. So: `timeclock_employees` it is, + * ported faithfully. + * + * ── naming deviation from eden.md, per task brief ──────────────────────── + * The task brief names the import-batch table `afd_import_batches` + * (this migration) rather than eden.md's `afd_imports`. Kept as briefed. + * Also simplified vs. eden.md's richer version: no S3 storage of the + * original file bytes (the file is never persisted outside afd_records' + * raw_line reconstruction — see afd-imports.controller.ts (CreateAfdImportDto) for why: this + * leva's import endpoint accepts the file as a JSON string, not a + * multipart upload, so there's no original file artifact worth archiving + * separately from the parsed lines themselves; a real multipart upload + + * S3 archival, matching eden.md's `file_s3_key`, is future work), and no + * `source` distinction (`manual_upload` only — `live_sync` is deferred + * along with the Control iD live-communication layer, eden.md §2-3). + * + * ── crc_valid / validation_status as TEXT enums, not a plain boolean ──── + * The task brief's own summary suggested a boolean `crc_valid`, but + * eden.md §1.5/§4.2 documents a 4-value enum (VALID/INVALID/ + * NOT_APPLICABLE/NOT_VALIDATED) because "no CRC field for this record + * type" (types 1/5/6/9) and "CRC field present but not checked" are + * different, useful facts from a real CRC mismatch (type 7's own hash + * chain) — collapsing that into a boolean would lose information the + * legacy explicitly modeled. Followed eden.md here per this task's own + * instruction to prefer the full read over the summary when they diverge. + */ + +exports.shorthands = undefined; + +exports.up = (pgm) => { + pgm.sql(` + -- ===================================================================== + -- timeclock_devices — Control iD REP iDClass equipment registry + -- ===================================================================== + CREATE TABLE timeclock_devices ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + 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), + company_id UUID, + name TEXT NOT NULL, + description TEXT, + branch TEXT, + host TEXT NOT NULL, + port INTEGER NOT NULL DEFAULT 443 CHECK (port > 0 AND port <= 65535), + use_https BOOLEAN NOT NULL DEFAULT true, + username TEXT NOT NULL, + password_encrypted TEXT NOT NULL, + serial_number TEXT, + model TEXT, + firmware_version TEXT, + timezone TEXT NOT NULL DEFAULT 'America/Sao_Paulo', + status TEXT NOT NULL DEFAULT 'UNKNOWN' CHECK (status IN ('UNKNOWN', 'ONLINE', 'OFFLINE', 'ERROR')), + last_seen_at TIMESTAMPTZ, + last_sync_at TIMESTAMPTZ, + last_error TEXT, + last_afd_nsr BIGINT, + active BOOLEAN NOT NULL DEFAULT true + ); + COMMENT ON TABLE timeclock_devices IS + 'Relógios de ponto Control iD REP iDClass — eden.md módulo 6 §1.1/§2. company_id nullable = equipamento "sem empresa" (compartilhado). DELIBERADAMENTE sem FK para companies: essa tabela (Organization bounded context) não existe neste worktree isolado no momento em que esta migration foi escrita — mesma situação de contracts.cjs (contracts.quote_id/client_registration_id) neste monorepo. A FK é trabalho de integração/merge, não desta migration.'; + COMMENT ON COLUMN timeclock_devices.password_encrypted IS + 'AES-256-GCM reversível via FieldEncryptionService (apps/api/src/crypto/field-encryption.service.ts, formato v1:iv:tag:ciphertext) — NUNCA hash, porque a aplicação precisa autenticar no equipamento sob demanda a cada operação (eden.md §2.2/§2.3). NUNCA retornada em texto puro por nenhuma rota GET — a API expõe apenas um flag hasCredential.'; + COMMENT ON COLUMN timeclock_devices.timezone IS + 'Metadado informativo do cadastro do equipamento. A interpretação de negócio das datas/horas do AFD SEMPRE usa o offset literal embutido no próprio campo de data/hora do arquivo (fixo -03:00 na prática, América/São_Paulo sem horário de verão desde 2019) — esta coluna não é consultada pelo parser (afd-parser.ts), que nunca assume o timezone do servidor nem de nenhuma configuração.'; + COMMENT ON COLUMN timeclock_devices.last_afd_nsr IS + 'Cursor de sincronização incremental do AFD (eden.md §3) — reservado para a leva de sync ao vivo; não é escrito por esta leva (só import manual de arquivo completo).'; + + CREATE TRIGGER trg_timeclock_devices_updated_at BEFORE UPDATE ON timeclock_devices + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + CREATE INDEX idx_timeclock_devices_company ON timeclock_devices(company_id); + + -- ===================================================================== + -- timeclock_employees — separate from 'users' (see migration header) + -- ===================================================================== + CREATE TABLE timeclock_employees ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + 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), + company_id UUID, + user_id UUID REFERENCES users(id), + full_name TEXT NOT NULL, + cpf TEXT NOT NULL, + registration TEXT, + active BOOLEAN NOT NULL DEFAULT true, + UNIQUE (company_id, cpf) + ); + COMMENT ON TABLE timeclock_employees IS + 'Cadastro de funcionários do módulo de ponto, SEPARADO de users (eden.md §1.2) — vínculo opcional via user_id. CPF (11 dígitos, sem máscara) é o identificador do usuário no equipamento Control iD no modo 671 — não existe um "controlid_user_id" separado. CPF único por empresa (company_id, cpf), não globalmente. work_schedule_id (jornada vinculada) é DEFERRADO para a leva de apuração — não existe ainda tabela timeclock_work_schedules.'; + COMMENT ON COLUMN timeclock_employees.cpf IS + '11 dígitos, sem máscara. Validação de dígito verificador é responsabilidade da camada de aplicação (mesma convenção de contract_parties.document/companies.cnpj neste monorepo) — sem CHECK de formato no banco nesta leva (nenhuma rota de CRUD de employees foi implementada ainda; a coluna existe para afd_records.employee_id ter o que referenciar e para a resolução de CPF do parser).'; + + CREATE TRIGGER trg_timeclock_employees_updated_at BEFORE UPDATE ON timeclock_employees + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + CREATE INDEX idx_timeclock_employees_company ON timeclock_employees(company_id); + CREATE INDEX idx_timeclock_employees_user ON timeclock_employees(user_id); + + -- ===================================================================== + -- afd_import_batches — one row per AFD file import (manual upload only + -- this leva; live_sync source is deferred with the Control iD live + -- communication layer, eden.md §2-3) + -- ===================================================================== + CREATE TABLE afd_import_batches ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + device_id UUID NOT NULL REFERENCES timeclock_devices(id), + company_id UUID, + source TEXT NOT NULL DEFAULT 'manual_upload' CHECK (source = 'manual_upload'), + file_hash TEXT NOT NULL UNIQUE, + file_size INTEGER NOT NULL, + record_count INTEGER NOT NULL DEFAULT 0, + inserted_count INTEGER NOT NULL DEFAULT 0, + valid_count INTEGER NOT NULL DEFAULT 0, + warning_count INTEGER NOT NULL DEFAULT 0, + invalid_count INTEGER NOT NULL DEFAULT 0, + unsupported_count INTEGER NOT NULL DEFAULT 0, + parse_failure_count INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL CHECK (status IN ('VALID', 'WARNING', 'INVALID')), + first_nsr BIGINT, + last_nsr BIGINT, + summary JSONB NOT NULL DEFAULT '{}'::jsonb, + imported_by UUID REFERENCES users(id), + imported_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + COMMENT ON TABLE afd_import_batches IS + 'Metadados de cada importação de arquivo AFD (eden.md §1.4, nomeada afd_import_batches por instrução desta leva em vez de afd_imports do legado). file_hash é a idempotência de arquivo inteiro (SHA-256) — UNIQUE garante que reimportar o mesmo conteúdo retorna already_imported em vez de duplicar. Simplificação desta leva: nenhum S3/armazenamento do arquivo original bruto (o transporte é um JSON string, não multipart — ver afd-imports.controller.ts (CreateAfdImportDto)); o dado legal é preservado integralmente em afd_records.raw_line, uma linha por registro.'; + COMMENT ON COLUMN afd_import_batches.file_hash IS + 'SHA-256 do CONTEÚDO recebido pela API (a string JSON do body, decodificada como texto) — não necessariamente idêntico byte-a-byte ao SHA-256 de um arquivo .afd bruto em disco, já que o transporte desta leva é string JSON, não upload multipart de bytes crus. Suficiente para o propósito de idempotência desta leva; ver nota de simplificação no cabeçalho da migration.'; + COMMENT ON COLUMN afd_import_batches.record_count IS + 'Total de linhas parseadas do arquivo (inclui INVALID/UNSUPPORTED_LAYOUT — nunca descartadas). inserted_count pode ser menor que record_count quando ON CONFLICT (device_id, nsr) DO NOTHING pulou registros já existentes de uma importação anterior (idempotência por registro, eden.md §1.5).'; + + CREATE INDEX idx_afd_import_batches_device ON afd_import_batches(device_id); + + -- ===================================================================== + -- afd_records — one row per AFD file LINE. IMMUTABLE: no application + -- route may ever UPDATE/DELETE this table, at any privilege level, + -- not even super_admin (eden.md módulo 6 §12) — enforced here again at + -- the DB level via trigger, same pattern as audit_log's + -- prevent_audit_log_mutation, but WITHOUT that trigger's escape-hatch + -- GUC (audit_log allows a controlled internal bypass; afd_records + -- deliberately does not, per this leva's explicit non-negotiable + -- instruction: "em nenhum nível de privilégio, nem para super_admin"). + -- ===================================================================== + CREATE TABLE afd_records ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + afd_import_batch_id UUID NOT NULL REFERENCES afd_import_batches(id), + device_id UUID NOT NULL REFERENCES timeclock_devices(id), + company_id UUID, + nsr BIGINT, + record_type TEXT NOT NULL, + event_datetime TIMESTAMPTZ, + event_datetime_raw TEXT, + employee_cpf_raw TEXT, + employee_id UUID REFERENCES timeclock_employees(id), + raw_line TEXT NOT NULL, + crc_received TEXT, + crc_calculated TEXT, + crc_valid TEXT NOT NULL DEFAULT 'NOT_APPLICABLE' CHECK (crc_valid IN ('VALID', 'INVALID', 'NOT_APPLICABLE', 'NOT_VALIDATED')), + parsed_data JSONB NOT NULL DEFAULT '{}'::jsonb, + validation_status TEXT NOT NULL CHECK (validation_status IN ('VALID', 'WARNING', 'INVALID', 'UNSUPPORTED_LAYOUT')), + validation_notes TEXT, + UNIQUE (device_id, nsr) + ); + COMMENT ON TABLE afd_records IS + 'Dado bruto de ponto, eden.md módulo 6 §1.5/§12 — IMUTÁVEL por design. Nenhuma rota de UPDATE/DELETE existe na camada de aplicação (apps/api/src/hr-timeclock/afd-records.controller.ts expõe apenas GET) e o banco reforça isso via trigger (trg_afd_records_no_update/no_delete abaixo) SEM nenhuma válvula de escape — nem para super_admin, diferente de audit_log. Preservado mesmo quando inválido: nsr é nullable de propósito (NSR ausente/não-numérico nunca vira 0/autoincrement), e todo registro tem raw_line completa, mesmo os classificados INVALID/UNSUPPORTED_LAYOUT.'; + COMMENT ON COLUMN afd_records.nsr IS + 'Número Sequencial de Registro. NULLABLE DE PROPÓSITO — NSR ausente/não-numérico é inválido mas precisa ser preservado, nunca substituído por 0/autoincrement (eden.md §12). UNIQUE (device_id, nsr): múltiplas linhas com nsr NULL do mesmo device são permitidas (semântica padrão de NULL em índice único do Postgres) — a idempotência por registro só se aplica a NSRs realmente numéricos.'; + COMMENT ON COLUMN afd_records.crc_valid IS + 'VALID/INVALID = CRC-16/KERMIT verificado (tipos 2/3/4). NOT_APPLICABLE = tipo 7, que usa cadeia de hash própria NUNCA recalculada pelo EDEN. NOT_VALIDATED = tipos 1/5/6/9, sem campo de CRC. Enum de 4 valores, não boolean — ver nota no cabeçalho da migration sobre por que.'; + COMMENT ON COLUMN afd_records.parsed_data IS + 'Campos decodificados específicos do tipo de registro, com posição EXATA documentada em eden.md §4.1 — qualquer trecho sem posição documentada vai para a chave "raw_tail" dentro deste JSONB, nunca é inventado/adivinhado (ver apps/api/src/hr-timeclock/afd-parser.ts).'; + COMMENT ON COLUMN afd_records.employee_cpf_raw IS + 'CPF exatamente como veio no campo do AFD (12 caracteres de largura documentada para tipos 3/7, 11 para o cpf_resp do tipo 4) — sempre string, nunca number, nunca perde zero à esquerda. employee_id é resolvido a partir daqui via normalizeAfdCpf (últimos 11 dígitos numéricos), escopado por company_id quando o device tem uma; NULL quando órfão (sem match) — nunca um match é inventado/forçado.'; + + CREATE INDEX idx_afd_records_employee ON afd_records(employee_id); + CREATE INDEX idx_afd_records_company ON afd_records(company_id); + CREATE INDEX idx_afd_records_type ON afd_records(record_type); + CREATE INDEX idx_afd_records_event_datetime ON afd_records(event_datetime); + CREATE INDEX idx_afd_records_batch ON afd_records(afd_import_batch_id); + + CREATE OR REPLACE FUNCTION prevent_afd_records_mutation() RETURNS TRIGGER AS $$ + BEGIN + RAISE EXCEPTION 'afd_records é imutável (dado bruto de ponto, eden.md módulo 6 §12) — UPDATE/DELETE bloqueados, sem exceção, mesmo para super_admin'; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER trg_afd_records_no_update BEFORE UPDATE ON afd_records + FOR EACH ROW EXECUTE FUNCTION prevent_afd_records_mutation(); + CREATE TRIGGER trg_afd_records_no_delete BEFORE DELETE ON afd_records + FOR EACH ROW EXECUTE FUNCTION prevent_afd_records_mutation(); + `); +}; + +exports.down = (pgm) => { + pgm.sql(` + DROP TRIGGER IF EXISTS trg_afd_records_no_delete ON afd_records; + DROP TRIGGER IF EXISTS trg_afd_records_no_update ON afd_records; + DROP FUNCTION IF EXISTS prevent_afd_records_mutation(); + DROP TABLE IF EXISTS afd_records; + DROP TABLE IF EXISTS afd_import_batches; + DROP TABLE IF EXISTS timeclock_employees; + DROP TABLE IF EXISTS timeclock_devices; + `); +}; diff --git a/packages/database/migrations/1788457368744_fiscal.cjs b/packages/database/migrations/1788457368744_fiscal.cjs new file mode 100644 index 0000000..50940f2 --- /dev/null +++ b/packages/database/migrations/1788457368744_fiscal.cjs @@ -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; + `); +}; diff --git a/packages/database/migrations/1788457629203_billing.cjs b/packages/database/migrations/1788457629203_billing.cjs new file mode 100644 index 0000000..236be36 --- /dev/null +++ b/packages/database/migrations/1788457629203_billing.cjs @@ -0,0 +1,185 @@ +/** + * Billing bounded context (recurring billing engine) — Master Prompt + * §6.10, docs/adr/0008-billing-finance-fiscal-separation.md. ADR-0008 is + * explicit: Billing (this module) calculates *what is owed* and produces + * an internal invoice; it is strictly separate from Finance/AR-AP + * (collecting/paying — títulos, boletos, dunning, conciliação) and from + * Fiscal (documento fiscal). Neither of those is built here. + * + * ADR-0008 lists nine Billing tables (billing_accounts, billing_cycles, + * subscriptions/services, charge_components, usage_charges, invoices, + * invoice_items, invoice_adjustments, billing_runs). This migration + * builds the coherent MVP subset needed for a working, testable + * subscription -> billing_run -> invoice flow: + * + * - billing_subscriptions (this migration) + * - billing_runs (this migration) + * - invoices (this migration) + * - invoice_items (this migration) + * + * Explicitly DEFERRED, not forgotten: + * - billing_accounts — this MVP hangs subscriptions/invoices directly + * off `reseller_id`; a billing_account (grouping multiple subscriptions + * under one consolidated bill) is deferred until there's a real need + * to bill more than one subscription together. + * - billing_cycles as its own table — this MVP derives the cycle from + * the (billing_day, reference_year, reference_month) triple on the + * run/subscription rather than a calendar table of cycle instances. + * - charge_components / usage_charges — consumption/telephony billing; + * needs the usage-rating module (out of scope), so every subscription + * in this MVP bills a single flat `monthly_value` with no metered + * component. + * - invoice_adjustments / credit_debit_adjustments — the sanctioned way + * to correct a *consolidated* invoice (never a silent edit); deferred, + * so today a consolidated billing_run's invoices are simply immutable + * from the application layer (see billing.ts's assertRunIsMutable). + * - billing_run_logs — this MVP uses the shared `audit_log` + * (auditRepo.appendAuditEvent) for every billing_run/invoice mutation + * instead of a dedicated structured log table. + * - proration — every subscription always bills the full monthly_value; + * no day-proportional calculation for a subscription that started or + * was cancelled mid-cycle. `invoice_items.item_type` already reserves + * the 'proration' vocabulary value for when this lands, so it won't + * need a migration of its own. + * + * Deferred cross-context foreign keys (no REFERENCES, plain UUID, same + * pattern used by Quotes/Contracts/ClientRegistrations for tables owned by + * a module built in a different worktree — see contracts' migration + * header for precedent): + * - billing_subscriptions.contract_id — `contracts` (Commercial/§6.6) is + * not guaranteed to exist in every worktree this batch might land on. + * - billing_subscriptions.reseller_id / invoices.reseller_id — + * `resellers` is not present in THIS worktree's migration set either + * (bootstrap-only checkout). Both FKs are integration work for + * whoever merges this alongside the resellers/contracts migrations. + */ + +exports.shorthands = undefined; + +exports.up = (pgm) => { + pgm.sql(` + -- ===================================================================== + -- billing_subscriptions — a recurring billing subscription derived + -- from an active contract + -- ===================================================================== + CREATE TABLE billing_subscriptions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + 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), + contract_id UUID NOT NULL, + reseller_id UUID, + monthly_value NUMERIC(14,2) NOT NULL CHECK (monthly_value >= 0), + billing_day INTEGER NOT NULL CHECK (billing_day BETWEEN 1 AND 28), + status TEXT NOT NULL DEFAULT 'active' + CHECK (status IN ('active', 'suspended', 'cancelled')), + started_at TIMESTAMPTZ NOT NULL DEFAULT now(), + cancelled_at TIMESTAMPTZ + ); + COMMENT ON TABLE billing_subscriptions IS + 'Assinatura de cobrança recorrente derivada de um contrato ativo (Master Prompt §6.10). contract_id e reseller_id são UUID simples sem REFERENCES — mesmo padrão de FK cruzada adiada de Quotes/Contracts/ClientRegistrations: contracts/resellers não existem garantidamente neste worktree (checkout de bootstrap). A FK real é trabalho de integração central, não desta migration.'; + COMMENT ON COLUMN billing_subscriptions.monthly_value IS + 'Snapshot do valor mensal no momento da criação da subscription — NUNCA recalculado automaticamente se o contrato de origem mudar depois (isso seria uma reversão silenciosa de fatura, proibida pelo Master Prompt §24 invariante nº4). Uma mudança de valor exige cancelar e recriar a subscription (ou, quando existir, um endpoint explícito de reajuste) — nesta leva não há reajuste, só criação/cancelamento.'; + COMMENT ON COLUMN billing_subscriptions.billing_day IS + 'Dia do mês do ciclo de cobrança, limitado a 1-28 deliberadamente para nunca cair em um mês sem esse dia (evita a ambiguidade de dia 29-31 em fevereiro/meses de 30 dias) — não há regra de "último dia do mês" nesta leva.'; + COMMENT ON COLUMN billing_subscriptions.cancelled_at IS + 'Setado uma única vez, quando status transiciona para cancelled (camada de repositório) — nunca sobrescrito depois.'; + CREATE INDEX idx_billing_subscriptions_reseller ON billing_subscriptions(reseller_id); + CREATE INDEX idx_billing_subscriptions_status_day ON billing_subscriptions(status, billing_day); + CREATE INDEX idx_billing_subscriptions_contract ON billing_subscriptions(contract_id); + + CREATE TRIGGER trg_billing_subscriptions_updated_at BEFORE UPDATE ON billing_subscriptions + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + + -- ===================================================================== + -- billing_runs — one billing-close execution for a + -- (reference_year, reference_month, billing_day) period + -- ===================================================================== + CREATE TABLE billing_runs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + reference_year INTEGER NOT NULL CHECK (reference_year BETWEEN 2000 AND 2100), + reference_month INTEGER NOT NULL CHECK (reference_month BETWEEN 1 AND 12), + billing_day INTEGER NOT NULL CHECK (billing_day BETWEEN 1 AND 28), + status TEXT NOT NULL DEFAULT 'draft' + CHECK (status IN ('draft', 'consolidated', 'failed')), + consolidated_at TIMESTAMPTZ, + triggered_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + COMMENT ON TABLE billing_runs IS + 'Execução de fechamento de billing para um período + billing_day (Master Prompt §6.10). draft é reexecutável: rodar de novo com os mesmos (reference_year, reference_month, billing_day) sobre um run ainda draft SUBSTITUI (delete+reinsert, mesma transação) as invoices geradas em vez de duplicar — implementado em packages/database/src/repositories/billing.ts, generateOrRegenerateBillingRun(). consolidated é terminal e imutável pela aplicação (ver invoices.status e assertRunIsMutable). failed é reservado para quando esta leva ganhar um passo que possa falhar de forma parcial (ex. rating de consumo) — nada nesta leva efetivamente produz failed.'; + COMMENT ON COLUMN billing_runs.triggered_by IS + 'Ator (usuário) que disparou a criação/regeneração deste run — atualizado a cada regeneração em draft para refletir quem rodou por último.'; + CREATE INDEX idx_billing_runs_period ON billing_runs(reference_year, reference_month, billing_day); + + -- Idempotência: no máximo um run 'draft' por (ano, mês, billing_day) — + -- índice único parcial em vez de UNIQUE(reference_year, reference_month, + -- billing_day, status) puro, porque este último permitiria dois runs + -- 'consolidated' distintos para o mesmo período sem impedir a corrida + -- de dois 'draft' concorrentes ao mesmo tempo (o problema real que essa + -- invariante precisa resolver). A regra "não criar novo run para um + -- período já consolidado" (refaturamento controlado é deferido) é + -- reforçada na camada de aplicação, não aqui, porque múltiplos runs + -- 'consolidated' históricos para o mesmo período não são logicamente + -- impossíveis no schema (só indesejados pelo fluxo atual). + CREATE UNIQUE INDEX uq_billing_runs_one_draft_per_period + ON billing_runs(reference_year, reference_month, billing_day) + WHERE status = 'draft'; + + -- ===================================================================== + -- invoices — internal invoice generated by a billing_run for one + -- billing_subscription + -- ===================================================================== + CREATE TABLE invoices ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + billing_run_id UUID NOT NULL REFERENCES billing_runs(id) ON DELETE CASCADE, + billing_subscription_id UUID NOT NULL REFERENCES billing_subscriptions(id), + reseller_id UUID, + amount NUMERIC(14,2) NOT NULL CHECK (amount >= 0), + reference_year INTEGER NOT NULL, + reference_month INTEGER NOT NULL CHECK (reference_month BETWEEN 1 AND 12), + status TEXT NOT NULL DEFAULT 'draft' + CHECK (status IN ('draft', 'consolidated', 'cancelled')), + due_date DATE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (billing_run_id, billing_subscription_id) + ); + COMMENT ON TABLE invoices IS + 'Fatura interna (Billing) gerada por um billing_run — DISTINTA de um título de AR/AP (Finance) e de um documento fiscal (Fiscal), ADR-0008. status espelha o status do run no momento da geração (draft -> pode ser apagada/regerada enquanto o run ainda é draft; consolidated -> imutável pela aplicação, ver invariante em billing.ts). UNIQUE(billing_run_id, billing_subscription_id) é uma segunda barreira de idempotência a nível de banco: o mesmo run nunca pode gerar duas invoices para a mesma subscription.'; + COMMENT ON COLUMN invoices.reseller_id IS + 'Denormalizado a partir de billing_subscriptions.reseller_id no momento da geração — evita join obrigatório para filtrar por revenda em listagens/relatórios; nunca é a fonte da verdade (a subscription é). Sem REFERENCES pelo mesmo motivo de billing_subscriptions.reseller_id (ver cabeçalho da migration).'; + COMMENT ON COLUMN invoices.due_date IS + 'Nesta leva, due_date = dia billing_day do mês de referência (reference_year/reference_month) — regra de vencimento mais sofisticada (ex. carência após o fechamento) é responsabilidade do módulo Finance/AR-AP (ADR-0008), fora de escopo aqui.'; + CREATE INDEX idx_invoices_run ON invoices(billing_run_id); + CREATE INDEX idx_invoices_subscription ON invoices(billing_subscription_id); + CREATE INDEX idx_invoices_reseller_status ON invoices(reseller_id, status); + CREATE INDEX idx_invoices_period ON invoices(reference_year, reference_month); + + -- ===================================================================== + -- invoice_items — invoice line items (generic vocabulary; this MVP + -- only ever generates 'recurring' automatically) + -- ===================================================================== + CREATE TABLE invoice_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE, + description TEXT NOT NULL, + amount NUMERIC(14,2) NOT NULL, + item_type TEXT NOT NULL + CHECK (item_type IN ('recurring', 'proration', 'implementation', 'adjustment')), + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + COMMENT ON TABLE invoice_items IS + 'Linhas da fatura, modeladas de forma genérica para não exigir migration nova quando proration/implementation/adjustment passarem a ser gerados automaticamente — nesta leva, a camada de aplicação só cria itens item_type=recurring (um por invoice, espelhando billing_subscriptions.monthly_value no momento da geração do run).'; + CREATE INDEX idx_invoice_items_invoice ON invoice_items(invoice_id); + `); +}; + +exports.down = (pgm) => { + pgm.sql(` + DROP TABLE IF EXISTS invoice_items; + DROP TABLE IF EXISTS invoices; + DROP TABLE IF EXISTS billing_runs; + DROP TABLE IF EXISTS billing_subscriptions; + `); +}; diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index 4a0c3a9..5d19387 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -10,6 +10,11 @@ export * as quotesRepo from "./repositories/quotes.js"; export * as contractsRepo from "./repositories/contracts.js"; export * as clientRegistrationsRepo from "./repositories/client-registrations.js"; export * as clientRegistrationPartnersRepo from "./repositories/client-registration-partners.js"; +export * as inventoryRepo from "./repositories/inventory.js"; +export * as supportTicketsRepo from "./repositories/support-tickets.js"; +export * as hrTimeclockRepo from "./repositories/hr-timeclock.js"; +export * as fiscalRepo from "./repositories/fiscal.js"; +export * as billingRepo from "./repositories/billing.js"; export { listLeads, getLead, @@ -74,3 +79,87 @@ export type { CreatePartnerInput, UpdatePartnerInput, } from "./repositories/client-registration-partners.js"; +export type { + Warehouse, + CreateWarehouseInput, + UpdateWarehouseInput, + StockMovement, + StockMovementType, + ListStockMovementsFilter, + CreateStockMovementInput, + SerializedAsset, + SerializedAssetStatus, + ListSerializedAssetsFilter, + CreateAssetInput, + ReserveAssetInput, + InstallAssetInput, + ReturnAssetInput, + AssetAssignment, + StockReservation, + StockReservationStatus, + ListStockReservationsFilter, + CreateGenericReservationInput, +} from "./repositories/inventory.js"; +export type { + TicketStatus, + TicketPriority, + SupportTicket, + TicketComment, + TicketStatusHistoryEntry, + SlaTimer, + TicketWithDetails, + ListTicketsFilter, + CreateTicketInput, + CreateTicketCommentInput, + UpdateTicketStatusExtra, +} from "./repositories/support-tickets.js"; +export type { + TimeclockDeviceStatus, + TimeclockDevice, + CreateTimeclockDeviceInput, + UpdateTimeclockDeviceInput, + TimeclockEmployee, + AfdImportBatchStatus, + AfdCrcValidity, + AfdValidationStatus, + AfdImportBatch, + CreateAfdImportBatchInput, + AfdRecord, + AfdRecordInsertInput, + ImportAfdBatchInput, + ImportAfdBatchResult, + ListAfdRecordsFilter, +} from "./repositories/hr-timeclock.js"; +export type { + FiscalNcm, + CreateSimpleCatalogInput, + UpdateSimpleCatalogInput, + ListCatalogFilter, + FiscalCfop, + CreateCfopInput, + UpdateCfopInput, + FiscalMunicipality, + CreateMunicipalityInput, + UpdateMunicipalityInput, + FiscalOperationNature, + FiscalBillingComponent, + FiscalDocumentType, + ProductFiscalProfile, + CreateProductFiscalProfileInput, +} from "./repositories/fiscal.js"; +export type { + BillingSubscriptionStatus, + BillingSubscription, + BillingRunStatus, + BillingRun, + InvoiceStatus, + Invoice, + InvoiceItemType, + InvoiceItem, + BillingRunWithInvoices, + CreateBillingSubscriptionInput, + ListBillingSubscriptionsFilter, + GenerateBillingRunInput, + ListBillingRunsFilter, + ListInvoicesFilter, +} from "./repositories/billing.js"; diff --git a/packages/database/src/repositories/billing.ts b/packages/database/src/repositories/billing.ts new file mode 100644 index 0000000..c863f4d --- /dev/null +++ b/packages/database/src/repositories/billing.ts @@ -0,0 +1,471 @@ +import { query, withTransaction } from "../pool.js"; + +/** + * Billing bounded context (Master Prompt §6.10, ADR-0008) — recurring + * subscription -> billing_run -> invoice flow. See PENDING_billing.cjs's + * header comment for the full list of tables this MVP builds vs. defers + * (billing_accounts, billing_cycles, charge_components/usage_charges, + * invoice_adjustments, billing_run_logs, proration — all deferred). + * + * Mirrors contracts.ts's conventions: snake_case rows, withTransaction for + * multi-row writes that must commit/rollback together, typed domain + * errors mapped to HTTP status by the service layer (never here). + */ + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type BillingSubscriptionStatus = "active" | "suspended" | "cancelled"; + +export interface BillingSubscription { + id: string; + created_at: Date; + updated_at: Date; + created_by: string | null; + updated_by: string | null; + contract_id: string; + reseller_id: string | null; + monthly_value: string; + billing_day: number; + status: BillingSubscriptionStatus; + started_at: Date; + cancelled_at: Date | null; +} + +export type BillingRunStatus = "draft" | "consolidated" | "failed"; + +export interface BillingRun { + id: string; + reference_year: number; + reference_month: number; + billing_day: number; + status: BillingRunStatus; + consolidated_at: Date | null; + triggered_by: string | null; + created_at: Date; +} + +export type InvoiceStatus = "draft" | "consolidated" | "cancelled"; + +export interface Invoice { + id: string; + billing_run_id: string; + billing_subscription_id: string; + reseller_id: string | null; + amount: string; + reference_year: number; + reference_month: number; + status: InvoiceStatus; + due_date: string | null; + created_at: Date; +} + +export type InvoiceItemType = "recurring" | "proration" | "implementation" | "adjustment"; + +export interface InvoiceItem { + id: string; + invoice_id: string; + description: string; + amount: string; + item_type: InvoiceItemType; + created_at: Date; +} + +export interface BillingRunWithInvoices extends BillingRun { + invoices: Invoice[]; + totalAmount: string; +} + +// --------------------------------------------------------------------------- +// Errors — mapped to HTTP status by the service layer, never here +// --------------------------------------------------------------------------- + +export class BillingSubscriptionNotFoundError extends Error { + constructor(public readonly subscriptionId: string) { + super(`Billing subscription ${subscriptionId} not found`); + this.name = "BillingSubscriptionNotFoundError"; + } +} + +export class BillingRunNotFoundError extends Error { + constructor(public readonly billingRunId: string) { + super(`Billing run ${billingRunId} not found`); + this.name = "BillingRunNotFoundError"; + } +} + +/** + * Thrown when an operation tries to write (regenerate invoices, alter + * items) against a billing_run that is already 'consolidated' — the + * central invariant of this module. The service layer maps this to 409. + */ +export class BillingRunConsolidatedError extends Error { + constructor(public readonly billingRunId: string) { + super(`Billing run ${billingRunId} is already consolidated and cannot be modified`); + this.name = "BillingRunConsolidatedError"; + } +} + +/** + * Thrown when creating/regenerating a run for a (year, month, billing_day) + * that already has a CONSOLIDATED run. Controlled re-billing (a new run + * deliberately superseding a consolidated one) is explicitly deferred + * (see migration header) — until it exists, this is a hard stop rather + * than a silent double-bill. + */ +export class BillingPeriodAlreadyConsolidatedError extends Error { + constructor( + public readonly referenceYear: number, + public readonly referenceMonth: number, + public readonly billingDay: number, + ) { + super( + `Billing run for ${referenceYear}-${referenceMonth}/day ${billingDay} is already consolidated`, + ); + this.name = "BillingPeriodAlreadyConsolidatedError"; + } +} + +// --------------------------------------------------------------------------- +// billing_subscriptions +// --------------------------------------------------------------------------- + +export interface CreateBillingSubscriptionInput { + contractId: string; + resellerId: string | null; + monthlyValue: number; + billingDay: number; + createdBy: string | null; +} + +export async function createBillingSubscription( + input: CreateBillingSubscriptionInput, +): Promise { + const { rows } = await query( + `INSERT INTO billing_subscriptions + (contract_id, reseller_id, monthly_value, billing_day, created_by, updated_by) + VALUES ($1, $2, $3, $4, $5, $5) + RETURNING *`, + [input.contractId, input.resellerId, input.monthlyValue, input.billingDay, input.createdBy], + ); + const row = rows[0]; + if (!row) throw new Error("failed to create billing subscription"); + return row; +} + +export async function getBillingSubscriptionById(id: string): Promise { + const { rows } = await query( + "SELECT * FROM billing_subscriptions WHERE id = $1", + [id], + ); + return rows[0] ?? null; +} + +export interface ListBillingSubscriptionsFilter { + scope: "all" | "reseller"; + /** Required (and enforced by the caller) when scope === 'reseller'. */ + resellerId?: string | null; + status?: BillingSubscriptionStatus; +} + +export async function listBillingSubscriptions( + filter: ListBillingSubscriptionsFilter, +): Promise { + const conditions: string[] = []; + const params: unknown[] = []; + + if (filter.scope === "reseller") { + // Deliberately never falls back to unfiltered: an actor with + // reseller-only scope and no resellerId sees nothing, not everything. + params.push(filter.resellerId ?? null); + conditions.push(`reseller_id = $${params.length}`); + } + if (filter.status) { + params.push(filter.status); + conditions.push(`status = $${params.length}`); + } + + const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; + const { rows } = await query( + `SELECT * FROM billing_subscriptions ${where} ORDER BY created_at DESC`, + params, + ); + return rows; +} + +/** + * Cancels a subscription — idempotent: cancelling an already-cancelled + * subscription is a no-op that returns the existing row rather than + * erroring, mirroring the "safe to retry" philosophy applied to billing + * runs elsewhere in this module. cancelled_at is set only the first time. + */ +export async function cancelBillingSubscription( + id: string, + actorId: string | null, +): Promise { + const { rows } = await query( + `UPDATE billing_subscriptions SET + status = 'cancelled', + cancelled_at = COALESCE(cancelled_at, now()), + updated_by = $2 + WHERE id = $1 + RETURNING *`, + [id, actorId], + ); + const row = rows[0]; + if (!row) throw new BillingSubscriptionNotFoundError(id); + return row; +} + +// --------------------------------------------------------------------------- +// billing_runs / invoices / invoice_items +// --------------------------------------------------------------------------- + +function dueDateFor(referenceYear: number, referenceMonth: number, billingDay: number): string { + const mm = String(referenceMonth).padStart(2, "0"); + const dd = String(billingDay).padStart(2, "0"); + return `${referenceYear}-${mm}-${dd}`; +} + +export interface GenerateBillingRunInput { + referenceYear: number; + referenceMonth: number; + billingDay: number; + triggeredBy: string | null; +} + +/** + * Idempotent billing close. Behavior for (referenceYear, referenceMonth, + * billingDay): + * - if a CONSOLIDATED run already exists for this exact period -> + * throws BillingPeriodAlreadyConsolidatedError (controlled re-billing + * is deferred, see migration header — this is a hard stop, not a + * silent double-bill). + * - if a DRAFT run already exists -> reused in place: its invoices (and + * their items) are deleted and regenerated from the current set of + * active subscriptions, all inside one transaction. Running this + * twice with the same parameters therefore never duplicates invoices + * — the second run replaces the first draft's invoices 1:1. + * - otherwise -> a new 'draft' run is created. + * + * One invoice per active billing_subscription whose billing_day matches, + * each carrying a single 'recurring' invoice_item for the full + * monthly_value (no proration in this MVP — see migration header). + */ +export async function generateOrRegenerateBillingRun( + input: GenerateBillingRunInput, +): Promise { + return withTransaction(async (client) => { + const { rows: existingRows } = await client.query( + `SELECT * FROM billing_runs + WHERE reference_year = $1 AND reference_month = $2 AND billing_day = $3 + AND status IN ('draft', 'consolidated') + FOR UPDATE`, + [input.referenceYear, input.referenceMonth, input.billingDay], + ); + + const consolidated = existingRows.find((r) => r.status === "consolidated"); + if (consolidated) { + throw new BillingPeriodAlreadyConsolidatedError( + input.referenceYear, + input.referenceMonth, + input.billingDay, + ); + } + + const existingDraft = existingRows.find((r) => r.status === "draft"); + let run: BillingRun; + + if (existingDraft) { + // Regeneration: wipe this draft's invoices (items cascade) and + // refresh triggered_by to reflect who ran it last. + await client.query( + `DELETE FROM invoices WHERE billing_run_id = $1`, + [existingDraft.id], + ); + const { rows: updatedRows } = await client.query( + `UPDATE billing_runs SET triggered_by = $2 WHERE id = $1 RETURNING *`, + [existingDraft.id, input.triggeredBy], + ); + run = updatedRows[0]!; + } else { + const { rows: createdRows } = await client.query( + `INSERT INTO billing_runs (reference_year, reference_month, billing_day, triggered_by) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [input.referenceYear, input.referenceMonth, input.billingDay, input.triggeredBy], + ); + run = createdRows[0]!; + } + + const { rows: subscriptions } = await client.query( + `SELECT * FROM billing_subscriptions WHERE status = 'active' AND billing_day = $1`, + [input.billingDay], + ); + + const dueDate = dueDateFor(input.referenceYear, input.referenceMonth, input.billingDay); + const invoices: Invoice[] = []; + + for (const subscription of subscriptions) { + const { rows: invoiceRows } = await client.query( + `INSERT INTO invoices + (billing_run_id, billing_subscription_id, reseller_id, amount, reference_year, reference_month, due_date) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING *`, + [ + run.id, + subscription.id, + subscription.reseller_id, + subscription.monthly_value, + input.referenceYear, + input.referenceMonth, + dueDate, + ], + ); + const invoice = invoiceRows[0]!; + + await client.query( + `INSERT INTO invoice_items (invoice_id, description, amount, item_type) + VALUES ($1, $2, $3, 'recurring')`, + [invoice.id, "Mensalidade recorrente", subscription.monthly_value], + ); + + invoices.push(invoice); + } + + return { ...run, invoices, totalAmount: sumAmounts(invoices) }; + }); +} + +export interface ListBillingRunsFilter { + referenceYear?: number; + referenceMonth?: number; + status?: BillingRunStatus; +} + +export async function listBillingRuns(filter: ListBillingRunsFilter = {}): Promise { + const conditions: string[] = []; + const params: unknown[] = []; + + if (filter.referenceYear != null) { + params.push(filter.referenceYear); + conditions.push(`reference_year = $${params.length}`); + } + if (filter.referenceMonth != null) { + params.push(filter.referenceMonth); + conditions.push(`reference_month = $${params.length}`); + } + if (filter.status) { + params.push(filter.status); + conditions.push(`status = $${params.length}`); + } + + const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; + const { rows } = await query( + `SELECT * FROM billing_runs ${where} ORDER BY created_at DESC`, + params, + ); + return rows; +} + +function sumAmounts(invoices: Invoice[]): string { + const total = invoices.reduce((sum, inv) => sum + Number(inv.amount), 0); + return total.toFixed(2); +} + +export async function getBillingRunById(id: string): Promise { + const { rows } = await query("SELECT * FROM billing_runs WHERE id = $1", [id]); + const run = rows[0]; + if (!run) return null; + + const { rows: invoices } = await query( + "SELECT * FROM invoices WHERE billing_run_id = $1 ORDER BY created_at ASC", + [id], + ); + return { ...run, invoices, totalAmount: sumAmounts(invoices) }; +} + +export async function listInvoiceItems(invoiceId: string): Promise { + const { rows } = await query( + "SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY created_at ASC", + [invoiceId], + ); + return rows; +} + +/** + * Consolidates a billing_run: flips the run and every invoice it owns to + * 'consolidated' and stamps consolidated_at, all in one transaction. + * 409s (via BillingRunConsolidatedError) if already consolidated — + * consolidation is a one-way door in this MVP (no un-consolidate). + */ +export async function consolidateBillingRun( + id: string, + _actorId: string | null, +): Promise { + return withTransaction(async (client) => { + const { rows } = await client.query( + "SELECT * FROM billing_runs WHERE id = $1 FOR UPDATE", + [id], + ); + const run = rows[0]; + if (!run) throw new BillingRunNotFoundError(id); + if (run.status === "consolidated") throw new BillingRunConsolidatedError(id); + + const { rows: updatedRows } = await client.query( + `UPDATE billing_runs SET status = 'consolidated', consolidated_at = now() WHERE id = $1 RETURNING *`, + [id], + ); + const updated = updatedRows[0]!; + + await client.query( + `UPDATE invoices SET status = 'consolidated' WHERE billing_run_id = $1`, + [id], + ); + + const { rows: invoices } = await client.query( + "SELECT * FROM invoices WHERE billing_run_id = $1 ORDER BY created_at ASC", + [id], + ); + return { ...updated, invoices, totalAmount: sumAmounts(invoices) }; + }); +} + +export interface ListInvoicesFilter { + scope: "all" | "reseller"; + /** Required (and enforced by the caller) when scope === 'reseller'. */ + resellerId?: string | null; + status?: InvoiceStatus; + referenceYear?: number; + referenceMonth?: number; +} + +export async function listInvoices(filter: ListInvoicesFilter): Promise { + const conditions: string[] = []; + const params: unknown[] = []; + + if (filter.scope === "reseller") { + params.push(filter.resellerId ?? null); + conditions.push(`reseller_id = $${params.length}`); + } + if (filter.status) { + params.push(filter.status); + conditions.push(`status = $${params.length}`); + } + if (filter.referenceYear != null) { + params.push(filter.referenceYear); + conditions.push(`reference_year = $${params.length}`); + } + if (filter.referenceMonth != null) { + params.push(filter.referenceMonth); + conditions.push(`reference_month = $${params.length}`); + } + + const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; + const { rows } = await query( + `SELECT * FROM invoices ${where} ORDER BY created_at DESC`, + params, + ); + return rows; +} diff --git a/packages/database/src/repositories/fiscal.ts b/packages/database/src/repositories/fiscal.ts new file mode 100644 index 0000000..7279312 --- /dev/null +++ b/packages/database/src/repositories/fiscal.ts @@ -0,0 +1,484 @@ +import { query, withTransaction } from "../pool.js"; + +/** + * Fiscal bounded context (Master Prompt §6.11, ADR-0011, eden.md módulo 5). + * See PENDING_fiscal.cjs migration header for full scope/deferral notes. + * This MVP covers 3 "catalog" tables (fiscal_ncm, fiscal_cfop, + * fiscal_municipality), the read-only fiscal_operation_nature seed, and + * product_fiscal_profiles — the piece that actually links a commercial + * product to its fiscal classification. + */ + +// =========================================================================== +// fiscal_ncm — "simple catalog" shape (eden.md §2.2) +// =========================================================================== + +export interface FiscalNcm { + id: string; + code: string; + description: string; + valid_from: string | null; + valid_until: string | null; + active: boolean; + source: string | null; + source_version: string | null; + source_updated_at: Date | null; + metadata: Record | null; + created_at: Date; + updated_at: Date; +} + +export interface CreateSimpleCatalogInput { + code: string; + description: string; + validFrom?: string | null; + validUntil?: string | null; + active?: boolean; + source?: string | null; + sourceVersion?: string | null; + sourceUpdatedAt?: string | null; + metadata?: Record | null; +} + +export interface UpdateSimpleCatalogInput { + code?: string; + description?: string; + validFrom?: string | null; + validUntil?: string | null; + active?: boolean; + source?: string | null; + sourceVersion?: string | null; + sourceUpdatedAt?: string | null; + metadata?: Record | null; +} + +export interface ListCatalogFilter { + q?: string; + active?: boolean; +} + +export async function listNcm(filter: ListCatalogFilter = {}): Promise { + const conditions: string[] = []; + const params: unknown[] = []; + if (filter.q) { + params.push(`%${filter.q}%`); + conditions.push(`(code ILIKE $${params.length} OR description ILIKE $${params.length})`); + } + if (filter.active !== undefined) { + params.push(filter.active); + conditions.push(`active = $${params.length}`); + } + const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; + const { rows } = await query( + `SELECT * FROM fiscal_ncm ${where} ORDER BY code ASC NULLS LAST`, + params, + ); + return rows; +} + +export async function getNcmById(id: string): Promise { + const { rows } = await query("SELECT * FROM fiscal_ncm WHERE id = $1", [id]); + return rows[0] ?? null; +} + +export async function createNcm(input: CreateSimpleCatalogInput): Promise { + const { rows } = await query( + `INSERT INTO fiscal_ncm (code, description, valid_from, valid_until, active, source, source_version, source_updated_at, metadata) + VALUES ($1,$2,$3,$4,COALESCE($5,true),$6,$7,$8,$9) + RETURNING *`, + [ + input.code, + input.description, + input.validFrom ?? null, + input.validUntil ?? null, + input.active ?? null, + input.source ?? null, + input.sourceVersion ?? null, + input.sourceUpdatedAt ?? null, + input.metadata ? JSON.stringify(input.metadata) : null, + ], + ); + const row = rows[0]; + if (!row) throw new Error("failed to create fiscal_ncm row"); + return row; +} + +export async function updateNcm(id: string, input: UpdateSimpleCatalogInput): Promise { + const { rows } = await query( + `UPDATE fiscal_ncm SET + code = COALESCE($2, code), + description = COALESCE($3, description), + valid_from = COALESCE($4, valid_from), + valid_until = COALESCE($5, valid_until), + active = COALESCE($6, active), + source = COALESCE($7, source), + source_version = COALESCE($8, source_version), + source_updated_at = COALESCE($9, source_updated_at), + metadata = COALESCE($10, metadata) + WHERE id = $1 + RETURNING *`, + [ + id, + input.code ?? null, + input.description ?? null, + input.validFrom ?? null, + input.validUntil ?? null, + input.active ?? null, + input.source ?? null, + input.sourceVersion ?? null, + input.sourceUpdatedAt ?? null, + input.metadata ? JSON.stringify(input.metadata) : null, + ], + ); + return rows[0] ?? null; +} + +// =========================================================================== +// fiscal_cfop — "simple catalog" shape + direction/scope (eden.md §2.3/§4) +// =========================================================================== + +export interface FiscalCfop { + id: string; + code: string; + description: string; + direction: string | null; + scope: string | null; + valid_from: string | null; + valid_until: string | null; + active: boolean; + source: string | null; + source_version: string | null; + source_updated_at: Date | null; + metadata: Record | null; + created_at: Date; + updated_at: Date; +} + +export interface CreateCfopInput extends CreateSimpleCatalogInput { + direction?: string | null; + scope?: string | null; +} + +export interface UpdateCfopInput extends UpdateSimpleCatalogInput { + direction?: string | null; + scope?: string | null; +} + +export async function listCfop(filter: ListCatalogFilter = {}): Promise { + const conditions: string[] = []; + const params: unknown[] = []; + if (filter.q) { + params.push(`%${filter.q}%`); + conditions.push(`(code ILIKE $${params.length} OR description ILIKE $${params.length})`); + } + if (filter.active !== undefined) { + params.push(filter.active); + conditions.push(`active = $${params.length}`); + } + const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; + const { rows } = await query( + `SELECT * FROM fiscal_cfop ${where} ORDER BY code ASC NULLS LAST`, + params, + ); + return rows; +} + +export async function getCfopById(id: string): Promise { + const { rows } = await query("SELECT * FROM fiscal_cfop WHERE id = $1", [id]); + return rows[0] ?? null; +} + +export async function createCfop(input: CreateCfopInput): Promise { + const { rows } = await query( + `INSERT INTO fiscal_cfop (code, description, direction, scope, valid_from, valid_until, active, source, source_version, source_updated_at, metadata) + VALUES ($1,$2,$3,$4,$5,$6,COALESCE($7,true),$8,$9,$10,$11) + RETURNING *`, + [ + input.code, + input.description, + input.direction ?? null, + input.scope ?? null, + input.validFrom ?? null, + input.validUntil ?? null, + input.active ?? null, + input.source ?? null, + input.sourceVersion ?? null, + input.sourceUpdatedAt ?? null, + input.metadata ? JSON.stringify(input.metadata) : null, + ], + ); + const row = rows[0]; + if (!row) throw new Error("failed to create fiscal_cfop row"); + return row; +} + +export async function updateCfop(id: string, input: UpdateCfopInput): Promise { + const { rows } = await query( + `UPDATE fiscal_cfop SET + code = COALESCE($2, code), + description = COALESCE($3, description), + direction = COALESCE($4, direction), + scope = COALESCE($5, scope), + valid_from = COALESCE($6, valid_from), + valid_until = COALESCE($7, valid_until), + active = COALESCE($8, active), + source = COALESCE($9, source), + source_version = COALESCE($10, source_version), + source_updated_at = COALESCE($11, source_updated_at), + metadata = COALESCE($12, metadata) + WHERE id = $1 + RETURNING *`, + [ + id, + input.code ?? null, + input.description ?? null, + input.direction ?? null, + input.scope ?? null, + input.validFrom ?? null, + input.validUntil ?? null, + input.active ?? null, + input.source ?? null, + input.sourceVersion ?? null, + input.sourceUpdatedAt ?? null, + input.metadata ? JSON.stringify(input.metadata) : null, + ], + ); + return rows[0] ?? null; +} + +// =========================================================================== +// fiscal_municipality — distinct shape, no valid_from/valid_until/metadata +// (eden.md §2.3) +// =========================================================================== + +export interface FiscalMunicipality { + id: string; + code: string; + name: string; + state: string; + active: boolean; + source: string | null; + source_version: string | null; + created_at: Date; + updated_at: Date; +} + +export interface CreateMunicipalityInput { + code: string; + name: string; + state: string; + active?: boolean; + source?: string | null; + sourceVersion?: string | null; +} + +export interface UpdateMunicipalityInput { + code?: string; + name?: string; + state?: string; + active?: boolean; + source?: string | null; + sourceVersion?: string | null; +} + +export async function listMunicipalities(filter: ListCatalogFilter = {}): Promise { + const conditions: string[] = []; + const params: unknown[] = []; + if (filter.q) { + params.push(`%${filter.q}%`); + conditions.push(`(code ILIKE $${params.length} OR name ILIKE $${params.length})`); + } + if (filter.active !== undefined) { + params.push(filter.active); + conditions.push(`active = $${params.length}`); + } + const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; + const { rows } = await query( + `SELECT * FROM fiscal_municipality ${where} ORDER BY code ASC NULLS LAST`, + params, + ); + return rows; +} + +export async function getMunicipalityById(id: string): Promise { + const { rows } = await query("SELECT * FROM fiscal_municipality WHERE id = $1", [id]); + return rows[0] ?? null; +} + +export async function createMunicipality(input: CreateMunicipalityInput): Promise { + const { rows } = await query( + `INSERT INTO fiscal_municipality (code, name, state, active, source, source_version) + VALUES ($1,$2,$3,COALESCE($4,true),$5,$6) + RETURNING *`, + [ + input.code, + input.name, + input.state, + input.active ?? null, + input.source ?? null, + input.sourceVersion ?? null, + ], + ); + const row = rows[0]; + if (!row) throw new Error("failed to create fiscal_municipality row"); + return row; +} + +export async function updateMunicipality( + id: string, + input: UpdateMunicipalityInput, +): Promise { + const { rows } = await query( + `UPDATE fiscal_municipality SET + code = COALESCE($2, code), + name = COALESCE($3, name), + state = COALESCE($4, state), + active = COALESCE($5, active), + source = COALESCE($6, source), + source_version = COALESCE($7, source_version) + WHERE id = $1 + RETURNING *`, + [ + id, + input.code ?? null, + input.name ?? null, + input.state ?? null, + input.active ?? null, + input.source ?? null, + input.sourceVersion ?? null, + ], + ); + return rows[0] ?? null; +} + +// =========================================================================== +// fiscal_operation_nature — internal enum-like catalog, read-only via API +// (eden.md §2.3) — seeded by the migration, no create/update in this MVP. +// =========================================================================== + +export interface FiscalOperationNature { + id: string; + code: string; + name: string; + description: string | null; + active: boolean; + created_at: Date; + updated_at: Date; +} + +export async function listOperationNatures(activeOnly = false): Promise { + const { rows } = await query( + activeOnly + ? "SELECT * FROM fiscal_operation_nature WHERE active = true ORDER BY code ASC" + : "SELECT * FROM fiscal_operation_nature ORDER BY code ASC", + ); + return rows; +} + +// =========================================================================== +// product_fiscal_profiles — links products to fiscal classification +// (eden.md §2.5). Reduced to the 4 catalogs created in this MVP. +// =========================================================================== + +export type FiscalBillingComponent = "RECURRING" | "IMPLEMENTATION"; +export type FiscalDocumentType = "NFCOM" | "NFSE" | "NFE" | "NONE"; + +export interface ProductFiscalProfile { + id: string; + product_id: string; + billing_component: FiscalBillingComponent; + document_type: FiscalDocumentType; + ncm_id: string | null; + cfop_id: string | null; + municipality_id: string | null; + operation_nature_id: string | null; + valid_from: string | null; + valid_until: string | null; + active: boolean; + created_at: Date; + updated_at: Date; + created_by: string | null; + updated_by: string | null; +} + +export async function listProductFiscalProfiles(productId: string): Promise { + const { rows } = await query( + `SELECT * FROM product_fiscal_profiles + WHERE product_id = $1 + ORDER BY billing_component ASC, created_at ASC`, + [productId], + ); + return rows; +} + +export async function getActiveProductFiscalProfile( + productId: string, + billingComponent: FiscalBillingComponent, +): Promise { + const { rows } = await query( + `SELECT * FROM product_fiscal_profiles + WHERE product_id = $1 AND billing_component = $2 AND active = true`, + [productId, billingComponent], + ); + return rows[0] ?? null; +} + +export interface CreateProductFiscalProfileInput { + productId: string; + billingComponent: FiscalBillingComponent; + documentType?: FiscalDocumentType; + ncmId?: string | null; + cfopId?: string | null; + municipalityId?: string | null; + operationNatureId?: string | null; + validFrom?: string | null; + validUntil?: string | null; + createdBy?: string | null; +} + +/** + * Creates a new ACTIVE profile for (productId, billingComponent), and — in + * the same transaction — inactivates whichever profile was previously + * active for that same pair (never a destructive UPDATE/DELETE of the old + * one, per eden.md §2.5's central invariant: at most one active profile per + * product per billing_component, enforced again at the DB level by + * idx_product_fiscal_profiles_active_component). If two concurrent callers + * raced here without the transaction ordering below, the partial unique + * index is the actual backstop — it would reject the second INSERT outright. + */ +export async function createActiveProductFiscalProfile( + input: CreateProductFiscalProfileInput, +): Promise { + return withTransaction(async (client) => { + await client.query( + `UPDATE product_fiscal_profiles + SET active = false, updated_by = $3 + WHERE product_id = $1 AND billing_component = $2 AND active = true`, + [input.productId, input.billingComponent, input.createdBy ?? null], + ); + + const { rows } = await client.query( + `INSERT INTO product_fiscal_profiles ( + product_id, billing_component, document_type, ncm_id, cfop_id, + municipality_id, operation_nature_id, valid_from, valid_until, + created_by, updated_by + ) VALUES ($1,$2,COALESCE($3,'NONE'),$4,$5,$6,$7,$8,$9,$10,$10) + RETURNING *`, + [ + input.productId, + input.billingComponent, + input.documentType ?? null, + input.ncmId ?? null, + input.cfopId ?? null, + input.municipalityId ?? null, + input.operationNatureId ?? null, + input.validFrom ?? null, + input.validUntil ?? null, + input.createdBy ?? null, + ], + ); + const row = rows[0]; + if (!row) throw new Error("failed to create product_fiscal_profiles row"); + return row; + }); +} diff --git a/packages/database/src/repositories/hr-timeclock.ts b/packages/database/src/repositories/hr-timeclock.ts new file mode 100644 index 0000000..9fea5e5 --- /dev/null +++ b/packages/database/src/repositories/hr-timeclock.ts @@ -0,0 +1,468 @@ +import { query, withTransaction } from "../pool.js"; +import type { PoolClient } from "pg"; + +/** + * HR/Timeclock (eden.md módulo 6) — foundation leva. See + * packages/database/migrations/PENDING_hr-timeclock.cjs for the full data + * model rationale (employees-vs-users decision, deferred tables, naming + * deviations from eden.md). This module deliberately does NOT expose any + * UPDATE/DELETE for afd_records — see importAfdBatch below and the + * afd-records.controller.ts (read-only) that consumes listAfdRecords. + */ + +// ───────────────────────────────────────────────────────────────────────── +// timeclock_devices +// ───────────────────────────────────────────────────────────────────────── + +export type TimeclockDeviceStatus = "UNKNOWN" | "ONLINE" | "OFFLINE" | "ERROR"; + +export interface TimeclockDevice { + id: string; + created_at: Date; + updated_at: Date; + created_by: string | null; + updated_by: string | null; + company_id: string | null; + name: string; + description: string | null; + branch: string | null; + host: string; + port: number; + use_https: boolean; + username: string; + password_encrypted: string; + serial_number: string | null; + model: string | null; + firmware_version: string | null; + timezone: string; + status: TimeclockDeviceStatus; + last_seen_at: Date | null; + last_sync_at: Date | null; + last_error: string | null; + last_afd_nsr: string | null; // BIGINT comes back as string from `pg` + active: boolean; +} + +export interface CreateTimeclockDeviceInput { + companyId?: string | null; + name: string; + description?: string | null; + branch?: string | null; + host: string; + port?: number; + useHttps?: boolean; + username: string; + /** Already encrypted (v1:iv:tag:ciphertext) by the caller via FieldEncryptionService — this repo never sees plaintext. */ + passwordEncrypted: string; + timezone?: string; + createdBy?: string | null; +} + +export async function listTimeclockDevices(includeInactive = false): Promise { + const { rows } = await query( + includeInactive + ? "SELECT * FROM timeclock_devices ORDER BY created_at DESC" + : "SELECT * FROM timeclock_devices WHERE active = true ORDER BY created_at DESC", + ); + return rows; +} + +export async function getTimeclockDeviceById(id: string): Promise { + const { rows } = await query("SELECT * FROM timeclock_devices WHERE id = $1", [id]); + return rows[0] ?? null; +} + +export async function createTimeclockDevice(input: CreateTimeclockDeviceInput): Promise { + const { rows } = await query( + `INSERT INTO timeclock_devices ( + company_id, name, description, branch, host, port, use_https, username, + password_encrypted, timezone, created_by + ) VALUES ($1,$2,$3,$4,$5,COALESCE($6,443),COALESCE($7,true),$8,$9,COALESCE($10,'America/Sao_Paulo'),$11) + RETURNING *`, + [ + input.companyId ?? null, + input.name, + input.description ?? null, + input.branch ?? null, + input.host, + input.port ?? null, + input.useHttps ?? null, + input.username, + input.passwordEncrypted, + input.timezone ?? null, + input.createdBy ?? null, + ], + ); + const device = rows[0]; + if (!device) throw new Error("failed to create timeclock device"); + return device; +} + +export interface UpdateTimeclockDeviceInput { + name?: string; + description?: string | null; + branch?: string | null; + host?: string; + port?: number; + useHttps?: boolean; + username?: string; + /** Only re-encrypted/set when the caller actually supplied a new password — omit to leave unchanged. */ + passwordEncrypted?: string; + active?: boolean; + updatedBy?: string | null; +} + +export async function updateTimeclockDevice( + id: string, + input: UpdateTimeclockDeviceInput, +): Promise { + const { rows } = await query( + `UPDATE timeclock_devices SET + name = COALESCE($2, name), + description = COALESCE($3, description), + branch = COALESCE($4, branch), + host = COALESCE($5, host), + port = COALESCE($6, port), + use_https = COALESCE($7, use_https), + username = COALESCE($8, username), + password_encrypted = COALESCE($9, password_encrypted), + active = COALESCE($10, active), + updated_by = COALESCE($11, updated_by) + WHERE id = $1 + RETURNING *`, + [ + id, + input.name ?? null, + input.description ?? null, + input.branch ?? null, + input.host ?? null, + input.port ?? null, + input.useHttps ?? null, + input.username ?? null, + input.passwordEncrypted ?? null, + input.active ?? null, + input.updatedBy ?? null, + ], + ); + return rows[0] ?? null; +} + +// ───────────────────────────────────────────────────────────────────────── +// timeclock_employees — no CRUD in this leva (see migration header); only +// the CPF-resolution lookup that AFD import needs is implemented here. +// ───────────────────────────────────────────────────────────────────────── + +export interface TimeclockEmployee { + id: string; + created_at: Date; + updated_at: Date; + created_by: string | null; + updated_by: string | null; + company_id: string | null; + user_id: string | null; + full_name: string; + cpf: string; + registration: string | null; + active: boolean; +} + +/** + * Looks up employees by normalized CPF (11 digits) for AFD import + * resolution — never invents a match. Returns every row found; the + * caller (afd-imports.service.ts) is responsible for the "same CPF + * matched in more than one company and the device has no company" case, + * where it must leave employee_id unresolved rather than guess. + */ +export async function findTimeclockEmployeesByCpfs(cpfs: string[]): Promise { + if (cpfs.length === 0) return []; + const { rows } = await query( + "SELECT * FROM timeclock_employees WHERE cpf = ANY($1)", + [cpfs], + ); + return rows; +} + +// ───────────────────────────────────────────────────────────────────────── +// afd_import_batches / afd_records +// ───────────────────────────────────────────────────────────────────────── + +export type AfdImportBatchStatus = "VALID" | "WARNING" | "INVALID"; +export type AfdCrcValidity = "VALID" | "INVALID" | "NOT_APPLICABLE" | "NOT_VALIDATED"; +export type AfdValidationStatus = "VALID" | "WARNING" | "INVALID" | "UNSUPPORTED_LAYOUT"; + +export interface AfdImportBatch { + id: string; + device_id: string; + company_id: string | null; + source: "manual_upload"; + file_hash: string; + file_size: number; + record_count: number; + inserted_count: number; + valid_count: number; + warning_count: number; + invalid_count: number; + unsupported_count: number; + parse_failure_count: number; + status: AfdImportBatchStatus; + first_nsr: string | null; + last_nsr: string | null; + summary: Record; + imported_by: string | null; + imported_at: Date; +} + +export interface CreateAfdImportBatchInput { + deviceId: string; + companyId: string | null; + fileHash: string; + fileSize: number; + recordCount: number; + validCount: number; + warningCount: number; + invalidCount: number; + unsupportedCount: number; + parseFailureCount: number; + status: AfdImportBatchStatus; + firstNsr: number | null; + lastNsr: number | null; + summary: Record; + importedBy: string | null; +} + +export interface AfdRecord { + id: string; + created_at: Date; + afd_import_batch_id: string; + device_id: string; + company_id: string | null; + nsr: string | null; // BIGINT -> string + record_type: string; + event_datetime: Date | null; + event_datetime_raw: string | null; + employee_cpf_raw: string | null; + employee_id: string | null; + raw_line: string; + crc_received: string | null; + crc_calculated: string | null; + crc_valid: AfdCrcValidity; + parsed_data: Record; + validation_status: AfdValidationStatus; + validation_notes: string | null; +} + +export interface AfdRecordInsertInput { + nsr: number | null; + recordType: string; + eventDatetime: Date | null; + eventDatetimeRaw: string | null; + employeeCpfRaw: string | null; + employeeId: string | null; + rawLine: string; + crcReceived: string | null; + crcCalculated: string | null; + crcValid: AfdCrcValidity; + parsedData: Record; + validationStatus: AfdValidationStatus; + validationNotes: string | null; +} + +export class TimeclockDeviceNotFoundError extends Error { + constructor(public readonly deviceId: string) { + super(`Timeclock device ${deviceId} not found`); + this.name = "TimeclockDeviceNotFoundError"; + } +} + +export async function findAfdImportBatchByFileHash(fileHash: string): Promise { + const { rows } = await query("SELECT * FROM afd_import_batches WHERE file_hash = $1", [fileHash]); + return rows[0] ?? null; +} + +export async function getAfdImportBatchById(id: string): Promise { + const { rows } = await query("SELECT * FROM afd_import_batches WHERE id = $1", [id]); + return rows[0] ?? null; +} + +export interface ImportAfdBatchInput { + batch: CreateAfdImportBatchInput; + records: AfdRecordInsertInput[]; +} + +export interface ImportAfdBatchResult { + batch: AfdImportBatch; + insertedCount: number; +} + +/** + * The whole AFD import write path, in one transaction: + * + * 1. `SELECT ... FOR UPDATE` locks the device row — this is what + * guarantees "never two concurrent imports for the same device" + * (eden.md módulo 6 §12's sequential-execution invariant for anything + * device-adjacent): a second concurrent import for the same device_id + * blocks here until the first one commits or rolls back. + * 2. Inserts the afd_import_batches row (needs to exist first — afd_records + * has a NOT NULL FK to it). + * 3. Inserts each afd_records row one at a time (matches this codebase's + * established multi-row-child pattern, see contracts.ts's item/party + * loops) with `ON CONFLICT (device_id, nsr) DO NOTHING` — the + * per-record idempotency layer (eden.md §12): a record whose + * (device_id, nsr) already exists from an earlier, different import is + * silently skipped, never duplicated, and never overwritten (afd_records + * has no UPDATE route at all — see the migration's trigger). + * 4. Updates the batch's `inserted_count` with how many rows actually + * landed (can be less than `records.length` because of step 3's + * ON CONFLICT skips). + * + * Whole-file idempotency (SHA-256 of the received content) is checked by + * the caller (afd-imports.service.ts) BEFORE calling this, via + * findAfdImportBatchByFileHash — this function does not re-check it, + * though the UNIQUE constraint on afd_import_batches.file_hash is a + * database-level backstop against a race between the two. + */ +export async function importAfdBatch(input: ImportAfdBatchInput): Promise { + return withTransaction(async (client: PoolClient) => { + const { rows: deviceRows } = await client.query<{ id: string }>( + "SELECT id FROM timeclock_devices WHERE id = $1 FOR UPDATE", + [input.batch.deviceId], + ); + if (!deviceRows[0]) { + throw new TimeclockDeviceNotFoundError(input.batch.deviceId); + } + + const { rows: batchRows } = await client.query( + `INSERT INTO afd_import_batches ( + device_id, company_id, file_hash, file_size, record_count, inserted_count, + valid_count, warning_count, invalid_count, unsupported_count, parse_failure_count, + status, first_nsr, last_nsr, summary, imported_by + ) VALUES ($1,$2,$3,$4,$5,0,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) + RETURNING *`, + [ + input.batch.deviceId, + input.batch.companyId, + input.batch.fileHash, + input.batch.fileSize, + input.batch.recordCount, + input.batch.validCount, + input.batch.warningCount, + input.batch.invalidCount, + input.batch.unsupportedCount, + input.batch.parseFailureCount, + input.batch.status, + input.batch.firstNsr, + input.batch.lastNsr, + JSON.stringify(input.batch.summary), + input.batch.importedBy, + ], + ); + const batch = batchRows[0]; + if (!batch) throw new Error("failed to create afd_import_batch"); + + let insertedCount = 0; + for (const record of input.records) { + const { rowCount } = await client.query( + `INSERT INTO afd_records ( + afd_import_batch_id, device_id, company_id, nsr, record_type, + event_datetime, event_datetime_raw, employee_cpf_raw, employee_id, raw_line, + crc_received, crc_calculated, crc_valid, parsed_data, validation_status, validation_notes + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) + ON CONFLICT (device_id, nsr) DO NOTHING`, + [ + batch.id, + input.batch.deviceId, + input.batch.companyId, + record.nsr, + record.recordType, + record.eventDatetime, + record.eventDatetimeRaw, + record.employeeCpfRaw, + record.employeeId, + record.rawLine, + record.crcReceived, + record.crcCalculated, + record.crcValid, + JSON.stringify(record.parsedData), + record.validationStatus, + record.validationNotes, + ], + ); + insertedCount += rowCount ?? 0; + } + + const { rows: updatedBatchRows } = await client.query( + "UPDATE afd_import_batches SET inserted_count = $2 WHERE id = $1 RETURNING *", + [batch.id, insertedCount], + ); + const updatedBatch = updatedBatchRows[0] ?? batch; + + return { batch: updatedBatch, insertedCount }; + }); +} + +export interface ListAfdRecordsFilter { + deviceId?: string; + employeeId?: string; + recordType?: string; + /** Inclusive lower bound on event_datetime. */ + dateFrom?: Date; + /** Exclusive upper bound on event_datetime (caller passes "day after dateTo" to include the whole end day). */ + dateTo?: Date; + limit?: number; + offset?: number; +} + +const DEFAULT_PAGE_SIZE = 50; + +/** + * Read-only, paginated. There is deliberately no equivalent write + * function for arbitrary fields — afd_records rows are only ever created + * via importAfdBatch's INSERT, never updated. + */ +export async function listAfdRecords(filter: ListAfdRecordsFilter): Promise<{ records: AfdRecord[]; total: number }> { + const conditions: string[] = []; + const params: unknown[] = []; + + if (filter.deviceId) { + params.push(filter.deviceId); + conditions.push(`device_id = $${params.length}`); + } + if (filter.employeeId) { + params.push(filter.employeeId); + conditions.push(`employee_id = $${params.length}`); + } + if (filter.recordType) { + params.push(filter.recordType); + conditions.push(`record_type = $${params.length}`); + } + if (filter.dateFrom) { + params.push(filter.dateFrom); + conditions.push(`event_datetime >= $${params.length}`); + } + if (filter.dateTo) { + params.push(filter.dateTo); + conditions.push(`event_datetime < $${params.length}`); + } + + const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + const limit = filter.limit ?? DEFAULT_PAGE_SIZE; + const offset = filter.offset ?? 0; + + const { rows: countRows } = await query<{ count: string }>( + `SELECT count(*) FROM afd_records ${where}`, + params, + ); + const total = Number(countRows[0]?.count ?? 0); + + const { rows } = await query( + `SELECT * FROM afd_records ${where} ORDER BY event_datetime ASC NULLS LAST, created_at ASC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`, + [...params, limit, offset], + ); + + return { records: rows, total }; +} + +export async function getAfdRecordById(id: string): Promise { + const { rows } = await query("SELECT * FROM afd_records WHERE id = $1", [id]); + return rows[0] ?? null; +} diff --git a/packages/database/src/repositories/inventory.ts b/packages/database/src/repositories/inventory.ts new file mode 100644 index 0000000..fd07f87 --- /dev/null +++ b/packages/database/src/repositories/inventory.ts @@ -0,0 +1,747 @@ +import { query, withTransaction } from "../pool.js"; +import type { PoolClient } from "pg"; + +/** + * Inventory/Warehouse/Assets bounded context — Master Prompt §6.8, + * ADR-0007. See the migration's header comment (PENDING_inventory.cjs) + * for the full list of possible entities and which 5 this MVP builds vs. + * explicitly deferred ones. + * + * Mirrors the conventions of companies.ts/products.ts (COALESCE-based + * partial writes, snake_case rows) and contracts.ts (withTransaction for + * multi-row writes that must commit/rollback together, custom error + * classes mapped to 404/409 by the service layer). + * + * NOT registered in the barrel (packages/database/src/index.ts) yet — + * that's deliberate integration work left to the merge step (see + * .claude/agents/eden-inventory.md task boundaries). + */ + +// =========================================================================== +// warehouses +// =========================================================================== + +export interface Warehouse { + id: string; + created_at: Date; + updated_at: Date; + created_by: string | null; + company_id: string; + name: string; + code: string; + is_active: boolean; +} + +export async function listWarehouses(includeInactive = false): Promise { + const { rows } = await query( + includeInactive + ? "SELECT * FROM warehouses ORDER BY created_at ASC" + : "SELECT * FROM warehouses WHERE is_active = true ORDER BY created_at ASC", + ); + return rows; +} + +export async function getWarehouseById(id: string): Promise { + const { rows } = await query("SELECT * FROM warehouses WHERE id = $1", [id]); + return rows[0] ?? null; +} + +export interface CreateWarehouseInput { + companyId: string; + name: string; + code: string; + isActive?: boolean; + createdBy?: string | null; +} + +export async function createWarehouse(input: CreateWarehouseInput): Promise { + const { rows } = await query( + `INSERT INTO warehouses (company_id, name, code, is_active, created_by) + VALUES ($1,$2,$3,COALESCE($4,true),$5) + RETURNING *`, + [input.companyId, input.name, input.code, input.isActive ?? null, input.createdBy ?? null], + ); + const warehouse = rows[0]; + if (!warehouse) throw new Error("failed to create warehouse"); + return warehouse; +} + +export interface UpdateWarehouseInput { + name?: string; + code?: string; + isActive?: boolean; +} + +export async function updateWarehouse(id: string, input: UpdateWarehouseInput): Promise { + const { rows } = await query( + `UPDATE warehouses SET + name = COALESCE($2, name), + code = COALESCE($3, code), + is_active = COALESCE($4, is_active) + WHERE id = $1 + RETURNING *`, + [id, input.name ?? null, input.code ?? null, input.isActive ?? null], + ); + return rows[0] ?? null; +} + +// =========================================================================== +// stock_movements — append-only ledger +// =========================================================================== + +export type StockMovementType = + | "purchase_in" + | "adjustment_in" + | "adjustment_out" + | "transfer_out" + | "transfer_in" + | "reservation" + | "reservation_release" + | "sale_out" + | "install_out" + | "comodato_out" + | "return_in" + | "rma_out" + | "write_off"; + +export interface StockMovement { + id: string; + created_at: Date; + created_by: string | null; + warehouse_id: string; + product_id: string; + movement_type: StockMovementType; + quantity: string; + reference_type: string | null; + reference_id: string | null; + notes: string | null; +} + +export interface ListStockMovementsFilter { + warehouseId?: string; + productId?: string; + referenceType?: string; + referenceId?: string; +} + +export async function listStockMovements(filter: ListStockMovementsFilter = {}): Promise { + const conditions: string[] = []; + const params: unknown[] = []; + + if (filter.warehouseId) { + params.push(filter.warehouseId); + conditions.push(`warehouse_id = $${params.length}`); + } + if (filter.productId) { + params.push(filter.productId); + conditions.push(`product_id = $${params.length}`); + } + if (filter.referenceType) { + params.push(filter.referenceType); + conditions.push(`reference_type = $${params.length}`); + } + if (filter.referenceId) { + params.push(filter.referenceId); + conditions.push(`reference_id = $${params.length}`); + } + + const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + const { rows } = await query( + `SELECT * FROM stock_movements ${where} ORDER BY created_at DESC`, + params, + ); + return rows; +} + +export interface CreateStockMovementInput { + warehouseId: string; + productId: string; + movementType: StockMovementType; + quantity: number; + referenceType?: string | null; + referenceId?: string | null; + notes?: string | null; + createdBy?: string | null; +} + +/** + * Appends one movement. No update/edit function exists on purpose — a + * correction is always a new call to this function with the reversing + * type (also enforced at the DB level by trg_stock_movements_no_update/ + * no_delete). Accepts an optional `client` so callers that need this + * insert inside a larger transaction (asset entry/install/return) can + * pass the transaction's client instead of grabbing a new pool + * connection — used by future endpoints (adjustment/transfer/write_off) + * that aren't wired up yet in this MVP; the asset lifecycle functions + * below (createAssetWithPurchaseMovement, reserveAsset, installAsset, + * returnAsset) inline their own movement INSERTs instead of calling this, + * since they already hold the transaction's client for the surrounding + * asset row lock. + */ +export async function recordStockMovement( + input: CreateStockMovementInput, + client?: PoolClient, +): Promise { + const sql = `INSERT INTO stock_movements (warehouse_id, product_id, movement_type, quantity, reference_type, reference_id, notes, created_by) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8) + RETURNING *`; + const params = [ + input.warehouseId, + input.productId, + input.movementType, + input.quantity, + input.referenceType ?? null, + input.referenceId ?? null, + input.notes ?? null, + input.createdBy ?? null, + ]; + const { rows } = client + ? await client.query(sql, params) + : await query(sql, params); + const row = rows[0]; + if (!row) throw new Error("failed to record stock movement"); + return row; +} + +// =========================================================================== +// serialized_assets +// =========================================================================== + +export type SerializedAssetStatus = + | "in_stock" + | "reserved" + | "installed" + | "comodato" + | "returned" + | "written_off"; + +export interface SerializedAsset { + id: string; + created_at: Date; + updated_at: Date; + created_by: string | null; + updated_by: string | null; + product_id: string; + serial: string | null; + patrimony_tag: string | null; + mac_address: string | null; + warehouse_id: string | null; + status: SerializedAssetStatus; + current_contract_id: string | null; + warranty_expires_at: Date | null; +} + +export interface ListSerializedAssetsFilter { + status?: SerializedAssetStatus; + warehouseId?: string; + productId?: string; +} + +export async function listSerializedAssets(filter: ListSerializedAssetsFilter = {}): Promise { + const conditions: string[] = []; + const params: unknown[] = []; + + if (filter.status) { + params.push(filter.status); + conditions.push(`status = $${params.length}`); + } + if (filter.warehouseId) { + params.push(filter.warehouseId); + conditions.push(`warehouse_id = $${params.length}`); + } + if (filter.productId) { + params.push(filter.productId); + conditions.push(`product_id = $${params.length}`); + } + + const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + const { rows } = await query( + `SELECT * FROM serialized_assets ${where} ORDER BY created_at DESC`, + params, + ); + return rows; +} + +export async function getSerializedAssetById(id: string): Promise { + const { rows } = await query("SELECT * FROM serialized_assets WHERE id = $1", [id]); + return rows[0] ?? null; +} + +async function getSerializedAssetByIdForUpdate(client: PoolClient, id: string): Promise { + const { rows } = await client.query( + "SELECT * FROM serialized_assets WHERE id = $1 FOR UPDATE", + [id], + ); + return rows[0] ?? null; +} + +export class AssetNotFoundError extends Error { + constructor(public readonly assetId: string) { + super(`Serialized asset ${assetId} not found`); + this.name = "AssetNotFoundError"; + } +} + +export class InvalidAssetTransitionError extends Error { + constructor( + public readonly fromStatus: SerializedAssetStatus, + public readonly action: string, + ) { + super(`Asset in status '${fromStatus}' cannot be ${action}`); + this.name = "InvalidAssetTransitionError"; + } +} + +export interface CreateAssetInput { + productId: string; + warehouseId: string; + serial?: string | null; + patrimonyTag?: string | null; + macAddress?: string | null; + warrantyExpiresAt?: Date | string | null; + notes?: string | null; + createdBy?: string | null; +} + +/** + * Entrada de um ativo novo — cria a linha em serialized_assets (status + * in_stock) e o stock_movement purchase_in correspondente numa única + * transação. Se o serial/patrimônio/MAC já estiver "ativo" em outro + * ativo, a constraint de banco (índice único parcial) rejeita o INSERT — + * o erro do Postgres (23505) é deixado subir; a camada de serviço decide + * como traduzir para 409. + */ +export async function createAssetWithPurchaseMovement(input: CreateAssetInput): Promise<{ + asset: SerializedAsset; + movement: StockMovement; +}> { + return withTransaction(async (client) => { + const { rows: assetRows } = await client.query( + `INSERT INTO serialized_assets ( + product_id, serial, patrimony_tag, mac_address, warehouse_id, status, + warranty_expires_at, created_by, updated_by + ) VALUES ($1,$2,$3,$4,$5,'in_stock',$6,$7,$7) + RETURNING *`, + [ + input.productId, + input.serial ?? null, + input.patrimonyTag ?? null, + input.macAddress ?? null, + input.warehouseId, + input.warrantyExpiresAt ?? null, + input.createdBy ?? null, + ], + ); + const asset = assetRows[0]; + if (!asset) throw new Error("failed to create serialized asset"); + + const { rows: movementRows } = await client.query( + `INSERT INTO stock_movements (warehouse_id, product_id, movement_type, quantity, reference_type, reference_id, notes, created_by) + VALUES ($1,$2,'purchase_in',1,'serialized_asset',$3,$4,$5) + RETURNING *`, + [input.warehouseId, input.productId, asset.id, input.notes ?? null, input.createdBy ?? null], + ); + const movement = movementRows[0]; + if (!movement) throw new Error("failed to record purchase_in movement"); + + return { asset, movement }; + }); +} + +export interface ReserveAssetInput { + referenceType?: string | null; + referenceId?: string | null; + notes?: string | null; + createdBy?: string | null; +} + +/** + * Reserva um ativo específico já em estoque: cria a stock_reservation + * (quantity=1, reference_type='serialized_asset', reference_id=asset.id) + * e move o ativo para status='reserved'. Só permitido a partir de + * 'in_stock' — throws InvalidAssetTransitionError caso contrário. + */ +export async function reserveAsset( + assetId: string, + input: ReserveAssetInput, +): Promise<{ asset: SerializedAsset; reservation: StockReservation; movement: StockMovement }> { + return withTransaction(async (client) => { + const asset = await getSerializedAssetByIdForUpdate(client, assetId); + if (!asset) throw new AssetNotFoundError(assetId); + if (asset.status !== "in_stock") throw new InvalidAssetTransitionError(asset.status, "reserved"); + if (!asset.warehouse_id) throw new Error(`asset ${assetId} has status in_stock but no warehouse_id`); + + const { rows: updatedRows } = await client.query( + `UPDATE serialized_assets SET status = 'reserved', updated_by = $2 WHERE id = $1 RETURNING *`, + [assetId, input.createdBy ?? null], + ); + const updated = updatedRows[0]; + if (!updated) throw new Error(`asset ${assetId} disappeared mid-transaction`); + + const { rows: reservationRows } = await client.query( + `INSERT INTO stock_reservations (warehouse_id, product_id, quantity, reference_type, reference_id, status, notes, created_by) + VALUES ($1,$2,1,'serialized_asset',$3,'active',$4,$5) + RETURNING *`, + [asset.warehouse_id, asset.product_id, assetId, input.notes ?? null, input.createdBy ?? null], + ); + const reservation = reservationRows[0]; + if (!reservation) throw new Error("failed to create reservation"); + + const { rows: movementRows } = await client.query( + `INSERT INTO stock_movements (warehouse_id, product_id, movement_type, quantity, reference_type, reference_id, notes, created_by) + VALUES ($1,$2,'reservation',1,'serialized_asset',$3,$4,$5) + RETURNING *`, + [asset.warehouse_id, asset.product_id, assetId, input.notes ?? null, input.createdBy ?? null], + ); + const movement = movementRows[0]; + if (!movement) throw new Error("failed to record reservation movement"); + + return { asset: updated, reservation, movement }; + }); +} + +export interface InstallAssetInput { + contractId: string; + assignmentType: "installed" | "comodato"; + notes?: string | null; + createdBy?: string | null; +} + +/** + * Vincula um ativo (em 'in_stock' ou 'reserved') a um contrato: cria + * asset_assignments, move o ativo para status='installed'/'comodato' + * (limpando warehouse_id — a localização agora é "no cliente"), grava o + * stock_movement install_out/comodato_out, e — se havia uma reserva + * ATIVA para este ativo específico — marca-a 'fulfilled'. Tudo numa + * transação. Só permitido a partir de in_stock/reserved. + */ +export async function installAsset( + assetId: string, + input: InstallAssetInput, +): Promise<{ asset: SerializedAsset; assignment: AssetAssignment; movement: StockMovement }> { + return withTransaction(async (client) => { + const asset = await getSerializedAssetByIdForUpdate(client, assetId); + if (!asset) throw new AssetNotFoundError(assetId); + if (asset.status !== "in_stock" && asset.status !== "reserved") { + throw new InvalidAssetTransitionError(asset.status, input.assignmentType); + } + if (!asset.warehouse_id) throw new Error(`asset ${assetId} has no warehouse_id before install`); + + const originWarehouseId = asset.warehouse_id; + const movementType = input.assignmentType === "comodato" ? "comodato_out" : "install_out"; + + const { rows: updatedRows } = await client.query( + `UPDATE serialized_assets + SET status = $2, warehouse_id = NULL, current_contract_id = $3, updated_by = $4 + WHERE id = $1 + RETURNING *`, + [assetId, input.assignmentType, input.contractId, input.createdBy ?? null], + ); + const updated = updatedRows[0]; + if (!updated) throw new Error(`asset ${assetId} disappeared mid-transaction`); + + const { rows: assignmentRows } = await client.query( + `INSERT INTO asset_assignments (asset_id, contract_id, assignment_type, created_by) + VALUES ($1,$2,$3,$4) + RETURNING *`, + [assetId, input.contractId, input.assignmentType, input.createdBy ?? null], + ); + const assignment = assignmentRows[0]; + if (!assignment) throw new Error("failed to create asset assignment"); + + const { rows: movementRows } = await client.query( + `INSERT INTO stock_movements (warehouse_id, product_id, movement_type, quantity, reference_type, reference_id, notes, created_by) + VALUES ($1,$2,$3,1,'contract',$4,$5,$6) + RETURNING *`, + [originWarehouseId, asset.product_id, movementType, input.contractId, input.notes ?? null, input.createdBy ?? null], + ); + const movement = movementRows[0]; + if (!movement) throw new Error("failed to record install/comodato movement"); + + // If there was an active asset-specific reservation, close it as fulfilled. + await client.query( + `UPDATE stock_reservations + SET status = 'fulfilled' + WHERE reference_type = 'serialized_asset' AND reference_id = $1 AND status = 'active'`, + [assetId], + ); + + return { asset: updated, assignment, movement }; + }); +} + +export interface ReturnAssetInput { + /** Warehouse receiving the physical unit back — not necessarily the one it left from (see migration comment on serialized_assets.warehouse_id). */ + warehouseId: string; + notes?: string | null; + createdBy?: string | null; +} + +export class NoOpenAssignmentError extends Error { + constructor(public readonly assetId: string) { + super(`Asset ${assetId} has no open assignment to close`); + this.name = "NoOpenAssignmentError"; + } +} + +/** + * Fecha o assignment aberto (unassigned_at = now()), grava o + * stock_movement return_in no warehouse de destino, e volta o ativo para + * status='in_stock' com current_contract_id limpo. Só permitido a partir + * de installed/comodato. + */ +export async function returnAsset( + assetId: string, + input: ReturnAssetInput, +): Promise<{ asset: SerializedAsset; movement: StockMovement }> { + return withTransaction(async (client) => { + const asset = await getSerializedAssetByIdForUpdate(client, assetId); + if (!asset) throw new AssetNotFoundError(assetId); + if (asset.status !== "installed" && asset.status !== "comodato") { + throw new InvalidAssetTransitionError(asset.status, "returned"); + } + + const { rows: assignmentRows } = await client.query( + `UPDATE asset_assignments SET unassigned_at = now() + WHERE asset_id = $1 AND unassigned_at IS NULL + RETURNING *`, + [assetId], + ); + if (!assignmentRows[0]) throw new NoOpenAssignmentError(assetId); + + const { rows: updatedRows } = await client.query( + `UPDATE serialized_assets + SET status = 'in_stock', warehouse_id = $2, current_contract_id = NULL, updated_by = $3 + WHERE id = $1 + RETURNING *`, + [assetId, input.warehouseId, input.createdBy ?? null], + ); + const updated = updatedRows[0]; + if (!updated) throw new Error(`asset ${assetId} disappeared mid-transaction`); + + const { rows: movementRows } = await client.query( + `INSERT INTO stock_movements (warehouse_id, product_id, movement_type, quantity, reference_type, reference_id, notes, created_by) + VALUES ($1,$2,'return_in',1,'contract',$3,$4,$5) + RETURNING *`, + [input.warehouseId, asset.product_id, asset.current_contract_id, input.notes ?? null, input.createdBy ?? null], + ); + const movement = movementRows[0]; + if (!movement) throw new Error("failed to record return_in movement"); + + return { asset: updated, movement }; + }); +} + +// =========================================================================== +// asset_assignments +// =========================================================================== + +export interface AssetAssignment { + id: string; + created_at: Date; + created_by: string | null; + asset_id: string; + contract_id: string; + assignment_type: "installed" | "comodato"; + assigned_at: Date; + unassigned_at: Date | null; +} + +export async function listAssetAssignments(assetId: string): Promise { + const { rows } = await query( + "SELECT * FROM asset_assignments WHERE asset_id = $1 ORDER BY assigned_at DESC", + [assetId], + ); + return rows; +} + +// =========================================================================== +// stock_reservations +// =========================================================================== + +export type StockReservationStatus = "active" | "released" | "fulfilled"; + +export interface StockReservation { + id: string; + created_at: Date; + updated_at: Date; + created_by: string | null; + warehouse_id: string; + product_id: string; + quantity: string; + reference_type: string | null; + reference_id: string | null; + status: StockReservationStatus; + notes: string | null; +} + +export interface ListStockReservationsFilter { + status?: StockReservationStatus; + warehouseId?: string; + productId?: string; +} + +export async function listStockReservations( + filter: ListStockReservationsFilter = {}, +): Promise { + const conditions: string[] = []; + const params: unknown[] = []; + + if (filter.status) { + params.push(filter.status); + conditions.push(`status = $${params.length}`); + } + if (filter.warehouseId) { + params.push(filter.warehouseId); + conditions.push(`warehouse_id = $${params.length}`); + } + if (filter.productId) { + params.push(filter.productId); + conditions.push(`product_id = $${params.length}`); + } + + const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + const { rows } = await query( + `SELECT * FROM stock_reservations ${where} ORDER BY created_at DESC`, + params, + ); + return rows; +} + +export async function getStockReservationById(id: string): Promise { + const { rows } = await query("SELECT * FROM stock_reservations WHERE id = $1", [id]); + return rows[0] ?? null; +} + +export class ReservationNotFoundError extends Error { + constructor(public readonly reservationId: string) { + super(`Stock reservation ${reservationId} not found`); + this.name = "ReservationNotFoundError"; + } +} + +export class InvalidReservationTransitionError extends Error { + constructor( + public readonly fromStatus: StockReservationStatus, + public readonly action: string, + ) { + super(`Reservation in status '${fromStatus}' cannot be ${action}`); + this.name = "InvalidReservationTransitionError"; + } +} + +export interface CreateGenericReservationInput { + warehouseId: string; + productId: string; + quantity: number; + referenceType?: string | null; + referenceId?: string | null; + notes?: string | null; + createdBy?: string | null; +} + +/** + * Reserva GENÉRICA de quantidade (produto+warehouse), sem serial + * selecionado ainda — o primeiro passo do fluxo descrito no Master + * Prompt §6.8 antes de "posteriormente selecionar unidade serializada + * específica". Distinta de reserveAsset() (que reserva um serial já + * escolhido). + */ +export async function createGenericReservation( + input: CreateGenericReservationInput, +): Promise<{ reservation: StockReservation; movement: StockMovement }> { + return withTransaction(async (client) => { + const { rows: reservationRows } = await client.query( + `INSERT INTO stock_reservations (warehouse_id, product_id, quantity, reference_type, reference_id, status, notes, created_by) + VALUES ($1,$2,$3,$4,$5,'active',$6,$7) + RETURNING *`, + [ + input.warehouseId, + input.productId, + input.quantity, + input.referenceType ?? null, + input.referenceId ?? null, + input.notes ?? null, + input.createdBy ?? null, + ], + ); + const reservation = reservationRows[0]; + if (!reservation) throw new Error("failed to create reservation"); + + const { rows: movementRows } = await client.query( + `INSERT INTO stock_movements (warehouse_id, product_id, movement_type, quantity, reference_type, reference_id, notes, created_by) + VALUES ($1,$2,'reservation',$3,$4,$5,$6,$7) + RETURNING *`, + [ + input.warehouseId, + input.productId, + input.quantity, + input.referenceType ?? null, + input.referenceId ?? null, + input.notes ?? null, + input.createdBy ?? null, + ], + ); + const movement = movementRows[0]; + if (!movement) throw new Error("failed to record reservation movement"); + + return { reservation, movement }; + }); +} + +/** + * Cancela uma reserva ATIVA (status -> released) e grava o + * reservation_release correspondente. Se a reserva era de um ativo + * específico (reference_type='serialized_asset'), também devolve esse + * ativo para 'in_stock' — desfazendo o que reserveAsset() fez. + */ +export async function releaseReservation( + reservationId: string, + actorId: string | null, +): Promise<{ reservation: StockReservation; movement: StockMovement }> { + return withTransaction(async (client) => { + const { rows: currentRows } = await client.query( + "SELECT * FROM stock_reservations WHERE id = $1 FOR UPDATE", + [reservationId], + ); + const current = currentRows[0]; + if (!current) throw new ReservationNotFoundError(reservationId); + if (current.status !== "active") { + throw new InvalidReservationTransitionError(current.status, "released"); + } + + const { rows: updatedRows } = await client.query( + `UPDATE stock_reservations SET status = 'released' WHERE id = $1 RETURNING *`, + [reservationId], + ); + const updated = updatedRows[0]; + if (!updated) throw new Error(`reservation ${reservationId} disappeared mid-transaction`); + + const { rows: movementRows } = await client.query( + `INSERT INTO stock_movements (warehouse_id, product_id, movement_type, quantity, reference_type, reference_id, notes, created_by) + VALUES ($1,$2,'reservation_release',$3,$4,$5,$6,$7) + RETURNING *`, + [ + current.warehouse_id, + current.product_id, + current.quantity, + current.reference_type, + current.reference_id, + null, + actorId, + ], + ); + const movement = movementRows[0]; + if (!movement) throw new Error("failed to record reservation_release movement"); + + if (current.reference_type === "serialized_asset" && current.reference_id) { + await client.query( + `UPDATE serialized_assets SET status = 'in_stock', updated_by = $2 + WHERE id = $1 AND status = 'reserved'`, + [current.reference_id, actorId], + ); + } + + return { reservation: updated, movement }; + }); +} diff --git a/packages/database/src/repositories/support-tickets.ts b/packages/database/src/repositories/support-tickets.ts new file mode 100644 index 0000000..c922458 --- /dev/null +++ b/packages/database/src/repositories/support-tickets.ts @@ -0,0 +1,423 @@ +import { query, withTransaction } from "../pool.js"; + +/** + * Support/Service Desk bounded context (Master Prompt §6.13). Greenfield — + * no legacy module to port. See the migration's header comment + * (PENDING_support.cjs) for the full list of tables this MVP builds vs. + * explicitly deferred ones (real S3 attachments, CSAT, dedicated work + * order entity, routing queues, configurable attendance calendar). + * + * Mirrors contracts.ts's conventions closely: withTransaction + SELECT + * ... FOR UPDATE for the status-machine transition, an explicit + * transitions table, a NotFound/InvalidTransition error pair the service + * layer maps to 404/409, and a *WithDetails aggregate for the "get one" + * endpoint. + * + * SLA breach/elapsed-time math (computeElapsedSeconds/isSlaBreached) is + * deliberately NOT implemented in this file — it's pure, DB-independent + * business logic that lives in apps/api/src/support/sla.ts (this package + * must not depend on apps/api). This repository only performs the SQL + * side effect of pausing/resuming the timer (using the DB's own `now()`, + * to avoid app/DB clock skew for that specific bookkeeping) and persists + * whatever met/unmet flags the service layer has already computed. + */ + +export type TicketStatus = + | "new" + | "triage" + | "in_progress" + | "waiting_customer" + | "waiting_third_party" + | "resolved" + | "closed" + | "cancelled"; + +export type TicketPriority = "low" | "medium" | "high" | "critical"; + +export interface SupportTicket { + id: string; + created_at: Date; + updated_at: Date; + created_by: string | null; + updated_by: string | null; + protocol_number: string; + title: string; + description: string; + category: string; + subcategory: string | null; + priority: TicketPriority; + status: TicketStatus; + reseller_id: string | null; + client_registration_id: string | null; + contract_id: string | null; + owner_user_id: string | null; + opened_by_user_id: string; + root_cause: string | null; + resolution_notes: string | null; + first_response_at: Date | null; + resolved_at: Date | null; + closed_at: Date | null; +} + +export interface TicketComment { + id: string; + ticket_id: string; + author_user_id: string; + body: string; + is_internal: boolean; + created_at: Date; +} + +export interface TicketStatusHistoryEntry { + id: string; + ticket_id: string; + from_status: TicketStatus | null; + to_status: TicketStatus; + reason: string | null; + changed_by: string | null; + changed_at: Date; +} + +export interface SlaTimer { + ticket_id: string; + created_at: Date; + updated_at: Date; + first_response_due_at: Date; + resolution_due_at: Date; + first_response_met: boolean | null; + resolution_met: boolean | null; + paused_since: Date | null; + accumulated_pause_seconds: number; +} + +export interface TicketWithDetails extends SupportTicket { + comments: TicketComment[]; + statusHistory: TicketStatusHistoryEntry[]; + slaTimer: SlaTimer | null; +} + +/** + * MVP lifecycle (eden-support persona spec): new -> triage -> in_progress + * <-> {waiting_customer, waiting_third_party} -> resolved -> closed, plus + * cancelled reachable from any non-terminal state. closed/cancelled are + * terminal (no outgoing transitions modeled). + */ +export const TICKET_STATUS_TRANSITIONS: Record = { + new: ["triage", "cancelled"], + triage: ["in_progress", "cancelled"], + in_progress: ["waiting_customer", "waiting_third_party", "resolved", "cancelled"], + waiting_customer: ["in_progress", "cancelled"], + waiting_third_party: ["in_progress", "cancelled"], + resolved: ["closed", "cancelled"], + closed: [], + cancelled: [], +}; + +export class TicketNotFoundError extends Error { + constructor(public readonly ticketId: string) { + super(`Support ticket ${ticketId} not found`); + this.name = "TicketNotFoundError"; + } +} + +export class InvalidTicketTransitionError extends Error { + constructor( + public readonly fromStatus: TicketStatus, + public readonly toStatus: TicketStatus, + ) { + super(`Invalid ticket status transition: '${fromStatus}' -> '${toStatus}'`); + this.name = "InvalidTicketTransitionError"; + } +} + +export interface ListTicketsFilter { + scope: "all" | "reseller"; + /** Required (and enforced) when scope === 'reseller'. Ignored when scope === 'all'. */ + resellerId?: string | null; +} + +export async function listTickets(filter: ListTicketsFilter): Promise { + if (filter.scope === "reseller") { + // Deliberately never falls back to an unfiltered query — deny-by-default, + // same discipline as contracts.ts::listContracts. + const { rows } = await query( + "SELECT * FROM support_tickets WHERE reseller_id = $1 ORDER BY created_at DESC", + [filter.resellerId ?? null], + ); + return rows; + } + const { rows } = await query("SELECT * FROM support_tickets ORDER BY created_at DESC"); + return rows; +} + +async function listTicketComments(ticketId: string): Promise { + const { rows } = await query( + "SELECT * FROM ticket_comments WHERE ticket_id = $1 ORDER BY created_at ASC", + [ticketId], + ); + return rows; +} + +async function listTicketStatusHistory(ticketId: string): Promise { + const { rows } = await query( + "SELECT * FROM ticket_status_history WHERE ticket_id = $1 ORDER BY changed_at ASC", + [ticketId], + ); + return rows; +} + +async function getSlaTimer(ticketId: string): Promise { + const { rows } = await query("SELECT * FROM sla_timers WHERE ticket_id = $1", [ticketId]); + return rows[0] ?? null; +} + +export async function getTicketById(id: string): Promise { + const { rows } = await query("SELECT * FROM support_tickets WHERE id = $1", [id]); + const ticket = rows[0]; + if (!ticket) return null; + + const [comments, statusHistory, slaTimer] = await Promise.all([ + listTicketComments(id), + listTicketStatusHistory(id), + getSlaTimer(id), + ]); + + return { ...ticket, comments, statusHistory, slaTimer }; +} + +export interface CreateTicketInput { + title: string; + description: string; + category: string; + subcategory?: string | null; + priority: TicketPriority; + resellerId?: string | null; + clientRegistrationId?: string | null; + contractId?: string | null; + ownerUserId?: string | null; + openedByUserId: string; + createdBy?: string | null; + /** Server-computed by the caller (apps/api/src/support/sla.ts) — never accepted raw from a client. */ + firstResponseDueAt: Date; + /** Server-computed by the caller (apps/api/src/support/sla.ts) — never accepted raw from a client. */ + resolutionDueAt: Date; +} + +/** + * Creates the ticket plus its 1:1 sla_timers row in a single transaction + * (all-or-nothing) — mirrors contracts.ts::createContract. Status always + * starts 'new'; protocol_number is allocated by the column DEFAULT + * (support_tickets_protocol_number_seq), same pattern as quotes.quote_number. + */ +export async function createTicket(input: CreateTicketInput): Promise { + return withTransaction(async (client) => { + const { rows } = await client.query( + `INSERT INTO support_tickets ( + title, description, category, subcategory, priority, + reseller_id, client_registration_id, contract_id, + owner_user_id, opened_by_user_id, created_by + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) + RETURNING *`, + [ + input.title, + input.description, + input.category, + input.subcategory ?? null, + input.priority, + input.resellerId ?? null, + input.clientRegistrationId ?? null, + input.contractId ?? null, + input.ownerUserId ?? null, + input.openedByUserId, + input.createdBy ?? null, + ], + ); + const ticket = rows[0]; + if (!ticket) throw new Error("failed to create support ticket"); + + const { rows: timerRows } = await client.query( + `INSERT INTO sla_timers (ticket_id, first_response_due_at, resolution_due_at) + VALUES ($1,$2,$3) + RETURNING *`, + [ticket.id, input.firstResponseDueAt, input.resolutionDueAt], + ); + const slaTimer = timerRows[0]; + if (!slaTimer) throw new Error("failed to create sla timer"); + + return { ...ticket, comments: [], statusHistory: [], slaTimer }; + }); +} + +export interface CreateTicketCommentInput { + authorUserId: string; + body: string; + isInternal: boolean; +} + +export async function addTicketComment( + ticketId: string, + input: CreateTicketCommentInput, +): Promise { + const { rows } = await query( + `INSERT INTO ticket_comments (ticket_id, author_user_id, body, is_internal) + VALUES ($1,$2,$3,$4) + RETURNING *`, + [ticketId, input.authorUserId, input.body, input.isInternal], + ); + const row = rows[0]; + if (!row) throw new Error("failed to add ticket comment"); + return row; +} + +/** + * Once the first public (non-internal) comment is added on a ticket whose + * first_response_at is still NULL, the service layer computes whether + * that response met the SLA (apps/api/src/support/sla.ts::isSlaBreached) + * and calls this to persist it. Idempotent by construction (WHERE ... IS + * NULL on both tables) — a second call after first_response_at is already + * set is a silent no-op, never overwrites. + */ +export async function recordFirstResponse( + ticketId: string, + met: boolean, + respondedAt: Date, +): Promise { + await query( + `UPDATE support_tickets SET first_response_at = $2 WHERE id = $1 AND first_response_at IS NULL`, + [ticketId, respondedAt], + ); + await query( + `UPDATE sla_timers SET first_response_met = $2, updated_at = now() + WHERE ticket_id = $1 AND first_response_met IS NULL`, + [ticketId, met], + ); +} + +/** + * Called by the service layer right after a transition into 'resolved', + * once it has computed the met/unmet flag from the (already-returned, + * necessarily-unpaused — see updateTicketStatus below) sla timer via + * sla.ts::isSlaBreached. + */ +export async function recordResolutionMet(ticketId: string, met: boolean): Promise { + await query(`UPDATE sla_timers SET resolution_met = $2, updated_at = now() WHERE ticket_id = $1`, [ + ticketId, + met, + ]); +} + +export interface UpdateTicketStatusExtra { + rootCause?: string | null; + resolutionNotes?: string | null; +} + +/** + * Validates the transition against TICKET_STATUS_TRANSITIONS and, only if + * valid, atomically: updates support_tickets.status, sets resolved_at/ + * closed_at via COALESCE the first time the ticket enters that state + * (never overwritten afterwards, same semantics as contracts.activated_at), + * accepts root_cause/resolution_notes only on the transition into + * 'closed', appends a ticket_status_history row, AND pauses/resumes the + * SLA timer: + * - entering waiting_customer/waiting_third_party -> paused_since = now() + * (COALESCE — a no-op if somehow already paused) + * - waiting_customer/waiting_third_party -> in_progress -> the elapsed + * pause is folded into accumulated_pause_seconds and paused_since is + * cleared + * All using the DB's own now() for this specific bookkeeping (single + * statement, no app/DB clock skew risk). Returns the updated ticket AND + * the post-transition sla timer row so the service layer can compute + * first_response_met/resolution_met with the pure functions in + * apps/api/src/support/sla.ts. Throws TicketNotFoundError / + * InvalidTicketTransitionError instead of silently no-oping — the service + * layer maps these to 404/409. + */ +export async function updateTicketStatus( + id: string, + toStatus: TicketStatus, + reason: string | null, + actorId: string | null, + extra?: UpdateTicketStatusExtra, +): Promise<{ ticket: SupportTicket; slaTimer: SlaTimer }> { + return withTransaction(async (client) => { + const { rows } = await client.query<{ status: TicketStatus }>( + "SELECT status FROM support_tickets WHERE id = $1 FOR UPDATE", + [id], + ); + const current = rows[0]; + if (!current) { + throw new TicketNotFoundError(id); + } + + const fromStatus = current.status; + const allowed = TICKET_STATUS_TRANSITIONS[fromStatus] ?? []; + if (!allowed.includes(toStatus)) { + throw new InvalidTicketTransitionError(fromStatus, toStatus); + } + + const enteringWaiting = toStatus === "waiting_customer" || toStatus === "waiting_third_party"; + const resumingFromWaiting = + (fromStatus === "waiting_customer" || fromStatus === "waiting_third_party") && toStatus === "in_progress"; + const enteringResolved = toStatus === "resolved"; + const enteringClosed = toStatus === "closed"; + + // Lock the timer row too (1:1, always exists once the ticket does) and + // apply the pause/resume side effect. All SET expressions in a single + // UPDATE see the row's OLD values, so referencing paused_since while + // also overwriting it in the same statement is safe standard SQL. + await client.query( + `UPDATE sla_timers SET + accumulated_pause_seconds = CASE + WHEN $3 AND paused_since IS NOT NULL + THEN accumulated_pause_seconds + GREATEST(0, EXTRACT(EPOCH FROM (now() - paused_since))::int) + ELSE accumulated_pause_seconds + END, + paused_since = CASE + WHEN $2 THEN COALESCE(paused_since, now()) + WHEN $3 THEN NULL + ELSE paused_since + END, + updated_at = now() + WHERE ticket_id = $1`, + [id, enteringWaiting, resumingFromWaiting], + ); + + const { rows: updatedRows } = await client.query( + `UPDATE support_tickets SET + status = $2, + resolved_at = CASE WHEN $3 THEN COALESCE(resolved_at, now()) ELSE resolved_at END, + closed_at = CASE WHEN $4 THEN COALESCE(closed_at, now()) ELSE closed_at END, + root_cause = CASE WHEN $4 THEN COALESCE($5, root_cause) ELSE root_cause END, + resolution_notes = CASE WHEN $4 THEN COALESCE($6, resolution_notes) ELSE resolution_notes END, + updated_by = $7 + WHERE id = $1 + RETURNING *`, + [ + id, + toStatus, + enteringResolved, + enteringClosed, + extra?.rootCause ?? null, + extra?.resolutionNotes ?? null, + actorId ?? null, + ], + ); + const ticket = updatedRows[0]; + if (!ticket) throw new Error(`support ticket ${id} disappeared mid-transaction`); + + await client.query( + `INSERT INTO ticket_status_history (ticket_id, from_status, to_status, reason, changed_by) + VALUES ($1,$2,$3,$4,$5)`, + [id, fromStatus, toStatus, reason ?? null, actorId ?? null], + ); + + const { rows: timerRows } = await client.query( + "SELECT * FROM sla_timers WHERE ticket_id = $1", + [id], + ); + const slaTimer = timerRows[0]; + if (!slaTimer) throw new Error(`sla timer for ticket ${id} missing`); + + return { ticket, slaTimer }; + }); +} diff --git a/packages/database/src/repositories/users.ts b/packages/database/src/repositories/users.ts index 1770ecf..aa7cd1b 100644 --- a/packages/database/src/repositories/users.ts +++ b/packages/database/src/repositories/users.ts @@ -142,3 +142,25 @@ export async function assignUserToReseller( ]); return (rowCount ?? 0) > 0; } + +/** + * Stages a TOTP secret without enabling MFA yet — `mfa_enabled` stays + * false until `confirmMfaEnrollment` verifies a code against it. Login + * only checks `mfa_enabled`, so an unconfirmed enrollment is inert. + */ +export async function setMfaSecret(userId: string, encryptedSecret: string): Promise { + await query("UPDATE users SET mfa_secret_encrypted = $2, mfa_enabled = false WHERE id = $1", [ + userId, + encryptedSecret, + ]); +} + +export async function confirmMfaEnrollment(userId: string): Promise { + await query("UPDATE users SET mfa_enabled = true WHERE id = $1", [userId]); +} + +export async function disableMfa(userId: string): Promise { + await query("UPDATE users SET mfa_enabled = false, mfa_secret_encrypted = null WHERE id = $1", [ + userId, + ]); +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 417714c..5d401f8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,10 +1,20 @@ { "name": "@eden/ui", - "version": "0.1.0", + "version": "0.2.0", "private": true, "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./tailwind-preset": { + "types": "./dist/tailwind-preset.d.ts", + "default": "./dist/tailwind-preset.js" + } + }, "scripts": { "build": "tsc -p tsconfig.json", "typecheck": "tsc -p tsconfig.json --noEmit", @@ -15,6 +25,7 @@ }, "devDependencies": { "@types/react": "^18.3.18", + "tailwindcss": "^3.4.17", "typescript": "^5.7.3" } } diff --git a/packages/ui/src/components/Alert.tsx b/packages/ui/src/components/Alert.tsx new file mode 100644 index 0000000..2518ff5 --- /dev/null +++ b/packages/ui/src/components/Alert.tsx @@ -0,0 +1,29 @@ +import type { HTMLAttributes, ReactNode } from "react"; +import { cn } from "../utils/cn.js"; + +export type AlertVariant = "info" | "success" | "warning" | "error"; + +const variantClasses: Record = { + info: "bg-info-transparent border-info-200 text-info-900", + success: "bg-success-transparent border-success-200 text-success-900", + warning: "bg-warning-transparent border-warning-200 text-warning-900", + error: "bg-danger-transparent border-danger-200 text-danger-900", +}; + +export interface AlertProps extends Omit, "title"> { + variant?: AlertVariant; + title?: ReactNode; +} + +export function Alert({ variant = "info", title, className, children, role, ...rest }: AlertProps) { + return ( +
+ {title ?

{title}

: null} + {children ?
{children}
: null} +
+ ); +} diff --git a/packages/ui/src/components/AppShell.tsx b/packages/ui/src/components/AppShell.tsx new file mode 100644 index 0000000..5511d75 --- /dev/null +++ b/packages/ui/src/components/AppShell.tsx @@ -0,0 +1,120 @@ +import type { ComponentType, ReactNode } from "react"; +import { cn } from "../utils/cn.js"; + +export interface AppShellNavItem { + key: string; + label: string; + href: string; + icon?: ReactNode; + active?: boolean; +} + +interface NavLinkRenderProps { + href: string; + className?: string; + children: ReactNode; +} + +export interface AppShellProps { + /** e.g. "EDEN Core" — shown next to the logo and used as the img alt text. */ + appName: string; + logoSrc?: string; + navItems: AppShellNavItem[]; + userName?: string; + userRole?: string; + onLogout?: () => void; + children: ReactNode; + /** + * Router-agnostic navigation: packages/ui doesn't depend on react-router. + * Defaults to a plain `
` (full page reload); pass e.g. + * `({ href, className, children }) => {children}` + * from the consuming app to get client-side routing. + */ + LinkComponent?: ComponentType; +} + +function DefaultLink({ href, className, children }: NavLinkRenderProps) { + return ( + + {children} + + ); +} + +/** + * Visually inspired by the DreamsERP theme's sidebar/topbar + * (react/src/components/common-sidebar, common-header) — sidebar with + * logo + dark active nav item, white topbar with a bordered bottom edge — + * without importing any of the theme's Redux/routing code. + */ +export function AppShell({ + appName, + logoSrc, + navItems, + userName, + userRole, + onLogout, + children, + LinkComponent = DefaultLink, +}: AppShellProps) { + return ( +
+ + +
+
+
+ {logoSrc ? {appName} : null} + {appName} +
+
+ {userName ? ( +
+ + {userName.charAt(0).toUpperCase()} + +
+

{userName}

+ {userRole ?

{userRole}

: null} +
+
+ ) : null} + {onLogout ? ( + + ) : null} +
+
+ +
{children}
+
+
+ ); +} diff --git a/packages/ui/src/components/Badge.tsx b/packages/ui/src/components/Badge.tsx new file mode 100644 index 0000000..69ef8a1 --- /dev/null +++ b/packages/ui/src/components/Badge.tsx @@ -0,0 +1,33 @@ +import type { HTMLAttributes } from "react"; +import { cn } from "../utils/cn.js"; + +export type BadgeVariant = "primary" | "secondary" | "success" | "warning" | "danger" | "info" | "neutral"; + +const variantClasses: Record = { + primary: "bg-primary-transparent text-primary-900", + secondary: "bg-secondary-transparent text-secondary-900", + success: "bg-success-transparent text-success-900", + warning: "bg-warning-transparent text-warning-900", + danger: "bg-danger-transparent text-danger-900", + info: "bg-info-transparent text-info-900", + neutral: "bg-light text-gray-900", +}; + +export interface BadgeProps extends HTMLAttributes { + variant?: BadgeVariant; +} + +export function Badge({ variant = "neutral", className, children, ...rest }: BadgeProps) { + return ( + + {children} + + ); +} diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx new file mode 100644 index 0000000..d7fbfa2 --- /dev/null +++ b/packages/ui/src/components/Button.tsx @@ -0,0 +1,54 @@ +import { forwardRef } from "react"; +import type { ButtonHTMLAttributes } from "react"; +import { cn } from "../utils/cn.js"; + +export type ButtonVariant = "primary" | "secondary" | "danger" | "ghost"; +export type ButtonSize = "sm" | "md"; + +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: ButtonVariant; + size?: ButtonSize; + /** Shows an inline spinner and disables the button — for in-flight submits (e.g. login). */ + loading?: boolean; +} + +const variantClasses: Record = { + // Theme source (login.tsx) uses a dark CTA that hovers to the primary-hover + // shade, not a flat primary fill — kept here as-is. + primary: "bg-dark text-white hover:bg-primary-hover", + secondary: "bg-white text-title border border-border-color hover:bg-light", + danger: "bg-danger text-white hover:bg-danger-hover", + ghost: "bg-transparent text-default hover:bg-light", +}; + +const sizeClasses: Record = { + sm: "text-xs px-3 py-1.5", + md: "text-sm px-4 py-2.5", +}; + +export const Button = forwardRef(function Button( + { variant = "primary", size = "md", loading = false, disabled, className, children, ...rest }, + ref, +) { + return ( + + ); +}); diff --git a/packages/ui/src/components/Card.tsx b/packages/ui/src/components/Card.tsx new file mode 100644 index 0000000..18d3025 --- /dev/null +++ b/packages/ui/src/components/Card.tsx @@ -0,0 +1,22 @@ +import type { HTMLAttributes, ReactNode } from "react"; +import { cn } from "../utils/cn.js"; + +export interface CardProps extends Omit, "title"> { + title?: ReactNode; + actions?: ReactNode; +} + +/** Theme source (auth-pages/login.tsx): `bg-white border border-border-color rounded-md shadow-sm`. */ +export function Card({ title, actions, className, children, ...rest }: CardProps) { + return ( +
+ {title || actions ? ( +
+ {title ?

{title}

: } + {actions ?
{actions}
: null} +
+ ) : null} +
{children}
+
+ ); +} diff --git a/packages/ui/src/components/Input.tsx b/packages/ui/src/components/Input.tsx new file mode 100644 index 0000000..618f17f --- /dev/null +++ b/packages/ui/src/components/Input.tsx @@ -0,0 +1,50 @@ +import { forwardRef, useId } from "react"; +import type { InputHTMLAttributes } from "react"; +import { cn } from "../utils/cn.js"; + +export interface InputProps extends InputHTMLAttributes { + label?: string; + /** Validation/server error message — shown below the field, field border turns danger. */ + error?: string; + /** Helper text shown below the field when there's no error. */ + hint?: string; +} + +export const Input = forwardRef(function Input( + { label, error, hint, id, className, ...rest }, + ref, +) { + const generatedId = useId(); + const inputId = id ?? generatedId; + + return ( +
+ {label ? ( + + ) : null} + + {error ? ( +

+ {error} +

+ ) : hint ? ( +

+ {hint} +

+ ) : null} +
+ ); +}); diff --git a/packages/ui/src/components/PageHeader.tsx b/packages/ui/src/components/PageHeader.tsx new file mode 100644 index 0000000..ccc981e --- /dev/null +++ b/packages/ui/src/components/PageHeader.tsx @@ -0,0 +1,20 @@ +import type { HTMLAttributes, ReactNode } from "react"; +import { cn } from "../utils/cn.js"; + +export interface PageHeaderProps extends Omit, "title"> { + title: ReactNode; + description?: ReactNode; + actions?: ReactNode; +} + +export function PageHeader({ title, description, actions, className, ...rest }: PageHeaderProps) { + return ( +
+
+

{title}

+ {description ?

{description}

: null} +
+ {actions ?
{actions}
: null} +
+ ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index d7b0a9d..d0966ae 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,7 +1,31 @@ /** * EDEN Design System, derived from the purchased DreamsERP theme - * (tema_do_Eden.zip) — no components ported yet. See + * (tema_do_Eden.zip → dreamserp-v1.0.1/react.zip). See * docs/project-inventory.md §2 and the eden-frontend agent - * (.claude/agents/eden-frontend.md) for the extraction plan. + * (.claude/agents/eden-frontend.md) for the extraction plan and + * `./tailwind-preset` for the extracted visual tokens. */ -export const UI_PACKAGE_VERSION = "0.1.0"; +export const UI_PACKAGE_VERSION = "0.2.0"; + +export { Button } from "./components/Button.js"; +export type { ButtonProps, ButtonVariant, ButtonSize } from "./components/Button.js"; + +export { Input } from "./components/Input.js"; +export type { InputProps } from "./components/Input.js"; + +export { Card } from "./components/Card.js"; +export type { CardProps } from "./components/Card.js"; + +export { Badge } from "./components/Badge.js"; +export type { BadgeProps, BadgeVariant } from "./components/Badge.js"; + +export { Alert } from "./components/Alert.js"; +export type { AlertProps, AlertVariant } from "./components/Alert.js"; + +export { PageHeader } from "./components/PageHeader.js"; +export type { PageHeaderProps } from "./components/PageHeader.js"; + +export { AppShell } from "./components/AppShell.js"; +export type { AppShellProps, AppShellNavItem } from "./components/AppShell.js"; + +export { cn } from "./utils/cn.js"; diff --git a/packages/ui/src/tailwind-preset.ts b/packages/ui/src/tailwind-preset.ts new file mode 100644 index 0000000..df993e7 --- /dev/null +++ b/packages/ui/src/tailwind-preset.ts @@ -0,0 +1,231 @@ +/** + * EDEN Tailwind preset — visual tokens extracted from the purchased + * DreamsERP theme (tema_do_Eden.zip → dreamserp-v1.0.1/react.zip → + * react/src/style/css/style.css, Tailwind v4 `@theme` block). + * + * The theme itself ships as Tailwind v4 (CSS-first `@theme`), but our apps + * are on Tailwind v3, so the same token values are re-expressed here as a + * plain JS/TS preset consumed via `presets: [require("@eden/ui/tailwind-preset")]` + * (or the ESM equivalent) in each app's own tailwind.config.js. + * + * Do not hand-edit color hexes without re-checking the source file — these + * are copied verbatim, not invented. + */ +import type { Config } from "tailwindcss"; + +type PresetTheme = Config["theme"]; + +const withHover = (base: Record, hover: string) => ({ + ...base, + hover, +}); + +const primary = withHover( + { + DEFAULT: "#0F766E", + 100: "#D7E5E3", + 200: "#C3D8D5", + 300: "#AFCCC8", + 400: "#9BBFBA", + 500: "#88B3AD", + 600: "#74A6A0", + 700: "#5F9A93", + 800: "#4A8E87", + 900: "#32827A", + transparent: "#EBF2F1", + }, + "#0C6760", +); + +const secondary = withHover( + { + DEFAULT: "#2563EB", + 100: "#D6E4FF", + 200: "#C2D7FE", + 300: "#AEC9FD", + 400: "#9ABCFB", + 500: "#87AEF9", + 600: "#74A0F7", + 700: "#6092F5", + 800: "#4D83F2", + 900: "#3973EE", + transparent: "#EAF2FF", + }, + "#1F56CF", +); + +const success = withHover( + { + DEFAULT: "#059669", + 100: "#DAECE2", + 200: "#C7E2D4", + 300: "#B4D9C6", + 400: "#A1CFB8", + 500: "#8EC6AB", + 600: "#7BBC9D", + 700: "#66B390", + 800: "#50A983", + 900: "#36A076", + transparent: "#ECF5F1", + }, + "#04845C", +); + +const warning = withHover( + { + DEFAULT: "#D97706", + 100: "#FAE7D9", + 200: "#F8DBC6", + 300: "#F5CFB2", + 400: "#F1C29F", + 500: "#EEB68C", + 600: "#EAAA78", + 700: "#E69D64", + 800: "#E2914D", + 900: "#DE8434", + transparent: "#FDF3EC", + }, + "#BF6805", +); + +const danger = withHover( + { + DEFAULT: "#B91C1C", + 100: "#F8D9D5", + 200: "#F3C6C0", + 300: "#EEB4AC", + 400: "#EBA198", + 500: "#E28E84", + 600: "#DB7B70", + 700: "#D3675C", + 800: "#CB5248", + 900: "#C23B33", + transparent: "#FCECEA", + }, + "#A31717", +); + +const info = withHover( + { + DEFAULT: "#0EA5E9", + 100: "#DCEFFC", + 200: "#CBE7FB", + 300: "#B9DFF9", + 400: "#A7D7F7", + 500: "#94CFF5", + 600: "#81C7F3", + 700: "#6DBFF1", + 800: "#57B6EE", + 900: "#3CAEEC", + transparent: "#EEF7FE", + }, + "#0B91CD", +); + +const gray = withHover( + { + 100: "#D1D4D8", + 200: "#BBBFC5", + 300: "#A5AAB3", + 400: "#9096A1", + 500: "#7B838F", + 600: "#67707D", + 700: "#545D6C", + 800: "#414B5B", + 900: "#2F3A4B", + transparent: "#E8E9EC", + }, + "#192333", +); + +const light = withHover( + { + DEFAULT: "#F8F9FA", + 100: "#FAFBFD", + 200: "#F6F8FB", + 300: "#F3F4F6", + 400: "#F3F6F9", + 500: "#F1F4F7", + 600: "#EEF2F6", + 700: "#ECF1F6", + 800: "#E9EFF3", + 900: "#E7EDF2", + transparent: "#FDFDFE", + }, + "#DBDCDE", +); + +export const edenColors = { + primary, + secondary, + success, + warning, + danger, + info, + gray, + light, + dark: "#1E293B", + white: "#FFFFFF", + black: "#000000", + orange: { DEFAULT: "#E65100", transparent: "#FFF0EB" }, + purple: { DEFAULT: "#6A1B9A", transparent: "#F1EBF7" }, + pink: { DEFAULT: "#CC25B0", transparent: "#FDEDF8" }, + indigo: { DEFAULT: "#303F9F", transparent: "#EAEDF7" }, + cyan: { DEFAULT: "#0891B2", transparent: "#EBF3F6" }, + teal: { DEFAULT: "#0D9488", transparent: "#EBF2F1" }, + default: "#67707D", + title: "#2F3A4B", + "border-color": "#E8E9EC", + skeleton: "#E4E7EB", +} satisfies Record; + +/** Sidebar/topbar layout tokens (`--sidenav-width`, `--topbar-height`, …). */ +export const edenLayout = { + sidebarWidth: "242px", + sidebarMiniWidth: "56px", + sidebarExpandMiniWidth: "80px", + topbarHeight: "56px", +}; + +const edenTheme: PresetTheme = { + extend: { + colors: edenColors, + fontFamily: { + // Theme source imports Google Fonts "Geist" as --font-family-primary. + // (Older EDEN docs referenced Nunito/Poppins for a prior theme + // revision — confirmed against the actual shipped tailwind/style.css + // that this is the current value; update here if the theme changes.) + sans: ["Geist", "ui-sans-serif", "system-ui", "-apple-system", "Segoe UI", "Roboto", "sans-serif"], + }, + borderRadius: { + xs: "2px", + sm: "4px", + md: "8px", + xl: "12px", + }, + boxShadow: { + DEFAULT: "0px 1px 4px 0px rgba(0, 0, 0, 0.05)", + sm: "0px 4px 4px 0px rgba(219, 219, 219, 0.25)", + lg: "0px 4px 30px 0px rgba(20, 20, 20, 0.08)", + primary: "0px 4px 10px 0px rgba(46, 55, 164, 0.1)", + }, + spacing: { + sidebar: edenLayout.sidebarWidth, + "sidebar-mini": edenLayout.sidebarMiniWidth, + topbar: edenLayout.topbarHeight, + }, + backgroundImage: { + "primary-gradient": "linear-gradient(90deg, #0F766E 0%, #059669 100%)", + "dark-gradient": "linear-gradient(90deg, #1E293B 0%, #0F172A 100%)", + }, + }, +}; + +// A preset intentionally omits `content` (each consuming app supplies its +// own) — Tailwind's `Config` type marks it required, so we type this as a +// partial config rather than a full `Config`. +const edenPreset: Partial = { + theme: edenTheme, +}; + +export default edenPreset; diff --git a/packages/ui/src/utils/cn.ts b/packages/ui/src/utils/cn.ts new file mode 100644 index 0000000..991c355 --- /dev/null +++ b/packages/ui/src/utils/cn.ts @@ -0,0 +1,4 @@ +/** Joins class name fragments, skipping falsy values. No `clsx` dependency needed for this. */ +export function cn(...parts: Array): string { + return parts.filter(Boolean).join(" "); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94fa0b4..97bfa4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: jsonwebtoken: specifier: ^9.0.2 version: 9.0.3 + otplib: + specifier: ^13.5.0 + version: 13.5.0 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -99,12 +102,18 @@ importers: apps/core-web: dependencies: + '@eden/ui': + specifier: workspace:* + version: link:../../packages/ui react: specifier: ^18.3.1 version: 18.3.1 react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^6.28.1 + version: 6.30.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: '@types/react': specifier: ^18.3.18 @@ -305,6 +314,9 @@ importers: '@types/react': specifier: ^18.3.18 version: 18.3.31 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.19(tsx@4.23.13) typescript: specifier: ^5.7.3 version: 5.9.3 @@ -1088,6 +1100,10 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@2.4.0': + resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1105,6 +1121,24 @@ packages: engines: {node: '>=8.0.0', npm: '>=5.0.0'} hasBin: true + '@otplib/core@13.5.0': + resolution: {integrity: sha512-2rURdkYkb3BDhMs3j/oCCPTve1ybJ6ruLfLfSe1ZSPV+y6RFTbLfAuT0m0ZCnps88ogkIq9t/+Li/kg5RofQwQ==} + + '@otplib/hotp@13.5.0': + resolution: {integrity: sha512-1EwwAti05CeWJn4xXOVMBK9N6dIJlJw/cQkQyiL9OVlkeS452wAve3ffodi+5IUZdWoF1wxVug38RisdxDqDWw==} + + '@otplib/plugin-base32-scure@13.5.0': + resolution: {integrity: sha512-3JEIHindUMiIeNL0jXepSAkZ/7IkWq4sPdG9eXK0lrMXZG9RdKu5oz/4tuuTodtsFUPYhKlRMqv5xUvQIkMIaA==} + + '@otplib/plugin-crypto-noble@13.5.0': + resolution: {integrity: sha512-fihOAGFvc4b8XTKyIK3jifFP2nLrUNc2bOaJ3UkUKJjy1XI2FbHKG0WmOH+jFsPfa6VsOnouCnEmSznDxIe0pA==} + + '@otplib/totp@13.5.0': + resolution: {integrity: sha512-GD9LzQnbHDwXCp79s8AaiA5cJR6luX5fEr9AN32AWx4ooRvllPbMkG80gHSA0jbGCnAIX/ChyOtTZ8tgJHMkSg==} + + '@otplib/uri@13.5.0': + resolution: {integrity: sha512-LsL1hqTEgJHY40U2eb/Qp6OR1tKkNGwHQTrqqjIuJj0cawGWkLEmeUFK1MHzWBQ4bgy5IdeB5Et7gIDSAH/CRw==} + '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -1112,6 +1146,10 @@ packages: resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} engines: {node: '>=10'} + '@remix-run/router@1.23.4': + resolution: {integrity: sha512-q7j5geK7xs3UJSdm9/iytUNclBnLmYx1EnSeCFXHPeutdqgIMeFeHtUZgS3EhlKxdBEAu8OwtJCwmLrEzpSs7Q==} + engines: {node: '>=14.0.0'} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1265,6 +1303,9 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@scure/base@2.4.0': + resolution: {integrity: sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg==} + '@smithy/core@3.33.3': resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==} engines: {node: '>=18.0.0'} @@ -2286,6 +2327,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + otplib@13.5.0: + resolution: {integrity: sha512-RpcC6aq4rANX6MverMuU3pqHVLgMPO7vl6qR8ga7YzoEHlPJknJ58cSVAaf3jBYnasiQVQHXXl2mj/4pYlsVEQ==} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -2461,6 +2505,19 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-router-dom@6.30.6: + resolution: {integrity: sha512-0RHKZz7wwffvkU+2MFVT2NnjK44ssLEV+m0CAJaS2Ksmorrwj7WxH00jO0SOCW26/tINUnJHToXblDs33I38YQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.6: + resolution: {integrity: sha512-5HfK7k5im7LTOB0EqCQmfvy4C13G92Ssj1VTmouTK3AJvyjKTnFuCV0vcMAD/JS+JC4DvDIBRrlAeJIFjh5VWg==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -3551,6 +3608,8 @@ snapshots: '@noble/hashes@1.8.0': {} + '@noble/hashes@2.4.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3571,12 +3630,41 @@ snapshots: transitivePeerDependencies: - encoding + '@otplib/core@13.5.0': {} + + '@otplib/hotp@13.5.0': + dependencies: + '@otplib/core': 13.5.0 + '@otplib/uri': 13.5.0 + + '@otplib/plugin-base32-scure@13.5.0': + dependencies: + '@otplib/core': 13.5.0 + '@scure/base': 2.4.0 + + '@otplib/plugin-crypto-noble@13.5.0': + dependencies: + '@noble/hashes': 2.4.0 + '@otplib/core': 13.5.0 + + '@otplib/totp@13.5.0': + dependencies: + '@otplib/core': 13.5.0 + '@otplib/hotp': 13.5.0 + '@otplib/uri': 13.5.0 + + '@otplib/uri@13.5.0': + dependencies: + '@otplib/core': 13.5.0 + '@paralleldrive/cuid2@2.3.1': dependencies: '@noble/hashes': 1.8.0 '@phc/format@1.0.0': {} + '@remix-run/router@1.23.4': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/pluginutils@5.4.0(rollup@4.63.1)': @@ -3664,6 +3752,8 @@ snapshots: '@scarf/scarf@1.4.0': {} + '@scure/base@2.4.0': {} + '@smithy/core@3.33.3': dependencies: '@smithy/types': 4.17.2 @@ -4773,6 +4863,15 @@ snapshots: dependencies: wrappy: 1.0.2 + otplib@13.5.0: + dependencies: + '@otplib/core': 13.5.0 + '@otplib/hotp': 13.5.0 + '@otplib/plugin-base32-scure': 13.5.0 + '@otplib/plugin-crypto-noble': 13.5.0 + '@otplib/totp': 13.5.0 + '@otplib/uri': 13.5.0 + package-json-from-dist@1.0.1: {} parseurl@1.3.3: {} @@ -4918,6 +5017,18 @@ snapshots: react-refresh@0.17.0: {} + react-router-dom@6.30.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.30.6(react@18.3.1) + + react-router@6.30.6(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.4 + react: 18.3.1 + react@18.3.1: dependencies: loose-envify: 1.4.0 diff --git a/turbo.json b/turbo.json index 267c280..5bf9535 100644 --- a/turbo.json +++ b/turbo.json @@ -3,7 +3,8 @@ "tasks": { "build": { "dependsOn": ["^build"], - "outputs": ["dist/**"] + "outputs": ["dist/**"], + "env": ["VITE_*"] }, "dev": { "cache": false,