Files
sirpremiumv2/app/Http/Livewire/Chat/PublicChat.php
T
LizandroandClaude Sonnet 4.6 1f25c31603 Fix "sin cuentas disponibles" and wrong pricing in bot/chat
Three bugs fixed:

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

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-18 16:02:54 +00:00

1447 lines
60 KiB
PHP
Executable File

<?php
namespace App\Http\Livewire\Chat;
use App\Mail\ChatOtpMail;
use App\Models\Role;
use App\Models\Cuentas;
use App\Models\ChatContact;
use Illuminate\Support\Facades\DB;
use App\Models\ChatConversation;
use App\Models\ChatMessage;
use App\Models\Historiale;
use App\Models\Historial_cuenta;
use App\Models\Preferencial;
use App\Models\Promociones;
use App\Services\CuentasHelper;
use App\Models\TarifaPromo;
use App\Models\Usuario_promo;
use App\Models\recarga;
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\Mail;
use Illuminate\Support\Str;
use Livewire\Component;
use Livewire\WithFileUploads;
class PublicChat extends Component
{
use WithFileUploads;
// ── Paso actual ───────────────────────────────────────────
public string $paso = 'email'; // email | verificacion | registro | chat
public string $email = '';
public string $nombreRegistro = '';
public string $cedulaRegistro = '';
public string $celularRegistro = '';
public string $codigoIngresado = '';
public ?string $emailMascarado = null;
public ?int $contactIdPending = null; // contact.id mientras espera verificacion
public string $input = '';
public array $mensajes = [];
public ?int $convId = null;
public string $estadoConv = 'bot';
// ── Usuario identificado ──────────────────────────────────
public ?int $userId = null;
public ?string $nombreUsuario = null;
public ?float $saldoUsuario = null;
// ── Estado del wizard ─────────────────────────────────────
public string $flujo = '';
public array $flujoData = [];
// ── Upload comprobante ────────────────────────────────────
public $fotoComprobante = null;
public bool $procesandoImagen = false;
public function mount(): void
{
$userId = session('chat_user_id');
$convId = session('chat_conv_id');
if (! $userId || ! $convId) {
return;
}
$user = User::with('saldo')->find($userId);
$conv = ChatConversation::find($convId);
if (! $user || ! $conv) {
session()->forget(['chat_user_id', 'chat_conv_id']);
return;
}
$this->userId = $user->id;
$this->nombreUsuario = $user->name;
$this->saldoUsuario = $user->saldo?->valor ?? 0;
$this->convId = $conv->id;
$this->estadoConv = $conv->estado;
$this->paso = 'chat';
$this->cargarMensajes();
}
protected $rules = [
'email' => 'required|email|max:100',
'nombreRegistro' => 'required|max:80',
'cedulaRegistro' => 'nullable|max:20',
'celularRegistro' => 'nullable|max:20',
'codigoIngresado' => 'nullable|digits:6',
];
// ─────────────────────────────────────────────────────────
// Paso 1: buscar por correo
// ─────────────────────────────────────────────────────────
public function iniciar(): void
{
$this->validate(['email' => 'required|email|max:100']);
$user = User::where('email', $this->email)->first();
if (! $user) {
$this->paso = 'registro';
return;
}
$contact = ChatContact::firstOrCreate(
['canal' => 'web', 'canal_id' => $user->email],
['nombre' => $user->name, 'telefono' => $user->email, 'user_id' => $user->id]
);
if (! $contact->user_id) {
$contact->update(['user_id' => $user->id]);
}
$this->contactIdPending = $contact->id;
$this->emailMascarado = $this->enmascararEmail($user->email);
$this->enviarCodigoOtp($contact->id, $user);
$this->paso = 'verificacion';
}
// ─────────────────────────────────────────────────────────
// Paso 2a: crear cuenta nueva
// ─────────────────────────────────────────────────────────
public function crearCuenta(): void
{
$this->validate([
'email' => 'required|email|max:100',
'nombreRegistro' => 'required|max:80',
'cedulaRegistro' => 'nullable|max:20',
'celularRegistro' => 'nullable|max:20',
]);
$rolCliente = Role::where('nombre', 'cliente')->first();
$user = User::create([
'name' => $this->nombreRegistro,
'email' => $this->email,
'password' => Hash::make(Str::random(16)),
'cedula' => $this->cedulaRegistro ?: null,
'celular' => $this->celularRegistro ?: null,
'estado' => 'activo',
'rol_id' => $rolCliente?->id,
]);
$contact = ChatContact::create([
'canal' => 'web',
'canal_id' => $user->email,
'nombre' => $user->name,
'telefono' => $user->email,
'user_id' => $user->id,
]);
$this->contactIdPending = $contact->id;
$this->emailMascarado = $this->enmascararEmail($user->email);
$this->enviarCodigoOtp($contact->id, $user);
$this->paso = 'verificacion';
}
public function rechazarRegistro(): void
{
$this->email = '';
$this->nombreRegistro = '';
$this->cedulaRegistro = '';
$this->celularRegistro = '';
$this->paso = 'email';
}
public function confirmarCierreSesion(): void
{
if (! $this->convId) {
return;
}
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => '¿Seguro que deseas cerrar sesión? Tendrás que volver a ingresar tu correo y código la próxima vez.',
'payload' => ['botones' => [
['label' => '✅ Sí, cerrar sesión', 'action' => 'sesion.cerrar.confirmar', 'data' => []],
['label' => '❌ No, quedarme', 'action' => 'menu.principal', 'data' => []],
]],
'leido' => true,
]);
}
public function cerrarSesion(): void
{
session()->forget(['chat_user_id', 'chat_conv_id']);
$this->email = '';
$this->nombreRegistro = '';
$this->cedulaRegistro = '';
$this->celularRegistro = '';
$this->codigoIngresado = '';
$this->emailMascarado = null;
$this->contactIdPending = null;
$this->userId = null;
$this->nombreUsuario = null;
$this->saldoUsuario = null;
$this->convId = null;
$this->estadoConv = 'bot';
$this->flujo = '';
$this->flujoData = [];
$this->mensajes = [];
$this->input = '';
$this->paso = 'email';
}
// ─────────────────────────────────────────────────────────
// Paso 2b: verificar código OTP
// ─────────────────────────────────────────────────────────
private function enviarCodigoOtp(int $contactId, User $user): void
{
$codigo = (string) random_int(100000, 999999);
Cache::put("chat_otp_{$contactId}", $codigo, now()->addMinutes(10));
try {
Mail::to($user->email)->send(new ChatOtpMail($codigo, $user->name));
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::warning('[ChatOTP] Error enviando correo: ' . $e->getMessage());
}
}
public function verificarCodigo(): void
{
$this->validate(['codigoIngresado' => 'required|digits:6']);
if (! $this->contactIdPending) {
$this->addError('codigoIngresado', 'Sesion expirada. Recarga la pagina.');
return;
}
$codigoGuardado = Cache::get("chat_otp_{$this->contactIdPending}");
if (! $codigoGuardado) {
$this->addError('codigoIngresado', 'El codigo expiro. Solicita uno nuevo.');
return;
}
if ($this->codigoIngresado !== $codigoGuardado) {
$this->addError('codigoIngresado', 'Codigo incorrecto. Intentalo de nuevo.');
return;
}
Cache::forget("chat_otp_{$this->contactIdPending}");
$contact = ChatContact::find($this->contactIdPending);
if (! $contact) {
$this->addError('codigoIngresado', 'Sesion invalida. Recarga la pagina.');
return;
}
$this->contactIdPending = null;
$this->codigoIngresado = '';
$this->abrirChat($contact);
}
public function reenviarCodigo(): void
{
if (! $this->contactIdPending) {
return;
}
$contact = ChatContact::find($this->contactIdPending);
if (! $contact || ! $contact->user_id) {
return;
}
$user = User::find($contact->user_id);
if ($user) {
$this->enviarCodigoOtp($this->contactIdPending, $user);
session()->flash('otp_reenviado', 'Codigo reenviado a tu correo.');
}
}
// ─────────────────────────────────────────────────────────
// Abrir chat (después de identificación)
// ─────────────────────────────────────────────────────────
private function abrirChat(ChatContact $contact): void
{
if ($contact->user_id) {
$user = User::with('saldo')->find($contact->user_id);
$this->userId = $user->id;
$this->nombreUsuario = $user->name;
$this->saldoUsuario = $user->saldo?->valor ?? 0;
}
$conv = $contact->activeConversation();
$esNueva = ! $conv;
if ($esNueva) {
$conv = ChatConversation::create([
'contact_id' => $contact->id,
'canal' => 'web',
'estado' => 'bot',
'ultimo_mensaje_at' => now(),
]);
$saludo = $this->nombreUsuario
? "¡Hola, {$this->nombreUsuario}! Bienvenido."
: "¡Hola! Bienvenido.";
$this->guardarMensajeBot($conv->id, $saludo);
} else {
// Conversación existente: el usuario acaba de verificarse por OTP
$saludo = $this->nombreUsuario
? "Bienvenido de nuevo, {$this->nombreUsuario}!"
: "Bienvenido de nuevo!";
$this->guardarMensajeBot($conv->id, $saludo);
}
$this->convId = $conv->id;
$this->estadoConv = $conv->estado;
$this->paso = 'chat';
session()->put('chat_user_id', $this->userId);
session()->put('chat_conv_id', $conv->id);
// Siempre muestra el menú al autenticarse (mount no llama a este método)
$this->mostrarMenuPrincipal($conv->id);
$this->cargarMensajes();
}
private function enmascararEmail(string $email): string
{
[$local, $dominio] = explode('@', $email, 2);
$n = min(4, mb_strlen($local));
$visible = mb_substr($local, 0, $n);
return $visible . str_repeat('*', max(0, mb_strlen($local) - $n)) . '@' . $dominio;
}
// ─────────────────────────────────────────────────────────
// Envío de texto libre
// ─────────────────────────────────────────────────────────
public function enviar(): void
{
if (! $this->convId || trim($this->input) === '') {
return;
}
$texto = trim($this->input);
$this->input = '';
$this->guardarMensajeUsuario($this->convId, $texto);
$conv = ChatConversation::find($this->convId);
if (! $conv || $conv->estado === 'agente') {
$this->cargarMensajes();
return;
}
// Capturar monto personalizado de recarga
if ($this->flujo === 'recarga.monto_custom') {
$monto = (int) preg_replace('/\D/', '', $texto);
if ($monto >= 1000) {
$this->flujo = '';
$this->flujoData = [];
$this->elegirMetodoRecarga($monto);
} else {
$this->guardarMensajeBot($this->convId, "El monto debe ser al menos $1.000. Escríbelo de nuevo:");
}
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
return;
}
$accion = $this->detectarIntencion($texto);
if ($accion) {
$this->clickBoton($accion['action'], $accion['data'] ?? []);
} else {
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => "No entendí bien tu mensaje. Puedes escribir cosas como:\n\"ver planes\", \"hacer recarga\", \"mis credenciales\", \"ver promociones\" o usar los botones:",
'payload' => ['botones' => [
['label' => 'Ver planes', 'action' => 'servicios.listar', 'data' => []],
['label' => 'Recargar saldo', 'action' => 'recarga.iniciar', 'data' => []],
['label' => 'Mis credenciales','action' => 'credenciales.listar','data' => []],
['label' => 'Menú principal', 'action' => 'menu.principal', 'data' => []],
]],
'leido' => true,
]);
}
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
}
// ─────────────────────────────────────────────────────────
// Clic en botón
// ─────────────────────────────────────────────────────────
public function clickBoton(string $action, array $data = []): void
{
if (! $this->convId) {
return;
}
match (true) {
$action === 'menu.principal' => $this->mostrarMenuPrincipal($this->convId),
$action === 'servicios.listar' => $this->listarServicios(),
$action === 'servicios.ver' => $this->verServicio((int) ($data['id'] ?? 0)),
$action === 'compra.iniciar' => $this->iniciarCompra((int) ($data['tarifa_id'] ?? 0)),
$action === 'compra.pagar.saldo' => $this->pagarConSaldo(),
$action === 'compra.pagar.mp' => $this->pagarConMP(),
$action === 'promo.listar' => $this->listarPromociones(),
$action === 'promo.ver' => $this->verPromocion((int) ($data['id'] ?? 0)),
$action === 'promo.comprar.saldo' => $this->comprarPromoConSaldo((int) ($data['id'] ?? 0)),
$action === 'promo.comprar.mp' => $this->comprarPromoConMP((int) ($data['id'] ?? 0)),
$action === 'recarga.iniciar' => $this->mostrarFormRecarga(),
$action === 'recarga.otro_valor' => $this->solicitarMontoPersonalizado(),
$action === 'recarga.elegir_metodo' => $this->elegirMetodoRecarga((int) ($data['monto'] ?? 0)),
$action === 'recarga.mp' => $this->generarLinkRecargaMP((int) ($data['monto'] ?? 0)),
$action === 'recarga.transferencia' => $this->mostrarTransferencia((int) ($data['monto'] ?? 0)),
$action === 'credenciales.listar' => $this->listarCredenciales(),
$action === 'historial.ver' => $this->verHistorial(),
$action === 'perfil.ver' => $this->verPerfil(),
$action === 'asesor.solicitar' => $this->solicitarAsesor(),
$action === 'pago.aplicar' => $this->aplicarRecargaValidada($data),
$action === 'sesion.cerrar' => $this->confirmarCierreSesion(),
$action === 'sesion.cerrar.confirmar' => $this->cerrarSesion(),
default => $this->mostrarMenuPrincipal($this->convId),
};
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
}
// ─────────────────────────────────────────────────────────
// Upload comprobante de pago
// ─────────────────────────────────────────────────────────
public function updatedFotoComprobante(): void
{
if (! $this->fotoComprobante || ! $this->convId) {
return;
}
if (\App\Models\ChatConfig::get('validacion_foto_habilitada', '0') !== '1') {
$this->guardarMensajeBot($this->convId, "El analisis de comprobantes no esta habilitado. Contacta a un asesor.");
$this->fotoComprobante = null;
$this->cargarMensajes();
return;
}
$this->procesandoImagen = true;
try {
$path = $this->fotoComprobante->store('comprobantes', 'public');
$url = asset('storage/' . $path);
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'usuario',
'tipo_ui' => 'imagen',
'contenido' => '[Comprobante de pago]',
'payload' => ['url' => $url],
'leido' => false,
]);
$base64 = base64_encode(file_get_contents($this->fotoComprobante->getRealPath()));
$mimeType = $this->fotoComprobante->getMimeType();
$datosPago = app(\App\Services\GeminiVisionService::class)->extraerPago($base64, $mimeType);
if (! $datosPago) {
$this->guardarMensajeBot($this->convId, "No pude leer el comprobante. Intenta con una foto mas clara.");
$this->cargarMensajes();
return;
}
$resultado = app(\App\Services\PagoValidadorService::class)->validar($datosPago);
$botones = [];
if ($resultado['estado'] === 'confirmado') {
$botones[] = [
'label' => 'Aplicar recarga de $' . number_format($datosPago['valor'] ?? 0),
'action' => 'pago.aplicar',
'data' => ['monto' => $datosPago['valor'], 'referencia' => $datosPago['referencia'] ?? null],
];
}
$botones[] = ['label' => 'Hablar con asesor', 'action' => 'asesor.solicitar', 'data' => []];
$botones[] = ['label' => '<- Menu', 'action' => 'menu.principal', 'data' => []];
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'validacion',
'contenido' => 'Resultado del analisis:',
'payload' => [
'estado' => $resultado['estado'],
'ia_datos' => $datosPago,
'botones' => $botones,
],
'leido' => true,
]);
} catch (\Throwable $e) {
$this->guardarMensajeBot($this->convId, "Error analizando el comprobante. Por favor intenta de nuevo.");
} finally {
$this->procesandoImagen = false;
$this->fotoComprobante = null;
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
}
}
// ─────────────────────────────────────────────────────────
// Menu principal
// ─────────────────────────────────────────────────────────
private function mostrarMenuPrincipal(int $convId): void
{
$this->flujo = '';
$this->flujoData = [];
ChatMessage::create([
'conversation_id' => $convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => 'En que te puedo ayudar?',
'payload' => ['botones' => [
['label' => 'Ver Servicios', 'action' => 'servicios.listar', 'data' => []],
['label' => 'Promociones', 'action' => 'promo.listar', 'data' => []],
['label' => 'Recargar Saldo', 'action' => 'recarga.iniciar', 'data' => []],
['label' => 'Mis Credenciales', 'action' => 'credenciales.listar', 'data' => []],
['label' => 'Historial', 'action' => 'historial.ver', 'data' => []],
['label' => 'Mi Perfil', 'action' => 'perfil.ver', 'data' => []],
['label' => 'Hablar con asesor', 'action' => 'asesor.solicitar', 'data' => []],
['label' => '🚪 Cerrar sesión', 'action' => 'sesion.cerrar', 'data' => []],
]],
'leido' => true,
]);
}
// ─────────────────────────────────────────────────────────
// Servicios
// ─────────────────────────────────────────────────────────
private function listarServicios(): void
{
$rolId = $this->userId ? User::find($this->userId)?->rol_id : Role::where('nombre', 'cliente')->value('id');
$servicios = Servicio::where('estado', 'activo')
->whereHas('tarifas', fn ($q) => $q->where('estado', 'activo')->where('rol_id', $rolId))
->orderBy('ubicacion')
->get();
if ($servicios->isEmpty()) {
$this->guardarMensajeBot($this->convId, "No hay servicios disponibles en este momento.");
$this->volver('menu.principal');
return;
}
$cards = $servicios->map(fn ($s) => [
'imagen' => $s->img_inicio,
'titulo' => $s->nombre,
'detalle' => $s->por_tiempo ? 'Por tiempo' : 'Por pantallas',
'accion' => ['label' => 'Ver planes', 'action' => 'servicios.ver', 'data' => ['id' => $s->id]],
])->values()->all();
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'cards_slider',
'contenido' => 'Selecciona el servicio:',
'payload' => ['cards' => $cards],
'leido' => true,
]);
$this->volver('menu.principal');
}
private function verServicio(int $id): void
{
$rolId = $this->userId ? User::find($this->userId)?->rol_id : Role::where('nombre', 'cliente')->value('id');
$servicio = Servicio::with(['tarifas' => fn ($q) => $q->where('estado', 'activo')->where('rol_id', $rolId)])->find($id);
if (! $servicio) {
$this->guardarMensajeBot($this->convId, "Servicio no encontrado.");
return;
}
$tarifas = $servicio->tarifas;
if ($tarifas->isEmpty()) {
$this->guardarMensajeBot($this->convId, "No hay planes para {$servicio->nombre} en este momento.");
$this->volver('servicios.listar');
return;
}
$botones = [];
foreach ($tarifas as $t) {
$precio = $this->efectivePrice($t->id, $this->userId, $t->valor);
$label = $servicio->por_tiempo
? "{$t->dias} dias - $" . number_format($precio)
: "{$t->pantallas} pantalla(s) / {$t->dias} dias - $" . number_format($precio);
$botones[] = ['label' => $label, 'action' => 'compra.iniciar', 'data' => ['tarifa_id' => $t->id]];
}
$botones[] = ['label' => 'Volver', 'action' => 'servicios.listar', 'data' => []];
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => "Planes de {$servicio->nombre}:",
'payload' => ['botones' => $botones],
'leido' => true,
]);
}
// ─────────────────────────────────────────────────────────
// Compra de plan
// ─────────────────────────────────────────────────────────
private function iniciarCompra(int $tarifaId): void
{
$tarifa = Tarifas::with('servicio')->find($tarifaId);
if (! $tarifa) {
$this->guardarMensajeBot($this->convId, "Plan no encontrado.");
return;
}
if ($this->contarDisponibles($tarifa) < 1) {
$this->guardarMensajeBot($this->convId, "Sin cuentas disponibles para este plan. Intenta con otro.");
$this->volver('servicios.listar');
return;
}
$precio = $this->efectivePrice($tarifaId, $this->userId, $tarifa->valor);
$this->flujo = 'compra';
$this->flujoData = [
'tarifa_id' => $tarifaId,
'valor' => $precio,
'servicio' => $tarifa->servicio->nombre,
'dias' => $tarifa->dias,
'pantallas' => $tarifa->pantallas,
];
$detalle = $tarifa->servicio->por_tiempo
? "{$tarifa->dias} dias"
: "{$tarifa->pantallas} pantalla(s) / {$tarifa->dias} dias";
$saldoInsuficiente = ! $this->userId || ((float) $this->saldoUsuario < $precio);
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => "{$tarifa->servicio->nombre} - {$detalle}\nPrecio: $" . number_format($precio) . "\n\nElige como pagar:",
'payload' => ['botones' => [
['label' => 'Saldo ($' . number_format($precio) . ')',
'action' => 'compra.pagar.saldo',
'data' => [],
'disabled' => $saldoInsuficiente],
['label' => 'MercadoPago', 'action' => 'compra.pagar.mp', 'data' => []],
['label' => 'Volver', 'action' => 'servicios.listar', 'data' => []],
]],
'leido' => true,
]);
}
private function pagarConSaldo(): void
{
if (! $this->userId || $this->flujo !== 'compra') {
$this->guardarMensajeBot($this->convId, "Debes estar identificado para pagar con saldo.");
return;
}
$tarifa = Tarifas::with('servicio')->find($this->flujoData['tarifa_id'] ?? 0);
if (! $tarifa) {
$this->guardarMensajeBot($this->convId, "Ocurrio un error. Intenta de nuevo.");
return;
}
$user = User::with('saldo')->find($this->userId);
$saldo = $user->saldo;
$precio = (float) ($this->flujoData['valor'] ?? $tarifa->valor);
if (! $saldo || $saldo->valor < $precio) {
$this->guardarMensajeBot(
$this->convId,
"Saldo insuficiente. Tienes $" . number_format($saldo?->valor ?? 0) .
" y el plan cuesta $" . number_format($precio) . "."
);
$this->volver('menu.principal');
return;
}
$cuenta = $this->buscarCuentaDisponible($tarifa);
if (! $cuenta) {
$this->guardarMensajeBot($this->convId, "No hay cuentas disponibles en este momento. Intenta más tarde o contacta a un asesor.");
return;
}
$nuevoSaldo = $saldo->valor - $precio;
DB::transaction(function () use ($tarifa, $user, $saldo, $cuenta, $precio, $nuevoSaldo) {
$historial = Historiale::create([
'fecha_inicio' => now(),
'fecha_final' => now()->addDays($tarifa->dias),
'valor' => $precio,
'utilidad' => $tarifa->utilidad ?? 0,
'tipo_pago' => 'saldo',
'estado' => 'entregado',
'vendedor_id' => $this->userId,
'tarifa_id' => $tarifa->id,
'cliente_id' => $this->userId,
'nombre_cliente' => $user->name,
]);
Historial_cuenta::create(['historial_id' => $historial->id, 'cuenta_id' => $cuenta->id]);
$saldo->update(['valor' => $nuevoSaldo]);
});
$this->saldoUsuario = $nuevoSaldo;
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'credenciales',
'contenido' => 'Compra exitosa! Tus credenciales:',
'payload' => [
'servicio' => $tarifa->servicio->nombre,
'email' => $cuenta->correo,
'password' => $cuenta->password,
'perfil' => $tarifa->pantallas ?? null,
'vence' => now()->addDays($tarifa->dias)->format('d/m/Y'),
],
'leido' => true,
]);
$this->flujo = '';
$this->flujoData = [];
$this->volver('menu.principal');
}
private function pagarConMP(): void
{
if ($this->flujo !== 'compra') {
return;
}
$valor = (int) ($this->flujoData['valor'] ?? 0);
$servicio = $this->flujoData['servicio'] ?? 'Plan streaming';
$ref = 'CHAT-' . strtoupper(Str::random(12));
$url = $this->crearPreferenciaMP($servicio, $valor, $ref);
if (! $url) {
$this->guardarMensajeBot($this->convId, "No se pudo generar el link de pago. Intenta de nuevo o contacta a un asesor.");
return;
}
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'link',
'contenido' => "Link de pago por $" . number_format($valor) . " listo. Despues de pagar, envia el comprobante aqui.",
'payload' => ['url' => $url, 'label' => 'Pagar $' . number_format($valor) . ' con MercadoPago', 'nota' => 'Ref: ' . $ref],
'leido' => true,
]);
$this->flujo = '';
$this->flujoData = [];
$this->volver('menu.principal');
}
// ─────────────────────────────────────────────────────────
// Promociones
// ─────────────────────────────────────────────────────────
private function listarPromociones(): void
{
$rolId = $this->userId
? User::find($this->userId)?->rol_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->guardarMensajeBot($this->convId, "No hay promociones activas en este momento.");
$this->volver('menu.principal');
return;
}
$cards = $promos->map(function ($p) {
$tp = $p->tarifaPromo->first();
$upric = $this->userId
? Usuario_promo::where('usuario_id', $this->userId)->where('promocion_id', $p->id)->first()
: null;
$precio = $upric ? $upric->precio : ($tp ? $tp->precio : $p->precio);
return [
'imagen' => $p->img_publicidad,
'titulo' => $p->nombre ?? 'Promocion especial',
'descripcion' => $p->descripcion ?? null,
'precio' => $precio,
'detalle' => $p->fecha_limite ? 'Hasta ' . Carbon::parse($p->fecha_limite)->format('d/m/Y') : null,
'accion' => ['label' => 'Comprar', 'action' => 'promo.ver', 'data' => ['id' => $p->id]],
];
})->values()->all();
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'cards_slider',
'contenido' => 'Promociones disponibles:',
'payload' => ['cards' => $cards],
'leido' => true,
]);
$this->volver('menu.principal');
}
private function verPromocion(int $id): void
{
$rolId = $this->userId
? User::find($this->userId)?->rol_id
: Role::where('nombre', 'cliente')->value('id');
$promo = Promociones::with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])->find($id);
if (! $promo) {
$this->guardarMensajeBot($this->convId, "Promocion no encontrada.");
return;
}
$upric = $this->userId
? Usuario_promo::where('usuario_id', $this->userId)->where('promocion_id', $id)->first()
: null;
$tp = $promo->tarifaPromo->first();
$precio = $upric ? $upric->precio : ($tp ? $tp->precio : $promo->precio);
$saldoInsuficiente = ! $this->userId || ($this->saldoUsuario < $precio);
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => ($promo->nombre ?? 'Promocion') . "\n$" . number_format($precio) . "\n\nElige como pagar:",
'payload' => ['botones' => [
['label' => 'Saldo ($' . number_format($precio) . ')',
'action' => 'promo.comprar.saldo',
'data' => ['id' => $id],
'disabled' => $saldoInsuficiente],
['label' => 'MercadoPago', 'action' => 'promo.comprar.mp', 'data' => ['id' => $id]],
['label' => 'Volver', 'action' => 'promo.listar', 'data' => []],
]],
'leido' => true,
]);
}
private function comprarPromoConSaldo(int $promoId): void
{
if (! $this->userId) {
$this->guardarMensajeBot($this->convId, "No se pudo procesar la compra.");
return;
}
$user = User::with('saldo')->find($this->userId);
$rolId = $user->rol_id;
$promo = Promociones::with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])->find($promoId);
if (! $promo) {
$this->guardarMensajeBot($this->convId, "No se pudo procesar la compra.");
return;
}
$upric = Usuario_promo::where('usuario_id', $this->userId)->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->guardarMensajeBot($this->convId, "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->guardarMensajeBot($this->convId, "No hay cuentas disponibles para esta promocion.");
return;
}
$historial = Historiale::create([
'fecha_inicio' => now(),
'fecha_final' => now()->addDays(30),
'valor' => $precio,
'utilidad' => $utilidad,
'tipo_pago' => 'saldo',
'estado' => 'entregado',
'vendedor_id' => $this->userId,
'promocion_id' => $promoId,
'cliente_id' => $this->userId,
'nombre_cliente' => $user->name,
]);
Historial_cuenta::create(['historial_id' => $historial->id, 'cuenta_id' => $cuenta->id]);
$cuenta->update(['estado' => 'ocupado']);
$saldo->update(['valor' => $saldo->valor - $precio]);
$this->saldoUsuario = $saldo->valor - $precio;
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'credenciales',
'contenido' => 'Promo activada! Tus credenciales:',
'payload' => [
'servicio' => $promo->nombre ?? 'Promocion',
'email' => $cuenta->correo,
'password' => $cuenta->password,
'vence' => now()->addDays(30)->format('d/m/Y'),
],
'leido' => true,
]);
$this->volver('menu.principal');
}
private function comprarPromoConMP(int $promoId): void
{
$rolId = $this->userId
? User::find($this->userId)?->rol_id
: Role::where('nombre', 'cliente')->value('id');
$promo = Promociones::with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])->find($promoId);
if (! $promo) {
return;
}
$upric = $this->userId
? Usuario_promo::where('usuario_id', $this->userId)->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->crearPreferenciaMP($promo->nombre ?? 'Promocion streaming', (int) $precio, $ref);
if (! $url) {
$this->guardarMensajeBot($this->convId, "No se pudo generar el link de pago. Intenta de nuevo o contacta a un asesor.");
return;
}
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'link',
'contenido' => "Link de pago por $" . number_format($precio) . ". Envia el comprobante aqui al pagar.",
'payload' => ['url' => $url, 'label' => 'Pagar con MercadoPago', 'nota' => 'Ref: ' . $ref],
'leido' => true,
]);
$this->volver('menu.principal');
}
// ─────────────────────────────────────────────────────────
// Recarga de saldo
// ─────────────────────────────────────────────────────────
private function mostrarFormRecarga(): void
{
$botones = [];
foreach ([20000, 50000, 100000, 200000, 500000] as $m) {
$botones[] = ['label' => '$' . number_format($m), 'action' => 'recarga.elegir_metodo', 'data' => ['monto' => $m]];
}
$botones[] = ['label' => 'Otro valor', 'action' => 'recarga.otro_valor', 'data' => []];
$botones[] = ['label' => 'Volver', 'action' => 'menu.principal', 'data' => []];
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => "Selecciona el monto a recargar:",
'payload' => ['botones' => $botones],
'leido' => true,
]);
}
private function solicitarMontoPersonalizado(): void
{
$this->flujo = 'recarga.monto_custom';
$this->flujoData = [];
$this->guardarMensajeBot($this->convId, "Escribe el monto que deseas recargar (mínimo $1.000):");
}
private function elegirMetodoRecarga(int $monto): void
{
if ($monto <= 0) {
return;
}
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => "Recarga de $" . number_format($monto) . "\n¿Como quieres pagar?",
'payload' => ['botones' => [
['label' => 'MercadoPago', 'action' => 'recarga.mp', 'data' => ['monto' => $monto]],
['label' => 'Bre-B', 'action' => 'recarga.transferencia', 'data' => ['monto' => $monto]],
['label' => 'Volver', 'action' => 'recarga.iniciar', 'data' => []],
]],
'leido' => true,
]);
}
private function mostrarTransferencia(int $monto): void
{
if ($monto <= 0) {
return;
}
$banco = \App\Models\ChatConfig::get('recarga_banco_nombre', '');
$clave = \App\Models\ChatConfig::get('recarga_banco_llave', '');
$titular = \App\Models\ChatConfig::get('recarga_banco_titular', '');
if (! $clave) {
$this->guardarMensajeBot($this->convId, "El pago por Bre-B no esta disponible en este momento. Usa MercadoPago o contacta a un asesor.");
return;
}
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'banco',
'contenido' => 'Datos para transferencia:',
'payload' => [
'banco' => $banco,
'clave' => $clave,
'titular' => $titular,
'monto' => $monto,
],
'leido' => true,
]);
$this->guardarMensajeBot($this->convId, "Cuando hayas transferido, envia el comprobante usando el icono de camara (📷) y el sistema lo validara automaticamente.");
$this->volver('menu.principal');
}
private function generarLinkRecargaMP(int $monto): void
{
if ($monto <= 0) {
$this->guardarMensajeBot($this->convId, "Monto invalido.");
return;
}
$ref = 'REC-' . strtoupper(Str::random(12));
$url = $this->crearPreferenciaMP('Recarga de saldo', $monto, $ref);
if (! $url) {
$this->guardarMensajeBot($this->convId, "No se pudo generar el link de recarga. Intenta de nuevo o contacta a un asesor.");
return;
}
if ($this->userId) {
$user = User::with('saldo')->find($this->userId);
$saldo = $user->saldo ?? Saldo::create(['usuario_id' => $this->userId, 'valor' => 0]);
recarga::create([
'monto' => $monto,
'reference' => $ref,
'status' => 'pendiente_mp',
'usuario_id' => $this->userId,
'saldo_id' => $saldo->id,
]);
}
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'link',
'contenido' => "Tu link de recarga por $" . number_format($monto) . " esta listo. Despues de pagar, envia el comprobante aqui.",
'payload' => ['url' => $url, 'label' => 'Recargar $' . number_format($monto) . ' con MercadoPago', 'nota' => 'Ref: ' . $ref],
'leido' => true,
]);
$this->volver('menu.principal');
}
private function crearPreferenciaMP(string $titulo, int $valor, string $ref): ?string
{
$accessToken = config('services.mercadopago.token');
if (! $accessToken) {
return null;
}
try {
$response = \Illuminate\Support\Facades\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']],
],
]);
if ($response->successful()) {
return $response->json('init_point');
}
\Illuminate\Support\Facades\Log::warning('[ChatMP] Error creando preferencia: ' . $response->body());
return null;
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::warning('[ChatMP] Excepcion: ' . $e->getMessage());
return null;
}
}
// ─────────────────────────────────────────────────────────
// Credenciales
// ─────────────────────────────────────────────────────────
private function listarCredenciales(): void
{
if (! $this->userId) {
$this->guardarMensajeBot($this->convId, "No encontre tu cuenta. Contacta a un asesor para ver tus credenciales.");
$this->volver('menu.principal');
return;
}
$historiales = Historiale::where('cliente_id', $this->userId)
->where('estado', 'entregado')
->where('fecha_final', '>=', now())
->with('tarifa.servicio')
->orderBy('fecha_final', 'desc')
->limit(10)
->get();
if ($historiales->isEmpty()) {
$this->guardarMensajeBot($this->convId, "No tienes planes activos en este momento.");
$this->volver('menu.principal');
return;
}
$this->guardarMensajeBot($this->convId, "Tus planes activos:");
foreach ($historiales as $h) {
foreach ($h->cuentas as $c) {
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'credenciales',
'contenido' => '',
'payload' => [
'servicio' => $h->tarifa?->servicio?->nombre ?? 'Servicio',
'email' => $c->correo,
'password' => $c->password,
'perfil' => $h->tarifa?->pantallas ?? null,
'vence' => Carbon::parse($h->fecha_final)->format('d/m/Y'),
],
'leido' => true,
]);
}
}
$this->volver('menu.principal');
}
// ─────────────────────────────────────────────────────────
// Historial
// ─────────────────────────────────────────────────────────
private function verHistorial(): void
{
if (! $this->userId) {
$this->guardarMensajeBot($this->convId, "Inicia sesion para ver tu historial.");
$this->volver('menu.principal');
return;
}
$historiales = Historiale::where('cliente_id', $this->userId)
->with('tarifa.servicio')
->orderBy('created_at', 'desc')
->limit(10)
->get();
if ($historiales->isEmpty()) {
$this->guardarMensajeBot($this->convId, "No tienes compras registradas.");
$this->volver('menu.principal');
return;
}
$lineas = ["Tus ultimas compras:\n"];
foreach ($historiales as $h) {
$activo = Carbon::parse($h->fecha_final)->gte(now()) ? '[activo]' : '[vencido]';
$nombre = $h->tarifa?->servicio?->nombre ?? ($h->promocion_id ? 'Promo' : 'Plan');
$fecha = Carbon::parse($h->created_at)->format('d/m/Y');
$lineas[] = "{$activo} {$nombre} - $" . number_format($h->valor) . " ({$fecha})";
}
$this->guardarMensajeBot($this->convId, implode("\n", $lineas));
$this->volver('menu.principal');
}
// ─────────────────────────────────────────────────────────
// Perfil
// ─────────────────────────────────────────────────────────
private function verPerfil(): void
{
if (! $this->userId) {
$this->guardarMensajeBot($this->convId, "No encontre tu cuenta. Contacta a un asesor.");
$this->volver('menu.principal');
return;
}
$user = User::with('saldo')->find($this->userId);
$this->guardarMensajeBot(
$this->convId,
"Tu perfil\nNombre: {$user->name}\nEmail: {$user->email}\nSaldo: $" . number_format($user->saldo?->valor ?? 0)
);
$this->volver('menu.principal');
}
// ─────────────────────────────────────────────────────────
// Asesor
// ─────────────────────────────────────────────────────────
private function solicitarAsesor(): void
{
$conv = ChatConversation::find($this->convId);
if ($conv) {
$conv->update(['estado' => 'agente']);
$this->estadoConv = 'agente';
}
$this->guardarMensajeBot($this->convId, "Un asesor se comunicara contigo en breve. Por favor espera.");
}
// ─────────────────────────────────────────────────────────
// Aplicar recarga validada por IA
// ─────────────────────────────────────────────────────────
private function aplicarRecargaValidada(array $data): void
{
if (! $this->userId) {
$this->guardarMensajeBot($this->convId, "No puedo aplicar la recarga: no estas identificado.");
return;
}
$monto = (int) ($data['monto'] ?? 0);
if ($monto <= 0) {
$this->guardarMensajeBot($this->convId, "Monto invalido.");
return;
}
$user = User::with('saldo')->find($this->userId);
$saldo = $user->saldo ?? Saldo::create(['usuario_id' => $this->userId, 'valor' => 0]);
$nuevoSaldo = $saldo->valor + $monto;
$saldo->update(['valor' => $nuevoSaldo]);
$this->saldoUsuario = $nuevoSaldo;
$this->guardarMensajeBot(
$this->convId,
"Recarga de $" . number_format($monto) . " aplicada. Tu nuevo saldo: $" . number_format($nuevoSaldo) . "."
);
$this->volver('menu.principal');
}
// ─────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────
// Detección de intención: keywords primero, Gemini como fallback
// ─────────────────────────────────────────────────────────
private function detectarIntencion(string $texto): ?array
{
return $this->detectarIntencionKeyword($texto)
?? $this->detectarIntencionIA($texto);
}
private function detectarIntencionKeyword(string $texto): ?array
{
$t = mb_strtolower(trim($texto));
$patrones = [
// Recarga de saldo
'recarga.iniciar' => '/recarg|recargar|cargar\s*(saldo|cuenta|dinero|plata)|abonar|depositar|agregar\s*(saldo|dinero|plata)|quiero\s*poner\s*saldo/',
// Ver servicios / planes
'servicios.listar' => '/servicio|plan(es)?|cat[aá]logo|qu[eé]\s*(hay|tienen|tienes)|ver\s*plan|netflix|disney|hbo|prime|spotify|crunchyroll|paramount|streaming|aplicacion|aplicaci[oó]n/',
// Promociones
'promo.listar' => '/promo(ci[oó]n)?|oferta|descuento|especial/',
// Credenciales / acceso
'credenciales.listar' => '/credencial|contrase[ñn]a|clave|password|acceso|usuario|mi\s*cuenta|mis\s*datos\s*de\s*acceso|c[oó]mo\s*(entro|accedo|me\s*conecto)/',
// Historial de compras
'historial.ver' => '/historial|compra(s)?|pedido(s)?|lo\s*que\s*(compr[eé]|he\s*comprado)|mis\s*compras/',
// Perfil
'perfil.ver' => '/perfil|mis\s*datos|mi\s*informaci[oó]n|mis\s*datos\s*personales/',
// Asesor humano
'asesor.solicitar' => '/asesor|agente|persona|humano|soporte|ayuda\s*(de\s*una\s*persona)?|hablar\s*con\s*(alguien|una\s*persona|un\s*asesor)/',
// Menú / saludo
'menu.principal' => '/^(hola|buenas?|buenos?|hey|men[uú]|inicio|principal|volver|regresar|empezar|comenzar|start)[\s!.]*$/',
];
foreach ($patrones as $action => $pattern) {
if (preg_match($pattern, $t)) {
return ['action' => $action, 'data' => []];
}
}
return null;
}
private function detectarIntencionIA(string $texto): ?array
{
try {
return app(\App\Services\GeminiIntentService::class)->detectar($texto);
} catch (\Throwable) {
return null;
}
}
// ─────────────────────────────────────────────────────────
// Cargar mensajes
// ─────────────────────────────────────────────────────────
public function cargarMensajes(): void
{
if (! $this->convId) {
return;
}
$this->mensajes = ChatMessage::where('conversation_id', $this->convId)
->orderBy('id')
->get()
->map(fn ($m) => [
'tipo' => $m->tipo,
'tipo_ui' => $m->tipo_ui ?? 'text',
'contenido' => $m->contenido,
'payload' => $m->payload ?? [],
'created_at' => $m->created_at->format('H:i'),
])
->toArray();
$this->dispatchBrowserEvent('scroll-chat');
}
// Llamado por wire:poll — igual que cargarMensajes pero sin forzar scroll
public function pollMensajes(): void
{
if (! $this->convId) {
return;
}
$this->mensajes = ChatMessage::where('conversation_id', $this->convId)
->orderBy('id')
->get()
->map(fn ($m) => [
'tipo' => $m->tipo,
'tipo_ui' => $m->tipo_ui ?? 'text',
'contenido' => $m->contenido,
'payload' => $m->payload ?? [],
'created_at' => $m->created_at->format('H:i'),
])
->toArray();
// No dispatches scroll-chat — el MutationObserver del JS maneja el badge
}
// ─────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────
private function guardarMensajeBot(int $convId, string $contenido): void
{
ChatMessage::create([
'conversation_id' => $convId,
'tipo' => 'bot',
'tipo_ui' => 'text',
'contenido' => $contenido,
'leido' => true,
]);
}
private function guardarMensajeUsuario(int $convId, string $contenido): void
{
ChatMessage::create([
'conversation_id' => $convId,
'tipo' => 'usuario',
'tipo_ui' => 'text',
'contenido' => $contenido,
'leido' => false,
]);
}
private function contarDisponibles(Tarifas $tarifa): int
{
return CuentasHelper::contar($tarifa);
}
private function buscarCuentaDisponible(Tarifas $tarifa): ?Cuentas
{
return CuentasHelper::buscar($tarifa);
}
private function efectivePrice(int $tarifaId, ?int $userId, float $base): float
{
return CuentasHelper::precio($tarifaId, $userId, $base);
}
private function volver(string $action): void
{
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => '',
'payload' => ['botones' => [
['label' => 'Menu principal', 'action' => $action, 'data' => []],
]],
'leido' => true,
]);
}
public function render()
{
return view('livewire.chat.public-chat');
}
}