Skip to content

Commit

Permalink
Merge pull request #5682 from mirumee/bugfix/create_checkout_with_unp…
Browse files Browse the repository at this point in the history
…ublished_product

Prevent creating checkout with unpublished product
  • Loading branch information
maarcingebala committed May 22, 2020
2 parents 6e9d657 + 0247375 commit db3f0c7
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@ All notable, unreleased changes to this project will be documented in this file.


- Add our implementation of UUID scalar - #5646 by @koradon
- Prevent creating checkout/draft order with unpublished product - #5676 by @d-wysocki

## 2.10.0

Expand Down
1 change: 1 addition & 0 deletions saleor/checkout/error_codes.py
Expand Up @@ -5,6 +5,7 @@ class CheckoutErrorCode(Enum):
BILLING_ADDRESS_NOT_SET = "billing_address_not_set"
CHECKOUT_NOT_FULLY_PAID = "checkout_not_fully_paid"
GRAPHQL_ERROR = "graphql_error"
PRODUCT_NOT_PUBLISHED = "product_not_published"
INSUFFICIENT_STOCK = "insufficient_stock"
INVALID = "invalid"
INVALID_SHIPPING_METHOD = "invalid_shipping_method"
Expand Down
3 changes: 3 additions & 0 deletions saleor/checkout/utils.py
Expand Up @@ -15,6 +15,7 @@
from ..account.utils import store_user_address
from ..checkout import calculations
from ..checkout.error_codes import CheckoutErrorCode
from ..core.exceptions import ProductNotPublished
from ..core.taxes import quantize_price, zero_taxed_money
from ..core.utils.promo_code import (
InvalidPromoCode,
Expand Down Expand Up @@ -129,6 +130,8 @@ def add_variant_to_checkout(
If `replace` is truthy then any previous quantity is discarded instead
of added to.
"""
if not variant.product.is_published:
raise ProductNotPublished()

new_quantity, line = check_variant_in_stock(
checkout,
Expand Down
7 changes: 7 additions & 0 deletions saleor/core/exceptions.py
Expand Up @@ -24,3 +24,10 @@ def __init__(self, msg=None):
if msg is None:
msg = "API runs in read-only mode"
super().__init__(msg)


class ProductNotPublished(Exception):
def __init__(self, context=None):
super().__init__("Can't add unpublished product.")
self.context = context
self.code = CheckoutErrorCode.PRODUCT_NOT_PUBLISHED
12 changes: 10 additions & 2 deletions saleor/graphql/checkout/mutations.py
Expand Up @@ -24,7 +24,7 @@
remove_promo_code_from_checkout,
)
from ...core import analytics
from ...core.exceptions import InsufficientStock
from ...core.exceptions import InsufficientStock, ProductNotPublished
from ...core.permissions import OrderPermissions
from ...core.taxes import TaxError
from ...core.utils.url import validate_storefront_url
Expand Down Expand Up @@ -283,7 +283,11 @@ def save(cls, info, instance: models.Checkout, cleaned_input):
raise ValidationError(
f"Insufficient product stock: {exc.item}", code=exc.code
)

except ProductNotPublished as exc:
raise ValidationError(
"Can't create checkout with unpublished product.",
code=exc.code,
)
# Save provided addresses and associate them to the checkout
cls.save_addresses(instance, cleaned_input)

Expand Down Expand Up @@ -354,6 +358,10 @@ def perform_mutation(cls, _root, info, checkout_id, lines, replace=False):
raise ValidationError(
f"Insufficient product stock: {exc.item}", code=exc.code
)
except ProductNotPublished as exc:
raise ValidationError(
"Can't add unpublished product.", code=exc.code,
)

lines = list(checkout)

Expand Down
1 change: 0 additions & 1 deletion saleor/graphql/core/fields.py
Expand Up @@ -192,7 +192,6 @@ def connection_resolver(
on_resolve = partial(cls.resolve_connection, connection, args)

filter_input = args.get(filters_name)

if filter_input and filterset_class:
instance = filterset_class(
data=dict(filter_input), queryset=iterable, request=info.context
Expand Down
15 changes: 15 additions & 0 deletions saleor/graphql/order/utils.py
Expand Up @@ -61,17 +61,32 @@ def validate_order_lines(order, country):
)


def validate_product_is_published(order):
for line in order:
if not line.variant.product.is_published:
raise ValidationError(
{
"lines": ValidationError(
"Can't finalize draft with unpublished product.",
code=OrderErrorCode.PRODUCT_NOT_PUBLISHED,
)
}
)


def validate_draft_order(order, country):
"""Check if the given order contains the proper data.
- Has proper customer data,
- Shipping address and method are set up,
- Product variants for order lines still exists in database.
- Product variants are availale in requested quantity.
- Product variants are published.
Returns a list of errors if any were found.
"""
if order.is_shipping_required():
validate_shipping_method(order)
validate_total_quantity(order)
validate_order_lines(order, country)
validate_product_is_published(order)
2 changes: 2 additions & 0 deletions saleor/graphql/schema.graphql
Expand Up @@ -867,6 +867,7 @@ enum CheckoutErrorCode {
BILLING_ADDRESS_NOT_SET
CHECKOUT_NOT_FULLY_PAID
GRAPHQL_ERROR
PRODUCT_NOT_PUBLISHED
INSUFFICIENT_STOCK
INVALID
INVALID_SHIPPING_METHOD
Expand Down Expand Up @@ -2672,6 +2673,7 @@ enum OrderErrorCode {
FULFILL_ORDER_LINE
GRAPHQL_ERROR
INVALID
PRODUCT_NOT_PUBLISHED
NOT_FOUND
ORDER_NO_SHIPPING_ADDRESS
PAYMENT_ERROR
Expand Down
1 change: 1 addition & 0 deletions saleor/order/error_codes.py
Expand Up @@ -12,6 +12,7 @@ class OrderErrorCode(Enum):
FULFILL_ORDER_LINE = "fulfill_order_line"
GRAPHQL_ERROR = "graphql_error"
INVALID = "invalid"
PRODUCT_NOT_PUBLISHED = "product_not_published"
NOT_FOUND = "not_found"
ORDER_NO_SHIPPING_ADDRESS = "order_no_shipping_address"
PAYMENT_ERROR = "payment_error"
Expand Down
123 changes: 105 additions & 18 deletions tests/api/test_checkout.py
Expand Up @@ -714,8 +714,9 @@ def test_checkout_no_available_shipping_methods_without_lines(api_client, checko
}
}
}
errors {
checkoutErrors {
field
code
message
}
}
Expand Down Expand Up @@ -743,7 +744,7 @@ def test_checkout_lines_add(
response = user_api_client.post_graphql(MUTATION_CHECKOUT_LINES_ADD, variables)
content = get_graphql_content(response)
data = content["data"]["checkoutLinesAdd"]
assert not data["errors"]
assert not data["checkoutErrors"]
checkout.refresh_from_db()
line = checkout.lines.latest("pk")
assert line.variant == variant
Expand All @@ -754,6 +755,29 @@ def test_checkout_lines_add(
)


def test_checkout_lines_add_with_unpublished_product(
user_api_client, checkout_with_item, stock
):
variant = stock.product_variant
stock.product_variant.product.is_published = False
stock.product_variant.product.save()

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

variables = {
"checkoutId": checkout_id,
"lines": [{"variantId": variant_id, "quantity": 1}],
}

response = user_api_client.post_graphql(MUTATION_CHECKOUT_LINES_ADD, variables)

content = get_graphql_content(response)
error = content["data"]["checkoutLinesAdd"]["checkoutErrors"][0]
assert error["message"] == "Can't add unpublished product."
assert error["code"] == "PRODUCT_NOT_PUBLISHED"


def test_checkout_lines_add_too_many(user_api_client, checkout_with_item, stock):
variant = stock.product_variant
variant_id = graphene.Node.to_global_id("ProductVariant", variant.pk)
Expand All @@ -766,9 +790,13 @@ def test_checkout_lines_add_too_many(user_api_client, checkout_with_item, stock)
response = user_api_client.post_graphql(MUTATION_CHECKOUT_LINES_ADD, variables)
content = get_graphql_content(response)["data"]["checkoutLinesAdd"]

assert content["errors"]
assert content["errors"] == [
{"field": "quantity", "message": "Cannot add more than 50 times this item."}
assert content["checkoutErrors"]
assert content["checkoutErrors"] == [
{
"field": "quantity",
"message": "Cannot add more than 50 times this item.",
"code": "QUANTITY_GREATER_THAN_LIMIT",
}
]


Expand All @@ -784,7 +812,7 @@ def test_checkout_lines_add_empty_checkout(user_api_client, checkout, stock):
response = user_api_client.post_graphql(MUTATION_CHECKOUT_LINES_ADD, variables)
content = get_graphql_content(response)
data = content["data"]["checkoutLinesAdd"]
assert not data["errors"]
assert not data["checkoutErrors"]
checkout.refresh_from_db()
line = checkout.lines.first()
assert line.variant == variant
Expand All @@ -805,7 +833,7 @@ def test_checkout_lines_add_variant_without_inventory_tracking(
response = user_api_client.post_graphql(MUTATION_CHECKOUT_LINES_ADD, variables)
content = get_graphql_content(response)
data = content["data"]["checkoutLinesAdd"]
assert not data["errors"]
assert not data["checkoutErrors"]
checkout.refresh_from_db()
line = checkout.lines.first()
assert line.variant == variant
Expand All @@ -824,10 +852,10 @@ def test_checkout_lines_add_check_lines_quantity(user_api_client, checkout, stoc
response = user_api_client.post_graphql(MUTATION_CHECKOUT_LINES_ADD, variables)
content = get_graphql_content(response)
data = content["data"]["checkoutLinesAdd"]
assert data["errors"][0]["message"] == (
assert data["checkoutErrors"][0]["message"] == (
"Could not add item Test product (SKU_A). Only 15 remaining in stock."
)
assert data["errors"][0]["field"] == "quantity"
assert data["checkoutErrors"][0]["field"] == "quantity"


def test_checkout_lines_invalid_variant_id(user_api_client, checkout, stock):
Expand All @@ -847,8 +875,8 @@ def test_checkout_lines_invalid_variant_id(user_api_client, checkout, stock):
content = get_graphql_content(response)
data = content["data"]["checkoutLinesAdd"]
error_msg = "Could not resolve to a node with the global id list of '%s'."
assert data["errors"][0]["message"] == error_msg % [invalid_variant_id]
assert data["errors"][0]["field"] == "variantId"
assert data["checkoutErrors"][0]["message"] == error_msg % [invalid_variant_id]
assert data["checkoutErrors"][0]["field"] == "variantId"


MUTATION_CHECKOUT_LINES_UPDATE = """
Expand All @@ -864,8 +892,9 @@ def test_checkout_lines_invalid_variant_id(user_api_client, checkout, stock):
}
}
}
errors {
checkoutErrors {
field
code
message
}
}
Expand Down Expand Up @@ -897,7 +926,7 @@ def test_checkout_lines_update(
content = get_graphql_content(response)

data = content["data"]["checkoutLinesUpdate"]
assert not data["errors"]
assert not data["checkoutErrors"]
checkout.refresh_from_db()
assert checkout.lines.count() == 1
line = checkout.lines.first()
Expand All @@ -909,12 +938,70 @@ def test_checkout_lines_update(
)


def test_create_checkout_with_unpublished_product(
user_api_client, checkout_with_item, stock
):
variant = stock.product_variant
variant.product.is_published = False
variant.product.save()
variant_id = graphene.Node.to_global_id("ProductVariant", variant.pk)

query = """
mutation CreateCheckout($checkoutInput: CheckoutCreateInput!) {
checkoutCreate(input: $checkoutInput) {
checkoutErrors {
code
message
}
checkout {
id
}
}
}
"""
variables = {
"checkoutInput": {
"email": "test@example.com",
"lines": [{"variantId": variant_id, "quantity": 1}],
}
}
response = get_graphql_content(user_api_client.post_graphql(query, variables))
error = response["data"]["checkoutCreate"]["checkoutErrors"][0]

assert error["message"] == "Can't create checkout with unpublished product."
assert error["code"] == "PRODUCT_NOT_PUBLISHED"


def test_checkout_lines_update_with_unpublished_product(
user_api_client, checkout_with_item
):
checkout = checkout_with_item
line = checkout.lines.first()
variant = line.variant
variant.product.is_published = False
variant.product.save()

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

variables = {
"checkoutId": checkout_id,
"lines": [{"variantId": variant_id, "quantity": 1}],
}
response = user_api_client.post_graphql(MUTATION_CHECKOUT_LINES_UPDATE, variables)

content = get_graphql_content(response)
error = content["data"]["checkoutLinesUpdate"]["checkoutErrors"][0]
assert error["message"] == "Can't add unpublished product."
assert error["code"] == "PRODUCT_NOT_PUBLISHED"


def test_checkout_lines_update_invalid_checkout_id(user_api_client):
variables = {"checkoutId": "test", "lines": []}
response = user_api_client.post_graphql(MUTATION_CHECKOUT_LINES_UPDATE, variables)
content = get_graphql_content(response)
data = content["data"]["checkoutLinesUpdate"]
assert data["errors"][0]["field"] == "checkoutId"
assert data["checkoutErrors"][0]["field"] == "checkoutId"


def test_checkout_lines_update_check_lines_quantity(
Expand All @@ -935,10 +1022,10 @@ def test_checkout_lines_update_check_lines_quantity(
content = get_graphql_content(response)

data = content["data"]["checkoutLinesUpdate"]
assert data["errors"][0]["message"] == (
assert data["checkoutErrors"][0]["message"] == (
"Could not add item Test product (123). Only 10 remaining in stock."
)
assert data["errors"][0]["field"] == "quantity"
assert data["checkoutErrors"][0]["field"] == "quantity"


def test_checkout_lines_update_with_chosen_shipping(
Expand All @@ -960,7 +1047,7 @@ def test_checkout_lines_update_with_chosen_shipping(
content = get_graphql_content(response)

data = content["data"]["checkoutLinesUpdate"]
assert not data["errors"]
assert not data["checkoutErrors"]
checkout.refresh_from_db()
assert checkout.quantity == 1

Expand Down Expand Up @@ -1038,7 +1125,7 @@ def test_checkout_line_delete_by_zero_quantity(
content = get_graphql_content(response)

data = content["data"]["checkoutLinesUpdate"]
assert not data["errors"]
assert not data["checkoutErrors"]
checkout.refresh_from_db()
assert checkout.lines.count() == 0
mocked_update_shipping_method.assert_called_once_with(
Expand Down
2 changes: 1 addition & 1 deletion tests/api/test_checkout_digital.py
Expand Up @@ -238,7 +238,7 @@ def test_checkout_lines_update_remove_shipping_if_removed_product_with_shipping(
content = get_graphql_content(response)

data = content["data"]["checkoutLinesUpdate"]
assert not data["errors"]
assert not data["checkoutErrors"]
checkout.refresh_from_db()
assert checkout.lines.count() == 1
assert not checkout.shipping_method
Expand Down

0 comments on commit db3f0c7

Please sign in to comment.