Pular para conteúdo

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 registros PENDING da composição ativa da garantidora, e o worker fecha cada um. O catálogo é da plataforma (credit_catalog_items, chave natural code; credit_profile_items diz 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, com effective_from) e o extrato sincronizado da API Full (provider_statement_entries, conciliado com a consulta que gerou cada débito por matched_credit_check_id). Uma consulta pode reaproveitar outra recente (reused_from_id, dentro de credit_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ó as CALENDAR futuras ainda pendentes.
  • Tipo, canal e encargo (spec 0054) — cada cobrança diz de que é feita (fee_type, com a decomposição setup_amount + guarantee_amount = amount) e por onde o dinheiro entra (channel, gravado quando a parcela nasce, a partir de partner_agencies.collects_monthly e tenant_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 é um documents com billing_id. A marcação OVERDUE é 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_PIX daquele mês; a tabela agency_settlements guarda 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 em agency_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ência TO_AGENCY (commission_settlement_id).
  • Mensalidade por tipo de tomador (spec 0057) — tenant_settings tem 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, com guarantee_rate_set_by_id e guarantee_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_id mais o papel AGENCY_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:

  1. FK recebida no corpo da requisição — um property_id ou renter_id que vem no payload precisa ser resolvido no escopo do solicitante antes de gravar. Isso é resolve_in_scope, em app/services/scoping.py (spec 0034): sem ele, nada impediria gravar uma análise apontando para um imóvel de outro tenant.
  2. 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.
  3. ADMIN_GLOBAL nã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).