run(); $user->givePermissionTo([ 'cutting.view', 'cutting.create', 'cutting.update', 'cutting.delete', ]); } function makeValidCuttingPayload(array $overrides = []): array { $price = RawMaterialPrice::factory()->create(['stock' => 100]); return array_merge([ 'product_name' => 'Produk Test', 'sample' => 5, 'original_outside_sample' => 10, 'cutting_result' => 15, 'description' => 'Keterangan test', 'materials' => [ [ 'raw_material_price_id' => $price->id, 'material_usage' => 20, 'material_result' => 15, 'combination_index' => null, ], ], 'combinations' => [], 'photo_key' => null, ], $overrides); } /* |-------------------------------------------------------------------------- | AUTHENTICATION |-------------------------------------------------------------------------- */ test('guests are redirected to the login page', function () { $response = $this->get(route('admin.manage.cuttings.index')); $response->assertRedirect(route('login')); }); test('guests are redirected when visiting create page', function () { $response = $this->get(route('admin.manage.cuttings.create')); $response->assertRedirect(route('login')); }); test('guests are redirected when visiting edit page', function () { $cutting = Cutting::factory()->create(); $response = $this->get(route('admin.manage.cuttings.edit', $cutting)); $response->assertRedirect(route('login')); }); test('guest cannot create cutting', function () { $response = $this->post(route('admin.manage.cuttings.store'), makeValidCuttingPayload()); $response->assertRedirect(route('login')); $this->assertDatabaseCount('cuttings', 0); }); test('guest cannot update cutting', function () { $cutting = Cutting::factory()->create(); $response = $this->put(route('admin.manage.cuttings.update', $cutting), makeValidCuttingPayload()); $response->assertRedirect(route('login')); }); test('guest cannot delete cutting', function () { $cutting = Cutting::factory()->create(); $response = $this->delete(route('admin.manage.cuttings.destroy', $cutting)); $response->assertRedirect(route('login')); $this->assertDatabaseHas('cuttings', ['id' => $cutting->id, 'deleted_at' => null]); }); /* |-------------------------------------------------------------------------- | AUTHORIZATION |-------------------------------------------------------------------------- */ test('user without permission cannot view cuttings', function () { $user = User::factory()->create(); $this->actingAs($user); $this->get(route('admin.manage.cuttings.index'))->assertForbidden(); }); test('user without permission cannot visit create page', function () { $user = User::factory()->create(); $this->actingAs($user); $this->get(route('admin.manage.cuttings.create'))->assertForbidden(); }); test('user without permission cannot store cutting', function () { $user = User::factory()->create(); $this->actingAs($user); $this->post(route('admin.manage.cuttings.store'), makeValidCuttingPayload())->assertForbidden(); $this->assertDatabaseCount('cuttings', 0); }); test('user without permission cannot visit edit page', function () { $user = User::factory()->create(); $this->actingAs($user); $cutting = Cutting::factory()->create(); $this->get(route('admin.manage.cuttings.edit', $cutting))->assertForbidden(); }); test('user without permission cannot update cutting', function () { $user = User::factory()->create(); $this->actingAs($user); $cutting = Cutting::factory()->create(); $this->put(route('admin.manage.cuttings.update', $cutting), makeValidCuttingPayload())->assertForbidden(); }); test('user without permission cannot delete cutting', function () { $user = User::factory()->create(); $this->actingAs($user); $cutting = Cutting::factory()->create(); $this->delete(route('admin.manage.cuttings.destroy', $cutting))->assertForbidden(); $this->assertDatabaseHas('cuttings', ['id' => $cutting->id, 'deleted_at' => null]); }); /* |-------------------------------------------------------------------------- | INDEX |-------------------------------------------------------------------------- */ test('user with permission can view cuttings', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); Cutting::factory()->count(3)->create(['created_by_id' => $user->id]); $this->get(route('admin.manage.cuttings.index'))->assertOk(); }); /* |-------------------------------------------------------------------------- | CREATE |-------------------------------------------------------------------------- */ test('user with permission can visit create page', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $this->get(route('admin.manage.cuttings.create'))->assertOk(); }); /* |-------------------------------------------------------------------------- | STORE - VALIDATION |-------------------------------------------------------------------------- */ test('store requires product_name', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $payload = makeValidCuttingPayload(); unset($payload['product_name']); $this->post(route('admin.manage.cuttings.store'), $payload) ->assertSessionHasErrors('product_name'); }); test('store requires sample', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $payload = makeValidCuttingPayload(); unset($payload['sample']); $this->post(route('admin.manage.cuttings.store'), $payload) ->assertSessionHasErrors('sample'); }); test('store requires original_outside_sample', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $payload = makeValidCuttingPayload(); unset($payload['original_outside_sample']); $this->post(route('admin.manage.cuttings.store'), $payload) ->assertSessionHasErrors('original_outside_sample'); }); test('store requires cutting_result', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $payload = makeValidCuttingPayload(); unset($payload['cutting_result']); $this->post(route('admin.manage.cuttings.store'), $payload) ->assertSessionHasErrors('cutting_result'); }); test('store requires at least one material', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $payload = makeValidCuttingPayload(['materials' => []]); $this->post(route('admin.manage.cuttings.store'), $payload) ->assertSessionHasErrors('materials'); }); test('store requires materials.raw_material_price_id', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $payload = makeValidCuttingPayload(); unset($payload['materials'][0]['raw_material_price_id']); $this->post(route('admin.manage.cuttings.store'), $payload) ->assertSessionHasErrors('materials.0.raw_material_price_id'); }); test('store requires materials.material_usage', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $payload = makeValidCuttingPayload(); unset($payload['materials'][0]['material_usage']); $this->post(route('admin.manage.cuttings.store'), $payload) ->assertSessionHasErrors('materials.0.material_usage'); }); test('store requires materials.material_result', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $payload = makeValidCuttingPayload(); unset($payload['materials'][0]['material_result']); $this->post(route('admin.manage.cuttings.store'), $payload) ->assertSessionHasErrors('materials.0.material_result'); }); /* |-------------------------------------------------------------------------- | STORE - SUCCESS |-------------------------------------------------------------------------- */ test('user with permission can store cutting', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.cuttings.store'), makeValidCuttingPayload()) ->assertRedirect(route('admin.manage.cuttings.index')); $this->assertDatabaseHas('cuttings', ['created_by_id' => $user->id]); $this->assertDatabaseHas('cutting_results', ['product_name' => 'Produk Test']); }); test('store creates cutting material', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $price = RawMaterialPrice::factory()->create(['stock' => 100]); $this->post(route('admin.manage.cuttings.store'), makeValidCuttingPayload([ 'materials' => [ [ 'raw_material_price_id' => $price->id, 'material_usage' => 20, 'material_result' => 15, 'combination_index' => null, ], ], ]))->assertRedirect(); $cutting = Cutting::latest()->first(); $this->assertDatabaseHas('cutting_materials', [ 'cutting_id' => $cutting->id, 'raw_material_price_id' => $price->id, 'material_usage' => 20, 'material_result' => 15, ]); }); test('store deducts stock from raw_material_price', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $price = RawMaterialPrice::factory()->create(['stock' => 100]); $this->post(route('admin.manage.cuttings.store'), makeValidCuttingPayload([ 'materials' => [ [ 'raw_material_price_id' => $price->id, 'material_usage' => 30, 'material_result' => 25, 'combination_index' => null, ], ], ]))->assertRedirect(); $price->refresh(); $this->assertEquals(70, $price->stock); }); test('store with combination creates combination record', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $price1 = RawMaterialPrice::factory()->create(['stock' => 100]); $price2 = RawMaterialPrice::factory()->create(['stock' => 50]); $this->post(route('admin.manage.cuttings.store'), makeValidCuttingPayload([ 'materials' => [ [ 'raw_material_price_id' => $price1->id, 'material_usage' => 15, 'material_result' => 12, 'combination_index' => 0, ], [ 'raw_material_price_id' => $price2->id, 'material_usage' => 10, 'material_result' => 8, 'combination_index' => 0, ], ], 'combinations' => [ ['material_result' => 20], ], ]))->assertRedirect(); $cutting = Cutting::latest()->first(); $this->assertDatabaseHas('cutting_material_combinations', [ 'cutting_id' => $cutting->id, 'material_result' => 20, ]); $combination = CuttingMaterialCombination::where('cutting_id', $cutting->id)->first(); $this->assertDatabaseHas('cutting_materials', [ 'cutting_id' => $cutting->id, 'raw_material_price_id' => $price1->id, 'combination_id' => $combination->id, ]); $this->assertDatabaseHas('cutting_materials', [ 'cutting_id' => $cutting->id, 'raw_material_price_id' => $price2->id, 'combination_id' => $combination->id, ]); }); test('store deducts stock for multiple materials', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $price1 = RawMaterialPrice::factory()->create(['stock' => 100]); $price2 = RawMaterialPrice::factory()->create(['stock' => 50]); $this->post(route('admin.manage.cuttings.store'), makeValidCuttingPayload([ 'materials' => [ [ 'raw_material_price_id' => $price1->id, 'material_usage' => 25, 'material_result' => 20, 'combination_index' => null, ], [ 'raw_material_price_id' => $price2->id, 'material_usage' => 15, 'material_result' => 12, 'combination_index' => 0, ], ], 'combinations' => [ ['material_result' => 18], ], ]))->assertRedirect(); $price1->refresh(); $price2->refresh(); $this->assertEquals(75, $price1->stock); $this->assertEquals(35, $price2->stock); }); test('store with photo_key registers media', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.cuttings.store'), makeValidCuttingPayload([ 'photo_key' => 'cuttings/test-photo.jpg', ]))->assertRedirect(); $cutting = Cutting::latest()->first(); $this->assertNotNull($cutting->getFirstMedia('photos')); }); /* |-------------------------------------------------------------------------- | STORE - STOCK VALIDATION |-------------------------------------------------------------------------- */ test('store fails when stock is insufficient', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $price = RawMaterialPrice::factory()->create(['stock' => 10]); $response = $this->post(route('admin.manage.cuttings.store'), makeValidCuttingPayload([ 'materials' => [ [ 'raw_material_price_id' => $price->id, 'material_usage' => 20, 'material_result' => 15, 'combination_index' => null, ], ], ])); $response->assertRedirect(); $this->assertDatabaseCount('cuttings', 0); $price->refresh(); $this->assertEquals(10, $price->stock); }); /* |-------------------------------------------------------------------------- | EDIT |-------------------------------------------------------------------------- */ test('user with permission can visit edit page', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); $this->get(route('admin.manage.cuttings.edit', $cutting))->assertOk(); }); /* |-------------------------------------------------------------------------- | UPDATE - VALIDATION |-------------------------------------------------------------------------- */ test('update requires product_name', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); $payload = makeValidCuttingPayload(); unset($payload['product_name']); $this->put(route('admin.manage.cuttings.update', $cutting), $payload) ->assertSessionHasErrors('product_name'); }); test('update requires sample', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); $payload = makeValidCuttingPayload(); unset($payload['sample']); $this->put(route('admin.manage.cuttings.update', $cutting), $payload) ->assertSessionHasErrors('sample'); }); test('update requires original_outside_sample', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); $payload = makeValidCuttingPayload(); unset($payload['original_outside_sample']); $this->put(route('admin.manage.cuttings.update', $cutting), $payload) ->assertSessionHasErrors('original_outside_sample'); }); test('update requires cutting_result', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); $payload = makeValidCuttingPayload(); unset($payload['cutting_result']); $this->put(route('admin.manage.cuttings.update', $cutting), $payload) ->assertSessionHasErrors('cutting_result'); }); /* |-------------------------------------------------------------------------- | UPDATE - SUCCESS |-------------------------------------------------------------------------- */ test('user with permission can update cutting', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $price = RawMaterialPrice::factory()->create(['stock' => 100]); $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); $this->put(route('admin.manage.cuttings.update', $cutting), makeValidCuttingPayload([ 'product_name' => 'Updated Product', 'materials' => [ [ 'raw_material_price_id' => $price->id, 'material_usage' => 25, 'material_result' => 20, 'combination_index' => null, ], ], ]))->assertRedirect(route('admin.manage.cuttings.index')); $this->assertDatabaseHas('cutting_results', [ 'cutting_id' => $cutting->id, 'product_name' => 'Updated Product', ]); }); test('update adjusts stock correctly', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $price = RawMaterialPrice::factory()->create(['stock' => 100]); // First store with usage 20 $this->post(route('admin.manage.cuttings.store'), makeValidCuttingPayload([ 'materials' => [ [ 'raw_material_price_id' => $price->id, 'material_usage' => 20, 'material_result' => 15, 'combination_index' => null, ], ], ])); $price->refresh(); $this->assertEquals(80, $price->stock); $cutting = Cutting::latest()->first(); // Update: restore old (20), then deduct new (30) -> 80 + 20 - 30 = 70 $this->put(route('admin.manage.cuttings.update', $cutting), makeValidCuttingPayload([ 'materials' => [ [ 'raw_material_price_id' => $price->id, 'material_usage' => 30, 'material_result' => 25, 'combination_index' => null, ], ], ])); $price->refresh(); $this->assertEquals(70, $price->stock); }); test('update fails when stock is insufficient after restore', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $price = RawMaterialPrice::factory()->create(['stock' => 5]); // Store with usage 3 $this->post(route('admin.manage.cuttings.store'), makeValidCuttingPayload([ 'materials' => [ [ 'raw_material_price_id' => $price->id, 'material_usage' => 3, 'material_result' => 2, 'combination_index' => null, ], ], ])); $price->refresh(); $this->assertEquals(2, $price->stock); $cutting = Cutting::latest()->first(); // Update: restore old (3) -> 2 + 3 = 5, then try to deduct 10 -> fail // Stock remains at 5 because restore happens before validation $this->put(route('admin.manage.cuttings.update', $cutting), makeValidCuttingPayload([ 'materials' => [ [ 'raw_material_price_id' => $price->id, 'material_usage' => 10, 'material_result' => 8, 'combination_index' => null, ], ], ]))->assertRedirect(); $price->refresh(); $this->assertEquals(5, $price->stock); }); /* |-------------------------------------------------------------------------- | DESTROY - SUCCESS |-------------------------------------------------------------------------- */ test('user with permission can delete cutting', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); $this->delete(route('admin.manage.cuttings.destroy', $cutting)) ->assertRedirect(route('admin.manage.cuttings.index')); $this->assertSoftDeleted('cuttings', ['id' => $cutting->id]); }); test('delete restores stock from raw_material_prices', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $price = RawMaterialPrice::factory()->create(['stock' => 100]); $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); CuttingMaterial::create([ 'cutting_id' => $cutting->id, 'raw_material_price_id' => $price->id, 'material_usage' => 30, 'material_result' => 25, ]); $this->delete(route('admin.manage.cuttings.destroy', $cutting)) ->assertRedirect(route('admin.manage.cuttings.index')); $price->refresh(); $this->assertEquals(130, $price->stock); }); test('delete restores stock for multiple materials', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $price1 = RawMaterialPrice::factory()->create(['stock' => 50]); $price2 = RawMaterialPrice::factory()->create(['stock' => 30]); $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); CuttingMaterial::create([ 'cutting_id' => $cutting->id, 'raw_material_price_id' => $price1->id, 'material_usage' => 20, 'material_result' => 15, ]); CuttingMaterial::create([ 'cutting_id' => $cutting->id, 'raw_material_price_id' => $price2->id, 'material_usage' => 10, 'material_result' => 8, ]); $this->delete(route('admin.manage.cuttings.destroy', $cutting)) ->assertRedirect(route('admin.manage.cuttings.index')); $price1->refresh(); $price2->refresh(); $this->assertEquals(70, $price1->stock); $this->assertEquals(40, $price2->stock); }); test('delete removes cutting_material_combinations', function () { $user = User::factory()->create(); giveCuttingPermissions($user); $this->actingAs($user); $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); $combination = CuttingMaterialCombination::create([ 'cutting_id' => $cutting->id, 'material_result' => 15, ]); $this->delete(route('admin.manage.cuttings.destroy', $cutting)) ->assertRedirect(route('admin.manage.cuttings.index')); $this->assertSoftDeleted('cutting_material_combinations', ['id' => $combination->id]); });