parfum/tests/Feature/Studio/Manage/Order/CreateTest.php

418 lines
13 KiB
PHP

<?php
use App\Enums\Gender;
use App\Enums\PaymentStatus;
use App\Enums\VoucherType;
use App\Livewire\Studio\Manage\Order\Create;
use App\Models\Bottle;
use App\Models\Formula;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Perfume;
use App\Models\Product;
use App\Models\User;
use App\Models\Voucher;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
beforeEach(function () {
$this->setupUser();
$role = Role::firstOrCreate(['name' => 'Owner']);
$permissions = ['view order', 'create order'];
foreach ($permissions as $p) {
Permission::firstOrCreate(['name' => $p]);
}
$role->syncPermissions($permissions);
$this->user->assignRole($role);
// Ensure user has an outlet
$this->user->outlets()->sync([$this->outlet->id]);
$this->tier = \App\Models\Tier::factory()->create();
// Create other roles required by the component logic
Role::firstOrCreate(['name' => 'Developer']);
Role::firstOrCreate(['name' => 'Leader']);
});
it('renders the order create page', function () {
$this->actingAs($this->user)
->get(route('studio.manage.order.create'))
->assertOk()
->assertSeeLivewire(Create::class);
});
it('can add a product to the cart', function () {
$product = Product::factory()->create(['sale_price' => 50000]);
$this->outlet->products()->attach($product->id, ['stock' => 100]);
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.outlet_id', $this->outlet->id)
->set('form.product_id', $product->id)
->set('form.quantity_product', '2')
->call('addProduct')
->assertHasNoErrors();
$this->assertDatabaseHas('order_items', [
'user_id' => $this->user->id,
'orderable_type' => Product::class,
'orderable_id' => $product->id,
'quantity' => 2,
'unit_price' => 50000,
'order_id' => null,
]);
});
it('can add a new perfume (perfume + bottle) to the cart', function () {
$perfume = Perfume::factory()->create(['sale_price' => 1000]); // per ml
$bottle = Bottle::factory()->create(['size' => '30ml', 'sale_price' => 15000]);
$formula = Formula::factory()->create(['size' => '30ml', 'volume' => 30, 'quality' => 'Premium']);
$this->outlet->perfumes()->attach($perfume->id, ['stock' => 1000]);
$this->outlet->bottles()->attach($bottle->id, ['stock' => 50]);
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.outlet_id', $this->outlet->id)
->set('tab', 'new')
->set('form.perfume_id', $perfume->id)
->set('form.bottle_id', $bottle->id)
->set('form.quality_id', $formula->id)
->call('addPerfume')
->assertHasNoErrors();
// Should add both perfume and bottle
$this->assertDatabaseHas('order_items', [
'user_id' => $this->user->id,
'orderable_type' => Perfume::class,
'orderable_id' => $perfume->id,
'quantity' => 30,
'quality' => 'Premium',
'order_id' => null,
]);
$this->assertDatabaseHas('order_items', [
'user_id' => $this->user->id,
'orderable_type' => Bottle::class,
'orderable_id' => $bottle->id,
'quantity' => 1,
'order_id' => null,
]);
});
it('can add a new perfume with custom quality', function () {
$perfume = Perfume::factory()->create(['sale_price' => 1000]);
$bottle = Bottle::factory()->create(['size' => 'Custom', 'sale_price' => 5000]);
$this->outlet->perfumes()->attach($perfume->id, ['stock' => 1000]);
$this->outlet->bottles()->attach($bottle->id, ['stock' => 50]);
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.outlet_id', $this->outlet->id)
->set('tab', 'new')
->set('form.perfume_id', $perfume->id)
->set('form.bottle_id', $bottle->id)
->set('form.quality_id', 'custom')
->set('form.bottle_size', '15')
->call('addPerfume')
->assertHasNoErrors();
$this->assertDatabaseHas('order_items', [
'user_id' => $this->user->id,
'orderable_type' => Perfume::class,
'orderable_id' => $perfume->id,
'quantity' => 15,
'quality' => 'Custom',
'order_id' => null,
]);
});
it('can add a refill perfume with custom quality', function () {
$perfume = Perfume::factory()->create(['sale_price' => 1000]);
$this->outlet->perfumes()->attach($perfume->id, ['stock' => 1000]);
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.outlet_id', $this->outlet->id)
->set('tab', 'refill')
->set('form.perfume_id', $perfume->id)
->set('form.quality_id', 'custom')
->set('form.bottle_size', '50')
->call('addPerfume')
->assertHasNoErrors();
$this->assertDatabaseHas('order_items', [
'user_id' => $this->user->id,
'orderable_type' => Perfume::class,
'orderable_id' => $perfume->id,
'quantity' => 50,
'quality' => 'Custom',
'order_id' => null,
]);
});
it('separates items with different qualities', function () {
$perfume = Perfume::factory()->create(['sale_price' => 1000]);
$bottle = Bottle::factory()->create(['size' => '30ml']);
$formula1 = Formula::factory()->create(['size' => '30ml', 'volume' => 30, 'quality' => 'Premium']);
$formula2 = Formula::factory()->create(['size' => '30ml', 'volume' => 30, 'quality' => 'Super']);
$this->outlet->perfumes()->attach($perfume->id, ['stock' => 1000]);
$this->outlet->bottles()->attach($bottle->id, ['stock' => 100]);
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.outlet_id', $this->outlet->id)
->set('tab', 'new')
->set('form.perfume_id', $perfume->id)
->set('form.bottle_id', $bottle->id)
->set('form.quality_id', $formula1->id)
->call('addPerfume')
->set('form.perfume_id', $perfume->id)
->set('form.bottle_id', $bottle->id)
->set('form.quality_id', $formula2->id) // Switch quality
->call('addPerfume');
$this->assertDatabaseHas('order_items', [
'orderable_id' => $perfume->id,
'quality' => 'Premium',
'quantity' => 30,
]);
$this->assertDatabaseHas('order_items', [
'orderable_id' => $perfume->id,
'quality' => 'Super',
'quantity' => 30,
]);
// Bottle SHOULD aggregated to 2 (since bottle isn't a perfume and logic allows increment)
$this->assertDatabaseHas('order_items', [
'orderable_id' => $bottle->id,
'quantity' => 2,
]);
$this->assertDatabaseCount('order_items', 3); // 2 perfumes (diff quality) + 1 bottle (aggregated)
});
it('can update item quantity in the cart', function () {
$product = Product::factory()->create(['sale_price' => 50000]);
$item = OrderItem::factory()->forProduct($product)->create([
'user_id' => $this->user->id,
'order_id' => null,
'quantity' => 1,
'unit_price' => 50000,
]);
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.item_id', $item->id)
->set('form.quantity_edit', '5')
->call('updateItem')
->assertHasNoErrors();
$this->assertDatabaseHas('order_items', [
'id' => $item->id,
'quantity' => 5,
]);
});
it('can delete an item from the cart', function () {
$product = Product::factory()->create();
$item = OrderItem::factory()->forProduct($product)->create([
'user_id' => $this->user->id,
'order_id' => null,
]);
Livewire::actingAs($this->user)
->test(Create::class)
->call('deleteItem', $item->id)
->assertHasNoErrors();
$this->assertSoftDeleted('order_items', ['id' => $item->id]);
});
it('can store an order and update outlet stock', function () {
$product = Product::factory()->create(['sale_price' => 100000, 'cost_price' => 60000]);
$this->outlet->products()->attach($product->id, ['stock' => 10]);
OrderItem::factory()->forProduct($product)->create([
'user_id' => $this->user->id,
'order_id' => null,
'quantity' => 2,
'unit_price' => 100000,
'cogs' => 60000,
]);
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.outlet_id', $this->outlet->id)
->set('form.payment_method', '1') // Cash
->call('save')
->assertHasNoErrors()
->assertRedirect();
$this->assertDatabaseHas('orders', [
'outlet_id' => $this->outlet->id,
'user_id' => $this->user->id,
'total' => 200000,
'subtotal' => 200000,
]);
// Check payment created
$order = Order::latest()->first();
$this->assertDatabaseHas('payments', [
'order_id' => $order->id,
'amount' => 200000,
'status' => PaymentStatus::PAID->value,
]);
// Check stock updated
$this->assertDatabaseHas('outlet_product', [
'outlet_id' => $this->outlet->id,
'product_id' => $product->id,
'stock' => 8,
]);
});
it('can store an order with member and earn points', function () {
$product = Product::factory()->create(['sale_price' => 100000]);
$this->outlet->products()->attach($product->id, ['stock' => 10]);
$memberUser = User::factory()->create();
$memberUser->customer()->create([
'name' => 'Member Test',
'gender' => Gender::MALE->value,
]);
$memberUser->membership()->create([
'total_spending' => 0,
'reward_points' => 0,
'tier_id' => $this->tier->id,
]);
OrderItem::factory()->forProduct($product)->create([
'user_id' => $this->user->id,
'order_id' => null,
'quantity' => 1,
'unit_price' => 100000,
]);
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.outlet_id', $this->outlet->id)
->set('form.member_id', $memberUser->customer->id)
->call('save')
->assertHasNoErrors();
// Check points earned (100000 / 5000 = 20 points)
$this->assertDatabaseHas('memberships', [
'user_id' => $memberUser->id,
'reward_points' => 20,
'total_spending' => 100000,
]);
$this->assertDatabaseHas('point_records', [
'user_id' => $memberUser->id,
'change' => 20,
'is_addition' => true,
]);
});
it('can store an order with voucher', function () {
$product = Product::factory()->create(['sale_price' => 100000]);
$this->outlet->products()->attach($product->id, ['stock' => 10]);
$memberUser = User::factory()->create();
$customer = $memberUser->customer()->create([
'name' => 'Voucher User',
'gender' => Gender::MALE->value,
]);
$memberUser->membership()->create([
'tier_id' => $this->tier->id,
'total_spending' => 0,
'reward_points' => 0,
]);
$voucher = Voucher::factory()->create([
'type' => VoucherType::FIXED->value,
'discount_amount' => 20000,
'start_date' => now()->subDay()->format('Y-m-d'), // Make it active
]);
// Assign voucher to user and outlet
DB::table('user_voucher')->insert([
'user_id' => $memberUser->id,
'voucher_id' => $voucher->id,
'quantity' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
$voucher->outlets()->attach($this->outlet->id);
OrderItem::factory()->forProduct($product)->create([
'user_id' => $this->user->id,
'order_id' => null,
'quantity' => 1,
'unit_price' => 100000,
]);
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.outlet_id', $this->outlet->id)
->set('form.member_id', $memberUser->customer->id)
->set('form.voucher_id', $voucher->id)
->set('form.payment_method', '1') // Cash
->call('save')
->assertHasNoErrors()
->assertRedirect();
$this->assertDatabaseHas('orders', [
'voucher_id' => $voucher->id,
'total' => 80000,
'discount' => 0, // Manual discount
'voucher_discount' => 20000, // Voucher discount
]);
// Check voucher used up
$this->assertDatabaseMissing('user_voucher', [
'user_id' => $memberUser->id,
'voucher_id' => $voucher->id,
]);
});
it('cannot create order with empty cart', function () {
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.outlet_id', $this->outlet->id)
->call('save')
->assertHasNoErrors(); // Shows toast
$this->assertDatabaseCount('orders', 0);
});
it('validates required fields', function () {
// Add something to cart first to pass empty cart check
OrderItem::factory()->forProduct()->create(['user_id' => $this->user->id, 'order_id' => null]);
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.outlet_id', '')
->call('save')
->assertHasErrors(['form.outlet_id' => 'required']);
});
it('cannot access order create without permission', function () {
$this->user->roles()->detach();
$this->user->permissions()->detach();
$this->actingAs($this->user)
->get(route('studio.manage.order.create'))
->assertForbidden();
});