From b308de2b3b32cc0ea75db2a9ec3cd1f99a70329b Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:07:37 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20routing=20por=20n=C3=BAmero=20remitente?= =?UTF-8?q?=20+=20rechazo=20de=20n=C3=BAmeros=20no=20habilitados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- admin/v1/WpWebhook.php | 120 +++++++++++++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 27 deletions(-) diff --git a/admin/v1/WpWebhook.php b/admin/v1/WpWebhook.php index efe2eb1..33085e8 100644 --- a/admin/v1/WpWebhook.php +++ b/admin/v1/WpWebhook.php @@ -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); }