diff --git a/app/Services/Master/SupplierService.php b/app/Services/Master/SupplierService.php index a236b27..ffbc0a1 100644 --- a/app/Services/Master/SupplierService.php +++ b/app/Services/Master/SupplierService.php @@ -8,9 +8,6 @@ class SupplierService { - /** - * @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery - */ public function paginateForIndex(array $tableQuery): LengthAwarePaginator { $query = Supplier::query() @@ -30,17 +27,11 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator ->withQueryString(); } - /** - * @param array $validated - */ public function create(array $validated): void { Supplier::create($validated); } - /** - * @param array $validated - */ public function update(Supplier $supplier, array $validated): void { $supplier->update($validated); @@ -51,6 +42,18 @@ public function delete(Supplier $supplier): void $supplier->delete(); } + public function getSelectOptions(): array + { + return Supplier::query() + ->orderBy('name') + ->get(['id', 'name']) + ->map(fn (Supplier $supplier) => [ + 'value' => $supplier->id, + 'label' => $supplier->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/suppliers/Index.vue b/resources/js/pages/admin/master/suppliers/Index.vue index 7f43dd2..d0bd86d 100644 --- a/resources/js/pages/admin/master/suppliers/Index.vue +++ b/resources/js/pages/admin/master/suppliers/Index.vue @@ -2,8 +2,6 @@ import { Head } from '@inertiajs/vue3'; import { Plus } from '@lucide/vue'; import { computed, ref, watch } from 'vue'; -import { createColumns } from './table/columns'; -import SupplierFormModal from './form/SupplierFormModal.vue'; import { DataTable } from '@/components/data-table'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; @@ -12,6 +10,8 @@ import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataT import AdminLayout from '@/layouts/AdminLayout.vue'; import type { DataTableSort } from '@/types/data-table'; import type { SupplierListItem, PaginatedSuppliers } from '@/types/supplier'; +import SupplierFormModal from './form/SupplierFormModal.vue'; +import { createColumns } from './table/columns'; const props = defineProps<{ suppliers: PaginatedSuppliers; diff --git a/tests/Feature/Admin/Master/SupplierTest.php b/tests/Feature/Admin/Master/SupplierTest.php new file mode 100644 index 0000000..2817130 --- /dev/null +++ b/tests/Feature/Admin/Master/SupplierTest.php @@ -0,0 +1,349 @@ +seed(RolePermissionSeeder::class); +}); + +// ─── Helper ─────────────────────────────────────────────── + +function createSupplierUserWithPermission(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('Supplier Index', function () { + test('authenticated user with permission can view supplier index', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW); + + Supplier::factory()->count(3)->create(); + + $this->actingAs($user) + ->get(route('admin.master.suppliers.index')) + ->assertOk(); + }); + + test('guest is redirected to login', function () { + $this->get(route('admin.master.suppliers.index')) + ->assertRedirect(route('login')); + }); + + test('user without permission is forbidden', function () { + $user = User::factory()->create(); + + $this->actingAs($user) + ->get(route('admin.master.suppliers.index')) + ->assertForbidden(); + }); + + test('index returns suppliers with pagination', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW); + + Supplier::factory()->count(15)->create(); + + $this->actingAs($user) + ->get(route('admin.master.suppliers.index')) + ->assertOk(); + }); + + test('index can search suppliers by name', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW); + + Supplier::factory()->create(['name' => 'PT Maju Jaya']); + Supplier::factory()->create(['name' => 'CV Berkah Tekstil']); + + $this->actingAs($user) + ->get(route('admin.master.suppliers.index', ['search' => 'Maju'])) + ->assertOk(); + }); +}); + +// ─── Store ──────────────────────────────────────────────── + +describe('Supplier Store', function () { + test('authenticated user with permission can create a supplier', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW, PermissionEnum::SUPPLIERS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.suppliers.store'), [ + 'name' => 'PT Supplier Baru', + 'phone_number' => '081234567890', + 'address' => 'Jl. Merdeka No. 1', + ]) + ->assertRedirect(route('admin.master.suppliers.index')); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'PT Supplier Baru', + 'phone_number' => '081234567890', + 'address' => 'Jl. Merdeka No. 1', + ]); + }); + + test('guest cannot create a supplier', function () { + $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'PT Supplier Baru', + ])->assertRedirect(route('login')); + }); + + test('user without create permission is forbidden', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW); + + $this->actingAs($user) + ->post(route('admin.master.suppliers.store'), [ + 'name' => 'PT Supplier Baru', + ]) + ->assertForbidden(); + + $this->assertDatabaseCount('suppliers', 0); + }); + + test('name is required', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW, PermissionEnum::SUPPLIERS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.suppliers.store'), [ + 'name' => '', + ]) + ->assertSessionHasErrors('name'); + + $this->assertDatabaseCount('suppliers', 0); + }); + + test('name must not exceed 200 characters', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW, PermissionEnum::SUPPLIERS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.suppliers.store'), [ + 'name' => str_repeat('a', 201), + ]) + ->assertSessionHasErrors('name'); + + $this->assertDatabaseCount('suppliers', 0); + }); + + test('phone number must start with 08', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW, PermissionEnum::SUPPLIERS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.suppliers.store'), [ + 'name' => 'PT Supplier Baru', + 'phone_number' => '0211234567', + ]) + ->assertSessionHasErrors('phone_number'); + + $this->assertDatabaseCount('suppliers', 0); + }); + + test('phone number can be null', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW, PermissionEnum::SUPPLIERS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.suppliers.store'), [ + 'name' => 'PT Supplier Baru', + 'phone_number' => null, + ]) + ->assertRedirect(route('admin.master.suppliers.index')); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'PT Supplier Baru', + 'phone_number' => null, + ]); + }); + + test('address can be null', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW, PermissionEnum::SUPPLIERS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.suppliers.store'), [ + 'name' => 'PT Supplier Baru', + 'address' => null, + ]) + ->assertRedirect(route('admin.master.suppliers.index')); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'PT Supplier Baru', + 'address' => null, + ]); + }); +}); + +// ─── Update ─────────────────────────────────────────────── + +describe('Supplier Update', function () { + test('authenticated user with permission can update a supplier', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW, PermissionEnum::SUPPLIERS_UPDATE); + + $supplier = Supplier::factory()->create(['name' => 'Nama Lama']); + + $this->actingAs($user) + ->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Nama Baru', + 'phone_number' => '089876543210', + 'address' => 'Jl. Baru No. 2', + ]) + ->assertRedirect(route('admin.master.suppliers.index')); + + $this->assertDatabaseHas('suppliers', [ + 'id' => $supplier->id, + 'name' => 'Nama Baru', + 'phone_number' => '089876543210', + 'address' => 'Jl. Baru No. 2', + ]); + }); + + test('guest cannot update a supplier', function () { + $supplier = Supplier::factory()->create(); + + $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Nama Baru', + ])->assertRedirect(route('login')); + }); + + test('user without update permission is forbidden', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW); + + $supplier = Supplier::factory()->create(['name' => 'Nama Lama']); + + $this->actingAs($user) + ->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Nama Baru', + ]) + ->assertForbidden(); + + $this->assertDatabaseHas('suppliers', [ + 'id' => $supplier->id, + 'name' => 'Nama Lama', + ]); + }); + + test('name is required on update', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW, PermissionEnum::SUPPLIERS_UPDATE); + + $supplier = Supplier::factory()->create(); + + $this->actingAs($user) + ->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => '', + ]) + ->assertSessionHasErrors('name'); + }); + + test('name must not exceed 200 characters on update', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW, PermissionEnum::SUPPLIERS_UPDATE); + + $supplier = Supplier::factory()->create(); + + $this->actingAs($user) + ->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => str_repeat('a', 201), + ]) + ->assertSessionHasErrors('name'); + }); + + test('phone number validation on update', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW, PermissionEnum::SUPPLIERS_UPDATE); + + $supplier = Supplier::factory()->create(); + + $this->actingAs($user) + ->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Nama Valid', + 'phone_number' => '123invalid', + ]) + ->assertSessionHasErrors('phone_number'); + }); +}); + +// ─── Destroy ────────────────────────────────────────────── + +describe('Supplier Destroy', function () { + test('authenticated user with permission can delete a supplier', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW, PermissionEnum::SUPPLIERS_DELETE); + + $supplier = Supplier::factory()->create(); + + $this->actingAs($user) + ->delete(route('admin.master.suppliers.destroy', $supplier)) + ->assertRedirect(route('admin.master.suppliers.index')); + + $this->assertSoftDeleted('suppliers', [ + 'id' => $supplier->id, + ]); + }); + + test('guest cannot delete a supplier', function () { + $supplier = Supplier::factory()->create(); + + $this->delete(route('admin.master.suppliers.destroy', $supplier)) + ->assertRedirect(route('login')); + + $this->assertNotSoftDeleted('suppliers', [ + 'id' => $supplier->id, + ]); + }); + + test('user without delete permission is forbidden', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW); + + $supplier = Supplier::factory()->create(); + + $this->actingAs($user) + ->delete(route('admin.master.suppliers.destroy', $supplier)) + ->assertForbidden(); + + $this->assertNotSoftDeleted('suppliers', [ + 'id' => $supplier->id, + ]); + }); + + test('deleted supplier is soft deleted', function () { + $user = createSupplierUserWithPermission(PermissionEnum::SUPPLIERS_VIEW, PermissionEnum::SUPPLIERS_DELETE); + + $supplier = Supplier::factory()->create(); + + $this->actingAs($user) + ->delete(route('admin.master.suppliers.destroy', $supplier)); + + $this->assertSoftDeleted('suppliers', ['id' => $supplier->id]); + $this->assertDatabaseHas('suppliers', ['id' => $supplier->id]); + }); +}); + +// ─── Supplier Model ─────────────────────────────────────── + +describe('Supplier Model', function () { + test('supplier uses soft deletes', function () { + $supplier = Supplier::factory()->create(); + + $supplier->delete(); + + expect($supplier->trashed())->toBeTrue(); + }); + + test('supplier can have purchases', function () { + $supplier = Supplier::factory()->create(); + + expect($supplier->purchases)->toBeInstanceOf(Collection::class); + }); +});