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) */ public function dispatchInitialChain(PricelistFile $pricelistFile): void { Bus::chain([ PricelistWorkflowStep::Preprocessing->getJob($pricelistFile), PricelistWorkflowStep::Validation->getJob($pricelistFile), ])->dispatch(); } /** * Elindítja a végrehajtási jobot (Approval után) */ public function dispatchExecutionJob(PricelistFile $pricelistFile): void { $job = PricelistWorkflowStep::Execution->getJob($pricelistFile); if ($job) { dispatch($job); } } public function updateStepStatus( PricelistFile $pricelistFile, PricelistWorkflowStep $stepEnum, string $status, ?string $message = null, ?int $percentage = 0 ): void { $effectiveMessage = $message ?? $stepEnum->label(); // Frissítjük a modellt az adatbázisból, hogy a legfrissebb workflow_steps-szel dolgozzunk if ($pricelistFile->exists) { $pricelistFile->refresh(); } $steps = $pricelistFile->workflow_steps ?? []; $found = false; foreach ($steps as &$step) { if ($step['name'] === $stepEnum->value) { $step['status'] = $status; $step['message'] = $effectiveMessage; $found = true; break; } } unset($step); // Ha valamiért nem találtuk meg a lépést (pl. régi rekord), adjuk hozzá a listához if (!$found) { $steps[] = [ 'name' => $stepEnum->value, 'label' => $stepEnum->label(), 'status' => $status, 'message' => $effectiveMessage, ]; } $updateData = [ 'workflow_steps' => $steps, 'processing_current_step' => ($message && $message !== $stepEnum->label()) ? $stepEnum->label() . ': ' . $message : $effectiveMessage, 'processing_current_step_percentage' => $percentage, ]; if ($status === 'inprogress') { if ($stepEnum === PricelistWorkflowStep::Approval) { $updateData['status'] = PricelistFileStatusEnum::waiting_for_approval; $updateData['processing_current_step'] = null; $updateData['processing_current_step_percentage'] = null; } else { $updateData['status'] = PricelistFileStatusEnum::inprogress; } } elseif ($status === 'failed') { $updateData['status'] = PricelistFileStatusEnum::fail; } $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) - Strukturális és technikai ellenőrzés * * @param PricelistFile $pricelistFile * @return bool */ public function preProcess(PricelistFile $pricelistFile): bool { $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 { // 1. Fájl fizikai ellenőrzése (10-20%) $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Fájl fizikai ellenőrzése...', 15); $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']); if ($this->hasBlockerError($meta['errors'])) { $meta['is_processable'] = false; $this->saveMetaAndFail($pricelistFile, $meta, $startTime); return false; } // 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; } catch (\Exception $e) { Log::error('Pricelist pre-process error: ' . $e->getMessage()); $meta['errors'][] = [ 'code' => 'UNEXPECTED_ERROR', 'message' => $e->getMessage(), 'severity' => 'blocker', ]; $meta['is_processable'] = false; $this->saveMetaAndFail($pricelistFile, $meta, $startTime); 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 * * @param PricelistFile $pricelistFile * @return bool */ public function validate(PricelistFile $pricelistFile): bool { sleep(15); // Szimuláció $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'inprogress', 'Üzleti szabályok ellenőrzése...', 60); try { // TODO: Validációs logika sleep(15); // Szimuláció $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'completed', 'Validálás sikeres.', 100); sleep(15); // Szimuláció // A validálás végén átadjuk a lépést a jóváhagyásnak (Approval) $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Approval, 'inprogress'); return true; } catch (\Exception $e) { Log::error('Pricelist validation error: ' . $e->getMessage()); $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'failed', $e->getMessage()); throw $e; } } /** * Végrehajtás (Execution) - Módosítások tényleges alkalmazása tranzakcióban * * @param PricelistFile $pricelistFile * @return bool */ public function execute(PricelistFile $pricelistFile): bool { $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Execution, 'inprogress', 'Árlista frissítése...', 0); DB::beginTransaction(); try { // TODO: Tényleges importálás végrehajtása // $this->priceListService->importPriceList(...); sleep(1); // Szimuláció $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Execution, 'completed', 'Sikeresen befejezve.', 100); DB::commit(); $pricelistFile->update([ 'status' => PricelistFileStatusEnum::done, ]); return true; } catch (\Exception $e) { DB::rollBack(); Log::error('Pricelist execution error: ' . $e->getMessage()); $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Execution, 'failed', $e->getMessage()); return false; } } }