We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
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         tickets = []
110         for order_ticket in order_tickets:
111             ticket = dict(
112                 id=order_ticket.ticket.id,
113                 price=order_ticket.ticket.price,
114                 quantity=order_ticket.quantity
115             )
116             tickets.append(ticket)
117         
118         # calculate order amount using helper function
119         order_amount = calculate_order_amount(tickets, discount_code=order.discount_code)            
120 
121         create_save_pdf(
122             render_template(
123                 'pdf/order_invoice.html',
124                 order=order,
125                 event=order.event,
126                 tax=order.event.tax,
127                 order_tickets=order_tickets,
128                 event_starts_at=order.event.starts_at_tz.strftime('%d %B %Y'),
129                 created_at=order.created_at.strftime('%d %B %Y'),
130                 admin_info=admin_info,
131                 order_amount=order_amount
132             ),
133             UPLOAD_PATHS['pdf']['order'],
134             dir_path='/static/uploads/pdf/tickets/',
135             identifier=order.identifier,
136             upload_dir='generated/invoices/',
137             new_renderer=True,
138         )
139         save_to_db(order)
140 
141 
142 def create_onsite_attendees_for_order(data):
143     """
144     Creates on site ticket holders for an order and adds it into the request data.
145     :param data: data initially passed in the POST request for order.
146     :return:
147     """
148     on_site_tickets = data.get('on_site_tickets')
149 
150     if not on_site_tickets:
151         raise UnprocessableEntityError(
152             {'pointer': 'data/attributes/on_site_tickets'}, 'on_site_tickets info missing'
153         )
154 
155     data['ticket_holders'] = []
156 
157     for on_site_ticket in on_site_tickets:
158         ticket_id = on_site_ticket['id']
159         quantity = int(on_site_ticket['quantity'])
160 
161         ticket = safe_query_without_soft_deleted_entries(
162             Ticket, 'id', ticket_id, 'ticket_id'
163         )
164 
165         ticket_sold_count = get_count(
166             db.session.query(TicketHolder.id).filter_by(
167                 ticket_id=int(ticket.id), deleted_at=None
168             )
169         )
170 
171         # Check if the ticket is already sold out or not.
172         if ticket_sold_count + quantity > ticket.quantity:
173             # delete the already created attendees.
174             for holder in data['ticket_holders']:
175                 ticket_holder = (
176                     db.session.query(TicketHolder).filter(id == int(holder)).one()
177                 )
178                 db.session.delete(ticket_holder)
179                 try:
180                     db.session.commit()
181                 except Exception:
182                     logging.exception('DB Exception!')
183                     db.session.rollback()
184 
185             raise ConflictError(
186                 {'pointer': '/data/attributes/on_site_tickets'},
187                 "Ticket with id: {} already sold out. You can buy at most {} tickets".format(
188                     ticket_id, ticket.quantity - ticket_sold_count
189                 ),
190             )
191 
192         for _ in range(1, quantity):
193             ticket_holder = TicketHolder(
194                 firstname='onsite',
195                 lastname='attendee',
196                 email='example@example.com',
197                 ticket_id=ticket.id,
198                 event_id=data.get('event'),
199             )
200             save_to_db(ticket_holder)
201             data['ticket_holders'].append(ticket_holder.id)
202 
203     # delete from the data.
204     del data['on_site_tickets']
205 
206 
207 def calculate_order_amount(tickets, discount_code=None):
208     from app.api.helpers.ticketing import validate_discount_code, validate_tickets
209     from app.models.discount_code import DiscountCode
210 
211     ticket_ids = {ticket['id'] for ticket in tickets}
212     ticket_map = {int(ticket['id']): ticket for ticket in tickets}
213     fetched_tickets = validate_tickets(ticket_ids)
214 
215     if tickets and discount_code:
216         discount_code = validate_discount_code(discount_code, tickets=tickets)
217 
218     event = tax = tax_included = fees = None
219     total_amount = total_tax = total_discount = 0.0
220     ticket_list = []
221     for ticket in fetched_tickets:
222         ticket_tax = discounted_tax = 0.0
223         ticket_info = ticket_map[ticket.id]
224         discount_amount = 0.0
225         discount_data = None
226         ticket_fee = 0.0
227 
228         quantity = ticket_info.get('quantity', 1)  # Default to single ticket
229         if not event:
230             event = ticket.event
231 
232             if event.deleted_at:
233                 raise ObjectNotFound(
234                     {'pointer': 'tickets/event'}, f'Event: {event.id} not found'
235                 )
236 
237             fees = TicketFees.query.filter_by(currency=event.payment_currency).first()
238 
239         if not tax and event.tax:
240             tax = event.tax
241             tax_included = tax.is_tax_included_in_price
242 
243         if ticket.type == 'donation':
244             price = ticket_info.get('price')
245             if not price or price > ticket.max_price or price < ticket.min_price:
246                 raise UnprocessableEntityError(
247                     {'pointer': 'tickets/price'},
248                     f"Price for donation ticket should be present and within range "
249                     f"{ticket.min_price} to {ticket.max_price}",
250                 )
251         else:
252             price = ticket.price if ticket.type != 'free' else 0.0
253 
254         if tax:
255             if tax_included:
256                 ticket_tax = price - price / (1 + tax.rate / 100)
257             else:
258                 ticket_tax = price * tax.rate / 100
259 
260         if discount_code and ticket.type != 'free':
261             code = (
262                 DiscountCode.query.with_parent(ticket)
263                 .filter_by(id=discount_code.id)
264                 .first()
265             )
266             if code:
267                 if discount_code.id == code.id:
268                     if code.type == 'amount':
269                         discount_amount = min(code.value, price)
270                         discount_percent = (discount_amount / price) * 100
271                         if tax:
272                             if tax_included:
273                                 discounted_tax = (price - discount_amount) - (price - discount_amount) / (1 + tax.rate / 100)
274                             else:
275                                 discounted_tax = (price - discount_amount) * tax.rate / 100
276                     else:
277                         discount_amount = (price * code.value) / 100
278                         if tax:
279                             discounted_tax = ticket_tax - (ticket_tax * code.value / 100)
280                         discount_percent = code.value
281                     discount_data = {
282                         'code': discount_code.code,
283                         'percent': round(discount_percent, 2),
284                         'amount': round(discount_amount, 2),
285                         'total': round(discount_amount * quantity, 2),
286                     }
287 
288         total_discount += round(discount_amount * quantity, 2)
289         if fees and not ticket.is_fee_absorbed:
290             ticket_fee = fees.service_fee * (price * quantity) / 100
291             if ticket_fee > fees.maximum_fee:
292                 ticket_fee = fees.maximum_fee
293         sub_total = ticket_fee + (price - discount_amount) * quantity
294         total_amount = total_amount + sub_total
295         ticket_list.append(
296             {
297                 'id': ticket.id,
298                 'name': ticket.name,
299                 'price': price,
300                 'quantity': quantity,
301                 'discount': discount_data,
302                 'ticket_fee': round(ticket_fee, 2),
303                 'sub_total': round(sub_total, 2),
304                 'ticket_tax': round(ticket_tax, 2),
305                 'discounted_tax': round(discounted_tax, 2)
306             }
307         )
308 
309     sub_total = total_amount
310     tax_dict = None
311     if tax:
312         if tax_included:
313             total_tax = total_amount - total_amount / (1 + tax.rate / 100)
314         else:
315             total_tax = total_amount * tax.rate / 100
316             total_amount += total_tax
317         tax_dict = dict(
318             included=tax_included,
319             amount=round(total_tax, 2),
320             percent=tax.rate if tax else 0.0,
321             name=tax.name,
322         )
323 
324     return dict(
325         tax=tax_dict,
326         sub_total=round(sub_total, 2),
327         total=round(total_amount, 2),
328         discount=round(total_discount, 2),
329         tickets=ticket_list,
330     )
331 
332 
333 def on_order_completed(order):
334     # send e-mail and notifications if the order status is completed
335     if not (order.status == 'completed' or order.status == 'placed'):
336         return
337 
338     create_pdf_tickets_for_holder(order)
339 
340     # send email and notifications.
341     send_email_to_attendees(order)
342     notify_ticket_purchase_attendee(order)
343 
344     if order.payment_mode in ['free', 'bank', 'cheque', 'onsite']:
345         order.completed_at = datetime.utcnow()
346 
347     organizer_set = set(
348         filter(
349             bool, order.event.organizers + order.event.coorganizers + [order.event.owner]
350         )
351     )
352     send_order_purchase_organizer_email(order, organizer_set)
353     notify_ticket_purchase_organizer(order)
354
```
--- 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
@@ -283,6 +283,7 @@
                         'percent': round(discount_percent, 2),
                         'amount': round(discount_amount, 2),
                         'total': round(discount_amount * quantity, 2),
+                        'type': code.type
                     }
 
         total_discount += round(discount_amount * quantity, 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@@ -283,6 +283,7 @@\n                         'percent': round(discount_percent, 2),\n                         'amount': round(discount_amount, 2),\n                         'total': round(discount_amount * quantity, 2),\n+                        'type': code.type\n                     }\n \n         total_discount += round(discount_amount * quantity, 2)\n", "issue": "PDF 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        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                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                        if tax:\n                            if tax_included:\n                                discounted_tax = (price - discount_amount) - (price - discount_amount) / (1 + tax.rate / 100)\n                            else:\n                                discounted_tax = (price - discount_amount) * tax.rate / 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"}], "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        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                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                        if tax:\n                            if tax_included:\n                                discounted_tax = (price - discount_amount) - (price - discount_amount) / (1 + tax.rate / 100)\n                            else:\n                                discounted_tax = (price - discount_amount) * tax.rate / 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                        'type': code.type\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"}]}