gruene-seele/tests/Feature/IngredientOrderFieldsTest.php
Kevin Adametz e08c8dec40 Warenwirtschaft: AP-50 — Lagerort löschbar (Buchungen übernehmen) + Köln als Default
Kundenentscheid 26.08.: „Waldbröl" gibt es nicht mehr, alles wird Köln; die
Standort-Funktion bleibt für künftige Standorte.

- LocationController::destroy überschrieben: ein belegter Lagerort lässt sich
  löschen, indem seine Buchungen (Wareneingänge, Produktionen, Ausschuss)
  transaktional auf einen Ziel-Lagerort übernommen werden. Ohne Ziel-Lagerort
  bleibt die Sperre bestehen; unbelegte Orte werden direkt gelöscht.
- Lagerort-Liste: Ziel-Auswahl („→ Köln" vorbelegt) im Lösch-Formular.
- Wareneingang: Köln als Standard-Lagerort vorbelegt (wie in der Produktion).

Außerdem AP-49-Folgeanpassung: Test-Helper hängt die Kategorie „Kosmetikrohstoffe"
an, damit Lieferanten im gefilterten INCI-Formular erscheinen; Lagerort-Sperr-Test
prüft die neue Meldung. Suite 322 grün.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-28 09:17:28 +00:00

209 lines
7.5 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\DeliveryTime;
use App\Models\Ingredient;
use App\Models\StorageRoom;
use App\Models\StorageShelf;
use App\Models\StorageSlot;
use App\Models\Supplier;
use App\Models\SupplierCategory;
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
{
$supplier = Supplier::factory()->create([
'name' => $name,
'active' => true,
]);
// AP-49: Das INCI-Formular zeigt nur Rohstoff-Lieferanten (Kategorie „Kosmetikrohstoffe").
$category = SupplierCategory::query()->firstOrCreate(['name' => 'Kosmetikrohstoffe'], ['pos' => 0]);
$supplier->supplierCategories()->attach($category->id);
return $supplier;
}
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');
});