Merge pull request #452 from pcoder/task/3701/enable_monthly_payments

Task/3701/enable monthly payments
This commit is contained in:
Pcoder 2017-08-24 13:42:42 +02:00 committed by GitHub
commit 6206f25b56
15 changed files with 499 additions and 105 deletions

View file

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"

View file

@ -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)),
],
),
]

View file

@ -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),
),
]

View file

@ -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

View file

@ -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()

View file

@ -67,14 +67,17 @@
<hr>
<p><b>{% trans "Configuration"%}</b> <span class="pull-right">{{request.session.template.name}}</span></p>
<hr>
<h4>{% trans "Total"%}<p class="pull-right"><b>{{vm.price}} CHF</b></p></h4>
<h4>{% trans "Total"%}<p class="pull-right"><b>{{vm.price}} CHF</b><span class="dcl-price-month"> /{% trans "Month" %}</span></p></h4>
{% endwith %}
</div>
<br/>
<form method="post">
{% csrf_token %}
<div class=" content pull-right">
<a href="{{next_url}}" ><button class="btn btn-info">{% trans "Place order"%}</button></a>
<div class="col-md-8 col-xs-7 pull-left tbl-no-padding">
<p class="dcl-place-order-text">{% 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 %}.</p>
</div>
<div class="col-md-4 col-xs-5 content tbl-no-padding">
<a href="{{next_url}}" ><button class="btn btn-info pull-right">{% trans "Place order"%}</button></a>
</div>
</form>
</div>

View file

@ -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):

View file

@ -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'))

View file

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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</a> 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"

View file

@ -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),
),
]

View file

@ -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

View file

@ -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 {

View file

@ -41,9 +41,9 @@
{%trans "Total" %} <span>{%trans "including VAT" %}</span>
</div>
<div class="col-xs-6 col-sm-6 col-md-6 col-lg-6 tbl-no-padding">
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6"></div>
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 tbl-total">{{request.session.specs.price}}
CHF
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4"></div>
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6 tbl-total">{{request.session.specs.price}}
CHF<span class="dcl-price-month">/{% trans "Month" %}</span>
</div>
</div>
</div>
@ -87,10 +87,7 @@
<div class="col-xs-12">
{% if not messages and not form.non_field_errors %}
<p class="card-warning-content card-warning-addtional-margin">
{% 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 %}
</p>
{% endif %}
<div id='payment_error'>
@ -147,10 +144,7 @@
<div class="col-xs-12">
{% if not messages and not form.non_field_errors %}
<p class="card-warning-content">
{% 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 %}
</p>
{% endif %}
<div id='payment_error'>

View file

@ -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

View file

@ -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'))