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 # 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 ## VERSIÓN 2.9.2
### 🟡 Bugs Resueltos ### 🟡 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 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", "label": "Borrar comprobantes Wordpress",
"type": "admin", "type": "admin",
"target": "iframe" "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 "supportsWebhookEvents": true

View File

@ -605,22 +605,15 @@ abstract class AbstractMessageNotifierFacade
"date" => $formattedDate, "date" => $formattedDate,
"jobDescription" => $jobDescription, "jobDescription" => $jobDescription,
"gmapsLocation" => $googleMapsUrl, "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 //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']); $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 //Comparar las contraseñas y obtener la que se enviará al instalador
if ($passwordAntenaClienteCRM === $passwordVault) { $jsonInstallerJobNotificationData['passwordAntenaCliente'] = $this->comparePasswords($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;
}
@ -638,7 +631,7 @@ abstract class AbstractMessageNotifierFacade
$jsonInstallerJobNotificationData, $jsonInstallerJobNotificationData,
false, false,
false, false,
$this->getVaultCredentialsByClientId($arrayClientCRM['id'])
); );
if ($result === false) { if ($result === false) {
@ -660,9 +653,19 @@ abstract class AbstractMessageNotifierFacade
"date" => $formattedDate, "date" => $formattedDate,
"jobDescription" => $jobDescription, "jobDescription" => $jobDescription,
"gmapsLocation" => $googleMapsUrl, "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; $attempts = 0;
$maxAttempts = 3; $maxAttempts = 3;
$result = false; $result = false;
@ -675,7 +678,7 @@ abstract class AbstractMessageNotifierFacade
$jsonInstallerJobNotificationData, $jsonInstallerJobNotificationData,
$reprogramming, $reprogramming,
$changeInstaller, $changeInstaller,
$this->getVaultCredentialsByClientId($arrayClientCRM['id'])
); );
if ($result === false) { 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 * Creates the Stripe Customer

View File

@ -218,7 +218,7 @@ class ClientCallBellAPI
return false; return false;
} }
public function sendJobNotificationWhatsAppToInstaller($installerWhatsAppNumber, $jobInstallerNotificationData, $reprogramming, $changeInstaller, $passwordAntenaCliente): bool public function sendJobNotificationWhatsAppToInstaller($installerWhatsAppNumber, $jobInstallerNotificationData, $reprogramming, $changeInstaller): bool
{ {
$log = PluginLogManager::create(); //Initialize Logger $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"; $campo1_combinado = "$installerName se reprogramó una tarea con el folio $jobId para el cliente $clientFullName, la nueva fecha será el $date";
$campo2 = $jobInstallerNotificationData['clientWhatsApp']; $campo2 = $jobInstallerNotificationData['clientWhatsApp'];
$campo3 = $jobInstallerNotificationData['gmapsLocation']; $campo3 = $jobInstallerNotificationData['gmapsLocation'];
$campo4 = $passwordAntenaCliente; $campo4 = $jobInstallerNotificationData['passwordAntenaCliente'];
//Enviar notificación de reprogramación //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); $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"; $campo1_combinado = "$installerName se te ha asignado una tarea con folio $jobId, del cliente $clientFullName, para el $date";
$campo2 = $jobInstallerNotificationData['clientWhatsApp']; $campo2 = $jobInstallerNotificationData['clientWhatsApp'];
$campo3 = $jobInstallerNotificationData['gmapsLocation']; $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); $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 }"; $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); curl_close($ch);
// Validar la respuesta de Callbell // Validar la respuesta de Callbell
$jsonResponse = json_decode($response, true); $jsonResponse = json_decode($response, true);

View File

@ -136,7 +136,7 @@ class Plugin
if ($jsonData['data']['object']['type'] === 'applied_to_payment') { 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->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'){ }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"} //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']; $paymentIntentId = $jsonData['data']['object']['unapplied_from_payment']['payment_intent'];

View File

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