Merge branch 'master' into task/3994/dg_add_signup_clarification_line

This commit is contained in:
PCoder 2017-12-27 21:18:13 +01:00
commit a743e4e8a0
8 changed files with 185 additions and 39 deletions

View file

@ -8,6 +8,7 @@ Next:
* #3601: [dcl, hosting] Change minimum required RAM from 2GB to 1GB * #3601: [dcl, hosting] Change minimum required RAM from 2GB to 1GB
* #3973: [dcl] Update datacenterlight and glasfaser contact address to Linthal and company name to "ungleich glarus ag" * #3973: [dcl] Update datacenterlight and glasfaser contact address to Linthal and company name to "ungleich glarus ag"
* #3993: [dg] Fix new user membership payment by setting cardholder_name field for UserBillingAddressForm * #3993: [dg] Fix new user membership payment by setting cardholder_name field for UserBillingAddressForm
* #3799: [dg] Make digital glarus billing work as monthly subscription
1.2.13: 2017-12-09 1.2.13: 2017-12-09
* [cms] Introduce UngleichHeaderBackgroundImageAndTextSliderPlugin that allows to have scrolling images and texts * [cms] Introduce UngleichHeaderBackgroundImageAndTextSliderPlugin that allows to have scrolling images and texts
* [cms] Remove <p> tag for ungleich cms customer item template * [cms] Remove <p> tag for ungleich cms customer item template

View file

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

View file

@ -1,6 +1,6 @@
import calendar import calendar
import time
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.db import models from django.db import models
@ -59,6 +59,17 @@ class MembershipType(models.Model):
return "{} - {}".format(datetime.strftime(start_date, "%b, %d %Y"), return "{} - {}".format(datetime.strftime(start_date, "%b, %d %Y"),
datetime.strftime(end_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): class Membership(models.Model):
type = models.ForeignKey(MembershipType) type = models.ForeignKey(MembershipType)
@ -71,12 +82,12 @@ class Membership(models.Model):
@classmethod @classmethod
def get_current_membership(cls, user): def get_current_membership(cls, user):
has_order_current_month = Q(
has_order_current_month = Q(membershiporder__customer__user=user, membershiporder__customer__user=user,
membershiporder__created_at__month=datetime.today().month) membershiporder__created_at__month=datetime.today().month
)
# import pdb;pdb.set_trace() # import pdb;pdb.set_trace()
return cls.objects.\ return cls.objects.filter(has_order_current_month).last()
filter(has_order_current_month).last()
# def get_current_active_membership(cls, user): # def get_current_active_membership(cls, user):
# membership = cls.get_current_membership(user) # membership = cls.get_current_membership(user)
@ -84,8 +95,7 @@ class Membership(models.Model):
@classmethod @classmethod
def get_by_user(cls, user): def get_by_user(cls, user):
return cls.objects.\ return cls.objects.filter(membershiporder__customer__user=user).last()
filter(membershiporder__customer__user=user).last()
@classmethod @classmethod
def create(cls, data): def create(cls, data):
@ -96,18 +106,23 @@ class Membership(models.Model):
def activate_or_crete(cls, data, user): def activate_or_crete(cls, data, user):
membership = cls.get_by_user(user) membership = cls.get_by_user(user)
membership_id = membership.id if membership else None 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 return obj
@classmethod @classmethod
def is_digitalglarus_active_member(cls, user): def is_digitalglarus_active_member(cls, user):
# past_month = (datetime.today() - relativedelta(months=1)).month # past_month = (datetime.today() - relativedelta(months=1)).month
has_order_current_month = Q(membershiporder__customer__user=user, has_order_current_month = Q(
membershiporder__created_at__month=datetime.today().month) membershiporder__customer__user=user,
membershiporder__created_at__month=datetime.today().month
)
# has_order_past_month = Q(membershiporder__customer__user=user, # has_order_past_month = Q(membershiporder__customer__user=user,
# membershiporder__created_at__month=past_month) # membershiporder__created_at__month=past_month)
active_membership = Q(active=True) 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).\ return cls.objects.filter(has_order_current_month).\
filter(active_membership).exists() filter(active_membership).exists()
@ -129,6 +144,7 @@ class MembershipOrder(Ordereable, models.Model):
membership = models.ForeignKey(Membership) membership = models.ForeignKey(Membership)
start_date = models.DateField() start_date = models.DateField()
end_date = models.DateField() end_date = models.DateField()
stripe_subscription_id = models.CharField(max_length=100, null=True)
@classmethod @classmethod
def current_membership_dates(cls, user): def current_membership_dates(cls, user):
@ -172,10 +188,12 @@ class MembershipOrder(Ordereable, models.Model):
@classmethod @classmethod
def create(cls, data): def create(cls, data):
stripe_charge = data.pop('stripe_charge', None) stripe_charge = data.pop('stripe_charge', None)
stripe_subscription_id = data.pop('stripe_subscription_id', None)
instance = cls.objects.create(**data) instance = cls.objects.create(**data)
instance.stripe_charge_id = stripe_charge.id instance.stripe_charge_id = stripe_charge.id
instance.last4 = stripe_charge.source.last4 instance.last4 = stripe_charge.source.last4
instance.cc_brand = stripe_charge.source.brand instance.cc_brand = stripe_charge.source.brand
instance.stripe_subscription_id = stripe_subscription_id
instance.save() instance.save()
return instance return instance

View file

@ -95,12 +95,9 @@
<a class="btn btn-primary btn-grey btn-deactivate print" href="{% url 'digitalglarus:membership_deactivate' %}">Deactivate</a> <a class="btn btn-primary btn-grey btn-deactivate print" href="{% url 'digitalglarus:membership_deactivate' %}">Deactivate</a>
</div> </div>
{% elif not current_membership.active %} {% elif not current_membership.active %}
<form method="POST" action="{% url 'digitalglarus:membership_reactivate' %}">
{% csrf_token %}
<div class="edit-button"> <div class="edit-button">
<button type="submit" class="btn btn-primary btn-grey btn-deactivate print" href="{% url 'digitalglarus:membership_reactivate' %}">Reactivate</button> <a class="btn btn-primary btn-grey btn-deactivate" href="{% url 'digitalglarus:membership_pricing' %}">Reactivate</a>
</div> </div>
</form>
{% endif %} {% endif %}
{% else %} {% else %}
<div class="edit-button"> <div class="edit-button">

View file

@ -1,5 +1,5 @@
from model_mommy import mommy from model_mommy import mommy
from unittest import mock from unittest import mock, skipIf
from django.test import TestCase from django.test import TestCase
from django.conf import settings from django.conf import settings
@ -126,6 +126,11 @@ class MembershipPaymentViewTest(BaseTestCase):
self.assertEqual(response.context['membership_type'], self.assertEqual(response.context['membership_type'],
self.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') @mock.patch('utils.stripe_utils.StripeUtils.create_customer')
def test_post(self, stripe_mocked_call): def test_post(self, stripe_mocked_call):

View file

@ -1,3 +1,5 @@
import logging
from django.conf import settings from django.conf import settings
from django.shortcuts import render from django.shortcuts import render
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
@ -9,25 +11,34 @@ from django.utils.translation import get_language
from djangocms_blog.models import Post from djangocms_blog.models import Post
from django.contrib import messages from django.contrib import messages
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from .models import Supporter
from .mixins import ChangeMembershipStatusMixin
from utils.forms import ContactUsForm from utils.forms import ContactUsForm
from utils.mailer import BaseEmail from utils.mailer import BaseEmail
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from membership.models import StripeCustomer from membership.models import StripeCustomer
from utils.views import LoginViewMixin, SignupViewMixin, \ from utils.views import (
PasswordResetViewMixin, PasswordResetConfirmViewMixin LoginViewMixin, SignupViewMixin, PasswordResetViewMixin,
from utils.forms import PasswordResetRequestForm, UserBillingAddressForm, EditCreditCardForm PasswordResetConfirmViewMixin
)
from utils.forms import (
PasswordResetRequestForm, UserBillingAddressForm, EditCreditCardForm
)
from utils.stripe_utils import StripeUtils from utils.stripe_utils import StripeUtils
from utils.models import UserBillingAddress 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 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,\ logger = logging.getLogger(__name__)
BookingOrder, BookingCancellation
from .mixins import MembershipRequiredMixin, IsNotMemberMixin
class IndexView(TemplateView): class IndexView(TemplateView):
@ -271,7 +282,6 @@ class BookingPaymentView(LoginRequiredMixin, MembershipRequiredMixin, FormView):
booking_data = { booking_data = {
'start_date': start_date, 'start_date': start_date,
'end_date': end_date, 'end_date': end_date,
'start_date': start_date,
'free_days': free_days, 'free_days': free_days,
'price': normal_price, 'price': normal_price,
'final_price': final_price, 'final_price': final_price,
@ -355,16 +365,21 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
membership_type = data.get('membership_type') membership_type = data.get('membership_type')
# Get or create stripe customer # Get or create stripe customer
customer = StripeCustomer.get_or_create(email=self.request.user.email, customer = StripeCustomer.get_or_create(
token=token) email=self.request.user.email, token=token
)
if not customer: if not customer:
form.add_error("__all__", "Invalid credit card") 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 # Make stripe charge to a customer
stripe_utils = StripeUtils() stripe_utils = StripeUtils()
charge_response = stripe_utils.make_charge(amount=membership_type.first_month_price, charge_response = stripe_utils.make_charge(
customer=customer.stripe_id) amount=membership_type.first_month_price,
customer=customer.stripe_id
)
charge = charge_response.get('response_object') charge = charge_response.get('response_object')
# Check if the payment was approved # Check if the payment was approved
@ -373,6 +388,58 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
'paymentError': charge_response.get('error'), 'paymentError': charge_response.get('error'),
'form': form '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) return render(request, self.template_name, context)
charge = charge_response.get('response_object') charge = charge_response.get('response_object')
@ -412,6 +479,7 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
'customer': customer, 'customer': customer,
'billing_address': billing_address, 'billing_address': billing_address,
'stripe_charge': charge, 'stripe_charge': charge,
'stripe_subscription_id': stripe_subscription_obj.id,
'amount': membership_type.first_month_price, 'amount': membership_type.first_month_price,
'start_date': membership_start_date, 'start_date': membership_start_date,
'end_date': membership_end_date 'end_date': membership_end_date
@ -481,8 +549,43 @@ class MembershipDeactivateView(LoginRequiredMixin, UpdateView):
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
membership = self.get_object() membership = self.get_object()
membership.deactivate() membership.deactivate()
messages.add_message(
messages.add_message(self.request, messages.SUCCESS, self.success_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) return HttpResponseRedirect(self.success_url)

View file

@ -1,3 +1,4 @@
{% load i18n %}
{% if instance.image %} {% if instance.image %}
<div class="bg_img" style="background-image:url({{ instance.image.url }})"></div> <div class="bg_img" style="background-image:url({{ instance.image.url }})"></div>
{% endif %} {% endif %}

View file

@ -210,12 +210,14 @@ class StripeUtils(object):
return return_value return return_value
@handleStripeError @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 Subscribes the given customer to the list of given plans
:param customer: The stripe customer identifier :param customer: The stripe customer identifier
:param plans: A list of stripe plans. :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 Ref: https://stripe.com/docs/api/python#create_subscription-items
e.g. e.g.
plans = [ plans = [
@ -227,8 +229,7 @@ class StripeUtils(object):
""" """
subscription_result = self.stripe.Subscription.create( subscription_result = self.stripe.Subscription.create(
customer=customer, customer=customer, items=plans, trial_end=trial_end
items=plans,
) )
return subscription_result return subscription_result