VYPR
Moderate severityNVD Advisory· Published Mar 27, 2024· Updated Aug 2, 2024

Saleor vulnerable to customers addresses leak when using Warehouse as a `Pickup: Local stock only` delivery method

CVE-2024-29888

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.

PackageAffected versionsPatched versions
saleorPyPI
>= 3.14.56, < 3.14.613.14.61
saleorPyPI
>= 3.15.31, < 3.15.373.15.37
saleorPyPI
>= 3.16.27, < 3.16.343.16.34
saleorPyPI
>= 3.17.25, < 3.17.323.17.32
saleorPyPI
>= 3.18.19, < 3.18.283.18.28
saleorPyPI
>= 3.19.5, < 3.19.153.19.15

Affected products

1

Patches

8
47cedfd7d652

Fix relation between order and click and collect address (#15697)

https://github.com/saleor/saleorIga KarbowiakMar 27, 2024via ghsa
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()
    
22a1aa3ef0bc

Fix tax calculation for Click and Collect option. (#15505)

https://github.com/saleor/saleorArturMar 1, 2024via ghsa
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.
     
    
d8ba545c16ad

Fix tax calculation for Click and Collect option. (#15491)

https://github.com/saleor/saleorArturFeb 29, 2024via ghsa
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.
     
    
dccc2c842b4e

Fix tax calculation for Click and Collect option. (#15490)

https://github.com/saleor/saleorArturFeb 29, 2024via ghsa
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.
     
    
b7cecda8b603

Fix tax calculation for Click and Collect option. (#15489)

https://github.com/saleor/saleorArturFeb 29, 2024via ghsa
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.
     
    
39abb0f4e4fe

Fix tax calculation for Click and Collect option. (#15488)

https://github.com/saleor/saleorArturFeb 29, 2024via ghsa
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.
     
    
ef003c76a304

Fix tax calculation for Click and Collect option. (#15487)

https://github.com/saleor/saleorArturFeb 29, 2024via ghsa
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.
     
    
997f7ea4f576

Fix `FLAT_RATE` tax calculation for Click and Collect option. (#15426)

https://github.com/saleor/saleorArturFeb 26, 2024via ghsa
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

News mentions

0

No linked articles in our index yet.