Fix "sin cuentas disponibles" and wrong pricing in bot/chat

Three bugs fixed:

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

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro
2026-07-18 16:02:54 +00:00
co-authored by Claude Sonnet 4.6
parent ba4ec8b6cf
commit 1f25c31603
3 changed files with 153 additions and 113 deletions
+4 -93
View File
@@ -13,6 +13,7 @@ use App\Models\Historiale;
use App\Models\Historial_cuenta;
use App\Models\Preferencial;
use App\Models\Promociones;
use App\Services\CuentasHelper;
use App\Models\TarifaPromo;
use App\Models\Usuario_promo;
use App\Models\recarga;
@@ -1411,107 +1412,17 @@ class PublicChat extends Component
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);
return CuentasHelper::contar($tarifa);
}
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;
return CuentasHelper::buscar($tarifa);
}
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;
return CuentasHelper::precio($tarifaId, $userId, $base);
}
private function volver(string $action): void
+136
View File
@@ -0,0 +1,136 @@
<?php
namespace App\Services;
use App\Models\Cuentas;
use App\Models\Historial_cuenta;
use App\Models\Preferencial;
use App\Models\Tarifas;
use App\Models\Usuario_tarifa;
class CuentasHelper
{
/**
* Cuenta cuentas disponibles para una tarifa, igual que la web.
*/
public static function contar(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');
// Cuenta completa
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();
}
// Cuenta por pantalla
$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);
}
/**
* Busca la primera cuenta disponible para una tarifa.
*/
public static function buscar(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');
// Cuenta completa
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();
}
// Cuenta por pantalla — intento normal
$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();
// Segundo intento sin filtro de inicio
if (! $cuenta) {
$baseQ2 = Cuentas::select('*')
->where('servicio_id', $servicio->id)
->whereDate('vencimiento', '>=', $fechaMin)
->whereDate('vencimiento', '<=', $fechaMax)
->where('estado', 'activo')
->withCount('historiales')
->orderBy('inicio', 'ASC');
$cuenta = Cuentas::fromSub($baseQ2, 'alias')
->where('historiales_count', '<=', $suma)
->orderBy('inicio', 'ASC')
->lockForUpdate()
->first();
}
return $cuenta;
}
/**
* Precio efectivo para un usuario y tarifa.
* Prioridad: Usuario_tarifa (web) Preferencial (legacy) precio base.
*/
public static function precio(int $tarifaId, ?int $userId, float $base): float
{
if ($userId) {
$ut = Usuario_tarifa::where('usuario_id', $userId)->where('tarifa_id', $tarifaId)->first();
if ($ut) {
return (float) $ut->precio;
}
$pref = Preferencial::where('usuario_id', $userId)->where('tarifa_id', $tarifaId)->first();
if ($pref) {
return (float) $pref->valor;
}
}
return (float) $base;
}
}
+13 -20
View File
@@ -13,6 +13,7 @@ use App\Models\Historiale;
use App\Models\Historial_cuenta;
use App\Models\Preferencial;
use App\Models\Promociones;
use App\Services\CuentasHelper;
use App\Models\TarifaPromo;
use App\Models\Usuario_promo;
use App\Models\recarga;
@@ -649,7 +650,7 @@ class TelegramBotService
$buttons = [];
foreach ($tarifas as $t) {
$precio = $this->efectivePrice($t->id, $state['user_id'], $t->valor);
$precio = CuentasHelper::precio($t->id, $state['user_id'], $t->valor);
$label = $servicio->por_tiempo
? "{$t->dias} días - \$" . number_format($precio)
: "{$t->pantallas} pantalla(s) / {$t->dias} días - \$" . number_format($precio);
@@ -672,8 +673,7 @@ class TelegramBotService
return;
}
$disponibles = Cuentas::where('tarifa_id', $tarifaId)->where('estado', 'activo')->count();
if ($disponibles < 1) {
if (CuentasHelper::contar($tarifa) < 1) {
$this->send($chatId, "Sin cuentas disponibles para este plan. Intenta con otro.");
$this->listServices($chatId);
return;
@@ -681,7 +681,7 @@ class TelegramBotService
$user = User::with('saldo')->find($state['user_id']);
$saldo = $user->saldo?->valor ?? 0;
$valor = $this->efectivePrice($tarifaId, $state['user_id'], $tarifa->valor);
$valor = CuentasHelper::precio($tarifaId, $state['user_id'], $tarifa->valor);
$state['data'] = ['flow' => 'compra', 'tarifa_id' => $tarifaId, 'valor' => $valor, 'servicio' => $tarifa->servicio->nombre, 'servicio_id' => $tarifa->servicio_id];
$this->setState($chatId, $state);
@@ -722,13 +722,15 @@ class TelegramBotService
return;
}
$saldo = $user->saldo;
if (! $saldo || $saldo->valor < $tarifa->valor) {
$this->send($chatId, "Saldo insuficiente (\$" . number_format($saldo?->valor ?? 0) . "). El plan cuesta \$" . number_format($tarifa->valor) . ".");
$precio = (float) ($data['valor'] ?? CuentasHelper::precio($tarifa->id, $state['user_id'], $tarifa->valor));
$saldo = $user->saldo;
if (! $saldo || $saldo->valor < $precio) {
$this->send($chatId, "Saldo insuficiente (\$" . number_format($saldo?->valor ?? 0) . "). El plan cuesta \$" . number_format($precio) . ".");
return;
}
$cuenta = Cuentas::where('tarifa_id', $tarifa->id)->where('estado', 'activo')->first();
$cuenta = CuentasHelper::buscar($tarifa);
if (! $cuenta) {
$this->send($chatId, "No hay cuentas disponibles en este momento. Intenta más tarde.");
return;
@@ -737,7 +739,7 @@ class TelegramBotService
$historial = Historiale::create([
'fecha_inicio' => now(),
'fecha_final' => now()->addDays($tarifa->dias),
'valor' => $tarifa->valor,
'valor' => $precio,
'utilidad' => $tarifa->utilidad ?? 0,
'tipo_pago' => 'saldo',
'estado' => 'entregado',
@@ -749,7 +751,7 @@ class TelegramBotService
Historial_cuenta::create(['historial_id' => $historial->id, 'cuenta_id' => $cuenta->id]);
$cuenta->update(['estado' => 'ocupado']);
$nuevoSaldo = $saldo->valor - $tarifa->valor;
$nuevoSaldo = $saldo->valor - $precio;
$saldo->update(['valor' => $nuevoSaldo]);
$text = "✅ *¡Compra exitosa!*\n\n";
@@ -760,6 +762,7 @@ class TelegramBotService
$text .= "📺 Perfil: {$tarifa->pantallas}\n";
}
$text .= "📅 Vence: " . now()->addDays($tarifa->dias)->format('d/m/Y') . "\n";
$text .= "💰 Pagado: *\$" . number_format($precio) . "*\n";
$text .= "\nSaldo restante: *\$" . number_format($nuevoSaldo) . "*";
$this->send($chatId, $text);
@@ -1395,16 +1398,6 @@ class TelegramBotService
// ─── Helpers ──────────────────────────────────────────────
private function efectivePrice(int $tarifaId, ?int $userId, float $base): float
{
if ($userId) {
$pref = Preferencial::where('usuario_id', $userId)->where('tarifa_id', $tarifaId)->first();
if ($pref) {
return (float) $pref->valor;
}
}
return (float) $base;
}
private function maskEmail(string $email): string
{