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 84b589b5..901a83df 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt @@ -28,9 +28,12 @@ import java.net.SocketTimeoutException import java.net.URL import java.net.URLEncoder import java.nio.charset.Charset +import java.text.DateFormat +import java.text.SimpleDateFormat import java.util.* import java.util.concurrent.TimeUnit import javax.net.ssl.SSLPeerUnverifiedException +import kotlin.math.exp open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, httpClientFactory: HttpClientFactory, val acceptLanguage: String? = null) { private val url: String @@ -162,6 +165,42 @@ open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, h return postResource(organizerResourceUrl("checkinrpc/redeem") + pd, body, null, callTimeout = callTimeout) } + fun annulBody(lists: List, nonce: String, explanation: String): JSONObject { + val tz = TimeZone.getTimeZone("UTC") + val df: DateFormat = SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss'Z'", + Locale.ENGLISH + ) // Quoted "Z" to indicate UTC, no timezone offset + df.timeZone = tz + val jlists = JSONArray() + for (l in lists) { + jlists.put(l) + } + val body = JSONObject() + body.put("nonce", nonce) + body.put("lists", jlists) + body.put("datetime", df.format(Date())) + body.put("error_explanation", explanation) + return body + } + + @Throws(ApiException::class, JSONException::class) + open fun annul( + lists: List, + nonce: String, + explanation: String, + idempotency_key: String?, + callTimeout: Long? + ): ApiResponse { + val body = annulBody(lists, nonce, explanation) + return postResource( + organizerResourceUrl("checkinrpc/annul"), + body, + idempotency_key = idempotency_key, + callTimeout = callTimeout + ) + } + @Throws(ApiException::class) open fun status(eventSlug: String, listId: Long): ApiResponse { return try { 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 a467d840..16d0a520 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/ProxyApi.kt @@ -31,6 +31,12 @@ data class MultiCheckInput( val exchange_medium_identifier: String?, ) +data class AnnulInput( + val events_and_checkin_lists: Map, + val nonce: String, + val explanation: String, +) + data class CheckInput( val ticketid: String, val answers: List?, 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 6fbc0c51..93d64bf5 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/AsyncCheckProvider.kt @@ -36,6 +36,8 @@ import org.json.JSONException import org.json.JSONObject import java.lang.Exception import java.nio.charset.Charset +import java.text.DateFormat +import java.text.SimpleDateFormat import java.time.Instant import java.time.OffsetDateTime import java.util.* @@ -44,14 +46,11 @@ import kotlin.collections.filter class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDatabase) : TicketCheckProvider { private var sentry: SentryInterface = DummySentryImplementation() - /* - */ - override fun setSentry(sentry: SentryInterface) { this.sentry = sentry } - private fun storeFailedCheckin(eventSlug: String, listId: Long, error_reason: String, raw_barcode: String, type: TicketCheckProvider.CheckInType, position: Long? = null, item: Long? = null, variation: Long? = null, subevent: Long? = null, nonce: String?) { + private fun storeFailedCheckin(eventSlug: String, listId: Long, error_reason: String, raw_barcode: String, type: TicketCheckProvider.CheckInType, position: Long? = null, item: Long? = null, variation: Long? = null, subevent: Long? = null, nonce: String) { /* : "exit" }) jdoc.put("error_reason", error_reason) - if (nonce != null) jdoc.put("nonce", nonce) + jdoc.put("nonce", nonce) if (position != null && position > 0) jdoc.put("position", position) if (item != null && item > 0) jdoc.put("item", item) if (variation != null && variation > 0) jdoc.put("variation", variation) @@ -86,7 +85,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa val api = PretixApi.fromConfig(config) // todo: uses wrong http client db.queuedCallQueries.insert( body = jdoc.toString(), - idempotency_key = NonceGenerator.nextNonce(), + idempotency_key = nonce, url = api.eventResourceUrl(eventSlug, "checkinlists") + listId + "/failed_checkins/", ) } @@ -289,7 +288,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa ticketid: String, type: TicketCheckProvider.CheckInType, answers: List?, - nonce: String?, + nonce: String, allowQuestions: Boolean ): TicketCheckProvider.CheckResult { val dt = now() @@ -365,6 +364,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa } val res = TicketCheckProvider.CheckResult(TicketCheckProvider.CheckResult.Type.ERROR, offline = true) + res.nonce = nonce res.eventSlug = eventSlug res.scanType = type res.ticket = item.internalName @@ -385,7 +385,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa val queuedCheckIns = db.queuedCheckInQueries.selectBySecret(ticketid) .executeAsList() - .filter { it.checkinListId == listId } + .filter { it.checkinListId == listId && it.annulled == null } .map { it.toModel() } .sortedWith(compareBy({ it.dateTime }, { it.id })) @@ -531,10 +531,11 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa datetime = dt.toDate(), datetime_string = QueuedCheckIn.formatDatetime(dt.toDate()), event_slug = eventSlug, - nonce = nonce ?: NonceGenerator.nextNonce(), + nonce = nonce, secret = ticketid, source_type = null, type = type.toString().lowercase(Locale.getDefault()), + annulled = null, ) } } @@ -546,6 +547,30 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa return check(eventsAndCheckinLists, ticketid, "barcode", ArrayList(), false, true, TicketCheckProvider.CheckInType.ENTRY) } + override fun annul( + eventsAndCheckinLists: Map, + nonce: String, + explanation: String + ): TicketCheckProvider.AnnulResult { + // In case we have a locally-stored check-in, set it to annulled so we don't consider it in future offline scans + db.checkInQueries.setAnnulledByNonce(Date(), nonce) + + // In case we have a queued check-in, set it to annulled so we don't consider it in future offline scans + // (it will still be uploaded) + db.queuedCheckInQueries.setAnnulledByNonce(Date(), nonce) + + // Queue the actual annulment for upload to server + val api = PretixApi.fromConfig(config) + val body = api.annulBody(eventsAndCheckinLists.values.toList(), nonce, explanation) + db.queuedCallQueries.insert( + body = body.toString(), + idempotency_key = NonceGenerator.nextNonce(), + url = api.organizerResourceUrl("checkinrpc/annul"), + ) + + return TicketCheckProvider.AnnulResult(true) + } + override fun check( eventsAndCheckinLists: Map, ticketid: String, @@ -560,6 +585,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa exchange_medium_type: String?, exchange_medium_identifier: String?, ): TicketCheckProvider.CheckResult { + val nonce = nonce ?: NonceGenerator.nextNonce() val ticketid_cleaned = cleanInput(ticketid, source_type) sentry.addBreadcrumb("provider.check", "offline check started") @@ -808,7 +834,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa answers: List?, ignore_unpaid: Boolean, type: TicketCheckProvider.CheckInType, - nonce: String?, + nonce: String, allowQuestions: Boolean, mediumUsed: Boolean ): TicketCheckProvider.CheckResult { @@ -889,6 +915,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa // !!! When extending this, also extend checkOfflineWithoutData !!! val res = TicketCheckProvider.CheckResult(TicketCheckProvider.CheckResult.Type.ERROR, offline = true) + res.nonce = nonce res.scanType = type res.ticket = positionItem.internalName val varid = position.variationServerId @@ -923,7 +950,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa val storedCheckIns = db.checkInQueries.selectByPositionId(position.id).executeAsList().map { it.toModel() } val checkIns = storedCheckIns.filter { - it.listServerId == listId + it.listServerId == listId && it.localAnnulled == null }.sortedWith(compareBy({ it.fullDateTime }, { it.id })) if (order.status != OrderModel.Status.PAID && order.status != OrderModel.Status.PENDING) { @@ -1157,10 +1184,11 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa datetime = dt.toDate(), datetime_string = QueuedCheckIn.formatDatetime(dt.toDate()), event_slug = eventSlug, - nonce = nonce ?: NonceGenerator.nextNonce(), + nonce = nonce, secret = position.secret, source_type = null, type = type.toString().lowercase(Locale.getDefault()), + annulled = null ) db.checkInQueries.insert( @@ -1170,6 +1198,8 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa type = type.toString().lowercase(Locale.getDefault()), datetime = dt.toDate(), json_data = "{\"local\": true, \"type\": \"${type.toString().lowercase(Locale.getDefault())}\", \"datetime\": \"${QueuedCheckIn.formatDatetime(dt.toDate())}\"}", + local_nonce = nonce, + local_annulled = null, ) } } @@ -1315,14 +1345,14 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa sr.positionId = position.positionId sr.secret = position.secret - val queuedCheckIns = db.queuedCheckInQueries.countForSecretAndLists( + val queuedCheckIns = db.queuedCheckInQueries.countForSecretAndListsNotAnnulled( secret = position.secret, checkin_list_ids = eventsAndCheckinLists.values.toList(), ).executeAsOne() val checkIns = db.checkInQueries.selectByPositionId(position.id).executeAsList().map { it.toModel() } var is_checked_in = queuedCheckIns > 0 for (ci in checkIns) { - if (eventsAndCheckinLists.containsValue(ci.listServerId)) { + if (eventsAndCheckinLists.containsValue(ci.listServerId) && ci.localAnnulled == null) { is_checked_in = true break } 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 8713e630..4780dcce 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/OnlineCheckProvider.kt @@ -61,6 +61,7 @@ class OnlineCheckProvider( return try { val res = TicketCheckProvider.CheckResult(TicketCheckProvider.CheckResult.Type.ERROR) res.scanType = type + res.nonce = nonce_cleaned val responseObj = if (config.knownPretixVersion >= 40120001001) { // >= 4.12.0.dev1 api.redeem( eventsAndCheckinLists.values.toList(), @@ -352,6 +353,37 @@ class OnlineCheckProvider( return check(eventsAndCheckinLists, ticketid, "barcode", ArrayList(), false, true, TicketCheckProvider.CheckInType.ENTRY) } + override fun annul( + eventsAndCheckinLists: Map, + nonce: String, + explanation: String + ): TicketCheckProvider.AnnulResult { + // In case we just switchted from offline to online, cleanup similarly to AsyncCheckProvider + db.checkInQueries.setAnnulledByNonce(Date(), nonce) + db.queuedCheckInQueries.setAnnulledByNonce(Date(), nonce) + + val idempotencyKey = NonceGenerator.nextNonce() + try { + api.annul( + eventsAndCheckinLists.values.toList(), + nonce, + explanation, + idempotencyKey, + if (fallback != null) fallbackTimeout.toLong() else null + ) + } catch (e: ApiException) { + e.printStackTrace() + // Queue the annulment for a later attempt + val body = api.annulBody(eventsAndCheckinLists.values.toList(), nonce, explanation) + db.queuedCallQueries.insert( + body = body.toString(), + idempotency_key = idempotencyKey, + url = api.organizerResourceUrl("checkinrpc/annul"), + ) + } + return TicketCheckProvider.AnnulResult(true) + } + @Throws(CheckException::class) override fun search(eventsAndCheckinLists: Map, query: String, page: Int): List { sentry.addBreadcrumb("provider.search", "started") 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 550d59ea..a69e9bcf 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/ProxyCheckProvider.kt @@ -10,6 +10,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.readValue import eu.pretix.libpretixsync.DummySentryImplementation import eu.pretix.libpretixsync.SentryInterface +import eu.pretix.libpretixsync.api.AnnulInput import eu.pretix.libpretixsync.api.ApiException import eu.pretix.libpretixsync.api.CheckInputAnswer import eu.pretix.libpretixsync.api.CheckInputQuestion @@ -217,4 +218,37 @@ class ProxyCheckProvider(private val config: ConfigStore, httpClientFactory: Htt throw CheckException(e.message, e) } } + + override fun annul( + eventsAndCheckinLists: Map, + nonce: String, + explanation: String + ): TicketCheckProvider.AnnulResult { + val data = AnnulInput( + events_and_checkin_lists = eventsAndCheckinLists, + nonce = nonce, + explanation = explanation, + ) + + return try { + val request = Request.Builder() + .url(config.apiUrl + "/proxyapi/v1/rpc/annul/") // todo: does not yet exist + .post(mapper.writeValueAsString(data).toRequestBody("application/json".toMediaType())) + .header("Authorization", "Device " + config.apiKey) + .build() + val body = execute(request) + mapper.readValue(body, TicketCheckProvider.AnnulResult::class.java) + } catch (e: ApiException) { + sentry.addBreadcrumb("provider.search", "API Error: " + e.message) + TicketCheckProvider.AnnulResult(false) + } catch (e: JsonProcessingException) { + e.printStackTrace() + TicketCheckProvider.AnnulResult(false) + } catch (e: IOException) { + e.printStackTrace() + TicketCheckProvider.AnnulResult(false) + } catch (e: CheckException) { + TicketCheckProvider.AnnulResult(false) + } + } } \ No newline at end of file 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 971d30f2..70e75e9e 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/check/TicketCheckProvider.kt @@ -64,6 +64,7 @@ interface TicketCheckProvider { } var type: Type? = null + var nonce: String? = null var scanType: CheckInType = CheckInType.ENTRY var ticket: String? = null var variation: String? = null @@ -110,6 +111,15 @@ interface TicketCheckProvider { } } + class AnnulResult { + // only says whether the call worked, not whether something was actually annulled, which might only be decided later on the server + var ok: Boolean = false + + constructor (ok: Boolean) { + this.ok = ok + } + } + class SearchResult { enum class Status { PAID, CANCELED, PENDING @@ -179,6 +189,9 @@ interface TicketCheckProvider { ): CheckResult fun check(eventsAndCheckinLists: Map, ticketid: String): CheckResult + + fun annul(eventsAndCheckinLists: Map, nonce: String, explanation: String): AnnulResult + @Throws(CheckException::class) fun search(eventsAndCheckinLists: Map, query: String, page: Int): List diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/CheckIn.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/CheckIn.kt index 22ba64c1..d644a509 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/CheckIn.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/CheckIn.kt @@ -9,4 +9,6 @@ data class CheckIn( val type: String?, val listServerId: Long?, val positionId: Long?, + val localNonce: String?, + val localAnnulled: OffsetDateTime? ) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/db/CheckInExtensions.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/db/CheckInExtensions.kt index b98fefb0..346d963e 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/db/CheckInExtensions.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/models/db/CheckInExtensions.kt @@ -3,11 +3,21 @@ package eu.pretix.libpretixsync.models.db import eu.pretix.libpretixsync.sqldelight.CheckIn import eu.pretix.libpretixsync.sqldelight.SafeOffsetDateTimeMapper import org.json.JSONObject +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.util.Date import eu.pretix.libpretixsync.models.CheckIn as CheckInModel fun CheckIn.toModel(): CheckInModel { val json = JSONObject(this.json_data) + fun toOffsetDateTime(d: Date?): OffsetDateTime? { + if (d == null) { + return null + } + return d.toInstant().atOffset(ZoneOffset.systemDefault().rules.getOffset(d.toInstant())) + } + return CheckInModel( id = this.id, serverId = this.server_id, @@ -16,5 +26,7 @@ fun CheckIn.toModel(): CheckInModel { type = this.type, // Use date values from JSON, as they contain time zone information dateTime = SafeOffsetDateTimeMapper.decode(json, "datetime"), + localAnnulled = toOffsetDateTime(this.local_annulled), + localNonce = this.local_nonce ) } diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/OrderSyncAdapter.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/OrderSyncAdapter.kt index 9b4a7389..e59ba586 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/OrderSyncAdapter.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/OrderSyncAdapter.kt @@ -232,6 +232,8 @@ class OrderSyncAdapter( position = positionId, server_id = ci.optLong("id"), type = ci.optString("type", "entry"), + local_annulled = null, + local_nonce = null, ) checkinCreateCache.add(ciobj) } @@ -262,6 +264,8 @@ class OrderSyncAdapter( position = it.position, server_id = it.server_id, type = it.type, + local_annulled = null, + local_nonce = null, ) } checkinCreateCache.clear() @@ -568,6 +572,8 @@ class OrderSyncAdapter( position = c.position, server_id = c.server_id, type = c.type, + local_annulled = null, + local_nonce = null, ) } diff --git a/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/CheckIn.sq b/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/CheckIn.sq index 8a892cfb..cbaa5f5b 100644 --- a/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/CheckIn.sq +++ b/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/CheckIn.sq @@ -23,6 +23,9 @@ DELETE FROM CheckIn WHERE id = ?; deleteByIdList: DELETE FROM CheckIn WHERE id IN ?; +setAnnulledByNonce: +UPDATE CheckIn SET local_annulled = :annulled WHERE local_nonce = :nonce; + insert: INSERT INTO CheckIn ( datetime, @@ -30,7 +33,9 @@ INSERT INTO CheckIn ( listId, position, server_id, - type + type, + local_annulled, + local_nonce ) VALUES ( ?, @@ -38,6 +43,8 @@ VALUES ( ?, ?, ?, + ?, + ?, ? ); diff --git a/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/QueuedCheckIn.sq b/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/QueuedCheckIn.sq index 94bbba89..df9922b8 100644 --- a/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/QueuedCheckIn.sq +++ b/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/QueuedCheckIn.sq @@ -16,15 +16,20 @@ count: SELECT COUNT(*) FROM QueuedCheckIn; -countForSecretAndLists: +countForSecretAndListsNotAnnulled: SELECT COUNT(*) FROM QueuedCheckIn -WHERE secret = :secret AND checkinListId IN :checkin_list_ids; +WHERE secret = :secret AND checkinListId IN :checkin_list_ids AND annulled IS NULL; delete: DELETE FROM QueuedCheckIn WHERE id = ?; +setAnnulledByNonce: +UPDATE QueuedCheckIn +SET annulled = :annulled +WHERE nonce = :nonce; + insert: INSERT INTO QueuedCheckIn ( answers, @@ -35,7 +40,8 @@ INSERT INTO QueuedCheckIn ( nonce, secret, source_type, - type + type, + annulled ) VALUES ( ?, @@ -46,5 +52,6 @@ VALUES ( ?, ?, ?, + ?, ? ); diff --git a/libpretixsync/src/main/sqldelight/postgres/eu/pretix/libpretixsync/sqldelight/CheckIn.sq b/libpretixsync/src/main/sqldelight/postgres/eu/pretix/libpretixsync/sqldelight/CheckIn.sq index fa4934bd..69b65c37 100644 --- a/libpretixsync/src/main/sqldelight/postgres/eu/pretix/libpretixsync/sqldelight/CheckIn.sq +++ b/libpretixsync/src/main/sqldelight/postgres/eu/pretix/libpretixsync/sqldelight/CheckIn.sq @@ -8,9 +8,12 @@ CREATE TABLE CheckIn ( listId bigint, server_id bigint, type character varying(255), + local_nonce character varying(255), + local_annulled DATE AS Date, "position" bigint REFERENCES OrderPosition (id) ON DELETE CASCADE ); CREATE INDEX CheckIn_listId_index ON CheckIn (listId); CREATE INDEX CheckIn_position_index ON CheckIn (position); CREATE INDEX CheckIn_server_id_index ON CheckIn (server_id); +CREATE INDEX CheckIn_local_nonce_index ON CheckIn (local_nonce); diff --git a/libpretixsync/src/main/sqldelight/postgres/eu/pretix/libpretixsync/sqldelight/QueuedCheckIn.sq b/libpretixsync/src/main/sqldelight/postgres/eu/pretix/libpretixsync/sqldelight/QueuedCheckIn.sq index 2d664aac..a9214aea 100644 --- a/libpretixsync/src/main/sqldelight/postgres/eu/pretix/libpretixsync/sqldelight/QueuedCheckIn.sq +++ b/libpretixsync/src/main/sqldelight/postgres/eu/pretix/libpretixsync/sqldelight/QueuedCheckIn.sq @@ -11,5 +11,8 @@ CREATE TABLE QueuedCheckIn ( nonce character varying(255), secret character varying(255), source_type character varying(255), - type character varying(255) + type character varying(255), + annulled DATE AS Date ); + +CREATE INDEX QueuedCheckIn_nonce_index ON QueuedCheckIn (nonce); diff --git a/libpretixsync/src/main/sqldelight/postgres/migrations/118.sqm b/libpretixsync/src/main/sqldelight/postgres/migrations/118.sqm new file mode 100644 index 00000000..42d5fb49 --- /dev/null +++ b/libpretixsync/src/main/sqldelight/postgres/migrations/118.sqm @@ -0,0 +1,5 @@ +ALTER TABLE QueuedCheckIn ADD COLUMN annulled DATE NULL; +ALTER TABLE CheckIn ADD COLUMN local_nonce character varying(255) NULL; +ALTER TABLE CheckIn ADD COLUMN local_annulled DATE NULL; +CREATE INDEX CheckIn_local_nonce_index ON CheckIn (local_nonce); +CREATE INDEX QueuedCheckIn_nonce_index ON QueuedCheckIn (nonce); diff --git a/libpretixsync/src/main/sqldelight/sqlite/eu/pretix/libpretixsync/sqldelight/CheckIn.sq b/libpretixsync/src/main/sqldelight/sqlite/eu/pretix/libpretixsync/sqldelight/CheckIn.sq index 9488de27..c730a162 100644 --- a/libpretixsync/src/main/sqldelight/sqlite/eu/pretix/libpretixsync/sqldelight/CheckIn.sq +++ b/libpretixsync/src/main/sqldelight/sqlite/eu/pretix/libpretixsync/sqldelight/CheckIn.sq @@ -7,9 +7,12 @@ CREATE TABLE CheckIn ( listId INTEGER, position INTEGER REFERENCES OrderPosition (id) ON DELETE CASCADE, server_id INTEGER, - type TEXT + type TEXT, + local_nonce TEXT, + local_annulled TEXT AS Date ); CREATE INDEX CheckIn_listId_index ON CheckIn (listId); CREATE INDEX CheckIn_position_index ON CheckIn (position); CREATE INDEX CheckIn_server_id_index ON CheckIn (server_id); +CREATE INDEX CheckIn_local_nonce_index ON CheckIn (local_nonce); diff --git a/libpretixsync/src/main/sqldelight/sqlite/eu/pretix/libpretixsync/sqldelight/QueuedCheckIn.sq b/libpretixsync/src/main/sqldelight/sqlite/eu/pretix/libpretixsync/sqldelight/QueuedCheckIn.sq index 0735cd30..88b782e2 100644 --- a/libpretixsync/src/main/sqldelight/sqlite/eu/pretix/libpretixsync/sqldelight/QueuedCheckIn.sq +++ b/libpretixsync/src/main/sqldelight/sqlite/eu/pretix/libpretixsync/sqldelight/QueuedCheckIn.sq @@ -10,5 +10,8 @@ CREATE TABLE QueuedCheckIn ( nonce TEXT, secret TEXT, source_type TEXT, - type TEXT + type TEXT, + annulled TEXT AS Date ); + +CREATE INDEX QueuedCheckIn_nonce_index ON QueuedCheckIn (nonce); diff --git a/libpretixsync/src/main/sqldelight/sqlite/migrations/118.sqm b/libpretixsync/src/main/sqldelight/sqlite/migrations/118.sqm new file mode 100644 index 00000000..b8f966f5 --- /dev/null +++ b/libpretixsync/src/main/sqldelight/sqlite/migrations/118.sqm @@ -0,0 +1,5 @@ +ALTER TABLE QueuedCheckIn ADD COLUMN annulled TEXT; +ALTER TABLE CheckIn ADD COLUMN local_nonce TEXT; +ALTER TABLE CheckIn ADD COLUMN local_annulled TEXT; +CREATE INDEX QueuedCheckIn_nonce_index ON QueuedCheckIn (nonce); +CREATE INDEX CheckIn_local_nonce_index ON CheckIn (local_nonce); 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 d1ca1ee7..7d235e51 100644 --- a/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderTest.kt +++ b/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderTest.kt @@ -2,6 +2,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.sync.CheckInListSyncAdapter import eu.pretix.libpretixsync.sync.EventSyncAdapter import eu.pretix.libpretixsync.sync.ItemSyncAdapter @@ -1177,4 +1178,45 @@ class AsyncCheckProviderTest : BaseDatabaseTest() { assertEquals(TicketCheckProvider.CheckResult.Type.VALID, r.type) assertEquals(db.queuedCheckInQueries.count().executeAsOne(), 1L) } + + @Test + fun testAnnullNotYetUploaded() { + var r = p!!.check(mapOf("demo" to 1L), "kfndgffgyw4tdgcacx6bb3bgemq69cxj") + assertEquals(TicketCheckProvider.CheckResult.Type.VALID, r.type) + val nonce = r.nonce!! + + r = p!!.check(mapOf("demo" to 1L), "kfndgffgyw4tdgcacx6bb3bgemq69cxj") + assertEquals(TicketCheckProvider.CheckResult.Type.USED, r.type) + assertEquals(db.queuedCallQueries.count().executeAsOne(), 1L) + + p!!.annul(mapOf("demo" to 1L), nonce, "Turnstile did not turn") + val qci = db.queuedCheckInQueries.selectAll().executeAsList() + assertEquals(qci.size, 1) + assertEquals(qci.first().nonce, nonce) + assert(qci.first().annulled != null) + assertEquals(db.queuedCallQueries.count().executeAsOne(), 2L) + + r = p!!.check(mapOf("demo" to 1L), "kfndgffgyw4tdgcacx6bb3bgemq69cxj") + assertEquals(TicketCheckProvider.CheckResult.Type.VALID, r.type) + } + + @Test + fun testSignedAnnullNotYetUploaded() { + var r = p!!.check(mapOf("demo" to 1L), "E4BibyTSylQOgeKjuMPiTDxi5HXPuTVsx1qCli3IL0143gj0EZXOB9iQInANxRFJTt4Pf9nXnHdB91Qk/RN0L5AIBABSxw2TKFnSUNUCKAEAPAQA") + assertEquals(TicketCheckProvider.CheckResult.Type.VALID, r.type) + val nonce = r.nonce!! + r = p!!.check(mapOf("demo" to 1L), "E4BibyTSylQOgeKjuMPiTDxi5HXPuTVsx1qCli3IL0143gj0EZXOB9iQInANxRFJTt4Pf9nXnHdB91Qk/RN0L5AIBABSxw2TKFnSUNUCKAEAPAQA") + assertEquals(TicketCheckProvider.CheckResult.Type.USED, r.type) + assertEquals(db.queuedCallQueries.count().executeAsOne(), 1L) + + p!!.annul(mapOf("demo" to 1L), nonce, "Turnstile did not turn") + val qci = db.queuedCheckInQueries.selectAll().executeAsList() + assertEquals(qci.size, 1) + assertEquals(qci.first().nonce, nonce) + assert(qci.first().annulled != null) + assertEquals(db.queuedCallQueries.count().executeAsOne(), 2L) + + r = p!!.check(mapOf("demo" to 1L), "E4BibyTSylQOgeKjuMPiTDxi5HXPuTVsx1qCli3IL0143gj0EZXOB9iQInANxRFJTt4Pf9nXnHdB91Qk/RN0L5AIBABSxw2TKFnSUNUCKAEAPAQA") + assertEquals(TicketCheckProvider.CheckResult.Type.VALID, r.type) + } } diff --git a/libpretixsync/src/test/java/eu/pretix/libpretixsync/db/BaseDatabaseTest.java b/libpretixsync/src/test/java/eu/pretix/libpretixsync/db/BaseDatabaseTest.java index 522908c9..81a2c492 100644 --- a/libpretixsync/src/test/java/eu/pretix/libpretixsync/db/BaseDatabaseTest.java +++ b/libpretixsync/src/test/java/eu/pretix/libpretixsync/db/BaseDatabaseTest.java @@ -62,6 +62,7 @@ public void setUpDb() throws NoSuchAlgorithmException { db = SyncDatabase.Companion.invoke( driver, new CheckIn.Adapter( + dateAdapter, dateAdapter ), new Closing.Adapter( @@ -79,6 +80,7 @@ public void setUpDb() throws NoSuchAlgorithmException { dateAdapter ), new QueuedCheckIn.Adapter( + dateAdapter, dateAdapter ), new Receipt.Adapter(