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
}