feat(whatsapp): mismo motor, menú y configuración que Telegram y el chat web

WhatsApp dejaba de usar su bot propio (árbol de menús sin login ni compras) y
pasa al motor compartido, así los tres canales quedan iguales.

- TelegramBotService recibe el canal por constructor; flujos, menús, Gemini,
  OCR, validación de pagos y datos bancarios salen de ChatConfig sin duplicar
- WhatsApp no tiene teclados inline: las opciones se envían numeradas y la
  respuesta numérica se traduce al callback equivalente
- estado en caché con prefijo por canal (Telegram conserva el suyo para no
  cerrar las sesiones abiertas)
- descarga de comprobantes y audios por la Graph API de Meta
- respuesta manual del asesor y aviso de vencimiento salen también por WhatsApp
- la pantalla de WhatsApp queda sólo con credenciales; el resto se administra
  en Chat / Bot

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-26 23:24:21 -05:00
co-authored by Claude Opus 5
parent e3ad0a423a
commit 5bc1ec14ca
10 changed files with 294 additions and 105 deletions
+31 -1
View File
@@ -19,4 +19,34 @@ assert($valida($firmar($payload, $secret), $payload, '') === false,
assert($valida('', $payload, $secret) === false, 'sin cabecera debe fallar');
assert($valida(hash_hmac('sha256', $payload, $secret), $payload, $secret) === false, 'sin prefijo sha256= debe fallar');
echo "OK: 6 casos de firma\n";
// ── Teclado numerado: mismo mapeo que TelegramBotService::numerarTeclado() ──
$numerar = function (array $teclado): array {
$mapa = []; $lineas = []; $n = 1;
foreach ($teclado as $fila) {
foreach ($fila as $boton) {
if (! isset($boton['callback_data'])) continue;
$mapa[(string) $n] = $boton['callback_data'];
$lineas[] = "{$n}. {$boton['text']}";
$n++;
}
}
return [$mapa, $lineas];
};
[$mapa, $lineas] = $numerar([
[['text' => 'Ver Servicios', 'callback_data' => 'svc_list'],
['text' => 'Promociones', 'callback_data' => 'promo_list']],
[['text' => 'Recargar', 'callback_data' => 'rec_init']],
]);
assert($lineas === ['1. Ver Servicios', '2. Promociones', '3. Recargar'], 'numeración corrida por filas');
assert($mapa['2'] === 'promo_list', 'el número devuelve el callback correcto');
assert($mapa['3'] === 'rec_init', 'la segunda fila sigue la numeración');
assert(count($mapa) === 3, 'un número por botón');
assert(! isset($mapa['4']), 'no inventa opciones');
[$vacio, $sinLineas] = $numerar([[['text' => 'sin accion']]]);
assert($vacio === [] && $sinLineas === [], 'botones sin callback_data se ignoran');
echo "OK: 6 casos de firma + 6 de teclado numerado\n";