create([ 'email' => uniqid('ap12_', true).'@test.example', 'password' => bcrypt('password'), ]); $user->forceFill([ 'admin' => $adminLevel, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false, ])->save(); return $user->fresh(); } function ap12MakeIngredient(string $name, ?float $minStockAlert = null): Ingredient { return Ingredient::query()->create([ 'name' => $name, 'trans_name' => '', 'inci' => $name, 'trans_inci' => '', 'effect' => '', 'trans_effect' => '', 'active' => true, 'pos' => 0, 'min_stock_alert' => $minStockAlert, ]); } /** * @param array $overrides */ function ap12MakeStock(Ingredient $ing, Location $location, Supplier $supplier, User $user, array $overrides = []): StockEntry { return StockEntry::query()->create(array_merge([ 'entry_type' => 'ingredient', 'ingredient_id' => $ing->id, 'supplier_id' => $supplier->id, 'location_id' => $location->id, 'unit' => 'gram', 'ordered_by' => $user->id, 'ordered_at' => '2026-01-01', 'ordered_quantity' => 10000, 'price_per_kg' => 15, 'status' => 'received', 'received_by' => $user->id, 'received_at' => '2026-01-15', 'received_quantity' => 10000, 'batch_number' => 'AP12-B', 'best_before' => '2030-12-31', ], $overrides)); } test('ausschuss wird je rohstoff summiert', function () { $location = Location::factory()->create(); $ing = ap12MakeIngredient('AP12-Sum'); StockDisposal::query()->create([ 'disposal_type' => 'ingredient', 'ingredient_id' => $ing->id, 'location_id' => $location->id, 'quantity' => 300, 'unit' => 'gram', 'reason' => 'Bruch', 'disposed_at' => '2026-03-01', ]); StockDisposal::query()->create([ 'disposal_type' => 'ingredient', 'ingredient_id' => $ing->id, 'location_id' => $location->id, 'quantity' => 200, 'unit' => 'gram', 'reason' => 'Verfall', 'disposed_at' => '2026-03-02', ]); expect(app(InventoryService::class)->disposedByIngredient([$ing->id])[$ing->id])->toBe(500.0); }); test('restbestand zieht ausschuss ab', function () { $user = ap12MakeUser(); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $ing = ap12MakeIngredient('AP12-Rest'); ap12MakeStock($ing, $location, $supplier, $user, ['received_quantity' => 10000]); $service = app(InventoryService::class); expect($service->remainingByIngredient([$ing->id])[$ing->id])->toBe(10000.0); StockDisposal::query()->create([ 'disposal_type' => 'ingredient', 'ingredient_id' => $ing->id, 'location_id' => $location->id, 'quantity' => 1500, 'unit' => 'gram', 'reason' => 'Bruch', 'disposed_at' => '2026-03-01', ]); expect($service->remainingByIngredient([$ing->id])[$ing->id])->toBe(8500.0) ->and($service->remainingByLocationForIngredient($ing->id)[$location->id])->toBe(8500.0); }); test('restbestand verpackung zieht ausschuss ab', function () { $user = ap12MakeUser(); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $item = PackagingItem::factory()->create(); StockEntry::query()->create([ 'entry_type' => 'packaging', 'packaging_item_id' => $item->id, 'supplier_id' => $supplier->id, 'location_id' => $location->id, 'unit' => 'piece', 'ordered_by' => $user->id, 'ordered_at' => '2026-01-01', 'ordered_quantity' => 500, 'price_total' => 100, 'status' => 'received', 'received_by' => $user->id, 'received_at' => '2026-01-15', 'received_quantity' => 500, ]); $service = app(InventoryService::class); expect($service->remainingByPackagingItem([$item->id])[$item->id])->toBe(500.0); StockDisposal::query()->create([ 'disposal_type' => 'packaging', 'packaging_item_id' => $item->id, 'location_id' => $location->id, 'quantity' => 40, 'unit' => 'piece', 'reason' => 'Bruch', 'disposed_at' => '2026-03-01', ]); expect($service->remainingByPackagingItem([$item->id])[$item->id])->toBe(460.0); }); test('admin bucht ausschuss per http und bestand sinkt', function () { $user = ap12MakeUser(7); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $ing = ap12MakeIngredient('AP12-HTTP'); ap12MakeStock($ing, $location, $supplier, $user, ['received_quantity' => 5000]); $this->actingAs($user, 'user'); $response = $this->post(route('admin.inventory.stock-disposals.store'), [ 'disposal_type' => 'ingredient', 'ingredient_id' => $ing->id, 'location_id' => $location->id, 'quantity' => '750', 'reason' => 'Verfall / MHD überschritten', 'note' => 'Charge verdorben', 'disposed_at' => '2026-03-10', ]); $response->assertRedirect(); $this->assertDatabaseHas('stock_disposals', [ 'ingredient_id' => $ing->id, 'quantity' => 750.00, 'reason' => 'Verfall / MHD überschritten', 'unit' => 'gram', ]); expect(app(InventoryService::class)->remainingByIngredient([$ing->id])[$ing->id])->toBe(4250.0); }); test('grund ist pflicht', function () { $user = ap12MakeUser(7); $location = Location::factory()->create(); $ing = ap12MakeIngredient('AP12-NoReason'); $this->actingAs($user, 'user'); $response = $this->post(route('admin.inventory.stock-disposals.store'), [ 'disposal_type' => 'ingredient', 'ingredient_id' => $ing->id, 'location_id' => $location->id, 'quantity' => '100', 'disposed_at' => '2026-03-10', ]); $response->assertSessionHasErrors('reason'); }); test('nicht-admin darf keinen ausschuss buchen', function () { $user = ap12MakeUser(1); $location = Location::factory()->create(); $ing = ap12MakeIngredient('AP12-NoAdmin'); $this->actingAs($user, 'user'); $response = $this->post(route('admin.inventory.stock-disposals.store'), [ 'disposal_type' => 'ingredient', 'ingredient_id' => $ing->id, 'location_id' => $location->id, 'quantity' => '100', 'reason' => 'Bruch / Beschädigung', 'disposed_at' => '2026-03-10', ]); $response->assertForbidden(); }); test('liste und formular rendern', function () { $user = ap12MakeUser(7); $location = Location::factory()->create(); $ing = ap12MakeIngredient('AP12-Render'); StockDisposal::query()->create([ 'disposal_type' => 'ingredient', 'ingredient_id' => $ing->id, 'location_id' => $location->id, 'quantity' => 50, 'unit' => 'gram', 'reason' => 'Bruch / Beschädigung', 'disposed_at' => '2026-03-01', ]); $this->actingAs($user, 'user'); $this->get(route('admin.inventory.stock-disposals.index'))->assertSuccessful()->assertSee('Ausgang / Ausschuss'); $this->get(route('admin.inventory.stock-disposals.create', ['ingredient_id' => $ing->id])) ->assertSuccessful() ->assertSee('AP12-Render'); }); test('chargen-endpoint liefert eingegangene chargen', function () { $user = ap12MakeUser(7); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $ing = ap12MakeIngredient('AP12-Charges'); ap12MakeStock($ing, $location, $supplier, $user, ['batch_number' => 'CHG-1']); $this->actingAs($user, 'user'); $response = $this->getJson(route('admin.inventory.api.disposals.ingredient-charges', $ing)); $response->assertSuccessful(); $response->assertJsonFragment(['location_id' => $location->id]); });