876 lines
33 KiB
PHP
876 lines
33 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Enums\PreProcessErrorCode;
|
|
use App\Enums\PricelistWorkflowStep;
|
|
use App\Models\PricelistFile;
|
|
use App\Enums\PricelistFileStatusEnum;
|
|
use App\Models\PricelistFileLine;
|
|
use App\Enums\PricelistFileLineStatusEnum;
|
|
use App\Enums\PricelistUnitEnum;
|
|
use App\Enums\PricelistSellerUnitEnum;
|
|
use App\Enums\PricelistVatEnum;
|
|
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
|
|
{
|
|
$this->resetWorkflow($pricelistFile);
|
|
|
|
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' => mb_strcut(
|
|
($message && $message !== $stepEnum->label())
|
|
? $stepEnum->label() . ': ' . $message
|
|
: $effectiveMessage,
|
|
0,
|
|
255
|
|
),
|
|
'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 és sorok rögzítése
|
|
*
|
|
* @param PricelistFile $pricelistFile
|
|
* @return bool
|
|
*/
|
|
public function validate(PricelistFile $pricelistFile): bool
|
|
{
|
|
$startTime = microtime(true);
|
|
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'inprogress', 'Sorok beolvasása és mentése...', 0);
|
|
|
|
try {
|
|
// Korábbi sorok törlése az újraindíthatóság érdekében
|
|
$pricelistFile->lines()->delete();
|
|
|
|
$fullPath = Storage::disk('public')->path($pricelistFile->filename);
|
|
$spreadsheet = IOFactory::load($fullPath);
|
|
$worksheet = $spreadsheet->getActiveSheet();
|
|
$highestRow = $worksheet->getHighestDataRow();
|
|
$expectedHeaders = PriceListService::EXPECTED_HEADERS;
|
|
|
|
$batchSize = 50;
|
|
$dataToInsert = [];
|
|
$processedCount = 0;
|
|
$totalDataRows = $highestRow >= 5 ? ($highestRow - 4) : 0;
|
|
|
|
if ($totalDataRows === 0) {
|
|
throw new \Exception('A fájl nem tartalmaz feldolgozható adatsort az 5. sortól.');
|
|
}
|
|
|
|
for ($row = 5; $row <= $highestRow; $row++) {
|
|
$rowData = [];
|
|
$hasData = false;
|
|
|
|
foreach ($expectedHeaders as $index => $label) {
|
|
$value = $worksheet->getCellByColumnAndRow($index + 1, $row)->getValue();
|
|
$rowData[$label] = $value;
|
|
if (!empty(trim((string)$value))) {
|
|
$hasData = true;
|
|
}
|
|
}
|
|
|
|
// Csak akkor mentjük, ha a sor nem teljesen üres
|
|
if ($hasData) {
|
|
$dataToInsert[] = [
|
|
'pricelist_file_id' => $pricelistFile->id,
|
|
'row_number' => $row,
|
|
'status' => PricelistFileLineStatusEnum::ok->value,
|
|
'payload' => json_encode($rowData),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
// A BaseAuditable trait miatt ezeket is érdemes lehet kitölteni,
|
|
// de a tömeges beszúrásnál (insert) nem futnak le a boot események.
|
|
'created_by' => auth()->id() ?? 1,
|
|
];
|
|
}
|
|
|
|
$processedCount++;
|
|
|
|
if (count($dataToInsert) >= $batchSize) {
|
|
PricelistFileLine::insert($dataToInsert);
|
|
$dataToInsert = [];
|
|
|
|
$percentage = round(($processedCount / $totalDataRows) * 100);
|
|
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'inprogress', "Sorok mentése ({$processedCount}/{$totalDataRows})...", $percentage);
|
|
}
|
|
}
|
|
|
|
if (!empty($dataToInsert)) {
|
|
PricelistFileLine::insert($dataToInsert);
|
|
}
|
|
|
|
$this->runBusinessValidation($pricelistFile);
|
|
|
|
$duration = round(microtime(true) - $startTime, 2);
|
|
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'completed', "Sikeres validálás ({$processedCount} sor, {$duration} mp).", 100);
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Szöveg normalizálása az összehasonlításhoz (mb_strtoupper, trim, speciális karakterek kezelése)
|
|
*/
|
|
protected function normalizeForComparison(string $text): string
|
|
{
|
|
// 1. Kis/Nagybetű érzéketlenség UTF-8 támogatással
|
|
$text = mb_strtoupper(trim($text), 'UTF-8');
|
|
|
|
// 2. Szeparátorok egységesítése (perjel és alulvonás cseréje)
|
|
// A hibaüzenetben látható alulvonás és a felhasználó által említett perjel miatt
|
|
$text = str_replace(['/', '_'], ' ', $text);
|
|
|
|
// 3. Felesleges szóközök eltávolítása (ha a csere után több szóköz maradt)
|
|
$text = preg_replace('/\s+/', ' ', $text);
|
|
|
|
return trim($text);
|
|
}
|
|
|
|
/**
|
|
* Termékcsoport lookup tábla felépítése az adatbázisból (hierarchia útvonala -> id)
|
|
*/
|
|
protected function getProductGroupLookupMap(): array
|
|
{
|
|
$allGroups = (new \App\Models\ProductGroup)->allWithPath(3);
|
|
$groupLookup = [];
|
|
|
|
foreach ($allGroups as $group) {
|
|
$parentsNames = $group['parentsNames'] ?? [];
|
|
|
|
// Technikai gyökér elhagyása (0. szint, pl. "Főcsoportok")
|
|
if (isset($group['depth']) && $group['depth'] > 0 && !empty($parentsNames)) {
|
|
array_shift($parentsNames);
|
|
}
|
|
|
|
$pathParts = array_merge($parentsNames, [$group['name']]);
|
|
$normalizedParts = array_map(fn($v) => $this->normalizeForComparison((string)$v), $pathParts);
|
|
$pathKey = implode('|', $normalizedParts);
|
|
$groupLookup[$pathKey] = $group['id'];
|
|
}
|
|
|
|
return $groupLookup;
|
|
}
|
|
|
|
/**
|
|
* Gyártó lookup tábla felépítése (név -> id)
|
|
*/
|
|
protected function getProducerLookupMap(): array
|
|
{
|
|
return \App\Models\Producer::all()
|
|
->keyBy(fn($p) => $this->normalizeForComparison($p->name))
|
|
->map->id
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* Termék lookup tábla felépítése a beszállítóhoz (cikkszám -> id)
|
|
*/
|
|
protected function getProductLookupMap(int $supplierId): array
|
|
{
|
|
return \App\Models\Product::where('supplier_id', $supplierId)
|
|
->whereNotNull('supplierProductNumber')
|
|
->get()
|
|
->keyBy('supplierProductNumber')
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* Termékcsoport útvonal összeállítása a payload-ból
|
|
*/
|
|
protected function getRowPathFromPayload(array $payload): string
|
|
{
|
|
$pathComponents = [
|
|
$payload[PriceListService::EXPECTED_HEADERS[1]] ?? null,
|
|
$payload[PriceListService::EXPECTED_HEADERS[2]] ?? null,
|
|
$payload[PriceListService::EXPECTED_HEADERS[3]] ?? null
|
|
];
|
|
|
|
return collect($pathComponents)
|
|
->filter(fn($v) => !empty(trim((string)$v)))
|
|
->map(fn($v) => $this->normalizeForComparison((string)$v))
|
|
->implode('|');
|
|
}
|
|
|
|
/**
|
|
* Termékcsoport validáció egy sorra
|
|
*/
|
|
protected function validateProductGroup(string $rowPath, array $groupLookup): ?string
|
|
{
|
|
if (empty($rowPath)) {
|
|
return "Hiányzó termékcsoport adatok.";
|
|
}
|
|
|
|
if (!isset($groupLookup[$rowPath])) {
|
|
return "Ismeretlen termékcsoport útvonal: $rowPath";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Mértékegység validáció egy sorra
|
|
*/
|
|
protected function validateUnit(array $payload): ?string
|
|
{
|
|
$unitValue = $payload[PriceListService::EXPECTED_HEADERS[7]] ?? null;
|
|
|
|
if (empty(trim((string)$unitValue))) {
|
|
return "Hiányzó mértékegység.";
|
|
}
|
|
|
|
$normalizedUnit = mb_strtolower(trim((string)$unitValue));
|
|
if (!PricelistUnitEnum::tryFrom($normalizedUnit)) {
|
|
return "Érvénytelen mértékegység: $unitValue. Elfogadott: " . implode(', ', PricelistUnitEnum::values());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Legkisebb eladási egység validáció egy sorra
|
|
*/
|
|
protected function validateSellerUnit(array $payload): ?string
|
|
{
|
|
$sellerUnitValue = $payload[PriceListService::EXPECTED_HEADERS[9]] ?? null;
|
|
|
|
if (empty(trim((string)$sellerUnitValue))) {
|
|
return "Hiányzó legkisebb eladási egység.";
|
|
}
|
|
|
|
$normalizedUnit = mb_strtolower(trim((string)$sellerUnitValue));
|
|
if (!PricelistSellerUnitEnum::tryFrom($normalizedUnit)) {
|
|
return "Érvénytelen legkisebb eladási egység: $sellerUnitValue. Elfogadott: " . implode(', ', PricelistSellerUnitEnum::values());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Mennyiségi egység validáció egy sorra
|
|
*/
|
|
protected function validateAmountUnit(array $payload): ?string
|
|
{
|
|
$amountUnitValue = $payload[PriceListService::EXPECTED_HEADERS[11]] ?? null;
|
|
|
|
if (empty(trim((string)$amountUnitValue))) {
|
|
return "Hiányzó mennyiségi egység.";
|
|
}
|
|
|
|
$normalizedUnit = mb_strtolower(trim((string)$amountUnitValue));
|
|
if (!PricelistSellerUnitEnum::tryFrom($normalizedUnit)) {
|
|
return "Érvénytelen mennyiségi egység: $amountUnitValue. Elfogadott: " . implode(', ', PricelistSellerUnitEnum::values());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* ÁFA % validáció egy sorra
|
|
*/
|
|
protected function validateVat(array $payload): ?string
|
|
{
|
|
$vatValue = $payload[PriceListService::EXPECTED_HEADERS[12]] ?? null;
|
|
|
|
if ($vatValue === null || (is_string($vatValue) && trim($vatValue) === '')) {
|
|
return "Hiányzó ÁFA %.";
|
|
}
|
|
|
|
// Kinyerjük a numerikus értéket, kezelve a százalékjelet, szóközöket és a tizedesvesszőt
|
|
$numericValue = (float) str_replace(['%', ' ', ','], ['', '', '.'], (string)$vatValue);
|
|
|
|
// Ha az érték 0 és 1 közé esik (pl. 0.05, 0.27), akkor valószínűleg Excel százalék formátum
|
|
// Ebben az esetben megszorozzuk 100-zal, hogy megkapjuk az egész értéket (pl. 5, 27)
|
|
if ($numericValue > 0 && $numericValue <= 1) {
|
|
$numericValue *= 100;
|
|
}
|
|
|
|
$finalVat = (int) round($numericValue);
|
|
|
|
if (!PricelistVatEnum::tryFrom($finalVat)) {
|
|
return "Érvénytelen ÁFA %: $vatValue. Elfogadott: " . implode(', ', PricelistVatEnum::labels());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Gyártó validáció egy sorra
|
|
*/
|
|
protected function validateProducer(array $payload, array $producerLookup): ?string
|
|
{
|
|
$producerName = $payload[PriceListService::EXPECTED_HEADERS[8]] ?? null;
|
|
|
|
if (empty(trim((string)$producerName))) {
|
|
return "Hiányzó gyártó megnevezés.";
|
|
}
|
|
|
|
$normalizedName = $this->normalizeForComparison((string)$producerName);
|
|
|
|
if (!isset($producerLookup[$normalizedName])) {
|
|
return "NEW_PRODUCER";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Üzleti validáció futtatása a mentett sorokon (Batch feldolgozás)
|
|
*
|
|
* @param PricelistFile $pricelistFile
|
|
*/
|
|
protected function runBusinessValidation(PricelistFile $pricelistFile): void
|
|
{
|
|
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'inprogress', 'Üzleti validálás...', 100);
|
|
|
|
$groupLookup = $this->getProductGroupLookupMap();
|
|
$producerLookup = $this->getProducerLookupMap();
|
|
$productLookup = $this->getProductLookupMap($pricelistFile->supplier_id);
|
|
|
|
$pricelistFile->lines()->chunk(1000, function ($lines) use ($groupLookup, $producerLookup, $productLookup) {
|
|
$updates = [];
|
|
|
|
foreach ($lines as $line) {
|
|
$payload = $line->payload;
|
|
$validationMessages = [];
|
|
|
|
// Termék ID kinyerése (cikkszám alapján)
|
|
$sku = (string)($payload[PriceListService::EXPECTED_HEADERS[0]] ?? '');
|
|
$existingProduct = $productLookup[$sku] ?? null;
|
|
$productId = $existingProduct['id'] ?? null;
|
|
|
|
// Termékcsoport útvonal és ID kinyerése
|
|
$rowPath = $this->getRowPathFromPayload($payload);
|
|
$groupId = $groupLookup[$rowPath] ?? null;
|
|
|
|
// Gyártó ID kinyerése
|
|
$producerName = $payload[PriceListService::EXPECTED_HEADERS[8]] ?? null;
|
|
$producerId = $producerLookup[$this->normalizeForComparison((string)$producerName)] ?? null;
|
|
|
|
// Különálló validációk meghívása
|
|
if ($groupError = $this->validateProductGroup($rowPath, $groupLookup)) {
|
|
$validationMessages['product_group'] = $groupError;
|
|
}
|
|
|
|
$hasNewProducer = false;
|
|
if ($producerError = $this->validateProducer($payload, $producerLookup)) {
|
|
if ($producerError === 'NEW_PRODUCER') {
|
|
$validationMessages['producer'] = "Új gyártó: " . ($payload[PriceListService::EXPECTED_HEADERS[8]] ?? '');
|
|
$hasNewProducer = true;
|
|
} else {
|
|
$validationMessages['producer'] = $producerError;
|
|
}
|
|
}
|
|
|
|
if ($unitError = $this->validateUnit($payload)) {
|
|
$validationMessages['product_unit'] = $unitError;
|
|
}
|
|
|
|
if ($sellerUnitError = $this->validateSellerUnit($payload)) {
|
|
$validationMessages['seller_unit'] = $sellerUnitError;
|
|
}
|
|
|
|
if ($amountUnitError = $this->validateAmountUnit($payload)) {
|
|
$validationMessages['amount_unit'] = $amountUnitError;
|
|
}
|
|
|
|
if ($vatError = $this->validateVat($payload)) {
|
|
$validationMessages['vat'] = $vatError;
|
|
}
|
|
|
|
// Státusz meghatározása
|
|
$hasError = false;
|
|
|
|
foreach ($validationMessages as $key => $msg) {
|
|
if (str_contains($msg, 'Hiányzó') || str_contains($msg, 'Ismeretlen') || str_contains($msg, 'Érvénytelen')) {
|
|
$hasError = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$status = PricelistFileLineStatusEnum::ok;
|
|
if ($hasError) {
|
|
$status = PricelistFileLineStatusEnum::error;
|
|
} elseif ($productId === null) {
|
|
// Ha nincs meglévő termék -> Új termék
|
|
$status = PricelistFileLineStatusEnum::new_product;
|
|
} else {
|
|
// Meglévő termék esetén nézzük a módosulásokat
|
|
$isUpdated = false;
|
|
|
|
// 1. Gyártó módosulás
|
|
if ($hasNewProducer) {
|
|
$isUpdated = true;
|
|
} elseif ($producerId !== null && (int)($existingProduct['producer_id'] ?? 0) !== (int)$producerId) {
|
|
$isUpdated = true;
|
|
$validationMessages['producer_update'] = "Módosult gyártó";
|
|
}
|
|
|
|
// 2. Termékcsoport módosulás
|
|
if ($groupId !== null && (int)($existingProduct['product_group_id'] ?? 0) !== (int)$groupId) {
|
|
$isUpdated = true;
|
|
$validationMessages['product_group_update'] = "Módosult termékcsoport";
|
|
}
|
|
|
|
if ($isUpdated) {
|
|
$status = PricelistFileLineStatusEnum::updated;
|
|
}
|
|
}
|
|
|
|
$updates[] = [
|
|
'id' => $line->id,
|
|
'pricelist_file_id' => $line->pricelist_file_id,
|
|
'row_number' => $line->row_number,
|
|
'status' => $status->value,
|
|
'product_id' => $productId,
|
|
'product_group_id' => $groupId,
|
|
'producer_id' => $producerId,
|
|
'payload' => json_encode($line->payload),
|
|
'validation_messages' => json_encode($validationMessages),
|
|
'created_at' => $line->created_at->toDateTimeString(),
|
|
'updated_at' => now()->toDateTimeString(),
|
|
];
|
|
}
|
|
|
|
// Kötegelt frissítés (upsert) a teljesítmény optimalizálásához
|
|
if (!empty($updates)) {
|
|
PricelistFileLine::upsert(
|
|
$updates,
|
|
['id'], // Egyedi azonosító
|
|
['status', 'validation_messages', 'product_id', 'product_group_id', 'producer_id', 'updated_at'] // Frissítendő mezők
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
}
|