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
214
specs/023-ux-melhorias-imoveis/contracts/properties-api.md
Normal file
214
specs/023-ux-melhorias-imoveis/contracts/properties-api.md
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
# 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)
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue