Anforderung 15.07.2026: Nahezu alle WaWi-Einträge sind jetzt löschbar (Soft-Delete, deleted_at), mit Abhängigkeitsprüfung und Bestätigungs- warnung vor jeder Löschung. - Migration: deleted_at auf 14 WaWi-Tabellen (Stammdaten, ingredients, stock_entries, stock_disposals, productions); SoftDeletes-Trait in den Models (suppliers/packaging_items hatten es bereits). - Neuer App\Services\DeletionGuard: zentrale Abhängigkeitsprüfung je Entität — referenzierte Einträge sind nicht löschbar, Fehlermeldung nennt die Verwendungen (behebt auch Audit M-4: kein 500er/stiller Cascade-Verlust mehr beim Löschen verwendeter Stammdaten). - SettingCrudController::destroy() prüft Blocker; Supplier-, PackagingItem-, Ingredient-destroy ebenso (Ingredient: vollständige Prüfung inkl. Produktion/Ausschuss, Route GET→DELETE, Audit M-6). - Wareneingänge: auch gebuchte Eingänge löschbar, sofern kein Produktions-Verbrauch/Ausschuss darauf gebucht ist. - Ausschuss: löschbar (Menge steht dem Bestand wieder zur Verfügung). - Produktionen: löschbar (admin >= 7); Produktbestand wird per Gegenbuchung zurückgeführt (reverseProductionStock, append-only, withTrashed), Rohstoffverbrauch zählt nicht mehr (Bestandsqueries schließen gelöschte Produktionen aus). - lastNetPricePerSupplier: DB::query statt Model-Query (Global Scope auf Sub-Select-Alias). - Debug-Log-Reste im PackagingItemController entfernt (Audit N-16). - Views: Lösch-Buttons mit deutschen Bestätigungswarnungen (Ausschuss, Produktionen, gebuchte Wareneingänge, INCI als DELETE-Formular). Tests: neuer InventoryDeletionTest (10 Tests), StorageSettingsTest an neue Löschregel angepasst. Volle Suite: 214 grün, 783 Assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
143 lines
5.3 KiB
PHP
143 lines
5.3 KiB
PHP
<?php
|
|
|
|
use App\Models\Ingredient;
|
|
use App\Models\StorageRoom;
|
|
use App\Models\StorageShelf;
|
|
use App\Models\StorageSlot;
|
|
use App\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
uses(TestCase::class, RefreshDatabase::class);
|
|
|
|
function storageSettingsUser(int $adminLevel = 8): User
|
|
{
|
|
$user = User::query()->create([
|
|
'email' => uniqid('storage_', true).'@test.example',
|
|
'password' => bcrypt('password'),
|
|
]);
|
|
$user->forceFill([
|
|
'admin' => $adminLevel,
|
|
'confirmed' => true,
|
|
'active' => true,
|
|
'wizard' => 100,
|
|
'blocked' => false,
|
|
])->save();
|
|
|
|
return $user->fresh();
|
|
}
|
|
|
|
test('allgemein-seite zeigt lagerort-karten', function () {
|
|
$this->actingAs(storageSettingsUser(8), 'user');
|
|
|
|
StorageRoom::factory()->create(['label' => 'Raum 1']);
|
|
StorageShelf::factory()->create(['label' => 'Regal 3']);
|
|
StorageSlot::factory()->create(['label' => 'A']);
|
|
|
|
$response = $this->get(route('admin.inventory.general'));
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertSee('Lagerorte: Räume', false);
|
|
$response->assertSee('Lagerorte: Regale', false);
|
|
$response->assertSee('Lagerorte: Fächer / Buchstaben', false);
|
|
$response->assertSee('Raum 1', false);
|
|
$response->assertSee('Regal 3', false);
|
|
$response->assertSee(route('admin.inventory.storage-rooms.create'), false);
|
|
$response->assertSee(route('admin.inventory.storage-shelves.create'), false);
|
|
$response->assertSee(route('admin.inventory.storage-slots.create'), false);
|
|
});
|
|
|
|
test('superadmin legt raum, regal und fach an', function () {
|
|
$this->actingAs(storageSettingsUser(8), 'user');
|
|
|
|
foreach ([
|
|
['route' => 'admin.inventory.storage-rooms', 'model' => StorageRoom::class, 'label' => 'Raum 2'],
|
|
['route' => 'admin.inventory.storage-shelves', 'model' => StorageShelf::class, 'label' => 'Regal 7'],
|
|
['route' => 'admin.inventory.storage-slots', 'model' => StorageSlot::class, 'label' => 'O'],
|
|
] as $case) {
|
|
$this->get(route($case['route'].'.create'))->assertSuccessful();
|
|
|
|
$this->post(route($case['route'].'.store'), [
|
|
'label' => $case['label'],
|
|
'active' => '1',
|
|
'pos' => 0,
|
|
])->assertRedirect(route('admin.inventory.general'));
|
|
|
|
expect($case['model']::query()->where('label', $case['label'])->exists())->toBeTrue();
|
|
}
|
|
});
|
|
|
|
test('raum kann bearbeitet, deaktiviert und geloescht werden', function () {
|
|
$this->actingAs(storageSettingsUser(8), 'user');
|
|
|
|
$room = StorageRoom::factory()->create(['label' => 'Raum 5', 'active' => true, 'pos' => 1]);
|
|
|
|
$this->get(route('admin.inventory.storage-rooms.edit', $room))->assertSuccessful();
|
|
|
|
$this->put(route('admin.inventory.storage-rooms.update', $room), [
|
|
'label' => 'Raum 5 (Keller)',
|
|
'pos' => 1,
|
|
])->assertRedirect(route('admin.inventory.general'));
|
|
|
|
$room->refresh();
|
|
expect($room->label)->toBe('Raum 5 (Keller)')
|
|
->and($room->active)->toBeFalse();
|
|
|
|
$this->delete(route('admin.inventory.storage-rooms.destroy', $room))
|
|
->assertRedirect(route('admin.inventory.general'));
|
|
|
|
expect(StorageRoom::query()->whereKey($room->id)->exists())->toBeFalse();
|
|
});
|
|
|
|
test('bezeichnung ist pflicht', function () {
|
|
$this->actingAs(storageSettingsUser(8), 'user');
|
|
|
|
$this->post(route('admin.inventory.storage-rooms.store'), [
|
|
'label' => '',
|
|
])->assertSessionHasErrors('label');
|
|
});
|
|
|
|
test('nur aktive segmente ueber active-scope', function () {
|
|
StorageShelf::factory()->create(['label' => 'Aktiv', 'active' => true]);
|
|
StorageShelf::factory()->create(['label' => 'Inaktiv', 'active' => false]);
|
|
|
|
expect(StorageShelf::query()->active()->pluck('label')->all())->toBe(['Aktiv']);
|
|
});
|
|
|
|
test('verwendeter lagerort-baustein kann nicht geloescht werden', function () {
|
|
$this->actingAs(storageSettingsUser(8), 'user');
|
|
|
|
$room = StorageRoom::factory()->create(['label' => 'Raum 9']);
|
|
$ingredient = Ingredient::query()->create([
|
|
'name' => 'Storage-FK-Test',
|
|
'active' => true,
|
|
'storage_room_id' => $room->id,
|
|
]);
|
|
|
|
$this->delete(route('admin.inventory.storage-rooms.destroy', $room))
|
|
->assertRedirect(route('admin.inventory.general'));
|
|
|
|
expect(session('alert-error'))->toContain('Löschen nicht möglich');
|
|
expect(StorageRoom::query()->find($room->id))->not->toBeNull();
|
|
expect($ingredient->fresh()->storage_room_id)->toBe($room->id);
|
|
});
|
|
|
|
test('unbenutzter lagerort-baustein wird soft-geloescht', function () {
|
|
$this->actingAs(storageSettingsUser(8), 'user');
|
|
|
|
$room = StorageRoom::factory()->create(['label' => 'Raum leer']);
|
|
|
|
$this->delete(route('admin.inventory.storage-rooms.destroy', $room))
|
|
->assertRedirect(route('admin.inventory.general'));
|
|
|
|
expect(StorageRoom::query()->find($room->id))->toBeNull();
|
|
expect(StorageRoom::withTrashed()->find($room->id))->not->toBeNull();
|
|
});
|
|
|
|
test('nicht-superadmin hat keinen zugriff auf lagerort-anlage', function () {
|
|
$this->actingAs(storageSettingsUser(7), 'user');
|
|
|
|
$this->get(route('admin.inventory.storage-rooms.create'))->assertRedirect('/home');
|
|
$this->get(route('admin.inventory.storage-shelves.create'))->assertRedirect('/home');
|
|
$this->get(route('admin.inventory.storage-slots.create'))->assertRedirect('/home');
|
|
});
|