Componentes internos (C4 nível 3)¶
apps/api — camadas¶
Estrita, dependências fluem só para baixo. A regra pura de negócio, sem I/O, desce para domain/:
flowchart TB
Router["Router (api/v1/routers/)\nvalida input via Pydantic, porteiro\nrequire_permission — fino, sem regra"]
Service["Service (services/)\nregra de negócio, transição de estado,\nescopo (scoping.py), log de auditoria"]
Domain["Domain (domain/)\nregra pura, sem sessão nem HTTP —\ncanal, encargo, mensalidade, adesão,\nresumo e acerto da cobrança"]
Repository["Repository (repositories/)\nBaseRepository[T] — CRUD assíncrono\ngenérico, filtro de tenant automático"]
Model["Model (models/)\nSQLAlchemy ORM, um arquivo\npor entidade de domínio"]
Provider["Integrações (services/credit_check_provider/,\ncep_service, email_service)\nclientes HTTP/SMTP de terceiros"]
Router --> Service
Service --> Domain
Service --> Repository --> Model
Service --> Provider Um só router de topo (app/api/v1/__init__.py) agrega os 27 módulos de api/v1/routers/ sob o prefixo /api/v1. Os que entraram desde setembro: agency_settlements (acerto com a imobiliária, spec 0055), partner_agency_users (equipe da imobiliária, spec 0051), credit_catalog (catálogo de consultas, do ADMIN_GLOBAL) e credit_finance (extrato e custo das consultas, spec 0052); o billing, que já existia, ganhou a cobrança por tipo e canal, a baixa com encargo e a planilha da competência (spec 0054). A leitura das novidades de versão (spec 0044) não tem router próprio: são POST /auth/me/version-reads e PATCH /auth/me/last-seen-version.
Transação por requisição — toda rota usa CommitRoute (app/core/transaction.py, fix 0003): o handler roda, a sessão é comitada e só então a resposta sai. O que precisa avisar outro processo — hoje, enfileirar a consulta de crédito para o worker — é registrado com depois_do_commit e roda logo após o COMMIT, porque antes dele o worker não encontraria a linha. tests/test_transaction_boundary.py falha se um router novo esquecer a classe.
Mixins de modelo (app/db/base.py): UUIDMixin, TimestampMixin, TenantMixin, SoftDeleteMixin — compostos por model conforme a necessidade (ver data-model.md).
Autorização é resolvida em core/dependencies.py: require_permission(recurso, ação), require_any_permission(...), require_all_permissions(...) e, para o punhado de telas fechadas por papel, require_system_role(...). As permissões têm o formato resource:action e a matriz dos papéis de sistema vive em app/db/rbac_seed.py. Sete papéis de sistema: ADMIN_GLOBAL > ADMIN_TENANT > ANALYST > OPERATOR > VIEWER, mais os dois do canal, escopados ao partner_agency_id — AGENCY_USER e AGENCY_MANAGER (o AGENCY_USER mais partner_agency_users:manage, spec 0051). A garantidora pode ter perfis customizados além desses.
Escopo — services/scoping.py responde de que garantidora e de que imobiliária o usuário enxerga registros, e resolve dentro desse escopo toda FK que chega no corpo da requisição (spec 0034). Ver data-model.md.
Segredo por tenant — o token da API Full de cada garantidora é guardado cifrado (Fernet, core/crypto.py, chave TENANT_SECRETS_KEY), porque a aplicação precisa recuperá-lo em claro para chamar o provedor.
O worker (app/worker.py)¶
Mesmo código e mesma imagem da API, outro processo. Ele reusa os services — não há camada própria:
flowchart LR
Sub["CreditCheckService\n(submissão, reexecução,\nitem avulso)"] -->|"depois_do_commit →\nenqueue_credit_check"| Q[("Redis")]
Q --> Job["run_credit_check_job"]
Job --> CW["credit_check_worker_service\n.complete_credit_check"]
CW --> AF["ApiFullClient\n(nunca lança: falha vira FAILED)"]
Cron6["cron 6h\ndaily_credit_finance_job"] --> Ret["credit_retention_service\n(purga do payload bruto, 12 meses)"]
Cron6 --> Sync["StatementSyncService +\nCreditFinanceService\n(extrato e preço, por tenant)"]
Cron7["cron 7h\ndaily_billing_overdue_job"] --> Ov["billing_overdue_service\n(marca OVERDUE, por tenant)"] Os jobs agendados abrem uma sessão e uma transação por garantidora: a falha de uma vai para o log com o tenant_id e não impede as outras. O fuso é America/Sao_Paulo.
apps/web — Refine sobre uma navegação própria¶
flowchart TB
App["App.tsx\nliga dataProvider, authProvider,\nrouterProvider, resources e rotas"]
Providers["providers/\ndata.ts (REST /api/v1), auth.ts (JWT),\napi-fetch.ts, i18n.ts"]
Nav["components/navigation/nav-groups.tsx\nbuildNavGroups / buildAdminNavGroups —\nfonte única do menu"]
Sider["custom-sider (desktop)\nmobile-shell (gaveta)\napp-layout (casca)"]
Pages["pages/<recurso>/\nList · Create/Edit · Show"]
Admin["admin/\npainel do ADMIN_GLOBAL (/admin)"]
Versao["components/versao/ + versoes/*.md\nnovidades da versão (specs 0043, 0044)"]
Shared["components/shared/\nFilterBar, RecordCard, FormPanel,\nDocumentsPanel, CreditCheckGroup…"]
DS["design-system/\ntema MUI + tokens CSS + .prompt.md"]
App --> Providers
App --> Sider --> Nav
App --> Pages
App --> Admin
Sider -->|"ícone na barra (desktop),\nlinha na gaveta (mobile)"| Versao
Pages --> Shared
Admin --> Shared
Shared --> DS O menu tem fonte única: nav-groups.tsx alimenta a sider desktop, a gaveta do mobile e o layout do admin; cada item declara a permissão (ou o papel) que o habilita, e o item some para quem não a tem. O custom-sider só consome essa lista — editar o menu nele deixaria a gaveta do celular para trás. Consequência que já causou bug mais de uma vez: módulo novo entra em dois lugares — App.tsx (resources[] + rota) e nav-groups.tsx (item no grupo); esquecer o segundo faz a rota funcionar com o item invisível. resources do Refine e menu são listas independentes: Ajuda, Novidades e Minha Imobiliária são rotas sem resource.
O inventário de telas — todo item de menu, do tenant e do admin, mais as telas de fora do menu — está em apps/web/CLAUDE.md, seção "Inventário de telas".
Base de path configurável: VITE_BASE_PATH decide se o app vive em /app (local e dev-remote) ou na raiz (produção); o base do Vite e o basename do React Router seguem esse valor.
Versão e novidades — a versão publicada chega ao bundle como VITE_APP_VERSION (build arg APP_VERSION, spec 0043), e as notas de cada versão são os arquivos de src/versoes/X.Y.Z.md. O que cada usuário já leu é guardado na API (user_version_reads e users.last_seen_version).
O design system deixou de ser pacote git externo na spec 0017 — ver ../decisions/ADR-0002-design-system-internalizado.md.