From 99c3981e2da877c9685621aa67ceb4113285ce7f Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 2 Dec 2020 16:10:05 +0100 Subject: [PATCH] Gift card API: Allow to inspect transactions (#1868) --- doc/api/resources/giftcards.rst | 61 +++++++++++++++++++++++++ src/pretix/api/serializers/organizer.py | 19 +++++++- src/pretix/api/urls.py | 4 ++ src/pretix/api/views/organizer.py | 28 ++++++++++-- src/tests/api/test_giftcards.py | 23 ++++++++++ src/tests/api/test_permissions.py | 2 + 6 files changed, 130 insertions(+), 7 deletions(-) diff --git a/doc/api/resources/giftcards.rst b/doc/api/resources/giftcards.rst index 8465d9e1c4..64097ea186 100644 --- a/doc/api/resources/giftcards.rst +++ b/doc/api/resources/giftcards.rst @@ -22,9 +22,28 @@ expires datetime Expiry date (or conditions string Special terms and conditions for this card (or ``null``) ===================================== ========================== ======================================================= +The gift card transaction resource contains the following public fields: + +.. rst-class:: rest-resource-table + +===================================== ========================== ======================================================= +Field Type Description +===================================== ========================== ======================================================= +id integer Internal ID of the gift card transaction +datetime datetime Creation date of the transaction +value money (string) Transaction amount +event string Event slug, if the gift card was used in the web shop (or ``null``) +order string Order code, if the gift card was used in the web shop (or ``null``) +text string Custom text of the transaction (or ``null``) +===================================== ========================== ======================================================= + Endpoints --------- +.. versionadded:: 3.14 + + The transaction list endpoint was added. + .. http:get:: /api/v1/organizers/(organizer)/giftcards/ Returns a list of all gift cards issued by a given organizer. @@ -250,3 +269,45 @@ Endpoints :statuscode 401: Authentication failure :statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource. :statuscode 409: There is not sufficient credit on the gift card. + +.. http:get:: /api/v1/organizers/(organizer)/giftcards/(id)/transactions/ + + List all transactions of a gift card. + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/organizers/bigevents/giftcards/1/transactions/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: application/json + + { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 82, + "datetime": "2020-06-22T15:41:42.800534Z", + "value": "50.00", + "event": "democon", + "order": "FXQYW", + "text": null + } + ] + } + + :param organizer: The ``slug`` field of the organizer to view + :param id: The ``id`` field of the gift card to view + :statuscode 200: no error + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource. diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index 60d3d094be..24692ab824 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -9,8 +9,8 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.order import CompatibleJSONField from pretix.base.auth import get_auth_backends from pretix.base.models import ( - Device, GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, - User, + Device, GiftCard, GiftCardTransaction, Organizer, SeatingPlan, Team, + TeamAPIToken, TeamInvite, User, ) from pretix.base.models.seating import SeatingPlanLayoutValidator from pretix.base.services.mail import SendMailException, mail @@ -59,6 +59,21 @@ class GiftCardSerializer(I18nAwareModelSerializer): fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions') +class OrderEventSlugField(serializers.RelatedField): + + def to_representation(self, obj): + return obj.event.slug + + +class GiftCardTransactionSerializer(I18nAwareModelSerializer): + order = serializers.SlugRelatedField(slug_field='code', read_only=True) + event = OrderEventSlugField(source='order', read_only=True) + + class Meta: + model = GiftCardTransaction + fields = ('id', 'datetime', 'value', 'event', 'order', 'text') + + class EventSlugField(serializers.SlugRelatedField): def get_queryset(self): return self.context['organizer'].events.all() diff --git a/src/pretix/api/urls.py b/src/pretix/api/urls.py index 5a59e9f29e..7cc16669ca 100644 --- a/src/pretix/api/urls.py +++ b/src/pretix/api/urls.py @@ -62,6 +62,9 @@ order_router = routers.DefaultRouter() order_router.register(r'payments', order.PaymentViewSet) order_router.register(r'refunds', order.RefundViewSet) +giftcard_router = routers.DefaultRouter() +giftcard_router.register(r'transactions', organizer.GiftCardTransactionViewSet) + # Force import of all plugins to give them a chance to register URLs with the router for app in apps.get_app_configs(): if hasattr(app, 'PretixPluginMeta'): @@ -71,6 +74,7 @@ for app in apps.get_app_configs(): urlpatterns = [ url(r'^', include(router.urls)), url(r'^organizers/(?P[^/]+)/', include(orga_router.urls)), + url(r'^organizers/(?P[^/]+)/giftcards/(?P[^/]+)/', include(giftcard_router.urls)), url(r'^organizers/(?P[^/]+)/events/(?P[^/]+)/settings/$', event.EventSettingsView.as_view(), name="event.settings"), url(r'^organizers/(?P[^/]+)/events/(?P[^/]+)/', include(event_router.urls)), diff --git a/src/pretix/api/views/organizer.py b/src/pretix/api/views/organizer.py index 532991d9ea..d33007cbfb 100644 --- a/src/pretix/api/views/organizer.py +++ b/src/pretix/api/views/organizer.py @@ -15,13 +15,13 @@ from rest_framework.viewsets import GenericViewSet from pretix.api.models import OAuthAccessToken from pretix.api.serializers.organizer import ( - DeviceSerializer, GiftCardSerializer, OrganizerSerializer, - SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer, - TeamMemberSerializer, TeamSerializer, + DeviceSerializer, GiftCardSerializer, GiftCardTransactionSerializer, + OrganizerSerializer, SeatingPlanSerializer, TeamAPITokenSerializer, + TeamInviteSerializer, TeamMemberSerializer, TeamSerializer, ) from pretix.base.models import ( - Device, GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, - User, + Device, GiftCard, GiftCardTransaction, Organizer, SeatingPlan, Team, + TeamAPIToken, TeamInvite, User, ) from pretix.helpers.dicts import merge_dicts @@ -191,6 +191,24 @@ class GiftCardViewSet(viewsets.ModelViewSet): raise MethodNotAllowed("Gift cards cannot be deleted.") +class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = GiftCardTransactionSerializer + queryset = GiftCardTransaction.objects.none() + permission = 'can_manage_gift_cards' + write_permission = 'can_manage_gift_cards' + + @cached_property + def giftcard(self): + if self.request.GET.get('include_accepted') == 'true': + qs = self.request.organizer.accepted_gift_cards + else: + qs = self.request.organizer.issued_gift_cards.all() + return get_object_or_404(qs, pk=self.kwargs.get('giftcard')) + + def get_queryset(self): + return self.giftcard.transactions.select_related('order', 'order__event') + + class TeamViewSet(viewsets.ModelViewSet): serializer_class = TeamSerializer queryset = Team.objects.none() diff --git a/src/tests/api/test_giftcards.py b/src/tests/api/test_giftcards.py index 527ab52531..331516ccba 100644 --- a/src/tests/api/test_giftcards.py +++ b/src/tests/api/test_giftcards.py @@ -186,3 +186,26 @@ def test_giftcard_no_deletion(token_client, organizer, event, giftcard): '/api/v1/organizers/{}/giftcards/{}/'.format(organizer.slug, giftcard.pk), ) assert resp.status_code == 405 + + +@pytest.mark.django_db +def test_giftcard_transactions(token_client, organizer, giftcard): + resp = token_client.get( + '/api/v1/organizers/{}/giftcards/{}/transactions/'.format(organizer.slug, giftcard.pk), + ) + assert resp.status_code == 200 + assert resp.data == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "id": giftcard.transactions.first().pk, + "datetime": giftcard.transactions.first().datetime.isoformat().replace("+00:00", "Z"), + "value": "23.00", + "event": None, + "order": None, + "text": None + } + ] + } diff --git a/src/tests/api/test_permissions.py b/src/tests/api/test_permissions.py index 94a43a526d..f44c49e249 100644 --- a/src/tests/api/test_permissions.py +++ b/src/tests/api/test_permissions.py @@ -148,6 +148,8 @@ org_permission_sub_urls = [ ('get', 'can_manage_gift_cards', 'giftcards/1/', 404), ('put', 'can_manage_gift_cards', 'giftcards/1/', 404), ('patch', 'can_manage_gift_cards', 'giftcards/1/', 404), + ('get', 'can_manage_gift_cards', 'giftcards/1/transactions/', 404), + ('get', 'can_manage_gift_cards', 'giftcards/1/transactions/1/', 404), ('get', 'can_change_organizer_settings', 'devices/', 200), ('post', 'can_change_organizer_settings', 'devices/', 400), ('get', 'can_change_organizer_settings', 'devices/1/', 404),