Saleor vulnerable to customers addresses leak when using Warehouse as a `Pickup: Local stock only` delivery method
Description
Saleor is an e-commerce platform that serves high-volume companies. When using Pickup: Local stock only click-and-collect as a delivery method in specific conditions the customer could overwrite the warehouse address with its own, which exposes its address as click-and-collect address. This issue has been patched in versions: 3.14.61, 3.15.37, 3.16.34, 3.17.32, 3.18.28, 3.19.15.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
saleorPyPI | >= 3.14.56, < 3.14.61 | 3.14.61 |
saleorPyPI | >= 3.15.31, < 3.15.37 | 3.15.37 |
saleorPyPI | >= 3.16.27, < 3.16.34 | 3.16.34 |
saleorPyPI | >= 3.17.25, < 3.17.32 | 3.17.32 |
saleorPyPI | >= 3.18.19, < 3.18.28 | 3.18.28 |
saleorPyPI | >= 3.19.5, < 3.19.15 | 3.19.15 |
Affected products
1Patches
847cedfd7d652Fix relation between order and click and collect address (#15697)
10 files changed · +140 −5
saleor/checkout/complete_checkout.py+3 −0 modified@@ -145,6 +145,9 @@ def _process_shipping_data_for_order( if checkout_info.user.addresses.filter(pk=shipping_address.pk).exists(): shipping_address = shipping_address.get_copy() + if shipping_address and delivery_method_info.warehouse_pk: + shipping_address = shipping_address.get_copy() + shipping_method = delivery_method_info.delivery_method tax_class = getattr(shipping_method, "tax_class", None)
saleor/graphql/checkout/tests/mutations/test_checkout_complete_with_payment.py+5 −3 modified@@ -3105,7 +3105,7 @@ def test_complete_checkout_for_local_click_and_collect( order_count = Order.objects.count() checkout = checkout_with_item_for_cc checkout.collection_point = warehouse_for_cc - checkout.shipping_address = None + checkout.shipping_address = warehouse_for_cc.address checkout.save(update_fields=["collection_point", "shipping_address"]) variables = { @@ -3147,7 +3147,8 @@ def test_complete_checkout_for_local_click_and_collect( assert order.collection_point == warehouse_for_cc assert order.shipping_method is None - assert order.shipping_address == warehouse_for_cc.address + assert order.shipping_address + assert order.shipping_address.id != warehouse_for_cc.address.id assert order.shipping_price == zero_taxed_money(payment.currency) assert order.lines.count() == 1 @@ -3215,7 +3216,8 @@ def test_complete_checkout_for_global_click_and_collect( assert order.collection_point == warehouse_for_cc assert order.shipping_method is None - assert order.shipping_address == warehouse_for_cc.address + assert order.shipping_address + assert order.shipping_address.id != warehouse_for_cc.address.id assert order.shipping_price == zero_taxed_money(payment.currency) assert order.lines.count() == 1
saleor/graphql/checkout/tests/mutations/test_checkout_complete_with_transactions.py+4 −2 modified@@ -2773,7 +2773,8 @@ def test_complete_checkout_for_local_click_and_collect( assert order.collection_point == warehouse_for_cc assert order.shipping_method is None - assert order.shipping_address == warehouse_for_cc.address + assert order.shipping_address + assert order.shipping_address.id != warehouse_for_cc.address.id assert order.shipping_price == zero_taxed_money(order.channel.currency_code) assert order.lines.count() == 1 @@ -2833,7 +2834,8 @@ def test_complete_checkout_for_global_click_and_collect( assert order.collection_point == warehouse_for_cc assert order.shipping_method is None - assert order.shipping_address == warehouse_for_cc.address + assert order.shipping_address + assert order.shipping_address.id != warehouse_for_cc.address.id assert order.shipping_price == zero_taxed_money(order.channel.currency_code) assert order.lines.count() == 1
saleor/order/migrations/0172_update_order_cc_addresses.py+39 −0 added@@ -0,0 +1,39 @@ +from django.db import migrations +from django.db.models import Exists, OuterRef +from django.forms.models import model_to_dict + +from .tasks.saleor3_19 import update_order_addresses_task + +# The batch of size 250 takes ~0.5 second and consumes ~20MB memory at peak +ADDRESS_UPDATE_BATCH_SIZE = 250 + + +def update_order_addresses(apps, schema_editor): + Order = apps.get_model("order", "Order") + Warehouse = apps.get_model("warehouse", "Warehouse") + Address = apps.get_model("account", "Address") + qs = Order.objects.filter( + Exists(Warehouse.objects.filter(address_id=OuterRef("shipping_address_id"))), + ) + order_ids = qs.values_list("pk", flat=True)[:ADDRESS_UPDATE_BATCH_SIZE] + addresses = [] + if order_ids: + orders = Order.objects.filter(id__in=order_ids) + for order in orders: + if cc_address := order.shipping_address: + order_address = Address(**model_to_dict(cc_address, exclude=["id"])) + order.shipping_address = order_address + addresses.append(order_address) + Address.objects.bulk_create(addresses, ignore_conflicts=True) + Order.objects.bulk_update(orders, ["shipping_address"]) + update_order_addresses_task.delay() + + +class Migration(migrations.Migration): + dependencies = [ + ("order", "0171_order_order_user_email_user_id_idx"), + ] + + operations = [ + migrations.RunPython(update_order_addresses, migrations.RunPython.noop), + ]
saleor/order/migrations/0176_merge_20240325_1315.py+12 −0 added@@ -0,0 +1,12 @@ +# Generated by Django 3.2.22 on 2024-03-25 13:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("order", "0172_update_order_cc_addresses"), + ("order", "0175_merge_20231122_1040"), + ] + + operations = []
saleor/order/migrations/0177_merge_20240325_1329.py+12 −0 added@@ -0,0 +1,12 @@ +# Generated by Django 3.2.22 on 2024-03-25 13:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("order", "0176_merge_20231122_1346"), + ("order", "0176_merge_20240325_1315"), + ] + + operations = []
saleor/order/migrations/0180_merge_20240325_1333.py+12 −0 added@@ -0,0 +1,12 @@ +# Generated by Django 3.2.22 on 2024-03-25 13:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("order", "0177_merge_20240325_1329"), + ("order", "0179_merge_20231122_1348"), + ] + + operations = []
saleor/order/migrations/0182_merge_20240325_1338.py+12 −0 added@@ -0,0 +1,12 @@ +# Generated by Django 3.2.22 on 2024-03-25 13:38 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("order", "0180_merge_20240325_1333"), + ("order", "0181_order_subtotal_as_a_field"), + ] + + operations = []
saleor/order/migrations/0184_merge_0182_merge_20240325_1338_0183_order_tax_error.py+12 −0 added@@ -0,0 +1,12 @@ +# Generated by Django 3.2.22 on 2024-03-25 13:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("order", "0182_merge_20240325_1338"), + ("order", "0183_order_tax_error"), + ] + + operations = []
saleor/order/migrations/tasks/saleor3_19.py+29 −0 added@@ -0,0 +1,29 @@ +from django.db.models import Exists, OuterRef +from django.forms.models import model_to_dict + +from ....account.models import Address +from ....celeryconf import app +from ....warehouse.models import Warehouse +from ...models import Order + +# The batch of size 250 takes ~0.5 second and consumes ~20MB memory at peak +ADDRESS_UPDATE_BATCH_SIZE = 250 + + +@app.task +def update_order_addresses_task(): + qs = Order.objects.filter( + Exists(Warehouse.objects.filter(address_id=OuterRef("shipping_address_id"))), + ) + order_ids = qs.values_list("pk", flat=True)[:ADDRESS_UPDATE_BATCH_SIZE] + addresses = [] + if order_ids: + orders = Order.objects.filter(id__in=order_ids) + for order in orders: + if cc_address := order.shipping_address: + order_address = Address(**model_to_dict(cc_address, exclude=["id"])) + order.shipping_address = order_address + addresses.append(order_address) + Address.objects.bulk_create(addresses, ignore_conflicts=True) + Order.objects.bulk_update(orders, ["shipping_address"]) + update_order_addresses_task.delay()
22a1aa3ef0bcFix tax calculation for Click and Collect option. (#15505)
7 files changed · +310 −42
saleor/checkout/error_codes.py+1 −0 modified@@ -32,6 +32,7 @@ class CheckoutErrorCode(Enum): INACTIVE_PAYMENT = "inactive_payment" NON_EDITABLE_GIFT_LINE = "non_editable_gift_line" NON_REMOVABLE_GIFT_LINE = "non_removable_gift_line" + SHIPPING_CHANGE_FORBIDDEN = "shipping_change_forbidden" class OrderCreateFromCheckoutErrorCode(Enum):
saleor/graphql/checkout/mutations/checkout_delivery_method_update.py+14 −7 modified@@ -21,6 +21,7 @@ from ....shipping import interface as shipping_interface from ....shipping import models as shipping_models from ....shipping.utils import convert_to_shipping_method_data +from ....warehouse import WarehouseClickAndCollectOption from ....warehouse import models as warehouse_models from ....webhook.const import APP_ID_PREFIX from ....webhook.event_types import WebhookEventAsyncType, WebhookEventSyncType @@ -59,7 +60,9 @@ class Arguments: class Meta: description = ( "Updates the delivery method (shipping method or pick up point) " - "of the checkout." + ADDED_IN_31 + "of the checkout. " + "Updates the checkout shipping_address for click and collect delivery " + "for a warehouse address. " + ADDED_IN_31 ) doc_category = DOC_CATEGORY_CHECKOUT error_type_class = CheckoutError @@ -238,6 +241,7 @@ def _update_delivery_method( external_shipping_method: Optional[shipping_interface.ShippingMethodData], collection_point: Optional[Warehouse], ) -> None: + checkout_fields_to_update = ["shipping_method", "collection_point"] checkout = checkout_info.checkout if external_shipping_method: set_external_shipping_id( @@ -247,15 +251,19 @@ def _update_delivery_method( delete_external_shipping_id(checkout=checkout) checkout.shipping_method = shipping_method checkout.collection_point = collection_point + if ( + collection_point is not None + and collection_point.click_and_collect_option + == WarehouseClickAndCollectOption.LOCAL_STOCK + ): + checkout.shipping_address = collection_point.address + checkout_info.shipping_address = collection_point.address + checkout_fields_to_update += ["shipping_address"] invalidate_prices_updated_fields = invalidate_checkout( checkout_info, lines, manager, save=False ) checkout.save( - update_fields=[ - "shipping_method", - "collection_point", - ] - + invalidate_prices_updated_fields + update_fields=checkout_fields_to_update + invalidate_prices_updated_fields ) get_or_create_checkout_metadata(checkout).save() cls.call_event(manager.checkout_updated, checkout) @@ -324,7 +332,6 @@ def perform_mutation( } ) type_name = cls._resolve_delivery_method_type(delivery_method_id) - checkout_info = fetch_checkout_info(checkout, lines, manager) if type_name == "Warehouse": return cls.perform_on_collection_point(
saleor/graphql/checkout/mutations/checkout_shipping_address_update.py+11 −0 modified@@ -33,6 +33,7 @@ from ..types import Checkout from .checkout_create import CheckoutAddressValidationRules from .utils import ( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, ERROR_DOES_NOT_SHIP, check_lines_quantity, get_checkout, @@ -153,6 +154,16 @@ def perform_mutation( ) } ) + # prevent from changing the shipping address when click and collect is used. + if checkout.collection_point_id: + raise ValidationError( + { + "shipping_address": ValidationError( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, + code=CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.value, + ) + } + ) address_validation_rules = validation_rules or {} shipping_address_instance = cls.validate_address( shipping_address,
saleor/graphql/checkout/mutations/utils.py+4 −0 modified@@ -46,6 +46,10 @@ ERROR_DOES_NOT_SHIP = "This checkout doesn't need shipping" +ERROR_CC_ADDRESS_CHANGE_FORBIDDEN = ( + "Can't change shipping address manually. " + "For click and collect delivery, address is set to a warehouse address." +) @dataclass
saleor/graphql/checkout/tests/mutations/test_checkout_delivery_method_update.py+251 −33 modified@@ -447,7 +447,116 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( assert checkout.shipping_method == shipping_method -@pytest.mark.parametrize("is_valid_delivery_method", [True, False]) +@pytest.mark.parametrize( + ("delivery_method", "node_name", "attribute_name"), + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create(country="US") + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", # noqa: PT006 + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, + warehouse_for_cc, +): + # given + mock_clean_delivery.return_value = True + checkout_address = Address.objects.create(country="US") + checkout = checkout_with_item_for_cc + checkout.shipping_address = checkout_address + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + assert checkout.shipping_address == delivery_method.address + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + @pytest.mark.parametrize( ("delivery_method", "node_name", "attribute_name"), [ @@ -460,17 +569,16 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_all_required_shipping_address_data( +def test_checkout_delivery_method_update_invalid_method_not_all_shipping_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create(country="US") @@ -486,7 +594,7 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -503,22 +611,16 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None -@pytest.mark.parametrize("is_valid_delivery_method", [True, False]) @pytest.mark.parametrize( - ("delivery_method", "node_name", "attribute_name"), + "delivery_method, node_name, attribute_name", # noqa: PT006 [ ("warehouse", "Warehouse", "collection_point"), ("shipping_method", "ShippingMethod", "shipping_method"), @@ -529,17 +631,16 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_valid_address_data( +def test_checkout_delivery_method_update_invalid_with_not_valid_address_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create( @@ -561,7 +662,7 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -578,17 +679,134 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", # noqa: PT006 + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", # noqa: PT006 + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method def test_with_active_problems_flow(
saleor/graphql/checkout/tests/mutations/test_checkout_shipping_address_update.py+26 −0 modified@@ -989,3 +989,29 @@ def test_with_active_problems_flow( # then assert not content["data"]["checkoutShippingAddressUpdate"]["errors"] + + +def test_checkout_shipping_address_update_with_collection_point_already_set( + user_api_client, + checkout_with_item, + graphql_address_data, + warehouse_for_cc, +): + checkout = checkout_with_item + checkout.collection_point_id = warehouse_for_cc.id + checkout.save(update_fields=["collection_point_id"]) + + shipping_address = graphql_address_data + variables = { + "id": to_global_id_or_none(checkout), + "shippingAddress": shipping_address, + } + + response = user_api_client.post_graphql( + MUTATION_CHECKOUT_SHIPPING_ADDRESS_UPDATE, variables + ) + content = get_graphql_content(response) + data = content["data"]["checkoutShippingAddressUpdate"] + errors = data["errors"] + assert errors[0]["code"] == CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.name + assert errors[0]["field"] == "shippingAddress"
saleor/graphql/schema.graphql+3 −2 modified@@ -19056,7 +19056,7 @@ type Mutation { ): CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo(asyncEvents: [CHECKOUT_UPDATED], syncEvents: [SHIPPING_LIST_METHODS_FOR_CHECKOUT]) @deprecated(reason: "This field will be removed in Saleor 4.0. Use `checkoutDeliveryMethodUpdate` instead.") """ - Updates the delivery method (shipping method or pick up point) of the checkout. + Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1. @@ -28897,6 +28897,7 @@ enum CheckoutErrorCode @doc(category: "Checkout") { INACTIVE_PAYMENT NON_EDITABLE_GIFT_LINE NON_REMOVABLE_GIFT_LINE + SHIPPING_CHANGE_FORBIDDEN } """ @@ -29333,7 +29334,7 @@ type CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo( } """ -Updates the delivery method (shipping method or pick up point) of the checkout. +Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1.
d8ba545c16adFix tax calculation for Click and Collect option. (#15491)
7 files changed · +310 −42
saleor/checkout/error_codes.py+1 −0 modified@@ -32,6 +32,7 @@ class CheckoutErrorCode(Enum): INACTIVE_PAYMENT = "inactive_payment" NON_EDITABLE_GIFT_LINE = "non_editable_gift_line" NON_REMOVABLE_GIFT_LINE = "non_removable_gift_line" + SHIPPING_CHANGE_FORBIDDEN = "shipping_change_forbidden" class OrderCreateFromCheckoutErrorCode(Enum):
saleor/graphql/checkout/mutations/checkout_delivery_method_update.py+14 −7 modified@@ -21,6 +21,7 @@ from ....shipping import interface as shipping_interface from ....shipping import models as shipping_models from ....shipping.utils import convert_to_shipping_method_data +from ....warehouse import WarehouseClickAndCollectOption from ....warehouse import models as warehouse_models from ....webhook.const import APP_ID_PREFIX from ....webhook.event_types import WebhookEventAsyncType, WebhookEventSyncType @@ -59,7 +60,9 @@ class Arguments: class Meta: description = ( "Updates the delivery method (shipping method or pick up point) " - "of the checkout." + ADDED_IN_31 + "of the checkout. " + "Updates the checkout shipping_address for click and collect delivery " + "for a warehouse address. " + ADDED_IN_31 ) doc_category = DOC_CATEGORY_CHECKOUT error_type_class = CheckoutError @@ -238,6 +241,7 @@ def _update_delivery_method( external_shipping_method: Optional[shipping_interface.ShippingMethodData], collection_point: Optional[Warehouse], ) -> None: + checkout_fields_to_update = ["shipping_method", "collection_point"] checkout = checkout_info.checkout if external_shipping_method: set_external_shipping_id( @@ -247,15 +251,19 @@ def _update_delivery_method( delete_external_shipping_id(checkout=checkout) checkout.shipping_method = shipping_method checkout.collection_point = collection_point + if ( + collection_point is not None + and collection_point.click_and_collect_option + == WarehouseClickAndCollectOption.LOCAL_STOCK + ): + checkout.shipping_address = collection_point.address + checkout_info.shipping_address = collection_point.address + checkout_fields_to_update += ["shipping_address"] invalidate_prices_updated_fields = invalidate_checkout( checkout_info, lines, manager, save=False ) checkout.save( - update_fields=[ - "shipping_method", - "collection_point", - ] - + invalidate_prices_updated_fields + update_fields=checkout_fields_to_update + invalidate_prices_updated_fields ) get_or_create_checkout_metadata(checkout).save() cls.call_event(manager.checkout_updated, checkout) @@ -324,7 +332,6 @@ def perform_mutation( } ) type_name = cls._resolve_delivery_method_type(delivery_method_id) - checkout_info = fetch_checkout_info(checkout, lines, manager) if type_name == "Warehouse": return cls.perform_on_collection_point(
saleor/graphql/checkout/mutations/checkout_shipping_address_update.py+11 −0 modified@@ -33,6 +33,7 @@ from ..types import Checkout from .checkout_create import CheckoutAddressValidationRules from .utils import ( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, ERROR_DOES_NOT_SHIP, check_lines_quantity, get_checkout, @@ -153,6 +154,16 @@ def perform_mutation( ) } ) + # prevent from changing the shipping address when click and collect is used. + if checkout.collection_point_id: + raise ValidationError( + { + "shipping_address": ValidationError( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, + code=CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.value, + ) + } + ) address_validation_rules = validation_rules or {} shipping_address_instance = cls.validate_address( shipping_address,
saleor/graphql/checkout/mutations/utils.py+4 −0 modified@@ -46,6 +46,10 @@ ERROR_DOES_NOT_SHIP = "This checkout doesn't need shipping" +ERROR_CC_ADDRESS_CHANGE_FORBIDDEN = ( + "Can't change shipping address manually. " + "For click and collect delivery, address is set to a warehouse address." +) @dataclass
saleor/graphql/checkout/tests/mutations/test_checkout_delivery_method_update.py+251 −33 modified@@ -447,7 +447,116 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( assert checkout.shipping_method == shipping_method -@pytest.mark.parametrize("is_valid_delivery_method", [True, False]) +@pytest.mark.parametrize( + ("delivery_method", "node_name", "attribute_name"), + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create(country="US") + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", # noqa: PT006 + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, + warehouse_for_cc, +): + # given + mock_clean_delivery.return_value = True + checkout_address = Address.objects.create(country="US") + checkout = checkout_with_item_for_cc + checkout.shipping_address = checkout_address + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + assert checkout.shipping_address == delivery_method.address + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + @pytest.mark.parametrize( ("delivery_method", "node_name", "attribute_name"), [ @@ -460,17 +569,16 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_all_required_shipping_address_data( +def test_checkout_delivery_method_update_invalid_method_not_all_shipping_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create(country="US") @@ -486,7 +594,7 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -503,22 +611,16 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None -@pytest.mark.parametrize("is_valid_delivery_method", [True, False]) @pytest.mark.parametrize( - ("delivery_method", "node_name", "attribute_name"), + "delivery_method, node_name, attribute_name", # noqa: PT006 [ ("warehouse", "Warehouse", "collection_point"), ("shipping_method", "ShippingMethod", "shipping_method"), @@ -529,17 +631,16 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_valid_address_data( +def test_checkout_delivery_method_update_invalid_with_not_valid_address_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create( @@ -561,7 +662,7 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -578,17 +679,134 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", # noqa: PT006 + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", # noqa: PT006 + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method def test_with_active_problems_flow(
saleor/graphql/checkout/tests/mutations/test_checkout_shipping_address_update.py+26 −0 modified@@ -989,3 +989,29 @@ def test_with_active_problems_flow( # then assert not content["data"]["checkoutShippingAddressUpdate"]["errors"] + + +def test_checkout_shipping_address_update_with_collection_point_already_set( + user_api_client, + checkout_with_item, + graphql_address_data, + warehouse_for_cc, +): + checkout = checkout_with_item + checkout.collection_point_id = warehouse_for_cc.id + checkout.save(update_fields=["collection_point_id"]) + + shipping_address = graphql_address_data + variables = { + "id": to_global_id_or_none(checkout), + "shippingAddress": shipping_address, + } + + response = user_api_client.post_graphql( + MUTATION_CHECKOUT_SHIPPING_ADDRESS_UPDATE, variables + ) + content = get_graphql_content(response) + data = content["data"]["checkoutShippingAddressUpdate"] + errors = data["errors"] + assert errors[0]["code"] == CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.name + assert errors[0]["field"] == "shippingAddress"
saleor/graphql/schema.graphql+3 −2 modified@@ -19056,7 +19056,7 @@ type Mutation { ): CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo(asyncEvents: [CHECKOUT_UPDATED], syncEvents: [SHIPPING_LIST_METHODS_FOR_CHECKOUT]) @deprecated(reason: "This field will be removed in Saleor 4.0. Use `checkoutDeliveryMethodUpdate` instead.") """ - Updates the delivery method (shipping method or pick up point) of the checkout. + Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1. @@ -28897,6 +28897,7 @@ enum CheckoutErrorCode @doc(category: "Checkout") { INACTIVE_PAYMENT NON_EDITABLE_GIFT_LINE NON_REMOVABLE_GIFT_LINE + SHIPPING_CHANGE_FORBIDDEN } """ @@ -29333,7 +29334,7 @@ type CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo( } """ -Updates the delivery method (shipping method or pick up point) of the checkout. +Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1.
dccc2c842b4eFix tax calculation for Click and Collect option. (#15490)
7 files changed · +310 −42
saleor/checkout/error_codes.py+1 −0 modified@@ -30,6 +30,7 @@ class CheckoutErrorCode(Enum): EMAIL_NOT_SET = "email_not_set" NO_LINES = "no_lines" INACTIVE_PAYMENT = "inactive_payment" + SHIPPING_CHANGE_FORBIDDEN = "shipping_change_forbidden" class OrderCreateFromCheckoutErrorCode(Enum):
saleor/graphql/checkout/mutations/checkout_delivery_method_update.py+14 −7 modified@@ -21,6 +21,7 @@ from ....shipping import interface as shipping_interface from ....shipping import models as shipping_models from ....shipping.utils import convert_to_shipping_method_data +from ....warehouse import WarehouseClickAndCollectOption from ....warehouse import models as warehouse_models from ....webhook.const import APP_ID_PREFIX from ....webhook.event_types import WebhookEventAsyncType, WebhookEventSyncType @@ -59,7 +60,9 @@ class Arguments: class Meta: description = ( "Updates the delivery method (shipping method or pick up point) " - "of the checkout." + ADDED_IN_31 + "of the checkout. " + "Updates the checkout shipping_address for click and collect delivery " + "for a warehouse address. " + ADDED_IN_31 ) doc_category = DOC_CATEGORY_CHECKOUT error_type_class = CheckoutError @@ -238,6 +241,7 @@ def _update_delivery_method( external_shipping_method: Optional[shipping_interface.ShippingMethodData], collection_point: Optional[Warehouse], ) -> None: + checkout_fields_to_update = ["shipping_method", "collection_point"] checkout = checkout_info.checkout if external_shipping_method: set_external_shipping_id( @@ -247,15 +251,19 @@ def _update_delivery_method( delete_external_shipping_id(checkout=checkout) checkout.shipping_method = shipping_method checkout.collection_point = collection_point + if ( + collection_point is not None + and collection_point.click_and_collect_option + == WarehouseClickAndCollectOption.LOCAL_STOCK + ): + checkout.shipping_address = collection_point.address + checkout_info.shipping_address = collection_point.address + checkout_fields_to_update += ["shipping_address"] invalidate_prices_updated_fields = invalidate_checkout_prices( checkout_info, lines, manager, save=False ) checkout.save( - update_fields=[ - "shipping_method", - "collection_point", - ] - + invalidate_prices_updated_fields + update_fields=checkout_fields_to_update + invalidate_prices_updated_fields ) get_or_create_checkout_metadata(checkout).save() cls.call_event(manager.checkout_updated, checkout) @@ -324,7 +332,6 @@ def perform_mutation( } ) type_name = cls._resolve_delivery_method_type(delivery_method_id) - checkout_info = fetch_checkout_info(checkout, lines, manager) if type_name == "Warehouse": return cls.perform_on_collection_point(
saleor/graphql/checkout/mutations/checkout_shipping_address_update.py+11 −0 modified@@ -33,6 +33,7 @@ from ..types import Checkout from .checkout_create import CheckoutAddressValidationRules from .utils import ( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, ERROR_DOES_NOT_SHIP, check_lines_quantity, get_checkout, @@ -153,6 +154,16 @@ def perform_mutation( ) } ) + # prevent from changing the shipping address when click and collect is used. + if checkout.collection_point_id: + raise ValidationError( + { + "shipping_address": ValidationError( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, + code=CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.value, + ) + } + ) address_validation_rules = validation_rules or {} shipping_address_instance = cls.validate_address( shipping_address,
saleor/graphql/checkout/mutations/utils.py+4 −0 modified@@ -41,6 +41,10 @@ ERROR_DOES_NOT_SHIP = "This checkout doesn't need shipping" +ERROR_CC_ADDRESS_CHANGE_FORBIDDEN = ( + "Can't change shipping address manually. " + "For click and collect delivery, address is set to a warehouse address." +) @dataclass
saleor/graphql/checkout/tests/mutations/test_checkout_delivery_method_update.py+251 −33 modified@@ -447,7 +447,116 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( assert checkout.shipping_method == shipping_method -@pytest.mark.parametrize("is_valid_delivery_method", [True, False]) +@pytest.mark.parametrize( + ("delivery_method", "node_name", "attribute_name"), + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create(country="US") + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", # noqa: PT006 + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, + warehouse_for_cc, +): + # given + mock_clean_delivery.return_value = True + checkout_address = Address.objects.create(country="US") + checkout = checkout_with_item_for_cc + checkout.shipping_address = checkout_address + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + assert checkout.shipping_address == delivery_method.address + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + @pytest.mark.parametrize( ("delivery_method", "node_name", "attribute_name"), [ @@ -460,17 +569,16 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_all_required_shipping_address_data( +def test_checkout_delivery_method_update_invalid_method_not_all_shipping_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create(country="US") @@ -486,7 +594,7 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -503,22 +611,16 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None -@pytest.mark.parametrize("is_valid_delivery_method", [True, False]) @pytest.mark.parametrize( - ("delivery_method", "node_name", "attribute_name"), + "delivery_method, node_name, attribute_name", # noqa: PT006 [ ("warehouse", "Warehouse", "collection_point"), ("shipping_method", "ShippingMethod", "shipping_method"), @@ -529,17 +631,16 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_valid_address_data( +def test_checkout_delivery_method_update_invalid_with_not_valid_address_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create( @@ -561,7 +662,7 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -578,17 +679,134 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", # noqa: PT006 + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", # noqa: PT006 + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method def test_with_active_problems_flow(
saleor/graphql/checkout/tests/mutations/test_checkout_shipping_address_update.py+26 −0 modified@@ -989,3 +989,29 @@ def test_with_active_problems_flow( # then assert not content["data"]["checkoutShippingAddressUpdate"]["errors"] + + +def test_checkout_shipping_address_update_with_collection_point_already_set( + user_api_client, + checkout_with_item, + graphql_address_data, + warehouse_for_cc, +): + checkout = checkout_with_item + checkout.collection_point_id = warehouse_for_cc.id + checkout.save(update_fields=["collection_point_id"]) + + shipping_address = graphql_address_data + variables = { + "id": to_global_id_or_none(checkout), + "shippingAddress": shipping_address, + } + + response = user_api_client.post_graphql( + MUTATION_CHECKOUT_SHIPPING_ADDRESS_UPDATE, variables + ) + content = get_graphql_content(response) + data = content["data"]["checkoutShippingAddressUpdate"] + errors = data["errors"] + assert errors[0]["code"] == CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.name + assert errors[0]["field"] == "shippingAddress"
saleor/graphql/schema.graphql+3 −2 modified@@ -18850,7 +18850,7 @@ type Mutation { ): CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo(asyncEvents: [CHECKOUT_UPDATED], syncEvents: [SHIPPING_LIST_METHODS_FOR_CHECKOUT]) @deprecated(reason: "This field will be removed in Saleor 4.0. Use `checkoutDeliveryMethodUpdate` instead.") """ - Updates the delivery method (shipping method or pick up point) of the checkout. + Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1. @@ -28513,6 +28513,7 @@ enum CheckoutErrorCode @doc(category: "Checkout") { EMAIL_NOT_SET NO_LINES INACTIVE_PAYMENT + SHIPPING_CHANGE_FORBIDDEN } """ @@ -28949,7 +28950,7 @@ type CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo( } """ -Updates the delivery method (shipping method or pick up point) of the checkout. +Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1.
b7cecda8b603Fix tax calculation for Click and Collect option. (#15489)
7 files changed · +309 −41
saleor/checkout/error_codes.py+1 −0 modified@@ -30,6 +30,7 @@ class CheckoutErrorCode(Enum): EMAIL_NOT_SET = "email_not_set" NO_LINES = "no_lines" INACTIVE_PAYMENT = "inactive_payment" + SHIPPING_CHANGE_FORBIDDEN = "shipping_change_forbidden" class OrderCreateFromCheckoutErrorCode(Enum):
saleor/graphql/checkout/mutations/checkout_delivery_method_update.py+14 −7 modified@@ -20,6 +20,7 @@ from ....shipping import interface as shipping_interface from ....shipping import models as shipping_models from ....shipping.utils import convert_to_shipping_method_data +from ....warehouse import WarehouseClickAndCollectOption from ....warehouse import models as warehouse_models from ....webhook.const import APP_ID_PREFIX from ....webhook.event_types import WebhookEventAsyncType, WebhookEventSyncType @@ -58,7 +59,9 @@ class Arguments: class Meta: description = ( "Updates the delivery method (shipping method or pick up point) " - "of the checkout." + ADDED_IN_31 + "of the checkout. " + "Updates the checkout shipping_address for click and collect delivery " + "for a warehouse address. " + ADDED_IN_31 ) doc_category = DOC_CATEGORY_CHECKOUT error_type_class = CheckoutError @@ -237,6 +240,7 @@ def _update_delivery_method( external_shipping_method: Optional[shipping_interface.ShippingMethodData], collection_point: Optional[Warehouse], ) -> None: + checkout_fields_to_update = ["shipping_method", "collection_point"] checkout = checkout_info.checkout if external_shipping_method: set_external_shipping_id( @@ -246,15 +250,19 @@ def _update_delivery_method( delete_external_shipping_id(checkout=checkout) checkout.shipping_method = shipping_method checkout.collection_point = collection_point + if ( + collection_point is not None + and collection_point.click_and_collect_option + == WarehouseClickAndCollectOption.LOCAL_STOCK + ): + checkout.shipping_address = collection_point.address + checkout_info.shipping_address = collection_point.address + checkout_fields_to_update += ["shipping_address"] invalidate_prices_updated_fields = invalidate_checkout_prices( checkout_info, lines, manager, save=False ) checkout.save( - update_fields=[ - "shipping_method", - "collection_point", - ] - + invalidate_prices_updated_fields + update_fields=checkout_fields_to_update + invalidate_prices_updated_fields ) get_or_create_checkout_metadata(checkout).save() cls.call_event(manager.checkout_updated, checkout) @@ -323,7 +331,6 @@ def perform_mutation( } ) type_name = cls._resolve_delivery_method_type(delivery_method_id) - checkout_info = fetch_checkout_info(checkout, lines, manager) if type_name == "Warehouse": return cls.perform_on_collection_point(
saleor/graphql/checkout/mutations/checkout_shipping_address_update.py+11 −0 modified@@ -32,6 +32,7 @@ from ..types import Checkout from .checkout_create import CheckoutAddressValidationRules from .utils import ( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, ERROR_DOES_NOT_SHIP, check_lines_quantity, get_checkout, @@ -152,6 +153,16 @@ def perform_mutation( ) } ) + # prevent from changing the shipping address when click and collect is used. + if checkout.collection_point_id: + raise ValidationError( + { + "shipping_address": ValidationError( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, + code=CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.value, + ) + } + ) address_validation_rules = validation_rules or {} shipping_address_instance = cls.validate_address( shipping_address,
saleor/graphql/checkout/mutations/utils.py+4 −0 modified@@ -45,6 +45,10 @@ ERROR_DOES_NOT_SHIP = "This checkout doesn't need shipping" +ERROR_CC_ADDRESS_CHANGE_FORBIDDEN = ( + "Can't change shipping address manually. " + "For click and collect delivery, address is set to a warehouse address." +) @dataclass
saleor/graphql/checkout/tests/mutations/test_checkout_delivery_method_update.py+250 −32 modified@@ -447,7 +447,116 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( assert checkout.shipping_method == shipping_method -@pytest.mark.parametrize("is_valid_delivery_method", (True, False)) +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create(country="US") + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, + warehouse_for_cc, +): + # given + mock_clean_delivery.return_value = True + checkout_address = Address.objects.create(country="US") + checkout = checkout_with_item_for_cc + checkout.shipping_address = checkout_address + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + assert checkout.shipping_address == delivery_method.address + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + @pytest.mark.parametrize( "delivery_method, node_name, attribute_name", [ @@ -460,17 +569,16 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_all_required_shipping_address_data( +def test_checkout_delivery_method_update_invalid_method_not_all_shipping_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create(country="US") @@ -486,7 +594,7 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -503,20 +611,14 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None -@pytest.mark.parametrize("is_valid_delivery_method", (True, False)) @pytest.mark.parametrize( "delivery_method, node_name, attribute_name", [ @@ -529,17 +631,16 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_valid_address_data( +def test_checkout_delivery_method_update_invalid_with_not_valid_address_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create( @@ -561,7 +662,7 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -578,17 +679,134 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method def test_with_active_problems_flow(
saleor/graphql/checkout/tests/mutations/test_checkout_shipping_address_update.py+26 −0 modified@@ -988,3 +988,29 @@ def test_with_active_problems_flow( # then assert not content["data"]["checkoutShippingAddressUpdate"]["errors"] + + +def test_checkout_shipping_address_update_with_collection_point_already_set( + user_api_client, + checkout_with_item, + graphql_address_data, + warehouse_for_cc, +): + checkout = checkout_with_item + checkout.collection_point_id = warehouse_for_cc.id + checkout.save(update_fields=["collection_point_id"]) + + shipping_address = graphql_address_data + variables = { + "id": to_global_id_or_none(checkout), + "shippingAddress": shipping_address, + } + + response = user_api_client.post_graphql( + MUTATION_CHECKOUT_SHIPPING_ADDRESS_UPDATE, variables + ) + content = get_graphql_content(response) + data = content["data"]["checkoutShippingAddressUpdate"] + errors = data["errors"] + assert errors[0]["code"] == CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.name + assert errors[0]["field"] == "shippingAddress"
saleor/graphql/schema.graphql+3 −2 modified@@ -18697,7 +18697,7 @@ type Mutation { ): CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo(asyncEvents: [CHECKOUT_UPDATED], syncEvents: [SHIPPING_LIST_METHODS_FOR_CHECKOUT]) @deprecated(reason: "This field will be removed in Saleor 4.0. Use `checkoutDeliveryMethodUpdate` instead.") """ - Updates the delivery method (shipping method or pick up point) of the checkout. + Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1. @@ -28234,6 +28234,7 @@ enum CheckoutErrorCode @doc(category: "Checkout") { EMAIL_NOT_SET NO_LINES INACTIVE_PAYMENT + SHIPPING_CHANGE_FORBIDDEN } """ @@ -28670,7 +28671,7 @@ type CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo( } """ -Updates the delivery method (shipping method or pick up point) of the checkout. +Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1.
39abb0f4e4feFix tax calculation for Click and Collect option. (#15488)
7 files changed · +309 −41
saleor/checkout/error_codes.py+1 −0 modified@@ -30,6 +30,7 @@ class CheckoutErrorCode(Enum): EMAIL_NOT_SET = "email_not_set" NO_LINES = "no_lines" INACTIVE_PAYMENT = "inactive_payment" + SHIPPING_CHANGE_FORBIDDEN = "shipping_change_forbidden" class OrderCreateFromCheckoutErrorCode(Enum):
saleor/graphql/checkout/mutations/checkout_delivery_method_update.py+14 −7 modified@@ -21,6 +21,7 @@ from ....shipping import interface as shipping_interface from ....shipping import models as shipping_models from ....shipping.utils import convert_to_shipping_method_data +from ....warehouse import WarehouseClickAndCollectOption from ....warehouse import models as warehouse_models from ....webhook.event_types import WebhookEventAsyncType, WebhookEventSyncType from ...core import ResolveInfo @@ -58,7 +59,9 @@ class Arguments: class Meta: description = ( "Updates the delivery method (shipping method or pick up point) " - "of the checkout." + ADDED_IN_31 + "of the checkout. " + "Updates the checkout shipping_address for click and collect delivery " + "for a warehouse address. " + ADDED_IN_31 ) doc_category = DOC_CATEGORY_CHECKOUT error_type_class = CheckoutError @@ -237,6 +240,7 @@ def _update_delivery_method( external_shipping_method: Optional[shipping_interface.ShippingMethodData], collection_point: Optional[Warehouse], ) -> None: + checkout_fields_to_update = ["shipping_method", "collection_point"] checkout = checkout_info.checkout if external_shipping_method: set_external_shipping_id( @@ -246,15 +250,19 @@ def _update_delivery_method( delete_external_shipping_id(checkout=checkout) checkout.shipping_method = shipping_method checkout.collection_point = collection_point + if ( + collection_point is not None + and collection_point.click_and_collect_option + == WarehouseClickAndCollectOption.LOCAL_STOCK + ): + checkout.shipping_address = collection_point.address + checkout_info.shipping_address = collection_point.address + checkout_fields_to_update += ["shipping_address"] invalidate_prices_updated_fields = invalidate_checkout_prices( checkout_info, lines, manager, save=False ) checkout.save( - update_fields=[ - "shipping_method", - "collection_point", - ] - + invalidate_prices_updated_fields + update_fields=checkout_fields_to_update + invalidate_prices_updated_fields ) get_or_create_checkout_metadata(checkout).save() cls.call_event(manager.checkout_updated, checkout) @@ -323,7 +331,6 @@ def perform_mutation( } ) type_name = cls._resolve_delivery_method_type(delivery_method_id) - checkout_info = fetch_checkout_info(checkout, lines, manager) if type_name == "Warehouse": return cls.perform_on_collection_point(
saleor/graphql/checkout/mutations/checkout_shipping_address_update.py+11 −0 modified@@ -32,6 +32,7 @@ from ..types import Checkout from .checkout_create import CheckoutAddressValidationRules from .utils import ( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, ERROR_DOES_NOT_SHIP, check_lines_quantity, get_checkout, @@ -152,6 +153,16 @@ def perform_mutation( ) } ) + # prevent from changing the shipping address when click and collect is used. + if checkout.collection_point_id: + raise ValidationError( + { + "shipping_address": ValidationError( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, + code=CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.value, + ) + } + ) address_validation_rules = validation_rules or {} shipping_address_instance = cls.validate_address( shipping_address,
saleor/graphql/checkout/mutations/utils.py+4 −0 modified@@ -45,6 +45,10 @@ ERROR_DOES_NOT_SHIP = "This checkout doesn't need shipping" +ERROR_CC_ADDRESS_CHANGE_FORBIDDEN = ( + "Can't change shipping address manually. " + "For click and collect delivery, address is set to a warehouse address." +) @dataclass
saleor/graphql/checkout/tests/mutations/test_checkout_delivery_method_update.py+250 −32 modified@@ -447,7 +447,116 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( assert checkout.shipping_method == shipping_method -@pytest.mark.parametrize("is_valid_delivery_method", (True, False)) +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create(country="US") + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, + warehouse_for_cc, +): + # given + mock_clean_delivery.return_value = True + checkout_address = Address.objects.create(country="US") + checkout = checkout_with_item_for_cc + checkout.shipping_address = checkout_address + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + assert checkout.shipping_address == delivery_method.address + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + @pytest.mark.parametrize( "delivery_method, node_name, attribute_name", [ @@ -460,17 +569,16 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_all_required_shipping_address_data( +def test_checkout_delivery_method_update_invalid_method_not_all_shipping_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create(country="US") @@ -486,7 +594,7 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -503,20 +611,14 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None -@pytest.mark.parametrize("is_valid_delivery_method", (True, False)) @pytest.mark.parametrize( "delivery_method, node_name, attribute_name", [ @@ -529,17 +631,16 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_valid_address_data( +def test_checkout_delivery_method_update_invalid_with_not_valid_address_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create( @@ -561,7 +662,7 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -578,17 +679,134 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method def test_with_active_problems_flow(
saleor/graphql/checkout/tests/mutations/test_checkout_shipping_address_update.py+26 −0 modified@@ -988,3 +988,29 @@ def test_with_active_problems_flow( # then assert not content["data"]["checkoutShippingAddressUpdate"]["errors"] + + +def test_checkout_shipping_address_update_with_collection_point_already_set( + user_api_client, + checkout_with_item, + graphql_address_data, + warehouse_for_cc, +): + checkout = checkout_with_item + checkout.collection_point_id = warehouse_for_cc.id + checkout.save(update_fields=["collection_point_id"]) + + shipping_address = graphql_address_data + variables = { + "id": to_global_id_or_none(checkout), + "shippingAddress": shipping_address, + } + + response = user_api_client.post_graphql( + MUTATION_CHECKOUT_SHIPPING_ADDRESS_UPDATE, variables + ) + content = get_graphql_content(response) + data = content["data"]["checkoutShippingAddressUpdate"] + errors = data["errors"] + assert errors[0]["code"] == CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.name + assert errors[0]["field"] == "shippingAddress"
saleor/graphql/schema.graphql+3 −2 modified@@ -17796,7 +17796,7 @@ type Mutation { ): CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo(asyncEvents: [CHECKOUT_UPDATED], syncEvents: [SHIPPING_LIST_METHODS_FOR_CHECKOUT]) @deprecated(reason: "This field will be removed in Saleor 4.0. Use `checkoutDeliveryMethodUpdate` instead.") """ - Updates the delivery method (shipping method or pick up point) of the checkout. + Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1. @@ -26874,6 +26874,7 @@ enum CheckoutErrorCode @doc(category: "Checkout") { EMAIL_NOT_SET NO_LINES INACTIVE_PAYMENT + SHIPPING_CHANGE_FORBIDDEN } """ @@ -27310,7 +27311,7 @@ type CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo( } """ -Updates the delivery method (shipping method or pick up point) of the checkout. +Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1.
ef003c76a304Fix tax calculation for Click and Collect option. (#15487)
7 files changed · +309 −41
saleor/checkout/error_codes.py+1 −0 modified@@ -30,6 +30,7 @@ class CheckoutErrorCode(Enum): EMAIL_NOT_SET = "email_not_set" NO_LINES = "no_lines" INACTIVE_PAYMENT = "inactive_payment" + SHIPPING_CHANGE_FORBIDDEN = "shipping_change_forbidden" class OrderCreateFromCheckoutErrorCode(Enum):
saleor/graphql/checkout/mutations/checkout_delivery_method_update.py+14 −7 modified@@ -21,6 +21,7 @@ from ....shipping import interface as shipping_interface from ....shipping import models as shipping_models from ....shipping.utils import convert_to_shipping_method_data +from ....warehouse import WarehouseClickAndCollectOption from ....warehouse import models as warehouse_models from ....webhook.event_types import WebhookEventAsyncType, WebhookEventSyncType from ...core import ResolveInfo @@ -58,7 +59,9 @@ class Arguments: class Meta: description = ( "Updates the delivery method (shipping method or pick up point) " - "of the checkout." + ADDED_IN_31 + "of the checkout. " + "Updates the checkout shipping_address for click and collect delivery " + "for a warehouse address. " + ADDED_IN_31 ) doc_category = DOC_CATEGORY_CHECKOUT error_type_class = CheckoutError @@ -237,6 +240,7 @@ def _update_delivery_method( external_shipping_method: Optional[shipping_interface.ShippingMethodData], collection_point: Optional[Warehouse], ) -> None: + checkout_fields_to_update = ["shipping_method", "collection_point"] checkout = checkout_info.checkout if external_shipping_method: set_external_shipping_id( @@ -246,15 +250,19 @@ def _update_delivery_method( delete_external_shipping_id(checkout=checkout) checkout.shipping_method = shipping_method checkout.collection_point = collection_point + if ( + collection_point is not None + and collection_point.click_and_collect_option + == WarehouseClickAndCollectOption.LOCAL_STOCK + ): + checkout.shipping_address = collection_point.address + checkout_info.shipping_address = collection_point.address + checkout_fields_to_update += ["shipping_address"] invalidate_prices_updated_fields = invalidate_checkout_prices( checkout_info, lines, manager, save=False ) checkout.save( - update_fields=[ - "shipping_method", - "collection_point", - ] - + invalidate_prices_updated_fields + update_fields=checkout_fields_to_update + invalidate_prices_updated_fields ) get_or_create_checkout_metadata(checkout).save() cls.call_event(manager.checkout_updated, checkout) @@ -323,7 +331,6 @@ def perform_mutation( } ) type_name = cls._resolve_delivery_method_type(delivery_method_id) - checkout_info = fetch_checkout_info(checkout, lines, manager) if type_name == "Warehouse": return cls.perform_on_collection_point(
saleor/graphql/checkout/mutations/checkout_shipping_address_update.py+11 −0 modified@@ -32,6 +32,7 @@ from ..types import Checkout from .checkout_create import CheckoutAddressValidationRules from .utils import ( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, ERROR_DOES_NOT_SHIP, check_lines_quantity, get_checkout, @@ -152,6 +153,16 @@ def perform_mutation( ) } ) + # prevent from changing the shipping address when click and collect is used. + if checkout.collection_point_id: + raise ValidationError( + { + "shipping_address": ValidationError( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, + code=CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.value, + ) + } + ) address_validation_rules = validation_rules or {} shipping_address_instance = cls.validate_address( shipping_address,
saleor/graphql/checkout/mutations/utils.py+4 −0 modified@@ -45,6 +45,10 @@ ERROR_DOES_NOT_SHIP = "This checkout doesn't need shipping" +ERROR_CC_ADDRESS_CHANGE_FORBIDDEN = ( + "Can't change shipping address manually. " + "For click and collect delivery, address is set to a warehouse address." +) @dataclass
saleor/graphql/checkout/tests/mutations/test_checkout_delivery_method_update.py+250 −32 modified@@ -447,7 +447,116 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( assert checkout.shipping_method == shipping_method -@pytest.mark.parametrize("is_valid_delivery_method", (True, False)) +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create(country="US") + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, + warehouse_for_cc, +): + # given + mock_clean_delivery.return_value = True + checkout_address = Address.objects.create(country="US") + checkout = checkout_with_item_for_cc + checkout.shipping_address = checkout_address + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + assert checkout.shipping_address == delivery_method.address + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + @pytest.mark.parametrize( "delivery_method, node_name, attribute_name", [ @@ -460,17 +569,16 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_all_required_shipping_address_data( +def test_checkout_delivery_method_update_invalid_method_not_all_shipping_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create(country="US") @@ -486,7 +594,7 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -503,20 +611,14 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None -@pytest.mark.parametrize("is_valid_delivery_method", (True, False)) @pytest.mark.parametrize( "delivery_method, node_name, attribute_name", [ @@ -529,17 +631,16 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_valid_address_data( +def test_checkout_delivery_method_update_invalid_with_not_valid_address_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create( @@ -561,7 +662,7 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -578,17 +679,134 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method def test_with_active_problems_flow(
saleor/graphql/checkout/tests/mutations/test_checkout_shipping_address_update.py+26 −0 modified@@ -988,3 +988,29 @@ def test_with_active_problems_flow( # then assert not content["data"]["checkoutShippingAddressUpdate"]["errors"] + + +def test_checkout_shipping_address_update_with_collection_point_already_set( + user_api_client, + checkout_with_item, + graphql_address_data, + warehouse_for_cc, +): + checkout = checkout_with_item + checkout.collection_point_id = warehouse_for_cc.id + checkout.save(update_fields=["collection_point_id"]) + + shipping_address = graphql_address_data + variables = { + "id": to_global_id_or_none(checkout), + "shippingAddress": shipping_address, + } + + response = user_api_client.post_graphql( + MUTATION_CHECKOUT_SHIPPING_ADDRESS_UPDATE, variables + ) + content = get_graphql_content(response) + data = content["data"]["checkoutShippingAddressUpdate"] + errors = data["errors"] + assert errors[0]["code"] == CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.name + assert errors[0]["field"] == "shippingAddress"
saleor/graphql/schema.graphql+3 −2 modified@@ -17586,7 +17586,7 @@ type Mutation { ): CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo(asyncEvents: [CHECKOUT_UPDATED], syncEvents: [SHIPPING_LIST_METHODS_FOR_CHECKOUT]) @deprecated(reason: "This field will be removed in Saleor 4.0. Use `checkoutDeliveryMethodUpdate` instead.") """ - Updates the delivery method (shipping method or pick up point) of the checkout. + Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1. @@ -26426,6 +26426,7 @@ enum CheckoutErrorCode @doc(category: "Checkout") { EMAIL_NOT_SET NO_LINES INACTIVE_PAYMENT + SHIPPING_CHANGE_FORBIDDEN } """ @@ -26862,7 +26863,7 @@ type CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo( } """ -Updates the delivery method (shipping method or pick up point) of the checkout. +Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1.
997f7ea4f576Fix `FLAT_RATE` tax calculation for Click and Collect option. (#15426)
8 files changed · +308 −37
saleor/checkout/error_codes.py+1 −0 modified@@ -30,6 +30,7 @@ class CheckoutErrorCode(Enum): EMAIL_NOT_SET = "email_not_set" NO_LINES = "no_lines" INACTIVE_PAYMENT = "inactive_payment" + SHIPPING_CHANGE_FORBIDDEN = "shipping_change_forbidden" class OrderCreateFromCheckoutErrorCode(Enum):
saleor/graphql/checkout/mutations/checkout_delivery_method_update.py+13 −2 modified@@ -21,6 +21,7 @@ from ....shipping import interface as shipping_interface from ....shipping import models as shipping_models from ....shipping.utils import convert_to_shipping_method_data +from ....warehouse import WarehouseClickAndCollectOption from ....warehouse import models as warehouse_models from ...core import ResolveInfo from ...core.descriptions import ADDED_IN_31, ADDED_IN_34, DEPRECATED_IN_3X_INPUT @@ -57,7 +58,9 @@ class Arguments: class Meta: description = ( "Updates the delivery method (shipping method or pick up point) " - "of the checkout." + ADDED_IN_31 + "of the checkout. " + "Updates the checkout shipping_address for click and collect delivery " + "for a warehouse address. " + ADDED_IN_31 ) doc_category = DOC_CATEGORY_CHECKOUT error_type_class = CheckoutError @@ -223,6 +226,7 @@ def _update_delivery_method( external_shipping_method: Optional[shipping_interface.ShippingMethodData], collection_point: Optional[Warehouse], ) -> None: + checkout_fields_to_update = ["shipping_method", "collection_point"] checkout = checkout_info.checkout if external_shipping_method: set_external_shipping_id( @@ -232,6 +236,14 @@ def _update_delivery_method( delete_external_shipping_id(checkout=checkout) checkout.shipping_method = shipping_method checkout.collection_point = collection_point + if ( + collection_point is not None + and collection_point.click_and_collect_option + == WarehouseClickAndCollectOption.LOCAL_STOCK + ): + checkout.shipping_address = collection_point.address + checkout_info.shipping_address = collection_point.address + checkout_fields_to_update += ["shipping_address"] invalidate_prices_updated_fields = invalidate_checkout_prices( checkout_info, lines, manager, save=False ) @@ -305,7 +317,6 @@ def perform_mutation( } ) type_name = cls._resolve_delivery_method_type(delivery_method_id) - checkout_info = fetch_checkout_info(checkout, lines, manager) if type_name == "Warehouse": return cls.perform_on_collection_point(
saleor/graphql/checkout/mutations/checkout_shipping_address_update.py+11 −0 modified@@ -29,6 +29,7 @@ from ..types import Checkout from .checkout_create import CheckoutAddressValidationRules from .utils import ( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, ERROR_DOES_NOT_SHIP, check_lines_quantity, get_checkout, @@ -137,6 +138,16 @@ def perform_mutation( ) } ) + # prevent from changing the shipping address when click and collect is used. + if checkout.collection_point_id: + raise ValidationError( + { + "shipping_address": ValidationError( + ERROR_CC_ADDRESS_CHANGE_FORBIDDEN, + code=CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.value, + ) + } + ) address_validation_rules = validation_rules or {} shipping_address_instance = cls.validate_address( shipping_address,
saleor/graphql/checkout/mutations/utils.py+4 −0 modified@@ -45,6 +45,10 @@ ERROR_DOES_NOT_SHIP = "This checkout doesn't need shipping" +ERROR_CC_ADDRESS_CHANGE_FORBIDDEN = ( + "Can't change shipping address manually. " + "For click and collect delivery, address is set to a warehouse address." +) @dataclass
saleor/graphql/checkout/tests/mutations/test_checkout_delivery_method_update.py+249 −32 modified@@ -447,7 +447,115 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( assert checkout.shipping_method == shipping_method -@pytest.mark.parametrize("is_valid_delivery_method", (True, False)) +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create(country="US") + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_method_not_all_shipping_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create(country="US") + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + @pytest.mark.parametrize( "delivery_method, node_name, attribute_name", [ @@ -460,17 +568,16 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel( "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_all_required_shipping_address_data( +def test_checkout_delivery_method_update_invalid_method_not_all_shipping_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create(country="US") @@ -486,7 +593,7 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -503,20 +610,14 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None -@pytest.mark.parametrize("is_valid_delivery_method", (True, False)) @pytest.mark.parametrize( "delivery_method, node_name, attribute_name", [ @@ -529,17 +630,16 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_ "saleor.graphql.checkout.mutations.checkout_delivery_method_update." "clean_delivery_method" ) -def test_checkout_delivery_method_update_with_not_valid_address_data( +def test_checkout_delivery_method_update_invalid_with_not_valid_address_data( mock_clean_delivery, api_client, delivery_method, node_name, attribute_name, checkout_with_item_for_cc, - is_valid_delivery_method, ): # given - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False checkout = checkout_with_item_for_cc checkout.shipping_address = Address.objects.create( @@ -561,7 +661,7 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( delivery_method.channel_listings.get(), ) query = MUTATION_UPDATE_DELIVERY_METHOD - mock_clean_delivery.return_value = is_valid_delivery_method + mock_clean_delivery.return_value = False method_id = graphene.Node.to_global_id(node_name, delivery_method.id) @@ -578,14 +678,131 @@ def test_checkout_delivery_method_update_with_not_valid_address_data( checkout_info=checkout_info, lines=lines, method=shipping_method_data ) errors = data["errors"] - if is_valid_delivery_method: - assert not errors - assert getattr(checkout, attribute_name) == delivery_method - else: - assert len(errors) == 1 - assert errors[0]["field"] == "deliveryMethodId" - assert ( - errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name - ) - assert checkout.shipping_method is None - assert checkout.collection_point is None + + assert len(errors) == 1 + assert errors[0]["field"] == "deliveryMethodId" + assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name + assert checkout.shipping_method is None + assert checkout.collection_point is None + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("shipping_method", "ShippingMethod", "shipping_method"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = convert_to_shipping_method_data( + delivery_method, + delivery_method.channel_listings.get(), + ) + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method + + +@pytest.mark.parametrize( + "delivery_method, node_name, attribute_name", + [ + ("warehouse", "Warehouse", "collection_point"), + ], + indirect=("delivery_method",), +) +@patch( + "saleor.graphql.checkout.mutations.checkout_delivery_method_update." + "clean_delivery_method" +) +def test_checkout_delivery_method_update_valid_with_not_valid_address_data_for_cc( + mock_clean_delivery, + api_client, + delivery_method, + node_name, + attribute_name, + checkout_with_item_for_cc, +): + # given + mock_clean_delivery.return_value = True + + checkout = checkout_with_item_for_cc + checkout.shipping_address = Address.objects.create( + country="US", + city="New York", + city_area="ABC", + street_address_1="New street", + postal_code="53-601", + ) + checkout.save() + manager = get_plugins_manager(allow_replica=False) + lines, _ = fetch_checkout_lines(checkout) + checkout_info = fetch_checkout_info(checkout, lines, manager) + + shipping_method_data = delivery_method + checkout_info.shipping_address = shipping_method_data.address + query = MUTATION_UPDATE_DELIVERY_METHOD + mock_clean_delivery.return_value = True + + method_id = graphene.Node.to_global_id(node_name, delivery_method.id) + + # when + response = api_client.post_graphql( + query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id} + ) + + # then + data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"] + checkout.refresh_from_db() + + mock_clean_delivery.assert_called_once_with( + checkout_info=checkout_info, lines=lines, method=shipping_method_data + ) + errors = data["errors"] + + assert not errors + assert getattr(checkout, attribute_name) == delivery_method
saleor/graphql/checkout/tests/mutations/test_checkout_shipping_address_update.py+26 −0 modified@@ -924,3 +924,29 @@ def test_checkout_shipping_address_update_with_not_applicable_voucher( assert checkout_with_item.shipping_address.country == new_address["country"] assert checkout_with_item.voucher_code is None + + +def test_checkout_shipping_address_update_with_collection_point_already_set( + user_api_client, + checkout_with_item, + graphql_address_data, + warehouse_for_cc, +): + checkout = checkout_with_item + checkout.collection_point_id = warehouse_for_cc.id + checkout.save(update_fields=["collection_point_id"]) + + shipping_address = graphql_address_data + variables = { + "id": to_global_id_or_none(checkout), + "shippingAddress": shipping_address, + } + + response = user_api_client.post_graphql( + MUTATION_CHECKOUT_SHIPPING_ADDRESS_UPDATE, variables + ) + content = get_graphql_content(response) + data = content["data"]["checkoutShippingAddressUpdate"] + errors = data["errors"] + assert errors[0]["code"] == CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.name + assert errors[0]["field"] == "shippingAddress"
saleor/graphql/schema.graphql+3 −2 modified@@ -16418,7 +16418,7 @@ type Mutation { ): CheckoutShippingMethodUpdate @doc(category: "Checkout") @deprecated(reason: "This field will be removed in Saleor 4.0. Use `checkoutDeliveryMethodUpdate` instead.") """ - Updates the delivery method (shipping method or pick up point) of the checkout. + Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1. """ @@ -24535,6 +24535,7 @@ enum CheckoutErrorCode @doc(category: "Checkout") { EMAIL_NOT_SET NO_LINES INACTIVE_PAYMENT + SHIPPING_CHANGE_FORBIDDEN } """Update billing address in the existing checkout.""" @@ -24904,7 +24905,7 @@ type CheckoutShippingMethodUpdate @doc(category: "Checkout") { } """ -Updates the delivery method (shipping method or pick up point) of the checkout. +Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. Added in Saleor 3.1. """
saleor/tax/utils.py+1 −1 modified@@ -19,7 +19,7 @@ def get_tax_country( is_shipping_required: bool, shipping_address: Optional["Address"] = None, billing_address: Optional["Address"] = None, -): +) -> str: """Get country code for tax calculations. For checkouts and orders, there are following rules for determining the country
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
19- github.com/advisories/GHSA-mrj3-f2h4-7w45ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-29888ghsaADVISORY
- github.com/saleor/saleor/commit/22a1aa3ef0bc54156405f69146788016a7f3f761ghsax_refsource_MISCWEB
- github.com/saleor/saleor/commit/39abb0f4e4fe6503f81bfbb871227e4f70bcdd5cghsax_refsource_MISCWEB
- github.com/saleor/saleor/commit/47cedfd7d6524d79bdb04708edcdbb235874de6bghsax_refsource_MISCWEB
- github.com/saleor/saleor/commit/997f7ea4f576543ec88679a86bfe1b14f7f2ff26ghsax_refsource_MISCWEB
- github.com/saleor/saleor/commit/b7cecda8b603f7472790150bb4508c7b655946d4ghsax_refsource_MISCWEB
- github.com/saleor/saleor/commit/d8ba545c16ad3153febc5b5be8fd2ef75da9fc95ghsax_refsource_MISCWEB
- github.com/saleor/saleor/commit/dccc2c842b4e2e09470929c80f07dc137e439182ghsax_refsource_MISCWEB
- github.com/saleor/saleor/commit/ef003c76a304c89ddb2dc65b7f1d5b3b2ba1c640ghsax_refsource_MISCWEB
- github.com/saleor/saleor/pull/15694ghsax_refsource_MISCWEB
- github.com/saleor/saleor/pull/15697ghsax_refsource_MISCWEB
- github.com/saleor/saleor/releases/tag/3.14.61ghsaWEB
- github.com/saleor/saleor/releases/tag/3.15.37ghsaWEB
- github.com/saleor/saleor/releases/tag/3.16.34ghsaWEB
- github.com/saleor/saleor/releases/tag/3.17.32ghsaWEB
- github.com/saleor/saleor/releases/tag/3.18.28ghsaWEB
- github.com/saleor/saleor/releases/tag/3.19.15ghsaWEB
- github.com/saleor/saleor/security/advisories/GHSA-mrj3-f2h4-7w45ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.