logger = $logger; $this->messageTextFactory = $messageTextFactory; $this->clientPhoneNumber = $clientPhoneNumber; $config = PluginConfigManager::create()->loadConfig(); $ipServer = $config['ipserver'] ?? 'localhost'; $apiUrl = "https://$ipServer/crm/api/v1.0/"; $client = new Client([ 'base_uri' => $apiUrl, 'verify' => false, ]); $this->ucrmApi = new UcrmApi($client, $config['apitoken'] ?? ''); } public function verifyPaymentActionToDo(NotificationData $notificationData): void { $arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null); foreach ($arrayPhones as $type => $phones) { $type = trim(strtolower($type)); if (!is_array($phones)) continue; foreach ($phones as $phone) { switch ($type) { case 'whatsapp': $this->notifyAndUpdate($notificationData, $phone); break; case 'whatsnotifica': $this->notify($notificationData, $phone); break; case 'whatsactualiza': $this->onlyUpdate($notificationData, $phone); break; } } } } public function verifyClientActionToDo(NotificationData $notificationData): void { $arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null); foreach ($arrayPhones as $type => $phones) { $type = trim(strtolower($type)); if (!is_array($phones)) continue; foreach ($phones as $phone) { if ($type === 'whatsapp' || $type === 'whatsactualiza') $this->onlyUpdate($notificationData, $phone); } } } public function verifyServiceActionToDo(NotificationData $notificationData): void { $arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null); foreach ($arrayPhones as $type => $phones) { $type = trim(strtolower($type)); if (!is_array($phones)) continue; foreach ($phones as $phone) { if ($type === 'whatsapp' || $type === 'whatsactualiza') $this->onlyUpdateService($notificationData, $phone); } } } public function verifyJobActionToDo($jsonNotificationData, $reprogramming = null, $changeInstaller = null): void { $this->logger->info('Iniciando verifyJobActionToDo'); $clientId = $jsonNotificationData['extraData']['entity']['clientId']; $installerId = $jsonNotificationData['extraData']['entity']['assignedUserId']; $jobId = $jsonNotificationData['entityId']; $dateString = $jsonNotificationData['extraData']['entity']['date'] ?? null; $formattedDate = $dateString ? sprintf("*%s*", (new DateTime($dateString))->format('d/m/Y')) : ''; $config = PluginConfigManager::create()->loadConfig(); $admin = $this->ucrmApi->get("users/admins/$installerId", []); $installerName = trim(($admin['firstName'] ?? '') . ' ' . ($admin['lastName'] ?? '')); $installerWhatsApp = ''; $installers = json_decode($config['installersDataWhatsApp'] ?? '{"instaladores":[]}', true); foreach ($installers['instaladores'] as $inst) { if ($inst['id'] == $installerId) { $installerWhatsApp = $inst['whatsapp']; break; } } if (empty($installerWhatsApp)) $this->logger->warning("No se encontró número de WhatsApp para el instalador ID: $installerId"); $clientCRM = $this->ucrmApi->get("clients/$clientId", []); $clientName = trim(($clientCRM['firstName'] ?? '') . ' ' . ($clientCRM['lastName'] ?? '')); $passCRM = ''; foreach ($clientCRM['attributes'] as $attr) { if ($attr['key'] === 'passwordAntenaCliente') { $passCRM = $attr['value']; break; } } $allPhones = $this->clientPhoneNumber->getAllUcrmClientNumbers($clientCRM); $phonesStr = implode(', ', array_map(fn($n) => $this->validarNumeroTelefono($n), $allPhones)); $api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']); $title = $jsonNotificationData['extraData']['entity']['title'] ?? ''; $isPending = (stripos($title, '[NOTIFICACION-PENDIENTE]') !== false); $isNoWhatsApp = (stripos($title, '[CLIENTE-SIN-WHATSAPP]') !== false); $reprogramming = filter_var($reprogramming, FILTER_VALIDATE_BOOLEAN); $changeInstaller = filter_var($changeInstaller, FILTER_VALIDATE_BOOLEAN); $clientPhones = $this->clientPhoneNumber->getUcrmClientNumbers(null, $clientCRM); $hasClientWhatsApp = false; foreach ($clientPhones as $type => $phones) { $type = trim(strtolower($type)); if (($type === 'whatsapp' || $type === 'whatsnotifica') && !empty($phones)) { $hasClientWhatsApp = true; break; } } $shouldNotifyTech = ($isPending || $reprogramming || $changeInstaller); $shouldNotifyClient = ($isPending || $isNoWhatsApp || $reprogramming || $changeInstaller); // 1. Notificar al Instalador Anterior (Desasignación) if ($changeInstaller) { $prevId = $jsonNotificationData['extraData']['entityBeforeEdit']['assignedUserId']; $prevAdmin = $this->ucrmApi->get("users/admins/$prevId", []); $prevWhatsApp = ''; foreach ($installers['instaladores'] as $inst) { if ($inst['id'] == $prevId) { $prevWhatsApp = $inst['whatsapp']; break; } } if ($prevWhatsApp) { $api->sendJobNotificationWhatsAppToInstaller($this->validarNumeroTelefono($prevWhatsApp), [ "installerName" => "👷🏻‍♂️" . trim(($prevAdmin['firstName'] ?? '') . ' ' . ($prevAdmin['lastName'] ?? '')), "subjectOfChange" => self::SUBJECT_OF_INSTALLER_CHANGE[1], "jobId" => $jobId, "clientFullName" => "[$clientId] $clientName", "additionalChangeData" => self::ADDITIONAL_CHANGE_DATA[1] . ' *' . $installerName . '*', ], $reprogramming, $changeInstaller); sleep(1); } } // 2. Notificar al Cliente (si aplica) $clientNotified = false; if ($shouldNotifyClient && $hasClientWhatsApp) { foreach ($clientPhones as $type => $phones) { $type = trim(strtolower($type)); if (!is_array($phones) || ($type !== 'whatsapp' && $type !== 'whatsnotifica')) continue; foreach ($phones as $phone) { if ($api->sendJobNotificationWhatsAppToClient($this->validarNumeroTelefono($phone), ["clientFullName" => $clientName, "jobId" => $jobId, "date" => $formattedDate, "installerName" => $installerName], $reprogramming, $changeInstaller)) { $clientNotified = true; } } } } // 2. Notificar al Técnico (si aplica) if ($shouldNotifyTech) { $passVault = $this->getVaultCredentialsByClientId($clientId); $api->sendJobNotificationWhatsAppToInstaller($this->validarNumeroTelefono($installerWhatsApp), [ "installerName" => $installerName, "clientFullName" => "$clientName [ID:$clientId]", "jobId" => $jobId, "clientAddress" => $clientCRM['fullAddress'] ?? 'N/A', "clientWhatsApp" => !empty($phonesStr) ? $phonesStr : 'Sin WhatsApp', "date" => $formattedDate, "jobDescription" => $jsonNotificationData['extraData']['entity']['description'] ?? 'S/D', "gmapsLocation" => ($clientCRM['addressGpsLat'] && $clientCRM['addressGpsLon']) ? "https://www.google.com/maps?q={$clientCRM['addressGpsLat']},{$clientCRM['addressGpsLon']}" : 'N/A', "passwordAntenaCliente" => $this->comparePasswords($passCRM, $passVault) ], $reprogramming, false); } // 3. Gestión del Título / Prefijos if ($isPending) { if ($clientNotified || $reprogramming || $changeInstaller) { $newTitle = str_ireplace('[NOTIFICACION-PENDIENTE]', '', $title); $this->ucrmApi->patch("scheduling/jobs/$jobId", ['title' => trim($newTitle)]); } else if (!$hasClientWhatsApp) { $newTitle = str_ireplace('[NOTIFICACION-PENDIENTE]', '[CLIENTE-SIN-WHATSAPP]', $title); $this->ucrmApi->patch("scheduling/jobs/$jobId", ['title' => trim($newTitle)]); } } else if ($isNoWhatsApp && ($clientNotified || $reprogramming || $changeInstaller)) { $newTitle = str_ireplace('[CLIENTE-SIN-WHATSAPP]', '', $title); $this->ucrmApi->patch("scheduling/jobs/$jobId", ['title' => trim($newTitle)]); } } public function verifyInvoiceActionToDo(NotificationData $notificationData): void { $arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null); foreach ($arrayPhones as $type => $phones) { $type = trim(strtolower($type)); if (!is_array($phones)) continue; foreach ($phones as $phone) { if ($type === 'whatsapp' || $type === 'whatsactualiza') $this->onlyUpdate($notificationData, $phone, false); } } } public function notify(NotificationData $notificationData, $phoneToNotify = null): void { $config = PluginConfigManager::create()->loadConfig(); $api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']); $phone = $this->validarNumeroTelefono($phoneToNotify); if (!$phone) return; if ($config['notificationTypeText'] ?? false) $api->sendTextPaymentNotificationWhatsApp($phone, $notificationData); else $api->sendPaymentNotificationWhatsApp($phone, $notificationData); } public function notifyAndUpdate(NotificationData $notificationData, $phoneToNotifyAndUpdate = null): void { $config = PluginConfigManager::create()->loadConfig(); $api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']); $phone = $this->validarNumeroTelefono($phoneToNotifyAndUpdate); if (!$phone) return; if ($config['notificationTypeText'] ?? false) { if ($api->sendTextPaymentNotificationWhatsApp($phone, $notificationData)) { $contact = json_decode($api->getContactWhatsapp($phone), true); if ($contact) $api->patchWhatsapp($contact, $notificationData); } } else { if ($api->sendPaymentNotificationWhatsApp($phone, $notificationData)) { $contact = json_decode($api->getContactWhatsapp($phone), true); if ($contact) $api->patchWhatsapp($contact, $notificationData); } } } public function notifyOverDue(NotificationData $notificationData): void { $config = PluginConfigManager::create()->loadConfig(); $api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']); $phone = $this->clientPhoneNumber->getUcrmClientNumber($notificationData); if ($phone) $api->sendOverdueNotificationWhatsApp($phone, $notificationData); } public function onlyUpdate(NotificationData $notificationData, $phoneToUpdate): void { $this->logger->debug("onlyUpdate: Iniciando actualización para teléfono: $phoneToUpdate"); $config = PluginConfigManager::create()->loadConfig(); $api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']); $phone = $this->validarNumeroTelefono($phoneToUpdate); $this->logger->debug("onlyUpdate: Teléfono validado: $phone"); $contact = json_decode($api->getContactWhatsapp($phone), true); $this->logger->debug("onlyUpdate: Contacto obtenido de CallBell: " . json_encode($contact)); if ($contact) { $this->logger->info("onlyUpdate: Ejecutando patchWhatsapp para teléfono: $phone"); $api->patchWhatsapp($contact, $notificationData); } else { $this->logger->warning("onlyUpdate: No se encontró contacto en CallBell para teléfono: $phone"); } } public function onlyUpdateService(NotificationData $notificationData, $phoneToUpdate): void { $config = PluginConfigManager::create()->loadConfig(); $api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']); $phone = $this->validarNumeroTelefono($phoneToUpdate); $contact = json_decode($api->getContactWhatsapp($phone), true); if ($contact) $api->patchServiceStatusWhatsApp($contact, $notificationData); } protected function getVaultCredentialsByClientId($clientId): string { $config = PluginConfigManager::create()->loadConfig(); $ipServer = $config['ipserver'] ?? ''; $crm = new Client(['base_uri' => "https://{$ipServer}/crm/api/v1.0/", 'verify' => false]); try { // OPT: Lazy Check - Si ya tiene pass válido en CRM, no hace falta procesar nada $respClient = $crm->get("clients/$clientId", ['headers' => ['X-Auth-Token' => $config['apitoken']]]); $clientData = json_decode($respClient->getBody()->getContents(), true); $passCRM = ''; if (isset($clientData['attributes'])) { foreach ($clientData['attributes'] as $attr) { if ($attr['key'] === 'passwordAntenaCliente') { $passCRM = $attr['value'] ?? ''; break; } } } // Si el campo no está vacío y no tiene advertencias, usamos el actual para ahorrar recursos if (!empty($passCRM) && strpos($passCRM, '⚠️') === false) { return $passCRM; } // 1. Obtener los servicios del cliente $respSvc = $crm->get('clients/services?clientId=' . $clientId, [ 'headers' => ['X-Auth-Token' => $config['apitoken']] ]); $svcs = json_decode($respSvc->getBody()->getContents(), true); if (empty($svcs)) { $msg = '⚠️ Cliente sin servicios/antenas'; $this->syncPasswordWithCrm((int)$clientId, $msg); return $msg; } $unms = new Client(['base_uri' => "https://{$ipServer}/nms/api/v2.1/", 'verify' => false]); $allServicePasswords = []; $isTestEnv = ($ipServer === '172.16.5.134' || $ipServer === 'pruebas.internet.mx' || $ipServer === 'venus.siip.mx'); $numServices = count($svcs); foreach ($svcs as $index => $svc) { $label = ($numServices > 1) ? "Servicio " . ($index + 1) . ":" : ""; $siteId = $svc['unmsClientSiteId'] ?? null; $passwordValue = ""; if (!$siteId) { $passwordValue = "⚠️ Sin sitio"; } else { if ($isTestEnv) { // Lógica de bypass: intentar recuperar de la cadena existente $foundInCRM = false; if (!empty($passCRM)) { if ($numServices > 1) { if (preg_match('/Servicio ' . ($index + 1) . ':\s*([^⚠️\s]+)/', $passCRM, $matches)) { $passwordValue = $matches[1]; $foundInCRM = true; } } else { // Caso de un solo servicio: si no tiene advertencias ni etiquetas, la tomamos a secas if (strpos($passCRM, '⚠️') === false && strpos($passCRM, 'Servicio') === false) { $passwordValue = trim($passCRM); $foundInCRM = true; } } } if (!$foundInCRM) { $passwordValue = $this->generateStrongPassword(16); } } else { // Lógica de producción try { $respDev = $unms->get("devices?siteId=$siteId", [ 'headers' => ['X-Auth-Token' => $config['unmsApiToken']] ]); $devs = json_decode($respDev->getBody()->getContents(), true); if (empty($devs)) { $passwordValue = "⚠️ Sin antena"; } else { $passVault = null; $firstDeviceId = null; foreach ($devs as $dev) { $deviceId = $dev['identification']['id'] ?? null; if (!$deviceId) continue; if (!$firstDeviceId) $firstDeviceId = $deviceId; try { $respVault = $unms->get("vault/$deviceId/credentials", [ 'headers' => ['X-Auth-Token' => $config['unmsApiToken']] ]); $vault = json_decode($respVault->getBody()->getContents(), true); if (isset($vault['credentials'][0]['password'])) { $passVault = $vault['credentials'][0]['password']; break; } } catch (\Exception $e) { continue; } } if ($passVault) { $passwordValue = $passVault; } else if ($firstDeviceId) { // Regenerar $newPass = $this->generateStrongPassword(16); try { $unms->post("vault/$firstDeviceId/credentials/regenerate", [ 'headers' => ['X-Auth-Token' => $config['unmsApiToken']], 'json' => [['username' => 'ubnt', 'password' => $newPass, 'readOnly' => true]] ]); $passwordValue = $newPass; } catch (\Exception $e) { $passwordValue = $newPass; } } else { $passwordValue = "⚠️ Sin antena"; } } } catch (\Exception $e) { $passwordValue = "⚠️ Error API"; } } } $allServicePasswords[] = trim("$label $passwordValue"); } $finalValue = implode(' ', $allServicePasswords); // Evitar sincronización redundante if ($finalValue === $passCRM) { return $finalValue; } $this->syncPasswordWithCrm((int)$clientId, $finalValue); return $finalValue; } catch (\Exception $e) { $this->logger->error("Error en getVaultCredentialsByClientId: " . $e->getMessage()); return 'Error: ' . $e->getMessage(); } } private function syncPasswordWithCrm(int $clientId, string $passVault): void { $config = PluginConfigManager::create()->loadConfig(); $crm = new Client(['base_uri' => "https://{$config['ipserver']}/crm/api/v1.0/", 'verify' => false]); try { $respClient = $crm->get("clients/$clientId", [ 'headers' => ['X-Auth-Token' => $config['apitoken']] ]); $clientData = json_decode($respClient->getBody()->getContents(), true); $passCRM = ''; $attributeId = 17; if (isset($clientData['attributes'])) { foreach ($clientData['attributes'] as $attr) { if ($attr['key'] === 'passwordAntenaCliente') { $passCRM = $attr['value'] ?? ''; $attributeId = $attr['customAttributeId']; break; } } } if (empty($passCRM) || $passCRM !== $passVault) { $this->logger->info("Sincronizando pass CRM cliente $clientId."); $this->patchClientCustomAttribute($clientId, (int)$attributeId, $passVault); } } catch (\Exception $e) { $this->logger->warning("Fallo sincronización pass CRM: " . $e->getMessage()); } } protected function generateStrongPassword(int $length = 16): string { $lower = 'abcdefghijkmnopqrstuvwxyz'; // Eliminamos 'l' $upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // Eliminamos 'I', 'O' $digits = '23456789'; // Eliminamos '1', '0' $symbols = '@#'; // Solo símbolos amigables para impresoras térmicas $all = $lower . $upper . $digits . $symbols; $pwChars = []; // Asegurar que tenga al menos uno de cada tipo si es posible $pwChars[] = $lower[random_int(0, strlen($lower) - 1)]; $pwChars[] = $upper[random_int(0, strlen($upper) - 1)]; $pwChars[] = $digits[random_int(0, strlen($digits) - 1)]; $pwChars[] = $symbols[random_int(0, strlen($symbols) - 1)]; for ($i = count($pwChars); $i < $length; $i++) { $pwChars[] = $all[random_int(0, strlen($all) - 1)]; } // Mezclar Fisher-Yates $n = count($pwChars); for ($i = $n - 1; $i > 0; $i--) { $j = random_int(0, $i); $tmp = $pwChars[$i]; $pwChars[$i] = $pwChars[$j]; $pwChars[$j] = $tmp; } return implode('', $pwChars); } protected function patchClientCustomAttribute(int $clientId, int $attributeId, string $value): bool { $config = PluginConfigManager::create()->loadConfig(); $crm = new Client(['base_uri' => "https://{$config['ipserver']}/crm/api/v1.0/", 'verify' => false]); try { $crm->patch("clients/$clientId", [ 'headers' => ['X-Auth-Token' => $config['apitoken']], 'json' => [ "attributes" => [['value' => $value, 'customAttributeId' => $attributeId]] ] ]); return true; } catch (\Exception $e) { $this->logger->error("Error patching custom attribute for client $clientId: " . $e->getMessage()); return false; } } protected function comparePasswords(?string $crm, ?string $vault): string { if ($vault && strpos($vault, 'Error') !== 0) return $vault; if ($crm && strpos($crm, 'Error') !== 0) return $crm; return '⚠️ Probar pass conocida.'; } protected function validarNumeroTelefono($n): string { if (!$n) return ''; $n = preg_replace('/\D/', '', (string)$n); return (strlen($n) === 10) ? '52' . $n : $n; } abstract protected function sendWhatsApp(NotificationData $notificationData, string $clientSmsNumber): void; }