Repository: fossasia/open-event-server

Issue:
Discount code quantity calculation logic is completely wrong
https://github.com/fossasia/open-event-server/blob/1aebc2fcf6910a48ea3d7dcc0f632639c831a466/app/api/helpers/ticketing.py#L35-L41

This just calculates all the bought tickets, and not the tickets bought with discount code. God knows how it was supposed to work.

Secondly, discount code should be redesigned as it is confusing, it is applicable to individual ticket instead of entire order, but is saved with the order instead. This needs to be discussed and decided later on as it is impossible to check which tickets the discount code was applied to in the order


Discussion:

Resolving patch:
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
@@ -32,18 +32,16 @@ def get_order_expiry():
     def match_discount_quantity(discount_code, tickets=None, ticket_holders=None):
         qty = 0
         ticket_ids = [ticket.id for ticket in discount_code.tickets]
-        old_holders = get_count(
-            TicketHolder.query.filter(
-                TicketHolder.ticket_id.in_(ticket_ids)  # pytype: disable=attribute-error
-            )
-            .join(Order)
-            .filter(Order.status.in_(['completed', 'placed']))
-        )
+        old_holders = discount_code.confirmed_attendees_count
         if ticket_holders:
-            for holder in ticket_holders:
-                ticket_holder = TicketHolder.query.filter_by(id=holder).one()
-                if ticket_holder.ticket.id in ticket_ids:
-                    qty += 1
+            # pytype: disable=attribute-error
+            qty = get_count(
+                TicketHolder.query.filter(
+                    TicketHolder.id.in_(ticket_holders),
+                    TicketHolder.ticket_id.in_(ticket_ids),
+                )
+            )
+            # pytype: enable=attribute-error
         elif tickets:
             for ticket in tickets:
                 if int(ticket['id']) in ticket_ids:
diff --git a/app/factories/discount_code.py b/app/factories/discount_code.py
--- a/app/factories/discount_code.py
+++ b/app/factories/discount_code.py
@@ -41,7 +41,7 @@ class Meta:
     type = "amount"
     is_active = True
     tickets_number = 30
-    min_quantity = 10
+    min_quantity = 1
     max_quantity = 20
     valid_from = common.date_
     valid_till = common.dateEnd_
diff --git a/app/models/discount_code.py b/app/models/discount_code.py
--- a/app/models/discount_code.py
+++ b/app/models/discount_code.py
@@ -1,7 +1,10 @@
 from datetime import datetime
 from sqlalchemy.sql import func
+from app.api.helpers.db import get_count
 from app.models import db
 from app.models.base import SoftDeletionModel
+from app.models.order import Order
+from app.models.ticket_holder import TicketHolder
 
 TICKET = 'ticket'
 EVENT = 'event'
@@ -75,6 +78,22 @@ def __repr__(self):
     def __str__(self):
         return self.__repr__()
 
+    def get_confirmed_attendees_query(self):
+        return (
+            TicketHolder.query.filter_by(deleted_at=None)
+            .join(Order)
+            .filter_by(discount_code_id=self.id, deleted_at=None)
+            .filter(Order.status.in_(['completed', 'placed']))
+        )
+
+    @property
+    def confirmed_attendees(self):
+        return self.get_confirmed_attendees_query().all()
+
+    @property
+    def confirmed_attendees_count(self):
+        return get_count(self.get_confirmed_attendees_query())
+
     @property
     def serialize(self):
         """Return object data in easily serializable format"""


Test patch:
diff --git a/tests/all/integration/api/helpers/test_ticketing.py b/tests/all/integration/api/helpers/test_ticketing.py
new file mode 100644
--- /dev/null
+++ b/tests/all/integration/api/helpers/test_ticketing.py
@@ -0,0 +1,51 @@
+from app.api.helpers.ticketing import TicketingManager
+from app.factories.attendee import AttendeeFactoryBase
+from app.factories.order import OrderFactory
+from app.factories.ticket import TicketFactory
+from app.factories.discount_code import DiscountCodeTicketFactory
+from app.models import db
+
+from tests.all.integration.utils import OpenEventTestCase
+
+
+class TestTicketing(OpenEventTestCase):
+    def test_match_discount_quantity(self):
+        """Method to test the quantity calculation of discount code"""
+
+        with self.app.test_request_context():
+            ticket = TicketFactory()
+            discount_code = DiscountCodeTicketFactory(tickets_number=5)
+            discount_code.tickets.append(ticket)
+
+            order_without_discount = OrderFactory(status='completed')
+
+            db.session.commit()
+
+            # Attendees associated with the order without discount code should not be counted
+            AttendeeFactoryBase.create_batch(
+                10, order_id=order_without_discount.id, ticket_id=ticket.id
+            )
+
+            self.assertTrue(
+                TicketingManager.match_discount_quantity(
+                    discount_code, ticket_holders=[1]
+                )
+            )
+
+            order_with_discount = OrderFactory(
+                status='completed', discount_code_id=discount_code.id
+            )
+
+            db.session.commit()
+
+            # Attendees associated with the order with discount code should be counted
+            AttendeeFactoryBase.create_batch(
+                5, order_id=order_with_discount.id, ticket_id=ticket.id
+            )
+
+            self.assertFalse(
+                TicketingManager.match_discount_quantity(
+                    discount_code, ticket_holders=[1]
+                )
+            )
+            self.assertEqual(5, discount_code.confirmed_attendees_count)