Commit Graph
353 Commits
Author SHA1 Message Date
LizandroandClaude Sonnet 4.6 807472f07d fix: indicador de subida colgado y apertura directa de servicio por nombre
1) "Subiendo comprobante..." no se quitaba nunca.

   La miniatura solo se limpiaba con livewire-upload-finish, que cubre la
   subida del archivo pero no el procesado posterior. Y updatedFotoComprobante
   tenía tres rutas de salida tempranas (sin convId, análisis deshabilitado,
   validación fallida) que ni siquiera llegaban al finally.

   Ahora todo el cuerpo va dentro de un try/finally que emite siempre
   'comprobante-listo', y el cliente limpia con ese evento del servidor en vez
   de depender de los eventos internos de Livewire. Se añade además un timeout
   de 45s como red de seguridad.

2) "cuenta de hbo" abría el listado completo en vez de HBO.

   El regex de servicios.listar tenía las marcas quemadas (netflix|hbo|disney…),
   así que capturaba la frase y devolvía data vacía antes de que la IA pudiera
   extraer el nombre. Telegram no falla porque sus keywords son solo intents
   genéricos y deja los específicos al agente.

   - Se sacan las marcas del regex genérico.
   - Nuevo detectarServicioPorNombre(): resuelve contra los nombres reales de
     la tabla servicios (nada quemado en código, sin llamada a la IA) y abre
     el servicio pedido.
   - clickBoton('servicios.listar') ahora honra data.servicio, como hace
     dispatchAction en Telegram.
   - El prompt de GeminiIntentService pide data.servicio. Este servicio lo usa
     solo el chat web; Telegram usa GeminiAgentService y no se toca.

   El patrón de credenciales se refuerza y pasa a evaluarse primero, para que
   "no puedo entrar a netflix" siga siendo soporte y no una intención de compra.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-01 14:19:34 +00:00
LizandroandClaude Sonnet 4.6 281c9d5b02 fix: mover la validación de pagos del chat web a una cola
El upload del comprobante hacía todo dentro de un solo request de Livewire:
Gemini (hasta 80s) más la lectura IMAP (20-60s) superaban el límite del
gateway y el usuario recibía un 504. Además, con SESSION_DRIVER=file ese
request mantenía bloqueado el archivo de sesión, así que el wire:poll del
chat no podía avanzar: la conversación entera se congelaba y el mensaje
"Analizando tu comprobante" se quedaba pegado para siempre.

Ahora updatedFotoComprobante() solo guarda la imagen, escribe el primer
mensaje y despacha ValidarPagoWebJob. Termina en ~1s.

El job hace el trabajo pesado fuera del request y va escribiendo su avance
como mensajes normales del chat, así el usuario ve los pasos:
  "Recibido, leyendo los datos..." -> "Datos leídos, buscando en el banco..."
  -> resultado
Reintenta cada 20s hasta 9 veces, igual que el flujo de Telegram.

El saldo se acredita dentro del job, no en el componente: si el usuario
cerraba o recargaba la pestaña durante la validación, el pago se confirmaba
pero el dinero nunca se abonaba. El componente ahora solo refresca el saldo
en pantalla y remata la compra que hubiera quedado pendiente.

Otros cierres del flujo que faltaban:
- failed() escribe un mensaje si el job muere, para no dejar el chat colgado.
- revisarPago() se rinde a los 6 min si el worker está caído.
- Los estados terminales dejan siempre un botón de menú.
- El botón Reintentar reencola en vez de validar dentro del request.

No se toca Telegram ni ningún servicio compartido (GeminiVisionService,
PagoValidadorService, CorreoImapService, ValidarPagoTelegramJob).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-01 14:12:57 +00:00
LizandroandClaude Sonnet 4.6 f4f689891c feat: chat público con sensación de WhatsApp/Telegram real
Bugs corregidos:
- La imagen del comprobante nunca se veía: se guardaba en el disco 'public'
  y se servía como asset('storage/...'), pero este proyecto no tiene el
  symlink public/storage. Ahora usa store('photos') + asset($path), la
  convención del resto del código (config/filesystems.php links).
- Dos wire:poll en el mismo nodo: Livewire v2 solo respeta el primero, así
  que autoReintentarPago nunca se ejecutaba. Ahora va en su propio elemento
  y solo se renderiza mientras hay un pago pendiente.
- Sin wire:key en los mensajes, morphdom recreaba toda la lista cada 3s:
  parpadeo, scroll saltando y estado Alpine perdido (el "¡Copiado!" de las
  credenciales desaparecía solo).
- pollMensajes cargaba la conversación entera cada 3s. Ahora consulta solo
  MAX(id) y recarga únicamente si llegó algo nuevo; el historial se limita
  a los 60 mensajes más recientes.
- El upload no se validaba: se añade regla image/mimes/max 8MB.
- Se elimina $procesandoImagen, propiedad muerta que viajaba en cada payload.

Interfaz:
- Envío optimista: la burbuja del usuario aparece al instante, sin esperar
  el round-trip, con reloj de "pendiente" y checks al confirmarse.
- Indicador "escribiendo…" con puntos animados mientras el bot responde.
- Visor de imagen a pantalla completa al tocar el comprobante.
- Textarea que crece con el texto (Shift+Enter para salto de línea),
  con wire:ignore para que el poll no le reinicie la altura al escribir.
- Colas de burbuja, fondo con textura y animación de entrada, esta última
  activada solo tras pintar el historial para no animarlo entero al abrir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-01 13:26:01 +00:00
LizandroandClaude Sonnet 4.6 d88f67ab5b fix: corregir pipeline de validación de pagos
- GeminiVisionService: eliminar thinkingConfig (rompe modelos flash),
  reducir maxOutputTokens 8192→512, timeout 20→40s, default gemini-2.0-flash
- PublicChat: detectar correctamente errores de Gemini (__error/__parse_error)
  que antes pasaban el guard if(!$datosPago) por ser arrays truthy
- PagoValidadorService: tolerancia de hora 0→3 min, remitente >=2→>=1 palabra
- CorreoImapService: break→continue en correos fuera de ventana (IMAP no
  garantiza orden cronológico), fetchSize *4→*2 para evitar timeout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-31 05:00:23 +00:00
LizandroandClaude Sonnet 4.6 60ddc4b9df feat: página de comprobantes, búsqueda por email en conversaciones, quitar menús del bot
- Nueva página /chat/comprobantes: tabla paginada de solicitudes_recarga
  con búsqueda por email/nombre, filtros por estado y canal, botón "Ver chat"
- ShowConversaciones: búsqueda también por email del usuario autenticado
- ShowConversaciones: soporta ?conv=ID para abrir conversación desde comprobantes
- Nav: reemplaza "Menús del Bot" (no funcional) por "Comprobantes"
- SolicitudRecarga: relación user() agregada
- ChatController: método comprobantes() agregado

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 21:13:31 +00:00
LizandroandClaude Sonnet 4.6 16315da176 fix: correcciones de seguridad pre-producción identificadas en code review
- autoAplicarRecarga: null-check en User::find + DB::transaction atómico
- pagarConSaldo: $cuenta->update('ocupado') dentro de la transacción (evita doble venta)
- comprarPromoConSaldo: Historiale::create movido dentro de la transacción (evita registros huérfanos)
- mostrarTransferencia: array_merge en lugar de asignación directa (preserva pending_purchase)
- Blade copiarTodo: lee valores desde x-ref del DOM en vez de escapado PHP→JS
- ValidarPagoTelegramJob: eliminado import App\Models\User no utilizado

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 19:24:36 +00:00
LizandroandClaude Sonnet 4.6 1245d255bb 4 mejoras en canales de comunicación con el cliente
1. Reintentos automáticos (Telegram Job): usa autoAplicarRecarga para ejecutar
   la compra pendiente automáticamente al confirmar el pago, sin botón extra.

2. Pagar servicio directo con Bre-B sin recargar saldo previamente (ambos canales):
   botón 'Pagar con Bre-B directo' en el flujo de compra con saldo insuficiente.

3. Mensaje de gracias + bloque de credenciales fácil de copiar al entregar servicio:
   Telegram usa bloque de código (tiene botón Copy nativo), Chat Web muestra
   el texto completo de gracias con copia por campo y botón 'Copiar todo'.

4. Botón copiar llave Bre-B: ya existía en Chat Web, confirmado sin cambio.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 12:04:33 +00:00
LizandroandClaude Sonnet 4.6 932a9b43c7 Saltar selección de monto si el cliente lo incluye en el mensaje de recarga
Tanto en Telegram como en el chat web, cuando el usuario escribe el monto
directamente (ej: "recargar 10.000"), el bot ahora va directo al paso de
método de pago sin volver a preguntar el monto.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 11:34:46 +00:00
LizandroandClaude Sonnet 4.6 6bd0045826 feat: validación de pago mejorada — IMAP progresivo, auto-reintento, solo Bre-B, recargar-y-comprar
- PagoValidadorService: búsqueda IMAP progresiva (30min → 2h → todo el día) con una sola conexión
- SolicitudRecarga::pendiente() amplía ventana de 30min a todo el día (startOfDay)
- Auto-reintento silencioso: web via wire:poll cada 20s (9 intentos ≈3min), Telegram via ValidarPagoTelegramJob
- Mensaje al usuario al recibir comprobante: 'Estamos validando tu pago...' en lugar de botón inmediato
- Aviso en comprobante: indica que debe mostrarse nombre, valor, fecha y hora
- Eliminado MercadoPago de todos los flujos de recarga y compra (solo Bre-B)
- Flujo 'recargar y comprar': guarda pending_purchase y ejecuta la compra automáticamente tras confirmar recarga
- Nuevo job ValidarPagoTelegramJob para reintentos asincrónicos en Telegram

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-26 21:09:58 +00:00
LizandroandClaude Sonnet 4.6 23eb448433 feat: requiere nombre y apellido (mínimo 2 palabras) en flujo de recarga
- PublicChat: valida ≥2 palabras al ingresar nombre_remitente en chat web
- PagoValidadorService: remitenteCoincide exige ≥2 palabras que coincidan en el correo
- TelegramBotService: processRemitenteNombre exige ≥2 palabras igual que el chat web

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 23:13:55 +00:00
LizandroandClaude Sonnet 4.6 c294d3f2b8 fix: pago.reintentar usa nombre_remitente del flujoData igual que el primer intento
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 22:53:06 +00:00
LizandroandClaude Sonnet 4.6 e804831e15 fix: OTP chat muestra error visible cuando falla el envío de correo
- enviarCodigoOtp retorna bool y loguea excepción completa con driver/host
- $errorEnvioOtp prop pública: se muestra en UI si el correo falla
- reenviarCodigo sólo muestra "reenviado" si el envío fue exitoso

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 21:11:21 +00:00
LizandroandClaude Sonnet 4.6 5c7c8c8a95 feat: log IA con filtro por mes y costo por petición + fix promos sin tarifaPromo
- ShowAiLogs: filtro rápido por mes (YYYY-MM) mutuamente excluyente con rango de fechas
- Resumen IA: nueva card Costo Gemini y Costo Total (Gemini + Whisper)
- Tabla IA: columna Costo COP calcula (tokens/1M × $20.000) para Gemini, costo guardado para Whisper
- PublicChat + TelegramBotService: quita whereHas('tarifaPromo') que ocultaba promos sin tarifa por rol

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 21:08:40 +00:00
LizandroandClaude Sonnet 4.6 a9d0787076 fix: limpiarChat crea conversación nueva en lugar de dejar convId null
Antes: limpiar → convId=null → 'Iniciando chat...' → refresh recuperaba la conv vieja.
Ahora: limpiar → nueva conversación → menú principal inmediato, sin refresh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 19:00:13 +00:00
LizandroandClaude Sonnet 4.6 7307d87c80 fix: chat bloqueado en 'Iniciando chat...' cuando sesión pierde chat_conv_id
Si el usuario tiene chat_user_id en sesión pero no chat_conv_id (sesión
expirada o servidor reiniciado), mount() ahora busca la conversación activa
del contacto en la BD o crea una nueva. Antes quedaba sin convId y el enviar
no hacía nada.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 18:56:16 +00:00
LizandroandClaude Sonnet 4.6 bb311e7dcd feat: validación exacta de hora + remitente obligatorio + nombres guardados
- Hora exacta: tolerancia 0 (timestamp comprobante = timestamp email)
- Remitente bloquea: si el email trae el nombre del remitente y no coincide, rechaza
- Nuevo paso antes de mostrar datos bancarios: bot pide nombre completo del titular
- Nombres aceptados se guardan en solicitudes_recarga.nombre_remitente
- Próxima recarga: muestra botones con nombres anteriores confirmados o ingresa uno nuevo
- Funciona igual en Telegram y web chat

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 15:27:31 +00:00
LizandroandClaude Sonnet 4.6 e42dd40d74 feat: registrar recarga en tabla recargas al auto-aplicar (Telegram y web chat)
Usa el mismo modelo recarga que el flujo admin, con status='Confirmado'
y reference='breb-bot/chat-{solicitud_id}' para trazabilidad.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 15:14:14 +00:00
LizandroandClaude Sonnet 4.6 437adc531f fix: verificar solicitud de recarga antes de llamar Gemini Vision e IMAP
Sin solicitud activa el bot mostraba el análisis completo (Gemini + IMAP) antes de rechazar.
Ahora se valida primero: si no hay solicitud pendiente, se rechaza de inmediato sin gastar API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 14:46:21 +00:00
LizandroandClaude Sonnet 4.6 a7d7c32350 feat: sistema de solicitud de recarga con auto-aplicación y botón Reintentar
- SolicitudRecarga: modelo + migración para atar cada intento de recarga al usuario (previene cruce de pagos entre usuarios)
- PublicChat: mostrarTransferencia crea solicitud, updatedFotoComprobante valida contra ella, auto-aplica sin botón extra
- Botón Reintentar en web chat cuando el correo aún no llega (pago_pendiente en flujoData)
- aplicarRecargaValidada corregido: error de sintaxis por fragmento duplicado eliminado

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 14:40:52 +00:00
LizandroandClaude Sonnet 4.6 d5e497bfc5 fix: aviso 15 min va en instrucciones de transferencia, no al recibir foto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 14:24:27 +00:00
LizandroandClaude Sonnet 4.6 28d3b1c719 feat: botón Reintentar en web chat + aviso de 15 min en ambos canales
- PublicChat: sin_coincidencia guarda datosPago en flujoData, muestra Reintentar
- PublicChat: pago.reintentar re-corre validación sin reenviar foto
- Telegram y web chat: aviso al recibir comprobante (15 min visibles, 30 reales)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 14:19:31 +00:00
LizandroandClaude Sonnet 4.6 eb448794c5 fix(chat): limpiarChat no cierra sesión, solo borra mensajes
mount() ahora solo requiere chat_user_id (no chat_conv_id) para mantener
al usuario logueado. limpiarChat() borra mensajes y convId pero preserva
userId, nombreUsuario, saldoUsuario y paso='chat'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-18 19:31:20 +00:00
LizandroandClaude Sonnet 4.6 c3f9984a9d feat(chat): menú tres puntos con Limpiar chat y Cerrar sesión
- Reemplaza botón suelto de logout por dropdown (Alpine.js) con dos opciones
- limpiarChat(): limpia mensajes visibles y cierra conversación actual
- cerrarSesion(): ya existía, se mantiene igual
- Cierra el dropdown al hacer clic fuera (x-click.outside)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-18 19:27:17 +00:00
LizandroandClaude Sonnet 4.6 1fd9a92afa Remove manual payment application button for unconfirmed receipts
Only email-confirmed payments get the apply button. Unconfirmed payments
show the failure reason and direct the user to an advisor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-18 16:44:28 +00:00
LizandroandClaude Sonnet 4.6 6c0396d3c0 Show validation failure reason and allow manual payment application
When email validation fails (IMAP disabled, no emails, no match), show the
specific reason instead of a generic "No confirmado". Also add a manual
"Aplicar de todas formas" button so the user isn't stuck — they can still
apply the recharge after visually verifying the receipt.

Also show banco/fecha/hora in the Telegram analysis result for context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-18 16:27:56 +00:00
LizandroandClaude Sonnet 4.6 b26f6f1130 Services and promos purchasable only with balance; redirect to recharge if short
Remove MercadoPago as a direct payment option for services and promotions.
If the user has enough balance: show only "Pagar con saldo".
If not: show current balance, amount missing, and a "Recargar saldo" button
that leads to the recharge flow (where MercadoPago and Bre-B remain available).

Applies to both TelegramBotService (startPurchase, viewPromo) and
PublicChat (iniciarCompra, verPromocion).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-18 16:15:45 +00:00
LizandroandClaude Sonnet 4.6 2f5be9253b Fix 5 bugs in chat and Telegram purchase flows
1. PublicChat::comprarPromoConSaldo — double saldo subtraction: $saldo->update()
   already updates the model in memory, so $this->saldoUsuario = $saldo->valor - precio
   was subtracting twice. Fixed by computing $nuevoSaldo before the update.
   Also wrapped in DB::transaction (was missing unlike pagarConSaldo).

2. TelegramBotService::listServices — showed all services without filtering by the
   user's rol_id, so users could see services with no plans for their role.
   Now uses whereHas('tarifas', rol_id) like PublicChat and the web.

3. TelegramBotService::payWithSaldo — no DB::transaction; if saldo update failed
   after historial was created, user got a free service. Now wrapped atomically.

4. TelegramBotService::buyPromoSaldo — same missing transaction + fecha_final was
   hardcoded to 30 days instead of promo->dias. Both fixed.

5. TelegramBotService::showCredentials — N+1 queries loading cuentas lazily per
   historial. Added cuentas to the eager-load list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-18 16:06:20 +00:00
LizandroandClaude Sonnet 4.6 1f25c31603 Fix "sin cuentas disponibles" and wrong pricing in bot/chat
Three bugs fixed:

1. TelegramBotService queried Cuentas by tarifa_id + estado=activo which
   never matched. Now uses CuentasHelper::contar/buscar — same date-range
   + historial-count logic as the web (pendiente for full accounts,
   activo + slot check for per-screen accounts).

2. efectivePrice in TelegramBotService and PublicChat used the Preferencial
   table (legacy) instead of Usuario_tarifa (used by the web admin panel).
   CuentasHelper::precio checks Usuario_tarifa first, Preferencial as fallback.

3. payWithSaldo compared saldo against tarifa->valor (base price) instead of
   the effective price stored in state. Also registered the purchase with the
   wrong amount.

Extract shared logic to CuentasHelper (contar, buscar, precio) to avoid
duplication between TelegramBotService and PublicChat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-18 16:02:54 +00:00
LizandroandClaude Sonnet 4.6 157691e166 Fix maxOutputTokens too low for gemini-3.5-flash thinking model
gemini-3.5-flash uses thinking tokens that share the same budget as output
tokens. With maxOutputTokens=200 (vision) or 100 (intent), the model would
exhaust the budget on thinking (~288-1043 tokens) and truncate the actual
response to 8-11 tokens, producing garbage output like "040825".

Fix: 2048 for vision (needs full JSON), 1024 for intent, 512 for connection test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-15 18:09:45 +00:00
LizandroandClaude Sonnet 4.6 cfa38e51df fix: change default Gemini model to gemini-3.5-flash
gemini-2.5-flash and gemini-2.0-flash-lite are restricted/unavailable.
gemini-3.5-flash is confirmed working on this account.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-15 04:21:55 +00:00
LizandroandClaude Sonnet 4.6 13199b1de4 fix: remove thinkingConfig entirely — field does not exist in Gemini API
The API returned: Unknown name "thinkingConfig": Cannot find field.
gemini-2.5-flash works with a plain generateContent request, no special config needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-15 04:15:06 +00:00
LizandroandClaude Sonnet 4.6 54736a8742 fix: use v1 API for Gemini 2.5+/3.x models; show real error + generateContent filter
- GeminiIntentService/VisionService: auto-select v1 for 2.5+ models, v1beta for older
- probarGemini: tries v1 then v1beta, shows actual API error message, filters model
  list to only those supporting generateContent so the user sees usable options

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-15 04:13:09 +00:00
LizandroandClaude Sonnet 4.6 77c3353213 fix: catch all exceptions in Telegram email flow + webhook global handler
- processEmail: wrap DB lookup and ChatContact create/update in try-catch
  so any exception sends an error message to the user instead of silently
  failing and leaving state stuck at await_email
- sendOtp: inform user if mail sending fails so they know to wait/retry
- TelegramWebhookController: top-level try-catch returns 200 always so
  Telegram never retries on server errors (retries caused duplicate state resets)
- ChatContact canal_id cast to string to avoid type mismatch on insert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-15 04:01:53 +00:00
LizandroandClaude Sonnet 4.6 eb6bb8e7f3 fix: move thinkingConfig to root level of Gemini request body
thinkingConfig must be a top-level key in the request, NOT nested inside
generationConfig. This is required for gemini-2.5-flash and newer models.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 20:24:17 +00:00
LizandroandClaude Sonnet 4.6 a311a7f0d3 fix: add thinkingConfig for Gemini 2.5+ models
gemini-2.5-flash and newer require thinkingBudget:0 to disable thinking
mode, otherwise the API rejects the request or returns unexpected format.
Regex matches gemini-2.5-* and gemini-3.x-* automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 20:19:17 +00:00
LizandroandClaude Sonnet 4.6 75eddd16a6 feat: make Gemini model configurable + auto-list available models on error
- gemini_model is now a saved config field (default: gemini-2.0-flash)
- Config panel shows a text input to set the model name
- probarGemini() reads the configured model and on error calls ListModels
  to show exactly which model names are available for the account's API key
- GeminiIntentService and GeminiVisionService build the URL from config at runtime

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 20:11:32 +00:00
LizandroandClaude Sonnet 4.6 deb4d040c3 fix: change Gemini model to 2.5-flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 20:07:52 +00:00
LizandroandClaude Sonnet 4.6 4674220c29 fix: upgrade Gemini model from 1.5-flash to 2.0-flash
gemini-1.5-flash was removed from the v1beta API. Updated all three
references (GeminiIntentService, GeminiVisionService, config test).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 20:04:54 +00:00
LizandroandClaude Sonnet 4.6 13386f5b49 feat: add Gemini connection test button in config panel
probarGemini() sends a minimal prompt to gemini-1.5-flash and reports
latency + trimmed response text, or the error message from the API.
Mirrors the existing Whisper test button pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 19:35:16 +00:00
LizandroandClaude Sonnet 4.6 30fc8c0f1b fix: email mask shows max 4 chars instead of half the local part
lizandro.guarnizo (17 chars) was showing 'lizandro.' (9 chars including the dot)
because ceil(17/2)=9. Now always shows exactly min(4, len) chars: 'liza*************@domain'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 19:34:14 +00:00
LizandroandClaude Sonnet 4.6 9a13bad235 feat: never-expire Telegram session + logout confirmation on both channels
- STATE_TTL: 7200s → 315360000s (10 years) so sessions survive indefinitely
- Telegram: "salir"/"/salir" now shows inline confirmation keyboard instead of going straight to menu; "Cerrar sesión" button added to main menu; logout_ask/logout_confirm/confirmLogout() handler clears state and returns to email prompt
- Web chat: "Cerrar sesión" button in main menu; clicking it shows a confirmation message with "Sí, cerrar sesión" / "No, quedarme" buttons; only the confirmed action calls cerrarSesion()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 19:32:06 +00:00
LizandroandClaude Sonnet 4.6 fb92ebfb1c Fix SQL string literals for PostgreSQL compatibility in AI logs query
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 19:21:39 +00:00
LizandroandClaude Sonnet 4.6 44297aa0bb Add AI usage logs for Gemini and Whisper
- Migration + AiUsageLog model with tokens, duration, time, cost fields
- GeminiIntentService and GeminiVisionService log every API call
  with input/output token counts from usageMetadata and response time
- TelegramBotService logs Whisper calls with audio duration (from Telegram)
  and calculates cost at $1/second
- Whisper voice duration now passed from TelegramWebhookController
- Free text in Telegram now tries Gemini intent detection before showing menu
- /chat/ia-logs: Livewire component with summary cards + filterable table
- Whisper connection test button in config panel
- "Logs IA" link added to Chat/Bot nav section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 19:13:59 +00:00
LizandroandClaude Sonnet 4.6 f39f3d4045 Add Whisper voice transcription to Telegram bot
- handleVoice() downloads Telegram audio and transcribes via Whisper API
- transcribeAudio() calls Whisper with Basic Auth, handles json response
- TelegramWebhookController now routes message.voice and message.audio
- Whisper URL, token and toggle configurable from admin config panel
- Transcribed text echoed back to user then processed as normal text intent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 19:00:57 +00:00
LizandroandClaude Sonnet 4.6 3f2bdc0d47 Add menu/salir commands to Telegram bot
- Any message matching salir/menu/volver/cancelar/inicio resets flow and shows main menu
- Register /menu /salir /cancelar as official bot commands via setMyCommands
- Commands are registered automatically when saving the webhook from admin panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 18:40:17 +00:00
LizandroandClaude Sonnet 4.6 50bbd159c6 Fix Telegram webhook registration: replace file_get_contents with Http client
file_get_contents fails on many hosts due to allow_url_fopen or SSL restrictions.
Http:: (Guzzle) is more reliable and shows a clearer error message on failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 18:26:39 +00:00
LizandroandClaude Sonnet 4.6 e43fe4043f Improve intent detection: keyword layer + better Gemini prompt
- Add detectarIntencionKeyword() with regex patterns in Colombian Spanish
  that runs before Gemini (instant, no API cost)
- Gemini is now only called for ambiguous text
- Improve Gemini prompt with real examples and informal Spanish
- Replace generic "no entendí" with actionable buttons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 15:25:39 +00:00
LizandroandClaude Sonnet 4.6 a21675b12c Fix promotion listing: role-based filtering and correct pricing
- Filter promos by TarifaPromo.rol_id (same as services)
- Fix visible filter: use string 'true' not boolean
- Get price from Usuario_promo or TarifaPromo instead of Promociones.precio
- Get utilidad from TarifaPromo for correct margin tracking
- Applied same fixes to TelegramBotService promo flows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 15:19:28 +00:00
LizandroandClaude Sonnet 4.6 b4624db056 fix: account lookup now uses real availability logic (servicio_id + pantallas)
- Replace tarifa_id+estado='ocupado' query with the same logic used in
  ShowServicios: filter by servicio_id, date range, historiales_count
- Full-account plans (pantallas == completa): look for estado='pendiente'
  accounts without assigned profiles
- Partial plans (screen sharing): look for estado='activo' accounts
  with remaining slots (historiales_count <= pantallas_servicio - pantallas)
- Wrap purchase in DB::transaction to avoid race conditions
- Remove incorrect cuenta->update(['estado' => 'ocupado']) — availability
  is now tracked via historial_cuentas count, not a flag
- Use effective price (Preferencial override) for saldo deduction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 13:46:36 +00:00
LizandroandClaude Sonnet 4.6 26953c9dd5 feat: smart scroll — keep position when reading, badge for new messages
- Add pollMensajes() so wire:poll no longer force-scrolls to bottom
- MutationObserver detects new DOM nodes: auto-scrolls only if user
  is already at the bottom (within 80px threshold)
- Floating '↓ N mensaje nuevo' badge appears when new bot messages
  arrive while user is scrolled up; tap badge to jump to bottom
- scroll-chat event (emitted on button clicks/sends) still forces
  scroll since the user just took an action

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 13:36:31 +00:00