add preprocessing workflow and meta data
This commit is contained in:
parent
67c108eeca
commit
dd22d93d16
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,3 +22,4 @@
|
|||||||
Homestead.json
|
Homestead.json
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
/storage/tmp/
|
||||||
|
|||||||
@ -36,8 +36,10 @@ public function handle(PricelistFileProcessService $service): int
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->info("Folyamat elindítása: {$pricelistFile->filename} (ID: {$id})");
|
$this->info("Folyamat elindítása: {$pricelistFile->filename} (ID: {$id})");
|
||||||
|
$this->info("Workflow állapotok alaphelyzetbe állítása...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
$service->resetWorkflow($pricelistFile);
|
||||||
$service->dispatchInitialChain($pricelistFile);
|
$service->dispatchInitialChain($pricelistFile);
|
||||||
$this->info("Az árlista feldolgozási lánc (Pre-process -> Validation) sikeresen ütemezve.");
|
$this->info("Az árlista feldolgozási lánc (Pre-process -> Validation) sikeresen ütemezve.");
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
53
app/Enums/PreProcessErrorCode.php
Normal file
53
app/Enums/PreProcessErrorCode.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum PreProcessErrorCode: string
|
||||||
|
{
|
||||||
|
case FILE_NOT_FOUND = 'FILE_NOT_FOUND';
|
||||||
|
case FILE_CORRUPTED = 'FILE_CORRUPTED';
|
||||||
|
case FILE_TOO_LARGE = 'FILE_TOO_LARGE';
|
||||||
|
case FILE_EMPTY = 'FILE_EMPTY';
|
||||||
|
case INVALID_EXTENSION = 'INVALID_EXTENSION';
|
||||||
|
case NO_WORKSHEET = 'NO_WORKSHEET';
|
||||||
|
case COLUMN_COUNT_MISMATCH = 'COLUMN_COUNT_MISMATCH';
|
||||||
|
case COLUMN_MISSING = 'COLUMN_MISSING';
|
||||||
|
case COLUMN_EXTRA = 'COLUMN_EXTRA';
|
||||||
|
case HEADER_MISMATCH = 'HEADER_MISMATCH';
|
||||||
|
case NO_DATA_ROWS = 'NO_DATA_ROWS';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::FILE_NOT_FOUND => 'A fájl nem található a szerveren.',
|
||||||
|
self::FILE_CORRUPTED => 'A fájl sérült vagy nem nyitható meg.',
|
||||||
|
self::FILE_TOO_LARGE => 'A fájl mérete meghaladja a megengedett határt.',
|
||||||
|
self::FILE_EMPTY => 'A fájl üres.',
|
||||||
|
self::INVALID_EXTENSION => 'A fájl kiterjesztése nem megfelelő.',
|
||||||
|
self::NO_WORKSHEET => 'Az Excel fájl nem tartalmaz munkalapot.',
|
||||||
|
self::COLUMN_COUNT_MISMATCH => 'Az oszlopok száma nem egyezik az elvárt struktúrával.',
|
||||||
|
self::COLUMN_MISSING => 'Hiányzó oszlop(ok) az elvárt struktúrából.',
|
||||||
|
self::COLUMN_EXTRA => 'Többlet oszlop(ok) az elvárt struktúrához képest.',
|
||||||
|
self::HEADER_MISMATCH => 'Egy vagy több oszlopfejléc nem egyezik az elvárttal.',
|
||||||
|
self::NO_DATA_ROWS => 'A fájl nem tartalmaz feldolgozható adatsort.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function severity(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::FILE_NOT_FOUND,
|
||||||
|
self::FILE_CORRUPTED,
|
||||||
|
self::FILE_EMPTY,
|
||||||
|
self::INVALID_EXTENSION,
|
||||||
|
self::NO_WORKSHEET,
|
||||||
|
self::COLUMN_COUNT_MISMATCH,
|
||||||
|
self::HEADER_MISMATCH,
|
||||||
|
self::NO_DATA_ROWS => 'blocker',
|
||||||
|
|
||||||
|
self::COLUMN_MISSING => 'error',
|
||||||
|
self::COLUMN_EXTRA,
|
||||||
|
self::FILE_TOO_LARGE => 'warning',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,89 +15,122 @@ public static function configure(Schema $schema): Schema
|
|||||||
{
|
{
|
||||||
return $schema
|
return $schema
|
||||||
->components([
|
->components([
|
||||||
Section::make('Fájl információk')
|
Section::make(null)
|
||||||
->poll('5s')
|
->poll('5s')
|
||||||
->schema([
|
->schema([
|
||||||
Grid::make(3)
|
Section::make('Fájl információk')
|
||||||
->schema([
|
->schema([
|
||||||
TextEntry::make('filename')
|
Grid::make(3)
|
||||||
->label('Fájlnév')
|
->schema([
|
||||||
->icon('heroicon-o-document-text')
|
TextEntry::make('filename')
|
||||||
->color('primary'),
|
->label('Fájlnév')
|
||||||
TextEntry::make('supplier.name')
|
->icon('heroicon-o-document-text')
|
||||||
->label('Beszállító')
|
->color('primary'),
|
||||||
->icon('heroicon-o-user'),
|
TextEntry::make('supplier.name')
|
||||||
TextEntry::make('status')
|
->label('Beszállító')
|
||||||
->label('Státusz')
|
->icon('heroicon-o-user'),
|
||||||
->badge()
|
TextEntry::make('status')
|
||||||
->formatStateUsing(fn (PricelistFileStatusEnum $state): string => $state->label())
|
->label('Státusz')
|
||||||
->color(fn (PricelistFileStatusEnum $state): string => match ($state) {
|
->badge()
|
||||||
PricelistFileStatusEnum::todo => 'gray',
|
->formatStateUsing(fn (PricelistFileStatusEnum $state): string => $state->label())
|
||||||
PricelistFileStatusEnum::inprogress => 'info',
|
->color(fn (PricelistFileStatusEnum $state): string => match ($state) {
|
||||||
PricelistFileStatusEnum::done => 'success',
|
PricelistFileStatusEnum::todo => 'gray',
|
||||||
PricelistFileStatusEnum::fail => 'danger',
|
PricelistFileStatusEnum::inprogress => 'info',
|
||||||
PricelistFileStatusEnum::waiting_for_approval => 'primary',
|
PricelistFileStatusEnum::done => 'success',
|
||||||
PricelistFileStatusEnum::closed => 'warning',
|
PricelistFileStatusEnum::fail => 'danger',
|
||||||
}),
|
PricelistFileStatusEnum::waiting_for_approval => 'primary',
|
||||||
|
PricelistFileStatusEnum::closed => 'warning',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
Grid::make(3)
|
||||||
|
->schema([
|
||||||
|
TextEntry::make('available_date')
|
||||||
|
->label('Életbelépés dátuma')
|
||||||
|
->date('Y.m.d'),
|
||||||
|
TextEntry::make('created_at')
|
||||||
|
->label('Feltöltés időpontja')
|
||||||
|
->dateTime('Y.m.d H:i'),
|
||||||
|
TextEntry::make('processing_current_step')
|
||||||
|
->label('Aktuális folyamat')
|
||||||
|
->weight('bold')
|
||||||
|
->color('info')
|
||||||
|
->formatStateUsing(fn ($state, $record) => "$state ({$record->processing_current_step_percentage}%)"),
|
||||||
|
]),
|
||||||
|
TextEntry::make('note')
|
||||||
|
->label('Megjegyzés')
|
||||||
|
->placeholder('Nincs megjegyzés')
|
||||||
|
->columnSpanFull(),
|
||||||
]),
|
]),
|
||||||
Grid::make(3)
|
|
||||||
->schema([
|
|
||||||
TextEntry::make('available_date')
|
|
||||||
->label('Életbelépés dátuma')
|
|
||||||
->date('Y.m.d'),
|
|
||||||
TextEntry::make('created_at')
|
|
||||||
->label('Feltöltés időpontja')
|
|
||||||
->dateTime('Y.m.d H:i'),
|
|
||||||
TextEntry::make('processing_current_step')
|
|
||||||
->label('Aktuális folyamat')
|
|
||||||
->weight('bold')
|
|
||||||
->color('info')
|
|
||||||
->formatStateUsing(fn ($state, $record) => "$state ({$record->processing_current_step_percentage}%)"),
|
|
||||||
]),
|
|
||||||
TextEntry::make('note')
|
|
||||||
->label('Megjegyzés')
|
|
||||||
->placeholder('Nincs megjegyzés')
|
|
||||||
->columnSpanFull(),
|
|
||||||
]),
|
|
||||||
|
|
||||||
Section::make('Feldolgozási folyamat (Workflow)')
|
Section::make('Előfeldolgozás eredménye')
|
||||||
->poll('5s')
|
|
||||||
->schema([
|
|
||||||
RepeatableEntry::make('workflow_steps')
|
|
||||||
->label(false)
|
|
||||||
->schema([
|
->schema([
|
||||||
TextEntry::make('label')
|
Grid::make(4)->schema([
|
||||||
->label('Lépés')
|
TextEntry::make('file_meta.preprocess.statistics.data_rows')
|
||||||
->weight('bold')
|
->label('Adatsorok')
|
||||||
->inlineLabel(),
|
->icon('heroicon-o-table-cells'),
|
||||||
TextEntry::make('status')
|
TextEntry::make('file_meta.preprocess.statistics.column_count')
|
||||||
->label('Állapot')
|
->label('Oszlopok száma'),
|
||||||
->badge()
|
TextEntry::make('file_meta.preprocess.file_info.file_size_human')
|
||||||
->inlineLabel()
|
->label('Fájlméret'),
|
||||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
TextEntry::make('file_meta.preprocess.duration_seconds')
|
||||||
'pending' => 'várakozik',
|
->label('Feldolgozási idő')
|
||||||
'inprogress' => 'folyamatban',
|
->suffix(' mp'),
|
||||||
'completed' => 'kész',
|
]),
|
||||||
'failed' => 'hiba',
|
RepeatableEntry::make('file_meta.preprocess.warnings')
|
||||||
default => $state,
|
->label('Figyelmeztetések')
|
||||||
})
|
->schema([
|
||||||
->color(fn (string $state): string => match ($state) {
|
TextEntry::make('message')->label('Leírás')->color('warning'),
|
||||||
'pending' => 'gray',
|
TextEntry::make('severity')->label('Súlyosság')->badge()->color('warning'),
|
||||||
'inprogress' => 'info',
|
])
|
||||||
'completed' => 'success',
|
->visible(fn ($record) => !empty($record->file_meta['preprocess']['warnings'] ?? [])),
|
||||||
'failed' => 'danger',
|
RepeatableEntry::make('file_meta.preprocess.errors')
|
||||||
default => 'gray',
|
->label('Hibák')
|
||||||
})
|
->schema([
|
||||||
->icon(fn (string $state): string => match ($state) {
|
TextEntry::make('message')->label('Leírás')->color('danger'),
|
||||||
'pending' => 'heroicon-o-clock',
|
TextEntry::make('severity')->label('Súlyosság')->badge()->color('danger'),
|
||||||
'inprogress' => 'heroicon-o-arrow-path',
|
])
|
||||||
'completed' => 'heroicon-o-check-circle',
|
->visible(fn ($record) => !empty($record->file_meta['preprocess']['errors'] ?? [])),
|
||||||
'failed' => 'heroicon-o-x-circle',
|
|
||||||
default => 'heroicon-o-question-mark-circle',
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
->columns(2)
|
->visible(fn ($record) => !empty($record->file_meta['preprocess'] ?? [])),
|
||||||
->columnSpanFull(),
|
|
||||||
|
Section::make('Feldolgozási folyamat (Workflow)')
|
||||||
|
->schema([
|
||||||
|
RepeatableEntry::make('workflow_steps')
|
||||||
|
->label(false)
|
||||||
|
->schema([
|
||||||
|
TextEntry::make('label')
|
||||||
|
->label('Lépés')
|
||||||
|
->weight('bold')
|
||||||
|
->inlineLabel(),
|
||||||
|
TextEntry::make('status')
|
||||||
|
->label('Állapot')
|
||||||
|
->badge()
|
||||||
|
->inlineLabel()
|
||||||
|
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||||
|
'pending' => 'várakozik',
|
||||||
|
'inprogress' => 'folyamatban',
|
||||||
|
'completed' => 'kész',
|
||||||
|
'failed' => 'hiba',
|
||||||
|
default => $state,
|
||||||
|
})
|
||||||
|
->color(fn (string $state): string => match ($state) {
|
||||||
|
'pending' => 'gray',
|
||||||
|
'inprogress' => 'info',
|
||||||
|
'completed' => 'success',
|
||||||
|
'failed' => 'danger',
|
||||||
|
default => 'gray',
|
||||||
|
})
|
||||||
|
->icon(fn (string $state): string => match ($state) {
|
||||||
|
'pending' => 'heroicon-o-clock',
|
||||||
|
'inprogress' => 'heroicon-o-arrow-path',
|
||||||
|
'completed' => 'heroicon-o-check-circle',
|
||||||
|
'failed' => 'heroicon-o-x-circle',
|
||||||
|
default => 'heroicon-o-question-mark-circle',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->columns(2)
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ class PricelistFile extends BaseAuditable
|
|||||||
'processing_current_step_percentage' => 'integer',
|
'processing_current_step_percentage' => 'integer',
|
||||||
'available_date' => 'date',
|
'available_date' => 'date',
|
||||||
'workflow_steps' => 'array',
|
'workflow_steps' => 'array',
|
||||||
|
'file_meta' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected static function booted()
|
protected static function booted()
|
||||||
|
|||||||
@ -78,6 +78,36 @@ public function getError()
|
|||||||
return $this->error->toArray();
|
return $this->error->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getExcelFieldPointer(): array
|
||||||
|
{
|
||||||
|
return $this->excelFieldPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public const EXPECTED_HEADERS = [
|
||||||
|
0 => 'Szállítói cikkszám',
|
||||||
|
1 => 'Fő termékcsoport',
|
||||||
|
2 => 'Alcsoport 1',
|
||||||
|
3 => 'Alcsoport 2',
|
||||||
|
4 => 'Termék megnevezése',
|
||||||
|
5 => 'Kiszerelés',
|
||||||
|
6 => 'Súly/Űrtartalom (nettó)',
|
||||||
|
7 => 'Mértékegység',
|
||||||
|
8 => 'Gyártó',
|
||||||
|
9 => 'Legkisebb eladási egység',
|
||||||
|
10 => 'Egységszorzó',
|
||||||
|
11 => 'Mennyiségi egység',
|
||||||
|
12 => 'ÁFA %',
|
||||||
|
13 => 'Előző számlázási nettó ár/mennyiségi egység',
|
||||||
|
14 => 'Új számlázási nettó ár/mennyiségi egység',
|
||||||
|
15 => 'Áráltozás %',
|
||||||
|
16 => 'Fix Hooreyca ID',
|
||||||
|
17 => 'Hooreyca mértékegység',
|
||||||
|
18 => 'Hooreyca mennyiségi szorzó',
|
||||||
|
19 => 'Vevői megnevezés',
|
||||||
|
20 => 'Megjegyzés',
|
||||||
|
21 => 'KREL',
|
||||||
|
];
|
||||||
|
|
||||||
private array $excelFieldPointer = [
|
private array $excelFieldPointer = [
|
||||||
'supplierProductNumber' => 0,
|
'supplierProductNumber' => 0,
|
||||||
'productGroupMain' => 1,
|
'productGroupMain' => 1,
|
||||||
|
|||||||
@ -2,12 +2,17 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Enums\PreProcessErrorCode;
|
||||||
use App\Enums\PricelistWorkflowStep;
|
use App\Enums\PricelistWorkflowStep;
|
||||||
use App\Models\PricelistFile;
|
use App\Models\PricelistFile;
|
||||||
use App\Enums\PricelistFileStatusEnum;
|
use App\Enums\PricelistFileStatusEnum;
|
||||||
use Illuminate\Support\Facades\Bus;
|
use Illuminate\Support\Facades\Bus;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
|
||||||
class PricelistFileProcessService
|
class PricelistFileProcessService
|
||||||
{
|
{
|
||||||
@ -15,6 +20,20 @@ public function __construct(
|
|||||||
protected PriceListService $priceListService
|
protected PriceListService $priceListService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alaphelyzetbe állítja a workflow-t és az állapotot az újraindításhoz
|
||||||
|
*/
|
||||||
|
public function resetWorkflow(PricelistFile $pricelistFile): void
|
||||||
|
{
|
||||||
|
$pricelistFile->update([
|
||||||
|
'workflow_steps' => PricelistWorkflowStep::defaultSteps(),
|
||||||
|
'status' => PricelistFileStatusEnum::todo,
|
||||||
|
'processing_current_step' => null,
|
||||||
|
'processing_current_step_percentage' => null,
|
||||||
|
'file_meta' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Elindítja az árlista feldolgozás kezdeti láncát (Pre-processing -> Validation)
|
* Elindítja az árlista feldolgozás kezdeti láncát (Pre-processing -> Validation)
|
||||||
*/
|
*/
|
||||||
@ -97,33 +116,299 @@ public function updateStepStatus(
|
|||||||
$pricelistFile->update($updateData);
|
$pricelistFile->update($updateData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
|
||||||
|
private const MIN_FILE_SIZE_BYTES = 1024; // 1 KB
|
||||||
|
private const ALLOWED_EXTENSIONS = ['xlsx', 'xls'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Előfeldolgozás (Pre-processing) - Adatok ellenőrzése és beolvasása
|
* Előfeldolgozás (Pre-processing) - Strukturális és technikai ellenőrzés
|
||||||
*
|
*
|
||||||
* @param PricelistFile $pricelistFile
|
* @param PricelistFile $pricelistFile
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function preProcess(PricelistFile $pricelistFile): bool
|
public function preProcess(PricelistFile $pricelistFile): bool
|
||||||
{
|
{
|
||||||
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Adatok ellenőrzése...', 10);
|
$startTime = microtime(true);
|
||||||
|
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Fájl ellenőrzése...', 10);
|
||||||
|
|
||||||
|
$meta = [
|
||||||
|
'completed_at' => null,
|
||||||
|
'duration_seconds' => null,
|
||||||
|
'file_info' => [],
|
||||||
|
'statistics' => [],
|
||||||
|
'errors' => [],
|
||||||
|
'warnings' => [],
|
||||||
|
'is_processable' => true,
|
||||||
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Itt hívjuk majd meg a PriceListService-t a fájl beolvasásához
|
// 1. Fájl fizikai ellenőrzése (10-20%)
|
||||||
// $data = $this->priceListService->readFromFile($pricelistFile->path);
|
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Fájl fizikai ellenőrzése...', 15);
|
||||||
// $results = $this->priceListService->importPriceListCheck($pricelistFile->supplier_id, $data);
|
$fileCheckResult = $this->checkFilePhysical($pricelistFile);
|
||||||
|
$meta['file_info'] = $fileCheckResult['info'];
|
||||||
|
$meta['errors'] = array_merge($meta['errors'], $fileCheckResult['errors']);
|
||||||
|
$meta['warnings'] = array_merge($meta['warnings'], $fileCheckResult['warnings']);
|
||||||
|
|
||||||
sleep(15); // Szimuláció
|
if ($this->hasBlockerError($meta['errors'])) {
|
||||||
|
$meta['is_processable'] = false;
|
||||||
|
$this->saveMetaAndFail($pricelistFile, $meta, $startTime);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'completed', 'Adatok ellenőrizve.', 100);
|
// 2. Excel megnyitás és munkalap ellenőrzés (20-40%)
|
||||||
|
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Munkalap ellenőrzése...', 30);
|
||||||
|
$fullPath = Storage::disk('public')->path($pricelistFile->filename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$spreadsheet = IOFactory::load($fullPath);
|
||||||
|
$worksheet = $spreadsheet->getActiveSheet();
|
||||||
|
$meta['file_info']['sheet_name'] = $worksheet->getTitle();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$meta['errors'][] = $this->buildError(PreProcessErrorCode::FILE_CORRUPTED, $e->getMessage());
|
||||||
|
$meta['is_processable'] = false;
|
||||||
|
$this->saveMetaAndFail($pricelistFile, $meta, $startTime);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Oszlopstruktúra validálás (40-70%)
|
||||||
|
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Oszlopstruktúra ellenőrzése...', 50);
|
||||||
|
$structureResult = $this->validateColumnStructure($worksheet);
|
||||||
|
$meta['errors'] = array_merge($meta['errors'], $structureResult['errors']);
|
||||||
|
$meta['warnings'] = array_merge($meta['warnings'], $structureResult['warnings']);
|
||||||
|
|
||||||
|
if ($this->hasBlockerError($meta['errors'])) {
|
||||||
|
$meta['is_processable'] = false;
|
||||||
|
$this->saveMetaAndFail($pricelistFile, $meta, $startTime);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Statisztika gyűjtés (70-90%)
|
||||||
|
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Statisztikai adatok gyűjtése...', 75);
|
||||||
|
$meta['statistics'] = $this->collectStatistics($worksheet);
|
||||||
|
|
||||||
|
if ($meta['statistics']['data_rows'] === 0) {
|
||||||
|
$meta['errors'][] = $this->buildError(PreProcessErrorCode::NO_DATA_ROWS);
|
||||||
|
$meta['is_processable'] = false;
|
||||||
|
$this->saveMetaAndFail($pricelistFile, $meta, $startTime);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Sikeres befejezés (100%)
|
||||||
|
$meta['completed_at'] = now()->toDateTimeString();
|
||||||
|
$meta['duration_seconds'] = round(microtime(true) - $startTime, 2);
|
||||||
|
|
||||||
|
$pricelistFile->update(['file_meta' => ['preprocess' => $meta]]);
|
||||||
|
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'completed', 'Előfeldolgozás sikeres.', 100);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Pricelist pre-process error: ' . $e->getMessage());
|
Log::error('Pricelist pre-process error: ' . $e->getMessage());
|
||||||
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'failed', $e->getMessage());
|
$meta['errors'][] = [
|
||||||
|
'code' => 'UNEXPECTED_ERROR',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'severity' => 'blocker',
|
||||||
|
];
|
||||||
|
$meta['is_processable'] = false;
|
||||||
|
$this->saveMetaAndFail($pricelistFile, $meta, $startTime);
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fájl fizikai ellenőrzése (létezés, kiterjesztés, méret)
|
||||||
|
*/
|
||||||
|
private function checkFilePhysical(PricelistFile $pricelistFile): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
$warnings = [];
|
||||||
|
$info = [
|
||||||
|
'original_filename' => $pricelistFile->filename,
|
||||||
|
'file_size_bytes' => null,
|
||||||
|
'file_size_human' => null,
|
||||||
|
'mime_type' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$disk = Storage::disk('public');
|
||||||
|
|
||||||
|
if (!$disk->exists($pricelistFile->filename)) {
|
||||||
|
$errors[] = $this->buildError(PreProcessErrorCode::FILE_NOT_FOUND);
|
||||||
|
return ['info' => $info, 'errors' => $errors, 'warnings' => $warnings];
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullPath = $disk->path($pricelistFile->filename);
|
||||||
|
$extension = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
if (!in_array($extension, self::ALLOWED_EXTENSIONS)) {
|
||||||
|
$errors[] = $this->buildError(
|
||||||
|
PreProcessErrorCode::INVALID_EXTENSION,
|
||||||
|
"Kiterjesztés: '{$extension}', elfogadott: " . implode(', ', self::ALLOWED_EXTENSIONS)
|
||||||
|
);
|
||||||
|
return ['info' => $info, 'errors' => $errors, 'warnings' => $warnings];
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileSize = $disk->size($pricelistFile->filename);
|
||||||
|
$info['file_size_bytes'] = $fileSize;
|
||||||
|
$info['file_size_human'] = $this->humanFileSize($fileSize);
|
||||||
|
$info['mime_type'] = $disk->mimeType($pricelistFile->filename);
|
||||||
|
|
||||||
|
if ($fileSize < self::MIN_FILE_SIZE_BYTES) {
|
||||||
|
$errors[] = $this->buildError(PreProcessErrorCode::FILE_EMPTY, "Fájlméret: {$info['file_size_human']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fileSize > self::MAX_FILE_SIZE_BYTES) {
|
||||||
|
$warnings[] = $this->buildWarning(PreProcessErrorCode::FILE_TOO_LARGE, "Fájlméret: {$info['file_size_human']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['info' => $info, 'errors' => $errors, 'warnings' => $warnings];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oszlopstruktúra és fejléc validálása
|
||||||
|
*/
|
||||||
|
private function validateColumnStructure(Worksheet $worksheet): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
$warnings = [];
|
||||||
|
|
||||||
|
$expectedColumnCount = count($this->priceListService->getExcelFieldPointer());
|
||||||
|
$highestColumn = $worksheet->getHighestDataColumn();
|
||||||
|
$columnCount = Coordinate::columnIndexFromString($highestColumn);
|
||||||
|
|
||||||
|
if ($columnCount < $expectedColumnCount) {
|
||||||
|
$errors[] = $this->buildError(
|
||||||
|
PreProcessErrorCode::COLUMN_COUNT_MISMATCH,
|
||||||
|
"Kevés oszlop ({$columnCount}), elvárt legalább: {$expectedColumnCount}"
|
||||||
|
);
|
||||||
|
} elseif ($columnCount > $expectedColumnCount) {
|
||||||
|
$warnings[] = $this->buildWarning(
|
||||||
|
PreProcessErrorCode::COLUMN_EXTRA,
|
||||||
|
"Többlet oszlopok ({$columnCount}), az első {$expectedColumnCount} kerül feldolgozásra."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fejléc ellenőrzés (4. sor a specifikáció szerint)
|
||||||
|
$headerRow = 4;
|
||||||
|
$expectedHeaders = PriceListService::EXPECTED_HEADERS;
|
||||||
|
$checkCount = min($columnCount, count($expectedHeaders));
|
||||||
|
|
||||||
|
for ($col = 0; $col < $checkCount; $col++) {
|
||||||
|
$cellValue = trim((string) $worksheet->getCellByColumnAndRow($col + 1, $headerRow)->getValue());
|
||||||
|
$expectedValue = $expectedHeaders[$col] ?? null;
|
||||||
|
|
||||||
|
if ($expectedValue !== null && $cellValue !== $expectedValue) {
|
||||||
|
// Az oszlopnevek eltérése blocker hiba
|
||||||
|
$errors[] = $this->buildError(
|
||||||
|
PreProcessErrorCode::HEADER_MISMATCH,
|
||||||
|
"A(z) " . ($col + 1) . ". oszlop fejléce '{$cellValue}' az elvárt '{$expectedValue}' helyett (a {$headerRow}. sorban)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['errors' => $errors, 'warnings' => $warnings];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statisztikai adatok gyűjtése a munkalapról
|
||||||
|
*/
|
||||||
|
private function collectStatistics(Worksheet $worksheet): array
|
||||||
|
{
|
||||||
|
$highestRow = $worksheet->getHighestDataRow();
|
||||||
|
$highestColumn = $worksheet->getHighestDataColumn();
|
||||||
|
$columnCount = Coordinate::columnIndexFromString($highestColumn);
|
||||||
|
|
||||||
|
$dataRows = 0;
|
||||||
|
$emptyRows = 0;
|
||||||
|
|
||||||
|
// A 4. sort fejlécnek tekintjük, az 5. sortól indul az adat
|
||||||
|
for ($row = 5; $row <= $highestRow; $row++) {
|
||||||
|
$supplierProductNumber = $worksheet->getCellByColumnAndRow(1, $row)->getValue();
|
||||||
|
if (!empty(trim((string) $supplierProductNumber))) {
|
||||||
|
$dataRows++;
|
||||||
|
} else {
|
||||||
|
$emptyRows++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_rows' => max(0, $highestRow - 4), // 4 sor fejléc/leírás után
|
||||||
|
'data_rows' => $dataRows,
|
||||||
|
'empty_rows' => $emptyRows,
|
||||||
|
'column_count' => $columnCount,
|
||||||
|
'expected_column_count' => count($this->priceListService->getExcelFieldPointer()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hiba rekord összeállítása
|
||||||
|
*/
|
||||||
|
private function buildError(PreProcessErrorCode $code, ?string $customMessage = null): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'code' => $code->value,
|
||||||
|
'message' => $customMessage ?? $code->label(),
|
||||||
|
'severity' => $code->severity(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Figyelmeztetés rekord összeállítása
|
||||||
|
*/
|
||||||
|
private function buildWarning(PreProcessErrorCode $code, ?string $customMessage = null, array $extra = []): array
|
||||||
|
{
|
||||||
|
return array_merge([
|
||||||
|
'code' => $code->value,
|
||||||
|
'message' => $customMessage ?? $code->label(),
|
||||||
|
'severity' => $code->severity(),
|
||||||
|
], $extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocker típusú hiba keresése a hibalistában
|
||||||
|
*/
|
||||||
|
private function hasBlockerError(array $errors): bool
|
||||||
|
{
|
||||||
|
foreach ($errors as $error) {
|
||||||
|
if (($error['severity'] ?? '') === 'blocker') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta adatok mentése és a workflow lépés hibásra állítása
|
||||||
|
*/
|
||||||
|
private function saveMetaAndFail(PricelistFile $pricelistFile, array $meta, float $startTime): void
|
||||||
|
{
|
||||||
|
$meta['completed_at'] = now()->toDateTimeString();
|
||||||
|
$meta['duration_seconds'] = round(microtime(true) - $startTime, 2);
|
||||||
|
|
||||||
|
$pricelistFile->update(['file_meta' => ['preprocess' => $meta]]);
|
||||||
|
|
||||||
|
$firstBlocker = collect($meta['errors'])->firstWhere('severity', 'blocker');
|
||||||
|
$failMessage = $firstBlocker['message'] ?? 'Előfeldolgozás sikertelen.';
|
||||||
|
|
||||||
|
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'failed', $failMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fájlméret olvasható formátumban
|
||||||
|
*/
|
||||||
|
private function humanFileSize(int $bytes): string
|
||||||
|
{
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
$i = 0;
|
||||||
|
$size = (float) $bytes;
|
||||||
|
|
||||||
|
while ($size >= 1024 && $i < count($units) - 1) {
|
||||||
|
$size /= 1024;
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($size, 1) . ' ' . $units[$i];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validálás (Validation) - Üzleti szabályok ellenőrzése
|
* Validálás (Validation) - Üzleti szabályok ellenőrzése
|
||||||
*
|
*
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('pricelist_files', function (Blueprint $table) {
|
||||||
|
$table->json('file_meta')->nullable()->after('workflow_steps');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('pricelist_files', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('file_meta');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user