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,27 +21,29 @@ 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;
$config = PluginConfigManager::create()->loadConfig(); $config = PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? 'localhost'; $ipServer = $config['ipserver'] ?? 'localhost';
$apiUrl = "https://$ipServer/crm/api/v1.0/"; $apiUrl = "https://$ipServer/crm/api/v1.0/";
$client = new Client([ $client = new Client([
'base_uri' => $apiUrl, 'base_uri' => $apiUrl,
'verify' => false, 'verify' => false,
]); ]);
$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']);
$customer = $eventJson['data']['object']['customer'] ?? null; $customer = $eventJson['data']['object']['customer'] ?? null;
$amount = $eventJson['data']['object']['net_amount'] ?? 0; $amount = $eventJson['data']['object']['net_amount'] ?? 0;
@ -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,18 +108,19 @@ 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'];
$piId = $data['applied_to_payment']['payment_intent'] ?? null; $piId = $data['applied_to_payment']['payment_intent'] ?? null;
if (!$piId) return; if (!$piId) return;
@ -110,18 +146,19 @@ 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;
if (($data['status'] ?? '') !== 'succeeded') { if (($data['status'] ?? '') !== 'succeeded') {
$this->logger->warning("PaymentIntent $piId no está succeeded (status: " . ($data['status']??'unknown') . ")"); $this->logger->warning("PaymentIntent $piId no está succeeded (status: " . ($data['status'] ?? 'unknown') . ")");
return; return;
} }
// Buscar clientId en metadata (prioridad: clientId > ucrm_client_id) // Buscar clientId en metadata (prioridad: clientId > ucrm_client_id)
$clientId = $data['metadata']['clientId'] ?? $data['metadata']['ucrm_client_id'] ?? null; $clientId = $data['metadata']['clientId'] ?? $data['metadata']['ucrm_client_id'] ?? null;
// Fallback: Buscar por Stripe Customer ID si no hay metadata // Fallback: Buscar por Stripe Customer ID si no hay metadata
if (!$clientId && !empty($data['customer'])) { if (!$clientId && !empty($data['customer'])) {
$clientId = $this->findClientIdByStripeCustomer($data['customer']); $clientId = $this->findClientIdByStripeCustomer($data['customer']);
@ -135,15 +172,40 @@ 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';
$methodSearchName = ($type === 'oxxo') ? 'OXXO' : 'Stripe'; $methodSearchName = ($type === 'oxxo') ? 'OXXO' : 'Stripe';
// Buscar ID de metodo en UCRM // Buscar ID de metodo en UCRM
$methodId = $this->findPaymentMethodId($methodSearchName); $methodId = $this->findPaymentMethodId($methodSearchName);
if (!$methodId && $methodSearchName === 'OXXO') { if (!$methodId && $methodSearchName === 'OXXO') {
$methodId = $this->findPaymentMethodId('Stripe'); // Fallback $methodId = $this->findPaymentMethodId('Stripe'); // Fallback
} }
if ($methodId) { if ($methodId) {
@ -156,7 +218,7 @@ abstract class AbstractStripeOperationsFacade
'note' => "Stripe ($methodSearchName) - PI: $piId", 'note' => "Stripe ($methodSearchName) - PI: $piId",
'createdDate' => date('c'), 'createdDate' => date('c'),
]); ]);
$this->logger->info("Pago registrado en UCRM vía PI Succeeded: $piId, Cliente: $clientId, Monto: $checkAmount"); $this->logger->info("Pago registrado en UCRM vía PI Succeeded: $piId, Cliente: $clientId, Monto: $checkAmount");
} else { } else {
$this->logger->warning("No se encontró método de pago para '$methodSearchName' en UCRM."); $this->logger->warning("No se encontró método de pago para '$methodSearchName' en UCRM.");
@ -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.
@ -177,7 +240,7 @@ abstract class AbstractStripeOperationsFacade
// Dado que no queremos matar el server, limitaremos la búsqueda o asumiremos que el FIX de metadata es el principal. // Dado que no queremos matar el server, limitaremos la búsqueda o asumiremos que el FIX de metadata es el principal.
// Sin embargo, para este caso específico, vamos a intentar buscar en los clientes recientes o usar una búsqueda básica. // Sin embargo, para este caso específico, vamos a intentar buscar en los clientes recientes o usar una búsqueda básica.
// UCRM API v1.0 GET /clients soporta ?customAttributeId&customAttributeValue // UCRM API v1.0 GET /clients soporta ?customAttributeId&customAttributeValue
$attrId = $this->resolveAttributeId('stripeCustomerId'); $attrId = $this->resolveAttributeId('stripeCustomerId');
if (!$attrId) return null; if (!$attrId) return null;
@ -195,19 +258,22 @@ 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) {
if (stripos($m['name'], $name) !== false) { if (stripos($m['name'], $name) !== false) {
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;
@ -216,7 +282,7 @@ abstract class AbstractStripeOperationsFacade
try { try {
$clientCRM = $this->ucrmApi->get("clients/$clientId", []); $clientCRM = $this->ucrmApi->get("clients/$clientId", []);
// Si intenta crear CLABE pero NO tiene stripeCustomerId, cancelamos // Si intenta crear CLABE pero NO tiene stripeCustomerId, cancelamos
if ($tagName === 'CREAR CLABE STRIPE') { if ($tagName === 'CREAR CLABE STRIPE') {
$hasStripeId = false; $hasStripeId = false;
@ -234,7 +300,7 @@ abstract class AbstractStripeOperationsFacade
} }
$customer = $this->createCustomerStripe($stripe, $clientCRM, $generateSpei); $customer = $this->createCustomerStripe($stripe, $clientCRM, $generateSpei);
if ($customer) { if ($customer) {
$this->logger->info("Cliente Stripe procesado para ID: $clientId (Tag: $tagName, SPEI: " . ($generateSpei ? 'SI' : 'NO') . ")"); $this->logger->info("Cliente Stripe procesado para ID: $clientId (Tag: $tagName, SPEI: " . ($generateSpei ? 'SI' : 'NO') . ")");
} }
@ -246,9 +312,10 @@ 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
$email = $clientCRM['username'] ?? null; $email = $clientCRM['username'] ?? null;
foreach ($clientCRM['contacts'] ?? [] as $contact) { foreach ($clientCRM['contacts'] ?? [] as $contact) {
@ -266,7 +333,7 @@ abstract class AbstractStripeOperationsFacade
// Obtener IDs de atributos dinámicamente desde el sistema // Obtener IDs de atributos dinámicamente desde el sistema
$cidAttrId = $this->resolveAttributeId('stripeCustomerId'); $cidAttrId = $this->resolveAttributeId('stripeCustomerId');
$clabeAttrId = $this->resolveAttributeId('clabeInterbancaria'); $clabeAttrId = $this->resolveAttributeId('clabeInterbancaria');
$this->logger->debug("IDs de atributos resueltos: Stripe=$cidAttrId, CLABE=$clabeAttrId"); $this->logger->debug("IDs de atributos resueltos: Stripe=$cidAttrId, CLABE=$clabeAttrId");
// Buscar cliente existente por metadata // Buscar cliente existente por metadata
@ -288,7 +355,7 @@ abstract class AbstractStripeOperationsFacade
'name' => $name, 'name' => $name,
'metadata' => ['ucrm_client_id' => $clientId] 'metadata' => ['ucrm_client_id' => $clientId]
]; ];
$customer = $stripe->customers->create($params); $customer = $stripe->customers->create($params);
$this->logger->info("Nuevo Cliente Stripe creado para ID: $clientId. CID: {$customer->id}"); $this->logger->info("Nuevo Cliente Stripe creado para ID: $clientId. CID: {$customer->id}");
// Guardar CID en UCRM // Guardar CID en UCRM
@ -309,7 +376,7 @@ abstract class AbstractStripeOperationsFacade
); );
$clabe = $fundingInstructions['bank_transfer']['financial_addresses'][0]['spei']['clabe'] ?? null; $clabe = $fundingInstructions['bank_transfer']['financial_addresses'][0]['spei']['clabe'] ?? null;
if ($clabe) { if ($clabe) {
$this->logger->info("CLABE obtenida via Funding Instructions para cliente $clientId: $clabe"); $this->logger->info("CLABE obtenida via Funding Instructions para cliente $clientId: $clabe");
$this->patchClientCustomAttribute($clientId, (int)$clabeAttrId, $clabe); $this->patchClientCustomAttribute($clientId, (int)$clabeAttrId, $clabe);
@ -324,10 +391,11 @@ 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'] ?? '';
try { try {
// OPT: Lazy Check - Si ya tiene pass válido en CRM, no hace falta procesar nada // OPT: Lazy Check - Si ya tiene pass válido en CRM, no hace falta procesar nada
$clientData = $this->ucrmApi->get("clients/$clientId"); $clientData = $this->ucrmApi->get("clients/$clientId");
@ -348,7 +416,7 @@ abstract class AbstractStripeOperationsFacade
// 1. Obtener los servicios del cliente // 1. Obtener los servicios del cliente
$svcs = $this->ucrmApi->get('clients/services', ['clientId' => $clientId]); $svcs = $this->ucrmApi->get('clients/services', ['clientId' => $clientId]);
if (empty($svcs)) { if (empty($svcs)) {
$msg = '⚠️ Cliente sin servicios/antenas'; $msg = '⚠️ Cliente sin servicios/antenas';
$this->syncPasswordWithCrm((int)$clientId, $msg); $this->syncPasswordWithCrm((int)$clientId, $msg);
@ -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";
} }
@ -443,28 +515,28 @@ abstract class AbstractStripeOperationsFacade
} }
$finalValue = implode(' ', $allServicePasswords); $finalValue = implode(' ', $allServicePasswords);
// Evitar sincronización redundante si el valor es idéntico al actual (anti-bucle) // Evitar sincronización redundante si el valor es idéntico al actual (anti-bucle)
if ($finalValue === $passCRM) { if ($finalValue === $passCRM) {
return $finalValue; return $finalValue;
} }
$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");
$passCRM = ''; $passCRM = '';
$attributeId = 17; // ID real para 'passwordAntenaCliente' $attributeId = 17; // ID real para 'passwordAntenaCliente'
if (isset($clientData['attributes'])) { if (isset($clientData['attributes'])) {
foreach ($clientData['attributes'] as $attr) { foreach ($clientData['attributes'] as $attr) {
if ($attr['key'] === 'passwordAntenaCliente') { if ($attr['key'] === 'passwordAntenaCliente') {
@ -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'
@ -492,7 +565,7 @@ abstract class AbstractStripeOperationsFacade
$all = $lower . $upper . $digits . $symbols; $all = $lower . $upper . $digits . $symbols;
$pwChars = []; $pwChars = [];
// Asegurar que tenga al menos uno de cada tipo si es posible // Asegurar que tenga al menos uno de cada tipo si es posible
$pwChars[] = $lower[random_int(0, strlen($lower) - 1)]; $pwChars[] = $lower[random_int(0, strlen($lower) - 1)];
$pwChars[] = $upper[random_int(0, strlen($upper) - 1)]; $pwChars[] = $upper[random_int(0, strlen($upper) - 1)];
@ -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;
@ -593,7 +671,7 @@ abstract class AbstractStripeOperationsFacade
break; break;
} }
} }
if ($targetTagId) { if ($targetTagId) {
$this->ucrmApi->patch("clients/$clientId/remove-tag/$targetTagId", []); $this->ucrmApi->patch("clients/$clientId/remove-tag/$targetTagId", []);
$this->logger->info("Etiqueta '$tagName' (ID: $targetTagId) removida del cliente $clientId via endpoint especializado."); $this->logger->info("Etiqueta '$tagName' (ID: $targetTagId) removida del cliente $clientId via endpoint especializado.");
@ -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;
@ -615,7 +694,7 @@ abstract class AbstractStripeOperationsFacade
{ {
// 1. Get Payment ID // 1. Get Payment ID
$paymentId = is_object($notificationObject) ? ($notificationObject->entityId ?? null) : ($notificationObject['entityId'] ?? null); $paymentId = is_object($notificationObject) ? ($notificationObject->entityId ?? null) : ($notificationObject['entityId'] ?? null);
if (!$paymentId) { if (!$paymentId) {
$this->logger->warning("ensureStripePaymentAttribute: No entityId found in notification."); $this->logger->warning("ensureStripePaymentAttribute: No entityId found in notification.");
return; return;
@ -649,29 +728,74 @@ abstract class AbstractStripeOperationsFacade
// 3. Update User ID if missing (Direct DB Patch via Microservice) // 3. Update User ID if missing (Direct DB Patch via Microservice)
// UCRM API doesn't support PATCH userId, so we use microservice // UCRM API doesn't support PATCH userId, so we use microservice
if ($stripeUserId) { if ($stripeUserId) {
try { try {
$payment = $this->ucrmApi->get('payments/' . $paymentId); $payment = $this->ucrmApi->get('payments/' . $paymentId);
if (empty($payment['userId'])) { if (empty($payment['userId'])) {
$this->logger->info("Payment $paymentId has no User ID. Assigning Stripe User ID: $stripeUserId"); $this->logger->info("Payment $paymentId has no User ID. Assigning Stripe User ID: $stripeUserId");
$httpClient->patch("$microserviceBaseUrl/payments/$paymentId/user", [ $httpClient->patch("$microserviceBaseUrl/payments/$paymentId/user", [
'json' => ['userId' => $stripeUserId], 'json' => ['userId' => $stripeUserId],
'timeout' => 5 'timeout' => 5
]); ]);
} else { } else {
$this->logger->debug("Payment $paymentId already has User ID: " . $payment['userId']); $this->logger->debug("Payment $paymentId already has User ID: " . $payment['userId']);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error("Failed to patch User ID via microservice: " . $e->getMessage()); $this->logger->error("Failed to patch User ID via microservice: " . $e->getMessage());
} }
}
// [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)
// A. Check Existing Attribute (Don't overwrite valid values unless Metadata says otherwise?) // A. Check Existing Attribute (Don't overwrite valid values unless Metadata says otherwise?)
// Actually, Metadata keys are stronger than manual edits if the flow is automatic. // Actually, Metadata keys are stronger than manual edits if the flow is automatic.
// But let's respect existing valid attributes if metadata is missing. // But let's respect existing valid attributes if metadata is missing.
$payment = $this->ucrmApi->get('payments/' . $paymentId); // Re-fetch in case changed? Or Use previous result. $payment = $this->ucrmApi->get('payments/' . $paymentId); // Re-fetch in case changed? Or Use previous result.
$currentValue = null; $currentValue = null;
$hasAttribute = false; $hasAttribute = false;
@ -694,23 +818,23 @@ abstract class AbstractStripeOperationsFacade
} }
} else { } else {
// Fallback to Method Name Guessing if Metadata missing // Fallback to Method Name Guessing if Metadata missing
if ($hasAttribute && in_array($currentValue, ['OXXO Pay', 'Transferencia Bancaria', 'Tarjeta de Crédito'])) { if ($hasAttribute && in_array($currentValue, ['OXXO Pay', 'Transferencia Bancaria', 'Tarjeta de Crédito'])) {
$this->logger->debug("Payment $paymentId ya tiene atributo '$currentValue' y no hay metadata. Respetando."); $this->logger->debug("Payment $paymentId ya tiene atributo '$currentValue' y no hay metadata. Respetando.");
return; return;
} }
$methodId = $payment['methodId']; $methodId = $payment['methodId'];
$method = $this->ucrmApi->get('payment-methods/' . $methodId); $method = $this->ucrmApi->get('payment-methods/' . $methodId);
$methodName = $method['name'] ?? ''; $methodName = $method['name'] ?? '';
if (stripos($methodName, 'OXXO') !== false) { if (stripos($methodName, 'OXXO') !== false) {
$targetValue = 'OXXO Pay'; $targetValue = 'OXXO Pay';
} elseif (stripos($methodName, 'Transferencia') !== false) { } elseif (stripos($methodName, 'Transferencia') !== false) {
$targetValue = 'Transferencia Bancaria'; $targetValue = 'Transferencia Bancaria';
} elseif (stripos($methodName, 'Tarjeta') !== false && stripos($methodName, 'Stripe') !== false) { } elseif (stripos($methodName, 'Tarjeta') !== false && stripos($methodName, 'Stripe') !== false) {
$targetValue = 'Tarjeta de Crédito'; $targetValue = 'Tarjeta de Crédito';
} }
$this->logger->debug("Fallback Method Guessing '$methodName' -> '$targetValue'"); $this->logger->debug("Fallback Method Guessing '$methodName' -> '$targetValue'");
} }
// 5. Apply Update // 5. Apply Update
@ -731,9 +855,8 @@ 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)
{ {
@ -156,7 +155,7 @@ class ClientCallBellAPI
if ($reprogramming && !$changeInstaller) { if ($reprogramming && !$changeInstaller) {
//{{1}}, se ha reprogramado su visita técnica con el folio {{2}} //{{1}}, se ha reprogramado su visita técnica con el folio {{2}}
$campo3 = '🗓️➡️ ' . sprintf('%s', $jobNotificationData['date']); $campo3 = '🗓️➡️ ' . sprintf('%s', $jobNotificationData['date']);
$campo1_combinado = "Estimado cliente $campo1 se ha reprogramado su visita técnica con folio $campo2"; $campo1_combinado = "Estimado cliente $campo1 se ha reprogramado su visita técnica con folio $campo2";
@ -245,7 +244,7 @@ class ClientCallBellAPI
'Content-Type: application/json', 'Content-Type: application/json',
]); ]);
if (!$reprogramming && $changeInstaller) { if (!$reprogramming && $changeInstaller) {
$campo1 = $jobInstallerNotificationData['installerName']; $campo1 = $jobInstallerNotificationData['installerName'];
$campo2 = $jobInstallerNotificationData['subjectOfChange']; $campo2 = $jobInstallerNotificationData['subjectOfChange'];
@ -273,7 +272,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 = $jobInstallerNotificationData['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);
@ -312,7 +311,7 @@ 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);
@ -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']);
@ -440,23 +465,23 @@ class ClientCallBellAPI
// 1. Instanciar Logger y MinioService // 1. Instanciar Logger y MinioService
$loggerService = new Logger(); // Servicio de Logging propio del namespace $loggerService = new Logger(); // Servicio de Logging propio del namespace
$minioService = new MinioStorageService($loggerService); $minioService = new MinioStorageService($loggerService);
// 2. Configurar Microservicio // 2. Configurar Microservicio
$ipMicroservice = $config['ipPuppeteer'] ?? 'localhost'; // Reutilizamos IP de Puppeteer $ipMicroservice = $config['ipPuppeteer'] ?? 'localhost'; // Reutilizamos IP de Puppeteer
$portMicroservice = '8050'; // Puerto definido en docker-compose $portMicroservice = '8050'; // Puerto definido en docker-compose
$microserviceUrl = "http://{$ipMicroservice}:{$portMicroservice}/process"; $microserviceUrl = "http://{$ipMicroservice}:{$portMicroservice}/process";
$log->appendLog("Procesando PDF con microservicio: $microserviceUrl" . PHP_EOL); $log->appendLog("Procesando PDF con microservicio: $microserviceUrl" . PHP_EOL);
// 2. DETECTAR METODO DE PAGO (Overlay) // 2. DETECTAR METODO DE PAGO (Overlay)
$overlayText = ''; $overlayText = '';
$note = $payments[0]['note'] ?? ''; $note = $payments[0]['note'] ?? '';
// Si la nota dice OXXO o Transferencia, preparamos el reemplazo // Si la nota dice OXXO o Transferencia, preparamos el reemplazo
if (stripos($note, 'OXXO') !== false) { if (stripos($note, 'OXXO') !== false) {
$overlayText = 'OXXO Pay'; $overlayText = 'OXXO Pay';
} elseif (stripos($note, 'Transferencia') !== false) { } elseif (stripos($note, 'Transferencia') !== false) {
$overlayText = 'Transferencia Bancaria'; $overlayText = 'Transferencia Bancaria';
} }
// Coordenadas APROXIMADAS para el template default de UCRM (Ajustar si es necesario) // Coordenadas APROXIMADAS para el template default de UCRM (Ajustar si es necesario)
@ -477,7 +502,7 @@ class ClientCallBellAPI
], ],
[ [
'name' => 'mode', 'name' => 'mode',
'contents' => 'full' 'contents' => 'full'
] ]
]; ];
@ -497,19 +522,19 @@ class ClientCallBellAPI
$httpClient = new Client(); $httpClient = new Client();
$responseMs = $httpClient->post($microserviceUrl, $guzzleParams); $responseMs = $httpClient->post($microserviceUrl, $guzzleParams);
if ($responseMs->getStatusCode() === 200) { if ($responseMs->getStatusCode() === 200) {
$jpgContent = $responseMs->getBody()->getContents(); $jpgContent = $responseMs->getBody()->getContents();
if (file_put_contents($rutaImagen, $jpgContent) !== false) { if (file_put_contents($rutaImagen, $jpgContent) !== false) {
$log->appendLog("Imagen generada por microservicio guardada en: $rutaImagen" . PHP_EOL); $log->appendLog("Imagen generada por microservicio guardada en: $rutaImagen" . PHP_EOL);
// 4. Subir a MinIO // 4. Subir a MinIO
$fileNameComprobanteImage = str_replace('.jpg', '_' . time() . '.jpg', basename($rutaImagen)); $fileNameComprobanteImage = str_replace('.jpg', '_' . time() . '.jpg', basename($rutaImagen));
$url_file = $minioService->uploadFile($rutaImagen, $fileNameComprobanteImage); $url_file = $minioService->uploadFile($rutaImagen, $fileNameComprobanteImage);
if ($url_file) { if ($url_file) {
$log->appendLog("Imagen subida a MinIO: $url_file" . PHP_EOL); $log->appendLog("Imagen subida a MinIO: $url_file" . PHP_EOL);
// 5. LIMPIEZA AUTOMÁTICA (Consistencia con req. de usuario) // 5. LIMPIEZA AUTOMÁTICA (Consistencia con req. de usuario)
if (file_exists($rutaArchivo)) { if (file_exists($rutaArchivo)) {
unlink($rutaArchivo); unlink($rutaArchivo);
@ -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

@ -1,6 +1,6 @@
<?php <?php
declare (strict_types = 1); declare(strict_types=1);
namespace SmsNotifier; namespace SmsNotifier;
@ -131,19 +131,17 @@ 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') {
$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); $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'];
//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':
@ -165,7 +163,7 @@ class Plugin
case 'payment_intent.succeeded': case 'payment_intent.succeeded':
$this->logger->info('Evento de pago exitoso (Stripe PI) recibido.'); $this->logger->info('Evento de pago exitoso (Stripe PI) recibido.');
if (isset($jsonData['data']['object'])) { if (isset($jsonData['data']['object'])) {
$this->pluginNotifierFacade->registerPaymentFromIntent($jsonData['data']['object']); $this->pluginNotifierFacade->registerPaymentFromIntent($jsonData['data']['object']);
} }
break; break;
case 'oxxo.retrieve': case 'oxxo.retrieve':
@ -175,7 +173,7 @@ class Plugin
if ($orderId) { if ($orderId) {
$status = $this->pluginOxxoNotifierFacade->getOxxoOrderStatus($orderId); $status = $this->pluginOxxoNotifierFacade->getOxxoOrderStatus($orderId);
$this->logger->debug("Estado recuperado para Orden #$orderId: " . json_encode($status)); $this->logger->debug("Estado recuperado para Orden #$orderId: " . json_encode($status));
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode($status); echo json_encode($status);
} else { } else {
@ -195,7 +193,7 @@ class Plugin
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode([ echo json_encode([
'oxxo_reference' => '', 'oxxo_reference' => '',
'url' => '', 'url' => '',
'error' => $stripeResult['data']['error'] ?? 'stripe_error', 'error' => $stripeResult['data']['error'] ?? 'stripe_error',
'status' => 'failed' 'status' => 'failed'
]); ]);
@ -203,13 +201,13 @@ class Plugin
} }
$oxxoData = $stripeResult['data']; $oxxoData = $stripeResult['data'];
// 2. Crear Orden (Sync) para obtener ID // 2. Crear Orden (Sync) para obtener ID
// Ahora pasamos $oxxoData que SI tiene clientID, clientFullName, etc. // Ahora pasamos $oxxoData que SI tiene clientID, clientFullName, etc.
$responseOxxo = $this->pluginOxxoNotifierFacade->createOxxoOrder($oxxoData); $responseOxxo = $this->pluginOxxoNotifierFacade->createOxxoOrder($oxxoData);
$orderId = $responseOxxo['order_id'] ?? null; $orderId = $responseOxxo['order_id'] ?? null;
// 3. Preparar respuesta inmediata (Pending) // 3. Preparar respuesta inmediata (Pending)
$responseArray = [ $responseArray = [
'oxxo_reference' => $responseOxxo['oxxo_reference'] ?? '', 'oxxo_reference' => $responseOxxo['oxxo_reference'] ?? '',
@ -229,13 +227,13 @@ class Plugin
while (ob_get_level() > 0) { while (ob_get_level() > 0) {
ob_end_clean(); ob_end_clean();
} }
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Connection: close'); header('Connection: close');
ignore_user_abort(true); // Permitir que el script siga corriendo ignore_user_abort(true); // Permitir que el script siga corriendo
echo json_encode($responseArray); echo json_encode($responseArray);
// Forzar envío al cliente // Forzar envío al cliente
if (function_exists('fastcgi_finish_request')) { if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request(); fastcgi_finish_request();
@ -250,18 +248,17 @@ class Plugin
$this->logger->info("Iniciando generación background para orden #$orderId"); $this->logger->info("Iniciando generación background para orden #$orderId");
set_time_limit(180); // 3 minutos para Puppeteer set_time_limit(180); // 3 minutos para Puppeteer
sleep(1); // Pequeña pausa para asegurar liberación del socket sleep(1); // Pequeña pausa para asegurar liberación del socket
$this->pluginOxxoNotifierFacade->generateOxxoVoucher($responseOxxo, true); $this->pluginOxxoNotifierFacade->generateOxxoVoucher($responseOxxo, true);
$this->logger->info("Generación background finalizada para orden #$orderId"); $this->logger->info("Generación background finalizada para orden #$orderId");
} else { } else {
$this->logger->error("No se pudo iniciar background job: Falta order_id"); $this->logger->error("No se pudo iniciar background job: Falta order_id");
} }
// Terminar proceso hijo
exit;
break;
// Terminar proceso hijo
exit;
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,9 +334,30 @@ class Plugin
} }
break; break;
case '1dd098fa-5d63-4c8d-88b7-3c27ffbbb6ae': case '1dd098fa-5d63-4c8d-88b7-3c27ffbbb6ae':
$payment_method = 'Tarjeta de crédito Stripe'; // [NEW] Logic to patch method ID based on metadata
if ($config['creditCardStripePaymentMethodId'] ?? false) { $this->pluginNotifierFacade->ensureStripePaymentAttribute($notification);
$this->notifierFacade->verifyPaymentActionToDo($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';
if ($config['creditCardStripePaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification);
}
} }
break; break;
case 'b9e1e9d1-5c7b-41d2-b6b2-3e568d700290': case 'b9e1e9d1-5c7b-41d2-b6b2-3e568d700290':
@ -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());
@ -444,10 +479,10 @@ class Plugin
// Automatización: Sincronizar cambios de Nombre o Email con Stripe // Automatización: Sincronizar cambios de Nombre o Email con Stripe
$nameBefore = trim(($entityBeforeEdit['firstName'] ?? '') . ' ' . ($entityBeforeEdit['lastName'] ?? '')); $nameBefore = trim(($entityBeforeEdit['firstName'] ?? '') . ' ' . ($entityBeforeEdit['lastName'] ?? ''));
if (empty($nameBefore)) $nameBefore = $entityBeforeEdit['companyName'] ?? ''; if (empty($nameBefore)) $nameBefore = $entityBeforeEdit['companyName'] ?? '';
$nameAfter = trim(($entity['firstName'] ?? '') . ' ' . ($entity['lastName'] ?? '')); $nameAfter = trim(($entity['firstName'] ?? '') . ' ' . ($entity['lastName'] ?? ''));
if (empty($nameAfter)) $nameAfter = $entity['companyName'] ?? ''; if (empty($nameAfter)) $nameAfter = $entity['companyName'] ?? '';
$emailBefore = null; $emailBefore = null;
foreach ($entityBeforeEdit['contacts'] ?? [] as $contact) { foreach ($entityBeforeEdit['contacts'] ?? [] as $contact) {
if ($contact['isBilling'] || $contact['isContact']) { if ($contact['isBilling'] || $contact['isContact']) {
@ -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";
}
}