Repository: saleor/saleor

Issue:
Dashboard 2.0: When changing the email address of an order match it to a User object
When using `orderUpdate` if `emailAddress` is specified the order's `user` field should be updated accordingly (set to null if the email does not match an active account and to a User instance otherwise).


Discussion:
Is it related to `orderUpdate` only, or to all order mutations?

Resolving patch:
diff --git a/saleor/graphql/order/mutations/draft_orders.py b/saleor/graphql/order/mutations/draft_orders.py
--- a/saleor/graphql/order/mutations/draft_orders.py
+++ b/saleor/graphql/order/mutations/draft_orders.py
@@ -2,7 +2,7 @@
 from graphene.types import InputObjectType
 from graphql_jwt.decorators import permission_required
 
-from ....account.models import Address
+from ....account.models import Address, User
 from ....core.exceptions import InsufficientStock
 from ....core.utils.taxes import ZERO_TAXED_MONEY
 from ....order import OrderEvents, OrderStatus, models
@@ -186,18 +186,31 @@ class Arguments:
     class Meta:
         description = 'Completes creating an order.'
 
+    @classmethod
+    def update_user_fields(cls, order, errors):
+        if order.user:
+            order.user_email = order.user.email
+        elif order.user_email:
+            try:
+                order.user = User.objects.get(email=order.user_email)
+            except User.DoesNotExist:
+                order.user = None
+        else:
+            cls.add_error(
+                errors, field=None,
+                message='Both user and user_email fields are null')
+
     @classmethod
     @permission_required('order.manage_orders')
     def mutate(cls, root, info, id):
         errors = []
         order = cls.get_node_or_error(info, id, errors, 'id', Order)
         errors = check_for_draft_order_errors(order, errors)
+        cls.update_user_fields(order, errors)
         if errors:
             return cls(errors=errors)
 
         order.status = OrderStatus.UNFULFILLED
-        if order.user:
-            order.user_email = order.user.email
         if not order.is_shipping_required():
             order.shipping_method_name = None
             order.shipping_price = ZERO_TAXED_MONEY
diff --git a/saleor/graphql/order/mutations/orders.py b/saleor/graphql/order/mutations/orders.py
--- a/saleor/graphql/order/mutations/orders.py
+++ b/saleor/graphql/order/mutations/orders.py
@@ -3,6 +3,7 @@
 from graphql_jwt.decorators import permission_required
 from payments import PaymentError, PaymentStatus
 
+from ....account.models import User
 from ....core.utils.taxes import ZERO_TAXED_MONEY
 from ....order import CustomPaymentChoices, OrderEvents, models
 from ....order.utils import cancel_order
@@ -122,6 +123,28 @@ class Meta:
         description = 'Updates an order.'
         model = models.Order
 
+    @classmethod
+    def clean_input(cls, info, instance, input, errors):
+        cleaned_input = super().clean_input(info, instance, input, errors)
+        if not instance.user and not cleaned_input.get('user_email'):
+            cls.add_error(
+                errors, field='user_email',
+                message='User_email field is null while order was created by '
+                        'anonymous user')
+        return cleaned_input
+
+    @classmethod
+    def save(cls, info, instance, cleaned_input):
+        super().save(info, instance, cleaned_input)
+        if instance.user_email:
+            try:
+                user = User.objects.get(email=instance.user_email)
+            except User.DoesNotExist:
+                instance.user = None
+            else:
+                instance.user = user
+        instance.save()
+
 
 class OrderUpdateShippingInput(graphene.InputObjectType):
     shipping_method = graphene.ID(


Test patch:
diff --git a/tests/api/test_order.py b/tests/api/test_order.py
--- a/tests/api/test_order.py
+++ b/tests/api/test_order.py
@@ -396,6 +396,85 @@ def test_draft_order_complete(admin_api_client, admin_user, draft_order):
     assert draft_placed_event.parameters == {}
 
 
+def test_draft_order_complete_existing_user_email_updates_user_field(
+        admin_api_client, draft_order, customer_user):
+    order = draft_order
+    order.user_email = customer_user.email
+    order.user = None
+    order.save()
+    query = """
+        mutation draftComplete($id: ID!) {
+            draftOrderComplete(id: $id) {
+                order {
+                    status
+                }
+            }
+        }
+        """
+    order_id = graphene.Node.to_global_id('Order', order.id)
+    variables = json.dumps({'id': order_id})
+    response = admin_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    assert 'errors' not in content
+    order.refresh_from_db()
+    assert order.user == customer_user
+
+
+def test_draft_order_complete_anonymous_user_email_sets_user_field_null(
+        admin_api_client, draft_order):
+    order = draft_order
+    order.user_email = 'anonymous@example.com'
+    order.user = None
+    order.save()
+    query = """
+        mutation draftComplete($id: ID!) {
+            draftOrderComplete(id: $id) {
+                order {
+                    status
+                }
+            }
+        }
+        """
+    order_id = graphene.Node.to_global_id('Order', order.id)
+    variables = json.dumps({'id': order_id})
+    response = admin_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    assert 'errors' not in content
+    order.refresh_from_db()
+    assert order.user is None
+
+
+def test_draft_order_complete_anonymous_user_no_email(
+        admin_api_client, draft_order):
+    order = draft_order
+    order.user_email = ''
+    order.user = None
+    order.save()
+    query = """
+        mutation draftComplete($id: ID!) {
+            draftOrderComplete(id: $id) {
+                order {
+                    status
+                }
+                errors {
+                    field
+                    message
+                }
+            }
+        }
+        """
+    order_id = graphene.Node.to_global_id('Order', order.id)
+    variables = json.dumps({'id': order_id})
+    response = admin_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    assert 'errors' in content['data']['draftOrderComplete']
+    assert content['data']['draftOrderComplete']['errors'][0] == {
+        'field': None, 'message': 'Both user and user_email fields are null'}
+
+
 DRAFT_ORDER_LINE_CREATE_MUTATION = """
     mutation DraftOrderLineCreate($orderId: ID!, $variantId: ID!, $quantity: Int!) {
         draftOrderLineCreate(id: $orderId, input: {variantId: $variantId, quantity: $quantity}) {
@@ -606,6 +685,8 @@ def test_require_draft_order_when_removing_lines(
 
 def test_order_update(admin_api_client, order_with_lines):
     order = order_with_lines
+    order.user = None
+    order.save()
     query = """
         mutation orderUpdate(
         $id: ID!, $email: String, $first_name: String, $last_name: String,
@@ -645,6 +726,98 @@ def test_order_update(admin_api_client, order_with_lines):
     order.refresh_from_db()
     assert order.shipping_address.first_name == first_name
     assert order.billing_address.last_name == last_name
+    assert order.user_email == email
+    assert order.user is None
+
+
+def test_order_update_anonymous_user_no_user_email(
+        admin_api_client, order_with_lines):
+    order = order_with_lines
+    order.user = None
+    order.save()
+    query = """
+            mutation orderUpdate(
+            $id: ID!, $first_name: String, $last_name: String, 
+            $country_code: String) {
+                orderUpdate(
+                    id: $id, input: {
+                        shippingAddress:
+                        {firstName: $first_name, country: $country_code},
+                        billingAddress:
+                        {lastName: $last_name, country: $country_code}}) {
+                    errors {
+                        field
+                        message
+                    }
+                    order {
+                        id
+                    }
+                }
+            }
+            """
+    first_name = 'Test fname'
+    last_name = 'Test lname'
+    order_id = graphene.Node.to_global_id('Order', order.id)
+    variables = json.dumps({
+        'id': order_id, 'first_name': first_name, 'last_name': last_name,
+        'country_code': 'PL'})
+    response = admin_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    assert 'errors' in content['data']['orderUpdate']
+    assert content['data']['orderUpdate']['errors'][0] == {
+        'field': 'userEmail',
+        'message': 'User_email field is null while order was created by '
+                   'anonymous user'}
+
+    order.refresh_from_db()
+    assert order.shipping_address.first_name != first_name
+    assert order.billing_address.last_name != last_name
+
+
+def test_order_update_user_email_existing_user(
+        admin_api_client, order_with_lines, customer_user):
+    order = order_with_lines
+    order.user = None
+    order.save()
+    query = """
+        mutation orderUpdate(
+        $id: ID!, $email: String, $first_name: String, $last_name: String,
+        $country_code: String) {
+            orderUpdate(
+                id: $id, input: {
+                    userEmail: $email, shippingAddress:
+                    {firstName: $first_name, country: $country_code},
+                    billingAddress:
+                    {lastName: $last_name, country: $country_code}}) {
+                errors {
+                    field
+                    message
+                }
+                order {
+                    userEmail
+                }
+            }
+        }
+        """
+    email = customer_user.email
+    first_name = 'Test fname'
+    last_name = 'Test lname'
+    order_id = graphene.Node.to_global_id('Order', order.id)
+    variables = json.dumps(
+        {'id': order_id, 'email': email, 'first_name': first_name,
+         'last_name': last_name, 'country_code': 'PL'})
+    response = admin_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    data = content['data']['orderUpdate']['order']
+    assert data['userEmail'] == email
+
+    order.refresh_from_db()
+    assert order.shipping_address.first_name == first_name
+    assert order.billing_address.last_name == last_name
+    assert order.user_email == email
+    assert order.user == customer_user
 
 
 def test_order_add_note(admin_api_client, order_with_lines, admin_user):