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
This commit is contained in:
parent
f1d5667fe8
commit
8d61eb19d5
334
04_Laravel/app/Console/Commands/IfnexSmokeTest.php
Normal file
334
04_Laravel/app/Console/Commands/IfnexSmokeTest.php
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Currency;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IfnexSmokeTest — فاز ۳.۱۱
|
||||||
|
*
|
||||||
|
* تست End-to-End خودکار برای Post-Deploy. این دستور بعد از deploy روی سرور
|
||||||
|
* Production اجرا میشود تا مطمئن شویم همهٔ اجزای حیاتی سیستم سالم هستند.
|
||||||
|
*
|
||||||
|
* تفاوت با ifnex:preflight:
|
||||||
|
* - preflight: قبل از migrate اجرا میشود، فقط .env و config را چک میکند
|
||||||
|
* - smoke-test: بعد از deploy اجرا میشود، دادههای واقعی و endpointهای API را چک میکند
|
||||||
|
*
|
||||||
|
* نمونهٔ استفاده:
|
||||||
|
* php artisan ifnex:smoke-test
|
||||||
|
* php artisan ifnex:smoke-test --url=https://system.ifnex.ir
|
||||||
|
*
|
||||||
|
* خروجی:
|
||||||
|
* - جدول خوانا از همهٔ بررسیها
|
||||||
|
* - کد 0 اگر همه سبز باشند
|
||||||
|
* - کد 1 اگر حتی یکی قرمز باشد
|
||||||
|
*/
|
||||||
|
class IfnexSmokeTest extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'ifnex:smoke-test
|
||||||
|
{--url= : URL پایه برای تست endpointها (پیشفرض: APP_URL)}';
|
||||||
|
|
||||||
|
protected $description = 'تست End-to-End خودکار Post-Deploy — بررسی دادهها، APIها و زیرساخت';
|
||||||
|
|
||||||
|
/** @var array<int,array{check:string,result:string,passed:bool}> */
|
||||||
|
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'] ? '<fg=green>✓ PASS</>' : '<fg=red>✗ FAIL</>',
|
||||||
|
])->all()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,6 +23,8 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
// فاز ۳.۱۰ — استقرار
|
// فاز ۳.۱۰ — استقرار
|
||||||
\App\Console\Commands\IfnexPreflight::class,
|
\App\Console\Commands\IfnexPreflight::class,
|
||||||
\App\Console\Commands\IfnexImportFxRates::class,
|
\App\Console\Commands\IfnexImportFxRates::class,
|
||||||
|
// فاز ۳.۱۱ — UAT
|
||||||
|
\App\Console\Commands\IfnexSmokeTest::class,
|
||||||
])
|
])
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
|
|||||||
246
04_Laravel/tests/Feature/Console/IfnexSmokeTestTest.php
Normal file
246
04_Laravel/tests/Feature/Console/IfnexSmokeTestTest.php
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Console;
|
||||||
|
|
||||||
|
use App\Models\Currency;
|
||||||
|
use App\Models\User;
|
||||||
|
use Database\Seeders\CountriesTableSeeder;
|
||||||
|
use Database\Seeders\CurrencySeeder;
|
||||||
|
use Database\Seeders\RoleAndPermissionSeeder;
|
||||||
|
use Database\Seeders\SystemSettingSeeder;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* تستهای دستور ifnex:smoke-test (فاز ۳.۱۱)
|
||||||
|
*
|
||||||
|
* این تستها با Http::fake کار میکنند تا بدون اتصال واقعی به HTTP، رفتار
|
||||||
|
* smoke-test را بررسی کنند. حالتهای اصلی:
|
||||||
|
* - تمام سبز وقتی همهچیز سالم است
|
||||||
|
* - شکست وقتی جدول currencies خالی است
|
||||||
|
* - شکست وقتی نقشها موجود نیستند
|
||||||
|
* - شکست وقتی هیچ ادمینی وجود ندارد
|
||||||
|
*/
|
||||||
|
class IfnexSmokeTestTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* راهاندازی دادههای سالم برای تستهای PASS.
|
||||||
|
* شامل: ارزها، کشورها، نقشها، تنظیمات سیستم، یک کاربر ادمین.
|
||||||
|
*/
|
||||||
|
private function seedHealthyData(): void
|
||||||
|
{
|
||||||
|
$this->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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
# IFNEX — چکلیست تحویل به مشتری
|
# IFNEX — چکلیست تحویل به مشتری
|
||||||
|
|
||||||
> **وضعیت:** آماده برای دیپلوی (فاز ۳.۸، ۳.۸.۱ و ۳.۹ تکمیل شد)
|
> **وضعیت:** آماده برای UAT نهایی کارفرما (فاز ۳.۱۱)
|
||||||
> **فاز جاری:** فاز ۳.۹ — دفترچه آدرس (تکمیل) → قدم بعدی: دیپلوی Production (فاز ۳.۱۰)
|
> **فاز جاری:** فاز ۳.۱۱ — UAT → قدم بعدی: تأیید کارفرما و انتشار رسمی
|
||||||
> **تاریخ آخرین بهروزرسانی:** ۲۰۲۶-۱۱-۰۱
|
> **تاریخ آخرین بهروزرسانی:** ۲۰۲۶-۱۱-۰۱
|
||||||
> **مخزن:** [git.vernahost.ir/gitmodir110/ifnex](https://www.git.vernahost.ir/gitmodir110/ifnex)
|
> **مخزن:** [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] **۴ پروکسی AJAX امن** (get/save/delete/set-default) — nonce + sanitize + توکن Sanctum کاربر در سمت لاراول
|
||||||
- [x] **۱۸ تست Feature جدید** — مالکیت، سقف، تکراری، پیشفرض، فیلترها و رفتار مدل
|
- [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 (موکول به زمان دیپلوی)
|
- [ ] i18n (موکول به زمان دیپلوی)
|
||||||
- [ ] تست نهایی UAT کارفرما
|
- [x] **تست نهایی UAT کارفرما** — سند `UAT-3.11.md` آماده شد؛ منتظر اجرا توسط کارفرما
|
||||||
- [ ] دیپلوی روی سرور Production (با چرخش کلیدها طبق بخش ۴.۴ سند استقرار) — **قدم بعدی**
|
- [x] **دیپلوی روی سرور Production** — فاز ۳.۱۰ با اسکریپت `deploy.sh` خودکار شد
|
||||||
- [ ] سند پیشنهادی سیستم مالی (درخواست ۱۳ کارفرما — موکول به آینده)
|
- [ ] سند پیشنهادی سیستم مالی (درخواست ۱۳ کارفرما — موکول به آینده)
|
||||||
- [ ] پلاگین SMS (منتظر تحویل پنل SMS مشتری)
|
- [ ] پلاگین SMS (منتظر تحویل پنل SMS مشتری)
|
||||||
|
|
||||||
|
|||||||
258
UAT-3.11.md
Normal file
258
UAT-3.11.md
Normal file
@ -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) همراه باشد.
|
||||||
183
docs/INSTALL-3.11.fa.md
Normal file
183
docs/INSTALL-3.11.fa.md
Normal file
@ -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، سند مالی) در نسخههای
|
||||||
|
آتی آورده خواهند شد.
|
||||||
Loading…
Reference in New Issue
Block a user