Modelo de dados e isolamento multi-tenant¶
As tabelas abaixo são as de apps/api/app/models/ hoje. O vocabulário de negócio de cada entidade (quem é garantidora, imobiliária, inquilino, o que é Pacote Locatício) está no glossário de produto; esta página cobre a estrutura: tabelas, chaves, e o mecanismo técnico que sustenta o isolamento.
Entidades de negócio¶
erDiagram
tenants ||--|| tenant_settings : "parâmetros"
tenants ||--o{ users : ""
tenants ||--o{ partner_agencies : "parceiras"
tenants ||--o{ renters : ""
tenants ||--o{ properties : ""
tenants ||--o{ credit_analyses : ""
partner_agencies ||--o{ users : "AGENCY_USER / AGENCY_MANAGER"
partner_agencies |o--o{ renters : "escopo"
partner_agencies |o--o{ properties : "escopo"
partner_agencies |o--o{ credit_analyses : "escopo"
partner_agencies |o--o{ guarantees : "escopo"
partner_agencies ||--o{ agency_settlements : "um por competência"
properties ||--o{ credit_analyses : ""
credit_analyses ||--o{ credit_analysis_participants : "LESSEE / CO_RESPONSIBLE"
renters ||--o{ credit_analysis_participants : ""
credit_analyses ||--o{ credit_analysis_status_history : "log imutável"
credit_analyses ||--o{ credit_analysis_change_log : "o que mudou"
credit_analyses ||--o| decisions : "parecer"
credit_analyses ||--o{ credit_checks : "consultas de crédito"
renters ||--o{ credit_checks : "consultado"
credit_analyses ||--o| guarantees : "emitida na aprovação"
guarantees ||--o{ billings : "parcelas"
agency_settlements |o--o{ billings : "agency_settlement_id"
agency_settlements |o--o{ billings : "commission_settlement_id"
agency_settlements ||--o{ agency_settlement_transfers : "movimentações"
documents }o--o| renters : ""
documents }o--o| properties : ""
documents }o--o| credit_analyses : ""
documents }o--o| guarantees : ""
documents }o--o| billings : "comprovante"
documents }o--o| agency_settlements : ""
documents }o--o| document_types : ""
billings {
uuid guarantee_id FK
date due_date
decimal amount
enum status "PENDING PAID OVERDUE CANCELLED"
text reference_month "YYYY-MM"
enum fee_type "SETUP GUARANTEE SETUP_AND_GUARANTEE MISC"
enum channel "GUARANTOR_BOLETO GUARANTOR_PIX AGENCY_PIX"
int installment_number "5 de 5/12"
int installment_total
enum origin "CALENDAR MANUAL"
uuid agency_settlement_id FK
uuid commission_settlement_id FK
timestamptz deleted_at
}
agency_settlements {
uuid partner_agency_id FK
text reference_month
enum status "OPEN CLOSED"
string invoice_number
}
agency_settlement_transfers {
uuid settlement_id FK
date received_on
decimal amount
enum direction "FROM_AGENCY TO_AGENCY"
}
credit_checks {
uuid credit_analysis_id FK
uuid renter_id FK
string catalog_item_code FK
enum status "PENDING RUNNING SUCCESS FAILED SKIPPED"
json raw_response "purgado após 12 meses"
json normalized
uuid reused_from_id FK
}
tenant_settings {
decimal pf_guarantee_rate_percent "e pf_min_ / pf_max_"
decimal pj_guarantee_rate_percent "e pj_min_ / pj_max_"
enum collection_method "BOLETO PIX"
enum initial_fee_type "FIXED PERCENTAGE NONE"
decimal commission_percent
text credit_check_token "cifrado"
enum credit_check_profile
} O que cada bloco recente acrescentou:
- Consultas de crédito (specs 0050 e 0052) —
credit_checksé uma linha por item consultado (o consultado é sempre o locatário principal da análise): a submissão da análise cria os registrosPENDINGda composição ativa da garantidora, e o worker fecha cada um. O catálogo é da plataforma (credit_catalog_items, chave naturalcode;credit_profile_itemsdiz que itens cada perfil —ECONOMICO,COMPLETO— inclui). Por garantidora: a composição escolhida (tenant_credit_check_selections), o preço vigente por item (tenant_credit_catalog_prices, comeffective_from) e o extrato sincronizado da API Full (provider_statement_entries, conciliado com a consulta que gerou cada débito pormatched_credit_check_id). Uma consulta pode reaproveitar outra recente (reused_from_id, dentro decredit_check_reuse_window_days). - Cobrança automática (spec 0048) — a assinatura da garantia gera o calendário inteiro de
billings(origin = CALENDAR); a cobrança lançada à mão éMANUAL. Encerrar a garantia cancela só asCALENDARfuturas ainda pendentes. - Tipo, canal e encargo (spec 0054) — cada cobrança diz de que é feita (
fee_type, com a decomposiçãosetup_amount + guarantee_amount = amount) e por onde o dinheiro entra (channel, gravado quando a parcela nasce, a partir departner_agencies.collects_monthlyetenant_settings.collection_method). A baixa registra a data em que o dinheiro entrou (paid_on), o canal efetivo (payment_channel), a distribuição do valor (setup_paid,guarantee_paid,misc_paid,charge_paid— este último, multa e juros) e o desconto. O comprovante é umdocumentscombilling_id. A marcaçãoOVERDUEé do job das 7h, só nos canais da garantidora. - Acerto com a imobiliária (spec 0055) — o acerto é o par (imobiliária, competência). Ele é derivado das cobranças
AGENCY_PIXdaquele mês; a tabelaagency_settlementsguarda só o que não se deriva (estado, quem fechou, nota fiscal) e nasce na primeira escrita. As declarações da imobiliária ficam na própria cobrança (agency_outcome,agency_paid_on,agency_late_fee,agency_declared_by), e as transferências emagency_settlement_transfers. - Taxa de adesão e comissão (spec 0056) — a imobiliária pode ter adesão e comissão próprias (
partner_agencies.initial_fee_custom,initial_fee_*,commission_percent), senão vale a da garantidora (tenant_settings.initial_fee_*,commission_percent,commission_tax_percent). A comissão é congelada na baixa da cobrança (billings.commission_*) e paga no acerto como transferênciaTO_AGENCY(commission_settlement_id). - Mensalidade por tipo de tomador (spec 0057) —
tenant_settingstem faixa sugerida, mínima e máxima separada para PF (pf_*) e PJ (pj_*). A taxa escolhida na aprovação fica no parecer (decisions.approved_guarantee_rate_percent, comguarantee_rate_set_by_ideguarantee_rate_set_at); sem ela, vale a sugerida do tipo do tomador, e é essa taxa em vigor que a garantia recebe. - Usuários da imobiliária (spec 0051) — não há tabela nova: é
users.partner_agency_idmais o papelAGENCY_MANAGER.
Documento pertence a uma entidade só. documents tem seis FKs de dono mutuamente exclusivas — renter_id, property_id, credit_analysis_id, guarantee_id, billing_id (comprovante, spec 0054) e agency_settlement_id (spec 0055). O arquivo é guardado no próprio banco (content, LargeBinary). document_types e credit_analysis_document_requirements dizem o que é exigido por área.
Tabelas de plataforma e de apoio¶
| Tabela | Para quê |
|---|---|
roles, permissions, role_permissions, user_roles | RBAC. roles.tenant_id nulo e is_system para os papéis de sistema; perfis customizados levam o tenant |
tenant_invites, verification_codes | convite de garantidora (do ADMIN_GLOBAL), verificação de e-mail, convite de usuário e recuperação de senha |
user_version_reads, users.last_seen_version | o que cada usuário já leu das novidades de versão (specs 0043 e 0044) |
pendency_types | o tipo da pendência ao devolver uma análise, referenciado por credit_analysis_status_history.pendency_type_id (spec 0037) |
numbering_sequences | numeração legível por garantidora e ano (CREDIT_ANALYSIS, GUARANTEE) |
audit_logs | log de auditoria por entidade e ação |
contract_templates, channel_integrations | existem no schema, sem router nem service que as use (só o purge de tenant as apaga) |
Isolamento por tenant — onde ele acontece e onde não acontece sozinho¶
BaseRepository[T] injeta filtro de tenant_id automaticamente em toda query de leitura e listagem. Isso cobre a maioria dos casos, mas três coisas que ele não resolve:
- FK recebida no corpo da requisição — um
property_idourenter_idque vem no payload precisa ser resolvido no escopo do solicitante antes de gravar. Isso éresolve_in_scope, emapp/services/scoping.py(spec 0034): sem ele, nada impediria gravar uma análise apontando para um imóvel de outro tenant. - 404 vs. 403 — registro de outro tenant, de outra imobiliária ou removido responde 404 (não existe, do ponto de vista de quem pediu), nunca 403 (que informaria que o registro existe em outro lugar). 403 é reservado para ação não permitida dentro do próprio escopo.
ADMIN_GLOBALnão tem filtro nenhum — o papel existe justamente para atravessar tenants, mas só em dado de plataforma (garantidoras, convites, catálogo de consultas). Aplicar o mesmo alcance a dado de negócio do cliente (inquilino, análise, garantia) é a fronteira que specs de segurança (0034, 0035) tratam como crítica.
Isolamento por imobiliária (AGENCY_USER, AGENCY_MANAGER)¶
Um segundo eixo de escopo, dentro do mesmo tenant: partner_agency_id corta renters, properties, credit_analyses, guarantees e users, e é derivado do usuário autenticado, nunca aceito do corpo da requisição. Documento herda o escopo do dono — nem metadado, nem download, nem contagem vazam para fora da imobiliária. O AGENCY_MANAGER administra só os usuários da própria imobiliária.
Remoção lógica e paginação¶
SoftDeleteMixin marca deleted_at em vez de apagar. Desde a spec 0049, as seis entidades removíveis pela tela — imobiliária, inquilino, imóvel, análise de crédito, garantia e cobrança — têm também restaurar (POST .../{id}/restore), com janela de 60 dias a partir de deleted_at (app/core/soft_delete.py); passado o prazo o registro continua no banco, só não pode mais ser restaurado. Não há exclusão física. documents, document_types, pendency_types e contract_templates também têm o mixin; users tem deleted_at próprio, fora dele.
O BaseRepository exclui deleted_at preenchido nas listagens e contagens; o get_by_id não filtra sozinho — quem resolve um ID trata o removido como inexistente (é o que resolve_in_scope faz). Toda listagem paginada ordena por created_at desc, id desc: sem essa ordem, o PostgreSQL não garante posição estável entre páginas quando um registro é editado — ver ../product/business-rules.md ("Paginação — sempre com ORDER BY").
Migrations¶
Alembic, com downgrade obrigatório desde a spec 0028 — é o caminho de volta de um deploy ruim em produção, não apenas higiene. Em produção a migration não roda no boot: é passo do /ship, com backup antes (ver deployment.md). apps/api/CLAUDE.md documenta o comando exato (o alembic.ini fica em alembic/, precisa do flag -c).