feat: soporte multi-servicio, optimización lazy loading y sincronización avanzada con CallBell

- Implementación de gestión multi-servicio para contraseñas de antena con etiquetas condicionales.
- Optimización de rendimiento mediante lazy loading para evitar llamadas redundantes a la API de UISP.
- Mejora de sincronización con CallBell: contraseñas en formato JSON, unificación de peticiones PATCH y corrección de comparación de saldo.
- Generación de contraseñas printer-friendly (alfanuméricas + @, #) sin caracteres ambiguos.
- Validación granular de provisionamiento para evitar errores en sitios inactivos.
- Actualización de CHANGELOG.md y README.md.
This commit is contained in:
DANYDHSV 2026-01-03 11:07:29 -06:00
parent 761cd667b5
commit f851ea6d7d
6 changed files with 152 additions and 93 deletions

View File

@ -7,12 +7,18 @@
3**Lazy Loading & Optimización de Recursos**: Implementación de un "Lazy Check" que detecta si ya hay una contraseña válida en el CRM para omitir llamadas innecesarias a la API de UISP, mejorando la velocidad y reduciendo el consumo de CPU.
### 🔵 Mejoras
1**Contraseñas "Printer-Friendly"**: El generador de contraseñas ahora utiliza un set de caracteres optimizado para mini-impresoras térmicas (Alfanumérico + `@`, `#`), eliminando caracteres ambiguos como `l`, `I`, `0`, `O`.
2**Mensajes de Estado en CRM**: Se agregaron alertas visuales en el campo de contraseña para indicar estados de provisión: `⚠️ Sin sitio vinculado`, `⚠️ Sin antena vinculada`, `⚠️ Cliente sin servicios`.
3**Robustez en Entornos de Prueba**: Refinamiento del bypass de desarrollo para mantener la estabilidad de las claves generadas y evitar bucles infinitos de webhooks.
1**Etiquetado Inteligente de Servicios**: Las etiquetas `Servicio 1:`, `Servicio 2:` ahora solo aparecen si el cliente tiene múltiples servicios; para un solo servicio, la contraseña se muestra directamente.
2**Sincronización Avanzada con CallBell**:
- Nuevo campo `Password Antena` enviado en formato JSON estructurado.
- Unificación de peticiones PATCH (Resumen + Campos) en una sola llamada para mayor eficiencia.
3**Contraseñas "Printer-Friendly"**: El generador de contraseñas ahora utiliza un set de caracteres optimizado para mini-impresoras térmicas (Alfanumérico + `@`, `#`), eliminando caracteres ambiguos como `l`, `I`, `0`, `O`.
4**Mensajes de Estado en CRM**: Se agregaron alertas visuales en el campo de contraseña para indicar estados de provisión: `⚠️ Sin sitio vinculado`, `⚠️ Sin antena vinculada`, `⚠️ Cliente sin servicios`.
5**Robustez en Entornos de Prueba**: Refinamiento del bypass de desarrollo para mantener la estabilidad de las claves generadas y evitar bucles infinitos de webhooks.
### 🟡 Bugs Resueltos
1⃣ Se solucionó el bucle infinito de actualizaciones en el atributo `passwordAntenaCliente` que ocurría al detectar cambios en servicios sin dispositivos vinculados.
1**Sincronización de Saldo**: Se corrigió la discrepancia de nombres entre `Saldo Actual` y `Saldo` que causaba actualizaciones redundantes infinitas con CallBell.
2**Parseo de Passwords**: Refinamiento de expresiones regulares para capturar correctamente contraseñas multi-servicio.
3⃣ Se solucionó el bucle infinito de actualizaciones en el atributo `passwordAntenaCliente` que ocurría al detectar cambios en servicios sin dispositivos vinculados.
## VERSIÓN 2.9.3 - 23-12-2025
### 🟢 Novedades

View File

@ -57,10 +57,14 @@ Es necesario configurar los siguientes atributos en UCRM:
### 📅 Gestión de Contraseñas y Visitas Técnicas
Cuando se asigna o reprograma una tarea:
1. **Detección Multi-Servicio**: El plugin identifica todos los servicios del cliente y formatea sus contraseñas como `Servicio 1: <pass> Servicio 2: ...`.
2. **Validación de Provisionamiento**: Antes de actuar, verifica en UISP si el sitio tiene dispositivos vinculados (evitando errores en estados "Location Inactive").
3. **Lazy Loading**: Si el CRM ya tiene una contraseña válida, el plugin omite llamadas redundantes a la API para ahorrar recursos.
4. **Notificación**:
1. **Detección Multi-Servicio & Etiquetas Inteligentes**:
- El plugin identifica todos los servicios del cliente.
- Si hay **más de un servicio**, utiliza el formato: `Servicio 1: <pass> Servicio 2: ...`.
- Si hay **solo un servicio**, muestra la contraseña directamente sin etiquetas para mayor claridad.
2. **Sincronización CallBell Avanzada**:
- Los datos técnicos y financieros se sincronizan en una sola petición optimizada.
- Las contraseñas se envían como un objeto JSON estructurado al campo `Password Antena`.
3. **Validación de Provisionamiento**: Antes de actuar, verifica en UISP si el sitio tiene dispositivos vinculados (evitando errores en estados "Location Inactive").
- Envía un mensaje al cliente con el nombre del técnico y la fecha (sin hora).
- Envía un mensaje al técnico con la dirección, ubicación en Google Maps y las contraseñas de las antenas optimizadas para lectura en impresoras térmicas (alfanumérico + `@`, `#`).

File diff suppressed because one or more lines are too long

View File

@ -287,8 +287,9 @@ abstract class AbstractMessageNotifierFacade
$allServicePasswords = [];
$isTestEnv = ($ipServer === '172.16.5.134' || $ipServer === 'pruebas.internet.mx' || $ipServer === 'venus.siip.mx');
$numServices = count($svcs);
foreach ($svcs as $index => $svc) {
$label = "Servicio " . ($index + 1) . ":";
$label = ($numServices > 1) ? "Servicio " . ($index + 1) . ":" : "";
$siteId = $svc['unmsClientSiteId'] ?? null;
$passwordValue = "";
@ -296,10 +297,24 @@ abstract class AbstractMessageNotifierFacade
$passwordValue = "⚠️ Sin sitio";
} else {
if ($isTestEnv) {
// Lógica de bypass: intentar recuperar de la cadena existente si existe y es válida
if (!empty($passCRM) && preg_match('/Servicio ' . ($index + 1) . ':\s*([^⚠️\s]+)/', $passCRM, $matches)) {
$passwordValue = $matches[1];
} else {
// 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 {
@ -352,7 +367,7 @@ abstract class AbstractMessageNotifierFacade
}
}
}
$allServicePasswords[] = "$label $passwordValue";
$allServicePasswords[] = trim("$label $passwordValue");
}
$finalValue = implode(' ', $allServicePasswords);

View File

@ -195,8 +195,9 @@ abstract class AbstractStripeOperationsFacade
$allServicePasswords = [];
$isTestEnv = ($ipServer === '172.16.5.134' || $ipServer === 'pruebas.internet.mx' || $ipServer === 'venus.siip.mx');
$numServices = count($svcs);
foreach ($svcs as $index => $svc) {
$label = "Servicio " . ($index + 1) . ":";
$label = ($numServices > 1) ? "Servicio " . ($index + 1) . ":" : "";
$siteId = $svc['unmsClientSiteId'] ?? null;
$passwordValue = "";
@ -204,10 +205,24 @@ abstract class AbstractStripeOperationsFacade
$passwordValue = "⚠️ Sin sitio";
} else {
if ($isTestEnv) {
// Lógica de bypass: intentar recuperar de la cadena existente si existe y es válida
if (!empty($passCRM) && preg_match('/Servicio ' . ($index + 1) . ':\s*([^⚠️\s]+)/', $passCRM, $matches)) {
$passwordValue = $matches[1];
} else {
// 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, asumimos que es el pass
if (strpos($passCRM, '⚠️') === false && strpos($passCRM, 'Servicio') === false) {
$passwordValue = trim($passCRM);
$foundInCRM = true;
}
}
}
if (!$foundInCRM) {
$passwordValue = $this->generateStrongPassword(16);
}
} else {
@ -260,7 +275,7 @@ abstract class AbstractStripeOperationsFacade
}
}
}
$allServicePasswords[] = "$label $passwordValue";
$allServicePasswords[] = trim("$label $passwordValue");
}
$finalValue = implode(' ', $allServicePasswords);

View File

@ -876,6 +876,7 @@ class ClientCallBellAPI
$attributes = $notificationData->clientData['attributes']; //Obtener los atributos del cliente
$site = '';
$antenaSectorial = '';
$passAntenaUCRM = '';
// Iterar sobre los atributos
foreach ($attributes as $attribute) {
@ -887,8 +888,29 @@ class ClientCallBellAPI
if ($attribute['key'] === 'antenaSectorial') {
$antenaSectorial = $attribute['value'];
}
if ($attribute['key'] === 'passwordAntenaCliente') {
$passAntenaUCRM = $attribute['value'] ?? '';
}
}
// Parsear contraseñas multi-servicio a JSON
$passAntenaJSON = [];
if (!empty($passAntenaUCRM)) {
if (strpos($passAntenaUCRM, 'Servicio') !== false) {
// Buscamos patrones "Servicio X: <valor>"
// Usamos una expresión más robusta para capturar hasta el siguiente "Servicio" o el final de la cadena
preg_match_all('/Servicio (\d+):\s*(.*?)(?=\s*Servicio \d+:|$)/', $passAntenaUCRM, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$numServicio = "Servicio " . $match[1];
$passAntenaJSON[$numServicio] = trim($match[2]);
}
} else {
// Un solo servicio sin etiqueta (o con advertencia ⚠️)
$passAntenaJSON["Servicio 1"] = trim($passAntenaUCRM);
}
}
$passAntenaFinal = !empty($passAntenaJSON) ? json_encode($passAntenaJSON) : "";
$log->appendLog("Dentro del proceso del patch: " . PHP_EOL);
$this->ucrmApi = UcrmApi::create();
$payments = $this->ucrmApi->get(
@ -917,16 +939,9 @@ class ClientCallBellAPI
'Content-Type: application/json',
]);
$ch2 = curl_init();
curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch2, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch2, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $this->CallBellAPIToken,
'Content-Type: application/json',
]);
$UrlChatCallBell = 'https://api.callbell.eu/v1/contacts/' . $uuid;
curl_setopt($ch, CURLOPT_URL, $UrlChatCallBell);
curl_setopt($ch2, CURLOPT_URL, $UrlChatCallBell);
$nombre_cliente = sprintf("%s %s", $notificationData->clientData['firstName'], $notificationData->clientData['lastName']);
$log->appendLog("Nombre del cliente que se va a actualizar: " . $nombre_cliente . PHP_EOL);
$log->appendLog("UUID: " . $uuid . PHP_EOL);
@ -978,22 +993,6 @@ class ClientCallBellAPI
//$fecha_ultimoPago = $fecha_ultimoPago->modify('-6 hours');
$fecha_ultimoPago_ajustada = $fecha_ultimoPago->format("d/m/Y H:i");
//$log->appendLog("las dos fechas ajustadas : " . $fecha_actual_ajustada.' aqui la otra: '.$fecha_ultimoPago_ajustada. PHP_EOL);
// $attributes = $notificationData->clientData['attributes']; //Obtener los atributos del cliente
// // Variable para almacenar los valores de los atributos que comienzan con "clabe"
// $clabeInterbancaria = '';
// // Iterar sobre los atributoss
// foreach ($attributes as $attribute) {
// // Verificar si la "key" comienza con "clabe"
// if (strpos($attribute['key'], 'clabe') === 0) {
// // Agregar el valor al array $clabeValues
// $clabeInterbancaria = $attribute['value'];
// }
// }
$accountBalance = $notificationData->clientData['accountBalance'];
$saldoTexto = '';
@ -1010,8 +1009,6 @@ class ClientCallBellAPI
$resumenClienteJSON = '{' .
'"Cliente": "' . $notificationData->clientData['id'] . '",' .
'"Domicilio": "' .
//(($notificationData->clientData['fullAddress'] == null) ? 'Sin domicilio' : '' . $notificationData->clientData['fullAddress']) . '",' .
'"Nombre": "' . sprintf("%s %s", $notificationData->clientData['firstName'], $notificationData->clientData['lastName']) . '",' .
'"URL": "https://sistema.siip.mx/crm/client/' . $notificationData->clientId . '",' .
'"Saldo Actual": "' . $saldoTexto . '",' .
@ -1022,11 +1019,11 @@ class ClientCallBellAPI
'"Fecha Ultima Actualizacion": "' . $fecha_actual_ajustada . '",' .
'"Clabe Interbancaria": "' . $clabeInterbancaria . '",' .
'"Site": "' . $site . '",' .
'"Antena/Sectorial": "' . $antenaSectorial . '"' .
'"Antena/Sectorial": "' . $antenaSectorial . '",' .
'"Password Antena": ' . (empty($passAntenaFinal) ? '""' : $passAntenaFinal) .
'}';
$data_CRM = [
//"uuid" => $json_responseAPI->contact->uuid,
"name" => sprintf("%s %s", $notificationData->clientData['firstName'], $notificationData->clientData['lastName']),
"custom_fields" => [
@ -1043,74 +1040,34 @@ class ClientCallBellAPI
"Clabe Interbancaria" => $clabeInterbancaria,
"Site" => $site,
"Antena/Sectorial" => $antenaSectorial,
"Password Antena" => $passAntenaFinal,
],
];
$log->appendLog("JSON con los datos a actualizar: " . json_encode($data_CRM) . PHP_EOL);
$data_CRM2 = [
"custom_fields" => [
"Resumen" => $resumenClienteJSON,
],
];
$log->appendLog("JSON con los datos a actualizar del resumen: " . $resumenClienteJSON . PHP_EOL);
// OPT: Comparación inteligente para evitar patches innecesarios
if (
$response_getContactCallBell['custom_fields']['Cliente'] != $data_CRM['custom_fields']['Cliente']
|| $response_getContactCallBell['custom_fields']['Domicilio'] != $data_CRM['custom_fields']['Domicilio']
|| $response_getContactCallBell['custom_fields']['Nombre'] != $data_CRM['custom_fields']['Nombre']
|| $response_getContactCallBell['custom_fields']['URL'] != $data_CRM['custom_fields']['URL']
|| $response_getContactCallBell['custom_fields']['Saldo'] != $data_CRM['custom_fields']['Saldo']
|| $response_getContactCallBell['custom_fields']['Saldo Actual'] != $data_CRM['custom_fields']['Saldo Actual']
|| $response_getContactCallBell['custom_fields']['Estado'] != $data_CRM['custom_fields']['Estado']
|| $response_getContactCallBell['custom_fields']['Fecha Ultimo Pago'] != $data_CRM['custom_fields']['Fecha Ultimo Pago']
|| $response_getContactCallBell['custom_fields']['Monto Ultimo Pago'] != $data_CRM['custom_fields']['Monto Ultimo Pago']
|| ($response_getContactCallBell['custom_fields']['Password Antena'] ?? '') != $data_CRM['custom_fields']['Password Antena']
|| $response_getContactCallBell['name'] != $data_CRM['name']
) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data_CRM));
curl_setopt($ch2, CURLOPT_POSTFIELDS, json_encode($data_CRM2));
$response = curl_exec($ch);
$log->appendLog("Response Patch CallBell: " . $response . PHP_EOL);
$response2 = curl_exec($ch2);
$log->appendLog("Response 2 Patch CallBell: " . $response2 . PHP_EOL);
curl_close($ch);
curl_close($ch2);
// if($fileNameComprobante != null){
// sleep(3);
// $this->deleteFileWordPressAndLocal($fileNameComprobante);
// }
// $json_data_patch = '{
// "attributes": [
// {
// "value": "' . $UrlChatCallBell . '",
// "customAttributeId": 21
// }
// ]
// }'; //JSON para hacer patch de los custom fields del cliente en el UISCP CRM, Campo para el Stripe Customer ID y la Clabe interbancaria
// $clientguzz = new Client(); //instancia de cliente GuzzleHttp para consumir API UISP CRM
// try {
// $responseCRM = $clientguzz->patch($baseUri . 'clients/' . $notificationData->clientData['id'], [
// 'json' => json_decode($json_data_patch, true),
// 'headers' => [
// 'X-Auth-App-Key' => $token, // Cambia el nombre de la cabecera de autorización
// 'Accept' => 'application/json', // Indica que esperamos una respuesta en formato JSON
// ],
// 'verify' => false,
// ]); //aquí se contruye la petición para hacer patch hacia el cliente en sus custom fields con la API del UISP UCRM
// } catch (GuzzleException $error) {
// echo "Error al hacer el patch al CRM: " . $error->getMessage() . PHP_EOL;
// //exit();
// }
// $log->appendLog(json_encode($responseCRM) . PHP_EOL); //imprimir respuesta del patch de CRM con la clabe y Customer ID Stripe
} else {
$log->appendLog("No hay cambios que actualizar " . PHP_EOL);
curl_close($ch);
}
}