- Muestra thumbnail de la imagen seleccionada inmediatamente (FileReader)
con spinner superpuesto mientras Livewire sube el archivo
- wire:loading wire:target="fotoComprobante" muestra "Analizando con IA..."
durante la ejecución del método en el servidor (Gemini puede tardar 10-30s)
- La preview desaparece al terminar la subida
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reemplaza el @if ($procesandoImagen) de PHP (que nunca se veía porque
se activa y desactiva en el mismo request) por un indicador Alpine.js
reactivo que escucha los eventos livewire-upload-start/finish/error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
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>
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>
- 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>
- 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>
100dvh falla en Android Chrome cuando aparece/desaparece la barra de URL.
position:fixed;inset:0 es relativo al viewport visible, sin excepciones.
Desktop mantiene card centrada con position:relative.
Se elimina el hack de visualViewport ya que no es necesario con fixed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Eliminado script de scroll en public-chat.blade.php (conflictaba con el del layout)
- Reescrito script en chat-publico.blade.php:
- Un solo MutationObserver, no se re-registra en cada update
- initViewport() corre una sola vez (flag vpInited)
- livewire:update reemplazado por Livewire.hook('message.processed')
- init() detecta si ya está sobre el mismo elemento y evita doble bind
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MutationObserver disparaba scroll repetido durante render de Livewire
bloqueando el touch scroll en Android. Ahora usa Livewire.hook('message.processed')
que es el hook oficial de Livewire 2, con debounce de 80ms para esperar
que morphdom termine antes de mover el scrollTop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
livewire:update no es un evento DOM válido en Livewire 2.
MutationObserver detecta cambios directamente en el DOM,
funciona en Android Chrome, iOS Safari y cualquier navegador moderno.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Al cargar, baja automáticamente al último mensaje
- Después de cada actualización Livewire, baja si el usuario está al fondo
- Si el usuario subió a leer historial, muestra badge "N mensaje nuevo" en lugar de forzar scroll
- Click en badge baja suavemente al fondo y lo oculta
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- 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>
El path 'photos/abc.webp' desde /chat/ se resolvía como /chat/photos/...
que no existe. asset() genera la URL absoluta correcta desde la raíz del sitio.
Se preservan URLs absolutas (http//) sin modificar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Sin este índice, dos procesos PHP simultáneos podían pasar el check yaUsado()
ambos con false y marcar el mismo hash dos veces, aplicando la recarga doble.
Con UNIQUE, el segundo INSERT falla y el catch existente lo bloquea (falla cerrado).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
El timestamp del correo Bancolombia registra la hora exacta de la transacción,
igual que el comprobante. El retraso es en la llegada al inbox, no en el contenido.
±2 min cubre desfases menores sin permitir cruces entre transacciones cercanas.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Con return inmediato, si Usuario A usaba email_A ($20k), Usuario B
(también $20k) encontraba ese email ya_usado y se bloqueaba sin revisar
el email_B que le pertenecía.
Ahora: ya_usado → continue (sigue iterando); solo retorna ya_usado
al final del loop si todos los emails coincidentes estaban agotados
(caso real de comprobante reutilizado).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Antes: monto coincide → hash ya_usado → retorna "reutilizado" (falso positivo
si había un email viejo del mismo valor en la ventana IMAP de 120 min).
Ahora: monto + fecha + hora + llave deben coincidir primero; solo entonces
se verifica el hash — garantizando que es el mismo comprobante exacto.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- 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>
- sin_coincidencia muestra mensaje explicativo + botón Reintentar
- datosPago guardado en estado para reintentar sin reenviar foto
- pay_retry callback: re-corre validación con datos guardados
- Si confirma: limpia pendiente y muestra botón aplicar recarga
- Si sigue sin llegar: mantiene botón Reintentar disponible
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Default de 5 min causaba que pagos enviados minutos después del correo
de Bancolombia quedaran fuera de la ventana si había otros correos recientes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Keywords: captura "no inicia sesión Netflix", "no abre", "error de acceso" → credenciales.listar
- GeminiAgent prompt: bot no pide pantallazos ni promete revisiones; problemas de acceso → credenciales.listar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si el cliente tiene Telegram → mensaje por Telegram.
Si el cliente tiene chat web activo → mensaje en la conversación.
Ambos canales pueden coexistir.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cuando el servicio de un cliente vence mañana, además del email existente
se envía mensaje por Telegram si el cliente tiene el bot vinculado.
Busca en chat_contacts canal=telegram con el user_id del cliente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nuevo GeminiAgentService: entiende lenguaje natural, responde conversacionalmente
y extrae acción + parámetros (ej: servicio:"Netflix", monto:50000)
- handleFreeText usa GeminiAgentService en lugar de GeminiIntentService
- dispatchAction acepta accion_data: va directo a Netflix sin pasar por el listado
- viewServiceByName: busca servicio por nombre (ILIKE) y abre sus planes
- Keywords simplificadas: solo intents genéricos sin parámetros
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'vamos a hacer una recarga' no era detectado por Gemini (thinkingBudget=0
+ frases coloquiales). Agrega detectarPorKeywords() con regex para las 6
acciones principales. Gemini queda como fallback para casos ambiguos.
También elimina asesor.solicitar del prompt de GeminiIntentService.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Con el asesor removido, el estado 'agente' en conversación bloqueaba
handleText y handleFreeText silenciosamente. Audio transcripto como
'hacer una recarga' no generaba respuesta por este return prematuro.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- 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>
- Eliminar botón del menú principal, pago confirmado, pago no confirmado
- callback 'agent' redirige al menú principal en vez de transferToAgent
- asesor.solicitar del intent también redirige al menú
- Limpiar textos 'contacta a un asesor' en mensajes de error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El check 'agente' devolvía silenciosamente antes de leer el step,
descartando el texto '1000' del usuario. Si el bot está esperando
input específico (await_amount, await_email, etc.) tiene prioridad
sobre el estado agente de la conversación.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si pagos_confirmados no existe o falla la BD, NO confirmar el pago (fail-closed).
Antes fallaba abierto: ignoraba el error y confirmaba igual, permitiendo
recargar con el mismo comprobante múltiples veces.
Agrega motivo error_sistema en Telegram para casos de error interno.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si pagos_confirmados no existe en producción (migración pendiente), la excepción
escapaba hasta handlePhoto y mostraba "Error analizando el comprobante".
Safety catch permite que la confirmación proceda aunque la tabla no esté.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El toggle puede no estar guardado como '1' en BD aunque el panel lo muestre activo.
Verificar correo_imap_host es más confiable: si hay host → intentar IMAP.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Con thinkingBudget=0 el modelo no consume tokens en razonamiento interno,
evitando que el JSON de respuesta quede truncado (bug: '{"banco":"Nu",...').
maxOutputTokens 2048→8192 en Vision, 1024→2048 en Intent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Livewire checkbox con value="1" puede guardar '1','on','true' según el navegador.
PagoValidadorService ahora acepta cualquier valor truthy en vez de comparación estricta.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
preg_replace('/[^0-9]/', '', '1,000.00') = '100000' porque el .00 aporta 00.
Fix: primero quitar la parte decimal (/[.,]\d{1,2}$/) antes de limpiar separadores.
Aplicado en BancolombiaParser::normalizarValor() y como doble defensa en PagoValidadorService.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PagoValidadorService: add detailed Log::info at every step (emails count,
body preview, parser result, which filter fails) so failures are visible in laravel.log
- CorreoImapService::cleanBody: detect and decode base64-encoded BODY[1] before
QP decoding — critical fix when Bancolombia sends HTML part as base64
- BancolombiaParser: make esBancolombia() broader (includes 'valor','$','abono' etc.),
add extraerPrimero() helper, add multiple regex fallbacks for remitente/fecha/hora/valor
to handle different Bancolombia email HTML formats beyond the SMS text pattern
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>