""" Inserción masiva en la base de datos con datos realistas de Celaya. Cantidades: - 200 usuarios (185 ciudadanos + 13 empleados + 2 admins) - 62 camiones (CEL-001 ... CEL-062) - ~340 domicilios (1-3 por ciudadano) - ~280 reportes ciudadanos con varios estados y fechas - ~150 reportes operativos de empleados - ~220 calificaciones de servicio Conserva las credenciales demo: - demo@celaya.gob.mx / Celaya2026 (CIUDADANO) - empleado@celaya.gob.mx / Empleado2026 (EMPLEADO) - admin@celaya.gob.mx / Admin2026 (ADMIN) """ import random import uuid from datetime import datetime, timedelta from sqlalchemy import text from app.database import Base, engine, SessionLocal from app.models import User, Address, Report, ServiceRating, OperationalReport, Truck from app.models.user import UserRole from app.services.auth_service import hash_password from app.services.eta_service import assign_route random.seed(42) # Reproducible # ─── Crear tablas y migrar columnas faltantes ──────────────────────────────── Base.metadata.create_all(bind=engine) with engine.connect() as conn: for sql in [ "ALTER TABLE users ADD COLUMN role VARCHAR DEFAULT 'CIUDADANO'", ]: try: conn.execute(text(sql)); conn.commit() except Exception: pass db = SessionLocal() # ─── Datos realistas ───────────────────────────────────────────────────────── NOMBRES_M = ['María','Guadalupe','Ana','Patricia','Carmen','Rosa','Laura','Sandra','Verónica','Adriana','Claudia','Mónica','Cecilia','Gabriela','Lourdes','Isabel','Yolanda','Alejandra','Leticia','Norma','Diana','Karla','Fabiola','Brenda','Lucía','Elena','Beatriz','Andrea','Sofía','Valeria','Daniela','Paola','Karina','Marisol','Magdalena'] NOMBRES_H = ['José','Juan','Pedro','Carlos','Luis','Miguel','Jorge','Francisco','Alejandro','Antonio','Ricardo','Roberto','Daniel','Fernando','Mario','Eduardo','Javier','Sergio','Raúl','Manuel','Rafael','Arturo','Gerardo','Héctor','Óscar','Ignacio','Salvador','Octavio','Rubén','Pablo','Andrés','Diego','Iván','Alberto','Enrique'] APELLIDOS = ['García','Hernández','González','Martínez','López','Rodríguez','Pérez','Sánchez','Ramírez','Cruz','Flores','Gómez','Morales','Vázquez','Reyes','Jiménez','Torres','Díaz','Ruiz','Mendoza','Aguilar','Castro','Romero','Vargas','Herrera','Castillo','Ortiz','Moreno','Rivera','Chávez','Ramos','Guzmán','Mendez','Estrada','Salazar','Núñez','Cervantes','Domínguez','Solís','Avila'] COLONIAS = ['Centro','Las Arboledas','San Juanico','Los Olivos','Rancho Seco','Rumbos de Roque','Ciudad Industrial','Hospital General','Sede UG Sur','Torres Landa','Las Insurgentes','Trojes','Irrigación','La Toscana','San José de Celaya','Tecnológico','Las Américas','Las Hadas','Real del Cid','Pueblitos','Villas del Mineral','Casa Blanca','La Aurora','Insurgentes Norte','Las Plazas','San Isidro','La Esperanza','Villas de la Hacienda','Camino Real','Los Sauces','Jardines','Praderas','Vista Hermosa'] CALLES = ['Hidalgo','Juárez','Morelos','Madero','Allende','Zaragoza','Independencia','5 de Mayo','16 de Septiembre','Reforma','Constituyentes','Insurgentes','Av. del Trabajo','Av. Tecnológico','Av. Torres Landa','Av. Irrigación','Av. Las Torres','Av. Juan Pablo II','Av. Universidad','Calzada de los Olivos','Mariano Escobedo','Vicente Guerrero','Benito Juárez','Cuauhtémoc','Galeana','Aldama','Pino Suárez','Niños Héroes','Emiliano Zapata','Lázaro Cárdenas'] LABELS_DOMICILIO = ['Casa','Trabajo','Casa de mis papás','Negocio','Casa de campo','Departamento','Casa de la abuela','Oficina','Local','Familia'] # Coordenadas reales aproximadas de zonas de Celaya ZONAS_CELAYA = [ (20.5215, -100.8142), # Centro (20.5410, -100.8130), # Av. Tecnológico (20.5290, -100.8320), # San Juanico (20.5295, -100.7890), # Los Olivos (20.5020, -100.8350), # Sur (20.5610, -100.8370), # Roque (20.5450, -100.7950), # Ciudad Industrial (20.5245, -100.7980), # Universidad Latina (20.5260, -100.8520), # Hospital (20.4990, -100.8390), # UG Sur (20.5280, -100.8250), # Torres Landa (20.5320, -100.7980), # Las Insurgentes (20.5420, -100.8080), # Trojes (20.5140, -100.8390), # La Toscana (20.5390, -100.8480), # San José ] MODELOS_CAMION = [ {'model': 'International 4300', 'capacity': 8000, 'year_min': 2018}, {'model': 'International 4400', 'capacity': 10000, 'year_min': 2019}, {'model': 'Mercedes-Benz Axor 1725', 'capacity': 12000, 'year_min': 2020}, {'model': 'Freightliner M2 106', 'capacity': 9000, 'year_min': 2017}, {'model': 'Ford F-750', 'capacity': 7500, 'year_min': 2018}, {'model': 'Hino 268', 'capacity': 8500, 'year_min': 2019}, {'model': 'Isuzu FTR', 'capacity': 7000, 'year_min': 2020}, {'model': 'Kenworth T270', 'capacity': 9500, 'year_min': 2019}, ] PATIOS = ['Patio Norte','Patio Sur','Patio Centro','Patio Industrial'] STATUS_CAMION = ['OPERATIVO','EN_RUTA','TALLER','MANTENIMIENTO','RESERVA'] STATUS_WEIGHTS = [0.45, 0.30, 0.08, 0.07, 0.10] DEMO_USERS = [ {'full_name':'María González Demo','email':'demo@celaya.gob.mx','phone':'461-123-4567','password':'Celaya2026','role':UserRole.CIUDADANO.value}, {'full_name':'Carlos Hernández (Empleado)','email':'empleado@celaya.gob.mx','phone':'461-200-1000','password':'Empleado2026','role':UserRole.EMPLEADO.value}, {'full_name':'Lic. Patricia Ramírez (Admin)','email':'admin@celaya.gob.mx','phone':'461-100-0001','password':'Admin2026','role':UserRole.ADMIN.value}, ] # ─── Helpers ───────────────────────────────────────────────────────────────── def random_name(): nombre = random.choice(NOMBRES_H if random.random() < 0.5 else NOMBRES_M) return f"{nombre} {random.choice(APELLIDOS)} {random.choice(APELLIDOS)}" def random_email(name, i): parts = name.lower().replace('á','a').replace('é','e').replace('í','i').replace('ó','o').replace('ú','u').replace('ñ','n').split() return f"{parts[0]}.{parts[-1]}{i:03d}@celaya.gob.mx" def random_phone(): return f"461-{random.randint(100,999)}-{random.randint(1000,9999)}" def random_coords(): lat0, lng0 = random.choice(ZONAS_CELAYA) return (lat0 + random.uniform(-0.005, 0.005), lng0 + random.uniform(-0.005, 0.005)) def random_password_hash(): return hash_password(f"Pwd{random.randint(1000,9999)}!") def random_date_in_last_days(days): return datetime.utcnow() - timedelta(days=random.randint(0, days), hours=random.randint(0,23), minutes=random.randint(0,59)) # ─── Limpiar ───────────────────────────────────────────────────────────────── print("🧹 Limpiando datos previos...") db.query(ServiceRating).delete() db.query(OperationalReport).delete() db.query(Report).delete() db.query(Address).delete() db.query(User).delete() db.query(Truck).delete() db.commit() # ─── 1) Usuarios DEMO + masivos (200 total) ────────────────────────────────── print("\n👥 Creando usuarios...") demo_password_hash = hash_password("xyz") # se sobreescribe created_users = [] for u in DEMO_USERS: user = User( full_name=u['full_name'], email=u['email'], phone=u['phone'], hashed_password=hash_password(u['password']), role=u['role'], is_active=True, ) db.add(user); db.commit(); db.refresh(user) created_users.append(user) print(f" ✓ Demo: {u['role']:9} {u['email']}") # 13 empleados adicionales for i in range(13): name = random_name() user = User( full_name=name, email=random_email(name, 100+i), phone=random_phone(), hashed_password=random_password_hash(), role=UserRole.EMPLEADO.value, is_active=True, ) db.add(user); db.commit(); db.refresh(user); created_users.append(user) # 2 admins adicionales for i in range(2): name = random_name() user = User( full_name=f"Lic. {name}", email=random_email(name, 200+i), phone=random_phone(), hashed_password=random_password_hash(), role=UserRole.ADMIN.value, is_active=True, ) db.add(user); db.commit(); db.refresh(user); created_users.append(user) # 184 ciudadanos adicionales (total 185 con el demo) for i in range(184): name = random_name() user = User( full_name=name, email=random_email(name, 300+i), phone=random_phone(), hashed_password=random_password_hash(), role=UserRole.CIUDADANO.value, is_active=True, ) db.add(user); db.commit(); db.refresh(user); created_users.append(user) n_citizen = sum(1 for u in created_users if u.role == 'CIUDADANO') n_employee = sum(1 for u in created_users if u.role == 'EMPLEADO') n_admin = sum(1 for u in created_users if u.role == 'ADMIN') print(f" ✓ Total: {len(created_users)} ({n_citizen} ciudadanos, {n_employee} empleados, {n_admin} admins)") # ─── 2) Camiones (62) ──────────────────────────────────────────────────────── print("\n🚛 Creando 62 camiones...") trucks = [] for i in range(1, 63): spec = random.choice(MODELOS_CAMION) status = random.choices(STATUS_CAMION, weights=STATUS_WEIGHTS)[0] # Asignar ruta solo si está OPERATIVO o EN_RUTA route_id = f"RUTA-{random.randint(1,15):02d}" if status in ('OPERATIVO','EN_RUTA') and random.random() < 0.7 else None truck = Truck( unit_number=f"CEL-{i:03d}", plate=f"GTO-{random.randint(100,999)}-{random.choice('ABCDEFGHJKLMNP')}", model=spec['model'], year=random.randint(spec['year_min'], 2025), capacity_kg=spec['capacity'] + random.randint(-500, 500), fuel_type='DIESEL' if random.random() < 0.9 else 'GAS_LP', status=status, route_id=route_id, base=random.choice(PATIOS), odometer_km=random.randint(15000, 280000), fuel_level_pct=random.randint(20, 100), last_maintenance=datetime.utcnow() - timedelta(days=random.randint(5, 180)), next_maintenance_km=random.randint(5000, 15000) * 1, ) db.add(truck); trucks.append(truck) db.commit() print(f" ✓ {len(trucks)} camiones (status: {dict((s, sum(1 for t in trucks if t.status==s)) for s in STATUS_CAMION)})") # ─── 3) Domicilios (1-3 por ciudadano) ─────────────────────────────────────── print("\n🏠 Creando domicilios...") ciudadanos = [u for u in created_users if u.role == 'CIUDADANO'] total_addresses = 0 created_addresses = [] for citizen in ciudadanos: n = random.choices([1,1,1,2,2,3], k=1)[0] # mayoría 1, algunos 2-3 for j in range(n): lat, lng = random_coords() route_id = assign_route(lat, lng) addr = Address( user_id=citizen.id, label=random.choice(LABELS_DOMICILIO), street=f"{random.choice(CALLES)} {random.randint(1,1500)}", colony=random.choice(COLONIAS), city="Celaya", lat=lat, lng=lng, route_id=route_id, is_default=(j == 0), ) db.add(addr); created_addresses.append(addr) total_addresses += 1 db.commit() for a in created_addresses: db.refresh(a) print(f" ✓ {total_addresses} domicilios creados") # ─── 4) Reportes ciudadanos (~280) ─────────────────────────────────────────── print("\n📋 Creando reportes ciudadanos...") TYPES = ['NO_PASO','RETRASO','ACUMULACION','OTRO'] TYPE_WEIGHTS = [0.35, 0.30, 0.25, 0.10] STATUSES = ['PENDIENTE','EN_PROCESO','RESUELTO','CERRADO'] STATUS_W = [0.30, 0.20, 0.40, 0.10] DESCRIPCIONES = { 'NO_PASO': ['El camión no pasó por mi calle.', 'Hoy no escuché el camión.', 'No vi pasar el camión en su horario.', 'El camión saltó nuestra cuadra.', 'Llevamos 2 días sin recolección.'], 'RETRASO': ['Pasó más de una hora tarde.', 'Llegó cuando ya nos íbamos a trabajar.', 'El retraso es de cada vez peor en la semana.', 'Llegó casi a mediodía.', 'Estuvo retrasado más de 90 minutos.'], 'ACUMULACION': ['Hay basura acumulada en la esquina.', 'Bolsas regadas en la calle desde hace días.', 'Acumulación que atrae perros.', 'Mal olor por basura acumulada.', 'Bote desbordado en parada de camión.'], 'OTRO': ['Solicito información sobre días festivos.', 'Pregunta sobre separación de residuos.', 'Comentario general sobre el servicio.', '¿Cuándo van a pasar por electrónicos?', 'Felicitación al equipo del camión.'], } n_reports = 0 for _ in range(280): if not created_addresses: break addr = random.choice(created_addresses) rtype = random.choices(TYPES, weights=TYPE_WEIGHTS)[0] status = random.choices(STATUSES, weights=STATUS_W)[0] folio = f"MRL-{datetime.utcnow().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6].upper()}" rep = Report( user_id=addr.user_id, address_id=addr.id, folio=folio, report_type=rtype, description=random.choice(DESCRIPCIONES[rtype]), status=status, ) db.add(rep); db.flush() rep.created_at = random_date_in_last_days(60) n_reports += 1 db.commit() print(f" ✓ {n_reports} reportes ciudadanos") # ─── 5) Reportes operativos de empleados (~150) ────────────────────────────── print("\n🚧 Creando reportes operativos...") empleados = [u for u in created_users if u.role == 'EMPLEADO'] CATEGORIES = ['NO_ARRANQUE','FALLA_MECANICA','ACCIDENTE','OBSTACULO','TRAFICO','COMBUSTIBLE','CLIMA','OTRO'] CAT_W = [0.10, 0.20, 0.05, 0.15, 0.20, 0.10, 0.15, 0.05] SEVERIDADES = ['BAJA','MEDIA','ALTA'] SEV_W = [0.45, 0.40, 0.15] OP_STATUSES = ['REPORTADO','EN_ATENCION','RESUELTO'] OP_STATUS_W = [0.25, 0.20, 0.55] DESC_OP = { 'NO_ARRANQUE': ['Batería descargada esta mañana.', 'No prendió, parece arranque.', 'Marcha fallada, intentos múltiples.'], 'FALLA_MECANICA': ['Frenos haciendo ruido.', 'Pérdida de aceite visible.', 'Falla en la transmisión.', 'Sobrecalentamiento del motor.', 'Ruido extraño en la dirección.'], 'ACCIDENTE': ['Golpe leve con vehículo particular en Av. Tecnológico.', 'Raspón con poste, sin lesiones.', 'Choque sin víctimas, esperando ajustador.'], 'OBSTACULO': ['Vehículo bloqueando paso en colonia.', 'Calle inundada, no se puede acceder.', 'Árbol caído atravesando la calle.', 'Obra municipal cerrando la ruta.'], 'TRAFICO': ['Tráfico inusual por evento religioso.', 'Manifestación cerrando avenida principal.', 'Embotellamiento por accidente ajeno.'], 'COMBUSTIBLE': ['Tanque bajo, requiere recarga.', 'Bomba de combustible reportando fallas.'], 'CLIMA': ['Lluvia intensa, suspendido temporalmente.', 'Granizada en la zona alta.', 'Viento fuerte que dificulta operación.'], 'OTRO': ['Comentario al supervisor.', 'Solicitud de mantenimiento preventivo.'], } n_op = 0 for _ in range(150): if not empleados: break employee = random.choice(empleados) cat = random.choices(CATEGORIES, weights=CAT_W)[0] sev = random.choices(SEVERIDADES, weights=SEV_W)[0] st = random.choices(OP_STATUSES, weights=OP_STATUS_W)[0] folio = f"OP-{datetime.utcnow().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6].upper()}" truck = random.choice(trucks) rep = OperationalReport( employee_id=employee.id, folio=folio, category=cat, description=random.choice(DESC_OP[cat]), severity=sev, route_id=truck.route_id or f"RUTA-{random.randint(1,15):02d}", truck_id=truck.id, status=st, ) db.add(rep); db.flush() rep.created_at = random_date_in_last_days(45) if st == 'RESUELTO': rep.resolved_at = rep.created_at + timedelta(hours=random.randint(2, 72)) n_op += 1 db.commit() print(f" ✓ {n_op} reportes operativos") # ─── 6) Calificaciones de servicio (~220) ──────────────────────────────────── print("\n⭐ Creando calificaciones...") COMENTARIOS_POS = ['Servicio puntual, gracias.', 'Buen trabajo del equipo.', 'Mejoró mucho este mes.', 'Excelente atención.', None, None, None] COMENTARIOS_NEG = ['Llegaron tarde varias veces.', 'Falta limpieza después del paso.', 'Hicieron mucho ruido.', None, None] n_ratings = 0 for _ in range(220): if not created_addresses: break addr = random.choice(created_addresses) rating_val = random.choices([5,4,3,2,1], weights=[0.45, 0.30, 0.15, 0.07, 0.03])[0] if rating_val >= 4: comment = random.choice(COMENTARIOS_POS) elif rating_val == 3: comment = None else: comment = random.choice(COMENTARIOS_NEG) rating = ServiceRating( user_id=addr.user_id, address_id=addr.id, rating=rating_val, comment=comment, ) db.add(rating); db.flush() rating.created_at = random_date_in_last_days(45) n_ratings += 1 db.commit() print(f" ✓ {n_ratings} calificaciones") db.close() # ─── Resumen final ─────────────────────────────────────────────────────────── print() print("═" * 65) print(" ✅ INSERCIÓN MASIVA COMPLETADA") print("═" * 65) print(f" 👥 Usuarios: {len(created_users):>5}") print(f" - Ciudadanos: {n_citizen:>5}") print(f" - Empleados: {n_employee:>5}") print(f" - Admins: {n_admin:>5}") print(f" 🏠 Domicilios: {total_addresses:>5}") print(f" 🚛 Camiones: {len(trucks):>5}") print(f" 📋 Reportes ciudadanos: {n_reports:>5}") print(f" 🚧 Reportes operativos: {n_op:>5}") print(f" ⭐ Calificaciones servicio: {n_ratings:>5}") print() print(" CREDENCIALES DEMO (sin cambios):") print(" 🧑 demo@celaya.gob.mx / Celaya2026") print(" 👮 empleado@celaya.gob.mx / Empleado2026") print(" 🛡️ admin@celaya.gob.mx / Admin2026") print()