Files
sirpremiumv2/app/Services/WhatsappBotService.php
T
2026-05-12 19:50:28 -05:00

339 lines
12 KiB
PHP

<?php
namespace App\Services;
use App\Models\WhatsappConversation;
use App\Models\WhatsappMenu;
use App\Models\WhatsappSystemConfig;
use App\Models\WhatsappUser;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class WhatsappBotService
{
private string $token;
private string $phoneNumberId;
private string $apiUrl;
public function __construct()
{
$this->token = WhatsappSystemConfig::get('whatsapp_token');
$this->phoneNumberId = WhatsappSystemConfig::get('phone_number_id');
$this->apiUrl = WhatsappSystemConfig::get('whatsapp_api_url', 'https://graph.facebook.com/v22.0/');
}
// ──────────────────────────────────────────────────────────
// Punto de entrada principal
// ──────────────────────────────────────────────────────────
public function handle(WhatsappUser $user, string $texto, string $tipo = 'text'): void
{
// Usuario bloqueado → ignorar
if ($user->status === 'blocked') {
return;
}
// Atendido por asesor → solo almacenar, no responder
if ($user->in_service) {
return;
}
// Bot deshabilitado para este usuario → ignorar
if (! $user->bot_enabled) {
return;
}
// Bot deshabilitado globalmente → ignorar
if (WhatsappSystemConfig::get('bot_enabled', '1') !== '1') {
return;
}
// Medios (imagen, video, audio, documento, etc.) → aviso y salir
if (! in_array($tipo, ['text', 'interactive'])) {
$msg = 'Recibimos tu ' . $tipo . '. Un asesor te contactará pronto.';
$this->saveOutgoing($user->id, $msg);
$this->sendText($user->phone_number, $msg);
return;
}
$texto = trim($texto);
// ── Comandos especiales ──────────────────────────────
if ($this->isMenuCommand($texto)) {
$this->enviarMenuPrincipal($user);
return;
}
if ($this->isBackCommand($texto)) {
$this->irAtras($user);
return;
}
if ($this->isAdvisorCommand($texto)) {
$this->transferirAsesor($user);
return;
}
// ── Primera visita → bienvenida ──────────────────────
if (! $user->welcome_sent_at) {
$this->enviarBienvenida($user);
return;
}
// ── Navegación por menú ──────────────────────────────
$this->procesarSeleccion($user, $texto);
}
// ──────────────────────────────────────────────────────────
// Comandos
// ──────────────────────────────────────────────────────────
private function isMenuCommand(string $texto): bool
{
return in_array(mb_strtolower($texto), ['menu', 'menú', 'inicio', 'start', 'hola', 'hi']);
}
private function isBackCommand(string $texto): bool
{
return in_array(mb_strtolower($texto), ['atras', 'atrás', 'volver', 'back', 'regresar']);
}
private function isAdvisorCommand(string $texto): bool
{
return in_array(mb_strtolower($texto), ['asesor', 'agente', 'humano', 'ayuda']);
}
// ──────────────────────────────────────────────────────────
// Flujos de respuesta
// ──────────────────────────────────────────────────────────
private function enviarBienvenida(WhatsappUser $user): void
{
$msg = WhatsappSystemConfig::get(
'welcome_message',
'¡Hola! Bienvenido. Escribe *menu* para ver las opciones disponibles.'
);
$user->update(['welcome_sent_at' => now(), 'current_menu_id' => null]);
$this->saveOutgoing($user->id, $msg);
$this->sendText($user->phone_number, $msg);
// Mostrar menú principal después del saludo
$this->enviarMenuPrincipal($user);
}
private function enviarMenuPrincipal(WhatsappUser $user): void
{
$menu = WhatsappMenu::where('is_main', 1)->where('is_active', 1)->first();
if (! $menu) {
$msg = WhatsappSystemConfig::get('default_no_match', 'No hay opciones disponibles en este momento.');
$this->saveOutgoing($user->id, $msg);
$this->sendText($user->phone_number, $msg);
return;
}
$user->update(['current_menu_id' => $menu->id]);
$texto = $this->formatMenu($menu);
$this->saveOutgoing($user->id, $texto);
$this->sendText($user->phone_number, $texto);
}
private function irAtras(WhatsappUser $user): void
{
$menuActual = $user->current_menu_id
? WhatsappMenu::find($user->current_menu_id)
: null;
$menuPadre = ($menuActual && $menuActual->parent_id)
? WhatsappMenu::find($menuActual->parent_id)
: null;
if ($menuPadre && $menuPadre->is_active) {
$user->update(['current_menu_id' => $menuPadre->id]);
$texto = $this->formatMenu($menuPadre);
$this->saveOutgoing($user->id, $texto);
$this->sendText($user->phone_number, $texto);
} else {
$this->enviarMenuPrincipal($user);
}
}
private function transferirAsesor(WhatsappUser $user): void
{
$msg = WhatsappSystemConfig::get(
'advisor_message',
'Un asesor se comunicará contigo en breve. Por favor espera.'
);
$user->update([
'advisor_requested' => true,
'in_service' => false,
'current_menu_id' => null,
]);
$this->saveOutgoing($user->id, $msg);
$this->sendText($user->phone_number, $msg);
}
private function procesarSeleccion(WhatsappUser $user, string $texto): void
{
if (! $user->current_menu_id) {
$this->enviarMenuPrincipal($user);
return;
}
$menu = WhatsappMenu::find($user->current_menu_id);
if (! $menu || ! $menu->is_active) {
$this->enviarMenuPrincipal($user);
return;
}
// Solo aceptar selección numérica
$numero = is_numeric($texto) ? (int) $texto : null;
$opcion = $numero
? $menu->options()->where('option_number', $numero)->where('is_active', 1)->first()
: null;
if (! $opcion) {
$noMatch = WhatsappSystemConfig::get('default_no_match', 'Opción no reconocida. Escribe el número de la opción deseada.');
$fullMsg = $noMatch . "\n\n" . $this->formatMenu($menu);
$this->saveOutgoing($user->id, $fullMsg);
$this->sendText($user->phone_number, $fullMsg);
return;
}
match ($opcion->action_type) {
'submenu' => $this->irASubmenu($user, $opcion),
'message' => $this->responderMensaje($user, $opcion),
'advisor' => $this->transferirAsesor($user),
'template' => $this->enviarPlantilla($user, $opcion),
default => $this->responderMensaje($user, $opcion),
};
}
private function irASubmenu(WhatsappUser $user, object $opcion): void
{
$subMenu = WhatsappMenu::find($opcion->action_value);
if (! $subMenu || ! $subMenu->is_active) {
$msg = WhatsappSystemConfig::get('default_no_match', 'Opción no disponible en este momento.');
$this->saveOutgoing($user->id, $msg);
$this->sendText($user->phone_number, $msg);
return;
}
$user->update(['current_menu_id' => $subMenu->id]);
$texto = $this->formatMenu($subMenu);
$this->saveOutgoing($user->id, $texto);
$this->sendText($user->phone_number, $texto);
}
private function responderMensaje(WhatsappUser $user, object $opcion): void
{
$texto = (string) ($opcion->action_value ?? '');
$this->saveOutgoing($user->id, $texto);
$this->sendText($user->phone_number, $texto);
// Volver al menú principal tras la respuesta
$menuPrincipal = WhatsappMenu::where('is_main', 1)->where('is_active', 1)->first();
if ($menuPrincipal) {
$user->update(['current_menu_id' => $menuPrincipal->id]);
$textoMenu = $this->formatMenu($menuPrincipal);
$this->saveOutgoing($user->id, $textoMenu);
$this->sendText($user->phone_number, $textoMenu);
}
}
private function enviarPlantilla(WhatsappUser $user, object $opcion): void
{
$templateName = (string) ($opcion->action_value ?? '');
$payload = [
'messaging_product' => 'whatsapp',
'to' => $user->phone_number,
'type' => 'template',
'template' => [
'name' => $templateName,
'language' => ['code' => 'es'],
],
];
$this->saveOutgoing($user->id, "[Plantilla: {$templateName}]");
$this->callApi('messages', $payload);
}
// ──────────────────────────────────────────────────────────
// Helpers de formato
// ──────────────────────────────────────────────────────────
private function formatMenu(WhatsappMenu $menu): string
{
$lines = [$menu->message];
foreach ($menu->options()->where('is_active', 1)->get() as $op) {
$lines[] = "{$op->option_number}. {$op->title}";
}
return implode("\n", $lines);
}
// ──────────────────────────────────────────────────────────
// Envío a la API de WhatsApp Cloud
// ──────────────────────────────────────────────────────────
public function sendText(string $to, string $body): bool
{
return $this->callApi('messages', [
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'text',
'text' => ['body' => $body, 'preview_url' => false],
]);
}
public function markRead(string $messageId): void
{
$this->callApi('messages', [
'messaging_product' => 'whatsapp',
'status' => 'read',
'message_id' => $messageId,
]);
}
private function callApi(string $endpoint, array $payload): bool
{
if (! $this->token || ! $this->phoneNumberId) {
Log::warning('[WhatsApp] Token o phone_number_id no configurados.');
return false;
}
try {
$response = Http::withToken($this->token)
->timeout(10)
->post("{$this->apiUrl}{$this->phoneNumberId}/{$endpoint}", $payload);
if (! $response->successful()) {
Log::error('[WhatsApp] API error: ' . $response->body());
}
return $response->successful();
} catch (\Throwable $e) {
Log::error('[WhatsApp] Excepción al llamar API: ' . $e->getMessage());
return false;
}
}
private function saveOutgoing(int $userId, string $content): void
{
WhatsappConversation::create([
'user_id' => $userId,
'direction' => 'outgoing',
'message_type' => 'text',
'content' => $content,
'status' => 'sent',
]);
}
}