Repository: fossasia/open-event-server

Issue:
API for order creation with attendee info using a single endpoint
**Is your feature request related to a problem? Please describe.**
Parent Issue - https://github.com/fossasia/open-event-server/issues/6302
API for order creation with attendee info using a single endpoint. It should not be needed to hit 30 endpoints to create an order

**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
On it



Discussion:
The first part would be to take in ticket info and quantity with optional discount code and return calculated price info with tax, discount code, access code calculation. Please create a sub-issue on it and first work on this
Ok. On it
@iamareebjamal Next step?
Design API to create an order. Will take same input as the price calculation API, along with attendee information as well and will create an order

Resolving patch:
diff --git a/app/api/custom/orders.py b/app/api/custom/orders.py
--- a/app/api/custom/orders.py
+++ b/app/api/custom/orders.py
@@ -1,21 +1,23 @@
-from flask import Blueprint, jsonify, request
+from flask import Blueprint, jsonify, request, make_response
 from flask_jwt_extended import current_user, jwt_required
 from sqlalchemy.orm.exc import NoResultFound
 
-
 from app.models import db
 from app.api.auth import return_file
-from app.api.helpers.db import safe_query
+from app.api.helpers.db import safe_query, get_count
 from app.api.helpers.mail import send_email_to_attendees
 from app.api.helpers.errors import ForbiddenError, UnprocessableEntityError, NotFoundError
 from app.api.helpers.order import calculate_order_amount, create_pdf_tickets_for_holder
 from app.api.helpers.storage import UPLOAD_PATHS
 from app.api.helpers.storage import generate_hash
-from app.api.helpers.ticketing import TicketingManager
+from app.api.schema.attendees import AttendeeSchema
+from app.api.schema.orders import OrderSchema
 from app.api.helpers.permission_manager import has_access
 from app.extensions.limiter import limiter
 from app.models.discount_code import DiscountCode
-from app.models.order import Order
+from app.models.order import Order, OrderTicket
+from app.models.ticket import Ticket
+from app.models.ticket_holder import TicketHolder
 
 order_blueprint = Blueprint('order_blueprint', __name__, url_prefix='/v1/orders')
 ticket_blueprint = Blueprint('ticket_blueprint', __name__, url_prefix='/v1/tickets')
@@ -79,16 +81,84 @@ def resend_emails():
         return ForbiddenError({'source': ''}, "Co-Organizer Access Required").respond()
 
 
-@order_blueprint.route('/calculate-amount', methods=['POST'])
-@jwt_required
-def calculate_amount():
-    data = request.get_json()
+def calculate_order_amount_wrapper(data):
     tickets = data['tickets']
     discount_code = None
     if 'discount-code' in data:
         discount_code_id = data['discount-code']
         discount_code = safe_query(db, DiscountCode, 'id', discount_code_id, 'id')
-        if not TicketingManager.match_discount_quantity(discount_code, tickets, None):
-            return UnprocessableEntityError({'source': 'discount-code'}, 'Discount Usage Exceeded').respond()
+    return tickets, discount_code
 
+
+@order_blueprint.route('/calculate-amount', methods=['POST'])
+@jwt_required
+def calculate_amount():
+    data = request.get_json()
+    tickets, discount_code = calculate_order_amount_wrapper(data)
     return jsonify(calculate_order_amount(tickets, discount_code))
+
+
+@order_blueprint.route('/create-order', methods=['POST'])
+@jwt_required
+def create_order():
+    data = request.get_json()
+    tickets, discount_code = calculate_order_amount_wrapper(data)
+    attendee = data['attendee']
+    for attribute in attendee:
+        attendee[attribute.replace('-', '_')] = attendee.pop(attribute)
+    schema = AttendeeSchema()
+    json_api_attendee = {"data": {"attributes": data['attendee'], "type": "attendee"}}
+    result = schema.load(json_api_attendee)
+    if result.errors:
+        return make_response(jsonify(result.errors), 422)
+    ticket_ids = {int(ticket['id']) for ticket in tickets}
+    quantity = {int(ticket['id']): ticket['quantity'] for ticket in tickets}
+    ticket_list = db.session.query(Ticket).filter(Ticket.id.in_(ticket_ids)).filter_by(event_id=data['event_id'],
+                                                                                       deleted_at=None).all()
+    ticket_ids_found = {ticket_information.id for ticket_information in ticket_list}
+    tickets_not_found = ticket_ids - ticket_ids_found
+    if tickets_not_found:
+        return make_response(jsonify(status='Order Unsuccessful', error='Tickets with id {} were not found in Event {}.'
+                                     .format(tickets_not_found, data['event_id'])), 404)
+    for ticket_info in ticket_list:
+        if (ticket_info.quantity - get_count(db.session.query(TicketHolder.id).filter_by(
+                ticket_id=int(ticket_info.id), deleted_at=None))) < quantity[ticket_info.id]:
+            return make_response(jsonify(status='Order Unsuccessful', error='Ticket already sold out.'), 409)
+    attendee_list = []
+    for ticket in tickets:
+        for ticket_amount in range(ticket['quantity']):
+            attendee = TicketHolder(**result[0], event_id=int(data['event_id']), ticket_id=int(ticket['id']))
+            db.session.add(attendee)
+            attendee_list.append(attendee)
+    ticket_pricing = calculate_order_amount(tickets, discount_code)
+    if not has_access('is_coorganizer', event_id=data['event_id']):
+        data['status'] = 'initializing'
+    # create on site attendees
+    # check if order already exists for this attendee.
+    # check for free tickets and verified user
+    order = Order(amount=ticket_pricing['total_amount'], user_id=current_user.id, event_id=int(data['event_id']),
+                  status=data['status'])
+    db.session.add(order)
+    db.session.commit()
+    db.session.refresh(order)
+    order_tickets = {}
+    for holder in attendee_list:
+        holder.order_id = order.id
+        db.session.add(holder)
+        if not order_tickets.get(holder.ticket_id):
+            order_tickets[holder.ticket_id] = 1
+        else:
+            order_tickets[holder.ticket_id] += 1
+
+    create_pdf_tickets_for_holder(order)
+
+    for ticket in order_tickets:
+        od = OrderTicket(order_id=order.id, ticket_id=ticket, quantity=order_tickets[ticket])
+        db.session.add(od)
+
+    order.quantity = order.tickets_count
+    db.session.add(order)
+    db.session.commit()
+    db.session.refresh(order)
+    order_schema = OrderSchema()
+    return order_schema.dump(order)
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,9 +4,9 @@
 from flask import render_template
 
 from app.settings import get_settings
-from app.api.helpers import ticketing
 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.errors import UnprocessableEntityError
 from app.api.helpers.files import create_save_pdf
 from app.api.helpers.storage import UPLOAD_PATHS
 from app.models import db
@@ -135,6 +135,11 @@ def create_onsite_attendees_for_order(data):
 
 
 def calculate_order_amount(tickets, discount_code):
+    from app.api.helpers.ticketing import TicketingManager
+    if discount_code:
+        if not TicketingManager.match_discount_quantity(discount_code, tickets, None):
+            return UnprocessableEntityError({'source': 'discount-code'}, 'Discount Usage Exceeded').respond()
+
     event = tax = tax_included = fees = None
     total_amount = total_tax = total_discount = 0.0
     ticket_list = []


Test patch: