gruene-seele/tests/Feature/IngredientOrderFieldsTest.php
Kevin Adametz d05df498d3 Warenwirtschaft: AP-21 INCI-Erweiterungen
- Lagerort-Stammdaten Raum/Regal/Fach (storage_rooms/shelves/slots) mit CRUD
  auf Einstellungen -> Allgemein (SettingCrudController-Kinder, ein gemeinsamer
  StoreStorageSegmentRequest, generische Partials setting-list-card/-segment-form)
- Ingredient: alt_name (Handelsname), Lagerort-FKs + storageLabel(),
  scopeUsedInActiveRecipes()
- INCI-Formular: Lieferanten-Repeater mit Shop-URL je Lieferant (leer = Mail),
  Alternativname, drei Lagerort-Dropdowns
- INCI-Liste: Filter "nur aktive (in Rezeptur aktiver Produkte)" + Spalte
  Alternativer Name
- Rohstoffbestand-Detail: Bestellweg pro INCI-Lieferant (Pivot-URL statt
  order_method-Fallback), Kopf zeigt Alternativname + Lagerort
- Hinweise-Doku fortgeschrieben inkl. Chargen-Format-Entscheid ttmmjj (AP-24)

Tests: StorageSettingsTest, IngredientActiveRecipeFilterTest,
IngredientOrderFieldsTest (Repeater-Format), RawMaterialStockTest erweitert
(volle Suite 170 gruen, 613 Assertions)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:36:28 +00:00

203 lines
7.2 KiB
PHP
Raw 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\DeliveryTime;
use App\Models\Ingredient;
use App\Models\StorageRoom;
use App\Models\StorageShelf;
use App\Models\StorageSlot;
use App\Models\Supplier;
use App\Models\TaxRate;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function ingredientOrderUser(int $adminLevel): User
{
$user = User::query()->create([
'email' => uniqid('ing_', true).'@test.example',
'password' => bcrypt('password'),
]);
$user->forceFill([
'admin' => $adminLevel,
'confirmed' => true,
'active' => true,
'wizard' => 100,
'blocked' => false,
])->save();
return $user->fresh();
}
function ingredientOrderSupplier(string $name): Supplier
{
return Supplier::factory()->create([
'name' => $name,
'active' => true,
]);
}
test('ingredient form shows suppliers, tax rates and delivery time templates', function () {
$supplier = ingredientOrderSupplier('Aktiv-Lieferant');
TaxRate::query()->create(['name' => 'Standard', 'percent' => 19, 'active' => true, 'pos' => 0]);
DeliveryTime::query()->create(['label' => '3–5 Werktage', 'days' => 5, 'active' => true, 'pos' => 0]);
DeliveryTime::query()->create(['label' => 'Inaktiv-Vorlage', 'active' => false, 'pos' => 1]);
$this->actingAs(ingredientOrderUser(7), 'user');
$this->get(route('admin_product_ingredient_edit', 'new'))
->assertSuccessful()
->assertSee('supplier-repeater')
->assertSee('weiteren Lieferanten anlegen')
->assertSee('alt_name')
->assertSee('storage_room_id')
->assertSee('storage_shelf_id')
->assertSee('storage_slot_id')
->assertSee('tax_rate_id')
->assertSee('Aktiv-Lieferant')
->assertSee('3–5 Werktage')
->assertSee('data-days="5"', false)
->assertDontSee('Inaktiv-Vorlage');
});
test('ingredient can be stored with tax rate, delivery time and suppliers', function () {
$taxRate = TaxRate::query()->create(['name' => 'Standard', 'percent' => 19, 'active' => true, 'pos' => 0]);
$supplierA = ingredientOrderSupplier('Lieferant A');
$supplierB = ingredientOrderSupplier('Lieferant B');
$this->actingAs(ingredientOrderUser(7), 'user');
$this->post(route('admin_product_ingredient_store'), [
'id' => 'new',
'name' => 'Sheabutter',
'alt_name' => 'Butyrospermum Parkii',
'active' => '1',
'tax_rate_id' => (string) $taxRate->id,
'delivery_time' => '3–5 Werktage',
'delivery_time_days' => '5',
'suppliers' => [
['id' => (string) $supplierA->id, 'url' => 'https://shop.example/sheabutter?variant=25kg'],
['id' => (string) $supplierB->id, 'url' => ''],
],
])->assertRedirect(route('admin_product_ingredients'));
$ingredient = Ingredient::query()->where('name', 'Sheabutter')->firstOrFail();
expect($ingredient->alt_name)->toBe('Butyrospermum Parkii')
->and($ingredient->tax_rate_id)->toBe($taxRate->id)
->and($ingredient->delivery_time)->toBe('3–5 Werktage')
->and($ingredient->delivery_time_days)->toBe(5)
->and($ingredient->suppliers()->pluck('suppliers.id')->all())
->toEqualCanonicalizing([$supplierA->id, $supplierB->id])
// URL je Lieferant: gesetzt ⇒ Shop-Link, leer ⇒ Mailbestellung (null)
->and($ingredient->suppliers->firstWhere('id', $supplierA->id)->pivot->url)
->toBe('https://shop.example/sheabutter?variant=25kg')
->and($ingredient->suppliers->firstWhere('id', $supplierB->id)->pivot->url)
->toBeNull();
});
test('ingredient suppliers are synced on update', function () {
$supplierA = ingredientOrderSupplier('Lieferant A');
$supplierB = ingredientOrderSupplier('Lieferant B');
$ingredient = Ingredient::query()->create([
'name' => 'Lavendelöl',
'active' => true,
]);
$ingredient->suppliers()->sync([$supplierA->id]);
$this->actingAs(ingredientOrderUser(7), 'user');
$this->post(route('admin_product_ingredient_store'), [
'id' => (string) $ingredient->id,
'name' => 'Lavendelöl',
'active' => '1',
'suppliers' => [
['id' => (string) $supplierB->id, 'url' => 'https://shop.example/lavendel'],
],
])->assertRedirect(route('admin_product_ingredients'));
expect($ingredient->fresh()->suppliers()->pluck('suppliers.id')->all())
->toEqualCanonicalizing([$supplierB->id]);
});
test('ingredient stores alt name and storage location', function () {
$room = StorageRoom::factory()->create(['label' => 'Raum 1']);
$shelf = StorageShelf::factory()->create(['label' => 'Regal 3']);
$slot = StorageSlot::factory()->create(['label' => 'A']);
$this->actingAs(ingredientOrderUser(7), 'user');
$this->post(route('admin_product_ingredient_store'), [
'id' => 'new',
'name' => 'Lecithin',
'alt_name' => 'Phospholipon 80 H',
'active' => '1',
'storage_room_id' => (string) $room->id,
'storage_shelf_id' => (string) $shelf->id,
'storage_slot_id' => (string) $slot->id,
])->assertRedirect(route('admin_product_ingredients'));
$ingredient = Ingredient::query()->where('name', 'Lecithin')->firstOrFail();
expect($ingredient->alt_name)->toBe('Phospholipon 80 H')
->and($ingredient->storage_room_id)->toBe($room->id)
->and($ingredient->storageLabel())->toBe('Raum 1 | Regal 3 | A');
});
test('ingredient storage location must exist', function () {
$this->actingAs(ingredientOrderUser(7), 'user');
$this->post(route('admin_product_ingredient_store'), [
'id' => 'new',
'name' => 'Falscher-Lagerort',
'active' => '1',
'storage_room_id' => '999999',
])->assertSessionHasErrors('storage_room_id');
});
test('ingredient supplier url must belong to existing supplier', function () {
$this->actingAs(ingredientOrderUser(7), 'user');
$this->post(route('admin_product_ingredient_store'), [
'id' => 'new',
'name' => 'Falscher-Lieferant',
'active' => '1',
'suppliers' => [
['id' => '999999', 'url' => 'https://shop.example'],
],
])->assertSessionHasErrors('suppliers.0.id');
});
test('ingredient delivery time days must be an integer', function () {
$this->actingAs(ingredientOrderUser(7), 'user');
$this->post(route('admin_product_ingredient_store'), [
'id' => 'new',
'name' => 'Falsche-Tage',
'active' => '1',
'delivery_time_days' => 'viele',
])->assertSessionHasErrors('delivery_time_days');
});
test('ingredient tax rate must exist', function () {
$this->actingAs(ingredientOrderUser(7), 'user');
$this->post(route('admin_product_ingredient_store'), [
'id' => 'new',
'name' => 'Falscher-Steuersatz',
'active' => '1',
'tax_rate_id' => '999999',
])->assertSessionHasErrors('tax_rate_id');
});
test('ingredient name is required', function () {
$this->actingAs(ingredientOrderUser(7), 'user');
$this->post(route('admin_product_ingredient_store'), [
'id' => 'new',
'name' => '',
'active' => '1',
])->assertSessionHasErrors('name');
});