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>
209 lines
7.5 KiB
PHP
209 lines
7.5 KiB
PHP
<?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');
|
||
});
|