siip-whatsapp-notifications.../src/Facade/AbstractMessageNotifierFacade.php

532 lines
25 KiB
PHP

<?php
declare(strict_types=1);
namespace SmsNotifier\Facade;
use GuzzleHttp\Client;
use SmsNotifier\Data\NotificationData;
use SmsNotifier\Facade\ClientCallBellAPI;
use SmsNotifier\Factory\MessageTextFactory;
use SmsNotifier\Service\Logger;
use SmsNotifier\Service\SmsNumberProvider;
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use \DateTime;
abstract class AbstractMessageNotifierFacade
{
protected $logger;
protected $messageTextFactory;
protected $clientPhoneNumber;
protected $ucrmApi;
const SUBJECT_OF_INSTALLER_CHANGE = ["se ha cancelado una tarea que tenías asignada con el folio ", "se te ha desasignado❌ la tarea con el folio "];
const ADDITIONAL_CHANGE_DATA = ["Ya no es necesario realizar la visita técnica.", "En tu lugar asistirá el técnico 👷🏻‍♂️➡️ "];
public function __construct(Logger $logger, MessageTextFactory $messageTextFactory, SmsNumberProvider $clientPhoneNumber)
{
$this->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;
}