diff --git a/datacenterlight/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po index 6511367f..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-03 03:10+0530\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" @@ -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" +msgstr "" +"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)sCHF " +"pro Monat belastet" + msgid "Place order" msgstr "Bestellen" 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)), + ], + ), + ] 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 fdfebc96..f7b50a01 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=256, null=True) + + @classmethod + def create(cls, stripe_plan_id): + stripe_plan = cls(stripe_plan_id=stripe_plan_id) + return stripe_plan diff --git a/datacenterlight/tasks.py b/datacenterlight/tasks.py index f036a461..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') @@ -91,9 +91,9 @@ def create_vm_task(self, vm_template_id, user, specs, template, 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, cc_details) # If the Stripe payment succeeds, set order status approved order.set_approved() diff --git a/datacenterlight/templates/datacenterlight/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index 8b1180bb..7a882236 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{% endblocktrans %}.

+
+
diff --git a/datacenterlight/tests.py b/datacenterlight/tests.py index b0768c9a..602fb403 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() @@ -61,12 +66,16 @@ class CeleryTaskTestCase(TestCase): 'cpu': 1, 'memory': 2, 'disk_size': 10, - 'price': 15, + 'price': 15 } 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 +92,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): diff --git a/datacenterlight/views.py b/datacenterlight/views.py index 399b7676..fd1435a1 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -473,25 +473,53 @@ 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'): - msg = charge_response.get('error') + 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='make_charge_error') + 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) - charge = charge_response.get('response_object') + 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}]) + 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') + 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, - charge) + stripe_subscription_obj, card_details_dict) request.session['order_confirmation'] = True return HttpResponseRedirect(reverse('datacenterlight:order_success')) diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po index 6f0ec86b..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" @@ -292,23 +298,10 @@ 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 " -"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." @@ -391,7 +384,7 @@ msgstr "Anzeigen" #, fuzzy #| msgid "Public SSH Key" -msgid "Public SSH key" +msgid "Public SSH Key" msgstr "Public SSH Key" msgid "Download" @@ -415,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" @@ -507,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" @@ -670,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/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), + ), + ] diff --git a/hosting/models.py b/hosting/models.py index 8cdc6114..478ed745 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -50,6 +50,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',) @@ -66,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, @@ -86,6 +88,23 @@ class HostingOrder(AssignPermissionsMixin, models.Model): self.cc_brand = stripe_charge.source.brand self.save() + 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 + 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.get('last4') + self.cc_brand = cc_details.get('brand') + self.save() + def get_cc_data(self): return { 'last4': self.last4, @@ -137,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 diff --git a/hosting/static/hosting/css/landing-page.css b/hosting/static/hosting/css/landing-page.css index 7a569dc8..d1dc657a 100644 --- a/hosting/static/hosting/css/landing-page.css +++ b/hosting/static/hosting/css/landing-page.css @@ -533,9 +533,21 @@ 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; + padding-left: 44px; +} + +.tbl-total .dcl-price-month { + font-size: 16px; + text-transform: capitalize; + color: #000; } .tbl-no-padding { @@ -782,4 +794,4 @@ a.list-group-item-danger.active:focus { } .panel-danger > .panel-heading .badge { background-color: #eb4d5c; -} \ No newline at end of file +} diff --git a/hosting/templates/hosting/payment.html b/hosting/templates/hosting/payment.html index 7bf84645..499511f8 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/{% trans "Month" %}
@@ -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 %}
diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index c9604425..f35a6b9c 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -1,6 +1,10 @@ +import logging import stripe from django.conf import settings +from datacenterlight.models import StripePlan + stripe.api_key = settings.STRIPE_API_PRIVATE_KEY +logger = logging.getLogger(__name__) def handleStripeError(f): @@ -26,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"}) @@ -55,6 +60,10 @@ class StripeUtils(object): CURRENCY = 'chf' INTERVAL = 'month' SUCCEEDED_STATUS = 'succeeded' + 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 @@ -96,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 @@ -129,13 +139,92 @@ 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_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. + + :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 + """ + _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: + try: + self.stripe.Plan.create( + amount=amount, + interval=self.INTERVAL, + name=name, + currency=self.CURRENCY, + 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) + 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. + """ + 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): + """ + 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", + }, + ] + :return: The subscription StripeObject + """ + + subscription_result = self.stripe.Subscription.create( + customer=customer, + items=plans, + ) + return subscription_result @handleStripeError def make_payment(self, customer, amount, token): @@ -145,3 +234,29 @@ class StripeUtils(object): customer=customer ) return charge + + @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 + + :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' + :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 4a29fa60..c4608e73 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -1,10 +1,15 @@ -from django.test import TestCase -from django.test import Client -from django.http.request import HttpRequest +import uuid +from unittest.mock import patch -from model_mommy import mommy -from utils.stripe_utils import StripeUtils import stripe +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 from django.conf import settings @@ -18,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) @@ -94,17 +100,16 @@ 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() - stripe.api_key = settings.STRIPE_API_PRIVATE_KEY - - def test_creating_stripe_customer(self): - test_name = "Monty Python" - token = stripe.Token.create( + self.stripe_utils = StripeUtils() + stripe.api_key = settings.STRIPE_API_PRIVATE_KEY_TEST + self.token = stripe.Token.create( card={ "number": '4111111111111111', "exp_month": 12, @@ -112,8 +117,121 @@ class TestStripeCustomerDescription(TestCase): "cvc": '123' }, ) - stripe_utils = StripeUtils() - stripe_data = stripe_utils.create_customer(token.id, self.customer.email, test_name) + 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) 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): + """ + A class to test Stripe plans + """ + + 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') + self.assertIsNone(stripe_plan.get('error')) + self.assertIsInstance(stripe_plan.get('response_object'), StripePlan) + + @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. This creates a Stripe error, the code should be able to handle the error + + :param mock_logger: + :return: + """ + 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_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 = 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 = 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' + 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)) + + 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')) + 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}]) + self.assertIsNone(result.get('response_object'), None) + self.assertIsNotNone(result.get('error'))