Files
sirpremiumv2/app/Http/Livewire/Chat/PublicChat.php
T
LizandroandClaude Sonnet 4.6 30fc8c0f1b fix: email mask shows max 4 chars instead of half the local part
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>
2026-07-14 19:34:14 +00:00

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');
}
}