We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Bug: Giftcard created event is not triggered when gift card is bought 
### What are you trying to achieve?

I'm trying to inform the external system about the creation of the gift card. 

### Steps to reproduce the problem

1. Create a Product type of Gift card
2. Create gift card -> Product
3. Create webhook which triggers on gift card creation 
4. Create draft order and add newly gift card 
5. Finalize draft order 

### What did you expect to happen?

Saleor should properly send a webhook after the gift card is created.

### Logs

_No response_

### Environment

Saleor version: 3.10


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `saleor/giftcard/utils.py`
Content:
```
1 from collections import defaultdict
2 from datetime import date
3 from typing import TYPE_CHECKING, DefaultDict, Iterable, List, Optional
4 from uuid import UUID
5 
6 from dateutil.relativedelta import relativedelta
7 from django.db import transaction
8 from django.db.models.expressions import Exists, OuterRef
9 from django.utils import timezone
10 
11 from ..checkout.models import Checkout
12 from ..core.exceptions import GiftCardNotApplicable
13 from ..core.tracing import traced_atomic_transaction
14 from ..core.utils.promo_code import InvalidPromoCode, generate_promo_code
15 from ..order.actions import OrderFulfillmentLineInfo, create_fulfillments
16 from ..order.models import OrderLine
17 from ..site import GiftCardSettingsExpiryType
18 from . import GiftCardEvents, GiftCardLineData, events
19 from .models import GiftCard, GiftCardEvent
20 from .notifications import send_gift_card_notification
21 
22 if TYPE_CHECKING:
23     from django.db.models import QuerySet
24 
25     from ..account.models import User
26     from ..app.models import App
27     from ..order.models import Order
28     from ..plugins.manager import PluginsManager
29     from ..site.models import SiteSettings
30 
31 
32 def add_gift_card_code_to_checkout(
33     checkout: Checkout, email: str, promo_code: str, currency: str
34 ):
35     """Add gift card data to checkout by code.
36 
37     Raise ValidationError if email is not provided.
38     Raise InvalidPromoCode if gift card cannot be applied.
39     """
40     from ..checkout.checkout_cleaner import validate_checkout_email
41 
42     validate_checkout_email(checkout)
43 
44     try:
45         # only active gift card with currency the same as channel currency can be used
46         gift_card = (
47             GiftCard.objects.active(date=date.today())
48             .filter(currency=currency)
49             .get(code=promo_code)
50         )
51     except GiftCard.DoesNotExist:
52         raise InvalidPromoCode()
53 
54     used_by_email = gift_card.used_by_email
55     # gift card can be used only by one user
56     if used_by_email and used_by_email != email:
57         raise InvalidPromoCode()
58 
59     checkout.gift_cards.add(gift_card)
60     checkout.save(update_fields=["last_change"])
61 
62 
63 def remove_gift_card_code_from_checkout(checkout: Checkout, gift_card_code: str):
64     """Remove gift card data from checkout by code.
65 
66     Return information whether promo code was removed.
67     """
68     gift_card = checkout.gift_cards.filter(code=gift_card_code).first()
69     if gift_card:
70         checkout.gift_cards.remove(gift_card)
71         checkout.save(update_fields=["last_change"])
72         return True
73     return False
74 
75 
76 def deactivate_gift_card(gift_card: GiftCard):
77     """Set gift card status as inactive."""
78     if gift_card.is_active:
79         gift_card.is_active = False
80         gift_card.save(update_fields=["is_active"])
81 
82 
83 def activate_gift_card(gift_card: GiftCard):
84     """Set gift card status as active."""
85     if not gift_card.is_active:
86         gift_card.is_active = True
87         gift_card.save(update_fields=["is_active"])
88 
89 
90 def fulfill_non_shippable_gift_cards(
91     order: "Order",
92     order_lines: Iterable[OrderLine],
93     settings: "SiteSettings",
94     requestor_user: Optional["User"],
95     app: Optional["App"],
96     manager: "PluginsManager",
97 ):
98     gift_card_lines = get_non_shippable_gift_card_lines(order_lines)
99     if not gift_card_lines:
100         return
101     fulfill_gift_card_lines(
102         gift_card_lines, requestor_user, app, order, settings, manager
103     )
104 
105 
106 def get_non_shippable_gift_card_lines(lines: Iterable[OrderLine]) -> "QuerySet":
107     gift_card_lines = get_gift_card_lines(lines)
108     non_shippable_lines = OrderLine.objects.filter(
109         id__in=[line.pk for line in gift_card_lines], is_shipping_required=False
110     )
111     return non_shippable_lines
112 
113 
114 def get_gift_card_lines(lines: Iterable[OrderLine]):
115     gift_card_lines = [line for line in lines if line.is_gift_card]
116     return gift_card_lines
117 
118 
119 def fulfill_gift_card_lines(
120     gift_card_lines: "QuerySet",
121     requestor_user: Optional["User"],
122     app: Optional["App"],
123     order: "Order",
124     settings: "SiteSettings",
125     manager: "PluginsManager",
126 ):
127     lines_for_warehouses: DefaultDict[
128         UUID, List[OrderFulfillmentLineInfo]
129     ] = defaultdict(list)
130     channel_slug = order.channel.slug
131     for line in gift_card_lines.prefetch_related(
132         "allocations__stock", "variant__stocks"
133     ):
134         if allocations := line.allocations.all():
135             for allocation in allocations:
136                 quantity = allocation.quantity_allocated
137                 if quantity > 0:
138                     warehouse_pk = allocation.stock.warehouse_id
139                     lines_for_warehouses[warehouse_pk].append(
140                         {"order_line": line, "quantity": quantity}
141                     )
142         else:
143             stock = line.variant.stocks.for_channel_and_country(channel_slug).first()
144             if not stock:
145                 raise GiftCardNotApplicable(
146                     message="Lack of gift card stock for checkout channel.",
147                 )
148             warehouse_pk = stock.warehouse_id
149             lines_for_warehouses[warehouse_pk].append(
150                 {"order_line": line, "quantity": line.quantity}
151             )
152 
153     return create_fulfillments(
154         requestor_user,
155         app,
156         order,
157         dict(lines_for_warehouses),
158         manager,
159         settings,
160         notify_customer=True,
161     )
162 
163 
164 @traced_atomic_transaction()
165 def gift_cards_create(
166     order: "Order",
167     gift_card_lines_info: Iterable["GiftCardLineData"],
168     settings: "SiteSettings",
169     requestor_user: Optional["User"],
170     app: Optional["App"],
171     manager: "PluginsManager",
172 ):
173     """Create purchased gift cards."""
174     customer_user = order.user
175     user_email = order.user_email
176     gift_cards = []
177     non_shippable_gift_cards = []
178     expiry_date = calculate_expiry_date(settings)
179     for line_data in gift_card_lines_info:
180         order_line = line_data.order_line
181         price = order_line.unit_price_gross
182         line_gift_cards = [
183             GiftCard(  # type: ignore[misc] # see below:
184                 code=generate_promo_code(),
185                 initial_balance=price,  # money field not supported by mypy_django_plugin # noqa: E501
186                 current_balance=price,  # money field not supported by mypy_django_plugin # noqa: E501
187                 created_by=customer_user,
188                 created_by_email=user_email,
189                 product=line_data.variant.product if line_data.variant else None,
190                 fulfillment_line=line_data.fulfillment_line,
191                 expiry_date=expiry_date,
192             )
193             for _ in range(line_data.quantity)
194         ]
195         gift_cards.extend(line_gift_cards)
196         if not order_line.is_shipping_required:
197             non_shippable_gift_cards.extend(line_gift_cards)
198 
199     gift_cards = GiftCard.objects.bulk_create(gift_cards)
200     events.gift_cards_bought_event(gift_cards, order, requestor_user, app)
201 
202     channel_slug = order.channel.slug
203     # send to customer all non-shippable gift cards
204     transaction.on_commit(
205         lambda: send_gift_cards_to_customer(
206             non_shippable_gift_cards,
207             user_email,
208             requestor_user,
209             app,
210             customer_user,
211             manager,
212             channel_slug,
213         )
214     )
215     return gift_cards
216 
217 
218 def calculate_expiry_date(settings):
219     """Calculate expiry date based on gift card settings."""
220     today = timezone.now().date()
221     expiry_date = None
222     if settings.gift_card_expiry_type == GiftCardSettingsExpiryType.EXPIRY_PERIOD:
223         expiry_period_type = settings.gift_card_expiry_period_type
224         time_delta = {f"{expiry_period_type}s": settings.gift_card_expiry_period}
225         expiry_date = today + relativedelta(**time_delta)
226     return expiry_date
227 
228 
229 def send_gift_cards_to_customer(
230     gift_cards: Iterable[GiftCard],
231     user_email: str,
232     requestor_user: Optional["User"],
233     app: Optional["App"],
234     customer_user: Optional["User"],
235     manager: "PluginsManager",
236     channel_slug: str,
237 ):
238     for gift_card in gift_cards:
239         send_gift_card_notification(
240             requestor_user,
241             app,
242             customer_user,
243             user_email,
244             gift_card,
245             manager,
246             channel_slug,
247             resending=False,
248         )
249 
250 
251 def deactivate_order_gift_cards(
252     order_id: UUID, user: Optional["User"], app: Optional["App"]
253 ):
254     gift_card_events = GiftCardEvent.objects.filter(
255         type=GiftCardEvents.BOUGHT, order_id=order_id
256     )
257     gift_cards = GiftCard.objects.filter(
258         Exists(gift_card_events.filter(gift_card_id=OuterRef("id")))
259     )
260     gift_cards.update(is_active=False)
261     events.gift_cards_deactivated_event(
262         gift_cards.values_list("id", flat=True), user, app
263     )
264 
265 
266 def order_has_gift_card_lines(order):
267     return any(order.lines.filter(is_gift_card=True))
268 
269 
270 def assign_user_gift_cards(user):
271     GiftCard.objects.filter(used_by_email=user.email).update(used_by=user)
272     GiftCard.objects.filter(created_by_email=user.email).update(created_by=user)
273 
274 
275 def is_gift_card_expired(gift_card: GiftCard):
276     """Return True when gift card expiry date pass."""
277     today = timezone.now().date()
278     return bool(gift_card.expiry_date) and gift_card.expiry_date < today  # type: ignore
279
```
--- 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/saleor/giftcard/utils.py b/saleor/giftcard/utils.py
--- a/saleor/giftcard/utils.py
+++ b/saleor/giftcard/utils.py
@@ -11,6 +11,7 @@
 from ..checkout.models import Checkout
 from ..core.exceptions import GiftCardNotApplicable
 from ..core.tracing import traced_atomic_transaction
+from ..core.utils.events import call_event
 from ..core.utils.promo_code import InvalidPromoCode, generate_promo_code
 from ..order.actions import OrderFulfillmentLineInfo, create_fulfillments
 from ..order.models import OrderLine
@@ -199,6 +200,9 @@
     gift_cards = GiftCard.objects.bulk_create(gift_cards)
     events.gift_cards_bought_event(gift_cards, order, requestor_user, app)
 
+    for gift_card in gift_cards:
+        call_event(manager.gift_card_created, gift_card)
+
     channel_slug = order.channel.slug
     # send to customer all non-shippable gift cards
     transaction.on_commit(

{"golden_diff": "diff --git a/saleor/giftcard/utils.py b/saleor/giftcard/utils.py\n--- a/saleor/giftcard/utils.py\n+++ b/saleor/giftcard/utils.py\n@@ -11,6 +11,7 @@\n from ..checkout.models import Checkout\n from ..core.exceptions import GiftCardNotApplicable\n from ..core.tracing import traced_atomic_transaction\n+from ..core.utils.events import call_event\n from ..core.utils.promo_code import InvalidPromoCode, generate_promo_code\n from ..order.actions import OrderFulfillmentLineInfo, create_fulfillments\n from ..order.models import OrderLine\n@@ -199,6 +200,9 @@\n     gift_cards = GiftCard.objects.bulk_create(gift_cards)\n     events.gift_cards_bought_event(gift_cards, order, requestor_user, app)\n \n+    for gift_card in gift_cards:\n+        call_event(manager.gift_card_created, gift_card)\n+\n     channel_slug = order.channel.slug\n     # send to customer all non-shippable gift cards\n     transaction.on_commit(\n", "issue": "Bug: Giftcard created event is not triggered when gift card is bought \n### What are you trying to achieve?\n\nI'm trying to inform the external system about the creation of the gift card. \n\n### Steps to reproduce the problem\n\n1. Create a Product type of Gift card\r\n2. Create gift card -> Product\r\n3. Create webhook which triggers on gift card creation \r\n4. Create draft order and add newly gift card \r\n5. Finalize draft order \n\n### What did you expect to happen?\n\nSaleor should properly send a webhook after the gift card is created.\n\n### Logs\n\n_No response_\n\n### Environment\n\nSaleor version: 3.10\r\n\n", "before_files": [{"content": "from collections import defaultdict\nfrom datetime import date\nfrom typing import TYPE_CHECKING, DefaultDict, Iterable, List, Optional\nfrom uuid import UUID\n\nfrom dateutil.relativedelta import relativedelta\nfrom django.db import transaction\nfrom django.db.models.expressions import Exists, OuterRef\nfrom django.utils import timezone\n\nfrom ..checkout.models import Checkout\nfrom ..core.exceptions import GiftCardNotApplicable\nfrom ..core.tracing import traced_atomic_transaction\nfrom ..core.utils.promo_code import InvalidPromoCode, generate_promo_code\nfrom ..order.actions import OrderFulfillmentLineInfo, create_fulfillments\nfrom ..order.models import OrderLine\nfrom ..site import GiftCardSettingsExpiryType\nfrom . import GiftCardEvents, GiftCardLineData, events\nfrom .models import GiftCard, GiftCardEvent\nfrom .notifications import send_gift_card_notification\n\nif TYPE_CHECKING:\n    from django.db.models import QuerySet\n\n    from ..account.models import User\n    from ..app.models import App\n    from ..order.models import Order\n    from ..plugins.manager import PluginsManager\n    from ..site.models import SiteSettings\n\n\ndef add_gift_card_code_to_checkout(\n    checkout: Checkout, email: str, promo_code: str, currency: str\n):\n    \"\"\"Add gift card data to checkout by code.\n\n    Raise ValidationError if email is not provided.\n    Raise InvalidPromoCode if gift card cannot be applied.\n    \"\"\"\n    from ..checkout.checkout_cleaner import validate_checkout_email\n\n    validate_checkout_email(checkout)\n\n    try:\n        # only active gift card with currency the same as channel currency can be used\n        gift_card = (\n            GiftCard.objects.active(date=date.today())\n            .filter(currency=currency)\n            .get(code=promo_code)\n        )\n    except GiftCard.DoesNotExist:\n        raise InvalidPromoCode()\n\n    used_by_email = gift_card.used_by_email\n    # gift card can be used only by one user\n    if used_by_email and used_by_email != email:\n        raise InvalidPromoCode()\n\n    checkout.gift_cards.add(gift_card)\n    checkout.save(update_fields=[\"last_change\"])\n\n\ndef remove_gift_card_code_from_checkout(checkout: Checkout, gift_card_code: str):\n    \"\"\"Remove gift card data from checkout by code.\n\n    Return information whether promo code was removed.\n    \"\"\"\n    gift_card = checkout.gift_cards.filter(code=gift_card_code).first()\n    if gift_card:\n        checkout.gift_cards.remove(gift_card)\n        checkout.save(update_fields=[\"last_change\"])\n        return True\n    return False\n\n\ndef deactivate_gift_card(gift_card: GiftCard):\n    \"\"\"Set gift card status as inactive.\"\"\"\n    if gift_card.is_active:\n        gift_card.is_active = False\n        gift_card.save(update_fields=[\"is_active\"])\n\n\ndef activate_gift_card(gift_card: GiftCard):\n    \"\"\"Set gift card status as active.\"\"\"\n    if not gift_card.is_active:\n        gift_card.is_active = True\n        gift_card.save(update_fields=[\"is_active\"])\n\n\ndef fulfill_non_shippable_gift_cards(\n    order: \"Order\",\n    order_lines: Iterable[OrderLine],\n    settings: \"SiteSettings\",\n    requestor_user: Optional[\"User\"],\n    app: Optional[\"App\"],\n    manager: \"PluginsManager\",\n):\n    gift_card_lines = get_non_shippable_gift_card_lines(order_lines)\n    if not gift_card_lines:\n        return\n    fulfill_gift_card_lines(\n        gift_card_lines, requestor_user, app, order, settings, manager\n    )\n\n\ndef get_non_shippable_gift_card_lines(lines: Iterable[OrderLine]) -> \"QuerySet\":\n    gift_card_lines = get_gift_card_lines(lines)\n    non_shippable_lines = OrderLine.objects.filter(\n        id__in=[line.pk for line in gift_card_lines], is_shipping_required=False\n    )\n    return non_shippable_lines\n\n\ndef get_gift_card_lines(lines: Iterable[OrderLine]):\n    gift_card_lines = [line for line in lines if line.is_gift_card]\n    return gift_card_lines\n\n\ndef fulfill_gift_card_lines(\n    gift_card_lines: \"QuerySet\",\n    requestor_user: Optional[\"User\"],\n    app: Optional[\"App\"],\n    order: \"Order\",\n    settings: \"SiteSettings\",\n    manager: \"PluginsManager\",\n):\n    lines_for_warehouses: DefaultDict[\n        UUID, List[OrderFulfillmentLineInfo]\n    ] = defaultdict(list)\n    channel_slug = order.channel.slug\n    for line in gift_card_lines.prefetch_related(\n        \"allocations__stock\", \"variant__stocks\"\n    ):\n        if allocations := line.allocations.all():\n            for allocation in allocations:\n                quantity = allocation.quantity_allocated\n                if quantity > 0:\n                    warehouse_pk = allocation.stock.warehouse_id\n                    lines_for_warehouses[warehouse_pk].append(\n                        {\"order_line\": line, \"quantity\": quantity}\n                    )\n        else:\n            stock = line.variant.stocks.for_channel_and_country(channel_slug).first()\n            if not stock:\n                raise GiftCardNotApplicable(\n                    message=\"Lack of gift card stock for checkout channel.\",\n                )\n            warehouse_pk = stock.warehouse_id\n            lines_for_warehouses[warehouse_pk].append(\n                {\"order_line\": line, \"quantity\": line.quantity}\n            )\n\n    return create_fulfillments(\n        requestor_user,\n        app,\n        order,\n        dict(lines_for_warehouses),\n        manager,\n        settings,\n        notify_customer=True,\n    )\n\n\n@traced_atomic_transaction()\ndef gift_cards_create(\n    order: \"Order\",\n    gift_card_lines_info: Iterable[\"GiftCardLineData\"],\n    settings: \"SiteSettings\",\n    requestor_user: Optional[\"User\"],\n    app: Optional[\"App\"],\n    manager: \"PluginsManager\",\n):\n    \"\"\"Create purchased gift cards.\"\"\"\n    customer_user = order.user\n    user_email = order.user_email\n    gift_cards = []\n    non_shippable_gift_cards = []\n    expiry_date = calculate_expiry_date(settings)\n    for line_data in gift_card_lines_info:\n        order_line = line_data.order_line\n        price = order_line.unit_price_gross\n        line_gift_cards = [\n            GiftCard(  # type: ignore[misc] # see below:\n                code=generate_promo_code(),\n                initial_balance=price,  # money field not supported by mypy_django_plugin # noqa: E501\n                current_balance=price,  # money field not supported by mypy_django_plugin # noqa: E501\n                created_by=customer_user,\n                created_by_email=user_email,\n                product=line_data.variant.product if line_data.variant else None,\n                fulfillment_line=line_data.fulfillment_line,\n                expiry_date=expiry_date,\n            )\n            for _ in range(line_data.quantity)\n        ]\n        gift_cards.extend(line_gift_cards)\n        if not order_line.is_shipping_required:\n            non_shippable_gift_cards.extend(line_gift_cards)\n\n    gift_cards = GiftCard.objects.bulk_create(gift_cards)\n    events.gift_cards_bought_event(gift_cards, order, requestor_user, app)\n\n    channel_slug = order.channel.slug\n    # send to customer all non-shippable gift cards\n    transaction.on_commit(\n        lambda: send_gift_cards_to_customer(\n            non_shippable_gift_cards,\n            user_email,\n            requestor_user,\n            app,\n            customer_user,\n            manager,\n            channel_slug,\n        )\n    )\n    return gift_cards\n\n\ndef calculate_expiry_date(settings):\n    \"\"\"Calculate expiry date based on gift card settings.\"\"\"\n    today = timezone.now().date()\n    expiry_date = None\n    if settings.gift_card_expiry_type == GiftCardSettingsExpiryType.EXPIRY_PERIOD:\n        expiry_period_type = settings.gift_card_expiry_period_type\n        time_delta = {f\"{expiry_period_type}s\": settings.gift_card_expiry_period}\n        expiry_date = today + relativedelta(**time_delta)\n    return expiry_date\n\n\ndef send_gift_cards_to_customer(\n    gift_cards: Iterable[GiftCard],\n    user_email: str,\n    requestor_user: Optional[\"User\"],\n    app: Optional[\"App\"],\n    customer_user: Optional[\"User\"],\n    manager: \"PluginsManager\",\n    channel_slug: str,\n):\n    for gift_card in gift_cards:\n        send_gift_card_notification(\n            requestor_user,\n            app,\n            customer_user,\n            user_email,\n            gift_card,\n            manager,\n            channel_slug,\n            resending=False,\n        )\n\n\ndef deactivate_order_gift_cards(\n    order_id: UUID, user: Optional[\"User\"], app: Optional[\"App\"]\n):\n    gift_card_events = GiftCardEvent.objects.filter(\n        type=GiftCardEvents.BOUGHT, order_id=order_id\n    )\n    gift_cards = GiftCard.objects.filter(\n        Exists(gift_card_events.filter(gift_card_id=OuterRef(\"id\")))\n    )\n    gift_cards.update(is_active=False)\n    events.gift_cards_deactivated_event(\n        gift_cards.values_list(\"id\", flat=True), user, app\n    )\n\n\ndef order_has_gift_card_lines(order):\n    return any(order.lines.filter(is_gift_card=True))\n\n\ndef assign_user_gift_cards(user):\n    GiftCard.objects.filter(used_by_email=user.email).update(used_by=user)\n    GiftCard.objects.filter(created_by_email=user.email).update(created_by=user)\n\n\ndef is_gift_card_expired(gift_card: GiftCard):\n    \"\"\"Return True when gift card expiry date pass.\"\"\"\n    today = timezone.now().date()\n    return bool(gift_card.expiry_date) and gift_card.expiry_date < today  # type: ignore\n", "path": "saleor/giftcard/utils.py"}], "after_files": [{"content": "from collections import defaultdict\nfrom datetime import date\nfrom typing import TYPE_CHECKING, DefaultDict, Iterable, List, Optional\nfrom uuid import UUID\n\nfrom dateutil.relativedelta import relativedelta\nfrom django.db import transaction\nfrom django.db.models.expressions import Exists, OuterRef\nfrom django.utils import timezone\n\nfrom ..checkout.models import Checkout\nfrom ..core.exceptions import GiftCardNotApplicable\nfrom ..core.tracing import traced_atomic_transaction\nfrom ..core.utils.events import call_event\nfrom ..core.utils.promo_code import InvalidPromoCode, generate_promo_code\nfrom ..order.actions import OrderFulfillmentLineInfo, create_fulfillments\nfrom ..order.models import OrderLine\nfrom ..site import GiftCardSettingsExpiryType\nfrom . import GiftCardEvents, GiftCardLineData, events\nfrom .models import GiftCard, GiftCardEvent\nfrom .notifications import send_gift_card_notification\n\nif TYPE_CHECKING:\n    from django.db.models import QuerySet\n\n    from ..account.models import User\n    from ..app.models import App\n    from ..order.models import Order\n    from ..plugins.manager import PluginsManager\n    from ..site.models import SiteSettings\n\n\ndef add_gift_card_code_to_checkout(\n    checkout: Checkout, email: str, promo_code: str, currency: str\n):\n    \"\"\"Add gift card data to checkout by code.\n\n    Raise ValidationError if email is not provided.\n    Raise InvalidPromoCode if gift card cannot be applied.\n    \"\"\"\n    from ..checkout.checkout_cleaner import validate_checkout_email\n\n    validate_checkout_email(checkout)\n\n    try:\n        # only active gift card with currency the same as channel currency can be used\n        gift_card = (\n            GiftCard.objects.active(date=date.today())\n            .filter(currency=currency)\n            .get(code=promo_code)\n        )\n    except GiftCard.DoesNotExist:\n        raise InvalidPromoCode()\n\n    used_by_email = gift_card.used_by_email\n    # gift card can be used only by one user\n    if used_by_email and used_by_email != email:\n        raise InvalidPromoCode()\n\n    checkout.gift_cards.add(gift_card)\n    checkout.save(update_fields=[\"last_change\"])\n\n\ndef remove_gift_card_code_from_checkout(checkout: Checkout, gift_card_code: str):\n    \"\"\"Remove gift card data from checkout by code.\n\n    Return information whether promo code was removed.\n    \"\"\"\n    gift_card = checkout.gift_cards.filter(code=gift_card_code).first()\n    if gift_card:\n        checkout.gift_cards.remove(gift_card)\n        checkout.save(update_fields=[\"last_change\"])\n        return True\n    return False\n\n\ndef deactivate_gift_card(gift_card: GiftCard):\n    \"\"\"Set gift card status as inactive.\"\"\"\n    if gift_card.is_active:\n        gift_card.is_active = False\n        gift_card.save(update_fields=[\"is_active\"])\n\n\ndef activate_gift_card(gift_card: GiftCard):\n    \"\"\"Set gift card status as active.\"\"\"\n    if not gift_card.is_active:\n        gift_card.is_active = True\n        gift_card.save(update_fields=[\"is_active\"])\n\n\ndef fulfill_non_shippable_gift_cards(\n    order: \"Order\",\n    order_lines: Iterable[OrderLine],\n    settings: \"SiteSettings\",\n    requestor_user: Optional[\"User\"],\n    app: Optional[\"App\"],\n    manager: \"PluginsManager\",\n):\n    gift_card_lines = get_non_shippable_gift_card_lines(order_lines)\n    if not gift_card_lines:\n        return\n    fulfill_gift_card_lines(\n        gift_card_lines, requestor_user, app, order, settings, manager\n    )\n\n\ndef get_non_shippable_gift_card_lines(lines: Iterable[OrderLine]) -> \"QuerySet\":\n    gift_card_lines = get_gift_card_lines(lines)\n    non_shippable_lines = OrderLine.objects.filter(\n        id__in=[line.pk for line in gift_card_lines], is_shipping_required=False\n    )\n    return non_shippable_lines\n\n\ndef get_gift_card_lines(lines: Iterable[OrderLine]):\n    gift_card_lines = [line for line in lines if line.is_gift_card]\n    return gift_card_lines\n\n\ndef fulfill_gift_card_lines(\n    gift_card_lines: \"QuerySet\",\n    requestor_user: Optional[\"User\"],\n    app: Optional[\"App\"],\n    order: \"Order\",\n    settings: \"SiteSettings\",\n    manager: \"PluginsManager\",\n):\n    lines_for_warehouses: DefaultDict[\n        UUID, List[OrderFulfillmentLineInfo]\n    ] = defaultdict(list)\n    channel_slug = order.channel.slug\n    for line in gift_card_lines.prefetch_related(\n        \"allocations__stock\", \"variant__stocks\"\n    ):\n        if allocations := line.allocations.all():\n            for allocation in allocations:\n                quantity = allocation.quantity_allocated\n                if quantity > 0:\n                    warehouse_pk = allocation.stock.warehouse_id\n                    lines_for_warehouses[warehouse_pk].append(\n                        {\"order_line\": line, \"quantity\": quantity}\n                    )\n        else:\n            stock = line.variant.stocks.for_channel_and_country(channel_slug).first()\n            if not stock:\n                raise GiftCardNotApplicable(\n                    message=\"Lack of gift card stock for checkout channel.\",\n                )\n            warehouse_pk = stock.warehouse_id\n            lines_for_warehouses[warehouse_pk].append(\n                {\"order_line\": line, \"quantity\": line.quantity}\n            )\n\n    return create_fulfillments(\n        requestor_user,\n        app,\n        order,\n        dict(lines_for_warehouses),\n        manager,\n        settings,\n        notify_customer=True,\n    )\n\n\n@traced_atomic_transaction()\ndef gift_cards_create(\n    order: \"Order\",\n    gift_card_lines_info: Iterable[\"GiftCardLineData\"],\n    settings: \"SiteSettings\",\n    requestor_user: Optional[\"User\"],\n    app: Optional[\"App\"],\n    manager: \"PluginsManager\",\n):\n    \"\"\"Create purchased gift cards.\"\"\"\n    customer_user = order.user\n    user_email = order.user_email\n    gift_cards = []\n    non_shippable_gift_cards = []\n    expiry_date = calculate_expiry_date(settings)\n    for line_data in gift_card_lines_info:\n        order_line = line_data.order_line\n        price = order_line.unit_price_gross\n        line_gift_cards = [\n            GiftCard(  # type: ignore[misc] # see below:\n                code=generate_promo_code(),\n                initial_balance=price,  # money field not supported by mypy_django_plugin # noqa: E501\n                current_balance=price,  # money field not supported by mypy_django_plugin # noqa: E501\n                created_by=customer_user,\n                created_by_email=user_email,\n                product=line_data.variant.product if line_data.variant else None,\n                fulfillment_line=line_data.fulfillment_line,\n                expiry_date=expiry_date,\n            )\n            for _ in range(line_data.quantity)\n        ]\n        gift_cards.extend(line_gift_cards)\n        if not order_line.is_shipping_required:\n            non_shippable_gift_cards.extend(line_gift_cards)\n\n    gift_cards = GiftCard.objects.bulk_create(gift_cards)\n    events.gift_cards_bought_event(gift_cards, order, requestor_user, app)\n\n    for gift_card in gift_cards:\n        call_event(manager.gift_card_created, gift_card)\n\n    channel_slug = order.channel.slug\n    # send to customer all non-shippable gift cards\n    transaction.on_commit(\n        lambda: send_gift_cards_to_customer(\n            non_shippable_gift_cards,\n            user_email,\n            requestor_user,\n            app,\n            customer_user,\n            manager,\n            channel_slug,\n        )\n    )\n    return gift_cards\n\n\ndef calculate_expiry_date(settings):\n    \"\"\"Calculate expiry date based on gift card settings.\"\"\"\n    today = timezone.now().date()\n    expiry_date = None\n    if settings.gift_card_expiry_type == GiftCardSettingsExpiryType.EXPIRY_PERIOD:\n        expiry_period_type = settings.gift_card_expiry_period_type\n        time_delta = {f\"{expiry_period_type}s\": settings.gift_card_expiry_period}\n        expiry_date = today + relativedelta(**time_delta)\n    return expiry_date\n\n\ndef send_gift_cards_to_customer(\n    gift_cards: Iterable[GiftCard],\n    user_email: str,\n    requestor_user: Optional[\"User\"],\n    app: Optional[\"App\"],\n    customer_user: Optional[\"User\"],\n    manager: \"PluginsManager\",\n    channel_slug: str,\n):\n    for gift_card in gift_cards:\n        send_gift_card_notification(\n            requestor_user,\n            app,\n            customer_user,\n            user_email,\n            gift_card,\n            manager,\n            channel_slug,\n            resending=False,\n        )\n\n\ndef deactivate_order_gift_cards(\n    order_id: UUID, user: Optional[\"User\"], app: Optional[\"App\"]\n):\n    gift_card_events = GiftCardEvent.objects.filter(\n        type=GiftCardEvents.BOUGHT, order_id=order_id\n    )\n    gift_cards = GiftCard.objects.filter(\n        Exists(gift_card_events.filter(gift_card_id=OuterRef(\"id\")))\n    )\n    gift_cards.update(is_active=False)\n    events.gift_cards_deactivated_event(\n        gift_cards.values_list(\"id\", flat=True), user, app\n    )\n\n\ndef order_has_gift_card_lines(order):\n    return any(order.lines.filter(is_gift_card=True))\n\n\ndef assign_user_gift_cards(user):\n    GiftCard.objects.filter(used_by_email=user.email).update(used_by=user)\n    GiftCard.objects.filter(created_by_email=user.email).update(created_by=user)\n\n\ndef is_gift_card_expired(gift_card: GiftCard):\n    \"\"\"Return True when gift card expiry date pass.\"\"\"\n    today = timezone.now().date()\n    return bool(gift_card.expiry_date) and gift_card.expiry_date < today  # type: ignore\n", "path": "saleor/giftcard/utils.py"}]}