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