Repository: dj-stripe/dj-stripe

Issue:
`Plan.create()` cannot create both Stripe object and Django object
This is the `djstripe.models.billing.Plan.create()` method.
```
       @classmethod
	def create(cls, **kwargs):
		# A few minor things are changed in the api-version of the create call
		api_kwargs = dict(kwargs)
		api_kwargs["amount"] = int(api_kwargs["amount"] * 100)
		cls._api_create(**api_kwargs)

		plan = Plan.objects.create(**kwargs)

		return plan
```
It's broken (as far as I can tell). Here's why:

* `api_kwargs` requires a string for `product`
* `kwargs` requires a model instance of `djstripe.Product`

To fix this, we should add `api_kwargs["product"] = kwargs["product"].id)` or change the string `repr` of `Product` to be the `id` attribute.


Discussion:
@therefromhere Can you do this?

```
@classmethod
def _api_create(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs):
    """
    Call the stripe API's create operation for this model.

    Convert StripeModel instances into their `id` (str) for Stripe API reference.

    :param api_key: The api key to use for this request. Defaults to 
    djstripe_settings.STRIPE_SECRET_KEY.
    :type api_key: string
    """
    for key in kwargs:
        if isinstance(kwargs[key], StripeModel):
            kwargs[key] = kwargs[key].id

    return cls.stripe_class.create(api_key=api_key, **kwargs)
```
If we were going to do that we should also change all the other similar method to check for isinstance(kwargs[key], StripeModel) - not necessarily against this but we should be consistent.

In this case though I wonder why we're not using `sync_from_stripe_data` inside Plan.create.
So this recommendation is actually for the `_api_create` method on the StripeModel superclass. 

That might solve it for Subscription, etc also.  

> On Apr 7, 2019, at 16:15, John Carter <notifications@github.com> wrote:
> 
> If we were going to do that we should also change all the other similar method to check for isinstance(kwargs[key], StripeModel) - not necessarily against this but we should be consistent.
> 
> In this case though I wonder why we're not using sync_from_stripe_data inside Plan.create.
> 
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub, or mute the thread.

Yeah, I meant we'd should probably change `_api_delete`, `api_list` etc as well
Those appear to be functioning just fine. It's specifically `create`, 
because `list` and `delete` don't require foreign keys to work.

On 4/7/2019 5:57 PM, John Carter wrote:
>
> Yeah, I meant we'd should probably change |_api_delete|, |api_list| 
> etc as well
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub 
> <https://github.com/dj-stripe/dj-stripe/issues/870#issuecomment-480649561>, 
> or mute the thread 
> <https://github.com/notifications/unsubscribe-auth/Aa_sBJJEp_um0qx15MUTWPY6ibTDEQ1oks5vepQBgaJpZM4cMx3n>.
>


Resolving patch:
diff --git a/djstripe/models/billing.py b/djstripe/models/billing.py
--- a/djstripe/models/billing.py
+++ b/djstripe/models/billing.py
@@ -799,9 +799,12 @@ def create(cls, **kwargs):
 		# A few minor things are changed in the api-version of the create call
 		api_kwargs = dict(kwargs)
 		api_kwargs["amount"] = int(api_kwargs["amount"] * 100)
-		cls._api_create(**api_kwargs)
 
-		plan = Plan.objects.create(**kwargs)
+		if isinstance(api_kwargs.get("product"), StripeModel):
+			api_kwargs["product"] = api_kwargs["product"].id
+
+		stripe_plan = cls._api_create(**api_kwargs)
+		plan = cls.sync_from_stripe_data(stripe_plan)
 
 		return plan
 


Test patch:
diff --git a/tests/test_plan.py b/tests/test_plan.py
--- a/tests/test_plan.py
+++ b/tests/test_plan.py
@@ -10,7 +10,7 @@
 
 from djstripe.admin import PlanAdmin
 from djstripe.enums import PlanUsageType
-from djstripe.models import Plan
+from djstripe.models import Plan, Product
 from djstripe.settings import STRIPE_SECRET_KEY
 
 from . import (
@@ -43,7 +43,7 @@ def test_update_name(self, plan_retrieve_mock):
 		# Would throw DoesNotExist if it didn't work
 		Plan.objects.get(name="Updated Plan Name")
 
-	@patch("stripe.Plan.create")
+	@patch("stripe.Plan.create", return_value=FAKE_PLAN_II)
 	@patch("stripe.Plan.retrieve")
 	def test_that_admin_save_does_create_new_object(
 		self, plan_retrieve_mock, plan_create_mock
@@ -75,25 +75,87 @@ def test_that_admin_save_does_update_object(
 		Plan.objects.get(name=self.plan.name)
 
 
-class PlanTest(AssertStripeFksMixin, TestCase):
+class PlanCreateTest(AssertStripeFksMixin, TestCase):
 	def setUp(self):
-		self.plan_data = deepcopy(FAKE_PLAN)
 		with patch("stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT)):
-			self.plan = Plan.sync_from_stripe_data(self.plan_data)
+			self.stripe_product = Product(id=FAKE_PRODUCT["id"]).api_retrieve()
 
-	@patch("djstripe.models.Plan.objects.create")
-	@patch("djstripe.models.Plan._api_create")
-	def test_create_with_metadata(self, api_create_mock, object_create_mock):
-		metadata = {"other_data": "more_data"}
-		Plan.create(metadata=metadata, arg1=1, arg2=2, amount=1, id=1)
+	@patch("stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT))
+	@patch("stripe.Plan.create", return_value=deepcopy(FAKE_PLAN))
+	def test_create_from_product_id(self, plan_create_mock, product_retrieve_mock):
+		fake_plan = deepcopy(FAKE_PLAN)
+		fake_plan["amount"] = fake_plan["amount"] / 100
+		self.assertIsInstance(fake_plan["product"], str)
 
-		api_create_mock.assert_called_once_with(
-			metadata=metadata, id=1, arg1=1, arg2=2, amount=100
+		plan = Plan.create(**fake_plan)
+
+		expected_create_kwargs = deepcopy(FAKE_PLAN)
+		expected_create_kwargs["api_key"] = STRIPE_SECRET_KEY
+
+		plan_create_mock.assert_called_once_with(**expected_create_kwargs)
+
+		self.assert_fks(plan, expected_blank_fks={"djstripe.Customer.coupon"})
+
+	@patch("stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT))
+	@patch("stripe.Plan.create", return_value=deepcopy(FAKE_PLAN))
+	def test_create_from_stripe_product(self, plan_create_mock, product_retrieve_mock):
+		fake_plan = deepcopy(FAKE_PLAN)
+		fake_plan["product"] = self.stripe_product
+		fake_plan["amount"] = fake_plan["amount"] / 100
+		self.assertIsInstance(fake_plan["product"], dict)
+
+		plan = Plan.create(**fake_plan)
+
+		expected_create_kwargs = deepcopy(FAKE_PLAN)
+		expected_create_kwargs["product"] = self.stripe_product
+
+		plan_create_mock.assert_called_once_with(
+			api_key=STRIPE_SECRET_KEY, **expected_create_kwargs
 		)
-		object_create_mock.assert_called_once_with(
-			metadata=metadata, id=1, arg1=1, arg2=2, amount=1
+
+		self.assert_fks(plan, expected_blank_fks={"djstripe.Customer.coupon"})
+
+	@patch("stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT))
+	@patch("stripe.Plan.create", return_value=deepcopy(FAKE_PLAN))
+	def test_create_from_djstripe_product(self, plan_create_mock, product_retrieve_mock):
+		fake_plan = deepcopy(FAKE_PLAN)
+		fake_plan["product"] = Product.sync_from_stripe_data(self.stripe_product)
+		fake_plan["amount"] = fake_plan["amount"] / 100
+		self.assertIsInstance(fake_plan["product"], Product)
+
+		plan = Plan.create(**fake_plan)
+
+		plan_create_mock.assert_called_once_with(api_key=STRIPE_SECRET_KEY, **FAKE_PLAN)
+
+		self.assert_fks(plan, expected_blank_fks={"djstripe.Customer.coupon"})
+
+	@patch("stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT))
+	@patch("stripe.Plan.create", return_value=deepcopy(FAKE_PLAN))
+	def test_create_with_metadata(self, plan_create_mock, product_retrieve_mock):
+		metadata = {"other_data": "more_data"}
+		fake_plan = deepcopy(FAKE_PLAN)
+		fake_plan["amount"] = fake_plan["amount"] / 100
+		fake_plan["metadata"] = metadata
+		self.assertIsInstance(fake_plan["product"], str)
+
+		plan = Plan.create(**fake_plan)
+
+		expected_create_kwargs = deepcopy(FAKE_PLAN)
+		expected_create_kwargs["metadata"] = metadata
+
+		plan_create_mock.assert_called_once_with(
+			api_key=STRIPE_SECRET_KEY, **expected_create_kwargs
 		)
 
+		self.assert_fks(plan, expected_blank_fks={"djstripe.Customer.coupon"})
+
+
+class PlanTest(AssertStripeFksMixin, TestCase):
+	def setUp(self):
+		self.plan_data = deepcopy(FAKE_PLAN)
+		with patch("stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT)):
+			self.plan = Plan.sync_from_stripe_data(self.plan_data)
+
 	def test_str(self):
 		self.assertEqual(str(self.plan), self.plan_data["name"])