parfum/tests/Feature/Livewire/Studio/Loyalty/TierTest.php

650 lines
22 KiB
PHP

<?php
use App\Livewire\Studio\Loyalty\Tier;
use App\Models\Bottle;
use App\Models\Brand;
use App\Models\Customer;
use App\Models\Employee;
use App\Models\Membership;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Outlet;
use App\Models\Perfume;
use App\Models\Product;
use App\Models\Tier as TierModel;
use App\Models\TierReward;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->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
});