Repository: saleor/saleor

Issue:
Bug:  PromotionCreate predicate is None unhandled case
### What are you trying to achieve?

when creating promotion by PromotionCreate mutation we can set catalogue predicate as None which will result in an error.

### Steps to reproduce the problem

create promotion with rules->cataloguePredicate as None 
```
{
"operationName": "PromotionCreate", 
"query": 
"mutation PromotionCreate($input: PromotionCreateInput!) {
  promotionCreate(input: $input) {
    errors {
      ...PromotionCreateError
      __typename
    }
    promotion {
      id
      __typename
    }
    __typename
  }
}

fragment PromotionCreateError on PromotionCreateError {
  field
  message
  code
  index
  __typename
}
"
, 
"variables": {
"input": {
"description": None, 
"endDate": None, 
"name": "name", 
"rules": [
{
"cataloguePredicate": "None", 
"channels": "[]", 
"description": "None", 
"name": "''", 
"rewardValue": "0", 
"rewardValueType": "'PERCENTAGE'"
}
], 
"startDate": None
}
}
}
```
In result we are getting 
```TypeError 
argument of type 'NoneType' is not iterable
```
the crash happens in `saleor/graphql/discount/mutations/promotion/validators.py` at line 143 



### What did you expect to happen?

It should be handled as the `cataloguePredicate` was an empty dict

### Logs

[link to sentry with this issue happening ](https://saleor.sentry.io/issues/4680318906/?alert_rule_id=11198640&alert_type=issue&notification_uuid=35924ea8-dbf7-47b6-b995-72333a9ebb21&project=6417854&referrer=slack)

### Environment

Saleor version: Saleor 3.17
OS and version: …



Discussion:

Resolving patch:
diff --git a/saleor/graphql/discount/mutations/promotion/validators.py b/saleor/graphql/discount/mutations/promotion/validators.py
--- a/saleor/graphql/discount/mutations/promotion/validators.py
+++ b/saleor/graphql/discount/mutations/promotion/validators.py
@@ -117,6 +117,9 @@ def clean_predicate(predicate, error_class, index=None):
     Operators cannot be mixed with other filter inputs. There could be only
     one operator on each level.
     """
+    if not predicate:
+        return {}
+
     if isinstance(predicate, list):
         return [
             clean_predicate(item, error_class, index)


Test patch:
diff --git a/saleor/graphql/discount/tests/mutations/test_promotion_create.py b/saleor/graphql/discount/tests/mutations/test_promotion_create.py
--- a/saleor/graphql/discount/tests/mutations/test_promotion_create.py
+++ b/saleor/graphql/discount/tests/mutations/test_promotion_create.py
@@ -1408,3 +1408,53 @@ def test_promotion_create_events_by_app(
     rule_ids = [event["ruleId"] for event in events if event.get("ruleId")]
     rules = data["promotion"]["rules"]
     assert all([rule["id"] in rule_ids for rule in rules])
+
+
+def test_promotion_create_without_catalogue_predicate(
+    staff_api_client,
+    permission_manage_discounts,
+    description_json,
+    channel_USD,
+    channel_PLN,
+    variant,
+    product,
+):
+    # given
+    start_date = timezone.now() - timedelta(days=30)
+    end_date = timezone.now() + timedelta(days=30)
+    channel_ids = [
+        graphene.Node.to_global_id("Channel", channel.pk)
+        for channel in [channel_USD, channel_PLN]
+    ]
+    promotion_name = "test promotion"
+    variables = {
+        "input": {
+            "name": promotion_name,
+            "description": description_json,
+            "startDate": start_date.isoformat(),
+            "endDate": end_date.isoformat(),
+            "rules": [
+                {
+                    "name": "test promotion rule",
+                    "description": description_json,
+                    "channels": channel_ids,
+                    "rewardValueType": RewardValueTypeEnum.PERCENTAGE.name,
+                    "rewardValue": Decimal("50"),
+                    "cataloguePredicate": None,
+                }
+            ],
+        }
+    }
+
+    # when
+    response = staff_api_client.post_graphql(
+        PROMOTION_CREATE_MUTATION, variables, permissions=(permission_manage_discounts,)
+    )
+
+    # then
+    content = get_graphql_content(response)
+    data = content["data"]["promotionCreate"]
+    assert not data["errors"]
+    assert data["promotion"]
+    assert len(data["promotion"]["rules"]) == 1
+    assert data["promotion"]["rules"][0]["cataloguePredicate"] == {}