553 lines
16 KiB
Markdown
553 lines
16 KiB
Markdown
|
|
# 💊 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**
|