487 lines
10 KiB
Markdown
487 lines
10 KiB
Markdown
|
|
# 🔍 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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
// 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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
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**
|