Repository: saleor/saleor
Pull request: Update default decimal places
Changing currency caused errors when running Saleor.
- Set `DEFAULT_DECIMAL_PLACES` to 3, which should be enough for all currencies (https://en.wikipedia.org/wiki/ISO_4217).
- Create mutations for changing `decimal_places` in `DecimalFields`
- Update `Money` ObjectType - return amount with the number of decimal places corresponding to a given currency
- Create `PositiveDecimal` that contains validation - prevent for creating an item with a negative value
- Add price amount validation - check if the price amount  does not have more decimal places than the given currency allows

⚠️ 
Require frontend changes

Resolves #5360 

<!-- Please mention all relevant issue numbers. -->

# Impact

* [x] New migrations
* [x] New/Updated API fields or mutations
* [ ] Deprecated API fields or mutations
* [ ] Removed API types, fields, or mutations
* [ ] Documentation needs to be updated

# Pull Request Checklist

<!-- Please keep this section. It will make the maintainer's life easier. -->

* [ ] Privileged queries and mutations are guarded by proper permission checks
* [ ] Database queries are optimized and the number of queries is constant
* [x] Database migration files are up to date
* [x] The changes are tested
* [x] GraphQL schema and type definitions are up to date
* [x] Changes are mentioned in the changelog


Problem statement:
Products cannot be updated in currencies that do not use decimal
### What I'm trying to achieve
If I set DEFAULT_CURRENCY as ISK (or another currencies that include "I" for ex: IDQ, IIR) I cannot update products. 

(When I set DEFAULT_CURRENCY as lowercase it works fine.)

Error:
`{"data": {"productUpdate": {"errors": [{"field": "minimalVariantPriceAmount", "message": "Ensure that there are no more than 0 decimal places.", "__typename": "Error"}], "product": null, "__typename": "ProductUpdate"}}}`

### Steps to reproduce the problem
1. Set DEFAULT_CURRENCY as "ISK" and DEFAULT_COUNTRY as "IS"
2. Run the project.
3. Try to update any products.

**System information**
Operating system: Both MacOS and Ubuntu 18.04

Hints from issue discussion:
I guess this could be moved to `saleor-dashboard`, the problem is that these currencies (and Japanese Yen for example) do not have any decimal places and the dashboard submits an amount with a precision of 1/100.

We could also attempt to strip non-significant zeroes from amounts before validating their precision in the core. @maarcingebala?
Hi @patrys thank you for the redirection. 

I made DEFAULT_DECIMAL_PLACES = 2 as a temporary solution. Everything is looking well. 

Hope it will be solved permanently soon. 

Thanks in advance. 
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Related to: https://github.com/mirumee/saleor/issues/5313

Commit message: Apply code review suggestions - update validate_price_amount validator

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5145e11ccb0..cc85c14dba1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,6 +48,7 @@ All notable, unreleased changes to this project will be documented in this file.
 - Do not allow negative product price - #6091 by @IKarbowiak
 - Handle None as attribute value - #6092 by @IKarbowiak
 - Fix for calling order_created before the order was saved - #6095 by @korycins
+- Update default decimal places - #6098 by @IKarbowiak
 
 ## 2.10.2
 
diff --git a/saleor/checkout/migrations/0029_auto_20200904_0529.py b/saleor/checkout/migrations/0029_auto_20200904_0529.py
new file mode 100644
index 00000000000..10291c6f384
--- /dev/null
+++ b/saleor/checkout/migrations/0029_auto_20200904_0529.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1 on 2020-09-04 05:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("checkout", "0028_auto_20200824_1019"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="checkout",
+            name="discount_amount",
+            field=models.DecimalField(decimal_places=3, default=0, max_digits=12),
+        ),
+    ]
diff --git a/saleor/data_feeds/tests/test_data_feeds.py b/saleor/data_feeds/tests/test_data_feeds.py
index a174578b09f..6e43d193863 100644
--- a/saleor/data_feeds/tests/test_data_feeds.py
+++ b/saleor/data_feeds/tests/test_data_feeds.py
@@ -43,8 +43,8 @@ def test_saleor_feed_items(product, discount_info, site_settings):
     assert attributes.get("mpn") == valid_variant.sku
     assert attributes.get("availability") == "in stock"
     assert attributes.get("tax") is None
-    assert attributes.get("price") == "10.00 USD"
-    assert attributes.get("sale_price") == "5.00 USD"
+    assert attributes.get("price") == "10.000 USD"
+    assert attributes.get("sale_price") == "5.000 USD"
 
 
 def test_saleor_get_feed_items_having_no_stock_info(variant, site_settings):
diff --git a/saleor/discount/migrations/0021_auto_20200902_1249.py b/saleor/discount/migrations/0021_auto_20200902_1249.py
new file mode 100644
index 00000000000..87292664c36
--- /dev/null
+++ b/saleor/discount/migrations/0021_auto_20200902_1249.py
@@ -0,0 +1,30 @@
+# Generated by Django 3.1 on 2020-09-02 12:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("discount", "0020_auto_20200709_1102"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="sale",
+            name="value",
+            field=models.DecimalField(decimal_places=3, default=0, max_digits=12),
+        ),
+        migrations.AlterField(
+            model_name="voucher",
+            name="discount_value",
+            field=models.DecimalField(decimal_places=3, max_digits=12),
+        ),
+        migrations.AlterField(
+            model_name="voucher",
+            name="min_spent_amount",
+            field=models.DecimalField(
+                blank=True, decimal_places=3, max_digits=12, null=True
+            ),
+        ),
+    ]
diff --git a/saleor/giftcard/migrations/0004_auto_20200902_1249.py b/saleor/giftcard/migrations/0004_auto_20200902_1249.py
new file mode 100644
index 00000000000..c00eaa40d44
--- /dev/null
+++ b/saleor/giftcard/migrations/0004_auto_20200902_1249.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1 on 2020-09-02 12:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("giftcard", "0003_auto_20200217_0350"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="giftcard",
+            name="current_balance_amount",
+            field=models.DecimalField(decimal_places=3, max_digits=12),
+        ),
+        migrations.AlterField(
+            model_name="giftcard",
+            name="initial_balance_amount",
+            field=models.DecimalField(decimal_places=3, max_digits=12),
+        ),
+    ]
diff --git a/saleor/graphql/core/scalars.py b/saleor/graphql/core/scalars.py
index df91269b005..02e198d7a94 100644
--- a/saleor/graphql/core/scalars.py
+++ b/saleor/graphql/core/scalars.py
@@ -36,6 +36,24 @@ def parse_value(value):
             return None
 
 
+class PositiveDecimal(Decimal):
+    """Positive Decimal scalar implementation.
+
+    Should be used in places where value must be positive.
+    """
+
+    @staticmethod
+    def parse_value(value):
+        value = super(PositiveDecimal, PositiveDecimal).parse_value(value)
+        if not value:
+            return value
+        if value < 0:
+            raise GraphQLError(
+                f"Value cannot be lower than 0. Unsupported value: {value}"
+            )
+        return value
+
+
 class WeightScalar(graphene.Scalar):
     @staticmethod
     def parse_value(value):
diff --git a/saleor/graphql/core/tests/test_core.py b/saleor/graphql/core/tests/test_core.py
index 1971be5d2ca..342bb61ed2f 100644
--- a/saleor/graphql/core/tests/test_core.py
+++ b/saleor/graphql/core/tests/test_core.py
@@ -77,38 +77,6 @@ def test_snake_to_camel_case():
     assert snake_to_camel_case(123) == 123
 
 
-def test_mutation_returns_error_field_in_camel_case(
-    staff_api_client, variant, permission_manage_products
-):
-    # costPrice is snake case variable (cost_price) in the backend
-    query = """
-    mutation testCamel($id: ID!, $price: Decimal, $cost: Decimal) {
-        productVariantUpdate(id: $id,
-        input: {costPrice: $cost, price: $price, trackInventory: false}) {
-            errors {
-                field
-                message
-            }
-            productVariant {
-                id
-            }
-        }
-    }
-    """
-    variables = {
-        "id": graphene.Node.to_global_id("ProductVariant", variant.id),
-        "price": 15,
-        "cost": 12.1234,
-    }
-    response = staff_api_client.post_graphql(
-        query, variables, permissions=[permission_manage_products]
-    )
-    content = get_graphql_content(response)
-    errors = content["data"]["productVariantUpdate"]["errors"]
-    assert len(errors) == 1
-    assert errors[0]["field"] == "costPriceAmount"
-
-
 def test_reporting_period_to_date():
     now = timezone.now()
     start_date = reporting_period_to_date(ReportingPeriod.TODAY)
@@ -161,11 +129,13 @@ def test_total_count_query(api_client, product):
     assert content["data"]["products"]["totalCount"] == Product.objects.count()
 
 
-def test_mutation_decimal_input(
+def test_mutation_positive_decimal_input(
     staff_api_client, variant, stock, permission_manage_products
 ):
     query = """
-    mutation decimalInput($id: ID!, $cost: Decimal, $price: Decimal) {
+    mutation PositiveDecimalInput(
+        $id: ID!, $cost: PositiveDecimal, $price: PositiveDecimal
+    ) {
         productVariantUpdate(id: $id, input: {costPrice: $cost, price: $price}) {
             errors {
                 field
@@ -193,11 +163,13 @@ def test_mutation_decimal_input(
     assert data["errors"] == []
 
 
-def test_mutation_decimal_input_without_arguments(
+def test_mutation_positive_decimal_input_without_arguments(
     staff_api_client, variant, permission_manage_products
 ):
     query = """
-    mutation ProductVariantUpdate($id: ID!, $price: Decimal, $costPrice: Decimal) {
+    mutation ProductVariantUpdate(
+        $id: ID!, $price: PositiveDecimal, $costPrice: PositiveDecimal
+    ) {
         productVariantUpdate(id: $id, input: {costPrice: $costPrice, price: $price}) {
             errors {
                 field
diff --git a/saleor/graphql/core/tests/test_validators.py b/saleor/graphql/core/tests/test_validators.py
new file mode 100644
index 00000000000..23c9567b792
--- /dev/null
+++ b/saleor/graphql/core/tests/test_validators.py
@@ -0,0 +1,40 @@
+from decimal import Decimal
+
+import pytest
+from django.core.exceptions import ValidationError
+
+from ..validators import validate_price_amount
+
+
+@pytest.mark.parametrize(
+    "value, currency",
+    [
+        (Decimal("1.1200"), "USD"),
+        (Decimal("1.12"), "USD"),
+        (Decimal("1"), "USD"),
+        (Decimal("1"), "ISK"),
+        (Decimal("1.00"), "ISK"),
+        (Decimal("5.12"), None),
+    ],
+)
+def test_validate_price_amount(value, currency):
+    # when
+    result = validate_price_amount(value, currency)
+
+    # then
+    assert result is None
+
+
+@pytest.mark.parametrize(
+    "value, currency",
+    [
+        (Decimal("1.1212"), "USD"),
+        (Decimal("1.128"), "USD"),
+        (Decimal("1.1"), "ISK"),
+        (Decimal("1.11"), "ISK"),
+        (Decimal("5.123"), None),
+    ],
+)
+def test_validate_price_amount_raise_error(value, currency):
+    with pytest.raises(ValidationError):
+        validate_price_amount(value, currency)
diff --git a/saleor/graphql/core/types/money.py b/saleor/graphql/core/types/money.py
index 0cbf0c91a86..ed70b40529c 100644
--- a/saleor/graphql/core/types/money.py
+++ b/saleor/graphql/core/types/money.py
@@ -1,6 +1,7 @@
 import graphene
 from django_prices.templatetags import prices
 
+from ....core.prices import quantize_price
 from ..enums import TaxRateType
 
 
@@ -19,6 +20,10 @@ class Money(graphene.ObjectType):
     class Meta:
         description = "Represents amount of money in specific currency."
 
+    @staticmethod
+    def resolve_amount(root, _info):
+        return quantize_price(root.amount, root.currency)
+
     @staticmethod
     def resolve_localized(root, _info):
         return prices.amount(root)
diff --git a/saleor/graphql/core/validators.py b/saleor/graphql/core/validators.py
index 44436fcddf3..4a4ab626500 100644
--- a/saleor/graphql/core/validators.py
+++ b/saleor/graphql/core/validators.py
@@ -1,5 +1,13 @@
+from typing import TYPE_CHECKING, Optional
+
+from django.conf import settings
+from django.core.exceptions import ValidationError
+from django_prices.utils.formatting import get_currency_fraction
 from graphql.error import GraphQLError
 
+if TYPE_CHECKING:
+    from decimal import Decimal
+
 
 def validate_one_of_args_is_in_query(arg1_name, arg1, arg2_name, arg2):
     if arg1 and arg2:
@@ -10,3 +18,22 @@ def validate_one_of_args_is_in_query(arg1_name, arg1, arg2_name, arg2):
         raise GraphQLError(
             f"Either '{arg1_name}'  or '{arg2_name}' argument is required"
         )
+
+
+def validate_price_amount(value: Optional["Decimal"], currency: str = None):
+    """Validate if price amount does not have too many decimal places.
+
+    Price amount can't have more decimal places than currency allow to.
+    Works only with decimal created from a string.
+    """
+
+    # check no needed when there is no value
+    if not value:
+        return
+
+    currency_fraction = get_currency_fraction(currency or settings.DEFAULT_CURRENCY)
+    value = value.normalize()
+    if abs(value.as_tuple().exponent) > currency_fraction:
+        raise ValidationError(
+            f"Value cannot have more than {currency_fraction} decimal places."
+        )
diff --git a/saleor/graphql/discount/mutations.py b/saleor/graphql/discount/mutations.py
index 8fc8dd3a02d..0739cae235c 100644
--- a/saleor/graphql/discount/mutations.py
+++ b/saleor/graphql/discount/mutations.py
@@ -10,8 +10,9 @@
     update_products_minimal_variant_prices_of_discount_task,
 )
 from ..core.mutations import BaseMutation, ModelDeleteMutation, ModelMutation
-from ..core.scalars import Decimal
+from ..core.scalars import PositiveDecimal
 from ..core.types.common import DiscountError
+from ..core.validators import validate_price_amount
 from ..product.types import Category, Collection, Product
 from .enums import DiscountValueTypeEnum, VoucherTypeEnum
 from .types import Sale, Voucher
@@ -95,7 +96,7 @@ class VoucherInput(graphene.InputObjectType):
     discount_value_type = DiscountValueTypeEnum(
         description="Choices: fixed or percentage."
     )
-    discount_value = Decimal(description="Value of the voucher.")
+    discount_value = PositiveDecimal(description="Value of the voucher.")
     products = graphene.List(
         graphene.ID, description="Products discounted by the voucher.", name="products"
     )
@@ -109,7 +110,7 @@ class VoucherInput(graphene.InputObjectType):
         description="Categories discounted by the voucher.",
         name="categories",
     )
-    min_amount_spent = Decimal(
+    min_amount_spent = PositiveDecimal(
         description="Min purchase amount required to apply the voucher."
     )
     min_checkout_items_quantity = graphene.Int(
@@ -161,6 +162,11 @@ def clean_input(cls, info, instance, data):
 
         min_spent_amount = cleaned_input.pop("min_amount_spent", None)
         if min_spent_amount is not None:
+            try:
+                validate_price_amount(min_spent_amount, instance.currency)
+            except ValidationError as error:
+                error.code = DiscountErrorCode.INVALID.value
+                raise ValidationError({"min_spent_amount": error})
             cleaned_input["min_spent_amount"] = min_spent_amount
         return cleaned_input
 
@@ -243,7 +249,7 @@ def perform_mutation(cls, _root, info, **data):
 class SaleInput(graphene.InputObjectType):
     name = graphene.String(description="Voucher name.")
     type = DiscountValueTypeEnum(description="Fixed or percentage.")
-    value = Decimal(description="Value of the voucher.")
+    value = PositiveDecimal(description="Value of the voucher.")
     products = graphene.List(
         graphene.ID, description="Products related to the discount.", name="products"
     )
diff --git a/saleor/graphql/discount/tests/test_discount.py b/saleor/graphql/discount/tests/test_discount.py
index 90d2b7fbb45..68bd383825c 100644
--- a/saleor/graphql/discount/tests/test_discount.py
+++ b/saleor/graphql/discount/tests/test_discount.py
@@ -143,7 +143,8 @@ def test_sale_query(staff_api_client, sale, permission_manage_discounts):
 mutation  voucherCreate(
     $type: VoucherTypeEnum, $name: String, $code: String,
     $discountValueType: DiscountValueTypeEnum, $usageLimit: Int,
-    $discountValue: Decimal, $minAmountSpent: Decimal, $minCheckoutItemsQuantity: Int,
+    $discountValue: PositiveDecimal,
+    $minAmountSpent: PositiveDecimal, $minCheckoutItemsQuantity: Int,
     $startDate: DateTime, $endDate: DateTime, $applyOncePerOrder: Boolean,
     $applyOncePerCustomer: Boolean) {
         voucherCreate(input: {
@@ -222,7 +223,7 @@ def test_create_voucher_with_empty_code(staff_api_client, permission_manage_disc
         "code": "",
         "discountValueType": DiscountValueTypeEnum.FIXED.name,
         "discountValue": 10.12,
-        "minSpent": 1.12,
+        "minAmountSpent": 1.12,
         "startDate": start_date.isoformat(),
         "endDate": end_date.isoformat(),
         "usageLimit": None,
@@ -265,6 +266,33 @@ def test_create_voucher_with_existing_gift_card_code(
     assert errors[0]["code"] == DiscountErrorCode.ALREADY_EXISTS.name
 
 
+def test_create_voucher_with_too_many_decimal_values_in_min_spent(
+    staff_api_client, permission_manage_discounts
+):
+    start_date = timezone.now() - timedelta(days=365)
+    end_date = timezone.now() + timedelta(days=365)
+    variables = {
+        "name": "test voucher",
+        "type": VoucherTypeEnum.ENTIRE_ORDER.name,
+        "code": "test123",
+        "discountValueType": DiscountValueTypeEnum.FIXED.name,
+        "discountValue": 10.12,
+        "minAmountSpent": 1.1201,
+        "startDate": start_date.isoformat(),
+        "endDate": end_date.isoformat(),
+        "usageLimit": None,
+    }
+
+    response = staff_api_client.post_graphql(
+        CREATE_VOUCHER_MUTATION, variables, permissions=[permission_manage_discounts]
+    )
+    content = get_graphql_content(response)
+    errors = content["data"]["voucherCreate"]["discountErrors"]
+    assert len(errors) == 1
+    assert errors[0]["field"] == "minSpentAmount"
+    assert errors[0]["code"] == DiscountErrorCode.INVALID.name
+
+
 def test_create_voucher_with_existing_voucher_code(
     staff_api_client, voucher_shipping_type, permission_manage_discounts
 ):
@@ -532,7 +560,7 @@ def test_voucher_remove_no_catalogues(
 def test_create_sale(staff_api_client, permission_manage_discounts):
     query = """
     mutation  saleCreate(
-            $type: DiscountValueTypeEnum, $name: String, $value: Decimal,
+            $type: DiscountValueTypeEnum, $name: String, $value: PositiveDecimal,
             $startDate: DateTime, $endDate: DateTime) {
         saleCreate(input: {
                 name: $name, type: $type, value: $value,
diff --git a/saleor/graphql/giftcard/mutations.py b/saleor/graphql/giftcard/mutations.py
index 789d1a1a81b..fbba4b5d3bb 100644
--- a/saleor/graphql/giftcard/mutations.py
+++ b/saleor/graphql/giftcard/mutations.py
@@ -12,8 +12,9 @@
 from ...giftcard.error_codes import GiftCardErrorCode
 from ...giftcard.utils import activate_gift_card, deactivate_gift_card
 from ..core.mutations import BaseMutation, ModelMutation
-from ..core.scalars import Decimal
+from ..core.scalars import PositiveDecimal
 from ..core.types.common import GiftCardError
+from ..core.validators import validate_price_amount
 from .types import GiftCard
 
 
@@ -24,7 +25,7 @@ class GiftCardUpdateInput(graphene.InputObjectType):
     end_date = graphene.types.datetime.Date(
         description="End date of the gift card in ISO 8601 format."
     )
-    balance = Decimal(description="Value of the gift card.")
+    balance = PositiveDecimal(description="Value of the gift card.")
     user_email = graphene.String(
         required=False, description="The customer's email of the gift card buyer."
     )
@@ -55,10 +56,17 @@ def clean_input(cls, info, instance, data):
         elif not is_available_promo_code(code):
             raise PromoCodeAlreadyExists(code=GiftCardErrorCode.ALREADY_EXISTS)
         cleaned_input = super().clean_input(info, instance, data)
+
         balance = cleaned_input.get("balance", None)
         if balance:
+            try:
+                validate_price_amount(balance, instance.currency)
+            except ValidationError as error:
+                error.code = GiftCardErrorCode.INVALID.value
+                raise ValidationError({"balance": error})
             cleaned_input["current_balance_amount"] = balance
             cleaned_input["initial_balance_amount"] = balance
+
         user_email = data.get("user_email", None)
         if user_email:
             try:
diff --git a/saleor/graphql/giftcard/tests/test_gift_card.py b/saleor/graphql/giftcard/tests/test_gift_card.py
index 2fe67b7eff8..27c4653271a 100644
--- a/saleor/graphql/giftcard/tests/test_gift_card.py
+++ b/saleor/graphql/giftcard/tests/test_gift_card.py
@@ -234,7 +234,7 @@ def test_query_gift_card_by_app_without_premissions(
 CREATE_GIFT_CARD_MUTATION = """
 mutation giftCardCreate(
     $code: String, $startDate: Date, $endDate: Date,
-    $balance: Decimal!, $userEmail: String) {
+    $balance: PositiveDecimal!, $userEmail: String) {
         giftCardCreate(input: {
                 code: $code, startDate: $startDate,
                 endDate: $endDate,
@@ -473,10 +473,38 @@ def test_create_gift_card_without_premissions(staff_api_client):
     assert_no_permission(response)
 
 
+def test_create_gift_card_with_to_many_decimal_places_in_amount(
+    staff_api_client, permission_manage_gift_card, permission_manage_users
+):
+    start_date = date(day=1, month=1, year=2018)
+    end_date = date(day=1, month=1, year=2019)
+    initial_balance = 10.123
+    variables = {
+        "code": "test12",
+        "startDate": start_date.isoformat(),
+        "endDate": end_date.isoformat(),
+        "balance": initial_balance,
+        "userEmail": staff_api_client.user.email,
+    }
+    response = staff_api_client.post_graphql(
+        CREATE_GIFT_CARD_MUTATION,
+        variables,
+        permissions=[permission_manage_gift_card, permission_manage_users],
+    )
+    content = get_graphql_content(response)
+    errors = content["data"]["giftCardCreate"]["giftCardErrors"]
+    data = content["data"]["giftCardCreate"]["giftCard"]
+
+    assert not data
+    assert len(errors) == 1
+    assert errors[0]["field"] == "balance"
+    assert errors[0]["code"] == GiftCardErrorCode.INVALID.name
+
+
 UPDATE_GIFT_CARD_MUTATION = """
 mutation giftCardUpdate(
     $id: ID!, $startDate: Date, $endDate: Date,
-    $balance: Decimal, $userEmail: String!) {
+    $balance: PositiveDecimal, $userEmail: String!) {
         giftCardUpdate(id: $id, input: {startDate: $startDate,
                 endDate: $endDate,
                 balance: $balance, userEmail: $userEmail}) {
diff --git a/saleor/graphql/order/mutations/draft_orders.py b/saleor/graphql/order/mutations/draft_orders.py
index 31b038597fd..531714c9442 100644
--- a/saleor/graphql/order/mutations/draft_orders.py
+++ b/saleor/graphql/order/mutations/draft_orders.py
@@ -21,7 +21,7 @@
 from ...account.i18n import I18nMixin
 from ...account.types import AddressInput
 from ...core.mutations import BaseMutation, ModelDeleteMutation, ModelMutation
-from ...core.scalars import Decimal
+from ...core.scalars import PositiveDecimal
 from ...core.types.common import OrderError
 from ...product.types import ProductVariant
 from ..types import Order, OrderLine
@@ -46,7 +46,7 @@ class DraftOrderInput(InputObjectType):
         descripton="Customer associated with the draft order.", name="user"
     )
     user_email = graphene.String(description="Email address of the customer.")
-    discount = Decimal(description="Discount amount for the order.")
+    discount = PositiveDecimal(description="Discount amount for the order.")
     shipping_address = AddressInput(description="Shipping address of the customer.")
     shipping_method = graphene.ID(
         description="ID of a selected shipping method.", name="shippingMethod"
diff --git a/saleor/graphql/order/mutations/orders.py b/saleor/graphql/order/mutations/orders.py
index 46da0496a7e..a3f81d3bcf7 100644
--- a/saleor/graphql/order/mutations/orders.py
+++ b/saleor/graphql/order/mutations/orders.py
@@ -19,7 +19,7 @@
 from ....payment import CustomPaymentChoices, PaymentError, TransactionKind, gateway
 from ...account.types import AddressInput
 from ...core.mutations import BaseMutation
-from ...core.scalars import UUID, Decimal
+from ...core.scalars import UUID, PositiveDecimal
 from ...core.types.common import OrderError
 from ...core.utils import validate_required_string_field
 from ...meta.deprecated.mutations import ClearMetaBaseMutation, UpdateMetaBaseMutation
@@ -362,7 +362,9 @@ class OrderCapture(BaseMutation):
 
     class Arguments:
         id = graphene.ID(required=True, description="ID of the order to capture.")
-        amount = Decimal(required=True, description="Amount of money to capture.")
+        amount = PositiveDecimal(
+            required=True, description="Amount of money to capture."
+        )
 
     class Meta:
         description = "Capture an order."
@@ -429,7 +431,9 @@ class OrderRefund(BaseMutation):
 
     class Arguments:
         id = graphene.ID(required=True, description="ID of the order to refund.")
-        amount = Decimal(required=True, description="Amount of money to refund.")
+        amount = PositiveDecimal(
+            required=True, description="Amount of money to refund."
+        )
 
     class Meta:
         description = "Refund an order."
diff --git a/saleor/graphql/order/tests/test_order.py b/saleor/graphql/order/tests/test_order.py
index 6812af7377f..079a2e76404 100644
--- a/saleor/graphql/order/tests/test_order.py
+++ b/saleor/graphql/order/tests/test_order.py
@@ -572,7 +572,7 @@ def test_draft_order_create(
     variant_0 = variant
     query = """
     mutation draftCreate(
-        $user: ID, $discount: Decimal, $lines: [OrderLineCreateInput],
+        $user: ID, $discount: PositiveDecimal, $lines: [OrderLineCreateInput],
         $shippingAddress: AddressInput, $shippingMethod: ID, $voucher: ID,
         $customerNote: String) {
             draftOrderCreate(
@@ -1666,7 +1666,7 @@ def test_order_capture(
 ):
     order = payment_txn_preauth.order
     query = """
-        mutation captureOrder($id: ID!, $amount: Decimal!) {
+        mutation captureOrder($id: ID!, $amount: PositiveDecimal!) {
             orderCapture(id: $id, amount: $amount) {
                 order {
                     paymentStatus
@@ -1873,7 +1873,7 @@ def test_order_void_payment_error(
 def test_order_refund(staff_api_client, permission_manage_orders, payment_txn_captured):
     order = payment_txn_captured.order
     query = """
-        mutation refundOrder($id: ID!, $amount: Decimal!) {
+        mutation refundOrder($id: ID!, $amount: PositiveDecimal!) {
             orderRefund(id: $id, amount: $amount) {
                 order {
                     paymentStatus
diff --git a/saleor/graphql/payment/mutations.py b/saleor/graphql/payment/mutations.py
index c7680f519b3..3b7a42aab6e 100644
--- a/saleor/graphql/payment/mutations.py
+++ b/saleor/graphql/payment/mutations.py
@@ -15,7 +15,7 @@
 from ..account.types import AddressInput
 from ..checkout.types import Checkout
 from ..core.mutations import BaseMutation
-from ..core.scalars import Decimal
+from ..core.scalars import PositiveDecimal
 from ..core.types import common as common_types
 from ..core.utils import from_global_id_strict_type
 from .types import Payment
@@ -34,7 +34,7 @@ class PaymentInput(graphene.InputObjectType):
             "billing data in a secure manner."
         ),
     )
-    amount = Decimal(
+    amount = PositiveDecimal(
         required=False,
         description=(
             "Total amount of the transaction, including "
@@ -190,7 +190,7 @@ class PaymentCapture(BaseMutation):
 
     class Arguments:
         payment_id = graphene.ID(required=True, description="Payment ID.")
-        amount = Decimal(description="Transaction amount.")
+        amount = PositiveDecimal(description="Transaction amount.")
 
     class Meta:
         description = "Captures the authorized payment amount."
diff --git a/saleor/graphql/payment/tests/test_payment.py b/saleor/graphql/payment/tests/test_payment.py
index f7122a1ed70..6019a87a9b8 100644
--- a/saleor/graphql/payment/tests/test_payment.py
+++ b/saleor/graphql/payment/tests/test_payment.py
@@ -398,7 +398,7 @@ def test_create_payment_for_checkout_with_active_payments(
 
 
 CAPTURE_QUERY = """
-    mutation PaymentCapture($paymentId: ID!, $amount: Decimal!) {
+    mutation PaymentCapture($paymentId: ID!, $amount: PositiveDecimal!) {
         paymentCapture(paymentId: $paymentId, amount: $amount) {
             payment {
                 id,
@@ -547,7 +547,7 @@ def test_payment_capture_gateway_dummy_credit_card_error(
 
 
 REFUND_QUERY = """
-    mutation PaymentRefund($paymentId: ID!, $amount: Decimal!) {
+    mutation PaymentRefund($paymentId: ID!, $amount: PositiveDecimal!) {
         paymentRefund(paymentId: $paymentId, amount: $amount) {
             payment {
                 id,
diff --git a/saleor/graphql/product/bulk_mutations/products.py b/saleor/graphql/product/bulk_mutations/products.py
index 14117de7cab..58d53e86f67 100644
--- a/saleor/graphql/product/bulk_mutations/products.py
+++ b/saleor/graphql/product/bulk_mutations/products.py
@@ -168,22 +168,10 @@ def clean_variant_input(
 
         cost_price_amount = cleaned_input.pop("cost_price", None)
         if cost_price_amount is not None:
-            if cost_price_amount < 0:
-                errors["costPrice"] = ValidationError(
-                    "Product price cannot be lower than 0.",
-                    code=ProductErrorCode.INVALID.value,
-                    params={"index": variant_index},
-                )
             cleaned_input["cost_price_amount"] = cost_price_amount
 
         price_amount = cleaned_input.pop("price", None)
         if price_amount is not None:
-            if price_amount < 0:
-                errors["price"] = ValidationError(
-                    "Product price cannot be lower than 0.",
-                    code=ProductErrorCode.INVALID.value,
-                    params={"index": variant_index},
-                )
             cleaned_input["price_amount"] = price_amount
 
         attributes = cleaned_input.get("attributes")
diff --git a/saleor/graphql/product/mutations/products.py b/saleor/graphql/product/mutations/products.py
index 210db68ea44..6decd124cb7 100644
--- a/saleor/graphql/product/mutations/products.py
+++ b/saleor/graphql/product/mutations/products.py
@@ -30,7 +30,7 @@
     generate_name_for_variant,
 )
 from ...core.mutations import BaseMutation, ModelDeleteMutation, ModelMutation
-from ...core.scalars import Decimal, WeightScalar
+from ...core.scalars import PositiveDecimal, WeightScalar
 from ...core.types import SeoInput, Upload
 from ...core.types.common import ProductError
 from ...core.utils import (
@@ -41,6 +41,7 @@
     validate_slug_and_generate_if_needed,
 )
 from ...core.utils.reordering import perform_reordering
+from ...core.validators import validate_price_amount
 from ...meta.deprecated.mutations import ClearMetaBaseMutation, UpdateMetaBaseMutation
 from ...warehouse.types import Warehouse
 from ..types import Category, Collection, Product, ProductImage, ProductVariant
@@ -535,7 +536,7 @@ class ProductInput(graphene.InputObjectType):
             "is only used if a product doesn't use variants."
         )
     )
-    base_price = Decimal(
+    base_price = PositiveDecimal(
         description=(
             "Default price for product variant. "
             "Note: this field is only used if a product doesn't use variants."
@@ -825,21 +826,17 @@ def clean_input(cls, info, instance, data):
                 {
                     "weight": ValidationError(
                         "Product can't have negative weight.",
-                        code=ProductErrorCode.INVALID,
+                        code=ProductErrorCode.INVALID.value,
                     )
                 }
             )
 
         base_price = cleaned_input.get("base_price")
-        if base_price is not None and base_price < 0:
-            raise ValidationError(
-                {
-                    "base_price": ValidationError(
-                        "Product price cannot be lower than 0.",
-                        code=ProductErrorCode.INVALID.value,
-                    )
-                }
-            )
+        try:
+            validate_price_amount(base_price, instance.currency)
+        except ValidationError as error:
+            error.code = ProductErrorCode.INVALID.value
+            raise ValidationError({"base_price": error})
 
         # Attributes are provided as list of `AttributeValueInput` objects.
         # We need to transform them into the format they're stored in the
@@ -1115,8 +1112,8 @@ class ProductVariantInput(graphene.InputObjectType):
         required=False,
         description="List of attributes specific to this variant.",
     )
-    cost_price = Decimal(description="Cost price of the variant.")
-    price = Decimal(description="Price of the particular variant.")
+    cost_price = PositiveDecimal(description="Cost price of the variant.")
+    price = PositiveDecimal(description="Price of the particular variant.")
     sku = graphene.String(description="Stock keeping unit.")
     track_inventory = graphene.Boolean(
         description=(
@@ -1202,15 +1199,11 @@ def clean_input(
 
         if "cost_price" in cleaned_input:
             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,
-                        )
-                    }
-                )
+            try:
+                validate_price_amount(cost_price, instance.currency)
+            except ValidationError as error:
+                error.code = ProductErrorCode.INVALID.value
+                raise ValidationError({"cost_price": error})
             cleaned_input["cost_price_amount"] = cost_price
 
         price = cleaned_input.get("price")
@@ -1225,16 +1218,11 @@ def clean_input(
             )
 
         if "price" in cleaned_input:
-            price = cleaned_input.pop("price")
-            if price is not None and price < 0:
-                raise ValidationError(
-                    {
-                        "price": ValidationError(
-                            "Product price cannot be lower than 0.",
-                            code=ProductErrorCode.INVALID.value,
-                        )
-                    }
-                )
+            try:
+                validate_price_amount(price, instance.currency)
+            except ValidationError as error:
+                error.code = ProductErrorCode.INVALID.value
+                raise ValidationError({"price": error})
             cleaned_input["price_amount"] = price
 
         stocks = cleaned_input.get("stocks")
diff --git a/saleor/graphql/product/tests/test_product.py b/saleor/graphql/product/tests/test_product.py
index d23c3c1c09a..d1da94929c1 100644
--- a/saleor/graphql/product/tests/test_product.py
+++ b/saleor/graphql/product/tests/test_product.py
@@ -33,6 +33,7 @@
 from ....warehouse.models import Allocation, Stock, Warehouse
 from ...core.enums import ReportingPeriod
 from ...tests.utils import (
+    assert_negative_positive_decimal_value,
     assert_no_permission,
     get_graphql_content,
     get_multipart_request_body,
@@ -1545,8 +1546,10 @@ def test_create_product_with_negative_base_price(
     description_json,
     permission_manage_products,
 ):
+    # given
     query = CREATE_PRODUCT_MUTATION
 
+    staff_api_client.user.user_permissions.add(permission_manage_products)
     description_json = json.dumps(description_json)
 
     product_type_id = graphene.Node.to_global_id("ProductType", product_type.pk)
@@ -1562,9 +1565,43 @@ def test_create_product_with_negative_base_price(
         }
     }
 
-    response = staff_api_client.post_graphql(
-        query, variables, permissions=[permission_manage_products]
-    )
+    # when
+    response = staff_api_client.post_graphql(query, variables)
+
+    # then
+    assert_negative_positive_decimal_value(response)
+
+
+def test_create_product_with_too_many_decimal_places_in_price(
+    staff_api_client,
+    product_type,
+    category,
+    description_json,
+    permission_manage_products,
+):
+    # given
+    query = CREATE_PRODUCT_MUTATION
+
+    staff_api_client.user.user_permissions.add(permission_manage_products)
+    description_json = json.dumps(description_json)
+
+    product_type_id = graphene.Node.to_global_id("ProductType", product_type.pk)
+    category_id = graphene.Node.to_global_id("Category", category.pk)
+    product_name = "test name"
+
+    variables = {
+        "input": {
+            "productType": product_type_id,
+            "category": category_id,
+            "name": product_name,
+            "basePrice": 1.1234,
+        }
+    }
+
+    # when
+    response = staff_api_client.post_graphql(query, variables)
+
+    # then
     content = get_graphql_content(response)
     data = content["data"]["productCreate"]
     error = data["productErrors"][0]
@@ -1613,7 +1650,7 @@ def test_create_product_with_unicode_in_slug_and_name(
         $productTypeId: ID!,
         $categoryId: ID!
         $name: String!,
-        $basePrice: Decimal!,
+        $basePrice: PositiveDecimal!,
         $sku: String,
         $trackInventory: Boolean)
     {
@@ -1887,6 +1924,75 @@ def assert_product_has_collections(product):
     get_graphql_content(response)
 
 
+MUTATION_UPDATE_PRODUCT = """
+    mutation updateProduct(
+        $productId: ID!,
+        $categoryId: ID!,
+        $name: String!,
+        $slug: String!,
+        $descriptionJson: JSONString!,
+        $isPublished: Boolean!,
+        $visibleInListings: Boolean!,
+        $chargeTaxes: Boolean!,
+        $taxCode: String!,
+        $basePrice: PositiveDecimal!,
+        $attributes: [AttributeValueInput!]) {
+            productUpdate(
+                id: $productId,
+                input: {
+                    category: $categoryId,
+                    name: $name,
+                    slug: $slug,
+                    descriptionJson: $descriptionJson,
+                    isPublished: $isPublished,
+                    visibleInListings: $visibleInListings,
+                    chargeTaxes: $chargeTaxes,
+                    taxCode: $taxCode,
+                    basePrice: $basePrice,
+                    attributes: $attributes
+                }) {
+                    product {
+                        category {
+                            name
+                        }
+                        descriptionJson
+                        isPublished
+                        chargeTaxes
+                        variants {
+                            price {
+                                amount
+                            }
+                        }
+                        taxType {
+                            taxCode
+                            description
+                        }
+                        name
+                        slug
+                        productType {
+                            name
+                        }
+                        attributes {
+                            attribute {
+                                id
+                                name
+                            }
+                            values {
+                                name
+                                slug
+                            }
+                        }
+                        visibleInListings
+                        }
+                        errors {
+                        message
+                        field
+                        }
+                    }
+                    }
+"""
+
+
 def test_update_product(
     staff_api_client,
     category,
@@ -1897,74 +2003,7 @@ def test_update_product(
     monkeypatch,
     color_attribute,
 ):
-    query = """
-        mutation updateProduct(
-            $productId: ID!,
-            $categoryId: ID!,
-            $name: String!,
-            $slug: String!,
-            $descriptionJson: JSONString!,
-            $isPublished: Boolean!,
-            $visibleInListings: Boolean!,
-            $chargeTaxes: Boolean!,
-            $taxCode: String!,
-            $basePrice: Decimal!,
-            $attributes: [AttributeValueInput!]) {
-                productUpdate(
-                    id: $productId,
-                    input: {
-                        category: $categoryId,
-                        name: $name,
-                        slug: $slug,
-                        descriptionJson: $descriptionJson,
-                        isPublished: $isPublished,
-                        visibleInListings: $visibleInListings,
-                        chargeTaxes: $chargeTaxes,
-                        taxCode: $taxCode,
-                        basePrice: $basePrice,
-                        attributes: $attributes
-                    }) {
-                        product {
-                            category {
-                                name
-                            }
-                            descriptionJson
-                            isPublished
-                            chargeTaxes
-                            variants {
-                                price {
-                                    amount
-                                }
-                            }
-                            taxType {
-                                taxCode
-                                description
-                            }
-                            name
-                            slug
-                            productType {
-                                name
-                            }
-                            attributes {
-                                attribute {
-                                    id
-                                    name
-                                }
-                                values {
-                                    name
-                                    slug
-                                }
-                            }
-                            visibleInListings
-                          }
-                          errors {
-                            message
-                            field
-                          }
-                        }
-                      }
-    """
-
+    query = MUTATION_UPDATE_PRODUCT
     other_description_json = json.dumps(other_description_json)
 
     product_id = graphene.Node.to_global_id("Product", product.pk)
@@ -2026,6 +2065,86 @@ def test_update_product(
     assert attributes[0]["values"][0]["slug"] == "rainbow"
 
 
+def test_update_product_when_default_currency_changeed(
+    staff_api_client,
+    category,
+    non_default_category,
+    product,
+    other_description_json,
+    permission_manage_products,
+    monkeypatch,
+    color_attribute,
+    settings,
+):
+    # Ensure that when default currency has changed, there is no errors
+    # when updating products with different currency which has different number
+    # of required decimal places
+
+    settings.DEFAULT_COUNTRY = "IS"
+    settings.DEFAULT_CURRENCY = "ISK"
+    query = MUTATION_UPDATE_PRODUCT
+
+    other_description_json = json.dumps(other_description_json)
+
+    product_id = graphene.Node.to_global_id("Product", product.pk)
+    category_id = graphene.Node.to_global_id("Category", non_default_category.pk)
+    product_name = "updated name"
+    product_slug = "updated-product"
+    product_is_published = True
+    product_visible_in_listings = False
+    product_charge_taxes = True
+    basePrice = 10.00
+    product_tax_rate = "STANDARD"
+
+    # Mock tax interface with fake response from tax gateway
+    monkeypatch.setattr(
+        PluginsManager,
+        "get_tax_code_from_object_meta",
+        lambda self, x: TaxType(description="", code=product_tax_rate),
+    )
+
+    attribute_id = graphene.Node.to_global_id("Attribute", color_attribute.pk)
+
+    variables = {
+        "productId": product_id,
+        "categoryId": category_id,
+        "name": product_name,
+        "slug": product_slug,
+        "descriptionJson": other_description_json,
+        "isPublished": product_is_published,
+        "visibleInListings": product_visible_in_listings,
+        "chargeTaxes": product_charge_taxes,
+        "taxCode": product_tax_rate,
+        "basePrice": basePrice,
+        "attributes": [{"id": attribute_id, "values": ["Rainbow"]}],
+    }
+
+    response = staff_api_client.post_graphql(
+        query, variables, permissions=[permission_manage_products]
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["productUpdate"]
+    assert data["errors"] == []
+    assert data["product"]["name"] == product_name
+    assert data["product"]["slug"] == product_slug
+    assert data["product"]["descriptionJson"] == other_description_json
+    assert data["product"]["isPublished"] == product_is_published
+    assert data["product"]["visibleInListings"] == product_visible_in_listings
+    assert data["product"]["chargeTaxes"] == product_charge_taxes
+    assert data["product"]["variants"][0]["price"]["amount"] == basePrice
+    assert data["product"]["taxType"]["taxCode"] == product_tax_rate
+    assert not data["product"]["category"]["name"] == category.name
+
+    attributes = data["product"]["attributes"]
+
+    assert len(attributes) == 1
+    assert len(attributes[0]["values"]) == 1
+
+    assert attributes[0]["attribute"]["id"] == attribute_id
+    assert attributes[0]["values"][0]["name"] == "Rainbow"
+    assert attributes[0]["values"][0]["slug"] == "rainbow"
+
+
 UPDATE_PRODUCT_SLUG_MUTATION = """
     mutation($id: ID!, $slug: String) {
         productUpdate(
@@ -2475,7 +2594,7 @@ def test_update_product_with_negative_base_price(
     query = """
         mutation updateProduct(
             $productId: ID!,
-            $basePrice: Decimal)
+            $basePrice: PositiveDecimal)
         {
             productUpdate(
                 id: $productId,
@@ -2494,14 +2613,98 @@ def test_update_product_with_negative_base_price(
             }
         }
     """
+    staff_api_client.user.user_permissions.add(permission_manage_products)
     product = product_with_default_variant
     product_id = graphene.Node.to_global_id("Product", product.pk)
 
     variables = {"productId": product_id, "basePrice": -1}
 
-    response = staff_api_client.post_graphql(
-        query, variables, permissions=[permission_manage_products]
-    )
+    response = staff_api_client.post_graphql(query, variables)
+
+    assert_negative_positive_decimal_value(response)
+
+
+def test_update_product_with_too_many_decimal_places_in_price(
+    staff_api_client, product_with_default_variant, permission_manage_products, product
+):
+    # given
+    query = """
+        mutation updateProduct(
+            $productId: ID!,
+            $basePrice: PositiveDecimal)
+        {
+            productUpdate(
+                id: $productId,
+                input: {
+                    basePrice: $basePrice
+                })
+            {
+                product {
+                    id
+                }
+                productErrors {
+                    field
+                    message
+                    code
+                }
+            }
+        }
+    """
+    staff_api_client.user.user_permissions.add(permission_manage_products)
+    product = product_with_default_variant
+    product_id = graphene.Node.to_global_id("Product", product.pk)
+
+    variables = {"productId": product_id, "basePrice": 1.1001}
+
+    # when
+    response = staff_api_client.post_graphql(query, variables)
+
+    # then
+    content = get_graphql_content(response)
+    data = content["data"]["productUpdate"]
+    error = data["productErrors"][0]
+    assert error["field"] == "basePrice"
+    assert error["code"] == ProductErrorCode.INVALID.name
+
+
+def test_update_product_with_too_many_decimal_places_in_price_different_currency(
+    staff_api_client, product_with_default_variant, permission_manage_products, product
+):
+    # given
+    query = """
+        mutation updateProduct(
+            $productId: ID!,
+            $basePrice: PositiveDecimal)
+        {
+            productUpdate(
+                id: $productId,
+                input: {
+                    basePrice: $basePrice
+                })
+            {
+                product {
+                    id
+                }
+                productErrors {
+                    field
+                    message
+                    code
+                }
+            }
+        }
+    """
+    product_with_default_variant.currency = "ISK"
+    product_with_default_variant.save(update_fields=["currency"])
+    staff_api_client.user.user_permissions.add(permission_manage_products)
+    product = product_with_default_variant
+    product_id = graphene.Node.to_global_id("Product", product.pk)
+
+    variables = {"productId": product_id, "basePrice": 1.1}
+
+    # when
+    response = staff_api_client.post_graphql(query, variables)
+
+    # then
     content = get_graphql_content(response)
     data = content["data"]["productUpdate"]
     error = data["productErrors"][0]
@@ -4368,7 +4571,7 @@ def test_product_filter_by_attribute_values(
         $name: String!,
         $sku: String,
         $stocks: [StockInput!],
-        $basePrice: Decimal!
+        $basePrice: PositiveDecimal!
         $trackInventory: Boolean)
     {
         productCreate(
@@ -4558,7 +4761,7 @@ def test_update_or_create_variant_stocks_empty_stocks_data(variant, warehouses):
         $category: ID!
         $name: String!,
         $sku: String,
-        $basePrice: Decimal!,
+        $basePrice: PositiveDecimal!,
         $weight: WeightScalar)
     {
         productCreate(
@@ -4663,7 +4866,7 @@ def test_create_product_with_weight_input(
             $category: ID!,
             $name: String!,
             $sku: String,
-            $basePrice: Decimal!)
+            $basePrice: PositiveDecimal!)
         {{
             productCreate(
                 input: {{
diff --git a/saleor/graphql/product/tests/test_product_minimal_variant_price.py b/saleor/graphql/product/tests/test_product_minimal_variant_price.py
index 23ffb96e839..916bb9d5c1a 100644
--- a/saleor/graphql/product/tests/test_product_minimal_variant_price.py
+++ b/saleor/graphql/product/tests/test_product_minimal_variant_price.py
@@ -4,9 +4,8 @@
 from freezegun import freeze_time
 from graphql_relay import from_global_id, to_global_id
 
-from ....product.error_codes import ProductErrorCode
 from ...discount.enums import DiscountValueTypeEnum
-from ...tests.utils import get_graphql_content
+from ...tests.utils import assert_negative_positive_decimal_value, get_graphql_content
 
 
 @patch(
@@ -23,7 +22,7 @@ def test_product_variant_create_updates_minimal_variant_price(
         mutation ProductVariantCreate(
             $productId: ID!,
             $sku: String!,
-            $price: Decimal,
+            $price: PositiveDecimal,
             $attributes: [AttributeValueInput]!,
         ) {
             productVariantCreate(
@@ -84,7 +83,7 @@ def test_product_variant_update_updates_minimal_variant_price(
     query = """
         mutation ProductVariantUpdate(
             $id: ID!,
-            $price: Decimal,
+            $price: PositiveDecimal,
         ) {
             productVariantUpdate(
                 id: $id,
@@ -133,7 +132,7 @@ def test_product_variant_update_updates_invalid_variant_price(
     query = """
         mutation ProductVariantUpdate(
             $id: ID!,
-            $price: Decimal,
+            $price: PositiveDecimal,
         ) {
             productVariantUpdate(
                 id: $id,
@@ -152,19 +151,15 @@ def test_product_variant_update_updates_invalid_variant_price(
             }
         }
     """
+    staff_api_client.user.user_permissions.add(permission_manage_products)
     variant = product.variants.first()
     variant_id = to_global_id("ProductVariant", variant.pk)
     price = "-1.99"
     variables = {"id": variant_id, "price": 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"] == "price"
-    assert data["productErrors"][0]["code"] == ProductErrorCode.INVALID.name
+    response = staff_api_client.post_graphql(query, variables)
+
+    assert_negative_positive_decimal_value(response)
 
 
 @patch(
@@ -180,7 +175,7 @@ def test_product_variant_update_updates_invalid_cost_price(
     query = """
         mutation ProductVariantUpdate(
             $id: ID!,
-            $costPrice: Decimal,
+            $costPrice: PositiveDecimal,
         ) {
             productVariantUpdate(
                 id: $id,
@@ -199,19 +194,15 @@ def test_product_variant_update_updates_invalid_cost_price(
             }
         }
     """
+    staff_api_client.user.user_permissions.add(permission_manage_products)
     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
+    response = staff_api_client.post_graphql(query, variables)
+
+    assert_negative_positive_decimal_value(response)
 
 
 @patch(
@@ -399,7 +390,7 @@ def test_sale_create_updates_products_minimal_variant_prices(
     mutation SaleCreate(
             $name: String,
             $type: DiscountValueTypeEnum,
-            $value: Decimal,
+            $value: PositiveDecimal,
             $products: [ID]
     ) {
         saleCreate(input: {
@@ -448,7 +439,7 @@ def test_sale_update_updates_products_minimal_variant_prices(
     permission_manage_discounts,
 ):
     query = """
-    mutation SaleUpdate($id: ID!, $value: Decimal) {
+    mutation SaleUpdate($id: ID!, $value: PositiveDecimal) {
         saleUpdate(id: $id, input: {value: $value}) {
             sale {
                 id
diff --git a/saleor/graphql/product/tests/test_variant.py b/saleor/graphql/product/tests/test_variant.py
index cc4d0ee206d..1f116199a83 100644
--- a/saleor/graphql/product/tests/test_variant.py
+++ b/saleor/graphql/product/tests/test_variant.py
@@ -12,7 +12,7 @@
 from ....warehouse.error_codes import StockErrorCode
 from ....warehouse.models import Stock, Warehouse
 from ...core.enums import WeightUnitsEnum
-from ...tests.utils import get_graphql_content
+from ...tests.utils import assert_negative_positive_decimal_value, get_graphql_content
 
 
 def test_fetch_variant(
@@ -96,8 +96,8 @@ def test_create_variant(
             $productId: ID!,
             $sku: String!,
             $stocks: [StockInput!],
-            $price: Decimal,
-            $costPrice: Decimal,
+            $price: PositiveDecimal,
+            $costPrice: PositiveDecimal,
             $attributes: [AttributeValueInput]!,
             $weight: WeightScalar,
             $trackInventory: Boolean!) {
@@ -284,7 +284,7 @@ def test_create_product_variant_not_all_attributes(
     query = """
             mutation createVariant (
                 $productId: ID!,
-                $price: Decimal,
+                $price: PositiveDecimal,
                 $sku: String!,
                 $attributes: [AttributeValueInput]!) {
                     productVariantCreate(
@@ -330,6 +330,53 @@ def test_create_product_variant_not_all_attributes(
     assert not product.variants.filter(sku=sku).exists()
 
 
+def test_create_product_variant_too_many_places_in_cost_price(
+    staff_api_client, product, product_type, permission_manage_products
+):
+    query = """
+        mutation createVariant (
+            $productId: ID!,
+            $attributes: [AttributeValueInput]!,
+            $weight: WeightScalar,
+            $costPrice: PositiveDecimal) {
+                productVariantCreate(
+                    input: {
+                        product: $productId,
+                        attributes: $attributes,
+                        weight: $weight,
+                        costPrice: $costPrice,
+                    }) {
+                    productErrors {
+                        field
+                        code
+                        message
+                    }
+                }
+            }
+    """
+    product_id = graphene.Node.to_global_id("Product", product.pk)
+
+    variant_id = graphene.Node.to_global_id(
+        "Attribute", product_type.variant_attributes.first().pk
+    )
+    variant_value = "test-value"
+
+    variables = {
+        "productId": product_id,
+        "weight": 1,
+        "attributes": [{"id": variant_id, "values": [variant_value]}],
+        "costPrice": 1.40001,
+    }
+    response = staff_api_client.post_graphql(
+        query, variables, permissions=[permission_manage_products]
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["productVariantCreate"]
+    error = data["productErrors"][0]
+    assert error["field"] == "costPrice"
+    assert error["code"] == ProductErrorCode.INVALID.name
+
+
 def test_create_product_variant_duplicated_attributes(
     staff_api_client,
     product_with_variant_with_two_attributes,
@@ -340,7 +387,7 @@ def test_create_product_variant_duplicated_attributes(
     query = """
         mutation createVariant (
             $productId: ID!,
-            $price: Decimal,
+            $price: PositiveDecimal,
             $sku: String!,
             $attributes: [AttributeValueInput]!
         ) {
@@ -393,8 +440,8 @@ def test_create_product_variant_update_with_new_attributes(
         mutation VariantUpdate(
           $id: ID!
           $attributes: [AttributeValueInput]
-          $costPrice: Decimal
-          $price: Decimal
+          $costPrice: PositiveDecimal
+          $price: PositiveDecimal
           $sku: String
           $trackInventory: Boolean!
         ) {
@@ -466,8 +513,8 @@ def test_update_product_variant(staff_api_client, product, permission_manage_pro
         mutation updateVariant (
             $id: ID!,
             $sku: String!,
-            $price: Decimal,
-            $costPrice: Decimal,
+            $price: PositiveDecimal,
+            $costPrice: PositiveDecimal,
             $trackInventory: Boolean!) {
                 productVariantUpdate(
                     id: $id,
@@ -525,7 +572,7 @@ def test_update_product_variant_with_negative_weight(
     query = """
         mutation updateVariant (
             $id: ID!,
-            $price: Decimal,
+            $price: PositiveDecimal,
             $weight: WeightScalar
         ) {
             productVariantUpdate(
@@ -570,8 +617,8 @@ def test_update_product_variant_unset_cost_price(
         mutation updateVariant (
             $id: ID!,
             $sku: String!,
-            $price: Decimal,
-            $costPrice: Decimal) {
+            $price: PositiveDecimal,
+            $costPrice: PositiveDecimal) {
                 productVariantUpdate(
                     id: $id,
                     input: {
@@ -615,7 +662,7 @@ def test_update_product_variant_unset_cost_price(
     mutation updateVariant (
         $id: ID!,
         $sku: String,
-        $price: Decimal,
+        $price: PositiveDecimal,
         $attributes: [AttributeValueInput]!) {
             productVariantUpdate(
                 id: $id,
@@ -637,6 +684,47 @@ def test_update_product_variant_unset_cost_price(
 """
 
 
+def test_update_product_variant_with_too_many_decimal_values_in_price(
+    staff_api_client, product, permission_manage_products
+):
+    query = """
+        mutation updateVariant (
+            $id: ID!,
+            $price: PositiveDecimal,
+            $weight: WeightScalar
+        ) {
+            productVariantUpdate(
+                id: $id,
+                input: {
+                    weight: $weight,
+                    price: $price
+                }
+            ){
+                productVariant {
+                    name
+                }
+                productErrors {
+                    field
+                    message
+                    code
+                }
+            }
+        }
+    """
+    variant = product.variants.first()
+    variant_id = graphene.Node.to_global_id("ProductVariant", variant.pk)
+    variables = {"id": variant_id, "weight": 1, "price": 15.234}
+    response = staff_api_client.post_graphql(
+        query, variables, permissions=[permission_manage_products]
+    )
+    variant.refresh_from_db()
+    content = get_graphql_content(response)
+    data = content["data"]["productVariantUpdate"]
+    error = data["productErrors"][0]
+    assert error["field"] == "price"
+    assert error["code"] == ProductErrorCode.INVALID.name
+
+
 def test_update_product_variant_not_all_attributes(
     staff_api_client, product, product_type, color_attribute, permission_manage_products
 ):
@@ -1191,6 +1279,7 @@ def test_product_variant_bulk_create_by_attribute_id(
 def test_product_variant_bulk_create_by_attribute_id_with_invalid_price(
     staff_api_client, product, size_attribute, permission_manage_products, price_field
 ):
+    # given
     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()
@@ -1206,13 +1295,14 @@ def test_product_variant_bulk_create_by_attribute_id_with_invalid_price(
 
     variables = {"productId": product_id, "variants": [variant]}
     staff_api_client.user.user_permissions.add(permission_manage_products)
+
+    # when
     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
+
+    # then
+    assert_negative_positive_decimal_value(response)
 
 
 def test_product_variant_bulk_create_empty_attribute(
diff --git a/saleor/graphql/schema.graphql b/saleor/graphql/schema.graphql
index e7eee51dc49..e2953571b50 100644
--- a/saleor/graphql/schema.graphql
+++ b/saleor/graphql/schema.graphql
@@ -1630,8 +1630,6 @@ type DeactivateAllUserTokens {
   accountErrors: [AccountError!]!
 }
 
-scalar Decimal
-
 type DeleteMetadata {
   errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
   metadataErrors: [MetadataError!]!
@@ -1778,7 +1776,7 @@ input DraftOrderCreateInput {
   billingAddress: AddressInput
   user: ID
   userEmail: String
-  discount: Decimal
+  discount: PositiveDecimal
   shippingAddress: AddressInput
   shippingMethod: ID
   voucher: ID
@@ -1796,7 +1794,7 @@ input DraftOrderInput {
   billingAddress: AddressInput
   user: ID
   userEmail: String
-  discount: Decimal
+  discount: PositiveDecimal
   shippingAddress: AddressInput
   shippingMethod: ID
   voucher: ID
@@ -2064,7 +2062,7 @@ type GiftCardCreate {
 input GiftCardCreateInput {
   startDate: Date
   endDate: Date
-  balance: Decimal
+  balance: PositiveDecimal
   userEmail: String
   code: String
 }
@@ -2099,7 +2097,7 @@ type GiftCardUpdate {
 input GiftCardUpdateInput {
   startDate: Date
   endDate: Date
-  balance: Decimal
+  balance: PositiveDecimal
   userEmail: String
 }
 
@@ -2674,8 +2672,8 @@ type Mutation {
   productVariantClearPrivateMetadata(id: ID!, input: MetaPath!): ProductVariantClearPrivateMeta @deprecated(reason: "Use the `deletePrivateMetadata` mutation instead. This field will be removed after 2020-07-31.")
   variantImageAssign(imageId: ID!, variantId: ID!): VariantImageAssign
   variantImageUnassign(imageId: ID!, variantId: ID!): VariantImageUnassign
-  paymentCapture(amount: Decimal, paymentId: ID!): PaymentCapture
-  paymentRefund(amount: Decimal, paymentId: ID!): PaymentRefund
+  paymentCapture(amount: PositiveDecimal, paymentId: ID!): PaymentCapture
+  paymentRefund(amount: PositiveDecimal, paymentId: ID!): PaymentRefund
   paymentVoid(paymentId: ID!): PaymentVoid
   pageCreate(input: PageInput!): PageCreate
   pageDelete(id: ID!): PageDelete
@@ -2694,7 +2692,7 @@ type Mutation {
   draftOrderUpdate(id: ID!, input: DraftOrderInput!): DraftOrderUpdate
   orderAddNote(order: ID!, input: OrderAddNoteInput!): OrderAddNote
   orderCancel(id: ID!): OrderCancel
-  orderCapture(amount: Decimal!, id: ID!): OrderCapture
+  orderCapture(amount: PositiveDecimal!, id: ID!): OrderCapture
   orderClearPrivateMeta(id: ID!, input: MetaPath!): OrderClearPrivateMeta @deprecated(reason: "Use the `deletePrivateMetadata` mutation instead. This field will be removed after 2020-07-31.")
   orderClearMeta(input: MetaPath!, token: UUID!): OrderClearMeta @deprecated(reason: "Use the `deleteMetadata` mutation instead. This field will be removed after 2020-07-31.")
   orderFulfill(input: OrderFulfillInput!, order: ID): OrderFulfill
@@ -2705,7 +2703,7 @@ type Mutation {
   orderFulfillmentUpdateMeta(id: ID!, input: MetaInput!): FulfillmentUpdateMeta @deprecated(reason: "Use the `updateMetadata` mutation instead. This field will be removed after 2020-07-31.")
   orderFulfillmentUpdatePrivateMeta(id: ID!, input: MetaInput!): FulfillmentUpdatePrivateMeta @deprecated(reason: "Use the `updatePrivateMetadata` mutation instead. This field will be removed after 2020-07-31.")
   orderMarkAsPaid(id: ID!): OrderMarkAsPaid
-  orderRefund(amount: Decimal!, id: ID!): OrderRefund
+  orderRefund(amount: PositiveDecimal!, id: ID!): OrderRefund
   orderUpdate(id: ID!, input: OrderUpdateInput!): OrderUpdate
   orderUpdateMeta(input: MetaInput!, token: UUID!): OrderUpdateMeta @deprecated(reason: "Use the `updateMetadata` mutation instead. This field will be removed after 2020-07-31.")
   orderUpdatePrivateMeta(id: ID!, input: MetaInput!): OrderUpdatePrivateMeta @deprecated(reason: "Use the `updatePrivateMetadata` mutation instead. This field will be removed after 2020-07-31.")
@@ -3448,7 +3446,7 @@ type PaymentGateway {
 input PaymentInput {
   gateway: String!
   token: String
-  amount: Decimal
+  amount: PositiveDecimal
   billingAddress: AddressInput
   returnUrl: String
 }
@@ -3617,6 +3615,8 @@ input PluginUpdateInput {
   configuration: [ConfigurationItemInput]
 }
 
+scalar PositiveDecimal
+
 input PriceRangeInput {
   gte: Float
   lte: Float
@@ -3724,7 +3724,7 @@ input ProductCreateInput {
   weight: WeightScalar
   sku: String
   trackInventory: Boolean
-  basePrice: Decimal
+  basePrice: PositiveDecimal
   visibleInListings: Boolean
   productType: ID!
   stocks: [StockInput!]
@@ -3858,7 +3858,7 @@ input ProductInput {
   weight: WeightScalar
   sku: String
   trackInventory: Boolean
-  basePrice: Decimal
+  basePrice: PositiveDecimal
   visibleInListings: Boolean
 }
 
@@ -4105,8 +4105,8 @@ type ProductVariantBulkCreate {
 
 input ProductVariantBulkCreateInput {
   attributes: [AttributeValueInput]!
-  costPrice: Decimal
-  price: Decimal
+  costPrice: PositiveDecimal
+  price: PositiveDecimal
   sku: String!
   trackInventory: Boolean
   weight: WeightScalar
@@ -4150,8 +4150,8 @@ type ProductVariantCreate {
 
 input ProductVariantCreateInput {
   attributes: [AttributeValueInput]!
-  costPrice: Decimal
-  price: Decimal
+  costPrice: PositiveDecimal
+  price: PositiveDecimal
   sku: String
   trackInventory: Boolean
   weight: WeightScalar
@@ -4167,8 +4167,8 @@ type ProductVariantDelete {
 
 input ProductVariantInput {
   attributes: [AttributeValueInput]
-  costPrice: Decimal
-  price: Decimal
+  costPrice: PositiveDecimal
+  price: PositiveDecimal
   sku: String
   trackInventory: Boolean
   weight: WeightScalar
@@ -4395,7 +4395,7 @@ input SaleFilterInput {
 input SaleInput {
   name: String
   type: DiscountValueTypeEnum
-  value: Decimal
+  value: PositiveDecimal
   products: [ID]
   categories: [ID]
   collections: [ID]
@@ -4641,9 +4641,9 @@ type ShippingPriceDelete {
 
 input ShippingPriceInput {
   name: String
-  price: Decimal
-  minimumOrderPrice: Decimal
-  maximumOrderPrice: Decimal
+  price: PositiveDecimal
+  minimumOrderPrice: PositiveDecimal
+  maximumOrderPrice: PositiveDecimal
   minimumOrderWeight: WeightScalar
   maximumOrderWeight: WeightScalar
   type: ShippingMethodTypeEnum
@@ -5353,11 +5353,11 @@ input VoucherInput {
   startDate: DateTime
   endDate: DateTime
   discountValueType: DiscountValueTypeEnum
-  discountValue: Decimal
+  discountValue: PositiveDecimal
   products: [ID]
   collections: [ID]
   categories: [ID]
-  minAmountSpent: Decimal
+  minAmountSpent: PositiveDecimal
   minCheckoutItemsQuantity: Int
   countries: [String]
   applyOncePerOrder: Boolean
diff --git a/saleor/graphql/shipping/mutations.py b/saleor/graphql/shipping/mutations.py
index 57c2512c168..257e4b75350 100644
--- a/saleor/graphql/shipping/mutations.py
+++ b/saleor/graphql/shipping/mutations.py
@@ -10,20 +10,21 @@
     get_countries_without_shipping_zone,
 )
 from ..core.mutations import BaseMutation, ModelDeleteMutation, ModelMutation
-from ..core.scalars import Decimal, WeightScalar
+from ..core.scalars import PositiveDecimal, WeightScalar
 from ..core.types.common import ShippingError
 from ..core.utils import get_duplicates_ids
+from ..core.validators import validate_price_amount
 from .enums import ShippingMethodTypeEnum
 from .types import ShippingMethod, ShippingZone
 
 
 class ShippingPriceInput(graphene.InputObjectType):
     name = graphene.String(description="Name of the shipping method.")
-    price = Decimal(description="Shipping price of the shipping method.")
-    minimum_order_price = Decimal(
+    price = PositiveDecimal(description="Shipping price of the shipping method.")
+    minimum_order_price = PositiveDecimal(
         description="Minimum order price to use this shipping method."
     )
-    maximum_order_price = Decimal(
+    maximum_order_price = PositiveDecimal(
         description="Maximum order price to use this shipping method."
     )
     minimum_order_weight = WeightScalar(
@@ -170,15 +171,11 @@ def clean_input(cls, info, instance, data, input_cls=None):
         # Rename the price field to price_amount (the model's)
         price_amount = cleaned_input.pop("price", None)
         if price_amount is not None:
-            if price_amount < 0:
-                raise ValidationError(
-                    {
-                        "price": ValidationError(
-                            ("Shipping rate price cannot be lower than 0."),
-                            code=ShippingErrorCode.INVALID,
-                        )
-                    }
-                )
+            try:
+                validate_price_amount(price_amount, instance.currency)
+            except ValidationError as error:
+                error.code = ShippingErrorCode.INVALID.value
+                raise ValidationError({"price": error})
             cleaned_input["price_amount"] = price_amount
 
         cleaned_type = cleaned_input.get("type")
@@ -188,9 +185,19 @@ def clean_input(cls, info, instance, data, input_cls=None):
                 max_price = cleaned_input.pop("maximum_order_price", None)
 
                 if min_price is not None:
+                    try:
+                        validate_price_amount(min_price, instance.currency)
+                    except ValidationError as error:
+                        error.code = ShippingErrorCode.INVALID.value
+                        raise ValidationError({"minimum_order_price": error})
                     cleaned_input["minimum_order_price_amount"] = min_price
 
                 if max_price is not None:
+                    try:
+                        validate_price_amount(max_price, instance.currency)
+                    except ValidationError as error:
+                        error.code = ShippingErrorCode.INVALID.value
+                        raise ValidationError({"maximum_order_price": error})
                     cleaned_input["maximum_order_price_amount"] = max_price
 
                 if (
diff --git a/saleor/graphql/shipping/tests/test_shipping_method.py b/saleor/graphql/shipping/tests/test_shipping_method.py
index f4e783b8426..3ee23ad9ca3 100644
--- a/saleor/graphql/shipping/tests/test_shipping_method.py
+++ b/saleor/graphql/shipping/tests/test_shipping_method.py
@@ -6,7 +6,7 @@
 from ....shipping.error_codes import ShippingErrorCode
 from ....shipping.utils import get_countries_without_shipping_zone
 from ...core.enums import WeightUnitsEnum
-from ...tests.utils import get_graphql_content
+from ...tests.utils import assert_negative_positive_decimal_value, get_graphql_content
 from ..types import ShippingMethodTypeEnum
 
 SHIPPING_ZONE_QUERY = """
@@ -523,9 +523,9 @@ def test_delete_shipping_zone(
 
 PRICE_BASED_SHIPPING_QUERY = """
     mutation createShippingPrice(
-        $type: ShippingMethodTypeEnum, $name: String!, $price: Decimal,
-        $shippingZone: ID!, $minimumOrderPrice: Decimal,
-        $maximumOrderPrice: Decimal) {
+        $type: ShippingMethodTypeEnum, $name: String!, $price: PositiveDecimal,
+        $shippingZone: ID!, $minimumOrderPrice: PositiveDecimal,
+        $maximumOrderPrice: PositiveDecimal) {
     shippingPriceCreate(input: {
             name: $name, price: $price, shippingZone: $shippingZone,
             minimumOrderPrice: $minimumOrderPrice,
@@ -600,10 +600,11 @@ def test_create_shipping_method(
     assert data["shippingZone"]["id"] == shipping_zone_id
 
 
-def test_create_shipping_method_with_invalid_price(
+def test_create_shipping_method_with_negative_price(
     staff_api_client, shipping_zone, permission_manage_shipping,
 ):
     query = PRICE_BASED_SHIPPING_QUERY
+    staff_api_client.user.user_permissions.add(permission_manage_shipping)
     name = "DHL"
     price = -12.34
     shipping_zone_id = graphene.Node.to_global_id("ShippingZone", shipping_zone.pk)
@@ -615,13 +616,94 @@ def test_create_shipping_method_with_invalid_price(
         "maximumOrderPrice": 20,
         "type": ShippingMethodTypeEnum.PRICE.name,
     }
-    response = staff_api_client.post_graphql(
-        query, variables, permissions=[permission_manage_shipping]
-    )
+
+    response = staff_api_client.post_graphql(query, variables)
+
+    assert_negative_positive_decimal_value(response)
+
+
+def test_create_shipping_method_with_to_many_decimal_places_in_price(
+    staff_api_client, shipping_zone, permission_manage_shipping,
+):  # given
+    query = PRICE_BASED_SHIPPING_QUERY
+    staff_api_client.user.user_permissions.add(permission_manage_shipping)
+    name = "DHL"
+    price = 12.345
+    shipping_zone_id = graphene.Node.to_global_id("ShippingZone", shipping_zone.pk)
+    variables = {
+        "shippingZone": shipping_zone_id,
+        "name": name,
+        "price": price,
+        "minimumOrderPrice": 0,
+        "maximumOrderPrice": 20,
+        "type": ShippingMethodTypeEnum.PRICE.name,
+    }
+
+    # when
+    response = staff_api_client.post_graphql(query, variables)
+
+    # then
     content = get_graphql_content(response)
     data = content["data"]["shippingPriceCreate"]
-    assert data["shippingErrors"][0]["field"] == "price"
-    assert data["shippingErrors"][0]["code"] == ShippingErrorCode.INVALID.name
+    error = data["shippingErrors"][0]
+    assert error["field"] == "price"
+    assert error["code"] == ShippingErrorCode.INVALID.name
+
+
+def test_create_shipping_method_with_to_many_decimal_places_in_minimum_order_price(
+    staff_api_client, shipping_zone, permission_manage_shipping,
+):  # given
+    query = PRICE_BASED_SHIPPING_QUERY
+    staff_api_client.user.user_permissions.add(permission_manage_shipping)
+    name = "DHL"
+    price = 12.34
+    shipping_zone_id = graphene.Node.to_global_id("ShippingZone", shipping_zone.pk)
+    variables = {
+        "shippingZone": shipping_zone_id,
+        "name": name,
+        "price": price,
+        "minimumOrderPrice": 1.2001,
+        "maximumOrderPrice": 20,
+        "type": ShippingMethodTypeEnum.PRICE.name,
+    }
+
+    # when
+    response = staff_api_client.post_graphql(query, variables)
+
+    # then
+    content = get_graphql_content(response)
+    data = content["data"]["shippingPriceCreate"]
+    error = data["shippingErrors"][0]
+    assert error["field"] == "minimumOrderPrice"
+    assert error["code"] == ShippingErrorCode.INVALID.name
+
+
+def test_create_shipping_method_with_to_many_decimal_places_in_maximum_order_price(
+    staff_api_client, shipping_zone, permission_manage_shipping,
+):  # given
+    query = PRICE_BASED_SHIPPING_QUERY
+    staff_api_client.user.user_permissions.add(permission_manage_shipping)
+    name = "DHL"
+    price = 12.34
+    shipping_zone_id = graphene.Node.to_global_id("ShippingZone", shipping_zone.pk)
+    variables = {
+        "shippingZone": shipping_zone_id,
+        "name": name,
+        "price": price,
+        "minimumOrderPrice": 0,
+        "maximumOrderPrice": 20.00001,
+        "type": ShippingMethodTypeEnum.PRICE.name,
+    }
+
+    # when
+    response = staff_api_client.post_graphql(query, variables)
+
+    # then
+    content = get_graphql_content(response)
+    data = content["data"]["shippingPriceCreate"]
+    error = data["shippingErrors"][0]
+    assert error["field"] == "maximumOrderPrice"
+    assert error["code"] == ShippingErrorCode.INVALID.name
 
 
 def test_create_price_shipping_method_errors(
@@ -633,7 +715,7 @@ def test_create_price_shipping_method_errors(
         "name": "DHL",
         "price": 12.34,
         "minimumOrderPrice": 20,
-        "maximumOrderPrice": 15,
+        "maximumOrderPrice": 10,
         "type": ShippingMethodTypeEnum.PRICE.name,
     }
     response = staff_api_client.post_graphql(
@@ -646,7 +728,7 @@ def test_create_price_shipping_method_errors(
 
 WEIGHT_BASED_SHIPPING_QUERY = """
     mutation createShippingPrice(
-        $type: ShippingMethodTypeEnum, $name: String!, $price: Decimal,
+        $type: ShippingMethodTypeEnum, $name: String!, $price: PositiveDecimal,
         $shippingZone: ID!, $maximumOrderWeight: WeightScalar,
         $minimumOrderWeight: WeightScalar) {
         shippingPriceCreate(
@@ -783,8 +865,8 @@ def test_update_shipping_method(
 ):
     query = """
     mutation updateShippingPrice(
-        $id: ID!, $price: Decimal, $shippingZone: ID!,
-        $type: ShippingMethodTypeEnum!, $minimumOrderPrice: Decimal) {
+        $id: ID!, $price: PositiveDecimal, $shippingZone: ID!,
+        $type: ShippingMethodTypeEnum!, $minimumOrderPrice: PositiveDecimal) {
         shippingPriceUpdate(
             id: $id, input: {
                 price: $price, shippingZone: $shippingZone,
diff --git a/saleor/graphql/tests/utils.py b/saleor/graphql/tests/utils.py
index 7bc4aabaac3..a6122dfc824 100644
--- a/saleor/graphql/tests/utils.py
+++ b/saleor/graphql/tests/utils.py
@@ -26,6 +26,14 @@ def assert_no_permission(response):
     ), content["errors"]
 
 
+def assert_negative_positive_decimal_value(response):
+    content = _get_graphql_content_from_response(response)
+    assert "errors" in content, content
+    assert "Value cannot be lower than 0." in content["errors"][0]["message"], content[
+        "errors"
+    ]
+
+
 def get_multipart_request_body(query, variables, file, file_name):
     """Create request body for multipart GraphQL requests.
 
diff --git a/saleor/order/migrations/0089_auto_20200902_1249.py b/saleor/order/migrations/0089_auto_20200902_1249.py
new file mode 100644
index 00000000000..2812c487105
--- /dev/null
+++ b/saleor/order/migrations/0089_auto_20200902_1249.py
@@ -0,0 +1,52 @@
+# Generated by Django 3.1 on 2020-09-02 12:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("order", "0088_auto_20200812_1101"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="order",
+            name="discount_amount",
+            field=models.DecimalField(decimal_places=3, default=0, max_digits=12),
+        ),
+        migrations.AlterField(
+            model_name="order",
+            name="shipping_price_gross_amount",
+            field=models.DecimalField(
+                decimal_places=3, default=0, editable=False, max_digits=12
+            ),
+        ),
+        migrations.AlterField(
+            model_name="order",
+            name="shipping_price_net_amount",
+            field=models.DecimalField(
+                decimal_places=3, default=0, editable=False, max_digits=12
+            ),
+        ),
+        migrations.AlterField(
+            model_name="order",
+            name="total_gross_amount",
+            field=models.DecimalField(decimal_places=3, default=0, max_digits=12),
+        ),
+        migrations.AlterField(
+            model_name="order",
+            name="total_net_amount",
+            field=models.DecimalField(decimal_places=3, default=0, max_digits=12),
+        ),
+        migrations.AlterField(
+            model_name="orderline",
+            name="unit_price_gross_amount",
+            field=models.DecimalField(decimal_places=3, max_digits=12),
+        ),
+        migrations.AlterField(
+            model_name="orderline",
+            name="unit_price_net_amount",
+            field=models.DecimalField(decimal_places=3, max_digits=12),
+        ),
+    ]
diff --git a/saleor/payment/migrations/0020_auto_20200902_1249.py b/saleor/payment/migrations/0020_auto_20200902_1249.py
new file mode 100644
index 00000000000..5d22ff5743d
--- /dev/null
+++ b/saleor/payment/migrations/0020_auto_20200902_1249.py
@@ -0,0 +1,36 @@
+# Generated by Django 3.1 on 2020-09-02 12:49
+
+from decimal import Decimal
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("payment", "0019_auto_20200812_1101"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="payment",
+            name="captured_amount",
+            field=models.DecimalField(
+                decimal_places=3, default=Decimal("0.0"), max_digits=12
+            ),
+        ),
+        migrations.AlterField(
+            model_name="payment",
+            name="total",
+            field=models.DecimalField(
+                decimal_places=3, default=Decimal("0.0"), max_digits=12
+            ),
+        ),
+        migrations.AlterField(
+            model_name="transaction",
+            name="amount",
+            field=models.DecimalField(
+                decimal_places=3, default=Decimal("0.0"), max_digits=12
+            ),
+        ),
+    ]
diff --git a/saleor/product/migrations/0123_auto_20200904_0529.py b/saleor/product/migrations/0123_auto_20200904_0529.py
new file mode 100644
index 00000000000..605c7c1b0a6
--- /dev/null
+++ b/saleor/product/migrations/0123_auto_20200904_0529.py
@@ -0,0 +1,32 @@
+# Generated by Django 3.1 on 2020-09-04 05:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("product", "0122_auto_20200828_1135"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="product",
+            name="minimal_variant_price_amount",
+            field=models.DecimalField(
+                blank=True, decimal_places=3, max_digits=12, null=True
+            ),
+        ),
+        migrations.AlterField(
+            model_name="productvariant",
+            name="cost_price_amount",
+            field=models.DecimalField(
+                blank=True, decimal_places=3, max_digits=12, null=True
+            ),
+        ),
+        migrations.AlterField(
+            model_name="productvariant",
+            name="price_amount",
+            field=models.DecimalField(decimal_places=3, max_digits=12),
+        ),
+    ]
diff --git a/saleor/product/tests/test_product_minimal_variant_price.py b/saleor/product/tests/test_product_minimal_variant_price.py
index deebbc174ff..09a98f8d4e1 100644
--- a/saleor/product/tests/test_product_minimal_variant_price.py
+++ b/saleor/product/tests/test_product_minimal_variant_price.py
@@ -81,7 +81,7 @@ def test_product_update_updates_minimal_variant_price(
     query = """
         mutation ProductUpdate(
             $productId: ID!,
-            $basePrice: Decimal!,
+            $basePrice: PositiveDecimal!,
         ) {
             productUpdate(
                 id: $productId
diff --git a/saleor/settings.py b/saleor/settings.py
index 381d2699fa0..afd42fe47c0 100644
--- a/saleor/settings.py
+++ b/saleor/settings.py
@@ -12,7 +12,6 @@
 import sentry_sdk
 from django.core.exceptions import ImproperlyConfigured
 from django.core.management.utils import get_random_secret_key
-from django_prices.utils.formatting import get_currency_fraction
 from pytimeparse import parse
 from sentry_sdk.integrations.celery import CeleryIntegration
 from sentry_sdk.integrations.django import DjangoIntegration
@@ -355,7 +354,7 @@ def get_bool_from_env(name, default_value):
 
 DEFAULT_COUNTRY = os.environ.get("DEFAULT_COUNTRY", "US")
 DEFAULT_CURRENCY = os.environ.get("DEFAULT_CURRENCY", "USD")
-DEFAULT_DECIMAL_PLACES = get_currency_fraction(DEFAULT_CURRENCY)
+DEFAULT_DECIMAL_PLACES = 3
 DEFAULT_MAX_DIGITS = 12
 DEFAULT_CURRENCY_CODE_LENGTH = 3
 
diff --git a/saleor/shipping/migrations/0020_auto_20200902_1249.py b/saleor/shipping/migrations/0020_auto_20200902_1249.py
new file mode 100644
index 00000000000..7580e29f51d
--- /dev/null
+++ b/saleor/shipping/migrations/0020_auto_20200902_1249.py
@@ -0,0 +1,32 @@
+# Generated by Django 3.1 on 2020-09-02 12:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("shipping", "0019_remove_shippingmethod_meta"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="shippingmethod",
+            name="maximum_order_price_amount",
+            field=models.DecimalField(
+                blank=True, decimal_places=3, max_digits=12, null=True
+            ),
+        ),
+        migrations.AlterField(
+            model_name="shippingmethod",
+            name="minimum_order_price_amount",
+            field=models.DecimalField(
+                blank=True, decimal_places=3, default=0, max_digits=12, null=True
+            ),
+        ),
+        migrations.AlterField(
+            model_name="shippingmethod",
+            name="price_amount",
+            field=models.DecimalField(decimal_places=3, default=0, max_digits=12),
+        ),
+    ]
diff --git a/saleor/tests/fixtures.py b/saleor/tests/fixtures.py
index b43560459cf..a21531f64ce 100644
--- a/saleor/tests/fixtures.py
+++ b/saleor/tests/fixtures.py
@@ -767,7 +767,7 @@ def product(product_type, category, warehouse):
     product = Product.objects.create(
         name="Test product",
         slug="test-product-11",
-        minimal_variant_price_amount="10.00",
+        minimal_variant_price_amount=Decimal(10),
         product_type=product_type,
         category=category,
         is_published=True,
diff --git a/saleor/webhook/tests/test_webhook_sample_payloads.py b/saleor/webhook/tests/test_webhook_sample_payloads.py
index 4852ec32927..09614d39783 100644
--- a/saleor/webhook/tests/test_webhook_sample_payloads.py
+++ b/saleor/webhook/tests/test_webhook_sample_payloads.py
@@ -128,6 +128,8 @@ def test_generate_sample_customer_payload(customer_user):
 
 def test_generate_sample_product_payload(variant):
     payload = generate_sample_payload(WebhookEventType.PRODUCT_CREATED)
+    product = variant.product
+    product.refresh_from_db()
     assert payload == json.loads(generate_product_payload(variant.product))
 
 


Code review comments:
diff_hunk: @@ -168,22 +168,10 @@ def clean_variant_input(
 
         cost_price_amount = cleaned_input.pop("cost_price", None)
         if cost_price_amount is not None:
-            if cost_price_amount < 0:
-                errors["costPrice"] = ValidationError(
-                    "Product price cannot be lower than 0.",
-                    code=ProductErrorCode.INVALID.value,
-                    params={"index": variant_index},
-                )
             cleaned_input["cost_price_amount"] = cost_price_amount
 
         price_amount = cleaned_input.pop("price", None)
         if price_amount is not None:
-            if price_amount < 0:
path: saleor/graphql/product/bulk_mutations/products.py
text: @user1:
Should we add `validate_price_amount` here?

@author:
Done

diff_hunk: @@ -10,3 +18,22 @@ def validate_one_of_args_is_in_query(arg1_name, arg1, arg2_name, arg2):
         raise GraphQLError(
             f"Either '{arg1_name}'  or '{arg2_name}' argument is required"
         )
+
+
+def validate_price_amount(value: Optional["Decimal"], currency: str = None):
path: saleor/graphql/core/validators.py
text: @user1:
Maby `validate_price_precision`?