seed(RolePermissionSeeder::class); Storage::fake('public'); }); // ─── Helper ─────────────────────────────────────────────── function createRawMaterialUserWithPermission(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 createRawMaterialWithPrices(): RawMaterial { $rawMaterial = RawMaterial::factory()->create(); $prices = RawMaterialPrice::factory()->count(2)->create(['raw_material_id' => $rawMaterial->id]); foreach ($prices as $price) { $price->addMedia(UploadedFile::fake()->image("{$price->variant}.jpg", 100, 100)) ->toMediaCollection('images'); } return $rawMaterial; } function priceWithImage(string $variant = 'Default', int $price = 50000, float $stock = 10): array { $disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public'); $key = 'raw-materials/'.Str::uuid().'.jpg'; $imageContent = UploadedFile::fake()->image("{$variant}.jpg", 100, 100)->get(); Storage::disk($disk)->put($key, $imageContent); return [ 'variant' => $variant, 'price' => $price, 'stock' => $stock, 's3_keys' => [$key], ]; } function createRawMaterialVerifierUser(): User { $user = User::factory()->create(); $user->givePermissionTo([ PermissionEnum::DASHBOARD_VIEW->value, PermissionEnum::OWNER_VERIFICATIONS_VIEW->value, PermissionEnum::OWNER_VERIFICATIONS_VERIFY->value, PermissionEnum::OWNER_VERIFICATIONS_REJECT->value, ]); $user->forgetCachedPermissions(); return $user; } // ─── Index ──────────────────────────────────────────────── describe('Raw Material Index', function () { test('authenticated user with permission can view raw material index', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); createRawMaterialWithPrices(); $this->actingAs($user) ->get(route('admin.master.raw_materials.index')) ->assertOk(); }); test('guest is redirected to login', function () { $this->get(route('admin.master.raw_materials.index')) ->assertRedirect(route('login')); }); test('user without permission is forbidden', function () { $user = User::factory()->create(); $this->actingAs($user) ->get(route('admin.master.raw_materials.index')) ->assertForbidden(); }); test('index displays raw materials with prices', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); createRawMaterialWithPrices(); createRawMaterialWithPrices(); $this->actingAs($user) ->get(route('admin.master.raw_materials.index')) ->assertOk(); }); test('index can search raw materials by name', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); $rawMaterial = RawMaterial::factory()->create(['name' => 'Kain Batik']); RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $this->actingAs($user) ->get(route('admin.master.raw_materials.index', ['search' => 'Kain'])) ->assertOk(); }); test('index can search raw materials by id', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); $rawMaterial1 = RawMaterial::factory()->create(['name' => 'Kain Batik']); RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial1->id]); $rawMaterial2 = RawMaterial::factory()->create(['name' => 'Sutra Premium']); RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial2->id]); $response = $this->actingAs($user) ->get(route('admin.master.raw_materials.index', ['search_id' => $rawMaterial1->id])); $response->assertOk(); $response->assertInertia(fn ($page) => $page ->has('rawMaterials.data', 1) ->where('rawMaterials.data.0.id', $rawMaterial1->id) ); }); test('index can filter by active status', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); createRawMaterialWithPrices(); $this->actingAs($user) ->get(route('admin.master.raw_materials.index', ['is_active' => '1'])) ->assertOk(); }); test('index can filter by stock status (out of stock)', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); $inStock = RawMaterial::factory()->create(); RawMaterialPrice::factory()->create(['raw_material_id' => $inStock->id, 'stock' => 10]); $outOfStock = RawMaterial::factory()->create(); RawMaterialPrice::factory()->create(['raw_material_id' => $outOfStock->id, 'stock' => 0]); $response = $this->actingAs($user) ->get(route('admin.master.raw_materials.index', ['stock_status' => 'out_of_stock'])); $response->assertOk(); $response->assertInertia(fn ($page) => $page ->has('rawMaterials.data', 1) ->where('rawMaterials.data.0.id', $outOfStock->id) ); }); test('index can filter by stock status (low stock)', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); $yardNormal = RawMaterial::factory()->create(['unit' => RawMaterialUnit::YARD->value]); RawMaterialPrice::factory()->create(['raw_material_id' => $yardNormal->id, 'stock' => 25]); $yardLow = RawMaterial::factory()->create(['unit' => RawMaterialUnit::YARD->value]); RawMaterialPrice::factory()->create(['raw_material_id' => $yardLow->id, 'stock' => 15]); $response = $this->actingAs($user) ->get(route('admin.master.raw_materials.index', ['stock_status' => 'low_stock'])); $response->assertOk(); $response->assertInertia(fn ($page) => $page ->has('rawMaterials.data', 1) ->where('rawMaterials.data.0.id', $yardLow->id) ); }); }); // ─── Create ─────────────────────────────────────────────── describe('Raw Material Create', function () { test('authenticated user with permission can view create form', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $this->actingAs($user) ->get(route('admin.master.raw_materials.create')) ->assertOk(); }); test('guest is redirected to login', function () { $this->get(route('admin.master.raw_materials.create')) ->assertRedirect(route('login')); }); test('user without create permission is forbidden', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); $this->actingAs($user) ->get(route('admin.master.raw_materials.create')) ->assertForbidden(); }); }); // ─── Store ──────────────────────────────────────────────── describe('Raw Material Store', function () { test('authenticated user with permission can create raw material', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $this->actingAs($user) ->post(route('admin.master.raw_materials.store'), [ 'name' => 'Kain Sutra', 'unit' => RawMaterialUnit::METER->value, 'prices' => [ priceWithImage('Merah', 75000, 50), ], ]) ->assertRedirect(route('admin.master.raw_materials.index')); $this->assertDatabaseHas('raw_materials', [ 'name' => 'Kain Sutra', 'unit' => RawMaterialUnit::METER->value, 'is_active' => true, ]); }); test('guest cannot create a raw material', function () { $this->post(route('admin.master.raw_materials.store'), [ 'name' => 'Kain Sutra', 'unit' => RawMaterialUnit::METER->value, 'prices' => [priceWithImage()], ])->assertRedirect(route('login')); }); test('user without create permission is forbidden', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); $this->actingAs($user) ->post(route('admin.master.raw_materials.store'), [ 'name' => 'Kain Sutra', 'unit' => RawMaterialUnit::METER->value, 'prices' => [priceWithImage()], ]) ->assertForbidden(); }); test('name is required', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $this->actingAs($user) ->post(route('admin.master.raw_materials.store'), [ 'name' => '', 'unit' => RawMaterialUnit::METER->value, 'prices' => [priceWithImage()], ]) ->assertSessionHasErrors('name'); }); test('name must not exceed 200 characters', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $this->actingAs($user) ->post(route('admin.master.raw_materials.store'), [ 'name' => str_repeat('a', 201), 'unit' => RawMaterialUnit::METER->value, 'prices' => [priceWithImage()], ]) ->assertSessionHasErrors('name'); }); test('unit is required', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $this->actingAs($user) ->post(route('admin.master.raw_materials.store'), [ 'name' => 'Kain Sutra', 'unit' => '', 'prices' => [priceWithImage()], ]) ->assertSessionHasErrors('unit'); }); test('unit must be a valid enum value', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $this->actingAs($user) ->post(route('admin.master.raw_materials.store'), [ 'name' => 'Kain Sutra', 'unit' => 'invalid_unit', 'prices' => [priceWithImage()], ]) ->assertSessionHasErrors('unit'); }); test('prices is required', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $this->actingAs($user) ->post(route('admin.master.raw_materials.store'), [ 'name' => 'Kain Sutra', 'unit' => RawMaterialUnit::METER->value, 'prices' => [], ]) ->assertSessionHasErrors('prices'); }); test('price variant is required', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $this->actingAs($user) ->post(route('admin.master.raw_materials.store'), [ 'name' => 'Kain Sutra', 'unit' => RawMaterialUnit::METER->value, 'prices' => [['variant' => '', 'price' => 50000, 'stock' => 10, 'images' => [UploadedFile::fake()->image('test.jpg')]]], ]) ->assertSessionHasErrors('prices.0.variant'); }); test('price amount is required and must be greater than 0', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $this->actingAs($user) ->post(route('admin.master.raw_materials.store'), [ 'name' => 'Kain Sutra', 'unit' => RawMaterialUnit::METER->value, 'prices' => [priceWithImage('Merah', 0, 10)], ]) ->assertSessionHasErrors('prices.0.price'); }); test('stock is required', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $this->actingAs($user) ->post(route('admin.master.raw_materials.store'), [ 'name' => 'Kain Sutra', 'unit' => RawMaterialUnit::METER->value, 'prices' => [['variant' => 'Merah', 'price' => 50000, 'stock' => '', 'images' => [UploadedFile::fake()->image('test.jpg')]]], ]) ->assertSessionHasErrors('prices.0.stock'); }); test('stock must be non-negative', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $this->actingAs($user) ->post(route('admin.master.raw_materials.store'), [ 'name' => 'Kain Sutra', 'unit' => RawMaterialUnit::METER->value, 'prices' => [priceWithImage('Merah', 50000, -5)], ]) ->assertSessionHasErrors('prices.0.stock'); }); test('price s3_keys is required', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $this->actingAs($user) ->post(route('admin.master.raw_materials.store'), [ 'name' => 'Kain Sutra', 'unit' => RawMaterialUnit::METER->value, 'prices' => [['variant' => 'Merah', 'price' => 50000, 'stock' => 10]], ]) ->assertSessionHasErrors('prices.0.s3_keys'); }); test('creating raw material also creates prices', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $this->actingAs($user) ->post(route('admin.master.raw_materials.store'), [ 'name' => 'Kain Premium', 'unit' => RawMaterialUnit::YARD->value, 'prices' => [ priceWithImage('Merah', 75000, 50), priceWithImage('Biru', 80000, 30), ], ]); $rawMaterial = RawMaterial::where('name', 'Kain Premium')->first(); expect($rawMaterial)->not->toBeNull(); expect($rawMaterial->prices)->toHaveCount(2); }); }); // ─── Edit ───────────────────────────────────────────────── describe('Raw Material Edit', function () { test('authenticated user with permission can view edit form', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE); $rawMaterial = createRawMaterialWithPrices(); $this->actingAs($user) ->get(route('admin.master.raw_materials.edit', $rawMaterial)) ->assertOk(); }); test('guest is redirected to login', function () { $rawMaterial = createRawMaterialWithPrices(); $this->get(route('admin.master.raw_materials.edit', $rawMaterial)) ->assertRedirect(route('login')); }); test('user without update permission is forbidden', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); $rawMaterial = createRawMaterialWithPrices(); $this->actingAs($user) ->get(route('admin.master.raw_materials.edit', $rawMaterial)) ->assertForbidden(); }); }); // ─── Update ─────────────────────────────────────────────── describe('Raw Material Update', function () { test('authenticated user with permission can submit raw material update request', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE); $rawMaterial = createRawMaterialWithPrices(); $price = $rawMaterial->prices->first(); $this->actingAs($user) ->put(route('admin.master.raw_materials.update', $rawMaterial), [ 'name' => 'Nama Diubah', 'unit' => RawMaterialUnit::KILOGRAM->value, 'prices' => [ ['id' => $price->id, 'variant' => 'Putih', 'price' => 90000, 'stock' => 25], ], ]) ->assertRedirect(route('admin.master.raw_materials.index')); $this->assertDatabaseHas('owner_verification_requests', [ 'subject_id' => $rawMaterial->id, 'subject_type' => RawMaterial::class, 'action' => OwnerVerificationAction::UPDATE->value, 'status' => OwnerVerificationStatus::PENDING->value, ]); expect($rawMaterial->fresh()->name)->not->toBe('Nama Diubah'); expect($price->fresh()->variant)->not->toBe('Putih'); }); test('approving update request applies raw material changes', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE); $verifier = createRawMaterialVerifierUser(); $rawMaterial = createRawMaterialWithPrices(); $price = $rawMaterial->prices->first(); $this->actingAs($user) ->put(route('admin.master.raw_materials.update', $rawMaterial), [ 'name' => 'Nama Diubah', 'unit' => RawMaterialUnit::KILOGRAM->value, 'prices' => [ ['id' => $price->id, 'variant' => 'Putih', 'price' => 90000, 'stock' => 25], ], ]); approveLatestOwnerVerificationRequest($verifier); expect($rawMaterial->fresh()->name)->toBe('Nama Diubah'); expect($price->fresh()->variant)->toBe('Putih'); }); test('guest cannot update a raw material', function () { $rawMaterial = createRawMaterialWithPrices(); $price = $rawMaterial->prices->first(); $this->put(route('admin.master.raw_materials.update', $rawMaterial), [ 'name' => 'Nama Baru', 'unit' => $rawMaterial->unit->value, 'prices' => [['id' => $price->id, 'variant' => $price->variant, 'price' => $price->price, 'stock' => $price->stock]], ])->assertRedirect(route('login')); }); test('user without update permission is forbidden', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); $rawMaterial = createRawMaterialWithPrices(); $price = $rawMaterial->prices->first(); $this->actingAs($user) ->put(route('admin.master.raw_materials.update', $rawMaterial), [ 'name' => 'Nama Baru', 'unit' => $rawMaterial->unit->value, 'prices' => [['id' => $price->id, 'variant' => $price->variant, 'price' => $price->price, 'stock' => $price->stock]], ]) ->assertForbidden(); }); test('name is required on update', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE); $rawMaterial = createRawMaterialWithPrices(); $price = $rawMaterial->prices->first(); $this->actingAs($user) ->put(route('admin.master.raw_materials.update', $rawMaterial), [ 'name' => '', 'unit' => $rawMaterial->unit->value, 'prices' => [ ['id' => $price->id, 'variant' => $price->variant, 'price' => $price->price, 'stock' => $price->stock], ], ]) ->assertSessionHasErrors('name'); }); test('prices is required on update', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE); $rawMaterial = createRawMaterialWithPrices(); $this->actingAs($user) ->put(route('admin.master.raw_materials.update', $rawMaterial), [ 'name' => 'Nama Bahan', 'unit' => $rawMaterial->unit->value, 'prices' => [], ]) ->assertSessionHasErrors('prices'); }); test('unsubmitted prices are removed after approving update request', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE); $verifier = createRawMaterialVerifierUser(); $rawMaterial = createRawMaterialWithPrices(); $priceToKeep = $rawMaterial->prices->first(); $this->actingAs($user) ->put(route('admin.master.raw_materials.update', $rawMaterial), [ 'name' => 'Updated Material', 'unit' => $rawMaterial->unit->value, 'prices' => [ ['id' => $priceToKeep->id, 'variant' => 'Kept', 'price' => 60000, 'stock' => 15], ], ]); approveLatestOwnerVerificationRequest($verifier); $rawMaterial->refresh(); expect($rawMaterial->prices)->toHaveCount(1); expect($rawMaterial->prices->first()->variant)->toBe('Kept'); }); }); // ─── Toggle Status ──────────────────────────────────────── describe('Raw Material Toggle Status', function () { test('authenticated user with permission can submit toggle status request', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_TOGGLE_STATUS); $rawMaterial = RawMaterial::factory()->create(); $this->actingAs($user) ->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [ 'is_active' => false, ]) ->assertRedirect(); $this->assertDatabaseHas('owner_verification_requests', [ 'subject_id' => $rawMaterial->id, 'subject_type' => RawMaterial::class, 'action' => OwnerVerificationAction::TOGGLE_STATUS->value, 'status' => OwnerVerificationStatus::PENDING->value, ]); expect($rawMaterial->fresh()->is_active)->toBeTrue(); }); test('raw material index exposes display is active for pending toggle status request', function () { $user = createRawMaterialUserWithPermission( PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_TOGGLE_STATUS, ); $rawMaterial = RawMaterial::factory()->create(['is_active' => true]); $this->actingAs($user) ->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [ 'is_active' => false, ]) ->assertRedirect(); $response = $this->actingAs($user) ->get(route('admin.master.raw_materials.index')); $response->assertOk(); $response->assertInertia(fn ($page) => $page ->where('rawMaterials.data.0.id', $rawMaterial->id) ->where('rawMaterials.data.0.is_active', true) ->where('rawMaterials.data.0.display_is_active', false) ->where('rawMaterials.data.0.has_pending_request', true) ->where('rawMaterials.data.0.pending_request_action', OwnerVerificationAction::TOGGLE_STATUS->value) ); }); test('approving toggle status request updates raw material status', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_TOGGLE_STATUS); $verifier = createRawMaterialVerifierUser(); $rawMaterial = RawMaterial::factory()->create(); $this->actingAs($user) ->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [ 'is_active' => false, ]); approveLatestOwnerVerificationRequest($verifier); expect($rawMaterial->fresh()->is_active)->toBeFalse(); }); test('guest cannot toggle raw material status', function () { $rawMaterial = RawMaterial::factory()->create(); $this->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [ 'is_active' => false, ])->assertRedirect(route('login')); }); test('user without toggle permission is forbidden', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); $rawMaterial = RawMaterial::factory()->create(); $this->actingAs($user) ->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [ 'is_active' => false, ]) ->assertForbidden(); }); test('is_active field is required', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_TOGGLE_STATUS); $rawMaterial = RawMaterial::factory()->create(); $this->actingAs($user) ->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [ 'is_active' => null, ]) ->assertSessionHasErrors('is_active'); }); }); // ─── Destroy ────────────────────────────────────────────── describe('Raw Material Destroy', function () { test('authenticated user with permission can delete raw material', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_DELETE); $rawMaterial = createRawMaterialWithPrices(); $priceIds = $rawMaterial->prices->pluck('id')->toArray(); $this->actingAs($user) ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)) ->assertRedirect(route('admin.master.raw_materials.index')); $this->assertSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); foreach ($priceIds as $priceId) { $this->assertSoftDeleted('raw_material_prices', ['id' => $priceId]); } }); test('guest cannot delete a raw material', function () { $rawMaterial = createRawMaterialWithPrices(); $this->delete(route('admin.master.raw_materials.destroy', $rawMaterial)) ->assertRedirect(route('login')); $this->assertNotSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); }); test('user without delete permission is forbidden', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); $rawMaterial = createRawMaterialWithPrices(); $this->actingAs($user) ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)) ->assertForbidden(); $this->assertNotSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); }); test('cannot delete raw material that is used in an in-progress cutting', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_DELETE); $rawMaterial = createRawMaterialWithPrices(); $price = $rawMaterial->prices->first(); $cutting = Cutting::factory()->create(['status' => CuttingStatus::IN_PROGRESS]); CuttingMaterial::factory()->create([ 'cutting_id' => $cutting->id, 'raw_material_price_id' => $price->id, 'user_id' => null, ]); $this->actingAs($user) ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)) ->assertRedirect(); $this->assertNotSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); }); test('can delete raw material that is used in a completed cutting', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_DELETE); $rawMaterial = createRawMaterialWithPrices(); $price = $rawMaterial->prices->first(); $cutting = Cutting::factory()->create(['status' => CuttingStatus::COMPLETED]); CuttingMaterial::factory()->create([ 'cutting_id' => $cutting->id, 'raw_material_price_id' => $price->id, 'user_id' => null, ]); $this->actingAs($user) ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)) ->assertRedirect(route('admin.master.raw_materials.index')); $this->assertSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); }); }); // ─── Raw Material Model ─────────────────────────────────── describe('Raw Material Model', function () { test('raw material uses soft deletes', function () { $rawMaterial = RawMaterial::factory()->create(); $rawMaterial->delete(); expect($rawMaterial->trashed())->toBeTrue(); }); test('raw material can have prices', function () { $rawMaterial = RawMaterial::factory()->create(); RawMaterialPrice::factory()->count(3)->create(['raw_material_id' => $rawMaterial->id]); expect($rawMaterial->fresh()->prices)->toHaveCount(3); }); test('raw material has is_active cast to boolean', function () { $rawMaterial = RawMaterial::factory()->create(['is_active' => true]); expect($rawMaterial->is_active)->toBeTrue(); $rawMaterial->update(['is_active' => false]); expect($rawMaterial->fresh()->is_active)->toBeFalse(); }); test('raw material has unit cast to enum', function () { $rawMaterial = RawMaterial::factory()->create(['unit' => RawMaterialUnit::YARD->value]); expect($rawMaterial->unit)->toBe(RawMaterialUnit::YARD); }); test('raw material has unit label accessor', function () { $rawMaterial = RawMaterial::factory()->create(['unit' => RawMaterialUnit::METER->value]); expect($rawMaterial->unit_label)->toBe('Meter'); }); test('raw material has unit abbreviation accessor', function () { $rawMaterial = RawMaterial::factory()->create(['unit' => RawMaterialUnit::KILOGRAM->value]); expect($rawMaterial->unit_abbreviation)->toBe('kg'); }); test('raw material has total_stock_formatted accessor', function () { $rawMaterial = RawMaterial::factory()->create(['unit' => RawMaterialUnit::METER->value]); RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 10.5]); RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 5.25]); expect($rawMaterial->fresh(['prices'])->total_stock_formatted)->toContain('15,75'); expect($rawMaterial->fresh(['prices'])->total_stock_formatted)->toContain('m'); }); }); // ─── RawMaterialPrice Model ──────────────────────────────── describe('RawMaterialPrice Model', function () { test('price has price cast to integer', function () { $rawMaterial = RawMaterial::factory()->create(); $price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'price' => 75000]); expect($price->price)->toBeInt(); expect($price->price)->toBe(75000); }); test('price has formatted price accessor', function () { $rawMaterial = RawMaterial::factory()->create(); $price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'price' => 1500000]); expect($price->price_formatted)->toBe('Rp 1.500.000'); }); test('price has price_input accessor', function () { $rawMaterial = RawMaterial::factory()->create(); $price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'price' => 200000]); expect($price->price_input)->toBe('200000'); }); test('price belongs to raw material', function () { $rawMaterial = RawMaterial::factory()->create(); $price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); expect($price->rawMaterial)->not->toBeNull(); expect($price->rawMaterial->id)->toBe($rawMaterial->id); }); test('price uses soft deletes', function () { $rawMaterial = RawMaterial::factory()->create(); $price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $price->delete(); expect($price->trashed())->toBeTrue(); }); }); // ─── Owner Direct Actions ────────────────────────────────── describe('Owner Direct Actions', function () { test('owner can create raw material directly without verification', function () { $owner = createRawMaterialUserWithPermission( PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE, PermissionEnum::OWNER_VERIFICATIONS_VERIFY, ); $this->actingAs($owner) ->post(route('admin.master.raw_materials.store'), [ 'name' => 'Kain Owner', 'unit' => RawMaterialUnit::METER->value, 'prices' => [ priceWithImage('Merah', 75000, 50), ], ]) ->assertRedirect(route('admin.master.raw_materials.index')); $rawMaterial = RawMaterial::where('name', 'Kain Owner')->first(); expect($rawMaterial)->not->toBeNull(); expect($rawMaterial->is_active)->toBeTrue(); $this->assertDatabaseMissing('owner_verification_requests', [ 'subject_id' => $rawMaterial->id, 'subject_type' => RawMaterial::class, ]); }); test('owner can update raw material directly without verification', function () { $owner = createRawMaterialUserWithPermission( PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE, PermissionEnum::OWNER_VERIFICATIONS_VERIFY, ); $rawMaterial = createRawMaterialWithPrices(); $price = $rawMaterial->prices->first(); $this->actingAs($owner) ->put(route('admin.master.raw_materials.update', $rawMaterial), [ 'name' => 'Kain Updated', 'unit' => RawMaterialUnit::KILOGRAM->value, 'prices' => [ ['id' => $price->id, 'variant' => 'Putih', 'price' => 90000, 'stock' => 25], ], ]) ->assertRedirect(route('admin.master.raw_materials.index')); expect($rawMaterial->fresh()->name)->toBe('Kain Updated'); expect($price->fresh()->variant)->toBe('Putih'); }); test('owner can delete raw material directly without verification', function () { $owner = createRawMaterialUserWithPermission( PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_DELETE, PermissionEnum::OWNER_VERIFICATIONS_VERIFY, ); $rawMaterial = createRawMaterialWithPrices(); $this->actingAs($owner) ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)) ->assertRedirect(route('admin.master.raw_materials.index')); $this->assertSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); }); test('owner can toggle raw material status directly without verification', function () { $owner = createRawMaterialUserWithPermission( PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_TOGGLE_STATUS, PermissionEnum::OWNER_VERIFICATIONS_VERIFY, ); $rawMaterial = RawMaterial::factory()->create(); $this->actingAs($owner) ->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [ 'is_active' => false, ]); expect($rawMaterial->fresh()->is_active)->toBeFalse(); }); }); // ─── Reject Update Request ───────────────────────────────── describe('Reject Update Request', function () { test('rejecting update request rolls back to old values', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE); $verifier = createRawMaterialVerifierUser(); $rawMaterial = createRawMaterialWithPrices(); $price = $rawMaterial->prices->first(); $originalName = $rawMaterial->name; $originalVariant = $price->variant; $this->actingAs($user) ->put(route('admin.master.raw_materials.update', $rawMaterial), [ 'name' => 'Nama Diubah', 'unit' => RawMaterialUnit::KILOGRAM->value, 'prices' => [ ['id' => $price->id, 'variant' => 'Putih', 'price' => 90000, 'stock' => 25], ], ]); $verificationRequest = OwnerVerificationRequest::query()->pending()->latest()->firstOrFail(); $this->actingAs($verifier) ->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [ 'reason' => 'Tidak sesuai standar', ]) ->assertRedirect(); expect($rawMaterial->fresh()->name)->toBe($originalName); expect($price->fresh()->variant)->toBe($originalVariant); }); }); // ─── Show JSON ──────────────────────────────────────────── describe('Raw Material Show JSON', function () { test('authenticated user with permission can get raw material details in JSON', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); $rawMaterial = createRawMaterialWithPrices(); $response = $this->actingAs($user) ->get(route('admin.master.raw_materials.show', $rawMaterial)) ->assertOk(); $response->assertJsonStructure([ 'id', 'name', 'unit', 'prices' => [ '*' => [ 'id', 'variant', 'price', 'price_formatted', 'price_input', 'stock_formatted', 'stock_input', 'images', ], ], ]); $data = $response->json(); expect($data['id'])->toBe($rawMaterial->id); expect($data['prices'])->toHaveCount(2); }); test('guest cannot get raw material details in JSON', function () { $rawMaterial = createRawMaterialWithPrices(); $this->get(route('admin.master.raw_materials.show', $rawMaterial)) ->assertRedirect(route('login')); }); test('user without view permission cannot get raw material details in JSON', function () { $user = User::factory()->create(); $rawMaterial = createRawMaterialWithPrices(); $this->actingAs($user) ->get(route('admin.master.raw_materials.show', $rawMaterial)) ->assertForbidden(); }); });