diff --git a/app/Console/Commands/PriceChannelTest.php b/app/Console/Commands/PriceChannelTest.php new file mode 100644 index 0000000..1f0bce6 --- /dev/null +++ b/app/Console/Commands/PriceChannelTest.php @@ -0,0 +1,91 @@ +diagnose(); + + $this->line(''); + $this->info('=== وضعیت ارسال قیمت به کانال ==='); + $this->line('قابلیت روشن (price_channel_enabled): ' . ($info['enabled'] ? 'بله' : 'خیر')); + $this->line('پیام‌رسان‌های انتخاب‌شده: ' . (implode(', ', $info['selected_messengers']) ?: '— (خالی)')); + $this->line('حداقل فاصله ارسال (ثانیه): ' . $info['min_seconds']); + $this->line('فاصله cooldown گذشته؟ ' . ($info['cooldown_elapsed'] ? 'بله' : 'خیر (هنوز زوده)')); + $this->line('تعداد محصولات واجد شرایط: ' . $info['eligible_products']); + $this->line('مقصدهای فعال (آماده ارسال): ' . (implode(', ', $info['active_targets']) ?: '— (هیچ)')); + + $this->line(''); + $this->info('--- جزئیات هر پیام‌رسان ---'); + $this->table( + ['پیام‌رسان', 'انتخاب‌شده', 'توکن', 'مقصدها', 'ارسال می‌شود؟'], + collect($info['messengers'])->map(fn ($m, $key) => [ + $key, + $m['selected'] ? '✓' : '✗', + $m['has_token'] ? '✓' : '✗ (خالی)', + implode(', ', $m['destinations']) ?: '—', + $m['will_send'] ? '✓' : '✗', + ])->values()->all() + ); + + $this->line(''); + $this->info('--- پیش‌نمایش پیام ---'); + $this->line($info['message_preview'] !== '' ? $info['message_preview'] : '(پیام خالی — هیچ محصول واجد شرایطی نیست)'); + + if ($this->diagnoseBlocker($info)) { + return self::SUCCESS; + } + + if ($this->option('send')) { + $this->line(''); + $this->info('=== ارسال آزمایشی (بدون cooldown) ==='); + $result = $service->forceSend(); + $this->line('ارسال شد؟ ' . ($result['sent'] ? 'بله' : 'خیر')); + foreach ($result['results'] as $messenger => $chatResults) { + $this->line("[{$messenger}]"); + foreach ((array) $chatResults as $chatId => $status) { + $this->line(" {$chatId} → {$status}"); + } + } + } else { + $this->line(''); + $this->comment('برای ارسال واقعی: php artisan price:channel-test --send'); + } + + return self::SUCCESS; + } + + /** پیام راهنما وقتی چیزی مانع ارسال است */ + private function diagnoseBlocker(array $info): bool + { + $blocked = false; + + if (! $info['enabled']) { + $this->warn('⚠ price_channel_enabled خاموش است — روشنش کن.'); + $blocked = true; + } + if ($info['selected_messengers'] === []) { + $this->warn('⚠ هیچ پیام‌رسانی در price_channel_messengers انتخاب نشده.'); + $blocked = true; + } + if ($info['active_targets'] === []) { + $this->warn('⚠ هیچ مقصد فعالی نیست — توکن بات و «مقصد ارسال» (price_channel_*_to) را پر کن.'); + $blocked = true; + } + if ($info['eligible_products'] === 0) { + $this->warn('⚠ هیچ محصول واجد شرایطی نیست (active + قیمت>0 + open_buy/sell + داخل ساعت کاری).'); + $blocked = true; + } + + return $blocked; + } +} diff --git a/app/Services/PriceChannelBroadcastService.php b/app/Services/PriceChannelBroadcastService.php index ed43e42..53066a9 100644 --- a/app/Services/PriceChannelBroadcastService.php +++ b/app/Services/PriceChannelBroadcastService.php @@ -64,6 +64,71 @@ class PriceChannelBroadcastService } } + /** + * ارسال دستی (عیب‌یابی) — cooldown و شرط تغییر قیمت را نادیده می‌گیرد. + * وضعیت روشن/خاموش، انتخاب پیام‌رسان، توکن و مقصد همچنان لازم است. + * + * @return array + */ + public function forceSend(): array + { + $message = $this->buildMessage(); + $targets = $this->resolveActiveTargets(); + + $results = []; + foreach ($targets as $messenger => $target) { + $results[$messenger] = $message === '' + ? ['skipped' => 'پیام خالی است (محصول واجد شرایط نیست)'] + : $this->dispatchToMessenger($messenger, $target['token'], $target['chat_ids'], $message); + } + + if ($message !== '' && $targets !== []) { + $this->markSent(); + } + + return [ + 'sent' => $message !== '' && $targets !== [], + 'results' => $results, + ]; + } + + /** + * گزارش وضعیت — چرا ارسال می‌شود یا نمی‌شود. + * + * @return array + */ + public function diagnose(): array + { + $selected = $this->selectedMessengers(); + $messengers = []; + + foreach (self::SUPPORTED_MESSENGERS as $messenger) { + $token = trim((string) $this->config->getString($this->tokenKey($messenger), '')); + $chatIds = $this->parseList($this->config->getString($this->chatIdsKey($messenger), '')); + $isSelected = in_array($messenger, $selected, true); + + $messengers[$messenger] = [ + 'selected' => $isSelected, + 'has_token' => $token !== '', + 'destinations' => $chatIds, + 'will_send' => $isSelected && $token !== '' && $chatIds !== [], + ]; + } + + $message = $this->buildMessage(); + + return [ + 'enabled' => $this->config->getBool('price_channel_enabled', false), + 'selected_messengers' => $selected, + 'min_seconds' => $this->config->getInt('price_channel_min_seconds', 120), + 'cooldown_elapsed' => $this->cooldownElapsed(), + 'eligible_products' => count($this->eligibleProducts()), + 'messengers' => $messengers, + 'active_targets' => array_keys($this->resolveActiveTargets()), + 'message_preview' => $message, + ]; + } + /** * پیام‌رسان‌های قابل ارسال: انتخاب‌شده + توکن معتبر + حداقل یک مقصد. * @@ -241,28 +306,48 @@ class PriceChannelBroadcastService return $lines; } - private function dispatchToMessenger(string $messenger, string $token, array $chatIds, string $message): void + /** + * @param array $chatIds + * @return array chatId => نتیجه (ok / خطا) + */ + private function dispatchToMessenger(string $messenger, string $token, array $chatIds, string $message): array { + $results = []; foreach ($chatIds as $chatId) { try { - match ($messenger) { + $response = match ($messenger) { 'bale' => $this->sendBale($token, $chatId, $message), 'telegram' => $this->sendTelegram($token, $chatId, $message), 'rubika' => $this->sendRubika($token, $chatId, $message), default => null, }; + + $status = $response?->status() ?? 0; + $ok = $response?->successful() ?? false; + $results[$chatId] = $ok ? "ok ({$status})" : "failed ({$status}): " . trim((string) $response?->body()); + + if (! $ok) { + Log::warning("PriceChannel {$messenger} send not ok", [ + 'chat_id' => $chatId, + 'status' => $status, + 'body' => $response?->body(), + ]); + } } catch (\Throwable $e) { + $results[$chatId] = 'exception: ' . $e->getMessage(); Log::warning("PriceChannel {$messenger} send failed", [ 'chat_id' => $chatId, 'message' => $e->getMessage(), ]); } } + + return $results; } - private function sendBale(string $token, string $chatId, string $message): void + private function sendBale(string $token, string $chatId, string $message): \Illuminate\Http\Client\Response { - Http::withOptions(Controller::httpClientOptions()) + return Http::withOptions(Controller::httpClientOptions()) ->withoutVerifying() ->timeout(6) ->post("https://tapi.bale.ai/bot{$token}/sendMessage", [ @@ -271,9 +356,9 @@ class PriceChannelBroadcastService ]); } - private function sendTelegram(string $token, string $chatId, string $message): void + private function sendTelegram(string $token, string $chatId, string $message): \Illuminate\Http\Client\Response { - Http::withOptions(Controller::httpClientOptions()) + return Http::withOptions(Controller::httpClientOptions()) ->timeout(6) ->post("https://api.telegram.org/bot{$token}/sendMessage", [ 'chat_id' => $chatId, @@ -281,9 +366,9 @@ class PriceChannelBroadcastService ]); } - private function sendRubika(string $token, string $chatId, string $message): void + private function sendRubika(string $token, string $chatId, string $message): \Illuminate\Http\Client\Response { - Http::withOptions(Controller::httpClientOptions()) + return Http::withOptions(Controller::httpClientOptions()) ->withoutVerifying() ->timeout(6) ->post("https://botapi.rubika.ir/v3/{$token}/sendMessage", [ diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index 42ed1cf..40a37bb 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -360,9 +360,12 @@ WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'price_channel_enabled' INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) SELECT 'price_channel_messengers', '', 'پیام‌رسان‌های ارسال قیمت', 1, 1, - 'کدام پیام‌رسان‌ها قیمت بگیرند؟ با ویرگول جدا کنید. گزینه‌ها: bale,telegram,rubika', 'text' + 'کدام پیام‌رسان‌ها قیمت بگیرند؟ می‌توانید چند مورد را انتخاب کنید. گزینه‌ها: بله، تلگرام، روبیکا.', 'multiselect' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'price_channel_messengers' LIMIT 1); +-- نوع multiselect را روی ردیف‌های موجود هم اعمال کن (نصب‌هایی که قبلاً text گرفته بودند) +UPDATE `settings` SET `type` = 'multiselect' WHERE `key` = 'price_channel_messengers'; + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) SELECT 'price_channel_diff', '0', 'اختلاف قیمت کانال (ریال)', 1, 1, 'مبلغی که به قیمت خرید کانال اضافه و از قیمت فروش کم می‌شود (اسپرد اضافه فقط برای کانال).', 'price' diff --git a/database/sql/price_channel_settings.sql b/database/sql/price_channel_settings.sql index 01b32d9..99d6c6d 100644 --- a/database/sql/price_channel_settings.sql +++ b/database/sql/price_channel_settings.sql @@ -24,9 +24,11 @@ WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'price_channel_enabled' INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) SELECT 'price_channel_messengers', '', 'پیام‌رسان‌های ارسال قیمت', 1, 1, - 'کدام پیام‌رسان‌ها قیمت بگیرند؟ با ویرگول جدا کنید. گزینه‌ها: bale,telegram,rubika', 'text' + 'کدام پیام‌رسان‌ها قیمت بگیرند؟ می‌توانید چند مورد را انتخاب کنید. گزینه‌ها: بله، تلگرام، روبیکا.', 'multiselect' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'price_channel_messengers' LIMIT 1); +UPDATE `settings` SET `type` = 'multiselect' WHERE `key` = 'price_channel_messengers'; + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) SELECT 'price_channel_diff', '0', 'اختلاف قیمت کانال (ریال)', 1, 1, 'مبلغی که به قیمت خرید کانال اضافه و از قیمت فروش کم می‌شود (اسپرد اضافه فقط برای کانال).', 'price'