firstOrCreate( ['code' => 'DE'], ['phone' => '00', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Germany', 'fr' => 'Germany', 'it' => 'Germany', 'ru' => 'Germany', 'active' => true] ); } function draftUser(int $adminLevel = 7): User { draftCountry(); $user = User::query()->create(['email' => uniqid('draft_', true).'@test.example', 'password' => bcrypt('password')]); $user->forceFill(['admin' => $adminLevel, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false])->save(); return $user->fresh(); } function draftRecipeProduct(Ingredient $ing): Product { $product = Product::query()->create(['name' => 'Draft-Produkt', 'title' => 'Draft-Produkt', 'active' => true]); // Hersteller-Rezeptur: 10 g * 1.0 * 1 Stück = 10 g Soll ProductIngredient::query()->create([ 'product_id' => $product->id, 'ingredient_id' => $ing->id, 'pos' => 0, 'gram' => 10, 'factor' => 1.0, 'recipe_type' => 'manufacturer', ]); return $product; } function draftIngredient(): Ingredient { return Ingredient::query()->create([ 'name' => 'Draft-Roh', 'trans_name' => '', 'inci' => 'DR', 'trans_inci' => '', 'effect' => '', 'trans_effect' => '', 'active' => true, 'pos' => 0, ]); } function draftStock(Ingredient $ing, Location $location, Supplier $supplier, User $user): StockEntry { return StockEntry::query()->create([ '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' => 1000, 'price_per_kg' => 1, 'status' => 'received', 'received_by' => $user->id, 'received_at' => '2026-01-15', 'received_quantity' => 1000, 'batch_number' => 'DRAFT-B', 'best_before' => '2030-12-31', 'quality_id' => null, ]); } test('AP-43: entwurf bucht keinen produktbestand und bleibt status draft', function () { $user = draftUser(); $location = Location::factory()->create(); $product = Product::query()->create(['name' => 'Draft-Simpel', 'title' => 'Draft-Simpel', 'active' => true, 'no_recipe_required' => true]); $production = app(ProductionService::class)->store( ['product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 20, 'notes' => 'WIP'], [], $user->id, [['product_id' => $product->id, 'quantity' => 20, 'batch_number' => null]], true // draft ); expect($production->isDraft())->toBeTrue() ->and(app(ProductStockService::class)->currentStock($product->id))->toBe(0); }); test('AP-43: entwurf reduziert den rohstoffbestand nicht', function () { $user = draftUser(); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $ing = draftIngredient(); $product = draftRecipeProduct($ing); $stock = draftStock($ing, $location, $supplier, $user); $before = app(InventoryService::class)->remainingByIngredient([$ing->id])[$ing->id] ?? 0.0; app(ProductionService::class)->store( ['product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 1, 'notes' => null], [['ingredient_id' => $ing->id, 'stock_entry_id' => $stock->id, 'quantity_used' => '10']], $user->id, [['product_id' => $product->id, 'quantity' => 1, 'batch_number' => null]], true // draft ); $after = app(InventoryService::class)->remainingByIngredient([$ing->id])[$ing->id] ?? 0.0; expect((float) $after)->toBe((float) $before); // Entwurf verbraucht nichts }); test('AP-43: entwurf abschließen bucht bestand und wird completed', function () { $user = draftUser(); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $ing = draftIngredient(); $product = draftRecipeProduct($ing); $stock = draftStock($ing, $location, $supplier, $user); $service = app(ProductionService::class); $draft = $service->store( ['product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 1, 'notes' => null], [['ingredient_id' => $ing->id, 'stock_entry_id' => $stock->id, 'quantity_used' => '10']], $user->id, [['product_id' => $product->id, 'quantity' => 1, 'batch_number' => null]], true ); expect(app(ProductStockService::class)->currentStock($product->id))->toBe(0); $completed = $service->updateProduction( $draft->fresh(), ['product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 1, 'notes' => null], [['ingredient_id' => $ing->id, 'stock_entry_id' => $stock->id, 'quantity_used' => '10']], $user->id, [['product_id' => $product->id, 'quantity' => 1, 'batch_number' => null]], false // abschließen ); expect($completed->isDraft())->toBeFalse() ->and($completed->status)->toBe(Production::STATUS_COMPLETED) ->and(app(ProductStockService::class)->currentStock($product->id))->toBe(1); // Jetzt zählt der Rohstoffverbrauch: 1000 - 10 = 990 $remaining = app(InventoryService::class)->remainingByIngredient([$ing->id])[$ing->id] ?? 0.0; expect((float) $remaining)->toBe(990.0); }); test('AP-43: abgeschlossene produktion kann nicht zum entwurf zurück', function () { $user = draftUser(); $location = Location::factory()->create(); $product = Product::query()->create(['name' => 'Draft-Done', 'title' => 'Draft-Done', 'active' => true, 'no_recipe_required' => true]); $completed = app(ProductionService::class)->store( ['product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 5, 'notes' => null], [], $user->id, [['product_id' => $product->id, 'quantity' => 5, 'batch_number' => null]], false ); expect(fn () => app(ProductionService::class)->updateProduction( $completed->fresh(), ['product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 5, 'notes' => null], [], $user->id, [['product_id' => $product->id, 'quantity' => 5, 'batch_number' => null]], true // Rück-Entwurf verboten ))->toThrow(ValidationException::class); }); test('AP-43: HTTP zwischenspeichern ohne ergebnis speichert entwurf; abschließen erzwingt ergebnis', function () { $user = draftUser(); $location = Location::factory()->create(); $product = Product::query()->create(['name' => 'Draft-HTTP', 'title' => 'Draft-HTTP', 'active' => true, 'no_recipe_required' => true]); Passport::actingAs($user); $this->actingAs($user, 'user'); // Entwurf: kein Produktionsergebnis nötig → wird gespeichert (Redirect zur Bearbeitung) $this->post(route('admin.inventory.productions.store'), [ 'save_mode' => 'draft', 'product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 3, ])->assertRedirect(); $draft = Production::query()->where('product_id', $product->id)->sole(); expect($draft->isDraft())->toBeTrue(); // Abschließen ohne outputs → Validierungsfehler (outputs required) $this->from(route('admin.inventory.productions.create'))->post(route('admin.inventory.productions.store'), [ 'save_mode' => 'complete', 'product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 3, ])->assertSessionHasErrors('outputs'); });