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