Repository: saleor/saleor
Pull request: Fix `OrderDiscount.type` when converting checkout to order.
I want to merge this change because it fixes https://github.com/saleor/saleor/issues/15233.

<!-- 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

# Docs

<!-- Docs are stored in a separate repository: https://github.com/saleor/saleor-docs/. -->
<!-- Please provide a link to the PR that updates documentation for your changes. -->
<!-- If changes in docs are not required, please mention that in the description. -->

- [ ] Link to documentation:

# Pull Request Checklist

<!-- Please keep this section. It will make the maintainer's life easier. -->

- [ ] Privileged queries and mutations are either absent or guarded by proper permission checks
- [ ] Database queries are optimized and the number of queries is constant
- [ ] Database migrations are either absent or optimized for zero downtime
- [ ] The changes are covered by test cases
- [ ] All new fields/inputs/mutations have proper labels added (`ADDED_IN_X`, `PREVIEW_FEATURE`, etc.)
- [ ] All migrations have proper dependencies
- [ ] All indexes are added concurrently in migrations
- [ ] All RunSql and RunPython migrations have revert option defined


Problem statement:
Bug: `CheckoutDiscount.type` is always set to `VOUCHER`
### What are you trying to achieve?

During `completeCheckout` mutation, when discount derives from promotion, I want to see the proper `CheckoutDiscount.type` ('promotion') 

### Steps to reproduce the problem

1. Fetch `test_promotion_applied_on_order_with_specified_total` branch.
2. Run `test_promotion_applied_on_checkout_with_specific_subtotal_CORE_2133`
3. Place breakpoint at the end of the test
4. Get `Order.objects.first().discounts.first().type`
5. It is `voucher` instead of `promotion`

### What did you expect to happen?

`CheckoutDiscount.type` should be set to 'promotion'

### Logs

_No response_

### Environment

Saleor version: …
OS and version: …

Hints from issue discussion:

Commit message: Fix linter.

Patch to review:
diff --git a/saleor/checkout/complete_checkout.py b/saleor/checkout/complete_checkout.py
index 7928ab92369..db4cf2e21c8 100644
--- a/saleor/checkout/complete_checkout.py
+++ b/saleor/checkout/complete_checkout.py
@@ -576,7 +576,7 @@ def _create_order(
         tax_exemption=checkout_info.checkout.tax_exemption,
     )
 
-    _handle_checkout_discount(order, checkout)
+    _create_order_discount(order, checkout_info)
 
     order_lines: list[OrderLine] = []
     order_line_discounts: list[OrderLineDiscount] = []
@@ -1053,21 +1053,39 @@ def _handle_allocations_of_order_lines(
     )
 
 
-def _handle_checkout_discount(order: "Order", checkout: "Checkout"):
+def _create_order_discount(order: "Order", checkout_info: "CheckoutInfo"):
+    checkout = checkout_info.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
-
-        order.discounts.create(
-            type=DiscountType.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,
-        )
+        checkout_discount = checkout.discounts.first()
+        if checkout_discount and checkout_discount.type == DiscountType.ORDER_PROMOTION:
+            order.discounts.create(
+                type=checkout_discount.type,
+                value_type=checkout_discount.value_type,
+                value=checkout_discount.value,
+                name=checkout_discount.name,
+                translated_name=checkout_discount.translated_name,
+                currency=checkout_discount.currency,
+                amount_value=checkout_discount.amount_value,
+                promotion_rule=checkout_discount.promotion_rule,
+            )
+
+        if not 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
+            order.discounts.create(
+                type=DiscountType.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,
+                voucher=checkout_info.voucher,
+                voucher_code=checkout_info.voucher_code.code
+                if checkout_info.voucher_code
+                else None,
+            )
 
 
 def _post_create_order_actions(
@@ -1199,7 +1217,7 @@ def _create_order_from_checkout(
     )
 
     # checkout discount
-    _handle_checkout_discount(order, checkout_info.checkout)
+    _create_order_discount(order, checkout_info)
 
     # lines
     order_lines_info = _create_order_lines_from_checkout_lines(
diff --git a/saleor/graphql/checkout/tests/mutations/test_order_create_from_checkout.py b/saleor/graphql/checkout/tests/mutations/test_order_create_from_checkout.py
index 471da32c67a..45f95d069e4 100644
--- a/saleor/graphql/checkout/tests/mutations/test_order_create_from_checkout.py
+++ b/saleor/graphql/checkout/tests/mutations/test_order_create_from_checkout.py
@@ -770,6 +770,9 @@ def test_order_from_checkout_with_voucher(
         order_discount.amount_value
         == (order.undiscounted_total - order.total).gross.amount
     )
+    assert order_discount.type == DiscountType.VOUCHER
+    assert order_discount.voucher == voucher_percentage
+    assert order_discount.voucher_code == code.code
 
     code.refresh_from_db()
     assert code.used == voucher_used_count + 1
@@ -847,6 +850,9 @@ def test_order_from_checkout_with_voucher_apply_once_per_order(
         order_discount.amount_value
         == (order.undiscounted_total - order.total).gross.amount
     )
+    assert order_discount.type == DiscountType.VOUCHER
+    assert order_discount.voucher == voucher_percentage
+    assert order_discount.voucher_code == code.code
 
     code.refresh_from_db()
     assert code.used == voucher_used_count + 1
@@ -911,6 +917,9 @@ def test_order_from_checkout_with_specific_product_voucher(
         order_discount.amount_value
         == (order.undiscounted_total - order.total).gross.amount
     )
+    assert order_discount.type == DiscountType.VOUCHER
+    assert order_discount.voucher == voucher_specific_product_type
+    assert order_discount.voucher_code == code.code
 
     code.refresh_from_db()
     assert code.used == voucher_used_count + 1
@@ -956,7 +965,7 @@ def test_order_from_checkout_voucher_not_increase_uses_on_preprocess_creation_fa
     assert code.used == 0
 
 
-def test_order_from_checkout_on_promotion(
+def test_order_from_checkout_on_catalogue_promotion(
     app_api_client,
     checkout_with_item_on_promotion,
     permission_handle_checkouts,
@@ -988,7 +997,6 @@ def test_order_from_checkout_on_promotion(
     assert not data["errors"]
 
     order = Order.objects.first()
-
     assert order.status == OrderStatus.UNCONFIRMED
     assert order.origin == OrderOrigin.CHECKOUT
     assert not order.original
@@ -1003,6 +1011,52 @@ def test_order_from_checkout_on_promotion(
     assert (
         discount.amount_value == (order.undiscounted_total - order.total).gross.amount
     )
+    assert not order.discounts.first()
+
+
+def test_order_from_checkout_on_order_promotion(
+    app_api_client,
+    checkout_with_item_and_order_discount,
+    permission_handle_checkouts,
+    permission_manage_checkouts,
+    address,
+    shipping_method,
+):
+    # given
+    checkout = checkout_with_item_and_order_discount
+    checkout.shipping_address = address
+    checkout.shipping_method = shipping_method
+    checkout.billing_address = address
+    checkout.save()
+
+    variables = {
+        "id": graphene.Node.to_global_id("Checkout", checkout.pk),
+    }
+
+    # when
+    response = app_api_client.post_graphql(
+        MUTATION_ORDER_CREATE_FROM_CHECKOUT,
+        variables,
+        permissions=[permission_handle_checkouts, permission_manage_checkouts],
+    )
+
+    # then
+    content = get_graphql_content(response)
+    data = content["data"]["orderCreateFromCheckout"]
+    assert not data["errors"]
+
+    order = Order.objects.first()
+    assert order.status == OrderStatus.UNCONFIRMED
+    assert order.origin == OrderOrigin.CHECKOUT
+    assert not order.original
+
+    order_discount = order.discounts.first()
+    assert order_discount.promotion_rule
+    assert (
+        order_discount.amount_value
+        == (order.undiscounted_total - order.total).gross.amount
+    )
+    assert order_discount.type == DiscountType.ORDER_PROMOTION
 
 
 def test_order_from_checkout_multiple_rules_applied(


Code review comments:
diff_hunk: @@ -1053,21 +1053,39 @@ def _handle_allocations_of_order_lines(
     )
 
 
-def _handle_checkout_discount(order: "Order", checkout: "Checkout"):
+def _create_order_discount(order: "Order", checkout_info: "CheckoutInfo"):
+    checkout = checkout_info.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
-
-        order.discounts.create(
-            type=DiscountType.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,
-        )
+        checkout_discount = checkout.discounts.first()
+        if checkout_discount and checkout_discount.type == DiscountType.ORDER_PROMOTION:
+            order.discounts.create(
+                type=checkout_discount.type,
+                value_type=checkout_discount.value_type,
+                value=checkout_discount.value,
+                name=checkout_discount.name,
+                translated_name=checkout_discount.translated_name,
+                currency=checkout_discount.currency,
+                amount_value=checkout_discount.amount_value,
+                promotion_rule=checkout_discount.promotion_rule,
+            )
path: saleor/checkout/complete_checkout.py
text: @user1:
As we already have the object that is almost the same. We can do something like that:
```
discount_data = model_to_dict(checkout_discount)
order.discount.create(**discount_data)
```