d2d.emegrendeles.hu/tests/Feature/PricelistGuardTest.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

134 lines
5.2 KiB
PHP

<?php
use App\Enums\PricelistFileLineStatusEnum;
use App\Enums\PricelistFileStatusEnum;
use App\Enums\PricelistWorkflowStep;
use App\Models\PricelistFile;
use App\Models\PricelistFileLine;
use App\Models\Supplier;
use App\Services\PricelistFileProcessService;
use App\Services\PricelistGuard;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function guardTestFile(Supplier $supplier, PricelistFileStatusEnum $status, string $executionStepStatus, array $attributes = []): PricelistFile
{
return PricelistFile::create(array_merge([
'filename' => 'arlista.xlsx',
'supplier_id' => $supplier->id,
'available_date' => now()->addWeek()->toDateString(),
'status' => $status,
'workflow_steps' => [
['name' => PricelistWorkflowStep::Preprocessing->value, 'label' => 'Előfeldolgozás', 'status' => 'completed'],
['name' => PricelistWorkflowStep::Validation->value, 'label' => 'Validálás', 'status' => 'completed'],
['name' => PricelistWorkflowStep::Approval->value, 'label' => 'Jóváhagyás', 'status' => 'completed'],
['name' => PricelistWorkflowStep::Execution->value, 'label' => 'Végrehajtás', 'status' => $executionStepStatus],
],
], $attributes));
}
beforeEach(function () {
$this->supplier = Supplier::factory()->create();
$this->guard = app(PricelistGuard::class);
});
test('a hibára futott végrehajtás blokkolja a beszállítót', function () {
$blocking = guardTestFile($this->supplier, PricelistFileStatusEnum::execution_failed, 'failed');
$found = $this->guard->blockingFileFor($this->supplier->id);
expect($found)->not->toBeNull()
->and($found->id)->toBe($blocking->id)
->and($this->guard->blockingMessage($found))->toContain('hibára futott');
});
test('a futó végrehajtás blokkolja a beszállítót', function () {
guardTestFile($this->supplier, PricelistFileStatusEnum::inprogress, 'inprogress');
expect($this->guard->isBlocked($this->supplier->id))->toBeTrue();
});
test('a validálás alatt álló fájl NEM blokkol', function () {
// Az előfeldolgozás és a validálás egyetlen terméket sem ír, ezért közben
// nyugodtan feltölthető másik árlista ugyanahhoz a beszállítóhoz.
guardTestFile($this->supplier, PricelistFileStatusEnum::inprogress, 'pending');
expect($this->guard->isBlocked($this->supplier->id))->toBeFalse();
});
test('a lezárt és elkészült fájlok nem blokkolnak', function () {
guardTestFile($this->supplier, PricelistFileStatusEnum::done, 'completed');
guardTestFile($this->supplier, PricelistFileStatusEnum::closed, 'reverted');
expect($this->guard->isBlocked($this->supplier->id))->toBeFalse();
});
test('a blokkolás beszállítónként külön él', function () {
guardTestFile($this->supplier, PricelistFileStatusEnum::execution_failed, 'failed');
$otherSupplier = Supplier::factory()->create();
expect($this->guard->isBlocked($this->supplier->id))->toBeTrue()
->and($this->guard->isBlocked($otherSupplier->id))->toBeFalse();
});
test('a fájl önmagát nem blokkolja', function () {
$file = guardTestFile($this->supplier, PricelistFileStatusEnum::execution_failed, 'failed');
expect($this->guard->isBlocked($this->supplier->id, $file->id))->toBeFalse();
});
test('a visszavonás után a beszállító újra felszabadul', function () {
$file = guardTestFile($this->supplier, PricelistFileStatusEnum::execution_failed, 'failed');
expect($this->guard->isBlocked($this->supplier->id))->toBeTrue();
$file->update(['status' => PricelistFileStatusEnum::closed]);
expect($this->guard->isBlocked($this->supplier->id))->toBeFalse();
});
test('blokkolt beszállítónál másik fájl nem hagyható jóvá', function () {
Queue::fake();
guardTestFile($this->supplier, PricelistFileStatusEnum::execution_failed, 'failed');
$waiting = guardTestFile($this->supplier, PricelistFileStatusEnum::waiting_for_approval, 'pending', [
'filename' => 'masodik.xlsx',
]);
PricelistFileLine::create([
'pricelist_file_id' => $waiting->id,
'row_number' => 5,
'status' => PricelistFileLineStatusEnum::new_product,
'payload' => [],
]);
// A fájl önmagában jóváhagyható lenne - csak a beszállítói blokkolás állítja meg.
expect($waiting->canBeApproved())->toBeTrue()
->and(app(PricelistFileProcessService::class)->approve($waiting))->toBeFalse();
Queue::assertNothingPushed();
expect($waiting->refresh()->status)->toBe(PricelistFileStatusEnum::waiting_for_approval);
});
test('a konzolos indítás is elakad blokkolt beszállítónál', function () {
Queue::fake();
guardTestFile($this->supplier, PricelistFileStatusEnum::execution_failed, 'failed');
$todo = guardTestFile($this->supplier, PricelistFileStatusEnum::todo, 'pending', [
'filename' => 'harmadik.xlsx',
]);
$this->artisan('pricelist:process', ['id' => $todo->id])
->expectsOutputToContain('nem indítható újabb árlista-feldolgozás')
->assertExitCode(1);
Queue::assertNothingPushed();
});