d2d.emegrendeles.hu/tests/Feature/PricelistApprovalTest.php
E98Developer eee27aa659 ADD EV3-357 Árlista feldolgozás phase5/2 jóváhagyás/elutasítás akció
- PricelistFile::canBeApproved() / hasExecutionStarted(): a jóváhagyás feltételei
  egy helyen, hogy a felületi gomb és a service ugyanazt ellenőrizze
- approve(): a státusz MÉG A DISPATCH ELŐTT billen át, különben a queue-latency
  alatt a gomb látszana és egy második kattintás párhuzamos importot indítana
- reject(): új 'rejected' lépésstátusz - nem 'failed', mert azt az
  updateStepStatus fail fájlstátuszra fordítaná, az elutasítás viszont closed
- a döntés (ki, mikor, milyen indokkal) a file_meta.approval-ba kerül, mert a
  BaseAuditable a nevével ellentétben nem naplóz
- Edit gomb szűkítése hasExecutionStarted()-tel három helyen, köztük az
  EditPricelistFile::afterSave()-ben, ami a teljes láncot újraindítja

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

206 lines
7.1 KiB
PHP

<?php
use App\Enums\PricelistFileLineStatusEnum;
use App\Enums\PricelistFileStatusEnum;
use App\Enums\PricelistWorkflowStep;
use App\Filament\Resources\PricelistFiles\Pages\ViewPricelistFile;
use App\Jobs\PricelistExecutionJob;
use App\Models\FeatureFlag;
use App\Models\PricelistFile;
use App\Models\PricelistFileLine;
use App\Models\Role;
use App\Models\Supplier;
use App\Models\User;
use App\Services\FeatureFlagRegistrar;
use App\Services\PricelistFileProcessService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
/**
* Jóváhagyásra váró fájl: a validálás lezárult, a jóváhagyás a soron következő lépés.
*/
function approvableFile(array $attributes = []): PricelistFile
{
return PricelistFile::create(array_merge([
'filename' => 'arlista.xlsx',
'supplier_id' => Supplier::factory()->create()->id,
'available_date' => now()->addWeek(),
'status' => PricelistFileStatusEnum::waiting_for_approval,
'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' => 'inprogress'],
['name' => PricelistWorkflowStep::Execution->value, 'label' => 'Végrehajtás', 'status' => 'pending'],
],
], $attributes));
}
function addLine(PricelistFile $file, PricelistFileLineStatusEnum $status, int $rowNumber = 5): PricelistFileLine
{
return PricelistFileLine::create([
'pricelist_file_id' => $file->id,
'row_number' => $rowNumber,
'status' => $status,
'payload' => [],
]);
}
function developerUser(): User
{
$user = User::factory()->create();
$user->addRole(Role::create(['name' => 'developer', 'display_name' => 'Developer']));
return $user;
}
function definePricelistExecutionFlag(array $attributes = []): void
{
FeatureFlag::create(array_merge([
'name' => 'PricelistExecution',
'label' => 'Árlista végrehajtás',
'enabled' => true,
'stages' => null,
'roles' => ['developer'],
], $attributes));
app(FeatureFlagRegistrar::class)->registerAll();
}
test('a hibátlan, jóváhagyásra váró fájl jóváhagyható', function () {
$file = approvableFile();
addLine($file, PricelistFileLineStatusEnum::new_product);
addLine($file, PricelistFileLineStatusEnum::updated, 6);
expect($file->canBeApproved())->toBeTrue();
});
test('hibás sor esetén nem hagyható jóvá', function () {
$file = approvableFile();
addLine($file, PricelistFileLineStatusEnum::ok);
addLine($file, PricelistFileLineStatusEnum::error, 6);
expect($file->canBeApproved())->toBeFalse();
});
test('nem jóváhagyásra váró státuszban nem hagyható jóvá', function () {
$file = approvableFile(['status' => PricelistFileStatusEnum::inprogress]);
expect($file->canBeApproved())->toBeFalse();
});
test('befejezetlen validálás esetén nem hagyható jóvá', function () {
$file = approvableFile([
'workflow_steps' => [
['name' => PricelistWorkflowStep::Validation->value, 'label' => 'Validálás', 'status' => 'failed'],
['name' => PricelistWorkflowStep::Approval->value, 'label' => 'Jóváhagyás', 'status' => 'pending'],
],
]);
expect($file->canBeApproved())->toBeFalse();
});
test('a jóváhagyás elindítja a végrehajtást és azonnal átbillenti a státuszt', function () {
Queue::fake();
$file = approvableFile();
addLine($file, PricelistFileLineStatusEnum::new_product);
expect(app(PricelistFileProcessService::class)->approve($file))->toBeTrue();
Queue::assertPushed(PricelistExecutionJob::class);
$file->refresh();
// A státusz még a dispatch előtt átbillen, különben a queue-latency alatt a
// Jóváhagyás gomb látszana, és egy második kattintás párhuzamos importot indítana.
expect($file->status)->toBe(PricelistFileStatusEnum::inprogress)
->and($file->stepStatus(PricelistWorkflowStep::Approval))->toBe('completed')
->and($file->stepStatus(PricelistWorkflowStep::Execution))->toBe('inprogress')
->and($file->file_meta['approval']['decision'])->toBe('approved');
});
test('a már nem jóváhagyható fájlra a jóváhagyás nem indít végrehajtást', function () {
Queue::fake();
$file = approvableFile();
addLine($file, PricelistFileLineStatusEnum::error);
expect(app(PricelistFileProcessService::class)->approve($file))->toBeFalse();
Queue::assertNothingPushed();
expect($file->refresh()->status)->toBe(PricelistFileStatusEnum::waiting_for_approval);
});
test('az elutasítás lezárja a fájlt indoklással, végrehajtás nélkül', function () {
Queue::fake();
$file = approvableFile();
addLine($file, PricelistFileLineStatusEnum::updated);
expect(app(PricelistFileProcessService::class)->reject($file, 'Rossz árlistát töltöttek fel.'))->toBeTrue();
Queue::assertNothingPushed();
$file->refresh();
expect($file->status)->toBe(PricelistFileStatusEnum::closed)
->and($file->stepStatus(PricelistWorkflowStep::Approval))->toBe('rejected')
->and($file->file_meta['approval']['reason'])->toBe('Rossz árlistát töltöttek fel.');
});
test('a végrehajtás elindulása után a fájl nem tölthető újra', function () {
$file = approvableFile();
expect($file->hasExecutionStarted())->toBeFalse();
app(PricelistFileProcessService::class)->updateStepStatus($file, PricelistWorkflowStep::Execution, 'failed', 'Hiba.');
expect($file->refresh()->hasExecutionStarted())->toBeTrue();
});
test('a döntési gombok csak aktív flaggel látszanak', function () {
definePricelistExecutionFlag();
$file = approvableFile();
addLine($file, PricelistFileLineStatusEnum::new_product);
$this->actingAs(developerUser());
Livewire::test(ViewPricelistFile::class, ['record' => $file->id])
->assertActionVisible('approve')
->assertActionVisible('reject');
});
test('a döntési gombok rejtve maradnak a flag nélküli felhasználónak', function () {
definePricelistExecutionFlag();
$file = approvableFile();
addLine($file, PricelistFileLineStatusEnum::new_product);
$user = User::factory()->create();
$user->addRole(Role::create(['name' => 'admin', 'display_name' => 'Admin']));
$this->actingAs($user);
Livewire::test(ViewPricelistFile::class, ['record' => $file->id])
->assertActionHidden('approve')
->assertActionHidden('reject');
});
test('hibás sor esetén a döntési gombok rejtve maradnak flaggel is', function () {
definePricelistExecutionFlag();
$file = approvableFile();
addLine($file, PricelistFileLineStatusEnum::error);
$this->actingAs(developerUser());
Livewire::test(ViewPricelistFile::class, ['record' => $file->id])
->assertActionHidden('approve')
->assertActionHidden('reject');
});