We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Unify tax calculation at one place
Right now tax calculation of the system is distributed in different places in different files. There is also repetition of code and some of this calculation is in frontend also. This creates confusion while editing tax info in one place. Also there in `/calculate-amount` endpoint in API for calculating `order_amount`. The idea is to unify tax calculation at one place, preferably `/calculate-amount` endpoint in API
PDF Invoice: Show Discounts in the invoice
The PDF invoice for ticket buyers does not show discount codes clearly. Please implement the following:
* Add the discount code into the description with "Discount code: samplecodehere, Discount: 10% or USD 2.00 etc."
* Show the discount in the item price list with the original price strike through

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `app/api/helpers/order.py`
Content:
```
1 import logging
2 from datetime import datetime, timedelta, timezone
3 
4 from flask import render_template
5 from flask_rest_jsonapi.exceptions import ObjectNotFound
6 
7 from app.api.helpers.db import (
8     get_count,
9     safe_query_without_soft_deleted_entries,
10     save_to_db,
11 )
12 from app.api.helpers.errors import ConflictError, UnprocessableEntityError
13 from app.api.helpers.files import create_save_pdf
14 from app.api.helpers.mail import (
15     send_email_to_attendees,
16     send_order_purchase_organizer_email,
17 )
18 from app.api.helpers.notification import (
19     notify_ticket_purchase_attendee,
20     notify_ticket_purchase_organizer,
21 )
22 from app.api.helpers.storage import UPLOAD_PATHS
23 from app.models import db
24 from app.models.order import OrderTicket
25 from app.models.ticket import Ticket
26 from app.models.ticket_fee import TicketFees
27 from app.models.ticket_holder import TicketHolder
28 from app.models.setting import Setting
29 from app.settings import get_settings
30 
31 
32 def delete_related_attendees_for_order(order):
33     """
34     Delete the associated attendees of an order when it is cancelled/deleted/expired
35     :param order: Order whose attendees have to be deleted.
36     :return:
37     """
38     for ticket_holder in order.ticket_holders:
39         db.session.delete(ticket_holder)
40         try:
41             db.session.commit()
42         except Exception:
43             logging.exception('DB Exception!')
44             db.session.rollback()
45 
46 
47 def set_expiry_for_order(order, override=False):
48     """
49     Expire the order after the time slot(10 minutes) if the order is initializing.
50     Also expires the order if we want to expire an order regardless of the state and time.
51     :param order: Order to be expired.
52     :param override: flag to force expiry.
53     :return:
54     """
55     order_expiry_time = get_settings()['order_expiry_time']
56     if (
57         order
58         and not order.paid_via
59         and (
60             override
61             or (
62                 order.status == 'initializing'
63                 and (order.created_at + timedelta(minutes=order_expiry_time))
64                 < datetime.now(timezone.utc)
65             )
66         )
67     ):
68         order.status = 'expired'
69         delete_related_attendees_for_order(order)
70         save_to_db(order)
71     return order
72 
73 
74 def create_pdf_tickets_for_holder(order):
75     """
76     Create tickets and invoices for the holders of an order.
77     :param order: The order for which to create tickets for.
78     """
79     if order.status == 'completed' or order.status == 'placed':
80         pdf = create_save_pdf(
81             render_template('pdf/ticket_purchaser.html', order=order),
82             UPLOAD_PATHS['pdf']['tickets_all'],
83             dir_path='/static/uploads/pdf/tickets/',
84             identifier=order.identifier,
85             extra_identifiers={'extra_identifier': order.identifier},
86             upload_dir='generated/tickets/',
87         )
88 
89         order.tickets_pdf_url = pdf
90 
91         for holder in order.ticket_holders:
92             # create attendee pdf for every ticket holder
93             pdf = create_save_pdf(
94                 render_template('pdf/ticket_attendee.html', order=order, holder=holder),
95                 UPLOAD_PATHS['pdf']['tickets_all'],
96                 dir_path='/static/uploads/pdf/tickets/',
97                 identifier=order.identifier,
98                 extra_identifiers={'extra_identifier': holder.id},
99                 upload_dir='generated/tickets/',
100             )
101             holder.pdf_url = pdf
102             save_to_db(holder)
103         
104         admin_info = Setting.query.first()
105 
106         # create order invoices pdf
107         order_tickets = OrderTicket.query.filter_by(order_id=order.id).all()
108 
109         attendee = TicketHolder.query.filter_by(order_id=order.id).first()
110 
111         tickets = []
112         for order_ticket in order_tickets:
113             ticket = dict(
114                 id=order_ticket.ticket.id,
115                 price=order_ticket.ticket.price,
116                 quantity=order_ticket.quantity
117             )
118             tickets.append(ticket)
119         
120         # calculate order amount using helper function
121         order_amount = calculate_order_amount(tickets, discount_code=order.discount_code)            
122 
123         create_save_pdf(
124             render_template(
125                 'pdf/order_invoice.html',
126                 order=order,
127                 event=order.event,
128                 tax=order.event.tax,
129                 order_tickets=order_tickets,
130                 attendee=attendee,
131                 event_starts_at=order.event.starts_at_tz.strftime('%d %B %Y'),
132                 created_at=order.created_at.strftime('%d %B %Y'),
133                 admin_info=admin_info,
134                 order_amount=order_amount
135             ),
136             UPLOAD_PATHS['pdf']['order'],
137             dir_path='/static/uploads/pdf/tickets/',
138             identifier=order.identifier,
139             upload_dir='generated/invoices/',
140             new_renderer=True,
141         )
142         save_to_db(order)
143 
144 
145 def create_onsite_attendees_for_order(data):
146     """
147     Creates on site ticket holders for an order and adds it into the request data.
148     :param data: data initially passed in the POST request for order.
149     :return:
150     """
151     on_site_tickets = data.get('on_site_tickets')
152 
153     if not on_site_tickets:
154         raise UnprocessableEntityError(
155             {'pointer': 'data/attributes/on_site_tickets'}, 'on_site_tickets info missing'
156         )
157 
158     data['ticket_holders'] = []
159 
160     for on_site_ticket in on_site_tickets:
161         ticket_id = on_site_ticket['id']
162         quantity = int(on_site_ticket['quantity'])
163 
164         ticket = safe_query_without_soft_deleted_entries(
165             Ticket, 'id', ticket_id, 'ticket_id'
166         )
167 
168         ticket_sold_count = get_count(
169             db.session.query(TicketHolder.id).filter_by(
170                 ticket_id=int(ticket.id), deleted_at=None
171             )
172         )
173 
174         # Check if the ticket is already sold out or not.
175         if ticket_sold_count + quantity > ticket.quantity:
176             # delete the already created attendees.
177             for holder in data['ticket_holders']:
178                 ticket_holder = (
179                     db.session.query(TicketHolder).filter(id == int(holder)).one()
180                 )
181                 db.session.delete(ticket_holder)
182                 try:
183                     db.session.commit()
184                 except Exception:
185                     logging.exception('DB Exception!')
186                     db.session.rollback()
187 
188             raise ConflictError(
189                 {'pointer': '/data/attributes/on_site_tickets'},
190                 "Ticket with id: {} already sold out. You can buy at most {} tickets".format(
191                     ticket_id, ticket.quantity - ticket_sold_count
192                 ),
193             )
194 
195         for _ in range(1, quantity):
196             ticket_holder = TicketHolder(
197                 firstname='onsite',
198                 lastname='attendee',
199                 email='example@example.com',
200                 ticket_id=ticket.id,
201                 event_id=data.get('event'),
202             )
203             save_to_db(ticket_holder)
204             data['ticket_holders'].append(ticket_holder.id)
205 
206     # delete from the data.
207     del data['on_site_tickets']
208 
209 
210 def calculate_order_amount(tickets, discount_code=None):
211     from app.api.helpers.ticketing import validate_discount_code, validate_tickets
212     from app.models.discount_code import DiscountCode
213 
214     ticket_ids = {ticket['id'] for ticket in tickets}
215     ticket_map = {int(ticket['id']): ticket for ticket in tickets}
216     fetched_tickets = validate_tickets(ticket_ids)
217 
218     if tickets and discount_code:
219         discount_code = validate_discount_code(discount_code, tickets=tickets)
220 
221     event = tax = tax_included = fees = None
222     total_amount = total_tax = total_discount = 0.0
223     ticket_list = []
224     for ticket in fetched_tickets:
225         ticket_info = ticket_map[ticket.id]
226         discount_amount = 0.0
227         discount_data = None
228         ticket_fee = 0.0
229 
230         quantity = ticket_info.get('quantity', 1)  # Default to single ticket
231         if not event:
232             event = ticket.event
233 
234             if event.deleted_at:
235                 raise ObjectNotFound(
236                     {'pointer': 'tickets/event'}, f'Event: {event.id} not found'
237                 )
238 
239             fees = TicketFees.query.filter_by(currency=event.payment_currency).first()
240 
241         if not tax and event.tax:
242             tax = event.tax
243             tax_included = tax.is_tax_included_in_price
244 
245         if ticket.type == 'donation':
246             price = ticket_info.get('price')
247             if not price or price > ticket.max_price or price < ticket.min_price:
248                 raise UnprocessableEntityError(
249                     {'pointer': 'tickets/price'},
250                     f"Price for donation ticket should be present and within range "
251                     f"{ticket.min_price} to {ticket.max_price}",
252                 )
253         else:
254             price = ticket.price if ticket.type != 'free' else 0.0
255 
256         if discount_code and ticket.type != 'free':
257             code = (
258                 DiscountCode.query.with_parent(ticket)
259                 .filter_by(id=discount_code.id)
260                 .first()
261             )
262             if code:
263                 if discount_code.id == code.id:
264                     if code.type == 'amount':
265                         discount_amount = min(code.value, price)
266                         discount_percent = (discount_amount / price) * 100
267                     else:
268                         discount_amount = (price * code.value) / 100
269                         discount_percent = code.value
270                     discount_data = {
271                         'code': discount_code.code,
272                         'percent': round(discount_percent, 2),
273                         'amount': round(discount_amount, 2),
274                         'total': round(discount_amount * quantity, 2),
275                     }
276 
277         total_discount += round(discount_amount * quantity, 2)
278         if fees and not ticket.is_fee_absorbed:
279             ticket_fee = fees.service_fee * (price * quantity) / 100
280             if ticket_fee > fees.maximum_fee:
281                 ticket_fee = fees.maximum_fee
282         sub_total = ticket_fee + (price - discount_amount) * quantity
283         total_amount = total_amount + sub_total
284         ticket_list.append(
285             {
286                 'id': ticket.id,
287                 'name': ticket.name,
288                 'price': price,
289                 'quantity': quantity,
290                 'discount': discount_data,
291                 'ticket_fee': round(ticket_fee, 2),
292                 'sub_total': round(sub_total, 2),
293             }
294         )
295 
296     sub_total = total_amount
297     tax_dict = None
298     if tax:
299         if tax_included:
300             total_tax = total_amount - total_amount / (1 + tax.rate / 100)
301         else:
302             total_tax = total_amount * tax.rate / 100
303             total_amount += total_tax
304         tax_dict = dict(
305             included=tax_included,
306             amount=round(total_tax, 2),
307             percent=tax.rate if tax else 0.0,
308             name=tax.name,
309         )
310 
311     return dict(
312         tax=tax_dict,
313         sub_total=round(sub_total, 2),
314         total=round(total_amount, 2),
315         discount=round(total_discount, 2),
316         tickets=ticket_list,
317     )
318 
319 
320 def on_order_completed(order):
321     # send e-mail and notifications if the order status is completed
322     if not (order.status == 'completed' or order.status == 'placed'):
323         return
324 
325     create_pdf_tickets_for_holder(order)
326 
327     # send email and notifications.
328     send_email_to_attendees(order)
329     notify_ticket_purchase_attendee(order)
330 
331     if order.payment_mode in ['free', 'bank', 'cheque', 'onsite']:
332         order.completed_at = datetime.utcnow()
333 
334     organizer_set = set(
335         filter(
336             bool, order.event.organizers + order.event.coorganizers + [order.event.owner]
337         )
338     )
339     send_order_purchase_organizer_email(order, organizer_set)
340     notify_ticket_purchase_organizer(order)
341
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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
@@ -222,6 +222,7 @@
     total_amount = total_tax = total_discount = 0.0
     ticket_list = []
     for ticket in fetched_tickets:
+        ticket_tax = discounted_tax = 0.0
         ticket_info = ticket_map[ticket.id]
         discount_amount = 0.0
         discount_data = None
@@ -253,6 +254,12 @@
         else:
             price = ticket.price if ticket.type != 'free' else 0.0
 
+        if tax:
+            if tax_included:
+                ticket_tax = price - price / (1 + tax.rate / 100)
+            else:
+                ticket_tax = price * tax.rate / 100
+
         if discount_code and ticket.type != 'free':
             code = (
                 DiscountCode.query.with_parent(ticket)
@@ -266,6 +273,8 @@
                         discount_percent = (discount_amount / price) * 100
                     else:
                         discount_amount = (price * code.value) / 100
+                        if tax:
+                            discounted_tax = ticket_tax - (ticket_tax * code.value / 100)
                         discount_percent = code.value
                     discount_data = {
                         'code': discount_code.code,
@@ -290,6 +299,8 @@
                 'discount': discount_data,
                 'ticket_fee': round(ticket_fee, 2),
                 'sub_total': round(sub_total, 2),
+                'ticket_tax': round(ticket_tax, 2),
+                'discounted_tax': round(discounted_tax, 2)
             }
         )

{"golden_diff": "diff --git a/app/api/helpers/order.py b/app/api/helpers/order.py\n--- a/app/api/helpers/order.py\n+++ b/app/api/helpers/order.py\n@@ -222,6 +222,7 @@\n     total_amount = total_tax = total_discount = 0.0\n     ticket_list = []\n     for ticket in fetched_tickets:\n+        ticket_tax = discounted_tax = 0.0\n         ticket_info = ticket_map[ticket.id]\n         discount_amount = 0.0\n         discount_data = None\n@@ -253,6 +254,12 @@\n         else:\n             price = ticket.price if ticket.type != 'free' else 0.0\n \n+        if tax:\n+            if tax_included:\n+                ticket_tax = price - price / (1 + tax.rate / 100)\n+            else:\n+                ticket_tax = price * tax.rate / 100\n+\n         if discount_code and ticket.type != 'free':\n             code = (\n                 DiscountCode.query.with_parent(ticket)\n@@ -266,6 +273,8 @@\n                         discount_percent = (discount_amount / price) * 100\n                     else:\n                         discount_amount = (price * code.value) / 100\n+                        if tax:\n+                            discounted_tax = ticket_tax - (ticket_tax * code.value / 100)\n                         discount_percent = code.value\n                     discount_data = {\n                         'code': discount_code.code,\n@@ -290,6 +299,8 @@\n                 'discount': discount_data,\n                 'ticket_fee': round(ticket_fee, 2),\n                 'sub_total': round(sub_total, 2),\n+                'ticket_tax': round(ticket_tax, 2),\n+                'discounted_tax': round(discounted_tax, 2)\n             }\n         )\n", "issue": "Unify tax calculation at one place\nRight now tax calculation of the system is distributed in different places in different files. There is also repetition of code and some of this calculation is in frontend also. This creates confusion while editing tax info in one place. Also there in `/calculate-amount` endpoint in API for calculating `order_amount`. The idea is to unify tax calculation at one place, preferably `/calculate-amount` endpoint in API\nPDF Invoice: Show Discounts in the invoice\nThe PDF invoice for ticket buyers does not show discount codes clearly. Please implement the following:\r\n* Add the discount code into the description with \"Discount code: samplecodehere, Discount: 10% or USD 2.00 etc.\"\r\n* Show the discount in the item price list with the original price strike through\n", "before_files": [{"content": "import logging\nfrom datetime import datetime, timedelta, timezone\n\nfrom flask import render_template\nfrom flask_rest_jsonapi.exceptions import ObjectNotFound\n\nfrom app.api.helpers.db import (\n    get_count,\n    safe_query_without_soft_deleted_entries,\n    save_to_db,\n)\nfrom app.api.helpers.errors import ConflictError, UnprocessableEntityError\nfrom app.api.helpers.files import create_save_pdf\nfrom app.api.helpers.mail import (\n    send_email_to_attendees,\n    send_order_purchase_organizer_email,\n)\nfrom app.api.helpers.notification import (\n    notify_ticket_purchase_attendee,\n    notify_ticket_purchase_organizer,\n)\nfrom app.api.helpers.storage import UPLOAD_PATHS\nfrom app.models import db\nfrom app.models.order import OrderTicket\nfrom app.models.ticket import Ticket\nfrom app.models.ticket_fee import TicketFees\nfrom app.models.ticket_holder import TicketHolder\nfrom app.models.setting import Setting\nfrom app.settings import get_settings\n\n\ndef delete_related_attendees_for_order(order):\n    \"\"\"\n    Delete the associated attendees of an order when it is cancelled/deleted/expired\n    :param order: Order whose attendees have to be deleted.\n    :return:\n    \"\"\"\n    for ticket_holder in order.ticket_holders:\n        db.session.delete(ticket_holder)\n        try:\n            db.session.commit()\n        except Exception:\n            logging.exception('DB Exception!')\n            db.session.rollback()\n\n\ndef set_expiry_for_order(order, override=False):\n    \"\"\"\n    Expire the order after the time slot(10 minutes) if the order is initializing.\n    Also expires the order if we want to expire an order regardless of the state and time.\n    :param order: Order to be expired.\n    :param override: flag to force expiry.\n    :return:\n    \"\"\"\n    order_expiry_time = get_settings()['order_expiry_time']\n    if (\n        order\n        and not order.paid_via\n        and (\n            override\n            or (\n                order.status == 'initializing'\n                and (order.created_at + timedelta(minutes=order_expiry_time))\n                < datetime.now(timezone.utc)\n            )\n        )\n    ):\n        order.status = 'expired'\n        delete_related_attendees_for_order(order)\n        save_to_db(order)\n    return order\n\n\ndef create_pdf_tickets_for_holder(order):\n    \"\"\"\n    Create tickets and invoices for the holders of an order.\n    :param order: The order for which to create tickets for.\n    \"\"\"\n    if order.status == 'completed' or order.status == 'placed':\n        pdf = create_save_pdf(\n            render_template('pdf/ticket_purchaser.html', order=order),\n            UPLOAD_PATHS['pdf']['tickets_all'],\n            dir_path='/static/uploads/pdf/tickets/',\n            identifier=order.identifier,\n            extra_identifiers={'extra_identifier': order.identifier},\n            upload_dir='generated/tickets/',\n        )\n\n        order.tickets_pdf_url = pdf\n\n        for holder in order.ticket_holders:\n            # create attendee pdf for every ticket holder\n            pdf = create_save_pdf(\n                render_template('pdf/ticket_attendee.html', order=order, holder=holder),\n                UPLOAD_PATHS['pdf']['tickets_all'],\n                dir_path='/static/uploads/pdf/tickets/',\n                identifier=order.identifier,\n                extra_identifiers={'extra_identifier': holder.id},\n                upload_dir='generated/tickets/',\n            )\n            holder.pdf_url = pdf\n            save_to_db(holder)\n        \n        admin_info = Setting.query.first()\n\n        # create order invoices pdf\n        order_tickets = OrderTicket.query.filter_by(order_id=order.id).all()\n\n        attendee = TicketHolder.query.filter_by(order_id=order.id).first()\n\n        tickets = []\n        for order_ticket in order_tickets:\n            ticket = dict(\n                id=order_ticket.ticket.id,\n                price=order_ticket.ticket.price,\n                quantity=order_ticket.quantity\n            )\n            tickets.append(ticket)\n        \n        # calculate order amount using helper function\n        order_amount = calculate_order_amount(tickets, discount_code=order.discount_code)            \n\n        create_save_pdf(\n            render_template(\n                'pdf/order_invoice.html',\n                order=order,\n                event=order.event,\n                tax=order.event.tax,\n                order_tickets=order_tickets,\n                attendee=attendee,\n                event_starts_at=order.event.starts_at_tz.strftime('%d %B %Y'),\n                created_at=order.created_at.strftime('%d %B %Y'),\n                admin_info=admin_info,\n                order_amount=order_amount\n            ),\n            UPLOAD_PATHS['pdf']['order'],\n            dir_path='/static/uploads/pdf/tickets/',\n            identifier=order.identifier,\n            upload_dir='generated/invoices/',\n            new_renderer=True,\n        )\n        save_to_db(order)\n\n\ndef create_onsite_attendees_for_order(data):\n    \"\"\"\n    Creates on site ticket holders for an order and adds it into the request data.\n    :param data: data initially passed in the POST request for order.\n    :return:\n    \"\"\"\n    on_site_tickets = data.get('on_site_tickets')\n\n    if not on_site_tickets:\n        raise UnprocessableEntityError(\n            {'pointer': 'data/attributes/on_site_tickets'}, 'on_site_tickets info missing'\n        )\n\n    data['ticket_holders'] = []\n\n    for on_site_ticket in on_site_tickets:\n        ticket_id = on_site_ticket['id']\n        quantity = int(on_site_ticket['quantity'])\n\n        ticket = safe_query_without_soft_deleted_entries(\n            Ticket, 'id', ticket_id, 'ticket_id'\n        )\n\n        ticket_sold_count = get_count(\n            db.session.query(TicketHolder.id).filter_by(\n                ticket_id=int(ticket.id), deleted_at=None\n            )\n        )\n\n        # Check if the ticket is already sold out or not.\n        if ticket_sold_count + quantity > ticket.quantity:\n            # delete the already created attendees.\n            for holder in data['ticket_holders']:\n                ticket_holder = (\n                    db.session.query(TicketHolder).filter(id == int(holder)).one()\n                )\n                db.session.delete(ticket_holder)\n                try:\n                    db.session.commit()\n                except Exception:\n                    logging.exception('DB Exception!')\n                    db.session.rollback()\n\n            raise ConflictError(\n                {'pointer': '/data/attributes/on_site_tickets'},\n                \"Ticket with id: {} already sold out. You can buy at most {} tickets\".format(\n                    ticket_id, ticket.quantity - ticket_sold_count\n                ),\n            )\n\n        for _ in range(1, quantity):\n            ticket_holder = TicketHolder(\n                firstname='onsite',\n                lastname='attendee',\n                email='example@example.com',\n                ticket_id=ticket.id,\n                event_id=data.get('event'),\n            )\n            save_to_db(ticket_holder)\n            data['ticket_holders'].append(ticket_holder.id)\n\n    # delete from the data.\n    del data['on_site_tickets']\n\n\ndef calculate_order_amount(tickets, discount_code=None):\n    from app.api.helpers.ticketing import validate_discount_code, validate_tickets\n    from app.models.discount_code import DiscountCode\n\n    ticket_ids = {ticket['id'] for ticket in tickets}\n    ticket_map = {int(ticket['id']): ticket for ticket in tickets}\n    fetched_tickets = validate_tickets(ticket_ids)\n\n    if tickets and discount_code:\n        discount_code = validate_discount_code(discount_code, tickets=tickets)\n\n    event = tax = tax_included = fees = None\n    total_amount = total_tax = total_discount = 0.0\n    ticket_list = []\n    for ticket in fetched_tickets:\n        ticket_info = ticket_map[ticket.id]\n        discount_amount = 0.0\n        discount_data = None\n        ticket_fee = 0.0\n\n        quantity = ticket_info.get('quantity', 1)  # Default to single ticket\n        if not event:\n            event = ticket.event\n\n            if event.deleted_at:\n                raise ObjectNotFound(\n                    {'pointer': 'tickets/event'}, f'Event: {event.id} not found'\n                )\n\n            fees = TicketFees.query.filter_by(currency=event.payment_currency).first()\n\n        if not tax and event.tax:\n            tax = event.tax\n            tax_included = tax.is_tax_included_in_price\n\n        if ticket.type == 'donation':\n            price = ticket_info.get('price')\n            if not price or price > ticket.max_price or price < ticket.min_price:\n                raise UnprocessableEntityError(\n                    {'pointer': 'tickets/price'},\n                    f\"Price for donation ticket should be present and within range \"\n                    f\"{ticket.min_price} to {ticket.max_price}\",\n                )\n        else:\n            price = ticket.price if ticket.type != 'free' else 0.0\n\n        if discount_code and ticket.type != 'free':\n            code = (\n                DiscountCode.query.with_parent(ticket)\n                .filter_by(id=discount_code.id)\n                .first()\n            )\n            if code:\n                if discount_code.id == code.id:\n                    if code.type == 'amount':\n                        discount_amount = min(code.value, price)\n                        discount_percent = (discount_amount / price) * 100\n                    else:\n                        discount_amount = (price * code.value) / 100\n                        discount_percent = code.value\n                    discount_data = {\n                        'code': discount_code.code,\n                        'percent': round(discount_percent, 2),\n                        'amount': round(discount_amount, 2),\n                        'total': round(discount_amount * quantity, 2),\n                    }\n\n        total_discount += round(discount_amount * quantity, 2)\n        if fees and not ticket.is_fee_absorbed:\n            ticket_fee = fees.service_fee * (price * quantity) / 100\n            if ticket_fee > fees.maximum_fee:\n                ticket_fee = fees.maximum_fee\n        sub_total = ticket_fee + (price - discount_amount) * quantity\n        total_amount = total_amount + sub_total\n        ticket_list.append(\n            {\n                'id': ticket.id,\n                'name': ticket.name,\n                'price': price,\n                'quantity': quantity,\n                'discount': discount_data,\n                'ticket_fee': round(ticket_fee, 2),\n                'sub_total': round(sub_total, 2),\n            }\n        )\n\n    sub_total = total_amount\n    tax_dict = None\n    if tax:\n        if tax_included:\n            total_tax = total_amount - total_amount / (1 + tax.rate / 100)\n        else:\n            total_tax = total_amount * tax.rate / 100\n            total_amount += total_tax\n        tax_dict = dict(\n            included=tax_included,\n            amount=round(total_tax, 2),\n            percent=tax.rate if tax else 0.0,\n            name=tax.name,\n        )\n\n    return dict(\n        tax=tax_dict,\n        sub_total=round(sub_total, 2),\n        total=round(total_amount, 2),\n        discount=round(total_discount, 2),\n        tickets=ticket_list,\n    )\n\n\ndef on_order_completed(order):\n    # send e-mail and notifications if the order status is completed\n    if not (order.status == 'completed' or order.status == 'placed'):\n        return\n\n    create_pdf_tickets_for_holder(order)\n\n    # send email and notifications.\n    send_email_to_attendees(order)\n    notify_ticket_purchase_attendee(order)\n\n    if order.payment_mode in ['free', 'bank', 'cheque', 'onsite']:\n        order.completed_at = datetime.utcnow()\n\n    organizer_set = set(\n        filter(\n            bool, order.event.organizers + order.event.coorganizers + [order.event.owner]\n        )\n    )\n    send_order_purchase_organizer_email(order, organizer_set)\n    notify_ticket_purchase_organizer(order)\n", "path": "app/api/helpers/order.py"}], "after_files": [{"content": "import logging\nfrom datetime import datetime, timedelta, timezone\n\nfrom flask import render_template\nfrom flask_rest_jsonapi.exceptions import ObjectNotFound\n\nfrom app.api.helpers.db import (\n    get_count,\n    safe_query_without_soft_deleted_entries,\n    save_to_db,\n)\nfrom app.api.helpers.errors import ConflictError, UnprocessableEntityError\nfrom app.api.helpers.files import create_save_pdf\nfrom app.api.helpers.mail import (\n    send_email_to_attendees,\n    send_order_purchase_organizer_email,\n)\nfrom app.api.helpers.notification import (\n    notify_ticket_purchase_attendee,\n    notify_ticket_purchase_organizer,\n)\nfrom app.api.helpers.storage import UPLOAD_PATHS\nfrom app.models import db\nfrom app.models.order import OrderTicket\nfrom app.models.ticket import Ticket\nfrom app.models.ticket_fee import TicketFees\nfrom app.models.ticket_holder import TicketHolder\nfrom app.models.setting import Setting\nfrom app.settings import get_settings\n\n\ndef delete_related_attendees_for_order(order):\n    \"\"\"\n    Delete the associated attendees of an order when it is cancelled/deleted/expired\n    :param order: Order whose attendees have to be deleted.\n    :return:\n    \"\"\"\n    for ticket_holder in order.ticket_holders:\n        db.session.delete(ticket_holder)\n        try:\n            db.session.commit()\n        except Exception:\n            logging.exception('DB Exception!')\n            db.session.rollback()\n\n\ndef set_expiry_for_order(order, override=False):\n    \"\"\"\n    Expire the order after the time slot(10 minutes) if the order is initializing.\n    Also expires the order if we want to expire an order regardless of the state and time.\n    :param order: Order to be expired.\n    :param override: flag to force expiry.\n    :return:\n    \"\"\"\n    order_expiry_time = get_settings()['order_expiry_time']\n    if (\n        order\n        and not order.paid_via\n        and (\n            override\n            or (\n                order.status == 'initializing'\n                and (order.created_at + timedelta(minutes=order_expiry_time))\n                < datetime.now(timezone.utc)\n            )\n        )\n    ):\n        order.status = 'expired'\n        delete_related_attendees_for_order(order)\n        save_to_db(order)\n    return order\n\n\ndef create_pdf_tickets_for_holder(order):\n    \"\"\"\n    Create tickets and invoices for the holders of an order.\n    :param order: The order for which to create tickets for.\n    \"\"\"\n    if order.status == 'completed' or order.status == 'placed':\n        pdf = create_save_pdf(\n            render_template('pdf/ticket_purchaser.html', order=order),\n            UPLOAD_PATHS['pdf']['tickets_all'],\n            dir_path='/static/uploads/pdf/tickets/',\n            identifier=order.identifier,\n            extra_identifiers={'extra_identifier': order.identifier},\n            upload_dir='generated/tickets/',\n        )\n\n        order.tickets_pdf_url = pdf\n\n        for holder in order.ticket_holders:\n            # create attendee pdf for every ticket holder\n            pdf = create_save_pdf(\n                render_template('pdf/ticket_attendee.html', order=order, holder=holder),\n                UPLOAD_PATHS['pdf']['tickets_all'],\n                dir_path='/static/uploads/pdf/tickets/',\n                identifier=order.identifier,\n                extra_identifiers={'extra_identifier': holder.id},\n                upload_dir='generated/tickets/',\n            )\n            holder.pdf_url = pdf\n            save_to_db(holder)\n        \n        admin_info = Setting.query.first()\n\n        # create order invoices pdf\n        order_tickets = OrderTicket.query.filter_by(order_id=order.id).all()\n\n        attendee = TicketHolder.query.filter_by(order_id=order.id).first()\n\n        tickets = []\n        for order_ticket in order_tickets:\n            ticket = dict(\n                id=order_ticket.ticket.id,\n                price=order_ticket.ticket.price,\n                quantity=order_ticket.quantity\n            )\n            tickets.append(ticket)\n        \n        # calculate order amount using helper function\n        order_amount = calculate_order_amount(tickets, discount_code=order.discount_code)            \n\n        create_save_pdf(\n            render_template(\n                'pdf/order_invoice.html',\n                order=order,\n                event=order.event,\n                tax=order.event.tax,\n                order_tickets=order_tickets,\n                attendee=attendee,\n                event_starts_at=order.event.starts_at_tz.strftime('%d %B %Y'),\n                created_at=order.created_at.strftime('%d %B %Y'),\n                admin_info=admin_info,\n                order_amount=order_amount\n            ),\n            UPLOAD_PATHS['pdf']['order'],\n            dir_path='/static/uploads/pdf/tickets/',\n            identifier=order.identifier,\n            upload_dir='generated/invoices/',\n            new_renderer=True,\n        )\n        save_to_db(order)\n\n\ndef create_onsite_attendees_for_order(data):\n    \"\"\"\n    Creates on site ticket holders for an order and adds it into the request data.\n    :param data: data initially passed in the POST request for order.\n    :return:\n    \"\"\"\n    on_site_tickets = data.get('on_site_tickets')\n\n    if not on_site_tickets:\n        raise UnprocessableEntityError(\n            {'pointer': 'data/attributes/on_site_tickets'}, 'on_site_tickets info missing'\n        )\n\n    data['ticket_holders'] = []\n\n    for on_site_ticket in on_site_tickets:\n        ticket_id = on_site_ticket['id']\n        quantity = int(on_site_ticket['quantity'])\n\n        ticket = safe_query_without_soft_deleted_entries(\n            Ticket, 'id', ticket_id, 'ticket_id'\n        )\n\n        ticket_sold_count = get_count(\n            db.session.query(TicketHolder.id).filter_by(\n                ticket_id=int(ticket.id), deleted_at=None\n            )\n        )\n\n        # Check if the ticket is already sold out or not.\n        if ticket_sold_count + quantity > ticket.quantity:\n            # delete the already created attendees.\n            for holder in data['ticket_holders']:\n                ticket_holder = (\n                    db.session.query(TicketHolder).filter(id == int(holder)).one()\n                )\n                db.session.delete(ticket_holder)\n                try:\n                    db.session.commit()\n                except Exception:\n                    logging.exception('DB Exception!')\n                    db.session.rollback()\n\n            raise ConflictError(\n                {'pointer': '/data/attributes/on_site_tickets'},\n                \"Ticket with id: {} already sold out. You can buy at most {} tickets\".format(\n                    ticket_id, ticket.quantity - ticket_sold_count\n                ),\n            )\n\n        for _ in range(1, quantity):\n            ticket_holder = TicketHolder(\n                firstname='onsite',\n                lastname='attendee',\n                email='example@example.com',\n                ticket_id=ticket.id,\n                event_id=data.get('event'),\n            )\n            save_to_db(ticket_holder)\n            data['ticket_holders'].append(ticket_holder.id)\n\n    # delete from the data.\n    del data['on_site_tickets']\n\n\ndef calculate_order_amount(tickets, discount_code=None):\n    from app.api.helpers.ticketing import validate_discount_code, validate_tickets\n    from app.models.discount_code import DiscountCode\n\n    ticket_ids = {ticket['id'] for ticket in tickets}\n    ticket_map = {int(ticket['id']): ticket for ticket in tickets}\n    fetched_tickets = validate_tickets(ticket_ids)\n\n    if tickets and discount_code:\n        discount_code = validate_discount_code(discount_code, tickets=tickets)\n\n    event = tax = tax_included = fees = None\n    total_amount = total_tax = total_discount = 0.0\n    ticket_list = []\n    for ticket in fetched_tickets:\n        ticket_tax = discounted_tax = 0.0\n        ticket_info = ticket_map[ticket.id]\n        discount_amount = 0.0\n        discount_data = None\n        ticket_fee = 0.0\n\n        quantity = ticket_info.get('quantity', 1)  # Default to single ticket\n        if not event:\n            event = ticket.event\n\n            if event.deleted_at:\n                raise ObjectNotFound(\n                    {'pointer': 'tickets/event'}, f'Event: {event.id} not found'\n                )\n\n            fees = TicketFees.query.filter_by(currency=event.payment_currency).first()\n\n        if not tax and event.tax:\n            tax = event.tax\n            tax_included = tax.is_tax_included_in_price\n\n        if ticket.type == 'donation':\n            price = ticket_info.get('price')\n            if not price or price > ticket.max_price or price < ticket.min_price:\n                raise UnprocessableEntityError(\n                    {'pointer': 'tickets/price'},\n                    f\"Price for donation ticket should be present and within range \"\n                    f\"{ticket.min_price} to {ticket.max_price}\",\n                )\n        else:\n            price = ticket.price if ticket.type != 'free' else 0.0\n\n        if tax:\n            if tax_included:\n                ticket_tax = price - price / (1 + tax.rate / 100)\n            else:\n                ticket_tax = price * tax.rate / 100\n\n        if discount_code and ticket.type != 'free':\n            code = (\n                DiscountCode.query.with_parent(ticket)\n                .filter_by(id=discount_code.id)\n                .first()\n            )\n            if code:\n                if discount_code.id == code.id:\n                    if code.type == 'amount':\n                        discount_amount = min(code.value, price)\n                        discount_percent = (discount_amount / price) * 100\n                    else:\n                        discount_amount = (price * code.value) / 100\n                        if tax:\n                            discounted_tax = ticket_tax - (ticket_tax * code.value / 100)\n                        discount_percent = code.value\n                    discount_data = {\n                        'code': discount_code.code,\n                        'percent': round(discount_percent, 2),\n                        'amount': round(discount_amount, 2),\n                        'total': round(discount_amount * quantity, 2),\n                    }\n\n        total_discount += round(discount_amount * quantity, 2)\n        if fees and not ticket.is_fee_absorbed:\n            ticket_fee = fees.service_fee * (price * quantity) / 100\n            if ticket_fee > fees.maximum_fee:\n                ticket_fee = fees.maximum_fee\n        sub_total = ticket_fee + (price - discount_amount) * quantity\n        total_amount = total_amount + sub_total\n        ticket_list.append(\n            {\n                'id': ticket.id,\n                'name': ticket.name,\n                'price': price,\n                'quantity': quantity,\n                'discount': discount_data,\n                'ticket_fee': round(ticket_fee, 2),\n                'sub_total': round(sub_total, 2),\n                'ticket_tax': round(ticket_tax, 2),\n                'discounted_tax': round(discounted_tax, 2)\n            }\n        )\n\n    sub_total = total_amount\n    tax_dict = None\n    if tax:\n        if tax_included:\n            total_tax = total_amount - total_amount / (1 + tax.rate / 100)\n        else:\n            total_tax = total_amount * tax.rate / 100\n            total_amount += total_tax\n        tax_dict = dict(\n            included=tax_included,\n            amount=round(total_tax, 2),\n            percent=tax.rate if tax else 0.0,\n            name=tax.name,\n        )\n\n    return dict(\n        tax=tax_dict,\n        sub_total=round(sub_total, 2),\n        total=round(total_amount, 2),\n        discount=round(total_discount, 2),\n        tickets=ticket_list,\n    )\n\n\ndef on_order_completed(order):\n    # send e-mail and notifications if the order status is completed\n    if not (order.status == 'completed' or order.status == 'placed'):\n        return\n\n    create_pdf_tickets_for_holder(order)\n\n    # send email and notifications.\n    send_email_to_attendees(order)\n    notify_ticket_purchase_attendee(order)\n\n    if order.payment_mode in ['free', 'bank', 'cheque', 'onsite']:\n        order.completed_at = datetime.utcnow()\n\n    organizer_set = set(\n        filter(\n            bool, order.event.organizers + order.event.coorganizers + [order.event.owner]\n        )\n    )\n    send_order_purchase_organizer_email(order, organizer_set)\n    notify_ticket_purchase_organizer(order)\n", "path": "app/api/helpers/order.py"}]}