lizandro.guarnizo (17 chars) was showing 'lizandro.' (9 chars including the dot) because ceil(17/2)=9. Now always shows exactly min(4, len) chars: 'liza*************@domain' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1536 lines
63 KiB
PHP
Executable File
1536 lines
63 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\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
|
|
{
|
|
$servicio = $tarifa->servicio;
|
|
$fechaBase = now()->subDays(8)->format('Y-m-d');
|
|
$fechaMin = now()->addDays($tarifa->dias - 15)->format('Y-m-d');
|
|
$fechaMax = now()->addDays($tarifa->dias + 15)->format('Y-m-d');
|
|
|
|
if ((int) $tarifa->pantallas === (int) $servicio->completa) {
|
|
return Cuentas::where('servicio_id', $servicio->id)
|
|
->whereDate('vencimiento', '>=', $fechaMin)
|
|
->whereDate('vencimiento', '<=', $fechaMax)
|
|
->where('estado', 'pendiente')
|
|
->doesntHave('perfil')
|
|
->count();
|
|
}
|
|
|
|
$suma = $servicio->pantallas - $tarifa->pantallas;
|
|
$baseQ = Cuentas::select('*')
|
|
->where('servicio_id', $servicio->id)
|
|
->whereDate('inicio', '>=', $fechaBase)
|
|
->whereDate('vencimiento', '>=', $fechaMin)
|
|
->whereDate('vencimiento', '<=', $fechaMax)
|
|
->where('estado', 'activo')
|
|
->withCount('historiales');
|
|
|
|
$cuentas = Cuentas::fromSub($baseQ, 'alias')
|
|
->where('historiales_count', '<=', $suma)
|
|
->get();
|
|
|
|
if ($cuentas->isEmpty()) {
|
|
return 0;
|
|
}
|
|
|
|
$tomadas = Historial_cuenta::whereIn('cuenta_id', $cuentas->pluck('id'))
|
|
->whereNotNull('historial_id')
|
|
->count();
|
|
|
|
return (int) floor((($cuentas->count() * $servicio->pantallas) - $tomadas) / $tarifa->pantallas);
|
|
}
|
|
|
|
private function buscarCuentaDisponible(Tarifas $tarifa): ?Cuentas
|
|
{
|
|
$servicio = $tarifa->servicio;
|
|
$fechaBase = now()->subDays(8)->format('Y-m-d');
|
|
$fechaMin = now()->addDays($tarifa->dias - 15)->format('Y-m-d');
|
|
$fechaMax = now()->addDays($tarifa->dias + 15)->format('Y-m-d');
|
|
|
|
if ((int) $tarifa->pantallas === (int) $servicio->completa) {
|
|
return Cuentas::where('servicio_id', $servicio->id)
|
|
->whereDate('vencimiento', '>=', $fechaMin)
|
|
->whereDate('vencimiento', '<=', $fechaMax)
|
|
->where('estado', 'pendiente')
|
|
->doesntHave('perfil')
|
|
->orderBy('inicio', 'ASC')
|
|
->first();
|
|
}
|
|
|
|
$suma = $servicio->pantallas - $tarifa->pantallas;
|
|
$baseQ = Cuentas::select('*')
|
|
->where('servicio_id', $servicio->id)
|
|
->whereDate('inicio', '>=', $fechaBase)
|
|
->whereDate('vencimiento', '>=', $fechaMin)
|
|
->whereDate('vencimiento', '<=', $fechaMax)
|
|
->where('estado', 'activo')
|
|
->withCount('historiales')
|
|
->orderBy('inicio', 'ASC');
|
|
|
|
$cuenta = Cuentas::fromSub($baseQ, 'alias')
|
|
->where('historiales_count', '<=', $suma)
|
|
->orderBy('inicio', 'ASC')
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if (! $cuenta) {
|
|
$suma2 = $servicio->pantallas - $tarifa->pantallas;
|
|
$baseQ2 = Cuentas::select('*')
|
|
->where('servicio_id', $servicio->id)
|
|
->whereDate('inicio', '>=', $fechaBase)
|
|
->whereDate('vencimiento', '>=', $fechaMin)
|
|
->whereDate('vencimiento', '<=', $fechaMax)
|
|
->withCount('historiales')
|
|
->where('estado', 'activo')
|
|
->orderBy('inicio', 'ASC');
|
|
|
|
$cuenta = Cuentas::fromSub($baseQ2, 'alias')
|
|
->where('historiales_count', '<=', $suma2)
|
|
->orderBy('inicio', 'ASC')
|
|
->lockForUpdate()
|
|
->first();
|
|
}
|
|
|
|
return $cuenta;
|
|
}
|
|
|
|
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 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');
|
|
}
|
|
}
|