antes de la implementación del reenvio de notificaciones a instaladores

This commit is contained in:
DANYDHSV 2026-03-10 14:01:07 -06:00
parent 8d4580e138
commit d0430dd891
37 changed files with 1617 additions and 185 deletions

View File

@ -1,5 +1,13 @@
# CHANGELOG - SIIP WhatsApp Notifications Plugin
## VERSIÓN 4.3.1 - 10-03-2026
### 🐛 Correcciones (Bug Fixes)
1**Fix Notificación de Cambio de Instalador**: Corregido bug crítico en `AbstractMessageNotifierFacade.php` donde al cambiar de técnico en una tarea **"En curso"**, el **nuevo** instalador recibía el mensaje de **desasignación** (❌ "se te ha desasignado la tarea...") en lugar de un mensaje de **asignación** con los datos del cliente.
**Causa raíz**: La bandera `$changeInstaller=true` se pasaba tanto a la notificación del técnico anterior como a la del nuevo, causando que ambos recibieran la plantilla de desasignación. El fix envía `false` al nuevo técnico para que use la plantilla de asignación normal.
## VERSIÓN 4.3.0 - 23-02-2026
### 🔐 Seguridad y Acceso (Login)

View File

@ -1,12 +1,16 @@
# SIIP - WhatsApp Notifications & Integrated Payment Portal
![Version](https://img.shields.io/badge/version-4.3.0-blue.svg?style=for-the-badge)
![Version](https://img.shields.io/badge/version-4.3.1-blue.svg?style=for-the-badge)
![UCRM Compatibility](https://img.shields.io/badge/UCRM-v2.1.0%2B-green.svg?style=for-the-badge)
![Status](https://img.shields.io/badge/status-PRODUCTION-success.svg?style=for-the-badge)
![Author](https://img.shields.io/badge/author-SIIP_INTERNET-orange.svg?style=for-the-badge)
Este plugin es una solución integral que transforma tu UCRM en un **Portal Administrativo de Última Generación**. No solo automatiza la comunicación por WhatsApp, sino que integra un Dashboard completo para la gestión de pagos online (Stripe/OXXO), visualización de comprobantes y coordinación de equipos técnicos.
## 🐛 Hotfix v4.3.1 (Installer Notification Fix)
- **🔧 Fix Cambio de Instalador**: Corregido bug donde el nuevo técnico recibía mensaje de desasignación en vez de asignación al cambiar instalador en una tarea "En curso".
## 🔐 Novedades v4.3.0 (Security & Premium UI)
- **🛡️ Sistema de Acceso Seguro**: Implementada validación híbrida (Server + Client). El plugin ahora protege las URLs públicas mediante una pantalla de inicio de sesión que requiere credenciales de Administrador de UCRM o autenticación 2FA.

File diff suppressed because it is too large Load Diff

View File

@ -5,13 +5,18 @@
"displayName": "SIIP - Procesador de Pagos en línea con Stripe, Oxxo y Transferencia, Sincronizador de CallBell y Envío de Notificaciones y comprobantes vía WhatsApp",
"description": "Este plugin sincroniza los clientes del sistema UISP CRM con los contactos de WhatsApp en CallBell, además procesa pagos de Stripe como las trasferencias bancarias y genera referencias de pago vía OXXO, además envía comprobantes de pago en formato imagen PNG o texto vía Whatsapp a los clientes",
"url": "https://siip.mx/",
"version": "4.3.0",
"version": "4.3.1",
"unmsVersionCompliancy": {
"min": "2.1.0",
"max": null
},
"author": "SIIP INTERNET",
"changelog": [
{
"version": "4.3.1",
"date": "2026-03-10",
"changes": "Hotfix: Corregido bug donde al cambiar de instalador en una tarea En Curso, el nuevo técnico recibía mensaje de desasignación en vez de asignación."
},
{
"version": "4.3.0",
"date": "2026-02-23",

338
public.php Executable file → Normal file
View File

@ -7,10 +7,8 @@ chdir(__DIR__);
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\UcrmSecurity;
use SmsNotifier\Service\PaymentIntentService;
// Carga manual del servicio
require_once __DIR__ . '/src/Service/PaymentIntentService.php';
// Eliminado: PaymentIntentService ya no se usa aquí
if (!file_exists(__DIR__ . '/data/config.json')) {
die('Acceso denegado o configuración no encontrada.');
@ -18,7 +16,6 @@ if (!file_exists(__DIR__ . '/data/config.json')) {
$configManager = PluginConfigManager::create();
$config = $configManager->loadConfig();
$logger = new \SmsNotifier\Service\Logger();
// LOG DE EMERGENCIA
$debugLogPath = __DIR__ . '/data/debug_public.log';
@ -50,11 +47,7 @@ $httpClient = new \GuzzleHttp\Client([
$ucrmApi = new UcrmApi($httpClient, $config['apitoken'] ?? '');
$stripeService = new PaymentIntentService(
$ucrmApi,
$config['tokenstripe'] ?? '',
$logger
);
$ucrmApi = new UcrmApi($httpClient, $config['apitoken'] ?? '');
// Admins Logic
$admins = [];
@ -110,19 +103,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_GET['action']) && $_GET['ac
$authToken = $resp->getHeaderLine('x-auth-token');
if ($statusCode === 200 && $authToken) {
$logger->info("NMS Login OK for user: {$username}");
echo json_encode(['success' => true, 'token' => $authToken, 'user' => $body]);
} elseif ($statusCode === 201) {
$logger->info("NMS Login requires 2FA for user: {$username}");
http_response_code(201);
echo json_encode(['requires2FA' => true, 'twoFactorToken' => $body]);
} else {
$logger->warning("NMS Login failed for user: {$username} (HTTP {$statusCode})");
http_response_code($statusCode ?: 401);
echo json_encode(['error' => $body['message'] ?? 'Credenciales inválidas.', 'statusCode' => $statusCode]);
}
} catch (\Exception $e) {
$logger->error("NMS Login exception: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Error de conexión con el servidor UISP.']);
}
@ -146,7 +135,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_GET['action']) && $_GET['ac
$authToken = $resp->getHeaderLine('x-auth-token');
if ($statusCode === 200 && $authToken) {
$logger->info("NMS 2FA Login OK");
echo json_encode(['success' => true, 'token' => $authToken, 'user' => $body]);
} else {
http_response_code($statusCode ?: 401);
@ -222,6 +210,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
$selfUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];
$debugLogPath = __DIR__ . '/data/debug_public.log';
file_put_contents($debugLogPath, "[" . date('Y-m-d H:i:s') . "] RESEND PAYMENT. URL: $selfUrl" . PHP_EOL, FILE_APPEND);
$ch = curl_init($selfUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
@ -229,10 +220,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_POSTREDIR, 3); // Mantener POST y payload en redirects 301/302
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$res = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
file_put_contents($debugLogPath, "[" . date('Y-m-d H:i:s') . "] RESEND CURL RESULT - Code: $code - Error: $err - Body: $res" . PHP_EOL, FILE_APPEND);
echo json_encode(['success' => true, 'message' => 'Notificación disparada.']);
} catch (\Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
@ -271,60 +268,6 @@ if (isset($_GET['action'])) {
exit;
}
if ($_GET['action'] === 'get_stripe_history') {
if (ob_get_level()) ob_end_clean();
header('Content-Type: application/json');
$stripeCustomerId = $_GET['stripeCustomerId'] ?? '';
if (!$stripeCustomerId) {
echo json_encode(['error' => 'Missing stripeCustomerId']);
exit;
}
try {
$history = $stripeService->getLastPayments($stripeCustomerId);
$balance = $stripeService->getCustomerCashBalance($stripeCustomerId);
echo json_encode([
'history' => $history,
'cashBalance' => $balance
]);
} catch (Exception $e) {
echo json_encode(['error' => $e->getMessage()]);
}
exit;
}
if ($_GET['action'] === 'get_oxxo_history') {
if (ob_get_level()) ob_end_clean();
header('Content-Type: application/json');
$stripeCustomerId = $_GET['stripeCustomerId'] ?? '';
if (!$stripeCustomerId) {
echo json_encode(['error' => 'Missing stripeCustomerId']);
exit;
}
try {
$history = $stripeService->getLastOxxoPayments($stripeCustomerId);
echo json_encode(['history' => $history]);
} catch (Exception $e) {
echo json_encode(['error' => $e->getMessage()]);
}
exit;
}
if ($_GET['action'] === 'search_stripe') {
$q = $_GET['q'] ?? '';
echo json_encode($stripeService->searchClients($q));
exit;
}
if ($_GET['action'] === 'get_stripe_details') {
echo json_encode($stripeService->getClientDetails($_GET['id'] ?? null));
exit;
}
if ($_GET['action'] === 'image' || $_GET['action'] === 'get_image') {
// Image Handler
if (ob_get_level()) ob_end_clean();
@ -355,14 +298,6 @@ if (isset($_GET['action'])) {
// POST Actions for Intents
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
header('Content-Type: application/json');
if ($_POST['action'] === 'create_intent') {
$clientId = $_POST['clientId'] ?? null;
$amount = $_POST['amount'] ?? 0;
$stripeCustomerId = $_POST['stripeCustomerId'] ?? null;
$adminId = $_POST['adminId'] ?? null;
echo json_encode($stripeService->createPaymentIntent($clientId, $amount, $stripeCustomerId, $adminId));
exit;
}
if ($_POST['action'] === 'create_oxxo_intent') {
try {
$builder = new \DI\ContainerBuilder();
@ -451,6 +386,46 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
display: flex;
justify-content: space-between;
align-items: center;
gap: 1.5rem;
flex-wrap: wrap;
}
.header-left {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.header-actions {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.header-logo {
width: 80px;
height: 80px;
object-fit: contain;
border-radius: 12px;
background: white;
padding: 5px;
border: 1px solid var(--border);
flex-shrink: 0;
}
.header-title {
margin: 0 0 5px 0;
font-size: 1.5rem;
color: var(--text-main);
line-height: 1.2;
}
.header-subtitle {
margin: 0;
color: var(--text-muted);
font-size: 0.95rem;
}
/* MENU DASHBOARD */
@ -806,6 +781,45 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
display: block;
}
/* STRIPE CASH BALANCE BADGE */
.balance-badge {
align-items: center;
background-color: #f1f5f9;
color: var(--text-main);
padding: 6px 14px;
border-radius: 8px;
font-size: 0.9em;
font-weight: 700;
border: 1px solid #e2e8f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
[data-theme="dark"] .balance-badge {
background-color: #bfdbfe;
/* Light blue background for contrast */
color: #1e3a8a;
/* Navy blue text */
border-color: #93c5fd;
}
/* ENHANCED DARK MODE CONTRAST FOR SEARCH BOXES */
[data-theme="dark"] .config-container {
background: #1e293b;
border: 1px solid #475569;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
[data-theme="dark"] .search-wrapper .form-control {
background: #0f172a;
border: 1px solid #475569;
color: #f1f5f9;
}
[data-theme="dark"] .search-wrapper .form-control:focus {
border-color: var(--primary-hover);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
/* THEME TOGGLE (Premium) */
.theme-toggle {
background: var(--bg-card);
@ -879,14 +893,56 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
table {
width: 100%;
border-collapse: collapse;
border-collapse: separate;
border-spacing: 0;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
th,
td {
padding: 12px;
thead {
background-color: var(--primary-surface);
}
th {
padding: 16px 12px;
text-align: left;
font-weight: 600;
color: var(--text-main);
text-transform: uppercase;
font-size: 0.85rem;
letter-spacing: 0.05em;
border-bottom: 2px solid var(--border);
}
td {
padding: 14px 12px;
border-bottom: 1px solid var(--border);
color: var(--text-muted);
vertical-align: middle;
transition: background-color 0.2s;
}
tbody tr:last-child td {
border-bottom: none;
}
tbody tr:hover td {
background-color: rgba(37, 99, 235, 0.04);
color: var(--text-main);
}
[data-theme="dark"] tbody tr:hover td {
background-color: rgba(96, 165, 250, 0.08);
}
[data-theme="dark"] thead {
background-color: rgba(30, 41, 59, 0.8);
}
[data-theme="dark"] th {
color: #e2e8f0;
}
/* Toast */
@ -1037,9 +1093,75 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
}
@media (max-width: 768px) {
body {
padding: 1rem;
}
.form-grid {
grid-template-columns: 1fr;
}
.header {
padding: 1.5rem;
flex-direction: column;
align-items: flex-start;
}
.header-left {
flex-direction: column;
align-items: flex-start;
min-width: 100%;
}
.header-logo {
width: 60px;
height: 60px;
}
.header-title {
font-size: 1.2rem;
}
.tabs {
flex-wrap: nowrap;
overflow-x: auto;
padding-bottom: 10px;
-webkit-overflow-scrolling: touch;
}
.tab {
padding: 10px 16px;
font-size: 0.9rem;
}
.config-container {
flex-direction: column;
align-items: stretch;
}
.actions-row {
flex-direction: column;
width: 100%;
}
.actions-row .btn {
width: 100%;
}
.menu-container {
grid-template-columns: 1fr;
}
.menu-item {
min-height: 200px;
padding: 1.5rem;
}
}
.table-responsive {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.form-group {
@ -1468,15 +1590,15 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
<div class="container">
<!-- HEADER -->
<div class="header">
<div style="display: flex; align-items: center; gap: 15px;">
<img src="?action=image&file=logo-empresa.png" style="width:80px; height:80px; object-fit: contain; border-radius: 12px; background: white; padding: 5px; border: 1px solid var(--border);">
<div class="header-left">
<img src="?action=image&file=logo-empresa.png" class="header-logo">
<div>
<!-- RESTORED HEADER TEXT -->
<h1 style="margin: 0; font-size: 24px;">Portal Administrativo de Pagos de STRIPE y Notificaciones WhatsApp</h1>
<p style="margin: 0; color: #666;">Administración de Notificaciones vía WhatsApp, Intenciones de pago con Stripe y Fichas de OXXO Pay</p>
<h1 class="header-title">Portal Administrativo de Pagos de STRIPE y Notificaciones WhatsApp</h1>
<p class="header-subtitle">Administración de Notificaciones vía WhatsApp, Intenciones de pago con Stripe y Fichas de OXXO Pay</p>
</div>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<div class="header-actions">
<button class="btn btn-secondary hidden" id="btnBackToMenu" onclick="showDashboard()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2-2H5a2 2 0 0 1-2-2z" />
@ -1484,14 +1606,16 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
</svg>
Menú Principal
</button>
<button id="logoutBtn" class="btn btn-secondary" style="color: #ef4444; border-color: rgba(239, 68, 68, 0.3); background: rgba(239, 68, 68, 0.05);" onclick="handleLogout()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:middle; margin-right:6px;">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
Cerrar Sesión
</button>
<?php if (strpos($_SERVER['REQUEST_URI'], '_plugins') !== false): ?>
<button class="btn btn-secondary" style="color: #ef4444; border-color: rgba(239, 68, 68, 0.3); background: rgba(239, 68, 68, 0.05);" onclick="handleLogout()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:middle; margin-right:6px;">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
Cerrar Sesión
</button>
<?php endif; ?>
<!-- RESTORED THEME BUTTON -->
<!-- PREMIUM THEME BUTTON -->
<button class="theme-toggle" id="themeBtn" onclick="toggleTheme()">
@ -1766,7 +1890,7 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
<div id="stripeHistoryContainer" style="display:none; margin-top: 2rem; border-top: 1px solid var(--border); padding-top: 1.5rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h4 class="section-title" style="margin-bottom: 0;">Historial de Pagos (Últimos 10)</h4>
<div id="stripeCashBalanceBadge" style="display:none; align-items: center; background-color: #f1f5f9; color: var(--text-main); padding: 5px 12px; border-radius: 8px; font-size: 0.9em; font-weight: 600; border: 1px solid transparent;">
<div id="stripeCashBalanceBadge" class="balance-badge" style="display:none;">
<img src="?action=image&file=account-balance.webp" style="width: 32px; height: 32px; margin-right: 8px;">
<span id="stripeCashBalanceText">Saldo Stripe: $0.00 MXN</span>
</div>
@ -1989,12 +2113,7 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
window.handleLogout = () => {
sessionStorage.removeItem('nms_auth_token');
sessionStorage.removeItem('nms_user');
if (!NEEDS_LOGIN) {
window.top.location.href = '/crm/logout';
} else {
window.location.reload();
}
window.location.reload();
};
@ -2220,7 +2339,6 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
<td>${new Date(p.createdDate).toLocaleString()}</td>
<td>$${p.amount} ${p.currencyCode}</td>
<td>${p.methodName}</td>
<td>${p.methodName}</td>
<td>
<button class="btn btn-whatsapp" onclick="resendPayment(${p.id})">
<img src="?action=image&file=whatsapp-logo-button.png" class="icon-btn" style="margin-right:5px;filter: brightness(0) invert(1);"> Re-enviar Notificación
@ -2348,7 +2466,7 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
const date = new Date(p.created * 1000).toLocaleString();
const voucherBtn = p.voucherUrl ?
`<a href="${p.voucherUrl}" target="_blank" class="btn btn-sm btn-outline-primary" style="padding: 2px 8px; font-size: 0.8rem;">Ver Ficha</a>` :
`<a href="${p.voucherUrl}" target="_blank" class="btn btn-uniform" style="padding: 4px 12px; font-size: 0.8rem;">Ver Ficha</a>` :
'-';
return `<tr>
@ -2655,9 +2773,9 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
balanceBadge.style.color = '#15803d';
balanceBadge.style.border = '1px solid #bbf7d0';
} else {
balanceBadge.style.backgroundColor = '#f1f5f9'; // Gray
balanceBadge.style.color = 'var(--text-main)';
balanceBadge.style.border = '1px solid transparent';
balanceBadge.style.backgroundColor = '';
balanceBadge.style.color = '';
balanceBadge.style.border = '';
}
}
@ -2903,16 +3021,6 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
});
// Inicialización: decidir si mostrar login o portal
sessionStorage.removeItem('nms_force_local_login'); // Cleanup from previous versions
// Ocultar botón de Cerrar Sesión si estamos dentro del iframe del CRM
document.addEventListener('DOMContentLoaded', () => {
if (window.top !== window.self) {
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) logoutBtn.style.display = 'none';
}
});
if (NEEDS_LOGIN) {
// No hay sesión UCRM, intentar con token almacenado
verifyStoredSession().then(valid => {

17
scripts-uisp/audit_client_passwords.php Executable file → Normal file
View File

@ -438,12 +438,17 @@ function resolveAttributeIds($ucrmApi, $customAttributeKey, $siteAttributeKey, $
try {
$attributes = $ucrmApi->get('custom-attributes', ['attributeType' => 'client']);
foreach ($attributes as $attr) {
match ($attr['key']) {
$customAttributeKey => $customAttributeId = $attr['id'],
$siteAttributeKey => $siteAttributeId = $attr['id'],
$antenaSectorialAttributeKey => $antenaSectorialAttributeId = $attr['id'],
default => null,
};
switch ($attr['key']) {
case $customAttributeKey:
$customAttributeId = $attr['id'];
break;
case $siteAttributeKey:
$siteAttributeId = $attr['id'];
break;
case $antenaSectorialAttributeKey:
$antenaSectorialAttributeId = $attr['id'];
break;
}
}
} catch (\Exception $e) {
logMessage("Error fetching attributes: " . $e->getMessage());

0
scripts-uisp/debug_clients.php Executable file → Normal file
View File

0
scripts-uisp/ejemplo_script_actualizador.php Executable file → Normal file
View File

0
scripts-uisp/generate_invalid_password_list.php Executable file → Normal file
View File

View File

@ -1 +0,0 @@
10
1 10

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

0
src/Data/NotificationData.php Executable file → Normal file
View File

0
src/Data/PluginData.php Executable file → Normal file
View File

0
src/Data/UcrmData.php Executable file → Normal file
View File

0
src/Exception/CurlException.php Executable file → Normal file
View File

95
src/Facade/AbstractMessageNotifierFacade.php Executable file → Normal file
View File

@ -24,7 +24,8 @@ abstract class AbstractMessageNotifierFacade
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) {
public function __construct(Logger $logger, MessageTextFactory $messageTextFactory, SmsNumberProvider $clientPhoneNumber)
{
$this->logger = $logger;
$this->messageTextFactory = $messageTextFactory;
$this->clientPhoneNumber = $clientPhoneNumber;
@ -41,22 +42,30 @@ abstract class AbstractMessageNotifierFacade
$this->ucrmApi = new UcrmApi($client, $config['apitoken'] ?? '');
}
public function verifyPaymentActionToDo(NotificationData $notificationData): void {
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;
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 {
public function verifyClientActionToDo(NotificationData $notificationData): void
{
$arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null);
foreach ($arrayPhones as $type => $phones) {
$type = trim(strtolower($type));
@ -67,7 +76,8 @@ abstract class AbstractMessageNotifierFacade
}
}
public function verifyServiceActionToDo(NotificationData $notificationData): void {
public function verifyServiceActionToDo(NotificationData $notificationData): void
{
$arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null);
foreach ($arrayPhones as $type => $phones) {
$type = trim(strtolower($type));
@ -78,7 +88,8 @@ abstract class AbstractMessageNotifierFacade
}
}
public function verifyJobActionToDo($jsonNotificationData, $reprogramming = null, $changeInstaller = null): void {
public function verifyJobActionToDo($jsonNotificationData, $reprogramming = null, $changeInstaller = null): void
{
$this->logger->info('Iniciando verifyJobActionToDo');
$clientId = $jsonNotificationData['extraData']['entity']['clientId'];
$installerId = $jsonNotificationData['extraData']['entity']['assignedUserId'];
@ -94,7 +105,10 @@ abstract class AbstractMessageNotifierFacade
$installerWhatsApp = '';
$installers = json_decode($config['installersDataWhatsApp'] ?? '{"instaladores":[]}', true);
foreach ($installers['instaladores'] as $inst) {
if ($inst['id'] == $installerId) { $installerWhatsApp = $inst['whatsapp']; break; }
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");
@ -103,7 +117,10 @@ abstract class AbstractMessageNotifierFacade
$clientName = trim(($clientCRM['firstName'] ?? '') . ' ' . ($clientCRM['lastName'] ?? ''));
$passCRM = '';
foreach ($clientCRM['attributes'] as $attr) {
if ($attr['key'] === 'passwordAntenaCliente') { $passCRM = $attr['value']; break; }
if ($attr['key'] === 'passwordAntenaCliente') {
$passCRM = $attr['value'];
break;
}
}
$allPhones = $this->clientPhoneNumber->getAllUcrmClientNumbers($clientCRM);
@ -137,7 +154,10 @@ abstract class AbstractMessageNotifierFacade
$prevAdmin = $this->ucrmApi->get("users/admins/$prevId", []);
$prevWhatsApp = '';
foreach ($installers['instaladores'] as $inst) {
if ($inst['id'] == $prevId) { $prevWhatsApp = $inst['whatsapp']; break; }
if ($inst['id'] == $prevId) {
$prevWhatsApp = $inst['whatsapp'];
break;
}
}
if ($prevWhatsApp) {
$api->sendJobNotificationWhatsAppToInstaller($this->validarNumeroTelefono($prevWhatsApp), [
@ -178,7 +198,7 @@ abstract class AbstractMessageNotifierFacade
"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, $changeInstaller);
], $reprogramming, false);
}
// 3. Gestión del Título / Prefijos
@ -196,7 +216,8 @@ abstract class AbstractMessageNotifierFacade
}
}
public function verifyInvoiceActionToDo(NotificationData $notificationData): void {
public function verifyInvoiceActionToDo(NotificationData $notificationData): void
{
$arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null);
foreach ($arrayPhones as $type => $phones) {
$type = trim(strtolower($type));
@ -207,7 +228,8 @@ abstract class AbstractMessageNotifierFacade
}
}
public function notify(NotificationData $notificationData, $phoneToNotify = null): void {
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);
@ -216,7 +238,8 @@ abstract class AbstractMessageNotifierFacade
else $api->sendPaymentNotificationWhatsApp($phone, $notificationData);
}
public function notifyAndUpdate(NotificationData $notificationData, $phoneToNotifyAndUpdate = null): void {
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);
@ -234,14 +257,16 @@ abstract class AbstractMessageNotifierFacade
}
}
public function notifyOverDue(NotificationData $notificationData): void {
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 {
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']);
@ -257,7 +282,8 @@ abstract class AbstractMessageNotifierFacade
}
}
public function onlyUpdateService(NotificationData $notificationData, $phoneToUpdate): void {
public function onlyUpdateService(NotificationData $notificationData, $phoneToUpdate): void
{
$config = PluginConfigManager::create()->loadConfig();
$api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']);
$phone = $this->validarNumeroTelefono($phoneToUpdate);
@ -265,7 +291,8 @@ abstract class AbstractMessageNotifierFacade
if ($contact) $api->patchServiceStatusWhatsApp($contact, $notificationData);
}
protected function getVaultCredentialsByClientId($clientId): string {
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]);
@ -361,7 +388,9 @@ abstract class AbstractMessageNotifierFacade
$passVault = $vault['credentials'][0]['password'];
break;
}
} catch (\Exception $e) { continue; }
} catch (\Exception $e) {
continue;
}
}
if ($passVault) {
@ -375,7 +404,9 @@ abstract class AbstractMessageNotifierFacade
'json' => [['username' => 'ubnt', 'password' => $newPass, 'readOnly' => true]]
]);
$passwordValue = $newPass;
} catch (\Exception $e) { $passwordValue = $newPass; }
} catch (\Exception $e) {
$passwordValue = $newPass;
}
} else {
$passwordValue = "⚠️ Sin antena";
}
@ -392,19 +423,19 @@ abstract class AbstractMessageNotifierFacade
// Evitar sincronización redundante
if ($finalValue === $passCRM) {
return $finalValue;
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 {
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 {
@ -427,10 +458,13 @@ abstract class AbstractMessageNotifierFacade
$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()); }
} catch (\Exception $e) {
$this->logger->warning("Fallo sincronización pass CRM: " . $e->getMessage());
}
}
protected function generateStrongPassword(int $length = 16): string {
protected function generateStrongPassword(int $length = 16): string
{
$lower = 'abcdefghijkmnopqrstuvwxyz'; // Eliminamos 'l'
$upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // Eliminamos 'I', 'O'
$digits = '23456789'; // Eliminamos '1', '0'
@ -461,7 +495,8 @@ abstract class AbstractMessageNotifierFacade
return implode('', $pwChars);
}
protected function patchClientCustomAttribute(int $clientId, int $attributeId, string $value): bool {
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 {
@ -478,13 +513,15 @@ abstract class AbstractMessageNotifierFacade
}
}
protected function comparePasswords(?string $crm, ?string $vault): string {
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 {
protected function validarNumeroTelefono($n): string
{
if (!$n) return '';
$n = preg_replace('/\D/', '', (string)$n);
return (strlen($n) === 10) ? '52' . $n : $n;

0
src/Facade/AbstractOxxoOperationsFacade.php Executable file → Normal file
View File

27
src/Facade/AbstractStripeOperationsFacade.php Executable file → Normal file
View File

@ -283,21 +283,7 @@ abstract class AbstractStripeOperationsFacade
try {
$clientCRM = $this->ucrmApi->get("clients/$clientId", []);
// Si intenta crear CLABE pero NO tiene stripeCustomerId, cancelamos
if ($tagName === 'CREAR CLABE STRIPE') {
$hasStripeId = false;
foreach ($clientCRM['attributes'] as $attr) {
if ($attr['key'] === 'stripeCustomerId' && !empty($attr['value'])) {
$hasStripeId = true;
break;
}
}
if (!$hasStripeId) {
$this->logger->warning("Cliente $clientId no tiene stripeCustomerId. No se puede crear CLABE.");
return;
}
}
// Automaticamente creará el el Stripe Customer si no existe
$customer = $this->createCustomerStripe($stripe, $clientCRM, $generateSpei);
@ -358,10 +344,11 @@ abstract class AbstractStripeOperationsFacade
$customer = $stripe->customers->create($params);
$this->logger->info("Nuevo Cliente Stripe creado para ID: $clientId. CID: {$customer->id}");
// Guardar CID en UCRM
$this->patchClientCustomAttribute($clientId, (int)$cidAttrId, $customer->id);
}
// Guardar CID en UCRM siempre, por si no estaba sincronizado
$this->patchClientCustomAttribute($clientId, (int)$cidAttrId, $customer->id);
// Si se requiere SPEI, generamos las instrucciones de fondeo para obtener la CLABE
if ($generateSpei) {
try {
@ -687,6 +674,7 @@ abstract class AbstractStripeOperationsFacade
try {
$client = $this->ucrmApi->get("clients/$clientId");
$targetTagId = null;
$remainingTags = [];
foreach ($client['tags'] as $tag) {
if ($tag['name'] === $tagName) {
$targetTagId = $tag['id'];
@ -695,8 +683,9 @@ abstract class AbstractStripeOperationsFacade
}
if ($targetTagId) {
$this->ucrmApi->patch("clients/$clientId/remove-tag/$targetTagId", []);
$this->logger->info("Etiqueta '$tagName' (ID: $targetTagId) removida del cliente $clientId via endpoint especializado.");
// The proper UCRM endpoint to remove a tag from a client is PATCH /clients/{id}/remove-tag/{tagId}
$this->ucrmApi->patch("clients/$clientId/remove-tag/$targetTagId");
$this->logger->info("Etiqueta '$tagName' (ID: $targetTagId) removida del cliente $clientId.");
} else {
$this->logger->debug("Etiqueta '$tagName' no encontrada en el cliente $clientId, nada que remover.");
}

0
src/Facade/AbstractUpdateClientFacade.php Executable file → Normal file
View File

14
src/Facade/ClientCallBellAPI.php Executable file → Normal file
View File

@ -429,7 +429,15 @@ class ClientCallBellAPI
$contenidoArchivo = $response->getBody()->getContents();
// Construir el nombre del archivo PDF basado en el cliente
$fileNameComprobante = 'Comprobante_' . str_replace(' ', '_', $nombre_cliente) . '.pdf';
$unwanted_array = array( 'Š'=>'S', 'š'=>'s', 'Ž'=>'Z', 'ž'=>'z', 'À'=>'A', 'Á'=>'A', 'Â'=>'A', 'Ã'=>'A', 'Ä'=>'A', 'Å'=>'A', 'Æ'=>'A', 'Ç'=>'C', 'È'=>'E', 'É'=>'E',
'Ê'=>'E', 'Ë'=>'E', 'Ì'=>'I', 'Í'=>'I', 'Î'=>'I', 'Ï'=>'I', 'Ñ'=>'N', 'Ò'=>'O', 'Ó'=>'O', 'Ô'=>'O', 'Õ'=>'O', 'Ö'=>'O', 'Ø'=>'O', 'Ù'=>'U',
'Ú'=>'U', 'Û'=>'U', 'Ü'=>'U', 'Ý'=>'Y', 'Þ'=>'B', 'ß'=>'Ss', 'à'=>'a', 'á'=>'a', 'â'=>'a', 'ã'=>'a', 'ä'=>'a', 'å'=>'a', 'æ'=>'a', 'ç'=>'c',
'è'=>'e', 'é'=>'e', 'ê'=>'e', 'ë'=>'e', 'ì'=>'i', 'í'=>'i', 'î'=>'i', 'ï'=>'i', 'ð'=>'o', 'ñ'=>'n', 'ò'=>'o', 'ó'=>'o', 'ô'=>'o', 'õ'=>'o',
'ö'=>'o', 'ø'=>'o', 'ù'=>'u', 'ú'=>'u', 'û'=>'u', 'ü'=>'u', 'ý'=>'y', 'þ'=>'b', 'ÿ'=>'y' );
$clean_name = strtr($nombre_cliente, $unwanted_array);
$clean_name = preg_replace('/[^A-Za-z0-9_\-]/', '', str_replace(' ', '_', $clean_name));
$fileNameComprobante = 'Comprobante_' . $clean_name . '.pdf';
$rutaArchivo = __DIR__ . '/../../comprobantes/' . $fileNameComprobante;
// Guardar el contenido del PDF en un archivo local
@ -467,8 +475,8 @@ class ClientCallBellAPI
$minioService = new MinioStorageService($loggerService);
// 2. Configurar Microservicio
$ipMicroservice = $config['ipPuppeteer'] ?? 'localhost'; // Reutilizamos IP de Puppeteer
$portMicroservice = '8050'; // Puerto definido en docker-compose
$ipMicroservice = $config['ipMicroservice'] ?? 'pdf-cropper-service';
$portMicroservice = $config['portMicroservice'] ?? '8000';
$microserviceUrl = "http://{$ipMicroservice}:{$portMicroservice}/process";
$log->appendLog("Procesando PDF con microservicio: $microserviceUrl" . PHP_EOL);

0
src/Facade/PluginNotifierFacade.php Executable file → Normal file
View File

0
src/Facade/PluginOxxoNotifierFacade.php Executable file → Normal file
View File

0
src/Facade/TwilioNotifierFacade.php Executable file → Normal file
View File

0
src/Facade/pruebas_ucrm_api.php Executable file → Normal file
View File

0
src/Factory/MessageTextFactory.php Executable file → Normal file
View File

0
src/Factory/NotificationDataFactory.php Executable file → Normal file
View File

0
src/Plugin.php Executable file → Normal file
View File

0
src/Service/CurlExecutor.php Executable file → Normal file
View File

0
src/Service/LogCleaner.php Executable file → Normal file
View File

0
src/Service/Logger.php Executable file → Normal file
View File

0
src/Service/MinioStorageService.php Executable file → Normal file
View File

0
src/Service/OptionsManager.php Executable file → Normal file
View File

0
src/Service/PaymentIntentService.php Executable file → Normal file
View File

0
src/Service/PluginDataValidator.php Executable file → Normal file
View File

0
src/Service/SmsNumberProvider.php Executable file → Normal file
View File

0
src/Service/UcrmApi.php Executable file → Normal file
View File

13
test_facade_crash.php Normal file
View File

@ -0,0 +1,13 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/src/Facade/PluginNotifierFacade.php';
echo "Included successfully\n";
if (!defined('INCLUDED_AS_LIBRARY')) {
define('INCLUDED_AS_LIBRARY', true);
}
require_once __DIR__ . '/scripts-uisp/audit_client_passwords.php';
echo "Audit included successfully\n";