-- ============================================================================ -- 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 -- ============================================================================