ADD EV3-444 Hibás számítás Hooreyca adatok - add log analisator command
This commit is contained in:
parent
b28a224e8e
commit
d3486cb699
336
app/Console/Commands/CheckApiLog.php
Normal file
336
app/Console/Commands/CheckApiLog.php
Normal file
@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\ProfitCenter;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Monolog\Formatter\LineFormatter;
|
||||
|
||||
class CheckApiLog extends Command
|
||||
{
|
||||
protected $signature = 'app:check-api-log {file? : A storage/logs alatti API log fájl neve (pl. api-2026-07-21.log). Ha nincs megadva, a fájlnév szerint legutolsó api-*.log fájlt választja.}';
|
||||
|
||||
protected $description = 'A storage/logs/api-*.log fájlban szereplő /api/v1/order/unprocessed válaszok ellenőrzése: profit center hooreycaDataActive állapota és a HooreycaVolumen/HooreycaUnitPrice számított mezők helyessége.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$filePath = $this->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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user