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)); // Palabras que siempre regresan al menú, desde cualquier punto del flujo $menuTriggers = [ '/start', '/menu', '/salir', '/inicio', '/cancelar', 'menu', 'menú', 'inicio', 'hola', 'salir', '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'; 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), '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); 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."); } } // ─── 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; } $user = User::whereRaw('LOWER(email) = ?', [$email])->first(); 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; } $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); } 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)); try { Mail::to($user->email)->send(new ChatOtpMail($codigo, $user->name)); } catch (\Throwable $e) { Log::warning('[TelegramOTP] Error enviando correo: ' . $e->getMessage()); } $this->setState($chatId, [ 'step' => 'await_otp', 'contact_id' => $contact->id, 'data' => [], ]); $masked = $this->maskEmail($user->email); $this->send($chatId, "✉️ Te enviamos un código a *{$masked}*.\n\nEscribe el código de 6 dígitos:"); } 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 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'], ], ]); } // ─── 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; } } $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); $visible = mb_substr($local, 0, min(3, mb_strlen($local))); return $visible . str_repeat('*', max(0, mb_strlen($local) - 3)) . '@' . $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; } } }