- 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>
192 lines
6.7 KiB
PHP
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);
|
|
});
|