test: flujos ejecutados de punta a punta contra un ERP falso
setup/tests/ corre NormalBot de verdad —no un espejo—: SQLite en memoria con los endpoints del seed reescritos hacia fixtures_api.php servido con php -S, asi el curl real se ejecuta y los POST capturados se comparan contra el contrato del procesador. ConversationContext, WhatsAppSender y AiBot son falsos en memoria; el resto es el codigo de produccion. 51 casos: finca restringida que entra sola, ausentismo completo con rango invertido rechazado, perfiles trabajador/supervisor, ciclos con multi-select paginado, mantenimiento con requires/resolver, labores con cuadrilla, y NLU que resuelve "plateo" por entity hasta el informe. Destaparon dos errores reales: - registrar_mantenimiento posteaba a labores_up sin novedad_id ni empleados, que el procesador exige: el ERP lo habria rechazado siempre. Ahora pide la labor del grupo elegido (endpoint nuevo novedades_mant_dn) y la cuadrilla. - resolverPorCampos reventaba con warning al sustituir arreglos en la URL. Uso: bash setup/tests/run.sh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
6df1d482cc
commit
1f7d33b40d
@@ -858,6 +858,8 @@ class NormalBot
|
||||
{
|
||||
if ($plantilla === '' || !str_contains($plantilla, '{')) return $plantilla;
|
||||
foreach ($recolectado as $k => $v) {
|
||||
// multi_select guarda arreglos; no son sustituibles en una URL
|
||||
if (is_array($v)) continue;
|
||||
$plantilla = str_replace('{' . $k . '}', (string)$v, $plantilla);
|
||||
}
|
||||
return $plantilla;
|
||||
|
||||
@@ -585,6 +585,27 @@ $configJson = [
|
||||
'label_field' => 'label',
|
||||
'empty_text' => '⚠️ No hay lotes con cantidad pendiente para ese grupo.',
|
||||
],
|
||||
[
|
||||
// labores_up exige la novedad concreta, no el grupo: se
|
||||
// ofrecen solo las del grupo ya elegido
|
||||
'key' => 'novedad_id',
|
||||
'label' => 'Labor',
|
||||
'type' => 'select',
|
||||
'prompt' => '¿Qué labor de mantenimiento?',
|
||||
'source_endpoint_key' => 'novedades_mant_dn',
|
||||
'value_field' => 'id',
|
||||
'label_field' => 'nombre',
|
||||
],
|
||||
[
|
||||
'key' => 'empleados',
|
||||
'label' => 'Trabajadores',
|
||||
'type' => 'multi_select',
|
||||
'prompt' => '¿Quiénes lo hicieron? (puedes marcar varios)',
|
||||
'source_endpoint_key' => 'empleados_labor_dn',
|
||||
'value_field' => 'id',
|
||||
'label_field' => 'nombre',
|
||||
'from_phone' => 'tercero_id',
|
||||
],
|
||||
[
|
||||
'key' => 'cantidad',
|
||||
'label' => 'Cantidad',
|
||||
@@ -903,6 +924,7 @@ $endpoints = [
|
||||
// Fase 3: las que arrastran inventario o autorizacion previa
|
||||
['key' => 'novedades_labor3_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=novedades_labor_dn&fase=3'],
|
||||
['key' => 'productos_fertilizacion_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=productos_fertilizacion_dn&lote_id={lote_id}'],
|
||||
['key' => 'novedades_mant_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=novedades_labor_dn&fase=3&grupo={grupo_id}'],
|
||||
['key' => 'lotes_mantenimiento_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=lotes_mantenimiento_dn&grupo={grupo_id}&finca_id={finca_id}'],
|
||||
['key' => 'lotes_mantenimiento_todos_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=lotes_mantenimiento_dn&grupo={grupo_id}&finca_id={finca_id}&todos=1'],
|
||||
// Todos los lotes (PDF) por ciclo
|
||||
|
||||
@@ -547,7 +547,9 @@ echo "\nMantenimiento (POST)\n";
|
||||
$mto = $flows['registrar_mantenimiento'] ?? [];
|
||||
$mf = array_column($mto['fields'] ?? [], null, 'key');
|
||||
|
||||
check('pide fecha, lote y cantidad', array_keys($mf), ['fecha', 'lote_id', 'cantidad']);
|
||||
// labores_up exige novedad y empleados: sin ellos el ERP rechazaba siempre
|
||||
check('pide fecha, lote, labor, cuadrilla y cantidad',
|
||||
array_keys($mf), ['fecha', 'lote_id', 'novedad_id', 'empleados', 'cantidad']);
|
||||
check('los lotes salen de los que tienen pendiente',
|
||||
$mf['lote_id']['source_endpoint_key'] ?? null, 'lotes_mantenimiento_dn');
|
||||
check('endpoint registrado', isset($eps['lotes_mantenimiento_dn']), true);
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Arnés para ejecutar NormalBot de verdad, sin MySQL ni WhatsApp:
|
||||
*
|
||||
* db() -> SQLite en memoria, con los endpoints del seed
|
||||
* reescritos hacia el servidor de fixtures
|
||||
* ConversationContext -> estado en memoria, un contexto por teléfono
|
||||
* WhatsAppSender -> captura los envíos directos en ::$enviados
|
||||
* AiBot -> respuestas guionadas en ::$respuestas
|
||||
*
|
||||
* El curl es real: pega contra fixtures_api.php servido con `php -S`.
|
||||
*/
|
||||
|
||||
const FIXTURE_BASE = 'http://127.0.0.1:8973/fixtures_api.php';
|
||||
|
||||
// ── db(): SQLite con los endpoints reales del seed ───────────────────────────
|
||||
function db(): PDO
|
||||
{
|
||||
static $pdo = null;
|
||||
if ($pdo !== null) return $pdo;
|
||||
|
||||
$pdo = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
$pdo->exec("CREATE TABLE company_endpoints (
|
||||
company_id INT, endpoint_key TEXT, direction TEXT, url TEXT,
|
||||
method TEXT DEFAULT 'GET', params TEXT DEFAULT NULL, is_active INT DEFAULT 1)");
|
||||
$pdo->exec("CREATE TABLE company_phones (
|
||||
company_id INT, wa_number TEXT, label TEXT, permission_type INT,
|
||||
tercero_id INT NULL, modulos_json TEXT NULL, es_supervisor INT DEFAULT 0, is_active INT DEFAULT 1)");
|
||||
|
||||
// Los endpoints salen del seed real: mismas claves y placeholders, otra base.
|
||||
$seed = file_get_contents(dirname(__DIR__) . '/seed_palmas.php');
|
||||
preg_match_all("/\['key' => '([^']+)',\s*'dir' => '([^']+)',\s*'url' => \\\$BASE \. '([^']*)'/", $seed, $m);
|
||||
$ins = $pdo->prepare("INSERT INTO company_endpoints (company_id, endpoint_key, direction, url) VALUES (1, ?, ?, ?)");
|
||||
foreach ($m[1] as $i => $key) {
|
||||
$ins->execute([$key, $m[2][$i], FIXTURE_BASE . $m[3][$i]]);
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
// ── ConversationContext en memoria ───────────────────────────────────────────
|
||||
class ConversationContext
|
||||
{
|
||||
public static array $store = [];
|
||||
|
||||
private static function &ctx(int $id): array
|
||||
{
|
||||
if (!isset(self::$store[$id])) {
|
||||
self::$store[$id] = ['current_node' => null, 'metadata' => []];
|
||||
}
|
||||
return self::$store[$id];
|
||||
}
|
||||
|
||||
public static function getOrCreate(int $companyId, string $phone, string $botType = 'normal'): array
|
||||
{
|
||||
// Un id estable por teléfono para aislar escenarios
|
||||
$id = crc32($companyId . '|' . $phone) % 100000;
|
||||
$c = self::ctx($id);
|
||||
return ['id' => $id, 'current_node' => $c['current_node'], 'metadata' => json_encode($c['metadata'])];
|
||||
}
|
||||
|
||||
public static function updateNode(int $id, ?string $node): void
|
||||
{
|
||||
self::ctx($id)['current_node'] = $node;
|
||||
}
|
||||
|
||||
public static function getMetadata(int $id): array
|
||||
{
|
||||
return self::ctx($id)['metadata'];
|
||||
}
|
||||
|
||||
public static function updateMetadata(int $id, array $meta): void
|
||||
{
|
||||
self::ctx($id)['metadata'] = $meta;
|
||||
}
|
||||
|
||||
public static function reset(int $id): void
|
||||
{
|
||||
self::$store[$id] = ['current_node' => null, 'metadata' => []];
|
||||
}
|
||||
}
|
||||
|
||||
// ── WhatsAppSender: captura ──────────────────────────────────────────────────
|
||||
class WhatsAppSender
|
||||
{
|
||||
public static array $enviados = [];
|
||||
|
||||
public static function sendText(string $to, string $text, string $phoneNumberId): array
|
||||
{
|
||||
self::$enviados[] = ['to' => $to, 'text' => $text];
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
public static function uploadMedia(string $file, string $mime, string $phoneNumberId): array
|
||||
{
|
||||
return ['success' => true, 'media_id' => 'test-media'];
|
||||
}
|
||||
|
||||
public static function sendDocument(string $to, string $mediaId, string $phoneNumberId, string $caption, string $name): array
|
||||
{
|
||||
self::$enviados[] = ['to' => $to, 'document' => $name];
|
||||
return ['success' => true];
|
||||
}
|
||||
}
|
||||
|
||||
// ── AiBot guionado ───────────────────────────────────────────────────────────
|
||||
class AiBot
|
||||
{
|
||||
/** Cola de respuestas para routeOrChat; vacía = chat vacío (cae al fallback). */
|
||||
public static array $respuestas = [];
|
||||
|
||||
public static function routeOrChat(array $company, array $context, string $input): array
|
||||
{
|
||||
return array_shift(self::$respuestas) ?? ['action' => 'chat', 'text' => ''];
|
||||
}
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/services/NormalBot.php';
|
||||
|
||||
// ── Utilidades del test ──────────────────────────────────────────────────────
|
||||
|
||||
function empresa(): array
|
||||
{
|
||||
static $config = null;
|
||||
if ($config === null) {
|
||||
$seed = file_get_contents(dirname(__DIR__) . '/seed_palmas.php');
|
||||
$open = strpos($seed, '[', strpos($seed, '$configJson = ['));
|
||||
$d = 0; $end = $open;
|
||||
for ($i = $open, $n = strlen($seed); $i < $n; $i++) {
|
||||
if ($seed[$i] === '[') $d++;
|
||||
elseif ($seed[$i] === ']') { $d--; if (!$d) { $end = $i; break; } }
|
||||
}
|
||||
$config = eval('return ' . substr($seed, $open, $end - $open + 1) . ';');
|
||||
// Sin saludo diario: mete un envío extra en cada primer mensaje
|
||||
$config['welcome']['enabled'] = false;
|
||||
}
|
||||
return [
|
||||
'id' => 1,
|
||||
'name' => 'test',
|
||||
'display_name' => 'Empresa Test',
|
||||
'api_key' => 'k',
|
||||
'config_json' => json_encode($config),
|
||||
'_permission_type' => 3,
|
||||
];
|
||||
}
|
||||
|
||||
function contexto(string $phone): array
|
||||
{
|
||||
return ['from' => $phone, 'name' => 'Tester', 'phone_number_id' => 'pn', 'permission_type' => 3];
|
||||
}
|
||||
|
||||
/** Texto plano de la respuesta del bot, venga como texto o interactivo. */
|
||||
function textoDe(?array $resp): string
|
||||
{
|
||||
if (!$resp) return '';
|
||||
$p = json_decode($resp['payload'] ?? '{}', true);
|
||||
if (isset($p['text'])) return $p['text'];
|
||||
$i = $p['interactive'] ?? [];
|
||||
return ($i['header']['text'] ?? '') . ' ' . ($i['body']['text'] ?? '');
|
||||
}
|
||||
|
||||
/** Filas/botones de una respuesta interactiva, como id => title. */
|
||||
function filasDe(?array $resp): array
|
||||
{
|
||||
$p = json_decode($resp['payload'] ?? '{}', true);
|
||||
$i = $p['interactive'] ?? [];
|
||||
$out = [];
|
||||
foreach ($i['action']['sections'] ?? [] as $sec) {
|
||||
foreach ($sec['rows'] ?? [] as $r) $out[$r['id']] = $r['title'];
|
||||
}
|
||||
foreach ($i['action']['buttons'] ?? [] as $b) {
|
||||
$out[$b['reply']['id']] = $b['reply']['title'];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** Las capturas del servidor de fixtures desde la última llamada. */
|
||||
function capturas(): array
|
||||
{
|
||||
$log = getenv('CAPTURE_LOG');
|
||||
if (!is_file($log)) return [];
|
||||
$out = [];
|
||||
foreach (file($log, FILE_IGNORE_NEW_LINES) as $l) $out[] = json_decode($l, true);
|
||||
file_put_contents($log, '');
|
||||
return $out;
|
||||
}
|
||||
|
||||
$GLOBALS['fallas'] = 0;
|
||||
function check(string $nombre, $obtenido, $esperado = true): void
|
||||
{
|
||||
$ok = $obtenido === $esperado;
|
||||
if (!$ok) $GLOBALS['fallas']++;
|
||||
printf("%s %s\n", $ok ? ' ok ' : ' FALLA', $nombre);
|
||||
if (!$ok) {
|
||||
echo " esperaba: " . json_encode($esperado, JSON_UNESCAPED_UNICODE) . "\n";
|
||||
echo " obtuvo: " . json_encode($obtenido, JSON_UNESCAPED_UNICODE) . "\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
/**
|
||||
* ERP falso para los tests unitarios. Se sirve con `php -S` y responde los
|
||||
* catálogos con datos fijos; los POST se capturan en un log JSONL para que el
|
||||
* test verifique el payload exacto que armó el bot.
|
||||
*/
|
||||
|
||||
$peticion = $_GET['peticion'] ?? '';
|
||||
$capture = getenv('CAPTURE_LOG') ?: sys_get_temp_dir() . '/bot_test_capture.jsonl';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// ── POST: capturar y responder como el ERP real ──────────────────────────────
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
file_put_contents($capture, json_encode([
|
||||
'peticion' => $peticion,
|
||||
'query' => $_GET,
|
||||
'body' => $body,
|
||||
], JSON_UNESCAPED_UNICODE) . "\n", FILE_APPEND);
|
||||
|
||||
// Mismo shape que BotEntradaProcesador para que el bot repita el detalle
|
||||
echo json_encode([
|
||||
'status' => 'ok',
|
||||
'mensaje' => 'capturado:' . $peticion,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── GET: catálogos fijos ──────────────────────────────────────────────────────
|
||||
$fincas = [
|
||||
['id' => '0', 'label' => '🌐 Todas las fincas'],
|
||||
['id' => '4', 'label' => 'REPOSO'],
|
||||
['id' => '7', 'label' => 'ROSA BLANCA'],
|
||||
];
|
||||
// El ERP acota por número: este wa solo tiene una finca asignada
|
||||
if (($_GET['wa'] ?? '') === '57300RESTRINGIDO') {
|
||||
$fincas = [['id' => '4', 'label' => 'REPOSO']];
|
||||
}
|
||||
|
||||
$novedades17 = [];
|
||||
foreach (range(1, 17) as $i) $novedades17[] = ['id' => (string)(100 + $i), 'nombre' => "NOVEDAD {$i}"];
|
||||
|
||||
$lotes12 = [];
|
||||
foreach (range(1, 12) as $i) $lotes12[] = ['id' => (string)$i, 'label' => "REPOSO {$i}A (" . (50 - $i) . "d)"];
|
||||
|
||||
$fixtures = [
|
||||
'fincas' => $fincas,
|
||||
'empleados' => [
|
||||
['id' => '412', 'nombre' => 'JULIO PEREZ GOMEZ'],
|
||||
['id' => '415', 'nombre' => 'JULIO CESAR RAMIREZ'],
|
||||
['id' => '418', 'nombre' => 'MARIA GOMEZ RUIZ'],
|
||||
],
|
||||
'novedades_ausentismo_dn' => $novedades17,
|
||||
'novedades_labor_dn' => !empty($_GET['grupo'])
|
||||
? [['id' => '77', 'nombre' => 'PLATEO MANUAL']]
|
||||
: [
|
||||
['id' => '42', 'nombre' => 'TRACTORISTA'],
|
||||
['id' => '43', 'nombre' => 'HORAS EXTRA'],
|
||||
],
|
||||
'lotes_apertura_dn' => $lotes12,
|
||||
'lotes_abiertos_dn' => array_slice($lotes12, 0, 3),
|
||||
'lotes_x_finca' => [
|
||||
['id' => '4', 'label' => 'REPOSO 1A'],
|
||||
['id' => '9', 'label' => 'REPOSO 2A'],
|
||||
],
|
||||
'grupos_mantenimiento' => [
|
||||
['id' => '5', 'label' => 'PLATEO'],
|
||||
['id' => '6', 'label' => 'CORONA'],
|
||||
],
|
||||
'lotes_mantenimiento_dn' => [
|
||||
['id' => '4', 'label' => 'REPOSO 1A (falta 12)', 'faltante' => 12],
|
||||
],
|
||||
'productos_fertilizacion_dn' => [
|
||||
['id' => '30', 'nombre' => 'UREA (falta 120)', 'dosis' => 120],
|
||||
],
|
||||
];
|
||||
|
||||
// Los informes de texto responden {status, message}
|
||||
if (str_contains($peticion, '_texto_bot')) {
|
||||
file_put_contents($capture, json_encode(['peticion' => $peticion, 'query' => $_GET]) . "\n", FILE_APPEND);
|
||||
echo json_encode(['status' => '1', 'message' => 'informe-de-prueba']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($fixtures[$peticion])) {
|
||||
// La paginación del ERP: los catálogos cortos devuelven 10 salvo &todos=1
|
||||
$datos = $fixtures[$peticion];
|
||||
if (in_array($peticion, ['lotes_apertura_dn', 'lotes_abiertos_dn'], true) && empty($_GET['todos'])) {
|
||||
$datos = array_slice($datos, 0, 10);
|
||||
}
|
||||
echo json_encode(['status' => '1', 'datos' => $datos], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(404);
|
||||
echo json_encode(['status' => 'error', 'mensaje' => "sin fixture: {$peticion}"]);
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
# Levanta el ERP falso, corre los tests de flujo y apaga el servidor.
|
||||
cd "$(dirname "$0")/../.."
|
||||
export CAPTURE_LOG="${TMPDIR:-/tmp}/bot_test_capture.jsonl"
|
||||
: > "$CAPTURE_LOG"
|
||||
CAPTURE_LOG="$CAPTURE_LOG" php -S 127.0.0.1:8973 setup/tests/fixtures_api.php >/dev/null 2>&1 &
|
||||
SERVER=$!
|
||||
trap "kill $SERVER 2>/dev/null" EXIT
|
||||
sleep 0.4
|
||||
php setup/tests/test_flujos.php
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Tests unitarios de los flujos de carga, ejecutando NormalBot de verdad.
|
||||
*
|
||||
* A diferencia de test_navegacion.php (que espeja la lógica), acá corre
|
||||
* process()/processInteractive() reales: el curl pega contra fixtures_api.php
|
||||
* y los POST capturados se comparan contra el contrato del ERP.
|
||||
*
|
||||
* Uso: php setup/tests/run.sh (levanta el servidor de fixtures y corre esto)
|
||||
*/
|
||||
|
||||
require __DIR__ . '/bootstrap.php';
|
||||
|
||||
$co = empresa();
|
||||
|
||||
// ════ 1. Finca restringida: una sola → entra directo ═════════════════════════
|
||||
echo "\nFinca — alcance por número\n";
|
||||
|
||||
$ctx = contexto('57300RESTRINGIDO');
|
||||
$r = NormalBot::process($co, $ctx, 'hola');
|
||||
check('con una sola finca no pregunta: cae al menú',
|
||||
str_contains(textoDe($r), '¿Qué deseas hacer?'));
|
||||
$meta = ConversationContext::getMetadata(ConversationContext::getOrCreate(1, '57300RESTRINGIDO')['id']);
|
||||
check('la finca quedó elegida sola', $meta['finca']['finca_id'] ?? null, '4');
|
||||
check('con su nombre para el footer', $meta['finca']['finca_label'] ?? null, 'REPOSO');
|
||||
$p = json_decode($r['payload'], true);
|
||||
check('y el footer la muestra', $p['interactive']['footer']['text'] ?? '', '📍 REPOSO');
|
||||
|
||||
$ctx = contexto('57300LIBRE');
|
||||
$r = NormalBot::process($co, $ctx, 'hola');
|
||||
check('sin restricción sí pregunta, con "Todas" primero',
|
||||
(string)array_key_first(filasDe($r)), '0');
|
||||
capturas();
|
||||
|
||||
// ════ 2. Ausentismo completo ═════════════════════════════════════════════════
|
||||
echo "\nAusentismo — de la primera pregunta al POST\n";
|
||||
|
||||
$ctx = contexto('57300AUSEN');
|
||||
$r = NormalBot::processInteractive($co, $ctx, 'registrar_ausentismo');
|
||||
check('arranca pidiendo el trabajador', str_contains(textoDe($r), 'nombre o documento'));
|
||||
|
||||
$r = NormalBot::process($co, $ctx, 'julio');
|
||||
check('varios homónimos: ofrece elegir', str_contains(textoDe($r), 'coincidencias'));
|
||||
check('con los tres del catálogo', count(filasDe($r)), 3);
|
||||
|
||||
$r = NormalBot::processInteractive($co, $ctx, '412');
|
||||
$filas = filasDe($r);
|
||||
check('sigue el motivo, paginado: 9 + "Ver más"', count($filas), 10);
|
||||
check('la última fila es "Ver más"', isset($filas['__mas']));
|
||||
|
||||
$r = NormalBot::processInteractive($co, $ctx, '__mas');
|
||||
$filas = filasDe($r);
|
||||
check('la página 2 trae las 8 restantes', count($filas), 8);
|
||||
check('y ya sin "Ver más"', !isset($filas['__mas']));
|
||||
|
||||
$r = NormalBot::processInteractive($co, $ctx, '112');
|
||||
check('sigue la fecha inicial', str_contains(textoDe($r), 'Desde qué fecha'));
|
||||
|
||||
$hoy = date('Y-m-d'); $ayer = date('Y-m-d', strtotime('-1 day'));
|
||||
$r = NormalBot::processInteractive($co, $ctx, $hoy);
|
||||
check('sigue la fecha final', str_contains(textoDe($r), 'Hasta qué fecha'));
|
||||
|
||||
WhatsAppSender::$enviados = [];
|
||||
$r = NormalBot::processInteractive($co, $ctx, $ayer);
|
||||
check('rechaza el rango invertido',
|
||||
str_contains(WhatsAppSender::$enviados[0]['text'] ?? '', 'no puede ser anterior'));
|
||||
check('y vuelve a mostrar el selector de fecha', str_contains(textoDe($r), 'Hasta qué fecha'));
|
||||
|
||||
$r = NormalBot::processInteractive($co, $ctx, $hoy);
|
||||
check('resumen con confirmación', isset(filasDe($r)['__cap_confirm']));
|
||||
|
||||
capturas(); // limpia GETs de catálogos
|
||||
NormalBot::processInteractive($co, $ctx, '__cap_confirm');
|
||||
$post = array_values(array_filter(capturas(), fn($c) => ($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', '112', $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";
|
||||
|
||||
db()->exec("INSERT INTO company_phones (company_id, wa_number, label, permission_type, tercero_id, es_supervisor)
|
||||
VALUES (1, '57300TRABAJADOR', 'Julio', 1, 412, 0),
|
||||
(1, '57300JEFE', 'Sup', 3, 412, 1)");
|
||||
|
||||
$ctx = contexto('57300TRABAJADOR');
|
||||
$r = NormalBot::processInteractive($co, $ctx, 'registrar_ausentismo');
|
||||
check('el trabajador vinculado no se pregunta: arranca por el motivo',
|
||||
str_contains(textoDe($r), 'motivo del 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');
|
||||
$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']));
|
||||
|
||||
$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');
|
||||
|
||||
$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 como arreglo', $post['body']['lote_ids'] ?? null, ['1', '2']);
|
||||
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']));
|
||||
|
||||
$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');
|
||||
|
||||
// ════ Resultado ═══════════════════════════════════════════════════════════════
|
||||
echo "\n" . ($GLOBALS['fallas'] ? "{$GLOBALS['fallas']} falla(s)\n" : "Todos los flujos ejecutan de punta a punta\n");
|
||||
exit($GLOBALS['fallas'] ? 1 : 0);
|
||||
Reference in New Issue
Block a user