Skip to content

Commit

Permalink
Fix CheckoutLinesUpdate fails to remove lines unavailable for purchas…
Browse files Browse the repository at this point in the history
…e. (saleor#8545)

* Fix CheckoutLinesUpdate fails to remove lines unavailable for purchase.

* Move checkout validations from add_variants_to_checkout method to mutations
  • Loading branch information
IKarbowiak authored Nov 10, 2021
1 parent 344d4b9 commit 6991dfc
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 69 deletions.
23 changes: 0 additions & 23 deletions saleor/checkout/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ def add_variants_to_checkout(
variants,
quantities,
channel_slug,
skip_stock_check=False,
replace=False,
replace_reservations=False,
reservation_length: Optional[int] = None,
Expand All @@ -156,29 +155,7 @@ def add_variants_to_checkout(
If quantity is set to 0, checkout line will be deleted.
Otherwise, quantity will be added or replaced (if replace argument is True).
"""
# check quantities
country_code = checkout.get_country()
if not skip_stock_check:
check_stock_and_preorder_quantity_bulk(
variants,
country_code,
quantities,
channel_slug,
check_reservations=bool(reservation_length),
)

channel_listings = product_models.ProductChannelListing.objects.filter(
channel_id=checkout.channel.id,
product_id__in=[v.product_id for v in variants],
)
channel_listings_by_product_id = {cl.product_id: cl for cl in channel_listings}

# check if variants are published
for variant in variants:
product_channel_listing = channel_listings_by_product_id[variant.product_id]
if not product_channel_listing or not product_channel_listing.is_published:
raise ProductNotPublished()

checkout_lines = checkout.lines.select_related("variant")
variant_ids_in_lines = {line.variant_id: line for line in checkout_lines}
to_create = []
Expand Down
107 changes: 63 additions & 44 deletions saleor/graphql/checkout/mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
validate_variants_in_checkout_lines,
)
from ...core import analytics
from ...core.exceptions import InsufficientStock, PermissionDenied, ProductNotPublished
from ...core.exceptions import InsufficientStock, PermissionDenied
from ...core.permissions import AccountPermissions
from ...core.tracing import traced_atomic_transaction
from ...core.transactions import transaction_with_commit_on_errors
Expand Down Expand Up @@ -73,7 +73,6 @@
from ..utils import get_user_or_app_from_context, resolve_global_ids_to_primary_keys
from ..warehouse.types import Warehouse
from .types import Checkout, CheckoutLine
from .utils import prepare_insufficient_stock_checkout_validation_error

ERROR_DOES_NOT_SHIP = "This checkout doesn't need shipping"

Expand Down Expand Up @@ -223,12 +222,34 @@ def validate_variants_available_for_purchase(variants_id: set, channel_id: int):
graphene.Node.to_global_id("ProductVariant", pk)
for pk in not_available_variants
]
error_code = CheckoutErrorCode.PRODUCT_UNAVAILABLE_FOR_PURCHASE
error_code = CheckoutErrorCode.PRODUCT_UNAVAILABLE_FOR_PURCHASE.value
raise ValidationError(
{
"lines": ValidationError(
"Cannot add lines for unavailable for purchase variants.",
code=error_code, # type: ignore
code=error_code,
params={"variants": variant_ids},
)
}
)


def validate_variants_are_published(variants_id: set, channel_id: int):
published_variants = product_models.ProductChannelListing.objects.filter(
channel_id=channel_id, product__variants__id__in=variants_id, is_published=True
).values_list("product__variants__id", flat=True)
not_published_variants = variants_id.difference(set(published_variants))
if not_published_variants:
variant_ids = [
graphene.Node.to_global_id("ProductVariant", pk)
for pk in not_published_variants
]
error_code = CheckoutErrorCode.PRODUCT_NOT_PUBLISHED.value
raise ValidationError(
{
"lines": ValidationError(
"Cannot add lines for unpublished variants.",
code=error_code,
params={"variants": variant_ids},
)
}
Expand Down Expand Up @@ -326,6 +347,7 @@ def clean_checkout_lines(
validate_variants_available_in_channel(
variant_db_ids, channel.id, CheckoutErrorCode.UNAVAILABLE_VARIANT_IN_CHANNEL
)
validate_variants_are_published(variant_db_ids, channel.id)
check_lines_quantity(
variants,
quantities,
Expand Down Expand Up @@ -401,7 +423,6 @@ def clean_input(cls, info, instance: models.Checkout, data, input_cls=None):
@classmethod
@traced_atomic_transaction()
def save(cls, info, instance: models.Checkout, cleaned_input):
channel = cleaned_input["channel"]
# Create the checkout object
instance.save()

Expand All @@ -410,25 +431,17 @@ def save(cls, info, instance: models.Checkout, cleaned_input):
instance.set_country(country)

# Create checkout lines
channel = cleaned_input["channel"]
variants = cleaned_input.get("variants")
quantities = cleaned_input.get("quantities")
if variants and quantities:
try:
add_variants_to_checkout(
instance,
variants,
quantities,
channel.slug,
reservation_length=get_reservation_length(info.context),
)
except InsufficientStock as exc:
error = prepare_insufficient_stock_checkout_validation_error(exc)
raise ValidationError({"lines": error})
except ProductNotPublished as exc:
raise ValidationError(
"Can't create checkout with unpublished product.",
code=exc.code,
)
add_variants_to_checkout(
instance,
variants,
quantities,
channel.slug,
reservation_length=get_reservation_length(info.context),
)

# Save addresses
shipping_address = cleaned_input.get("shipping_address")
Expand Down Expand Up @@ -527,31 +540,37 @@ def clean_input(
channel_slug,
lines=lines,
)
variants_db_ids = {variant.id for variant in variants}
validate_variants_available_for_purchase(variants_db_ids, checkout.channel_id)
validate_variants_available_in_channel(
variants_db_ids,
checkout.channel_id,
CheckoutErrorCode.UNAVAILABLE_VARIANT_IN_CHANNEL,
)

variants_ids_to_validate = {
variant.id
for variant, quantity in zip(variants, quantities)
if quantity != 0
}

# validate variant only when line quantity is bigger than 0
if variants_ids_to_validate:
validate_variants_available_for_purchase(
variants_ids_to_validate, checkout.channel_id
)
validate_variants_available_in_channel(
variants_ids_to_validate,
checkout.channel_id,
CheckoutErrorCode.UNAVAILABLE_VARIANT_IN_CHANNEL,
)
validate_variants_are_published(
variants_ids_to_validate, checkout.channel_id
)

if variants and quantities:
try:
checkout = add_variants_to_checkout(
checkout,
variants,
quantities,
channel_slug,
skip_stock_check=True, # already checked by validate_checkout_lines
replace=replace,
replace_reservations=True,
reservation_length=get_reservation_length(info.context),
)
except ProductNotPublished as exc:
raise ValidationError(
"Can't add unpublished product.",
code=exc.code,
)
checkout = add_variants_to_checkout(
checkout,
variants,
quantities,
channel_slug,
replace=replace,
replace_reservations=True,
reservation_length=get_reservation_length(info.context),
)

lines = fetch_checkout_lines(checkout)
checkout_info.valid_shipping_methods = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ def test_create_checkout_with_reservations(
}
}

with django_assert_num_queries(54):
with django_assert_num_queries(52):
response = api_client.post_graphql(query, variables)
assert get_graphql_content(response)["data"]["checkoutCreate"]
assert Checkout.objects.first().lines.count() == 1
Expand All @@ -465,7 +465,7 @@ def test_create_checkout_with_reservations(
}
}

with django_assert_num_queries(54):
with django_assert_num_queries(52):
response = api_client.post_graphql(query, variables)
assert get_graphql_content(response)["data"]["checkoutCreate"]
assert Checkout.objects.first().lines.count() == 10
Expand Down
34 changes: 34 additions & 0 deletions saleor/graphql/checkout/tests/test_checkout_lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,40 @@ def test_checkout_line_delete_by_zero_quantity(
mocked_update_shipping_method.assert_called_once_with(checkout_info, lines)


@mock.patch(
"saleor.graphql.checkout.mutations.update_checkout_shipping_method_if_invalid",
wraps=update_checkout_shipping_method_if_invalid,
)
def test_checkout_line_delete_by_zero_quantity_when_variant_unavailable_for_purchase(
mocked_update_shipping_method, user_api_client, checkout_with_item
):
checkout = checkout_with_item
assert checkout.lines.count() == 1
line = checkout.lines.first()
variant = line.variant
assert line.quantity == 3
variant.channel_listings.all().delete()
variant.product.channel_listings.all().delete()

variant_id = graphene.Node.to_global_id("ProductVariant", variant.pk)

variables = {
"token": checkout.token,
"lines": [{"variantId": variant_id, "quantity": 0}],
}
response = user_api_client.post_graphql(MUTATION_CHECKOUT_LINES_UPDATE, variables)
content = get_graphql_content(response)

data = content["data"]["checkoutLinesUpdate"]
assert not data["errors"]
checkout.refresh_from_db()
assert checkout.lines.count() == 0
manager = get_plugins_manager()
lines = fetch_checkout_lines(checkout)
checkout_info = fetch_checkout_info(checkout, lines, [], manager)
mocked_update_shipping_method.assert_called_once_with(checkout_info, lines)


@mock.patch(
"saleor.graphql.checkout.mutations.update_checkout_shipping_method_if_invalid",
wraps=update_checkout_shipping_method_if_invalid,
Expand Down

0 comments on commit 6991dfc

Please sign in to comment.