From 3cd69c80cb3e92c61f109cf6a94c53f60be7d1e1 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Wed, 20 May 2026 10:38:47 +0200 Subject: [PATCH 01/16] Add support for already exchanged ticket error --- .../main/java/eu/pretix/libpretixsync/api/PretixApi.kt | 1 + .../eu/pretix/libpretixsync/check/AsyncCheckProvider.kt | 9 +++++++++ .../eu/pretix/libpretixsync/check/OnlineCheckProvider.kt | 2 ++ .../eu/pretix/libpretixsync/check/TicketCheckProvider.kt | 2 +- .../eu/pretix/libpretixsync/sqldelight/ReusableMedium.sq | 6 ++++++ 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt index 66715694..33e71625 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt @@ -129,6 +129,7 @@ open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, h body.put("answers", answerbody) body.put("questions_supported", questions_supported) body.put("canceled_supported", true) + body.put("media_exchange_supported", true) body.put("secret", secret) val jlists = JSONArray() for (l in lists) { diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt index b37f653c..4913ab26 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt @@ -781,6 +781,15 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa return res } + val linkedReusableMedium = db.reusableMediumQueries.selectByLinkedOrderPosition(position.positionId) + .executeAsOneOrNull()?.toModel() + if (linkedReusableMedium != null) { + res.type = TicketCheckProvider.CheckResult.Type.ALREADY_EXCHANGED + res.isCheckinAllowed = false + storeFailedCheckin(eventSlug, list.serverId, "already_exchanged", position.secret!!, type, position = position.serverId, item = positionItem.serverId, variation = position.variationServerId, subevent = position.subEventServerId, nonce = nonce) + return res + } + // !!! When extending this, also extend checkOfflineWithoutData !!! val rules = list.rules diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt index 6e26db3c..a587a925 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt @@ -163,6 +163,8 @@ class OnlineCheckProvider( res.isCheckinAllowed = includePending && response.has("position") && response.getJSONObject("position").optString("order__status", "n") == "n" } else if ("product" == reason) { res.type = TicketCheckProvider.CheckResult.Type.PRODUCT + } else if ("already_exchanged" == reason) { + res.type = TicketCheckProvider.CheckResult.Type.ALREADY_EXCHANGED } else { res.type = TicketCheckProvider.CheckResult.Type.ERROR } diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt index d2229836..ce7b79a5 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt @@ -57,7 +57,7 @@ interface TicketCheckProvider { class CheckResult { enum class Type { INVALID, VALID, USED, ERROR, UNPAID, BLOCKED, INVALID_TIME, CANCELED, PRODUCT, RULES, - ANSWERS_REQUIRED, AMBIGUOUS, REVOKED, UNAPPROVED + ANSWERS_REQUIRED, AMBIGUOUS, REVOKED, UNAPPROVED, ALREADY_EXCHANGED } var type: Type? = null diff --git a/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/ReusableMedium.sq b/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/ReusableMedium.sq index ede9a5ad..320e32c4 100644 --- a/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/ReusableMedium.sq +++ b/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/ReusableMedium.sq @@ -22,6 +22,12 @@ WHERE ReusableMedium.identifier = :identifier AND ReusableMedium.type = :type AND orders.event_slug IN :event_slugs; +selectByLinkedOrderPosition: +SELECT ReusableMedium.* +FROM ReusableMedium +LEFT JOIN ReusableMedium_OrderPosition ON ReusableMedium_OrderPosition.OrderPositionId = ReusableMedium.id +WHERE OrderPositionId = :order_position_id; + deleteByServerId: DELETE FROM ReusableMedium WHERE server_id = ?; From 97c52225e51b58b608d73b3308e98e28d121b523 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Thu, 21 May 2026 14:48:46 +0200 Subject: [PATCH 02/16] Add server side identfier to MediaPolicy enum --- .../java/eu/pretix/libpretixsync/db/MediaPolicy.kt | 12 ++++++------ .../pretix/libpretixsync/models/db/ItemExtensions.kt | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/MediaPolicy.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/MediaPolicy.kt index 6e034678..f5499030 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/MediaPolicy.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/MediaPolicy.kt @@ -1,8 +1,8 @@ package eu.pretix.libpretixsync.db -enum class MediaPolicy { - NONE, - REUSE, - NEW, - REUSE_OR_NEW, -} +enum class MediaPolicy(val serverName: String?) { + NONE(null), + REUSE("reuse"), + NEW("new"), + REUSE_OR_NEW("reuse_or_new") +} \ No newline at end of file diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/db/ItemExtensions.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/db/ItemExtensions.kt index 045803ec..38f150a4 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/db/ItemExtensions.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/db/ItemExtensions.kt @@ -208,9 +208,9 @@ private fun parseHasFreePrice(json: JSONObject): Boolean { private fun parseMediaPolicy(json: JSONObject): MediaPolicy { return try { val mp: String = json.optString("media_policy") ?: return MediaPolicy.NONE - if (mp == "reuse") return MediaPolicy.REUSE - if (mp == "new") return MediaPolicy.NEW - if (mp == "reuse_or_new") MediaPolicy.REUSE_OR_NEW else MediaPolicy.NONE + MediaPolicy.valueOf(mp) + } catch (_: IllegalArgumentException) { + MediaPolicy.NONE } catch (e: JSONException) { e.printStackTrace() MediaPolicy.NONE From 8a9e4d337faeb066e22932520cd4022fd96cc864 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Thu, 21 May 2026 14:51:45 +0200 Subject: [PATCH 03/16] Return checkin type EXCHANGE_REQUIRED for server responding with status: exchange --- .../pretix/libpretixsync/check/AsyncCheckProvider.kt | 2 ++ .../libpretixsync/check/OnlineCheckProvider.kt | 12 ++++++++++++ .../libpretixsync/check/TicketCheckProvider.kt | 6 +++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt index 4913ab26..d4a80db3 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt @@ -790,6 +790,8 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa return res } + // FIXME: support media exchange, throw EXCHANGE_REQUIRED + // !!! When extending this, also extend checkOfflineWithoutData !!! val rules = list.rules diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt index a587a925..fc01f918 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt @@ -8,7 +8,9 @@ import eu.pretix.libpretixsync.api.PretixApi import eu.pretix.libpretixsync.api.TimeoutApiException import eu.pretix.libpretixsync.config.ConfigStore import eu.pretix.libpretixsync.db.Answer +import eu.pretix.libpretixsync.db.MediaPolicy import eu.pretix.libpretixsync.db.NonceGenerator +import eu.pretix.libpretixsync.db.ReusableMediaType import eu.pretix.libpretixsync.models.db.toModel import eu.pretix.libpretixsync.sqldelight.Question import eu.pretix.libpretixsync.sqldelight.SyncDatabase @@ -124,6 +126,16 @@ class OnlineCheckProvider( required_answers.add(TicketCheckProvider.QuestionAnswer(question, q.toString(), "")) } res.requiredAnswers = required_answers + } else if ("exchange" == status){ + res.type = TicketCheckProvider.CheckResult.Type.EXCHANGE_REQUIRED + try { + res.requiredMediaPolicy = + MediaPolicy.valueOf(response.optString("media_policy")) + res.requiredMediaType = + ReusableMediaType.valueOf(response.optString("media_type")) + } catch (_: IllegalArgumentException) { + // silently fall back to null + } } else { val reason = response.optString("reason") if ("already_redeemed" == reason) { diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt index ce7b79a5..511c8225 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt @@ -2,6 +2,8 @@ package eu.pretix.libpretixsync.check import eu.pretix.libpretixsync.SentryInterface import eu.pretix.libpretixsync.db.Answer +import eu.pretix.libpretixsync.db.MediaPolicy +import eu.pretix.libpretixsync.db.ReusableMediaType import eu.pretix.libpretixsync.models.db.toModel import eu.pretix.libpretixsync.sqldelight.Question import eu.pretix.libpretixsync.models.Question as QuestionModel @@ -57,7 +59,7 @@ interface TicketCheckProvider { class CheckResult { enum class Type { INVALID, VALID, USED, ERROR, UNPAID, BLOCKED, INVALID_TIME, CANCELED, PRODUCT, RULES, - ANSWERS_REQUIRED, AMBIGUOUS, REVOKED, UNAPPROVED, ALREADY_EXCHANGED + ANSWERS_REQUIRED, AMBIGUOUS, REVOKED, UNAPPROVED, ALREADY_EXCHANGED, EXCHANGE_REQUIRED } var type: Type? = null @@ -80,6 +82,8 @@ interface TicketCheckProvider { var position: JSONObject? = null var eventSlug: String? = null var offline: Boolean = false + var requiredMediaPolicy: MediaPolicy? = null + var requiredMediaType: ReusableMediaType? = null constructor(type: Type?, message: String?, offline: Boolean = false) { this.type = type From 3d1fdc346f702d5ef038d91c513bf7eaf80435db Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Tue, 26 May 2026 18:35:55 +0200 Subject: [PATCH 04/16] Rename .java to .kt --- .../db/{ReusableMediaType.java => ReusableMediaType.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename libpretixsync/src/main/java/eu/pretix/libpretixsync/db/{ReusableMediaType.java => ReusableMediaType.kt} (100%) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/ReusableMediaType.java b/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/ReusableMediaType.kt similarity index 100% rename from libpretixsync/src/main/java/eu/pretix/libpretixsync/db/ReusableMediaType.java rename to libpretixsync/src/main/java/eu/pretix/libpretixsync/db/ReusableMediaType.kt From 795cedd2a1380e03c467f1ad8086da6620f142a6 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Tue, 26 May 2026 18:35:55 +0200 Subject: [PATCH 05/16] Enable lookup of MediaPolicy and ResuableMediaType by server side identifier --- .../libpretixsync/check/OnlineCheckProvider.kt | 4 ++-- .../eu/pretix/libpretixsync/db/MediaPolicy.kt | 7 ++++++- .../pretix/libpretixsync/db/ReusableMediaType.kt | 15 +++++++-------- .../libpretixsync/models/db/ItemExtensions.kt | 6 ++---- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt index fc01f918..12e78623 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt @@ -130,9 +130,9 @@ class OnlineCheckProvider( res.type = TicketCheckProvider.CheckResult.Type.EXCHANGE_REQUIRED try { res.requiredMediaPolicy = - MediaPolicy.valueOf(response.optString("media_policy")) + MediaPolicy.getByServerName(response.optString("media_policy")) res.requiredMediaType = - ReusableMediaType.valueOf(response.optString("media_type")) + ReusableMediaType.getByServerName(response.optString("media_type")) } catch (_: IllegalArgumentException) { // silently fall back to null } diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/MediaPolicy.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/MediaPolicy.kt index f5499030..eba41046 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/MediaPolicy.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/MediaPolicy.kt @@ -4,5 +4,10 @@ enum class MediaPolicy(val serverName: String?) { NONE(null), REUSE("reuse"), NEW("new"), - REUSE_OR_NEW("reuse_or_new") + REUSE_OR_NEW("reuse_or_new"); + + companion object { + private val map = entries.associateBy(MediaPolicy::serverName) + fun getByServerName(serverName: String?) = map[serverName] + } } \ No newline at end of file diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/ReusableMediaType.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/ReusableMediaType.kt index c4377bb8..2da54545 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/ReusableMediaType.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/ReusableMediaType.kt @@ -1,6 +1,6 @@ -package eu.pretix.libpretixsync.db; +package eu.pretix.libpretixsync.db -public enum ReusableMediaType { +enum class ReusableMediaType(val serverName: String?) { NONE(null), BARCODE("barcode"), NFC_UID("nfc_uid"), @@ -8,13 +8,12 @@ public enum ReusableMediaType { NFC_MF0AES("nfc_mf0aes"), UNSUPPORTED(null); - public final String serverName; - - private ReusableMediaType(String serverName) { - this.serverName = serverName; + fun isNfcBased(): Boolean { + return this.serverName?.startsWith("nfc_") ?: false; } - public boolean isNfcBased() { - return this.serverName.startsWith("nfc_"); + companion object { + private val map = ReusableMediaType.entries.associateBy(ReusableMediaType::serverName) + fun getByServerName(serverName: String?) = map[serverName] } } diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/db/ItemExtensions.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/db/ItemExtensions.kt index 38f150a4..7307ed9d 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/db/ItemExtensions.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/db/ItemExtensions.kt @@ -208,7 +208,7 @@ private fun parseHasFreePrice(json: JSONObject): Boolean { private fun parseMediaPolicy(json: JSONObject): MediaPolicy { return try { val mp: String = json.optString("media_policy") ?: return MediaPolicy.NONE - MediaPolicy.valueOf(mp) + MediaPolicy.getByServerName(mp) ?: return MediaPolicy.NONE } catch (_: IllegalArgumentException) { MediaPolicy.NONE } catch (e: JSONException) { @@ -220,9 +220,7 @@ private fun parseMediaPolicy(json: JSONObject): MediaPolicy { private fun parseMediaType(json: JSONObject): ReusableMediaType { return try { val mp: String = json.optString("media_type") ?: return ReusableMediaType.NONE - if (mp == "barcode") return ReusableMediaType.BARCODE - if (mp == "nfc_uid") return ReusableMediaType.NFC_UID - if (mp == "nfc_mf0aes") ReusableMediaType.NFC_MF0AES else ReusableMediaType.UNSUPPORTED + ReusableMediaType.getByServerName(mp) ?: ReusableMediaType.UNSUPPORTED } catch (e: JSONException) { e.printStackTrace() ReusableMediaType.NONE From 728cefb10a3ff8d384a29befcc44485a34ebabe8 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Tue, 26 May 2026 18:36:13 +0200 Subject: [PATCH 06/16] Add API calls for loading and linking resuable media --- .../eu/pretix/libpretixsync/api/PretixApi.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt index 33e71625..15465b46 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt @@ -178,6 +178,25 @@ open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, h } } + open fun loadMedium(type: String, identifier: String): ApiResponse { + val payload = JSONObject() + payload.put("type", type) + payload.put("identifier", identifier) + return postResource( + organizerResourceUrl("reusablemedia") + "lookup/?expand=linked_orderposition&expand=linked_orderpositions&expand=linked_giftcard&expand=linked_giftcard.owner_ticket", + payload + ) + } + + open fun linkMedium(reusableMediumId: Long, orderPositionId: Long): ApiResponse { + val payload = JSONObject() + payload.put("linked_orderposition", orderPositionId) + return patchResource( + organizerResourceUrl("reusablemedia/${reusableMediumId}"), + payload + ) + } + fun apiURL(suffix: String): String { return try { URL(URL(url), "/api/v1/$suffix").toString() From 16973fe281fb8ededaa02cc6572f305fef9bcb87 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Thu, 28 May 2026 11:57:26 +0200 Subject: [PATCH 07/16] Handle exchange required in AsyncCheckProvider too, gate already_exchanged behind reusableMediaUsageEnforced setting --- .../libpretixsync/check/AsyncCheckProvider.kt | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt index d4a80db3..9afc964b 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt @@ -8,8 +8,10 @@ import eu.pretix.libpretixsync.crypto.isValidSignature import eu.pretix.libpretixsync.crypto.readPubkeyFromPem import eu.pretix.libpretixsync.crypto.sig1.TicketProtos import eu.pretix.libpretixsync.db.Answer +import eu.pretix.libpretixsync.db.MediaPolicy import eu.pretix.libpretixsync.db.NonceGenerator import eu.pretix.libpretixsync.db.QuestionLike +import eu.pretix.libpretixsync.db.ReusableMediaType import eu.pretix.libpretixsync.models.CheckIn import eu.pretix.libpretixsync.models.Event import eu.pretix.libpretixsync.models.Order as OrderModel @@ -781,16 +783,25 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa return res } + val settings = db.settingsQueries.selectBySlug(eventSlug).executeAsOneOrNull()?.toModel() + val reusableMediaUsageEnforced = (settings?.json?.optBoolean("reusable_media_usage_enforced", false) == true) + val linkedReusableMedium = db.reusableMediumQueries.selectByLinkedOrderPosition(position.positionId) .executeAsOneOrNull()?.toModel() - if (linkedReusableMedium != null) { - res.type = TicketCheckProvider.CheckResult.Type.ALREADY_EXCHANGED - res.isCheckinAllowed = false - storeFailedCheckin(eventSlug, list.serverId, "already_exchanged", position.secret!!, type, position = position.serverId, item = positionItem.serverId, variation = position.variationServerId, subevent = position.subEventServerId, nonce = nonce) - return res - } - // FIXME: support media exchange, throw EXCHANGE_REQUIRED + if (positionItem.mediaPolicy != MediaPolicy.NONE && positionItem.mediaType != ReusableMediaType.NONE) { + if (linkedReusableMedium == null) { + res.type = TicketCheckProvider.CheckResult.Type.EXCHANGE_REQUIRED + res.isCheckinAllowed = false + storeFailedCheckin(eventSlug, list.serverId, "exchange", position.secret!!, type, position = position.serverId, item = positionItem.serverId, variation = position.variationServerId, subevent = position.subEventServerId, nonce = nonce) + return res + } else if (reusableMediaUsageEnforced) { + res.type = TicketCheckProvider.CheckResult.Type.ALREADY_EXCHANGED + res.isCheckinAllowed = false + storeFailedCheckin(eventSlug, list.serverId, "already_exchanged", position.secret!!, type, position = position.serverId, item = positionItem.serverId, variation = position.variationServerId, subevent = position.subEventServerId, nonce = nonce) + return res + } + } // !!! When extending this, also extend checkOfflineWithoutData !!! From 1c58073717163bfc53eca7e1639c4fbdd5051ab7 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Sat, 30 May 2026 14:15:43 +0200 Subject: [PATCH 08/16] Add support for reusable media exchange to checkinrpc --- .../eu/pretix/libpretixsync/api/PretixApi.kt | 22 ++++++++++++++-- .../eu/pretix/libpretixsync/api/ProxyApi.kt | 5 +++- .../libpretixsync/check/AsyncCheckProvider.kt | 25 +++++++++++++++++-- .../check/OnlineCheckProvider.kt | 8 +++++- .../libpretixsync/check/ProxyCheckProvider.kt | 8 +++++- .../check/TicketCheckProvider.kt | 16 +++++++++++- 6 files changed, 76 insertions(+), 8 deletions(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt index 15465b46..3ba0981b 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt @@ -99,7 +99,23 @@ open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, h } @Throws(ApiException::class, JSONException::class) - open fun redeem(lists: List, secret: String, datetime: String?, force: Boolean, nonce: String?, answers: List?, ignore_unpaid: Boolean, pdf_data: Boolean, type: String?, source_type: String?, callTimeout: Long? = null, questions_supported: Boolean = true): ApiResponse { + open fun redeem( + lists: List, + secret: String, + datetime: String?, + force: Boolean, + nonce: String?, + answers: List?, + ignore_unpaid: Boolean, + pdf_data: Boolean, + type: String?, + source_type: String?, + callTimeout: Long? = null, + questions_supported: Boolean = true, + media_type: String? = null, + media_identifier: String? = null, + media_action: String? = null, + ): ApiResponse { val body = JSONObject() if (datetime != null) { body.put("datetime", datetime) @@ -129,13 +145,15 @@ open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, h body.put("answers", answerbody) body.put("questions_supported", questions_supported) body.put("canceled_supported", true) - body.put("media_exchange_supported", true) body.put("secret", secret) val jlists = JSONArray() for (l in lists) { jlists.put(l) } body.put("lists", jlists) + if (media_type != null) body.put("media_type", media_type) + if (media_identifier != null) body.put("media_identifier", media_identifier) + if (media_action != null) body.put("media_action", media_action) var pd = "?expand=answers.question" if (pdf_data) { pd += "&pdf_data=true" diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt index 3021f0bb..1e4eb7d0 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt @@ -25,7 +25,10 @@ data class MultiCheckInput( // TODO: Check unused values val allowQuestions: Boolean, - val nonce: String? + val nonce: String?, + val media_type: String?, + val media_identifier: String?, + val media_action: String?, ) data class CheckInput( diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt index 9afc964b..e68db80d 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt @@ -281,7 +281,14 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa return RSAResult(givenAnswers, requiredAnswers, shownAnswers, askQuestions) } - private fun checkOfflineWithoutData(eventsAndCheckinLists: Map, ticketid: String, type: TicketCheckProvider.CheckInType, answers: List?, nonce: String?, allowQuestions: Boolean): TicketCheckProvider.CheckResult { + private fun checkOfflineWithoutData( + eventsAndCheckinLists: Map, + ticketid: String, + type: TicketCheckProvider.CheckInType, + answers: List?, + nonce: String?, + allowQuestions: Boolean + ): TicketCheckProvider.CheckResult { val dt = now() val events = db.eventQueries.selectBySlugList(eventsAndCheckinLists.keys.toList()) .executeAsList() @@ -546,11 +553,16 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa type: TicketCheckProvider.CheckInType, nonce: String?, allowQuestions: Boolean, + media_type: String?, + media_identifier: String?, + media_action: String?, ): TicketCheckProvider.CheckResult { val ticketid_cleaned = cleanInput(ticketid, source_type) sentry.addBreadcrumb("provider.check", "offline check started") + // FIXME: we don't do offline reusable media exchange, error out if set + val tickets = db.orderPositionQueries.selectBySecretAndEventSlugs( secret = ticketid_cleaned, event_slugs = eventsAndCheckinLists.keys.toList(), @@ -611,7 +623,16 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa return checkOfflineWithData(eventsAndCheckinLists, ticketid_cleaned, tickets, answers, ignore_unpaid, type, nonce = nonce, allowQuestions = allowQuestions) } - private fun checkOfflineWithData(eventsAndCheckinLists: Map, secret: String, tickets: List, answers: List?, ignore_unpaid: Boolean, type: TicketCheckProvider.CheckInType, nonce: String?, allowQuestions: Boolean): TicketCheckProvider.CheckResult { + private fun checkOfflineWithData( + eventsAndCheckinLists: Map, + secret: String, + tickets: List, + answers: List?, + ignore_unpaid: Boolean, + type: TicketCheckProvider.CheckInType, + nonce: String?, + allowQuestions: Boolean, + ): TicketCheckProvider.CheckResult { // !!! When extending this, also extend checkOfflineWithoutData !!! val dt = now() diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt index 12e78623..dd4e1ddb 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt @@ -49,7 +49,10 @@ class OnlineCheckProvider( with_badge_data: Boolean, type: TicketCheckProvider.CheckInType, nonce: String?, - allowQuestions: Boolean + allowQuestions: Boolean, + media_type: String?, + media_identifier: String?, + media_action: String?, ): TicketCheckProvider.CheckResult { val ticketid_cleaned = cleanInput(ticketid, source_type) val nonce_cleaned = nonce ?: NonceGenerator.nextNonce() @@ -72,6 +75,9 @@ class OnlineCheckProvider( source_type, callTimeout = if (fallback != null) fallbackTimeout.toLong() else null, questions_supported = allowQuestions, + media_type = media_type, + media_identifier = media_identifier, + media_action = media_action, ) } else { if (eventsAndCheckinLists.size != 1) throw CheckException("Multi-event scan not supported by server.") diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt index 81ac73c0..250aa245 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt @@ -109,7 +109,10 @@ class ProxyCheckProvider(private val config: ConfigStore, httpClientFactory: Htt with_badge_data: Boolean, type: TicketCheckProvider.CheckInType, nonce: String?, - allowQuestions: Boolean + allowQuestions: Boolean, + media_type: String?, + media_identifier: String?, + media_action: String?, ): TicketCheckProvider.CheckResult { val answersInput = answers?.map { val questionModel = it.question as Question // TODO: Can we avoid the cast? @@ -130,6 +133,9 @@ class ProxyCheckProvider(private val config: ConfigStore, httpClientFactory: Htt type = type.name, allowQuestions = allowQuestions, nonce = nonce, + media_type = media_type, + media_identifier = media_identifier, + media_action = media_action, ) return try { diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt index 511c8225..2e85420e 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt @@ -161,7 +161,21 @@ interface TicketCheckProvider { class StatusResult(var eventName: String?, var totalTickets: Int, var alreadyScanned: Int, var currentlyInside: Int?, var items: List?) { } - fun check(eventsAndCheckinLists: Map, ticketid: String, source_type: String, answers: List?, ignore_unpaid: Boolean, with_badge_data: Boolean, type: CheckInType, nonce: String? = null, allowQuestions: Boolean = true): CheckResult + fun check( + eventsAndCheckinLists: Map, + ticketid: String, + source_type: String, + answers: List?, + ignore_unpaid: Boolean, + with_badge_data: Boolean, + type: CheckInType, + nonce: String? = null, + allowQuestions: Boolean = true, + media_type: String? = null, + media_identifier: String? = null, + media_action: String? = null, + ): CheckResult + fun check(eventsAndCheckinLists: Map, ticketid: String): CheckResult @Throws(CheckException::class) fun search(eventsAndCheckinLists: Map, query: String, page: Int): List From 9b3be03e9c6519ae987e7cd36ff8aa2fe79b3287 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Sat, 30 May 2026 14:33:28 +0200 Subject: [PATCH 09/16] Add media exchange parameters to FakePretixApi too --- .../pretix/pretixscan/scanproxy/tests/test/FakePretixApi.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libpretixsync/src/testFixtures/java/eu/pretix/pretixscan/scanproxy/tests/test/FakePretixApi.kt b/libpretixsync/src/testFixtures/java/eu/pretix/pretixscan/scanproxy/tests/test/FakePretixApi.kt index 284b4182..bb0a82dc 100644 --- a/libpretixsync/src/testFixtures/java/eu/pretix/pretixscan/scanproxy/tests/test/FakePretixApi.kt +++ b/libpretixsync/src/testFixtures/java/eu/pretix/pretixscan/scanproxy/tests/test/FakePretixApi.kt @@ -42,7 +42,10 @@ class FakePretixApi : PretixApi("http://1.1.1.1/", "a", "demo", 1, DefaultHttpCl type: String?, source_type: String?, callTimeout: Long?, - questions_supported: Boolean + questions_supported: Boolean, + media_type: String?, + media_identifier: String?, + media_action: String?, ): ApiResponse { redeemRequestSecret = secret redeemRequestDatetime = datetime From f049a27274acd3f85fa9a5f56257e5b323ccdd55 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Tue, 9 Jun 2026 10:21:28 +0200 Subject: [PATCH 10/16] Use exchange_ prefix and singular for api properties --- .../java/eu/pretix/libpretixsync/api/PretixApi.kt | 12 ++++++------ .../java/eu/pretix/libpretixsync/api/ProxyApi.kt | 6 +++--- .../pretix/libpretixsync/check/AsyncCheckProvider.kt | 6 +++--- .../libpretixsync/check/OnlineCheckProvider.kt | 12 ++++++------ .../pretix/libpretixsync/check/ProxyCheckProvider.kt | 12 ++++++------ .../libpretixsync/check/TicketCheckProvider.kt | 6 +++--- .../pretixscan/scanproxy/tests/test/FakePretixApi.kt | 6 +++--- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt index 3ba0981b..f121261d 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt @@ -112,9 +112,9 @@ open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, h source_type: String?, callTimeout: Long? = null, questions_supported: Boolean = true, - media_type: String? = null, - media_identifier: String? = null, - media_action: String? = null, + exchange_medium_type: String? = null, + exchange_medium_identifier: String? = null, + exchange_link_action: String? = null, ): ApiResponse { val body = JSONObject() if (datetime != null) { @@ -151,9 +151,9 @@ open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, h jlists.put(l) } body.put("lists", jlists) - if (media_type != null) body.put("media_type", media_type) - if (media_identifier != null) body.put("media_identifier", media_identifier) - if (media_action != null) body.put("media_action", media_action) + if (exchange_medium_type != null) body.put("exchange_medium_type", exchange_medium_type) + if (exchange_medium_identifier != null) body.put("exchange_medium_identifier", exchange_medium_identifier) + if (exchange_link_action != null) body.put("exchange_link_action", exchange_link_action) var pd = "?expand=answers.question" if (pdf_data) { pd += "&pdf_data=true" diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt index 1e4eb7d0..2f395906 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt @@ -26,9 +26,9 @@ data class MultiCheckInput( // TODO: Check unused values val allowQuestions: Boolean, val nonce: String?, - val media_type: String?, - val media_identifier: String?, - val media_action: String?, + val exchange_medium_type: String?, + val exchange_medium_identifier: String?, + val exchange_link_action: String?, ) data class CheckInput( diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt index e68db80d..1d65adb8 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt @@ -553,9 +553,9 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa type: TicketCheckProvider.CheckInType, nonce: String?, allowQuestions: Boolean, - media_type: String?, - media_identifier: String?, - media_action: String?, + exchange_medium_type: String?, + exchange_medium_identifier: String?, + exchange_link_action: String?, ): TicketCheckProvider.CheckResult { val ticketid_cleaned = cleanInput(ticketid, source_type) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt index dd4e1ddb..60cfe31e 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt @@ -50,9 +50,9 @@ class OnlineCheckProvider( type: TicketCheckProvider.CheckInType, nonce: String?, allowQuestions: Boolean, - media_type: String?, - media_identifier: String?, - media_action: String?, + exchange_medium_type: String?, + exchange_medium_identifier: String?, + exchange_link_action: String?, ): TicketCheckProvider.CheckResult { val ticketid_cleaned = cleanInput(ticketid, source_type) val nonce_cleaned = nonce ?: NonceGenerator.nextNonce() @@ -75,9 +75,9 @@ class OnlineCheckProvider( source_type, callTimeout = if (fallback != null) fallbackTimeout.toLong() else null, questions_supported = allowQuestions, - media_type = media_type, - media_identifier = media_identifier, - media_action = media_action, + exchange_medium_type = exchange_medium_type, + exchange_medium_identifier = exchange_medium_identifier, + exchange_link_action = exchange_link_action, ) } else { if (eventsAndCheckinLists.size != 1) throw CheckException("Multi-event scan not supported by server.") diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt index 250aa245..a2851b62 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt @@ -110,9 +110,9 @@ class ProxyCheckProvider(private val config: ConfigStore, httpClientFactory: Htt type: TicketCheckProvider.CheckInType, nonce: String?, allowQuestions: Boolean, - media_type: String?, - media_identifier: String?, - media_action: String?, + exchange_medium_type: String?, + exchange_medium_identifier: String?, + exchange_link_action: String?, ): TicketCheckProvider.CheckResult { val answersInput = answers?.map { val questionModel = it.question as Question // TODO: Can we avoid the cast? @@ -133,9 +133,9 @@ class ProxyCheckProvider(private val config: ConfigStore, httpClientFactory: Htt type = type.name, allowQuestions = allowQuestions, nonce = nonce, - media_type = media_type, - media_identifier = media_identifier, - media_action = media_action, + exchange_medium_type = exchange_medium_type, + exchange_medium_identifier = exchange_medium_identifier, + exchange_link_action = exchange_link_action, ) return try { diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt index 2e85420e..a229f6ca 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt @@ -171,9 +171,9 @@ interface TicketCheckProvider { type: CheckInType, nonce: String? = null, allowQuestions: Boolean = true, - media_type: String? = null, - media_identifier: String? = null, - media_action: String? = null, + exchange_medium_type: String? = null, + exchange_medium_identifier: String? = null, + exchange_link_action: String? = null, ): CheckResult fun check(eventsAndCheckinLists: Map, ticketid: String): CheckResult diff --git a/libpretixsync/src/testFixtures/java/eu/pretix/pretixscan/scanproxy/tests/test/FakePretixApi.kt b/libpretixsync/src/testFixtures/java/eu/pretix/pretixscan/scanproxy/tests/test/FakePretixApi.kt index bb0a82dc..96467b20 100644 --- a/libpretixsync/src/testFixtures/java/eu/pretix/pretixscan/scanproxy/tests/test/FakePretixApi.kt +++ b/libpretixsync/src/testFixtures/java/eu/pretix/pretixscan/scanproxy/tests/test/FakePretixApi.kt @@ -43,9 +43,9 @@ class FakePretixApi : PretixApi("http://1.1.1.1/", "a", "demo", 1, DefaultHttpCl source_type: String?, callTimeout: Long?, questions_supported: Boolean, - media_type: String?, - media_identifier: String?, - media_action: String?, + exchange_medium_type: String?, + exchange_medium_identifier: String?, + exchange_link_action: String?, ): ApiResponse { redeemRequestSecret = secret redeemRequestDatetime = datetime From 01ada7220d311d10f3bdfa7fd0eba65bbd1c5cae Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Tue, 9 Jun 2026 10:28:25 +0200 Subject: [PATCH 11/16] Add new error types for reusable medium exchange --- .../java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt index a229f6ca..1e5eed09 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt @@ -59,7 +59,8 @@ interface TicketCheckProvider { class CheckResult { enum class Type { INVALID, VALID, USED, ERROR, UNPAID, BLOCKED, INVALID_TIME, CANCELED, PRODUCT, RULES, - ANSWERS_REQUIRED, AMBIGUOUS, REVOKED, UNAPPROVED, ALREADY_EXCHANGED, EXCHANGE_REQUIRED + ANSWERS_REQUIRED, AMBIGUOUS, REVOKED, UNAPPROVED, ALREADY_EXCHANGED, MEDIUM_INVALID, + MEDIUM_EXISTS, EXCHANGE_REQUIRED } var type: Type? = null From d978e9fae11126ac26e0ba5a51b69a6721c41283 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Tue, 9 Jun 2026 12:19:25 +0200 Subject: [PATCH 12/16] Fix ambiguous null ReusableMediaType --- .../main/java/eu/pretix/libpretixsync/db/ReusableMediaType.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/ReusableMediaType.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/ReusableMediaType.kt index 2da54545..bed4d0da 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/ReusableMediaType.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/ReusableMediaType.kt @@ -6,7 +6,7 @@ enum class ReusableMediaType(val serverName: String?) { NFC_UID("nfc_uid"), NFC_MF0AES("nfc_mf0aes"), - UNSUPPORTED(null); + UNSUPPORTED("unsupported"); fun isNfcBased(): Boolean { return this.serverName?.startsWith("nfc_") ?: false; From a7f59fe4aa384c4c6cf6c73e2ed6886d4f28a2ff Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Tue, 9 Jun 2026 16:40:16 +0200 Subject: [PATCH 13/16] Remove exchange_link_action, server knows best --- .../main/java/eu/pretix/libpretixsync/api/PretixApi.kt | 2 -- .../src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt | 1 - .../eu/pretix/libpretixsync/check/AsyncCheckProvider.kt | 8 ++++---- .../eu/pretix/libpretixsync/check/OnlineCheckProvider.kt | 2 -- .../eu/pretix/libpretixsync/check/ProxyCheckProvider.kt | 2 -- .../eu/pretix/libpretixsync/check/TicketCheckProvider.kt | 1 - .../pretixscan/scanproxy/tests/test/FakePretixApi.kt | 1 - 7 files changed, 4 insertions(+), 13 deletions(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt index f121261d..0642a275 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt @@ -114,7 +114,6 @@ open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, h questions_supported: Boolean = true, exchange_medium_type: String? = null, exchange_medium_identifier: String? = null, - exchange_link_action: String? = null, ): ApiResponse { val body = JSONObject() if (datetime != null) { @@ -153,7 +152,6 @@ open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, h body.put("lists", jlists) if (exchange_medium_type != null) body.put("exchange_medium_type", exchange_medium_type) if (exchange_medium_identifier != null) body.put("exchange_medium_identifier", exchange_medium_identifier) - if (exchange_link_action != null) body.put("exchange_link_action", exchange_link_action) var pd = "?expand=answers.question" if (pdf_data) { pd += "&pdf_data=true" diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt index 2f395906..800512b6 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt @@ -28,7 +28,6 @@ data class MultiCheckInput( val nonce: String?, val exchange_medium_type: String?, val exchange_medium_identifier: String?, - val exchange_link_action: String?, ) data class CheckInput( diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt index 1d65adb8..9a85454a 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt @@ -555,7 +555,6 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa allowQuestions: Boolean, exchange_medium_type: String?, exchange_medium_identifier: String?, - exchange_link_action: String?, ): TicketCheckProvider.CheckResult { val ticketid_cleaned = cleanInput(ticketid, source_type) @@ -807,11 +806,12 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa val settings = db.settingsQueries.selectBySlug(eventSlug).executeAsOneOrNull()?.toModel() val reusableMediaUsageEnforced = (settings?.json?.optBoolean("reusable_media_usage_enforced", false) == true) - val linkedReusableMedium = db.reusableMediumQueries.selectByLinkedOrderPosition(position.positionId) - .executeAsOneOrNull()?.toModel() + val hasLinkedReusableMedium = + db.reusableMediumQueries.selectByLinkedOrderPosition(position.positionId) + .executeAsList().isNotEmpty() if (positionItem.mediaPolicy != MediaPolicy.NONE && positionItem.mediaType != ReusableMediaType.NONE) { - if (linkedReusableMedium == null) { + if (!hasLinkedReusableMedium) { res.type = TicketCheckProvider.CheckResult.Type.EXCHANGE_REQUIRED res.isCheckinAllowed = false storeFailedCheckin(eventSlug, list.serverId, "exchange", position.secret!!, type, position = position.serverId, item = positionItem.serverId, variation = position.variationServerId, subevent = position.subEventServerId, nonce = nonce) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt index 60cfe31e..7af71942 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt @@ -52,7 +52,6 @@ class OnlineCheckProvider( allowQuestions: Boolean, exchange_medium_type: String?, exchange_medium_identifier: String?, - exchange_link_action: String?, ): TicketCheckProvider.CheckResult { val ticketid_cleaned = cleanInput(ticketid, source_type) val nonce_cleaned = nonce ?: NonceGenerator.nextNonce() @@ -77,7 +76,6 @@ class OnlineCheckProvider( questions_supported = allowQuestions, exchange_medium_type = exchange_medium_type, exchange_medium_identifier = exchange_medium_identifier, - exchange_link_action = exchange_link_action, ) } else { if (eventsAndCheckinLists.size != 1) throw CheckException("Multi-event scan not supported by server.") diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt index a2851b62..232fb758 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt @@ -112,7 +112,6 @@ class ProxyCheckProvider(private val config: ConfigStore, httpClientFactory: Htt allowQuestions: Boolean, exchange_medium_type: String?, exchange_medium_identifier: String?, - exchange_link_action: String?, ): TicketCheckProvider.CheckResult { val answersInput = answers?.map { val questionModel = it.question as Question // TODO: Can we avoid the cast? @@ -135,7 +134,6 @@ class ProxyCheckProvider(private val config: ConfigStore, httpClientFactory: Htt nonce = nonce, exchange_medium_type = exchange_medium_type, exchange_medium_identifier = exchange_medium_identifier, - exchange_link_action = exchange_link_action, ) return try { diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt index 1e5eed09..b8b08774 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt @@ -174,7 +174,6 @@ interface TicketCheckProvider { allowQuestions: Boolean = true, exchange_medium_type: String? = null, exchange_medium_identifier: String? = null, - exchange_link_action: String? = null, ): CheckResult fun check(eventsAndCheckinLists: Map, ticketid: String): CheckResult diff --git a/libpretixsync/src/testFixtures/java/eu/pretix/pretixscan/scanproxy/tests/test/FakePretixApi.kt b/libpretixsync/src/testFixtures/java/eu/pretix/pretixscan/scanproxy/tests/test/FakePretixApi.kt index 96467b20..fbf2730e 100644 --- a/libpretixsync/src/testFixtures/java/eu/pretix/pretixscan/scanproxy/tests/test/FakePretixApi.kt +++ b/libpretixsync/src/testFixtures/java/eu/pretix/pretixscan/scanproxy/tests/test/FakePretixApi.kt @@ -45,7 +45,6 @@ class FakePretixApi : PretixApi("http://1.1.1.1/", "a", "demo", 1, DefaultHttpCl questions_supported: Boolean, exchange_medium_type: String?, exchange_medium_identifier: String?, - exchange_link_action: String?, ): ApiResponse { redeemRequestSecret = secret redeemRequestDatetime = datetime From d5a6723c5fd3fc759b2af458a3da9ab8df7af651 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Thu, 11 Jun 2026 10:04:00 +0200 Subject: [PATCH 14/16] Add MediaPolicy.APPEND and MediaPolicy.APPEND_OR_NEW --- .../src/main/java/eu/pretix/libpretixsync/db/MediaPolicy.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/MediaPolicy.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/MediaPolicy.kt index eba41046..89158905 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/MediaPolicy.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/db/MediaPolicy.kt @@ -2,9 +2,11 @@ package eu.pretix.libpretixsync.db enum class MediaPolicy(val serverName: String?) { NONE(null), - REUSE("reuse"), NEW("new"), - REUSE_OR_NEW("reuse_or_new"); + REUSE("reuse"), + REUSE_OR_NEW("reuse_or_new"), + APPEND("append"), + APPEND_OR_NEW("append_or_new"); companion object { private val map = entries.associateBy(MediaPolicy::serverName) From 71815c932f78aea55245403366282d569806cf91 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Thu, 11 Jun 2026 10:11:07 +0200 Subject: [PATCH 15/16] Don't try to support media exchange while offline --- .../pretix/libpretixsync/check/AsyncCheckProvider.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt index 9a85454a..4eb5b403 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt @@ -560,7 +560,13 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa sentry.addBreadcrumb("provider.check", "offline check started") - // FIXME: we don't do offline reusable media exchange, error out if set + if (exchange_medium_type != null || exchange_medium_identifier != null) { + return TicketCheckProvider.CheckResult( + TicketCheckProvider.CheckResult.Type.ERROR, + "Media exchange is not supported in offline mode", + offline = true + ) + } val tickets = db.orderPositionQueries.selectBySecretAndEventSlugs( secret = ticketid_cleaned, @@ -812,8 +818,9 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa if (positionItem.mediaPolicy != MediaPolicy.NONE && positionItem.mediaType != ReusableMediaType.NONE) { if (!hasLinkedReusableMedium) { - res.type = TicketCheckProvider.CheckResult.Type.EXCHANGE_REQUIRED + res.type = TicketCheckProvider.CheckResult.Type.ERROR // EXCHANGE_REQUIRED, but not in offline mode res.isCheckinAllowed = false + res.reasonExplanation = "This ticket needs to be exchanged, but this isn't possible while offline" storeFailedCheckin(eventSlug, list.serverId, "exchange", position.secret!!, type, position = position.serverId, item = positionItem.serverId, variation = position.variationServerId, subevent = position.subEventServerId, nonce = nonce) return res } else if (reusableMediaUsageEnforced) { From 302482b9a63a54ba1f40223ab726bf2dc7d20082 Mon Sep 17 00:00:00 2001 From: robbi5 Date: Thu, 11 Jun 2026 12:44:45 +0200 Subject: [PATCH 16/16] Remove loadMedium and linkMedium from API again --- .../eu/pretix/libpretixsync/api/PretixApi.kt | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt index 0642a275..d000dad7 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt @@ -194,25 +194,6 @@ open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, h } } - open fun loadMedium(type: String, identifier: String): ApiResponse { - val payload = JSONObject() - payload.put("type", type) - payload.put("identifier", identifier) - return postResource( - organizerResourceUrl("reusablemedia") + "lookup/?expand=linked_orderposition&expand=linked_orderpositions&expand=linked_giftcard&expand=linked_giftcard.owner_ticket", - payload - ) - } - - open fun linkMedium(reusableMediumId: Long, orderPositionId: Long): ApiResponse { - val payload = JSONObject() - payload.put("linked_orderposition", orderPositionId) - return patchResource( - organizerResourceUrl("reusablemedia/${reusableMediumId}"), - payload - ) - } - fun apiURL(suffix: String): String { return try { URL(URL(url), "/api/v1/$suffix").toString()