1431 lines
55 KiB
PHP
1431 lines
55 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Mail\ChatOtpMail;
|
||
use App\Models\AiUsageLog;
|
||
use App\Models\ChatConfig;
|
||
use App\Models\ChatContact;
|
||
use App\Models\ChatConversation;
|
||
use App\Models\ChatMessage;
|
||
use App\Models\Cuentas;
|
||
use App\Models\Historiale;
|
||
use App\Models\Historial_cuenta;
|
||
use App\Models\Preferencial;
|
||
use App\Models\Promociones;
|
||
use App\Models\TarifaPromo;
|
||
use App\Models\Usuario_promo;
|
||
use App\Models\recarga;
|
||
use App\Models\Role;
|
||
use App\Models\Saldo;
|
||
use App\Models\Servicio;
|
||
use App\Models\Tarifas;
|
||
use App\Models\User;
|
||
use Carbon\Carbon;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Illuminate\Support\Facades\Hash;
|
||
use Illuminate\Support\Facades\Http;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Illuminate\Support\Facades\Mail;
|
||
use Illuminate\Support\Str;
|
||
|
||
class TelegramBotService
|
||
{
|
||
private const STATE_TTL = 315360000; // 10 years — session never expires automatically
|
||
|
||
private ?int $convId = null;
|
||
|
||
// ─── Entry points ─────────────────────────────────────────
|
||
|
||
public function handleText(string $chatId, string $text, string $nombre = ''): void
|
||
{
|
||
$text = trim($text);
|
||
$state = $this->getState($chatId);
|
||
$this->convId = $state['conv_id'] ?? null;
|
||
|
||
// Save user message if we have a conversation
|
||
if ($this->convId && $text !== '') {
|
||
$conv = ChatConversation::find($this->convId);
|
||
if ($conv) {
|
||
ChatMessage::create([
|
||
'conversation_id' => $this->convId,
|
||
'tipo' => 'usuario',
|
||
'contenido' => $text,
|
||
'leido' => false,
|
||
]);
|
||
$conv->update(['ultimo_mensaje_at' => now()]);
|
||
|
||
if ($conv->estado === 'agente') {
|
||
return; // Human agent is handling this
|
||
}
|
||
}
|
||
}
|
||
|
||
$lower = strtolower(trim($text));
|
||
|
||
// "salir" siempre pide confirmación de cierre de sesión
|
||
if (in_array($lower, ['salir', '/salir'])) {
|
||
if ($state['user_id'] ?? null) {
|
||
$this->showLogoutConfirmation($chatId);
|
||
} else {
|
||
$this->askEmail($chatId);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Palabras que siempre regresan al menú, desde cualquier punto del flujo
|
||
$menuTriggers = [
|
||
'/start', '/menu', '/inicio', '/cancelar',
|
||
'menu', 'menú', 'inicio', 'hola', 'volver',
|
||
'regresar', 'cancelar', 'empezar', 'comenzar', 'start',
|
||
];
|
||
|
||
if (in_array($lower, $menuTriggers)) {
|
||
if ($state['user_id'] ?? null) {
|
||
// Limpiar cualquier flujo pendiente
|
||
$cleanState = $this->getState($chatId);
|
||
unset($cleanState['step']);
|
||
$cleanState['step'] = 'idle';
|
||
$this->setState($chatId, $cleanState);
|
||
$this->showMainMenu($chatId);
|
||
} else {
|
||
$this->askEmail($chatId);
|
||
}
|
||
return;
|
||
}
|
||
|
||
$step = $state['step'] ?? 'await_email';
|
||
|
||
// Guardar último texto para que handleFreeText pueda pasarlo a Gemini
|
||
if (! in_array($step, ['await_email', 'await_otp', 'await_nombre', 'await_celular', 'await_amount'])) {
|
||
$state['data']['last_text'] = $text;
|
||
$this->setState($chatId, $state);
|
||
}
|
||
|
||
match ($step) {
|
||
'await_email' => $this->processEmail($chatId, $text, $nombre),
|
||
'await_otp' => $this->processOtp($chatId, $text),
|
||
'await_nombre' => $this->processNombre($chatId, $text),
|
||
'await_celular' => $this->processCelular($chatId, $text),
|
||
'await_amount' => $this->processCustomAmount($chatId, $text),
|
||
default => $this->handleFreeText($chatId, $state),
|
||
};
|
||
}
|
||
|
||
public function handleCallback(string $chatId, string $callbackId, string $data): void
|
||
{
|
||
$this->answerCallback($callbackId);
|
||
|
||
$state = $this->getState($chatId);
|
||
$this->convId = $state['conv_id'] ?? null;
|
||
|
||
if (! ($state['user_id'] ?? null)) {
|
||
$this->askEmail($chatId);
|
||
return;
|
||
}
|
||
|
||
$parts = explode('|', $data, 3);
|
||
$action = $parts[0];
|
||
|
||
match ($action) {
|
||
'menu' => $this->showMainMenu($chatId),
|
||
'logout_ask' => $this->showLogoutConfirmation($chatId),
|
||
'logout_confirm' => $this->confirmLogout($chatId),
|
||
'svc_list' => $this->listServices($chatId),
|
||
'svc_view' => $this->viewService($chatId, (int) ($parts[1] ?? 0)),
|
||
'buy_start' => $this->startPurchase($chatId, (int) ($parts[1] ?? 0)),
|
||
'buy_saldo' => $this->payWithSaldo($chatId),
|
||
'buy_mp' => $this->payWithMP($chatId),
|
||
'promo_list' => $this->listPromos($chatId),
|
||
'promo_view' => $this->viewPromo($chatId, (int) ($parts[1] ?? 0)),
|
||
'promo_saldo' => $this->buyPromoSaldo($chatId, (int) ($parts[1] ?? 0)),
|
||
'promo_mp' => $this->buyPromoMP($chatId, (int) ($parts[1] ?? 0)),
|
||
'rec_init' => $this->showRechargeAmounts($chatId),
|
||
'rec_custom' => $this->askCustomAmount($chatId),
|
||
'rec_amount' => $this->chooseRechargeMethod($chatId, (int) ($parts[1] ?? 0)),
|
||
'rec_mp' => $this->rechargeMP($chatId, (int) ($parts[1] ?? 0)),
|
||
'rec_breb' => $this->rechargeBreb($chatId, (int) ($parts[1] ?? 0)),
|
||
'creds' => $this->showCredentials($chatId),
|
||
'history' => $this->showHistory($chatId),
|
||
'profile' => $this->showProfile($chatId),
|
||
'agent' => $this->transferToAgent($chatId),
|
||
'pay_apply' => $this->applyPayment($chatId, (int) ($parts[1] ?? 0), $parts[2] ?? null),
|
||
default => $this->showMainMenu($chatId),
|
||
};
|
||
}
|
||
|
||
public function handlePhoto(string $chatId, string $fileId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$this->convId = $state['conv_id'] ?? null;
|
||
|
||
if (! ($state['user_id'] ?? null)) {
|
||
$this->send($chatId, "Para enviar un comprobante primero debes identificarte. Escribe tu correo.");
|
||
return;
|
||
}
|
||
|
||
if (ChatConfig::get('validacion_foto_habilitada', '0') !== '1') {
|
||
$this->send($chatId, "El análisis de comprobantes no está habilitado. Contacta a un asesor.");
|
||
return;
|
||
}
|
||
|
||
$this->send($chatId, "⏳ Analizando tu comprobante...");
|
||
|
||
try {
|
||
$imageData = $this->downloadTelegramFile($fileId);
|
||
if (! $imageData) {
|
||
$this->send($chatId, "No pude descargar la imagen. Intenta de nuevo.");
|
||
return;
|
||
}
|
||
|
||
$base64 = base64_encode($imageData['content']);
|
||
$mime = $imageData['mime'];
|
||
$datosPago = app(GeminiVisionService::class)->extraerPago($base64, $mime, $state['user_id'] ?? null, 'telegram');
|
||
|
||
if (! $datosPago) {
|
||
$this->send($chatId, "No pude leer el comprobante. Intenta con una foto más clara.");
|
||
return;
|
||
}
|
||
|
||
$resultado = app(PagoValidadorService::class)->validar($datosPago);
|
||
$monto = number_format($datosPago['valor'] ?? 0);
|
||
$ref = $datosPago['referencia'] ?? 'N/A';
|
||
|
||
$texto = "📊 *Resultado del análisis:*\n\n";
|
||
$texto .= "Monto: *\$$monto*\n";
|
||
$texto .= "Referencia: `{$ref}`\n";
|
||
$texto .= "Estado: *" . ($resultado['estado'] === 'confirmado' ? '✅ Confirmado' : '⚠️ No confirmado') . "*";
|
||
|
||
if ($resultado['estado'] === 'confirmado') {
|
||
$buttons = [
|
||
[['text' => "✅ Aplicar recarga de \$$monto", 'callback_data' => 'pay_apply|' . ($datosPago['valor'] ?? 0) . '|' . $ref]],
|
||
[['text' => '🔙 Menú principal', 'callback_data' => 'menu']],
|
||
];
|
||
} else {
|
||
$buttons = [
|
||
[['text' => '🧑 Hablar con asesor', 'callback_data' => 'agent']],
|
||
[['text' => '🔙 Menú principal', 'callback_data' => 'menu']],
|
||
];
|
||
}
|
||
|
||
$this->sendWithKeyboard($chatId, $texto, $buttons);
|
||
} catch (\Throwable $e) {
|
||
Log::warning('[TelegramBot] Error procesando foto: ' . $e->getMessage());
|
||
$this->send($chatId, "Error analizando el comprobante. Por favor intenta de nuevo.");
|
||
}
|
||
}
|
||
|
||
public function handleVoice(string $chatId, string $fileId, string $nombre = '', int $duracionSegundos = 0): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$this->convId = $state['conv_id'] ?? null;
|
||
$usuarioId = $state['user_id'] ?? null;
|
||
|
||
if (ChatConfig::get('whisper_habilitado', '0') !== '1') {
|
||
$this->send($chatId, "🎤 Recibí tu audio, pero la transcripción de voz no está habilitada. Por favor escribe tu mensaje.");
|
||
return;
|
||
}
|
||
|
||
$this->send($chatId, "🎤 Transcribiendo tu audio...");
|
||
|
||
try {
|
||
$audioData = $this->downloadTelegramFile($fileId);
|
||
if (! $audioData) {
|
||
$this->send($chatId, "No pude descargar el audio. Intenta de nuevo o escribe tu mensaje.");
|
||
return;
|
||
}
|
||
|
||
$resultado = $this->transcribeAudio($audioData['content'], $duracionSegundos, $usuarioId);
|
||
|
||
if (! $resultado['texto']) {
|
||
$this->send($chatId, "No pude entender el audio. Intenta de nuevo o escribe tu mensaje.");
|
||
return;
|
||
}
|
||
|
||
$this->send($chatId, "🎙️ Escuché: _\"{$resultado['texto']}\"_\nProcesando...");
|
||
$this->handleText($chatId, $resultado['texto'], $nombre);
|
||
|
||
} catch (\Throwable $e) {
|
||
Log::warning('[TelegramBot] handleVoice error: ' . $e->getMessage());
|
||
$this->send($chatId, "Error procesando el audio. Escribe tu mensaje.");
|
||
}
|
||
}
|
||
|
||
private function transcribeAudio(string $audioContent, int $duracionSegundos = 0, ?int $usuarioId = null): array
|
||
{
|
||
$whisperUrl = ChatConfig::get('whisper_url');
|
||
$whisperToken = ChatConfig::get('whisper_token');
|
||
$inicio = microtime(true);
|
||
|
||
if (! $whisperUrl) {
|
||
return ['texto' => null];
|
||
}
|
||
|
||
try {
|
||
// response_format y language como query params
|
||
$sep = str_contains($whisperUrl, '?') ? '&' : '?';
|
||
$url = rtrim($whisperUrl, '?') . $sep . 'response_format=json&language=es';
|
||
|
||
$response = Http::timeout(60)
|
||
->withBasicAuth('whisper', $whisperToken)
|
||
->attach('audio_file', $audioContent, 'audio.ogg')
|
||
->post($url);
|
||
|
||
$tiempoMs = (int) ((microtime(true) - $inicio) * 1000);
|
||
|
||
// Intentar JSON primero, luego texto plano como fallback
|
||
$json = $response->json();
|
||
$texto = trim($json['text'] ?? $json['transcription'] ?? '');
|
||
|
||
if ($texto === '' && $response->successful()) {
|
||
// El servidor devolvió texto plano
|
||
$raw = trim($response->body());
|
||
if ($raw !== '' && ! str_starts_with($raw, '{') && ! str_starts_with($raw, '[')) {
|
||
$texto = $raw;
|
||
}
|
||
}
|
||
|
||
$ok = $response->successful() && $texto !== '';
|
||
|
||
AiUsageLog::registrar([
|
||
'servicio' => 'whisper',
|
||
'canal' => 'telegram',
|
||
'usuario_id' => $usuarioId,
|
||
'audio_segundos' => $duracionSegundos ?: null,
|
||
'tiempo_ms' => $tiempoMs,
|
||
'costo' => $duracionSegundos, // $1 por segundo
|
||
'resultado' => $ok ? 'ok' : 'error',
|
||
'detalle' => [
|
||
'texto' => $ok ? substr($texto, 0, 200) : null,
|
||
'error' => $ok ? null : substr($response->body(), 0, 300),
|
||
'segundos' => $duracionSegundos,
|
||
],
|
||
]);
|
||
|
||
if (! $ok) {
|
||
Log::warning('[TelegramBot] Whisper error: ' . $response->body());
|
||
return ['texto' => null];
|
||
}
|
||
|
||
return ['texto' => $texto];
|
||
|
||
} catch (\Throwable $e) {
|
||
$tiempoMs = (int) ((microtime(true) - $inicio) * 1000);
|
||
Log::warning('[TelegramBot] Whisper exception: ' . $e->getMessage());
|
||
AiUsageLog::registrar([
|
||
'servicio' => 'whisper',
|
||
'canal' => 'telegram',
|
||
'usuario_id' => $usuarioId,
|
||
'audio_segundos' => $duracionSegundos ?: null,
|
||
'tiempo_ms' => $tiempoMs,
|
||
'resultado' => 'error',
|
||
'detalle' => ['error' => $e->getMessage()],
|
||
]);
|
||
return ['texto' => null];
|
||
}
|
||
}
|
||
|
||
// ─── Auth flow ────────────────────────────────────────────
|
||
|
||
private function askEmail(string $chatId): void
|
||
{
|
||
$this->setState($chatId, ['step' => 'await_email', 'data' => []]);
|
||
$this->send($chatId, "👋 Hola! Para continuar escribe tu correo electrónico registrado:");
|
||
}
|
||
|
||
private function processEmail(string $chatId, string $email, string $nombre): void
|
||
{
|
||
$email = strtolower(trim($email));
|
||
|
||
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||
$this->send($chatId, "❌ Eso no parece un correo válido. Escríbelo de nuevo:");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
$user = User::whereRaw('LOWER(email) = ?', [$email])->first();
|
||
} catch (\Throwable $e) {
|
||
Log::error('[TelegramBot] processEmail DB error: ' . $e->getMessage());
|
||
$this->send($chatId, "⚠️ Error de conexión. Escribe tu correo de nuevo:");
|
||
return;
|
||
}
|
||
|
||
if (! $user) {
|
||
$this->setState($chatId, [
|
||
'step' => 'await_nombre',
|
||
'data' => ['email' => $email, 'nombre_tg' => $nombre],
|
||
]);
|
||
$this->send($chatId,
|
||
"No encontré una cuenta con ese correo.\n\n" .
|
||
"¿Deseas crear una cuenta? Escribe tu *nombre completo* o escribe /cancelar para intentar con otro correo."
|
||
);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
$contact = ChatContact::where('canal', 'telegram')->where('canal_id', (string) $chatId)->first();
|
||
if ($contact) {
|
||
$contact->update(['user_id' => $user->id, 'nombre' => $user->name]);
|
||
} else {
|
||
$contact = ChatContact::create([
|
||
'canal' => 'telegram',
|
||
'canal_id' => (string) $chatId,
|
||
'nombre' => $user->name,
|
||
'user_id' => $user->id,
|
||
]);
|
||
}
|
||
} catch (\Throwable $e) {
|
||
Log::error('[TelegramBot] processEmail contact error: ' . $e->getMessage());
|
||
$this->send($chatId, "⚠️ Error interno. Escribe tu correo de nuevo:");
|
||
return;
|
||
}
|
||
|
||
$this->sendOtp($chatId, $contact, $user);
|
||
}
|
||
|
||
private function sendOtp(string $chatId, ChatContact $contact, User $user): void
|
||
{
|
||
$codigo = (string) random_int(100000, 999999);
|
||
Cache::put("chat_otp_{$contact->id}", $codigo, now()->addMinutes(10));
|
||
|
||
$mailError = null;
|
||
try {
|
||
Mail::to($user->email)->send(new ChatOtpMail($codigo, $user->name));
|
||
} catch (\Throwable $e) {
|
||
$mailError = $e->getMessage();
|
||
Log::warning('[TelegramOTP] Error enviando correo: ' . $mailError);
|
||
}
|
||
|
||
$this->setState($chatId, [
|
||
'step' => 'await_otp',
|
||
'contact_id' => $contact->id,
|
||
'data' => [],
|
||
]);
|
||
|
||
$masked = $this->maskEmail($user->email);
|
||
|
||
if (! $mailError) {
|
||
$this->send($chatId, "✉️ Te enviamos un código a `{$masked}`\n\nEscribe el código de 6 dígitos:");
|
||
} else {
|
||
$this->send($chatId,
|
||
"⚠️ Error enviando correo a `{$masked}`\n\n`{$mailError}`\n\nContacta al administrador."
|
||
);
|
||
}
|
||
}
|
||
|
||
private function processOtp(string $chatId, string $text): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$contactId = $state['contact_id'] ?? null;
|
||
|
||
if (! $contactId || ! preg_match('/^\d{6}$/', $text)) {
|
||
$this->send($chatId, "El código debe tener 6 dígitos. Intenta de nuevo:");
|
||
return;
|
||
}
|
||
|
||
$saved = Cache::get("chat_otp_{$contactId}");
|
||
|
||
if (! $saved) {
|
||
$this->send($chatId, "El código expiró. Escribe tu correo de nuevo:");
|
||
$this->setState($chatId, ['step' => 'await_email', 'data' => []]);
|
||
return;
|
||
}
|
||
|
||
if ($text !== $saved) {
|
||
$this->send($chatId, "❌ Código incorrecto. Intenta de nuevo:");
|
||
return;
|
||
}
|
||
|
||
Cache::forget("chat_otp_{$contactId}");
|
||
|
||
$contact = ChatContact::find($contactId);
|
||
$user = User::with('saldo')->find($contact->user_id);
|
||
$this->openSession($chatId, $contact, $user);
|
||
}
|
||
|
||
// ─── Registration flow ────────────────────────────────────
|
||
|
||
private function processNombre(string $chatId, string $text): void
|
||
{
|
||
if (strtolower($text) === '/cancelar') {
|
||
$this->setState($chatId, ['step' => 'await_email', 'data' => []]);
|
||
$this->send($chatId, "De acuerdo. Escribe tu correo electrónico:");
|
||
return;
|
||
}
|
||
|
||
$state = $this->getState($chatId);
|
||
$state['data']['nombre'] = $text;
|
||
$state['step'] = 'await_celular';
|
||
$this->setState($chatId, $state);
|
||
$this->send($chatId, "Tu número de celular (o escribe /omitir para saltarlo):");
|
||
}
|
||
|
||
private function processCelular(string $chatId, string $text): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$state['data']['celular'] = strtolower($text) === '/omitir' ? null : $text;
|
||
|
||
$email = $state['data']['email'] ?? null;
|
||
$nombre = $state['data']['nombre'] ?? null;
|
||
|
||
if (! $email || ! $nombre) {
|
||
$this->send($chatId, "Ocurrió un error. Escribe /start para comenzar de nuevo.");
|
||
$this->setState($chatId, ['step' => 'await_email', 'data' => []]);
|
||
return;
|
||
}
|
||
|
||
$rolCliente = Role::where('nombre', 'cliente')->first();
|
||
$user = User::create([
|
||
'name' => $nombre,
|
||
'email' => $email,
|
||
'password' => Hash::make(Str::random(16)),
|
||
'cedula' => $state['data']['cedula'] ?? null,
|
||
'celular' => $state['data']['celular'] ?? null,
|
||
'estado' => 'activo',
|
||
'rol_id' => $rolCliente?->id,
|
||
]);
|
||
|
||
$contact = ChatContact::where('canal', 'telegram')->where('canal_id', $chatId)->first();
|
||
if ($contact) {
|
||
$contact->update(['user_id' => $user->id, 'nombre' => $user->name]);
|
||
} else {
|
||
$contact = ChatContact::create([
|
||
'canal' => 'telegram',
|
||
'canal_id' => $chatId,
|
||
'nombre' => $user->name,
|
||
'user_id' => $user->id,
|
||
]);
|
||
}
|
||
|
||
$this->sendOtp($chatId, $contact, $user);
|
||
}
|
||
|
||
// ─── Open session ─────────────────────────────────────────
|
||
|
||
private function openSession(string $chatId, ChatContact $contact, User $user): void
|
||
{
|
||
$conv = $contact->activeConversation();
|
||
if (! $conv) {
|
||
$conv = ChatConversation::create([
|
||
'contact_id' => $contact->id,
|
||
'canal' => 'telegram',
|
||
'estado' => 'bot',
|
||
'ultimo_mensaje_at' => now(),
|
||
]);
|
||
}
|
||
|
||
$this->convId = $conv->id;
|
||
|
||
$this->setState($chatId, [
|
||
'step' => 'menu',
|
||
'user_id' => $user->id,
|
||
'contact_id' => $contact->id,
|
||
'conv_id' => $conv->id,
|
||
'data' => [],
|
||
]);
|
||
|
||
$saludo = "¡Bienvenido, *{$user->name}*! 🎉\nSaldo disponible: *\$" . number_format($user->saldo?->valor ?? 0) . "*";
|
||
$this->send($chatId, $saludo);
|
||
$this->showMainMenu($chatId);
|
||
}
|
||
|
||
// ─── Main menu ────────────────────────────────────────────
|
||
|
||
private function showLogoutConfirmation(string $chatId): void
|
||
{
|
||
$this->sendWithKeyboard(
|
||
$chatId,
|
||
"⚠️ ¿Deseas cerrar sesión?\nDeberás volver a ingresar tu correo y código para continuar.",
|
||
[
|
||
[
|
||
['text' => '✅ Sí, cerrar sesión', 'callback_data' => 'logout_confirm'],
|
||
['text' => '❌ No, quedarme', 'callback_data' => 'menu'],
|
||
],
|
||
]
|
||
);
|
||
}
|
||
|
||
private function confirmLogout(string $chatId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
unset($state['user_id'], $state['conv_id'], $state['step'], $state['data']);
|
||
$state['step'] = 'await_email';
|
||
$this->setState($chatId, $state);
|
||
$this->send($chatId, "👋 Sesión cerrada. Hasta pronto.");
|
||
$this->askEmail($chatId);
|
||
}
|
||
|
||
private function showMainMenu(string $chatId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$state['step'] = 'menu';
|
||
$state['data'] = [];
|
||
$this->setState($chatId, $state);
|
||
|
||
$this->sendWithKeyboard($chatId, "¿En qué te puedo ayudar?", [
|
||
[
|
||
['text' => '📺 Ver Servicios', 'callback_data' => 'svc_list'],
|
||
['text' => '🎁 Promociones', 'callback_data' => 'promo_list'],
|
||
],
|
||
[
|
||
['text' => '💰 Recargar Saldo', 'callback_data' => 'rec_init'],
|
||
['text' => '🔑 Mis Credenciales', 'callback_data' => 'creds'],
|
||
],
|
||
[
|
||
['text' => '📋 Historial', 'callback_data' => 'history'],
|
||
['text' => '👤 Mi Perfil', 'callback_data' => 'profile'],
|
||
],
|
||
[
|
||
['text' => '🧑 Hablar con asesor', 'callback_data' => 'agent'],
|
||
],
|
||
[
|
||
['text' => '🚪 Cerrar sesión', 'callback_data' => 'logout_ask'],
|
||
],
|
||
]);
|
||
}
|
||
|
||
// ─── Services ─────────────────────────────────────────────
|
||
|
||
private function listServices(string $chatId): void
|
||
{
|
||
$servicios = Servicio::where('estado', 'activo')->orderBy('ubicacion')->get();
|
||
|
||
if ($servicios->isEmpty()) {
|
||
$this->send($chatId, "No hay servicios disponibles en este momento.");
|
||
$this->showMainMenu($chatId);
|
||
return;
|
||
}
|
||
|
||
$buttons = [];
|
||
foreach ($servicios as $s) {
|
||
$buttons[] = [['text' => $s->nombre, 'callback_data' => 'svc_view|' . $s->id]];
|
||
}
|
||
$buttons[] = [['text' => '🔙 Menú principal', 'callback_data' => 'menu']];
|
||
|
||
$this->sendWithKeyboard($chatId, "📺 *Selecciona el servicio:*", $buttons);
|
||
}
|
||
|
||
private function viewService(string $chatId, int $servicioId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$user = User::find($state['user_id']);
|
||
$rolId = $user?->rol_id ?? Role::where('nombre', 'cliente')->value('id');
|
||
$servicio = Servicio::with(['tarifas' => fn ($q) => $q->where('estado', 'activo')->where('rol_id', $rolId)])->find($servicioId);
|
||
|
||
if (! $servicio) {
|
||
$this->send($chatId, "Servicio no encontrado.");
|
||
return;
|
||
}
|
||
|
||
$tarifas = $servicio->tarifas;
|
||
|
||
if ($tarifas->isEmpty()) {
|
||
$this->send($chatId, "No hay planes disponibles para {$servicio->nombre}.");
|
||
$this->listServices($chatId);
|
||
return;
|
||
}
|
||
|
||
$buttons = [];
|
||
foreach ($tarifas as $t) {
|
||
$precio = $this->efectivePrice($t->id, $state['user_id'], $t->valor);
|
||
$label = $servicio->por_tiempo
|
||
? "{$t->dias} días - \$" . number_format($precio)
|
||
: "{$t->pantallas} pantalla(s) / {$t->dias} días - \$" . number_format($precio);
|
||
$buttons[] = [['text' => $label, 'callback_data' => 'buy_start|' . $t->id]];
|
||
}
|
||
$buttons[] = [['text' => '🔙 Ver servicios', 'callback_data' => 'svc_list']];
|
||
|
||
$this->sendWithKeyboard($chatId, "📺 *{$servicio->nombre}* — elige un plan:", $buttons);
|
||
}
|
||
|
||
// ─── Purchase ─────────────────────────────────────────────
|
||
|
||
private function startPurchase(string $chatId, int $tarifaId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$tarifa = Tarifas::with('servicio')->find($tarifaId);
|
||
|
||
if (! $tarifa) {
|
||
$this->send($chatId, "Plan no encontrado.");
|
||
return;
|
||
}
|
||
|
||
$disponibles = Cuentas::where('tarifa_id', $tarifaId)->where('estado', 'activo')->count();
|
||
if ($disponibles < 1) {
|
||
$this->send($chatId, "Sin cuentas disponibles para este plan. Intenta con otro.");
|
||
$this->listServices($chatId);
|
||
return;
|
||
}
|
||
|
||
$user = User::with('saldo')->find($state['user_id']);
|
||
$saldo = $user->saldo?->valor ?? 0;
|
||
$valor = $this->efectivePrice($tarifaId, $state['user_id'], $tarifa->valor);
|
||
|
||
$state['data'] = ['flow' => 'compra', 'tarifa_id' => $tarifaId, 'valor' => $valor, 'servicio' => $tarifa->servicio->nombre, 'servicio_id' => $tarifa->servicio_id];
|
||
$this->setState($chatId, $state);
|
||
|
||
$detalle = $tarifa->servicio->por_tiempo
|
||
? "{$tarifa->dias} días"
|
||
: "{$tarifa->pantallas} pantalla(s) / {$tarifa->dias} días";
|
||
|
||
$buttons = [];
|
||
if ($saldo >= $valor) {
|
||
$buttons[] = [['text' => "💳 Pagar con saldo (\$" . number_format($valor) . ")", 'callback_data' => 'buy_saldo']];
|
||
}
|
||
$buttons[] = [['text' => '🏦 MercadoPago', 'callback_data' => 'buy_mp']];
|
||
$buttons[] = [['text' => '🔙 Ver planes', 'callback_data' => 'svc_view|' . $tarifa->servicio_id]];
|
||
|
||
$this->sendWithKeyboard(
|
||
$chatId,
|
||
"*{$tarifa->servicio->nombre}* — {$detalle}\nPrecio: *\$" . number_format($valor) . "*\n\nElige cómo pagar:",
|
||
$buttons
|
||
);
|
||
}
|
||
|
||
private function payWithSaldo(string $chatId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$data = $state['data'] ?? [];
|
||
|
||
if (($data['flow'] ?? '') !== 'compra') {
|
||
$this->showMainMenu($chatId);
|
||
return;
|
||
}
|
||
|
||
$tarifa = Tarifas::with('servicio')->find($data['tarifa_id'] ?? 0);
|
||
$user = User::with('saldo')->find($state['user_id']);
|
||
|
||
if (! $tarifa || ! $user) {
|
||
$this->send($chatId, "Ocurrió un error. Intenta de nuevo.");
|
||
return;
|
||
}
|
||
|
||
$saldo = $user->saldo;
|
||
if (! $saldo || $saldo->valor < $tarifa->valor) {
|
||
$this->send($chatId, "Saldo insuficiente (\$" . number_format($saldo?->valor ?? 0) . "). El plan cuesta \$" . number_format($tarifa->valor) . ".");
|
||
return;
|
||
}
|
||
|
||
$cuenta = Cuentas::where('tarifa_id', $tarifa->id)->where('estado', 'activo')->first();
|
||
if (! $cuenta) {
|
||
$this->send($chatId, "No hay cuentas disponibles en este momento. Intenta más tarde.");
|
||
return;
|
||
}
|
||
|
||
$historial = Historiale::create([
|
||
'fecha_inicio' => now(),
|
||
'fecha_final' => now()->addDays($tarifa->dias),
|
||
'valor' => $tarifa->valor,
|
||
'utilidad' => $tarifa->utilidad ?? 0,
|
||
'tipo_pago' => 'saldo',
|
||
'estado' => 'entregado',
|
||
'vendedor_id' => $state['user_id'],
|
||
'tarifa_id' => $tarifa->id,
|
||
'cliente_id' => $state['user_id'],
|
||
'nombre_cliente' => $user->name,
|
||
]);
|
||
|
||
Historial_cuenta::create(['historial_id' => $historial->id, 'cuenta_id' => $cuenta->id]);
|
||
$cuenta->update(['estado' => 'ocupado']);
|
||
$nuevoSaldo = $saldo->valor - $tarifa->valor;
|
||
$saldo->update(['valor' => $nuevoSaldo]);
|
||
|
||
$text = "✅ *¡Compra exitosa!*\n\n";
|
||
$text .= "🎬 *{$tarifa->servicio->nombre}*\n";
|
||
$text .= "📧 Email: `{$cuenta->correo}`\n";
|
||
$text .= "🔑 Password: `{$cuenta->password}`\n";
|
||
if ($tarifa->pantallas) {
|
||
$text .= "📺 Perfil: {$tarifa->pantallas}\n";
|
||
}
|
||
$text .= "📅 Vence: " . now()->addDays($tarifa->dias)->format('d/m/Y') . "\n";
|
||
$text .= "\nSaldo restante: *\$" . number_format($nuevoSaldo) . "*";
|
||
|
||
$this->send($chatId, $text);
|
||
|
||
$state['data'] = [];
|
||
$this->setState($chatId, $state);
|
||
$this->showMainMenu($chatId);
|
||
}
|
||
|
||
private function payWithMP(string $chatId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$data = $state['data'] ?? [];
|
||
|
||
if (($data['flow'] ?? '') !== 'compra') {
|
||
$this->showMainMenu($chatId);
|
||
return;
|
||
}
|
||
|
||
$valor = (int) ($data['valor'] ?? 0);
|
||
$servicio = $data['servicio'] ?? 'Plan streaming';
|
||
$ref = 'CHAT-' . strtoupper(Str::random(12));
|
||
$url = $this->createMPPreference($servicio, $valor, $ref);
|
||
|
||
if (! $url) {
|
||
$this->send($chatId, "No se pudo generar el link de pago. Intenta de nuevo o contacta a un asesor.");
|
||
return;
|
||
}
|
||
|
||
$this->sendWithKeyboard(
|
||
$chatId,
|
||
"🏦 *Pago con MercadoPago*\n\n{$servicio} — \$" . number_format($valor) . "\nRef: `{$ref}`\n\n[👉 Pagar ahora]({$url})\n\nDespués de pagar, envía la foto del comprobante aquí.",
|
||
[[['text' => '🔙 Menú principal', 'callback_data' => 'menu']]]
|
||
);
|
||
|
||
$state['data'] = [];
|
||
$this->setState($chatId, $state);
|
||
}
|
||
|
||
// ─── Promos ───────────────────────────────────────────────
|
||
|
||
private function listPromos(string $chatId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$rolId = isset($state['user_id'])
|
||
? (User::find($state['user_id'])?->rol_id ?? Role::where('nombre', 'cliente')->value('id'))
|
||
: Role::where('nombre', 'cliente')->value('id');
|
||
|
||
$promos = Promociones::where('visible', 'true')
|
||
->where(fn ($q) => $q->whereNull('fecha_limite')->orWhere('fecha_limite', '>=', now()))
|
||
->whereHas('tarifaPromo', fn ($q) => $q->where('rol_id', $rolId))
|
||
->with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])
|
||
->get();
|
||
|
||
if ($promos->isEmpty()) {
|
||
$this->send($chatId, "No hay promociones activas en este momento.");
|
||
$this->showMainMenu($chatId);
|
||
return;
|
||
}
|
||
|
||
$text = "🎁 *Promociones disponibles:*\n\n";
|
||
$buttons = [];
|
||
|
||
foreach ($promos as $p) {
|
||
$upric = isset($state['user_id'])
|
||
? Usuario_promo::where('usuario_id', $state['user_id'])->where('promocion_id', $p->id)->first()
|
||
: null;
|
||
$tp = $p->tarifaPromo->first();
|
||
$precio = $upric ? $upric->precio : ($tp ? $tp->precio : $p->precio);
|
||
|
||
$text .= "*{$p->nombre}* — \$" . number_format($precio) . "\n";
|
||
if ($p->descripcion) {
|
||
$text .= "{$p->descripcion}\n";
|
||
}
|
||
if ($p->fecha_limite) {
|
||
$text .= "Hasta: " . Carbon::parse($p->fecha_limite)->format('d/m/Y') . "\n";
|
||
}
|
||
$text .= "\n";
|
||
$buttons[] = [['text' => ($p->nombre ?? 'Promo') . ' — $' . number_format($precio), 'callback_data' => 'promo_view|' . $p->id]];
|
||
}
|
||
$buttons[] = [['text' => '🔙 Menú principal', 'callback_data' => 'menu']];
|
||
|
||
$this->sendWithKeyboard($chatId, $text, $buttons);
|
||
}
|
||
|
||
private function viewPromo(string $chatId, int $promoId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$rolId = isset($state['user_id'])
|
||
? (User::find($state['user_id'])?->rol_id ?? Role::where('nombre', 'cliente')->value('id'))
|
||
: Role::where('nombre', 'cliente')->value('id');
|
||
|
||
$promo = Promociones::with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])->find($promoId);
|
||
|
||
if (! $promo) {
|
||
$this->send($chatId, "Promoción no encontrada.");
|
||
return;
|
||
}
|
||
|
||
$user = isset($state['user_id']) ? User::with('saldo')->find($state['user_id']) : null;
|
||
$saldo = $user?->saldo?->valor ?? 0;
|
||
|
||
$upric = isset($state['user_id'])
|
||
? Usuario_promo::where('usuario_id', $state['user_id'])->where('promocion_id', $promoId)->first()
|
||
: null;
|
||
$tp = $promo->tarifaPromo->first();
|
||
$precio = $upric ? $upric->precio : ($tp ? $tp->precio : $promo->precio);
|
||
|
||
$buttons = [];
|
||
if ($saldo >= $precio) {
|
||
$buttons[] = [['text' => "💳 Pagar con saldo (\$" . number_format($precio) . ")", 'callback_data' => 'promo_saldo|' . $promoId]];
|
||
}
|
||
$buttons[] = [['text' => '🏦 MercadoPago', 'callback_data' => 'promo_mp|' . $promoId]];
|
||
$buttons[] = [['text' => '🔙 Promociones', 'callback_data' => 'promo_list']];
|
||
|
||
$this->sendWithKeyboard(
|
||
$chatId,
|
||
"*{$promo->nombre}*\n\$" . number_format($precio) . "\n\nElige cómo pagar:",
|
||
$buttons
|
||
);
|
||
}
|
||
|
||
private function buyPromoSaldo(string $chatId, int $promoId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$user = isset($state['user_id']) ? User::with('saldo')->find($state['user_id']) : null;
|
||
|
||
if (! $user) {
|
||
$this->send($chatId, "No se pudo procesar la compra.");
|
||
return;
|
||
}
|
||
|
||
$rolId = $user->rol_id;
|
||
$promo = Promociones::with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])->find($promoId);
|
||
|
||
if (! $promo) {
|
||
$this->send($chatId, "No se pudo procesar la compra.");
|
||
return;
|
||
}
|
||
|
||
$upric = Usuario_promo::where('usuario_id', $user->id)->where('promocion_id', $promoId)->first();
|
||
$tp = $promo->tarifaPromo->first();
|
||
$precio = $upric ? $upric->precio : ($tp ? $tp->precio : $promo->precio);
|
||
$utilidad = $upric ? $upric->utilidad : ($tp ? $tp->utilidad : 0);
|
||
|
||
$saldo = $user->saldo;
|
||
if (! $saldo || $saldo->valor < $precio) {
|
||
$this->send($chatId, "Saldo insuficiente (\$" . number_format($saldo?->valor ?? 0) . "). La promo cuesta \$" . number_format($precio) . ".");
|
||
return;
|
||
}
|
||
|
||
$cuenta = Cuentas::where('promocion_id', $promoId)->where('estado', 'activo')->first();
|
||
if (! $cuenta) {
|
||
$this->send($chatId, "No hay cuentas disponibles para esta promoción.");
|
||
return;
|
||
}
|
||
|
||
$historial = Historiale::create([
|
||
'fecha_inicio' => now(),
|
||
'fecha_final' => now()->addDays(30),
|
||
'valor' => $precio,
|
||
'utilidad' => $utilidad,
|
||
'tipo_pago' => 'saldo',
|
||
'estado' => 'entregado',
|
||
'vendedor_id' => $user->id,
|
||
'promocion_id' => $promoId,
|
||
'cliente_id' => $user->id,
|
||
'nombre_cliente' => $user->name,
|
||
]);
|
||
|
||
Historial_cuenta::create(['historial_id' => $historial->id, 'cuenta_id' => $cuenta->id]);
|
||
$cuenta->update(['estado' => 'ocupado']);
|
||
$nuevoSaldo = $saldo->valor - $precio;
|
||
$saldo->update(['valor' => $nuevoSaldo]);
|
||
|
||
$text = "✅ *¡Promo activada!*\n\n";
|
||
$text .= "🎬 *{$promo->nombre}*\n";
|
||
$text .= "📧 Email: `{$cuenta->correo}`\n";
|
||
$text .= "🔑 Password: `{$cuenta->password}`\n";
|
||
$text .= "📅 Vence: " . now()->addDays(30)->format('d/m/Y') . "\n";
|
||
$text .= "\nSaldo restante: *\$" . number_format($nuevoSaldo) . "*";
|
||
|
||
$this->send($chatId, $text);
|
||
$this->showMainMenu($chatId);
|
||
}
|
||
|
||
private function buyPromoMP(string $chatId, int $promoId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$rolId = isset($state['user_id'])
|
||
? (User::find($state['user_id'])?->rol_id ?? Role::where('nombre', 'cliente')->value('id'))
|
||
: Role::where('nombre', 'cliente')->value('id');
|
||
|
||
$promo = Promociones::with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])->find($promoId);
|
||
if (! $promo) {
|
||
return;
|
||
}
|
||
|
||
$upric = isset($state['user_id'])
|
||
? Usuario_promo::where('usuario_id', $state['user_id'])->where('promocion_id', $promoId)->first()
|
||
: null;
|
||
$tp = $promo->tarifaPromo->first();
|
||
$precio = $upric ? $upric->precio : ($tp ? $tp->precio : $promo->precio);
|
||
|
||
$ref = 'PROMO-' . strtoupper(Str::random(10));
|
||
$url = $this->createMPPreference($promo->nombre ?? 'Promocion streaming', (int) $precio, $ref);
|
||
|
||
if (! $url) {
|
||
$this->send($chatId, "No se pudo generar el link de pago. Intenta de nuevo o contacta a un asesor.");
|
||
return;
|
||
}
|
||
|
||
$this->sendWithKeyboard(
|
||
$chatId,
|
||
"🏦 *Pago con MercadoPago*\n\n{$promo->nombre} — \$" . number_format($precio) . "\nRef: `{$ref}`\n\n[👉 Pagar ahora]({$url})\n\nDespués de pagar, envía la foto del comprobante aquí.",
|
||
[[['text' => '🔙 Menú principal', 'callback_data' => 'menu']]]
|
||
);
|
||
}
|
||
|
||
// ─── Recharge ─────────────────────────────────────────────
|
||
|
||
private function showRechargeAmounts(string $chatId): void
|
||
{
|
||
$buttons = [
|
||
[
|
||
['text' => '$20.000', 'callback_data' => 'rec_amount|20000'],
|
||
['text' => '$50.000', 'callback_data' => 'rec_amount|50000'],
|
||
],
|
||
[
|
||
['text' => '$100.000', 'callback_data' => 'rec_amount|100000'],
|
||
['text' => '$200.000', 'callback_data' => 'rec_amount|200000'],
|
||
],
|
||
[
|
||
['text' => '$500.000', 'callback_data' => 'rec_amount|500000'],
|
||
['text' => '✏️ Otro valor', 'callback_data' => 'rec_custom'],
|
||
],
|
||
[['text' => '🔙 Menú principal', 'callback_data' => 'menu']],
|
||
];
|
||
|
||
$this->sendWithKeyboard($chatId, "💰 *¿Cuánto quieres recargar?*", $buttons);
|
||
}
|
||
|
||
private function askCustomAmount(string $chatId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$state['step'] = 'await_amount';
|
||
$this->setState($chatId, $state);
|
||
$this->send($chatId, "Escribe el monto que deseas recargar (mínimo \$1.000):");
|
||
}
|
||
|
||
private function processCustomAmount(string $chatId, string $text): void
|
||
{
|
||
$monto = (int) preg_replace('/\D/', '', $text);
|
||
|
||
if ($monto < 1000) {
|
||
$this->send($chatId, "El monto debe ser al menos \$1.000. Escríbelo de nuevo:");
|
||
return;
|
||
}
|
||
|
||
$state = $this->getState($chatId);
|
||
$state['step'] = 'menu';
|
||
$this->setState($chatId, $state);
|
||
|
||
$this->chooseRechargeMethod($chatId, $monto);
|
||
}
|
||
|
||
private function chooseRechargeMethod(string $chatId, int $monto): void
|
||
{
|
||
if ($monto <= 0) {
|
||
return;
|
||
}
|
||
|
||
$llave = ChatConfig::get('recarga_banco_llave', '');
|
||
$buttons = [
|
||
[['text' => '🏦 MercadoPago', 'callback_data' => 'rec_mp|' . $monto]],
|
||
];
|
||
if ($llave) {
|
||
$buttons[] = [['text' => '🔑 Bre-B', 'callback_data' => 'rec_breb|' . $monto]];
|
||
}
|
||
$buttons[] = [['text' => '🔙 Volver', 'callback_data' => 'rec_init']];
|
||
|
||
$this->sendWithKeyboard(
|
||
$chatId,
|
||
"Recarga de *\$" . number_format($monto) . "*\n\n¿Cómo quieres pagar?",
|
||
$buttons
|
||
);
|
||
}
|
||
|
||
private function rechargeMP(string $chatId, int $monto): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
|
||
if ($monto <= 0) {
|
||
$this->send($chatId, "Monto inválido.");
|
||
return;
|
||
}
|
||
|
||
$ref = 'REC-' . strtoupper(Str::random(12));
|
||
$url = $this->createMPPreference('Recarga de saldo', $monto, $ref);
|
||
|
||
if (! $url) {
|
||
$this->send($chatId, "No se pudo generar el link de recarga. Intenta de nuevo o contacta a un asesor.");
|
||
return;
|
||
}
|
||
|
||
if ($state['user_id'] ?? null) {
|
||
$user = User::with('saldo')->find($state['user_id']);
|
||
$saldo = $user->saldo ?? Saldo::create(['usuario_id' => $state['user_id'], 'valor' => 0]);
|
||
recarga::create([
|
||
'monto' => $monto,
|
||
'reference' => $ref,
|
||
'status' => 'pendiente_mp',
|
||
'usuario_id' => $state['user_id'],
|
||
'saldo_id' => $saldo->id,
|
||
]);
|
||
}
|
||
|
||
$this->sendWithKeyboard(
|
||
$chatId,
|
||
"🏦 *Recarga con MercadoPago*\n\n\$" . number_format($monto) . "\nRef: `{$ref}`\n\n[👉 Recargar ahora]({$url})\n\nDespués de pagar, envía la foto del comprobante aquí.",
|
||
[[['text' => '🔙 Menú principal', 'callback_data' => 'menu']]]
|
||
);
|
||
}
|
||
|
||
private function rechargeBreb(string $chatId, int $monto): void
|
||
{
|
||
$banco = ChatConfig::get('recarga_banco_nombre', '');
|
||
$llave = ChatConfig::get('recarga_banco_llave', '');
|
||
$titular = ChatConfig::get('recarga_banco_titular', '');
|
||
|
||
if (! $llave) {
|
||
$this->send($chatId, "El pago por Bre-B no está disponible. Usa MercadoPago o contacta a un asesor.");
|
||
return;
|
||
}
|
||
|
||
$text = "🏧 *Pago por Bre-B*\n\n";
|
||
$text .= "Monto: *\$" . number_format($monto) . "*\n\n";
|
||
if ($banco) {
|
||
$text .= "Banco/Billetera: *{$banco}*\n";
|
||
}
|
||
$text .= "Llave: `{$llave}`\n";
|
||
if ($titular) {
|
||
$text .= "Titular: *{$titular}*\n";
|
||
}
|
||
$text .= "\n⚠️ Después de transferir, envía la *foto del comprobante* aquí y el sistema lo validará automáticamente.";
|
||
|
||
$this->sendWithKeyboard($chatId, $text, [
|
||
[['text' => '🔙 Menú principal', 'callback_data' => 'menu']],
|
||
]);
|
||
}
|
||
|
||
// ─── Credentials ──────────────────────────────────────────
|
||
|
||
private function showCredentials(string $chatId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
|
||
$historiales = Historiale::where('cliente_id', $state['user_id'])
|
||
->where('estado', 'entregado')
|
||
->where('fecha_final', '>=', now())
|
||
->with('tarifa.servicio')
|
||
->orderBy('fecha_final', 'desc')
|
||
->limit(10)
|
||
->get();
|
||
|
||
if ($historiales->isEmpty()) {
|
||
$this->send($chatId, "No tienes planes activos en este momento.");
|
||
$this->showMainMenu($chatId);
|
||
return;
|
||
}
|
||
|
||
$this->send($chatId, "🔑 *Tus planes activos:*");
|
||
|
||
foreach ($historiales as $h) {
|
||
foreach ($h->cuentas as $c) {
|
||
$text = "🎬 *" . ($h->tarifa?->servicio?->nombre ?? 'Servicio') . "*\n";
|
||
$text .= "📧 Email: `{$c->correo}`\n";
|
||
$text .= "🔑 Password: `{$c->password}`\n";
|
||
if ($h->tarifa?->pantallas) {
|
||
$text .= "📺 Perfil: {$h->tarifa->pantallas}\n";
|
||
}
|
||
$text .= "📅 Vence: " . Carbon::parse($h->fecha_final)->format('d/m/Y');
|
||
$this->send($chatId, $text);
|
||
}
|
||
}
|
||
|
||
$this->sendWithKeyboard($chatId, "", [[['text' => '🔙 Menú principal', 'callback_data' => 'menu']]]);
|
||
}
|
||
|
||
// ─── History ──────────────────────────────────────────────
|
||
|
||
private function showHistory(string $chatId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
|
||
$historiales = Historiale::where('cliente_id', $state['user_id'])
|
||
->with('tarifa.servicio')
|
||
->orderBy('created_at', 'desc')
|
||
->limit(10)
|
||
->get();
|
||
|
||
if ($historiales->isEmpty()) {
|
||
$this->send($chatId, "No tienes compras registradas.");
|
||
$this->showMainMenu($chatId);
|
||
return;
|
||
}
|
||
|
||
$text = "📋 *Tus últimas compras:*\n\n";
|
||
foreach ($historiales as $h) {
|
||
$activo = Carbon::parse($h->fecha_final)->gte(now()) ? '✅' : '❌';
|
||
$nombre = $h->tarifa?->servicio?->nombre ?? ($h->promocion_id ? 'Promo' : 'Plan');
|
||
$fecha = Carbon::parse($h->created_at)->format('d/m/Y');
|
||
$text .= "{$activo} {$nombre} — \$" . number_format($h->valor) . " ({$fecha})\n";
|
||
}
|
||
|
||
$this->sendWithKeyboard($chatId, $text, [[['text' => '🔙 Menú principal', 'callback_data' => 'menu']]]);
|
||
}
|
||
|
||
// ─── Profile ──────────────────────────────────────────────
|
||
|
||
private function showProfile(string $chatId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
$user = User::with('saldo')->find($state['user_id']);
|
||
|
||
$text = "👤 *Tu perfil*\n\n";
|
||
$text .= "Nombre: *{$user->name}*\n";
|
||
$text .= "Email: {$user->email}\n";
|
||
if ($user->celular) {
|
||
$text .= "Celular: {$user->celular}\n";
|
||
}
|
||
$text .= "Saldo: *\$" . number_format($user->saldo?->valor ?? 0) . "*";
|
||
|
||
$this->sendWithKeyboard($chatId, $text, [[['text' => '🔙 Menú principal', 'callback_data' => 'menu']]]);
|
||
}
|
||
|
||
// ─── Agent ────────────────────────────────────────────────
|
||
|
||
private function transferToAgent(string $chatId): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
if ($state['conv_id'] ?? null) {
|
||
ChatConversation::where('id', $state['conv_id'])->update(['estado' => 'agente']);
|
||
}
|
||
$this->send($chatId, ChatConfig::get('mensaje_transferencia', 'Un asesor se comunicará contigo en breve. Por favor espera.'));
|
||
}
|
||
|
||
// ─── Apply validated payment ──────────────────────────────
|
||
|
||
private function applyPayment(string $chatId, int $monto, ?string $ref): void
|
||
{
|
||
$state = $this->getState($chatId);
|
||
|
||
if (! ($state['user_id'] ?? null) || $monto <= 0) {
|
||
$this->send($chatId, "No se pudo aplicar la recarga.");
|
||
return;
|
||
}
|
||
|
||
$user = User::with('saldo')->find($state['user_id']);
|
||
$saldo = $user->saldo ?? Saldo::create(['usuario_id' => $state['user_id'], 'valor' => 0]);
|
||
$nuevoSaldo = $saldo->valor + $monto;
|
||
$saldo->update(['valor' => $nuevoSaldo]);
|
||
|
||
$this->send(
|
||
$chatId,
|
||
"✅ Recarga de *\$" . number_format($monto) . "* aplicada.\nNuevo saldo: *\$" . number_format($nuevoSaldo) . "*"
|
||
);
|
||
$this->showMainMenu($chatId);
|
||
}
|
||
|
||
// ─── Free text fallback ───────────────────────────────────
|
||
|
||
private function handleFreeText(string $chatId, array $state): void
|
||
{
|
||
if (! ($state['user_id'] ?? null)) {
|
||
$this->askEmail($chatId);
|
||
return;
|
||
}
|
||
|
||
if ($state['conv_id'] ?? null) {
|
||
$conv = ChatConversation::find($state['conv_id']);
|
||
if ($conv && $conv->estado === 'agente') {
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Intentar detectar intención con Gemini antes de mostrar menú
|
||
$texto = $state['data']['last_text'] ?? '';
|
||
if ($texto) {
|
||
$accion = app(GeminiIntentService::class)->detectar($texto, $state['user_id'] ?? null, 'telegram');
|
||
if ($accion && $accion['action'] !== 'menu.principal') {
|
||
$this->dispatchAction($chatId, $accion['action'], $state);
|
||
return;
|
||
}
|
||
}
|
||
|
||
$this->showMainMenu($chatId);
|
||
}
|
||
|
||
private function dispatchAction(string $chatId, string $action, array $state): void
|
||
{
|
||
match ($action) {
|
||
'servicios.listar' => $this->listServices($chatId),
|
||
'promo.listar' => $this->listPromos($chatId),
|
||
'recarga.iniciar' => $this->showRechargeAmounts($chatId),
|
||
'credenciales.listar' => $this->showCredentials($chatId),
|
||
'historial.ver' => $this->showHistory($chatId),
|
||
'perfil.ver' => $this->showProfile($chatId),
|
||
'asesor.solicitar' => $this->transferToAgent($chatId),
|
||
default => $this->showMainMenu($chatId),
|
||
};
|
||
}
|
||
|
||
// ─── State management ─────────────────────────────────────
|
||
|
||
private function getState(string $chatId): array
|
||
{
|
||
return Cache::get("tgbot_state_{$chatId}", ['step' => 'await_email', 'data' => []]);
|
||
}
|
||
|
||
private function setState(string $chatId, array $state): void
|
||
{
|
||
Cache::put("tgbot_state_{$chatId}", $state, now()->addSeconds(self::STATE_TTL));
|
||
}
|
||
|
||
// ─── Telegram API ─────────────────────────────────────────
|
||
|
||
public function registrarComandos(): bool
|
||
{
|
||
return $this->apiCall('setMyCommands', [
|
||
'commands' => [
|
||
['command' => 'start', 'description' => 'Iniciar / Menú principal'],
|
||
['command' => 'menu', 'description' => 'Ver el menú principal'],
|
||
['command' => 'salir', 'description' => 'Volver al menú desde cualquier punto'],
|
||
['command' => 'cancelar','description' => 'Cancelar la acción actual'],
|
||
],
|
||
]);
|
||
}
|
||
|
||
public function send(string $chatId, string $text): bool
|
||
{
|
||
if ($this->convId) {
|
||
ChatMessage::create([
|
||
'conversation_id' => $this->convId,
|
||
'tipo' => 'bot',
|
||
'contenido' => $text,
|
||
'leido' => true,
|
||
]);
|
||
}
|
||
|
||
return $this->apiCall('sendMessage', [
|
||
'chat_id' => $chatId,
|
||
'text' => $text,
|
||
'parse_mode' => 'Markdown',
|
||
]);
|
||
}
|
||
|
||
public function sendWithKeyboard(string $chatId, string $text, array $inlineKeyboard): bool
|
||
{
|
||
if ($this->convId) {
|
||
ChatMessage::create([
|
||
'conversation_id' => $this->convId,
|
||
'tipo' => 'bot',
|
||
'contenido' => $text,
|
||
'leido' => true,
|
||
]);
|
||
}
|
||
|
||
return $this->apiCall('sendMessage', [
|
||
'chat_id' => $chatId,
|
||
'text' => $text ?: '', // zero-width space fallback
|
||
'parse_mode' => 'Markdown',
|
||
'reply_markup' => json_encode(['inline_keyboard' => $inlineKeyboard]),
|
||
]);
|
||
}
|
||
|
||
private function answerCallback(string $callbackQueryId): void
|
||
{
|
||
$this->apiCall('answerCallbackQuery', ['callback_query_id' => $callbackQueryId]);
|
||
}
|
||
|
||
private function apiCall(string $method, array $data): bool
|
||
{
|
||
$token = ChatConfig::get('telegram_token');
|
||
if (! $token) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
$response = Http::timeout(10)
|
||
->post("https://api.telegram.org/bot{$token}/{$method}", $data);
|
||
|
||
return $response->successful();
|
||
} catch (\Throwable $e) {
|
||
Log::warning("[TelegramBot] apiCall {$method} failed: " . $e->getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private function downloadTelegramFile(string $fileId): ?array
|
||
{
|
||
$token = ChatConfig::get('telegram_token');
|
||
if (! $token) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
$info = Http::timeout(10)->get("https://api.telegram.org/bot{$token}/getFile", ['file_id' => $fileId]);
|
||
$filePath = $info->json('result.file_path');
|
||
if (! $filePath) {
|
||
return null;
|
||
}
|
||
|
||
$content = Http::timeout(30)->get("https://api.telegram.org/file/bot{$token}/{$filePath}")->body();
|
||
if (! $content) {
|
||
return null;
|
||
}
|
||
|
||
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||
$mime = match ($ext) {
|
||
'png' => 'image/png',
|
||
'webp' => 'image/webp',
|
||
default => 'image/jpeg',
|
||
};
|
||
|
||
return ['content' => $content, 'mime' => $mime];
|
||
|
||
} catch (\Throwable $e) {
|
||
Log::warning('[TelegramBot] downloadTelegramFile failed: ' . $e->getMessage());
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ─── Helpers ──────────────────────────────────────────────
|
||
|
||
private function efectivePrice(int $tarifaId, ?int $userId, float $base): float
|
||
{
|
||
if ($userId) {
|
||
$pref = Preferencial::where('usuario_id', $userId)->where('tarifa_id', $tarifaId)->first();
|
||
if ($pref) {
|
||
return (float) $pref->valor;
|
||
}
|
||
}
|
||
return (float) $base;
|
||
}
|
||
|
||
private function maskEmail(string $email): string
|
||
{
|
||
[$local, $domain] = explode('@', $email, 2);
|
||
$len = mb_strlen($local);
|
||
$visible = min(4, $len); // siempre 4 caracteres máximo
|
||
return mb_substr($local, 0, $visible) . str_repeat('*', max(0, $len - $visible)) . '@' . $domain;
|
||
}
|
||
|
||
private function createMPPreference(string $titulo, int $valor, string $ref): ?string
|
||
{
|
||
$accessToken = config('services.mercadopago.token');
|
||
if (! $accessToken) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
$response = Http::withToken($accessToken)
|
||
->timeout(10)
|
||
->post('https://api.mercadopago.com/checkout/preferences', [
|
||
'items' => [[
|
||
'title' => $titulo,
|
||
'quantity' => 1,
|
||
'unit_price' => $valor,
|
||
'currency_id' => 'COP',
|
||
]],
|
||
'external_reference' => $ref,
|
||
'back_urls' => [
|
||
'success' => url('/chat'),
|
||
'failure' => url('/chat'),
|
||
'pending' => url('/chat'),
|
||
],
|
||
'auto_return' => 'approved',
|
||
'payment_methods' => [
|
||
'excluded_payment_types' => [['id' => 'ticket']],
|
||
],
|
||
]);
|
||
|
||
return $response->successful() ? $response->json('init_point') : null;
|
||
} catch (\Throwable $e) {
|
||
Log::warning('[TelegramMP] Error: ' . $e->getMessage());
|
||
return null;
|
||
}
|
||
}
|
||
}
|