feat: pre-menú de selección de empresa para números multi-empresa
Cuando un número pertenece a más de una empresa, el bot muestra un menú interactivo "¿Con cuál empresa deseas comunicarte?" antes de cualquier flujo normal. La selección se cachea 8h en la tabla multi_company_sessions. Keywords de reinicio (salir, inicio, menú...) limpian la sesión y vuelven a mostrar el pre-menú. Números de una sola empresa: flujo intacto. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
b349230362
commit
8f43ccf4c2
+141
-27
@@ -167,46 +167,141 @@ class WpWebhook
|
||||
}
|
||||
}
|
||||
|
||||
// Resolución principal: busca el número remitente en company_phones
|
||||
private static function resolveCompanyByNumber(string $from): bool
|
||||
// Devuelve todas las empresas a las que pertenece el número
|
||||
private static function findCompaniesByNumber(string $from): array
|
||||
{
|
||||
if ($from === '') {
|
||||
self::$currentCompany = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($from === '') return [];
|
||||
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
|
||||
ORDER BY cp.company_id ASC
|
||||
");
|
||||
$stmt->execute([$from]);
|
||||
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
} catch (\PDOException $e) {
|
||||
self::log('ERROR', 'resolveCompanyByNumber DB: ' . $e->getMessage());
|
||||
self::$currentCompany = null;
|
||||
self::log('ERROR', 'findCompaniesByNumber DB: ' . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-menú multi-empresa: retorna true si la empresa quedó resuelta, false si hay que esperar al usuario
|
||||
private static function handleMultiCompany(string $from, string $name, array $companies, string $rawText, string $phoneNumberId): bool
|
||||
{
|
||||
// 1. El usuario tocó un botón de selección de empresa
|
||||
if (str_starts_with($rawText, '__co_')) {
|
||||
$selectedId = (int)substr($rawText, 5);
|
||||
$company = CompanyRepository::findById($selectedId);
|
||||
if (!$company) {
|
||||
self::sendCompanyPreMenu($from, $phoneNumberId, $name, $companies);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
db()->prepare("
|
||||
INSERT INTO multi_company_sessions (wa_number, companies_json, selected_id, expires_at)
|
||||
VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL 8 HOUR))
|
||||
ON DUPLICATE KEY UPDATE selected_id = VALUES(selected_id), expires_at = VALUES(expires_at)
|
||||
")->execute([$from, json_encode($companies), $selectedId]);
|
||||
} catch (\PDOException $e) {
|
||||
self::log('ERROR', 'handleMultiCompany save session: ' . $e->getMessage());
|
||||
}
|
||||
$company['_permission_type'] = self::getPermissionType($companies, $selectedId);
|
||||
self::$currentCompany = $company;
|
||||
$displayName = $company['display_name'] ?: $company['name'];
|
||||
WhatsAppSender::sendText($from, "✅ Conectado con *{$displayName}*.\nEscribe tu consulta o *menú* para ver las opciones.", $phoneNumberId);
|
||||
self::log('INFO', "Multi-empresa: {$from} seleccionó empresa #{$selectedId} ({$displayName})");
|
||||
return false; // el clic en botón no se procesa como comando del bot
|
||||
}
|
||||
|
||||
// 2. Keyword de reinicio → borrar sesión y mostrar pre-menú
|
||||
if (self::isResetKeyword($rawText)) {
|
||||
try { db()->prepare("DELETE FROM multi_company_sessions WHERE wa_number = ?")->execute([$from]); } catch (\PDOException $e) {}
|
||||
self::sendCompanyPreMenu($from, $phoneNumberId, $name, $companies);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$row) {
|
||||
self::$currentCompany = null;
|
||||
self::log('WARN', "Número {$from} no registrado en ninguna empresa");
|
||||
return false;
|
||||
// 3. Sesión activa → usar empresa guardada
|
||||
try {
|
||||
$stmt = db()->prepare("SELECT selected_id FROM multi_company_sessions WHERE wa_number = ? AND selected_id IS NOT NULL AND expires_at > NOW() LIMIT 1");
|
||||
$stmt->execute([$from]);
|
||||
$session = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
} catch (\PDOException $e) {
|
||||
self::log('ERROR', 'handleMultiCompany read session: ' . $e->getMessage());
|
||||
$session = null;
|
||||
}
|
||||
|
||||
$company = CompanyRepository::findById((int)$row['company_id']);
|
||||
if ($company === null) {
|
||||
self::$currentCompany = null;
|
||||
return false;
|
||||
if ($session) {
|
||||
$selectedId = (int)$session['selected_id'];
|
||||
try { db()->prepare("UPDATE multi_company_sessions SET expires_at = DATE_ADD(NOW(), INTERVAL 8 HOUR) WHERE wa_number = ?")->execute([$from]); } catch (\PDOException $e) {}
|
||||
$company = CompanyRepository::findById($selectedId);
|
||||
if (!$company) {
|
||||
try { db()->prepare("DELETE FROM multi_company_sessions WHERE wa_number = ?")->execute([$from]); } catch (\PDOException $e) {}
|
||||
self::sendCompanyPreMenu($from, $phoneNumberId, $name, $companies);
|
||||
return false;
|
||||
}
|
||||
$company['_permission_type'] = self::getPermissionType($companies, $selectedId);
|
||||
self::$currentCompany = $company;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
// 4. Sin sesión → mostrar pre-menú
|
||||
try {
|
||||
db()->prepare("
|
||||
INSERT INTO multi_company_sessions (wa_number, companies_json, selected_id, expires_at)
|
||||
VALUES (?, ?, NULL, DATE_ADD(NOW(), INTERVAL 10 MINUTE))
|
||||
ON DUPLICATE KEY UPDATE companies_json = VALUES(companies_json), selected_id = NULL, expires_at = VALUES(expires_at)
|
||||
")->execute([$from, json_encode($companies)]);
|
||||
} catch (\PDOException $e) {
|
||||
self::log('ERROR', 'handleMultiCompany insert pending: ' . $e->getMessage());
|
||||
}
|
||||
self::sendCompanyPreMenu($from, $phoneNumberId, $name, $companies);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function sendCompanyPreMenu(string $to, string $phoneNumberId, string $name, array $companies): void
|
||||
{
|
||||
$items = [];
|
||||
foreach ($companies as $cp) {
|
||||
$co = CompanyRepository::findById((int)$cp['company_id']);
|
||||
if ($co) $items[] = ['id' => (int)$cp['company_id'], 'name' => $co['display_name'] ?: $co['name']];
|
||||
}
|
||||
if (empty($items)) return;
|
||||
|
||||
$greeting = $name ? "Hola *{$name}* 👋\n" : "Hola 👋\n";
|
||||
$body = $greeting . "¿Con cuál empresa deseas comunicarte?";
|
||||
|
||||
if (count($items) <= 3) {
|
||||
$buttons = [];
|
||||
foreach ($items as $item) {
|
||||
$buttons[] = ['type' => 'reply', 'reply' => ['id' => '__co_' . $item['id'], 'title' => mb_substr($item['name'], 0, 20)]];
|
||||
}
|
||||
$interactive = ['type' => 'button', 'body' => ['text' => $body], 'action' => ['buttons' => $buttons]];
|
||||
} else {
|
||||
$rows = [];
|
||||
foreach ($items as $item) {
|
||||
$rows[] = ['id' => '__co_' . $item['id'], 'title' => mb_substr($item['name'], 0, 24)];
|
||||
}
|
||||
$interactive = ['type' => 'list', 'body' => ['text' => $body], 'action' => ['button' => 'Ver empresas', 'sections' => [['title' => 'Empresas', 'rows' => $rows]]]];
|
||||
}
|
||||
|
||||
WhatsAppSender::sendInteractive($to, $interactive, $phoneNumberId);
|
||||
self::log('INFO', "Pre-menú de empresa enviado a {$to} (" . count($items) . " opciones)");
|
||||
}
|
||||
|
||||
private static function isResetKeyword(string $text): bool
|
||||
{
|
||||
$n = mb_strtolower(trim($text));
|
||||
$n = str_replace(['á','é','í','ó','ú','ü','ñ'], ['a','e','i','o','u','u','n'], $n);
|
||||
return in_array($n, ['salir','inicio','menu','reiniciar','volver','reset','0','atras','regresar'], true);
|
||||
}
|
||||
|
||||
private static function getPermissionType(array $companies, int $companyId): int
|
||||
{
|
||||
foreach ($companies as $cp) {
|
||||
if ((int)$cp['company_id'] === $companyId) return (int)$cp['permission_type'];
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
|
||||
private static function sendRejectionMessage(string $to, string $phoneNumberId): void
|
||||
@@ -283,15 +378,34 @@ class WpWebhook
|
||||
}
|
||||
}
|
||||
|
||||
// Resolver empresa por número remitente
|
||||
$allowed = self::resolveCompanyByNumber($from);
|
||||
// Resolver empresa(s) por número remitente
|
||||
$matchedCompanies = self::findCompaniesByNumber($from);
|
||||
|
||||
if (!$allowed) {
|
||||
if (empty($matchedCompanies)) {
|
||||
self::saveWebhookLog('messages', $from, $name, $type, '[NÚMERO NO HABILITADO]');
|
||||
self::sendRejectionMessage($from, $phoneNumberId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($matchedCompanies) === 1) {
|
||||
$co = CompanyRepository::findById((int)$matchedCompanies[0]['company_id']);
|
||||
if (!$co) continue;
|
||||
$co['_permission_type'] = (int)$matchedCompanies[0]['permission_type'];
|
||||
self::$currentCompany = $co;
|
||||
self::log('INFO', "Número {$from} → empresa: {$co['name']} (permiso {$matchedCompanies[0]['permission_type']})");
|
||||
} else {
|
||||
$rawText = match ($type) {
|
||||
'text' => $msg['text']['body'] ?? '',
|
||||
'interactive' => $msg['interactive']['button_reply']['id'] ?? $msg['interactive']['list_reply']['id'] ?? '',
|
||||
'button' => $msg['button']['payload'] ?? $msg['button']['text'] ?? '',
|
||||
default => '',
|
||||
};
|
||||
if (!self::handleMultiCompany($from, $name, $matchedCompanies, $rawText, $phoneNumberId)) {
|
||||
continue;
|
||||
}
|
||||
self::log('INFO', "Número {$from} → empresa (multi-sel): " . (self::$currentCompany['name'] ?? '?'));
|
||||
}
|
||||
|
||||
$context = [
|
||||
'from' => $from,
|
||||
'name' => $name,
|
||||
|
||||
@@ -244,6 +244,17 @@ $db->exec("
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS multi_company_sessions (
|
||||
wa_number VARCHAR(20) NOT NULL PRIMARY KEY,
|
||||
companies_json TEXT NOT NULL,
|
||||
selected_id INT DEFAULT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_expires (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
|
||||
// ─── Campo erp_active_users en companies ──────────────────────────────────────
|
||||
try {
|
||||
$db->exec("ALTER TABLE companies ADD COLUMN erp_active_users INT DEFAULT 0 AFTER is_active");
|
||||
|
||||
Reference in New Issue
Block a user