330 lines
11 KiB
PHP
330 lines
11 KiB
PHP
<?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);
|
||
}
|
||
|
||
}
|