feat(stripe-sync): implementar resolución dinámica de métodos de pago y corregir validación API

- Se agregó 'getPaymentMethodIdByName' para buscar automáticamente el ID de "Transferencia bancaria" por nombre, asegurando portabilidad entre servidores UISP.
- Se implementó el manejo del webhook 'customer_cash_balance_transaction.created' para el registro automático de pagos fondeados.
- Fix: Se corrigió error 422 en la API de UCRM forzando el cast de 'clientId' a integer y 'methodId' a string (GUID).
- Se actualizó la documentación (README/CHANGELOG) con instrucciones de configuración de webhooks.
This commit is contained in:
DANYDHSV 2025-12-23 14:50:57 -06:00
parent 506615e911
commit 0e37fd153f
8 changed files with 2728 additions and 113 deletions

View File

@ -4,6 +4,14 @@ Este plugin sincroniza los clientes del sitema UISP CRM con los contactos de Wha
# REGISTRO DE CAMBIOS
## VERSIÓN 2.9.3 - 23-12-2025
### 🟢 Novedades
1⃣ Resolución dinámica del ID del método de pago ("Transferencia bancaria") mediante consulta a la API de UISP, mejorando la portabilidad del plugin entre distintos servidores.
2⃣ Implementación de registro de pago automático desde Webhook Stripe para eventos de tipo `customer_cash_balance_transaction.created` (Saldo aplicado).
### 🟡 Bugs Resueltos
1⃣ Se corrigió el error de validación de la API de UCRM (422) mediante el cast explícito de `clientId` a integer y el uso de `methodId` como string.
## VERSIÓN 2.9.2
### 🟡 Bugs Resueltos
1⃣ Se solucionó un bug que impedía obtener la contraseñas de la bóveda, ya que el response de la API cambió en la última actualización y la esstructura nueva impedía acceder al dato del password

File diff suppressed because one or more lines are too long

View File

@ -202,18 +202,6 @@
"label": "Borrar comprobantes Wordpress",
"type": "admin",
"target": "iframe"
},
{
"key": "Reports",
"label": "Generador de Clabes CBM en Stripe",
"type": "admin",
"target": "iframe"
},
{
"key": "Reports",
"label": "Obtener datos del Network de Cliente",
"type": "admin",
"target": "iframe"
}
],
"supportsWebhookEvents": true

View File

@ -605,22 +605,15 @@ abstract class AbstractMessageNotifierFacade
"date" => $formattedDate,
"jobDescription" => $jobDescription,
"gmapsLocation" => $googleMapsUrl,
"passwordAntenaCliente" => ''
];
//obtener la contraseña de la antena con el método getVaultCredentialsByClientId y compararlo con el campo de la contraseña de la antena en el CRM
$passwordVault = $this->getVaultCredentialsByClientId($arrayClientCRM['id']);
// Validar si la contraseña de la antena del cliente en el CRM es igual a la contraseña de la antena del cliente en el Vault
if ($passwordAntenaClienteCRM === $passwordVault) {
$this->logger->debug('La contraseña de la antena del cliente en el CRM
es igual a la contraseña de la antena del cliente en el Vault.' . PHP_EOL);
$jsonInstallerJobNotificationData['passwordAntenaCliente'] = $passwordAntenaClienteCRM;
} else {
$this->logger->debug('La contraseña de la antena del cliente en el CRM
no es igual a la contraseña de la antena del cliente en el Vault.' . PHP_EOL);
$jsonInstallerJobNotificationData['passwordAntenaCliente'] = $passwordVault;
}
//Comparar las contraseñas y obtener la que se enviará al instalador
$jsonInstallerJobNotificationData['passwordAntenaCliente'] = $this->comparePasswords($passwordAntenaClienteCRM, $passwordVault);
@ -638,7 +631,7 @@ abstract class AbstractMessageNotifierFacade
$jsonInstallerJobNotificationData,
false,
false,
$this->getVaultCredentialsByClientId($arrayClientCRM['id'])
);
if ($result === false) {
@ -660,9 +653,19 @@ abstract class AbstractMessageNotifierFacade
"date" => $formattedDate,
"jobDescription" => $jobDescription,
"gmapsLocation" => $googleMapsUrl,
"passwordAntenaCliente" => ''
];
//obtener la contraseña de la antena con el método getVaultCredentialsByClientId y compararlo con el campo de la contraseña de la antena en el CRM
$passwordVault = $this->getVaultCredentialsByClientId($arrayClientCRM['id']);
//Comparar las contraseñas y obtener la que se enviará al instalador
$jsonInstallerJobNotificationData['passwordAntenaCliente'] = $this->comparePasswords($passwordAntenaClienteCRM, $passwordVault);
$attempts = 0;
$maxAttempts = 3;
$result = false;
@ -675,7 +678,7 @@ abstract class AbstractMessageNotifierFacade
$jsonInstallerJobNotificationData,
$reprogramming,
$changeInstaller,
$this->getVaultCredentialsByClientId($arrayClientCRM['id'])
);
if ($result === false) {
@ -1836,4 +1839,35 @@ abstract class AbstractMessageNotifierFacade
}
function comparePasswords($passwordVault, $passwordAntenaClienteCRM): string
{
$erroresVaultPrefix = [
'Error:',
'No se encon',
'Falla en',
];
// Validar si la contraseña de la antena del cliente en el CRM es igual a la contraseña de la antena del cliente en el Vault y de ser iguales devolver la contraseña del vault
if ($passwordAntenaClienteCRM === $passwordVault) {
$this->logger->debug('La contraseña de la antena del cliente en el CRM
es igual a la contraseña de la antena del cliente en el Vault.' . PHP_EOL);
return $passwordVault;
} else {
//si no son iguales entonces validar si alguna de las contraseñas no contiene un prefijo de error para decidir cuál devolver; si
if (!empty($passwordAntenaClienteCRM) && !array_filter($erroresVaultPrefix, fn($prefix) => str_starts_with($passwordAntenaClienteCRM, $prefix))) { //verificar que la contraseña del CRM no comience con ninguno de los prefijos de error o esté vacía
$this->logger->debug('La contraseña de la antena del cliente en el CRM será la que se envíe al instalador.' . PHP_EOL);
return $passwordAntenaClienteCRM;
} elseif (!empty($passwordVault) && !array_filter($erroresVaultPrefix, fn($prefix) => str_starts_with($passwordVault, $prefix))) { //verificar que la contraseña del vault no comience con ninguno de los prefijos de error o esté vacía
$this->logger->debug('La contraseña de la antena del cliente en el Vault será la que se envíe al instalador.' . PHP_EOL);
return $passwordVault;
} else {
//asignar la variable con un texto de advertencia sobre que ninguna de las contraseñas es válida para ser enviada al instalador
$this->logger->debug('Ninguna de las contraseñas de la antena es válida para ser enviada al instalador.' . PHP_EOL);
return '⚠️ Probar una contraseña conocida como Siip.567 etc. ya que no se encontró una contraseña en el UISP Vault ni en el CRM.';
}
}
}
}

View File

@ -135,6 +135,59 @@ abstract class AbstractStripeOperationsFacade
}
}
public function registerPaymentFromWebhook($event_json)
{
$this->logger->info("Procesando pago funded desde webhook..." . PHP_EOL);
$configManager = \Ubnt\UcrmPluginSdk\Service\PluginConfigManager::create();
$config = $configManager->loadConfig();
$StripeToken = $config['tokenstripe'];
$stripe = new \Stripe\StripeClient($StripeToken);
$data = $event_json['data']['object'];
if (isset($data['applied_to_payment']['payment_intent'])) {
$piId = $data['applied_to_payment']['payment_intent'];
try {
$pi = $stripe->paymentIntents->retrieve($piId);
$clientId = $pi->metadata->clientId ?? null;
$amount = abs($data['net_amount']) / 100;
if ($clientId) {
$this->ucrmApi = UcrmApi::create();
// Dynamic lookup for payment method ID
$methodId = null;
$methods = $this->ucrmApi->get('payment-methods');
foreach ($methods as $method) {
if ($method['name'] === 'Transferencia bancaria') {
$methodId = $method['id'];
break;
}
}
if (!$methodId) {
$this->logger->error("Error registrando pago: No se encontró el método 'Transferencia bancaria'");
return;
}
$this->ucrmApi->post('payments', [
'clientId' => (int)$clientId,
'amount' => $amount,
'currencyCode' => strtoupper($pi->currency),
'methodId' => $methodId,
'note' => "Pago via Webhook Stripe (Saldo Aplicado) - PI: $piId",
'createdDate' => date('c'),
]);
$this->logger->info("Pago registrado en UCRM para el cliente $clientId");
} else {
$this->logger->warning("No se encontró clientId en metadata del PaymentIntent $piId");
}
} catch (\Exception $e) {
$this->logger->error("Error registrando pago: " . $e->getMessage());
}
}
}
/*
* Creates the Stripe Customer

View File

@ -218,7 +218,7 @@ class ClientCallBellAPI
return false;
}
public function sendJobNotificationWhatsAppToInstaller($installerWhatsAppNumber, $jobInstallerNotificationData, $reprogramming, $changeInstaller, $passwordAntenaCliente): bool
public function sendJobNotificationWhatsAppToInstaller($installerWhatsAppNumber, $jobInstallerNotificationData, $reprogramming, $changeInstaller): bool
{
$log = PluginLogManager::create(); //Initialize Logger
@ -271,7 +271,7 @@ class ClientCallBellAPI
$campo1_combinado = "$installerName se reprogramó una tarea con el folio $jobId para el cliente $clientFullName, la nueva fecha será el $date";
$campo2 = $jobInstallerNotificationData['clientWhatsApp'];
$campo3 = $jobInstallerNotificationData['gmapsLocation'];
$campo4 = $passwordAntenaCliente;
$campo4 = $jobInstallerNotificationData['passwordAntenaCliente'];
//Enviar notificación de reprogramación
$log->appendLog("Enviando notificación de reprogramación al instalador, valor de reprogramming $reprogramming y valor de changeInstaller $changeInstaller " . PHP_EOL);
@ -295,7 +295,7 @@ class ClientCallBellAPI
$campo1_combinado = "$installerName se te ha asignado una tarea con folio $jobId, del cliente $clientFullName, para el $date";
$campo2 = $jobInstallerNotificationData['clientWhatsApp'];
$campo3 = $jobInstallerNotificationData['gmapsLocation'];
$campo4 = $passwordAntenaCliente;
$campo4 = $jobInstallerNotificationData['passwordAntenaCliente'];
$log->appendLog("Enviando notificación normal de tarea al instalador, valor de reprogramming $reprogramming y valor de changeInstaller $changeInstaller " . PHP_EOL);
$curl_string = "{\n \"to\": \"$installerWhatsAppNumber\",\n \"from\": \"whatsapp\",\n \"type\": \"text\",\n \"content\": {\n \"text\": \"S/M\"\n },\n \"template_values\": [\"$campo1_combinado\", \"$campo2\", \"$campo3\", \"$campo4\"],\n \"template_uuid\": \"88eeb6420a214fd8870dd28d741021c4\",\n \"optin_contact\": true\n }";
@ -309,6 +309,9 @@ class ClientCallBellAPI
curl_close($ch);
// Validar la respuesta de Callbell
$jsonResponse = json_decode($response, true);

View File

@ -136,7 +136,7 @@ class Plugin
if ($jsonData['data']['object']['type'] === 'applied_to_payment') {
$this->logger->info('Se aplicó el saldo en Stripe de un pago: ' . json_encode($jsonData) . PHP_EOL);
$this->pluginNotifierFacade->registerPaymentFromWebhook($jsonData);
}elseif ($jsonData['data']['object']['type'] === 'unapplied_from_payment'){
//ejemplo de json para transferencia de dinero cancelada: {"id":"evt_1RlEGgEFY1WEUtgR6Bp2DzDP","object":"event","api_version":"2023-10-16","created":1752606717,"data":{"object":{"id":"ccsbtxn_1RlEGfEFY1WEUtgRv8jAUGmE","object":"customer_cash_balance_transaction","created":1752606717,"currency":"mxn","customer":"cus_PetN1dhr4rx0kX","ending_balance":18000,"livemode":false,"net_amount":18000,"type":"unapplied_from_payment","unapplied_from_payment":{"payment_intent":"pi_3RlDPdEFY1WEUtgR1JBgNhTQ"}}},"livemode":false,"pending_webhooks":2,"request":{"id":"req_954mskVBfAI0jn","idempotency_key":"749518f6-baa0-4ae9-99e4-8029a35719aa"},"type":"customer_cash_balance_transaction.created"}
$paymentIntentId = $jsonData['data']['object']['unapplied_from_payment']['payment_intent'];

View File

@ -5,7 +5,7 @@
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => '2e21e09c2c0bf6b481fa5c3cab13a5aec6487c74',
'reference' => '506615e9116829588e84ecacf5e6e28769e6dd87',
'name' => 'ucrm-plugins/sms-twilio',
'dev' => false,
),
@ -307,7 +307,7 @@
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => '2e21e09c2c0bf6b481fa5c3cab13a5aec6487c74',
'reference' => '506615e9116829588e84ecacf5e6e28769e6dd87',
'dev_requirement' => false,
),
),