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); });