Files

189 lines
7.0 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Api\Service;
use App\Enums\Order\OrderStatus;
use App\Models\Coupon;
use App\Models\CouponRedemption;
use App\Models\Customer;
use App\Models\Order;
use App\Models\Product;
use App\Models\ProductPricings;
use App\Services\Api\Order\OrderService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus;
use Illuminate\Testing\TestResponse;
use Tests\TestCase;
class CouponOrderTest extends TestCase
{
use RefreshDatabase;
private Customer $customer;
private Product $product;
protected function setUp(): void
{
parent::setUp();
$this->customer = Customer::factory()->create(['total' => 10_000_000]);
$this->product = Product::factory()->create(['product_type' => 'vps', 'is_active' => true]);
ProductPricings::create([
'product_id' => $this->product->id,
'billing_cycle' => 'monthly',
'price' => 100_000,
'is_active' => true,
]);
}
private function coupon(array $overrides = []): Coupon
{
return Coupon::create(array_merge([
'code' => 'SALE10',
'type' => Coupon::TYPE_PERCENT,
'value' => 10,
'applies_to' => Coupon::APPLIES_NEW,
'is_active' => true,
], $overrides));
}
private function as(Customer $customer): static
{
return $this->actingAs($customer, 'client')
->withHeaders(['x-access-token' => $this->buildClientAccessToken()]);
}
private function order(string $code, array $extra = [], ?Customer $customer = null): TestResponse
{
return $this->as($customer ?? $this->customer)->postJson('/v1/orders', array_merge([
'product_id' => $this->product->uuid,
'billing_cycle' => 'monthly',
'coupon_code' => $code,
], $extra));
}
public function test_preview_returns_discount_capped_by_max_discount(): void
{
$this->coupon(['value' => 50, 'max_discount' => 20_000]);
$this->as($this->customer)->postJson('/v1/orders/coupon-preview', [
'product_id' => $this->product->uuid,
'billing_cycle' => 'monthly',
'coupon_code' => ' sale10 ',
])->assertOk()
->assertJsonPath('data.discount', 20_000)
->assertJsonPath('data.total', 80_000);
}
public function test_order_stores_discount_on_order_invoice_and_charges_discounted_total(): void
{
Bus::fake();
$this->coupon();
$response = $this->order('SALE10', ['discount_amount' => 99_999, 'total' => 1, 'price' => 1])
->assertCreated();
$order = Order::where('order_num', $response->json('data.order.order_num'))->firstOrFail();
$this->assertSame(100_000, (int) $order->sub_total);
$this->assertSame(10_000, (int) $order->discount_amount);
$this->assertSame(90_000, (int) $order->total);
$this->assertSame('SALE10', $order->coupon_code);
$this->assertSame(10_000, (int) $order->invoice->discount_amount);
$this->assertSame(90_000, (int) $order->invoice->total);
$this->assertSame(100_000, (int) $order->invoice->sub_total);
$this->assertSame('SALE10', $order->invoice->coupon_code);
$this->assertSame(1, CouponRedemption::where('order_id', $order->id)->whereNull('released_at')->count());
app(OrderService::class)->pay($order->fresh(['invoice', 'customer']), 'balance');
$this->assertSame(10_000_000 - 90_000, $this->customer->fresh()->total);
}
public function test_client_cannot_override_price_without_coupon(): void
{
Bus::fake();
$response = $this->order('', ['coupon_code' => null, 'discount_amount' => 50_000, 'total' => 1])->assertCreated();
$order = Order::where('order_num', $response->json('data.order.order_num'))->firstOrFail();
$this->assertSame(0, (int) $order->discount_amount);
$this->assertSame(100_000, (int) $order->total);
}
public function test_expired_and_not_started_and_inactive_codes_are_rejected(): void
{
$this->coupon(['code' => 'OLD', 'ends_at' => now()->subDay()]);
$this->coupon(['code' => 'SOON', 'starts_at' => now()->addDay()]);
$this->coupon(['code' => 'OFF', 'is_active' => false]);
foreach (['OLD', 'SOON', 'OFF', 'MISSING'] as $code) {
$this->order($code)->assertStatus(422);
}
$this->assertSame(0, Order::count());
}
public function test_scope_and_minimum_order_are_enforced(): void
{
$this->coupon(['code' => 'HOST', 'product_types' => ['hosting']]);
$this->coupon(['code' => 'CYCLE', 'billing_cycles' => ['annually']]);
$this->coupon(['code' => 'BIG', 'min_order' => 500_000]);
$this->coupon(['code' => 'RENEW', 'applies_to' => Coupon::APPLIES_RENEWAL]);
$this->coupon(['code' => 'OTHERPROD', 'product_ids' => [$this->product->id + 999]]);
foreach (['HOST', 'CYCLE', 'BIG', 'RENEW', 'OTHERPROD'] as $code) {
$this->order($code)->assertStatus(422);
}
$this->assertSame(0, Order::count());
}
public function test_usage_limit_is_enforced_across_customers(): void
{
Bus::fake();
$this->coupon(['usage_limit' => 1]);
$other = Customer::factory()->create(['total' => 10_000_000]);
$this->order('SALE10')->assertCreated();
$this->order('SALE10', [], $other)->assertStatus(422)
->assertJsonPath('message', 'Mã giảm giá đã hết lượt sử dụng.');
}
public function test_per_customer_limit_is_enforced(): void
{
Bus::fake();
$this->coupon(['per_customer_limit' => 1]);
$first = $this->order('SALE10')->assertCreated();
Order::where('order_num', $first->json('data.order.order_num'))->update(['status' => OrderStatus::ACTIVE->value]);
$this->order('SALE10')->assertStatus(422)
->assertJsonPath('message', 'Bạn đã dùng hết số lần cho phép của mã giảm giá này.');
}
public function test_cancelling_order_releases_redemption_and_restoring_reclaims_it(): void
{
Bus::fake();
$this->coupon(['usage_limit' => 1]);
$response = $this->order('SALE10')->assertCreated();
$order = Order::where('order_num', $response->json('data.order.order_num'))->firstOrFail();
$order->update(['status' => OrderStatus::CANCELLED->value]);
$this->assertNotNull(CouponRedemption::where('order_id', $order->id)->value('released_at'));
$order->update(['status' => OrderStatus::PENDING->value]);
$this->assertNull(CouponRedemption::where('order_id', $order->id)->value('released_at'));
}
public function test_discount_never_drops_total_below_minimum_payable(): void
{
Bus::fake();
$this->coupon(['code' => 'FREE', 'type' => Coupon::TYPE_FIXED, 'value' => 5_000_000]);
$this->order('FREE')->assertCreated();
$this->assertSame(1_000, (int) Order::first()->invoice->total);
}
}