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,189 @@
# REST API Contracts: Property Detail Page (004)
**Feature**: `004-property-detail-page`
**Date**: 2026-04-13
**Base URL**: `/api/v1`
---
## GET /properties/{slug}
Retorna o detalhe completo de um imóvel ativo pelo slug.
### Request
```
GET /api/v1/properties/{slug}
Content-Type: application/json
Auth: não requerida
```
**Path parameters**:
| Param | Tipo | Descrição |
|-------|------|-----------|
| `slug` | string `[a-z0-9-]+` | Slug único do imóvel |
### Response 200 OK
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Apartamento 3 quartos no Centro",
"slug": "apartamento-3-quartos-centro-123",
"code": "AP-00042",
"description": "Excelente apartamento com vista para o jardim...",
"address": "Rua das Flores, 100, Centro",
"price": "850000.00",
"condo_fee": "650.00",
"type": "venda",
"bedrooms": 3,
"bathrooms": 2,
"parking_spots": 2,
"area_m2": 95,
"is_featured": true,
"subtype": { "id": 10, "name": "Apartamento", "slug": "apartamento" },
"city": { "id": 5, "name": "Franca", "slug": "franca", "state": "SP" },
"neighborhood": { "id": 12, "name": "Jardim São Luiz", "slug": "jardim-sao-luiz" },
"photos": [
{ "url": "https://...", "alt_text": "Sala de estar", "display_order": 0 },
{ "url": "https://...", "alt_text": "Quarto principal", "display_order": 1 }
],
"amenities": [
{ "id": 3, "name": "Aceita animais", "slug": "aceita-animais", "group": "caracteristica" },
{ "id": 7, "name": "Piscina", "slug": "piscina", "group": "lazer" }
]
}
```
**Notas do schema**:
- `price` e `condo_fee` são strings de decimal (ex: `"850000.00"`) — use `parseFloat()` no frontend
- `type` é o campo canônico (equivale a `listing_type` na documentação da spec)
- `photos` ordenadas por `display_order` ASC (índice 0 = foto principal do carrossel)
- `code` e `description` são `null` se não preenchidos no cadastro
- `address` é `null` se não preenchido
### Response 404 Not Found
Imóvel inexistente **ou** `is_active = false`.
```json
{ "error": "Imóvel não encontrado" }
```
> Retornar 404 (não 403) para imóveis inativos evita vazar informação sobre existência.
---
## POST /properties/{slug}/contact
Registra um lead de contato vinculado ao imóvel identificado pelo slug.
### Request
```
POST /api/v1/properties/{slug}/contact
Content-Type: application/json
Auth: não requerida
```
**Path parameters**:
| Param | Tipo | Descrição |
|-------|------|-----------|
| `slug` | string | Slug do imóvel para o qual o contato é enviado |
**Request body**:
```json
{
"name": "João Silva",
"email": "joao@email.com",
"phone": "(16) 99999-0000",
"message": "Tenho interesse no imóvel, gostaria de agendar uma visita."
}
```
**Campo** | **Tipo** | **Obrigatório** | **Restrições**
----------|----------|-----------------|---------------
`name` | string | sim | 2150 caracteres
`email` | string | sim | formato e-mail válido (RFC 5322)
`phone` | string | não | máximo 20 caracteres
`message` | string | sim | 102000 caracteres
### Response 201 Created
```json
{
"id": 88,
"message": "Mensagem enviada com sucesso!"
}
```
### Response 404 Not Found
Slug não encontrado ou imóvel inativo.
```json
{ "error": "Imóvel não encontrado" }
```
### Response 422 Unprocessable Entity
Falha de validação Pydantic.
```json
{
"error": "Dados inválidos",
"details": {
"email": ["E-mail inválido"],
"message": ["Campo obrigatório"]
}
}
```
---
## Comportamentos Implícitos
| Situação | Comportamento |
|----------|--------------|
| `slug` com caracteres inválidos (maiúsculas, /, etc.) | 404 (SQLAlchemy não encontra correspondência) |
| `property_id` na tabela `contact_leads` quando imóvel é deletado | SET NULL (lead preservado) |
| Duplo envio de formulário | Dois registros criados (UI bloqueia durante `submitting`) |
| `photos` vazia | Array `[]` retornado; frontend exibe placeholder |
| `amenities` vazia | Array `[]` retornado; frontend omite seção de amenidades |
---
## Integração Frontend
### `getProperty(slug: string): Promise<PropertyDetail>`
```typescript
// services/properties.ts
export async function getProperty(slug: string): Promise<PropertyDetail> {
const response = await api.get<PropertyDetail>(`/properties/${slug}`)
return response.data
}
```
- `404` → Axios lança `AxiosError` com `status 404` → frontend exibe estado "não encontrado"
- Outros erros → propagar para tratamento genérico
### `submitContactForm(slug: string, data: ContactFormData): Promise<{ id: number; message: string }>`
```typescript
// services/properties.ts
export async function submitContactForm(
slug: string,
data: ContactFormData,
): Promise<{ id: number; message: string }> {
const response = await api.post<{ id: number; message: string }>(
`/properties/${slug}/contact`,
data,
)
return response.data
}
```
- `201` → retorna `{ id, message }`
- `422``AxiosError` com `response.data.details` disponível para mapear erros por campo
- `5xx` → exibir mensagem genérica sem apagar dados do formulário