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