create([ 'email' => uniqid('ap09_', true).'@test.example', 'password' => bcrypt('password'), ]); $user->forceFill([ 'admin' => $adminLevel, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false, ])->save(); return $user->fresh(); } function ap09EnsureCountry(): Country { return Country::query()->firstOrCreate( ['code' => 'DE'], [ 'phone' => '00', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Germany', 'fr' => 'Germany', 'it' => 'Germany', 'ru' => 'Germany', 'active' => true, ] ); } function ap09MakeIngredient(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 ap09MakeStock(Ingredient $ing, Location $location, Supplier $supplier, User $user, array $overrides = []): StockEntry { return StockEntry::query()->create(array_merge([ 'entry_type' => 'ingredient', 'ingredient_id' => $ing->id, 'packaging_item_id' => null, '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' => 1, 'status' => 'received', 'received_by' => $user->id, 'received_at' => '2026-01-15', 'received_quantity' => 10000, 'batch_number' => 'AP09-B', 'best_before' => '2030-12-31', 'quality_id' => null, ], $overrides)); } test('production rechnet soll-verbrauch aus hersteller-rezeptur', function () { ap09EnsureCountry(); $user = ap09MakeUser(); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $ing = ap09MakeIngredient('AP09-Soll'); $product = Product::query()->create([ 'name' => 'AP09-Soll-Produkt', 'title' => 'AP09-Soll-Produkt', 'active' => true, ]); // Produkt-Rezeptur (darf NICHT verwendet werden) ProductIngredient::query()->create([ 'product_id' => $product->id, 'ingredient_id' => $ing->id, 'pos' => 0, 'gram' => 999, 'factor' => 1.0, 'recipe_type' => 'product', ]); // Hersteller-Rezeptur (maßgeblich): 10 g * 1.2 * 5 Stück = 60 g ProductIngredient::query()->create([ 'product_id' => $product->id, 'ingredient_id' => $ing->id, 'pos' => 0, 'gram' => 10, 'factor' => 1.2, 'recipe_type' => 'manufacturer', ]); $stock = ap09MakeStock($ing, $location, $supplier, $user); $production = app(ProductionService::class)->store( [ 'product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-03-01', 'quantity' => 5, 'notes' => null, ], [ ['ingredient_id' => $ing->id, 'stock_entry_id' => $stock->id, 'quantity_used' => '60'], ], $user->id ); expect((float) $production->productionIngredients->first()->quantity_used)->toBe(60.0); }); test('production blockiert ohne hersteller-rezeptur trotz vorhandener produkt-rezeptur', function () { ap09EnsureCountry(); $user = ap09MakeUser(); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $ing = ap09MakeIngredient('AP09-NoMfg'); $product = Product::query()->create([ 'name' => 'AP09-NoMfg-Produkt', 'title' => 'AP09-NoMfg-Produkt', 'active' => true, ]); ProductIngredient::query()->create([ 'product_id' => $product->id, 'ingredient_id' => $ing->id, 'pos' => 0, 'gram' => 10, 'factor' => 1.0, 'recipe_type' => 'product', ]); $stock = ap09MakeStock($ing, $location, $supplier, $user); expect(fn () => app(ProductionService::class)->store( [ 'product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-03-01', 'quantity' => 1, 'notes' => null, ], [ ['ingredient_id' => $ing->id, 'stock_entry_id' => $stock->id, 'quantity_used' => '10'], ], $user->id ))->toThrow(ValidationException::class); }); test('recipe json meldet fehlende hersteller-rezeptur', function () { ap09EnsureCountry(); $user = ap09MakeUser(1); $location = Location::factory()->create(); $product = Product::query()->create([ 'name' => 'AP09-Recipe-Empty', 'title' => 'AP09-Recipe-Empty', 'active' => true, ]); $this->actingAs($user, 'user'); $response = $this->getJson(route('admin.inventory.api.products.recipe', ['product' => $product->id]).'?location_id='.$location->id.'&quantity=1'); $response->assertSuccessful(); $response->assertJsonPath('has_recipe', false); expect($response->json('ingredients'))->toBe([]); }); test('chargen ohne restbestand erscheinen nicht im rezept', function () { ap09EnsureCountry(); $user = ap09MakeUser(); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $ing = ap09MakeIngredient('AP09-Rest'); $product = Product::query()->create([ 'name' => 'AP09-Rest-Produkt', 'title' => 'AP09-Rest-Produkt', 'active' => true, ]); ProductIngredient::query()->create([ 'product_id' => $product->id, 'ingredient_id' => $ing->id, 'pos' => 0, 'gram' => 100, 'factor' => 1.0, 'recipe_type' => 'manufacturer', ]); // Charge komplett aufgebraucht $emptyStock = ap09MakeStock($ing, $location, $supplier, $user, [ 'batch_number' => 'AP09-LEER', 'received_quantity' => 100, 'best_before' => '2030-01-01', ]); // Charge mit Restbestand $fullStock = ap09MakeStock($ing, $location, $supplier, $user, [ 'batch_number' => 'AP09-VOLL', 'received_quantity' => 100, 'best_before' => '2030-06-01', ]); // 1 Produktion verbraucht die erste Charge vollständig (100 g * 1.0 * 1 Stück) app(ProductionService::class)->store( [ 'product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-03-01', 'quantity' => 1, 'notes' => null, ], [ ['ingredient_id' => $ing->id, 'stock_entry_id' => $emptyStock->id, 'quantity_used' => '100'], ], $user->id ); $payload = app(ProductionService::class)->buildRecipePayload($product->fresh(), $location->id, 1); $entryIds = collect($payload['ingredients'][0]['stock_entries'])->pluck('id')->all(); expect($entryIds)->toContain($fullStock->id); expect($entryIds)->not->toContain($emptyStock->id); }); test('chargen-label zeigt lieferant, charge und datum ohne mhd-text', function () { ap09EnsureCountry(); $user = ap09MakeUser(); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(['name' => 'AP09-Lieferant']); $ing = ap09MakeIngredient('AP09-Label'); $product = Product::query()->create([ 'name' => 'AP09-Label-Produkt', 'title' => 'AP09-Label-Produkt', 'active' => true, ]); ProductIngredient::query()->create([ 'product_id' => $product->id, 'ingredient_id' => $ing->id, 'pos' => 0, 'gram' => 10, 'factor' => 1.0, 'recipe_type' => 'manufacturer', ]); ap09MakeStock($ing, $location, $supplier, $user, [ 'batch_number' => 'CHARGE-123', 'best_before' => '2030-12-31', ]); $payload = app(ProductionService::class)->buildRecipePayload($product->fresh(), $location->id, 1); $label = $payload['ingredients'][0]['stock_entries'][0]['label']; expect($label)->toBe('AP09-Lieferant - CHARGE-123 - 31.12.2030'); expect($label)->not->toContain('MHD'); }); test('produkt ohne rezeptur kann ohne chargen produziert werden', function () { ap09EnsureCountry(); $user = ap09MakeUser(); $location = Location::factory()->create(); $product = Product::query()->create([ 'name' => 'AP09-Broschuere', 'title' => 'AP09-Broschuere', 'active' => true, 'no_recipe_required' => true, ]); $production = app(ProductionService::class)->store( [ 'product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-03-01', 'quantity' => 25, 'notes' => null, ], [], $user->id ); expect($production->productionIngredients)->toHaveCount(0); expect($production->quantity)->toBe(25); }); test('recipe json meldet recipe_required false bei eigenprodukt', function () { ap09EnsureCountry(); $user = ap09MakeUser(1); $location = Location::factory()->create(); $product = Product::query()->create([ 'name' => 'AP09-Etikett', 'title' => 'AP09-Etikett', 'active' => true, 'no_recipe_required' => true, ]); $this->actingAs($user, 'user'); $response = $this->getJson(route('admin.inventory.api.products.recipe', ['product' => $product->id]).'?location_id='.$location->id.'&quantity=10'); $response->assertSuccessful(); $response->assertJsonPath('recipe_required', false); }); test('produktion per http speichert eigenprodukt ohne chargen', function () { ap09EnsureCountry(); $user = ap09MakeUser(); $location = Location::factory()->create(); $product = Product::query()->create([ 'name' => 'AP09-HTTP-Eigen', 'title' => 'AP09-HTTP-Eigen', 'active' => true, 'no_recipe_required' => true, ]); $this->actingAs($user, 'user'); $response = $this->post(route('admin.inventory.productions.store'), [ 'product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '01.03.2026', 'quantity' => 12, ]); $response->assertRedirect(); $this->assertDatabaseHas('productions', [ 'product_id' => $product->id, 'quantity' => 12, ]); }); test('produktentwicklung platzhalter rendert briefing-hinweis', function () { ap09EnsureCountry(); $user = ap09MakeUser(1); $this->actingAs($user, 'user'); $response = $this->get(route('admin.inventory.product-development')); $response->assertSuccessful(); $response->assertSee('Briefing ausstehend'); });