gruene-seele/tests/Feature/RecipePhaseTest.php
Kevin Adametz 97d420c98a Warenwirtschaft: AP-23 Rezeptur-Phasen (Hersteller-Rezeptur)
- Tabelle recipe_phases + product_ingredients.recipe_phase_id (nullable,
  rueckwaertskompatibel: Bestandsrezepturen bleiben phasenlos)
- Produktformular: Hersteller-Rezeptur in Phasen-Bloecke gegliedert
  (Name, Notiz, Rohstoffe je Phase, Drag&Drop auch zwischen Phasen,
  "+ Neue Phase", 100%-Summe global ueber alle Phasen)
- Repository: Phasen-Persistierung (Auto-Name Phase A/B..., einzelner
  unbenannter Block bleibt phasenlos), copy() uebernimmt Phasenstruktur
- Produktions-Sollmengen rechnen unveraendert ueber alle Zeilen
- Blade-Fix: inline @php() + @php-Block im selben Template paarten sich
  falsch (Raw-Block verschluckte halbes Formular) - auf Block-Form vereinheitlicht
- Hinweise-Doku fortgeschrieben

Tests: RecipePhaseTest (6 gruen), volle Suite 178 gruen, 649 Assertions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:02:39 +00:00

192 lines
6.7 KiB
PHP

<?php
use App\Models\Ingredient;
use App\Models\Product;
use App\Models\RecipePhase;
use App\Repositories\ProductRepository;
use App\Services\ProductionService;
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);
});