*/ private array $results = []; public function handle(): int { $this->info('🚀 در حال اجرای تست E2E Post-Deploy IFNEX...'); $this->newLine(); $this->checkDatabaseConnection(); $this->checkCoreTablesExist(); $this->checkCurrenciesSeeded(); $this->checkCountriesSeeded(); $this->checkRolesExist(); $this->checkAdminUserExists(); $this->checkSystemSettings(); $this->checkStorageWritable(); $this->checkCacheConfig(); $this->checkApiEndpoints(); $this->renderReport(); $failed = collect($this->results)->filter(fn ($r) => ! $r['passed'])->count(); if ($failed > 0) { $this->newLine(); $this->error("❌ {$failed} بررسی ناموفق بود — سیستم آمادهٔ UAT نیست."); return Command::FAILURE; } $this->newLine(); $this->info('✅ همهٔ بررسی‌ها سبز هستند. سیستم آمادهٔ UAT توسط کارفرماست.'); return Command::SUCCESS; } // ─── اتصال دیتابیس ─────────────────────────────────────── private function checkDatabaseConnection(): void { try { DB::select('SELECT 1'); $this->addResult('اتصال دیتابیس', 'OK', true); } catch (Throwable $e) { $this->addResult('اتصال دیتابیس', 'FAIL: ' . $e->getMessage(), false); } } // ─── وجود جدول‌های اصلی ────────────────────────────────── private function checkCoreTablesExist(): void { $required = [ 'users', 'currencies', 'countries', 'system_settings', 'shipments', 'wallets', 'wallet_transactions', 'address_book_entries', ]; foreach ($required as $table) { try { $exists = Schema::hasTable($table); $this->addResult("table: {$table}", $exists ? 'exists' : 'MISSING', $exists); } catch (Throwable $e) { $this->addResult("table: {$table}", 'ERR: ' . $e->getMessage(), false); } } } // ─── ارزها ────────────────────────────────────────────── private function checkCurrenciesSeeded(): void { $required = ['USD', 'AED', 'EUR', 'CNY', 'IRR']; try { $currencies = Currency::whereIn('code', $required)->get()->keyBy('code'); } catch (Throwable $e) { $this->addResult('currencies query', 'ERR: ' . $e->getMessage(), false); return; } $missing = array_diff($required, $currencies->keys()->all()); $this->addResult( '5 ارز پایه موجود (USD/AED/EUR/CNY/IRR)', empty($missing) ? 'all 5 present' : 'MISSING: ' . implode(',', $missing), empty($missing) ); // بررسی نرخ غیر صفر (به‌جز IRR که ۱ است) foreach (['USD', 'AED', 'EUR', 'CNY'] as $code) { $currency = $currencies->get($code); $rate = $currency ? (float) $currency->rate_to_rial : 0; $this->addResult( "rate: {$code} > 0", $rate > 0 ? "rate = {$rate}" : 'rate = 0 (import نشده)', $rate > 0 ); } } // ─── کشورها ───────────────────────────────────────────── private function checkCountriesSeeded(): void { try { $count = DB::table('countries')->count(); } catch (Throwable $e) { $this->addResult('countries count', 'ERR: ' . $e->getMessage(), false); return; } $this->addResult( 'countries seeded', "{$count} countries", $count >= 100 // انتظار حداقل ۱۰۰ کشور ); } // ─── نقش‌ها ────────────────────────────────────────────── private function checkRolesExist(): void { $required = ['super_admin', 'admin', 'staff', 'customer']; try { $existing = DB::table('roles')->whereIn('name', $required)->pluck('name')->all(); } catch (Throwable $e) { $this->addResult('roles query', 'ERR: ' . $e->getMessage(), false); return; } $missing = array_diff($required, $existing); $this->addResult( 'roles seeded', empty($missing) ? 'all 4 present' : 'MISSING: ' . implode(',', $missing), empty($missing) ); } // ─── حساب ادمین ───────────────────────────────────────── private function checkAdminUserExists(): void { try { $count = User::whereHas('roles', function ($q) { $q->whereIn('name', ['super_admin', 'admin']); })->count(); } catch (Throwable $e) { $this->addResult('admin users query', 'ERR: ' . $e->getMessage(), false); return; } $this->addResult( 'admin user exists', $count > 0 ? "{$count} admin(s)" : 'NO ADMIN USER', $count > 0 ); } // ─── تنظیمات سیستم ────────────────────────────────────── private function checkSystemSettings(): void { $required = ['vat_rate', 'profit_margin', 'packing_cost']; try { $existing = DB::table('system_settings') ->whereIn('key', $required) ->pluck('key') ->all(); } catch (Throwable $e) { $this->addResult('system_settings query', 'ERR: ' . $e->getMessage(), false); return; } $missing = array_diff($required, $existing); $this->addResult( 'system_settings seeded', empty($missing) ? 'all present' : 'MISSING: ' . implode(',', $missing), empty($missing) ); } // ─── storage قابل‌نوشتن ────────────────────────────────── private function checkStorageWritable(): void { $paths = [ 'storage' => base_path('storage'), 'storage/logs' => base_path('storage/logs'), 'storage/app' => base_path('storage/app'), 'storage/framework' => base_path('storage/framework'), 'bootstrap/cache' => base_path('bootstrap/cache'), ]; foreach ($paths as $label => $path) { $writable = file_exists($path) && is_writable($path); $this->addResult( "writable: {$label}", $writable ? 'writable' : 'NOT writable', $writable ); } } // ─── Cache config ─────────────────────────────────────── private function checkCacheConfig(): void { // بررسی اینکه config cache ساخته شده یا نه $configCached = file_exists(base_path('bootstrap/cache/config.php')); $this->addResult( 'config:cache اجرا شده', $configCached ? 'yes' : 'no — برای سرعت اجرا کنید: php artisan config:cache', $configCached ); $routeCached = file_exists(base_path('bootstrap/cache/routes-v7.php')); $this->addResult( 'route:cache اجرا شده', $routeCached ? 'yes' : 'no — برای سرعت اجرا کنید: php artisan route:cache', $routeCached ); } // ─── API Endpoints ────────────────────────────────────── private function checkApiEndpoints(): void { $baseUrl = (string) ($this->option('url') ?: config('app.url')); // حذف slash انتهایی $baseUrl = rtrim($baseUrl, '/'); // ۱. Health check endpoint $this->checkUrl("{$baseUrl}/up", 200, 'GET /up (health)'); // ۲. Public countries endpoint (بدون auth) $this->checkUrl("{$baseUrl}/api/v1/countries", 200, 'GET /api/v1/countries'); // ۳. Tracking endpoint با AWB نامعتبر — انتظار 404 یا 422 (نه 500) $this->checkUrl( "{$baseUrl}/api/v1/track/INVALID-AWB-TEST", [404, 422], 'GET /api/v1/track/{invalid} — 404/422 expected (نه 500)' ); // ۴. Protected endpoint بدون توکن — انتظار 401 $this->checkUrl( "{$baseUrl}/api/v1/customer/profile", 401, 'GET /api/v1/customer/profile (بدون توکن) — 401 expected' ); } private function checkUrl(string $url, int|array $expectedStatus, string $label): void { try { $response = Http::timeout(10)->get($url); $status = $response->status(); $expected = is_array($expectedStatus) ? $expectedStatus : [$expectedStatus]; $ok = in_array($status, $expected, true); $this->addResult( $label, "HTTP {$status}" . ($ok ? '' : ' (expected ' . implode('/', $expected) . ')'), $ok ); } catch (Throwable $e) { $this->addResult( $label, 'ERR: ' . $e->getMessage(), false ); } } // ─── Helpers ──────────────────────────────────────────── private function addResult(string $check, string $result, bool $passed): void { $this->results[] = compact('check', 'result', 'passed'); } private function renderReport(): void { $this->table( ['Check', 'Value', 'Status'], collect($this->results)->map(fn ($r) => [ $r['check'], $r['result'], $r['passed'] ? '✓ PASS' : '✗ FAIL', ])->all() ); } }