Merge pull request #553 from pcoder/task/3799/dg_monthly_subscription
Task/3799/dg monthly subscription
This commit is contained in:
commit
9c513e667f
6 changed files with 183 additions and 39 deletions
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue