diff --git a/tests/Feature/Livewire/Studio/Loyalty/CustomerTest.php b/tests/Feature/Livewire/Studio/Loyalty/CustomerTest.php index eb79ef5..313d2b1 100644 --- a/tests/Feature/Livewire/Studio/Loyalty/CustomerTest.php +++ b/tests/Feature/Livewire/Studio/Loyalty/CustomerTest.php @@ -1,30 +1,211 @@ 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 mountCustomerComponent(User $user) +{ + return Livewire::actingAs($user)->test(CustomerComponent::class); +} + +/* +|-------------------------------------------------------------------------- +| Component Rendering & Mount +|-------------------------------------------------------------------------- +*/ + +it('renders page successfully', function () { + mountCustomerComponent($this->user) ->assertViewIs('livewire.studio.loyalty.customers') ->assertViewHas('pageTitle', 'Customer'); }); -it('displays all customers', function () { - CustomerModel::factory()->count(10)->create(); +it('initializes form properties on mount', function () { + $component = mountCustomerComponent($this->user); - $customer = CustomerModel::first(); - - Livewire::test(Customer::class)->assertSee($customer->name); + expect($component->method)->toBe('create'); + expect($component->modalTitle)->toBe(''); + expect($component->form->name)->toBe(''); + expect($component->form->phone_number)->toBeNull(); + expect($component->form->gender)->toBe(''); }); -it('can create a customer', function () { - $data = CustomerModel::factory()->raw(); +it('loads stats on mount', function () { + // Create customers without users + Customer::factory()->count(3)->create(['user_id' => null]); - Livewire::test(Customer::class) + // Create customers with users + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + Customer::factory()->create(['user_id' => $user1->id]); + Customer::factory()->create(['user_id' => $user2->id]); + + $component = mountCustomerComponent($this->user); + + expect($component->stats)->toBeArray(); + expect($component->stats)->toHaveCount(3); + expect($component->stats[0]['title'])->toBe('Total Customer'); + expect($component->stats[1]['title'])->toBe('Total Member'); + expect($component->stats[2]['title'])->toBe('Top Member'); +}); + +it('calculates total customer count correctly', function () { + Customer::factory()->count(5)->create(['user_id' => null]); + Customer::factory()->count(3)->create(['user_id' => User::factory()->create()->id]); + + $component = mountCustomerComponent($this->user); + + expect($component->stats[0]['value'])->toBe(5); +}); + +it('calculates total member count correctly', function () { + Customer::factory()->count(2)->create(['user_id' => null]); + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + Customer::factory()->create(['user_id' => $user1->id]); + Customer::factory()->create(['user_id' => $user2->id]); + + $component = mountCustomerComponent($this->user); + + expect($component->stats[1]['value'])->toBe(2); +}); + +it('displays top member name correctly', function () { + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + $customer1 = Customer::factory()->create(['user_id' => $user1->id, 'name' => 'Top Customer']); + $customer2 = Customer::factory()->create(['user_id' => $user2->id, 'name' => 'Regular Customer']); + + // Create more orders for customer1 + $outlet = Outlet::factory()->create(); + Order::factory()->count(5)->create(['customer_id' => $customer1->id, 'user_id' => $user1->id, 'outlet_id' => $outlet->id]); + Order::factory()->count(2)->create(['customer_id' => $customer2->id, 'user_id' => $user2->id, 'outlet_id' => $outlet->id]); + + $component = mountCustomerComponent($this->user); + + expect($component->stats[2]['value'])->toBe('Top Customer'); +}); + +it('displays null when no members exist', function () { + Customer::factory()->count(3)->create(['user_id' => null]); + + $component = mountCustomerComponent($this->user); + + expect($component->stats[2]['value'])->toBeNull(); +}); + +it('displays null when members have no orders', function () { + $user = User::factory()->create(); + Customer::factory()->create(['user_id' => $user->id]); + + $component = mountCustomerComponent($this->user); + + expect($component->stats[2]['value'])->toBeNull(); +}); + +/* +|-------------------------------------------------------------------------- +| Modal Management +|-------------------------------------------------------------------------- +*/ + +it('opens modal with create method', function () { + $component = mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer'); + + expect($component->method)->toBe('create'); + expect($component->modalTitle)->toBe('Tambah Customer'); +}); + +it('opens modal with update method', function () { + $customer = Customer::factory()->create(); + + $component = mountCustomerComponent($this->user) + ->call('openModal', 'update', 'Edit Customer', $customer->hash); + + expect($component->method)->toBe('update'); + expect($component->modalTitle)->toBe('Edit Customer'); +}); + +it('sets form data when opening update modal', function () { + $customer = Customer::factory()->create([ + 'name' => 'John Doe', + 'phone_number' => '0812 3456 7890', + 'gender' => Gender::MALE->value, + ]); + + $component = mountCustomerComponent($this->user) + ->call('openModal', 'update', 'Edit Customer', $customer->hash); + + expect($component->form->name)->toBe('John Doe'); + expect($component->form->phone_number)->toBe('0812 3456 7890'); + expect($component->form->gender)->toEqual(Gender::MALE->value); +}); + +it('resets validation and error bag when opening modal', function () { + $component = mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', '') + ->call('create') + ->assertHasErrors(['form.name']); + + $component->call('openModal', 'create', 'Tambah Customer') + ->assertHasNoErrors(); +}); + +/* +|-------------------------------------------------------------------------- +| Create Customer +|-------------------------------------------------------------------------- +*/ + +it('prevents create when user is unauthorized', function () { + $this->ownerRole->revokePermissionTo('create customer'); + + Gate::define('create customer', fn () => false); + + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', 'John Doe') + ->set('form.gender', Gender::MALE->value) + ->call('create') + ->assertForbidden(); +}); + +it('can create a customer when authorized', function () { + $data = [ + 'name' => 'John Doe', + 'phone_number' => '0812 3456 7890', + 'gender' => Gender::MALE->value, + ]; + + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') ->set('form.name', $data['name']) ->set('form.phone_number', $data['phone_number']) ->set('form.gender', $data['gender']) @@ -32,38 +213,431 @@ ->assertHasNoErrors() ->assertDispatched('refreshDatatable'); - expect(CustomerModel::count())->toBe(1); - expect(CustomerModel::first()->name)->toBe($data['name']); + $this->assertDatabaseHas('customers', [ + 'name' => $data['name'], + 'phone_number' => $data['phone_number'], + 'gender' => Gender::MALE->value, + ]); }); -it('can update a customer', function () { - $customer = CustomerModel::factory()->create(); +it('can create a customer without phone number', function () { + $data = [ + 'name' => 'Jane Doe', + 'phone_number' => null, + 'gender' => Gender::FEMALE->value, + ]; - $data = CustomerModel::factory()->raw(); - - Livewire::test(Customer::class) - ->call('openModal', 'update', 'Edit Customer', $customer->id) + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') ->set('form.name', $data['name']) ->set('form.phone_number', $data['phone_number']) ->set('form.gender', $data['gender']) + ->call('create') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => $data['name'], + 'phone_number' => null, + 'gender' => Gender::FEMALE->value, + ]); +}); + +it('can create a customer with user_id', function () { + $user = User::factory()->create(); + $data = [ + 'name' => 'Member Customer', + 'gender' => Gender::MALE->value, + ]; + + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', $data['name']) + ->set('form.gender', $data['gender']) + ->set('form.user_id', $user->id) + ->call('create') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => $data['name'], + 'user_id' => $user->id, + ]); +}); + +it('validates required fields when creating customer', function () { + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->call('create') + ->assertHasErrors([ + 'form.name', + 'form.gender', + ]); +}); + +it('validates name field max length when creating customer', function () { + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', str_repeat('a', 51)) + ->set('form.gender', Gender::MALE->value) + ->call('create') + ->assertHasErrors(['form.name']); +}); + +it('validates gender field is required when creating customer', function () { + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', 'John Doe') + ->set('form.gender', '') + ->call('create') + ->assertHasErrors(['form.gender']); +}); + +it('validates gender field is valid enum when creating customer', function () { + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', 'John Doe') + ->set('form.gender', 'invalid') + ->call('create') + ->assertHasErrors(['form.gender']); +}); + +it('validates phone number format when creating customer', function () { + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', 'John Doe') + ->set('form.phone_number', 'invalid-phone') + ->set('form.gender', Gender::MALE->value) + ->call('create') + ->assertHasErrors(['form.phone_number']); +}); + +it('accepts valid phone number format when creating customer', function () { + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', 'John Doe') + ->set('form.phone_number', '0812 3456 7890') + ->set('form.gender', Gender::MALE->value) + ->call('create') + ->assertHasNoErrors(); +}); + +it('dispatches refreshDatatable event after creating customer', function () { + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', 'John Doe') + ->set('form.gender', Gender::MALE->value) + ->call('create') + ->assertDispatched('refreshDatatable'); +}); + +/* +|-------------------------------------------------------------------------- +| Update Customer +|-------------------------------------------------------------------------- +*/ + +it('prevents update when user is unauthorized', function () { + $this->ownerRole->revokePermissionTo('update customer'); + Gate::define('update customer', fn () => false); + + $customer = Customer::factory()->create(); + + mountCustomerComponent($this->user) + ->call('openModal', 'update', 'Edit Customer', $customer->hash) + ->set('form.name', 'Updated Name') + ->call('update') + ->assertForbidden(); +}); + +it('can update a customer when authorized', function () { + $customer = Customer::factory()->create([ + 'name' => 'John Doe', + 'phone_number' => '0812 3456 7890', + 'gender' => Gender::MALE, + ]); + + mountCustomerComponent($this->user) + ->call('openModal', 'update', 'Edit Customer', $customer->hash) + ->assertSet('form.name', 'John Doe') + ->assertSet('form.phone_number', '0812 3456 7890') + ->assertSet('form.gender', Gender::MALE->value) + ->set('form.name', 'Jane Doe') + ->set('form.phone_number', '0812 9999 8888') + ->set('form.gender', Gender::FEMALE->value) ->call('update') ->assertHasNoErrors() ->assertDispatched('refreshDatatable'); $customer->refresh(); - - expect($customer->name)->toBe($data['name']); - expect($customer->phone_number)->toBe($data['phone_number']); - expect($customer->gender)->toBe($data['gender']); + expect($customer->name)->toBe('Jane Doe'); + expect($customer->phone_number)->toBe('0812 9999 8888'); + expect($customer->gender)->toBe(Gender::FEMALE); }); -it('can delete an customer', function () { - $customer = CustomerModel::factory()->create(); +it('can update customer phone number to null', function () { + $customer = Customer::factory()->create([ + 'phone_number' => '0812 3456 7890', + ]); - Livewire::test(Customer::class) + mountCustomerComponent($this->user) + ->call('openModal', 'update', 'Edit Customer', $customer->hash) + ->set('form.phone_number', null) + ->call('update') + ->assertHasNoErrors(); + + $customer->refresh(); + expect($customer->phone_number)->toBeNull(); +}); + +it('validates required fields when updating customer', function () { + $customer = Customer::factory()->create(); + + mountCustomerComponent($this->user) + ->call('openModal', 'update', 'Edit Customer', $customer->hash) + ->set('form.name', '') + ->set('form.gender', '') + ->call('update') + ->assertHasErrors([ + 'form.name', + 'form.gender', + ]); +}); + +it('validates name field max length when updating customer', function () { + $customer = Customer::factory()->create(); + + mountCustomerComponent($this->user) + ->call('openModal', 'update', 'Edit Customer', $customer->hash) + ->set('form.name', str_repeat('a', 51)) + ->call('update') + ->assertHasErrors(['form.name']); +}); + +it('validates phone number format when updating customer', function () { + $customer = Customer::factory()->create(); + + mountCustomerComponent($this->user) + ->call('openModal', 'update', 'Edit Customer', $customer->hash) + ->set('form.phone_number', 'invalid-phone') + ->call('update') + ->assertHasErrors(['form.phone_number']); +}); + +it('dispatches refreshDatatable event after updating customer', function () { + $customer = Customer::factory()->create(); + + mountCustomerComponent($this->user) + ->call('openModal', 'update', 'Edit Customer', $customer->hash) + ->set('form.name', 'Updated Name') + ->call('update') + ->assertDispatched('refreshDatatable'); +}); + +/* +|-------------------------------------------------------------------------- +| Delete Customer +|-------------------------------------------------------------------------- +*/ + +it('can delete a customer', function () { + $customer = Customer::factory()->create(); + + mountCustomerComponent($this->user) ->call('delete', $customer) ->assertHasNoErrors() ->assertDispatched('refreshDatatable'); - expect(CustomerModel::count())->toBe(0); + expect(Customer::count())->toBe(0); + $this->assertSoftDeleted('customers', ['id' => $customer->id]); +}); + +it('soft deletes customer when deleting', function () { + $customer = Customer::factory()->create(); + + mountCustomerComponent($this->user) + ->call('delete', $customer); + + $customer->refresh(); + expect($customer->trashed())->toBeTrue(); +}); + +it('dispatches refreshDatatable event after deleting customer', function () { + $customer = Customer::factory()->create(); + + mountCustomerComponent($this->user) + ->call('delete', $customer) + ->assertDispatched('refreshDatatable'); +}); + +it('can delete customer with orders', function () { + $outlet = Outlet::factory()->create(); + $user = User::factory()->create(); + $customer = Customer::factory()->create(['user_id' => $user->id]); + Order::factory()->count(3)->create([ + 'customer_id' => $customer->id, + 'user_id' => $user->id, + 'outlet_id' => $outlet->id, + ]); + + mountCustomerComponent($this->user) + ->call('delete', $customer); + + expect($customer->trashed())->toBeTrue(); + // Orders should still exist (cascade delete might be configured differently) + expect(Order::count())->toBe(3); +}); + +/* +|-------------------------------------------------------------------------- +| Stats Recalculation +|-------------------------------------------------------------------------- +*/ + +it('recalculates stats after creating customer', function () { + $initialStats = mountCustomerComponent($this->user)->stats; + $initialCustomerCount = $initialStats[0]['value']; + + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', 'New Customer') + ->set('form.gender', Gender::MALE->value) + ->call('create'); + + $newComponent = mountCustomerComponent($this->user); + expect($newComponent->stats[0]['value'])->toBe($initialCustomerCount + 1); +}); + +it('recalculates stats after creating member customer', function () { + $user = User::factory()->create(); + + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', 'New Member') + ->set('form.gender', Gender::MALE->value) + ->set('form.user_id', $user->id) + ->call('create'); + + $newComponent = mountCustomerComponent($this->user); + expect($newComponent->stats[1]['value'])->toBeGreaterThan(0); +}); + +it('recalculates top member after creating orders', function () { + $user = User::factory()->create(); + $customer = Customer::factory()->create(['user_id' => $user->id, 'name' => 'Top Member']); + $outlet = Outlet::factory()->create(); + + Order::factory()->count(5)->create([ + 'customer_id' => $customer->id, + 'user_id' => $user->id, + 'outlet_id' => $outlet->id, + ]); + + $component = mountCustomerComponent($this->user); + expect($component->stats[2]['value'])->toBe('Top Member'); +}); + +/* +|-------------------------------------------------------------------------- +| Edge Cases +|-------------------------------------------------------------------------- +*/ + +it('handles customer with very long name at max length', function () { + $longName = str_repeat('a', 50); + + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', $longName) + ->set('form.gender', Gender::MALE->value) + ->call('create') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('customers', ['name' => $longName]); +}); + +it('handles customer with all gender types', function () { + foreach (Gender::cases() as $gender) { + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', "Customer {$gender->name}") + ->set('form.gender', $gender->value) + ->call('create') + ->assertHasNoErrors(); + } + + expect(Customer::where('gender', Gender::MALE->value)->count())->toBeGreaterThan(0); + expect(Customer::where('gender', Gender::FEMALE->value)->count())->toBeGreaterThan(0); +}); + +it('handles updating customer with same data', function () { + $customer = Customer::factory()->create([ + 'name' => 'John Doe', + 'phone_number' => '0812 3456 7890', + 'gender' => Gender::MALE, + ]); + + mountCustomerComponent($this->user) + ->call('openModal', 'update', 'Edit Customer', $customer->hash) + ->call('update') + ->assertHasNoErrors(); + + $customer->refresh(); + expect($customer->name)->toBe('John Doe'); +}); + +it('handles customer with special characters in name', function () { + $specialName = "O'Brien & Co. - Test"; + + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', $specialName) + ->set('form.gender', Gender::MALE->value) + ->call('create') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('customers', ['name' => $specialName]); +}); + +it('handles multiple customers with same phone number', function () { + $phoneNumber = '0812 3456 7890'; + + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', 'Customer 1') + ->set('form.phone_number', $phoneNumber) + ->set('form.gender', Gender::MALE->value) + ->call('create') + ->assertHasNoErrors(); + + mountCustomerComponent($this->user) + ->call('openModal', 'create', 'Tambah Customer') + ->set('form.name', 'Customer 2') + ->set('form.phone_number', $phoneNumber) + ->set('form.gender', Gender::FEMALE->value) + ->call('create') + ->assertHasNoErrors(); + + expect(Customer::where('phone_number', $phoneNumber)->count())->toBe(2); +}); + +it('handles empty stats when no customers exist', function () { + $component = mountCustomerComponent($this->user); + + expect($component->stats[0]['value'])->toBe(0); + expect($component->stats[1]['value'])->toBe(0); + expect($component->stats[2]['value'])->toBeNull(); +}); + +it('handles customer deletion when customer does not exist', function () { + $customer = Customer::factory()->create(); + $customerId = $customer->id; + $customer->delete(); + + // Try to delete already deleted customer + $deletedCustomer = Customer::withTrashed()->find($customerId); + + mountCustomerComponent($this->user) + ->call('delete', $deletedCustomer) + ->assertHasNoErrors(); + + expect(Customer::withTrashed()->find($customerId)->trashed())->toBeTrue(); });