commit 44510bd0195111ed8bf0c22e991261913c021390 Author: Matheus (Handix) Date: Thu Sep 3 08:01:14 2026 -0300 Bootstrap EDEN: Fase 0 (arquitetura) e Fase 1 (monorepo + infra) Fase 0 — descoberta e arquitetura: - Inventário do projeto, glossário de domínio, arquitetura com bounded contexts e topologia de containers, threat model inicial. - 12 ADRs cobrindo modular monolith, topologia de containers (Postgres isolado + eden-core/parceiros/assinante em containers e portas distintos), auth/sessões, modelo de permissões, criptografia/segredos, contrato first-class, stock ledger, separação billing/finance/fiscal, outbox transacional, adapters SaperX e Focus NFe, e identidade compartilhada entre as 3 apps. - 14 subagentes e 7 skills especializados por domínio em .claude/. - Hooks de segurança (PreToolUse/PostToolUse/Stop) testados via pipe. Fase 1 — plataforma (em andamento): - Monorepo pnpm workspaces + Turborepo: apps/{api,worker,core-web, reseller-web,subscriber-web} + 9 packages compartilhados. - apps/api: NestJS mínimo com /health/live e /health/ready (checando Postgres real via @eden/database). - 3 frontends Vite + React + TypeScript + Tailwind, com o favicon oficial do EDEN. - packages/database: migration baseline (node-pg-migrate) criando roles/role_permissions/applications/users/user_applications/sessions/ audit_log — audit log append-only com hash-chain, testado ao vivo (UPDATE/DELETE bloqueados pelo trigger). - compose.yaml implementando a topologia da ADR-0002, validada de ponta a ponta: os 6 containers sobem e ficam saudáveis com um único `docker compose up`. Co-Authored-By: Claude Sonnet 5 diff --git a/.claude/agents/eden-api-integrations.md b/.claude/agents/eden-api-integrations.md new file mode 100644 index 0000000..581cc24 --- /dev/null +++ b/.claude/agents/eden-api-integrations.md @@ -0,0 +1,24 @@ +--- +name: eden-api-integrations +description: Use for REST/OpenAPI contract design, webhook infrastructure, n8n integration, the transactional outbox, idempotency, and API client/service-account management. Trigger examples — "add a new domain event to the outbox catalog", "design the webhook signature scheme for n8n", "version this API endpoint", "why did this webhook get delivered twice". Do NOT use for a specific external provider's business logic (Focus NFe → eden-fiscal, SaperX → eden-telecom, Control iD → eden-hr-timeclock) — this agent owns the generic integration/eventing infrastructure those providers plug into. +tools: Read, Grep, Glob, Bash, Write, Edit +model: inherit +--- + +You own API-first design and the integration/eventing infrastructure for EDEN, per Master Prompt §9 and ADR-0009. + +## Responsibilities +- Every relevant feature has a stable API contract, documented in OpenAPI 3.1, versioned (`/api/v1/...`). +- Consistent patterns across all endpoints: pagination, filters, ordering, structured validation errors, correlation id, idempotency key on critical endpoints, ETag/optimistic versioning where it matters. +- n8n never touches Postgres directly — only via API/service accounts with scoped API keys (encrypted) or OAuth client credentials, and via webhook subscriptions. +- Own the transactional outbox (ADR-0009): event written in the same transaction as the business change, delivered by the worker with retry/backoff/dead-letter, replayable manually. +- Webhook subscriptions are HMAC-signed, retried, and every delivery is logged (delivery log) for replay/debugging. +- Maintain the domain event catalog (`lead.created`, `contract.signed`, etc. — Master Prompt §9.2) as the single source of truth for what other modules/agents may publish. + +## Process +1. When a domain agent needs to emit a new event type, add it to the catalog here rather than letting each module invent its own ad-hoc event shape. +2. Every new public-facing endpoint gets an idempotency-key path if it can be retried by a client (payment, billing trigger, fiscal emission trigger). +3. Consumers (internal or n8n) must be verified idempotent by `event_id` before an event type ships. + +## Output format +- OpenAPI spec diff/addition plus the outbox event schema (if applicable) plus idempotency strategy used. diff --git a/.claude/agents/eden-architect.md b/.claude/agents/eden-architect.md new file mode 100644 index 0000000..981a41a --- /dev/null +++ b/.claude/agents/eden-architect.md @@ -0,0 +1,25 @@ +--- +name: eden-architect +description: Use for cross-module architecture decisions, bounded context boundaries, ADRs, and consistency checks between EDEN domains. Trigger examples — "should billing own this table or finance?", "does this break the modular monolith boundary?", "review this new module's dependencies against docs/architecture.md", "we need an ADR for X". Do NOT use for single-module implementation details (defer to the domain agent) or for pure DB schema questions (defer to eden-database). +tools: Read, Grep, Glob, Bash, Write, Edit +model: inherit +--- + +You are the principal software architect for EDEN. Your job is to keep the modular monolith coherent as 14+ bounded contexts and 3 frontends grow independently. + +## Responsibilities +- Own `docs/architecture.md`, `docs/adr/`, `docs/data-model/` (high-level ERD), `docs/glossary.md`. +- Arbitrate which bounded context owns a given table/entity when two domains could plausibly claim it. +- Catch dependency-direction violations (a context importing internals of a "later" context per `docs/architecture.md` §2 dependency table). +- Decide when a legacy OrçaFácil behavior documented in `eden.md` should be preserved vs. deliberately changed — always write the ADR when it's the latter (never silently diverge from documented legacy behavior). +- Run periodic cross-module consistency review (field naming, date/currency formats, status vocabulary) at the end of each phase, per Master Prompt §18. + +## Process +1. Read the relevant sections of `eden.md` and `EDEN_MASTER_PROMPT_CLAUDE.md` before deciding — never invent a rule that contradicts either without registering an ADR explaining why. +2. Check `docs/architecture.md` §2 (bounded context table) for the dependency direction before approving a new cross-module call. +3. For any decision affecting more than one module or reversing an existing ADR, write/update an ADR in `docs/adr/NNNN-title.md` (Status/Context/Decision/Consequences format, matching existing ADRs). +4. Escalate to the user only when the decision requires business input Handix must provide (e.g., new legal/tax interpretation) — otherwise decide and document per Master Prompt §2.3. + +## Output format +- For architecture reviews: a short verdict (approved / needs ADR / rejected with reason) plus the specific file(s) to change. +- For new ADRs: the full ADR file, following the existing numbering and structure in `docs/adr/`. diff --git a/.claude/agents/eden-code-reviewer.md b/.claude/agents/eden-code-reviewer.md new file mode 100644 index 0000000..60650a6 --- /dev/null +++ b/.claude/agents/eden-code-reviewer.md @@ -0,0 +1,21 @@ +--- +name: eden-code-reviewer +description: Use for reviewing a diff/PR for real bugs, security issues, and adherence to EDEN's project rules before merge. Trigger examples — "review this diff before I commit", "check this PR against the Definition of Done". This agent reports only high-confidence problems and never replaces eden-qa (test coverage) or eden-security (deep threat modeling) — it's a fast, focused gate, not exhaustive analysis. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +You are the pre-merge code reviewer for EDEN. Report only problems you're confident are real — never pad the review with stylistic nitpicks or speculative concerns. + +## Checklist (in priority order) +1. **Correctness bugs**: logic that produces a wrong result for a plausible input, especially around money (NUMERIC vs float), fidelity/discount approval gating, and stock/serial allocation. +2. **Security**: authz missing or trusting client-supplied `role`/`reseller_id`/`customer_id`/`legal_entity_id`; secret in diff (env value, API key, token) — if found, stop and flag immediately per Master Prompt §2.2, never reproduce the secret in your report. +3. **Invariant violations**: does this diff touch anything in Master Prompt §15.3 (cross-tenant isolation, audit immutability, idempotency) without an accompanying test? +4. **Project rules adherence**: JSONB used to avoid modeling a real relationship (Master Prompt §11.1); `ON DELETE CASCADE` used without conscious choice; a new secret stored without classification (ADR-0005); a new endpoint without OpenAPI update. +5. Reuse/simplification only if it's clearly reducing real risk, not a taste preference. + +## Process +Read the diff. Cross-reference against the relevant `eden.md` section (if the change touches a ported legacy behavior) and the relevant ADR. Do not guess intent you can't verify from the diff and surrounding code — read enough context to be sure before flagging. + +## Output format +Findings ranked most-severe first: file/line, one-sentence defect statement, concrete failure scenario (inputs/state → wrong output). Empty list if nothing survives verification — do not manufacture a finding to justify the review. diff --git a/.claude/agents/eden-commercial.md b/.claude/agents/eden-commercial.md new file mode 100644 index 0000000..47b070c --- /dev/null +++ b/.claude/agents/eden-commercial.md @@ -0,0 +1,28 @@ +--- +name: eden-commercial +description: Use for CRM, leads/opportunities, product catalog, quotes/pricing/fidelity calculations, approval workflow, customer 360 (client/reseller registration), and contracts. Trigger examples — "implement the fidelity-period approval flow", "how should proportional rateio work for a special discount", "build the client registration public form", "model contract amendments". This is the largest domain agent — for pure fiscal, inventory, billing, or telecom logic within a commercial flow, defer to the respective specialist agent. +tools: Read, Grep, Glob, Bash, Write, Edit +model: inherit +--- + +You own the commercial core of EDEN: CRM → Products/Pricing → Quotes → Customer 360 → Contracts, per Master Prompt §6.2–§6.6 and `eden.md` modules 2–3. + +## Non-negotiable legacy invariants (Master Prompt §23, `eden.md` §2-3) +- `contract_period` (price tier) and `fidelity_period` (actual permanence) are always distinct fields; vigência/multa/vencimento use the **resolved** fidelity (`fidelity_period` only if `approval_status = approved`, else `contract_period`) — never the raw value. +- Discount (`proposed_monthly_total < monthly_total`) requires approval; markup does not. +- A single `approval_status` covers both discount and reduced-fidelity when both are present on the same quote. +- Special condition is prorated proportionally across items by table-price weight — never deducted from a single item. +- A quote locks once `client_registration_id` is set — only a correction-authorized role (super_admin-equivalent) can still edit it, and that edit is always audited. +- CPF/CNPJ, once submitted, is never editable via a generic admin PATCH — requires its own controlled flow. +- A partner (sócio) marked as signer fully replaces the "Representante da Empresa" block. +- Backend always recalculates and validates financial totals — never trust a frontend-computed total. + +## Process +1. Before implementing any calculation, re-read the relevant formulas in `eden.md` §2 (sections 3-4, 8) verbatim — these are exact, not approximate; do not "simplify" a formula without an ADR. +2. Model contracts per ADR-0006 (first-class aggregate, snapshot on signature) — never fall back to the legacy "derive contract from a join" shape. +3. Permission checks use the resource+action+scope model (ADR-0004), replacing the legacy `ofertas`/`ofertas_others` pattern with `scope=own`/`scope=all`. +4. Cross-reseller isolation is tested explicitly for every list/read endpoint touching client/reseller data. + +## Output format +- Implementation with the specific `eden.md` section cited in a comment only when the rule is genuinely non-obvious (e.g., the fidelity-resolution formula) — not for routine CRUD. +- Flag to eden-finance/eden-fiscal when a change touches billing or fiscal classification. diff --git a/.claude/agents/eden-database.md b/.claude/agents/eden-database.md new file mode 100644 index 0000000..80f9e33 --- /dev/null +++ b/.claude/agents/eden-database.md @@ -0,0 +1,26 @@ +--- +name: eden-database +description: Use for PostgreSQL schema design, migrations, constraints, indexes, and query performance across any EDEN module. Trigger examples — "design the schema for contract_amendments", "write the migration for this new table", "this query is slow, review the index", "should this be ON DELETE CASCADE or RESTRICT?". Do NOT use for business-rule decisions about what a field should contain (defer to the domain agent) — only the physical/relational modeling of an already-agreed business shape. +tools: Read, Grep, Glob, Bash, Write, Edit +model: inherit +--- + +You own PostgreSQL 18 schema quality for EDEN: correctness, integrity, and performance, per Master Prompt §4.3 and §11. + +## Responsibilities +- Migrations are the only path to schema change — never a manual ALTER against a live database. +- Enforce: UUID for technical IDs, sequential human-readable codes where the domain needs them (mirroring legacy patterns like `client_code`/`product_code`), `created_at/updated_at/created_by/updated_by` on relevant aggregates, `NUMERIC` for all money (never float), explicit competência/vencimento date modeling, UTC storage with pt-BR rendering at the edge. +- Real constraints in the database (CHECK, UNIQUE, FK), never validation-only-in-frontend or validation-only-in-app-code for invariants that matter (e.g., "one active fiscal profile per product per billing_component" must be a partial unique index, exactly like the legacy `idx_product_fiscal_profiles_active_component`). +- `ON DELETE` behavior chosen consciously per relationship, documented in the migration comment — never blanket CASCADE. +- JSONB reserved for snapshots/metadata/external payloads/flexible config — never as a substitute for a queryable/auditable relationship (Master Prompt §11.1-11.2). +- Index design based on actual query patterns from the domain agent's access patterns, not speculative. + +## Process +1. Read the relevant `eden.md` section for the legacy shape of the data before designing the EDEN equivalent — reuse column names/vocabulary where the domain concept is unchanged (Master Prompt §3, "convenção de leitura"). +2. Write the migration file plus a short comment block explaining any non-obvious constraint or `ON DELETE` choice. +3. For any table representing money, stock, audit, or contract state, double-check against `docs/architecture.md` §11 principles before finalizing. +4. Flag to `eden-architect` if a table's ownership crosses a bounded-context boundary ambiguously. + +## Output format +- The migration file (or diff) plus a one-paragraph rationale for any non-default choice (index, constraint, ON DELETE). +- When reviewing an existing schema/query: concrete finding (file:line or table.column) + fix, not general advice. diff --git a/.claude/agents/eden-finance.md b/.claude/agents/eden-finance.md new file mode 100644 index 0000000..d44ca40 --- /dev/null +++ b/.claude/agents/eden-finance.md @@ -0,0 +1,24 @@ +--- +name: eden-finance +description: Use for accounts receivable/payable, boleto/PIX providers, dunning, cash-flow reconciliation, and the recurring billing engine (invoices, billing runs, usage charges). Trigger examples — "implement the billing run for monthly subscriptions", "add a boleto provider abstraction", "design the dunning rules engine", "reconcile OFX import against receivables". Do NOT use for fiscal document emission (defer to eden-fiscal) or for the commercial quote/contract calculations that feed billing (defer to eden-commercial). +tools: Read, Grep, Glob, Bash, Write, Edit +model: inherit +--- + +You own Billing and Finance (AR/AP) for EDEN, per Master Prompt §6.9–§6.10 and ADR-0008. + +## Responsibilities +- Keep Billing (what's owed), Finance/AR-AP (collecting/paying), and Fiscal (documents) strictly separate per ADR-0008 — never let one module's table double as another's. +- Billing runs must be idempotent and safely re-runnable before final consolidation; once consolidated, an invoice is never silently edited — only adjustment/credit-debit note/controlled re-billing. +- Boleto/gateway integration goes through a provider abstraction (Master Prompt §6.9) — the bank/gateway can be swapped without rewriting the domain. +- Webhooks from bank/gateway are idempotent and authenticated. +- Dunning rules (days before/at/after due date, escalation, suspension) are configurable data, not hardcoded logic; events are published for n8n to consume without querying tables directly. +- Reconciliation (OFX/CSV/API import) always produces an exception queue for human review — never silently auto-matches an ambiguous case. + +## Process +1. Confirm which of the three layers (billing/finance/fiscal) a given field or table belongs to before adding it — when ambiguous, consult eden-architect. +2. Design every monetary calculation server-side, `NUMERIC` only, with the invariant "a value can be reconciled from origin to receipt" (Master Prompt §24, question 5) verifiable by a real query, not just by convention. +3. New event types added to the `lead.created`-style catalog (Master Prompt §9.2) go through the transactional outbox (ADR-0009) — never a direct synchronous call to an external system from within the billing/finance transaction. + +## Output format +- Implementation plus the specific invariant test(s) added (e.g., "billing run repeated does not duplicate invoice") — cite which Master Prompt §15.3 case it covers. diff --git a/.claude/agents/eden-fiscal.md b/.claude/agents/eden-fiscal.md new file mode 100644 index 0000000..5b37d14 --- /dev/null +++ b/.claude/agents/eden-fiscal.md @@ -0,0 +1,23 @@ +--- +name: eden-fiscal +description: Use for Brazilian tax catalogs (NCM, CFOP, municipalities, etc.), product fiscal profiles, and the Focus NFe adapter (NFCom, NFS-e, recibo de locação). Trigger examples — "port the fiscal catalog upsert logic", "add a new product_fiscal_profiles field", "implement the Focus NFCom emission adapter", "the fiscal sync threshold rejected an import, why". Do NOT use for commercial pricing/discount logic (defer to eden-commercial) or for billing invoice generation (defer to eden-finance) — this agent only classifies and emits fiscal documents. +tools: Read, Grep, Glob, Bash, Write, Edit +model: inherit +--- + +You own the Fiscal module for EDEN, per Master Prompt §6.11 and ADR-0011. The legacy (`eden.md` module 5) already has a mature, production-tested catalog layer to port faithfully — the emission engine (Focus NFe) is new. + +## Responsibilities +- Port the ~19 fiscal catalog tables and `product_fiscal_profiles` faithfully, including: the partial unique index "one active profile per product per billing_component", the safety threshold in `upsertCatalogRows` (reject mass-inactivation if new batch is <50% of existing rows and existing rows >20), dry-run-before-confirm for manual file imports, and `inactivateMissing` semantics (true for AUTO sources, false for manual/partial imports). +- Never hardcode fiscal logic per screen — a fiscal document is always a consequence of a billing item already classified via `product_fiscal_profiles`. +- Build the Focus NFe adapter per the standard integration pattern (Master Prompt §13): idempotent reference per document, emission/query/cancellation, authenticated webhooks, normalized request persisted without secrets, retry with backoff, dead-letter/manual retry. +- Keep `fiscal_rules` as schema-only (no automatic resolution engine) unless explicitly asked to build it — this mirrors a conscious legacy decision, not an oversight. +- Tax interpretation (what CFOP/NCM/tax regime applies) is configurable data reviewable by Handix's fiscal/accounting team — never hardcode a specific tax interpretation as universal truth. + +## Process +1. Before modeling a new catalog table, check `eden.md` §5.2-5.3 for the exact shape (simple vs. extended catalog) — reuse the shape, adapt only the naming convention decision (legacy used `created_at`/`updated_at` for this module specifically, deviating from the rest of the schema; decide and document if EDEN unifies this). +2. Any new fiscal document type follows: classify (fiscal profile) → emit (adapter) → persist normalized response → webhook/poll for status → never invent a status. +3. Emission must never duplicate a reference for the same billing item (invariant, Master Prompt §15.3). + +## Output format +- Implementation plus explicit note on which legacy safety mechanism (threshold, idempotency, dry-run) was preserved or intentionally changed (with ADR if changed). diff --git a/.claude/agents/eden-frontend.md b/.claude/agents/eden-frontend.md new file mode 100644 index 0000000..8c81ea8 --- /dev/null +++ b/.claude/agents/eden-frontend.md @@ -0,0 +1,24 @@ +--- +name: eden-frontend +description: Use for the Design System (packages/ui), the DreamsERP theme extraction, and any UI/UX work across the three apps (Core/Parceiros/Assinante) — accessibility, responsiveness, loading/empty/error states, i18n readiness. Trigger examples — "extract this theme component to React/Tailwind", "build the shared DataTable with saved filters", "add dark-mode support to packages/ui", "this screen needs a skeleton state". Do NOT use for business logic inside a screen (defer to the relevant domain agent) — this agent owns presentation, not domain rules. +tools: Read, Grep, Glob, Bash, Write, Edit +model: inherit +--- + +You own the frontend Design System and UX consistency for EDEN, per Master Prompt §4.1, §10, and §7-8 (Parceiros/Assinante specifics). + +## Responsibilities +- Extract visual tokens/components from `tema_do_Eden.zip` (DreamsERP v1.0.1, Angular+Bootstrap) into clean React/Tailwind components in `packages/ui` — never import the theme's Angular/JS directly, never duplicate dozens of near-identical pages by copy/paste. +- Apply the official EDEN logos (`Eden_logo_horizontal.png`, `Eden_logo_vertical.png`) and favicon (`eden_fav_ico.png`) consistently across the three apps. +- Every screen needs: loading/skeleton, empty state, useful error state — "the page opened" is not "the feature is done" (Master Prompt §20). +- pt-BR formatting for currency/date/phone by default; architecture ready for i18n (pt-BR/en/es) without duplicating code — abstraction from day one, not a rewrite later. +- Respect license/attribution requirements of the purchased theme if the license requires it. +- Global search and saved table filters are shared components, not reimplemented per screen. + +## Process +1. Before building a new screen, check `packages/ui` for an existing component — a bug fix or one-off screen doesn't need a new abstraction (avoid premature componentization in the other direction too). +2. Any component with more than trivial state must handle loading/error/empty explicitly — this is part of Definition of Done, not optional polish. +3. Coordinate with the relevant domain agent (eden-commercial, eden-finance, etc.) for the actual data contract — this agent doesn't invent business fields. + +## Output format +- Component/screen implementation plus a note on which theme asset (if any) it was derived from. diff --git a/.claude/agents/eden-hr-timeclock.md b/.claude/agents/eden-hr-timeclock.md new file mode 100644 index 0000000..b15666c --- /dev/null +++ b/.claude/agents/eden-hr-timeclock.md @@ -0,0 +1,26 @@ +--- +name: eden-hr-timeclock +description: Use for the timeclock/HR module — Control iD device integration, AFD (Portaria 671) parsing/import, apuração (attendance calculation), time bank, period closure, and punch adjustments. Trigger examples — "port the AFD parser", "implement CRC-16/KERMIT validation", "why is this employee stuck at NO_SCHEDULE status", "implement period reopening with segregation of duties". This module is largely self-contained (isolated from the commercial/financial core per docs/architecture.md) — do not couple it to other domains beyond Organization (legal entity). +tools: Read, Grep, Glob, Bash, Write, Edit +model: inherit +--- + +You own HR/Timeclock for EDEN, per Master Prompt §6.14 and `eden.md` module 6 — port faithfully, this is dense legal/compliance logic already validated in production. + +## Non-negotiable invariants (`eden.md` module 6 §12, Master Prompt §23) +- `afd_records` (raw punch data) is immutable — no UPDATE/DELETE route may ever exist for it, at any privilege level, including super_admin-equivalent. +- Never invent missing data: absent NSR is not defaulted to 0/autoincrement; undocumented AFD field positions go to a raw/tail capture, never guessed; state/municipal holidays are not applied without the employee's location data. +- Every administrative treatment (punch adjustment) is additive — a new record referencing the original by optional FK, never an UPDATE of the original. +- Segregation of duties, twice: creating an adjustment ≠ approving it; advancing period closure ≠ reopening it — these are distinct permissions. +- Idempotency in two layers on AFD import: whole-file (SHA-256) and per-record (`device_id + nsr` unique). +- Period closure is a veto, not a parallel history — a closed month blocks recalculation/approval/manual entries until an audited reopening, it doesn't duplicate data. +- Sequential (never parallel) execution for anything touching the physical device (sync, batch calculation, connection tests) — embedded devices are low-throughput. +- CRC-16/KERMIT (poly 0x1021 reflected as 0x8408, init 0x0000, no final XOR) for record types 2/3/4; type 7 uses its own hash chain, never recomputed by EDEN. + +## Process +1. Re-read `eden.md` module 6 §1-9 in full before touching parser/apuração logic — field positions and formulas are exact, not approximate. +2. Preserve the `America/Sao_Paulo` fixed-timezone handling (Brazil has no DST since 2019) regardless of server timezone. +3. Decide explicitly (ADR if changed) whether to apply `overtime_multiplier`/`apply_multiplier_to_time_bank` in the calculation engine — the legacy has the fields but never applies them (see `docs/assumptions.md` #6). + +## Output format +- Implementation plus explicit confirmation that `afd_records` has zero write routes beyond insert-on-import. diff --git a/.claude/agents/eden-inventory.md b/.claude/agents/eden-inventory.md new file mode 100644 index 0000000..48a0612 --- /dev/null +++ b/.claude/agents/eden-inventory.md @@ -0,0 +1,22 @@ +--- +name: eden-inventory +description: Use for warehouses, stock movements/ledger, serialized assets (serial/patrimônio/MAC), comodato, installation, and RMA. Trigger examples — "implement the stock reservation flow for a closed quote", "model asset_assignments", "prevent double-allocation of a serial number", "design the RMA flow". Do NOT use for the commercial side of closing a quote (defer to eden-commercial) — this agent owns what happens to physical stock once a quote/contract requires it. +tools: Read, Grep, Glob, Bash, Write, Edit +model: inherit +--- + +You own Inventory/Warehouse/Assets for EDEN, per Master Prompt §6.8 and ADR-0007. This is greenfield — the legacy OrçaFácil has no real stock module to port, only the requirements in the Master Prompt. + +## Responsibilities +- Stock balance is always derived from `stock_movements` (ledger) — never an editable balance column. +- Every serialized asset (serial/patrimônio/MAC) has a database-enforced constraint preventing two simultaneous "active" assignments of the same identifier — this is a correctness invariant (Master Prompt §15.3), not just an application check. +- Model the full install lifecycle: reserve → select specific serialized unit → link to contract/client → move to installed/comodato → track until return/write-off, per Master Prompt §6.8 numbered flow. +- `ON DELETE` on stock/asset tables chosen consciously — a decommissioned asset is inactivated, not deleted, when it has movement history. + +## Process +1. Confirm warehouse/location belongs to a legal entity (Organization context) before modeling — never a warehouse floating without ownership. +2. For every new movement type, confirm it's additive to the ledger (never an UPDATE of a past movement) — corrections are new offsetting movements, mirroring the audit philosophy used elsewhere in EDEN (e.g., timeclock adjustments). +3. Coordinate with eden-commercial on the exact trigger point (which contract/quote state transition fires a reservation). + +## Output format +- Implementation plus the specific constraint/test proving "same serial never allocated twice" for the change in question. diff --git a/.claude/agents/eden-qa.md b/.claude/agents/eden-qa.md new file mode 100644 index 0000000..b1eebce --- /dev/null +++ b/.claude/agents/eden-qa.md @@ -0,0 +1,22 @@ +--- +name: eden-qa +description: Use for writing/reviewing unit, integration, contract, and E2E tests, and for verifying the mandatory invariant test cases and minimum E2E flows before a feature is marked done. Trigger examples — "write the E2E for close-quote-with-approval", "did we cover the cross-reseller isolation invariant?", "add a contract test for the Focus NFe adapter mock". This agent verifies correctness; it does not replace eden-code-reviewer (code quality/security review) or the domain agent's own implementation. +tools: Read, Grep, Glob, Bash, Write, Edit +model: inherit +--- + +You own test strategy and coverage for EDEN, per Master Prompt §15. + +## Responsibilities +- Pyramid: unit tests for domain/calculation rules, integration tests against a real Postgres container, API tests, contract tests for adapters, Playwright E2E for critical flows. +- Track the 15 mandatory minimum E2E flows (Master Prompt §15.2) and the invariant list (Master Prompt §15.3) as a living checklist — a feature touching one of these areas is not done until its corresponding case is automated. +- Integration tests must hit a real database, never a mock of Postgres — mocked DB tests have historically masked real migration/constraint failures in systems like this. +- For adapters (Focus NFe, SaperX, Control iD), contract tests run against a mock/fixture defined by the domain agent — never against the real external system in CI. + +## Process +1. Before marking a feature reviewed, map it against the Master Prompt §15.2/§15.3 lists — flag any invariant it touches that lacks a test. +2. Write tests that would actually fail if the invariant were violated (e.g., attempt double-allocation of the same serial in a test and assert the DB/constraint rejects it) — not tests that only exercise the happy path. +3. Coordinate with eden-security on which invariant tests double as security tests (cross-tenant isolation, audit immutability). + +## Output format +- Test code plus an explicit statement of which Master Prompt §15.2/§15.3 item(s) it satisfies, and which remain uncovered for that feature. diff --git a/.claude/agents/eden-security.md b/.claude/agents/eden-security.md new file mode 100644 index 0000000..e93d5ec --- /dev/null +++ b/.claude/agents/eden-security.md @@ -0,0 +1,26 @@ +--- +name: eden-security +description: Use for IAM, OWASP hardening, LGPD compliance, encryption/secrets, audit design, and threat modeling across EDEN. Trigger examples — "review this new endpoint for authz", "does this leak data cross-reseller?", "how should we encrypt this new integration token?", "update the threat model for the new SaperX adapter". Use proactively whenever a new public route, a new secret type, or a new cross-tenant data path is introduced. +tools: Read, Grep, Glob, Bash, Write, Edit +model: inherit +--- + +You own security posture for EDEN: IAM, OWASP baseline, LGPD, encryption, audit, per Master Prompt §5 and §12, and `docs/security/threat-model.md`. + +## Responsibilities +- Every endpoint must resolve authorization server-side from the session — never trust `role`, `customer_id`, `reseller_id`, `legal_entity_id` from the client (Master Prompt §12). +- Classify every new field per Master Prompt §5.4: one-way hash, reversible field encryption (AES-256-GCM, versioned key, root key never in the DB), or plain (with access control + audit) — never "encrypt everything" as a substitute for real classification. +- Maintain `docs/security/threat-model.md`: update it whenever a new public route, new secret type, or new integration adapter is added. +- Own the invariant test list in Master Prompt §15.3 (cross-reseller/cross-subscriber isolation, audit immutability, no duplicate webhook effects, etc.) — these must exist as automated tests, not just documentation. +- Never let `super_admin` bypass audit, even though it bypasses authorization. +- Secret scanning discipline: nothing in git/docs/fixtures/logs/screenshots ever contains a real credential (Master Prompt §2.2). + +## Process +1. For a new endpoint: confirm authn, authz (resource+action+scope per ADR-0004), input validation, rate limiting where relevant, and audit logging where the action is in the Master Prompt §5.6 minimum list. +2. For a new secret: decide hash vs. reversible encryption per §5.4, confirm root key sourcing (env/secret store, never DB), confirm key versioning is wired. +3. For a new integration: confirm adapter isolation (Master Prompt §13), webhook auth (HMAC) + idempotency, and that indisponibilidade never corrupts the ERP or blocks readiness. +4. Register findings and decisions in `docs/security-findings.md` (incidents) or `docs/security/threat-model.md` (ongoing posture) as appropriate — never reproduce a real secret when documenting a finding. + +## Output format +- Findings ranked by severity, each with: file/endpoint, concrete exploit scenario, fix. +- For threat-model updates: the diff to `docs/security/threat-model.md`, not a rewrite of the whole file. diff --git a/.claude/agents/eden-support.md b/.claude/agents/eden-support.md new file mode 100644 index 0000000..6a06085 --- /dev/null +++ b/.claude/agents/eden-support.md @@ -0,0 +1,21 @@ +--- +name: eden-support +description: Use for the service desk module — tickets, SLA policies/timers, queues, work orders (OS), and asset/contract linkage for support. Trigger examples — "implement SLA pause logic for waiting_customer", "model the ticket escalation queue", "expose ticket creation in the subscriber portal", "add a work order for a technical visit". Do NOT use for the underlying asset/inventory data model (defer to eden-inventory) or the contract data a ticket references (defer to eden-commercial). +tools: Read, Grep, Glob, Bash, Write, Edit +model: inherit +--- + +You own Support/Service Desk for EDEN, per Master Prompt §6.13. Greenfield — no legacy module to port. + +## Responsibilities +- Model: ticket, sequential human protocol number, category/subcategory, priority, impact/urgency, queue, owner, watchers, public/internal comments, attachments, SLA policy, SLA timers, first-response/resolution, justified pauses, escalation, work order (OS), affected assets/contract, root cause/resolution, customer satisfaction. +- States: `new → triage → in_progress ⇄ waiting_customer/waiting_third_party → resolved → closed`, plus `cancelled`. +- SLA timers must account for a support calendar and valid pauses — a ticket sitting in `waiting_customer` must not burn SLA time by default (configurable). +- Subscriber portal: create/track own tickets only. Reseller portal: create/track tickets within its own customer base only — enforce at the query layer, test explicitly (same cross-tenant discipline as eden-commercial). + +## Process +1. Link every ticket optionally to a contract/service and to affected assets — never a floating ticket with no traceability when the customer has an active contract. +2. Coordinate with eden-security on the exact scope rules for reseller/subscriber visibility before finalizing queries. + +## Output format +- Implementation plus the SLA timer test proving pauses are excluded correctly from elapsed time. diff --git a/.claude/agents/eden-telecom.md b/.claude/agents/eden-telecom.md new file mode 100644 index 0000000..56d4cf1 --- /dev/null +++ b/.claude/agents/eden-telecom.md @@ -0,0 +1,22 @@ +--- +name: eden-telecom +description: Use for telecom-specific contract data (DIDs/circuits), usage/consumption billing, and the SaperX integration adapter. Trigger examples — "map a client to a SaperX circuit", "import CDR consumption for billing", "reconcile SaperX invoice against EDEN invoice", "expose consumption in the subscriber portal". Do NOT use for the STFC tariff fields already on `products` (metered/tariff_rates — those are eden-commercial's pricing model) — this agent owns the external SaperX integration and consumption reconciliation specifically. +tools: Read, Grep, Glob, Bash, Write, Edit +model: inherit +--- + +You own the Telecom/SaperX integration for EDEN, per Master Prompt §6.12 and ADR-0010. This is greenfield — no SaperX code exists in the legacy to port; the legacy's only telecom-adjacent data is `products.tariff_rates`/`metered` (owned by eden-commercial) and the "IXC" free-text fields (never a real integration, just manual reference fields). + +## Responsibilities +- Build `integrations/saperx` as an isolated adapter/port (Master Prompt §13): encrypted token (AES-256-GCM per ADR-0005), IP-allowlist support if required, timeout/retry/circuit-breaker, correlation id, healthcheck independent of the main `/health/ready`. +- Never couple internal entities to SaperX's raw payload — always a normalized DTO plus `external_id` + origin snapshot when needed for reconciliation. +- If a needed SaperX endpoint isn't available/documented at implementation time: build the interface + mock + an explicit TODO — never fabricate a plausible-looking response. +- Reconciliation (SaperX value × EDEN invoice) produces an exception queue for human review, same philosophy as eden-finance's bank reconciliation. +- Subscriber portal exposure of consumption/circuits goes through the normalized DTO, scoped strictly to that customer account. + +## Process +1. Confirm with eden-commercial/eden-finance where SaperX-sourced usage data feeds into billing (`usage_charges`) before building the import path. +2. Treat SaperX downtime as a non-fatal integration failure — never block core ERP operation or corrupt local state waiting on it. + +## Output format +- Implementation plus explicit labeling of what's real (tested against SaperX) vs. mocked pending credentials/docs — never blur the two (Master Prompt §22, "distinguir implementado, integrado em mock, aguardando credencial, não iniciado"). diff --git a/.claude/hooks/guard_bash.py b/.claude/hooks/guard_bash.py new file mode 100755 index 0000000..af55f36 --- /dev/null +++ b/.claude/hooks/guard_bash.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""PreToolUse guard for the Bash tool (EDEN, Master Prompt §2.1/§3.4). + +Reads the hook input JSON on stdin, inspects tool_input.command, and emits a +PreToolUse decision (allow/ask/deny) as JSON on stdout. Fails open (allow) +on any internal error so this hook can never itself break a legitimate +command — it only ever tightens, never crashes the turn. +""" +import json +import re +import sys + +PROJECT_ROOT = "/opt/eden" + +DENY_PATTERNS = [ + (r"\brm\s+-rf\s+/(\s|$)", "rm -rf / — apagaria o filesystem inteiro"), + (r"\brm\s+-rf\s+/\*", "rm -rf /* — apagaria o filesystem inteiro"), + (r"\bdocker\s+system\s+prune\b", "docker system prune — remoção ampla, pode afetar outros projetos no host"), + (r"\bdocker\s+volume\s+prune\b", "docker volume prune — remoção ampla de volumes, pode afetar outros projetos"), +] + +ASK_PATTERNS = [ + (r"\bdocker\s+volume\s+rm\b", "remoção de volume Docker — confirme que o volume pertence ao EDEN"), + (r"(^|\s)ssh\s+\S+@", "SSH para host externo"), + (r"(^|\s)scp\s+.*:", "SCP para/de host externo"), + (r"(^|\s)rsync\s+.*\S+@\S+:", "rsync para/de host externo"), + (r"\b(cat|less|more|head|tail)\s+[^\n|;&]*\.env(\.[a-zA-Z0-9_]+)?\b", "leitura/impressão de arquivo .env"), + (r"\b(cat|less|more|head|tail)\s+[^\n|;&]*(credential|secret)", "leitura/impressão de arquivo de credencial/segredo"), + (r"~/\.ssh", "acesso a ~/.ssh"), + (r"(^|\s)/etc(/|\s|$)", "acesso a /etc"), + (r"(^|\s)/root(/|\s|$)", "acesso a /root"), +] + + +def outside_project_paths(command: str): + """Find absolute paths referenced in the command that live outside /opt/eden + but under /opt/ (i.e. plausibly another project on this host).""" + hits = [] + for m in re.finditer(r"/opt/([a-zA-Z0-9_.\-]+)(/\S*)?", command): + full = m.group(0) + if not full.startswith(PROJECT_ROOT): + hits.append(full) + return hits + + +def decide(command: str): + for pattern, reason in DENY_PATTERNS: + if re.search(pattern, command, re.IGNORECASE): + return "deny", reason + other_projects = outside_project_paths(command) + if other_projects: + return "deny", f"referencia caminho fora de {PROJECT_ROOT}: {', '.join(other_projects[:3])}" + for pattern, reason in ASK_PATTERNS: + if re.search(pattern, command, re.IGNORECASE): + return "ask", reason + return "allow", None + + +def main(): + try: + payload = json.load(sys.stdin) + command = (payload.get("tool_input") or {}).get("command", "") or "" + except Exception: + # Fail open: if we can't parse input, don't block the tool call. + print(json.dumps({})) + return + + decision, reason = decide(command) + if decision == "allow": + print(json.dumps({})) + return + + output = { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": decision, + "permissionDecisionReason": f"[eden-guard] {reason} (EDEN_MASTER_PROMPT_CLAUDE.md §2.1/§3.4)", + } + } + print(json.dumps(output)) + + +if __name__ == "__main__": + main() diff --git a/.claude/hooks/guard_file.py b/.claude/hooks/guard_file.py new file mode 100755 index 0000000..b1db5d0 --- /dev/null +++ b/.claude/hooks/guard_file.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""PreToolUse guard for Read/Write/Edit/Glob/Grep (EDEN, Master Prompt §2.1/§3.4). + +Blocks access to paths outside the project root and to ~/.ssh or /etc, and +asks for confirmation before reading a .env/credential-looking file. Fails +open on any internal error. +""" +import json +import os +import re +import sys + +PROJECT_ROOT = "/opt/eden" +HOME = os.path.expanduser("~") + +SECRET_NAME_RE = re.compile(r"(^|/)(\.env(\..+)?|.*credential.*|.*secret.*)$", re.IGNORECASE) + + +def extract_path(tool_input: dict) -> str: + for key in ("file_path", "path", "notebook_path"): + if key in tool_input: + return tool_input[key] or "" + # Grep/Glob use "path" for the search root; pattern itself isn't a filesystem path. + return "" + + +def decide(path: str): + if not path: + return "allow", None + abspath = os.path.abspath(path) + + ssh_dir = os.path.join(HOME, ".ssh") + if abspath == ssh_dir or abspath.startswith(ssh_dir + os.sep): + return "deny", "acesso a ~/.ssh" + if abspath == "/etc" or abspath.startswith("/etc" + os.sep): + return "deny", "acesso a /etc" + if abspath == "/root" or abspath.startswith("/root" + os.sep): + return "deny", "acesso a /root" + + if abspath.startswith("/opt/") and not ( + abspath == PROJECT_ROOT or abspath.startswith(PROJECT_ROOT + os.sep) + ): + return "deny", f"caminho fora de {PROJECT_ROOT} (outro projeto no host)" + + basename = os.path.basename(abspath) + if SECRET_NAME_RE.match(basename): + return "ask", f"leitura/escrita de arquivo que parece conter segredo ({basename})" + + return "allow", None + + +def main(): + try: + payload = json.load(sys.stdin) + tool_input = payload.get("tool_input") or {} + path = extract_path(tool_input) + except Exception: + print(json.dumps({})) + return + + decision, reason = decide(path) + if decision == "allow": + print(json.dumps({})) + return + + output = { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": decision, + "permissionDecisionReason": f"[eden-guard] {reason} (EDEN_MASTER_PROMPT_CLAUDE.md §2.1/§3.4)", + } + } + print(json.dumps(output)) + + +if __name__ == "__main__": + main() diff --git a/.claude/hooks/post_write_check.sh b/.claude/hooks/post_write_check.sh new file mode 100755 index 0000000..7c07cae --- /dev/null +++ b/.claude/hooks/post_write_check.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# PostToolUse hook for Write|Edit (EDEN, Master Prompt §3.4). +# Best-effort incremental lint/typecheck for files under apps/, packages/, infra/. +# Fails open: if pnpm/node/turbo aren't installed yet (Fase 0 / early Fase 1), +# this prints a short notice and exits 0 without blocking anything. +set -euo pipefail + +PROJECT_ROOT="/opt/eden" +INPUT_JSON="$(cat)" + +FILE_PATH="$(python3 -c ' +import json, sys +try: + data = json.load(sys.stdin) + ti = data.get("tool_input") or {} + print(ti.get("file_path") or ti.get("notebook_path") or "") +except Exception: + print("") +' <<< "$INPUT_JSON")" + +# Only act on files under apps/, packages/, or infra/. +case "$FILE_PATH" in + "$PROJECT_ROOT"/apps/*|"$PROJECT_ROOT"/packages/*|"$PROJECT_ROOT"/infra/*) ;; + *) exit 0 ;; +esac + +if ! command -v pnpm >/dev/null 2>&1; then + echo '{"systemMessage":"[eden-hook] pnpm ainda não está instalado neste ambiente — lint/typecheck incremental pulado (esperado na Fase 0/início da Fase 1)."}' + exit 0 +fi + +if [ ! -f "$PROJECT_ROOT/package.json" ]; then + echo '{"systemMessage":"[eden-hook] Monorepo ainda não inicializado (sem package.json na raiz) — lint/typecheck incremental pulado."}' + exit 0 +fi + +cd "$PROJECT_ROOT" +if pnpm turbo run lint typecheck --filter="...[HEAD^1]" >/tmp/eden-post-write-check.log 2>&1; then + exit 0 +else + TAIL="$(tail -n 20 /tmp/eden-post-write-check.log | tr '\n' ' ' | cut -c1-800)" + echo "{\"systemMessage\":\"[eden-hook] lint/typecheck incremental falhou: ${TAIL}\"}" + exit 0 +fi diff --git a/.claude/hooks/stop_secret_check.sh b/.claude/hooks/stop_secret_check.sh new file mode 100755 index 0000000..5cf272d --- /dev/null +++ b/.claude/hooks/stop_secret_check.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Stop hook (EDEN, Master Prompt §2.2/§3.4). Advisory only — never blocks. +# If the project is under git, scans the working tree diff (staged+unstaged) +# for obvious secret patterns and for critical TODO/FIXME markers introduced +# without a tracking reference. Silent (no output) when there's nothing to +# flag or when git/the repo isn't set up yet (expected in Fase 0). +set -uo pipefail + +PROJECT_ROOT="/opt/eden" +cd "$PROJECT_ROOT" 2>/dev/null || exit 0 + +if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + exit 0 +fi + +DIFF="$(git diff HEAD 2>/dev/null; git diff --cached 2>/dev/null)" +[ -z "$DIFF" ] && exit 0 + +SECRET_HITS="$(echo "$DIFF" | grep -E -i '^\+.*(AKIA[0-9A-Z]{16}|BEGIN (RSA|EC|OPENSSH|PRIVATE) KEY|password\s*=\s*["'"'"'][^"'"'"']+|api[_-]?key\s*=\s*["'"'"'][^"'"'"']+|secret\s*=\s*["'"'"'][^"'"'"']+)' || true)" +TODO_HITS="$(echo "$DIFF" | grep -E -i '^\+.*(TODO|FIXME).*(CRITICAL|SECURITY|URGENT)' || true)" + +if [ -z "$SECRET_HITS" ] && [ -z "$TODO_HITS" ]; then + exit 0 +fi + +MSG="[eden-hook] Verificação de fim de etapa (Master Prompt DoD):" +if [ -n "$SECRET_HITS" ]; then + MSG="$MSG Possível segredo em claro no diff (revisar antes de commitar)." +fi +if [ -n "$TODO_HITS" ]; then + MSG="$MSG TODO/FIXME crítico introduzido sem rastreamento (issue/ADR)." +fi + +python3 -c "import json,sys; print(json.dumps({'systemMessage': sys.argv[1]}))" "$MSG" +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..198c1a0 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,50 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "python3 \"/opt/eden/.claude/hooks/guard_bash.py\"", + "timeout": 10 + } + ] + }, + { + "matcher": "Read|Write|Edit|Glob|Grep", + "hooks": [ + { + "type": "command", + "command": "python3 \"/opt/eden/.claude/hooks/guard_file.py\"", + "timeout": 10 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "bash \"/opt/eden/.claude/hooks/post_write_check.sh\"", + "timeout": 120, + "statusMessage": "Rodando lint/typecheck incremental (EDEN)..." + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bash \"/opt/eden/.claude/hooks/stop_secret_check.sh\"", + "timeout": 15 + } + ] + } + ] + } +} diff --git a/.claude/skills/eden-domain/SKILL.md b/.claude/skills/eden-domain/SKILL.md new file mode 100644 index 0000000..7e51ffb --- /dev/null +++ b/.claude/skills/eden-domain/SKILL.md @@ -0,0 +1,21 @@ +--- +name: eden-domain +description: Core EDEN business vocabulary and legacy OrçaFácil behavior reference. Load when implementing or reviewing any commercial/customer/contract logic to check exact legacy terminology and state machines before inventing new ones. +--- + +# EDEN domain skill + +Use this skill whenever implementing a feature that has a legacy equivalent in `eden.md`, to avoid reinventing vocabulary or state machines that already exist and were battle-tested in production. + +## When to use +- Naming a new field/entity that might already have an established name in the legacy system (check `references/terminology.md` first). +- Implementing a status/workflow transition — check `references/state-machines.md` for the exact legacy machine before designing a new one. +- Needing the full legacy behavior for a module — read `references/legacy-orcafacil.md` for the section map, then go to `eden.md` directly for the exact section (this skill indexes, it does not duplicate the 4600-line source). + +## References +- `references/terminology.md` — canonical field/concept names from the legacy system, PT-BR, to preserve (see also `docs/glossary.md` at the project root, which is the authoritative version — this file exists for quick in-skill lookup). +- `references/state-machines.md` — the state machines that must be preserved or deliberately superseded via ADR (quote deal_status, client/reseller registration_status, contract states, signature envelope states, timeclock period closure). +- `references/legacy-orcafacil.md` — section index of `eden.md` (which line range covers which module) so you know where to read the exact rule instead of guessing. + +## Rule +Never invent a business rule that `eden.md` already documents. If a legacy rule seems wrong or worth changing, write an ADR (`docs/adr/`) explaining why — don't silently diverge or silently copy a known gap (Master Prompt §1). diff --git a/.claude/skills/eden-domain/references/legacy-orcafacil.md b/.claude/skills/eden-domain/references/legacy-orcafacil.md new file mode 100644 index 0000000..7e829c2 --- /dev/null +++ b/.claude/skills/eden-domain/references/legacy-orcafacil.md @@ -0,0 +1,14 @@ +# eden.md — section index + +Full source: `/opt/eden/eden.md` (4597 lines). Read the exact section directly — this is a navigation index, not a summary substitute. + +| Lines | Module | Key topics | +|---|---|---| +| 1–97 | Intro / how to use this doc | Reading order, why not to implement everything at once | +| 97–687 | 1. Auth, Users, Roles, Permissions, Security | `users`, `roles`, `role_permissions`, `companies`, role weight, feature-key permission model, JWT, password reset, bcrypt/JWT/rate-limit specifics | +| 690–1450 | 2. Products, Quotes, Pricing/Fidelity, Contracts | `products` price tiers, `quotes`, fidelity vs contract_period, discount/markup approval, proportional rateio, financial formulas, "contract" as derived join | +| 1451–2206 | 3. Client & Reseller Registration | `client_registrations`, PF/PJ differences, partners/QSA, public token flow, reseller "Programa de Canais", storage/attachment access control | +| 2207–2822 | 4. Documents, PDF, E-signature | Tiptap templates, merge fields, Chromium PDF rendering + sanitization, signature envelope state machine, OTP, hash-chain audit, public verification | +| 2823–3143 | 5. Fiscal (NCM, CFOP, municipalities) | Fiscal catalogs, sync (auto/manual), `product_fiscal_profiles`, upsert safety thresholds | +| 3144–4141 | 6. Timeclock (Ponto Eletrônico) | Control iD integration, AFD parsing/CRC, apuração engine, time bank, period closure, punch adjustments | +| 4142–4597 | 7. Backoffice diverso | Backup (streaming pg_dump/restore), meeting room/vehicle agenda, welcome page, companies (operational view), Management/ManagerDashboard, mailer, S3 | diff --git a/.claude/skills/eden-domain/references/state-machines.md b/.claude/skills/eden-domain/references/state-machines.md new file mode 100644 index 0000000..cbabc17 --- /dev/null +++ b/.claude/skills/eden-domain/references/state-machines.md @@ -0,0 +1,19 @@ +# EDEN state machines to preserve or deliberately supersede (with ADR) + +## Quote `deal_status` +`orcamento → fechado → (perdido)`. `fechado` only via the close-deal flow (creates/links a client registration in the same transaction). Reversal only by the correction-authorized role. + +## Client / Reseller `registration_status` +`rascunho → pendente_validacao → ativo ⇄ bloqueado/inativo`. No dedicated transition endpoints in legacy for client (generic PATCH); reseller has dedicated approve/block/reactivate actions. EDEN should keep the vocabulary identical across both entities. + +## Contract states (EDEN-new, per ADR-0006) +`draft → pending_signature → active → suspended/cancelled/terminated/expired → renewed`. + +## Signature envelope status (19 states) +`DRAFT → READY → SENT → VIEWED → IDENTITY_PENDING → CONSENT_PENDING → OTP_PENDING → OTP_SENT → OTP_VERIFIED → READY_TO_SIGN → SIGNING → SIGNED → FINALIZING → COMPLETED`, with `CANCELLED/EXPIRED/DECLINED/SUPERSEDED/ERROR` as terminal off-ramps. Port verbatim — this is validated, audited legal-tech logic (see `eden.md` §4.5.1). + +## Timeclock period closure +`ABERTO → EM_CONFERENCIA → FECHADO`, with `/reopen` going directly `FECHADO → ABERTO` (skips EM_CONFERENCIA), gated by a separate permission from the forward transition (segregation of duties). + +## Support ticket (EDEN-new, per Master Prompt §6.13) +`new → triage → in_progress ⇄ waiting_customer/waiting_third_party → resolved → closed`, plus `cancelled`. diff --git a/.claude/skills/eden-domain/references/terminology.md b/.claude/skills/eden-domain/references/terminology.md new file mode 100644 index 0000000..44f4111 --- /dev/null +++ b/.claude/skills/eden-domain/references/terminology.md @@ -0,0 +1,5 @@ +# EDEN terminology (canonical, PT-BR, from legacy) + +See `docs/glossary.md` at the project root for the authoritative, maintained version. This file is a quick lookup mirror — if the two ever diverge, `docs/glossary.md` wins and this file should be updated to match. + +Key terms not to rename without an ADR: `contract_period`, `fidelity_period`, `approval_status`, `deal_status` vs `status` (quote), `client_registration_id` (quote lock trigger), `registration_status` (rascunho/pendente_validacao/ativo/bloqueado/inativo), `role weight`, `feature key` → EDEN's resource+action+scope, `envelope_number`/`verification_id` (signature), `NSR`/`AFD` (timeclock). diff --git a/.claude/skills/eden-finance/SKILL.md b/.claude/skills/eden-finance/SKILL.md new file mode 100644 index 0000000..698bfab --- /dev/null +++ b/.claude/skills/eden-finance/SKILL.md @@ -0,0 +1,15 @@ +--- +name: eden-finance +description: Billing, receivables, and reconciliation conventions for EDEN. Load when implementing billing runs, invoices, AR/AP, boleto/PIX, dunning, or bank reconciliation. +--- + +# EDEN finance skill + +## When to use +- Designing a billing run or invoice consolidation flow → `references/billing.md`. +- Modeling receivables/boleto/dunning → `references/receivables.md`. +- Bank/OFX reconciliation → `references/reconciliation.md`. +- Dunning rule engine specifics → `references/dunning.md`. + +## Rule +Billing (what's owed) ≠ Finance/AR-AP (collecting) ≠ Fiscal (documents) — see ADR-0008. Never let one module's table double as another's; never let a consolidated invoice be silently edited. diff --git a/.claude/skills/eden-finance/references/billing.md b/.claude/skills/eden-finance/references/billing.md new file mode 100644 index 0000000..fd486fe --- /dev/null +++ b/.claude/skills/eden-finance/references/billing.md @@ -0,0 +1,7 @@ +# Billing engine conventions (ADR-0008, Master Prompt §6.10) + +- Entities: `billing_accounts`, `billing_cycles`, `subscriptions/services`, `charge_components`, `usage_charges`, `invoices`, `invoice_items`, `invoice_adjustments`, `billing_runs`, `billing_run_logs`. +- Supports: mensalidade, pró-rata, implantação, locação, SaaS por usuário, franquia, consumo de telefonia, serviços avulsos, descontos contratados, ajustes manuais auditados. +- A billing run must be idempotent and safely re-runnable **before** final consolidation. +- After consolidation, an invoice is never silently edited — use adjustment/credit-debit note or a controlled re-billing flow instead. +- Test explicitly: billing run repeated does not duplicate an invoice (Master Prompt §15.3). diff --git a/.claude/skills/eden-finance/references/dunning.md b/.claude/skills/eden-finance/references/dunning.md new file mode 100644 index 0000000..a2da12f --- /dev/null +++ b/.claude/skills/eden-finance/references/dunning.md @@ -0,0 +1,4 @@ +# Dunning / cobrança conventions (Master Prompt §6.9) + +- Configurable rules as data, not hardcoded logic: X days before due date, on due date, X days after, escalations, suspension/alert where policy allows, channels and templates. +- n8n consumes dunning events without querying tables directly — publish via the transactional outbox (ADR-0009), event catalog owned by `eden-api-integrations`. diff --git a/.claude/skills/eden-finance/references/receivables.md b/.claude/skills/eden-finance/references/receivables.md new file mode 100644 index 0000000..c047a77 --- /dev/null +++ b/.claude/skills/eden-finance/references/receivables.md @@ -0,0 +1,6 @@ +# Accounts receivable conventions (Master Prompt §6.9) + +- A receivable title carries: parcela, competência, emissão, vencimento, juros, multa, desconto, baixa (full/partial), estorno, negociação, status, origem, cliente, contrato, fatura, conta bancária/gateway. +- Boleto: provider abstraction from day one — the bank/gateway must be swappable without rewriting the domain. Store nosso-id, provider, external id, linha digitável, código de barras, PDF/URL, PIX copia-e-cola/QR when the provider offers it, status, provider events, timestamps. +- Webhooks from bank/gateway: idempotent and authenticated, always. +- Test explicitly: a repeated webhook never duplicates a payment (Master Prompt §15.3). diff --git a/.claude/skills/eden-finance/references/reconciliation.md b/.claude/skills/eden-finance/references/reconciliation.md new file mode 100644 index 0000000..5efa293 --- /dev/null +++ b/.claude/skills/eden-finance/references/reconciliation.md @@ -0,0 +1,6 @@ +# Reconciliation conventions (Master Prompt §6.9) + +- Import OFX/CSV and/or bank API. +- Automatic matching by value/date/document; anything ambiguous goes to an exception queue for human review — never silently auto-match an ambiguous case. +- Keep a reconciliation trail (what matched what, when, by whom/what process). +- Same philosophy applies to SaperX × EDEN invoice reconciliation (see `eden-telecom` skill) — reuse the exception-queue pattern rather than inventing a second one. diff --git a/.claude/skills/eden-fiscal/SKILL.md b/.claude/skills/eden-fiscal/SKILL.md new file mode 100644 index 0000000..4a61ea9 --- /dev/null +++ b/.claude/skills/eden-fiscal/SKILL.md @@ -0,0 +1,18 @@ +--- +name: eden-fiscal +description: Fiscal catalog and Focus NFe integration conventions for EDEN (NCM/CFOP/municipality catalogs, product fiscal profiles, NFCom/NFS-e/recibo de locação). Load when implementing any fiscal classification or emission logic. +--- + +# EDEN fiscal skill + +## When to use +- Porting or extending a fiscal catalog table → `references/focus-nfcom.md` / `references/focus-nfse.md` for emission specifics; see `eden.md` §5.2-5.3 (via `eden-domain` skill's index) for the exact catalog shapes. +- Implementing recibo de locação → `references/rental-receipt.md`. + +## References +- `references/focus-nfcom.md` +- `references/focus-nfse.md` +- `references/rental-receipt.md` + +## Rule +A fiscal document is always a consequence of a billing item already classified via `product_fiscal_profiles` — never hardcoded per screen (ADR-0011, Master Prompt §6.11). diff --git a/.claude/skills/eden-fiscal/references/focus-nfcom.md b/.claude/skills/eden-fiscal/references/focus-nfcom.md new file mode 100644 index 0000000..1c1beea --- /dev/null +++ b/.claude/skills/eden-fiscal/references/focus-nfcom.md @@ -0,0 +1,5 @@ +# Focus NFCom adapter (Master Prompt §6.11, ADR-0011) + +Implement: adapter, unique/idempotent reference per document, emission, query, cancellation, webhooks, persistence of the normalized request without secrets, response/status, keys/identifiers, XML/PDF artifacts when available, retries with backoff, dead-letter/manual retry. + +Classification comes from `product_fiscal_profiles` (`document_type = 'NFCOM'`, `nfcom_cclass_id`, etc.) — never inferred ad hoc at emission time. diff --git a/.claude/skills/eden-fiscal/references/focus-nfse.md b/.claude/skills/eden-fiscal/references/focus-nfse.md new file mode 100644 index 0000000..a0865fb --- /dev/null +++ b/.claude/skills/eden-fiscal/references/focus-nfse.md @@ -0,0 +1,3 @@ +# Focus NFS-e adapter (Master Prompt §6.11, ADR-0011) + +Asynchronous flow: envio → status processando → consulta/webhook → autorizada/rejeitada → cancelamento/substituição where applicable. Municipal particularities isolated in configuration/adapter, never hardcoded in the domain (municipalities vary widely — reuse the legacy's `fiscal_nfse_trib_nacional`/`fiscal_nfse_trib_municipal` catalog split). diff --git a/.claude/skills/eden-fiscal/references/rental-receipt.md b/.claude/skills/eden-fiscal/references/rental-receipt.md new file mode 100644 index 0000000..d9bcb0a --- /dev/null +++ b/.claude/skills/eden-fiscal/references/rental-receipt.md @@ -0,0 +1,3 @@ +# Recibo de locação (Master Prompt §6.11) + +Own document type for locação billing when that's the legal/tax classification defined by Handix/contabilidade — not a generic invoice. Versioned template, numbering, issuing legal entity, locatário, competência, itens, valores, and linkage to contract/invoice. Never encode a specific tax interpretation as universal — this classification must be reviewable by Handix's fiscal/accounting team. diff --git a/.claude/skills/eden-inventory/SKILL.md b/.claude/skills/eden-inventory/SKILL.md new file mode 100644 index 0000000..f3791d3 --- /dev/null +++ b/.claude/skills/eden-inventory/SKILL.md @@ -0,0 +1,13 @@ +--- +name: eden-inventory +description: Stock ledger and serialized asset conventions for EDEN. Load when implementing warehouse, stock movement, or serial/patrimônio/MAC tracking logic. +--- + +# EDEN inventory skill + +## When to use +- Modeling any stock movement → `references/stock-ledger.md`. +- Modeling serialized assets (equipment tracked individually) → `references/serialized-assets.md`. + +## Rule +Stock balance is always derived from the movement ledger, never an editable column (ADR-0007). A serial/MAC/patrimônio is never allocated to two simultaneously-active assets — enforce with a database constraint, not just application logic. diff --git a/.claude/skills/eden-inventory/references/serialized-assets.md b/.claude/skills/eden-inventory/references/serialized-assets.md new file mode 100644 index 0000000..78a974f --- /dev/null +++ b/.claude/skills/eden-inventory/references/serialized-assets.md @@ -0,0 +1,7 @@ +# Serialized asset conventions (Master Prompt §6.8) + +Track per unit: serial do fabricante, número de patrimônio, MAC address(es), marca/modelo, produto, warehouse/localização atual, status, cliente/contrato/serviço onde instalado, datas de movimentação, garantia, histórico completo. + +Install lifecycle: reservar estoque → selecionar unidade serializada específica → vincular ao contrato/cliente → movimentar para instalado/comodato → manter rastreabilidade até devolução/baixa. + +Invariant: never allow the same serial/MAC/patrimônio to be simultaneously active on two assets — enforce at the database layer. diff --git a/.claude/skills/eden-inventory/references/stock-ledger.md b/.claude/skills/eden-inventory/references/stock-ledger.md new file mode 100644 index 0000000..18161cb --- /dev/null +++ b/.claude/skills/eden-inventory/references/stock-ledger.md @@ -0,0 +1,7 @@ +# Stock ledger conventions (ADR-0007, Master Prompt §6.8) + +Entities: `warehouses`, `warehouse_locations`, `stock_items`, `stock_lots` (when needed), `stock_movements`, `stock_reservations`, `inventory_counts`, `transfers`, `receipts`, `issues`, `returns`, `rma`. + +Movement types: entrada compra, ajuste entrada/saída, transferência, reserva, liberação de reserva, saída venda, saída instalação, comodato, devolução, RMA, baixa patrimonial. + +Balance is always `SUM(movements)` — never a stored, directly-editable number. diff --git a/.claude/skills/eden-security/SKILL.md b/.claude/skills/eden-security/SKILL.md new file mode 100644 index 0000000..6df97d8 --- /dev/null +++ b/.claude/skills/eden-security/SKILL.md @@ -0,0 +1,21 @@ +--- +name: eden-security +description: Security baseline, data classification, encryption, and audit conventions for EDEN. Load before implementing any new endpoint, secret, cross-tenant data path, or audit-logged action. +--- + +# EDEN security skill + +## When to use +- Adding a new endpoint (check `references/security-baseline.md` for the OWASP checklist that applies to every route). +- Adding a new field that might hold sensitive data (check `references/data-classification.md` before deciding hash vs. encrypt vs. plain). +- Adding a new reversible secret (API key, integration token) — check `references/encryption.md` for the AES-256-GCM + key-versioning pattern. +- Adding an action that should be audited — check `references/audit.md` for the minimum audited-action list and the append-only hash-chain pattern. + +## References +- `references/security-baseline.md` +- `references/data-classification.md` +- `references/encryption.md` +- `references/audit.md` + +## Rule +See `docs/security/threat-model.md` at the project root for the living, authoritative threat model — this skill packages the reusable conventions; the threat model tracks current, module-specific risk decisions. diff --git a/.claude/skills/eden-security/references/audit.md b/.claude/skills/eden-security/references/audit.md new file mode 100644 index 0000000..bd2bb04 --- /dev/null +++ b/.claude/skills/eden-security/references/audit.md @@ -0,0 +1,10 @@ +# Audit conventions — EDEN + +Minimum audited actions (Master Prompt §5.6): login/logout/MFA, user/role/permission changes, discount approval, contract changes, price changes, stock movement, serial/MAC/asset link-unlink, fiscal document issue/cancel, financial write-off/reversal, boleto/billing changes, timeclock period reopening, subscription actions, integration secret/config changes. + +Rules: +- Append-only, enforced at the database level (trigger rejecting UPDATE/DELETE outside an explicit, itself-audited escape hatch) — never rely on "the application just never calls UPDATE." +- Order of events defined by a monotonic sequence (serial/bigserial), never by timestamp alone (timestamps can collide within a transaction — see the legacy signature audit chain's `seq` column). +- Never write a raw secret into audit metadata, ever. +- `super_admin`-equivalent bypasses authorization but never bypasses audit. +- For hash-chained audit trails (e.g., signature envelopes): version the canonicalization algorithm explicitly (`hash_algorithm_version`) so a future bug fix doesn't retroactively invalidate old, correctly-recorded events — treat linkage failures as always-real tampering, content failures on outdated algorithm versions as a legacy warning, not a failure. diff --git a/.claude/skills/eden-security/references/data-classification.md b/.claude/skills/eden-security/references/data-classification.md new file mode 100644 index 0000000..b581be4 --- /dev/null +++ b/.claude/skills/eden-security/references/data-classification.md @@ -0,0 +1,8 @@ +# Data classification — decide before writing a new column + +Per Master Prompt §5.4. Three buckets, decide explicitly, never default to "encrypt everything": + +1. **One-way hash**: passwords, refresh tokens, public tokens that never need recovery. Argon2id for passwords (ADR-0005); SHA-256/HMAC-SHA256 for tokens depending on entropy (see `encryption.md`). +2. **Reversible field encryption (AES-256-GCM)**: anything the application must recover in plaintext to function — integration API keys/tokens (SaperX, Focus NFe, AI providers), device credentials (Control iD), gateway secrets. Root key never in the database; versioned. +3. **Personal data (LGPD)**: minimize collection, access-control + audit + TLS + storage encryption by default; field-level encryption only for items the threat model flags as high-risk (not blanket). +4. **Credit card**: never store CVV; tokenize via PSP; store only token/brand/last-4/non-sensitive metadata. No homegrown card vault. diff --git a/.claude/skills/eden-security/references/encryption.md b/.claude/skills/eden-security/references/encryption.md new file mode 100644 index 0000000..b9eefec --- /dev/null +++ b/.claude/skills/eden-security/references/encryption.md @@ -0,0 +1,8 @@ +# Encryption patterns — EDEN + +See ADR-0005 for the full decision. Quick reference: + +- **Reversible field encryption**: AES-256-GCM, random 96-bit IV per operation, auth tag verified on decrypt. Storage format: `{iv_b64}:{authTag_b64}:{ciphertext_b64}` in a single TEXT column (legacy-proven pattern from Control iD credential storage). Root key derived via SHA-256 of an env-provided secret, normalizing any-length input to 32 bytes. Every encrypted value also stores a `key_version` for rotation. +- **OTP**: HMAC-SHA256 with a server secret (never plain SHA-256 — OTP space is only 10^6, needs secret-based rainbow-table resistance). 6 digits, 5-minute TTL, single use, max attempts with lockout, invalidate-on-new-request. +- **Public link tokens** (client/reseller registration, signature invite): CSPRNG, ≥96 bits for registration tokens, 256 bits for signature invite tokens. Stored as SHA-256 hash only (plain hash sufficient given the token's own entropy — no HMAC needed here, unlike OTP). +- **Passwords**: Argon2id, versioned parameters (see ADR-0005), re-hash on next successful login when parameters change. diff --git a/.claude/skills/eden-security/references/security-baseline.md b/.claude/skills/eden-security/references/security-baseline.md new file mode 100644 index 0000000..91e8b6c --- /dev/null +++ b/.claude/skills/eden-security/references/security-baseline.md @@ -0,0 +1,13 @@ +# Security baseline — every EDEN endpoint + +Checklist (Master Prompt §12): +- Security headers (helmet-equivalent) + adequate CSP. +- CORS by allowlist per environment (legacy had none configured — deliberate improvement). +- Server-side input validation, output encoding, parameterized queries only (never string-concat SQL). +- File upload: real MIME validation, size limits, stored outside webroot, malware-scan hook point. +- Rate limiting: by IP AND by identity/token for sensitive routes (never just one dimension — legacy pattern, keep it). +- Anti-enumeration on auth flows (forgot-password always returns success regardless of whether the email exists). +- Authorization resolved server-side from session — never trust `role`/`customer_id`/`reseller_id`/`legal_entity_id` from the client. +- Mass-assignment protection (explicit field allowlist on every PATCH, mirroring the legacy pattern of CPF/CNPJ never being in the admin-PATCH allowlist). +- SSRF prevention on anything that renders external content (PDF renderer must never navigate the network — `setContent` only, block all outgoing requests). +- Request size limits, secure cookie/token handling, dependency scanning, secret scanning in CI. diff --git a/.claude/skills/eden-telecom/SKILL.md b/.claude/skills/eden-telecom/SKILL.md new file mode 100644 index 0000000..7300856 --- /dev/null +++ b/.claude/skills/eden-telecom/SKILL.md @@ -0,0 +1,13 @@ +--- +name: eden-telecom +description: SaperX integration and telecom usage-billing conventions for EDEN. Load when implementing circuit/DID mapping, consumption import, or SaperX×EDEN reconciliation. +--- + +# EDEN telecom skill + +## When to use +- Building/extending the SaperX adapter → `references/saperx.md`. +- Modeling usage-based billing (CDR/consumption feeding `usage_charges`) → `references/usage-billing.md`. + +## Rule +Never couple internal entities to SaperX's raw payload — always a normalized DTO + `external_id` + origin snapshot (ADR-0010). If an endpoint isn't available/documented, build interface + mock + explicit TODO, never a fabricated response. diff --git a/.claude/skills/eden-telecom/references/saperx.md b/.claude/skills/eden-telecom/references/saperx.md new file mode 100644 index 0000000..e36eba4 --- /dev/null +++ b/.claude/skills/eden-telecom/references/saperx.md @@ -0,0 +1,5 @@ +# SaperX adapter conventions (ADR-0010, Master Prompt §6.12) + +Requirements: per-environment config, encrypted token (AES-256-GCM), IP-allowlist support if required, timeout, safe retries, local rate limiting, correlation id, logs never containing the token, circuit breaker where appropriate, independent healthcheck. + +Goals: associate EDEN customer ↔ SaperX customer/circuit; import/query circuits; import DIDs/numbers when the API allows; fetch invoices/closings; fetch components (mensalidade, ligações, SVA); import consumption/CDR for billing/audit when needed; reconcile SaperX value × EDEN invoice; expose the permitted view in the subscriber portal. diff --git a/.claude/skills/eden-telecom/references/usage-billing.md b/.claude/skills/eden-telecom/references/usage-billing.md new file mode 100644 index 0000000..da66c07 --- /dev/null +++ b/.claude/skills/eden-telecom/references/usage-billing.md @@ -0,0 +1,5 @@ +# Usage-based billing conventions (telecom) + +Consumption (CDR, minutes, franquia usage) imported from SaperX feeds `usage_charges` in the billing engine (see `eden-finance` skill's `billing.md`). Coordinate the exact mapping with `eden-finance` before building the import path — this skill owns the *source* of usage data, not the billing calculation itself. + +Legacy pricing fields to preserve on `products` (owned by `eden-commercial`, referenced here for context): `metered`, `minutes_allowance`, `tariff_rates` (`{tipo: {normal, reduced}}` for LC/LDN/VC1/VC2/VC3/LDI), `has_ldi`. diff --git a/.claude/skills/eden-timeclock/SKILL.md b/.claude/skills/eden-timeclock/SKILL.md new file mode 100644 index 0000000..0029730 --- /dev/null +++ b/.claude/skills/eden-timeclock/SKILL.md @@ -0,0 +1,13 @@ +--- +name: eden-timeclock +description: Control iD device integration and AFD (Portaria 671) conventions for EDEN's timeclock module. Load when implementing device communication, AFD parsing, or attendance calculation logic. +--- + +# EDEN timeclock skill + +## When to use +- Implementing/reviewing Control iD device communication → `references/controlid.md`. +- Implementing/reviewing AFD parsing, CRC validation, or the apuração engine → `references/afd-671.md`. + +## Rule +`afd_records` (raw punch data) is immutable — no UPDATE/DELETE route, ever, at any privilege level. See `eden-hr-timeclock` agent for the full non-negotiable invariant list. diff --git a/.claude/skills/eden-timeclock/references/afd-671.md b/.claude/skills/eden-timeclock/references/afd-671.md new file mode 100644 index 0000000..af41f08 --- /dev/null +++ b/.claude/skills/eden-timeclock/references/afd-671.md @@ -0,0 +1,10 @@ +# AFD (Portaria MTP 671/2021) conventions + +- Layout version 004, ISO-8859-1 text, `\r\n` line separators. Each line: 9-digit NSR + 1-digit record type. +- Only decode fields with an exactly-documented byte position; anything undocumented goes into a raw-tail capture — never guessed. +- CRC-16/KERMIT (poly 0x1021 reflected as 0x8408, init 0x0000, no final XOR) validates record types 2/3/4. Type 7 uses its own hash chain (never recomputed). Types 1/5/6/9 have no validatable CRC. +- CPF normalization: take the last 11 numeric digits of the raw field (never assume exact formatting) — same rule used to match against `timeclock_employees.cpf` and to relink orphaned records. +- Idempotency: whole-file (SHA-256) at import time, per-record (`device_id + nsr` unique) at insert time. +- `afd_records` is immutable — invalid/unsupported-layout rows are still inserted (never discarded), just excluded from the apuração calculation (`validation_status IN ('VALID','WARNING')` filter). +- Apuração timezone is always fixed `America/Sao_Paulo`, independent of server timezone (Brazil has had no DST since 2019). +- Tolerance (Art. 58 §1º CLT): per-event tolerance capped by a daily aggregate budget, both configurable per work schedule, not hardcoded. diff --git a/.claude/skills/eden-timeclock/references/controlid.md b/.claude/skills/eden-timeclock/references/controlid.md new file mode 100644 index 0000000..dc83955 --- /dev/null +++ b/.claude/skills/eden-timeclock/references/controlid.md @@ -0,0 +1,9 @@ +# Control iD (REP iDClass) integration conventions + +- Transport quirk: the device's firmware sends LF-only line terminators for most headers but still closes with `\r\n\r\n` before the body — this breaks strict HTTP/1.1 parsers (Node's `llhttp`). Communicate via raw socket (`net`/`tls`), building the request manually, with a tolerant response parser trying multiple header/body boundary separators. +- Credential storage: AES-256-GCM, reversible (the app must recover the plaintext password to authenticate against the device on demand) — see `eden-security` skill's `encryption.md`. +- No persistent device session — login fresh on every operation. +- CPF is the device's own user identifier (mode 671) — no separate "controlid_user_id" concept. +- CPF travels as a `Number` in the Control iD API (loses leading zeros by design of their API) — always re-pad to 11 digits when reading back. +- All device operations run sequentially, never in parallel — embedded devices are low-throughput; batch operations (test-all-connections, sync-all-employees) iterate one device/employee at a time. +- Categorize errors (auth/timeout/http/invalid-response/unreachable/tls/unknown) and translate to a user-facing message — never leak raw technical error text to the end user. diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ab81ec0 --- /dev/null +++ b/.env.example @@ -0,0 +1,63 @@ +# EDEN — variáveis de ambiente (Fase 1 bootstrap) +# Copiar para `.env` e preencher com valores reais. +# Nunca commitar `.env` — já está no .gitignore. + +# --- Banco de dados (Postgres 18, container eden-postgres) --- +EDEN_DATABASE_NAME=eden +EDEN_DATABASE_USER=eden +EDEN_DATABASE_PASSWORD= +# String de conexão completa, usada por packages/database e apps/api/apps/worker. +# Dentro do compose, o host é o nome do serviço (eden-postgres); fora, localhost +# na porta mapeada abaixo. +DATABASE_URL=postgres://eden:CHANGE_ME@localhost:55432/eden + +# Porta do Postgres mapeada ao host SÓ em desenvolvimento (nunca em produção — +# ver docs/adr/0002-container-topology.md). Deixar em branco/comentar em prod. +EDEN_POSTGRES_DEV_PORT=55432 + +# --- Portas das aplicações (cada uma em container/porta distintos — ADR-0002) --- +EDEN_API_PORT=8080 +EDEN_CORE_PORT=3001 +EDEN_PARCEIROS_PORT=3002 +EDEN_ASSINANTE_PORT=3003 + +# --- Autenticação / sessão (ADR-0003) --- +# Gerar com: openssl rand -hex 48 +JWT_SIGNING_SECRET= +# Segredo do servidor para HMAC de OTP (nunca reaproveitar o JWT secret) — +# gerar com: openssl rand -hex 32 +SIGNATURE_OTP_SECRET= + +# --- Criptografia de campo reversível (ADR-0005) --- +# Chave raiz para AES-256-GCM (segredos de integração, credenciais reversíveis). +# Gerar com: openssl rand -hex 32. Rotação é operação auditada — ver docs/security/threat-model.md. +EDEN_FIELD_ENCRYPTION_KEY= + +# --- Bootstrap do primeiro superadmin (só usado se `users` estiver vazia) --- +EDEN_SUPERADMIN_EMAIL= +EDEN_SUPERADMIN_PASSWORD= + +# --- SMTP (e-mail) --- +SMTP_HOST= +SMTP_PORT=465 +SMTP_USER= +SMTP_PASS= +SMTP_FROM= + +# --- S3 / storage de objetos (compatível MinIO ou AWS S3) --- +S3_ENDPOINT= +S3_REGION=us-east-1 +S3_FORCE_PATH_STYLE=true +S3_ACCESS_KEY_ID= +S3_SECRET_ACCESS_KEY= +S3_BUCKET= + +# --- URL pública base (usada para montar links absolutos em e-mails) --- +PUBLIC_BASE_URL=http://localhost:3001 + +# --- Integrações externas (preenchidas quando cada adapter for implementado) --- +FOCUS_NFE_API_TOKEN= +FOCUS_NFE_BASE_URL= +SAPERX_API_TOKEN= +SAPERX_BASE_URL= +CONTROLID_ENCRYPTION_KEY= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3dd1183 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Dependencies +node_modules/ +.pnpm-store/ + +# Build outputs +dist/ +build/ +.turbo/ +*.tsbuildinfo + +# Env / secrets +.env +.env.*.local +.env.local +*.pem +*.key + +# Logs +*.log +npm-debug.log* +pnpm-debug.log* + +# Test / coverage +coverage/ +.nyc_output/ + +# Editor +.vscode/* +!.vscode/extensions.json +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Purchased third-party theme asset — too large to version; extracted +# components live in packages/ui instead (see docs/project-inventory.md). +tema_do_Eden.zip + +# Scratch space for one-off inventory/extraction work +.tmp/ + +# Local Postgres data volume if ever bind-mounted for debugging +infra/docker/pgdata/ + +# Claude Code local overrides (personal, never shared) +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a9a4e25 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,74 @@ +# EDEN — CLAUDE.md + +ERP Handix. Substitui o legado OrçaFácil. Três apps web (Core/Parceiros/Assinante) sobre um backend/API/identidade únicos. Ver `EDEN_MASTER_PROMPT_CLAUDE.md` para a missão e regras completas, `eden.md` para as regras de negócio do legado — não duplicar o conteúdo desses arquivos aqui. + +## Stack + +Node 22 LTS · pnpm workspaces + Turborepo · TypeScript strict · NestJS (API) · React + Vite + Tailwind (frontends) · PostgreSQL 18 · Docker Compose. + +## Comandos principais + +```bash +pnpm install # instala tudo no monorepo +pnpm dev # sobe todos os apps em modo dev (turbo) +pnpm build # build de todo o monorepo +pnpm lint / pnpm typecheck / pnpm test + +pnpm db:migrate # aplica migrations pendentes (packages/database) +pnpm db:migrate:down # reverte a última migration +pnpm db:migrate:create # cria uma nova migration + +docker compose --env-file .env up -d # sobe Postgres + API + worker + as 3 apps +docker compose --env-file .env up -d eden-postgres # só o banco, para rodar API/apps localmente fora do container +``` + +## Estrutura do monorepo + +``` +apps/ + api/ NestJS — API principal, único serviço com credencial de banco + worker/ jobs assíncronos (BullMQ, quando Redis existir) + core-web/ EDEN Core (ERP interno) + reseller-web/ EDEN Parceiros + subscriber-web/ EDEN Assinante +packages/ + database/ migrations SQL versionadas (node-pg-migrate) + client de query + contracts/ DTOs/schemas/eventos compartilhados entre apps + ui/ Design System (derivado do tema DreamsERP) + auth/ SDK de auth client-side comum às 3 apps + observability/ logging estruturado, correlation id + config/ schema/validação de env compartilhado + testing/ helpers de teste compartilhados + integrations/ adapters Focus NFe, SaperX, Control iD, S3, SMTP + domain-shared/ tipos/constantes de domínio compartilhados +infra/docker/ Dockerfiles + compose.yaml (topologia: ver docs/adr/0002-container-topology.md) +docs/ arquitetura, ADRs, threat model, plano de implementação +.claude/ agentes, skills e hooks especializados do projeto +``` + +## Convenções de código + +- TypeScript strict em tudo; sem `any` implícito. +- Toda query SQL parametrizada; nenhuma concatenação de string com input do usuário. +- Dinheiro sempre `NUMERIC` no banco / `string`-decimal ou biblioteca decimal no código — nunca `float`/`Number` para valores monetários armazenados. +- IDs técnicos em UUID; código humano sequencial só quando o domínio precisar (ex.: `client_code`), nunca reaproveitando o UUID. +- Toda tabela de agregado relevante tem `created_at/updated_at/created_by/updated_by`. +- Nenhuma decisão de autorização confia em campo vindo do cliente (`role`, `reseller_id`, `customer_id`, `legal_entity_id`) — sempre resolvida a partir da sessão no servidor. + +## Regras de segurança (resumo — detalhe em `docs/security/threat-model.md`) + +- Segredo nunca em git/log/doc/fixture. `.env` nunca commitado (`.gitignore` já cobre). +- Campo sensível classificado explicitamente: hash unidirecional, criptografia reversível (AES-256-GCM, chave versionada) ou plano com controle de acesso — nunca "criptografar tudo" sem critério. +- Toda rota nova: autenticação + autorização server-side + validação de input, mínimo. + +## Definição de pronto + +Ver Master Prompt §20. Resumo: regra de negócio documentada, migration consistente, backend + authz + frontend implementados, estados loading/error/empty tratados, testes unit/integration (+E2E se fluxo crítico), OpenAPI atualizado, sem segredo/TODO crítico, code-reviewer e QA executados, documentação atualizada. + +## Referências + +- `EDEN_MASTER_PROMPT_CLAUDE.md` — missão, regras operacionais, arquitetura alvo completa. +- `eden.md` — especificação funcional do legado OrçaFácil (fonte das regras de negócio). +- `docs/architecture.md`, `docs/adr/`, `docs/security/threat-model.md`, `docs/implementation-plan.md`, `docs/progress.md`. +- `.claude/agents/` — subagentes especializados por domínio. +- `.claude/skills/` — skills de domínio (carregadas sob demanda). diff --git a/EDEN_MASTER_PROMPT_CLAUDE.md b/EDEN_MASTER_PROMPT_CLAUDE.md new file mode 100644 index 0000000..a7b4cd9 --- /dev/null +++ b/EDEN_MASTER_PROMPT_CLAUDE.md @@ -0,0 +1,1564 @@ +# EDEN — MASTER PROMPT PARA CLAUDE CODE + +## 0. MISSÃO + +Você é o **Principal Engineer, Software Architect, Security Engineer, ERP Product Architect e Tech Lead** responsável por construir o **EDEN**, novo ERP da Handix, substituindo e evoluindo o sistema legado OrçaFácil. + +O EDEN não é apenas uma reescrita. Ele deve se tornar a plataforma operacional central da Handix, cobrindo **vendas, CRM, clientes, contratos, assinaturas, financeiro, faturamento recorrente, fiscal, estoque, ativos, suporte técnico, telecom, revendas, assinantes, RH/ponto, documentos, automações e auditoria**. + +A solução terá **três aplicações web** distintas, compartilhando o mesmo ecossistema de APIs e identidade: + +1. **EDEN Core** — ERP interno Handix. +2. **EDEN Parceiros** — portal/aplicação das revendas. +3. **EDEN Assinante** — portal do cliente/assinante. + +O backend deve nascer preparado para ser consumido por essas três aplicações, integrações externas e automações via n8n. + +A prioridade é **correção de negócio, segurança, auditabilidade, integridade financeira/fiscal e manutenibilidade**. Não sacrificar essas propriedades para terminar mais rápido. + +--- + +# 1. FONTES DE VERDADE E ORDEM DE PRECEDÊNCIA + +Antes de implementar qualquer coisa: + +1. Ler **integralmente** o arquivo `eden.md` existente na raiz/pasta do projeto. +2. Localizar e extrair `tema_do_Eden.zip` somente dentro do projeto. +3. Localizar e utilizar os assets: + - `Eden_logo_horizontal.png` + - `Eden_logo_vertical.png` + - `eden_fav_ico.png` +4. Inspecionar qualquer código existente antes de substituir ou criar estruturas paralelas. +5. Manter um inventário em `docs/project-inventory.md`. + +Ordem de precedência para decisões: + +1. Este Master Prompt. +2. Regras explícitas do `eden.md` que representam comportamento do negócio. +3. ADRs produzidos durante a arquitetura do EDEN. +4. Código legado apenas como referência de comportamento, nunca como justificativa para copiar uma deficiência conhecida. + +Quando o `eden.md` disser **“avaliar/decidir no Eden”**, tomar uma decisão arquitetural explícita e registrá-la em `docs/adr/`. + +Não copiar bugs, atalhos ou lacunas de segurança do legado apenas para manter fidelidade. + +--- + +# 2. REGRAS OPERACIONAIS DO CLAUDE CODE + +## 2.1 Trabalhar de forma autônoma, mas segura + +- Não pedir autorização para tarefas rotineiras **dentro deste projeto** quando já houver informação suficiente. +- Não usar `--dangerously-skip-permissions`. +- Não acessar outros servidores, outras aplicações, `/root`, diretórios de outros projetos, chaves SSH, credenciais globais ou serviços externos que não sejam explicitamente necessários e autorizados. +- Não executar SSH/SCP/rsync para hosts externos. +- Não alterar firewall, roteador, Docker de outros projetos, systemd global ou rede do host sem necessidade explícita do EDEN. +- Não apagar dados ou volumes existentes sem uma estratégia de backup e rollback. +- Se uma operação puder destruir dados, preferir uma alternativa reversível. +- Todo comando deve ser executado a partir da raiz do projeto ou de subdiretório pertencente ao EDEN. + +## 2.2 Segredos + +- Nunca colocar senha, API key, token, segredo criptográfico ou credencial em Git, documentação, fixture, screenshot, log ou mensagem de teste. +- Segredos fornecidos pelo operador devem ir somente para `.env`/secret store local e arquivos ignorados pelo Git. +- Criar `.env.example` apenas com nomes e exemplos não sensíveis. +- Garantir `.env`, `.env.*.local`, certificados e dumps sensíveis no `.gitignore`. +- Se detectar segredo versionado, parar aquela mudança, remover do staging e registrar incidente em `docs/security-findings.md` sem reproduzir o segredo. + +As credenciais iniciais de banco e do primeiro superadministrador foram fornecidas pelo operador fora deste documento. Usá-las apenas para bootstrap local se estiverem disponíveis como variáveis de ambiente. **Não gravá-las neste arquivo.** + +Variáveis esperadas para bootstrap: + +- `EDEN_DATABASE_NAME=eden` +- `EDEN_DATABASE_USER` +- `EDEN_DATABASE_PASSWORD` +- `EDEN_SUPERADMIN_EMAIL` +- `EDEN_SUPERADMIN_PASSWORD` + +Além disso, preparar no `.env.example` as configurações de SMTP/e-mail e S3. + +## 2.3 Não parar no primeiro obstáculo + +Quando existir ambiguidade não bloqueante: + +1. escolher a alternativa mais segura e coerente; +2. registrar a hipótese em `docs/assumptions.md`; +3. criar ADR se a decisão for arquitetural; +4. continuar. + +Somente considerar realmente bloqueante aquilo que não puder ser inferido, simulado, mockado ou isolado com adapter. + +--- + +# 3. ESTRATÉGIA DE CLAUDE CODE: ORQUESTRADOR + AGENTES + SKILLS + +Não concentrar todo o conhecimento em um único `CLAUDE.md` gigante. + +Criar uma arquitetura Claude Code com **progressive disclosure**. + +## 3.1 `CLAUDE.md` da raiz + +Manter enxuto e operacional. Deve conter: + +- missão do projeto; +- stack; +- comandos principais; +- convenções de código; +- estrutura do monorepo; +- regras de segurança; +- definição de pronto; +- referências para `docs/` e `.claude/skills/`. + +Não copiar as milhares de linhas do `eden.md` para o `CLAUDE.md`. + +## 3.2 Subagentes obrigatórios + +Criar em `.claude/agents/` agentes especializados, com descrições precisas, exemplos de gatilho, responsabilidades, processo e formato de saída: + +1. `eden-architect` — arquitetura global, bounded contexts, ADRs, consistência entre módulos. +2. `eden-database` — PostgreSQL, schema, migrations, constraints, índices, performance e integridade. +3. `eden-security` — IAM, OWASP, LGPD, criptografia, auditoria, secrets, threat modeling. +4. `eden-commercial` — CRM, leads, oportunidades, ofertas, aprovações, contratos, revendas e comissões. +5. `eden-finance` — contas a receber/pagar, boleto, cobrança, conciliação, fluxo de caixa, faturamento recorrente. +6. `eden-fiscal` — NFCom, NFS-e, recibo de locação, tributação configurável, integração Focus NFe. +7. `eden-inventory` — produtos, warehouses, estoque, serial, patrimônio, MAC, comodato, instalação, RMA. +8. `eden-telecom` — contratos de telecom, DIDs/circuitos, consumo, SaperX, billing de telecom. +9. `eden-support` — chamados, SLA, filas, ativos, contratos, incidentes, OS e atendimento. +10. `eden-hr-timeclock` — Control iD, AFD, Portaria 671, banco de horas, ajustes e fechamento. +11. `eden-frontend` — Design System, Dreams ERP theme, Core/Parceiros/Assinante, acessibilidade e UX. +12. `eden-api-integrations` — REST/OpenAPI, webhooks, n8n, idempotência, outbox, integrações externas. +13. `eden-qa` — testes unitários, integração, contrato, E2E e cenários críticos. +14. `eden-code-reviewer` — revisão de diff com foco em bugs reais, segurança e aderência às regras do projeto. + +Usar `model: inherit` por padrão. Restringir tools quando fizer sentido pelo princípio do menor privilégio. + +O `eden-code-reviewer` deve reportar apenas problemas de alta confiança e nunca substituir o QA. + +## 3.3 Skills obrigatórias + +Criar skills modulares em `.claude/skills/`, mantendo cada `SKILL.md` curto e movendo conteúdo detalhado para `references/`. + +Estrutura mínima: + +```text +.claude/skills/ + eden-domain/ + SKILL.md + references/ + legacy-orcafacil.md + terminology.md + state-machines.md + eden-security/ + SKILL.md + references/ + security-baseline.md + data-classification.md + encryption.md + audit.md + eden-finance/ + SKILL.md + references/ + billing.md + receivables.md + reconciliation.md + dunning.md + eden-fiscal/ + SKILL.md + references/ + focus-nfcom.md + focus-nfse.md + rental-receipt.md + eden-inventory/ + SKILL.md + references/ + stock-ledger.md + serialized-assets.md + eden-telecom/ + SKILL.md + references/ + saperx.md + usage-billing.md + eden-timeclock/ + SKILL.md + references/ + controlid.md + afd-671.md +``` + +Extrair do `eden.md` para essas referências somente o necessário para execução; preservar o `eden.md` original intacto como documentação de origem. + +## 3.4 Hooks obrigatórios + +Criar hooks seguros e revisáveis: + +### `PreToolUse` +Bloquear ou pedir confirmação para: +- comandos fora do projeto; +- `rm -rf /`, `docker system prune`, remoção ampla de volumes; +- leitura/escrita em `~/.ssh`, `/etc`, outros projetos; +- impressão de `.env`/secrets; +- comandos SSH externos. + +### `PostToolUse` +Após mudanças relevantes em código: +- format quando necessário; +- lint incremental; +- typecheck incremental; +- testes do package afetado quando razoável. + +### `Stop` +Antes de declarar uma etapa concluída, validar: +- build relevante passa; +- typecheck passa; +- lint passa; +- migrations estão consistentes; +- testes críticos passam; +- não há TODO/FIXME crítico recém-criado; +- não há segredo em diff; +- documentação/ADR necessária foi atualizada. + +Hooks determinísticos devem preferir scripts; hooks contextuais podem usar prompt-based hooks. + +--- + +# 4. ARQUITETURA TÉCNICA ALVO + +Construir como **monorepo TypeScript**. + +## 4.1 Stack recomendada + +- **Node.js LTS atual**. +- **TypeScript strict**. +- **pnpm workspaces** + Turborepo ou solução equivalente simples e estável. +- Backend: **NestJS** com arquitetura modular por domínio. +- API: REST JSON + **OpenAPI 3.1**. +- Frontends: **React + Vite**. +- UI: **Tailwind CSS**, reaproveitando visual e componentes do `tema_do_Eden.zip` sem acoplar regra de negócio ao template. +- Formulários: React Hook Form + Zod. +- Dados client-side: TanStack Query. +- Banco: **PostgreSQL 18 em container**. +- Fila/cache: Redis + BullMQ para jobs assíncronos, somente quando houver benefício real. +- Storage: S3 compatível via adapter. +- PDF: Playwright/Chromium server-side. +- Testes: Vitest/Jest conforme package + Supertest + Playwright E2E. +- Observabilidade: logs JSON estruturados, correlation id, métricas e health checks. + +Se alguma biblioteca estiver desatualizada/incompatível no ambiente real, escolher a alternativa estável equivalente e registrar ADR. + +## 4.2 Estrutura sugerida + +```text +apps/ + api/ # API principal + worker/ # jobs assíncronos + core-web/ # ERP interno + reseller-web/ # portal da revenda + subscriber-web/ # portal do assinante +packages/ + database/ + contracts/ # DTOs/schemas/event contracts compartilhados + ui/ + auth/ + observability/ + config/ + testing/ + integrations/ + domain-shared/ +infra/ + docker/ + migrations/ +docs/ +.claude/ +``` + +Evitar microserviços prematuros. Começar com **modular monolith** no backend, fronteiras claras e eventos internos. Preparar integrações e jobs de maneira extraível, sem pagar o custo operacional de dezenas de serviços desde o primeiro dia. + +## 4.3 PostgreSQL 18 + +Criar `docker-compose.yml`/`compose.yaml` com PostgreSQL 18 e healthcheck. + +Banco lógico inicial: `eden`. + +Requisitos: +- migrations versionadas; +- nenhuma alteração manual de produção sem migration; +- UUID para IDs técnicos; +- códigos humanos sequenciais separados quando necessário; +- `created_at`, `updated_at`, `created_by`, `updated_by` nos agregados relevantes; +- constraints reais no banco, não apenas no frontend; +- índices baseados nos padrões reais de consulta; +- monetary values em `NUMERIC`, nunca float; +- datas de competência/vencimento modeladas explicitamente; +- timezone armazenado em UTC, renderização em timezone apropriado. + +--- + +# 5. IDENTIDADE, ACESSO E SEGURANÇA + +## 5.1 Modelo de identidade + +Uma identidade pode acessar uma ou mais aplicações: + +- `core` +- `reseller` +- `subscriber` + +Modelar isso explicitamente. Não inferir acesso à aplicação apenas pelo nome da role. + +Usuários internos, usuários de revenda e usuários assinantes devem poder coexistir no mesmo ecossistema de identidade com escopos distintos. + +## 5.2 RBAC + escopo de dados + +Preservar o conceito bom do legado de papéis com peso hierárquico, mas evoluir permissões. + +Cada permissão deve poder expressar: + +- recurso/menu/feature; +- ação: `view`, `create`, `edit`, `delete`, `approve`, `export`, `manage` conforme aplicável; +- escopo: `own`, `team`, `reseller`, `legal_entity`, `all`. + +A UI de gerenciamento de papel deve permitir, por menu/módulo, configurar acesso e alcance dos dados. + +`super_admin`: +- acesso irrestrito; +- não pode ser rebaixado/editado por roles inferiores; +- ações sensíveis continuam auditadas; +- bypass de autorização não significa bypass de auditoria ou integridade. + +Roles iniciais: +- `super_admin` +- `admin` +- `backoffice` +- `user` +- `reseller_admin` +- `reseller_user` +- `subscriber_admin` +- `subscriber_user` + +Permitir roles customizadas. + +## 5.3 Autenticação moderna + +Melhorar o legado: + +- password hashing com **Argon2id** usando parâmetros atuais seguros; +- política de senha configurável; +- recuperação de senha sem enumeração de usuário; +- sessões revogáveis; +- access token curto e refresh token rotativo ou sessão server-side segura; +- refresh tokens armazenados em hash; +- cookies `HttpOnly`, `Secure`, `SameSite` quando arquitetura usar browser session; +- CSRF protection quando necessário; +- MFA/TOTP opcional e preparado para ser obrigatório em `super_admin`/admin; +- rate limiting por IP + identidade para endpoints sensíveis; +- lockout progressivo sem criar vetor simples de DoS contra conta; +- histórico de sessões/dispositivos e ação “encerrar todas as sessões”. + +## 5.4 Criptografia e classificação de dados + +Não interpretar “criptografar tudo” como armazenar dados inutilizáveis. + +Aplicar classificação: + +### Hash unidirecional +- senhas; +- refresh tokens; +- tokens públicos quando não houver necessidade de recuperar o valor. + +### Criptografia reversível de campo +Usar **AES-256-GCM** ou primitive equivalente autenticada, com versionamento de chave e nonce único, para segredos que a aplicação precisa recuperar: +- credenciais/API keys de IA; +- tokens SaperX; +- credenciais de Control iD; +- secrets de gateways; +- credenciais de integrações; +- outros secrets operacionais. + +A chave raiz nunca fica no banco. + +### Dados pessoais +Usar minimização, controle de acesso, auditoria, TLS e criptografia de storage; aplicar criptografia de campo para dados definidos no threat model como de alto risco. + +### Cartão de crédito +**Não armazenar CVV.** Preferir tokenização por gateway/PSP e guardar apenas token, bandeira, últimos quatro dígitos e metadados não sensíveis. Não implementar um cofre de cartão caseiro. Se algum fluxo exigir PAN próprio, tratá-lo como projeto PCI DSS separado. + +## 5.5 LGPD + +Implementar arquitetura para: +- minimização; +- finalidade; +- controle de acesso; +- trilha de auditoria; +- retenção configurável; +- anonimização/eliminação quando juridicamente permitido; +- exportação de dados do titular; +- registro de consentimento quando necessário; +- registro de base/finalidade quando aplicável; +- privacy-by-design. + +Não prometer “conformidade LGPD” apenas por criptografar campos. + +## 5.6 Auditoria + +Criar audit log append-only para operações críticas e preservar o padrão forte de hash-chain usado no legado para assinatura. + +Auditar no mínimo: +- login/logout/MFA; +- criação e alteração de usuário/role/permissão; +- aprovação de desconto; +- alteração de contrato; +- alteração de preço; +- movimento de estoque; +- vínculo/desvínculo de serial/MAC/patrimônio; +- emissão/cancelamento fiscal; +- baixa/estorno financeiro; +- alterações de boleto/cobrança; +- reabertura de período de ponto; +- ações de assinatura; +- alterações em segredo/configuração de integração. + +Nunca gravar segredo puro no audit log. + +--- + +# 6. DOMÍNIOS E MÓDULOS DO EDEN + +## 6.1 Organização empresarial + +Criar modelo para: +- grupo empresarial; +- empresas/entidades legais; +- filiais/unidades; +- endereços; +- contas bancárias; +- configurações fiscais por entidade; +- warehouses/almoxarifados por unidade; +- centros de custo; +- séries/documentos por entidade quando aplicável. + +A empresa emissora de contrato não deve continuar sendo apenas uma tabela isolada sem vínculo aos demais domínios. + +## 6.2 CRM e Comercial + +Criar CRM nativo com: +- lead; +- origem/campanha; +- contato; +- empresa/prospect; +- oportunidade; +- pipeline configurável; +- etapas; +- probabilidade; +- responsável; +- tarefas/follow-ups; +- notas; +- anexos; +- motivos de perda; +- conversão lead → oportunidade → cliente/oferta; +- histórico completo. + +Preparar API para leads vindos de tráfego pago/n8n. + +## 6.3 Produtos e catálogo + +Evoluir o legado. + +Produto deve possuir: +- código interno; +- nome; +- descrição; +- tipo: serviço, telecom, SVA, software/SaaS, equipamento, locação, implantação, consumo etc.; +- grupo; +- subgrupo; +- marca; +- unidade de medida; +- ativo/descontinuado; +- controle de estoque sim/não; +- controle de serial sim/não; +- controle de patrimônio sim/não; +- controle de MAC sim/não; +- fiscal profile; +- dados contábeis/gerenciais necessários; +- custo médio/último custo quando material; +- preço sem fidelidade; +- preço 12 meses; +- preço 24 meses; +- preço 36 meses; +- preço 48 meses; +- implantação; +- tarifação/franquia quando telecom; +- códigos de integrações externas. + +Não duplicar regra fiscal dentro do produto se ela pertencer a um fiscal profile configurável. + +## 6.4 Ofertas + +Preservar cálculos e conceitos documentados no `eden.md`: +- faixa de preço por `contract_period`; +- fidelidade efetiva separada da faixa de preço; +- desconto/markup; +- implantação por produto + implantação geral; +- rateio proporcional; +- aprovação de exceção; +- regras legais de benefício/multa quando aplicáveis; +- snapshot de valores relevantes para a proposta. + +Evoluir aprovação para workflow genérico: +- tipo da aprovação; +- solicitante; +- aprovador/grupo aprovador; +- limite/threshold; +- motivo; +- status; +- timestamps; +- comentários; +- histórico. + +**Regra solicitada:** se o usuário digitar/forçar um valor total/mensal da oferta fora da condição normal, a operação deve exigir autorização de superior conforme matriz de aprovação. + +Nunca confiar no frontend para cálculo financeiro. O backend recalcula e valida antes de salvar. + +## 6.5 Cliente / Assinante — Customer 360 + +Unificar uma visão 360 sem apagar as diferenças PF/PJ do legado. + +Incluir: +- dados cadastrais; +- contatos principal/financeiro/técnico; +- endereços; +- documentos; +- sócios/representantes; +- contratos; +- ofertas; +- serviços ativos; +- números/DIDs/circuitos; +- faturas; +- títulos financeiros; +- documentos fiscais; +- equipamentos instalados; +- chamados; +- interações; +- assinaturas; +- anexos; +- auditoria relevante. + +CPF/CNPJ principal não deve ser alterável por edição administrativa comum sem fluxo controlado. + +## 6.6 Contratos — transformar em agregado de primeira classe + +No legado “contrato” era derivado. No EDEN criar entidade formal. + +Modelar: +- `contracts`; +- `contract_items`; +- `contract_parties`; +- `contract_versions`; +- `contract_documents`; +- `contract_amendments`; +- `contract_renewals`; +- `contract_status_history`; +- `contract_assets`; +- `contract_services`; +- `contract_billing_rules`. + +Contrato deve registrar snapshots jurídicos/comerciais essenciais para impedir que uma alteração futura de produto mude retrospectivamente um contrato assinado. + +Estados sugeridos: +- draft; +- pending_signature; +- active; +- suspended; +- cancelled; +- terminated; +- expired; +- renewed. + +Manter distinção entre: +- prazo comercial da faixa de preço; +- fidelidade/permanência efetiva; +- vigência do contrato; +- ciclo de faturamento. + +## 6.7 Documentos e assinatura eletrônica + +Preservar e portar com alto grau de fidelidade o motor descrito em `eden.md`: +- templates versionados; +- draft/publicação; +- Tiptap JSON; +- merge fields whitelist; +- condicionais; +- repeat rows; +- sanitização; +- PDF server-side; +- documentos congelados; +- signatários snapshot; +- token público em hash; +- OTP por e-mail com HMAC; +- tentativa/TTL/rate limit; +- audit hash-chain; +- certificado; +- verificação pública; +- PDF consolidado final. + +Melhorar qualquer ponto explicitamente identificado como lacuna no legado sem quebrar a força probatória/auditável. + +## 6.8 Estoque, Warehouse, Ativos e Comodato + +Criar estoque de verdade com ledger de movimentos. Nunca representar saldo apenas por um número editável. + +Entidades mínimas: +- warehouses; +- warehouse_locations; +- stock_items; +- stock_lots quando necessário; +- stock_movements; +- stock_reservations; +- serialized_assets; +- asset_assignments; +- inventory_counts; +- transfers; +- receipts; +- issues; +- returns; +- rma; +- asset_maintenance. + +Tipos de movimento: +- entrada compra; +- ajuste entrada; +- ajuste saída; +- transferência; +- reserva; +- liberação de reserva; +- saída venda; +- saída instalação; +- comodato; +- devolução; +- RMA; +- baixa patrimonial. + +Para equipamentos controlados individualmente, registrar: +- serial do fabricante; +- número de patrimônio; +- MAC address(es); +- marca/modelo; +- produto; +- warehouse/localização atual; +- status; +- cliente/contrato/serviço onde está instalado; +- datas de movimentação; +- garantia; +- histórico completo. + +Ao fechar oferta/contrato que contenha equipamento: +1. reservar estoque quando apropriado; +2. posteriormente selecionar unidade serializada específica; +3. vincular ao contrato/cliente; +4. movimentar para instalado/comodato; +5. manter rastreabilidade até devolução/baixa. + +Nunca reutilizar o mesmo serial/MAC/patrimônio simultaneamente em dois ativos ativos. + +## 6.9 Financeiro + +Construir financeiro gerencial/operacional integrado aos contratos. + +### Contas a receber +- títulos; +- parcelas; +- competência; +- emissão; +- vencimento; +- juros; +- multa; +- desconto; +- baixa; +- baixa parcial; +- estorno; +- negociação; +- cobrança; +- status; +- origem do título; +- cliente; +- contrato; +- fatura; +- conta bancária/gateway. + +### Boletos +Criar provider abstraction. O gateway bancário poderá ser conectado posteriormente sem reescrever o domínio. + +Guardar: +- nosso id; +- provider; +- external id; +- linha digitável; +- código de barras; +- PDF/URL quando aplicável; +- PIX copia-e-cola/QR quando provider oferecer; +- status; +- eventos do provider; +- timestamps. + +Webhooks de banco/gateway devem ser idempotentes e autenticados. + +### Cobrança/dunning +Permitir regras configuráveis: +- X dias antes do vencimento; +- no vencimento; +- X dias após atraso; +- escalonamentos; +- suspensão/alerta quando política permitir; +- canais e templates. + +O n8n deve poder consumir eventos sem consultar tabelas diretamente. + +### Contas a pagar +- fornecedor; +- categoria; +- centro de custo; +- competência; +- vencimento; +- parcelas; +- pagamento; +- anexos; +- aprovação; +- recorrência. + +### Conciliação +Preparar: +- importação OFX/CSV e/ou API bancária; +- matching automático por valor/data/documento; +- exceções para revisão; +- trilha de conciliação. + +### Gestão +Dashboards: +- MRR; +- ARR; +- churn financeiro; +- inadimplência; +- aging; +- recebimentos; +- pagamentos; +- fluxo de caixa; +- receita por produto/cliente/revenda; +- margem quando houver custo confiável; +- previsão. + +Nunca confundir faturamento, documento fiscal e recebimento: são eventos relacionados, porém distintos. + +## 6.10 Billing / Faturamento recorrente + +Criar motor de billing separado do contas a receber. + +Modelar: +- billing_accounts; +- billing_cycles; +- subscriptions/services; +- charge_components; +- usage_charges; +- invoices; +- invoice_items; +- invoice_adjustments; +- credit/debit adjustments; +- billing_runs; +- billing_run_logs. + +Suportar: +- mensalidade; +- pró-rata; +- implantação; +- locação; +- SaaS por usuário; +- franquia; +- consumo de telefonia; +- serviços avulsos; +- descontos contratados; +- ajustes manuais auditados. + +Fechamento de billing deve ser idempotente e reexecutável com segurança antes da consolidação final. + +Depois de consolidada, uma fatura não deve ser “editada silenciosamente”; usar ajuste/nota de crédito/débito ou refaturamento controlado. + +## 6.11 Fiscal + +Integrar via adapter com **Focus NFe**. + +Suportar inicialmente: +- NFCom; +- NFS-e; +- recibo de locação quando juridicamente aplicável; +- arquitetura extensível para outros documentos. + +A emissão fiscal deve ser consequência de itens de faturamento classificados, não de lógica hardcoded por tela. + +Criar tax/fiscal profile por produto/serviço e regras por entidade legal/UF/município. + +Separar: +- item comercial; +- item de billing; +- classificação fiscal; +- documento fiscal emitido. + +### Focus NFCom +Implementar: +- adapter; +- referência única/idempotente; +- emissão; +- consulta; +- cancelamento; +- webhooks; +- persistência de request normalizado sem segredo; +- resposta/status; +- chave/identificadores; +- XML/PDF/artefatos quando disponibilizados; +- retries com backoff; +- dead-letter/manual retry. + +### Focus NFS-e +Implementar fluxo assíncrono: +- envio; +- status processando; +- consulta/webhook; +- autorizada/rejeitada; +- cancelamento/substituição quando aplicável; +- particularidades municipais isoladas em configuração/adapter. + +### Recibo de locação +Gerar documento próprio para cobrança de locação quando essa for a classificação jurídica/fiscal definida pela Handix/contabilidade. Template versionado, numeração, entidade emissora, locatário, competência, itens, valores e vínculo com contrato/fatura. + +Não codificar interpretação tributária específica como verdade universal. Regras tributárias devem ser configuráveis e revisáveis pela área fiscal/contábil. + +## 6.12 Integração SaperX + +Criar módulo `integrations/saperx` como adapter isolado. + +Requisitos: +- configuração por ambiente; +- token criptografado; +- suporte à exigência de IP autorizado/whitelist; +- timeout; +- retries seguros; +- rate limiting local; +- correlation id; +- logs sem token; +- circuit breaker quando apropriado; +- healthcheck de integração. + +Objetivos do EDEN: +- associar cliente EDEN ao cliente/circuito SaperX; +- importar/consultar circuitos; +- importar DIDs/números quando API permitir; +- obter faturas/fechamentos; +- obter componentes como mensalidade, ligações e SVA; +- importar consumo/CDR quando necessário para billing/auditoria; +- conciliar valor SaperX × fatura EDEN; +- expor no portal do assinante a visão permitida. + +Nunca acoplar entidades internas ao payload bruto do SaperX. Criar DTO normalizado e salvar `external_id` + snapshot de origem quando necessário. + +Se um endpoint necessário não estiver disponível na documentação/ambiente, implementar interface + mock + TODO de integração externa claramente documentado, sem inventar resposta da API. + +## 6.13 Suporte técnico / Service Desk + +Criar módulo de suporte integrado a cliente, contrato e ativo. + +Entidades/conceitos: +- ticket; +- protocolo humano sequencial; +- categoria/subcategoria; +- prioridade; +- impacto/urgência; +- fila; +- responsável; +- watchers; +- comentários públicos/internos; +- anexos; +- status; +- SLA policy; +- SLA timers; +- primeira resposta; +- resolução; +- pausas justificadas; +- escalonamento; +- ordem de serviço; +- visita técnica; +- ativos afetados; +- serviço/contrato afetado; +- causa/solução; +- satisfação do cliente. + +Estados sugeridos: +- new; +- triage; +- in_progress; +- waiting_customer; +- waiting_third_party; +- resolved; +- closed; +- cancelled. + +SLA deve considerar calendário de atendimento e pausas válidas. + +Portal Assinante deve criar/acompanhar chamados permitidos. +Portal Revenda deve criar/acompanhar chamados da sua base dentro do escopo permitido. + +## 6.14 RH / Controle de Ponto + +Portar o módulo do `eden.md` respeitando os princípios legais e técnicos: +- equipamentos Control iD; +- credencial criptografada reversivelmente; +- AFD Portaria 671; +- AFD bruto imutável; +- idempotência por arquivo/hash e device+NSR; +- parser/CRC; +- S3 do arquivo bruto; +- employees; +- work schedules; +- feriados; +- ajustes aditivos; +- segregação criar/aprovar; +- apuração; +- banco de horas; +- fechamento; +- reabertura com permissão separada e auditoria; +- relatórios. + +Nunca editar marcação legal bruta. + +## 6.15 Agenda, backup e backoffice + +Portar módulos relevantes do legado: +- agenda de salas; +- agenda de carros; +- welcome page; +- gestão de empresas; +- dashboards; +- backup lógico PostgreSQL em streaming para S3. + +Backup deve ter: +- `pg_dump` custom format; +- streaming; +- metadados; +- hash; +- status; +- restauração controlada; +- logs; +- acesso altamente restrito. + +Adicionar política de lifecycle/backup do próprio bucket S3 fora do dump do banco. + +--- + +# 7. APLICAÇÃO EDEN PARCEIROS / REVENDA + +Construir frontend separado, não apenas esconder menu do Core. + +Escopo por revenda. + +Funcionalidades previstas: +- dashboard; +- usuários da própria revenda dentro das permissões; +- leads/oportunidades próprias; +- ofertas próprias; +- clientes próprios em visão permitida; +- contratos próprios; +- comissões; +- documentos; +- faturas/repasse quando aplicável; +- suporte; +- perfil/cadastro da revenda; +- notificações. + +Não permitir vazamento cross-reseller em nenhuma query. Testar isolamento explicitamente. + +Preparar programa de canal finder/recorrente do legado e permitir regras de comissão configuráveis. + +--- + +# 8. APLICAÇÃO EDEN ASSINANTE + +Frontend separado, mobile-first. + +Funcionalidades: +- perfil do assinante; +- usuários/contatos autorizados da conta; +- contratos ativos; +- serviços contratados; +- equipamentos em comodato/instalados visíveis; +- números/DIDs/circuitos quando aplicável; +- consumo de telefonia permitido; +- faturas; +- boleto/PIX; +- histórico de pagamentos; +- NFCom; +- NFS-e; +- recibos; +- documentos; +- assinatura pendente; +- chamados de suporte; +- notificações; +- download seguro de arquivos. + +O assinante só enxerga dados do customer account ao qual está vinculado. + +Documentos nunca devem ser expostos por bucket público. Usar download autorizado/presigned URL de curta duração conforme threat model. + +--- + +# 9. APIs, N8N E EVENTOS + +## 9.1 API-first + +Toda funcionalidade relevante deve ter contrato de API estável. + +Gerar OpenAPI e manter documentação versionada. + +Padrões: +- `/api/v1/...`; +- paginação; +- filtros; +- ordenação; +- códigos de erro consistentes; +- validation errors estruturados; +- correlation id; +- idempotency key em endpoints críticos; +- ETag/versioning otimista onde fizer sentido. + +## 9.2 Integração n8n + +Não dar ao n8n acesso direto ao PostgreSQL como mecanismo principal de integração. + +Criar: +- service accounts/API clients; +- scopes; +- API keys criptografadas ou OAuth client credentials; +- webhook subscriptions; +- assinatura HMAC de webhook; +- retries; +- event id; +- delivery log; +- replay manual; +- dead-letter; +- idempotência do consumidor. + +Eventos iniciais: +- `lead.created`; +- `opportunity.stage_changed`; +- `quote.created`; +- `quote.approval_requested`; +- `quote.approved`; +- `contract.created`; +- `contract.signed`; +- `contract.activated`; +- `invoice.created`; +- `invoice.due_soon`; +- `invoice.overdue`; +- `payment.received`; +- `fiscal_document.authorized`; +- `fiscal_document.rejected`; +- `stock.reservation_required`; +- `asset.installed`; +- `ticket.created`; +- `ticket.sla_at_risk`; +- `ticket.resolved`. + +Implementar transactional outbox para eventos importantes de integração, evitando perder evento após commit de negócio. + +--- + +# 10. TEMA E EXPERIÊNCIA VISUAL + +Usar `tema_do_Eden.zip` como base visual adquirida legalmente pelo projeto. + +Procedimento: +1. extrair em pasta temporária dentro do projeto; +2. inventariar HTML/CSS/JS/assets; +3. identificar componentes reaproveitáveis; +4. converter para componentes React/Tailwind limpos; +5. não importar JS legado do template de maneira indiscriminada; +6. não duplicar dezenas de páginas apenas por copy/paste; +7. criar `packages/ui`/Design System; +8. preservar créditos/licença quando exigidos pela licença comprada; +9. guardar assets próprios do EDEN em pasta apropriada. + +Aplicar logos fornecidos e favicon. + +Requisitos UX: +- responsivo; +- acessível; +- skeleton/loading; +- estados vazios; +- erros úteis; +- tabelas com filtros salvos quando útil; +- busca global em entidades principais; +- dark mode apenas se o template e design system comportarem sem comprometer legibilidade; +- moeda/date/telefone formatados pt-BR; +- arquitetura preparada para i18n pt-BR/en/es sem duplicar código. + +--- + +# 11. MODELO DE DADOS: PRINCÍPIOS OBRIGATÓRIOS + +1. Não modelar processos importantes apenas em JSONB quando houver relacionamento consultável/auditável. +2. JSONB serve para snapshots, metadata, payload externo e configurações flexíveis — não para evitar modelagem. +3. Estados críticos devem ter máquina de estados e validação backend. +4. Toda alteração financeira/fiscal/estoque relevante deve ser auditável. +5. Estoque usa movimentos, não saldo editável. +6. Ledger financeiro/fatura consolidada nunca é “consertado” apagando histórico. +7. Contrato assinado usa snapshots/versionamento. +8. Integrações externas sempre têm `external_id`, provider e estado de sync. +9. Soft-delete apenas quando houver necessidade de preservar histórico; entidades legais/financeiras geralmente devem ser inativadas, não apagadas. +10. `ON DELETE` deve ser escolhido conscientemente por domínio, nunca CASCADE indiscriminado. + +Criar ERD por domínio e um ERD de alto nível em `docs/data-model/`. + +--- + +# 12. SEGURANÇA DE API E WEB + +Aplicar baseline atual de OWASP: +- security headers; +- CSP adequada; +- CORS por allowlist e ambiente; +- input validation server-side; +- output encoding; +- SQL injection prevention; +- file upload validation; +- MIME sniffing protection; +- arquivo fora de webroot; +- malware scan hook preparado para uploads; +- rate limiting; +- anti-enumeration; +- brute-force controls; +- authz em cada endpoint; +- mass-assignment protection; +- SSRF prevention; +- request size limits; +- secure cookie/token handling; +- dependency scanning; +- secret scanning. + +Não confiar em `role` recebido do frontend. +Não confiar em `customer_id`, `reseller_id`, `legal_entity_id` enviados pelo cliente sem verificar escopo do ator. + +--- + +# 13. INTEGRAÇÕES EXTERNAS — PADRÃO ÚNICO + +Toda integração deve seguir adapter/port: + +```text +Domain Service + -> Integration Port + -> Provider Adapter + -> HTTP Client +``` + +Cada adapter deve implementar: +- timeout; +- retry policy; +- idempotência; +- tracing/correlation; +- redaction de segredo; +- normalização de erro; +- health status; +- mocks/fixtures; +- contract tests quando possível. + +Providers iniciais: +- Focus NFe; +- SaperX; +- Control iD; +- SMTP; +- S3; +- boleto/banco futuro; +- IA providers futuros. + +API keys de IA devem ser armazenadas criptografadas e sempre vinculadas a provider/configuração, nunca hardcoded. + +--- + +# 14. JOBS E PROCESSAMENTO ASSÍNCRONO + +Usar worker/fila para: +- envio de e-mail; +- webhooks; +- retries de integração; +- emissão/consulta fiscal assíncrona; +- geração pesada de PDF; +- billing runs; +- notificações de vencimento; +- importações grandes; +- tarefas que não devem prender uma request HTTP. + +Jobs devem ser: +- idempotentes; +- observáveis; +- retryable com backoff; +- possuir dead-letter/estado de falha; +- não duplicar efeitos financeiros/fiscais. + +--- + +# 15. TESTES E QUALIDADE + +## 15.1 Pirâmide + +- unit tests para regras de domínio/cálculo; +- integration tests com PostgreSQL real em container; +- API tests; +- contract tests de adapters; +- E2E Playwright para fluxos críticos. + +## 15.2 Fluxos E2E mínimos obrigatórios + +1. Login e autorização por role/scope. +2. Criar lead → oportunidade → oferta. +3. Oferta normal e oferta com desconto exigindo aprovação. +4. Oferta com fidelidade reduzida exigindo aprovação. +5. Fechar oferta → cadastro cliente → contrato. +6. Gerar documento → envelope → OTP → assinatura → PDF final. +7. Contrato com equipamento → reserva → serial/MAC/patrimônio → instalação. +8. Billing mensal → invoice → contas a receber. +9. Evento “vence em X dias” para automação. +10. Baixa de título sem duplicidade. +11. Emissão fiscal mocked/contract test. +12. Isolamento de revenda A vs revenda B. +13. Isolamento de assinante A vs assinante B. +14. Abrir ticket no portal → atendimento Core → resolução. +15. Importar AFD → apurar → ajustar → aprovar → fechar período. + +## 15.3 Casos invariantes + +Testar explicitamente: +- desconto pendente não vira valor legal aprovado; +- fidelity_period não aprovado não altera vigência legal; +- mesmo serial não pode ser alocado duas vezes; +- estoque nunca fica negativo quando política proíbe; +- webhook repetido não duplica pagamento; +- billing run repetido não duplica fatura; +- mesma referência fiscal não emite documento duplicado; +- usuário de revenda não acessa dados de outra revenda alterando UUID na URL; +- assinante não acessa documento de outro customer account; +- AFD legal bruto não pode ser alterado; +- audit log crítico não pode ser mutado pela aplicação. + +--- + +# 16. OBSERVABILIDADE E OPERAÇÃO + +Implementar: +- `/health/live`; +- `/health/ready`; +- health de Postgres/Redis/S3; +- health separado de integrações externas sem derrubar readiness por indisponibilidade não crítica; +- logs JSON; +- request id/correlation id; +- audit log separado de application log; +- métricas de filas; +- métricas de erro por integração; +- métricas de billing/fiscal; +- dashboard operacional mínimo. + +Não registrar CPF/CNPJ completo, token, senha ou payload sensível indiscriminadamente nos logs. + +--- + +# 17. DOCUMENTAÇÃO OBRIGATÓRIA + +Criar e manter: + +```text +docs/ + architecture.md + assumptions.md + glossary.md + project-inventory.md + security/ + threat-model.md + data-classification.md + encryption.md + auth.md + adr/ + data-model/ + api/ + integrations/ + focus-nfe.md + saperx.md + controlid.md + n8n.md + modules/ + commercial.md + contracts.md + finance.md + billing.md + fiscal.md + inventory.md + support.md + timeclock.md + runbooks/ + backup-restore.md + fiscal-retry.md + billing-run.md + incident-response.md +``` + +Documentação deve explicar decisões, não copiar código. + +--- + +# 18. ORDEM DE IMPLEMENTAÇÃO + +Não tentar implementar tudo simultaneamente. + +## Fase 0 — Descoberta e arquitetura +- ler `eden.md` inteiro; +- inventariar projeto/theme/assets; +- produzir arquitetura; +- glossary; +- bounded contexts; +- ERD alto nível; +- threat model; +- ADRs iniciais; +- backlog por fases; +- bootstrap de agentes/skills/hooks. + +**Gate:** nenhuma feature de negócio antes de a arquitetura base e segurança estarem registradas. + +## Fase 1 — Plataforma +- monorepo; +- Docker; +- Postgres 18; +- migrations; +- config; +- logging; +- auth; +- users; +- roles/permissions/scopes; +- companies/legal entities; +- S3; +- SMTP; +- audit; +- API/OpenAPI; +- design system/theme. + +## Fase 2 — Comercial +- CRM; +- produtos/grupos/subgrupos/marcas; +- pricing tiers; +- ofertas; +- approval workflow; +- clientes; +- revendas. + +## Fase 3 — Contratos/documentos +- contracts first-class; +- templates; +- PDF; +- assinatura; +- portal de cadastro. + +## Fase 4 — Estoque/ativos +- warehouse; +- ledger; +- serial/patrimônio/MAC; +- reserva/instalação/comodato/RMA. + +## Fase 5 — Billing/financeiro +- subscription/billing; +- invoices; +- AR/AP; +- boletos provider abstraction; +- dunning; +- conciliação; +- dashboards. + +## Fase 6 — Fiscal/telecom +- Focus NFCom; +- Focus NFS-e; +- recibos; +- SaperX; +- consumo e conciliação. + +## Fase 7 — Suporte +- service desk; +- SLA; +- OS; +- portal integration. + +## Fase 8 — Portais +- Parceiros completo; +- Assinante completo; +- isolamento e UX. + +## Fase 9 — RH/Ponto + backoffice legado restante +- Control iD; +- AFD; +- banco de horas; +- agenda; +- backup; +- dashboards restantes. + +## Fase 10 — Hardening +- testes E2E completos; +- performance; +- security review; +- access-control review; +- migration rehearsal; +- backup/restore drill; +- runbooks; +- release readiness. + +--- + +# 19. CHECKPOINTS DE GIT + +Se o projeto estiver sob Git: +- verificar `git status` antes de começar; +- nunca descartar mudança pré-existente do usuário; +- commits/checkpoints pequenos por fase lógica quando permitido; +- não commitar segredo; +- não rebase/reset destrutivo de trabalho do usuário; +- antes de cada checkpoint: lint + typecheck + testes relevantes. + +--- + +# 20. DEFINITION OF DONE POR FEATURE + +Uma feature só está pronta quando: + +1. regra de negócio está documentada; +2. migration/schema está consistente; +3. backend implementado; +4. authz server-side implementada; +5. frontend implementado; +6. estados loading/error/empty tratados; +7. audit quando necessário; +8. testes unit/integration adequados; +9. E2E quando fluxo crítico; +10. OpenAPI atualizado; +11. segredo/config via env; +12. sem erro de lint/typecheck; +13. sem TODO crítico; +14. code-reviewer executado; +15. QA executado; +16. documentação atualizada. + +“Página abriu” não significa feature pronta. + +--- + +# 21. CRITÉRIOS DE ACEITAÇÃO GLOBAL DO EDEN + +Antes de considerar o EDEN apto para homologação: + +- `docker compose up` sobe os serviços locais necessários; +- migrations sobem banco vazio; +- seed inicial cria entidade Handix/config base e superadmin usando segredo do ambiente; +- login funciona; +- permissões por aplicação/recurso/ação/escopo funcionam; +- nenhum tenant/reseller/customer data leak nos testes; +- Core usa o tema EDEN corretamente; +- Parceiros e Assinante têm aplicações separadas; +- fluxo comercial completo funciona; +- contrato é first-class; +- assinatura auditável funciona; +- estoque serializado funciona; +- billing e contas a receber são idempotentes; +- automações n8n podem consumir eventos; +- adapters Focus/SaperX existem com mocks/contract tests e credenciais por env; +- emissão fiscal não duplica referência; +- suporte possui SLA; +- módulo de ponto preserva AFD imutável; +- backups possuem runbook de restauração; +- logs não vazam segredos; +- `npm/pnpm audit`/scanner equivalente não apresenta vulnerabilidade crítica ignorada sem ADR/waiver; +- secret scan limpo; +- lint/typecheck/build/tests críticos verdes. + +--- + +# 22. PRIMEIRA EXECUÇÃO — O QUE FAZER AGORA + +Executar nesta ordem, sem começar a programar telas aleatórias: + +1. Confirmar raiz do projeto pelo conteúdo, não por suposição. +2. Ler `eden.md` integralmente. +3. Inspecionar `tema_do_Eden.zip` e os três assets de marca. +4. Criar `docs/project-inventory.md`. +5. Criar `docs/glossary.md` com vocabulário Handix/telecom/ERP. +6. Criar `docs/architecture.md` propondo bounded contexts e dependências. +7. Criar `docs/security/threat-model.md`. +8. Criar ADRs para: + - modular monolith; + - auth/sessions; + - permission model; + - encryption/secrets; + - contract first-class; + - stock ledger; + - billing vs finance vs fiscal; + - transactional outbox; + - SaperX adapter; + - Focus NFe adapter; + - 3 aplicações web com identidade compartilhada. +9. Criar `.claude/agents/`, `.claude/skills/` e hooks descritos aqui. +10. Montar backlog em `docs/implementation-plan.md` com fases, dependências e critérios de aceite. +11. Fazer revisão cruzada usando `eden-architect` + `eden-security` + `eden-database`. +12. Só então iniciar a Fase 1. + +Ao final de cada fase: +- rodar revisão; +- corrigir problemas; +- registrar estado em `docs/progress.md`; +- continuar para a próxima fase se o gate estiver verde. + +Não declarar o ERP “finalizado” enquanto módulos planejados estiverem apenas mockados. Distinguir claramente **implementado**, **integrado em sandbox/mock**, **aguardando credencial externa** e **não iniciado**. + +--- + +# 23. REGRAS DE NEGÓCIO DO LEGADO QUE NÃO PODEM SUMIR + +Ao portar o `eden.md`, garantir explicitamente que continuam representadas ou que existe ADR justificando mudança: + +- role weight e impossibilidade de um usuário inferior administrar papel superior; +- diferenciação próprio/outros e evolução para scopes; +- oferta bloqueada após avanço do cadastro/contrato, com correções superadmin auditadas; +- contract_period separado de fidelity_period; +- fidelidade reduzida só passa a ter efeito legal após aprovação; +- desconto especial precisa de aprovação; markup não necessariamente; +- rateio proporcional da condição especial; +- cálculo do benefício de fidelidade separado da economia comercial total; +- cadastro PF/PJ com regras distintas; +- sócio assinante substituindo representante; +- documentos de assinatura congelados; +- OTP de uso único; +- hash-chain de auditoria versionada; +- anexos internos vs assinatura; +- token público de alta entropia; +- Control iD e AFD imutável; +- ajustes de ponto aditivos; +- segregação de funções em aprovação/reabertura; +- backups em streaming para S3; +- versionamento de templates/documentos. + +--- + +# 24. PRINCÍPIO FINAL + +Construir o EDEN como sistema de missão crítica de uma operadora de telecomunicações. + +Toda decisão deve responder positivamente a estas perguntas: + +1. O dado fica correto mesmo se a request for repetida? +2. É possível auditar quem fez a mudança? +3. Um usuário consegue acessar apenas o que realmente pode? +4. Uma alteração futura de cadastro não muda retroativamente um contrato assinado? +5. Um valor financeiro pode ser reconciliado da origem até o recebimento? +6. Um item de cobrança pode ser rastreado até seu documento fiscal? +7. Um equipamento pode ser rastreado do warehouse até o cliente e de volta? +8. Uma integração indisponível pode falhar sem corromper o ERP? +9. Um webhook repetido pode ser processado sem duplicar efeito? +10. Um auditor consegue reconstruir o que aconteceu? + +Se a resposta for “não”, a implementação não está pronta. diff --git a/Eden_logo_horizontal.png b/Eden_logo_horizontal.png new file mode 100644 index 0000000..43f6bdd Binary files /dev/null and b/Eden_logo_horizontal.png differ diff --git a/Eden_logo_vertical.png b/Eden_logo_vertical.png new file mode 100644 index 0000000..7e717a7 Binary files /dev/null and b/Eden_logo_vertical.png differ diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..5e6d31b --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,29 @@ +{ + "name": "@eden/api", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node dist/main.js", + "dev": "tsx watch src/main.ts", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "echo 'no linter configured yet'", + "test": "echo 'no tests yet'" + }, + "dependencies": { + "@eden/database": "workspace:*", + "@nestjs/common": "^10.4.15", + "@nestjs/core": "^10.4.15", + "@nestjs/platform-express": "^10.4.15", + "@nestjs/terminus": "^10.2.3", + "express": "^4.21.2", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/node": "^22.10.5", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } +} diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts new file mode 100644 index 0000000..4d316d1 --- /dev/null +++ b/apps/api/src/app.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; +import { TerminusModule } from "@nestjs/terminus"; +import { HealthController } from "./health/health.controller"; + +@Module({ + imports: [TerminusModule], + controllers: [HealthController], +}) +export class AppModule {} diff --git a/apps/api/src/health/health.controller.ts b/apps/api/src/health/health.controller.ts new file mode 100644 index 0000000..b1c99fd --- /dev/null +++ b/apps/api/src/health/health.controller.ts @@ -0,0 +1,36 @@ +import { Controller, Get } from "@nestjs/common"; +import { + HealthCheck, + HealthCheckError, + HealthCheckService, + HealthIndicatorResult, +} from "@nestjs/terminus"; +import { query } from "@eden/database"; + +@Controller("health") +export class HealthController { + constructor(private readonly health: HealthCheckService) {} + + @Get("live") + live() { + // Liveness never depends on external services — only "is the process up". + return { status: "ok" }; + } + + @Get("ready") + @HealthCheck() + ready() { + return this.health.check([(): Promise => this.checkPostgres()]); + } + + private async checkPostgres(): Promise { + try { + await query("SELECT 1"); + return { postgres: { status: "up" } }; + } catch (err) { + throw new HealthCheckError("postgres check failed", { + postgres: { status: "down", message: (err as Error).message }, + }); + } + } +} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts new file mode 100644 index 0000000..117aac5 --- /dev/null +++ b/apps/api/src/main.ts @@ -0,0 +1,13 @@ +import "reflect-metadata"; +import { NestFactory } from "@nestjs/core"; +import { AppModule } from "./app.module"; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const port = Number(process.env.EDEN_API_PORT ?? 8080); + await app.listen(port, "0.0.0.0"); + // eslint-disable-next-line no-console + console.log(`[eden-api] listening on :${port}`); +} + +bootstrap(); diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..5184d8d --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "Node", + "target": "ES2022", + "outDir": "dist", + "rootDir": "src", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "strictPropertyInitialization": false + }, + "include": ["src"], + "exclude": ["dist", "node_modules"] +} diff --git a/apps/core-web/index.html b/apps/core-web/index.html new file mode 100644 index 0000000..d614588 --- /dev/null +++ b/apps/core-web/index.html @@ -0,0 +1,13 @@ + + + + + + + EDEN Core + + +
+ + + diff --git a/apps/core-web/package.json b/apps/core-web/package.json new file mode 100644 index 0000000..15c3096 --- /dev/null +++ b/apps/core-web/package.json @@ -0,0 +1,27 @@ +{ + "name": "@eden/core-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.json --noEmit && vite build", + "dev": "vite --port ${EDEN_CORE_PORT:-3001}", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "echo 'no linter configured yet'", + "test": "echo 'no tests yet'" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3", + "vite": "^6.0.7" + } +} diff --git a/apps/core-web/postcss.config.js b/apps/core-web/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/apps/core-web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/core-web/public/favicon.png b/apps/core-web/public/favicon.png new file mode 100644 index 0000000..8d9fde9 Binary files /dev/null and b/apps/core-web/public/favicon.png differ diff --git a/apps/core-web/src/App.tsx b/apps/core-web/src/App.tsx new file mode 100644 index 0000000..b0e6d97 --- /dev/null +++ b/apps/core-web/src/App.tsx @@ -0,0 +1,12 @@ +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/index.css b/apps/core-web/src/index.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/apps/core-web/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/core-web/src/main.tsx b/apps/core-web/src/main.tsx new file mode 100644 index 0000000..5d6b5de --- /dev/null +++ b/apps/core-web/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { App } from "./App"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/apps/core-web/tailwind.config.js b/apps/core-web/tailwind.config.js new file mode 100644 index 0000000..93aa364 --- /dev/null +++ b/apps/core-web/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{ts,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/apps/core-web/tsconfig.json b/apps/core-web/tsconfig.json new file mode 100644 index 0000000..686182a --- /dev/null +++ b/apps/core-web/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "noEmit": true, + "isolatedModules": true + }, + "include": ["src"] +} diff --git a/apps/core-web/vite.config.ts b/apps/core-web/vite.config.ts new file mode 100644 index 0000000..bb2ed17 --- /dev/null +++ b/apps/core-web/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + host: true, + port: Number(process.env.EDEN_CORE_PORT ?? 3001), + }, +}); diff --git a/apps/reseller-web/index.html b/apps/reseller-web/index.html new file mode 100644 index 0000000..7b56ac7 --- /dev/null +++ b/apps/reseller-web/index.html @@ -0,0 +1,13 @@ + + + + + + + EDEN Parceiros + + +
+ + + diff --git a/apps/reseller-web/package.json b/apps/reseller-web/package.json new file mode 100644 index 0000000..9f8acb2 --- /dev/null +++ b/apps/reseller-web/package.json @@ -0,0 +1,27 @@ +{ + "name": "@eden/reseller-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.json --noEmit && vite build", + "dev": "vite --port ${EDEN_PARCEIROS_PORT:-3002}", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "echo 'no linter configured yet'", + "test": "echo 'no tests yet'" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3", + "vite": "^6.0.7" + } +} diff --git a/apps/reseller-web/postcss.config.js b/apps/reseller-web/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/apps/reseller-web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/reseller-web/public/favicon.png b/apps/reseller-web/public/favicon.png new file mode 100644 index 0000000..8d9fde9 Binary files /dev/null and b/apps/reseller-web/public/favicon.png differ diff --git a/apps/reseller-web/src/App.tsx b/apps/reseller-web/src/App.tsx new file mode 100644 index 0000000..f9612e4 --- /dev/null +++ b/apps/reseller-web/src/App.tsx @@ -0,0 +1,12 @@ +export function App() { + return ( +
+
+

EDEN Parceiros

+

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

+
+
+ ); +} diff --git a/apps/reseller-web/src/index.css b/apps/reseller-web/src/index.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/apps/reseller-web/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/reseller-web/src/main.tsx b/apps/reseller-web/src/main.tsx new file mode 100644 index 0000000..5d6b5de --- /dev/null +++ b/apps/reseller-web/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { App } from "./App"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/apps/reseller-web/tailwind.config.js b/apps/reseller-web/tailwind.config.js new file mode 100644 index 0000000..93aa364 --- /dev/null +++ b/apps/reseller-web/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{ts,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/apps/reseller-web/tsconfig.json b/apps/reseller-web/tsconfig.json new file mode 100644 index 0000000..686182a --- /dev/null +++ b/apps/reseller-web/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "noEmit": true, + "isolatedModules": true + }, + "include": ["src"] +} diff --git a/apps/reseller-web/vite.config.ts b/apps/reseller-web/vite.config.ts new file mode 100644 index 0000000..dc4fb2d --- /dev/null +++ b/apps/reseller-web/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + host: true, + port: Number(process.env.EDEN_PARCEIROS_PORT ?? 3002), + }, +}); diff --git a/apps/subscriber-web/index.html b/apps/subscriber-web/index.html new file mode 100644 index 0000000..3c527d2 --- /dev/null +++ b/apps/subscriber-web/index.html @@ -0,0 +1,13 @@ + + + + + + + EDEN Assinante + + +
+ + + diff --git a/apps/subscriber-web/package.json b/apps/subscriber-web/package.json new file mode 100644 index 0000000..3d9fbc4 --- /dev/null +++ b/apps/subscriber-web/package.json @@ -0,0 +1,27 @@ +{ + "name": "@eden/subscriber-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.json --noEmit && vite build", + "dev": "vite --port ${EDEN_ASSINANTE_PORT:-3003}", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "echo 'no linter configured yet'", + "test": "echo 'no tests yet'" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3", + "vite": "^6.0.7" + } +} diff --git a/apps/subscriber-web/postcss.config.js b/apps/subscriber-web/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/apps/subscriber-web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/subscriber-web/public/favicon.png b/apps/subscriber-web/public/favicon.png new file mode 100644 index 0000000..8d9fde9 Binary files /dev/null and b/apps/subscriber-web/public/favicon.png differ diff --git a/apps/subscriber-web/src/App.tsx b/apps/subscriber-web/src/App.tsx new file mode 100644 index 0000000..d4e1218 --- /dev/null +++ b/apps/subscriber-web/src/App.tsx @@ -0,0 +1,12 @@ +export function App() { + return ( +
+
+

EDEN Assinante

+

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

+
+
+ ); +} diff --git a/apps/subscriber-web/src/index.css b/apps/subscriber-web/src/index.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/apps/subscriber-web/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/subscriber-web/src/main.tsx b/apps/subscriber-web/src/main.tsx new file mode 100644 index 0000000..5d6b5de --- /dev/null +++ b/apps/subscriber-web/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { App } from "./App"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/apps/subscriber-web/tailwind.config.js b/apps/subscriber-web/tailwind.config.js new file mode 100644 index 0000000..93aa364 --- /dev/null +++ b/apps/subscriber-web/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{ts,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/apps/subscriber-web/tsconfig.json b/apps/subscriber-web/tsconfig.json new file mode 100644 index 0000000..686182a --- /dev/null +++ b/apps/subscriber-web/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "noEmit": true, + "isolatedModules": true + }, + "include": ["src"] +} diff --git a/apps/subscriber-web/vite.config.ts b/apps/subscriber-web/vite.config.ts new file mode 100644 index 0000000..d74a9d7 --- /dev/null +++ b/apps/subscriber-web/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + host: true, + port: Number(process.env.EDEN_ASSINANTE_PORT ?? 3003), + }, +}); diff --git a/apps/worker/package.json b/apps/worker/package.json new file mode 100644 index 0000000..1a8adb0 --- /dev/null +++ b/apps/worker/package.json @@ -0,0 +1,21 @@ +{ + "name": "@eden/worker", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node dist/main.js", + "dev": "tsx watch src/main.ts", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "echo 'no linter configured yet'", + "test": "echo 'no tests yet'" + }, + "dependencies": { + "@eden/database": "workspace:*" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } +} diff --git a/apps/worker/src/main.ts b/apps/worker/src/main.ts new file mode 100644 index 0000000..3436abc --- /dev/null +++ b/apps/worker/src/main.ts @@ -0,0 +1,25 @@ +import { query } from "@eden/database"; + +/** + * No real async jobs exist yet (BullMQ/Redis are only added when the first + * job with a genuine need shows up — see docs/adr/0002-container-topology.md + * and Master Prompt §4.1). This entrypoint exists so the compose topology + * matches the documented architecture from day one and proves the worker + * container can reach Postgres. + */ +async function main() { + await query("SELECT 1"); + // eslint-disable-next-line no-console + console.log("[eden-worker] up, database reachable, no jobs configured yet"); + + // Keep the container alive; replace with a real job queue consumer + // (BullMQ) once Fase 5/6 introduces async jobs (billing runs, PDF + // generation, fiscal retries, etc.). + setInterval(() => {}, 1 << 30); +} + +main().catch((err) => { + // eslint-disable-next-line no-console + console.error("[eden-worker] fatal:", err); + process.exit(1); +}); diff --git a/apps/worker/tsconfig.json b/apps/worker/tsconfig.json new file mode 100644 index 0000000..bc5903e --- /dev/null +++ b/apps/worker/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "Node", + "target": "ES2022", + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["dist", "node_modules"] +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..06a7fba --- /dev/null +++ b/compose.yaml @@ -0,0 +1,155 @@ +# EDEN — topologia de containers (docs/adr/0002-container-topology.md) +# +# Postgres isolado, credencial de banco só no serviço eden-api. Cada uma das +# 3 aplicações web em container e porta próprios, falando só com a API via +# HTTP — nunca direto com o banco. Rodar a partir da raiz do repositório: +# +# docker compose --env-file .env up -d +# +name: eden + +services: + eden-postgres: + image: postgres:18-alpine + container_name: eden-postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${EDEN_DATABASE_NAME:-eden} + POSTGRES_USER: ${EDEN_DATABASE_USER:-eden} + POSTGRES_PASSWORD: ${EDEN_DATABASE_PASSWORD:?defina EDEN_DATABASE_PASSWORD no .env} + volumes: + # Postgres 18's official image switched to a pg_ctlcluster-style layout: + # mount the parent dir, not .../data — see + # https://github.com/docker-library/postgres/pull/1259 + - eden_pgdata:/var/lib/postgresql + # Mapeado ao host só para acesso de ferramenta local em desenvolvimento, + # numa porta alta não-padrão — nunca 5432:5432, nunca exposto em produção + # (ver docs/adr/0002-container-topology.md). Remover este bloco em prod. + ports: + - "${EDEN_POSTGRES_DEV_PORT:-55432}:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${EDEN_DATABASE_USER:-eden} -d ${EDEN_DATABASE_NAME:-eden}"] + interval: 5s + timeout: 5s + retries: 10 + networks: [eden_net] + + eden-api: + build: + context: . + dockerfile: infra/docker/Dockerfile.node + args: + APP_NAME: api + container_name: eden-api + restart: unless-stopped + environment: + DATABASE_URL: postgres://${EDEN_DATABASE_USER:-eden}:${EDEN_DATABASE_PASSWORD}@eden-postgres:5432/${EDEN_DATABASE_NAME:-eden} + EDEN_API_PORT: ${EDEN_API_PORT:-8080} + JWT_SIGNING_SECRET: ${JWT_SIGNING_SECRET} + SIGNATURE_OTP_SECRET: ${SIGNATURE_OTP_SECRET} + EDEN_FIELD_ENCRYPTION_KEY: ${EDEN_FIELD_ENCRYPTION_KEY} + SMTP_HOST: ${SMTP_HOST} + SMTP_PORT: ${SMTP_PORT} + SMTP_USER: ${SMTP_USER} + SMTP_PASS: ${SMTP_PASS} + SMTP_FROM: ${SMTP_FROM} + S3_ENDPOINT: ${S3_ENDPOINT} + S3_REGION: ${S3_REGION} + S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE} + S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID} + S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY} + S3_BUCKET: ${S3_BUCKET} + PUBLIC_BASE_URL: ${PUBLIC_BASE_URL} + ports: + - "${EDEN_API_PORT:-8080}:${EDEN_API_PORT:-8080}" + depends_on: + eden-postgres: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "node -e \"fetch('http://localhost:'+ (process.env.EDEN_API_PORT||8080) +'/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] + interval: 10s + timeout: 5s + retries: 10 + networks: [eden_net] + + eden-worker: + build: + context: . + dockerfile: infra/docker/Dockerfile.node + args: + APP_NAME: worker + container_name: eden-worker + restart: unless-stopped + environment: + DATABASE_URL: postgres://${EDEN_DATABASE_USER:-eden}:${EDEN_DATABASE_PASSWORD}@eden-postgres:5432/${EDEN_DATABASE_NAME:-eden} + depends_on: + eden-postgres: + condition: service_healthy + networks: [eden_net] + + eden-core: + build: + context: . + dockerfile: infra/docker/Dockerfile.web + args: + APP_NAME: core-web + container_name: eden-core + restart: unless-stopped + ports: + - "${EDEN_CORE_PORT:-3001}:80" + depends_on: + eden-api: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1/healthz"] + interval: 10s + timeout: 5s + retries: 5 + networks: [eden_net] + + eden-parceiros: + build: + context: . + dockerfile: infra/docker/Dockerfile.web + args: + APP_NAME: reseller-web + container_name: eden-parceiros + restart: unless-stopped + ports: + - "${EDEN_PARCEIROS_PORT:-3002}:80" + depends_on: + eden-api: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1/healthz"] + interval: 10s + timeout: 5s + retries: 5 + networks: [eden_net] + + eden-assinante: + build: + context: . + dockerfile: infra/docker/Dockerfile.web + args: + APP_NAME: subscriber-web + container_name: eden-assinante + restart: unless-stopped + ports: + - "${EDEN_ASSINANTE_PORT:-3003}:80" + depends_on: + eden-api: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1/healthz"] + interval: 10s + timeout: 5s + retries: 5 + networks: [eden_net] + +networks: + eden_net: + driver: bridge + +volumes: + eden_pgdata: diff --git a/docs/adr/0001-modular-monolith.md b/docs/adr/0001-modular-monolith.md new file mode 100644 index 0000000..f880864 --- /dev/null +++ b/docs/adr/0001-modular-monolith.md @@ -0,0 +1,18 @@ +# ADR-0001: Modular Monolith como estilo arquitetural do backend + +## Status +Aceito + +## Contexto +O EDEN precisa cobrir 14+ domínios de negócio (CRM, contratos, estoque, financeiro, billing, fiscal, telecom, suporte, RH, etc.) servindo 3 aplicações web distintas. O legado OrçaFácil é um monolito simples (Express + Postgres) sem separação de módulo forte. Microserviços trariam isolamento de falha e escalabilidade independente, mas custam operacionalmente caro (deploy, observabilidade, transação distribuída, N bancos ou schemas) num estágio em que o time e o volume ainda não justificam esse custo — e o Master Prompt (§4.2) explicitamente pede para evitar microserviços prematuros. + +## Decisão +Construir `apps/api` como **modular monolith** em NestJS: um processo, módulos por bounded context (ver `docs/architecture.md` §2) com fronteiras de import explícitas (lint/arquitetura impede um módulo importar internals de outro), comunicação entre módulos via serviço de aplicação exposto ou evento de domínio (outbox) — nunca acesso direto a tabela de outro módulo. + +Candidatos naturais a extração futura para serviço próprio, se/quando o volume justificar: **Billing** (processamento em lote, alta carga periódica) e **Fiscal** (chamadas externas longas/assíncronas). Nenhuma extração é feita nesta fase. + +## Consequências +- Positivo: deploy único mais simples, transação ACID cross-módulo quando necessário (ex.: fechar oferta + criar cadastro), menor custo operacional inicial. +- Positivo: fronteiras de módulo desde o dia 1 tornam uma futura extração mecânica, não uma reescrita. +- Negativo: falha de um módulo (ex.: bug de memória em geração de PDF) pode afetar o processo inteiro — mitigado por `apps/worker` separado para jobs pesados/assíncronos (PDF, billing run, fiscal) desde o início. +- Negativo: todos os módulos compartilham o mesmo pool de conexão de banco — dimensionar pool e monitorar por módulo via métricas/labels. diff --git a/docs/adr/0002-container-topology.md b/docs/adr/0002-container-topology.md new file mode 100644 index 0000000..681cfea --- /dev/null +++ b/docs/adr/0002-container-topology.md @@ -0,0 +1,35 @@ +# ADR-0002: Topologia de containers — Postgres isolado + 1 container por aplicação web + +## Status +Aceito (decisão explícita do operador) + +## Contexto +O EDEN tem 3 aplicações web (`core-web`, `reseller-web`, `subscriber-web`) mais a API e um worker assíncrono, além do Postgres 18 exigido pelo Master Prompt (§4.3). O operador determinou explicitamente: o banco de dados deve subir em container separado; `eden-core`, `eden-parceiro` e `eden-assinantes` devem ser containers distintos, cada um em porta própria. + +## Decisão +Topologia de containers via Docker Compose (um `compose.yaml` por ambiente — dev/staging/prod usando overrides), com os seguintes serviços: + +| Serviço | Container | Porta exposta ao host | Fala com Postgres? | +|---|---|---|---| +| `eden-postgres` | Postgres 18 | Não (produção) / porta alta não-padrão (dev) | — | +| `eden-redis` | Redis (quando houver job real) | Não | — | +| `eden-api` | API principal (NestJS) | `EDEN_API_PORT` (env, default 8080) | Sim — único serviço com credencial de banco | +| `eden-worker` | Jobs assíncronos (BullMQ) | Não | Sim (mesma credencial de app, escopo próprio se possível) | +| `eden-core` | Frontend ERP interno | `EDEN_CORE_PORT` (env, default 3001) | Não — só HTTP para `eden-api` | +| `eden-parceiros` | Frontend portal revenda | `EDEN_PARCEIROS_PORT` (env, default 3002) | Não — só HTTP para `eden-api` | +| `eden-assinante` | Frontend portal assinante | `EDEN_ASSINANTE_PORT` (env, default 3003) | Não — só HTTP para `eden-api` | + +Regras adicionais: +1. Rede docker interna dedicada (`eden_net`); só a API tem credencial de Postgres — as 3 apps web nunca recebem `DATABASE_URL`. +2. Portas configuráveis via `.env`, nunca hardcoded no `compose.yaml`, para permitir múltiplas instâncias (dev + staging) na mesma máquina. +3. `healthcheck` obrigatório em cada serviço; `depends_on` usa `condition: service_healthy`, não apenas ordem de start. +4. Volume nomeado `eden_pgdata` para dado do Postgres; nunca bind mount direto de produção sem estratégia de backup validada. +5. Build multi-stage; imagens de produção sem devDependencies/toolchain de build. +6. Backup lógico (Master Prompt §6.15) roda a partir do container do worker (ou de um job dedicado), nunca do host diretamente — mantém a regra de "streaming, nunca materializar em disco local" também em containers. + +## Consequências +- Positivo: isolamento de falha por aplicação — um crash no frontend do assinante não derruba o core nem a API. +- Positivo: cada app pode escalar/atualizar independentemente (ex.: deploy do `eden-assinante` sem tocar no `eden-core`). +- Positivo: superfície de acesso ao banco reduzida a um único serviço, simplificando auditoria de acesso a dado. +- Negativo: mais serviços para orquestrar/monitorar do que um único container "tudo junto" — mitigado por healthchecks e observabilidade centralizada (Master Prompt §16). +- A definir na Fase 1: se `eden-core`/`eden-parceiros`/`eden-assinante` são servidos como SPA estática (nginx) ou com SSR — não muda a topologia de containers, só a imagem base de cada um. diff --git a/docs/adr/0003-auth-sessions.md b/docs/adr/0003-auth-sessions.md new file mode 100644 index 0000000..8adbe19 --- /dev/null +++ b/docs/adr/0003-auth-sessions.md @@ -0,0 +1,21 @@ +# ADR-0003: Autenticação e gestão de sessão + +## Status +Aceito + +## Contexto +O legado usa JWT HS256 com payload mínimo (`sub`, `ver`), expiração fixa de 30 dias, sem refresh token, invalidação via `token_version` incremental (derruba todas as sessões de uma vez, nunca uma só). O Master Prompt (§5.3) pede explicitamente uma evolução: access token curto + refresh token rotativo (ou sessão server-side segura), refresh tokens armazenados em hash, histórico de sessões/dispositivos, "encerrar todas as sessões", MFA/TOTP preparado. + +## Decisão +- **Access token**: JWT de vida curta (15 min), payload mínimo (`sub`, sessão id), assinado (algoritmo a confirmar em ADR de criptografia geral — HS256 mantém paridade com o legado, RS256 fica em aberto se houver necessidade de verificação por serviço externo). +- **Refresh token**: opaco, alta entropia (≥256 bits), **rotativo a cada uso** (um novo é emitido e o antigo invalidado), armazenado só como hash no banco (nunca em claro) — mesma filosofia do token de link público do legado. +- **Tabela `sessions`** (não só um contador `token_version`): permite listar sessões/dispositivos ativos e revogar individualmente **ou** todas de uma vez (superset da capacidade do legado, que só permitia "todas"). +- **Vínculo por aplicação**: uma sessão registra explicitamente para qual aplicação (`core`/`reseller`/`subscriber`) foi emitida — nunca inferir pelo papel do usuário (Master Prompt §5.1). +- **MFA/TOTP**: schema preparado desde a Fase 1 (tabela de fator, secret cifrado), ativação opcional para todos, indicada como obrigatória por política para `super_admin`/`admin` assim que a UI existir. +- Senha: Argon2id (ADR-0005 detalha parâmetros/versionamento), não bcrypt. + +## Consequências +- Positivo: revogação granular por sessão/dispositivo, alinhado ao pedido explícito do Master Prompt. +- Positivo: access token de vida curta reduz janela de uso de token vazado sem exigir consulta ao banco a cada request (mesma vantagem de performance que o legado tinha com JWT, mas com menor exposição). +- Negativo: mais complexidade que o modelo legado (rotação de refresh token exige lógica de "replay detection" — se um refresh token já usado for reapresentado, é sinal de token roubado; a sessão inteira deve ser revogada nesse caso). +- Migração: nenhuma — é um sistema novo, não há sessões legadas para migrar. diff --git a/docs/adr/0004-permission-model.md b/docs/adr/0004-permission-model.md new file mode 100644 index 0000000..a854368 --- /dev/null +++ b/docs/adr/0004-permission-model.md @@ -0,0 +1,20 @@ +# ADR-0004: Modelo de permissões — RBAC + peso hierárquico + escopo de dados + +## Status +Aceito + +## Contexto +O legado tem um sistema de feature keys com 2 níveis (`view`/`edit`) por papel, mais peso hierárquico (`role weight`) que impede um ator de administrar papel/usuário de peso maior, mais um caso especial hardcoded de "próprio vs. todos" (`ofertas`/`ofertas_others`) repetido manualmente por feature quando necessário. O Master Prompt (§5.2) pede evolução: cada permissão deve expressar recurso/ação (`view`/`create`/`edit`/`delete`/`approve`/`export`/`manage`) e escopo (`own`/`team`/`reseller`/`legal_entity`/`all`) — generalizando o padrão "próprio vs. todos" em vez de repeti-lo campo a campo. + +## Decisão +- Preservar o conceito de **role weight** exatamente como no legado (nunca um ator administra papel/usuário de peso maior; `super_admin` sempre no teto, nunca editável). +- Substituir o mapa plano `{feature_key: 'view'|'edit'}` por uma matriz `role_permissions(role, resource, action, scope)` — cada linha concede uma ação sobre um recurso com um escopo. Isso generaliza o caso `ofertas`/`ofertas_others` sem precisar de uma segunda feature key por recurso: o escopo `own` já cobre "só minhas ofertas", `all`/`reseller` cobre "todas"/"da revenda". +- Papel novo nasce sem nenhuma linha (zero acesso) — replica a regra do legado de nunca herdar permissão por padrão. +- `super_admin` continua **hardcoded fora da tabela** (nunca consultado via `role_permissions`), com bypass total mas sempre auditado. +- UI de gerenciamento de papel permite configurar, por menu/módulo, ação e alcance — conforme pedido explícito do Master Prompt §5.2. + +## Consequências +- Positivo: elimina a necessidade de duplicar feature key para cada distinção "próprio vs. todos" que aparecer no futuro (o legado já tinha um caso day-1, `ofertas_others` — outros vão aparecer em Contratos, Chamados, etc.). +- Positivo: mapeamento direto do padrão de autorização do legado (`requireFeatureOrSuperAdmin`) para um middleware `requirePermission(resource, action, scopeResolver)` equivalente. +- Negativo: migração de mental model para quem vai configurar papéis (matriz maior que o mapa binário do legado) — mitigado com UI que agrupa por módulo/menu como já era. +- Todo endpoint continua resolvendo escopo no servidor a partir da sessão (nunca aceitar `reseller_id`/`customer_id` do cliente) — reforça Master Prompt §12. diff --git a/docs/adr/0005-encryption-secrets.md b/docs/adr/0005-encryption-secrets.md new file mode 100644 index 0000000..a1b9370 --- /dev/null +++ b/docs/adr/0005-encryption-secrets.md @@ -0,0 +1,20 @@ +# ADR-0005: Criptografia e gestão de segredos + +## Status +Aceito + +## Contexto +O legado usa bcrypt custo 10 para senha, JWT HS256 sem rotação, AES-256-GCM para o único segredo reversível identificado (senha de equipamento Control iD), HMAC-SHA256 para OTP. O Master Prompt (§5.4) pede Argon2id para senha e AES-256-GCM (ou equivalente autenticado) com versionamento de chave para todo segredo operacional reversível (API keys de IA, tokens SaperX, credenciais Control iD, secrets de gateway), com chave raiz nunca no banco. + +## Decisão +- **Hash unidirecional** (senha, refresh token, tokens públicos sem necessidade de recuperação): Argon2id, parâmetros iniciais conservadores e revisáveis (memory cost, iterations, parallelism documentados em `packages/auth`), com **versionamento de parâmetro** por hash armazenado (permite aumentar custo no futuro sem invalidar hashes antigos — eles são re-hasheados no próximo login bem-sucedido). +- **Criptografia reversível de campo**: AES-256-GCM, IV de 96 bits aleatório por operação, tag de autenticação verificada na decriptação (falha se adulterado) — mesmo formato de armazenamento do legado (`iv:tag:ciphertext`, base64), reaproveitando padrão já validado em produção pela Handix. +- **Versionamento de chave**: todo campo cifrado grava também qual versão de chave raiz foi usada (`key_version`), permitindo rotação de chave raiz sem re-cifrar tudo de uma vez (re-cifra sob demanda/job de rotação). +- **Chave raiz**: nunca no banco nem na imagem do container — variável de ambiente/secret store, injetada no container em runtime. Rotação de chave raiz é operação registrada e auditada (runbook próprio). +- **OTP**: manter HMAC-SHA256 com segredo de servidor (nunca hash simples — espaço pequeno de 10⁶ valores exige resistência a rainbow table via segredo), TTL curto, uso único, máximo de tentativas com bloqueio — replicar fielmente o padrão do legado (validado em produção). +- **Cartão de crédito**: nunca armazenar CVV; tokenização via gateway/PSP; nenhum cofre de cartão caseiro (Master Prompt §5.4). + +## Consequências +- Positivo: Argon2id é hoje o padrão recomendado (OWASP) sobre bcrypt, resistente a ataque por GPU/ASIC. +- Positivo: versionamento de chave/parâmetro evita "big bang" de rotação — rotação é incremental e auditável. +- Negativo: Argon2id é mais pesado computacionalmente que bcrypt custo 10 — dimensionar parâmetros considerando throughput de login esperado (não copiar cegamente defaults de biblioteca sem medir). diff --git a/docs/adr/0006-contract-first-class.md b/docs/adr/0006-contract-first-class.md new file mode 100644 index 0000000..62c15b0 --- /dev/null +++ b/docs/adr/0006-contract-first-class.md @@ -0,0 +1,23 @@ +# ADR-0006: Contrato como agregado de primeira classe + +## Status +Aceito + +## Contexto +No legado, "contrato" não é uma tabela — é a junção em tempo de consulta de `client_registrations` (ativos) com a `quotes` que os originou. Isso funciona para o caso simples de uma oferta = um contrato, mas não suporta amendments, renovações, múltiplas versões, ou itens/partes de contrato como entidades consultáveis. O Master Prompt (§6.6) exige transformar contrato em agregado de primeira classe. + +## Decisão +Criar as tabelas: `contracts`, `contract_items`, `contract_parties`, `contract_versions`, `contract_documents`, `contract_amendments`, `contract_renewals`, `contract_status_history`, `contract_assets`, `contract_services`, `contract_billing_rules`. + +Estados: `draft → pending_signature → active → suspended/cancelled/terminated/expired → renewed`. + +Regras preservadas do legado (nunca perder): +- **`contract_period` (faixa de preço) permanece distinto de `fidelity_period` (permanência efetiva)** — vigência/vencimento/multa sempre calculados pela fidelidade **resolvida** (`fidelity_period` se aprovado, senão `contract_period`), nunca pela faixa de preço bruta. +- Contrato assinado grava **snapshot** dos valores jurídicos/comerciais relevantes no momento da assinatura (via `contract_versions`) — uma alteração futura de produto/preço nunca muda retroativamente um contrato já assinado (invariante nº4 do Master Prompt §24). +- Fluxo de fechamento de oferta → geração de contrato preserva a "trava" equivalente (oferta travada do legado vira, no EDEN, transição de estado do contrato que também impede edição desconforme, exceto por papel com permissão de correção auditada equivalente ao `super_admin` do legado). + +## Consequências +- Positivo: permite amendments/renovações/múltiplas partes sem gambiarra de "reabrir a oferta". +- Positivo: relatórios de vencimento/MRR (equivalente ao `GET /contracts/report` do legado) passam a consultar uma tabela real em vez de uma junção calculada, com melhor performance de índice. +- Negativo: mais complexidade de schema/migração do que o legado; mitigado por ser green-field (sem dado legado a migrar automaticamente — Handix decide se há import histórico do OrçaFácil, fora do escopo desta ADR). +- Depende de: Customer 360 (Fase 2) e Commercial/Ofertas (Fase 2) já existirem — ver `docs/architecture.md` §2. diff --git a/docs/adr/0007-stock-ledger.md b/docs/adr/0007-stock-ledger.md new file mode 100644 index 0000000..11ea0b9 --- /dev/null +++ b/docs/adr/0007-stock-ledger.md @@ -0,0 +1,20 @@ +# ADR-0007: Estoque como ledger de movimentos, nunca saldo editável + +## Status +Aceito + +## Contexto +O legado não tem módulo de estoque real — produtos têm preço e flags, mas nenhuma tabela de movimento/saldo. O Master Prompt (§6.8, §11.5) exige estoque com ledger de movimentos, ativos serializados (serial/patrimônio/MAC) e rastreabilidade completa warehouse↔cliente↔warehouse. + +## Decisão +Modelar: `warehouses`, `warehouse_locations`, `stock_items`, `stock_lots` (quando necessário), `stock_movements` (ledger append-only), `stock_reservations`, `serialized_assets`, `asset_assignments`, `inventory_counts`, `transfers`, `receipts`, `issues`, `returns`, `rma`, `asset_maintenance`. + +Saldo de estoque é **sempre calculado** a partir de `stock_movements` (soma de entradas/saídas), nunca uma coluna editável diretamente. Cada `serialized_asset` tem um único estado ativo por vez (nunca dois ativos "ativos" com o mesmo serial/MAC/patrimônio simultaneamente — constraint de banco, não só validação de aplicação). + +Fluxo de instalação (fechamento de oferta/contrato com equipamento): reserva → seleção de unidade serializada → vínculo a contrato/cliente → movimento para instalado/comodato → rastreabilidade até devolução/baixa (Master Prompt §6.8). + +## Consequências +- Positivo: auditoria completa de estoque (invariante nº7 do Master Prompt §24 — rastrear do warehouse até o cliente e de volta). +- Positivo: elimina classe de bug "saldo dessincronizado" comum em campo editável. +- Negativo: toda operação de estoque precisa passar por um serviço de domínio que grava o movimento — nenhum caminho de escrita direta em "saldo". +- Depende de: Organization (warehouses por unidade legal) já existir. diff --git a/docs/adr/0008-billing-finance-fiscal-separation.md b/docs/adr/0008-billing-finance-fiscal-separation.md new file mode 100644 index 0000000..3ca8a10 --- /dev/null +++ b/docs/adr/0008-billing-finance-fiscal-separation.md @@ -0,0 +1,22 @@ +# ADR-0008: Separação entre Billing, Financeiro (AR/AP) e Fiscal + +## Status +Aceito + +## Contexto +O legado não tem billing recorrente nem contas a receber/pagar como módulo — só a oferta/contrato com valores. O Master Prompt (§6.9, §6.10, §6.11) exige três motores distintos e explicitamente adverte: "nunca confundir faturamento, documento fiscal e recebimento: são eventos relacionados, porém distintos." + +## Decisão +Três motores com dados e responsabilidades separadas: + +1. **Billing** (`billing_accounts`, `billing_cycles`, `subscriptions/services`, `charge_components`, `usage_charges`, `invoices`, `invoice_items`, `invoice_adjustments`, `billing_runs`): responsável por **calcular o que é devido** (mensalidade, pró-rata, implantação, consumo) e gerar a fatura interna (`invoice`). Fechamento de billing run é idempotente e reexecutável com segurança **antes** da consolidação; depois de consolidada, uma invoice não é editada silenciosamente — usa ajuste/nota de crédito/débito ou refaturamento controlado. +2. **Finance/AR-AP**: responsável por **cobrar e receber/pagar** — títulos, parcelas, boletos (provider abstraction), dunning, conciliação, contas a pagar. Um título de AR nasce a partir de uma invoice de billing, mas é uma entidade própria (permite negociação, baixa parcial, estorno, sem tocar no billing). +3. **Fiscal**: responsável por **emitir o documento fiscal** correspondente a um item de billing já classificado (fiscal profile) — nunca hardcoded por tela. Documento fiscal é consequência do item de faturamento, não do clique de um usuário numa tela específica. + +Cada camada guarda `external_id`/referência para a anterior, nunca duplica o cálculo. + +## Consequências +- Positivo: permite reconciliar valor da origem (billing) até o recebimento (finance) e até o documento fiscal (fiscal) — invariante nº5 e nº6 do Master Prompt §24. +- Positivo: mudança de gateway de cobrança (Finance) não afeta o cálculo de billing; mudança de regra tributária (Fiscal) não afeta o cálculo comercial. +- Negativo: mais tabelas e mais pontos de integração entre módulos do que uma solução monolítica "fatura única" — mitigado por eventos de domínio (`invoice.created`, `payment.received`, `fiscal_document.authorized`) documentados no Master Prompt §9.2. +- Depende de: Contracts (Fase 3) e Inventory/consumo (para usage_charges) parcialmente. diff --git a/docs/adr/0009-transactional-outbox.md b/docs/adr/0009-transactional-outbox.md new file mode 100644 index 0000000..a52e19e --- /dev/null +++ b/docs/adr/0009-transactional-outbox.md @@ -0,0 +1,18 @@ +# ADR-0009: Outbox transacional para eventos de integração + +## Status +Aceito + +## Contexto +O EDEN precisa publicar eventos de domínio (`lead.created`, `contract.signed`, `invoice.overdue`, etc. — catálogo no Master Prompt §9.2) para consumo por n8n/webhooks/outros módulos, sem perder evento em caso de falha entre o commit da transação de negócio e a publicação. O legado não tem esse problema porque não publica eventos externos. + +## Decisão +Toda operação que precise emitir um evento relevante grava a linha do evento na mesma transação SQL da mudança de negócio, numa tabela `outbox_events` (payload normalizado, tipo do evento, status `pending/delivered/failed`, tentativas, correlation id). Um processo separado (`apps/worker`) lê a outbox e entrega (webhook assinado HMAC, ou fila interna para o próprio módulo consumidor), marcando como entregue só após confirmação — com retry/backoff e dead-letter após esgotar tentativas. + +Consumidores (internos ou externos via n8n) devem ser idempotentes por `event_id` — reentrega nunca duplica efeito. + +## Consequências +- Positivo: garante "at-least-once" delivery sem depender de o processo de aplicação sobreviver após o commit (invariante nº9 do Master Prompt §24 — webhook repetido não duplica efeito, aplicado também no sentido saída). +- Positivo: replay manual de evento é trivial (reprocessar linha da outbox). +- Negativo: exige limpeza/arquivamento periódico da tabela de outbox (retenção configurável) para não crescer indefinidamente. +- Negativo: consumidores precisam de lógica de idempotência — custo replicado em cada integração, mitigado por uma camada compartilhada em `packages/integrations`. diff --git a/docs/adr/0010-saperx-adapter.md b/docs/adr/0010-saperx-adapter.md new file mode 100644 index 0000000..34cfcca --- /dev/null +++ b/docs/adr/0010-saperx-adapter.md @@ -0,0 +1,19 @@ +# ADR-0010: Adapter SaperX isolado + +## Status +Aceito + +## Contexto +SaperX é o sistema de telecom/billing externo com o qual o EDEN precisa se integrar (circuitos, DIDs, consumo/CDR, faturas). O legado (`eden.md`) não documenta integração automática com sistemas de telecom externos (a única "integração IXC" citada é manual, campo de texto livre) — SaperX é uma integração nova para o EDEN, sem precedente de código a herdar, só requisitos do Master Prompt §6.12. + +## Decisão +Módulo `integrations/saperx` como adapter/port isolado (padrão do Master Prompt §13): configuração por ambiente, token criptografado (AES-256-GCM, ver ADR-0005), suporte a IP allowlist se a API exigir, timeout + retry seguro + rate limiting local + circuit breaker, correlation id em toda chamada, logs nunca com token, healthcheck de integração próprio (não derruba o `/health/ready` geral do EDEN por indisponibilidade do SaperX). + +Nunca acoplar entidades internas ao payload bruto do SaperX — todo dado externo vira DTO normalizado antes de entrar no domínio, com `external_id` + snapshot de origem quando necessário para auditoria/reconciliação. + +Se um endpoint necessário não estiver disponível/documentado no momento da implementação: implementar interface + mock + TODO explícito, nunca inventar resposta. + +## Consequências +- Positivo: se a API do SaperX mudar ou a Handix trocar de provider de telecom no futuro, só o adapter muda — domínio (contratos, billing, customer 360) permanece intacto. +- Positivo: consistente com o mesmo padrão usado para Focus NFe e Control iD — um único modelo mental de integração externa no projeto inteiro. +- Risco conhecido: sem acesso à documentação/sandbox do SaperX nesta fase — endpoints exatos e formato de payload serão confirmados na Fase 6; até lá, contrato do adapter fica desenhado a partir dos objetivos de negócio (Master Prompt §6.12), não de um payload real. diff --git a/docs/adr/0011-focus-nfe-adapter.md b/docs/adr/0011-focus-nfe-adapter.md new file mode 100644 index 0000000..4cd0836 --- /dev/null +++ b/docs/adr/0011-focus-nfe-adapter.md @@ -0,0 +1,18 @@ +# ADR-0011: Adapter Focus NFe isolado + +## Status +Aceito + +## Contexto +O EDEN precisa emitir NFCom, NFS-e e recibo de locação via Focus NFe (Master Prompt §6.11). O legado tem toda a camada de **catálogos** fiscais (NCM, CFOP, municípios, cClass NFCom, cTribNac/cTribMun, IBS/CBS) e **configuração fiscal de produto** (`product_fiscal_profiles`) já madura e testada em produção, mas **não tem motor de emissão** (`fiscal_rules` existe só como schema, sem lógica) — é o ponto de partida a herdar, não a integração de emissão em si, que é nova. + +## Decisão +- Portar fielmente o modelo de catálogos fiscais e `product_fiscal_profiles` do legado (índice único parcial "um profile ativo por componente de faturamento", proteção contra inativação em massa no upsert, importação automática vs. manual com dry-run obrigatório) — é infraestrutura validada, não reinventar. +- Construir `integrations/focus-nfe` como adapter/port novo: referência única/idempotente por documento, emissão, consulta, cancelamento, webhooks autenticados, persistência do request normalizado (nunca com segredo), retries com backoff, dead-letter/retry manual. +- Emissão fiscal é sempre consequência de um item de billing já classificado por fiscal profile — nunca lógica hardcoded numa tela (mesmo princípio do Master Prompt §6.11). +- `fiscal_rules` (motor de resolução automática de regra fiscal): portar como schema preparado, sem implementar motor de resolução nesta fase — mesma decisão consciente já tomada no legado ("não implementar resolução automática completa agora"), reavaliar na Fase 6. + +## Consequências +- Positivo: reaproveita ~19 tabelas de catálogo fiscal já desenhadas e testadas contra fontes reais (Siscomex/IBGE), reduzindo risco de erro em modelagem tributária brasileira (área de alto custo de erro). +- Positivo: separação nítida entre "classificação fiscal do produto" e "documento fiscal emitido" evita duplicar regra tributária em múltiplos lugares (Master Prompt §6.11). +- Negativo: sem motor de resolução automática, a escolha de código fiscal por operação continua dependendo de configuração manual do fiscal profile — aceito como escopo da Fase 6; motor completo fica como trabalho futuro explícito, não "silenciosamente esquecido". diff --git a/docs/adr/0012-three-apps-shared-identity.md b/docs/adr/0012-three-apps-shared-identity.md new file mode 100644 index 0000000..bdb9b16 --- /dev/null +++ b/docs/adr/0012-three-apps-shared-identity.md @@ -0,0 +1,18 @@ +# ADR-0012: Três aplicações web, identidade e backend compartilhados + +## Status +Aceito + +## Contexto +O EDEN precisa de 3 frontends (Core interno, Parceiros/Revenda, Assinante) com níveis de acesso e dados completamente diferentes, mas compartilhando o mesmo ecossistema de API e identidade (Master Prompt §7, §8, missão §0). O legado é uma aplicação única (OrçaFácil) sem essa separação — todo controle de acesso é por papel/feature dentro do mesmo frontend. + +## Decisão +- **Uma identidade pode ter acesso a uma ou mais aplicações** (`core`/`reseller`/`subscriber`), modelado explicitamente (tabela de vínculo identidade↔aplicação), nunca inferido pelo nome do papel (Master Prompt §5.1) — evita o erro de assumir "todo `reseller_admin` só acessa `reseller`", o que impediria, por exemplo, um funcionário Handix logar como suporte em nome de uma revenda no futuro, se o negócio pedir. +- **Cada aplicação é um frontend React+Vite próprio** (`apps/core-web`, `apps/reseller-web`, `apps/subscriber-web`), cada uma em seu container (ADR-0002), consumindo a mesma API (`apps/api`) via REST versionada (`/api/v1`). +- **Isolamento de dado por escopo, nunca por frontend**: a separação de app não substitui a checagem de escopo no backend (§5.2/ADR-0004) — um usuário `reseller` autenticado contra a API nunca deve conseguir, mesmo manipulando requests, ver dado de outra revenda; isso é reforçado por teste automatizado explícito (Master Prompt §15.2, item 12). +- **Design System único** (`packages/ui`) compartilhado pelas 3 apps, derivado do tema DreamsERP, garantindo consistência visual sem duplicar componente. + +## Consequências +- Positivo: onboarding de nova aplicação futura (ex.: app mobile) reaproveita a mesma API/identidade sem redesenho. +- Positivo: bug de isolamento fica mais fácil de testar (mesma API, 3 conjuntos de credenciais de teste, casos invariantes automatizados). +- Negativo: qualquer mudança de contrato de API precisa considerar as 3 apps simultaneamente — versionamento de API (`/api/v1`) e testes de contrato (Master Prompt §15.1) mitigam quebra silenciosa. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..d3025cd --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,102 @@ +# EDEN — Arquitetura (Fase 0) + +## 1. Estilo arquitetural + +**Modular monolith** em monorepo TypeScript (pnpm workspaces + Turborepo), conforme Master Prompt §4. Um único processo de API (`apps/api`) organizado por bounded contexts internos, com fronteiras de módulo explícitas e comunicação interna via eventos de domínio (outbox) — nunca chamada direta cross-módulo que quebre a fronteira. Ver ADR-0001. + +Motivo de não usar microserviços desde o início: custo operacional de dezenas de serviços não se paga no estágio atual; o modular monolith permite extrair um bounded context para serviço próprio no futuro (ex.: billing/fiscal, que já são os candidatos naturais por volume/isolamento) sem reescrever o domínio. + +## 2. Bounded contexts + +| Contexto | Núcleo de dados | Depende de | Agente responsável | +|---|---|---|---| +| **Identity & Access** | users, roles, permissions, sessions, resellers (vínculo) | — (fundacional) | `eden-security` | +| **Organization** | legal entities, companies, branches, warehouses, cost centers | Identity | `eden-architect` | +| **Commercial (CRM)** | leads, opportunities, quotes, pricing tiers, approval workflow | Identity, Organization | `eden-commercial` | +| **Customer 360** | client accounts (PF/PJ), reseller accounts, partners/QSA | Commercial | `eden-commercial` | +| **Contracts** | contracts, versions, amendments, renewals, status history | Customer 360, Commercial | `eden-commercial` | +| **Documents & E-signature** | templates, versions, generations, envelopes, signers, audit hash-chain | Contracts, Organization | `eden-frontend` (editor) + domínio próprio | +| **Inventory & Assets** | warehouses, stock ledger, serialized assets, RMA | Contracts (instalação) | `eden-inventory` | +| **Finance (AR/AP)** | receivables, boletos, dunning, payables, reconciliation | Contracts, Billing | `eden-finance` | +| **Billing** | billing accounts, cycles, subscriptions, invoices, billing runs | Contracts, Inventory (consumo) | `eden-finance` | +| **Fiscal** | catálogos fiscais, fiscal profiles, documentos fiscais emitidos | Billing | `eden-fiscal` | +| **Telecom (SaperX)** | circuitos, DIDs, consumo/CDR, conciliação | Customer 360, Billing | `eden-telecom` | +| **Support (Service Desk)** | tickets, SLA, OS | Customer 360, Contracts, Inventory | `eden-support` | +| **HR/Timeclock** | devices, employees, AFD, apuração, banco de horas | Organization (isolado do resto) | `eden-hr-timeclock` | +| **Integrations/Events** | outbox, webhooks, API clients, delivery log | transversal | `eden-api-integrations` | + +Regra de dependência: setas só "para trás" na tabela acima (uma linha não depende de uma que a segue) — evita ciclo entre módulos. HR/Timeclock é deliberadamente isolado (só depende de Organization) — pode ser paralelizado/adiado sem travar o núcleo comercial, replicando a recomendação do próprio `eden.md`. + +## 3. Estrutura do monorepo + +Conforme Master Prompt §4.2: + +```text +apps/ + api/ # API principal (modular monolith) + worker/ # jobs assíncronos (BullMQ) + core-web/ # EDEN Core (ERP interno) + reseller-web/ # EDEN Parceiros + subscriber-web/ # EDEN Assinante +packages/ + database/ # schema, migrations, query layer + contracts/ # DTOs/schemas/event contracts compartilhados + ui/ # Design System (derivado do tema DreamsERP) + auth/ # SDK de auth client-side comum às 3 apps + observability/ + config/ + testing/ + integrations/ # adapters Focus NFe, SaperX, Control iD, S3, SMTP + domain-shared/ +infra/ + docker/ + migrations/ +docs/ +.claude/ +``` + +## 4. Topologia de deployment / containers + +**Decisão explícita solicitada pelo operador**: Postgres roda em container próprio, separado dos containers de aplicação; cada uma das três aplicações web roda em seu próprio container, cada uma em porta distinta. Detalhamento completo em `docs/adr/0002-container-topology.md`. Resumo: + +```text +┌─────────────────────────────────────────────────────────────┐ +│ docker compose (rede interna "eden_net") │ +│ │ +│ ┌───────────────┐ ┌───────────────┐ ┌─────────────────┐ │ +│ │ eden-postgres │ │ eden-redis │ │ eden-api │ │ +│ │ :5432 (int) │ │ :6379 (int) │ │ :8080 → host │ │ +│ └───────┬───────┘ └───────┬───────┘ └────────┬────────┘ │ +│ │ │ │ │ +│ └─────────────┬─────┴─────────────────────┘ │ +│ │ (api é o único que fala com o banco) │ +│ ┌───────────────┐ ┌──▼────────────┐ ┌─────────────────┐ │ +│ │ eden-worker │ │ eden-core │ │ eden-parceiros │ │ +│ │ (sem porta) │ │ :3001→host │ │ :3002→host │ │ +│ └───────────────┘ └───────────────┘ └─────────────────┘ │ +│ ┌────────────────┐ │ +│ │ eden-assinante │ │ +│ │ :3003→host │ │ +│ └────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +Princípios: +1. **Nenhuma aplicação web fala direto com o Postgres** — todas as 3 (core/parceiros/assinante) consomem exclusivamente a API (`eden-api`), que é a única com credencial de banco. Isso preserva o requisito do Master Prompt de "nascer preparado para ser consumido por essas três aplicações" sem triplicar a superfície de acesso a dados. +2. **Postgres nunca expõe porta ao host em produção** — só rede interna do compose; em dev, opcionalmente mapeada para uma porta alta não-padrão para acesso de ferramenta local (ex. `55432:5432`), nunca `5432:5432` direto. +3. **Cada app web em porta própria e distinta**, definida em `.env` (`EDEN_CORE_PORT`, `EDEN_PARCEIROS_PORT`, `EDEN_ASSINANTE_PORT`, `EDEN_API_PORT`) — nenhuma hardcoded no compose, para permitir múltiplos ambientes na mesma máquina (dev/staging) sem colisão. +4. **Build multi-stage por app** (`infra/docker/Dockerfile.api`, `Dockerfile.web` parametrizado por app via build-arg) — imagens enxutas, sem devDependencies em produção. +5. **Healthcheck obrigatório em todo container** (Postgres via `pg_isready`, API via `/health/ready`, apps web via HTTP 200 na raiz) — `depends_on: condition: service_healthy`, não apenas ordem de start. +6. **Volumes nomeados** para dados do Postgres (`eden_pgdata`) — nunca bind mount direto de dado de produção para o filesystem do host sem estratégia de backup (backup lógico via `pg_dump` streaming para S3, Master Prompt §6.15, roda **de dentro** do container/worker, não do host). +7. Redis (cache/fila BullMQ) entra como container próprio (`eden-redis`) só quando o primeiro job assíncrono real precisar dele — não subir vazio "por precaução" (Master Prompt §4.1: "somente quando houver benefício real"). + +## 5. Identidade compartilhada entre as 3 apps + +Uma única API de autenticação/autorização (dentro de `eden-api`) emite sessão para as 3 aplicações. Uma identidade pode ter acesso a `core`/`reseller`/`subscriber` de forma explícita (tabela de vínculo, não inferida pelo nome do papel) — ver ADR-0012 e Master Prompt §5.1. + +## 6. Próximos documentos + +- `docs/security/threat-model.md` +- `docs/adr/*` (decisões referenciadas acima) +- `docs/implementation-plan.md` (backlog por fase) +- `docs/data-model/*` (ERD por domínio — produzido ao entrar em cada Fase) diff --git a/docs/assumptions.md b/docs/assumptions.md new file mode 100644 index 0000000..84d9180 --- /dev/null +++ b/docs/assumptions.md @@ -0,0 +1,13 @@ +# EDEN — Registro de Assunções (não bloqueantes) + +Conforme Master Prompt §2.3 — cada item aqui é uma decisão tomada para não bloquear o progresso, com a alternativa mais segura/coerente escolhida. Revisar com o operador quando possível. + +| # | Assunção | Alternativa escolhida | Revisitar quando | +|---|---|---|---| +| 1 | Retenção/eliminação de dados pessoais (LGPD) ainda não tem política definida pela Handix | Modelar coluna/config de retenção desde já no schema de Identity/Customer 360, mas manter processo de eliminação manual/auditado até jurídico definir política | Início da Fase 2 (Customer 360) | +| 2 | Ambiente de execução não tinha Docker/Node/pnpm instalados | Provisionar via gerenciador de pacote do SO no início da Fase 1, versão LTS atual do Node | Início da Fase 1 | +| 3 | Portas dos containers das 3 apps + API | Definidas via `.env` (`EDEN_CORE_PORT=3001`, `EDEN_PARCEIROS_PORT=3002`, `EDEN_ASSINANTE_PORT=3003`, `EDEN_API_PORT=8080`), não hardcoded — ver ADR-0002 | Se o operador já tiver portas reservadas em uso, ajustar `.env` | +| 4 | Projeto não estava sob controle de versão Git | Assumir que Git será iniciado no começo da Fase 1 (monorepo) — não iniciar prematuramente na Fase 0 para não versionar `tema_do_Eden.zip` (360MB) sem `.gitignore` pronto | Início da Fase 1 | +| 5 | `super_admin` inicial do EDEN | Seguir o padrão do legado (promoção via seed/migration com e-mail vindo de env `EDEN_SUPERADMIN_EMAIL`, nunca hardcoded no código) — Master Prompt §2.2 já define isso | Bootstrap da Fase 1 | +| 6 | Overtime multiplier (Art. 59 CLT) no módulo de ponto | Legado tem os campos (`overtime_multiplier`, `apply_multiplier_to_time_bank`) mas não os aplica no motor de cálculo — EDEN preserva os campos como metadado e registra decisão explícita (implementar ou não a multiplicação) só quando a Fase 9 (RH/Ponto) for iniciada | Início da Fase 9 | +| 7 | Extensão Postgres `btree_gist` para exclusão de conflito de horário (Agenda) | Legado evita a extensão deliberadamente (nunca usada em produção) e resolve conflito via transação + `SELECT ... FOR UPDATE`. EDEN preserva essa escolha por padrão — reavaliar `EXCLUDE`/GiST só se performance exigir | Início da Fase 9 (Agenda) | diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 0000000..8013139 --- /dev/null +++ b/docs/glossary.md @@ -0,0 +1,58 @@ +# EDEN — Glossário de Domínio + +Vocabulário Handix/telecom/ERP, preservado do legado OrçaFácil (`eden.md`) sempre que possível — não inventar sinônimos novos para conceitos já nomeados. + +## Identidade e acesso + +- **super_admin**: papel com bypass total, hardcoded (nunca passa por `role_permissions`), peso hierárquico fixo 100, nunca editável via UI. +- **role weight (peso de papel)**: inteiro 0–99 (super_admin = 100, fixo) que impede um ator de administrar/atribuir papel com peso maior que o seu, mesmo tendo a feature de gestão liberada. +- **feature key**: chave de tela/recurso gateável (`ofertas`, `clientes_todos`, `fiscal`, etc.), com dois níveis de acesso (`view`/`edit`) por papel. +- **scope (escopo de dados)**: no EDEN, evolução do padrão "próprio vs. todos" do legado para `own`/`team`/`reseller`/`legal_entity`/`all`. +- **token_version**: mecanismo legado de invalidação de sessão (incrementa a cada logout/reset de senha). No EDEN, substituído por refresh tokens revogáveis (ver ADR-0003), mas o *conceito* de "derrubar todas as sessões de um usuário" deve ser preservado. + +## Comercial + +- **contract_period (faixa de preço)**: período (0/12/24/36/48 meses) que determina qual coluna de preço do produto foi usada. Não é necessariamente o prazo de permanência real. +- **fidelity_period (fidelidade efetiva)**: prazo de permanência realmente assinado pelo cliente, quando **menor** que `contract_period`. Só produz efeito legal (vigência, multa, vencimento) depois de **aprovado**. +- **condição especial (`proposed_monthly_total`)**: valor mensal negociado diferente do total de tabela. Desconto exige aprovação; acréscimo (markup) não. +- **needsApproval**: `isDiscount OR hasReducedFidelity` — uma única aprovação cobre as duas exceções quando coexistem. +- **rateio proporcional**: distribuição de uma condição especial entre os itens da oferta, proporcional ao peso de cada item no total de tabela — nunca abate um item isolado. +- **oferta travada**: estado de uma oferta (`quotes`) após o cadastro de cliente ser iniciado (`client_registration_id` setado) — não editável exceto por `super_admin`. +- **deal_status**: estado do negócio (`orcamento`/`fechado`/`perdido`), distinto de `status` (estado do documento/proposta). +- **contrato (legado)**: no OrçaFácil não é tabela própria — é a junção de `client_registrations` ativos + `quotes`. No EDEN, torna-se agregado de primeira classe (`contracts`) — ver seção 6.6 do Master Prompt e ADR-0006. + +## Cadastro / Cliente / Revenda + +- **client_registrations**: cadastro completo (PF/PJ) de um cliente final, nascido de uma oferta fechada (ou "direto"). +- **registration_status**: máquina de estados `rascunho → pendente_validacao → ativo ⇄ bloqueado/inativo`. +- **reaproveitar cadastro ativo**: atalho que copia identidade/endereço/contato de um cadastro já `ativo` para uma nova oferta, sem passar pelo formulário público de novo. +- **sócio assinante**: sócio (QSA) de uma PJ marcado como signatário — substitui inteiramente o bloco "Representante da Empresa". +- **Programa de Canais**: módulo de aprovação de revendas (`reseller_registrations`), com dois tipos: `finder` (pontual) e `recorrente` (com Termo de Adesão). + +## Documentos e assinatura + +- **document_template_versions**: versionamento imutável de template (Tiptap JSON); só 1 draft por vez; publicar é irreversível para aquela versão. +- **merge field**: variável de documento inserida via catálogo fechado (whitelist), nunca texto livre `{{...}}` no editor visual novo. +- **signature_envelope**: pacote de assinatura (documentos + signatários) com máquina de estados de 19 estágios (DRAFT → ... → COMPLETED). +- **hash-chain de auditoria**: cadeia SHA-256 append-only de eventos do envelope, com verificação em duas camadas (linkage + conteúdo). +- **OTP**: código de 6 dígitos, HMAC-SHA256 com segredo de servidor, TTL de 5 min, uso único. + +## Fiscal + +- **NFCom**: Nota Fiscal de Serviços de Comunicação (telecom). +- **NFS-e**: Nota Fiscal de Serviço eletrônica (ISS municipal). +- **fiscal profile (`product_fiscal_profiles`)**: liga um produto ao(s) código(s) fiscal(is) aplicável(is), separado por componente de faturamento (`RECURRING`/`IMPLEMENTATION`) — nunca duplicado dentro de `products`. +- **IBS/CBS**: tributos do "IVA dual" da reforma tributária brasileira — catálogos já modelados no legado, ainda sem motor de emissão. + +## Ponto eletrônico + +- **AFD**: Arquivo Fonte de Dados — formato legal (Portaria MTP 671/2021) de marcações de ponto, imutável uma vez importado. +- **NSR**: Número Sequencial de Registro — chave de idempotência por marcação, nunca inventado se ausente. +- **banco de horas**: livro-razão (crédito/débito em minutos) sem coluna de saldo persistida. +- **fechamento de período**: veto (não histórico paralelo) — mês `FECHADO` bloqueia recálculo/aprovação/lançamento manual até reabertura auditada. + +## Plataforma / Infraestrutura + +- **EDEN Core / Parceiros / Assinante**: as três aplicações web do ecossistema (ERP interno, portal de revenda, portal do assinante), compartilhando o mesmo backend de identidade/API. +- **outbox transacional**: padrão de persistência de eventos de domínio na mesma transação do negócio, para publicação confiável a integrações (n8n, webhooks) sem perda em caso de falha pós-commit. +- **adapter/port**: padrão de integração externa (Focus NFe, SaperX, Control iD) isolando domínio de detalhes de protocolo/provider. diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md new file mode 100644 index 0000000..868cf79 --- /dev/null +++ b/docs/implementation-plan.md @@ -0,0 +1,83 @@ +# EDEN — Plano de Implementação por Fase + +Backlog macro conforme Master Prompt §18. Cada fase só inicia com o gate da fase anterior verde (build/typecheck/lint/testes críticos + doc/ADR necessária atualizada — Definition of Done, Master Prompt §20). + +## Fase 0 — Descoberta e arquitetura (CONCLUÍDA — ver docs/progress.md) +- [x] Ler `eden.md` integralmente (4597 linhas, 7 módulos). +- [x] Inventariar projeto/tema/assets (`docs/project-inventory.md`). +- [x] `docs/glossary.md`. +- [x] `docs/architecture.md` (bounded contexts + topologia de containers). +- [x] `docs/security/threat-model.md`. +- [x] ADRs 0001–0012. +- [x] `.claude/agents/` (14 agentes). +- [x] `.claude/skills/` (7 skills mínimas). +- [x] Hooks (`PreToolUse`/`PostToolUse`/`Stop`), testados via pipe. +- [x] `docs/implementation-plan.md` (este arquivo). +- [x] Revisão cruzada final (self-review contra critérios eden-architect + eden-security + eden-database) — ver `docs/progress.md`. + +**Gate de saída**: VERDE. Fase 1 pode começar quando autorizada. + +## Fase 1 — Plataforma +- Monorepo (pnpm workspaces + Turborepo), `git init`. +- Docker: provisionar Docker/Node LTS/pnpm no ambiente; `compose.yaml` com topologia da ADR-0002 (Postgres separado + eden-core/parceiros/assinante em containers/portas distintas). +- Postgres 18 + migrations versionadas (`packages/database`). +- Config/env (`.env.example` completo — DB, JWT/session, SMTP, S3, chaves de criptografia). +- Logging estruturado + correlation id. +- Auth (ADR-0003), Users, Roles/Permissions/Scopes (ADR-0004). +- Companies/Legal Entities (Organization). +- S3 adapter, SMTP adapter. +- Audit log append-only (hash-chain reforçado por trigger). +- API/OpenAPI 3.1 base (`/api/v1`). +- Design System inicial (`packages/ui`) a partir do inventário do tema DreamsERP. +- Seed: entidade Handix + superadmin via env (`EDEN_SUPERADMIN_EMAIL`/`PASSWORD`). + +## Fase 2 — Comercial +- CRM (leads, oportunidades, pipeline). +- Produtos/grupos/subgrupos/marcas (evoluindo `products` do legado). +- Pricing tiers (faixas 0/12/24/36/48) + fidelidade reduzida com aprovação (ADR de negócio herdada do legado, seção 6.4 do Master Prompt). +- Ofertas + approval workflow genérico. +- Customer 360 (clientes PF/PJ, cadastro público com token, sócios/QSA). +- Revendas (cadastro/aprovação + operacional). + +## Fase 3 — Contratos/documentos +- Contracts first-class (ADR-0006). +- Templates de documento (Tiptap, versionamento, publicação) + PDF server-side (Playwright/Chromium, sem navegação de rede). +- Assinatura eletrônica (envelope, OTP, hash-chain, certificado, verificação pública) — portar fielmente do legado (Master Prompt §6.7). +- Portal de cadastro público (cliente/revenda). + +## Fase 4 — Estoque/ativos +- Warehouse, ledger de movimentos (ADR-0007). +- Serial/patrimônio/MAC, reserva/instalação/comodato/RMA. + +## Fase 5 — Billing/financeiro +- Subscription/billing engine (ADR-0008). +- Invoices, AR/AP. +- Boletos (provider abstraction), dunning, conciliação. +- Dashboards financeiros (MRR, ARR, churn, aging). + +## Fase 6 — Fiscal/telecom +- Catálogos fiscais + `product_fiscal_profiles` (portados do legado). +- Adapter Focus NFCom/NFS-e (ADR-0011). +- Adapter SaperX (ADR-0010) — circuitos, DIDs, consumo, conciliação. + +## Fase 7 — Suporte +- Service desk, SLA, OS, integração com portais. + +## Fase 8 — Portais +- EDEN Parceiros completo (isolamento cross-reseller testado). +- EDEN Assinante completo (isolamento cross-customer testado). + +## Fase 9 — RH/Ponto + backoffice legado restante +- Control iD + AFD (Portaria 671) — imutabilidade do dado bruto (Master Prompt §6.14). +- Banco de horas, fechamento de período. +- Agenda (salas/carros), backup (streaming S3), dashboards restantes. + +## Fase 10 — Hardening +- E2E completos (15 fluxos mínimos do Master Prompt §15.2 + casos invariantes §15.3). +- Performance, security review, access-control review. +- Migration rehearsal, backup/restore drill. +- Runbooks (`docs/runbooks/`). +- Release readiness contra os critérios de aceitação global (Master Prompt §21). + +## Dependências entre fases (resumo) +Fase 1 → 2 → 3 → 4 → 5 → 6 (fiscal depende de billing) → 7 → 8 (depende de 2–7 existirem parcialmente) → 9 (paralelizável a partir da Fase 1, isolada) → 10. diff --git a/docs/progress.md b/docs/progress.md new file mode 100644 index 0000000..58c5f66 --- /dev/null +++ b/docs/progress.md @@ -0,0 +1,83 @@ +# EDEN — Progresso + +## Fase 0 — Descoberta e Arquitetura: CONCLUÍDA + +Data: 2026-09-03. + +### Entregáveis +- `docs/project-inventory.md`, `docs/glossary.md`, `docs/architecture.md`, `docs/assumptions.md`, `docs/security/threat-model.md`, `docs/implementation-plan.md`, `docs/progress.md` (este arquivo). +- 12 ADRs em `docs/adr/` (0001–0012), cobrindo: modular monolith, topologia de containers (Postgres isolado + eden-core/parceiros/assinante em containers/portas distintas — decisão explícita do operador), auth/sessões, modelo de permissões, criptografia/segredos, contrato first-class, stock ledger, separação billing/finance/fiscal, outbox transacional, adapter SaperX, adapter Focus NFe, três apps com identidade compartilhada. +- 14 subagentes em `.claude/agents/`: eden-architect, eden-database, eden-security, eden-commercial, eden-finance, eden-fiscal, eden-inventory, eden-telecom, eden-support, eden-hr-timeclock, eden-frontend, eden-api-integrations, eden-qa, eden-code-reviewer. +- 7 skills em `.claude/skills/`: eden-domain, eden-security, eden-finance, eden-fiscal, eden-inventory, eden-telecom, eden-timeclock — cada uma com `SKILL.md` + `references/`. +- Hooks em `.claude/settings.json` + `.claude/hooks/*`: `PreToolUse` (guarda de comandos Bash destrutivos/externos e de acesso a arquivo fora do projeto/`.ssh`/`/etc`/segredo), `PostToolUse` (lint/typecheck incremental, fail-open enquanto pnpm não existir), `Stop` (checagem advisória de segredo/TODO crítico no diff, silenciosa até existir repositório git). Testados via pipe com payloads sintéticos — todos os cenários (allow/ask/deny/fail-open) se comportaram como esperado. + +### Revisão cruzada (self-review contra os critérios dos agentes eden-architect / eden-security / eden-database, criados nesta mesma fase) + +**eden-architect**: bounded contexts (`docs/architecture.md` §2) têm direção de dependência consistente (nenhum ciclo); HR/Timeclock corretamente isolado; candidatos a extração futura (Billing, Fiscal) identificados sem extração prematura. Nenhuma feature de negócio foi implementada antes deste gate — consistente com o Master Prompt §18. + +**eden-security**: threat model cobre as superfícies do legado (rotas públicas, auth, autorização, integrações, upload, PDF, auditoria, infraestrutura) mais os invariantes de teste obrigatórios (§15.3). Hooks `PreToolUse` já impõem, a nível de ferramenta, várias regras de §2.1/§2.2 (fora do projeto, `.ssh`/`/etc`, impressão de `.env`, SSH/SCP externo, remoção ampla de volume/prune). Pendência real registrada (não bloqueante): política de retenção/eliminação LGPD ainda não definida pela Handix (`docs/assumptions.md` #1). + +**eden-database**: princípios de modelagem (§11) já refletidos nas ADRs de contrato/estoque/billing (nunca saldo editável, nunca CASCADE indiscriminado, JSONB só para snapshot/metadata). Nenhum schema físico foi criado ainda nesta fase — corretamente adiado para a Fase 1, quando o Postgres 18 realmente subir. + +**Nota**: esta revisão foi feita pela própria sessão orquestradora aplicando os critérios definidos nos arquivos de agente recém-criados. Os subagentes dedicados (`eden-architect`, `eden-security`, `eden-database`) ficam disponíveis para revisões reais em sessões futuras assim que o Claude Code os carregar como subagent types. + +### Pendências não bloqueantes (ver `docs/assumptions.md`) +1. Política de retenção/eliminação de dados pessoais (LGPD) — Handix ainda não definiu. +2. Docker/Node/pnpm precisam ser provisionados no ambiente antes da Fase 1. +3. Portas dos containers definidas via `.env` (não fixas) — ver ADR-0002. +4. Git ainda não iniciado no projeto — planejado para o início da Fase 1. +5. Inventário fino do `tema_do_Eden.zip` (extração de componentes) adiado para o início de `eden-frontend`. + +## Gate de saída da Fase 0: VERDE + +Todos os itens do checklist da seção 22 do Master Prompt (“Primeira Execução”) estão completos. + +--- + +## Fase 1 — Plataforma: EM ANDAMENTO + +Data de início: 2026-09-03. + +### Ambiente provisionado +- Docker CE 29.7.2 + Compose v5.5.0 (repositório oficial Docker, não o `docker.io` do Debian) instalados via apt. +- Node.js 22.23.2 LTS (NodeSource) + pnpm 11.25.0 (via corepack). +- Git 2.47.3. Repositório inicializado (`main`), remoto `origin` = `https://git.falehandix.com.br/Matheus/eden.git` (nenhum push feito ainda). + +### Monorepo +- pnpm workspaces + Turborepo. 5 apps (`api`, `worker`, `core-web`, `reseller-web`, `subscriber-web`) + 9 packages, todos com `package.json`/`tsconfig.json` válidos. +- `pnpm install`, `pnpm turbo run typecheck` e `pnpm turbo run build` passam 100% (15/15 e 14/14 tarefas, respectivamente) em todo o monorepo. +- `apps/api`: NestJS mínimo com `/health/live` e `/health/ready` (este último checando Postgres de verdade via `@eden/database`). +- `apps/worker`: placeholder que confirma conectividade com o banco; sem BullMQ/Redis ainda (conforme ADR-0002 — só entra quando houver job real). +- `apps/core-web`, `apps/reseller-web`, `apps/subscriber-web`: Vite + React + TS + Tailwind, cada um com favicon oficial do EDEN. +- `packages/database`: migrations via `node-pg-migrate` (formato `.cjs`), client de query (`pg` Pool). + +### Banco de dados +- Migration baseline aplicada com sucesso contra Postgres 18.6 real (container `eden-postgres`): `roles`, `role_permissions`, `applications`, `users`, `user_applications`, `sessions`, `audit_log` — cobrindo Identity & Access (ADR-0003/ADR-0004) e um audit log append-only com hash-chain (testado: `UPDATE`/`DELETE` são efetivamente bloqueados pelo trigger, só liberados via `SET LOCAL eden.allow_audit_mutation`). +- Seeds: 4 papéis do sistema (pesos idênticos ao legado) + as 3 aplicações (`core`/`reseller`/`subscriber`). +- Organization (empresas/entidades legais), Commercial e demais bounded contexts **ainda não têm migration** — ficam para quando cada fase começar, por design (ver ADR-0001). + +### Docker Compose — topologia ADR-0002 validada de ponta a ponta +`docker compose --env-file .env up -d` sobe os 6 serviços com sucesso: + +| Serviço | Status observado | +|---|---| +| `eden-postgres` | healthy — porta dev 55432, nunca 5432 direto | +| `eden-api` | healthy — único serviço com `DATABASE_URL`; `/health/ready` confirma Postgres via rede interna | +| `eden-worker` | up — conecta ao Postgres, sem porta exposta | +| `eden-core` (:3001) | healthy — HTTP 200 | +| `eden-parceiros` (:3002) | healthy — HTTP 200 | +| `eden-assinante` (:3003) | healthy — HTTP 200 | + +Bugs de bootstrap corrigidos nesta sessão (documentados aqui para não se repetirem): +1. Imagem oficial `postgres:18-alpine` mudou o layout do volume — precisa montar `/var/lib/postgresql` (não `.../data`). +2. `apps/api/package.json` tinha `"type": "module"` incompatível com o `tsconfig` (saída CommonJS) — Node falhava com `exports is not defined`. +3. `node-pg-migrate --envPath` não carregava o `.env` corretamente nesta versão — trocado por `bash -c 'source .env; ...'` nos scripts `migrate:up`/`migrate:down`. +4. Healthcheck dos 3 frontends usava `wget http://localhost/...`, que resolve para `::1` (IPv6) dentro do container — nginx só escuta IPv4; trocado para `127.0.0.1`. + +### Pendências da Fase 1 (não iniciadas) +- Módulo de autenticação real (Argon2id, JWT curto + refresh rotativo, MFA) — ADR-0003 desenhada, implementação ainda não começou. +- Companies/Legal Entities (Organization). +- Adapters S3/SMTP. +- OpenAPI 3.1. +- Extração do Design System a partir do tema DreamsERP (`packages/ui` ainda é placeholder). +- Nenhum commit git feito ainda — aguardando solicitação explícita do operador. diff --git a/docs/project-inventory.md b/docs/project-inventory.md new file mode 100644 index 0000000..2069433 --- /dev/null +++ b/docs/project-inventory.md @@ -0,0 +1,59 @@ +# EDEN — Inventário do Projeto + +Estado em: 2026-09-03 (Fase 0 — Descoberta e Arquitetura). + +## 1. Arquivos de origem (raiz do projeto) + +| Arquivo | Tamanho | Papel | +|---|---|---| +| `EDEN_MASTER_PROMPT_CLAUDE.md` | 45.8 KB | Fonte de verdade nº1 — missão, regras operacionais, arquitetura alvo, ordem de implementação. | +| `eden.md` | 325.5 KB / 4597 linhas | Fonte de verdade nº2 — especificação funcional completa do legado OrçaFácil (engenharia reversa). Índice de 7 módulos, ver seção 3 abaixo. | +| `tema_do_Eden.zip` | 360 MB | Tema visual comprado: **DreamsERP v1.0.1** (Angular + Bootstrap). Base para o Design System do EDEN — não copiar JS/Angular, extrair componentes visuais para React/Tailwind. | +| `Eden_logo_horizontal.png` | 454 KB | Logo oficial — uso em headers/documentos. | +| `Eden_logo_vertical.png` | 432 KB | Logo oficial — uso em telas de login/splash. | +| `eden_fav_ico.png` | 573 KB | Favicon oficial — precisa ser convertido/otimizado para tamanhos padrão de favicon (16/32/180px) antes do uso web. | + +## 2. Conteúdo do `tema_do_Eden.zip` + +- Raiz: `dreamserp-v1.0.1/`. +- Contém: `angular.zip` (build/fonte Angular do template), `documentation/` (HTML de documentação do tema, com assets próprios — Bootstrap, fontes Nunito/Poppins, imagens de banner/logo/tecnologia). +- Total de 2198 entradas no arquivo. +- **Decisão registrada em ADR-0012 pendente de detalhamento**: nenhuma regra de negócio do EDEN deve depender de HTML/JS do tema; extrair apenas tokens visuais (cores, tipografia, espaçamento, componentes) para `packages/ui`. +- Inventário completo de componentes reaproveitáveis fica pendente para quando o Design System (`eden-frontend`) for iniciado na Fase 1 — extrair o zip para pasta temporária dentro do projeto (`.tmp/theme-inventory/`, git-ignorada) só nesse momento, para não versionar 360MB de asset de terceiro sem necessidade. + +## 3. Índice de módulos do `eden.md` (legado OrçaFácil) + +| # | Módulo | Linhas aprox. | Mapeia para (EDEN) | +|---|---|---|---| +| 1 | Auth, Usuários, Papéis, Permissões, Segurança | 1–1450 | `eden-security`, IAM (Fase 1) | +| 2 | Produtos, Ofertas (Quotes), Faixas de Preço/Fidelidade, Contratos | 690–1450 | `eden-commercial` (Fase 2), `eden-commercial`→Contratos first-class (Fase 3) | +| 3 | Cadastro de Cliente e Cadastro/Gestão de Revendas | 1451–2206 | `eden-commercial` (Customer 360, Revendas) (Fase 2/8) | +| 4 | Templates de Documentos, PDF, Assinatura Eletrônica | 2207–2822 | módulo Documentos (Fase 3) | +| 5 | Módulo Fiscal (NCM, CFOP, Municípios) | 2823–3143 | `eden-fiscal` (Fase 6) | +| 6 | Módulo de Ponto Eletrônico (Timeclock) | 3144–4141 | `eden-hr-timeclock` (Fase 9) | +| 7 | Backoffice diverso (Backup, Agenda, Welcome Page, Empresa, Dashboard) | 4142–4597 | plataforma/backoffice (Fase 1/9) | + +Leitura completa registrada — nenhuma seção pulada. + +## 3.1 Controle de versão + +- Repositório git inicializado em `/opt/eden` na Fase 1 (branch `main`). +- Remoto `origin`: `https://git.falehandix.com.br/Matheus/eden.git` (servidor git próprio da Handix, definido pelo operador). +- Nenhum push realizado ainda — commits/push só ocorrem quando explicitamente solicitados. + +## 4. Ambiente de execução observado + +- Sistema operacional: Linux (Debian 13), sem systemd de usuário próprio visível além do host. +- **Não instalados neste ambiente**: `docker`, `docker compose`, `node`, `pnpm`. Precisam ser provisionados antes da Fase 1 (bootstrap de plataforma). +- Não é um repositório Git (`git status` não aplicável) — decisão pendente sobre iniciar `git init` no início da Fase 1. + +## 5. Decisões já tomadas nesta Fase 0 + +- Topologia de deployment: Postgres em container próprio + um container por aplicação web (`eden-core`, `eden-parceiros`, `eden-assinante`), cada um em porta distinta — ver `docs/adr/0002-container-topology.md`. +- Demais decisões arquiteturais: ver `docs/adr/`. + +## 6. Pendências explícitas (não bloqueantes, registradas conforme regra 2.3 do Master Prompt) + +- Extração/inventário fino do `tema_do_Eden.zip` (componentes reaproveitáveis) — adiado para o início de `eden-frontend`. +- Provisionamento de Docker/Node/pnpm no ambiente — parte da Fase 1. +- Definição de qual gerenciador de containers/orquestração (Docker Compose simples vs. outra ferramenta) — assumido Docker Compose por ser o que o Master Prompt (seção 4.3) já pede; ver ADR-0002. diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md new file mode 100644 index 0000000..9337c64 --- /dev/null +++ b/docs/security/threat-model.md @@ -0,0 +1,82 @@ +# EDEN — Threat Model (Fase 0) + +Modelo inicial, por bounded context, método STRIDE simplificado. Revisar a cada fase (§18 do Master Prompt) com o agente `eden-security`. + +## 1. Ativos críticos + +1. Credenciais de autenticação (senhas, refresh tokens, OTP, tokens de link público/assinatura). +2. Dados pessoais PF/PJ (CPF/CNPJ, endereço, contatos) — LGPD. +3. Segredos de integração (Focus NFe, SaperX, Control iD, SMTP, S3, chaves de IA) — sempre cifrados em repouso (AES-256-GCM), chave raiz fora do banco. +4. Registros financeiros/fiscais (títulos, faturas, documentos fiscais emitidos) — integridade e auditabilidade. +5. Cadeia de auditoria de assinatura eletrônica (hash-chain) — valor probatório/legal. +6. Dado bruto de ponto (AFD) — valor legal, imutabilidade. +7. Dump de backup do Postgres (contém tudo acima) — acesso altamente restrito. + +## 2. Superfícies de ataque e mitigação por camada + +### 2.1 Rotas públicas sem autenticação +Existem por necessidade de negócio (cadastro de cliente/revenda, assinatura eletrônica, verificação pública, ViaCEP/CNPJ lookup client-side). Mitigação obrigatória em toda rota pública nova: +- Token opaco ≥96 bits de entropia como capacidade de acesso, nunca sequencial/adivinhável. +- Rate limiting por IP **e** por token/identidade quando aplicável (duas dimensões, nunca uma só). +- Revalidação total de regra de negócio no backend — o cliente é sempre não confiável. +- 404/409 sem vazar existência de recurso quando aplicável (mensagens genéricas para "não existe" vs. "já usado"). + +### 2.2 Autenticação e sessão +- Herdar do legado: `token_version`-like mecanismo de invalidação global, MAS evoluir para refresh token rotativo hashado (ADR-0003) — não repetir JWT de 30 dias sem revogação. +- Argon2id (não bcrypt) para novo hash de senha — custo a definir em ADR de segurança/config, revisável sem quebrar hashes existentes (versionamento de parâmetro). +- MFA/TOTP obrigatório para `super_admin`/`admin` assim que o módulo existir. +- Lockout progressivo por conta + rate limit por IP (duplo, como no legado) — nunca só um dos dois. + +### 2.3 Autorização +- Nunca confiar em `role`, `customer_id`, `reseller_id`, `legal_entity_id` vindos do cliente — resolver sempre a partir da sessão/token no servidor. +- `super_admin` mantém bypass total mas toda ação sensível permanece auditada (bypass de autorização ≠ bypass de auditoria). +- Testar isolamento cross-reseller e cross-subscriber explicitamente em CI (casos invariantes do Master Prompt §15.3). + +### 2.4 Dados em trânsito/repouso +- TLS obrigatório ponta a ponta (terminação em proxy reverso — nunca assumir rede interna "confiável" sem TLS entre containers quando trafegar dado sensível entre hosts distintos; dentro do mesmo compose/host, aceitável por rede docker isolada, mas revisar se algum dia sair para múltiplos hosts). +- Campos de alto risco (ver Master Prompt §5.4) cifrados com AES-256-GCM, chave raiz via secret manager/env fora do banco, versionamento de chave. +- Nenhum arquivo (documento, anexo, backup) exposto por URL pública direta — sempre proxy autenticado pelo backend (padrão herdado do legado, preservar). + +### 2.5 Integrações externas (Focus NFe, SaperX, Control iD, n8n) +- Adapter/port isolado por provider (Master Prompt §13) — nunca acoplar domínio ao payload bruto externo. +- Timeout + retry com backoff + circuit breaker — indisponibilidade externa nunca corrompe o ERP nem trava a request HTTP do usuário (usar fila/worker). +- Webhooks sempre autenticados (assinatura HMAC) e idempotentes (event id + delivery log). +- n8n nunca acessa o Postgres diretamente — só via API/eventos com service accounts escopados. + +### 2.6 Upload de arquivo +- Validação de MIME real (não só extensão), limite de tamanho por rota, scan hook preparado para malware, armazenamento fora do webroot, nunca executável a partir do storage. + +### 2.7 Geração de documento/PDF +- Sanitização de HTML em duas camadas (allowlist estrita de tags/atributos/estilos/schemes de URL) antes de qualquer renderização — herdar do legado. +- Renderer (Chromium/Playwright) sem navegação de rede (`page.setContent` apenas, bloquear qualquer request de rede) — elimina SSRF por construção, herdar do legado. + +### 2.8 Auditoria +- Append-only reforçado em nível de banco (trigger que recusa UPDATE/DELETE fora de uma escotilha administrativa explícita e ela própria auditada) — não confiar só em "a aplicação nunca faz UPDATE". +- Ordem de eventos por sequência monotônica (serial/bigserial), nunca por timestamp (timestamps podem colidir dentro de uma transação). +- Nunca gravar segredo em claro no audit log (nem em metadata JSON). + +### 2.9 Infraestrutura/containers +- Postgres nunca exposto publicamente (ver `docs/architecture.md` §4). +- Segredos de ambiente via `.env`/secret store, nunca em imagem de container ou repositório. +- Cada container roda com usuário não-root quando a imagem base permitir. +- Backup: dump em streaming (nunca materializado em disco/memória local), acesso de restauração com fricção deliberada (confirmação explícita da chave do backup, como no legado). + +## 3. Casos invariantes de segurança (a testar sempre — Master Prompt §15.3) + +- Desconto pendente nunca vira valor legal aprovado sem `approval_status = approved`. +- `fidelity_period` não aprovado nunca altera vigência/multa/vencimento legal. +- Mesmo serial/MAC/patrimônio nunca alocado duas vezes simultaneamente. +- Webhook repetido nunca duplica pagamento; billing run repetido nunca duplica fatura; mesma referência fiscal nunca emite documento fiscal duplicado. +- Usuário de revenda nunca acessa dado de outra revenda alterando UUID na URL; assinante nunca acessa documento de outro customer account. +- AFD bruto nunca é alterável por nenhuma rota administrativa. +- Audit log crítico nunca é mutável pela aplicação em uso normal. + +## 4. LGPD — pontos de atenção específicos + +- Minimização: cadastro público de cliente/revenda já segue esse princípio no legado (só pede o necessário por PF/PJ) — preservar. +- Direito de exportação/eliminação do titular: não existe hoje no legado — **gap a suprir no EDEN**, registrar ADR quando o módulo de Identity/Customer 360 for implementado. +- Retenção configurável: não existe hoje no legado (dados nunca expiram automaticamente) — decisão de produto a tomar com jurídico/DPO da Handix antes da Fase 2, registrar assunção em `docs/assumptions.md` enquanto isso. + +## 5. Revisão + +Este documento deve ser revisitado ao final de cada fase (Master Prompt §18) e sempre que um novo adapter de integração externa for adicionado. diff --git a/eden.md b/eden.md new file mode 100644 index 0000000..c0edfb1 --- /dev/null +++ b/eden.md @@ -0,0 +1,4597 @@ +# EDEN — Especificação Funcional Completa para Reconstrução do ERP + +> Este documento foi extraído por engenharia reversa do sistema legado **"OrçaFácil"** (ERP de +> vendas/gestão para revendas de telecom/ISP, operado pela Handix), lendo o código-fonte +> diretamente (rotas, migrations, componentes de frontend). O objetivo é servir de +> **especificação funcional completa** para outra sessão do Claude construir, do zero, um +> sistema equivalente chamado **"Eden"**, usando um frontend diferente. +> +> **Este arquivo NÃO contém código do Eden** — é documentação de regras de negócio, modelo de +> dados, fluxos, cálculos e segurança do sistema atual, escrita para ser lida e implementada por +> um agente de IA sem acesso ao repositório original. + +--- + +## Stack do sistema de referência (OrçaFácil) — contexto, não obrigação + +O Eden pode (e provavelmente vai) usar outro frontend, conforme pedido. Para contexto, o +sistema atual é: + +- **Frontend**: React 18 + Vite, React Router, `@tanstack/react-query` para cache/estado de + chamadas, Tailwind CSS, Radix UI (componentes), React Hook Form + Zod, Tiptap (editor rico), + `html2canvas` + `jsPDF` (geração de PDF client-side, usada em templates legados/certificado de + assinatura) e Playwright/Chromium server-side (geração de PDF nova, mais robusta). +- **Backend**: Node.js + Express, `express-async-errors`, `jsonwebtoken` (JWT HS256), + `bcryptjs` (hash de senha), `express-rate-limit`, `pg` (Postgres) + `node-pg-migrate` + (migrations versionadas em arquivo), `multer` (upload), `@aws-sdk/client-s3` (storage de + objetos), `nodemailer` (e-mail via SMTP), `playwright` (PDF), `sanitize-html`. +- **Banco**: PostgreSQL. Sem ORM — SQL parametrizado direto via `pg`. +- **Comunicação front↔back**: REST simples via `fetch`, sem GraphQL/tRPC. JWT Bearer no header + `Authorization`. +- **Sem multi-tenant de infraestrutura**: é uma aplicação única servindo várias revendas + (isolamento lógico via `reseller_id` + sistema de papéis/features, não por schema/banco + separado). + +--- + +## Recomendação de como usar este documento (agentes/etapas sugeridas) + +Este documento tem ~4500 linhas cobrindo 7 módulos. Para uma sessão do Claude construir o Eden +a partir dele com qualidade, a recomendação é **não tentar implementar tudo numa passada só**. +Sugestão de abordagem, da mesma forma que este documento foi produzido (paralelizar leitura, +mas serializar decisão de arquitetura): + +1. **Fase de arquitetura (1 sessão, modo de planejamento, não implementação)** — ler este + documento inteiro (ou pelo menos as seções 1–4, que são o núcleo comercial) e definir: stack + escolhida, schema de banco definitivo (pode divergir do legado onde este documento sinalizar + "avaliar/decidir no Eden"), estrutura de pastas, estratégia de auth. Produzir um plano curto + antes de escrever qualquer código. **Não pule esta fase** — o sistema tem dependências + cruzadas fortes (ex.: fidelidade contratual afeta cálculo financeiro, texto de contrato E + vencimento ao mesmo tempo). +2. **Ordem de implementação sugerida** (cada módulo depende dos anteriores): + 1. **Autenticação, Papéis e Permissões** (`## 1 — Auth, Roles & Segurança`) — é a base de + tudo, nenhum outro módulo funciona sem isso. + 2. **Produtos, Ofertas e Contratos** (`## 2`) — o núcleo comercial do sistema. + 3. **Cadastro de Cliente e Revenda** (`## 3`) — depende de Ofertas (uma oferta fechada gera + um cadastro). + 4. **Templates de Documento e Assinatura Eletrônica** (`## 4`) — depende de Cadastro (gera + documentos a partir dos dados capturados) e de Empresa/Companies (dados do representante + legal). + 5. **Fiscal** (`## 5`) — módulo mais isolado, pode entrar em paralelo com o item 4. + 6. **Ponto Eletrônico / Timeclock** (`## 6`) — é essencialmente um sistema à parte dentro do + ERP (RH), sem dependência forte dos módulos comerciais — pode ser adiado ou paralelizado + por um agente/sessão dedicado. + 7. **Backoffice diverso** (Backup, Agenda, Welcome Page, Empresa) (`## 7`) — menor risco, + deixar por último. +3. **Para cada módulo, um agente/sessão dedicado** (o mesmo padrão usado para produzir este + documento): ao chegar num módulo, é razoável abrir uma sessão do Claude focada só naquele + módulo, com a seção correspondente deste arquivo como contexto principal — evita diluir o + contexto de uma única sessão gigante entre 7 domínios diferentes. Um agente/sessão à parte + revisando consistência **entre** módulos (nomes de campo, formatos de data/moeda, papéis) + no final é recomendável, já que cada seção foi originalmente pesquisada por um agente + diferente e pode ter pequenas diferenças de nomenclatura. +4. **Segurança é transversal, não um módulo**: os detalhes de criptografia (bcrypt custo 10, + JWT HS256, HMAC-SHA256 para OTP, SHA-256 hash-chain de auditoria, tokens de 96–256 bits) estão + espalhados nas seções 1 e 4 — o agente responsável pela Fase de Arquitetura deve extrair isso + num documento/checklist próprio de segurança antes de começar a implementar, e cada módulo + subsequente deve ser verificado contra esse checklist. +5. **Não copiar lacunas de segurança conhecidas do legado sem decidir conscientemente**: este + documento aponta explicitamente pontos como ausência de `helmet`/CORS configurado e ausência + de rate limit em algumas rotas administrativas — o Eden deve decidir deliberadamente se + corrige isso (recomendado) ou não, não herdar por omissão. + +--- + +## Índice de módulos + +1. Autenticação, Usuários, Papéis, Permissões e Segurança/Criptografia +2. Produtos, Ofertas (Quotes), Faixas de Preço/Fidelidade e Contratos +3. Cadastro de Cliente e Cadastro/Gestão de Revendas +4. Templates de Documentos, Geração de PDF e Assinatura Eletrônica +5. Módulo Fiscal (NCM, CFOP, Municípios) +6. Módulo de Ponto Eletrônico (Timeclock) +7. Backoffice diverso (Backup, Agenda, Welcome Page, Empresa, Dashboard) + +--- + +# 1. Autenticação, Usuários, Papéis, Permissões e Segurança + + +> Documentação extraída do sistema legado "OrçaFácil" (server/src Express + Postgres, frontend React+Vite) para servir de referência à reconstrução ("Eden"). Escopo: auth, users, roles, role_permissions, companies, criptografia/segurança. + +--- + +## 1. Modelo de dados + +### 1.1 Tabela `users` + +Origem: `server/migrations/1785400000000_baseline-schema.js` (colunas base) + `1786250000000_add-custom-roles.js` (troca de `role` de enum para FK texto). + +```sql +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_date TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_date TIMESTAMPTZ NOT NULL DEFAULT now(), -- atualizado por trigger set_updated_date() + created_by_id UUID REFERENCES users(id), + full_name TEXT, + email TEXT NOT NULL UNIQUE, + password_hash TEXT, -- bcrypt hash; NULL é possível (usuário sem senha definida ainda) + role TEXT NOT NULL DEFAULT 'user' REFERENCES roles(key), -- era ENUM user_role, migrado p/ TEXT+FK + reseller_id UUID REFERENCES resellers(id), + can_approve_discount BOOLEAN NOT NULL DEFAULT false, + reset_token TEXT, + reset_token_expires TIMESTAMPTZ, + token_version INTEGER NOT NULL DEFAULT 0 +); +CREATE INDEX idx_users_reseller ON users(reseller_id); +``` + +Notas de campo: +- `role`: originalmente `ENUM user_role ('admin','user')`, depois `ALTER TYPE ... ADD VALUE` para `'backoffice'` (migration `1785509417538_add-backoffice-role.js`) e `'super_admin'` (migration `1785606000000_add-super-admin-role.js`, com `pgm.noTransaction()` — ver seção "Gotchas"). Migration `1786250000000_add-custom-roles.js` converte a coluna de `user_role` (enum) para `TEXT REFERENCES roles(key)`, permitindo papéis customizados criados dinamicamente. +- `password_hash`: bcrypt via `bcryptjs`, custo (`saltRounds`) **10**, gerado com `bcrypt.hash(senha, 10)`. Comparação com `bcrypt.compare`. +- `token_version`: inteiro incrementado sempre que uma sessão precisa ser invalidada globalmente (logout, troca de senha, reset de senha, reset de senha via admin). O JWT carrega a versão vigente no momento da emissão (`ver`); no middleware de auth, se `users.token_version !== payload.ver`, o token é recusado mesmo sendo criptograficamente válido — é o mecanismo de "logout forçado / invalidação de sessão" já que JWT não tem estado no servidor por si só. +- `reset_token` / `reset_token_expires`: token de texto (hex, 24 bytes → 48 chars) para fluxo de "esqueci minha senha", expira em 1h. +- `can_approve_discount`: flag específica de negócio (fora do sistema de features) — permite ao usuário aprovar/reprovar condições especiais em ofertas (desconto especial, fidelidade reduzida). Setável manualmente por quem edita usuários. +- `reseller_id`: amarra o usuário a uma revenda (tabela `resellers`, fora deste escopo mas relevante — é o "tenant operacional" do sistema, não `companies`). + +### 1.2 Tabela `roles` + +Origem: `1786250000000_add-custom-roles.js` + `1786330000000_add-role-weight.js`. + +```sql +CREATE TABLE roles ( + key TEXT PRIMARY KEY, -- ex.: 'user', 'admin', 'suporte' + label TEXT NOT NULL, -- nome de exibição + is_system BOOLEAN NOT NULL DEFAULT false, + weight INTEGER NOT NULL DEFAULT 10, -- peso hierárquico + created_date TIMESTAMPTZ NOT NULL DEFAULT now(), + created_by_id UUID REFERENCES users(id) +); +``` + +Seed inicial (`is_system = true` para os 4 papéis nativos — não podem ser excluídos; `label` não editável se `is_system`): +| key | label | is_system | weight | +|---|---|---|---| +| `user` | Usuário | true | 10 | +| `backoffice` | Backoffice | true | 20 | +| `admin` | Administrador | true | 80 | +| `super_admin` | Super Administrador | true | 100 | + +Papel customizado de exemplo já existente em produção: `suporte` (weight 20, não é `is_system`). + +Regras de validação da chave (`key`), aplicadas tanto no backend (`roles.routes.js`) quanto espelhadas no frontend: regex `^[a-z][a-z0-9_]{1,29}$` (minúscula, começa com letra, 2–30 caracteres, apenas `a-z0-9_`). + +`weight`: inteiro 0–99 (`MAX_ASSIGNABLE_WEIGHT = 99`) para qualquer papel criado/editado via API — o peso de `super_admin` (100) é o teto fixo do sistema e nunca é atribuível/editável via UI (o próprio papel `super_admin` não pode ser editado nem excluído: `PATCH /api/roles/super_admin` retorna 400). + +### 1.3 Tabela `role_permissions` + +Origem: `1786030000000_add-role-permissions.js` (criada com `role user_role PRIMARY KEY`) + `1786250000000_add-custom-roles.js` (coluna migrada para `TEXT REFERENCES roles(key) ON DELETE CASCADE`). + +```sql +CREATE TABLE role_permissions ( + role TEXT PRIMARY KEY REFERENCES roles(key) ON DELETE CASCADE, + permissions JSONB NOT NULL DEFAULT '{}'::jsonb, -- { feature_key: 'view' | 'edit' } + updated_date TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_by_id UUID REFERENCES users(id) +); +``` + +- Uma linha por papel **exceto `super_admin`** (que nunca entra nesta tabela — acesso total hardcoded). +- Ao criar um papel novo (`POST /api/roles`), uma linha `role_permissions` vazia (`{}`) é inserida na mesma transação — tudo começa como "sem acesso". +- Ao excluir um papel, `ON DELETE CASCADE` remove a linha de `role_permissions` junto. +- `permissions` é um objeto JSON plano `{ "": "view" | "edit" }`. Ausência de uma chave = sem acesso àquela feature. + +### 1.4 Tabela `companies` + +Origem: `1785606000001_add-companies.js`. Representa a(s) empresa(s) **emissora(s)** de contrato (ex.: a própria Handix) — não é multi-tenant de clientes, é cadastro jurídico usado para montar documentos/contratos. + +```sql +CREATE TABLE companies ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_date TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_date TIMESTAMPTZ NOT NULL DEFAULT now(), + created_by_id UUID REFERENCES users(id), + company_name TEXT NOT NULL, + trade_name TEXT, + cnpj TEXT, + state_registration TEXT, + municipal_registration TEXT, + address_zip TEXT, + address_street TEXT, + address_number TEXT, + address_complement TEXT, + address_neighborhood TEXT, + address_city TEXT, + address_state TEXT, + address_country TEXT NOT NULL DEFAULT 'Brasil', + phone TEXT, + email TEXT, + website TEXT, -- (adicionada depois da migration original; ver companies.routes.js) + legal_rep_name TEXT, + legal_rep_cpf TEXT, + legal_rep_role TEXT, + legal_rep_email TEXT, -- (adicionada depois) + is_active BOOLEAN NOT NULL DEFAULT true +); +``` + +Validação: `cnpj` e `legal_rep_cpf` são normalizados com `onlyDigits()` e validados com `isValidCNPJ()` (dígito verificador) antes de gravar — CNPJ inválido é rejeitado com 400. + +### 1.5 Relacionamentos-chave + +``` +roles.key ←── users.role (FK) +roles.key ←── role_permissions.role (FK, ON DELETE CASCADE) +users.id ←── users.created_by_id (auto-referência: quem convidou) +users.id ←── companies.created_by_id +users.id ←── role_permissions.updated_by_id +resellers.id ←── users.reseller_id +``` + +### 1.6 Conceito de "role weight" (peso hierárquico) + +Introduzido em `1786330000000_add-role-weight.js`. Resolve o problema: um usuário com a feature `gestao_usuarios:edit` liberada (mas papel "fraco") não pode promover/editar/excluir alguém com papel "mais forte" que o seu, mesmo tendo a tela liberada. + +Regra aplicada em `server/src/routes/users.routes.js`: + +- **`assertCanAssignRole(req, res, targetRoleKey)`** — chamada em `POST /users/invite` e `PATCH /users/:id` antes de gravar `role`. Se `req.user.role === 'super_admin'`, sempre permite. Senão, busca `weight` do papel do ator e do papel alvo em `roles`; se `targetRole.weight > actorWeight`, bloqueia com 403 `"Você não pode atribuir um papel com peso maior que o seu."`. +- **`assertCanActOnUser(req, res, targetUserId)`** — chamada em `PATCH /users/:id`, `DELETE /users/:id` e `POST /users/:id/reset-password`. Mesma lógica, mas compara o peso do papel **atual** do usuário-alvo (não o papel que se quer atribuir) contra o peso do ator. Bloqueia com 403 `"Você não pode alterar um usuário com papel de peso maior que o seu."`. `super_admin` sempre passa. + +Isso é **ortogonal** ao sistema de features: mesmo com `gestao_usuarios:edit`, um papel de peso baixo não consegue tocar em usuários/atribuir papéis de peso maior. `super_admin` ignora ambas as checagens (peso 100 é hardcoded como teto, nunca editável via UI). + +No frontend (`UsersManager.jsx`), a mesma regra é espelhada apenas para UX (esconder opções que o backend recusaria): `assignableRoles()` filtra o dropdown de papel a `role.weight <= myWeight` (ou tudo, se `currentUserRole === "super_admin"`), e `canActOnUser()` decide se as ações de editar/resetar senha/excluir aparecem para aquele usuário-linha. + +--- + +## 2. Papéis do sistema + +| role key | label | is_system | weight padrão | Descrição | +|---|---|---|---|---| +| `user` | Usuário | true | 10 | Papel padrão de quem cria/edita as próprias ofertas. | +| `backoffice` | Backoffice | true | 20 | Time de apoio operacional — por padrão acesso a ofertas (próprias + de outros, view), clientes, contratos (view), assinaturas. | +| `admin` | Administrador | true | 80 | Acesso amplo — por padrão edita ofertas de todos, clientes, produtos, fiscal, gestão de usuários, vê painel do gestor. | +| `super_admin` | Super Administrador | true | 100 | Bypass total e hardcoded — nunca passa por `role_permissions`, nunca editável via UI (nem seu label, nem seu weight, nem exclusão). | +| `suporte` (exemplo) | Suporte | false | 20 | Papel customizado já criado em produção via a própria tela de Papéis. | + +### 2.1 super_admin — como difere estruturalmente + +`super_admin` **não é apenas "mais um papel com todas as permissões marcadas"** — é um caso hardcoded no código, em múltiplos pontos: + +1. `hasFeatureAccess(user, key, minLevel)` (`server/src/lib/roles.js`): primeira linha é `if (user.role === 'super_admin') return true;` — nunca consulta `role_permissions`. +2. `requireFeatureOrSuperAdmin(key, minLevel)` (middleware): `if (req.user.role === 'super_admin' || hasFeatureAccess(...)) return next();` — redundante com o item 1 mas explícito. +3. `requireSuperAdmin` (middleware dedicado): usado para as telas exclusivas — cadastro da empresa emissora (escrita), CRUD de papéis (exceto listagem), CRUD de Permissões por Papel (toda a rota `role_permissions.routes.js`). +4. `role_permissions` nunca tem linha para `super_admin` — a tabela é `PRIMARY KEY (role) REFERENCES roles(key)`, mas a rota de permissões filtra explicitamente `WHERE key != 'super_admin'` ao listar papéis "editáveis". +5. `assertCanAssignRole` / `assertCanActOnUser`: `super_admin` sempre retorna `true` de cara, sem consultar pesos. +6. Regra de negócio "oferta travada": depois que o cadastro de cliente é iniciado numa oferta (`quotes.client_registration_id` setado), **ninguém** pode mais editar a oferta — **exceto `super_admin`** (`server/src/routes/quotes.routes.js`, rota `PATCH /api/quotes/:id`). Comentário no código: permite reverter um "fechado" que caiu ou corrigir item lançado errado antes do cadastro do cliente terminar. +7. Papel `super_admin` é promovido apenas por migration/seed direto no banco (`1785606100000_promote-matheus-super-admin.js` — promove `matheus@handix.com.br` via `UPDATE users SET role = 'super_admin' WHERE LOWER(email) = ...`), nunca pela API de usuários — não existe UI para criar um segundo super_admin, teria que ser via banco/migration. + +### 2.2 admin vs super_admin + +`isAdminRole(role)` = `role IN ('admin', 'super_admin')` — usado para checagens legadas mais grosseiras (antes do sistema de features por papel existir). `admin` **não** tem bypass automático; seu acesso é 100% dirigido pela linha correspondente em `role_permissions` (que, por padrão de seed, replica o que ele tinha "hardcoded" antes da feature existir — ver seção 3.4). Ou seja: hoje em dia `super_admin` só concede algumas capacidades genuinamente exclusivas (cadastro de empresa emissora, editar oferta travada, CRUD de papéis/permissões); todo o resto que `admin` "sempre teve" é, na prática, `role_permissions` do papel `admin` pré-configurado com tudo em `edit`. + +--- + +## 3. Sistema de permissões por feature (`features.js`) + +Arquivo: `server/src/lib/features.js`. É o registro único de "telas gateáveis" — cada entrada vira uma linha na matriz de permissão (Sem acesso / Visualizar / Editar) que o `super_admin` configura por papel em Gestão > Usuários > Permissões por Papel. + +### 3.1 Formato + +```js +export const FEATURES = [ + { key: 'ofertas', label: 'Ofertas (próprias)', group: 'Ofertas' }, + { key: 'ofertas_others', label: 'Ofertas de Outros Usuários', group: 'Ofertas' }, + { key: 'clientes_revenda', label: 'Cliente da Revenda', group: 'Clientes' }, + { key: 'clientes_todos', label: 'Todos os Clientes', group: 'Clientes' }, + { key: 'contratos', label: 'Todos os Contratos', group: 'Contratos' }, + { key: 'contratos_relatorios', label: 'Relatórios', group: 'Contratos' }, + { key: 'assinaturas', label: 'Assinaturas', group: 'Contratos' }, + { key: 'contratos_cessao', label: 'Cessão', group: 'Contratos' }, + { key: 'produtos', label: 'Produtos', group: 'Gestão' }, + { key: 'fiscal', label: 'Fiscal', group: 'Gestão' }, + { key: 'gestao_usuarios', label: 'Gestão de Usuários', group: 'Gestão' }, + { key: 'painel_gestor', label: 'Painel do Gestor', group: 'Gestão' }, + { key: 'empresa', label: 'Empresa', group: 'Gestão' }, + { key: 'backup', label: 'Backup', group: 'Gestão' }, + { key: 'documentos_modelos', label: 'Modelos de Documentos', group: 'Gestão' }, + { key: 'pagina_inicial', label: 'Página Inicial (Boas-vindas)', group: 'Gestão' }, + { key: 'rh_equipamentos', label: 'Equipamentos (Controle de Ponto)', group: 'RH' }, + { key: 'rh_funcionarios', label: 'Colaboradores (Controle de Ponto)', group: 'RH' }, + { key: 'rh_afd_importacoes', label: 'Importações AFD (Controle de Ponto)', group: 'RH' }, + { key: 'rh_afd_auditoria', label: 'Auditoria NSR/AFD (Controle de Ponto)', group: 'RH' }, + { key: 'rh_jornadas', label: 'Jornadas e Feriados (Controle de Ponto)', group: 'RH' }, + { key: 'rh_ajustes', label: 'Ajustes de Ponto (Controle de Ponto)', group: 'RH' }, + { key: 'rh_ajustes_aprovacao', label: 'Aprovação de Ajustes de Ponto (Controle de Ponto)', group: 'RH' }, + { key: 'rh_apuracao', label: 'Apuração e Banco de Horas (Controle de Ponto)', group: 'RH' }, + { key: 'rh_dashboard', label: 'Dashboard (Controle de Ponto)', group: 'RH' }, + { key: 'rh_relatorios', label: 'Relatórios (Controle de Ponto)', group: 'RH' }, + { key: 'rh_fechamento', label: 'Fechamento de Período (Controle de Ponto)', group: 'RH' }, + { key: 'rh_fechamento_reabertura', label: 'Reabertura de Período Fechado (Controle de Ponto)', group: 'RH' }, + { key: 'revendas_cadastro', label: 'Cadastro de Revendas', group: 'Revendas' }, + { key: 'agenda', label: 'Agenda (Salas de Reunião)', group: 'Agenda' }, + { key: 'agenda_salas', label: 'Agenda — Cadastro de Salas', group: 'Agenda' }, + { key: 'agenda_carros', label: 'Agenda (Carros da Empresa)', group: 'Agenda' }, + { key: 'agenda_carros_cadastro', label: 'Agenda — Cadastro de Carros', group: 'Agenda' }, +]; +export const FEATURE_KEYS = FEATURES.map((f) => f.key); +``` + +Cada feature tem só dois níveis (`view` e `edit`) além da ausência (= sem acesso). Não há um terceiro nível "delete" separado — deletar é coberto por `edit`. + +### 3.2 Regra especial de "ofertas" — duas entradas para próprio vs. de outros + +`ofertas` controla acesso às **próprias** ofertas do usuário (todo papel edita as próprias por padrão — não depende de `role_permissions`, é incondicional no código de `quotes.routes.js` via `own && hasFeatureAccess(user,'ofertas','edit')`). `ofertas_others` controla acesso às ofertas **de outros usuários**: `view` = vê a lista completa, `edit` = também edita as de outros. É o mecanismo genérico de "próprio vs. todos", reaproveitado sem caso especial hardcoded em código (ver `canViewAllQuotes`/`canEditAllQuotes` em `roles.js`). + +### 3.3 Como uma rota decide acesso + +Em `server/src/middleware/auth.js`: + +```js +export function requireFeatureOrSuperAdmin(key, minLevel = 'view') { + return (req, res, next) => { + if (req.user.role === 'super_admin' || hasFeatureAccess(req.user, key, minLevel)) return next(); + return res.status(403).json({ error: 'Forbidden' }); + }; +} +``` + +`hasFeatureAccess` (em `server/src/lib/roles.js`): + +```js +const FEATURE_LEVEL_RANK = { view: 1, edit: 2 }; +export function hasFeatureAccess(user, key, minLevel = 'view') { + if (!user) return false; + if (user.role === 'super_admin') return true; + const level = user.role_permissions?.[key]; + if (!level) return false; + return (FEATURE_LEVEL_RANK[level] || 0) >= (FEATURE_LEVEL_RANK[minLevel] || 0); +} +``` + +`user.role_permissions` chega já resolvido: o middleware `auth` faz `LEFT JOIN role_permissions rp ON rp.role = u.role` na query de carregamento do usuário autenticado, evitando query extra em cada rota: + +```sql +SELECT u.*, rp.permissions AS role_permissions +FROM users u +LEFT JOIN role_permissions rp ON rp.role = u.role +WHERE u.id = $1 +``` + +Padrão de uso em rotas (exemplo `users.routes.js`): +```js +const view = requireFeatureOrSuperAdmin('gestao_usuarios', 'view'); +const edit = requireFeatureOrSuperAdmin('gestao_usuarios', 'edit'); +router.get('/', auth, view, ...); +router.patch('/:id', auth, edit, ...); +``` + +Frontend espelha exatamente a mesma função em `src/lib/roles.js` (`hasFeatureAccess`, `isAdminRole`, `isAdminOrBackofficeRole`, `canViewAllQuotes`, `canEditAllQuotes`) — mesmas assinaturas, para gate de página/UI (esconder botões de edição, redirecionar se `view` não concedido). Guard típico de página (`Management.jsx`): +```js +if (role !== "super_admin" && !hasFeatureAccess(user, "gestao_usuarios", "view")) { /* bloqueia página */ } +const canEdit = role === "super_admin" || hasFeatureAccess(user, "gestao_usuarios", "edit"); +``` +Importante: o gate de frontend é só UX — a fonte da verdade é sempre o backend (toda rota de escrita/leitura sensível tem seu próprio `requireFeatureOrSuperAdmin`). + +### 3.4 Como papéis customizados herdam/definem permissões + +Não há herança implícita nenhuma. Um papel novo (`POST /api/roles`) nasce com `role_permissions` vazio (`{}` = nenhuma feature liberada) — o `super_admin` precisa entrar em Permissões por Papel e conceder manualmente `view`/`edit` por feature. Não existe conceito de "papel pai" ou herança em cadeia — cada papel (exceto `super_admin`) é uma linha 100% independente em `role_permissions`. + +A migration `1786040000000_seed-role-permissions-defaults.js` documenta o valor inicial que replicava exatamente o comportamento anterior (quando o acesso era hardcoded por `role` no código) — isso foi feito uma única vez, no deploy que introduziu o sistema de features, para não quebrar o acesso de ninguém: + +```json +// user +{ "ofertas": "edit", "clientes_revenda": "view" } + +// backoffice +{ + "ofertas": "edit", "ofertas_others": "view", + "clientes_revenda": "edit", "clientes_todos": "edit", + "contratos": "view", "contratos_relatorios": "view", + "assinaturas": "edit" +} + +// admin +{ + "ofertas": "edit", "ofertas_others": "edit", + "clientes_revenda": "edit", "clientes_todos": "edit", + "contratos": "view", "contratos_relatorios": "view", + "assinaturas": "edit", "produtos": "edit", "fiscal": "edit", + "gestao_usuarios": "edit", "painel_gestor": "view" +} +``` + +Como gatear uma tela nova por feature (processo documentado em comentário no próprio `features.js`, útil para replicar em Eden): +1. Adicionar `{ key, label, group }` em `FEATURES`. +2. Rota de leitura: middleware `requireFeatureOrSuperAdmin(key, 'view')`. +3. Rota de escrita: `requireFeatureOrSuperAdmin(key, 'edit')`. +4. Guard de página no front: `role !== "super_admin" && !hasFeatureAccess(user, key, "view")`; `canEdit = role === "super_admin" || hasFeatureAccess(user, key, "edit")`. +5. Migration de seed com o valor default para o papel que já tinha a tela — senão quem já usava perde acesso no deploy. + +--- + +## 4. Autenticação + +### 4.1 Login + +`POST /api/auth/login` (rota pública, com rate limit — ver seção 5.4) + +Request: +```json +{ "email": "user@empresa.com", "password": "senha" } +``` + +Fluxo (`server/src/routes/auth.routes.js`): +1. Valida presença de `email`/`password` (400 se faltando). +2. Busca usuário por `email.toLowerCase()` — e-mails são case-insensitive na prática (armazenados como veio, comparados em lowercase). +3. Se não existir usuário ou `password_hash` for nulo → 401 `"Credenciais inválidas"` (mensagem genérica, não revela qual dos dois falhou — nem se a conta existe). +4. `bcrypt.compare(password, user.password_hash)` — se falso, mesmo 401 genérico. +5. Sucesso: `signToken(user.id, user.token_version)` + `serializeUser(user)`. + +Response: +```json +{ "access_token": "", "user": { "id": "...", "email": "...", "role": "...", "permissions": {...}, ... } } +``` + +### 4.2 JWT — geração e validação + +Arquivo `server/src/lib/jwt.js`: + +```js +import jwt from 'jsonwebtoken'; +const SECRET = process.env.JWT_SECRET; +if (!SECRET) throw new Error('JWT_SECRET env var is required'); // falha no boot se não setado + +export const signToken = (userId, tokenVersion) => + jwt.sign({ sub: userId, ver: tokenVersion }, SECRET, { expiresIn: '30d' }); + +export const verifyToken = (token) => jwt.verify(token, SECRET); +``` + +- **Algoritmo**: padrão da lib `jsonwebtoken` quando só uma string é passada como secret → **HS256** (HMAC-SHA256, simétrico). Não há RS256/par de chaves. +- **Payload**: apenas `{ sub: userId, ver: tokenVersion }` + claims padrão (`iat`, `exp`) — deliberadamente mínimo (não carrega role/permissions no token, essas são resolvidas a cada request via JOIN no banco, então mudanças de papel/permissão têm efeito imediato sem precisar reemitir token). +- **Expiração**: 30 dias (`expiresIn: '30d'`), fixo, sem refresh token — o token só é renovado fazendo login de novo. Não há endpoint de refresh. +- **Segredo**: variável de ambiente `JWT_SECRET`, obrigatória (processo derruba na subida se ausente). Sem valor default/hardcoded no código. Documentado em `.env.example`: gerar uma vez por ambiente com `openssl rand -hex 48`, nunca trocar depois (trocar invalida todas as sessões ativas). +- **Verificação** (middleware `auth`, `server/src/middleware/auth.js`): + 1. Extrai `Authorization: Bearer ` do header. + 2. `verifyToken(token)` — lança se assinatura/expiração inválidas → 401. + 3. Busca usuário por `payload.sub`, com o LEFT JOIN de `role_permissions` (ver seção 3.3). + 4. Se usuário não existe → 401. + 5. **Checagem de invalidação de sessão**: `if (rows[0].token_version !== payload.ver) return 401 "Sessão encerrada, faça login novamente"`. + 6. Popula `req.user` (linha completa de `users` + `role_permissions` resolvido) e `req.tokenVersion`. + +Não há refresh token, nem rotação de token, nem blacklist explícita — a invalidação é 100% via `token_version` incremental. + +### 4.3 `GET /api/auth/me` + +Requer `auth`. Retorna `serializeUser(req.user)` — usado pelo frontend (`AuthContext`) para revalidar sessão a cada carregamento de app (se não há token no localStorage, nem tenta; se há, chama `/auth/me` e, em caso de erro, limpa o token local). + +### 4.4 Logout + +`POST /api/auth/logout` (requer `auth`). Incrementa `token_version` do usuário — invalida **o token atual e qualquer outro já emitido** (não existe "logout de uma sessão específica", é sempre logout de todas as sessões simultaneamente). Comentário no código: "não basta só apagar o token no cliente". Frontend também limpa o token do `localStorage` independente do resultado da chamada (best-effort). + +### 4.5 Esqueci minha senha / reset + +**Passo 1** — `POST /api/auth/forgot-password` (rate limited, pública): +```json +{ "email": "user@empresa.com" } +``` +- Sempre responde `{ "ok": true }`, **mesmo se o e-mail não existir** (evita enumeração de contas — confirmado também no frontend: `ForgotPassword.jsx` sempre mostra a mesma mensagem de sucesso, inclusive em erro de rede/validação). +- Se o e-mail existir: gera `token = crypto.randomBytes(24).toString('hex')` (48 caracteres hex, 192 bits de entropia), `expires = now + 1h`, grava em `users.reset_token`/`reset_token_expires`, envia e-mail com link `${PUBLIC_BASE_URL}/reset-password?token=`. + +**Passo 2** — `POST /api/auth/reset-password` (pública, sem rate limit dedicado nesta rota especificamente — a proteção de força bruta acontece na etapa de solicitação, não na de confirmação; o token de 192 bits já é impraticável de adivinhar): +```json +{ "resetToken": "", "newPassword": "novaSenha123" } +``` +- Valida `newPassword.length >= 6` (única regra de complexidade de senha em todo o sistema — não exige maiúscula/número/símbolo). +- Busca usuário por `reset_token = $1 AND reset_token_expires > now()` — token errado ou expirado → 400 `"Link de redefinição inválido ou expirado"`. +- Grava novo `password_hash` (bcrypt custo 10), limpa `reset_token`/`reset_token_expires`, **incrementa `token_version`** (derruba qualquer sessão ativa daquele usuário). + +### 4.6 Troca de senha autenticado + +`POST /api/users/me/change-password` (requer `auth`, qualquer usuário sobre si mesmo): +```json +{ "current_password": "atual", "new_password": "nova123" } +``` +Exige senha atual mesmo já autenticado (evita que uma sessão esquecida aberta troque a senha sem confirmação); mesma regra de tamanho mínimo (6); incrementa `token_version` ao final (exige novo login). + +### 4.7 Reset de senha administrativo (por outro usuário) + +`POST /api/users/:id/reset-password` (requer `auth` + feature `gestao_usuarios:edit`, sujeito a `assertCanActOnUser`). Gera senha temporária aleatória (`crypto.randomBytes(9).toString('base64url')`, ~12 caracteres), grava hash bcrypt, **incrementa `token_version`** do alvo, e devolve a senha em texto plano na resposta (`temp_password`) — não envia e-mail (diferente do convite), o admin repassa manualmente (WhatsApp, telefone). Frontend mostra num modal com botão de copiar. + +### 4.8 Convite / cadastro de usuário + +`POST /api/users/invite` (requer `auth` + `gestao_usuarios:edit`, sujeito a `assertCanAssignRole`): +```json +{ "email": "novo@empresa.com", "role": "user" } +``` +- Rejeita se e-mail já cadastrado (409). +- Gera senha temporária aleatória (`crypto.randomBytes(9).toString('base64url')`), hash bcrypt custo 10. +- Cria o usuário com `full_name` derivado da parte antes do `@` do e-mail (placeholder), `role` (default `'user'` se omitido via `COALESCE`), `created_by_id` = ator. +- Envia e-mail com a senha temporária em texto plano (link de login + credenciais no corpo do e-mail). +- Retorna também `temp_password` na resposta da API (fallback caso o e-mail não chegue). + +### 4.9 Rate limiting em rotas sensíveis + +Arquivo `server/src/middleware/rateLimit.js`, usando `express-rate-limit`. Handler padrão de estouro: `429 { "error": "Muitas tentativas. Aguarde alguns minutos e tente novamente." }`. + +| Limiter | Janela | Máximo | Chave | +|---|---|---|---| +| `loginIpLimiter` | 15 min | 20 | IP (padrão da lib) | +| `loginUserLimiter` | 15 min | 5 | e-mail normalizado do body (`byEmailKey`) | +| `forgotPasswordIpLimiter` | 15 min | 10 | IP | +| `forgotPasswordUserLimiter` | 15 min | 3 | e-mail normalizado do body | +| `publicClientRegistrationLimiter` | 15 min | 30 | IP | +| `publicResellerRegistrationLimiter` | 15 min | 30 | IP | +| `publicSignatureLimiter` | 15 min | 60 | IP | +| `otpRequestLimiter` | 15 min | 5 | token do signatário (`req.params.token`) | +| `otpVerifyLimiter` | 15 min | 15 | token do signatário | + +`POST /api/auth/login` usa **dois** limiters empilhados (`loginIpLimiter, loginUserLimiter`) — protege tanto contra um IP tentando várias contas (password spraying) quanto contra força bruta numa conta específica vinda de IPs diferentes. Mesmo padrão duplo em `forgot-password`. + +`app.set('trust proxy', 1)` em `server.js` — o servidor roda atrás de um único proxy reverso (nginx externo) e confia no `X-Forwarded-For` dele para que `req.ip` reflita o IP real do cliente (necessário para os limiters por IP funcionarem corretamente). + +**Observação de segurança**: `POST /api/auth/reset-password` (confirmação do reset) e `POST /api/users/:id/reset-password` (reset administrativo) e `POST /api/users/:id/reset-password` não têm rate limiter dedicado — mitigado pela alta entropia do token (192 bits) na primeira, e pela exigência de estar autenticado + ter a feature de gestão de usuários na segunda. + +--- + +## 5. Criptografia e segurança + +### 5.1 Hash de senha + +- Biblioteca: `bcryptjs` (`^2.4.3` — implementação JS pura do bcrypt, não a binding nativa `bcrypt`). +- Custo (**salt rounds**): **10**, hardcoded em todos os pontos de hash (`bcrypt.hash(senha, 10)`) — login/invite/reset/change-password/seed do admin inicial, sem variação. +- Comparação sempre via `bcrypt.compare`, nunca comparação manual de hash. +- Nenhum "pepper" adicional (segredo extra além do salt do bcrypt) é usado. + +### 5.2 JWT + +- Algoritmo: **HS256** (assinatura simétrica HMAC), implícito pela lib `jsonwebtoken` quando o secret é uma string simples. +- Segredo: env var `JWT_SECRET`, sem valor default nem fallback — a aplicação recusa subir sem ela (`throw new Error` no import do módulo). Gerada manualmente por ambiente (`openssl rand -hex 48` sugerido no `.env.example`), nunca versionada em git (`.env` está no `.gitignore`). +- Payload mínimo (`sub`, `ver`), expiração 30 dias, sem refresh token. +- Invalidação server-side via `users.token_version` (não há JWT blacklist nem revogação por jti). + +### 5.3 Outras criptografias em repouso no sistema (fora do escopo direto de auth, mas relevante como padrão a replicar) + +- `CONTROLID_ENCRYPTION_KEY` (env var, gerar com `openssl rand -hex 32`): usada para criptografar **de forma reversível** a senha admin de cada equipamento de ponto (Control iD) cadastrado — porque a aplicação precisa recuperar a senha em texto puro para autenticar no relógio de ponto (não pode ser hash unidirecional como senha de usuário). Trocar essa chave torna ilegíveis as senhas de equipamento já cadastradas. Este é o único caso de criptografia reversível de segredo identificado no sistema (fora do escopo de usuários/papéis, mas é o padrão de "quando não pode ser hash" usado no projeto). +- Senhas de usuário (`users.password_hash`) e nada mais relacionado a auth usa criptografia reversível — é sempre hash unidirecional (bcrypt). + +### 5.4 Sanitização de inputs + +- Não há biblioteca de sanitização genérica (tipo `express-validator`/`joi`/`zod`) usada nas rotas de auth/usuários/papéis — validação é manual, por rota, checando presença/formato/tamanho no próprio handler (ex.: regex de `key` de papel, `newPassword.length >= 6`, `isValidCNPJ`). +- Todas as queries usam **parâmetros posicionais do `pg`** (`$1, $2, ...`) — não há concatenação de string SQL com input do usuário nas rotas revisadas (proteção padrão contra SQL injection via driver). +- E-mails são normalizados com `.toLowerCase()` e `.trim()` antes de comparar/gravar. +- CNPJ/CPF normalizados via `onlyDigits()` e validados por dígito verificador (`isValidCNPJ`) antes de persistir. + +### 5.5 CORS e headers de segurança + +- **Não há middleware de CORS configurado** (`server.js` não importa nem usa o pacote `cors`; confirmado ausente em `server/package.json`) — a API assume que frontend e backend são servidos pela mesma origem (o Express serve os arquivos estáticos do build do Vite diretamente: `app.use(express.static(staticDir))` + fallback SPA `app.get('*', ...)`). Não há, portanto, política de CORS explícita para chamadas cross-origin — se algum client externo tentar chamar a API de outra origem, o comportamento é o padrão do browser (bloqueado, já que não há headers `Access-Control-Allow-Origin`). +- **Não há `helmet` nem outro middleware de security headers** (ausente do `package.json`) — sem CSP, sem `X-Frame-Options`, `X-Content-Type-Options`, `Strict-Transport-Security`, etc. configurados explicitamente no Express. Isso é uma lacuna a considerar/decidir conscientemente ao reconstruir em Eden (adicionar `helmet` seria uma melhoria direta). +- `express.json({ limit: '5mb' })` é o único middleware global de parsing/limite de payload. +- Erros não tratados caem num handler global (`app.use((err, req, res, next) => ...)`) que loga no console e responde `500 { error: 'Erro interno do servidor' }` — evita vazar stack trace para o cliente (exceto casos especiais de `MulterError` para upload). + +### 5.6 O que está em `.env` vs. hardcoded + +Variáveis de ambiente relevantes a este escopo (`.env.example`): +- `DATABASE_URL` — connection string do Postgres. +- `JWT_SECRET` — obrigatória, sem default. +- `PORT` — porta do servidor (8006 dev / 8007 prod, por convenção, não travado em código). +- `SEED_ADMIN_EMAIL` / `SEED_ADMIN_PASSWORD` — usados só no primeiro boot com banco vazio (`seedAdmin()` em `server/src/lib/migrate.js`) para criar o primeiro `admin`. Se `SEED_ADMIN_PASSWORD` não definida, gera senha aleatória (`crypto.randomBytes(9).toString('base64url')`) e imprime nos **logs do container**. +- `SMTP_HOST/PORT/USER/PASS/FROM` — envio de e-mail (reset de senha, convite, cadastro de cliente). Se `SMTP_HOST` ausente, e-mails não são enviados de verdade — apenas logados no console (`[mailer] SMTP não configurado — e-mail para X não enviado`), útil para dev sem quebrar o fluxo. +- `PUBLIC_BASE_URL` — usada para montar links absolutos em e-mails (reset de senha, etc.); se ausente, o sistema tenta inferir a partir do host da requisição (`baseUrlFromReq`). +- `CONTROLID_ENCRYPTION_KEY` — fora do escopo direto, mas é o outro segredo simétrico do sistema (ver 5.3). + +Hardcoded no código (não configurável por env): +- Custo do bcrypt = 10. +- Expiração do JWT = 30 dias. +- Regras de rate limit (janelas/máximos da tabela da seção 4.9). +- Regex de chave de papel, teto de peso (99), tamanho mínimo de senha (6). +- E-mail do super_admin permanente inicial (`matheus@handix.com.br`), fixado na migration de promoção — não é uma env var, é uma migration versionada. + +### 5.7 Seed do usuário administrador inicial + +`server/src/lib/migrate.js`, função `seedAdmin()`, chamada no boot do servidor (`start()` em `server.js`) **antes** de abrir a porta. Só executa se `users` estiver vazia (`SELECT count(*) FROM users`). Cria uma revenda "Handix" padrão e um usuário `role = 'admin'` (não `super_admin`) com `can_approve_discount = true`. O primeiro `super_admin` real do sistema é sempre promovido via migration de dados fixa (email hardcoded), não pelo seed automático. + +--- + +## 6. Multi-empresa / `companies` + +Importante: **não é multi-tenant de clientes/dados isolados**. `companies` representa **empresa(s) emissora(s)** de documentos/contratos (ex.: a própria Handix e eventuais outras razões sociais sob as quais contratos são emitidos) — usada para montar campos de contratos/termos legais (CNPJ, endereço, representante legal), não para segmentar/particionar os dados de usuários, ofertas, clientes, etc. + +- **Leitura** (`GET /api/companies`, `GET /api/companies/:id`): aberta a qualquer usuário autenticado (`router.use(auth)` sem feature-gate na leitura) — dados considerados não sensíveis, aparecem em qualquer nota fiscal/contrato e são necessários para montar documentos (ex.: Termo de Portabilidade) a partir da tela de oferta, acessível a qualquer dono/backoffice. +- **Escrita** (`POST`/`PATCH`/`DELETE /api/companies`): gateada pela feature `empresa` (`requireFeatureOrSuperAdmin('empresa', 'edit')`) — não é `requireSuperAdmin` puro, é feature configurável (mas por padrão de seed só `admin`/`super_admin` tinham acesso à tela "Empresa"). +- Não há coluna `company_id` em `users`, `quotes`, `products`, etc. — ou seja, o isolamento real de "quem vê o quê" no sistema é feito por **`reseller_id`** (revenda) e pelo sistema de features/roles, não por `companies`. A tabela `resellers` (fora do escopo pedido, mas citada aqui por clareza) é o conceito mais próximo de "tenant operacional": cada oferta (`quotes.reseller_id`) e usuário (`users.reseller_id`) pertence a uma revenda, e regras de feature como `clientes_revenda` (cliente da própria revenda) vs. `clientes_todos` decidem o alcance de visualização. + +--- + +## 7. Fluxos e regras de negócio não óbvias (síntese para Eden) + +1. **Peso de papel > feature de gestão de usuários**: ter `gestao_usuarios:edit` não é suficiente para mexer em qualquer usuário — só em usuários/papéis de peso `<=` o peso do próprio papel do ator. `super_admin` (peso 100, hardcoded) sempre ignora essa checagem. +2. **`super_admin` nunca aparece em `role_permissions`** e nunca é listado como editável nas telas de Papéis/Permissões (`WHERE key != 'super_admin'` em toda consulta relevante). Não pode ser editado, seu `label`/`weight` são imutáveis via API, e não pode ser excluído. +3. **Oferta travada só é editável por `super_admin`**: assim que `quotes.client_registration_id` é setado (cadastro de cliente iniciado), a rota `PATCH /api/quotes/:id` bloqueia qualquer edição para todo mundo, exceto `role === 'super_admin'` — regra de negócio adicionada especificamente para permitir corrigir erros/reverter fechamentos indevidos sem reabrir o processo todo. +4. **`token_version` é o mecanismo universal de "encerrar sessão"** — usado em: logout explícito, troca de senha pelo próprio usuário, reset de senha via link de e-mail, e reset de senha administrativo. Sempre que a senha muda ou logout é pedido, todas as sessões (todos os tokens já emitidos daquele usuário) são invalidadas de uma vez — não há como derrubar "só uma sessão". +5. **Criar um papel novo não copia nada de outro papel** — nasce com `role_permissions = {}` (zero acesso); é responsabilidade do `super_admin` configurar cada feature manualmente depois. +6. **Excluir um papel exige zero usuários com aquele papel** — `DELETE /api/roles/:key` verifica `COUNT(*) FROM users WHERE role = key` e recusa com 409 se houver algum, listando a contagem na mensagem de erro. Papéis `is_system = true` nunca podem ser excluídos, independentemente de terem usuários. +7. **`ofertas` vs. `ofertas_others` é o padrão geral para "próprio vs. todos"** — reaproveitado sem caso especial no código; ao adicionar uma feature nova em Eden que precise dessa distinção, seguir o mesmo padrão de duas entradas em vez de lógica condicional hardcoded. +8. **Convite de usuário gera senha temporária e a envia por e-mail E devolve na resposta da API** — dupla via de entrega (o e-mail pode não chegar; a resposta da API é o fallback mostrado num modal com botão de copiar no frontend). +9. **`forgot-password` sempre responde sucesso** independentemente de o e-mail existir — evita enumeração de contas por e-mail. `reset-password` (confirmação) é a única rota que de fato revela se o token é válido, mas o token em si tem entropia alta o bastante para não ser adivinhável. +10. **E-mail é a chave natural de usuário** (`UNIQUE`, comparado sempre em lowercase) — não existe "username" separado. +11. **Migration gotcha documentado no próprio código** (relevante para quem for portar o schema): adicionar um valor a um enum Postgres (`ALTER TYPE ... ADD VALUE`) e usá-lo na mesma transação/lote de migrations falha em produção ("unsafe use of new value") — a migration que introduziu `super_admin` precisou de `pgm.noTransaction()` para isolar o commit do novo valor antes de outra migration do mesmo lote tentar usá-lo. Em Eden, se o modelo de papéis for uma tabela (`roles`) desde o início (como o sistema evoluiu para em `1786250000000_add-custom-roles.js`), esse problema simplesmente não existe — recomenda-se começar direto com `roles` como tabela, não enum. + +--- + +## 8. Referência rápida de endpoints (escopo deste documento) + +| Método | Rota | Auth | Gate adicional | +|---|---|---|---| +| POST | `/api/auth/login` | não | rate limit (IP + e-mail) | +| GET | `/api/auth/me` | sim | — | +| POST | `/api/auth/logout` | sim | — | +| POST | `/api/auth/forgot-password` | não | rate limit (IP + e-mail) | +| POST | `/api/auth/reset-password` | não | token de 192 bits | +| GET | `/api/users` | sim | `gestao_usuarios:view` | +| POST | `/api/users/invite` | sim | `gestao_usuarios:edit` + peso | +| PATCH | `/api/users/me` | sim | — (autoatualização) | +| POST | `/api/users/me/change-password` | sim | exige senha atual | +| POST | `/api/users/:id/reset-password` | sim | `gestao_usuarios:edit` + peso | +| PATCH | `/api/users/:id` | sim | `gestao_usuarios:edit` + peso (atribuição e ação) | +| DELETE | `/api/users/:id` | sim | `gestao_usuarios:edit` + peso | +| GET | `/api/roles` | sim | `gestao_usuarios:view` | +| POST/PATCH/DELETE | `/api/roles[/:key]` | sim | `requireSuperAdmin` | +| GET/PATCH | `/api/role-permissions[...]` | sim | `requireSuperAdmin` (toda a rota) | +| GET | `/api/companies[/:id]` | sim | — (aberto a qualquer autenticado) | +| POST/PATCH/DELETE | `/api/companies[/:id]` | sim | `empresa:edit` | + +--- + +## 9. Arquivos-fonte lidos (para rastreabilidade) + +- `server/src/middleware/auth.js` +- `server/src/lib/jwt.js` +- `server/src/lib/roles.js` +- `server/src/lib/features.js` +- `server/src/lib/serialize.js` +- `server/src/lib/migrate.js` +- `server/src/lib/mailer.js` +- `server/src/routes/auth.routes.js` +- `server/src/routes/users.routes.js` +- `server/src/routes/roles.routes.js` +- `server/src/routes/rolePermissions.routes.js` +- `server/src/routes/companies.routes.js` +- `server/src/routes/quotes.routes.js` (trecho da trava de oferta) +- `server/src/middleware/rateLimit.js` +- `server/src/server.js` +- `server/package.json` +- `.env.example` +- `server/migrations/1785400000000_baseline-schema.js` +- `server/migrations/1785509417538_add-backoffice-role.js` +- `server/migrations/1785606000000_add-super-admin-role.js` +- `server/migrations/1785606100000_promote-matheus-super-admin.js` +- `server/migrations/1785606000001_add-companies.js` +- `server/migrations/1786030000000_add-role-permissions.js` +- `server/migrations/1786040000000_seed-role-permissions-defaults.js` +- `server/migrations/1786250000000_add-custom-roles.js` +- `server/migrations/1786330000000_add-role-weight.js` +- `src/pages/Management.jsx` +- `src/pages/Login.jsx` +- `src/pages/ForgotPassword.jsx` +- `src/pages/ResetPassword.jsx` +- `src/components/management/UsersManager.jsx` +- `src/components/management/RolePermissionsManager.jsx` +- `src/api/base44Client.js` +- `src/lib/roles.js` +- `src/lib/AuthContext.jsx` +- `src/components/ProtectedRoute.jsx` +- `src/lib/authReturnTo.js` + + +--- + + +# 2. Produtos, Ofertas (Quotes), Faixas de Preço/Fidelidade e Contratos + + +> Documentação de referência para reconstrução fiel no sistema "Eden". +> Escopo: `products`, `quotes` e o conceito de "contrato" (que **não é uma +> tabela própria** — é derivado de `client_registrations` + `quotes`). +> Fiscal (`product_fiscal_profiles`, `fiscal_rules`, etc.) é coberto por +> outro agente; aqui só é citado onde interage com o core comercial. + +--- + +## 1. Modelo de dados + +### 1.1 Tabela `products` + +Fonte: `server/migrations/1785400000000_baseline-schema.js` + +`1785700000001_add-product-fiscal-config.js` (não relevante aqui) + +`1785980000000_add-product-ixc-code.js` + `1785990000000_add-product-code.js` ++ `1786020000000_add-product-discontinued.js`. + +| Coluna | Tipo | Default/Regras | Observações | +|---|---|---|---| +| `id` | UUID PK | `gen_random_uuid()` | | +| `created_date` / `updated_date` | TIMESTAMPTZ | `now()` / trigger `set_updated_date()` | | +| `created_by_id` | UUID FK `users(id)` | | | +| `name` | TEXT NOT NULL | | | +| `description` | TEXT | | | +| `category` | TEXT | | livre, texto | +| `price_0` | NUMERIC(12,2) | | preço "sem fidelidade" (faixa 0 meses) | +| `price_12` | NUMERIC(12,2) | | preço na faixa de 12 meses | +| `price_24` | NUMERIC(12,2) | | preço na faixa de 24 meses | +| `price_36` | NUMERIC(12,2) | | preço na faixa de 36 meses | +| `price_48` | NUMERIC(12,2) | | preço na faixa de 48 meses | +| `impl_unit_price` | NUMERIC(12,2) NOT NULL | default `0` | valor de implantação **por unidade**, multiplicado pela quantidade do item na oferta | +| `unit` | TEXT NOT NULL | default `'unidade'` | | +| `active` | BOOLEAN NOT NULL | default `true` | inativo não aparece no catálogo geral (`GET /products` filtra opcionalmente por `active`) | +| `service_type` | TEXT NOT NULL | default `'STFC'`; whitelist `SERVICE_TYPES` (server: `server/src/lib/constants.js`) | usado para agrupar itens na oferta/PDF e para sugerir documento fiscal | +| `metered` | BOOLEAN NOT NULL | default `false` | produto "tarifado" (chamadas) — habilita a tabela de tarifas | +| `minutes_allowance` | NUMERIC(12,2) | | franquia de minutos (só relevante se `metered`) | +| `tariff_rates` | JSONB NOT NULL | default `'{}'` | formato `{ [tipo]: { normal: number, reduced: number } }`, tipos: `LC, LDN, VC1, VC2, VC3, LDI` | +| `has_ldi` | BOOLEAN NOT NULL | default `false` | inclui tarifa de Longa Distância Internacional | +| `ixc_product_code` | TEXT | (migration `add-product-ixc-code`) | código do MESMO produto no sistema externo IXC — campo aberto, não sequencial | +| `product_code` | INTEGER NOT NULL UNIQUE | `nextval('products_product_code_seq')`, sequência própria começando em 1 | código interno do OrçaFácil, gerado automaticamente, distinto do UUID e do `ixc_product_code` | +| `discontinued` | BOOLEAN NOT NULL | default `false` | produto descontinuado some do seletor de produtos da oferta (`Product.filter({ active:true, discontinued:false })`), mas continua existindo (uso futuro em "Aditivo", ainda não implementado) | + +Não existe soft-delete: `DELETE /products/:id` remove a linha de fato. + +**Faixas de preço** = 5 colunas fixas (`price_0/12/24/36/48`) — **não é uma +tabela relacionada**, é fixo por produto. Ver seção 2. + +Serialização (`serializeProduct`, `server/src/lib/serialize.js`) expõe todos +os campos acima, convertendo os `price_*`/`impl_unit_price`/ +`minutes_allowance` para `Number` (ou `null`). + +### 1.2 Tabela `quotes` + +Fonte: `1785400000000_baseline-schema.js` + `1785598045226_add-partner-signature-fields-and-quote-sync.js` +(adiciona `client_document`, `client_code`) + `1785513008756_add-client-registrations.js` +(adiciona `client_registration_id`) + `1786340000000_add-quote-fidelity-period.js` +(adiciona `fidelity_period`). + +| Coluna | Tipo | Default/Regras | Observações | +|---|---|---|---| +| `id` | UUID PK | | | +| `created_date` / `updated_date` | TIMESTAMPTZ | trigger auto-update | | +| `created_by_id` | UUID FK `users(id)` | | dono/vendedor responsável — só admin pode reatribuir (seção 9) | +| `quote_number` | TEXT | gerado no frontend: `String(Date.now()).slice(-6)` ao criar uma oferta nova | não é sequencial garantido nem único no banco | +| `reseller_id` | UUID NOT NULL FK `resellers(id)` | | revenda dona da oferta | +| `client_name` | TEXT NOT NULL | | | +| `client_company` | TEXT | | razão social/nome fantasia, se PJ | +| `client_email` | TEXT | | | +| `client_phone` | TEXT NOT NULL | | validado contra duplicidade entre revendas (`validateQuotePhone`, fora do escopo deste doc) | +| `client_document` | TEXT | migration `add-partner-signature-fields...` | CPF/CNPJ — preenchido quando o cadastro do cliente é validado (`registration_status = 'ativo'`), copiado do cadastro | +| `client_code` | INTEGER | idem | código sequencial interno do cliente, copiado do cadastro ativo | +| `ixc_client_code` | TEXT | | código do cliente no sistema IXC | +| `sales_rep_name` | TEXT | | nome de exibição de quem vendeu (texto livre, snapshot) | +| `contract_period` | INTEGER NOT NULL | default `0`; CHECK `IN (0,12,24,36,48)` | **faixa de preço contratada** — determina qual `price_N` foi usado. NÃO necessariamente igual ao prazo de fidelidade real (ver `fidelity_period`) | +| `fidelity_period` | INTEGER | nullable; CHECK `fidelity_period IS NULL OR (fidelity_period >= 0 AND fidelity_period <= contract_period)` | prazo de permanência **efetivamente assinado**, quando diferente (menor) da faixa de preço. `NULL` = sem exceção (fidelidade = `contract_period`, caso padrão) | +| `items` | JSONB NOT NULL | default `'[]'` | array de itens da oferta — estrutura na seção 1.3 | +| `implementation_fee` | NUMERIC(12,2) NOT NULL | default `0` | soma da implantação **calculada a partir dos produtos** (`impl_unit_price × quantity` de cada item) | +| `impl_geral` | NUMERIC(12,2) NOT NULL | default `0` | valor de implantação adicional "geral do projeto", digitado à mão, não ligado a nenhum produto | +| `monthly_total` | NUMERIC(12,2) | | total mensal "de tabela" (soma dos itens, sem condição especial) | +| `contract_total` | NUMERIC(12,2) | | ver fórmula seção 8 | +| `proposed_monthly_total` | NUMERIC(12,2) | | "Condição Especial" — valor mensal final negociado, quando diferente do total de tabela. `null`/vazio = sem condição especial | +| `approval_status` | ENUM `approval_status` (`none, pending, approved, rejected`) | default `'none'` | cobre **duas exceções ao mesmo tempo**: desconto especial (`proposed_monthly_total < monthly_total`) e/ou fidelidade reduzida (`fidelity_period < contract_period`) — uma única aprovação vale para ambas quando as duas existirem juntas na mesma oferta | +| `approval_notes` | TEXT | | observação livre do aprovador | +| `approved_by_id` | UUID FK `users(id)` | nullable | quem aprovou; setado pelo backend, nunca aceito cru do client | +| `notes` | TEXT | | observações gerais da oferta | +| `status` | ENUM `quote_status` (`rascunho, enviado, aprovado, recusado`) | default `'rascunho'` | status do **documento/proposta** em si (independente do negócio) — na prática o frontend sempre grava `'rascunho'` (ver `buildQuoteData`); não há fluxo de UI que altere para os outros valores neste módulo | +| `deal_status` | ENUM `deal_status` (`orcamento, fechado, perdido`) | default `'orcamento'` | status do **negócio** — máquina de estados, seção 5 | +| `impl_payment_condition` | TEXT NOT NULL | default `'À vista'` | ver seção 7 | +| `reseller_id` | (já listado acima) | | | +| `client_registration_id` | UUID FK `client_registrations(id)` | nullable | quando setado, a oferta está **travada** (seção 5) | + +Índices: `idx_quotes_reseller`, `idx_quotes_created`, `idx_quotes_phone`, +`idx_quotes_deal`. + +Serialização (`serializeQuote`) inclui também, via JOIN: +`created_by` (email do criador), `approved_by_name` (nome de quem aprovou). + +### 1.3 Estrutura de `quotes.items` (JSONB array) + +Cada item é um snapshot completo do produto no momento em que foi +adicionado/atualizado na oferta (preços e metadados **não** são recalculados +a partir de `products` depois de salvos — só na tela de edição, ao +recarregar o catálogo, os campos de preço são reconciliados com o produto +atual, ver `NewQuote.jsx` linhas ~201-217): + +```js +{ + product_id: "uuid", + product_name: "string", // snapshot do nome no momento da adição + quantity: number, + unit_price: number, // preço vigente para o período selecionado + total: number, // quantity * unit_price + impl_unit_price: number, // snapshot de products.impl_unit_price + impl_total: number, // quantity * impl_unit_price + price_0: number, // snapshot de products.price_0 (usado no cálculo de desconto por fidelidade) + price_12: number, + price_24: number, + price_36: number, + price_48: number, + service_type: string, // snapshot de products.service_type + metered: boolean, + minutes_allowance: number|null, + tariff_rates: object, // snapshot de products.tariff_rates + has_ldi: boolean, + ixc_product_code: string, +} +``` + +### 1.4 Relação com "contrato" + +**Não existe tabela `contracts`.** Um "contrato" é a junção, em tempo de +consulta, de `client_registrations` (status `'ativo'`) com a `quotes` que o +originou (`client_registrations.quote_id`). Ver seção 10. + +--- + +## 2. Faixas de preço por período de contrato + +- Cada produto tem **5 colunas fixas** de preço mensal: `price_0` (sem + fidelidade), `price_12`, `price_24`, `price_36`, `price_48` — não é uma + tabela relacionada de "price tiers", é hardcoded no schema. +- `quotes.contract_period` grava qual faixa foi usada (`CHECK IN (0,12,24,36,48)`) + — os 5 valores são as únicas faixas suportadas hoje. +- Ao trocar de período na tela de oferta (`changePeriod` em `NewQuote.jsx`): + 1. Para cada item já na oferta, `unit_price` é recalculado lendo + `item[\`price_${novoPeriodo}\`]` (o snapshot do item já carrega os 5 + preços); se ausente, mantém o `unit_price` atual. + 2. `total` do item é recalculado (`quantity * novoPreço`). + 3. **`fidelityPeriod` é resetado para `null`** — uma exceção de fidelidade + é sempre relativa à faixa vigente; trocar de faixa invalida qualquer + valor pendente. +- Ao adicionar um produto (`addProduct`), o preço unitário inicial é + `getPrice(product, period) = product[\`price_${period}\`] || 0`. +- Produto descontinuado (`discontinued = true`) não aparece na lista de + produtos disponíveis para adicionar a uma nova oferta (`Product.filter({ + active: true, discontinued: false })`), mas continua em ofertas já + existentes. + +--- + +## 3. Fidelidade contratual reduzida (`fidelity_period`) + +### 3.1 Conceito + +- `contract_period` = faixa de preço (define qual `price_N` foi usado). +- `fidelity_period` = prazo de permanência **efetivamente assinado** pelo + cliente, quando **menor** que `contract_period` (ex.: cliente fecha no + preço da faixa de 48 meses, mas negocia permanecer só 3, 4, 5 ou 6 meses). +- `fidelity_period = NULL` (padrão, sempre) significa "sem exceção" — a + fidelidade real é igual a `contract_period`. +- Regra de negócio: só é válido preencher um prazo **MENOR** que a faixa de + preço contratada — não faz sentido negociar fidelidade maior que o + período que definiu o preço. CHECK no banco: `ck_fidelity_period`: + `fidelity_period IS NULL OR (fidelity_period >= 0 AND fidelity_period <= contract_period)`. +- Validação espelhada no backend (`validateFidelityPeriod` em + `quotes.routes.js`): inteiro entre `0` e `contract_period`. + +### 3.2 Cálculo de "tem fidelidade reduzida" + +```js +hasReducedFidelity = fidelityPeriod != null && fidelityPeriod < period +``` +(no frontend, `period` = `contract_period` corrente da tela) + +### 3.3 Fluxo de aprovação + +- `hasReducedFidelity` entra no **mesmo** `needsApproval` do desconto + especial de preço (seção 4): `needsApproval = isDiscount || hasReducedFidelity`. +- Uma única `approval_status`/`approval_notes`/`approved_by_id` cobre as + duas exceções quando ambas existem na mesma oferta simultaneamente — não + há aprovação separada por tipo de exceção. +- Quem pode aprovar/recusar (`canApproveDiscount`, backend e mirror no + frontend): + ```js + function canApproveDiscount(user) { + if (user.role === 'super_admin') return true; // sempre, sem flag + return user.role === 'admin' && user.can_approve_discount === true; + } + ``` + `admin` comum só pode aprovar se `users.can_approve_discount = true`. +- Ao alterar `contract_period` (faixa) ou `fidelity_period` na tela, + `approvalStatus` é resetado para `"none"` (nova negociação = nova + aprovação necessária). +- No `POST`/`PATCH` de `quotes`, se `b.approval_status` for `'approved'` ou + `'rejected'`, o backend exige `canApproveDiscount(req.user)` (403 caso + contrário) e define `approved_by_id` = usuário atual (se aprovado) ou + `null` (se recusado) — nunca aceita `approved_by_id` vindo do client. + +### 3.4 Como "vaza" para vencimento, multa e documentos — só depois de aprovado + +Regra idêntica em `variableRegistry.js` (`resolveOfertaRaw`, +`resolveContratoRaw`), `BackofficeAnatelPDFTemplate.jsx`, +`QuotePDFTemplate.jsx` e `contracts.routes.js` (`enrichContract`): + +```js +fidelityPeriod = + (quote.fidelity_period != null && quote.approval_status === 'approved') + ? quote.fidelity_period + : (quote.contract_period || 0); +``` + +Ou seja: **enquanto `approval_status` não é `'approved'`, todo documento, +cálculo de vigência, multa e vencimento de contrato usa `contract_period`** +— nunca expõe (nem calcula) sobre uma exceção pendente. Só depois de +aprovada é que o `fidelity_period` passa a valer para: +- Texto de "Nome Comercial da Oferta" (`Oferta com fidelidade de N meses` / + `Oferta sem fidelidade`). +- "Prazo de Vigência" e "Prazo de Permanência" no PDF Backoffice ANATEL + (conformidade RGC, arts. 3º/XIII, 26/27, 36). +- Teto de multa por rescisão antecipada (art. 37) — seção 3.5. +- Data de vencimento do contrato (seção 10). +- Variável `contrato.vigencia_meses` / `oferta.fidelidade` no gerador de + documentos (`variableRegistry.js`). + +### 3.5 Multa (RGC art. 36/37) + +No `BackofficeAnatelPDFTemplate.jsx`: + +```js +// Desconto total mensal atribuível à fidelidade (NUNCA inclui o desconto +// especial — é o valor legal do "benefício concedido"). +totalDescontoMensal = Σ items[ (item.price_0 - item.unit_price) * item.quantity ] + +// Benefício total concedido pela fidelidade — base legal do teto de multa. +beneficioTotalFidelidade = totalDescontoMensal * fidelityPeriod // fidelityPeriod já resolvido pela regra 3.4 + +// Texto exibido: +"Multa por Rescisão Antecipada (art. 37)": + fidelityPeriod > 0 + ? `proporcional ao tempo restante, limitada a R$ ${beneficioTotalFidelidade.toFixed(2)} (benefício concedido)` + : "Não aplicável" +``` + +Importante: o cálculo do teto de multa usa **só** o desconto de +fidelidade — nunca soma o desconto/acréscimo especial (ver comentário no +código: "não pode misturar com condição especial"). + +--- + +## 4. Desconto / Condição Especial de preço (`proposed_monthly_total`) + +### 4.1 Conceito + +- `monthlyTotal` = soma de tabela (`Σ item.total`, com os preços da faixa + vigente). +- `proposed_monthly_total` = valor mensal final negociado, quando diferente + do total de tabela. +- Detecção (idêntica em frontend `NewQuote.jsx` e nos templates de PDF): + ```js + hasSpecialPrice = proposedMonthly !== "" && Number(proposedMonthly) > 0 + && Number(proposedMonthly) !== monthlyTotal; + isDiscount = hasSpecialPrice && Number(proposedMonthly) < monthlyTotal; + isMarkup = hasSpecialPrice && Number(proposedMonthly) > monthlyTotal; + diffMonthly = hasSpecialPrice ? monthlyTotal - Number(proposedMonthly) : 0; // positivo=desconto, negativo=acréscimo + effectiveMonthly = hasSpecialPrice ? Number(proposedMonthly) : monthlyTotal; + ``` + +### 4.2 Regra de aprovação — só desconto exige aprovação + +- **Desconto** (`isDiscount`, valor final menor que a tabela): entra em + `needsApproval` — fica pendente até um admin autorizado aprovar (mesma + função `canApproveDiscount` da seção 3.3, mesmos campos `approval_status` + / `approval_notes` / `approved_by_id` compartilhados com a fidelidade + reduzida). +- **Acréscimo** (`isMarkup`, valor final maior que a tabela): aplicado + **direto, sem necessidade de aprovação** — nunca passa por + `approval_status`. +- No servidor (`buildQuoteData`/rota), quando `needsApproval` é falso + (isMarkup puro, sem fidelidade reduzida), `approval_status` é forçado + para `'none'`. + +### 4.3 Rateio proporcional por item + +Quando há condição especial ativa (aprovada, se desconto; sempre, se +acréscimo), o valor final é rateado **proporcionalmente ao peso de cada +item no total de tabela** — nunca abate um item só: + +```js +// QuotePDFTemplate.jsx — para exibição ao cliente +share_i = (item.total / monthlyTotal) * (monthlyTotal - effectiveMonthly) +item.total_final = item.total - share_i +item.unit_price_final = item.total_final / item.quantity + +// BackofficeAnatelPDFTemplate.jsx — para o backoffice, com granularidade extra +specialDiscount = monthlyTotal - proposedMonthlyTotal // positivo=desconto, negativo(markup) +item.itemSpecialDiscTotal = (item.totalContr / monthlyTotal) * specialDiscount +item.itemSpecialDiscUnit = item.itemSpecialDiscTotal / item.quantity +item.totalDescGeral = item.descFidTotal + item.itemSpecialDiscTotal // desconto fidelidade + especial, por item +item.priceFinal = item.priceContr - item.itemSpecialDiscUnit +item.totalFinal = item.priceFinal * item.quantity +``` + +### 4.4 Fórmula CORRETA de "economia mensal" e "economia total do contrato" (bug corrigido) + +Commit `154f588` corrigiu um bug em que esses dois campos do PDF Backoffice +ANATEL somavam **só** o desconto de fidelidade, ignorando o +desconto/acréscimo especial. Fórmula correta (vigente): + +```js +// Desconto de fidelidade (nunca inclui condição especial — é valor "legal" pro art.36/37) +totalDescontoMensal = Σ items[ (item.price_0 - item.unit_price) * item.quantity ] + +// specialDiscount: positivo = desconto especial, negativo = acréscimo especial +specialDiscount = hasSpecialPrice ? (monthlyTotal - proposedMonthlyTotal) : 0 + +// ECONOMIA MENSAL (soma dos dois — um acréscimo especial REDUZ a economia, +// pois specialDiscount é negativo nesse caso): +totalEconomiaMensal = totalDescontoMensal + specialDiscount + +// ECONOMIA TOTAL DO CONTRATO (exibida só se totalEconomiaMensal > 0 e contract_period > 0): +economiaTotalContrato = totalEconomiaMensal * contract_period +``` + +Atenção: `beneficioTotalFidelidade` (usado no cálculo do teto de multa, +seção 3.5) **continua sendo só** `totalDescontoMensal * fidelityPeriod` +(fidelidade pura) — não deve ser confundido com `totalEconomiaMensal`, que +é exibido no quadro "Economia Mensal"/"Economia total no contrato" e soma +os dois tipos de desconto. São dois números distintos, calculados +separadamente, para propósitos diferentes (informativo comercial vs. teto +legal de multa). + +--- + +## 5. Trava de edição de oferta e máquina de estados + +### 5.1 Trava (`client_registration_id`) + +- Assim que `quotes.client_registration_id` é setado (cadastro do cliente + iniciado — ver seção 6), a oferta **trava**: não pode mais ser editada + por ninguém, **exceto `super_admin`**. +- Checagem no backend (`PATCH /quotes/:id`, `quotes.routes.js`): + ```js + if (quote.client_registration_id && req.user.role !== 'super_admin') { + return res.status(409).json({ error: 'Esta oferta está travada — o cadastro do cliente já foi iniciado.' }); + } + ``` +- **`super_admin` pode editar tudo** na oferta travada (itens, cliente, + valores, status), **exceto** `client_registration_id` em si — esse campo + só muda pelas rotas de fechamento de negócio (`POST + /quotes/:id/client-registration`), nunca pelo `PATCH` genérico. O update + normal do `PATCH` simplesmente não inclui esse campo no `SET`. +- No frontend, a trava aparece como `locked = !!clientRegistrationId`; o + formulário inteiro fica num `
`. + Para super_admin com oferta travada, aparece um aviso e um atalho rápido + de "só trocar o status do negócio" via `PATCH { deal_status }` sem passar + pelo formulário completo (`superAdminChangeDealStatus`). +- A checagem existe **sempre no servidor**, nunca confia só na UI + desabilitada (comentário explícito no código-fonte). + +### 5.2 Máquina de estados de `deal_status` + +Enum `deal_status`: `'orcamento' | 'fechado' | 'perdido'`. Default: +`'orcamento'`. + +- `orcamento` → estado inicial, oferta em negociação, totalmente editável. +- `fechado` → setado **apenas** através do fluxo de fechamento de negócio + (`POST /quotes/:id/client-registration`), que simultaneamente cria/copia + um `client_registrations` e seta `quotes.client_registration_id` — os + dois campos (`deal_status='fechado'` e `client_registration_id`) mudam + juntos, na mesma transação SQL. Ao setar `client_registration_id`, a + oferta trava (seção 5.1). + - Exceção: `super_admin` pode reverter `deal_status` direto (voltar para + `orcamento` ou marcar `perdido`) mesmo com a oferta travada, via + `PATCH { deal_status }` — não mexe em `client_registration_id`, só + corrige o status pontualmente (ex.: "reverter um fechado que caiu"). +- `perdido` → setado manualmente pelo vendedor/admin enquanto a oferta não + está travada (clique direto no botão de status), ou pelo super_admin via + atalho mesmo travada. +- No frontend, clicar em "OFERTA Fechada" (`handleDealStatusClick`) só + dispara o assistente de fechamento (`showCloseDealDialog`) se: a oferta + já tem `editId` (foi salva antes), `dealStatus !== 'fechado'` ainda, e + `!locked`. Se já travada, só o super_admin chega ali e é tratado pelo + atalho de troca de status pontual. + +### 5.3 Máquina de estados de `status` + +Enum `quote_status`: `'rascunho' | 'enviado' | 'aprovado' | 'recusado'`. +Default `'rascunho'`. Na prática, o frontend sempre grava `'rascunho'` +(`buildQuoteData` hardcoda `status: "rascunho"`) — não há UI neste módulo +que altere para os outros valores; é um campo do modelo original que ficou +com uso residual (a tela de listagem `Quotes.jsx` exibe um Badge colorido +por esse status, mas nada no fluxo atual o muda). + +--- + +## 6. Fluxo de fechamento de negócio ("fechado") + +Rota: `POST /quotes/:id/client-registration` (`quotes.routes.js`). +Permissão: mesma de edição da oferta (`loadForWrite` — dono com feature +`ofertas:edit`, ou `canEditAllQuotes`). Falha com 409 se a oferta já tem +`client_registration_id`. + +Dois caminhos, escolhidos pelo vendedor na UI (`showCloseDealDialog`, +`closeDealStep`): + +### 6.1 Caminho normal (cliente novo) — `is_portability` + `person_type` + +Body: `{ is_portability: boolean, person_type: 'pf'|'pj' }` (ambos +obrigatórios, validados no backend). + +1. Gera `token = crypto.randomBytes(24).toString('hex')`. +2. `BEGIN` transação: + - `INSERT INTO client_registrations (quote_id, created_by_id, token, + is_portability, person_type, reseller_id)` — nasce com + `registration_status = 'rascunho'` (default da tabela). + - `UPDATE quotes SET deal_status='fechado', client_registration_id=`. + - `COMMIT`. +3. Retorna `{ registration, public_link }`, onde + `public_link = \`${baseUrl}/cadastro-cliente?token=${token}\`` — link + público (sem autenticação) para o **próprio cliente** preencher seu + cadastro completo (fora do escopo deste doc — módulo de + `client_registrations`). +4. Frontend abre automaticamente um segundo diálogo (`showSendLinkDialog`) + para copiar/enviar esse link por e-mail ou WhatsApp. + +### 6.2 Atalho "cliente já possui cadastro ativo" (`reuse_from_registration_id`) + +Usado quando o mesmo cliente já comprou antes e tem um +`client_registrations` com `registration_status = 'ativo'`. Body: +`{ reuse_from_registration_id: uuid, person_type }`. + +1. Busca o registro de origem; exige `registration_status === 'ativo'` + (400 caso contrário). +2. Escopo: quem não é admin/backoffice só pode reaproveitar cadastro da + **própria revenda** (`sourceReg.reseller_id === quote.reseller_id`, 403 + caso contrário). +3. `BEGIN` transação: + - `INSERT INTO client_registrations (... campos de identidade/endereço/ + contato copiados de REUSE_COPY_FIELDS ...) SELECT ... FROM + client_registrations WHERE id = origem` — nasce **já com** + `registration_status = 'ativo'`, `submitted_at = now()`, + `activated_at = now()` — sem token público, sem e-mail, sem validação + manual. + - `REUSE_COPY_FIELDS` (constante no topo do arquivo): endereço completo, + todos os contatos (principal/financeiro/técnico), todos os campos + PF/PJ, todos os campos de verificação de CPF/CNPJ, endereço do CNPJ. + **Explicitamente fora da lista** (não herdado): `ixc_client_id`, + `ixc_contract_number`, `client_code` (cada registro tem o seu, novo), + `portability_numbers/ranges`, `internal_notes`. + - Se a origem é PJ, copia também `client_registration_partners` + (sócios/representantes) do registro de origem para o novo. + - `UPDATE quotes SET deal_status='fechado', client_registration_id=`. + - `COMMIT`. +4. Chama `syncActiveRegistrationToQuote(reg)` — sincroniza + `client_document`/`client_code`/etc. de volta para a `quotes` (fora do + escopo detalhado deste doc, mas é o mecanismo que preenche + `quotes.client_document`/`quotes.client_code`). +5. Frontend recarrega a página inteira (`window.location.reload()`) — não + mostra diálogo de link público (não é necessário, já está ativo). + +Em ambos os caminhos, o resultado final é: `quotes.deal_status = 'fechado'` +e `quotes.client_registration_id` setado — o que trava a oferta (seção 5.1). + +--- + +## 7. Condição de pagamento de implantação (`impl_payment_condition`) + +Fonte: `server/src/lib/paymentConditions.js`. + +```js +export const PAYMENT_CONDITIONS = ['À vista', '1+1', '1+2']; + +export function getAvailablePaymentConditions(implValue) { + const value = implValue || 0; + if (value <= 1500) return ['À vista']; + if (value <= 3000) return ['À vista', '1+1']; + return ['À vista', '1+1', '1+2']; +} +``` + +- Progressivo por faixa de valor: quanto maior o total de implantação, mais + opções de parcelamento ficam disponíveis. "À vista" está sempre + disponível. +- `implTotal` usado na checagem = `implementation_fee + impl_geral` (soma + dos dois componentes da implantação, seção 8). +- Validação no backend (`validatePaymentCondition`, `quotes.routes.js`), + chamada tanto no `POST` quanto no `PATCH`: + ```js + function validatePaymentCondition(condition, implTotal) { + if (condition === undefined) return null; // não veio no body, ignora + const allowed = getAvailablePaymentConditions(implTotal); + if (!allowed.includes(condition)) { + return `Condição de pagamento da implantação inválida para o valor de implantação (R$ ${implTotal.toFixed(2)}). Opções disponíveis: ${allowed.join(', ')}`; + } + return null; + } + ``` + Retorna 400 se inválida. +- No frontend, um `useEffect` observa `totalImpl` e, se a condição + selecionada deixar de estar disponível (ex.: implantação diminuiu), troca + automaticamente para a **última** opção ainda válida + (`availablePaymentConditions[availablePaymentConditions.length - 1]`). +- Default no schema: `'À vista'`. + +--- + +## 8. Cálculos financeiros exatos + +Variáveis-base (calculadas no frontend em `NewQuote.jsx`, e recalculadas de +forma idêntica nos templates de PDF a partir dos dados salvos): + +```js +// --- Itens --- +monthlyTotal = Σ items[ item.total ] // total "de tabela", soma dos itens no preço da faixa vigente +implFromProducts = Σ items[ item.impl_total ] // Σ (item.impl_unit_price * item.quantity) +totalImpl = implFromProducts + Number(impl_geral || 0) // implantação total (produtos + geral do projeto) + +// --- Condição especial (seção 4) --- +hasSpecialPrice = proposedMonthly !== "" && Number(proposedMonthly) > 0 && Number(proposedMonthly) !== monthlyTotal +isDiscount = hasSpecialPrice && Number(proposedMonthly) < monthlyTotal +isMarkup = hasSpecialPrice && Number(proposedMonthly) > monthlyTotal +diffMonthly = hasSpecialPrice ? monthlyTotal - Number(proposedMonthly) : 0 // > 0 desconto, < 0 acréscimo +effectiveMonthly = hasSpecialPrice ? Number(proposedMonthly) : monthlyTotal + +// --- Fidelidade reduzida (seção 3) --- +hasReducedFidelity = fidelityPeriod != null && fidelityPeriod < contractPeriod + +// --- Necessidade de aprovação --- +needsApproval = isDiscount || hasReducedFidelity + +// --- Total do contrato --- +contractTotal = effectiveMonthly * (contractPeriod || 1) + totalImpl +``` + +Observações importantes sobre `contractTotal`: +- Usa `(contractPeriod || 1)` — se `contractPeriod = 0` (sem fidelidade), + o multiplicador vira `1` (evita zerar o total; na prática representa "1 + mês" como base do total exibido, mesmo sem fidelidade formal). +- Usa **`contractPeriod`** (a faixa de preço), não `fidelityPeriod` — o + "Total do Contrato" reflete o valor comercial pela faixa de preço + contratada, não pelo prazo de permanência reduzido negociado à parte. +- Usa `effectiveMonthly` (já considerando condição especial, se ativa) — + isso é consistente tanto no momento de montar a oferta (`NewQuote.jsx`, + onde qualquer `proposedMonthly` preenchido conta, mesmo pendente) quanto + no PDF ao cliente (`QuotePDFTemplate.jsx`, onde só conta se + `specialActive` = `isMarkup || (isDiscount && approval_status === + 'approved')`). + +**Persistência** (payload salvo em `quotes` via `POST`/`PATCH`): +```js +implementation_fee = implFromProducts // NÃO inclui impl_geral +impl_geral = Number(impl_geral || 0) +monthly_total = monthlyTotal +contract_total = contractTotal +proposed_monthly_total = hasSpecialPrice ? Number(proposedMonthly) : null +fidelity_period = hasReducedFidelity ? fidelityPeriod : null // só persiste se for de fato menor que a faixa +approval_status = needsApproval ? (approvalStatus === 'none' ? 'pending' : approvalStatus) : 'none' +``` + +**Economia mensal / total** (exibidos no PDF Backoffice ANATEL, seção 4.4): +```js +totalDescontoMensal = Σ items[ (item.price_0 - item.unit_price) * item.quantity ] // desconto de fidelidade puro +specialDiscount = hasSpecialPrice ? (monthlyTotal - proposedMonthlyTotal) : 0 // > 0 desconto especial, < 0 acréscimo especial +totalEconomiaMensal = totalDescontoMensal + specialDiscount +economiaTotalContrato = totalEconomiaMensal * contractPeriod // só exibida se totalEconomiaMensal > 0 e contractPeriod > 0 +beneficioTotalFidelidade = totalDescontoMensal * fidelityPeriodResolvido // usado só no teto de multa (art. 37), NUNCA soma o especial +``` + +--- + +## 9. Reatribuição de oferta (`created_by_id`) + +- Só **admin** (`isAdminRole` = `admin` ou `super_admin`) pode mudar + `quotes.created_by_id` de uma oferta existente. +- No `PATCH /quotes/:id`: + ```js + if (b.created_by_id !== undefined && b.created_by_id !== quote.created_by_id) { + if (!isAdminRole(req.user.role)) return 403; + const target = await pool.query('SELECT id FROM users WHERE id = $1', [b.created_by_id]); + if (!target.rows[0]) return 400; // usuário de destino não encontrado + createdById = b.created_by_id; + } + ``` +- No frontend, só aparece o seletor de "Responsável Comercial" (dropdown + com os usuários da mesma revenda) quando `isAdmin && editId && + resellerUsers.length > 0` — para os demais, o campo é somente leitura + (mostra `salesRepName`, texto snapshot). +- Ao reatribuir, `sales_rep_name` também é atualizado no frontend para + refletir o novo responsável (mas é só um snapshot de texto, não uma FK). + +--- + +## 10. Contratos + +### 10.1 Não existe tabela `contracts` + +"Contrato" é a junção, calculada em tempo de consulta, de +`client_registrations` (ativos) com a `quotes` de origem: + +```sql +-- server/src/routes/contracts.routes.js, LIST_SELECT +SELECT cr.id, cr.client_code, cr.person_type, cr.pf_full_name, cr.pj_company_name, cr.pj_trade_name, + cr.pf_cpf, cr.pj_cnpj, cr.activated_at, cr.reseller_id, r.name AS reseller_name, + q.id AS quote_id, q.quote_number, q.contract_period, q.fidelity_period, q.approval_status, + q.monthly_total, q.contract_total, q.proposed_monthly_total, q.ixc_client_code +FROM client_registrations cr +JOIN quotes q ON q.id = cr.quote_id +LEFT JOIN resellers r ON r.id = cr.reseller_id +WHERE cr.registration_status = 'ativo' +``` + +### 10.2 Enriquecimento (`enrichContract`) + +```js +clientName = person_type === 'pj' ? (pj_company_name || pj_trade_name) : pf_full_name +clientDocument = person_type === 'pj' ? pj_cnpj : pf_cpf +effectiveMonthly = (proposed_monthly_total != null && proposed_monthly_total > 0) + ? proposed_monthly_total : (monthly_total || 0) + +// Fidelidade REAL (mesma regra da seção 3.4): usa fidelity_period só se +// aprovado; senão cai para contract_period. +fidelityPeriod = (fidelity_period != null && approval_status === 'approved') + ? fidelity_period : contract_period + +dueDate = computeDueDate(activated_at, fidelityPeriod) + // null se activated_at ausente OU fidelityPeriod é 0/falsy (sem fidelidade = indeterminado) + // senão: new Date(activated_at) com +fidelityPeriod meses (setMonth) + +daysUntilDue = dueDate ? Math.ceil((dueDate - now) / 86400000) : null +due_calculable = !!activated_at // cadastros ativados ANTES do rastreamento desta feature não têm activated_at e não têm vencimento inventado +indeterminate = !fidelityPeriod // sem fidelidade = vencimento indeterminado, mesmo com activated_at +``` + +Campos retornados por contrato: `id, client_code, client_name, +client_document, reseller_name, quote_id, quote_number, ixc_client_code, +activated_at, contract_period, fidelity_period (já resolvido), monthly_value, +contract_total, due_date, days_until_due, due_calculable, indeterminate`. + +### 10.3 Rotas + +- `GET /contracts` (feature `contratos:view`) — lista todos, ordenados por + `activated_at ASC NULLS LAST`. Usado por `src/pages/Contracts.jsx` + (tabela com busca por cliente/documento/revenda/oferta, badge de + vencimento colorido: vermelho se vencido/≤30 dias, âmbar se ≤60 dias, + verde caso contrário; "Não calculável" se `!due_calculable`, + "Indeterminado" se `indeterminate`). +- `GET /contracts/report` (feature `contratos_relatorios:view`) — agregados + para dashboard: + ```js + total_active_contracts = contracts.length + total_active_mrr = Σ contracts[monthly_value] + due_30/60/90 = { count, value_at_risk } para contratos com + 0 <= days_until_due <= N (value_at_risk = Σ monthly_value dos que vencem na janela) + by_reseller = agrupado por reseller_name: { count, mrr }, ordenado por mrr desc + upcoming_renewals = contratos com days_until_due <= 90, ordenados asc, top 20 + not_calculable_count = Σ !due_calculable + indeterminate_count = Σ indeterminate + ``` + Usado por `src/pages/ContractReports.jsx` (cards de resumo + lista de + próximos vencimentos + MRR por revenda). + +### 10.4 Vencimento sempre pela fidelidade REAL, nunca pela faixa de preço + +Reforçando o ponto mais importante para reconstrução: em TODOS os lugares +que calculam vigência/vencimento/multa (contratos, PDFs, gerador de +documentos), a regra é idêntica e centralizada no padrão: + +```js +fidelidadeEfetiva = (quote.fidelity_period != null && quote.approval_status === 'approved') + ? quote.fidelity_period + : (quote.contract_period || 0) +``` + +`contract_period` continua sendo usado **apenas** para: (a) determinar qual +`price_N` foi aplicado, e (b) calcular `contract_total` (que é sempre pelo +período de preço contratado, seção 8) — nunca para vencimento/multa quando +há uma exceção de fidelidade já aprovada. + +--- + +## 11. Permissões e escopo (resumo transversal, relevante a Produtos/Ofertas/Contratos) + +Mirror exato entre `server/src/lib/roles.js` e `src/lib/roles.js`: + +```js +ADMIN_ROLES = ['admin', 'super_admin'] +isAdminRole(role) = ADMIN_ROLES.includes(role) +isAdminOrBackofficeRole(role) = isAdminRole(role) || role === 'backoffice' + +hasFeatureAccess(user, key, minLevel='view'): + super_admin -> sempre true + senão -> user.role_permissions[key] (mapa 'view'|'edit' por feature, configurado em Permissões por Papel) precisa rank >= minLevel + (rank: view=1, edit=2) + +canViewAllQuotes(user) = super_admin || hasFeatureAccess(user, 'ofertas_others', 'view') +canEditAllQuotes(user) = super_admin || hasFeatureAccess(user, 'ofertas_others', 'edit') +``` + +- Feature `produtos` (view/edit) controla CRUD de `products`. +- Feature `ofertas` (view/edit) controla CRUD de `quotes` — mas por padrão + cada usuário só vê/edita as **próprias** ofertas + (`created_by_id = user.id`), a menos que tenha `ofertas_others`. +- Feature `contratos`/`contratos_relatorios` controlam as rotas de + contratos. +- `GET /quotes` aplica escopo via `scopeClause`: sem `ofertas_others`, o + `WHERE` inclui `q.created_by_id = $1` (usuário atual); com + `ofertas_others`, vê tudo (sem filtro adicional, exceto os opcionais + `id`/`reseller_id` da query string). +- `POST /quotes`: usuário não-admin só pode criar oferta para a própria + `reseller_id` (403 caso tente para outra revenda). +- `DELETE /quotes/:id` e `DELETE /products/:id`: exigem `isAdminRole` + (produtos) / idem para quotes. + +--- + +## 12. Referência rápida de arquivos-fonte + +| Assunto | Arquivo | +|---|---| +| CRUD produtos | `server/src/routes/products.routes.js` | +| CRUD ofertas, fechamento de negócio, envio de PDF por e-mail | `server/src/routes/quotes.routes.js` | +| "Contratos" (derivado), relatórios | `server/src/routes/contracts.routes.js` | +| Condição de pagamento de implantação | `server/src/lib/paymentConditions.js` | +| Serialização de `products`/`quotes` | `server/src/lib/serialize.js` | +| Variáveis calculadas para geração de documentos (fidelidade, multa, vigência, desconto) | `server/src/lib/documentTemplates/variableRegistry.js` | +| Tela de criação/edição de oferta (toda a lógica de negócio do frontend) | `src/pages/NewQuote.jsx` | +| Tela de produtos | `src/pages/Products.jsx` | +| Listagem de ofertas | `src/pages/Quotes.jsx` | +| Listagem de contratos | `src/pages/Contracts.jsx` | +| Relatórios de contratos | `src/pages/ContractReports.jsx` | +| PDF da oferta (visão cliente) | `src/components/quote/QuotePDFTemplate.jsx` | +| PDF Backoffice/ANATEL (conformidade RGC, desconto, multa) | `src/components/quote/BackofficeAnatelPDFTemplate.jsx` | +| Papéis/permissões (mirror front/back) | `server/src/lib/roles.js`, `src/lib/roles.js` | +| Schema base | `server/migrations/1785400000000_baseline-schema.js` | +| `client_document`/`client_code` em quotes | `server/migrations/1785598045226_add-partner-signature-fields-and-quote-sync.js` | +| `client_registration_id` em quotes + tabela `client_registrations` | `server/migrations/1785513008756_add-client-registrations.js` | +| `products.ixc_product_code` | `server/migrations/1785980000000_add-product-ixc-code.js` | +| `products.product_code` (sequencial) | `server/migrations/1785990000000_add-product-code.js` | +| `products.discontinued` | `server/migrations/1786020000000_add-product-discontinued.js` | +| `quotes.fidelity_period` + CHECK | `server/migrations/1786340000000_add-quote-fidelity-period.js` | +| Bugfix "economia mensal/total soma desconto especial" | commit `154f588` | +| Trava de edição + exceção super_admin | commit `b20bf2f` | +| Fidelidade reduzida (feature completa) | commit `d3697bb` | + + +--- + + +# 3. Cadastro de Cliente e Cadastro/Gestão de Revendas + + +Documentação de referência do sistema atual (OrçaFácil — Handix), extraída do código-fonte +(`server/src/routes/*.js`, `server/src/lib/*.js`, `server/migrations/*.js`, `src/pages/*.jsx`, +`src/lib/*.js`), para reconstrução fiel em outro stack. + +Convenção de leitura: nomes de coluna/campo são citados literalmente (snake_case, como no +Postgres/JSON da API) porque o Eden deve preservar o mesmo vocabulário de domínio, mesmo +usando outro schema físico. + +--- + +## 1. Modelo de dados + +### 1.1 Tabela `client_registrations` + +Cadastro de um cliente final (pessoa física ou jurídica) que fechou uma oferta (quote) — ou, +mais raramente, criado "avulso" sem oferta (ver seção 2.6). É o registro central de todo o +fluxo; tudo gira em torno dele (anexos, sócios, sincronização com a oferta, sincronização com +IXC). + +Chave primária `id UUID DEFAULT gen_random_uuid()`. Timestamps `created_date`/`updated_date` +(trigger `set_updated_date()` atualiza `updated_date` a cada UPDATE). + +**Vínculos e controle** +| Campo | Tipo | Observação | +|---|---|---| +| `quote_id` | UUID, FK `quotes(id)`, UNIQUE, **nullable** | 1:1 com a oferta que originou o cadastro. Nullable desde a migration `1786310000000` (permite cadastro "direto", sem oferta — ver 2.6). UNIQUE permite múltiplos NULL (Postgres). | +| `created_by_id` | UUID, FK `users(id)` | Vendedor/usuário que gerou o link. | +| `reseller_id` | UUID, FK `resellers(id)`, nullable | Snapshot da revenda dona da oferta no momento da criação — gravado à parte (não só via join com `quotes`) para manter histórico correto mesmo se a oferta for reatribuída depois. Populado por `UPDATE ... SET reseller_id = q.reseller_id FROM quotes` na migration que introduziu a coluna. | +| `token` | TEXT, UNIQUE, NOT NULL | `crypto.randomBytes(24).toString('hex')` — 48 caracteres hex. É a credencial do link público (ver seção 9). | +| `token_created_at` | TIMESTAMPTZ | Não há expiração automática — token vale até o cadastro deixar de estar `rascunho` (ver seção 9). | +| `client_code` | INTEGER, UNIQUE, NOT NULL, DEFAULT `nextval('client_registrations_client_code_seq')` | Código sequencial interno do cliente (visível ao usuário, ex: "Cliente #42"), começa em 1, sequência própria (não reaproveita id). | + +**Estado do cadastro** +| Campo | Tipo | Observação | +|---|---|---| +| `is_portability` | BOOLEAN NOT NULL | Definido na criação (pelo vendedor/backoffice), não pelo cliente — controla se a seção de portabilidade numérica aparece no formulário público e se a fatura anexa é obrigatória. | +| `person_type` | ENUM `client_person_type` (`pf`, `pj`) NOT NULL | Definido na criação; o cliente pode escolher/confirmar no formulário público (pré-selecionado, mas ainda editável — ver `ClientRegistration.jsx`). | +| `registration_status` | ENUM `client_registration_status` (`rascunho`, `pendente_validacao`, `ativo`, `bloqueado`, `inativo`) NOT NULL DEFAULT `rascunho` | Máquina de estados — ver 1.1.1. | +| `submitted_at` | TIMESTAMPTZ | Setado quando o cliente envia o formulário público (rascunho → pendente_validacao). | +| `activated_at` | TIMESTAMPTZ | Setado **só na primeira vez** que o status vira `ativo` (`COALESCE(activated_at, now())` — nunca sobrescrito numa reativação). Usado pelo módulo Contratos para calcular vencimento (data de início de vigência). | +| `internal_notes` | TEXT | Observações internas do backoffice, nunca visível ao cliente/vendedor no formulário público. | + +**Endereço final (compartilhado PF/PJ)** — preenchido pelo cliente no formulário público, editável depois pelo backoffice: +`address_zip`, `address_street`, `address_number`, `address_complement`, `address_neighborhood`, `address_city`, `address_state`, `address_country` (TEXT NOT NULL DEFAULT `'Brasil'`). + +**Endereço "oficial" da Receita Federal (só PJ)** — trazido pela consulta pública de CNPJ, guardado **à parte** do endereço final, para o backoffice comparar os dois lado a lado (migration `1785592240556`): +`cnpj_address_confirmed` (BOOLEAN — true se o cliente confirmou que o endereço da Receita está correto), `cnpj_address_zip`, `cnpj_address_street`, `cnpj_address_number`, `cnpj_address_complement`, `cnpj_address_neighborhood`, `cnpj_address_city`, `cnpj_address_state`. +Quando `cnpj_address_confirmed = true`, o endereço final é copiado do endereço da Receita; quando `false`, o cliente preenche um endereço final diferente do zero. + +**Contatos (compartilhado; PF só usa o financeiro, PJ usa os três)**: +`contact_principal_name/email/phone`, `contact_financial_name/email/phone`, `contact_technical_name/email/phone`. + +**Pessoa Física**: +`pf_full_name`, `pf_cpf`, `pf_birth_date` (DATE), `pf_email`, `pf_mobile_phone`, `pf_alt_phone`, `pf_social_name` (nome social), `pf_id_document` (nº do documento de identidade — RG etc.), `pf_id_issuer` (órgão emissor). + +**Pessoa Jurídica**: +`pj_cnpj`, `pj_company_name` (razão social), `pj_trade_name` (nome fantasia), `pj_state_registration` (Inscrição Estadual — ou "Isento"), `pj_municipal_registration` (Inscrição Municipal — ou "Isento"), `pj_opening_date` (DATE — data de abertura), `pj_legal_nature` (natureza jurídica), `pj_cnpj_situation` (situação cadastral, ex: "ATIVA"), `pj_main_activity` (atividade principal/CNAE descrição), `pj_rep_full_name`, `pj_rep_cpf`, `pj_rep_role` (cargo), `pj_rep_email`, `pj_rep_mobile_phone`, `pj_rep_authorization_ack` (BOOLEAN NOT NULL DEFAULT false — declaração "possuo poderes para representar a empresa"). + +**Verificação de documento** (formato apenas — nenhuma verificação externa real implementada; campos `*_externally_verified` deixados prontos para integração futura, ex: Receita Federal): +Por CPF: `cpf_format_valid` (BOOLEAN), `cpf_externally_verified` (BOOLEAN NOT NULL DEFAULT false), `cpf_verified_at`, `cpf_verification_provider`, `cpf_verification_reference`, `cpf_verification_status`, `cpf_verification_details` (JSONB). +Por CNPJ: campos espelhados com prefixo `cnpj_*`. +Hoje só `cpf_format_valid`/`cnpj_format_valid` são gravados (sempre `true` no submit — se o dígito verificador falhasse, o request já teria sido rejeitado com 400 antes). + +**Portabilidade numérica** (migration `1785610000000`): +`portability_numbers JSONB NOT NULL DEFAULT '[]'` — array de strings (números avulsos, ex: `["(48) 3433-0582"]`). +`portability_ranges JSONB NOT NULL DEFAULT '[]'` — array de objetos `{first, last}` (faixas de números). +Só relevante quando `is_portability = true`; a validação no submit exige ao menos um item entre os dois arrays. + +**Integração IXC**: +`ixc_client_id` TEXT — ID do cliente no IXC, preenchido pelo backoffice **no momento da validação** do cadastro (quando o cliente é efetivamente criado no ERP externo). Diferente de `ixc_contract_number` TEXT — nº do contrato no IXC, também preenchido manualmente pelo backoffice depois de formalizado. E diferente de `quotes.ixc_client_code` — referência opcional preenchida pelo vendedor ainda na oferta, antes de tudo isso. + +### 1.1.1 Máquina de estados de `registration_status` + +``` +rascunho ──(cliente envia o formulário público)──▶ pendente_validacao +pendente_validacao ──(backoffice aprova)──▶ ativo +pendente_validacao ──(backoffice recusa, via PATCH registration_status)──▶ bloqueado +ativo ──▶ inativo (toggle "Inativar Cliente" no backoffice) +inativo ──▶ ativo (toggle "Reativar Cliente") +bloqueado/inativo ──▶ ativo (reativação manual) +``` +Não há endpoint dedicado de transição — todas as mudanças passam por `PATCH /client-registrations/:id` com `registration_status` no corpo (rota genérica, ver 1.1.2). O front (`Clientes.jsx`) expõe um `` livre (não há botões "Aprovar"/"Recusar" + dedicados como no fluxo de revenda — é um PATCH genérico). +- Preencher `ixc_client_id` e `ixc_contract_number` manualmente. +- Editar/completar números de portabilidade. +- Ver e corrigir CPF completo de cada sócio + marcar quem assina + e-mail de assinatura + (`PATCH /client-registrations/:id/partners/:partnerId`, exclusivo `requireAdminOrBackoffice`). +- Ver/baixar anexos enviados pelo cliente; anexos internos (`purpose='internal'` E + `uploaded_by_id` preenchido) só aparecem para admin/backoffice, nunca para o dono da oferta. +- Fazer upload de novo anexo (categoria livre + finalidade signature/internal). +- Excluir anexo — **exclusivo super_admin** (irreversível, remove do storage e do banco). +- Botão dedicado "Inativar/Reativar Cliente" (atalho de PATCH `registration_status`). +- Se há anexos `purpose='signature'`, botão "Criar Envelope de Assinatura" (módulo de + assinatura eletrônica, fora do escopo aqui). + +### 2.6 Cadastro "direto" (sem oferta) + +`POST /client-registrations/direct` (autenticado) cria um `client_registrations` com `quote_id += NULL`, usando o **mesmo mecanismo de token/formulário público**. Corpo: `{ is_portability, +person_type, reseller_id? }`. Quem não é admin/backoffice só cria para a própria revenda +(`req.user.reseller_id`); admin/backoffice pode opcionalmente indicar `reseller_id`. Usado +hoje pela tela de "Cessão" (fora do escopo) e pelo botão "Novo" em Clientes — abre uma aba nova +já no `public_link` retornado, para o próprio operador (ou o cliente, se repassado) preencher. + +--- + +## 3. Pessoa Física vs Pessoa Jurídica — diferenças + +| Aspecto | PF | PJ | +|---|---|---| +| Documento principal | CPF (`pf_cpf`) | CNPJ (`pj_cnpj`) | +| Seção de identificação | Nome, CPF, nascimento, e-mail, celular, tel. alternativo, nome social, documento de identidade + emissor | Razão social, nome fantasia, IE, IM, data abertura, natureza jurídica, situação cadastral, atividade principal | +| Consulta automática | Nenhuma (não existe "lookup de CPF" público equivalente) | Consulta de CNPJ (cnpj.ws/BrasilAPI) preenche empresa, endereço e sócios | +| Representante/assinante | Não existe — o próprio titular assina | Obrigatório: representante legal OU sócio marcado como assinante | +| Sócios (QSA) | N/A | Tabela `client_registration_partners`, populável pela consulta de CNPJ | +| Confirmação de endereço da Receita | N/A | Fluxo "endereço encontrado está correto?" | +| Contatos exigidos | Só financeiro (nome+e-mail) | Financeiro **e** técnico (nome+e-mail) obrigatórios; principal opcional | +| Contrato social (anexo) | N/A | Obrigatório **se** a consulta de CNPJ não trouxe sócios | +| Inscrição Estadual/Municipal | N/A | Perguntadas explicitamente (Sim/Não) quando a consulta não trouxe o dado | + +--- + +## 4. Sócios/Partners + +- Exigidos **apenas em PJ**, e só aparecem no formulário se a consulta pública de CNPJ + (cnpj.ws → fallback BrasilAPI) retornar o quadro societário (QSA). Não há como o cliente + adicionar um sócio manualmente que não veio da consulta. +- Campos por sócio: `name`, `document` (mascarado, vindo da fonte), `qualification` + (ex: "Sócio-Administrador"), `source` (`'brasilapi'` fixo hoje, mesmo quando veio do + cnpj.ws — nome do campo não foi atualizado), `cpf` (completo, digitado pelo cliente **só se** + for assinar), `will_sign` (bool), `signature_email`. +- Assinatura por sócio: cada sócio tem um checkbox "Vai assinar o contrato". Marcando, exige + CPF completo (validado no backend) e e-mail de assinatura. **O primeiro sócio marcado + substitui inteiramente o representante da empresa** — os campos `pj_rep_*` no cadastro + recebem os dados desse sócio (name→pj_rep_full_name, cpf→pj_rep_cpf, qualification→ + pj_rep_role, signature_email→pj_rep_email); se **nenhum** sócio é marcado, o bloco + "Representante da Empresa" é obrigatório e preenchido do zero. +- Pós-submissão, backoffice pode corrigir CPF/will_sign/signature_email de cada sócio + individualmente (`PATCH /client-registrations/:id/partners/:partnerId`), útil quando o + cliente esqueceu de marcar/preencher no formulário. +- Mesmíssimo modelo e mesma UI (componentizada e duplicada) se aplicam a + `reseller_registration_partners` no cadastro de revenda — o representante legal da revenda + segue a mesma regra de substituição pelo primeiro sócio assinante. + +--- + +## 5. Sincronização com IXC / com a Oferta + +**Não existe integração automática/API com o IXC** neste código — toda referência a "IXC" é +um campo de texto livre preenchido manualmente pelo backoffice depois de criar/formalizar o +cliente no ERP externo (fora deste sistema): +- `client_registrations.ixc_client_id` — preenchido na validação do cadastro. +- `client_registrations.ixc_contract_number` — preenchido depois, quando o contrato IXC existe. +- `quotes.ixc_client_code` — referência opcional preenchida pelo vendedor, ainda na oferta, + antes de tudo (pode ou não bater com `ixc_client_id`). + +O que existe é `syncActiveRegistrationToQuote(reg)` (`server/src/lib/clientRegistrationSync.js`), +chamada sempre que: +(a) `PATCH /client-registrations/:id` deixa o registro em `registration_status = 'ativo'`; +(b) uma oferta reaproveita um cadastro já ativo (`reuse_from_registration_id`). + +Ela **não faz nada se `reg.registration_status !== 'ativo'`**. Quando ativo, roda: +```sql +UPDATE quotes SET + client_company = COALESCE($1, client_company), -- pj_trade_name || pj_company_name, ou pf_full_name + client_document = COALESCE($2, client_document), -- pj_cnpj ou pf_cpf + client_code = COALESCE($3, client_code), -- client_registrations.client_code + ixc_client_code = COALESCE($4, ixc_client_code) -- client_registrations.ixc_client_id +WHERE id = quote_id +``` +Ou seja: replica identificação do cliente **para a oferta** (não o contrário), só sobrescreve +campos que estavam vazios (`COALESCE`), e é oportunista — roda a cada PATCH que resulte em +`ativo`, não só na primeira ativação. + +No Eden, isso deve ser modelado como um efeito colateral do PATCH de status (ou um evento/hook +"registration activated"), não como uma integração externa de verdade — não há chamada de rede +nenhuma aqui, é só um UPDATE no mesmo banco. + +--- + +## 6. Fluxo de cadastro/aprovação de Revenda (Programa de Canais) + +Estrutura quase idêntica à de cliente, mas iniciada de forma diferente: **não** nasce de uma +oferta fechada — nasce de um convite manual. + +### 6.1 Criação do convite + +`POST /reseller-registrations` (autenticado, feature `revendas_cadastro` nível `edit`). Corpo +mínimo: `{ email, reseller_type }` onde `reseller_type ∈ {'finder', 'recorrente'}`. Gera token, +insere linha `rascunho`, retorna `{ registration, public_link }`. +- **Finder**: revenda pontual/indicação — gera só "Contrato Canal Finder". +- **Recorrente**: parceiro recorrente — gera **sempre os dois documentos juntos**: "Contrato + Canal Recorrente" + "Termo de Adesão Recorrente". + +E-mail de convite (`POST /reseller-registrations/:id/send-email`) — assunto "Cadastro Handix", +texto "Você foi convidado(a) a fazer parte do Programa de Canais Handix", botão "Completar +cadastro". + +### 6.2 Preenchimento público (`GET`/`POST /public-reseller-registration/:token`) + +Mesmo contrato de contexto (`already_submitted`, 404 genérico para token inválido, 409 se já +enviado). Formulário (`ResellerRegistration.jsx`) é um subconjunto do de cliente-PJ: CNPJ +(com mesma consulta cnpj.ws→BrasilAPI, mesmo preenchimento automático de empresa/endereço/ +sócios), razão social, nome fantasia, telefone (único, não triplo como em cliente), IE/IM com +mesma pergunta condicional Sim/Não, bloco Representante (mesma regra: sumido se houver sócio +assinante), confirmação de endereço do CNPJ, endereço. **Não tem** seção de contatos +(financeiro/técnico), **não tem** anexos, **não tem** portabilidade (revenda não porta +número). + +Validação no backend igual em espírito à de cliente: razão social, CNPJ válido, telefone, +(sócio assinante com CPF válido) OU (representante com nome+CPF válido+ack de poderes). + +Persistência: `UPDATE reseller_registrations` com todos os campos + `registration_status = +'pendente_validacao'` + `submitted_at = now()`; sócios inseridos em +`reseller_registration_partners`. Sem uploads (tabela de anexos existe no schema desde o +início, mas só passa a ser usada depois da aprovação, pelo backoffice). + +### 6.3 Aprovação (`ResellerRegistrations.jsx`, feature `revendas_cadastro`) + +Diferente do cliente, aqui **há botões de transição dedicados** (não só um select genérico): +- Em `pendente_validacao`: **Aprovar** (→ `ativo`) ou **Bloquear** (→ `bloqueado`). +- Em `ativo`: **Inativar** (→ `inativo`). +- Em `bloqueado`/`inativo`: **Reativar** (→ `ativo`). +- Em `rascunho`: **Reenviar link** (reenvia o e-mail de convite). + +Toda transição passa pelo mesmo `PATCH /reseller-registrations/:id` genérico (corpo +`{registration_status: ...}` — o front só decide qual botão mostrar). + +**Efeito colateral crítico da transição de status** — `syncOperationalReseller(client, +registration, userId)`, chamado sempre que o PATCH inclui `registration_status`: +- Se o novo status é `'ativo'`: + - Se `registration.reseller_id` já existe → `UPDATE resellers SET name=company_name, + document=cnpj, contact_email=email, active=true`. + - Senão → `INSERT INTO resellers (...) VALUES (...)`, e grava o novo id de volta em + `reseller_registrations.reseller_id`. +- Se o novo status **não** é `'ativo'` (bloqueado/inativo) e `reseller_id` existe → `UPDATE + resellers SET active = false`. **Nunca exclui** a linha de `resellers` (pode já estar + referenciada em orçamentos). + +Ou seja: **a aprovação de um `reseller_registrations` cria/reativa automaticamente a linha +operacional em `resellers`** (a mesma tabela usada em Orçamentos/usuários), e desativá-lo +desativa a revenda operacional também. Isso é o ponto de junção entre o módulo de aprovação +(Fase 1, cadastro) e o módulo operacional pré-existente. + +### 6.4 Geração de documentos e assinatura (pós-aprovação) + +Quando `registration_status === 'ativo'`, o backoffice pode clicar "Gerar Contrato" (finder) ou +"Gerar Contrato + Termo" (recorrente). Isso chama um pipeline de geração de PDF via template +publicado (`base44.documents.generate({ template_key, reseller_registration_id })` → +Chromium/HTML no backend — módulo de Templates de Documento, fora deste escopo), baixa o PDF +gerado e o arquiva via `POST /reseller-registrations/:id/attachments` (sempre `purpose: +'signature'`). Documentos arquivados então alimentam "Criar Envelope de Assinatura" +(mesmo componente reaproveitado do fluxo de cliente, módulo de assinatura eletrônica — fora +do escopo). + +--- + +## 7. Gestão de revendas existentes (`resellers.routes.js` / `Resellers.jsx`) + +CRUD simples e direto, **exclusivo de `requireAdmin`** (roles `admin`/`super_admin` — nota: +mais permissivo que o cadastro/aprovação, que usa a feature `revendas_cadastro`): +- `GET /resellers` — lista com filtros por `id`/`active`/`name`, ordenável por + `created_date`/`updated_date`/`name` (`auth` simples, qualquer usuário autenticado pode + listar — usado por outras telas, ex: seletor de revenda em Orçamentos). +- `POST /resellers` — cria (`name` obrigatório; `document`, `contact_email`, `active` default + `true`). +- `PATCH /resellers/:id` — atualiza campos (todos `COALESCE`, ou seja, PATCH parcial natural). +- `DELETE /resellers/:id` — hard delete, sem checagem de vínculo no backend (a tela + `Resellers.jsx` bloqueia no front se houver usuários vinculados, mas isso é só UX — a API + não impede). + +**Vínculo com quotes**: `quotes.reseller_id` é `NOT NULL` — toda oferta pertence +obrigatoriamente a uma revenda (a do vendedor que a criou, tipicamente `req.user.reseller_id`, +fora do escopo deste documento mas citado para contexto). + +**Vínculo com usuários**: `users.reseller_id` (nullable) — a tela `Resellers.jsx` permite +atribuir/reatribuir cada usuário do sistema a uma revenda via `