seed(RolePermissionSeeder::class); }); // ─── Helper ─────────────────────────────────────────────── function createStokOpnameUserWithPermission(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; } function createCatalogSetup(): array { $product = Product::factory()->create(); $variant = ProductVariant::factory()->create([ 'product_id' => $product->id, 'stock' => 10, 'retail_stock' => 5, 'reject_stock' => 2, ]); return ['product' => $product, 'variant' => $variant]; } // ─── Index & Create ─────────────────────────────────────── describe('Stok Opname Index & Create', function () { test('authenticated user with permission can view stok opnames index', function () { $user = createStokOpnameUserWithPermission(PermissionEnum::STOK_OPNAMES_VIEW); $this->actingAs($user) ->get(route('admin.manage.stok-opnames.index')) ->assertOk(); }); test('user without permission is forbidden to view index', function () { $user = User::factory()->create(); $this->actingAs($user) ->get(route('admin.manage.stok-opnames.index')) ->assertForbidden(); }); test('user with permission can view create page', function () { $user = createStokOpnameUserWithPermission( PermissionEnum::STOK_OPNAMES_VIEW, PermissionEnum::STOK_OPNAMES_CREATE ); $this->actingAs($user) ->get(route('admin.manage.stok-opnames.create')) ->assertOk(); }); }); // ─── Store & Update ─────────────────────────────────────── describe('Stok Opname Store & Update', function () { test('user can store a new stok opname', function () { $user = createStokOpnameUserWithPermission( PermissionEnum::STOK_OPNAMES_VIEW, PermissionEnum::STOK_OPNAMES_CREATE ); $setup = createCatalogSetup(); $payload = [ 'opname_date' => now()->format('Y-m-d'), 'notes' => 'Test Opname Notes', 'items' => [ [ 'product_variant_id' => $setup['variant']->id, 'stock_quality' => ProductStockQuality::GOOD->value, 'physical_stock' => 8, 'notes' => 'Kurang 2 bagus', ], [ 'product_variant_id' => $setup['variant']->id, 'stock_quality' => ProductStockQuality::REJECT->value, 'physical_stock' => 3, 'notes' => 'Kelebihan 1 reject', ] ], ]; $this->actingAs($user) ->post(route('admin.manage.stok-opnames.store'), $payload) ->assertRedirect(route('admin.manage.stok-opnames.index')); $this->assertDatabaseHas('stok_opnames', [ 'notes' => 'Test Opname Notes', 'status' => StokOpnameStatus::DRAFT->value, ]); $this->assertDatabaseHas('stok_opname_items', [ 'product_variant_id' => $setup['variant']->id, 'stock_quality' => ProductStockQuality::GOOD->value, 'physical_stock' => 8, 'difference' => -2, ]); $this->assertDatabaseHas('stok_opname_items', [ 'product_variant_id' => $setup['variant']->id, 'stock_quality' => ProductStockQuality::REJECT->value, 'physical_stock' => 3, 'difference' => 1, ]); }); test('user can auto-save a stok opname draft', function () { $user = createStokOpnameUserWithPermission( PermissionEnum::STOK_OPNAMES_VIEW, PermissionEnum::STOK_OPNAMES_CREATE ); $setup = createCatalogSetup(); $payload = [ 'opname_date' => now()->format('Y-m-d'), 'notes' => 'Draft Notes', 'items' => [ [ 'product_variant_id' => $setup['variant']->id, 'stock_quality' => ProductStockQuality::RETAIL->value, 'physical_stock' => 6, 'notes' => 'Tambah eceran', ] ], ]; $response = $this->actingAs($user) ->postJson(route('admin.manage.stok-opnames.auto_save'), $payload) ->assertOk(); $stokOpnameId = $response->json('stok_opname_id'); $this->assertDatabaseHas('stok_opnames', [ 'id' => $stokOpnameId, 'status' => StokOpnameStatus::DRAFT->value, ]); }); test('user can update a draft stok opname', function () { $user = createStokOpnameUserWithPermission( PermissionEnum::STOK_OPNAMES_VIEW, PermissionEnum::STOK_OPNAMES_UPDATE ); $setup = createCatalogSetup(); $stokOpname = StokOpname::create([ 'opname_date' => now(), 'status' => StokOpnameStatus::DRAFT, 'created_by_id' => $user->id, ]); $item = StokOpnameItem::create([ 'stok_opname_id' => $stokOpname->id, 'product_variant_id' => $setup['variant']->id, 'stock_quality' => ProductStockQuality::GOOD, 'system_stock' => 10, 'physical_stock' => 8, 'difference' => -2, ]); $payload = [ 'opname_date' => now()->format('Y-m-d'), 'notes' => 'Updated Notes', 'items' => [ [ 'product_variant_id' => $setup['variant']->id, 'stock_quality' => ProductStockQuality::GOOD->value, 'physical_stock' => 9, 'notes' => 'Diubah jadi 9', ] ], ]; $this->actingAs($user) ->put(route('admin.manage.stok-opnames.update', $stokOpname), $payload) ->assertRedirect(route('admin.manage.stok-opnames.index')); $this->assertDatabaseHas('stok_opnames', [ 'id' => $stokOpname->id, 'notes' => 'Updated Notes', ]); $this->assertDatabaseHas('stok_opname_items', [ 'stok_opname_id' => $stokOpname->id, 'product_variant_id' => $setup['variant']->id, 'stock_quality' => ProductStockQuality::GOOD->value, 'physical_stock' => 9, 'difference' => -1, ]); }); }); // ─── Submit, Verify & Reject ────────────────────────────── describe('Stok Opname Submission & Verification', function () { test('user can submit a draft stok opname for verification', function () { $user = createStokOpnameUserWithPermission( PermissionEnum::STOK_OPNAMES_VIEW, PermissionEnum::STOK_OPNAMES_SUBMIT ); $setup = createCatalogSetup(); $stokOpname = StokOpname::create([ 'opname_date' => now(), 'status' => StokOpnameStatus::DRAFT, 'created_by_id' => $user->id, ]); StokOpnameItem::create([ 'stok_opname_id' => $stokOpname->id, 'product_variant_id' => $setup['variant']->id, 'stock_quality' => ProductStockQuality::GOOD, 'system_stock' => 10, 'physical_stock' => 8, 'difference' => -2, ]); $this->actingAs($user) ->post(route('admin.manage.stok-opnames.submit', $stokOpname)) ->assertRedirect(route('admin.manage.stok-opnames.index')); $this->assertEquals(StokOpnameStatus::PENDING, $stokOpname->refresh()->status); }); test('user with verify permission can verify stok opname and reconcile multiple qualities', function () { $user = createStokOpnameUserWithPermission( PermissionEnum::STOK_OPNAMES_VIEW, PermissionEnum::STOK_OPNAMES_VERIFY ); $setup = createCatalogSetup(); $stokOpname = StokOpname::create([ 'opname_date' => now(), 'status' => StokOpnameStatus::PENDING, 'created_by_id' => $user->id, ]); // Create opname items for Good, Retail, and Reject qualities StokOpnameItem::create([ 'stok_opname_id' => $stokOpname->id, 'product_variant_id' => $setup['variant']->id, 'stock_quality' => ProductStockQuality::GOOD, 'system_stock' => 10, 'physical_stock' => 8, 'difference' => -2, ]); StokOpnameItem::create([ 'stok_opname_id' => $stokOpname->id, 'product_variant_id' => $setup['variant']->id, 'stock_quality' => ProductStockQuality::RETAIL, 'system_stock' => 5, 'physical_stock' => 7, 'difference' => 2, ]); StokOpnameItem::create([ 'stok_opname_id' => $stokOpname->id, 'product_variant_id' => $setup['variant']->id, 'stock_quality' => ProductStockQuality::REJECT, 'system_stock' => 2, 'physical_stock' => 0, 'difference' => -2, ]); $this->actingAs($user) ->post(route('admin.manage.stok-opnames.verify', $stokOpname), [ 'verification_notes' => 'Verified successfully', ]) ->assertRedirect(route('admin.manage.stok-opnames.index')); $stokOpname->refresh(); $setup['variant']->refresh(); $this->assertEquals(StokOpnameStatus::VERIFIED, $stokOpname->status); $this->assertEquals('Verified successfully', $stokOpname->verification_notes); $this->assertEquals($user->id, $stokOpname->verified_by_id); // Verify variant stocks are updated correctly matching their quality enums $this->assertEquals(8, $setup['variant']->stock); $this->assertEquals(7, $setup['variant']->retail_stock); $this->assertEquals(0, $setup['variant']->reject_stock); }); test('user with verify permission can reject stok opname', function () { $user = createStokOpnameUserWithPermission( PermissionEnum::STOK_OPNAMES_VIEW, PermissionEnum::STOK_OPNAMES_VERIFY ); $setup = createCatalogSetup(); $stokOpname = StokOpname::create([ 'opname_date' => now(), 'status' => StokOpnameStatus::PENDING, 'created_by_id' => $user->id, ]); $this->actingAs($user) ->post(route('admin.manage.stok-opnames.reject', $stokOpname), [ 'reason' => 'Data is invalid', ]) ->assertRedirect(route('admin.manage.stok-opnames.index')); $stokOpname->refresh(); $this->assertEquals(StokOpnameStatus::REJECTED, $stokOpname->status); $this->assertEquals('Data is invalid', $stokOpname->verification_notes); }); });