Repository: saleor/saleor
Pull request: Add checkout assignment to the logged in customer
I want to merge this change because resolve #3584 

<!-- Please mention all relevant issue numbers. -->

### Screenshots

<!-- If your changes affect the UI, providing "before" and "after" screenshots will
greatly reduce the amount of work needed to review your work. -->

### Pull Request Checklist

<!-- Please keep this section. It will make maintainer's life easier. -->

1. [x] Privileged views and APIs are guarded by proper permission checks.
1. [x] All visible strings are translated with proper context.
1. [x] All data-formatting is locale-aware (dates, numbers, and so on).
1. [x] Database queries are optimized and the number of queries is constant.
1. [x] Database migration files are up to date.
1. [x] The changes are tested.
1. [x] The code is documented (docstrings, project documentation).
1. [x] GraphQL schema and type definitions are up to date.
1. [x] Changes are mentioned in the changelog.


Problem statement:
Checkout should be assigned to the logged in customer
When we create checkout as a logged in customer it should be assigned to him.

### Steps to reproduce the problem
1. Login to GraphQL API 
2. Create checkout
3. Check user assigned to checkout

Hints from issue discussion:

Commit message: Changelog

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dae59518547..801602d0fdd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,3 +5,4 @@ All notable, unreleased changes to this project will be documented in this file.
 - Use USERNAME_FIELD instead of hard-code email field when resolving user - #3577 by @jxltom
 - Support returning user's checkouts in GraphQL API - #3578 by @fowczarek
 - Catch GraphqQL syntax errors and output it to errors field - #3576 by @jxltom
+- Add checkout assignment to the logged in customer - #3587 by @fowczarek
diff --git a/saleor/graphql/checkout/mutations.py b/saleor/graphql/checkout/mutations.py
index 07afb621900..eb994b7d84a 100644
--- a/saleor/graphql/checkout/mutations.py
+++ b/saleor/graphql/checkout/mutations.py
@@ -77,6 +77,16 @@ def check_lines_quantity(variants, quantities):
     return errors
 
 
+def get_validated_address_data(checkout, errors, user, address_data):
+    if address_data:
+        address_data, errors = checkout.validate_address(
+            address_data, errors)
+        return address_data
+    if not user.is_anonymous:
+        return user.default_shipping_address
+    return None
+
+
 class CheckoutLineInput(graphene.InputObjectType):
     quantity = graphene.Int(
         description='The number of items purchased.')
@@ -93,6 +103,8 @@ class CheckoutCreateInput(graphene.InputObjectType):
     shipping_address = AddressInput(
         description=(
             'The mailling address to where the checkout will be shipped.'))
+    billing_address = AddressInput(
+        description=('Billing address of the customer.'))
 
 
 class CheckoutCreate(ModelMutation, I18nMixin):
@@ -108,6 +120,7 @@ class Meta:
     @classmethod
     def clean_input(cls, info, instance, input, errors):
         cleaned_input = super().clean_input(info, instance, input, errors)
+        user = info.context.user
         lines = input.pop('lines', None)
         if lines:
             variant_ids = [line.get('variant_id') for line in lines]
@@ -124,20 +137,39 @@ def clean_input(cls, info, instance, input, errors):
                 cleaned_input['quantities'] = quantities
 
         shipping_address_data = input.pop('shipping_address', None)
-        if shipping_address_data:
-            shipping_address, errors = cls.validate_address(
-                shipping_address_data, errors)
+        shipping_address = get_validated_address_data(
+            cls, errors, user, shipping_address_data)
+        if shipping_address:
             cleaned_input['shipping_address'] = shipping_address
+
+        billing_address_data = input.pop('billing_address', None)
+        billing_address = get_validated_address_data(
+            cls, errors, user, billing_address_data)
+        if billing_address:
+            cleaned_input['billing_address'] = billing_address
+
         return cleaned_input
 
     @classmethod
     def save(cls, info, instance, cleaned_input):
         shipping_address = cleaned_input.get('shipping_address')
+        billing_address = cleaned_input.get('billing_address')
+        update_fields = []
         if shipping_address:
             shipping_address.save()
             instance.shipping_address = shipping_address
+            update_fields.append('shipping_address')
+        if billing_address:
+            billing_address.save()
+            instance.billing_address = billing_address
+            update_fields.append('billing_address')
         super().save(info, instance, cleaned_input)
-        instance.save(update_fields=['shipping_address'])
+        user = info.context.user
+        if not user.is_anonymous:
+            instance.user = user
+            update_fields.append('user')
+        instance.save(update_fields=update_fields)
+
         variants = cleaned_input.get('variants')
         quantities = cleaned_input.get('quantities')
         if variants and quantities:
diff --git a/saleor/graphql/schema.graphql b/saleor/graphql/schema.graphql
index aa0e3c31356..c84fe07b69b 100644
--- a/saleor/graphql/schema.graphql
+++ b/saleor/graphql/schema.graphql
@@ -323,6 +323,7 @@ input CheckoutCreateInput {
   lines: [CheckoutLineInput]
   email: String
   shippingAddress: AddressInput
+  billingAddress: AddressInput
 }
 
 type CheckoutCustomerAttach {
diff --git a/tests/api/test_checkout.py b/tests/api/test_checkout.py
index 787c6e1f8bb..ae88491769c 100644
--- a/tests/api/test_checkout.py
+++ b/tests/api/test_checkout.py
@@ -25,7 +25,7 @@
     """
 
 
-def test_checkout_create(user_api_client, variant, graphql_address_data):
+def test_checkout_create(api_client, variant, graphql_address_data):
     """Create checkout object using GraphQL API."""
     variant_id = graphene.Node.to_global_id('ProductVariant', variant.id)
     test_email = 'test@example.com'
@@ -38,7 +38,7 @@ def test_checkout_create(user_api_client, variant, graphql_address_data):
             'email': test_email,
             'shippingAddress': shipping_address}}
     assert not Cart.objects.exists()
-    response = user_api_client.post_graphql(
+    response = api_client.post_graphql(
         MUTATION_CHECKOUT_CREATE, variables)
     content = get_graphql_content(response)
 
@@ -64,6 +64,83 @@ def test_checkout_create(user_api_client, variant, graphql_address_data):
     assert new_cart.shipping_address.city == shipping_address['city']
 
 
+def test_checkout_create_logged_in_customer(
+        user_api_client, variant):
+    variant_id = graphene.Node.to_global_id('ProductVariant', variant.id)
+    variables = {
+        'checkoutInput': {
+            'lines': [{
+                'quantity': 1,
+                'variantId': variant_id}]}}
+    assert not Cart.objects.exists()
+    response = user_api_client.post_graphql(
+        MUTATION_CHECKOUT_CREATE, variables)
+    content = get_graphql_content(response)
+    new_cart = Cart.objects.first()
+    assert new_cart is not None
+    checkout_data = content['data']['checkoutCreate']['checkout']
+    assert checkout_data['token'] == str(new_cart.token)
+    cart_user = new_cart.user
+    customer = user_api_client.user
+    assert customer.id == cart_user.id
+    assert customer.default_shipping_address_id == new_cart.shipping_address_id
+    assert customer.default_billing_address_id == new_cart.billing_address_id
+
+
+def test_checkout_create_logged_in_customer_custom_email(
+        user_api_client, variant):
+    variant_id = graphene.Node.to_global_id('ProductVariant', variant.id)
+    customer = user_api_client.user
+    custom_email = 'custom@example.com'
+    variables = {
+        'checkoutInput': {
+            'lines': [{
+                'quantity': 1,
+                'variantId': variant_id}],
+            'email': custom_email}}
+    assert not Cart.objects.exists()
+    assert not custom_email == customer.email
+    response = user_api_client.post_graphql(
+        MUTATION_CHECKOUT_CREATE, variables)
+    content = get_graphql_content(response)
+    new_cart = Cart.objects.first()
+    assert new_cart is not None
+    checkout_data = content['data']['checkoutCreate']['checkout']
+    assert checkout_data['token'] == str(new_cart.token)
+    cart_user = new_cart.user
+    assert customer.id == cart_user.id
+    assert new_cart.email == custom_email
+
+
+def test_checkout_create_logged_in_customer_custom_addresses(
+        user_api_client, variant, graphql_address_data):
+    variant_id = graphene.Node.to_global_id('ProductVariant', variant.id)
+    shipping_address = graphql_address_data
+    billing_address = graphql_address_data
+    variables = {
+        'checkoutInput': {
+            'lines': [{
+                'quantity': 1,
+                'variantId': variant_id}],
+            'shippingAddress': shipping_address,
+            'billingAddress': billing_address}}
+    assert not Cart.objects.exists()
+    response = user_api_client.post_graphql(
+        MUTATION_CHECKOUT_CREATE, variables)
+    content = get_graphql_content(response)
+    new_cart = Cart.objects.first()
+    assert new_cart is not None
+    checkout_data = content['data']['checkoutCreate']['checkout']
+    assert checkout_data['token'] == str(new_cart.token)
+    cart_user = new_cart.user
+    customer = user_api_client.user
+    assert customer.id == cart_user.id
+    assert not customer.default_shipping_address_id == new_cart.shipping_address_id
+    assert not customer.default_billing_address_id == new_cart.billing_address_id
+    assert new_cart.shipping_address.first_name == shipping_address['firstName']
+    assert new_cart.billing_address.first_name == billing_address['firstName']
+
+
 def test_checkout_create_check_lines_quantity(
         user_api_client, variant, graphql_address_data):
     variant_id = graphene.Node.to_global_id('ProductVariant', variant.id)
@@ -76,6 +153,7 @@ def test_checkout_create_check_lines_quantity(
                 'variantId': variant_id}],
             'email': test_email,
             'shippingAddress': shipping_address}}
+    assert not Cart.objects.exists()
     response = user_api_client.post_graphql(
         MUTATION_CHECKOUT_CREATE, variables)
     content = get_graphql_content(response)


Code review comments:
diff_hunk: @@ -77,6 +77,16 @@ def check_lines_quantity(variants, quantities):
     return errors
 
 
+def get_validated_address_data(checkout, errors, user, address_data):
path: saleor/graphql/checkout/mutations.py
text: @user1:
It's just a feeling, but shouldn't it be part of the checkout mutation class? Or maybe even `18nMixin`? This function is not being used elsewhere but during the checkout process, and maybe in general internalization process and `checkout` object is being called as a `cls` below, which for me is a little counterintuitive.