feat: add full project - backend, frontend, docker, specs and configs

This commit is contained in:
MatheusAlves96 2026-04-20 23:59:45 -03:00
parent b77c7d5a01
commit e6cb06255b
24489 changed files with 61341 additions and 36 deletions

View file

@ -0,0 +1,30 @@
codigo_csv,traducao,opcao_1,opcao_2,opcao_3,escolha
COLD_FLOOR,Piso frio,nova:caracteristica → piso-frio,ignorar,,
DIVIDERS,Divisórias,nova:caracteristica → divisorias,ignorar,,
FENCE,Cerca / Muro,nova:seguranca → cerca-muro,ignorar,,
FRUIT_TREES,Árvores frutíferas,nova:caracteristica → arvores-frutiferas,mapear → jardim,ignorar,
FULL_FLOOR,Andar corrido,nova:caracteristica → andar-corrido,ignorar,,
GARAGE,Garagem (estrutural),ignorar (já capturado em vagas_garagem),nova:caracteristica → garagem,,
INTEGRATED_ENVIRONMENTS,Ambientes integrados,nova:caracteristica → ambientes-integrados,ignorar,,
KITCHEN,Cozinha,ignorar (estrutural),nova:caracteristica → cozinha,,
LARGE_ROOM,Sala ampla,nova:caracteristica → sala-ampla,ignorar,,
LUNCH_ROOM,Sala de almoço,mapear → copa,nova:caracteristica → sala-de-almoco,ignorar,
MASSAGE_ROOM,Sala de massagem,nova:lazer → sala-de-massagem,mapear → spa,ignorar,
NEAR_ACCESS_ROADS,Próximo a vias de acesso,nova:localizacao → proximo-a-vias-de-acesso,ignorar,,
NEAR_HOSPITAL,Próximo a hospital,nova:localizacao → proximo-a-hospital,ignorar,,
NEAR_PUBLIC_TRANSPORT,Próximo a transporte público,nova:localizacao → proximo-a-transporte-publico,ignorar,,
NEAR_SCHOOL,Próximo a escola,nova:localizacao → proximo-a-escola,ignorar,,
NEAR_SHOPPING_CENTER,Próximo a shopping,nova:localizacao → proximo-a-shopping,ignorar,,
NUMBER_OF_FLOORS,Número de andares,ignorar (valor numérico),,,
PANTRY,Despensa,nova:caracteristica → despensa,ignorar,,
PARKING,Estacionamento,ignorar (já capturado em vagas_garagem),mapear → vaga-para-visitante,,
PLAN,Planta (tipo de layout),ignorar (não é amenidade),,,
RESTAURANT,Restaurante,nova:condominio → restaurante,ignorar,,
SANCA,Sanca (acabamento de gesso),nova:caracteristica → sanca,ignorar,,
SERVICE_ENTRANCE,Entrada de serviço,nova:caracteristica → entrada-de-servico,ignorar,,
SIDE_ENTRANCE,Entrada lateral,nova:caracteristica → entrada-lateral,ignorar,,
SLAB,Laje,nova:caracteristica → laje,ignorar,,
SMALL_ROOM,Cômodo pequeno,ignorar,nova:caracteristica → comodo-pequeno,,
SQUARE,Praça,nova:localizacao → praca,mapear → espaco-verde-parque,ignorar,
STAIR,Escada,ignorar (estrutural),nova:caracteristica → escada,,
TEEN_SPACE,Espaço teen,nova:lazer → espaco-teen,mapear → sala-de-jogos,ignorar,
1 codigo_csv traducao opcao_1 opcao_2 opcao_3 escolha
2 COLD_FLOOR Piso frio nova:caracteristica → piso-frio ignorar
3 DIVIDERS Divisórias nova:caracteristica → divisorias ignorar
4 FENCE Cerca / Muro nova:seguranca → cerca-muro ignorar
5 FRUIT_TREES Árvores frutíferas nova:caracteristica → arvores-frutiferas mapear → jardim ignorar
6 FULL_FLOOR Andar corrido nova:caracteristica → andar-corrido ignorar
7 GARAGE Garagem (estrutural) ignorar (já capturado em vagas_garagem) nova:caracteristica → garagem
8 INTEGRATED_ENVIRONMENTS Ambientes integrados nova:caracteristica → ambientes-integrados ignorar
9 KITCHEN Cozinha ignorar (estrutural) nova:caracteristica → cozinha
10 LARGE_ROOM Sala ampla nova:caracteristica → sala-ampla ignorar
11 LUNCH_ROOM Sala de almoço mapear → copa nova:caracteristica → sala-de-almoco ignorar
12 MASSAGE_ROOM Sala de massagem nova:lazer → sala-de-massagem mapear → spa ignorar
13 NEAR_ACCESS_ROADS Próximo a vias de acesso nova:localizacao → proximo-a-vias-de-acesso ignorar
14 NEAR_HOSPITAL Próximo a hospital nova:localizacao → proximo-a-hospital ignorar
15 NEAR_PUBLIC_TRANSPORT Próximo a transporte público nova:localizacao → proximo-a-transporte-publico ignorar
16 NEAR_SCHOOL Próximo a escola nova:localizacao → proximo-a-escola ignorar
17 NEAR_SHOPPING_CENTER Próximo a shopping nova:localizacao → proximo-a-shopping ignorar
18 NUMBER_OF_FLOORS Número de andares ignorar (valor numérico)
19 PANTRY Despensa nova:caracteristica → despensa ignorar
20 PARKING Estacionamento ignorar (já capturado em vagas_garagem) mapear → vaga-para-visitante
21 PLAN Planta (tipo de layout) ignorar (não é amenidade)
22 RESTAURANT Restaurante nova:condominio → restaurante ignorar
23 SANCA Sanca (acabamento de gesso) nova:caracteristica → sanca ignorar
24 SERVICE_ENTRANCE Entrada de serviço nova:caracteristica → entrada-de-servico ignorar
25 SIDE_ENTRANCE Entrada lateral nova:caracteristica → entrada-lateral ignorar
26 SLAB Laje nova:caracteristica → laje ignorar
27 SMALL_ROOM Cômodo pequeno ignorar nova:caracteristica → comodo-pequeno
28 SQUARE Praça nova:localizacao → praca mapear → espaco-verde-parque ignorar
29 STAIR Escada ignorar (estrutural) nova:caracteristica → escada
30 TEEN_SPACE Espaço teen nova:lazer → espaco-teen mapear → sala-de-jogos ignorar

12064
backend/seeds/data/fotos.csv Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,385 @@
"""
Importa imóveis de imoveis.csv + fotos.csv para o banco de dados.
Uso:
cd backend
uv run python seeds/import_from_csv.py [--csv-dir CAMINHO]
Padrão de CAMINHO: ../../aluguel_helper (relativo ao diretório backend/)
Idempotente: pula imóveis cujo `code` existe no banco.
"""
import argparse
import csv
import os
import re
import sys
import unicodedata
from collections import defaultdict
from decimal import Decimal
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app import create_app
from app.extensions import db
from app.models.location import City, Neighborhood
from app.models.property import Property, PropertyPhoto
from app.models.catalog import Amenity, PropertyType
from app.models.imobiliaria import Imobiliaria
# ── Configurações ─────────────────────────────────────────────────────────────
CITY_NAME = "Franca"
CITY_STATE = "SP"
# Mapeamento tipo_imovel (CSV) → slug do subtipo (já criado pelo seed.py)
SUBTYPE_MAP: dict[str, str] = {
"casa": "casa",
"apartamento": "apartamento",
"flat/studio": "flat",
"store": "loja-salao-ponto-comercial",
"landform": "terreno-lote-condominio",
}
# Mapeamento amenidade CSV (inglês) → slug português do banco
# Valores sem correspondência no sistema são ignorados (None).
AMENITY_MAP: dict[str, str | None] = {
# ── Características ───────────────────────────────────────────────────────
"PETS_ALLOWED": "aceita-animais",
"AIR_CONDITIONING": "ar-condicionado",
"SERVICE_AREA": "area-de-servico",
"BUILTIN_WARDROBE": "armario-embutido",
"BEDROOM_WARDROBE": "armario-embutido-no-quarto",
"KITCHEN_CABINETS": "armario-na-cozinha",
"BATHROOM_CABINETS": "armario-no-banheiro",
"BLINDEX_BOX": "box-blindex",
"CLOSET": "closet",
"DRESS_ROOM2": "closet",
"INTERNET_ACCESS": "conexao-a-internet",
"AMERICAN_KITCHEN": "cozinha-americana",
"LARGE_KITCHEN": "cozinha-americana",
"GOURMET_KITCHEN": "cozinha-gourmet",
"COOKER": "fogao",
"INTERCOM": "interfone",
"LARGE_WINDOW": "janela-grande",
"FURNISHED": "mobiliado",
"PLANNED_FURNITURE": "mobiliado",
"BACKYARD": "quintal",
"CABLE_TV": "tv-a-cabo",
"BALCONY": "varanda",
"WALL_BALCONY": "varanda",
"GOURMET_BALCONY": "varanda-gourmet",
"BARBECUE_BALCONY": "varanda-gourmet",
"NATURAL_VENTILATION": "ventilacao-natural",
"PANORAMIC_VIEW": "vista-panoramica",
"EXTERIOR_VIEW": "vista-panoramica",
"GAS_SHOWER": "aquecedor-a-gas",
"HEATING": "aquecimento-central",
"BATHTUB": "banheira",
"SERVICE_BATHROOM": "banheiro-de-servico",
"COPA": "copa",
"LUNCH_ROOM": "copa",
"EMPLOYEE_DEPENDENCY": "dependencia-de-empregada",
"DEPOSIT": "deposito",
"EDICULE": "edicula",
"SOLAR_ENERGY": "energia-solar",
"PET_SPACE": "espaco-pet",
"ENTRANCE_HALL": "hall-de-entrada",
"HOME_OFFICE": "home-office",
"CORNER_PROPERTY": "imovel-de-esquina",
"SOUNDPROOFING": "isolamento-acustico",
"ALUMINUM_WINDOW": "janela-de-aluminio",
"LAVABO": "lavabo",
"MEZZANINE": "mezanino",
"WOOD_FLOOR": "piso-de-madeira",
"LAMINATED_FLOOR": "piso-laminado",
"VINYL_FLOOR": "piso-vinilico",
"PORCELAIN": "porcelanato",
"DINNER_ROOM": "sala-de-jantar",
"PANTRY": "despensa",
"SERVICE_ENTRANCE": "entrada-de-servico",
"INTEGRATED_ENVIRONMENTS": "ambientes-integrados",
# ── Lazer ─────────────────────────────────────────────────────────────────
"FITNESS_ROOM": "academia",
"GYM": "academia",
"BAR": "bar",
"CINEMA": "cinema",
"BARBECUE_GRILL": "churrasqueira",
"PIZZA_OVEN": "churrasqueira",
"GOURMET_SPACE": "espaco-gourmet",
"GREEN_SPACE": "espaco-verde-parque",
"RECREATION_AREA": "espaco-verde-parque",
"WHIRLPOOL": "hidromassagem",
"GARDEN": "jardim",
"POOL": "piscina",
"PRIVATE_POOL": "piscina",
"ADULT_POOL": "piscina",
"HEATED_POOL": "piscina",
"CHILDRENS_POOL": "piscina",
"PLAYGROUND": "playground",
"TOYS_PLACE": "playground",
"SPORTS_COURT": "quadra-poliesportiva",
"FOOTBALL_FIELD": "quadra-poliesportiva",
"GAMES_ROOM": "sala-de-jogos",
"ADULT_GAME_ROOM": "sala-de-jogos",
"YOUTH_GAME_ROOM": "sala-de-jogos",
"TEEN_SPACE": "sala-de-jogos",
"PARTY_HALL": "salao-de-festas",
"SAUNA": "sauna",
"SPA": "spa",
"MASSAGE_ROOM": "spa",
# ── Condomínio ────────────────────────────────────────────────────────────
"DISABLED_ACCESS": "acesso-para-deficientes",
"BICYCLES_PLACE": "bicicletario",
"ELEVATOR": "elevador",
"ELECTRIC_GENERATOR": "gerador-eletrico",
"GRASS": "gramado",
"LAUNDRY": "lavanderia",
"RECEPTION": "recepcao",
"MEETING_ROOM": "sala-de-reunioes",
"COVENTION_HALL": "salao-de-convencoes",
"GUEST_PARKING": "vaga-para-visitante",
"RESTAURANT": "restaurante",
# ── Segurança ─────────────────────────────────────────────────────────────
"FENCE": "cerca-muro",
"SAFETY_CIRCUIT": "circuito-de-seguranca",
"SECURITY_CAMERA": "circuito-de-seguranca",
"GATED_COMMUNITY": "condominio-fechado",
"ELECTRONIC_GATE": "portao-eletronico",
"CONCIERGE_24H": "portaria-24h",
"ALARM_SYSTEM": "sistema-de-alarme",
"WATCHMAN": "vigia",
"PATROL": "vigia",
"SECURITY_CABIN": "vigia",
}
# ── Helpers ────────────────────────────────────────────────────────────────────
def _slugify(text: str) -> str:
text = unicodedata.normalize("NFD", text)
text = "".join(c for c in text if unicodedata.category(c) != "Mn")
text = text.lower()
text = re.sub(r"[^a-z0-9\s-]", "", text)
text = re.sub(r"[\s/]+", "-", text.strip())
return text
def _decimal_or_none(value: str) -> Decimal | None:
v = value.strip()
if not v:
return None
try:
return Decimal(v)
except Exception:
return None
def _int_or_zero(value: str) -> int:
v = value.strip()
if not v:
return 0
try:
return int(float(v))
except Exception:
return 0
# ── Leitura dos CSVs ───────────────────────────────────────────────────────────
def load_imoveis(csv_dir: str) -> list[dict]:
path = os.path.join(csv_dir, "imoveis.csv")
with open(path, encoding="utf-8", newline="") as f:
return list(csv.DictReader(f))
def load_fotos(csv_dir: str) -> dict[str, list[str]]:
"""Retorna {id_imovel: [url_ordenada_por_indice]}"""
path = os.path.join(csv_dir, "fotos.csv")
grouped: dict[str, list[tuple[int, str]]] = defaultdict(list)
with open(path, encoding="utf-8", newline="") as f:
for row in csv.DictReader(f):
grouped[row["id_imovel"]].append((int(row["indice"]), row["url"]))
return {
imovel_id: [url for _, url in sorted(fotos)]
for imovel_id, fotos in grouped.items()
}
# ── Importação ─────────────────────────────────────────────────────────────────
def importar(csv_dir: str) -> None:
app = create_app()
with app.app_context():
imoveis = load_imoveis(csv_dir)
fotos_map = load_fotos(csv_dir)
# ── Cidade Franca ─────────────────────────────────────────────────────
city_slug = _slugify(CITY_NAME)
city = City.query.filter_by(slug=city_slug).first()
if not city:
city = City(name=CITY_NAME, slug=city_slug, state=CITY_STATE)
db.session.add(city)
db.session.flush()
print(f" Cidade criada: {CITY_NAME}")
else:
print(f" Cidade existente: {CITY_NAME}")
# ── Cache de bairros ──────────────────────────────────────────────────
neighborhood_cache: dict[str, Neighborhood] = {
nbh.slug: nbh for nbh in Neighborhood.query.filter_by(city_id=city.id).all()
}
def get_or_create_neighborhood(name: str) -> Neighborhood:
slug = _slugify(name)
if slug not in neighborhood_cache:
nbh = Neighborhood(name=name, slug=slug, city_id=city.id)
db.session.add(nbh)
db.session.flush()
neighborhood_cache[slug] = nbh
return neighborhood_cache[slug]
# ── Cache de subtipos ─────────────────────────────────────────────────
subtype_cache: dict[str, PropertyType] = {
pt.slug: pt for pt in PropertyType.query.all()
}
# ── Cache de amenidades ───────────────────────────────────────────────
amenity_cache: dict[str, Amenity] = {a.slug: a for a in Amenity.query.all()}
# ── Cache de imobiliárias (cria se não existir) ───────────────────────
imob_cache: dict[str, Imobiliaria] = {
i.name: i for i in Imobiliaria.query.all()
}
def get_or_create_imobiliaria(name: str) -> Imobiliaria:
if name not in imob_cache:
imob = Imobiliaria(name=name, is_active=True, display_order=0)
db.session.add(imob)
db.session.flush()
imob_cache[name] = imob
print(f" Imobiliária criada: {name}")
return imob_cache[name]
# ── Imóveis existentes (por code) ─────────────────────────────────────
existing_codes: set[str] = {
p.code for p in Property.query.with_entities(Property.code).all() if p.code
}
created = 0
skipped = 0
updated = 0
for row in imoveis:
code = row["id"].strip()
titulo = row["titulo"].strip()
listing_type = "aluguel" if "alugar" in titulo.lower() else "venda"
if code in existing_codes:
# Corrige o type de registros já importados
rows_updated = (
Property.query.filter_by(code=code)
.update({"type": listing_type})
)
if rows_updated:
updated += 1
skipped += 1
continue
# Bairro
bairro_nome = row["bairro"].strip()
nbh = get_or_create_neighborhood(bairro_nome) if bairro_nome else None
# Subtipo
tipo_raw = row["tipo_imovel"].strip().lower()
subtype_slug = SUBTYPE_MAP.get(tipo_raw, "casa")
subtype = subtype_cache.get(subtype_slug)
if subtype is None:
# Fallback: pega qualquer subtipo residencial
subtype = subtype_cache.get("casa")
base_slug = _slugify(titulo)[:200]
slug = base_slug
suffix = 1
while Property.query.filter_by(slug=slug).first():
slug = f"{base_slug}-{suffix}"
suffix += 1
# Amenidades
amenidade_raw = row.get("amenidades", "").strip()
prop_amenities: list[Amenity] = []
if amenidade_raw:
for code_en in amenidade_raw.split("|"):
slug_pt = AMENITY_MAP.get(code_en.strip())
if slug_pt and slug_pt in amenity_cache:
amenity = amenity_cache[slug_pt]
if amenity not in prop_amenities:
prop_amenities.append(amenity)
# Imobiliária
imob_name = row.get("imobiliaria", "").strip()
imob = get_or_create_imobiliaria(imob_name) if imob_name else None
listing_type = "aluguel" if "alugar" in titulo.lower() else "venda"
prop = Property(
title=titulo,
slug=slug,
code=code,
description=row["descricao"].strip() or None,
address=row["endereco"].strip() or None,
price=Decimal(row["preco_aluguel"].strip()),
condo_fee=_decimal_or_none(row["condominio"]),
iptu_anual=_decimal_or_none(row["iptu"]),
type=listing_type,
subtype_id=subtype.id if subtype else None,
city_id=city.id,
neighborhood_id=nbh.id if nbh else None,
bedrooms=_int_or_zero(row["quartos"]),
bathrooms=_int_or_zero(row["banheiros"]),
parking_spots=_int_or_zero(row["vagas_garagem"]),
area_m2=_int_or_zero(row["area_m2"]) or 1,
is_featured=False,
is_active=True,
imobiliaria_id=imob.id if imob else None,
)
prop.amenities = prop_amenities
db.session.add(prop)
db.session.flush()
# Fotos
for order, url in enumerate(fotos_map.get(code, [])):
db.session.add(
PropertyPhoto(
property_id=prop.id,
url=url,
alt_text=titulo,
display_order=order,
)
)
existing_codes.add(code)
created += 1
db.session.commit()
print(
f"\nImportação concluída: {created} imóveis criados, "
f"{updated} tipo(s) atualizado(s), {skipped - updated} já existiam sem alteração."
)
# ── Entrypoint ─────────────────────────────────────────────────────────────────
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Importa imóveis do aluguel_helper.")
parser.add_argument(
"--csv-dir",
default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "data"),
help="Diretório contendo imoveis.csv e fotos.csv",
)
args = parser.parse_args()
csv_dir = os.path.abspath(args.csv_dir)
print(f"==> Lendo CSVs de: {csv_dir}")
importar(csv_dir)

798
backend/seeds/seed.py Normal file
View file

@ -0,0 +1,798 @@
"""
Popula o banco com dados de exemplo para desenvolvimento.
Uso: cd backend && uv run python seeds/seed.py
Idempotente: apaga e recria tudo a cada execução.
"""
import sys
import os
import unicodedata
import re
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import bcrypt
from app import create_app
from app.extensions import db
from app.models.homepage import HomepageConfig
from app.models.property import Property, PropertyPhoto
from app.models.catalog import Amenity, PropertyType
from app.models.location import City, Neighborhood
from app.models.user import ClientUser
from app.models.agent import Agent
# ── Property Types ────────────────────────────────────────────────────────────
PROPERTY_TYPES = [
{
"name": "Residencial",
"slug": "residencial",
"subtypes": [
{"name": "Apartamento", "slug": "apartamento"},
{"name": "Studio", "slug": "studio"},
{"name": "Kitnet", "slug": "kitnet"},
{"name": "Casa", "slug": "casa"},
{"name": "Casa de Condomínio", "slug": "casa-de-condominio"},
{"name": "Casa de Vila", "slug": "casa-de-vila"},
{"name": "Cobertura", "slug": "cobertura"},
{"name": "Flat", "slug": "flat"},
{"name": "Loft", "slug": "loft"},
{"name": "Terreno / Lote / Condomínio", "slug": "terreno-lote-condominio"},
{"name": "Sobrado", "slug": "sobrado"},
{"name": "Fazenda / Sítio / Chácara", "slug": "fazenda-sitio-chacara"},
],
},
{
"name": "Comercial",
"slug": "comercial",
"subtypes": [
{
"name": "Loja / Salão / Ponto Comercial",
"slug": "loja-salao-ponto-comercial",
},
{"name": "Conjunto Comercial / Sala", "slug": "conjunto-comercial-sala"},
{"name": "Casa Comercial", "slug": "casa-comercial"},
{"name": "Hotel / Motel / Pousada", "slug": "hotel-motel-pousada"},
{"name": "Andar / Laje Corporativa", "slug": "andar-laje-corporativa"},
{"name": "Prédio Inteiro", "slug": "predio-inteiro"},
{"name": "Terrenos / Lotes Comerciais", "slug": "terreno-lote-comercial"},
{"name": "Galpão / Depósito / Armazém", "slug": "galpao-deposito-armazem"},
{"name": "Garagem", "slug": "garagem"},
],
},
]
# ── Amenities ────────────────────────────────────────────────────────────────
AMENITIES: dict[str, list[str]] = {
"caracteristica": [
"Aceita animais",
"Aquecedor a gás",
"Aquecimento central",
"Ar-condicionado",
"Área de serviço",
"Armário embutido",
"Armário embutido no quarto",
"Armário na cozinha",
"Armário no banheiro",
"Banheira",
"Banheiro de serviço",
"Box blindex",
"Closet",
"Conexão à internet",
"Copa",
"Cozinha americana",
"Cozinha gourmet",
"Dependência de empregada",
"Depósito",
"Despensa",
"Edícula",
"Energia solar",
"Entrada de serviço",
"Espaço pet",
"Fogão",
"Hall de entrada",
"Home office",
"Imóvel de esquina",
"Interfone",
"Ambientes integrados",
"Isolamento acústico",
"Janela de alumínio",
"Janela grande",
"Lavabo",
"Mezanino",
"Mobiliado",
"Piso de madeira",
"Piso laminado",
"Piso vinílico",
"Porcelanato",
"Quintal",
"Sala de jantar",
"TV a cabo",
"Varanda",
"Varanda gourmet",
"Ventilação natural",
"Vista panorâmica",
],
"lazer": [
"Academia",
"Bar",
"Cinema",
"Churrasqueira",
"Espaço gourmet",
"Espaço verde / Parque",
"Hidromassagem",
"Jardim",
"Piscina",
"Playground",
"Quadra poliesportiva",
"Sala de jogos",
"Salão de festas",
"Sauna",
"Spa",
],
"condominio": [
"Acesso para deficientes",
"Bicicletário",
"Restaurante",
"Elevador",
"Gerador elétrico",
"Gramado",
"Lavanderia",
"Recepção",
"Sala de reuniões",
"Salão de convenções",
"Vaga para visitante",
],
"seguranca": [
"Cerca / Muro",
"Circuito de segurança",
"Condomínio fechado",
"Portão eletrônico",
"Portaria 24h",
"Sistema de alarme",
"Vigia",
],
}
def _slugify(text: str) -> str:
text = unicodedata.normalize("NFD", text)
text = "".join(c for c in text if unicodedata.category(c) != "Mn")
text = text.lower()
text = re.sub(r"[^a-z0-9\s-]", "", text)
text = re.sub(r"[\s/]+", "-", text.strip())
return text
# ── Agents ─────────────────────────────────────────────────────────────────────
SAMPLE_AGENTS = [
{
"name": "Carlos Eduardo Mota",
"photo_url": "https://randomuser.me/api/portraits/men/32.jpg",
"creci": "123456-F",
"email": "carlos.mota@imobiliaria.com",
"phone": "(11) 98765-4321",
"bio": "Especialista em imóveis residenciais de alto padrão na Grande São Paulo. Mais de 10 anos de experiência.",
"is_active": True,
"display_order": 1,
},
{
"name": "Fernanda Lima Souza",
"photo_url": "https://randomuser.me/api/portraits/women/44.jpg",
"creci": "234567-F",
"email": "fernanda.lima@imobiliaria.com",
"phone": "(11) 97654-3210",
"bio": "Formada em Administração com pós em Mercado Imobiliário. Foco em imóveis comerciais e corporativos.",
"is_active": True,
"display_order": 2,
},
{
"name": "Ricardo Alves Pereira",
"photo_url": "https://randomuser.me/api/portraits/men/51.jpg",
"creci": "345678-F",
"email": "ricardo.alves@imobiliaria.com",
"phone": "(11) 96543-2109",
"bio": "Especializado em lançamentos e imóveis na planta. Parceiro dos principais construtores da região.",
"is_active": True,
"display_order": 3,
},
{
"name": "Juliana Nascimento",
"photo_url": "https://randomuser.me/api/portraits/women/68.jpg",
"creci": "456789-F",
"email": "juliana.nascimento@imobiliaria.com",
"phone": "(11) 95432-1098",
"bio": "Apaixonada por conectar famílias ao imóvel perfeito. Especialista em Alphaville e Tamboré.",
"is_active": True,
"display_order": 4,
},
{
"name": "Marcos Vinícius Costa",
"photo_url": "https://randomuser.me/api/portraits/men/77.jpg",
"creci": "567890-F",
"email": "marcos.costa@imobiliaria.com",
"phone": "(11) 94321-0987",
"bio": "Corretor com expertise em locações comerciais e residenciais. Atendimento 100% personalizado.",
"is_active": True,
"display_order": 5,
},
{
"name": "Patrícia Rodrigues",
"photo_url": "https://randomuser.me/api/portraits/women/85.jpg",
"creci": "678901-F",
"email": "patricia.rodrigues@imobiliaria.com",
"phone": "(11) 93210-9876",
"bio": "Referência em imóveis de luxo. Fluente em inglês e espanhol para atendimento internacional.",
"is_active": True,
"display_order": 6,
},
{
"name": "Bruno Henrique Ferreira",
"photo_url": "https://randomuser.me/api/portraits/men/22.jpg",
"creci": "789012-F",
"email": "bruno.ferreira@imobiliaria.com",
"phone": "(11) 92109-8765",
"bio": "Especialista em avaliação de imóveis e consultoria de investimentos imobiliários.",
"is_active": True,
"display_order": 7,
},
{
"name": "Aline Mendes Torres",
"photo_url": "https://randomuser.me/api/portraits/women/12.jpg",
"creci": "890123-F",
"email": "aline.mendes@imobiliaria.com",
"phone": "(11) 91098-7654",
"bio": "Dedicada ao mercado de locação residencial. Processo ágil e desburocratizado do início ao fim.",
"is_active": True,
"display_order": 8,
},
]
# ── Cities & Neighborhoods ─────────────────────────────────────────────────────
LOCATIONS = [
{
"name": "São Paulo",
"slug": "sao-paulo",
"state": "SP",
"neighborhoods": [
{"name": "Centro", "slug": "centro"},
{"name": "Jardim Primavera", "slug": "jardim-primavera"},
{"name": "Vila Nova", "slug": "vila-nova"},
{"name": "Pinheiros", "slug": "pinheiros"},
{"name": "Itaim Bibi", "slug": "itaim-bibi"},
{"name": "Vila Olímpia", "slug": "vila-olimpia"},
{"name": "Centro Histórico", "slug": "centro-historico"},
{"name": "Zona Norte", "slug": "zona-norte"},
{"name": "Alto da Boa Vista", "slug": "alto-da-boa-vista"},
],
},
{
"name": "Rio de Janeiro",
"slug": "rio-de-janeiro",
"state": "RJ",
"neighborhoods": [
{"name": "Praia Grande", "slug": "praia-grande"},
{"name": "Copacabana", "slug": "copacabana"},
{"name": "Ipanema", "slug": "ipanema"},
{"name": "Barra da Tijuca", "slug": "barra-da-tijuca"},
],
},
{
"name": "Campinas",
"slug": "campinas",
"state": "SP",
"neighborhoods": [
{"name": "Alphaville", "slug": "alphaville"},
{"name": "Universitário", "slug": "universitario"},
],
},
]
# ── Sample Properties ─────────────────────────────────────────────────────────
SAMPLE_PROPERTIES = [
{
"title": "Apartamento 3 quartos — Centro",
"slug": "apartamento-3-quartos-centro",
"address": "Rua das Flores, 123 — Centro",
"code": "SP-001",
"description": "Excelente apartamento localizado no coração do Centro, com ótima iluminação natural e acabamento de qualidade. O imóvel conta com sala ampla, cozinha bem distribuída e quartos espaçosos com armários embutidos.\n\nPróximo a comércio, transporte público e todas as facilidades urbanas. Condomínio completo com elevador e portaria. Não perca esta oportunidade!",
"price": "750000.00",
"condo_fee": "800.00",
"type": "venda",
"subtype_slug": "apartamento",
"city_slug": "sao-paulo",
"neighborhood_slug": "centro",
"bedrooms": 3,
"bathrooms": 2,
"parking_spots": 1,
"area_m2": 98,
"is_featured": True,
"is_active": True,
"amenity_slugs": [
"ar-condicionado",
"armario-embutido",
"interfone",
"elevador",
],
"photos": [
{
"url": "https://picsum.photos/seed/apt1/800/450",
"alt_text": "Sala de estar ampla com varanda",
"display_order": 0,
}
],
},
{
"title": "Casa 4 quartos — Jardim Primavera",
"slug": "casa-4-quartos-jardim-primavera",
"address": "Av. das Palmeiras, 456 — Jardim Primavera",
"price": "1250000.00",
"condo_fee": None,
"type": "venda",
"subtype_slug": "casa",
"city_slug": "sao-paulo",
"neighborhood_slug": "jardim-primavera",
"bedrooms": 4,
"bathrooms": 3,
"parking_spots": 2,
"area_m2": 220,
"is_featured": True,
"is_active": True,
"amenity_slugs": ["piscina", "churrasqueira", "jardim", "quintal"],
"photos": [
{
"url": "https://picsum.photos/seed/house2/800/450",
"alt_text": "Fachada da casa com jardim",
"display_order": 0,
}
],
},
{
"title": "Studio moderno — Vila Nova",
"slug": "studio-moderno-vila-nova",
"address": "Rua dos Ipês, 789 — Vila Nova",
"price": "2800.00",
"condo_fee": "350.00",
"type": "aluguel",
"subtype_slug": "studio",
"city_slug": "sao-paulo",
"neighborhood_slug": "vila-nova",
"bedrooms": 1,
"bathrooms": 1,
"parking_spots": 0,
"area_m2": 38,
"is_featured": True,
"is_active": True,
"amenity_slugs": ["cozinha-americana", "mobiliado", "conexao-a-internet"],
"photos": [
{
"url": "https://picsum.photos/seed/studio3/800/450",
"alt_text": "Ambiente integrado do studio",
"display_order": 0,
}
],
},
{
"title": "Cobertura duplex — Beira Mar",
"slug": "cobertura-duplex-beira-mar",
"address": "Av. Beira Mar, 1000 — Praia Grande",
"price": "3200000.00",
"condo_fee": "1500.00",
"type": "venda",
"subtype_slug": "cobertura",
"city_slug": "rio-de-janeiro",
"neighborhood_slug": "praia-grande",
"bedrooms": 5,
"bathrooms": 4,
"parking_spots": 3,
"area_m2": 380,
"is_featured": True,
"is_active": True,
"amenity_slugs": [
"piscina",
"vista-panoramica",
"varanda-gourmet",
"cozinha-gourmet",
"closet",
],
"photos": [
{
"url": "https://picsum.photos/seed/cobertura4/800/450",
"alt_text": "Vista da cobertura para o mar",
"display_order": 0,
}
],
},
{
"title": "Kitnet — Próximo à Universidade",
"slug": "kitnet-proximo-universidade",
"address": "Rua Estudante, 50 — Universitário",
"price": "1500.00",
"condo_fee": "200.00",
"type": "aluguel",
"subtype_slug": "kitnet",
"city_slug": "campinas",
"neighborhood_slug": "universitario",
"bedrooms": 1,
"bathrooms": 1,
"parking_spots": 0,
"area_m2": 25,
"is_featured": True,
"is_active": True,
"amenity_slugs": ["mobiliado", "conexao-a-internet", "fogao"],
"photos": [
{
"url": "https://picsum.photos/seed/kitnet5/800/450",
"alt_text": "Interior da kitnet mobiliada",
"display_order": 0,
}
],
},
{
"title": "Sobrado 3 quartos — Bairro Nobre",
"slug": "sobrado-3-quartos-bairro-nobre",
"address": "Rua Nobreza, 321 — Alto da Boa Vista",
"price": "890000.00",
"condo_fee": None,
"type": "venda",
"subtype_slug": "sobrado",
"city_slug": "sao-paulo",
"neighborhood_slug": "alto-da-boa-vista",
"bedrooms": 3,
"bathrooms": 2,
"parking_spots": 2,
"area_m2": 160,
"is_featured": True,
"is_active": True,
"amenity_slugs": [
"churrasqueira",
"jardim",
"portao-eletronico",
"sistema-de-alarme",
],
"photos": [
{
"url": "https://picsum.photos/seed/sobrado6/800/450",
"alt_text": "Fachada do sobrado com garagem",
"display_order": 0,
}
],
},
{
"title": "Loft industrial — Centro Histórico",
"slug": "loft-industrial-centro-historico",
"address": "Rua da Consolação, 200 — Centro Histórico",
"price": "4200.00",
"condo_fee": "500.00",
"type": "aluguel",
"subtype_slug": "loft",
"city_slug": "sao-paulo",
"neighborhood_slug": "centro-historico",
"bedrooms": 1,
"bathrooms": 1,
"parking_spots": 1,
"area_m2": 65,
"is_featured": False,
"is_active": True,
"amenity_slugs": [
"ar-condicionado",
"mobiliado",
"conexao-a-internet",
"elevador",
],
"photos": [
{
"url": "https://picsum.photos/seed/loft7/800/450",
"alt_text": "Interior do loft",
"display_order": 0,
}
],
},
{
"title": "Apartamento 2 quartos — Pinheiros",
"slug": "apartamento-2-quartos-pinheiros",
"address": "Rua Teodoro Sampaio, 450 — Pinheiros",
"price": "580000.00",
"condo_fee": "650.00",
"type": "venda",
"subtype_slug": "apartamento",
"city_slug": "sao-paulo",
"neighborhood_slug": "pinheiros",
"bedrooms": 2,
"bathrooms": 1,
"parking_spots": 1,
"area_m2": 72,
"is_featured": False,
"is_active": True,
"amenity_slugs": [
"ar-condicionado",
"varanda",
"interfone",
"academia",
"elevador",
],
"photos": [
{
"url": "https://picsum.photos/seed/apt8/800/450",
"alt_text": "Sala com varanda",
"display_order": 0,
}
],
},
{
"title": "Casa de Condomínio — Alphaville",
"slug": "casa-condominio-alphaville",
"address": "Alameda Itu, 10 — Alphaville",
"price": "2100000.00",
"condo_fee": "1200.00",
"type": "venda",
"subtype_slug": "casa-de-condominio",
"city_slug": "campinas",
"neighborhood_slug": "alphaville",
"bedrooms": 4,
"bathrooms": 4,
"parking_spots": 4,
"area_m2": 350,
"is_featured": False,
"is_active": True,
"amenity_slugs": [
"piscina",
"churrasqueira",
"salao-de-festas",
"portaria-24h",
"condominio-fechado",
"playground",
],
"photos": [
{
"url": "https://picsum.photos/seed/cond9/800/450",
"alt_text": "Área de lazer do condomínio",
"display_order": 0,
}
],
},
{
"title": "Flat executivo — Itaim Bibi",
"slug": "flat-executivo-itaim-bibi",
"address": "Av. Faria Lima, 3000 — Itaim Bibi",
"price": "6500.00",
"condo_fee": "800.00",
"type": "aluguel",
"subtype_slug": "flat",
"city_slug": "sao-paulo",
"neighborhood_slug": "itaim-bibi",
"bedrooms": 1,
"bathrooms": 1,
"parking_spots": 1,
"area_m2": 45,
"is_featured": False,
"is_active": True,
"amenity_slugs": [
"ar-condicionado",
"mobiliado",
"recepcao",
"academia",
"elevador",
"portaria-24h",
],
"photos": [
{
"url": "https://picsum.photos/seed/flat10/800/450",
"alt_text": "Suite executiva",
"display_order": 0,
}
],
},
{
"title": "Terreno 600m² — Zona Norte",
"slug": "terreno-600m2-zona-norte",
"address": "Estrada das Amendoeiras, s/n — Zona Norte",
"price": "320000.00",
"condo_fee": None,
"type": "venda",
"subtype_slug": "terreno-lote-condominio",
"city_slug": "sao-paulo",
"neighborhood_slug": "zona-norte",
"bedrooms": 0,
"bathrooms": 0,
"parking_spots": 0,
"area_m2": 600,
"is_featured": False,
"is_active": True,
"amenity_slugs": [],
"photos": [
{
"url": "https://picsum.photos/seed/terreno11/800/450",
"alt_text": "Terreno plano",
"display_order": 0,
}
],
},
{
"title": "Conjunto Comercial 80m² — Vila Olímpia",
"slug": "conjunto-comercial-80m2-vila-olimpia",
"address": "Rua Funchal, 500 — Vila Olímpia",
"price": "8000.00",
"condo_fee": "1800.00",
"type": "aluguel",
"subtype_slug": "conjunto-comercial-sala",
"city_slug": "sao-paulo",
"neighborhood_slug": "vila-olimpia",
"bedrooms": 0,
"bathrooms": 2,
"parking_spots": 2,
"area_m2": 80,
"is_featured": False,
"is_active": True,
"amenity_slugs": ["ar-condicionado", "recepcao", "elevador", "portaria-24h"],
"photos": [
{
"url": "https://picsum.photos/seed/comercial12/800/450",
"alt_text": "Sala comercial",
"display_order": 0,
}
],
},
]
def seed() -> None:
app = create_app()
with app.app_context():
# ── Wipe existing data (FK order) ──────────────────────────────────────
db.session.execute(db.text("DELETE FROM property_amenity"))
db.session.query(PropertyPhoto).delete()
db.session.query(Property).delete()
db.session.query(Neighborhood).delete()
db.session.query(City).delete()
db.session.query(Amenity).delete()
# Delete subtypes before parents to satisfy self-referential FK
db.session.query(PropertyType).filter(
PropertyType.parent_id.isnot(None)
).delete()
db.session.query(PropertyType).delete()
db.session.query(HomepageConfig).delete()
db.session.commit()
# ── Property Types ──────────────────────────────────────────────────────
subtype_map: dict[str, PropertyType] = {}
for cat_data in PROPERTY_TYPES:
cat = PropertyType(name=cat_data["name"], slug=cat_data["slug"])
db.session.add(cat)
db.session.flush()
for sub_data in cat_data["subtypes"]:
sub = PropertyType(
name=sub_data["name"], slug=sub_data["slug"], parent_id=cat.id
)
db.session.add(sub)
db.session.flush()
subtype_map[sub.slug] = sub
# ── Amenities ───────────────────────────────────────────────────────────
amenity_map: dict[str, Amenity] = {}
for group, names in AMENITIES.items():
for name in names:
slug = _slugify(name)
amenity = Amenity(name=name, slug=slug, group=group)
db.session.add(amenity)
db.session.flush()
amenity_map[slug] = amenity
# ── Homepage Config ─────────────────────────────────────────────────────
db.session.add(
HomepageConfig(
hero_headline="Encontre o imóvel dos seus sonhos",
hero_subheadline="Mais de 200 imóveis disponíveis em toda a região",
hero_cta_label="Ver Imóveis",
hero_cta_url="/imoveis",
featured_properties_limit=6,
)
)
# ── Cities & Neighborhoods ────────────────────────────────────────────
city_map: dict[str, City] = {}
neighborhood_map: dict[str, Neighborhood] = {}
for loc_data in LOCATIONS:
city = City(
name=loc_data["name"],
slug=loc_data["slug"],
state=loc_data["state"],
)
db.session.add(city)
db.session.flush()
city_map[city.slug] = city
for nbh_data in loc_data["neighborhoods"]:
nbh = Neighborhood(
name=nbh_data["name"],
slug=nbh_data["slug"],
city_id=city.id,
)
db.session.add(nbh)
db.session.flush()
neighborhood_map[f"{city.slug}/{nbh.slug}"] = nbh
# ── Properties ──────────────────────────────────────────────────────────
for data in SAMPLE_PROPERTIES:
amenity_slugs: list[str] = data.pop("amenity_slugs")
photos_data: list[dict] = data.pop("photos")
subtype_slug: str = data.pop("subtype_slug")
city_slug: str | None = data.pop("city_slug", None)
neighborhood_slug: str | None = data.pop("neighborhood_slug", None)
city_id = city_map[city_slug].id if city_slug else None
nbh_key = (
f"{city_slug}/{neighborhood_slug}"
if city_slug and neighborhood_slug
else None
)
neighborhood_id = neighborhood_map[nbh_key].id if nbh_key else None
prop = Property(
**data,
subtype_id=subtype_map[subtype_slug].id,
city_id=city_id,
neighborhood_id=neighborhood_id,
)
db.session.add(prop)
db.session.flush()
for photo_data in photos_data:
db.session.add(PropertyPhoto(property_id=prop.id, **photo_data))
for slug in amenity_slugs:
if slug in amenity_map:
prop.amenities.append(amenity_map[slug])
db.session.commit()
# ── Admin user ──────────────────────────────────────────────────────────
ADMIN_EMAIL = "admin@master.com"
ADMIN_PASSWORD = "Hn84pFUgatYX"
admin = ClientUser.query.filter_by(email=ADMIN_EMAIL).first()
if not admin:
admin = ClientUser(
name="Admin",
email=ADMIN_EMAIL,
password_hash=bcrypt.hashpw(
ADMIN_PASSWORD.encode(), bcrypt.gensalt()
).decode(),
role="admin",
)
db.session.add(admin)
else:
admin.password_hash = bcrypt.hashpw(
ADMIN_PASSWORD.encode(), bcrypt.gensalt()
).decode()
admin.role = "admin"
db.session.commit()
print(f"Admin: {ADMIN_EMAIL}")
total_amenities = sum(len(v) for v in AMENITIES.values())
total_types = sum(1 + len(c["subtypes"]) for c in PROPERTY_TYPES)
total_cities = len(LOCATIONS)
total_nbhs = sum(len(loc["neighborhoods"]) for loc in LOCATIONS)
print(
f"Seed concluído: {len(SAMPLE_PROPERTIES)} imóveis, "
f"{total_types} tipos, {total_amenities} amenidades, "
f"{total_cities} cidades, {total_nbhs} bairros."
)
# ── Agents ─────────────────────────────────────────────────────────────
db.session.query(Agent).delete()
db.session.commit()
for agent_data in SAMPLE_AGENTS:
db.session.add(Agent(**agent_data))
db.session.commit()
print(f"Corretores seed: {len(SAMPLE_AGENTS)} cadastrados.")
if __name__ == "__main__":
seed()