Merge pull request #452 from pcoder/task/3701/enable_monthly_payments
Task/3701/enable monthly payments
This commit is contained in:
commit
6206f25b56
15 changed files with 499 additions and 105 deletions
|
@ -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"
|
||||
|
||||
|
|
22
datacenterlight/migrations/0007_stripeplan.py
Normal file
22
datacenterlight/migrations/0007_stripeplan.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
20
datacenterlight/migrations/0008_auto_20170821_2024.py
Normal file
20
datacenterlight/migrations/0008_auto_20170821_2024.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
20
hosting/migrations/0042_hostingorder_subscription_id.py
Normal file
20
hosting/migrations/0042_hostingorder_subscription_id.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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
|
||||
|
|
156
utils/tests.py
156
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'))
|
||||
|
|
Loading…
Reference in a new issue