diff --git a/app/Services/Master/CustomerService.php b/app/Services/Master/CustomerService.php index 9a13084..d4391bf 100644 --- a/app/Services/Master/CustomerService.php +++ b/app/Services/Master/CustomerService.php @@ -8,9 +8,6 @@ class CustomerService { - /** - * @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery - */ public function paginateForIndex(array $tableQuery): LengthAwarePaginator { $query = Customer::query() @@ -30,17 +27,11 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator ->withQueryString(); } - /** - * @param array $validated - */ public function create(array $validated): void { Customer::create($validated); } - /** - * @param array $validated - */ public function update(Customer $customer, array $validated): void { $customer->update($validated); @@ -51,6 +42,18 @@ public function delete(Customer $customer): void $customer->delete(); } + public function getSelectOptions(): array + { + return Customer::query() + ->orderBy('name') + ->get(['id', 'name']) + ->map(fn (Customer $customer) => [ + 'value' => $customer->id, + 'label' => $customer->name, + ]) + ->all(); + } + private function applySorting(Builder $query, string $sort, string $direction): void { if (in_array($sort, ['name', 'phone_number', 'address'], true)) { diff --git a/resources/js/pages/admin/master/customers/Index.vue b/resources/js/pages/admin/master/customers/Index.vue index 7296ecb..01fc30e 100644 --- a/resources/js/pages/admin/master/customers/Index.vue +++ b/resources/js/pages/admin/master/customers/Index.vue @@ -2,16 +2,16 @@ import { Head } from '@inertiajs/vue3'; import { Plus } from '@lucide/vue'; import { computed, ref, watch } from 'vue'; -import { createColumns } from './table/columns'; -import CustomerFormModal from './form/CustomerFormModal.vue'; import { DataTable } from '@/components/data-table'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { useCan } from '@/composables/useCan'; import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery'; import AdminLayout from '@/layouts/AdminLayout.vue'; -import type { DataTableSort } from '@/types/data-table'; import type { CustomerListItem, PaginatedCustomers } from '@/types/customer'; +import type { DataTableSort } from '@/types/data-table'; +import CustomerFormModal from './form/CustomerFormModal.vue'; +import { createColumns } from './table/columns'; const props = defineProps<{ customers: PaginatedCustomers; diff --git a/resources/js/pages/admin/master/customers/table/columns.ts b/resources/js/pages/admin/master/customers/table/columns.ts index 30b5739..5f571a3 100644 --- a/resources/js/pages/admin/master/customers/table/columns.ts +++ b/resources/js/pages/admin/master/customers/table/columns.ts @@ -1,8 +1,8 @@ import type { ColumnDef } from '@tanstack/vue-table'; import { h } from 'vue'; import { DataTableColumnHeader } from '@/components/data-table'; -import DataTableActions from './data-table-actions.vue'; import type { CustomerListItem } from '@/types/customer'; +import DataTableActions from './data-table-actions.vue'; export function createColumns(onEdit: (customer: CustomerListItem) => void): ColumnDef[] { return [ diff --git a/tests/Feature/Admin/Master/CustomerTest.php b/tests/Feature/Admin/Master/CustomerTest.php new file mode 100644 index 0000000..f8d02f3 --- /dev/null +++ b/tests/Feature/Admin/Master/CustomerTest.php @@ -0,0 +1,349 @@ +seed(RolePermissionSeeder::class); +}); + +// ─── Helper ─────────────────────────────────────────────── + +function createCustomerUserWithPermission(PermissionEnum ...$permissions): User +{ + $user = User::factory()->create(); + + $user->givePermissionTo( + array_merge( + [PermissionEnum::DASHBOARD_VIEW->value], + array_map(fn (PermissionEnum $p) => $p->value, $permissions) + ) + ); + + $user->forgetCachedPermissions(); + + return $user; +} + +// ─── Index ──────────────────────────────────────────────── + +describe('Customer Index', function () { + test('authenticated user with permission can view customer index', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW); + + Customer::factory()->count(3)->create(); + + $this->actingAs($user) + ->get(route('admin.master.customers.index')) + ->assertOk(); + }); + + test('guest is redirected to login', function () { + $this->get(route('admin.master.customers.index')) + ->assertRedirect(route('login')); + }); + + test('user without permission is forbidden', function () { + $user = User::factory()->create(); + + $this->actingAs($user) + ->get(route('admin.master.customers.index')) + ->assertForbidden(); + }); + + test('index returns customers with pagination', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW); + + Customer::factory()->count(15)->create(); + + $this->actingAs($user) + ->get(route('admin.master.customers.index')) + ->assertOk(); + }); + + test('index can search customers by name', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW); + + Customer::factory()->create(['name' => 'Budi']); + Customer::factory()->create(['name' => 'Ucup']); + + $this->actingAs($user) + ->get(route('admin.master.customers.index', ['search' => 'Maju'])) + ->assertOk(); + }); +}); + +// ─── Store ──────────────────────────────────────────────── + +describe('Customer Store', function () { + test('authenticated user with permission can create a customer', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW, PermissionEnum::CUSTOMERS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.customers.store'), [ + 'name' => 'Ujang', + 'phone_number' => '081234567890', + 'address' => 'Jl. Merdeka No. 1', + ]) + ->assertRedirect(route('admin.master.customers.index')); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Ujang', + 'phone_number' => '081234567890', + 'address' => 'Jl. Merdeka No. 1', + ]); + }); + + test('guest cannot create a customer', function () { + $this->post(route('admin.master.customers.store'), [ + 'name' => 'Ujang', + ])->assertRedirect(route('login')); + }); + + test('user without create permission is forbidden', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW); + + $this->actingAs($user) + ->post(route('admin.master.customers.store'), [ + 'name' => 'Ujang', + ]) + ->assertForbidden(); + + $this->assertDatabaseCount('customers', 0); + }); + + test('name is required', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW, PermissionEnum::CUSTOMERS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.customers.store'), [ + 'name' => '', + ]) + ->assertSessionHasErrors('name'); + + $this->assertDatabaseCount('customers', 0); + }); + + test('name must not exceed 200 characters', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW, PermissionEnum::CUSTOMERS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.customers.store'), [ + 'name' => str_repeat('a', 201), + ]) + ->assertSessionHasErrors('name'); + + $this->assertDatabaseCount('customers', 0); + }); + + test('phone number must start with 08', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW, PermissionEnum::CUSTOMERS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.customers.store'), [ + 'name' => 'Ujang', + 'phone_number' => '0211234567', + ]) + ->assertSessionHasErrors('phone_number'); + + $this->assertDatabaseCount('customers', 0); + }); + + test('phone number can be null', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW, PermissionEnum::CUSTOMERS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.customers.store'), [ + 'name' => 'Ujang', + 'phone_number' => null, + ]) + ->assertRedirect(route('admin.master.customers.index')); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Ujang', + 'phone_number' => null, + ]); + }); + + test('address can be null', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW, PermissionEnum::CUSTOMERS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.customers.store'), [ + 'name' => 'Ujang', + 'address' => null, + ]) + ->assertRedirect(route('admin.master.customers.index')); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Ujang', + 'address' => null, + ]); + }); +}); + +// ─── Update ─────────────────────────────────────────────── + +describe('Customer Update', function () { + test('authenticated user with permission can update a customer', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW, PermissionEnum::CUSTOMERS_UPDATE); + + $customer = Customer::factory()->create(['name' => 'Nama Lama']); + + $this->actingAs($user) + ->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Nama Baru', + 'phone_number' => '089876543210', + 'address' => 'Jl. Baru No. 2', + ]) + ->assertRedirect(route('admin.master.customers.index')); + + $this->assertDatabaseHas('customers', [ + 'id' => $customer->id, + 'name' => 'Nama Baru', + 'phone_number' => '089876543210', + 'address' => 'Jl. Baru No. 2', + ]); + }); + + test('guest cannot update a customer', function () { + $customer = Customer::factory()->create(); + + $this->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Nama Baru', + ])->assertRedirect(route('login')); + }); + + test('user without update permission is forbidden', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW); + + $customer = Customer::factory()->create(['name' => 'Nama Lama']); + + $this->actingAs($user) + ->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Nama Baru', + ]) + ->assertForbidden(); + + $this->assertDatabaseHas('customers', [ + 'id' => $customer->id, + 'name' => 'Nama Lama', + ]); + }); + + test('name is required on update', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW, PermissionEnum::CUSTOMERS_UPDATE); + + $customer = Customer::factory()->create(); + + $this->actingAs($user) + ->put(route('admin.master.customers.update', $customer), [ + 'name' => '', + ]) + ->assertSessionHasErrors('name'); + }); + + test('name must not exceed 200 characters on update', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW, PermissionEnum::CUSTOMERS_UPDATE); + + $customer = Customer::factory()->create(); + + $this->actingAs($user) + ->put(route('admin.master.customers.update', $customer), [ + 'name' => str_repeat('a', 201), + ]) + ->assertSessionHasErrors('name'); + }); + + test('phone number validation on update', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW, PermissionEnum::CUSTOMERS_UPDATE); + + $customer = Customer::factory()->create(); + + $this->actingAs($user) + ->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Nama Valid', + 'phone_number' => '123invalid', + ]) + ->assertSessionHasErrors('phone_number'); + }); +}); + +// ─── Destroy ────────────────────────────────────────────── + +describe('Customer Destroy', function () { + test('authenticated user with permission can delete a customer', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW, PermissionEnum::CUSTOMERS_DELETE); + + $customer = Customer::factory()->create(); + + $this->actingAs($user) + ->delete(route('admin.master.customers.destroy', $customer)) + ->assertRedirect(route('admin.master.customers.index')); + + $this->assertSoftDeleted('customers', [ + 'id' => $customer->id, + ]); + }); + + test('guest cannot delete a customer', function () { + $customer = Customer::factory()->create(); + + $this->delete(route('admin.master.customers.destroy', $customer)) + ->assertRedirect(route('login')); + + $this->assertNotSoftDeleted('customers', [ + 'id' => $customer->id, + ]); + }); + + test('user without delete permission is forbidden', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW); + + $customer = Customer::factory()->create(); + + $this->actingAs($user) + ->delete(route('admin.master.customers.destroy', $customer)) + ->assertForbidden(); + + $this->assertNotSoftDeleted('customers', [ + 'id' => $customer->id, + ]); + }); + + test('deleted customer is soft deleted', function () { + $user = createCustomerUserWithPermission(PermissionEnum::CUSTOMERS_VIEW, PermissionEnum::CUSTOMERS_DELETE); + + $customer = Customer::factory()->create(); + + $this->actingAs($user) + ->delete(route('admin.master.customers.destroy', $customer)); + + $this->assertSoftDeleted('customers', ['id' => $customer->id]); + $this->assertDatabaseHas('customers', ['id' => $customer->id]); + }); +}); + +// ─── Customer Model ─────────────────────────────────────── + +describe('Customer Model', function () { + test('customer uses soft deletes', function () { + $customer = Customer::factory()->create(); + + $customer->delete(); + + expect($customer->trashed())->toBeTrue(); + }); + + test('customer can have orders', function () { + $customer = Customer::factory()->create(); + + expect($customer->orders)->toBeInstanceOf(Collection::class); + }); +});