- 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>
126 lines
4.6 KiB
PHP
126 lines
4.6 KiB
PHP
<?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);
|
||
});
|