# 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: ```python 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` ```sql -- 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 ```python 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 ```python 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) ```typescript 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 } ```