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('loads tiers on mount', function () { $tier1 = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]); $tier2 = TierModel::factory()->create(['name' => 'Silver', 'min_spending' => 100001, 'max_spending' => 500000]); $component = mountTierComponent($this->user); expect($component->tiers)->toBeInstanceOf(\Illuminate\Support\Collection::class); expect($component->tiers)->toHaveCount(2); expect($component->tiers[0]->name)->toBe('Bronze'); expect($component->tiers[1]->name)->toBe('Silver'); }); it('displays all tiers in the view', function () { $tier = TierModel::factory()->create(['name' => 'Gold', 'min_spending' => 1000000, 'max_spending' => 5000000]); mountTierComponent($this->user) ->assertSee('Gold') ->assertSee('1.000.000'); }); it('sorts tiers by min_spending ascending', function () { $tier3 = TierModel::factory()->create(['name' => 'Platinum', 'min_spending' => 10000000, 'max_spending' => null]); $tier1 = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 1000000]); $tier2 = TierModel::factory()->create(['name' => 'Silver', 'min_spending' => 1000001, 'max_spending' => 5000000]); $component = mountTierComponent($this->user); expect($component->tiers)->toBeInstanceOf(\Illuminate\Support\Collection::class); 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_spending', '1000000') ->set('form.max_spending', '5000000') ->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_spending', '1.000.000') ->set('form.max_spending', '5.000.000') ->call('create') ->assertHasNoErrors() ->assertSet('tiers', function ($tiers) { return $tiers instanceof \Illuminate\Support\Collection && $tiers->count() === 1 && $tiers[0]->name === 'Gold'; }); $this->assertDatabaseHas('tiers', [ 'name' => 'Gold', 'min_spending' => 1000000, 'max_spending' => null, // reorderTierChain makes last tier null ]); }); it('can create a tier without max_spending', function () { mountTierComponent($this->user) ->call('openModal', 'create', 'Tambah Tier') ->set('form.name', 'Platinum') ->set('form.min_spending', '5.000.001') ->set('form.max_spending', null) ->call('create') ->assertHasNoErrors(); $this->assertDatabaseHas('tiers', [ 'name' => 'Platinum', 'min_spending' => 5000001, 'max_spending' => null, ]); }); it('sorts tiers after creating new tier', function () { TierModel::factory()->create(['name' => 'Silver', 'min_spending' => 500001, 'max_spending' => 1000000]); TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 500000]); $component = mountTierComponent($this->user) ->call('openModal', 'create', 'Tambah Tier') ->set('form.name', 'Gold') ->set('form.min_spending', '1.000.001') ->set('form.max_spending', '2.000.000') ->call('create'); expect($component->tiers)->toBeInstanceOf(\Illuminate\Support\Collection::class); 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_spending', ]); }); 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_spending', '100') ->call('create') ->assertHasErrors(['form.name']); }); it('validates min_spending is unsigned integer when creating tier', function () { mountTierComponent($this->user) ->call('openModal', 'create', 'Tambah Tier') ->set('form.name', 'Gold') ->set('form.min_spending', '-100') ->call('create') ->assertHasErrors(['form.min_spending']); }); it('validates max_spending is unsigned integer when creating tier', function () { mountTierComponent($this->user) ->call('openModal', 'create', 'Tambah Tier') ->set('form.name', 'Gold') ->set('form.min_spending', '100') ->set('form.max_spending', '-50') ->call('create') ->assertHasErrors(['form.max_spending']); }); /* |-------------------------------------------------------------------------- | 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_spending' => 0, 'max_spending' => 100000]); 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_spending' => 0, 'max_spending' => 100000]); mountTierComponent($this->user) ->call('openModal', 'update', 'Ubah Tier', $tier->hash) ->assertSet('form.name', 'Bronze') ->assertSet('form.min_spending', formatCurrencyNumber($tier->min_spending)) ->assertSet('form.max_spending', formatCurrencyNumber($tier->max_spending)) ->set('form.name', 'Gold') ->set('form.min_spending', '1.000.000') ->set('form.max_spending', '5.000.000') ->call('update') ->assertHasNoErrors() ->assertSet('tiers', function ($tiers) use ($tier) { $updatedTier = $tiers->firstWhere('hash', $tier->hash); return $updatedTier->name === 'Gold'; }); $tier->refresh(); expect($tier->name)->toBe('Gold'); expect($tier->min_spending)->toBe(1000000); expect($tier->max_spending)->toBeNull(); // reorderTierChain makes last tier null }); it('can update tier without max_spending', function () { $tier = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]); mountTierComponent($this->user) ->call('openModal', 'update', 'Ubah Tier', $tier->hash) ->set('form.max_spending', null) ->call('update') ->assertHasNoErrors(); $tier->refresh(); expect($tier->max_spending)->toBeNull(); }); it('validates required fields when updating tier', function () { $tier = TierModel::factory()->create(); mountTierComponent($this->user) ->call('openModal', 'update', 'Ubah Tier', $tier->hash) ->set('form.name', '') ->set('form.min_spending', '') ->call('update') ->assertHasErrors([ 'form.name', 'form.min_spending', ]); }); 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() ->assertSet('tiers', function ($tiers) use ($tier) { return ! $tiers->contains(function ($t) use ($tier) { return $t->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, 'total_spending' => 100000, ]); mountTierComponent($this->user) ->call('delete', $tier) ->assertHasNoErrors(); expect(Membership::count())->toBe(0); $this->assertSoftDeleted('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, 'total_spending' => 100000]); Membership::create(['user_id' => $user2->id, 'tier_id' => $tier->id, 'total_spending' => 200000]); $component = mountTierComponent($this->user); $loadedTier = $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', 'points' => 100]); TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 2', 'points' => 200]); TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 3', 'points' => 300]); $component = mountTierComponent($this->user); $loadedTier = $component->tiers->firstWhere('id', $tier->id); expect($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 total_spending $membership1 = Membership::create([ 'user_id' => $user1->id, 'tier_id' => $tier->id, 'total_spending' => 5000000, // Higher spending ]); $membership2 = Membership::create([ 'user_id' => $user2->id, 'tier_id' => $tier->id, 'total_spending' => 2000000, // Lower spending ]); // 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, ]); $product1 = Product::create([ 'name' => 'Test Product 1', 'slug' => 'test-product-1', 'sku' => 'PR1-0001', 'cost_price' => 5000, 'sale_price' => 25000, ]); $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, ]); 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 = $component->tiers->firstWhere('id', $tier->id); expect($loadedTier->top_member)->not->toBeNull(); expect($loadedTier->top_member['name'])->toBe('John Doe'); expect($loadedTier->top_member['spending'])->toContain('5.000.000'); expect($loadedTier->top_member['total_orders'])->toBe('2'); expect($loadedTier->top_member['items']['total_perfume'])->toBe('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 = $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 $tiers->count() === 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); expect($component->form->min_spending)->toBe(formatCurrencyNumber($tier->min_spending)); expect($component->form->max_spending)->toBe($tier->max_spending ? formatCurrencyNumber($tier->max_spending) : null); }); it('sets form data when opening update modal', function () { $tier = TierModel::factory()->create([ 'name' => 'Gold', 'min_spending' => 1000000, 'max_spending' => 5000000, ]); $component = mountTierComponent($this->user) ->call('openModal', 'update', 'Ubah Tier', $tier->hash); expect($component->form->name)->toBe('Gold'); expect($component->form->min_spending)->toBe('1.000.000'); expect($component->form->max_spending)->toBe('5.000.000'); }); /* |-------------------------------------------------------------------------- | Form Data Formatting |-------------------------------------------------------------------------- */ it('formats spending with currency when displaying', function () { $tier = TierModel::factory()->create([ 'min_spending' => 1000000, 'max_spending' => 5000000, ]); $component = mountTierComponent($this->user); $loadedTier = $component->tiers->firstWhere('id', $tier->id); expect(formatCurrencyNumber($loadedTier->min_spending))->toContain('1.000.000'); expect(formatCurrencyNumber($loadedTier->max_spending))->toContain('5.000.000'); }); it('handles currency formatting in form input', function () { mountTierComponent($this->user) ->call('openModal', 'create', 'Tambah Tier') ->set('form.name', 'Gold') ->set('form.min_spending', '1.000.000') ->set('form.max_spending', '5.000.000') ->call('create') ->assertHasNoErrors(); $tier = TierModel::first(); expect($tier->min_spending)->toBe(1000000); expect($tier->max_spending)->toBeNull(); // ReorderTierChain makes the last tier null }); /* |-------------------------------------------------------------------------- | Edge Cases |-------------------------------------------------------------------------- */ it('handles tier with null max_spending correctly', function () { $tier = TierModel::factory()->create([ 'name' => 'Platinum', 'min_spending' => 10000000, 'max_spending' => null, ]); $component = mountTierComponent($this->user); $loadedTier = $component->tiers->firstWhere('id', $tier->id); expect(formatCurrencyNumber($loadedTier->max_spending))->not->toBeNull(); }); it('handles tier with zero min_spending', function () { $tier = TierModel::factory()->create([ 'name' => 'Starter', 'min_spending' => 0, 'max_spending' => 100000, ]); $component = mountTierComponent($this->user); $loadedTier = $component->tiers->firstWhere('id', $tier->id); expect(formatCurrencyNumber($loadedTier->min_spending))->toContain('0'); }); it('handles empty tiers list', function () { $component = mountTierComponent($this->user); expect($component->tiers)->toBeInstanceOf(Collection::class); }); it('updates tier array correctly after update', function () { $tier = TierModel::factory()->create([ 'name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000, ]); $component = mountTierComponent($this->user) ->call('openModal', 'update', 'Ubah Tier', $tier->hash) ->set('form.name', 'Gold') ->set('form.min_spending', '1.000.000') ->set('form.max_spending', '5.000.000') ->call('update'); $updatedTier = $component->tiers->firstWhere('hash', $tier->hash); expect($updatedTier->name)->toBe('Gold'); expect($updatedTier->formatted_min_spending)->toContain('1.000.000'); expect($updatedTier->max_spending)->toBeNull(); // ReorderTierChain makes the last tier null });