Repository: fossasia/open-event-server

Issue:
Refactor exception handling in the charges layer
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
The exception handling in the charges layer needs to be refactored. Exceptions need to be raised at proper place and should not catched using a generic exception handler.


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
@@ -42,13 +42,10 @@ def create_object(self, data, view_kwargs):
                 raise UnprocessableEntity({'source': ''}, "stripe token is missing")
             if not order.event.can_pay_by_stripe:
                 raise ConflictException({'': ''}, "This event doesn't accept payments by Stripe")
-            try:
-                success, response = TicketingManager.charge_stripe_order_payment(order, data['stripe'])
-                data['status'] = success
-                data['message'] = response
-            except Exception as e:
-                data['status'] = False
-                data['message'] = str(e)
+
+            success, response = TicketingManager.charge_stripe_order_payment(order, data['stripe'])
+            data['status'] = success
+            data['message'] = response
 
         # charge through paypal
         elif order.payment_mode == 'paypal':
@@ -56,12 +53,9 @@ def create_object(self, data, view_kwargs):
                 raise UnprocessableEntity({'source': ''}, "paypal braintree nonce is missing")
             if not order.event.can_pay_by_paypal:
                 raise ConflictException({'': ''}, "This event doesn't accept payments by Paypal")
-            try:
-                success, response = TicketingManager.charge_paypal_order_payment(order, data['paypal_braintree_nonce'])
-                data['status'] = success
-                data['message'] = response
-            except Exception as e:
-                data['status'] = False
-                data['message'] = str(e)
+
+            success, response = TicketingManager.charge_paypal_order_payment(order, data['paypal_braintree_nonce'])
+            data['status'] = success
+            data['message'] = response
 
         return data
diff --git a/app/api/helpers/payment.py b/app/api/helpers/payment.py
--- a/app/api/helpers/payment.py
+++ b/app/api/helpers/payment.py
@@ -6,7 +6,7 @@
 from forex_python.converter import CurrencyRates
 
 from app.api.helpers.cache import cache
-from app.api.helpers.exceptions import ForbiddenException
+from app.api.helpers.exceptions import ForbiddenException, ConflictException
 from app.api.helpers.utilities import represents_int
 from app.models.stripe_authorization import StripeAuthorization
 from app.settings import get_settings, Environment
@@ -22,6 +22,10 @@ def forex(from_currency, to_currency, amount):
 
 
 class StripePaymentsManager(object):
+    """
+    Class to manage payments through Stripe.
+    """
+
     @staticmethod
     def get_credentials(event=None):
         """
@@ -64,7 +68,7 @@ def get_event_organizer_credentials_from_stripe(stripe_auth_code):
         credentials = StripePaymentsManager.get_credentials()
 
         if not credentials:
-            raise ForbiddenException({'pointer': ''}, "Stripe payment isn't configured properly for this Event")
+            raise ForbiddenException({'pointer': ''}, "Stripe payment isn't configured properly for the Platform")
 
         data = {
             'client_secret': credentials['SECRET_KEY'],
@@ -77,11 +81,19 @@ def get_event_organizer_credentials_from_stripe(stripe_auth_code):
 
     @staticmethod
     def capture_payment(order_invoice, currency=None, credentials=None):
+        """
+        Capture payments through stripe.
+        :param order_invoice: Order to be charged for
+        :param currency: Currency of the order amount.
+        :param credentials: Stripe credentials.
+        :return: charge/None depending on success/failure.
+        """
         if not credentials:
             credentials = StripePaymentsManager.get_credentials(order_invoice.event)
 
         if not credentials:
-            raise Exception('Stripe credentials not found for the event.')
+            raise ConflictException({'pointer': ''},
+                                    'Stripe credentials not found for the event.')
         stripe.api_key = credentials['SECRET_KEY']
 
         if not currency:
@@ -109,13 +121,13 @@ def capture_payment(order_invoice, currency=None, credentials=None):
                 description=order_invoice.event.name
             )
             return charge
-        except:
-            return None
+        except Exception as e:
+            raise ConflictException({'pointer': ''}, str(e))
 
 
 class PayPalPaymentsManager(object):
     """
-    Class to manage payment through Paypal Braintree.
+    Class to manage payments through Paypal Braintree.
     """
 
     @staticmethod
@@ -128,7 +140,7 @@ def get_credentials():
         settings = get_settings()
         paypal_braintree_access_token = settings['paypal_braintree_sandbox_access_token']
 
-        # Switch to production if environment is production.
+        # Switch to production if paypal_mode is production.
         if settings['paypal_mode'] == Environment.PRODUCTION:
             paypal_braintree_access_token = settings['paypal_braintree_access_token']
 
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
@@ -3,6 +3,7 @@
 from flask_login import current_user
 
 from app.api.helpers.db import save_to_db, get_count
+from app.api.helpers.exceptions import ConflictException
 from app.api.helpers.files import make_frontend_url
 from app.api.helpers.mail import send_email_to_attendees
 from app.api.helpers.notification import send_notif_to_attendees, send_notif_ticket_purchase_organizer
@@ -88,7 +89,17 @@ def charge_stripe_order_payment(order, token_id):
         save_to_db(order)
 
         # charge the user
-        charge = StripePaymentsManager.capture_payment(order)
+        try:
+            charge = StripePaymentsManager.capture_payment(order)
+        except ConflictException as e:
+            # payment failed hence expire the order
+            order.status = 'expired'
+            save_to_db(order)
+
+            # delete related attendees to unlock the tickets
+            delete_related_attendees_for_order(order)
+
+            raise e
 
         # charge.paid is true if the charge succeeded, or was successfully authorized for later capture.
         if charge.paid:
@@ -135,9 +146,6 @@ def charge_paypal_order_payment(order, paypal_nonce):
         :param paypal_nonce: Paypal token
         :return:
         """
-        if not paypal_nonce:
-            return False, 'Paypal nonce missing'
-
         # save the paypal nonce with the order
         order.paypal_token = paypal_nonce
         save_to_db(order)


Test patch: