Files
Lizandro GuarnizoandClaude Opus 5 6b74db3e4f Versionar la verificación del BSUID
Quedó fuera del commit anterior porque tests/ está en .gitignore, pese a
que el mensaje la mencionaba. Va en scripts/, que sí se versiona, junto al
backfill al que acompaña.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:26:54 -05:00

163 lines
7.9 KiB
PHP

<?php
/**
* Verificación de la identidad por BSUID (nombres de usuario de WhatsApp).
* Ejecutar: php test_bsuid.php
*/
require_once __DIR__ . '/../services/WhatsAppService.php';
$fallos = 0;
function chequear($descripcion, $condicion) {
global $fallos;
if ($condicion) { echo " ok $descripcion\n"; }
else { echo " FALLA $descripcion\n"; $fallos++; }
}
// --- 1. Reconocer un BSUID y no confundirlo con un teléfono ---
chequear('reconoce el BSUID real de hoy', WhatsAppService::esBsuid('CO.1761088155094242'));
chequear('reconoce el BSUID largo de Meta', WhatsAppService::esBsuid('US.13491208655302741918'));
chequear('un celular NO es BSUID', !WhatsAppService::esBsuid('573214533764'));
chequear('vacío NO es BSUID', !WhatsAppService::esBsuid(''));
chequear('texto suelto NO es BSUID', !WhatsAppService::esBsuid('CO.abc'));
// --- 2. formatPhoneNumber no debe destrozar el BSUID ---
$svc = (new ReflectionClass('WhatsAppService'))->newInstanceWithoutConstructor();
$fmt = new ReflectionMethod('WhatsAppService', 'formatPhoneNumber');
$fmt->setAccessible(true);
chequear('el BSUID sale intacto',
$fmt->invoke($svc, 'CO.1761088155094242') === 'CO.1761088155094242');
chequear('el celular sin indicativo sigue recibiendo el 57',
$fmt->invoke($svc, '3214533764') === '573214533764');
chequear('el celular con indicativo se conserva',
$fmt->invoke($svc, '573214533764') === '573214533764');
// --- 3. El payload debe usar `recipient` para BSUID y `to` para teléfonos ---
// Se replica la transformación de sendMessage(), que es privada y hace red.
$transformar = function (array $payload) {
if (isset($payload['to']) && WhatsAppService::esBsuid($payload['to'])) {
$payload['recipient'] = $payload['to'];
unset($payload['to']);
}
return $payload;
};
$conBsuid = $transformar(['to' => 'CO.1761088155094242', 'type' => 'text']);
chequear('con BSUID se envía en `recipient`', ($conBsuid['recipient'] ?? null) === 'CO.1761088155094242');
chequear('con BSUID desaparece `to`', !isset($conBsuid['to']));
$conTel = $transformar(['to' => '573214533764', 'type' => 'text']);
chequear('con teléfono se mantiene `to`', ($conTel['to'] ?? null) === '573214533764');
chequear('con teléfono no aparece `recipient`', !isset($conTel['recipient']));
// --- 4. El webhook debe extraer el remitente del payload real que llegó hoy ---
$payloadReal = json_decode('{"messaging_product":"whatsapp",
"contacts":[{"profile":{"name":"con la fe puesta en Dios","username":"Luzmarytosfer"},
"user_id":"CO.1761088155094242"}],
"messages":[{"from_user_id":"CO.1761088155094242","id":"wamid.XXX",
"timestamp":"1786481656","text":{"body":"Buenos dias"},"type":"text"}]}', true);
$m = $payloadReal['messages'][0];
$remitente = $m['from'] ?? ($m['wa_id'] ?? ($m['from_user_id'] ?? null));
chequear('el mensaje que se perdía ahora sí tiene remitente', $remitente === 'CO.1761088155094242');
$nombres = [];
foreach ($payloadReal['contacts'] as $c) {
$n = $c['profile']['name'] ?? null;
if (!$n) continue;
foreach ([$c['wa_id'] ?? null, $c['user_id'] ?? null] as $k) {
if ($k) $nombres[$k] = $n;
}
}
chequear('se rescata el nombre aunque no haya wa_id',
($nombres['CO.1761088155094242'] ?? null) === 'con la fe puesta en Dios');
// --- 5. El identificador debe caber en la columna ---
chequear('el BSUID más largo cabe en varchar(32)', strlen('US.13491208655302741918') <= 32);
// --- 6. Extraer el teléfono del contacto compartido (webhook `contacts` de Meta) ---
$extraerTelefono = function (array $mensaje) {
foreach ($mensaje['contacts'] ?? [] as $c) {
foreach ($c['phones'] ?? [] as $t) {
$cand = $t['wa_id'] ?? ($t['phone'] ?? null);
if ($cand) return preg_replace('/[^0-9]/', '', $cand);
}
}
return null;
};
$porBoton = ['type' => 'contacts', 'from_user_id' => 'CO.1761088155094242',
'contacts' => [['origin' => 'contact_request',
'phones' => [['phone' => '+57 321 4533764', 'wa_id' => '573214533764', 'type' => 'CELL']]]]];
chequear('del botón se extrae el teléfono', $extraerTelefono($porBoton) === '573214533764');
// Compartido a mano: puede venir con vcard y sin wa_id
$aMano = ['type' => 'contacts', 'from_user_id' => 'CO.1761088155094242',
'contacts' => [['origin' => 'other', 'vcard' => 'BEGIN:VCARD...',
'phones' => [['phone' => '+57 321 453 3764', 'type' => 'CELL']]]]];
chequear('compartido a mano también se extrae', $extraerTelefono($aMano) === '573214533764');
chequear('sin teléfono devuelve nulo', $extraerTelefono(['type' => 'contacts', 'contacts' => [[]]]) === null);
// --- 7. El botón para pedir el contacto debe armarse como exige Meta ---
$pedir = new ReflectionMethod('WhatsAppService', 'pedirContacto');
chequear('pedirContacto existe y es pública', $pedir->isPublic());
$payloadBoton = [
'messaging_product' => 'whatsapp',
'recipient_type' => 'individual',
'to' => $fmt->invoke($svc, 'CO.1761088155094242'),
'type' => 'interactive',
'interactive' => ['type' => 'request_contact_info',
'body' => ['text' => 'texto'],
'action' => ['name' => 'request_contact_info']],
];
$enviado = $transformar($payloadBoton);
chequear('el botón viaja con `recipient` cuando es BSUID',
($enviado['recipient'] ?? null) === 'CO.1761088155094242' && !isset($enviado['to']));
chequear('el tipo interactivo es el que Meta espera',
$enviado['interactive']['type'] === 'request_contact_info'
&& $enviado['interactive']['action']['name'] === 'request_contact_info');
// --- 8. La ficha del paciente no debe recibir el BSUID como teléfono ---
// Réplica de la decisión de Paciente::obtenerOCrearDesdeWhatsapp()
$fichaDesde = function ($phoneNumber, $nombre = null) {
$sinTelefono = esBsuid($phoneNumber);
return [
'nombre_completo' => $nombre ?? ($sinTelefono ? 'Paciente sin identificar' : 'Paciente ' . $phoneNumber),
'telefono' => $sinTelefono ? null : $phoneNumber,
];
};
$fichaOculta = $fichaDesde('CO.1761088155094242');
chequear('la ficha queda SIN teléfono si es BSUID', $fichaOculta['telefono'] === null);
chequear('y sin el identificador metido en el nombre',
strpos($fichaOculta['nombre_completo'], 'CO.') === false);
$fichaNormal = $fichaDesde('573214533764');
chequear('con teléfono real la ficha lo conserva', $fichaNormal['telefono'] === '573214533764');
chequear('y el nombre por defecto no cambia', $fichaNormal['nombre_completo'] === 'Paciente 573214533764');
// El daño concreto que se evita: normalizarTelefono() dejaba 16 dígitos con
// toda la pinta de un número real dentro de la historia clínica.
$comoQuedaba = preg_replace('/[^0-9+]/', '', 'CO.1761088155094242');
chequear('antes se guardaba un número falso creíble', $comoQuedaba === '1761088155094242');
chequear('y ahora eso ya no llega al campo teléfono', $fichaOculta['telefono'] !== $comoQuedaba);
// --- 9. Solo se pide el contacto a quien hace falta, y una sola vez ---
$decidir = function ($phoneNumber, $pedidoAt) {
if (!esBsuid($phoneNumber)) return 'no_hace_falta';
if (!empty($pedidoAt)) return 'ya_se_pidio';
return 'pedido';
};
chequear('a quien tiene teléfono no se le pide', $decidir('573214533764', null) === 'no_hace_falta');
chequear('a quien lo oculta sí se le pide', $decidir('CO.1761088155094242', null) === 'pedido');
chequear('y no se le insiste una segunda vez',
$decidir('CO.1761088155094242', '2026-08-11 16:00:00') === 'ya_se_pidio');
// --- 10. La comprobación vive en un solo sitio ---
chequear('esBsuid() global y el del servicio coinciden',
esBsuid('CO.1761088155094242') === WhatsAppService::esBsuid('CO.1761088155094242')
&& esBsuid('573214533764') === WhatsAppService::esBsuid('573214533764'));
echo $fallos === 0 ? "\nTodo correcto.\n" : "\n$fallos verificaciones fallaron.\n";
exit($fallos === 0 ? 0 : 1);