sass-imobiliaria/.specify/features/006-client-area/data-model.md

6.9 KiB

Data Model: Área do Cliente (Feature 006)

Date: 2026-04-13 Depends On: Feature 005 — client_users table (ClientUser model)


Entidades Novas

SavedProperty

Coluna Tipo SQLAlchemy Nullable Restrições
id UUID(as_uuid=True) NOT NULL PK, default=uuid4
user_id UUID(as_uuid=True) NOT NULL FK → client_users.id ON DELETE CASCADE
property_id UUID(as_uuid=True) NULL FK → properties.id ON DELETE SET NULL
created_at DateTime NOT NULL server_default=now()

Unique constraint: (user_id, property_id)uq_saved_property_user_property

Relacionamentos:

  • user → ClientUser (lazy="joined")
  • property → Property (lazy="joined") — usado para retornar detalhes do imóvel na rota favorites

Lógica de remoção: se o imóvel for deletado (ON DELETE SET NULL), property_id vira NULL; o registro SavedProperty é mantido para não perder histórico. A rota GET /me/favorites filtra registros onde property.is_active = True ou exibe badge "Imóvel indisponível".


VisitRequest

Coluna Tipo SQLAlchemy Nullable Restrições
id UUID(as_uuid=True) NOT NULL PK, default=uuid4
user_id UUID(as_uuid=True) NULL FK → client_users.id ON DELETE SET NULL
property_id UUID(as_uuid=True) NULL FK → properties.id ON DELETE SET NULL
message Text NOT NULL
status VARCHAR(20) NOT NULL default='pending'; valores: pending, confirmed, cancelled, completed
scheduled_at DateTime NULL Preenchido pelo admin ao confirmar
created_at DateTime NOT NULL server_default=now()

Relacionamentos:

  • user → ClientUser (lazy="select")
  • property → Property (lazy="joined") — para retornar PropertyBrief embutido

Transições de status:

pending → confirmed (admin, com scheduled_at)
pending → cancelled (admin)
confirmed → completed (admin)
confirmed → cancelled (admin)

(Transições não são validadas em código no MVP — qualquer valor do enum é aceito via API admin)


Boleto

Coluna Tipo SQLAlchemy Nullable Restrições
id UUID(as_uuid=True) NOT NULL PK, default=uuid4
user_id UUID(as_uuid=True) NULL FK → client_users.id ON DELETE SET NULL
property_id UUID(as_uuid=True) NULL FK → properties.id ON DELETE SET NULL
description String(200) NOT NULL
amount Numeric(12, 2) NOT NULL
due_date Date NOT NULL
status VARCHAR(20) NOT NULL default='pending'; valores: pending, paid, overdue
url String(500) NULL Link externo do boleto/PDF
created_at DateTime NOT NULL server_default=now()

Relacionamentos:

  • user → ClientUser (lazy="select")
  • property → Property (lazy="joined") — para exibir imóvel vinculado na listagem

Entidades Existentes com Impacto

ClientUser (Feature 005 — pré-requisito)

Nenhuma alteração de schema. Relacionamentos inversos adicionados opcionalmente:

saved_properties = db.relationship("SavedProperty", backref="user", ...)  # opcional
visit_requests   = db.relationship("VisitRequest", ...)                    # opcional
boletos          = db.relationship("Boleto", ...)                          # opcional

(Relacionamentos inversos são adicionados nos novos modelos via backref, não em ClientUser diretamente)

Property (existente)

Nenhuma alteração de schema. Relacionamentos inversos são implícitos via backref nos novos modelos.


Migração Alembic

Nome do arquivo: xxxx_add_saved_properties_visit_requests_boletos.py

-- saved_properties
CREATE TABLE saved_properties (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES client_users(id) ON DELETE CASCADE,
    property_id UUID REFERENCES properties(id) ON DELETE SET NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    CONSTRAINT uq_saved_property_user_property UNIQUE (user_id, property_id)
);

-- visit_requests
CREATE TABLE visit_requests (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES client_users(id) ON DELETE SET NULL,
    property_id UUID REFERENCES properties(id) ON DELETE SET NULL,
    message TEXT NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    scheduled_at TIMESTAMP,
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

-- boletos
CREATE TABLE boletos (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES client_users(id) ON DELETE SET NULL,
    property_id UUID REFERENCES properties(id) ON DELETE SET NULL,
    description VARCHAR(200) NOT NULL,
    amount NUMERIC(12, 2) NOT NULL,
    due_date DATE NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    url VARCHAR(500),
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

Downgrade: DROP TABLE boletos; DROP TABLE visit_requests; DROP TABLE saved_properties;


Schemas Pydantic (backend/app/schemas/client_area.py)

Saída

class PropertyBrief(BaseModel):
    id: UUID
    title: str
    slug: str
    model_config = ConfigDict(from_attributes=True)

class SavedPropertyOut(BaseModel):
    property: PropertyDetailOut  # reutiliza schema existente de property.py
    created_at: datetime
    model_config = ConfigDict(from_attributes=True)

class VisitRequestOut(BaseModel):
    id: UUID
    property: PropertyBrief | None
    message: str
    status: str
    scheduled_at: datetime | None
    created_at: datetime
    model_config = ConfigDict(from_attributes=True)

class BoletoOut(BaseModel):
    id: UUID
    description: str
    amount: Decimal
    due_date: date
    status: str
    url: str | None
    model_config = ConfigDict(from_attributes=True)

Entrada

class FavoriteIn(BaseModel):
    property_id: UUID

class VisitStatusIn(BaseModel):
    status: Literal["pending", "confirmed", "cancelled", "completed"]
    scheduled_at: datetime | None = None

class BoletoCreateIn(BaseModel):
    user_id: UUID
    property_id: UUID | None = None
    description: str
    amount: Decimal
    due_date: date
    url: str | None = None

Tipos TypeScript (frontend/src/types/clientArea.ts)

export interface VisitRequest {
  id: string;
  property: { id: string; title: string; slug: string } | null;
  message: string;
  status: "pending" | "confirmed" | "cancelled" | "completed";
  scheduled_at: string | null;
  created_at: string;
}

export interface Boleto {
  id: string;
  description: string;
  amount: number;
  due_date: string;
  status: "pending" | "paid" | "overdue";
  url: string | null;
}

export interface ComparisonState {
  properties: Property[];  // max 3 — Property importado de types/property.ts
}