339 lines
12 KiB
PHP
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',
|
|
]);
|
|
}
|
|
}
|