seed(RolePermissionSeeder::class); }); // ─── Helper ─────────────────────────────────────────────── function createStockUserWithPermission(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 createCompletedCuttingSetup(): array { $product = Product::factory()->create(); $variant = ProductVariant::factory()->create([ 'product_id' => $product->id, 'stock' => 10, 'reject_stock' => 5, ]); $rawMaterial = RawMaterial::factory()->create(); $rawMaterialPrice = RawMaterialPrice::factory()->create([ 'raw_material_id' => $rawMaterial->id, ]); $cutting = Cutting::factory()->create([ 'status' => CuttingStatus::COMPLETED, ]); CuttingMaterial::factory()->create([ 'cutting_id' => $cutting->id, 'raw_material_price_id' => $rawMaterialPrice->id, ]); CuttingResult::factory()->create([ 'cutting_id' => $cutting->id, 'product_variant_id' => $variant->id, 'cutting_result' => 7, 'sample' => 5, // Good stock result 'original_outside_sample' => 2, // Reject stock result ]); return [ 'cutting' => $cutting, 'variant' => $variant, 'raw_material_price' => $rawMaterialPrice, ]; } // ─── Tests ──────────────────────────────────────────────── describe('Stock Warehouse Management', function () { test('authenticated user with permission can view stocks index', function () { $user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW); $this->actingAs($user) ->get(route('admin.manage.stocks.index')) ->assertOk(); }); test('user with permission can submit verification for completed cutting', function () { $user = createStockUserWithPermission( PermissionEnum::STOCKS_VIEW, PermissionEnum::CUTTINGS_VERIFY ); $setup = createCompletedCuttingSetup(); $cutting = $setup['cutting']; $payload = [ 'verification_note' => 'Verification notes', 'results' => [ [ 'product_variant_id' => $setup['variant']->id, 'good' => 6, // changed from 5 to 6 'reject' => 1, // changed from 2 to 1 ], ], 'result_prices' => [ [ 'product_variant_id' => $setup['variant']->id, 'prices' => [ [ 'type' => 'harga_modal', 'price' => 50000, ], [ 'type' => 'retail', 'price' => 75000, ], ], ], ], ]; $this->actingAs($user) ->post(route('admin.manage.stocks.verify', $cutting), $payload) ->assertRedirect(route('admin.manage.stocks.index')); $cutting->refresh(); $this->assertEquals(CuttingStatus::PENDING_VERIFICATION, $cutting->status); // Verify cutting results are updated $this->assertDatabaseHas('cutting_results', [ 'cutting_id' => $cutting->id, 'product_variant_id' => $setup['variant']->id, 'sample' => 6, 'original_outside_sample' => 1, ]); }); test('user with permission can approve verification and increase stock', function () { $user = createStockUserWithPermission( PermissionEnum::STOCKS_VIEW, PermissionEnum::OWNER_VERIFICATIONS_VERIFY ); $setup = createCompletedCuttingSetup(); $cutting = $setup['cutting']; $cutting->status = CuttingStatus::PENDING_VERIFICATION; $cutting->save(); $this->actingAs($user) ->post(route('admin.manage.owner_verifications.approve', $cutting), [ 'approval_note' => 'Approved by owner', ]) ->assertRedirect(); $cutting->refresh(); $setup['variant']->refresh(); $this->assertEquals(CuttingStatus::VERIFIED, $cutting->status); // Stock (good) should increment by 5 (10 + 5 = 15) $this->assertEquals(15, $setup['variant']->stock); // Reject stock should increment by 2 (5 + 2 = 7) $this->assertEquals(7, $setup['variant']->reject_stock); }); test('user with permission can reject verification', function () { $user = createStockUserWithPermission( PermissionEnum::STOCKS_VIEW, PermissionEnum::OWNER_VERIFICATIONS_REJECT ); $setup = createCompletedCuttingSetup(); $cutting = $setup['cutting']; $cutting->status = CuttingStatus::PENDING_VERIFICATION; $cutting->save(); $this->actingAs($user) ->post(route('admin.manage.owner_verifications.reject', $cutting), [ 'reason' => 'Invalid results', ]) ->assertRedirect(); $cutting->refresh(); // Status should revert to COMPLETED $this->assertEquals(CuttingStatus::COMPLETED, $cutting->status); $this->assertDatabaseHas('rejections', [ 'rejectable_id' => $cutting->id, 'rejectable_type' => Cutting::class, 'reason' => 'Invalid results', ]); }); }); // ─── Retail Stock Transfer ────────────────────────────────── describe('Retail Stock Transfer', function () { test('user with permission can transfer stock from good to retail', function () { $user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW); $product = Product::factory()->create(); $variant = ProductVariant::factory()->create([ 'product_id' => $product->id, 'stock' => 10, 'retail_stock' => 5, ]); $this->actingAs($user) ->postJson(route('admin.manage.retail-stock.transfer'), [ 'product_variant_id' => $variant->id, 'quantity' => 3, 'notes' => 'Transfer test', ]) ->assertOk(); $variant->refresh(); $this->assertEquals(7, $variant->stock); $this->assertEquals(8, $variant->retail_stock); $this->assertDatabaseHas('retail_stock_histories', [ 'product_variant_id' => $variant->id, 'user_id' => $user->id, 'quantity' => 3, 'stock_before' => 10, 'retail_stock_before' => 5, 'stock_after' => 7, 'retail_stock_after' => 8, 'notes' => 'Transfer test', ]); }); test('retail stock transfer does not require owner verification', function () { $user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW); $product = Product::factory()->create(); $variant = ProductVariant::factory()->create([ 'product_id' => $product->id, 'stock' => 10, 'retail_stock' => 5, ]); $this->actingAs($user) ->postJson(route('admin.manage.retail-stock.transfer'), [ 'product_variant_id' => $variant->id, 'quantity' => 3, ]) ->assertOk(); $this->assertDatabaseCount('owner_verification_requests', 0); $variant->refresh(); $this->assertEquals(7, $variant->stock); $this->assertEquals(8, $variant->retail_stock); }); test('retail stock transfer fails when quantity exceeds stock', function () { $user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW); $product = Product::factory()->create(); $variant = ProductVariant::factory()->create([ 'product_id' => $product->id, 'stock' => 5, 'retail_stock' => 0, ]); $this->actingAs($user) ->postJson(route('admin.manage.retail-stock.transfer'), [ 'product_variant_id' => $variant->id, 'quantity' => 10, ]) ->assertUnprocessable() ->assertJsonValidationErrors('quantity'); }); test('retail stock transfer fails when quantity is zero', function () { $user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW); $product = Product::factory()->create(); $variant = ProductVariant::factory()->create([ 'product_id' => $product->id, 'stock' => 10, 'retail_stock' => 5, ]); $this->actingAs($user) ->postJson(route('admin.manage.retail-stock.transfer'), [ 'product_variant_id' => $variant->id, 'quantity' => 0, ]) ->assertUnprocessable() ->assertJsonValidationErrors('quantity'); }); test('user without permission cannot transfer stock', function () { $user = User::factory()->create(); $product = Product::factory()->create(); $variant = ProductVariant::factory()->create([ 'product_id' => $product->id, 'stock' => 10, 'retail_stock' => 5, ]); $this->actingAs($user) ->postJson(route('admin.manage.retail-stock.transfer'), [ 'product_variant_id' => $variant->id, 'quantity' => 3, ]) ->assertForbidden(); }); test('owner can verify stock directly without pending verification', function () { $user = createStockUserWithPermission( PermissionEnum::STOCKS_VIEW, PermissionEnum::CUTTINGS_VERIFY, PermissionEnum::OWNER_VERIFICATIONS_VERIFY ); $setup = createCompletedCuttingSetup(); $cutting = $setup['cutting']; $payload = [ 'verification_note' => 'Verified directly by owner', 'results' => [ [ 'product_variant_id' => $setup['variant']->id, 'good' => 6, 'reject' => 1, ], ], 'result_prices' => [ [ 'product_variant_id' => $setup['variant']->id, 'prices' => [ [ 'type' => 'harga_modal', 'price' => 50000, ], [ 'type' => 'retail', 'price' => 75000, ], ], ], ], ]; $this->actingAs($user) ->post(route('admin.manage.stocks.verify', $cutting), $payload) ->assertRedirect(route('admin.manage.stocks.index')); $cutting->refresh(); $this->assertEquals(CuttingStatus::VERIFIED, $cutting->status); $setup['variant']->refresh(); // Base stock was 10. Added good stock is 6. $this->assertEquals(16, $setup['variant']->stock); }); });