Skip to content

Commit

Permalink
Merge pull request #30 from ArabellaTech/feature/fixing-end-subscript…
Browse files Browse the repository at this point in the history
…ions

Adding better handling of canceling subscriptions
  • Loading branch information
jacoor committed Sep 9, 2017
2 parents de673e9 + 941a31e commit f4d859e
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 15 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Change Log
All notable changes to this project will be documented in this file.

## [0.2.20]
### Added
- Improved end_subscriptions

## [0.2.19]
### Added
- Bugfixes
Expand Down
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ Utility functions for subscriptions
* subscription.refresh_from_stripe() - gets updated subscription data from Stripe. Example usage: parsing webhooks - when webhook altering subscription is received it is good practice to verify the subscription at Stripe before making any actions.
* subscription.cancel() - cancels subscription at Stripe.
* StripeSubscription.get_subcriptions_for_cancel() - returns all subscriptions that should be canceled. Stripe does not support end date for subscription so it is up the user to implement expiration mechanism. Subscription has end_date that can be used for that.
* StripeSubscription.end_subscriptions() - cancels all subscriptions on Stripe that has passed end date. Use with caution, check internal comments.
* management command: end_subscription.py. Terminates outdated subscriptions in a safe way. In case of error returns it at the end, using Sentry if available or in console. Should be used in cron script.
* StripeSubscription.end_subscriptions() - cancels all subscriptions on Stripe that has passed end date. Use with caution, check internal comments.
* management command: end_subscription.py. Terminates outdated subscriptions in a safe way. In case of error returns it at the end, using Sentry if available or in console. Should be used in cron script. By default sets at_period_end=True.

Subscription Plans
------------------
Expand Down
2 changes: 1 addition & 1 deletion aa_stripe/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
__title__ = "Arabella Stripe"
__version__ = "0.2.19"
__version__ = "0.2.20"
__author__ = "Jacek Ostanski"
__license__ = "MIT"
__copyright__ = "Copyright 2017 Arabella"
Expand Down
9 changes: 8 additions & 1 deletion aa_stripe/management/commands/end_subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,21 @@


class Command(BaseCommand):
"""
Terminates outdated subscriptions at period end.
Should be run hourly.
Exceptions are queued and returned at the end.
"""

help = "Terminate outdated subscriptions"

def handle(self, *args, **options):
subscriptions = StripeSubscription.get_subcriptions_for_cancel()
exceptions = []
for subscription in subscriptions:
try:
subscription.cancel()
subscription.cancel(at_period_end=True)
sleep(0.25) # 4 requests per second tops
except Exception as e:
exc_type, exc_value, exc_traceback = sys.exc_info()
Expand Down
20 changes: 20 additions & 0 deletions aa_stripe/migrations/0013_stripesubscription_at_period_end.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-09-09 16:18
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('aa_stripe', '0012_auto_20170908_1150'),
]

operations = [
migrations.AddField(
model_name='stripesubscription',
name='at_period_end',
field=models.BooleanField(default=False),
),
]
23 changes: 13 additions & 10 deletions aa_stripe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import simplejson as json
import six
import stripe
from dateutil.relativedelta import relativedelta
from django import dispatch
from django.conf import settings
from django.contrib.contenttypes import fields as generic
Expand Down Expand Up @@ -405,6 +406,7 @@ class StripeSubscription(StripeBasicModel):
help_text="https://stripe.com/docs/api/python#create_subscription-coupon")
end_date = models.DateField(null=True, blank=True, db_index=True)
canceled_at = models.DateTimeField(null=True, blank=True, db_index=True)
at_period_end = models.BooleanField(default=False)

def create_at_stripe(self):
if self.is_created_at_stripe:
Expand Down Expand Up @@ -446,30 +448,31 @@ def refresh_from_stripe(self):
self.set_stripe_data(subscription)
return subscription

def _stripe_cancel(self):
def _stripe_cancel(self, at_period_end=False):
subscription = self.refresh_from_stripe()
if subscription["status"] != "canceled":
return stripe.Subscription.delete(subscription)
return subscription.delete(at_period_end=at_period_end)

def cancel(self):
sub = self._stripe_cancel()
if sub and sub["status"] == "canceled":
def cancel(self, at_period_end=False):
sub = self._stripe_cancel(at_period_end=at_period_end)
if sub and sub["status"] == "canceled" or sub["cancel_at_period_end"]:
self.canceled_at = timezone.now()
self.status = self.STATUS_CANCELED
self.at_period_end = at_period_end
self.save()

@classmethod
def get_subcriptions_for_cancel(cls):
today = timezone.localtime(timezone.now()).date()
today = timezone.localtime(timezone.now() + relativedelta(hours=1)).date()
return cls.objects.filter(
end_date__lte=today, status=cls.STATUS_ACTIVE)

@classmethod
def end_subscriptions(cls):
# do not use in cron - one broken subscription will kill all.
# instead please use end_subscriptions.py script.
def end_subscriptions(cls, at_period_end=False):
# do not use in cron - one broken subscription will exit script.
# Instead please use end_subscriptions.py script.
for subscription in cls.get_subcriptions_for_cancel():
subscription.cancel()
subscription.cancel(at_period_end)
sleep(0.25) # 4 requests per second tops


Expand Down
2 changes: 1 addition & 1 deletion tests/test_subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def test_subscriptions_end(self):

with freeze_time("2017-07-04 12:00:00+00"):
call_command("end_subscriptions")
mocked_cancel.assert_called_with()
mocked_cancel.assert_called_with(at_period_end=True)

subscription.refresh_from_db()
self.assertIsNotNone(subscription.canceled_at)
Expand Down

0 comments on commit f4d859e

Please sign in to comment.