sass-imobiliaria/backend/seeds/seed.py

798 lines
28 KiB
Python

"""
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()