# 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` ```typescript // services/properties.ts export async function getProperty(slug: string): Promise { const response = await api.get(`/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