- 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:
DANYDHSV 2026-04-28 10:15:09 -06:00
parent 7942f6b50c
commit 45a0d84caa
12 changed files with 7059 additions and 230 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
*.jpeg *.jpeg
.vscode/ .vscode/
*.zip *.zip
*.csv
*.apib *.apib
*.jrxml *.jrxml
*.jasper *.jasper

View File

@ -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)

View File

@ -1,12 +1,18 @@
# SIIP - WhatsApp Notifications & Integrated Payment Portal # SIIP - WhatsApp Notifications & Integrated Payment Portal
![Version](https://img.shields.io/badge/version-4.4.0-blue.svg?style=for-the-badge) ![Version](https://img.shields.io/badge/version-4.6.0-blue.svg?style=for-the-badge)
![UCRM Compatibility](https://img.shields.io/badge/UCRM-v2.1.0%2B-green.svg?style=for-the-badge) ![UCRM Compatibility](https://img.shields.io/badge/UCRM-v2.1.0%2B-green.svg?style=for-the-badge)
![Status](https://img.shields.io/badge/status-PRODUCTION-success.svg?style=for-the-badge) ![Status](https://img.shields.io/badge/status-PRODUCTION-success.svg?style=for-the-badge)
![Author](https://img.shields.io/badge/author-SIIP_INTERNET-orange.svg?style=for-the-badge) ![Author](https://img.shields.io/badge/author-SIIP_INTERNET-orange.svg?style=for-the-badge)
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.

File diff suppressed because one or more lines are too long

View File

@ -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",

View File

@ -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>

View 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";

View 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";

View File

@ -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',

View File

@ -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 [];
} }
} }

View File

@ -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>

View File

@ -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;
} }