From ef3a70e02387199d0aea417c716852e71df088e7 Mon Sep 17 00:00:00 2001 From: Shreyansh Dwivedi Date: Thu, 15 Aug 2019 16:01:51 +0530 Subject: [PATCH] feat: API to fetch total order amount by passing tickets and discount code adds function to order helper adds checks for donation tickets adds discount code overconsumption check included ticket service fee calculation --- app/api/auth.py | 19 +++++++- app/api/helpers/order.py | 84 ++++++++++++++++++++++++++++++++++++ app/api/helpers/ticketing.py | 15 ++++--- app/api/orders.py | 2 +- 4 files changed, 113 insertions(+), 7 deletions(-) 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")