Repository: saleor/saleor

Issue:
Extensions: method product_created is executed too early
### What I'm trying to achieve
I'm using webhooks plugin to handle product creation (`PRODUCT_CREATED` hook). In resulting payload I expect to receive list of collections the products is assigned to. Currently the list is always `null`, because the `product_created` method of plugins is executed before many-to-many relationships of product are set.

Currently, `product_created` method is executed during `save` method of `ProductCreate` mutation:

```python
    @classmethod
    @transaction.atomic
    def save(cls, info, instance, cleaned_input):
        instance.save()
        info.context.extensions.product_created(instance)
        ...
```
It should be executed after `_save_m2m` method instead.

### Steps to reproduce the problem
1. Create a webhook for "Product created" event
2. Create a new product, with collections set

The resulting webhook payload has `collections` field set to `null`.

### What I expected to happen
The resulting webhook payload should contain proper collections data, e.g.:

```
        "collections": [
            {
                "type": "Collection",
                "name": "Summer collection",
                "slug": "summer-collection"
            }
        ],
```


Discussion:

Resolving patch:
diff --git a/saleor/graphql/product/mutations/products.py b/saleor/graphql/product/mutations/products.py
--- a/saleor/graphql/product/mutations/products.py
+++ b/saleor/graphql/product/mutations/products.py
@@ -882,7 +882,6 @@ def get_instance(cls, info, **data):
     @transaction.atomic
     def save(cls, info, instance, cleaned_input):
         instance.save()
-        info.context.extensions.product_created(instance)
         if not instance.product_type.has_variants:
             site_settings = info.context.site.settings
             track_inventory = cleaned_input.get(
@@ -906,6 +905,12 @@ def _save_m2m(cls, info, instance, cleaned_data):
         if collections is not None:
             instance.collections.set(collections)
 
+    @classmethod
+    def perform_mutation(cls, _root, info, **data):
+        response = super().perform_mutation(_root, info, **data)
+        info.context.extensions.product_created(response.product)
+        return response
+
 
 class ProductUpdate(ProductCreate):
     class Arguments:


Test patch:
diff --git a/tests/api/test_product.py b/tests/api/test_product.py
--- a/tests/api/test_product.py
+++ b/tests/api/test_product.py
@@ -1083,6 +1083,68 @@ def test_product_create_without_category_and_true_is_published_value(
     assert errors[0]["message"] == "You must select a category to be able to publish"
 
 
+def test_product_create_with_collections_webhook(
+    staff_api_client,
+    permission_manage_products,
+    collection,
+    product_type,
+    category,
+    monkeypatch,
+):
+    query = """
+    mutation createProduct($productTypeId: ID!, $collectionId: ID!, $categoryId: ID!) {
+        productCreate(input: {
+                name: "Product",
+                basePrice: "2.5",
+                productType: $productTypeId,
+                isPublished: true,
+                collections: [$collectionId],
+                category: $categoryId
+            }) {
+            product {
+                id,
+                collections {
+                    slug
+                },
+                category {
+                    slug
+                }
+            }
+            errors {
+                message
+                field
+            }
+        }
+    }
+
+    """
+
+    def assert_product_has_collections(product):
+        assert product.collections.count() > 0
+        assert product.collections.first() == collection
+
+    monkeypatch.setattr(
+        "saleor.extensions.manager.ExtensionsManager.product_created",
+        lambda _, product: assert_product_has_collections(product),
+    )
+
+    product_type_id = graphene.Node.to_global_id("ProductType", product_type.pk)
+    category_id = graphene.Node.to_global_id("Category", category.pk)
+    collection_id = graphene.Node.to_global_id("Collection", collection.pk)
+
+    response = staff_api_client.post_graphql(
+        query,
+        {
+            "productTypeId": product_type_id,
+            "categoryId": category_id,
+            "collectionId": collection_id,
+        },
+        permissions=[permission_manage_products],
+    )
+
+    get_graphql_content(response)
+
+
 def test_update_product(
     staff_api_client,
     category,