From eef87d176f901e2c040ccd2cab7f0978b1393fc7 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Mon, 8 Dec 2025 10:04:38 +0700 Subject: [PATCH] feat(tier): implementasi test dan memperbaiki after create and update di component nya --- app/Livewire/Studio/Loyalty/Tier.php | 17 +- .../Livewire/Studio/Loyalty/TierTest.php | 636 +++++++++++++++++- 2 files changed, 613 insertions(+), 40 deletions(-) diff --git a/app/Livewire/Studio/Loyalty/Tier.php b/app/Livewire/Studio/Loyalty/Tier.php index 388db73..a9fd925 100644 --- a/app/Livewire/Studio/Loyalty/Tier.php +++ b/app/Livewire/Studio/Loyalty/Tier.php @@ -39,8 +39,8 @@ public function mount() #[On('data:tiersUpdated')] public function loadTiers() { - $this->tiers = TierModel::with(['memberships.user.customer.orders.items']) - ->withCount('memberships') + $this->tiers = TierModel::with(['rewards', 'memberships.user.customer.orders.items']) + ->withCount(['memberships', 'rewards']) ->orderBy('min_points', 'asc') ->get() ->map(function ($tier) { @@ -89,7 +89,7 @@ public function loadTiers() 'min_points' => currency($tier->min_points), 'max_points' => currency($tier->max_points), 'total_members' => currency($tier->memberships_count), - 'total_rewards' => $tier->rewards->count(), + 'total_rewards' => currency($tier->rewards_count), 'top_member' => $topMemberStats, ]; }) @@ -118,12 +118,14 @@ public function create() $tiers = $this->tiers; $tiers[] = [ + 'id' => $tier->id, 'hash' => $tier->hash, 'name' => $tier->name, 'min_points' => currency($tier->min_points, ''), 'max_points' => currency($tier->max_points, ''), 'total_members' => 0, 'top_member' => null, + 'total_rewards' => 0, ]; usort($tiers, fn ($a, $b) => replaceCurrency($a['min_points']) <=> replaceCurrency($b['min_points'])); @@ -145,12 +147,9 @@ public function update() $tiers = $this->tiers; foreach ($tiers as $index => $item) { if ($item['hash'] === $tier->hash) { - $tiers[$index] = [ - 'hash' => $tier->hash, - 'name' => $tier->name, - 'min_points' => currency($tier->min_points, ''), - 'max_points' => currency($tier->max_points, ''), - ]; + $tiers[$index]['name'] = $tier->name; + $tiers[$index]['min_points'] = currency($tier->min_points, ''); + $tiers[$index]['max_points'] = currency($tier->max_points, ''); break; } } diff --git a/tests/Feature/Livewire/Studio/Loyalty/TierTest.php b/tests/Feature/Livewire/Studio/Loyalty/TierTest.php index aebe9ae..9733519 100644 --- a/tests/Feature/Livewire/Studio/Loyalty/TierTest.php +++ b/tests/Feature/Livewire/Studio/Loyalty/TierTest.php @@ -1,69 +1,643 @@ seed(RolePermissionSeeder::class); + + $this->user = User::factory() + ->active() + ->has(Employee::factory()) + ->create(); + + $this->ownerRole = Role::where('name', 'Owner')->first(); + + $this->user->roles()->attach($this->ownerRole->id); +}); + +function mountTierComponent(User $user) +{ + return Livewire::actingAs($user)->test(Tier::class); +} + +/* +|-------------------------------------------------------------------------- +| Component Rendering & Mount +|-------------------------------------------------------------------------- +*/ + +it('renders page successfully', function () { + mountTierComponent($this->user) ->assertViewIs('livewire.studio.loyalty.tiers') ->assertViewHas('pageTitle', 'Tier'); }); -it('displays all tiers', function () { - TierModel::factory()->count(10)->create(); +it('loads tiers on mount', function () { + $tier1 = TierModel::factory()->create(['name' => 'Bronze', 'min_points' => 0, 'max_points' => 100]); + $tier2 = TierModel::factory()->create(['name' => 'Silver', 'min_points' => 101, 'max_points' => 500]); - $tier = TierModel::first(); + $component = mountTierComponent($this->user); - Livewire::test(Tier::class)->assertSee($tier->name); + expect($component->tiers)->toHaveCount(2); + expect($component->tiers[0]['name'])->toBe('Bronze'); + expect($component->tiers[1]['name'])->toBe('Silver'); }); -it('can create a tier', function () { - $data = TierModel::factory()->raw(); +it('displays all tiers in the view', function () { + $tier = TierModel::factory()->create(['name' => 'Gold', 'min_points' => 100, 'max_points' => 500]); - Livewire::test(Tier::class) - ->set('form.name', $data['name']) - ->set('form.min_points', $data['min_points']) - ->set('form.max_points', $data['max_points']) + mountTierComponent($this->user) + ->assertSee('Gold') + ->assertSee('100') + ->assertSee('500'); +}); + +it('sorts tiers by min_points ascending', function () { + $tier3 = TierModel::factory()->create(['name' => 'Platinum', 'min_points' => 1000, 'max_points' => null]); + $tier1 = TierModel::factory()->create(['name' => 'Bronze', 'min_points' => 0, 'max_points' => 100]); + $tier2 = TierModel::factory()->create(['name' => 'Silver', 'min_points' => 101, 'max_points' => 500]); + + $component = mountTierComponent($this->user); + + expect($component->tiers[0]['name'])->toBe('Bronze'); + expect($component->tiers[1]['name'])->toBe('Silver'); + expect($component->tiers[2]['name'])->toBe('Platinum'); +}); + +/* +|-------------------------------------------------------------------------- +| Create Tier +|-------------------------------------------------------------------------- +*/ + +it('prevents create when user is unauthorized', function () { + $this->ownerRole->revokePermissionTo('create tier'); + + Gate::define('create tier', fn () => false); + + mountTierComponent($this->user) + ->set('form.name', 'Gold') + ->set('form.min_points', '1000') + ->set('form.max_points', '5000') + ->call('create') + ->assertForbidden(); +}); + +it('can create a tier when authorized', function () { + mountTierComponent($this->user) + ->call('openModal', 'create', 'Tambah Tier') + ->set('form.name', 'Gold') + ->set('form.min_points', '1.000') + ->set('form.max_points', '5.000') ->call('create') ->assertHasNoErrors() - ->assertDispatched('refreshDatatable'); + ->assertSet('tiers', function ($tiers) { + return count($tiers) === 1 && $tiers[0]['name'] === 'Gold'; + }); - expect(TierModel::count())->toBe(1); - expect(TierModel::first()->name)->toBe($data['name']); + $this->assertDatabaseHas('tiers', [ + 'name' => 'Gold', + 'min_points' => 1000, + 'max_points' => 5000, + ]); }); -it('can update a tier', function () { - $tier = TierModel::factory()->create(); +it('can create a tier without max_points', function () { + mountTierComponent($this->user) + ->call('openModal', 'create', 'Tambah Tier') + ->set('form.name', 'Platinum') + ->set('form.min_points', '5.001') + ->set('form.max_points', null) + ->call('create') + ->assertHasNoErrors(); - $data = TierModel::factory()->raw(); + $this->assertDatabaseHas('tiers', [ + 'name' => 'Platinum', + 'min_points' => 5001, + 'max_points' => null, + ]); +}); - Livewire::test(Tier::class) - ->call('openModal', 'update', 'Edit Tier', $tier->id) - ->set('form.name', $data['name']) - ->set('form.min_points', $data['min_points']) - ->set('form.max_points', $data['max_points']) +it('sorts tiers after creating new tier', function () { + TierModel::factory()->create(['name' => 'Silver', 'min_points' => 101, 'max_points' => 500]); + TierModel::factory()->create(['name' => 'Bronze', 'min_points' => 0, 'max_points' => 100]); + + $component = mountTierComponent($this->user) + ->call('openModal', 'create', 'Tambah Tier') + ->set('form.name', 'Gold') + ->set('form.min_points', '501') + ->set('form.max_points', '1000') + ->call('create'); + + expect($component->tiers[0]['name'])->toBe('Bronze'); + expect($component->tiers[1]['name'])->toBe('Silver'); + expect($component->tiers[2]['name'])->toBe('Gold'); +}); + +it('validates required fields when creating tier', function () { + mountTierComponent($this->user) + ->call('openModal', 'create', 'Tambah Tier') + ->call('create') + ->assertHasErrors([ + 'form.name', + 'form.min_points', + ]); +}); + +it('validates name field max length when creating tier', function () { + mountTierComponent($this->user) + ->call('openModal', 'create', 'Tambah Tier') + ->set('form.name', str_repeat('a', 51)) + ->set('form.min_points', '100') + ->call('create') + ->assertHasErrors(['form.name']); +}); + +it('validates min_points is unsigned integer when creating tier', function () { + mountTierComponent($this->user) + ->call('openModal', 'create', 'Tambah Tier') + ->set('form.name', 'Gold') + ->set('form.min_points', '-100') + ->call('create') + ->assertHasErrors(['form.min_points']); +}); + +it('validates max_points is unsigned integer when creating tier', function () { + mountTierComponent($this->user) + ->call('openModal', 'create', 'Tambah Tier') + ->set('form.name', 'Gold') + ->set('form.min_points', '100') + ->set('form.max_points', '-50') + ->call('create') + ->assertHasErrors(['form.max_points']); +}); + +/* +|-------------------------------------------------------------------------- +| Update Tier +|-------------------------------------------------------------------------- +*/ + +it('prevents update when user is unauthorized', function () { + $this->ownerRole->revokePermissionTo('update tier'); + Gate::define('update tier', fn () => false); + + $tier = TierModel::factory()->create(['name' => 'Bronze', 'min_points' => 0, 'max_points' => 100]); + + mountTierComponent($this->user) + ->call('openModal', 'update', 'Ubah Tier', $tier->hash) + ->set('form.name', 'Gold') + ->call('update') + ->assertForbidden(); +}); + +it('can update a tier when authorized', function () { + $tier = TierModel::factory()->create(['name' => 'Bronze', 'min_points' => 0, 'max_points' => 100]); + + mountTierComponent($this->user) + ->call('openModal', 'update', 'Ubah Tier', $tier->hash) + ->assertSet('form.name', 'Bronze') + ->assertSet('form.min_points', '0') + ->assertSet('form.max_points', '100') + ->set('form.name', 'Gold') + ->set('form.min_points', '1.000') + ->set('form.max_points', '5.000') ->call('update') ->assertHasNoErrors() - ->assertDispatched('refreshDatatable'); + ->assertSet('tiers', function ($tiers) use ($tier) { + $updatedTier = collect($tiers)->firstWhere('hash', $tier->hash); + + return $updatedTier['name'] === 'Gold'; + }); $tier->refresh(); - - expect($tier->name)->toBe($data['name']); - expect($tier->min_points)->toBe($data['min_points']); - expect($tier->max_points)->toBe($data['max_points']); + expect($tier->name)->toBe('Gold'); + expect($tier->min_points)->toBe(1000); + expect($tier->max_points)->toBe(5000); }); -it('can delete an tier', function () { +it('can update tier without max_points', function () { + $tier = TierModel::factory()->create(['name' => 'Bronze', 'min_points' => 0, 'max_points' => 100]); + + mountTierComponent($this->user) + ->call('openModal', 'update', 'Ubah Tier', $tier->hash) + ->set('form.max_points', null) + ->call('update') + ->assertHasNoErrors(); + + $tier->refresh(); + expect($tier->max_points)->toBeNull(); +}); + +it('validates required fields when updating tier', function () { $tier = TierModel::factory()->create(); - Livewire::test(Tier::class) + mountTierComponent($this->user) + ->call('openModal', 'update', 'Ubah Tier', $tier->hash) + ->set('form.name', '') + ->set('form.min_points', '') + ->call('update') + ->assertHasErrors([ + 'form.name', + 'form.min_points', + ]); +}); + +it('validates name field max length when updating tier', function () { + $tier = TierModel::factory()->create(); + + mountTierComponent($this->user) + ->call('openModal', 'update', 'Ubah Tier', $tier->hash) + ->set('form.name', str_repeat('a', 51)) + ->call('update') + ->assertHasErrors(['form.name']); +}); + +it('resets validation and error bag when opening modal', function () { + $tier = TierModel::factory()->create(); + + $component = mountTierComponent($this->user) + ->call('openModal', 'update', 'Ubah Tier', $tier->hash) + ->set('form.name', '') + ->call('update') + ->assertHasErrors(['form.name']); + + $component->call('openModal', 'update', 'Ubah Tier', $tier->hash) + ->assertHasNoErrors(); +}); + +/* +|-------------------------------------------------------------------------- +| Delete Tier +|-------------------------------------------------------------------------- +*/ + +it('can delete a tier when authorized', function () { + $tier = TierModel::factory()->create(['name' => 'Bronze']); + + mountTierComponent($this->user) ->call('delete', $tier) ->assertHasNoErrors() - ->assertDispatched('refreshDatatable'); + ->assertSet('tiers', function ($tiers) use ($tier) { + return ! collect($tiers)->contains('hash', $tier->hash); + }); expect(TierModel::count())->toBe(0); + $this->assertSoftDeleted('tiers', ['id' => $tier->id]); +}); + +it('cascades delete to memberships when deleting tier', function () { + $tier = TierModel::factory()->create(); + $user = User::factory()->create(); + $customer = Customer::factory()->create(['user_id' => $user->id]); + $membership = Membership::create([ + 'user_id' => $user->id, + 'tier_id' => $tier->id, + 'tier_points' => 100, + ]); + + mountTierComponent($this->user) + ->call('delete', $tier) + ->assertHasNoErrors(); + + expect(Membership::count())->toBe(0); + $this->assertDatabaseMissing('memberships', ['id' => $membership->id]); +}); + +/* +|-------------------------------------------------------------------------- +| Load Tiers +|-------------------------------------------------------------------------- +*/ + +it('loads tiers with membership count', function () { + $tier = TierModel::factory()->create(); + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + $customer1 = Customer::factory()->create(['user_id' => $user1->id]); + $customer2 = Customer::factory()->create(['user_id' => $user2->id]); + + Membership::create(['user_id' => $user1->id, 'tier_id' => $tier->id, 'tier_points' => 100]); + Membership::create(['user_id' => $user2->id, 'tier_id' => $tier->id, 'tier_points' => 200]); + + $component = mountTierComponent($this->user); + + $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id); + expect($loadedTier['total_members'])->toBe('2'); +}); + +it('loads tiers with rewards count', function () { + $tier = TierModel::factory()->create(); + TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 1', 'value' => 100]); + TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 2', 'value' => 200]); + TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 3', 'value' => 300]); + + $component = mountTierComponent($this->user); + + $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id); + + expect((int) $loadedTier['total_rewards'])->toBe(3); +}); + +it('loads top member stats when tier has memberships', function () { + $outlet = Outlet::factory()->operational()->create(); + $tier = TierModel::factory()->create(['name' => 'Gold']); + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + $customer1 = Customer::factory()->create(['user_id' => $user1->id, 'name' => 'John Doe']); + $customer2 = Customer::factory()->create(['user_id' => $user2->id, 'name' => 'Jane Doe']); + + // Create memberships with different tier_points + $membership1 = Membership::create([ + 'user_id' => $user1->id, + 'tier_id' => $tier->id, + 'tier_points' => 500, // Higher points + ]); + + $membership2 = Membership::create([ + 'user_id' => $user2->id, + 'tier_id' => $tier->id, + 'tier_points' => 200, // Lower points + ]); + + // Create orders for user1 (top member) + $order1 = Order::create([ + 'outlet_id' => $outlet->id, + 'customer_id' => $customer1->id, + 'user_id' => $user1->id, + 'invoice_number' => 'INV-001', + 'cogs' => 50000, + 'subtotal' => 100000, + 'discount' => 10000, + 'total' => 90000, + 'ordered_at' => now(), + ]); + + $order2 = Order::create([ + 'outlet_id' => $outlet->id, + 'customer_id' => $customer1->id, + 'user_id' => $user1->id, + 'invoice_number' => 'INV-002', + 'cogs' => 25000, + 'subtotal' => 50000, + 'discount' => 5000, + 'total' => 45000, + 'ordered_at' => now(), + ]); + + // Create order items for order1 + $brand = Brand::factory()->create(); + $perfume1 = Perfume::create([ + 'brand_id' => $brand->id, + 'name' => 'Test Perfume 1', + 'slug' => 'test-perfume-1', + 'sku' => 'TP1-0001', + 'cost_price' => 10000, + 'sale_price' => 50000, + 'point_per_ml' => 500, + ]); + $product1 = Product::create([ + 'name' => 'Test Product 1', + 'slug' => 'test-product-1', + 'sku' => 'PR1-0001', + 'cost_price' => 5000, + 'sale_price' => 25000, + 'point_per_pcs' => 250, + ]); + $bottle1 = Bottle::factory()->create(); + + OrderItem::create([ + 'order_id' => $order1->id, + 'user_id' => $user1->id, + 'orderable_type' => Perfume::class, + 'orderable_id' => $perfume1->id, + 'cogs' => 10000, + 'unit_price' => 50000, + 'quantity' => 2, + ]); + + OrderItem::create([ + 'order_id' => $order1->id, + 'user_id' => $user1->id, + 'orderable_type' => Product::class, + 'orderable_id' => $product1->id, + 'cogs' => 5000, + 'unit_price' => 25000, + 'quantity' => 3, + ]); + + OrderItem::create([ + 'order_id' => $order1->id, + 'user_id' => $user1->id, + 'orderable_type' => Bottle::class, + 'orderable_id' => $bottle1->id, + 'cogs' => 3000, + 'unit_price' => 15000, + 'quantity' => 1, + ]); + + // Create order items for order2 + $perfume2 = Perfume::create([ + 'brand_id' => $brand->id, + 'name' => 'Test Perfume 2', + 'slug' => 'test-perfume-2', + 'sku' => 'TP2-0001', + 'cost_price' => 10000, + 'sale_price' => 50000, + 'point_per_ml' => 500, + ]); + OrderItem::create([ + 'order_id' => $order2->id, + 'user_id' => $user1->id, + 'orderable_type' => Perfume::class, + 'orderable_id' => $perfume2->id, + 'cogs' => 10000, + 'unit_price' => 50000, + 'quantity' => 1, + ]); + + $component = mountTierComponent($this->user); + + $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id); + expect($loadedTier['top_member'])->not->toBeNull(); + expect($loadedTier['top_member']['name'])->toBe('John Doe'); + expect($loadedTier['top_member']['points'])->toContain('500'); + expect($loadedTier['top_member']['total_orders'])->toContain('2'); + expect($loadedTier['top_member']['items']['total_perfume'])->toContain('3'); + expect($loadedTier['top_member']['items']['total_product'])->toContain('3'); + expect($loadedTier['top_member']['items']['total_bottle'])->toContain('1'); +}); + +it('shows null top member when tier has no memberships', function () { + $tier = TierModel::factory()->create(); + + $component = mountTierComponent($this->user); + + $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id); + expect($loadedTier['top_member'])->toBeNull(); +}); + +it('reloads tiers when data:tiersUpdated event is dispatched', function () { + $tier1 = TierModel::factory()->create(['name' => 'Bronze']); + $component = mountTierComponent($this->user); + + expect($component->tiers)->toHaveCount(1); + + $tier2 = TierModel::factory()->create(['name' => 'Silver']); + + $component->dispatch('data:tiersUpdated') + ->assertSet('tiers', function ($tiers) { + return count($tiers) === 2; + }); +}); + +/* +|-------------------------------------------------------------------------- +| Modal Management +|-------------------------------------------------------------------------- +*/ + +it('opens modal with correct method and title for create', function () { + $component = mountTierComponent($this->user) + ->call('openModal', 'create', 'Tambah Tier'); + + expect($component->method)->toBe('create'); + expect($component->modalTitle)->toBe('Tambah Tier'); +}); + +it('opens modal with correct method and title for update', function () { + $tier = TierModel::factory()->create(); + + $component = mountTierComponent($this->user) + ->call('openModal', 'update', 'Ubah Tier', $tier->hash); + + expect($component->method)->toBe('update'); + expect($component->modalTitle)->toBe('Ubah Tier'); + expect($component->form->name)->toBe($tier->name); +}); + +it('sets form data when opening update modal', function () { + $tier = TierModel::factory()->create([ + 'name' => 'Gold', + 'min_points' => 1000, + 'max_points' => 5000, + ]); + + $component = mountTierComponent($this->user) + ->call('openModal', 'update', 'Ubah Tier', $tier->hash); + + expect($component->form->name)->toBe('Gold'); + expect($component->form->min_points)->toBe('1000'); + expect($component->form->max_points)->toBe('5000'); +}); + +/* +|-------------------------------------------------------------------------- +| Form Data Formatting +|-------------------------------------------------------------------------- +*/ + +it('formats points with currency when displaying', function () { + $tier = TierModel::factory()->create([ + 'min_points' => 1000, + 'max_points' => 5000, + ]); + + $component = mountTierComponent($this->user); + + $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id); + expect($loadedTier['min_points'])->toContain('1.000'); + expect($loadedTier['max_points'])->toContain('5.000'); +}); + +it('handles currency formatting in form input', function () { + mountTierComponent($this->user) + ->call('openModal', 'create', 'Tambah Tier') + ->set('form.name', 'Gold') + ->set('form.min_points', '1.000') + ->set('form.max_points', '5.000') + ->call('create') + ->assertHasNoErrors(); + + $tier = TierModel::first(); + expect($tier->min_points)->toBe(1000); + expect($tier->max_points)->toBe(5000); +}); + +/* +|-------------------------------------------------------------------------- +| Edge Cases +|-------------------------------------------------------------------------- +*/ + +it('handles tier with null max_points correctly', function () { + $tier = TierModel::factory()->create([ + 'name' => 'Platinum', + 'min_points' => 10000, + 'max_points' => null, + ]); + + $component = mountTierComponent($this->user); + + $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id); + expect($loadedTier['max_points'])->not->toBeNull(); +}); + +it('handles tier with zero min_points', function () { + $tier = TierModel::factory()->create([ + 'name' => 'Starter', + 'min_points' => 0, + 'max_points' => 100, + ]); + + $component = mountTierComponent($this->user); + + $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id); + expect($loadedTier['min_points'])->toContain('0'); +}); + +it('handles empty tiers list', function () { + $component = mountTierComponent($this->user); + + expect($component->tiers)->toBe([]); +}); + +it('updates tier array correctly after update', function () { + $tier = TierModel::factory()->create([ + 'name' => 'Bronze', + 'min_points' => 0, + 'max_points' => 100, + ]); + + $component = mountTierComponent($this->user) + ->call('openModal', 'update', 'Ubah Tier', $tier->hash) + ->set('form.name', 'Gold') + ->set('form.min_points', '1.000') + ->set('form.max_points', '5.000') + ->call('update'); + + $updatedTier = collect($component->tiers)->firstWhere('hash', $tier->hash); + expect($updatedTier['name'])->toBe('Gold'); + expect($updatedTier['min_points'])->toContain('1.000'); + expect($updatedTier['max_points'])->toContain('5.000'); });