feat: add full project - backend, frontend, docker, specs and configs

This commit is contained in:
MatheusAlves96 2026-04-20 23:59:45 -03:00
parent b77c7d5a01
commit e6cb06255b
24489 changed files with 61341 additions and 36 deletions

View 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
}
```