feat(v4.2.0): Visualizador de Pagos + Fix Sincronización CallBell

Nuevas Características: • Visualizador de pagos mensuales con gráfica de dona (Chart.js) • Tarjetas estadísticas: clientes activos, pagados y pendientes • Tabla de clientes pendientes con saldos en tiempo real • Microservicio Node.js para metadata de Stripe (acceso directo a BD)

Mejoras: • Fix crítico: Sincronización automática de saldo en CallBell al agregar facturas • Categorización mejorada de pagos OXXO y Transferencias Stripe • Normalización de valores: "OXXO" → "OXXO Pay" para evitar errores 422 • Configuración .env para credenciales de base de datos

Correcciones: • Saldo y estado ahora se actualizan correctamente en CallBell • Fix networking Docker (ECONNREFUSED resuelto) • Fix validación de atributos en API de UCRM • Actualización automática de userId en pagos Stripe

Archivos principales:

public.php (visualizador de pagos)
AbstractMessageNotifierFacade.php (logging sync)
ClientCallBellAPI.php (comparación de campos)
AbstractStripeOperationsFacade.php (normalización)
manifest.json, README.md, CHANGELOG.md (docs)
This commit is contained in:
DANYDHSV 2026-01-18 18:22:00 -06:00
parent 84a25a829c
commit 2926211060
3070 changed files with 4103 additions and 290 deletions

View File

@ -1,5 +1,53 @@
# CHANGELOG - siip-whatsapp-notifications # CHANGELOG - siip-whatsapp-notifications
## VERSIÓN 4.2.0 - 17-01-2026
### 🚀 Nuevas Características (Features)
1. **Visualizador de Pagos Mensuales**:
* Nueva sección en el portal administrativo para análisis visual de pagos.
* Selector de mes para consultar estadísticas de cualquier período.
* Tarjetas estadísticas mostrando: Total de clientes activos, Clientes que pagaron, Clientes pendientes.
* Gráfica de dona (doughnut) interactiva con Chart.js mostrando proporción de pagos.
* Tabla detallada de clientes pendientes con información de saldo.
* Cálculo automático de porcentajes de completitud.
* Loader/spinner animado mientras se cargan los datos.
2. **Integración de Microservicio para Metadata de Stripe**:
* Nuevo microservicio Node.js (`vouchers-oxxopay-generator-service`) con acceso directo a la base de datos UCRM.
* Endpoint `/stripe-metadata/:id` para obtener metadata de pagos Stripe que no está disponible en la API de UCRM.
* Endpoint `/payments/:id/user` para actualizar el `userId` de pagos vía SQL directo (campo no modificable por API).
* Configuración mediante archivo `.env` para credenciales de base de datos.
### 🔵 Mejoras (Enhancements)
1. **Sincronización Mejorada con CallBell**:
* Fix crítico en sincronización de saldo: Ahora se actualiza correctamente en CallBell cuando se agregan facturas.
* Logging detallado agregado a `onlyUpdate()` para diagnóstico de sincronización.
* Comparación inteligente de campos para evitar PATCH innecesarios.
* Sincronización automática al agregar/editar facturas (`invoice.add`, `invoice.edit`).
* Sincronización automática al editar servicios (`service.edit`, `service.suspend`, etc.).
2. **Mejora en Categorización de Pagos Stripe**:
* Lógica mejorada en `ensureStripePaymentAttribute()` para asignar correctamente el atributo `tipoPagoStripe`.
* Normalización de valores de metadata: "OXXO" → "OXXO Pay" para coincidir con opciones de UCRM.
* Prioridad a metadata obtenida del microservicio sobre adivinación por nombre de método.
* Fix de validación 422 para pagos OXXO que fallaban por valor de atributo no válido.
3. **Configuración para Producción**:
* Archivo `.env` agregado al microservicio con todas las credenciales de base de datos.
* `docker-compose.yml` refactorizado para usar variables de entorno desde `.env`.
* Red Docker `unms_internal` configurada para comunicación segura entre servicios.
* Puerto 5432 de PostgreSQL expuesto en `docker-compose.yml` principal para acceso del microservicio.
### 🐛 Correcciones (Bug Fixes)
1. **Fix Metadata de Pagos Stripe**: Los pagos por transferencia bancaria y OXXO ahora se categorizan correctamente.
2. **Fix sincronización CallBell**: Saldo y estado del cliente ahora se actualizan correctamente en tiempo real.
3. **Fix networking Docker**: Resueltos problemas de `ECONNREFUSED` entre contenedores mediante configuración de red interna.
4. **Fix validación de atributos**: Normalización de valores para evitar errores 422 en la API de UCRM.
### 📝 Documentación
1. Logging mejorado en `ClientCallBellAPI.php` para comparaciones de campo.
2. Logging agregado en `AbstractMessageNotifierFacade.php` para diagnóstico de sync.
3. Documentación completa del flujo de datos del visualizador de pagos.
## VERSIÓN 4.1.0 - 15-01-2026 ## VERSIÓN 4.1.0 - 15-01-2026
### 🚀 Nuevas Características (Features) ### 🚀 Nuevas Características (Features)
1. **Microservicio PDF (`pdf-cropper`)**: 1. **Microservicio PDF (`pdf-cropper`)**:

View File

@ -1,107 +0,0 @@
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
// URL base de la API
$baseUri = 'https://172.16.5.134/crm/api/v1.0/';
// Token de autenticación
$token = '6abef18c-783d-4dd0-b530-be6e6a7bbd1d';
// Configuración del cliente GuzzleHttp
$clientGuzzleHttp = new Client([
'base_uri' => $baseUri,
'headers' => [
'X-Auth-App-Key' => $token, // Cambia el nombre de la cabecera de autorización
'Accept' => 'application/pdf', // Indica que esperamos una respuesta en formato JSON
],
'verify' => false,
]);
// Hacer la solicitud GET
$response = $clientGuzzleHttp->request('GET', "payments/89/pdf");
// Obtener el cuerpo de la respuesta (contenido binario)
$binaryData = $response->getBody()->getContents();
// Abrir un recurso de flujo de datos en memoria
$fileHandle = fopen('php://temp', 'r+');
// Escribir los datos binarios en el recurso de flujo de datos en memoria
fwrite($fileHandle, $binaryData);
// Rebobinar el puntero del flujo de datos para leer desde el principio
rewind($fileHandle);
if (!isset($fileHandle)) {
print_r("viene vacia la variable filehandle" . PHP_EOL);
}
// Subir el archivo al servidor FTP usando ftp_fput
$remoteFilename = 'archivo.pdf';
$pdf_payment_path = '/home/unms/data/ucrm/ucrm/data/payment_receipts/hola.txt';
$permisos = 0777; // Permisos 777 (lectura, escritura y ejecución para todos)
// Verificar si el archivo existe antes de cambiar los permisos
if (file_exists($pdf_payment_path)) {
// Cambiar los permisos del archivo
if (chmod($pdf_payment_path, $permisos)) {
print_r("Los permisos del archivo se han cambiado correctamente." . PHP_EOL);
} else {
print_r("No se pudieron cambiar los permisos del archivo." . PHP_EOL);
}
} else {
print_r("El archivo no existe en la ruta especificada." . PHP_EOL);
}
UploadFileWordpress($fileHandle);
function UploadFileWordpress($fileHandle): string
{
// Configuración de conexión FTP
$ftp_server = "siip.mx";
$ftp_username = "siip0001";
$ftp_password = '$spGiT,[wa)n';
//$fileName = $fileNameComprobantePDF;
$remote_file = "/public_html/wp/wp-content/uploads/pdf/Comprobante_de_pago.pdf";
//$file_to_upload = '/home/unms/data/ucrm/ucrm/data/payment_receipts/' . $fileName;
$url = 'http://siip.mx/wp/wp-content/uploads/pdf/Comprobante_de_pago.pdf';
// Conexión FTP
$ftp_conn = ftp_connect($ftp_server) or die("No se pudo conectar al servidor FTP");
$login = ftp_login($ftp_conn, $ftp_username, $ftp_password);
// Verificar conexión y login
if ($ftp_conn && $login) {
print_r("Conexión FTP exitosa" . PHP_EOL);
var_dump($ftp_conn);
// Cargar archivo
if (ftp_fput($ftp_conn, $remote_file, $fileHandle, FTP_BINARY)) {
print_r("El archivo ha sido cargado exitosamente." . PHP_EOL);
print_r("La URL es: " . $url . PHP_EOL);
// Cerrar conexión FTP
ftp_close($ftp_conn);
return $url;
} else {
print_r("Error al cargar el archivo " . PHP_EOL);
}
// Cerrar conexión FTP
ftp_close($ftp_conn);
return '';
} else {
print_r("No se pudo conectar o iniciar sesión en el servidor FTP." . PHP_EOL);
return '';
}
}

View File

@ -1,12 +1,23 @@
# SIIP - WhatsApp Notifications & Integrated Payment Portal # SIIP - WhatsApp Notifications & Integrated Payment Portal
![Version](https://img.shields.io/badge/version-4.1.0-blue.svg?style=for-the-badge) ![Version](https://img.shields.io/badge/version-4.2.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.2.0 (Analytics & Sync)
- **📊 Visualizador de Pagos Mensuales**: Nueva herramienta de análisis que permite seleccionar cualquier mes y visualizar gráficamente:
- Estadísticas de clientes activos vs clientes que pagaron
- Gráfica de dona interactiva con Chart.js
- Listado detallado de clientes pendientes con saldos
- Porcentajes de cobranza en tiempo real
- **🔄 Sincronización Mejorada CallBell**: Fix crítico que garantiza la actualización automática del saldo y estado del cliente en CallBell cuando se agregan facturas o se modifican servicios.
- **🎯 Categorización Inteligente de Pagos**: Nuevo microservicio con acceso directo a la base de datos para obtener metadata de Stripe (tipo de pago) y asignar correctamente los atributos incluso cuando la API de UCRM no tiene la información.
- **⚙️ Configuración para Producción**: Sistema de `.env` implementado para gestión segura de credenciales de base de datos.
## 🚀 Novedades v4.1.0 (Performance & Storage) ## 🚀 Novedades v4.1.0 (Performance & Storage)
- **⚡ Microservicio PDF (`pdf-cropper`)**: Nuevo motor de renderizado externo (Python/FastAPI) que reemplaza librerías legacy, aumentando la velocidad y eliminando problemas de memoria en el servidor principal. - **⚡ Microservicio PDF (`pdf-cropper`)**: Nuevo motor de renderizado externo (Python/FastAPI) que reemplaza librerías legacy, aumentando la velocidad y eliminando problemas de memoria en el servidor principal.

26
check_method.php Normal file
View File

@ -0,0 +1,26 @@
<?php
chdir(__DIR__);
require_once 'vendor/autoload.php';
$ucrmConfig = json_decode(file_get_contents('ucrm.json'), true);
$ucrmUrl = $ucrmConfig['ucrmLocalUrl'] ?? 'http://localhost';
$appKey = $ucrmConfig['pluginAppKey'];
$client = new \GuzzleHttp\Client([
'base_uri' => rtrim($ucrmUrl, '/') . '/api/v1.0/',
'verify' => false,
'headers' => [
'X-Auth-App-Key' => $appKey
]
]);
$methodId = '1dd098fa-5d63-4c8d-88b7-3c27ffbbb6ae';
try {
$response = $client->get("payment-methods/$methodId");
$method = json_decode($response->getBody(), true);
echo "Method ID: $methodId\n";
echo "Name: " . $method['name'] . "\n";
} catch (\Exception $e) {
echo "ERROR: " . $e->getMessage() . "\n";
}

View File

@ -1,175 +0,0 @@
Pregunta: Algún microservicio que pueda levantar con docker compose y que funcione como API para pasarle un PDF y me devuelva una imagen y recortada en ciertas coordenadas o ajustada a formato cuadrado.
Respuesta: Para lo que necesitas, la mejor opción no es una herramienta genérica (como Gotenberg), ya que estas suelen convertir la página entera pero no permiten recortar por coordenadas en la misma petición.
La solución más limpia y eficiente es crear un microservicio ligero en Python (usando FastAPI y pdf2image) que puedas levantar con Docker. Python tiene las mejores librerías para manipulación de PDFs e imágenes.
Aquí tienes la solución completa "Copy-Paste":
1. Estructura del proyecto
Crea una carpeta (ej. pdf-cropper) con estos 3 archivos:
Dockerfile
main.py
docker-compose.yml
2. El Código (Python + FastAPI)
Este script recibe el PDF, lo convierte a imagen y aplica el recorte o ajuste cuadrado según lo que le pidas.
Archivo: main.py
Python
from fastapi import FastAPI, UploadFile, File, Form
from pdf2image import convert_from_bytes
from PIL import Image
import io
app = FastAPI()
@app.post("/process")
async def process_pdf(
file: UploadFile = File(...),
mode: str = Form("square"), # 'square' o 'coords'
x: int = Form(0),
y: int = Form(0),
w: int = Form(500),
h: int = Form(500)
):
# 1. Leer el archivo PDF en memoria
pdf_bytes = await file.read()
# 2. Convertir la primera página a imagen
# poppler_path no es necesario si se instala en el sistema (ver Dockerfile)
images = convert_from_bytes(pdf_bytes, fmt="jpeg")
if not images:
return {"error": "No se pudo leer el PDF"}
img = images[0] # Tomamos la primera página
# 3. Lógica de recorte
if mode == "square":
# Recorte cuadrado centrado automático
width, height = img.size
new_dim = min(width, height)
left = (width - new_dim) / 2
top = (height - new_dim) / 2
right = (width + new_dim) / 2
bottom = (height + new_dim) / 2
img = img.crop((left, top, right, bottom))
# Opcional: Redimensionar al tamaño final deseado
img = img.resize((w, h))
elif mode == "coords":
# Recorte por coordenadas exactas
img = img.crop((x, y, x + w, y + h))
# 4. Devolver la imagen procesada
img_byte_arr = io.BytesIO()
img.save(img_byte_arr, format='JPEG', quality=90)
img_byte_arr.seek(0)
from starlette.responses import StreamingResponse
return StreamingResponse(img_byte_arr, media_type="image/jpeg")
3. El Dockerfile
Es crucial instalar poppler-utils, que es el motor que procesa el PDF.
Archivo: Dockerfile
Dockerfile
FROM python:3.9-slim
# Instalar poppler (necesario para pdf2image)
RUN apt-get update && apt-get install -y poppler-utils && rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Instalar dependencias de Python
RUN pip install fastapi uvicorn python-multipart pdf2image Pillow
COPY main.py .
# Ejecutar la API
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
4. Docker Compose
Para levantarlo.
Archivo: docker-compose.yml
YAML
services:
pdf-cropper:
build: .
ports:
- "8080:8000"
restart: always
Comando para iniciar:
Bash
docker compose up -d --build
5. Cómo usarlo desde PHP
Ahora tienes una API corriendo en http://localhost:8080/process. Aquí tienes el script PHP para enviarle un PDF y guardar la imagen recortada.
Archivo: cliente.php
PHP
<?php
$pdfPath = 'documento.pdf';
$outputPath = 'resultado.jpg';
// URL de tu microservicio docker
$url = 'http://localhost:8080/process';
$curl = curl_init();
$fields = [
'file' => new CURLFile($pdfPath, 'application/pdf', 'documento.pdf'),
// OPCIÓN A: Recorte cuadrado automático (centrado)
'mode' => 'square',
'w' => 800, // Tamaño final del cuadrado (px)
'h' => 800,
// OPCIÓN B: Coordenadas específicas (descomentar para usar)
// 'mode' => 'coords',
// 'x' => 100, // Píxeles desde la izquierda
// 'y' => 200, // Píxeles desde arriba
// 'w' => 500, // Ancho del recorte
// 'h' => 300 // Alto del recorte
];
curl_setopt_array($curl, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $fields,
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($httpCode == 200) {
file_put_contents($outputPath, $response);
echo "Imagen guardada exitosamente en $outputPath";
} else {
echo "Error: " . curl_error($curl);
echo "Respuesta del servidor: " . $response;
}
curl_close($curl);
?>
¿Por qué esta solución?
Atomocidad: Hace la conversión y el recorte en un solo paso de memoria (RAM), sin guardar archivos temporales en disco.
Control: Puedes ajustar la lógica de "cuadrado" (si quieres que sea centrado o desde la esquina superior) modificando fácilmente el main.py.
Ligero: La imagen Docker pesará poco y consumirá memoria solo cuando procese un archivo.

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,"logging_level":true,"minioEndpoint":"http://172.16.5.134:9002","minioPublicUrl":"https://aws-venus.siip.mx","minioAccessKey":"minioadmin","minioSecretKey":"minioadmin","minioBucket":"vouchers-oxxo"} {"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}

3496
data/plugin.log Normal file → Executable file

File diff suppressed because one or more lines are too long

34
explore_db.php Normal file
View File

@ -0,0 +1,34 @@
<?php
$host = 'localhost';
$port = '5432';
$dbname = 'unms';
$user = 'ucrm';
$password = 'MtxWkaa5O2Cwy3PRFVXtC01bXFyQjykRpAIqEOXC5FmpMURD';
try {
$dsn = "pgsql:host=$host;port=$port;dbname=$dbname;user=$user;password=$password";
$pdo = new PDO($dsn);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "Connected to DB.\n";
// List tables in ucrm schema matching 'stripe'
$stmt = $pdo->query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'ucrm' AND table_name LIKE '%stripe%'");
$tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
echo "Tables found:\n";
foreach ($tables as $table) {
echo "- $table\n";
}
// Also check for 'payment' tables if stripe ones aren't obvious/enough
$stmt2 = $pdo->query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'ucrm' AND table_name LIKE '%payment%' LIMIT 20");
$tables2 = $stmt2->fetchAll(PDO::FETCH_COLUMN);
echo "\nPayment Tables sample:\n";
foreach ($tables2 as $table) {
echo "- $table\n";
}
} catch (PDOException $e) {
echo "Connection failed: " . $e->getMessage() . "\n";
}

33
find_attribute_id.php Normal file
View File

@ -0,0 +1,33 @@
<?php
chdir(__DIR__);
require_once 'vendor/autoload.php';
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
$ucrmConfig = json_decode(file_get_contents('ucrm.json'), true);
$ucrmUrl = $ucrmConfig['ucrmLocalUrl'] ?? 'http://localhost';
$appKey = $ucrmConfig['pluginAppKey'];
$client = new \GuzzleHttp\Client([
'base_uri' => rtrim($ucrmUrl, '/') . '/api/v1.0/',
'verify' => false,
'headers' => [
'X-Auth-App-Key' => $appKey
]
]);
try {
$response = $client->get('custom-attributes');
$attributes = json_decode($response->getBody(), true);
foreach ($attributes as $attr) {
if ($attr['key'] === 'tipoPagoStripe') {
echo "FOUND: ID=" . $attr['id'] . " Name='" . $attr['name'] . "' Type=" . $attr['attributeType'] . "\n";
exit(0);
}
}
echo "NOT FOUND: tipoPagoStripe\n";
} catch (\Exception $e) {
echo "ERROR: " . $e->getMessage() . "\n";
}

24
inspect_payment.php Normal file
View File

@ -0,0 +1,24 @@
<?php
chdir(__DIR__);
require_once 'vendor/autoload.php';
$ucrmConfig = json_decode(file_get_contents('ucrm.json'), true);
$ucrmUrl = $ucrmConfig['ucrmLocalUrl'] ?? 'http://localhost';
$appKey = $ucrmConfig['pluginAppKey'];
$client = new \GuzzleHttp\Client([
'base_uri' => rtrim($ucrmUrl, '/') . '/api/v1.0/',
'verify' => false,
'headers' => [
'X-Auth-App-Key' => $appKey
]
]);
$paymentId = 818; // From screenshot
try {
$response = $client->get("payments/$paymentId");
echo $response->getBody();
} catch (\Exception $e) {
echo "ERROR: " . $e->getMessage() . "\n";
}

0
list_attributes.php Normal file → Executable file
View File

View File

@ -5,7 +5,7 @@
"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.1.0", "version": "4.2.0",
"unmsVersionCompliancy": { "unmsVersionCompliancy": {
"min": "2.1.0", "min": "2.1.0",
"max": null "max": null

View File

@ -688,11 +688,39 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
.search-item .details { .search-item .details {
font-size: 12px; font-size: 12px;
color: var(--text-muted);
} }
.section-view { display: none; } .section-view { display: none; }
.section-view.active { display: block; } .section-view.active { display: block; }
/* Payment Visualizer Styles */
.stat-card {
background: var(--bg-card);
padding: 1.5rem;
border-radius: 12px;
border: 1px solid var(--border);
}
.stat-card h3 {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.stat-card p {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-main);
margin: 0;
}
.stat-card.success p { color: var(--success); }
.stat-card.danger p { color: var(--danger); }
@keyframes spin {
to { transform: rotate(360deg); }
}
</style> </style>
</head> </head>
<body> <body>
@ -718,6 +746,14 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
<img src="?action=image&file=oxxo-logo.png" alt="" style="width: 28px; height: 28px; object-fit: contain; flex-shrink: 0;"> <img src="?action=image&file=oxxo-logo.png" alt="" style="width: 28px; height: 28px; object-fit: contain; flex-shrink: 0;">
<span>Pagos OXXO</span> <span>Pagos OXXO</span>
</a> </a>
<a href="#" class="nav-link" onclick="switchSection('payments-viz', this)">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="20" x2="12" y2="10"></line>
<line x1="18" y1="20" x2="18" y2="4"></line>
<line x1="6" y1="20" x2="6" y2="16"></line>
</svg>
<span>Visualizador Pagos</span>
</a>
</nav> </nav>
</aside> </aside>
@ -904,7 +940,74 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
<!-- Contenedor de Resultado Inline: Full Width --> <!-- Contenedor de Resultado Inline: Full Width -->
<div id="oxxoInlineResult" style="margin-top: 1.5rem; display: none;"></div> <div id="oxxoInlineResult" style="margin-top: 1.5rem; display: none;"></div>
</div> </div>
</div> </section>
<section id="section-payments-viz" class="section-view">
<!-- Selector de Mes -->
<div class="card" style="margin-bottom: 2rem;">
<div style="display: flex; gap: 1rem; align-items: flex-end;">
<div class="form-group" style="flex: 1; margin-bottom: 0;">
<label>Seleccionar Mes</label>
<input type="month" id="monthSelector" class="form-control" value="<?= date('Y-m') ?>">
</div>
<button class="btn btn-primary" onclick="loadPaymentsData()" style="height: 46px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
Cargar Datos
</button>
</div>
</div>
<!-- Loader -->
<div id="paymentsLoader" style="display: none; text-align: center; padding: 3rem;">
<div style="width: 50px; height: 50px; border: 4px solid var(--border); border-top-color: var(--primary); border-radius: 50%; margin: 0 auto; animation: spin 1s linear infinite;"></div>
<p style="margin-top: 1rem; color: var(--text-muted);">Cargando datos...</p>
</div>
<!-- Estadísticas Resumen -->
<div id="paymentsStats" style="display: none;">
<div class="stats-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
<div class="stat-card">
<h3>Total Clientes Activos</h3>
<p id="total-clients">0</p>
</div>
<div class="stat-card success">
<h3>Clientes que Pagaron</h3>
<p id="clients-paid">0</p>
<span id="paid-percentage" style="color: var(--text-muted); font-size: 0.875rem;"></span>
</div>
<div class="stat-card danger">
<h3>Clientes Pendientes</h3>
<p id="clients-pending">0</p>
<span id="pending-percentage" style="color: var(--text-muted); font-size: 0.875rem;"></span>
</div>
</div>
<!-- Gráfica -->
<div class="card" style="margin-bottom: 2rem;">
<canvas id="paymentsChart" style="max-height: 400px;"></canvas>
</div>
<!-- Tabla de Clientes Pendientes -->
<div class="card">
<h3 style="margin-bottom: 1.5rem;">Clientes Pendientes de Pago</h3>
<div class="table-container">
<table id="pendingTable">
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Email</th>
<th>Saldo</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</section>
</main> </main>
<!-- Modal Form --> <!-- Modal Form -->
@ -949,6 +1052,7 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
<div id="toast"></div> <div id="toast"></div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script> <script>
const store = { const store = {
installers: <?php echo json_encode($installersData['instaladores']); ?>, installers: <?php echo json_encode($installersData['instaladores']); ?>,
@ -986,6 +1090,9 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
} else if (sectionId === 'oxxo') { } else if (sectionId === 'oxxo') {
title.textContent = 'Pagos OXXO Pay'; title.textContent = 'Pagos OXXO Pay';
desc.textContent = 'Genera fichas de pago para establecimientos OXXO'; desc.textContent = 'Genera fichas de pago para establecimientos OXXO';
} else if (sectionId === 'payments-viz') {
title.textContent = 'Visualizador de Pagos';
desc.textContent = 'Análisis mensual de pagos de clientes activos';
} }
} }
@ -1584,6 +1691,177 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
setTimeout(() => toast.classList.remove('show'), 3000); setTimeout(() => toast.classList.remove('show'), 3000);
} }
// Payment Visualizer Functions
let paymentsChart = null;
async function loadPaymentsData() {
const month = document.getElementById('monthSelector').value;
if (!month) {
showToast('Por favor selecciona un mes', true);
return;
}
const [year, monthNum] = month.split('-');
const firstDay = `${year}-${monthNum.padStart(2, '0')}-01`;
const lastDay = new Date(year, monthNum, 0).toISOString().split('T')[0];
// Show loader
document.getElementById('paymentsLoader').style.display = 'block';
document.getElementById('paymentsStats').style.display = 'none';
try {
// 1. Fetch active clients
const clients = await fetchActiveClients();
// 2. Fetch payments for the month
const payments = await fetchPaymentsByMonth(firstDay, lastDay);
// 3. Calculate stats
const stats = calculateStats(clients, payments);
// 4. Update UI
updateStatsDisplay(stats);
updateChart(stats);
updatePendingTable(stats.pendingClients);
// Hide loader, show stats
document.getElementById('paymentsLoader').style.display = 'none';
document.getElementById('paymentsStats').style.display = 'block';
} catch (error) {
console.error('Error loading payments data:', error);
showToast('Error al cargar datos: ' + error.message, true);
document.getElementById('paymentsLoader').style.display = 'none';
}
}
async function fetchActiveClients() {
const response = await fetch('/crm/api/v1.0/clients?isArchived=0&limit=1000', {
headers: { 'X-Auth-App-Key': '<?= $config["apitoken"] ?>' }
});
if (!response.ok) throw new Error('Error al obtener clientes');
return await response.json();
}
async function fetchPaymentsByMonth(from, to) {
const response = await fetch(`/crm/api/v1.0/payments?createdDateFrom=${from}&createdDateTo=${to}&limit=5000`, {
headers: { 'X-Auth-App-Key': '<?= $config["apitoken"] ?>' }
});
if (!response.ok) throw new Error('Error al obtener pagos');
return await response.json();
}
function calculateStats(clients, payments) {
// Filter only clients with active services (not suspended or archived)
const activeClients = clients.filter(c => !c.hasSuspendedService && !c.isArchived);
// Create set of client IDs who paid this month
const paidClientIds = new Set(payments.map(p => p.clientId));
// Clients who paid
const clientsPaid = activeClients.filter(c => paidClientIds.has(c.id));
// Pending clients
const clientsPending = activeClients.filter(c => !paidClientIds.has(c.id));
const paidPercentage = activeClients.length > 0
? (clientsPaid.length / activeClients.length * 100).toFixed(1)
: 0;
return {
totalClients: activeClients.length,
clientsPaid: clientsPaid.length,
clientsPending: clientsPending.length,
pendingClients: clientsPending,
paidPercentage: paidPercentage
};
}
function updateStatsDisplay(stats) {
document.getElementById('total-clients').textContent = stats.totalClients;
document.getElementById('clients-paid').textContent = stats.clientsPaid;
document.getElementById('clients-pending').textContent = stats.clientsPending;
document.getElementById('paid-percentage').textContent = `${stats.paidPercentage}% del total`;
document.getElementById('pending-percentage').textContent = `${(100 - stats.paidPercentage).toFixed(1)}% del total`;
}
function updateChart(stats) {
const ctx = document.getElementById('paymentsChart').getContext('2d');
if (paymentsChart) paymentsChart.destroy();
paymentsChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Pagaron', 'Pendientes'],
datasets: [{
data: [stats.clientsPaid, stats.clientsPending],
backgroundColor: ['#22c55e', '#ef4444'],
borderWidth: 0,
hoverOffset: 10
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
position: 'bottom',
labels: {
font: { size: 14, weight: '600' },
padding: 20
}
},
title: {
display: true,
text: `Estado de Pagos del Mes (${stats.paidPercentage}% completado)`,
font: { size: 18, weight: '700' },
padding: { bottom: 30 }
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed || 0;
const percentage = ((value / stats.totalClients) * 100).toFixed(1);
return `${label}: ${value} clientes (${percentage}%)`;
}
}
}
}
}
});
}
function updatePendingTable(pendingClients) {
const tbody = document.querySelector('#pendingTable tbody');
tbody.innerHTML = '';
if (pendingClients.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; padding: 2rem; color: var(--text-muted);">¡Todos los clientes activos han pagado este mes! 🎉</td></tr>';
return;
}
pendingClients.forEach(client => {
const email = client.contacts?.find(c => c.email)?.email || 'Sin email';
const nombre = `${client.firstName || ''} ${client.lastName || ''}`.trim() || client.companyName || 'Sin nombre';
const saldo = client.accountBalance < 0
? `$${Math.abs(client.accountBalance).toFixed(2)} pendientes`
: `$${client.accountBalance.toFixed(2)} a favor`;
const row = document.createElement('tr');
row.innerHTML = `
<td>${client.id}</td>
<td>${nombre}</td>
<td>${email}</td>
<td style="color: ${client.accountBalance < 0 ? 'var(--danger)' : 'var(--success)'}; font-weight: 600;">${saldo}</td>
`;
tbody.appendChild(row);
});
}
renderTable(); renderTable();
</script> </script>
</body> </body>

View File

@ -242,11 +242,19 @@ abstract class AbstractMessageNotifierFacade
} }
public function onlyUpdate(NotificationData $notificationData, $phoneToUpdate): void { public function onlyUpdate(NotificationData $notificationData, $phoneToUpdate): void {
$this->logger->debug("onlyUpdate: Iniciando actualización para teléfono: $phoneToUpdate");
$config = PluginConfigManager::create()->loadConfig(); $config = PluginConfigManager::create()->loadConfig();
$api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']); $api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']);
$phone = $this->validarNumeroTelefono($phoneToUpdate); $phone = $this->validarNumeroTelefono($phoneToUpdate);
$this->logger->debug("onlyUpdate: Teléfono validado: $phone");
$contact = json_decode($api->getContactWhatsapp($phone), true); $contact = json_decode($api->getContactWhatsapp($phone), true);
if ($contact) $api->patchWhatsapp($contact, $notificationData); $this->logger->debug("onlyUpdate: Contacto obtenido de CallBell: " . json_encode($contact));
if ($contact) {
$this->logger->info("onlyUpdate: Ejecutando patchWhatsapp para teléfono: $phone");
$api->patchWhatsapp($contact, $notificationData);
} else {
$this->logger->warning("onlyUpdate: No se encontró contacto en CallBell para teléfono: $phone");
}
} }
public function onlyUpdateService(NotificationData $notificationData, $phoneToUpdate): void { public function onlyUpdateService(NotificationData $notificationData, $phoneToUpdate): void {

View File

@ -611,5 +611,133 @@ abstract class AbstractStripeOperationsFacade
return (strlen($n) === 10) ? '52' . $n : $n; return (strlen($n) === 10) ? '52' . $n : $n;
} }
public function ensureStripePaymentAttribute($notificationObject): void
{
// 1. Get Payment ID
$paymentId = is_object($notificationObject) ? ($notificationObject->entityId ?? null) : ($notificationObject['entityId'] ?? null);
if (!$paymentId) {
$this->logger->warning("ensureStripePaymentAttribute: No entityId found in notification.");
return;
}
$this->logger->info("Verificando existencia de atributo 'tipoPagoStripe' para Payment ID: $paymentId");
try {
// Load Config
$config = PluginConfigManager::create()->loadConfig();
$ipPuppeteer = $config['ipPuppeteer'] ?? 'localhost';
$portPuppeteer = $config['portPuppeteer'] ?? '3000';
$stripeUserId = $config['idPaymentAdminCRM'] ?? null;
$microserviceBaseUrl = "http://$ipPuppeteer:$portPuppeteer";
$httpClient = new Client();
// 2. Fetch Metadata from Microservice (DB Access)
$metadataTipoPago = null;
try {
$response = $httpClient->get("$microserviceBaseUrl/stripe-metadata/$paymentId", ['timeout' => 5]);
$data = json_decode($response->getBody()->getContents(), true);
if (isset($data['metadata']['tipoPago'])) {
$metadataTipoPago = $data['metadata']['tipoPago'];
$this->logger->info("Microservice found metadata: tipoPago = '$metadataTipoPago'");
}
} catch (\Throwable $e) {
$this->logger->warning("Microservice metadata fetch failed: " . $e->getMessage());
}
// 3. Update User ID if missing (Direct DB Patch via Microservice)
// UCRM API doesn't support PATCH userId, so we use microservice
if ($stripeUserId) {
try {
$payment = $this->ucrmApi->get('payments/' . $paymentId);
if (empty($payment['userId'])) {
$this->logger->info("Payment $paymentId has no User ID. Assigning Stripe User ID: $stripeUserId");
$httpClient->patch("$microserviceBaseUrl/payments/$paymentId/user", [
'json' => ['userId' => $stripeUserId],
'timeout' => 5
]);
} else {
$this->logger->debug("Payment $paymentId already has User ID: " . $payment['userId']);
}
} catch (\Throwable $e) {
$this->logger->error("Failed to patch User ID via microservice: " . $e->getMessage());
}
}
// 4. Determine Target Attribute Value
// 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?)
// Actually, Metadata keys are stronger than manual edits if the flow is automatic.
// 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.
$currentValue = null;
$hasAttribute = false;
foreach ($payment['attributes'] as $attr) {
if ($attr['key'] === 'tipoPagoStripe' || $attr['customAttributeId'] == 20) {
$hasAttribute = true;
$currentValue = $attr['value'];
break;
}
}
$targetValue = null;
if ($metadataTipoPago) {
// Normalize Metadata Values to Attribute Choice Values
if ($metadataTipoPago === 'OXXO') {
$targetValue = 'OXXO Pay';
} else {
$targetValue = $metadataTipoPago;
}
} else {
// Fallback to Method Name Guessing if Metadata missing
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.");
return;
}
$methodId = $payment['methodId'];
$method = $this->ucrmApi->get('payment-methods/' . $methodId);
$methodName = $method['name'] ?? '';
if (stripos($methodName, 'OXXO') !== false) {
$targetValue = 'OXXO Pay';
} elseif (stripos($methodName, 'Transferencia') !== false) {
$targetValue = 'Transferencia Bancaria';
} elseif (stripos($methodName, 'Tarjeta') !== false && stripos($methodName, 'Stripe') !== false) {
$targetValue = 'Tarjeta de Crédito';
}
$this->logger->debug("Fallback Method Guessing '$methodName' -> '$targetValue'");
}
// 5. Apply Update
if ($targetValue) {
// Check redundancy
if ($hasAttribute && $currentValue === $targetValue) {
$this->logger->debug("Attribute already matches target '$targetValue'. Skipping patch.");
return;
}
$this->logger->info("PATCHING Payment $paymentId: Setting tipoPagoStripe = '$targetValue'");
$this->ucrmApi->patch('payments/' . $paymentId, [
'attributes' => [
[
'customAttributeId' => 20,
'value' => $targetValue
]
]
]);
} else {
$this->logger->debug("No se pudo determinar el tipoPagoStripe.");
}
} catch (\Throwable $e) {
$this->logger->error("Error in ensureStripePaymentAttribute: " . $e->getMessage());
}
}
abstract protected function sendWhatsApp(NotificationData $notificationData, string $clientPhoneNumber): void; abstract protected function sendWhatsApp(NotificationData $notificationData, string $clientPhoneNumber): void;
} }

View File

@ -1150,6 +1150,11 @@ class ClientCallBellAPI
$contact = $response_getContactCallBell['contact'] ?? []; $contact = $response_getContactCallBell['contact'] ?? [];
$contactCustomFields = $contact['customFields'] ?? []; $contactCustomFields = $contact['customFields'] ?? [];
$log->appendLog("DEBUG COMPARACIÓN - CallBell Saldo Actual: '" . ($contactCustomFields['Saldo Actual'] ?? 'NULL') . "'" . PHP_EOL);
$log->appendLog("DEBUG COMPARACIÓN - UCRM Saldo Actual: '" . $data_CRM['custom_fields']['Saldo Actual'] . "'" . PHP_EOL);
$log->appendLog("DEBUG COMPARACIÓN - CallBell Estado: '" . ($contactCustomFields['Estado'] ?? 'NULL') . "'" . PHP_EOL);
$log->appendLog("DEBUG COMPARACIÓN - UCRM Estado: '" . $data_CRM['custom_fields']['Estado'] . "'" . PHP_EOL);
if ( if (
($contactCustomFields['Cliente'] ?? '') != $data_CRM['custom_fields']['Cliente'] ($contactCustomFields['Cliente'] ?? '') != $data_CRM['custom_fields']['Cliente']
|| ($contactCustomFields['Domicilio'] ?? '') != $data_CRM['custom_fields']['Domicilio'] || ($contactCustomFields['Domicilio'] ?? '') != $data_CRM['custom_fields']['Domicilio']
@ -1163,13 +1168,14 @@ class ClientCallBellAPI
|| ($contact['name'] ?? '') != $data_CRM['name'] || ($contact['name'] ?? '') != $data_CRM['name']
) { ) {
$log->appendLog("EJECUTANDO PATCH - Se detectaron cambios" . PHP_EOL);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data_CRM)); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data_CRM));
$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 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

@ -299,6 +299,9 @@ 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
$this->pluginNotifierFacade->ensureStripePaymentAttribute($notification);
$payment_method_id = $notification->paymentData['methodId']; $payment_method_id = $notification->paymentData['methodId'];
$payment_method = ''; $payment_method = '';

0
src/Service/MinioStorageService.php Normal file → Executable file
View File

0
src/Service/PaymentIntentService.php Normal file → Executable file
View File

0
unms-swagger.json Normal file → Executable file
View File

0
unms.yaml Normal file → Executable file
View File

0
unmscrm.apib Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/CODE_OF_CONDUCT.md vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/LICENSE vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/NOTICE vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/README.md vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/composer.json vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/AwsCredentials.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/CredentialsProvider.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/Signable.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SignatureType.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SignedBodyHeaderType.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/Signing.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SigningAlgorithm.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SigningConfigAWS.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SigningResult.php vendored Normal file → Executable file
View File

View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/CRT.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/HTTP/Headers.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/HTTP/Message.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/HTTP/Request.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/HTTP/Response.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/IO/EventLoopGroup.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/IO/InputStream.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/Internal/Encoding.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/Internal/Extension.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/Log.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/NativeResource.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-crt-php/src/AWS/CRT/Options.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/CODE_OF_CONDUCT.md vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/CRT_INSTRUCTIONS.md vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/LICENSE vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/NOTICE vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/THIRD-PARTY-LICENSES vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/composer.json vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/ACMPCA/ACMPCAClient.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/ACMPCA/Exception/ACMPCAException.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/AIOps/AIOpsClient.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/AIOps/Exception/AIOpsException.php vendored Normal file → Executable file
View File

View File

View File

0
vendor/aws/aws-sdk-php/src/ARCZonalShift/ARCZonalShiftClient.php vendored Normal file → Executable file
View File

View File

0
vendor/aws/aws-sdk-php/src/AbstractConfigurationProvider.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/AccessAnalyzer/AccessAnalyzerClient.php vendored Normal file → Executable file
View File

View File

0
vendor/aws/aws-sdk-php/src/Account/AccountClient.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Account/Exception/AccountException.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Acm/AcmClient.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Acm/Exception/AcmException.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Amplify/AmplifyClient.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Amplify/Exception/AmplifyException.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/AmplifyBackend/AmplifyBackendClient.php vendored Normal file → Executable file
View File

View File

View File

View File

0
vendor/aws/aws-sdk-php/src/Api/AbstractModel.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/ApiProvider.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/DateTimeResult.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/DocModel.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/ErrorParser/AbstractErrorParser.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/ErrorParser/JsonParserTrait.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/ErrorParser/JsonRpcErrorParser.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/ErrorParser/RestJsonErrorParser.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/ErrorParser/XmlErrorParser.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/ListShape.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/MapShape.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/Operation.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/Parser/AbstractParser.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/Parser/AbstractRestParser.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/Parser/Crc32ValidatingParser.php vendored Normal file → Executable file
View File

View File

0
vendor/aws/aws-sdk-php/src/Api/Parser/EventParsingIterator.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/Parser/Exception/ParserException.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/Parser/JsonParser.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/Parser/JsonRpcParser.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/Parser/MetadataParserTrait.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/Parser/PayloadParserTrait.php vendored Normal file → Executable file
View File

0
vendor/aws/aws-sdk-php/src/Api/Parser/QueryParser.php vendored Normal file → Executable file
View File

Some files were not shown because too many files have changed in this diff Show More