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.tier.index') ->assertViewHas('pageTitle', 'Tier'); }); 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]); $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_points' => 100, 'max_points' => 500]); 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)->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_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() ->assertSet('tiers', function ($tiers) { return $tiers instanceof \Illuminate\Support\Collection && $tiers->count() === 1 && $tiers[0]->name === 'Gold'; }); $this->assertDatabaseHas('tiers', [ 'name' => 'Gold', 'min_points' => 1000, 'max_points' => 5000, ]); }); 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(); $this->assertDatabaseHas('tiers', [ 'name' => 'Platinum', 'min_points' => 5001, 'max_points' => null, ]); }); 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)->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_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() ->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_points)->toBe(1000); expect($tier->max_points)->toBe(5000); }); 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(); 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() ->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, 'tier_points' => 100, ]); 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, 'tier_points' => 100]); Membership::create(['user_id' => $user2->id, 'tier_id' => $tier->id, 'tier_points' => 200]); $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', '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 = $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 = $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 = $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_points)->toBe((string) $tier->min_points); expect($component->form->max_points)->toBe($tier->max_points ? (string) $tier->max_points : null); }); 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 = $component->tiers->firstWhere('id', $tier->id); expect(currency($loadedTier->min_points))->toContain('1.000'); expect(currency($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 = $component->tiers->firstWhere('id', $tier->id); expect(currency($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 = $component->tiers->firstWhere('id', $tier->id); expect(currency($loadedTier->min_points))->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_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 = $component->tiers->firstWhere('hash', $tier->hash); expect($updatedTier->name)->toBe('Gold'); expect(currency($updatedTier->min_points))->toContain('1.000'); expect(currency($updatedTier->max_points))->toContain('5.000'); });