createLogQso(['log_id' => $logId]); $this->createQsoResult([ 'evaluation_run_id' => $runId, 'log_qso_id' => $logQso->id, 'is_valid' => $valid, 'points' => $points, ]); WorkingQso::create([ 'evaluation_run_id' => $runId, 'log_qso_id' => $logQso->id, 'log_id' => $logId, 'ts_utc' => $ts, 'band_id' => $bandId, 'call_norm' => 'OK1AAA', 'rcall_norm' => 'OK1BBB', 'loc_norm' => 'JN00AA', 'rloc_norm' => 'JN00AA', ]); return $logQso->id; } public function test_pick_best_window_prefers_higher_score(): void { $ruleSet = $this->createRuleSet([ 'use_multipliers' => false, 'multiplier_type' => 'NONE', 'multiplier_scope' => 'OVERALL', 'multiplier_source' => 'VALID_ONLY', ]); $round = $this->createRound(); $log = $this->createLog(['round_id' => $round->id]); $run = $this->createEvaluationRun(['rule_set_id' => $ruleSet->id, 'round_id' => $round->id]); $band = $this->createBand(); $t0 = Carbon::create(2025, 1, 1, 0, 0, 0, 'UTC'); $t1 = Carbon::create(2025, 1, 1, 1, 0, 0, 'UTC'); $t2 = Carbon::create(2025, 1, 1, 7, 0, 0, 'UTC'); $t3 = Carbon::create(2025, 1, 1, 8, 0, 0, 'UTC'); $firstA = $this->createQso($run->id, $log->id, $band->id, $t0, 10); $firstB = $this->createQso($run->id, $log->id, $band->id, $t1, 10); $bestA = $this->createQso($run->id, $log->id, $band->id, $t2, 25); $bestB = $this->createQso($run->id, $log->id, $band->id, $t3, 25); $service = new OperatingWindowService(); $result = $service->pickBestOperatingWindow($run->id, $log->id, 6, $ruleSet); $this->assertNotNull($result); $this->assertSame($t0->toDateTimeString(), $result['startUtc']->toDateTimeString()); $this->assertSame($t1->toDateTimeString(), $result['endUtc']->toDateTimeString()); $this->assertSame($t2->toDateTimeString(), $result['secondStartUtc']?->toDateTimeString()); $this->assertSame($t3->toDateTimeString(), $result['secondEndUtc']?->toDateTimeString()); $this->assertSame([$firstA, $firstB, $bestA, $bestB], $result['includedLogQsoIds']); $this->assertSame(4, $result['qsoCount']); } public function test_tie_break_prefers_earlier_start(): void { $ruleSet = $this->createRuleSet([ 'use_multipliers' => false, 'multiplier_type' => 'NONE', 'multiplier_scope' => 'OVERALL', 'multiplier_source' => 'VALID_ONLY', ]); $round = $this->createRound(); $log = $this->createLog(['round_id' => $round->id]); $run = $this->createEvaluationRun(['rule_set_id' => $ruleSet->id, 'round_id' => $round->id]); $band = $this->createBand(); $t0 = Carbon::create(2025, 1, 1, 0, 0, 0, 'UTC'); $t1 = Carbon::create(2025, 1, 1, 1, 0, 0, 'UTC'); $t2 = Carbon::create(2025, 1, 1, 15, 0, 0, 'UTC'); $t3 = Carbon::create(2025, 1, 1, 21, 0, 0, 'UTC'); $firstA = $this->createQso($run->id, $log->id, $band->id, $t0, 10); $firstB = $this->createQso($run->id, $log->id, $band->id, $t1, 10); $this->createQso($run->id, $log->id, $band->id, $t2, 10); $this->createQso($run->id, $log->id, $band->id, $t3, 10); $service = new OperatingWindowService(); $result = $service->pickBestOperatingWindow($run->id, $log->id, 6, $ruleSet); $this->assertNotNull($result); $this->assertSame($t0->toDateTimeString(), $result['startUtc']->toDateTimeString()); $this->assertSame($t1->toDateTimeString(), $result['endUtc']->toDateTimeString()); $this->assertNull($result['secondStartUtc']); $this->assertNull($result['secondEndUtc']); $this->assertSame([$firstA, $firstB], $result['includedLogQsoIds']); } public function test_window_includes_boundary_at_six_hours(): void { $ruleSet = $this->createRuleSet([ 'use_multipliers' => false, 'multiplier_type' => 'NONE', 'multiplier_scope' => 'OVERALL', 'multiplier_source' => 'VALID_ONLY', ]); $round = $this->createRound(); $log = $this->createLog(['round_id' => $round->id]); $run = $this->createEvaluationRun(['rule_set_id' => $ruleSet->id, 'round_id' => $round->id]); $band = $this->createBand(); $t0 = Carbon::create(2025, 1, 1, 0, 0, 0, 'UTC'); $t1 = Carbon::create(2025, 1, 1, 6, 0, 0, 'UTC'); $this->createQso($run->id, $log->id, $band->id, $t0, 10); $this->createQso($run->id, $log->id, $band->id, $t1, 10); $service = new OperatingWindowService(); $result = $service->pickBestOperatingWindow($run->id, $log->id, 6, $ruleSet); $this->assertNotNull($result); $this->assertSame($t0->toDateTimeString(), $result['startUtc']->toDateTimeString()); $this->assertSame($t1->toDateTimeString(), $result['endUtc']->toDateTimeString()); $this->assertSame(2, $result['qsoCount']); } public function test_returns_null_when_no_valid_qso(): void { $ruleSet = $this->createRuleSet([ 'use_multipliers' => false, 'multiplier_type' => 'NONE', 'multiplier_scope' => 'OVERALL', 'multiplier_source' => 'VALID_ONLY', ]); $round = $this->createRound(); $log = $this->createLog(['round_id' => $round->id]); $run = $this->createEvaluationRun(['rule_set_id' => $ruleSet->id, 'round_id' => $round->id]); $band = $this->createBand(); $t0 = Carbon::create(2025, 1, 1, 0, 0, 0, 'UTC'); $this->createQso($run->id, $log->id, $band->id, $t0, 10, false); $service = new OperatingWindowService(); $result = $service->pickBestOperatingWindow($run->id, $log->id, 6, $ruleSet); $this->assertNull($result); } }