CVE-2024-29888 (published 2024-03-27T19:15Z)

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`.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/saleor/saleor
commit_details: repo_url: https://github.com/saleor/saleor
author: Artur
author_date: 2024-03-01 10:02:18+01:00
msg: Fix  tax calculation for Click and Collect option. (#15505)
repository: repo_url: https://github.com/saleor/saleor
repo_name: saleor/saleor
description: Saleor Core: the high performance, composable, headless commerce API.
repo_language: Python
owner: saleor
file_changes: filename: error_codes.py
change_type: ModificationType.MODIFY
diff: @@ -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):

programming_language: Python
filename: checkout_delivery_method_update.py
change_type: ModificationType.MODIFY
diff: @@ -21,6 +21,7 @@ from ....checkout.utils import (
 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 CheckoutDeliveryMethodUpdate(BaseMutation):
     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 @@ class CheckoutDeliveryMethodUpdate(BaseMutation):
         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 @@ class CheckoutDeliveryMethodUpdate(BaseMutation):
             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 @@ class CheckoutDeliveryMethodUpdate(BaseMutation):
                 }
             )
         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(

programming_language: Python
filename: checkout_shipping_address_update.py
change_type: ModificationType.MODIFY
diff: @@ -33,6 +33,7 @@ from ...site.dataloaders import get_site_promise
 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 @@ class CheckoutShippingAddressUpdate(AddressMetadataMixin, BaseMutation, I18nMixi
                     )
                 }
             )
+        # 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,

programming_language: Python
filename: utils.py
change_type: ModificationType.MODIFY
diff: @@ -46,6 +46,10 @@ if TYPE_CHECKING:
 
 
 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

programming_language: Python
filename: test_checkout_delivery_method_update.py
change_type: ModificationType.MODIFY
diff: @@ -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(

programming_language: Python
filename: test_checkout_shipping_address_update.py
change_type: ModificationType.MODIFY
diff: @@ -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"

programming_language: Python
filename: schema.graphql
change_type: ModificationType.MODIFY
diff: @@ -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.
 

programming_language: Python
repo_url: https://github.com/saleor/saleor
commit_details: repo_url: https://github.com/saleor/saleor
author: Iga Karbowiak
author_date: 2024-03-27 13:56:53+01:00
msg: Fix relation between order and click and collect address (#15697)

* Create a copy of collection point address during checkout completion

* Add migration for creating order shipping addresses
repository: repo_url: https://github.com/saleor/saleor
repo_name: saleor/saleor
description: Saleor Core: the high performance, composable, headless commerce API.
repo_language: Python
owner: saleor
file_changes: filename: complete_checkout.py
change_type: ModificationType.MODIFY
diff: @@ -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)
 

programming_language: Python
filename: test_checkout_complete_with_payment.py
change_type: ModificationType.MODIFY
diff: @@ -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
 

programming_language: Python
filename: test_checkout_complete_with_transactions.py
change_type: ModificationType.MODIFY
diff: @@ -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
 

programming_language: Python
filename: 0172_update_order_cc_addresses.py
change_type: ModificationType.ADD
diff: @@ -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),
+    ]

programming_language: Python
filename: 0176_merge_20240325_1315.py
change_type: ModificationType.ADD
diff: @@ -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 = []

programming_language: Python
filename: 0177_merge_20240325_1329.py
change_type: ModificationType.ADD
diff: @@ -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 = []

programming_language: Python
filename: 0180_merge_20240325_1333.py
change_type: ModificationType.ADD
diff: @@ -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 = []

programming_language: Python
filename: 0182_merge_20240325_1338.py
change_type: ModificationType.ADD
diff: @@ -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 = []

programming_language: Python
filename: 0184_merge_0182_merge_20240325_1338_0183_order_tax_error.py
change_type: ModificationType.ADD
diff: @@ -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 = []

programming_language: Python
filename: saleor3_19.py
change_type: ModificationType.ADD
diff: @@ -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()

programming_language: Python