gruene-seele/app/Services/DeletionGuard.php
Kevin Adametz 190713c21b Warenwirtschaft: AP-48 — Produktbestand-Buchungsgründe pflegbar, getrennt Ein/Aus
Kundenentscheid 26.08. (D-5): Eingang und Ausgang haben eigene, pflegbare
Gründe-Listen statt einer festen Liste im Controller.

- Neues Modell/Tabelle stock_movement_reasons (label, direction in/out, pos,
  active, SoftDeletes) inkl. Seed der bisherigen Gründe sinnvoll auf Ein-/Ausgang
  aufgeteilt.
- CRUD wie DisposalReason (SettingCrudController + FormRequest + Formular mit
  Richtungs-Auswahl); zwei Listen (Eingangs-/Ausgangs-Gründe) auf der Seite
  Einstellungen → Allgemeines, „Neu anlegen" mit vorbelegter Richtung.
- Produktbestand-Maske: das Grund-Dropdown wird jetzt je nach Eingang/Ausgang
  per JS aus den passenden, gepflegten Gründen befüllt (manualReasons entfernt).
- DeletionGuard: StockMovementReason immer löschbar.

Migration create_stock_movement_reasons auf Dev-/Live-DB. 4 Tests, Suite 326 grün.

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

232 lines
8.3 KiB
PHP

<?php
namespace App\Services;
use App\Models\DeliveryTime;
use App\Models\DisposalReason;
use App\Models\Ingredient;
use App\Models\Location;
use App\Models\MaterialQuality;
use App\Models\PackagingItem;
use App\Models\PackagingMaterial;
use App\Models\ProductIngredient;
use App\Models\Production;
use App\Models\ProductionIngredient;
use App\Models\ProductionPackaging;
use App\Models\StockDisposal;
use App\Models\StockEntry;
use App\Models\StockMovementReason;
use App\Models\StorageSegment;
use App\Models\Supplier;
use App\Models\SupplierCategory;
use App\Models\TaxRate;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
/**
* Zentrale Abhängigkeitsprüfung vor dem Löschen von WaWi-Einträgen
* (Anforderung 15.07.2026): Referenzierte Einträge dürfen nicht gelöscht
* werden. Liefert je Blocker eine deutschsprachige Begründung; leeres
* Array = Löschen erlaubt. Soft-gelöschte Referenzen zählen nicht
* (SoftDeletes-Global-Scope bzw. explizite whereNull-Joins).
*/
class DeletionGuard
{
/**
* @return array<int, string>
*/
public function blockersFor(Model $model): array
{
return match (true) {
$model instanceof TaxRate => $this->taxRateBlockers($model),
$model instanceof MaterialQuality => $this->materialQualityBlockers($model),
$model instanceof Location => $this->locationBlockers($model),
$model instanceof SupplierCategory => $this->supplierCategoryBlockers($model),
$model instanceof PackagingMaterial => $this->packagingMaterialBlockers($model),
$model instanceof StorageSegment => $this->storageSegmentBlockers($model),
$model instanceof Ingredient => $this->ingredientBlockers($model),
$model instanceof StockEntry => $this->stockEntryBlockers($model),
$model instanceof Supplier => $this->supplierBlockers($model),
$model instanceof PackagingItem => $this->packagingItemBlockers($model),
// Bewusst immer löschbar (keine eingehenden Referenzen):
$model instanceof DeliveryTime,
$model instanceof DisposalReason,
$model instanceof StockMovementReason,
$model instanceof StockDisposal,
$model instanceof Production => [],
default => [],
};
}
/**
* @return array<int, string>
*/
protected function taxRateBlockers(TaxRate $taxRate): array
{
$blockers = [];
$this->push($blockers, Ingredient::query()->where('tax_rate_id', $taxRate->id)->count(), ':n Rohstoff(e)');
$this->push($blockers, StockEntry::query()->where('tax_rate_id', $taxRate->id)->count(), ':n Wareneingänge/Bestellungen');
return $blockers;
}
/**
* @return array<int, string>
*/
protected function materialQualityBlockers(MaterialQuality $quality): array
{
$blockers = [];
$this->push($blockers, Ingredient::query()->where('material_quality_id', $quality->id)->count(), ':n Rohstoff(e)');
$this->push($blockers, StockEntry::query()->where('quality_id', $quality->id)->count(), ':n Wareneingänge/Bestellungen');
return $blockers;
}
/**
* @return array<int, string>
*/
protected function locationBlockers(Location $location): array
{
$blockers = [];
$this->push($blockers, StockEntry::query()->where('location_id', $location->id)->count(), ':n Wareneingänge/Bestellungen');
$this->push($blockers, Production::query()->where('location_id', $location->id)->count(), ':n Produktion(en)');
$this->push($blockers, StockDisposal::query()->where('location_id', $location->id)->count(), ':n Ausschuss-Buchung(en)');
return $blockers;
}
/**
* @return array<int, string>
*/
protected function supplierCategoryBlockers(SupplierCategory $category): array
{
$count = DB::table('supplier_supplier_category')
->join('suppliers', 'suppliers.id', '=', 'supplier_supplier_category.supplier_id')
->whereNull('suppliers.deleted_at')
->where('supplier_supplier_category.supplier_category_id', $category->id)
->count();
$blockers = [];
$this->push($blockers, $count, ':n Lieferant(en)');
return $blockers;
}
/**
* @return array<int, string>
*/
protected function packagingMaterialBlockers(PackagingMaterial $material): array
{
$blockers = [];
$this->push($blockers, PackagingItem::query()->where('packaging_material_id', $material->id)->count(), ':n Verpackungsartikel');
return $blockers;
}
/**
* @return array<int, string>
*/
protected function storageSegmentBlockers(StorageSegment $segment): array
{
$column = match ($segment->getTable()) {
'storage_rooms' => 'storage_room_id',
'storage_shelves' => 'storage_shelf_id',
'storage_slots' => 'storage_slot_id',
};
$blockers = [];
$this->push($blockers, Ingredient::query()->where($column, $segment->id)->count(), ':n Rohstoff(e)');
return $blockers;
}
/**
* @return array<int, string>
*/
protected function ingredientBlockers(Ingredient $ingredient): array
{
$blockers = [];
$this->push($blockers, ProductIngredient::query()->where('ingredient_id', $ingredient->id)->count(), ':n Rezeptur-Zeile(n)');
$this->push($blockers, StockEntry::query()->where('ingredient_id', $ingredient->id)->count(), ':n Wareneingänge/Bestellungen');
$this->push($blockers, StockDisposal::query()->where('ingredient_id', $ingredient->id)->count(), ':n Ausschuss-Buchung(en)');
$this->push(
$blockers,
ProductionIngredient::query()->where('ingredient_id', $ingredient->id)->whereHas('production')->count(),
':n Produktions-Verbräuche'
);
return $blockers;
}
/**
* @return array<int, string>
*/
protected function stockEntryBlockers(StockEntry $stockEntry): array
{
$blockers = [];
$this->push(
$blockers,
ProductionIngredient::query()->where('stock_entry_id', $stockEntry->id)->whereHas('production')->count(),
':n Produktions-Verbräuche'
);
$this->push($blockers, StockDisposal::query()->where('stock_entry_id', $stockEntry->id)->count(), ':n Ausschuss-Buchung(en)');
return $blockers;
}
/**
* @return array<int, string>
*/
protected function supplierBlockers(Supplier $supplier): array
{
$blockers = [];
$this->push($blockers, StockEntry::query()->where('supplier_id', $supplier->id)->count(), ':n Wareneingänge/Bestellungen');
return $blockers;
}
/**
* @return array<int, string>
*/
protected function packagingItemBlockers(PackagingItem $item): array
{
$blockers = [];
$this->push($blockers, StockEntry::query()->where('packaging_item_id', $item->id)->count(), ':n Wareneingänge/Bestellungen');
$this->push($blockers, StockDisposal::query()->where('packaging_item_id', $item->id)->count(), ':n Ausschuss-Buchung(en)');
$this->push(
$blockers,
ProductionPackaging::query()->where('packaging_item_id', $item->id)->whereHas('production')->count(),
':n Produktions-Verbräuche'
);
$bomCount = DB::table('product_packagings')
->join('products', 'products.id', '=', 'product_packagings.product_id')
->whereNull('products.deleted_at')
->where('product_packagings.packaging_item_id', $item->id)
->count();
$this->push($blockers, $bomCount, ':n Produkt-Stückliste(n)');
return $blockers;
}
/**
* Formatiert die Lösch-Blocker als Fehlermeldung fürs Flash-Alert.
*
* @param array<int, string> $blockers
*/
public function message(array $blockers): string
{
return __('Löschen nicht möglich — der Eintrag wird noch verwendet: :list', [
'list' => implode(', ', $blockers),
]);
}
/**
* @param array<int, string> $blockers
*/
protected function push(array &$blockers, int $count, string $template): void
{
if ($count > 0) {
$blockers[] = str_replace(':n', (string) $count, __($template));
}
}
}