refactor: move tier data loading to render method, adjust form return types, and add feature tests for tier management.

This commit is contained in:
Yoga Pangestu 2026-01-02 18:35:29 +07:00
parent 53554a0260
commit c8fde13435
3 changed files with 179 additions and 40 deletions

View File

@ -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;
});
}

View File

@ -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,
]);
}
}

View File

@ -0,0 +1,158 @@
<?php
use App\Livewire\Studio\Loyalty\Tier;
use App\Models\Tier as TierModel;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
beforeEach(function () {
$this->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();
});