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:
parent
506615e911
commit
0e37fd153f
@ -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
|
||||||
|
|||||||
2695
data/plugin.log
2695
data/plugin.log
File diff suppressed because one or more lines are too long
@ -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
|
||||||
|
|||||||
@ -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.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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'];
|
||||||
|
|||||||
4
vendor/composer/installed.php
vendored
4
vendor/composer/installed.php
vendored
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user