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

644 lines
21 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\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.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)->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[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 count($tiers) === 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[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 = collect($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 ! collect($tiers)->contains('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 = collect($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 = collect($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 = collect($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 = collect($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 count($tiers) === 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);
});
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 = collect($component->tiers)->firstWhere('id', $tier->id);
expect($loadedTier['min_points'])->toContain('1.000');
expect($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 = collect($component->tiers)->firstWhere('id', $tier->id);
expect($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 = collect($component->tiers)->firstWhere('id', $tier->id);
expect($loadedTier['min_points'])->toContain('0');
});
it('handles empty tiers list', function () {
$component = mountTierComponent($this->user);
expect($component->tiers)->toBe([]);
});
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 = collect($component->tiers)->firstWhere('hash', $tier->hash);
expect($updatedTier['name'])->toBe('Gold');
expect($updatedTier['min_points'])->toContain('1.000');
expect($updatedTier['max_points'])->toContain('5.000');
});