# 📋 Sistema de Validación de Protocolos ## 🎯 Objetivo Crear un sistema para validar la secuencia de pasos de protocolos y las dependencias entre protocolos. --- ## 📋 Reglas de Negocio ### 1. Validación de Secuencia de Pasos #### Reglas de Secuencia - ✅ Los pasos deben estar numerados secuencialmente (1, 2, 3...) - ✅ No puede haber pasos faltantes en la secuencia - ✅ Los pasos deben tener un orden lógico (no se puede hacer paso 3 antes del paso 1) - ✅ Pasos críticos deben estar marcados y no pueden omitirse - ✅ Pasos opcionales deben estar claramente marcados #### Tipos de Pasos - **Obligatorio:** Debe ejecutarse siempre - **Condicional:** Se ejecuta si se cumple condición - **Opcional:** Puede omitirse según contexto - **Crítico:** Bloquea el protocolo si se omite ### 2. Dependencias entre Protocolos #### Tipos de Dependencias - **Prerequisito:** Protocolo A debe completarse antes de Protocolo B - **Siguiente:** Protocolo A normalmente lleva a Protocolo B - **Alternativo:** Protocolo A o Protocolo B (mutuamente excluyentes) - **Complementario:** Protocolo A se usa junto con Protocolo B --- ## 🏗️ Arquitectura ### Domain Layer ```typescript // domain/entities/Protocol.ts export interface Protocol { readonly id: string; readonly title: string; readonly shortTitle: string; readonly category: Category; readonly priority: Priority; readonly ageGroup: AgeGroup; readonly steps: readonly ProtocolStep[]; readonly prerequisites: readonly string[]; // IDs de protocolos prerequisito readonly nextProtocols: readonly string[]; // IDs de protocolos siguientes readonly alternatives: readonly string[]; // IDs de protocolos alternativos readonly complements: readonly string[]; // IDs de protocolos complementarios readonly criticalSteps: readonly number[]; // Índices de pasos críticos readonly status: ContentStatus; } export interface ProtocolStep { readonly order: number; // Orden en la secuencia (1, 2, 3...) readonly type: 'obligatory' | 'conditional' | 'optional' | 'critical'; readonly content: string; readonly condition?: string; // Condición para pasos condicionales readonly equipment?: readonly string[]; readonly drugs?: readonly string[]; readonly warnings?: readonly string[]; readonly estimatedTime?: number; // Segundos estimados } // domain/value-objects/ProtocolSequence.ts export class ProtocolSequence { private constructor(private readonly steps: readonly ProtocolStep[]) {} static create(steps: ProtocolStep[]): ProtocolSequence { // Validar secuencia const sortedSteps = [...steps].sort((a, b) => a.order - b.order); // Verificar que no hay huecos 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}`); } } // Verificar que no hay duplicados const orders = sortedSteps.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'); } return new ProtocolSequence(sortedSteps); } validateExecution(executedSteps: number[]): ProtocolValidationResult { const errors: string[] = []; const warnings: string[] = []; // Verificar que todos los pasos críticos están ejecutados const criticalSteps = this.steps .filter(s => s.type === 'critical') .map(s => s.order); const missingCritical = criticalSteps.filter(step => !executedSteps.includes(step)); if (missingCritical.length > 0) { errors.push( `Pasos críticos no ejecutados: ${missingCritical.join(', ')}` ); } // Verificar que todos los pasos obligatorios están ejecutados const obligatorySteps = this.steps .filter(s => s.type === 'obligatory') .map(s => s.order); const missingObligatory = obligatorySteps.filter(step => !executedSteps.includes(step)); if (missingObligatory.length > 0) { errors.push( `Pasos obligatorios no ejecutados: ${missingObligatory.join(', ')}` ); } // Verificar orden de ejecución const sortedExecuted = [...executedSteps].sort((a, b) => a - b); if (JSON.stringify(executedSteps) !== JSON.stringify(sortedExecuted)) { warnings.push('Los pasos no se ejecutaron en orden secuencial'); } // Verificar pasos condicionales for (const step of this.steps) { if (step.type === 'conditional' && executedSteps.includes(step.order)) { if (!step.condition) { warnings.push(`Paso ${step.order} es condicional pero no tiene condición definida`); } } } return { valid: errors.length === 0, errors, warnings, executedSteps, missingSteps: this.steps .map(s => s.order) .filter(order => !executedSteps.includes(order)) }; } getNextStep(currentStep?: number): ProtocolStep | null { if (currentStep === undefined) { return this.steps[0] || null; } const currentIndex = this.steps.findIndex(s => s.order === currentStep); if (currentIndex === -1) return null; return this.steps[currentIndex + 1] || null; } getStep(order: number): ProtocolStep | null { return this.steps.find(s => s.order === order) || null; } } export interface ProtocolValidationResult { valid: boolean; errors: string[]; warnings: string[]; executedSteps: number[]; missingSteps: number[]; } // domain/value-objects/ProtocolDependency.ts export class ProtocolDependency { private constructor( readonly protocolId: string, readonly dependencyType: 'prerequisite' | 'next' | 'alternative' | 'complement', readonly targetProtocolId: string, readonly required: boolean ) {} static createPrerequisite( protocolId: string, prerequisiteId: string, required: boolean = true ): ProtocolDependency { return new ProtocolDependency( protocolId, 'prerequisite', prerequisiteId, required ); } static createNext( protocolId: string, nextProtocolId: string ): ProtocolDependency { return new ProtocolDependency( protocolId, 'next', nextProtocolId, false ); } static createAlternative( protocolId: string, alternativeId: string ): ProtocolDependency { return new ProtocolDependency( protocolId, 'alternative', alternativeId, false ); } static createComplement( protocolId: string, complementId: string ): ProtocolDependency { return new ProtocolDependency( protocolId, 'complement', complementId, false ); } validate( protocolExecuted: boolean, targetProtocolExecuted: boolean ): { valid: boolean; error?: string } { switch (this.dependencyType) { case 'prerequisite': if (this.required && protocolExecuted && !targetProtocolExecuted) { return { valid: false, error: `Protocolo ${this.targetProtocolId} debe ejecutarse antes de ${this.protocolId}` }; } break; case 'alternative': if (protocolExecuted && targetProtocolExecuted) { return { valid: false, error: `Protocolos ${this.protocolId} y ${this.targetProtocolId} son mutuamente excluyentes` }; } break; } return { valid: true }; } } ``` ### Application Layer ```typescript // application/services/ProtocolValidationService.ts export class ProtocolValidationService { constructor( private readonly protocolRepository: IProtocolRepository, private readonly dependencyRepository: IDependencyRepository ) {} async validateProtocolSequence( protocolId: string, executedSteps: number[] ): Promise { const protocol = await this.protocolRepository.findById(protocolId); if (!protocol) { throw new Error(`Protocolo ${protocolId} no encontrado`); } const sequence = ProtocolSequence.create(protocol.steps); return sequence.validateExecution(executedSteps); } async validateProtocolDependencies( protocolId: string, executedProtocols: string[] ): Promise { const protocol = await this.protocolRepository.findById(protocolId); if (!protocol) { throw new Error(`Protocolo ${protocolId} no encontrado`); } const dependencies = await this.dependencyRepository.findByProtocol(protocolId); const errors: string[] = []; const warnings: string[] = []; // Validar prerequisitos for (const prereqId of protocol.prerequisites) { if (!executedProtocols.includes(prereqId)) { const prereq = await this.protocolRepository.findById(prereqId); errors.push( `Protocolo prerequisito "${prereq?.title || prereqId}" no ha sido ejecutado` ); } } // Validar alternativos for (const altId of protocol.alternatives) { if (executedProtocols.includes(altId)) { const alt = await this.protocolRepository.findById(altId); warnings.push( `Protocolo alternativo "${alt?.title || altId}" ya fue ejecutado. ` + `Estos protocolos son mutuamente excluyentes.` ); } } // Sugerir protocolos siguientes const suggestedNext: string[] = []; for (const nextId of protocol.nextProtocols) { if (!executedProtocols.includes(nextId)) { suggestedNext.push(nextId); } } return { valid: errors.length === 0, errors, warnings, suggestedNext }; } async getProtocolExecutionPath( startProtocolId: string, context: ProtocolContext ): Promise { const path: ProtocolExecutionPath = { protocols: [], totalEstimatedTime: 0, warnings: [] }; const visited = new Set(); const queue: string[] = [startProtocolId]; while (queue.length > 0) { const protocolId = queue.shift()!; if (visited.has(protocolId)) continue; visited.add(protocolId); const protocol = await this.protocolRepository.findById(protocolId); if (!protocol) continue; // Validar prerequisitos const depValidation = await this.validateProtocolDependencies( protocolId, path.protocols.map(p => p.id) ); if (!depValidation.valid) { path.warnings.push(...depValidation.errors); continue; // Saltar si faltan prerequisitos } // Calcular tiempo estimado const estimatedTime = protocol.steps.reduce( (sum, step) => sum + (step.estimatedTime || 30), 0 ); path.protocols.push({ id: protocol.id, title: protocol.title, estimatedTime }); path.totalEstimatedTime += estimatedTime; // Agregar protocolos siguientes a la cola for (const nextId of protocol.nextProtocols) { if (!visited.has(nextId) && !queue.includes(nextId)) { queue.push(nextId); } } } return path; } } export interface DependencyValidationResult { valid: boolean; errors: string[]; warnings: string[]; suggestedNext: string[]; } export interface ProtocolExecutionPath { protocols: Array<{ id: string; title: string; estimatedTime: number; }>; totalEstimatedTime: number; warnings: string[]; } export interface ProtocolContext { ageGroup: AgeGroup; priority: Priority; availableEquipment?: string[]; availableDrugs?: string[]; } ``` --- ## 🗄️ Esquema de Base de Datos ```sql -- Tabla de pasos de protocolo CREATE TABLE tes_content.protocol_steps ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), protocol_id VARCHAR(100) NOT NULL REFERENCES tes_content.content_items(id), step_order INTEGER NOT NULL, step_type VARCHAR(20) NOT NULL DEFAULT 'obligatory', content TEXT NOT NULL, condition TEXT, equipment TEXT[], drugs TEXT[], warnings TEXT[], estimated_time_seconds INTEGER, CONSTRAINT chk_step_type CHECK (step_type IN ('obligatory', 'conditional', 'optional', 'critical')), CONSTRAINT unique_protocol_step_order UNIQUE (protocol_id, step_order) ); CREATE INDEX idx_protocol_steps_protocol_id ON tes_content.protocol_steps(protocol_id); CREATE INDEX idx_protocol_steps_order ON tes_content.protocol_steps(protocol_id, step_order); -- Tabla de dependencias entre protocolos CREATE TABLE tes_content.protocol_dependencies ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), protocol_id VARCHAR(100) NOT NULL REFERENCES tes_content.content_items(id), dependency_type VARCHAR(20) NOT NULL, target_protocol_id VARCHAR(100) NOT NULL REFERENCES tes_content.content_items(id), required BOOLEAN NOT NULL DEFAULT false, notes TEXT, CONSTRAINT chk_dependency_type CHECK ( dependency_type IN ('prerequisite', 'next', 'alternative', 'complement') ), CONSTRAINT chk_no_self_dependency CHECK (protocol_id != target_protocol_id) ); CREATE INDEX idx_protocol_dependencies_protocol ON tes_content.protocol_dependencies(protocol_id); CREATE INDEX idx_protocol_dependencies_target ON tes_content.protocol_dependencies(target_protocol_id); CREATE INDEX idx_protocol_dependencies_type ON tes_content.protocol_dependencies(dependency_type); ``` --- ## ✅ Casos de Uso ### Caso 1: Validar ejecución de protocolo ```typescript const validation = await protocolValidationService.validateProtocolSequence( 'rcp-adulto-svb', [1, 2, 3, 4, 5] // Pasos ejecutados ); if (!validation.valid) { console.error('Errores:', validation.errors); // Bloquear continuación del protocolo } ``` ### Caso 2: Validar dependencias antes de iniciar protocolo ```typescript const depValidation = await protocolValidationService.validateProtocolDependencies( 'rcp-adulto-sva', ['rcp-adulto-svb'] // Protocolos ya ejecutados ); if (!depValidation.valid) { throw new Error(`No se puede ejecutar: ${depValidation.errors.join(', ')}`); } ``` ### Caso 3: Obtener ruta de ejecución sugerida ```typescript const path = await protocolValidationService.getProtocolExecutionPath( 'rcp-adulto-svb', { ageGroup: 'adulto', priority: 'critico' } ); console.log(`Ruta sugerida: ${path.protocols.map(p => p.title).join(' → ')}`); console.log(`Tiempo estimado: ${path.totalEstimatedTime} segundos`); ``` --- ## 🧪 Tests ```typescript describe('ProtocolValidationService', () => { describe('validateProtocolSequence', () => { it('should reject if critical steps are missing', async () => { const result = await service.validateProtocolSequence( 'rcp-adulto-svb', [1, 2, 3] // Faltan pasos críticos 4 y 5 ); expect(result.valid).toBe(false); expect(result.errors).toContain(expect.stringContaining('críticos')); }); it('should warn if steps are out of order', async () => { const result = await service.validateProtocolSequence( 'rcp-adulto-svb', [1, 3, 2, 4, 5] // Orden incorrecto ); expect(result.warnings).toContain(expect.stringContaining('orden')); }); }); describe('validateProtocolDependencies', () => { it('should reject if prerequisites are missing', async () => { const result = await service.validateProtocolDependencies( 'rcp-adulto-sva', [] // No hay protocolos ejecutados ); expect(result.valid).toBe(false); expect(result.errors).toContain(expect.stringContaining('prerequisito')); }); }); }); ``` --- **Fin del documento**