sass-imobiliaria/.specify/features/004-property-detail-page/data-model.md

181 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Data Model: Property Detail Page (004)
**Feature**: `004-property-detail-page`
**Date**: 2026-04-13
---
## Entidades Modificadas
### Property (existente — colunas adicionadas)
| Campo | Tipo SQL | Tipo Python | Nullable | Notas |
|-------|----------|-------------|----------|-------|
| `id` | UUID PK | `UUID` | não | existente |
| `title` | VARCHAR(200) | `str` | não | existente |
| `slug` | VARCHAR(220) UNIQUE | `str` | não | existente |
| **`code`** | **VARCHAR(30) UNIQUE** | **`str \| None`** | **sim** | **novo** — ex: `"AP-00042"` |
| **`description`** | **TEXT** | **`str \| None`** | **sim** | **novo** — descrição narrativa |
| `address` | VARCHAR(300) | `str \| None` | sim | existente |
| `price` | NUMERIC(12,2) | `Decimal` | não | existente |
| `condo_fee` | NUMERIC(10,2) | `Decimal \| None` | sim | existente |
| `type` | ENUM(venda,aluguel) | `Literal["venda","aluguel"]` | não | existente |
| `subtype_id` | INT FK → property_types | `int \| None` | sim | existente |
| `bedrooms` | INT | `int` | não | existente |
| `bathrooms` | INT | `int` | não | existente |
| `parking_spots` | INT | `int` | não | existente |
| `area_m2` | INT | `int` | não | existente |
| `city_id` | INT FK → cities | `int \| None` | sim | existente |
| `neighborhood_id` | INT FK → neighborhoods | `int \| None` | sim | existente |
| `is_featured` | BOOLEAN | `bool` | não | existente |
| `is_active` | BOOLEAN | `bool` | não | existente |
| `created_at` | TIMESTAMP | `datetime` | não | existente |
**Relacionamentos existentes** (sem mudança):
- `photos`: 1:M → `PropertyPhoto` (cascade delete-orphan, order by `display_order`)
- `subtype`: M:1 → `PropertyType` (joined)
- `city`: M:1 → `City` (joined)
- `neighborhood`: M:1 → `Neighborhood` (joined)
- `amenities`: M:M via `property_amenity`
---
## Entidades Novas
### ContactLead (novo)
Registra cada solicitação de contato feita por um visitante em relação a um imóvel.
| Campo | Tipo SQL | Tipo Python | Nullable | Restrições |
|-------|----------|-------------|----------|------------|
| `id` | SERIAL PK | `int` | não | autoincrement |
| `property_id` | UUID FK → properties | `UUID \| None` | **sim** | ON DELETE SET NULL |
| `name` | VARCHAR(150) | `str` | não | |
| `email` | VARCHAR(254) | `str` | não | |
| `phone` | VARCHAR(20) | `str \| None` | sim | |
| `message` | TEXT | `str` | não | |
| `created_at` | TIMESTAMP WITH TIME ZONE | `datetime` | não | server_default=NOW() |
**Índices**:
- `ix_contact_leads_property_id` em `property_id` (consultas futuras de admin)
- `ix_contact_leads_created_at` em `created_at` (ordenação)
**Relacionamento**:
- `ContactLead.property``Property` (lazy="select", nullable via FK SET NULL)
**Sem cascade delete**: leads são preservados mesmo se o imóvel for deletado (histórico de negócio).
---
## Schemas Pydantic (backend)
### PropertyDetailOut (novo — herda de PropertyOut)
```python
class PropertyDetailOut(PropertyOut):
model_config = ConfigDict(from_attributes=True)
address: str | None
code: str | None
description: str | None
```
Usado exclusivamente pelo endpoint `GET /api/v1/properties/<slug>`.
### ContactLeadIn (novo — input de validação)
```python
class ContactLeadIn(BaseModel):
name: Annotated[str, Field(min_length=2, max_length=150)]
email: EmailStr
phone: Annotated[str | None, Field(max_length=20)] = None
message: Annotated[str, Field(min_length=10, max_length=2000)]
```
### ContactLeadCreatedOut (novo — resposta 201)
```python
class ContactLeadCreatedOut(BaseModel):
id: int
message: str
```
---
## Types TypeScript (frontend)
### PropertyDetail (novo — herda de Property)
```typescript
export interface PropertyDetail extends Property {
address: string | null
code: string | null
description: string | null
}
```
### ContactFormData (novo)
```typescript
export interface ContactFormData {
name: string
email: string
phone: string
message: string
}
```
---
## Estado do React — PropertyDetailPage
```
PropertyDetailPage
├── property: PropertyDetail | null (fetch por slug)
├── notFound: boolean (true se 404)
├── loading: boolean
└── ContactSection
├── formData: ContactFormData
├── submitting: boolean
├── submitStatus: 'idle' | 'success' | 'error'
└── fieldErrors: Partial<Record<keyof ContactFormData, string>>
```
---
## Validações de Estado
| Regra | Onde aplicada |
|-------|---------------|
| `is_active = false` → 404 | Backend (GET /properties/\<slug\>) |
| slug inexistente → 404 | Backend |
| `name` obrigatório, 2150 chars | Backend (Pydantic) + Frontend (HTML validation) |
| `email` formato válido | Backend (EmailStr) + Frontend (type="email") |
| `phone` max 20 chars, opcional | Backend (Pydantic) |
| `message` obrigatório, 102000 chars | Backend (Pydantic) + Frontend |
| `property_id` via slug (nunca do cliente) | Backend |
---
## Transições de Estado do Formulário de Contato
```
idle → [usuário clica Enviar]
↓ validação frontend falha → exibe erros de campo → idle
↓ validação ok → submitting (botão desabilitado)
↓ 201 Created → success (mensagem de confirmação, formulário limpo)
↓ 4xx/5xx → error (mensagem de erro, dados preservados)
error → [usuário edita] → idle
```
---
## Agrupamento de Amenidades
| Valor `group` no BD | Label exibido |
|--------------------|---------------|
| `caracteristica` | Características |
| `lazer` | Lazer |
| `condominio` | Condomínio |
| `seguranca` | Segurança |
Grupos sem amenidade associada ao imóvel **não são renderizados** (FR-F07).