Repository: saleor/saleor

Issue:
It should not be possible to set negative prices
### Steps to reproduce the problem
1. Go to dashboard 
2. Open any product
3. Set its price to -1

### What I expected to happen
To throw an error


Discussion:

Resolving patch:
diff --git a/saleor/graphql/product/bulk_mutations/products.py b/saleor/graphql/product/bulk_mutations/products.py
--- a/saleor/graphql/product/bulk_mutations/products.py
+++ b/saleor/graphql/product/bulk_mutations/products.py
@@ -155,10 +155,28 @@ def clean_input(cls, info, instance: models.ProductVariant, data: dict):
 
         cost_price_amount = cleaned_input.pop("cost_price", None)
         if cost_price_amount is not None:
+            if cost_price_amount < 0:
+                raise ValidationError(
+                    {
+                        "costPrice": ValidationError(
+                            "Product price cannot be lower than 0.",
+                            code=ProductErrorCode.INVALID.value,
+                        )
+                    }
+                )
             cleaned_input["cost_price_amount"] = cost_price_amount
 
         price_override_amount = cleaned_input.pop("price_override", None)
         if price_override_amount is not None:
+            if price_override_amount < 0:
+                raise ValidationError(
+                    {
+                        "priceOverride": ValidationError(
+                            "Product price cannot be lower than 0.",
+                            code=ProductErrorCode.INVALID.value,
+                        )
+                    }
+                )
             cleaned_input["price_override_amount"] = price_override_amount
 
         attributes = cleaned_input.get("attributes")
diff --git a/saleor/graphql/product/mutations/products.py b/saleor/graphql/product/mutations/products.py
--- a/saleor/graphql/product/mutations/products.py
+++ b/saleor/graphql/product/mutations/products.py
@@ -833,6 +833,15 @@ def clean_input(cls, info, instance, data):
         # from the schema, only "basePrice" should be used here.
         price = data.get("base_price", data.get("price"))
         if price is not None:
+            if price < 0:
+                raise ValidationError(
+                    {
+                        "basePrice": ValidationError(
+                            "Product base price cannot be lower than 0.",
+                            code=ProductErrorCode.INVALID,
+                        )
+                    }
+                )
             cleaned_input["price_amount"] = price
             if instance.minimal_variant_price_amount is None:
                 # Set the default "minimal_variant_price" to the "price"
@@ -1175,10 +1184,30 @@ def clean_input(
         cleaned_input = super().clean_input(info, instance, data)
 
         if "cost_price" in cleaned_input:
-            cleaned_input["cost_price_amount"] = cleaned_input.pop("cost_price")
+            cost_price = cleaned_input.pop("cost_price")
+            if cost_price and cost_price < 0:
+                raise ValidationError(
+                    {
+                        "costPrice": ValidationError(
+                            "Product price cannot be lower than 0.",
+                            code=ProductErrorCode.INVALID.value,
+                        )
+                    }
+                )
+            cleaned_input["cost_price_amount"] = cost_price
 
         if "price_override" in cleaned_input:
-            cleaned_input["price_override_amount"] = cleaned_input.pop("price_override")
+            price_override = cleaned_input.pop("price_override")
+            if price_override and price_override < 0:
+                raise ValidationError(
+                    {
+                        "priceOverride": ValidationError(
+                            "Product price cannot be lower than 0.",
+                            code=ProductErrorCode.INVALID.value,
+                        )
+                    }
+                )
+            cleaned_input["price_override_amount"] = price_override
 
         stocks = cleaned_input.get("stocks")
         if stocks:


Test patch:
diff --git a/tests/api/test_product.py b/tests/api/test_product.py
--- a/tests/api/test_product.py
+++ b/tests/api/test_product.py
@@ -1586,6 +1586,46 @@ def test_update_product_slug_and_name(
         assert errors[0]["code"] == ProductErrorCode.REQUIRED.name
 
 
+UPDATE_PRODUCT_PRICE_MUTATION = """
+    mutation($id: ID!, $basePrice: Decimal) {
+        productUpdate(
+            id: $id
+            input: {
+                basePrice: $basePrice
+            }
+        ) {
+            product{
+                name
+                slug
+            }
+            productErrors {
+                field
+                message
+                code
+            }
+        }
+    }
+"""
+
+
+def test_update_product_invalid_price(
+    staff_api_client, product, permission_manage_products,
+):
+
+    node_id = graphene.Node.to_global_id("Product", product.id)
+    variables = {"basePrice": Decimal("-19"), "id": node_id}
+    response = staff_api_client.post_graphql(
+        UPDATE_PRODUCT_PRICE_MUTATION,
+        variables,
+        permissions=[permission_manage_products],
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["productUpdate"]
+    errors = data["productErrors"]
+    assert errors[0]["field"] == "basePrice"
+    assert errors[0]["code"] == ProductErrorCode.INVALID.name
+
+
 SET_ATTRIBUTES_TO_PRODUCT_QUERY = """
     mutation updateProduct($productId: ID!, $attributes: [AttributeValueInput!]) {
       productUpdate(id: $productId, input: { attributes: $attributes }) {
diff --git a/tests/api/test_product_minimal_variant_price.py b/tests/api/test_product_minimal_variant_price.py
--- a/tests/api/test_product_minimal_variant_price.py
+++ b/tests/api/test_product_minimal_variant_price.py
@@ -6,6 +6,7 @@
 from prices import Money
 
 from saleor.graphql.discount.enums import DiscountValueTypeEnum
+from saleor.product.error_codes import ProductErrorCode
 from tests.api.utils import get_graphql_content
 
 
@@ -224,6 +225,100 @@ def test_product_variant_update_updates_minimal_variant_price(
     )
 
 
+@patch(
+    "saleor.graphql.product.mutations.products"
+    ".update_product_minimal_variant_price_task"
+)
+def test_product_variant_update_updates_invalid_variant_price(
+    mock_update_product_minimal_variant_price_task,
+    staff_api_client,
+    product,
+    permission_manage_products,
+):
+    query = """
+        mutation ProductVariantUpdate(
+            $id: ID!,
+            $priceOverride: Decimal,
+        ) {
+            productVariantUpdate(
+                id: $id,
+                input: {
+                    priceOverride: $priceOverride,
+                }
+            ) {
+                productVariant {
+                    name
+                }
+                productErrors {
+                    field
+                    message
+                    code
+                }
+            }
+        }
+    """
+    variant = product.variants.first()
+    variant_id = to_global_id("ProductVariant", variant.pk)
+    price_override = "-1.99"
+    variables = {"id": variant_id, "priceOverride": price_override}
+    response = staff_api_client.post_graphql(
+        query, variables, permissions=[permission_manage_products]
+    )
+    assert response.status_code == 200
+
+    content = get_graphql_content(response)
+    data = content["data"]["productVariantUpdate"]
+    assert data["productErrors"][0]["field"] == "priceOverride"
+    assert data["productErrors"][0]["code"] == ProductErrorCode.INVALID.name
+
+
+@patch(
+    "saleor.graphql.product.mutations.products"
+    ".update_product_minimal_variant_price_task"
+)
+def test_product_variant_update_updates_invalid_cost_price(
+    mock_update_product_minimal_variant_price_task,
+    staff_api_client,
+    product,
+    permission_manage_products,
+):
+    query = """
+        mutation ProductVariantUpdate(
+            $id: ID!,
+            $costPrice: Decimal,
+        ) {
+            productVariantUpdate(
+                id: $id,
+                input: {
+                    costPrice: $costPrice,
+                }
+            ) {
+                productVariant {
+                    name
+                }
+                productErrors {
+                    field
+                    message
+                    code
+                }
+            }
+        }
+    """
+    variant = product.variants.first()
+    variant_id = to_global_id("ProductVariant", variant.pk)
+    cost_price = "-1.99"
+    variables = {"id": variant_id, "costPrice": cost_price}
+    response = staff_api_client.post_graphql(
+        query, variables, permissions=[permission_manage_products]
+    )
+    assert response.status_code == 200
+
+    content = get_graphql_content(response)
+    data = content["data"]["productVariantUpdate"]
+    assert data["productErrors"][0]["field"] == "costPrice"
+    assert data["productErrors"][0]["code"] == ProductErrorCode.INVALID.name
+
+
 @patch(
     "saleor.graphql.product.mutations.products."
     "update_product_minimal_variant_price_task"
diff --git a/tests/api/test_variant.py b/tests/api/test_variant.py
--- a/tests/api/test_variant.py
+++ b/tests/api/test_variant.py
@@ -887,6 +887,36 @@ def test_product_variant_bulk_create_by_attribute_id(
     assert not product_variant.price_override
 
 
+@pytest.mark.parametrize(
+    "price_field", ("priceOverride", "costPrice",),
+)
+def test_product_variant_bulk_create_by_attribute_id_with_invalid_price(
+    staff_api_client, product, size_attribute, permission_manage_products, price_field
+):
+    product_id = graphene.Node.to_global_id("Product", product.pk)
+    attribut_id = graphene.Node.to_global_id("Attribute", size_attribute.pk)
+    attribute_value = size_attribute.values.last()
+    sku = str(uuid4())[:12]
+    variant = {
+        "sku": sku,
+        "quantity": 1000,
+        "weight": 2.5,
+        "trackInventory": True,
+        "attributes": [{"id": attribut_id, "values": [attribute_value.name]}],
+    }
+    variant[price_field] = "-1"
+
+    variables = {"productId": product_id, "variants": [variant]}
+    staff_api_client.user.user_permissions.add(permission_manage_products)
+    response = staff_api_client.post_graphql(
+        PRODUCT_VARIANT_BULK_CREATE_MUTATION, variables
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["productVariantBulkCreate"]
+    assert data["bulkProductErrors"][0]["field"] == price_field
+    assert data["bulkProductErrors"][0]["code"] == ProductErrorCode.INVALID.name
+
+
 def test_product_variant_bulk_create_empty_attribute(
     staff_api_client, product, size_attribute, permission_manage_products
 ):