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 @@ -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
Expand Down Expand Up @@ -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<Long>, 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<Long>,
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ data class MultiCheckInput(
val exchange_medium_identifier: String?,
)

data class AnnulInput(
val events_and_checkin_lists: Map<String, Long>,
val nonce: String,
val explanation: String,
)

data class CheckInput(
val ticketid: String,
val answers: List<CheckInputAnswer>?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -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) {
/*
:<json boolean error_reason: One of ``canceled``, ``invalid``, ``unpaid``, ``product``, ``rules``, ``revoked``,
``incomplete``, ``already_redeemed``, ``blocked``, ``invalid_time``, or ``error``. Required.
Expand All @@ -77,7 +76,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa
TicketCheckProvider.CheckInType.EXIT -> "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)
Expand All @@ -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/",
)
}
Expand Down Expand Up @@ -289,7 +288,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa
ticketid: String,
type: TicketCheckProvider.CheckInType,
answers: List<Answer>?,
nonce: String?,
nonce: String,
allowQuestions: Boolean
): TicketCheckProvider.CheckResult {
val dt = now()
Expand Down Expand Up @@ -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
Expand All @@ -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 }))

Expand Down Expand Up @@ -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,
)
}
}
Expand All @@ -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<String, Long>,
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<String, Long>,
ticketid: String,
Expand All @@ -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")
Expand Down Expand Up @@ -808,7 +834,7 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa
answers: List<Answer>?,
ignore_unpaid: Boolean,
type: TicketCheckProvider.CheckInType,
nonce: String?,
nonce: String,
allowQuestions: Boolean,
mediumUsed: Boolean
): TicketCheckProvider.CheckResult {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand All @@ -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,
)
}
}
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -352,6 +353,37 @@ class OnlineCheckProvider(
return check(eventsAndCheckinLists, ticketid, "barcode", ArrayList(), false, true, TicketCheckProvider.CheckInType.ENTRY)
}

override fun annul(
eventsAndCheckinLists: Map<String, Long>,
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<String, Long>, query: String, page: Int): List<TicketCheckProvider.SearchResult> {
sentry.addBreadcrumb("provider.search", "started")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -217,4 +218,37 @@ class ProxyCheckProvider(private val config: ConfigStore, httpClientFactory: Htt
throw CheckException(e.message, e)
}
}

override fun annul(
eventsAndCheckinLists: Map<String, Long>,
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -179,6 +189,9 @@ interface TicketCheckProvider {
): CheckResult

fun check(eventsAndCheckinLists: Map<String, Long>, ticketid: String): CheckResult

fun annul(eventsAndCheckinLists: Map<String, Long>, nonce: String, explanation: String): AnnulResult

@Throws(CheckException::class)
fun search(eventsAndCheckinLists: Map<String, Long>, query: String, page: Int): List<SearchResult>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ data class CheckIn(
val type: String?,
val listServerId: Long?,
val positionId: Long?,
val localNonce: String?,
val localAnnulled: OffsetDateTime?
)
Loading
Loading