gruene-seele/tests/Feature/RecipePhaseTest.php
Kevin Adametz d60947c34d Warenwirtschaft: AP-36 — Rezeptur-Textphasen ohne Rohstoffe anzeigen
Phasen ohne Rohstoffzeile (reine Hinweisphasen wie „Vorbereitung"/„Nachbereitung")
verschwanden in der Produktionsmaske, weil buildPhaseGroups() leere Gruppen
wegfilterte. Filter so angepasst, dass benannte Phasen erhalten bleiben und nur
der namenlose „lose" Sammeltopf verworfen wird. Die Phasen kommen serverseitig
bereits in pos-Reihenfolge (recipePhases orderBy pos), sodass „Vorbereitung"
korrekt über „Phase A" steht. Test sichert den recipeJson-Datenvertrag ab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-27 13:59:51 +00:00

243 lines
8.6 KiB
PHP

<?php
use App\Models\Ingredient;
use App\Models\Location;
use App\Models\Product;
use App\Models\RecipePhase;
use App\Repositories\ProductRepository;
use App\Services\ProductionService;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function ap23MakeIngredient(string $name): Ingredient
{
return Ingredient::query()->create([
'name' => $name,
'trans_name' => '',
'inci' => $name,
'trans_inci' => '',
'effect' => '',
'trans_effect' => '',
'active' => true,
'pos' => 0,
]);
}
function ap23MakeProduct(string $name): Product
{
return Product::query()->create([
'name' => $name,
'title' => $name,
'active' => true,
]);
}
/**
* @param array<string, mixed> $extra
*/
function ap23SaveManufacturerRecipe(Product $product, array $extra): void
{
$repo = new ProductRepository($product);
$repo->update(array_merge([
'id' => (string) $product->id,
'name' => $product->name,
'title' => $product->title,
'active' => '1',
'manufacturer_inci_sync_sent' => '1',
], $extra));
}
test('phasen mit zeilen, notiz und reihenfolge werden gespeichert', function () {
$product = ap23MakeProduct('AP23-Phasen');
$a = ap23MakeIngredient('AP23-Sheabutter');
$b = ap23MakeIngredient('AP23-Lecithin');
$c = ap23MakeIngredient('AP23-Wasser');
ap23SaveManufacturerRecipe($product, [
'mfg_phase_name' => ['Phase A', 'Phase B'],
'mfg_phase_note' => ['Fettphase auf 70 °C erhitzen', ''],
'mfg_ingredient_id' => [(string) $a->id, (string) $b->id, (string) $c->id],
'mfg_row_phase' => ['0', '0', '1'],
'mfg_gram' => ['61,456', '8,544', '30'],
'mfg_factor' => ['1,10', '1,10', '1,00'],
]);
$phases = $product->recipePhases()->get();
expect($phases)->toHaveCount(2)
->and($phases[0]->name)->toBe('Phase A')
->and($phases[0]->note)->toBe('Fettphase auf 70 °C erhitzen')
->and($phases[0]->pos)->toBe(0)
->and($phases[1]->name)->toBe('Phase B')
->and($phases[1]->note)->toBeNull();
$rows = $product->manufacturer_ingredients()->get();
expect($rows)->toHaveCount(3)
->and($rows->firstWhere('id', $a->id)->pivot->recipe_phase_id)->toBe($phases[0]->id)
->and($rows->firstWhere('id', $b->id)->pivot->recipe_phase_id)->toBe($phases[0]->id)
->and($rows->firstWhere('id', $c->id)->pivot->recipe_phase_id)->toBe($phases[1]->id);
});
test('leerer phasenname wird automatisch benannt', function () {
$product = ap23MakeProduct('AP23-AutoName');
$a = ap23MakeIngredient('AP23-Auto-A');
ap23SaveManufacturerRecipe($product, [
'mfg_phase_name' => ['', 'Wasserphase'],
'mfg_phase_note' => ['', ''],
'mfg_ingredient_id' => [(string) $a->id],
'mfg_row_phase' => ['1'],
'mfg_gram' => ['100'],
'mfg_factor' => ['1,00'],
]);
expect($product->recipePhases()->pluck('name')->all())->toBe(['Phase A', 'Wasserphase']);
});
test('einzelner unbenannter block bleibt phasenlos (altverhalten)', function () {
$product = ap23MakeProduct('AP23-Implizit');
$a = ap23MakeIngredient('AP23-Implizit-A');
ap23SaveManufacturerRecipe($product, [
'mfg_phase_name' => [''],
'mfg_phase_note' => [''],
'mfg_ingredient_id' => [(string) $a->id],
'mfg_row_phase' => ['0'],
'mfg_gram' => ['100'],
'mfg_factor' => ['1,00'],
]);
expect(RecipePhase::query()->where('product_id', $product->id)->count())->toBe(0)
->and($product->manufacturer_ingredients()->first()->pivot->recipe_phase_id)->toBeNull();
});
test('erneutes speichern ersetzt phasen vollstaendig', function () {
$product = ap23MakeProduct('AP23-Resave');
$a = ap23MakeIngredient('AP23-Resave-A');
ap23SaveManufacturerRecipe($product, [
'mfg_phase_name' => ['Phase A', 'Phase B'],
'mfg_phase_note' => ['', ''],
'mfg_ingredient_id' => [(string) $a->id],
'mfg_row_phase' => ['0'],
'mfg_gram' => ['100'],
'mfg_factor' => ['1,00'],
]);
ap23SaveManufacturerRecipe($product, [
'mfg_phase_name' => ['Nur noch eine'],
'mfg_phase_note' => ['Neue Notiz'],
'mfg_ingredient_id' => [(string) $a->id],
'mfg_row_phase' => ['0'],
'mfg_gram' => ['100'],
'mfg_factor' => ['1,00'],
]);
$phases = RecipePhase::query()->where('product_id', $product->id)->get();
expect($phases)->toHaveCount(1)
->and($phases[0]->name)->toBe('Nur noch eine')
->and($phases[0]->note)->toBe('Neue Notiz');
});
test('produkt-kopie uebernimmt phasen und zuordnung', function () {
$product = ap23MakeProduct('AP23-Original');
$a = ap23MakeIngredient('AP23-Kopie-A');
$b = ap23MakeIngredient('AP23-Kopie-B');
ap23SaveManufacturerRecipe($product, [
'mfg_phase_name' => ['Phase A', 'Phase B'],
'mfg_phase_note' => ['Notiz A', ''],
'mfg_ingredient_id' => [(string) $a->id, (string) $b->id],
'mfg_row_phase' => ['0', '1'],
'mfg_gram' => ['60', '40'],
'mfg_factor' => ['1,10', '1,00'],
]);
$repo = new ProductRepository(new Product);
$repo->copy($product->fresh());
$copy = Product::query()->where('name', 'Kopie: AP23-Original')->firstOrFail();
$copyPhases = $copy->recipePhases()->get();
expect($copyPhases)->toHaveCount(2)
->and($copyPhases[0]->name)->toBe('Phase A')
->and($copyPhases[0]->note)->toBe('Notiz A');
$copyRows = $copy->manufacturer_ingredients()->get();
expect($copyRows)->toHaveCount(2)
->and($copyRows->firstWhere('id', $a->id)->pivot->recipe_phase_id)->toBe($copyPhases[0]->id)
->and($copyRows->firstWhere('id', $b->id)->pivot->recipe_phase_id)->toBe($copyPhases[1]->id)
// Kopie hat eigene Phasen, nicht die des Originals
->and($copyPhases[0]->id)->not->toBe($product->recipePhases()->first()->id);
});
test('produktion rechnet unveraendert ueber alle phasen', function () {
$product = ap23MakeProduct('AP23-Produktion');
$a = ap23MakeIngredient('AP23-Prod-A');
$b = ap23MakeIngredient('AP23-Prod-B');
ap23SaveManufacturerRecipe($product, [
'mfg_phase_name' => ['Phase A', 'Phase B'],
'mfg_phase_note' => ['', ''],
'mfg_ingredient_id' => [(string) $a->id, (string) $b->id],
'mfg_row_phase' => ['0', '1'],
'mfg_gram' => ['60', '40'],
'mfg_factor' => ['1,00', '1,00'],
]);
// 5 Stück à (60 g + 40 g) => Soll je Rohstoff phasenunabhängig
$required = app(ProductionService::class)->requiredGramsByIngredient($product->fresh(), 5);
expect($required[$a->id])->toBe(300.0)
->and($required[$b->id])->toBe(200.0);
});
test('AP-36: recipeJson liefert auch phasen ohne rohstoffe (textphasen)', function () {
// Die eigentliche Anzeige-Korrektur sitzt im JS (buildPhaseGroups rendert leere
// benannte Phasen). Dieser Test sichert den Datenvertrag ab, auf den das JS baut:
// eine Phase ohne Rohstoffzeile muss im recipeJson-Payload erhalten bleiben.
$product = ap23MakeProduct('AP36-Textphase');
$a = ap23MakeIngredient('AP36-Sheabutter');
ap23SaveManufacturerRecipe($product, [
'mfg_phase_name' => ['Vorbereitung', 'Phase A'],
'mfg_phase_note' => ['Arbeitsplatz desinfizieren', ''],
// nur die zweite Phase trägt einen Rohstoff — „Vorbereitung" bleibt leer
'mfg_ingredient_id' => [(string) $a->id],
'mfg_row_phase' => ['1'],
'mfg_gram' => ['100'],
'mfg_factor' => ['1,00'],
]);
$user = phaseJsonUser();
$this->actingAs($user, 'user');
$location = Location::factory()->create();
$response = $this->getJson(route('admin.inventory.api.products.recipe', [
'product' => $product->id,
'location_id' => $location->id,
'quantity' => 1,
]))->assertSuccessful();
$phases = collect($response->json('phases'));
expect($phases)->toHaveCount(2);
$vorbereitung = $phases->firstWhere('name', 'Vorbereitung');
expect($vorbereitung)->not->toBeNull()
->and($vorbereitung['note'])->toBe('Arbeitsplatz desinfizieren');
// Vorbereitung steht vor Phase A (pos-Reihenfolge)
expect($phases[0]['name'])->toBe('Vorbereitung');
});
function phaseJsonUser(): User
{
$user = User::query()->create([
'email' => uniqid('ap36_', true).'@test.example',
'password' => bcrypt('password'),
]);
$user->forceFill([
'admin' => 7, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false,
])->save();
return $user->fresh();
}