diff --git a/setup/audit_flujos.php b/setup/audit_flujos.php new file mode 100644 index 0000000..a316a43 --- /dev/null +++ b/setup/audit_flujos.php @@ -0,0 +1,220 @@ + url +preg_match_all("/\['key' => '([^']+)',\s*'dir' => '([^']+)',\s*'url' => \\\$BASE \. '([^']*)'/", substr($seed, $end), $m); +$endpoints = []; +foreach ($m[1] as $i => $k) $endpoints[$k] = ['dir' => $m[2][$i], 'url' => $m[3][$i]]; + +// ─── Lo que expone el ERP ──────────────────────────────────────────────────── +$apiPath = $rutaErp . '/php/controller/controller_api_externa.php'; +$hayErp = is_file($apiPath); +$peticiones = []; +$procesador = []; +if ($hayErp) { + preg_match_all("/case '([a-z0-9_]+)':/i", file_get_contents($apiPath), $mp); + $peticiones = array_flip($mp[1]); + + $procPath = $rutaErp . '/php/services/BotEntradaProcesador.php'; + if (is_file($procPath)) { + $proc = file_get_contents($procPath); + // requeridos por tipo, para cruzar con lo que manda el bot + if (preg_match_all("/\\\$requeridos = \[([^\]]+)\]/", $proc, $mr)) $procesador['labores_up'] = $mr[1][0]; + if (preg_match_all("/\\\$required = \[([^\]]+)\]/", $proc, $mq)) $procesador['otros'] = $mq[1]; + } +} + +// ─── Tipos de campo y de flujo implementados ───────────────────────────────── +$bot = file_get_contents(dirname(__DIR__) . '/services/NormalBot.php'); +$tiposFlujo = []; +if (preg_match_all("/'([a-z_]+)'\s*=> self::handle|'([a-z_]+)'\s*=> self::send|'([a-z_]+)'\s*=> self::build/", $bot, $mt)) { + $tiposFlujo = array_filter(array_merge($mt[1], $mt[2], $mt[3])); +} +$tiposFlujo = array_flip(array_merge($tiposFlujo, ['text', 'menu', 'function', 'image'])); + +$tiposCampo = ['text', 'lookup', 'select', 'multi_select', 'static_select', 'date_quick']; + +// ─── Auditoría ─────────────────────────────────────────────────────────────── +$prob = []; +$avis = []; +$ok = 0; + +function grupoDeMeta(array $config, string $clave): bool { + // Claves que salen del metadata acumulado, no de un campo del flujo + foreach ($config['flows'] as $f) { + if (($f['meta_key'] ?? '') === $clave) return true; + } + return in_array($clave, ['finca_id', 'grupo_id', 'wa_number'], true); +} + +foreach (['global' => []] + $config['per_type'] as $cat => $pt) { + $flows = array_merge($config['flows'], $pt['flows'] ?? []); + $menus = array_merge($config['menus'], $pt['menus'] ?? []); + + foreach ($flows as $key => $f) { + $tipo = $f['type'] ?? 'text'; + $ref = "cat {$cat} · {$key}"; + + if (!isset($tiposFlujo[$tipo])) { + $prob[] = "{$ref}: tipo de flujo '{$tipo}' no implementado"; + continue; + } + + // Endpoints que el flujo usa, con los placeholders que llevan + $usados = []; + foreach (['endpoint_key', 'source_endpoint_key', 'full_endpoint_key', 'source_endpoint_key_all'] as $k) { + if (!empty($f[$k])) $usados[] = $f[$k]; + } + foreach ($f['fields'] ?? [] as $campo) { + foreach (['source_endpoint_key', 'full_endpoint_key', 'lookup_endpoint_key'] as $k) { + if (!empty($campo[$k])) $usados[] = $campo[$k]; + } + $tc = $campo['type'] ?? 'text'; + if (!in_array($tc, $tiposCampo, true)) { + $prob[] = "{$ref}: campo '{$campo['key']}' usa tipo '{$tc}', que no existe"; + } + } + if (!empty($f['params']['endpoint_key'])) $usados[] = $f['params']['endpoint_key']; + if (!empty($f['params']['next_endpoint_key'])) $usados[] = $f['params']['next_endpoint_key']; + + $clavesCampo = array_column($f['fields'] ?? [], 'key'); + + foreach (array_unique($usados) as $ek) { + // La clave puede depender de lo elegido: lotes_{accion}_dn + $variantes = [$ek]; + if (preg_match('/\{(\w+)\}/', $ek, $mm)) { + $campo = $mm[1]; + $opts = []; + foreach ($f['fields'] ?? [] as $c) { + if (($c['key'] ?? '') === $campo) $opts = array_column($c['options'] ?? [], 'id'); + } + if (!$opts) { + $prob[] = "{$ref}: la clave '{$ek}' depende de '{$campo}', que no es un campo con opciones"; + continue; + } + $variantes = array_map(fn($o) => str_replace($mm[0], $o, $ek), $opts); + } + + foreach ($variantes as $v) { + if (!isset($endpoints[$v])) { + $prob[] = "{$ref}: endpoint '{$v}' sin registrar"; + continue; + } + $ok++; + $url = $endpoints[$v]['url']; + + // La peticion tiene que existir del otro lado + if ($hayErp && preg_match('/peticion=([a-z0-9_{}]+)/i', $url, $mu)) { + $pet = $mu[1]; + $pets = [$pet]; + if (preg_match('/\{(\w+)\}/', $pet, $mc)) { + $opts = []; + foreach ($f['fields'] ?? [] as $c) { + if (($c['key'] ?? '') === $mc[1]) $opts = array_column($c['options'] ?? [], 'id'); + } + $pets = array_map(fn($o) => str_replace($mc[0], $o, $pet), $opts); + } + foreach ($pets as $p) { + if (!isset($peticiones[$p])) $prob[] = "{$ref}: el ERP no expone '?peticion={$p}'"; + } + } + + // Cada {placeholder} de la URL tiene que poder resolverse + preg_match_all('/\{(\w+)\}/', $url, $mph); + foreach (array_unique($mph[1]) as $ph) { + if (in_array($ph, $clavesCampo, true)) continue; + if (grupoDeMeta($config, $ph)) continue; + $prob[] = "{$ref}: '{$ph}' en la URL de '{$v}' no sale de ningún campo ni del contexto"; + } + } + } + + // Un flujo que postea sin campos no manda nada + if ($tipo === 'collect_and_post' && empty($f['fields'])) { + $prob[] = "{$ref}: collect_and_post sin campos"; + } + // requires necesita su resolver, y el resolver tiene que existir + if (!empty($f['requires'])) { + $r = $f['resolver'] ?? ''; + if ($r === '') $prob[] = "{$ref}: declara requires sin resolver"; + elseif (!isset($flows[$r])) $prob[] = "{$ref}: resolver '{$r}' no existe"; + } + } + + // Cada opción de menú debe llevar a algún lado + foreach ($menus as $mk => $menu) { + $ids = array_column($menu['buttons'] ?? [], 'id'); + foreach ($menu['sections'] ?? [] as $sec) $ids = array_merge($ids, array_column($sec['rows'] ?? [], 'id')); + foreach ($ids as $id) { + if (!isset($flows[$id]) && !isset($menus[$id])) { + $prob[] = "cat {$cat} · menú {$mk}: la opción '{$id}' no lleva a ningún flujo"; + } + } + // WhatsApp corta las listas largas + foreach ($menu['sections'] ?? [] as $sec) { + if (count($sec['rows'] ?? []) > 10) { + $avis[] = "cat {$cat} · menú {$mk}: " . count($sec['rows']) . ' filas, WhatsApp muestra 10'; + } + } + if (count($menu['buttons'] ?? []) > 3) { + $prob[] = "cat {$cat} · menú {$mk}: " . count($menu['buttons']) . ' botones, WhatsApp admite 3'; + } + } +} + +// Endpoints registrados que nadie usa +$usadosTodos = []; +foreach ($config['flows'] as $f) { + foreach (['endpoint_key', 'source_endpoint_key', 'full_endpoint_key', 'source_endpoint_key_all'] as $k) { + if (!empty($f[$k])) $usadosTodos[] = $f[$k]; + } + foreach ($f['fields'] ?? [] as $c) { + foreach (['source_endpoint_key', 'full_endpoint_key', 'lookup_endpoint_key'] as $k) { + if (!empty($c[$k])) $usadosTodos[] = $c[$k]; + } + } + if (!empty($f['params']['endpoint_key'])) $usadosTodos[] = $f['params']['endpoint_key']; + if (!empty($f['params']['next_endpoint_key'])) $usadosTodos[] = $f['params']['next_endpoint_key']; +} +$patrones = array_map(fn($u) => '/^' . str_replace('\{accion\}', '\w+', preg_quote($u, '/')) . '$/', $usadosTodos); +foreach ($endpoints as $k => $e) { + $usado = false; + foreach ($patrones as $p) { if (preg_match($p, $k)) { $usado = true; break; } } + if (!$usado && $k !== 'numeros_dn') $avis[] = "endpoint '{$k}' registrado pero sin usar"; +} + +// ─── Resultado ─────────────────────────────────────────────────────────────── +echo "\nAuditoría de flujos\n"; +echo str_repeat('─', 60) . "\n"; +echo "ERP: " . ($hayErp ? $apiPath : 'no encontrado — se omiten esos chequeos') . "\n"; +echo "Endpoints: " . count($endpoints) . " registrados, {$ok} referencias resueltas\n"; +echo "Peticiones: " . count($peticiones) . " expuestas por el ERP\n\n"; + +foreach (array_unique($avis) as $a) echo " aviso {$a}\n"; +foreach (array_unique($prob) as $p) echo " PROBLEMA {$p}\n"; + +$n = count(array_unique($prob)); +echo "\n" . ($n ? "{$n} problema(s)\n" : "Sin problemas: todos los flujos cierran de punta a punta\n"); +exit($n ? 1 : 0);