d2d.emegrendeles.hu/app/Services/PricelistFileProcessService.php
2026-03-08 06:46:54 +01:00

472 lines
17 KiB
PHP

<?php
namespace App\Services;
use App\Enums\PreProcessErrorCode;
use App\Enums\PricelistWorkflowStep;
use App\Models\PricelistFile;
use App\Enums\PricelistFileStatusEnum;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\DB;
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
{
public function __construct(
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)
*/
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;
}
}
}