Warenwirtschaft: AP-51 — Lagerplatz-Felder für Produkte & Verpackung

Lagerplatz-Zuordnung (Raum/Regal/Fach) gab es bisher nur bei Rohstoffen. Jetzt
dieselbe Storage-Hierarchie (StorageRoom/Shelf/Slot) auch für Produkte und
Verpackungsartikel, inkl. Anzeige an den passenden Stellen.

- Migration: storage_room_id/shelf/slot an products + packaging_items
  (nullable, FK constrained, nullOnDelete).
- Models Product + PackagingItem: fillable + storageRoom/Shelf/Slot-Relationen +
  storageLabel() („Raum 1 | Regal 3 | A").
- Produktformular (Inventar-Abschnitt) + Verpackungsformular: Auswahl-Zeile
  Raum/Regal/Fach; Optionen kommen aktiv/sortiert aus den zentral pflegbaren
  Bausteinen (Einstellungen → Allgemein).
- Speichern: ProductRepository normalisiert leere Auswahl -> null (FK);
  PackagingItem-Requests + Repository um die drei Felder erweitert.
- Anzeige: Produktbestand (Sub-Zeile mit Marker), Produktverpackung-Übersicht
  (neue Spalte „Lagerplatz").
- DeletionGuard: Lagerbaustein ist nicht löschbar, solange Produkte/Verpackung
  darauf verweisen (analog Rohstoffe).

Tests: 4 neue (Produkt speichern + leer->null, Verpackung speichern + Anzeige,
Löschsperre, freier Baustein löschbar). Migration auf Dev-/Live-DB gelaufen.
Suite 331 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-08-28 09:46:40 +00:00
parent eb47c8a82d
commit 30d4e28696
16 changed files with 363 additions and 5 deletions

View file

@ -7,10 +7,14 @@ use App\Http\Requests\Inventory\StorePackagingItemRequest;
use App\Http\Requests\Inventory\UpdatePackagingItemRequest;
use App\Models\PackagingItem;
use App\Models\PackagingMaterial;
use App\Models\StorageRoom;
use App\Models\StorageShelf;
use App\Models\StorageSlot;
use App\Models\Supplier;
use App\Repositories\PackagingItemRepository;
use App\Services\DeletionGuard;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
class PackagingItemController extends Controller
{
@ -24,7 +28,7 @@ class PackagingItemController extends Controller
$isShipping = $category === 'shipping';
$query = PackagingItem::query()
->with(['packagingMaterial', 'supplier'])
->with(['packagingMaterial', 'supplier', 'storageRoom', 'storageShelf', 'storageSlot'])
->orderBy('name');
if ($isShipping) {
@ -52,9 +56,24 @@ class PackagingItemController extends Controller
'packagingMaterials' => PackagingMaterial::query()->orderBy('pos')->orderBy('name')->get(),
'suppliers' => Supplier::query()->orderBy('name')->get(),
'category' => $category,
...$this->storageOptions(),
]);
}
/**
* AP-51: Lagerplatz-Bausteine (Raum/Regal/Fach) für das Formular.
*
* @return array<string, Collection<int, mixed>>
*/
private function storageOptions(): array
{
return [
'storageRooms' => StorageRoom::query()->active()->orderBy('pos')->orderBy('label')->get(),
'storageShelves' => StorageShelf::query()->active()->orderBy('pos')->orderBy('label')->get(),
'storageSlots' => StorageSlot::query()->active()->orderBy('pos')->orderBy('label')->get(),
];
}
public function store(StorePackagingItemRequest $request)
{
$validated = $request->validated();
@ -77,6 +96,7 @@ class PackagingItemController extends Controller
'packagingMaterials' => PackagingMaterial::query()->orderBy('pos')->orderBy('name')->get(),
'suppliers' => Supplier::query()->orderBy('name')->get(),
'category' => $category,
...$this->storageOptions(),
]);
}

View file

@ -25,7 +25,7 @@ class ProductStockController extends Controller
->where('is_set', false)
->where('show_in_product_stock', true)
->whereNull('main_product_id')
->with('images')
->with(['images', 'storageRoom', 'storageShelf', 'storageSlot'])
->orderBy('pos')
->orderBy('name')
->get();

View file

@ -8,6 +8,9 @@ use App\Models\PackagingItem;
use App\Models\Product;
use App\Models\ProductImage;
use App\Models\ProductIngredient;
use App\Models\StorageRoom;
use App\Models\StorageShelf;
use App\Models\StorageSlot;
use App\Repositories\ProductRepository;
use Illuminate\Database\Eloquent\Collection;
use Request;
@ -58,6 +61,9 @@ class ProductController extends Controller
'ingredient_catalog' => Ingredient::query()->where('active', true)->with('materialQuality')->orderBy('name')->get(['id', 'name', 'inci', 'effect', 'default_factor', 'material_quality_id']),
'packaging_catalog' => PackagingItem::query()->where('active', true)->with('packagingMaterial')->orderBy('name')->get(),
'set_product_catalog' => $this->setProductCatalog($model),
'storageRooms' => StorageRoom::query()->active()->orderBy('pos')->orderBy('label')->get(),
'storageShelves' => StorageShelf::query()->active()->orderBy('pos')->orderBy('label')->get(),
'storageSlots' => StorageSlot::query()->active()->orderBy('pos')->orderBy('label')->get(),
];
return view('admin.product.edit', $data);
@ -127,6 +133,9 @@ class ProductController extends Controller
'ingredient_catalog' => Ingredient::query()->where('active', true)->with('materialQuality')->orderBy('name')->get(['id', 'name', 'inci', 'effect', 'default_factor', 'material_quality_id']),
'packaging_catalog' => PackagingItem::query()->where('active', true)->with('packagingMaterial')->orderBy('name')->get(),
'set_product_catalog' => $this->setProductCatalog($model),
'storageRooms' => StorageRoom::query()->active()->orderBy('pos')->orderBy('label')->get(),
'storageShelves' => StorageShelf::query()->active()->orderBy('pos')->orderBy('label')->get(),
'storageSlots' => StorageSlot::query()->active()->orderBy('pos')->orderBy('label')->get(),
];
if ($validator->fails()) {

View file

@ -26,6 +26,9 @@ class StorePackagingItemRequest extends FormRequest
'min_stock_alert' => ['nullable', 'integer', 'min:0'],
'url' => ['nullable', 'string', 'url:http,https', 'max:2048'],
'product_id' => ['nullable', 'integer', 'exists:products,id'],
'storage_room_id' => ['nullable', 'integer', 'exists:storage_rooms,id'],
'storage_shelf_id' => ['nullable', 'integer', 'exists:storage_shelves,id'],
'storage_slot_id' => ['nullable', 'integer', 'exists:storage_slots,id'],
'active' => ['sometimes', 'boolean'],
];
}
@ -38,7 +41,7 @@ class StorePackagingItemRequest extends FormRequest
'weight_grams' => $this->filled('weight_grams') ? reFormatWawiNumber($this->input('weight_grams')) : null,
]);
foreach (['supplier_id', 'product_id', 'min_stock_alert'] as $key) {
foreach (['supplier_id', 'product_id', 'min_stock_alert', 'storage_room_id', 'storage_shelf_id', 'storage_slot_id'] as $key) {
if ($this->input($key) === '' || $this->input($key) === null) {
$this->merge([$key => null]);
}

View file

@ -26,6 +26,9 @@ class UpdatePackagingItemRequest extends FormRequest
'min_stock_alert' => ['nullable', 'integer', 'min:0'],
'url' => ['nullable', 'string', 'url:http,https', 'max:2048'],
'product_id' => ['nullable', 'integer', 'exists:products,id'],
'storage_room_id' => ['nullable', 'integer', 'exists:storage_rooms,id'],
'storage_shelf_id' => ['nullable', 'integer', 'exists:storage_shelves,id'],
'storage_slot_id' => ['nullable', 'integer', 'exists:storage_slots,id'],
'active' => ['sometimes', 'boolean'],
];
}
@ -38,7 +41,7 @@ class UpdatePackagingItemRequest extends FormRequest
'weight_grams' => $this->filled('weight_grams') ? reFormatWawiNumber($this->input('weight_grams')) : null,
]);
foreach (['supplier_id', 'product_id', 'min_stock_alert'] as $key) {
foreach (['supplier_id', 'product_id', 'min_stock_alert', 'storage_room_id', 'storage_shelf_id', 'storage_slot_id'] as $key) {
if ($this->input($key) === '' || $this->input($key) === null) {
$this->merge([$key => null]);
}

View file

@ -23,6 +23,9 @@ class PackagingItem extends Model
'min_stock_alert',
'url',
'product_id',
'storage_room_id',
'storage_shelf_id',
'storage_slot_id',
'active',
];
@ -72,4 +75,44 @@ class PackagingItem extends Model
->withPivot('quantity', 'pos')
->withTimestamps();
}
/**
* AP-51: Lagerplatz (Raum/Regal/Fach) — gleiche Hierarchie wie beim Rohstoff.
*
* @return BelongsTo<StorageRoom, $this>
*/
public function storageRoom(): BelongsTo
{
return $this->belongsTo(StorageRoom::class);
}
/**
* @return BelongsTo<StorageShelf, $this>
*/
public function storageShelf(): BelongsTo
{
return $this->belongsTo(StorageShelf::class);
}
/**
* @return BelongsTo<StorageSlot, $this>
*/
public function storageSlot(): BelongsTo
{
return $this->belongsTo(StorageSlot::class);
}
/**
* Lagerplatz-Anzeige im Schema „Raum 1 | Regal 3 | A" (nur gepflegte Teile).
*/
public function storageLabel(): ?string
{
$parts = array_filter([
$this->storageRoom?->label,
$this->storageShelf?->label,
$this->storageSlot?->label,
]);
return $parts === [] ? null : implode(' | ', $parts);
}
}

View file

@ -237,6 +237,9 @@ class Product extends Model
'contents_total',
'fill_weight',
'production_relevant',
'storage_room_id',
'storage_shelf_id',
'storage_slot_id',
'unit',
'number',
'wp_number',
@ -404,6 +407,46 @@ class Product extends Model
return $this->hasMany(Production::class);
}
/**
* AP-51: Lagerplatz (Raum/Regal/Fach) — gleiche Hierarchie wie beim Rohstoff.
*
* @return BelongsTo<StorageRoom, $this>
*/
public function storageRoom(): BelongsTo
{
return $this->belongsTo(StorageRoom::class);
}
/**
* @return BelongsTo<StorageShelf, $this>
*/
public function storageShelf(): BelongsTo
{
return $this->belongsTo(StorageShelf::class);
}
/**
* @return BelongsTo<StorageSlot, $this>
*/
public function storageSlot(): BelongsTo
{
return $this->belongsTo(StorageSlot::class);
}
/**
* Lagerplatz-Anzeige im Schema „Raum 1 | Regal 3 | A" (nur gepflegte Teile).
*/
public function storageLabel(): ?string
{
$parts = array_filter([
$this->storageRoom?->label,
$this->storageShelf?->label,
$this->storageSlot?->label,
]);
return $parts === [] ? null : implode(' | ', $parts);
}
/**
* Produktbestands-Bewegungen (Eingang/Ausgang).
*

View file

@ -23,7 +23,7 @@ class PackagingItemRepository
'error' => $e->getMessage(),
'sql' => $lastQ['query'] ?? 'unknown',
'bindings' => $lastQ['bindings'] ?? [],
'binding_types' => array_map(fn($v) => gettype($v) . ':' . json_encode($v), $lastQ['bindings'] ?? []),
'binding_types' => array_map(fn ($v) => gettype($v).':'.json_encode($v), $lastQ['bindings'] ?? []),
'attributes_passed' => $attrs,
]);
@ -60,6 +60,9 @@ class PackagingItemRepository
'min_stock_alert',
'url',
'product_id',
'storage_room_id',
'storage_shelf_id',
'storage_slot_id',
'active',
])->all();

View file

@ -57,6 +57,15 @@ class ProductRepository extends BaseRepository
? trim((string) $data['batch_prefix'])
: null;
// AP-51: Lagerplatz (Raum/Regal/Fach) — leere Auswahl => null (FK).
foreach (['storage_room_id', 'storage_shelf_id', 'storage_slot_id'] as $storageKey) {
if (array_key_exists($storageKey, $data)) {
$data[$storageKey] = ($data[$storageKey] === '' || $data[$storageKey] === null)
? null
: (int) $data[$storageKey];
}
}
// AP-11: Produktbestand-Schwellwerte (leer => null).
$data['min_product_stock'] = isset($data['min_product_stock']) && $data['min_product_stock'] !== ''
? max(0, (int) $data['min_product_stock'])

View file

@ -9,6 +9,7 @@ use App\Models\Location;
use App\Models\MaterialQuality;
use App\Models\PackagingItem;
use App\Models\PackagingMaterial;
use App\Models\Product;
use App\Models\ProductIngredient;
use App\Models\Production;
use App\Models\ProductionIngredient;
@ -136,6 +137,9 @@ class DeletionGuard
$blockers = [];
$this->push($blockers, Ingredient::query()->where($column, $segment->id)->count(), ':n Rohstoff(e)');
// AP-51: Lagerplatz jetzt auch an Produkten und Verpackungsartikeln.
$this->push($blockers, Product::query()->where($column, $segment->id)->count(), ':n Produkt(e)');
$this->push($blockers, PackagingItem::query()->where($column, $segment->id)->count(), ':n Verpackungsartikel');
return $blockers;
}

View file

@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* AP-51: Lagerplatz-Zuordnung (Raum/Regal/Fach) auch für Produkte und
* Verpackungsartikel — gleiche Storage-Hierarchie wie bei Rohstoffen
* (StorageRoom/Shelf/Slot). Referenzen werden beim Löschen eines
* Lagerbausteins genullt (nullOnDelete).
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->foreignId('storage_room_id')->nullable()->after('weight')
->constrained('storage_rooms')->nullOnDelete();
$table->foreignId('storage_shelf_id')->nullable()->after('storage_room_id')
->constrained('storage_shelves')->nullOnDelete();
$table->foreignId('storage_slot_id')->nullable()->after('storage_shelf_id')
->constrained('storage_slots')->nullOnDelete();
});
Schema::table('packaging_items', function (Blueprint $table) {
$table->foreignId('storage_room_id')->nullable()->after('product_id')
->constrained('storage_rooms')->nullOnDelete();
$table->foreignId('storage_shelf_id')->nullable()->after('storage_room_id')
->constrained('storage_shelves')->nullOnDelete();
$table->foreignId('storage_slot_id')->nullable()->after('storage_shelf_id')
->constrained('storage_slots')->nullOnDelete();
});
}
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropConstrainedForeignId('storage_room_id');
$table->dropConstrainedForeignId('storage_shelf_id');
$table->dropConstrainedForeignId('storage_slot_id');
});
Schema::table('packaging_items', function (Blueprint $table) {
$table->dropConstrainedForeignId('storage_room_id');
$table->dropConstrainedForeignId('storage_shelf_id');
$table->dropConstrainedForeignId('storage_slot_id');
});
}
};

View file

@ -84,6 +84,37 @@
@enderror
</div>
<div class="form-row">
<div class="form-group col-sm-4">
<label class="form-label" for="storage_room_id">{{ __('Lagerort: Raum') }}</label>
<select name="storage_room_id" id="storage_room_id" class="form-control">
<option value="">{{ __('— keine Angabe —') }}</option>
@foreach($storageRooms as $room)
<option value="{{ $room->id }}" @selected((string) old('storage_room_id', $model->storage_room_id) === (string) $room->id)>{{ $room->label }}</option>
@endforeach
</select>
</div>
<div class="form-group col-sm-4">
<label class="form-label" for="storage_shelf_id">{{ __('Lagerort: Regal') }}</label>
<select name="storage_shelf_id" id="storage_shelf_id" class="form-control">
<option value="">{{ __('— keine Angabe —') }}</option>
@foreach($storageShelves as $shelf)
<option value="{{ $shelf->id }}" @selected((string) old('storage_shelf_id', $model->storage_shelf_id) === (string) $shelf->id)>{{ $shelf->label }}</option>
@endforeach
</select>
</div>
<div class="form-group col-sm-4">
<label class="form-label" for="storage_slot_id">{{ __('Lagerort: Fach / Buchstabe') }}</label>
<select name="storage_slot_id" id="storage_slot_id" class="form-control">
<option value="">{{ __('— keine Angabe —') }}</option>
@foreach($storageSlots as $slot)
<option value="{{ $slot->id }}" @selected((string) old('storage_slot_id', $model->storage_slot_id) === (string) $slot->id)>{{ $slot->label }}</option>
@endforeach
</select>
<small class="text-muted">{{ __('Bausteine zentral pflegbar unter Einstellungen → Allgemein.') }}</small>
</div>
</div>
<div class="form-group">
<label class="custom-control custom-checkbox">
<input type="checkbox" name="active" value="1" class="custom-control-input"

View file

@ -23,6 +23,7 @@
<th>{{ __('Material') }}</th>
<th>{{ __('Gewicht (g)') }}</th>
<th>{{ __('Lieferant') }}</th>
<th>{{ __('Lagerplatz') }}</th>
<th>{{ __('URL') }}</th>
<th>{{ __('Status') }}</th>
<th></th>
@ -40,6 +41,7 @@
<td>{{ $value->packagingMaterial?->name ?? '—' }}</td>
<td>{{ $value->weight_grams }}</td>
<td>{{ $value->supplier?->name ?? '—' }}</td>
<td>{{ $value->storageLabel() ?? '—' }}</td>
<td>
@if($value->url)
<a href="{{ $value->url }}" target="_blank" title="{{ $value->url }}"><i class="fa fa-external-link-alt"></i></a>

View file

@ -103,6 +103,9 @@
@if (!empty($product->number))
<div class="wawi-item-sub">{{ $product->number }}</div>
@endif
@if ($product->storageLabel())
<div class="wawi-item-sub text-muted"><span class="fas fa-map-marker-alt mr-1"></span>{{ $product->storageLabel() }}</div>
@endif
</td>
<td class="text-right {{ $stockClass }}">
{{ \App\Services\Util::formatNumber($row['stock'], 0) }} <span class="text-muted">{{ __('Stück') }}</span>

View file

@ -1140,6 +1140,38 @@
</div>
</div>
<hr>
<p class="text-muted small mb-2">{{ __('Lagerplatz (Raum / Regal / Fach). Bausteine zentral pflegbar unter Einstellungen → Allgemein.') }}</p>
<div class="form-row">
<div class="form-group col-sm-4">
<label class="form-label" for="storage_room_id">{{ __('Lagerort: Raum') }}</label>
<select name="storage_room_id" id="storage_room_id" class="form-control">
<option value="">{{ __('— keine Angabe —') }}</option>
@foreach ($storageRooms as $room)
<option value="{{ $room->id }}" @selected((string) old('storage_room_id', $product->storage_room_id) === (string) $room->id)>{{ $room->label }}</option>
@endforeach
</select>
</div>
<div class="form-group col-sm-4">
<label class="form-label" for="storage_shelf_id">{{ __('Lagerort: Regal') }}</label>
<select name="storage_shelf_id" id="storage_shelf_id" class="form-control">
<option value="">{{ __('— keine Angabe —') }}</option>
@foreach ($storageShelves as $shelf)
<option value="{{ $shelf->id }}" @selected((string) old('storage_shelf_id', $product->storage_shelf_id) === (string) $shelf->id)>{{ $shelf->label }}</option>
@endforeach
</select>
</div>
<div class="form-group col-sm-4">
<label class="form-label" for="storage_slot_id">{{ __('Lagerort: Fach / Buchstabe') }}</label>
<select name="storage_slot_id" id="storage_slot_id" class="form-control">
<option value="">{{ __('— keine Angabe —') }}</option>
@foreach ($storageSlots as $slot)
<option value="{{ $slot->id }}" @selected((string) old('storage_slot_id', $product->storage_slot_id) === (string) $slot->id)>{{ $slot->label }}</option>
@endforeach
</select>
</div>
</div>
<hr>
<p class="text-muted small mb-2">{{ __('Optional: dieses Einzelprodukt einem Hauptprodukt zuordnen (z. B. „50 × 15 ml"). Der Produktbestand führt nur Haupt-/Einzelprodukte.') }}</p>
<div class="form-row">

View file

@ -0,0 +1,103 @@
<?php
use App\Models\PackagingItem;
use App\Models\PackagingMaterial;
use App\Models\Product;
use App\Models\StorageRoom;
use App\Models\StorageShelf;
use App\Models\StorageSlot;
use App\Repositories\ProductRepository;
use App\Services\DeletionGuard;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function storageTestUser(int $adminLevel = 9): 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('AP-51: produkt speichert lagerplatz und setzt leere auswahl auf null', function () {
$room = StorageRoom::factory()->create(['label' => 'Raum 1']);
$shelf = StorageShelf::factory()->create(['label' => 'Regal 3']);
$slot = StorageSlot::factory()->create(['label' => 'A']);
$product = Product::query()->create(['name' => 'AP51 Produkt', 'title' => 'AP51 Produkt', 'active' => true, 'is_set' => false]);
(new ProductRepository($product))->update([
'id' => (string) $product->id,
'name' => $product->name,
'title' => $product->title,
'active' => '1',
'storage_room_id' => (string) $room->id,
'storage_shelf_id' => (string) $shelf->id,
'storage_slot_id' => (string) $slot->id,
]);
$product->refresh();
expect($product->storage_room_id)->toBe($room->id)
->and($product->storage_shelf_id)->toBe($shelf->id)
->and($product->storage_slot_id)->toBe($slot->id)
->and($product->storageLabel())->toBe('Raum 1 | Regal 3 | A');
// Leere Auswahl (Select „— keine Angabe —" sendet "") => null, kein FK-Fehler.
(new ProductRepository($product))->update([
'id' => (string) $product->id,
'name' => $product->name,
'title' => $product->title,
'active' => '1',
'storage_room_id' => '',
'storage_shelf_id' => '',
'storage_slot_id' => '',
]);
$product->refresh();
expect($product->storage_room_id)->toBeNull()
->and($product->storageLabel())->toBeNull();
});
test('AP-51: verpackungsartikel speichert lagerplatz und zeigt ihn an', function () {
$material = PackagingMaterial::factory()->create();
$room = StorageRoom::factory()->create(['label' => 'Raum 2']);
$slot = StorageSlot::factory()->create(['label' => 'B']);
$this->actingAs(storageTestUser(), 'user');
$this->post(route('admin.inventory.packaging-items.store'), [
'packaging_material_id' => (string) $material->id,
'name' => 'AP51 Karton',
'category' => 'packaging',
'storage_room_id' => (string) $room->id,
'storage_slot_id' => (string) $slot->id,
'active' => '1',
])->assertRedirect(route('admin.inventory.packaging-items.index', ['category' => 'packaging']));
$item = PackagingItem::query()->where('name', 'AP51 Karton')->firstOrFail();
expect($item->storage_room_id)->toBe($room->id)
->and($item->storage_shelf_id)->toBeNull()
->and($item->storageLabel())->toBe('Raum 2 | B');
// Anzeige in der Übersicht
$this->get(route('admin.inventory.packaging-items.index', ['category' => 'packaging']))
->assertOk()
->assertSee('Raum 2 | B');
});
test('AP-51: lagerbaustein mit produkt-/verpackungs-referenz ist nicht löschbar', function () {
$room = StorageRoom::factory()->create(['label' => 'Raum belegt']);
Product::query()->create(['name' => 'AP51 Ref', 'title' => 'AP51 Ref', 'active' => true, 'is_set' => false, 'storage_room_id' => $room->id]);
$blockers = app(DeletionGuard::class)->blockersFor($room);
expect($blockers)->not->toBeEmpty()
->and(implode(' ', $blockers))->toContain('Produkt');
});
test('AP-51: freier lagerbaustein bleibt löschbar', function () {
$room = StorageRoom::factory()->create(['label' => 'Raum frei']);
expect(app(DeletionGuard::class)->blockersFor($room))->toBeEmpty();
});