Merge pull request #553 from pcoder/task/3799/dg_monthly_subscription

Task/3799/dg monthly subscription
This commit is contained in:
Pcoder 2017-12-27 20:29:39 +01:00 committed by GitHub
commit 9c513e667f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 183 additions and 39 deletions

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

View file

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

View file

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

View file

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

View file

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