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.
185 lines
5.4 KiB
PHP
185 lines
5.4 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Services;
|
||
|
||
use App\Exceptions\DiscountCodeUnavailableException;
|
||
use App\Models\DiscountCode;
|
||
use App\Models\DiscountCodeUsage;
|
||
use App\Models\Shipment;
|
||
use App\Models\User;
|
||
use App\Services\DiscountCodeService;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use Illuminate\Support\Facades\DB;
|
||
use PHPUnit\Framework\Attributes\Test;
|
||
use Tests\TestCase;
|
||
|
||
/**
|
||
* تستهای سرویس مصرف کد تخفیف (سختسازی Production — فاز ۳.۸.۱)
|
||
*/
|
||
class DiscountCodeServiceTest extends TestCase
|
||
{
|
||
use RefreshDatabase;
|
||
|
||
private User $user;
|
||
private DiscountCodeService $service;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
|
||
$this->user = User::factory()->create();
|
||
$this->service = app(DiscountCodeService::class);
|
||
}
|
||
|
||
private function makeShipment(): Shipment
|
||
{
|
||
return Shipment::create([
|
||
'user_id' => $this->user->id,
|
||
'awb_no' => 'IFN-2026-' . str_pad((string) random_int(1, 99999), 5, '0', STR_PAD_LEFT),
|
||
'direction' => 'export',
|
||
'type' => 'DOC_NORMAL',
|
||
]);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_consumes_code_and_increments_used_count()
|
||
{
|
||
$code = DiscountCode::create([
|
||
'code' => 'SAVE10',
|
||
'type' => 'percentage',
|
||
'value' => 10,
|
||
'usage_limit' => 5,
|
||
'used_count' => 0,
|
||
'is_active' => true,
|
||
]);
|
||
|
||
$shipment = $this->makeShipment();
|
||
|
||
$result = DB::transaction(fn () => $this->service->consumeForOrder(
|
||
'SAVE10',
|
||
$this->user,
|
||
$shipment,
|
||
discountedAmount: 50000
|
||
));
|
||
|
||
$this->assertTrue($result);
|
||
$this->assertEquals(1, $code->fresh()->used_count);
|
||
|
||
// رکورد Audit ثبت شده است
|
||
$usage = DiscountCodeUsage::where('discount_code_id', $code->id)->first();
|
||
$this->assertNotNull($usage);
|
||
$this->assertEquals($this->user->id, $usage->user_id);
|
||
$this->assertEquals($shipment->id, $usage->shipment_id);
|
||
$this->assertEquals(50000, (float) $usage->discounted_amount);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_is_idempotent_for_the_same_shipment()
|
||
{
|
||
$code = DiscountCode::create([
|
||
'code' => 'ONCE',
|
||
'type' => 'fixed',
|
||
'value' => 10000,
|
||
'usage_limit' => 5,
|
||
'used_count' => 0,
|
||
'is_active' => true,
|
||
]);
|
||
|
||
$shipment = $this->makeShipment();
|
||
|
||
DB::transaction(fn () => $this->service->consumeForOrder('ONCE', $this->user, $shipment));
|
||
DB::transaction(fn () => $this->service->consumeForOrder('ONCE', $this->user, $shipment));
|
||
|
||
// مصرف دوباره روی همان سفارش نباید شمارنده را بالا ببرد (Idempotency)
|
||
$this->assertEquals(1, $code->fresh()->used_count);
|
||
$this->assertEquals(1, DiscountCodeUsage::where('discount_code_id', $code->id)->count());
|
||
}
|
||
|
||
#[Test]
|
||
public function it_throws_when_usage_limit_is_reached()
|
||
{
|
||
DiscountCode::create([
|
||
'code' => 'LIMIT2',
|
||
'type' => 'fixed',
|
||
'value' => 10000,
|
||
'usage_limit' => 2,
|
||
'used_count' => 2, // ظرفیت پر شده
|
||
'is_active' => true,
|
||
]);
|
||
|
||
$shipment = $this->makeShipment();
|
||
|
||
$this->expectException(DiscountCodeUnavailableException::class);
|
||
$this->expectExceptionMessage('ظرفیت');
|
||
|
||
DB::transaction(fn () => $this->service->consumeForOrder('LIMIT2', $this->user, $shipment));
|
||
}
|
||
|
||
#[Test]
|
||
public function it_rejects_inactive_code()
|
||
{
|
||
DiscountCode::create([
|
||
'code' => 'OFF',
|
||
'type' => 'fixed',
|
||
'value' => 10000,
|
||
'is_active' => false,
|
||
]);
|
||
|
||
$shipment = $this->makeShipment();
|
||
|
||
$this->expectException(DiscountCodeUnavailableException::class);
|
||
|
||
DB::transaction(fn () => $this->service->consumeForOrder('OFF', $this->user, $shipment));
|
||
}
|
||
|
||
#[Test]
|
||
public function it_rejects_expired_code()
|
||
{
|
||
DiscountCode::create([
|
||
'code' => 'OLD',
|
||
'type' => 'fixed',
|
||
'value' => 10000,
|
||
'is_active' => true,
|
||
'expires_at' => now()->subDay(),
|
||
]);
|
||
|
||
$shipment = $this->makeShipment();
|
||
|
||
$this->expectException(DiscountCodeUnavailableException::class);
|
||
|
||
DB::transaction(fn () => $this->service->consumeForOrder('OLD', $this->user, $shipment));
|
||
}
|
||
|
||
#[Test]
|
||
public function it_does_nothing_when_code_is_empty()
|
||
{
|
||
$shipment = $this->makeShipment();
|
||
|
||
// بدون کد — نه خطا، نه مصرف
|
||
$this->assertFalse(
|
||
DB::transaction(fn () => $this->service->consumeForOrder(null, $this->user, $shipment))
|
||
);
|
||
$this->assertFalse(
|
||
DB::transaction(fn () => $this->service->consumeForOrder(' ', $this->user, $shipment))
|
||
);
|
||
$this->assertEquals(0, DiscountCodeUsage::count());
|
||
}
|
||
|
||
#[Test]
|
||
public function try_consume_returns_false_instead_of_throwing()
|
||
{
|
||
DiscountCode::create([
|
||
'code' => 'DEAD',
|
||
'type' => 'fixed',
|
||
'value' => 10000,
|
||
'is_active' => false,
|
||
]);
|
||
|
||
$shipment = $this->makeShipment();
|
||
|
||
$this->assertFalse(
|
||
$this->service->tryConsumeForOrder('DEAD', $this->user, $shipment)
|
||
);
|
||
}
|
||
}
|