diff --git a/app/api/auth.py b/app/api/auth.py index 9ec6a1bdc3..6003e5c45c 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -29,16 +29,18 @@ from app.api.helpers.mail import send_email_with_action, \ send_email_confirmation from app.api.helpers.notification import send_notification_with_action -from app.api.helpers.order import create_pdf_tickets_for_holder +from app.api.helpers.order import create_pdf_tickets_for_holder, calculate_order_amount from app.api.helpers.storage import UPLOAD_PATHS from app.api.helpers.storage import generate_hash from app.api.helpers.third_party_auth import GoogleOAuth, FbOAuth, TwitterOAuth, InstagramOAuth +from app.api.helpers.ticketing import TicketingManager from app.api.helpers.utilities import get_serializer, str_generator from app.api.helpers.permission_manager import has_access from app.models import db from app.models.mail import PASSWORD_RESET, PASSWORD_CHANGE, \ PASSWORD_RESET_AND_VERIFY from app.models.notification import PASSWORD_CHANGE as PASSWORD_CHANGE_NOTIF +from app.models.discount_code import DiscountCode from app.models.order import Order from app.models.user import User from app.models.event_invoice import EventInvoice @@ -503,3 +505,18 @@ def resend_emails(): "Only placed and completed orders have confirmation").respond() else: return ForbiddenError({'source': ''}, "Co-Organizer Access Required").respond() + + +@ticket_blueprint.route('/orders/calculate-amount', methods=['POST']) +@jwt_required +def calculate_amount(): + data = request.get_json() + 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 jsonify(calculate_order_amount(tickets, discount_code)) diff --git a/app/api/helpers/order.py b/app/api/helpers/order.py index 85a0402f38..7f8e5d92b3 100644 --- a/app/api/helpers/order.py +++ b/app/api/helpers/order.py @@ -11,6 +11,7 @@ from app.api.helpers.storage import UPLOAD_PATHS from app.models import db from app.models.ticket import Ticket +from app.models.ticket_fee import TicketFees from app.models.ticket_holder import TicketHolder from app.models.order import OrderTicket @@ -131,3 +132,86 @@ def create_onsite_attendees_for_order(data): # delete from the data. del data['on_site_tickets'] + + +def calculate_order_amount(tickets, discount_code): + event = tax = tax_included = fees = None + total_amount = total_tax = total_discount = 0.0 + ticket_list = [] + for ticket_info in tickets: + tax_amount = tax_percent = 0.0 + tax_data = {} + discount_amount = discount_percent = 0.0 + discount_data = {} + sub_total = ticket_fee = 0.0 + + ticket_identifier = ticket_info['id'] + quantity = ticket_info['quantity'] + ticket = safe_query_without_soft_deleted_entries(db, Ticket, 'id', ticket_identifier, 'id') + if not event: + event = ticket.event + fees = TicketFees.query.filter_by(currency=event.payment_currency).first() + elif ticket.event.id != event.id: + raise UnprocessableEntity({'source': 'data/tickets'}, "Invalid Ticket") + + if not tax and event.tax: + tax = event.tax + tax_included = tax.is_tax_included_in_price + + if ticket.type == 'donation': + price = ticket_info.get('price') + if not price or price > ticket.max_price or price < ticket.min_price: + raise UnprocessableEntity({'source': 'data/tickets'}, "Price for donation ticket invalid") + else: + price = ticket.price + + if discount_code: + for code in ticket.discount_codes: + if discount_code.id == code.id: + if code.type == 'amount': + discount_amount = code.value + discount_percent = (discount_amount / price) * 100 + else: + discount_amount = (price * code.value)/100 + discount_percent = code.value + discount_data = { + 'code': discount_code.code, + 'percent': round(discount_percent, 2), + 'amount': round(discount_amount, 2) + } + + if tax: + if not tax_included: + tax_amount = ((price - discount_amount) * tax.rate)/100 + tax_percent = tax.rate + else: + tax_amount = ((price - discount_amount) * tax.rate)/(100 + tax.rate) + tax_percent = tax.rate + tax_data = { + 'percent': round(tax_percent, 2), + 'amount': round(tax_amount, 2), + } + + total_tax = total_tax + tax_amount * quantity + total_discount = total_discount + discount_amount*quantity + if fees and not ticket.is_fee_absorbed: + ticket_fee = fees.service_fee * (price * quantity) / 100 + if ticket_fee > fees.maximum_fee: + ticket_fee = fees.maximum_fee + if tax_included: + sub_total = ticket_fee + (price - discount_amount)*quantity + else: + sub_total = ticket_fee + (price + tax_amount - discount_amount)*quantity + total_amount = total_amount + sub_total + ticket_list.append({ + 'id': ticket.id, + 'name': ticket.name, + 'price': price, + 'quantity': quantity, + 'discount': discount_data, + 'tax': tax_data, + 'ticket_fee': round(ticket_fee, 2), + 'sub_total': round(sub_total, 2) + }) + return dict(tax_included=tax_included, total_amount=round(total_amount, 2), total_tax=round(total_tax, 2), + total_discount=round(total_discount, 2), tickets=ticket_list) diff --git a/app/api/helpers/ticketing.py b/app/api/helpers/ticketing.py index ee72e52a6b..f65e7fb9ed 100644 --- a/app/api/helpers/ticketing.py +++ b/app/api/helpers/ticketing.py @@ -22,15 +22,20 @@ def get_order_expiry(): return 10 @staticmethod - def match_discount_quantity(discount_code, ticket_holders=None): + 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)) .join(Order).filter(Order.status.in_(['completed', 'placed']))) - for holder in ticket_holders: - ticket_holder = TicketHolder.query.filter_by(id=holder).one() - if ticket_holder.ticket.id in ticket_ids: - qty += 1 + 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 + elif tickets: + for ticket in tickets: + if int(ticket['id']) in ticket_ids: + qty += ticket['quantity'] if (qty + old_holders) <= discount_code.tickets_number and \ discount_code.min_quantity <= qty <= discount_code.max_quantity: return True diff --git a/app/api/orders.py b/app/api/orders.py index 587f42e339..e507087782 100644 --- a/app/api/orders.py +++ b/app/api/orders.py @@ -158,7 +158,7 @@ def before_create_object(self, data, view_kwargs): valid_till = discount_code.valid_till if not (valid_from <= now <= valid_till): raise UnprocessableEntity({'source': 'discount_code_id'}, "Inactive Discount Code") - if not TicketingManager.match_discount_quantity(discount_code, data['ticket_holders']): + if not TicketingManager.match_discount_quantity(discount_code, None, data['ticket_holders']): raise UnprocessableEntity({'source': 'discount_code_id'}, 'Discount Usage Exceeded') if discount_code.event.id != int(data['event']): raise UnprocessableEntity({'source': 'discount_code_id'}, "Invalid Discount Code")