From b0fa3f4fdeef9e80fe77d68b8f432ede48b56092 Mon Sep 17 00:00:00 2001 From: nickpons666 Date: Sun, 19 Apr 2026 20:05:41 -0600 Subject: [PATCH] =?UTF-8?q?Debug:=20A=C3=B1adido=20log=20al=20webhook=20y?= =?UTF-8?q?=20corregida=20excepcion=20CSRF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/TelegramController.php | 1 + app/Models/TelegramAccount.php | 3 + app/Services/TelegramBotService.php | 434 +++++++----------- bootstrap/app.php | 3 +- ...d_bot_state_to_telegram_accounts_table.php | 29 ++ 5 files changed, 213 insertions(+), 257 deletions(-) create mode 100644 database/migrations/2026_04_20_014821_add_bot_state_to_telegram_accounts_table.php diff --git a/app/Http/Controllers/TelegramController.php b/app/Http/Controllers/TelegramController.php index 26783b0..9aa6497 100755 --- a/app/Http/Controllers/TelegramController.php +++ b/app/Http/Controllers/TelegramController.php @@ -95,6 +95,7 @@ class TelegramController extends Controller */ public function webhook(Request $request) { + Log::debug('>>> Webhook hit detected <<<'); try { $update = $request->all(); diff --git a/app/Models/TelegramAccount.php b/app/Models/TelegramAccount.php index cc37941..4bcf511 100755 --- a/app/Models/TelegramAccount.php +++ b/app/Models/TelegramAccount.php @@ -24,6 +24,8 @@ class TelegramAccount extends Model 'chat_id', 'verification_code', 'is_verified', + 'bot_state', + 'bot_data', ]; /** @@ -35,6 +37,7 @@ class TelegramAccount extends Model { return [ 'is_verified' => 'boolean', + 'bot_data' => 'array', ]; } diff --git a/app/Services/TelegramBotService.php b/app/Services/TelegramBotService.php index ef97c00..8f74e5a 100755 --- a/app/Services/TelegramBotService.php +++ b/app/Services/TelegramBotService.php @@ -18,9 +18,6 @@ class TelegramBotService $this->webhookUrl = rtrim(config('app.url'), '/') . '/telegram/webhook'; } - /** - * Procesar actualización recibida del webhook - */ public function handleUpdate(array $update): array { if (!isset($update['message'])) { @@ -30,308 +27,233 @@ class TelegramBotService $message = $update['message']; $chatId = $message['chat']['id']; $text = $message['text'] ?? ''; - $from = $message['from'] ?? []; - Log::info('Telegram update received', [ - 'chat_id' => $chatId, - 'text' => $text, - 'from' => $from - ]); + Log::info('Telegram update received', ['chat_id' => $chatId, 'text' => $text]); - // Verificar si el usuario está verificado $telegramAccount = TelegramAccount::where('chat_id', $chatId)->first(); if (!$telegramAccount || !$telegramAccount->is_verified) { return $this->handleUnverifiedUser($chatId, $text); } - // Procesar comandos del usuario verificado - return $this->handleCommand($telegramAccount->user, $text, $chatId); + if ($telegramAccount->bot_state) { + return $this->handleBotState($telegramAccount, $text, $chatId); + } + + return $this->handleCommand($telegramAccount, $text, $chatId); } - /** - * Manejar usuario no verificado - */ private function handleUnverifiedUser(string $chatId, string $text): array { - // Si es un código de verificación (6 dígitos numéricos) if (strlen($text) === 6 && is_numeric($text)) { - // Buscamos la cuenta que tiene este código de verificación y no está verificada $telegramAccount = TelegramAccount::where('verification_code', $text) ->where('is_verified', false) ->first(); if ($telegramAccount) { - // Actualizar la cuenta con el chat_id del usuario que mandó el código - $telegramAccount->update([ - 'chat_id' => $chatId, - 'is_verified' => true, - 'verification_code' => null - ]); - - $user = $telegramAccount->user; - $this->sendMessage($chatId, "¡Hola {$user->name}! 👋\n\nVerificación exitosa. Tu cuenta de Telegram ha sido vinculada correctamente. Ahora puedes usar comandos como /resumen para ver tu estado."); - + $telegramAccount->update(['chat_id' => $chatId, 'is_verified' => true, 'verification_code' => null]); + $this->sendMenu($chatId, "¡Hola {$telegramAccount->user->name}! 👋\n\nVerificación exitosa. He activado tu menú de control:"); return ['ok' => true, 'verified' => true]; - } else { - $this->sendMessage($chatId, "❌ El código $text es inválido o ya ha sido usado. Por favor, genera un código nuevo en tu panel de usuario."); - return ['ok' => true, 'verified' => false]; } } - // Mensaje de bienvenida para usuarios no verificados - $this->sendMessage($chatId, "👋 ¡Hola! Soy el bot de Nómina Pegaso.\n\nPara usar este bot, primero debes vincular tu cuenta:\n\n1️⃣ Ve a tu panel web\n2️⃣ Sección Telegram -> Vincular\n3️⃣ Envía aquí el código de 6 dígitos que veas allá."); - + $this->sendMessage($chatId, "👋 ¡Hola! Soy el bot de Nómina Pegaso.\n\nPara empezar, envía el código de 6 dígitos de tu panel web."); return ['ok' => true, 'verified' => false]; } - /** - * Manejar comandos de usuario verificado - */ - private function handleCommand(User $user, string $text, string $chatId): array + private function handleBotState(TelegramAccount $account, string $text, string $chatId): array { - $command = strtolower(trim($text)); - $commandParts = explode(' ', $command); - $mainCommand = $commandParts[0] ?? ''; + $state = $account->bot_state; + $data = $account->bot_data ?? []; - switch ($mainCommand) { - case '/start': - $this->sendMessage($chatId, "¡Hola {$user->name}! Usa /help para ver los comandos disponibles."); - break; - - case '/help': - $this->sendHelp($chatId); - break; - - case '/mes': - $this->showCurrentMonth($user, $chatId); - break; - - case '/ventas': - $this->showSales($user, $chatId); - break; - - case '/gastos': - $this->showExpenses($user, $chatId); - break; - - case '/resumen': - $this->showSummary($user, $chatId); - break; - - default: - $this->sendMessage($chatId, "Comando no reconocido. Usa /help para ver los comandos disponibles."); + if (strtolower(trim($text)) === '/cancelar' || $text === '❌ Cancelar') { + $account->update(['bot_state' => null, 'bot_data' => null]); + $this->sendMenu($chatId, "Operación cancelada."); + return ['ok' => true]; } + switch ($state) { + case 'AWAITING_SALE_DATE': + case 'AWAITING_EXPENSE_DATE': + $data['date'] = $this->parseDate($text); + $nextState = ($state === 'AWAITING_SALE_DATE') ? 'AWAITING_SALE_USER_AMOUNT' : 'AWAITING_EXPENSE_AMOUNT'; + $nextMsg = ($state === 'AWAITING_SALE_DATE') ? "💰 Monto vendido por *usuario*:" : "💸 Monto del gasto:"; + $account->update(['bot_state' => $nextState, 'bot_data' => $data]); + $this->sendMessage($chatId, $nextMsg, $this->cancelKeyboard()); + break; + + case 'AWAITING_SALE_USER_AMOUNT': + if (!is_numeric($text)) return $this->sendInvalidNumeric($chatId); + $data['user_sales'] = (float) $text; + $account->update(['bot_state' => 'AWAITING_SALE_SYSTEM_AMOUNT', 'bot_data' => $data]); + $this->sendMessage($chatId, "🖥️ Monto vendido según *sistema*:", $this->cancelKeyboard()); + break; + + case 'AWAITING_SALE_SYSTEM_AMOUNT': + if (!is_numeric($text)) return $this->sendInvalidNumeric($chatId); + $data['system_sales'] = (float) $text; + $month = $account->user->getCurrentMonth(); + if (!$month) return $this->sendNoOpenMonth($chatId, $account); + + $month->dailySales()->create(['date' => $data['date'], 'user_sales' => $data['user_sales'], 'system_sales' => $data['system_sales']]); + $account->update(['bot_state' => null, 'bot_data' => null]); + $this->sendMenu($chatId, "✅ *Venta registrada!*\n\n📅 {$data['date']}\n👤 U: $" . number_format($data['user_sales'], 2) . "\n🖥️ S: $" . number_format($data['system_sales'], 2)); + break; + + case 'AWAITING_EXPENSE_AMOUNT': + if (!is_numeric($text)) return $this->sendInvalidNumeric($chatId); + $data['amount'] = (float) $text; + $account->update(['bot_state' => 'AWAITING_EXPENSE_DESC', 'bot_data' => $data]); + $this->sendMessage($chatId, "📝 Describe el gasto:", $this->cancelKeyboard()); + break; + + case 'AWAITING_EXPENSE_DESC': + $data['description'] = $text; + $month = $account->user->getCurrentMonth(); + if (!$month) return $this->sendNoOpenMonth($chatId, $account); + + $month->expenses()->create(['date' => $data['date'], 'amount' => $data['amount'], 'description' => $data['description'], 'type' => 'other']); + $account->update(['bot_state' => null, 'bot_data' => null]); + $this->sendMenu($chatId, "✅ *Gasto registrado!*\n\n📅 {$data['date']}\n💰 $" . number_format($data['amount'], 2) . "\n📝 {$data['description']}"); + break; + } return ['ok' => true]; } - /** - * Enviar mensaje - */ - public function sendMessage(string $chatId, string $text): array + private function handleCommand(TelegramAccount $account, string $text, string $chatId): array { - if (!$this->botToken) { - Log::warning('Telegram bot token not configured'); - return ['ok' => false, 'error' => 'Bot token not configured']; + $cmd = strtolower(trim($text)); + $user = $account->user; + + switch ($cmd) { + case '/start': + case '/menu': + case '🏠 menú': + $this->sendMenu($chatId, "Control de Nómina - {$user->name}"); + break; + case '/resumen': + case '💵 resumen': + $this->showSummary($user, $chatId); + break; + case '/agregar_venta': + case '➕ agregar venta': + $account->update(['bot_state' => 'AWAITING_SALE_DATE']); + $this->sendMessage($chatId, "🗓️ ¿Fecha de la venta?", $this->dateKeyboard()); + break; + case '/agregar_gasto': + case '➖ agregar gasto': + $account->update(['bot_state' => 'AWAITING_EXPENSE_DATE']); + $this->sendMessage($chatId, "🗓️ ¿Fecha del gasto?", $this->dateKeyboard()); + break; + case '/ventas': + case '💰 mis ventas': + $this->showSales($user, $chatId); + break; + case '/gastos': + case '📝 mis gastos': + $this->showExpenses($user, $chatId); + break; + case '/help': + $this->sendMenu($chatId, "Usa los botones del teclado para navegar o enviar comandos."); + break; + default: + $this->sendMenu($chatId, "❓ No entiendo ese comando."); } + return ['ok' => true]; + } + + private function sendMenu(string $chatId, string $text) + { + $keyboard = [ + 'keyboard' => [ + [['text' => '💵 Resumen'], ['text' => '🏠 Menú']], + [['text' => '➕ Agregar Venta'], ['text' => '➖ Agregar Gasto']], + [['text' => '💰 Mis Ventas'], ['text' => '📝 Mis Gastos']] + ], + 'resize_keyboard' => true, + 'one_time_keyboard' => false + ]; + return $this->sendMessage($chatId, $text, $keyboard); + } + + private function dateKeyboard() { + return [ + 'keyboard' => [[['text' => 'Hoy'], ['text' => 'Ayer']], [['text' => '❌ Cancelar']]], + 'resize_keyboard' => true, + 'one_time_keyboard' => true + ]; + } + + private function cancelKeyboard() { + return [ + 'keyboard' => [[['text' => '❌ Cancelar']]], + 'resize_keyboard' => true, + 'one_time_keyboard' => true + ]; + } + + public function sendMessage(string $chatId, string $text, $replyMarkup = null): array + { + if (!$this->botToken) return ['ok' => false]; + $params = ['chat_id' => $chatId, 'text' => $text, 'parse_mode' => 'Markdown']; + if ($replyMarkup) $params['reply_markup'] = json_encode($replyMarkup); try { - $response = Http::post("https://api.telegram.org/bot{$this->botToken}/sendMessage", [ - 'chat_id' => $chatId, - 'text' => $text, - 'parse_mode' => 'Markdown' - ]); - - return $response->json(); - } catch (\Exception $e) { - Log::error('Telegram send message error', ['error' => $e->getMessage()]); - return ['ok' => false, 'error' => $e->getMessage()]; - } + return Http::post("https://api.telegram.org/bot{$this->botToken}/sendMessage", $params)->json(); + } catch (\Exception $e) { return ['ok' => false]; } } - /** - * Enviar mensaje de ayuda - */ - private function sendHelp(string $chatId): void + private function parseDate(string $text): string { - $text = "📋 *Comandos disponibles:*\n\n" . - "• /start - Iniciar bot\n" . - "• /help - Mostrar ayuda\n" . - "• /mes - Ver mes actual\n" . - "• /ventas - Ver ventas del mes\n" . - "• /gastos - Ver gastos del mes\n" . - "• /resumen - Resumen de comisiones\n"; - - $this->sendMessage($chatId, $text); + $text = strtolower(trim($text)); + if ($text === 'hoy') return date('Y-m-d'); + if ($text === 'ayer') return date('Y-m-d', strtotime('-1 day')); + $ts = strtotime($text); + return $ts ? date('Y-m-d', $ts) : date('Y-m-d'); } - /** - * Mostrar mes actual - */ - private function showCurrentMonth(User $user, string $chatId): void + private function sendInvalidNumeric(string $chatId): array { + $this->sendMessage($chatId, "⚠️ Ingresa solo números.", $this->cancelKeyboard()); + return ['ok' => true]; + } + + private function sendNoOpenMonth(string $chatId, $account): array + { + $account->update(['bot_state' => null, 'bot_data' => null]); + $this->sendMenu($chatId, "❌ No hay mes abierto."); + return ['ok' => true]; + } + + private function showSummary(User $user, string $chatId) { $month = $user->getCurrentMonth(); - - if (!$month) { - $this->sendMessage($chatId, "No tienes ningún mes abierto actualmente."); - return; - } - - $statusText = match($month->status) { - 'open' => '🟢 Abierto', - 'closed' => '🟡 Cerrado', - 'paid' => '✅ Pagado', - default => 'Desconocido' - }; - - $text = "📅 *Mes Actual*\n\n" . - "• *Nombre:* {$month->name} {$month->year}\n" . - "• *Estado:* {$statusText}"; - - $this->sendMessage($chatId, $text); + if (!$month) return $this->sendMessage($chatId, "No hay mes abierto."); + $data = \App\Services\CommissionCalculator::calculateForMonth($user, $month); + $text = "💵 *Resumen Financiero*\n\n" . + "• Comisiones: \$" . number_format($data['commission_amount'], 2) . "\n" . + "• Salario: \$" . number_format($data['monthly_salary'], 2) . "\n" . + "• Gastos: \$" . number_format($data['total_expenses'], 2) . "\n" . + "⭐ *Neto: \$" . number_format($data['total_earning'], 2) . "*"; + $this->sendMenu($chatId, $text); } - /** - * Mostrar ventas del mes - */ - private function showSales(User $user, string $chatId): void - { + private function showSales(User $user, string $chatId) { $month = $user->getCurrentMonth(); - - if (!$month) { - $this->sendMessage($chatId, "No tienes ningún mes abierto actualmente."); - return; - } - - $totalUserSales = $month->dailySales()->sum('user_sales'); - $totalSystemSales = $month->dailySales()->sum('system_sales'); - $diff = $totalUserSales - $totalSystemSales; - - $text = "💰 *Ventas del Mes*\n\n" . - "• *Usuario:* $" . number_format($totalUserSales, 2) . "\n" . - "• *Sistema:* $" . number_format($totalSystemSales, 2) . "\n" . - "• *Diferencia:* $" . number_format($diff, 2); - - $this->sendMessage($chatId, $text); + if (!$month) return; + $u = $month->dailySales()->sum('user_sales'); + $s = $month->dailySales()->sum('system_sales'); + $this->sendMenu($chatId, "💰 *Ventas*\n👤 U: \$" . number_format($u, 2) . "\n🖥️ S: \$" . number_format($s, 2)); } - /** - * Mostrar gastos del mes - */ - private function showExpenses(User $user, string $chatId): void - { + private function showExpenses(User $user, string $chatId) { $month = $user->getCurrentMonth(); - - if (!$month) { - $this->sendMessage($chatId, "No tienes ningún mes abierto actualmente."); - return; - } - - $totalExpenses = $month->expenses()->sum('amount'); - $expensesList = $month->expenses()->latest()->limit(5)->get(); - - $text = "📝 *Gastos del Mes*\n\n" . - "• *Total:* $" . number_format($totalExpenses, 2) . "\n\n"; - - if ($expensesList->count() > 0) { - $text .= "Últimos gastos:\n"; - foreach ($expensesList as $expense) { - $text .= "• {$expense->description}: \$" . number_format($expense->amount, 2) . "\n"; - } - } - - $this->sendMessage($chatId, $text); + if (!$month) return; + $t = $month->expenses()->sum('amount'); + $this->sendMenu($chatId, "📝 *Total Gastos:* \$" . number_format($t, 2)); } - /** - * Mostrar resumen de comisiones - */ - private function showSummary(User $user, string $chatId): void - { - $month = $user->getCurrentMonth(); - - if (!$month) { - $this->sendMessage($chatId, "No tienes ningún mes abierto actualmente."); - return; - } - - $data = CommissionCalculator::calculateForMonth($user, $month); - - $text = "💵 *Resumen de Comisiones*\n\n" . - "• *Mes:* {$data['month_name']}\n" . - "• *Ventas Sistema:* \$" . number_format($data['total_system_sales'], 2) . "\n" . - "• *Comisión ({$data['commission_percentage']}%):* \$" . number_format($data['commission_amount'], 2) . "\n" . - "• *Salario:* \$" . number_format($data['monthly_salary'], 2) . "\n" . - "• *Gastos:* \$" . number_format($data['total_expenses'], 2) . "\n" . - "• *Total a Recibir:* \$" . number_format($data['total_earning'], 2); - - $this->sendMessage($chatId, $text); - } - - /** - * Generar código de verificación - */ - public static function generateVerificationCode(): string - { - return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); - } - - /** - * Configurar webhook - */ - public function setWebhook(): bool - { - if (!$this->botToken || !$this->webhookUrl) { - Log::warning('Cannot set webhook: missing configuration'); - return false; - } - - try { - $response = Http::post("https://api.telegram.org/bot{$this->botToken}/setWebhook", [ - 'url' => $this->webhookUrl - ]); - - return $response->json('ok', false); - } catch (\Exception $e) { - Log::error('Telegram set webhook error', ['error' => $e->getMessage()]); - return false; - } - } - - /** - * Obtener información del webhook - */ - public function getWebhookInfo(): array - { - if (!$this->botToken) { - return ['ok' => false, 'error' => 'Bot token not configured']; - } - - try { - $response = Http::get("https://api.telegram.org/bot{$this->botToken}/getWebhookInfo"); - return $response->json(); - } catch (\Exception $e) { - Log::error('Telegram get webhook info error', ['error' => $e->getMessage()]); - return ['ok' => false, 'error' => $e->getMessage()]; - } - } - - /** - * Borrar webhook - */ - public function deleteWebhook(): bool - { - if (!$this->botToken) { - return false; - } - - try { - $response = Http::post("https://api.telegram.org/bot{$this->botToken}/deleteWebhook"); - return $response->json('ok', false); - } catch (\Exception $e) { - Log::error('Telegram delete webhook error', ['error' => $e->getMessage()]); - return false; - } - } + public static function generateVerificationCode(): string { return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); } + public function setWebhook(): bool { try { return Http::post("https://api.telegram.org/bot{$this->botToken}/setWebhook", ['url' => $this->webhookUrl])->json('ok', false); } catch (\Exception $e) { return false; } } + public function getWebhookInfo(): array { try { return Http::get("https://api.telegram.org/bot{$this->botToken}/getWebhookInfo")->json(); } catch (\Exception $e) { return ['ok' => false]; } } + public function deleteWebhook(): bool { try { return Http::post("https://api.telegram.org/bot{$this->botToken}/deleteWebhook")->json('ok', false); } catch (\Exception $e) { return false; } } } \ No newline at end of file diff --git a/bootstrap/app.php b/bootstrap/app.php index 0be650f..be59230 100755 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -14,7 +14,8 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->trustProxies(at: '*'); $middleware->validateCsrfTokens(except: [ - '/telegram/webhook', + 'telegram/webhook', + 'telegram/webhook/*', ]); }) ->withExceptions(function (Exceptions $exceptions): void { diff --git a/database/migrations/2026_04_20_014821_add_bot_state_to_telegram_accounts_table.php b/database/migrations/2026_04_20_014821_add_bot_state_to_telegram_accounts_table.php new file mode 100644 index 0000000..bc61c72 --- /dev/null +++ b/database/migrations/2026_04_20_014821_add_bot_state_to_telegram_accounts_table.php @@ -0,0 +1,29 @@ +string('bot_state')->nullable()->after('is_verified'); + $blueprint->json('bot_data')->nullable()->after('bot_state'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('telegram_accounts', function (Blueprint $blueprint) { + $blueprint->dropColumn(['bot_state', 'bot_data']); + }); + } +}; \ No newline at end of file