- 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>
- 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>
- 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>
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>
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>
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>
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>
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>
- Create pagos_confirmados table to track consumed email receipts
- PagoValidadorService now validates: valor + fecha + hora (±10min) + llave Bancolombia
- Each matched email is hashed (sha1 body+date) and marked as used; a second
match returns estado=ya_usado instead of confirmado
- Add partial remitente name matching against the registered user's name
- GeminiVisionService prompt now extracts llave (@xxx) from the receipt image
- TelegramBotService shows distinct message for ya_usado state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rewrote extraerPago() to try v1 then v1beta for model compatibility
- Returns __error key with actual API message when both fail
- TelegramBotService shows that error in chat instead of generic message
- Also ensures model is read fresh from config (no hardcoded URL in constructor)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Asterisks in the masked email (liza*****@domain) broke Telegram Markdown
parsing, causing the message to silently fail. Backticks (inline code)
render correctly regardless of the email content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Add language=es to Whisper query params so audio is always transcribed in Spanish regardless of accent or background noise
- Show "Procesando..." after echoing the transcript so user knows the AI is interpreting the intent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- 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>
- Send response_format=json as URL query param (not form field)
- If JSON parse yields empty text, fall back to raw plain-text body
- Prevents false 'error' logs when Whisper returns plain text
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- 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>
- 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>
apiCall() and downloadTelegramFile() were using file_get_contents() for
outgoing requests to api.telegram.org, which fails on restricted servers.
Now use Http:: (Guzzle) consistently, matching the rest of the project.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Web chat: 'Otro valor' button sets flujo=recarga.monto_custom,
next text message is parsed as the amount (min $1.000)
- Telegram: '✏️ Otro valor' button sets step=await_amount,
next message is parsed and routed to method selection
- Preset amounts in Telegram now show 2 per row for compactness
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New TelegramBotService: full OTP auth, registration, main menu with
inline keyboards, buy plans, buy promos, recharge (MP + Bre-B),
credentials, history, profile, agent transfer, photo receipt analysis
- TelegramWebhookController: now handles text, callback_query and photo
- Role-based pricing: tarifas filtered by user rol_id in both web chat
and Telegram; Preferencial price overrides base tarifa valor
- Services and promos now render as horizontal scroll slider
(cards_slider tipo_ui) instead of stacked individual cards
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>