codigo0/docs/CASOS_BORDE.md
planetazuzu 5d7a6500fe refactor: Fase 1 - Clean Architecture, refactorización modular y eliminación de duplicidades
-  Ticket 1.1: Estructura Clean Architecture en backend
-  Ticket 1.2: Schemas Zod compartidos
-  Ticket 1.3: Refactorización drugs.ts (1362 → 8 archivos modulares)
-  Ticket 1.4: Refactorización procedures.ts (3583 → 6 archivos modulares)
-  Ticket 1.5: Eliminación de duplicidades (~50 líneas)

Cambios principales:
- Creada estructura Clean Architecture en backend/src/
- Schemas Zod compartidos en backend/src/shared/schemas/
- Refactorización modular de drugs y procedures
- Utilidades genéricas en src/utils/ (filter, validation)
- Eliminados scripts obsoletos y documentación antigua
- Corregidos errores: QueryClient, import test-error-handling
- Build verificado y funcionando correctamente
2026-01-25 21:09:47 +01:00

10 KiB

🔍 Casos de Borde - Análisis Uno por Uno

🎯 Objetivo

Estudiar caso por caso todos los edge cases posibles en la aplicación médica.


1. Dosis Pediátricas

Caso 1.1: Peso = 0 o negativo

Problema: División por cero o cálculo inválido

Solución:

class PatientWeight {
  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');
  }
}

Validación: Bloquear cálculo si peso <= 0


Caso 1.2: Peso fuera de rango para edad

Problema: Niño de 5 años con peso de 100kg (imposible)

Solución:

isValidForAge(age: PatientAge): { valid: boolean; warning?: string } {
  const range = age.getWeightRange();
  const weightKg = this.toKg();
  
  if (weightKg < range.min * 0.5) {
    return {
      valid: false,
      warning: `Peso extremadamente bajo. Verificar entrada.`
    };
  }
  
  if (weightKg > range.max * 2) {
    return {
      valid: false,
      warning: `Peso extremadamente alto. Verificar entrada.`
    };
  }
  
  return { valid: true };
}

Validación: Bloquear si peso < 50% del mínimo O > 200% del máximo


Caso 1.3: Dosis pediátrica no definida

Problema: Fármaco sin dosis pediátrica pero se intenta calcular

Solución:

async calculatePediatricDose(...): Promise<DoseCalculation> {
  if (!drug.pediatricDose) {
    throw new Error(
      `No hay dosis pediátrica definida para ${drug.genericName}. ` +
      `Usar dosis adulto con precaución o consultar referencia.`
    );
  }
  // ...
}

Validación: Bloquear cálculo, mostrar advertencia


Caso 1.4: Cálculo resulta en dosis < 0.01mg

Problema: Dosis muy pequeña, difícil de medir

Solución:

if (calculatedDose < 0.01) {
  warnings.push(
    `Dosis calculada (${calculatedDose}mg) es muy pequeña para medir con precisión. ` +
    `Considerar dilución o consultar referencia especializada.`
  );
}

Validación: Advertencia, no bloquea


2. Protocolos

Caso 2.1: Protocolo sin pasos

Problema: Protocolo creado sin pasos definidos

Solución:

static create(...): Protocol {
  if (!steps || steps.length === 0) {
    throw new Error('Protocolo debe tener al menos un paso');
  }
  // ...
}

Validación: Bloquear creación/validación


Caso 2.2: Pasos con números duplicados

Problema: Dos pasos con order = 3

Solución:

static create(steps: ProtocolStep[]): ProtocolSequence {
  const orders = steps.map(s => s.order);
  const uniqueOrders = new Set(orders);
  if (orders.length !== uniqueOrders.size) {
    throw new Error('Hay pasos con el mismo número de orden');
  }
  // ...
}

Validación: Bloquear creación


Caso 2.3: Pasos faltantes en secuencia

Problema: Pasos 1, 2, 4, 5 (falta el 3)

Solución:

for (let i = 0; i < sortedSteps.length; i++) {
  if (sortedSteps[i].order !== i + 1) {
    throw new Error(
      `Paso faltante en secuencia: esperado ${i + 1}, encontrado ${sortedSteps[i].order}`
    );
  }
}

Validación: Bloquear creación


Caso 2.4: Protocolo prerequisito no existe

Problema: Protocolo A requiere Protocolo B que no existe

Solución:

async validateProtocolDependencies(...): Promise<DependencyValidationResult> {
  for (const prereqId of protocol.prerequisites) {
    const prereq = await this.protocolRepository.findById(prereqId);
    if (!prereq) {
      errors.push(`Protocolo prerequisito "${prereqId}" no existe`);
    }
  }
  // ...
}

Validación: Bloquear si prerequisito no existe


Caso 2.5: Dependencia circular

Problema: Protocolo A requiere B, B requiere A

Solución:

async detectCircularDependency(
  protocolId: string,
  targetId: string,
  visited: Set<string> = new Set()
): Promise<boolean> {
  if (visited.has(protocolId)) {
    return protocolId === targetId; // Circular
  }
  
  visited.add(protocolId);
  const protocol = await this.protocolRepository.findById(protocolId);
  
  for (const prereqId of protocol.prerequisites) {
    if (await this.detectCircularDependency(prereqId, targetId, visited)) {
      return true;
    }
  }
  
  return false;
}

Validación: Bloquear creación de dependencia circular


3. Contenido Médico

Caso 3.1: Contenido sin título

Problema: Crear contenido con título vacío

Solución:

static create(...): ContentItem {
  if (!title || title.trim().length === 0) {
    throw new Error('Título es obligatorio');
  }
  // ...
}

Validación: Bloquear creación


Caso 3.2: Intentar publicar sin validación médica

Problema: Publicar contenido que no ha sido revisado

Solución:

async publishContent(...): Promise<void> {
  if (content.status !== 'approved') {
    throw new Error('Solo contenido aprobado puede publicarse');
  }
  
  if (!content.validatedBy) {
    throw new Error('Contenido debe estar validado por un médico');
  }
  // ...
}

Validación: Bloquear publicación


Caso 3.3: Contenido rechazado intenta publicarse

Problema: Publicar contenido que fue rechazado

Solución:

const rejectedReviews = reviews.filter(r => r.status === 'rejected');
if (rejectedReviews.length > 0 && content.status === 'draft') {
  // Contenido fue rechazado y está en borrador
  // Permitir edición pero no publicación directa
}

Validación: Bloquear publicación hasta nueva revisión


Caso 3.4: Slug duplicado

Problema: Dos contenidos con el mismo slug

Solución:

async save(content: ContentItem): Promise<ContentItem> {
  const existing = await this.findBySlug(content.slug);
  if (existing && existing.id !== content.id) {
    throw new Error(`Slug "${content.slug}" ya existe`);
  }
  // ...
}

Validación: Bloquear creación/actualización


4. Búsqueda

Caso 4.1: Búsqueda vacía

Problema: Query string vacío

Solución:

if (!search || search.trim().length === 0) {
  return { items: [], total: 0 };
}

Validación: Retornar resultados vacíos, no error


Caso 4.2: Búsqueda sin resultados

Problema: No hay resultados para la búsqueda

Solución:

if (results.length === 0) {
  return {
    items: [],
    total: 0,
    suggestions: await this.getSearchSuggestions(search)
  };
}

Validación: Retornar sugerencias, no error


Caso 4.3: Búsqueda con caracteres especiales

Problema: SQL injection o caracteres problemáticos

Solución:

const sanitized = search.trim().replace(/[%_]/g, '\\$&');
// Usar parámetros preparados siempre

Validación: Sanitizar entrada, usar parámetros preparados


5. Offline

Caso 5.1: Contenido crítico no en cache

Problema: Usuario offline, contenido crítico no disponible

Solución:

async getContent(id: string): Promise<ContentItem | null> {
  // Intentar cache primero
  const cached = await cache.get(`content:${id}`);
  if (cached) return cached;
  
  // Si offline y crítico, retornar versión mínima
  if (isOffline() && isCritical(id)) {
    return getMinimalCriticalContent(id);
  }
  
  return null;
}

Validación: Retornar versión mínima si crítico, error si no crítico


Caso 5.2: Cache corrupto

Problema: Datos en cache corruptos o incompletos

Solución:

try {
  const cached = JSON.parse(cacheData);
  if (!isValidContent(cached)) {
    throw new Error('Cache corrupto');
  }
  return cached;
} catch (error) {
  // Limpiar cache y recargar
  await cache.delete(key);
  return await loadFromSource(id);
}

Validación: Validar integridad, limpiar si corrupto


6. Validación Médica

Caso 6.1: Revisor intenta aprobar su propio contenido

Problema: Conflicto de intereses

Solución:

async approveContent(...): Promise<void> {
  if (content.createdBy === reviewerId) {
    throw new Error('No puedes aprobar tu propio contenido');
  }
  // ...
}

Validación: Bloquear auto-aprobación


Caso 6.2: Múltiples revisores aprueban simultáneamente

Problema: Race condition

Solución:

// Usar transacción con lock
await db.transaction(async (tx) => {
  const content = await tx.query(
    'SELECT * FROM content_items WHERE id = $1 FOR UPDATE',
    [contentId]
  );
  
  if (content.status !== 'in_review') {
    throw new Error('Estado cambió durante la revisión');
  }
  
  // Actualizar...
});

Validación: Usar locks de base de datos


7. Medios Audiovisuales

Caso 7.1: Archivo demasiado grande

Problema: Upload de archivo > límite

Solución:

const MAX_SIZE = 50 * 1024 * 1024; // 50MB

if (file.size > MAX_SIZE) {
  throw new Error(`Archivo excede tamaño máximo (${MAX_SIZE / 1024 / 1024}MB)`);
}

Validación: Rechazar upload


Caso 7.2: Tipo de archivo no permitido

Problema: Upload de archivo .exe o .bat

Solución:

const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'video/mp4', ...];

if (!ALLOWED_TYPES.includes(file.mimetype)) {
  throw new Error(`Tipo de archivo no permitido: ${file.mimetype}`);
}

Validación: Rechazar upload


Caso 7.3: Archivo corrupto o inválido

Problema: Imagen corrupta que no se puede procesar

Solución:

try {
  const image = await sharp(file.buffer).metadata();
  if (!image.width || !image.height) {
    throw new Error('Imagen inválida');
  }
} catch (error) {
  throw new Error('Archivo corrupto o inválido');
}

Validación: Validar integridad antes de guardar


📝 Resumen de Validaciones

Caso Severidad Acción
Peso <= 0 Crítico Bloquear
Dosis fuera de rango Crítico Bloquear
Paso crítico omitido Crítico Bloquear
Contenido sin validación Crítico Bloquear publicación
Peso fuera de percentiles Advertencia Mostrar, permitir con confirmación
Dosis en límites Advertencia Mostrar, permitir
Búsqueda sin resultados Info Mostrar sugerencias

Fin del documento