# API Contract — `GET /api/v1/properties` **Feature**: 023-ux-melhorias-imoveis **Versão**: extensão da rota existente — sem breaking changes **Arquivo**: `backend/app/routes/properties.py` --- ## Descrição Rota de listagem paginada de imóveis. Esta feature adiciona dois novos parâmetros query opcionais (`q` e `sort`) à rota existente. Todos os parâmetros existentes são preservados sem alteração. --- ## Request ``` GET /api/v1/properties ``` ### Query Parameters #### Parâmetros novos (adicionados nesta feature) | Parâmetro | Tipo | Obrigatório | Valores | Default | Descrição | |---|---|---|---|---|---| | `q` | `string` | Não | qualquer string, máx 200 chars | — (sem filtro textual) | Busca case-insensitive em `title`, `address`, `code`, `neighborhood.name` | | `sort` | `string` | Não | `relevance` · `price_asc` · `price_desc` · `area_desc` · `newest` | `relevance` | Critério de ordenação dos resultados | #### Parâmetros existentes (preservados) | Parâmetro | Tipo | Descrição | |---|---|---| | `listing_type` | `'venda' \| 'aluguel'` | Tipo de negócio | | `subtype_id` | `integer` | ID do subtipo de imóvel | | `city_id` | `integer` | ID da cidade | | `neighborhood_id` | `integer` | ID do bairro | | `imobiliaria_id` | `integer` | ID da imobiliária | | `price_min` | `number` | Preço mínimo | | `price_max` | `number` | Preço máximo | | `include_condo` | `'true'` | Incluir condomínio no cálculo de preço | | `bedrooms_min` | `integer` | Mínimo de quartos | | `bedrooms_max` | `integer` | Máximo de quartos | | `bathrooms_min` | `integer` | Mínimo de banheiros | | `bathrooms_max` | `integer` | Máximo de banheiros | | `parking_min` | `integer` | Mínimo de vagas | | `parking_max` | `integer` | Máximo de vagas | | `area_min` | `integer` | Área mínima (m²) | | `area_max` | `integer` | Área máxima (m²) | | `amenity_ids` | `string` (lista separada por vírgula) | IDs de comodidades (AND lógico) | | `page` | `integer` | Página atual (default: 1) | | `per_page` | `integer` | Resultados por página (default: 24, max: 48) | --- ## Response ### 200 OK Sem alterações no schema de resposta existente. ```json { "items": [ { "id": "uuid", "title": "Apartamento Jardins 2 quartos", "slug": "apartamento-jardins-2-quartos", "address": "Rua Oscar Freire, 123", "code": "AP-0042", "price": "3500.00", "type": "aluguel", "bedrooms": 2, "bathrooms": 1, "parking_spots": 1, "area_m2": 75, "is_featured": false, "created_at": "2026-04-11T14:30:00", "photos": [ { "url": "/imoveis/ap-0042/foto1.jpg", "alt_text": "Sala de estar" } ], "subtype": { "id": 1, "name": "Apartamento" }, "city": { "id": 1, "name": "São Paulo" }, "neighborhood": { "id": 5, "name": "Jardins" } } ], "total": 42, "page": 1, "per_page": 16, "pages": 3 } ``` ### Erros | Status | Condição | |---|---| | `400` | Não aplicável — parâmetros inválidos são ignorados silenciosamente (comportamento existente) | | `500` | Erro interno do servidor | --- ## Semântica dos Novos Parâmetros ### `q` — Busca Textual - **Campos buscados**: `title`, `address`, `code`, `neighborhood.name` - **Operador**: `ILIKE '%termo%'` (case-insensitive, busca parcial) - **Lógica**: OR entre os campos (`title ILIKE $q OR address ILIKE $q OR ...`) - **Combinação com outros filtros**: AND com todos os filtros existentes - **Sanitização**: `.strip()` + truncamento em 200 chars - **Segurança**: bind parameter do SQLAlchemy ORM — sem risco de SQL injection **Exemplo**: `?q=Jardins&listing_type=aluguel` retorna apenas imóveis de aluguel cujo título, endereço, código ou bairro contenha "Jardins". **Exemplo**: `?q=AP-0042` retorna o imóvel com `code = 'AP-0042'` (ou qualquer imóvel com "AP-0042" no título/endereço). ### `sort` — Ordenação | Valor | `ORDER BY` gerado | Comportamento | |---|---|---| | `relevance` | `property.created_at DESC` | Mais recentes primeiro (comportamento anterior) | | `price_asc` | `property.price ASC` | Menor preço primeiro | | `price_desc` | `property.price DESC` | Maior preço primeiro | | `area_desc` | `property.area_m2 DESC` | Maior área primeiro | | `newest` | `property.created_at DESC` | Igual a `relevance` | | *(valor desconhecido)* | `property.created_at DESC` | Fallback para `relevance` | --- ## Exemplos de Chamada ```bash # Busca por bairro + tipo de negócio ordenado por preço GET /api/v1/properties?q=Jardins&sort=price_asc&listing_type=aluguel&page=1&per_page=16 # Busca por código exato GET /api/v1/properties?q=AP-0042 # Imóveis mais recentes de São Paulo GET /api/v1/properties?sort=newest&city_id=1 # Menor área mínima + maior preço (combinação complexa) GET /api/v1/properties?sort=price_desc&bedrooms_min=3&city_id=1&q=Pinheiros ``` --- ## Implementação Backend ```python # Em backend/app/routes/properties.py — adicionar antes da paginação # ── Busca textual (q) ──────────────────────────────────────────────────────── q = args.get("q", "").strip() if len(q) > 200: q = q[:200] if q: from sqlalchemy import or_ from sqlalchemy.orm import aliased from app.models.location import Neighborhood as NeighborhoodAlias nbh_alias = aliased(NeighborhoodAlias) query = query.outerjoin(nbh_alias, Property.neighborhood_id == nbh_alias.id) pattern = f"%{q}%" query = query.filter(or_( Property.title.ilike(pattern), Property.address.ilike(pattern), Property.code.ilike(pattern), nbh_alias.name.ilike(pattern), )) # ── Ordenação (sort) — SUBSTITUIR o order_by existente ───────────────────── sort = args.get("sort", "relevance") sort_map = { "price_asc": Property.price.asc(), "price_desc": Property.price.desc(), "area_desc": Property.area_m2.desc(), "newest": Property.created_at.desc(), } order_clause = sort_map.get(sort, Property.created_at.desc()) # ── Paginação (existente — apenas mover order_by) ─────────────────────────── props = ( query.order_by(order_clause) # ← substituiu o .order_by(Property.created_at.desc()) fixo .offset((page - 1) * per_page) .limit(per_page) .all() ) ``` --- ## Implementação Frontend ```ts // services/properties.ts — adicionar ao PropertyFilters q?: string sort?: 'relevance' | 'price_asc' | 'price_desc' | 'area_desc' | 'newest' // No getProperties(): if (filters.q?.trim()) params.q = filters.q.trim() if (filters.sort && filters.sort !== 'relevance') params.sort = filters.sort ``` ```ts // PropertiesPage.tsx — filtersFromParams q: get('q') ?? undefined, sort: (get('sort') as SortOption) ?? undefined, // PropertiesPage.tsx — filtersToParams if (filters.q) p.set('q', filters.q) if (filters.sort && filters.sort !== 'relevance') p.set('sort', filters.sort) ```