Skip to content

Commit

Permalink
Add support for Coupons and Discounts objects
Browse files Browse the repository at this point in the history
  • Loading branch information
Paris Kolios authored and ticosax committed Nov 16, 2017
1 parent 6360316 commit 131931a
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 5 deletions.
12 changes: 12 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 @@ -28,9 +32,17 @@ def sync_coupons():
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 @@ -162,6 +163,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
64 changes: 61 additions & 3 deletions pinax/stripe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,29 @@ def __repr__(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 @@ -105,6 +123,21 @@ 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),
self.duration,
self.livemode,
self.max_redemptions,
self.times_redeemed,
str(self.stripe_id),
))


@python_2_unicode_compatible
class EventProcessingException(models.Model):
Expand Down Expand Up @@ -306,6 +339,28 @@ 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 __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 @@ -330,7 +385,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
113 changes: 111 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 @@ -283,6 +286,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 @@ -1164,6 +1185,38 @@ 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": False,
"max_redemptions": None,
"metadata": {
},
"percent_off": 35,
"redeem_by": None,
"times_redeemed": 1,
"valid": True
}
cs1 = coupons.sync_coupon_from_stripe_data(coupon)
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 @@ -1561,6 +1614,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 @@ -1572,7 +1653,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 @@ -1598,11 +1702,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
32 changes: 32 additions & 0 deletions pinax/stripe/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Charge,
Coupon,
Customer,
Discount,
Event,
EventProcessingException,
Invoice,
Expand Down Expand Up @@ -76,6 +77,10 @@ def test_plan_display_invoiceitem(self):
i = InvoiceItem(plan=p)
self.assertEquals(i.plan_display(), "My Plan")

def test_coupon_repr(self):
c = Coupon(id="test", percent_off=25, duration="repeating", duration_in_months=3,)
self.assertEquals(repr(c), "Coupon(pk='test', valid=False, amount_off=None, percent_off=25, currency='usd', duration='repeating', livemode=False, max_redemptions=None, times_redeemed=None, stripe_id='')")

def test_coupon_percent(self):
c = Coupon(percent_off=25, duration="repeating", duration_in_months=3)
self.assertEquals(str(c), "Coupon for 25% off, repeating")
Expand All @@ -97,6 +102,25 @@ def test_invoice_status(self):
def test_invoice_status_not_paid(self):
self.assertEquals(Invoice(paid=False).status, "Open")

def test_discount_repr(self):
c = Coupon()
d = Discount(coupon=c)
self.assertEquals(repr(d), "Discount(coupon=Coupon(pk=None, valid=False, amount_off=None, percent_off=None, currency='usd', duration='once', livemode=False, max_redemptions=None, times_redeemed=None, stripe_id=''), subscription=None)")

def test_discount_apply_discount(self):
c = Coupon(duration="once", currency="usd")
d = Discount(coupon=c)
self.assertEquals(d.apply_discount(decimal.Decimal(50.00)), decimal.Decimal(50.00))
c = Coupon(amount_off=decimal.Decimal(50.00), duration="once", currency="usd")
d = Discount(coupon=c)
self.assertEquals(d.apply_discount(decimal.Decimal(50.00)), decimal.Decimal(0.00))
c = Coupon(percent_off=decimal.Decimal(50.00), duration="once", currency="usd")
d.coupon = c
self.assertEquals(d.apply_discount(decimal.Decimal(100.00)), decimal.Decimal(50.00))
c = Coupon(percent_off=decimal.Decimal(50.00), duration="repeating", currency="usd")
d.end = timezone.now() - datetime.timedelta(days=1)
self.assertEquals(d.apply_discount(decimal.Decimal(100.00)), decimal.Decimal(100.00))

def test_subscription_repr(self):
s = Subscription()
self.assertEquals(repr(s), "Subscription(pk=None, customer=None, plan=None, status='', stripe_id='')")
Expand All @@ -115,6 +139,14 @@ def test_subscription_total_amount(self):
sub = Subscription(plan=Plan(name="Pro Plan", amount=decimal.Decimal("100")), quantity=2)
self.assertEquals(sub.total_amount, decimal.Decimal("200"))

@patch("pinax.stripe.models.Discount.apply_discount")
def test_subscription_total_amount_discount(self, ApplyDiscountMock):
c = Coupon(amount_off=decimal.Decimal(50.00), duration="once", currency="usd")
sub = Subscription(plan=Plan(name="Pro Plan", amount=decimal.Decimal("100")), quantity=2)
Discount(coupon=c, subscription=sub)
sub.total_amount()
self.assertTrue(ApplyDiscountMock.called)

def test_subscription_plan_display(self):
sub = Subscription(plan=Plan(name="Pro Plan"))
self.assertEquals(sub.plan_display(), "Pro Plan")
Expand Down
Loading

0 comments on commit 131931a

Please sign in to comment.