Meta no permite averiguar el teléfono a partir del BSUID: no hay endpoint de
consulta inversa, cada empresa debe llevar su propia equivalencia. Pero el
BSUID llega en TODOS los webhooks de mensaje, también en los que aún traen
teléfono, así que la equivalencia se puede ir guardando sola mientras la
persona todavía muestra su número.
- users.bsuid guarda esa equivalencia, y el webhook la anota en cada mensaje.
Cuando alguien oculte su número, se le seguirá reconociendo y respondiendo
a su teléfono de siempre.
- Va en columna aparte y no en phone_number porque el teléfono además cruza
con el paciente y el turnero; mezclarlos rompería esos cruces.
- Para quien nunca escribió mostrando el número queda pedirle el contacto:
pedirContacto() manda el botón request_contact_info, y el webhook atiende
el mensaje `contacts` que llega si acepta.
- Al vincular puede aparecer un segundo registro de la misma persona. No se
fusionan: una fusión mal hecha mezcla dos historias clínicas. Gana el que
tiene el teléfono, el otro queda anotado en el log para revisarlo a mano.
- scripts/backfill_bsuid.php carga las equivalencias del histórico (6.423),
con --simular para verlas sin escribir.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Meta desplegó los nombres de usuario: quien oculta su teléfono llega al
webhook sin `from` ni `wa_id`, identificado solo por su BSUID con la forma
"CO.1761088155094242". El webhook exigía teléfono y descartaba el mensaje
en silencio, sin guardarlo ni dejar rastro: 221 mensajes de 65 personas
ignorados por completo, ya el 3-6% de lo que entra cada día y subiendo.
- webhook.php lee `from_user_id` cuando no llega el teléfono, y rescata el
nombre del contacto indexando también por `user_id`.
- WhatsAppService responde por BSUID: Meta exige el campo `recipient` en
lugar de `to`. Se traduce en sendMessage(), el punto por donde pasan todos
los envíos, en vez de en los diez métodos que arman payloads.
- formatPhoneNumber() devuelve el BSUID intacto; antes le quitaba el punto
y las letras y lo dejaba en un destinatario inexistente.
- Las cuatro columnas del flujo pasan a varchar(32): los BSUID de Meta
llegan a 23 caracteres y se truncaban en silencio en varchar(20).
De estas personas no tendremos el teléfono; si un flujo lo necesita, hay
que identificarlas por documento.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Turnero messages were triggering the SSE event that conversations.php listens to,
causing the contact to flash briefly at the top of the list before disappearing
on the next poll (since get_conversations.php filters them out by canal).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WhatsAppService($canal): when 'turnero', swaps phoneNumberId to whatsapp_phone_number_id_turnero
so outgoing messages actually go from the turnero number (not the main bot)
- webhook.php: reads metadata.phone_number_id from the payload and compares it to
whatsapp_phone_number_id_turnero; if it matches, saves canal='turnero' and skips bot processing
- Previously all incoming messages had canal=NULL/bot so Chat Turnero never saw them,
and all outgoing messages from Chat Turnero were sent via the wrong (main bot) number
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- conversations.message_id ahora tiene UNIQUE index (aplicado en BD prod)
- saveMessage() usa INSERT IGNORE: si el mismo wamid llega dos veces en
paralelo solo una llamada procesa el mensaje y llama al bot
- La segunda llamada recibe rowCount=0 y hace continue sin pasar al bot
- Elimina race condition que enviaba bienvenida + términos simultáneamente