codigo0/docs/SISTEMA_VALIDACION_DOSIS.md

553 lines
16 KiB
Markdown
Raw Normal View History

# 💊 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<AgeGroup, { min: number; max: number }> = {
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<DoseValidationResult> {
// 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<DoseCalculation> {
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<DoseRange | null> {
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**