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
189
.specify/features/004-property-detail-page/contracts/rest.md
Normal file
189
.specify/features/004-property-detail-page/contracts/rest.md
Normal 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 | 2–150 caracteres
|
||||
`email` | string | sim | formato e-mail válido (RFC 5322)
|
||||
`phone` | string | não | máximo 20 caracteres
|
||||
`message` | string | sim | 10–2000 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue