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 1 commit
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
Next Next commit
Add support for Coupons and Discounts objects
  • Loading branch information
Paris Kolios authored and blueyed committed Nov 20, 2017
commit f2820d402b83eb18c7a16f25f4dbcdf90fee4a42
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
67 changes: 64 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),
str(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,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 @@ -330,7 +388,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
41 changes: 41 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=1, percent_off=25, duration="repeating", duration_in_months=3,)
self.assertEquals(repr(c), "Coupon(pk=1, valid=False, amount_off=None, percent_off=25, currency='usd', duration='repeating', livemode=False, max_redemptions=None, times_redeemed=None, stripe_id='')")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uncommon to set id/pk manually, but ok.


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,34 @@ def test_invoice_status(self):
def test_invoice_status_not_paid(self):
self.assertEquals(Invoice(paid=False).status, "Open")

def test_discount_str(self):
c = Coupon(amount_off=5, duration="once")
d = Discount(coupon=c)
self.assertEquals(str(d), "Coupon for $5, once - None")

c = Coupon(percent_off=5, duration="repeating")
d = Discount(coupon=c)
self.assertEquals(str(d), "Coupon for 5% off, repeating - None")

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 +148,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