- Se corrigió el número de versión estático mostrado en el footer del portal (public.php), el cual mostraba incorrectamente la versión 4.2.1.
- Ahora la versión visual coincide correctamente con la versión actual definida en el `manifest.json` (4.6.0).
This commit is contained in:
parent
7942f6b50c
commit
45a0d84caa
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,6 +4,7 @@
|
|||||||
*.jpeg
|
*.jpeg
|
||||||
.vscode/
|
.vscode/
|
||||||
*.zip
|
*.zip
|
||||||
|
*.csv
|
||||||
*.apib
|
*.apib
|
||||||
*.jrxml
|
*.jrxml
|
||||||
*.jasper
|
*.jasper
|
||||||
|
|||||||
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,5 +1,16 @@
|
|||||||
# CHANGELOG - SIIP WhatsApp Notifications Plugin
|
# CHANGELOG - SIIP WhatsApp Notifications Plugin
|
||||||
|
|
||||||
|
## VERSIÓN 4.6.0 - 11-04-2026
|
||||||
|
|
||||||
|
### 🚀 Nuevas Características (Features)
|
||||||
|
1️⃣ **Auditoría de Pagos Incompletos**: Agregados nuevos scripts (`scripts-uisp/audit_incomplete_pi.php` y `clean_incomplete_pi.php`) para auditar y limpiar intenciones de pago en el limbo que ocurrieron debido a falsas duplicidades de Stripe, protegiendo las referencias OXXO vigentes.
|
||||||
|
2️⃣ **Mejora del Historial SPEI**: Ajuste de vistas (`views/stripe.php` y `public.php`) para desplegar correctamente un "Historial de Intenciones de Pago por transferencia".
|
||||||
|
|
||||||
|
### 🐛 Correcciones (Bug Fixes)
|
||||||
|
1️⃣ **Fix Duplicidad Intenciones de Pago**: Removida la lógica basada en metadatos y fechas debido al retraso del Webhook de Stripe. Se implementó una verificación instantánea a nivel cliente (`customer_cash_balance_transaction.created`) de su saldo (`CashBalance`) en Stripe, evitando intenciones de pago duplicadas tras la financiación mediante transferencia SPEI.
|
||||||
|
2️⃣ **Fix Fichas OXXO y Microservicio**: Restaurado el microservicio Docker (`puppeteer-server`) mediante actualización de dependencias (`p-limit`).
|
||||||
|
3️⃣ **Fix Diseño de Ficha OXXO**: Evita que el panel izquierdo de llenado se estire junto a la imagen generada de la ficha, reubicando funcionalmente los accesos "Ver en CRM".
|
||||||
|
|
||||||
## VERSIÓN 4.5.0 - 13-03-2026
|
## VERSIÓN 4.5.0 - 13-03-2026
|
||||||
|
|
||||||
### ⚠️ Requisito Técnico (Update)
|
### ⚠️ Requisito Técnico (Update)
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
# SIIP - WhatsApp Notifications & Integrated Payment Portal
|
# SIIP - WhatsApp Notifications & Integrated Payment Portal
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
Este plugin es una solución integral que transforma tu UCRM en un **Portal Administrativo de Última Generación**. No solo automatiza la comunicación por WhatsApp, sino que integra un Dashboard completo para la gestión de pagos online (Stripe/OXXO), visualización de comprobantes y coordinación de equipos técnicos.
|
Este plugin es una solución integral que transforma tu UCRM en un **Portal Administrativo de Última Generación**. No solo automatiza la comunicación por WhatsApp, sino que integra un Dashboard completo para la gestión de pagos online (Stripe/OXXO), visualización de comprobantes y coordinación de equipos técnicos.
|
||||||
|
|
||||||
|
## ✨ Novedades v4.6.0 (Stripe & OXXO Stability)
|
||||||
|
|
||||||
|
- **🛡️ Estabilidad en Pagos (Stripe CashBalance)**: Nuevo sistema para comprobar fondos y validación contra intención de pagos para evitar las intenciones huérfanas o duplicadas tras recibir transferencias SPEI.
|
||||||
|
- **🧹 Limpieza y Auditoría Local**: Integración local de scripts `audit_incomplete_pi` y `clean_incomplete_pi` para mantenimiento seguro de intenciones de pago incompletas sin afectar comprobantes de OXXO vigentes.
|
||||||
|
- **🖼️ Interfaz OXXO Optimizada**: Rediseño interno y de contenedores CSS + validación del microservicio `puppeteer-server` para devolver fichas OXXO perfectamente legibles.
|
||||||
|
|
||||||
## ✨ Novedades v4.4.0 (Resend Job Notifications)
|
## ✨ Novedades v4.4.0 (Resend Job Notifications)
|
||||||
|
|
||||||
- **📋 Tabla de Tareas Activas por Instalador**: Nuevo módulo dentro de "Gestión de Instaladores" que muestra los jobs "En curso" de cada técnico con datos de cliente, fecha y descripción.
|
- **📋 Tabla de Tareas Activas por Instalador**: Nuevo módulo dentro de "Gestión de Instaladores" que muestra los jobs "En curso" de cada técnico con datos de cliente, fecha y descripción.
|
||||||
|
|||||||
6539
data/plugin.log
6539
data/plugin.log
File diff suppressed because one or more lines are too long
@ -5,13 +5,18 @@
|
|||||||
"displayName": "SIIP - Procesador de Pagos en línea con Stripe, Oxxo y Transferencia, Sincronizador de CallBell y Envío de Notificaciones y comprobantes vía WhatsApp",
|
"displayName": "SIIP - Procesador de Pagos en línea con Stripe, Oxxo y Transferencia, Sincronizador de CallBell y Envío de Notificaciones y comprobantes vía WhatsApp",
|
||||||
"description": "Este plugin sincroniza los clientes del sistema UISP CRM con los contactos de WhatsApp en CallBell, además procesa pagos de Stripe como las trasferencias bancarias y genera referencias de pago vía OXXO, además envía comprobantes de pago en formato imagen PNG o texto vía Whatsapp a los clientes",
|
"description": "Este plugin sincroniza los clientes del sistema UISP CRM con los contactos de WhatsApp en CallBell, además procesa pagos de Stripe como las trasferencias bancarias y genera referencias de pago vía OXXO, además envía comprobantes de pago en formato imagen PNG o texto vía Whatsapp a los clientes",
|
||||||
"url": "https://siip.mx/",
|
"url": "https://siip.mx/",
|
||||||
"version": "4.5.0",
|
"version": "4.6.0",
|
||||||
"unmsVersionCompliancy": {
|
"unmsVersionCompliancy": {
|
||||||
"min": "2.1.0",
|
"min": "2.1.0",
|
||||||
"max": null
|
"max": null
|
||||||
},
|
},
|
||||||
"author": "SIIP INTERNET",
|
"author": "SIIP INTERNET",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
|
{
|
||||||
|
"version": "4.6.0",
|
||||||
|
"date": "2026-04-11",
|
||||||
|
"changes": "Actualización: Resolución de duplicidad de intenciones de pago Stripe mediante validación CashBalance. Reparación del renderizado de fichas de OXXO, rediseño de OXXO UI y scripts de auditoría de PI."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"date": "2026-03-13",
|
"date": "2026-03-13",
|
||||||
|
|||||||
11
public.php
11
public.php
@ -337,11 +337,14 @@ if (isset($_GET['action'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($_GET['action'] === 'get_stripe_history') {
|
if ($_GET['action'] === 'get_stripe_history') {
|
||||||
$stripeCustomerId = $_GET['stripeCustomerId'] ?? null;
|
$stripeCustomerId = $_GET['stripeCustomerId'] ?? $_GET['customerId'] ?? null;
|
||||||
if ($stripeCustomerId) {
|
if ($stripeCustomerId) {
|
||||||
echo json_encode(['history' => $paymentIntentService->getLastPayments($stripeCustomerId, 10)]);
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'payments' => $paymentIntentService->getLastPayments($stripeCustomerId, 10)
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
echo json_encode(['error' => 'Missing customer id']);
|
echo json_encode(['success' => false, 'error' => 'Missing customer id']);
|
||||||
}
|
}
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@ -1995,7 +1998,7 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
|
|||||||
Plugin Desarrollado por <strong>SIIP INTERNET</strong> - Todos los derechos reservados.
|
Plugin Desarrollado por <strong>SIIP INTERNET</strong> - Todos los derechos reservados.
|
||||||
</p>
|
</p>
|
||||||
<p style="margin: 5px 0 0 0; font-size: 0.85rem; color: var(--text-muted);">
|
<p style="margin: 5px 0 0 0; font-size: 0.85rem; color: var(--text-muted);">
|
||||||
© <?php echo date('Y'); ?> SIIP Internet. Versión 4.2.1
|
© <?php echo date('Y'); ?> SIIP Internet. Versión 4.6.0
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
128
scripts-uisp/audit_incomplete_pi.php
Normal file
128
scripts-uisp/audit_incomplete_pi.php
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
$initialDir = getcwd();
|
||||||
|
chdir(__DIR__ . '/../');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
|
||||||
|
use Stripe\StripeClient;
|
||||||
|
use Stripe\Exception\ApiErrorException;
|
||||||
|
|
||||||
|
function logMessage($message)
|
||||||
|
{
|
||||||
|
$logFile = __DIR__ . '/audit_incomplete_pi.log';
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
file_put_contents($logFile, "[$timestamp] $message\n", FILE_APPEND);
|
||||||
|
if (php_sapi_name() === 'cli') {
|
||||||
|
fwrite(STDERR, "[$timestamp] $message\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Initialization --
|
||||||
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
|
$stripeApiKey = $config['tokenstripe'] ?? '';
|
||||||
|
|
||||||
|
if (empty($stripeApiKey)) {
|
||||||
|
logMessage("Error: Stripe Secret Key is missing in plugin configuration.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stripeClient = new StripeClient($stripeApiKey);
|
||||||
|
|
||||||
|
logMessage("Iniciando auditoría de Intenciones de Pago (SPEI/Transferencia) incompletas...");
|
||||||
|
|
||||||
|
$hasMore = true;
|
||||||
|
$nextPage = null;
|
||||||
|
$totalIncomplete = 0;
|
||||||
|
$customersWithIncomplete = [];
|
||||||
|
$pisToReview = [];
|
||||||
|
|
||||||
|
while ($hasMore) {
|
||||||
|
try {
|
||||||
|
$queryParams = [
|
||||||
|
'query' => 'status:"requires_payment_method" OR status:"requires_action"',
|
||||||
|
'limit' => 100
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($nextPage) {
|
||||||
|
$queryParams['page'] = $nextPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = $stripeClient->paymentIntents->search($queryParams);
|
||||||
|
|
||||||
|
foreach ($results->data as $pi) {
|
||||||
|
// Filtrar y proteger explícitamente los pagos OXXO
|
||||||
|
$paymentMethodTypes = $pi->payment_method_types ?? [];
|
||||||
|
$isOxxo = in_array('oxxo', $paymentMethodTypes);
|
||||||
|
|
||||||
|
// Si es un pago de OXXO vigente (o en general para estar seguros), lo saltamos
|
||||||
|
if ($isOxxo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrar solo las que son de Transferencia Bancaria real (SPEI)
|
||||||
|
$isBankTransfer = in_array('customer_balance', $paymentMethodTypes);
|
||||||
|
$tipoPago = $pi->metadata['tipoPago'] ?? '';
|
||||||
|
|
||||||
|
// Garantizar que solo tocamos las transferencias bancarias
|
||||||
|
if ($isBankTransfer || ($tipoPago === 'Transferencia Bancaria' && !$isOxxo)) {
|
||||||
|
$totalIncomplete++;
|
||||||
|
$customerId = $pi->customer;
|
||||||
|
|
||||||
|
if (!isset($customersWithIncomplete[$customerId])) {
|
||||||
|
$customersWithIncomplete[$customerId] = 0;
|
||||||
|
}
|
||||||
|
$customersWithIncomplete[$customerId]++;
|
||||||
|
|
||||||
|
$pisToReview[] = [
|
||||||
|
'id' => $pi->id,
|
||||||
|
'customer' => $customerId,
|
||||||
|
'amount' => $pi->amount / 100,
|
||||||
|
'currency' => $pi->currency,
|
||||||
|
'status' => $pi->status,
|
||||||
|
'created' => date('Y-m-d H:i:s', $pi->created)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasMore = $results->has_more;
|
||||||
|
$nextPage = $results->next_page;
|
||||||
|
|
||||||
|
// Pausa breve para evitar Rate Limits de Stripe
|
||||||
|
usleep(500000); // 0.5s
|
||||||
|
|
||||||
|
} catch (ApiErrorException $e) {
|
||||||
|
logMessage("Error en la API de Stripe: " . $e->getMessage());
|
||||||
|
break;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
logMessage("Error inesperado: " . $e->getMessage());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logMessage("=== RESULTADOS DE LA AUDITORÍA ===");
|
||||||
|
logMessage("Total de Intenciones de Pago Incompletas (SPEI): " . $totalIncomplete);
|
||||||
|
logMessage("Total de Clientes afectados: " . count($customersWithIncomplete));
|
||||||
|
|
||||||
|
$csvFile = __DIR__ . '/audit_incomplete_pi_results.csv';
|
||||||
|
$fp = fopen($csvFile, 'w');
|
||||||
|
if ($fp) {
|
||||||
|
fputcsv($fp, ['PaymentIntent_ID', 'Stripe_Customer', 'Amount', 'Currency', 'Status', 'Created_At']);
|
||||||
|
foreach ($pisToReview as $pi) {
|
||||||
|
fputcsv($fp, [
|
||||||
|
$pi['id'],
|
||||||
|
$pi['customer'],
|
||||||
|
$pi['amount'],
|
||||||
|
$pi['currency'],
|
||||||
|
$pi['status'],
|
||||||
|
$pi['created']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
fclose($fp);
|
||||||
|
logMessage("Se ha generado un archivo CSV con el detalle completo en: $csvFile");
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nResumen:\n";
|
||||||
|
echo "1. Intenciones huérfanas encontradas: $totalIncomplete\n";
|
||||||
|
echo "2. Clientes afectados: " . count($customersWithIncomplete) . "\n";
|
||||||
|
echo "-> Verifica el log y el archivo CSV para los detalles.\n";
|
||||||
122
scripts-uisp/clean_incomplete_pi.php
Normal file
122
scripts-uisp/clean_incomplete_pi.php
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
$initialDir = getcwd();
|
||||||
|
chdir(__DIR__ . '/../');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
|
||||||
|
use Stripe\StripeClient;
|
||||||
|
use Stripe\Exception\ApiErrorException;
|
||||||
|
|
||||||
|
function logMessage($message)
|
||||||
|
{
|
||||||
|
$logFile = __DIR__ . '/clean_incomplete_pi.log';
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
file_put_contents($logFile, "[$timestamp] $message\n", FILE_APPEND);
|
||||||
|
if (php_sapi_name() === 'cli') {
|
||||||
|
fwrite(STDERR, "[$timestamp] $message\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Argument Parsing --
|
||||||
|
$isDryRun = true;
|
||||||
|
foreach ($argv as $arg) {
|
||||||
|
if ($arg === '--confirm') {
|
||||||
|
$isDryRun = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Initialization --
|
||||||
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
|
$stripeApiKey = $config['tokenstripe'] ?? '';
|
||||||
|
|
||||||
|
if (empty($stripeApiKey)) {
|
||||||
|
logMessage("Error: Stripe Secret Key is missing in plugin configuration.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stripeClient = new StripeClient($stripeApiKey);
|
||||||
|
|
||||||
|
logMessage("Iniciando LIMPIEZA de Intenciones de Pago (SPEI/Transferencia) incompletas...");
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
logMessage("ATENCIÓN: Ejecutando en MODO PRUEBA (Dry Run). No se cancelará nada en Stripe.");
|
||||||
|
logMessage("Para ejecutar la limpieza real, pasa el argumento: --confirm");
|
||||||
|
} else {
|
||||||
|
logMessage("ATENCIÓN: Ejecutando en MODO DESTRUCTIVO. Se procederá a cancelar las intenciones encontradas.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasMore = true;
|
||||||
|
$nextPage = null;
|
||||||
|
$totalCanceled = 0;
|
||||||
|
$totalFound = 0;
|
||||||
|
|
||||||
|
while ($hasMore) {
|
||||||
|
try {
|
||||||
|
$queryParams = [
|
||||||
|
'query' => 'status:"requires_payment_method" OR status:"requires_action"',
|
||||||
|
'limit' => 100
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($nextPage) {
|
||||||
|
$queryParams['page'] = $nextPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = $stripeClient->paymentIntents->search($queryParams);
|
||||||
|
|
||||||
|
foreach ($results->data as $pi) {
|
||||||
|
// Proteger y omitir por completo cualquier intención de OXXO
|
||||||
|
$paymentMethodTypes = $pi->payment_method_types ?? [];
|
||||||
|
$isOxxo = in_array('oxxo', $paymentMethodTypes);
|
||||||
|
|
||||||
|
if ($isOxxo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrar solo las que son de Transferencia Bancaria
|
||||||
|
$isBankTransfer = in_array('customer_balance', $paymentMethodTypes);
|
||||||
|
$tipoPago = $pi->metadata['tipoPago'] ?? '';
|
||||||
|
|
||||||
|
if ($isBankTransfer || ($tipoPago === 'Transferencia Bancaria' && !$isOxxo)) {
|
||||||
|
$totalFound++;
|
||||||
|
$logStr = "PI: {$pi->id} | Cliente: {$pi->customer} | Monto: " . ($pi->amount / 100) . " {$pi->currency}";
|
||||||
|
|
||||||
|
if (!$isDryRun) {
|
||||||
|
try {
|
||||||
|
$stripeClient->paymentIntents->cancel($pi->id, [
|
||||||
|
'cancellation_reason' => 'abandoned'
|
||||||
|
]);
|
||||||
|
logMessage("[CANCELADA] $logStr");
|
||||||
|
$totalCanceled++;
|
||||||
|
} catch (ApiErrorException $e) {
|
||||||
|
logMessage("[ERROR al cancelar] $logStr - Detalle: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logMessage("[SIMULACIÓN - Sería Cancelada] $logStr");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasMore = $results->has_more;
|
||||||
|
$nextPage = $results->next_page;
|
||||||
|
|
||||||
|
// Pausa breve para evitar Rate Limits
|
||||||
|
usleep(500000); // 0.5s
|
||||||
|
|
||||||
|
} catch (ApiErrorException $e) {
|
||||||
|
logMessage("Error en la API de Stripe: " . $e->getMessage());
|
||||||
|
break;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
logMessage("Error inesperado: " . $e->getMessage());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logMessage("=== RESUMEN DE LA LIMPIEZA ===");
|
||||||
|
logMessage("Total encontradas (SPEI): $totalFound");
|
||||||
|
if (!$isDryRun) {
|
||||||
|
logMessage("Total CANCELADAS exitosamente: $totalCanceled");
|
||||||
|
} else {
|
||||||
|
logMessage("Modo PRUEBA finalizado. Ninguna fue cancelada.");
|
||||||
|
}
|
||||||
|
echo "\nVerifica el log 'clean_incomplete_pi.log' para los detalles.\n";
|
||||||
@ -55,40 +55,21 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
try {
|
try {
|
||||||
$stripeCustomer = $stripe->customers->retrieve($customer);
|
$stripeCustomer = $stripe->customers->retrieve($customer);
|
||||||
$ucrmClientId = $stripeCustomer->metadata->ucrm_client_id ?? null;
|
$ucrmClientId = $stripeCustomer->metadata->ucrm_client_id ?? null;
|
||||||
|
// [FIX] Validar el Saldo Disponible en la Billetera (Cash Balance)
|
||||||
|
// Si la cuenta tiene reconciliación automática, Stripe absorbe fondos instantáneamente
|
||||||
|
// para pagar intenciones preexistentes. Si ya se gastaron los fondos, evitamos duplicar.
|
||||||
|
$cashBalance = $stripe->customers->retrieveCashBalance($customer, []);
|
||||||
|
$availableMxn = $cashBalance->available->mxn ?? 0;
|
||||||
|
$amountCents = (int)$amount;
|
||||||
|
|
||||||
// [NEW] Check for existing PENDING PaymentIntent to avoid duplicates
|
// Si el saldo disponible es menor al monto fondeado, significa que una PI
|
||||||
// Especially useful if webhook is retried or if one was already created.
|
// preexistente ya "se tragó" este saldo automáticamente.
|
||||||
$existingPIs = $stripe->paymentIntents->search([
|
if ($availableMxn < $amountCents) {
|
||||||
'query' => "customer:'$customer' AND status:'requires_payment_method' AND amount>=" . ((int)$amount - 100) . " AND amount<=" . ((int)$amount + 100) . " AND metadata['tipoPago']:'Transferencia Bancaria'",
|
$this->logger->info("Fondos ya reconciliados por Stripe. Saldo disponible ({$availableMxn}) es menor al evento ({$amountCents}). Se descarta la creación de un nuevo PaymentIntent duplicado.");
|
||||||
'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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$pi = $stripe->paymentIntents->create([
|
$pi = $stripe->paymentIntents->create([
|
||||||
'amount' => (int)$amount,
|
'amount' => (int)$amount,
|
||||||
'currency' => 'mxn',
|
'currency' => 'mxn',
|
||||||
|
|||||||
@ -144,27 +144,52 @@ class PaymentIntentService
|
|||||||
public function getLastPayments($stripeCustomerId, $limit = 10)
|
public function getLastPayments($stripeCustomerId, $limit = 10)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
// Buscamos más registros para filtrar por tipo de pago (transferencias)
|
||||||
$collection = $this->stripeClient->paymentIntents->all([
|
$collection = $this->stripeClient->paymentIntents->all([
|
||||||
'customer' => $stripeCustomerId,
|
'customer' => $stripeCustomerId,
|
||||||
'limit' => $limit,
|
'limit' => 50,
|
||||||
|
'expand' => ['data.charges']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$result = [];
|
$result = [];
|
||||||
foreach ($collection->data as $payment) {
|
foreach ($collection->data as $payment) {
|
||||||
$description = $payment->description ?? $payment->metadata['description'] ?? 'Pago Stripe';
|
// Filtramos por transferencias bancarias (balance de cliente)
|
||||||
|
$isBankTransfer = in_array('customer_balance', $payment->payment_method_types);
|
||||||
|
|
||||||
|
if ($isBankTransfer) {
|
||||||
|
$description = $payment->description ?? $payment->metadata['description'] ?? 'Transferencia Bancaria';
|
||||||
|
|
||||||
|
// Extraer referencia si existe en los cargos
|
||||||
|
$reference = '-';
|
||||||
|
if (!empty($payment->charges->data)) {
|
||||||
|
foreach ($payment->charges->data as $charge) {
|
||||||
|
if ($charge->payment_method_details->type === 'customer_balance') {
|
||||||
|
$bankTransfer = $charge->payment_method_details->customer_balance->bank_transfer;
|
||||||
|
if ($bankTransfer && isset($bankTransfer->reference)) {
|
||||||
|
$reference = $bankTransfer->reference;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$result[] = [
|
$result[] = [
|
||||||
'id' => $payment->id,
|
'id' => $payment->id,
|
||||||
'amount' => $payment->amount / 100,
|
'amount' => $payment->amount / 100,
|
||||||
'currency' => strtoupper($payment->currency),
|
'currency' => strtoupper($payment->currency),
|
||||||
'status' => $payment->status,
|
'status' => $payment->status,
|
||||||
'created' => $payment->created,
|
'created' => $payment->created,
|
||||||
'description' => $description
|
'date' => date('d/m/Y H:i', $payment->created),
|
||||||
|
'description' => $description,
|
||||||
|
'reference' => $reference
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (count($result) >= $limit) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return $result;
|
return $result;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->log("Error fetching last payments: " . $e->getMessage());
|
$this->log("Error fetching last payments: " . $e->getMessage());
|
||||||
return ['error' => $e->getMessage()];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,9 +33,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="oxxoDetailContainer" style="display: none; margin-top: 2rem;">
|
<div id="oxxoDetailContainer" style="display: none; margin-top: 2rem;">
|
||||||
<div class="stripe-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: start;">
|
<div class="stripe-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||||
<!-- Left Column: Form & Client Info -->
|
<!-- Left Column: Form & Client Info -->
|
||||||
<div class="card" style="height: 100%; display: flex; flex-direction: column;">
|
<div class="card" style="align-self: start; display: flex; flex-direction: column;">
|
||||||
<!-- Client Header -->
|
<!-- Client Header -->
|
||||||
<div style="border-bottom: 1px solid var(--border); padding-bottom: 1rem; margin-bottom: 1.5rem;">
|
<div style="border-bottom: 1px solid var(--border); padding-bottom: 1rem; margin-bottom: 1.5rem;">
|
||||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 0.5rem;">
|
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 0.5rem;">
|
||||||
@ -46,12 +46,17 @@
|
|||||||
<p id="oxxoClientIdDisplay" style="color: var(--text-muted); margin: 0; font-size: 0.9rem;">ID: #0</p>
|
<p id="oxxoClientIdDisplay" style="color: var(--text-muted); margin: 0; font-size: 0.9rem;">ID: #0</p>
|
||||||
<span class="badge" id="oxxoBalanceBadge" style="background: #fee2e2; color: #ef4444; font-size: 0.8rem; padding: 2px 8px; border-radius: 4px;">Saldo: $0.00</span>
|
<span class="badge" id="oxxoBalanceBadge" style="background: #fee2e2; color: #ef4444; font-size: 0.8rem; padding: 2px 8px; border-radius: 4px;">Saldo: $0.00</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top: 1rem;">
|
||||||
|
<a id="btnOxxoCrm" href="#" target="_blank" class="btn btn-uniform" style="text-align: center; display: flex; justify-content: center; width: 100%; padding: 12px; border-radius: 8px; font-size: 1rem;">
|
||||||
|
<img src="?action=image&file=crm.webp" class="icon-crm" style="vertical-align: middle; margin-right: 5px; width: 16px;"> Ver en CRM
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OXXO Form -->
|
<!-- OXXO Form -->
|
||||||
<div style="flex: 1; display: flex; flex-direction: column; justify-content: center;">
|
<div style="flex: 1; display: flex; flex-direction: column; justify-content: center;">
|
||||||
<div style="text-align: center; margin-bottom: 1.5rem;">
|
<div style="text-align: center; margin-bottom: 1.5rem;">
|
||||||
<img src="?action=image&file=oxxo-logo.png" style="max-width: 100px; height: auto;">
|
<img src="?action=image&file=oxxo-logo.png" style="max-width: 200px; height: auto;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" style="margin-bottom: 1.5rem;">
|
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||||
@ -66,9 +71,6 @@
|
|||||||
<button id="btnCreateOxxoIntent" class="btn btn-primary" style="width: 100%; justify-content: center; background-color: #E20613; border-color: #E20613; color: white; display: flex; align-items: center; gap: 10px; padding: 12px; border-radius: 8px; font-weight: 600; font-size: 1rem; transition: all 0.2s;">
|
<button id="btnCreateOxxoIntent" class="btn btn-primary" style="width: 100%; justify-content: center; background-color: #E20613; border-color: #E20613; color: white; display: flex; align-items: center; gap: 10px; padding: 12px; border-radius: 8px; font-weight: 600; font-size: 1rem; transition: all 0.2s;">
|
||||||
Generar Ficha OXXO Pay
|
Generar Ficha OXXO Pay
|
||||||
</button>
|
</button>
|
||||||
<a id="btnOxxoCrm" href="#" target="_blank" class="btn btn-uniform" style="text-align: center; display: flex; justify-content: center; width: 100%; padding: 12px; border-radius: 8px; font-size: 1rem;">
|
|
||||||
<img src="?action=image&file=crm.webp" class="icon-crm" style="vertical-align: middle; margin-right: 5px; width: 16px;"> Ver en CRM
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -178,7 +180,9 @@
|
|||||||
resDiv.innerHTML=`
|
resDiv.innerHTML=`
|
||||||
<div style="text-align:center; width: 100%; animation: fadeIn 0.5s ease-out;">
|
<div style="text-align:center; width: 100%; animation: fadeIn 0.5s ease-out;">
|
||||||
<div style="background: #dcfce7; color: #15803d; width: 60px; height: 60px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
|
<div style="background: #dcfce7; color: #15803d; width: 60px; height: 60px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
|
||||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
||||||
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 style="color:var(--text-main); margin-bottom: 0.5rem;">¡Ficha Generada!</h3>
|
<h3 style="color:var(--text-main); margin-bottom: 0.5rem;">¡Ficha Generada!</h3>
|
||||||
<p style="color:var(--text-muted); margin-bottom: 1.5rem;">Referencia OXXO Pay</p>
|
<p style="color:var(--text-muted); margin-bottom: 1.5rem;">Referencia OXXO Pay</p>
|
||||||
@ -191,7 +195,11 @@
|
|||||||
|
|
||||||
<div style="display: flex; gap: 10px; justify-content: center;">
|
<div style="display: flex; gap: 10px; justify-content: center;">
|
||||||
<a href="${url}" target="_blank" class="btn btn-primary" style="display: inline-flex; align-items: center; gap: 8px;">
|
<a href="${url}" target="_blank" class="btn btn-primary" style="display: inline-flex; align-items: center; gap: 8px;">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
|
</svg>
|
||||||
Descargar Ficha
|
Descargar Ficha
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -101,7 +101,7 @@
|
|||||||
|
|
||||||
<div id="stripeHistoryContainer" style="display:none; margin-top: 2rem; border-top: 1px solid var(--border); padding-top: 1.5rem;">
|
<div id="stripeHistoryContainer" style="display:none; margin-top: 2rem; border-top: 1px solid var(--border); padding-top: 1.5rem;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||||
<h4 class="section-title" style="margin-bottom: 0;">Historial de Pagos (Últimos 10)</h4>
|
<h4 class="section-title" style="margin-bottom: 0;">Historial de Intenciones de Pago por transferencia</h4>
|
||||||
<div id="stripeCashBalanceBadge" class="balance-badge" style="display:none;">
|
<div id="stripeCashBalanceBadge" class="balance-badge" style="display:none;">
|
||||||
<img src="?action=image&file=account-balance.webp" style="width: 32px; height: 32px; margin-right: 8px;">
|
<img src="?action=image&file=account-balance.webp" style="width: 32px; height: 32px; margin-right: 8px;">
|
||||||
<span id="stripeCashBalanceText">Saldo Stripe: $0.00 MXN</span>
|
<span id="stripeCashBalanceText">Saldo Stripe: $0.00 MXN</span>
|
||||||
@ -195,7 +195,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!d.payments || d.payments.length === 0) {
|
if (!d.payments || d.payments.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;padding:2rem;color:var(--text-muted)">No hay pagos recientes</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;padding:2rem;color:var(--text-muted)">No hay intenciones de pago recientes</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user