# 💊 Sistema de Validación de Dosis ## 🎯 Objetivo Crear un sistema robusto para validar dosis médicas con rangos máximos/mínimos por edad/peso y validación de cálculos pediátricos. --- ## 📋 Reglas de Negocio ### 1. Rangos de Dosis por Edad/Peso #### Grupos de Edad - **Neonatal:** 0-28 días - **Lactante:** 29 días - 12 meses - **Pediátrico:** 1-12 años - **Adolescente:** 13-17 años - **Adulto:** ≥18 años #### Cálculo Pediátrico - **Por peso:** `dosis = peso_kg × dosis_por_kg` - **Por superficie corporal:** `dosis = SCA × dosis_por_m2` (SCA = Superficie Corporal) - **Por edad:** Tablas de dosis según edad ### 2. Validaciones Críticas #### ✅ Validaciones Obligatorias 1. **Peso mínimo:** No permitir dosis si peso < límite mínimo 2. **Peso máximo:** Alertar si peso excede límite máximo (posible error) 3. **Dosis mínima:** No permitir dosis por debajo del mínimo terapéutico 4. **Dosis máxima:** Bloquear dosis que excedan el máximo seguro 5. **Concentración:** Validar que la concentración del fármaco es correcta 6. **Vía de administración:** Validar que la vía es apropiada para la edad #### ⚠️ Alertas (No bloquean, pero advierten) 1. Dosis en límite superior del rango 2. Dosis en límite inferior del rango 3. Peso fuera de percentiles normales 4. Dosis acumulada diaria alta --- ## 🏗️ Arquitectura ### Domain Layer ```typescript // domain/entities/DoseCalculation.ts export interface DoseCalculation { readonly drugId: string; readonly patientWeight: number; // kg readonly patientAge: number; // años readonly patientAgeMonths?: number; // meses (para <2 años) readonly calculatedDose: number; // mg o ml readonly unit: 'mg' | 'ml' | 'mcg' | 'UI'; readonly route: AdministrationRoute; readonly method: 'weight' | 'bsa' | 'age'; // Método de cálculo } // domain/value-objects/DoseRange.ts export class DoseRange { private constructor( readonly min: number, readonly max: number, readonly unit: string, readonly ageGroup: AgeGroup ) {} static create( min: number, max: number, unit: string, ageGroup: AgeGroup ): DoseRange { if (min < 0) throw new Error('Dosis mínima no puede ser negativa'); if (max < min) throw new Error('Dosis máxima debe ser mayor que mínima'); return new DoseRange(min, max, unit, ageGroup); } isValid(dose: number): boolean { return dose >= this.min && dose <= this.max; } getWarningLevel(dose: number): 'safe' | 'low' | 'high' | 'critical' { const range = this.max - this.min; const lowerThreshold = this.min + (range * 0.1); // 10% del rango const upperThreshold = this.max - (range * 0.1); if (dose < this.min || dose > this.max) return 'critical'; if (dose < lowerThreshold) return 'low'; if (dose > upperThreshold) return 'high'; return 'safe'; } } // domain/value-objects/PatientAge.ts export class PatientAge { private constructor( readonly years: number, readonly months?: number, readonly days?: number ) {} static fromYears(years: number): PatientAge { if (years < 0) throw new Error('Edad no puede ser negativa'); return new PatientAge(years); } static fromMonths(months: number): PatientAge { if (months < 0) throw new Error('Edad en meses no puede ser negativa'); const years = Math.floor(months / 12); const remainingMonths = months % 12; return new PatientAge(years, remainingMonths); } static fromDays(days: number): PatientAge { if (days < 0) throw new Error('Edad en días no puede ser negativa'); const months = Math.floor(days / 30); const years = Math.floor(months / 12); const remainingMonths = months % 12; const remainingDays = days % 30; return new PatientAge(years, remainingMonths, remainingDays); } getAgeGroup(): AgeGroup { if (this.days !== undefined && this.days <= 28) return 'neonatal'; if (this.months !== undefined && this.months < 12) return 'lactante'; if (this.years < 1) return 'lactante'; if (this.years < 13) return 'pediatrico'; if (this.years < 18) return 'adolescente'; return 'adulto'; } getWeightRange(): { min: number; max: number } { const ageGroup = this.getAgeGroup(); // Rangos de peso normales por grupo de edad (percentiles 5-95) const ranges: Record = { neonatal: { min: 2.0, max: 4.5 }, lactante: { min: 3.0, max: 12.0 }, pediatrico: { min: 10.0, max: 50.0 }, adolescente: { min: 40.0, max: 80.0 }, adulto: { min: 45.0, max: 120.0 }, todos: { min: 2.0, max: 120.0 } }; return ranges[ageGroup]; } } // domain/value-objects/PatientWeight.ts export class PatientWeight { private constructor(readonly value: number, readonly unit: 'kg' | 'g') {} static fromKg(kg: number): PatientWeight { if (kg <= 0) throw new Error('Peso debe ser mayor que cero'); if (kg > 200) throw new Error('Peso excesivo, verificar entrada'); return new PatientWeight(kg, 'kg'); } static fromGrams(grams: number): PatientWeight { if (grams <= 0) throw new Error('Peso debe ser mayor que cero'); return new PatientWeight(grams / 1000, 'kg'); } toKg(): number { return this.unit === 'kg' ? this.value : this.value / 1000; } isValidForAge(age: PatientAge): { valid: boolean; warning?: string } { const range = age.getWeightRange(); const weightKg = this.toKg(); if (weightKg < range.min) { return { valid: false, warning: `Peso (${weightKg}kg) está por debajo del rango normal para esta edad (${range.min}-${range.max}kg). Verificar entrada.` }; } if (weightKg > range.max) { return { valid: true, warning: `Peso (${weightKg}kg) está por encima del rango normal para esta edad (${range.min}-${range.max}kg). Verificar entrada.` }; } return { valid: true }; } } ``` ### Application Layer ```typescript // application/services/DoseValidationService.ts export class DoseValidationService { constructor( private readonly drugRepository: IDrugRepository, private readonly doseRangeRepository: IDoseRangeRepository ) {} async validateDose( drugId: string, dose: number, patientWeight: PatientWeight, patientAge: PatientAge, route: AdministrationRoute ): Promise { // 1. Obtener fármaco const drug = await this.drugRepository.findById(drugId); if (!drug) { throw new Error(`Fármaco ${drugId} no encontrado`); } // 2. Validar peso para edad const weightValidation = patientWeight.isValidForAge(patientAge); if (!weightValidation.valid) { return { valid: false, errors: [weightValidation.warning!], warnings: [], critical: true }; } // 3. Obtener rango de dosis para grupo de edad const ageGroup = patientAge.getAgeGroup(); const doseRange = await this.doseRangeRepository.findByDrugAndAgeGroup( drugId, ageGroup ); if (!doseRange) { return { valid: false, errors: [`No hay rango de dosis definido para ${drug.genericName} en grupo de edad ${ageGroup}`], warnings: [], critical: true }; } // 4. Validar dosis contra rango const isValid = doseRange.isValid(dose); const warningLevel = doseRange.getWarningLevel(dose); const errors: string[] = []; const warnings: string[] = []; if (!isValid) { errors.push( `Dosis ${dose}${doseRange.unit} está fuera del rango permitido ` + `(${doseRange.min}-${doseRange.max}${doseRange.unit}) para ${ageGroup}` ); } if (warningLevel === 'low') { warnings.push(`Dosis está en el límite inferior del rango. Verificar necesidad.`); } if (warningLevel === 'high') { warnings.push(`Dosis está en el límite superior del rango. Monitorizar efectos.`); } // 5. Validar vía de administración if (!drug.routes.includes(route)) { errors.push( `Vía ${route} no está indicada para ${drug.genericName}. ` + `Vías permitidas: ${drug.routes.join(', ')}` ); } // 6. Validar contraindicaciones por edad const ageContraindications = this.checkAgeContraindications( drug, patientAge ); if (ageContraindications.length > 0) { errors.push(...ageContraindications); } return { valid: errors.length === 0, errors, warnings, critical: errors.length > 0, calculatedDose: dose, recommendedRange: { min: doseRange.min, max: doseRange.max, unit: doseRange.unit } }; } async calculatePediatricDose( drugId: string, patientWeight: PatientWeight, patientAge: PatientAge, method: 'weight' | 'bsa' = 'weight' ): Promise { const drug = await this.drugRepository.findById(drugId); if (!drug) { throw new Error(`Fármaco ${drugId} no encontrado`); } if (!drug.pediatricDose) { throw new Error(`No hay dosis pediátrica definida para ${drug.genericName}`); } // Parsear dosis pediátrica (ej: "0.01 mg/kg" o "10 mg/m²") const dosePerKg = this.parseDosePerKg(drug.pediatricDose); if (!dosePerKg) { throw new Error(`Formato de dosis pediátrica inválido: ${drug.pediatricDose}`); } const weightKg = patientWeight.toKg(); let calculatedDose: number; if (method === 'weight') { calculatedDose = weightKg * dosePerKg.value; } else { // BSA (Body Surface Area) usando fórmula de Mosteller const heightCm = this.estimateHeight(patientAge); const bsa = Math.sqrt((weightKg * heightCm) / 3600); calculatedDose = bsa * dosePerKg.value; } return { drugId, patientWeight: weightKg, patientAge: patientAge.years, patientAgeMonths: patientAge.months, calculatedDose: Math.round(calculatedDose * 100) / 100, // Redondear a 2 decimales unit: dosePerKg.unit, route: drug.routes[0], // Primera vía permitida method }; } private parseDosePerKg(doseString: string): { value: number; unit: string } | null { // Parsear formatos como "0.01 mg/kg" o "10 mg/m²" const match = doseString.match(/(\d+\.?\d*)\s*(mg|mcg|ml|UI|g)\/(kg|m²)/); if (!match) return null; return { value: parseFloat(match[1]), unit: match[2] }; } private estimateHeight(age: PatientAge): number { // Estimación de altura según edad (fórmulas aproximadas) if (age.years < 1) { return 50 + (age.months || 0) * 2.5; // cm } if (age.years < 2) { return 75 + (age.years - 1) * 10; } // Fórmula de altura para niños: altura = edad × 6 + 77 return age.years * 6 + 77; } private checkAgeContraindications( drug: Drug, age: PatientAge ): string[] { const errors: string[] = []; const ageGroup = age.getAgeGroup(); // Verificar contraindicaciones específicas por edad if (drug.contraindications.some(c => c.includes('neonatal')) && ageGroup === 'neonatal') { errors.push(`Fármaco contraindicado en neonatos`); } if (drug.contraindications.some(c => c.includes('lactante')) && ageGroup === 'lactante') { errors.push(`Fármaco contraindicado en lactantes`); } return errors; } } export interface DoseValidationResult { valid: boolean; errors: string[]; warnings: string[]; critical: boolean; calculatedDose?: number; recommendedRange?: { min: number; max: number; unit: string; }; } ``` ### Infrastructure Layer ```typescript // infrastructure/repositories/DoseRangeRepository.ts export class DoseRangeRepository implements IDoseRangeRepository { constructor(private readonly db: Database) {} async findByDrugAndAgeGroup( drugId: string, ageGroup: AgeGroup ): Promise { const result = await this.db.query( `SELECT min_dose, max_dose, unit, age_group FROM tes_content.dose_ranges WHERE drug_id = $1 AND age_group = $2`, [drugId, ageGroup] ); if (result.rows.length === 0) return null; const row = result.rows[0]; return DoseRange.create( row.min_dose, row.max_dose, row.unit, row.age_group ); } } ``` --- ## 🗄️ Esquema de Base de Datos ```sql -- Tabla de rangos de dosis CREATE TABLE tes_content.dose_ranges ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), drug_id UUID NOT NULL REFERENCES tes_content.drugs(id), age_group tes_content.age_group NOT NULL, min_dose DECIMAL(10, 3) NOT NULL, max_dose DECIMAL(10, 3) NOT NULL, unit VARCHAR(20) NOT NULL, method VARCHAR(20) NOT NULL DEFAULT 'weight', -- 'weight', 'bsa', 'age' source VARCHAR(200), notes TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_min_max CHECK (max_dose > min_dose), CONSTRAINT chk_min_positive CHECK (min_dose > 0), CONSTRAINT unique_drug_age UNIQUE (drug_id, age_group) ); CREATE INDEX idx_dose_ranges_drug_id ON tes_content.dose_ranges(drug_id); CREATE INDEX idx_dose_ranges_age_group ON tes_content.dose_ranges(age_group); ``` --- ## ✅ Casos de Uso ### Caso 1: Validar dosis antes de administrar ```typescript const validation = await doseValidationService.validateDose( 'adrenalina', 0.5, // mg PatientWeight.fromKg(70), PatientAge.fromYears(35), 'IM' ); if (!validation.valid) { // Bloquear administración throw new Error(`Dosis inválida: ${validation.errors.join(', ')}`); } if (validation.warnings.length > 0) { // Mostrar advertencias pero permitir console.warn(validation.warnings); } ``` ### Caso 2: Calcular dosis pediátrica ```typescript const calculation = await doseValidationService.calculatePediatricDose( 'adrenalina', PatientWeight.fromKg(20), PatientAge.fromYears(5), 'weight' ); console.log(`Dosis calculada: ${calculation.calculatedDose}${calculation.unit}`); ``` --- ## 🧪 Tests ```typescript describe('DoseValidationService', () => { describe('validateDose', () => { it('should reject dose below minimum', async () => { const result = await service.validateDose( 'adrenalina', 0.1, // mg - muy bajo PatientWeight.fromKg(70), PatientAge.fromYears(35), 'IM' ); expect(result.valid).toBe(false); expect(result.errors).toContain(expect.stringContaining('mínima')); }); it('should reject dose above maximum', async () => { const result = await service.validateDose( 'adrenalina', 10, // mg - muy alto PatientWeight.fromKg(70), PatientAge.fromYears(35), 'IM' ); expect(result.valid).toBe(false); expect(result.errors).toContain(expect.stringContaining('máxima')); }); it('should warn for weight outside normal range', async () => { const result = await service.validateDose( 'adrenalina', 0.5, PatientWeight.fromKg(150), // Peso muy alto PatientAge.fromYears(10), // Niño de 10 años 'IM' ); expect(result.warnings).toContain(expect.stringContaining('peso')); }); }); }); ``` --- ## 📝 Notas Importantes 1. **Errores críticos bloquean la acción:** Dosis fuera de rango, peso inválido, vía incorrecta 2. **Advertencias no bloquean:** Dosis en límites, peso fuera de percentiles normales 3. **Validación siempre en backend:** El frontend puede pre-validar, pero el backend es la fuente de verdad 4. **Logs de todas las validaciones:** Registrar todas las validaciones para auditoría --- **Fin del documento**