217 lines
6.9 KiB
Markdown
217 lines
6.9 KiB
Markdown
# 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
|
|
}
|
|
```
|