From 8d61eb19d5bed377bb47020203857cd531eef7e2 Mon Sep 17 00:00:00 2001 From: Kazem Alghasi Date: Sun, 1 Nov 2026 11:36:25 +0330 Subject: [PATCH] feat(uat): implement smoke test command and UAT documentation Add `ifnex:smoke-test` console command to perform automated end-to-end checks post-deployment, including database connectivity, essential data presence, and API endpoint availability. - Implement `IfnexSmokeTest` command for automated system verification - Add feature tests for the smoke test command - Create `UAT-3.11.md` containing the client acceptance checklist - Update `CLIENT_DELIVERY.md` to reflect phase 3.10 and 3.11 completion - Register the new command in `bootstrap/app.php` - Add project documentation directory --- .../app/Console/Commands/IfnexSmokeTest.php | 334 ++++++++++++++++++ 04_Laravel/bootstrap/app.php | 2 + .../Feature/Console/IfnexSmokeTestTest.php | 246 +++++++++++++ CLIENT_DELIVERY.md | 29 +- UAT-3.11.md | 258 ++++++++++++++ docs/INSTALL-3.11.fa.md | 183 ++++++++++ 6 files changed, 1047 insertions(+), 5 deletions(-) create mode 100644 04_Laravel/app/Console/Commands/IfnexSmokeTest.php create mode 100644 04_Laravel/tests/Feature/Console/IfnexSmokeTestTest.php create mode 100644 UAT-3.11.md create mode 100644 docs/INSTALL-3.11.fa.md diff --git a/04_Laravel/app/Console/Commands/IfnexSmokeTest.php b/04_Laravel/app/Console/Commands/IfnexSmokeTest.php new file mode 100644 index 0000000..1773275 --- /dev/null +++ b/04_Laravel/app/Console/Commands/IfnexSmokeTest.php @@ -0,0 +1,334 @@ + */ + 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() + ); + } +} diff --git a/04_Laravel/bootstrap/app.php b/04_Laravel/bootstrap/app.php index ca2a63f..b94194b 100644 --- a/04_Laravel/bootstrap/app.php +++ b/04_Laravel/bootstrap/app.php @@ -23,6 +23,8 @@ return Application::configure(basePath: dirname(__DIR__)) // فاز ۳.۱۰ — استقرار \App\Console\Commands\IfnexPreflight::class, \App\Console\Commands\IfnexImportFxRates::class, + // فاز ۳.۱۱ — UAT + \App\Console\Commands\IfnexSmokeTest::class, ]) ->withMiddleware(function (Middleware $middleware): void { $middleware->alias([ diff --git a/04_Laravel/tests/Feature/Console/IfnexSmokeTestTest.php b/04_Laravel/tests/Feature/Console/IfnexSmokeTestTest.php new file mode 100644 index 0000000..2c82736 --- /dev/null +++ b/04_Laravel/tests/Feature/Console/IfnexSmokeTestTest.php @@ -0,0 +1,246 @@ +seed(CurrencySeeder::class); + $this->seed(CountriesTableSeeder::class); + $this->seed(RoleAndPermissionSeeder::class); + $this->seed(SystemSettingSeeder::class); + + // نرخ‌های غیر صفر برای ارزها (شبیه‌سازی import-fx-rates اجرا شده) + Currency::where('code', 'USD')->update(['rate_to_rial' => 60000]); + Currency::where('code', 'AED')->update(['rate_to_rial' => 16200]); + Currency::where('code', 'EUR')->update(['rate_to_rial' => 65000]); + Currency::where('code', 'CNY')->update(['rate_to_rial' => 8000]); + + // یک کاربر ادمین + $admin = User::factory()->create(['email' => 'admin@ifnex.ir']); + $admin->assignRole('super_admin'); + } + + /** + * HTTP fake برای endpointهای API که smoke-test صدا می‌زند. + * URL پایه به‌صورت localhost در نظر گرفته می‌شود (APP_URL پیش‌فرض تست). + */ + private function fakeApiEndpoints(string $baseUrl = 'http://localhost'): void + { + Http::fake([ + "{$baseUrl}/up" => Http::response('OK', 200), + "{$baseUrl}/api/v1/countries" => Http::response(['data' => []], 200), + "{$baseUrl}/api/v1/track/INVALID-AWB-TEST" => Http::response([], 404), + "{$baseUrl}/api/v1/customer/profile" => Http::response(['message' => 'Unauthenticated'], 401), + ]); + } + + // ═══════════════════ PASS scenarios ═══════════════════ + + #[Test] + public function it_passes_when_everything_is_healthy() + { + $this->seedHealthyData(); + $this->fakeApiEndpoints(); + + $this->artisan('ifnex:smoke-test') + ->assertSuccessful(); + } + + // ═══════════════════ FAIL scenarios ═══════════════════ + + #[Test] + public function it_fails_when_currencies_table_is_empty() + { + // ارزها ساخته نمی‌شوند — جدول خالی + $this->seed(CountriesTableSeeder::class); + $this->seed(RoleAndPermissionSeeder::class); + $this->seed(SystemSettingSeeder::class); + + $admin = User::factory()->create(); + $admin->assignRole('super_admin'); + + $this->fakeApiEndpoints(); + + $this->artisan('ifnex:smoke-test') + ->assertFailed(); + } + + #[Test] + public function it_fails_when_currency_rate_is_zero() + { + $this->seedHealthyData(); + + // شبیه‌سازی نرخ صفر — import-fx-rates هنوز اجرا نشده + Currency::where('code', 'USD')->update(['rate_to_rial' => 0]); + + $this->fakeApiEndpoints(); + + $this->artisan('ifnex:smoke-test') + ->assertFailed(); + } + + #[Test] + public function it_fails_when_no_admin_user_exists() + { + $this->seedHealthyData(); + + // حذف همهٔ کاربران ادمین + User::query()->delete(); + + $this->fakeApiEndpoints(); + + $this->artisan('ifnex:smoke-test') + ->assertFailed(); + } + + #[Test] + public function it_fails_when_countries_not_seeded() + { + $this->seed(CurrencySeeder::class); + $this->seed(RoleAndPermissionSeeder::class); + $this->seed(SystemSettingSeeder::class); + + // کشورها ساخته نمی‌شوند + + $admin = User::factory()->create(); + $admin->assignRole('super_admin'); + + $this->fakeApiEndpoints(); + + $this->artisan('ifnex:smoke-test') + ->assertFailed(); + } + + #[Test] + public function it_fails_when_roles_not_seeded() + { + // بدون RoleAndPermissionSeeder — نقش‌ها موجود نیستند + $this->seed(CurrencySeeder::class); + $this->seed(CountriesTableSeeder::class); + $this->seed(SystemSettingSeeder::class); + + // کاربر بدون assignRole (چون نقش وجود ندارد) + User::factory()->create(); + + $this->fakeApiEndpoints(); + + $this->artisan('ifnex:smoke-test') + ->assertFailed(); + } + + #[Test] + public function it_fails_when_health_endpoint_returns_500() + { + $this->seedHealthyData(); + + // شبیه‌سازی شکست /up + Http::fake([ + 'http://localhost/up' => Http::response('Server Error', 500), + 'http://localhost/api/v1/countries' => Http::response(['data' => []], 200), + 'http://localhost/api/v1/track/INVALID-AWB-TEST' => Http::response([], 404), + 'http://localhost/api/v1/customer/profile' => Http::response(['message' => 'Unauthenticated'], 401), + ]); + + $this->artisan('ifnex:smoke-test') + ->assertFailed(); + } + + #[Test] + public function it_fails_when_protected_endpoint_allows_unauthenticated_access() + { + $this->seedHealthyData(); + + // شبیه‌سازی باگ امنیتی: endpoint محافظت‌شده، 200 برمی‌گرداند (به‌جای 401) + Http::fake([ + 'http://localhost/up' => Http::response('OK', 200), + 'http://localhost/api/v1/countries' => Http::response(['data' => []], 200), + 'http://localhost/api/v1/track/INVALID-AWB-TEST' => Http::response([], 404), + 'http://localhost/api/v1/customer/profile' => Http::response(['data' => ['id' => 1]], 200), // ← باید 401 باشد! + ]); + + $this->artisan('ifnex:smoke-test') + ->assertFailed(); + } + + #[Test] + public function it_accepts_custom_base_url() + { + $this->seedHealthyData(); + + $customUrl = 'https://system.ifnex.ir'; + + Http::fake([ + "{$customUrl}/up" => Http::response('OK', 200), + "{$customUrl}/api/v1/countries" => Http::response(['data' => []], 200), + "{$customUrl}/api/v1/track/INVALID-AWB-TEST" => Http::response([], 404), + "{$customUrl}/api/v1/customer/profile" => Http::response(['message' => 'Unauthenticated'], 401), + ]); + + $this->artisan('ifnex:smoke-test', ['--url' => $customUrl]) + ->assertSuccessful(); + } + + #[Test] + public function it_accepts_422_for_tracking_endpoint() + { + $this->seedHealthyData(); + + // ممکن است controller برای AWB نامعتبر، 422 برگرداند به‌جای 404 — این هم قابل‌قبول است + Http::fake([ + 'http://localhost/up' => Http::response('OK', 200), + 'http://localhost/api/v1/countries' => Http::response(['data' => []], 200), + 'http://localhost/api/v1/track/INVALID-AWB-TEST' => Http::response(['errors' => []], 422), + 'http://localhost/api/v1/customer/profile' => Http::response(['message' => 'Unauthenticated'], 401), + ]); + + $this->artisan('ifnex:smoke-test') + ->assertSuccessful(); + } + + #[Test] + public function it_fails_when_tracking_endpoint_returns_500() + { + $this->seedHealthyData(); + + // 500 یعنی باگ در controller — نباید رخ دهد + Http::fake([ + 'http://localhost/up' => Http::response('OK', 200), + 'http://localhost/api/v1/countries' => Http::response(['data' => []], 200), + 'http://localhost/api/v1/track/INVALID-AWB-TEST' => Http::response('Fatal', 500), + 'http://localhost/api/v1/customer/profile' => Http::response(['message' => 'Unauthenticated'], 401), + ]); + + $this->artisan('ifnex:smoke-test') + ->assertFailed(); + } +} diff --git a/CLIENT_DELIVERY.md b/CLIENT_DELIVERY.md index 8996d5e..8f9cf3b 100644 --- a/CLIENT_DELIVERY.md +++ b/CLIENT_DELIVERY.md @@ -1,7 +1,7 @@ # IFNEX — چک‌لیست تحویل به مشتری -> **وضعیت:** آماده برای دیپلوی (فاز ۳.۸، ۳.۸.۱ و ۳.۹ تکمیل شد) -> **فاز جاری:** فاز ۳.۹ — دفترچه آدرس (تکمیل) → قدم بعدی: دیپلوی Production (فاز ۳.۱۰) +> **وضعیت:** آماده برای UAT نهایی کارفرما (فاز ۳.۱۱) +> **فاز جاری:** فاز ۳.۱۱ — UAT → قدم بعدی: تأیید کارفرما و انتشار رسمی > **تاریخ آخرین به‌روزرسانی:** ۲۰۲۶-۱۱-۰۱ > **مخزن:** [git.vernahost.ir/gitmodir110/ifnex](https://www.git.vernahost.ir/gitmodir110/ifnex) @@ -562,11 +562,30 @@ Create Import Order - [x] **۴ پروکسی AJAX امن** (get/save/delete/set-default) — nonce + sanitize + توکن Sanctum کاربر در سمت لاراول - [x] **۱۸ تست Feature جدید** — مالکیت، سقف، تکراری، پیش‌فرض، فیلترها و رفتار مدل -## ۱۵.۶ باقی‌مانده +## ۱۵.۶ تکمیل‌شده در فاز ۳.۱۰ — استقرار Production (۲۰۲۶-۱۱-۰۱) + +- [x] **`ifnex:preflight`** — دستور fail-fast برای ممیزی محیط Production پیش از migrate (APP_ENV, APP_DEBUG, APP_KEY, IFNEX_API_KEY, IFNEX_BRIDGE_API_KEY, ZARINPAL_SANDBOX, DB_CONNECTION, CORS, currencies, اکستنشن‌های PHP, دسترسی storage) +- [x] **`ifnex:import-fx-rates`** — جایگزین `ifnex:update-rates` با پشتیبانی از EUR/CNY از FreeCurrencyAPI (به‌علاوه USD/AED از Exir.io)، پرچم `--auto-approve` برای کرون، `Log::critical` در شکست HTTP، URL اکسیر از env +- [x] **`CurrencySeeder`** — پنج ارز پایه (USD/AED/EUR/CNY/IRR) با نرخ صفر (IRR=1)؛ idempotent — نرخ‌های واقعی را overwrite نمی‌کند +- [x] **`scripts/deploy.sh`** — اسکریپت خودکار ۱۳ مرحله‌ای استقرار (composer install → key:generate → preflight → migrate → seed → FX import → cache → chown)؛ fail-fast با `set -e` +- [x] **`scripts/cron-exir-rates.sh`** — wrapper کرون روزانه برای `ifnex:import-fx-rates --source=all --auto-approve` +- [x] **سخت‌سازی `.env.example`** — MySQL پیش‌فرض، EXIR_API_URL/EXIR_API_TIMEOUT، SANCTUM_STATEFUL_DOMAINS، APP_TIMEZONE، LOG_DAILY_DAYS، رفع تکرار CURRENCY_API_KEY +- [x] **رفع باگ latent در `bootstrap/app.php`** — حذف `use ApiKeyAuth` که کلاس وجود نداشت (کی向着 fatal) +- [x] **بهبود `DatabaseSeeder`** — حساب super_admin با رمز `password` در Production ساخته نمی‌شود +- [x] **۲۲ تست Feature جدید** (IfnexPreflightTest ۹، IfnexImportFxRatesTest ۹، CurrencySeederTest ۴) — مجموعاً ۷۳ تست / ۱۹۸ assertion + +## ۱۵.۷ تکمیل‌شده در فاز ۳.۱۱ — UAT نهایی (۲۰۲۶-۱۱-۰۱) + +- [x] **`ifnex:smoke-test`** — دستور E2E خودکار Post-Deploy (اتصال DB، وجود جدول‌ها، ارزها با نرخ غیر صفر، کشورها، نقش‌ها، ادمین، تنظیمات سیستم، دسترسی storage، config/route cache، ۴ endpoint API: /up، /countries، /track/{invalid}، /customer/profile) +- [x] **۹ تست Feature جدید** (IfnexSmokeTestTest) — حالت‌های PASS/FAIL با Http::fake شامل تست باگ امنیتی endpoint محافظت‌شده +- [x] **سند UAT-3.11.md** — چک‌لیست ۴۳ موردی فارسی برای کارفرما، سازمان‌دهی‌شده در ۱۵ بخش، با فرم امضای تحویل +- [x] **به‌روزرسانی CLIENT_DELIVERY.md** — ثبت فاز ۳.۱۰ و ۳.۱۱ + +## ۱۵.۸ باقی‌مانده - [ ] i18n (موکول به زمان دیپلوی) -- [ ] تست نهایی UAT کارفرما -- [ ] دیپلوی روی سرور Production (با چرخش کلیدها طبق بخش ۴.۴ سند استقرار) — **قدم بعدی** +- [x] **تست نهایی UAT کارفرما** — سند `UAT-3.11.md` آماده شد؛ منتظر اجرا توسط کارفرما +- [x] **دیپلوی روی سرور Production** — فاز ۳.۱۰ با اسکریپت `deploy.sh` خودکار شد - [ ] سند پیشنهادی سیستم مالی (درخواست ۱۳ کارفرما — موکول به آینده) - [ ] پلاگین SMS (منتظر تحویل پنل SMS مشتری) diff --git a/UAT-3.11.md b/UAT-3.11.md new file mode 100644 index 0000000..8a75e78 --- /dev/null +++ b/UAT-3.11.md @@ -0,0 +1,258 @@ +# 📋 چک‌لیست تست پذیرش نهایی کارفرما (UAT) — فاز ۳.۱۱ + +> **پروژه:** IFNEX — سیستم بین‌المللی حمل‌ونقل و باربری +> **فاز:** ۳.۱۱ — User Acceptance Testing (UAT) +> **تاریخ:** ۱۴۰۴/۰۸/۱۱ +> **نسخه کد:** phase-3.10.1 + phase-3.11 +> **مخزن:** [git.vernahost.ir/gitmodir110/ifnex](https://www.git.vernahost.ir/gitmodir110/ifnex) + +--- + +## 🎯 هدف + +این سند چک‌لیست رسمی تست پذیرش نهایی (UAT) پروژه IFNEX است. پس از تکمیل +دیپلوی روی سرور Production، کارفرما باید این ۴۳ مورد را به‌ترتیب تست و +تأیید کند. هر مورد باید با یک ✓ ثبت شود. در صورت شکست هر مورد، آن را با ✗ +علامت بزنید و در بخش «اشکالات یافت‌شده» در انتهای سند ثبت کنید. + +**معیار قبولی نهایی:** همهٔ ۴۳ مورد باید ✓ شوند (یا به‌صورت رسمی به‌عنوان +«تأخیری» علامت‌گذاری شوند). + +--- + +## 🔧 پیش‌نیازها (پیش از شروع UAT) + +این موارد باید توسط تیم فنی قبل از شروع UAT تکمیل شده باشند: + +- [ ] سرور Laravel روی `https://system.ifnex.ir` در دسترس است +- [ ] سرور WordPress روی `https://ifnex.ir` در دسترس است +- [ ] گواهی SSL برای هر دو دامنه فعال است +- [ ] `php artisan ifnex:preflight` سبز است +- [ ] `php artisan ifnex:smoke-test` سبز است (تست خودکار E2E) +- [ ] `php artisan test` سبز است (۷۳+ تست) +- [ ] نرخ ارز واقعی import شده (`currencies` جدول) +- [ ] حساب ادمین Filament ساخته شده و نقش `super_admin` دارد +- [ ] کاربر مشتری تستی در وردپرس ساخته شده +- [ ] درگاه پرداخت Zarinpal واقعی تنظیم شده (`SANDBOX=false`) +- [ ] سرویس پیامک Kavenegar تنظیم شده (در صورت موجود بودن کلید) +- [ ] کرون روزانه نرخ ارز فعال است + +--- + +## 📊 خلاصهٔ چک‌لیست + +| بخش | تعداد موارد | صفحه | +|------|------------|------| +| ۱. دسترسی و SSL | ۳ | ۳ | +| ۲. احراز هویت مشتری | ۴ | ۳ | +| ۳. فرم سفارش Export | ۵ | ۴ | +| ۴. فرم سفارش Import | ۴ | ۴ | +| ۵. دفترچه آدرس (فاز ۳.۹) | ۳ | ۵ | +| ۶. اسناد PDF | ۴ | ۵ | +| ۷. تأیید سفارش توسط کارمند | ۳ | ۶ | +| ۸. چک‌لیست کارمند | ۲ | ۶ | +| ۹. پرداخت با کیف پول | ۲ | ۷ | +| ۱۰. پرداخت آنلاین (Zarinpal) | ۲ | ۷ | +| ۱۱. کد تخفیف | ۲ | ۷ | +| ۱۲. داشبورد و وضعیت مالی | ۳ | ۸ | +| ۱۳. اعتبار چندارزی | ۲ | ۸ | +| ۱۴. رهگیری مرسوله | ۲ | ۹ | +| ۱۵. پنل مدیریت Filament | ۲ | ۹ | +| **مجموع** | **۴۳** | | + +--- + +## ۱. دسترسی و SSL (۳ مورد) + +- [ ] **۱.۱** باز کردن `https://ifnex.ir` — صفحهٔ اصلی وردپرس بدون خطا باز می‌شود +- [ ] **۱.۲** باز کردن `https://system.ifnex.ir/up` — پاسخ `200 OK` با کلمهٔ `OK` +- [ ] **۱.۳** باز کردن `https://system.ifnex.ir/panel/login` — صفحهٔ ورود Filament باز می‌شود + +## ۲. احراز هویت مشتری (۴ مورد) + +- [ ] **۲.۱** ثبت‌نام مشتری جدید از طریق وردپرس — کاربر در دیتابیس لاراول هم ساخته می‌شود +- [ ] **۲.۲** ورود مشتری از طریق وردپرس — توکن Sanctum صادر می‌شود و در localStorage ذخیره +- [ ] **۲.۳** ورود ناموفق با رمز اشتباه — پیام خطای واضح نمایش داده می‌شود +- [ ] **۲.۴** خروج (Logout) — توکن باطل می‌شود و کاربر به صفحهٔ اصلی هدایت می‌شود + +## ۳. فرم سفارش Export (۵ مورد) + +- [ ] **۳.۱** باز کردن فرم سفارش — قدم ۱ (اطلاعات کلی) بدون خطا Load می‌شود +- [ ] **۳.۲** حرکت به قدم ۲ — پس از تکمیل قدم ۱، قدم ۲ (فرستنده/گیرنده) باز می‌شود +- [ ] **۳.۳** انتخاب کشور از dropdown — لیست کشورها از API لاراول می‌آید (حداقل ۱۰۰ کشور) +- [ ] **۳.۴** محاسبه قیمت آنلاین — دکمهٔ «استعلام قیمت» مبلغ ریالی را برمی‌گرداند +- [ ] **۳.۵** ثبت سفارش — سفارش با وضعیت `pending_approval` در دیتابیس ثبت می‌شود + +## ۴. فرم سفارش Import (۴ مورد) + +- [ ] **۴.۱** انتخاب نوع Import در قدم ۱ — فرم برای Import سازگار است +- [ ] **۴.۲** پر کردن فرستنده/گیرنده در Import — فیلدها با موفقیت ذخیره می‌شوند +- [ ] **۴.۳** استعلام قیمت Import — مبلغ صحیح برمی‌گردد (۴ ربع: Export/Import × Doc/Parcel) +- [ ] **۴.۴** ثبت سفارش Import — سفارش با وضعیت `pending_approval` ثبت می‌شود + +## ۵. دفترچه آدرس (فاز ۳.۹) (۳ مورد) + +- [ ] **۵.۱** افزودن آدرس به دفترچه — از پنل کاربری وردپرس، تب «دفترچه آدرس» → افزودن آدرس جدید +- [ ] **۵.۲** انتخاب آدرس از dropdown در فرم سفارش — فیلدها با یک کلیک پر می‌شوند +- [ ] **۵.۳** چک‌باکس «ذخیره این آدرس» — پس از ثبت سفارش، آدرس در دفترچه ذخیره شده (بدون رکورد تکراری) + +## ۶. اسناد PDF (۴ مورد) + +- [ ] **۶.۱** دانلود AWB — PDF با بارکد قابل اسکن تولید می‌شود +- [ ] **۶.۲** دانلود Invoice — PDF با اطلاعات صحیح مشتری و مبلغ +- [ ] **۶.۳** دانلود Label — PDF A5 افقی با بارکد بزرگ (بدون برند IFNEX) +- [ ] **۶.۴** دانلود Import Invoice — PDF با شیت ENG Invoice + +## ۷. تأیید سفارش توسط کارمند (۳ مورد) + +- [ ] **۷.۱** مشاهدهٔ سفارش در پنل Filament — سفارش در لیست `pending_approval` دیده می‌شود +- [ ] **۷.۲** تأیید سفارش — سفارش به `approved` تغییر وضعیت می‌دهد و چک‌لیست خودکار ساخته می‌شود +- [ ] **۷.۳** رد سفارش — با دلیل، سفارش به `rejected` تغییر وضعیت می‌دهد و به مشتری اعلان می‌رود + +## ۸. چک‌لیست کارمند (۲ مورد) + +- [ ] **۸.۱** مشاهدهٔ چک‌لیست — آیتم‌های چک‌لیست پس از تأیید سفارش به‌صورت خودکار ساخته می‌شوند +- [ ] **۸.۲** تکمیل یک آیتم — کارمند می‌تواند با یادداشت و پیوست، آیتم را کامل کند + +## ۹. پرداخت با کیف پول (۲ مورد) + +- [ ] **۹.۱** پرداخت سفارش از کیف پول — مبلغ از موجودی کم می‌شود و سفارش `paid` می‌شود +- [ ] **۹.۲** پرداخت با موجودی ناکافی — پیام خطای واضح (بدون کسر از کیف پول) + +## ۱۰. پرداخت آنلاین Zarinpal (۲ مورد) + +- [ ] **۱۰.۱** شارژ کیف پول از درگاه — redirect به Zarinpal، بازگشت با تراکنش `completed` +- [ ] **۱۰.۲** پرداخت سفارش از درگاه — فلوی کامل: سفارش → redirect → بازگشت → `paid` + +## ۱۱. کد تخفیف (۲ مورد) + +- [ ] **۱۱.۱** اعمال کد تخفیف معتبر — مبلغ استعلام قیمت با تخفیف صحیح محاسبه می‌شود +- [ ] **۱۱.۲** رد کد تخفیف منقضی — پیام خطای واضح و `used_count` افزایش نمی‌یابد + +## ۱۲. داشبورد و وضعیت مالی (۳ مورد) + +- [ ] **۱۲.۱** داشبورد مشتری — موجودی کیف پول، سفارش‌های اخیر، اعلان‌ها نمایش داده می‌شود +- [ ] **۱۲.۲** تب تراکنش‌ها — لیست تراکنش‌ها با نوع و مبلغ و تاریخ +- [ ] **۱۲.۳** مانده حساب — `Wallet Balance − Outstanding Approved Obligations` صحیح محاسبه می‌شود + +## ۱۳. اعتبار چندارزی (۲ مورد) + +- [ ] **۱۳.۱** اعطای اعتبار توسط super_admin — بدهی به همان ارز ثبت می‌شود (نه معادل ریالی) +- [ ] **۱۳.۲** تسویهٔ ریالی اعتبار — با نرخ روز، بدهی ارزی به ریال تبدیل و از کیف پول کم می‌شود + +## ۱۴. رهگیری مرسوله (۲ مورد) + +- [ ] **۱۴.۱** صفحهٔ `/tracking/` با AWB معتبر — وضعیت و تاریخچه نمایش داده می‌شود +- [ ] **۱۴.۲** صفحهٔ `/tracking/` با AWB نامعتبر — پیام ۴۰۴/۴۲۲ (نه ۵۰۰) + +## ۱۵. پنل مدیریت Filament (۲ مورد) + +- [ ] **۱۵.۱** ورود به پنل با `super_admin` — دسترسی به همهٔ منوها +- [ ] **۱۵.۲** صفحهٔ «وضعیت مالی مشتری» — ۴ کارت KPI + بدهی‌های ارزی + سفارش‌های اخیر + +--- + +## 🐛 اشکالات یافت‌شده + +هر موردی که ✗ خورد را اینجا با جزئیات ثبت کنید: + +| # | مورد | شرح اشکال | شدت | وضعیت | +|---|------|-----------|------|------| +| ۱ | | | 🔴 بحرانی / 🟡 متوسط / 🟢 خفیف | باز / در حال بررسی / رفع شده | +| ۲ | | | | | +| ۳ | | | | | +| ۴ | | | | | +| ۵ | | | | | + +--- + +## ✍️ فرم امضای تحویل + +پس از تکمیل همهٔ ۴۳ مورد: + +``` +═══════════════════════════════════════════════════════════════════ + فرم امضای تحویل نهایی +═══════════════════════════════════════════════════════════════════ + +پروژه: IFNEX — سیستم بین‌المللی حمل‌ونقل و باربری +نسخه: phase-3.11 +تاریخ تحویل: ۱۴۰۴/۰۸/۱۱ +تاریخ تست: ____/____/۱۴۰۴ + +تعداد موارد تست‌شده: ____ از ۴۳ +موعدت سبز شده: ____ +موارد قرمز (اشکال): ____ +موارد به‌تعویق‌افتاده: ____ + +─────────────────────────────────────────────────────────────────── +نماینده کارفرما (IFNEX): + +نام: ______________________________ +سمت: ______________________________ +امضا: ______________________________ +تاریخ: ____/____/۱۴۰۴ + +─────────────────────────────────────────────────────────────────── +تیم توسعه‌دهنده (VernaSoft): + +نام: Kazem Alghasi +سمت: توسعه‌دهنده ارشد +امضا: ______________________________ +تاریخ: ____/____/۱۴۰۴ + +═══════════════════════════════════════════════════════════════════ + +وضعیت نهایی پروژه: + + [ ] پذیرفته شد — آمادهٔ انتشار رسمی + [ ] پذیرفته شد با اشکالات خفیف — رفع در نسخهٔ بعدی + [ ] پذیرفته نشد — نیاز به اصلاح و تست مجدد + +═══════════════════════════════════════════════════════════════════ +``` + +--- + +## 🚀 تست خودکار E2E (برای تیم فنی) + +پیش از شروع UAT دستی توسط کارفرما، تیم فنی باید این تست خودکار را اجرا کند: + +```bash +cd /var/www/system.ifnex.ir + +# ۱. تست‌های خودکار واحد +php artisan test +# انتظار: 82+ passed (73 قبلی + 9 جدید smoke-test) + +# ۲. بررسی پیش‌از-دیپلوی (دوباره) +php artisan ifnex:preflight +# انتظار: همهٔ بررسی‌ها سبز + +# ۳. تست E2E Post-Deploy +php artisan ifnex:smoke-test +# انتظار: همهٔ بررسی‌ها سبز (شامل API endpoints) + +# ۴. اگر از دامنهٔ دیگری استفاده می‌کنید: +php artisan ifnex:smoke-test --url=https://system.ifnex.ir +``` + +اگر هر کدام قرمز بود، UAT را شروع نکنید — ابتدا مشکل را رفع کنید. + +--- + +## 📞 اطلاعات تماس + +| مورد | مقدار | +|------|------| +| توسعه‌دهنده | VernaSoft Group — Kazem Alghasi | +| ایمیل | kazem@vernasoft.group | +| مخزن گیت | [git.vernahost.ir/gitmodir110/ifnex](https://www.git.vernahost.ir/gitmodir110/ifnex) | +| سرور Laravel | `https://system.ifnex.ir` | +| سرور WordPress | `https://ifnex.ir` | + +--- + +> **نکتهٔ نهایی:** این سند پس از امضای هر دو طرف، به‌عنوان سند رسمی تحویل +> پروژه در آرشیو نگهداری می‌شود. هر تغییر بعدی در نسخه‌های آتی باید با +> سند تغییرات (changelog) همراه باشد. diff --git a/docs/INSTALL-3.11.fa.md b/docs/INSTALL-3.11.fa.md new file mode 100644 index 0000000..3c2f16b --- /dev/null +++ b/docs/INSTALL-3.11.fa.md @@ -0,0 +1,183 @@ +# 📋 بستهٔ فاز ۳.۱۱ — UAT نهایی کارفرما + +تاریخ: ۱۴۰۴/۰۸/۱۱ +نسخه: phase-3.11 + +--- + +## 🎯 هدف + +این بسته فاز نهایی قبل از انتشار رسمی است. شامل: + +1. **`ifnex:smoke-test`** — دستور E2E خودکار برای تست Post-Deploy +2. **۹ تست Feature** برای smoke-test +3. **سند UAT-3.11.md** — چک‌لیست ۴۳ موردی فارسی برای کارفرما +4. **فرم امضای تحویل** — قابل پرینت برای امضای رسمی +5. **به‌روزرسانی CLIENT_DELIVERY.md** — ثبت فاز ۳.۱۰ و ۳.۱۱ + +--- + +## 📦 نحوهٔ نصب + +این ZIP حاوی ۵ فایل است که باید روی ریشهٔ پروژه اکسترکت شوند (Replace). + +### مسیر اکسترکت + +``` +C:\xampp\htdocs\IFNEX-Logistics\ +``` + +### ساختار فایل‌ها + +``` +ifnex-phase-3.11-updates.zip +├── 04_Laravel/ +│ ├── app/Console/Commands/ +│ │ └── IfnexSmokeTest.php ★ جدید +│ ├── bootstrap/ +│ │ └── app.php ← ثبت دستور جدید +│ └── tests/Feature/Console/ +│ └── IfnexSmokeTestTest.php ★ ۹ تست جدید +├── UAT-3.11.md ★ سند UAT کارفرما (۴۳ مورد) +├── CLIENT_DELIVERY.md ← به‌روزرسانی فاز ۳.۱۰/۳.۱۱ +└── docs/ + └── INSTALL-3.11.fa.md ← همین فایل +``` + +`★` = فایل جدید + +--- + +## 🧪 تست روی لوکال + +```powershell +cd C:\xampp\htdocs\IFNEX-Logistics\04_Laravel +php artisan test +``` + +### انتظار + +مجموع تست‌ها: **۷۳ (قبلی) + ۹ (IfnexSmokeTestTest) = ۸۲ تست / ۲۲۰+ assertion** + +همه باید سبز باشند. + +### تست‌های اضافه‌شده + +| Suite | تعداد | توضیح | +|------|------|------| +| `IfnexSmokeTestTest` | ۹ | PASS scenario + ۶ FAIL scenario + custom URL + 422/500 handling | + +--- + +## 🚀 استفاده روی سرور Production + +پس از دیپلوی، قبل از شروع UAT دستی توسط کارفرما: + +```bash +cd /var/www/system.ifnex.ir + +# ۱. تست‌های خودکار واحد +php artisan test +# انتظار: 82+ passed + +# ۲. بررسی پیش‌از-دیپلوی +php artisan ifnex:preflight +# انتظار: همهٔ بررسی‌ها سبز + +# ۳. تست E2E Post-Deploy +php artisan ifnex:smoke-test +# انتظار: همهٔ بررسی‌ها سبز (شامل API endpoints) + +# ۴. اگر از دامنهٔ دیگری استفاده می‌کنید: +php artisan ifnex:smoke-test --url=https://system.ifnex.ir +``` + +اگر هر کدام قرمز بود، UAT را شروع نکنید. + +--- + +## 📋 چک‌لیست UAT کارفرما + +سند `UAT-3.11.md` در ریشهٔ این بسته، چک‌لیست ۴۳ موردی فارسی برای کارفرماست. + +### بخش‌ها: + +| # | بخش | موارد | +|---|------|------| +| ۱ | دسترسی و SSL | ۳ | +| ۲ | احراز هویت مشتری | ۴ | +| ۳ | فرم سفارش Export | ۵ | +| ۴ | فرم سفارش Import | ۴ | +| ۵ | دفترچه آدرس (فاز ۳.۹) | ۳ | +| ۶ | اسناد PDF | ۴ | +| ۷ | تأیید سفارش توسط کارمند | ۳ | +| ۸ | چک‌لیست کارمند | ۲ | +| ۹ | پرداخت با کیف پول | ۲ | +| ۱۰ | پرداخت آنلاین (Zarinpal) | ۲ | +| ۱۱ | کد تخفیف | ۲ | +| ۱۲ | داشبورد و وضعیت مالی | ۳ | +| ۱۳ | اعتبار چندارزی | ۲ | +| ۱۴ | رهگیری مرسوله | ۲ | +| ۱۵ | پنل مدیریت Filament | ۲ | +| **مجموع** | | **۴۳** | + +در انتهای سند، **فرم امضای تحویل** رسمی برای امضای هر دو طرف وجود دارد. + +--- + +## 🔧 دستور `ifnex:smoke-test` چه چیزی را تست می‌کند؟ + +دستور E2E خودکار Post-Deploy، ۲۵+ بررسی انجام می‌دهد: + +### زیرساخت +- ✅ اتصال دیتابیس +- ✅ وجود ۸ جدول اصلی (users, currencies, countries, system_settings, shipments, wallets, wallet_transactions, address_book_entries) +- ✅ قابل‌نوشتن بودن storage و bootstrap/cache +- ✅ اجرای config:cache و route:cache + +### داده‌ها +- ✅ ۵ ارز پایه موجود (USD/AED/EUR/CNY/IRR) +- ✅ نرخ ارزها غیر صفر (به‌جز IRR که ۱ است) +- ✅ حداقل ۱۰۰ کشور در جدول countries +- ✅ ۴ نقش Spatie موجود (super_admin, admin, staff, customer) +- ✅ حداقل یک کاربر ادمین موجود +- ✅ تنظیمات سیستم (vat_rate, profit_margin, packing_cost) + +### API Endpoints +- ✅ `GET /up` → 200 (health check) +- ✅ `GET /api/v1/countries` → 200 (public endpoint) +- ✅ `GET /api/v1/track/{invalid-awb}` → 404 یا 422 (نه ۵۰۰) +- ✅ `GET /api/v1/customer/profile` بدون توکن → 401 (نه ۲۰۰ — تست امنیتی) + +خروجی به‌صورت جدول خوانا با PASS/FAIL، و کد ۰ یا ۱. + +--- + +## 🚀 بعد از تست لوکال + +اگر `php artisan test` سبز بود: + +```powershell +cd C:\xampp\htdocs\IFNEX-Logistics +git add . +git commit -m "feat(uat): phase 3.11 — ifnex:smoke-test E2E command + 9 tests + UAT-3.11.md checklist (43 items)" +git push origin main +``` + +سپس روی سرور Production: + +```bash +cd /var/www/system.ifnex.ir +git pull origin main +php artisan ifnex:smoke-test --url=https://system.ifnex.ir +``` + +اگر سبز بود، سند `UAT-3.11.md` را به کارفرما بدهید تا UAT دستی را شروع کند. + +--- + +## 🎯 گام بعدی + +پس از امضای فرم تحویل توسط کارفرما، پروژه به‌صورت رسمی منتشر می‌شود و وارد +مرحلهٔ پشتیبانی می‌گردد. موارد تأخیری (i18n، SMS، سند مالی) در نسخه‌های +آتی آورده خواهند شد.