diff --git a/app/Livewire/Forms/Studio/Loyalty/TierForm.php b/app/Livewire/Forms/Studio/Loyalty/TierForm.php index 8271a82..c449dff 100644 --- a/app/Livewire/Forms/Studio/Loyalty/TierForm.php +++ b/app/Livewire/Forms/Studio/Loyalty/TierForm.php @@ -59,23 +59,23 @@ public function setTier(Tier $tier): void $this->max_spending = $tier->max_spending ? formatCurrencyNumber($tier->max_spending) : null; } - public function store(): Tier + public function store(): void { $this->validate(); - return DB::transaction(function () { - return Tier::create($this->prepareSavedData()); + DB::transaction(function () { + Tier::create($this->prepareSavedData()); }); } - public function update(): Tier + public function update(): void { $this->validate(); - return DB::transaction(function () { + DB::transaction(function () { $this->tier->update($this->prepareSavedData()); - return $this->tier; + $this->tier; }); } diff --git a/app/Livewire/Studio/Loyalty/Tier.php b/app/Livewire/Studio/Loyalty/Tier.php index 0244400..e806328 100644 --- a/app/Livewire/Studio/Loyalty/Tier.php +++ b/app/Livewire/Studio/Loyalty/Tier.php @@ -25,37 +25,12 @@ class Tier extends Component { use WithAuthorization, WithCloseModal, WithConfirmation, WithSubscribeNotification, WithToast, WithUpdatedData; - public array $tiers = []; - public TierForm $form; public string $method = 'create'; public string $modalTitle = ''; - public function mount(): void - { - $this->loadTiers(); - } - - #[On('data:tiersUpdated')] - public function loadTiers(): void - { - $this->tiers = TierModel::withCount('memberships') - ->orderBy('min_spending', 'asc') - ->get() - ->map(fn (TierModel $tier) => [ - 'id' => $tier->id, - 'hash' => $tier->hash, - 'name' => $tier->name, - 'min_spending' => formatCurrencyNumber($tier->min_spending, 'Rp'), - 'max_spending' => $tier->max_spending ? formatCurrencyNumber($tier->max_spending, 'Rp') : 'Tak terbatas', - 'total_members' => formatCurrencyNumber($tier->memberships_count), - 'top_member' => $this->getTopMemberStats($tier->id), - ]) - ->toArray(); - } - public function getTopMemberStats($tierId): ?array { $topMembership = Membership::where('tier_id', $tierId) @@ -107,9 +82,6 @@ public function create(): void // Reorder the entire tier chain to ensure proper max_spending TierModel::reorderTierChain(); - // Reload tiers to reflect the changes - $this->loadTiers(); - $this->toast('Tier berhasil ditambahkan.'); Flux::modals()->close(); @@ -129,9 +101,6 @@ public function update(): void TierModel::reorderTierChain(); } - // Reload tiers to reflect any changes from reordering - $this->loadTiers(); - $this->toast('Tier berhasil diperbarui.'); Flux::modals()->close(); @@ -154,9 +123,6 @@ public function delete(TierModel $tier): void // Reorder tier chain after deletion to ensure proper max_spending TierModel::reorderTierChain(); - // Reload tiers to reflect the changes - $this->loadTiers(); - $this->toast('Tier berhasil dihapus.'); Flux::modals()->close(); @@ -176,8 +142,23 @@ public function openModal(string $method, string $modalTitle, ?string $id = null public function render(): View { + $tiers = TierModel::withCount('memberships') + ->orderBy('min_spending', 'asc') + ->get() + ->map(fn (TierModel $tier) => [ + 'id' => $tier->id, + 'hash' => $tier->hash, + 'name' => $tier->name, + 'min_spending' => formatCurrencyNumber($tier->min_spending, 'Rp'), + 'max_spending' => $tier->max_spending ? formatCurrencyNumber($tier->max_spending, 'Rp') : 'Tak terbatas', + 'total_members' => formatCurrencyNumber($tier->memberships_count), + 'top_member' => $this->getTopMemberStats($tier->id), + ]) + ->toArray(); + return view('livewire.studio.loyalty.tiers', [ 'pageTitle' => 'Tier', + 'tiers' => $tiers, ]); } } diff --git a/tests/Feature/Studio/Loyalty/TierTest.php b/tests/Feature/Studio/Loyalty/TierTest.php new file mode 100644 index 0000000..555df88 --- /dev/null +++ b/tests/Feature/Studio/Loyalty/TierTest.php @@ -0,0 +1,158 @@ +setupUser(); + + $role = Role::firstOrCreate(['name' => 'Owner']); + Permission::firstOrCreate(['name' => 'view tier']); + Permission::firstOrCreate(['name' => 'create tier']); + Permission::firstOrCreate(['name' => 'update tier']); + Permission::firstOrCreate(['name' => 'delete tier']); + $role->givePermissionTo(['view tier', 'create tier', 'update tier', 'delete tier']); + $this->user->assignRole($role); +}); + +it('renders the tier index page correctly', function () { + $this->actingAs($this->user) + ->get(route('studio.loyalty.tier.index')) + ->assertOk() + ->assertSeeLivewire(Tier::class); +}); + +it('can open create modal', function () { + Livewire::actingAs($this->user) + ->test(Tier::class) + ->dispatch('modal:open', method: 'create', modalTitle: 'Tambah Tier') + ->assertSet('method', 'create') + ->assertSet('modalTitle', 'Tambah Tier'); +}); + +it('validates required fields on create', function () { + Livewire::actingAs($this->user) + ->test(Tier::class) + ->call('create') + ->assertHasErrors(['form.name' => 'required', 'form.min_spending' => 'required']); +}); + +it('can store a new tier', function () { + // Ensure no tiers exist first + TierModel::query()->delete(); + + Livewire::actingAs($this->user) + ->test(Tier::class) + ->set('form.name', 'Bronze') + ->set('form.min_spending', '0') + ->call('create') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('tiers', [ + 'name' => 'Bronze', + 'min_spending' => 0, + ]); +}); + +it('automatically reorders tier chain on create', function () { + TierModel::query()->delete(); + + // Create first tier manually + TierModel::create(['name' => 'Bronze', 'min_spending' => 0]); + + // Create second tier via Livewire + Livewire::actingAs($this->user) + ->test(Tier::class) + ->set('form.name', 'Silver') + ->set('form.min_spending', '10.000') // 10,000 + ->call('create') + ->assertHasNoErrors(); + + // First tier should now have max_spending = 9999 + $bronze = TierModel::where('name', 'Bronze')->first(); + expect($bronze->max_spending)->toBe(9999); + + // Silver should have null max_spending + $silver = TierModel::where('name', 'Silver')->first(); + expect($silver->max_spending)->toBeNull(); +}); + +it('can open edit modal and load data', function () { + $tier = TierModel::factory()->create([ + 'name' => 'Gold', + 'min_spending' => 50000, + 'max_spending' => null, + ]); + + Livewire::actingAs($this->user) + ->test(Tier::class) + ->call('openModal', 'update', 'Ubah Tier', $tier->hash_id) + ->set('form.tier', $tier) + ->set('form.name', 'Gold') + ->set('form.min_spending', formatCurrencyNumber(50000)) + ->assertSet('method', 'update') + ->assertSet('modalTitle', 'Ubah Tier') + ->assertSet('form.name', 'Gold') + ->assertSet('form.min_spending', formatCurrencyNumber(50000)); +}); + +it('can update a tier', function () { + $tier = TierModel::factory()->create(['name' => 'Old Name', 'min_spending' => 0]); + + Livewire::actingAs($this->user) + ->test(Tier::class) + ->call('openModal', 'update', 'Ubah Tier', $tier->hash_id) + ->set('form.tier', $tier) + ->set('form.name', 'New Name') + ->set('form.min_spending', '0') + ->call('update') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('tiers', [ + 'id' => $tier->id, + 'name' => 'New Name', + ]); +}); + +it('can delete the last tier', function () { + TierModel::query()->delete(); + $tier1 = TierModel::create(['name' => 'Bronze', 'min_spending' => 0]); + $tier2 = TierModel::create(['name' => 'Silver', 'min_spending' => 10000]); + TierModel::reorderTierChain(); + + Livewire::actingAs($this->user) + ->test(Tier::class) + ->call('delete', $tier2->id); // Deleting highest tier (Silver) + + $this->assertSoftDeleted('tiers', ['id' => $tier2->id]); + + // Bronze should now be the last tier with null max_spending + $tier1->refresh(); + expect($tier1->max_spending)->toBeNull(); +}); + +it('cannot delete a middle tier', function () { + TierModel::query()->delete(); + $tier1 = TierModel::create(['name' => 'Bronze', 'min_spending' => 0]); + $tier2 = TierModel::create(['name' => 'Silver', 'min_spending' => 10000]); + $tier3 = TierModel::create(['name' => 'Gold', 'min_spending' => 20000]); + TierModel::reorderTierChain(); + + Livewire::actingAs($this->user) + ->test(Tier::class) + ->call('delete', $tier2->id); + + $this->assertDatabaseHas('tiers', ['id' => $tier2->id]); +}); + +it('cannot access tier page without permission', function () { + $this->user->roles()->detach(); + $this->user->permissions()->detach(); + + $this->actingAs($this->user) + ->get(route('studio.loyalty.tier.index')) + ->assertForbidden(); +});