Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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<String, Long>, positions: List<OrderPositionModel>): Pair<List<OrderPositionModel>, List<PositionFilteringError>> {
var results = mutableListOf<OrderPositionModel>()
val errors = mutableListOf<PositionFilteringError>()
val listsByPositionId = mutableMapOf<Long, CheckInList>()
positions.forEach { position ->
val order = db.orderQueries.selectById(position.orderId).executeAsOne().toModel()

Expand Down Expand Up @@ -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))
Expand All @@ -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.
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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 -> {}
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading
Loading