mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-09 05:47:28 +00:00
fix(lotw): 呼号冲突批量全阻时也进预览,呼号不符改为显示记录≠证书
整批记录都被呼号冲突挡住(0 条可传)时,统计闸门直接弹「N QSO(s) can't be uploaded」 消息并返回,带【改写为…】【换一张证书】两个动作的预览弹窗根本走不到,那批记录无从 处理。现在只要原因里有呼号冲突就放行到预览:0 条可传也出预览(显示 0 QSO(s) + 冲突 提示 + 两个动作),上传键置灰——以前点确认会返回“预览已过期”。其它原因的 0 条可传 批次保持原消息弹窗不变。 「MY callsign does not match the certificate」的示例值由某条记录的对方呼号改为 「记录呼号 ≠ 证书呼号」,把需要协调的两个值都打出来(旧文案读起来像证书呼号)。 空预览不再显示空的日期区间。 新增 LoTWUploadMessagesTest 3 例:冲突批量必须进预览、无冲突的 0 条可传批次保持消息、 有待传记录时不走冲突兜底;仓库层用例新增:全阻批量仍返回带冲突清单的预览、且无载荷 时确认安全地按“过期”失败。
This commit is contained in:
8 files changed
+151
-8
No files matched your search
@@ -37,7 +37,12 @@ internal class LoTWSigner(private val config: LoTWConfig) {
|
||||
val call = record.theirCallsign.trim().uppercase(Locale.US)
|
||||
if (record.status != QsoStatus.COMPLETE || !call.matches(Regex("[A-Z0-9]+(/[A-Z0-9]+)*")) ||
|
||||
!call.any(Char::isLetter) || !call.any(Char::isDigit)) fail(LoTWProblem.INVALID_CONTACT, call)
|
||||
if (!record.myCallsign.trim().equals(key.info.callsign, true)) fail(LoTWProblem.CALLSIGN_MISMATCH, call)
|
||||
// Report the two callsigns the operator has to reconcile (record vs certificate). A generic
|
||||
// sample — the opposite station's call — says nothing about which side is wrong, which is
|
||||
// exactly what a "MY callsign does not match the certificate" message must tell.
|
||||
if (!record.myCallsign.trim().equals(key.info.callsign, true)) {
|
||||
fail(LoTWProblem.CALLSIGN_MISMATCH, "${record.myCallsign.trim()} ≠ ${key.info.callsign}")
|
||||
}
|
||||
val date = utc(record.startUtcMillis, "yyyy-MM-dd")
|
||||
// A record dated after "now" gets its own message: it happens when the operator
|
||||
// logged against a pass that had not started yet (out-of-window flow), and "fix the
|
||||
|
||||
+50
@@ -22,6 +22,7 @@ import com.rtbishop.look4sat.core.domain.logbook.QsoStatus
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWOperationException
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWProblem
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWStation
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWUploadResult
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -120,6 +121,55 @@ class LoTWUploadRepositoryPreviewTest {
|
||||
}
|
||||
}
|
||||
|
||||
/** Nothing signable at all: the preview still comes back (count 0) with the conflicts listed,
|
||||
* so the UI can offer the rewrite / use-another-certificate actions instead of a dead end. */
|
||||
@Test
|
||||
fun `fully blocked batch still returns a preview with the conflicts`() {
|
||||
runBlocking {
|
||||
val storage = MemStorage()
|
||||
val repo = LoTWUploadRepository(
|
||||
storage,
|
||||
{ LoTWConfig(File("src/main/assets/lotw/config.tq6").inputStream()) },
|
||||
now = { System.currentTimeMillis() }
|
||||
)
|
||||
repo.importCertificate(fixture("test_tqsl_empty.p12"), charArrayOf())
|
||||
repo.saveStation(LoTWStation(grid = "OL62TI"))
|
||||
val start = 1_787_000_000_000L
|
||||
val preview = repo.prepare(
|
||||
listOf(
|
||||
sampleRecord(start, "BH6RJD", "XX0YY"),
|
||||
sampleRecord(start + 60_000L, "BG7QBL", "XX0ZZ")
|
||||
),
|
||||
resubmit = false
|
||||
)
|
||||
assertEquals(0, preview.count)
|
||||
assertEquals(2, preview.unavailableSkipped)
|
||||
assertEquals(listOf(start, start + 60_000L), preview.callsignConflicts)
|
||||
assertEquals(2, preview.unavailableReasons[LoTWProblem.CALLSIGN_MISMATCH])
|
||||
assertEquals(0, preview.missingCallsign)
|
||||
// No payload was prepared: confirming this preview must fail as expired, not crash.
|
||||
assertTrue(repo.upload(preview.id) is LoTWUploadResult.ExpiredPreview)
|
||||
}
|
||||
}
|
||||
|
||||
/** The mismatch reason names both callsigns (record vs certificate). The old detail showed the
|
||||
* opposite station's call, which told the operator nothing about which side to fix. */
|
||||
@Test
|
||||
fun `callsign mismatch detail names the record and the certificate callsign`() {
|
||||
runBlocking {
|
||||
val storage = MemStorage()
|
||||
val repo = LoTWUploadRepository(
|
||||
storage,
|
||||
{ LoTWConfig(File("src/main/assets/lotw/config.tq6").inputStream()) },
|
||||
now = { System.currentTimeMillis() }
|
||||
)
|
||||
repo.importCertificate(fixture("test_tqsl_empty.p12"), charArrayOf())
|
||||
repo.saveStation(LoTWStation(grid = "OL62TI"))
|
||||
val audit = repo.audit(listOf(sampleRecord(1_787_000_000_000L, "BH6RJD", "XX0YY")))
|
||||
assertEquals("XX0YY ≠ BA7OPF", audit.details[LoTWProblem.CALLSIGN_MISMATCH])
|
||||
}
|
||||
}
|
||||
|
||||
private fun sampleRecord(start: Long, call: String, myCallsign: String) = QsoRecord(
|
||||
id = start,
|
||||
startUtcMillis = start,
|
||||
|
||||
+12
@@ -18,6 +18,7 @@
|
||||
package com.rtbishop.look4sat.core.domain.logbook
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWProblem
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWUploadAudit
|
||||
|
||||
/**
|
||||
* Wording for the reasons a QSO cannot be signed for LoTW.
|
||||
@@ -69,3 +70,14 @@ fun unavailableUploadSummary(
|
||||
val detail = parts.joinToString(", ").ifBlank { "check the logbook" }
|
||||
return "$subject — $detail"
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a batch that has nothing uploadable still has to reach the preview dialog.
|
||||
*
|
||||
* The preview is the only place that carries the callsign-conflict actions (rewrite the records, or
|
||||
* upload them under another certificate). Short-circuiting such a batch into the plain "can't be
|
||||
* uploaded" message strands exactly the records the operator needs to act on — the message has no
|
||||
* buttons to get out of, so those QSOs could never be uploaded.
|
||||
*/
|
||||
fun LoTWUploadAudit.needsPreviewForConflicts(): Boolean =
|
||||
pending == 0 && (reasons[LoTWProblem.CALLSIGN_MISMATCH] ?: 0) > 0
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.logbook
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWProblem
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWUploadAudit
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* The preview dialog is the only place carrying the callsign-conflict actions (rewrite / use another
|
||||
* certificate), so a conflict-only batch must NOT be short-circuited into the plain
|
||||
* "can't be uploaded" message: that dialog has no buttons to get out of.
|
||||
*/
|
||||
class LoTWUploadMessagesTest {
|
||||
|
||||
@Test
|
||||
fun `conflict-only batch asks for the preview even with nothing pending`() {
|
||||
assertTrue(audit(pending = 0, mismatch = 107, satellite = 1).needsPreviewForConflicts())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blocked batch without conflicts keeps the plain message`() {
|
||||
assertFalse(audit(pending = 0, mismatch = 0, satellite = 3).needsPreviewForConflicts())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `batch with something to upload never takes the conflict fallback`() {
|
||||
assertFalse(audit(pending = 5, mismatch = 2, satellite = 1).needsPreviewForConflicts())
|
||||
}
|
||||
|
||||
private fun audit(pending: Int, mismatch: Int, satellite: Int) = LoTWUploadAudit(
|
||||
total = 108,
|
||||
pending = pending,
|
||||
uploaded = 0,
|
||||
unknown = 0,
|
||||
unavailable = 108 - pending,
|
||||
reasons = buildMap {
|
||||
if (mismatch > 0) put(LoTWProblem.CALLSIGN_MISMATCH, mismatch)
|
||||
if (satellite > 0) put(LoTWProblem.SATELLITE, satellite)
|
||||
}
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user