diff --git a/digitalglarus/migrations/0025_membershiporder_stripe_subscription_id.py b/digitalglarus/migrations/0025_membershiporder_stripe_subscription_id.py
new file mode 100644
index 00000000..127d5ff8
--- /dev/null
+++ b/digitalglarus/migrations/0025_membershiporder_stripe_subscription_id.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2017-12-23 22:56
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('digitalglarus', '0024_bookingcancellation'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='membershiporder',
+ name='stripe_subscription_id',
+ field=models.CharField(max_length=100, null=True),
+ ),
+ ]
diff --git a/digitalglarus/models.py b/digitalglarus/models.py
index 16d6b639..fc4289aa 100644
--- a/digitalglarus/models.py
+++ b/digitalglarus/models.py
@@ -1,6 +1,6 @@
-
-
import calendar
+import time
+
from datetime import datetime, date, timedelta
from dateutil.relativedelta import relativedelta
from django.db import models
@@ -59,6 +59,17 @@ class MembershipType(models.Model):
return "{} - {}".format(datetime.strftime(start_date, "%b, %d %Y"),
datetime.strftime(end_date, "%b, %d %Y"))
+ @cached_property
+ def next_month_in_sec_since_epoch(self):
+ """
+ First day of the next month expressed in seconds since the epoch time
+ :return: Time in seconds
+ """
+ start_date, end_date = self.first_month_range
+ first_day_next_month = end_date + timedelta(days=1)
+ epoch_time = int(time.mktime(first_day_next_month.timetuple()))
+ return epoch_time
+
class Membership(models.Model):
type = models.ForeignKey(MembershipType)
@@ -71,12 +82,12 @@ class Membership(models.Model):
@classmethod
def get_current_membership(cls, user):
-
- has_order_current_month = Q(membershiporder__customer__user=user,
- membershiporder__created_at__month=datetime.today().month)
+ has_order_current_month = Q(
+ membershiporder__customer__user=user,
+ membershiporder__created_at__month=datetime.today().month
+ )
# import pdb;pdb.set_trace()
- return cls.objects.\
- filter(has_order_current_month).last()
+ return cls.objects.filter(has_order_current_month).last()
# def get_current_active_membership(cls, user):
# membership = cls.get_current_membership(user)
@@ -84,8 +95,7 @@ class Membership(models.Model):
@classmethod
def get_by_user(cls, user):
- return cls.objects.\
- filter(membershiporder__customer__user=user).last()
+ return cls.objects.filter(membershiporder__customer__user=user).last()
@classmethod
def create(cls, data):
@@ -96,18 +106,23 @@ class Membership(models.Model):
def activate_or_crete(cls, data, user):
membership = cls.get_by_user(user)
membership_id = membership.id if membership else None
- obj, created = cls.objects.update_or_create(id=membership_id, defaults=data)
+ obj, created = cls.objects.update_or_create(
+ id=membership_id, defaults=data
+ )
return obj
@classmethod
def is_digitalglarus_active_member(cls, user):
# past_month = (datetime.today() - relativedelta(months=1)).month
- has_order_current_month = Q(membershiporder__customer__user=user,
- membershiporder__created_at__month=datetime.today().month)
+ has_order_current_month = Q(
+ membershiporder__customer__user=user,
+ membershiporder__created_at__month=datetime.today().month
+ )
# has_order_past_month = Q(membershiporder__customer__user=user,
# membershiporder__created_at__month=past_month)
active_membership = Q(active=True)
- # return cls.objects.filter(has_order_past_month | has_order_current_month).\
+ # return cls.objects.filter(
+ # has_order_past_month | has_order_current_month).\
return cls.objects.filter(has_order_current_month).\
filter(active_membership).exists()
@@ -129,6 +144,7 @@ class MembershipOrder(Ordereable, models.Model):
membership = models.ForeignKey(Membership)
start_date = models.DateField()
end_date = models.DateField()
+ stripe_subscription_id = models.CharField(max_length=100, null=True)
@classmethod
def current_membership_dates(cls, user):
@@ -172,10 +188,12 @@ class MembershipOrder(Ordereable, models.Model):
@classmethod
def create(cls, data):
stripe_charge = data.pop('stripe_charge', None)
+ stripe_subscription_id = data.pop('stripe_subscription_id', None)
instance = cls.objects.create(**data)
instance.stripe_charge_id = stripe_charge.id
instance.last4 = stripe_charge.source.last4
instance.cc_brand = stripe_charge.source.brand
+ instance.stripe_subscription_id = stripe_subscription_id
instance.save()
return instance
diff --git a/digitalglarus/templates/digitalglarus/membership_orders_list.html b/digitalglarus/templates/digitalglarus/membership_orders_list.html
index ceeea6f1..dd49837d 100644
--- a/digitalglarus/templates/digitalglarus/membership_orders_list.html
+++ b/digitalglarus/templates/digitalglarus/membership_orders_list.html
@@ -95,12 +95,9 @@
Deactivate
{% elif not current_membership.active %}
-
{% endif %}
{% else %}
diff --git a/digitalglarus/test_views.py b/digitalglarus/test_views.py
index cdd23bad..b7fc6c3a 100644
--- a/digitalglarus/test_views.py
+++ b/digitalglarus/test_views.py
@@ -1,5 +1,5 @@
from model_mommy import mommy
-from unittest import mock
+from unittest import mock, skipIf
from django.test import TestCase
from django.conf import settings
@@ -126,6 +126,11 @@ class MembershipPaymentViewTest(BaseTestCase):
self.assertEqual(response.context['membership_type'],
self.membership_type)
+ @skipIf(
+ settings.STRIPE_API_PRIVATE_KEY_TEST is None or
+ settings.STRIPE_API_PRIVATE_KEY_TEST is "",
+ """Stripe details unavailable, so skipping CeleryTaskTestCase"""
+ )
@mock.patch('utils.stripe_utils.StripeUtils.create_customer')
def test_post(self, stripe_mocked_call):
diff --git a/digitalglarus/views.py b/digitalglarus/views.py
index 96983d9b..32d8e1f5 100644
--- a/digitalglarus/views.py
+++ b/digitalglarus/views.py
@@ -1,3 +1,5 @@
+import logging
+
from django.conf import settings
from django.shortcuts import render
from django.http import HttpResponseRedirect
@@ -9,25 +11,34 @@ from django.utils.translation import get_language
from djangocms_blog.models import Post
from django.contrib import messages
from django.views.generic import DetailView, ListView
-from .models import Supporter
-from .mixins import ChangeMembershipStatusMixin
from utils.forms import ContactUsForm
from utils.mailer import BaseEmail
from django.views.generic.edit import FormView
from membership.models import StripeCustomer
-from utils.views import LoginViewMixin, SignupViewMixin, \
- PasswordResetViewMixin, PasswordResetConfirmViewMixin
-from utils.forms import PasswordResetRequestForm, UserBillingAddressForm, EditCreditCardForm
+from utils.views import (
+ LoginViewMixin, SignupViewMixin, PasswordResetViewMixin,
+ PasswordResetConfirmViewMixin
+)
+from utils.forms import (
+ PasswordResetRequestForm, UserBillingAddressForm, EditCreditCardForm
+)
from utils.stripe_utils import StripeUtils
from utils.models import UserBillingAddress
+from utils.tasks import send_plain_email_task
-from .forms import LoginForm, SignupForm, MembershipBillingForm, BookingDateForm,\
+from .forms import (
+ LoginForm, SignupForm, MembershipBillingForm, BookingDateForm,
BookingBillingForm, CancelBookingForm
+)
+from .models import (
+ MembershipType, Membership, MembershipOrder, Booking, BookingPrice,
+ BookingOrder, BookingCancellation, Supporter
+)
+from .mixins import (
+ MembershipRequiredMixin, IsNotMemberMixin, ChangeMembershipStatusMixin
+)
-from .models import MembershipType, Membership, MembershipOrder, Booking, BookingPrice,\
- BookingOrder, BookingCancellation
-
-from .mixins import MembershipRequiredMixin, IsNotMemberMixin
+logger = logging.getLogger(__name__)
class IndexView(TemplateView):
@@ -271,7 +282,6 @@ class BookingPaymentView(LoginRequiredMixin, MembershipRequiredMixin, FormView):
booking_data = {
'start_date': start_date,
'end_date': end_date,
- 'start_date': start_date,
'free_days': free_days,
'price': normal_price,
'final_price': final_price,
@@ -355,16 +365,21 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
membership_type = data.get('membership_type')
# Get or create stripe customer
- customer = StripeCustomer.get_or_create(email=self.request.user.email,
- token=token)
+ customer = StripeCustomer.get_or_create(
+ email=self.request.user.email, token=token
+ )
if not customer:
form.add_error("__all__", "Invalid credit card")
- return self.render_to_response(self.get_context_data(form=form))
+ return self.render_to_response(
+ self.get_context_data(form=form)
+ )
# Make stripe charge to a customer
stripe_utils = StripeUtils()
- charge_response = stripe_utils.make_charge(amount=membership_type.first_month_price,
- customer=customer.stripe_id)
+ charge_response = stripe_utils.make_charge(
+ amount=membership_type.first_month_price,
+ customer=customer.stripe_id
+ )
charge = charge_response.get('response_object')
# Check if the payment was approved
@@ -373,6 +388,58 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
'paymentError': charge_response.get('error'),
'form': form
})
+ email_to_admin_data = {
+ 'subject': "Could not create charge for Digital Glarus "
+ "user: {user}".format(
+ user=self.request.user.email
+ ),
+ 'from_email': 'info@digitalglarus.ch',
+ 'to': ['info@ungleich.ch'],
+ 'body': "\n".join(
+ ["%s=%s" % (k, v) for (k, v) in
+ charge_response.items()]),
+ }
+ send_plain_email_task.delay(email_to_admin_data)
+ return render(request, self.template_name, context)
+
+ # Subscribe the customer to dg plan from the next month onwards
+ stripe_plan = stripe_utils.get_or_create_stripe_plan(
+ amount=membership_type.price,
+ name='Digital Glarus {sub_type_name} Subscription'.format(
+ sub_type_name=membership_type.name
+ ),
+ stripe_plan_id='dg-{sub_type_name}'.format(
+ sub_type_name=membership_type.name
+ )
+ )
+ subscription_result = stripe_utils.subscribe_customer_to_plan(
+ customer.stripe_id,
+ [{"plan": stripe_plan.get('response_object').stripe_plan_id}],
+ trial_end=membership_type.next_month_in_sec_since_epoch
+ )
+ stripe_subscription_obj = subscription_result.get(
+ 'response_object'
+ )
+ # Check if call to create subscription was ok
+ if (stripe_subscription_obj is None or
+ (stripe_subscription_obj.status != 'active' and
+ stripe_subscription_obj.status != 'trialing')):
+ context.update({
+ 'paymentError': subscription_result.get('error'),
+ 'form': form
+ })
+ email_to_admin_data = {
+ 'subject': "Could not create Stripe subscription for "
+ "Digital Glarus user: {user}".format(
+ user=self.request.user.email
+ ),
+ 'from_email': 'info@digitalglarus.ch',
+ 'to': ['info@ungleich.ch'],
+ 'body': "\n".join(
+ ["%s=%s" % (k, v) for (k, v) in
+ subscription_result.items()]),
+ }
+ send_plain_email_task.delay(email_to_admin_data)
return render(request, self.template_name, context)
charge = charge_response.get('response_object')
@@ -412,6 +479,7 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
'customer': customer,
'billing_address': billing_address,
'stripe_charge': charge,
+ 'stripe_subscription_id': stripe_subscription_obj.id,
'amount': membership_type.first_month_price,
'start_date': membership_start_date,
'end_date': membership_end_date
@@ -481,8 +549,43 @@ class MembershipDeactivateView(LoginRequiredMixin, UpdateView):
def post(self, *args, **kwargs):
membership = self.get_object()
membership.deactivate()
-
- messages.add_message(self.request, messages.SUCCESS, self.success_message)
+ messages.add_message(
+ self.request, messages.SUCCESS, self.success_message
+ )
+ # cancel Stripe subscription
+ stripe_utils = StripeUtils()
+ membership_order = MembershipOrder.objects.filter(
+ customer__user=self.request.user
+ ).last()
+ if membership_order:
+ if membership_order.stripe_subscription_id:
+ result = stripe_utils.unsubscribe_customer(
+ subscription_id=membership_order.stripe_subscription_id
+ )
+ stripe_subscription_obj = result.get('response_object')
+ # Check if the subscription was canceled
+ if (stripe_subscription_obj is None or
+ stripe_subscription_obj.status != 'canceled'):
+ error_msg = result.get('error')
+ logger.error(
+ "Could not cancel Digital Glarus subscription. "
+ "Reason: {reason}".format(
+ reason=error_msg
+ )
+ )
+ else:
+ logger.error(
+ "User {user} may have Stripe subscriptions created "
+ "manually. Please check.".format(
+ user=self.request.user.name
+ )
+ )
+ else:
+ logger.error(
+ "MembershipOrder for {user} not found".format(
+ user=self.request.user.name
+ )
+ )
return HttpResponseRedirect(self.success_url)
diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py
index 58840be0..79bca243 100644
--- a/utils/stripe_utils.py
+++ b/utils/stripe_utils.py
@@ -210,12 +210,14 @@ class StripeUtils(object):
return return_value
@handleStripeError
- def subscribe_customer_to_plan(self, customer, plans):
+ def subscribe_customer_to_plan(self, customer, plans, trial_end=None):
"""
Subscribes the given customer to the list of given plans
:param customer: The stripe customer identifier
:param plans: A list of stripe plans.
+ :param trial_end: An integer representing when the Stripe subscription
+ is supposed to end
Ref: https://stripe.com/docs/api/python#create_subscription-items
e.g.
plans = [
@@ -227,8 +229,7 @@ class StripeUtils(object):
"""
subscription_result = self.stripe.Subscription.create(
- customer=customer,
- items=plans,
+ customer=customer, items=plans, trial_end=trial_end
)
return subscription_result