feat: add full project - backend, frontend, docker, specs and configs
This commit is contained in:
parent
b77c7d5a01
commit
e6cb06255b
24489 changed files with 61341 additions and 36 deletions
217
.specify/features/006-client-area/data-model.md
Normal file
217
.specify/features/006-client-area/data-model.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# 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
|
||||
}
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue