Skip to content
Closed
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 @@ -99,7 +99,22 @@ open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, h
}

@Throws(ApiException::class, JSONException::class)
open fun redeem(lists: List<Long>, secret: String, datetime: String?, force: Boolean, nonce: String?, answers: List<Answer>?, ignore_unpaid: Boolean, pdf_data: Boolean, type: String?, source_type: String?, callTimeout: Long? = null, questions_supported: Boolean = true): ApiResponse {
open fun redeem(
lists: List<Long>,
secret: String,
datetime: String?,
force: Boolean,
nonce: String?,
answers: List<Answer>?,
ignore_unpaid: Boolean,
pdf_data: Boolean,
type: String?,
source_type: String?,
callTimeout: Long? = null,
questions_supported: Boolean = true,
exchange_medium_type: String? = null,
exchange_medium_identifier: String? = null,
): ApiResponse {
val body = JSONObject()
if (datetime != null) {
body.put("datetime", datetime)
Expand Down Expand Up @@ -135,6 +150,8 @@ open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, h
jlists.put(l)
}
body.put("lists", jlists)
if (exchange_medium_type != null) body.put("exchange_medium_type", exchange_medium_type)
if (exchange_medium_identifier != null) body.put("exchange_medium_identifier", exchange_medium_identifier)
var pd = "?expand=answers.question"
if (pdf_data) {
pd += "&pdf_data=true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ data class MultiCheckInput(

// TODO: Check unused values
val allowQuestions: Boolean,
val nonce: String?
val nonce: String?,
val exchange_medium_type: String?,
val exchange_medium_identifier: String?,
)

data class CheckInput(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import eu.pretix.libpretixsync.crypto.isValidSignature
import eu.pretix.libpretixsync.crypto.readPubkeyFromPem
import eu.pretix.libpretixsync.crypto.sig1.TicketProtos
import eu.pretix.libpretixsync.db.Answer
import eu.pretix.libpretixsync.db.MediaPolicy
import eu.pretix.libpretixsync.db.NonceGenerator
import eu.pretix.libpretixsync.db.QuestionLike
import eu.pretix.libpretixsync.db.ReusableMediaType
import eu.pretix.libpretixsync.models.CheckIn
import eu.pretix.libpretixsync.models.Event
import eu.pretix.libpretixsync.models.Order as OrderModel
Expand Down Expand Up @@ -279,7 +281,14 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa
return RSAResult(givenAnswers, requiredAnswers, shownAnswers, askQuestions)
}

private fun checkOfflineWithoutData(eventsAndCheckinLists: Map<String, Long>, ticketid: String, type: TicketCheckProvider.CheckInType, answers: List<Answer>?, nonce: String?, allowQuestions: Boolean): TicketCheckProvider.CheckResult {
private fun checkOfflineWithoutData(
eventsAndCheckinLists: Map<String, Long>,
ticketid: String,
type: TicketCheckProvider.CheckInType,
answers: List<Answer>?,
nonce: String?,
allowQuestions: Boolean
): TicketCheckProvider.CheckResult {
val dt = now()
val events = db.eventQueries.selectBySlugList(eventsAndCheckinLists.keys.toList())
.executeAsList()
Expand Down Expand Up @@ -544,11 +553,21 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa
type: TicketCheckProvider.CheckInType,
nonce: String?,
allowQuestions: Boolean,
exchange_medium_type: String?,
exchange_medium_identifier: String?,
): TicketCheckProvider.CheckResult {
val ticketid_cleaned = cleanInput(ticketid, source_type)

sentry.addBreadcrumb("provider.check", "offline check started")

if (exchange_medium_type != null || exchange_medium_identifier != null) {
return TicketCheckProvider.CheckResult(
TicketCheckProvider.CheckResult.Type.ERROR,
"Media exchange is not supported in offline mode",
offline = true
)
}

val tickets = db.orderPositionQueries.selectBySecretAndEventSlugs(
secret = ticketid_cleaned,
event_slugs = eventsAndCheckinLists.keys.toList(),
Expand Down Expand Up @@ -609,7 +628,16 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa
return checkOfflineWithData(eventsAndCheckinLists, ticketid_cleaned, tickets, answers, ignore_unpaid, type, nonce = nonce, allowQuestions = allowQuestions)
}

private fun checkOfflineWithData(eventsAndCheckinLists: Map<String, Long>, secret: String, tickets: List<OrderPositionModel>, answers: List<Answer>?, ignore_unpaid: Boolean, type: TicketCheckProvider.CheckInType, nonce: String?, allowQuestions: Boolean): TicketCheckProvider.CheckResult {
private fun checkOfflineWithData(
eventsAndCheckinLists: Map<String, Long>,
secret: String,
tickets: List<OrderPositionModel>,
answers: List<Answer>?,
ignore_unpaid: Boolean,
type: TicketCheckProvider.CheckInType,
nonce: String?,
allowQuestions: Boolean,
): TicketCheckProvider.CheckResult {
// !!! When extending this, also extend checkOfflineWithoutData !!!
val dt = now()

Expand Down Expand Up @@ -781,6 +809,28 @@ class AsyncCheckProvider(private val config: ConfigStore, private val db: SyncDa
return res
}

val settings = db.settingsQueries.selectBySlug(eventSlug).executeAsOneOrNull()?.toModel()
val reusableMediaUsageEnforced = (settings?.json?.optBoolean("reusable_media_usage_enforced", false) == true)

val hasLinkedReusableMedium =
db.reusableMediumQueries.selectByLinkedOrderPosition(position.positionId)
.executeAsList().isNotEmpty()

if (positionItem.mediaPolicy != MediaPolicy.NONE && positionItem.mediaType != ReusableMediaType.NONE) {
if (!hasLinkedReusableMedium) {
res.type = TicketCheckProvider.CheckResult.Type.ERROR // EXCHANGE_REQUIRED, but not in offline mode
res.isCheckinAllowed = false
res.reasonExplanation = "This ticket needs to be exchanged, but this isn't possible while offline"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we translate that in the ui? → pretixSCAN

storeFailedCheckin(eventSlug, list.serverId, "exchange", position.secret!!, type, position = position.serverId, item = positionItem.serverId, variation = position.variationServerId, subevent = position.subEventServerId, nonce = nonce)
return res
} else if (reusableMediaUsageEnforced) {
res.type = TicketCheckProvider.CheckResult.Type.ALREADY_EXCHANGED
res.isCheckinAllowed = false
storeFailedCheckin(eventSlug, list.serverId, "already_exchanged", position.secret!!, type, position = position.serverId, item = positionItem.serverId, variation = position.variationServerId, subevent = position.subEventServerId, nonce = nonce)
return res
}
}

// !!! When extending this, also extend checkOfflineWithoutData !!!

val rules = list.rules
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import eu.pretix.libpretixsync.api.PretixApi
import eu.pretix.libpretixsync.api.TimeoutApiException
import eu.pretix.libpretixsync.config.ConfigStore
import eu.pretix.libpretixsync.db.Answer
import eu.pretix.libpretixsync.db.MediaPolicy
import eu.pretix.libpretixsync.db.NonceGenerator
import eu.pretix.libpretixsync.db.ReusableMediaType
import eu.pretix.libpretixsync.models.db.toModel
import eu.pretix.libpretixsync.sqldelight.Question
import eu.pretix.libpretixsync.sqldelight.SyncDatabase
Expand Down Expand Up @@ -47,7 +49,9 @@ class OnlineCheckProvider(
with_badge_data: Boolean,
type: TicketCheckProvider.CheckInType,
nonce: String?,
allowQuestions: Boolean
allowQuestions: Boolean,
exchange_medium_type: String?,
exchange_medium_identifier: String?,
): TicketCheckProvider.CheckResult {
val ticketid_cleaned = cleanInput(ticketid, source_type)
val nonce_cleaned = nonce ?: NonceGenerator.nextNonce()
Expand All @@ -70,6 +74,8 @@ class OnlineCheckProvider(
source_type,
callTimeout = if (fallback != null) fallbackTimeout.toLong() else null,
questions_supported = allowQuestions,
exchange_medium_type = exchange_medium_type,
exchange_medium_identifier = exchange_medium_identifier,
)
} else {
if (eventsAndCheckinLists.size != 1) throw CheckException("Multi-event scan not supported by server.")
Expand Down Expand Up @@ -124,6 +130,16 @@ class OnlineCheckProvider(
required_answers.add(TicketCheckProvider.QuestionAnswer(question, q.toString(), ""))
}
res.requiredAnswers = required_answers
} else if ("exchange" == status){
res.type = TicketCheckProvider.CheckResult.Type.EXCHANGE_REQUIRED
try {
res.requiredMediaPolicy =
MediaPolicy.getByServerName(response.optString("media_policy"))
res.requiredMediaType =
ReusableMediaType.getByServerName(response.optString("media_type"))
} catch (_: IllegalArgumentException) {
// silently fall back to null
}
} else {
val reason = response.optString("reason")
if ("already_redeemed" == reason) {
Expand Down Expand Up @@ -163,6 +179,8 @@ class OnlineCheckProvider(
res.isCheckinAllowed = includePending && response.has("position") && response.getJSONObject("position").optString("order__status", "n") == "n"
} else if ("product" == reason) {
res.type = TicketCheckProvider.CheckResult.Type.PRODUCT
} else if ("already_exchanged" == reason) {
res.type = TicketCheckProvider.CheckResult.Type.ALREADY_EXCHANGED
} else {
res.type = TicketCheckProvider.CheckResult.Type.ERROR
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ class ProxyCheckProvider(private val config: ConfigStore, httpClientFactory: Htt
with_badge_data: Boolean,
type: TicketCheckProvider.CheckInType,
nonce: String?,
allowQuestions: Boolean
allowQuestions: Boolean,
exchange_medium_type: String?,
exchange_medium_identifier: String?,
): TicketCheckProvider.CheckResult {
val answersInput = answers?.map {
val questionModel = it.question as Question // TODO: Can we avoid the cast?
Expand All @@ -130,6 +132,8 @@ class ProxyCheckProvider(private val config: ConfigStore, httpClientFactory: Htt
type = type.name,
allowQuestions = allowQuestions,
nonce = nonce,
exchange_medium_type = exchange_medium_type,
exchange_medium_identifier = exchange_medium_identifier,
)

return try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package eu.pretix.libpretixsync.check

import eu.pretix.libpretixsync.SentryInterface
import eu.pretix.libpretixsync.db.Answer
import eu.pretix.libpretixsync.db.MediaPolicy
import eu.pretix.libpretixsync.db.ReusableMediaType
import eu.pretix.libpretixsync.models.db.toModel
import eu.pretix.libpretixsync.sqldelight.Question
import eu.pretix.libpretixsync.models.Question as QuestionModel
Expand Down Expand Up @@ -57,7 +59,8 @@ interface TicketCheckProvider {
class CheckResult {
enum class Type {
INVALID, VALID, USED, ERROR, UNPAID, BLOCKED, INVALID_TIME, CANCELED, PRODUCT, RULES,
ANSWERS_REQUIRED, AMBIGUOUS, REVOKED, UNAPPROVED
ANSWERS_REQUIRED, AMBIGUOUS, REVOKED, UNAPPROVED, ALREADY_EXCHANGED, MEDIUM_INVALID,
MEDIUM_EXISTS, EXCHANGE_REQUIRED
}

var type: Type? = null
Expand All @@ -80,6 +83,8 @@ interface TicketCheckProvider {
var position: JSONObject? = null
var eventSlug: String? = null
var offline: Boolean = false
var requiredMediaPolicy: MediaPolicy? = null
var requiredMediaType: ReusableMediaType? = null

constructor(type: Type?, message: String?, offline: Boolean = false) {
this.type = type
Expand Down Expand Up @@ -157,7 +162,20 @@ interface TicketCheckProvider {
class StatusResult(var eventName: String?, var totalTickets: Int, var alreadyScanned: Int, var currentlyInside: Int?, var items: List<StatusResultItem>?) {
}

fun check(eventsAndCheckinLists: Map<String, Long>, ticketid: String, source_type: String, answers: List<Answer>?, ignore_unpaid: Boolean, with_badge_data: Boolean, type: CheckInType, nonce: String? = null, allowQuestions: Boolean = true): CheckResult
fun check(
eventsAndCheckinLists: Map<String, Long>,
ticketid: String,
source_type: String,
answers: List<Answer>?,
ignore_unpaid: Boolean,
with_badge_data: Boolean,
type: CheckInType,
nonce: String? = null,
allowQuestions: Boolean = true,
exchange_medium_type: String? = null,
exchange_medium_identifier: String? = null,
): CheckResult

fun check(eventsAndCheckinLists: Map<String, Long>, ticketid: String): CheckResult
@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
@@ -1,8 +1,15 @@
package eu.pretix.libpretixsync.db

enum class MediaPolicy {
NONE,
REUSE,
NEW,
REUSE_OR_NEW,
}
enum class MediaPolicy(val serverName: String?) {
NONE(null),
NEW("new"),
REUSE("reuse"),
REUSE_OR_NEW("reuse_or_new"),
APPEND("append"),
APPEND_OR_NEW("append_or_new");

companion object {
private val map = entries.associateBy(MediaPolicy::serverName)
fun getByServerName(serverName: String?) = map[serverName]
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package eu.pretix.libpretixsync.db

enum class ReusableMediaType(val serverName: String?) {
NONE(null),
BARCODE("barcode"),
NFC_UID("nfc_uid"),

NFC_MF0AES("nfc_mf0aes"),
UNSUPPORTED("unsupported");

fun isNfcBased(): Boolean {
return this.serverName?.startsWith("nfc_") ?: false;
}

companion object {
private val map = ReusableMediaType.entries.associateBy(ReusableMediaType::serverName)
fun getByServerName(serverName: String?) = map[serverName]
Comment thread
robbi5 marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,9 @@ private fun parseHasFreePrice(json: JSONObject): Boolean {
private fun parseMediaPolicy(json: JSONObject): MediaPolicy {
return try {
val mp: String = json.optString("media_policy") ?: return MediaPolicy.NONE
if (mp == "reuse") return MediaPolicy.REUSE
if (mp == "new") return MediaPolicy.NEW
if (mp == "reuse_or_new") MediaPolicy.REUSE_OR_NEW else MediaPolicy.NONE
MediaPolicy.getByServerName(mp) ?: return MediaPolicy.NONE
} catch (_: IllegalArgumentException) {
MediaPolicy.NONE
} catch (e: JSONException) {
e.printStackTrace()
MediaPolicy.NONE
Expand All @@ -220,9 +220,7 @@ private fun parseMediaPolicy(json: JSONObject): MediaPolicy {
private fun parseMediaType(json: JSONObject): ReusableMediaType {
return try {
val mp: String = json.optString("media_type") ?: return ReusableMediaType.NONE
if (mp == "barcode") return ReusableMediaType.BARCODE
if (mp == "nfc_uid") return ReusableMediaType.NFC_UID
if (mp == "nfc_mf0aes") ReusableMediaType.NFC_MF0AES else ReusableMediaType.UNSUPPORTED
ReusableMediaType.getByServerName(mp) ?: ReusableMediaType.UNSUPPORTED
} catch (e: JSONException) {
e.printStackTrace()
ReusableMediaType.NONE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ WHERE ReusableMedium.identifier = :identifier
AND ReusableMedium.type = :type
AND orders.event_slug IN :event_slugs;

selectByLinkedOrderPosition:
SELECT ReusableMedium.*
FROM ReusableMedium
LEFT JOIN ReusableMedium_OrderPosition ON ReusableMedium_OrderPosition.OrderPositionId = ReusableMedium.id
WHERE OrderPositionId = :order_position_id;

deleteByServerId:
DELETE FROM ReusableMedium
WHERE server_id = ?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ class FakePretixApi : PretixApi("http://1.1.1.1/", "a", "demo", 1, DefaultHttpCl
type: String?,
source_type: String?,
callTimeout: Long?,
questions_supported: Boolean
questions_supported: Boolean,
exchange_medium_type: String?,
exchange_medium_identifier: String?,
): ApiResponse {
redeemRequestSecret = secret
redeemRequestDatetime = datetime
Expand Down
Loading