-- ======================================================================================== -- SISTEMA DE GESTIÓN PELUQUERÍA CANINA LUTHOR - ARQUITECTURA EMPRESARIAL COMPLETA v3.0 -- ======================================================================================== -- TOTAL LÍNEAS: 3,247 -- AUTOR: Arquitecto Senior de Bases de Datos -- FECHA: 2026 -- ======================================================================================== -- ============================================================ -- 1. CONFIGURACIÓN INICIAL Y EXTENSIONES -- ============================================================ -- Crear esquema principal CREATE SCHEMA IF NOT EXISTS luthor; SET search_path TO luthor, public; -- Extensiones necesarias CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pg_trgm"; CREATE EXTENSION IF NOT EXISTS "btree_gin"; CREATE EXTENSION IF NOT EXISTS "btree_gist"; CREATE EXTENSION IF NOT EXISTS "pgcrypto"; CREATE EXTENSION IF NOT EXISTS "tablefunc"; -- ============================================================ -- 2. PARÁMETROS DE CONFIGURACIÓN DEL SISTEMA -- ============================================================ CREATE TABLE luthor.configuracion_sistema ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), clave VARCHAR(100) UNIQUE NOT NULL, valor TEXT NOT NULL, tipo_dato VARCHAR(20) NOT NULL CHECK (tipo_dato IN ('STRING', 'INTEGER', 'BOOLEAN', 'DECIMAL', 'JSON', 'DATE')), descripcion TEXT, categoria VARCHAR(50) NOT NULL, modificable_publico BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID ); -- ============================================================ -- 3. TABLAS PRINCIPALES - DOMINIO DE NEGOCIO -- ============================================================ -- 3.1. USUARIOS (Sistema de autenticación y perfiles completo) CREATE TABLE luthor.usuarios ( -- Identificador y autenticación user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, salt VARCHAR(64) NOT NULL, token_recuperacion VARCHAR(255), token_expiracion TIMESTAMPTZ, -- Datos personales nombre_completo VARCHAR(200) NOT NULL, documento_identidad VARCHAR(20) UNIQUE, tipo_documento VARCHAR(10) DEFAULT 'CC' CHECK (tipo_documento IN ('CC', 'CE', 'NIT', 'PS', 'PASAPORTE')), telefono_principal VARCHAR(20), telefono_secundario VARCHAR(20), direccion TEXT, ciudad VARCHAR(100), departamento VARCHAR(100), pais VARCHAR(50) DEFAULT 'Colombia', codigo_postal VARCHAR(20), fecha_nacimiento DATE, genero CHAR(1) CHECK (genero IN ('M', 'F', 'O')), -- Datos del sistema rol VARCHAR(20) NOT NULL DEFAULT 'CLIENTE' CHECK (rol IN ('SUPER_ADMIN', 'ADMIN', 'GERENTE', 'VETERINARIO', 'ESTILISTA', 'CAJA', 'CLIENTE', 'INVITADO')), nivel_acceso INTEGER DEFAULT 1 CHECK (nivel_acceso BETWEEN 1 AND 5), permisos JSONB DEFAULT '{}'::jsonb, -- Estado y control de acceso estado VARCHAR(20) DEFAULT 'ACTIVO' CHECK (estado IN ('ACTIVO', 'INACTIVO', 'BLOQUEADO', 'SUSPENDIDO', 'ELIMINADO')), intentos_fallidos INTEGER DEFAULT 0, bloqueado_hasta TIMESTAMPTZ, ultimo_login TIMESTAMPTZ, ultima_ip INET, ultimo_user_agent TEXT, sesion_activa UUID, -- Preferencias preferencias JSONB DEFAULT '{}'::jsonb, idioma VARCHAR(10) DEFAULT 'es', zona_horaria VARCHAR(50) DEFAULT 'America/Bogota', formato_fecha VARCHAR(20) DEFAULT 'DD/MM/YYYY', -- Métricas total_citas INTEGER DEFAULT 0, total_gastado DECIMAL(10,2) DEFAULT 0, promedio_calificacion DECIMAL(3,2), ultima_actividad TIMESTAMPTZ, -- Auditoría created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMPTZ, created_by UUID, updated_by UUID, -- Constraints CONSTRAINT email_valido CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'), CONSTRAINT username_length CHECK (LENGTH(username) >= 3), CONSTRAINT nombre_completo_length CHECK (LENGTH(nombre_completo) >= 2) ); -- Índices de usuario CREATE INDEX idx_usuarios_email ON luthor.usuarios(email) WHERE deleted_at IS NULL; CREATE INDEX idx_usuarios_username ON luthor.usuarios(username) WHERE deleted_at IS NULL; CREATE INDEX idx_usuarios_rol ON luthor.usuarios(rol); CREATE INDEX idx_usuarios_estado ON luthor.usuarios(estado); CREATE INDEX idx_usuarios_ciudad ON luthor.usuarios(ciudad); CREATE INDEX idx_usuarios_documento ON luthor.usuarios(documento_identidad) WHERE deleted_at IS NULL; CREATE INDEX idx_usuarios_nombre_trgm ON luthor.usuarios USING gin (nombre_completo gin_trgm_ops); CREATE INDEX idx_usuarios_email_trgm ON luthor.usuarios USING gin (email gin_trgm_ops); CREATE INDEX idx_usuarios_busqueda ON luthor.usuarios USING gin( to_tsvector('spanish', COALESCE(nombre_completo, '') || ' ' || COALESCE(email, '') || ' ' || COALESCE(documento_identidad, '') || ' ' || COALESCE(telefono_principal, '') ) ); CREATE INDEX idx_usuarios_ultimo_login ON luthor.usuarios(ultimo_login DESC); CREATE INDEX idx_usuarios_created_at ON luthor.usuarios(created_at DESC); -- 3.2. MASCOTAS (Historial médico completo y seguimiento) CREATE TABLE luthor.mascotas ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), propietario_id UUID NOT NULL REFERENCES luthor.usuarios(user_id) ON DELETE RESTRICT, -- Datos básicos nombre VARCHAR(100) NOT NULL, especie VARCHAR(50) NOT NULL DEFAULT 'PERRO' CHECK (especie IN ('PERRO', 'GATO', 'AVE', 'ROEDOR', 'REPTIL', 'PEZ', 'OTRO')), raza VARCHAR(100), color VARCHAR(50), sexo CHAR(1) NOT NULL CHECK (sexo IN ('M', 'F')), fecha_nacimiento DATE, edad_estimada BOOLEAN DEFAULT false, peso_actual DECIMAL(5,2) CHECK (peso_actual > 0), unidad_peso VARCHAR(10) DEFAULT 'KG' CHECK (unidad_peso IN ('KG', 'LB')), microchip VARCHAR(20) UNIQUE, numero_registro VARCHAR(50), -- Datos médicos alergias_conocidas TEXT, condiciones_medicas TEXT, medicamentos_actuales TEXT, comportamiento TEXT, dieta_especial TEXT, vacunas TEXT, desparasitaciones TEXT, esterilizado BOOLEAN DEFAULT false, fecha_esterilizacion DATE, -- Estado de salud nivel_actividad VARCHAR(20) DEFAULT 'NORMAL' CHECK (nivel_actividad IN ('BAJO', 'NORMAL', 'ALTO', 'MUY_ALTO')), condicion_fisica VARCHAR(20) DEFAULT 'BUENA' CHECK (condicion_fisica IN ('EXCELENTE', 'BUENA', 'REGULAR', 'MALA', 'CRITICA')), -- Características tamanio VARCHAR(20) CHECK (tamanio IN ('PEQUEÑO', 'MEDIANO', 'GRANDE', 'GIGANTE')), pelaje VARCHAR(30) CHECK (pelaje IN ('CORTO', 'MEDIO', 'LARGO', 'RIZADO', 'LANUDO', 'SIN_PELAJE')), -- Gestión activo BOOLEAN DEFAULT true, requiere_atencion_especial BOOLEAN DEFAULT false, necesita_bozal BOOLEAN DEFAULT false, es_peligroso BOOLEAN DEFAULT false, ultima_visita TIMESTAMPTZ, proxima_visita TIMESTAMPTZ, -- Notas y observaciones notas_generales TEXT, instrucciones_especiales TEXT, -- Auditoría created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMPTZ, created_by UUID REFERENCES luthor.usuarios(user_id), updated_by UUID REFERENCES luthor.usuarios(user_id), CONSTRAINT peso_positivo CHECK (peso_actual > 0), CONSTRAINT fecha_nacimiento_valida CHECK (fecha_nacimiento <= CURRENT_DATE) ); -- Índices de mascotas CREATE INDEX idx_mascotas_propietario ON luthor.mascotas(propietario_id) WHERE deleted_at IS NULL; CREATE INDEX idx_mascotas_nombre_trgm ON luthor.mascotas USING gin (nombre gin_trgm_ops); CREATE INDEX idx_mascotas_especie ON luthor.mascotas(especie); CREATE INDEX idx_mascotas_microchip ON luthor.mascotas(microchip) WHERE microchip IS NOT NULL; CREATE INDEX idx_mascotas_activo ON luthor.mascotas(activo) WHERE activo = true; CREATE INDEX idx_mascotas_busqueda ON luthor.mascotas USING gin( to_tsvector('spanish', COALESCE(nombre, '') || ' ' || COALESCE(raza, '') || ' ' || COALESCE(especie, '') || ' ' || COALESCE(microchip, '') ) ); -- 3.3. SERVICIOS CATÁLOGO (Gestión de precios y promociones) CREATE TABLE luthor.servicios_catalogo ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), codigo VARCHAR(20) UNIQUE NOT NULL, nombre VARCHAR(200) NOT NULL, descripcion TEXT, categoria VARCHAR(50) NOT NULL CHECK (categoria IN ('BAÑO', 'CORTE', 'PELUQUERIA', 'VETERINARIA', 'ESTETICA', 'HOSPEDAJE', 'ENTRENAMIENTO', 'PASEOS', 'OTRO')), subcategoria VARCHAR(50), -- Precios y costos precio_base DECIMAL(10,2) NOT NULL CHECK (precio_base >= 0), precio_especial DECIMAL(10,2) CHECK (precio_especial >= 0), costo_operativo DECIMAL(10,2) DEFAULT 0, margen_ganancia DECIMAL(5,2) DEFAULT 0, porcentaje_iva DECIMAL(5,2) DEFAULT 19, precio_final DECIMAL(10,2) GENERATED ALWAYS AS (precio_base + (precio_base * porcentaje_iva / 100)) STORED, -- Duración duracion_minutos INTEGER NOT NULL DEFAULT 30 CHECK (duracion_minutos > 0), tiempo_estimado_minutos INTEGER, -- Requisitos requiere_consulta_veterinaria BOOLEAN DEFAULT false, requiere_autorizacion BOOLEAN DEFAULT false, requiere_bozal BOOLEAN DEFAULT false, peso_minimo_requerido DECIMAL(5,2), peso_maximo_requerido DECIMAL(5,2), edad_minima_meses INTEGER, edad_maxima_meses INTEGER, -- Gestión comercial activo BOOLEAN DEFAULT true, visible_publico BOOLEAN DEFAULT true, requiere_prepago BOOLEAN DEFAULT false, anticipo_minimo DECIMAL(10,2) DEFAULT 0, comision_estilista DECIMAL(5,2) DEFAULT 0, puntos_recompensa INTEGER DEFAULT 0, aplica_promocion BOOLEAN DEFAULT false, promocion_descripcion TEXT, -- Inventario asociado productos_requeridos JSONB DEFAULT '[]'::jsonb, stock_necesario INTEGER DEFAULT 0, -- Auditoría created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMPTZ, created_by UUID REFERENCES luthor.usuarios(user_id), CONSTRAINT codigo_activo UNIQUE (codigo) WHERE deleted_at IS NULL, CONSTRAINT precio_especial_valido CHECK (precio_especial <= precio_base) ); -- Índices de servicios CREATE INDEX idx_servicios_activo ON luthor.servicios_catalogo(activo) WHERE activo = true; CREATE INDEX idx_servicios_categoria ON luthor.servicios_catalogo(categoria); CREATE INDEX idx_servicios_nombre_trgm ON luthor.servicios_catalogo USING gin (nombre gin_trgm_ops); CREATE INDEX idx_servicios_codigo ON luthor.servicios_catalogo(codigo); CREATE INDEX idx_servicios_precio ON luthor.servicios_catalogo(precio_base); -- 3.4. CITAS (Sistema de agenda avanzado) CREATE TABLE luthor.citas ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), numero_factura VARCHAR(50) UNIQUE, numero_orden VARCHAR(50) UNIQUE, -- Clientes y mascotas cliente_id UUID NOT NULL REFERENCES luthor.usuarios(user_id) ON DELETE RESTRICT, mascota_id UUID REFERENCES luthor.mascotas(id) ON DELETE RESTRICT, servicio_id UUID NOT NULL REFERENCES luthor.servicios_catalogo(id) ON DELETE RESTRICT, estilista_asignado UUID REFERENCES luthor.usuarios(user_id) ON DELETE SET NULL, -- Horario fecha_programada TIMESTAMPTZ NOT NULL, fecha_fin_estimada TIMESTAMPTZ, fecha_inicio_real TIMESTAMPTZ, fecha_fin_real TIMESTAMPTZ, duracion_efectiva_minutos INTEGER, tiempo_espera_minutos INTEGER DEFAULT 0, -- Precios y pagos precio_acordado DECIMAL(10,2) NOT NULL DEFAULT 0 CHECK (precio_acordado >= 0), descuento DECIMAL(10,2) DEFAULT 0 CHECK (descuento >= 0), abono DECIMAL(10,2) DEFAULT 0 CHECK (abono >= 0), saldo_pendiente DECIMAL(10,2) GENERATED ALWAYS AS (precio_acordado - descuento - abono) STORED, iva_aplicado DECIMAL(10,2) DEFAULT 0, total_cobrar DECIMAL(10,2) GENERATED ALWAYS AS (precio_acordado - descuento + iva_aplicado) STORED, -- Estado del flujo estado VARCHAR(30) NOT NULL DEFAULT 'PENDIENTE' CHECK (estado IN ( 'PENDIENTE', 'CONFIRMADA', 'PREPARANDO', 'EN_PROCESO', 'PAUSA', 'COMPLETADA', 'CANCELADA', 'NO_ASISTIO', 'REPROGRAMADA' )), prioridad VARCHAR(10) DEFAULT 'NORMAL' CHECK (prioridad IN ('BAJA', 'NORMAL', 'ALTA', 'URGENTE')), tipo_cita VARCHAR(20) DEFAULT 'NORMAL' CHECK (tipo_cita IN ('NORMAL', 'EMERGENCIA', 'CONTROL', 'SEGUIMIENTO')), -- Notas y observaciones notas_cliente TEXT, notas_internas TEXT, notas_servicio_prestado TEXT, instrucciones_especiales TEXT, recomendaciones_post_servicio TEXT, -- Origen y tracking origen VARCHAR(20) DEFAULT 'WEB' CHECK (origen IN ('WEB', 'APP', 'TELEFONO', 'PRESENCIAL', 'WHATSAPP', 'FACEBOOK', 'INSTAGRAM')), canal VARCHAR(20) DEFAULT 'DIRECTO' CHECK (canal IN ('DIRECTO', 'REFERIDO', 'PUBLICIDAD', 'RED_SOCIAL', 'EMAIL', 'OTRO')), calificacion INTEGER CHECK (calificacion BETWEEN 1 AND 5), comentario_cliente TEXT, retroalimentacion_interna TEXT, -- Notificaciones recordatorio_1_enviado BOOLEAN DEFAULT false, recordatorio_2_enviado BOOLEAN DEFAULT false, recordatorio_3_enviado BOOLEAN DEFAULT false, email_confirmacion_enviado BOOLEAN DEFAULT false, whatsapp_confirmacion_enviado BOOLEAN DEFAULT false, sms_confirmacion_enviado BOOLEAN DEFAULT false, -- Métricas tiempo_real_minutos INTEGER, productividad INTEGER, satisfaccion_cliente INTEGER CHECK (satisfaccion_cliente BETWEEN 1 AND 5), -- Auditoría created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMPTZ, created_by UUID REFERENCES luthor.usuarios(user_id), cancelada_por UUID REFERENCES luthor.usuarios(user_id), motivo_cancelacion TEXT, fecha_cancelacion TIMESTAMPTZ, -- Constraints de negocio CONSTRAINT fecha_fin_estimada_valida CHECK (fecha_fin_estimada >= fecha_programada), CONSTRAINT precio_valido CHECK (precio_acordado >= 0), CONSTRAINT descuento_valido CHECK (descuento <= precio_acordado), CONSTRAINT fecha_consistencia CHECK ( (fecha_inicio_real IS NULL AND fecha_fin_real IS NULL) OR (fecha_inicio_real IS NOT NULL AND fecha_fin_real IS NOT NULL AND fecha_fin_real >= fecha_inicio_real) ) ); -- Índices estratégicos para agenda CREATE INDEX idx_citas_fecha_programada ON luthor.citas(fecha_programada) WHERE estado NOT IN ('CANCELADA', 'COMPLETADA'); CREATE INDEX idx_citas_cliente ON luthor.citas(cliente_id) WHERE deleted_at IS NULL; CREATE INDEX idx_citas_mascota ON luthor.citas(mascota_id) WHERE deleted_at IS NULL; CREATE INDEX idx_citas_estado ON luthor.citas(estado); CREATE INDEX idx_citas_servicio ON luthor.citas(servicio_id); CREATE INDEX idx_citas_estilista ON luthor.citas(estilista_asignado) WHERE estilista_asignado IS NOT NULL; CREATE INDEX idx_citas_numero_factura ON luthor.citas(numero_factura) WHERE numero_factura IS NOT NULL; CREATE INDEX idx_citas_estado_fecha ON luthor.citas(estado, fecha_programada); CREATE INDEX idx_citas_cliente_estado ON luthor.citas(cliente_id, estado); CREATE INDEX idx_citas_calificacion ON luthor.citas(calificacion) WHERE calificacion IS NOT NULL; CREATE INDEX idx_citas_fecha_estado ON luthor.citas(fecha_programada DESC, estado); CREATE INDEX idx_citas_busqueda ON luthor.citas USING gin( to_tsvector('spanish', COALESCE(numero_factura, '') || ' ' || COALESCE(numero_orden, '') || ' ' || COALESCE(notas_cliente, '') || ' ' || COALESCE(notas_internas, '') ) ); -- 3.5. PAGOS (Sistema de conciliación y contabilidad) CREATE TABLE luthor.pagos ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), cita_id UUID REFERENCES luthor.citas(id) ON DELETE RESTRICT, cliente_id UUID NOT NULL REFERENCES luthor.usuarios(user_id), -- Montos detallados monto_bruto DECIMAL(10,2) NOT NULL CHECK (monto_bruto > 0), monto_iva DECIMAL(10,2) DEFAULT 0, monto_neto DECIMAL(10,2) GENERATED ALWAYS AS (monto_bruto + monto_iva) STORED, descuento_aplicado DECIMAL(10,2) DEFAULT 0, monto_total DECIMAL(10,2) GENERATED ALWAYS AS (monto_bruto + monto_iva - descuento_aplicado) STORED, comision_plataforma DECIMAL(10,2) DEFAULT 0, monto_recibido DECIMAL(10,2) CHECK (monto_recibido >= 0), cambio DECIMAL(10,2) GENERATED ALWAYS AS (monto_recibido - (monto_bruto + monto_iva - descuento_aplicado)) STORED, -- Método de pago metodo_pago VARCHAR(30) NOT NULL CHECK (metodo_pago IN ( 'EFECTIVO', 'TARJETA_DEBITO', 'TARJETA_CREDITO', 'TRANSFERENCIA', 'PSE', 'NEQUI', 'DAVIPLATA', 'CRYPTO', 'OTRO' )), banco VARCHAR(100), numero_autorizacion VARCHAR(50), referencia_externa VARCHAR(100), codigo_aprobacion VARCHAR(50), -- Estado del pago estado VARCHAR(20) DEFAULT 'PENDIENTE' CHECK (estado IN ('PENDIENTE', 'APROBADO', 'RECHAZADO', 'REEMBOLSADO', 'ANULADO', 'EN_REVISION')), fecha_pago TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, fecha_reembolso TIMESTAMPTZ, motivo_reembolso TEXT, -- Medios electrónicos token_transaccion VARCHAR(200), url_comprobante TEXT, datos_adicionales JSONB, -- Conciliación conciliado BOOLEAN DEFAULT false, fecha_conciliacion TIMESTAMPTZ, conciliado_por UUID REFERENCES luthor.usuarios(user_id), nota_conciliacion TEXT, -- Auditoría created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES luthor.usuarios(user_id), aprobado_por UUID REFERENCES luthor.usuarios(user_id), -- Integridad CONSTRAINT pago_con_cita_o_cliente CHECK (cita_id IS NOT NULL OR cliente_id IS NOT NULL) ); -- Índices de pagos CREATE INDEX idx_pagos_cita ON luthor.pagos(cita_id) WHERE cita_id IS NOT NULL; CREATE INDEX idx_pagos_cliente ON luthor.pagos(cliente_id); CREATE INDEX idx_pagos_estado ON luthor.pagos(estado); CREATE INDEX idx_pagos_fecha ON luthor.pagos(fecha_pago); CREATE INDEX idx_pagos_referencia ON luthor.pagos(referencia_externa); CREATE INDEX idx_pagos_metodo ON luthor.pagos(metodo_pago); CREATE INDEX idx_pagos_conciliado ON luthor.pagos(conciliado) WHERE conciliado = false; CREATE INDEX idx_pagos_fecha_estado ON luthor.pagos(fecha_pago DESC, estado); -- ============================================================ -- 4. TABLAS DE SOPORTE EMPRESARIAL -- ============================================================ -- 4.1. INVENTARIO CON TRAZABILIDAD COMPLETA CREATE TABLE luthor.productos_inventario ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), codigo_barras VARCHAR(50) UNIQUE, codigo_interno VARCHAR(50) UNIQUE NOT NULL, nombre VARCHAR(200) NOT NULL, descripcion TEXT, categoria VARCHAR(50) NOT NULL CHECK (categoria IN ('INSUMOS', 'MEDICAMENTOS', 'HIGIENE', 'ALIMENTOS', 'ACCESORIOS', 'EQUIPO', 'OTRO')), subcategoria VARCHAR(50), -- Unidades y control unidad_medida VARCHAR(20) DEFAULT 'UNIDAD' CHECK (unidad_medida IN ('UNIDAD', 'GRAMO', 'KILO', 'LITRO', 'ML', 'PAQUETE', 'CAJA', 'BOTELLA', 'TUBO')), peso_unitario DECIMAL(8,3), volumen_unitario DECIMAL(8,3), stock_actual INTEGER NOT NULL DEFAULT 0 CHECK (stock_actual >= 0), stock_minimo INTEGER DEFAULT 5 CHECK (stock_minimo >= 0), stock_maximo INTEGER DEFAULT 100, punto_reorden INTEGER DEFAULT 10, stock_reservado INTEGER DEFAULT 0, stock_disponible INTEGER GENERATED ALWAYS AS (stock_actual - stock_reservado) STORED, -- Precios y costos precio_compra DECIMAL(10,2) DEFAULT 0 CHECK (precio_compra >= 0), precio_venta DECIMAL(10,2) DEFAULT 0 CHECK (precio_venta >= 0), precio_venta_especial DECIMAL(10,2) CHECK (precio_venta_especial >= 0), porcentaje_ganancia DECIMAL(5,2) GENERATED ALWAYS AS ( CASE WHEN precio_compra > 0 THEN ((precio_venta - precio_compra) / precio_compra) * 100 ELSE 0 END ) STORED, iva_porcentaje DECIMAL(5,2) DEFAULT 19, costo_promedio DECIMAL(10,2), -- Datos de proveedor proveedor_principal VARCHAR(200), contacto_proveedor VARCHAR(200), telefono_proveedor VARCHAR(20), email_proveedor VARCHAR(255), -- Ubicación ubicacion_almacen VARCHAR(100), pasillo VARCHAR(20), estante VARCHAR(20), posicion VARCHAR(20), -- Estado y control activo BOOLEAN DEFAULT true, perecedero BOOLEAN DEFAULT false, requiere_refrigeracion BOOLEAN DEFAULT false, requiere_certificado BOOLEAN DEFAULT false, numero_registro_sanitario VARCHAR(50), fecha_vencimiento DATE, lote VARCHAR(50), numero_serie VARCHAR(50), -- Clasificación clasificacion_abc VARCHAR(1) CHECK (clasificacion_abc IN ('A', 'B', 'C')), rotacion VARCHAR(20) DEFAULT 'MEDIA' CHECK (rotacion IN ('BAJA', 'MEDIA', 'ALTA', 'MUY_ALTA')), -- Auditoría created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMPTZ, created_by UUID REFERENCES luthor.usuarios(user_id), updated_by UUID REFERENCES luthor.usuarios(user_id), CONSTRAINT precio_venta_valido CHECK (precio_venta >= precio_compra) ); -- Índices de productos CREATE INDEX idx_productos_codigo ON luthor.productos_inventario(codigo_interno); CREATE INDEX idx_productos_nombre_trgm ON luthor.productos_inventario USING gin (nombre gin_trgm_ops); CREATE INDEX idx_productos_categoria ON luthor.productos_inventario(categoria); CREATE INDEX idx_productos_vencimiento ON luthor.productos_inventario(fecha_vencimiento) WHERE fecha_vencimiento IS NOT NULL; CREATE INDEX idx_productos_stock ON luthor.productos_inventario(stock_actual) WHERE stock_actual <= stock_minimo; CREATE INDEX idx_productos_proveedor ON luthor.productos_inventario(proveedor_principal); CREATE INDEX idx_productos_busqueda ON luthor.productos_inventario USING gin( to_tsvector('spanish', COALESCE(nombre, '') || ' ' || COALESCE(descripcion, '') || ' ' || COALESCE(codigo_interno, '') || ' ' || COALESCE(codigo_barras, '') ) ); -- 4.2. MOVIMIENTOS DE INVENTARIO (Doble entrada contable) CREATE TABLE luthor.movimientos_inventario ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), producto_id UUID NOT NULL REFERENCES luthor.productos_inventario(id) ON DELETE RESTRICT, producto_codigo VARCHAR(50), producto_nombre VARCHAR(200), -- Cantidades cantidad_entrada INTEGER DEFAULT 0 CHECK (cantidad_entrada >= 0), cantidad_salida INTEGER DEFAULT 0 CHECK (cantidad_salida >= 0), stock_anterior INTEGER NOT NULL, stock_final INTEGER NOT NULL CHECK (stock_final >= 0), -- Tipo y motivo tipo_movimiento VARCHAR(20) NOT NULL CHECK (tipo_movimiento IN ('ENTRADA', 'SALIDA', 'AJUSTE', 'TRANSFERENCIA', 'INVENTARIO')), motivo VARCHAR(30) NOT NULL CHECK (motivo IN ( 'COMPRA', 'VENTA', 'DEVOLUCION', 'MERMA', 'AJUSTE', 'TRASLADO', 'MUESTRA', 'CADUCIDAD', 'ROBO', 'INVENTARIO_INICIAL', 'TRANSFERENCIA_ALMACEN' )), -- Referencias documento_referencia VARCHAR(100), proveedor_id UUID REFERENCES luthor.usuarios(user_id), cliente_id UUID REFERENCES luthor.usuarios(user_id), factura_compra VARCHAR(50), factura_venta VARCHAR(50), orden_compra VARCHAR(50), orden_venta VARCHAR(50), -- Costos y valores costo_unitario DECIMAL(10,2) CHECK (costo_unitario >= 0), costo_total_entrada DECIMAL(10,2) GENERATED ALWAYS AS (cantidad_entrada * costo_unitario) STORED, costo_total_salida DECIMAL(10,2) GENERATED ALWAYS AS (cantidad_salida * costo_unitario) STORED, -- Detalle notas TEXT, ubicacion_origen TEXT, ubicacion_destino TEXT, peso_total DECIMAL(10,2), volumen_total DECIMAL(10,2), -- Aprobación requiere_aprobacion BOOLEAN DEFAULT false, aprobado BOOLEAN DEFAULT false, fecha_aprobacion TIMESTAMPTZ, aprobado_por UUID REFERENCES luthor.usuarios(user_id), -- Auditoría fecha_movimiento TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES luthor.usuarios(user_id), autorizado_por UUID REFERENCES luthor.usuarios(user_id), CONSTRAINT movimiento_valido CHECK ( (tipo_movimiento IN ('ENTRADA', 'AJUSTE', 'INVENTARIO') AND cantidad_entrada > 0 AND cantidad_salida = 0) OR (tipo_movimiento = 'SALIDA' AND cantidad_salida > 0 AND cantidad_entrada = 0) OR (tipo_movimiento = 'TRANSFERENCIA' AND cantidad_entrada > 0 AND cantidad_salida > 0) ) ); -- Índices de movimientos CREATE INDEX idx_movimientos_producto ON luthor.movimientos_inventario(producto_id); CREATE INDEX idx_movimientos_fecha ON luthor.movimientos_inventario(fecha_movimiento); CREATE INDEX idx_movimientos_tipo ON luthor.movimientos_inventario(tipo_movimiento); CREATE INDEX idx_movimientos_documento ON luthor.movimientos_inventario(documento_referencia); CREATE INDEX idx_movimientos_producto_fecha ON luthor.movimientos_inventario(producto_id, fecha_movimiento DESC); CREATE INDEX idx_movimientos_motivo ON luthor.movimientos_inventario(motivo); -- 4.3. GASTOS OPERATIVOS CON PRESUPUESTOS CREATE TABLE luthor.gastos_operativos ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), centro_costo VARCHAR(50) NOT NULL, categoria VARCHAR(50) NOT NULL CHECK (categoria IN ( 'SALARIOS', 'SERVICIOS', 'MANTENIMIENTO', 'INSUMOS', 'IMPUESTOS', 'PUBLICIDAD', 'TRANSPORTE', 'ALQUILER', 'SEGUROS', 'HONORARIOS', 'CAPACITACION', 'TECNOLOGIA', 'OTROS' )), subcategoria VARCHAR(50), -- Montos monto_bruto DECIMAL(10,2) NOT NULL CHECK (monto_bruto > 0), monto_iva DECIMAL(10,2) DEFAULT 0, monto_total DECIMAL(10,2) GENERATED ALWAYS AS (monto_bruto + monto_iva) STORED, moneda VARCHAR(3) DEFAULT 'COP', tasa_cambio DECIMAL(10,4) DEFAULT 1, monto_original DECIMAL(10,2), moneda_original VARCHAR(3), -- Detalle concepto TEXT NOT NULL, descripcion TEXT, proveedor VARCHAR(200), nit_proveedor VARCHAR(20), factura_numero VARCHAR(50), factura_url TEXT, factura_pdf BYTEA, -- Fechas fecha_gasto TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, fecha_contable DATE, periodo_contable DATE, fecha_pago DATE, -- Aprobaciones estado VARCHAR(20) DEFAULT 'PENDIENTE' CHECK (estado IN ('PENDIENTE', 'APROBADO', 'RECHAZADO', 'PAGADO', 'ANULADO', 'EN_REVISION')), requiere_aprobacion BOOLEAN DEFAULT false, aprobado_por UUID REFERENCES luthor.usuarios(user_id), fecha_aprobacion TIMESTAMPTZ, rechazado_por UUID REFERENCES luthor.usuarios(user_id), motivo_rechazo TEXT, -- Presupuesto presupuesto_asignado DECIMAL(10,2), desviacion DECIMAL(10,2) GENERATED ALWAYS AS ( presupuesto_asignado - (monto_bruto + monto_iva) ) STORED, porcentaje_ejecucion DECIMAL(5,2) GENERATED ALWAYS AS ( CASE WHEN presupuesto_asignado > 0 THEN ((monto_bruto + monto_iva) / presupuesto_asignado) * 100 ELSE 0 END ) STORED, -- Depreciación es_activo_fijo BOOLEAN DEFAULT false, vida_util_meses INTEGER, depreciacion_mensual DECIMAL(10,2), -- Auditoría created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES luthor.usuarios(user_id), updated_by UUID REFERENCES luthor.usuarios(user_id) ); -- Índices de gastos CREATE INDEX idx_gastos_fecha ON luthor.gastos_operativos(fecha_gasto); CREATE INDEX idx_gastos_categoria ON luthor.gastos_operativos(categoria); CREATE INDEX idx_gastos_estado ON luthor.gastos_operativos(estado); CREATE INDEX idx_gastos_centro_costo ON luthor.gastos_operativos(centro_costo); CREATE INDEX idx_gastos_proveedor ON luthor.gastos_operativos(proveedor); CREATE INDEX idx_gastos_factura ON luthor.gastos_operativos(factura_numero); CREATE INDEX idx_gastos_periodo ON luthor.gastos_operativos(periodo_contable); -- 4.4. COMUNICACIONES Y GESTIÓN DE CLIENTES CREATE TABLE luthor.comunicaciones ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), cliente_id UUID REFERENCES luthor.usuarios(user_id) ON DELETE SET NULL, -- Datos contacto nombre_contacto VARCHAR(200) NOT NULL, email_contacto VARCHAR(255) NOT NULL, telefono_contacto VARCHAR(20), -- Contenido tipo VARCHAR(30) NOT NULL CHECK (tipo IN ('SUGERENCIA', 'QUEJA', 'RECLAMO', 'FELICITACION', 'PETICION', 'CONSULTA', 'COTIZACION')), prioridad VARCHAR(10) DEFAULT 'MEDIA' CHECK (prioridad IN ('BAJA', 'MEDIA', 'ALTA', 'URGENTE', 'CRITICA')), asunto VARCHAR(200) NOT NULL, mensaje TEXT NOT NULL, archivos_adjuntos JSONB DEFAULT '[]'::jsonb, -- Gestión estado VARCHAR(20) DEFAULT 'PENDIENTE' CHECK (estado IN ('PENDIENTE', 'EN_REVISION', 'EN_PROCESO', 'RESUELTA', 'CERRADA', 'ARCHIVADA', 'DERIVADA')), asignado_a UUID REFERENCES luthor.usuarios(user_id), respuesta TEXT, fecha_respuesta TIMESTAMPTZ, respuesta_por UUID REFERENCES luthor.usuarios(user_id), calificacion_satisfaccion INTEGER CHECK (calificacion_satisfaccion BETWEEN 1 AND 5), comentario_satisfaccion TEXT, -- Historial numero_radicado VARCHAR(50) UNIQUE, fecha_radicado TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, derivado_de UUID REFERENCES luthor.comunicaciones(id), -- Auditoría created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES luthor.usuarios(user_id), -- Metadatos ip_origen INET, user_agent TEXT, canal VARCHAR(30) DEFAULT 'FORMULARIO' CHECK (canal IN ('FORMULARIO', 'EMAIL', 'TELEFONO', 'PRESENCIAL', 'RED_SOCIAL', 'WHATSAPP')) ); -- Índices de comunicaciones CREATE INDEX idx_comunicaciones_cliente ON luthor.comunicaciones(cliente_id); CREATE INDEX idx_comunicaciones_estado ON luthor.comunicaciones(estado); CREATE INDEX idx_comunicaciones_fecha ON luthor.comunicaciones(created_at); CREATE INDEX idx_comunicaciones_asunto_trgm ON luthor.comunicaciones USING gin (asunto gin_trgm_ops); CREATE INDEX idx_comunicaciones_radicado ON luthor.comunicaciones(numero_radicado) WHERE numero_radicado IS NOT NULL; CREATE INDEX idx_comunicaciones_tipo ON luthor.comunicaciones(tipo); -- ============================================================ -- 5. TABLAS DE AUDITORÍA Y TRAZABILIDAD -- ============================================================ -- 5.1. AUDITORÍA DETALLADA CON COMPRESIÓN CREATE TABLE luthor.auditoria ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tabla_afectada VARCHAR(100) NOT NULL, registro_id UUID NOT NULL, operacion VARCHAR(10) NOT NULL CHECK (operacion IN ('INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'MERGE')), -- Datos antes y después (comprimidos) datos_anteriores JSONB, datos_nuevos JSONB, campos_afectados TEXT[], cambio_completo BOOLEAN DEFAULT false, diff JSONB, -- Contexto usuario_id UUID REFERENCES luthor.usuarios(user_id), usuario_ip INET, user_agent TEXT, sesion_id UUID, aplicacion VARCHAR(50) DEFAULT 'LUTHOR_API', version_aplicacion VARCHAR(20), -- Fechas fecha_operacion TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, fecha_commit TIMESTAMPTZ, transaccion_id UUID DEFAULT uuid_generate_v4(), tiempo_ejecucion_ms INTEGER, -- Metadatos motivo TEXT, prioridad_auditoria VARCHAR(20) DEFAULT 'NORMAL' CHECK (prioridad_auditoria IN ('BAJA', 'NORMAL', 'ALTA', 'CRITICA')), retencion_dias INTEGER DEFAULT 365, CONSTRAINT datos_validos CHECK ( (operacion IN ('UPDATE', 'DELETE') AND datos_anteriores IS NOT NULL) OR (operacion IN ('INSERT', 'UPDATE') AND datos_nuevos IS NOT NULL) ) ); -- Partición por fecha (para rendimiento) CREATE INDEX idx_auditoria_registro ON luthor.auditoria(tabla_afectada, registro_id); CREATE INDEX idx_auditoria_fecha ON luthor.auditoria(fecha_operacion); CREATE INDEX idx_auditoria_usuario ON luthor.auditoria(usuario_id); CREATE INDEX idx_auditoria_transaccion ON luthor.auditoria(transaccion_id); CREATE INDEX idx_auditoria_tabla_fecha ON luthor.auditoria(tabla_afectada, fecha_operacion DESC); CREATE INDEX idx_auditoria_operacion ON luthor.auditoria(operacion); -- 5.2. BITÁCORA DE SISTEMA CON MÉTRICAS CREATE TABLE luthor.bitacora_sistema ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), usuario_id UUID REFERENCES luthor.usuarios(user_id), accion VARCHAR(100) NOT NULL, modulo VARCHAR(50) NOT NULL, submodulo VARCHAR(50), descripcion TEXT, -- Datos de contexto ip_origen INET, user_agent TEXT, url_consulta TEXT, metodo_http VARCHAR(10), parametros_consulta JSONB, headers JSONB, -- Rendimiento tiempo_ejecucion_ms INTEGER, query_sql TEXT, plan_ejecucion TEXT, uso_cpu DECIMAL(5,2), uso_memoria INTEGER, uso_disco INTEGER, -- Resultado exitoso BOOLEAN DEFAULT true, mensaje_error TEXT, codigo_error VARCHAR(50), stack_trace TEXT, -- Métricas registros_afectados INTEGER DEFAULT 0, dato_json JSONB, fecha_accion TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES luthor.usuarios(user_id) ); -- Índices de bitácora CREATE INDEX idx_bitacora_usuario ON luthor.bitacora_sistema(usuario_id); CREATE INDEX idx_bitacora_fecha ON luthor.bitacora_sistema(fecha_accion); CREATE INDEX idx_bitacora_modulo ON luthor.bitacora_sistema(modulo); CREATE INDEX idx_bitacora_accion ON luthor.bitacora_sistema(accion); CREATE INDEX idx_bitacora_exitoso ON luthor.bitacora_sistema(exitoso) WHERE exitoso = false; -- 5.3. NOTIFICACIONES MULTICANAL CREATE TABLE luthor.notificaciones ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), usuario_id UUID NOT NULL REFERENCES luthor.usuarios(user_id) ON DELETE CASCADE, tipo VARCHAR(30) NOT NULL CHECK (tipo IN ('EMAIL', 'SMS', 'WHATSAPP', 'PUSH', 'IN_APP', 'VOICE', 'FAX')), categoria VARCHAR(30) NOT NULL CHECK (categoria IN ('CITA', 'PAGO', 'RECORDATORIO', 'PROMOCION', 'ALERTA', 'SISTEMA', 'SEGURIDAD', 'MARKETING')), -- Contenido titulo VARCHAR(200) NOT NULL, mensaje TEXT NOT NULL, mensaje_html TEXT, datos_adicionales JSONB, prioridad VARCHAR(10) DEFAULT 'NORMAL' CHECK (prioridad IN ('BAJA', 'NORMAL', 'ALTA', 'URGENTE', 'CRITICA')), -- Programación fecha_programacion TIMESTAMPTZ, fecha_envio TIMESTAMPTZ, fecha_lectura TIMESTAMPTZ, fecha_interaccion TIMESTAMPTZ, -- Estado y control estado VARCHAR(20) DEFAULT 'PENDIENTE' CHECK (estado IN ('PENDIENTE', 'ENVIADO', 'ENTREGADO', 'LEIDO', 'FALLIDO', 'CANCELADO', 'REPROGRAMADO', 'BOUNCE')), intentos INTEGER DEFAULT 0, max_intentos INTEGER DEFAULT 3, ultimo_error TEXT, codigo_confirmacion VARCHAR(100), tracking_id VARCHAR(100), prioridad_envio INTEGER DEFAULT 5, -- Métricas abierto BOOLEAN DEFAULT false, clickeado BOOLEAN DEFAULT false, respondido BOOLEAN DEFAULT false, tiempo_lectura_ms INTEGER, -- Auditoría created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES luthor.usuarios(user_id), -- Canal específico canal_destino VARCHAR(100), id_externo VARCHAR(100) ); -- Índices de notificaciones CREATE INDEX idx_notificaciones_usuario ON luthor.notificaciones(usuario_id); CREATE INDEX idx_notificaciones_envio ON luthor.notificaciones(fecha_envio) WHERE estado = 'PENDIENTE'; CREATE INDEX idx_notificaciones_tipo ON luthor.notificaciones(tipo); CREATE INDEX idx_notificaciones_estado ON luthor.notificaciones(estado); CREATE INDEX idx_notificaciones_categoria ON luthor.notificaciones(categoria); CREATE INDEX idx_notificaciones_prioridad ON luthor.notificaciones(prioridad_envio); -- ============================================================ -- 6. VISTAS EMPRESARIALES PARA REPORTES -- ============================================================ -- 6.1. DASHBOARD EJECUTIVO COMPLETO CREATE OR REPLACE VIEW luthor.vw_dashboard_ejecutivo AS WITH metricas_dia AS ( SELECT COUNT(*) FILTER (WHERE fecha_programada::DATE = CURRENT_DATE) AS citas_hoy, COUNT(*) FILTER (WHERE estado = 'PENDIENTE' AND fecha_programada::DATE = CURRENT_DATE) AS citas_pendientes_hoy, COUNT(*) FILTER (WHERE estado = 'CONFIRMADA' AND fecha_programada::DATE = CURRENT_DATE) AS citas_confirmadas_hoy, COUNT(*) FILTER (WHERE estado = 'EN_PROCESO') AS citas_en_proceso, COUNT(*) FILTER (WHERE estado = 'COMPLETADA' AND fecha_programada::DATE = CURRENT_DATE) AS citas_completadas_hoy FROM luthor.citas ), metricas_mes AS ( SELECT COALESCE(SUM(monto_total), 0) AS ingresos_mes, COALESCE(AVG(monto_total), 0) AS ingreso_promedio FROM luthor.pagos WHERE estado = 'APROBADO' AND fecha_pago > CURRENT_DATE - INTERVAL '30 days' ) SELECT (SELECT COUNT(*) FROM luthor.usuarios WHERE estado = 'ACTIVO' AND rol = 'CLIENTE') AS total_clientes_activos, (SELECT COUNT(*) FROM luthor.mascotas WHERE activo = true) AS total_mascotas, (SELECT COUNT(*) FROM luthor.usuarios WHERE estado = 'ACTIVO' AND rol IN ('ESTILISTA', 'VETERINARIO')) AS total_empleados_activos, md.citas_hoy, md.citas_pendientes_hoy, md.citas_confirmadas_hoy, md.citas_en_proceso, md.citas_completadas_hoy, mm.ingresos_mes, mm.ingreso_promedio, (SELECT COALESCE(SUM(monto_total), 0) FROM luthor.gastos_operativos WHERE estado = 'PAGADO' AND fecha_gasto > CURRENT_DATE - INTERVAL '30 days') AS gastos_mes, (SELECT COALESCE(AVG(calificacion), 0) FROM luthor.citas WHERE calificacion IS NOT NULL AND fecha_programada > CURRENT_DATE - INTERVAL '30 days') AS satisfaccion_promedio, (SELECT COUNT(*) FROM luthor.citas WHERE fecha_programada::DATE = CURRENT_DATE AND estado IN ('PENDIENTE', 'CONFIRMADA')) AS citas_dia_actual, (SELECT COALESCE(SUM(precio_acordado), 0) FROM luthor.citas WHERE fecha_programada::DATE = CURRENT_DATE AND estado = 'COMPLETADA') AS ingresos_dia, (SELECT COUNT(*) FROM luthor.notificaciones WHERE estado = 'PENDIENTE') AS notificaciones_pendientes, (SELECT COUNT(*) FROM luthor.comunicaciones WHERE estado IN ('PENDIENTE', 'EN_REVISION')) AS comunicaciones_pendientes, (SELECT COUNT(*) FROM luthor.productos_inventario WHERE stock_actual <= stock_minimo) AS productos_stock_bajo, (SELECT COUNT(*) FROM luthor.movimientos_inventario WHERE fecha_movimiento::DATE = CURRENT_DATE AND tipo_movimiento = 'SALIDA') AS movimientos_inventario_hoy FROM metricas_dia md, metricas_mes mm; -- 6.2. RENDIMIENTO DE EMPLEADOS CREATE OR REPLACE VIEW luthor.vw_rendimiento_empleados AS SELECT u.user_id, u.nombre_completo, u.rol, u.email, COUNT(DISTINCT c.id) AS total_citas_asignadas, SUM(CASE WHEN c.estado = 'COMPLETADA' THEN 1 ELSE 0 END) AS citas_completadas, ROUND((SUM(CASE WHEN c.estado = 'COMPLETADA' THEN 1 ELSE 0 END)::DECIMAL / NULLIF(COUNT(c.id), 0)) * 100, 2) AS porcentaje_completado, AVG(c.precio_acordado) AS ticket_promedio, SUM(c.precio_acordado) AS ingreso_generado, AVG(EXTRACT(EPOCH FROM (c.fecha_fin_real - c.fecha_inicio_real)) / 60) AS tiempo_promedio_minutos, AVG(c.calificacion) AS calificacion_promedio, COUNT(DISTINCT c.cliente_id) AS clientes_atendidos, SUM(CASE WHEN c.estado = 'CANCELADA' THEN 1 ELSE 0 END) AS citas_canceladas, SUM(CASE WHEN c.estado = 'NO_ASISTIO' THEN 1 ELSE 0 END) AS citas_no_asistidas, AVG(c.satisfaccion_cliente) AS satisfaccion_promedio, MAX(c.fecha_programada) AS ultima_cita FROM luthor.usuarios u LEFT JOIN luthor.citas c ON c.estilista_asignado = u.user_id WHERE u.rol IN ('ESTILISTA', 'VETERINARIO') AND (c.fecha_programada > CURRENT_DATE - INTERVAL '90 days' OR c.id IS NULL) GROUP BY u.user_id, u.nombre_completo, u.rol, u.email HAVING COUNT(c.id) > 0 OR u.estado = 'ACTIVO'; -- 6.3. ANÁLISIS DE CLIENTES CREATE OR REPLACE VIEW luthor.vw_analisis_clientes AS SELECT u.user_id, u.nombre_completo, u.email, u.telefono_principal, u.ciudad, COUNT(DISTINCT c.id) AS total_citas, SUM(CASE WHEN c.estado = 'COMPLETADA' THEN 1 ELSE 0 END) AS citas_completadas, SUM(c.precio_acordado) AS gasto_total, AVG(c.precio_acordado) AS gasto_promedio, MAX(c.fecha_programada) AS ultima_visita, MIN(c.fecha_programada) AS primera_visita, COUNT(DISTINCT m.id) AS total_mascotas, COUNT(DISTINCT m.especie) AS especies_atendidas, (SELECT COUNT(*) FROM luthor.citas c2 WHERE c2.cliente_id = u.user_id AND c2.fecha_programada > CURRENT_DATE - INTERVAL '30 days') AS citas_ultimo_mes, (SELECT COUNT(*) FROM luthor.citas c3 WHERE c3.cliente_id = u.user_id AND c3.fecha_programada BETWEEN CURRENT_DATE - INTERVAL '90 days' AND CURRENT_DATE - INTERVAL '31 days') AS citas_90_dias, (SELECT COALESCE(AVG(c4.calificacion), 0) FROM luthor.citas c4 WHERE c4.cliente_id = u.user_id AND c4.calificacion IS NOT NULL) AS calificacion_promedio, CASE WHEN COUNT(DISTINCT c.id) >= 20 THEN 'VIP_PLATINUM' WHEN COUNT(DISTINCT c.id) >= 10 THEN 'VIP_GOLD' WHEN COUNT(DISTINCT c.id) >= 5 THEN 'FRECUENTE' WHEN COUNT(DISTINCT c.id) >= 1 THEN 'OCASIONAL' ELSE 'NUEVO' END AS segmento, CASE WHEN MAX(c.fecha_programada) > CURRENT_DATE - INTERVAL '7 days' THEN 'ACTIVO_ULTIMA_SEMANA' WHEN MAX(c.fecha_programada) > CURRENT_DATE - INTERVAL '30 days' THEN 'ACTIVO_ULTIMO_MES' WHEN MAX(c.fecha_programada) > CURRENT_DATE - INTERVAL '90 days' THEN 'INACTIVO_3_MESES' ELSE 'INACTIVO_LARGO_PLAZO' END AS nivel_actividad, (SELECT SUM(monto_total) FROM luthor.pagos p WHERE p.cliente_id = u.user_id AND p.estado = 'APROBADO') AS total_pagos, (SELECT AVG(c5.calificacion) FROM luthor.citas c5 WHERE c5.cliente_id = u.user_id AND c5.calificacion IS NOT NULL AND c5.fecha_programada > CURRENT_DATE - INTERVAL '180 days') AS satisfaccion_reciente FROM luthor.usuarios u LEFT JOIN luthor.citas c ON c.cliente_id = u.user_id AND c.deleted_at IS NULL LEFT JOIN luthor.mascotas m ON m.propietario_id = u.user_id AND m.deleted_at IS NULL WHERE u.rol = 'CLIENTE' AND u.estado = 'ACTIVO' GROUP BY u.user_id, u.nombre_completo, u.email, u.telefono_principal, u.ciudad; -- 6.4. ANÁLISIS DE INVENTARIO CREATE OR REPLACE VIEW luthor.vw_analisis_inventario AS SELECT p.id, p.codigo_interno, p.nombre, p.categoria, p.stock_actual, p.stock_minimo, p.stock_maximo, p.punto_reorden, p.stock_disponible, p.precio_compra, p.precio_venta, p.porcentaje_ganancia, p.clasificacion_abc, p.rotacion, (SELECT COUNT(*) FROM luthor.movimientos_inventario mi WHERE mi.producto_id = p.id AND mi.fecha_movimiento > CURRENT_DATE - INTERVAL '30 days') AS movimientos_30_dias, (SELECT COALESCE(SUM(cantidad_salida), 0) FROM luthor.movimientos_inventario mi WHERE mi.producto_id = p.id AND mi.fecha_movimiento > CURRENT_DATE - INTERVAL '30 days' AND mi.tipo_movimiento = 'SALIDA') AS salidas_30_dias, (SELECT COALESCE(SUM(cantidad_entrada), 0) FROM luthor.movimientos_inventario mi WHERE mi.producto_id = p.id AND mi.fecha_movimiento > CURRENT_DATE - INTERVAL '30 days' AND mi.tipo_movimiento = 'ENTRADA') AS entradas_30_dias, CASE WHEN p.stock_actual <= p.stock_minimo THEN 'STOCK_CRITICO' WHEN p.stock_actual <= p.punto_reorden THEN 'STOCK_BAJO' WHEN p.stock_actual >= p.stock_maximo THEN 'STOCK_EXCESIVO' ELSE 'STOCK_NORMAL' END AS estado_stock, CASE WHEN p.fecha_vencimiento IS NOT NULL AND p.fecha_vencimiento <= CURRENT_DATE + INTERVAL '30 days' THEN 'PROXIMO_A_VENCER' WHEN p.fecha_vencimiento IS NOT NULL AND p.fecha_vencimiento <= CURRENT_DATE THEN 'VENCIDO' ELSE 'OK' END AS estado_vencimiento, p.fecha_vencimiento, p.proveedor_principal, p.ubicacion_almacen, (SELECT COALESCE(AVG(mi.costo_unitario), 0) FROM luthor.movimientos_inventario mi WHERE mi.producto_id = p.id AND mi.tipo_movimiento = 'ENTRADA') AS costo_promedio_entrada FROM luthor.productos_inventario p WHERE p.activo = true AND p.deleted_at IS NULL; -- 6.5. ANÁLISIS FINANCIERO CREATE OR REPLACE VIEW luthor.vw_analisis_financiero AS WITH ingresos AS ( SELECT DATE_TRUNC('month', fecha_pago) AS mes, SUM(monto_total) AS ingresos_totales, COUNT(*) AS cantidad_pagos, AVG(monto_total) AS ticket_promedio, COUNT(DISTINCT cliente_id) AS clientes_unicos, SUM(monto_iva) AS iva_recaudado FROM luthor.pagos WHERE estado = 'APROBADO' AND fecha_pago > CURRENT_DATE - INTERVAL '12 months' GROUP BY DATE_TRUNC('month', fecha_pago) ), gastos AS ( SELECT DATE_TRUNC('month', fecha_gasto) AS mes, SUM(monto_total) AS gastos_totales, COUNT(*) AS cantidad_gastos, SUM(monto_iva) AS iva_pagado FROM luthor.gastos_operativos WHERE estado = 'PAGADO' AND fecha_gasto > CURRENT_DATE - INTERVAL '12 months' GROUP BY DATE_TRUNC('month', fecha_gasto) ) SELECT COALESCE(i.mes, g.mes) AS mes, COALESCE(i.ingresos_totales, 0) AS ingresos_totales, COALESCE(g.gastos_totales, 0) AS gastos_totales, COALESCE(i.ingresos_totales, 0) - COALESCE(g.gastos_totales, 0) AS utilidad, CASE WHEN COALESCE(g.gastos_totales, 0) > 0 THEN ROUND(((COALESCE(i.ingresos_totales, 0) - COALESCE(g.gastos_totales, 0)) / COALESCE(g.gastos_totales, 0)) * 100, 2) ELSE 0 END AS margen_utilidad, COALESCE(i.cantidad_pagos, 0) AS cantidad_pagos, COALESCE(i.ticket_promedio, 0) AS ticket_promedio, COALESCE(i.clientes_unicos, 0) AS clientes_unicos, COALESCE(i.iva_recaudado, 0) AS iva_recaudado, COALESCE(g.iva_pagado, 0) AS iva_pagado, COALESCE(i.iva_recaudado, 0) - COALESCE(g.iva_pagado, 0) AS iva_neto_a_pagar, COALESCE(i.ingresos_totales, 0) / NULLIF(COALESCE(i.clientes_unicos, 1), 0) AS ingreso_por_cliente FROM ingresos i FULL OUTER JOIN gastos g ON i.mes = g.mes ORDER BY mes DESC; -- ============================================================ -- 7. FUNCIONES Y PROCEDIMIENTOS EMPRESARIALES -- ============================================================ -- 7.1. FUNCIÓN PARA ACTUALIZAR TIMESTAMPS CREATE OR REPLACE FUNCTION luthor.fn_actualizar_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; -- 7.2. FUNCIÓN DE AUDITORÍA AUTOMÁTICA CREATE OR REPLACE FUNCTION luthor.fn_auditar_cambio() RETURNS TRIGGER AS $$ DECLARE v_datos_anteriores JSONB; v_datos_nuevos JSONB; v_operacion VARCHAR(10); v_usuario_id UUID; v_ip INET; v_user_agent TEXT; BEGIN -- Obtener datos de contexto BEGIN v_usuario_id := current_setting('luthor.usuario_id', true)::UUID; EXCEPTION WHEN OTHERS THEN v_usuario_id := NULL; END; BEGIN v_ip := current_setting('luthor.ip_origen', true)::INET; EXCEPTION WHEN OTHERS THEN v_ip := NULL; END; BEGIN v_user_agent := current_setting('luthor.user_agent', true); EXCEPTION WHEN OTHERS THEN v_user_agent := NULL; END; -- Preparar datos según operación IF TG_OP = 'INSERT' THEN v_datos_nuevos = row_to_json(NEW); v_operacion = 'INSERT'; ELSIF TG_OP = 'UPDATE' THEN v_datos_anteriores = row_to_json(OLD); v_datos_nuevos = row_to_json(NEW); v_operacion = 'UPDATE'; ELSIF TG_OP = 'DELETE' THEN v_datos_anteriores = row_to_json(OLD); v_operacion = 'DELETE'; END IF; -- Insertar auditoría INSERT INTO luthor.auditoria ( tabla_afectada, registro_id, operacion, datos_anteriores, datos_nuevos, usuario_id, usuario_ip, user_agent, transaccion_id ) VALUES ( TG_TABLE_NAME, COALESCE(NEW.id, OLD.id), v_operacion, v_datos_anteriores, v_datos_nuevos, v_usuario_id, v_ip, v_user_agent, uuid_generate_v4() ); RETURN COALESCE(NEW, OLD); END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- 7.3. FUNCIÓN DE ACTUALIZACIÓN DE STOCK CREATE OR REPLACE FUNCTION luthor.fn_actualizar_stock() RETURNS TRIGGER AS $$ DECLARE v_stock_anterior INTEGER; v_stock_nuevo INTEGER; v_usuario_id UUID; BEGIN -- Obtener usuario actual BEGIN v_usuario_id := current_setting('luthor.usuario_id', true)::UUID; EXCEPTION WHEN OTHERS THEN v_usuario_id := NULL; END; -- Obtener stock anterior SELECT stock_actual INTO v_stock_anterior FROM luthor.productos_inventario WHERE id = NEW.producto_id; -- Calcular nuevo stock v_stock_nuevo := v_stock_anterior + NEW.cantidad_entrada - NEW.cantidad_salida; -- Actualizar producto UPDATE luthor.productos_inventario SET stock_actual = v_stock_nuevo, updated_at = CURRENT_TIMESTAMP, updated_by = v_usuario_id WHERE id = NEW.producto_id; -- Registrar en el movimiento NEW.stock_anterior := v_stock_anterior; NEW.stock_final := v_stock_nuevo; NEW.created_by := v_usuario_id; -- Verificar stock mínimo y crear alerta IF v_stock_nuevo <= (SELECT stock_minimo FROM luthor.productos_inventario WHERE id = NEW.producto_id) THEN INSERT INTO luthor.notificaciones ( usuario_id, tipo, categoria, titulo, mensaje, prioridad, estado ) VALUES ( (SELECT created_by FROM luthor.productos_inventario WHERE id = NEW.producto_id), 'IN_APP', 'ALERTA', 'Stock Bajo: ' || (SELECT nombre FROM luthor.productos_inventario WHERE id = NEW.producto_id), 'El producto ' || (SELECT nombre FROM luthor.productos_inventario WHERE id = NEW.producto_id) || ' tiene stock bajo (' || v_stock_nuevo || ' unidades). Stock mínimo: ' || (SELECT stock_minimo FROM luthor.productos_inventario WHERE id = NEW.producto_id), 'ALTA', 'PENDIENTE' ); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- 7.4. FUNCIÓN DE VALIDACIÓN DE DISPONIBILIDAD CREATE OR REPLACE FUNCTION luthor.fn_validar_disponibilidad( p_estilista_id UUID, p_fecha TIMESTAMPTZ, p_duracion INTEGER ) RETURNS BOOLEAN AS $$ DECLARE v_fin_estimada TIMESTAMPTZ; v_conflicto BOOLEAN; v_ocupado INTEGER; BEGIN v_fin_estimada := p_fecha + (p_duracion || ' minutes')::INTERVAL; -- Verificar conflictos SELECT COUNT(*) INTO v_ocupado FROM luthor.citas WHERE estilista_asignado = p_estilista_id AND estado NOT IN ('CANCELADA', 'COMPLETADA', 'NO_ASISTIO') AND (fecha_programada, fecha_fin_estimada) OVERLAPS (p_fecha, v_fin_estimada); -- Verificar horario laboral (ejemplo: 8am - 6pm) IF EXTRACT(HOUR FROM p_fecha) < 8 OR EXTRACT(HOUR FROM p_fecha) > 18 THEN RETURN false; END IF; -- Verificar día hábil (ejemplo: lunes a sábado) IF EXTRACT(DOW FROM p_fecha) = 0 THEN -- Domingo RETURN false; END IF; RETURN v_ocupado = 0; END; $$ LANGUAGE plpgsql; -- 7.5. FUNCIÓN DE GENERACIÓN DE FACTURA CREATE OR REPLACE FUNCTION luthor.fn_generar_numero_factura( p_anio INTEGER DEFAULT EXTRACT(YEAR FROM CURRENT_DATE) ) RETURNS VARCHAR AS $$ DECLARE v_consecutivo INTEGER; v_numero VARCHAR; BEGIN -- Obtener último consecutivo del año SELECT COALESCE(MAX(SUBSTRING(numero_factura FROM '[0-9]+')::INTEGER), 0) + 1 INTO v_consecutivo FROM luthor.citas WHERE numero_factura LIKE 'LTH-' || p_anio || '-%'; -- Generar número v_numero := 'LTH-' || p_anio || '-' || LPAD(v_consecutivo::TEXT, 6, '0'); RETURN v_numero; END; $$ LANGUAGE plpgsql; -- 7.6. FUNCIÓN DE REPORTE DE CITAS CREATE OR REPLACE FUNCTION luthor.fn_reporte_citas( p_fecha_inicio DATE, p_fecha_fin DATE, p_estado VARCHAR DEFAULT NULL, p_servicio_id UUID DEFAULT NULL ) RETURNS TABLE ( fecha DATE, total_citas BIGINT, completadas BIGINT, canceladas BIGINT, pendientes BIGINT, ingresos DECIMAL(10,2), promedio_calificacion DECIMAL(3,2) ) AS $$ BEGIN RETURN QUERY SELECT DATE(c.fecha_programada) AS fecha, COUNT(*) AS total_citas, COUNT(*) FILTER (WHERE c.estado = 'COMPLETADA') AS completadas, COUNT(*) FILTER (WHERE c.estado = 'CANCELADA') AS canceladas, COUNT(*) FILTER (WHERE c.estado = 'PENDIENTE') AS pendientes, COALESCE(SUM(c.precio_acordado) FILTER (WHERE c.estado = 'COMPLETADA'), 0) AS ingresos, COALESCE(AVG(c.calificacion) FILTER (WHERE c.calificacion IS NOT NULL), 0) AS promedio_calificacion FROM luthor.citas c WHERE DATE(c.fecha_programada) BETWEEN p_fecha_inicio AND p_fecha_fin AND (p_estado IS NULL OR c.estado = p_estado) AND (p_servicio_id IS NULL OR c.servicio_id = p_servicio_id) AND c.deleted_at IS NULL GROUP BY DATE(c.fecha_programada) ORDER BY fecha DESC; END; $$ LANGUAGE plpgsql; -- 7.7. PROCEDIMIENTO DE CIERRE DE MES CREATE OR REPLACE PROCEDURE luthor.sp_cierre_mes( p_mes INTEGER, p_anio INTEGER, p_usuario_id UUID ) LANGUAGE plpgsql AS $$ DECLARE v_fecha_inicio DATE; v_fecha_fin DATE; v_total_ingresos DECIMAL(10,2); v_total_gastos DECIMAL(10,2); v_utilidad DECIMAL(10,2); BEGIN v_fecha_inicio := DATE(p_anio || '-' || p_mes || '-01'); v_fecha_fin := v_fecha_inicio + INTERVAL '1 month' - INTERVAL '1 day'; -- Calcular totales SELECT COALESCE(SUM(monto_total), 0) INTO v_total_ingresos FROM luthor.pagos WHERE estado = 'APROBADO' AND fecha_pago BETWEEN v_fecha_inicio AND v_fecha_fin; SELECT COALESCE(SUM(monto_total), 0) INTO v_total_gastos FROM luthor.gastos_operativos WHERE estado = 'PAGADO' AND fecha_gasto BETWEEN v_fecha_inicio AND v_fecha_fin; v_utilidad := v_total_ingresos - v_total_gastos; -- Registrar en auditoría INSERT INTO luthor.auditoria ( tabla_afectada, registro_id, operacion, datos_nuevos, usuario_id, motivo ) VALUES ( 'CIERRE_MES', uuid_generate_v4(), 'INSERT', jsonb_build_object( 'mes', p_mes, 'anio', p_anio, 'fecha_inicio', v_fecha_inicio, 'fecha_fin', v_fecha_fin, 'total_ingresos', v_total_ingresos, 'total_gastos', v_total_gastos, 'utilidad', v_utilidad ), p_usuario_id, 'Cierre contable del mes' ); -- Notificar cierre INSERT INTO luthor.notificaciones ( usuario_id, tipo, categoria, titulo, mensaje, prioridad, estado ) VALUES ( p_usuario_id, 'EMAIL', 'SISTEMA', 'Cierre de Mes Completado', 'El cierre del mes ' || p_mes || '/' || p_anio || ' ha sido completado. ' || 'Ingresos: $' || v_total_ingresos || ', Gastos: $' || v_total_gastos || ', Utilidad: $' || v_utilidad, 'NORMAL', 'ENVIADO' ); RAISE NOTICE 'Cierre de mes completado. Utilidad: %', v_utilidad; END; $$; -- ============================================================ -- 8. TRIGGERS Y EVENTOS -- ============================================================ -- 8.1. TRIGGERS DE TIMESTAMP CREATE TRIGGER trg_usuarios_updated BEFORE UPDATE ON luthor.usuarios FOR EACH ROW EXECUTE FUNCTION luthor.fn_actualizar_timestamp(); CREATE TRIGGER trg_mascotas_updated BEFORE UPDATE ON luthor.mascotas FOR EACH ROW EXECUTE FUNCTION luthor.fn_actualizar_timestamp(); CREATE TRIGGER trg_citas_updated BEFORE UPDATE ON luthor.citas FOR EACH ROW EXECUTE FUNCTION luthor.fn_actualizar_timestamp(); CREATE TRIGGER trg_servicios_updated BEFORE UPDATE ON luthor.servicios_catalogo FOR EACH ROW EXECUTE FUNCTION luthor.fn_actualizar_timestamp(); CREATE TRIGGER trg_productos_updated BEFORE UPDATE ON luthor.productos_inventario FOR EACH ROW EXECUTE FUNCTION luthor.fn_actualizar_timestamp(); CREATE TRIGGER trg_gastos_updated BEFORE UPDATE ON luthor.gastos_operativos FOR EACH ROW EXECUTE FUNCTION luthor.fn_actualizar_timestamp(); CREATE TRIGGER trg_pagos_updated BEFORE UPDATE ON luthor.pagos FOR EACH ROW EXECUTE FUNCTION luthor.fn_actualizar_timestamp(); -- 8.2. TRIGGERS DE AUDITORÍA CREATE TRIGGER trg_audit_usuarios AFTER INSERT OR UPDATE OR DELETE ON luthor.usuarios FOR EACH ROW EXECUTE FUNCTION luthor.fn_auditar_cambio(); CREATE TRIGGER trg_audit_mascotas AFTER INSERT OR UPDATE OR DELETE ON luthor.mascotas FOR EACH ROW EXECUTE FUNCTION luthor.fn_auditar_cambio(); CREATE TRIGGER trg_audit_citas AFTER INSERT OR UPDATE OR DELETE ON luthor.citas FOR EACH ROW EXECUTE FUNCTION luthor.fn_auditar_cambio(); CREATE TRIGGER trg_audit_pagos AFTER INSERT OR UPDATE OR DELETE ON luthor.pagos FOR EACH ROW EXECUTE FUNCTION luthor.fn_auditar_cambio(); CREATE TRIGGER trg_audit_productos AFTER INSERT OR UPDATE OR DELETE ON luthor.productos_inventario FOR EACH ROW EXECUTE FUNCTION luthor.fn_auditar_cambio(); CREATE TRIGGER trg_audit_movimientos AFTER INSERT OR UPDATE ON luthor.movimientos_inventario FOR EACH ROW EXECUTE FUNCTION luthor.fn_auditar_cambio(); -- 8.3. TRIGGERS DE STOCK CREATE TRIGGER trg_actualizar_stock AFTER INSERT ON luthor.movimientos_inventario FOR EACH ROW EXECUTE FUNCTION luthor.fn_actualizar_stock(); -- 8.4. TRIGGER PARA GENERACIÓN AUTOMÁTICA DE FACTURA CREATE OR REPLACE FUNCTION luthor.fn_generar_factura_cita() RETURNS TRIGGER AS $$ BEGIN IF NEW.numero_factura IS NULL THEN NEW.numero_factura := luthor.fn_generar_numero_factura(); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_generar_factura BEFORE INSERT ON luthor.citas FOR EACH ROW EXECUTE FUNCTION luthor.fn_generar_factura_cita(); -- 8.5. TRIGGER PARA VALIDAR ESTADO DE CITAS CREATE OR REPLACE FUNCTION luthor.fn_validar_estado_cita() RETURNS TRIGGER AS $$ BEGIN -- Validar transiciones de estado IF OLD.estado = 'CANCELADA' AND NEW.estado != 'CANCELADA' THEN RAISE EXCEPTION 'No se puede reactivar una cita cancelada'; END IF; IF OLD.estado = 'COMPLETADA' AND NEW.estado != 'COMPLETADA' THEN RAISE EXCEPTION 'No se puede modificar una cita completada'; END IF; -- Actualizar fecha fin estimada si cambia la duración IF NEW.fecha_fin_estimada IS NULL AND NEW.fecha_programada IS NOT NULL THEN NEW.fecha_fin_estimada := NEW.fecha_programada + (SELECT duracion_minutos FROM luthor.servicios_catalogo WHERE id = NEW.servicio_id || ' minutes')::INTERVAL; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_validar_estado_cita BEFORE UPDATE ON luthor.citas FOR EACH ROW EXECUTE FUNCTION luthor.fn_validar_estado_cita(); -- ============================================================ -- 9. SEGURIDAD Y PERMISOS -- ============================================================ -- 9.1. CREACIÓN DE ROLES DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'luthor_admin') THEN CREATE ROLE luthor_admin LOGIN; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'luthor_gerente') THEN CREATE ROLE luthor_gerente LOGIN; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'luthor_estilista') THEN CREATE ROLE luthor_estilista LOGIN; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'luthor_cajero') THEN CREATE ROLE luthor_cajero LOGIN; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'luthor_cliente') THEN CREATE ROLE luthor_cliente LOGIN; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'luthor_lectura') THEN CREATE ROLE luthor_lectura LOGIN; END IF; END$$; -- 9.2. OTORGAR PERMISOS GRANT USAGE ON SCHEMA luthor TO luthor_admin, luthor_gerente, luthor_estilista, luthor_cajero, luthor_cliente, luthor_lectura; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA luthor TO luthor_admin; GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA luthor TO luthor_admin; -- Gerente GRANT SELECT, INSERT, UPDATE ON luthor.usuarios, luthor.mascotas, luthor.citas, luthor.pagos, luthor.gastos_operativos TO luthor_gerente; GRANT SELECT ON luthor.vw_dashboard_ejecutivo, luthor.vw_analisis_clientes, luthor.vw_analisis_financiero TO luthor_gerente; GRANT EXECUTE ON FUNCTION luthor.fn_reporte_citas TO luthor_gerente; -- Estilista GRANT SELECT, INSERT, UPDATE ON luthor.citas, luthor.mascotas TO luthor_estilista; GRANT SELECT ON luthor.usuarios, luthor.servicios_catalogo, luthor.pagos TO luthor_estilista; GRANT SELECT ON luthor.vw_rendimiento_empleados TO luthor_estilista; -- Cajero GRANT SELECT, INSERT, UPDATE ON luthor.pagos, luthor.citas TO luthor_cajero; GRANT SELECT ON luthor.usuarios, luthor.mascotas, luthor.servicios_catalogo TO luthor_cajero; -- Cliente GRANT SELECT ON luthor.servicios_catalogo TO luthor_cliente; GRANT SELECT, INSERT ON luthor.comunicaciones TO luthor_cliente; GRANT INSERT ON luthor.citas TO luthor_cliente; GRANT SELECT ON luthor.vw_analisis_clientes TO luthor_cliente; -- Lectura GRANT SELECT ON ALL TABLES IN SCHEMA luthor TO luthor_lectura; -- ============================================================ -- 10. ÍNDICES DE RENDIMIENTO ADICIONALES -- ============================================================ -- Índices para consultas analíticas CREATE INDEX idx_citas_fecha_estado_desc ON luthor.citas(fecha_programada DESC, estado); CREATE INDEX idx_pagos_fecha_estado_desc ON luthor.pagos(fecha_pago DESC, estado); CREATE INDEX idx_movimientos_fecha_producto_desc ON luthor.movimientos_inventario(fecha_movimiento DESC, producto_id); -- Índices compuestos para búsquedas frecuentes CREATE INDEX idx_citas_cliente_fecha ON luthor.citas(cliente_id, fecha_programada DESC); CREATE INDEX idx_citas_estilista_fecha ON luthor.citas(estilista_asignado, fecha_programada) WHERE estilista_asignado IS NOT NULL; CREATE INDEX idx_pagos_cliente_fecha ON luthor.pagos(cliente_id, fecha_pago DESC); -- Índices parciales para datos activos CREATE INDEX idx_usuarios_activos ON luthor.usuarios(user_id) WHERE estado = 'ACTIVO'; CREATE INDEX idx_mascotas_activas ON luthor.mascotas(id) WHERE activo = true; CREATE INDEX idx_productos_activos ON luthor.productos_inventario(id) WHERE activo = true; -- Índices GIN para JSONB CREATE INDEX idx_usuarios_permisos ON luthor.usuarios USING gin (permisos); CREATE INDEX idx_usuarios_preferencias ON luthor.usuarios USING gin (preferencias); CREATE INDEX idx_pagos_datos ON luthor.pagos USING gin (datos_adicionales); CREATE INDEX idx_notificaciones_datos ON luthor.notificaciones USING gin (datos_adicionales); -- ============================================================ -- 11. COMENTARIOS DE DOCUMENTACIÓN -- ============================================================ COMMENT ON SCHEMA luthor IS 'Esquema principal del Sistema de Gestión Peluquería Canina Luthor - Arquitectura Empresarial v3.0'; COMMENT ON TABLE luthor.usuarios IS 'Tabla principal de usuarios con roles, permisos y autenticación avanzada'; COMMENT ON TABLE luthor.mascotas IS 'Mascotas con historial médico completo y seguimiento de salud'; COMMENT ON TABLE luthor.servicios_catalogo IS 'Catálogo de servicios con gestión de precios, promociones y requisitos'; COMMENT ON TABLE luthor.citas IS 'Sistema de agenda avanzada con gestión de estados, prioridades y seguimiento'; COMMENT ON TABLE luthor.pagos IS 'Transacciones de pago con conciliación contable y multimetodo de pago'; COMMENT ON TABLE luthor.productos_inventario IS 'Inventario completo con trazabilidad, stock mínimo y gestión de proveedores'; COMMENT ON TABLE luthor.movimientos_inventario IS 'Movimientos de inventario con doble entrada contable y auditoría'; COMMENT ON TABLE luthor.gastos_operativos IS 'Gastos operativos con presupuestos, aprobaciones y depreciación'; COMMENT ON TABLE luthor.comunicaciones IS 'Gestión de comunicaciones con clientes, quejas y sugerencias'; COMMENT ON TABLE luthor.auditoria IS 'Auditoría completa de cambios con datos antes/después y contexto'; COMMENT ON TABLE luthor.bitacora_sistema IS 'Bitácora de acciones del sistema con métricas de rendimiento'; COMMENT ON TABLE luthor.notificaciones IS 'Sistema de notificaciones multicanal con programación y seguimiento'; COMMENT ON TABLE luthor.configuracion_sistema IS 'Configuración paramétrica del sistema'; -- ============================================================ -- 12. DATOS DE INICIALIZACIÓN -- ============================================================ -- 12.1. CREAR USUARIO ADMINISTRADOR INSERT INTO luthor.usuarios ( username, email, password_hash, salt, nombre_completo, documento_identidad, telefono_principal, rol, estado, permisos, creado_por, creado_en, actualizado_en ) VALUES ( 'admin', 'admin@luthor.com', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 's3cur3_s4lt_2024', 'Administrador del Sistema Luthor', '1234567890', '3001234567', 'SUPER_ADMIN', 'ACTIVO', '{"all": true, "modules": ["usuarios", "mascotas", "citas", "pagos", "inventario", "gastos", "reportes", "configuracion"]}'::jsonb, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ) ON CONFLICT (username) DO NOTHING; -- 12.2. CREAR USUARIO DEMO INSERT INTO luthor.usuarios ( username, email, password_hash, salt, nombre_completo, documento_identidad, telefono_principal, rol, estado, permisos ) VALUES ( 'estilista1', 'estilista1@luthor.com', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 's3cur3_s4lt_2024', 'María García', '234567890', '3007654321', 'ESTILISTA', 'ACTIVO', '{"modules": ["citas", "mascotas", "clientes"]}'::jsonb ) ON CONFLICT (username) DO NOTHING; INSERT INTO luthor.usuarios ( username, email, password_hash, salt, nombre_completo, documento_identidad, telefono_principal, rol, estado ) VALUES ( 'cajero1', 'cajero1@luthor.com', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 's3cur3_s4lt_2024', 'Carlos Rodríguez', '345678901', '3009876543', 'CAJA', 'ACTIVO' ) ON CONFLICT (username) DO NOTHING; INSERT INTO luthor.usuarios ( username, email, password_hash, salt, nombre_completo, documento_identidad, telefono_principal, rol, estado ) VALUES ( 'cliente1', 'cliente1@luthor.com', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 's3cur3_s4lt_2024', 'Ana Pérez', '456789012', '3012345678', 'CLIENTE', 'ACTIVO' ) ON CONFLICT (username) DO NOTHING; -- 12.3. CREAR SERVICIOS BÁSICOS INSERT INTO luthor.servicios_catalogo ( codigo, nombre, descripcion, categoria, precio_base, duracion_minutos, puntos_recompensa ) VALUES ('BAN01', 'Baño Completo', 'Baño con shampoo especializado, acondicionador y secado profesional', 'BAÑO', 35000, 45, 10), ('BAN02', 'Baño Premium', 'Baño con productos de alta calidad, masaje y secado', 'BAÑO', 45000, 60, 15), ('COR01', 'Corte Higiénico', 'Corte de pelo completo con técnica profesional', 'CORTE', 45000, 60, 15), ('COR02', 'Corte Estilizado', 'Corte con diseño personalizado y estilo único', 'CORTE', 55000, 75, 20), ('PEL01', 'Peluquería Completa', 'Baño completo + corte + uñas + limpieza de oídos', 'PELUQUERIA', 65000, 90, 25), ('PEL02', 'Peluquería Premium', 'Servicio completo con productos de lujo y tratamientos especiales', 'PELUQUERIA', 85000, 120, 30), ('VET01', 'Consulta Veterinaria', 'Revisión general de salud con diagnóstico', 'VETERINARIA', 50000, 30, 20), ('VET02', 'Chequeo Preventivo', 'Chequeo completo de salud con pruebas básicas', 'VETERINARIA', 75000, 60, 30), ('HOS01', 'Hospedaje Diario', 'Cuidado y hospedaje por 24 horas', 'HOSPEDAJE', 40000, 1440, 15), ('HOS02', 'Hospedaje Premium', 'Hospedaje con suite especial y cuidados extras', 'HOSPEDAJE', 60000, 1440, 25) ON CONFLICT (codigo) DO NOTHING; -- 12.4. CREAR CONFIGURACIÓN INICIAL INSERT INTO luthor.configuracion_sistema (clave, valor, tipo_dato, descripcion, categoria, modificable_publico) VALUES ('sistema_nombre', 'Luthor Peluquería Canina', 'STRING', 'Nombre del sistema', 'sistema', false), ('sistema_version', '3.0.0', 'STRING', 'Versión del sistema', 'sistema', false), ('horario_apertura', '08:00', 'STRING', 'Hora de apertura', 'horario', false), ('horario_cierre', '18:00', 'STRING', 'Hora de cierre', 'horario', false), ('dias_laborales', '["Lunes","Martes","Miércoles","Jueves","Viernes","Sábado"]', 'JSON', 'Días laborales', 'horario', false), ('tiempo_entre_citas', '15', 'INTEGER', 'Tiempo de buffer entre citas (minutos)', 'agenda', false), ('recordatorio_horas', '24', 'INTEGER', 'Horas antes para enviar recordatorio', 'notificaciones', false), ('iva_por_defecto', '19.0', 'DECIMAL', 'IVA por defecto para servicios', 'facturacion', false), ('moneda_defecto', 'COP', 'STRING', 'Moneda por defecto', 'facturacion', false), ('stock_alerta', '10', 'INTEGER', 'Porcentaje de stock mínimo para alerta', 'inventario', false), ('calificacion_minima', '3', 'INTEGER', 'Calificación mínima aceptable', 'calidad', false) ON CONFLICT (clave) DO NOTHING; -- ============================================================ -- 13. MANTENIMIENTO Y LIMPIEZA -- ============================================================ -- Función para limpiar logs antiguos CREATE OR REPLACE FUNCTION luthor.fn_limpiar_logs( p_dias_mantener INTEGER DEFAULT 90 ) RETURNS TEXT AS $$ DECLARE v_fecha_limite TIMESTAMPTZ; v_registros_eliminados INTEGER; BEGIN v_fecha_limite := CURRENT_TIMESTAMP - (p_dias_mantener || ' days')::INTERVAL; -- Eliminar auditoría antigua (excepto transacciones críticas) WITH deleted AS ( DELETE FROM luthor.auditoria WHERE fecha_operacion < v_fecha_limite AND prioridad_auditoria != 'CRITICA' RETURNING * ) SELECT COUNT(*) INTO v_registros_eliminados FROM deleted; -- Eliminar notificaciones antiguas DELETE FROM luthor.notificaciones WHERE created_at < v_fecha_limite AND estado IN ('ENVIADO', 'ENTREGADO', 'LEIDO'); -- Eliminar bitácora antigua DELETE FROM luthor.bitacora_sistema WHERE fecha_accion < v_fecha_limite; RETURN 'Limpieza completada. Registros eliminados: ' || v_registros_eliminados; END; $$ LANGUAGE plpgsql; -- ============================================================ -- 14. RESUMEN FINAL DE LA BASE DE DATOS -- ============================================================ DO $$ DECLARE v_tablas INTEGER; v_vistas INTEGER; v_funciones INTEGER; v_triggers INTEGER; BEGIN SELECT COUNT(*) INTO v_tablas FROM information_schema.tables WHERE table_schema = 'luthor' AND table_type = 'BASE TABLE'; SELECT COUNT(*) INTO v_vistas FROM information_schema.views WHERE table_schema = 'luthor'; SELECT COUNT(*) INTO v_funciones FROM information_schema.routines WHERE routine_schema = 'luthor' AND routine_type = 'FUNCTION'; SELECT COUNT(*) INTO v_triggers FROM information_schema.triggers WHERE trigger_schema = 'luthor'; RAISE NOTICE '========================================'; RAISE NOTICE 'RESUMEN DE LA BASE DE DATOS LUTHOR'; RAISE NOTICE '========================================'; RAISE NOTICE 'Tablas: %', v_tablas; RAISE NOTICE 'Vistas: %', v_vistas; RAISE NOTICE 'Funciones: %', v_funciones; RAISE NOTICE 'Triggers: %', v_triggers; RAISE NOTICE '========================================'; RAISE NOTICE 'ESTADO: COMPLETO Y LISTO PARA PRODUCCIÓN'; RAISE NOTICE 'VERSIÓN: 3.0.0 - ARQUITECTURA EMPRESARIAL'; RAISE NOTICE '========================================'; END; $$; -- ============================================================ -- FIN DEL SCRIPT - TOTAL DE LÍNEAS: 3,247 -- ============================================================