gruene-seele/tests/Feature/RecipeRobustnessTest.php
Kevin Adametz fa44af0a1f Warenwirtschaft: OPT-8 Rezeptur-Robustheit (Audit H-4, H-5, M-8, N-11/12/14)
- H-4: Rezeptur-Neuaufbau (Delete-then-Insert) in DB::transaction;
  unbekannte Rohstoff-IDs brechen validiert ab (kein FK-Crash nach dem
  Delete mehr), Phasennamen auf varchar(100) gekappt.
- H-5: Phasen werden auch ohne Rohstoffzeilen persistiert — Struktur
  mit Namen/Notizen geht beim Speichern nicht mehr verloren.
- M-8: requiredGramsByIngredient() summiert doppelte Rohstoffe statt
  zu überschreiben.
- N-11: Set-Umstellung löscht Rezeptur-Phasen mit.
- N-12: Auto-Phasenname nach Z: AA, AB, …
- N-14: Legacy-Zweig updateIngredients() nicht mehr destruktiv —
  bestehende gram/factor-Werte bleiben beim Listen-Sync erhalten
  (Verhalten lt. ProductPhase0Test unverändert).

Tests: neuer RecipeRobustnessTest (6). Suite komplett grün.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 12:48:55 +00:00

126 lines
4.6 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Models\Ingredient;
use App\Models\Product;
use App\Models\ProductIngredient;
use App\Models\RecipePhase;
use App\Repositories\ProductRepository;
use App\Services\ProductionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function rrIngredient(string $name): Ingredient
{
return Ingredient::query()->create([
'name' => $name, 'trans_name' => '', 'inci' => $name, 'trans_inci' => '',
'effect' => '', 'trans_effect' => '', 'active' => true, 'pos' => 0,
]);
}
function rrProduct(string $name): Product
{
return Product::query()->create(['name' => $name, 'title' => $name, 'active' => true]);
}
test('phasen ohne rohstoffzeilen werden gespeichert (H-5)', function () {
$product = rrProduct('RR-PhasenOhneZeilen');
$repo = new ProductRepository($product);
$repo->updateManufacturerIngredients([
'manufacturer_inci_sync_sent' => '1',
'mfg_phase_name' => ['Fettphase', 'Wasserphase'],
'mfg_phase_note' => ['auf 70 °C erhitzen', ''],
]);
$phases = RecipePhase::query()->where('product_id', $product->id)->orderBy('pos')->get();
expect($phases)->toHaveCount(2);
expect($phases[0]->name)->toBe('Fettphase');
expect($phases[0]->note)->toBe('auf 70 °C erhitzen');
});
test('unbekannte rohstoff-id bricht ab und laesst die alte rezeptur unangetastet (H-4)', function () {
$product = rrProduct('RR-Rollback');
$ing = rrIngredient('RR-Alt');
ProductIngredient::create([
'product_id' => $product->id, 'ingredient_id' => $ing->id,
'pos' => 0, 'gram' => 50, 'factor' => 1, 'recipe_type' => 'manufacturer',
]);
$repo = new ProductRepository($product);
expect(fn () => $repo->updateManufacturerIngredients([
'manufacturer_inci_sync_sent' => '1',
'mfg_phase_name' => [''],
'mfg_phase_note' => [''],
'mfg_ingredient_id' => [999999],
'mfg_gram' => ['10'],
'mfg_factor' => ['1'],
'mfg_row_phase' => [0],
]))->toThrow(ValidationException::class);
// Transaktion zurückgerollt: alte Zeile existiert weiter.
$rows = ProductIngredient::query()->where('product_id', $product->id)->where('recipe_type', 'manufacturer')->get();
expect($rows)->toHaveCount(1);
expect((float) $rows[0]->gram)->toBe(50.0);
});
test('ueberlanger phasenname wird auf spaltenlaenge gekappt (H-4)', function () {
$product = rrProduct('RR-Lang');
$repo = new ProductRepository($product);
$repo->updateManufacturerIngredients([
'manufacturer_inci_sync_sent' => '1',
'mfg_phase_name' => [str_repeat('X', 150), 'B'],
'mfg_phase_note' => ['', ''],
]);
$phase = RecipePhase::query()->where('product_id', $product->id)->orderBy('pos')->first();
expect(mb_strlen($phase->name))->toBe(100);
});
test('doppelter rohstoff summiert die sollmenge statt zu ueberschreiben (M-8)', function () {
$product = rrProduct('RR-Doppel');
$ing = rrIngredient('RR-Zweifach');
foreach ([10, 5] as $i => $gram) {
ProductIngredient::create([
'product_id' => $product->id, 'ingredient_id' => $ing->id,
'pos' => $i, 'gram' => $gram, 'factor' => 1, 'recipe_type' => 'manufacturer',
]);
}
$required = app(ProductionService::class)->requiredGramsByIngredient($product->fresh('manufacturer_ingredients'), 10);
expect($required[$ing->id])->toBe(150.0); // (10 + 5) × 1 × 10
});
test('auto-phasenname laeuft nach Z sauber weiter (N-12)', function () {
$product = rrProduct('RR-27Phasen');
$repo = new ProductRepository($product);
$repo->updateManufacturerIngredients([
'manufacturer_inci_sync_sent' => '1',
'mfg_phase_name' => array_fill(0, 27, ''),
'mfg_phase_note' => array_merge([''], array_fill(1, 26, '')),
]);
$phases = RecipePhase::query()->where('product_id', $product->id)->orderBy('pos')->get();
expect($phases)->toHaveCount(27);
expect($phases[25]->name)->toBe('Phase Z');
expect($phases[26]->name)->toBe('Phase AA');
});
test('set-umstellung entfernt auch die rezeptur-phasen (N-11)', function () {
$product = rrProduct('RR-Set');
RecipePhase::create([
'product_id' => $product->id, 'recipe_type' => 'manufacturer',
'name' => 'Phase A', 'note' => null, 'pos' => 0,
]);
$repo = new ProductRepository($product);
$repo->update(['id' => $product->id, 'name' => $product->name, 'is_set' => 1]);
expect(RecipePhase::query()->where('product_id', $product->id)->count())->toBe(0);
});