From fc1ddbdd3b51c4f56e7360e96ff15f2e772f4621 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 17 Aug 2017 04:22:23 +0530 Subject: [PATCH 01/41] Added StripePlan model --- datacenterlight/models.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/datacenterlight/models.py b/datacenterlight/models.py index fdfebc96..d8237ed2 100644 --- a/datacenterlight/models.py +++ b/datacenterlight/models.py @@ -59,3 +59,15 @@ class VMTemplate(models.Model): def create(cls, name, opennebula_vm_template_id): vm_template = cls(name=name, opennebula_vm_template_id=opennebula_vm_template_id) return vm_template + + +class StripePlan(models.Model): + """ + A model to store Data Center Light's created Stripe plans + """ + stripe_plan_id = models.CharField(max_length=100, null=True) + + @classmethod + def create(cls, stripe_plan_id): + stripe_plan = cls(stripe_plan_id=stripe_plan_id) + return stripe_plan From 974617badbdbbfab1f475a0f99d2f0cd26f957eb Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 17 Aug 2017 04:23:22 +0530 Subject: [PATCH 02/41] Added get_or_create_plan and subscribe_customer_to_plan and get_stripe_plan_id_string functions to StripeUtils class --- utils/stripe_utils.py | 70 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index aaf3d9e9..6ef50f31 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -1,5 +1,7 @@ import stripe from django.conf import settings +from datacenterlight.models import StripePlan + stripe.api_key = settings.STRIPE_API_PRIVATE_KEY @@ -129,13 +131,52 @@ class StripeUtils(object): return charge @handleStripeError - def create_plan(self, amount, name, id): - self.stripe.Plan.create( - amount=amount, - interval=self.INTERVAL, - name=name, - currency=self.CURRENCY, - id=id) + def get_or_create_plan(self, amount, name, stripe_plan_id): + """ + This function checks if a StripePlan with the given id already exists. If it exists then the function returns + this object otherwise it creates a new StripePlan and returns the new object. + + :param amount: The amount in CHF + :param name: The name of the Stripe plan to be created. + :param stripe_plan_id: The id of the Stripe plan to be created. Use get_stripe_plan_id_string function to obtain the name of the plan to be created + :return: The StripePlan object if it exists or if can create a Plan object with Stripe, None otherwise + """ + _amount = float(amount) + amount = int(_amount * 100) # stripe amount unit, in cents + + try: + stripe_plan_db_obj = StripePlan.objects.get(stripe_plan_id=stripe_plan_id) + except StripePlan.DoesNotExist: + stripe_plan = self.stripe.Plan.create( + amount=amount, + interval=self.INTERVAL, + name=name, + currency=self.CURRENCY, + id=stripe_plan_id) + if not stripe_plan.get('response_object') and not stripe_plan.get('error'): + stripe_plan_db_obj = StripePlan.objects.create(stripe_plan_id=stripe_plan_id) + else: + stripe_plan_db_obj = None + return stripe_plan_db_obj + + @handleStripeError + def subscribe_customer_to_plan(self, customer, plans): + """ + Subscribes the given customer to the list of plans + + :param customer: The stripe customer identifier + :param plans: A list of stripe plan id strings to which the customer needs to be subscribed to + :return: The subscription StripeObject + """ + stripe.Subscription.create( + customer=customer, + items=[ + { + "plan": "silver-elite-429", + }, + ], + itemss=[{"plan": plans[index] for index, item in enumerate(plans)}] + ) @handleStripeError def make_payment(self, customer, amount, token): @@ -145,3 +186,18 @@ class StripeUtils(object): customer=customer ) return charge + + @staticmethod + def get_stripe_plan_id_string(cpu, ram, ssd, version): + """ + Returns the stripe plan id string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters + + :param cpu: The number of cores + :param ram: The size of the RAM in GB + :param ssd: The size of storage in GB + :param version: The version of the Stripe plans + :return: A string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb` + """ + DCL_PLAN_STRING = 'cpu-{cpu}-ram-{ram}gb-ssd-{ssd}gb'.format(cpu=cpu, ram=ram, ssd=ssd) + STRIPE_PLAN_ID_STRING = '{app}-v{version}-{plan}'.format(app='dcl', version=version, plan=DCL_PLAN_STRING) + return STRIPE_PLAN_ID_STRING From 9d4085138ab0ba4760123c724b3b4546ee702f15 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 17 Aug 2017 04:25:07 +0530 Subject: [PATCH 03/41] Added StripePlanTestCase --- utils/tests.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/utils/tests.py b/utils/tests.py index 4a29fa60..2a226c33 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -6,6 +6,7 @@ from model_mommy import mommy from utils.stripe_utils import StripeUtils import stripe from django.conf import settings +from datacenterlight.models import StripePlan class BaseTestCase(TestCase): @@ -117,3 +118,17 @@ class TestStripeCustomerDescription(TestCase): self.assertEqual(stripe_data.get('error'), None) customer_data = stripe_data.get('response_object') self.assertEqual(customer_data.description, test_name) + + +class StripePlanTestCase(TestStripeCustomerDescription): + """ + A class to test Stripe plans + """ + + def test_create_plan(self): + stripe_utils = StripeUtils() + plan_id_string = stripe_utils.get_stripe_plan_id_string(2, 20, 100, 1) + self.assertEqual(plan_id_string, 'dcl-v1-cpu-2-ram-20gb-ssd-100gb') + stripe_plan = stripe_utils.get_or_create_plan(2000, "test plan 1", stripe_plan_id='test-plan-1') + self.assertEqual(stripe_plan.get('error'), None) + self.assertIsInstance(stripe_plan.get('response_object'), StripePlan) From 1a2b0e4e294a6a7514bd30525ab17db7cdd8fab1 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 17 Aug 2017 04:25:58 +0530 Subject: [PATCH 04/41] Added datacenterlight/migrations/0007_stripeplan.py migration file --- datacenterlight/migrations/0007_stripeplan.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 datacenterlight/migrations/0007_stripeplan.py diff --git a/datacenterlight/migrations/0007_stripeplan.py b/datacenterlight/migrations/0007_stripeplan.py new file mode 100644 index 00000000..95892205 --- /dev/null +++ b/datacenterlight/migrations/0007_stripeplan.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-08-16 19:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0006_vmtemplate'), + ] + + operations = [ + migrations.CreateModel( + name='StripePlan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_plan_id', models.CharField(max_length=100, null=True)), + ], + ), + ] From 783ab5714c9e7aeae31f2b3de8e6484bbbf059ee Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 17 Aug 2017 13:01:05 +0530 Subject: [PATCH 05/41] Improved get_or_create_plan and subscribe_customer_to_plan --- utils/stripe_utils.py | 50 +++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index 6ef50f31..971b7046 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -1,8 +1,10 @@ import stripe from django.conf import settings from datacenterlight.models import StripePlan +import logging stripe.api_key = settings.STRIPE_API_PRIVATE_KEY +logger = logging.getLogger(__name__) def handleStripeError(f): @@ -57,6 +59,8 @@ class StripeUtils(object): CURRENCY = 'chf' INTERVAL = 'month' SUCCEEDED_STATUS = 'succeeded' + PLAN_ALREADY_EXISTS = 'Plan already exists' + PLAN_EXISTS_ERROR_MSG = 'Plan {} exists already.\nCreating a local StripePlan now.' def __init__(self): self.stripe = stripe @@ -147,36 +151,44 @@ class StripeUtils(object): try: stripe_plan_db_obj = StripePlan.objects.get(stripe_plan_id=stripe_plan_id) except StripePlan.DoesNotExist: - stripe_plan = self.stripe.Plan.create( - amount=amount, - interval=self.INTERVAL, - name=name, - currency=self.CURRENCY, - id=stripe_plan_id) - if not stripe_plan.get('response_object') and not stripe_plan.get('error'): - stripe_plan_db_obj = StripePlan.objects.create(stripe_plan_id=stripe_plan_id) - else: - stripe_plan_db_obj = None + try: + stripe_plan = self.stripe.Plan.create( + amount=amount, + interval=self.INTERVAL, + name=name, + currency=self.CURRENCY, + id=stripe_plan_id) + if not stripe_plan.get('response_object') and not stripe_plan.get('error'): + stripe_plan_db_obj = StripePlan.objects.create(stripe_plan_id=stripe_plan_id) + else: + stripe_plan_db_obj = None + except stripe.error.InvalidRequestError as e: + if self.PLAN_ALREADY_EXISTS in str(e): + logger.debug(self.PLAN_EXISTS_ERROR_MSG.format(stripe_plan_id)) + stripe_plan_db_obj = StripePlan.objects.create(stripe_plan_id=stripe_plan_id) return stripe_plan_db_obj @handleStripeError def subscribe_customer_to_plan(self, customer, plans): """ - Subscribes the given customer to the list of plans + Subscribes the given customer to the list of given plans :param customer: The stripe customer identifier - :param plans: A list of stripe plan id strings to which the customer needs to be subscribed to + :param plans: A list of stripe plans. Ref: https://stripe.com/docs/api/python#create_subscription-items + e.g. + plans = [ + { + "plan": "dcl-v1-cpu-2-ram-5gb-ssd-10gb", + }, + ] :return: The subscription StripeObject """ - stripe.Subscription.create( + + subscription_result = self.stripe.Subscription.create( customer=customer, - items=[ - { - "plan": "silver-elite-429", - }, - ], - itemss=[{"plan": plans[index] for index, item in enumerate(plans)}] + items=plans, ) + return subscription_result @handleStripeError def make_payment(self, customer, amount, token): From 08c5485352b453977a53a7647ddffdc49adca2c6 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 17 Aug 2017 13:01:48 +0530 Subject: [PATCH 06/41] Added test_create_duplicate_plans_error_handling --- utils/tests.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/utils/tests.py b/utils/tests.py index 2a226c33..de117292 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -7,6 +7,8 @@ from utils.stripe_utils import StripeUtils import stripe from django.conf import settings from datacenterlight.models import StripePlan +import uuid +from unittest.mock import patch class BaseTestCase(TestCase): @@ -125,10 +127,27 @@ class StripePlanTestCase(TestStripeCustomerDescription): A class to test Stripe plans """ - def test_create_plan(self): + def test_get_or_create_plan(self): stripe_utils = StripeUtils() plan_id_string = stripe_utils.get_stripe_plan_id_string(2, 20, 100, 1) self.assertEqual(plan_id_string, 'dcl-v1-cpu-2-ram-20gb-ssd-100gb') stripe_plan = stripe_utils.get_or_create_plan(2000, "test plan 1", stripe_plan_id='test-plan-1') self.assertEqual(stripe_plan.get('error'), None) self.assertIsInstance(stripe_plan.get('response_object'), StripePlan) + + @patch('utils.stripe_utils.logger') + def test_create_duplicate_plans_error_handling(self, mock_logger): + stripe_utils = StripeUtils() + unique_id = str(uuid.uuid4().hex) + new_plan_id_str = 'test-plan-{}'.format(unique_id) + stripe_plan = stripe_utils.get_or_create_plan(2000, "test plan {}".format(unique_id), + stripe_plan_id=new_plan_id_str) + self.assertIsInstance(stripe_plan.get('response_object'), StripePlan) + self.assertEqual(stripe_plan.get('response_object').stripe_plan_id, new_plan_id_str) + # Test creating the same plan again and expect the PLAN_EXISTS_ERROR_MSG + StripePlan.objects.filter(stripe_plan_id=new_plan_id_str).all().delete() + stripe_plan_1 = stripe_utils.get_or_create_plan(2000, "test plan {}".format(unique_id), + stripe_plan_id=new_plan_id_str) + mock_logger.debug.assert_called_with(stripe_utils.PLAN_EXISTS_ERROR_MSG.format(new_plan_id_str)) + self.assertIsInstance(stripe_plan_1.get('response_object'), StripePlan) + self.assertEqual(stripe_plan_1.get('response_object').stripe_plan_id, new_plan_id_str) From bba82e95d1a05e9d08709e06ab1d3edd5b635cb0 Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 17 Aug 2017 11:04:22 +0200 Subject: [PATCH 07/41] Cleaned up some unused code --- utils/stripe_utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index 971b7046..eb2471f8 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -137,17 +137,18 @@ class StripeUtils(object): @handleStripeError def get_or_create_plan(self, amount, name, stripe_plan_id): """ - This function checks if a StripePlan with the given id already exists. If it exists then the function returns - this object otherwise it creates a new StripePlan and returns the new object. + This function checks if a StripePlan with the given stripe_plan_id already exists. If it exists then the + function returns this object otherwise it creates a new StripePlan and returns the new object. :param amount: The amount in CHF :param name: The name of the Stripe plan to be created. - :param stripe_plan_id: The id of the Stripe plan to be created. Use get_stripe_plan_id_string function to obtain the name of the plan to be created + :param stripe_plan_id: The id of the Stripe plan to be created. Use get_stripe_plan_id_string function to obtain + the name of the plan to be created :return: The StripePlan object if it exists or if can create a Plan object with Stripe, None otherwise """ _amount = float(amount) amount = int(_amount * 100) # stripe amount unit, in cents - + stripe_plan_db_obj = None try: stripe_plan_db_obj = StripePlan.objects.get(stripe_plan_id=stripe_plan_id) except StripePlan.DoesNotExist: @@ -160,8 +161,6 @@ class StripeUtils(object): id=stripe_plan_id) if not stripe_plan.get('response_object') and not stripe_plan.get('error'): stripe_plan_db_obj = StripePlan.objects.create(stripe_plan_id=stripe_plan_id) - else: - stripe_plan_db_obj = None except stripe.error.InvalidRequestError as e: if self.PLAN_ALREADY_EXISTS in str(e): logger.debug(self.PLAN_EXISTS_ERROR_MSG.format(stripe_plan_id)) From 2a348c40d1a1f84bb5729305ce2959ce6a3b934b Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 17 Aug 2017 12:42:34 +0200 Subject: [PATCH 08/41] Refactored code and added delete_stripe_plan method --- utils/stripe_utils.py | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index eb2471f8..9973641c 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -59,8 +59,10 @@ class StripeUtils(object): CURRENCY = 'chf' INTERVAL = 'month' SUCCEEDED_STATUS = 'succeeded' - PLAN_ALREADY_EXISTS = 'Plan already exists' + STRIPE_PLAN_ALREADY_EXISTS = 'Plan already exists' + STRIPE_NO_SUCH_PLAN = 'No such plan' PLAN_EXISTS_ERROR_MSG = 'Plan {} exists already.\nCreating a local StripePlan now.' + PLAN_DOES_NOT_EXIST_ERROR_MSG = 'Plan {} does not exist.' def __init__(self): self.stripe = stripe @@ -135,7 +137,7 @@ class StripeUtils(object): return charge @handleStripeError - def get_or_create_plan(self, amount, name, stripe_plan_id): + def get_or_create_stripe_plan(self, amount, name, stripe_plan_id): """ This function checks if a StripePlan with the given stripe_plan_id already exists. If it exists then the function returns this object otherwise it creates a new StripePlan and returns the new object. @@ -144,7 +146,8 @@ class StripeUtils(object): :param name: The name of the Stripe plan to be created. :param stripe_plan_id: The id of the Stripe plan to be created. Use get_stripe_plan_id_string function to obtain the name of the plan to be created - :return: The StripePlan object if it exists or if can create a Plan object with Stripe, None otherwise + :return: The StripePlan object if it exists or if can create a Plan object with Stripe, None otherwise. In case + of a Stripe error, it returns the error dictionary """ _amount = float(amount) amount = int(_amount * 100) # stripe amount unit, in cents @@ -153,20 +156,39 @@ class StripeUtils(object): stripe_plan_db_obj = StripePlan.objects.get(stripe_plan_id=stripe_plan_id) except StripePlan.DoesNotExist: try: - stripe_plan = self.stripe.Plan.create( + self.stripe.Plan.create( amount=amount, interval=self.INTERVAL, name=name, currency=self.CURRENCY, id=stripe_plan_id) - if not stripe_plan.get('response_object') and not stripe_plan.get('error'): - stripe_plan_db_obj = StripePlan.objects.create(stripe_plan_id=stripe_plan_id) + stripe_plan_db_obj = StripePlan.objects.create(stripe_plan_id=stripe_plan_id) except stripe.error.InvalidRequestError as e: - if self.PLAN_ALREADY_EXISTS in str(e): + if self.STRIPE_PLAN_ALREADY_EXISTS in str(e): logger.debug(self.PLAN_EXISTS_ERROR_MSG.format(stripe_plan_id)) stripe_plan_db_obj = StripePlan.objects.create(stripe_plan_id=stripe_plan_id) return stripe_plan_db_obj + @handleStripeError + def delete_stripe_plan(self, stripe_plan_id): + """ + Deletes the Plan in Stripe and also deletes the local db copy of the plan if it exists + + :param stripe_plan_id: The stripe plan id that needs to be deleted + :return: True if the plan was deleted successfully from Stripe, False otherwise. In case of a Stripe error, it + returns the error dictionary + """ + return_value = False + try: + plan = self.stripe.Plan.retrieve(stripe_plan_id) + plan.delete() + return_value = True + StripePlan.objects.filter(stripe_plan_id=stripe_plan_id).all().delete() + except stripe.error.InvalidRequestError as e: + if self.STRIPE_NO_SUCH_PLAN in str(e): + logger.debug(self.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(stripe_plan_id)) + return return_value + @handleStripeError def subscribe_customer_to_plan(self, customer, plans): """ From 1073b25a948418cc01ec72ab5e85afa1036df5f9 Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 17 Aug 2017 12:43:42 +0200 Subject: [PATCH 09/41] Refactored and added test_delete_unexisting_plan_should_fail --- utils/tests.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/utils/tests.py b/utils/tests.py index de117292..e7f1d292 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -131,8 +131,8 @@ class StripePlanTestCase(TestStripeCustomerDescription): stripe_utils = StripeUtils() plan_id_string = stripe_utils.get_stripe_plan_id_string(2, 20, 100, 1) self.assertEqual(plan_id_string, 'dcl-v1-cpu-2-ram-20gb-ssd-100gb') - stripe_plan = stripe_utils.get_or_create_plan(2000, "test plan 1", stripe_plan_id='test-plan-1') - self.assertEqual(stripe_plan.get('error'), None) + stripe_plan = stripe_utils.get_or_create_stripe_plan(2000, "test plan 1", stripe_plan_id='test-plan-1') + self.assertIsNone(stripe_plan.get('error')) self.assertIsInstance(stripe_plan.get('response_object'), StripePlan) @patch('utils.stripe_utils.logger') @@ -140,14 +140,29 @@ class StripePlanTestCase(TestStripeCustomerDescription): stripe_utils = StripeUtils() unique_id = str(uuid.uuid4().hex) new_plan_id_str = 'test-plan-{}'.format(unique_id) - stripe_plan = stripe_utils.get_or_create_plan(2000, "test plan {}".format(unique_id), - stripe_plan_id=new_plan_id_str) + stripe_plan = stripe_utils.get_or_create_stripe_plan(2000, "test plan {}".format(unique_id), + stripe_plan_id=new_plan_id_str) self.assertIsInstance(stripe_plan.get('response_object'), StripePlan) self.assertEqual(stripe_plan.get('response_object').stripe_plan_id, new_plan_id_str) + # Test creating the same plan again and expect the PLAN_EXISTS_ERROR_MSG + + # We first delete the local Stripe Plan StripePlan.objects.filter(stripe_plan_id=new_plan_id_str).all().delete() - stripe_plan_1 = stripe_utils.get_or_create_plan(2000, "test plan {}".format(unique_id), - stripe_plan_id=new_plan_id_str) + stripe_plan_1 = stripe_utils.get_or_create_stripe_plan(2000, "test plan {}".format(unique_id), + stripe_plan_id=new_plan_id_str) mock_logger.debug.assert_called_with(stripe_utils.PLAN_EXISTS_ERROR_MSG.format(new_plan_id_str)) self.assertIsInstance(stripe_plan_1.get('response_object'), StripePlan) self.assertEqual(stripe_plan_1.get('response_object').stripe_plan_id, new_plan_id_str) + + # Delete the test stripe plan that we just created + delete_result = stripe_utils.delete_stripe_plan(new_plan_id_str) + self.assertIsInstance(delete_result, bool) + + @patch('utils.stripe_utils.logger') + def test_delete_unexisting_plan_should_fail(self, mock_logger): + plan_id = 'crazy-plan-id-that-does-not-exist' + stripe_utils = StripeUtils() + result = stripe_utils.delete_stripe_plan(plan_id) + self.assertEqual(result.get('response_object'), False) + mock_logger.debug.assert_called_with(stripe_utils.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(plan_id)) From 3ef34d84a880bfd3e208eef9902bbfa732cb69bf Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 17 Aug 2017 13:02:39 +0200 Subject: [PATCH 10/41] Added some comments and improved the test code --- utils/tests.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/utils/tests.py b/utils/tests.py index e7f1d292..5984d989 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -1,15 +1,16 @@ -from django.test import TestCase -from django.test import Client -from django.http.request import HttpRequest - -from model_mommy import mommy -from utils.stripe_utils import StripeUtils -import stripe -from django.conf import settings -from datacenterlight.models import StripePlan import uuid from unittest.mock import patch +import stripe +from django.conf import settings +from django.http.request import HttpRequest +from django.test import Client +from django.test import TestCase +from model_mommy import mommy + +from datacenterlight.models import StripePlan +from utils.stripe_utils import StripeUtils + class BaseTestCase(TestCase): """ @@ -137,6 +138,15 @@ class StripePlanTestCase(TestStripeCustomerDescription): @patch('utils.stripe_utils.logger') def test_create_duplicate_plans_error_handling(self, mock_logger): + """ + Test details: + 1. Create a test plan in Stripe with a particular id + 2. Try to recreate the plan with the same id + 3. The code should be able to handle this + + :param mock_logger: + :return: + """ stripe_utils = StripeUtils() unique_id = str(uuid.uuid4().hex) new_plan_id_str = 'test-plan-{}'.format(unique_id) @@ -146,8 +156,7 @@ class StripePlanTestCase(TestStripeCustomerDescription): self.assertEqual(stripe_plan.get('response_object').stripe_plan_id, new_plan_id_str) # Test creating the same plan again and expect the PLAN_EXISTS_ERROR_MSG - - # We first delete the local Stripe Plan + # We first delete the local Stripe Plan, so that the code tries to create a new plan in Stripe StripePlan.objects.filter(stripe_plan_id=new_plan_id_str).all().delete() stripe_plan_1 = stripe_utils.get_or_create_stripe_plan(2000, "test plan {}".format(unique_id), stripe_plan_id=new_plan_id_str) @@ -157,12 +166,14 @@ class StripePlanTestCase(TestStripeCustomerDescription): # Delete the test stripe plan that we just created delete_result = stripe_utils.delete_stripe_plan(new_plan_id_str) - self.assertIsInstance(delete_result, bool) + self.assertIsInstance(delete_result, dict) + self.assertEqual(delete_result.get('response_object'), True) @patch('utils.stripe_utils.logger') def test_delete_unexisting_plan_should_fail(self, mock_logger): plan_id = 'crazy-plan-id-that-does-not-exist' stripe_utils = StripeUtils() result = stripe_utils.delete_stripe_plan(plan_id) + self.assertIsInstance(result, dict) self.assertEqual(result.get('response_object'), False) mock_logger.debug.assert_called_with(stripe_utils.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(plan_id)) From ffd0d24f1ad4795faaf647a974be21eb2123ab7b Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 17 Aug 2017 13:23:49 +0200 Subject: [PATCH 11/41] Refactored code --- utils/stripe_utils.py | 13 ++++++------- utils/tests.py | 34 +++++++++++++++++----------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index 9973641c..f0bcf264 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -146,8 +146,8 @@ class StripeUtils(object): :param name: The name of the Stripe plan to be created. :param stripe_plan_id: The id of the Stripe plan to be created. Use get_stripe_plan_id_string function to obtain the name of the plan to be created - :return: The StripePlan object if it exists or if can create a Plan object with Stripe, None otherwise. In case - of a Stripe error, it returns the error dictionary + :return: The StripePlan object if it exists else creates a Plan object in Stripe and a local StripePlan and + returns it. Returns None in case of Stripe error """ _amount = float(amount) amount = int(_amount * 100) # stripe amount unit, in cents @@ -175,8 +175,7 @@ class StripeUtils(object): Deletes the Plan in Stripe and also deletes the local db copy of the plan if it exists :param stripe_plan_id: The stripe plan id that needs to be deleted - :return: True if the plan was deleted successfully from Stripe, False otherwise. In case of a Stripe error, it - returns the error dictionary + :return: True if the plan was deleted successfully from Stripe, False otherwise. """ return_value = False try: @@ -231,6 +230,6 @@ class StripeUtils(object): :param version: The version of the Stripe plans :return: A string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb` """ - DCL_PLAN_STRING = 'cpu-{cpu}-ram-{ram}gb-ssd-{ssd}gb'.format(cpu=cpu, ram=ram, ssd=ssd) - STRIPE_PLAN_ID_STRING = '{app}-v{version}-{plan}'.format(app='dcl', version=version, plan=DCL_PLAN_STRING) - return STRIPE_PLAN_ID_STRING + dcl_plan_string = 'cpu-{cpu}-ram-{ram}gb-ssd-{ssd}gb'.format(cpu=cpu, ram=ram, ssd=ssd) + stripe_plan_id_string = '{app}-v{version}-{plan}'.format(app='dcl', version=version, plan=dcl_plan_string) + return stripe_plan_id_string diff --git a/utils/tests.py b/utils/tests.py index 5984d989..2b08f92c 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -104,7 +104,8 @@ class TestStripeCustomerDescription(TestCase): self.customer.set_password(self.dummy_password) self.customer.email = self.dummy_email self.customer.save() - stripe.api_key = settings.STRIPE_API_PRIVATE_KEY + self.stripe_utils = StripeUtils() + # self.stripe.api_key = settings.STRIPE_API_PRIVATE_KEY def test_creating_stripe_customer(self): test_name = "Monty Python" @@ -116,8 +117,8 @@ class TestStripeCustomerDescription(TestCase): "cvc": '123' }, ) - stripe_utils = StripeUtils() - stripe_data = stripe_utils.create_customer(token.id, self.customer.email, test_name) + + stripe_data = self.stripe_utils.create_customer(token.id, self.customer.email, test_name) self.assertEqual(stripe_data.get('error'), None) customer_data = stripe_data.get('response_object') self.assertEqual(customer_data.description, test_name) @@ -128,11 +129,12 @@ class StripePlanTestCase(TestStripeCustomerDescription): A class to test Stripe plans """ - def test_get_or_create_plan(self): - stripe_utils = StripeUtils() - plan_id_string = stripe_utils.get_stripe_plan_id_string(2, 20, 100, 1) + def test_get_stripe_plan_id_string(self): + plan_id_string = StripeUtils.get_stripe_plan_id_string(cpu=2, ram=20, ssd=100, version=1) self.assertEqual(plan_id_string, 'dcl-v1-cpu-2-ram-20gb-ssd-100gb') - stripe_plan = stripe_utils.get_or_create_stripe_plan(2000, "test plan 1", stripe_plan_id='test-plan-1') + + def test_get_or_create_plan(self): + stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, "test plan 1", stripe_plan_id='test-plan-1') self.assertIsNone(stripe_plan.get('error')) self.assertIsInstance(stripe_plan.get('response_object'), StripePlan) @@ -147,33 +149,31 @@ class StripePlanTestCase(TestStripeCustomerDescription): :param mock_logger: :return: """ - stripe_utils = StripeUtils() unique_id = str(uuid.uuid4().hex) new_plan_id_str = 'test-plan-{}'.format(unique_id) - stripe_plan = stripe_utils.get_or_create_stripe_plan(2000, "test plan {}".format(unique_id), - stripe_plan_id=new_plan_id_str) + stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, "test plan {}".format(unique_id), + stripe_plan_id=new_plan_id_str) self.assertIsInstance(stripe_plan.get('response_object'), StripePlan) self.assertEqual(stripe_plan.get('response_object').stripe_plan_id, new_plan_id_str) # Test creating the same plan again and expect the PLAN_EXISTS_ERROR_MSG # We first delete the local Stripe Plan, so that the code tries to create a new plan in Stripe StripePlan.objects.filter(stripe_plan_id=new_plan_id_str).all().delete() - stripe_plan_1 = stripe_utils.get_or_create_stripe_plan(2000, "test plan {}".format(unique_id), - stripe_plan_id=new_plan_id_str) - mock_logger.debug.assert_called_with(stripe_utils.PLAN_EXISTS_ERROR_MSG.format(new_plan_id_str)) + stripe_plan_1 = self.stripe_utils.get_or_create_stripe_plan(2000, "test plan {}".format(unique_id), + stripe_plan_id=new_plan_id_str) + mock_logger.debug.assert_called_with(self.stripe_utils.PLAN_EXISTS_ERROR_MSG.format(new_plan_id_str)) self.assertIsInstance(stripe_plan_1.get('response_object'), StripePlan) self.assertEqual(stripe_plan_1.get('response_object').stripe_plan_id, new_plan_id_str) # Delete the test stripe plan that we just created - delete_result = stripe_utils.delete_stripe_plan(new_plan_id_str) + delete_result = self.stripe_utils.delete_stripe_plan(new_plan_id_str) self.assertIsInstance(delete_result, dict) self.assertEqual(delete_result.get('response_object'), True) @patch('utils.stripe_utils.logger') def test_delete_unexisting_plan_should_fail(self, mock_logger): plan_id = 'crazy-plan-id-that-does-not-exist' - stripe_utils = StripeUtils() - result = stripe_utils.delete_stripe_plan(plan_id) + result = self.stripe_utils.delete_stripe_plan(plan_id) self.assertIsInstance(result, dict) self.assertEqual(result.get('response_object'), False) - mock_logger.debug.assert_called_with(stripe_utils.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(plan_id)) + mock_logger.debug.assert_called_with(self.stripe_utils.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(plan_id)) From 0bc1766e4a3dd8c43ca702fc8554b4d14070f690 Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 17 Aug 2017 15:32:13 +0200 Subject: [PATCH 12/41] Refactored tests and added test_subscribe_customer_to_plan --- utils/tests.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/utils/tests.py b/utils/tests.py index 2b08f92c..827dad0a 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -2,13 +2,13 @@ import uuid from unittest.mock import patch import stripe -from django.conf import settings from django.http.request import HttpRequest from django.test import Client from django.test import TestCase from model_mommy import mommy from datacenterlight.models import StripePlan +from membership.models import StripeCustomer from utils.stripe_utils import StripeUtils @@ -98,18 +98,15 @@ class TestStripeCustomerDescription(TestCase): """ def setUp(self): - self.dummy_password = 'test_password' - self.dummy_email = 'test@ungleich.ch' + self.customer_password = 'test_password' + self.customer_email = 'test@ungleich.ch' + self.customer_name = "Monty Python" self.customer = mommy.make('membership.CustomUser') - self.customer.set_password(self.dummy_password) - self.customer.email = self.dummy_email + self.customer.set_password(self.customer_password) + self.customer.email = self.customer_email self.customer.save() self.stripe_utils = StripeUtils() - # self.stripe.api_key = settings.STRIPE_API_PRIVATE_KEY - - def test_creating_stripe_customer(self): - test_name = "Monty Python" - token = stripe.Token.create( + self.token = stripe.Token.create( card={ "number": '4111111111111111', "exp_month": 12, @@ -118,10 +115,11 @@ class TestStripeCustomerDescription(TestCase): }, ) - stripe_data = self.stripe_utils.create_customer(token.id, self.customer.email, test_name) + def test_creating_stripe_customer(self): + stripe_data = self.stripe_utils.create_customer(self.token.id, self.customer.email, self.customer_name) self.assertEqual(stripe_data.get('error'), None) customer_data = stripe_data.get('response_object') - self.assertEqual(customer_data.description, test_name) + self.assertEqual(customer_data.description, self.customer_name) class StripePlanTestCase(TestStripeCustomerDescription): @@ -144,7 +142,7 @@ class StripePlanTestCase(TestStripeCustomerDescription): Test details: 1. Create a test plan in Stripe with a particular id 2. Try to recreate the plan with the same id - 3. The code should be able to handle this + 3. This creates a Stripe error, the code should be able to handle the error :param mock_logger: :return: @@ -177,3 +175,13 @@ class StripePlanTestCase(TestStripeCustomerDescription): self.assertIsInstance(result, dict) self.assertEqual(result.get('response_object'), False) mock_logger.debug.assert_called_with(self.stripe_utils.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(plan_id)) + + def test_subscribe_customer_to_plan(self): + stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, "test plan 1", stripe_plan_id='test-plan-1') + stripe_customer = StripeCustomer.get_or_create(email=self.customer_email, + token=self.token) + result = self.stripe_utils.subscribe_customer_to_plan(stripe_customer.stripe_id, + [{"plan": stripe_plan.get( + 'response_object').stripe_plan_id}]) + self.assertIsInstance(result.get('response_object'), stripe.Subscription) + self.assertIsNone(result.get('error')) From 5c92e47ffe7a62b869c97c365852638bd772c52d Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 17 Aug 2017 16:42:32 +0200 Subject: [PATCH 13/41] Added card that fails and a new test case to test failed payment --- utils/tests.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/utils/tests.py b/utils/tests.py index 827dad0a..d595f307 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -114,6 +114,14 @@ class TestStripeCustomerDescription(TestCase): "cvc": '123' }, ) + self.failed_token = stripe.Token.create( + card={ + "number": '4000000000000341', + "exp_month": 12, + "exp_year": 2022, + "cvc": '123' + }, + ) def test_creating_stripe_customer(self): stripe_data = self.stripe_utils.create_customer(self.token.id, self.customer.email, self.customer_name) @@ -185,3 +193,15 @@ class StripePlanTestCase(TestStripeCustomerDescription): 'response_object').stripe_plan_id}]) self.assertIsInstance(result.get('response_object'), stripe.Subscription) self.assertIsNone(result.get('error')) + self.assertEqual(result.get('response_object').get('status'), 'active') + + def test_subscribe_customer_to_plan_failed_payment(self): + stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, "test plan 1", stripe_plan_id='test-plan-1') + stripe_customer = StripeCustomer.get_or_create(email=self.customer_email, + token=self.failed_token) + result = self.stripe_utils.subscribe_customer_to_plan(stripe_customer.stripe_id, + [{"plan": stripe_plan.get( + 'response_object').stripe_plan_id}]) + print(result) + self.assertIsNone(result.get('response_object'), None) + self.assertIsNotNone(result.get('error')) From ecbbe1eb6ddc1d50aee8e30fba0a59353392911f Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 17 Aug 2017 18:16:36 +0200 Subject: [PATCH 14/41] Added subscription_id field and reformatted the code --- hosting/models.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/hosting/models.py b/hosting/models.py index 88386913..62717a79 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -1,16 +1,13 @@ -import os import logging - +import os +from Crypto.PublicKey import RSA from django.db import models from django.utils.functional import cached_property - -from Crypto.PublicKey import RSA - from membership.models import StripeCustomer, CustomUser -from utils.models import BillingAddress from utils.mixins import AssignPermissionsMixin +from utils.models import BillingAddress logger = logging.getLogger(__name__) @@ -42,7 +39,6 @@ class HostingPlan(models.Model): class HostingOrder(AssignPermissionsMixin, models.Model): - ORDER_APPROVED_STATUS = 'Approved' ORDER_DECLINED_STATUS = 'Declined' @@ -55,6 +51,7 @@ class HostingOrder(AssignPermissionsMixin, models.Model): cc_brand = models.CharField(max_length=10) stripe_charge_id = models.CharField(max_length=100, null=True) price = models.FloatField() + subscription_id = models.CharField(max_length=100, null=True) permissions = ('view_hostingorder',) @@ -91,6 +88,16 @@ class HostingOrder(AssignPermissionsMixin, models.Model): self.cc_brand = stripe_charge.source.brand self.save() + def set_subscription_id(self, subscription_object): + """ + When creating a subscription, we have subscription id. We store this in the subscription_id field + + :param subscription_object: Stripe's subscription object + :return: + """ + self.subscription_id = subscription_object.id + self.save() + def get_cc_data(self): return { 'last4': self.last4, @@ -101,7 +108,7 @@ class HostingOrder(AssignPermissionsMixin, models.Model): class UserHostingKey(models.Model): user = models.ForeignKey(CustomUser) public_key = models.TextField() - private_key = models.FileField(upload_to='private_keys', blank=True) + private_key = models.FileField(upload_to='private_keys', blank=True) created_at = models.DateTimeField(auto_now_add=True) name = models.CharField(max_length=100) From b0b1b6091a57f480bde0ff99428d387ee44a1625 Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 17 Aug 2017 18:17:52 +0200 Subject: [PATCH 15/41] Refactored code and added app as a parameter in get_stripe_plan_id --- utils/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/tests.py b/utils/tests.py index d595f307..2062ac6a 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -136,7 +136,7 @@ class StripePlanTestCase(TestStripeCustomerDescription): """ def test_get_stripe_plan_id_string(self): - plan_id_string = StripeUtils.get_stripe_plan_id_string(cpu=2, ram=20, ssd=100, version=1) + plan_id_string = StripeUtils.get_stripe_plan_id(cpu=2, ram=20, ssd=100, version=1, app='dcl') self.assertEqual(plan_id_string, 'dcl-v1-cpu-2-ram-20gb-ssd-100gb') def test_get_or_create_plan(self): From 5fe6007ad2f7de82e8b3d515e05ef3d8125b06d4 Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 17 Aug 2017 18:21:53 +0200 Subject: [PATCH 16/41] Refactored code and added app as a parameter in get_stripe_plan_id --- utils/stripe_utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index f0bcf264..53262816 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -1,7 +1,9 @@ +import logging + import stripe from django.conf import settings + from datacenterlight.models import StripePlan -import logging stripe.api_key = settings.STRIPE_API_PRIVATE_KEY logger = logging.getLogger(__name__) @@ -220,7 +222,7 @@ class StripeUtils(object): return charge @staticmethod - def get_stripe_plan_id_string(cpu, ram, ssd, version): + def get_stripe_plan_id(cpu, ram, ssd, version, app): """ Returns the stripe plan id string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters @@ -231,5 +233,5 @@ class StripeUtils(object): :return: A string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb` """ dcl_plan_string = 'cpu-{cpu}-ram-{ram}gb-ssd-{ssd}gb'.format(cpu=cpu, ram=ram, ssd=ssd) - stripe_plan_id_string = '{app}-v{version}-{plan}'.format(app='dcl', version=version, plan=dcl_plan_string) + stripe_plan_id_string = '{app}-v{version}-{plan}'.format(app=app, version=version, plan=dcl_plan_string) return stripe_plan_id_string From 171a463261279525f1b2dc747469f1cff7c466b6 Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 17 Aug 2017 18:24:10 +0200 Subject: [PATCH 17/41] Updated dcl views.py to create subscriptions instead of one time charge --- datacenterlight/views.py | 75 ++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/datacenterlight/views.py b/datacenterlight/views.py index 0851a33f..39bda8bb 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -1,26 +1,28 @@ -from django.views.generic import FormView, CreateView, TemplateView, DetailView -from django.http import HttpResponseRedirect -from .forms import BetaAccessForm -from .models import BetaAccess, BetaAccessVMType, BetaAccessVM, VMTemplate -from django.contrib import messages -from django.core.urlresolvers import reverse -from django.core.mail import EmailMessage -from utils.mailer import BaseEmail -from django.shortcuts import render -from django.shortcuts import redirect -from django import forms -from django.core.exceptions import ValidationError -from django.views.decorators.cache import cache_control -from django.conf import settings -from django.utils.translation import ugettext_lazy as _ -from utils.forms import BillingAddressForm, UserBillingAddressForm -from utils.models import BillingAddress -from hosting.models import HostingOrder, HostingBill -from utils.stripe_utils import StripeUtils from datetime import datetime + +from django import forms +from django.conf import settings +from django.contrib import messages +from django.core.exceptions import ValidationError +from django.core.mail import EmailMessage +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect +from django.shortcuts import redirect +from django.shortcuts import render +from django.utils.translation import ugettext_lazy as _ +from django.views.decorators.cache import cache_control +from django.views.generic import FormView, CreateView, TemplateView, DetailView + +from hosting.models import HostingOrder, HostingBill from membership.models import CustomUser, StripeCustomer from opennebula_api.models import OpenNebulaManager from opennebula_api.serializers import VirtualMachineTemplateSerializer, VirtualMachineSerializer, VMTemplateSerializer +from utils.forms import BillingAddressForm, UserBillingAddressForm +from utils.mailer import BaseEmail +from utils.models import BillingAddress +from utils.stripe_utils import StripeUtils +from .forms import BetaAccessForm +from .models import BetaAccess, BetaAccessVMType, BetaAccessVM, VMTemplate class LandingProgramView(TemplateView): @@ -460,19 +462,32 @@ class OrderConfirmationView(DetailView): # Make stripe charge to a customer stripe_utils = StripeUtils() - charge_response = stripe_utils.make_charge(amount=final_price, - customer=customer.stripe_id) - charge = charge_response.get('response_object') - - # Check if the payment was approved - if not charge: + cpu = specs.get('cpu') + memory = specs.get('memory') + disk_size = specs.get('disk_size') + amount_to_be_charged = (cpu * 5) + (memory * 2) + (disk_size * 0.6) + plan_name = "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format(cpu=cpu, + memory=memory, + disk_size=disk_size) + stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu, + ram=memory, + ssd=disk_size, + version=1, + app='dcl') + stripe_plan = stripe_utils.get_or_create_stripe_plan(amount=amount_to_be_charged, + name=plan_name, + stripe_plan_id=stripe_plan_id) + subscription_result = stripe_utils.subscribe_customer_to_plan(customer.stripe_id, + [{"plan": stripe_plan.get( + 'response_object').stripe_plan_id}]) + response_object = subscription_result.get('response_object') + if response_object is None: context = {} context.update({ - 'paymentError': charge_response.get('error') + 'paymentError': response_object.get('error') }) return render(request, self.payment_template_name, context) - charge = charge_response.get('response_object') # Create OpenNebulaManager manager = OpenNebulaManager(email=settings.OPENNEBULA_USERNAME, @@ -511,10 +526,10 @@ class OrderConfirmationView(DetailView): billing_address_user_form.is_valid() billing_address_user_form.save() - # Associate an order with a stripe payment - order.set_stripe_charge(charge) + # Associate an order with a stripe subscription + order.set_subscription_id(response_object) - # If the Stripe payment was successed, set order status approved + # If the Stripe payment succeeded, set order status approved order.set_approved() vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data From fcaa5e31dd80f250adafede561e9dcafe259e6b7 Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 17 Aug 2017 18:31:27 +0200 Subject: [PATCH 18/41] Added condition that the status should be 'active' for new subscription --- datacenterlight/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/datacenterlight/views.py b/datacenterlight/views.py index 39bda8bb..0fa7e81b 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -481,14 +481,13 @@ class OrderConfirmationView(DetailView): [{"plan": stripe_plan.get( 'response_object').stripe_plan_id}]) response_object = subscription_result.get('response_object') - if response_object is None: + if response_object is None or response_object.status is not 'active': context = {} context.update({ 'paymentError': response_object.get('error') }) return render(request, self.payment_template_name, context) - # Create OpenNebulaManager manager = OpenNebulaManager(email=settings.OPENNEBULA_USERNAME, password=settings.OPENNEBULA_PASSWORD) From 3445579a8edc0e52731ad31cc080d4bdd24843ea Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 17 Aug 2017 18:36:14 +0200 Subject: [PATCH 19/41] Added 0042_hostingorder_subscription_id.py migration file --- .../0042_hostingorder_subscription_id.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 hosting/migrations/0042_hostingorder_subscription_id.py diff --git a/hosting/migrations/0042_hostingorder_subscription_id.py b/hosting/migrations/0042_hostingorder_subscription_id.py new file mode 100644 index 00000000..2aa634a8 --- /dev/null +++ b/hosting/migrations/0042_hostingorder_subscription_id.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-08-17 16:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0041_userhostingkey_private_key'), + ] + + operations = [ + migrations.AddField( + model_name='hostingorder', + name='subscription_id', + field=models.CharField(max_length=100, null=True), + ), + ] From 7e1bfc9fab822414198533dcd744fc6a3207015d Mon Sep 17 00:00:00 2001 From: PCoder Date: Fri, 18 Aug 2017 00:47:34 +0530 Subject: [PATCH 20/41] Removed a print --- utils/tests.py | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/tests.py b/utils/tests.py index 2062ac6a..3921756d 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -202,6 +202,5 @@ class StripePlanTestCase(TestStripeCustomerDescription): result = self.stripe_utils.subscribe_customer_to_plan(stripe_customer.stripe_id, [{"plan": stripe_plan.get( 'response_object').stripe_plan_id}]) - print(result) self.assertIsNone(result.get('response_object'), None) self.assertIsNotNone(result.get('error')) From 35736ae593447c32f6d49ab0dcae04bbafc0ab51 Mon Sep 17 00:00:00 2001 From: PCoder Date: Fri, 18 Aug 2017 01:49:27 +0530 Subject: [PATCH 21/41] Refactored some code --- hosting/models.py | 3 ++- utils/stripe_utils.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/hosting/models.py b/hosting/models.py index 62717a79..3aa27fbe 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -90,7 +90,8 @@ class HostingOrder(AssignPermissionsMixin, models.Model): def set_subscription_id(self, subscription_object): """ - When creating a subscription, we have subscription id. We store this in the subscription_id field + When creating a Stripe subscription, we have subscription id. We store this in the subscription_id field. + This method sets the subscription id from subscription_object. :param subscription_object: Stripe's subscription object :return: diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index 36056fac..da1594d3 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -1,8 +1,6 @@ import logging - import stripe from django.conf import settings - from datacenterlight.models import StripePlan stripe.api_key = settings.STRIPE_API_PRIVATE_KEY From 29d36faef099fede9f896f573f2d9b3c837d9d6c Mon Sep 17 00:00:00 2001 From: PCoder Date: Mon, 21 Aug 2017 00:41:46 +0530 Subject: [PATCH 22/41] Refactored some code --- hosting/models.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hosting/models.py b/hosting/models.py index 3aa27fbe..a1951bc6 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -1,13 +1,12 @@ +import os import logging -import os -from Crypto.PublicKey import RSA from django.db import models from django.utils.functional import cached_property - +from Crypto.PublicKey import RSA from membership.models import StripeCustomer, CustomUser -from utils.mixins import AssignPermissionsMixin from utils.models import BillingAddress +from utils.mixins import AssignPermissionsMixin logger = logging.getLogger(__name__) From 506b5a904a00f90db72448e5afe18be441fd56e1 Mon Sep 17 00:00:00 2001 From: PCoder Date: Mon, 21 Aug 2017 01:54:55 +0530 Subject: [PATCH 23/41] Added missing code from the merge --- datacenterlight/tasks.py | 4 ++-- datacenterlight/views.py | 39 +++++++++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/datacenterlight/tasks.py b/datacenterlight/tasks.py index b897c54a..3cab0ea0 100644 --- a/datacenterlight/tasks.py +++ b/datacenterlight/tasks.py @@ -89,9 +89,9 @@ def create_vm_task(self, vm_template_id, user, specs, template, stripe_customer_ billing_address_user_form.is_valid() billing_address_user_form.save() - # Associate an order with a stripe payment + # Associate an order with a stripe subscription charge_object = DictDotLookup(charge) - order.set_stripe_charge(charge_object) + order.set_subscription_id(charge_object) # If the Stripe payment succeeds, set order status approved order.set_approved() diff --git a/datacenterlight/views.py b/datacenterlight/views.py index db7f2e53..9761b471 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -444,22 +444,37 @@ class OrderConfirmationView(DetailView): billing_address_data = request.session.get('billing_address_data') billing_address_id = request.session.get('billing_address') vm_template_id = template.get('id', 1) - final_price = specs.get('price') # Make stripe charge to a customer stripe_utils = StripeUtils() - charge_response = stripe_utils.make_charge(amount=final_price, - customer=customer.stripe_id) - - # Check if the payment was approved - if not charge_response.get('response_object') and not charge_response.get('paid'): - msg = charge_response.get('error') - messages.add_message(self.request, messages.ERROR, msg, extra_tags='make_charge_error') - return HttpResponseRedirect(reverse('datacenterlight:payment') + '#payment_error') - - charge = charge_response.get('response_object') + cpu = specs.get('cpu') + memory = specs.get('memory') + disk_size = specs.get('disk_size') + amount_to_be_charged = (cpu * 5) + (memory * 2) + (disk_size * 0.6) + plan_name = "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format(cpu=cpu, + memory=memory, + disk_size=disk_size) + stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu, + ram=memory, + ssd=disk_size, + version=1, + app='dcl') + stripe_plan = stripe_utils.get_or_create_stripe_plan(amount=amount_to_be_charged, + name=plan_name, + stripe_plan_id=stripe_plan_id) + subscription_result = stripe_utils.subscribe_customer_to_plan(customer.stripe_id, + [{"plan": stripe_plan.get( + 'response_object').stripe_plan_id}]) + response_object = subscription_result.get('response_object') + # Check if the subscription was approved and is active + if response_object is None or response_object.status is not 'active': + context = {} + context.update({ + 'paymentError': response_object.get('error') + }) + return render(request, self.payment_template_name, context) create_vm_task.delay(vm_template_id, user, specs, template, stripe_customer_id, billing_address_data, billing_address_id, - charge) + response_object) request.session['order_confirmation'] = True return HttpResponseRedirect(reverse('datacenterlight:order_success')) From 83df42b5dfa893ee9bbdaffbab81dd982c258aff Mon Sep 17 00:00:00 2001 From: PCoder Date: Tue, 22 Aug 2017 00:07:58 +0530 Subject: [PATCH 24/41] Fixed checking active status of subscription + redirection to payment page on error --- datacenterlight/views.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/datacenterlight/views.py b/datacenterlight/views.py index 9761b471..91dbf900 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -467,12 +467,10 @@ class OrderConfirmationView(DetailView): 'response_object').stripe_plan_id}]) response_object = subscription_result.get('response_object') # Check if the subscription was approved and is active - if response_object is None or response_object.status is not 'active': - context = {} - context.update({ - 'paymentError': response_object.get('error') - }) - return render(request, self.payment_template_name, context) + if response_object is None or response_object.status != 'active': + msg = subscription_result.get('error') + messages.add_message(self.request, messages.ERROR, msg, extra_tags='failed_payment') + return HttpResponseRedirect(reverse('datacenterlight:payment') + '#payment_error') create_vm_task.delay(vm_template_id, user, specs, template, stripe_customer_id, billing_address_data, billing_address_id, response_object) From 487ab6b9278b4fac4ab8b405b79e84c8d4673312 Mon Sep 17 00:00:00 2001 From: PCoder Date: Tue, 22 Aug 2017 01:55:45 +0530 Subject: [PATCH 25/41] Changed the subscription plan id field size to 256 characters --- .../migrations/0008_auto_20170821_2024.py | 20 +++++++++++++++++++ datacenterlight/models.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 datacenterlight/migrations/0008_auto_20170821_2024.py diff --git a/datacenterlight/migrations/0008_auto_20170821_2024.py b/datacenterlight/migrations/0008_auto_20170821_2024.py new file mode 100644 index 00000000..5357a404 --- /dev/null +++ b/datacenterlight/migrations/0008_auto_20170821_2024.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-08-21 20:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0007_stripeplan'), + ] + + operations = [ + migrations.AlterField( + model_name='stripeplan', + name='stripe_plan_id', + field=models.CharField(max_length=256, null=True), + ), + ] diff --git a/datacenterlight/models.py b/datacenterlight/models.py index d8237ed2..f7b50a01 100644 --- a/datacenterlight/models.py +++ b/datacenterlight/models.py @@ -65,7 +65,7 @@ class StripePlan(models.Model): """ A model to store Data Center Light's created Stripe plans """ - stripe_plan_id = models.CharField(max_length=100, null=True) + stripe_plan_id = models.CharField(max_length=256, null=True) @classmethod def create(cls, stripe_plan_id): From 9eecfbda643d396b5724119798e1abcbfcfbe339 Mon Sep 17 00:00:00 2001 From: PCoder Date: Tue, 22 Aug 2017 02:12:43 +0530 Subject: [PATCH 26/41] Added hdd parameter as an optional parameter to the subscription plan id --- utils/stripe_utils.py | 8 ++++++-- utils/tests.py | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index da1594d3..92e1dcd3 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -220,16 +220,20 @@ class StripeUtils(object): return charge @staticmethod - def get_stripe_plan_id(cpu, ram, ssd, version, app): + def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None): """ Returns the stripe plan id string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters :param cpu: The number of cores :param ram: The size of the RAM in GB - :param ssd: The size of storage in GB + :param ssd: The size of ssd storage in GB + :param hdd: The size of hdd storage in GB :param version: The version of the Stripe plans + :param app: The application to which the stripe plan belongs to. By default it is 'dcl' :return: A string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb` """ dcl_plan_string = 'cpu-{cpu}-ram-{ram}gb-ssd-{ssd}gb'.format(cpu=cpu, ram=ram, ssd=ssd) + if hdd is not None: + dcl_plan_string = '{dcl_plan_string}-hdd-{hdd}gb'.format(dcl_plan_string=dcl_plan_string, hdd=hdd) stripe_plan_id_string = '{app}-v{version}-{plan}'.format(app=app, version=version, plan=dcl_plan_string) return stripe_plan_id_string diff --git a/utils/tests.py b/utils/tests.py index 3921756d..2da1edf2 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -138,6 +138,8 @@ class StripePlanTestCase(TestStripeCustomerDescription): def test_get_stripe_plan_id_string(self): plan_id_string = StripeUtils.get_stripe_plan_id(cpu=2, ram=20, ssd=100, version=1, app='dcl') self.assertEqual(plan_id_string, 'dcl-v1-cpu-2-ram-20gb-ssd-100gb') + plan_id_string = StripeUtils.get_stripe_plan_id(cpu=2, ram=20, ssd=100, version=1, app='dcl', hdd=200) + self.assertEqual(plan_id_string, 'dcl-v1-cpu-2-ram-20gb-ssd-100gb-hdd-200gb') def test_get_or_create_plan(self): stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, "test plan 1", stripe_plan_id='test-plan-1') From b5e488e326c5b462076cdf6735c6f8bf3b5fc9ff Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 24 Aug 2017 10:31:57 +0530 Subject: [PATCH 27/41] Formatted utils/stripe_utils.py --- utils/stripe_utils.py | 81 +++++++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index 92e1dcd3..f35a6b9c 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -30,7 +30,8 @@ def handleStripeError(f): response.update({'error': err['message']}) return response except stripe.error.RateLimitError as e: - response.update({'error': "Too many requests made to the API too quickly"}) + response.update( + {'error': "Too many requests made to the API too quickly"}) return response except stripe.error.InvalidRequestError as e: response.update({'error': "Invalid parameters"}) @@ -104,7 +105,8 @@ class StripeUtils(object): customer = stripe.Customer.retrieve(id) except stripe.InvalidRequestError: customer = self.create_customer(token, user.email, user.name) - user.stripecustomer.stripe_id = customer.get('response_object').get('id') + user.stripecustomer.stripe_id = customer.get( + 'response_object').get('id') user.stripecustomer.save() return customer @@ -139,21 +141,26 @@ class StripeUtils(object): @handleStripeError def get_or_create_stripe_plan(self, amount, name, stripe_plan_id): """ - This function checks if a StripePlan with the given stripe_plan_id already exists. If it exists then the - function returns this object otherwise it creates a new StripePlan and returns the new object. + This function checks if a StripePlan with the given + stripe_plan_id already exists. If it exists then the function + returns this object otherwise it creates a new StripePlan and + returns the new object. :param amount: The amount in CHF :param name: The name of the Stripe plan to be created. - :param stripe_plan_id: The id of the Stripe plan to be created. Use get_stripe_plan_id_string function to obtain - the name of the plan to be created - :return: The StripePlan object if it exists else creates a Plan object in Stripe and a local StripePlan and - returns it. Returns None in case of Stripe error + :param stripe_plan_id: The id of the Stripe plan to be + created. Use get_stripe_plan_id_string function to + obtain the name of the plan to be created + :return: The StripePlan object if it exists else creates a + Plan object in Stripe and a local StripePlan and + returns it. Returns None in case of Stripe error """ _amount = float(amount) amount = int(_amount * 100) # stripe amount unit, in cents stripe_plan_db_obj = None try: - stripe_plan_db_obj = StripePlan.objects.get(stripe_plan_id=stripe_plan_id) + stripe_plan_db_obj = StripePlan.objects.get( + stripe_plan_id=stripe_plan_id) except StripePlan.DoesNotExist: try: self.stripe.Plan.create( @@ -162,30 +169,38 @@ class StripeUtils(object): name=name, currency=self.CURRENCY, id=stripe_plan_id) - stripe_plan_db_obj = StripePlan.objects.create(stripe_plan_id=stripe_plan_id) + stripe_plan_db_obj = StripePlan.objects.create( + stripe_plan_id=stripe_plan_id) except stripe.error.InvalidRequestError as e: if self.STRIPE_PLAN_ALREADY_EXISTS in str(e): - logger.debug(self.PLAN_EXISTS_ERROR_MSG.format(stripe_plan_id)) - stripe_plan_db_obj = StripePlan.objects.create(stripe_plan_id=stripe_plan_id) + logger.debug( + self.PLAN_EXISTS_ERROR_MSG.format(stripe_plan_id)) + stripe_plan_db_obj = StripePlan.objects.create( + stripe_plan_id=stripe_plan_id) return stripe_plan_db_obj @handleStripeError def delete_stripe_plan(self, stripe_plan_id): """ - Deletes the Plan in Stripe and also deletes the local db copy of the plan if it exists + Deletes the Plan in Stripe and also deletes the local db copy + of the plan if it exists - :param stripe_plan_id: The stripe plan id that needs to be deleted - :return: True if the plan was deleted successfully from Stripe, False otherwise. + :param stripe_plan_id: The stripe plan id that needs to be + deleted + :return: True if the plan was deleted successfully from + Stripe, False otherwise. """ return_value = False try: plan = self.stripe.Plan.retrieve(stripe_plan_id) plan.delete() return_value = True - StripePlan.objects.filter(stripe_plan_id=stripe_plan_id).all().delete() + StripePlan.objects.filter( + stripe_plan_id=stripe_plan_id).all().delete() except stripe.error.InvalidRequestError as e: if self.STRIPE_NO_SUCH_PLAN in str(e): - logger.debug(self.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(stripe_plan_id)) + logger.debug( + self.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(stripe_plan_id)) return return_value @handleStripeError @@ -194,13 +209,14 @@ class StripeUtils(object): Subscribes the given customer to the list of given plans :param customer: The stripe customer identifier - :param plans: A list of stripe plans. Ref: https://stripe.com/docs/api/python#create_subscription-items - e.g. - plans = [ - { - "plan": "dcl-v1-cpu-2-ram-5gb-ssd-10gb", - }, - ] + :param plans: A list of stripe plans. + Ref: https://stripe.com/docs/api/python#create_subscription-items + e.g. + plans = [ + { + "plan": "dcl-v1-cpu-2-ram-5gb-ssd-10gb", + }, + ] :return: The subscription StripeObject """ @@ -222,18 +238,25 @@ class StripeUtils(object): @staticmethod def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None): """ - Returns the stripe plan id string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters + Returns the stripe plan id string of the form + `dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters :param cpu: The number of cores :param ram: The size of the RAM in GB :param ssd: The size of ssd storage in GB :param hdd: The size of hdd storage in GB :param version: The version of the Stripe plans - :param app: The application to which the stripe plan belongs to. By default it is 'dcl' + :param app: The application to which the stripe plan belongs + to. By default it is 'dcl' :return: A string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb` """ - dcl_plan_string = 'cpu-{cpu}-ram-{ram}gb-ssd-{ssd}gb'.format(cpu=cpu, ram=ram, ssd=ssd) + dcl_plan_string = 'cpu-{cpu}-ram-{ram}gb-ssd-{ssd}gb'.format(cpu=cpu, + ram=ram, + ssd=ssd) if hdd is not None: - dcl_plan_string = '{dcl_plan_string}-hdd-{hdd}gb'.format(dcl_plan_string=dcl_plan_string, hdd=hdd) - stripe_plan_id_string = '{app}-v{version}-{plan}'.format(app=app, version=version, plan=dcl_plan_string) + dcl_plan_string = '{dcl_plan_string}-hdd-{hdd}gb'.format( + dcl_plan_string=dcl_plan_string, hdd=hdd) + stripe_plan_id_string = '{app}-v{version}-{plan}'.format(app=app, + version=version, + plan=dcl_plan_string) return stripe_plan_id_string From 0256b0bf03f283294ed5b4612c063d2e13dfb1fe Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 24 Aug 2017 11:49:41 +0530 Subject: [PATCH 28/41] Added credit card details as param for create_vm_task --- datacenterlight/tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datacenterlight/tasks.py b/datacenterlight/tasks.py index 69929ed9..1e3e1caa 100644 --- a/datacenterlight/tasks.py +++ b/datacenterlight/tasks.py @@ -44,7 +44,7 @@ def retry_task(task, exception=None): def create_vm_task(self, vm_template_id, user, specs, template, stripe_customer_id, billing_address_data, billing_address_id, - charge): + charge, cc_details): vm_id = None try: final_price = specs.get('price') @@ -93,7 +93,7 @@ def create_vm_task(self, vm_template_id, user, specs, template, # Associate an order with a stripe subscription charge_object = DictDotLookup(charge) - order.set_subscription_id(charge_object) + order.set_subscription_id(charge_object, cc_details) # If the Stripe payment succeeds, set order status approved order.set_approved() From 311125567322215bf9f662477b67ac66c2f854a8 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 24 Aug 2017 11:50:43 +0530 Subject: [PATCH 29/41] Updated test_create_vm_task for monthly subscriptions --- datacenterlight/tests.py | 56 ++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/datacenterlight/tests.py b/datacenterlight/tests.py index b0768c9a..c993bf79 100644 --- a/datacenterlight/tests.py +++ b/datacenterlight/tests.py @@ -50,7 +50,12 @@ class CeleryTaskTestCase(TestCase): call_command('fetchvmtemplates') def test_create_vm_task(self): - """Tests the create vm task.""" + """Tests the create vm task for monthly subscription + + This test is supposed to validate the proper execution + of celery create_vm_task on production, as we have no + other way to do this. + """ # We create a VM from the first template available to DCL vm_template = VMTemplate.objects.all().first() @@ -60,13 +65,16 @@ class CeleryTaskTestCase(TestCase): specs = { 'cpu': 1, 'memory': 2, - 'disk_size': 10, - 'price': 15, + 'disk_size': 10 } stripe_customer = StripeCustomer.get_or_create( email=self.customer_email, token=self.token) + card_details = self.stripe_utils.get_card_details( + stripe_customer.stripe_id, + self.token) + card_details_dict = card_details.get('response_object') billing_address = BillingAddress( cardholder_name=self.customer_name, postal_code='1232', @@ -83,28 +91,44 @@ class CeleryTaskTestCase(TestCase): billing_address_id = billing_address.id vm_template_id = template_data.get('id', 1) - final_price = specs.get('price') - # Make stripe charge to a customer - stripe_utils = StripeUtils() - charge_response = stripe_utils.make_charge( - amount=final_price, - customer=stripe_customer.stripe_id) + cpu = specs.get('cpu') + memory = specs.get('memory') + disk_size = specs.get('disk_size') + amount_to_be_charged = (cpu * 5) + (memory * 2) + (disk_size * 0.6) + plan_name = "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format( + cpu=cpu, + memory=memory, + disk_size=disk_size) - # Check if the payment was approved - if not charge_response.get( - 'response_object'): - msg = charge_response.get('error') - raise Exception("make_charge failed: {}".format(msg)) + stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu, + ram=memory, + ssd=disk_size, + version=1, + app='dcl') + stripe_plan = self.stripe_utils.get_or_create_stripe_plan( + amount=amount_to_be_charged, + name=plan_name, + stripe_plan_id=stripe_plan_id) + subscription_result = self.stripe_utils.subscribe_customer_to_plan( + stripe_customer.stripe_id, + [{"plan": stripe_plan.get( + 'response_object').stripe_plan_id}]) + stripe_subscription_obj = subscription_result.get('response_object') + # Check if the subscription was approved and is active + if stripe_subscription_obj is None or \ + stripe_subscription_obj.status != 'active': + msg = subscription_result.get('error') + raise Exception("Creating subscription failed: {}".format(msg)) - charge = charge_response.get('response_object') async_task = create_vm_task.delay(vm_template_id, self.user, specs, template_data, stripe_customer.id, billing_address_data, billing_address_id, - charge) + stripe_subscription_obj, + card_details_dict) new_vm_id = 0 res = None for i in range(0, 10): From 16f000c7232839e929655d7212b98961fd9697fd Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 24 Aug 2017 11:54:01 +0530 Subject: [PATCH 30/41] dcl views.py: passing cc details to create_vm_task + reformatted code --- datacenterlight/views.py | 48 +++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/datacenterlight/views.py b/datacenterlight/views.py index fd5c7959..fd1435a1 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -476,32 +476,50 @@ class OrderConfirmationView(DetailView): # Make stripe charge to a customer stripe_utils = StripeUtils() + card_details = stripe_utils.get_card_details(customer.stripe_id, + request.session.get( + 'token')) + if not card_details.get('response_object'): + msg = card_details.get('error') + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='failed_payment') + return HttpResponseRedirect( + reverse('datacenterlight:payment') + '#payment_error') + card_details_dict = card_details.get('response_object') cpu = specs.get('cpu') memory = specs.get('memory') disk_size = specs.get('disk_size') amount_to_be_charged = (cpu * 5) + (memory * 2) + (disk_size * 0.6) - plan_name = "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format(cpu=cpu, - memory=memory, - disk_size=disk_size) + plan_name = "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format( + cpu=cpu, + memory=memory, + disk_size=disk_size) + stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu, ram=memory, ssd=disk_size, version=1, app='dcl') - stripe_plan = stripe_utils.get_or_create_stripe_plan(amount=amount_to_be_charged, - name=plan_name, - stripe_plan_id=stripe_plan_id) - subscription_result = stripe_utils.subscribe_customer_to_plan(customer.stripe_id, - [{"plan": stripe_plan.get( - 'response_object').stripe_plan_id}]) - response_object = subscription_result.get('response_object') + stripe_plan = stripe_utils.get_or_create_stripe_plan( + amount=amount_to_be_charged, + name=plan_name, + stripe_plan_id=stripe_plan_id) + subscription_result = stripe_utils.subscribe_customer_to_plan( + customer.stripe_id, + [{"plan": stripe_plan.get( + 'response_object').stripe_plan_id}]) + stripe_subscription_obj = subscription_result.get('response_object') # Check if the subscription was approved and is active - if response_object is None or response_object.status != 'active': + if stripe_subscription_obj is None or \ + stripe_subscription_obj.status != 'active': msg = subscription_result.get('error') - messages.add_message(self.request, messages.ERROR, msg, extra_tags='failed_payment') - return HttpResponseRedirect(reverse('datacenterlight:payment') + '#payment_error') - create_vm_task.delay(vm_template_id, user, specs, template, stripe_customer_id, billing_address_data, + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='failed_payment') + return HttpResponseRedirect( + reverse('datacenterlight:payment') + '#payment_error') + create_vm_task.delay(vm_template_id, user, specs, template, + stripe_customer_id, billing_address_data, billing_address_id, - response_object) + stripe_subscription_obj, card_details_dict) request.session['order_confirmation'] = True return HttpResponseRedirect(reverse('datacenterlight:order_success')) From 7c3a0b86d871f4e28634dd53868d9b42b1723cee Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 24 Aug 2017 11:59:38 +0530 Subject: [PATCH 31/41] HostingOrder: Updated set_subscription_id to include cc details as parameter --- hosting/models.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/hosting/models.py b/hosting/models.py index a1951bc6..cc75f2bb 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -67,7 +67,8 @@ class HostingOrder(AssignPermissionsMixin, models.Model): return self.ORDER_APPROVED_STATUS if self.approved else self.ORDER_DECLINED_STATUS @classmethod - def create(cls, price=None, vm_id=None, customer=None, billing_address=None): + def create(cls, price=None, vm_id=None, customer=None, + billing_address=None): instance = cls.objects.create( price=price, vm_id=vm_id, @@ -87,15 +88,21 @@ class HostingOrder(AssignPermissionsMixin, models.Model): self.cc_brand = stripe_charge.source.brand self.save() - def set_subscription_id(self, subscription_object): + def set_subscription_id(self, subscription_object, cc_details): """ - When creating a Stripe subscription, we have subscription id. We store this in the subscription_id field. - This method sets the subscription id from subscription_object. + When creating a Stripe subscription, we have subscription id. + We store this in the subscription_id field. + This method sets the subscription id from subscription_object + and also the last4 and credit card brands used for this order. :param subscription_object: Stripe's subscription object + :param cc_details: A dict containing card details + {last4, brand} :return: """ self.subscription_id = subscription_object.id + self.last4 = cc_details.last4 + self.cc_brand = cc_details.brand self.save() def get_cc_data(self): @@ -149,5 +156,6 @@ class HostingBill(AssignPermissionsMixin, models.Model): @classmethod def create(cls, customer=None, billing_address=None): - instance = cls.objects.create(customer=customer, billing_address=billing_address) + instance = cls.objects.create(customer=customer, + billing_address=billing_address) return instance From eab0c9fac1fd929fd8648a1ef3811df0905b6043 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 24 Aug 2017 12:30:17 +0530 Subject: [PATCH 32/41] Added price to specs as it is being used in create_vm_task --- datacenterlight/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/datacenterlight/tests.py b/datacenterlight/tests.py index c993bf79..602fb403 100644 --- a/datacenterlight/tests.py +++ b/datacenterlight/tests.py @@ -65,7 +65,8 @@ class CeleryTaskTestCase(TestCase): specs = { 'cpu': 1, 'memory': 2, - 'disk_size': 10 + 'disk_size': 10, + 'price': 15 } stripe_customer = StripeCustomer.get_or_create( From 75da4a771117077af0126ed919b01b497645de3d Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 24 Aug 2017 12:51:55 +0530 Subject: [PATCH 33/41] hosting/models.py: Fixed bug extracting cc info from dict --- hosting/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hosting/models.py b/hosting/models.py index cc75f2bb..478ed745 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -101,8 +101,8 @@ class HostingOrder(AssignPermissionsMixin, models.Model): :return: """ self.subscription_id = subscription_object.id - self.last4 = cc_details.last4 - self.cc_brand = cc_details.brand + self.last4 = cc_details.get('last4') + self.cc_brand = cc_details.get('brand') self.save() def get_cc_data(self): From 2ae88ae2874806dc6da8fc7473cebc55efed45f7 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 24 Aug 2017 12:54:12 +0530 Subject: [PATCH 34/41] Added style for /month text --- hosting/static/hosting/css/landing-page.css | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/hosting/static/hosting/css/landing-page.css b/hosting/static/hosting/css/landing-page.css index 7a569dc8..883d7bc2 100644 --- a/hosting/static/hosting/css/landing-page.css +++ b/hosting/static/hosting/css/landing-page.css @@ -536,6 +536,13 @@ a.unlink:hover { .dcl-order-table-total .tbl-total { text-align: center; color: #000; + padding-left: 44px; +} + +.tbl-total .dcl-price-month { + font-size: 16px; + text-transform: capitalize; + color: #000; } .tbl-no-padding { @@ -782,4 +789,4 @@ a.list-group-item-danger.active:focus { } .panel-danger > .panel-heading .badge { background-color: #eb4d5c; -} \ No newline at end of file +} From ebff2cbd94b8e4b8e1c114a399b75122aff4f859 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 24 Aug 2017 12:55:22 +0530 Subject: [PATCH 35/41] hosting/payment.html: Added /month text and adjusted width --- hosting/templates/hosting/payment.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hosting/templates/hosting/payment.html b/hosting/templates/hosting/payment.html index 7bf84645..62f79f5d 100644 --- a/hosting/templates/hosting/payment.html +++ b/hosting/templates/hosting/payment.html @@ -41,9 +41,9 @@ {%trans "Total" %} {%trans "including VAT" %}
-
-
{{request.session.specs.price}} - CHF +
+
{{request.session.specs.price}} + CHF/Month
From f2b0ff04cab8d78e56e521586d228b1c2b9da29c Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 24 Aug 2017 13:41:48 +0530 Subject: [PATCH 36/41] Added text and styles for monthly payment clarification --- .../templates/datacenterlight/order_detail.html | 9 ++++++--- hosting/static/hosting/css/landing-page.css | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/datacenterlight/templates/datacenterlight/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index 8b1180bb..61499134 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -67,14 +67,17 @@

{% trans "Configuration"%} {{request.session.template.name}}


-

{% trans "Total"%}

{{vm.price}} CHF

+

{% trans "Total"%}

{{vm.price}} CHF /{% trans "Month" %}

{% endwith %}
{% csrf_token %} -
- +
+

{% blocktrans with vm_price=request.session.specs.price %}By clicking "Place order" this plan will charge your credit card account with the fee of {{ vm_price }}CHF/month (incl. VAT) {% endblocktrans %}.

+
+
diff --git a/hosting/static/hosting/css/landing-page.css b/hosting/static/hosting/css/landing-page.css index 883d7bc2..d1dc657a 100644 --- a/hosting/static/hosting/css/landing-page.css +++ b/hosting/static/hosting/css/landing-page.css @@ -533,6 +533,11 @@ a.unlink:hover { padding-left: 5px; } +.dcl-place-order-text{ + font-size: 13px; + color: #808080; +} + .dcl-order-table-total .tbl-total { text-align: center; color: #000; From 60df9a888caf96823da4c27d0640d8c47e229ba5 Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 24 Aug 2017 12:48:30 +0200 Subject: [PATCH 37/41] Added translation for month --- datacenterlight/locale/de/LC_MESSAGES/django.po | 15 ++++++++++++++- hosting/templates/hosting/payment.html | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/datacenterlight/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po index 6511367f..b9fab2a1 100644 --- a/datacenterlight/locale/de/LC_MESSAGES/django.po +++ b/datacenterlight/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-03 03:10+0530\n" +"POT-Creation-Date: 2017-08-24 10:25+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -276,6 +276,19 @@ msgstr "Konfiguration" msgid "Total" msgstr "" +#, fuzzy +#| msgid "month" +msgid "Month" +msgstr "Monat" + +#, python-format +msgid "" +"By clicking \"Place order\" this plan will charge your credit card account " +"with the fee of %(vm_price)sCHF/month (incl. VAT) " +msgstr "" +"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)sCHF " +"pro Monat belastet" + msgid "Place order" msgstr "Bestellen" diff --git a/hosting/templates/hosting/payment.html b/hosting/templates/hosting/payment.html index 62f79f5d..3a533df6 100644 --- a/hosting/templates/hosting/payment.html +++ b/hosting/templates/hosting/payment.html @@ -43,7 +43,7 @@
{{request.session.specs.price}} - CHF/Month + CHF/{% trans "Month" %}
From f32494af2f5e9fd2d8978ab80cd0395292832a6c Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 24 Aug 2017 13:05:26 +0200 Subject: [PATCH 38/41] hosting po file: Removed fuzzy for a text --- hosting/locale/de/LC_MESSAGES/django.po | 8 -------- 1 file changed, 8 deletions(-) diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po index 6f0ec86b..97434487 100644 --- a/hosting/locale/de/LC_MESSAGES/django.po +++ b/hosting/locale/de/LC_MESSAGES/django.po @@ -292,14 +292,6 @@ msgstr "" "\"https://stripe.com\" target=\"_blank\">Stripe für die Bezahlung und " "speichern keine Informationen in unserer Datenbank." -#, fuzzy -#| msgid "" -#| "\n" -#| " You are not making any " -#| "payment yet. After submitting your card\n" -#| " information, you will be " -#| "taken to the Confirm Order Page.\n" -#| " " msgid "" "\n" " You are not making any " From 5e26d6df04376b7b5122e3649e1a88d224973bbf Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 24 Aug 2017 13:18:59 +0200 Subject: [PATCH 39/41] Simplified text -> You are not making payment + translation --- hosting/locale/de/LC_MESSAGES/django.po | 46 ++++++++++++------------- hosting/templates/hosting/payment.html | 10 ++---- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po index 97434487..ddb853da 100644 --- a/hosting/locale/de/LC_MESSAGES/django.po +++ b/hosting/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-20 21:37+0530\n" +"POT-Creation-Date: 2017-08-24 11:12+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -254,6 +254,9 @@ msgstr "Betrag" msgid "Status" msgstr "" +msgid "See Invoice" +msgstr "Rechnung" + msgid "View Detail" msgstr "Details anzeigen" @@ -272,6 +275,9 @@ msgstr "Konfiguration" msgid "including VAT" msgstr "inkl. Mehrwertsteuer" +msgid "Month" +msgstr "Monat" + msgid "Billing Address" msgstr "Rechnungsadresse" @@ -293,14 +299,9 @@ msgstr "" "speichern keine Informationen in unserer Datenbank." msgid "" -"\n" -" You are not making any " -"payment yet. After submitting your card\n" -" information, you will be " -"taken to the Confirm Order Page.\n" -" " +"You are not making any payment yet. After submitting your card information, " +"you will be taken to the Confirm Order Page." msgstr "" -"\n" "Es wird noch keine Bezahlung vorgenommen. Nach der Eingabe Deiner " "Kreditkateninformationen wirst du auf die Bestellbestätigungsseite " "weitergeleitet." @@ -383,7 +384,7 @@ msgstr "Anzeigen" #, fuzzy #| msgid "Public SSH Key" -msgid "Public SSH key" +msgid "Public SSH Key" msgstr "Public SSH Key" msgid "Download" @@ -407,12 +408,6 @@ msgstr "Abrechnungen" msgid "Current Pricing" msgstr "Aktueller Preis" -msgid "Month" -msgstr "Monat" - -msgid "See Invoice" -msgstr "Rechnung" - msgid "Your VM is" msgstr "Deine VM ist" @@ -499,6 +494,19 @@ msgid "" "contact Data Center Light Support." msgstr "" +#~ msgid "" +#~ "\n" +#~ " You are not making any " +#~ "payment yet. After submitting your card\n" +#~ " information, you will be " +#~ "taken to the Confirm Order Page.\n" +#~ " " +#~ msgstr "" +#~ "\n" +#~ "Es wird noch keine Bezahlung vorgenommen. Nach der Eingabe Deiner " +#~ "Kreditkateninformationen wirst du auf die Bestellbestätigungsseite " +#~ "weitergeleitet." + #~ msgid "Approved" #~ msgstr "Akzeptiert" @@ -662,14 +670,6 @@ msgstr "" #~ msgid "Place Order" #~ msgstr "Bestelle" -#~ msgid "" -#~ "You are not making any payment yet. After placing your order, you will be " -#~ "taken to the Submit Payment Page." -#~ msgstr "" -#~ "Es wird noch keine Bezahlung vorgenommen. Nach der Eingabe deiner " -#~ "Kreditkateninformationen wirst du auf die Bestellbestätigungsseite " -#~ "weitergeleitet." - #~ msgid "CARD NUMBER" #~ msgstr "Kreditkartennummer" diff --git a/hosting/templates/hosting/payment.html b/hosting/templates/hosting/payment.html index 3a533df6..499511f8 100644 --- a/hosting/templates/hosting/payment.html +++ b/hosting/templates/hosting/payment.html @@ -87,10 +87,7 @@
{% if not messages and not form.non_field_errors %}

- {% blocktrans %} - You are not making any payment yet. After submitting your card - information, you will be taken to the Confirm Order Page. - {% endblocktrans %} + {% blocktrans %}You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page.{% endblocktrans %}

{% endif %}
@@ -147,10 +144,7 @@
{% if not messages and not form.non_field_errors %}

- {% blocktrans %} - You are not making any payment yet. After submitting your card - information, you will be taken to the Confirm Order Page. - {% endblocktrans %} + {% blocktrans %}You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page.{% endblocktrans %}

{% endif %}
From 7153b635e52b4d8a371f0bc0fa9bc52a381efd44 Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 24 Aug 2017 13:29:34 +0200 Subject: [PATCH 40/41] Removed (incl. VAT) text in hosting,dcl payment template --- datacenterlight/locale/de/LC_MESSAGES/django.po | 4 ++-- datacenterlight/templates/datacenterlight/order_detail.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/datacenterlight/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po index b9fab2a1..3853d4e3 100644 --- a/datacenterlight/locale/de/LC_MESSAGES/django.po +++ b/datacenterlight/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-24 10:25+0000\n" +"POT-Creation-Date: 2017-08-24 11:28+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -284,7 +284,7 @@ msgstr "Monat" #, python-format msgid "" "By clicking \"Place order\" this plan will charge your credit card account " -"with the fee of %(vm_price)sCHF/month (incl. VAT) " +"with the fee of %(vm_price)sCHF/month" msgstr "" "Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)sCHF " "pro Monat belastet" diff --git a/datacenterlight/templates/datacenterlight/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index 61499134..7a882236 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -74,7 +74,7 @@
{% csrf_token %}
-

{% blocktrans with vm_price=request.session.specs.price %}By clicking "Place order" this plan will charge your credit card account with the fee of {{ vm_price }}CHF/month (incl. VAT) {% endblocktrans %}.

+

{% blocktrans with vm_price=request.session.specs.price %}By clicking "Place order" this plan will charge your credit card account with the fee of {{ vm_price }}CHF/month{% endblocktrans %}.

From c9357058f76299afaacc16c3c3992ae12d5d9b4d Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Thu, 24 Aug 2017 13:38:10 +0200 Subject: [PATCH 41/41] Added STRIPE_API_PRIVATE_KEY_TEST to the test case --- utils/tests.py | 83 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/utils/tests.py b/utils/tests.py index 2da1edf2..c4608e73 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -10,6 +10,7 @@ from model_mommy import mommy from datacenterlight.models import StripePlan from membership.models import StripeCustomer from utils.stripe_utils import StripeUtils +from django.conf import settings class BaseTestCase(TestCase): @@ -22,8 +23,9 @@ class BaseTestCase(TestCase): self.dummy_password = 'test_password' # Users - self.customer, self.another_customer = mommy.make('membership.CustomUser', - _quantity=2) + self.customer, self.another_customer = mommy.make( + 'membership.CustomUser', + _quantity=2) self.customer.set_password(self.dummy_password) self.customer.save() self.another_customer.set_password(self.dummy_password) @@ -106,6 +108,7 @@ class TestStripeCustomerDescription(TestCase): self.customer.email = self.customer_email self.customer.save() self.stripe_utils = StripeUtils() + stripe.api_key = settings.STRIPE_API_PRIVATE_KEY_TEST self.token = stripe.Token.create( card={ "number": '4111111111111111', @@ -124,7 +127,9 @@ class TestStripeCustomerDescription(TestCase): ) def test_creating_stripe_customer(self): - stripe_data = self.stripe_utils.create_customer(self.token.id, self.customer.email, self.customer_name) + stripe_data = self.stripe_utils.create_customer(self.token.id, + self.customer.email, + self.customer_name) self.assertEqual(stripe_data.get('error'), None) customer_data = stripe_data.get('response_object') self.assertEqual(customer_data.description, self.customer_name) @@ -136,13 +141,19 @@ class StripePlanTestCase(TestStripeCustomerDescription): """ def test_get_stripe_plan_id_string(self): - plan_id_string = StripeUtils.get_stripe_plan_id(cpu=2, ram=20, ssd=100, version=1, app='dcl') + plan_id_string = StripeUtils.get_stripe_plan_id(cpu=2, ram=20, ssd=100, + version=1, app='dcl') self.assertEqual(plan_id_string, 'dcl-v1-cpu-2-ram-20gb-ssd-100gb') - plan_id_string = StripeUtils.get_stripe_plan_id(cpu=2, ram=20, ssd=100, version=1, app='dcl', hdd=200) - self.assertEqual(plan_id_string, 'dcl-v1-cpu-2-ram-20gb-ssd-100gb-hdd-200gb') + plan_id_string = StripeUtils.get_stripe_plan_id(cpu=2, ram=20, ssd=100, + version=1, app='dcl', + hdd=200) + self.assertEqual(plan_id_string, + 'dcl-v1-cpu-2-ram-20gb-ssd-100gb-hdd-200gb') def test_get_or_create_plan(self): - stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, "test plan 1", stripe_plan_id='test-plan-1') + stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, + "test plan 1", + stripe_plan_id='test-plan-1') self.assertIsNone(stripe_plan.get('error')) self.assertIsInstance(stripe_plan.get('response_object'), StripePlan) @@ -159,19 +170,27 @@ class StripePlanTestCase(TestStripeCustomerDescription): """ unique_id = str(uuid.uuid4().hex) new_plan_id_str = 'test-plan-{}'.format(unique_id) - stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, "test plan {}".format(unique_id), + stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, + "test plan {}".format( + unique_id), stripe_plan_id=new_plan_id_str) self.assertIsInstance(stripe_plan.get('response_object'), StripePlan) - self.assertEqual(stripe_plan.get('response_object').stripe_plan_id, new_plan_id_str) + self.assertEqual(stripe_plan.get('response_object').stripe_plan_id, + new_plan_id_str) # Test creating the same plan again and expect the PLAN_EXISTS_ERROR_MSG # We first delete the local Stripe Plan, so that the code tries to create a new plan in Stripe - StripePlan.objects.filter(stripe_plan_id=new_plan_id_str).all().delete() - stripe_plan_1 = self.stripe_utils.get_or_create_stripe_plan(2000, "test plan {}".format(unique_id), + StripePlan.objects.filter( + stripe_plan_id=new_plan_id_str).all().delete() + stripe_plan_1 = self.stripe_utils.get_or_create_stripe_plan(2000, + "test plan {}".format( + unique_id), stripe_plan_id=new_plan_id_str) - mock_logger.debug.assert_called_with(self.stripe_utils.PLAN_EXISTS_ERROR_MSG.format(new_plan_id_str)) + mock_logger.debug.assert_called_with( + self.stripe_utils.PLAN_EXISTS_ERROR_MSG.format(new_plan_id_str)) self.assertIsInstance(stripe_plan_1.get('response_object'), StripePlan) - self.assertEqual(stripe_plan_1.get('response_object').stripe_plan_id, new_plan_id_str) + self.assertEqual(stripe_plan_1.get('response_object').stripe_plan_id, + new_plan_id_str) # Delete the test stripe plan that we just created delete_result = self.stripe_utils.delete_stripe_plan(new_plan_id_str) @@ -184,25 +203,35 @@ class StripePlanTestCase(TestStripeCustomerDescription): result = self.stripe_utils.delete_stripe_plan(plan_id) self.assertIsInstance(result, dict) self.assertEqual(result.get('response_object'), False) - mock_logger.debug.assert_called_with(self.stripe_utils.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(plan_id)) + mock_logger.debug.assert_called_with( + self.stripe_utils.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(plan_id)) def test_subscribe_customer_to_plan(self): - stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, "test plan 1", stripe_plan_id='test-plan-1') - stripe_customer = StripeCustomer.get_or_create(email=self.customer_email, - token=self.token) - result = self.stripe_utils.subscribe_customer_to_plan(stripe_customer.stripe_id, - [{"plan": stripe_plan.get( - 'response_object').stripe_plan_id}]) - self.assertIsInstance(result.get('response_object'), stripe.Subscription) + stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, + "test plan 1", + stripe_plan_id='test-plan-1') + stripe_customer = StripeCustomer.get_or_create( + email=self.customer_email, + token=self.token) + result = self.stripe_utils.subscribe_customer_to_plan( + stripe_customer.stripe_id, + [{"plan": stripe_plan.get( + 'response_object').stripe_plan_id}]) + self.assertIsInstance(result.get('response_object'), + stripe.Subscription) self.assertIsNone(result.get('error')) self.assertEqual(result.get('response_object').get('status'), 'active') def test_subscribe_customer_to_plan_failed_payment(self): - stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, "test plan 1", stripe_plan_id='test-plan-1') - stripe_customer = StripeCustomer.get_or_create(email=self.customer_email, - token=self.failed_token) - result = self.stripe_utils.subscribe_customer_to_plan(stripe_customer.stripe_id, - [{"plan": stripe_plan.get( - 'response_object').stripe_plan_id}]) + stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, + "test plan 1", + stripe_plan_id='test-plan-1') + stripe_customer = StripeCustomer.get_or_create( + email=self.customer_email, + token=self.failed_token) + result = self.stripe_utils.subscribe_customer_to_plan( + stripe_customer.stripe_id, + [{"plan": stripe_plan.get( + 'response_object').stripe_plan_id}]) self.assertIsNone(result.get('response_object'), None) self.assertIsNotNone(result.get('error'))