Implement critical security hardening and resolve a P0 bug in the discount code consumption flow. This update transitions the system from a development-heavy configuration to a production-ready state. Key changes: - Fix P0 bug where `consumeDiscountCode()` was missing, breaking order registration; replaced with a robust `DiscountCodeService` using `discount_code_usages` for audit trails and idempotency. - Implement strict production guards for the Zarinpal payment gateway to prevent accidental mock processing in live environments. - Refactor WordPress bridge asset management to use `filemtime` based versioning instead of `time()` to prevent unnecessary cache busting. - Remove debug-only hacks (`opcache_reset`, `header` cache control) from the WordPress plugin. - Enhance file upload security by enforcing strict `mimetypes` validation. - Update `.gitignore` to exclude sensitive WordPress configuration files and local PHPUnit caches. - Add comprehensive documentation for production deployment, including security checklists and key rotation procedures. - Update test suite to reflect new architecture and ensure 100% coverage of the discount service.
216 lines
7.1 KiB
PHP
216 lines
7.1 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Services;
|
||
|
||
use App\Models\Country;
|
||
use App\Models\CountryZone;
|
||
use App\Models\RateEntry;
|
||
use App\Models\SystemSetting;
|
||
use App\Services\PriceCalculatorService;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use PHPUnit\Framework\Attributes\Test;
|
||
use Tests\TestCase;
|
||
|
||
/**
|
||
* تست محاسبهگر قیمت — بر اساس معماری ۴-رُبعی (Export/Import × Doc/Parcel)
|
||
* و ذخیرهسازی نرخها به ریال (IRR) — فاز ۳.۷ و ۳.۸.۱
|
||
*/
|
||
class PriceCalculatorServiceTest extends TestCase
|
||
{
|
||
use RefreshDatabase;
|
||
|
||
#[Test]
|
||
public function it_calculates_price_correctly_for_standard_package()
|
||
{
|
||
// ۱. تنظیم SystemSetting ها برای تست
|
||
SystemSetting::create(['key' => 'profit_margin', 'value' => 1.0]); // بدون سود
|
||
SystemSetting::create(['key' => 'vat_rate', 'value' => 0.0]); // بدون مالیات
|
||
|
||
// ۲. ایجاد کشور
|
||
Country::factory()->create([
|
||
'iso_code' => 'US',
|
||
'name' => 'United States',
|
||
'is_active' => true,
|
||
]);
|
||
|
||
// ۳. نگاشت زون — جدول country_zones (سیستم ۴-رُبعی)
|
||
CountryZone::create([
|
||
'country_name' => 'United States',
|
||
'country_iso2' => 'US',
|
||
'document_zone' => 1,
|
||
'parcel_zone' => 1,
|
||
]);
|
||
|
||
// ۴. نرخ حمل — جدول rate_entries، مستقیماً به ریال (IRR)
|
||
RateEntry::create([
|
||
'direction' => 'export',
|
||
'service_type' => 'document',
|
||
'weight' => 3.0,
|
||
'zone' => 1,
|
||
'rate' => 40000.0, // ۴۰,۰۰۰ ریال
|
||
'currency' => 'IRR',
|
||
'effective_at' => now()->subDay(),
|
||
]);
|
||
|
||
// ۵. اجرای سرویس
|
||
$service = app(PriceCalculatorService::class);
|
||
|
||
$data = [
|
||
'direction' => 'Outbound', // ← در سرویس به 'export' تبدیل میشود
|
||
'type' => 'DOC_NORMAL',
|
||
'country_iso' => 'US',
|
||
'weight' => 2.5,
|
||
'volumetric_weight' => 3.0,
|
||
'extra_service' => 10000, // ۱۰,۰۰۰ ریال
|
||
'packing_cost' => 0,
|
||
'domestic_pickup' => 0,
|
||
'domestic_delivery' => 0,
|
||
'warehousing_cost' => 0,
|
||
];
|
||
|
||
$result = $service->calculate($data);
|
||
|
||
// ۶. بررسی ساختار خروجی
|
||
$this->assertIsArray($result);
|
||
$this->assertArrayHasKey('total_fee', $result);
|
||
$this->assertArrayHasKey('base_price', $result);
|
||
|
||
// ۷. بررسی مقادیر
|
||
$this->assertEquals(1, $result['zone']);
|
||
$this->assertEquals(3.0, $result['chargeable_weight']);
|
||
$this->assertEquals(40000.0, $result['base_price']);
|
||
$this->assertFalse($result['discount_applied']);
|
||
|
||
// محاسبهٔ دستی (همه به ریال):
|
||
// base_price = 40000
|
||
// net_rial = 40000 × 1.0 (سود) + 10000 (خدمات) = 50000
|
||
// vat = 0
|
||
// تخفیف = 0
|
||
// total_fee = گردش به نزدیکترین ۱۰۰۰ = 50000
|
||
$this->assertEquals(50000, $result['total_fee']);
|
||
$this->assertEquals(50000, $result['net_rial']);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_applies_discount_from_code_in_pricing()
|
||
{
|
||
SystemSetting::create(['key' => 'profit_margin', 'value' => 1.0]);
|
||
SystemSetting::create(['key' => 'vat_rate', 'value' => 0.0]);
|
||
|
||
Country::factory()->create([
|
||
'iso_code' => 'DE',
|
||
'name' => 'Germany',
|
||
'is_active' => true,
|
||
]);
|
||
|
||
CountryZone::create([
|
||
'country_name' => 'Germany',
|
||
'country_iso2' => 'DE',
|
||
'document_zone' => 2,
|
||
'parcel_zone' => 2,
|
||
]);
|
||
|
||
RateEntry::create([
|
||
'direction' => 'export',
|
||
'service_type' => 'document',
|
||
'weight' => 1.0,
|
||
'zone' => 2,
|
||
'rate' => 100000.0, // ۱۰۰,۰۰۰ ریال
|
||
'currency' => 'IRR',
|
||
'effective_at' => now()->subDay(),
|
||
]);
|
||
|
||
// کد تخفیف ۱۰٪ فعال با ظرفیت
|
||
\App\Models\DiscountCode::create([
|
||
'code' => 'PCT10',
|
||
'type' => 'percentage',
|
||
'value' => 10,
|
||
'usage_limit' => 10,
|
||
'used_count' => 0,
|
||
'is_active' => true,
|
||
]);
|
||
|
||
$service = app(PriceCalculatorService::class);
|
||
|
||
$result = $service->calculate([
|
||
'direction' => 'export',
|
||
'type' => 'DOC_NORMAL',
|
||
'country_iso' => 'DE',
|
||
'weight' => 1.0,
|
||
'volumetric_weight' => 0,
|
||
'extra_service' => 0,
|
||
'packing_cost' => 0,
|
||
'domestic_pickup' => 0,
|
||
'domestic_delivery' => 0,
|
||
'warehousing_cost' => 0,
|
||
'discount_code' => 'PCT10',
|
||
]);
|
||
|
||
// base = 100000، سود = 1.0، vat = 0 → subtotal = 100000
|
||
// تخفیف = ۱۰٪ = 10000 → total = 90000 (گردش به ۱۰۰۰)
|
||
$this->assertTrue($result['discount_applied']);
|
||
$this->assertEquals(10000, $result['discount_amount']);
|
||
$this->assertEquals(90000, $result['total_fee']);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_ignores_discount_code_whose_usage_limit_is_reached()
|
||
{
|
||
SystemSetting::create(['key' => 'profit_margin', 'value' => 1.0]);
|
||
SystemSetting::create(['key' => 'vat_rate', 'value' => 0.0]);
|
||
|
||
Country::factory()->create([
|
||
'iso_code' => 'AE',
|
||
'name' => 'United Arab Emirates',
|
||
'is_active' => true,
|
||
]);
|
||
|
||
CountryZone::create([
|
||
'country_name' => 'United Arab Emirates',
|
||
'country_iso2' => 'AE',
|
||
'document_zone' => 1,
|
||
'parcel_zone' => 1,
|
||
]);
|
||
|
||
RateEntry::create([
|
||
'direction' => 'export',
|
||
'service_type' => 'document',
|
||
'weight' => 1.0,
|
||
'zone' => 1,
|
||
'rate' => 100000.0,
|
||
'currency' => 'IRR',
|
||
'effective_at' => now()->subDay(),
|
||
]);
|
||
|
||
// کد فعال ولی ظرفیتش پر شده — نباید در قیمت اعمال شود (سختسازی)
|
||
\App\Models\DiscountCode::create([
|
||
'code' => 'FULL',
|
||
'type' => 'percentage',
|
||
'value' => 50,
|
||
'usage_limit' => 3,
|
||
'used_count' => 3,
|
||
'is_active' => true,
|
||
]);
|
||
|
||
$service = app(PriceCalculatorService::class);
|
||
|
||
$result = $service->calculate([
|
||
'direction' => 'export',
|
||
'type' => 'DOC_NORMAL',
|
||
'country_iso' => 'AE',
|
||
'weight' => 1.0,
|
||
'volumetric_weight' => 0,
|
||
'extra_service' => 0,
|
||
'packing_cost' => 0,
|
||
'domestic_pickup' => 0,
|
||
'domestic_delivery' => 0,
|
||
'warehousing_cost' => 0,
|
||
'discount_code' => 'FULL',
|
||
]);
|
||
|
||
$this->assertFalse($result['discount_applied']);
|
||
$this->assertEquals(0, $result['discount_amount']);
|
||
$this->assertEquals(100000, $result['total_fee']);
|
||
}
|
||
}
|