resolveLogFile($this->argument('file')); if (! $filePath) { $this->error($this->argument('file') ? 'A megadott log fájl nem található: storage/logs/'.$this->argument('file') : 'Nem található api-*.log fájl a storage/logs könyvtárban.'); return self::FAILURE; } $this->info('Elemzett fájl: '.$filePath); [$totalQueries, $totalOrders] = $this->preprocess($filePath); if ($totalQueries === 0) { $this->warn('Nem található "unprocessed" API válasz (pagination mezővel) ebben a fájlban.'); return self::SUCCESS; } $this->info("Talált lekérdezések (API hívások): {$totalQueries}, összesen {$totalOrders} megrendeléssel."); $errorLogPath = $this->errorLogPathFor($filePath); $errorLogger = Log::build([ 'driver' => 'single', 'path' => $errorLogPath, ]); $formatter = new LineFormatter(null, null, true, true); $formatter->setJsonPrettyPrint(true); foreach ($errorLogger->getLogger()->getHandlers() as $handler) { $handler->setFormatter($formatter); } $bar = $this->output->createProgressBar($totalOrders); $bar->start(); $queryIndex = 0; $queriesWithErrors = 0; $ordersWithErrors = 0; $itemErrorCount = 0; $profitCenterErrorCount = 0; $handle = fopen($filePath, 'r'); while (($line = fgets($handle)) !== false) { $responseBody = $this->extractUnprocessedResponse($line); if ($responseBody === null) { continue; } $queryIndex++; $queryHasError = false; foreach ($responseBody['data'] ?? [] as $order) { $orderErrors = $this->checkProfitCenter($order); if ($orderErrors) { $profitCenterErrorCount++; } foreach ($order['items'] ?? [] as $item) { $itemErrors = $this->checkItemCalculations($item); if ($itemErrors) { $itemErrorCount++; foreach ($itemErrors as $err) { $orderErrors[] = 'item id='.($item['id'] ?? '?').': '.$err; } } } if ($orderErrors) { $ordersWithErrors++; $queryHasError = true; $errorLogger->error('Hibás megrendelés találat', [ 'sourceQueryIndex' => $queryIndex, 'orderId' => $order['id'] ?? null, 'errors' => $orderErrors, 'order' => $order, ]); } $bar->advance(); } if ($queryHasError) { $queriesWithErrors++; } } fclose($handle); $bar->finish(); $this->newLine(2); $this->table(['Metrika', 'Érték'], [ ['Vizsgált lekérdezések (API hívások) száma', $queryIndex], ['Hibát tartalmazó lekérdezések száma', $queriesWithErrors], ['Vizsgált megrendelések száma', $totalOrders], ['Hibás megrendelések száma', $ordersWithErrors], ['Hibás tétel(item) számítás találatok száma', $itemErrorCount], ['Profit center hiba találatok száma', $profitCenterErrorCount], ]); if ($ordersWithErrors > 0) { $this->warn('Hibás rendelések naplózva ide: '.$errorLogPath); } else { $this->info('Nem található hiba.'); } return self::SUCCESS; } private function resolveLogFile(?string $file): ?string { $logsDir = storage_path('logs'); if ($file) { $path = $logsDir.DIRECTORY_SEPARATOR.$file; return is_file($path) ? $path : null; } $dated = []; foreach (glob($logsDir.DIRECTORY_SEPARATOR.'api-*.log') ?: [] as $path) { if (preg_match('/api-(\d{4}-\d{2}-\d{2})\.log$/', basename($path), $m)) { $dated[$m[1]] = $path; } } if (! $dated) { return null; } krsort($dated); return reset($dated); } private function errorLogPathFor(string $filePath): string { $suffix = preg_match('/api-(\d{4}-\d{2}-\d{2})\.log$/', basename($filePath), $m) ? $m[1] : now()->format('Y-m-d'); return storage_path("logs/api-check-errors-{$suffix}.log"); } private function preprocess(string $filePath): array { $totalQueries = 0; $totalOrders = 0; $handle = fopen($filePath, 'r'); while (($line = fgets($handle)) !== false) { $responseBody = $this->extractUnprocessedResponse($line); if ($responseBody === null) { continue; } $totalQueries++; $totalOrders += count($responseBody['data'] ?? []); } fclose($handle); return [$totalQueries, $totalOrders]; } /** * Csak a /api/v1/order/unprocessed válaszokat ismeri fel (a "pagination" kulcs csak ott van jelen, * a setProcessed/status válaszokban nincs). */ private function extractUnprocessedResponse(string $line): ?array { if (! str_contains($line, 'API response')) { return null; } $context = $this->extractBalancedJson($line, 'API response'); if ($context === null || ! isset($context['responseBody']) || ! is_array($context['responseBody'])) { return null; } $body = $context['responseBody']; if (! isset($body['pagination'], $body['data']) || ! is_array($body['data'])) { return null; } return $body; } /** * A Monolog sorvégi %context%/%extra% JSON-jából kiszedi az első kapcsos zárójel-pártól * a hozzá tartozó záró zárójelig terjedő, kiegyensúlyozott JSON-t (idézőjelen belüli * kapcsos zárójeleket figyelmen kívül hagyva), hogy a sor végén álló extra tartalom * (pl. üres %extra% tömb) ne zavarja a dekódolást. */ private function extractBalancedJson(string $line, string $marker): ?array { $pos = strpos($line, $marker); if ($pos === false) { return null; } $jsonStart = strpos($line, '{', $pos); if ($jsonStart === false) { return null; } $depth = 0; $inString = false; $escape = false; $end = null; $len = strlen($line); for ($i = $jsonStart; $i < $len; $i++) { $ch = $line[$i]; if ($escape) { $escape = false; continue; } if ($ch === '\\') { $escape = true; continue; } if ($ch === '"') { $inString = ! $inString; continue; } if ($inString) { continue; } if ($ch === '{') { $depth++; } elseif ($ch === '}') { $depth--; if ($depth === 0) { $end = $i; break; } } } if ($end === null) { return null; } $decoded = json_decode(substr($line, $jsonStart, $end - $jsonStart + 1), true); return json_last_error() === JSON_ERROR_NONE ? $decoded : null; } /** * A profit center hooreycaDataActive állapotát a JELENLEGI DB-állapot alapján ellenőrzi * (a log maga nem tartalmazza a nyers flag értékét), és jelzi, ha a válasz tartalma * (profitCenterHooreycaId/profitCenterName jelenléte) nem egyezik a DB jelenlegi állapotával. */ private function checkProfitCenter(array $order): array { $errors = []; $profitCenterId = $order['profit_center_id'] ?? null; $profitCenter = $profitCenterId ? ProfitCenter::withTrashed()->find($profitCenterId) : null; $dbActive = $profitCenter?->hooreycaDataActive; $hasResponseData = ! empty($order['profitCenterHooreycaId']) || ! empty($order['profitCenterName']); if (! $profitCenter) { $errors[] = "profit_center_id={$profitCenterId} nem található a DB-ben (törölve vagy soha nem létezett)"; } elseif ($profitCenter->trashed()) { $errors[] = "profit_center_id={$profitCenterId} soft-deleted a DB-ben, mégis szerepelt a válaszban"; } elseif (! $dbActive) { $errors[] = "profit_center_id={$profitCenterId} hooreycaDataActive JELENLEG nem aktív a DB-ben, mégis szerepelt a válaszban"; } if ($profitCenter && ! $profitCenter->trashed() && $dbActive && ! $hasResponseData) { $errors[] = "profit_center_id={$profitCenterId} a DB-ben aktív, de a válaszból hiányzik a profitCenterHooreycaId/profitCenterName"; } return $errors; } /** * A HooreycaVolumen/HooreycaUnitPrice mezőket a jelenleg érvényes (OrderController:: * convertItemCollectionToHooreycaAPIItems-ben lévő) képlettel újraszámolja a naplózott * nyers mezőkből, és összeveti a naplózott számított értékkel. */ private function checkItemCalculations(array $item): array { $errors = []; $quantity = $item['quantity'] ?? null; $unitMultiplier = $item['unitMultiplier'] ?? null; $hooreycaMultiplier = $item['HooreycaMultiplier'] ?? null; $price = $item['price'] ?? null; $loggedVolumen = $item['HooreycaVolumen'] ?? null; $loggedUnitPrice = $item['HooreycaUnitPrice'] ?? null; if ($hooreycaMultiplier === null) { $expectedVolumen = null; $expectedUnitPrice = null; } else { $expectedVolumen = $quantity * $hooreycaMultiplier; $expectedUnitPrice = $expectedVolumen != 0 ? ($quantity * $unitMultiplier * $price) / $expectedVolumen : null; } if (! $this->numbersMatch($expectedVolumen, $loggedVolumen)) { $errors[] = sprintf('HooreycaVolumen eltér: várt=%s, naplózott=%s', json_encode($expectedVolumen), json_encode($loggedVolumen)); } if (! $this->numbersMatch($expectedUnitPrice, $loggedUnitPrice)) { $errors[] = sprintf('HooreycaUnitPrice eltér: várt=%s, naplózott=%s', json_encode($expectedUnitPrice), json_encode($loggedUnitPrice)); } return $errors; } private function numbersMatch($expected, $actual, float $epsilon = 0.0001): bool { if ($expected === null || $actual === null) { return $expected === $actual; } return abs((float) $expected - (float) $actual) < $epsilon; } }