Files
vkv/app/Jobs/UpsertClaimedLogResultJob.php
Zdeněk Burda 41e3ce6f25 Initial commit
2026-01-09 21:26:40 +01:00

330 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Jobs;
use App\Models\EdiBand;
use App\Models\EdiCategory;
use App\Models\Log;
use App\Models\LogResult;
use App\Models\Round;
use App\Services\Evaluation\ClaimedRunResolver;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
/**
* Job: UpsertClaimedLogResultJob
*
* Projekce deklarovaných výsledků (claimed) do log_results
* pro CLAIMED evaluation run.
*/
class UpsertClaimedLogResultJob implements ShouldQueue
{
use Queueable;
public function __construct(
protected int $logId
) {
}
public function handle(): void
{
$log = Log::find($this->logId);
if (! $log) {
return;
}
$round = Round::with(['bands', 'categories', 'powerCategories'])->find($log->round_id);
$remarksEval = $this->decodeRemarksEval($log->remarks_eval);
// TDate validace hlídá, že log odpovídá termínu kola.
$tDateInvalid = ! $this->isTDateWithinRound($log->tdate, $round);
if ($tDateInvalid) {
$this->addRemark($remarksEval, 'Datum v TDate neodpovídá termínu závodu.');
$log->remarks_eval = $this->encodeRemarksEval($remarksEval);
$log->save();
}
$categoryId = $this->resolveCategoryId($log, $round, $remarksEval);
$bandId = $this->resolveBandId($log, $round, $remarksEval);
[$powerCategoryId, $powerMismatch] = $this->resolvePowerCategoryId($log, $round, $remarksEval);
// 6H kategorie je povolená jen pro vybraná pásma.
if ($log->sixhr_category && ! $this->isSixHourBand($bandId)) {
$remarksEval[] = '6H kategorie je povolena jen pro pásma 145 MHz a 435 MHz.';
}
$missingClaimedQso = $log->claimed_qso_count === null;
$missingClaimedScore = $log->claimed_score === null;
if ($missingClaimedQso) {
$remarksEval[] = 'Nebyl načten CQSOs.';
}
if ($missingClaimedScore) {
$remarksEval[] = 'Nebyl načten CToSc.';
}
$log->remarks_eval = $this->encodeRemarksEval($remarksEval);
$log->save();
$claimedRun = ClaimedRunResolver::forRound($log->round_id);
$claimedQsoCount = $log->claimed_qso_count ?? 0;
$claimedScore = $log->claimed_score ?? 0;
$scorePerQso = $claimedQsoCount > 0 ? round($claimedScore / $claimedQsoCount, 2) : null;
$status = 'OK';
$statusReason = null;
if ($this->isCheckLog($log)) {
$status = 'CHECK';
}
// IGNORED = log nelze bezpečně zařadit do claimed scoreboardu.
if ($tDateInvalid || $categoryId === null || $bandId === null || $missingClaimedQso || $missingClaimedScore || $powerMismatch) {
$status = 'IGNORED';
$reasons = [];
if ($tDateInvalid) {
$reasons[] = 'TDate mimo termín závodu.';
}
if ($categoryId === null) {
$reasons[] = 'Kategorie nebyla rozpoznána.';
}
if ($bandId === null) {
$reasons[] = 'Pásmo nebylo rozpoznáno.';
}
if ($missingClaimedQso) {
$reasons[] = 'Chybí CQSOs.';
}
if ($missingClaimedScore) {
$reasons[] = 'Chybí CToSc.';
}
if ($powerMismatch) {
$reasons[] = 'Výkon neodpovídá zvolené kategorii.';
}
$statusReason = implode(' ', $reasons);
}
LogResult::updateOrCreate(
['log_id' => $log->id, 'evaluation_run_id' => $claimedRun->id],
[
'evaluation_run_id' => $claimedRun->id,
'category_id' => $categoryId,
'band_id' => $bandId,
'power_category_id' => $powerCategoryId,
'claimed_qso_count' => $log->claimed_qso_count,
'claimed_score' => $log->claimed_score,
'total_qso_count' => $claimedQsoCount,
'discarded_qso_count' => 0,
'discarded_points' => 0,
'discarded_qso_percent' => 0,
'unique_qso_count' => 0,
'score_per_qso' => $scorePerQso,
'official_score' => 0,
'penalty_score' => 0,
'status' => $status,
'status_reason' => $statusReason,
]
);
}
protected function resolveCategoryId(Log $log, ?Round $round, array &$remarksEval): ?int
{
$resolveCategory = function (?string $value) use ($round): ?int {
if (! $value) {
return null;
}
$ediCat = EdiCategory::whereRaw('LOWER(value) = ?', [mb_strtolower(trim($value))])->first();
if (! $ediCat) {
$ediCat = $this->matchEdiCategoryByRegex($value);
if (! $ediCat) {
return null;
}
}
$mappedCategoryId = $ediCat->categories()->value('categories.id');
if (! $mappedCategoryId) {
return null;
}
if ($round && $round->categories()->where('categories.id', $mappedCategoryId)->exists()) {
return $mappedCategoryId;
}
if (! $round || $round->categories()->count() === 0) {
return $mappedCategoryId;
}
return null;
};
// V PSect může být více tokenů zkoušíme je postupně.
$categoryId = null;
if ($log->psect) {
$categoryId = $resolveCategory($log->psect);
if ($categoryId === null) {
$parts = preg_split('/\\s+/', trim((string) $log->psect)) ?: [];
if (count($parts) > 1) {
foreach ($parts as $part) {
$categoryId = $resolveCategory($part);
if ($categoryId !== null) {
break;
}
}
}
}
}
if ($categoryId === null) {
$remarksEval[] = 'Kategorie nebyla rozpoznána.';
}
return $categoryId;
}
protected function matchEdiCategoryByRegex(string $value): ?EdiCategory
{
$candidates = EdiCategory::whereNotNull('regex_pattern')->get();
foreach ($candidates as $candidate) {
$pattern = $candidate->regex_pattern;
if (! $pattern) {
continue;
}
$delimited = preg_match('/^([#\\/]).+\\1[imsxuADSUXJ]*$/', $pattern) ? $pattern : '/' . $pattern . '/i';
set_error_handler(function () {
});
$matched = @preg_match($delimited, $value) === 1;
restore_error_handler();
if ($matched) {
return $candidate;
}
}
return null;
}
protected function resolveBandId(Log $log, ?Round $round, array &$remarksEval): ?int
{
$bandId = null;
if ($log->pband) {
// Nejprve přímá mapa přes EDI bandy, fallback je interval v MHz.
$pbandVal = mb_strtolower(trim($log->pband));
$ediBand = EdiBand::whereRaw('LOWER(value) = ?', [$pbandVal])->first();
if ($ediBand) {
$mappedBandId = $ediBand->bands()->value('bands.id');
if ($mappedBandId) {
if ($round && $round->bands()->where('bands.id', $mappedBandId)->exists()) {
$bandId = $mappedBandId;
} elseif (! $round || $round->bands()->count() === 0) {
$bandId = $mappedBandId;
}
}
} else {
$num = is_numeric($pbandVal) ? (float) $pbandVal : null;
if ($num === null && $log->pband) {
if (preg_match('/([0-9]+(?:[\\.,][0-9]+)?)/', $log->pband, $m)) {
$num = (float) str_replace(',', '.', $m[1]);
}
}
if ($num !== null) {
$bandMatch = \App\Models\Band::where('edi_band_begin', '<=', $num)
->where('edi_band_end', '>=', $num)
->first();
if ($bandMatch) {
if ($round && $round->bands()->where('bands.id', $bandMatch->id)->exists()) {
$bandId = $bandMatch->id;
} elseif (! $round || $round->bands()->count() === 0) {
$bandId = $bandMatch->id;
}
}
}
}
}
if ($bandId === null) {
$remarksEval[] = 'Pásmo nebylo rozpoznáno.';
}
return $bandId;
}
protected function resolvePowerCategoryId(Log $log, ?Round $round, array &$remarksEval): array
{
$powerCategoryId = null;
$powerMismatch = false;
if ($log->power_category_id) {
$powerCategoryId = $log->power_category_id;
}
if ($round && $round->powerCategories()->count() > 0) {
$exists = $round->powerCategories()->where('power_categories.id', $powerCategoryId)->exists();
if (! $exists) {
$powerMismatch = true;
}
}
return [$powerCategoryId, $powerMismatch];
}
protected function isSixHourBand(?int $bandId): bool
{
if (! $bandId) {
return false;
}
return in_array($bandId, [1, 2], true);
}
protected function isCheckLog(Log $log): bool
{
$psect = trim((string) $log->psect);
return $psect !== '' && preg_match('/\\bcheck\\b/i', $psect) === 1;
}
protected function decodeRemarksEval(?string $value): array
{
if (! $value) {
return [];
}
$decoded = json_decode($value, true);
return is_array($decoded) ? $decoded : [];
}
protected function encodeRemarksEval(array $value): ?string
{
$filtered = array_values(array_filter($value, fn ($item) => is_string($item) && trim($item) !== ''));
$filtered = array_values(array_unique($filtered));
if (count($filtered) === 0) {
return null;
}
return json_encode($filtered, JSON_UNESCAPED_UNICODE);
}
protected function addRemark(array &$remarksEval, string $message): void
{
if (! in_array($message, $remarksEval, true)) {
$remarksEval[] = $message;
}
}
protected function isTDateWithinRound(?string $tdate, ?Round $round): bool
{
if (! $tdate || ! $round || ! $round->start_time || ! $round->end_time) {
return true;
}
$parts = explode(';', $tdate);
if (count($parts) !== 2) {
return false;
}
if (!preg_match('/^\\d{8}$/', $parts[0]) || !preg_match('/^\\d{8}$/', $parts[1])) {
return false;
}
$start = \Carbon\Carbon::createFromFormat('Ymd', $parts[0])->startOfDay();
$end = \Carbon\Carbon::createFromFormat('Ymd', $parts[1])->endOfDay();
return $start->lte($round->end_time) && $end->gte($round->start_time);
}
}