Compare commits
No commits in common. "45a0d84caab208c3c9d0f5cd70fdfb3e941c9efb" and "84a25a829cd607c1373444ac2efb89f92e775d33" have entirely different histories.
45a0d84caa
...
84a25a829c
@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -4,13 +4,5 @@
|
|||||||
*.jpeg
|
*.jpeg
|
||||||
.vscode/
|
.vscode/
|
||||||
*.zip
|
*.zip
|
||||||
*.csv
|
|
||||||
*.apib
|
|
||||||
*.jrxml
|
|
||||||
*.jasper
|
|
||||||
vouchers_oxxo/
|
vouchers_oxxo/
|
||||||
comprobantes/
|
comprobantes/
|
||||||
pack-plugin-versioned.php
|
|
||||||
*.txt
|
|
||||||
Callbell Public API.postman_collection.json
|
|
||||||
unms-swagger.json
|
|
||||||
|
|||||||
230
CHANGELOG.md
230
CHANGELOG.md
@ -1,201 +1,31 @@
|
|||||||
# CHANGELOG - SIIP WhatsApp Notifications Plugin
|
# CHANGELOG - siip-whatsapp-notifications
|
||||||
|
|
||||||
## 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
|
## VERSIÓN 4.1.0 - 15-01-2026
|
||||||
|
|
||||||
### 🚀 Nuevas Características (Features)
|
### 🚀 Nuevas Características (Features)
|
||||||
|
|
||||||
1. **Microservicio PDF (`pdf-cropper`)**:
|
1. **Microservicio PDF (`pdf-cropper`)**:
|
||||||
- Nuevo servicio en Python (FastAPI + Poppler) desplegado en Docker (Puerto 8050) para la conversión de PDF a Imagen.
|
* 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.
|
* 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").
|
* **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.
|
* **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**:
|
2. **Integración S3 / MinIO**:
|
||||||
- Implementación de `MinioStorageService`.
|
* Implementación de `MinioStorageService`.
|
||||||
- Los comprobantes (JPG) y Vouchers OXXO se suben automáticamente a MinIO en lugar de usar FTP.
|
* Los comprobantes (JPG) y Vouchers OXXO se suben automáticamente a MinIO en lugar de usar FTP.
|
||||||
- Generación de URLs públicas seguras.
|
* Generación de URLs públicas seguras.
|
||||||
|
|
||||||
### 🧹 Mantenimiento y Limpieza (Chores/Refactor)
|
### 🧹 Mantenimiento y Limpieza (Chores/Refactor)
|
||||||
|
|
||||||
1. **Limpieza Automática (Cleanup)**:
|
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.
|
* 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`**:
|
2. **Refactor `ClientCallBellAPI`**:
|
||||||
- Eliminación de código muerto relacionado con FTP de WordPress.
|
* Eliminación de código muerto relacionado con FTP de WordPress.
|
||||||
- Implementación de cliente HTTP Guzzle para comunicación con `pdf-cropper`.
|
* 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.
|
* **Cache Busting**: Se añade Timestamp al nombre de archivo (`_time()`) para evitar caché agresivo en WhatsApp/Navegadores.
|
||||||
|
|
||||||
### 🐛 Correcciones (Bug Fixes)
|
### 🐛 Correcciones (Bug Fixes)
|
||||||
|
|
||||||
1. **Fix Recorte de Imagen**: Se eliminó la restricción de relación de aspecto 1:1 (cuadrado) para evitar recortes.
|
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.
|
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
|
## VERSIÓN 4.0.0 - 10-01-2026
|
||||||
|
|
||||||
### 🟢 Novedades (Re-diseño UI/UX + Integ. Pagos)
|
### 🟢 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.
|
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.
|
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.
|
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.
|
||||||
@ -203,119 +33,91 @@ _ Border superior prominente con color primario
|
|||||||
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.
|
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
|
### 🔵 Mejoras
|
||||||
|
|
||||||
1️⃣ **Navegación Intuitiva**: Menú lateral rediseñado con accesos directos claros a "Pagos Stripe", "Notificaciones" y "Gestión de Instaladores".
|
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.).
|
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.
|
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
|
## VERSIÓN 3.1.0 - 07-01-2026
|
||||||
|
|
||||||
### 🟢 Novedades
|
### 🟢 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.
|
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.
|
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
|
## VERSIÓN 3.0.0 - 02-01-2026
|
||||||
|
|
||||||
### 🟢 Novedades
|
### 🟢 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> ...`.
|
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".
|
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.
|
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
|
### 🔵 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.
|
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.
|
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`.
|
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`.
|
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.
|
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
|
### 🟡 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.
|
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.
|
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.
|
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
|
## VERSIÓN 2.9.3 - 23-12-2025
|
||||||
|
|
||||||
### 🟢 Novedades
|
### 🟢 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.
|
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).
|
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
|
### 🟡 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.
|
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
|
## VERSIÓN 2.9.2
|
||||||
|
|
||||||
### 🟡 Bugs Resueltos
|
### 🟡 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
|
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
|
## VERSIÓN 2.8.8
|
||||||
|
|
||||||
### 🟡 Bugs Resueltos
|
### 🟡 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.
|
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
|
## VERSIÓN 2.8.7
|
||||||
|
|
||||||
### 🟢 Novedades
|
### 🟢 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.
|
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
|
### 🔵 Mejoras
|
||||||
|
1️⃣ Se modificaron nodos del bot ***OXXO_BOT*** para poder adaptar esta actualización correctamente.
|
||||||
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.
|
2️⃣ Mejoras en el código fuente del flujo de trabajo para las referencias de OXXO PAGO.
|
||||||
|
|
||||||
## VERSIÓN 2.8.6
|
## VERSIÓN 2.8.6
|
||||||
|
|
||||||
### 🔵 Mejoras
|
### 🔵 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
|
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
|
## VERSIÓN 2.8.5
|
||||||
|
|
||||||
### 🔵 Mejoras
|
### 🔵 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.
|
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.
|
2️⃣ Se crearon nuevas plantillas de tipo utilidad y se adaptaron al flujo de trabajo en el código.
|
||||||
|
|
||||||
### 🟡 Bugs Resueltos
|
### 🟡 Bugs Resueltos
|
||||||
|
|
||||||
1️⃣ Se soluciono el bug que impedía enviar notificaciones a los instaladores para nuevas tareas.
|
1️⃣ Se soluciono el bug que impedía enviar notificaciones a los instaladores para nuevas tareas.
|
||||||
|
|
||||||
## VERSIÓN 2.8.2
|
## VERSIÓN 2.8.2
|
||||||
|
|
||||||
### 🟡 Bugs Resueltos
|
### 🟡 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.
|
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.
|
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
|
## VERSIÓN 2.8.1
|
||||||
|
|
||||||
### 🟡 Bugs Resueltos
|
### 🟡 Bugs Resueltos
|
||||||
|
|
||||||
1️⃣ No se enviaban las notificaciones de las tareas al instalador.
|
1️⃣ No se enviaban las notificaciones de las tareas al instalador.
|
||||||
|
|
||||||
## VERSIÓN 2.8.0
|
## VERSIÓN 2.8.0
|
||||||
|
|
||||||
### 🟢 Novedades
|
### 🟢 Novedades
|
||||||
|
|
||||||
1️⃣ Envío de contraseña de antena en el mensaje que se manda al instalador cuando se le asigna una tarea/servicio.
|
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.
|
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.
|
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.
|
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
|
### 🔵 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.
|
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.
|
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.
|
3️⃣ Se renombró el uso de la etiqueta “CREARCLABESTRIPE” por “CREAR CLABE STRIPE” para un mejor entendimiento de su uso.
|
||||||
|
|
||||||
### 🟡 Bugs Resueltos
|
### 🟡 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.
|
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.
|
||||||
|
|||||||
@ -1,88 +0,0 @@
|
|||||||
📢 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_
|
|
||||||
107
PHP_PermisosArchivos.php
Executable file
107
PHP_PermisosArchivos.php
Executable file
@ -0,0 +1,107 @@
|
|||||||
|
<?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 '';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
39
README.md
39
README.md
@ -1,45 +1,12 @@
|
|||||||
# SIIP - WhatsApp Notifications & Integrated Payment Portal
|
# SIIP - WhatsApp Notifications & Integrated Payment Portal
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
Este plugin es una solución integral que transforma tu UCRM en un **Portal Administrativo de Última Generación**. No solo automatiza la comunicación por WhatsApp, sino que integra un Dashboard completo para la gestión de pagos online (Stripe/OXXO), visualización de comprobantes y coordinación de equipos técnicos.
|
Este plugin es una solución integral que transforma tu UCRM en un **Portal Administrativo de Última Generación**. No solo automatiza la comunicación por WhatsApp, sino que integra un Dashboard completo para la gestión de pagos online (Stripe/OXXO), visualización de comprobantes y coordinación de equipos técnicos.
|
||||||
|
|
||||||
## ✨ Novedades v4.6.0 (Stripe & OXXO Stability)
|
|
||||||
|
|
||||||
- **🛡️ Estabilidad en Pagos (Stripe CashBalance)**: Nuevo sistema para comprobar fondos y validación contra intención de pagos para evitar las intenciones huérfanas o duplicadas tras recibir transferencias SPEI.
|
|
||||||
- **🧹 Limpieza y Auditoría Local**: Integración local de scripts `audit_incomplete_pi` y `clean_incomplete_pi` para mantenimiento seguro de intenciones de pago incompletas sin afectar comprobantes de OXXO vigentes.
|
|
||||||
- **🖼️ Interfaz OXXO Optimizada**: Rediseño interno y de contenedores CSS + validación del microservicio `puppeteer-server` para devolver fichas OXXO perfectamente legibles.
|
|
||||||
|
|
||||||
## ✨ Novedades v4.4.0 (Resend Job Notifications)
|
|
||||||
|
|
||||||
- **📋 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.
|
|
||||||
- **📨 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.
|
|
||||||
- **🔗 Integración con API de Scheduling**: Consulta dinámica de `GET /scheduling/jobs?assignedUserId=X&statuses[]=1` para listar tareas activas en tiempo real.
|
|
||||||
|
|
||||||
## 🐛 Hotfix v4.3.1 (Installer Notification Fix)
|
|
||||||
|
|
||||||
- **🔧 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".
|
|
||||||
|
|
||||||
## 🔐 Novedades v4.3.0 (Security & Premium UI)
|
|
||||||
|
|
||||||
- **🛡️ 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.
|
|
||||||
- **💎 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).
|
|
||||||
- **🚀 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.
|
|
||||||
|
|
||||||
## 🚀 Novedades v4.2.0 (Analytics & Sync)
|
|
||||||
|
|
||||||
- **📊 Visualizador de Pagos Mensuales**: Nueva herramienta de análisis que permite seleccionar cualquier mes y visualizar gráficamente:
|
|
||||||
- Estadísticas de clientes activos vs clientes que pagaron
|
|
||||||
- Gráfica de dona interactiva con Chart.js
|
|
||||||
- Listado detallado de clientes pendientes con saldos
|
|
||||||
- Porcentajes de cobranza en tiempo real
|
|
||||||
- **🔄 Sincronización Mejorada CallBell**: Fix crítico que garantiza la actualización automática del saldo y estado del cliente en CallBell cuando se agregan facturas o se modifican servicios.
|
|
||||||
- **🎯 Categorización Inteligente de Pagos**: Nuevo microservicio con acceso directo a la base de datos para obtener metadata de Stripe (tipo de pago) y asignar correctamente los atributos incluso cuando la API de UCRM no tiene la información.
|
|
||||||
- **⚙️ Configuración para Producción**: Sistema de `.env` implementado para gestión segura de credenciales de base de datos.
|
|
||||||
|
|
||||||
## 🚀 Novedades v4.1.0 (Performance & Storage)
|
## 🚀 Novedades v4.1.0 (Performance & Storage)
|
||||||
|
|
||||||
- **⚡ Microservicio PDF (`pdf-cropper`)**: Nuevo motor de renderizado externo (Python/FastAPI) que reemplaza librerías legacy, aumentando la velocidad y eliminando problemas de memoria en el servidor principal.
|
- **⚡ Microservicio PDF (`pdf-cropper`)**: Nuevo motor de renderizado externo (Python/FastAPI) que reemplaza librerías legacy, aumentando la velocidad y eliminando problemas de memoria en el servidor principal.
|
||||||
@ -59,19 +26,16 @@ Este plugin es una solución integral que transforma tu UCRM en un **Portal Admi
|
|||||||
## ⚡ Funcionalidades Principales
|
## ⚡ Funcionalidades Principales
|
||||||
|
|
||||||
### 1. Gestión de Cobranza Avanzada
|
### 1. Gestión de Cobranza Avanzada
|
||||||
|
|
||||||
- **Pagos SPEI**: Instrucciones inmediatas con CLABE personalizada.
|
- **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).
|
- **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.
|
- **Historial en Tiempo Real**: Consulta los últimos 10 pagos de cualquier cliente al instante.
|
||||||
|
|
||||||
### 2. Automatización WhatsApp (CallBell)
|
### 2. Automatización WhatsApp (CallBell)
|
||||||
|
|
||||||
- **Notificaciones Dinámicas**: Envío automático de facturas, recordatorios de pago y avisos de corte.
|
- **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.
|
- **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.
|
- **Sincronización Total**: Mantiene los contactos de CallBell siempre actualizados con los datos del CRM.
|
||||||
|
|
||||||
### 3. Coordinación Técnica (Jobs/Tasks)
|
### 3. Coordinación Técnica (Jobs/Tasks)
|
||||||
|
|
||||||
- **Agenda Inteligente**: Notifica a instaladores sobre nuevas tareas y reprogramaciones.
|
- **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.
|
- **Datos para Técnicos**: Envía coordenadas GPS y contraseñas de equipos (formato impresora térmica) directamente al WhatsApp del técnico.
|
||||||
|
|
||||||
@ -107,7 +71,6 @@ Para desbloquear todo el potencial, el plugin requiere:
|
|||||||
El plugin se configura directamente desde el panel de UCRM (`Ajustes -> Plugins -> siip-whatsapp-notifications`).
|
El plugin se configura directamente desde el panel de UCRM (`Ajustes -> Plugins -> siip-whatsapp-notifications`).
|
||||||
|
|
||||||
### Parámetros Críticos
|
### Parámetros Críticos
|
||||||
|
|
||||||
- `ipserver`: Tu dominio UCRM.
|
- `ipserver`: Tu dominio UCRM.
|
||||||
- `tokenstripe`: Tu Secret Key de Stripe.
|
- `tokenstripe`: Tu Secret Key de Stripe.
|
||||||
- `tokencallbell`: Tu API Key de CallBell.
|
- `tokencallbell`: Tu API Key de CallBell.
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
<?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";
|
|
||||||
}
|
|
||||||
175
consulta a Gemini.txt
Normal file
175
consulta a Gemini.txt
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
|
||||||
|
Pregunta: Algún microservicio que pueda levantar con docker compose y que funcione como API para pasarle un PDF y me devuelva una imagen y recortada en ciertas coordenadas o ajustada a formato cuadrado.
|
||||||
|
|
||||||
|
Respuesta: Para lo que necesitas, la mejor opción no es una herramienta genérica (como Gotenberg), ya que estas suelen convertir la página entera pero no permiten recortar por coordenadas en la misma petición.
|
||||||
|
|
||||||
|
La solución más limpia y eficiente es crear un microservicio ligero en Python (usando FastAPI y pdf2image) que puedas levantar con Docker. Python tiene las mejores librerías para manipulación de PDFs e imágenes.
|
||||||
|
|
||||||
|
Aquí tienes la solución completa "Copy-Paste":
|
||||||
|
|
||||||
|
1. Estructura del proyecto
|
||||||
|
Crea una carpeta (ej. pdf-cropper) con estos 3 archivos:
|
||||||
|
|
||||||
|
Dockerfile
|
||||||
|
|
||||||
|
main.py
|
||||||
|
|
||||||
|
docker-compose.yml
|
||||||
|
|
||||||
|
2. El Código (Python + FastAPI)
|
||||||
|
Este script recibe el PDF, lo convierte a imagen y aplica el recorte o ajuste cuadrado según lo que le pidas.
|
||||||
|
|
||||||
|
Archivo: main.py
|
||||||
|
|
||||||
|
Python
|
||||||
|
|
||||||
|
from fastapi import FastAPI, UploadFile, File, Form
|
||||||
|
from pdf2image import convert_from_bytes
|
||||||
|
from PIL import Image
|
||||||
|
import io
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.post("/process")
|
||||||
|
async def process_pdf(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
mode: str = Form("square"), # 'square' o 'coords'
|
||||||
|
x: int = Form(0),
|
||||||
|
y: int = Form(0),
|
||||||
|
w: int = Form(500),
|
||||||
|
h: int = Form(500)
|
||||||
|
):
|
||||||
|
# 1. Leer el archivo PDF en memoria
|
||||||
|
pdf_bytes = await file.read()
|
||||||
|
|
||||||
|
# 2. Convertir la primera página a imagen
|
||||||
|
# poppler_path no es necesario si se instala en el sistema (ver Dockerfile)
|
||||||
|
images = convert_from_bytes(pdf_bytes, fmt="jpeg")
|
||||||
|
|
||||||
|
if not images:
|
||||||
|
return {"error": "No se pudo leer el PDF"}
|
||||||
|
|
||||||
|
img = images[0] # Tomamos la primera página
|
||||||
|
|
||||||
|
# 3. Lógica de recorte
|
||||||
|
if mode == "square":
|
||||||
|
# Recorte cuadrado centrado automático
|
||||||
|
width, height = img.size
|
||||||
|
new_dim = min(width, height)
|
||||||
|
left = (width - new_dim) / 2
|
||||||
|
top = (height - new_dim) / 2
|
||||||
|
right = (width + new_dim) / 2
|
||||||
|
bottom = (height + new_dim) / 2
|
||||||
|
img = img.crop((left, top, right, bottom))
|
||||||
|
# Opcional: Redimensionar al tamaño final deseado
|
||||||
|
img = img.resize((w, h))
|
||||||
|
|
||||||
|
elif mode == "coords":
|
||||||
|
# Recorte por coordenadas exactas
|
||||||
|
img = img.crop((x, y, x + w, y + h))
|
||||||
|
|
||||||
|
# 4. Devolver la imagen procesada
|
||||||
|
img_byte_arr = io.BytesIO()
|
||||||
|
img.save(img_byte_arr, format='JPEG', quality=90)
|
||||||
|
img_byte_arr.seek(0)
|
||||||
|
|
||||||
|
from starlette.responses import StreamingResponse
|
||||||
|
return StreamingResponse(img_byte_arr, media_type="image/jpeg")
|
||||||
|
3. El Dockerfile
|
||||||
|
Es crucial instalar poppler-utils, que es el motor que procesa el PDF.
|
||||||
|
|
||||||
|
Archivo: Dockerfile
|
||||||
|
|
||||||
|
Dockerfile
|
||||||
|
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
# Instalar poppler (necesario para pdf2image)
|
||||||
|
RUN apt-get update && apt-get install -y poppler-utils && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Instalar dependencias de Python
|
||||||
|
RUN pip install fastapi uvicorn python-multipart pdf2image Pillow
|
||||||
|
|
||||||
|
COPY main.py .
|
||||||
|
|
||||||
|
# Ejecutar la API
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
4. Docker Compose
|
||||||
|
Para levantarlo.
|
||||||
|
|
||||||
|
Archivo: docker-compose.yml
|
||||||
|
|
||||||
|
YAML
|
||||||
|
|
||||||
|
services:
|
||||||
|
pdf-cropper:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:8000"
|
||||||
|
restart: always
|
||||||
|
Comando para iniciar:
|
||||||
|
|
||||||
|
Bash
|
||||||
|
|
||||||
|
docker compose up -d --build
|
||||||
|
5. Cómo usarlo desde PHP
|
||||||
|
Ahora tienes una API corriendo en http://localhost:8080/process. Aquí tienes el script PHP para enviarle un PDF y guardar la imagen recortada.
|
||||||
|
|
||||||
|
Archivo: cliente.php
|
||||||
|
|
||||||
|
PHP
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$pdfPath = 'documento.pdf';
|
||||||
|
$outputPath = 'resultado.jpg';
|
||||||
|
|
||||||
|
// URL de tu microservicio docker
|
||||||
|
$url = 'http://localhost:8080/process';
|
||||||
|
|
||||||
|
$curl = curl_init();
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'file' => new CURLFile($pdfPath, 'application/pdf', 'documento.pdf'),
|
||||||
|
|
||||||
|
// OPCIÓN A: Recorte cuadrado automático (centrado)
|
||||||
|
'mode' => 'square',
|
||||||
|
'w' => 800, // Tamaño final del cuadrado (px)
|
||||||
|
'h' => 800,
|
||||||
|
|
||||||
|
// OPCIÓN B: Coordenadas específicas (descomentar para usar)
|
||||||
|
// 'mode' => 'coords',
|
||||||
|
// 'x' => 100, // Píxeles desde la izquierda
|
||||||
|
// 'y' => 200, // Píxeles desde arriba
|
||||||
|
// 'w' => 500, // Ancho del recorte
|
||||||
|
// 'h' => 300 // Alto del recorte
|
||||||
|
];
|
||||||
|
|
||||||
|
curl_setopt_array($curl, [
|
||||||
|
CURLOPT_URL => $url,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $fields,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($curl);
|
||||||
|
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
|
||||||
|
if ($httpCode == 200) {
|
||||||
|
file_put_contents($outputPath, $response);
|
||||||
|
echo "Imagen guardada exitosamente en $outputPath";
|
||||||
|
} else {
|
||||||
|
echo "Error: " . curl_error($curl);
|
||||||
|
echo "Respuesta del servidor: " . $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_close($curl);
|
||||||
|
?>
|
||||||
|
¿Por qué esta solución?
|
||||||
|
Atomocidad: Hace la conversión y el recorte en un solo paso de memoria (RAM), sin guardar archivos temporales en disco.
|
||||||
|
|
||||||
|
Control: Puedes ajustar la lógica de "cuadrado" (si quieres que sea centrado o desde la esquina superior) modificando fácilmente el main.py.
|
||||||
|
|
||||||
|
Ligero: La imagen Docker pesará poco y consumirá memoria solo cuando procese un archivo.
|
||||||
@ -1 +1 @@
|
|||||||
{"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}
|
{"ipserver":"venus.siip.mx","apitoken":"gvcnIJqXdUjneVSjhl6THLlQcYXJyIFCcwHKVba2bvIrNraanCTb5VeoWuJ0TFZ9","unmsApiToken":"079c28f5-888c-457d-bd7a-0a4202590f75","tokencallbell":"g8thcZkXGd3xBj2g3TtYNYFMH1fuesbJ.b6a940ea7d78cf6c9e42f067b21c8ddf96e9fa2a9e307bfd0c7c7c4d7fa38f79","tokenstripe":"sk_test_51OkG0REFY1WEUtgRH6UxBK5pu80Aq5Iy8EcdPnf0cOWzuVLQTpyLCd7CbPzqMsWMafZOHElCxhEHF7g8boURjWlJ00tBwE0W1M","hostServerFTP":"siip.mx","usernameServerFTP":"siip0001","passServerFTP":"$spGiT,[wa)n","ipPuppeteer":"172.16.5.134","portPuppeteer":"4100","idPaymentAdminCRM":"1180","cashPaymentMethodId":false,"courtesyPaymentMethodId":false,"bankTransferPaymentMethodId":true,"paypalPaymentMethodId":true,"creditCardPaypalPaymentMethodId":true,"creditCardStripePaymentMethodId":true,"stripeSubscriptionCreditCardPaymentMethodId":true,"paypalSubscriptionPaymentMethodId":true,"mercadopagoPaymentMethodId":true,"checkPaymentMethodId":true,"customPaymentMethodId":true,"notificationTypeText":false,"installersDataWhatsApp":"{\r\n \"instaladores\": [\r\n {\r\n \"id\": 1019,\r\n \"nombre\": \"Mucio Robledo\",\r\n \"whatsapp\": \"4181878106\"\r\n },\r\n {\r\n \"id\": 1173,\r\n \"nombre\": \"Ángel Arvizu\",\r\n \"whatsapp\": \"4181878106\"\r\n },\r\n {\r\n \"id\": 1172,\r\n \"nombre\": \"Juan Rostro\",\r\n \"whatsapp\": \"4181878106\"\r\n },\r\n {\r\n \"id\": 1015,\r\n \"nombre\": \"Daniel Humberto\",\r\n \"whatsapp\": \"4181878106\"\r\n },\r\n {\r\n \"id\": 1131,\r\n \"nombre\": \"Gricelda Avalos\",\r\n \"whatsapp\": \"4181817609\"\r\n }\r\n ]\r\n}","debugMode":true,"logging_level":true,"minioEndpoint":"http://172.16.5.134:9002","minioPublicUrl":"https://aws-venus.siip.mx","minioAccessKey":"minioadmin","minioSecretKey":"minioadmin","minioBucket":"vouchers-oxxo"}
|
||||||
15992
data/plugin.log
Executable file → Normal file
15992
data/plugin.log
Executable file → Normal file
File diff suppressed because one or more lines are too long
@ -1,34 +0,0 @@
|
|||||||
<?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";
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
<?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";
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
@ -1,24 +0,0 @@
|
|||||||
<?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 +0,0 @@
|
|||||||
10
|
|
||||||
|
0
list_attributes.php
Executable file → Normal file
0
list_attributes.php
Executable file → Normal file
@ -5,38 +5,13 @@
|
|||||||
"displayName": "SIIP - Procesador de Pagos en línea con Stripe, Oxxo y Transferencia, Sincronizador de CallBell y Envío de Notificaciones y comprobantes vía WhatsApp",
|
"displayName": "SIIP - Procesador de Pagos en línea con Stripe, Oxxo y Transferencia, Sincronizador de CallBell y Envío de Notificaciones y comprobantes vía WhatsApp",
|
||||||
"description": "Este plugin sincroniza los clientes del sistema UISP CRM con los contactos de WhatsApp en CallBell, además procesa pagos de Stripe como las trasferencias bancarias y genera referencias de pago vía OXXO, además envía comprobantes de pago en formato imagen PNG o texto vía Whatsapp a los clientes",
|
"description": "Este plugin sincroniza los clientes del sistema UISP CRM con los contactos de WhatsApp en CallBell, además procesa pagos de Stripe como las trasferencias bancarias y genera referencias de pago vía OXXO, además envía comprobantes de pago en formato imagen PNG o texto vía Whatsapp a los clientes",
|
||||||
"url": "https://siip.mx/",
|
"url": "https://siip.mx/",
|
||||||
"version": "4.6.0",
|
"version": "4.1.0",
|
||||||
"unmsVersionCompliancy": {
|
"unmsVersionCompliancy": {
|
||||||
"min": "2.1.0",
|
"min": "2.1.0",
|
||||||
"max": null
|
"max": null
|
||||||
},
|
},
|
||||||
"author": "SIIP INTERNET",
|
"author": "SIIP INTERNET",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
{
|
|
||||||
"version": "4.6.0",
|
|
||||||
"date": "2026-04-11",
|
|
||||||
"changes": "Actualización: Resolución de duplicidad de intenciones de pago Stripe mediante validación CashBalance. Reparación del renderizado de fichas de OXXO, rediseño de OXXO UI y scripts de auditoría de PI."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "4.5.0",
|
|
||||||
"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",
|
"version": "4.1.0",
|
||||||
"date": "2026-01-15",
|
"date": "2026-01-15",
|
||||||
@ -214,20 +189,6 @@
|
|||||||
"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",
|
||||||
@ -295,7 +256,7 @@
|
|||||||
"menu": [
|
"menu": [
|
||||||
{
|
{
|
||||||
"key": "Reports",
|
"key": "Reports",
|
||||||
"label": "Portal Administrativo de Pagos de STRIPE y Notificaciones WhatsApp",
|
"label": "Administrar Notificaciones por WhatsApp",
|
||||||
"type": "admin",
|
"type": "admin",
|
||||||
"target": "iframe"
|
"target": "iframe"
|
||||||
}
|
}
|
||||||
|
|||||||
3728
public.php
3728
public.php
File diff suppressed because it is too large
Load Diff
@ -1,707 +0,0 @@
|
|||||||
<?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.");
|
|
||||||
}
|
|
||||||
@ -1,128 +0,0 @@
|
|||||||
<?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";
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
<?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";
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
<?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
@ -1,166 +0,0 @@
|
|||||||
<?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");
|
|
||||||
}
|
|
||||||
BIN
src/.DS_Store
vendored
BIN
src/.DS_Store
vendored
Binary file not shown.
@ -24,8 +24,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
const SUBJECT_OF_INSTALLER_CHANGE = ["se ha cancelado una tarea que tenías asignada con el folio ", "se te ha desasignado❌ la tarea con el folio "];
|
const SUBJECT_OF_INSTALLER_CHANGE = ["se ha cancelado una tarea que tenías asignada con el folio ", "se te ha desasignado❌ la tarea con el folio "];
|
||||||
const ADDITIONAL_CHANGE_DATA = ["Ya no es necesario realizar la visita técnica.", "En tu lugar asistirá el técnico 👷🏻♂️➡️ "];
|
const ADDITIONAL_CHANGE_DATA = ["Ya no es necesario realizar la visita técnica.", "En tu lugar asistirá el técnico 👷🏻♂️➡️ "];
|
||||||
|
|
||||||
public function __construct(Logger $logger, MessageTextFactory $messageTextFactory, SmsNumberProvider $clientPhoneNumber)
|
public function __construct(Logger $logger, MessageTextFactory $messageTextFactory, SmsNumberProvider $clientPhoneNumber) {
|
||||||
{
|
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
$this->messageTextFactory = $messageTextFactory;
|
$this->messageTextFactory = $messageTextFactory;
|
||||||
$this->clientPhoneNumber = $clientPhoneNumber;
|
$this->clientPhoneNumber = $clientPhoneNumber;
|
||||||
@ -42,30 +41,22 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
$this->ucrmApi = new UcrmApi($client, $config['apitoken'] ?? '');
|
$this->ucrmApi = new UcrmApi($client, $config['apitoken'] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function verifyPaymentActionToDo(NotificationData $notificationData): void
|
public function verifyPaymentActionToDo(NotificationData $notificationData): void {
|
||||||
{
|
|
||||||
$arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null);
|
$arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null);
|
||||||
foreach ($arrayPhones as $type => $phones) {
|
foreach ($arrayPhones as $type => $phones) {
|
||||||
$type = trim(strtolower($type));
|
$type = trim(strtolower($type));
|
||||||
if (!is_array($phones)) continue;
|
if (!is_array($phones)) continue;
|
||||||
foreach ($phones as $phone) {
|
foreach ($phones as $phone) {
|
||||||
switch ($type) {
|
switch ($type) {
|
||||||
case 'whatsapp':
|
case 'whatsapp': $this->notifyAndUpdate($notificationData, $phone); break;
|
||||||
$this->notifyAndUpdate($notificationData, $phone);
|
case 'whatsnotifica': $this->notify($notificationData, $phone); break;
|
||||||
break;
|
case 'whatsactualiza': $this->onlyUpdate($notificationData, $phone); break;
|
||||||
case 'whatsnotifica':
|
|
||||||
$this->notify($notificationData, $phone);
|
|
||||||
break;
|
|
||||||
case 'whatsactualiza':
|
|
||||||
$this->onlyUpdate($notificationData, $phone);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function verifyClientActionToDo(NotificationData $notificationData): void
|
public function verifyClientActionToDo(NotificationData $notificationData): void {
|
||||||
{
|
|
||||||
$arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null);
|
$arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null);
|
||||||
foreach ($arrayPhones as $type => $phones) {
|
foreach ($arrayPhones as $type => $phones) {
|
||||||
$type = trim(strtolower($type));
|
$type = trim(strtolower($type));
|
||||||
@ -76,8 +67,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function verifyServiceActionToDo(NotificationData $notificationData): void
|
public function verifyServiceActionToDo(NotificationData $notificationData): void {
|
||||||
{
|
|
||||||
$arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null);
|
$arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null);
|
||||||
foreach ($arrayPhones as $type => $phones) {
|
foreach ($arrayPhones as $type => $phones) {
|
||||||
$type = trim(strtolower($type));
|
$type = trim(strtolower($type));
|
||||||
@ -88,8 +78,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function verifyJobActionToDo($jsonNotificationData, $reprogramming = null, $changeInstaller = null): void
|
public function verifyJobActionToDo($jsonNotificationData, $reprogramming = null, $changeInstaller = null): void {
|
||||||
{
|
|
||||||
$this->logger->info('Iniciando verifyJobActionToDo');
|
$this->logger->info('Iniciando verifyJobActionToDo');
|
||||||
$clientId = $jsonNotificationData['extraData']['entity']['clientId'];
|
$clientId = $jsonNotificationData['extraData']['entity']['clientId'];
|
||||||
$installerId = $jsonNotificationData['extraData']['entity']['assignedUserId'];
|
$installerId = $jsonNotificationData['extraData']['entity']['assignedUserId'];
|
||||||
@ -105,10 +94,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
$installerWhatsApp = '';
|
$installerWhatsApp = '';
|
||||||
$installers = json_decode($config['installersDataWhatsApp'] ?? '{"instaladores":[]}', true);
|
$installers = json_decode($config['installersDataWhatsApp'] ?? '{"instaladores":[]}', true);
|
||||||
foreach ($installers['instaladores'] as $inst) {
|
foreach ($installers['instaladores'] as $inst) {
|
||||||
if ($inst['id'] == $installerId) {
|
if ($inst['id'] == $installerId) { $installerWhatsApp = $inst['whatsapp']; break; }
|
||||||
$installerWhatsApp = $inst['whatsapp'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($installerWhatsApp)) $this->logger->warning("No se encontró número de WhatsApp para el instalador ID: $installerId");
|
if (empty($installerWhatsApp)) $this->logger->warning("No se encontró número de WhatsApp para el instalador ID: $installerId");
|
||||||
@ -117,10 +103,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
$clientName = trim(($clientCRM['firstName'] ?? '') . ' ' . ($clientCRM['lastName'] ?? ''));
|
$clientName = trim(($clientCRM['firstName'] ?? '') . ' ' . ($clientCRM['lastName'] ?? ''));
|
||||||
$passCRM = '';
|
$passCRM = '';
|
||||||
foreach ($clientCRM['attributes'] as $attr) {
|
foreach ($clientCRM['attributes'] as $attr) {
|
||||||
if ($attr['key'] === 'passwordAntenaCliente') {
|
if ($attr['key'] === 'passwordAntenaCliente') { $passCRM = $attr['value']; break; }
|
||||||
$passCRM = $attr['value'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$allPhones = $this->clientPhoneNumber->getAllUcrmClientNumbers($clientCRM);
|
$allPhones = $this->clientPhoneNumber->getAllUcrmClientNumbers($clientCRM);
|
||||||
@ -154,10 +137,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
$prevAdmin = $this->ucrmApi->get("users/admins/$prevId", []);
|
$prevAdmin = $this->ucrmApi->get("users/admins/$prevId", []);
|
||||||
$prevWhatsApp = '';
|
$prevWhatsApp = '';
|
||||||
foreach ($installers['instaladores'] as $inst) {
|
foreach ($installers['instaladores'] as $inst) {
|
||||||
if ($inst['id'] == $prevId) {
|
if ($inst['id'] == $prevId) { $prevWhatsApp = $inst['whatsapp']; break; }
|
||||||
$prevWhatsApp = $inst['whatsapp'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if ($prevWhatsApp) {
|
if ($prevWhatsApp) {
|
||||||
$api->sendJobNotificationWhatsAppToInstaller($this->validarNumeroTelefono($prevWhatsApp), [
|
$api->sendJobNotificationWhatsAppToInstaller($this->validarNumeroTelefono($prevWhatsApp), [
|
||||||
@ -198,7 +178,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
"jobDescription" => $jsonNotificationData['extraData']['entity']['description'] ?? 'S/D',
|
"jobDescription" => $jsonNotificationData['extraData']['entity']['description'] ?? 'S/D',
|
||||||
"gmapsLocation" => ($clientCRM['addressGpsLat'] && $clientCRM['addressGpsLon']) ? "https://www.google.com/maps?q={$clientCRM['addressGpsLat']},{$clientCRM['addressGpsLon']}" : 'N/A',
|
"gmapsLocation" => ($clientCRM['addressGpsLat'] && $clientCRM['addressGpsLon']) ? "https://www.google.com/maps?q={$clientCRM['addressGpsLat']},{$clientCRM['addressGpsLon']}" : 'N/A',
|
||||||
"passwordAntenaCliente" => $this->comparePasswords($passCRM, $passVault)
|
"passwordAntenaCliente" => $this->comparePasswords($passCRM, $passVault)
|
||||||
], $reprogramming, false);
|
], $reprogramming, $changeInstaller);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Gestión del Título / Prefijos
|
// 3. Gestión del Título / Prefijos
|
||||||
@ -216,8 +196,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function verifyInvoiceActionToDo(NotificationData $notificationData): void
|
public function verifyInvoiceActionToDo(NotificationData $notificationData): void {
|
||||||
{
|
|
||||||
$arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null);
|
$arrayPhones = $this->clientPhoneNumber->getUcrmClientNumbers($notificationData, null);
|
||||||
foreach ($arrayPhones as $type => $phones) {
|
foreach ($arrayPhones as $type => $phones) {
|
||||||
$type = trim(strtolower($type));
|
$type = trim(strtolower($type));
|
||||||
@ -228,8 +207,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function notify(NotificationData $notificationData, $phoneToNotify = null): void
|
public function notify(NotificationData $notificationData, $phoneToNotify = null): void {
|
||||||
{
|
|
||||||
$config = PluginConfigManager::create()->loadConfig();
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
$api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']);
|
$api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']);
|
||||||
$phone = $this->validarNumeroTelefono($phoneToNotify);
|
$phone = $this->validarNumeroTelefono($phoneToNotify);
|
||||||
@ -238,8 +216,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
else $api->sendPaymentNotificationWhatsApp($phone, $notificationData);
|
else $api->sendPaymentNotificationWhatsApp($phone, $notificationData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function notifyAndUpdate(NotificationData $notificationData, $phoneToNotifyAndUpdate = null): void
|
public function notifyAndUpdate(NotificationData $notificationData, $phoneToNotifyAndUpdate = null): void {
|
||||||
{
|
|
||||||
$config = PluginConfigManager::create()->loadConfig();
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
$api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']);
|
$api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']);
|
||||||
$phone = $this->validarNumeroTelefono($phoneToNotifyAndUpdate);
|
$phone = $this->validarNumeroTelefono($phoneToNotifyAndUpdate);
|
||||||
@ -257,33 +234,22 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function notifyOverDue(NotificationData $notificationData): void
|
public function notifyOverDue(NotificationData $notificationData): void {
|
||||||
{
|
|
||||||
$config = PluginConfigManager::create()->loadConfig();
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
$api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']);
|
$api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']);
|
||||||
$phone = $this->clientPhoneNumber->getUcrmClientNumber($notificationData);
|
$phone = $this->clientPhoneNumber->getUcrmClientNumber($notificationData);
|
||||||
if ($phone) $api->sendOverdueNotificationWhatsApp($phone, $notificationData);
|
if ($phone) $api->sendOverdueNotificationWhatsApp($phone, $notificationData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onlyUpdate(NotificationData $notificationData, $phoneToUpdate): void
|
public function onlyUpdate(NotificationData $notificationData, $phoneToUpdate): void {
|
||||||
{
|
|
||||||
$this->logger->debug("onlyUpdate: Iniciando actualización para teléfono: $phoneToUpdate");
|
|
||||||
$config = PluginConfigManager::create()->loadConfig();
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
$api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']);
|
$api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']);
|
||||||
$phone = $this->validarNumeroTelefono($phoneToUpdate);
|
$phone = $this->validarNumeroTelefono($phoneToUpdate);
|
||||||
$this->logger->debug("onlyUpdate: Teléfono validado: $phone");
|
|
||||||
$contact = json_decode($api->getContactWhatsapp($phone), true);
|
$contact = json_decode($api->getContactWhatsapp($phone), true);
|
||||||
$this->logger->debug("onlyUpdate: Contacto obtenido de CallBell: " . json_encode($contact));
|
if ($contact) $api->patchWhatsapp($contact, $notificationData);
|
||||||
if ($contact) {
|
|
||||||
$this->logger->info("onlyUpdate: Ejecutando patchWhatsapp para teléfono: $phone");
|
|
||||||
$api->patchWhatsapp($contact, $notificationData);
|
|
||||||
} else {
|
|
||||||
$this->logger->warning("onlyUpdate: No se encontró contacto en CallBell para teléfono: $phone");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onlyUpdateService(NotificationData $notificationData, $phoneToUpdate): void
|
public function onlyUpdateService(NotificationData $notificationData, $phoneToUpdate): void {
|
||||||
{
|
|
||||||
$config = PluginConfigManager::create()->loadConfig();
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
$api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']);
|
$api = new ClientCallBellAPI($config['apitoken'], $config['ipserver'], $config['tokencallbell']);
|
||||||
$phone = $this->validarNumeroTelefono($phoneToUpdate);
|
$phone = $this->validarNumeroTelefono($phoneToUpdate);
|
||||||
@ -291,8 +257,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
if ($contact) $api->patchServiceStatusWhatsApp($contact, $notificationData);
|
if ($contact) $api->patchServiceStatusWhatsApp($contact, $notificationData);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getVaultCredentialsByClientId($clientId): string
|
protected function getVaultCredentialsByClientId($clientId): string {
|
||||||
{
|
|
||||||
$config = PluginConfigManager::create()->loadConfig();
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
$ipServer = $config['ipserver'] ?? '';
|
$ipServer = $config['ipserver'] ?? '';
|
||||||
$crm = new Client(['base_uri' => "https://{$ipServer}/crm/api/v1.0/", 'verify' => false]);
|
$crm = new Client(['base_uri' => "https://{$ipServer}/crm/api/v1.0/", 'verify' => false]);
|
||||||
@ -388,9 +353,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
$passVault = $vault['credentials'][0]['password'];
|
$passVault = $vault['credentials'][0]['password'];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) { continue; }
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($passVault) {
|
if ($passVault) {
|
||||||
@ -404,9 +367,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
'json' => [['username' => 'ubnt', 'password' => $newPass, 'readOnly' => true]]
|
'json' => [['username' => 'ubnt', 'password' => $newPass, 'readOnly' => true]]
|
||||||
]);
|
]);
|
||||||
$passwordValue = $newPass;
|
$passwordValue = $newPass;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) { $passwordValue = $newPass; }
|
||||||
$passwordValue = $newPass;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$passwordValue = "⚠️ Sin antena";
|
$passwordValue = "⚠️ Sin antena";
|
||||||
}
|
}
|
||||||
@ -428,14 +389,14 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
|
|
||||||
$this->syncPasswordWithCrm((int)$clientId, $finalValue);
|
$this->syncPasswordWithCrm((int)$clientId, $finalValue);
|
||||||
return $finalValue;
|
return $finalValue;
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->logger->error("Error en getVaultCredentialsByClientId: " . $e->getMessage());
|
$this->logger->error("Error en getVaultCredentialsByClientId: " . $e->getMessage());
|
||||||
return 'Error: ' . $e->getMessage();
|
return 'Error: ' . $e->getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function syncPasswordWithCrm(int $clientId, string $passVault): void
|
private function syncPasswordWithCrm(int $clientId, string $passVault): void {
|
||||||
{
|
|
||||||
$config = PluginConfigManager::create()->loadConfig();
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
$crm = new Client(['base_uri' => "https://{$config['ipserver']}/crm/api/v1.0/", 'verify' => false]);
|
$crm = new Client(['base_uri' => "https://{$config['ipserver']}/crm/api/v1.0/", 'verify' => false]);
|
||||||
try {
|
try {
|
||||||
@ -458,13 +419,10 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
$this->logger->info("Sincronizando pass CRM cliente $clientId.");
|
$this->logger->info("Sincronizando pass CRM cliente $clientId.");
|
||||||
$this->patchClientCustomAttribute($clientId, (int)$attributeId, $passVault);
|
$this->patchClientCustomAttribute($clientId, (int)$attributeId, $passVault);
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) { $this->logger->warning("Fallo sincronización pass CRM: " . $e->getMessage()); }
|
||||||
$this->logger->warning("Fallo sincronización pass CRM: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function generateStrongPassword(int $length = 16): string
|
protected function generateStrongPassword(int $length = 16): string {
|
||||||
{
|
|
||||||
$lower = 'abcdefghijkmnopqrstuvwxyz'; // Eliminamos 'l'
|
$lower = 'abcdefghijkmnopqrstuvwxyz'; // Eliminamos 'l'
|
||||||
$upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // Eliminamos 'I', 'O'
|
$upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // Eliminamos 'I', 'O'
|
||||||
$digits = '23456789'; // Eliminamos '1', '0'
|
$digits = '23456789'; // Eliminamos '1', '0'
|
||||||
@ -495,8 +453,7 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
return implode('', $pwChars);
|
return implode('', $pwChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function patchClientCustomAttribute(int $clientId, int $attributeId, string $value): bool
|
protected function patchClientCustomAttribute(int $clientId, int $attributeId, string $value): bool {
|
||||||
{
|
|
||||||
$config = PluginConfigManager::create()->loadConfig();
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
$crm = new Client(['base_uri' => "https://{$config['ipserver']}/crm/api/v1.0/", 'verify' => false]);
|
$crm = new Client(['base_uri' => "https://{$config['ipserver']}/crm/api/v1.0/", 'verify' => false]);
|
||||||
try {
|
try {
|
||||||
@ -513,15 +470,13 @@ abstract class AbstractMessageNotifierFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function comparePasswords(?string $crm, ?string $vault): string
|
protected function comparePasswords(?string $crm, ?string $vault): string {
|
||||||
{
|
|
||||||
if ($vault && strpos($vault, 'Error') !== 0) return $vault;
|
if ($vault && strpos($vault, 'Error') !== 0) return $vault;
|
||||||
if ($crm && strpos($crm, 'Error') !== 0) return $crm;
|
if ($crm && strpos($crm, 'Error') !== 0) return $crm;
|
||||||
return '⚠️ Probar pass conocida.';
|
return '⚠️ Probar pass conocida.';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function validarNumeroTelefono($n): string
|
protected function validarNumeroTelefono($n): string {
|
||||||
{
|
|
||||||
if (!$n) return '';
|
if (!$n) return '';
|
||||||
$n = preg_replace('/\D/', '', (string)$n);
|
$n = preg_replace('/\D/', '', (string)$n);
|
||||||
return (strlen($n) === 10) ? '52' . $n : $n;
|
return (strlen($n) === 10) ? '52' . $n : $n;
|
||||||
|
|||||||
@ -21,8 +21,7 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
protected $ucrmApi;
|
protected $ucrmApi;
|
||||||
private $systemAttributesCache = null;
|
private $systemAttributesCache = null;
|
||||||
|
|
||||||
public function __construct(Logger $logger, MessageTextFactory $messageTextFactory, SmsNumberProvider $clientPhoneNumber)
|
public function __construct(Logger $logger, MessageTextFactory $messageTextFactory, SmsNumberProvider $clientPhoneNumber) {
|
||||||
{
|
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
$this->messageTextFactory = $messageTextFactory;
|
$this->messageTextFactory = $messageTextFactory;
|
||||||
$this->clientPhoneNumber = $clientPhoneNumber;
|
$this->clientPhoneNumber = $clientPhoneNumber;
|
||||||
@ -39,8 +38,7 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
$this->ucrmApi = new UcrmApi($client, $config['apitoken'] ?? '');
|
$this->ucrmApi = new UcrmApi($client, $config['apitoken'] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createPaymentIntent(array $eventJson): void
|
public function createPaymentIntent(array $eventJson): void {
|
||||||
{
|
|
||||||
$config = PluginConfigManager::create()->loadConfig();
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
$stripe = new StripeClient($config['tokenstripe']);
|
$stripe = new StripeClient($config['tokenstripe']);
|
||||||
|
|
||||||
@ -55,20 +53,6 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
try {
|
try {
|
||||||
$stripeCustomer = $stripe->customers->retrieve($customer);
|
$stripeCustomer = $stripe->customers->retrieve($customer);
|
||||||
$ucrmClientId = $stripeCustomer->metadata->ucrm_client_id ?? null;
|
$ucrmClientId = $stripeCustomer->metadata->ucrm_client_id ?? null;
|
||||||
// [FIX] Validar el Saldo Disponible en la Billetera (Cash Balance)
|
|
||||||
// Si la cuenta tiene reconciliación automática, Stripe absorbe fondos instantáneamente
|
|
||||||
// para pagar intenciones preexistentes. Si ya se gastaron los fondos, evitamos duplicar.
|
|
||||||
$cashBalance = $stripe->customers->retrieveCashBalance($customer, []);
|
|
||||||
$availableMxn = $cashBalance->available->mxn ?? 0;
|
|
||||||
$amountCents = (int)$amount;
|
|
||||||
|
|
||||||
// Si el saldo disponible es menor al monto fondeado, significa que una PI
|
|
||||||
// preexistente ya "se tragó" este saldo automáticamente.
|
|
||||||
if ($availableMxn < $amountCents) {
|
|
||||||
$this->logger->info("Fondos ya reconciliados por Stripe. Saldo disponible ({$availableMxn}) es menor al evento ({$amountCents}). Se descarta la creación de un nuevo PaymentIntent duplicado.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$pi = $stripe->paymentIntents->create([
|
$pi = $stripe->paymentIntents->create([
|
||||||
'amount' => (int)$amount,
|
'amount' => (int)$amount,
|
||||||
@ -89,15 +73,14 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
'signedInAdminId' => $config['idPaymentAdminCRM'],
|
'signedInAdminId' => $config['idPaymentAdminCRM'],
|
||||||
'tipoPago' => 'Transferencia Bancaria'
|
'tipoPago' => 'Transferencia Bancaria'
|
||||||
],
|
],
|
||||||
], ['idempotency_key' => $eventJson['id'] ?? null]);
|
]);
|
||||||
$this->logger->info("PaymentIntent creado: " . $pi->id);
|
$this->logger->info("PaymentIntent creado: " . $pi->id);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->logger->error("Error creando PaymentIntent: " . $e->getMessage());
|
$this->logger->error("Error creando PaymentIntent: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function registerPaymentFromWebhook(array $eventJson): void
|
public function registerPaymentFromWebhook(array $eventJson): void {
|
||||||
{
|
|
||||||
$config = PluginConfigManager::create()->loadConfig();
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
$stripe = new StripeClient($config['tokenstripe']);
|
$stripe = new StripeClient($config['tokenstripe']);
|
||||||
$data = $eventJson['data']['object'];
|
$data = $eventJson['data']['object'];
|
||||||
@ -127,8 +110,7 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function registerPaymentFromIntent(array $data): void
|
public function registerPaymentFromIntent(array $data): void {
|
||||||
{
|
|
||||||
$piId = $data['id'] ?? null;
|
$piId = $data['id'] ?? null;
|
||||||
if (!$piId) return;
|
if (!$piId) return;
|
||||||
|
|
||||||
@ -153,31 +135,6 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [NEW] Check for duplicate payment (same PI ID)
|
|
||||||
try {
|
|
||||||
$existingPayments = $this->ucrmApi->get('payments', [
|
|
||||||
'clientId' => $clientId,
|
|
||||||
'limit' => 20,
|
|
||||||
'order' => 'createdDate',
|
|
||||||
'direction' => 'DESC'
|
|
||||||
]);
|
|
||||||
|
|
||||||
foreach ($existingPayments as $p) {
|
|
||||||
// Check note for PI ID
|
|
||||||
if (isset($p['note']) && strpos($p['note'], $piId) !== false) {
|
|
||||||
$this->logger->info("Pago duplicado detectado para PI $piId (ID existente: {$p['id']}). Omitiendo creación.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Also check duplicate by transaction ID if applicable
|
|
||||||
if (isset($p['transactionId']) && $p['transactionId'] === $piId) {
|
|
||||||
$this->logger->info("Pago duplicado detectado (Transaction ID) para PI $piId (ID existente: {$p['id']}). Omitiendo creación.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->logger->warning("Falló la verificación de duplicados para PI $piId: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Intentar detectar Payment Method name basado en tipo
|
// Intentar detectar Payment Method name basado en tipo
|
||||||
$type = $data['payment_method_types'][0] ?? 'card';
|
$type = $data['payment_method_types'][0] ?? 'card';
|
||||||
@ -209,8 +166,7 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function findClientIdByStripeCustomer(string $stripeCustomerId): ?int
|
private function findClientIdByStripeCustomer(string $stripeCustomerId): ?int {
|
||||||
{
|
|
||||||
try {
|
try {
|
||||||
// Nota: Esto puede ser lento si hay muchos clientes, pero es un fallback.
|
// Nota: Esto puede ser lento si hay muchos clientes, pero es un fallback.
|
||||||
// Idealmente usaríamos $this->ucrmApi->get('clients', ['customAttributeKey' => 'stripeCustomerId', ...]) si existiera ese filtro.
|
// Idealmente usaríamos $this->ucrmApi->get('clients', ['customAttributeKey' => 'stripeCustomerId', ...]) si existiera ese filtro.
|
||||||
@ -239,8 +195,7 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function findPaymentMethodId(string $name): ?int
|
private function findPaymentMethodId(string $name): ?int {
|
||||||
{
|
|
||||||
try {
|
try {
|
||||||
$methods = $this->ucrmApi->get('payment-methods');
|
$methods = $this->ucrmApi->get('payment-methods');
|
||||||
foreach ($methods as $m) {
|
foreach ($methods as $m) {
|
||||||
@ -248,13 +203,11 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
return $m['id'];
|
return $m['id'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch(\Exception $e) {}
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createStripeClient(NotificationData $notificationData, string $tagName, bool $generateSpei = true): void
|
public function createStripeClient(NotificationData $notificationData, string $tagName, bool $generateSpei = true): void {
|
||||||
{
|
|
||||||
$clientId = $notificationData->clientId;
|
$clientId = $notificationData->clientId;
|
||||||
if (!$clientId) return;
|
if (!$clientId) return;
|
||||||
|
|
||||||
@ -264,7 +217,21 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
try {
|
try {
|
||||||
$clientCRM = $this->ucrmApi->get("clients/$clientId", []);
|
$clientCRM = $this->ucrmApi->get("clients/$clientId", []);
|
||||||
|
|
||||||
// Automaticamente creará el el Stripe Customer si no existe
|
// Si intenta crear CLABE pero NO tiene stripeCustomerId, cancelamos
|
||||||
|
if ($tagName === 'CREAR CLABE STRIPE') {
|
||||||
|
$hasStripeId = false;
|
||||||
|
foreach ($clientCRM['attributes'] as $attr) {
|
||||||
|
if ($attr['key'] === 'stripeCustomerId' && !empty($attr['value'])) {
|
||||||
|
$hasStripeId = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$hasStripeId) {
|
||||||
|
$this->logger->warning("Cliente $clientId no tiene stripeCustomerId. No se puede crear CLABE.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$customer = $this->createCustomerStripe($stripe, $clientCRM, $generateSpei);
|
$customer = $this->createCustomerStripe($stripe, $clientCRM, $generateSpei);
|
||||||
|
|
||||||
@ -279,8 +246,7 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createCustomerStripe(StripeClient $stripe, array $clientCRM, bool $generateSpei): ?\Stripe\Customer
|
protected function createCustomerStripe(StripeClient $stripe, array $clientCRM, bool $generateSpei): ?\Stripe\Customer {
|
||||||
{
|
|
||||||
$clientId = $clientCRM['id'];
|
$clientId = $clientCRM['id'];
|
||||||
|
|
||||||
// Extraer email de contactos (prioridad) o username
|
// Extraer email de contactos (prioridad) o username
|
||||||
@ -325,10 +291,9 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
|
|
||||||
$customer = $stripe->customers->create($params);
|
$customer = $stripe->customers->create($params);
|
||||||
$this->logger->info("Nuevo Cliente Stripe creado para ID: $clientId. CID: {$customer->id}");
|
$this->logger->info("Nuevo Cliente Stripe creado para ID: $clientId. CID: {$customer->id}");
|
||||||
}
|
// Guardar CID en UCRM
|
||||||
|
|
||||||
// Guardar CID en UCRM siempre, por si no estaba sincronizado
|
|
||||||
$this->patchClientCustomAttribute($clientId, (int)$cidAttrId, $customer->id);
|
$this->patchClientCustomAttribute($clientId, (int)$cidAttrId, $customer->id);
|
||||||
|
}
|
||||||
|
|
||||||
// Si se requiere SPEI, generamos las instrucciones de fondeo para obtener la CLABE
|
// Si se requiere SPEI, generamos las instrucciones de fondeo para obtener la CLABE
|
||||||
if ($generateSpei) {
|
if ($generateSpei) {
|
||||||
@ -359,8 +324,7 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
return $customer;
|
return $customer;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getVaultCredentialsByClientId($clientId): string
|
protected function getVaultCredentialsByClientId($clientId): string {
|
||||||
{
|
|
||||||
$config = PluginConfigManager::create()->loadConfig();
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
$ipServer = $config['ipserver'] ?? '';
|
$ipServer = $config['ipserver'] ?? '';
|
||||||
|
|
||||||
@ -424,11 +388,6 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
|
|
||||||
if (!$foundInCRM) {
|
if (!$foundInCRM) {
|
||||||
$passwordValue = $this->generateStrongPassword(16);
|
$passwordValue = $this->generateStrongPassword(16);
|
||||||
// [NEW] Static Test Data for Site/Antenna
|
|
||||||
$this->syncNetworkDataWithCrm((int)$clientId, 'VENUS', 'Sectorial de pruebas 172.16.5.134');
|
|
||||||
} else {
|
|
||||||
// Even if found, ensure static data in test env
|
|
||||||
$this->syncNetworkDataWithCrm((int)$clientId, 'VENUS', 'Sectorial de pruebas 172.16.5.134');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Lógica de producción
|
// Lógica de producción
|
||||||
@ -456,9 +415,7 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
$passVault = $vault['credentials'][0]['password'];
|
$passVault = $vault['credentials'][0]['password'];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) { continue; }
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($passVault) {
|
if ($passVault) {
|
||||||
@ -472,9 +429,7 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
'json' => [['username' => 'ubnt', 'password' => $newPass, 'readOnly' => true]]
|
'json' => [['username' => 'ubnt', 'password' => $newPass, 'readOnly' => true]]
|
||||||
]);
|
]);
|
||||||
$passwordValue = $newPass;
|
$passwordValue = $newPass;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) { $passwordValue = $newPass; }
|
||||||
$passwordValue = $newPass;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$passwordValue = "⚠️ Sin antena";
|
$passwordValue = "⚠️ Sin antena";
|
||||||
}
|
}
|
||||||
@ -496,31 +451,14 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
|
|
||||||
$this->syncPasswordWithCrm((int)$clientId, $finalValue);
|
$this->syncPasswordWithCrm((int)$clientId, $finalValue);
|
||||||
return $finalValue;
|
return $finalValue;
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->logger->error("Excepción en getVaultCredentialsByClientId (Cliente: $clientId): " . $e->getMessage());
|
$this->logger->error("Excepción en getVaultCredentialsByClientId (Cliente: $clientId): " . $e->getMessage());
|
||||||
return 'Error: ' . $e->getMessage();
|
return 'Error: ' . $e->getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function syncNetworkDataWithCrm(int $clientId, string $siteName, string $deviceInfo): void
|
private function syncPasswordWithCrm(int $clientId, string $passVault): void {
|
||||||
{
|
|
||||||
try {
|
|
||||||
$siteAttrId = $this->resolveAttributeId('site');
|
|
||||||
$antennaAttrId = $this->resolveAttributeId('antenaSectorial');
|
|
||||||
|
|
||||||
if ($siteAttrId) {
|
|
||||||
$this->patchClientCustomAttribute($clientId, $siteAttrId, $siteName);
|
|
||||||
}
|
|
||||||
if ($antennaAttrId) {
|
|
||||||
$this->patchClientCustomAttribute($clientId, $antennaAttrId, $deviceInfo);
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->logger->warning("Fallo al sincronizar datos de red estáticos en test env para cliente $clientId: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function syncPasswordWithCrm(int $clientId, string $passVault): void
|
|
||||||
{
|
|
||||||
try {
|
try {
|
||||||
$clientData = $this->ucrmApi->get("clients/$clientId");
|
$clientData = $this->ucrmApi->get("clients/$clientId");
|
||||||
|
|
||||||
@ -546,8 +484,7 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function generateStrongPassword(int $length = 16): string
|
protected function generateStrongPassword(int $length = 16): string {
|
||||||
{
|
|
||||||
$lower = 'abcdefghijkmnopqrstuvwxyz'; // Eliminamos 'l'
|
$lower = 'abcdefghijkmnopqrstuvwxyz'; // Eliminamos 'l'
|
||||||
$upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // Eliminamos 'I', 'O'
|
$upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // Eliminamos 'I', 'O'
|
||||||
$digits = '23456789'; // Eliminamos '1', '0'
|
$digits = '23456789'; // Eliminamos '1', '0'
|
||||||
@ -578,8 +515,7 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
return implode('', $pwChars);
|
return implode('', $pwChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function patchClientCustomAttribute(int $clientId, int $attributeId, string $value): bool
|
protected function patchClientCustomAttribute(int $clientId, int $attributeId, string $value): bool {
|
||||||
{
|
|
||||||
if ($attributeId <= 0) {
|
if ($attributeId <= 0) {
|
||||||
$this->logger->error("Intento de patchAttribute con ID inválido ($attributeId) para cliente $clientId");
|
$this->logger->error("Intento de patchAttribute con ID inválido ($attributeId) para cliente $clientId");
|
||||||
return false;
|
return false;
|
||||||
@ -601,8 +537,7 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveAttributeId(string $key): int
|
private function resolveAttributeId(string $key): int {
|
||||||
{
|
|
||||||
if ($this->systemAttributesCache === null) {
|
if ($this->systemAttributesCache === null) {
|
||||||
try {
|
try {
|
||||||
$this->systemAttributesCache = $this->ucrmApi->get('custom-attributes', ['attributeType' => 'client']);
|
$this->systemAttributesCache = $this->ucrmApi->get('custom-attributes', ['attributeType' => 'client']);
|
||||||
@ -621,15 +556,13 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function comparePasswords(?string $crm, ?string $vault): string
|
protected function comparePasswords(?string $crm, ?string $vault): string {
|
||||||
{
|
|
||||||
if ($crm && strpos($crm, 'Error') !== 0) return $crm;
|
if ($crm && strpos($crm, 'Error') !== 0) return $crm;
|
||||||
if ($vault && strpos($vault, 'Error') !== 0) return $vault;
|
if ($vault && strpos($vault, 'Error') !== 0) return $vault;
|
||||||
return '⚠️ Probar pass conocida.';
|
return '⚠️ Probar pass conocida.';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function syncStripeCustomerData(int $clientId, string $name, ?string $email): void
|
public function syncStripeCustomerData(int $clientId, string $name, ?string $email): void {
|
||||||
{
|
|
||||||
$config = PluginConfigManager::create()->loadConfig();
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
$stripe = new StripeClient($config['tokenstripe']);
|
$stripe = new StripeClient($config['tokenstripe']);
|
||||||
try {
|
try {
|
||||||
@ -650,12 +583,10 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function removeTagFromClient(int $clientId, string $tagName): void
|
protected function removeTagFromClient(int $clientId, string $tagName): void {
|
||||||
{
|
|
||||||
try {
|
try {
|
||||||
$client = $this->ucrmApi->get("clients/$clientId");
|
$client = $this->ucrmApi->get("clients/$clientId");
|
||||||
$targetTagId = null;
|
$targetTagId = null;
|
||||||
$remainingTags = [];
|
|
||||||
foreach ($client['tags'] as $tag) {
|
foreach ($client['tags'] as $tag) {
|
||||||
if ($tag['name'] === $tagName) {
|
if ($tag['name'] === $tagName) {
|
||||||
$targetTagId = $tag['id'];
|
$targetTagId = $tag['id'];
|
||||||
@ -664,9 +595,8 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($targetTagId) {
|
if ($targetTagId) {
|
||||||
// The proper UCRM endpoint to remove a tag from a client is PATCH /clients/{id}/remove-tag/{tagId}
|
$this->ucrmApi->patch("clients/$clientId/remove-tag/$targetTagId", []);
|
||||||
$this->ucrmApi->patch("clients/$clientId/remove-tag/$targetTagId");
|
$this->logger->info("Etiqueta '$tagName' (ID: $targetTagId) removida del cliente $clientId via endpoint especializado.");
|
||||||
$this->logger->info("Etiqueta '$tagName' (ID: $targetTagId) removida del cliente $clientId.");
|
|
||||||
} else {
|
} else {
|
||||||
$this->logger->debug("Etiqueta '$tagName' no encontrada en el cliente $clientId, nada que remover.");
|
$this->logger->debug("Etiqueta '$tagName' no encontrada en el cliente $clientId, nada que remover.");
|
||||||
}
|
}
|
||||||
@ -675,184 +605,11 @@ abstract class AbstractStripeOperationsFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function validarNumeroTelefono($n): string
|
protected function validarNumeroTelefono($n): string {
|
||||||
{
|
|
||||||
if (!$n) return '';
|
if (!$n) return '';
|
||||||
$n = preg_replace('/\D/', '', (string)$n);
|
$n = preg_replace('/\D/', '', (string)$n);
|
||||||
return (strlen($n) === 10) ? '52' . $n : $n;
|
return (strlen($n) === 10) ? '52' . $n : $n;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ensureStripePaymentAttribute($notificationObject): void
|
|
||||||
{
|
|
||||||
// 1. Get Payment ID
|
|
||||||
$paymentId = is_object($notificationObject) ? ($notificationObject->entityId ?? null) : ($notificationObject['entityId'] ?? null);
|
|
||||||
|
|
||||||
if (!$paymentId) {
|
|
||||||
$this->logger->warning("ensureStripePaymentAttribute: No entityId found in notification.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logger->info("Verificando existencia de atributo 'tipoPagoStripe' para Payment ID: $paymentId");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Load Config
|
|
||||||
$config = PluginConfigManager::create()->loadConfig();
|
|
||||||
$ipPuppeteer = $config['ipPuppeteer'] ?? 'localhost';
|
|
||||||
$portPuppeteer = $config['portPuppeteer'] ?? '3000';
|
|
||||||
$stripeUserId = $config['idPaymentAdminCRM'] ?? null;
|
|
||||||
|
|
||||||
$microserviceBaseUrl = "http://$ipPuppeteer:$portPuppeteer";
|
|
||||||
$httpClient = new Client();
|
|
||||||
|
|
||||||
// 2. Fetch Metadata from Microservice (DB Access)
|
|
||||||
$metadataTipoPago = null;
|
|
||||||
try {
|
|
||||||
$response = $httpClient->get("$microserviceBaseUrl/stripe-metadata/$paymentId", ['timeout' => 5]);
|
|
||||||
$data = json_decode($response->getBody()->getContents(), true);
|
|
||||||
if (isset($data['metadata']['tipoPago'])) {
|
|
||||||
$metadataTipoPago = $data['metadata']['tipoPago'];
|
|
||||||
$this->logger->info("Microservice found metadata: tipoPago = '$metadataTipoPago'");
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$this->logger->warning("Microservice metadata fetch failed: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Update User ID if missing (Direct DB Patch via Microservice)
|
|
||||||
// UCRM API doesn't support PATCH userId, so we use microservice
|
|
||||||
if ($stripeUserId) {
|
|
||||||
try {
|
|
||||||
$payment = $this->ucrmApi->get('payments/' . $paymentId);
|
|
||||||
if (empty($payment['userId'])) {
|
|
||||||
$this->logger->info("Payment $paymentId has no User ID. Assigning Stripe User ID: $stripeUserId");
|
|
||||||
$httpClient->patch("$microserviceBaseUrl/payments/$paymentId/user", [
|
|
||||||
'json' => ['userId' => $stripeUserId],
|
|
||||||
'timeout' => 5
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
$this->logger->debug("Payment $paymentId already has User ID: " . $payment['userId']);
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$this->logger->error("Failed to patch User ID via microservice: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// [NEW] 3.5. Patch Payment Method ID via Microservice
|
|
||||||
$targetMethodId = null;
|
|
||||||
|
|
||||||
if ($metadataTipoPago === 'OXXO') {
|
|
||||||
$targetMethodId = 'b01c0b35-b42c-48d9-9ad9-ea6591adfbbb'; // OXXO Pay
|
|
||||||
} elseif ($metadataTipoPago === 'Transferencia Bancaria') {
|
|
||||||
$targetMethodId = '4145b5f5-3bbc-45e3-8fc5-9cda970c62fb'; // Transferencia Bancaria
|
|
||||||
} else {
|
|
||||||
// [NEW] Default to "Credit/Debit Card" if no specific metadata type found
|
|
||||||
$targetMethodId = '93814765-66a1-4c7d-a777-05c18fd6aab3'; // Tarjeta de crédito/débito
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($targetMethodId) {
|
|
||||||
// [NEW] Update Notification Object in Memory so the calling code knows the change
|
|
||||||
if (is_object($notificationObject) && isset($notificationObject->paymentData)) {
|
|
||||||
// Fix for "Indirect modification of overloaded property" error
|
|
||||||
// We must read the array, modify it, and write it back.
|
|
||||||
$pData = $notificationObject->paymentData;
|
|
||||||
if (is_array($pData)) {
|
|
||||||
$pData['methodId'] = $targetMethodId;
|
|
||||||
$notificationObject->paymentData = $pData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check current methodId (reuse 'payment' if available, otherwise fetch)
|
|
||||||
// Note: We fetched 'payment' in Step 3 ONLY if stripeUserId was valid.
|
|
||||||
// Safe to fetch again or reuse specific check.
|
|
||||||
$paymentCheck = $this->ucrmApi->get('payments/' . $paymentId);
|
|
||||||
|
|
||||||
if ($paymentCheck['methodId'] !== $targetMethodId) {
|
|
||||||
$this->logger->info("Payment $paymentId has wrong Method ID ({$paymentCheck['methodId']}). Patching to $targetMethodId via Microservice.");
|
|
||||||
|
|
||||||
$httpClient->patch("$microserviceBaseUrl/payments/$paymentId/method", [
|
|
||||||
'json' => ['methodId' => $targetMethodId],
|
|
||||||
'timeout' => 5
|
|
||||||
]);
|
|
||||||
$this->logger->info("Payment Method ID patched successfully.");
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$this->logger->error("Failed to patch Payment Method ID via microservice: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Determine Target Attribute Value
|
|
||||||
// Truth Source Priority: 1. Metadata (DB), 2. Existing Attribute, 3. Method Name (Guess)
|
|
||||||
|
|
||||||
// A. Check Existing Attribute (Don't overwrite valid values unless Metadata says otherwise?)
|
|
||||||
// Actually, Metadata keys are stronger than manual edits if the flow is automatic.
|
|
||||||
// But let's respect existing valid attributes if metadata is missing.
|
|
||||||
|
|
||||||
$payment = $this->ucrmApi->get('payments/' . $paymentId); // Re-fetch in case changed? Or Use previous result.
|
|
||||||
$currentValue = null;
|
|
||||||
$hasAttribute = false;
|
|
||||||
foreach ($payment['attributes'] as $attr) {
|
|
||||||
if ($attr['key'] === 'tipoPagoStripe' || $attr['customAttributeId'] == 20) {
|
|
||||||
$hasAttribute = true;
|
|
||||||
$currentValue = $attr['value'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$targetValue = null;
|
|
||||||
|
|
||||||
if ($metadataTipoPago) {
|
|
||||||
// Normalize Metadata Values to Attribute Choice Values
|
|
||||||
if ($metadataTipoPago === 'OXXO') {
|
|
||||||
$targetValue = 'OXXO Pay';
|
|
||||||
} else {
|
|
||||||
$targetValue = $metadataTipoPago;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback to Method Name Guessing if Metadata missing
|
|
||||||
if ($hasAttribute && in_array($currentValue, ['OXXO Pay', 'Transferencia Bancaria', 'Tarjeta de Crédito'])) {
|
|
||||||
$this->logger->debug("Payment $paymentId ya tiene atributo '$currentValue' y no hay metadata. Respetando.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$methodId = $payment['methodId'];
|
|
||||||
$method = $this->ucrmApi->get('payment-methods/' . $methodId);
|
|
||||||
$methodName = $method['name'] ?? '';
|
|
||||||
|
|
||||||
if (stripos($methodName, 'OXXO') !== false) {
|
|
||||||
$targetValue = 'OXXO Pay';
|
|
||||||
} elseif (stripos($methodName, 'Transferencia') !== false) {
|
|
||||||
$targetValue = 'Transferencia Bancaria';
|
|
||||||
} elseif (stripos($methodName, 'Tarjeta') !== false && stripos($methodName, 'Stripe') !== false) {
|
|
||||||
$targetValue = 'Tarjeta de Crédito';
|
|
||||||
}
|
|
||||||
$this->logger->debug("Fallback Method Guessing '$methodName' -> '$targetValue'");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Apply Update
|
|
||||||
if ($targetValue) {
|
|
||||||
// Check redundancy
|
|
||||||
if ($hasAttribute && $currentValue === $targetValue) {
|
|
||||||
$this->logger->debug("Attribute already matches target '$targetValue'. Skipping patch.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logger->info("PATCHING Payment $paymentId: Setting tipoPagoStripe = '$targetValue'");
|
|
||||||
$this->ucrmApi->patch('payments/' . $paymentId, [
|
|
||||||
'attributes' => [
|
|
||||||
[
|
|
||||||
'customAttributeId' => 20,
|
|
||||||
'value' => $targetValue
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
$this->logger->debug("No se pudo determinar el tipoPagoStripe.");
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$this->logger->error("Error in ensureStripePaymentAttribute: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract protected function sendWhatsApp(NotificationData $notificationData, string $clientPhoneNumber): void;
|
abstract protected function sendWhatsApp(NotificationData $notificationData, string $clientPhoneNumber): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace SmsNotifier\Facade;
|
namespace SmsNotifier\Facade;
|
||||||
|
|
||||||
use DateTime;
|
use DateTime;
|
||||||
@ -65,7 +64,9 @@ class ClientCallBellAPI
|
|||||||
$this->CallBellAPIToken = $CallBellAPIToken;
|
$this->CallBellAPIToken = $CallBellAPIToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateContact($client_uuid) {}
|
public function updateContact($client_uuid)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public function printPrueba($clientWhatsAppNumber, $notificationData)
|
public function printPrueba($clientWhatsAppNumber, $notificationData)
|
||||||
{
|
{
|
||||||
@ -356,43 +357,17 @@ class ClientCallBellAPI
|
|||||||
$config = PluginConfigManager::create()->loadConfig();
|
$config = PluginConfigManager::create()->loadConfig();
|
||||||
$gClient = new Client(['base_uri' => "https://{$this->IPServer}/crm/api/v1.0/", 'verify' => false]);
|
$gClient = new Client(['base_uri' => "https://{$this->IPServer}/crm/api/v1.0/", 'verify' => false]);
|
||||||
$this->ucrmApi = new UcrmApi($gClient, $this->UCRMAPIToken ?? '');
|
$this->ucrmApi = new UcrmApi($gClient, $this->UCRMAPIToken ?? '');
|
||||||
$payment_id = $notificationData->paymentData['id'];
|
$payments = $this->ucrmApi->get(
|
||||||
$payment_amount = '$' . $notificationData->paymentData['amount'];
|
'payments/',
|
||||||
|
[
|
||||||
|
'clientId' => $notificationData->clientData['id'],
|
||||||
|
'limit' => 1,
|
||||||
|
'direction' => 'DESC',
|
||||||
|
|
||||||
// We already have the payment data in $notificationData, no need to fetch 'payments/' again to get the ID.
|
]
|
||||||
// However, if we need the 'note' field for the overlay which might not be in notificationData (depending on richness),
|
);
|
||||||
// we should try to use what we have or fetch SPECIFICALLY this payment.
|
|
||||||
|
|
||||||
// Let's verify if 'note' is in paymentData. Usually UCRM webhook payload has it.
|
|
||||||
// But to be safe and consistent with previous logic, if we need 'note', we can fetch THIS payment.
|
|
||||||
|
|
||||||
// $payments = $this->ucrmApi->get('payments/'.$payment_id); // This would be better if we need details.
|
|
||||||
|
|
||||||
// The previous code did:
|
|
||||||
// $payments = $this->ucrmApi->get('payments/', ['clientId' => ..., 'limit' => 1 ...]);
|
|
||||||
// $payment_id = $payments[0]['id']; <-- THIS WAS THE BUG. Always getting latest.
|
|
||||||
|
|
||||||
// Fix: Use the ID passed in notificationData.
|
|
||||||
|
|
||||||
// If we need the NOTE for the Overlay (OXXO/Transfer check later in code), we should ensure we have it.
|
|
||||||
// $notificationData->paymentData usually contains 'note'.
|
|
||||||
$note = $notificationData->paymentData['note'] ?? '';
|
|
||||||
|
|
||||||
// Let's keep $payments array structure if downstream code expects it, OR refactor downstream.
|
|
||||||
// Downstream uses $payments[0]['note'].
|
|
||||||
|
|
||||||
// Let's just mock $payments[0] with our data OR fetch the correct single payment.
|
|
||||||
// Fetching single payment is safer to ensure we have the Note.
|
|
||||||
try {
|
|
||||||
$fetchedPayment = $this->ucrmApi->get('payments/' . $payment_id);
|
|
||||||
$payments = [$fetchedPayment];
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
// Fallback if fetch fails (unlikely if ID is valid)
|
|
||||||
$payments = [$notificationData->paymentData];
|
|
||||||
}
|
|
||||||
|
|
||||||
// $payment_id is already set above.
|
|
||||||
|
|
||||||
|
$payment_id = $payments[0]['id'];
|
||||||
$payment_amount = '$' . $payments[0]['amount'];
|
$payment_amount = '$' . $payments[0]['amount'];
|
||||||
//$saldo = '$' . $notificationData->clientData['accountBalance'];
|
//$saldo = '$' . $notificationData->clientData['accountBalance'];
|
||||||
$nombre_cliente = sprintf("%s %s", $notificationData->clientData['firstName'], $notificationData->clientData['lastName']);
|
$nombre_cliente = sprintf("%s %s", $notificationData->clientData['firstName'], $notificationData->clientData['lastName']);
|
||||||
@ -429,15 +404,7 @@ class ClientCallBellAPI
|
|||||||
$contenidoArchivo = $response->getBody()->getContents();
|
$contenidoArchivo = $response->getBody()->getContents();
|
||||||
|
|
||||||
// Construir el nombre del archivo PDF basado en el cliente
|
// Construir el nombre del archivo PDF basado en el cliente
|
||||||
$unwanted_array = array( 'Š'=>'S', 'š'=>'s', 'Ž'=>'Z', 'ž'=>'z', 'À'=>'A', 'Á'=>'A', 'Â'=>'A', 'Ã'=>'A', 'Ä'=>'A', 'Å'=>'A', 'Æ'=>'A', 'Ç'=>'C', 'È'=>'E', 'É'=>'E',
|
$fileNameComprobante = 'Comprobante_' . str_replace(' ', '_', $nombre_cliente) . '.pdf';
|
||||||
'Ê'=>'E', 'Ë'=>'E', 'Ì'=>'I', 'Í'=>'I', 'Î'=>'I', 'Ï'=>'I', 'Ñ'=>'N', 'Ò'=>'O', 'Ó'=>'O', 'Ô'=>'O', 'Õ'=>'O', 'Ö'=>'O', 'Ø'=>'O', 'Ù'=>'U',
|
|
||||||
'Ú'=>'U', 'Û'=>'U', 'Ü'=>'U', 'Ý'=>'Y', 'Þ'=>'B', 'ß'=>'Ss', 'à'=>'a', 'á'=>'a', 'â'=>'a', 'ã'=>'a', 'ä'=>'a', 'å'=>'a', 'æ'=>'a', 'ç'=>'c',
|
|
||||||
'è'=>'e', 'é'=>'e', 'ê'=>'e', 'ë'=>'e', 'ì'=>'i', 'í'=>'i', 'î'=>'i', 'ï'=>'i', 'ð'=>'o', 'ñ'=>'n', 'ò'=>'o', 'ó'=>'o', 'ô'=>'o', 'õ'=>'o',
|
|
||||||
'ö'=>'o', 'ø'=>'o', 'ù'=>'u', 'ú'=>'u', 'û'=>'u', 'ü'=>'u', 'ý'=>'y', 'þ'=>'b', 'ÿ'=>'y' );
|
|
||||||
$clean_name = strtr($nombre_cliente, $unwanted_array);
|
|
||||||
$clean_name = preg_replace('/[^A-Za-z0-9_\-]/', '', str_replace(' ', '_', $clean_name));
|
|
||||||
|
|
||||||
$fileNameComprobante = 'Comprobante_' . $clean_name . '.pdf';
|
|
||||||
$rutaArchivo = __DIR__ . '/../../comprobantes/' . $fileNameComprobante;
|
$rutaArchivo = __DIR__ . '/../../comprobantes/' . $fileNameComprobante;
|
||||||
|
|
||||||
// Guardar el contenido del PDF en un archivo local
|
// Guardar el contenido del PDF en un archivo local
|
||||||
@ -475,8 +442,8 @@ class ClientCallBellAPI
|
|||||||
$minioService = new MinioStorageService($loggerService);
|
$minioService = new MinioStorageService($loggerService);
|
||||||
|
|
||||||
// 2. Configurar Microservicio
|
// 2. Configurar Microservicio
|
||||||
$ipMicroservice = $config['ipMicroservice'] ?? 'pdf-cropper-service';
|
$ipMicroservice = $config['ipPuppeteer'] ?? 'localhost'; // Reutilizamos IP de Puppeteer
|
||||||
$portMicroservice = $config['portMicroservice'] ?? '8000';
|
$portMicroservice = '8050'; // Puerto definido en docker-compose
|
||||||
$microserviceUrl = "http://{$ipMicroservice}:{$portMicroservice}/process";
|
$microserviceUrl = "http://{$ipMicroservice}:{$portMicroservice}/process";
|
||||||
|
|
||||||
$log->appendLog("Procesando PDF con microservicio: $microserviceUrl" . PHP_EOL);
|
$log->appendLog("Procesando PDF con microservicio: $microserviceUrl" . PHP_EOL);
|
||||||
@ -553,6 +520,7 @@ class ClientCallBellAPI
|
|||||||
//$log->appendLog("Archivo JPG temporal eliminado." . PHP_EOL);
|
//$log->appendLog("Archivo JPG temporal eliminado." . PHP_EOL);
|
||||||
}
|
}
|
||||||
$log->appendLog("Archivos temporales (PDF/JPG) eliminados tras subida exitosa." . PHP_EOL);
|
$log->appendLog("Archivos temporales (PDF/JPG) eliminados tras subida exitosa." . PHP_EOL);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$log->appendLog("Error: Falló la subida a MinIO." . PHP_EOL);
|
$log->appendLog("Error: Falló la subida a MinIO." . PHP_EOL);
|
||||||
return false;
|
return false;
|
||||||
@ -565,6 +533,7 @@ class ClientCallBellAPI
|
|||||||
$log->appendLog("Error microservicio PDF: HTTP " . $responseMs->getStatusCode() . PHP_EOL);
|
$log->appendLog("Error microservicio PDF: HTTP " . $responseMs->getStatusCode() . PHP_EOL);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$log->appendLog("Excepción en flujo Microservicio/MinIO: " . $e->getMessage() . PHP_EOL);
|
$log->appendLog("Excepción en flujo Microservicio/MinIO: " . $e->getMessage() . PHP_EOL);
|
||||||
return false;
|
return false;
|
||||||
@ -1001,32 +970,24 @@ class ClientCallBellAPI
|
|||||||
//ejemplo de $notificationData: {"uuid":"cad4fa25-176e-4823-9f4c-1421d05d2a31","changeType":"edit","entity":"client","entityId":2,"message":null,"clientId":2,"eventName":"client.edit","clientData":{"id":2,"userIdent":null,"previousIsp":null,"isLead":false,"clientType":1,"companyName":null,"companyRegistrationNumber":null,"companyTaxId":null,"companyWebsite":null,"street1":"31 Chiapas","street2":null,"city":"Dolores Hidalgo Cuna de la Independencia Nacional","countryId":173,"stateId":null,"zipCode":"37800","fullAddress":"Chiapas 31, Centro, 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":"Cliente espacial, el m\u00e1s chido","sendInvoiceByPost":null,"invoiceMaturityDays":null,"stopServiceDue":null,"stopServiceDueDays":null,"organizationId":1,"tax1Id":null,"tax2Id":null,"tax3Id":null,"registrationDate":"2024-01-25T00:00:00-0600","leadConvertedAt":null,"companyContactFirstName":null,"companyContactLastName":null,"isActive":true,"firstName":"Daniel Humberto","lastName":"Soto Villegas Pollerias2","username":"danydhsv","contacts":[{"id":2,"clientId":2,"email":"dhsv.141089@gmail.com","phone":"5214181878106","name":"Personal","isBilling":false,"isContact":false,"types":[{"id":1000,"name":"WhatsApp"}]},{"id":170,"clientId":2,"email":null,"phone":"5214181817609","name":"Bussiness","isBilling":false,"isContact":false,"types":[]}],"attributes":[{"id":112,"clientId":2,"customAttributeId":10,"name":"Stripe Customer ID","key":"stripeCustomerId","value":"cus_PetN1dhr4rx0kX","clientZoneVisible":true},{"id":113,"clientId":2,"customAttributeId":11,"name":"Clabe Interbancaria","key":"clabeInterbancaria","value":"0021804341999569810","clientZoneVisible":true},{"id":178,"clientId":2,"customAttributeId":15,"name":"Site","key":"site","value":"0LOCS","clientZoneVisible":false},{"id":179,"clientId":2,"customAttributeId":16,"name":"Antena\/Sectorial","key":"antenaSectorial","value":"Sectorial-4b 172.16.13.16\/24","clientZoneVisible":false}],"accountBalance":1553.33,"accountCredit":1553.33,"accountOutstanding":0,"currencyCode":"MXN","organizationName":"SIIP Pruebas","bankAccounts":[],"tags":[{"id":2,"name":"NS EXENTO","colorBackground":"#42a3df","colorText":"#fff"}],"invitationEmailSentDate":null,"avatarColor":"#f1df43","addressGpsLat":21.1564209,"addressGpsLon":-100.9384185,"isArchived":false,"generateProformaInvoices":null,"usesProforma":false,"hasOverdueInvoice":false,"hasOutage":true,"hasSuspendedService":false,"hasServiceWithoutDevices":false,"referral":null,"hasPaymentSubscription":false,"hasAutopayCreditCard":false},"serviceData":null,"invoiceData":null,"paymentData":null}
|
//ejemplo de $notificationData: {"uuid":"cad4fa25-176e-4823-9f4c-1421d05d2a31","changeType":"edit","entity":"client","entityId":2,"message":null,"clientId":2,"eventName":"client.edit","clientData":{"id":2,"userIdent":null,"previousIsp":null,"isLead":false,"clientType":1,"companyName":null,"companyRegistrationNumber":null,"companyTaxId":null,"companyWebsite":null,"street1":"31 Chiapas","street2":null,"city":"Dolores Hidalgo Cuna de la Independencia Nacional","countryId":173,"stateId":null,"zipCode":"37800","fullAddress":"Chiapas 31, Centro, 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":"Cliente espacial, el m\u00e1s chido","sendInvoiceByPost":null,"invoiceMaturityDays":null,"stopServiceDue":null,"stopServiceDueDays":null,"organizationId":1,"tax1Id":null,"tax2Id":null,"tax3Id":null,"registrationDate":"2024-01-25T00:00:00-0600","leadConvertedAt":null,"companyContactFirstName":null,"companyContactLastName":null,"isActive":true,"firstName":"Daniel Humberto","lastName":"Soto Villegas Pollerias2","username":"danydhsv","contacts":[{"id":2,"clientId":2,"email":"dhsv.141089@gmail.com","phone":"5214181878106","name":"Personal","isBilling":false,"isContact":false,"types":[{"id":1000,"name":"WhatsApp"}]},{"id":170,"clientId":2,"email":null,"phone":"5214181817609","name":"Bussiness","isBilling":false,"isContact":false,"types":[]}],"attributes":[{"id":112,"clientId":2,"customAttributeId":10,"name":"Stripe Customer ID","key":"stripeCustomerId","value":"cus_PetN1dhr4rx0kX","clientZoneVisible":true},{"id":113,"clientId":2,"customAttributeId":11,"name":"Clabe Interbancaria","key":"clabeInterbancaria","value":"0021804341999569810","clientZoneVisible":true},{"id":178,"clientId":2,"customAttributeId":15,"name":"Site","key":"site","value":"0LOCS","clientZoneVisible":false},{"id":179,"clientId":2,"customAttributeId":16,"name":"Antena\/Sectorial","key":"antenaSectorial","value":"Sectorial-4b 172.16.13.16\/24","clientZoneVisible":false}],"accountBalance":1553.33,"accountCredit":1553.33,"accountOutstanding":0,"currencyCode":"MXN","organizationName":"SIIP Pruebas","bankAccounts":[],"tags":[{"id":2,"name":"NS EXENTO","colorBackground":"#42a3df","colorText":"#fff"}],"invitationEmailSentDate":null,"avatarColor":"#f1df43","addressGpsLat":21.1564209,"addressGpsLon":-100.9384185,"isArchived":false,"generateProformaInvoices":null,"usesProforma":false,"hasOverdueInvoice":false,"hasOutage":true,"hasSuspendedService":false,"hasServiceWithoutDevices":false,"referral":null,"hasPaymentSubscription":false,"hasAutopayCreditCard":false},"serviceData":null,"invoiceData":null,"paymentData":null}
|
||||||
|
|
||||||
//buscar el attribute con el key site y antenaSectorial
|
//buscar el attribute con el key site y antenaSectorial
|
||||||
$attributes = $notificationData->clientData['attributes'] ?? []; //Obtener los atributos del cliente
|
$attributes = $notificationData->clientData['attributes']; //Obtener los atributos del cliente
|
||||||
$site = '';
|
$site = '';
|
||||||
$antenaSectorial = '';
|
$antenaSectorial = '';
|
||||||
$passAntenaUCRM = '';
|
$passAntenaUCRM = '';
|
||||||
$clabeAttr = '';
|
|
||||||
|
|
||||||
// Iterar sobre los atributos
|
// Iterar sobre los atributos
|
||||||
foreach ($attributes as $attribute) {
|
foreach ($attributes as $attribute) {
|
||||||
// Verificar si el key es "site"
|
// Verificar si el key es "site"
|
||||||
if ($attribute['key'] === 'site') {
|
if ($attribute['key'] === 'site') {
|
||||||
$site = $attribute['value'] ?? '';
|
$site = $attribute['value'];
|
||||||
}
|
}
|
||||||
// Verificar si el key es "antenaSectorial"
|
// Verificar si el key es "antenaSectorial"
|
||||||
if ($attribute['key'] === 'antenaSectorial') {
|
if ($attribute['key'] === 'antenaSectorial') {
|
||||||
$antenaSectorial = $attribute['value'] ?? '';
|
$antenaSectorial = $attribute['value'];
|
||||||
}
|
}
|
||||||
if ($attribute['key'] === 'passwordAntenaCliente') {
|
if ($attribute['key'] === 'passwordAntenaCliente') {
|
||||||
$passAntenaUCRM = $attribute['value'] ?? '';
|
$passAntenaUCRM = $attribute['value'] ?? '';
|
||||||
}
|
}
|
||||||
if ($attribute['key'] === 'clabeInterbancaria') {
|
|
||||||
$clabeAttr = $attribute['value'] ?? '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($clabeInterbancaria)) {
|
|
||||||
$clabeInterbancaria = $clabeAttr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parsear contraseñas multi-servicio a JSON
|
// Parsear contraseñas multi-servicio a JSON
|
||||||
@ -1148,6 +1109,7 @@ class ClientCallBellAPI
|
|||||||
$resumenClienteJSON = '{' .
|
$resumenClienteJSON = '{' .
|
||||||
'"Cliente": "' . $notificationData->clientData['id'] . '",' .
|
'"Cliente": "' . $notificationData->clientData['id'] . '",' .
|
||||||
'"Nombre": "' . sprintf("%s %s", $notificationData->clientData['firstName'], $notificationData->clientData['lastName']) . '",' .
|
'"Nombre": "' . sprintf("%s %s", $notificationData->clientData['firstName'], $notificationData->clientData['lastName']) . '",' .
|
||||||
|
'"URL": "https://sistema.siip.mx/crm/client/' . $notificationData->clientId . '",' .
|
||||||
'"Saldo Actual": "' . $saldoTexto . '",' .
|
'"Saldo Actual": "' . $saldoTexto . '",' .
|
||||||
'"Monto Ultimo Pago": "$ ' . $payments[0]['amount'] . '",' .
|
'"Monto Ultimo Pago": "$ ' . $payments[0]['amount'] . '",' .
|
||||||
'"Estado": "' .
|
'"Estado": "' .
|
||||||
@ -1155,6 +1117,8 @@ class ClientCallBellAPI
|
|||||||
'"Fecha Ultimo Pago": " ' . $fecha_ultimoPago_ajustada . ' con ' . $payment_method . '",' .
|
'"Fecha Ultimo Pago": " ' . $fecha_ultimoPago_ajustada . ' con ' . $payment_method . '",' .
|
||||||
'"Fecha Ultima Actualizacion": "' . $fecha_actual_ajustada . '",' .
|
'"Fecha Ultima Actualizacion": "' . $fecha_actual_ajustada . '",' .
|
||||||
'"Clabe Interbancaria": "' . $clabeInterbancaria . '",' .
|
'"Clabe Interbancaria": "' . $clabeInterbancaria . '",' .
|
||||||
|
'"Site": "' . $site . '",' .
|
||||||
|
'"Antena/Sectorial": "' . $antenaSectorial . '",' .
|
||||||
'"Password Antena": ' . (empty($passAntenaFinal) ? '""' : $passAntenaFinal) .
|
'"Password Antena": ' . (empty($passAntenaFinal) ? '""' : $passAntenaFinal) .
|
||||||
'}';
|
'}';
|
||||||
|
|
||||||
@ -1186,38 +1150,26 @@ class ClientCallBellAPI
|
|||||||
$contact = $response_getContactCallBell['contact'] ?? [];
|
$contact = $response_getContactCallBell['contact'] ?? [];
|
||||||
$contactCustomFields = $contact['customFields'] ?? [];
|
$contactCustomFields = $contact['customFields'] ?? [];
|
||||||
|
|
||||||
$log->appendLog("DEBUG COMPARACIÓN - CallBell Saldo Actual: '" . ($contactCustomFields['Saldo Actual'] ?? 'NULL') . "'" . PHP_EOL);
|
if (
|
||||||
$log->appendLog("DEBUG COMPARACIÓN - UCRM Saldo Actual: '" . $data_CRM['custom_fields']['Saldo Actual'] . "'" . PHP_EOL);
|
($contactCustomFields['Cliente'] ?? '') != $data_CRM['custom_fields']['Cliente']
|
||||||
$log->appendLog("DEBUG COMPARACIÓN - CallBell Estado: '" . ($contactCustomFields['Estado'] ?? 'NULL') . "'" . PHP_EOL);
|
|| ($contactCustomFields['Domicilio'] ?? '') != $data_CRM['custom_fields']['Domicilio']
|
||||||
$log->appendLog("DEBUG COMPARACIÓN - UCRM Estado: '" . $data_CRM['custom_fields']['Estado'] . "'" . PHP_EOL);
|
|| ($contactCustomFields['Nombre'] ?? '') != $data_CRM['custom_fields']['Nombre']
|
||||||
|
|| ($contactCustomFields['URL'] ?? '') != $data_CRM['custom_fields']['URL']
|
||||||
|
|| ($contactCustomFields['Saldo Actual'] ?? '') != $data_CRM['custom_fields']['Saldo Actual']
|
||||||
|
|| ($contactCustomFields['Estado'] ?? '') != $data_CRM['custom_fields']['Estado']
|
||||||
|
|| ($contactCustomFields['Fecha Ultimo Pago'] ?? '') != $data_CRM['custom_fields']['Fecha Ultimo Pago']
|
||||||
|
|| ($contactCustomFields['Monto Ultimo Pago'] ?? '') != $data_CRM['custom_fields']['Monto Ultimo Pago']
|
||||||
|
|| ($contactCustomFields['password-antena'] ?? '') != $data_CRM['custom_fields']['password-antena']
|
||||||
|
|| ($contact['name'] ?? '') != $data_CRM['name']
|
||||||
|
|
||||||
$hasChanges = false;
|
) {
|
||||||
if (($contact['name'] ?? '') != $data_CRM['name']) {
|
|
||||||
$log->appendLog("DEBUG COMPARACIÓN - Diferencia en name. CallBell: '" . ($contact['name'] ?? 'NULL') . "', UCRM: '" . $data_CRM['name'] . "'" . PHP_EOL);
|
|
||||||
$hasChanges = true;
|
|
||||||
} else {
|
|
||||||
foreach ($data_CRM['custom_fields'] as $key => $value) {
|
|
||||||
if ($key === 'Fecha Ultima Actualizacion') {
|
|
||||||
continue; // Skip this as it always changes and shouldn't trigger a patch on its own
|
|
||||||
}
|
|
||||||
|
|
||||||
$callBellValue = $contactCustomFields[$key] ?? '';
|
|
||||||
if ((string)$callBellValue !== (string)$value) {
|
|
||||||
$log->appendLog("DEBUG COMPARACIÓN - Diferencia encontrada en $key. CallBell: '$callBellValue', UCRM: '$value'" . PHP_EOL);
|
|
||||||
$hasChanges = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($hasChanges) {
|
|
||||||
$log->appendLog("EJECUTANDO PATCH - Se detectaron cambios" . PHP_EOL);
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data_CRM));
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data_CRM));
|
||||||
$response = curl_exec($ch);
|
$response = curl_exec($ch);
|
||||||
$log->appendLog("Response Patch CallBell: " . $response . PHP_EOL);
|
//$log->appendLog("Response Patch CallBell: " . $response . PHP_EOL);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$log->appendLog("NO SE EJECUTA PATCH - No hay cambios que actualizar" . PHP_EOL);
|
//$log->appendLog("No hay cambios que actualizar " . PHP_EOL);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,6 @@ use GuzzleHttp\Client as GuzzleClient;
|
|||||||
use GuzzleHttp\Exception\GuzzleException;
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
|
||||||
|
|
||||||
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
|
|
||||||
|
|
||||||
class PluginNotifierFacade extends AbstractStripeOperationsFacade
|
class PluginNotifierFacade extends AbstractStripeOperationsFacade
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@ -51,83 +49,6 @@ class PluginNotifierFacade extends AbstractStripeOperationsFacade
|
|||||||
$this->getVaultCredentialsByClientId($clientId);
|
$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');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Implementation of abstract method from AbstractStripeOperationsFacade
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Implementation of abstract method from AbstractStripeOperationsFacade
|
* Implementation of abstract method from AbstractStripeOperationsFacade
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -109,9 +109,6 @@ class Plugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
$userInput = file_get_contents('php://input');
|
$userInput = file_get_contents('php://input');
|
||||||
|
|
||||||
// DEBUG TEMPORAL
|
|
||||||
|
|
||||||
$this->logger->debug('Payload recibido: ' . $userInput . PHP_EOL);
|
$this->logger->debug('Payload recibido: ' . $userInput . PHP_EOL);
|
||||||
|
|
||||||
if (! $userInput) {
|
if (! $userInput) {
|
||||||
@ -134,6 +131,7 @@ class Plugin
|
|||||||
if ($jsonData['data']['object']['type'] === 'funded') {
|
if ($jsonData['data']['object']['type'] === 'funded') {
|
||||||
$this->logger->info('Evento de transferencia de un cliente recibido: ' . json_encode($jsonData) . PHP_EOL);
|
$this->logger->info('Evento de transferencia de un cliente recibido: ' . json_encode($jsonData) . PHP_EOL);
|
||||||
$this->pluginNotifierFacade->createPaymentIntent($jsonData);
|
$this->pluginNotifierFacade->createPaymentIntent($jsonData);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($jsonData['data']['object']['type'] === 'applied_to_payment') {
|
if ($jsonData['data']['object']['type'] === 'applied_to_payment') {
|
||||||
@ -145,6 +143,7 @@ class Plugin
|
|||||||
//Se canceló una transferencia de dinero, imprimir que se canceló y además el monto neto
|
//Se canceló una transferencia de dinero, imprimir que se canceló y además el monto neto
|
||||||
$this->logger->warning('Evento de transferencia cancelada para el pago: ' . $paymentIntentId . PHP_EOL);
|
$this->logger->warning('Evento de transferencia cancelada para el pago: ' . $paymentIntentId . PHP_EOL);
|
||||||
$this->logger->warning('Monto neto de la transferencia cancelada: ' . $jsonData['data']['object']['net_amount'] . PHP_EOL);
|
$this->logger->warning('Monto neto de la transferencia cancelada: ' . $jsonData['data']['object']['net_amount'] . PHP_EOL);
|
||||||
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'payout.failed':
|
case 'payout.failed':
|
||||||
@ -262,6 +261,7 @@ class Plugin
|
|||||||
// Terminar proceso hijo
|
// Terminar proceso hijo
|
||||||
exit;
|
exit;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -299,9 +299,6 @@ class Plugin
|
|||||||
$result = json_encode($notification);
|
$result = json_encode($notification);
|
||||||
$this->logger->debug('Notification encodificado en JSON:' . $result . PHP_EOL);
|
$this->logger->debug('Notification encodificado en JSON:' . $result . PHP_EOL);
|
||||||
|
|
||||||
// [MOVED] Attempt to patch method ID only if it comes as "Stripe Credit Card" (catch-all)
|
|
||||||
//$this->pluginNotifierFacade->ensureStripePaymentAttribute($notification); (Removed from top)
|
|
||||||
|
|
||||||
$payment_method_id = $notification->paymentData['methodId'];
|
$payment_method_id = $notification->paymentData['methodId'];
|
||||||
$payment_method = '';
|
$payment_method = '';
|
||||||
|
|
||||||
@ -337,31 +334,10 @@ class Plugin
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case '1dd098fa-5d63-4c8d-88b7-3c27ffbbb6ae':
|
case '1dd098fa-5d63-4c8d-88b7-3c27ffbbb6ae':
|
||||||
// [NEW] Logic to patch method ID based on metadata
|
|
||||||
$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);
|
|
||||||
}
|
|
||||||
} 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';
|
$payment_method = 'Tarjeta de crédito Stripe';
|
||||||
if ($config['creditCardStripePaymentMethodId'] ?? false) {
|
if ($config['creditCardStripePaymentMethodId'] ?? false) {
|
||||||
$this->notifierFacade->verifyPaymentActionToDo($notification);
|
$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)';
|
||||||
@ -393,28 +369,11 @@ class Plugin
|
|||||||
$this->notifierFacade->verifyPaymentActionToDo($notification);
|
$this->notifierFacade->verifyPaymentActionToDo($notification);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'b01c0b35-b42c-48d9-9ad9-ea6591adfbbb':
|
|
||||||
$payment_method = 'OXXO Pay';
|
|
||||||
if ($config['oxxoPayPaymentMethodId'] ?? false) {
|
|
||||||
$this->notifierFacade->verifyPaymentActionToDo($notification);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case '4145b5f5-3bbc-45e3-8fc5-9cda970c62fb':
|
|
||||||
$payment_method = 'Transferencia bancaria';
|
|
||||||
if ($config['bankTransferPaymentMethodId'] ?? false) {
|
|
||||||
$this->notifierFacade->verifyPaymentActionToDo($notification);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case '93814765-66a1-4c7d-a777-05c18fd6aab3':
|
|
||||||
$payment_method = 'Tarjeta de crédito/débito';
|
|
||||||
if ($config['creditDebitCardPaymentMethodId'] ?? false) {
|
|
||||||
$this->notifierFacade->verifyPaymentActionToDo($notification);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
$payment_method = 'Desconocido';
|
$payment_method = 'Desconocido';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if ($notification->eventName === 'client.edit') {
|
} else if ($notification->eventName === 'client.edit') {
|
||||||
$this->logger->info('Procesando evento client.edit para entityId: ' . ($jsonData['entityId'] ?? 'unknown'));
|
$this->logger->info('Procesando evento client.edit para entityId: ' . ($jsonData['entityId'] ?? 'unknown'));
|
||||||
$this->logger->debug('Payload completo client.edit: ' . json_encode($jsonData));
|
$this->logger->debug('Payload completo client.edit: ' . json_encode($jsonData));
|
||||||
@ -426,7 +385,7 @@ class Plugin
|
|||||||
} else {
|
} else {
|
||||||
$this->logger->info('Llamando a updatePasswordAntenaIfNeeded para cliente: ' . $clientID);
|
$this->logger->info('Llamando a updatePasswordAntenaIfNeeded para cliente: ' . $clientID);
|
||||||
$this->pluginNotifierFacade->updatePasswordAntenaIfNeeded((int)$clientID, $jsonData);
|
$this->pluginNotifierFacade->updatePasswordAntenaIfNeeded((int)$clientID, $jsonData);
|
||||||
// $this->lcdf4d7cf10e2ogger->info('Llamada finalizada exitosamente.');
|
$this->logger->info('Llamada finalizada exitosamente.');
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->logger->error('ERROR FATAL procesando client.edit: ' . $e->getMessage());
|
$this->logger->error('ERROR FATAL procesando client.edit: ' . $e->getMessage());
|
||||||
@ -455,22 +414,17 @@ class Plugin
|
|||||||
|
|
||||||
$clabeTagExistsBefore = false;
|
$clabeTagExistsBefore = false;
|
||||||
$stripeTagExistsBefore = false;
|
$stripeTagExistsBefore = false;
|
||||||
$passwordAntenaTagExistsBefore = false;
|
|
||||||
|
|
||||||
$clabeTagExists = false;
|
$clabeTagExists = false;
|
||||||
$stripeTagExists = false;
|
$stripeTagExists = false;
|
||||||
$passwordAntenaTagExists = false;
|
|
||||||
|
|
||||||
foreach ($tagsBefore as $tag) {
|
foreach ($tagsBefore as $tag) {
|
||||||
if ($tag['name'] === 'CREAR CLABE STRIPE') $clabeTagExistsBefore = true;
|
if ($tag['name'] === 'CREAR CLABE STRIPE') $clabeTagExistsBefore = true;
|
||||||
if ($tag['name'] === 'CREAR CLIENTE STRIPE') $stripeTagExistsBefore = true;
|
if ($tag['name'] === 'CREAR CLIENTE STRIPE') $stripeTagExistsBefore = true;
|
||||||
if ($tag['name'] === 'OBTENER PASSWORD ANTENA') $passwordAntenaTagExistsBefore = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($tags as $tag) {
|
foreach ($tags as $tag) {
|
||||||
if ($tag['name'] === 'CREAR CLABE STRIPE') $clabeTagExists = true;
|
if ($tag['name'] === 'CREAR CLABE STRIPE') $clabeTagExists = true;
|
||||||
if ($tag['name'] === 'CREAR CLIENTE STRIPE') $stripeTagExists = true;
|
if ($tag['name'] === 'CREAR CLIENTE STRIPE') $stripeTagExists = true;
|
||||||
if ($tag['name'] === 'OBTENER PASSWORD ANTENA') $passwordAntenaTagExists = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($clabeTagExists && !$clabeTagExistsBefore) {
|
if ($clabeTagExists && !$clabeTagExistsBefore) {
|
||||||
@ -482,11 +436,6 @@ class Plugin
|
|||||||
$this->logger->debug('La etiqueta CREAR CLIENTE STRIPE se agregó al cliente');
|
$this->logger->debug('La etiqueta CREAR CLIENTE STRIPE se agregó al cliente');
|
||||||
$this->pluginNotifierFacade->createStripeClient($notification, 'CREAR CLIENTE STRIPE', false);
|
$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
|
// Automatización: Sincronizar cambios de Nombre o Email con Stripe
|
||||||
@ -521,15 +470,18 @@ class Plugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->notifierFacade->verifyClientActionToDo($notification);
|
$this->notifierFacade->verifyClientActionToDo($notification);
|
||||||
|
|
||||||
} else if ($notification->eventName === 'client.add') {
|
} else if ($notification->eventName === 'client.add') {
|
||||||
$this->logger->debug('Se agregó un nuevo cliente');
|
$this->logger->debug('Se agregó un nuevo cliente');
|
||||||
$this->logger->debug('Valor de json_data: ' . json_encode($jsonData));
|
$this->logger->debug('Valor de json_data: ' . json_encode($jsonData));
|
||||||
|
|
||||||
} else if ($notification->eventName === 'service.edit') {
|
} else if ($notification->eventName === 'service.edit') {
|
||||||
$this->logger->debug('Se editó el servicio a un cliente' . PHP_EOL);
|
$this->logger->debug('Se editó el servicio a un cliente' . PHP_EOL);
|
||||||
$this->notifierFacade->verifyServiceActionToDo($notification);
|
$this->notifierFacade->verifyServiceActionToDo($notification);
|
||||||
//ejemplo de json_data: {"uuid":"06d281ca-d78e-4f0a-a282-3a6b77d25da0","changeType":"edit","entity":"service","entityId":"155","eventName":"service.edit","extraData":{"entity":{"id":155,"prepaid":false,"clientId":171,"status":1,"name":"Basico 300","fullAddress":"Campeche 56, Dolores Hidalgo, 37800","street1":"Campeche 56","street2":null,"city":"Dolores Hidalgo","countryId":173,"stateId":null,"zipCode":"37800","note":null,"addressGpsLat":21.1572461,"addressGpsLon":-100.9377137,"servicePlanId":6,"servicePlanPeriodId":26,"price":300,"hasIndividualPrice":false,"totalPrice":300,"currencyCode":"MXN","invoiceLabel":null,"contractId":null,"contractLengthType":1,"minimumContractLengthMonths":null,"activeFrom":"2025-05-21T00:00:00-0600","activeTo":null,"contractEndDate":null,"discountType":0,"discountValue":null,"discountInvoiceLabel":"Descuento","discountFrom":null,"discountTo":null,"tax1Id":null,"tax2Id":null,"tax3Id":null,"invoicingStart":"2025-05-21T00:00:00-0600","invoicingPeriodType":1,"invoicingPeriodStartDay":1,"nextInvoicingDayAdjustment":10,"invoicingProratedSeparately":true,"invoicingSeparately":false,"sendEmailsAutomatically":null,"useCreditAutomatically":true,"servicePlanName":"Basico 300","servicePlanPrice":300,"servicePlanPeriod":1,"servicePlanType":"Internet","downloadSpeed":8,"uploadSpeed":8,"hasOutage":false,"unmsClientSiteStatus":null,"fccBlockId":null,"lastInvoicedDate":null,"unmsClientSiteId":"359cb58d-e64f-453a-890e-23d5abb4f116","attributes":[],"addressData":null,"suspensionReasonId":null,"serviceChangeRequestId":null,"setupFeePrice":null,"earlyTerminationFeePrice":null,"downloadSpeedOverride":null,"uploadSpeedOverride":null,"trafficShapingOverrideEnd":null,"trafficShapingOverrideEnabled":false,"servicePlanGroupId":null,"suspensionPeriods":[],"surcharges":[]},"entityBeforeEdit":{"id":155,"prepaid":false,"clientId":171,"status":1,"name":"Basico 300","fullAddress":"Campeche 56, Dolores Hidalgo, 37800","street1":"Campeche 56","street2":null,"city":"Dolores Hidalgo","countryId":173,"stateId":null,"zipCode":"37800","note":null,"addressGpsLat":21.1572461,"addressGpsLon":-100.9377137,"servicePlanId":6,"servicePlanPeriodId":26,"price":300,"hasIndividualPrice":false,"totalPrice":300,"currencyCode":"MXN","invoiceLabel":null,"contractId":null,"contractLengthType":1,"minimumContractLengthMonths":null,"activeFrom":"2025-05-21T00:00:00-0600","activeTo":null,"contractEndDate":null,"discountType":0,"discountValue":null,"discountInvoiceLabel":"Descuento","discountFrom":null,"discountTo":null,"tax1Id":null,"tax2Id":null,"tax3Id":null,"invoicingStart":"2025-05-21T00:00:00-0600","invoicingPeriodType":1,"invoicingPeriodStartDay":1,"nextInvoicingDayAdjustment":10,"invoicingProratedSeparately":true,"invoicingSeparately":false,"sendEmailsAutomatically":null,"useCreditAutomatically":true,"servicePlanName":"Basico 300","servicePlanPrice":300,"servicePlanPeriod":1,"servicePlanType":"Internet","downloadSpeed":8,"uploadSpeed":8,"hasOutage":false,"unmsClientSiteStatus":null,"fccBlockId":null,"lastInvoicedDate":null,"unmsClientSiteId":"359cb58d-e64f-453a-890e-23d5abb4f116","attributes":[],"addressData":null,"suspensionReasonId":null,"serviceChangeRequestId":null,"setupFeePrice":null,"earlyTerminationFeePrice":null,"downloadSpeedOverride":null,"uploadSpeedOverride":null,"trafficShapingOverrideEnd":null,"trafficShapingOverrideEnabled":false,"servicePlanGroupId":null,"suspensionPeriods":[],"surcharges":[]}}}
|
//ejemplo de json_data: {"uuid":"06d281ca-d78e-4f0a-a282-3a6b77d25da0","changeType":"edit","entity":"service","entityId":"155","eventName":"service.edit","extraData":{"entity":{"id":155,"prepaid":false,"clientId":171,"status":1,"name":"Basico 300","fullAddress":"Campeche 56, Dolores Hidalgo, 37800","street1":"Campeche 56","street2":null,"city":"Dolores Hidalgo","countryId":173,"stateId":null,"zipCode":"37800","note":null,"addressGpsLat":21.1572461,"addressGpsLon":-100.9377137,"servicePlanId":6,"servicePlanPeriodId":26,"price":300,"hasIndividualPrice":false,"totalPrice":300,"currencyCode":"MXN","invoiceLabel":null,"contractId":null,"contractLengthType":1,"minimumContractLengthMonths":null,"activeFrom":"2025-05-21T00:00:00-0600","activeTo":null,"contractEndDate":null,"discountType":0,"discountValue":null,"discountInvoiceLabel":"Descuento","discountFrom":null,"discountTo":null,"tax1Id":null,"tax2Id":null,"tax3Id":null,"invoicingStart":"2025-05-21T00:00:00-0600","invoicingPeriodType":1,"invoicingPeriodStartDay":1,"nextInvoicingDayAdjustment":10,"invoicingProratedSeparately":true,"invoicingSeparately":false,"sendEmailsAutomatically":null,"useCreditAutomatically":true,"servicePlanName":"Basico 300","servicePlanPrice":300,"servicePlanPeriod":1,"servicePlanType":"Internet","downloadSpeed":8,"uploadSpeed":8,"hasOutage":false,"unmsClientSiteStatus":null,"fccBlockId":null,"lastInvoicedDate":null,"unmsClientSiteId":"359cb58d-e64f-453a-890e-23d5abb4f116","attributes":[],"addressData":null,"suspensionReasonId":null,"serviceChangeRequestId":null,"setupFeePrice":null,"earlyTerminationFeePrice":null,"downloadSpeedOverride":null,"uploadSpeedOverride":null,"trafficShapingOverrideEnd":null,"trafficShapingOverrideEnabled":false,"servicePlanGroupId":null,"suspensionPeriods":[],"surcharges":[]},"entityBeforeEdit":{"id":155,"prepaid":false,"clientId":171,"status":1,"name":"Basico 300","fullAddress":"Campeche 56, Dolores Hidalgo, 37800","street1":"Campeche 56","street2":null,"city":"Dolores Hidalgo","countryId":173,"stateId":null,"zipCode":"37800","note":null,"addressGpsLat":21.1572461,"addressGpsLon":-100.9377137,"servicePlanId":6,"servicePlanPeriodId":26,"price":300,"hasIndividualPrice":false,"totalPrice":300,"currencyCode":"MXN","invoiceLabel":null,"contractId":null,"contractLengthType":1,"minimumContractLengthMonths":null,"activeFrom":"2025-05-21T00:00:00-0600","activeTo":null,"contractEndDate":null,"discountType":0,"discountValue":null,"discountInvoiceLabel":"Descuento","discountFrom":null,"discountTo":null,"tax1Id":null,"tax2Id":null,"tax3Id":null,"invoicingStart":"2025-05-21T00:00:00-0600","invoicingPeriodType":1,"invoicingPeriodStartDay":1,"nextInvoicingDayAdjustment":10,"invoicingProratedSeparately":true,"invoicingSeparately":false,"sendEmailsAutomatically":null,"useCreditAutomatically":true,"servicePlanName":"Basico 300","servicePlanPrice":300,"servicePlanPeriod":1,"servicePlanType":"Internet","downloadSpeed":8,"uploadSpeed":8,"hasOutage":false,"unmsClientSiteStatus":null,"fccBlockId":null,"lastInvoicedDate":null,"unmsClientSiteId":"359cb58d-e64f-453a-890e-23d5abb4f116","attributes":[],"addressData":null,"suspensionReasonId":null,"serviceChangeRequestId":null,"setupFeePrice":null,"earlyTerminationFeePrice":null,"downloadSpeedOverride":null,"uploadSpeedOverride":null,"trafficShapingOverrideEnd":null,"trafficShapingOverrideEnabled":false,"servicePlanGroupId":null,"suspensionPeriods":[],"surcharges":[]}}}
|
||||||
$clientID = $jsonData['extraData']['entity']['clientId'];
|
$clientID = $jsonData['extraData']['entity']['clientId'];
|
||||||
$this->pluginNotifierFacade->updatePasswordAntenaIfNeeded($clientID, $jsonData);
|
$this->pluginNotifierFacade->updatePasswordAntenaIfNeeded($clientID, $jsonData);
|
||||||
|
|
||||||
} else if ($notification->eventName === 'service.suspend') {
|
} else if ($notification->eventName === 'service.suspend') {
|
||||||
$this->logger->debug('Se suspendió el servicio a un cliente' . PHP_EOL);
|
$this->logger->debug('Se suspendió el servicio a un cliente' . PHP_EOL);
|
||||||
$this->notifierFacade->verifyServiceActionToDo($notification);
|
$this->notifierFacade->verifyServiceActionToDo($notification);
|
||||||
@ -577,6 +529,7 @@ class Plugin
|
|||||||
'title' => $title,
|
'title' => $title,
|
||||||
]);
|
]);
|
||||||
$this->logger->debug('Respuesta de la API al agregar el trabajo: ' . json_encode($responsePatch));
|
$this->logger->debug('Respuesta de la API al agregar el trabajo: ' . json_encode($responsePatch));
|
||||||
|
|
||||||
} else if ($notification->eventName === 'job.edit') {
|
} else if ($notification->eventName === 'job.edit') {
|
||||||
$this->logger->debug('Se actualiza un trabajo' . PHP_EOL);
|
$this->logger->debug('Se actualiza un trabajo' . PHP_EOL);
|
||||||
// $this->logger->debug('Valor de json_data: ' . json_encode($jsonData));
|
// $this->logger->debug('Valor de json_data: ' . json_encode($jsonData));
|
||||||
@ -633,12 +586,14 @@ class Plugin
|
|||||||
$this->logger->debug('Edición de trabajo "En curso" sin cambios de fecha o técnico relevantes para notificación');
|
$this->logger->debug('Edición de trabajo "En curso" sin cambios de fecha o técnico relevantes para notificación');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$this->logger->warning('El campo assignedUserId no existe en entityBeforeEdit o entity');
|
$this->logger->warning('El campo assignedUserId no existe en entityBeforeEdit o entity');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$this->logger->warning('Los datos entityBeforeEdit o entity no están presentes en extraData');
|
$this->logger->warning('Los datos entityBeforeEdit o entity no están presentes en extraData');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//$this->notifierFacade->update($notification);
|
//$this->notifierFacade->update($notification);
|
||||||
|
|||||||
0
src/Service/MinioStorageService.php
Executable file → Normal file
0
src/Service/MinioStorageService.php
Executable file → Normal file
164
src/Service/PaymentIntentService.php
Executable file → Normal file
164
src/Service/PaymentIntentService.php
Executable file → Normal file
@ -8,27 +8,45 @@ use Stripe\Exception\ApiErrorException;
|
|||||||
|
|
||||||
class PaymentIntentService
|
class PaymentIntentService
|
||||||
{
|
{
|
||||||
private $ucrmApi;
|
private $ucrmApiUrl;
|
||||||
|
private $ucrmApiKey;
|
||||||
private $stripeApiKey;
|
private $stripeApiKey;
|
||||||
|
private $httpClient;
|
||||||
private $stripeClient;
|
private $stripeClient;
|
||||||
private $logger;
|
private $logger;
|
||||||
|
|
||||||
public function __construct($ucrmApi, $stripeApiKey, $logger = null)
|
public function __construct($ucrmApiUrl, $ucrmApiKey, $stripeApiKey, $logger = null)
|
||||||
{
|
{
|
||||||
$this->ucrmApi = $ucrmApi;
|
$this->ucrmApiUrl = rtrim($ucrmApiUrl, '/');
|
||||||
|
$this->ucrmApiKey = $ucrmApiKey;
|
||||||
$this->stripeApiKey = $stripeApiKey;
|
$this->stripeApiKey = $stripeApiKey;
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
|
|
||||||
|
$this->httpClient = new Client([
|
||||||
|
'base_uri' => $this->ucrmApiUrl . '/api/v1.0/',
|
||||||
|
'headers' => [
|
||||||
|
'X-Auth-App-Key' => $this->ucrmApiKey,
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
],
|
||||||
|
'verify' => false,
|
||||||
|
'timeout' => 10,
|
||||||
|
]);
|
||||||
|
|
||||||
$this->stripeClient = new StripeClient($this->stripeApiKey);
|
$this->stripeClient = new StripeClient($this->stripeApiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function searchClients($query)
|
public function searchClients($query)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$clients = $this->ucrmApi->get('clients', [
|
$response = $this->httpClient->get('clients', [
|
||||||
|
'query' => [
|
||||||
'query' => $query,
|
'query' => $query,
|
||||||
'limit' => 5
|
'limit' => 5
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$clients = json_decode($response->getBody(), true);
|
||||||
|
|
||||||
return array_map(function ($client) {
|
return array_map(function ($client) {
|
||||||
return [
|
return [
|
||||||
'id' => $client['id'],
|
'id' => $client['id'],
|
||||||
@ -37,12 +55,10 @@ class PaymentIntentService
|
|||||||
'username' => $client['username'],
|
'username' => $client['username'],
|
||||||
'fullAddress' => $client['fullAddress'] ?? '',
|
'fullAddress' => $client['fullAddress'] ?? '',
|
||||||
'companyName' => $client['companyName'],
|
'companyName' => $client['companyName'],
|
||||||
'clientType' => $client['clientType'],
|
'clientType' => $client['clientType'], // 1 = residential, 2 = company
|
||||||
'isSuspended' => $client['isSuspended'] ?? false,
|
|
||||||
'contacts' => $client['contacts'] ?? [],
|
|
||||||
'attributes' => $client['attributes'] ?? []
|
|
||||||
];
|
];
|
||||||
}, $clients);
|
}, $clients);
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return ['error' => $e->getMessage()];
|
return ['error' => $e->getMessage()];
|
||||||
}
|
}
|
||||||
@ -51,7 +67,8 @@ class PaymentIntentService
|
|||||||
public function getClientDetails($clientId)
|
public function getClientDetails($clientId)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$client = $this->ucrmApi->get("clients/{$clientId}");
|
$response = $this->httpClient->get("clients/{$clientId}");
|
||||||
|
$client = json_decode($response->getBody(), true);
|
||||||
|
|
||||||
$stripeCustomerId = null;
|
$stripeCustomerId = null;
|
||||||
$clabeInterbancaria = null;
|
$clabeInterbancaria = null;
|
||||||
@ -76,6 +93,7 @@ class PaymentIntentService
|
|||||||
'email' => $this->getClientEmail($client),
|
'email' => $this->getClientEmail($client),
|
||||||
'accountOutstanding' => $client['accountOutstanding'] ?? 0
|
'accountOutstanding' => $client['accountOutstanding'] ?? 0
|
||||||
];
|
];
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return ['error' => $e->getMessage()];
|
return ['error' => $e->getMessage()];
|
||||||
}
|
}
|
||||||
@ -90,7 +108,7 @@ class PaymentIntentService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $clientData['username'] ?? '';
|
return $clientData['username'] ?? ''; // Fallback, though username might not be email
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createPaymentIntent($clientId, $amount, $stripeCustomerId, $adminId = null)
|
public function createPaymentIntent($clientId, $amount, $stripeCustomerId, $adminId = null)
|
||||||
@ -134,6 +152,7 @@ class PaymentIntentService
|
|||||||
'currency' => $paymentIntent->currency,
|
'currency' => $paymentIntent->currency,
|
||||||
'next_action' => $paymentIntent->next_action
|
'next_action' => $paymentIntent->next_action
|
||||||
];
|
];
|
||||||
|
|
||||||
} catch (ApiErrorException $e) {
|
} catch (ApiErrorException $e) {
|
||||||
return ['success' => false, 'error' => $e->getMessage()];
|
return ['success' => false, 'error' => $e->getMessage()];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
@ -141,131 +160,6 @@ class PaymentIntentService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
private function log($message)
|
||||||
{
|
{
|
||||||
if ($this->logger) {
|
if ($this->logger) {
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
<?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";
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
<?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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0
unms-swagger.json
Executable file → Normal file
0
unms-swagger.json
Executable file → Normal file
0
unmscrm.apib
Executable file → Normal file
0
unmscrm.apib
Executable file → Normal file
0
vendor/aws/aws-crt-php/CODE_OF_CONDUCT.md
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/CODE_OF_CONDUCT.md
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/LICENSE
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/LICENSE
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/NOTICE
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/NOTICE
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/README.md
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/README.md
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/composer.json
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/composer.json
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/AwsCredentials.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/AwsCredentials.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/CredentialsProvider.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/CredentialsProvider.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/Signable.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/Signable.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SignatureType.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SignatureType.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SignedBodyHeaderType.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SignedBodyHeaderType.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/Signing.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/Signing.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SigningAlgorithm.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SigningAlgorithm.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SigningConfigAWS.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SigningConfigAWS.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SigningResult.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/SigningResult.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/StaticCredentialsProvider.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Auth/StaticCredentialsProvider.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/CRT.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/CRT.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/HTTP/Headers.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/HTTP/Headers.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/HTTP/Message.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/HTTP/Message.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/HTTP/Request.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/HTTP/Request.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/HTTP/Response.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/HTTP/Response.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/IO/EventLoopGroup.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/IO/EventLoopGroup.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/IO/InputStream.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/IO/InputStream.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Internal/Encoding.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Internal/Encoding.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Internal/Extension.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Internal/Extension.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Log.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Log.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/NativeResource.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/NativeResource.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Options.php
vendored
Executable file → Normal file
0
vendor/aws/aws-crt-php/src/AWS/CRT/Options.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/CODE_OF_CONDUCT.md
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/CODE_OF_CONDUCT.md
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/CRT_INSTRUCTIONS.md
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/CRT_INSTRUCTIONS.md
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/LICENSE
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/LICENSE
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/NOTICE
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/NOTICE
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/THIRD-PARTY-LICENSES
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/THIRD-PARTY-LICENSES
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/composer.json
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/composer.json
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/ACMPCA/ACMPCAClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/ACMPCA/ACMPCAClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/ACMPCA/Exception/ACMPCAException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/ACMPCA/Exception/ACMPCAException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AIOps/AIOpsClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AIOps/AIOpsClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AIOps/Exception/AIOpsException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AIOps/Exception/AIOpsException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/ARCRegionSwitch/ARCRegionSwitchClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/ARCRegionSwitch/ARCRegionSwitchClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/ARCRegionSwitch/Exception/ARCRegionSwitchException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/ARCRegionSwitch/Exception/ARCRegionSwitchException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/ARCZonalShift/ARCZonalShiftClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/ARCZonalShift/ARCZonalShiftClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/ARCZonalShift/Exception/ARCZonalShiftException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/ARCZonalShift/Exception/ARCZonalShiftException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AbstractConfigurationProvider.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AbstractConfigurationProvider.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AccessAnalyzer/AccessAnalyzerClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AccessAnalyzer/AccessAnalyzerClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AccessAnalyzer/Exception/AccessAnalyzerException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AccessAnalyzer/Exception/AccessAnalyzerException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Account/AccountClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Account/AccountClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Account/Exception/AccountException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Account/Exception/AccountException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Acm/AcmClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Acm/AcmClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Acm/Exception/AcmException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Acm/Exception/AcmException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Amplify/AmplifyClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Amplify/AmplifyClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Amplify/Exception/AmplifyException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Amplify/Exception/AmplifyException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AmplifyBackend/AmplifyBackendClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AmplifyBackend/AmplifyBackendClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AmplifyBackend/Exception/AmplifyBackendException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AmplifyBackend/Exception/AmplifyBackendException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AmplifyUIBuilder/AmplifyUIBuilderClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AmplifyUIBuilder/AmplifyUIBuilderClient.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AmplifyUIBuilder/Exception/AmplifyUIBuilderException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/AmplifyUIBuilder/Exception/AmplifyUIBuilderException.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Api/AbstractModel.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Api/AbstractModel.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Api/ApiProvider.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Api/ApiProvider.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Api/DateTimeResult.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Api/DateTimeResult.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Api/DocModel.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Api/DocModel.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Api/ErrorParser/AbstractErrorParser.php
vendored
Executable file → Normal file
0
vendor/aws/aws-sdk-php/src/Api/ErrorParser/AbstractErrorParser.php
vendored
Executable file → Normal file
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user