diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index 69e708d2..9d64dc60 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -446,8 +446,8 @@ AUTH_USER_MODEL = 'membership.CustomUser' # PAYMENT -STRIPE_API_PUBLIC_KEY = 'pk_test_ZRg6P8g5ybiHE6l2RW5pSaYV' # used in frontend to call from user browser -STRIPE_API_PRIVATE_KEY = 'sk_test_uIPMdgXoRGydrcD7fkwcn7dj' # used in backend payment +STRIPE_API_PUBLIC_KEY = 'pk_test_QqBZ50Am8KOxaAlOxbcm9Psl' # used in frontend to call from user browser +STRIPE_API_PRIVATE_KEY = 'sk_test_dqAmbKAij12QCGfkYZ3poGt2' # used in backend payment STRIPE_DESCRIPTION_ON_PAYMENT = "Payment for ungleich GmbH services" # EMAIL MESSAGES diff --git a/hosting/managers.py b/hosting/managers.py new file mode 100644 index 00000000..c474d61e --- /dev/null +++ b/hosting/managers.py @@ -0,0 +1,9 @@ +from django.db import models + + +class VMPlansManager(models.Manager): + + def active(self, user, **kwargs): + return self.prefetch_related('hosting_orders__customer__user').\ + filter(hosting_orders__customer__user=user, hosting_orders__approved=True, **kwargs)\ + .distinct() diff --git a/hosting/migrations/0012_auto_20160501_1850.py b/hosting/migrations/0012_auto_20160501_1850.py new file mode 100644 index 00000000..3ef15a46 --- /dev/null +++ b/hosting/migrations/0012_auto_20160501_1850.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-05-01 18:50 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0011_auto_20160426_0555'), + ] + + operations = [ + migrations.AddField( + model_name='hostingorder', + name='cc_brand', + field=models.CharField(default='Visa', max_length=10), + preserve_default=False, + ), + migrations.AddField( + model_name='hostingorder', + name='last4', + field=models.CharField(default=1111, max_length=4), + preserve_default=False, + ), + ] diff --git a/hosting/migrations/0013_auto_20160505_0302.py b/hosting/migrations/0013_auto_20160505_0302.py new file mode 100644 index 00000000..ac4cd8e5 --- /dev/null +++ b/hosting/migrations/0013_auto_20160505_0302.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-05-05 03:02 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0012_auto_20160501_1850'), + ] + + operations = [ + migrations.AlterField( + model_name='hostingorder', + name='VMPlan', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hosting.VirtualMachinePlan'), + ), + ] diff --git a/hosting/migrations/0014_auto_20160505_0541.py b/hosting/migrations/0014_auto_20160505_0541.py new file mode 100644 index 00000000..532b25e8 --- /dev/null +++ b/hosting/migrations/0014_auto_20160505_0541.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-05-05 05:41 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0013_auto_20160505_0302'), + ] + + operations = [ + migrations.AlterField( + model_name='hostingorder', + name='VMPlan', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hosting_orders', to='hosting.VirtualMachinePlan'), + ), + ] diff --git a/hosting/models.py b/hosting/models.py index f3e2a178..dd686312 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -2,10 +2,13 @@ import json from django.db import models from django.utils.translation import ugettext_lazy as _ +from django.utils.functional import cached_property from django.core import serializers from membership.models import StripeCustomer from utils.models import BillingAddress +from .managers import VMPlansManager + class RailsBetaUser(models.Model): email = models.EmailField(unique=True) @@ -81,6 +84,17 @@ class VirtualMachinePlan(models.Model): vm_type = models.ForeignKey(VirtualMachineType) price = models.FloatField() + objects = VMPlansManager() + + @cached_property + def hosting_company_name(self): + return self.vm_type.get_hosting_company_display() + + @cached_property + def name(self): + name = 'vm-%s' % self.id + return name + @classmethod def create(cls, data, user): instance = cls.objects.create(**data) @@ -88,13 +102,23 @@ class VirtualMachinePlan(models.Model): class HostingOrder(models.Model): - VMPlan = models.OneToOneField(VirtualMachinePlan) + + ORDER_APPROVED_STATUS = 'Approved' + ORDER_DECLINED_STATUS = 'Declined' + + VMPlan = models.ForeignKey(VirtualMachinePlan, related_name='hosting_orders') customer = models.ForeignKey(StripeCustomer) billing_address = models.ForeignKey(BillingAddress) created_at = models.DateTimeField(auto_now_add=True) approved = models.BooleanField(default=False) + last4 = models.CharField(max_length=4) + cc_brand = models.CharField(max_length=10) stripe_charge_id = models.CharField(max_length=100, null=True) + @cached_property + def status(self): + return self.ORDER_APPROVED_STATUS if self.approved else self.ORDER_DECLINED_STATUS + @classmethod def create(cls, VMPlan=None, customer=None, billing_address=None): instance = cls.objects.create(VMPlan=VMPlan, customer=customer, @@ -107,6 +131,8 @@ class HostingOrder(models.Model): def set_stripe_charge(self, stripe_charge): self.stripe_charge_id = stripe_charge.id + self.last4 = stripe_charge.source.last4 + self.cc_brand = stripe_charge.source.brand self.save() diff --git a/hosting/static/hosting/css/commons.css b/hosting/static/hosting/css/commons.css new file mode 100644 index 00000000..f386cfd6 --- /dev/null +++ b/hosting/static/hosting/css/commons.css @@ -0,0 +1,20 @@ +.dashboard-container { + padding-top:5%; padding-bottom: 11%; +} + +.borderless td { + border: none !important; +} +.borderless thead { +} + +.borderless tbody:before { + content: "-"; + display: block; + color: transparent; +} + +.inline-headers h3, .inline-headers h4 { + display: inline-block; + vertical-align: baseline; +} diff --git a/hosting/static/hosting/css/order.css b/hosting/static/hosting/css/order.css new file mode 100644 index 00000000..8e0c5919 --- /dev/null +++ b/hosting/static/hosting/css/order.css @@ -0,0 +1,17 @@ +.order-detail-container {padding-top:5%; padding-bottom: 11%;} + +.order-detail-container .invoice-title h2, .invoice-title h3 { + display: inline-block; +} + +.order-detail-container .table > tbody > tr > .no-line { + border-top: none; +} + +.order-detail-container .table > thead > tr > .no-line { + border-bottom: none; +} + +.order-detail-container .table > tbody > tr > .thick-line { + border-top: 2px solid; +} diff --git a/hosting/static/hosting/css/orders.css b/hosting/static/hosting/css/orders.css new file mode 100644 index 00000000..64e95328 --- /dev/null +++ b/hosting/static/hosting/css/orders.css @@ -0,0 +1,5 @@ +.orders-container {padding-top:5%; padding-bottom: 11%;} + +.orders-container .table > tbody > tr > td { + vertical-align: middle; +} \ No newline at end of file diff --git a/hosting/static/hosting/css/virtual-machine.css b/hosting/static/hosting/css/virtual-machine.css new file mode 100644 index 00000000..570d5118 --- /dev/null +++ b/hosting/static/hosting/css/virtual-machine.css @@ -0,0 +1,42 @@ +.virtual-machine-container .tabs-left, .virtual-machine-container .tabs-right { + border-bottom: none; + padding-top: 2px; +} +.virtual-machine-container .tabs-left { + border-right: 1px solid #ddd; +} +.virtual-machine-container .tabs-right { + border-left: 1px solid #ddd; +} +.virtual-machine-container .tabs-left>li, .virtual-machine-container .tabs-right>li { + float: none; + margin-bottom: 2px; +} +.virtual-machine-container .tabs-left>li { + margin-right: -1px; +} +.virtual-machine-container .tabs-right>li { + margin-left: -1px; +} +.virtual-machine-container .tabs-left>li.active>a, +.virtual-machine-container .tabs-left>li.active>a:hover, +.virtual-machine-container .tabs-left>li.active>a:focus { + border-bottom-color: #ddd; + border-right-color: transparent; +} + +.virtual-machine-container .tabs-right>li.active>a, +.virtual-machine-container .tabs-right>li.active>a:hover, +.virtual-machine-container .tabs-right>li.active>a:focus { + border-bottom: 1px solid #ddd; + border-left-color: transparent; +} +.virtual-machine-container .tabs-left>li>a { + border-radius: 4px 0 0 4px; + margin-right: 0; + display:block; +} +.virtual-machine-container .tabs-right>li>a { + border-radius: 0 4px 4px 0; + margin-right: 0; +} \ No newline at end of file diff --git a/hosting/templates/hosting/base_short.html b/hosting/templates/hosting/base_short.html index fabfa030..339020de 100644 --- a/hosting/templates/hosting/base_short.html +++ b/hosting/templates/hosting/base_short.html @@ -18,6 +18,10 @@ + + + + @@ -49,30 +53,51 @@ - + diff --git a/hosting/templates/hosting/invoice.html b/hosting/templates/hosting/invoice.html deleted file mode 100644 index 302028fe..00000000 --- a/hosting/templates/hosting/invoice.html +++ /dev/null @@ -1,79 +0,0 @@ -{% extends "hosting/base_short.html" %} -{% load staticfiles bootstrap3 %} -{% block content %} - - - -
-
-
-
-

Invoice

Order # {{order.id}}

-
-
-
-
-
-

Billed To:

- John Smith
- 1234 Main
- Apt. 4B
- Springfield, ST 54321 -
-
-
-
- Order Date:
- {{order.created_at}}

-
-
-
-
-
-
- Payment Method:
- {{brand}} ending **** {{last4}}
- {{user.email}} -
-
-
-
-
- -
-
-

Order summary

-
-
-

Type {{request.session.vm_specs.hosting_company_name}}

-
-

Cores {{request.session.vm_specs.cores}}

-
-

Memory {{request.session.vm_specs.memory}} GiB

-
-

Disk space {{request.session.vm_specs.disk_size}} GiB

-
-

Total

{{request.session.vm_specs.final_price}} CHF

-
-
-
-
-{%endblock%} diff --git a/hosting/templates/hosting/order_detail.html b/hosting/templates/hosting/order_detail.html new file mode 100644 index 00000000..74b8d5bb --- /dev/null +++ b/hosting/templates/hosting/order_detail.html @@ -0,0 +1,64 @@ +{% extends "hosting/base_short.html" %} +{% load staticfiles bootstrap3 %} +{% block content %} + +
+
+
+
+

Invoice

Order # {{object.id}}

+
+
+
+
+
+

Billed To:

+ {{user.name}}
+ {{object.billing_address.street_address}},{{order.billing_address.postal_code}}
+ {{object.billing_address.city}}, {{object.billing_address.country}}. +
+
+
+
+ Order Date:
+ {{object.created_at}}

+ Status:
+ {{object.status}} +

+
+ +
+
+
+
+
+ Payment Method:
+ {{object.cc_brand}} ending **** {{object.last4}}
+ {{user.email}} +
+
+
+
+
+ +
+
+

Order summary

+
+
+

Type {{object.VMPlan.hosting_company_name}}

+
+

Cores {{object.VMPlan.cores}}

+
+

Memory {{object.VMPlan.memory}} GiB

+
+

Disk space {{object.VMPlan.disk_size}} GiB

+
+

Total

{{object.VMPlan.price}} CHF

+
+
+
+
+{%endblock%} diff --git a/hosting/templates/hosting/orders.html b/hosting/templates/hosting/orders.html index df5e6216..99c6464f 100644 --- a/hosting/templates/hosting/orders.html +++ b/hosting/templates/hosting/orders.html @@ -1,31 +1,59 @@ {% extends "hosting/base_short.html" %} {% load staticfiles bootstrap3 %} {% block content %} +
-
+
- - +
My orders.
+

My Orders

+
+ {% for order in orders %} - + - - - + + + + {% endfor %}
# Date Amount Status
{{order.id}}{{order.id}} {{order.created_at}}{{order.VMPlan.price}}{{order.approved}}
{{order.VMPlan.price}} CHF{% if order.approved %} + Approved + {% else%} + Declined + {% endif%} + + +
+ + {% if is_paginated %} + + {% endif %} +
diff --git a/hosting/templates/hosting/virtual_machine_detail.html b/hosting/templates/hosting/virtual_machine_detail.html new file mode 100644 index 00000000..e51e1d4e --- /dev/null +++ b/hosting/templates/hosting/virtual_machine_detail.html @@ -0,0 +1,155 @@ +{% extends "hosting/base_short.html" %} +{% load staticfiles bootstrap3 %} +{% block content %} +
+
+
+
+
+

{{virtual_machine.name}}

+
+ + +
+ +
+
+
+
+

{{virtual_machine.hosting_company_name}}

+
+
+
+
+
+
+
+
+ Cores
+ {{virtual_machine.cores}} +
+
+
+
+ Memory
+ {{virtual_machine.memory}} GiB +
+
+
+
+ Disk
+ {{virtual_machine.disk_size}} GiB +
+
+
+
+
+ + +
+
+ +
+
+

Current pricing

+ {{virtual_machine.price|floatformat}} CHF/mo +
+
+
+
+
+
+
+ +

Orders

+
+ + + + + + + + + + + {% for order in virtual_machine.hosting_orders.all %} + + + + + + + + {% endfor %} + +
#DateAmountStatus
{{order.id}}{{order.created_at}}{{order.VMPlan.price}} CHF{% if order.approved %} + Approved + {% else%} + Declined + {% endif%} + + +
+
+
+
+
+ +
+
+

Current status

+ Online +
+
+
+
+
+
+ +
+
+
+ +
+
+ +
+ +{%endblock%} + + + + + + + + + diff --git a/hosting/templates/hosting/virtual_machines.html b/hosting/templates/hosting/virtual_machines.html new file mode 100644 index 00000000..fa673c99 --- /dev/null +++ b/hosting/templates/hosting/virtual_machines.html @@ -0,0 +1,56 @@ +{% extends "hosting/base_short.html" %} +{% load staticfiles bootstrap3 %} +{% block content %} +
+
+
+
+ +

Virtual Machines

+
+ + + + + + + + + + {% for vm in vms %} + + + + + + + {% endfor %} + +
IDTypeAmount
{{vm.name}}{{vm.hosting_company_name}}{{vm.price}} CHF + +
+ + {% if is_paginated %} + + {% endif %} + +
+ +
+
+ +
+ +{%endblock%} \ No newline at end of file diff --git a/hosting/test_views.py b/hosting/test_views.py new file mode 100644 index 00000000..b2dfe9ae --- /dev/null +++ b/hosting/test_views.py @@ -0,0 +1,72 @@ +from django.test import TestCase +from django.core.urlresolvers import reverse +from django.core.urlresolvers import resolve +from .models import VirtualMachineType +from .views import DjangoHostingView, RailsHostingView, NodeJSHostingView + + +class ProcessVMSelectionTestMixin(object): + + def url_resolve_to_view_correctly(self): + found = resolve(self.url) + self.assertEqual(found.func.__name__, self.view.__name__) + + def test_get(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertEqual(self.view.get_context_data(), self.expected_context) + self.assertEqual(response.context['hosting'], self.expected_context['hosting']) + self.assertTemplateUsed(response, self.expected_template) + + def test_anonymous_post(self): + response = self.client.post(self.url) + self.assertRedirects(response, expected_url=reverse('hosting:login'), + status_code=302, target_status_code=200) + + +class DjangoHostingViewTest(TestCase, ProcessVMSelectionTestMixin): + + def setUp(self): + self.url = reverse('django.hosting') + self.view = DjangoHostingView() + self.expected_template = 'hosting/django.html' + self.expected_context = { + 'hosting': "django", + 'hosting_long': "Django", + 'domain': "django-hosting.ch", + 'google_analytics': "UA-62285904-6", + 'email': "info@django-hosting.ch", + 'vm_types': VirtualMachineType.get_serialized_vm_types(), + } + + +class RailsHostingViewTest(TestCase, ProcessVMSelectionTestMixin): + + def setUp(self): + self.url = reverse('rails.hosting') + self.view = RailsHostingView() + self.expected_template = 'hosting/rails.html' + self.expected_context = { + 'hosting': "rails", + 'hosting_long': "Ruby On Rails", + 'domain': "rails-hosting.ch", + 'google_analytics': "UA-62285904-5", + 'email': "info@rails-hosting.ch", + 'vm_types': VirtualMachineType.get_serialized_vm_types(), + } + + +class NodeJSHostingViewTest(TestCase, ProcessVMSelectionTestMixin): + + def setUp(self): + self.url = reverse('node.hosting') + self.view = NodeJSHostingView() + self.expected_template = 'hosting/nodejs.html' + self.expected_context = { + 'hosting': "nodejs", + 'hosting_long': "NodeJS", + 'domain': "node-hosting.ch", + 'google_analytics': "UA-62285904-7", + 'email': "info@node-hosting.ch", + 'vm_types': VirtualMachineType.get_serialized_vm_types(), + } diff --git a/hosting/urls.py b/hosting/urls.py index 3b40dff8..bb195b79 100644 --- a/hosting/urls.py +++ b/hosting/urls.py @@ -1,20 +1,24 @@ from django.conf.urls import url from .views import DjangoHostingView, RailsHostingView, PaymentVMView, \ - NodeJSHostingView, LoginView, SignupView, IndexView, \ - InvoiceVMView, OrdersHostingView + NodeJSHostingView, LoginView, SignupView, IndexView, \ + OrdersHostingListView, OrdersHostingDetailView, VirtualMachinesPlanListView,\ + VirtualMachineDetailListView urlpatterns = [ # url(r'pricing/?$', VMPricingView.as_view(), name='pricing'), url(r'index/?$', IndexView.as_view(), name='index'), url(r'django/?$', DjangoHostingView.as_view(), name='djangohosting'), url(r'nodejs/?$', NodeJSHostingView.as_view(), name='nodejshosting'), - url(r'rails/?$', RailsHostingView.as_view(), name='railshosting'), + url(r'rails/?$', RailsHostingView.as_view(), name='railshosting'), url(r'payment/?$', PaymentVMView.as_view(), name='payment'), - url(r'invoice/?$', InvoiceVMView.as_view(), name='invoice'), - url(r'orders/?$', OrdersHostingView.as_view(), name='orders'), - url(r'login/?$', LoginView.as_view(), name='login'), + url(r'orders/?$', OrdersHostingListView.as_view(), name='orders'), + url(r'orders/(?P\d+)/?$', OrdersHostingDetailView.as_view(), name='orders'), + url(r'my-virtual-machines/?$', VirtualMachinesPlanListView.as_view(), name='virtual_machines'), + url(r'my-virtual-machines/(?P\d+)/?$', VirtualMachineDetailListView.as_view(), + name='virtual_machines'), + url(r'login/?$', LoginView.as_view(), name='login'), url(r'signup/?$', SignupView.as_view(), name='signup'), url(r'^logout/?$', 'django.contrib.auth.views.logout', - {'next_page': '/ungleich_page'}, name='logout') + {'next_page': '/ungleich_page'}, name='logout') ] diff --git a/hosting/views.py b/hosting/views.py index 65bbbcf3..b0e76028 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -2,16 +2,12 @@ from django.shortcuts import get_object_or_404, render from django.core.urlresolvers import reverse_lazy, reverse from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator -from django.views.generic import View, CreateView, FormView -from django.shortcuts import redirect +from django.views.generic import View, CreateView, FormView, ListView, DetailView from django.http import HttpResponseRedirect from django.contrib.auth import authenticate, login from django.conf import settings -from membership.forms import PaymentForm from membership.models import CustomUser, StripeCustomer from utils.stripe_utils import StripeUtils from utils.forms import BillingAddressForm @@ -21,7 +17,6 @@ from .forms import HostingUserSignupForm, HostingUserLoginForm from .mixins import ProcessVMSelectionMixin - class DjangoHostingView(ProcessVMSelectionMixin, View): template_name = "hosting/django.html" @@ -106,7 +101,7 @@ class IndexView(View): class LoginView(FormView): template_name = 'hosting/login.html' - success_url = reverse_lazy('hosting:login') + success_url = reverse_lazy('hosting:orders') form_class = HostingUserLoginForm moodel = CustomUser @@ -217,60 +212,45 @@ class PaymentVMView(FormView): 'order': order.id, 'billing_address': billing_address.id }) - return HttpResponseRedirect(reverse('hosting:invoice')) + return HttpResponseRedirect(reverse('hosting:orders', kwargs={'pk': order.id})) else: return self.form_invalid(form) -class InvoiceVMView(LoginRequiredMixin, View): - template_name = "hosting/invoice.html" +class OrdersHostingDetailView(LoginRequiredMixin, DetailView): + template_name = "hosting/order_detail.html" login_url = reverse_lazy('hosting:login') - - def get_context_data(self, **kwargs): - charge = self.request.session.get('charge') - order_id = self.request.session.get('order') - billing_address_id = self.request.session.get('billing_address') - last4 = charge.get('source').get('last4') - brand = charge.get('source').get('brand') - - order = get_object_or_404(HostingOrder, pk=order_id) - billing_address = get_object_or_404(BillingAddress, pk=billing_address_id) - - if not charge: - return - - context = { - 'last4': last4, - 'brand': brand, - 'order': order, - 'billing_address': billing_address, - } - return context - - def get(self, request, *args, **kwargs): - - context = self.get_context_data() - - return render(request, self.template_name, context) + model = HostingOrder -class OrdersHostingView(LoginRequiredMixin, View): +class OrdersHostingListView(LoginRequiredMixin, ListView): template_name = "hosting/orders.html" login_url = reverse_lazy('hosting:login') + context_object_name = "orders" + model = HostingOrder + paginate_by = 10 - def get_context_data(self, **kwargs): + def get_queryset(self): user = self.request.user - orders = HostingOrder.objects.filter(customer__user=user) - context = { - 'orders':orders - } - - return context - - def get(self, request, *args, **kwargs): - - context = self.get_context_data() - - return render(request, self.template_name, context) + self.queryset = HostingOrder.objects.filter(customer__user=user) + return super(OrdersHostingListView, self).get_queryset() +class VirtualMachinesPlanListView(LoginRequiredMixin, ListView): + template_name = "hosting/virtual_machines.html" + login_url = reverse_lazy('hosting:login') + context_object_name = "vms" + model = VirtualMachinePlan + paginate_by = 10 + + def get_queryset(self): + user = self.request.user + self.queryset = VirtualMachinePlan.objects.active(user) + return super(VirtualMachinesPlanListView, self).get_queryset() + + +class VirtualMachineDetailListView(LoginRequiredMixin, DetailView): + template_name = "hosting/virtual_machine_detail.html" + login_url = reverse_lazy('hosting:login') + model = VirtualMachinePlan + context_object_name = "virtual_machine" diff --git a/membership/models.py b/membership/models.py index a607906c..75962660 100644 --- a/membership/models.py +++ b/membership/models.py @@ -131,7 +131,7 @@ class StripeCustomer(models.Model): Check if there is a registered stripe customer with that email or create a new one """ - + stripe_customer = None try: stripe_utils = StripeUtils() stripe_customer = cls.objects.get(user__email=email) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index c0ea630b..ce14d417 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -52,8 +52,6 @@ def handleStripeError(f): return handleProblems - - class StripeUtils(object): CURRENCY = 'chf' INTERVAL = 'month' @@ -71,7 +69,7 @@ class StripeUtils(object): customer = stripe.Customer.retrieve(id) except stripe.InvalidRequestError: customer = self.create_customer(token, user.email) - user.stripecustomer.stripe_id = customer.get('id') + user.stripecustomer.stripe_id = customer.get('response_object').get('id') user.stripecustomer.save() return customer