($c['peticion'] ?? '') === 'ausentismos_up'))[0] ?? null; check('el POST llegó al ERP', $post !== null); check('con el contrato exacto del procesador', [$post['body']['empleado_id'] ?? null, $post['body']['novedad_id'] ?? null, $post['body']['fecha_inicial'] ?? null, $post['body']['fecha_final'] ?? null], ['412', '46', $hoy, $hoy]); check('y la trazabilidad', $post['body']['telefono'] ?? null, '57300AUSEN'); // ════ 3. Ausentismo según el perfil del número ═══════════════════════════════ echo "\nPerfil — trabajador vinculado y supervisor\n"; fakePhone('57300TRABAJADOR', 412, 0); fakePhone('57300JEFE', 412, 1); $ctx = contexto('57300TRABAJADOR'); $r = NormalBot::processInteractive($co, $ctx, 'registrar_ausentismo'); check('el trabajador vinculado no se pregunta: arranca por el tipo', str_contains(textoDe($r), 'tipo de ausentismo')); $ctx = contexto('57300JEFE'); $r = NormalBot::processInteractive($co, $ctx, 'registrar_ausentismo'); check('el supervisor elige aunque tenga tercero', str_contains(textoDe($r), 'nombre o documento')); capturas(); // ════ 4. Ciclos: apertura con selección múltiple ═════════════════════════════ echo "\nCiclos — apertura de varios lotes\n"; $ctx = contexto('57300CICLOS'); $id = ConversationContext::getOrCreate(1, '57300CICLOS')['id']; ConversationContext::updateMetadata($id, ['finca' => ['finca_id' => '4', 'finca_label' => 'REPOSO']]); $r = NormalBot::processInteractive($co, $ctx, 'registrar_ciclo'); check('pregunta el tipo de ciclo', str_contains(textoDe($r), 'Qué ciclo')); $r = NormalBot::processInteractive($co, $ctx, 'cosecha'); check('después la acción', isset(filasDe($r)['apertura'])); $r = NormalBot::processInteractive($co, $ctx, 'apertura'); $r = NormalBot::processInteractive($co, $ctx, $hoy); $filas = filasDe($r); check('lista de lotes con "Otro lote" y "Listo"', isset($filas['__todos']) && isset($filas['__listo'])); check('solo lotes de la finca elegida, ninguno ajeno', !isset($filas['99']) && !str_contains(implode(' ', $filas), 'ROSA BLANCA')); $r = NormalBot::processInteractive($co, $ctx, '1'); check('marcar redibuja con el check', str_contains(filasDe($r)['1'] ?? '', '✅')); $r = NormalBot::processInteractive($co, $ctx, '2'); check('el contador acumula', str_contains(filasDe($r)['__listo'] ?? '', '(2)')); $r = NormalBot::processInteractive($co, $ctx, '__todos'); check('"Otro lote" pasa al catálogo completo, paginado', isset(filasDe($r)['__mas'])); check('sin perder lo marcado', str_contains(filasDe($r)['1'] ?? '', '✅')); $r = NormalBot::processInteractive($co, $ctx, '__mas'); check('y la página 2 alcanza el lote 12', isset(filasDe($r)['12'])); $r = NormalBot::processInteractive($co, $ctx, '__mas'); // vuelve: no hay página 3 $r = NormalBot::processInteractive($co, $ctx, '__mas'); // Escribir varios de una: seis toques eran seis mensajes $r = NormalBot::process($co, $ctx, '3a, 4a 5a'); $filas = filasDe($r); check('escribir marca varios en un solo mensaje', str_contains($filas['__listo'] ?? '', '(5)')); // El contador no depende de la pagina que se este viendo check('suma a lo tocado en vez de reemplazarlo: 2 + 3 = 5', str_contains($filas['__listo'] ?? '', '(5)')); WhatsAppSender::$enviados = []; $r = NormalBot::process($co, $ctx, '6a, 9z9z'); check('lo que no reconoce lo avisa, no lo ignora', str_contains(WhatsAppSender::$enviados[0]['text'] ?? '', '"9z9z"')); check('y marca igual lo que sí entendió', str_contains(filasDe($r)['__listo'] ?? '', '(6)')); $r = NormalBot::processInteractive($co, $ctx, '__listo'); check('resumen final', isset(filasDe($r)['__cap_confirm'])); capturas(); NormalBot::processInteractive($co, $ctx, '__cap_confirm'); $post = array_values(array_filter(capturas(), fn($c) => str_contains($c['peticion'] ?? '', 'ciclos_')))[0] ?? null; check('la URL resolvió el tipo elegido', $post['peticion'] ?? null, 'ciclos_cosecha_up'); check('lote_ids lleva los tocados y los escritos', $post['body']['lote_ids'] ?? null, ['1', '2', '3', '4', '5', '6']); check('con la acción y la fecha', [$post['body']['accion'] ?? null, $post['body']['fecha'] ?? null], ['apertura', $hoy]); // ════ 5. Mantenimiento: requires/resolver + payload completo ═════════════════ echo "\nMantenimiento — el grupo se pide primero\n"; $ctx = contexto('57300MANT'); $r = NormalBot::processInteractive($co, $ctx, 'registrar_mantenimiento'); check('sin grupo elegido desvía al selector', isset(filasDe($r)['5'])); $r = NormalBot::processInteractive($co, $ctx, '5'); check('y vuelve al flujo: pide la fecha', str_contains(textoDe($r), 'De qué fecha')); $r = NormalBot::processInteractive($co, $ctx, $hoy); check('el lote muestra la cantidad faltante', str_contains(filasDe($r)['4'] ?? '', 'falta 12')); $r = NormalBot::processInteractive($co, $ctx, '4'); check('la labor sale del grupo elegido', filasDe($r), ['77' => 'PLATEO MANUAL']); $r = NormalBot::processInteractive($co, $ctx, '77'); $r = NormalBot::processInteractive($co, $ctx, '412'); $r = NormalBot::processInteractive($co, $ctx, '__listo'); $r = NormalBot::process($co, $ctx, '12'); check('resumen', isset(filasDe($r)['__cap_confirm'])); capturas(); NormalBot::processInteractive($co, $ctx, '__cap_confirm'); $post = array_values(array_filter(capturas(), fn($c) => ($c['peticion'] ?? '') === 'labores_up'))[0] ?? null; check('postea todo lo que labores_up exige', [$post['body']['novedad_id'] ?? null, $post['body']['lote_id'] ?? null, $post['body']['empleados'] ?? null, $post['body']['cantidad'] ?? null], ['77', '4', ['412'], '12']); // ════ 6. Labores fase 2 ═══════════════════════════════════════════════════════ echo "\nLabores diarias — cuadrilla completa\n"; $ctx = contexto('57300LABOR'); $id = ConversationContext::getOrCreate(1, '57300LABOR')['id']; ConversationContext::updateMetadata($id, ['finca' => ['finca_id' => '4', 'finca_label' => 'REPOSO']]); $r = NormalBot::processInteractive($co, $ctx, 'registrar_labor'); $r = NormalBot::processInteractive($co, $ctx, $hoy); check('el catálogo viene filtrado por fase', filasDe($r), ['42' => 'TRACTORISTA', '43' => 'HORAS EXTRA']); $r = NormalBot::processInteractive($co, $ctx, '42'); check('los lotes son los de su finca', isset(filasDe($r)['4']) && isset(filasDe($r)['9'])); check('sin lotes de otras fincas', !isset(filasDe($r)['99'])); $r = NormalBot::processInteractive($co, $ctx, '4'); $r = NormalBot::processInteractive($co, $ctx, '412'); $r = NormalBot::processInteractive($co, $ctx, '415'); $r = NormalBot::processInteractive($co, $ctx, '__listo'); $r = NormalBot::process($co, $ctx, '1'); capturas(); NormalBot::processInteractive($co, $ctx, '__cap_confirm'); $post = array_values(array_filter(capturas(), fn($c) => ($c['peticion'] ?? '') === 'labores_up'))[0] ?? null; check('la cuadrilla viaja como arreglo', $post['body']['empleados'] ?? null, ['412', '415']); // ════ 7. NLU: entity resuelve el grupo y ejecuta el informe ═══════════════════ echo "\nNLU — \"informe de mantenimiento de plateo\" en un mensaje\n"; $ctx = contexto('57300NLU'); $id = ConversationContext::getOrCreate(1, '57300NLU')['id']; ConversationContext::updateMetadata($id, ['finca' => ['finca_id' => '4', 'finca_label' => 'REPOSO']]); ConversationContext::updateNode($id, '__greeted'); AiBot::$respuestas = [[ 'action' => 'route', 'key' => 'ciclo_mantenimiento_top', 'entities' => ['grupo' => 'plateo'], ]]; capturas(); $r = NormalBot::process($co, $ctx, 'quiero informe de mantenimiento de plateo'); check('el informe llegó sin menús intermedios', str_contains(textoDe($r), 'informe-de-prueba')); $get = array_values(array_filter(capturas(), fn($c) => str_contains($c['peticion'] ?? '', 'mantenimiento_top')))[0] ?? null; check('la entity resolvió el grupo en la URL', $get['query']['grupo'] ?? null, '5'); check('y la consulta dice quién pregunta', $get['query']['wa'] ?? null, '57300NLU'); // ════ 8. NLU coherente con los módulos del perfil ═════════════════════════════ echo "\nNLU — respeta los módulos habilitados\n"; // Este número solo puede cargar pluviometría fakePhone('57300SOLOPLUVIO', null, 0); FakeDb::$phones['57300SOLOPLUVIO']['modulos_json'] = json_encode(['carga' => ['pluviometria']]); $ctx = contexto('57300SOLOPLUVIO'); $id = ConversationContext::getOrCreate(1, '57300SOLOPLUVIO')['id']; ConversationContext::updateMetadata($id, ['finca' => ['finca_id' => '4', 'finca_label' => 'REPOSO']]); ConversationContext::updateNode($id, '__greeted'); // "subir pluviometría" → directo a la fecha, un solo mensaje AiBot::$respuestas = [['action' => 'route', 'key' => 'registrar_pluviometria']]; $r = NormalBot::process($co, $ctx, 'subir pluviometría'); check('"subir pluviometría" va de una a la fecha', str_contains(textoDe($r), 'De qué fecha')); check('el NLU recibió su alcance de módulos', AiBot::$modulosRecibidos, ['pluviometria']); // El modelo devuelve una clave fuera del alcance: el guard la corta ConversationContext::updateNode($id, '__greeted'); ConversationContext::updateMetadata($id, ['finca' => ['finca_id' => '4', 'finca_label' => 'REPOSO']]); WhatsAppSender::$enviados = []; AiBot::$respuestas = [['action' => 'route', 'key' => 'registrar_ausentismo']]; $r = NormalBot::process($co, $ctx, 'registrar ausentismo'); check('un módulo vetado no ejecuta el flujo', !str_contains(textoDe($r), 'nombre o documento')); check('avisa que no entendió', str_contains(WhatsAppSender::$enviados[0]['text'] ?? '', 'No estoy seguro')); // Sin restricción, "subir labores diarias" también entra directo $ctx = contexto('57300LABORNLU'); $id = ConversationContext::getOrCreate(1, '57300LABORNLU')['id']; ConversationContext::updateMetadata($id, ['finca' => ['finca_id' => '4', 'finca_label' => 'REPOSO']]); ConversationContext::updateNode($id, '__greeted'); AiBot::$respuestas = [['action' => 'route', 'key' => 'registrar_labor']]; $r = NormalBot::process($co, $ctx, 'subir labores diarias'); check('"subir labores diarias" arranca el flujo directo', str_contains(textoDe($r), 'De qué fecha es la labor')); capturas(); // ════ 9. Editar desde el resumen ══════════════════════════════════════════════ echo "\nEditar — corregir sin rehacer el formulario\n"; $ctx = contexto('57300EDITAR'); NormalBot::processInteractive($co, $ctx, 'registrar_ausentismo'); NormalBot::process($co, $ctx, 'julio'); NormalBot::processInteractive($co, $ctx, '412'); NormalBot::processInteractive($co, $ctx, 'Incapacidad'); NormalBot::processInteractive($co, $ctx, '46'); NormalBot::processInteractive($co, $ctx, $hoy); $r = NormalBot::processInteractive($co, $ctx, $manana); check('el resumen ofrece editar', isset(filasDe($r)['__cap_edit'])); $r = NormalBot::processInteractive($co, $ctx, '__cap_edit'); $campos = filasDe($r); check('lista los campos ya cargados', array_keys($campos), ['__cap_ed:empleado_id', '__cap_ed:grupo_novedad', '__cap_ed:novedad_id', '__cap_ed:fecha_inicial', '__cap_ed:fecha_final', '__cap_ed_volver']); // Un campo hoja: se corrige y vuelve derecho al resumen $r = NormalBot::processInteractive($co, $ctx, '__cap_ed:fecha_final'); check('pregunta solo ese campo', str_contains(textoDe($r), 'Hasta qué fecha')); $pasado = date('Y-m-d', strtotime('+2 days')); $r = NormalBot::processInteractive($co, $ctx, $pasado); check('y vuelve al resumen, no al siguiente campo', isset(filasDe($r)['__cap_confirm'])); check('con el valor corregido', str_contains(textoDe($r), $pasado)); // Un campo del que otro depende: se re-pregunta en cadena $r = NormalBot::processInteractive($co, $ctx, '__cap_edit'); $r = NormalBot::processInteractive($co, $ctx, '__cap_ed:grupo_novedad'); check('editar el tipo vuelve a preguntar el tipo', str_contains(textoDe($r), 'tipo de ausentismo')); $r = NormalBot::processInteractive($co, $ctx, 'Permiso'); check('y encadena el motivo, que quedó inválido', str_contains(textoDe($r), 'Cuál es el motivo')); check('acotado al tipo nuevo', array_map('strval', array_keys(filasDe($r))), ['50']); $r = NormalBot::processInteractive($co, $ctx, '50'); check('recién ahí vuelve al resumen', isset(filasDe($r)['__cap_confirm'])); // Salir sin tocar nada $r = NormalBot::processInteractive($co, $ctx, '__cap_edit'); $r = NormalBot::processInteractive($co, $ctx, '__cap_ed_volver'); check('"Volver" deja todo como estaba', isset(filasDe($r)['__cap_confirm'])); capturas(); NormalBot::processInteractive($co, $ctx, '__cap_confirm'); $post = array_values(array_filter(capturas(), fn($c) => ($c['peticion'] ?? '') === 'ausentismos_up'))[0] ?? null; check('el POST lleva lo editado, sin restos del valor viejo', [$post['body']['novedad_id'] ?? null, $post['body']['fecha_final'] ?? null], ['50', $pasado]); // ════ Resultado ═══════════════════════════════════════════════════════════════ echo "\n" . ($GLOBALS['fallas'] ? "{$GLOBALS['fallas']} falla(s)\n" : "Todos los flujos ejecutan de punta a punta\n"); exit($GLOBALS['fallas'] ? 1 : 0);