feat: routing por número remitente + rechazo de números no habilitados

- resolveCompanyByNumber(): busca el número en company_phones, identifica empresa
- sendRejectionMessage(): responde "su número no se encuentra habilitado" via WA API
- handleMessages(): por cada mensaje resuelve empresa por remitente; si no está
  registrado, guarda log y envía rechazo automático
- permission_type disponible en context para BotRouter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-19 22:07:37 -05:00
co-authored by Claude Sonnet 4.6
parent 7657e0f55c
commit b308de2b3b
+93 -27
View File
@@ -126,8 +126,8 @@ class WpWebhook
private static function resolveCompany(array $payload): void
{
// Fallback: resolver por phone_number_id (usado para statuses y auditoría)
$phoneNumberId = '';
foreach ($payload['entry'] ?? [] as $entry) {
foreach ($entry['changes'] ?? [] as $change) {
$metadata = $change['value']['metadata'] ?? [];
@@ -137,19 +137,77 @@ class WpWebhook
}
}
}
if ($phoneNumberId !== '') {
self::$currentCompany = CompanyRepository::findByPhoneNumberId($phoneNumberId);
}
}
if ($phoneNumberId === '') {
self::log('WARN', 'No se pudo identificar phone_number_id en el payload');
return;
// Resolución principal: busca el número remitente en company_phones
private static function resolveCompanyByNumber(string $from): bool
{
if ($from === '') {
self::$currentCompany = null;
return false;
}
self::$currentCompany = CompanyRepository::findByPhoneNumberId($phoneNumberId);
if (self::$currentCompany === null) {
self::log('WARN', "No hay empresa configurada para phone_number_id: {$phoneNumberId}");
} else {
self::log('INFO', "Mensaje enrutado a empresa: " . (self::$currentCompany['name'] ?? '?'));
try {
$stmt = db()->prepare("
SELECT cp.company_id, cp.permission_type
FROM company_phones cp
WHERE cp.wa_number = ? AND cp.is_active = 1
LIMIT 1
");
$stmt->execute([$from]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
self::log('ERROR', 'resolveCompanyByNumber DB: ' . $e->getMessage());
self::$currentCompany = null;
return false;
}
if (!$row) {
self::$currentCompany = null;
self::log('WARN', "Número {$from} no registrado en ninguna empresa");
return false;
}
$company = CompanyRepository::findById((int)$row['company_id']);
if ($company === null) {
self::$currentCompany = null;
return false;
}
// Adjuntar tipo de permiso al contexto de empresa
$company['_permission_type'] = (int)$row['permission_type'];
self::$currentCompany = $company;
self::log('INFO', "Número {$from} → empresa: {$company['name']} (permiso tipo {$row['permission_type']})");
return true;
}
private static function sendRejectionMessage(string $to, string $phoneNumberId): void
{
$token = env('WHATSAPP_ACCESS_TOKEN', '');
if ($token === '' || $to === '' || $phoneNumberId === '') return;
$body = json_encode([
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'text',
'text' => ['body' => 'Comuníquese con el administrador, su número no se encuentra habilitado.'],
]);
$ch = curl_init("https://graph.facebook.com/v17.0/{$phoneNumberId}/messages");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 8,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
self::log('INFO', "Rechazo enviado a {$to} HTTP {$code}");
}
// ─── Reenvío a empresa ────────────────────────────────────────────────────
@@ -187,12 +245,11 @@ class WpWebhook
$displayPhone = $metadata['display_phone_number'] ?? '';
foreach ($messages as $msg) {
$from = $msg['from'] ?? ''; // Número del remitente
$msgId = $msg['id'] ?? '';
$type = $msg['type'] ?? 'unknown';
$ts = $msg['timestamp'] ?? time();
$from = $msg['from'] ?? '';
$msgId = $msg['id'] ?? '';
$type = $msg['type'] ?? 'unknown';
$ts = $msg['timestamp'] ?? time();
// Nombre del contacto (si existe)
$name = '';
foreach ($contacts as $c) {
if (($c['wa_id'] ?? '') === $from) {
@@ -201,6 +258,15 @@ class WpWebhook
}
}
// Resolver empresa por número remitente
$allowed = self::resolveCompanyByNumber($from);
if (!$allowed) {
self::saveWebhookLog('messages', $from, $name, $type, '[NÚMERO NO HABILITADO]');
self::sendRejectionMessage($from, $phoneNumberId);
continue;
}
$context = [
'from' => $from,
'name' => $name,
@@ -209,24 +275,24 @@ class WpWebhook
'timestamp' => $ts,
'phone_number_id' => $phoneNumberId,
'display_phone' => $displayPhone,
'permission_type' => self::$currentCompany['_permission_type'] ?? 3,
];
switch ($type) {
case 'text': self::handleText($msg, $context); break;
case 'image': self::handleMedia($msg, $context, 'image'); break;
case 'audio': self::handleMedia($msg, $context, 'audio'); break;
case 'video': self::handleMedia($msg, $context, 'video'); break;
case 'document': self::handleMedia($msg, $context, 'document'); break;
case 'sticker': self::handleMedia($msg, $context, 'sticker'); break;
case 'location': self::handleLocation($msg, $context); break;
case 'interactive': self::handleInteractive($msg, $context); break;
case 'button': self::handleButton($msg, $context); break;
case 'reaction': self::handleReaction($msg, $context); break;
default: self::log('INFO', "Tipo de mensaje no manejado: $type | from=$from"); break;
case 'text': self::handleText($msg, $context); break;
case 'image': self::handleMedia($msg, $context, 'image'); break;
case 'audio': self::handleMedia($msg, $context, 'audio'); break;
case 'video': self::handleMedia($msg, $context, 'video'); break;
case 'document': self::handleMedia($msg, $context, 'document'); break;
case 'sticker': self::handleMedia($msg, $context, 'sticker'); break;
case 'location': self::handleLocation($msg, $context); break;
case 'interactive': self::handleInteractive($msg, $context); break;
case 'button': self::handleButton($msg, $context); break;
case 'reaction': self::handleReaction($msg, $context); break;
default: self::log('INFO', "Tipo no manejado: $type | from=$from"); break;
}
}
// Estados que pueden venir dentro del mismo field 'messages'
if (!empty($value['statuses'])) {
self::handleStatuses($value);
}