corregido doble payment intent y reenvio de comprobantes

This commit is contained in:
DANYDHSV 2026-01-31 11:44:23 -06:00
parent d2ce14a7e3
commit 24c32f6334
7 changed files with 3358 additions and 9875 deletions

View File

@ -1 +1 @@
{"ipserver":"venus.siip.mx","apitoken":"gvcnIJqXdUjneVSjhl6THLlQcYXJyIFCcwHKVba2bvIrNraanCTb5VeoWuJ0TFZ9","unmsApiToken":"079c28f5-888c-457d-bd7a-0a4202590f75","tokencallbell":"g8thcZkXGd3xBj2g3TtYNYFMH1fuesbJ.b6a940ea7d78cf6c9e42f067b21c8ddf96e9fa2a9e307bfd0c7c7c4d7fa38f79","tokenstripe":"sk_test_51OkG0REFY1WEUtgRH6UxBK5pu80Aq5Iy8EcdPnf0cOWzuVLQTpyLCd7CbPzqMsWMafZOHElCxhEHF7g8boURjWlJ00tBwE0W1M","hostServerFTP":"siip.mx","usernameServerFTP":"siip0001","passServerFTP":"$spGiT,[wa)n","ipPuppeteer":"172.16.5.134","portPuppeteer":"4100","idPaymentAdminCRM":"1180","cashPaymentMethodId":false,"courtesyPaymentMethodId":false,"bankTransferPaymentMethodId":true,"paypalPaymentMethodId":true,"creditCardPaypalPaymentMethodId":true,"creditCardStripePaymentMethodId":true,"stripeSubscriptionCreditCardPaymentMethodId":true,"paypalSubscriptionPaymentMethodId":true,"mercadopagoPaymentMethodId":true,"checkPaymentMethodId":true,"customPaymentMethodId":true,"notificationTypeText":false,"installersDataWhatsApp":"{\r\n \"instaladores\": [\r\n {\r\n \"id\": 1019,\r\n \"nombre\": \"Mucio Robledo\",\r\n \"whatsapp\": \"4181878106\"\r\n },\r\n {\r\n \"id\": 1173,\r\n \"nombre\": \"Ángel Arvizu\",\r\n \"whatsapp\": \"4181878106\"\r\n },\r\n {\r\n \"id\": 1172,\r\n \"nombre\": \"Juan Rostro\",\r\n \"whatsapp\": \"4181878106\"\r\n },\r\n {\r\n \"id\": 1015,\r\n \"nombre\": \"Daniel Humberto\",\r\n \"whatsapp\": \"4181878106\"\r\n },\r\n {\r\n \"id\": 1131,\r\n \"nombre\": \"Gricelda Avalos\",\r\n \"whatsapp\": \"4181817609\"\r\n }\r\n ]\r\n}","debugMode":true,"minioEndpoint":"http://172.16.5.134:9002","minioPublicUrl":"https://aws-venus.siip.mx","minioAccessKey":"minioadmin","minioSecretKey":"minioadmin","minioBucket":"vouchers-oxxo","logging_level":true} {"ipserver":"venus.siip.mx","apitoken":"gvcnIJqXdUjneVSjhl6THLlQcYXJyIFCcwHKVba2bvIrNraanCTb5VeoWuJ0TFZ9","unmsApiToken":"079c28f5-888c-457d-bd7a-0a4202590f75","tokencallbell":"g8thcZkXGd3xBj2g3TtYNYFMH1fuesbJ.b6a940ea7d78cf6c9e42f067b21c8ddf96e9fa2a9e307bfd0c7c7c4d7fa38f79","tokenstripe":"sk_test_51OkG0REFY1WEUtgRH6UxBK5pu80Aq5Iy8EcdPnf0cOWzuVLQTpyLCd7CbPzqMsWMafZOHElCxhEHF7g8boURjWlJ00tBwE0W1M","hostServerFTP":"siip.mx","usernameServerFTP":"siip0001","passServerFTP":"$spGiT,[wa)n","ipPuppeteer":"172.16.5.134","portPuppeteer":"4100","idPaymentAdminCRM":"1180","cashPaymentMethodId":true,"courtesyPaymentMethodId":true,"bankTransferPaymentMethodId":true,"paypalPaymentMethodId":true,"creditCardPaypalPaymentMethodId":true,"creditCardStripePaymentMethodId":true,"stripeSubscriptionCreditCardPaymentMethodId":true,"paypalSubscriptionPaymentMethodId":true,"mercadopagoPaymentMethodId":true,"checkPaymentMethodId":true,"customPaymentMethodId":true,"notificationTypeText":false,"installersDataWhatsApp":"{\r\n \"instaladores\": [\r\n {\r\n \"id\": 1019,\r\n \"nombre\": \"Mucio Robledo\",\r\n \"whatsapp\": \"4181878106\"\r\n },\r\n {\r\n \"id\": 1173,\r\n \"nombre\": \"Ángel Arvizu\",\r\n \"whatsapp\": \"4181878106\"\r\n },\r\n {\r\n \"id\": 1172,\r\n \"nombre\": \"Juan Rostro\",\r\n \"whatsapp\": \"4181878106\"\r\n },\r\n {\r\n \"id\": 1015,\r\n \"nombre\": \"Daniel Humberto\",\r\n \"whatsapp\": \"4181878106\"\r\n },\r\n {\r\n \"id\": 1131,\r\n \"nombre\": \"Gricelda Avalos\",\r\n \"whatsapp\": \"4181817609\"\r\n }\r\n ]\r\n}","debugMode":true,"minioEndpoint":"http://172.16.5.134:9002","minioPublicUrl":"https://aws-venus.siip.mx","minioAccessKey":"minioadmin","minioSecretKey":"minioadmin","minioBucket":"vouchers-oxxo","logging_level":true,"oxxoPayPaymentMethodId":true,"creditDebitCardPaymentMethodId":true}

File diff suppressed because one or more lines are too long

View File

@ -189,6 +189,20 @@
"required": 0, "required": 0,
"type": "checkbox" "type": "checkbox"
}, },
{
"key": "oxxoPayPaymentMethodId",
"label": "Envío de Comprobante por pago de OXXO Pay",
"description": "Habilita el envío de comprobantes en formato de imagen por WhatsApp cuando el método de pago es OXXO Pay",
"required": 0,
"type": "checkbox"
},
{
"key": "creditDebitCardPaymentMethodId",
"label": "Envío de Comprobante por pago de Tarjeta de Crédito/Débito (Genérico)",
"description": "Habilita el envío de comprobantes en formato de imagen por WhatsApp cuando el método de pago es Tarjeta de Crédito/Débito (Genérico)",
"required": 0,
"type": "checkbox"
},
{ {
"key": "notificationTypeText", "key": "notificationTypeText",
"label": "Envío de Comprobante por medio de plantilla de texto", "label": "Envío de Comprobante por medio de plantilla de texto",

View File

@ -21,7 +21,8 @@ abstract class AbstractStripeOperationsFacade
protected $ucrmApi; protected $ucrmApi;
private $systemAttributesCache = null; private $systemAttributesCache = null;
public function __construct(Logger $logger, MessageTextFactory $messageTextFactory, SmsNumberProvider $clientPhoneNumber) { public function __construct(Logger $logger, MessageTextFactory $messageTextFactory, SmsNumberProvider $clientPhoneNumber)
{
$this->logger = $logger; $this->logger = $logger;
$this->messageTextFactory = $messageTextFactory; $this->messageTextFactory = $messageTextFactory;
$this->clientPhoneNumber = $clientPhoneNumber; $this->clientPhoneNumber = $clientPhoneNumber;
@ -38,7 +39,8 @@ abstract class AbstractStripeOperationsFacade
$this->ucrmApi = new UcrmApi($client, $config['apitoken'] ?? ''); $this->ucrmApi = new UcrmApi($client, $config['apitoken'] ?? '');
} }
public function createPaymentIntent(array $eventJson): void { public function createPaymentIntent(array $eventJson): void
{
$config = PluginConfigManager::create()->loadConfig(); $config = PluginConfigManager::create()->loadConfig();
$stripe = new StripeClient($config['tokenstripe']); $stripe = new StripeClient($config['tokenstripe']);
@ -54,6 +56,39 @@ abstract class AbstractStripeOperationsFacade
$stripeCustomer = $stripe->customers->retrieve($customer); $stripeCustomer = $stripe->customers->retrieve($customer);
$ucrmClientId = $stripeCustomer->metadata->ucrm_client_id ?? null; $ucrmClientId = $stripeCustomer->metadata->ucrm_client_id ?? null;
// [NEW] Check for existing PENDING PaymentIntent to avoid duplicates
// Especially useful if webhook is retried or if one was already created.
$existingPIs = $stripe->paymentIntents->search([
'query' => "customer:'$customer' AND status:'requires_payment_method' AND amount>=" . ((int)$amount - 100) . " AND amount<=" . ((int)$amount + 100) . " AND metadata['tipoPago']:'Transferencia Bancaria'",
'limit' => 1
]);
// Note: Range check just in case, or exact check. Using exact check is safer if amount is precise.
// Let's use exact check for now, but sometimes small variations happen? No, Bank Transfer is exact.
// Actually, 'requires_payment_method' or 'requires_action' or 'processing'?
// If it's bank transfer funded, it might be in 'requires_confirmation' if not auto-confirmed.
// But if we are CREATING it, we want to know if we already created one that is waiting.
// If we created it with confirm=true, it should transition to succeeded immediately if funds are available (which they are, cause this event says so).
// However, IF the previous attempt failed or timed out but created the PI...
// BETTER: Check if we have processed this Event ID before?
// The method doesn't receive Event ID in the args easily (it's in $eventJson['id']).
// But we don't store Event IDs in DB.
// Let's stick to checking if there is a PI created recently (last 5 mins?) with same amount?
// Stripe Search API is powerful.
// query: "customer:'$customer' AND amount=$amount AND created>" . (time() - 300)
$fiveMinsAgo = time() - 300;
$existingPIs = $stripe->paymentIntents->search([
'query' => "customer:'$customer' AND amount=" . (int)$amount . " AND created>$fiveMinsAgo AND metadata['createdBy']:'UCRM'",
'limit' => 1
]);
if (count($existingPIs->data) > 0) {
$this->logger->info("PaymentIntent duplicado evitado. Ya existe uno reciente tras el evento de fondos. ID: " . $existingPIs->data[0]->id);
return;
}
$pi = $stripe->paymentIntents->create([ $pi = $stripe->paymentIntents->create([
'amount' => (int)$amount, 'amount' => (int)$amount,
'currency' => 'mxn', 'currency' => 'mxn',
@ -73,14 +108,15 @@ abstract class AbstractStripeOperationsFacade
'signedInAdminId' => $config['idPaymentAdminCRM'], 'signedInAdminId' => $config['idPaymentAdminCRM'],
'tipoPago' => 'Transferencia Bancaria' 'tipoPago' => 'Transferencia Bancaria'
], ],
]); ], ['idempotency_key' => $eventJson['id'] ?? null]);
$this->logger->info("PaymentIntent creado: " . $pi->id); $this->logger->info("PaymentIntent creado: " . $pi->id);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error("Error creando PaymentIntent: " . $e->getMessage()); $this->logger->error("Error creando PaymentIntent: " . $e->getMessage());
} }
} }
public function registerPaymentFromWebhook(array $eventJson): void { public function registerPaymentFromWebhook(array $eventJson): void
{
$config = PluginConfigManager::create()->loadConfig(); $config = PluginConfigManager::create()->loadConfig();
$stripe = new StripeClient($config['tokenstripe']); $stripe = new StripeClient($config['tokenstripe']);
$data = $eventJson['data']['object']; $data = $eventJson['data']['object'];
@ -110,7 +146,8 @@ abstract class AbstractStripeOperationsFacade
} }
} }
public function registerPaymentFromIntent(array $data): void { public function registerPaymentFromIntent(array $data): void
{
$piId = $data['id'] ?? null; $piId = $data['id'] ?? null;
if (!$piId) return; if (!$piId) return;
@ -135,6 +172,31 @@ abstract class AbstractStripeOperationsFacade
return; return;
} }
// [NEW] Check for duplicate payment (same PI ID)
try {
$existingPayments = $this->ucrmApi->get('payments', [
'clientId' => $clientId,
'limit' => 20,
'order' => 'createdDate',
'direction' => 'DESC'
]);
foreach ($existingPayments as $p) {
// Check note for PI ID
if (isset($p['note']) && strpos($p['note'], $piId) !== false) {
$this->logger->info("Pago duplicado detectado para PI $piId (ID existente: {$p['id']}). Omitiendo creación.");
return;
}
// Also check duplicate by transaction ID if applicable
if (isset($p['transactionId']) && $p['transactionId'] === $piId) {
$this->logger->info("Pago duplicado detectado (Transaction ID) para PI $piId (ID existente: {$p['id']}). Omitiendo creación.");
return;
}
}
} catch (\Exception $e) {
$this->logger->warning("Falló la verificación de duplicados para PI $piId: " . $e->getMessage());
}
try { try {
// Intentar detectar Payment Method name basado en tipo // Intentar detectar Payment Method name basado en tipo
$type = $data['payment_method_types'][0] ?? 'card'; $type = $data['payment_method_types'][0] ?? 'card';
@ -166,7 +228,8 @@ abstract class AbstractStripeOperationsFacade
} }
} }
private function findClientIdByStripeCustomer(string $stripeCustomerId): ?int { private function findClientIdByStripeCustomer(string $stripeCustomerId): ?int
{
try { try {
// Nota: Esto puede ser lento si hay muchos clientes, pero es un fallback. // Nota: Esto puede ser lento si hay muchos clientes, pero es un fallback.
// Idealmente usaríamos $this->ucrmApi->get('clients', ['customAttributeKey' => 'stripeCustomerId', ...]) si existiera ese filtro. // Idealmente usaríamos $this->ucrmApi->get('clients', ['customAttributeKey' => 'stripeCustomerId', ...]) si existiera ese filtro.
@ -195,7 +258,8 @@ abstract class AbstractStripeOperationsFacade
return null; return null;
} }
private function findPaymentMethodId(string $name): ?int { private function findPaymentMethodId(string $name): ?int
{
try { try {
$methods = $this->ucrmApi->get('payment-methods'); $methods = $this->ucrmApi->get('payment-methods');
foreach ($methods as $m) { foreach ($methods as $m) {
@ -203,11 +267,13 @@ abstract class AbstractStripeOperationsFacade
return $m['id']; return $m['id'];
} }
} }
} catch(\Exception $e) {} } catch (\Exception $e) {
}
return null; return null;
} }
public function createStripeClient(NotificationData $notificationData, string $tagName, bool $generateSpei = true): void { public function createStripeClient(NotificationData $notificationData, string $tagName, bool $generateSpei = true): void
{
$clientId = $notificationData->clientId; $clientId = $notificationData->clientId;
if (!$clientId) return; if (!$clientId) return;
@ -246,7 +312,8 @@ abstract class AbstractStripeOperationsFacade
} }
} }
protected function createCustomerStripe(StripeClient $stripe, array $clientCRM, bool $generateSpei): ?\Stripe\Customer { protected function createCustomerStripe(StripeClient $stripe, array $clientCRM, bool $generateSpei): ?\Stripe\Customer
{
$clientId = $clientCRM['id']; $clientId = $clientCRM['id'];
// Extraer email de contactos (prioridad) o username // Extraer email de contactos (prioridad) o username
@ -324,7 +391,8 @@ abstract class AbstractStripeOperationsFacade
return $customer; return $customer;
} }
protected function getVaultCredentialsByClientId($clientId): string { protected function getVaultCredentialsByClientId($clientId): string
{
$config = PluginConfigManager::create()->loadConfig(); $config = PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? ''; $ipServer = $config['ipserver'] ?? '';
@ -415,7 +483,9 @@ abstract class AbstractStripeOperationsFacade
$passVault = $vault['credentials'][0]['password']; $passVault = $vault['credentials'][0]['password'];
break; break;
} }
} catch (\Exception $e) { continue; } } catch (\Exception $e) {
continue;
}
} }
if ($passVault) { if ($passVault) {
@ -429,7 +499,9 @@ abstract class AbstractStripeOperationsFacade
'json' => [['username' => 'ubnt', 'password' => $newPass, 'readOnly' => true]] 'json' => [['username' => 'ubnt', 'password' => $newPass, 'readOnly' => true]]
]); ]);
$passwordValue = $newPass; $passwordValue = $newPass;
} catch (\Exception $e) { $passwordValue = $newPass; } } catch (\Exception $e) {
$passwordValue = $newPass;
}
} else { } else {
$passwordValue = "⚠️ Sin antena"; $passwordValue = "⚠️ Sin antena";
} }
@ -451,14 +523,14 @@ abstract class AbstractStripeOperationsFacade
$this->syncPasswordWithCrm((int)$clientId, $finalValue); $this->syncPasswordWithCrm((int)$clientId, $finalValue);
return $finalValue; return $finalValue;
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error("Excepción en getVaultCredentialsByClientId (Cliente: $clientId): " . $e->getMessage()); $this->logger->error("Excepción en getVaultCredentialsByClientId (Cliente: $clientId): " . $e->getMessage());
return 'Error: ' . $e->getMessage(); return 'Error: ' . $e->getMessage();
} }
} }
private function syncPasswordWithCrm(int $clientId, string $passVault): void { private function syncPasswordWithCrm(int $clientId, string $passVault): void
{
try { try {
$clientData = $this->ucrmApi->get("clients/$clientId"); $clientData = $this->ucrmApi->get("clients/$clientId");
@ -484,7 +556,8 @@ abstract class AbstractStripeOperationsFacade
} }
} }
protected function generateStrongPassword(int $length = 16): string { protected function generateStrongPassword(int $length = 16): string
{
$lower = 'abcdefghijkmnopqrstuvwxyz'; // Eliminamos 'l' $lower = 'abcdefghijkmnopqrstuvwxyz'; // Eliminamos 'l'
$upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // Eliminamos 'I', 'O' $upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // Eliminamos 'I', 'O'
$digits = '23456789'; // Eliminamos '1', '0' $digits = '23456789'; // Eliminamos '1', '0'
@ -515,7 +588,8 @@ abstract class AbstractStripeOperationsFacade
return implode('', $pwChars); return implode('', $pwChars);
} }
protected function patchClientCustomAttribute(int $clientId, int $attributeId, string $value): bool { protected function patchClientCustomAttribute(int $clientId, int $attributeId, string $value): bool
{
if ($attributeId <= 0) { if ($attributeId <= 0) {
$this->logger->error("Intento de patchAttribute con ID inválido ($attributeId) para cliente $clientId"); $this->logger->error("Intento de patchAttribute con ID inválido ($attributeId) para cliente $clientId");
return false; return false;
@ -537,7 +611,8 @@ abstract class AbstractStripeOperationsFacade
} }
} }
private function resolveAttributeId(string $key): int { private function resolveAttributeId(string $key): int
{
if ($this->systemAttributesCache === null) { if ($this->systemAttributesCache === null) {
try { try {
$this->systemAttributesCache = $this->ucrmApi->get('custom-attributes', ['attributeType' => 'client']); $this->systemAttributesCache = $this->ucrmApi->get('custom-attributes', ['attributeType' => 'client']);
@ -556,13 +631,15 @@ abstract class AbstractStripeOperationsFacade
return 0; return 0;
} }
protected function comparePasswords(?string $crm, ?string $vault): string { protected function comparePasswords(?string $crm, ?string $vault): string
{
if ($crm && strpos($crm, 'Error') !== 0) return $crm; if ($crm && strpos($crm, 'Error') !== 0) return $crm;
if ($vault && strpos($vault, 'Error') !== 0) return $vault; if ($vault && strpos($vault, 'Error') !== 0) return $vault;
return '⚠️ Probar pass conocida.'; return '⚠️ Probar pass conocida.';
} }
public function syncStripeCustomerData(int $clientId, string $name, ?string $email): void { public function syncStripeCustomerData(int $clientId, string $name, ?string $email): void
{
$config = PluginConfigManager::create()->loadConfig(); $config = PluginConfigManager::create()->loadConfig();
$stripe = new StripeClient($config['tokenstripe']); $stripe = new StripeClient($config['tokenstripe']);
try { try {
@ -583,7 +660,8 @@ abstract class AbstractStripeOperationsFacade
} }
} }
protected function removeTagFromClient(int $clientId, string $tagName): void { protected function removeTagFromClient(int $clientId, string $tagName): void
{
try { try {
$client = $this->ucrmApi->get("clients/$clientId"); $client = $this->ucrmApi->get("clients/$clientId");
$targetTagId = null; $targetTagId = null;
@ -605,7 +683,8 @@ abstract class AbstractStripeOperationsFacade
} }
} }
protected function validarNumeroTelefono($n): string { protected function validarNumeroTelefono($n): string
{
if (!$n) return ''; if (!$n) return '';
$n = preg_replace('/\D/', '', (string)$n); $n = preg_replace('/\D/', '', (string)$n);
return (strlen($n) === 10) ? '52' . $n : $n; return (strlen($n) === 10) ? '52' . $n : $n;
@ -665,6 +744,51 @@ abstract class AbstractStripeOperationsFacade
} }
} }
// [NEW] 3.5. Patch Payment Method ID via Microservice
$targetMethodId = null;
if ($metadataTipoPago === 'OXXO') {
$targetMethodId = 'b01c0b35-b42c-48d9-9ad9-ea6591adfbbb'; // OXXO Pay
} elseif ($metadataTipoPago === 'Transferencia Bancaria') {
$targetMethodId = '4145b5f5-3bbc-45e3-8fc5-9cda970c62fb'; // Transferencia Bancaria
} else {
// [NEW] Default to "Credit/Debit Card" if no specific metadata type found
$targetMethodId = '93814765-66a1-4c7d-a777-05c18fd6aab3'; // Tarjeta de crédito/débito
}
if ($targetMethodId) {
// [NEW] Update Notification Object in Memory so the calling code knows the change
if (is_object($notificationObject) && isset($notificationObject->paymentData)) {
// Fix for "Indirect modification of overloaded property" error
// We must read the array, modify it, and write it back.
$pData = $notificationObject->paymentData;
if (is_array($pData)) {
$pData['methodId'] = $targetMethodId;
$notificationObject->paymentData = $pData;
}
}
try {
// Check current methodId (reuse 'payment' if available, otherwise fetch)
// Note: We fetched 'payment' in Step 3 ONLY if stripeUserId was valid.
// Safe to fetch again or reuse specific check.
$paymentCheck = $this->ucrmApi->get('payments/' . $paymentId);
if ($paymentCheck['methodId'] !== $targetMethodId) {
$this->logger->info("Payment $paymentId has wrong Method ID ({$paymentCheck['methodId']}). Patching to $targetMethodId via Microservice.");
$httpClient->patch("$microserviceBaseUrl/payments/$paymentId/method", [
'json' => ['methodId' => $targetMethodId],
'timeout' => 5
]);
$this->logger->info("Payment Method ID patched successfully.");
}
} catch (\Throwable $e) {
$this->logger->error("Failed to patch Payment Method ID via microservice: " . $e->getMessage());
}
}
// 4. Determine Target Attribute Value // 4. Determine Target Attribute Value
// Truth Source Priority: 1. Metadata (DB), 2. Existing Attribute, 3. Method Name (Guess) // Truth Source Priority: 1. Metadata (DB), 2. Existing Attribute, 3. Method Name (Guess)
@ -733,7 +857,6 @@ abstract class AbstractStripeOperationsFacade
} else { } else {
$this->logger->debug("No se pudo determinar el tipoPagoStripe."); $this->logger->debug("No se pudo determinar el tipoPagoStripe.");
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error("Error in ensureStripePaymentAttribute: " . $e->getMessage()); $this->logger->error("Error in ensureStripePaymentAttribute: " . $e->getMessage());
} }

View File

@ -1,4 +1,5 @@
<?php <?php
namespace SmsNotifier\Facade; namespace SmsNotifier\Facade;
use DateTime; use DateTime;
@ -64,9 +65,7 @@ class ClientCallBellAPI
$this->CallBellAPIToken = $CallBellAPIToken; $this->CallBellAPIToken = $CallBellAPIToken;
} }
public function updateContact($client_uuid) public function updateContact($client_uuid) {}
{
}
public function printPrueba($clientWhatsAppNumber, $notificationData) public function printPrueba($clientWhatsAppNumber, $notificationData)
{ {
@ -357,17 +356,43 @@ class ClientCallBellAPI
$config = PluginConfigManager::create()->loadConfig(); $config = PluginConfigManager::create()->loadConfig();
$gClient = new Client(['base_uri' => "https://{$this->IPServer}/crm/api/v1.0/", 'verify' => false]); $gClient = new Client(['base_uri' => "https://{$this->IPServer}/crm/api/v1.0/", 'verify' => false]);
$this->ucrmApi = new UcrmApi($gClient, $this->UCRMAPIToken ?? ''); $this->ucrmApi = new UcrmApi($gClient, $this->UCRMAPIToken ?? '');
$payments = $this->ucrmApi->get( $payment_id = $notificationData->paymentData['id'];
'payments/', $payment_amount = '$' . $notificationData->paymentData['amount'];
[
'clientId' => $notificationData->clientData['id'],
'limit' => 1,
'direction' => 'DESC',
] // We already have the payment data in $notificationData, no need to fetch 'payments/' again to get the ID.
); // However, if we need the 'note' field for the overlay which might not be in notificationData (depending on richness),
// we should try to use what we have or fetch SPECIFICALLY this payment.
// Let's verify if 'note' is in paymentData. Usually UCRM webhook payload has it.
// But to be safe and consistent with previous logic, if we need 'note', we can fetch THIS payment.
// $payments = $this->ucrmApi->get('payments/'.$payment_id); // This would be better if we need details.
// The previous code did:
// $payments = $this->ucrmApi->get('payments/', ['clientId' => ..., 'limit' => 1 ...]);
// $payment_id = $payments[0]['id']; <-- THIS WAS THE BUG. Always getting latest.
// Fix: Use the ID passed in notificationData.
// If we need the NOTE for the Overlay (OXXO/Transfer check later in code), we should ensure we have it.
// $notificationData->paymentData usually contains 'note'.
$note = $notificationData->paymentData['note'] ?? '';
// Let's keep $payments array structure if downstream code expects it, OR refactor downstream.
// Downstream uses $payments[0]['note'].
// Let's just mock $payments[0] with our data OR fetch the correct single payment.
// Fetching single payment is safer to ensure we have the Note.
try {
$fetchedPayment = $this->ucrmApi->get('payments/' . $payment_id);
$payments = [$fetchedPayment];
} catch (\Exception $e) {
// Fallback if fetch fails (unlikely if ID is valid)
$payments = [$notificationData->paymentData];
}
// $payment_id is already set above.
$payment_id = $payments[0]['id'];
$payment_amount = '$' . $payments[0]['amount']; $payment_amount = '$' . $payments[0]['amount'];
//$saldo = '$' . $notificationData->clientData['accountBalance']; //$saldo = '$' . $notificationData->clientData['accountBalance'];
$nombre_cliente = sprintf("%s %s", $notificationData->clientData['firstName'], $notificationData->clientData['lastName']); $nombre_cliente = sprintf("%s %s", $notificationData->clientData['firstName'], $notificationData->clientData['lastName']);
@ -520,7 +545,6 @@ class ClientCallBellAPI
//$log->appendLog("Archivo JPG temporal eliminado." . PHP_EOL); //$log->appendLog("Archivo JPG temporal eliminado." . PHP_EOL);
} }
$log->appendLog("Archivos temporales (PDF/JPG) eliminados tras subida exitosa." . PHP_EOL); $log->appendLog("Archivos temporales (PDF/JPG) eliminados tras subida exitosa." . PHP_EOL);
} else { } else {
$log->appendLog("Error: Falló la subida a MinIO." . PHP_EOL); $log->appendLog("Error: Falló la subida a MinIO." . PHP_EOL);
return false; return false;
@ -533,7 +557,6 @@ class ClientCallBellAPI
$log->appendLog("Error microservicio PDF: HTTP " . $responseMs->getStatusCode() . PHP_EOL); $log->appendLog("Error microservicio PDF: HTTP " . $responseMs->getStatusCode() . PHP_EOL);
return false; return false;
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$log->appendLog("Excepción en flujo Microservicio/MinIO: " . $e->getMessage() . PHP_EOL); $log->appendLog("Excepción en flujo Microservicio/MinIO: " . $e->getMessage() . PHP_EOL);
return false; return false;
@ -1173,7 +1196,6 @@ class ClientCallBellAPI
$response = curl_exec($ch); $response = curl_exec($ch);
$log->appendLog("Response Patch CallBell: " . $response . PHP_EOL); $log->appendLog("Response Patch CallBell: " . $response . PHP_EOL);
curl_close($ch); curl_close($ch);
} else { } else {
$log->appendLog("NO SE EJECUTA PATCH - No hay cambios que actualizar" . PHP_EOL); $log->appendLog("NO SE EJECUTA PATCH - No hay cambios que actualizar" . PHP_EOL);
curl_close($ch); curl_close($ch);

View File

@ -131,7 +131,6 @@ class Plugin
if ($jsonData['data']['object']['type'] === 'funded') { if ($jsonData['data']['object']['type'] === 'funded') {
$this->logger->info('Evento de transferencia de un cliente recibido: ' . json_encode($jsonData) . PHP_EOL); $this->logger->info('Evento de transferencia de un cliente recibido: ' . json_encode($jsonData) . PHP_EOL);
$this->pluginNotifierFacade->createPaymentIntent($jsonData); $this->pluginNotifierFacade->createPaymentIntent($jsonData);
} }
if ($jsonData['data']['object']['type'] === 'applied_to_payment') { if ($jsonData['data']['object']['type'] === 'applied_to_payment') {
@ -143,7 +142,6 @@ class Plugin
//Se canceló una transferencia de dinero, imprimir que se canceló y además el monto neto //Se canceló una transferencia de dinero, imprimir que se canceló y además el monto neto
$this->logger->warning('Evento de transferencia cancelada para el pago: ' . $paymentIntentId . PHP_EOL); $this->logger->warning('Evento de transferencia cancelada para el pago: ' . $paymentIntentId . PHP_EOL);
$this->logger->warning('Monto neto de la transferencia cancelada: ' . $jsonData['data']['object']['net_amount'] . PHP_EOL); $this->logger->warning('Monto neto de la transferencia cancelada: ' . $jsonData['data']['object']['net_amount'] . PHP_EOL);
} }
break; break;
case 'payout.failed': case 'payout.failed':
@ -261,7 +259,6 @@ class Plugin
// Terminar proceso hijo // Terminar proceso hijo
exit; exit;
break; break;
} }
} }
return; return;
@ -299,8 +296,8 @@ class Plugin
$result = json_encode($notification); $result = json_encode($notification);
$this->logger->debug('Notification encodificado en JSON:' . $result . PHP_EOL); $this->logger->debug('Notification encodificado en JSON:' . $result . PHP_EOL);
// [NEW] Attempt to patch the payment with correct Stripe attribute if applicable // [MOVED] Attempt to patch method ID only if it comes as "Stripe Credit Card" (catch-all)
$this->pluginNotifierFacade->ensureStripePaymentAttribute($notification); //$this->pluginNotifierFacade->ensureStripePaymentAttribute($notification); (Removed from top)
$payment_method_id = $notification->paymentData['methodId']; $payment_method_id = $notification->paymentData['methodId'];
$payment_method = ''; $payment_method = '';
@ -337,10 +334,31 @@ class Plugin
} }
break; break;
case '1dd098fa-5d63-4c8d-88b7-3c27ffbbb6ae': case '1dd098fa-5d63-4c8d-88b7-3c27ffbbb6ae':
// [NEW] Logic to patch method ID based on metadata
$this->pluginNotifierFacade->ensureStripePaymentAttribute($notification);
// Check if Method ID was updated in memory to OXXO or Transfer
$patchedMethodId = $notification->paymentData['methodId'];
if ($patchedMethodId === 'b01c0b35-b42c-48d9-9ad9-ea6591adfbbb') {
// It is OXXO Pay
$payment_method = 'OXXO Pay';
if ($config['oxxoPayPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification);
}
} elseif ($patchedMethodId === '4145b5f5-3bbc-45e3-8fc5-9cda970c62fb') {
// It is Bank Transfer
$payment_method = 'Transferencia bancaria';
if ($config['bankTransferPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification);
}
} else {
// Default: Credit Card Stripe
$payment_method = 'Tarjeta de crédito Stripe'; $payment_method = 'Tarjeta de crédito Stripe';
if ($config['creditCardStripePaymentMethodId'] ?? false) { if ($config['creditCardStripePaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification); $this->notifierFacade->verifyPaymentActionToDo($notification);
} }
}
break; break;
case 'b9e1e9d1-5c7b-41d2-b6b2-3e568d700290': case 'b9e1e9d1-5c7b-41d2-b6b2-3e568d700290':
$payment_method = 'Suscripción de Stripe (tarjeta de crédito)'; $payment_method = 'Suscripción de Stripe (tarjeta de crédito)';
@ -372,11 +390,28 @@ class Plugin
$this->notifierFacade->verifyPaymentActionToDo($notification); $this->notifierFacade->verifyPaymentActionToDo($notification);
} }
break; break;
case 'b01c0b35-b42c-48d9-9ad9-ea6591adfbbb':
$payment_method = 'OXXO Pay';
if ($config['oxxoPayPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification);
}
break;
case '4145b5f5-3bbc-45e3-8fc5-9cda970c62fb':
$payment_method = 'Transferencia bancaria';
if ($config['bankTransferPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification);
}
break;
case '93814765-66a1-4c7d-a777-05c18fd6aab3':
$payment_method = 'Tarjeta de crédito/débito';
if ($config['creditDebitCardPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification);
}
break;
default: default:
$payment_method = 'Desconocido'; $payment_method = 'Desconocido';
break; break;
} }
} else if ($notification->eventName === 'client.edit') { } else if ($notification->eventName === 'client.edit') {
$this->logger->info('Procesando evento client.edit para entityId: ' . ($jsonData['entityId'] ?? 'unknown')); $this->logger->info('Procesando evento client.edit para entityId: ' . ($jsonData['entityId'] ?? 'unknown'));
$this->logger->debug('Payload completo client.edit: ' . json_encode($jsonData)); $this->logger->debug('Payload completo client.edit: ' . json_encode($jsonData));
@ -388,7 +423,7 @@ class Plugin
} else { } else {
$this->logger->info('Llamando a updatePasswordAntenaIfNeeded para cliente: ' . $clientID); $this->logger->info('Llamando a updatePasswordAntenaIfNeeded para cliente: ' . $clientID);
$this->pluginNotifierFacade->updatePasswordAntenaIfNeeded((int)$clientID, $jsonData); $this->pluginNotifierFacade->updatePasswordAntenaIfNeeded((int)$clientID, $jsonData);
$this->logger->info('Llamada finalizada exitosamente.'); // $this->lcdf4d7cf10e2ogger->info('Llamada finalizada exitosamente.');
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error('ERROR FATAL procesando client.edit: ' . $e->getMessage()); $this->logger->error('ERROR FATAL procesando client.edit: ' . $e->getMessage());
@ -473,18 +508,15 @@ class Plugin
} }
$this->notifierFacade->verifyClientActionToDo($notification); $this->notifierFacade->verifyClientActionToDo($notification);
} else if ($notification->eventName === 'client.add') { } else if ($notification->eventName === 'client.add') {
$this->logger->debug('Se agregó un nuevo cliente'); $this->logger->debug('Se agregó un nuevo cliente');
$this->logger->debug('Valor de json_data: ' . json_encode($jsonData)); $this->logger->debug('Valor de json_data: ' . json_encode($jsonData));
} else if ($notification->eventName === 'service.edit') { } else if ($notification->eventName === 'service.edit') {
$this->logger->debug('Se editó el servicio a un cliente' . PHP_EOL); $this->logger->debug('Se editó el servicio a un cliente' . PHP_EOL);
$this->notifierFacade->verifyServiceActionToDo($notification); $this->notifierFacade->verifyServiceActionToDo($notification);
//ejemplo de json_data: {"uuid":"06d281ca-d78e-4f0a-a282-3a6b77d25da0","changeType":"edit","entity":"service","entityId":"155","eventName":"service.edit","extraData":{"entity":{"id":155,"prepaid":false,"clientId":171,"status":1,"name":"Basico 300","fullAddress":"Campeche 56, Dolores Hidalgo, 37800","street1":"Campeche 56","street2":null,"city":"Dolores Hidalgo","countryId":173,"stateId":null,"zipCode":"37800","note":null,"addressGpsLat":21.1572461,"addressGpsLon":-100.9377137,"servicePlanId":6,"servicePlanPeriodId":26,"price":300,"hasIndividualPrice":false,"totalPrice":300,"currencyCode":"MXN","invoiceLabel":null,"contractId":null,"contractLengthType":1,"minimumContractLengthMonths":null,"activeFrom":"2025-05-21T00:00:00-0600","activeTo":null,"contractEndDate":null,"discountType":0,"discountValue":null,"discountInvoiceLabel":"Descuento","discountFrom":null,"discountTo":null,"tax1Id":null,"tax2Id":null,"tax3Id":null,"invoicingStart":"2025-05-21T00:00:00-0600","invoicingPeriodType":1,"invoicingPeriodStartDay":1,"nextInvoicingDayAdjustment":10,"invoicingProratedSeparately":true,"invoicingSeparately":false,"sendEmailsAutomatically":null,"useCreditAutomatically":true,"servicePlanName":"Basico 300","servicePlanPrice":300,"servicePlanPeriod":1,"servicePlanType":"Internet","downloadSpeed":8,"uploadSpeed":8,"hasOutage":false,"unmsClientSiteStatus":null,"fccBlockId":null,"lastInvoicedDate":null,"unmsClientSiteId":"359cb58d-e64f-453a-890e-23d5abb4f116","attributes":[],"addressData":null,"suspensionReasonId":null,"serviceChangeRequestId":null,"setupFeePrice":null,"earlyTerminationFeePrice":null,"downloadSpeedOverride":null,"uploadSpeedOverride":null,"trafficShapingOverrideEnd":null,"trafficShapingOverrideEnabled":false,"servicePlanGroupId":null,"suspensionPeriods":[],"surcharges":[]},"entityBeforeEdit":{"id":155,"prepaid":false,"clientId":171,"status":1,"name":"Basico 300","fullAddress":"Campeche 56, Dolores Hidalgo, 37800","street1":"Campeche 56","street2":null,"city":"Dolores Hidalgo","countryId":173,"stateId":null,"zipCode":"37800","note":null,"addressGpsLat":21.1572461,"addressGpsLon":-100.9377137,"servicePlanId":6,"servicePlanPeriodId":26,"price":300,"hasIndividualPrice":false,"totalPrice":300,"currencyCode":"MXN","invoiceLabel":null,"contractId":null,"contractLengthType":1,"minimumContractLengthMonths":null,"activeFrom":"2025-05-21T00:00:00-0600","activeTo":null,"contractEndDate":null,"discountType":0,"discountValue":null,"discountInvoiceLabel":"Descuento","discountFrom":null,"discountTo":null,"tax1Id":null,"tax2Id":null,"tax3Id":null,"invoicingStart":"2025-05-21T00:00:00-0600","invoicingPeriodType":1,"invoicingPeriodStartDay":1,"nextInvoicingDayAdjustment":10,"invoicingProratedSeparately":true,"invoicingSeparately":false,"sendEmailsAutomatically":null,"useCreditAutomatically":true,"servicePlanName":"Basico 300","servicePlanPrice":300,"servicePlanPeriod":1,"servicePlanType":"Internet","downloadSpeed":8,"uploadSpeed":8,"hasOutage":false,"unmsClientSiteStatus":null,"fccBlockId":null,"lastInvoicedDate":null,"unmsClientSiteId":"359cb58d-e64f-453a-890e-23d5abb4f116","attributes":[],"addressData":null,"suspensionReasonId":null,"serviceChangeRequestId":null,"setupFeePrice":null,"earlyTerminationFeePrice":null,"downloadSpeedOverride":null,"uploadSpeedOverride":null,"trafficShapingOverrideEnd":null,"trafficShapingOverrideEnabled":false,"servicePlanGroupId":null,"suspensionPeriods":[],"surcharges":[]}}} //ejemplo de json_data: {"uuid":"06d281ca-d78e-4f0a-a282-3a6b77d25da0","changeType":"edit","entity":"service","entityId":"155","eventName":"service.edit","extraData":{"entity":{"id":155,"prepaid":false,"clientId":171,"status":1,"name":"Basico 300","fullAddress":"Campeche 56, Dolores Hidalgo, 37800","street1":"Campeche 56","street2":null,"city":"Dolores Hidalgo","countryId":173,"stateId":null,"zipCode":"37800","note":null,"addressGpsLat":21.1572461,"addressGpsLon":-100.9377137,"servicePlanId":6,"servicePlanPeriodId":26,"price":300,"hasIndividualPrice":false,"totalPrice":300,"currencyCode":"MXN","invoiceLabel":null,"contractId":null,"contractLengthType":1,"minimumContractLengthMonths":null,"activeFrom":"2025-05-21T00:00:00-0600","activeTo":null,"contractEndDate":null,"discountType":0,"discountValue":null,"discountInvoiceLabel":"Descuento","discountFrom":null,"discountTo":null,"tax1Id":null,"tax2Id":null,"tax3Id":null,"invoicingStart":"2025-05-21T00:00:00-0600","invoicingPeriodType":1,"invoicingPeriodStartDay":1,"nextInvoicingDayAdjustment":10,"invoicingProratedSeparately":true,"invoicingSeparately":false,"sendEmailsAutomatically":null,"useCreditAutomatically":true,"servicePlanName":"Basico 300","servicePlanPrice":300,"servicePlanPeriod":1,"servicePlanType":"Internet","downloadSpeed":8,"uploadSpeed":8,"hasOutage":false,"unmsClientSiteStatus":null,"fccBlockId":null,"lastInvoicedDate":null,"unmsClientSiteId":"359cb58d-e64f-453a-890e-23d5abb4f116","attributes":[],"addressData":null,"suspensionReasonId":null,"serviceChangeRequestId":null,"setupFeePrice":null,"earlyTerminationFeePrice":null,"downloadSpeedOverride":null,"uploadSpeedOverride":null,"trafficShapingOverrideEnd":null,"trafficShapingOverrideEnabled":false,"servicePlanGroupId":null,"suspensionPeriods":[],"surcharges":[]},"entityBeforeEdit":{"id":155,"prepaid":false,"clientId":171,"status":1,"name":"Basico 300","fullAddress":"Campeche 56, Dolores Hidalgo, 37800","street1":"Campeche 56","street2":null,"city":"Dolores Hidalgo","countryId":173,"stateId":null,"zipCode":"37800","note":null,"addressGpsLat":21.1572461,"addressGpsLon":-100.9377137,"servicePlanId":6,"servicePlanPeriodId":26,"price":300,"hasIndividualPrice":false,"totalPrice":300,"currencyCode":"MXN","invoiceLabel":null,"contractId":null,"contractLengthType":1,"minimumContractLengthMonths":null,"activeFrom":"2025-05-21T00:00:00-0600","activeTo":null,"contractEndDate":null,"discountType":0,"discountValue":null,"discountInvoiceLabel":"Descuento","discountFrom":null,"discountTo":null,"tax1Id":null,"tax2Id":null,"tax3Id":null,"invoicingStart":"2025-05-21T00:00:00-0600","invoicingPeriodType":1,"invoicingPeriodStartDay":1,"nextInvoicingDayAdjustment":10,"invoicingProratedSeparately":true,"invoicingSeparately":false,"sendEmailsAutomatically":null,"useCreditAutomatically":true,"servicePlanName":"Basico 300","servicePlanPrice":300,"servicePlanPeriod":1,"servicePlanType":"Internet","downloadSpeed":8,"uploadSpeed":8,"hasOutage":false,"unmsClientSiteStatus":null,"fccBlockId":null,"lastInvoicedDate":null,"unmsClientSiteId":"359cb58d-e64f-453a-890e-23d5abb4f116","attributes":[],"addressData":null,"suspensionReasonId":null,"serviceChangeRequestId":null,"setupFeePrice":null,"earlyTerminationFeePrice":null,"downloadSpeedOverride":null,"uploadSpeedOverride":null,"trafficShapingOverrideEnd":null,"trafficShapingOverrideEnabled":false,"servicePlanGroupId":null,"suspensionPeriods":[],"surcharges":[]}}}
$clientID = $jsonData['extraData']['entity']['clientId']; $clientID = $jsonData['extraData']['entity']['clientId'];
$this->pluginNotifierFacade->updatePasswordAntenaIfNeeded($clientID, $jsonData); $this->pluginNotifierFacade->updatePasswordAntenaIfNeeded($clientID, $jsonData);
} else if ($notification->eventName === 'service.suspend') { } else if ($notification->eventName === 'service.suspend') {
$this->logger->debug('Se suspendió el servicio a un cliente' . PHP_EOL); $this->logger->debug('Se suspendió el servicio a un cliente' . PHP_EOL);
$this->notifierFacade->verifyServiceActionToDo($notification); $this->notifierFacade->verifyServiceActionToDo($notification);
@ -532,7 +564,6 @@ class Plugin
'title' => $title, 'title' => $title,
]); ]);
$this->logger->debug('Respuesta de la API al agregar el trabajo: ' . json_encode($responsePatch)); $this->logger->debug('Respuesta de la API al agregar el trabajo: ' . json_encode($responsePatch));
} else if ($notification->eventName === 'job.edit') { } else if ($notification->eventName === 'job.edit') {
$this->logger->debug('Se actualiza un trabajo' . PHP_EOL); $this->logger->debug('Se actualiza un trabajo' . PHP_EOL);
// $this->logger->debug('Valor de json_data: ' . json_encode($jsonData)); // $this->logger->debug('Valor de json_data: ' . json_encode($jsonData));
@ -589,14 +620,12 @@ class Plugin
$this->logger->debug('Edición de trabajo "En curso" sin cambios de fecha o técnico relevantes para notificación'); $this->logger->debug('Edición de trabajo "En curso" sin cambios de fecha o técnico relevantes para notificación');
} }
} }
} else { } else {
$this->logger->warning('El campo assignedUserId no existe en entityBeforeEdit o entity'); $this->logger->warning('El campo assignedUserId no existe en entityBeforeEdit o entity');
} }
} else { } else {
$this->logger->warning('Los datos entityBeforeEdit o entity no están presentes en extraData'); $this->logger->warning('Los datos entityBeforeEdit o entity no están presentes en extraData');
} }
} }
//$this->notifierFacade->update($notification); //$this->notifierFacade->update($notification);

View File

@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use GuzzleHttp\Client;
$config = PluginConfigManager::create()->loadConfig();
$ip = $config['ipPuppeteer'] ?? '127.0.0.1'; // Fallback
$port = $config['portPuppeteer'] ?? '4100'; // Fallback, docker-compose says 4100 host -> 4000 container
$paymentId = 907;
$oxxoMethodId = 'b01c0b35-b42c-48d9-9ad9-ea6591adfbbb';
echo "Testing Microservice Patch on Payment $paymentId to OXXO Pay ($oxxoMethodId)...\n";
$client = new Client();
try {
$url = "http://$ip:$port/payments/$paymentId/method";
echo "URL: $url\n";
$response = $client->patch($url, [
'json' => ['methodId' => $oxxoMethodId]
]);
echo "Response Code: " . $response->getStatusCode() . "\n";
echo "Body: " . $response->getBody()->getContents() . "\n";
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
if (method_exists($e, 'getResponse') && $e->getResponse()) {
echo "Response Error: " . $e->getResponse()->getBody()->getContents() . "\n";
}
}