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 d23af64a..47db9b0c 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt @@ -465,6 +465,28 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa res.isRequireAttention = require_attention || (variation?.isCheckin_attention == true) res.checkinTexts = listOfNotNull(variation?.checkin_text?.trim(), item.checkInText?.trim()).filterNot { it.isBlank() }.filterNot { it.isBlank() || it == "null" } + val settings = db.settingsQueries.selectBySlug(eventSlug).executeAsOneOrNull()?.toModel() + val reusableMediaUsageEnforced = (settings?.json?.optBoolean("reusable_media_usage_enforced", false) == true) + + if (item.mediaPolicy != MediaPolicy.NONE && item.mediaType != ReusableMediaType.NONE && reusableMediaUsageEnforced) { + res.type = TicketCheckProvider.CheckResult.Type.EXCHANGE_REQUIRED_OFFLINE + res.isCheckinAllowed = false + res.reasonExplanation = "This ticket needs to be exchanged, but this isn't possible while offline" + storeFailedCheckin( + eventSlug, + listId, + "exchange", + ticketid, + source_type, + type, + item = item.serverId, + variation = decoded.variation, + subevent = decoded.subevent, + nonce = nonce + ) + return res + } + val queuedCheckIns = db.queuedCheckInQueries.selectBySecret(ticketid) .executeAsList() .filter { it.checkinListId == listId && it.annulled == null } @@ -824,9 +846,20 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa val message: String? = null ) + private fun isProductOnList(list: CheckInList, position: OrderPositionModel): Boolean { + if (list.allItems) { + return true + } + + return db.checkInListQueries.selectItemIdsForList(list.id) + .executeAsList() + .any { it.id == position.itemId } + } + private fun filterPositions(eventsAndCheckinLists: Map, positions: List): Pair, List> { var results = mutableListOf() val errors = mutableListOf() + val listsByPositionId = mutableMapOf() positions.forEach { position -> val order = db.orderQueries.selectById(position.orderId).executeAsOne().toModel() @@ -864,25 +897,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa candidates.addAll(orderPositions.filter { it.addonToServerId == position.serverId }) - // server side: 3b. - val filteredCandidates = if (!list.allItems) { - val items = db.checkInListQueries.selectItemIdsForList(list.id) - .executeAsList() - .map { - // Not-null assertion needed for SQLite - it.id!! - } - .toHashSet() - candidates.filter { candidate -> - val candidateItem = - db.itemQueries.selectById(candidate.itemId).executeAsOne() - items.contains(candidateItem.id) - } - } else { - // This is a useless configuration that the backend won't allow, but we'll still handle - // it here for completeness - candidates - } + val filteredCandidates = candidates.filter { isProductOnList(list, it) } if (filteredCandidates.isEmpty()) { errors.add(PositionFilteringError(position, eventSlug, list, TicketCheckProvider.CheckResult.Type.PRODUCT)) @@ -896,6 +911,14 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa } results.addAll(resultingPositions) + resultingPositions.forEach { listsByPositionId[it.id] = list } + } + + val candidatePositions = results.toList() + + // server side: 3b. + if (results.size > 1) { + results = results.filter { isProductOnList(listsByPositionId.getValue(it.id), it) }.toMutableList() } // server side: 3c. @@ -910,50 +933,16 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa // We try to improve the error message by selecting the product that will "work next" or - if none matches - "worked last". if (results.isEmpty()) { val nowOdt = javaTimeNow() - var nearestCandidate: OrderPositionModel? = null - - positions.forEach { - if (it.validFrom != null && - (it.validFrom > nowOdt || - (nearestCandidate != null && it.validFrom < nearestCandidate.validFrom))) { - nearestCandidate = it - } - } - - if (nearestCandidate == null) { - positions.forEach { - if (it.validUntil != null && - (it.validUntil < nowOdt || - (nearestCandidate != null && it.validUntil > nearestCandidate.validUntil))) { - nearestCandidate = it - } - } - } - - if (nearestCandidate != null) { - val order = db.orderQueries.selectById(nearestCandidate.orderId).executeAsOne().toModel() - - val eventSlug = order.eventSlug - val event = db.eventQueries.selectBySlug(eventSlug).executeAsOneOrNull()?.toModel() - if (event == null) { - return Pair(listOf(), listOf(PositionFilteringError(nearestCandidate, eventSlug, null, TicketCheckProvider.CheckResult.Type.ERROR, "Event not found"))) - } - - val listId = eventsAndCheckinLists[eventSlug] - if (listId == null) { - return Pair(listOf(), listOf(PositionFilteringError(nearestCandidate, eventSlug, null, TicketCheckProvider.CheckResult.Type.ERROR, "No check-in list selected"))) - } - - val list = db.checkInListQueries.selectByServerIdAndEventSlug( - server_id = listId, - event_slug = eventSlug, - ).executeAsOneOrNull()?.toModel() - - if (list == null) { - return Pair(listOf(), listOf(PositionFilteringError(nearestCandidate, eventSlug, null, TicketCheckProvider.CheckResult.Type.ERROR, "Check-in list not found"))) - } - - return Pair(listOf(), listOf(PositionFilteringError(nearestCandidate, eventSlug, list, TicketCheckProvider.CheckResult.Type.INVALID_TIME))) + val fallbackCandidate = candidatePositions + .filter { it.validFrom != null && it.validFrom > nowOdt } + .minByOrNull { it.validFrom!! } + ?: candidatePositions + .filter { it.validUntil != null && it.validUntil < nowOdt } + .maxByOrNull { it.validUntil!! } + ?: candidatePositions.firstOrNull() + + if (fallbackCandidate != null) { + results = mutableListOf(fallbackCandidate) } } @@ -1010,20 +999,6 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa subevent = firstError.position.subEventServerId, nonce = nonce ) - TicketCheckProvider.CheckResult.Type.INVALID_TIME -> - storeFailedCheckin( - firstError.eventSlug, - firstError.list.serverId, - "invalid_time", - secret, - source_type, - type, - position = firstError.position.serverId, - item = item.serverId, - variation = firstError.position.variationServerId, - subevent = firstError.position.subEventServerId, - nonce = nonce - ) else -> {} } } @@ -1201,7 +1176,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa eventSlug, list.serverId, "invalid_time", - position.secret!!, + secret, source_type, type, position = position.serverId, @@ -1220,7 +1195,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa eventSlug, list.serverId, "invalid_time", - position.secret!!, + secret, source_type, type, position = position.serverId, @@ -1261,8 +1236,8 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa if (list.subEventId != null && list.subEventId > 0 && list.subEventId != position.subEventServerId) { storeFailedCheckin( eventSlug, - list.subEventId, - "invalid", + list.serverId, + "product", position.secret!!, source_type, type, @@ -1272,7 +1247,9 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa subevent = position.subEventServerId, nonce = nonce ) - return TicketCheckProvider.CheckResult(TicketCheckProvider.CheckResult.Type.INVALID, offline = true) + res.type = TicketCheckProvider.CheckResult.Type.PRODUCT + res.isCheckinAllowed = false + return res } if (!order.hasValidStatus && !(ignore_unpaid && list.includePending)) { diff --git a/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderAddonMatchGapTest.kt b/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderAddonMatchGapTest.kt new file mode 100644 index 00000000..9613d938 --- /dev/null +++ b/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderAddonMatchGapTest.kt @@ -0,0 +1,70 @@ +package eu.pretix.libpretixsync.check + +import eu.pretix.libpretixsync.db.BaseDatabaseTest +import eu.pretix.libpretixsync.sqldelight.QueuedCall +import eu.pretix.libpretixsync.sync.CheckInListSyncAdapter +import eu.pretix.libpretixsync.sync.EventSyncAdapter +import eu.pretix.libpretixsync.sync.ItemSyncAdapter +import eu.pretix.libpretixsync.sync.OrderSyncAdapter +import eu.pretix.libpretixsync.sync.ReusableMediaSyncAdapter +import eu.pretix.pretixscan.scanproxy.tests.test.FakeConfigStore +import eu.pretix.pretixscan.scanproxy.tests.test.FakeFileStorage +import eu.pretix.pretixscan.scanproxy.tests.test.FakePretixApi +import eu.pretix.pretixscan.scanproxy.tests.test.jsonResource +import org.joda.time.format.ISODateTimeFormat +import org.json.JSONObject +import org.junit.Before +import org.junit.Test + +import org.junit.Assert.assertEquals + +class AsyncCheckProviderAddonMatchGapTest : BaseDatabaseTest() { + private var configStore: FakeConfigStore? = null + private var fakeApi: FakePretixApi? = null + private var p: AsyncCheckProvider? = null + + @Before + fun setUpFakes() { + configStore = FakeConfigStore("mtrmt", "event1") + fakeApi = FakePretixApi("mtrmt") + p = AsyncCheckProvider(configStore!!, db) + + EventSyncAdapter(db, FakeFileStorage(),"event1", "event1", fakeApi!!, "", null).standaloneRefreshFromJSON(jsonResource("events/rmevent1.json")) + ItemSyncAdapter(db, FakeFileStorage(), "event1", fakeApi!!, "", null).standaloneRefreshFromJSON(jsonResource("items/rmevent1-item1.json")) + ItemSyncAdapter(db, FakeFileStorage(), "event1", fakeApi!!, "", null).standaloneRefreshFromJSON(jsonResource("items/rmevent1-item2.json")) + CheckInListSyncAdapter(db, FakeFileStorage(), "event1", fakeApi!!, "", null, 0).standaloneRefreshFromJSON( + jsonResource("checkinlists/rmevent1-list2.json") + ) + + val osa = OrderSyncAdapter(db, FakeFileStorage(), "event1", 0, true, false, fakeApi!!, "", null) + osa.standaloneRefreshFromJSON(jsonResource("orders/rmevent1-order2.json")) + + val rmsa = ReusableMediaSyncAdapter(db, FakeFileStorage(), fakeApi!!, "", null) + rmsa.standaloneRefreshFromJSON(jsonResource("reusablemedia/mtrmt-medium10.json")) + + p!!.setNow(ISODateTimeFormat.dateTime().parseDateTime("2026-08-01T00:00:01.000Z")) + } + + @Test + fun testTwoAddonsGapEntryScanReportsAddonThatWorksNext() { + assertEquals(0, db.queuedCallQueries.count().executeAsOne()) + val r = p!!.check(mapOf("event1" to 36L), "0000") + assertEquals(TicketCheckProvider.CheckResult.Type.INVALID_TIME, r.type) + assertEquals("ADDMT-4", r.orderCodeAndPositionId()) + assertEquals("Add-on ticket", r.ticket) + + assertEquals(1, db.queuedCallQueries.count().executeAsOne()) + val queuedCall : QueuedCall = db.queuedCallQueries.selectAll().executeAsList().first() + val data = JSONObject(queuedCall.body) + assertEquals("invalid_time", data.getString("error_reason")) + assertEquals("0000", data.getString("raw_barcode")) + } + + @Test + fun testTwoAddonsGapExitScanIsValid() { + val r = p!!.check(mapOf("event1" to 36L), "0000", "barcode", null, false, false, TicketCheckProvider.CheckInType.EXIT) + assertEquals(TicketCheckProvider.CheckResult.Type.VALID, r.type) + assertEquals("ADDMT-4", r.orderCodeAndPositionId()) + assertEquals("Add-on ticket", r.ticket) + } +} diff --git a/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderReusableMediumTest.kt b/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderReusableMediumTest.kt index 673bb0aa..126d3d78 100644 --- a/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderReusableMediumTest.kt +++ b/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderReusableMediumTest.kt @@ -32,16 +32,25 @@ class AsyncCheckProviderReusableMediumTest : BaseDatabaseTest() { EventSyncAdapter(db, FakeFileStorage(),"event1", "event1", fakeApi!!, "", null).standaloneRefreshFromJSON(jsonResource("events/rmevent1.json")) EventSyncAdapter(db, FakeFileStorage(),"event2", "event2", fakeApi!!, "", null).standaloneRefreshFromJSON(jsonResource("events/rmevent2.json")) ItemSyncAdapter(db, FakeFileStorage(), "event1", fakeApi!!, "", null).standaloneRefreshFromJSON(jsonResource("items/rmevent1-item1.json")) + ItemSyncAdapter(db, FakeFileStorage(), "event1", fakeApi!!, "", null).standaloneRefreshFromJSON(jsonResource("items/rmevent1-item2.json")) + ItemSyncAdapter(db, FakeFileStorage(), "event1", fakeApi!!, "", null).standaloneRefreshFromJSON(jsonResource("items/rmevent1-item3.json")) ItemSyncAdapter(db, FakeFileStorage(), "event2", fakeApi!!, "", null).standaloneRefreshFromJSON(jsonResource("items/rmevent2-item1.json")) CheckInListSyncAdapter(db, FakeFileStorage(), "event1", fakeApi!!, "", null, 0).standaloneRefreshFromJSON( jsonResource("checkinlists/rmevent1-list1.json") ) + CheckInListSyncAdapter(db, FakeFileStorage(), "event1", fakeApi!!, "", null, 0).standaloneRefreshFromJSON( + jsonResource("checkinlists/rmevent1-list3.json") + ) + CheckInListSyncAdapter(db, FakeFileStorage(), "event1", fakeApi!!, "", null, 0).standaloneRefreshFromJSON( + jsonResource("checkinlists/rmevent1-list4.json") + ) CheckInListSyncAdapter(db, FakeFileStorage(), "event2", fakeApi!!, "", null, 0).standaloneRefreshFromJSON( jsonResource("checkinlists/rmevent2-list1.json") ) val osa = OrderSyncAdapter(db, FakeFileStorage(), "event1", 0, true, false, fakeApi!!, "", null) osa.standaloneRefreshFromJSON(jsonResource("orders/rmevent1-order1.json")) + osa.standaloneRefreshFromJSON(jsonResource("orders/rmevent1-order3.json")) val osa2 = OrderSyncAdapter(db, FakeFileStorage(), "event2", 0, true, false, fakeApi!!, "", null) osa2.standaloneRefreshFromJSON(jsonResource("orders/rmevent2-order1.json")) @@ -53,6 +62,9 @@ class AsyncCheckProviderReusableMediumTest : BaseDatabaseTest() { rmsa.standaloneRefreshFromJSON(jsonResource("reusablemedia/mtrmt-medium5.json")) rmsa.standaloneRefreshFromJSON(jsonResource("reusablemedia/mtrmt-medium6.json")) rmsa.standaloneRefreshFromJSON(jsonResource("reusablemedia/mtrmt-medium7.json")) + rmsa.standaloneRefreshFromJSON(jsonResource("reusablemedia/mtrmt-medium8.json")) + rmsa.standaloneRefreshFromJSON(jsonResource("reusablemedia/mtrmt-medium9.json")) + rmsa.standaloneRefreshFromJSON(jsonResource("reusablemedia/mtrmt-medium11.json")) } @Test @@ -83,6 +95,22 @@ class AsyncCheckProviderReusableMediumTest : BaseDatabaseTest() { assertEquals(1L, r.positionId) } + @Test + fun testTwoTicketsOverlappingListProductFilterSelectsListedProduct() { + p!!.setNow(ISODateTimeFormat.dateTime().parseDateTime("2026-06-01T00:00:01.000Z")) + val r = p!!.check(mapOf("event1" to 37L), "1212", "nfc_uid") + assertEquals(TicketCheckProvider.CheckResult.Type.VALID, r.type) + assertEquals("Regular ticket", r.ticket) + assertEquals("RNWMT-1", r.orderCodeAndPositionId()) + } + + @Test + fun testTwoTicketsOverlappingListProductFilterNoneListedReportsProduct() { + p!!.setNow(ISODateTimeFormat.dateTime().parseDateTime("2026-06-01T00:00:01.000Z")) + val r = p!!.check(mapOf("event1" to 38L), "1212", "nfc_uid") + assertEquals(TicketCheckProvider.CheckResult.Type.PRODUCT, r.type) + } + @Test fun testTwoTicketsTimesNonOverlappingSameEvent() { p!!.setNow(ISODateTimeFormat.dateTime().parseDateTime("2026-01-01T00:00:01.000Z")) @@ -140,6 +168,54 @@ class AsyncCheckProviderReusableMediumTest : BaseDatabaseTest() { assertEquals("W0JKM-7", r.orderCodeAndPositionId()) } + @Test + fun testTwoTicketsGapExitScanIsValid() { + p!!.setNow(ISODateTimeFormat.dateTime().parseDateTime("2026-08-01T00:00:01.000Z")) + val r = p!!.check(mapOf("event1" to 35L), "7777", "barcode", null, false, false, TicketCheckProvider.CheckInType.EXIT) + assertEquals(TicketCheckProvider.CheckResult.Type.VALID, r.type) + assertEquals("W0JKM-7", r.orderCodeAndPositionId()) + assertEquals(TicketCheckProvider.CheckInType.EXIT, r.scanType) + } + + @Test + fun testSinglePositionOutOfWindowExitScanIsValid() { + p!!.setNow(ISODateTimeFormat.dateTime().parseDateTime("2026-08-01T00:00:01.000Z")) + val r = p!!.check(mapOf("event1" to 35L), "8888", "nfc_uid", null, false, false, TicketCheckProvider.CheckInType.EXIT) + assertEquals(TicketCheckProvider.CheckResult.Type.VALID, r.type) + assertEquals("W0JKM-3", r.orderCodeAndPositionId()) + assertEquals(TicketCheckProvider.CheckInType.EXIT, r.scanType) + } + + @Test + fun testTwoTicketsGapPicksNextRegardlessOfOrder() { + // use the candidate that will "work next", even though a candidate from the past is stored after it + p!!.setNow(ISODateTimeFormat.dateTime().parseDateTime("2026-08-01T00:00:01.000Z")) + var r = p!!.check(mapOf("event1" to 35L), "9999") + assertEquals(TicketCheckProvider.CheckResult.Type.INVALID_TIME, r.type) + assertEquals("W0JKM-3", r.orderCodeAndPositionId()) + + // no candidate in the future, use the one that worked last + p!!.setNow(ISODateTimeFormat.dateTime().parseDateTime("2027-06-01T00:00:01.000Z")) + r = p!!.check(mapOf("event1" to 35L), "9999") + assertEquals(TicketCheckProvider.CheckResult.Type.INVALID_TIME, r.type) + assertEquals("W0JKM-3", r.orderCodeAndPositionId()) + } + + @Test + fun testInvalidTimeFailedCheckinStoresScannedIdentifier() { + val sourceType = "nfc_uid" + assertEquals(0, db.queuedCallQueries.count().executeAsOne()) + p!!.setNow(ISODateTimeFormat.dateTime().parseDateTime("2026-08-01T00:00:01.000Z")) + val r = p!!.check(mapOf("event1" to 35L), "8888", sourceType) + assertEquals(TicketCheckProvider.CheckResult.Type.INVALID_TIME, r.type) + + assertEquals(1, db.queuedCallQueries.count().executeAsOne()) + val queuedCall : QueuedCall = db.queuedCallQueries.selectAll().executeAsList().first() + val data = JSONObject(queuedCall.body) + assertEquals("invalid_time", data.getString("error_reason")) + assertEquals(sourceType, data.getString("raw_source_type")) + assertEquals("8888", data.getString("raw_barcode")) + } @Test fun testMediumExpiredFailedCheckinSourceTypeStored() { diff --git a/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderTest.kt b/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderTest.kt index 95180614..4d5828c2 100644 --- a/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderTest.kt +++ b/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderTest.kt @@ -3,6 +3,7 @@ package eu.pretix.libpretixsync.check import eu.pretix.libpretixsync.db.Answer import eu.pretix.libpretixsync.db.BaseDatabaseTest import eu.pretix.libpretixsync.db.NonceGenerator +import eu.pretix.libpretixsync.sqldelight.QueuedCall import eu.pretix.libpretixsync.sync.CheckInListSyncAdapter import eu.pretix.libpretixsync.sync.EventSyncAdapter import eu.pretix.libpretixsync.sync.ItemSyncAdapter @@ -23,9 +24,12 @@ import java.util.ArrayList import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Ignore class AsyncCheckProviderTest : BaseDatabaseTest() { + private val mediumRequiringSignedSecret = "DMdHmFHt3jd7zwPz8ag9UtICm7d0w1URu4Vxw2GSWLRkw0Q6lyLFG8+6mz0956lYb0zgPu8iCpBO8rQbfhj7g7AIsARMBlERF10QG5UCKAEAPAQA" + private var configStore: FakeConfigStore? = null private var fakeApi: FakePretixApi? = null private var p: AsyncCheckProvider? = null @@ -63,10 +67,14 @@ class AsyncCheckProviderTest : BaseDatabaseTest() { CheckInListSyncAdapter(db, FakeFileStorage(), "demo", fakeApi!!, "", null, 0).standaloneRefreshFromJSON( jsonResource("checkinlists/list10.json") ) + CheckInListSyncAdapter(db, FakeFileStorage(), "demo", fakeApi!!, "", null, 0).standaloneRefreshFromJSON( + jsonResource("checkinlists/list11.json") + ) CheckInListSyncAdapter(db, FakeFileStorage(), "demo2", fakeApi!!, "", null, 0).standaloneRefreshFromJSON( jsonResource("checkinlists/event2-list7.json") ) SubEventSyncAdapter(db, FakeFileStorage(), "demo", "14", fakeApi!!, "", null).standaloneRefreshFromJSON(jsonResource("subevents/subevent1.json")) + SubEventSyncAdapter(db, FakeFileStorage(), "demo", "15", fakeApi!!, "", null).standaloneRefreshFromJSON(jsonResource("subevents/subevent2.json")) val osa = OrderSyncAdapter(db, FakeFileStorage(), "demo", 0, true, false, fakeApi!!, "", null) osa.standaloneRefreshFromJSON(jsonResource("orders/order1.json")) @@ -222,7 +230,26 @@ class AsyncCheckProviderTest : BaseDatabaseTest() { assertEquals("Regular ticket", r.ticket) } - // TODO: invalid subevent + @Test + fun testInvalidSubEvent() { + val sourceType = "nfc_uid" + assertEquals(0, db.queuedCallQueries.count().executeAsOne()) + + val r = p!!.check(mapOf("demo" to 11L), "VQwFXDZWhoXDuXBvKxWqq76kVtLlFWaY", sourceType) + assertEquals(TicketCheckProvider.CheckResult.Type.PRODUCT, r.type) + assertEquals("RR3IP-1", r.orderCodeAndPositionId()) + assertEquals("Alexis Johnson", r.attendee_name) + assertEquals("Regular ticket", r.ticket) + assertEquals(false, r.isCheckinAllowed) + + assertEquals(1, db.queuedCallQueries.count().executeAsOne()) + val queuedCall: QueuedCall = db.queuedCallQueries.selectAll().executeAsList().first() + val data = JSONObject(queuedCall.body) + assertEquals("product", data.getString("error_reason")) + assertEquals(sourceType, data.getString("raw_source_type")) + assertEquals(14L, data.getLong("subevent")) + assertTrue(queuedCall.url!!.endsWith("/checkinlists/11/failed_checkins/")) + } @Test fun testSimpleRedeemed() { @@ -1133,6 +1160,69 @@ class AsyncCheckProviderTest : BaseDatabaseTest() { assertEquals(db.queuedCheckInQueries.count().executeAsOne(), 0L) } + @Test + fun testSignedMediumRequiredAndEnforced() { + syncMediumRequiringItem(reusableMediaUsageEnforced = true) + + val r = p!!.check(mapOf("demo" to 1L), mediumRequiringSignedSecret) + assertEquals(TicketCheckProvider.CheckResult.Type.EXCHANGE_REQUIRED_OFFLINE, r.type) + assertEquals(false, r.isCheckinAllowed) + assertEquals("This ticket needs to be exchanged, but this isn't possible while offline", r.reasonExplanation) + assertEquals(db.queuedCheckInQueries.count().executeAsOne(), 0L) + + assertEquals(1, db.queuedCallQueries.count().executeAsOne()) + val queuedCall: QueuedCall = db.queuedCallQueries.selectAll().executeAsList().first() + val data = JSONObject(queuedCall.body) + assertEquals("exchange", data.getString("error_reason")) + assertEquals("entry", data.getString("type")) + assertEquals(44L, data.getLong("item")) + assertTrue(queuedCall.url!!.endsWith("/checkinlists/1/failed_checkins/")) + } + + @Test + fun testSignedMediumRequiredAndEnforcedOnExit() { + syncMediumRequiringItem(reusableMediaUsageEnforced = true) + + val r = p!!.check(mapOf("demo" to 1L), mediumRequiringSignedSecret, "barcode", null, false, false, TicketCheckProvider.CheckInType.EXIT) + assertEquals(TicketCheckProvider.CheckResult.Type.EXCHANGE_REQUIRED_OFFLINE, r.type) + assertEquals(false, r.isCheckinAllowed) + assertEquals("This ticket needs to be exchanged, but this isn't possible while offline", r.reasonExplanation) + assertEquals(db.queuedCheckInQueries.count().executeAsOne(), 0L) + + assertEquals(1, db.queuedCallQueries.count().executeAsOne()) + val queuedCall: QueuedCall = db.queuedCallQueries.selectAll().executeAsList().first() + val data = JSONObject(queuedCall.body) + assertEquals("exchange", data.getString("error_reason")) + assertEquals("exit", data.getString("type")) + } + + @Test + fun testSignedMediumRequiredButNotEnforced() { + syncMediumRequiringItem(reusableMediaUsageEnforced = false) + + val r = p!!.check(mapOf("demo" to 1L), mediumRequiringSignedSecret) + assertEquals(TicketCheckProvider.CheckResult.Type.VALID, r.type) + assertEquals("NFC ticket", r.ticket) + assertEquals(db.queuedCheckInQueries.count().executeAsOne(), 1L) + assertEquals(0, db.queuedCallQueries.count().executeAsOne()) + } + + private fun syncMediumRequiringItem(reusableMediaUsageEnforced: Boolean) { + ItemSyncAdapter(db, FakeFileStorage(), "demo", fakeApi!!, "", null).standaloneRefreshFromJSON(jsonResource("items/item4.json")) + db.settingsQueries.insert( + address = "", + city = "", + country = "", + json_data = JSONObject().put("reusable_media_usage_enforced", reusableMediaUsageEnforced).toString(), + name = "", + pretixpos_additional_receipt_text = "", + slug = "demo", + tax_id = "", + vat_id = "", + zipcode = "", + ) + } + @Test @Throws(CheckException::class) fun testSearchMultipleLists() { diff --git a/libpretixsync/src/testFixtures/resources/checkinlists/list11.json b/libpretixsync/src/testFixtures/resources/checkinlists/list11.json new file mode 100644 index 00000000..63e31299 --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/checkinlists/list11.json @@ -0,0 +1,13 @@ +{ + "id": 11, + "name": "Date 2 only", + "all_products": true, + "limit_products": [], + "subevent": 15, + "checkin_count": 0, + "position_count": 0, + "include_pending": false, + "allow_multiple_entries": false, + "allow_entry_after_exit": true, + "rules": {} +} diff --git a/libpretixsync/src/testFixtures/resources/checkinlists/rmevent1-list2.json b/libpretixsync/src/testFixtures/resources/checkinlists/rmevent1-list2.json new file mode 100644 index 00000000..4767350c --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/checkinlists/rmevent1-list2.json @@ -0,0 +1,19 @@ +{ + "id": 36, + "name": "Add-on match", + "all_products": false, + "limit_products": [ + 215 + ], + "subevent": null, + "checkin_count": 0, + "position_count": 2, + "include_pending": false, + "allow_multiple_entries": false, + "allow_entry_after_exit": true, + "rules": {}, + "exit_all_at": null, + "addon_match": true, + "ignore_in_statistics": false, + "consider_tickets_used": true +} diff --git a/libpretixsync/src/testFixtures/resources/checkinlists/rmevent1-list3.json b/libpretixsync/src/testFixtures/resources/checkinlists/rmevent1-list3.json new file mode 100644 index 00000000..aacc3032 --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/checkinlists/rmevent1-list3.json @@ -0,0 +1,19 @@ +{ + "id": 37, + "name": "Regular ticket only", + "all_products": false, + "limit_products": [ + 214 + ], + "subevent": null, + "checkin_count": 0, + "position_count": 1, + "include_pending": false, + "allow_multiple_entries": false, + "allow_entry_after_exit": true, + "rules": {}, + "exit_all_at": null, + "addon_match": false, + "ignore_in_statistics": false, + "consider_tickets_used": true +} diff --git a/libpretixsync/src/testFixtures/resources/checkinlists/rmevent1-list4.json b/libpretixsync/src/testFixtures/resources/checkinlists/rmevent1-list4.json new file mode 100644 index 00000000..2cc619d1 --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/checkinlists/rmevent1-list4.json @@ -0,0 +1,19 @@ +{ + "id": 38, + "name": "Add-on ticket only", + "all_products": false, + "limit_products": [ + 215 + ], + "subevent": null, + "checkin_count": 0, + "position_count": 0, + "include_pending": false, + "allow_multiple_entries": false, + "allow_entry_after_exit": true, + "rules": {}, + "exit_all_at": null, + "addon_match": false, + "ignore_in_statistics": false, + "consider_tickets_used": true +} diff --git a/libpretixsync/src/testFixtures/resources/items/item4.json b/libpretixsync/src/testFixtures/resources/items/item4.json new file mode 100644 index 00000000..1382e853 --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/items/item4.json @@ -0,0 +1,39 @@ +{ + "id": 44, + "category": null, + "name": { + "en": "NFC ticket" + }, + "internal_name": null, + "active": true, + "sales_channels": [ + "web" + ], + "description": {}, + "default_price": "23.00", + "free_price": false, + "tax_rate": "0.00", + "tax_rule": null, + "admission": true, + "position": 0, + "picture": null, + "available_from": null, + "available_until": null, + "require_voucher": false, + "hide_without_voucher": false, + "allow_cancel": true, + "require_bundling": false, + "min_per_order": null, + "max_per_order": null, + "checkin_attention": false, + "has_variations": false, + "variations": [], + "addons": [], + "bundles": [], + "original_price": null, + "require_approval": false, + "generate_tickets": null, + "issue_giftcard": false, + "media_policy": "append_or_new", + "media_type": "nfc_uid" +} diff --git a/libpretixsync/src/testFixtures/resources/items/rmevent1-item2.json b/libpretixsync/src/testFixtures/resources/items/rmevent1-item2.json new file mode 100644 index 00000000..a8c5aff6 --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/items/rmevent1-item2.json @@ -0,0 +1,70 @@ +{ + "id": 215, + "category": null, + "name": { + "en": "Add-on ticket" + }, + "internal_name": null, + "active": true, + "all_sales_channels": true, + "limit_sales_channels": [], + "description": null, + "default_price": "0.00", + "free_price": false, + "free_price_suggestion": null, + "tax_rate": "0.00", + "tax_rule": null, + "admission": false, + "personalized": true, + "position": 1, + "picture": null, + "available_from": null, + "available_from_mode": "hide", + "available_until": null, + "available_until_mode": "hide", + "require_voucher": false, + "hide_without_voucher": false, + "allow_cancel": true, + "require_bundling": false, + "min_per_order": null, + "max_per_order": null, + "checkin_attention": false, + "checkin_text": null, + "has_variations": false, + "variations": [], + "addons": [], + "bundles": [], + "program_times": [], + "original_price": null, + "require_approval": false, + "generate_tickets": null, + "show_quota_left": null, + "hidden_if_available": null, + "hidden_if_item_available": null, + "hidden_if_item_available_mode": "hide", + "allow_waitinglist": true, + "issue_giftcard": false, + "meta_data": {}, + "require_membership": false, + "require_membership_types": [], + "require_membership_hidden": false, + "grant_membership_type": null, + "grant_membership_duration_like_event": true, + "grant_membership_duration_days": 0, + "grant_membership_duration_months": 0, + "validity_mode": null, + "validity_fixed_from": null, + "validity_fixed_until": null, + "validity_dynamic_duration_minutes": null, + "validity_dynamic_duration_hours": null, + "validity_dynamic_duration_days": null, + "validity_dynamic_duration_months": null, + "validity_dynamic_start_choice": false, + "validity_dynamic_start_choice_day_limit": null, + "media_policy": null, + "media_type": null, + "sales_channels": [ + "pretixpos", + "web" + ] +} diff --git a/libpretixsync/src/testFixtures/resources/items/rmevent1-item3.json b/libpretixsync/src/testFixtures/resources/items/rmevent1-item3.json new file mode 100644 index 00000000..5006bc5d --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/items/rmevent1-item3.json @@ -0,0 +1,70 @@ +{ + "id": 216, + "category": null, + "name": { + "en": "Renewal ticket" + }, + "internal_name": null, + "active": true, + "all_sales_channels": true, + "limit_sales_channels": [], + "description": null, + "default_price": "0.00", + "free_price": false, + "free_price_suggestion": null, + "tax_rate": "0.00", + "tax_rule": null, + "admission": true, + "personalized": true, + "position": 2, + "picture": null, + "available_from": null, + "available_from_mode": "hide", + "available_until": null, + "available_until_mode": "hide", + "require_voucher": false, + "hide_without_voucher": false, + "allow_cancel": true, + "require_bundling": false, + "min_per_order": null, + "max_per_order": null, + "checkin_attention": false, + "checkin_text": null, + "has_variations": false, + "variations": [], + "addons": [], + "bundles": [], + "program_times": [], + "original_price": null, + "require_approval": false, + "generate_tickets": null, + "show_quota_left": null, + "hidden_if_available": null, + "hidden_if_item_available": null, + "hidden_if_item_available_mode": "hide", + "allow_waitinglist": true, + "issue_giftcard": false, + "meta_data": {}, + "require_membership": false, + "require_membership_types": [], + "require_membership_hidden": false, + "grant_membership_type": null, + "grant_membership_duration_like_event": true, + "grant_membership_duration_days": 0, + "grant_membership_duration_months": 0, + "validity_mode": null, + "validity_fixed_from": null, + "validity_fixed_until": null, + "validity_dynamic_duration_minutes": null, + "validity_dynamic_duration_hours": null, + "validity_dynamic_duration_days": null, + "validity_dynamic_duration_months": null, + "validity_dynamic_start_choice": false, + "validity_dynamic_start_choice_day_limit": null, + "media_policy": null, + "media_type": null, + "sales_channels": [ + "pretixpos", + "web" + ] +} diff --git a/libpretixsync/src/testFixtures/resources/orders/rmevent1-order2.json b/libpretixsync/src/testFixtures/resources/orders/rmevent1-order2.json new file mode 100644 index 00000000..4b2df041 --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/orders/rmevent1-order2.json @@ -0,0 +1,226 @@ +{ + "code": "ADDMT", + "event": "event1", + "status": "p", + "testmode": true, + "secret": "j2n9wvahp44qd1rs", + "email": "reusablemedia@example.org", + "phone": null, + "locale": "en", + "datetime": "2026-05-08T10:01:43.195739Z", + "expires": "2026-05-22T23:59:59Z", + "payment_date": "2026-05-08", + "payment_provider": "free", + "fees": [], + "total": "0.00", + "tax_rounding_mode": "line", + "comment": "", + "custom_followup_at": null, + "invoice_address": { + "last_modified": "2026-05-08T10:01:43.312471Z", + "is_business": false, + "company": "", + "name": "", + "name_parts": { + "_scheme": "given_family" + }, + "street": "", + "zipcode": "", + "city": "", + "country": "", + "state": "", + "vat_id": "", + "vat_id_validated": false, + "custom_field": null, + "internal_reference": "", + "transmission_type": "email", + "transmission_info": {} + }, + "positions": [ + { + "id": 18711, + "order": "ADDMT", + "positionid": 1, + "item": 214, + "variation": null, + "price": "0.00", + "attendee_name": "", + "attendee_name_parts": { + "_scheme": "given_family" + }, + "company": null, + "street": null, + "zipcode": null, + "city": null, + "country": null, + "state": null, + "discount": null, + "attendee_email": null, + "voucher": null, + "tax_rate": "0.00", + "tax_value": "0.00", + "secret": "hs8xzpgxmqbgmutbcvna3agbkq5uyzpr", + "addon_to": null, + "subevent": null, + "checkins": [], + "print_logs": [], + "downloads": [], + "answers": [], + "tax_rule": null, + "pseudonymization_id": "K8RLPGWTCJ", + "seat": null, + "canceled": false, + "tax_code": null, + "valid_from": "2026-01-01T00:00:00Z", + "valid_until": "2026-05-01T23:59:59Z", + "blocked": null, + "voucher_budget_use": null, + "plugin_data": {} + }, + { + "id": 18712, + "order": "ADDMT", + "positionid": 2, + "item": 215, + "variation": null, + "price": "0.00", + "attendee_name": "", + "attendee_name_parts": { + "_scheme": "given_family" + }, + "company": null, + "street": null, + "zipcode": null, + "city": null, + "country": null, + "state": null, + "discount": null, + "attendee_email": null, + "voucher": null, + "tax_rate": "0.00", + "tax_value": "0.00", + "secret": "jzuxw6dhukwmq2r3pfkeq6nfg8tkzvgc", + "addon_to": 18711, + "subevent": null, + "checkins": [], + "print_logs": [], + "downloads": [], + "answers": [], + "tax_rule": null, + "pseudonymization_id": "QWZ7VMAD2R", + "seat": null, + "canceled": false, + "tax_code": null, + "valid_from": "2026-01-01T00:00:00Z", + "valid_until": "2026-05-01T23:59:59Z", + "blocked": null, + "voucher_budget_use": null, + "plugin_data": {} + }, + { + "id": 18713, + "order": "ADDMT", + "positionid": 3, + "item": 214, + "variation": null, + "price": "0.00", + "attendee_name": "", + "attendee_name_parts": { + "_scheme": "given_family" + }, + "company": null, + "street": null, + "zipcode": null, + "city": null, + "country": null, + "state": null, + "discount": null, + "attendee_email": null, + "voucher": null, + "tax_rate": "0.00", + "tax_value": "0.00", + "secret": "b5pmt6fzaqvcxdg9dyr7ce3xzt4hrwum", + "addon_to": null, + "subevent": null, + "checkins": [], + "print_logs": [], + "downloads": [], + "answers": [], + "tax_rule": null, + "pseudonymization_id": "P9CVLXNQ6T", + "seat": null, + "canceled": false, + "tax_code": null, + "valid_from": "2026-09-01T00:00:00Z", + "valid_until": "2026-12-31T23:59:59Z", + "blocked": null, + "voucher_budget_use": null, + "plugin_data": {} + }, + { + "id": 18714, + "order": "ADDMT", + "positionid": 4, + "item": 215, + "variation": null, + "price": "0.00", + "attendee_name": "", + "attendee_name_parts": { + "_scheme": "given_family" + }, + "company": null, + "street": null, + "zipcode": null, + "city": null, + "country": null, + "state": null, + "discount": null, + "attendee_email": null, + "voucher": null, + "tax_rate": "0.00", + "tax_value": "0.00", + "secret": "vhc3r9nfkwyy2bt8s6mzgjduap7qexf5", + "addon_to": 18713, + "subevent": null, + "checkins": [], + "print_logs": [], + "downloads": [], + "answers": [], + "tax_rule": null, + "pseudonymization_id": "V4DHYS8KRE", + "seat": null, + "canceled": false, + "tax_code": null, + "valid_from": "2026-09-01T00:00:00Z", + "valid_until": "2026-12-31T23:59:59Z", + "blocked": null, + "voucher_budget_use": null, + "plugin_data": {} + } + ], + "downloads": [], + "checkin_attention": false, + "checkin_text": null, + "last_modified": "2026-05-08T10:08:45.099234Z", + "payments": [ + { + "local_id": 1, + "state": "confirmed", + "amount": "0.00", + "created": "2026-05-08T10:01:43.322983Z", + "payment_date": "2026-05-08T10:01:43.494978Z", + "provider": "free", + "payment_url": null, + "details": {} + } + ], + "refunds": [], + "require_approval": false, + "sales_channel": "web", + "url": "http://localhost/mtrmt/event1/order/ADDMT/j2n9wvahp44qd1rs/", + "customer": null, + "valid_if_pending": false, + "api_meta": {}, + "cancellation_date": null, + "plugin_data": {} +} diff --git a/libpretixsync/src/testFixtures/resources/orders/rmevent1-order3.json b/libpretixsync/src/testFixtures/resources/orders/rmevent1-order3.json new file mode 100644 index 00000000..80e57580 --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/orders/rmevent1-order3.json @@ -0,0 +1,146 @@ +{ + "code": "RNWMT", + "event": "event1", + "status": "p", + "testmode": true, + "secret": "p2qr8v4mzt6wkc9d", + "email": "reusablemedia@example.org", + "phone": null, + "locale": "en", + "datetime": "2026-05-08T10:01:43.195739Z", + "expires": "2026-05-22T23:59:59Z", + "payment_date": "2026-05-08", + "payment_provider": "free", + "fees": [], + "total": "0.00", + "tax_rounding_mode": "line", + "comment": "", + "custom_followup_at": null, + "invoice_address": { + "last_modified": "2026-05-08T10:01:43.312471Z", + "is_business": false, + "company": "", + "name": "", + "name_parts": { + "_scheme": "given_family" + }, + "street": "", + "zipcode": "", + "city": "", + "country": "", + "state": "", + "vat_id": "", + "vat_id_validated": false, + "custom_field": null, + "internal_reference": "", + "transmission_type": "email", + "transmission_info": {} + }, + "positions": [ + { + "id": 18721, + "order": "RNWMT", + "positionid": 1, + "item": 214, + "variation": null, + "price": "0.00", + "attendee_name": "", + "attendee_name_parts": { + "_scheme": "given_family" + }, + "company": null, + "street": null, + "zipcode": null, + "city": null, + "country": null, + "state": null, + "discount": null, + "attendee_email": null, + "voucher": null, + "tax_rate": "0.00", + "tax_value": "0.00", + "secret": "hbz9mkeq4trvxs7ad2cfn6pguyw3jl5t", + "addon_to": null, + "subevent": null, + "checkins": [], + "print_logs": [], + "downloads": [], + "answers": [], + "tax_rule": null, + "pseudonymization_id": "QM4TDZR7WK", + "seat": null, + "canceled": false, + "tax_code": null, + "valid_from": "2026-01-01T00:00:00Z", + "valid_until": "2026-12-31T23:59:59Z", + "blocked": null, + "voucher_budget_use": null, + "plugin_data": {} + }, + { + "id": 18722, + "order": "RNWMT", + "positionid": 2, + "item": 216, + "variation": null, + "price": "0.00", + "attendee_name": "", + "attendee_name_parts": { + "_scheme": "given_family" + }, + "company": null, + "street": null, + "zipcode": null, + "city": null, + "country": null, + "state": null, + "discount": null, + "attendee_email": null, + "voucher": null, + "tax_rate": "0.00", + "tax_value": "0.00", + "secret": "v7nxcp3kd8sfyzg2mtqaw9rebju6h4l5", + "addon_to": null, + "subevent": null, + "checkins": [], + "print_logs": [], + "downloads": [], + "answers": [], + "tax_rule": null, + "pseudonymization_id": "ZP9XKBTNDM", + "seat": null, + "canceled": false, + "tax_code": null, + "valid_from": "2026-01-01T00:00:00Z", + "valid_until": "2026-12-31T23:59:59Z", + "blocked": null, + "voucher_budget_use": null, + "plugin_data": {} + } + ], + "downloads": [], + "checkin_attention": false, + "checkin_text": null, + "last_modified": "2026-05-08T10:08:45.099234Z", + "payments": [ + { + "local_id": 1, + "state": "confirmed", + "amount": "0.00", + "created": "2026-05-08T10:01:43.322983Z", + "payment_date": "2026-05-08T10:01:43.494978Z", + "provider": "free", + "payment_url": null, + "details": {} + } + ], + "refunds": [], + "require_approval": false, + "sales_channel": "web", + "url": "http://localhost/mtrmt/event1/order/RNWMT/p2qr8v4mzt6wkc9d/", + "customer": null, + "valid_if_pending": false, + "api_meta": {}, + "cancellation_date": null, + "plugin_data": {} +} diff --git a/libpretixsync/src/testFixtures/resources/reusablemedia/mtrmt-medium10.json b/libpretixsync/src/testFixtures/resources/reusablemedia/mtrmt-medium10.json new file mode 100644 index 00000000..c85b2a85 --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/reusablemedia/mtrmt-medium10.json @@ -0,0 +1,21 @@ +{ + "id": 36, + "organizer": "mtrmt", + "created": "2026-05-13T07:31:23.075971Z", + "updated": "2026-05-13T07:31:23.076011Z", + "type": "barcode", + "identifier": "0000", + "claim_token": null, + "label": null, + "active": true, + "expires": null, + "customer": null, + "linked_orderpositions": [ + 18711, + 18713 + ], + "linked_giftcard": null, + "info": {}, + "notes": "Two tickets with add-ons, non overlapping with a bit spacing in between, same event", + "linked_orderposition": null +} diff --git a/libpretixsync/src/testFixtures/resources/reusablemedia/mtrmt-medium11.json b/libpretixsync/src/testFixtures/resources/reusablemedia/mtrmt-medium11.json new file mode 100644 index 00000000..48d0b6ab --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/reusablemedia/mtrmt-medium11.json @@ -0,0 +1,21 @@ +{ + "id": 37, + "organizer": "mtrmt", + "created": "2026-05-08T10:00:05.758077Z", + "updated": "2026-05-08T10:07:20.207322Z", + "type": "nfc_uid", + "identifier": "1212", + "claim_token": null, + "label": null, + "active": true, + "expires": null, + "customer": null, + "linked_orderpositions": [ + 18722, + 18721 + ], + "linked_giftcard": null, + "info": {}, + "notes": "Two tickets of different products with overlapping validity in the same event", + "linked_orderposition": null +} diff --git a/libpretixsync/src/testFixtures/resources/reusablemedia/mtrmt-medium8.json b/libpretixsync/src/testFixtures/resources/reusablemedia/mtrmt-medium8.json new file mode 100644 index 00000000..8bfb9a0c --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/reusablemedia/mtrmt-medium8.json @@ -0,0 +1,20 @@ +{ + "id": 34, + "organizer": "mtrmt", + "created": "2026-05-13T07:31:23.075971Z", + "updated": "2026-05-13T07:31:23.076011Z", + "type": "nfc_uid", + "identifier": "8888", + "claim_token": null, + "label": null, + "active": true, + "expires": null, + "customer": null, + "linked_orderpositions": [18699], + "linked_giftcard": null, + "info": { + + }, + "notes": "One ticket, validity starting in the future, same event", + "linked_orderposition": null +} diff --git a/libpretixsync/src/testFixtures/resources/reusablemedia/mtrmt-medium9.json b/libpretixsync/src/testFixtures/resources/reusablemedia/mtrmt-medium9.json new file mode 100644 index 00000000..07a84dd4 --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/reusablemedia/mtrmt-medium9.json @@ -0,0 +1,20 @@ +{ + "id": 35, + "organizer": "mtrmt", + "created": "2026-05-13T07:31:23.075971Z", + "updated": "2026-05-13T07:31:23.076011Z", + "type": "barcode", + "identifier": "9999", + "claim_token": null, + "label": null, + "active": true, + "expires": null, + "customer": null, + "linked_orderpositions": [18699, 18702], + "linked_giftcard": null, + "info": { + + }, + "notes": "Two tickets, non overlapping, the one starting later stored first, same event", + "linked_orderposition": null +} diff --git a/libpretixsync/src/testFixtures/resources/subevents/subevent2.json b/libpretixsync/src/testFixtures/resources/subevents/subevent2.json new file mode 100644 index 00000000..d99e4fa8 --- /dev/null +++ b/libpretixsync/src/testFixtures/resources/subevents/subevent2.json @@ -0,0 +1,22 @@ +{ + "id": 15, + "name": { + "de": "Test 2" + }, + "date_from": "2020-02-02T10:00:00Z", + "date_to": "2020-02-02T14:00:00Z", + "date_admission": "2020-02-02T09:00:00Z", + "active": true, + "presale_start": null, + "presale_end": null, + "location": {}, + "geo_lat": null, + "geo_lon": null, + "event": "demo", + "is_public": true, + "seating_plan": null, + "item_price_overrides": [], + "variation_price_overrides": [], + "meta_data": {}, + "seat_category_mapping": {} +}