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);