From bcd687764cd00408268bbc67bf9c5afb32489702 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 22 Jan 2020 17:15:40 +0100 Subject: [PATCH] API: Allow to create payments directly --- doc/api/resources/orders.rst | 59 +++++++++++++++++++++++++++++ src/pretix/api/serializers/order.py | 14 +++++++ src/pretix/api/views/order.py | 54 ++++++++++++++++++++++++-- src/tests/api/test_orders.py | 42 ++++++++++++++++++++ 4 files changed, 165 insertions(+), 4 deletions(-) diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index f845ff17f9..d71d16b88b 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -1621,6 +1621,10 @@ Order payment endpoints These endpoints have been added. +.. versionchanged:: 3.6 + + Payments can now be created through the API. + .. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/ Returns a list of all payments for an order. @@ -1829,6 +1833,61 @@ Order payment endpoints :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. :statuscode 404: The requested order or payment does not exist. +.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/ + + Creates a new payment. + + Be careful with the ``info`` parameter: You can pass a nested JSON object that will be set as the internal ``info`` + value of the payment object that will be created. How this value is handled is up to the payment provider and you + should only use this if you know the specific payment provider in detail. Please keep in mind that the payment + provider will not be called to do anything about this (i.e. if you pass a bank account to a debit provider, *no* + charge will be created), this is just informative in case you *handled the payment already*. + + **Example request**: + + .. sourcecode:: http + + POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/payments/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + Content-Type: application/json + + { + "state": "confirmed", + "amount": "23.00", + "payment_date": "2017-12-04T12:13:12Z", + "info": {}, + "provider": "banktransfer" + } + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 Created + Vary: Accept + Content-Type: application/json + + { + "local_id": 1, + "state": "confirmed", + "amount": "23.00", + "created": "2017-12-01T10:00:00Z", + "payment_date": "2017-12-04T12:13:12Z", + "payment_url": null, + "details": {}, + "provider": "banktransfer" + } + + :param organizer: The ``slug`` field of the organizer to access + :param event: The ``slug`` field of the event to access + :param order: The ``code`` field of the order to access + :statuscode 201: no error + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. + :statuscode 404: The requested order does not exist. + Order refund endpoints ---------------------- diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 178ad4789f..577f7870ae 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -1034,6 +1034,20 @@ class InvoiceSerializer(I18nAwareModelSerializer): 'internal_reference') +class OrderPaymentCreateSerializer(I18nAwareModelSerializer): + provider = serializers.CharField(required=True, allow_null=False, allow_blank=False) + info = CompatibleJSONField(required=False) + + class Meta: + model = OrderPayment + fields = ('state', 'amount', 'payment_date', 'provider', 'info') + + def create(self, validated_data): + order = OrderPayment(order=self.context['order'], **validated_data) + order.save() + return order + + class OrderRefundCreateSerializer(I18nAwareModelSerializer): payment = serializers.IntegerField(required=False, allow_null=True) provider = serializers.CharField(required=True, allow_null=False, allow_blank=False) diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 915e8aed5c..4276d2d574 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -23,9 +23,10 @@ from rest_framework.response import Response from pretix.api.models import OAuthAccessToken from pretix.api.serializers.order import ( - InvoiceSerializer, OrderCreateSerializer, OrderPaymentSerializer, - OrderPositionSerializer, OrderRefundCreateSerializer, - OrderRefundSerializer, OrderSerializer, PriceCalcSerializer, + InvoiceSerializer, OrderCreateSerializer, OrderPaymentCreateSerializer, + OrderPaymentSerializer, OrderPositionSerializer, + OrderRefundCreateSerializer, OrderRefundSerializer, OrderSerializer, + PriceCalcSerializer, ) from pretix.base.i18n import language from pretix.base.models import ( @@ -825,17 +826,62 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS raise ValidationError(str(e)) -class PaymentViewSet(viewsets.ReadOnlyModelViewSet): +class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet): serializer_class = OrderPaymentSerializer queryset = OrderPayment.objects.none() permission = 'can_view_orders' write_permission = 'can_change_orders' lookup_field = 'local_id' + def get_serializer_context(self): + ctx = super().get_serializer_context() + ctx['order'] = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event) + return ctx + def get_queryset(self): order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event) return order.payments.all() + def create(self, request, *args, **kwargs): + serializer = OrderPaymentCreateSerializer(data=request.data, context=self.get_serializer_context()) + serializer.is_valid(raise_exception=True) + with transaction.atomic(): + mark_confirmed = False + if serializer.validated_data['state'] == OrderPayment.PAYMENT_STATE_CONFIRMED: + serializer.validated_data['state'] = OrderPayment.PAYMENT_STATE_PENDING + mark_confirmed = True + self.perform_create(serializer) + r = serializer.instance + if mark_confirmed: + try: + r.confirm( + user=self.request.user if self.request.user.is_authenticated else None, + auth=self.request.auth, + count_waitinglist=False, + force=request.data.get('force', False) + ) + except Quota.QuotaExceededException: + pass + except SendMailException: + pass + + serializer = OrderPaymentSerializer(r, context=serializer.context) + + r.order.log_action( + 'pretix.event.order.payment.started', { + 'local_id': r.local_id, + 'provider': r.provider, + }, + user=request.user if request.user.is_authenticated else None, + auth=request.auth + ) + + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def perform_create(self, serializer): + serializer.save() + @action(detail=True, methods=['POST']) def confirm(self, request, **kwargs): payment = self.get_object() diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index b45ab21447..571cf332bd 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -401,6 +401,48 @@ def test_payment_detail(token_client, organizer, event, order): assert TEST_PAYMENTS_RES[0] == resp.data +@pytest.mark.django_db +def test_payment_create_confirmed(token_client, organizer, event, order): + resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/payments/'.format( + organizer.slug, event.slug, order.code + ), format='json', data={ + 'provider': 'banktransfer', + 'state': 'confirmed', + 'amount': order.total, + 'info': { + 'foo': 'bar' + } + }) + with scopes_disabled(): + p = order.payments.last() + assert resp.status_code == 201 + assert p.state == OrderPayment.PAYMENT_STATE_CONFIRMED + assert p.info_data == {'foo': 'bar'} + order.refresh_from_db() + assert order.status == Order.STATUS_PAID + + +@pytest.mark.django_db +def test_payment_create_pending(token_client, organizer, event, order): + resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/payments/'.format( + organizer.slug, event.slug, order.code + ), format='json', data={ + 'provider': 'banktransfer', + 'state': 'pending', + 'amount': order.total, + 'info': { + 'foo': 'bar' + } + }) + with scopes_disabled(): + p = order.payments.last() + assert resp.status_code == 201 + assert p.state == OrderPayment.PAYMENT_STATE_PENDING + assert p.info_data == {'foo': 'bar'} + order.refresh_from_db() + assert order.status == Order.STATUS_PENDING + + @pytest.mark.django_db def test_payment_confirm(token_client, organizer, event, order): resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/payments/2/confirm/'.format(