From afa4f1a6fcb4a12c8e1eed7112972a5323cdad6e Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Fri, 16 Jan 2026 08:22:57 -0600 Subject: [PATCH] =?UTF-8?q?Migraci=C3=B3n=20desde=20clinica-veterinaria/da?= =?UTF-8?q?tabase=20-=20Est=C3=A1ndar=20multi-repo=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Código migrado del proyecto monorepo original Co-Authored-By: Claude Opus 4.5 --- schemas/01-veterinaria-schema-ddl.sql | 387 +++++++++++++++++++ schemas/02-veterinaria-farmacia-ddl.sql | 464 +++++++++++++++++++++++ seeds/fase8/01-veterinaria-catalogos.sql | 146 +++++++ 3 files changed, 997 insertions(+) create mode 100644 schemas/01-veterinaria-schema-ddl.sql create mode 100644 schemas/02-veterinaria-farmacia-ddl.sql create mode 100644 seeds/fase8/01-veterinaria-catalogos.sql diff --git a/schemas/01-veterinaria-schema-ddl.sql b/schemas/01-veterinaria-schema-ddl.sql new file mode 100644 index 0000000..5959b93 --- /dev/null +++ b/schemas/01-veterinaria-schema-ddl.sql @@ -0,0 +1,387 @@ +-- ============================================================================ +-- VETERINARIA SCHEMA - Especialización de ERP-Clínicas +-- Clínica Veterinaria +-- ============================================================================ +-- Fecha: 2026-01-04 +-- Versión: 1.0 +-- Hereda de: erp-clinicas FASE-8 +-- ============================================================================ + +-- Schema +CREATE SCHEMA IF NOT EXISTS veterinaria; + +-- ============================================================================ +-- ENUMS +-- ============================================================================ + +DO $$ BEGIN + CREATE TYPE veterinaria.sexo_animal AS ENUM ('macho', 'hembra', 'desconocido'); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + CREATE TYPE veterinaria.estado_hospitalizacion AS ENUM ( + 'ingresado', 'en_tratamiento', 'estable', 'critico', 'alta', 'fallecido' + ); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +-- ============================================================================ +-- CATÁLOGOS +-- ============================================================================ + +-- Especies +CREATE TABLE IF NOT EXISTS veterinaria.especies ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + nombre VARCHAR(50) NOT NULL, + nombre_cientifico VARCHAR(100), + descripcion TEXT, + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.especies IS 'Catálogo de especies animales'; + +-- Razas +CREATE TABLE IF NOT EXISTS veterinaria.razas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + especie_id UUID NOT NULL REFERENCES veterinaria.especies(id) ON DELETE CASCADE, + nombre VARCHAR(100) NOT NULL, + descripcion TEXT, + tamanio_promedio VARCHAR(20), -- 'pequeño', 'mediano', 'grande', 'gigante' + peso_promedio_kg NUMERIC(5,2), + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.razas IS 'Catálogo de razas por especie'; + +-- Vacunas +CREATE TABLE IF NOT EXISTS veterinaria.vacunas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + nombre VARCHAR(100) NOT NULL, + descripcion TEXT, + especie_id UUID REFERENCES veterinaria.especies(id), + laboratorio VARCHAR(100), + dosis_ml NUMERIC(5,2), + intervalo_refuerzo_dias INTEGER, + es_obligatoria BOOLEAN DEFAULT false, + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.vacunas IS 'Catálogo de vacunas veterinarias'; + +-- ============================================================================ +-- TABLAS PRINCIPALES +-- ============================================================================ + +-- Propietarios (dueños de mascotas) +CREATE TABLE IF NOT EXISTS veterinaria.propietarios ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + partner_id UUID, -- Referencia opcional a core.partners + nombre VARCHAR(100) NOT NULL, + apellidos VARCHAR(100), + telefono VARCHAR(20), + telefono_emergencia VARCHAR(20), + email VARCHAR(100), + direccion TEXT, + rfc VARCHAR(13), + -- Control + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.propietarios IS 'Propietarios/dueños de mascotas'; + +-- Mascotas (pacientes) +CREATE TABLE IF NOT EXISTS veterinaria.mascotas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + propietario_id UUID NOT NULL REFERENCES veterinaria.propietarios(id), + especie_id UUID NOT NULL REFERENCES veterinaria.especies(id), + raza_id UUID REFERENCES veterinaria.razas(id), + -- Datos básicos + nombre VARCHAR(100) NOT NULL, + sexo veterinaria.sexo_animal DEFAULT 'desconocido', + fecha_nacimiento DATE, + edad_aproximada VARCHAR(50), -- "3 años", "6 meses" + color VARCHAR(50), + peso_kg NUMERIC(6,2), + -- Identificación + numero_chip VARCHAR(50), + tiene_chip BOOLEAN DEFAULT false, + -- Estado + esterilizado BOOLEAN DEFAULT false, + fecha_esterilizacion DATE, + -- Notas + alergias TEXT, + condiciones_especiales TEXT, + notas TEXT, + -- Foto + foto_url VARCHAR(255), + -- Control + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.mascotas IS 'Mascotas/pacientes de la clínica veterinaria'; + +-- Cartilla de vacunación +CREATE TABLE IF NOT EXISTS veterinaria.cartilla_vacunacion ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + mascota_id UUID NOT NULL REFERENCES veterinaria.mascotas(id) ON DELETE CASCADE, + vacuna_id UUID NOT NULL REFERENCES veterinaria.vacunas(id), + veterinario_id UUID, -- Referencia a clinica.doctors + -- Datos de aplicación + fecha_aplicacion DATE NOT NULL, + fecha_proximo_refuerzo DATE, + lote VARCHAR(50), + laboratorio VARCHAR(100), + -- Notas + observaciones TEXT, + -- Control + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.cartilla_vacunacion IS 'Historial de vacunación de mascotas'; + +-- Desparasitaciones +CREATE TABLE IF NOT EXISTS veterinaria.desparasitaciones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + mascota_id UUID NOT NULL REFERENCES veterinaria.mascotas(id) ON DELETE CASCADE, + veterinario_id UUID, + -- Datos + tipo VARCHAR(50) NOT NULL, -- 'interna', 'externa', 'ambas' + producto VARCHAR(100) NOT NULL, + dosis VARCHAR(50), + via_administracion VARCHAR(50), + fecha_aplicacion DATE NOT NULL, + fecha_proxima DATE, + -- Notas + observaciones TEXT, + -- Control + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.desparasitaciones IS 'Historial de desparasitaciones'; + +-- Hospitalización +CREATE TABLE IF NOT EXISTS veterinaria.hospitalizacion ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + mascota_id UUID NOT NULL REFERENCES veterinaria.mascotas(id), + veterinario_id UUID, + consultation_id UUID, -- Referencia a clinica.consultations + -- Datos de ingreso + fecha_ingreso TIMESTAMPTZ NOT NULL DEFAULT NOW(), + motivo_ingreso TEXT NOT NULL, + diagnostico_ingreso TEXT, + -- Ubicación + area VARCHAR(50), -- 'jaula_pequena', 'jaula_grande', 'quirofano', 'uci' + numero_jaula VARCHAR(20), + -- Estado + estado veterinaria.estado_hospitalizacion DEFAULT 'ingresado', + -- Alta + fecha_alta TIMESTAMPTZ, + diagnostico_alta TEXT, + instrucciones_alta TEXT, + -- Control + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.hospitalizacion IS 'Registro de hospitalizaciones'; + +-- Monitoreo de hospitalización +CREATE TABLE IF NOT EXISTS veterinaria.hospitalizacion_monitoreo ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + hospitalizacion_id UUID NOT NULL REFERENCES veterinaria.hospitalizacion(id) ON DELETE CASCADE, + -- Signos vitales + fecha_hora TIMESTAMPTZ NOT NULL DEFAULT NOW(), + peso_kg NUMERIC(6,2), + temperatura NUMERIC(4,1), + frecuencia_cardiaca INTEGER, + frecuencia_respiratoria INTEGER, + -- Alimentación + comio BOOLEAN, + bebio_agua BOOLEAN, + -- Eliminación + orino BOOLEAN, + defeco BOOLEAN, + consistencia_heces VARCHAR(50), + -- Estado + estado_animo VARCHAR(50), + nivel_dolor INTEGER CHECK (nivel_dolor BETWEEN 0 AND 10), + -- Notas + observaciones TEXT, + registrado_por UUID, -- employee_id + -- Control + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.hospitalizacion_monitoreo IS 'Monitoreo durante hospitalización'; + +-- Servicios de estética +CREATE TABLE IF NOT EXISTS veterinaria.estetica ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + mascota_id UUID NOT NULL REFERENCES veterinaria.mascotas(id), + estilista_id UUID, -- employee_id + -- Servicios + fecha_servicio TIMESTAMPTZ NOT NULL DEFAULT NOW(), + servicios TEXT[], -- ['baño', 'corte', 'limpieza_oidos', 'corte_unas'] + tipo_corte VARCHAR(50), + shampoo_usado VARCHAR(100), + -- Estado + estado VARCHAR(20) DEFAULT 'pendiente', -- 'pendiente', 'en_proceso', 'terminado' + hora_inicio TIME, + hora_fin TIME, + -- Notas + observaciones TEXT, + observaciones_piel TEXT, + -- Precio + precio NUMERIC(10,2), + -- Control + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.estetica IS 'Servicios de estética/grooming'; + +-- ============================================================================ +-- EXTENSIONES A TABLAS DE ERP-CLINICAS +-- ============================================================================ + +-- Extensión a clinica.consultations (si existe) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = 'clinica' AND table_name = 'consultations') THEN + + -- mascota_id + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'clinica' AND table_name = 'consultations' + AND column_name = 'mascota_id') THEN + ALTER TABLE clinica.consultations ADD COLUMN mascota_id UUID + REFERENCES veterinaria.mascotas(id); + END IF; + + -- peso_actual + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'clinica' AND table_name = 'consultations' + AND column_name = 'peso_actual') THEN + ALTER TABLE clinica.consultations ADD COLUMN peso_actual NUMERIC(6,2); + END IF; + + -- temperatura + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'clinica' AND table_name = 'consultations' + AND column_name = 'temperatura') THEN + ALTER TABLE clinica.consultations ADD COLUMN temperatura NUMERIC(4,1); + END IF; + + END IF; +END $$; + +-- ============================================================================ +-- ÍNDICES +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_especies_tenant ON veterinaria.especies(tenant_id); +CREATE INDEX IF NOT EXISTS idx_razas_tenant ON veterinaria.razas(tenant_id); +CREATE INDEX IF NOT EXISTS idx_razas_especie ON veterinaria.razas(especie_id); +CREATE INDEX IF NOT EXISTS idx_vacunas_tenant ON veterinaria.vacunas(tenant_id); +CREATE INDEX IF NOT EXISTS idx_vacunas_especie ON veterinaria.vacunas(especie_id); + +CREATE INDEX IF NOT EXISTS idx_propietarios_tenant ON veterinaria.propietarios(tenant_id); +CREATE INDEX IF NOT EXISTS idx_propietarios_telefono ON veterinaria.propietarios(telefono); + +CREATE INDEX IF NOT EXISTS idx_mascotas_tenant ON veterinaria.mascotas(tenant_id); +CREATE INDEX IF NOT EXISTS idx_mascotas_propietario ON veterinaria.mascotas(propietario_id); +CREATE INDEX IF NOT EXISTS idx_mascotas_especie ON veterinaria.mascotas(especie_id); +CREATE INDEX IF NOT EXISTS idx_mascotas_chip ON veterinaria.mascotas(numero_chip) WHERE numero_chip IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_cartilla_tenant ON veterinaria.cartilla_vacunacion(tenant_id); +CREATE INDEX IF NOT EXISTS idx_cartilla_mascota ON veterinaria.cartilla_vacunacion(mascota_id); +CREATE INDEX IF NOT EXISTS idx_cartilla_fecha ON veterinaria.cartilla_vacunacion(fecha_proximo_refuerzo); + +CREATE INDEX IF NOT EXISTS idx_desparasitaciones_tenant ON veterinaria.desparasitaciones(tenant_id); +CREATE INDEX IF NOT EXISTS idx_desparasitaciones_mascota ON veterinaria.desparasitaciones(mascota_id); + +CREATE INDEX IF NOT EXISTS idx_hospitalizacion_tenant ON veterinaria.hospitalizacion(tenant_id); +CREATE INDEX IF NOT EXISTS idx_hospitalizacion_mascota ON veterinaria.hospitalizacion(mascota_id); +CREATE INDEX IF NOT EXISTS idx_hospitalizacion_estado ON veterinaria.hospitalizacion(tenant_id, estado); + +CREATE INDEX IF NOT EXISTS idx_hospitalizacion_monitoreo_hosp ON veterinaria.hospitalizacion_monitoreo(hospitalizacion_id); + +CREATE INDEX IF NOT EXISTS idx_estetica_tenant ON veterinaria.estetica(tenant_id); +CREATE INDEX IF NOT EXISTS idx_estetica_mascota ON veterinaria.estetica(mascota_id); +CREATE INDEX IF NOT EXISTS idx_estetica_fecha ON veterinaria.estetica(fecha_servicio); + +-- ============================================================================ +-- RLS +-- ============================================================================ + +ALTER TABLE veterinaria.especies ENABLE ROW LEVEL SECURITY; +ALTER TABLE veterinaria.razas ENABLE ROW LEVEL SECURITY; +ALTER TABLE veterinaria.vacunas ENABLE ROW LEVEL SECURITY; +ALTER TABLE veterinaria.propietarios ENABLE ROW LEVEL SECURITY; +ALTER TABLE veterinaria.mascotas ENABLE ROW LEVEL SECURITY; +ALTER TABLE veterinaria.cartilla_vacunacion ENABLE ROW LEVEL SECURITY; +ALTER TABLE veterinaria.desparasitaciones ENABLE ROW LEVEL SECURITY; +ALTER TABLE veterinaria.hospitalizacion ENABLE ROW LEVEL SECURITY; +ALTER TABLE veterinaria.hospitalizacion_monitoreo ENABLE ROW LEVEL SECURITY; +ALTER TABLE veterinaria.estetica ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS tenant_isolation_especies ON veterinaria.especies; +CREATE POLICY tenant_isolation_especies ON veterinaria.especies + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_razas ON veterinaria.razas; +CREATE POLICY tenant_isolation_razas ON veterinaria.razas + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_vacunas ON veterinaria.vacunas; +CREATE POLICY tenant_isolation_vacunas ON veterinaria.vacunas + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_propietarios ON veterinaria.propietarios; +CREATE POLICY tenant_isolation_propietarios ON veterinaria.propietarios + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_mascotas ON veterinaria.mascotas; +CREATE POLICY tenant_isolation_mascotas ON veterinaria.mascotas + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_cartilla ON veterinaria.cartilla_vacunacion; +CREATE POLICY tenant_isolation_cartilla ON veterinaria.cartilla_vacunacion + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_desparasitaciones ON veterinaria.desparasitaciones; +CREATE POLICY tenant_isolation_desparasitaciones ON veterinaria.desparasitaciones + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_hospitalizacion ON veterinaria.hospitalizacion; +CREATE POLICY tenant_isolation_hospitalizacion ON veterinaria.hospitalizacion + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_hosp_monitoreo ON veterinaria.hospitalizacion_monitoreo; +CREATE POLICY tenant_isolation_hosp_monitoreo ON veterinaria.hospitalizacion_monitoreo + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_estetica ON veterinaria.estetica; +CREATE POLICY tenant_isolation_estetica ON veterinaria.estetica + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- ============================================================================ +-- FIN VETERINARIA SCHEMA +-- ============================================================================ diff --git a/schemas/02-veterinaria-farmacia-ddl.sql b/schemas/02-veterinaria-farmacia-ddl.sql new file mode 100644 index 0000000..2287e32 --- /dev/null +++ b/schemas/02-veterinaria-farmacia-ddl.sql @@ -0,0 +1,464 @@ +-- ============================================================================ +-- VETERINARIA SCHEMA - Farmacia (VET-006) +-- Sistema de gestion de farmacia veterinaria +-- ============================================================================ +-- Fecha: 2026-01-07 +-- Version: 1.0 +-- Basado en: VET-006-farmacia.md +-- ============================================================================ + +-- ============================================================================ +-- ENUMS +-- ============================================================================ + +DO $$ BEGIN + CREATE TYPE veterinaria.categoria_medicamento AS ENUM ( + 'antibiotico', + 'antiparasitario', + 'analgesico', + 'antiinflamatorio', + 'vacuna', + 'vitamina', + 'dermatologico', + 'oftalmico', + 'cardiaco', + 'digestivo', + 'otro' + ); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + CREATE TYPE veterinaria.tipo_movimiento_farmacia AS ENUM ( + 'entrada', + 'salida', + 'ajuste_positivo', + 'ajuste_negativo', + 'devolucion', + 'merma' + ); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + CREATE TYPE veterinaria.fraccion_controlada AS ENUM ( + 'no_controlado', + 'fraccion_i', + 'fraccion_ii', + 'fraccion_iii', + 'fraccion_iv' + ); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +-- ============================================================================ +-- TABLAS PRINCIPALES +-- ============================================================================ + +-- Catalogo de medicamentos +CREATE TABLE IF NOT EXISTS veterinaria.medicamentos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + -- Identificacion + codigo VARCHAR(50), + nombre VARCHAR(150) NOT NULL, + nombre_comercial VARCHAR(150), + principio_activo VARCHAR(200), + -- Clasificacion + categoria veterinaria.categoria_medicamento DEFAULT 'otro', + -- Presentacion + presentacion VARCHAR(100), -- 'tabletas', 'inyectable', 'suspension', etc. + concentracion VARCHAR(50), -- '500mg', '100mg/ml' + contenido VARCHAR(50), -- '30 tabletas', '100ml' + -- Fabricante + laboratorio VARCHAR(100), + -- Control + requiere_receta BOOLEAN DEFAULT false, + controlado BOOLEAN DEFAULT false, + fraccion_controlada veterinaria.fraccion_controlada DEFAULT 'no_controlado', + -- Stock + stock_minimo INTEGER DEFAULT 10, + stock_actual INTEGER DEFAULT 0, + -- Precios + precio_compra NUMERIC(10,2), + precio_venta NUMERIC(10,2), + -- Especies aplicables (NULL = todas) + especies_aplicables UUID[], -- Array de especie_id + -- Estado + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.medicamentos IS 'Catalogo de medicamentos veterinarios'; + +-- Lotes de medicamentos (control de caducidad) +CREATE TABLE IF NOT EXISTS veterinaria.medicamentos_lotes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + medicamento_id UUID NOT NULL REFERENCES veterinaria.medicamentos(id) ON DELETE CASCADE, + -- Lote + numero_lote VARCHAR(50) NOT NULL, + fecha_caducidad DATE NOT NULL, + -- Cantidades + cantidad_inicial INTEGER NOT NULL, + cantidad_actual INTEGER NOT NULL, + -- Compra + precio_compra NUMERIC(10,2), + factura_compra VARCHAR(50), + proveedor VARCHAR(100), + fecha_recepcion DATE DEFAULT CURRENT_DATE, + -- Estado + bloqueado BOOLEAN DEFAULT false, -- Bloquear si esta vencido + motivo_bloqueo TEXT, + -- Control + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.medicamentos_lotes IS 'Lotes de medicamentos con control de caducidad'; + +-- Dispensaciones de medicamentos +CREATE TABLE IF NOT EXISTS veterinaria.dispensaciones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + -- Referencias + medicamento_id UUID NOT NULL REFERENCES veterinaria.medicamentos(id), + lote_id UUID NOT NULL REFERENCES veterinaria.medicamentos_lotes(id), + mascota_id UUID REFERENCES veterinaria.mascotas(id), + veterinario_id UUID, -- Referencia a clinica.doctors + receta_id UUID, -- Referencia a clinica.prescriptions si existe + consultation_id UUID, -- Referencia a clinica.consultations + -- Dispensacion + cantidad INTEGER NOT NULL, + fecha_dispensacion TIMESTAMPTZ DEFAULT NOW(), + -- Instrucciones + dosis VARCHAR(100), -- '1 tableta cada 8 horas' + duracion_tratamiento VARCHAR(50), -- '7 dias' + instrucciones TEXT, + -- Control + dispensado_por UUID, -- employee_id + notas TEXT, + -- Auditoria + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.dispensaciones IS 'Registro de dispensacion de medicamentos'; + +-- Movimientos de inventario (kardex) +CREATE TABLE IF NOT EXISTS veterinaria.movimientos_farmacia ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + medicamento_id UUID NOT NULL REFERENCES veterinaria.medicamentos(id), + lote_id UUID REFERENCES veterinaria.medicamentos_lotes(id), + -- Movimiento + tipo veterinaria.tipo_movimiento_farmacia NOT NULL, + cantidad INTEGER NOT NULL, + stock_anterior INTEGER NOT NULL, + stock_posterior INTEGER NOT NULL, + -- Referencia + referencia_tipo VARCHAR(50), -- 'dispensacion', 'compra', 'ajuste' + referencia_id UUID, + -- Detalles + motivo TEXT, + documento VARCHAR(100), -- Factura, nota, etc. + -- Auditoria + usuario_id UUID, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.movimientos_farmacia IS 'Kardex de movimientos de inventario de farmacia'; + +-- Bitacora de medicamentos controlados +CREATE TABLE IF NOT EXISTS veterinaria.bitacora_controlados ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + medicamento_id UUID NOT NULL REFERENCES veterinaria.medicamentos(id), + lote_id UUID REFERENCES veterinaria.medicamentos_lotes(id), + dispensacion_id UUID REFERENCES veterinaria.dispensaciones(id), + -- Movimiento + tipo_movimiento veterinaria.tipo_movimiento_farmacia NOT NULL, + cantidad INTEGER NOT NULL, + -- Paciente + mascota_id UUID REFERENCES veterinaria.mascotas(id), + propietario_nombre VARCHAR(200), -- Snapshot del nombre + -- Prescripcion + receta_id UUID, + veterinario_id UUID, + veterinario_cedula VARCHAR(50), + -- Justificacion + justificacion TEXT NOT NULL, + diagnostico TEXT, + -- Auditoria + fecha_registro TIMESTAMPTZ DEFAULT NOW(), + registrado_por UUID NOT NULL, + ip_address VARCHAR(45), + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE veterinaria.bitacora_controlados IS 'Bitacora de movimientos de medicamentos controlados (requerido por COFEPRIS)'; + +-- ============================================================================ +-- INDICES +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_medicamentos_tenant ON veterinaria.medicamentos(tenant_id); +CREATE INDEX IF NOT EXISTS idx_medicamentos_codigo ON veterinaria.medicamentos(tenant_id, codigo); +CREATE INDEX IF NOT EXISTS idx_medicamentos_nombre ON veterinaria.medicamentos(tenant_id, nombre); +CREATE INDEX IF NOT EXISTS idx_medicamentos_categoria ON veterinaria.medicamentos(tenant_id, categoria); +CREATE INDEX IF NOT EXISTS idx_medicamentos_controlado ON veterinaria.medicamentos(tenant_id, controlado) WHERE controlado = true; +CREATE INDEX IF NOT EXISTS idx_medicamentos_stock_bajo ON veterinaria.medicamentos(tenant_id) + WHERE stock_actual <= stock_minimo; + +CREATE INDEX IF NOT EXISTS idx_lotes_tenant ON veterinaria.medicamentos_lotes(tenant_id); +CREATE INDEX IF NOT EXISTS idx_lotes_medicamento ON veterinaria.medicamentos_lotes(medicamento_id); +CREATE INDEX IF NOT EXISTS idx_lotes_caducidad ON veterinaria.medicamentos_lotes(fecha_caducidad); +CREATE INDEX IF NOT EXISTS idx_lotes_numero ON veterinaria.medicamentos_lotes(tenant_id, numero_lote); +CREATE INDEX IF NOT EXISTS idx_lotes_proximos_caducar ON veterinaria.medicamentos_lotes(tenant_id, fecha_caducidad) + WHERE cantidad_actual > 0 AND bloqueado = false; + +CREATE INDEX IF NOT EXISTS idx_dispensaciones_tenant ON veterinaria.dispensaciones(tenant_id); +CREATE INDEX IF NOT EXISTS idx_dispensaciones_medicamento ON veterinaria.dispensaciones(medicamento_id); +CREATE INDEX IF NOT EXISTS idx_dispensaciones_mascota ON veterinaria.dispensaciones(mascota_id); +CREATE INDEX IF NOT EXISTS idx_dispensaciones_fecha ON veterinaria.dispensaciones(fecha_dispensacion); +CREATE INDEX IF NOT EXISTS idx_dispensaciones_veterinario ON veterinaria.dispensaciones(veterinario_id); + +CREATE INDEX IF NOT EXISTS idx_movimientos_tenant ON veterinaria.movimientos_farmacia(tenant_id); +CREATE INDEX IF NOT EXISTS idx_movimientos_medicamento ON veterinaria.movimientos_farmacia(medicamento_id); +CREATE INDEX IF NOT EXISTS idx_movimientos_fecha ON veterinaria.movimientos_farmacia(created_at); + +CREATE INDEX IF NOT EXISTS idx_bitacora_tenant ON veterinaria.bitacora_controlados(tenant_id); +CREATE INDEX IF NOT EXISTS idx_bitacora_medicamento ON veterinaria.bitacora_controlados(medicamento_id); +CREATE INDEX IF NOT EXISTS idx_bitacora_fecha ON veterinaria.bitacora_controlados(fecha_registro); + +-- ============================================================================ +-- RLS +-- ============================================================================ + +ALTER TABLE veterinaria.medicamentos ENABLE ROW LEVEL SECURITY; +ALTER TABLE veterinaria.medicamentos_lotes ENABLE ROW LEVEL SECURITY; +ALTER TABLE veterinaria.dispensaciones ENABLE ROW LEVEL SECURITY; +ALTER TABLE veterinaria.movimientos_farmacia ENABLE ROW LEVEL SECURITY; +ALTER TABLE veterinaria.bitacora_controlados ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS tenant_isolation_medicamentos ON veterinaria.medicamentos; +CREATE POLICY tenant_isolation_medicamentos ON veterinaria.medicamentos + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_lotes ON veterinaria.medicamentos_lotes; +CREATE POLICY tenant_isolation_lotes ON veterinaria.medicamentos_lotes + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_dispensaciones ON veterinaria.dispensaciones; +CREATE POLICY tenant_isolation_dispensaciones ON veterinaria.dispensaciones + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_movimientos ON veterinaria.movimientos_farmacia; +CREATE POLICY tenant_isolation_movimientos ON veterinaria.movimientos_farmacia + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_bitacora ON veterinaria.bitacora_controlados; +CREATE POLICY tenant_isolation_bitacora ON veterinaria.bitacora_controlados + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- ============================================================================ +-- TRIGGERS +-- ============================================================================ + +-- Trigger para actualizar stock_actual en medicamentos +CREATE OR REPLACE FUNCTION veterinaria.actualizar_stock_medicamento() +RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + -- Actualizar stock del medicamento + UPDATE veterinaria.medicamentos + SET stock_actual = stock_actual + NEW.cantidad_inicial, + updated_at = NOW() + WHERE id = NEW.medicamento_id; + ELSIF TG_OP = 'UPDATE' THEN + -- Si cambia cantidad_actual + UPDATE veterinaria.medicamentos + SET stock_actual = ( + SELECT COALESCE(SUM(cantidad_actual), 0) + FROM veterinaria.medicamentos_lotes + WHERE medicamento_id = NEW.medicamento_id + AND bloqueado = false + ), + updated_at = NOW() + WHERE id = NEW.medicamento_id; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_actualizar_stock ON veterinaria.medicamentos_lotes; +CREATE TRIGGER trg_actualizar_stock + AFTER INSERT OR UPDATE OF cantidad_actual ON veterinaria.medicamentos_lotes + FOR EACH ROW + EXECUTE FUNCTION veterinaria.actualizar_stock_medicamento(); + +COMMENT ON FUNCTION veterinaria.actualizar_stock_medicamento() IS 'Actualiza stock_actual en medicamentos cuando cambian los lotes'; + +-- Trigger para registrar movimiento en dispensacion +CREATE OR REPLACE FUNCTION veterinaria.registrar_movimiento_dispensacion() +RETURNS TRIGGER AS $$ +DECLARE + v_stock_anterior INTEGER; +BEGIN + -- Obtener stock anterior + SELECT stock_actual INTO v_stock_anterior + FROM veterinaria.medicamentos + WHERE id = NEW.medicamento_id; + + -- Descontar del lote + UPDATE veterinaria.medicamentos_lotes + SET cantidad_actual = cantidad_actual - NEW.cantidad, + updated_at = NOW() + WHERE id = NEW.lote_id; + + -- Registrar movimiento + INSERT INTO veterinaria.movimientos_farmacia ( + tenant_id, medicamento_id, lote_id, + tipo, cantidad, stock_anterior, stock_posterior, + referencia_tipo, referencia_id, usuario_id + ) VALUES ( + NEW.tenant_id, NEW.medicamento_id, NEW.lote_id, + 'salida', NEW.cantidad, v_stock_anterior, v_stock_anterior - NEW.cantidad, + 'dispensacion', NEW.id, NEW.dispensado_por + ); + + -- Si es controlado, registrar en bitacora + IF EXISTS ( + SELECT 1 FROM veterinaria.medicamentos + WHERE id = NEW.medicamento_id AND controlado = true + ) THEN + INSERT INTO veterinaria.bitacora_controlados ( + tenant_id, medicamento_id, lote_id, dispensacion_id, + tipo_movimiento, cantidad, mascota_id, + propietario_nombre, receta_id, veterinario_id, + justificacion, registrado_por + ) + SELECT + NEW.tenant_id, NEW.medicamento_id, NEW.lote_id, NEW.id, + 'salida', NEW.cantidad, NEW.mascota_id, + CONCAT(p.nombre, ' ', COALESCE(p.apellidos, '')), + NEW.receta_id, NEW.veterinario_id, + COALESCE(NEW.notas, 'Dispensacion de medicamento'), + NEW.dispensado_por + FROM veterinaria.mascotas m + JOIN veterinaria.propietarios p ON m.propietario_id = p.id + WHERE m.id = NEW.mascota_id; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_registrar_dispensacion ON veterinaria.dispensaciones; +CREATE TRIGGER trg_registrar_dispensacion + AFTER INSERT ON veterinaria.dispensaciones + FOR EACH ROW + EXECUTE FUNCTION veterinaria.registrar_movimiento_dispensacion(); + +COMMENT ON FUNCTION veterinaria.registrar_movimiento_dispensacion() IS 'Registra movimiento y bitacora al dispensar medicamentos'; + +-- ============================================================================ +-- FUNCIONES DE CONSULTA +-- ============================================================================ + +-- Funcion para obtener lotes proximos a caducar +CREATE OR REPLACE FUNCTION veterinaria.get_lotes_proximos_caducar( + p_tenant_id UUID, + p_dias INTEGER DEFAULT 30 +) +RETURNS TABLE ( + lote_id UUID, + medicamento_id UUID, + medicamento_nombre VARCHAR, + numero_lote VARCHAR, + fecha_caducidad DATE, + dias_restantes INTEGER, + cantidad_actual INTEGER +) AS $$ +BEGIN + RETURN QUERY + SELECT + l.id AS lote_id, + m.id AS medicamento_id, + m.nombre AS medicamento_nombre, + l.numero_lote, + l.fecha_caducidad, + (l.fecha_caducidad - CURRENT_DATE)::INTEGER AS dias_restantes, + l.cantidad_actual + FROM veterinaria.medicamentos_lotes l + JOIN veterinaria.medicamentos m ON l.medicamento_id = m.id + WHERE l.tenant_id = p_tenant_id + AND l.cantidad_actual > 0 + AND l.bloqueado = false + AND l.fecha_caducidad <= CURRENT_DATE + p_dias + ORDER BY l.fecha_caducidad ASC; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION veterinaria.get_lotes_proximos_caducar(UUID, INTEGER) IS 'Obtiene lotes que caducaran en los proximos N dias'; + +-- Funcion para obtener medicamentos con stock bajo +CREATE OR REPLACE FUNCTION veterinaria.get_medicamentos_stock_bajo(p_tenant_id UUID) +RETURNS TABLE ( + medicamento_id UUID, + codigo VARCHAR, + nombre VARCHAR, + stock_actual INTEGER, + stock_minimo INTEGER, + diferencia INTEGER +) AS $$ +BEGIN + RETURN QUERY + SELECT + m.id AS medicamento_id, + m.codigo, + m.nombre, + m.stock_actual, + m.stock_minimo, + (m.stock_minimo - m.stock_actual) AS diferencia + FROM veterinaria.medicamentos m + WHERE m.tenant_id = p_tenant_id + AND m.active = true + AND m.stock_actual <= m.stock_minimo + ORDER BY diferencia DESC; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION veterinaria.get_medicamentos_stock_bajo(UUID) IS 'Obtiene medicamentos con stock igual o menor al minimo'; + +-- Funcion para seleccionar lote FEFO (First Expired, First Out) +CREATE OR REPLACE FUNCTION veterinaria.seleccionar_lote_fefo( + p_medicamento_id UUID, + p_cantidad INTEGER +) +RETURNS UUID AS $$ +DECLARE + v_lote_id UUID; +BEGIN + SELECT id INTO v_lote_id + FROM veterinaria.medicamentos_lotes + WHERE medicamento_id = p_medicamento_id + AND cantidad_actual >= p_cantidad + AND bloqueado = false + AND fecha_caducidad > CURRENT_DATE + ORDER BY fecha_caducidad ASC + LIMIT 1; + + IF v_lote_id IS NULL THEN + RAISE EXCEPTION 'No hay lotes disponibles con stock suficiente para el medicamento %', p_medicamento_id; + END IF; + + RETURN v_lote_id; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION veterinaria.seleccionar_lote_fefo(UUID, INTEGER) IS 'Selecciona el lote con fecha de caducidad mas proxima (FEFO)'; + +-- ============================================================================ +-- FIN VETERINARIA FARMACIA SCHEMA +-- ============================================================================ diff --git a/seeds/fase8/01-veterinaria-catalogos.sql b/seeds/fase8/01-veterinaria-catalogos.sql new file mode 100644 index 0000000..d0fff84 --- /dev/null +++ b/seeds/fase8/01-veterinaria-catalogos.sql @@ -0,0 +1,146 @@ +-- ============================================================================ +-- SEED DATA: Catálogos de Veterinaria +-- Especialización de ERP-Clínicas +-- ============================================================================ +-- NOTA: Ejecutar después de SET app.current_tenant_id = 'UUID-DEL-TENANT'; +-- ============================================================================ + +-- Especies +INSERT INTO veterinaria.especies (tenant_id, nombre, nombre_cientifico) +SELECT current_setting('app.current_tenant_id', true)::UUID, nombre, nombre_cientifico +FROM (VALUES + ('Perro', 'Canis lupus familiaris'), + ('Gato', 'Felis silvestris catus'), + ('Ave', NULL), + ('Reptil', NULL), + ('Roedor', NULL), + ('Conejo', 'Oryctolagus cuniculus'), + ('Pez', NULL), + ('Hurón', 'Mustela putorius furo'), + ('Otro', NULL) +) AS t(nombre, nombre_cientifico) +WHERE current_setting('app.current_tenant_id', true) IS NOT NULL + AND current_setting('app.current_tenant_id', true) != '' +ON CONFLICT DO NOTHING; + +-- Razas de perro +INSERT INTO veterinaria.razas (tenant_id, especie_id, nombre, tamanio_promedio, peso_promedio_kg) +SELECT + current_setting('app.current_tenant_id', true)::UUID, + e.id, + r.nombre, + r.tamanio, + r.peso +FROM veterinaria.especies e +CROSS JOIN (VALUES + ('Mestizo', 'mediano', 15.0), + ('Chihuahua', 'pequeño', 2.5), + ('Poodle', 'pequeño', 5.0), + ('Bulldog Francés', 'pequeño', 12.0), + ('Beagle', 'mediano', 12.0), + ('Labrador Retriever', 'grande', 30.0), + ('Golden Retriever', 'grande', 32.0), + ('Pastor Alemán', 'grande', 35.0), + ('Rottweiler', 'grande', 45.0), + ('Husky Siberiano', 'grande', 25.0), + ('Pug', 'pequeño', 8.0), + ('Yorkshire Terrier', 'pequeño', 3.0), + ('Schnauzer', 'mediano', 7.0), + ('Boxer', 'grande', 30.0), + ('Pitbull', 'mediano', 25.0) +) AS r(nombre, tamanio, peso) +WHERE e.nombre = 'Perro' + AND e.tenant_id = current_setting('app.current_tenant_id', true)::UUID +ON CONFLICT DO NOTHING; + +-- Razas de gato +INSERT INTO veterinaria.razas (tenant_id, especie_id, nombre, tamanio_promedio, peso_promedio_kg) +SELECT + current_setting('app.current_tenant_id', true)::UUID, + e.id, + r.nombre, + r.tamanio, + r.peso +FROM veterinaria.especies e +CROSS JOIN (VALUES + ('Mestizo', 'mediano', 4.0), + ('Siamés', 'mediano', 4.5), + ('Persa', 'mediano', 5.0), + ('Maine Coon', 'grande', 8.0), + ('Bengalí', 'mediano', 5.5), + ('Ragdoll', 'grande', 7.0), + ('British Shorthair', 'mediano', 6.0), + ('Angora', 'mediano', 4.5), + ('Sphynx', 'mediano', 4.0), + ('Abisinio', 'mediano', 4.0) +) AS r(nombre, tamanio, peso) +WHERE e.nombre = 'Gato' + AND e.tenant_id = current_setting('app.current_tenant_id', true)::UUID +ON CONFLICT DO NOTHING; + +-- Vacunas para perros +INSERT INTO veterinaria.vacunas (tenant_id, especie_id, nombre, descripcion, intervalo_refuerzo_dias, es_obligatoria) +SELECT + current_setting('app.current_tenant_id', true)::UUID, + e.id, + v.nombre, + v.descripcion, + v.intervalo, + v.obligatoria +FROM veterinaria.especies e +CROSS JOIN (VALUES + ('Parvovirus', 'Protege contra parvovirus canino', 365, false), + ('Moquillo', 'Protege contra distemper canino', 365, false), + ('Hepatitis', 'Protege contra hepatitis infecciosa canina', 365, false), + ('Rabia', 'Vacuna antirrábica - OBLIGATORIA', 365, true), + ('Leptospirosis', 'Protege contra leptospirosis', 365, false), + ('Bordetella', 'Protege contra tos de las perreras', 180, false), + ('Cuádruple', 'Moquillo, Hepatitis, Parvo, Parainfluenza', 365, false), + ('Séxtuple', 'Cuádruple + Coronavirus + Leptospira', 365, false) +) AS v(nombre, descripcion, intervalo, obligatoria) +WHERE e.nombre = 'Perro' + AND e.tenant_id = current_setting('app.current_tenant_id', true)::UUID +ON CONFLICT DO NOTHING; + +-- Vacunas para gatos +INSERT INTO veterinaria.vacunas (tenant_id, especie_id, nombre, descripcion, intervalo_refuerzo_dias, es_obligatoria) +SELECT + current_setting('app.current_tenant_id', true)::UUID, + e.id, + v.nombre, + v.descripcion, + v.intervalo, + v.obligatoria +FROM veterinaria.especies e +CROSS JOIN (VALUES + ('Triple Felina', 'Rinotraqueitis, Calicivirus, Panleucopenia', 365, false), + ('Leucemia Felina', 'Protege contra FeLV', 365, false), + ('Rabia', 'Vacuna antirrábica', 365, true), + ('PIF', 'Peritonitis Infecciosa Felina', 365, false) +) AS v(nombre, descripcion, intervalo, obligatoria) +WHERE e.nombre = 'Gato' + AND e.tenant_id = current_setting('app.current_tenant_id', true)::UUID +ON CONFLICT DO NOTHING; + +-- Skills específicos veterinarios +INSERT INTO hr.skills (tenant_id, skill_type_id, name, requiere_cedula) +SELECT + current_setting('app.current_tenant_id', true)::UUID, + st.id, + unnest(ARRAY[ + 'Medicina Veterinaria General', + 'Cirugía Veterinaria', + 'Dermatología Veterinaria', + 'Cardiología Veterinaria', + 'Oftalmología Veterinaria', + 'Ortopedia Veterinaria', + 'Oncología Veterinaria', + 'Medicina de Exóticos', + 'Anestesiología Veterinaria', + 'Imagenología Veterinaria' + ]), + true +FROM hr.skill_types st +WHERE st.name = 'Especialidad Médica' + AND st.tenant_id = current_setting('app.current_tenant_id', true)::UUID +ON CONFLICT DO NOTHING;