Compare commits

..

69 Commits

Author SHA1 Message Date
45a0d84caa - 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).
2026-04-28 10:15:09 -06:00
7942f6b50c corregido el historial de fichas de oxxo para los clientes 2026-04-07 13:36:52 -06:00
d680132d3e referencias stripe corregidas 2026-04-06 13:58:11 -06:00
01f42e07d7 Se ajustó el campo de RESUMEN que se sincroniza con callbell par redicir los caracteres a menos de 500 porque se estaban rechazando por la API de callbell 2026-03-30 13:34:50 -06:00
5c2ab3de74 antes de la refactorización 2026-03-22 11:14:05 -06:00
9a43d68958 fix del botón de cierre de sesión implementado 2026-03-18 17:43:27 -06:00
d2c6a30ee4 reeemplazo de archivos desde el servidor de producción 2026-03-18 15:43:05 -06:00
e113c750b9 antes del cambio de color de los botones 2026-03-10 17:42:34 -06:00
d0430dd891 antes de la implementación del reenvio de notificaciones a instaladores 2026-03-10 14:01:07 -06:00
8d4580e138 antes del reemplazo de archivos 2026-03-09 13:35:16 -06:00
3cddcb548e Botón de cerrar sesión implementado 2026-02-20 13:58:18 -06:00
caaa195fba Botón de modo oscuro en el login listo 2026-02-20 13:08:01 -06:00
be8b0d0c38 login realizado 2026-02-19 12:28:15 -06:00
c819bb5aab antes del login 2026-02-19 12:15:23 -06:00
b0ba48d111 antes del relleno de site y sectorial como custom attributes estáticos 2026-02-18 17:45:50 -06:00
28f700e64c antes de la etiqueta OBTENER PASSWORD ANTENA 2026-02-18 17:29:41 -06:00
1d562cad5a Antes de acomodar la agrupación 2026-02-12 15:44:42 -06:00
c989d180fa Falta agrupar por personas encargadas de comunidad el corte 2026-02-12 15:09:19 -06:00
12bb5c04a4 se eliminó el buscador en el gestor de instaladores 2026-02-12 12:48:41 -06:00
fc9fcca6c0 arreglados los contenedores, falta quitar el buscador de instaladores y arreglar los botones de ese crud 2026-02-12 12:42:38 -06:00
bd313fdfa4 Antes del acomodo de contenedores para los tabs 2026-02-12 12:17:11 -06:00
aa42284cc5 saldo stripe conseguido 2026-02-12 08:13:26 -06:00
28b4324406 skill instalada 2026-02-11 12:54:00 -06:00
5dc9f87618 modificación del diseño del módulo SPEI 2026-02-11 11:50:03 -06:00
e9b440810c iconos arreglados 2026-02-11 11:28:12 -06:00
852c9f1758 comprobantes ya se generan con oxxo pago y transferencia bancaria 2026-02-02 02:22:54 -06:00
24c32f6334 corregido doble payment intent y reenvio de comprobantes 2026-01-31 11:44:23 -06:00
d2ce14a7e3 antes del flujo de cambiar los métodos de pago. 2026-01-30 12:25:03 -06:00
2926211060 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)
2026-01-18 18:22:00 -06:00
84a25a829c Feat(Plugin): Implementación Microservicio PDF, Integración MinIO, Fix Recorte y Overlay de Pagos
Descripción:

Se ha realizado una refactorización mayor del sistema de notificaciones y generación de comprobantes para eliminar dependencias legacy (Imagick/FTP) y modernizar la arquitectura.

🚀 Nuevas Características (Features)
Microservicio PDF (pdf-cropper):

Nuevo servicio en Python (FastAPI + Poppler) desplegado en Docker (Puerto 8050) para la conversión de PDF a Imagen.
Reemplaza la librería Imagick de PHP, aislando el procesamiento pesado fuera del plugin.
Modo Full: Garantiza la conversión del PDF completo respetando su relación de aspecto original (sin recortes ni "square crop").
Overlay de Texto: Capacidad nativa en el backend para sobreescribir texto dinámicamente (ej. cambiar "Tarjeta de crédito Stripe" por "OXXO Pay") basado en parámetros.
Integración S3 / MinIO:

Implementación de
MinioStorageService
.
Los comprobantes (JPG) y Vouchers OXXO se suben automáticamente a MinIO en lugar de usar FTP.
Generación de URLs públicas seguras.
🧹 Mantenimiento y Limpieza (Chores/Refactor)
Limpieza Automática (Cleanup):

Se implementó lógica para eliminar archivos temporales locales (PDFs descargados, JPGs generados) inmediatamente después de una subida exitosa a MinIO.
Aplica tanto para comprobantes de pago como para Vouchers OXXO.
Refactor
ClientCallBellAPI
:

Eliminación de código muerto relacionado con FTP de WordPress.
Implementación de cliente HTTP Guzzle para comunicación con pdf-cropper.
Cache Busting: Se añade Timestamp al nombre de archivo (_time()) para evitar caché agresivo en WhatsApp/Navegadores.
Detección de Método: Lógica preliminar para detectar "OXXO" o "Transferencia" en notas de pago y aplicar Overlay.
Docker Infraestructura:

Actualización de
docker-compose.yml
 para incluir pdf-cropper.
Instalación de fuentes (fonts-dejavu-core) para renderizado correcto de texto.
🐛 Correcciones (Bug Fixes)
Fix Recorte de Imagen: Se eliminó la restricción de relación de aspecto 1:1 (cuadrado) que cortaba headers y footers en documentos rectangulares.
Standardización de Notas: Actualización de
AbstractStripeOperationsFacade
 para incluir explícitamente "OXXO" o "Transferencia" en las notas de pago de UCRM (preparación para lógica de plantillas).
2026-01-14 22:56:17 -06:00
e1937f9ac3 feat: integrate Stripe payments and UI refinements into WhatsApp Dashboard
- Migrated PaymentIntentService with plugin-specific namespacing.
- Added AJAX endpoints for client searching and SPEI reference generation.
- Implemented global premium header with SIIP Blue gradient.
- Optimized sidebar UI: increased logo size to 180px and nav icons/text.
- Improved Dark Mode accessibility with high-contrast primary colors.
- Added "View in CRM" functionality and automated Stripe admin logic.
- Fixed layout nesting issues and polished CSS layering.
2026-01-08 15:30:07 -06:00
f851ea6d7d feat: soporte multi-servicio, optimización lazy loading y sincronización avanzada con CallBell
- Implementación de gestión multi-servicio para contraseñas de antena con etiquetas condicionales.
- Optimización de rendimiento mediante lazy loading para evitar llamadas redundantes a la API de UISP.
- Mejora de sincronización con CallBell: contraseñas en formato JSON, unificación de peticiones PATCH y corrección de comparación de saldo.
- Generación de contraseñas printer-friendly (alfanuméricas + @, #) sin caracteres ambiguos.
- Validación granular de provisionamiento para evitar errores en sitios inactivos.
- Actualización de CHANGELOG.md y README.md.
2026-01-03 11:07:29 -06:00
761cd667b5 feat: soporte multi-servicio, optimización de recursos y refinamiento de contraseñas
- Implementado soporte multi-servicio para antenas con formato "Servicio X: <pass>".
- Añadido sistema de "Lazy Check" para evitar llamadas redundantes a la API de UISP si ya existe una contraseña válida.
- Refinado algoritmo de generación de contraseñas: ahora son "Printer-Friendly" (alfanumérico + @, #) y sin caracteres ambiguos.
- Mejorada la lógica de detección de antenas: validación por etapas (Servicio -> Sitio -> Dispositivo) para evitar errores en sitios inactivas.
- Agregados mensajes informativos de estado en el CRM (⚠️ Sin sitio, ⚠️ Sin antena).
- Corregido bucle infinito de webhooks en entornos de prueba mediante validación de idempotencia.
- Actualizada documentación (README.md y CHANGELOG.md) a la versión 3.0.0.
2026-01-02 22:41:54 -06:00
b0b56a59ce feat: Dashboard administrativo, integración con API UCRM y optimización de Stripe
- Implementación de Dashboard profesional en public.php con CRUD de instaladores.
- Sincronización con la API de UCRM para selección y validación automática de administradores.
- Soporte para creación selectiva de clientes en Stripe (Etiqueta "CREAR CLIENTE STRIPE" vs "CREAR CLABE STRIPE").
- Mejora en la lógica de notificaciones de tareas (nuevo prefijo [CLIENTE-SIN-WHATSAPP]).
- Refactorización integral de fachadas y saneamiento de código muerto.
- Sistema de Modo Oscuro persistente con UI refinada.
2025-12-27 23:15:32 -06:00
5425659428 antes de la limpieza y refacorización del código 2025-12-24 02:03:41 -06:00
0e37fd153f feat(stripe-sync): implementar resolución dinámica de métodos de pago y corregir validación API
- Se agregó 'getPaymentMethodIdByName' para buscar automáticamente el ID de "Transferencia bancaria" por nombre, asegurando portabilidad entre servidores UISP.
- Se implementó el manejo del webhook 'customer_cash_balance_transaction.created' para el registro automático de pagos fondeados.
- Fix: Se corrigió error 422 en la API de UCRM forzando el cast de 'clientId' a integer y 'methodId' a string (GUID).
- Se actualizó la documentación (README/CHANGELOG) con instrucciones de configuración de webhooks.
2025-12-23 14:50:57 -06:00
506615e911 Se agrega nuevo campo en los ajustes del plugin para agregar la API KEY del UNMS con el ID 'unmsApiToken' 2025-10-20 15:13:21 -06:00
2e21e09c2c Corrección de bug para poder utilizar la dirección ip del servicio Puppeteer dentro del código para referencias de OXXO PAGO, también se agregó un campo de configuración para el servicio de Puppeteer así como el código para utilizar dicho campo. 2025-08-01 08:47:33 -06:00
a0f286969a Actualización que ajusta los permisos de acceso a la API del CRM y NMS, además se realizaron cambios en el menú de ajustes para agregar el campo del toke de la api de nms y el campo del id del administrador para pagos en linea con Stripe, además se hizo el ajuste para el bug que no permitía realizar intenciones de pago cuando la cantidad trae signo de pesos 2025-07-31 15:23:49 -06:00
c9e2466353 Solucionado el bug que impedía regresar en la respuesta el monto total hacia el bot de CallBell, lo que hacía que mostrara siempre Cantidad cero, esto se realizó en el archivo AbstractOxxoOperationsFacade.php 2025-06-11 11:07:59 -06:00
8d8a1ec648 Modificaciones para enviar referencias de OXXO PAGO por medio de imagen 2025-06-10 02:46:31 -06:00
c73a51bbf2 Se modificó la información para el envío de notificaciones a llos instaladores en el flujo de trabajo para la desasignación de tareas que hacía que no se viera correctamente la información 2025-06-05 12:01:03 -06:00
5374054289 Cambios para quitar la hora de los mensajes que se envían a los clientes, corrección en la función getVaultCredentials para poder enviar el mensaje con la contraseña al instalador 2025-06-02 17:45:28 -06:00
e899945ca3 Versión 2.8.2 Rutina de envio de mensajes con plantillas de utilidad refactorizado, además tipo de pago 'applied_to_payment' en en analisis de webhooks de Stripe 2025-05-27 15:57:43 -06:00
e34b3ec0f8 Versión 2.8.0 README ACTUALIZADO 2025-05-21 21:39:50 -06:00
3a39a53da6 Versión 2.8.0 2025-05-21 21:31:09 -06:00
7cb26fe735 Se agregó una función que permite recuperar una contraseña de antena en función del ID del cliente y enviarla por mensaje de notificación al instalador para tareas que se le asignen, además de modificaron los IDs de plantillas de mensajes y se eleiminó el campo de dirección para la notioficación de tarea o servicio del instalador. 2025-05-10 11:21:43 -06:00
9a3af1f3d4 Version 2.7.1 con la opción de generar clabes interbancarias por medio de una etiqueta con nombre 'CREARCLABESTRIPE' y además permite crear las clabes de manera automática cuando el cliente pasa de potencial a cliente regular, se corrige un error que permitía generar de manera indefinida clabes para un cliente, además también se agregó la eliminación de la etiqueta 'CREARCLABESTRIPE' de manera automática cuando termina de generar una clabe interbancaria y cliente de Sripe y también valida si ya tiene uno creado con anterioridad para descartar solicitudes hechas a clientes que ya posean dichos datos 2025-03-25 07:46:08 -06:00
96d7fe6b27 Version 2.7.0 con la opción de generar clabes interbancarias por medio de una etiqueta con nombre 'CREARCLABESTRIPE' y además permite crear las clabes de manera automática cuando el cliente pasa de potencial a cliente regular 2025-03-24 22:47:43 -06:00
9db9febc39 Versión 2.6.9.1 con modificaciones para devolver mas campos en la respuesta a una creación de referencia de OXXO, como tal son los campos de oxxo_reference, error, failDescription, clientID, clientFullName, amount como número entero 2025-03-08 08:13:48 +00:00
59aac17968 Versión 2.6.9 con modificaciones para devolver mas campos en la respuesta a una creación de referencia de OXXO, como tal son los campos de oxxo_reference, error, failDescription, clientID, clientFullName, amount 2025-03-08 00:09:06 +00:00
2819804ad8 HotFix para manejo de excepciones de timeout para referencias de oxxo, ahora se envian las claves 'error' y 'url' en el mismo cuerpo de la respuesta, también se mejoró el código del método para crear la referencia de oxxo y el de edición del cliente para agregar los campos personalizados de Site y Antena/Sectorial 2025-03-05 05:33:07 +00:00
4276097212 HotFix para manejo de excepciones de timeout para referencias de oxxo y otros errores versión 2 con ajuste de codigos 2025-03-04 10:04:00 +00:00
cd74cafeda HotFix para manejo de excepciones de timeout para referencias de oxxo y otros errores 2025-03-04 09:32:03 +00:00
398dcc462f Adición de los eventos: [invoice.draft_approved, invoice.add_draft] para sincronizar el saldo del cliente a CallBell así como del evento service.edit 2025-02-18 23:27:27 +00:00
c3824ba827 Hot fix para poder enviar notoficaciones a mas de un contacto de tipo whatsapp en un mismo cliente, verificacion para la sincroniazacion de clientes hacia callbell cuando se agrega una factura o se edita ademas s ajustó para que solo se pueda editar el email en stripe cuando se haga una edición en el CRM 2025-02-18 10:42:18 +00:00
e1e3f409c9 Hot fix para arreglar el error que no permitía generar pagos por referencia 2025-02-06 23:48:51 +00:00
eafc867742 Versión 2.6.3 HOT FIX que arregla el bug que impedía hacer referencias de oxxo porque se duplicaba la multiplicación del factor 100 para la conversión de centavos 2025-02-02 22:42:29 +00:00
12d19c46de Versión 2.6.2 correción de la rutina de envio de pdf 2025-01-27 09:29:45 +00:00
9fc3bcb8d9 Versión 2.6.1 ajuste de la hora, negritas en el folio y el envio correcto de desasignacion de servicio 2025-01-25 16:42:25 +00:00
8e5b144796 Versión 2.6.1 ajuste de la hora, negritas en el folio y el envio correcto de desasignacion de servicio 2025-01-25 16:36:47 +00:00
869331b02c Versión 2.6.0 completa 2025-01-24 16:07:10 +00:00
11b7d7cacc corrección de referencias oxxo con linea return y rutina para ir evaluando los jobs 2025-01-18 03:52:06 +00:00
efd6f36c37 Rework para clientes pontenciales nuevos, eliminacion del tiempo de espera para intentos de creacion de referencias de oxxo 2025-01-14 09:10:39 +00:00
93a0f30eec Hot fix para nuevos clientes potenciales, se evitara hacer un Stripe customer ID, se ajuto el email del cliente por defecto para crear paymenIntent para pagos de Oxxo 2025-01-07 23:14:34 +00:00
3b3cd70ece Cambios para validar cantidad, mas logs para errores y poner email por defecto al cliente, manifest 2025-01-05 19:43:42 +00:00
f7119a4512 Cambios para validar cantidad, mas logs para errores y poner email por defecto al cliente 2025-01-05 19:39:57 +00:00
9e78efdf2f Cambios para procesar referencias oxxo con hasta 5 intentos 2025-01-04 08:36:11 +00:00
1731eb1813 Cambios en el comprobante de pago, imagen sin recorte 2025-01-03 21:36:27 +00:00
7741 changed files with 530645 additions and 71947 deletions

View File

@ -0,0 +1,45 @@
---
name: frontend-design
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
license: Complete terms in LICENSE.txt
---
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
## Design Thinking
Before coding, understand the context and commit to a BOLD aesthetic direction:
- **Purpose**: What problem does this interface solve? Who uses it?
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
- **Constraints**: Technical requirements (framework, performance, accessibility).
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
- Production-grade and functional
- Visually striking and memorable
- Cohesive with a clear aesthetic point-of-view
- Meticulously refined in every detail
## Frontend Aesthetics Guidelines
Focus on:
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.

16
.gitignore vendored Executable file
View File

@ -0,0 +1,16 @@
*.pdf
*.log
*.png
*.jpeg
.vscode/
*.zip
*.csv
*.apib
*.jrxml
*.jasper
vouchers_oxxo/
comprobantes/
pack-plugin-versioned.php
*.txt
Callbell Public API.postman_collection.json
unms-swagger.json

5
.vscode/extensions.json vendored Executable file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"github.copilot-chat"
]
}

69
.vscode/launch.json vendored Executable file
View File

@ -0,0 +1,69 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 0,
"runtimeArgs": [
"-dxdebug.start_with_request=yes"
],
"env": {
"XDEBUG_MODE": "debug,develop",
"XDEBUG_CONFIG": "client_port=${port}"
}
},
{
"name": "Launch Built-in web server",
"type": "php",
"request": "launch",
"runtimeArgs": [
"-dxdebug.mode=debug",
"-dxdebug.start_with_request=yes",
"-S",
"localhost:0"
],
"program": "",
"cwd": "${workspaceRoot}",
"port": 9003,
"serverReadyAction": {
"pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
"uriFormat": "http://localhost:%s",
"action": "openExternally"
}
},
{
"name": "Launch built-in server and Debug",
"type": "php",
"request": "launch",
"noDebug": false,
"runtimeArgs": [
"-S",
"localhost:8000",
"-t",
"."
],
"cwd": "${workspaceRoot}/.",
"serverReadyAction": {
"action": "openExternally"
}
},
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch"
}
]
}

321
CHANGELOG.md Executable file
View File

@ -0,0 +1,321 @@
# 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
### ⚠️ Requisito Técnico (Update)
- **PHP**: Se requiere **PHP 8.1 o superior** para soportar las nuevas dependencias de AWS. (Habilitado en servidor de pruebas).
### ✨ Nuevas Características (Features)
1**Gestión de Instaladores**: Módulo para visualizar tareas en curso y realizar el reenvío manual de notificaciones de asignación.
### 🐛 Correcciones (Bug Fixes)
1**Fix Notificaciones**: Corregido bug que enviaba mensajes de desasignación al cambiar de técnico en tareas activas.
## VERSIÓN 4.4.0 - 10-03-2026
### ✨ Nuevas Características (Features)
1**Reenvío manual de notificaciones de tareas**: Nuevo módulo en la sección "Gestión de Instaladores" que permite ver las tareas activas ("En curso") de cada técnico y reenviar manualmente la notificación WhatsApp de asignación con un solo clic.
- **Nuevo endpoint `GET ?action=get_installer_jobs`**: Consulta la API de UCRM para obtener los jobs activos (`status=1`) filtrados por instalador, enriquecidos con nombre del cliente.
- **Nuevo endpoint `POST action=resend_job_notification`**: Simula un webhook `job.edit` (status 0→1) vía loopback curl para disparar el flujo completo de notificación.
- **Tabla interactiva**: Card "📋 Tareas Activas del Instalador" con columnas Folio, Cliente, Fecha, Descripción y botón 📨 Reenviar.
- **Icono 📋 en tabla de instaladores**: Permite cargar las tareas activas de cualquier técnico con un clic.
## VERSIÓN 4.3.1 - 10-03-2026
### 🐛 Correcciones (Bug Fixes)
1**Fix Notificación de Cambio de Instalador**: Corregido bug crítico en `AbstractMessageNotifierFacade.php` donde al cambiar de técnico en una tarea **"En curso"**, el **nuevo** instalador recibía el mensaje de **desasignación** (❌ "se te ha desasignado la tarea...") en lugar de un mensaje de **asignación** con los datos del cliente.
**Causa raíz**: La bandera `$changeInstaller=true` se pasaba tanto a la notificación del técnico anterior como a la del nuevo, causando que ambos recibieran la plantilla de desasignación. El fix envía `false` al nuevo técnico para que use la plantilla de asignación normal.
## VERSIÓN 4.3.0 - 23-02-2026
### 🔐 Seguridad y Acceso (Login)
1**Sistema de Login Integrado**:
_ Implementada pantalla de inicio de sesión "Premium" para acceso externo
_ Validación estricta con la API de UISP (endpoint `/user/login`)
_ Soporte completo para autenticación de dos factores (2FA / TOTP)
_ Control de roles: Acceso restringido exclusivamente a administradores y súper administradores
\_ Manejo de estado de sesiones con `sessionStorage` y validación híbrida PHP/JS
2**Protección de Rutas (Proxy de Sesión)**:
_ Nuevo sistema PHP para detectar el contexto de la URL (Pública vs Privada CRM)
_ Auto-login transparente si existe una sesión válida en el navegador desde el portal UCRM
\_ Botón condicional de "Cerrar Sesión" exclusivo para accesos desde URL pública
### 💎 Rediseño Premium de Interfaz (UI/UX)
1**Autenticación Premium (Stitch Glassmorphism)**:
_ Pantalla de login elegante usando tendencias de Glassmorphism (`blur`, transparencias RGBA, bordes sutiles iluminados)
_ Partículas flotantes decorativas con efecto desenfoque radial en el fondo oscuro
_ Inputs "Glow" reactivos y botones con gradientes vibrantes y sombra perimetral flotante
_ Interruptor ('Toggle Switch') animado con física de rebote para alternar entre modo Claro y Oscuro
\_ Ocultamiento de textos truncados ("Modo Osc...") en favor de una experiencia más visual y limpia con íconos deslizables
## VERSIÓN 4.2.2 - 10-02-2026
### ✨ Nuevas Características (Features)
1**Sistema de Navegación por Pestañas**:
_ Implementado sistema de pestañas horizontal similar a siip-payments-tools
_ Permite cambiar entre módulos sin volver al dashboard principal
_ Pestañas sticky que permanecen fijas al hacer scroll
_ Navegación fluida: Instaladores ↔ Notificaciones ↔ Pagos SPEI ↔ Pagos OXXO
_ Mejora significativa en la UX de navegación
_ Inicialización inteligente de módulos (carga solo cuando se accede)
### 🎨 Mejoras Visuales
- Pestañas con iconos representativos de cada módulo
- Estilo consistente con el diseño del plugin siip-payments-tools
- Adaptación completa a modos claro y oscuro
- Animaciones suaves en hover y transiciones de pestañas
## VERSIÓN 4.2.1 - 10-02-2026
### 🎨 Mejoras Visuales (Visual Enhancements)
1**Footer Sticky (Pegado al Fondo)**:
_ Implementado sistema Flexbox en body y container
_ El footer ahora siempre se posiciona al fondo de la página \* Resuelve problema de footer flotante en módulos con poco contenido
2**Footer con Versión Dinámica**:
_ Agregado footer profesional con branding SIIP Internet
_ Copyright dinámico con año actual usando PHP
_ Visualización clara de la versión del plugin (4.2.1)
_ Border superior prominente con color primario
### 🔄 Consistencia Visual
- Alineado con mejoras visuales del plugin `siip-payments-tools` v2.2.11
- Mantenida coherencia en diseño entre todos los plugins SIIP
### 📝 Nota
- Las esquinas redondeadas en imágenes del menú se consideraron pero se revirtieron
debido a que los logos rectangulares (Stripe, OXXO) se distorsionaban con `object-fit: cover`
## 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
### 🚀 Nuevas Características (Features)
1. **Microservicio PDF (`pdf-cropper`)**:
- Nuevo servicio en Python (FastAPI + Poppler) desplegado en Docker (Puerto 8050) para la conversión de PDF a Imagen.
- Reemplaza la librería `Imagick` de PHP, aislando el procesamiento pesado fuera del plugin.
- **Modo Full**: Garantiza la conversión del PDF completo respetando su relación de aspecto original (sin recortes ni "square crop").
- **Overlay de Texto**: Capacidad nativa en el backend para sobreescribir texto dinámicamente (ej. cambiar "Tarjeta de crédito Stripe" por "OXXO Pay") basado en parámetros.
2. **Integración S3 / MinIO**:
- Implementación de `MinioStorageService`.
- Los comprobantes (JPG) y Vouchers OXXO se suben automáticamente a MinIO en lugar de usar FTP.
- Generación de URLs públicas seguras.
### 🧹 Mantenimiento y Limpieza (Chores/Refactor)
1. **Limpieza Automática (Cleanup)**:
- Se implementó lógica para eliminar archivos temporales locales (PDFs descargados, JPGs generados) inmediatamente después de una subida exitosa a MinIO.
2. **Refactor `ClientCallBellAPI`**:
- Eliminación de código muerto relacionado con FTP de WordPress.
- Implementación de cliente HTTP Guzzle para comunicación con `pdf-cropper`.
- **Cache Busting**: Se añade Timestamp al nombre de archivo (`_time()`) para evitar caché agresivo en WhatsApp/Navegadores.
### 🐛 Correcciones (Bug Fixes)
1. **Fix Recorte de Imagen**: Se eliminó la restricción de relación de aspecto 1:1 (cuadrado) para evitar recortes.
2. **Standardización de Notas**: Actualización de `AbstractStripeOperationsFacade` para incluir explícitamente "OXXO" o "Transferencia" en las notas de pago.
## VERSIÓN 4.0.0 - 10-01-2026
### 🟢 Novedades (Re-diseño UI/UX + Integ. Pagos)
1**Portal Administrativo Integrado (Stripe + Oxxo)**: Se ha rediseñado completamente el panel administrativo (`Dashboard`) para incluir una sección dedicada a **Pagos Online**, permitiendo la generación de referencias SPEI y OXXO Pay sin salir del portal.
2**Visualización Inline de Vouchers OXXO**: Ahora los comprobantes de pago OXXO se generan y visualizan inmediatamente en una vista partida (Datos + Imagen) dentro del dashboard, eliminando la necesidad de abrir enlaces externos para verificación.
3**Carga FTP Híbrida**: Optimización inteligente que sube los vouchers al servidor FTP solo cuando es necesario (integraciones externas/webhooks) y utiliza visualización local para el administrador, garantizando máxima velocidad de interfaz.
4**Modo Oscuro & UI Premium**: Nueva capa visual con sidebar expandido, cabecera global "Sticky", y un sistema de colores adaptativo (Modo Sol/Luna) para una mejor experiencia de usuario.
5**Seguridad Reforzada**: Refactorización completa del manejo de credenciales, eliminando datos hardcodeados en el código y centralizándolos en la configuración segura del plugin.
### 🔵 Mejoras
1**Navegación Intuitiva**: Menú lateral rediseñado con accesos directos claros a "Pagos Stripe", "Notificaciones" y "Gestión de Instaladores".
2**Feedback Visual**: Nuevas alertas toast y modales informativos para confirmar acciones (Generación de referencias, errores de API, etc.).
3**Compatibilidad**: Ajustes en `manifest.json` y estructura de archivos para asegurar compatibilidad total con las últimas versiones de UISP.
## VERSIÓN 3.1.0 - 07-01-2026
### 🟢 Novedades
1**Re-envío Manual de Notificaciones de Pago**: Se añadió un nuevo apartado en el Dashboard que permite buscar clientes y re-disparar notificaciones de WhatsApp para pagos específicos de forma manual.
2**Buscador de Clientes en Dashboard**: Integración de buscador dinámico para localizar clientes y visualizar su historial de los últimos 10 pagos.
## VERSIÓN 3.0.0 - 02-01-2026
### 🟢 Novedades
1**Soporte Multi-Servicio para Antenas**: Ahora el plugin gestiona múltiples servicios por cliente, mostrando cada contraseña con el formato `Servicio 1: <pass> Servicio 2: <pass> ...`.
2**Validación Granular de Provisionamiento**: Se implementó una lógica de detección por etapas (Servicio -> Sitio UISP -> Dispositivo) para evitar generar contraseñas en sitios "Location Inactive".
3**Lazy Loading & Optimización de Recursos**: Implementación de un "Lazy Check" que detecta si ya hay una contraseña válida en el CRM para omitir llamadas innecesarias a la API de UISP, mejorando la velocidad y reduciendo el consumo de CPU.
### 🔵 Mejoras
1**Etiquetado Inteligente de Servicios**: Las etiquetas `Servicio 1:`, `Servicio 2:` ahora solo aparecen si el cliente tiene múltiples servicios; para un solo servicio, la contraseña se muestra directamente.
2**Sincronización Avanzada con CallBell**: - Nuevo campo `Password Antena` enviado en formato JSON estructurado. - Unificación de peticiones PATCH (Resumen + Campos) en una sola llamada para mayor eficiencia.
3**Contraseñas "Printer-Friendly"**: El generador de contraseñas ahora utiliza un set de caracteres optimizado para mini-impresoras térmicas (Alfanumérico + `@`, `#`), eliminando caracteres ambiguos como `l`, `I`, `0`, `O`.
4**Mensajes de Estado en CRM**: Se agregaron alertas visuales en el campo de contraseña para indicar estados de provisión: `⚠️ Sin sitio vinculado`, `⚠️ Sin antena vinculada`, `⚠️ Cliente sin servicios`.
5**Robustez en Entornos de Prueba**: Refinamiento del bypass de desarrollo para mantener la estabilidad de las claves generadas y evitar bucles infinitos de webhooks.
### 🟡 Bugs Resueltos
1**Sincronización de Saldo**: Se corrigió la discrepancia de nombres entre `Saldo Actual` y `Saldo` que causaba actualizaciones redundantes infinitas con CallBell.
2**Parseo de Passwords**: Refinamiento de expresiones regulares para capturar correctamente contraseñas multi-servicio.
3⃣ Se solucionó el bucle infinito de actualizaciones en el atributo `passwordAntenaCliente` que ocurría al detectar cambios en servicios sin dispositivos vinculados.
## VERSIÓN 2.9.3 - 23-12-2025
### 🟢 Novedades
1⃣ Resolución dinámica del ID del método de pago ("Transferencia bancaria") mediante consulta a la API de UISP, mejorando la portabilidad del plugin entre distintos servidores.
2⃣ Implementación de registro de pago automático desde Webhook Stripe para eventos de tipo `customer_cash_balance_transaction.created` (Saldo aplicado).
### 🟡 Bugs Resueltos
1⃣ Se corrigió el error de validación de la API de UCRM (422) mediante el cast explícito de `clientId` a integer y el uso de `methodId` como string.
## VERSIÓN 2.9.2
### 🟡 Bugs Resueltos
1⃣ Se solucionó un bug que impedía obtener la contraseñas de la bóveda, ya que el response de la API cambió en la última actualización y la esstructura nueva impedía acceder al dato del password
## VERSIÓN 2.8.8
### 🟡 Bugs Resueltos
1⃣ Se solucionó un bug que impedía al BOT del CallBell mostrar el monto de la referencia de OXXO en el mensaje donde se le entrega el voucher al cliente, para el caso donde el cliente elegía crear su referencia con la CANTIDAD TOTAL.
## VERSIÓN 2.8.7
### 🟢 Novedades
1⃣ Ahora las referencias de **OXXO Pago** han cambiado, en lugar de enviarse la URL o link de pago al cliente será la imagen del código de barras y la información que aparece en el link directamente en el mensaje, de esta manera será más cómodo para el cliente tener la imagen en su chat a tener que abrir una URL o link externo.
### 🔵 Mejoras
1⃣ Se modificaron nodos del bot **_OXXO_BOT_** para poder adaptar esta actualización correctamente.
2⃣ Mejoras en el código fuente del flujo de trabajo para las referencias de OXXO PAGO.
## VERSIÓN 2.8.6
### 🔵 Mejoras
1⃣ Se modificó la información para el envío de notificaciones a llos instaladores en el flujo de trabajo para la desasignación de tareas que hacía que no se viera correctamente la información
## VERSIÓN 2.8.5
### 🔵 Mejoras
1⃣ Se modificó la información para el envío de notificaciones a los clientes para su visita técnica: ahora ya no se envían las horas en que serán las visitas, SOLO LA FECHA.
2⃣ Se crearon nuevas plantillas de tipo utilidad y se adaptaron al flujo de trabajo en el código.
### 🟡 Bugs Resueltos
1⃣ Se soluciono el bug que impedía enviar notificaciones a los instaladores para nuevas tareas.
## VERSIÓN 2.8.2
### 🟡 Bugs Resueltos
1⃣ No se enviaban las notificaciones de las tareas al instalador. Se cambió la plantilla de CallBell o WhatsApp con 3 variables en lugar de 8.
2⃣ Se agregó un nuevo tipo de pago ("applied_to_payment") en las propiedades de los Webhooks recibidos mediante Stripe por concepto de transferencias bancarias. Ya que sólo se revisaba el tipo de pago "funded" y eso hacía que no enviara los comprobantes de pago a los clientes para todos los casos.
## VERSIÓN 2.8.1
### 🟡 Bugs Resueltos
1⃣ No se enviaban las notificaciones de las tareas al instalador.
## VERSIÓN 2.8.0
### 🟢 Novedades
1⃣ Envío de contraseña de antena en el mensaje que se manda al instalador cuando se le asigna una tarea/servicio.
2⃣ Para clientes nuevos o que no tengan el campo personalizado de “Password Antena Cliente” al actualizarlos se les asignará ese campo o cuando se les actualice su servicio.
3⃣ Ahora se puede modificar tantas veces sea necesaria una tarea o servicio como su fecha o el instalador mientras la tarea permanezca en estado “Abiertos” SIN que se envíen notificaciones a los clientes o instaladores.
4⃣ Se agregó un prefijo al título de la tarea/servicio agendado cuando recién se da de alta para identificar que esta no ha sido notificada aún y se pueden realizar ajustes.
### 🔵 Mejoras
1⃣ Se modificó el flujo de trabajo para el envío de notificaciones a los clientes para su visita técnica, ahora son cuatro distintos tipos de flujo, anteriormente tres.
2⃣ Se reemplazaron algunas plantillas de mensajes de CallBell.
3⃣ Se renombró el uso de la etiqueta “CREARCLABESTRIPE” por “CREAR CLABE STRIPE” para un mejor entendimiento de su uso.
### 🟡 Bugs Resueltos
1⃣ Envío de notificaciones a los clientes y a los instaladores al CERRAR una tarea/servicio (Se mandaban mensajes de asignación y de visita del técnico ) haciendo no posible cerrar las tareas.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,88 @@
📢 Buenos días equipo, Mega Actualización del Plugin Notificaciones y Pagos WhatsApp SIIP 🚀 *Versiones 3.0.0 hasta 4.3.0*
Se ha completado una de las actualizaciones tecnológicas más grandes e importantes en la historia del sistema central de notificaciones y cobranza. Aquí se detallan todos los cambios monumentales:
━━━━━━━━━━━━━━━━━━━━━━
💻 *1. El Nuevo Portal Administrativo SIIP*
Se integró un portal web completo y dedicado directamente dentro del CRM. Este portal centraliza toda la operación en 4 módulos principales:
• 👷‍♂️ *Módulo de Instaladores*: Permite gestionar y vincular a los técnicos e instaladores de la empresa para el enrutamiento de notificaciones.
• ✉️ *Módulo de Notificaciones y Comprobantes*: Brinda un historial de los pagos de cada cliente, con la función (altamente solicitada) para **reenviar manualmente los comprobantes de pago** por WhatsApp con un solo clic.
• 💳 *Módulo de Cobranza con Tarjetas (Stripe)*: Permite realizar cargos a tarjetas de crédito/débito en tiempo real o generar enlaces de pago, además de visualizar el historial financiero de los clientes.
• 🏪 *Módulo de Cobranza en Efectivo (Oxxo)*: Desde aquí se generan referencias de Oxxo Pay al instante para que el cliente pague en tienda, llevando un control exacto de las referencias emitidas.
━━━━━━━━━━━━━━━━━━━━━━
🤖 *2. El Nuevo Cerebro de WhatsApp (Callbell)*
• 📱 *Nuevo Bot de Callbell*: Se migró toda la operación a un nuevo bot construido desde cero. Esto permite un envío de mensajes más estable y con mucha mayor capacidad de respuesta.
• ⚡ *Multiservicio*: Soporte ampliado para procesar múltiples servicios simultáneos sin cuellos de botella.
━━━━━━━━━━━━━━━━━━━━━━
🧾 *3. La Revolución de Oxxo Pay y Comprobantes*
• 🛒 *Flujo por Órdenes y Base de Datos*: Se cambió la forma en que el sistema de Oxxo Pay funciona. Ahora trabaja bajo un sistema de órdenes respaldado por base de datos, ¡eliminando por fin los fallos y cobros no reflejados por *timeout*!
• 🏭 *Nuevos Microservicios Dedicados*: Se extrajo la carga de trabajo pesada a un nuevo microservicio.
→ Ahora este se encarga de generar los vouchers de Oxxo y los comprobantes de pago.
→ Realiza el recorte automático (cropping) preciso de las imágenes.
→ Inserta dinámicamente el *nombre del cliente* en la imagen ("overlay"), dejándolos listos para enviar.
━━━━━━━━━━━━━━━━━━━━━━
☁️ *4. MinIO: La Nueva Nube Privada (Estilo AWS)*
• 🚀 *Adiós al FTP de WordPress*: Se ha dejado de depender de cargas lentas e inseguras por FTP hacia WordPress.
• 📦 *Almacenamiento MinIO*: Se implementó un sistema de almacenamiento de objetos idéntico a Amazon Web Services S3. Ahora todos los vouchers, comprobantes y PDFs se guardan de forma instantánea, segura y generan enlaces públicos a la velocidad de la luz.
━━━━━━━━━━━━━━━━━━━━━━
🎨 *5. Interfaz Gráfica y Experiencia (UI/UX)*
• 💎 *Diseño Premium (Glassmorphism)*: Renovación visual completa usando estética de cristal moderno en el nuevo Portal Administrativo.
• 🔐 *Sistema de Login Seguro*: Ya no cualquiera puede entrar; ahora el portal valida la sesión de manera segura directo con el core de UCRM (al igual que el Hub de Herramientas de Pagos).
• 👁️ *Vouchers Inline*: Ahora se pueden visualizar los vouchers generados directamente dentro de la interfaz del portal sin tener que descargarlos previamente.
• 🌙 *Modo Oscuro*: El portal ahora respeta la vista con un modo oscuro elegante y funcional para todo el equipo.
━━━━━━━━━━━━━━━━━━━━━━
📦 *Historial de Versiones Resumido (Desde v2.9.2)*
*v4.3.0* (Actual)
→ Sistema de Login con validación UCRM y Rediseño Premium Glassmorphism UI.
*v4.1.0*
→ Integración de Microservicio PDF y MinIO, Auto-recorte de comprobantes y textos dinámicos sobre imagen.
*v4.0.0*
→ Re-diseño UI con Nuevo Portal, integración Stripe (SPEI/OXXO), visualización inline y Optimización de seguridad FTP/Nube.
*v3.1.0*
→ Módulos Administrativos y Re-envío manual de notificaciones de pago.
*v3.0.0*
→ Soporte multiservicios y optimizaciones pesadas de rendimiento backend.
━━━━━━━━━━━━━━━━━━━━━━
🚀 *Cómo Acceder*
Al igual que siempre, lo encuentran integrado en el CRM en:
*Reportes → Portal Administrativo de Pagos de STRIPE y Notificaciones WhatsApp*.
━━━━━━━━━━━━━━━━━━━━━━
📝 *Notas Finales*
Esta no es solo una actualización visual, se cambió por completo el *motor interno* del plugin. Ahora el sistema opera con arquitectura de microservicios, cuenta con un portal administrativo completo y usa nubes privadas (MinIO) de nivel empresarial. Los fallos de timeout al cruzar cobros con Oxxo y las lentitudes de subir archivos por FTP pasaron a la historia.
Cualquier duda, favor de reportarla de inmediato para revisión.
*¡A sacarle provecho!* 💪
━━━━━━━━━━━━━━━━━━━━━━
_Desarrollado con ❤️ por SIIP Internet_
_Versión 4.3.0 - Marzo 2026_

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

298
README.md
View File

@ -1,212 +1,120 @@
# SMS notifier - Twilio # SIIP - WhatsApp Notifications & Integrated Payment Portal
* This plugin sends SMS notifications to clients. ![Version](https://img.shields.io/badge/version-4.6.0-blue.svg?style=for-the-badge)
* SMS is triggered by an event which happened in UCRM, e.g. new invoice has been issued, or client's sevice became suspended. ![UCRM Compatibility](https://img.shields.io/badge/UCRM-v2.1.0%2B-green.svg?style=for-the-badge)
* It only sends SMS to clients having a phone number set in their contacts details. ![Status](https://img.shields.io/badge/status-PRODUCTION-success.svg?style=for-the-badge)
* [Twilio](https://www.twilio.com/) account is required to access its API. ![Author](https://img.shields.io/badge/author-SIIP_INTERNET-orange.svg?style=for-the-badge)
## Configuration 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.
* Install the plugin into UCRM and enable it. I.e. download the plugin [zip file](https://github.com/Ubiquiti-App/UCRM-plugins/raw/master/plugins/sms-twilio/sms-twilio.zip) and upload it to UCRM in System > Plugins. ## ✨ Novedades v4.6.0 (Stripe & OXXO Stability)
* Keep execution period at "don't execute automatically" - the plugin will react to webhook events.
* Set up with data which you obtain from [Twilio Console](https://twilio.com/console):
* Account SID
* Auth Token
* SMS number to send from
Note: there are two sets of credentials available, the default ("LIVE credentials") for actual use and [test credentials](https://www.twilio.com/console/project/settings) for development. - **🛡️ 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.
* Customize the texts you wish to send to a client when an event happens ## ✨ Novedades v4.4.0 (Resend Job Notifications)
* Each event has its own row
* Empty row means "do not send SMS for this"
* It is possible to replace predefined variables: `%%some.variable%%`, see full list below
* If a variable is not set for a client, it is replaced with an empty string
* Save the configuration
* Enable the plugin
* Add webhook (button next to Public URL)
* Select events about which to notify clients and save the webhook endpoint
## Usage - **📋 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.
* In UCRM admin, go to System / Webhooks / Endpoints - **📨 Reenvío Manual de Notificaciones**: Botón para reenviar la notificación WhatsApp de asignación de tarea a cualquier instalador desde la interfaz del plugin.
* Click Test Endpoint - **🔗 Integración con API de Scheduling**: Consulta dinámica de `GET /scheduling/jobs?assignedUserId=X&statuses[]=1` para listar tareas activas en tiempo real.
* Go to System / Plugins / SMS notifications via Twilio
* In the log output, you'll see `Webhook test successful.`
## Variables replaced ## 🐛 Hotfix v4.3.1 (Installer Notification Fix)
These are loaded from UCRM API, and reflect the structure returned. - **🔧 Fix Cambio de Instalador**: Corregido bug donde el nuevo técnico recibía mensaje de desasignación en vez de asignación al cambiar instalador en una tarea "En curso".
Client variables are replaced always; payment invoice and service only with the applicable events.
### Client variables ## 🔐 Novedades v4.3.0 (Security & Premium UI)
* `%%client.id%%` => 20 - **🛡️ Sistema de Acceso Seguro**: Implementada validación híbrida (Server + Client). El plugin ahora protege las URLs públicas mediante una pantalla de inicio de sesión que requiere credenciales de Administrador de UCRM o autenticación 2FA.
* `%%client.userIdent%%` => '18' - **💎 Autenticación Premium (Glassmorphism)**: Nueva pantalla de inicio de sesión con una interfaz elegante y de lujo basada en Glassmorphism interactivo (esferas flotantes, desenfoques profundos, switch animado tipo iOS para modo oscuro).
* `%%client.previousIsp%%` => '' - **🚀 Single-Sign-On Ciego**: Si el administrador ya ingresó desde el portal interno UCRM, la capa de seguridad detecta la sesión local y le otorga acceso sin pedir credenciales, optimizando el flujo de trabajo.
* `%%client.isLead%%` => false
* `%%client.clientType%%` => 1
* `%%client.companyName%%` => ''
* `%%client.companyRegistrationNumber%%` => ''
* `%%client.companyTaxId%%` => ''
* `%%client.companyWebsite%%` => ''
* `%%client.street1%%` => '2544 Hillview Drive'
* `%%client.street2%%` => ''
* `%%client.city%%` => 'San Jose'
* `%%client.countryId%%` => 249
* `%%client.stateId%%` => 5
* `%%client.zipCode%%` => '95113'
* `%%client.invoiceStreet1%%` => ''
* `%%client.invoiceStreet2%%` => ''
* `%%client.invoiceCity%%` => ''
* `%%client.invoiceStateId%%` => ''
* `%%client.invoiceCountryId%%` => ''
* `%%client.invoiceZipCode%%` => ''
* `%%client.invoiceAddressSameAsContact%%` => true
* `%%client.note%%` => ''
* `%%client.sendInvoiceByPost%%` => false
* `%%client.invoiceMaturityDays%%` => 14
* `%%client.stopServiceDue%%` => true
* `%%client.stopServiceDueDays%%` => 7
* `%%client.organizationId%%` => 1
* `%%client.tax1Id%%` => 1
* `%%client.tax2Id%%` => ''
* `%%client.tax3Id%%` => ''
* `%%client.registrationDate%%` => '2016-04-26 00:00'
* `%%client.companyContactFirstName%%` => ''
* `%%client.companyContactLastName%%` => ''
* `%%client.isActive%%` => false
* `%%client.firstName%%` => 'Tyson'
* `%%client.lastName%%` => 'Doe'
* `%%client.username%%` => 'tyson.doe@example.com'
* `%%client.accountBalance%%` => 0
* `%%client.accountCredit%%` => 0
* `%%client.accountOutstanding%%` => 0
* `%%client.currencyCode%%` => 'USD'
* `%%client.organizationName%%` => 'UBNT ISP'
* `%%client.invitationEmailSentDate%%` => ''
* `%%client.avatarColor%%` => '#e53935'
* `%%client.addressGpsLat%%` => 37.401482000001
* `%%client.addressGpsLon%%` => -121.966545
* `%%client.message%%` => 'This is an example message sent from the Messaging feature.'
### Invoice variables ## 🚀 Novedades v4.2.0 (Analytics & Sync)
* `%%invoice.id%%` => 4
* `%%invoice.clientId%%` => 20
* `%%invoice.number%%` => '2016050002'
* `%%invoice.createdDate%%` => '2016-05-03 00:00'
* `%%invoice.dueDate%%` => '2016-05-17 00:00'
* `%%invoice.emailSentDate%%` => '2018-08-24 00:00'
* `%%invoice.maturityDays%%` => 14
* `%%invoice.notes%%` => ''
* `%%invoice.adminNotes%%` => ''
* `%%invoice.subtotal%%` => 7.88
* `%%invoice.discount%%` => ''
* `%%invoice.discountLabel%%` => ''
* `%%invoice.total%%` => 7.88
* `%%invoice.amountPaid%%` => 7.88
* `%%invoice.currencyCode%%` => 'USD'
* `%%invoice.status%%` => 3
* `%%invoice.invoiceTemplateId%%` => 1
* `%%invoice.organizationName%%` => 'UBNT ISP'
* `%%invoice.organizationRegistrationNumber%%` => ''
* `%%invoice.organizationTaxId%%` => ''
* `%%invoice.organizationStreet1%%` => '2580 Orchard Parkway'
* `%%invoice.organizationStreet2%%` => ''
* `%%invoice.organizationCity%%` => 'New York'
* `%%invoice.organizationStateId%%` => 1
* `%%invoice.organizationCountryId%%` => 249
* `%%invoice.organizationZipCode%%` => '10017'
* `%%invoice.organizationBankAccountName%%` => ''
* `%%invoice.organizationBankAccountField1%%` => ''
* `%%invoice.organizationBankAccountField2%%` => ''
* `%%invoice.clientFirstName%%` => 'Tyson'
* `%%invoice.clientLastName%%` => 'Doe'
* `%%invoice.clientCompanyName%%` => ''
* `%%invoice.clientCompanyRegistrationNumber%%` => ''
* `%%invoice.clientCompanyTaxId%%` => ''
* `%%invoice.clientStreet1%%` => '685 Third Avenue'
* `%%invoice.clientStreet2%%` => ''
* `%%invoice.clientCity%%` => 'New York'
* `%%invoice.clientCountryId%%` => 249
* `%%invoice.clientStateId%%` => 5
* `%%invoice.clientZipCode%%` => '10017'
* `%%invoice.uncollectible%%` => false
### Payment variables - **📊 Visualizador de Pagos Mensuales**: Nueva herramienta de análisis que permite seleccionar cualquier mes y visualizar gráficamente:
* `%%payment.id%%` => 28 - Estadísticas de clientes activos vs clientes que pagaron
* `%%payment.clientId%%` => 20 - Gráfica de dona interactiva con Chart.js
* `%%payment.invoiceId%%` => '' - Listado detallado de clientes pendientes con saldos
* `%%payment.method%%` => 2 - Porcentajes de cobranza en tiempo real
* `%%payment.checkNumber%%` => '' - **🔄 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.
* `%%payment.createdDate%%` => '2018-08-24 11:36' - **🎯 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.
* `%%payment.amount%%` => 1 - **⚙️ Configuración para Producción**: Sistema de `.env` implementado para gestión segura de credenciales de base de datos.
* `%%payment.currencyCode%%` => 'USD'
* `%%payment.note%%` => ''
* `%%payment.receiptSentDate%%` => ''
* `%%payment.providerName%%` => ''
* `%%payment.providerPaymentId%%` => ''
* `%%payment.providerPaymentTime%%` => ''
* `%%payment.creditAmount%%` => 0
* `%%payment.applyToInvoicesAutomatically%%` => false
### Service variables ## 🚀 Novedades v4.1.0 (Performance & Storage)
* `%%service.id%%` => 23
* `%%service.clientId%%` => 20
* `%%service.status%%` => 1
* `%%service.name%%` => 'Mini'
* `%%service.street1%%` => '622 Hide A Way Road'
* `%%service.street2%%` => ''
* `%%service.city%%` => 'San Jose'
* `%%service.countryId%%` => 249
* `%%service.stateId%%` => 5
* `%%service.zipCode%%` => '95135'
* `%%service.note%%` => ''
* `%%service.addressGpsLat%%` => 37.232849
* `%%service.addressGpsLon%%` => -121.752502
* `%%service.servicePlanId%%` => 1
* `%%service.servicePlanPeriodId%%` => 2
* `%%service.price%%` => 25
* `%%service.hasIndividualPrice%%` => false
* `%%service.totalPrice%%` => 25
* `%%service.currencyCode%%` => 'USD'
* `%%service.invoiceLabel%%` => ''
* `%%service.contractId%%` => ''
* `%%service.contractLengthType%%` => 1
* `%%service.minimumContractLengthMonths%%` => ''
* `%%service.activeFrom%%` => '2016-05-03T00:00:00+0000'
* `%%service.activeTo%%` => ''
* `%%service.contractEndDate%%` => ''
* `%%service.discountType%%` => 0
* `%%service.discountValue%%` => ''
* `%%service.discountInvoiceLabel%%` => ''
* `%%service.discountFrom%%` => ''
* `%%service.discountTo%%` => ''
* `%%service.tax1Id%%` => ''
* `%%service.tax2Id%%` => ''
* `%%service.tax3Id%%` => ''
* `%%service.invoicingStart%%` => '2016-05-03T00:00:00+0000'
* `%%service.invoicingPeriodType%%` => 1
* `%%service.invoicingPeriodStartDay%%` => 1
* `%%service.nextInvoicingDayAdjustment%%` => 0
* `%%service.invoicingProratedSeparately%%` => true
* `%%service.invoicingSeparately%%` => false
* `%%service.sendEmailsAutomatically%%` => false
* `%%service.useCreditAutomatically%%` => true
* `%%service.servicePlanName%%` => 'Mini'
* `%%service.servicePlanPrice%%` => 25
* `%%service.servicePlanPeriod%%` => 3
* `%%service.downloadSpeed%%` => 10
* `%%service.uploadSpeed%%` => 10
* `%%service.hasOutage%%` => true
* `%%service.stopReason%%` => 'Payments overdue'
- **⚡ 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.
- **📦 MinIO / S3 Storage**: Migración completa del almacenamiento de comprobantes. Adiós FTP, hola almacenamiento de objetos seguro y escalable con URLs firmadas y limpieza automática.
- **🖼️ Smart Image Processing**: Generación de imágenes "Full Mode" (sin recortes) y capacidad de **Edición Dinámica de Texto** (Overlay) para personalizar métodos de pago en los comprobantes.
## Developers ## 🚀 Novedades v4.0.0 (Portal Dashboard)
* This plugin is MIT-licensed and can be used by developers as a template for integrating with a different messaging solution:
* Create a new plugin based on this one
* Replace the TwilioNotifierFacade and any references to it with a different class which extends AbstractMessageNotifierFacade
* Update libraries in composer.json as needed
* Communicate with the remote system in the sendMessage() function
* Preferably also change the SmsNotifier namespace to some other (not strictly necessary).
Read more about creating your own plugin in the [Developer documentation](https://github.com/Ubiquiti-App/UCRM-plugins/blob/master/docs/index.md). - **🖥️ Dashboard de Pagos Integrado**: Nueva interfaz visual dentro de UCRM para gestionar cobros de Stripe y OXXO sin salir de la plataforma.
- **🏪 OXXO Pay Híbrido & Visual**: Generación de fichas OXXO con dos modos inteligentes:
- **Inline (Admin)**: Visualización inmediata del voucher (local) para máxima velocidad.
- **FTP (Webhook)**: Carga automática a servidor externo para compartir links públicos.
- **🌑 UI/UX Premium**: Interfaz rediseñada con Sidebar expandible, cabecera global "Sticky", Modo Oscuro automático y componentes responsivos.
---
## ⚡ Funcionalidades Principales
### 1. Gestión de Cobranza Avanzada
- **Pagos SPEI**: Instrucciones inmediatas con CLABE personalizada.
- **Vouchers OXXO**: Generación instantánea de códigos de barras con vista previa "Split-Screen" (Datos + Imagen).
- **Historial en Tiempo Real**: Consulta los últimos 10 pagos de cualquier cliente al instante.
### 2. Automatización WhatsApp (CallBell)
- **Notificaciones Dinámicas**: Envío automático de facturas, recordatorios de pago y avisos de corte.
- **Re-envío Manual**: Herramienta para disparar notificaciones específicas desde el Dashboard si el proceso automático falla.
- **Sincronización Total**: Mantiene los contactos de CallBell siempre actualizados con los datos del CRM.
### 3. Coordinación Técnica (Jobs/Tasks)
- **Agenda Inteligente**: Notifica a instaladores sobre nuevas tareas y reprogramaciones.
- **Datos para Técnicos**: Envía coordenadas GPS y contraseñas de equipos (formato impresora térmica) directamente al WhatsApp del técnico.
---
## 🏗️ Arquitectura del Sistema
El plugin utiliza una arquitectura modular basada en **Facades** y **Servicios** para garantizar estabilidad y escalabilidad.
### Componentes Clave
- **Frontend (`public.php`)**: Una Single Page Application (SPA) ligera incrustada en UCRM, construida con Vanilla JS y CSS moderno (Variables, Flexbox/Grid).
- **Despachador (`Plugin.php`)**: Router de eventos que delega acciones a los controladores específicos.
- **Motor de Pagos (`PaymentIntentStripe`)**: Servicio robusto que se comunica con la API de Stripe para crear intenciones de pago y métodos.
- **Generador OXXO (`AbstractOxxoOperationsFacade`)**: Orquesta el flujo complejo: Stripe -> Puppeteer (Screenshot) -> FTP/Local Storage -> Respuesta JSON.
---
## 🛠️ Requisitos e Integraciones
Para desbloquear todo el potencial, el plugin requiere:
1. **UISP/UCRM**: Versión 2.1.0 o superior.
2. **Stripe API**: Keys de producción/test para procesar pagos.
3. **CallBell API**: Token para el envío de mensajes de WhatsApp.
4. **Microservicio Puppeteer**: Contenedor Docker para renderizar los vouchers de OXXO a imagen.
5. **Servidor FTP (Opcional)**: Requerido solo si se desea generar URLs públicas para compartir los vouchers externamente.
---
## 🏷️ Configuración Rápida
El plugin se configura directamente desde el panel de UCRM (`Ajustes -> Plugins -> siip-whatsapp-notifications`).
### Parámetros Críticos
- `ipserver`: Tu dominio UCRM.
- `tokenstripe`: Tu Secret Key de Stripe.
- `tokencallbell`: Tu API Key de CallBell.
- `hostServerFTP` / `user` / `pass`: Credenciales para el almacenamiento de vouchers remotos.
---
## 📝 Registro de Cambios
Para consultar la evolución detallada del proyecto, ver el archivo [CHANGELOG.md](./CHANGELOG.md).

View File

@ -1,73 +1,30 @@
<?php <?php
use GuzzleHttp\Client;
require 'vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
// $baseUri = 'https://172.16.5.120/crm/api/v1.0/'; //endpoint de la API REST del CRM use Ubnt\UcrmPluginSdk\Service\PluginLogManager;
// $token = '3d5fa4c9-e134-4e8b-b8d7-aae394d48d4d'; //Token para acceder a los recursos del CRM use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
$baseUri = 'https://172.16.5.134/crm/api/v1.0/'; //endpoint de la API REST del CRM
$token = '6abef18c-783d-4dd0-b530-be6e6a7bbd1d'; //Token para acceder a los recursos del CRM
$stripe = new \Stripe\StripeClient('sk_test_51OkG0REFY1WEUtgRH6UxBK5pu80Aq5Iy8EcdPnf0cOWzuVLQTpyLCd7CbPzqMsWMafZOHElCxhEHF7g8boURjWlJ00tBwE0W1M'); //Token de clave privada en modo prueba para la API de Stripe chdir(__DIR__);
//$stripe = new \Stripe\StripeClient('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'); //Token de clave privada en modo prodcucción para la API de Stripe
//cliente GuzzleHttp para consumir API del UISP CRM // Inicializar Log (usando data/ para permisos de escritura)
$clientGuzzleHttp = new Client([ $debugLogPath = __DIR__ . '/data/plugin.log';
'base_uri' => $baseUri, file_put_contents($debugLogPath, "[" . date('Y-m-d H:i:s') . "] Hit StripeWebhook.php - Delegando a Plugin->run()" . PHP_EOL, FILE_APPEND);
'headers' => [
'X-Auth-App-Key' => $token,
'Accept' => 'application/json',
],
'verify' => false,
]);
// Obtén el contenido del cuerpo de la solicitud // Solo permitir acceso si estamos en un entorno UCRM válido
$input = @file_get_contents('php://input'); if (!file_exists(__DIR__ . '/data/config.json')) {
$event_json = json_decode($input); die('Acceso denegado o configuración no encontrada.');
$cadena = json_encode($event_json);
file_put_contents(__DIR__ . '/logsiip.log', "[" . date('Y-m-d H:i:s') . "] " . "Valor json: ".$cadena.PHP_EOL);
// Maneja el evento del webhook
if ($event_json) {
switch ($event_json->type) {
case 'cash_balance.funds_available':
$customer = $event_json->data->object;
$customerId = $customer->id;
$newBalance = $customer->balance; // El nuevo saldo del cliente
try {// Aquí puedes crear un nuevo PaymentIntent o un Cargo (Charge) usando el nuevo saldo
$paymentIntent = \Stripe\PaymentIntent::create([
'amount' => 30000,
'currency' => 'mxn',
'customer' => $customerId,
'payment_method_types' => ['customer_balance'],
'payment_method_data' => ['type' => 'customer_balance'],
'confirm' => true,
'payment_method_options' => [
'customer_balance' => [
'funding_type' => 'bank_transfer',
'bank_transfer' => ['type' => 'mx_bank_transfer']
],
],
'metadata' => [
'clientId' => $customerId, // ID del cliente en Ubiquiti
'createdBy' => 'UCRM', // ID de la factura en Ubiquiti
'paymentType' => 'card.one_time',
'signedInAdminId' => 1015,
],
]);
file_put_contents(__DIR__ . '/logsiip.log', "[" . date('Y-m-d H:i:s') . "] " . "PaymentIntent creado: " . $paymentIntent->id . PHP_EOL, FILE_APPEND);
} catch (\Stripe\Exception\ApiErrorException $e) {
file_put_contents(__DIR__ . '/logsiip.log', "[" . date('Y-m-d H:i:s') . "] " ."Error creando PaymentIntent: " . $e->getMessage() . PHP_EOL, FILE_APPEND);
}
break;
// Otros eventos relevantes
}
} }
http_response_code(200); // Responde con un 200 OK a Stripe try {
// Delegar ejecución al Plugin principal
$builder = new \DI\ContainerBuilder();
$container = $builder->build();
$plugin = $container->get(\SmsNotifier\Plugin::class);
$plugin->run();
} catch (\Exception $e) {
file_put_contents($debugLogPath, "[" . date('Y-m-d H:i:s') . "] Error en StripeWebhook.php: " . $e->getMessage() . PHP_EOL, FILE_APPEND);
http_response_code(500);
echo "Error interno";
}

26
check_method.php Executable 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

@ -16,6 +16,7 @@
"katzgrau/klogger": "^1.2", "katzgrau/klogger": "^1.2",
"stripe/stripe-php": "^13.11", "stripe/stripe-php": "^13.11",
"ext-imagick": "*", "ext-imagick": "*",
"google/apiclient": "^2.0" "google/apiclient": "^2.0",
"aws/aws-sdk-php": "^3.0"
} }
} }

512
composer.lock generated
View File

@ -4,20 +4,171 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "899edd228cc3a9ded57208726a91db8d", "content-hash": "72a3878ac2514c6e954519ec54c8054c",
"packages": [ "packages": [
{ {
"name": "firebase/php-jwt", "name": "aws/aws-crt-php",
"version": "v6.10.1", "version": "v1.2.7",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/firebase/php-jwt.git", "url": "https://github.com/awslabs/aws-crt-php.git",
"reference": "500501c2ce893c824c801da135d02661199f60c5" "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5", "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e",
"reference": "500501c2ce893c824c801da135d02661199f60c5", "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
"ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "AWS SDK Common Runtime Team",
"email": "aws-sdk-common-runtime@amazon.com"
}
],
"description": "AWS Common Runtime for PHP",
"homepage": "https://github.com/awslabs/aws-crt-php",
"keywords": [
"amazon",
"aws",
"crt",
"sdk"
],
"support": {
"issues": "https://github.com/awslabs/aws-crt-php/issues",
"source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7"
},
"time": "2024-10-18T22:15:13+00:00"
},
{
"name": "aws/aws-sdk-php",
"version": "3.369.11",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "014c521eae8eeb1f9562cb512523b17cd6c1bec7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/014c521eae8eeb1f9562cb512523b17cd6c1bec7",
"reference": "014c521eae8eeb1f9562cb512523b17cd6c1bec7",
"shasum": ""
},
"require": {
"aws/aws-crt-php": "^1.2.3",
"ext-json": "*",
"ext-pcre": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/promises": "^2.0",
"guzzlehttp/psr7": "^2.4.5",
"mtdowling/jmespath.php": "^2.8.0",
"php": ">=8.1",
"psr/http-message": "^1.0 || ^2.0",
"symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0"
},
"require-dev": {
"andrewsville/php-token-reflection": "^1.4",
"aws/aws-php-sns-message-validator": "~1.0",
"behat/behat": "~3.0",
"composer/composer": "^2.7.8",
"dms/phpunit-arraysubset-asserts": "^0.4.0",
"doctrine/cache": "~1.4",
"ext-dom": "*",
"ext-openssl": "*",
"ext-sockets": "*",
"phpunit/phpunit": "^9.6",
"psr/cache": "^2.0 || ^3.0",
"psr/simple-cache": "^2.0 || ^3.0",
"sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0",
"yoast/phpunit-polyfills": "^2.0"
},
"suggest": {
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
"doctrine/cache": "To use the DoctrineCacheAdapter",
"ext-curl": "To send requests using cURL",
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
"ext-pcntl": "To use client-side monitoring",
"ext-sockets": "To use client-side monitoring"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"Aws\\": "src/"
},
"exclude-from-classmap": [
"src/data/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Amazon Web Services",
"homepage": "http://aws.amazon.com"
}
],
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
"homepage": "http://aws.amazon.com/sdkforphp",
"keywords": [
"amazon",
"aws",
"cloud",
"dynamodb",
"ec2",
"glacier",
"s3",
"sdk"
],
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.369.11"
},
"time": "2026-01-12T19:14:19+00:00"
},
{
"name": "firebase/php-jwt",
"version": "v6.10.2",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "30c19ed0f3264cb660ea496895cfb6ef7ee3653b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/30c19ed0f3264cb660ea496895cfb6ef7ee3653b",
"reference": "30c19ed0f3264cb660ea496895cfb6ef7ee3653b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -65,40 +216,40 @@
], ],
"support": { "support": {
"issues": "https://github.com/firebase/php-jwt/issues", "issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v6.10.1" "source": "https://github.com/firebase/php-jwt/tree/v6.10.2"
}, },
"time": "2024-05-18T18:05:11+00:00" "time": "2024-11-24T11:22:49+00:00"
}, },
{ {
"name": "google/apiclient", "name": "google/apiclient",
"version": "v2.16.0", "version": "v2.18.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/googleapis/google-api-php-client.git", "url": "https://github.com/googleapis/google-api-php-client.git",
"reference": "017400f609c1fb71ab5ad824c50eabd4c3eaf779" "reference": "d8d201ba8a189a3cd7fb34e4da569f2ed440eee7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/017400f609c1fb71ab5ad824c50eabd4c3eaf779", "url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/d8d201ba8a189a3cd7fb34e4da569f2ed440eee7",
"reference": "017400f609c1fb71ab5ad824c50eabd4c3eaf779", "reference": "d8d201ba8a189a3cd7fb34e4da569f2ed440eee7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"firebase/php-jwt": "~6.0", "firebase/php-jwt": "^6.0",
"google/apiclient-services": "~0.350", "google/apiclient-services": "~0.350",
"google/auth": "^1.37", "google/auth": "^1.37",
"guzzlehttp/guzzle": "^6.5.8||^7.4.5", "guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^1.9.1||^2.2.1", "guzzlehttp/psr7": "^2.6",
"monolog/monolog": "^2.9||^3.0", "monolog/monolog": "^2.9||^3.0",
"php": "^7.4|^8.0", "php": "^8.0",
"phpseclib/phpseclib": "^3.0.36" "phpseclib/phpseclib": "^3.0.36"
}, },
"require-dev": { "require-dev": {
"cache/filesystem-adapter": "^1.1", "cache/filesystem-adapter": "^1.1",
"composer/composer": "^1.10.23", "composer/composer": "^1.10.23",
"phpcompatibility/php-compatibility": "^9.2", "phpcompatibility/php-compatibility": "^9.2",
"phpspec/prophecy-phpunit": "^2.0", "phpspec/prophecy-phpunit": "^2.1",
"phpunit/phpunit": "^9.5", "phpunit/phpunit": "^9.6",
"squizlabs/php_codesniffer": "^3.8", "squizlabs/php_codesniffer": "^3.8",
"symfony/css-selector": "~2.1", "symfony/css-selector": "~2.1",
"symfony/dom-crawler": "~2.1" "symfony/dom-crawler": "~2.1"
@ -134,22 +285,22 @@
], ],
"support": { "support": {
"issues": "https://github.com/googleapis/google-api-php-client/issues", "issues": "https://github.com/googleapis/google-api-php-client/issues",
"source": "https://github.com/googleapis/google-api-php-client/tree/v2.16.0" "source": "https://github.com/googleapis/google-api-php-client/tree/v2.18.2"
}, },
"time": "2024-04-24T00:59:47+00:00" "time": "2024-12-16T22:52:40+00:00"
}, },
{ {
"name": "google/apiclient-services", "name": "google/apiclient-services",
"version": "v0.359.0", "version": "v0.389.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/googleapis/google-api-php-client-services.git", "url": "https://github.com/googleapis/google-api-php-client-services.git",
"reference": "e975e6d0efa47f7e49280c4ea7fd6a93b6d7e338" "reference": "6274e67ee52b1a416ccee0a4eaf337d1139cdaf8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/e975e6d0efa47f7e49280c4ea7fd6a93b6d7e338", "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/6274e67ee52b1a416ccee0a4eaf337d1139cdaf8",
"reference": "e975e6d0efa47f7e49280c4ea7fd6a93b6d7e338", "reference": "6274e67ee52b1a416ccee0a4eaf337d1139cdaf8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -178,29 +329,29 @@
], ],
"support": { "support": {
"issues": "https://github.com/googleapis/google-api-php-client-services/issues", "issues": "https://github.com/googleapis/google-api-php-client-services/issues",
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.359.0" "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.389.0"
}, },
"time": "2024-06-10T01:02:17+00:00" "time": "2025-01-05T01:04:21+00:00"
}, },
{ {
"name": "google/auth", "name": "google/auth",
"version": "v1.40.0", "version": "v1.44.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/googleapis/google-auth-library-php.git", "url": "https://github.com/googleapis/google-auth-library-php.git",
"reference": "bff9f2d01677e71a98394b5ac981b99523df5178" "reference": "5670e56307d7a2eac931f677c0e59a4f8abb2e43"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/bff9f2d01677e71a98394b5ac981b99523df5178", "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/5670e56307d7a2eac931f677c0e59a4f8abb2e43",
"reference": "bff9f2d01677e71a98394b5ac981b99523df5178", "reference": "5670e56307d7a2eac931f677c0e59a4f8abb2e43",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"firebase/php-jwt": "^6.0", "firebase/php-jwt": "^6.0",
"guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^2.4.5", "guzzlehttp/psr7": "^2.4.5",
"php": "^8.0", "php": "^8.1",
"psr/cache": "^2.0||^3.0", "psr/cache": "^2.0||^3.0",
"psr/http-message": "^1.1||^2.0" "psr/http-message": "^1.1||^2.0"
}, },
@ -238,28 +389,28 @@
"support": { "support": {
"docs": "https://googleapis.github.io/google-auth-library-php/main/", "docs": "https://googleapis.github.io/google-auth-library-php/main/",
"issues": "https://github.com/googleapis/google-auth-library-php/issues", "issues": "https://github.com/googleapis/google-auth-library-php/issues",
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.40.0" "source": "https://github.com/googleapis/google-auth-library-php/tree/v1.44.0"
}, },
"time": "2024-05-31T19:16:15+00:00" "time": "2024-12-04T15:34:58+00:00"
}, },
{ {
"name": "guzzlehttp/guzzle", "name": "guzzlehttp/guzzle",
"version": "7.8.1", "version": "7.10.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/guzzle.git", "url": "https://github.com/guzzle/guzzle.git",
"reference": "41042bc7ab002487b876a0683fc8dce04ddce104" "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
"reference": "41042bc7ab002487b876a0683fc8dce04ddce104", "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-json": "*", "ext-json": "*",
"guzzlehttp/promises": "^1.5.3 || ^2.0.1", "guzzlehttp/promises": "^2.3",
"guzzlehttp/psr7": "^1.9.1 || ^2.5.1", "guzzlehttp/psr7": "^2.8",
"php": "^7.2.5 || ^8.0", "php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0", "psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0" "symfony/deprecation-contracts": "^2.2 || ^3.0"
@ -270,9 +421,9 @@
"require-dev": { "require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2", "bamarni/composer-bin-plugin": "^1.8.2",
"ext-curl": "*", "ext-curl": "*",
"php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", "guzzle/client-integration-tests": "3.0.2",
"php-http/message-factory": "^1.1", "php-http/message-factory": "^1.1",
"phpunit/phpunit": "^8.5.36 || ^9.6.15", "phpunit/phpunit": "^8.5.39 || ^9.6.20",
"psr/log": "^1.1 || ^2.0 || ^3.0" "psr/log": "^1.1 || ^2.0 || ^3.0"
}, },
"suggest": { "suggest": {
@ -350,7 +501,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/guzzle/guzzle/issues", "issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.8.1" "source": "https://github.com/guzzle/guzzle/tree/7.10.0"
}, },
"funding": [ "funding": [
{ {
@ -366,20 +517,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-12-03T20:35:24+00:00" "time": "2025-08-23T22:36:01+00:00"
}, },
{ {
"name": "guzzlehttp/promises", "name": "guzzlehttp/promises",
"version": "2.0.2", "version": "2.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/promises.git", "url": "https://github.com/guzzle/promises.git",
"reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" "reference": "481557b130ef3790cf82b713667b43030dc9c957"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957",
"reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", "reference": "481557b130ef3790cf82b713667b43030dc9c957",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -387,7 +538,7 @@
}, },
"require-dev": { "require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2", "bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.36 || ^9.6.15" "phpunit/phpunit": "^8.5.44 || ^9.6.25"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -433,7 +584,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/guzzle/promises/issues", "issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.0.2" "source": "https://github.com/guzzle/promises/tree/2.3.0"
}, },
"funding": [ "funding": [
{ {
@ -449,20 +600,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-12-03T20:19:20+00:00" "time": "2025-08-22T14:34:08+00:00"
}, },
{ {
"name": "guzzlehttp/psr7", "name": "guzzlehttp/psr7",
"version": "2.6.2", "version": "2.8.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/psr7.git", "url": "https://github.com/guzzle/psr7.git",
"reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" "reference": "21dc724a0583619cd1652f673303492272778051"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
"reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", "reference": "21dc724a0583619cd1652f673303492272778051",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -477,8 +628,8 @@
}, },
"require-dev": { "require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2", "bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "^0.9", "http-interop/http-factory-tests": "0.9.0",
"phpunit/phpunit": "^8.5.36 || ^9.6.15" "phpunit/phpunit": "^8.5.44 || ^9.6.25"
}, },
"suggest": { "suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
@ -549,7 +700,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/guzzle/psr7/issues", "issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.6.2" "source": "https://github.com/guzzle/psr7/tree/2.8.0"
}, },
"funding": [ "funding": [
{ {
@ -565,7 +716,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-12-03T20:05:35+00:00" "time": "2025-08-23T21:21:41+00:00"
}, },
{ {
"name": "katzgrau/klogger", "name": "katzgrau/klogger",
@ -623,26 +774,27 @@
}, },
{ {
"name": "laravel/serializable-closure", "name": "laravel/serializable-closure",
"version": "v1.3.3", "version": "v1.3.7",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/serializable-closure.git", "url": "https://github.com/laravel/serializable-closure.git",
"reference": "3dbf8a8e914634c48d389c1234552666b3d43754" "reference": "4f48ade902b94323ca3be7646db16209ec76be3d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3dbf8a8e914634c48d389c1234552666b3d43754", "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/4f48ade902b94323ca3be7646db16209ec76be3d",
"reference": "3dbf8a8e914634c48d389c1234552666b3d43754", "reference": "4f48ade902b94323ca3be7646db16209ec76be3d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.3|^8.0" "php": "^7.3|^8.0"
}, },
"require-dev": { "require-dev": {
"nesbot/carbon": "^2.61", "illuminate/support": "^8.0|^9.0|^10.0|^11.0",
"nesbot/carbon": "^2.61|^3.0",
"pestphp/pest": "^1.21.3", "pestphp/pest": "^1.21.3",
"phpstan/phpstan": "^1.8.2", "phpstan/phpstan": "^1.8.2",
"symfony/var-dumper": "^5.4.11" "symfony/var-dumper": "^5.4.11|^6.2.0|^7.0.0"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -679,20 +831,20 @@
"issues": "https://github.com/laravel/serializable-closure/issues", "issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure" "source": "https://github.com/laravel/serializable-closure"
}, },
"time": "2023-11-08T14:08:06+00:00" "time": "2024-11-14T18:34:49+00:00"
}, },
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "2.9.3", "version": "2.10.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Seldaek/monolog.git", "url": "https://github.com/Seldaek/monolog.git",
"reference": "a30bfe2e142720dfa990d0a7e573997f5d884215" "reference": "5cf826f2991858b54d5c3809bee745560a1042a7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/a30bfe2e142720dfa990d0a7e573997f5d884215", "url": "https://api.github.com/repos/Seldaek/monolog/zipball/5cf826f2991858b54d5c3809bee745560a1042a7",
"reference": "a30bfe2e142720dfa990d0a7e573997f5d884215", "reference": "5cf826f2991858b54d5c3809bee745560a1042a7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -769,7 +921,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/Seldaek/monolog/issues", "issues": "https://github.com/Seldaek/monolog/issues",
"source": "https://github.com/Seldaek/monolog/tree/2.9.3" "source": "https://github.com/Seldaek/monolog/tree/2.10.0"
}, },
"funding": [ "funding": [
{ {
@ -781,28 +933,94 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-04-12T20:52:51+00:00" "time": "2024-11-12T12:43:37+00:00"
}, },
{ {
"name": "paragonie/constant_time_encoding", "name": "mtdowling/jmespath.php",
"version": "v2.7.0", "version": "2.8.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git", "url": "https://github.com/jmespath/jmespath.php.git",
"reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105" "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105", "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc",
"reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105", "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7|^8" "php": "^7.2.5 || ^8.0",
"symfony/polyfill-mbstring": "^1.17"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^6|^7|^8|^9", "composer/xdebug-handler": "^3.0.3",
"vimeo/psalm": "^1|^2|^3|^4" "phpunit/phpunit": "^8.5.33"
},
"bin": [
"bin/jp.php"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.8-dev"
}
},
"autoload": {
"files": [
"src/JmesPath.php"
],
"psr-4": {
"JmesPath\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Declaratively specify how to extract elements from a JSON document",
"keywords": [
"json",
"jsonpath"
],
"support": {
"issues": "https://github.com/jmespath/jmespath.php/issues",
"source": "https://github.com/jmespath/jmespath.php/tree/2.8.0"
},
"time": "2024-09-04T18:46:31+00:00"
},
{
"name": "paragonie/constant_time_encoding",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "df1e7fde177501eee2037dd159cf04f5f301a512"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512",
"reference": "df1e7fde177501eee2037dd159cf04f5f301a512",
"shasum": ""
},
"require": {
"php": "^8"
},
"require-dev": {
"phpunit/phpunit": "^9",
"vimeo/psalm": "^4|^5"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@ -848,7 +1066,7 @@
"issues": "https://github.com/paragonie/constant_time_encoding/issues", "issues": "https://github.com/paragonie/constant_time_encoding/issues",
"source": "https://github.com/paragonie/constant_time_encoding" "source": "https://github.com/paragonie/constant_time_encoding"
}, },
"time": "2024-05-08T12:18:48+00:00" "time": "2024-05-08T12:36:18+00:00"
}, },
{ {
"name": "paragonie/random_compat", "name": "paragonie/random_compat",
@ -957,16 +1175,16 @@
}, },
{ {
"name": "php-di/php-di", "name": "php-di/php-di",
"version": "7.0.6", "version": "7.0.7",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHP-DI/PHP-DI.git", "url": "https://github.com/PHP-DI/PHP-DI.git",
"reference": "8097948a89f6ec782839b3e958432f427cac37fd" "reference": "e87435e3c0e8f22977adc5af0d5cdcc467e15cf1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/8097948a89f6ec782839b3e958432f427cac37fd", "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/e87435e3c0e8f22977adc5af0d5cdcc467e15cf1",
"reference": "8097948a89f6ec782839b3e958432f427cac37fd", "reference": "e87435e3c0e8f22977adc5af0d5cdcc467e15cf1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1014,7 +1232,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/PHP-DI/PHP-DI/issues", "issues": "https://github.com/PHP-DI/PHP-DI/issues",
"source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.6" "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.7"
}, },
"funding": [ "funding": [
{ {
@ -1026,24 +1244,24 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-11-02T10:04:50+00:00" "time": "2024-07-21T15:55:45+00:00"
}, },
{ {
"name": "phpseclib/phpseclib", "name": "phpseclib/phpseclib",
"version": "3.0.37", "version": "3.0.43",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpseclib/phpseclib.git", "url": "https://github.com/phpseclib/phpseclib.git",
"reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8" "reference": "709ec107af3cb2f385b9617be72af8cf62441d02"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cfa2013d0f68c062055180dd4328cc8b9d1f30b8", "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/709ec107af3cb2f385b9617be72af8cf62441d02",
"reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8", "reference": "709ec107af3cb2f385b9617be72af8cf62441d02",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"paragonie/constant_time_encoding": "^1|^2", "paragonie/constant_time_encoding": "^1|^2|^3",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99", "paragonie/random_compat": "^1.4|^2.0|^9.99.99",
"php": ">=5.6.1" "php": ">=5.6.1"
}, },
@ -1120,7 +1338,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/phpseclib/phpseclib/issues", "issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.37" "source": "https://github.com/phpseclib/phpseclib/tree/3.0.43"
}, },
"funding": [ "funding": [
{ {
@ -1136,7 +1354,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-03-03T02:14:58+00:00" "time": "2024-12-14T21:12:59+00:00"
}, },
{ {
"name": "psr/cache", "name": "psr/cache",
@ -1294,20 +1512,20 @@
}, },
{ {
"name": "psr/http-factory", "name": "psr/http-factory",
"version": "1.0.2", "version": "1.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/php-fig/http-factory.git", "url": "https://github.com/php-fig/http-factory.git",
"reference": "e616d01114759c4c489f93b099585439f795fe35" "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
"reference": "e616d01114759c4c489f93b099585439f795fe35", "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.0.0", "php": ">=7.1",
"psr/http-message": "^1.0 || ^2.0" "psr/http-message": "^1.0 || ^2.0"
}, },
"type": "library", "type": "library",
@ -1331,7 +1549,7 @@
"homepage": "https://www.php-fig.org/" "homepage": "https://www.php-fig.org/"
} }
], ],
"description": "Common interfaces for PSR-7 HTTP message factories", "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
"keywords": [ "keywords": [
"factory", "factory",
"http", "http",
@ -1343,9 +1561,9 @@
"response" "response"
], ],
"support": { "support": {
"source": "https://github.com/php-fig/http-factory/tree/1.0.2" "source": "https://github.com/php-fig/http-factory"
}, },
"time": "2023-04-10T20:10:41+00:00" "time": "2024-04-15T12:06:14+00:00"
}, },
{ {
"name": "psr/http-message", "name": "psr/http-message",
@ -1555,16 +1773,16 @@
}, },
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
"version": "v3.4.0", "version": "v3.6.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git", "url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1572,12 +1790,12 @@
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": {
"dev-main": "3.4-dev"
},
"thanks": { "thanks": {
"name": "symfony/contracts", "url": "https://github.com/symfony/contracts",
"url": "https://github.com/symfony/contracts" "name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
} }
}, },
"autoload": { "autoload": {
@ -1602,7 +1820,7 @@
"description": "A generic function and convention to trigger deprecation notices", "description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
}, },
"funding": [ "funding": [
{ {
@ -1618,20 +1836,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-05-23T14:45:45+00:00" "time": "2024-09-25T14:21:43+00:00"
}, },
{ {
"name": "symfony/filesystem", "name": "symfony/filesystem",
"version": "v6.4.6", "version": "v6.4.30",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/filesystem.git", "url": "https://github.com/symfony/filesystem.git",
"reference": "9919b5509ada52cc7f66f9a35c86a4a29955c9d3" "reference": "441c6b69f7222aadae7cbf5df588496d5ee37789"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/9919b5509ada52cc7f66f9a35c86a4a29955c9d3", "url": "https://api.github.com/repos/symfony/filesystem/zipball/441c6b69f7222aadae7cbf5df588496d5ee37789",
"reference": "9919b5509ada52cc7f66f9a35c86a4a29955c9d3", "reference": "441c6b69f7222aadae7cbf5df588496d5ee37789",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1639,6 +1857,9 @@
"symfony/polyfill-ctype": "~1.8", "symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8" "symfony/polyfill-mbstring": "~1.8"
}, },
"require-dev": {
"symfony/process": "^5.4|^6.4|^7.0"
},
"type": "library", "type": "library",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -1665,7 +1886,7 @@
"description": "Provides basic utilities for the filesystem", "description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/filesystem/tree/v6.4.6" "source": "https://github.com/symfony/filesystem/tree/v6.4.30"
}, },
"funding": [ "funding": [
{ {
@ -1676,29 +1897,33 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-03-21T19:36:20+00:00" "time": "2025-11-26T14:43:45+00:00"
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.29.0", "version": "v1.33.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.1" "php": ">=7.2"
}, },
"provide": { "provide": {
"ext-ctype": "*" "ext-ctype": "*"
@ -1709,8 +1934,8 @@
"type": "library", "type": "library",
"extra": { "extra": {
"thanks": { "thanks": {
"name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "name": "symfony/polyfill"
} }
}, },
"autoload": { "autoload": {
@ -1744,7 +1969,7 @@
"portable" "portable"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
}, },
"funding": [ "funding": [
{ {
@ -1755,29 +1980,34 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-01-29T20:11:03+00:00" "time": "2024-09-09T11:45:10+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.29.0", "version": "v1.33.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.1" "ext-iconv": "*",
"php": ">=7.2"
}, },
"provide": { "provide": {
"ext-mbstring": "*" "ext-mbstring": "*"
@ -1788,8 +2018,8 @@
"type": "library", "type": "library",
"extra": { "extra": {
"thanks": { "thanks": {
"name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "name": "symfony/polyfill"
} }
}, },
"autoload": { "autoload": {
@ -1824,7 +2054,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
}, },
"funding": [ "funding": [
{ {
@ -1835,12 +2065,16 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-01-29T20:11:03+00:00" "time": "2024-12-23T08:48:59+00:00"
}, },
{ {
"name": "twilio/sdk", "name": "twilio/sdk",
@ -1965,5 +2199,5 @@
"ext-imagick": "*" "ext-imagick": "*"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.2.0" "plugin-api-version": "2.6.0"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 KiB

View File

@ -1 +1 @@
{"ipserver":"172.16.5.134","apitoken":"6abef18c-783d-4dd0-b530-be6e6a7bbd1d","tokencallbell":"g8thcZkXGd3xBj2g3TtYNYFMH1fuesbJ.b6a940ea7d78cf6c9e42f067b21c8ddf96e9fa2a9e307bfd0c7c7c4d7fa38f79","tokenstripe":"sk_test_51OkG0REFY1WEUtgRH6UxBK5pu80Aq5Iy8EcdPnf0cOWzuVLQTpyLCd7CbPzqMsWMafZOHElCxhEHF7g8boURjWlJ00tBwE0W1M","unmsApiToken":null,"hostServerFTP":"siip.mx","usernameServerFTP":"siip0001","passServerFTP":"$spGiT,[wa)n","cashPaymentMethodId":false,"courtesyPaymentMethodId":false,"bankTransferPaymentMethodId":true,"paypalPaymentMethodId":true,"creditCardPaypalPaymentMethodId":true,"creditCardStripePaymentMethodId":true,"stripeSubscriptionCreditCardPaymentMethodId":true,"paypalSubscriptionPaymentMethodId":true,"mercadopagoPaymentMethodId":true,"checkPaymentMethodId":true,"customPaymentMethodId":true,"notificationTypeText":false,"debugMode":true,"logging_level":true,"dataInstallersWhatsApp":"{ \"instaladores\": [{\"ID\": 1015, \"nombre\": \"Daniel Peña, \"whatsapp\": 4181878106}, {\"ID\": 1020, \"nombre\": \"Angel Arvizu\", \"whatsapp\": 4123456789}, {\"ID\": 1035,\"nombre\": \"Juan Rostro, \"whatsapp\": 4198765432}] }"} {"ipserver":"venus.siip.mx","apitoken":"gvcnIJqXdUjneVSjhl6THLlQcYXJyIFCcwHKVba2bvIrNraanCTb5VeoWuJ0TFZ9","unmsApiToken":"4f5219de-cc5b-413d-b2fb-5133d02f3b26","tokencallbell":"g8thcZkXGd3xBj2g3TtYNYFMH1fuesbJ.b6a940ea7d78cf6c9e42f067b21c8ddf96e9fa2a9e307bfd0c7c7c4d7fa38f79","tokenstripe":"sk_test_51OkG0REFY1WEUtgRH6UxBK5pu80Aq5Iy8EcdPnf0cOWzuVLQTpyLCd7CbPzqMsWMafZOHElCxhEHF7g8boURjWlJ00tBwE0W1M","hostServerFTP":"siip.mx","usernameServerFTP":"siip0001","passServerFTP":"$spGiT,[wa)n","ipPuppeteer":"172.16.5.134","portPuppeteer":"4100","idPaymentAdminCRM":"1180","cashPaymentMethodId":true,"courtesyPaymentMethodId":true,"bankTransferPaymentMethodId":true,"paypalPaymentMethodId":true,"creditCardPaypalPaymentMethodId":true,"creditCardStripePaymentMethodId":true,"stripeSubscriptionCreditCardPaymentMethodId":true,"paypalSubscriptionPaymentMethodId":true,"mercadopagoPaymentMethodId":true,"checkPaymentMethodId":true,"customPaymentMethodId":true,"oxxoPayPaymentMethodId":true,"creditDebitCardPaymentMethodId":true,"notificationTypeText":false,"installersDataWhatsApp":"{\"instaladores\":[{\"id\":1019,\"nombre\":\"Mucio Robledo\",\"whatsapp\":\"4181878106\"},{\"id\":1173,\"nombre\":\"Ángel Arvizu\",\"whatsapp\":\"4181878106\"},{\"id\":1172,\"nombre\":\"Juan Rostro\",\"whatsapp\":\"4181878106\"},{\"id\":1015,\"nombre\":\"Daniel Humberto\",\"whatsapp\":\"4181878106\"},{\"id\":1131,\"nombre\":\"Gricelda Avalos\",\"whatsapp\":\"4181817609\"},{\"id\":\"1184\",\"nombre\":\"José Luis Enrique Sánchez\",\"whatsapp\":\"4181878106\"}]}","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}

View File

@ -1 +1 @@
{"twilioAccountSid":null,"twilioAuthToken":null,"twilioSmsNumber":null,"displayedErrors":"Not valid configuration: Twilio Account SID must be configured\nNot valid configuration: Twilio Auth Token must be configured","event_client_add":null,"event_client_archive":null,"event_client_delete":null,"event_client_edit":null,"event_invoice_add":null,"event_invoice_add_draft":null,"event_invoice_draft_approved":null,"event_invoice_delete":null,"event_invoice_edit":null,"event_payment_add":null,"event_payment_delete":null,"event_payment_edit":null,"event_payment_unmatch":null,"event_service_activate":null,"event_service_add":null,"event_service_archive":null,"event_service_end":null,"event_service_postpone":null,"event_service_suspend_cancel":null,"event_service_suspend":null,"event_invoice_near_due":null,"event_invoice_overdue":null,"event_client_message":null} {"twilioAccountSid":null,"twilioAuthToken":null,"twilioSmsNumber":null,"displayedErrors":null}

11228
data/plugin.log Normal file → Executable file

File diff suppressed because one or more lines are too long

34
explore_db.php Executable 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 Executable 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";
}

BIN
img/webp/account-balance.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
img/webp/client.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
img/webp/crm.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
img/webp/delete.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
img/webp/edit.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

24
inspect_payment.php Executable 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";
}

1
invalid_ids.csv Executable file
View File

@ -0,0 +1 @@
10
1 10

35
list_attributes.php Executable file
View File

@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
use SmsNotifier\Service\Logger;
use SmsNotifier\Service\OptionsManager;
use SmsNotifier\Service\PluginDataValidator;
use SmsNotifier\Factory\NotificationDataFactory;
use SmsNotifier\Facade\PluginNotifierFacade;
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use GuzzleHttp\Client;
$config = PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? 'localhost';
$apiUrl = "https://$ipServer/crm/api/v1.0/";
$token = $config['apitoken'] ?? '';
$client = new Client([
'base_uri' => $apiUrl,
'verify' => false,
]);
$ucrmApi = new UcrmApi($client, $token);
try {
echo "Fecthing custom attributes from $apiUrl\n";
$attributes = $ucrmApi->get('custom-attributes', ['attributeType' => 'client']);
echo "Total attributes: " . count($attributes) . "\n";
foreach ($attributes as $attr) {
echo sprintf("ID: %s | Name: %s | Key: %s\n", $attr['id'], $attr['name'], $attr['key']);
}
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}

View File

@ -13,7 +13,7 @@ require 'vendor/autoload.php';
$plugin->run(); $plugin->run();
// cleanup plugin log // cleanup plugin log
$container->get(\SmsNotifier\Service\LogCleaner::class)->clean(); //$container->get(\SmsNotifier\Service\LogCleaner::class)->clean();
} catch (Exception $e) { } catch (Exception $e) {
$logger = new \SmsNotifier\Service\Logger(); $logger = new \SmsNotifier\Service\Logger();
$logger->error($e->getMessage()); $logger->error($e->getMessage());

View File

@ -2,15 +2,62 @@
"version": "1", "version": "1",
"information": { "information": {
"name": "siip-whatsapp-notifications", "name": "siip-whatsapp-notifications",
"displayName": "SIIP - Procesador de Pagos en línea con Stripe, 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 sitema 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 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": "2.4.7", "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": [
{
"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",
"date": "2026-03-13",
"changes": "Actualización: Implementado reenvío manual de notificaciones de instaladores, fix de lógica de asignación técnica y optimización de AWS SDK (Requiere PHP 8.1+)."
},
{
"version": "4.4.0",
"date": "2026-03-10",
"changes": "Nueva funcionalidad: Reenvío manual de notificaciones de tareas a instaladores desde el módulo Gestión de Instaladores. Tabla de jobs activos por técnico con botón de reenvío."
},
{
"version": "4.3.1",
"date": "2026-03-10",
"changes": "Hotfix: Corregido bug donde al cambiar de instalador en una tarea En Curso, el nuevo técnico recibía mensaje de desasignación en vez de asignación."
},
{
"version": "4.3.0",
"date": "2026-02-23",
"changes": "Implementación de Sistema de Login Seguro con validación de sesiones UCRM y Rediseño Premium Glassmorphism UI."
},
{
"version": "4.1.0",
"date": "2026-01-15",
"changes": "Implementación de Microservicio PDF, Integración MinIO, Fix Recorte y Soporte overlay textos."
},
{
"version": "4.0.0",
"date": "2026-01-10",
"changes": "Re-diseño completo de UI, integración total de Stripe (SPEI/OXXO), visualización inline de vouchers, optimización de seguridad FTP y modo oscuro."
},
{
"version": "3.1.0",
"date": "2026-01-07",
"changes": "Añadida funcionalidad de re-envío manual de notificaciones de pago."
},
{
"version": "3.0.0",
"date": "2026-01-02",
"changes": "Soporte multi-servicio y optimizaciones de rendimiento."
}
]
}, },
"configuration": [ "configuration": [
{ {
@ -23,10 +70,17 @@
{ {
"key": "apitoken", "key": "apitoken",
"label": "Token de la API UCRM", "label": "Token de la API UCRM",
"description": "Token de autenticación para el uso de la API del sistema UISP UCRM. Contiene 36 caracteres, ejemplo: 3d3fa6c9-e268-6e8b-b4d5-aae394d99d7d", "description": "Token de autenticación necesario para el uso de la API del sistema UISP UCRM, se utiliza para gestionar cualquier información de los clientes. Contiene 64 caracteres y se genera desde el módulo de Ajustes del UISP CRM en la opción de 'Seguridad' y en la sección de 'Claves app'.",
"required": 1, "required": 1,
"type": "text" "type": "text"
}, },
{
"key": "unmsApiToken",
"label": "Token de la API UNMS",
"description": "Token de autenticación necesario para el uso de la API del sistema UISP UNMS, se utiliza para gestionar información de antenas u otros dispositivos de red. Contiene 34 caracteres y se genera desde el módulo de Ajustes del UISP Network en la opción de 'Usuarios' y en apartado de 'API tokens'.",
"required": 0,
"type": "text"
},
{ {
"key": "tokencallbell", "key": "tokencallbell",
"label": "Token de la API de CallBell", "label": "Token de la API de CallBell",
@ -37,17 +91,10 @@
{ {
"key": "tokenstripe", "key": "tokenstripe",
"label": "Token de la API de Stripe", "label": "Token de la API de Stripe",
"description": "Token de autenticación para el uso de la API de Stripe que maneja las funciones realacionadas con los pagos en línea. ", "description": "Token de autenticación para el uso de la API de Stripe que maneja las funciones realacionadas con los pagos en línea (Transferencia y OXXO PAGO). ",
"required": 1, "required": 1,
"type": "text" "type": "text"
}, },
{
"key": "unmsApiToken",
"label": "Token de la API UNMS",
"description": "Token API creado para este plugin en la seccion Network de UNMS, solo necesario cuando se utiliza UNMS v1",
"required": 0,
"type": "text"
},
{ {
"key": "hostServerFTP", "key": "hostServerFTP",
"label": "IP o dominio del servidor FTP", "label": "IP o dominio del servidor FTP",
@ -58,14 +105,35 @@
{ {
"key": "usernameServerFTP", "key": "usernameServerFTP",
"label": "Usuario FTP", "label": "Usuario FTP",
"description": "Nombre de usuario para inicio de sesión el servidor FTP", "description": "Nombre de usuario para inicio de sesión el servidor FTP, necesario para la carga de comprobantes de pago del sistema y su posterior envío",
"required": 1, "required": 1,
"type": "text" "type": "text"
}, },
{ {
"key": "passServerFTP", "key": "passServerFTP",
"label": "Password FTP", "label": "Password FTP",
"description": "Contraseña para inicio de sesión en el servidor FTP", "description": "Contraseña para inicio de sesión en el servidor FTP, necesario para la carga de comprobantes de pago del sistema y su posterior envío",
"required": 1,
"type": "text"
},
{
"key": "ipPuppeteer",
"label": "Dirección IP del servicio de Puppeteer",
"description": "Dirección IP del contenedor docker que ejecuta el servicio de Puppeteer para la generación de comprobantes de pago en formato imagen PNG",
"required": 1,
"type": "text"
},
{
"key": "portPuppeteer",
"label": "Puerto del servicio de Puppeteer",
"description": "Puerto del contenedor docker que ejecuta el servicio de Puppeteer, por defecto es el 3000",
"required": 1,
"type": "text"
},
{
"key": "idPaymentAdminCRM",
"label": "ID del usuario para pagos en línea",
"description": "ID del usuario administrador del CRM asigando para realizar pagos en línea con Stripe. Todos los pagos que llegan desde Stripe se asignan a este usuario. Se recomienda crear un usuario exclusivo para este fin.",
"required": 1, "required": 1,
"type": "text" "type": "text"
}, },
@ -146,6 +214,20 @@
"required": 0, "required": 0,
"type": "checkbox" "type": "checkbox"
}, },
{
"key": "oxxoPayPaymentMethodId",
"label": "Envío de Comprobante por pago de OXXO Pay",
"description": "Habilita el envío de comprobantes en formato de imagen por WhatsApp cuando el método de pago es OXXO Pay",
"required": 0,
"type": "checkbox"
},
{
"key": "creditDebitCardPaymentMethodId",
"label": "Envío de Comprobante por pago de Tarjeta de Crédito/Débito (Genérico)",
"description": "Habilita el envío de comprobantes en formato de imagen por WhatsApp cuando el método de pago es Tarjeta de Crédito/Débito (Genérico)",
"required": 0,
"type": "checkbox"
},
{ {
"key": "notificationTypeText", "key": "notificationTypeText",
"label": "Envío de Comprobante por medio de plantilla de texto", "label": "Envío de Comprobante por medio de plantilla de texto",
@ -154,10 +236,11 @@
"type": "checkbox" "type": "checkbox"
}, },
{ {
"key": "dataInstallersWhatsApp", "key": "installersDataWhatsApp",
"label": "Datos de los instaladores en formato JSON", "label": "Datos de los instaladores en formato JSON",
"description": "El número identificador (id) del instalador se obtiene desde la API del CRM en su ednpoint \"/api/v1.0/user\" y se debe de obtener el campo 'id' de cada instalador para agregarlo en este campo en formato JSON",
"required": 1, "required": 1,
"type": "text" "type": "textarea"
}, },
{ {
"key": "debugMode", "key": "debugMode",
@ -166,6 +249,41 @@
"required": 0, "required": 0,
"type": "checkbox" "type": "checkbox"
}, },
{
"key": "minioEndpoint",
"label": "MinIO Endpoint (Internal)",
"description": "URL interna para subir archivos (ej: http://localhost:9002)",
"required": 1,
"type": "text"
},
{
"key": "minioPublicUrl",
"label": "MinIO Public URL (External)",
"description": "URL externa para generar enlaces públicos (ej: https://aws-venus.siip.mx)",
"required": 1,
"type": "text"
},
{
"key": "minioAccessKey",
"label": "MinIO Access Key",
"description": "Clave de acceso de MinIO",
"required": 1,
"type": "text"
},
{
"key": "minioSecretKey",
"label": "MinIO Secret Key",
"description": "Clave secreta de MinIO",
"required": 1,
"type": "text"
},
{
"key": "minioBucket",
"label": "MinIO Bucket",
"description": "Nombre del bucket (ej: vouchers-oxxo)",
"required": 1,
"type": "text"
},
{ {
"key": "logging_level", "key": "logging_level",
"label": "enable debug logs", "label": "enable debug logs",
@ -177,19 +295,7 @@
"menu": [ "menu": [
{ {
"key": "Reports", "key": "Reports",
"label": "Borrar comprobantes Wordpress", "label": "Portal Administrativo de Pagos de STRIPE y Notificaciones WhatsApp",
"type": "admin",
"target": "iframe"
},
{
"key": "Reports",
"label": "Generador de Clabes CBM en Stripe",
"type": "admin",
"target": "iframe"
},
{
"key": "Reports",
"label": "Obtener datos del Network de Cliente",
"type": "admin", "type": "admin",
"target": "iframe" "target": "iframe"
} }

3067
public.php

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,707 @@
<?php
$initialDir = getcwd();
chdir(__DIR__ . '/../');
require_once __DIR__ . '/../vendor/autoload.php';
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use GuzzleHttp\Client;
// -- Configuration --
$customAttributeKey = 'passwordAntenaCliente';
$siteAttributeKey = 'site';
$antenaSectorialAttributeKey = 'antenaSectorial';
$logFile = __DIR__ . '/audit_passwords.log';
// -- Argument Parsing --
$fixOption = null;
foreach ($argv as $arg) {
if (strpos($arg, '--fix=') === 0) {
$fixOption = substr($arg, 6);
}
}
$fixLimit = 0;
if ($fixOption === 'all') {
$fixLimit = PHP_INT_MAX;
} elseif (is_numeric($fixOption)) {
$fixLimit = (int)$fixOption;
}
// -- Helpers --
function logMessage($message)
{
$logFile = __DIR__ . '/audit_passwords.log';
$timestamp = date('Y-m-d H:i:s');
file_put_contents($logFile, "[$timestamp] $message\n", FILE_APPEND);
// Only write to STDERR if running in CLI mode, not web server (optional, but good practice)
if (php_sapi_name() === 'cli') {
fwrite(STDERR, "[$timestamp] $message\n");
}
}
function isTestEnvironment($config)
{
$ipServer = $config['ipserver'] ?? '';
return ($ipServer === '172.16.5.134' || $ipServer === 'venus.siip.mx' || $ipServer === 'pruebas.internet.mx');
}
// -- Initialization --
$config = PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? 'localhost';
$apiUrl = "https://$ipServer/crm/api/v1.0/";
$token = $config['apitoken'] ?? '';
if (empty($token)) {
logMessage("Error: API Token is missing in plugin configuration.");
exit(1);
}
// Initialize UCRM Client
$client = new Client([
'base_uri' => $apiUrl,
'verify' => false,
'headers' => [
'X-Auth-App-Key' => $token,
'Content-Type' => 'application/json',
],
]);
$ucrmApi = new UcrmApi($client, $token);
// Initialize UNMS Client
$unmsClient = new Client([
'base_uri' => "https://{$ipServer}/nms/api/v2.1/",
'verify' => false,
'headers' => [
'X-Auth-Token' => $config['unmsApiToken'] ?? ''
]
]);
function fetchAllClients($client)
{
$allClients = [];
$page = 1;
$limit = 500;
do {
try {
$response = $client->get('clients', [
'query' => [
'limit' => $limit,
'offset' => ($page - 1) * $limit,
'isArchived' => 0,
]
]);
$data = json_decode($response->getBody()->getContents(), true);
if (empty($data)) break;
$allClients = array_merge($allClients, $data);
if (count($data) < $limit) break;
$page++;
} catch (\Exception $e) {
logMessage("Error fetching page $page: " . $e->getMessage());
break;
}
} while (true);
return $allClients;
}
function generateStrongPassword($length = 16)
{
$lower = 'abcdefghijkmnopqrstuvwxyz';
$upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
$digits = '23456789';
$symbols = '!@#$%&*()-_=+[]{};:,.<>?';
$all = $lower . $upper . $digits . $symbols;
$pwChars = [];
$pwChars[] = $lower[random_int(0, strlen($lower) - 1)];
$pwChars[] = $upper[random_int(0, strlen($upper) - 1)];
$pwChars[] = $digits[random_int(0, strlen($digits) - 1)];
$pwChars[] = $symbols[random_int(0, strlen($symbols) - 1)];
for ($i = count($pwChars); $i < $length; $i++) {
$pwChars[] = $all[random_int(0, strlen($all) - 1)];
}
shuffle($pwChars);
return implode('', $pwChars);
}
/**
* Fix client password AND extract network data (Site, Antena/Sectorial).
* Returns an associative array with 'password', 'siteName', 'deviceInfo'.
*
* Edge cases handled:
* - Client with tag 'NS REPETIDOR' password = 'Este cliente funciona como Repetidor'
* - Service status Ended(2) or Obsolete(5) password = 'Servicio Finalizado'
* - Device disconnected password = 'Antena desconectada al momento de obtener la contraseña'
*/
function fixClientData($clientId, $ucrmApi, $unmsClient, $attributeIds, $config, $currentPassword)
{
$passwordAttributeId = $attributeIds['password'];
$siteAttributeId = $attributeIds['site'] ?? null;
$antenaSectorialAttributeId = $attributeIds['antenaSectorial'] ?? null;
$result = [
'password' => '',
'siteName' => null,
'deviceInfo' => null,
];
try {
$isTest = isTestEnvironment($config);
// ── Edge Case 1: Check client tags for "NS REPETIDOR" ──
try {
$clientData = $ucrmApi->get("clients/$clientId");
$clientTags = $clientData['tags'] ?? [];
foreach ($clientTags as $tag) {
if (stripos($tag['name'] ?? '', 'NS REPETIDOR') !== false) {
$msg = 'Este cliente funciona como Repetidor';
$result['password'] = $msg;
$result['deviceInfo'] = 'REPETIDOR';
if ($currentPassword !== $msg) {
$attrUpdates = [$passwordAttributeId => $msg];
if ($antenaSectorialAttributeId) {
$attrUpdates[$antenaSectorialAttributeId] = 'REPETIDOR';
}
patchClientAttributes($ucrmApi, $clientId, $attrUpdates);
$result['changed'] = true;
} else {
$result['changed'] = false;
}
logMessage("Client $clientId: Tag NS REPETIDOR detected → skipping");
return $result;
}
}
} catch (\Exception $e) {
logMessage("Warning: Could not fetch client tags for $clientId: " . $e->getMessage());
}
// ── Get Services ──
$svcs = $ucrmApi->get('clients/services', ['clientId' => $clientId]);
if (empty($svcs)) {
$msg = '⚠️ Cliente sin servicios/antenas';
$result['password'] = $msg;
if ($currentPassword !== $msg) {
patchClientAttributes($ucrmApi, $clientId, [
$passwordAttributeId => $msg,
]);
$result['changed'] = true;
} else {
$result['changed'] = false;
}
return $result;
}
$allServicePasswords = [];
$numServices = count($svcs);
$siteName = null;
$deviceInfo = null;
foreach ($svcs as $index => $svc) {
$label = ($numServices > 1) ? "Servicio " . ($index + 1) . ":" : "";
$siteId = $svc['unmsClientSiteId'] ?? null;
$serviceStatus = $svc['status'] ?? null;
$passwordValue = "";
// ── Edge Case 2: Service Ended (2) or Obsolete (5) ──
if ($serviceStatus === 2 || $serviceStatus === 5) {
$passwordValue = 'Servicio Finalizado';
logMessage("Client $clientId: Service " . ($index + 1) . " has status $serviceStatus (Ended/Obsolete)");
$allServicePasswords[] = trim("$label $passwordValue");
continue;
}
if (!$siteId) {
$passwordValue = "⚠️ Sin sitio";
} else {
if ($isTest) {
// Test Env Logic: Preserve existing if valid
$foundInCRM = false;
if (!empty($currentPassword)) {
if ($numServices > 1) {
if (preg_match('/Servicio ' . ($index + 1) . ':\s*([^⚠️\s]+)/', $currentPassword, $matches)) {
$passwordValue = $matches[1];
$foundInCRM = true;
}
} else {
if (strpos($currentPassword, '⚠️') === false && strpos($currentPassword, 'Servicio') === false) {
$passwordValue = trim($currentPassword);
$foundInCRM = true;
}
}
}
if (!$foundInCRM) {
$passwordValue = generateStrongPassword(20);
// Static Test Data
$siteName = 'VENUS';
$deviceInfo = 'Sectorial de pruebas 172.16.5.134';
logMessage("Test Env: Generated new password for Client $clientId (Service $index) and set static network data.");
} else {
// Even if password exists, we might want to ensure static data is set if missing?
// User request implies "when generating that password... it should also fill".
// Let's safe-set it if we are touching the client.
// Actually, if we found it in CRM, we preserve existing password.
// But maybe we should update the network data anyway?
// The user said "when generating". So only on new generation seems safer/stricter to request,
// but usually test env data should be consistent.
// Let's set it always in Test Env if we are in this block?
// No, let's stick to "when generating" or if meaningful to update.
// If we are strictly "Testing", we might want to overwrite "Real" data with "Test" data to avoid confusion?
// But let's stick to the generation block for now as requested.
$siteName = 'VENUS';
$deviceInfo = 'Sectorial de pruebas 172.16.5.134';
logMessage("Test Env: Preserved existing password for Client $clientId but ensured static network data.");
}
} else {
// Production Logic
try {
$respDev = $unmsClient->get("devices?siteId=$siteId");
$devs = json_decode($respDev->getBody()->getContents(), true);
if (empty($devs)) {
$passwordValue = "⚠️ Sin antena";
} else {
$passVault = null;
$firstDeviceId = null;
// ── Edge Case 3: Check if device is disconnected ──
$firstDev = $devs[0] ?? null;
$deviceStatus = $firstDev['overview']['status'] ?? 'unknown';
if ($deviceStatus === 'disconnected') {
$passwordValue = 'Antena desconectada al momento de obtener la contraseña';
logMessage("Client $clientId: Device is disconnected (siteId=$siteId)");
// Still extract network data even if disconnected
if ($firstDev && $index === 0) {
$siteName = $firstDev['identification']['site']['parent']['name'] ?? null;
$apDeviceName = $firstDev['attributes']['apDevice']['name'] ?? null;
$apDeviceId = $firstDev['attributes']['apDevice']['id'] ?? null;
if ($apDeviceName && $apDeviceId) {
try {
$respApDev = $unmsClient->get("devices/$apDeviceId");
$apDevData = json_decode($respApDev->getBody()->getContents(), true);
$apDeviceIP = $apDevData['ipAddress'] ?? '';
$deviceInfo = trim("$apDeviceName $apDeviceIP");
} catch (\Exception $e) {
$deviceInfo = $apDeviceName;
}
} elseif (!$apDeviceName) {
$deviceInfo = 'REPETIDOR';
}
}
$allServicePasswords[] = trim("$label $passwordValue");
continue; // Skip vault/regenerate for this service
}
// $firstDev already set above (line 258)
if ($firstDev && $index === 0) {
// Site name from parent site
$siteName = $firstDev['identification']['site']['parent']['name'] ?? null;
// AP Device name and IP
$apDeviceName = $firstDev['attributes']['apDevice']['name'] ?? null;
$apDeviceId = $firstDev['attributes']['apDevice']['id'] ?? null;
if ($apDeviceName && $apDeviceId) {
// Fetch AP device IP
try {
$respApDev = $unmsClient->get("devices/$apDeviceId");
$apDevData = json_decode($respApDev->getBody()->getContents(), true);
$apDeviceIP = $apDevData['ipAddress'] ?? '';
$deviceInfo = trim("$apDeviceName $apDeviceIP");
} catch (\Exception $e) {
$deviceInfo = $apDeviceName;
logMessage("Warning: Could not fetch AP device IP for $apDeviceId: " . $e->getMessage());
}
} elseif (!$apDeviceName) {
// No apDevice = possibly a repeater
$deviceInfo = 'REPETIDOR';
logMessage("Client $clientId: Device is a repeater (no apDevice)");
}
}
foreach ($devs as $dev) {
$deviceId = $dev['identification']['id'] ?? null;
if (!$deviceId) continue;
if (!$firstDeviceId) $firstDeviceId = $deviceId;
try {
$respVault = $unmsClient->get("vault/$deviceId/credentials");
$vault = json_decode($respVault->getBody()->getContents(), true);
if (isset($vault['credentials'][0]['password'])) {
$passVault = $vault['credentials'][0]['password'];
break;
}
} catch (\Exception $e) {
continue;
}
}
if ($passVault) {
$passwordValue = $passVault;
} elseif ($firstDeviceId) {
// Regenerate on Device
$newPass = generateStrongPassword(16);
try {
$unmsClient->post("vault/$firstDeviceId/credentials/regenerate", [
'json' => [['username' => 'ubnt', 'password' => $newPass, 'readOnly' => true]]
]);
$passwordValue = $newPass;
logMessage("Prod Env: Regenerated password on UNMS Device $firstDeviceId");
} catch (\Exception $e) {
$passwordValue = "⚠️ Error Regenerating: " . $e->getMessage();
}
} else {
$passwordValue = "⚠️ Sin dispositivo válido";
}
}
} catch (\Exception $e) {
$passwordValue = "⚠️ Error API UNMS: " . $e->getMessage();
}
}
}
$allServicePasswords[] = trim("$label $passwordValue");
}
$finalValue = implode(' ', $allServicePasswords);
$result['password'] = $finalValue;
$result['siteName'] = $siteName;
$result['deviceInfo'] = $deviceInfo;
// Build attribute updates (always patch all available fields)
$attrUpdates = [];
// Password: only if changed
if ($finalValue !== $currentPassword) {
$attrUpdates[$passwordAttributeId] = $finalValue;
}
// Site: patch if we got data and attribute ID exists
if ($siteName !== null && $siteAttributeId) {
$attrUpdates[$siteAttributeId] = $siteName;
}
// Antena/Sectorial: patch if we got data and attribute ID exists
if ($deviceInfo !== null && $antenaSectorialAttributeId) {
$attrUpdates[$antenaSectorialAttributeId] = $deviceInfo;
}
if (!empty($attrUpdates)) {
patchClientAttributes($ucrmApi, $clientId, $attrUpdates);
logMessage("Client $clientId: Patched " . count($attrUpdates) . " attribute(s)");
}
$result['changed'] = ($finalValue !== $currentPassword) || !empty($attrUpdates);
return $result;
} catch (\Exception $e) {
$result['password'] = "Error fixing: " . $e->getMessage();
return $result;
}
}
/**
* Patch multiple client custom attributes in a single API call.
*/
function patchClientAttributes($ucrmApi, $clientId, array $attributeUpdates)
{
$attributes = [];
foreach ($attributeUpdates as $attrId => $value) {
if ($attrId && $value !== null) {
$attributes[] = ['customAttributeId' => (int)$attrId, 'value' => (string)$value];
}
}
if (!empty($attributes)) {
$ucrmApi->patch("clients/$clientId", ['attributes' => $attributes]);
}
}
/**
* Resolves UCRM custom attribute IDs based on keys.
*/
function resolveAttributeIds($ucrmApi, $customAttributeKey, $siteAttributeKey, $antenaSectorialAttributeKey)
{
$customAttributeId = null;
$siteAttributeId = null;
$antenaSectorialAttributeId = null;
try {
$attributes = $ucrmApi->get('custom-attributes', ['attributeType' => 'client']);
foreach ($attributes as $attr) {
switch ($attr['key']) {
case $customAttributeKey:
$customAttributeId = $attr['id'];
break;
case $siteAttributeKey:
$siteAttributeId = $attr['id'];
break;
case $antenaSectorialAttributeKey:
$antenaSectorialAttributeId = $attr['id'];
break;
}
}
} catch (\Exception $e) {
logMessage("Error fetching attributes: " . $e->getMessage());
return null; // Return null on failure
}
if (!$customAttributeId) {
logMessage("Error: Custom attribute '$customAttributeKey' not found.");
return null;
}
// Log resolved IDs
logMessage("Resolved attribute IDs: password=$customAttributeId, site=" . ($siteAttributeId ?? 'N/A') . ", antenaSectorial=" . ($antenaSectorialAttributeId ?? 'N/A'));
return [
'password' => $customAttributeId,
'site' => $siteAttributeId,
'antenaSectorial' => $antenaSectorialAttributeId,
];
}
// -- Main Execution --
if (!defined('INCLUDED_AS_LIBRARY')) {
// Initialization
$config = PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? 'localhost';
$apiUrl = "https://$ipServer/crm/api/v1.0/";
$token = $config['apitoken'] ?? '';
if (empty($token)) {
logMessage("Error: API Token is missing in plugin configuration.");
exit(1);
}
// Initialize UCRM Client
$client = new Client([
'base_uri' => $apiUrl,
'verify' => false,
'headers' => [
'X-Auth-App-Key' => $token,
'Content-Type' => 'application/json',
],
]);
$ucrmApi = new UcrmApi($client, $token);
// Initialize UNMS Client
$unmsClient = new Client([
'base_uri' => "https://{$ipServer}/nms/api/v2.1/",
'verify' => false,
'headers' => [
'X-Auth-Token' => $config['unmsApiToken'] ?? ''
]
]);
// Resolve IDs
$attributeIds = resolveAttributeIds($ucrmApi, $customAttributeKey, $siteAttributeKey, $antenaSectorialAttributeKey);
if (!$attributeIds) {
exit(1);
}
if ($fixOption === null) {
// Check for --file argument
$fileOption = null;
foreach ($argv as $arg) {
if (strpos($arg, '--file=') === 0) {
$fileOption = substr($arg, 7);
}
}
if ($fileOption) {
if ($fileOption[0] !== '/') {
$fileOption = $initialDir . '/' . $fileOption;
}
if (!file_exists($fileOption)) {
logMessage("Error: File not found: $fileOption");
exit(1);
}
logMessage("Processing from file: $fileOption");
// Read all lines
$lines = file($fileOption, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// Process lines
$originalCount = count($lines);
$processedCount = 0;
// Helper to rewrite file
$rewriteFile = function ($remainingLines) use ($fileOption) {
file_put_contents($fileOption, implode("\n", $remainingLines) . "\n");
};
$header = null;
if (!empty($lines)) {
$firstLine = $lines[0];
$parts = explode(',', $firstLine);
if (!is_numeric($parts[0])) {
$header = array_shift($lines); // Remove header from processing list
}
}
$queue = $lines; // Working copy
while (!empty($queue)) {
$line = array_shift($queue); // Take next
if (empty(trim($line))) continue;
$clientId = trim($line);
if (!is_numeric($clientId)) {
logMessage("Skipping invalid line: $line");
continue;
}
$clientId = (int)$clientId;
logMessage("Processing ID from file: $clientId");
try {
$clientDataResponse = $ucrmApi->get("clients/$clientId");
$currentAttributes = $clientDataResponse['attributes'] ?? [];
$currentPass = null;
foreach ($currentAttributes as $attr) {
if ($attr['key'] === $customAttributeKey) {
$currentPass = $attr['value'];
break;
}
}
$fixResult = fixClientData($clientId, $ucrmApi, $unmsClient, $attributeIds, $config, $currentPass);
$newPass = $fixResult['password'];
$siteInfo = $fixResult['siteName'] ? " | Site: {$fixResult['siteName']}" : '';
$devInfo = $fixResult['deviceInfo'] ? " | Antena: {$fixResult['deviceInfo']}" : '';
logMessage("Client $clientId result: $newPass$siteInfo$devInfo");
} catch (\Exception $e) {
logMessage("Error processing Client $clientId: " . $e->getMessage());
}
// Rewrite file
$validLines = $queue;
if ($header) {
array_unshift($validLines, $header);
}
$rewriteFile($validLines);
}
logMessage("File processing complete.");
exit(0);
}
}
if ($fixLimit > 0) {
logMessage("Fix mode enabled. Limit: " . ($fixOption === 'all' ? 'ALL' : $fixLimit));
}
// 1. Fetch all clients
logMessage("Fetching clients...");
$clients = fetchAllClients($client);
logMessage("Total active clients fetched: " . count($clients));
// 2. Filter & Fix
$results = [];
$fixedCount = 0;
foreach ($clients as $clientData) {
$password = null;
$attributes = $clientData['attributes'] ?? [];
foreach ($attributes as $attr) {
if ($attr['key'] === $customAttributeKey) {
$password = $attr['value'];
break;
}
}
// Validation Logic
$isValid = true;
$reason = "";
// Detect explicit error states that should be retried (e.g. "⚠️ Error Regenerating...")
$isError = (stripos((string)$password, 'Error') !== false);
$isManaged = (strpos((string)$password, 'Servicio') !== false || strpos((string)$password, '⚠️') !== false);
if (empty($password)) {
$isValid = false;
$reason = "Missing";
} elseif ($isError) {
$isValid = false;
$reason = "Error (Retry)";
} elseif (!$isManaged) {
$len = strlen($password);
if ($len < 8 || $len > 20) {
$isValid = false;
$reason = "Length ($len)";
} elseif (strpos($password, ' ') !== false) {
$isValid = false;
$reason = "Spaces";
}
}
if (!$isValid) {
$newPassword = "";
$actionTaken = "None";
// Fix Logic
if ($fixLimit > 0 && $fixedCount < $fixLimit) {
logMessage("Fixing client {$clientData['id']}...");
$fixResult = fixClientData($clientData['id'], $ucrmApi, $unmsClient, $attributeIds, $config, $password);
$newPassword = $fixResult['password'];
// Basic check if fix resulted in a change/value
if ($fixResult['changed'] ?? false) {
$actionTaken = "Updated";
$fixedCount++;
} else {
$actionTaken = "Attempted (No Change/Error)";
}
}
$results[] = [
'clientId' => $clientData['id'],
'name' => ($clientData['firstName'] ?? '') . ' ' . ($clientData['lastName'] ?? ''),
'userIdent' => $clientData['userIdent'] ?? '',
'currentPassword' => $password ?? '',
'action' => $actionTaken,
'newPassword' => $newPassword
];
}
}
// 3. Output CSV
logMessage("Found " . count($results) . " clients with invalid passwords.");
if ($fixLimit > 0) {
logMessage("Fixed $fixedCount clients.");
}
echo "clientId,name,userIdent,currentPassword,action,newPassword\n";
foreach ($results as $row) {
echo sprintf(
"%s,\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n",
$row['clientId'],
str_replace('"', '""', $row['name']),
str_replace('"', '""', $row['userIdent']),
str_replace('"', '""', $row['currentPassword']),
$row['action'],
str_replace('"', '""', $row['newPassword'])
);
}
logMessage("Done.");
}

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

63
scripts-uisp/debug_clients.php Executable file
View File

@ -0,0 +1,63 @@
<?php
$initialDir = getcwd();
chdir(__DIR__ . '/../');
require_once __DIR__ . '/../vendor/autoload.php';
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use GuzzleHttp\Client;
$config = PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? 'localhost';
$apiUrl = "https://$ipServer/crm/api/v1.0/";
$token = $config['apitoken'] ?? '';
$client = new Client([
'base_uri' => $apiUrl,
'verify' => false,
'headers' => [
'X-Auth-App-Key' => $token,
'Content-Type' => 'application/json',
],
]);
$ucrmApi = new UcrmApi($client, $token);
$ids = [18, 20];
$customAttributeKey = 'passwordAntenaCliente';
foreach ($ids as $id) {
try {
$data = $ucrmApi->get("clients/$id");
echo "Client ID: $id\n";
echo "IsArchived: " . ($data['isArchived'] ? 'Yes' : 'No') . "\n";
echo "All Attributes:\n";
foreach ($data['attributes'] as $attr) {
echo " - {$attr['key']} (ID: {$attr['customAttributeId']}): [{$attr['value']}]\n";
}
flush();
$passVal = null;
$foundKey = false;
foreach ($data['attributes'] as $attr) {
if ($attr['key'] === $customAttributeKey) {
$passVal = $attr['value'];
$foundKey = true;
break;
}
}
if ($foundKey) {
echo "Attribute '$customAttributeKey' FOUND.\n";
echo "Value: [" . $passVal . "]\n";
echo "Type: " . gettype($passVal) . "\n";
echo "Empty? " . (empty($passVal) ? 'Yes' : 'No') . "\n";
} else {
echo "Attribute '$customAttributeKey' NOT FOUND in attributes list.\n";
}
echo "--------------------------------------\n";
} catch (\Exception $e) {
echo "Error fetching $id: " . $e->getMessage() . "\n";
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,166 @@
<?php
$initialDir = getcwd();
chdir(__DIR__ . '/../');
require_once __DIR__ . '/../vendor/autoload.php';
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use GuzzleHttp\Client;
// -- Configuration --
$customAttributeKey = 'passwordAntenaCliente';
// -- Initialization --
$config = PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? 'localhost';
$apiUrl = "https://$ipServer/crm/api/v1.0/";
$token = $config['apitoken'] ?? '';
if (empty($token)) {
fwrite(STDERR, "Error: API Token is missing in plugin configuration.\n");
exit(1);
}
// Initialize UCRM Client
$client = new Client([
'base_uri' => $apiUrl,
'verify' => false,
'headers' => [
'X-Auth-App-Key' => $token,
'Content-Type' => 'application/json',
],
]);
$ucrmApi = new UcrmApi($client, $token);
function fetchAllClients($client)
{
$allClients = [];
$page = 1;
$limit = 500;
do {
try {
$response = $client->get('clients', [
'query' => [
'limit' => $limit,
'offset' => ($page - 1) * $limit,
'isArchived' => 0,
]
]);
$data = json_decode($response->getBody()->getContents(), true);
if (empty($data)) break;
$allClients = array_merge($allClients, $data);
if (count($data) < $limit) break;
$page++;
} catch (\Exception $e) {
fwrite(STDERR, "Error fetching page $page: " . $e->getMessage() . "\n");
break;
}
} while (true);
return $allClients;
}
// 0. Resolve Attribute ID
$customAttributeId = null;
try {
$attributes = $ucrmApi->get('custom-attributes', ['attributeType' => 'client']);
foreach ($attributes as $attr) {
if ($attr['key'] === $customAttributeKey) {
$customAttributeId = $attr['id'];
break;
}
}
} catch (\Exception $e) {
fwrite(STDERR, "Error fetching attributes: " . $e->getMessage() . "\n");
exit(1);
}
if (!$customAttributeId) {
fwrite(STDERR, "Error: Custom attribute '$customAttributeKey' not found.\n");
exit(1);
}
// 1. Fetch all clients
fwrite(STDERR, "Fetching clients...\n");
$clients = fetchAllClients($client);
fwrite(STDERR, "Total active clients fetched: " . count($clients) . "\n");
// 2. Identify Invalid
$results = [];
foreach ($clients as $clientData) {
$password = null;
$attributes = $clientData['attributes'] ?? [];
foreach ($attributes as $attr) {
if ($attr['key'] === $customAttributeKey) {
$password = $attr['value'];
break;
}
}
// Detect explicit error states that should be retried (e.g. "⚠️ Error Regenerating...")
$isError = (stripos((string)$password, 'Error') !== false);
// Detect managed states (valid non-passwords like "⚠️ Sin sitio", "Servicio Finalizado", etc.)
$isManaged = (strpos((string)$password, 'Servicio') !== false || strpos((string)$password, '⚠️') !== false);
$isValid = true;
if (empty($password)) {
$isValid = false;
} elseif ($isError) {
$isValid = false;
} elseif (!$isManaged) {
$len = strlen($password);
if ($len < 8 || $len > 20) {
$isValid = false;
} elseif (strpos($password, ' ') !== false) {
$isValid = false;
}
}
if (!$isValid) {
$results[] = [
'id' => $clientData['id'],
'name' => ($clientData['firstName'] ?? '') . ' ' . ($clientData['lastName'] ?? '')
];
}
}
// 3. Output CSV
// 3. Output CSV
$outputFile = null;
foreach ($argv as $arg) {
if (strpos($arg, '--output=') === 0) {
$outputFile = substr($arg, 9);
}
}
if ($outputFile) {
// Resolve relative path to initial execution directory
if ($outputFile[0] !== '/') {
global $initialDir;
$outputFile = $initialDir . '/' . $outputFile;
}
$outputHandle = fopen($outputFile, 'w');
fwrite(STDERR, "Writing to file: $outputFile\n");
} else {
$outputHandle = STDOUT;
}
foreach ($results as $row) {
fwrite($outputHandle, $row['id'] . "\n");
}
if ($outputFile) {
fclose($outputHandle);
fwrite(STDERR, "Found " . count($results) . " invalid clients written to $outputFile.\n");
} else {
fwrite(STDERR, "Found " . count($results) . " invalid clients.\n");
}

Binary file not shown.

BIN
src/.DS_Store vendored Executable file

Binary file not shown.

View File

@ -13,140 +13,150 @@ class PluginData extends UcrmData
/** /**
* @var string * @var string
*/ */
public $twilioAccountSid; public $ipserver;
/** /**
* @var string * @var string
*/ */
public $apitoken;
/**
* @var string
*/
public $unmsApiToken;
/**
* @var string
*/
public $tokencallbell;
/**
* @var string
*/
public $tokenstripe;
/**
* @var string
*/
public $hostServerFTP;
/**
* @var string
*/
public $usernameServerFTP;
/**
* @var string
*/
public $passServerFTP;
/**
* @var string
*/
public $ipPuppeteer;
/**
* @var string
*/
public $portPuppeteer;
/**
* @var string
*/
public $idPaymentAdminCRM;
/**
* @var bool
*/
public $cashPaymentMethodId;
/**
* @var bool
*/
public $courtesyPaymentMethodId;
/**
* @var bool
*/
public $bankTransferPaymentMethodId;
/**
* @var bool
*/
public $paypalPaymentMethodId;
/**
* @var bool
*/
public $creditCardPaypalPaymentMethodId;
/**
* @var bool
*/
public $creditCardStripePaymentMethodId;
/**
* @var bool
*/
public $stripeSubscriptionCreditCardPaymentMethodId;
/**
* @var bool
*/
public $paypalSubscriptionPaymentMethodId;
/**
* @var bool
*/
public $mercadopagoPaymentMethodId;
/**
* @var bool
*/
public $checkPaymentMethodId;
/**
* @var bool
*/
public $customPaymentMethodId;
/**
* @var bool
*/
public $notificationTypeText;
/**
* @var string
*/
public $installersDataWhatsApp;
/**
* @var bool
*/
public $debugMode;
/**
* @var bool
*/
public $logging_level;
/**
* @var string|null
*/
public $twilioAccountSid;
/**
* @var string|null
*/
public $twilioAuthToken; public $twilioAuthToken;
/** /**
* @var string * @var string|null
*/ */
public $twilioSmsNumber; public $twilioSmsNumber;
/** /**
* @var string * @var string|null
*/ */
public $displayedErrors; public $displayedErrors;
/**
* @var string
*/
public $event_client_add;
/**
* @var string
*/
public $event_client_archive;
/**
* @var string
*/
public $event_client_delete;
/**
* @var string
*/
public $event_client_edit;
/**
* @var string
*/
public $event_invoice_add;
/**
* @var string
*/
public $event_invoice_add_draft;
/**
* @var string
*/
public $event_invoice_draft_approved;
/**
* @var string
*/
public $event_invoice_delete;
/**
* @var string
*/
public $event_invoice_edit;
/**
* @var string
*/
public $event_payment_add;
/**
* @var string
*/
public $event_payment_delete;
/**
* @var string
*/
public $event_payment_edit;
/**
* @var string
*/
public $event_payment_unmatch;
/**
* @var string
*/
public $event_service_activate;
/**
* @var string
*/
public $event_service_add;
/**
* @var string
*/
public $event_service_archive;
/**
* @var string
*/
public $event_service_end;
/**
* @var string
*/
public $event_service_postpone;
/**
* @var string
*/
public $event_service_suspend_cancel;
/**
* @var string
*/
public $event_service_suspend;
/**
* @var string
*/
public $event_invoice_near_due;
/**
* @var string
*/
public $event_invoice_overdue;
/**
* @var string
*/
public $event_client_message;
/**
* @var string
*/
public $logging_level;
} }

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,8 @@ use SmsNotifier\Service\SmsNumberProvider;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use Ubnt\UcrmPluginSdk\Service\UcrmApi; use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use Ubnt\UcrmPluginSdk\Service\PluginLogManager;
/* /*
@ -33,6 +35,12 @@ abstract class AbstractOxxoOperationsFacade
* @var SmsNumberProvider * @var SmsNumberProvider
*/ */
protected $clientPhoneNumber; protected $clientPhoneNumber;
/**
* @var \SmsNotifier\Service\MinioStorageService
*/
protected $minioStorage;
/** /**
* @var UcrmApi * @var UcrmApi
*/ */
@ -44,168 +52,441 @@ abstract class AbstractOxxoOperationsFacade
public function __construct( public function __construct(
Logger $logger, Logger $logger,
MessageTextFactory $messageTextFactory, MessageTextFactory $messageTextFactory,
SmsNumberProvider $clientPhoneNumber SmsNumberProvider $clientPhoneNumber,
\SmsNotifier\Service\MinioStorageService $minioStorage
) { ) {
$this->logger = $logger; $this->logger = $logger;
$this->messageTextFactory = $messageTextFactory; $this->messageTextFactory = $messageTextFactory;
$this->clientPhoneNumber = $clientPhoneNumber; $this->clientPhoneNumber = $clientPhoneNumber;
$this->minioStorage = $minioStorage;
$config = PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? 'localhost';
$apiUrl = "https://$ipServer/crm/api/v1.0/";
$client = new Client([
'base_uri' => $apiUrl,
'verify' => false,
]);
$this->ucrmApi = new UcrmApi($client, $config['apitoken'] ?? '');
} }
/* /*
* Creates a PaymentIntent for OXXO in Stripe for a Customer * Creates a PaymentIntent for OXXO in Stripe for a Customer
* @param array $event_json
* @param int|null $amount
* @return array
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \Stripe\Exception\ApiErrorException
* @throws Exception
*/ */
public function createOxxoPaymentIntent($event_json):string public function createOxxoPaymentIntent($event_json, $amount = null, bool $uploadToFtp = true): array
{ {
$this->logger->info("Creando referencia del cliente para oxxo: ".PHP_EOL); // 1. Obtener Referencia de Stripe (Lógica Común)
$configManager = \Ubnt\UcrmPluginSdk\Service\PluginConfigManager::create(); $stripeResult = $this->createStripeReference($event_json, $amount);
// Si hubo error en Stripe, retornar el resultado de error
if ($stripeResult['hasError']) {
return $stripeResult['data'];
}
// 2. Procesamiento Síncrono (Generación inmediata)
// Usamos los datos obtenidos de Stripe
$oxxoData = $stripeResult['data'];
return $this->processSynchronousGeneration($oxxoData, $uploadToFtp);
}
/**
* Crea la orden en el servicio Docker y retorna el ID para flujo asíncrono
*/
public function createAsyncOxxoOrder($event_json, $amount = null): array
{
// 1. Obtener Referencia de Stripe
$stripeResult = $this->createStripeReference($event_json, $amount);
if ($stripeResult['hasError']) {
return $stripeResult['data'];
}
$data = $stripeResult['data'];
// 2. Llamar al servicio Docker para crear la orden (POST /orders)
$config = PluginConfigManager::create()->loadConfig();
$ipPuppeteer = $config['ipPuppeteer'];
$portPuppeteer = $config['portPuppeteer'];
$api_url = "http://$ipPuppeteer:$portPuppeteer/orders";
try {
$client = new Client(['timeout' => 5]);
$response = $client->post($api_url, [
'json' => [
'client_id' => $data['clientID'],
'amount' => $data['amount'],
'client_full_name' => $data['clientFullName'],
'oxxo_reference' => $data['oxxo_reference'],
'stripe_url' => $data['url']
]
]);
$orderData = json_decode($response->getBody()->getContents(), true);
// Agregar el order_id al resultado para que el Plugin lo devuelva
$data['order_id'] = $orderData['order_id'] ?? null;
return $data; // Retornamos los datos básicos + order_id
} catch (Exception $e) {
$this->logger->error("Error creando orden asíncrona en Docker: " . $e->getMessage());
// Fallback: Retornar los datos de Stripe aunque falle el guardado en DB local (mejor que nada)
$data['error'] = 'docker_order_creation_failed';
return $data;
}
}
/**
* Trigger para generar la imagen en background, subirla y actualizar la orden
*/
public function generateAndUploadOrder(int $orderId, string $stripeUrl, string $clientFullName, bool $uploadToFtp = true): void
{
$this->logger->info("Iniciando generación en background para Orden #$orderId");
$config = PluginConfigManager::create()->loadConfig();
$ipPuppeteer = $config['ipPuppeteer'];
$portPuppeteer = $config['portPuppeteer'];
// 1. Solicitar Generación (POST /orders/:id/generate)
// Calculamos nombre de archivo
$clientFullNameWithoutSpaces = str_replace(' ', '_', $clientFullName);
$voucherFileName = 'voucher_'.$clientFullNameWithoutSpaces.'_' . time() . '.jpeg';
$output_filename = realpath(__DIR__ . '/../../vouchers_oxxo') . '/'. $voucherFileName;
$generateUrl = "http://$ipPuppeteer:$portPuppeteer/orders/$orderId/generate";
try {
$client = new Client(['timeout' => 60]); // Mayor timeout para Puppeteer
$response = $client->post($generateUrl, [
'json' => [
'url' => $stripeUrl,
'filename' => $voucherFileName,
'clip' => ['x' => 325, 'y' => 30, 'width' => 550, 'height' => 550]
]
]);
if ($response->getStatusCode() === 200) {
// Guardar imagen localmente
$imageContent = $response->getBody()->getContents();
file_put_contents($output_filename, $imageContent);
$this->logger->info("Imagen generada y guardada localmente: $voucherFileName");
$voucherUrl = '';
// 2. Subir a S3/MinIO (si aplica)
if ($uploadToFtp) {
// $voucherUrl = $this->UploadVoucherToWordpressByImageFileName($voucherFileName);
// $this->logger->info("Imagen subida a FTP: $voucherUrl");
// Nueva implementación MinIO
$voucherUrl = $this->minioStorage->uploadFile($output_filename, $voucherFileName);
$this->logger->info("Imagen subida a MinIO: $voucherUrl");
}
// 3. Actualizar Orden (POST /orders/:id/complete)
if ($voucherUrl) {
$completeUrl = "http://$ipPuppeteer:$portPuppeteer/orders/$orderId/complete";
$client->post($completeUrl, [
'json' => ['voucher_image_url' => $voucherUrl]
]);
}
}
} catch (Exception $e) {
$this->logger->error("Error en flujo de background para Orden #$orderId: " . $e->getMessage());
}
}
/**
* Obtiene el estado de una orden desde el servicio Docker
*/
public function getOxxoOrderStatus($orderId): array
{
$config = PluginConfigManager::create()->loadConfig();
$ipPuppeteer = $config['ipPuppeteer'];
$portPuppeteer = $config['portPuppeteer'];
$api_url = "http://$ipPuppeteer:$portPuppeteer/orders/$orderId";
try {
$client = new Client(['timeout' => 5]);
$response = $client->get($api_url);
$data = json_decode($response->getBody()->getContents(), true);
// Si la URL de la imagen es relativa o local, ajustarla si es necesario
// pero el servicio ya guarda la URL completa del FTP si se subió.
return $data;
} catch (Exception $e) {
$this->logger->error("Error obteniendo estado de orden #$orderId: " . $e->getMessage());
return ['error' => 'fetch_failed', 'message' => $e->getMessage()];
}
}
/**
* Lógica extraída de Stripe
* Retorna ['hasError' => bool, 'data' => array]
*/
public function createStripeReference($event_json, $amount = null): array
{
$arrayOxxoPayment = [];
$integerAmount = $amount;
// Inicializar Config y Clientes (Copied logic)
$configManager = PluginConfigManager::create();
$config = $configManager->loadConfig(); $config = $configManager->loadConfig();
$StripeToken = $config['tokenstripe']; $StripeToken = $config['tokenstripe'];
$IPServer = $config['ipserver']; $IPServer = $config['ipserver'];
$tokenCRM = $config['apitoken']; $tokenCRM = $config['apitoken'];
// $this->logger->info("Datos de configuración: ".PHP_EOL);
// $this->logger->info("Stripe token: ".$StripeToken.PHP_EOL);
// $this->logger->info("IP server: ".$IPServer.PHP_EOL);
// $this->logger->info("Token CRM: ".$tokenCRM.PHP_EOL);
$baseUri = 'https://'.$IPServer.'/crm/api/v1.0/'; //endpoint de la API REST del CRM
$stripe = new \Stripe\StripeClient($StripeToken); //Token de clave privada para la API de Stripe $baseUri = 'https://' . $IPServer . '/crm/api/v1.0/';
//$stripe = new \Stripe\StripeClient('sk_test_51OkG0REFY1WEUtgRH6UxBK5pu80Aq5Iy8EcdPnf0cOWzuVLQTpyLCd7CbPzqMsWMafZOHElCxhEHF7g8boURjWlJ00tBwE0W1M'); //Token de clave privada para la API de Stripe modo pruebas if (!isset($this->ucrmApi)) {
// Re-instanciar si es necesario, aunque ya está en consructor
$this->ucrmApi = UcrmApi::create(); // Pero usamos la propiedad de clase
$currentUserAdmin = $this->ucrmApi->get('users/admins',[]); }
$clientID = $event_json->client_id;
// ... Lógica de obtención de cliente, Stripe Customer, PaymentIntent ...
// [RESUMIDO: Copiaremos el tocho de código aquí, simplificado]
$clientID = $event_json['client_id'];
$clientFullName = '';
// --- 1. Obtener Cliente CRM ---
try { try {
$clientGuzzle = new Client([
$clientGuzzleHttp = new Client([
'base_uri' => $baseUri, 'base_uri' => $baseUri,
'headers' => [ 'headers' => ['X-Auth-App-Key' => $tokenCRM, 'Accept' => 'application/json'],
'X-Auth-App-Key' => $tokenCRM, // Cambia el nombre de la cabecera de autorización para el UISP CRM
'Accept' => 'application/json', // Indica que esperamos una respuesta en formato JSON
],
'verify' => false, 'verify' => false,
'timeout' => 5
]); ]);
$resp = $clientGuzzle->get("clients/" . $clientID);
// Hacer la solicitud GET para obtener los datos del cliente $arrayClientCRM = json_decode($resp->getBody()->getContents(), true);
$response = $clientGuzzleHttp->request('GET', "clients/".$clientID); $clientFullName = $arrayClientCRM['firstName'] . ' ' . $arrayClientCRM['lastName'];
// Obtener el cuerpo de la respuesta como JSON } catch (Exception $e) {
$arrayClientCRM = json_decode($response->getBody()->getContents(), true); return ['hasError' => true, 'data' => $this->buildErrorArray('errorGetClient', 'Error obteniendo cliente', $clientID, $clientFullName, $amount)];
// Inicializamos una variable para guardar el valor del 'stripeCustomerId'
$stripeCustomerId = null;
$clientEmail = '';
// Recorrer los atributos para encontrar el 'stripeCustomerId'
foreach ($arrayClientCRM['attributes'] as $attribute) {
if ($attribute['key'] === 'stripeCustomerId') {
$stripeCustomerId = $attribute['value'];
break;
}
} }
// Recorrer los contactos // --- 2. Obtener Email y Stripe ID ---
$clientEmail = 'siip8873@gmail.com'; // Default
foreach ($arrayClientCRM['contacts'] as $contact) { foreach ($arrayClientCRM['contacts'] as $contact) {
if ($contact['email'] != '') { if (!empty($contact['email'])) { $clientEmail = $contact['email']; break; }
$clientEmail = $contact['email'];
break;
}else{
$clientEmail = 'siip8873@gmail.com';
break;
}
} }
$this->logger->info("Se obtuvieron los datos del cliente y Stripe Customer ID: ".$stripeCustomerId.PHP_EOL); $stripeCustomerId = null;
foreach ($arrayClientCRM['attributes'] as $attr) {
} catch (RequestException $e) { if ($attr['key'] === 'stripeCustomerId') { $stripeCustomerId = $attr['value']; break; }
// Manejo de errores en la petición }
$this->logger->error("Error al obtener atributos personalizados del cliente: " . $e->getMessage().PHP_EOL); if (!$stripeCustomerId) {
return ['hasError' => true, 'data' => $this->buildErrorArray('errorGetCustomerStripe', 'Cliente sin Stripe ID', $clientID, $clientFullName, $amount)];
} }
$amount = abs($arrayClientCRM['accountOutstanding']) * 100; // --- 3. Calcular Monto ---
if ($amount === null) {
$amount = abs($arrayClientCRM['accountOutstanding']);
} else {
if(!is_numeric($amount)) $amount = preg_replace('/[^\d.]/', '', $amount);
}
//$this->logger->info("User Admin ID: " . json_encode($currentUserAdmin) . PHP_EOL); if ($amount <= 10) {
if($amount>0){ return ['hasError' => true, 'data' => $this->buildErrorArray('errorsinadeudo', 'Monto insuficiente o sin deuda', $clientID, $clientFullName, $amount)];
}
// --- 4. Crear Payment Intent en Stripe ---
try { try {
// Crear el PaymentIntent vinculado al cliente $amountInCents = intval($amount * 100);
$paymentIntent = $stripe->paymentIntents->create([ $guzzleClient = new Client(['timeout' => 10]);
'amount' => $amount, // Monto en centavos (300 MXN)
// Payment Intent
$piResp = $guzzleClient->post('https://api.stripe.com/v1/payment_intents', [
'auth' => [$StripeToken, ''],
'form_params' => [
'amount' => $amountInCents,
'currency' => 'mxn', 'currency' => 'mxn',
'payment_method_types' => ['customer_balance', 'card' ,'oxxo'], 'payment_method_types' => ['customer_balance', 'card', 'oxxo'],
'description' => 'Pago de servicio de SIIP Internet', 'customer' => $stripeCustomerId,
'customer' => $stripeCustomerId, // Vincular al cliente existente
'metadata' => [ 'metadata' => [
'clientId' => $clientID, // ID del cliente en UISP 'clientId' => $clientID,
'ucrm_client_id' => $clientID, // Backwards compatibility
'createdBy' => 'UCRM', 'createdBy' => 'UCRM',
'paymentType' => 'card.one_time', 'paymentType' => 'card.one_time',
'signedInAdminId' => $currentUserAdmin[0]['id'] 'signedInAdminId' => $config['idPaymentAdminCRM'] ?? 1,
], 'tipoPago' => 'OXXO'
'payment_method_options' => [
'oxxo' => [
'expires_after_days' => 3, // El voucher expira en 3 días
],
], ],
'payment_method_options' => ['oxxo' => ['expires_after_days' => 3]]
]
]); ]);
$paymentIntent = json_decode($piResp->getBody()->getContents(), true);
// Crear un método de pago OXXO con detalles de facturación // Payment Method
$paymentMethod = $stripe->paymentMethods->create([ $firstName = $arrayClientCRM['firstName'] ?? '';
$lastName = $arrayClientCRM['lastName'] ?? '';
$pmResp = $guzzleClient->post('https://api.stripe.com/v1/payment_methods', [
'auth' => [$StripeToken, ''],
'form_params' => [
'type' => 'oxxo', 'type' => 'oxxo',
'billing_details' => [ 'billing_details' => ['name' => "$firstName $lastName", 'email' => $clientEmail]
'name' => $arrayClientCRM['firstName'].' '.$arrayClientCRM['lastName'], // Proporciona el nombre del cliente ]
'email' => $clientEmail, // Proporciona el correo del cliente
],
]); ]);
$paymentMethod = json_decode($pmResp->getBody()->getContents(), true);
// Confirmar el PaymentIntent con el método de pago OXXO // Confirm
$paymentIntent = $stripe->paymentIntents->confirm( $confResp = $guzzleClient->post('https://api.stripe.com/v1/payment_intents/' . $paymentIntent['id'] . '/confirm', [
$paymentIntent->id, 'auth' => [$StripeToken, ''],
['payment_method' => $paymentMethod->id] 'form_params' => ['payment_method' => $paymentMethod['id']]
); ]);
$paymentIntentConfirm = json_decode($confResp->getBody()->getContents(), true);
// Verificar si hay acciones pendientes (next_action)
if (!empty($paymentIntent->next_action) && isset($paymentIntent->next_action->oxxo_display_details)) {
$oxxoPayment = $paymentIntent->next_action->oxxo_display_details;
//print_r($oxxoPayment);
// Obtener la referencia OXXO y la URL del recibo
$oxxo_reference = $oxxoPayment->number;
$oxxo_receipt_url = $oxxoPayment->hosted_voucher_url;
// Mostrar la referencia y la URL del recibo al cliente
$this->logger->info("Referencia OXXO: " . $oxxo_reference.PHP_EOL);
$this->logger->info("URL del recibo: " . $oxxo_receipt_url.PHP_EOL);
return $oxxo_receipt_url;
// echo "Referencia OXXO: " . $oxxo_reference . "\n";
// echo "URL del recibo: " . $oxxo_receipt_url . "\n";
} else {
// Manejo de error si no hay detalles de OXXO
// echo "El PaymentIntent no tiene detalles de OXXO disponibles aún.\n";
// echo "Estado actual del PaymentIntent: " . $paymentIntent->status . "\n";
$this->logger->info("El PaymentIntent no tiene detalles de OXXO disponibles aún. " .PHP_EOL);
$this->logger->info("Estado actual del PaymentIntent: " . $paymentIntent->status.PHP_EOL);
return 'error';
}
} catch (Exception $e) { } catch (Exception $e) {
// Manejar cualquier error return ['hasError' => true, 'data' => $this->buildErrorArray('errorStripeApi', $e->getMessage(), $clientID, $clientFullName, $amount)];
//echo 'Error: ' . $e->getMessage();
$this->logger->error('Error al crear el payment intent: ' . $e->getMessage().PHP_EOL);
return 'error';
} }
}else{
$this->logger->info("Este cliente no tiene adeudos por lo tanto no se puede genear su referencia de oxxo. " .PHP_EOL); // --- 5. Extraer Datos OXXO ---
return 'errorsinadeudo'; if (!empty($paymentIntentConfirm['next_action']['oxxo_display_details'])) {
$oxxoDetails = $paymentIntentConfirm['next_action']['oxxo_display_details'];
return [
'hasError' => false,
'data' => [
'oxxo_reference' => $oxxoDetails['number'],
'url' => $oxxoDetails['hosted_voucher_url'],
'error' => '',
'clientID' => $clientID,
'clientFullName' => $clientFullName,
'amount' => $amount,
'voucher_image_url' => ''
]
];
}
return ['hasError' => true, 'data' => $this->buildErrorArray('errorNoOxxoDetails', 'No se recibieron detalles OXXO', $clientID, $clientFullName, $amount)];
}
private function buildErrorArray($code, $desc, $clientId, $name, $amount) {
return [
'oxxo_reference' => '',
'url' => '',
'error' => $code,
'failDescription' => $desc,
'clientID' => $clientId,
'clientFullName' => $name,
'amount' => $amount,
'voucher_image_url' => ''
];
}
/**
* Crea la orden en el microservicio (Paso 1 del flujo asíncrono)
*/
public function createOxxoOrder($arrayOxxoPayment) {
$config = PluginConfigManager::create()->loadConfig();
$ipPuppeteer = $config['ipPuppeteer'];
$portPuppeteer = $config['portPuppeteer'];
$baseUrl = 'http://'.$ipPuppeteer.':'.$portPuppeteer;
$guzzleClient = new Client(['timeout' => 10]);
try {
$createOrderResp = $guzzleClient->post($baseUrl . '/orders', [
'json' => [
'client_id' => $arrayOxxoPayment['clientID'],
'amount' => $arrayOxxoPayment['amount'],
'client_full_name' => $arrayOxxoPayment['clientFullName'],
'oxxo_reference' => $arrayOxxoPayment['oxxo_reference'],
'stripe_url' => $arrayOxxoPayment['url']
]
]);
$orderData = json_decode($createOrderResp->getBody()->getContents(), true);
$arrayOxxoPayment['order_id'] = $orderData['order_id'] ?? null;
return $arrayOxxoPayment;
} catch (Exception $e) {
$this->logger->error("Error creando orden OXXO: " . $e->getMessage());
return $arrayOxxoPayment;
}
}
/**
* Genera el voucher y lo sube (Paso 2 del flujo asíncrono)
*/
public function generateOxxoVoucher($arrayOxxoPayment, $uploadToFtp) {
if (empty($arrayOxxoPayment['order_id'])) {
$this->logger->error("Intentando generar voucher sin order_id");
return $arrayOxxoPayment;
}
$config = PluginConfigManager::create()->loadConfig();
$ipPuppeteer = $config['ipPuppeteer'];
$portPuppeteer = $config['portPuppeteer'];
$baseUrl = 'http://'.$ipPuppeteer.':'.$portPuppeteer;
$guzzleClient = new Client(['timeout' => 60]);
$clientFullNameWithoutSpaces = str_replace(' ', '_', $arrayOxxoPayment['clientFullName']);
$voucherFileName = 'voucher_'.$clientFullNameWithoutSpaces.'_' . time() . '.jpeg';
$output_filename = realpath(__DIR__ . '/../../vouchers_oxxo') . '/'. $voucherFileName;
$request_data = [
'url' => $arrayOxxoPayment['url'],
'filename' => $voucherFileName,
'clip' => ['x' => 325, 'y' => 30, 'width' => 550, 'height' => 550]
];
try {
$response = $guzzleClient->post($baseUrl . '/orders/' . $arrayOxxoPayment['order_id'] . '/generate', [
'json' => $request_data,
'headers' => ['Accept' => 'image/jpeg, image/png']
]);
if ($response->getStatusCode() === 200) {
file_put_contents($output_filename, $response->getBody()->getContents());
$arrayOxxoPayment['voucher_filename'] = $voucherFileName;
if ($uploadToFtp) {
$url_file = $this->minioStorage->uploadFile($output_filename, $voucherFileName);
$arrayOxxoPayment['voucher_image_url'] = $url_file;
try {
$guzzleClient->post($baseUrl . '/orders/' . $arrayOxxoPayment['order_id'] . '/complete', [
'json' => ['voucher_image_url' => $url_file]
]);
// CLEANUP: Eliminar voucher local tras subida exitosa
if (file_exists($output_filename)) {
unlink($output_filename);
//$this->logger->info("Limpieza: Voucher local eliminado ($voucherFileName)");
}
} catch (Exception $ex) {
$this->logger->warning("No se pudo actualizar URL final: " . $ex->getMessage());
}
}
}
} catch (Exception $e) {
$this->logger->error("Error generando voucher (Puppeteer): " . $e->getMessage());
}
return $arrayOxxoPayment;
}
/**
* Mantiene compatibilidad con llamadas anteriores
*/
private function processSynchronousGeneration($arrayOxxoPayment, $uploadToFtp) {
$arrayOxxoPayment = $this->createOxxoOrder($arrayOxxoPayment);
if (!empty($arrayOxxoPayment['order_id'])) {
$arrayOxxoPayment = $this->generateOxxoVoucher($arrayOxxoPayment, $uploadToFtp);
}
return $arrayOxxoPayment;
} }
}
// /** // /**
@ -221,7 +502,7 @@ abstract class AbstractOxxoOperationsFacade
function validarEmail($email) function validarEmail($email)
{ {
$this->logger->info('SE VALIDA EL EMAIL!!! '.PHP_EOL); $this->logger->debug('SE VALIDA EL EMAIL!!! ' . PHP_EOL);
// Utilizar la función filter_var con el filtro FILTER_VALIDATE_EMAIL para validar el email // Utilizar la función filter_var con el filtro FILTER_VALIDATE_EMAIL para validar el email
if (filter_var($email, FILTER_VALIDATE_EMAIL)) { if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
// Si el email es válido, devolver el email // Si el email es válido, devolver el email
@ -237,21 +518,18 @@ abstract class AbstractOxxoOperationsFacade
{ {
// $log = PluginLogManager::create(); //Initialize Logger // $log = PluginLogManager::create(); //Initialize Logger
// $configManager = PluginConfigManager::create(); $configManager = PluginConfigManager::create();
// $config = $configManager->loadConfig(); $config = $configManager->loadConfig();
// // Configuración de conexión FTP // Configuración de conexión FTP
// $ftp_server = $config['hostServerFTP']; $ftp_server = $config['hostServerFTP'];
// $ftp_username = $config['usernameServerFTP']; $ftp_username = $config['usernameServerFTP'];
// $ftp_password = $config['passServerFTP']; $ftp_password = $config['passServerFTP'];
$remote_folder = "/public_html/wp/wp-content/uploads/img/"; $remote_folder = "/public_html/wp/wp-content/uploads/img/";
// Configuración de conexión FTP // Configuración de conexión FTP
$ftp_server = "siip.mx";
$ftp_username = "siip0001";
$ftp_password = '$spGiT,[wa)n';
$remote_file = "/public_html/wp/wp-content/uploads/img/" . $imageFileName; $remote_file = "/public_html/wp/wp-content/uploads/img/" . $imageFileName;
$file_to_upload = __DIR__ . '/' . $imageFileName; $file_to_upload = __DIR__ . '/' . $imageFileName;
$url = 'https://siip.mx/wp/wp-content/uploads/img/' . $imageFileName; $url = 'https://siip.mx/wp/wp-content/uploads/img/' . $imageFileName;
@ -339,4 +617,150 @@ abstract class AbstractOxxoOperationsFacade
} }
function UploadVoucherToWordpressByImageFileName($imageFileName): string
{
$log = PluginLogManager::create(); //Initialize Logger
$configManager = PluginConfigManager::create();
$config = $configManager->loadConfig();
// Configuración de conexión FTP
$ftp_server = $config['hostServerFTP'];
$ftp_username = $config['usernameServerFTP'];
$ftp_password = $config['passServerFTP'];
$remote_folder = "/public_html/wp/wp-content/uploads/vouchers_oxxo/";
$log->appendLog("Subiendo voucher a worpdpress " . PHP_EOL);
// Configuración de conexión FTP
$remote_file = "/public_html/wp/wp-content/uploads/vouchers_oxxo/" . $imageFileName;
$file_to_upload = __DIR__ . '/../../vouchers_oxxo/' . $imageFileName;
$url = 'https://siip.mx/wp/wp-content/uploads/vouchers_oxxo/' . $imageFileName;
$log->appendLog("file_to_upload: " . $file_to_upload . PHP_EOL);
// 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);
ftp_pasv($ftp_conn, true);
// Verificar conexión y login
if ($ftp_conn && $login) {
$log->appendLog("Conexión FTP exitosa" . PHP_EOL);
// Cargar archivo
if (ftp_put($ftp_conn, $remote_file, $file_to_upload, FTP_BINARY)) {
$log->appendLog("El archivo ha sido cargado exitosamente." . PHP_EOL);
$log->appendLog("La URL es: " . $url . PHP_EOL);
// try {
// $this->deleteFilesWordpressExceptLastHundred($log, $ftp_conn, $remote_folder);
// } catch (\Exception $e) {
// $log->appendLog("Error en limpieza de archivos antiguos: " . $e->getMessage() . PHP_EOL);
// }
// Cerrar conexión FTP (o dejar abierta si se prefiere, pero mejor cerrar al final si es posible, aunque aquí retornamos directo)
// ftp_close($ftp_conn);
// Cerrar conexión FTP
//ftp_close($ftp_conn); //COMENTAR AQUÍ SI SE BORRAN LOS ARCHIVOS DE WORDPRESS DESCOMENTANDO EL CÓDIGO DE MÁS ABAJO
return $url; //COMENTAR AQUÍ SI SE BORRAN LOS ARCHIVOS DE WORDPRESS DESCOMENTANDO EL CÓDIGO DE MÁS ABAJO
} else {
$log->appendLog("Error al cargar el archivo " . PHP_EOL);
ftp_close($ftp_conn);
return '';
}
//SI SE DECIDE VOLVER A ELIMINAR LOS COMPROBANTES ENTONCES DESCOMENTAR ESTA PARTE DE ABAJO Y COMENTAR LA SECCIÓN DE ARRIBA
// Obtener lista de archivos en la carpeta
// $files = ftp_nlist($ftp_conn, $remote_folder);
// if (is_array($files)) {
// // Eliminar la ruta del directorio de los archivos
// $files = array_map(function ($file) use ($remote_folder) {
// return str_replace($remote_folder, '', $file);
// }, $files);
// // Obtener fechas de modificación
// $filesWithTime = [];
// foreach ($files as $file) {
// $modifiedTime = ftp_mdtm($ftp_conn, $remote_folder . $file);
// if ($modifiedTime != -1) {
// $filesWithTime[$file] = $modifiedTime;
// }
// }
// // Ordenar archivos por fecha de modificación, más recientes primero
// arsort($filesWithTime);
// // Obtener los archivos a eliminar (todos menos los 50 más recientes)
// $filesToDelete = array_slice(array_keys($filesWithTime), 50);
// // Eliminar archivos antiguos
// foreach ($filesToDelete as $file) {
// if (ftp_delete($ftp_conn, $remote_folder . $file)) {
// $log->appendLog("Comprobante eliminado de Wordpress: " . $file . PHP_EOL);
// } else {
// $log->appendLog('Error al borrar comprobante' . $file . PHP_EOL);
// }
// }
// $log->appendLog("Archivos eliminados" . PHP_EOL);
// ftp_close($ftp_conn);
// return $url;
// } else {
// $log->appendLog("No se pudo obtener la lista de archivos de la carpeta FTP" . PHP_EOL);
// ftp_close($ftp_conn);
// return $url;
// }
} else {
$log->appendLog("No se pudo conectar o iniciar sesión en el servidor FTP." . PHP_EOL);
return '';
}
}
function deleteFilesWordpressExceptLastHundred($log, $ftp_conn, $remote_folder): bool
{
// Obtener lista de archivos en la carpeta
$files = ftp_nlist($ftp_conn, $remote_folder);
if (is_array($files)) {
// Eliminar la ruta del directorio de los archivos
$files = array_map(function ($file) use ($remote_folder) {
return str_replace($remote_folder, '', $file);
}, $files);
// Obtener fechas de modificación
$filesWithTime = [];
foreach ($files as $file) {
$modifiedTime = ftp_mdtm($ftp_conn, $remote_folder . $file);
if ($modifiedTime != -1) {
$filesWithTime[$file] = $modifiedTime;
}
}
// Ordenar archivos por fecha de modificación, más recientes primero
arsort($filesWithTime);
// Obtener los archivos a eliminar (todos menos los 50 más recientes)
$filesToDelete = array_slice(array_keys($filesWithTime), 50);
// Eliminar archivos antiguos
foreach ($filesToDelete as $file) {
if (ftp_delete($ftp_conn, $remote_folder . $file)) {
$log->appendLog("Voucher eliminado de Wordpress: " . $file . PHP_EOL);
} else {
$log->appendLog('Error al borrar voucher' . $file . PHP_EOL);
}
}
$log->appendLog("Archivos eliminados" . PHP_EOL);
ftp_close($ftp_conn);
return true;
} else {
$log->appendLog("No se pudo obtener la lista de archivos de la carpeta FTP" . PHP_EOL);
ftp_close($ftp_conn);
return false;
}
}
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,13 @@ use SmsNotifier\Service\Logger;
use SmsNotifier\Service\OptionsManager; use SmsNotifier\Service\OptionsManager;
use SmsNotifier\Service\SmsNumberProvider; use SmsNotifier\Service\SmsNumberProvider;
use Twilio\Rest\Client; use Twilio\Rest\Client;
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\GuzzleException;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
class PluginNotifierFacade extends AbstractStripeOperationsFacade class PluginNotifierFacade extends AbstractStripeOperationsFacade
{ {
/** /**
@ -36,16 +41,99 @@ class PluginNotifierFacade extends AbstractStripeOperationsFacade
$this->pluginData = $optionsManager->load(); $this->pluginData = $optionsManager->load();
} }
public function updatePasswordAntenaIfNeeded(int $clientId, array $jsonData): void
{
$this->logger->info("Iniciando verificación/sincronización de contraseña para el cliente ID: $clientId");
// El método base getVaultCredentialsByClientId ya incluye la lógica para:
// 1. Obtener la contraseña desde el Vault de UISP (iterando sobre dispositivos).
// 2. Compararla con el atributo 'passwordAntenaCliente' del CRM.
// 3. Actualizar el CRM si está vacío o si es diferente.
$this->getVaultCredentialsByClientId($clientId);
}
/**
* Procesar la lógica de corrección de contraseña cuando se detecta la etiqueta "OBTENER PASSWORD ANTENA".
* Reutiliza la lógica del script audit_client_passwords.php.
*/
public function processClientPasswordAntenna(int $clientId, array $clientEntity): void
{
$this->logger->info("Procesando etiqueta OBTENER PASSWORD ANTENA para cliente $clientId");
// 1. Define constant to prevent script execution when including
if (!defined('INCLUDED_AS_LIBRARY')) {
define('INCLUDED_AS_LIBRARY', true);
}
// 2. Include the script functions
$scriptPath = __DIR__ . '/../../scripts-uisp/audit_client_passwords.php';
if (file_exists($scriptPath)) {
require_once $scriptPath;
} else {
$this->logger->error("No se encontró el script de auditoría en: $scriptPath");
return;
}
// 3. Initialize Config & UNMS Client
// Note: $this->ucrmApi is already available from parent class.
$ucrmConfig = PluginConfigManager::create()->loadConfig();
$ipServer = $ucrmConfig['ipserver'] ?? 'localhost';
$unmsToken = $ucrmConfig['unmsApiToken'] ?? $this->pluginData->unmsAppToken ?? '';
if (empty($unmsToken)) {
$this->logger->error("UNMS API Token is missing. Cannot proceed with password fix.");
return;
}
$unmsClientInstance = new GuzzleClient([
'base_uri' => "https://{$ipServer}/nms/api/v2.1/",
'verify' => false,
'headers' => [
'X-Auth-Token' => $unmsToken
]
]);
// 4. Resolve Attributes
// Use global function from script
$attributeIds = \resolveAttributeIds($this->ucrmApi, 'passwordAntenaCliente', 'site', 'antenaSectorial');
if (!$attributeIds) {
$this->logger->error("Could not resolve attributes for password fix.");
return;
}
// 5. Get current password
$currentPassword = null;
foreach ($clientEntity['attributes'] ?? [] as $attr) {
if ($attr['key'] === 'passwordAntenaCliente') {
$currentPassword = $attr['value'];
break;
}
}
// 6. Fix Data
try {
$fixResult = \fixClientData($clientId, $this->ucrmApi, $unmsClientInstance, $attributeIds, $ucrmConfig, $currentPassword);
$this->logger->info("Password fix result for client $clientId: " . json_encode($fixResult));
} catch (\Exception $e) {
$this->logger->error("Error executing fixClientData: " . $e->getMessage());
}
// 7. Remove Tag
// Reuse parent method
$this->removeTagFromClient($clientId, 'OBTENER PASSWORD ANTENA');
}
/* /*
* Send WhatsApp through the CallBell API * Implementation of abstract method from AbstractStripeOperationsFacade
*/ */
protected function sendWhatsApp(
string $message,
): void {
$this->logger->debug('Enviando mensaje desde twilio notifier facade: '.$message);
/*
* Implementation of abstract method from AbstractStripeOperationsFacade
*/
protected function sendWhatsApp(NotificationData $notificationData, string $clientPhoneNumber): void
{
$this->logger->info("PluginNotifierFacade: Enviando WhatsApp (Mock): " . $clientPhoneNumber);
} }
/* /*
@ -53,6 +141,6 @@ class PluginNotifierFacade extends AbstractStripeOperationsFacade
*/ */
private function getSenderNumber(): string private function getSenderNumber(): string
{ {
return $this->pluginData->twilioSmsNumber; return $this->pluginData->twilioSmsNumber ?? '';
} }
} }

View File

@ -29,9 +29,10 @@ class PluginOxxoNotifierFacade extends AbstractOxxoOperationsFacade
Logger $logger, Logger $logger,
MessageTextFactory $messageTextFactory, MessageTextFactory $messageTextFactory,
SmsNumberProvider $smsNumberProvider, SmsNumberProvider $smsNumberProvider,
OptionsManager $optionsManager OptionsManager $optionsManager,
\SmsNotifier\Service\MinioStorageService $minioStorage
) { ) {
parent::__construct($logger, $messageTextFactory, $smsNumberProvider); parent::__construct($logger, $messageTextFactory, $smsNumberProvider, $minioStorage);
// load config data // load config data
$this->pluginData = $optionsManager->load(); $this->pluginData = $optionsManager->load();
} }

View File

@ -11,18 +11,10 @@ use SmsNotifier\Service\Logger;
use SmsNotifier\Service\OptionsManager; use SmsNotifier\Service\OptionsManager;
use SmsNotifier\Service\SmsNumberProvider; use SmsNotifier\Service\SmsNumberProvider;
use Twilio\Rest\Client; use Twilio\Rest\Client;
use SmsNotifier\Facade\ClientCallBellAPI;
class TwilioNotifierFacade extends AbstractMessageNotifierFacade class TwilioNotifierFacade extends AbstractMessageNotifierFacade
{ {
/**
* @var Client
*/
private $twilioClient; private $twilioClient;
/**
* @var PluginData
*/
private $pluginData; private $pluginData;
public function __construct( public function __construct(
@ -32,62 +24,41 @@ class TwilioNotifierFacade extends AbstractMessageNotifierFacade
OptionsManager $optionsManager OptionsManager $optionsManager
) { ) {
parent::__construct($logger, $messageTextFactory, $smsNumberProvider); parent::__construct($logger, $messageTextFactory, $smsNumberProvider);
// load config data
$this->pluginData = $optionsManager->load(); $this->pluginData = $optionsManager->load();
} }
/*
* Get Twilio SMS API object (unless it's already initialized)
*/
public function getTwilioClient(): Client public function getTwilioClient(): Client
{ {
if (!$this->twilioClient) { if (!$this->twilioClient) {
$this->twilioClient = new Client( $this->twilioClient = new Client(
$this->pluginData->token_callbell $this->pluginData->twilioAccountSid ?? '',
$this->pluginData->twilioAuthToken ?? ''
); );
} }
return $this->twilioClient; return $this->twilioClient;
} }
/*
* Send WhatsApp through the CallBell API
*/
protected function sendWhatsApp(
string $message,
): void {
$this->logger->debug('Enviando mensaje desde twilio notifier facade: '.$message);
protected function sendWhatsApp(NotificationData $notificationData, string $clientSmsNumber): void
{
$this->logger->info("TwilioNotifierFacade: Enviando WhatsApp a $clientSmsNumber (Mock)");
} }
/*
* Send message through the Twilio client
*/
protected function sendMessage( protected function sendMessage(
NotificationData $notificationData, NotificationData $notificationData,
string $clientSmsNumber, string $clientSmsNumber,
string $messageBody string $messageBody
): void { ): void {
$this->logger->debug(sprintf('Sending: %s', $messageBody)); try {
$message = $this->getTwilioClient()->messages->create(
$messageInstance = $this->getTwilioClient()->messages->create(
$clientSmsNumber, $clientSmsNumber,
[ [
'from' => $this->getSenderNumber(), 'from' => $this->pluginData->twilioSmsNumber ?? '',
'body' => $messageBody, 'body' => $messageBody,
] ]
); );
$this->logger->info(sprintf('Twilio status: %s, ID: %s', $message->status, $message->sid));
$this->logger->debug((string) $messageInstance); } catch (\Exception $e) {
$this->logger->info(sprintf('Twilio status: %s, message id: %s', $messageInstance->status, $messageInstance->sid)); $this->logger->error("Error en Twilio: " . $e->getMessage());
if ($messageInstance->errorCode) {
$this->logger->warning(sprintf('Twilio error: %s %s', $messageInstance->errorCode, $messageInstance->errorMessage));
} }
} }
/*
* Phone number of sender - required by Twilio. In this plugin, we only load it from config.
*/
private function getSenderNumber(): string
{
return $this->pluginData->twilioSmsNumber;
}
} }

View File

@ -37,8 +37,10 @@ class NotificationDataFactory
$notificationData->eventName = $jsonData['eventName']; $notificationData->eventName = $jsonData['eventName'];
$notificationData->message = $jsonData['extraData']['message'] ?? null; $notificationData->message = $jsonData['extraData']['message'] ?? null;
// Check if the given webhook exists. // Check if the given webhook exists. Skip for manual triggers.
if ($notificationData->uuid !== 'manual-trigger') {
$this->ucrmApi->query('webhook-events/' . $notificationData->uuid); $this->ucrmApi->query('webhook-events/' . $notificationData->uuid);
}
$this->resolveUcrmData($notificationData); $this->resolveUcrmData($notificationData);

View File

@ -2,18 +2,18 @@
declare(strict_types=1); declare(strict_types=1);
namespace SmsNotifier; namespace SmsNotifier;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
use SmsNotifier\Facade\TwilioNotifierFacade;
use SmsNotifier\Facade\PluginNotifierFacade; use SmsNotifier\Facade\PluginNotifierFacade;
use SmsNotifier\Facade\PluginOxxoNotifierFacade; use SmsNotifier\Facade\PluginOxxoNotifierFacade;
use SmsNotifier\Facade\TwilioNotifierFacade;
use SmsNotifier\Factory\NotificationDataFactory; use SmsNotifier\Factory\NotificationDataFactory;
use SmsNotifier\Service\Logger; use SmsNotifier\Service\Logger;
use SmsNotifier\Service\OptionsManager; use SmsNotifier\Service\OptionsManager;
use SmsNotifier\Service\PluginDataValidator; use SmsNotifier\Service\PluginDataValidator;
use Twilio\Exceptions\TwilioException; use Twilio\Exceptions\TwilioException;
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
class Plugin class Plugin
{ {
@ -50,6 +50,11 @@ class Plugin
*/ */
private $notificationDataFactory; private $notificationDataFactory;
/**
* @var UcrmApi
*/
protected $ucrmApi;
public function __construct( public function __construct(
Logger $logger, Logger $logger,
OptionsManager $optionsManager, OptionsManager $optionsManager,
@ -66,24 +71,26 @@ class Plugin
$this->pluginNotifierFacade = $pluginNotifierFacade; $this->pluginNotifierFacade = $pluginNotifierFacade;
$this->pluginOxxoNotifierFacade = $pluginOxxoNotifierFacade; $this->pluginOxxoNotifierFacade = $pluginOxxoNotifierFacade;
$this->notificationDataFactory = $notificationDataFactory; $this->notificationDataFactory = $notificationDataFactory;
$config = \Ubnt\UcrmPluginSdk\Service\PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? 'localhost';
$apiUrl = "https://$ipServer/crm/api/v1.0/";
$client = new \GuzzleHttp\Client([
'base_uri' => $apiUrl,
'verify' => false,
]);
$this->ucrmApi = new UcrmApi($client, $config['apitoken'] ?? '');
} }
public function run(): void public function run(): void
{ {
// $hola = PHP_SAPI; if (PHP_SAPI === 'fpm-fcgi' || PHP_SAPI === 'cgi-fcgi' || PHP_SAPI === 'apache2handler') {
// $this->logger->info('valor de PHP_SAPI: ' . $hola);
if (PHP_SAPI === 'fpm-fcgi') {
$this->logger->debug('Whatsapp over HTTP started');
$this->processHttpRequest(); $this->processHttpRequest();
$this->logger->debug('HTTP request processing ended.');
} elseif (PHP_SAPI === 'cli') { } elseif (PHP_SAPI === 'cli') {
$this->logger->debug('Whatsapp over CLI started');
$this->processCli(); $this->processCli();
$this->logger->debug('CLI process ended.');
} else { } else {
throw new \UnexpectedValueException('Unknown PHP_SAPI type: ' . PHP_SAPI); $this->logger->error('SAPI desconocido: ' . PHP_SAPI . '. Intentando procesar como HTTP.');
$this->processHttpRequest();
} }
} }
private function processCli(): void private function processCli(): void
@ -97,97 +104,189 @@ class Plugin
private function processHttpRequest(): void private function processHttpRequest(): void
{ {
$pluginData = $this->optionsManager->load(); $pluginData = $this->optionsManager->load();
if ($pluginData->logging_level) { if ($pluginData->logging_level || $pluginData->debugMode) {
$this->logger->setLogLevelThreshold(LogLevel::DEBUG); $this->logger->setLogLevelThreshold(LogLevel::DEBUG);
} }
$userInput = file_get_contents('php://input'); $userInput = file_get_contents('php://input');
// DEBUG TEMPORAL
$this->logger->debug('Payload recibido: ' . $userInput . PHP_EOL);
if (!$userInput) { if (! $userInput) {
$this->logger->warning('no input'); $this->logger->warning('No se recibió input en la petición HTTP.');
return; return;
} }
$event_json = json_decode($userInput);
$webhook_string = json_encode($event_json); $jsonData = @json_decode((string)$userInput, true, 50);
$this->logger->debug("El valor del evento recibio en json: " . $webhook_string . PHP_EOL);
// Maneja el evento del webhook if (! isset($jsonData['uuid'])) {
if ($event_json) { $this->logger->info('No UUID found in the webhook data');
switch ($event_json->type) { //$this->logger->error('JSON error: ' . json_last_error_msg());
//return;
// Maneja el evento del webhook externo
if ($jsonData) {
switch ($jsonData['type']) {
case 'customer_cash_balance_transaction.created': case 'customer_cash_balance_transaction.created':
$this->logger->info('Evento de transfencia al cliente encontrado'); if ($jsonData['data']['object']['type'] === 'funded') {
$this->logger->info('Valor del EventJSON: ' . json_encode($event_json) . PHP_EOL); $this->logger->info('Evento de transferencia de un cliente recibido: ' . json_encode($jsonData) . PHP_EOL);
$this->pluginNotifierFacade->createPaymentIntent($event_json); $this->pluginNotifierFacade->createPaymentIntent($jsonData);
}
if ($jsonData['data']['object']['type'] === 'applied_to_payment') {
$this->logger->info('Se aplicó el saldo en Stripe de un pago: ' . json_encode($jsonData) . PHP_EOL);
$this->pluginNotifierFacade->registerPaymentFromWebhook($jsonData);
} elseif ($jsonData['data']['object']['type'] === 'unapplied_from_payment') {
//ejemplo de json para transferencia de dinero cancelada: {"id":"evt_1RlEGgEFY1WEUtgR6Bp2DzDP","object":"event","api_version":"2023-10-16","created":1752606717,"data":{"object":{"id":"ccsbtxn_1RlEGfEFY1WEUtgRv8jAUGmE","object":"customer_cash_balance_transaction","created":1752606717,"currency":"mxn","customer":"cus_PetN1dhr4rx0kX","ending_balance":18000,"livemode":false,"net_amount":18000,"type":"unapplied_from_payment","unapplied_from_payment":{"payment_intent":"pi_3RlDPdEFY1WEUtgR1JBgNhTQ"}}},"livemode":false,"pending_webhooks":2,"request":{"id":"req_954mskVBfAI0jn","idempotency_key":"749518f6-baa0-4ae9-99e4-8029a35719aa"},"type":"customer_cash_balance_transaction.created"}
$paymentIntentId = $jsonData['data']['object']['unapplied_from_payment']['payment_intent'];
//Se canceló una transferencia de dinero, imprimir que se canceló y además el monto neto
$this->logger->warning('Evento de transferencia cancelada para el pago: ' . $paymentIntentId . PHP_EOL);
$this->logger->warning('Monto neto de la transferencia cancelada: ' . $jsonData['data']['object']['net_amount'] . PHP_EOL);
}
break;
case 'payout.failed':
$this->logger->info('Evento de transferencia fallida encontrado: ' . json_encode($jsonData) . PHP_EOL);
//imprimir detalles del fallo
$this->logger->info('Detalles del fallo: ' . json_encode($jsonData));
break;
case 'payment_intent.partially_funded':
$this->logger->info('Evento de pago parcialmente financiado encontrado: ' . json_encode($jsonData) . PHP_EOL);
//imprimir detalles del evento o pago
$this->logger->info('Detalles del evento: ' . json_encode($jsonData));
break; break;
case 'inbound_payment.payment_attempt': case 'inbound_payment.payment_attempt':
$this->logger->info('Evento de Pagode OXXO recibido'); //$this->logger->info('Evento de Pago de OXXO recibido: ' . json_encode($jsonData) . PHP_EOL);
$this->logger->info('Valor del EventJSON: ' . json_encode($event_json) . PHP_EOL);
break; break;
case 'energy.alert': case 'cash_balance.funds_available':
$this->logger->info('Evento de Energía recibido: ' . $event_json->message . PHP_EOL); $this->logger->info('Evento de Pago de fondos disponibles recibido: ' . json_encode($jsonData) . PHP_EOL);
break;
case 'payment_intent.succeeded':
$this->logger->info('Evento de pago exitoso (Stripe PI) recibido.');
if (isset($jsonData['data']['object'])) {
$this->pluginNotifierFacade->registerPaymentFromIntent($jsonData['data']['object']);
}
break;
case 'oxxo.retrieve':
$this->logger->info('Evento de recuperación de orden OXXO recibido');
$orderId = $jsonData['order_id'] ?? null;
if ($orderId) {
$status = $this->pluginOxxoNotifierFacade->getOxxoOrderStatus($orderId);
$this->logger->debug("Estado recuperado para Orden #$orderId: " . json_encode($status));
header('Content-Type: application/json');
echo json_encode($status);
} else {
$this->logger->warning('Solicitud de oxxo.retrieve sin order_id');
echo json_encode(['error' => 'missing_order_id']);
}
return; // Terminar ejecución aquí
break; break;
case 'oxxo.request': case 'oxxo.request':
$this->logger->info('Evento de referencia de oxxo recibido: ' . PHP_EOL); $this->logger->info('Evento de solicitud de referencia de OXXO recibido (Async Flow)');
// Construir la URL basada en el "client_id" // 1. Obtener datos de Stripe (OXXO Reference, URL, etc.)
// $url = "https://siip.mx/wp/wp-content/uploads/img/voucher.png"; $stripeResult = $this->pluginOxxoNotifierFacade->createStripeReference($jsonData, $jsonData['amount'] ?? null);
$url = $this->pluginOxxoNotifierFacade->createOxxoPaymentIntent($event_json);
// Crear una respuesta en formato JSON
// $response = [
// 'event' => 'response.siip',
// 'status' => 'success',
// 'url' => $url
// ];
$response = '{' . if ($stripeResult['hasError'] ?? false) {
'"url": "' . $url . '"' . $this->logger->error('Error generando referencia Stripe: ' . json_encode($stripeResult));
'}';
$this->logger->debug('Este ese el reponse que se envía a CallBell: ' . $response);
// $json_codificado = json_encode($response);
// if (json_last_error() !== JSON_ERROR_NONE) {
// $this->logger->error('Error en la codificación JSON: ' . json_last_error_msg() . PHP_EOL);
// }
//$this->logger->info('Se está enviando esta respuesta: ' . json_encode($json_codificado) . PHP_EOL);
// Enviar el encabezado de respuesta como JSON
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode([
'oxxo_reference' => '',
'url' => '',
'error' => $stripeResult['data']['error'] ?? 'stripe_error',
'status' => 'failed'
]);
exit;
}
// Enviar la respuesta en formato JSON $oxxoData = $stripeResult['data'];
//echo json_encode($response);
echo $response;
// 2. Crear Orden (Sync) para obtener ID
// Ahora pasamos $oxxoData que SI tiene clientID, clientFullName, etc.
$responseOxxo = $this->pluginOxxoNotifierFacade->createOxxoOrder($oxxoData);
$orderId = $responseOxxo['order_id'] ?? null;
// 3. Preparar respuesta inmediata (Pending)
$responseArray = [
'oxxo_reference' => $responseOxxo['oxxo_reference'] ?? '',
'url' => '', // No hay URL de imagen todavía
'stripe_url' => $responseOxxo['url'] ?? '',
'voucher_image_url' => '',
'order_id' => $orderId,
'clientFullName' => $responseOxxo['clientFullName'] ?? '',
'clientID' => $responseOxxo['clientID'] ?? '',
'amount' => $responseOxxo['amount'] ?? '',
'error' => $responseOxxo['error'] ?? '',
'status' => 'generating' // Estado esperado por el bot para iniciar polling
];
// 4. Enviar Respuesta y CERRAR Conexión
// Limpiamos buffers
while (ob_get_level() > 0) {
ob_end_clean();
}
header('Content-Type: application/json');
header('Connection: close');
ignore_user_abort(true); // Permitir que el script siga corriendo
echo json_encode($responseArray);
// Forzar envío al cliente
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} else {
// Fallback para servidores no FastCGI
header('Content-Length: ' . strlen(json_encode($responseArray)));
flush();
}
// 5. Trabajo en Pesado (Background)
if ($orderId) {
$this->logger->info("Iniciando generación background para orden #$orderId");
set_time_limit(180); // 3 minutos para Puppeteer
sleep(1); // Pequeña pausa para asegurar liberación del socket
$this->pluginOxxoNotifierFacade->generateOxxoVoucher($responseOxxo, true);
$this->logger->info("Generación background finalizada para orden #$orderId");
} else {
$this->logger->error("No se pudo iniciar background job: Falta order_id");
}
// Terminar proceso hijo
exit;
break; break;
// Otros eventos relevantes
} }
} }
$jsonData = @json_decode($userInput, true, 10);
if (!isset($jsonData['uuid'])) {
$this->logger->error('JSON error: ' . json_last_error_msg());
return; return;
} }
$notification = $this->notificationDataFactory->getObject($jsonData); $notification = $this->notificationDataFactory->getObject($jsonData);
$this->logger->debug('valor el evento recibido por webhook: ' . $notification->eventName . PHP_EOL); $this->logger->debug('Evento recibido: ' . $notification->eventName);
if ($notification->changeType === 'test') { if ($notification->changeType === 'test') {
$configManager = \Ubnt\UcrmPluginSdk\Service\PluginConfigManager::create();
$config = $configManager->loadConfig();
$this->logger->info('Webhook test successful.'); $this->logger->info('Webhook test successful.');
return; return;
} else if ($notification->changeType === 'paperless.update') {
//imprimir el webhook json
$this->logger->info('Webhook de paperless update: ' . json_encode($jsonData) . PHP_EOL);
} }
if (!$notification->clientId) { // if (!$notification->clientId) {
$this->logger->warning('No client specified, cannot notify them.'); // $this->logger->warning('No client specified, cannot notify them.');
return; // return;
} // }
$configManager = \Ubnt\UcrmPluginSdk\Service\PluginConfigManager::create(); $configManager = \Ubnt\UcrmPluginSdk\Service\PluginConfigManager::create();
$config = $configManager->loadConfig(); $config = $configManager->loadConfig();
@ -198,100 +297,239 @@ class Plugin
if ($notification->eventName === 'payment.add') { if ($notification->eventName === 'payment.add') {
$result = json_encode($notification); $result = json_encode($notification);
$this->logger->debug('datos del notification para el invoice add:' . $result . PHP_EOL); $this->logger->debug('Notification encodificado en JSON:' . $result . PHP_EOL);
// [MOVED] Attempt to patch method ID only if it comes as "Stripe Credit Card" (catch-all)
//$this->pluginNotifierFacade->ensureStripePaymentAttribute($notification); (Removed from top)
$datos_payment = $notification->paymentData;
$this->logger->debug('valor del payment data: ' . json_encode($datos_payment) . PHP_EOL);
$payment_method_id = $notification->paymentData['methodId']; $payment_method_id = $notification->paymentData['methodId'];
//$this->logger->debug('Metodo de pago: ' . $notification->paymentData['methodId'] . PHP_EOL);
$payment_method = ''; $payment_method = '';
switch ($payment_method_id) { switch ($payment_method_id) {
case '11721cdf-a498-48be-903e-daa67552e4f6': case '11721cdf-a498-48be-903e-daa67552e4f6':
$payment_method = 'Cheque'; $payment_method = 'Cheque';
if ($config['checkPaymentMethodId']) { if ($config['checkPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification); $this->notifierFacade->verifyPaymentActionToDo($notification);
} }
break; break;
case '6efe0fa8-36b2-4dd1-b049-427bffc7d369': case '6efe0fa8-36b2-4dd1-b049-427bffc7d369':
$payment_method = 'Efectivo'; $payment_method = 'Efectivo';
if ($config['cashPaymentMethodId']) { if ($config['cashPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification); $this->notifierFacade->verifyPaymentActionToDo($notification);
} }
break; break;
case '4145b5f5-3bbc-45e3-8fc5-9cda970c62fb': case '4145b5f5-3bbc-45e3-8fc5-9cda970c62fb':
$payment_method = 'Transferencia bancaria'; $payment_method = 'Transferencia bancaria';
if ($config['bankTransferPaymentMethodId']) { if ($config['bankTransferPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification); $this->notifierFacade->verifyPaymentActionToDo($notification);
} }
break; break;
case '78e84000-9b5b-44a4-8367-da43df86ce34': case '78e84000-9b5b-44a4-8367-da43df86ce34':
$payment_method = 'PayPal'; $payment_method = 'PayPal';
if ($config['paypalPaymentMethodId']) { if ($config['paypalPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification); $this->notifierFacade->verifyPaymentActionToDo($notification);
} }
break; break;
case '6da98bb9-6df7-4c41-8608-5cdd7fde7d5d': case '6da98bb9-6df7-4c41-8608-5cdd7fde7d5d':
$payment_method = 'Tarjeta de crédito PayPal'; $payment_method = 'Tarjeta de crédito PayPal';
if ($config['creditCardPaypalPaymentMethodId']) { if ($config['creditCardPaypalPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification); $this->notifierFacade->verifyPaymentActionToDo($notification);
} }
break; break;
case '1dd098fa-5d63-4c8d-88b7-3c27ffbbb6ae': case '1dd098fa-5d63-4c8d-88b7-3c27ffbbb6ae':
$payment_method = 'Tarjeta de crédito Stripe'; // [NEW] Logic to patch method ID based on metadata
if ($config['creditCardStripePaymentMethodId']) { $this->pluginNotifierFacade->ensureStripePaymentAttribute($notification);
// Check if Method ID was updated in memory to OXXO or Transfer
$patchedMethodId = $notification->paymentData['methodId'];
if ($patchedMethodId === 'b01c0b35-b42c-48d9-9ad9-ea6591adfbbb') {
// It is OXXO Pay
$payment_method = 'OXXO Pay';
if ($config['oxxoPayPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification); $this->notifierFacade->verifyPaymentActionToDo($notification);
} }
} elseif ($patchedMethodId === '4145b5f5-3bbc-45e3-8fc5-9cda970c62fb') {
// It is Bank Transfer
$payment_method = 'Transferencia bancaria';
if ($config['bankTransferPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification);
}
} else {
// Default: Credit Card Stripe
$payment_method = 'Tarjeta de crédito Stripe';
if ($config['creditCardStripePaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification);
}
}
break; break;
case 'b9e1e9d1-5c7b-41d2-b6b2-3e568d700290': case 'b9e1e9d1-5c7b-41d2-b6b2-3e568d700290':
$payment_method = 'Suscripción de Stripe (tarjeta de crédito)'; $payment_method = 'Suscripción de Stripe (tarjeta de crédito)';
if ($config['stripeSubscriptionCreditCardPaymentMethodId']) { if ($config['stripeSubscriptionCreditCardPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification); $this->notifierFacade->verifyPaymentActionToDo($notification);
} }
break; break;
case '939f7701-00b7-4676-9b1e-17afb268c8ba': case '939f7701-00b7-4676-9b1e-17afb268c8ba':
$payment_method = 'Suscripción de PayPal'; $payment_method = 'Suscripción de PayPal';
if ($config['paypalSubscriptionPaymentMethodId']) { if ($config['paypalSubscriptionPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification); $this->notifierFacade->verifyPaymentActionToDo($notification);
} }
break; break;
case '1c963e35-df24-444d-95d2-12592d5107e8': case '1c963e35-df24-444d-95d2-12592d5107e8':
$payment_method = 'MercadoPago'; $payment_method = 'MercadoPago';
if ($config['mercadopagoPaymentMethodId']) { if ($config['mercadopagoPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification); $this->notifierFacade->verifyPaymentActionToDo($notification);
} }
break; break;
case 'd8c1eae9-d41d-479f-aeaf-38497975d7b3': case 'd8c1eae9-d41d-479f-aeaf-38497975d7b3':
$payment_method = 'Personalizado'; $payment_method = 'Personalizado';
if ($config['customPaymentMethodId']) { if ($config['customPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification); $this->notifierFacade->verifyPaymentActionToDo($notification);
} }
break; break;
case '72271b72-5c0a-45e2-94d1-cdf4d7cf10e2': case '72271b72-5c0a-45e2-94d1-cdf4d7cf10e2':
$payment_method = 'Cortesía'; $payment_method = 'Cortesía';
if ($config['courtesyPaymentMethodId']) { if ($config['courtesyPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification);
}
break;
case 'b01c0b35-b42c-48d9-9ad9-ea6591adfbbb':
$payment_method = 'OXXO Pay';
if ($config['oxxoPayPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification);
}
break;
case '4145b5f5-3bbc-45e3-8fc5-9cda970c62fb':
$payment_method = 'Transferencia bancaria';
if ($config['bankTransferPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification);
}
break;
case '93814765-66a1-4c7d-a777-05c18fd6aab3':
$payment_method = 'Tarjeta de crédito/débito';
if ($config['creditDebitCardPaymentMethodId'] ?? false) {
$this->notifierFacade->verifyPaymentActionToDo($notification); $this->notifierFacade->verifyPaymentActionToDo($notification);
} }
break; break;
default: default:
$payment_method = 'Desconocido, revisar metodos de pago no contemplados'; $payment_method = 'Desconocido';
break; break;
}
} else if ($notification->eventName === 'client.edit') {
$this->logger->info('Procesando evento client.edit para entityId: ' . ($jsonData['entityId'] ?? 'unknown'));
$this->logger->debug('Payload completo client.edit: ' . json_encode($jsonData));
try {
$clientID = $jsonData['entityId'];
if (!$this->pluginNotifierFacade) {
$this->logger->error('Falla crítica: pluginNotifierFacade no está inicializado.');
} else {
$this->logger->info('Llamando a updatePasswordAntenaIfNeeded para cliente: ' . $clientID);
$this->pluginNotifierFacade->updatePasswordAntenaIfNeeded((int)$clientID, $jsonData);
// $this->lcdf4d7cf10e2ogger->info('Llamada finalizada exitosamente.');
}
} catch (\Throwable $e) {
$this->logger->error('ERROR FATAL procesando client.edit: ' . $e->getMessage());
$this->logger->error('Trace: ' . $e->getTraceAsString());
} }
//ejemplo de json_data: {"uuid":"17e043a7-03b5-4312-ab81-a7818124a77e","changeType":"edit","entity":"client","entityId":"158","eventName":"client.edit","extraData":{"entity":{"id":158,"userIdent":null,"previousIsp":null,"isLead":false,"clientType":1,"companyName":null,"companyRegistrationNumber":null,"companyTaxId":null,"companyWebsite":null,"street1":"23 San Luis","street2":null,"city":"Dolores Hidalgo Cuna de la Independencia Nacional","countryId":173,"stateId":null,"zipCode":"37804","fullAddress":"San Luis 23, Guadalupe, Dolores Hidalgo Cuna de la Independencia Nacional, Gto., M\u00e9xico","invoiceStreet1":null,"invoiceStreet2":null,"invoiceCity":null,"invoiceStateId":null,"invoiceCountryId":null,"invoiceZipCode":null,"invoiceAddressSameAsContact":true,"note":null,"sendInvoiceByPost":null,"invoiceMaturityDays":null,"stopServiceDue":null,"stopServiceDueDays":null,"organizationId":1,"tax1Id":null,"tax2Id":null,"tax3Id":null,"registrationDate":"2025-01-06T00:00:00-0600","leadConvertedAt":"2025-02-09T03:15:49-0600","companyContactFirstName":null,"companyContactLastName":null,"isActive":false,"firstName":"Luis","lastName":"Guti\u00e9rrez","username":null,"contacts":[{"id":162,"clientId":158,"email":null,"phone":null,"name":null,"isBilling":true,"isContact":true,"types":[{"id":1,"name":"Billing"},{"id":2,"name":"General"}]}],"attributes":[],"accountBalance":0,"accountCredit":0,"accountOutstanding":0,"currencyCode":"MXN","organizationName":"SIIP Pruebas","bankAccounts":[],"tags":[],"invitationEmailSentDate":null,"avatarColor":"#2196f3","addressGpsLat":21.153272,"addressGpsLon":-100.9134508,"isArchived":false,"generateProformaInvoices":null,"usesProforma":false,"hasOverdueInvoice":false,"hasOutage":false,"hasSuspendedService":false,"hasServiceWithoutDevices":false,"referral":null,"hasPaymentSubscription":false,"hasAutopayCreditCard":false},"entityBeforeEdit":{"id":158,"userIdent":null,"previousIsp":null,"isLead":true,"clientType":1,"companyName":null,"companyRegistrationNumber":null,"companyTaxId":null,"companyWebsite":null,"street1":"23 San Luis","street2":null,"city":"Dolores Hidalgo Cuna de la Independencia Nacional","countryId":173,"stateId":null,"zipCode":"37804","fullAddress":"San Luis 23, Guadalupe, Dolores Hidalgo Cuna de la Independencia Nacional, Gto., M\u00e9xico","invoiceStreet1":null,"invoiceStreet2":null,"invoiceCity":null,"invoiceStateId":null,"invoiceCountryId":null,"invoiceZipCode":null,"invoiceAddressSameAsContact":true,"note":null,"sendInvoiceByPost":null,"invoiceMaturityDays":null,"stopServiceDue":null,"stopServiceDueDays":null,"organizationId":1,"tax1Id":null,"tax2Id":null,"tax3Id":null,"registrationDate":"2025-01-06T00:00:00-0600","leadConvertedAt":null,"companyContactFirstName":null,"companyContactLastName":null,"isActive":false,"firstName":"Luis","lastName":"Guti\u00e9rrez","username":null,"contacts":[{"id":162,"clientId":158,"email":null,"phone":null,"name":null,"isBilling":true,"isContact":true,"types":[{"id":1,"name":"Billing"},{"id":2,"name":"General"}]}],"attributes":[],"accountBalance":0,"accountCredit":0,"accountOutstanding":0,"currencyCode":"MXN","organizationName":"SIIP Pruebas","bankAccounts":[],"tags":[],"invitationEmailSentDate":null,"avatarColor":"#2196f3","addressGpsLat":21.153272,"addressGpsLon":-100.9134508,"isArchived":false,"generateProformaInvoices":null,"usesProforma":false,"hasOverdueInvoice":false,"hasOutage":false,"hasSuspendedService":false,"hasServiceWithoutDevices":false,"referral":null,"hasPaymentSubscription":false,"hasAutopayCreditCard":false}}}
if (isset($jsonData['extraData']['entityBeforeEdit'], $jsonData['extraData']['entity'])) {
$entityBeforeEdit = $jsonData['extraData']['entityBeforeEdit'];
$entity = $jsonData['extraData']['entity'];
if (isset($entityBeforeEdit['isLead'], $entity['isLead'])) {
$isLeadBefore = $entityBeforeEdit['isLead'];
$isLeadAfter = $entity['isLead'];
if ($isLeadBefore === true && $isLeadAfter === false) {
$this->logger->info("El cliente $clientID cambió de potencial a cliente. Iniciando creación automática en Stripe...");
$this->pluginNotifierFacade->createStripeClient($notification, 'CREAR CLIENTE STRIPE', false);
}
}
if (isset($entity['tags'], $entityBeforeEdit['tags'])) {
$tags = $entity['tags'];
$tagsBefore = $entityBeforeEdit['tags'];
$clabeTagExistsBefore = false;
$stripeTagExistsBefore = false;
$passwordAntenaTagExistsBefore = false;
$clabeTagExists = false;
$stripeTagExists = false;
$passwordAntenaTagExists = false;
foreach ($tagsBefore as $tag) {
if ($tag['name'] === 'CREAR CLABE STRIPE') $clabeTagExistsBefore = true;
if ($tag['name'] === 'CREAR CLIENTE STRIPE') $stripeTagExistsBefore = true;
if ($tag['name'] === 'OBTENER PASSWORD ANTENA') $passwordAntenaTagExistsBefore = true;
}
foreach ($tags as $tag) {
if ($tag['name'] === 'CREAR CLABE STRIPE') $clabeTagExists = true;
if ($tag['name'] === 'CREAR CLIENTE STRIPE') $stripeTagExists = true;
if ($tag['name'] === 'OBTENER PASSWORD ANTENA') $passwordAntenaTagExists = true;
}
if ($clabeTagExists && !$clabeTagExistsBefore) {
$this->logger->debug('La etiqueta CREAR CLABE STRIPE se agregó al cliente');
$this->pluginNotifierFacade->createStripeClient($notification, 'CREAR CLABE STRIPE', true);
}
if ($stripeTagExists && !$stripeTagExistsBefore) {
$this->logger->debug('La etiqueta CREAR CLIENTE STRIPE se agregó al cliente');
$this->pluginNotifierFacade->createStripeClient($notification, 'CREAR CLIENTE STRIPE', false);
}
if ($passwordAntenaTagExists && !$passwordAntenaTagExistsBefore) {
$this->logger->debug('La etiqueta OBTENER PASSWORD ANTENA se agregó al cliente');
$this->pluginNotifierFacade->processClientPasswordAntenna($clientID, $entity);
}
}
// Automatización: Sincronizar cambios de Nombre o Email con Stripe
$nameBefore = trim(($entityBeforeEdit['firstName'] ?? '') . ' ' . ($entityBeforeEdit['lastName'] ?? ''));
if (empty($nameBefore)) $nameBefore = $entityBeforeEdit['companyName'] ?? '';
$nameAfter = trim(($entity['firstName'] ?? '') . ' ' . ($entity['lastName'] ?? ''));
if (empty($nameAfter)) $nameAfter = $entity['companyName'] ?? '';
$emailBefore = null;
foreach ($entityBeforeEdit['contacts'] ?? [] as $contact) {
if ($contact['isBilling'] || $contact['isContact']) {
$emailBefore = $contact['email'] ?? null;
if ($emailBefore) break;
}
}
$emailAfter = null;
foreach ($entity['contacts'] ?? [] as $contact) {
if ($contact['isBilling'] || $contact['isContact']) {
$emailAfter = $contact['email'] ?? null;
if ($emailAfter) break;
}
}
if ($nameBefore !== $nameAfter || $emailBefore !== $emailAfter) {
$this->logger->info("Detectado cambio en datos básicos del cliente $clientID. Sincronizando con Stripe...");
$this->pluginNotifierFacade->syncStripeCustomerData((int)$clientID, $nameAfter, $emailAfter);
}
} else {
$this->logger->warning('Los datos entityBeforeEdit o entity no están presentes en extraData');
}
} else if ($notification->eventName === 'client.edit') {
$this->logger->debug('Se actualiza a un cliente' . PHP_EOL);
$this->notifierFacade->verifyClientActionToDo($notification); $this->notifierFacade->verifyClientActionToDo($notification);
} else if ($notification->eventName === 'client.add') { } else if ($notification->eventName === 'client.add') {
$this->logger->debug('Se agregó un nuevo cliente' . PHP_EOL); $this->logger->debug('Se agregó un nuevo cliente');
$this->pluginNotifierFacade->createStripeClient($notification); $this->logger->debug('Valor de json_data: ' . json_encode($jsonData));
} else if ($notification->eventName === 'service.edit') {
$this->logger->debug('Se editó el servicio a un cliente' . PHP_EOL);
$this->notifierFacade->verifyServiceActionToDo($notification);
//ejemplo de json_data: {"uuid":"06d281ca-d78e-4f0a-a282-3a6b77d25da0","changeType":"edit","entity":"service","entityId":"155","eventName":"service.edit","extraData":{"entity":{"id":155,"prepaid":false,"clientId":171,"status":1,"name":"Basico 300","fullAddress":"Campeche 56, Dolores Hidalgo, 37800","street1":"Campeche 56","street2":null,"city":"Dolores Hidalgo","countryId":173,"stateId":null,"zipCode":"37800","note":null,"addressGpsLat":21.1572461,"addressGpsLon":-100.9377137,"servicePlanId":6,"servicePlanPeriodId":26,"price":300,"hasIndividualPrice":false,"totalPrice":300,"currencyCode":"MXN","invoiceLabel":null,"contractId":null,"contractLengthType":1,"minimumContractLengthMonths":null,"activeFrom":"2025-05-21T00:00:00-0600","activeTo":null,"contractEndDate":null,"discountType":0,"discountValue":null,"discountInvoiceLabel":"Descuento","discountFrom":null,"discountTo":null,"tax1Id":null,"tax2Id":null,"tax3Id":null,"invoicingStart":"2025-05-21T00:00:00-0600","invoicingPeriodType":1,"invoicingPeriodStartDay":1,"nextInvoicingDayAdjustment":10,"invoicingProratedSeparately":true,"invoicingSeparately":false,"sendEmailsAutomatically":null,"useCreditAutomatically":true,"servicePlanName":"Basico 300","servicePlanPrice":300,"servicePlanPeriod":1,"servicePlanType":"Internet","downloadSpeed":8,"uploadSpeed":8,"hasOutage":false,"unmsClientSiteStatus":null,"fccBlockId":null,"lastInvoicedDate":null,"unmsClientSiteId":"359cb58d-e64f-453a-890e-23d5abb4f116","attributes":[],"addressData":null,"suspensionReasonId":null,"serviceChangeRequestId":null,"setupFeePrice":null,"earlyTerminationFeePrice":null,"downloadSpeedOverride":null,"uploadSpeedOverride":null,"trafficShapingOverrideEnd":null,"trafficShapingOverrideEnabled":false,"servicePlanGroupId":null,"suspensionPeriods":[],"surcharges":[]},"entityBeforeEdit":{"id":155,"prepaid":false,"clientId":171,"status":1,"name":"Basico 300","fullAddress":"Campeche 56, Dolores Hidalgo, 37800","street1":"Campeche 56","street2":null,"city":"Dolores Hidalgo","countryId":173,"stateId":null,"zipCode":"37800","note":null,"addressGpsLat":21.1572461,"addressGpsLon":-100.9377137,"servicePlanId":6,"servicePlanPeriodId":26,"price":300,"hasIndividualPrice":false,"totalPrice":300,"currencyCode":"MXN","invoiceLabel":null,"contractId":null,"contractLengthType":1,"minimumContractLengthMonths":null,"activeFrom":"2025-05-21T00:00:00-0600","activeTo":null,"contractEndDate":null,"discountType":0,"discountValue":null,"discountInvoiceLabel":"Descuento","discountFrom":null,"discountTo":null,"tax1Id":null,"tax2Id":null,"tax3Id":null,"invoicingStart":"2025-05-21T00:00:00-0600","invoicingPeriodType":1,"invoicingPeriodStartDay":1,"nextInvoicingDayAdjustment":10,"invoicingProratedSeparately":true,"invoicingSeparately":false,"sendEmailsAutomatically":null,"useCreditAutomatically":true,"servicePlanName":"Basico 300","servicePlanPrice":300,"servicePlanPeriod":1,"servicePlanType":"Internet","downloadSpeed":8,"uploadSpeed":8,"hasOutage":false,"unmsClientSiteStatus":null,"fccBlockId":null,"lastInvoicedDate":null,"unmsClientSiteId":"359cb58d-e64f-453a-890e-23d5abb4f116","attributes":[],"addressData":null,"suspensionReasonId":null,"serviceChangeRequestId":null,"setupFeePrice":null,"earlyTerminationFeePrice":null,"downloadSpeedOverride":null,"uploadSpeedOverride":null,"trafficShapingOverrideEnd":null,"trafficShapingOverrideEnabled":false,"servicePlanGroupId":null,"suspensionPeriods":[],"surcharges":[]}}}
$clientID = $jsonData['extraData']['entity']['clientId'];
$this->pluginNotifierFacade->updatePasswordAntenaIfNeeded($clientID, $jsonData);
} else if ($notification->eventName === 'service.suspend') { } else if ($notification->eventName === 'service.suspend') {
$this->logger->debug('Se suspendió el servicio a un cliente' . PHP_EOL); $this->logger->debug('Se suspendió el servicio a un cliente' . PHP_EOL);
$this->notifierFacade->verifyServiceActionToDo($notification); $this->notifierFacade->verifyServiceActionToDo($notification);
@ -301,7 +539,7 @@ class Plugin
} else if ($notification->eventName === 'service.postpone') { } else if ($notification->eventName === 'service.postpone') {
$this->logger->debug('Se pospuso la suspención del servicio a un cliente' . PHP_EOL); $this->logger->debug('Se pospuso la suspención del servicio a un cliente' . PHP_EOL);
$this->notifierFacade->verifyServiceActionToDo($notification); $this->notifierFacade->verifyServiceActionToDo($notification);
}else if ($notification->eventName === 'invoice.near_due') { } else if ($notification->eventName === 'invoice.near_due') {
$this->logger->debug('Factura casi por vencer' . PHP_EOL); $this->logger->debug('Factura casi por vencer' . PHP_EOL);
$this->notifierFacade->notifyOverDue($notification); $this->notifierFacade->notifyOverDue($notification);
} else if ($notification->eventName === 'invoice.overdue') { } else if ($notification->eventName === 'invoice.overdue') {
@ -315,16 +553,93 @@ class Plugin
$this->logger->debug('datos del notification para el invoice add:' . $result . PHP_EOL); $this->logger->debug('datos del notification para el invoice add:' . $result . PHP_EOL);
$accountBalance = $notification->clientData['accountBalance']; $accountBalance = $notification->clientData['accountBalance'];
$invoiceAmountPaid = $notification->invoiceData['amountPaid']; //$invoiceAmountPaid = $notification->invoiceData['amountPaid'];
$this->logger->debug("Account Balance: " . $accountBalance . PHP_EOL); $this->logger->debug("Account Balance: " . $accountBalance . PHP_EOL);
$this->logger->debug("Pago hecho con la factura: " . $invoiceAmountPaid . PHP_EOL); // $this->logger->debug("Pago hecho con la factura: " . $invoiceAmountPaid . PHP_EOL);
$this->notifierFacade->verifyInvoiceActionToDo($notification); $this->notifierFacade->verifyInvoiceActionToDo($notification);
}else if($notification->eventName === 'invoice.edit'){ } else if ($notification->eventName === 'invoice.edit') {
$this->logger->debug('Edición de Factura' . PHP_EOL); $this->logger->debug('Edición de Factura' . PHP_EOL);
$this->notifierFacade->verifyInvoiceActionToDo($notification); $this->notifierFacade->verifyInvoiceActionToDo($notification);
} else if ($notification->eventName === 'invoice.add_draft') {
$this->logger->debug('Adición de borrador de Factura' . PHP_EOL);
$this->notifierFacade->verifyInvoiceActionToDo($notification);
} else if ($notification->eventName === 'invoice.draft_approved') {
$this->logger->debug('Aprobación de Factura' . PHP_EOL);
$this->notifierFacade->verifyInvoiceActionToDo($notification);
} else if ($notification->eventName === 'invoice.delete') {
$this->logger->debug('Eliminación de Factura' . PHP_EOL);
$this->notifierFacade->verifyInvoiceActionToDo($notification);
} else if ($notification->eventName === 'job.add') {
$this->logger->debug('Se ha agregado un nuevo trabajo');
$title = $jsonData['extraData']['entity']['title'] ?? '';
$title = '[NOTIFICACION-PENDIENTE]' . $title;
$responsePatch = $this->ucrmApi->patch('scheduling/jobs/' . $jsonData['entityId'], [
'title' => $title,
]);
$this->logger->debug('Respuesta de la API al agregar el trabajo: ' . json_encode($responsePatch));
} else if ($notification->eventName === 'job.edit') {
$this->logger->debug('Se actualiza un trabajo' . PHP_EOL);
// $this->logger->debug('Valor de json_data: ' . json_encode($jsonData));
// Validar que 'extraData' existe y contiene las claves necesarias
if (
isset($jsonData['extraData']['entityBeforeEdit']) &&
isset($jsonData['extraData']['entity'])
) {
$entityBeforeEdit = $jsonData['extraData']['entityBeforeEdit'];
$entity = $jsonData['extraData']['entity'];
$this->logger->debug('Validando claves dentro de entityBeforeEdit y entity');
// Validar que 'assignedUserId' existe en ambas entidades
if (array_key_exists('assignedUserId', $entityBeforeEdit) && array_key_exists('assignedUserId', $entity)) {
// $this->logger->debug('Los datos entityBeforeEdit y entity contienen el campo assignedUserId');
$assignedUserIdBefore = $entityBeforeEdit['assignedUserId'];
$assignedUserIdAfter = $entity['assignedUserId'];
$dateBefore = $entityBeforeEdit['date'];
$dateAfter = $entity['date'];
$statusBefore = $entityBeforeEdit['status'];
$statusAfter = $entity['status'];
$title = $entityBeforeEdit['title'];
$pendingPrefix = '[NOTIFICACION-PENDIENTE]'; // Prefijo para trabajos pendientes de notificación
$currentTitle = $entity['title'] ?? ''; // Obtener el título actual
//Valores de status y su significado: 0 = abierto, 1= En curso, 2 = Cerrado
if ($statusAfter == 1) {
$isNewActivation = ($statusBefore == 0 && $statusAfter == 1);
$isTechChange = ($assignedUserIdBefore != null && $assignedUserIdAfter != $assignedUserIdBefore && $dateBefore === $dateAfter);
$isReprogramming = ($assignedUserIdBefore != null && $assignedUserIdBefore === $assignedUserIdAfter && $dateBefore != $dateAfter);
$isBothChange = ($assignedUserIdBefore != null && $assignedUserIdAfter != $assignedUserIdBefore && $dateBefore != $dateAfter);
$isDeassignment = ($assignedUserIdBefore != null && $assignedUserIdAfter === null);
if ($isNewActivation) {
$this->logger->debug('Trabajo iniciado o asignado: Notificando...');
$this->notifierFacade->verifyJobActionToDo($jsonData, false, false);
} else if ($isTechChange) {
$this->logger->debug('Cambio de técnico sin reprogramación');
$this->notifierFacade->verifyJobActionToDo($jsonData, false, true);
} else if ($isReprogramming) {
$this->logger->debug('Reprogramación sin cambio de técnico');
$this->notifierFacade->verifyJobActionToDo($jsonData, true, false);
} else if ($isBothChange) {
$this->logger->debug('Reprogramación y cambio de técnico');
$this->notifierFacade->verifyJobActionToDo($jsonData, true, true);
} else if ($isDeassignment) {
$this->logger->debug('Técnico desasignado');
$this->notifierFacade->verifyJobActionToDo($jsonData, null, true);
} else {
$this->logger->debug('Edición de trabajo "En curso" sin cambios de fecha o técnico relevantes para notificación');
}
}
} else {
$this->logger->warning('El campo assignedUserId no existe en entityBeforeEdit o entity');
}
} else {
$this->logger->warning('Los datos entityBeforeEdit o entity no están presentes en extraData');
}
} }
//$this->notifierFacade->update($notification); //$this->notifierFacade->update($notification);
} catch (TwilioException $exception) { } catch (TwilioException $exception) {

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace SmsNotifier\Service;
use Aws\S3\S3Client;
use Aws\Exception\AwsException;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
class MinioStorageService
{
/**
* @var S3Client
*/
private $s3Client;
/**
* @var string
*/
private $bucket;
/**
* @var string
*/
private $publicUrl;
/**
* @var Logger
*/
private $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
$this->initializeClient();
}
private function initializeClient(): void
{
$config = PluginConfigManager::create()->loadConfig();
$endpoint = $config['minioEndpoint'] ?? 'http://localhost:9002';
$key = $config['minioAccessKey'] ?? 'minioadmin';
$secret = $config['minioSecretKey'] ?? 'minioadmin';
$this->bucket = $config['minioBucket'] ?? 'vouchers-oxxo';
$this->publicUrl = rtrim($config['minioPublicUrl'] ?? 'http://localhost:9002', '/');
try {
$this->s3Client = new S3Client([
'version' => 'latest',
'region' => 'us-east-1', // MinIO doesn't really care about region, but SDK requires it
'endpoint' => $endpoint,
'use_path_style_endpoint' => true,
'credentials' => [
'key' => $key,
'secret' => $secret,
],
]);
} catch (\Exception $e) {
$this->logger->error("Error initializing S3 Client: " . $e->getMessage());
}
}
public function uploadFile(string $sourcePath, string $filename): string
{
if (!$this->s3Client) {
$this->logger->error("S3 Client not initialized. Cannot upload.");
return '';
}
try {
$result = $this->s3Client->putObject([
'Bucket' => $this->bucket,
'Key' => $filename,
'SourceFile' => $sourcePath,
]);
// Construct Public URL
// We do NOT use $result['ObjectURL'] because that might point to the internal container IP/Port
// We specifically want to use the public domain provided in config.
$finalUrl = $this->publicUrl . '/' . $this->bucket . '/' . $filename;
$this->logger->info("Archivo subido exitosamente a MinIO: $finalUrl");
return $finalUrl;
} catch (AwsException $e) {
$this->logger->error("AWS S3 Error uploading file $filename: " . $e->getMessage());
return '';
} catch (\Exception $e) {
$this->logger->error("General Error uploading file $filename: " . $e->getMessage());
return '';
}
}
}

View File

@ -0,0 +1,277 @@
<?php
namespace SmsNotifier\Service;
use GuzzleHttp\Client;
use Stripe\StripeClient;
use Stripe\Exception\ApiErrorException;
class PaymentIntentService
{
private $ucrmApi;
private $stripeApiKey;
private $stripeClient;
private $logger;
public function __construct($ucrmApi, $stripeApiKey, $logger = null)
{
$this->ucrmApi = $ucrmApi;
$this->stripeApiKey = $stripeApiKey;
$this->logger = $logger;
$this->stripeClient = new StripeClient($this->stripeApiKey);
}
public function searchClients($query)
{
try {
$clients = $this->ucrmApi->get('clients', [
'query' => $query,
'limit' => 5
]);
return array_map(function ($client) {
return [
'id' => $client['id'],
'firstName' => $client['firstName'],
'lastName' => $client['lastName'],
'username' => $client['username'],
'fullAddress' => $client['fullAddress'] ?? '',
'companyName' => $client['companyName'],
'clientType' => $client['clientType'],
'isSuspended' => $client['isSuspended'] ?? false,
'contacts' => $client['contacts'] ?? [],
'attributes' => $client['attributes'] ?? []
];
}, $clients);
} catch (\Exception $e) {
return ['error' => $e->getMessage()];
}
}
public function getClientDetails($clientId)
{
try {
$client = $this->ucrmApi->get("clients/{$clientId}");
$stripeCustomerId = null;
$clabeInterbancaria = null;
if (isset($client['attributes'])) {
foreach ($client['attributes'] as $attribute) {
if ($attribute['key'] === 'stripeCustomerId' || $attribute['name'] === 'Stripe Customer ID') {
$stripeCustomerId = $attribute['value'];
}
if ($attribute['key'] === 'clabeInterbancaria' || $attribute['name'] === 'Clabe Interbancaria') {
$clabeInterbancaria = $attribute['value'];
}
}
}
return [
'id' => $client['id'],
'fullName' => ($client['clientType'] == 1)
? $client['firstName'] . ' ' . $client['lastName']
: $client['companyName'],
'stripeCustomerId' => $stripeCustomerId,
'clabeInterbancaria' => $clabeInterbancaria,
'email' => $this->getClientEmail($client),
'accountOutstanding' => $client['accountOutstanding'] ?? 0
];
} catch (\Exception $e) {
return ['error' => $e->getMessage()];
}
}
private function getClientEmail($clientData)
{
if (!empty($clientData['contacts'])) {
foreach ($clientData['contacts'] as $contact) {
if (!empty($contact['email'])) {
return $contact['email'];
}
}
}
return $clientData['username'] ?? '';
}
public function createPaymentIntent($clientId, $amount, $stripeCustomerId, $adminId = null)
{
if ($amount < 10) {
throw new \Exception("El monto debe ser mayor a 10 MXN");
}
try {
$amountCentavos = intval($amount * 100);
$paymentIntent = $this->stripeClient->paymentIntents->create([
'amount' => $amountCentavos,
'currency' => 'mxn',
'customer' => $stripeCustomerId,
'payment_method_types' => ['customer_balance'],
'payment_method_data' => ['type' => 'customer_balance'],
'confirm' => true,
'payment_method_options' => [
'customer_balance' => [
'funding_type' => 'bank_transfer',
'bank_transfer' => ['type' => 'mx_bank_transfer']
],
],
'metadata' => [
'clientId' => $clientId,
'createdBy' => 'UCRM',
'paymentType' => 'card.one_time',
'signedInAdminId' => $adminId,
'tipoPago' => 'Transferencia Bancaria'
],
]);
$this->log("PaymentIntent created: " . $paymentIntent->id . " Status: " . $paymentIntent->status);
return [
'success' => true,
'id' => $paymentIntent->id,
'status' => $paymentIntent->status,
'amount' => $paymentIntent->amount / 100,
'currency' => $paymentIntent->currency,
'next_action' => $paymentIntent->next_action
];
} catch (ApiErrorException $e) {
return ['success' => false, 'error' => $e->getMessage()];
} catch (\Exception $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
}
public function getLastPayments($stripeCustomerId, $limit = 10)
{
try {
// Buscamos más registros para filtrar por tipo de pago (transferencias)
$collection = $this->stripeClient->paymentIntents->all([
'customer' => $stripeCustomerId,
'limit' => 50,
'expand' => ['data.charges']
]);
$result = [];
foreach ($collection->data as $payment) {
// 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[] = [
'id' => $payment->id,
'amount' => $payment->amount / 100,
'currency' => strtoupper($payment->currency),
'status' => $payment->status,
'created' => $payment->created,
'date' => date('d/m/Y H:i', $payment->created),
'description' => $description,
'reference' => $reference
];
if (count($result) >= $limit) break;
}
}
return $result;
} catch (\Exception $e) {
$this->log("Error fetching last payments: " . $e->getMessage());
return [];
}
}
public function getLastOxxoPayments($stripeCustomerId, $limit = 5)
{
try {
// Fetch a bit more to allow for filtering
$collection = $this->stripeClient->paymentIntents->all([
'customer' => $stripeCustomerId,
'limit' => 100,
'expand' => ['data.charges'],
]);
$result = [];
foreach ($collection->data as $payment) {
// Check if it's an OXXO payment
$isOxxo = false;
if (isset($payment->payment_method_types) && in_array('oxxo', $payment->payment_method_types)) {
$isOxxo = true;
}
if ($isOxxo) {
$oxxoNumber = $payment->next_action->oxxo_display_details->number ?? '-';
$pdfUrl = $payment->next_action->oxxo_display_details->hosted_voucher_url ?? null;
// Fallback for Succeeded payments (where next_action might be null)
if ($oxxoNumber === '-' && !empty($payment->charges->data)) {
$charge = $payment->charges->data[0];
if (isset($charge->payment_method_details->oxxo->number)) {
$oxxoNumber = $charge->payment_method_details->oxxo->number;
}
}
$result[] = [
'id' => $payment->id,
'amount' => $payment->amount / 100,
'currency' => strtoupper($payment->currency),
'status' => $payment->status,
'created' => $payment->created,
'oxxoNumber' => $oxxoNumber, // Reference Number
'voucherUrl' => $pdfUrl
];
}
if (count($result) >= $limit) break;
}
return $result;
} catch (\Exception $e) {
$this->log("Error fetching OXXO payments: " . $e->getMessage());
return ['error' => $e->getMessage()];
}
}
public function getCustomerCashBalance($stripeCustomerId)
{
try {
$balanceObj = $this->stripeClient->customers->retrieveCashBalance(
$stripeCustomerId,
[]
);
$amount = $balanceObj->available['mxn'] ?? 0;
return $amount / 100;
} catch (\Exception $e) {
// Fallback or old api version check
try {
$customer = $this->stripeClient->customers->retrieve(
$stripeCustomerId,
['expand' => ['cash_balance']]
);
return ($customer->cash_balance->available['mxn'] ?? 0) / 100;
} catch (\Exception $ex) {
return 0;
}
}
}
private function log($message)
{
if ($this->logger) {
$this->logger->debug($message);
} else {
error_log($message);
}
}
}

View File

@ -31,13 +31,18 @@ class PluginDataValidator
{ {
$pluginData = $this->optionsManager->load(); $pluginData = $this->optionsManager->load();
$valid = true; $valid = true;
if (empty($pluginData->twilioAccountSid)) { if (empty($pluginData->tokencallbell)) {
$this->errors[] = 'Not valid configuration: Twilio Account SID must be configured'; $this->errors[] = 'Configuración inválida: Token de CallBell es requerido';
$valid = false; $valid = false;
} }
if (empty($pluginData->twilioAuthToken)) { if (empty($pluginData->apitoken)) {
$this->errors[] = 'Not valid configuration: Twilio Auth Token must be configured'; $this->errors[] = 'Configuración inválida: Token de API de UCRM es requerido';
$valid = false;
}
if (empty($pluginData->ipserver)) {
$this->errors[] = 'Configuración inválida: IP del Servidor es requerida';
$valid = false; $valid = false;
} }

View File

@ -33,27 +33,74 @@ class SmsNumberProvider
/* /*
* go through client's contacts and find an applicable one, if any * go through client's contacts and find an applicable one, if any
*/ */
public function getUcrmClientNumbers(NotificationData $notificationData) public function getUcrmClientNumbers(NotificationData $notificationData = null, $arrayClientCRM = null)
{
$log = PluginLogManager::create(); //Initialize Logger
$arrayPhones = [];
if ($arrayClientCRM != null) {
foreach ($arrayClientCRM['contacts'] as $contact) {
if (!empty($contact['phone'])) {
if (isset($contact['types']) && is_array($contact['types'])) {
foreach ($contact['types'] as $type) {
$typeNameLower = trim(strtolower($type['name']));
if (in_array($typeNameLower, ['whatsapp', 'whatsnotifica'])) {
// Almacena varios números bajo el mismo tipo normalizado
$arrayPhones[$type['name']][] = $contact['phone'];
}
}
}
}
}
} else {
foreach ($notificationData->clientData['contacts'] as $contact) {
if (!empty($contact['phone'])) {
if (isset($contact['types']) && is_array($contact['types'])) {
foreach ($contact['types'] as $type) {
$typeNameLower = trim(strtolower($type['name']));
if (in_array($typeNameLower, ['whatsapp', 'whatsnotifica', 'whatsactualiza'])) {
// Almacena varios números bajo el mismo tipo normalizado
$arrayPhones[$type['name']][] = $contact['phone'];
}
}
}
}
}
}
return $arrayPhones; // Devolver el arreglo de teléfonos por tipo
}
/*
* go through client's contacts and find an applicable one, if any
*/
public function getAllUcrmClientNumbers($arrayClientCRM = null)
{ {
$log = PluginLogManager::create(); //Initialize Logger
//$log->appendLog("Ejecutando metodo getUcrmClientNumbers: " . json_encode($arrayClientCRM) . PHP_EOL);
// Array asociativo donde almacenarás los números de teléfono con sus tipos correspondientes // Array asociativo donde almacenarás los números de teléfono con sus tipos correspondientes
$arrayPhones = []; $arrayPhones = [];
//$log->appendLog("Entrando al if del método getUcrmClientNumbers: " . $arrayClientCRM . PHP_EOL);
//$jsonNotificationData = json_decode($jsonNotificationData, true);
// Recorrer los contactos del cliente para encontrar los teléfonos con sus tipos // Recorrer los contactos del cliente para encontrar los teléfonos con sus tipos
foreach ($notificationData->clientData['contacts'] as $contact) { foreach ($arrayClientCRM['contacts'] as $contact) {
if (!empty($contact['phone'])) { // Verificar que el teléfono no esté vacío if (!empty($contact['phone'])) { // Verificar que el teléfono no esté vacío
if (isset($contact['types']) && is_array($contact['types'])) { // Verificar que 'types' exista y sea un array // agregamos al array asociativo
foreach ($contact['types'] as $type) { $arrayPhones[] = $contact['phone'];
if (in_array($type['name'], ['WhatsApp', 'WhatsNotifica', 'WhatsActualiza'])) {
// Si el tipo de contacto es uno de los deseados, agregamos al array asociativo
$arrayPhones[$type['name']] = $contact['phone'];
}
}
}
} }
} }
return $arrayPhones; // Devolver el arreglo de teléfonos por tipo return $arrayPhones; // Devolver el arreglo de teléfonos por tipo
} }
/* /*
* not every contact has a phone; also check if the type of notification is applicable to contact * not every contact has a phone; also check if the type of notification is applicable to contact
*/ */
@ -76,7 +123,7 @@ class SmsNumberProvider
$types = $contact['types'] ?? []; $types = $contact['types'] ?? [];
foreach ($types as $type) { foreach ($types as $type) {
//$log->appendLog("Entrando al case client del switch: " . json_encode($type)); //$log->appendLog("Entrando al case client del switch: " . json_encode($type));
if ($type['name'] == 'WhatsApp') { if (trim(strtolower($type['name'])) == 'whatsapp') {
return true; return true;
} }
} }
@ -88,19 +135,18 @@ class SmsNumberProvider
$types = $contact['types'] ?? []; $types = $contact['types'] ?? [];
foreach ($types as $type) { foreach ($types as $type) {
//$log->appendLog("Entrando al case client del switch: " . json_encode($type)); //$log->appendLog("Entrando al case client del switch: " . json_encode($type));
if ($type['name'] == 'WhatsApp') { if (trim(strtolower($type['name'])) == 'whatsapp') {
return true; return true;
} }
} }
return false; return false;
} }
case 'service': case 'service': {
{
$types = $contact['types'] ?? []; $types = $contact['types'] ?? [];
foreach ($types as $type) { foreach ($types as $type) {
//$log->appendLog("Entrando al case client del switch: " . json_encode($type)); //$log->appendLog("Entrando al case client del switch: " . json_encode($type));
if ($type['name'] == 'WhatsApp') { if (trim(strtolower($type['name'])) == 'whatsapp') {
return true; return true;
} }
} }

13
test_facade_crash.php Executable file
View File

@ -0,0 +1,13 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/src/Facade/PluginNotifierFacade.php';
echo "Included successfully\n";
if (!defined('INCLUDED_AS_LIBRARY')) {
define('INCLUDED_AS_LIBRARY', true);
}
require_once __DIR__ . '/scripts-uisp/audit_client_passwords.php';
echo "Audit included successfully\n";

32
test_microservice_patch.php Executable file
View File

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

View File

@ -1 +1 @@
{"ucrmPublicUrl":"https://venus.siip.mx/crm/","ucrmLocalUrl":"http://localhost/crm/","unmsLocalUrl":"http://unms:8081/nms/","pluginPublicUrl":"https://venus.siip.mx/crm/_plugins/siip-whatsapp-notifications/public.php","pluginAppKey":"5dVI22XIa9NSK0tiDeiQ3SLDBa1UZVT7g5s/tJzi944Bygx89WIHH7I2/niVPiBK","pluginId":16} {"ucrmPublicUrl":"https://venus.siip.mx/crm/","ucrmLocalUrl":"http://localhost/crm/","unmsLocalUrl":"http://unms-api:8081/nms/","pluginPublicUrl":"https://venus.siip.mx/crm/_plugins/siip-whatsapp-notifications/public.php","pluginAppKey":"5dVI22XIa9NSK0tiDeiQ3SLDBa1UZVT7g5s/tJzi944Bygx89WIHH7I2/niVPiBK","pluginId":16}

90014
unms-swagger.json Executable file

File diff suppressed because it is too large Load Diff

6591
unms.yaml Executable file

File diff suppressed because it is too large Load Diff

8392
unmscrm.apib Executable file

File diff suppressed because it is too large Load Diff

18
vendor/autoload.php vendored
View File

@ -2,6 +2,24 @@
// autoload.php @generated by Composer // autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php'; require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitd6442a7ec79f2f43fc97d16bdcdd18ee::getLoader(); return ComposerAutoloaderInitd6442a7ec79f2f43fc97d16bdcdd18ee::getLoader();

4
vendor/aws/aws-crt-php/CODE_OF_CONDUCT.md vendored Executable file
View File

@ -0,0 +1,4 @@
## Code of Conduct
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
opensource-codeofconduct@amazon.com with any additional questions or comments.

175
vendor/aws/aws-crt-php/LICENSE vendored Executable file
View File

@ -0,0 +1,175 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

1
vendor/aws/aws-crt-php/NOTICE vendored Executable file
View File

@ -0,0 +1 @@
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

117
vendor/aws/aws-crt-php/README.md vendored Executable file
View File

@ -0,0 +1,117 @@
# AWS Common Runtime PHP bindings
## Requirements
* PHP 5.5+ on UNIX platforms, 7.2+ on Windows
* CMake 3.x
* GCC 4.4+, clang 3.8+ on UNIX, Visual Studio build tools on Windows
* Tests require [Composer](https://getcomposer.org)
## Installing with Composer and PECL
The package has two different package published to [composer](https://packagist.org/packages/aws/aws-crt-php) and [PECL](https://pecl.php.net/package/awscrt).
On UNIX, you can get the package from package manager or build from source:
```
pecl install awscrt
composer require aws/aws-crt-php
```
On Windows, you need to build from source as instruction written below for the native extension `php_awscrt.dll` . And, follow https://www.php.net/manual/en/install.pecl.windows.php#install.pecl.windows.loading to load extension. After that:
```
composer require aws/aws-crt-php
```
## Building from Github source
```sh
$ git clone --recursive https://github.com/awslabs/aws-crt-php.git
$ cd aws-crt-php
$ phpize
$ ./configure
$ make
$ ./dev-scripts/run_tests.sh
```
## Building on Windows
### Requirements for Windows
* Ensure you have the [windows PHP SDK](https://github.com/microsoft/php-sdk-binary-tools) (this example assumes installation of the SDK to C:\php-sdk and that you've checked out the PHP source to php-src within the build directory) and it works well on your machine.
* Ensure you have "Development package (SDK to develop PHP extensions)" and PHP available from your system path. You can download them from https://windows.php.net/download/. You can check if they are available by running `phpize -v` and `php -v`
### Instructions
From Command Prompt (not powershell). The instruction is based on Visual Studio 2019 on 64bit Windows.
```bat
> git clone --recursive https://github.com/awslabs/aws-crt-php.git
> git clone https://github.com/microsoft/php-sdk-binary-tools.git C:\php-sdk
> C:\php-sdk\phpsdk-vs16-x64.bat
C:\php-sdk\
$ cd <your-path-to-aws-crt-php>
<your-path-to-aws-crt-php>\
$ phpize
# --with-prefix only required when your php runtime in system path is different than the runtime you wish to use.
<your-path-to-aws-crt-php>\
$ configure --enable-awscrt=shared --with-prefix=<your-path-to-php-prefix>
<your-path-to-aws-crt-php>\
$ nmake
<your-path-to-aws-crt-php>\
$ nmake generate-php-ini
# check .\php-win.ini, it now has the full path to php_awscrt.dll that you can manually load to your php runtime, or you can run the following command to run tests and load the required native extension for awscrt.
<your-path-to-aws-crt-php>\
$ .\dev-scripts\run_tests.bat <your-path-to-php-binary>
```
Note: for VS2017, Cmake will default to build for Win32, refer to [here](https://cmake.org/cmake/help/latest/generator/Visual%20Studio%2015%202017.html). If you are building for x64 php, you can set environment variable as follow to let cmake pick x64 compiler.
```bat
set CMAKE_GENERATOR=Visual Studio 15 2017
set CMAKE_GENERATOR_PLATFORM=x64
```
## Debugging
Using [PHPBrew](https://github.com/phpbrew/phpbrew) to build/manage multiple versions of PHP is helpful.
Note: You must use a debug build of PHP to debug native extensions.
See the [PHP Internals Book](https://www.phpinternalsbook.com/php7/build_system/building_php.html) for more info
```shell
# PHP 8 example
$ phpbrew install --stdout -j 8 8.0 +default -- CFLAGS=-Wno-error --disable-cgi --enable-debug
# PHP 5.5 example
$ phpbrew install --stdout -j 8 5.5 +default -openssl -mbstring -- CFLAGS="-w -Wno-error" --enable-debug --with-zlib=/usr/local/opt/zlib
$ phpbrew switch php-8.0.6 # or whatever version is current, it'll be at the end of the build output
$ phpize
$ ./configure
$ make CMAKE_BUILD_TYPE=Debug
```
Ensure that the php you launch from your debugger is the result of `which php` , not just
the system default php.
## Security
See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
## Known OpenSSL related issue (Unix only)
* When your php loads a different version of openssl than your system openssl version, awscrt may fail to load or weirdly crash. You can find the openssl version php linked via: `php -i | grep 'OpenSSL'`, and awscrt linked from the build log, which will be `Found OpenSSL: * (found version *)`
The easiest workaround to those issue is to build from source and get aws-lc for awscrt to depend on instead.
TO do that, same instructions as [here](#building-from-github-source), but use `USE_OPENSSL=OFF make` instead of `make`
## License
This project is licensed under the Apache-2.0 License.

35
vendor/aws/aws-crt-php/composer.json vendored Executable file
View File

@ -0,0 +1,35 @@
{
"name": "aws/aws-crt-php",
"homepage": "https://github.com/awslabs/aws-crt-php",
"description": "AWS Common Runtime for PHP",
"keywords": ["aws","amazon","sdk","crt"],
"type": "library",
"authors": [
{
"name": "AWS SDK Common Runtime Team",
"email": "aws-sdk-common-runtime@amazon.com"
}
],
"minimum-stability": "alpha",
"require": {
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit":"^4.8.35||^5.6.3||^9.5",
"yoast/phpunit-polyfills": "^1.0"
},
"autoload": {
"classmap": [
"src/"
]
},
"suggest": {
"ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
},
"scripts": {
"test": "./dev-scripts/run_tests.sh",
"test-extension": "@test",
"test-win": ".\\dev-scripts\\run_tests.bat"
},
"license": "Apache-2.0"
}

46
vendor/aws/aws-crt-php/format-check.py vendored Executable file
View File

@ -0,0 +1,46 @@
#!/usr/bin/env python3
import argparse
import os
from pathlib import Path
import re
from subprocess import list2cmdline, run
from tempfile import NamedTemporaryFile
CLANG_FORMAT_VERSION = '18.1.6'
INCLUDE_REGEX = re.compile(r'^ext/.*\.(c|h|inl)$')
EXCLUDE_REGEX = re.compile(r'^$')
arg_parser = argparse.ArgumentParser(description="Check with clang-format")
arg_parser.add_argument('-i', '--inplace-edit', action='store_true',
help="Edit files inplace")
args = arg_parser.parse_args()
os.chdir(Path(__file__).parent)
# create file containing list of all files to format
filepaths_file = NamedTemporaryFile(delete=False)
for dirpath, dirnames, filenames in os.walk('.'):
for filename in filenames:
# our regexes expect filepath to use forward slash
filepath = Path(dirpath, filename).as_posix()
if not INCLUDE_REGEX.match(filepath):
continue
if EXCLUDE_REGEX.match(filepath):
continue
filepaths_file.write(f"{filepath}\n".encode())
filepaths_file.close()
# use pipx to run clang-format from PyPI
# this is a simple way to run the same clang-format version regardless of OS
cmd = ['pipx', 'run', f'clang-format=={CLANG_FORMAT_VERSION}',
f'--files={filepaths_file.name}']
if args.inplace_edit:
cmd += ['-i']
else:
cmd += ['--Werror', '--dry-run']
print(f"{Path.cwd()}$ {list2cmdline(cmd)}")
if run(cmd).returncode:
exit(1)

View File

@ -0,0 +1,69 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\Auth;
use AWS\CRT\NativeResource as NativeResource;
use AWS\CRT\Options as Options;
/**
* Represents a set of AWS credentials
*
* @param array options:
* - string access_key_id - AWS Access Key Id
* - string secret_access_key - AWS Secret Access Key
* - string session_token - Optional STS session token
* - int expiration_timepoint_seconds - Optional time to expire these credentials
*/
final class AwsCredentials extends NativeResource {
static function defaults() {
return [
'access_key_id' => '',
'secret_access_key' => '',
'session_token' => '',
'expiration_timepoint_seconds' => 0,
];
}
private $access_key_id;
private $secret_access_key;
private $session_token;
private $expiration_timepoint_seconds = 0;
public function __get($name) {
return $this->$name;
}
function __construct(array $options = []) {
parent::__construct();
$options = new Options($options, self::defaults());
$this->access_key_id = $options->access_key_id->asString();
$this->secret_access_key = $options->secret_access_key->asString();
$this->session_token = $options->session_token ? $options->session_token->asString() : null;
$this->expiration_timepoint_seconds = $options->expiration_timepoint_seconds->asInt();
if (strlen($this->access_key_id) == 0) {
throw new \InvalidArgumentException("access_key_id must be provided");
}
if (strlen($this->secret_access_key) == 0) {
throw new \InvalidArgumentException("secret_access_key must be provided");
}
$creds_options = self::$crt->aws_credentials_options_new();
self::$crt->aws_credentials_options_set_access_key_id($creds_options, $this->access_key_id);
self::$crt->aws_credentials_options_set_secret_access_key($creds_options, $this->secret_access_key);
self::$crt->aws_credentials_options_set_session_token($creds_options, $this->session_token);
self::$crt->aws_credentials_options_set_expiration_timepoint_seconds($creds_options, $this->expiration_timepoint_seconds);
$this->acquire(self::$crt->aws_credentials_new($creds_options));
self::$crt->aws_credentials_options_release($creds_options);
}
function __destruct() {
self::$crt->aws_credentials_release($this->release());
parent::__destruct();
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\Auth;
use AWS\CRT\NativeResource as NativeResource;
/**
* Base class for credentials providers
*/
abstract class CredentialsProvider extends NativeResource {
function __construct(array $options = []) {
parent::__construct();
}
function __destruct() {
self::$crt->credentials_provider_release($this->release());
parent::__destruct();
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\Auth;
use AWS\CRT\IO\InputStream;
use AWS\CRT\NativeResource as NativeResource;
class Signable extends NativeResource {
public static function fromHttpRequest($http_message) {
return new Signable(function() use ($http_message) {
return self::$crt->signable_new_from_http_request($http_message->native);
});
}
public static function fromChunk($chunk_stream, $previous_signature="") {
if (!($chunk_stream instanceof InputStream)) {
$chunk_stream = new InputStream($chunk_stream);
}
return new Signable(function() use($chunk_stream, $previous_signature) {
return self::$crt->signable_new_from_chunk($chunk_stream->native, $previous_signature);
});
}
public static function fromCanonicalRequest($canonical_request) {
return new Signable(function() use($canonical_request) {
return self::$crt->signable_new_from_canonical_request($canonical_request);
});
}
protected function __construct($ctor) {
parent::__construct();
$this->acquire($ctor());
}
function __destruct() {
self::$crt->signable_release($this->release());
parent::__destruct();
}
}

View File

@ -0,0 +1,15 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\Auth;
class SignatureType {
const HTTP_REQUEST_HEADERS = 0;
const HTTP_REQUEST_QUERY_PARAMS = 1;
const HTTP_REQUEST_CHUNK = 2;
const HTTP_REQUEST_EVENT = 3;
const CANONICAL_REQUEST_HEADERS = 4;
const CANONICAL_REQUEST_QUERY_PARAMS = 5;
}

View File

@ -0,0 +1,11 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\Auth;
class SignedBodyHeaderType {
const NONE = 0;
const X_AMZ_CONTENT_SHA256 = 1;
}

View File

@ -0,0 +1,22 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\Auth;
use AWS\CRT\NativeResource;
abstract class Signing extends NativeResource {
static function signRequestAws($signable, $signing_config, $on_complete) {
return self::$crt->sign_request_aws($signable->native, $signing_config->native,
function($result, $error_code) use ($on_complete) {
$signing_result = SigningResult::fromNative($result);
$on_complete($signing_result, $error_code);
}, null);
}
static function testVerifySigV4ASigning($signable, $signing_config, $expected_canonical_request, $signature, $ecc_key_pub_x, $ecc_key_pub_y) {
return self::$crt->test_verify_sigv4a_signing($signable, $signing_config, $expected_canonical_request, $signature, $ecc_key_pub_x, $ecc_key_pub_y);
}
}

View File

@ -0,0 +1,11 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\Auth;
class SigningAlgorithm {
const SIGv4 = 0;
const SIGv4_ASYMMETRIC = 1;
}

View File

@ -0,0 +1,75 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\Auth;
use AWS\CRT\NativeResource as NativeResource;
use AWS\CRT\Options as Options;
class SigningConfigAWS extends NativeResource {
public static function defaults() {
return [
'algorithm' => SigningAlgorithm::SIGv4,
'signature_type' => SignatureType::HTTP_REQUEST_HEADERS,
'credentials_provider' => null,
'region' => null,
'service' => null,
'use_double_uri_encode' => false,
'should_normalize_uri_path' => false,
'omit_session_token' => false,
'signed_body_value' => null,
'signed_body_header_type' => SignedBodyHeaderType::NONE,
'expiration_in_seconds' => 0,
'date' => time(),
'should_sign_header' => null,
];
}
private $options;
public function __construct(array $options = []) {
parent::__construct();
$this->options = $options = new Options($options, self::defaults());
$sc = $this->acquire(self::$crt->signing_config_aws_new());
self::$crt->signing_config_aws_set_algorithm($sc, $options->algorithm->asInt());
self::$crt->signing_config_aws_set_signature_type($sc, $options->signature_type->asInt());
if ($credentials_provider = $options->credentials_provider->asObject()) {
self::$crt->signing_config_aws_set_credentials_provider(
$sc,
$credentials_provider->native);
}
self::$crt->signing_config_aws_set_region(
$sc, $options->region->asString());
self::$crt->signing_config_aws_set_service(
$sc, $options->service->asString());
self::$crt->signing_config_aws_set_use_double_uri_encode(
$sc, $options->use_double_uri_encode->asBool());
self::$crt->signing_config_aws_set_should_normalize_uri_path(
$sc, $options->should_normalize_uri_path->asBool());
self::$crt->signing_config_aws_set_omit_session_token(
$sc, $options->omit_session_token->asBool());
self::$crt->signing_config_aws_set_signed_body_value(
$sc, $options->signed_body_value->asString());
self::$crt->signing_config_aws_set_signed_body_header_type(
$sc, $options->signed_body_header_type->asInt());
self::$crt->signing_config_aws_set_expiration_in_seconds(
$sc, $options->expiration_in_seconds->asInt());
self::$crt->signing_config_aws_set_date($sc, $options->date->asInt());
if ($should_sign_header = $options->should_sign_header->asCallable()) {
self::$crt->signing_config_aws_set_should_sign_header_fn($sc, $should_sign_header);
}
}
function __destruct()
{
self::$crt->signing_config_aws_release($this->release());
parent::__destruct();
}
public function __get($name) {
return $this->options->get($name);
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\Auth;
use AWS\CRT\NativeResource;
use AWS\CRT\HTTP\Request;
class SigningResult extends NativeResource {
protected function __construct($native) {
parent::__construct();
$this->acquire($native);
}
function __destruct() {
// No destruction necessary, SigningResults are transient, just release
$this->release();
parent::__destruct();
}
public static function fromNative($ptr) {
return new SigningResult($ptr);
}
public function applyToHttpRequest(&$http_request) {
self::$crt->signing_result_apply_to_http_request($this->native, $http_request->native);
// Update http_request from native
$http_request = Request::unmarshall($http_request->toBlob());
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\Auth;
/**
* Provides a static set of AWS credentials
*
* @param array options:
* - string access_key_id - AWS Access Key Id
* - string secret_access_key - AWS Secret Access Key
* - string session_token - Optional STS session token
*/
final class StaticCredentialsProvider extends CredentialsProvider {
private $credentials;
public function __get($name) {
return $this->$name;
}
function __construct(array $options = []) {
parent::__construct();
$this->credentials = new AwsCredentials($options);
$provider_options = self::$crt->credentials_provider_static_options_new();
self::$crt->credentials_provider_static_options_set_access_key_id($provider_options, $this->credentials->access_key_id);
self::$crt->credentials_provider_static_options_set_secret_access_key($provider_options, $this->credentials->secret_access_key);
self::$crt->credentials_provider_static_options_set_session_token($provider_options, $this->credentials->session_token);
$this->acquire(self::$crt->credentials_provider_static_new($provider_options));
self::$crt->credentials_provider_static_options_release($provider_options);
}
}

358
vendor/aws/aws-crt-php/src/AWS/CRT/CRT.php vendored Executable file
View File

@ -0,0 +1,358 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT;
use AWS\CRT\Internal\Extension;
use \RuntimeException;
/**
* Wrapper for the interface to the CRT. There only ever needs to be one of these, but
* additional instances won't cost anything other than their memory.
* Creating an instance of any NativeResource will activate the CRT binding. User code
* should only need to create one of these if they are only accessing CRT:: static functions.
*/
final class CRT {
private static $impl = null;
private static $refcount = 0;
function __construct() {
if (is_null(self::$impl)) {
try {
self::$impl = new Extension();
} catch (RuntimeException $rex) {
throw new RuntimeException("Unable to initialize AWS CRT via awscrt extension: \n$rex", -1);
}
}
++self::$refcount;
}
function __destruct() {
if (--self::$refcount == 0) {
self::$impl = null;
}
}
/**
* @return bool whether or not the CRT is currently loaded
*/
public static function isLoaded() {
return !is_null(self::$impl);
}
/**
* @return bool whether or not the CRT is available via one of the possible backends
*/
public static function isAvailable() {
try {
new CRT();
return true;
} catch (RuntimeException $ex) {
return false;
}
}
/**
* @return integer last error code reported within the CRT
*/
public static function last_error() {
return self::$impl->aws_crt_last_error();
}
/**
* @param integer $error Error code from the CRT, usually delivered via callback or {@see last_error}
* @return string Human-readable description of the provided error code
*/
public static function error_str($error) {
return self::$impl->aws_crt_error_str((int) $error);
}
/**
* @param integer $error Error code from the CRT, usually delivered via callback or {@see last_error}
* @return string Name/enum identifier for the provided error code
*/
public static function error_name($error) {
return self::$impl->aws_crt_error_name((int) $error);
}
public static function log_to_stdout() {
return self::$impl->aws_crt_log_to_stdout();
}
public static function log_to_stderr() {
return self::$impl->aws_crt_log_to_stderr();
}
public static function log_to_file($filename) {
return self::$impl->aws_crt_log_to_file($filename);
}
public static function log_to_stream($stream) {
return self::$impl->aws_crt_log_to_stream($stream);
}
public static function log_set_level($level) {
return self::$impl->aws_crt_log_set_level($level);
}
public static function log_stop() {
return self::$impl->aws_crt_log_stop();
}
public static function log_message($level, $message) {
return self::$impl->aws_crt_log_message($level, $message);
}
/**
* @return object Pointer to native event_loop_group_options
*/
function event_loop_group_options_new() {
return self::$impl->aws_crt_event_loop_group_options_new();
}
/**
* @param object $elg_options Pointer to native event_loop_group_options
*/
function event_loop_group_options_release($elg_options) {
self::$impl->aws_crt_event_loop_group_options_release($elg_options);
}
/**
* @param object $elg_options Pointer to native event_loop_group_options
* @param integer $max_threads Maximum number of threads to allow the event loop group to use, default: 0/1 per CPU core
*/
function event_loop_group_options_set_max_threads($elg_options, $max_threads) {
self::$impl->aws_crt_event_loop_group_options_set_max_threads($elg_options, (int)$max_threads);
}
/**
* @param object Pointer to event_loop_group_options, {@see event_loop_group_options_new}
* @return object Pointer to the new event loop group
*/
function event_loop_group_new($options) {
return self::$impl->aws_crt_event_loop_group_new($options);
}
/**
* @param object $elg Pointer to the event loop group to release
*/
function event_loop_group_release($elg) {
self::$impl->aws_crt_event_loop_group_release($elg);
}
/**
* return object Pointer to native AWS credentials options
*/
function aws_credentials_options_new() {
return self::$impl->aws_crt_credentials_options_new();
}
function aws_credentials_options_release($options) {
self::$impl->aws_crt_credentials_options_release($options);
}
function aws_credentials_options_set_access_key_id($options, $access_key_id) {
self::$impl->aws_crt_credentials_options_set_access_key_id($options, $access_key_id);
}
function aws_credentials_options_set_secret_access_key($options, $secret_access_key) {
self::$impl->aws_crt_credentials_options_set_secret_access_key($options, $secret_access_key);
}
function aws_credentials_options_set_session_token($options, $session_token) {
self::$impl->aws_crt_credentials_options_set_session_token($options, $session_token);
}
function aws_credentials_options_set_expiration_timepoint_seconds($options, $expiration_timepoint_seconds) {
self::$impl->aws_crt_credentials_options_set_expiration_timepoint_seconds($options, $expiration_timepoint_seconds);
}
function aws_credentials_new($options) {
return self::$impl->aws_crt_credentials_new($options);
}
function aws_credentials_release($credentials) {
self::$impl->aws_crt_credentials_release($credentials);
}
function credentials_provider_release($provider) {
self::$impl->aws_crt_credentials_provider_release($provider);
}
function credentials_provider_static_options_new() {
return self::$impl->aws_crt_credentials_provider_static_options_new();
}
function credentials_provider_static_options_release($options) {
self::$impl->aws_crt_credentials_provider_static_options_release($options);
}
function credentials_provider_static_options_set_access_key_id($options, $access_key_id) {
self::$impl->aws_crt_credentials_provider_static_options_set_access_key_id($options, $access_key_id);
}
function credentials_provider_static_options_set_secret_access_key($options, $secret_access_key) {
self::$impl->aws_crt_credentials_provider_static_options_set_secret_access_key($options, $secret_access_key);
}
function credentials_provider_static_options_set_session_token($options, $session_token) {
self::$impl->aws_crt_credentials_provider_static_options_set_session_token($options, $session_token);
}
function credentials_provider_static_new($options) {
return self::$impl->aws_crt_credentials_provider_static_new($options);
}
function input_stream_options_new() {
return self::$impl->aws_crt_input_stream_options_new();
}
function input_stream_options_release($options) {
self::$impl->aws_crt_input_stream_options_release($options);
}
function input_stream_options_set_user_data($options, $user_data) {
self::$impl->aws_crt_input_stream_options_set_user_data($options, $user_data);
}
function input_stream_new($options) {
return self::$impl->aws_crt_input_stream_new($options);
}
function input_stream_release($stream) {
self::$impl->aws_crt_input_stream_release($stream);
}
function input_stream_seek($stream, $offset, $basis) {
return self::$impl->aws_crt_input_stream_seek($stream, $offset, $basis);
}
function input_stream_read($stream, $length) {
return self::$impl->aws_crt_input_stream_read($stream, $length);
}
function input_stream_eof($stream) {
return self::$impl->aws_crt_input_stream_eof($stream);
}
function input_stream_get_length($stream) {
return self::$impl->aws_crt_input_stream_get_length($stream);
}
function http_message_new_from_blob($blob) {
return self::$impl->aws_crt_http_message_new_from_blob($blob);
}
function http_message_to_blob($message) {
return self::$impl->aws_crt_http_message_to_blob($message);
}
function http_message_release($message) {
self::$impl->aws_crt_http_message_release($message);
}
function signing_config_aws_new() {
return self::$impl->aws_crt_signing_config_aws_new();
}
function signing_config_aws_release($signing_config) {
return self::$impl->aws_crt_signing_config_aws_release($signing_config);
}
function signing_config_aws_set_algorithm($signing_config, $algorithm) {
self::$impl->aws_crt_signing_config_aws_set_algorithm($signing_config, (int)$algorithm);
}
function signing_config_aws_set_signature_type($signing_config, $signature_type) {
self::$impl->aws_crt_signing_config_aws_set_signature_type($signing_config, (int)$signature_type);
}
function signing_config_aws_set_credentials_provider($signing_config, $credentials_provider) {
self::$impl->aws_crt_signing_config_aws_set_credentials_provider($signing_config, $credentials_provider);
}
function signing_config_aws_set_region($signing_config, $region) {
self::$impl->aws_crt_signing_config_aws_set_region($signing_config, $region);
}
function signing_config_aws_set_service($signing_config, $service) {
self::$impl->aws_crt_signing_config_aws_set_service($signing_config, $service);
}
function signing_config_aws_set_use_double_uri_encode($signing_config, $use_double_uri_encode) {
self::$impl->aws_crt_signing_config_aws_set_use_double_uri_encode($signing_config, $use_double_uri_encode);
}
function signing_config_aws_set_should_normalize_uri_path($signing_config, $should_normalize_uri_path) {
self::$impl->aws_crt_signing_config_aws_set_should_normalize_uri_path($signing_config, $should_normalize_uri_path);
}
function signing_config_aws_set_omit_session_token($signing_config, $omit_session_token) {
self::$impl->aws_crt_signing_config_aws_set_omit_session_token($signing_config, $omit_session_token);
}
function signing_config_aws_set_signed_body_value($signing_config, $signed_body_value) {
self::$impl->aws_crt_signing_config_aws_set_signed_body_value($signing_config, $signed_body_value);
}
function signing_config_aws_set_signed_body_header_type($signing_config, $signed_body_header_type) {
self::$impl->aws_crt_signing_config_aws_set_signed_body_header_type($signing_config, $signed_body_header_type);
}
function signing_config_aws_set_expiration_in_seconds($signing_config, $expiration_in_seconds) {
self::$impl->aws_crt_signing_config_aws_set_expiration_in_seconds($signing_config, $expiration_in_seconds);
}
function signing_config_aws_set_date($signing_config, $timestamp) {
self::$impl->aws_crt_signing_config_aws_set_date($signing_config, $timestamp);
}
function signing_config_aws_set_should_sign_header_fn($signing_config, $should_sign_header_fn) {
self::$impl->aws_crt_signing_config_aws_set_should_sign_header_fn($signing_config, $should_sign_header_fn);
}
function signable_new_from_http_request($http_message) {
return self::$impl->aws_crt_signable_new_from_http_request($http_message);
}
function signable_new_from_chunk($chunk_stream, $previous_signature) {
return self::$impl->aws_crt_signable_new_from_chunk($chunk_stream, $previous_signature);
}
function signable_new_from_canonical_request($canonical_request) {
return self::$impl->aws_crt_signable_new_from_canonical_request($canonical_request);
}
function signable_release($signable) {
self::$impl->aws_crt_signable_release($signable);
}
function signing_result_release($signing_result) {
self::$impl->aws_crt_signing_result_release($signing_result);
}
function signing_result_apply_to_http_request($signing_result, $http_message) {
return self::$impl->aws_crt_signing_result_apply_to_http_request(
$signing_result, $http_message);
}
function sign_request_aws($signable, $signing_config, $on_complete, $user_data) {
return self::$impl->aws_crt_sign_request_aws($signable, $signing_config, $on_complete, $user_data);
}
function test_verify_sigv4a_signing($signable, $signing_config, $expected_canonical_request, $signature, $ecc_key_pub_x, $ecc_key_pub_y) {
return self::$impl->aws_crt_test_verify_sigv4a_signing($signable, $signing_config, $expected_canonical_request, $signature, $ecc_key_pub_x, $ecc_key_pub_y);
}
public static function crc32($input, $previous = 0) {
return self::$impl->aws_crt_crc32($input, $previous);
}
public static function crc32c($input, $previous = 0) {
return self::$impl->aws_crt_crc32c($input, $previous);
}
}

View File

@ -0,0 +1,50 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\HTTP;
use AWS\CRT\Internal\Encoding;
final class Headers {
private $headers;
public function __construct($headers = []) {
$this->headers = $headers;
}
public static function marshall($headers) {
$buf = "";
foreach ($headers->headers as $header => $value) {
$buf .= Encoding::encodeString($header);
$buf .= Encoding::encodeString($value);
}
return $buf;
}
public static function unmarshall($buf) {
$strings = Encoding::readStrings($buf);
$headers = [];
for ($idx = 0; $idx < count($strings);) {
$headers[$strings[$idx++]] = $strings[$idx++];
}
return new Headers($headers);
}
public function count() {
return count($this->headers);
}
public function get($header) {
return isset($this->headers[$header]) ? $this->headers[$header] : null;
}
public function set($header, $value) {
$this->headers[$header] = $value;
}
public function toArray() {
return $this->headers;
}
}

View File

@ -0,0 +1,95 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\HTTP;
use AWS\CRT\NativeResource;
use AWS\CRT\Internal\Encoding;
abstract class Message extends NativeResource {
private $method;
private $path;
private $query;
private $headers;
public function __construct($method, $path, $query = [], $headers = []) {
parent::__construct();
$this->method = $method;
$this->path = $path;
$this->query = $query;
$this->headers = new Headers($headers);
$this->acquire(self::$crt->http_message_new_from_blob(self::marshall($this)));
}
public function __destruct() {
self::$crt->http_message_release($this->release());
parent::__destruct();
}
public function toBlob() {
return self::$crt->http_message_to_blob($this->native);
}
protected static function marshall($msg) {
$buf = "";
$buf .= Encoding::encodeString($msg->method);
$buf .= Encoding::encodeString($msg->pathAndQuery());
$buf .= Headers::marshall($msg->headers);
return $buf;
}
protected static function _unmarshall($buf, $class=Message::class) {
$method = Encoding::readString($buf);
$path_and_query = Encoding::readString($buf);
$parts = explode("?", $path_and_query, 2);
$path = isset($parts[0]) ? $parts[0] : "";
$query = isset($parts[1]) ? $parts[1] : "";
$headers = Headers::unmarshall($buf);
// Turn query params back into a dictionary
if (strlen($query)) {
$query = rawurldecode($query);
$query = explode("&", $query);
$query = array_reduce($query, function($params, $pair) {
list($param, $value) = explode("=", $pair, 2);
$params[$param] = $value;
return $params;
}, []);
} else {
$query = [];
}
return new $class($method, $path, $query, $headers->toArray());
}
public function pathAndQuery() {
$path = $this->path;
$queries = [];
foreach ($this->query as $param => $value) {
$queries []= urlencode($param) . "=" . urlencode($value);
}
$query = implode("&", $queries);
if (strlen($query)) {
$path = implode("?", [$path, $query]);
}
return $path;
}
public function method() {
return $this->method;
}
public function path() {
return $this->path;
}
public function query() {
return $this->query;
}
public function headers() {
return $this->headers;
}
}

View File

@ -0,0 +1,32 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\HTTP;
use AWS\CRT\IO\InputStream;
class Request extends Message {
private $body_stream = null;
public function __construct($method, $path, $query = [], $headers = [], $body_stream = null) {
parent::__construct($method, $path, $query, $headers);
if (!is_null($body_stream) && !($body_stream instanceof InputStream)) {
throw new \InvalidArgumentException('body_stream must be an instance of ' . InputStream::class);
}
$this->body_stream = $body_stream;
}
public static function marshall($request) {
return parent::marshall($request);
}
public static function unmarshall($buf) {
return parent::_unmarshall($buf, Request::class);
}
public function body_stream() {
return $this->body_stream;
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\HTTP;
class Response extends Message {
private $status_code;
public function __construct($method, $path, $query, $headers, $status_code) {
parent::__construct($method, $path, $query, $headers);
$this->status_code = $status_code;
}
public static function marshall($response) {
return parent::marshall($response);
}
public static function unmarshall($buf) {
return parent::_unmarshall($buf, Response::class);
}
public function status_code() {
return $this->status_code;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\IO;
use AWS\CRT\NativeResource as NativeResource;
use AWS\CRT\Options as Options;
/**
* Represents 1 or more event loops (1 per thread) for doing I/O and background tasks.
* Typically, every application has one EventLoopGroup.
*
* @param array options:
* - int num_threads - Number of worker threads in the EventLoopGroup. Defaults to 0/1 per logical core.
*/
final class EventLoopGroup extends NativeResource {
static function defaults() {
return [
'max_threads' => 0,
];
}
function __construct(array $options = []) {
parent::__construct();
$options = new Options($options, self::defaults());
$elg_options = self::$crt->event_loop_group_options_new();
self::$crt->event_loop_group_options_set_max_threads($elg_options, $options->getInt('max_threads'));
$this->acquire(self::$crt->event_loop_group_new($elg_options));
self::$crt->event_loop_group_options_release($elg_options);
}
function __destruct() {
self::$crt->event_loop_group_release($this->release());
parent::__destruct();
}
}

View File

@ -0,0 +1,50 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\IO;
use AWS\CRT\NativeResource as NativeResource;
final class InputStream extends NativeResource {
private $stream = null;
const SEEK_BEGIN = 0;
const SEEK_END = 2;
public function __construct($stream) {
parent::__construct();
$this->stream = $stream;
$options = self::$crt->input_stream_options_new();
// The stream implementation in native just converts the PHP stream into
// a native php_stream* and executes operations entirely in native
self::$crt->input_stream_options_set_user_data($options, $stream);
$this->acquire(self::$crt->input_stream_new($options));
self::$crt->input_stream_options_release($options);
}
public function __destruct() {
$this->release();
parent::__destruct();
}
public function eof() {
return self::$crt->input_stream_eof($this->native);
}
public function length() {
return self::$crt->input_stream_get_length($this->native);
}
public function read($length = 0) {
if ($length == 0) {
$length = $this->length();
}
return self::$crt->input_stream_read($this->native, $length);
}
public function seek($offset, $basis) {
return self::$crt->input_stream_seek($this->native, $offset, $basis);
}
}

View File

@ -0,0 +1,37 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\Internal;
final class Encoding {
public static function readString(&$buffer) {
list($len, $str) = self::decodeString($buffer);
// Advance by sizeof(length) + strlen(str)
$buffer = substr($buffer, 4 + $len);
return $str;
}
public static function readStrings($buffer) {
$strings = [];
while (strlen($buffer)) {
$strings []= self::readString($buffer);
}
return $strings;
}
public static function decodeString($buffer) {
$len = unpack("N", $buffer)[1];
$buffer = substr($buffer, 4);
$str = unpack("a{$len}", $buffer)[1];
return [$len, $str];
}
public static function encodeString($str) {
if (is_array($str)) {
$str = $str[0];
}
return pack("Na*", strlen($str), $str);
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT\Internal;
use \RuntimeException;
/**
* @internal
* Forwards calls on to awscrt PHP extension functions
*/
final class Extension {
function __construct() {
if (!extension_loaded('awscrt')) {
throw new RuntimeException('awscrt extension is not loaded');
}
}
/**
* Forwards any call made on this object to the extension function of the
* same name with the supplied arguments. Argument type hinting and checking
* occurs at the CRT wrapper.
*/
function __call($name, $args) {
return call_user_func_array($name, $args);
}
}

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

@ -0,0 +1,47 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT;
use AWS\CRT\CRT;
final class Log {
const NONE = 0;
const FATAL = 1;
const ERROR = 2;
const WARN = 3;
const INFO = 4;
const DEBUG = 5;
const TRACE = 6;
public static function toStdout() {
CRT::log_to_stdout();
}
public static function toStderr() {
CRT::log_to_stderr();
}
public static function toFile($filename) {
CRT::log_to_file($filename);
}
public static function toStream($stream) {
assert(get_resource_type($stream) == "stream");
CRT::log_to_stream($stream);
}
public static function stop() {
CRT::log_stop();
}
public static function setLogLevel($level) {
assert($level >= self::NONE && $level <= self::TRACE);
CRT::log_set_level($level);
}
public static function log($level, $message) {
CRT::log_message($level, $message);
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT;
use AWS\CRT\CRT as CRT;
/**
* Base class for all native resources, tracks all outstanding resources
* and provides basic leak checking
*/
abstract class NativeResource {
protected static $crt = null;
protected static $resources = [];
protected $native = null;
protected function __construct() {
if (is_null(self::$crt)) {
self::$crt = new CRT();
}
self::$resources[spl_object_hash($this)] = 1;
}
protected function acquire($handle) {
return $this->native = $handle;
}
protected function release() {
$native = $this->native;
$this->native = null;
return $native;
}
function __destruct() {
// Should have been destroyed and released by derived resource
assert($this->native == null);
unset(self::$resources[spl_object_hash($this)]);
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
namespace AWS\CRT;
final class OptionValue {
private $value;
function __construct($value) {
$this->value = $value;
}
public function asObject() {
return $this->value;
}
public function asMixed() {
return $this->value;
}
public function asInt() {
return empty($this->value) ? 0 : (int)$this->value;
}
public function asBool() {
return boolval($this->value);
}
public function asString() {
return !empty($this->value) ? strval($this->value) : "";
}
public function asArray() {
return is_array($this->value) ? $this->value : (!empty($this->value) ? [$this->value] : []);
}
public function asCallable() {
return is_callable($this->value) ? $this->value : null;
}
}
final class Options {
private $options;
public function __construct($opts = [], $defaults = []) {
$this->options = array_replace($defaults, empty($opts) ? [] : $opts);
}
public function __get($name) {
return $this->get($name);
}
public function asArray() {
return $this->options;
}
public function toArray() {
return array_merge_recursive([], $this->options);
}
public function get($name) {
return new OptionValue($this->options[$name]);
}
public function getInt($name) {
return $this->get($name)->asInt();
}
public function getString($name) {
return $this->get($name)->asString();
}
public function getBool($name) {
return $this->get($name)->asBool();
}
}

4
vendor/aws/aws-sdk-php/CODE_OF_CONDUCT.md vendored Executable file
View File

@ -0,0 +1,4 @@
## Code of Conduct
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
opensource-codeofconduct@amazon.com with any additional questions or comments.

4
vendor/aws/aws-sdk-php/CRT_INSTRUCTIONS.md vendored Executable file
View File

@ -0,0 +1,4 @@
## Building and enabling the Common Run Time
1. **Follow instructions on crt repo** Clone and build the repo as shown [here][https://github.com/awslabs/aws-crt-php].
1. **Enable the CRT** add the following line to your php.ini file `extension=path/to/aws-crt-php/modules/awscrt.so`

141
vendor/aws/aws-sdk-php/LICENSE vendored Executable file
View File

@ -0,0 +1,141 @@
# Apache License
Version 2.0, January 2004
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
## 1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1
through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the
License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled
by, or are under common control with that entity. For the purposes of this definition, "control" means
(i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract
or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial
ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software
source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form,
including but not limited to compiled object code, generated documentation, and conversions to other media
types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License,
as indicated by a copyright notice that is included in or attached to the work (an example is provided in the
Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from)
the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent,
as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not
include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work
and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any
modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to
Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to
submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of
electronic, verbal, or written communication sent to the Licensor or its representatives, including but not
limited to communication on electronic mailing lists, source code control systems, and issue tracking systems
that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been
received by Licensor and subsequently incorporated within the Work.
## 2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
## 3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent
license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such
license applies only to those patent claims licensable by such Contributor that are necessarily infringed by
their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such
Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim
or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work
constitutes direct or contributory patent infringement, then any patent licenses granted to You under this
License for that Work shall terminate as of the date such litigation is filed.
## 4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You meet the following conditions:
1. You must give any other recipients of the Work or Derivative Works a copy of this License; and
2. You must cause any modified files to carry prominent notices stating that You changed the files; and
3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent,
trademark, and attribution notices from the Source form of the Work, excluding those notices that do
not pertain to any part of the Derivative Works; and
4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that
You distribute must include a readable copy of the attribution notices contained within such NOTICE
file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed as part of the Derivative Works; within
the Source form or documentation, if provided along with the Derivative Works; or, within a display
generated by the Derivative Works, if and wherever such third-party notices normally appear. The
contents of the NOTICE file are for informational purposes only and do not modify the License. You may
add Your own attribution notices within Derivative Works that You distribute, alongside or as an
addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be
construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license
terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative
Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the
conditions stated in this License.
## 5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by
You to the Licensor shall be under the terms and conditions of this License, without any additional terms or
conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate
license agreement you may have executed with Licensor regarding such Contributions.
## 6. Trademarks.
This License does not grant permission to use the trade names, trademarks, service marks, or product names of
the Licensor, except as required for reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
## 7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor
provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT,
MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
## 8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless
required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any
Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential
damages of any character arising as a result of this License or out of the use or inability to use the Work
(including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has been advised of the possibility
of such damages.
## 9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for,
acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole
responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold
each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

17
vendor/aws/aws-sdk-php/NOTICE vendored Executable file
View File

@ -0,0 +1,17 @@
# AWS SDK for PHP
<http://aws.amazon.com/php>
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License").
You may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

84
vendor/aws/aws-sdk-php/THIRD-PARTY-LICENSES vendored Executable file
View File

@ -0,0 +1,84 @@
The AWS SDK for PHP includes the following third-party software/licensing:
** Guzzle - https://github.com/guzzle/guzzle
Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
----------------
** jmespath.php - https://github.com/mtdowling/jmespath.php
Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
----------------
** phpunit-mock-objects -- https://github.com/sebastianbergmann/phpunit-mock-objects
Copyright (c) 2002-2018, Sebastian Bergmann <sebastian@phpunit.de>.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of Sebastian Bergmann nor the names of his
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

73
vendor/aws/aws-sdk-php/composer.json vendored Executable file
View File

@ -0,0 +1,73 @@
{
"name": "aws/aws-sdk-php",
"homepage": "http://aws.amazon.com/sdkforphp",
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
"keywords": ["aws","amazon","sdk","s3","ec2","dynamodb","cloud","glacier"],
"type": "library",
"license": "Apache-2.0",
"authors": [
{
"name": "Amazon Web Services",
"homepage": "http://aws.amazon.com"
}
],
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues"
},
"require": {
"php": ">=8.1",
"guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^2.4.5",
"guzzlehttp/promises": "^2.0",
"mtdowling/jmespath.php": "^2.8.0",
"ext-pcre": "*",
"ext-json": "*",
"ext-simplexml": "*",
"aws/aws-crt-php": "^1.2.3",
"psr/http-message": "^1.0 || ^2.0",
"symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0"
},
"require-dev": {
"composer/composer" : "^2.7.8",
"ext-openssl": "*",
"ext-dom": "*",
"ext-sockets": "*",
"phpunit/phpunit": "^9.6",
"behat/behat": "~3.0",
"doctrine/cache": "~1.4",
"aws/aws-php-sns-message-validator": "~1.0",
"andrewsville/php-token-reflection": "^1.4",
"psr/cache": "^2.0 || ^3.0",
"psr/simple-cache": "^2.0 || ^3.0",
"sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0",
"yoast/phpunit-polyfills": "^2.0",
"dms/phpunit-arraysubset-asserts": "^0.4.0"
},
"suggest": {
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
"ext-curl": "To send requests using cURL",
"ext-sockets": "To use client-side monitoring",
"ext-pcntl": "To use client-side monitoring",
"doctrine/cache": "To use the DoctrineCacheAdapter",
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications"
},
"autoload": {
"psr-4": {
"Aws\\": "src/"
},
"files": ["src/functions.php"],
"exclude-from-classmap": ["src/data/"]
},
"autoload-dev": {
"psr-4": {
"Aws\\Test\\": "tests/"
},
"classmap": ["build/"]
},
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Aws\ACMPCA;
use Aws\AwsClient;
/**
* This client is used to interact with the **AWS Certificate Manager Private Certificate Authority** service.
* @method \Aws\Result createCertificateAuthority(array $args = [])
* @method \GuzzleHttp\Promise\Promise createCertificateAuthorityAsync(array $args = [])
* @method \Aws\Result createCertificateAuthorityAuditReport(array $args = [])
* @method \GuzzleHttp\Promise\Promise createCertificateAuthorityAuditReportAsync(array $args = [])
* @method \Aws\Result createPermission(array $args = [])
* @method \GuzzleHttp\Promise\Promise createPermissionAsync(array $args = [])
* @method \Aws\Result deleteCertificateAuthority(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteCertificateAuthorityAsync(array $args = [])
* @method \Aws\Result deletePermission(array $args = [])
* @method \GuzzleHttp\Promise\Promise deletePermissionAsync(array $args = [])
* @method \Aws\Result deletePolicy(array $args = [])
* @method \GuzzleHttp\Promise\Promise deletePolicyAsync(array $args = [])
* @method \Aws\Result describeCertificateAuthority(array $args = [])
* @method \GuzzleHttp\Promise\Promise describeCertificateAuthorityAsync(array $args = [])
* @method \Aws\Result describeCertificateAuthorityAuditReport(array $args = [])
* @method \GuzzleHttp\Promise\Promise describeCertificateAuthorityAuditReportAsync(array $args = [])
* @method \Aws\Result getCertificate(array $args = [])
* @method \GuzzleHttp\Promise\Promise getCertificateAsync(array $args = [])
* @method \Aws\Result getCertificateAuthorityCertificate(array $args = [])
* @method \GuzzleHttp\Promise\Promise getCertificateAuthorityCertificateAsync(array $args = [])
* @method \Aws\Result getCertificateAuthorityCsr(array $args = [])
* @method \GuzzleHttp\Promise\Promise getCertificateAuthorityCsrAsync(array $args = [])
* @method \Aws\Result getPolicy(array $args = [])
* @method \GuzzleHttp\Promise\Promise getPolicyAsync(array $args = [])
* @method \Aws\Result importCertificateAuthorityCertificate(array $args = [])
* @method \GuzzleHttp\Promise\Promise importCertificateAuthorityCertificateAsync(array $args = [])
* @method \Aws\Result issueCertificate(array $args = [])
* @method \GuzzleHttp\Promise\Promise issueCertificateAsync(array $args = [])
* @method \Aws\Result listCertificateAuthorities(array $args = [])
* @method \GuzzleHttp\Promise\Promise listCertificateAuthoritiesAsync(array $args = [])
* @method \Aws\Result listPermissions(array $args = [])
* @method \GuzzleHttp\Promise\Promise listPermissionsAsync(array $args = [])
* @method \Aws\Result listTags(array $args = [])
* @method \GuzzleHttp\Promise\Promise listTagsAsync(array $args = [])
* @method \Aws\Result putPolicy(array $args = [])
* @method \GuzzleHttp\Promise\Promise putPolicyAsync(array $args = [])
* @method \Aws\Result restoreCertificateAuthority(array $args = [])
* @method \GuzzleHttp\Promise\Promise restoreCertificateAuthorityAsync(array $args = [])
* @method \Aws\Result revokeCertificate(array $args = [])
* @method \GuzzleHttp\Promise\Promise revokeCertificateAsync(array $args = [])
* @method \Aws\Result tagCertificateAuthority(array $args = [])
* @method \GuzzleHttp\Promise\Promise tagCertificateAuthorityAsync(array $args = [])
* @method \Aws\Result untagCertificateAuthority(array $args = [])
* @method \GuzzleHttp\Promise\Promise untagCertificateAuthorityAsync(array $args = [])
* @method \Aws\Result updateCertificateAuthority(array $args = [])
* @method \GuzzleHttp\Promise\Promise updateCertificateAuthorityAsync(array $args = [])
*/
class ACMPCAClient extends AwsClient {}

View File

@ -0,0 +1,9 @@
<?php
namespace Aws\ACMPCA\Exception;
use Aws\Exception\AwsException;
/**
* Represents an error interacting with the **AWS Certificate Manager Private Certificate Authority** service.
*/
class ACMPCAException extends AwsException {}

View File

@ -0,0 +1,31 @@
<?php
namespace Aws\AIOps;
use Aws\AwsClient;
/**
* This client is used to interact with the **AWS AI Ops** service.
* @method \Aws\Result createInvestigationGroup(array $args = [])
* @method \GuzzleHttp\Promise\Promise createInvestigationGroupAsync(array $args = [])
* @method \Aws\Result deleteInvestigationGroup(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteInvestigationGroupAsync(array $args = [])
* @method \Aws\Result deleteInvestigationGroupPolicy(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteInvestigationGroupPolicyAsync(array $args = [])
* @method \Aws\Result getInvestigationGroup(array $args = [])
* @method \GuzzleHttp\Promise\Promise getInvestigationGroupAsync(array $args = [])
* @method \Aws\Result getInvestigationGroupPolicy(array $args = [])
* @method \GuzzleHttp\Promise\Promise getInvestigationGroupPolicyAsync(array $args = [])
* @method \Aws\Result listInvestigationGroups(array $args = [])
* @method \GuzzleHttp\Promise\Promise listInvestigationGroupsAsync(array $args = [])
* @method \Aws\Result listTagsForResource(array $args = [])
* @method \GuzzleHttp\Promise\Promise listTagsForResourceAsync(array $args = [])
* @method \Aws\Result putInvestigationGroupPolicy(array $args = [])
* @method \GuzzleHttp\Promise\Promise putInvestigationGroupPolicyAsync(array $args = [])
* @method \Aws\Result tagResource(array $args = [])
* @method \GuzzleHttp\Promise\Promise tagResourceAsync(array $args = [])
* @method \Aws\Result untagResource(array $args = [])
* @method \GuzzleHttp\Promise\Promise untagResourceAsync(array $args = [])
* @method \Aws\Result updateInvestigationGroup(array $args = [])
* @method \GuzzleHttp\Promise\Promise updateInvestigationGroupAsync(array $args = [])
*/
class AIOpsClient extends AwsClient {}

View File

@ -0,0 +1,9 @@
<?php
namespace Aws\AIOps\Exception;
use Aws\Exception\AwsException;
/**
* Represents an error interacting with the **AWS AI Ops** service.
*/
class AIOpsException extends AwsException {}

View File

@ -0,0 +1,51 @@
<?php
namespace Aws\ARCRegionSwitch;
use Aws\AwsClient;
/**
* This client is used to interact with the **ARC - Region switch** service.
* @method \Aws\Result approvePlanExecutionStep(array $args = [])
* @method \GuzzleHttp\Promise\Promise approvePlanExecutionStepAsync(array $args = [])
* @method \Aws\Result cancelPlanExecution(array $args = [])
* @method \GuzzleHttp\Promise\Promise cancelPlanExecutionAsync(array $args = [])
* @method \Aws\Result createPlan(array $args = [])
* @method \GuzzleHttp\Promise\Promise createPlanAsync(array $args = [])
* @method \Aws\Result deletePlan(array $args = [])
* @method \GuzzleHttp\Promise\Promise deletePlanAsync(array $args = [])
* @method \Aws\Result getPlan(array $args = [])
* @method \GuzzleHttp\Promise\Promise getPlanAsync(array $args = [])
* @method \Aws\Result getPlanEvaluationStatus(array $args = [])
* @method \GuzzleHttp\Promise\Promise getPlanEvaluationStatusAsync(array $args = [])
* @method \Aws\Result getPlanExecution(array $args = [])
* @method \GuzzleHttp\Promise\Promise getPlanExecutionAsync(array $args = [])
* @method \Aws\Result getPlanInRegion(array $args = [])
* @method \GuzzleHttp\Promise\Promise getPlanInRegionAsync(array $args = [])
* @method \Aws\Result listPlanExecutionEvents(array $args = [])
* @method \GuzzleHttp\Promise\Promise listPlanExecutionEventsAsync(array $args = [])
* @method \Aws\Result listPlanExecutions(array $args = [])
* @method \GuzzleHttp\Promise\Promise listPlanExecutionsAsync(array $args = [])
* @method \Aws\Result listPlans(array $args = [])
* @method \GuzzleHttp\Promise\Promise listPlansAsync(array $args = [])
* @method \Aws\Result listPlansInRegion(array $args = [])
* @method \GuzzleHttp\Promise\Promise listPlansInRegionAsync(array $args = [])
* @method \Aws\Result listRoute53HealthChecks(array $args = [])
* @method \GuzzleHttp\Promise\Promise listRoute53HealthChecksAsync(array $args = [])
* @method \Aws\Result listRoute53HealthChecksInRegion(array $args = [])
* @method \GuzzleHttp\Promise\Promise listRoute53HealthChecksInRegionAsync(array $args = [])
* @method \Aws\Result listTagsForResource(array $args = [])
* @method \GuzzleHttp\Promise\Promise listTagsForResourceAsync(array $args = [])
* @method \Aws\Result startPlanExecution(array $args = [])
* @method \GuzzleHttp\Promise\Promise startPlanExecutionAsync(array $args = [])
* @method \Aws\Result tagResource(array $args = [])
* @method \GuzzleHttp\Promise\Promise tagResourceAsync(array $args = [])
* @method \Aws\Result untagResource(array $args = [])
* @method \GuzzleHttp\Promise\Promise untagResourceAsync(array $args = [])
* @method \Aws\Result updatePlan(array $args = [])
* @method \GuzzleHttp\Promise\Promise updatePlanAsync(array $args = [])
* @method \Aws\Result updatePlanExecution(array $args = [])
* @method \GuzzleHttp\Promise\Promise updatePlanExecutionAsync(array $args = [])
* @method \Aws\Result updatePlanExecutionStep(array $args = [])
* @method \GuzzleHttp\Promise\Promise updatePlanExecutionStepAsync(array $args = [])
*/
class ARCRegionSwitchClient extends AwsClient {}

View File

@ -0,0 +1,9 @@
<?php
namespace Aws\ARCRegionSwitch\Exception;
use Aws\Exception\AwsException;
/**
* Represents an error interacting with the **ARC - Region switch** service.
*/
class ARCRegionSwitchException extends AwsException {}

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