seed(RolePermissionSeeder::class); Storage::fake('public'); }); // ─── Helper ─────────────────────────────────────────────── function createCuttingUserWithPermission(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 createCuttingWithMaterialsAndResults(?User $user = null): Cutting { $user ??= User::factory()->create(); $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); $rawMaterial = RawMaterial::factory()->create(); $price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); CuttingResult::factory()->create([ 'cutting_id' => $cutting->id, 'product_name' => 'Test Product', ]); CuttingResult::factory()->create([ 'cutting_id' => $cutting->id, 'product_name' => 'Second Product', ]); return $cutting; } function setupDraftItems(User $user): array { $rawMaterial = RawMaterial::factory()->create(); $price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); return ['price' => $price]; } function makeResultData(): array { return [ 'product_name' => 'Test Product', 'cutting_result' => 10, 'sample' => 8, 'original_outside_sample' => 2, ]; } // ─── Index ──────────────────────────────────────────────── describe('Cutting Index', function () { test('authenticated user with permission can view cutting index', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW); $this->actingAs($user) ->get(route('admin.manage.cuttings.index')) ->assertOk(); }); test('guest is redirected to login', function () { $this->get(route('admin.manage.cuttings.index')) ->assertRedirect(route('login')); }); test('user without permission is forbidden', function () { $user = User::factory()->create(); $this->actingAs($user) ->get(route('admin.manage.cuttings.index')) ->assertForbidden(); }); test('index displays cuttings', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW); createCuttingWithMaterialsAndResults($user); createCuttingWithMaterialsAndResults($user); $this->actingAs($user) ->get(route('admin.manage.cuttings.index')) ->assertOk(); }); test('index can search cuttings by description', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW); Cutting::factory()->create([ 'created_by_id' => $user->id, 'description' => 'Cutting kain batik', ]); $this->actingAs($user) ->get(route('admin.manage.cuttings.index', ['search' => 'batik'])) ->assertOk(); }); }); // ─── Create ─────────────────────────────────────────────── describe('Cutting Create', function () { test('authenticated user with permission can view create form', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $this->actingAs($user) ->get(route('admin.manage.cuttings.create')) ->assertOk(); }); test('guest is redirected to login', function () { $this->get(route('admin.manage.cuttings.create')) ->assertRedirect(route('login')); }); test('user without create permission can still view create form', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW); $this->actingAs($user) ->get(route('admin.manage.cuttings.create')) ->assertOk(); }); }); // ─── Store ──────────────────────────────────────────────── describe('Cutting Store', function () { test('authenticated user with permission can create a cutting', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $draft = setupDraftItems($user); // Create draft material $this->actingAs($user) ->postJson(route('admin.manage.cuttings.draft_materials.store'), [ 'raw_material_price_id' => $draft['price']->id, 'material_usage' => 5, ]) ->assertOk(); // Store cutting with results directly $this->actingAs($user) ->post(route('admin.manage.cuttings.store'), [ 'description' => 'Cutting test', 'sewing_cost' => 50000, 'other_cost' => 10000, 'results' => [makeResultData()], ]) ->assertRedirect(route('admin.manage.cuttings.index')); $cutting = Cutting::where('description', 'Cutting test')->first(); expect($cutting)->not->toBeNull(); expect($cutting->status)->toBe(CuttingStatus::IN_PROGRESS); expect($cutting->materials)->toHaveCount(1); expect($cutting->results)->toHaveCount(1); }); test('draft material can have material_result', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $draft = setupDraftItems($user); $this->actingAs($user) ->postJson(route('admin.manage.cuttings.draft_materials.store'), [ 'raw_material_price_id' => $draft['price']->id, 'material_usage' => 5, 'material_result' => 10, ]) ->assertOk() ->assertJsonPath('item.material_result', 10); $this->assertDatabaseHas('cutting_materials', [ 'raw_material_price_id' => $draft['price']->id, 'material_result' => 10, ]); }); test('material_result is preserved when creating cutting from drafts', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $draft = setupDraftItems($user); // Create draft material with material_result $this->actingAs($user) ->postJson(route('admin.manage.cuttings.draft_materials.store'), [ 'raw_material_price_id' => $draft['price']->id, 'material_usage' => 5, 'material_result' => 10, ]) ->assertOk(); // Store cutting with results directly $this->actingAs($user) ->post(route('admin.manage.cuttings.store'), [ 'description' => 'Cutting with result', 'results' => [makeResultData()], ]) ->assertRedirect(route('admin.manage.cuttings.index')); $cutting = Cutting::where('description', 'Cutting with result')->first(); expect($cutting->materials->first()->material_result)->toBe(10); }); test('guest cannot create a cutting', function () { $this->post(route('admin.manage.cuttings.store'), [ 'description' => 'Test', ])->assertRedirect(route('login')); }); test('user without create permission is forbidden', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW); $this->actingAs($user) ->post(route('admin.manage.cuttings.store'), [ 'description' => 'Test', ]) ->assertForbidden(); }); test('store fails without draft materials', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); // Send results but no draft material $this->actingAs($user) ->post(route('admin.manage.cuttings.store'), [ 'description' => 'Test', 'results' => [makeResultData()], ]) ->assertSessionHasErrors('materials'); }); test('store fails without results', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $draft = setupDraftItems($user); // Create draft material but don't send results $this->actingAs($user) ->postJson(route('admin.manage.cuttings.draft_materials.store'), [ 'raw_material_price_id' => $draft['price']->id, 'material_usage' => 5, ]) ->assertOk(); $this->actingAs($user) ->post(route('admin.manage.cuttings.store'), [ 'description' => 'Test', ]) ->assertSessionHasErrors('results'); }); }); // ─── Edit ───────────────────────────────────────────────── describe('Cutting Edit', function () { test('authenticated user with permission can view edit form', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE); $cutting = createCuttingWithMaterialsAndResults($user); $this->actingAs($user) ->get(route('admin.manage.cuttings.edit', $cutting)) ->assertOk(); }); test('guest is redirected to login', function () { $cutting = createCuttingWithMaterialsAndResults(); $this->get(route('admin.manage.cuttings.edit', $cutting)) ->assertRedirect(route('login')); }); test('user without update permission can still view edit form', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW); $cutting = createCuttingWithMaterialsAndResults($user); $this->actingAs($user) ->get(route('admin.manage.cuttings.edit', $cutting)) ->assertOk(); }); test('completed cutting cannot be edited', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE); $cutting = Cutting::factory()->completed()->create(['created_by_id' => $user->id]); $this->actingAs($user) ->get(route('admin.manage.cuttings.edit', $cutting)) ->assertRedirect(route('admin.manage.cuttings.index')); }); }); // ─── Update ─────────────────────────────────────────────── describe('Cutting Update', function () { test('authenticated user with permission can update a cutting', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE); $cutting = createCuttingWithMaterialsAndResults($user); $rawMaterial = RawMaterial::factory()->create(); $price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $this->actingAs($user) ->put(route('admin.manage.cuttings.update', $cutting), [ 'description' => 'Updated description', 'sewing_cost' => 75000, 'other_cost' => 15000, 'materials' => [ ['raw_material_price_id' => $price->id, 'material_usage' => 10], ], 'results' => [ [ 'product_name' => 'Test Product', 'cutting_result' => 20, 'sample' => 15, 'original_outside_sample' => 5, ], ], ]) ->assertRedirect(route('admin.manage.cuttings.index')); expect($cutting->fresh()->description)->toBe('Updated description'); }); test('update can save material_result for single materials', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE); $cutting = createCuttingWithMaterialsAndResults($user); $rawMaterial = RawMaterial::factory()->create(); $price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $this->actingAs($user) ->put(route('admin.manage.cuttings.update', $cutting), [ 'description' => 'With material result', 'sewing_cost' => 0, 'other_cost' => 0, 'materials' => [ ['raw_material_price_id' => $price->id, 'material_usage' => 10, 'material_result' => 25], ], 'results' => [ [ 'product_name' => 'Test Product', 'cutting_result' => 20, 'sample' => 15, 'original_outside_sample' => 5, ], ], ]) ->assertRedirect(route('admin.manage.cuttings.index')); $cutting->refresh(); expect($cutting->materials->first()->material_result)->toBe(25); }); test('guest cannot update a cutting', function () { $cutting = createCuttingWithMaterialsAndResults(); $this->put(route('admin.manage.cuttings.update', $cutting), [ 'description' => 'Test', 'materials' => [['raw_material_price_id' => 1, 'material_usage' => 1]], 'results' => [['product_name' => 'P', 'cutting_result' => 1, 'sample' => 1, 'original_outside_sample' => 0]], ])->assertRedirect(route('login')); }); test('user without update permission is forbidden', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW); $cutting = createCuttingWithMaterialsAndResults($user); $this->actingAs($user) ->put(route('admin.manage.cuttings.update', $cutting), [ 'description' => 'Test', 'materials' => [['raw_material_price_id' => 1, 'material_usage' => 1]], 'results' => [['product_name' => 'P', 'cutting_result' => 1, 'sample' => 1, 'original_outside_sample' => 0]], ]) ->assertForbidden(); }); }); // ─── Destroy ────────────────────────────────────────────── describe('Cutting Destroy', function () { test('authenticated user with permission can delete a cutting', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_DELETE); $cutting = createCuttingWithMaterialsAndResults($user); $this->actingAs($user) ->delete(route('admin.manage.cuttings.destroy', $cutting)) ->assertRedirect(route('admin.manage.cuttings.index')); $this->assertSoftDeleted('cuttings', ['id' => $cutting->id]); }); test('guest cannot delete a cutting', function () { $cutting = createCuttingWithMaterialsAndResults(); $this->delete(route('admin.manage.cuttings.destroy', $cutting)) ->assertRedirect(route('login')); $this->assertNotSoftDeleted('cuttings', ['id' => $cutting->id]); }); test('user without delete permission can still delete cutting', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW); $cutting = createCuttingWithMaterialsAndResults($user); $this->actingAs($user) ->delete(route('admin.manage.cuttings.destroy', $cutting)) ->assertRedirect(route('admin.manage.cuttings.index')); $this->assertSoftDeleted('cuttings', ['id' => $cutting->id]); }); test('completed cutting cannot be deleted', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_DELETE); $cutting = Cutting::factory()->completed()->create(['created_by_id' => $user->id]); $this->actingAs($user) ->delete(route('admin.manage.cuttings.destroy', $cutting)) ->assertSessionHasErrors('status'); $this->assertNotSoftDeleted('cuttings', ['id' => $cutting->id]); }); }); // ─── Status Transition ──────────────────────────────────── describe('Cutting Status Transition', function () { test('authenticated user can complete a cutting', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_COMPLETE); $cutting = createCuttingWithMaterialsAndResults($user); $this->actingAs($user) ->post(route('admin.manage.cuttings.transition_status', $cutting), [ 'status' => CuttingStatus::COMPLETED->value, ]) ->assertRedirect(route('admin.manage.cuttings.index')); expect($cutting->fresh()->status)->toBe(CuttingStatus::COMPLETED); }); test('guest cannot transition status', function () { $cutting = createCuttingWithMaterialsAndResults(); $this->post(route('admin.manage.cuttings.transition_status', $cutting), [ 'status' => CuttingStatus::COMPLETED->value, ])->assertRedirect(route('login')); }); test('user without permission cannot complete cutting', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW); $cutting = createCuttingWithMaterialsAndResults($user); $this->actingAs($user) ->post(route('admin.manage.cuttings.transition_status', $cutting), [ 'status' => CuttingStatus::COMPLETED->value, ]) ->assertForbidden(); }); test('completed cutting cannot transition to another status', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_COMPLETE, PermissionEnum::CUTTINGS_UPDATE); $cutting = createCuttingWithMaterialsAndResults($user); $this->actingAs($user) ->post(route('admin.manage.cuttings.transition_status', $cutting), [ 'status' => CuttingStatus::COMPLETED->value, ]); expect($cutting->fresh()->status)->toBe(CuttingStatus::COMPLETED); // COMPLETED is final, cannot transition to another status $this->actingAs($user) ->post(route('admin.manage.cuttings.transition_status', $cutting), [ 'status' => CuttingStatus::IN_PROGRESS->value, ]) ->assertSessionHasErrors('status'); }); }); // ─── Draft Combination ────────────────────────────────────── describe('Draft Combination', function () { test('authenticated user with permission can create draft combination', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $rawMaterial = RawMaterial::factory()->create(); $price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $this->actingAs($user) ->postJson(route('admin.manage.cuttings.draft_combinations.store'), [ 'materials' => [ ['raw_material_price_id' => $price1->id, 'material_usage' => 3], ['raw_material_price_id' => $price2->id, 'material_usage' => 2], ], ]) ->assertOk() ->assertJsonStructure(['items' => [['raw_material_price_id', 'combination_id']]]); $this->assertDatabaseCount('cutting_material_combinations', 1); $this->assertDatabaseCount('cutting_materials', 2); }); test('draft combination can have material_result', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $rawMaterial = RawMaterial::factory()->create(); $price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $this->actingAs($user) ->postJson(route('admin.manage.cuttings.draft_combinations.store'), [ 'materials' => [ ['raw_material_price_id' => $price1->id, 'material_usage' => 3], ['raw_material_price_id' => $price2->id, 'material_usage' => 2], ], 'material_result' => 15, ]) ->assertOk(); $combination = CuttingMaterialCombination::first(); expect($combination->material_result)->toBe(15); }); test('draft combination requires at least 2 materials', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $rawMaterial = RawMaterial::factory()->create(); $price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $this->actingAs($user) ->postJson(route('admin.manage.cuttings.draft_combinations.store'), [ 'materials' => [ ['raw_material_price_id' => $price->id, 'material_usage' => 3], ], ]) ->assertUnprocessable(); }); test('authenticated user can delete draft combination', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $rawMaterial = RawMaterial::factory()->create(); $price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $this->actingAs($user) ->postJson(route('admin.manage.cuttings.draft_combinations.store'), [ 'materials' => [ ['raw_material_price_id' => $price1->id, 'material_usage' => 3], ['raw_material_price_id' => $price2->id, 'material_usage' => 2], ], ]) ->assertOk(); $combination = CuttingMaterialCombination::first(); $this->actingAs($user) ->deleteJson(route('admin.manage.cuttings.draft_combinations.destroy', $combination->id)) ->assertOk(); $this->assertDatabaseCount('cutting_material_combinations', 0); $this->assertDatabaseCount('cutting_materials', 0); }); test('user without permission cannot create draft combination', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW); $rawMaterial = RawMaterial::factory()->create(); $price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $this->actingAs($user) ->postJson(route('admin.manage.cuttings.draft_combinations.store'), [ 'materials' => [ ['raw_material_price_id' => $price1->id, 'material_usage' => 3], ['raw_material_price_id' => $price2->id, 'material_usage' => 2], ], ]) ->assertForbidden(); }); }); // ─── Quick Create Product ───────────────────────────────── describe('Quick Create Product', function () { test('authenticated user with permission can quick create product without prices', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $this->actingAs($user) ->postJson('/admin/manage/cuttings/quick-create-product', [ 'name' => 'Test Product', 'description' => 'Test Description', 'variants' => [ [ 'name' => 'Varian 1', 'stock' => 10, 'retail_stock' => 5, ], ], ]) ->assertOk() ->assertJsonStructure(['product' => ['id', 'name']]); $this->assertDatabaseHas('products', ['name' => 'Test Product']); $this->assertDatabaseHas('product_variants', ['name' => 'Varian 1', 'stock' => 10]); }); test('quick create product without images is allowed', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $this->actingAs($user) ->postJson('/admin/manage/cuttings/quick-create-product', [ 'name' => 'Product No Image', 'variants' => [ [ 'name' => 'Varian 1', 'stock' => 5, 'retail_stock' => 2, ], ], ]) ->assertOk(); $this->assertDatabaseHas('products', ['name' => 'Product No Image']); }); test('quick create product requires name', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $this->actingAs($user) ->postJson('/admin/manage/cuttings/quick-create-product', [ 'variants' => [ [ 'name' => 'Varian 1', 'stock' => 5, 'retail_stock' => 2, ], ], ]) ->assertUnprocessable(); }); test('quick create product requires at least one variant', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $this->actingAs($user) ->postJson('/admin/manage/cuttings/quick-create-product', [ 'name' => 'Test Product', 'variants' => [], ]) ->assertUnprocessable(); }); test('quick create product with categories', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $category = Category::factory()->create(); $this->actingAs($user) ->postJson('/admin/manage/cuttings/quick-create-product', [ 'name' => 'Product With Category', 'category_ids' => [$category->id], 'variants' => [ [ 'name' => 'Varian 1', 'stock' => 5, 'retail_stock' => 2, ], ], ]) ->assertOk(); $product = Product::where('name', 'Product With Category')->first(); expect($product)->not->toBeNull(); expect($product->categories)->toHaveCount(1); }); test('user without permission cannot quick create product', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW); $this->actingAs($user) ->postJson('/admin/manage/cuttings/quick-create-product', [ 'name' => 'Test Product', 'variants' => [ [ 'name' => 'Varian 1', 'stock' => 5, 'retail_stock' => 2, ], ], ]) ->assertForbidden(); }); }); // ─── Store with Combination ───────────────────────────────── describe('Cutting Store with Combination', function () { test('cutting can be created with combination materials', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $rawMaterial = RawMaterial::factory()->create(); $price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); // Create draft combination $this->actingAs($user) ->postJson(route('admin.manage.cuttings.draft_combinations.store'), [ 'materials' => [ ['raw_material_price_id' => $price1->id, 'material_usage' => 3], ['raw_material_price_id' => $price2->id, 'material_usage' => 2], ], ]) ->assertOk(); // Store cutting with results directly $this->actingAs($user) ->post(route('admin.manage.cuttings.store'), [ 'description' => 'Cutting with combination', 'results' => [makeResultData()], ]) ->assertRedirect(route('admin.manage.cuttings.index')); $cutting = Cutting::where('description', 'Cutting with combination')->first(); expect($cutting)->not->toBeNull(); expect($cutting->materials)->toHaveCount(2); expect($cutting->materials->first()->combination_id)->not->toBeNull(); expect($cutting->materials->first()->combination_id)->toBe($cutting->materials->last()->combination_id); }); }); // ─── Update with Combination ──────────────────────────────── describe('Cutting Update with Combination', function () { test('cutting can be updated with combination materials', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE); $cutting = createCuttingWithMaterialsAndResults($user); $rawMaterial = RawMaterial::factory()->create(); $price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $this->actingAs($user) ->put(route('admin.manage.cuttings.update', $cutting), [ 'description' => 'Updated with combination', 'sewing_cost' => 0, 'other_cost' => 0, 'materials' => [ ['raw_material_price_id' => $price1->id, 'material_usage' => 3, 'combination_id' => 999], ['raw_material_price_id' => $price2->id, 'material_usage' => 2, 'combination_id' => 999], ], 'results' => [ [ 'product_name' => 'Test Product', 'cutting_result' => 10, 'sample' => 8, 'original_outside_sample' => 2, ], ], ]) ->assertRedirect(route('admin.manage.cuttings.index')); $cutting->refresh(); expect($cutting->description)->toBe('Updated with combination'); expect($cutting->materials)->toHaveCount(2); // Both materials should have the same combination_id expect($cutting->materials->first()->combination_id)->not->toBeNull(); expect($cutting->materials->first()->combination_id)->toBe($cutting->materials->last()->combination_id); }); test('update can save combination_material_result', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE); $cutting = createCuttingWithMaterialsAndResults($user); $rawMaterial = RawMaterial::factory()->create(); $price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $this->actingAs($user) ->put(route('admin.manage.cuttings.update', $cutting), [ 'description' => 'Combination with result', 'sewing_cost' => 0, 'other_cost' => 0, 'materials' => [ ['raw_material_price_id' => $price1->id, 'material_usage' => 3, 'combination_id' => 999, 'combination_material_result' => 20], ['raw_material_price_id' => $price2->id, 'material_usage' => 2, 'combination_id' => 999, 'combination_material_result' => 20], ], 'results' => [ [ 'product_name' => 'Test Product', 'cutting_result' => 10, 'sample' => 8, 'original_outside_sample' => 2, ], ], ]) ->assertRedirect(route('admin.manage.cuttings.index')); $cutting->refresh(); $combination = $cutting->combinations()->first(); expect($combination)->not->toBeNull(); expect($combination->material_result)->toBe(20); }); }); // ─── Cutting Model ──────────────────────────────────────── describe('Cutting Model', function () { test('cutting uses soft deletes', function () { $cutting = Cutting::factory()->create(); $cutting->delete(); expect($cutting->trashed())->toBeTrue(); }); test('cutting has status cast to enum', function () { $cutting = Cutting::factory()->create(['status' => CuttingStatus::IN_PROGRESS->value]); expect($cutting->status)->toBe(CuttingStatus::IN_PROGRESS); }); test('cutting has status label accessor', function () { $cutting = Cutting::factory()->create(['status' => CuttingStatus::IN_PROGRESS->value]); expect($cutting->status_label)->toBe('Proses'); }); test('cutting can have materials', function () { $cutting = Cutting::factory()->create(); CuttingMaterial::factory()->count(3)->create(['cutting_id' => $cutting->id]); expect($cutting->fresh()->materials)->toHaveCount(3); }); test('cutting can have results', function () { $cutting = Cutting::factory()->create(); CuttingResult::factory()->count(2)->create(['cutting_id' => $cutting->id]); expect($cutting->fresh()->results)->toHaveCount(2); }); test('cutting belongs to created by user', function () { $user = User::factory()->create(); $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); expect($cutting->createdBy)->not->toBeNull(); expect($cutting->createdBy->id)->toBe($user->id); }); test('cutting status is editable for in_progress', function () { $inProgress = Cutting::factory()->create(['status' => CuttingStatus::IN_PROGRESS->value]); expect($inProgress->status->isEditable())->toBeTrue(); }); }); // ─── CuttingMaterial Model ───────────────────────────────── describe('CuttingMaterial Model', function () { test('cutting material has material_usage cast to decimal', function () { $cutting = Cutting::factory()->create(); $material = CuttingMaterial::factory()->create([ 'cutting_id' => $cutting->id, 'material_usage' => 10.5, ]); expect((float) $material->material_usage)->toBe(10.5); }); test('cutting material belongs to cutting', function () { $cutting = Cutting::factory()->create(); $material = CuttingMaterial::factory()->create(['cutting_id' => $cutting->id]); expect($material->cutting)->not->toBeNull(); expect($material->cutting->id)->toBe($cutting->id); }); test('cutting material uses soft deletes', function () { $cutting = Cutting::factory()->create(); $material = CuttingMaterial::factory()->create(['cutting_id' => $cutting->id]); $material->delete(); expect($material->trashed())->toBeTrue(); }); test('cutting material has material_result cast to integer', function () { $cutting = Cutting::factory()->create(); $material = CuttingMaterial::factory()->create([ 'cutting_id' => $cutting->id, 'material_result' => 10, ]); expect($material->material_result)->toBeInt(); expect($material->material_result)->toBe(10); }); }); // ─── CuttingResult Model ─────────────────────────────────── describe('CuttingResult Model', function () { test('cutting result has integer casts', function () { $cutting = Cutting::factory()->create(); $result = CuttingResult::factory()->create([ 'cutting_id' => $cutting->id, 'cutting_result' => 50, 'sample' => 40, 'original_outside_sample' => 10, ]); expect($result->cutting_result)->toBeInt(); expect($result->sample)->toBeInt(); expect($result->original_outside_sample)->toBeInt(); }); test('cutting result belongs to cutting', function () { $cutting = Cutting::factory()->create(); $result = CuttingResult::factory()->create(['cutting_id' => $cutting->id]); expect($result->cutting)->not->toBeNull(); expect($result->cutting->id)->toBe($cutting->id); }); test('cutting result uses soft deletes', function () { $cutting = Cutting::factory()->create(); $result = CuttingResult::factory()->create(['cutting_id' => $cutting->id]); $result->delete(); expect($result->trashed())->toBeTrue(); }); }); // ─── CuttingMaterialCombination Model ──────────────────────── describe('CuttingMaterialCombination Model', function () { test('combination uses soft deletes', function () { $combination = CuttingMaterialCombination::factory()->create(); $combination->delete(); expect($combination->trashed())->toBeTrue(); }); test('combination belongs to cutting', function () { $cutting = Cutting::factory()->create(); $combination = CuttingMaterialCombination::factory()->create([ 'cutting_id' => $cutting->id, ]); expect($combination->cutting)->not->toBeNull(); expect($combination->cutting->id)->toBe($cutting->id); }); test('combination has many materials', function () { $combination = CuttingMaterialCombination::factory()->create(); $rawMaterial = RawMaterial::factory()->create(); $price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); $price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); CuttingMaterial::factory()->create([ 'combination_id' => $combination->id, 'raw_material_price_id' => $price1->id, ]); CuttingMaterial::factory()->create([ 'combination_id' => $combination->id, 'raw_material_price_id' => $price2->id, ]); expect($combination->materials)->toHaveCount(2); }); test('combination can be draft (cutting_id null)', function () { $user = User::factory()->create(); $combination = CuttingMaterialCombination::factory()->create([ 'user_id' => $user->id, 'cutting_id' => null, ]); expect($combination->cutting_id)->toBeNull(); expect($combination->user_id)->toBe($user->id); }); test('combination has material_result cast to integer', function () { $combination = CuttingMaterialCombination::factory()->create([ 'material_result' => 15, ]); expect($combination->material_result)->toBeInt(); expect($combination->material_result)->toBe(15); }); }); // ─── Raw Material Catalog Stock Filter ─────────────────────── describe('Raw Material Catalog Stock Filter', function () { test('rawMaterialCatalog excludes variants with zero stock', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); $rawMaterial = RawMaterial::factory()->create(['is_active' => true]); $priceWithStock = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 10]); $priceWithoutStock = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 0]); $this->actingAs($user) ->get(route('admin.manage.cuttings.create')) ->assertOk(); $service = app(CuttingService::class); $catalog = $service->rawMaterialCatalog(user: $user); $material = $catalog->firstWhere('id', $rawMaterial->id); $priceIds = $material->prices->pluck('id')->all(); expect($priceIds)->toContain($priceWithStock->id); expect($priceIds)->not->toContain($priceWithoutStock->id); }); test('rawMaterialCatalog includes zero-stock variants already used in cutting', function () { $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE); $rawMaterial = RawMaterial::factory()->create(['is_active' => true]); $priceZeroStock = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 0]); $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); CuttingMaterial::factory()->create([ 'cutting_id' => $cutting->id, 'raw_material_price_id' => $priceZeroStock->id, ]); $service = app(CuttingService::class); $catalog = $service->rawMaterialCatalog($cutting); $material = $catalog->firstWhere('id', $rawMaterial->id); $priceIds = $material?->prices->pluck('id')->all() ?? []; expect($priceIds)->toContain($priceZeroStock->id); }); });