From f244088391fbb4197b200f93b462f13e4e8fd425 Mon Sep 17 00:00:00 2001 From: earlinn Date: Thu, 11 Jan 2024 11:39:30 +0600 Subject: [PATCH 1/5] Make successful_pay endpoint for online-payment results page, changed success and cancel urls during Stripe Checkout Session creation, fix order and shoppingcart endpoints so that they responses contain serialized data only, fix descriptions and responses in api docs --- backend/api/orders_serializers.py | 33 ++++ backend/api/orders_views.py | 243 ++++++++++++++++++++---------- backend/payments/urls.py | 4 +- 3 files changed, 198 insertions(+), 82 deletions(-) diff --git a/backend/api/orders_serializers.py b/backend/api/orders_serializers.py index 4194f96..2161f09 100644 --- a/backend/api/orders_serializers.py +++ b/backend/api/orders_serializers.py @@ -291,3 +291,36 @@ class StripeCheckoutSessionCreateSerializer(serializers.ModelSerializer): class Meta: model = Order fields = tuple() + + +class StripeSessionCreateSerializer(serializers.Serializer): + """Serializer to show Stripe Checkout Session URL.""" + + checkout_session_url = serializers.URLField() + + +class StripePaySuccessPageSerializer(serializers.Serializer): + """Serializer to get the order number from Stripe Checkout Session after payment.""" + + stripe_session_id = serializers.CharField() + order_id = serializers.CharField(read_only=True) + order_number = serializers.CharField(read_only=True) + + +class StripeError500Serializer(serializers.Serializer): + """Serializer for errors during Stripe payments.""" + + message = serializers.CharField() + errors = serializers.CharField() + + +class CustomSuccessSerializer(serializers.Serializer): + """Serializer for custom success messages.""" + + message = serializers.CharField() + + +class CustomErrorSerializer(serializers.Serializer): + """Serializer for custom errors.""" + + errors = serializers.CharField() diff --git a/backend/api/orders_views.py b/backend/api/orders_views.py index 62c8bea..9940e8c 100644 --- a/backend/api/orders_views.py +++ b/backend/api/orders_views.py @@ -22,6 +22,8 @@ from .mixins import MESSAGE_ON_DELETE, DestroyWithPayloadMixin from .orders_serializers import ( + CustomErrorSerializer, + CustomSuccessSerializer, OrderCreateAnonSerializer, OrderCreateAuthSerializer, OrderGetAnonSerializer, @@ -29,6 +31,9 @@ ShoppingCartListSerializer, ShoppingCartSerializer, StripeCheckoutSessionCreateSerializer, + StripeError500Serializer, + StripePaySuccessPageSerializer, + StripeSessionCreateSerializer, ) from .products_serializers import CouponSerializer from .products_views import STATUS_200_RESPONSE_ON_DELETE_IN_DOCS @@ -50,18 +55,23 @@ DELIVERY_ERROR_MESSAGE = "Отмена заказа после комплектования невозможна." PAY_SOMEONE_ELSE_ORDER_ERROR_MESSAGE = "Заказ №{pk} не принадлежит пользователю {user}." PAY_ALREADY_PAID_ORDER_ERROR_MESSAGE = "Заказ №{pk} уже был оплачен." -STRIPE_SESSION_CREATE_ERROR_MESSAGE = ( - "Что-то пошло не так при создании Stripe Checkout Session." +STRIPE_SESSION_CREATE_ERROR_MESSAGE = "Ошибка создания Stripe Checkout Session." +STRIPE_INVALID_SESSION_ID_ERROR_MESSAGE = ( + "Stripe Checkout Session c таким session_id не обнаружена." ) +STRIPE_API_KEY_ERROR_MESSAGE = "Неверный Stripe API key." SHOP_CART_CLEAR_MESSAGE = "Ваша корзина очищена, все товары из нее удалены." COUPON_ERROR_MESSAGE = "Промокод {code} недействителен." +# TODO: check that API endpoints contain serialized data (i.e. serializer(payload).data) +# inside Response objects, and not Python dictionaries + + @method_decorator( name="list", decorator=swagger_auto_schema( operation_summary="Retrieve a shopping cart", - operation_description="Returns a shopping cart of a user via session", responses={200: ShoppingCartListSerializer}, ), ) @@ -69,10 +79,6 @@ name="create", decorator=swagger_auto_schema( operation_summary="Post and edit products in a shopping cart", - operation_description=( - "Adds new products to the shopping cart or edits the number of products " - "already in the shopping cart (zero is not allowed)" - ), responses={ 201: ShoppingCartListSerializer, 400: ValidationErrorResponseSerializer, @@ -83,8 +89,25 @@ name="destroy", decorator=swagger_auto_schema( operation_summary="Remove product from shopping cart", - operation_description="Removes a product from the shopping cart using its id", - responses={200: ShoppingCartListSerializer, 404: ErrorResponse404Serializer}, + responses={200: ShoppingCartListSerializer, 404: CustomErrorSerializer}, + ), +) +@method_decorator( + name="remove_all", + decorator=swagger_auto_schema( + operation_summary="Clear shopping cart", + responses={200: CustomSuccessSerializer, 400: CustomErrorSerializer}, + ), +) +@method_decorator( + name="coupon_apply", + decorator=swagger_auto_schema( + operation_summary="Apply promocode", + responses={ + 201: CouponSerializer, + 400: CustomErrorSerializer, + 403: CustomErrorSerializer, + }, ), ) class ShoppingCartViewSet( @@ -107,54 +130,40 @@ def get_serializer_class(self): return CouponSerializer return ShoppingCartSerializer - @swagger_auto_schema( - method="delete", - operation_summary="Clear shopping cart", - operation_description=( - "Deletes a product from a user's favorites (authorized user only)" - ), - responses={ - 200: '{"message": ' + SHOP_CART_CLEAR_MESSAGE + "}", - 400: '{"errors": ' + SHOP_CART_ERROR + "}", - }, - ) # TODO: test this endpoint @transaction.atomic @action(detail=False, methods=["delete"], permission_classes=[permissions.AllowAny]) def remove_all(self, request): + """ + Deletes all the products from a user's shopping cart + (both anonymous and authorized). + """ shopping_cart = ShopCart(request) if not shopping_cart: logger.error(SHOP_CART_ERROR) return Response( - {"errors": SHOP_CART_ERROR}, + CustomErrorSerializer({"errors": SHOP_CART_ERROR}).data, status=status.HTTP_400_BAD_REQUEST, ) if request.session.get("coupon_id"): del request.session["coupon_id"] shopping_cart.clear() logger.info(SHOP_CART_CLEAR_MESSAGE) - return Response({"message": SHOP_CART_CLEAR_MESSAGE}, status=status.HTTP_200_OK) + return Response( + CustomSuccessSerializer({"message": SHOP_CART_CLEAR_MESSAGE}).data, + status=status.HTTP_200_OK, + ) # TODO: test this endpoint - @method_decorator( - name="create", - decorator=swagger_auto_schema( - operation_summary="Apply promocode", - responses={ - 201: CouponSerializer, - 403: '{"errors": some error message}', - }, - ), - ) @action(methods=["post"], detail=False) def coupon_apply(self, request): - """Validates the promocode and saves it to the session.""" + """Validates the promocode and saves it to the Django session.""" shopping_cart = ShopCart(request) now = timezone.now() if not shopping_cart: logger.error(SHOP_CART_ERROR) return Response( - {"errors": SHOP_CART_ERROR}, + CustomErrorSerializer({"errors": SHOP_CART_ERROR}).data, status=status.HTTP_400_BAD_REQUEST, ) serializer = self.get_serializer_class() @@ -181,12 +190,14 @@ def coupon_apply(self, request): ) except Coupon.DoesNotExist: request.session["coupon_id"] = None + payload = {"errors": COUPON_ERROR_MESSAGE.format(code=code)} return Response( - {"errors": COUPON_ERROR_MESSAGE.format(code=code)}, + CustomErrorSerializer(payload).data, status=status.HTTP_403_FORBIDDEN, ) def list(self, request, **kwargs): + """Returns a shopping cart of a user via Django session.""" shopping_cart = ShopCart(request) serializer = self.get_serializer_class() payload = { @@ -198,6 +209,10 @@ def list(self, request, **kwargs): return Response(serializer(payload).data, status=status.HTTP_200_OK) def create(self, request, **kwargs): + """ + Adds new products to the shopping cart or edits the number of products + already in the shopping cart (zero is not allowed). + """ if request.session.get("coupon_id"): del request.session["coupon_id"] shopping_cart = ShopCart(request) @@ -218,13 +233,14 @@ def create(self, request, **kwargs): ) def destroy(self, request, **kwargs): + """Removes a product from the shopping cart using its id.""" if request.session.get("coupon_id"): del request.session["coupon_id"] shopping_cart = ShopCart(self.request) if not shopping_cart: logger.error(NO_SHOP_CART_ERROR_MESSAGE) return Response( - {"errors": NO_SHOP_CART_ERROR_MESSAGE}, + CustomErrorSerializer({"errors": NO_SHOP_CART_ERROR_MESSAGE}).data, status=status.HTTP_404_NOT_FOUND, ) product_id = int(self.kwargs["pk"]) @@ -232,7 +248,7 @@ def destroy(self, request, **kwargs): if product_id not in products: logger.error(SHOP_CART_ERROR_MESSAGE) return Response( - {"errors": SHOP_CART_ERROR_MESSAGE}, + CustomErrorSerializer({"errors": SHOP_CART_ERROR_MESSAGE}).data, status=status.HTTP_404_NOT_FOUND, ) shopping_cart.remove(product_id) @@ -252,26 +268,15 @@ def destroy(self, request, **kwargs): name="list", decorator=swagger_auto_schema( operation_summary="List all orders", - operation_description=( - "Returns a list of all the orders of a user (admin or authorized user)" - ), - responses={ - 200: OrderGetAuthSerializer, - 401: METHOD_ERROR_MESSAGE, - 403: ErrorResponse403Serializer, - }, + responses={200: OrderGetAuthSerializer, 401: CustomErrorSerializer}, ), ) @method_decorator( name="retrieve", decorator=swagger_auto_schema( operation_summary="Get order by id", - operation_description=( - "Retrieves an order of a user by its id (admin or authorized user)" - ), responses={ 200: OrderGetAuthSerializer, - 401: ErrorResponse401Serializer, 403: ErrorResponse403Serializer, 404: ErrorResponse404Serializer, }, @@ -281,12 +286,10 @@ def destroy(self, request, **kwargs): name="create", decorator=swagger_auto_schema( operation_summary="Create order", - operation_description="Creates an order of a user (authorized only)", responses={ 201: OrderGetAnonSerializer, 400: ValidationErrorResponseSerializer, - 401: ErrorResponse401Serializer, - 403: ErrorResponse403Serializer, + 404: CustomErrorSerializer, }, ), ) @@ -294,7 +297,6 @@ def destroy(self, request, **kwargs): name="destroy", decorator=swagger_auto_schema( operation_summary="Delete order", - operation_description="Deletes an order by its id (authorized only)", responses={ 200: STATUS_200_RESPONSE_ON_DELETE_IN_DOCS, 401: ErrorResponse401Serializer, @@ -303,6 +305,28 @@ def destroy(self, request, **kwargs): }, ), ) +@method_decorator( + name="pay", + decorator=swagger_auto_schema( + operation_summary="Online payment", + responses={ + 201: StripeSessionCreateSerializer, + 403: CustomErrorSerializer, + 404: ErrorResponse404Serializer, + 500: StripeError500Serializer, + }, + ), +) +@method_decorator( + name="successful_pay", + decorator=swagger_auto_schema( + operation_summary="Get order number after stripe payment", + responses={ + 200: StripePaySuccessPageSerializer, + 500: StripeError500Serializer, + }, + ), +) class OrderViewSet( mixins.ListModelMixin, mixins.CreateModelMixin, @@ -318,6 +342,8 @@ class OrderViewSet( def get_serializer_class(self): if self.action == "pay": return StripeCheckoutSessionCreateSerializer + if self.action == "successful_pay": + return StripePaySuccessPageSerializer if self.request.method in permissions.SAFE_METHODS: if self.request.user.is_authenticated: return OrderGetAuthSerializer @@ -359,17 +385,29 @@ def create_order_data_and_new_address(self, data): return order_data def retrieve(self, request, **kwargs): + """ + Retrieves an order of a user by its id, + anonymous users can only view anonymous orders, + and authorized users can only view their own orders. + """ user = self.request.user order = get_object_or_404(Order, id=self.kwargs.get("pk")) if user.is_anonymous and order.user is not None: - raise PermissionDenied() + return Response( + CustomErrorSerializer({"errors": ORDER_USER_ERROR_MESSAGE}).data, + status=status.HTTP_403_FORBIDDEN, + ) if user.is_authenticated and order.user != user: - return Response({"errors": ORDER_USER_ERROR_MESSAGE}) + return Response( + CustomErrorSerializer({"errors": ORDER_USER_ERROR_MESSAGE}).data, + status=status.HTTP_403_FORBIDDEN, + ) serializer = self.get_serializer(order) logger.info("The user's order was successfully received.") return Response(serializer.data, status=status.HTTP_200_OK) def list(self, request, **kwargs): + """Returns a list of all the orders of an authorized user.""" if self.request.user.is_authenticated: queryset = self.get_queryset().filter(user=self.request.user) serializer = self.get_serializer(queryset, many=True) @@ -377,16 +415,17 @@ def list(self, request, **kwargs): return Response(serializer.data, status=status.HTTP_200_OK) logger.error(METHOD_ERROR_MESSAGE) return Response( - {"errors": METHOD_ERROR_MESSAGE}, + CustomErrorSerializer({"errors": METHOD_ERROR_MESSAGE}).data, status=status.HTTP_401_UNAUTHORIZED, ) def create(self, request, *args, **kwargs): + """Creates an order of a user (both anonymous and authorized).""" shopping_cart = ShopCart(request) if not shopping_cart: logger.error(SHOP_CART_ERROR) return Response( - {"errors": SHOP_CART_ERROR}, + CustomErrorSerializer({"errors": SHOP_CART_ERROR}).data, status=status.HTTP_404_NOT_FOUND, ) shopping_data = { @@ -439,6 +478,7 @@ def create(self, request, *args, **kwargs): return Response(response_serializer.data, status=status.HTTP_201_CREATED) def destroy(self, request, *args, **kwargs): + """Deletes an order by its id (authorized only).""" order_restricted_deletion_statuses = [ Order.COLLECTING, Order.GATHERED, @@ -446,17 +486,29 @@ def destroy(self, request, *args, **kwargs): Order.DELIVERED, Order.COMPLETED, ] + # непонятно, почему для неавторизованного мы ищем заказ по номеру, + # а для авторизованного - по id. Кажется нет разницы, тем более + # теперь номер заказа не совпадает с id, значит искать нужно всегда + # по id заказа if not self.request.user.is_authenticated: order = Order.objects.get(order_number=self.kwargs.get("pk")) + # кажется не нужно давать анониму возможность удалять заказ + # (обсудить с другими в чате), кажется нужен статус 401 для этого случая else: order = get_object_or_404(Order, id=self.kwargs.get("pk")) + + # тут можно делать как ниже - кидать Response с объяснением + # в виде CustomErrorSerializer + # и статус 403, перенести само сообщение наверх в константы. + # и поменять описание ответов в доках апи if order.user != self.request.user: logger.error("PermissionDenied during order creation.") raise PermissionDenied() if order.status in order_restricted_deletion_statuses: logger.error(DELIVERY_ERROR_MESSAGE) - return Response({"errors": DELIVERY_ERROR_MESSAGE}) + # TODO: add CustomErrorSerializer, поменять описание ответов в доках апи + return Response({"errors": DELIVERY_ERROR_MESSAGE}) # TODO: no status code! response_serializer = ( OrderGetAuthSerializer if self.request.user.is_authenticated @@ -469,32 +521,26 @@ def destroy(self, request, *args, **kwargs): return Response(serializer_data, status=status.HTTP_200_OK) # TODO: test this endpoint - @swagger_auto_schema( - method="post", - operation_summary="Online payment", - operation_description=( - "Creates a link for online payment for an order using Stripe" - ), - responses={ - 201: '{"checkout_session_url": some url}', - 403: '{"errors": some error message}', - }, - ) @action(methods=["POST"], detail=True, permission_classes=[permissions.AllowAny]) def pay(self, request, *args, **kwargs): + """Creates a link for online payment for an order using Stripe.""" order = get_object_or_404(Order, id=self.kwargs.get("pk")) if order.user is not None and order.user != self.request.user: + payload = { + "errors": PAY_SOMEONE_ELSE_ORDER_ERROR_MESSAGE.format( + pk=order.pk, user=request.user + ) + } return Response( - { - "errors": PAY_SOMEONE_ELSE_ORDER_ERROR_MESSAGE.format( - pk=order.pk, user=request.user - ) - }, + CustomErrorSerializer(payload).data, status=status.HTTP_403_FORBIDDEN, ) if order.is_paid is True: + payload = { + "errors": PAY_ALREADY_PAID_ORDER_ERROR_MESSAGE.format(pk=order.pk) + } return Response( - {"errors": PAY_ALREADY_PAID_ORDER_ERROR_MESSAGE.format(pk=order.pk)}, + CustomErrorSerializer(payload).data, status=status.HTTP_403_FORBIDDEN, ) stripe.api_key = settings.STRIPE_SECRET_KEY @@ -517,22 +563,59 @@ def pay(self, request, *args, **kwargs): "quantity": 1, } ], - success_url=domain_url + "catalog", - cancel_url=domain_url + "contacts", + success_url=domain_url + + "payment-good?session_id={CHECKOUT_SESSION_ID}", + cancel_url=domain_url + "payment-bad?session_id={CHECKOUT_SESSION_ID}", client_reference_id=request.user.username if request.user.is_authenticated else None, payment_method_types=["card"], mode="payment", - metadata={"order_id": order.id}, + metadata={"order_id": order.id, "order_number": order.order_number}, ) logger.info("Stripe Checkout Session created successfully.") + payload = {"checkout_session_url": checkout_session.url} return Response( - {"checkout_session_url": checkout_session.url}, + StripeSessionCreateSerializer(payload).data, status=status.HTTP_201_CREATED, ) except Exception as e: + payload = {"message": STRIPE_SESSION_CREATE_ERROR_MESSAGE, "errors": str(e)} + return Response( + StripeError500Serializer(payload).data, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + # TODO: test this endpoint + @action(methods=["POST"], detail=False, permission_classes=[permissions.AllowAny]) + def successful_pay(self, request, *args, **kwargs): + """Shows the order id and number from Stripe Checkout Session after payment.""" + try: + stripe.api_key = settings.STRIPE_SECRET_KEY + stripe_session_id = request.data["stripe_session_id"] + session = stripe.checkout.Session.retrieve(stripe_session_id) + payload = { + "stripe_session_id": stripe_session_id, + "order_id": session["metadata"]["order_id"], + "order_number": session["metadata"]["order_number"], + } + return Response( + StripePaySuccessPageSerializer(payload).data, status=status.HTTP_200_OK + ) + except stripe._error.AuthenticationError as e: + logger.error(f"{e}") + payload = {"message": STRIPE_API_KEY_ERROR_MESSAGE, "errors": str(e)} + return Response( + StripeError500Serializer(payload).data, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + except stripe._error.InvalidRequestError as e: + logger.error(f"{e}") + payload = { + "message": STRIPE_INVALID_SESSION_ID_ERROR_MESSAGE, + "errors": str(e), + } return Response( - {"message": STRIPE_SESSION_CREATE_ERROR_MESSAGE, "errors": str(e)}, + StripeError500Serializer(payload).data, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) diff --git a/backend/payments/urls.py b/backend/payments/urls.py index b24ca14..e72c9e3 100644 --- a/backend/payments/urls.py +++ b/backend/payments/urls.py @@ -12,7 +12,7 @@ views.create_checkout_session, name="create-checkout-session", ), - path("success/", views.SuccessView.as_view()), - path("cancel/", views.CancelledView.as_view()), + path("payment-good/", views.SuccessView.as_view()), + path("payment-bad/", views.CancelledView.as_view()), path("webhooks/stripe/", views.stripe_webhook, name="stripe-webhook"), ] From 9403adabee1300eb82ad2c3ce021838265102b73 Mon Sep 17 00:00:00 2001 From: earlinn Date: Thu, 11 Jan 2024 15:07:08 +0600 Subject: [PATCH 2/5] Fix order destroy endpoint, update tests, deploy --- .github/workflows/good_food_workflow.yaml | 2 +- backend/api/orders_views.py | 57 +++++++++++------------ backend/tests/api_tests/test_order.py | 2 +- 3 files changed, 28 insertions(+), 33 deletions(-) diff --git a/.github/workflows/good_food_workflow.yaml b/.github/workflows/good_food_workflow.yaml index d192668..14726b1 100644 --- a/.github/workflows/good_food_workflow.yaml +++ b/.github/workflows/good_food_workflow.yaml @@ -60,7 +60,7 @@ jobs: name: Push Docker image to Docker Hub runs-on: ubuntu-latest needs: flake8_and_tests - if: github.ref == 'refs/heads/main' + # if: github.ref == 'refs/heads/main' steps: - name: Check out the repo uses: actions/checkout@v4 diff --git a/backend/api/orders_views.py b/backend/api/orders_views.py index 9940e8c..5c37152 100644 --- a/backend/api/orders_views.py +++ b/backend/api/orders_views.py @@ -1,13 +1,11 @@ import stripe from django.conf import settings from django.contrib.sites.shortcuts import get_current_site -from django.core.exceptions import PermissionDenied from django.db import transaction from django.db.models import Q from django.utils import timezone from django.utils.decorators import method_decorator from drf_standardized_errors.openapi_serializers import ( - ErrorResponse401Serializer, ErrorResponse403Serializer, ErrorResponse404Serializer, ValidationErrorResponseSerializer, @@ -52,7 +50,12 @@ ) METHOD_ERROR_MESSAGE = "История заказов доступна только авторизованным пользователям." SHOP_CART_ERROR = "В вашей корзине нет товаров, наполните её." -DELIVERY_ERROR_MESSAGE = "Отмена заказа после комплектования невозможна." +DELETE_ORDER_WITH_RESTRICTED_STATUS_ERROR_MESSAGE = ( + "Отмена заказа после начала комплектования невозможна." +) +DELETE_ORDER_BY_ANONYMOUS_ERROR_MESSAGE = ( + "Отмена заказа анонимным пользователем невозможна." +) PAY_SOMEONE_ELSE_ORDER_ERROR_MESSAGE = "Заказ №{pk} не принадлежит пользователю {user}." PAY_ALREADY_PAID_ORDER_ERROR_MESSAGE = "Заказ №{pk} уже был оплачен." STRIPE_SESSION_CREATE_ERROR_MESSAGE = "Ошибка создания Stripe Checkout Session." @@ -64,10 +67,6 @@ COUPON_ERROR_MESSAGE = "Промокод {code} недействителен." -# TODO: check that API endpoints contain serialized data (i.e. serializer(payload).data) -# inside Response objects, and not Python dictionaries - - @method_decorator( name="list", decorator=swagger_auto_schema( @@ -299,8 +298,8 @@ def destroy(self, request, **kwargs): operation_summary="Delete order", responses={ 200: STATUS_200_RESPONSE_ON_DELETE_IN_DOCS, - 401: ErrorResponse401Serializer, - 403: ErrorResponse403Serializer, + 401: CustomErrorSerializer, + 403: CustomErrorSerializer, 404: ErrorResponse404Serializer, }, ), @@ -486,29 +485,25 @@ def destroy(self, request, *args, **kwargs): Order.DELIVERED, Order.COMPLETED, ] - # непонятно, почему для неавторизованного мы ищем заказ по номеру, - # а для авторизованного - по id. Кажется нет разницы, тем более - # теперь номер заказа не совпадает с id, значит искать нужно всегда - # по id заказа - if not self.request.user.is_authenticated: - order = Order.objects.get(order_number=self.kwargs.get("pk")) - # кажется не нужно давать анониму возможность удалять заказ - # (обсудить с другими в чате), кажется нужен статус 401 для этого случая - else: - order = get_object_or_404(Order, id=self.kwargs.get("pk")) - # тут можно делать как ниже - кидать Response с объяснением - # в виде CustomErrorSerializer - # и статус 403, перенести само сообщение наверх в константы. - # и поменять описание ответов в доках апи + if self.request.user.is_anonymous: + payload = {"errors": DELETE_ORDER_BY_ANONYMOUS_ERROR_MESSAGE} + return Response( + CustomErrorSerializer(payload).data, status=status.HTTP_401_UNAUTHORIZED + ) + order = get_object_or_404(Order, id=self.kwargs.get("pk")) if order.user != self.request.user: - logger.error("PermissionDenied during order creation.") - raise PermissionDenied() - + logger.error(ORDER_USER_ERROR_MESSAGE) + payload = {"errors": ORDER_USER_ERROR_MESSAGE} + return Response( + CustomErrorSerializer(payload).data, status=status.HTTP_403_FORBIDDEN + ) if order.status in order_restricted_deletion_statuses: - logger.error(DELIVERY_ERROR_MESSAGE) - # TODO: add CustomErrorSerializer, поменять описание ответов в доках апи - return Response({"errors": DELIVERY_ERROR_MESSAGE}) # TODO: no status code! + logger.error(DELETE_ORDER_WITH_RESTRICTED_STATUS_ERROR_MESSAGE) + payload = {"errors": DELETE_ORDER_WITH_RESTRICTED_STATUS_ERROR_MESSAGE} + return Response( + CustomErrorSerializer(payload).data, status=status.HTTP_403_FORBIDDEN + ) response_serializer = ( OrderGetAuthSerializer if self.request.user.is_authenticated @@ -564,8 +559,8 @@ def pay(self, request, *args, **kwargs): } ], success_url=domain_url - + "payment-good?session_id={CHECKOUT_SESSION_ID}", - cancel_url=domain_url + "payment-bad?session_id={CHECKOUT_SESSION_ID}", + + "payment-results?session_id={CHECKOUT_SESSION_ID}", + cancel_url=domain_url + "cart", client_reference_id=request.user.username if request.user.is_authenticated else None, diff --git a/backend/tests/api_tests/test_order.py b/backend/tests/api_tests/test_order.py index 583c26d..a3e33ad 100644 --- a/backend/tests/api_tests/test_order.py +++ b/backend/tests/api_tests/test_order.py @@ -150,4 +150,4 @@ def test_delete_order_anonimus_client(self, client, products, delivery_points): assert response.status_code == status.HTTP_201_CREATED order = Order.objects.get() response = client.delete(f"/api/order/{order.order_number}/", format="json") - assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.status_code == status.HTTP_401_UNAUTHORIZED From 085e47b6b776c493330d1bea13eacda50446fa84 Mon Sep 17 00:00:00 2001 From: earlinn Date: Thu, 11 Jan 2024 15:22:09 +0600 Subject: [PATCH 3/5] Restore deploy protection --- .github/workflows/good_food_workflow.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/good_food_workflow.yaml b/.github/workflows/good_food_workflow.yaml index 14726b1..d192668 100644 --- a/.github/workflows/good_food_workflow.yaml +++ b/.github/workflows/good_food_workflow.yaml @@ -60,7 +60,7 @@ jobs: name: Push Docker image to Docker Hub runs-on: ubuntu-latest needs: flake8_and_tests - # if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' steps: - name: Check out the repo uses: actions/checkout@v4 From d13d1104d7df872505a275bb1ca14e3baad17340 Mon Sep 17 00:00:00 2001 From: earlinn Date: Thu, 11 Jan 2024 15:59:47 +0600 Subject: [PATCH 4/5] Change stripe checkout session success_url and cancel_url --- .github/workflows/good_food_workflow.yaml | 2 +- backend/api/orders_views.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/good_food_workflow.yaml b/.github/workflows/good_food_workflow.yaml index d192668..14726b1 100644 --- a/.github/workflows/good_food_workflow.yaml +++ b/.github/workflows/good_food_workflow.yaml @@ -60,7 +60,7 @@ jobs: name: Push Docker image to Docker Hub runs-on: ubuntu-latest needs: flake8_and_tests - if: github.ref == 'refs/heads/main' + # if: github.ref == 'refs/heads/main' steps: - name: Check out the repo uses: actions/checkout@v4 diff --git a/backend/api/orders_views.py b/backend/api/orders_views.py index 5c37152..6ca7dce 100644 --- a/backend/api/orders_views.py +++ b/backend/api/orders_views.py @@ -559,8 +559,9 @@ def pay(self, request, *args, **kwargs): } ], success_url=domain_url - + "payment-results?session_id={CHECKOUT_SESSION_ID}", - cancel_url=domain_url + "cart", + + "payment-is-processing?session_id={CHECKOUT_SESSION_ID}", + cancel_url=domain_url + + "payment-cancelled?session_id={CHECKOUT_SESSION_ID}", client_reference_id=request.user.username if request.user.is_authenticated else None, From 518707b6e0c92d1167e0694d9554e0127b7ce46b Mon Sep 17 00:00:00 2001 From: earlinn Date: Thu, 11 Jan 2024 16:13:37 +0600 Subject: [PATCH 5/5] Fix payment urls, restore deploy protection --- .github/workflows/good_food_workflow.yaml | 2 +- backend/payments/urls.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/good_food_workflow.yaml b/.github/workflows/good_food_workflow.yaml index 14726b1..d192668 100644 --- a/.github/workflows/good_food_workflow.yaml +++ b/.github/workflows/good_food_workflow.yaml @@ -60,7 +60,7 @@ jobs: name: Push Docker image to Docker Hub runs-on: ubuntu-latest needs: flake8_and_tests - # if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' steps: - name: Check out the repo uses: actions/checkout@v4 diff --git a/backend/payments/urls.py b/backend/payments/urls.py index e72c9e3..bdc3b35 100644 --- a/backend/payments/urls.py +++ b/backend/payments/urls.py @@ -12,7 +12,7 @@ views.create_checkout_session, name="create-checkout-session", ), - path("payment-good/", views.SuccessView.as_view()), - path("payment-bad/", views.CancelledView.as_view()), + path("payment-is-processing/", views.SuccessView.as_view()), + path("payment-cancelled/", views.CancelledView.as_view()), path("webhooks/stripe/", views.stripe_webhook, name="stripe-webhook"), ]