Repository: saleor/saleor
Pull request: Associate guest checkout with account if exists
I want to merge this change because it fixes associating guest checkout emails with an account if it exists.
It fixes #12443

<!-- Please mention all relevant issue numbers. -->

# Impact

* [ ] New migrations
* [ ] New/Updated API fields or mutations
* [ ] Deprecated API fields or mutations
* [ ] Removed API types, fields, or mutations
* [ ] Documentation needs to be updated

# Pull Request Checklist

<!-- Please keep this section. It will make the maintainer's life easier. -->

* [ ] Privileged queries and mutations are guarded by proper permission checks
* [ ] Database queries are optimized and the number of queries is constant
* [ ] Database migration files are up to date
* [x ] The changes are tested
* [ ] GraphQL schema and type definitions are up to date
* [ ] Changes are mentioned in the changelog


Problem statement:
Associate guest checkout with an account if it exists
### What I'm trying to achieve
I have an active customer account at Saleor. When I place an order as a guest but use my account's email, the order is not associated with the account. If the email is confirmed (email confirmation is enabled), my order should be automatically associated with my account.

### Describe a proposed solution
Adjust the `complete_checkout` function to assign the user account if the checkout's email is a valid customer account in Saleor.

### Other solutions I've tried and won't work
…

### Screenshots or mockups
<!-- Please provide any illustrations that could help others understand the problem or the proposed solution. -->

Hints from issue discussion:

Commit message: changelog edit

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ddaf93a2ccc..883a1908f5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,7 @@ All notable, unreleased changes to this project will be documented in this file.
 - Add `ORDER_REFUNDED`, `ORDER_FULLY_REFUNDED`, `ORDER_PAID` webhooks - #12533 by @korycins
 - Handle error raised by 0Auth when fetching token - #12672 by @IKarbowiakg
 - Fix adding new lines to draft order when existing line has deleted product - #12711 by @SzymJ
+- Upgrade checkout `complete_checkout` to assign guest checkout to account if exists - #12758 by @FremahA
 
 # 3.13.0
 
diff --git a/saleor/checkout/complete_checkout.py b/saleor/checkout/complete_checkout.py
index e1864686890..6f52b1020cb 100644
--- a/saleor/checkout/complete_checkout.py
+++ b/saleor/checkout/complete_checkout.py
@@ -23,7 +23,7 @@
 
 from ..account.error_codes import AccountErrorCode
 from ..account.models import User
-from ..account.utils import store_user_address
+from ..account.utils import store_user_address, retrieve_user_by_email
 from ..channel import MarkAsPaidStrategy
 from ..checkout import CheckoutAuthorizeStatus, calculations
 from ..checkout.error_codes import CheckoutErrorCode
@@ -1321,6 +1321,12 @@ def complete_checkout(
     metadata_list: Optional[List] = None,
     private_metadata_list: Optional[List] = None,
 ) -> Tuple[Optional[Order], bool, dict]:
+    # Assign checkout user to an existing user if checkout email matches a valid customer account
+    if user is None and checkout_info.checkout.email:
+        existing_user = retrieve_user_by_email(checkout_info.checkout.email)
+        user = existing_user if existing_user else user
+        checkout_info.user = user
+
     transactions = checkout_info.checkout.payment_transactions.all()
     fetch_checkout_data(checkout_info, manager, lines, discounts=discounts)
 
diff --git a/saleor/checkout/tests/test_associate_checkout_with_account.py b/saleor/checkout/tests/test_associate_checkout_with_account.py
new file mode 100644
index 00000000000..e756461f715
--- /dev/null
+++ b/saleor/checkout/tests/test_associate_checkout_with_account.py
@@ -0,0 +1,53 @@
+import pytest
+from unittest import mock
+from saleor.checkout.complete_checkout import complete_checkout
+from saleor.channel import MarkAsPaidStrategy
+from ..fetch import fetch_checkout_info, fetch_checkout_lines
+from ...plugins.manager import get_plugins_manager
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize(
+    "email, paid_strategy", 
+    [
+        ("test@example.com", MarkAsPaidStrategy.TRANSACTION_FLOW), 
+        ("guest@email.com", MarkAsPaidStrategy.TRANSACTION_FLOW),
+        ("test@example.com", MarkAsPaidStrategy.PAYMENT_FLOW), 
+        ("guest@email.com", MarkAsPaidStrategy.PAYMENT_FLOW),
+    ],
+)
+def test_associate_guest_checkout_with_account_if_exists(
+    email, 
+    paid_strategy, 
+    app, 
+    address, 
+    checkout, 
+    customer_user
+):
+    # set the checkout email
+    checkout.email = email
+    checkout.save()
+    user = None
+    manager = get_plugins_manager()
+    lines, _ = fetch_checkout_lines(checkout)
+    checkout_info = fetch_checkout_info(checkout, lines, [], manager)
+    checkout_info.billing_address = address
+    checkout_info.channel.order_mark_as_paid_strategy == paid_strategy
+
+    # call the complete_checkout function with the checkout object
+    order, _, _ = complete_checkout(
+        checkout_info=checkout_info,
+        manager=manager,
+        lines=lines,
+        payment_data={},
+        store_source=False,
+        discounts=None,
+        user=user,
+        app=app
+    )
+
+    # assert that the order is associated with the correct user
+    if checkout_info.user:
+        assert order.user == customer_user
+    else:
+        assert order.user is None
\ No newline at end of file


Code review comments:
diff_hunk: @@ -0,0 +1,53 @@
+import pytest
+from unittest import mock
+from saleor.checkout.complete_checkout import complete_checkout
+from saleor.channel import MarkAsPaidStrategy
+from ..fetch import fetch_checkout_info, fetch_checkout_lines
+from ...plugins.manager import get_plugins_manager
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize(
+    "email, paid_strategy", 
+    [
+        ("test@example.com", MarkAsPaidStrategy.TRANSACTION_FLOW), 
+        ("guest@email.com", MarkAsPaidStrategy.TRANSACTION_FLOW),
+        ("test@example.com", MarkAsPaidStrategy.PAYMENT_FLOW), 
+        ("guest@email.com", MarkAsPaidStrategy.PAYMENT_FLOW),
+    ],
+)
+def test_associate_guest_checkout_with_account_if_exists(
+    email, 
+    paid_strategy, 
+    app, 
+    address, 
+    checkout, 
+    customer_user
+):
+    # set the checkout email
+    checkout.email = email
+    checkout.save()
+    user = None
+    manager = get_plugins_manager()
+    lines, _ = fetch_checkout_lines(checkout)
+    checkout_info = fetch_checkout_info(checkout, lines, [], manager)
+    checkout_info.billing_address = address
+    checkout_info.channel.order_mark_as_paid_strategy == paid_strategy
+
+    # call the complete_checkout function with the checkout object
+    order, _, _ = complete_checkout(
+        checkout_info=checkout_info,
+        manager=manager,
+        lines=lines,
+        payment_data={},
+        store_source=False,
+        discounts=None,
+        user=user,
+        app=app
+    )
+
+    # assert that the order is associated with the correct user
+    if checkout_info.user:
path: saleor/checkout/tests/test_associate_checkout_with_account.py
text: @user3:
We should split it into two separate tests. One for `test@user1.com` and the second for `guest@user2.com`. 
The good practice in the test is avoiding to use of conditions in asserts. Creating 2 separate tests in such cases is a more clean solution. 

@author:
@user3 Thank you for your review and feedback on my pull request. I will make the necessary changes with these guidelines.
Please let me know if you have any additional suggestions or concerns.

diff_hunk: @@ -1321,6 +1321,12 @@ def complete_checkout(
     metadata_list: Optional[List] = None,
     private_metadata_list: Optional[List] = None,
 ) -> Tuple[Optional[Order], bool, dict]:
+    # Assign checkout user to an existing user if checkout email matches a valid customer account
+    if user is None and checkout_info.checkout.email:
+        existing_user = retrieve_user_by_email(checkout_info.checkout.email)
+        user = existing_user if existing_user else user
+        checkout_info.user = user
path: saleor/checkout/complete_checkout.py
text: @user1:
How about? 
```suggestion
        checkout_info.user = existing_user if existing_user else None
```