Repository: saleor/saleor

Issue:
Having trouble with emails during order checkout
### What I'm trying to achieve
…
Email sent with the order summary

### Steps to reproduce the problem
1. Register a new acc.
2. Pick desired items in the store.
3. Checkout with valid parameters.
4. Hit place order button.

### What I expected to happen
…
Celery worker sends email with order, instead it raises exception.


### Screenshots
<!-- If applicable, add screenshots to help explain your problem. -->
![image](https://user-images.githubusercontent.com/58126707/78720154-2a487880-78f3-11ea-9ed9-f2d857b6b748.png)

**System information**
Operating system: Windows 10



Discussion:
It's a race condition in transaction concurrency, Celery worker picks the task before the transaction that created it is committed. @tomaszszymanski129 reassigning to myself as I already have a fix.

Resolving patch:
diff --git a/saleor/checkout/utils.py b/saleor/checkout/utils.py
--- a/saleor/checkout/utils.py
+++ b/saleor/checkout/utils.py
@@ -758,8 +758,12 @@ def create_order(
     order_created(order=order, user=user)
 
     # Send the order confirmation email
-    send_order_confirmation.delay(order.pk, redirect_url, user.pk)
-    send_staff_order_confirmation.delay(order.pk, redirect_url)
+    transaction.on_commit(
+        lambda: send_order_confirmation.delay(order.pk, redirect_url, user.pk)
+    )
+    transaction.on_commit(
+        lambda: send_staff_order_confirmation.delay(order.pk, redirect_url)
+    )
 
     return order
 


Test patch:
diff --git a/tests/api/benchmark/test_checkout_mutations.py b/tests/api/benchmark/test_checkout_mutations.py
--- a/tests/api/benchmark/test_checkout_mutations.py
+++ b/tests/api/benchmark/test_checkout_mutations.py
@@ -3,7 +3,8 @@
 
 from saleor.checkout import calculations
 from saleor.checkout.models import Checkout
-from tests.api.utils import get_graphql_content
+
+from ..utils import get_graphql_content
 
 FRAGMENT_PRICE = """
     fragment Price on TaxedMoney {
diff --git a/tests/api/conftest.py b/tests/api/conftest.py
--- a/tests/api/conftest.py
+++ b/tests/api/conftest.py
@@ -10,6 +10,7 @@
 
 from saleor.account.models import ServiceAccount, User
 
+from ..utils import flush_post_commit_hooks
 from .utils import assert_no_permission
 
 API_PATH = reverse("api")
@@ -91,7 +92,9 @@ def post_graphql(
                 self.service_account.permissions.add(*permissions)
             else:
                 self.user.user_permissions.add(*permissions)
-        return super().post(API_PATH, data, **kwargs)
+        result = super().post(API_PATH, data, **kwargs)
+        flush_post_commit_hooks()
+        return result
 
     def post_multipart(self, *args, permissions=None, **kwargs):
         """Send a multipart POST request.
diff --git a/tests/test_checkout.py b/tests/test_checkout.py
--- a/tests/test_checkout.py
+++ b/tests/test_checkout.py
@@ -37,6 +37,8 @@
 from saleor.order.models import OrderEvent
 from saleor.shipping.models import ShippingZone
 
+from .utils import flush_post_commit_hooks
+
 
 def test_is_valid_shipping_method(checkout_with_item, address, shipping_zone):
     checkout = checkout_with_item
@@ -111,6 +113,7 @@ def test_create_order_creates_expected_events(
         user=customer_user if not is_anonymous_user else AnonymousUser(),
         redirect_url="https://www.example.com",
     )
+    flush_post_commit_hooks()
 
     # Ensure only two events were created, and retrieve them
     placement_event, email_sent_event = order.events.all()  # type: OrderEvent
diff --git a/tests/utils.py b/tests/utils.py
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -3,6 +3,7 @@
 from urllib.parse import urlparse
 
 from django.core.files.uploadedfile import SimpleUploadedFile
+from django.db import connections, transaction
 from PIL import Image
 from prices import Money
 
@@ -28,7 +29,6 @@ def create_image(image_name="product2"):
     img_data = BytesIO()
     image = Image.new("RGB", size=(1, 1), color=(255, 0, 0, 0))
     image.save(img_data, format="JPEG")
-    image_name = image_name
     image = SimpleUploadedFile(image_name + ".jpg", img_data.getvalue(), "image/png")
     return image, image_name
 
@@ -44,14 +44,16 @@ def money(amount):
 
 
 def generate_attribute_map(obj: Union[Product, ProductVariant]) -> Dict[int, Set[int]]:
-    """Generate a map from a product or variant instance, useful to quickly compare
-    the assigned attribute values against expected IDs.
+    """Generate a map from a product or variant instance.
+
+    Useful to quickly compare the assigned attribute values against expected IDs.
 
     The below association map will be returned.
-    {
-      attribute_pk (int) => {attribute_value_pk (int), ...}
-      ...
-    }
+
+        {
+            attribute_pk (int) => {attribute_value_pk (int), ...}
+            ...
+        }
     """
 
     qs = obj.attributes.select_related("assignment__attribute")
@@ -61,3 +63,16 @@ def generate_attribute_map(obj: Union[Product, ProductVariant]) -> Dict[int, Set
         assignment.attribute.pk: {value.pk for value in assignment.values.all()}
         for assignment in qs
     }
+
+
+def flush_post_commit_hooks():
+    """Run all pending `transaction.on_commit()` callbacks.
+
+    Forces all `on_commit()` hooks to run even if the transaction was not committed yet.
+    """
+    for alias in connections:
+        connection = transaction.get_connection(alias)
+        was_atomic = connection.in_atomic_block
+        connection.in_atomic_block = False
+        connection.run_and_clear_commit_hooks()
+        connection.in_atomic_block = was_atomic