Repository: saleor/saleor

Issue:
Add the same address validation for address create as is used for shipping/billing address in checkout
### What I'm trying to achieve
Use the same mechanism for validation in accountAddressCreate/accountAddressUpdate mutations as used for shipping/billing addresses in checkout



Discussion:
I would like to take up this issue.
I'm looking at picking up this issue
I don't quite understand the codebase, however for anyone picking this up, here's some relevant files:

Implement changes here(?):

https://github.com/mirumee/saleor/blob/041f2aeab90b132abbbde9c505355413c3e4d601/saleor/graphql/account/mutations/account.py#L174-L190

This is possibly the implementation we want to mimic:

https://github.com/mirumee/saleor/blob/041f2aeab90b132abbbde9c505355413c3e4d601/saleor/graphql/checkout/mutations.py#L458-L496

Here's the method for validation:

https://github.com/mirumee/saleor/blob/041f2aeab90b132abbbde9c505355413c3e4d601/saleor/graphql/account/i18n.py#L15-L39

`{
Mutation {
    accountAddressCreate{
	"id" : "eR3tg439frw",
	"name" : "Mary",
	"address" : {..},
	"birthday" : "26 july, 1999",
    }
    accountUpdateAddress{
    from (:2){
    "address" : {..},
    "shippingaddress" {
    "address1" : {..}
    }
    "billing address" : {
    "address2" : {..},
    }}
         }
}

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.


Resolving patch:
diff --git a/saleor/graphql/account/mutations/account.py b/saleor/graphql/account/mutations/account.py
--- a/saleor/graphql/account/mutations/account.py
+++ b/saleor/graphql/account/mutations/account.py
@@ -15,6 +15,7 @@
 from ...core.types.common import AccountError
 from ...meta.deprecated.mutations import UpdateMetaBaseMutation
 from ...meta.deprecated.types import MetaInput
+from ..i18n import I18nMixin
 from .base import (
     INVALID_TOKEN,
     BaseAddressDelete,
@@ -223,7 +224,7 @@ def perform_mutation(cls, _root, info, **data):
         return cls.success_response(user)
 
 
-class AccountAddressCreate(ModelMutation):
+class AccountAddressCreate(ModelMutation, I18nMixin):
     user = graphene.Field(
         User, description="A user instance for which the address was created."
     )
@@ -253,14 +254,18 @@ def check_permissions(cls, context):
 
     @classmethod
     def perform_mutation(cls, root, info, **data):
-        success_response = super().perform_mutation(root, info, **data)
         address_type = data.get("type", None)
         user = info.context.user
-        success_response.user = user
+        cleaned_input = cls.clean_input(
+            info=info, instance=Address(), data=data.get("input")
+        )
+        address = cls.validate_address(cleaned_input)
+        cls.clean_instance(info, address)
+        cls.save(info, address, cleaned_input)
+        cls._save_m2m(info, address, cleaned_input)
         if address_type:
-            instance = success_response.address
-            utils.change_user_default_address(user, instance, address_type)
-        return success_response
+            utils.change_user_default_address(user, address, address_type)
+        return AccountAddressCreate(user=user, address=address)
 
     @classmethod
     def save(cls, info, instance, cleaned_input):
diff --git a/saleor/graphql/account/mutations/base.py b/saleor/graphql/account/mutations/base.py
--- a/saleor/graphql/account/mutations/base.py
+++ b/saleor/graphql/account/mutations/base.py
@@ -26,6 +26,7 @@
 from ...core.types.common import AccountError
 from ...meta.deprecated.mutations import ClearMetaBaseMutation, UpdateMetaBaseMutation
 
+
 BILLING_ADDRESS_FIELD = "default_billing_address"
 SHIPPING_ADDRESS_FIELD = "default_shipping_address"
 INVALID_TOKEN = "Invalid or expired token."
@@ -228,7 +229,7 @@ def perform_mutation(cls, _root, info, **data):
         return PasswordChange(user=user)
 
 
-class BaseAddressUpdate(ModelMutation):
+class BaseAddressUpdate(ModelMutation, I18nMixin):
     """Base mutation for address update used by staff and account."""
 
     user = graphene.Field(
@@ -254,12 +255,20 @@ def clean_input(cls, info, instance, data):
 
     @classmethod
     def perform_mutation(cls, root, info, **data):
-        response = super().perform_mutation(root, info, **data)
-        user = response.address.user_addresses.first()
-        address = info.context.plugins.change_user_address(response.address, None, user)
-        response.user = user
-        response.address = address
-        return response
+        instance = cls.get_instance(info, **data)
+        cleaned_input = cls.clean_input(
+            info=info, instance=instance, data=data.get("input")
+        )
+        address = cls.validate_address(cleaned_input, instance=instance)
+        user = address.user_addresses.first()
+        cls.clean_instance(info, address)
+        cls.save(info, address, cleaned_input)
+        cls._save_m2m(info, address, cleaned_input)
+        address = info.context.plugins.change_user_address(address, None, user)
+        success_response = cls.success_response(address)
+        success_response.user = user
+        success_response.address = address
+        return success_response
 
 
 class BaseAddressDelete(ModelDeleteMutation):


Test patch:
diff --git a/tests/api/test_account.py b/tests/api/test_account.py
--- a/tests/api/test_account.py
+++ b/tests/api/test_account.py
@@ -2699,9 +2699,9 @@ def test_address_update_mutation(
     )
     content = get_graphql_content(response)
     data = content["data"]["addressUpdate"]
-    assert data["address"]["city"] == graphql_address_data["city"]
+    assert data["address"]["city"] == graphql_address_data["city"].upper()
     address_obj.refresh_from_db()
-    assert address_obj.city == graphql_address_data["city"]
+    assert address_obj.city == graphql_address_data["city"].upper()
 
 
 ACCOUNT_ADDRESS_UPDATE_MUTATION = """
@@ -2734,9 +2734,29 @@ def test_customer_update_own_address(
     response = user_api_client.post_graphql(query, variables)
     content = get_graphql_content(response)
     data = content["data"]["accountAddressUpdate"]
-    assert data["address"]["city"] == address_data["city"]
+    assert data["address"]["city"] == address_data["city"].upper()
     address_obj.refresh_from_db()
-    assert address_obj.city == address_data["city"]
+    assert address_obj.city == address_data["city"].upper()
+
+
+def test_customer_update_own_address_not_updated_when_validation_fails(
+    user_api_client, customer_user, graphql_address_data
+):
+    query = ACCOUNT_ADDRESS_UPDATE_MUTATION
+    address_obj = customer_user.addresses.first()
+    address_data = graphql_address_data
+    address_data["city"] = "Poznań"
+    address_data["postalCode"] = "wrong postal code"
+    assert address_data["city"] != address_obj.city
+
+    variables = {
+        "addressId": graphene.Node.to_global_id("Address", address_obj.id),
+        "address": address_data,
+    }
+    user_api_client.post_graphql(query, variables)
+    address_obj.refresh_from_db()
+    assert address_obj.city != address_data["city"]
+    assert address_obj.postal_code != address_data["postalCode"]
 
 
 @pytest.mark.parametrize(
@@ -3190,7 +3210,7 @@ def test_customer_create_address(user_api_client, graphql_address_data):
     content = get_graphql_content(response)
     data = content["data"][mutation_name]
 
-    assert data["address"]["city"] == graphql_address_data["city"]
+    assert data["address"]["city"] == graphql_address_data["city"].upper()
 
     user.refresh_from_db()
     assert user.addresses.count() == nr_of_addresses + 1
@@ -3217,7 +3237,7 @@ def test_customer_create_default_address(user_api_client, graphql_address_data):
     response = user_api_client.post_graphql(query, variables)
     content = get_graphql_content(response)
     data = content["data"][mutation_name]
-    assert data["address"]["city"] == graphql_address_data["city"]
+    assert data["address"]["city"] == graphql_address_data["city"].upper()
 
     user.refresh_from_db()
     assert user.addresses.count() == nr_of_addresses + 1
@@ -3230,7 +3250,7 @@ def test_customer_create_default_address(user_api_client, graphql_address_data):
     response = user_api_client.post_graphql(query, variables)
     content = get_graphql_content(response)
     data = content["data"][mutation_name]
-    assert data["address"]["city"] == graphql_address_data["city"]
+    assert data["address"]["city"] == graphql_address_data["city"].upper()
 
     user.refresh_from_db()
     assert user.addresses.count() == nr_of_addresses + 2
@@ -3246,6 +3266,24 @@ def test_anonymous_user_create_address(api_client, graphql_address_data):
     assert_no_permission(response)
 
 
+def test_address_not_created_after_validation_fails(
+    user_api_client, graphql_address_data
+):
+    user = user_api_client.user
+    nr_of_addresses = user.addresses.count()
+
+    query = ACCOUNT_ADDRESS_CREATE_MUTATION
+
+    graphql_address_data["postalCode"] = "wrong postal code"
+
+    address_type = AddressType.SHIPPING.upper()
+    variables = {"addressInput": graphql_address_data, "addressType": address_type}
+    user_api_client.post_graphql(query, variables)
+
+    user.refresh_from_db()
+    assert user.addresses.count() == nr_of_addresses
+
+
 ACCOUNT_SET_DEFAULT_ADDRESS_MUTATION = """
 mutation($id: ID!, $type: AddressTypeEnum!) {
   accountSetDefaultAddress(id: $id, type: $type) {