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:
co-authored by
Claude Sonnet 4.6
parent
7657e0f55c
commit
b308de2b3b
+93
-27
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user