Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skip address validation. #16034

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading