16 KiB
Feature Specification: Página de Detalhe do Imóvel
Feature Branch: 004-property-detail-page
Created: 2026-04-13
Status: Draft
Contexto
Página pública /imoveis/<slug> que exibe todas as informações de um imóvel específico: galeria de fotos em carrossel, dados técnicos, preço, diferenciais e formas de contato. É o ponto de conversão do funil — o visitante que chegou via listagem ou link direto deve encontrar tudo que precisa para solicitar uma visita ou mais informações.
User Stories
US1 — Visitante visualiza o imóvel em detalhe (P1)
Given o visitante acessa /imoveis/apartamento-3-quartos-centro-123, When a página carrega com sucesso, Then vê o carrossel de fotos, título, código do imóvel, breadcrumb de localização, estatísticas-chave (quartos, banheiros, vagas, área), caixa de preço com label "Venda" ou "Aluguel", e a descrição completa do imóvel.
Why this priority: É o núcleo da feature — sem visualização nenhum outro story faz sentido.
Independent Test: Acessar /imoveis/<slug> de um imóvel ativo com fotos e verificar que todos os blocos de informação são renderizados corretamente.
Acceptance Scenarios:
- Given um imóvel ativo com 5 fotos e todos os campos preenchidos, When o visitante acessa a URL do imóvel, Then a foto de índice 0 (
display_ordermais baixo) aparece como principal, as demais aparecem como miniaturas, e todas as informações do imóvel são exibidas. - Given o carrossel está exibindo a primeira foto, When o visitante clica na miniatura da terceira foto, Then a foto principal muda para a terceira foto e a miniatura ativa recebe destaque visual.
- Given o carrossel está em foco, When o visitante pressiona a tecla
←ou→, Then a foto principal avança ou recua junto às miniaturas. - Given o visitante está em um dispositivo móvel, When faz swipe horizontal no carrossel, Then a foto principal muda na direção do gesto.
- Given um imóvel do tipo
aluguel, When a página carrega, Then a caixa de preço exibe o label "Aluguel", o valor principal, e — secondo_feenão for nulo — o valor de condomínio separado abaixo. - Given um imóvel do tipo
venda, When a página carrega, Then o label exibe "Venda" e não há linha de condomínio secondo_feefor nulo. - Given o visitante está em desktop, When rola a página para baixo, Then a caixa de preço permanece visível (sticky) ao lado do conteúdo principal.
- Given a página está carregando, When os dados ainda não chegaram, Then esqueletos de carregamento ocupam as áreas de foto, estatísticas e preço (sem layout shift).
US2 — Visitante solicita contato pelo formulário ou WhatsApp (P2)
Given o visitante está na página de detalhe de um imóvel, When rola até a seção de contato, Then vê dois caminhos: botão de WhatsApp com número da imobiliária e formulário de contato (nome, e-mail, telefone, mensagem).
Why this priority: Conversão é o objetivo do negócio. O formulário captura leads que não usam WhatsApp.
Independent Test: Preencher e enviar o formulário; verificar que o lead aparece no banco de dados com property_id correto. Clicar no botão de WhatsApp e verificar que o link wa.me abre com texto pré-preenchido referenciando o imóvel.
Acceptance Scenarios:
- Given o formulário de contato está visível, When o visitante preenche nome, e-mail válido, telefone e mensagem e clica em "Enviar", Then o sistema registra o lead na tabela
contact_leads, exibe mensagem de confirmação "Mensagem enviada com sucesso!", e o formulário é limpo. - Given o formulário está exibido, When o visitante tenta enviar sem preencher nome ou e-mail, Then os campos obrigatórios são destacados com mensagem de erro e o envio é bloqueado no próprio frontend.
- Given o e-mail informado tem formato inválido (ex: "joao@"), When o visitante tenta enviar, Then o campo de e-mail exibe mensagem "E-mail inválido" e o envio é bloqueado.
- Given o botão de WhatsApp está visível, When o visitante clica nele, Then uma nova aba abre com
https://wa.me/<numero>?text=..., onde o texto pré-preenchido menciona o código e o título do imóvel. - Given o backend retorna erro 5xx ao tentar salvar o lead, When o envio falha, Then o formulário exibe mensagem "Erro ao enviar. Tente novamente mais tarde." sem apagar os dados já digitados.
- Given o formulário está sendo enviado, When a requisição está em andamento, Then o botão de envio fica desabilitado com indicador de carregamento para evitar duplo envio.
US3 — Visitante consulta diferenciais e localização (P3)
Given o visitante está na página de detalhe, When rola até as seções de diferenciais e mapa, Then vê a lista de amenidades agrupadas por categoria (características, lazer, condomínio, segurança) e um mapa com marcador na localização do imóvel.
Why this priority: Informações de apoio à decisão — importantes mas não bloqueantes para o MVP mínimo funcional.
Independent Test: Acessar a página de um imóvel com amenidades em múltiplos grupos e verificar que cada grupo tem seu título e checklist. Verificar mapa embutido com o endereço correto.
Acceptance Scenarios:
- Given um imóvel possui amenidades nos grupos "caracteristica", "lazer" e "segurança", When a página carrega, Then cada grupo é exibido como seção distinta com título ("Características", "Lazer", "Segurança") e a lista de amenidades correspondente.
- Given um imóvel não possui nenhuma amenidade cadastrada, When a página carrega, Then a seção de diferenciais não é renderizada (sem seção vazia).
- Given o imóvel possui endereço cadastrado, When a seção de localização é exibida, Then um mapa embutido mostra o pin na localização aproximada do imóvel.
- Given o breadcrumb está exibido, When o visitante clica em "Imóveis", Then é redirecionado para
/imoveis. Clicar na cidade aplica o filtro de cidade na listagem. Clicar no bairro aplica o filtro de bairro.
Edge Cases
- Imóvel não encontrado: Se o
slugnão corresponder a nenhum imóvel na base, o backend retorna404e o frontend exibe página de "Imóvel não encontrado" com link de volta para/imoveis. - Imóvel inativo: Se
is_active = false, o backend retorna404na rota pública (o imóvel não existe para visitantes). Não retornar403para não vazar informação sobre existência. - Sem fotos: Se o imóvel não tiver nenhuma
PropertyPhoto, o carrossel exibe um placeholder visual (imagem genérica de imóvel) sem quebrar o layout. - Uma única foto: O carrossel exibe a foto principal sem strip de miniaturas e sem os botões de navegação.
- Campo
condo_feenulo em aluguel: A linha de condomínio não é renderizada na caixa de preço. - Endereço sem coordenadas precisas: O mapa pode usar geocoding por endereço completo; se falhar, a seção de mapa é omitida silenciosamente.
- Envio duplicado de lead: O backend não deduplica — cada envio gera um novo
ContactLead. O bloqueio de UI durante envio (US2, cenário 6) é suficiente para o MVP. - Slug com caracteres especiais: A rota aceita slugs no formato
[a-z0-9-]+apenas; outros formatos retornam404.
API Contract
Endpoint: GET /api/v1/properties/
Retorna o detalhe completo de um imóvel ativo pelo slug.
Resposta 200 OK:
{
"id": 42,
"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",
"listing_type": "venda",
"bedrooms": 3,
"bathrooms": 2,
"parking_spots": 2,
"area_m2": 95.0,
"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": [
{ "id": 1, "url": "https://...", "alt_text": "Sala de estar", "display_order": 0 },
{ "id": 2, "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" }
]
}
Resposta 404 Not Found (imóvel inexistente ou inativo):
{ "error": "Imóvel não encontrado" }
Endpoint: POST /api/v1/properties//contact
Registra um lead de contato vinculado ao imóvel.
Request body:
{
"name": "João Silva",
"email": "joao@email.com",
"phone": "(16) 99999-0000",
"message": "Tenho interesse no imóvel, gostaria de agendar uma visita."
}
Validações no backend:
name: obrigatório, string, 2–150 caracteresemail: obrigatório, formato de e-mail válidophone: opcional, string, máximo 20 caracteresmessage: obrigatório, string, 10–2000 caracteres
Resposta 201 Created:
{
"id": 88,
"message": "Mensagem enviada com sucesso!"
}
Resposta 404 Not Found (slug não encontrado ou imóvel inativo):
{ "error": "Imóvel não encontrado" }
Resposta 422 Unprocessable Entity (validação falhou):
{
"error": "Dados inválidos",
"details": {
"email": ["E-mail inválido"],
"message": ["Campo obrigatório"]
}
}
Modelos necessários
ContactLead (novo)
Registra cada solicitação de contato feita por um visitante em relação a um imóvel.
| Campo | Tipo | Restrições |
|---|---|---|
id |
SERIAL PK | — |
property_id |
FK → Property | NOT NULL |
name |
VARCHAR(150) | NOT NULL |
email |
VARCHAR(254) | NOT NULL |
phone |
VARCHAR(20) | nullable |
message |
TEXT | NOT NULL |
created_at |
TIMESTAMP WITH TIME ZONE | NOT NULL, default NOW() |
Não há relação de exclusão em cascata com
Property— leads são preservados mesmo se o imóvel for deletado (para histórico de negócio). A FK deve ser SET NULL ou restrita por política — para o MVP: ON DELETE SET NULL é suficiente.
Requisitos Funcionais
Backend
- FR-B01: O sistema DEVE expor
GET /api/v1/properties/<slug>retornando o imóvel ativo com fotos e amenidades aninhadas. - FR-B02: A rota
GET /api/v1/properties/<slug>DEVE retornar404para imóveis comis_active = falseou slug inexistente. - FR-B03: O sistema DEVE expor
POST /api/v1/properties/<slug>/contactque valida o payload com Pydantic e persiste umContactLeadno banco. - FR-B04: A criação de
ContactLeadDEVE usarproperty_idresolvido viaslug; nunca aceitarproperty_iddiretamente do cliente. - FR-B05: As rotas públicas de detalhe e contato NÃO requerem autenticação.
- FR-B06: A migração Alembic para a tabela
contact_leadsDEVE ser criada antes de qualquer deploy. - FR-B07: O
PropertyCardna listagem/imoveisDEVE tornarse clicável, linkando para/imoveis/<slug>.
Frontend
- FR-F01: A aplicação DEVE renderizar a rota
/imoveis/:slugcomoPropertyDetailPage. - FR-F02: O carrossel DEVE exibir a foto ativa em tamanho grande e um strip de miniaturas abaixo (ou lateral); a foto ativa é destacada no strip.
- FR-F03: O carrossel DEVE suportar navegação por teclado (teclas
←e→) quando em foco. - FR-F04: O carrossel DEVE suportar swipe touchscreen (dispositivos móveis).
- FR-F05: Em telas desktop (≥ 1024px), a caixa de preço DEVE ser sticky durante o scroll da página.
- FR-F06: O breadcrumb DEVE exibir: "Imóveis > [Cidade] > [Bairro] > [Título do imóvel]", onde "Imóveis" linka para
/imoveis, cidade e bairro linkam para/imoveiscom filtros pré-aplicados. - FR-F07: As amenidades DEVEM ser agrupadas pelas categorias: "Características" (group=caracteristica), "Lazer" (group=lazer), "Condomínio" (group=condominio), "Segurança" (group=seguranca). Grupos sem amenidade NÃO são renderizados.
- FR-F08: O formulário de contato DEVE validar
nameeemailcomo obrigatórios eemailcomo formato válido antes de enviar a requisição. - FR-F09: O botão de WhatsApp DEVE abrir
https://wa.me/<NUMERO>?text=<texto_codificado>em nova aba, onde o texto menciona o código e título do imóvel. - FR-F10: O número de WhatsApp DEVE ser configurável via variável de ambiente no frontend (
VITE_WHATSAPP_NUMBER). - FR-F11: A página DEVE exibir skeleton loaders durante o carregamento dos dados.
- FR-F12: Ao receber
404do backend, o frontend DEVE renderizar um estado de "Imóvel não encontrado" com CTA para/imoveis. - FR-F13: Todos os componentes DEVEM seguir o design system Linear dark definido em
DESIGN.md(canvas#08090a, panel#0f1011, accent#7170ff, tipografia Inter Variable).
Key Entities
- Property (existente): imóvel com fotos (1:M) e amenidades (M:M). Nenhum campo novo necessário.
- PropertyPhoto (existente): foto vinculada ao imóvel com
display_orderpara ordenação do carrossel. - Amenity (existente): diferencial com
grouppara agrupamento visual. - ContactLead (novo): registro de interesse de um visitante por um imóvel específico.
Success Criteria
- SC-001: Um visitante consegue acessar a página de detalhe a partir de um card na listagem e visualizar todas as informações em menos de 3 segundos em conexão padrão.
- SC-002: 100% dos campos do imóvel (fotos, preço, estatísticas, descrição) são exibidos sem erros de layout para imóveis com dados completos.
- SC-003: O visitante consegue preencher e enviar o formulário de contato em menos de 2 minutos; a confirmação de envio é visível imediatamente após o sucesso.
- SC-004: Os leads enviados pelo formulário ficam registrados no banco de dados com
property_id,name,emailecreated_atcorretos. - SC-005: Acessar o slug de um imóvel inexistente ou inativo nunca resulta em página em branco ou erro 500 — sempre exibe estado de "não encontrado".
- SC-006: O carrossel de fotos é navegável por teclado e por swipe em 100% dos testes de interação.
Assumptions
- O campo
listing_typeemPropertyusa os valores"venda"e"aluguel"(já implementado). - O slug é único por imóvel e imutável após criação (não há redirecionamento de slugs antigos no MVP).
- O número de WhatsApp da imobiliária é único e configurado por variável de ambiente (
VITE_WHATSAPP_NUMBER); não há múltiplos corretores no MVP. - O mapa embutido usa o serviço de mapas via endereço textual (geocoding pelo Google Maps Embed ou similar); coordenadas geográficas não são armazenadas no modelo
Propertyno MVP. - Não há sistema de autenticação de visitante — o formulário de contato é anônimo, e o campo
phoneé opcional. PropertyCardna listagem (/imoveis) já renderizaslugnos dados retornados pela API existenteGET /api/v1/properties.- Rate limiting no endpoint de contato está fora do escopo do MVP (será tratado em feature de segurança dedicada).
- Notificação por e-mail para a imobiliária ao receber um lead está fora do escopo do MVP (apenas persistência em DB).
- O IPTU não está modelado em
Propertyatualmente; a exibição de IPTU está fora do escopo desta feature.
Out of Scope
- Painel administrativo para visualizar leads recebidos (feature futura)
- Notificação por e-mail ou push ao receber novo lead
- Imóveis similares / "Veja também"
- Compartilhamento em redes sociais
- Favoritar imóvel (requer autenticação de visitante)
- Comparador de imóveis
- Tour virtual / vídeo embutido
- IPTU na caixa de preço (campo não modelado em
Property) - Múltiplos corretores com contato individualizado
- Slug redirect (slugs antigos não são preservados)