Skip to content

Commit

Permalink
Skip address validation
Browse files Browse the repository at this point in the history
  • Loading branch information
zedzior committed May 22, 2024
1 parent d0ab3ca commit e94f710
Show file tree
Hide file tree
Showing 44 changed files with 1,447 additions and 113 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Add taxes to undiscounted prices - #14095 by @jakubkuc
- Mark as deprecated: `ordersTotal`, `reportProductSales` and `homepageEvents` - #14806 by @8r2y5
- Add `identifier` field to App graphql object. Identifier field is the same as Manifest.id field (explicit ID set by the app).
- Add `skipValidation` field to `AddressInput` - #15985 by @zedzior

### Saleor Apps

Expand All @@ -68,6 +69,8 @@ All notable, unreleased changes to this project will be documented in this file.
- Added new parameter `identifier` for `create_app` command.
- When `taxAppId` is provided for `TaxConfiguration` do not allow to finalize `checkoutComplete` or `draftOrderComplete` mutations if Tax App or Avatax plugin didn't respond.
- Add `unique_type` to `OrderLineDiscount` and `CheckoutLineDiscount` models - #15774 by @zedzior
- Allow to skip address validation - #15985 by @zedzior
- Added new field `Address.validation_skipped`.

# 3.18.0

Expand Down
25 changes: 25 additions & 0 deletions saleor/account/migrations/0085_address_validation_skipped.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 3.2.24 on 2024-05-17 12:12

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("account", "0084_merge_20240229_1435"),
]

operations = [
migrations.AddField(
model_name="address",
name="validation_skipped",
field=models.BooleanField(default=False),
),
migrations.RunSQL(
"""
ALTER TABLE account_address
ALTER COLUMN validation_skipped
SET DEFAULT false;
""",
reverse_sql=migrations.RunSQL.noop,
),
]
3 changes: 2 additions & 1 deletion saleor/account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class Address(ModelWithMetadata):
country = CountryField()
country_area = models.CharField(max_length=128, blank=True)
phone = PossiblePhoneNumberField(blank=True, default="", db_index=True)
validation_skipped = models.BooleanField(default=False)

objects = AddressManager()

Expand Down Expand Up @@ -110,7 +111,7 @@ def as_data(self):
data = model_to_dict(self, exclude=["id", "user"])
if isinstance(data["country"], Country):
data["country"] = data["country"].code
if isinstance(data["phone"], PhoneNumber):
if isinstance(data["phone"], PhoneNumber) and not data["validation_skipped"]:
data["phone"] = data["phone"].as_e164
return data

Expand Down
1 change: 1 addition & 0 deletions saleor/account/tests/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ def test_address_as_data(address):
"phone": "+48713988102",
"metadata": {},
"private_metadata": {},
"validation_skipped": False,
}


Expand Down
11 changes: 8 additions & 3 deletions saleor/graphql/account/bulk_mutations/customer_bulk_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,20 +181,22 @@ def clean_address(
field,
index,
index_error_map,
info,
format_check=True,
required_check=True,
enable_normalization=True,
):
try:
address_form = cls.validate_address_form(
address = cls.validate_address(
address_data,
address_type,
address_type=address_type,
format_check=format_check,
required_check=required_check,
enable_normalization=enable_normalization,
info=info,
)

return address_form.cleaned_data
return address.as_data()
except ValidationError as exc:
cls.format_errors(index, exc, index_error_map, field_prefix=field)

Expand Down Expand Up @@ -281,6 +283,7 @@ def clean_customers(cls, info, customers_input, index_error_map):
field=SHIPPING_ADDRESS_FIELD,
index=index,
index_error_map=index_error_map,
info=info,
)
customer_input["input"][SHIPPING_ADDRESS_FIELD] = clean_shipping_address

Expand All @@ -291,6 +294,7 @@ def clean_customers(cls, info, customers_input, index_error_map):
field=BILLING_ADDRESS_FIELD,
index=index,
index_error_map=index_error_map,
info=info,
)
customer_input["input"][BILLING_ADDRESS_FIELD] = clean_billing_address

Expand Down Expand Up @@ -510,6 +514,7 @@ def save_customers(cls, instances_data_with_errors_list, manager):
"country_area",
"phone",
"metadata",
"validation_skipped",
],
)

Expand Down
86 changes: 77 additions & 9 deletions saleor/graphql/account/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,44 @@
from ...account.forms import get_address_form
from ...account.models import Address
from ...account.validators import validate_possible_number
from ...core.exceptions import PermissionDenied
from ...permission.auth_filters import AuthorizationFilters
from ...permission.enums import (
AccountPermissions,
BasePermissionEnum,
CheckoutPermissions,
OrderPermissions,
ProductPermissions,
SitePermissions,
)
from ...permission.utils import all_permissions_required
from ..core import ResolveInfo

SKIP_ADDRESS_VALIDATION_PERMISSION_MAP: dict[str, list[BasePermissionEnum]] = {
"addressCreate": [AccountPermissions.MANAGE_USERS],
"addressUpdate": [AccountPermissions.MANAGE_USERS],
"customerBulkUpdate": [AccountPermissions.MANAGE_USERS],
"draftOrderCreate": [OrderPermissions.MANAGE_ORDERS],
"draftOrderUpdate": [OrderPermissions.MANAGE_ORDERS],
"orderUpdate": [OrderPermissions.MANAGE_ORDERS],
"orderBulkCreate": [OrderPermissions.MANAGE_ORDERS_IMPORT],
"createWarehouse": [ProductPermissions.MANAGE_PRODUCTS],
"updateWarehouse": [ProductPermissions.MANAGE_PRODUCTS],
"shopAddressUpdate": [SitePermissions.MANAGE_SETTINGS],
"checkoutCreate": [
CheckoutPermissions.HANDLE_CHECKOUTS,
AuthorizationFilters.AUTHENTICATED_APP,
],
"checkoutShippingAddressUpdate": [
CheckoutPermissions.HANDLE_CHECKOUTS,
AuthorizationFilters.AUTHENTICATED_APP,
],
"checkoutBillingAddressUpdate": [
CheckoutPermissions.HANDLE_CHECKOUTS,
AuthorizationFilters.AUTHENTICATED_APP,
],
}


class I18nMixin:
"""A mixin providing methods necessary to fulfill the internationalization process.
Expand All @@ -23,7 +59,7 @@ def clean_instance(cls, _info: ResolveInfo, _instance):
pass

@classmethod
def validate_address_form(
def _validate_address_form(
cls,
address_data: dict,
address_type: Optional[str] = None,
Expand Down Expand Up @@ -120,16 +156,48 @@ def validate_address(
)
}
)
address_form = cls.validate_address_form(
address_data,
address_type,
format_check=format_check,
required_check=required_check,
enable_normalization=enable_normalization,
)

if skip_validation := address_data.get("skip_validation", False):
cls.can_skip_address_validation(info)
cls._meta.exclude.append("phone") # type: ignore[attr-defined]
else:
address_form = cls._validate_address_form(
address_data,
address_type,
format_check=format_check,
required_check=required_check,
enable_normalization=enable_normalization,
)
address_data = address_form.cleaned_data

address_data["validation_skipped"] = skip_validation
if not instance:
instance = Address()

cls.construct_instance(instance, address_form.cleaned_data)
cls.construct_instance(instance, address_data)
cls.clean_instance(info, instance)
return instance

@classmethod
def can_skip_address_validation(cls, info: Optional[ResolveInfo]):
required_permissions = None
if info:
mutation_name = info.field_name
required_permissions = SKIP_ADDRESS_VALIDATION_PERMISSION_MAP.get(
mutation_name
)

if not required_permissions:
raise ValidationError(
{
"skip_validation": ValidationError(
"This mutation doesn't allow to skip address validation.",
code="invalid",
)
}
)
elif info and not all_permissions_required(info.context, required_permissions):
raise PermissionDenied(
f"To skip address validation, you need following permissions: "
f"{', '.join(perm.name for perm in required_permissions)}.",
)
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ def perform_mutation( # type: ignore[override]
user = cast(models.User, user)
cleaned_input = cls.clean_input(info=info, instance=Address(), data=input)
with traced_atomic_transaction():
address = cls.validate_address(cleaned_input, address_type=address_type)
address = cls.validate_address(
cleaned_input, address_type=address_type, info=info
)
cls.clean_instance(info, address)
cls.save(info, address, cleaned_input)
cls._save_m2m(info, address, cleaned_input)
Expand Down
2 changes: 1 addition & 1 deletion saleor/graphql/account/mutations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def perform_mutation(cls, _root, info: ResolveInfo, /, **data):
info=info, instance=instance, data=data.get("input")
)
cls.update_metadata(instance, cleaned_input.pop("metadata", list()))
address = cls.validate_address(cleaned_input, instance=instance)
address = cls.validate_address(cleaned_input, instance=instance, info=info)
cls.clean_instance(info, address)
cls.save(info, address, cleaned_input)
cls._save_m2m(info, address, cleaned_input)
Expand Down
2 changes: 1 addition & 1 deletion saleor/graphql/account/mutations/staff/address_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def perform_mutation(cls, root, info: ResolveInfo, /, **data):
data = data.get("input")
with traced_atomic_transaction():
cleaned_input = cls.clean_input(info, instance, data)
instance = cls.validate_address(cleaned_input, instance=instance)
instance = cls.validate_address(cleaned_input, instance=instance, info=info)
cls.clean_instance(info, instance)
cls.save(info, instance, cleaned_input)
cls.post_save_action(info, instance, cleaned_input)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@
key
value
}
postalCode
}
defaultBillingAddress {
metadata {
key
value
}
postalCode
}
}
}
Expand Down Expand Up @@ -538,6 +540,7 @@ def test_customers_bulk_update_with_address(
address_data = convert_dict_keys_to_camel_case(address.as_data())
address_data.pop("metadata")
address_data.pop("privateMetadata")
address_data.pop("validationSkipped")

new_street_address = "Updated street address"
address_data["streetAddress1"] = new_street_address
Expand Down Expand Up @@ -597,6 +600,7 @@ def test_customers_bulk_update_with_address_when_no_default(
customer_id = graphene.Node.to_global_id("User", customer_user.pk)

address_data = convert_dict_keys_to_camel_case(shipping_address.as_data())
address_data.pop("validationSkipped")
address_data.pop("metadata")
address_data.pop("privateMetadata")

Expand Down Expand Up @@ -647,6 +651,7 @@ def test_customers_bulk_update_with_invalid_address(
customer_id = graphene.Node.to_global_id("User", customer_user.pk)

address_data = convert_dict_keys_to_camel_case(address.as_data())
address_data.pop("validationSkipped")
address_data.pop("metadata")
address_data.pop("privateMetadata")
address_data.pop("country")
Expand Down Expand Up @@ -914,3 +919,54 @@ def test_customers_bulk_update_trigger_gift_card_search_vector_update(
for card in gift_card_list:
card.refresh_from_db()
assert card.search_index_dirty is True


def test_customers_bulk_update_skip_address_validation(
staff_api_client,
customer_user,
graphql_address_data_skipped_validation,
permission_manage_users,
):
# given
shipping_address, billing_address = (
customer_user.default_shipping_address,
customer_user.default_billing_address,
)
assert shipping_address
assert billing_address

staff_api_client.user.user_permissions.add(permission_manage_users)
customer_id = graphene.Node.to_global_id("User", customer_user.pk)
address_data = graphql_address_data_skipped_validation
wrong_postal_code = "wrong postal code"
address_data["postalCode"] = wrong_postal_code

customers_input = [
{
"id": customer_id,
"input": {
"defaultBillingAddress": address_data,
"defaultShippingAddress": address_data,
},
}
]

variables = {"customers": customers_input}

# when
response = staff_api_client.post_graphql(CUSTOMER_BULK_UPDATE_MUTATION, variables)
content = get_graphql_content(response)

# then
data = content["data"]["customerBulkUpdate"]
assert data["count"] == 1
assert not data["results"][0]["errors"]
customer_data = data["results"][0]["customer"]
assert customer_data["defaultShippingAddress"]["postalCode"] == wrong_postal_code
assert customer_data["defaultBillingAddress"]["postalCode"] == wrong_postal_code
shipping_address.refresh_from_db()
assert shipping_address.postal_code == wrong_postal_code
assert shipping_address.validation_skipped is True
billing_address.refresh_from_db()
assert billing_address.postal_code == wrong_postal_code
assert billing_address.validation_skipped is True
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
code
field
addressType
message
}
}
}
Expand All @@ -54,9 +55,9 @@ def test_customer_create_address(user_api_client, graphql_address_data):

user.refresh_from_db()

assert user.addresses.exclude(id__in=user_addresses_ids).first().metadata == {
"public": "public_value"
}
address = user.addresses.exclude(id__in=user_addresses_ids).first()
assert address.metadata == {"public": "public_value"}
assert address.validation_skipped is False
assert user.addresses.count() == user_addresses_count + 1
assert (
generate_address_search_document_value(user.addresses.last())
Expand Down Expand Up @@ -199,3 +200,25 @@ def test_address_not_created_after_validation_fails(
assert data["errors"][0]["addressType"] == address_type
user.refresh_from_db()
assert user.addresses.count() == user_addresses_count


def test_customer_create_address_skip_validation(
user_api_client,
graphql_address_data_skipped_validation,
):
# given
query = ACCOUNT_ADDRESS_CREATE_MUTATION
address_data = graphql_address_data_skipped_validation
invalid_postal_code = "invalid_postal_code"
address_data["postalCode"] = invalid_postal_code
variables = {"addressInput": address_data}

# when
response = user_api_client.post_graphql(query, variables)
content = get_graphql_content(response)

# then
data = content["data"]["accountAddressCreate"]
assert not data["user"]
assert data["errors"][0]["field"] == "skipValidation"
assert data["errors"][0]["code"] == "INVALID"
Loading

0 comments on commit e94f710

Please sign in to comment.