From 18b380e591433745e6d5a219626d6830821ade72 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 9 Jun 2026 12:02:08 +0200 Subject: [PATCH] block expired media --- doc/api/resources/checkin.rst | 2 +- src/pretix/base/models/media.py | 2 +- src/pretix/base/services/media.py | 14 ++++++++++++++ src/tests/api/test_checkinrpc.py | 25 +++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/doc/api/resources/checkin.rst b/doc/api/resources/checkin.rst index 1368b9b47c..1d57162b05 100644 --- a/doc/api/resources/checkin.rst +++ b/doc/api/resources/checkin.rst @@ -230,7 +230,7 @@ Checking a ticket in * ``revoked`` - Ticket code has been revoked. * ``unapproved`` - Order has not yet been approved. * ``already_exchanged`` - Ticket already has been exchanged for a reusable medium that must now be used for check-in. - * ``medium_invalid`` - Reusable medium identifier given was not found and could not be automatically created. + * ``medium_invalid`` - Reusable medium identifier given was not found or is not valid. * ``medium_exists`` - Reusable medium identifier already exists, but expected to be new. * ``error`` - Internal error. diff --git a/src/pretix/base/models/media.py b/src/pretix/base/models/media.py index eabefae59f..59520a3afa 100644 --- a/src/pretix/base/models/media.py +++ b/src/pretix/base/models/media.py @@ -129,7 +129,7 @@ class ReusableMedium(LoggedModel): @property def is_expired(self): - return self.expires and self.expires > now() + return self.expires and self.expires < now() class Meta: unique_together = (("identifier", "type", "organizer"),) diff --git a/src/pretix/base/services/media.py b/src/pretix/base/services/media.py index 903cdd4479..b58eedf83c 100644 --- a/src/pretix/base/services/media.py +++ b/src/pretix/base/services/media.py @@ -130,7 +130,15 @@ def perform_media_exchange(organizer, media_type, identifier, link_action, link_ raise CheckInError( _('Reusable medium not found.'), Checkin.REASON_MEDIUM_INVALID, + reason=_('Reusable medium not found.'), ) + else: + if medium.is_expired or not medium.active: + raise CheckInError( + _('Reusable medium is inactive or expired.'), + Checkin.REASON_MEDIUM_INVALID, + reason=_('Reusable medium is inactive or expired.'), + ) elif media_policy == Item.MEDIA_POLICY_REUSE_OR_NEW: medium, created = ReusableMedium.objects.get_or_create( @@ -144,6 +152,12 @@ def perform_media_exchange(organizer, media_type, identifier, link_action, link_ user=user, auth=auth, ) + elif medium.is_expired or not medium.active: + raise CheckInError( + _('Reusable medium is inactive or expired.'), + Checkin.REASON_MEDIUM_INVALID, + reason=_('Reusable medium is inactive or expired.'), + ) elif media_policy == Item.MEDIA_POLICY_NEW: try: diff --git a/src/tests/api/test_checkinrpc.py b/src/tests/api/test_checkinrpc.py index ad7cfc4627..c2d1e4e3aa 100644 --- a/src/tests/api/test_checkinrpc.py +++ b/src/tests/api/test_checkinrpc.py @@ -1451,6 +1451,31 @@ def test_exchange_reuse_exists_append(token_client, organizer, clist, event, ord assert rm.linked_orderpositions.filter(secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w").exists() +@pytest.mark.django_db +def test_exchange_reuse_expired(token_client, organizer, clist, event, order, item): + organizer.settings.reusable_media_type_nfc_uid = True + item.media_type = "nfc_uid" + item.media_policy = Item.MEDIA_POLICY_REUSE + item.save() + with scopes_disabled(): + rm = ReusableMedium.objects.create( + type="nfc_uid", + identifier="12345678", + organizer=organizer, + expires=now() - datetime.timedelta(hours=2), + ) + rm.linked_orderpositions.add(order.positions.last()) + resp = _redeem(token_client, organizer, clist, "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", { + "source_type": "barcode", + "exchange_medium_type": "nfc_uid", + "exchange_medium_identifier": "12345678", + "exchange_link_action": "replace", + }) + assert resp.status_code == 400 + assert resp.data['status'] == 'error' + assert resp.data['reason'] == 'medium_invalid' + + @pytest.mark.django_db def test_exchange_reuse_not_exists(token_client, organizer, clist, event, order, item): organizer.settings.reusable_media_type_nfc_uid = True