sass-imobiliaria/specs/023-ux-melhorias-imoveis/contracts/properties-api.md

7 KiB

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.

{
  "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

# 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

# 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

// 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
// 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)