Repository: fossasia/open-event-server

Issue:
Integrate onsite attendee feature.
**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 [...] -->
Orga app should be allowed to create orders without specifying the attendees. It can be achieved in the following way:
1. The POST request should be made with query param `?onsite=True`.
2. The payload should contain an additional attribute 'on-site-tickets' with info about the id and quantity of each ticket purchased.
3. The server will generate the attendees with firstName = "onsite" and lastName = "attendee".



Discussion:

Resolving patch:
diff --git a/app/api/helpers/order.py b/app/api/helpers/order.py
--- a/app/api/helpers/order.py
+++ b/app/api/helpers/order.py
@@ -4,10 +4,13 @@
 from flask import render_template
 
 from app.api.helpers import ticketing
-from app.api.helpers.db import save_to_db
+from app.api.helpers.db import save_to_db, safe_query_without_soft_deleted_entries, get_count
+from app.api.helpers.exceptions import UnprocessableEntity, ConflictException
 from app.api.helpers.files import create_save_pdf
 from app.api.helpers.storage import UPLOAD_PATHS
 from app.models import db
+from app.models.ticket import Ticket
+from app.models.ticket_holder import TicketHolder
 
 
 def delete_related_attendees_for_order(order):
@@ -66,3 +69,54 @@ def create_pdf_tickets_for_holder(order):
             save_to_db(holder)
 
         save_to_db(order)
+
+
+def create_onsite_attendees_for_order(data):
+    """
+    Creates on site ticket holders for an order and adds it into the request data.
+    :param data: data initially passed in the POST request for order.
+    :return:
+    """
+    on_site_tickets = data.get('on_site_tickets')
+
+    if not on_site_tickets:
+        raise UnprocessableEntity({'pointer': 'data/attributes/on_site_tickets'}, 'on_site_tickets info missing')
+
+    data['ticket_holders'] = []
+
+    for on_site_ticket in on_site_tickets:
+        ticket_id = on_site_ticket['id']
+        quantity = int(on_site_ticket['quantity'])
+
+        ticket = safe_query_without_soft_deleted_entries(db, Ticket, 'id', ticket_id, 'ticket_id')
+
+        ticket_sold_count = get_count(db.session.query(TicketHolder.id).
+                                      filter_by(ticket_id=int(ticket.id), deleted_at=None))
+
+        # Check if the ticket is already sold out or not.
+        if ticket_sold_count + quantity > ticket.quantity:
+            # delete the already created attendees.
+            for holder in data['ticket_holders']:
+                ticket_holder = db.session.query(TicketHolder).filter(id == int(holder)).one()
+                db.session.delete(ticket_holder)
+                try:
+                    db.session.commit()
+                except Exception as e:
+                    logging.error('DB Exception! %s' % e)
+                    db.session.rollback()
+
+            raise ConflictException(
+                {'pointer': '/data/attributes/on_site_tickets'},
+                "Ticket with id: {} already sold out. You can buy at most {} tickets".format(ticket_id,
+                                                                                             ticket.quantity -
+                                                                                             ticket_sold_count)
+            )
+
+        for _ in range(1, quantity):
+            ticket_holder = TicketHolder(firstname='onsite', lastname='attendee', email='example@example.com',
+                                         ticket_id=ticket.id, event_id=data.get('event'))
+            save_to_db(ticket_holder)
+            data['ticket_holders'].append(ticket_holder.id)
+
+    # delete from the data.
+    del data['on_site_tickets']
diff --git a/app/api/orders.py b/app/api/orders.py
--- a/app/api/orders.py
+++ b/app/api/orders.py
@@ -18,7 +18,7 @@
 from app.api.helpers.notification import send_notif_to_attendees, send_notif_ticket_purchase_organizer, \
     send_notif_ticket_cancel
 from app.api.helpers.order import delete_related_attendees_for_order, set_expiry_for_order, \
-    create_pdf_tickets_for_holder
+    create_pdf_tickets_for_holder, create_onsite_attendees_for_order
 from app.api.helpers.payment import PayPalPaymentsManager
 from app.api.helpers.permission_manager import has_access
 from app.api.helpers.permissions import jwt_required
@@ -48,7 +48,15 @@ def before_post(self, args, kwargs, data=None):
         :param data:
         :return:
         """
-        require_relationship(['event', 'ticket_holders'], data)
+        require_relationship(['event'], data)
+
+        # Create on site attendees.
+        if request.args.get('onsite', False):
+            create_onsite_attendees_for_order(data)
+        elif data.get('on_site_tickets'):
+            del data['on_site_tickets']
+        require_relationship(['ticket_holders'], data)
+
         # Ensuring that default status is always pending, unless the user is event co-organizer
         if not has_access('is_coorganizer', event_id=data['event']):
             data['status'] = 'pending'
diff --git a/app/api/schema/orders.py b/app/api/schema/orders.py
--- a/app/api/schema/orders.py
+++ b/app/api/schema/orders.py
@@ -9,6 +9,15 @@
 from utils.common import use_defaults
 
 
+class OnSiteTicketSchema(SoftDeletionSchema):
+    class Meta:
+        type_ = 'on-site-ticket'
+        inflect = dasherize
+
+    id = fields.Str(load_only=True, required=True)
+    quantity = fields.Str(load_only=True, required=True)
+
+
 @use_defaults()
 class OrderSchema(SoftDeletionSchema):
     class Meta:
@@ -61,6 +70,9 @@ def initial_values(self, data):
     order_notes = fields.Str(allow_none=True)
     tickets_pdf_url = fields.Url(dump_only=True)
 
+    # only used in the case of an on site attendee.
+    on_site_tickets = fields.List(cls_or_instance=fields.Nested(OnSiteTicketSchema), load_only=True, allow_none=True)
+
     attendees = Relationship(attribute='ticket_holders',
                              self_view='v1.order_attendee',
                              self_view_kwargs={'order_identifier': '<identifier>'},


Test patch:
diff --git a/tests/hook_main.py b/tests/hook_main.py
--- a/tests/hook_main.py
+++ b/tests/hook_main.py
@@ -3864,6 +3864,16 @@ def create_order(transaction):
     transaction['skip'] = True
 
 
+@hooks.before("Orders > Create Order with on site Attendees > Create Order with on site Attendees")
+def create_order_with_on_site_attendee(transaction):
+    """
+    GET /orders?onsite=true
+    :param transaction:
+    :return:
+    """
+    transaction['skip'] = True
+
+
 @hooks.before("Orders > Order Detail > Get Order Detail")
 def order_detail(transaction):
     """