Repository: fossasia/open-event-server

Issue:
Add checks on creating/editing discount codes
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
Currently their is no server side validation of data sent on creating/editing a discount code. 
If we try to enter amount > the ticket price, it is accepted.
If we try to enter % greater than 100, it is accepted

**To Reproduce**
Steps to reproduce the behavior:
1. Go to `Manage Events`
2. Click on `Tickets`
3. Scroll down to `Discount Codes` and try creating discount code with wrong data
4. See error

**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
A check should be implemented on -
 - Discount Amount
 - Discount %

**Additional context**
<!-- Add any other context about the problem here. -->
Working on it



Discussion:

Resolving patch:
diff --git a/app/api/schema/discount_codes.py b/app/api/schema/discount_codes.py
--- a/app/api/schema/discount_codes.py
+++ b/app/api/schema/discount_codes.py
@@ -8,6 +8,7 @@
 from app.api.helpers.utilities import dasherize
 from app.api.schema.base import SoftDeletionSchema
 from app.models.discount_code import DiscountCode
+from app.models.ticket import Ticket
 
 
 class DiscountCodeSchemaPublic(SoftDeletionSchema):
@@ -153,6 +154,55 @@ def validate_quantity(self, data, original_data):
                 raise UnprocessableEntity({'pointer': '/data/attributes/tickets-number'},
                                           "tickets-number should be greater than max-quantity")
 
+    @validates_schema(pass_original=True)
+    def validate_value(self, data, original_data):
+        if 'id' in original_data['data']:
+            try:
+                discount_code = DiscountCode.query.filter_by(id=original_data['data']['id']).one()
+            except NoResultFound:
+                raise ObjectNotFound({'parameter': '{code}'}, "DiscountCode: not found")
+
+            if 'type' not in data:
+                data['type'] = discount_code.type
+
+            if 'value' not in data:
+                data['value'] = discount_code.value
+
+        if data['type'] == "percent":
+            if 'tickets' in data:
+                for ticket in data['tickets']:
+                    ticket_object = Ticket.query.filter_by(id=ticket).one()
+                    if not ticket_object.price:
+                        raise UnprocessableEntity(
+                            {'pointer': '/data/attributes/tickets'},
+                            "discount code cannot be applied on free tickets"
+                        )
+            if data['value'] < 0 or data['value'] > 100:
+                raise UnprocessableEntity(
+                    {'pointer': '/data/attributes/value'},
+                    "discount percent must be within range of 0 and 100"
+                )
+
+        if data['type'] == "amount":
+            if 'tickets' in data:
+                for ticket in data['tickets']:
+                    ticket_object = Ticket.query.filter_by(id=ticket).one()
+                    if not ticket_object.price:
+                        raise UnprocessableEntity(
+                            {'pointer': '/data/attributes/tickets'},
+                            "discount code cannot be applied on free tickets"
+                        )
+                    if ticket_object.price < data['value']:
+                        raise UnprocessableEntity(
+                            {'pointer': '/data/attributes/value'},
+                            "discount amount cannot be more than ticket amount"
+                        )
+            if data['value'] < 0:
+                raise UnprocessableEntity(
+                    {'pointer': '/data/attributes/value'},
+                    "discount amount cannot be less than zero"
+                )
+
     @validates_schema(pass_original=True)
     def validate_date(self, data, original_data):
         if 'id' in original_data['data']:


Test patch:
diff --git a/tests/all/integration/api/validation/test_discount_codes.py b/tests/all/integration/api/validation/test_discount_codes.py
--- a/tests/all/integration/api/validation/test_discount_codes.py
+++ b/tests/all/integration/api/validation/test_discount_codes.py
@@ -4,9 +4,9 @@
 from tests.all.integration.utils import OpenEventTestCase
 from app.api.helpers.exceptions import UnprocessableEntity
 from app.api.schema.discount_codes import DiscountCodeSchemaTicket
-from app.factories.discount_code import DiscountCodeFactory
-from app.models import db
 from app.api.helpers.db import save_to_db
+from app.factories.discount_code import DiscountCodeFactory
+from app.factories.ticket import TicketFactory
 from tests.all.integration.setup_database import Setup
 
 
@@ -64,6 +64,71 @@ def test_quantity_max_gt_tickets_number(self):
         with self.assertRaises(UnprocessableEntity):
             DiscountCodeSchemaTicket.validate_quantity(schema, data, original_data)
 
+    def test_amount_lte_ticket_price(self):
+        """
+        Discount Code Validate Amount Value - Tests if function runs without an exception
+        :return:
+        """
+        with app.test_request_context():
+            ticket = TicketFactory()
+            ticket.price = 100
+            save_to_db(ticket)
+
+            schema = DiscountCodeSchemaTicket()
+            original_data = {
+                'data': {}
+            }
+            data = {
+                'type': 'amount',
+                'value': 70,
+                'tickets': ['1']
+            }
+            DiscountCodeSchemaTicket.validate_value(schema, data, original_data)
+
+    def test_amount_gt_ticket_price(self):
+        """
+        Discount Code Validate Amount Value - Tests if exception is raised when discount value is gt ticket price
+        :return:
+        """
+        with app.test_request_context():
+            ticket = TicketFactory()
+            ticket.price = 100
+            save_to_db(ticket)
+
+            schema = DiscountCodeSchemaTicket()
+            original_data = {
+                'data': {}
+            }
+            data = {
+                'type': 'amount',
+                'value': 150,
+                'tickets': ['1']
+            }
+            with self.assertRaises(UnprocessableEntity):
+                DiscountCodeSchemaTicket.validate_value(schema, data, original_data)
+
+    def test_free_ticket(self):
+        """
+        Discount Code Validate Amount Value - Tests exception when discount code is created for free ticket
+        :return:
+        """
+        with app.test_request_context():
+            ticket = TicketFactory()
+            ticket.price = 0
+            save_to_db(ticket)
+
+            schema = DiscountCodeSchemaTicket()
+            original_data = {
+                'data': {}
+            }
+            data = {
+                'type': 'amount',
+                'value': 150,
+                'tickets': ['1']
+            }
+            with self.assertRaises(UnprocessableEntity):
+                DiscountCodeSchemaTicket.validate_value(schema, data, original_data)
+
     def test_quantity_db_populate(self):
         """
         Discount Code Validate Quantity - Tests if validation works on values stored in db and not given in 'data'
diff --git a/tests/all/unit/api/validation/test_discount_codes.py b/tests/all/unit/api/validation/test_discount_codes.py
new file mode 100644
--- /dev/null
+++ b/tests/all/unit/api/validation/test_discount_codes.py
@@ -0,0 +1,45 @@
+import unittest
+
+from tests.all.integration.utils import OpenEventTestCase
+from app.api.helpers.exceptions import UnprocessableEntity
+from app.api.schema.discount_codes import DiscountCodeSchemaTicket
+
+
+class TestDiscountCodeValidation(OpenEventTestCase):
+
+    def test_percent_value_lte_hundred(self):
+        """
+        Discount Code Validate Percentage Value - Tests if function runs without an exception
+        :return:
+        """
+        schema = DiscountCodeSchemaTicket()
+        original_data = {
+            'data': {}
+        }
+        data = {
+            'type': 'percent',
+            'value': 90,
+            'tickets': []
+        }
+        DiscountCodeSchemaTicket.validate_value(schema, data, original_data)
+
+    def test_percent_value_gt_hundred(self):
+        """
+        Discount Code Validate Percentage Value - Tests if exception is raised when percentage value is greater than 100
+        :return:
+        """
+        schema = DiscountCodeSchemaTicket()
+        original_data = {
+            'data': {}
+        }
+        data = {
+            'type': 'percent',
+            'value': 110,
+            'tickets': []
+        }
+        with self.assertRaises(UnprocessableEntity):
+            DiscountCodeSchemaTicket.validate_value(schema, data, original_data)
+
+
+if __name__ == '__main__':
+    unittest.main()