create([ 'email' => uniqid('del_', true).'@test.example', 'password' => bcrypt('password'), ]); $user->forceFill([ 'admin' => $adminLevel, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false, ])->save(); return $user->fresh(); } function delIngredient(string $name): Ingredient { return Ingredient::query()->create([ 'name' => $name, 'trans_name' => '', 'inci' => $name, 'trans_inci' => '', 'effect' => '', 'trans_effect' => '', 'active' => true, 'pos' => 0, ]); } /** * @param array $overrides */ function delStock(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' => 1000, 'status' => 'received', 'received_by' => $user->id, 'received_at' => '2026-01-15', 'received_quantity' => 1000, 'batch_number' => 'DEL-B1', 'best_before' => '2030-12-31', ], $overrides)); } function delSimpleProduct(string $name): Product { return Product::query()->create([ 'name' => $name, 'title' => $name, 'active' => true, 'no_recipe_required' => true, ]); } // --- Stammdaten (SettingCrudController) --- test('verwendeter steuersatz kann nicht geloescht werden', function () { $this->actingAs(delUser(8), 'user'); $taxRate = TaxRate::factory()->create(); $user = delUser(); $ing = delIngredient('Del-Tax-INCI'); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); delStock($ing, $location, $supplier, $user, ['tax_rate_id' => $taxRate->id]); $this->delete(route('admin.inventory.tax-rates.destroy', $taxRate)); expect(session('alert-error'))->toContain('Löschen nicht möglich'); expect(TaxRate::query()->find($taxRate->id))->not->toBeNull(); }); test('unbenutzter steuersatz wird soft-geloescht', function () { $this->actingAs(delUser(8), 'user'); $taxRate = TaxRate::factory()->create(); $this->delete(route('admin.inventory.tax-rates.destroy', $taxRate)); expect(TaxRate::query()->find($taxRate->id))->toBeNull(); expect(TaxRate::withTrashed()->find($taxRate->id))->not->toBeNull(); }); test('lagerort mit wareneingang kann nicht geloescht werden (kein 500)', function () { $this->actingAs(delUser(8), 'user'); $user = delUser(); $ing = delIngredient('Del-Loc-INCI'); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); delStock($ing, $location, $supplier, $user); // AP-50: belegter Lagerort ohne Ziel-Lagerort bleibt gesperrt (Übernahme-Ziel nötig). $this->delete(route('admin.inventory.locations.destroy', $location)) ->assertRedirect(); expect(session('alert-error'))->toContain('Ziel-Lagerort'); expect(Location::query()->find($location->id))->not->toBeNull(); }); test('lieferant mit wareneingang kann nicht geloescht werden', function () { $this->actingAs(delUser(7), 'user'); $user = delUser(); $ing = delIngredient('Del-Sup-INCI'); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); delStock($ing, $location, $supplier, $user); $this->delete(route('admin.inventory.suppliers.destroy', $supplier)); expect(session('alert-error'))->toContain('Löschen nicht möglich'); expect(Supplier::query()->find($supplier->id))->not->toBeNull(); }); // --- Wareneingänge --- test('gebuchter wareneingang ohne abhaengigkeiten wird geloescht und bestand sinkt', function () { $admin = delUser(7); $this->actingAs($admin, 'user'); $ing = delIngredient('Del-SE-INCI'); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $entry = delStock($ing, $location, $supplier, $admin); expect(app(InventoryService::class)->remainingByIngredient([$ing->id])[$ing->id] ?? 0.0)->toBe(1000.0); $this->delete(route('admin.inventory.stock-entries.destroy', $entry)) ->assertRedirect(route('admin.inventory.stock-entries.index')); expect(StockEntry::query()->find($entry->id))->toBeNull(); expect(StockEntry::withTrashed()->find($entry->id))->not->toBeNull(); expect(app(InventoryService::class)->remainingByIngredient([$ing->id])[$ing->id] ?? 0.0)->toBe(0.0); }); test('wareneingang mit produktions-verbrauch kann nicht geloescht werden', function () { $admin = delUser(7); $this->actingAs($admin, 'user'); $ing = delIngredient('Del-SE-Blocked'); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $entry = delStock($ing, $location, $supplier, $admin); $product = Product::query()->create(['name' => 'Del-Prod', 'title' => 'Del-Prod', 'active' => true]); ProductIngredient::create([ 'product_id' => $product->id, 'ingredient_id' => $ing->id, 'pos' => 0, 'gram' => 10, 'factor' => 1, 'recipe_type' => 'manufacturer', ]); app(ProductionService::class)->store( [ 'product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-07-01', 'quantity' => 10, 'notes' => null, ], [['ingredient_id' => $ing->id, 'stock_entry_id' => $entry->id, 'quantity_used' => 100]], $admin->id, ); $this->delete(route('admin.inventory.stock-entries.destroy', $entry)); expect(session('alert-error'))->toContain('Löschen nicht möglich'); expect(StockEntry::query()->find($entry->id))->not->toBeNull(); }); // --- Ausschuss --- test('geloeschter ausschuss gibt bestand wieder frei', function () { $admin = delUser(7); $this->actingAs($admin, 'user'); $ing = delIngredient('Del-Disposal'); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); delStock($ing, $location, $supplier, $admin); $disposal = StockDisposal::query()->create([ 'disposal_type' => 'ingredient', 'ingredient_id' => $ing->id, 'location_id' => $location->id, 'quantity' => 400, 'unit' => 'gram', 'reason' => 'MHD abgelaufen', 'disposed_at' => '2026-07-01', 'user_id' => $admin->id, ]); expect(app(InventoryService::class)->remainingByIngredient([$ing->id])[$ing->id])->toBe(600.0); $this->delete(route('admin.inventory.stock-disposals.destroy', $disposal)) ->assertRedirect(route('admin.inventory.stock-disposals.index')); expect(StockDisposal::query()->find($disposal->id))->toBeNull(); expect(app(InventoryService::class)->remainingByIngredient([$ing->id])[$ing->id])->toBe(1000.0); }); // --- Produktionen --- test('geloeschte produktion bucht produktbestand zurueck und gibt rohstoffe frei', function () { $admin = delUser(7); $this->actingAs($admin, 'user'); $ing = delIngredient('Del-Prod-INCI'); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $entry = delStock($ing, $location, $supplier, $admin); $product = Product::query()->create(['name' => 'Del-Prod2', 'title' => 'Del-Prod2', 'active' => true]); ProductIngredient::create([ 'product_id' => $product->id, 'ingredient_id' => $ing->id, 'pos' => 0, 'gram' => 10, 'factor' => 1, 'recipe_type' => 'manufacturer', ]); $production = app(ProductionService::class)->store( [ 'product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-07-01', 'quantity' => 10, 'notes' => null, ], [['ingredient_id' => $ing->id, 'stock_entry_id' => $entry->id, 'quantity_used' => 100]], $admin->id, ); expect(app(ProductStockService::class)->currentStock($product->id))->toBe(10); expect(app(InventoryService::class)->remainingByIngredient([$ing->id])[$ing->id])->toBe(900.0); $this->delete(route('admin.inventory.productions.destroy', $production)) ->assertRedirect(route('admin.inventory.productions.index')); // Produktion soft-gelöscht, Bestand zurückgebucht (append-only), Rohstoff frei. expect(Production::query()->find($production->id))->toBeNull(); expect(Production::withTrashed()->find($production->id))->not->toBeNull(); expect(app(ProductStockService::class)->currentStock($product->id))->toBe(0); expect(app(InventoryService::class)->remainingByIngredient([$ing->id])[$ing->id])->toBe(1000.0); expect(ProductStockMovement::query()->where('product_id', $product->id)->count())->toBe(2); }); test('nicht-admin darf produktion nicht loeschen', function () { $admin = delUser(7); $location = Location::factory()->create(); $product = delSimpleProduct('Del-Guard'); $production = app(ProductionService::class)->store( [ 'product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-07-01', 'quantity' => 5, 'notes' => null, ], [], $admin->id, ); $this->actingAs(delUser(1), 'user'); $this->delete(route('admin.inventory.productions.destroy', $production)) ->assertRedirect(route('home')); expect(Production::query()->find($production->id))->not->toBeNull(); }); // --- INCI --- test('inci mit wareneingang kann nicht geloescht werden, sauberer inci schon', function () { $admin = delUser(7); $this->actingAs($admin, 'user'); $used = delIngredient('Del-INCI-used'); $clean = delIngredient('Del-INCI-clean'); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); delStock($used, $location, $supplier, $admin); $this->delete(route('admin_product_ingredient_delete', $used->id)); expect(session('alert-error'))->toContain('Löschen nicht möglich'); expect(Ingredient::query()->find($used->id))->not->toBeNull(); $this->delete(route('admin_product_ingredient_delete', $clean->id)); expect(Ingredient::query()->find($clean->id))->toBeNull(); expect(Ingredient::withTrashed()->find($clean->id))->not->toBeNull(); });