El arreglo anterior dejó un defecto: obtenerOCrearDesdeWhatsapp() copiaba
users.phone_number al teléfono de la ficha, y para quien oculta su número ahí
va el BSUID. normalizarTelefono() le quitaba el punto y las letras, así que
"CO.1761088155094242" quedaba guardado como "1761088155094242": un número de
16 dígitos, falso, con apariencia de real, dentro de una historia clínica y
sin que nadie lo notara. Ahora esa ficha se crea sin teléfono, que es la
verdad: no lo tenemos.
- esBsuid() queda definido una sola vez, en config.php, que es donde lo ven
tanto el servicio de WhatsApp como las clases del laboratorio.
- La lista y el detalle de pacientes dicen "Solo por WhatsApp" en vez de
mostrar el identificador crudo: recepción necesita entender por qué no
puede llamar, no ver un código.
- Al crear la ficha desde una conversación se le pide el número con el botón
de Meta, una sola vez (users.contacto_pedido_at). El texto es editable
desde configuración. Solo se marca como pedido si Meta aceptó el envío,
para poder reintentar si falló.
- tests/test_bsuid.php: 30 verificaciones sobre identidad, envío, vinculación
y creación de fichas.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Cuando se usaban rawComponents, el parámetro $meta no se agregaba
al payload, causando que los mensajes del turnero se guardaran con
canal='bot' y aparecieran en conversations.php.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WhatsApp API returns "(#100) Invalid parameter — Parameter name is
missing or empty" for templates that use named variables.
- chat.php sendTemplate(): collect params as [{name, value}] objects
using data-var-index as the parameter name
- WhatsAppService::sendTemplateMessage(): handle {name, value} objects
in the indexed-array path by emitting {type, parameter_name, text}
(backward-compatible — plain strings still work as positional params)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WhatsAppService::makeRequest: include error_subcode and error_data.details
in the thrown exception message to surface the real reason for #100 errors
- chat_send_message: log template name/lang/params before sending;
pass canal+operator_id meta so outgoing templates are tagged correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
saveOutgoingMessage() now reads __app_meta.canal and stores it in conversations.
sendMediaById() accepts a $meta param and forwards it to saveOutgoingMessage.
chat_upload_media.php passes $extra (canal=turnero) to sendMediaById.
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>