diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index ae9b0c3349..7f3cc2723b 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -284,11 +284,12 @@ class FailedCheckinSerializer(I18nAwareModelSerializer): raw_item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True) raw_variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True) raw_subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True) + nonce = serializers.CharField(required=False, allow_null=True) class Meta: model = Checkin fields = ('error_reason', 'error_explanation', 'raw_barcode', 'raw_item', 'raw_variation', - 'raw_subevent', 'datetime', 'type', 'position') + 'raw_subevent', 'nonce', 'datetime', 'type', 'position') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py index 41a482066e..ada7cabea5 100644 --- a/src/pretix/api/views/checkin.py +++ b/src/pretix/api/views/checkin.py @@ -164,8 +164,21 @@ class CheckinListViewSet(viewsets.ModelViewSet): secret=serializer.validated_data['raw_barcode'] ).first() + clist = self.get_object() + if serializer.validated_data.get('nonce'): + if kwargs.get('position'): + prev = kwargs['position'].all_checkins.filter(nonce=serializer.validated_data['nonce']).first() + else: + prev = clist.checkins.filter( + nonce=serializer.validated_data['nonce'], + raw_barcode=serializer.validated_data['raw_barcode'], + ).first() + if prev: + # Ignore because nonce is already handled + return Response(serializer.data, status=201) + c = serializer.save( - list=self.get_object(), + list=clist, successful=False, forced=True, force_sent=True, diff --git a/src/tests/api/test_checkin.py b/src/tests/api/test_checkin.py index b7ef1b46ce..d412794d5c 100644 --- a/src/tests/api/test_checkin.py +++ b/src/tests/api/test_checkin.py @@ -794,6 +794,26 @@ def test_reupload_same_nonce(token_client, organizer, clist, event, order): resp = _redeem(token_client, organizer, clist, p.pk, {'nonce': 'foobar'}) assert resp.status_code == 201 assert resp.data['status'] == 'ok' + with scopes_disabled(): + assert p.all_checkins.count() == 1 + + +@pytest.mark.django_db +def test_reupload_same_nonce_not_ignored_after_failed(token_client, organizer, clist, event, order): + with scopes_disabled(): + p = order.positions.first() + p.all_checkins.create( + type=Checkin.TYPE_ENTRY, + nonce='foobar', + successful=False, + list=clist, + ) + + resp = _redeem(token_client, organizer, clist, p.pk, {'nonce': 'foobar'}) + assert resp.status_code == 201 + assert resp.data['status'] == 'ok' + with scopes_disabled(): + assert p.all_checkins.count() == 2 @pytest.mark.django_db @@ -1105,6 +1125,7 @@ def test_store_failed(token_client, organizer, clist, event, order): organizer.slug, event.slug, clist.pk, ), { 'raw_barcode': '123456', + 'nonce': '4321', 'error_reason': 'invalid' }, format='json') assert resp.status_code == 201 @@ -1115,6 +1136,7 @@ def test_store_failed(token_client, organizer, clist, event, order): organizer.slug, event.slug, clist.pk, ), { 'raw_barcode': '123456', + 'nonce': '1234', 'position': p.pk, 'error_reason': 'unpaid' }, format='json') @@ -1122,6 +1144,28 @@ def test_store_failed(token_client, organizer, clist, event, order): with scopes_disabled(): assert p.all_checkins.filter(successful=False).count() == 1 + # Ignore sending the same nonces again + resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/failed_checkins/'.format( + organizer.slug, event.slug, clist.pk, + ), { + 'raw_barcode': '123456', + 'nonce': '4321', + 'error_reason': 'invalid' + }, format='json') + assert resp.status_code == 201 + + resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/failed_checkins/'.format( + organizer.slug, event.slug, clist.pk, + ), { + 'raw_barcode': '123456', + 'nonce': '1234', + 'position': p.pk, + 'error_reason': 'unpaid' + }, format='json') + assert resp.status_code == 201 + with scopes_disabled(): + assert Checkin.all.filter(successful=False).count() == 2 + resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/failed_checkins/'.format( organizer.slug, event.slug, clist.pk, ), {