d2d.emegrendeles.hu/app/Services/PricelistGuard.php
E98Developer 8bcfeba0f9 ADD EV3-357 Árlista feldolgozás phase5/5 beszállítói blokkolás (PricelistGuard)
Egy beszállítóhoz egyszerre legfeljebb egy nyitott végrehajtás tartozhat: egy
execution_failed fájl után a termékek félig frissített állapotban vannak, egy új
import erre a kevert alapra rétegződne rá (és a validálás is ehhez számolná a
diffeket); két párhuzamos végrehajtás pedig nem determinisztikus eredményt adna.

- új PricelistGuard service: egyetlen igazságforrás a blokkoláshoz
- mind a NÉGY belépési pont véd: modern Filament create, régebbi
  PriceListProcessor oldal, legacy Admin\PriceListController import, konzol
  parancs. A legacy import a legfontosabb - az közvetlenül ír a products
  táblába, megkerülve a modult, és a beszállítókat ma még nagyrészt ott kezelik.
- az `inprogress` önmagában nem blokkol: az előfeldolgozás és a validálás
  egyetlen terméket sem ír, csak a már elindult végrehajtás számít
- a beszállító nem tűnik el a select listából, hanem konkrét magyarázatot kap a
  felhasználó arról, melyik fájl blokkol és miért
- blokkolt beszállítónál másik fájl jóváhagyása sem indítható

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 06:46:40 +02:00

79 lines
3.4 KiB
PHP

<?php
namespace App\Services;
use App\Enums\PricelistFileStatusEnum;
use App\Models\PricelistFile;
/**
* Egy beszállítóhoz egyszerre legfeljebb egy "nyitott" végrehajtás tartozhat.
*
* Amíg egy fájl végrehajtása hibára futott (`execution_failed`), a beszállító
* termékei félig frissített állapotban vannak: egy részük már az új árlista adatait
* hordozza, más részük még a régit. Ha erre az állapotra új árlistát vinnénk fel, a
* validálás ehhez a kevert állapothoz számolná a diffeket - egyes sorok tévesen
* "rendben"-nek látszanának, mások fölöslegesen "módosítás"-nak -, az import pedig
* egy inkonzisztens alapra rétegződne rá.
*
* A futó (`inprogress`) végrehajtás blokkolása még közvetlenebb: két párhuzamos
* import ugyanarra a termékhalmazra nem determinisztikus végeredményt adna.
*
* FONTOS: ezt a védelmet MINDEN belépési ponton alkalmazni kell (modern Filament
* felület, régebbi PriceListProcessor oldal, legacy admin import, konzol parancs) -
* ha csak az űrlapot védjük, a blokkolás látszatvédelem marad, mert a beszállítókat
* ma még nagyrészt a legacy felületen kezelik.
*/
class PricelistGuard
{
/**
* A beszállítót blokkoló árlista fájl, ha van ilyen.
*
* @param int|null $exceptFileId ezt a fájlt hagyjuk figyelmen kívül (önmagát ne blokkolja)
*/
public function blockingFileFor(?int $supplierId, ?int $exceptFileId = null): ?PricelistFile
{
if (! $supplierId) {
return null;
}
return PricelistFile::query()
->where('supplier_id', $supplierId)
->when($exceptFileId, fn ($query) => $query->whereKeyNot($exceptFileId))
->whereIn('status', [
PricelistFileStatusEnum::execution_failed->value,
PricelistFileStatusEnum::inprogress->value,
])
->latest('id')
->get()
// Az `inprogress` önmagában nem blokkol: az előfeldolgozás és a validálás
// egyetlen terméket sem ír, ezért közben nyugodtan feltölthető másik fájl.
// Csak az számít, ahol a végrehajtás már elindult - ezt a workflow_steps
// JSON tartalma dönti el, amit SQL-ben nem szűrnénk tisztán, a beszállítónkénti
// jelöltek száma viszont elenyésző.
->first(fn (PricelistFile $file) => $file->status === PricelistFileStatusEnum::execution_failed
|| $file->hasExecutionStarted());
}
public function isBlocked(?int $supplierId, ?int $exceptFileId = null): bool
{
return $this->blockingFileFor($supplierId, $exceptFileId) !== null;
}
/**
* Felhasználónak szóló magyarázat: melyik fájl blokkol és miért.
*/
public function blockingMessage(PricelistFile $blockingFile): string
{
$state = $blockingFile->status === PricelistFileStatusEnum::execution_failed
? 'végrehajtása hibára futott'
: 'végrehajtása jelenleg fut';
return sprintf(
'A beszállítóhoz tartozó "%s" árlista (#%d) %s. Amíg ez nincs lezárva (folytatás vagy visszavonás), nem indítható újabb árlista-feldolgozás, mert a termékadatok félig frissített állapotban lehetnek.',
$blockingFile->filename,
$blockingFile->id,
$state,
);
}
}