diff --git a/djstripe/contrib/rest_framework/serializers.py b/djstripe/contrib/rest_framework/serializers.py index 5f52f1f4f3..9e0816213f 100644 --- a/djstripe/contrib/rest_framework/serializers.py +++ b/djstripe/contrib/rest_framework/serializers.py @@ -25,3 +25,4 @@ class CreateSubscriptionSerializer(serializers.Serializer): stripe_token = serializers.CharField(max_length=200) plan = serializers.CharField(max_length=50) + charge_immediately = serializers.NullBooleanField(required=False) diff --git a/djstripe/contrib/rest_framework/views.py b/djstripe/contrib/rest_framework/views.py index befc2495d9..9135886db8 100644 --- a/djstripe/contrib/rest_framework/views.py +++ b/djstripe/contrib/rest_framework/views.py @@ -50,13 +50,21 @@ def post(self, request, format=None): if serializer.is_valid(): try: - customer, _created = Customer.get_or_create(subscriber=subscriber_request_callback(self.request)) + customer, created = Customer.get_or_create( + subscriber=subscriber_request_callback(self.request) + ) customer.add_card(serializer.data["stripe_token"]) - customer.subscribe(serializer.data["plan"]) + customer.subscribe( + serializer.data["plan"], + serializer.data.get("charge_immediately", True) + ) return Response(serializer.data, status=status.HTTP_201_CREATED) except: # TODO: Better error messages - return Response("Something went wrong processing the payment.", status=status.HTTP_400_BAD_REQUEST) + return Response( + "Something went wrong processing the payment.", + status=status.HTTP_400_BAD_REQUEST + ) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/docs/cookbook.rst b/docs/cookbook.rst index 99736577f9..728f8a1515 100644 --- a/docs/cookbook.rst +++ b/docs/cookbook.rst @@ -166,6 +166,8 @@ DELETE will cancel the current subscription, based on the settings. - input - stripe_token (string) - plan (string) + - charge_immediately (boolean, optional) + - Does not send an invoice to the Customer immediately - output (201) - stripe_token (string) diff --git a/tests/test_contrib/test_views.py b/tests/test_contrib/test_views.py index 8f45c4cce7..2b72385fee 100644 --- a/tests/test_contrib/test_views.py +++ b/tests/test_contrib/test_views.py @@ -37,6 +37,13 @@ def setUp(self): @patch("djstripe.models.Customer.add_card", autospec=True) @patch("stripe.Customer.create", return_value=deepcopy(FAKE_CUSTOMER)) def test_create_subscription(self, stripe_customer_create_mock, add_card_mock, subscribe_mock): + """Test a POST to the SubscriptionRestView. + + Should: + - Create a Customer object + - Add a card to the Customer object + - Subcribe the Customer to a plan + """ self.assertEqual(0, Customer.objects.count()) data = { "plan": "test0", @@ -45,7 +52,27 @@ def test_create_subscription(self, stripe_customer_create_mock, add_card_mock, s response = self.client.post(self.url, data) self.assertEqual(1, Customer.objects.count()) add_card_mock.assert_called_once_with(self.user.customer, "cake") - subscribe_mock.assert_called_once_with(self.user.customer, "test0") + subscribe_mock.assert_called_once_with(self.user.customer, "test0", True) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.data, data) + + @patch("djstripe.models.Customer.subscribe", autospec=True) + @patch("djstripe.models.Customer.add_card", autospec=True) + @patch("stripe.Customer.create", return_value=deepcopy(FAKE_CUSTOMER)) + def test_create_subscription_charge_immediately(self, stripe_customer_create_mock, add_card_mock, subscribe_mock): + """Test a POST to the SubscriptionRestView. + + Should be able to accept an charge_immediately. + This will not send an invoice to the customer on subscribe. + """ + self.assertEqual(0, Customer.objects.count()) + data = { + "plan": "test0", + "stripe_token": "cake", + "charge_immediately": False, + } + response = self.client.post(self.url, data) + subscribe_mock.assert_called_once_with(self.user.customer, "test0", False) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data, data) @@ -53,6 +80,10 @@ def test_create_subscription(self, stripe_customer_create_mock, add_card_mock, s @patch("djstripe.models.Customer.add_card", autospec=True) @patch("stripe.Customer.create", return_value=deepcopy(FAKE_CUSTOMER)) def test_create_subscription_exception(self, stripe_customer_create_mock, add_card_mock, subscribe_mock): + """Test a POST to the SubscriptionRestView. + + Should return a 400 when an Exception is raised. + """ subscribe_mock.side_effect = Exception data = { "plan": "test0", @@ -61,11 +92,24 @@ def test_create_subscription_exception(self, stripe_customer_create_mock, add_ca response = self.client.post(self.url, data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - def test_get_no_content_for_subscription(self): - response = self.client.get(self.url) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + def test_create_subscription_incorrect_data(self): + """Test a POST to the SubscriptionRestView. + + Should return a 400 when a the serializer is invalid. + """ + self.assertEqual(0, Customer.objects.count()) + data = { + "foo": "bar", + } + response = self.client.post(self.url, data) + self.assertEqual(0, Customer.objects.count()) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_get_subscription(self): + """Test a GET to the SubscriptionRestView. + + Should return the correct data. + """ Customer.objects.create(subscriber=self.user, stripe_id=FAKE_CUSTOMER["id"], currency="usd") plan = Plan.sync_from_stripe_data(deepcopy(FAKE_PLAN)) subscription = Subscription.sync_from_stripe_data(deepcopy(FAKE_SUBSCRIPTION)) @@ -76,8 +120,20 @@ def test_get_subscription(self): self.assertEqual(response.data['status'], subscription.status) self.assertEqual(response.data['cancel_at_period_end'], subscription.cancel_at_period_end) + def test_get_no_content_for_subscription(self): + """Test a GET to the SubscriptionRestView. + + Should return a 204 when an exception is raised. + """ + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + @patch("djstripe.models.Subscription.cancel") def test_cancel_subscription(self, cancel_subscription_mock): + """Test a DELETE to the SubscriptionRestView. + + Should cancel a Customer objects subscription. + """ def _cancel_sub(*args, **kwargs): subscription = Subscription.objects.first() subscription.status = Subscription.STATUS_CANCELED @@ -108,16 +164,11 @@ def _cancel_sub(*args, **kwargs): self.assertTrue(self.user.is_authenticated()) def test_cancel_subscription_exception(self): - response = self.client.delete(self.url) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + """Test a DELETE to the SubscriptionRestView. - def test_create_subscription_incorrect_data(self): - self.assertEqual(0, Customer.objects.count()) - data = { - "foo": "bar", - } - response = self.client.post(self.url, data) - self.assertEqual(0, Customer.objects.count()) + Should return a 400 when an exception is raised. + """ + response = self.client.delete(self.url) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)