189 lines
7.0 KiB
PHP
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);
|
|
}
|
|
}
|