ifnex/04_Laravel/tests/Feature/Services/DiscountCodeServiceTest.php
Kazem Alghasi b9ede16613 refactor(core): harden production environment and fix discount logic
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.
2026-10-31 19:18:58 +03:30

185 lines
5.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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)
);
}
}