Repository: fossasia/open-event-server

Issue:
Fix ChargesLayer for Payments
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
The ChargesLayer can be improved with better control flow logic and handling. Also the default pending status of the order needs to be removed.

Parent issue: #4852 


Discussion:

Resolving patch:
diff --git a/app/api/data_layers/ChargesLayer.py b/app/api/data_layers/ChargesLayer.py
--- a/app/api/data_layers/ChargesLayer.py
+++ b/app/api/data_layers/ChargesLayer.py
@@ -1,6 +1,7 @@
 from flask_rest_jsonapi.data_layers.base import BaseDataLayer
+from flask_rest_jsonapi.exceptions import ObjectNotFound
 
-from app.api.helpers.exceptions import UnprocessableEntity
+from app.api.helpers.exceptions import UnprocessableEntity, ConflictException
 from app.api.helpers.ticketing import TicketingManager
 from app.models.order import Order
 
@@ -8,16 +9,37 @@
 class ChargesLayer(BaseDataLayer):
 
     def create_object(self, data, view_kwargs):
+        """
+        create_object method for the Charges layer
+        charge the user using paypal or stripe
+        :param data:
+        :param view_kwargs:
+        :return:
+        """
         order = Order.query.filter_by(id=view_kwargs['id']).first()
+        if not order:
+            raise ObjectNotFound({'parameter': 'id'},
+                                 "Order with id: {} not found".format(view_kwargs['id']))
+        elif order.status == 'cancelled' or order.status == 'expired':
+            raise ConflictException({'parameter': 'id'},
+                                    "You cannot charge payments on a cancelled or expired order")
+        elif (not order.amount) or order.amount == 0:
+            raise ConflictException({'parameter': 'id'},
+                                    "You cannot charge payments on a free order")
+
+        # charge through stripe
         if order.payment_mode == 'stripe':
-            if data.get('stripe') is None:
+            if not data.get('stripe'):
                 raise UnprocessableEntity({'source': ''}, "stripe token is missing")
             success, response = TicketingManager.charge_stripe_order_payment(order, data['stripe'])
             if not success:
                 raise UnprocessableEntity({'source': 'stripe_token_id'}, response)
 
+        # charge through paypal
         elif order.payment_mode == 'paypal':
-            success, response = TicketingManager.charge_paypal_order_payment(order)
+            if not data.get('paypal'):
+                raise UnprocessableEntity({'source': ''}, "paypal token is missing")
+            success, response = TicketingManager.charge_paypal_order_payment(order, data['paypal'])
             if not success:
-                raise UnprocessableEntity({'source': ''}, response)
+                raise UnprocessableEntity({'source': 'paypal'}, response)
         return order
diff --git a/app/api/helpers/ticketing.py b/app/api/helpers/ticketing.py
--- a/app/api/helpers/ticketing.py
+++ b/app/api/helpers/ticketing.py
@@ -72,8 +72,17 @@ def calculate_update_amount(order):
 
     @staticmethod
     def charge_stripe_order_payment(order, token_id):
+        """
+        Charge the user through Stripe
+        :param order: Order for which to charge for
+        :param token_id: Stripe token
+        :return:
+        """
+        # save the stripe token with the order
         order.stripe_token = token_id
         save_to_db(order)
+
+        # charge the user
         charge = StripePaymentsManager.capture_payment(order)
 
         if charge:
@@ -99,8 +108,21 @@ def charge_stripe_order_payment(order, token_id):
             return False, 'Error'
 
     @staticmethod
-    def charge_paypal_order_payment(order):
+    def charge_paypal_order_payment(order, token_id):
+        """
+        Charge the user through paypal.
+        :param order: Order for which to charge for
+        :param token_id: Paypal token
+        :return:
+        """
+        # save the paypal token with the order
+        order.paypal_token = token_id
+        save_to_db(order)
+
+        # get relevant details from Paypal using the token
         payment_details = PayPalPaymentsManager.get_approved_payment_details(order)
+
+        # charge the user
         if 'PAYERID' in payment_details:
             capture_result = PayPalPaymentsManager.capture_payment(order, payment_details['PAYERID'])
             if capture_result['ACK'] == 'Success':
diff --git a/app/api/orders.py b/app/api/orders.py
--- a/app/api/orders.py
+++ b/app/api/orders.py
@@ -1,6 +1,6 @@
 from datetime import datetime
 
-from flask import request, render_template
+from flask import render_template
 from flask_jwt import current_identity as current_user
 from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship
 from marshmallow_jsonapi import fields
@@ -35,7 +35,7 @@ class OrdersListPost(ResourceList):
 
     def before_post(self, args, kwargs, data=None):
         """
-        before post method to check for required relationship and proper permission
+        before post method to check for required relationships and permissions
         :param args:
         :param kwargs:
         :param data:
@@ -150,9 +150,7 @@ def before_get(self, args, kwargs):
         :param kwargs:
         :return:
         """
-        if 'GET' in request.method and kwargs.get('event_id') is None:
-            pass
-        elif not has_access('is_coorganizer', event_id=kwargs['event_id']):
+        if kwargs.get('event_id') and not has_access('is_coorganizer', event_id=kwargs['event_id']):
             raise ForbiddenException({'source': ''}, "Co-Organizer Access Required")
 
     def query(self, view_kwargs):
@@ -189,7 +187,7 @@ def before_get_object(self, view_kwargs):
         order = safe_query(self, Order, 'identifier', view_kwargs['order_identifier'], 'order_identifier')
 
         if not has_access('is_coorganizer_or_user_itself', event_id=order.event_id, user_id=order.user_id):
-            return ForbiddenException({'source': ''}, 'Access Forbidden')
+            return ForbiddenException({'source': ''}, 'You can only access your orders or your event\'s orders')
 
     def before_update_object(self, order, data, view_kwargs):
         """
@@ -274,6 +272,7 @@ class Meta:
 
     id = fields.Str(dump_only=True)
     stripe = fields.Str(allow_none=True)
+    paypal = fields.Str(allow_none=True)
 
 
 class ChargeList(ResourceList):
@@ -287,3 +286,5 @@ class ChargeList(ResourceList):
         'class': ChargesLayer,
         'session': db.session
     }
+
+    decorators = (jwt_required,)


Test patch: