Repository: saleor/saleor
Pull request: Create order discounts for all voucher types
Create order discounts for all voucher types. 

Fixes https://github.com/saleor/saleor/issues/12109, https://github.com/saleor/saleor/issues/8910

<!-- Please mention all relevant issue numbers. -->

# Impact

* [ ] New migrations
* [ ] 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
* [ ] Database migration files are up to date
* [ ] The changes are tested
* [ ] GraphQL schema and type definitions are up to date
* [ ] Changes are mentioned in the changelog


Problem statement:
Bug: Missing discount info for orders with applied specific products and "once per order" vouchers.
### What are you trying to achieve?

As the specific product, and apply "once per order" vouchers are now included in the checkout discount, we should also create the appropriate `discount` object for the order when checkout is completed. It allows for properly showing the information about the discount in the dashboard.

### Steps to reproduce the problem

1. Create the specific product voucher
2. Create the checkout with the product that is included in the discount
3. Attach the voucher code to checkout
4. Complete the checkout
5. Check the order discounts - it will be empty 

### What did you expect to happen?

Attach discount information to the order about applied specific products and "once per order" vouchers.

### Logs

_No response_

### Environment

Saleor version:3.9

Hints from issue discussion:

Commit message: Update changelog

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5852b9f23de..432d468fc87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ All notable, unreleased changes to this project will be documented in this file.
 - [Preview] Add `StockBulkUpdate` mutation - #12139 by @SzymJ
 
 ### Other changes
+- Create order discounts for all voucher types - #12272 by @IKarbowiak
 
 # 3.12.0
 
diff --git a/saleor/checkout/complete_checkout.py b/saleor/checkout/complete_checkout.py
index 50b9eeedda0..2b761cd1faf 100644
--- a/saleor/checkout/complete_checkout.py
+++ b/saleor/checkout/complete_checkout.py
@@ -31,7 +31,7 @@
 from ..core.tracing import traced_atomic_transaction
 from ..core.transactions import transaction_with_commit_on_errors
 from ..core.utils.url import validate_storefront_url
-from ..discount import DiscountInfo, DiscountValueType, OrderDiscountType, VoucherType
+from ..discount import DiscountInfo, DiscountValueType, OrderDiscountType
 from ..discount.models import NotApplicable
 from ..discount.utils import (
     add_voucher_usage_by_customer,
@@ -84,7 +84,6 @@
 
 if TYPE_CHECKING:
     from ..app.models import App
-    from ..discount.models import Voucher
     from ..plugins.manager import PluginsManager
     from ..site.models import SiteSettings
 
@@ -553,23 +552,15 @@ def _create_order(
         # store voucher as a fixed value as it this the simplest solution for now.
         # This will be solved when we refactor the voucher logic to use .discounts
         # relations
-        voucher = order_data.get("voucher")
-        # When we have a voucher for specific products we track it directly in the
-        # Orderline. Voucher with 'apply_once_per_order' is handled in the same way
-        # as we apply it only for single quantity of the cheapest item.
-        if not voucher or (
-            voucher.type != VoucherType.SPECIFIC_PRODUCT
-            and not voucher.apply_once_per_order
-        ):
-            order.discounts.create(
-                type=OrderDiscountType.VOUCHER,
-                value_type=DiscountValueType.FIXED,
-                value=checkout.discount.amount,
-                name=checkout.discount_name,
-                translated_name=checkout.translated_discount_name,
-                currency=checkout.currency,
-                amount_value=checkout.discount_amount,
-            )
+        order.discounts.create(
+            type=OrderDiscountType.VOUCHER,
+            value_type=DiscountValueType.FIXED,
+            value=checkout.discount.amount,
+            name=checkout.discount_name,
+            translated_name=checkout.translated_discount_name,
+            currency=checkout.currency,
+            amount_value=checkout.discount_amount,
+        )
 
     order_lines = []
     for line_info in order_lines_info:
@@ -980,30 +971,20 @@ def _handle_allocations_of_order_lines(
     )
 
 
-def _handle_checkout_discount(
-    order: "Order", checkout: "Checkout", voucher: Optional["Voucher"]
-):
+def _handle_checkout_discount(order: "Order", checkout: "Checkout"):
     if checkout.discount:
         # store voucher as a fixed value as it this the simplest solution for now.
         # This will be solved when we refactor the voucher logic to use .discounts
         # relations
-
-        # When we have a voucher for specific products we track it directly in the
-        # Orderline. Voucher with 'apply_once_per_order' is handled in the same way
-        # as we apply it only for single quantity of the cheapest item.
-        if not voucher or (
-            voucher.type != VoucherType.SPECIFIC_PRODUCT
-            and not voucher.apply_once_per_order
-        ):
-            order.discounts.create(
-                type=OrderDiscountType.VOUCHER,
-                value_type=DiscountValueType.FIXED,
-                value=checkout.discount.amount,
-                name=checkout.discount_name,
-                translated_name=checkout.translated_discount_name,
-                currency=checkout.currency,
-                amount_value=checkout.discount_amount,
-            )
+        order.discounts.create(
+            type=OrderDiscountType.VOUCHER,
+            value_type=DiscountValueType.FIXED,
+            value=checkout.discount.amount,
+            name=checkout.discount_name,
+            translated_name=checkout.translated_discount_name,
+            currency=checkout.currency,
+            amount_value=checkout.discount_amount,
+        )
 
 
 def _post_create_order_actions(
@@ -1140,7 +1121,7 @@ def _create_order_from_checkout(
     )
 
     # checkout discount
-    _handle_checkout_discount(order, checkout_info.checkout, voucher)
+    _handle_checkout_discount(order, checkout_info.checkout)
 
     # lines
     order_lines_info = _create_order_lines_from_checkout_lines(
diff --git a/saleor/graphql/checkout/tests/mutations/test_checkout_complete.py b/saleor/graphql/checkout/tests/mutations/test_checkout_complete.py
index 3e3f1313e55..a33b91092e5 100644
--- a/saleor/graphql/checkout/tests/mutations/test_checkout_complete.py
+++ b/saleor/graphql/checkout/tests/mutations/test_checkout_complete.py
@@ -19,6 +19,7 @@
 from .....checkout.models import Checkout
 from .....core.exceptions import InsufficientStock, InsufficientStockData
 from .....core.taxes import TaxError, zero_money, zero_taxed_money
+from .....discount import OrderDiscountType
 from .....giftcard import GiftCardEvents
 from .....giftcard.models import GiftCard, GiftCardEvent
 from .....order import OrderOrigin, OrderStatus
@@ -921,6 +922,116 @@ def test_checkout_with_voucher_complete(
 
     voucher_percentage.refresh_from_db()
     assert voucher_percentage.used == voucher_used_count + 1
+    order_discount = order.discounts.filter(type=OrderDiscountType.VOUCHER).first()
+    assert order_discount
+    assert (
+        order_discount.amount_value
+        == (order.undiscounted_total - order.total).gross.amount
+    )
+
+    assert not Checkout.objects.filter(
+        pk=checkout.pk
+    ).exists(), "Checkout should have been deleted"
+
+
+@pytest.mark.integration
+def test_checkout_complete_with_voucher_apply_once_per_customer(
+    user_api_client,
+    checkout_with_voucher_percentage,
+    voucher_percentage,
+    payment_dummy,
+    address,
+    shipping_method,
+):
+    # given
+    voucher_used_count = voucher_percentage.used
+    voucher_percentage.usage_limit = voucher_used_count + 1
+    voucher_percentage.apply_once_per_order = True
+    voucher_percentage.save(update_fields=["usage_limit", "apply_once_per_order"])
+
+    checkout = checkout_with_voucher_percentage
+
+    checkout_line = checkout.lines.first()
+    checkout_line_quantity = checkout_line.quantity
+    checkout_line_variant = checkout_line.variant
+
+    checkout.shipping_address = address
+    checkout.shipping_method = shipping_method
+    checkout.billing_address = address
+    checkout.metadata_storage.store_value_in_metadata(items={"accepted": "true"})
+    checkout.metadata_storage.store_value_in_private_metadata(
+        items={"accepted": "false"}
+    )
+    discount_amount = checkout_line_variant.channel_listings.get(
+        channel=checkout.channel
+    ).price * (
+        voucher_percentage.channel_listings.get(channel=checkout.channel).discount_value
+        / 100
+    )
+    checkout.discount = discount_amount
+    checkout.save()
+    checkout.metadata_storage.save()
+
+    manager = get_plugins_manager()
+    lines, _ = fetch_checkout_lines(checkout)
+    checkout_info = fetch_checkout_info(checkout, lines, [], manager)
+
+    total = calculations.checkout_total(
+        manager=manager, checkout_info=checkout_info, lines=lines, address=address
+    )
+    payment = payment_dummy
+    payment.is_active = True
+    payment.order = None
+    payment.total = total.gross.amount
+    payment.currency = total.gross.currency
+    payment.checkout = checkout
+    payment.save()
+    assert not payment.transactions.exists()
+
+    orders_count = Order.objects.count()
+    variables = {
+        "id": to_global_id_or_none(checkout),
+        "redirectUrl": "https://www.example.com",
+    }
+
+    # when
+    response = user_api_client.post_graphql(MUTATION_CHECKOUT_COMPLETE, variables)
+
+    # then
+    content = get_graphql_content(response)
+    data = content["data"]["checkoutComplete"]
+    assert not data["errors"]
+
+    order_token = data["order"]["token"]
+    order_id = data["order"]["id"]
+    assert Order.objects.count() == orders_count + 1
+    order = Order.objects.first()
+    assert str(order.id) == order_token
+    assert order_id == graphene.Node.to_global_id("Order", order.id)
+    assert order.metadata == checkout.metadata_storage.metadata
+    assert order.private_metadata == checkout.metadata_storage.private_metadata
+
+    assert order.total == total
+    assert order.undiscounted_total == total + discount_amount
+
+    order_line = order.lines.first()
+    assert checkout_line_quantity == order_line.quantity
+    assert checkout_line_variant == order_line.variant
+    assert order.shipping_address == address
+    assert order.shipping_method == checkout.shipping_method
+    assert order.payments.exists()
+    order_payment = order.payments.first()
+    assert order_payment == payment
+    assert payment.transactions.count() == 1
+
+    voucher_percentage.refresh_from_db()
+    assert voucher_percentage.used == voucher_used_count + 1
+    order_discount = order.discounts.filter(type=OrderDiscountType.VOUCHER).first()
+    assert order_discount
+    assert (
+        order_discount.amount_value
+        == (order.undiscounted_total - order.total).gross.amount
+    )
 
     assert not Checkout.objects.filter(
         pk=checkout.pk
@@ -1108,6 +1219,13 @@ def test_checkout_with_voucher_on_specific_product_complete(
     assert order_payment == payment
     assert payment.transactions.count() == 1
 
+    order_discount = order.discounts.filter(type=OrderDiscountType.VOUCHER).first()
+    assert order_discount
+    assert (
+        order_discount.amount_value
+        == (order.undiscounted_total - order.total).gross.amount
+    )
+
     voucher_specific_product_type.refresh_from_db()
     assert voucher_specific_product_type.used == voucher_used_count + 1
 


Code review comments:
diff_hunk: @@ -553,23 +552,15 @@ def _create_order(
         # store voucher as a fixed value as it this the simplest solution for now.
         # This will be solved when we refactor the voucher logic to use .discounts
         # relations
-        voucher = order_data.get("voucher")
-        # When we have a voucher for specific products we track it directly in the
-        # Orderline. Voucher with 'apply_once_per_order' is handled in the same way
-        # as we apply it only for single quantity of the cheapest item.
-        if not voucher or (
-            voucher.type != VoucherType.SPECIFIC_PRODUCT
-            and not voucher.apply_once_per_order
-        ):
-            order.discounts.create(
-                type=OrderDiscountType.VOUCHER,
-                value_type=DiscountValueType.FIXED,
-                value=checkout.discount.amount,
-                name=checkout.discount_name,
-                translated_name=checkout.translated_discount_name,
-                currency=checkout.currency,
-                amount_value=checkout.discount_amount,
-            )
+        order.discounts.create(
+            type=OrderDiscountType.VOUCHER,
+            value_type=DiscountValueType.FIXED,
+            value=checkout.discount.amount,
+            name=checkout.discount_name,
+            translated_name=checkout.translated_discount_name,
+            currency=checkout.currency,
+            amount_value=checkout.discount_amount,
+        )
path: saleor/checkout/complete_checkout.py
text: @user1:
This could be replaced along with the `if checkout.discount:` from line 551 to reuse the utility function and prevent the duplicated code, wdyt?

```suggestion
    _handle_checkout_discount(order, checkout)
```

@author:
Good point. Thanks! 👍