Files
ATLAS/backend/alembic/versions/20260121_000000_001_initial_schema.py
FlotillasGPS Developer 51d78bacf4 FlotillasGPS - Sistema completo de monitoreo de flotillas GPS
Sistema completo para monitoreo y gestion de flotas de vehiculos con:
- Backend FastAPI con PostgreSQL/TimescaleDB
- Frontend React con TypeScript y TailwindCSS
- App movil React Native con Expo
- Soporte para dispositivos GPS, Meshtastic y celulares
- Video streaming en vivo con MediaMTX
- Geocercas, alertas, viajes y reportes
- Autenticacion JWT y WebSockets en tiempo real

Documentacion completa y guias de usuario incluidas.
2026-01-21 08:18:00 +00:00

848 lines
47 KiB
Python

"""Initial schema with TimescaleDB support.
Revision ID: 001
Revises:
Create Date: 2026-01-21
Este migration crea:
- Extensiones PostgreSQL (TimescaleDB, PostGIS opcional)
- Todas las tablas del sistema
- Hypertable para ubicaciones (time-series)
- Índices y constraints
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "001"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ========================================================================
# Extensiones PostgreSQL
# ========================================================================
# TimescaleDB para series temporales
op.execute("CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;")
# PostGIS para datos geoespaciales (opcional)
op.execute("CREATE EXTENSION IF NOT EXISTS postgis;")
# UUID generation
op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";')
# ========================================================================
# Tipos ENUM
# ========================================================================
tipo_vehiculo_enum = postgresql.ENUM(
"sedan", "suv", "pickup", "van", "camion", "motocicleta", "autobus", "otro",
name="tipo_vehiculo",
create_type=True,
)
tipo_vehiculo_enum.create(op.get_bind(), checkfirst=True)
estado_vehiculo_enum = postgresql.ENUM(
"activo", "inactivo", "mantenimiento", "baja",
name="estado_vehiculo",
create_type=True,
)
estado_vehiculo_enum.create(op.get_bind(), checkfirst=True)
tipo_conductor_enum = postgresql.ENUM(
"interno", "externo", "temporal",
name="tipo_conductor",
create_type=True,
)
tipo_conductor_enum.create(op.get_bind(), checkfirst=True)
tipo_dispositivo_enum = postgresql.ENUM(
"gps", "obd", "dashcam", "sensor", "meshtastic", "otro",
name="tipo_dispositivo",
create_type=True,
)
tipo_dispositivo_enum.create(op.get_bind(), checkfirst=True)
estado_dispositivo_enum = postgresql.ENUM(
"activo", "inactivo", "sin_senal", "bateria_baja",
name="estado_dispositivo",
create_type=True,
)
estado_dispositivo_enum.create(op.get_bind(), checkfirst=True)
tipo_geocerca_enum = postgresql.ENUM(
"circular", "poligonal",
name="tipo_geocerca",
create_type=True,
)
tipo_geocerca_enum.create(op.get_bind(), checkfirst=True)
severidad_alerta_enum = postgresql.ENUM(
"baja", "media", "alta", "critica",
name="severidad_alerta",
create_type=True,
)
severidad_alerta_enum.create(op.get_bind(), checkfirst=True)
prioridad_mensaje_enum = postgresql.ENUM(
"baja", "normal", "alta", "urgente",
name="prioridad_mensaje",
create_type=True,
)
prioridad_mensaje_enum.create(op.get_bind(), checkfirst=True)
# ========================================================================
# Tabla: usuarios
# ========================================================================
op.create_table(
"usuarios",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("email", sa.String(255), nullable=False),
sa.Column("hashed_password", sa.String(255), nullable=False),
sa.Column("nombre", sa.String(100), nullable=False),
sa.Column("apellido", sa.String(100), nullable=True),
sa.Column("telefono", sa.String(20), nullable=True),
sa.Column("rol", sa.String(50), nullable=False, server_default="operador"),
sa.Column("activo", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("ultimo_login", sa.DateTime(timezone=True), nullable=True),
sa.Column("avatar_url", sa.String(500), nullable=True),
sa.Column("preferencias", postgresql.JSONB(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("email"),
)
op.create_index("ix_usuarios_email", "usuarios", ["email"])
op.create_index("ix_usuarios_rol", "usuarios", ["rol"])
# ========================================================================
# Tabla: grupos_vehiculos
# ========================================================================
op.create_table(
"grupos_vehiculos",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("nombre", sa.String(100), nullable=False),
sa.Column("descripcion", sa.Text(), nullable=True),
sa.Column("color", sa.String(7), nullable=True, server_default="#3B82F6"),
sa.Column("icono", sa.String(50), nullable=True),
sa.Column("activo", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("nombre"),
)
# ========================================================================
# Tabla: conductores
# ========================================================================
op.create_table(
"conductores",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("numero_empleado", sa.String(50), nullable=True),
sa.Column("nombre", sa.String(100), nullable=False),
sa.Column("apellido", sa.String(100), nullable=False),
sa.Column("email", sa.String(255), nullable=True),
sa.Column("telefono", sa.String(20), nullable=True),
sa.Column("licencia_numero", sa.String(50), nullable=True),
sa.Column("licencia_tipo", sa.String(20), nullable=True),
sa.Column("licencia_vigencia", sa.Date(), nullable=True),
sa.Column("fecha_nacimiento", sa.Date(), nullable=True),
sa.Column("fecha_contratacion", sa.Date(), nullable=True),
sa.Column("direccion", sa.Text(), nullable=True),
sa.Column("contacto_emergencia", sa.String(100), nullable=True),
sa.Column("telefono_emergencia", sa.String(20), nullable=True),
sa.Column("foto_url", sa.String(500), nullable=True),
sa.Column("tipo", tipo_conductor_enum, nullable=False, server_default="interno"),
sa.Column("activo", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("notas", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_conductores_numero_empleado", "conductores", ["numero_empleado"])
op.create_index("ix_conductores_licencia_numero", "conductores", ["licencia_numero"])
op.create_index("ix_conductores_activo", "conductores", ["activo"])
# ========================================================================
# Tabla: vehiculos
# ========================================================================
op.create_table(
"vehiculos",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("nombre", sa.String(100), nullable=False),
sa.Column("placa", sa.String(20), nullable=False),
sa.Column("vin", sa.String(17), nullable=True),
sa.Column("marca", sa.String(50), nullable=True),
sa.Column("modelo", sa.String(50), nullable=True),
sa.Column("anio", sa.Integer(), nullable=True),
sa.Column("color", sa.String(30), nullable=True),
sa.Column("tipo", tipo_vehiculo_enum, nullable=False, server_default="sedan"),
sa.Column("capacidad_pasajeros", sa.Integer(), nullable=True),
sa.Column("capacidad_carga_kg", sa.Float(), nullable=True),
sa.Column("odometro_actual", sa.Float(), nullable=True, server_default="0"),
sa.Column("consumo_promedio", sa.Float(), nullable=True),
sa.Column("tipo_combustible", sa.String(20), nullable=True),
sa.Column("capacidad_tanque", sa.Float(), nullable=True),
sa.Column("icono", sa.String(50), nullable=True),
sa.Column("foto_url", sa.String(500), nullable=True),
sa.Column("estado", estado_vehiculo_enum, nullable=False, server_default="activo"),
sa.Column("grupo_id", sa.Integer(), nullable=True),
sa.Column("conductor_actual_id", sa.Integer(), nullable=True),
sa.Column("ultima_latitud", sa.Float(), nullable=True),
sa.Column("ultima_longitud", sa.Float(), nullable=True),
sa.Column("ultima_velocidad", sa.Float(), nullable=True),
sa.Column("ultimo_rumbo", sa.Float(), nullable=True),
sa.Column("ultima_ubicacion_tiempo", sa.DateTime(timezone=True), nullable=True),
sa.Column("motor_encendido", sa.Boolean(), nullable=True),
sa.Column("en_movimiento", sa.Boolean(), nullable=True, server_default="false"),
sa.Column("velocidad_maxima", sa.Float(), nullable=True, server_default="120"),
sa.Column("notas", sa.Text(), nullable=True),
sa.Column("metadata", postgresql.JSONB(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("placa"),
sa.ForeignKeyConstraint(["grupo_id"], ["grupos_vehiculos.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["conductor_actual_id"], ["conductores.id"], ondelete="SET NULL"),
)
op.create_index("ix_vehiculos_placa", "vehiculos", ["placa"])
op.create_index("ix_vehiculos_estado", "vehiculos", ["estado"])
op.create_index("ix_vehiculos_grupo_id", "vehiculos", ["grupo_id"])
# ========================================================================
# Tabla: dispositivos
# ========================================================================
op.create_table(
"dispositivos",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("identificador", sa.String(100), nullable=False),
sa.Column("nombre", sa.String(100), nullable=True),
sa.Column("tipo", tipo_dispositivo_enum, nullable=False, server_default="gps"),
sa.Column("marca", sa.String(50), nullable=True),
sa.Column("modelo", sa.String(50), nullable=True),
sa.Column("numero_serie", sa.String(100), nullable=True),
sa.Column("numero_sim", sa.String(20), nullable=True),
sa.Column("operador_sim", sa.String(50), nullable=True),
sa.Column("imei", sa.String(20), nullable=True),
sa.Column("firmware_version", sa.String(50), nullable=True),
sa.Column("protocolo", sa.String(50), nullable=True),
sa.Column("estado", estado_dispositivo_enum, nullable=False, server_default="activo"),
sa.Column("vehiculo_id", sa.Integer(), nullable=True),
sa.Column("ultima_comunicacion", sa.DateTime(timezone=True), nullable=True),
sa.Column("nivel_bateria", sa.Float(), nullable=True),
sa.Column("nivel_senal", sa.Float(), nullable=True),
sa.Column("configuracion", postgresql.JSONB(), nullable=True),
sa.Column("notas", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("identificador"),
sa.ForeignKeyConstraint(["vehiculo_id"], ["vehiculos.id"], ondelete="SET NULL"),
)
op.create_index("ix_dispositivos_identificador", "dispositivos", ["identificador"])
op.create_index("ix_dispositivos_vehiculo_id", "dispositivos", ["vehiculo_id"])
op.create_index("ix_dispositivos_imei", "dispositivos", ["imei"])
# ========================================================================
# Tabla: ubicaciones (TimescaleDB Hypertable)
# ========================================================================
op.create_table(
"ubicaciones",
sa.Column("tiempo", sa.DateTime(timezone=True), nullable=False),
sa.Column("vehiculo_id", sa.Integer(), nullable=False),
sa.Column("dispositivo_id", sa.Integer(), nullable=True),
sa.Column("latitud", sa.Float(), nullable=False),
sa.Column("longitud", sa.Float(), nullable=False),
sa.Column("altitud", sa.Float(), nullable=True),
sa.Column("velocidad", sa.Float(), nullable=True),
sa.Column("rumbo", sa.Float(), nullable=True),
sa.Column("precision", sa.Float(), nullable=True),
sa.Column("satelites", sa.Integer(), nullable=True),
sa.Column("hdop", sa.Float(), nullable=True),
sa.Column("motor_encendido", sa.Boolean(), nullable=True),
sa.Column("nivel_bateria", sa.Float(), nullable=True),
sa.Column("nivel_combustible", sa.Float(), nullable=True),
sa.Column("odometro", sa.Float(), nullable=True),
sa.Column("temperatura", sa.Float(), nullable=True),
sa.Column("evento", sa.String(50), nullable=True),
sa.Column("direccion", sa.String(500), nullable=True),
sa.Column("datos_extra", postgresql.JSONB(), nullable=True),
sa.PrimaryKeyConstraint("tiempo", "vehiculo_id"),
sa.ForeignKeyConstraint(["vehiculo_id"], ["vehiculos.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["dispositivo_id"], ["dispositivos.id"], ondelete="SET NULL"),
)
# Convertir a hypertable de TimescaleDB
op.execute(
"SELECT create_hypertable('ubicaciones', 'tiempo', "
"chunk_time_interval => INTERVAL '1 day', "
"if_not_exists => TRUE);"
)
# Índices para ubicaciones
op.create_index("ix_ubicaciones_vehiculo_tiempo", "ubicaciones", ["vehiculo_id", "tiempo"])
op.create_index("ix_ubicaciones_tiempo", "ubicaciones", ["tiempo"])
# Políticas de retención y compresión
op.execute(
"SELECT add_retention_policy('ubicaciones', INTERVAL '1 year', if_not_exists => TRUE);"
)
op.execute(
"ALTER TABLE ubicaciones SET ("
"timescaledb.compress, "
"timescaledb.compress_segmentby = 'vehiculo_id'"
");"
)
op.execute(
"SELECT add_compression_policy('ubicaciones', INTERVAL '7 days', if_not_exists => TRUE);"
)
# ========================================================================
# Tabla: viajes
# ========================================================================
op.create_table(
"viajes",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("vehiculo_id", sa.Integer(), nullable=False),
sa.Column("conductor_id", sa.Integer(), nullable=True),
sa.Column("inicio", sa.DateTime(timezone=True), nullable=False),
sa.Column("fin", sa.DateTime(timezone=True), nullable=True),
sa.Column("duracion_segundos", sa.Integer(), nullable=True),
sa.Column("distancia_km", sa.Float(), nullable=True),
sa.Column("velocidad_maxima", sa.Float(), nullable=True),
sa.Column("velocidad_promedio", sa.Float(), nullable=True),
sa.Column("latitud_inicio", sa.Float(), nullable=True),
sa.Column("longitud_inicio", sa.Float(), nullable=True),
sa.Column("latitud_fin", sa.Float(), nullable=True),
sa.Column("longitud_fin", sa.Float(), nullable=True),
sa.Column("direccion_inicio", sa.String(500), nullable=True),
sa.Column("direccion_fin", sa.String(500), nullable=True),
sa.Column("odometro_inicio", sa.Float(), nullable=True),
sa.Column("odometro_fin", sa.Float(), nullable=True),
sa.Column("combustible_consumido", sa.Float(), nullable=True),
sa.Column("costo_combustible", sa.Float(), nullable=True),
sa.Column("num_paradas", sa.Integer(), nullable=True, server_default="0"),
sa.Column("num_alertas", sa.Integer(), nullable=True, server_default="0"),
sa.Column("puntuacion_conduccion", sa.Float(), nullable=True),
sa.Column("proposito", sa.String(100), nullable=True),
sa.Column("notas", sa.Text(), nullable=True),
sa.Column("ruta_simplificada", postgresql.JSONB(), nullable=True),
sa.Column("activo", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(["vehiculo_id"], ["vehiculos.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["conductor_id"], ["conductores.id"], ondelete="SET NULL"),
)
op.create_index("ix_viajes_vehiculo_id", "viajes", ["vehiculo_id"])
op.create_index("ix_viajes_inicio", "viajes", ["inicio"])
op.create_index("ix_viajes_vehiculo_inicio", "viajes", ["vehiculo_id", "inicio"])
# ========================================================================
# Tabla: paradas
# ========================================================================
op.create_table(
"paradas",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("viaje_id", sa.Integer(), nullable=False),
sa.Column("vehiculo_id", sa.Integer(), nullable=False),
sa.Column("inicio", sa.DateTime(timezone=True), nullable=False),
sa.Column("fin", sa.DateTime(timezone=True), nullable=True),
sa.Column("duracion_segundos", sa.Integer(), nullable=True),
sa.Column("latitud", sa.Float(), nullable=False),
sa.Column("longitud", sa.Float(), nullable=False),
sa.Column("direccion", sa.String(500), nullable=True),
sa.Column("motor_encendido", sa.Boolean(), nullable=True),
sa.Column("poi_id", sa.Integer(), nullable=True),
sa.Column("geocerca_id", sa.Integer(), nullable=True),
sa.Column("tipo", sa.String(50), nullable=True),
sa.Column("notas", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(["viaje_id"], ["viajes.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["vehiculo_id"], ["vehiculos.id"], ondelete="CASCADE"),
)
op.create_index("ix_paradas_viaje_id", "paradas", ["viaje_id"])
op.create_index("ix_paradas_vehiculo_id", "paradas", ["vehiculo_id"])
# ========================================================================
# Tabla: tipos_alerta
# ========================================================================
op.create_table(
"tipos_alerta",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("codigo", sa.String(50), nullable=False),
sa.Column("nombre", sa.String(100), nullable=False),
sa.Column("descripcion", sa.Text(), nullable=True),
sa.Column("severidad", severidad_alerta_enum, nullable=False, server_default="media"),
sa.Column("icono", sa.String(50), nullable=True),
sa.Column("color", sa.String(7), nullable=True),
sa.Column("sonido", sa.String(100), nullable=True),
sa.Column("requiere_atencion", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("notificar_email", sa.Boolean(), nullable=False, server_default="false"),
sa.Column("notificar_push", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("notificar_sms", sa.Boolean(), nullable=False, server_default="false"),
sa.Column("activo", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("configuracion", postgresql.JSONB(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("codigo"),
)
# ========================================================================
# Tabla: alertas
# ========================================================================
op.create_table(
"alertas",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("tipo_alerta_id", sa.Integer(), nullable=False),
sa.Column("vehiculo_id", sa.Integer(), nullable=False),
sa.Column("conductor_id", sa.Integer(), nullable=True),
sa.Column("viaje_id", sa.Integer(), nullable=True),
sa.Column("tiempo", sa.DateTime(timezone=True), nullable=False),
sa.Column("latitud", sa.Float(), nullable=True),
sa.Column("longitud", sa.Float(), nullable=True),
sa.Column("direccion", sa.String(500), nullable=True),
sa.Column("valor_detectado", sa.Float(), nullable=True),
sa.Column("valor_limite", sa.Float(), nullable=True),
sa.Column("mensaje", sa.Text(), nullable=True),
sa.Column("datos_extra", postgresql.JSONB(), nullable=True),
sa.Column("atendida", sa.Boolean(), nullable=False, server_default="false"),
sa.Column("atendida_por_id", sa.Integer(), nullable=True),
sa.Column("atendida_tiempo", sa.DateTime(timezone=True), nullable=True),
sa.Column("comentario_atencion", sa.Text(), nullable=True),
sa.Column("notificada", sa.Boolean(), nullable=False, server_default="false"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(["tipo_alerta_id"], ["tipos_alerta.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["vehiculo_id"], ["vehiculos.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["conductor_id"], ["conductores.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["viaje_id"], ["viajes.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["atendida_por_id"], ["usuarios.id"], ondelete="SET NULL"),
)
op.create_index("ix_alertas_vehiculo_id", "alertas", ["vehiculo_id"])
op.create_index("ix_alertas_tiempo", "alertas", ["tiempo"])
op.create_index("ix_alertas_atendida", "alertas", ["atendida"])
op.create_index("ix_alertas_tipo_tiempo", "alertas", ["tipo_alerta_id", "tiempo"])
# ========================================================================
# Tabla: geocercas
# ========================================================================
op.create_table(
"geocercas",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("nombre", sa.String(100), nullable=False),
sa.Column("descripcion", sa.Text(), nullable=True),
sa.Column("tipo", tipo_geocerca_enum, nullable=False),
sa.Column("centro_latitud", sa.Float(), nullable=True),
sa.Column("centro_longitud", sa.Float(), nullable=True),
sa.Column("radio_metros", sa.Float(), nullable=True),
sa.Column("coordenadas", postgresql.JSONB(), nullable=True),
sa.Column("color", sa.String(7), nullable=True, server_default="#EF4444"),
sa.Column("alertar_entrada", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("alertar_salida", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("velocidad_maxima", sa.Float(), nullable=True),
sa.Column("horario_inicio", sa.Time(), nullable=True),
sa.Column("horario_fin", sa.Time(), nullable=True),
sa.Column("dias_semana", postgresql.ARRAY(sa.Integer()), nullable=True),
sa.Column("activa", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("metadata", postgresql.JSONB(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_geocercas_activa", "geocercas", ["activa"])
# ========================================================================
# Tabla: geocercas_vehiculos (many-to-many)
# ========================================================================
op.create_table(
"geocercas_vehiculos",
sa.Column("geocerca_id", sa.Integer(), nullable=False),
sa.Column("vehiculo_id", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint("geocerca_id", "vehiculo_id"),
sa.ForeignKeyConstraint(["geocerca_id"], ["geocercas.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["vehiculo_id"], ["vehiculos.id"], ondelete="CASCADE"),
)
# ========================================================================
# Tabla: pois (puntos de interés)
# ========================================================================
op.create_table(
"pois",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("nombre", sa.String(100), nullable=False),
sa.Column("descripcion", sa.Text(), nullable=True),
sa.Column("categoria", sa.String(50), nullable=True),
sa.Column("latitud", sa.Float(), nullable=False),
sa.Column("longitud", sa.Float(), nullable=False),
sa.Column("direccion", sa.String(500), nullable=True),
sa.Column("telefono", sa.String(20), nullable=True),
sa.Column("email", sa.String(255), nullable=True),
sa.Column("horario", sa.String(200), nullable=True),
sa.Column("icono", sa.String(50), nullable=True),
sa.Column("color", sa.String(7), nullable=True),
sa.Column("radio_metros", sa.Float(), nullable=True, server_default="100"),
sa.Column("activo", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("metadata", postgresql.JSONB(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_pois_categoria", "pois", ["categoria"])
op.create_index("ix_pois_activo", "pois", ["activo"])
# Update paradas foreign key to pois
op.create_foreign_key(
"fk_paradas_poi_id",
"paradas", "pois",
["poi_id"], ["id"],
ondelete="SET NULL"
)
op.create_foreign_key(
"fk_paradas_geocerca_id",
"paradas", "geocercas",
["geocerca_id"], ["id"],
ondelete="SET NULL"
)
# ========================================================================
# Tabla: cargas_combustible
# ========================================================================
op.create_table(
"cargas_combustible",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("vehiculo_id", sa.Integer(), nullable=False),
sa.Column("conductor_id", sa.Integer(), nullable=True),
sa.Column("fecha", sa.DateTime(timezone=True), nullable=False),
sa.Column("litros", sa.Float(), nullable=False),
sa.Column("precio_litro", sa.Float(), nullable=True),
sa.Column("costo_total", sa.Float(), nullable=True),
sa.Column("odometro", sa.Float(), nullable=True),
sa.Column("tanque_lleno", sa.Boolean(), nullable=True, server_default="true"),
sa.Column("tipo_combustible", sa.String(20), nullable=True),
sa.Column("estacion", sa.String(100), nullable=True),
sa.Column("latitud", sa.Float(), nullable=True),
sa.Column("longitud", sa.Float(), nullable=True),
sa.Column("numero_factura", sa.String(50), nullable=True),
sa.Column("foto_ticket_url", sa.String(500), nullable=True),
sa.Column("notas", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(["vehiculo_id"], ["vehiculos.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["conductor_id"], ["conductores.id"], ondelete="SET NULL"),
)
op.create_index("ix_cargas_combustible_vehiculo_id", "cargas_combustible", ["vehiculo_id"])
op.create_index("ix_cargas_combustible_fecha", "cargas_combustible", ["fecha"])
# ========================================================================
# Tabla: tipos_mantenimiento
# ========================================================================
op.create_table(
"tipos_mantenimiento",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("codigo", sa.String(50), nullable=False),
sa.Column("nombre", sa.String(100), nullable=False),
sa.Column("descripcion", sa.Text(), nullable=True),
sa.Column("categoria", sa.String(50), nullable=True),
sa.Column("intervalo_km", sa.Float(), nullable=True),
sa.Column("intervalo_dias", sa.Integer(), nullable=True),
sa.Column("costo_estimado", sa.Float(), nullable=True),
sa.Column("duracion_estimada_horas", sa.Float(), nullable=True),
sa.Column("activo", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("codigo"),
)
# ========================================================================
# Tabla: mantenimientos
# ========================================================================
op.create_table(
"mantenimientos",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("vehiculo_id", sa.Integer(), nullable=False),
sa.Column("tipo_mantenimiento_id", sa.Integer(), nullable=False),
sa.Column("fecha_programada", sa.DateTime(timezone=True), nullable=True),
sa.Column("fecha_realizado", sa.DateTime(timezone=True), nullable=True),
sa.Column("odometro_programado", sa.Float(), nullable=True),
sa.Column("odometro_realizado", sa.Float(), nullable=True),
sa.Column("estado", sa.String(20), nullable=False, server_default="pendiente"),
sa.Column("proveedor", sa.String(100), nullable=True),
sa.Column("costo", sa.Float(), nullable=True),
sa.Column("duracion_horas", sa.Float(), nullable=True),
sa.Column("descripcion_trabajo", sa.Text(), nullable=True),
sa.Column("repuestos_utilizados", postgresql.JSONB(), nullable=True),
sa.Column("numero_factura", sa.String(50), nullable=True),
sa.Column("documento_url", sa.String(500), nullable=True),
sa.Column("notas", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(["vehiculo_id"], ["vehiculos.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["tipo_mantenimiento_id"], ["tipos_mantenimiento.id"], ondelete="CASCADE"),
)
op.create_index("ix_mantenimientos_vehiculo_id", "mantenimientos", ["vehiculo_id"])
op.create_index("ix_mantenimientos_estado", "mantenimientos", ["estado"])
op.create_index("ix_mantenimientos_fecha_programada", "mantenimientos", ["fecha_programada"])
# ========================================================================
# Tabla: camaras
# ========================================================================
op.create_table(
"camaras",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("identificador", sa.String(100), nullable=False),
sa.Column("nombre", sa.String(100), nullable=True),
sa.Column("vehiculo_id", sa.Integer(), nullable=True),
sa.Column("tipo", sa.String(50), nullable=True),
sa.Column("posicion", sa.String(50), nullable=True),
sa.Column("resolucion", sa.String(20), nullable=True),
sa.Column("url_stream", sa.String(500), nullable=True),
sa.Column("url_snapshot", sa.String(500), nullable=True),
sa.Column("activa", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("grabando", sa.Boolean(), nullable=False, server_default="false"),
sa.Column("configuracion", postgresql.JSONB(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("identificador"),
sa.ForeignKeyConstraint(["vehiculo_id"], ["vehiculos.id"], ondelete="SET NULL"),
)
op.create_index("ix_camaras_vehiculo_id", "camaras", ["vehiculo_id"])
# ========================================================================
# Tabla: grabaciones
# ========================================================================
op.create_table(
"grabaciones",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("camara_id", sa.Integer(), nullable=False),
sa.Column("vehiculo_id", sa.Integer(), nullable=True),
sa.Column("inicio", sa.DateTime(timezone=True), nullable=False),
sa.Column("fin", sa.DateTime(timezone=True), nullable=True),
sa.Column("duracion_segundos", sa.Integer(), nullable=True),
sa.Column("tamano_bytes", sa.BigInteger(), nullable=True),
sa.Column("archivo_url", sa.String(500), nullable=True),
sa.Column("thumbnail_url", sa.String(500), nullable=True),
sa.Column("estado", sa.String(20), nullable=False, server_default="grabando"),
sa.Column("tipo", sa.String(50), nullable=True),
sa.Column("metadata", postgresql.JSONB(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(["camara_id"], ["camaras.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["vehiculo_id"], ["vehiculos.id"], ondelete="SET NULL"),
)
op.create_index("ix_grabaciones_camara_id", "grabaciones", ["camara_id"])
op.create_index("ix_grabaciones_inicio", "grabaciones", ["inicio"])
# ========================================================================
# Tabla: eventos_video
# ========================================================================
op.create_table(
"eventos_video",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("grabacion_id", sa.Integer(), nullable=True),
sa.Column("camara_id", sa.Integer(), nullable=False),
sa.Column("vehiculo_id", sa.Integer(), nullable=True),
sa.Column("tiempo", sa.DateTime(timezone=True), nullable=False),
sa.Column("tipo", sa.String(50), nullable=False),
sa.Column("descripcion", sa.Text(), nullable=True),
sa.Column("latitud", sa.Float(), nullable=True),
sa.Column("longitud", sa.Float(), nullable=True),
sa.Column("snapshot_url", sa.String(500), nullable=True),
sa.Column("clip_url", sa.String(500), nullable=True),
sa.Column("duracion_clip_segundos", sa.Integer(), nullable=True),
sa.Column("confianza", sa.Float(), nullable=True),
sa.Column("metadata", postgresql.JSONB(), nullable=True),
sa.Column("revisado", sa.Boolean(), nullable=False, server_default="false"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(["grabacion_id"], ["grabaciones.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["camara_id"], ["camaras.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["vehiculo_id"], ["vehiculos.id"], ondelete="SET NULL"),
)
op.create_index("ix_eventos_video_camara_id", "eventos_video", ["camara_id"])
op.create_index("ix_eventos_video_tiempo", "eventos_video", ["tiempo"])
op.create_index("ix_eventos_video_tipo", "eventos_video", ["tipo"])
# ========================================================================
# Tabla: mensajes
# ========================================================================
op.create_table(
"mensajes",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("remitente_usuario_id", sa.Integer(), nullable=True),
sa.Column("remitente_conductor_id", sa.Integer(), nullable=True),
sa.Column("destinatario_usuario_id", sa.Integer(), nullable=True),
sa.Column("destinatario_conductor_id", sa.Integer(), nullable=True),
sa.Column("destinatario_vehiculo_id", sa.Integer(), nullable=True),
sa.Column("asunto", sa.String(200), nullable=True),
sa.Column("contenido", sa.Text(), nullable=False),
sa.Column("tipo", sa.String(50), nullable=False, server_default="texto"),
sa.Column("prioridad", prioridad_mensaje_enum, nullable=False, server_default="normal"),
sa.Column("leido", sa.Boolean(), nullable=False, server_default="false"),
sa.Column("leido_tiempo", sa.DateTime(timezone=True), nullable=True),
sa.Column("respuesta_a_id", sa.Integer(), nullable=True),
sa.Column("adjuntos", postgresql.JSONB(), nullable=True),
sa.Column("metadata", postgresql.JSONB(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(["remitente_usuario_id"], ["usuarios.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["remitente_conductor_id"], ["conductores.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["destinatario_usuario_id"], ["usuarios.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["destinatario_conductor_id"], ["conductores.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["destinatario_vehiculo_id"], ["vehiculos.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["respuesta_a_id"], ["mensajes.id"], ondelete="SET NULL"),
)
op.create_index("ix_mensajes_destinatario_usuario_id", "mensajes", ["destinatario_usuario_id"])
op.create_index("ix_mensajes_destinatario_conductor_id", "mensajes", ["destinatario_conductor_id"])
op.create_index("ix_mensajes_leido", "mensajes", ["leido"])
# ========================================================================
# Tabla: configuracion
# ========================================================================
op.create_table(
"configuracion",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("clave", sa.String(100), nullable=False),
sa.Column("valor", sa.Text(), nullable=True),
sa.Column("tipo", sa.String(20), nullable=False, server_default="string"),
sa.Column("categoria", sa.String(50), nullable=True),
sa.Column("descripcion", sa.Text(), nullable=True),
sa.Column("editable", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("usuario_id", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("clave", "usuario_id", name="uq_configuracion_clave_usuario"),
sa.ForeignKeyConstraint(["usuario_id"], ["usuarios.id"], ondelete="CASCADE"),
)
op.create_index("ix_configuracion_clave", "configuracion", ["clave"])
op.create_index("ix_configuracion_categoria", "configuracion", ["categoria"])
# ========================================================================
# Datos iniciales: Tipos de alerta predefinidos
# ========================================================================
op.execute("""
INSERT INTO tipos_alerta (codigo, nombre, descripcion, severidad, icono, color, requiere_atencion)
VALUES
('EXCESO_VELOCIDAD', 'Exceso de velocidad', 'El vehículo superó el límite de velocidad configurado', 'alta', 'speed', '#EF4444', true),
('ENTRADA_GEOCERCA', 'Entrada a geocerca', 'El vehículo entró a una zona delimitada', 'media', 'map-pin', '#3B82F6', false),
('SALIDA_GEOCERCA', 'Salida de geocerca', 'El vehículo salió de una zona delimitada', 'media', 'map-pin', '#F59E0B', false),
('BATERIA_BAJA', 'Batería baja', 'La batería del dispositivo está por debajo del nivel crítico', 'alta', 'battery-low', '#EF4444', true),
('SIN_SENAL', 'Sin señal GPS', 'El dispositivo perdió señal GPS por tiempo prolongado', 'alta', 'signal-off', '#EF4444', true),
('MOTOR_ENCENDIDO', 'Motor encendido', 'El motor del vehículo fue encendido', 'baja', 'power', '#22C55E', false),
('MOTOR_APAGADO', 'Motor apagado', 'El motor del vehículo fue apagado', 'baja', 'power-off', '#6B7280', false),
('PARADA_PROLONGADA', 'Parada prolongada', 'El vehículo lleva detenido más tiempo del permitido', 'media', 'clock', '#F59E0B', true),
('ACELERACION_BRUSCA', 'Aceleración brusca', 'Se detectó una aceleración brusca', 'media', 'trending-up', '#F59E0B', false),
('FRENADO_BRUSCO', 'Frenado brusco', 'Se detectó un frenado brusco', 'media', 'trending-down', '#F59E0B', false),
('MANTENIMIENTO_PROXIMO', 'Mantenimiento próximo', 'El vehículo requiere mantenimiento pronto', 'media', 'tool', '#3B82F6', true),
('LICENCIA_POR_VENCER', 'Licencia por vencer', 'La licencia del conductor está próxima a vencer', 'alta', 'id-card', '#EF4444', true),
('COLISION_DETECTADA', 'Colisión detectada', 'Se detectó una posible colisión', 'critica', 'alert-triangle', '#DC2626', true),
('SOS', 'Botón de pánico', 'El conductor presionó el botón de emergencia', 'critica', 'alert-circle', '#DC2626', true)
ON CONFLICT DO NOTHING;
""")
# ========================================================================
# Datos iniciales: Tipos de mantenimiento predefinidos
# ========================================================================
op.execute("""
INSERT INTO tipos_mantenimiento (codigo, nombre, descripcion, categoria, intervalo_km, intervalo_dias)
VALUES
('CAMBIO_ACEITE', 'Cambio de aceite', 'Cambio de aceite y filtro de motor', 'Motor', 5000, 90),
('CAMBIO_FILTRO_AIRE', 'Cambio de filtro de aire', 'Reemplazo del filtro de aire del motor', 'Motor', 15000, 365),
('CAMBIO_FILTRO_COMBUSTIBLE', 'Cambio de filtro de combustible', 'Reemplazo del filtro de combustible', 'Motor', 20000, 365),
('CAMBIO_BUJIAS', 'Cambio de bujías', 'Reemplazo de bujías de encendido', 'Motor', 40000, NULL),
('ROTACION_LLANTAS', 'Rotación de llantas', 'Rotación de llantas para desgaste uniforme', 'Llantas', 10000, 180),
('ALINEACION_BALANCEO', 'Alineación y balanceo', 'Alineación y balanceo de llantas', 'Llantas', 20000, 365),
('CAMBIO_LLANTAS', 'Cambio de llantas', 'Reemplazo de llantas desgastadas', 'Llantas', 50000, NULL),
('REVISION_FRENOS', 'Revisión de frenos', 'Inspección del sistema de frenos', 'Frenos', 20000, 180),
('CAMBIO_BALATAS', 'Cambio de balatas', 'Reemplazo de balatas/pastillas de freno', 'Frenos', 40000, NULL),
('CAMBIO_DISCOS', 'Cambio de discos', 'Reemplazo de discos de freno', 'Frenos', 80000, NULL),
('REVISION_SUSPENSION', 'Revisión de suspensión', 'Inspección del sistema de suspensión', 'Suspensión', 30000, 365),
('CAMBIO_AMORTIGUADORES', 'Cambio de amortiguadores', 'Reemplazo de amortiguadores', 'Suspensión', 80000, NULL),
('REVISION_TRANSMISION', 'Revisión de transmisión', 'Inspección de la transmisión', 'Transmisión', 50000, 365),
('CAMBIO_LIQUIDO_TRANSMISION', 'Cambio de líquido de transmisión', 'Reemplazo del líquido de transmisión', 'Transmisión', 60000, NULL),
('REVISION_BATERIA', 'Revisión de batería', 'Inspección y prueba de batería', 'Eléctrico', NULL, 180),
('CAMBIO_BATERIA', 'Cambio de batería', 'Reemplazo de batería', 'Eléctrico', NULL, 730),
('REVISION_LUCES', 'Revisión de luces', 'Inspección del sistema de iluminación', 'Eléctrico', NULL, 90),
('REVISION_AC', 'Revisión de A/C', 'Inspección del sistema de aire acondicionado', 'Climatización', NULL, 365),
('RECARGA_AC', 'Recarga de A/C', 'Recarga de gas refrigerante del A/C', 'Climatización', NULL, 730),
('VERIFICACION_VEHICULAR', 'Verificación vehicular', 'Verificación de emisiones contaminantes', 'Legal', NULL, 180),
('REVISION_GENERAL', 'Revisión general', 'Inspección general del vehículo', 'General', 10000, 90)
ON CONFLICT DO NOTHING;
""")
def downgrade() -> None:
# Drop tables in reverse order
op.drop_table("configuracion")
op.drop_table("mensajes")
op.drop_table("eventos_video")
op.drop_table("grabaciones")
op.drop_table("camaras")
op.drop_table("mantenimientos")
op.drop_table("tipos_mantenimiento")
op.drop_table("cargas_combustible")
op.drop_table("pois")
op.drop_table("geocercas_vehiculos")
op.drop_table("geocercas")
op.drop_table("alertas")
op.drop_table("tipos_alerta")
op.drop_table("paradas")
op.drop_table("viajes")
op.drop_table("ubicaciones")
op.drop_table("dispositivos")
op.drop_table("vehiculos")
op.drop_table("conductores")
op.drop_table("grupos_vehiculos")
op.drop_table("usuarios")
# Drop ENUMs
op.execute("DROP TYPE IF EXISTS prioridad_mensaje;")
op.execute("DROP TYPE IF EXISTS severidad_alerta;")
op.execute("DROP TYPE IF EXISTS tipo_geocerca;")
op.execute("DROP TYPE IF EXISTS estado_dispositivo;")
op.execute("DROP TYPE IF EXISTS tipo_dispositivo;")
op.execute("DROP TYPE IF EXISTS tipo_conductor;")
op.execute("DROP TYPE IF EXISTS estado_vehiculo;")
op.execute("DROP TYPE IF EXISTS tipo_vehiculo;")
# Drop extensions
op.execute("DROP EXTENSION IF EXISTS postgis;")
op.execute('DROP EXTENSION IF EXISTS "uuid-ossp";')
# Note: TimescaleDB extension usually can't be dropped easily