feat: Telegram transactional flows + role pricing + services slider

- New TelegramBotService: full OTP auth, registration, main menu with
  inline keyboards, buy plans, buy promos, recharge (MP + Bre-B),
  credentials, history, profile, agent transfer, photo receipt analysis
- TelegramWebhookController: now handles text, callback_query and photo
- Role-based pricing: tarifas filtered by user rol_id in both web chat
  and Telegram; Preferencial price overrides base tarifa valor
- Services and promos now render as horizontal scroll slider
  (cards_slider tipo_ui) instead of stacked individual cards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro
2026-07-14 04:36:03 +00:00
co-authored by Claude Sonnet 4.6
parent a3bd91c204
commit 2fc2aba8a3
4 changed files with 1262 additions and 59 deletions
+62 -45
View File
@@ -10,6 +10,7 @@ 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\recarga;
use App\Models\Saldo;
@@ -495,7 +496,11 @@ class PublicChat extends Component
private function listarServicios(): void
{
$servicios = Servicio::where('estado', 'activo')->orderBy('ubicacion')->get();
$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.");
@@ -503,36 +508,36 @@ class PublicChat extends Component
return;
}
$this->guardarMensajeBot($this->convId, "Selecciona el servicio:");
$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();
foreach ($servicios as $s) {
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'card',
'contenido' => '',
'payload' => [
'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]],
],
'leido' => true,
]);
}
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
{
$servicio = Servicio::with('tarifas')->find($id);
$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->where('estado', 'activo');
$tarifas = $servicio->tarifas;
if ($tarifas->isEmpty()) {
$this->guardarMensajeBot($this->convId, "No hay planes para {$servicio->nombre} en este momento.");
@@ -542,9 +547,10 @@ class PublicChat extends Component
$botones = [];
foreach ($tarifas as $t) {
$label = $servicio->por_tiempo
? "{$t->dias} dias - $" . number_format($t->valor)
: "{$t->pantallas} pantalla(s) / {$t->dias} dias - $" . number_format($t->valor);
$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' => []];
@@ -578,10 +584,12 @@ class PublicChat extends Component
return;
}
$precio = $this->efectivePrice($tarifaId, $this->userId, $tarifa->valor);
$this->flujo = 'compra';
$this->flujoData = [
'tarifa_id' => $tarifaId,
'valor' => $tarifa->valor,
'valor' => $precio,
'servicio' => $tarifa->servicio->nombre,
'dias' => $tarifa->dias,
'pantallas' => $tarifa->pantallas,
@@ -590,15 +598,15 @@ class PublicChat extends Component
$detalle = $tarifa->servicio->por_tiempo
? "{$tarifa->dias} dias"
: "{$tarifa->pantallas} pantalla(s) / {$tarifa->dias} dias";
$saldoInsuficiente = ! $this->userId || ($this->saldoUsuario < $tarifa->valor);
$saldoInsuficiente = ! $this->userId || ($this->saldoUsuario < $precio);
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => "{$tarifa->servicio->nombre} - {$detalle}\nPrecio: $" . number_format($tarifa->valor) . "\n\nElige como pagar:",
'contenido' => "{$tarifa->servicio->nombre} - {$detalle}\nPrecio: $" . number_format($precio) . "\n\nElige como pagar:",
'payload' => ['botones' => [
['label' => 'Saldo ($' . number_format($tarifa->valor) . ')',
['label' => 'Saldo ($' . number_format($precio) . ')',
'action' => 'compra.pagar.saldo',
'data' => [],
'disabled' => $saldoInsuficiente],
@@ -716,7 +724,7 @@ class PublicChat extends Component
private function listarPromociones(): void
{
$promos = Promociones::where('visible', true)
->where(fn($q) => $q->whereNull('fecha_limite')->orWhere('fecha_limite', '>=', now()))
->where(fn ($q) => $q->whereNull('fecha_limite')->orWhere('fecha_limite', '>=', now()))
->get();
if ($promos->isEmpty()) {
@@ -725,25 +733,23 @@ class PublicChat extends Component
return;
}
$this->guardarMensajeBot($this->convId, "Promociones disponibles:");
$cards = $promos->map(fn ($p) => [
'imagen' => $p->img_publicidad,
'titulo' => $p->nombre ?? 'Promocion especial',
'descripcion' => $p->descripcion ?? null,
'precio' => $p->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();
foreach ($promos as $p) {
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'card',
'contenido' => '',
'payload' => [
'imagen' => $p->img_publicidad,
'titulo' => $p->nombre ?? 'Promocion especial',
'descripcion' => $p->descripcion ?? null,
'precio' => $p->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]],
],
'leido' => true,
]);
}
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'cards_slider',
'contenido' => 'Promociones disponibles:',
'payload' => ['cards' => $cards],
'leido' => true,
]);
$this->volver('menu.principal');
}
@@ -1233,6 +1239,17 @@ class PublicChat extends Component
]);
}
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([