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

Coupons #506

Open
wants to merge 5 commits into
base: original
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pinax/stripe/actions/coupons.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ def sync_coupons():
coupons = iter(stripe.Coupon.all().data)

for coupon in coupons:
sync_coupon_from_stripe_data(coupon)


def sync_coupon_from_stripe_data(coupon, stripe_account=None):
defaults = dict(
amount_off=(
utils.convert_amount_for_db(coupon["amount_off"], coupon["currency"])
Expand All @@ -22,15 +26,24 @@ def sync_coupons():
currency=coupon["currency"] or "",
duration=coupon["duration"],
duration_in_months=coupon["duration_in_months"],
livemode=coupon["livemode"],
max_redemptions=coupon["max_redemptions"],
metadata=coupon["metadata"],
percent_off=coupon["percent_off"],
redeem_by=utils.convert_tstamp(coupon["redeem_by"]) if coupon["redeem_by"] else None,
times_redeemed=coupon["times_redeemed"],
valid=coupon["valid"],
stripe_account=stripe_account,
)
obj, created = models.Coupon.objects.get_or_create(
stripe_id=coupon["id"],
stripe_account=stripe_account,
defaults=defaults
)
utils.update_with_defaults(obj, defaults, created)
return obj


def purge_local(coupon, stripe_account=None):
return models.Coupon.objects.filter(
stripe_id=coupon["id"], stripe_account=stripe_account).delete()
13 changes: 13 additions & 0 deletions pinax/stripe/actions/subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import stripe

from .. import hooks, models, utils
from .coupons import sync_coupon_from_stripe_data


def cancel(subscription, at_period_end=True):
Expand Down Expand Up @@ -167,6 +168,18 @@ def sync_subscription_from_stripe_data(customer, subscription):
defaults=defaults
)
sub = utils.update_with_defaults(sub, defaults, created)
if subscription.get("discount", None):
defaults = {
"start": utils.convert_tstamp(subscription["discount"]["start"]),
"end": utils.convert_tstamp(subscription["discount"]["end"]) if subscription["discount"]["end"] else None,
"coupon": sync_coupon_from_stripe_data(subscription["discount"]["coupon"], stripe_account=customer.stripe_account),
}

obj, created = models.Discount.objects.get_or_create(
subscription=sub,
defaults=defaults
)
utils.update_with_defaults(obj, defaults, created)
return sub


Expand Down
58 changes: 58 additions & 0 deletions pinax/stripe/migrations/0011_coupons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.8 on 2017-11-16 14:51
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('pinax_stripe', '0010_connect'),
]

operations = [
migrations.CreateModel(
name='Discount',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start', models.DateTimeField(null=True)),
('end', models.DateTimeField(null=True)),
],
),
migrations.AddField(
model_name='coupon',
name='stripe_account',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Account'),
),
migrations.AlterField(
model_name='coupon',
name='duration',
field=models.CharField(choices=[('forever', 'forever'), ('once', 'once'), ('repeating', 'repeating')], default='once', max_length=10),
),
migrations.AlterField(
model_name='coupon',
name='stripe_id',
field=models.CharField(max_length=191),
),
migrations.AlterUniqueTogether(
name='coupon',
unique_together=set([('stripe_id', 'stripe_account')]),
),
migrations.AddField(
model_name='discount',
name='coupon',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Coupon'),
),
migrations.AddField(
model_name='discount',
name='customer',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Customer'),
),
migrations.AddField(
model_name='discount',
name='subscription',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Subscription'),
),
]
73 changes: 70 additions & 3 deletions pinax/stripe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,28 @@ def stripe_plan(self):


@python_2_unicode_compatible
class Coupon(StripeObject):
class Coupon(models.Model):
stripe_id = models.CharField(max_length=191)
created_at = models.DateTimeField(default=timezone.now)
stripe_account = models.ForeignKey(
"pinax_stripe.Account",
on_delete=models.CASCADE,
null=True,
default=None,
blank=True,
)

class Meta:
unique_together = ("stripe_id", "stripe_account")

DURATION_CHOICES = (
("forever", "forever"),
("once", "once"),
("repeating", "repeating"),
)
amount_off = models.DecimalField(decimal_places=2, max_digits=9, null=True)
currency = models.CharField(max_length=10, default="usd")
duration = models.CharField(max_length=10, default="once")
duration = models.CharField(max_length=10, default="once", choices=DURATION_CHOICES)
duration_in_months = models.PositiveIntegerField(null=True)
livemode = models.BooleanField(default=False)
max_redemptions = models.PositiveIntegerField(null=True)
Expand All @@ -112,6 +129,28 @@ def __str__(self):

return "Coupon for {}, {}".format(description, self.duration)

def __repr__(self):
return ("Coupon(pk={!r}, valid={!r}, amount_off={!r}, percent_off={!r}, currency={!r}, "
"duration={!r}, livemode={!r}, max_redemptions={!r}, times_redeemed={!r}, stripe_id={!r})".format(
self.pk,
self.valid,
self.amount_off,
self.percent_off,
str(self.currency),
str(self.duration),
self.livemode,
self.max_redemptions,
self.times_redeemed,
str(self.stripe_id),
))

@property
def stripe_coupon(self):
return stripe.Coupon.retrieve(
self.stripe_id,
stripe_account=self.stripe_account.stripe_id,
)


@python_2_unicode_compatible
class EventProcessingException(models.Model):
Expand Down Expand Up @@ -332,6 +371,31 @@ class BitcoinReceiver(StripeObject):
used_for_payment = models.BooleanField(default=False)


@python_2_unicode_compatible
class Discount(models.Model):

coupon = models.ForeignKey("Coupon", on_delete=models.CASCADE)
customer = models.OneToOneField("Customer", null=True, on_delete=models.CASCADE)
subscription = models.OneToOneField("Subscription", null=True, on_delete=models.CASCADE)
start = models.DateTimeField(null=True)
end = models.DateTimeField(null=True)

def __str__(self):
return "{} - {}".format(self.coupon, self.subscription)

def __repr__(self):
return "Discount(coupon={!r}, subscription={!r})".format(self.coupon, self.subscription)

def apply_discount(self, amount):
if self.end is not None and self.end < timezone.now():
return amount
if self.coupon.amount_off:
return decimal.Decimal(amount - self.coupon.amount_off)
elif self.coupon.percent_off:
return decimal.Decimal("{:.2f}".format(amount - (decimal.Decimal(self.coupon.percent_off) / 100 * amount)))
return amount


class Subscription(StripeAccountFromCustomerMixin, StripeObject):

STATUS_CURRENT = ["trialing", "active"]
Expand All @@ -356,7 +420,10 @@ def stripe_subscription(self):

@property
def total_amount(self):
return self.plan.amount * self.quantity
total_amount = self.plan.amount * self.quantity
if hasattr(self, "discount"):
total_amount = self.discount.apply_discount(total_amount)
return total_amount

def plan_display(self):
return self.plan.name
Expand Down
115 changes: 113 additions & 2 deletions pinax/stripe/tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ..actions import (
accounts,
charges,
coupons,
customers,
events,
externalaccounts,
Expand All @@ -30,7 +31,9 @@
BitcoinReceiver,
Card,
Charge,
Coupon,
Customer,
Discount,
Event,
Invoice,
Plan,
Expand Down Expand Up @@ -292,6 +295,24 @@ def test_update_availability(self, SyncMock):
self.assertTrue(SyncMock.called)


class CouponsTests(TestCase):

def test_purge_local(self):
Coupon.objects.create(stripe_id="100OFF", percent_off=decimal.Decimal(100.00))
self.assertTrue(Coupon.objects.filter(stripe_id="100OFF").exists())
coupons.purge_local({"id": "100OFF"})
self.assertFalse(Coupon.objects.filter(stripe_id="100OFF").exists())

def test_purge_local_with_account(self):
account = Account.objects.create(stripe_id="acc_XXX")
Coupon.objects.create(stripe_id="100OFF", percent_off=decimal.Decimal(100.00), stripe_account=account)
self.assertTrue(Coupon.objects.filter(stripe_id="100OFF").exists())
coupons.purge_local({"id": "100OFF"})
self.assertTrue(Coupon.objects.filter(stripe_id="100OFF").exists())
coupons.purge_local({"id": "100OFF"}, stripe_account=account)
self.assertFalse(Coupon.objects.filter(stripe_id="100OFF").exists())


class CustomersTests(TestCase):

def setUp(self):
Expand Down Expand Up @@ -1184,6 +1205,40 @@ def setUp(self):
stripe_id="cus_xxxxxxxxxxxxxxx"
)

def test_sync_coupon_from_stripe_data(self):
account = Account.objects.create(
stripe_id="acct_X",
type="standard",
)
coupon = {
"id": "35OFF",
"object": "coupon",
"amount_off": None,
"created": 1391694467,
"currency": None,
"duration": "repeating",
"duration_in_months": 3,
"livemode": True,
"max_redemptions": None,
"metadata": {
},
"percent_off": 35,
"redeem_by": None,
"times_redeemed": 1,
"valid": True
}
cs1 = coupons.sync_coupon_from_stripe_data(coupon)
self.assertTrue(cs1.livemode)
c1 = Coupon.objects.get(stripe_id=coupon["id"], stripe_account=None)
self.assertEquals(c1, cs1)
self.assertEquals(c1.percent_off, decimal.Decimal(35.00))

cs2 = coupons.sync_coupon_from_stripe_data(coupon, stripe_account=account)
c2 = Coupon.objects.get(stripe_id=coupon["id"], stripe_account=account)
self.assertEquals(c2, cs2)
self.assertEquals(c2.percent_off, decimal.Decimal(35.00))
self.assertFalse(c1 == c2)

@patch("stripe.Plan.all")
@patch("stripe.Plan.auto_paging_iter", create=True, side_effect=AttributeError)
def test_sync_plans_deprecated(self, PlanAutoPagerMock, PlanAllMock):
Expand Down Expand Up @@ -1581,6 +1636,34 @@ def test_sync_subscription_from_stripe_data(self):
self.assertEquals(Subscription.objects.get(stripe_id=subscription["id"]), sub)
self.assertEquals(sub.status, "trialing")

subscription["discount"] = {
"object": "discount",
"coupon": {
"id": "35OFF",
"object": "coupon",
"amount_off": None,
"created": 1391694467,
"currency": None,
"duration": "repeating",
"duration_in_months": 3,
"livemode": False,
"max_redemptions": None,
"metadata": {
},
"percent_off": 35,
"redeem_by": None,
"times_redeemed": 1,
"valid": True
},
"customer": self.customer.stripe_id,
"end": 1399384361,
"start": 1391694761,
"subscription": subscription["id"]
}
subscriptions.sync_subscription_from_stripe_data(self.customer, subscription)
d = Subscription.objects.get(stripe_id=subscription["id"]).discount
self.assertEquals(d.coupon.percent_off, decimal.Decimal(35.00))

def test_sync_subscription_from_stripe_data_updated(self):
Plan.objects.create(stripe_id="pro2", interval="month", interval_count=1, amount=decimal.Decimal("19.99"))
subscription = {
Expand All @@ -1592,7 +1675,30 @@ def test_sync_subscription_from_stripe_data_updated(self):
"current_period_end": 1448758544,
"current_period_start": 1448499344,
"customer": self.customer.stripe_id,
"discount": None,
"discount": {
"object": "discount",
"coupon": {
"id": "35OFF",
"object": "coupon",
"amount_off": None,
"created": 1391694467,
"currency": None,
"duration": "repeating",
"duration_in_months": 3,
"livemode": False,
"max_redemptions": None,
"metadata": {
},
"percent_off": 35,
"redeem_by": None,
"times_redeemed": 1,
"valid": True
},
"customer": self.customer.stripe_id,
"end": 1399384361,
"start": 1391694761,
"subscription": "sub_7Q4BX0HMfqTpN8"
},
"ended_at": None,
"metadata": {
},
Expand All @@ -1618,11 +1724,16 @@ def test_sync_subscription_from_stripe_data_updated(self):
"trial_end": 1448758544,
"trial_start": 1448499344
}
with self.assertRaises(Discount.DoesNotExist):
Discount.objects.get(subscription__stripe_id="sub_7Q4BX0HMfqTpN8")
subscriptions.sync_subscription_from_stripe_data(self.customer, subscription)
self.assertEquals(Subscription.objects.get(stripe_id=subscription["id"]).status, "trialing")
subscription.update({"status": "active"})
subscriptions.sync_subscription_from_stripe_data(self.customer, subscription)
self.assertEquals(Subscription.objects.get(stripe_id=subscription["id"]).status, "active")
s = Subscription.objects.get(stripe_id=subscription["id"])
self.assertEquals(s.status, "active")
self.assertTrue(Discount.objects.filter(subscription__stripe_id="sub_7Q4BX0HMfqTpN8").exists())
self.assertEquals(s.discount.coupon.stripe_id, "35OFF")

@patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data")
@patch("pinax.stripe.actions.sources.sync_payment_source_from_stripe_data")
Expand Down
Loading