Repository: saleor/saleor

Issue:
Voucher one per order doesn't work
The once per user option doesn't work on percentage discounts. It always gives percentage of the whole order value. But it works for dollar amounts.


Discussion:

Resolving patch:
diff --git a/saleor/discount/utils.py b/saleor/discount/utils.py
--- a/saleor/discount/utils.py
+++ b/saleor/discount/utils.py
@@ -5,7 +5,7 @@
 from django.db.models import F
 from django.utils.translation import pgettext
 
-from ..core.utils.taxes import ZERO_MONEY, ZERO_TAXED_MONEY
+from ..core.utils.taxes import ZERO_MONEY
 from . import DiscountInfo
 from .models import NotApplicable, Sale
 
@@ -76,8 +76,7 @@ def get_shipping_voucher_discount(voucher, total_price, shipping_price):
 def get_products_voucher_discount(voucher, prices):
     """Calculate discount value for a voucher of product or category type."""
     if voucher.apply_once_per_order:
-        product_total = sum(prices, ZERO_TAXED_MONEY)
-        return voucher.get_discount_amount_for(product_total)
+        return voucher.get_discount_amount_for(min(prices))
     discounts = (voucher.get_discount_amount_for(price) for price in prices)
     total_amount = sum(discounts, ZERO_MONEY)
     return total_amount
diff --git a/saleor/graphql/discount/mutations.py b/saleor/graphql/discount/mutations.py
--- a/saleor/graphql/discount/mutations.py
+++ b/saleor/graphql/discount/mutations.py
@@ -100,6 +100,9 @@ class VoucherInput(graphene.InputObjectType):
         graphene.String,
         description="Country codes that can be used with the shipping voucher",
     )
+    apply_once_per_order = graphene.Boolean(
+        description="Voucher should be applied to the cheapest item or entire order"
+    )
 
 
 class VoucherCreate(ModelMutation):


Test patch:
diff --git a/tests/api/test_discount.py b/tests/api/test_discount.py
--- a/tests/api/test_discount.py
+++ b/tests/api/test_discount.py
@@ -3,11 +3,11 @@
 import graphene
 import pytest
 from django_countries import countries
-from tests.api.utils import get_graphql_content
 
 from saleor.discount import DiscountValueType, VoucherType
 from saleor.discount.models import Sale, Voucher
 from saleor.graphql.discount.enums import DiscountValueTypeEnum, VoucherTypeEnum
+from tests.api.utils import get_graphql_content
 
 
 @pytest.fixture
@@ -139,13 +139,14 @@ def test_sale_query(staff_api_client, sale, permission_manage_discounts):
     $type: VoucherTypeEnum, $name: String, $code: String,
     $discountValueType: DiscountValueTypeEnum,
     $discountValue: Decimal, $minAmountSpent: Decimal,
-    $startDate: Date, $endDate: Date) {
+    $startDate: Date, $endDate: Date, $applyOncePerOrder: Boolean) {
         voucherCreate(input: {
                 name: $name, type: $type, code: $code,
                 discountValueType: $discountValueType,
                 discountValue: $discountValue,
                 minAmountSpent: $minAmountSpent,
-                startDate: $startDate, endDate: $endDate}) {
+                startDate: $startDate, endDate: $endDate,
+                applyOncePerOrder: $applyOncePerOrder}) {
             errors {
                 field
                 message
@@ -160,6 +161,7 @@ def test_sale_query(staff_api_client, sale, permission_manage_discounts):
                 discountValueType
                 startDate
                 endDate
+                applyOncePerOrder
             }
         }
     }
@@ -178,6 +180,7 @@ def test_create_voucher(staff_api_client, permission_manage_discounts):
         "minAmountSpent": 1.12,
         "startDate": start_date.isoformat(),
         "endDate": end_date.isoformat(),
+        "applyOncePerOrder": True,
     }
 
     response = staff_api_client.post_graphql(
@@ -192,6 +195,7 @@ def test_create_voucher(staff_api_client, permission_manage_discounts):
     assert data["discountValueType"] == DiscountValueType.FIXED.upper()
     assert data["startDate"] == start_date.isoformat()
     assert data["endDate"] == end_date.isoformat()
+    assert data["applyOncePerOrder"]
 
 
 def test_create_voucher_with_empty_code(staff_api_client, permission_manage_discounts):
@@ -271,9 +275,12 @@ def test_create_voucher_with_existing_voucher_code(
 def test_update_voucher(staff_api_client, voucher, permission_manage_discounts):
     query = """
     mutation  voucherUpdate($code: String,
-        $discountValueType: DiscountValueTypeEnum, $id: ID!) {
+        $discountValueType: DiscountValueTypeEnum, $id: ID!,
+        $applyOncePerOrder: Boolean) {
             voucherUpdate(id: $id, input: {
-                code: $code, discountValueType: $discountValueType}) {
+                code: $code, discountValueType: $discountValueType,
+                applyOncePerOrder: $applyOncePerOrder
+                }) {
                 errors {
                     field
                     message
@@ -281,10 +288,12 @@ def test_update_voucher(staff_api_client, voucher, permission_manage_discounts):
                 voucher {
                     code
                     discountValueType
+                    applyOncePerOrder
                 }
             }
         }
     """
+    apply_once_per_order = not voucher.apply_once_per_order
     # Set discount value type to 'fixed' and change it in mutation
     voucher.discount_value_type = DiscountValueType.FIXED
     voucher.save()
@@ -293,6 +302,7 @@ def test_update_voucher(staff_api_client, voucher, permission_manage_discounts):
         "id": graphene.Node.to_global_id("Voucher", voucher.id),
         "code": "testcode123",
         "discountValueType": DiscountValueTypeEnum.PERCENTAGE.name,
+        "applyOncePerOrder": apply_once_per_order,
     }
 
     response = staff_api_client.post_graphql(
@@ -302,6 +312,7 @@ def test_update_voucher(staff_api_client, voucher, permission_manage_discounts):
     data = content["data"]["voucherUpdate"]["voucher"]
     assert data["code"] == "testcode123"
     assert data["discountValueType"] == DiscountValueType.PERCENTAGE.upper()
+    assert data["applyOncePerOrder"] == apply_once_per_order
 
 
 def test_voucher_delete_mutation(
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,4 @@
+import uuid
 from io import BytesIO
 from unittest.mock import MagicMock, Mock
 
@@ -93,6 +94,16 @@ def checkout_with_item(checkout, product):
     return checkout
 
 
+@pytest.fixture
+def checkout_with_items(checkout, product_list, product):
+    variant = product.variants.get()
+    add_variant_to_checkout(checkout, variant, 1)
+    for prod in product_list:
+        variant = prod.variants.get()
+        add_variant_to_checkout(checkout, variant, 1)
+    return checkout
+
+
 @pytest.fixture
 def checkout_with_voucher(checkout, product, voucher):
     variant = product.variants.get()
@@ -508,6 +519,28 @@ def product_list(product_type, category):
             ),
         ]
     )
+    ProductVariant.objects.bulk_create(
+        [
+            ProductVariant(
+                product=products[0],
+                sku=str(uuid.uuid4()).replace("-", ""),
+                track_inventory=True,
+                quantity=100,
+            ),
+            ProductVariant(
+                product=products[1],
+                sku=str(uuid.uuid4()).replace("-", ""),
+                track_inventory=True,
+                quantity=100,
+            ),
+            ProductVariant(
+                product=products[2],
+                sku=str(uuid.uuid4()).replace("-", ""),
+                track_inventory=True,
+                quantity=100,
+            ),
+        ]
+    )
     return products
 
 
diff --git a/tests/test_checkout.py b/tests/test_checkout.py
--- a/tests/test_checkout.py
+++ b/tests/test_checkout.py
@@ -849,6 +849,37 @@ def test_get_discount_for_checkout_value_voucher(
     assert discount == Money(discount_amount, "USD")
 
 
+@pytest.mark.parametrize(
+    "discount_value, discount_type, apply_once_per_order, discount_amount",
+    [
+        (5, DiscountValueType.FIXED, True, 5),
+        (5, DiscountValueType.FIXED, False, 15),
+        (10000, DiscountValueType.FIXED, True, 10),
+        (10, DiscountValueType.PERCENTAGE, True, 1),
+        (10, DiscountValueType.PERCENTAGE, False, 5),
+    ],
+)
+def test_get_discount_for_checkout_apply_once_per_order(
+    checkout_with_items,
+    product_list,
+    discount_value,
+    discount_type,
+    apply_once_per_order,
+    discount_amount,
+):
+    voucher = Voucher.objects.create(
+        code="unique",
+        type=VoucherType.PRODUCT,
+        discount_value_type=discount_type,
+        discount_value=discount_value,
+        apply_once_per_order=apply_once_per_order,
+    )
+    for product in product_list:
+        voucher.products.add(product)
+    discount = get_voucher_discount_for_checkout(voucher, checkout_with_items)
+    assert discount == Money(discount_amount, "USD")
+
+
 def test_get_discount_for_checkout_value_voucher_not_applicable():
     voucher = Voucher(
         code="unique",
diff --git a/tests/test_discounts.py b/tests/test_discounts.py
--- a/tests/test_discounts.py
+++ b/tests/test_discounts.py
@@ -1,8 +1,7 @@
 from datetime import date, timedelta
 
-from prices import Money, TaxedMoney
-
 import pytest
+from prices import Money, TaxedMoney
 
 from saleor.checkout.utils import get_voucher_discount_for_checkout
 from saleor.discount import DiscountInfo, DiscountValueType, VoucherType
@@ -89,8 +88,8 @@ def test_voucher_queryset_active(voucher):
     [
         ([10], 10, DiscountValueType.FIXED, True, 10),
         ([5], 10, DiscountValueType.FIXED, True, 5),
-        ([5, 5], 10, DiscountValueType.FIXED, True, 10),
-        ([2, 3], 10, DiscountValueType.FIXED, True, 5),
+        ([5, 5], 10, DiscountValueType.FIXED, True, 5),
+        ([2, 3], 10, DiscountValueType.FIXED, True, 2),
         ([10, 10], 5, DiscountValueType.FIXED, False, 10),
         ([5, 2], 5, DiscountValueType.FIXED, False, 7),
         ([10, 10, 10], 5, DiscountValueType.FIXED, False, 15),
@@ -244,8 +243,8 @@ def test_get_shipping_voucher_discount(
     "prices, discount_value_type, discount_value, voucher_type, expected_value",
     [  # noqa
         ([5, 10, 15], DiscountValueType.PERCENTAGE, 10, VoucherType.PRODUCT, 3),
-        ([5, 10, 15], DiscountValueType.FIXED, 2, VoucherType.PRODUCT, 2),
-        ([5, 10, 15], DiscountValueType.FIXED, 2, VoucherType.COLLECTION, 2),
+        ([5, 10, 15], DiscountValueType.FIXED, 2, VoucherType.PRODUCT, 6),
+        ([5, 10, 15], DiscountValueType.FIXED, 2, VoucherType.COLLECTION, 6),
     ],
 )
 def test_get_voucher_discount_all_products(
@@ -260,7 +259,6 @@ def test_get_voucher_discount_all_products(
         type=voucher_type,
         discount_value_type=discount_value_type,
         discount_value=discount_value,
-        apply_once_per_order=True,
     )
     voucher.save()
     discount = get_products_voucher_discount(voucher, prices)