Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding better handling of canceling subscriptions #30

Merged
merged 1 commit into from
Sep 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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