diff --git a/requirements-dev.txt b/requirements-dev.txt index 01b58ef8..6377e8b5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,4 @@ pycodestyle==2.5.0 coveralls==1.8.2 responses==0.10.6 six==1.12.0 -awscli==1.16.237 +awscli==1.16.238 diff --git a/requirements.txt b/requirements.txt index 0c33f6b7..a503721a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,9 +3,9 @@ djangorestframework==3.10.3 django-cors-headers==3.1.0 django-filter==2.2.0 coreapi==2.3.3 -factory-boy==2.11.1 +factory-boy==2.12.0 Markdown==2.6.11 -Pygments==2.3.1 +Pygments==2.4.2 django-anymail==6.1.0 Pillow==6.1.0 django-simple-history==2.7.3 diff --git a/store/tests/tests_viewset_Order.py b/store/tests/tests_viewset_Order.py index 9b791e56..4a35c640 100644 --- a/store/tests/tests_viewset_Order.py +++ b/store/tests/tests_viewset_Order.py @@ -184,6 +184,17 @@ def setUp(self): reserved_seats=1, has_shared_rooms=True, ) + + self.option_retreat: OptionProduct = OptionProduct.objects.create( + name="Vegan", + details="Vegan details", + available=True, + price=50, + max_quantity=10 + ) + self.option_retreat.available_on_products.add(self.retreat) + self.option_retreat.save() + self.retreat_no_seats = Retreat.objects.create( name="no_place_left_retreat", seats=0, @@ -2489,10 +2500,17 @@ def test_validate_coupon(self): 'content_type': 'timeslot', 'object_id': self.time_slot.id, 'quantity': 1, + 'options': [] }, { 'content_type': 'retreat', 'object_id': self.retreat.id, 'quantity': 1, + 'options': [ + { + 'id': self.option_retreat.id, + 'quantity': 2 + } + ] }], 'coupon': "ABCD1234", } diff --git a/store/views.py b/store/views.py index 9a8b262b..192e2fa8 100644 --- a/store/views.py +++ b/store/views.py @@ -4,6 +4,7 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType +from django.db import transaction from django.http import Http404, HttpResponse, HttpRequest from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -20,7 +21,7 @@ from .exceptions import PaymentAPIError from .models import (Package, Membership, Order, OrderLine, PaymentProfile, CustomPayment, Coupon, CouponUser, Refund, BaseProduct, - OptionProduct) + OptionProduct, OrderLineBaseProduct) from .permissions import IsOwner from .resources import (MembershipResource, PackageResource, OrderResource, OrderLineResource, CustomPaymentResource, @@ -259,6 +260,7 @@ class OrderViewSet(ExportMixin, viewsets.ModelViewSet): export_resource = OrderResource() + @transaction.atomic @action( methods=['post'], detail=False, permission_classes=[IsAuthenticated]) def validate_coupon(self, request, pk=None): @@ -269,57 +271,77 @@ def validate_coupon(self, request, pk=None): This is not the best way to do it and it could be improved. """ - serializer = serializers.OrderSerializer( - data=request.data, - context={'request': request} - ) - serializer.is_valid(raise_exception=True) - # Coupon is necessary for this view - if not serializer.validated_data.get('coupon'): - error = { - 'coupon': [_("This field is required.")] - } - return Response(error, status=status.HTTP_400_BAD_REQUEST) - orderlines = serializer.validated_data.pop('order_lines', None) - coupon = serializer.validated_data.pop('coupon', None) - serializer.validated_data.pop('payment_token', None) - serializer.validated_data.pop('single_use_token', None) - order = Order( - **serializer.validated_data, - transaction_date=timezone.now(), - user=request.user, - ) - order.save() - - orderline_list = [] - for idx, orderline in enumerate(orderlines): - orderline_list.append( - OrderLine( - **orderline, - order=order, - ) + try: + sid = transaction.savepoint() + serializer = serializers.OrderSerializer( + data=request.data, + context={'request': request} ) - orderline_list[idx].save() - - response = validate_coupon_for_order(coupon, order) - response['orderline'] = serializers.OrderLineSerializerNoOrder( - response['orderline'], - context={'request': request} - ).data - response['orderline'].pop('url', None) - response['orderline'].pop('id', None) - response['orderline'].pop('order', None) - response['orderline'].pop('coupon', None) - response['orderline'].pop('coupon_real_value', None) - response['orderline'].pop('cost', None) - order.delete() - for orderline in orderline_list: - orderline.delete() - if response['valid_use']: + serializer.is_valid(raise_exception=True) + # Coupon is necessary for this view + if not serializer.validated_data.get('coupon'): + error = { + 'coupon': [_("This field is required.")] + } + return Response(error, status=status.HTTP_400_BAD_REQUEST) + orderlines = serializer.validated_data.pop('order_lines', None) + coupon = serializer.validated_data.pop('coupon', None) + serializer.validated_data.pop('payment_token', None) + serializer.validated_data.pop('single_use_token', None) + order = Order( + **serializer.validated_data, + transaction_date=timezone.now(), + user=request.user, + ) + order.save() + + orderline_list = [] + for idx, orderline in enumerate(orderlines): + if 'options' in orderline.keys(): + options = orderline.pop('options') + orderline_list.append( + OrderLine( + **orderline, + order=order, + ) + ) + + orderline_list[idx].save() + + if 'options' in orderline.keys(): + for option in options: + OrderLineBaseProduct.objects.create( + order_line=orderline_list[idx], + option_id=option.get('id'), + quantity=option.get('quantity'), + ) + orderline_list[idx].save() + + response = validate_coupon_for_order(coupon, order) + response['orderline'] = serializers.OrderLineSerializerNoOrder( + response['orderline'], + context={'request': request} + ).data + + except Exception as err: + raise err + finally: + # add rollback in finally, so it always rollback the data + transaction.savepoint_rollback(sid) + + if response.get('valid_use', False): + response['orderline'].pop('url', None) + response['orderline'].pop('id', None) + response['orderline'].pop('order', None) + response['orderline'].pop('coupon', None) + response['orderline'].pop('coupon_real_value', None) + response['orderline'].pop('cost', None) response.pop('valid_use', None) response.pop('error', None) return Response(response) - return Response(response['error'], status=status.HTTP_400_BAD_REQUEST) + + return Response(response['error'], + status=status.HTTP_400_BAD_REQUEST) def get_queryset(self): """