Skip to content

Commit

Permalink
Merge branch 'master' into issue-294
Browse files Browse the repository at this point in the history
  • Loading branch information
paltman authored Dec 21, 2016
2 parents 321585b + acc400e commit 9e9e0b8
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 7 deletions.
37 changes: 37 additions & 0 deletions pinax/stripe/actions/coupons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import stripe

from .. import utils
from .. import models


def sync_coupons():
"""
Syncronizes all coupons from the Stripe API
"""
try:
coupons = stripe.Coupon.auto_paging_iter()
except AttributeError:
coupons = iter(stripe.Coupon.all().data)

for coupon in coupons:
defaults = dict(
amount_off=(
utils.convert_amount_for_db(coupon["amount_off"], coupon["currency"])
if coupon["amount_off"]
else None
),
currency=coupon["currency"] or "",
duration=coupon["duration"],
duration_in_months=coupon["duration_in_months"],
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"],
)
obj, created = models.Coupon.objects.get_or_create(
stripe_id=coupon["id"],
defaults=defaults
)
utils.update_with_defaults(obj, defaults, created)
20 changes: 15 additions & 5 deletions pinax/stripe/actions/customers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.db import IntegrityError, transaction
from django.utils import timezone
from django.utils.encoding import smart_str

Expand Down Expand Up @@ -28,7 +29,9 @@ def can_charge(customer):

def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True):
"""
Creates a Stripe customer
Creates a Stripe customer.
If a customer already exists, the existing customer will be returned.
Args:
user: a user object
Expand All @@ -48,10 +51,17 @@ def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_imme
plan=plan,
trial_end=trial_end
)
cus = models.Customer.objects.create(
user=user,
stripe_id=stripe_customer["id"]
)
try:
with transaction.atomic():
cus = models.Customer.objects.create(
user=user,
stripe_id=stripe_customer["id"]
)
except IntegrityError:
# There is already a Customer object for this user
stripe.Customer.retrieve(stripe_customer["id"]).delete()
return models.Customer.objects.get(user=user)

sync_customer(cus, stripe_customer)

if plan and charge_immediately:
Expand Down
3 changes: 3 additions & 0 deletions pinax/stripe/actions/subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, ch
stripe_subscription.prorate = False
if coupon:
stripe_subscription.coupon = coupon
if charge_immediately:
if utils.convert_tstamp(stripe_subscription.trial_end) > timezone.now():
stripe_subscription.trial_end = 'now'
sub = stripe_subscription.save()
customer = models.Customer.objects.get(pk=subscription.customer.pk)
sync_subscription_from_stripe_data(customer, sub)
37 changes: 37 additions & 0 deletions pinax/stripe/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Invoice,
InvoiceItem,
Plan,
Coupon,
Transfer,
TransferChargeFee
)
Expand Down Expand Up @@ -316,6 +317,42 @@ def customer_user(obj):
)


admin.site.register(
Coupon,
list_display=[
"stripe_id",
"amount_off",
"currency",
"percent_off",
"duration",
"duration_in_months",
"redeem_by",
"valid"
],
search_fields=[
"stripe_id",
],
list_filter=[
"currency",
"valid",
],
readonly_fields=[
"stripe_id",
"amount_off",
"currency",
"duration",
"duration_in_months",
"max_redemptions",
"metadata",
"percent_off",
"redeem_by",
"times_redeemed",
"valid",
"created_at"
],
)


class TransferChargeFeeInline(admin.TabularInline):
model = TransferChargeFee
extra = 0
Expand Down
11 changes: 11 additions & 0 deletions pinax/stripe/management/commands/sync_coupons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.core.management.base import BaseCommand

from ...actions import coupons


class Command(BaseCommand):

help = "Make sure your Stripe account has the coupons"

def handle(self, *args, **options):
coupons.sync_coupons()
39 changes: 39 additions & 0 deletions pinax/stripe/migrations/0006_coupon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.10 on 2016-12-20 03:16
from __future__ import unicode_literals

from django.db import migrations, models
import django.utils.timezone
import jsonfield.fields


class Migration(migrations.Migration):

dependencies = [
('pinax_stripe', '0005_auto_20161006_1445'),
]

operations = [
migrations.CreateModel(
name='Coupon',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stripe_id', models.CharField(max_length=255, unique=True)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('amount_off', models.DecimalField(decimal_places=2, max_digits=9, null=True)),
('currency', models.CharField(default='usd', max_length=10)),
('duration', models.CharField(default='once', max_length=10)),
('duration_in_months', models.PositiveIntegerField(null=True)),
('livemode', models.BooleanField(default=False)),
('max_redemptions', models.PositiveIntegerField(null=True)),
('metadata', jsonfield.fields.JSONField(null=True)),
('percent_off', models.PositiveIntegerField(null=True)),
('redeem_by', models.DateTimeField(null=True)),
('times_redeemed', models.PositiveIntegerField(null=True)),
('valid', models.BooleanField(default=False)),
],
options={
'abstract': False,
},
),
]
24 changes: 24 additions & 0 deletions pinax/stripe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,30 @@ def __str__(self):
return "{} ({}{})".format(self.name, CURRENCY_SYMBOLS.get(self.currency, ""), self.amount)


@python_2_unicode_compatible
class Coupon(StripeObject):

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_in_months = models.PositiveIntegerField(null=True)
livemode = models.BooleanField(default=False)
max_redemptions = models.PositiveIntegerField(null=True)
metadata = JSONField(null=True)
percent_off = models.PositiveIntegerField(null=True)
redeem_by = models.DateTimeField(null=True)
times_redeemed = models.PositiveIntegerField(null=True)
valid = models.BooleanField(default=False)

def __str__(self):
if self.amount_off is None:
description = "{}% off".format(self.percent_off,)
else:
description = "{}{}".format(CURRENCY_SYMBOLS.get(self.currency, ""), self.amount_off)

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


@python_2_unicode_compatible
class EventProcessingException(models.Model):

Expand Down
55 changes: 55 additions & 0 deletions pinax/stripe/tests/test_actions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import decimal
from unittest import skipIf
import time

import django
from django.test import TestCase
Expand Down Expand Up @@ -138,6 +139,36 @@ def test_customer_create_user_only(self, CreateMock, SyncMock):
self.assertIsNone(kwargs["trial_end"])
self.assertTrue(SyncMock.called)

@patch("stripe.Customer.retrieve")
@patch("stripe.Customer.create")
def test_customer_create_user_duplicate(self, CreateMock, RetrieveMock):
# Create an existing database customer for this user
original = Customer.objects.create(user=self.user, stripe_id='cus_XXXXX')

new_customer = Mock()
RetrieveMock.return_value = new_customer

# customers.Create will return a new customer instance
CreateMock.return_value = dict(id="cus_YYYYY")
customer = customers.create(self.user)

# But only one customer will exist - the original one
self.assertEqual(Customer.objects.count(), 1)
self.assertEqual(customer.stripe_id, original.stripe_id)

# Check that the customer hasn't been modified
self.assertEqual(customer.user, self.user)
self.assertEqual(customer.stripe_id, "cus_XXXXX")
_, kwargs = CreateMock.call_args
self.assertEqual(kwargs["email"], self.user.email)
self.assertIsNone(kwargs["source"])
self.assertIsNone(kwargs["plan"])
self.assertIsNone(kwargs["trial_end"])

# But a customer *was* created, retrieved, and then disposed of.
RetrieveMock.assert_called_once_with("cus_YYYYY")
new_customer.delete.assert_called_once()

@patch("pinax.stripe.actions.invoices.create_and_pay")
@patch("pinax.stripe.actions.customers.sync_customer")
@patch("stripe.Customer.create")
Expand Down Expand Up @@ -577,6 +608,30 @@ def test_update_plan_coupon(self, SyncMock):
self.assertTrue(SubMock.stripe_subscription.save.called)
self.assertTrue(SyncMock.called)

@patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data")
def test_update_plan_charge_now(self, SyncMock):
SubMock = Mock()
SubMock.customer = self.customer
SubMock.stripe_subscription.trial_end = time.time() + 1000000.0

subscriptions.update(SubMock, charge_immediately=True)
self.assertEquals(SubMock.stripe_subscription.trial_end, 'now')
self.assertTrue(SubMock.stripe_subscription.save.called)
self.assertTrue(SyncMock.called)

@patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data")
def test_update_plan_charge_now_old_trial(self, SyncMock):
trial_end = time.time() - 1000000.0
SubMock = Mock()
SubMock.customer = self.customer
SubMock.stripe_subscription.trial_end = trial_end

subscriptions.update(SubMock, charge_immediately=True)
# Trial end date hasn't changed
self.assertEquals(SubMock.stripe_subscription.trial_end, trial_end)
self.assertTrue(SubMock.stripe_subscription.save.called)
self.assertTrue(SyncMock.called)

@patch("stripe.Customer.retrieve")
@patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data")
def test_subscription_create(self, SyncMock, CustomerMock):
Expand Down
51 changes: 50 additions & 1 deletion pinax/stripe/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from mock import patch

from ..models import Customer, Plan
from ..models import Customer, Plan, Coupon


class CommandTests(TestCase):
Expand Down Expand Up @@ -70,6 +70,55 @@ def test_plans_create(self, PlanAutoPagerMock):
self.assertEquals(Plan.objects.all()[0].stripe_id, "entry-monthly")
self.assertEquals(Plan.objects.all()[0].amount, decimal.Decimal("9.54"))

@patch("stripe.Coupon.auto_paging_iter", create=True)
def test_coupons_create(self, CouponAutoPagerMock):
CouponAutoPagerMock.return_value = [{
"id": "test-coupon",
"object": "coupon",
"amount_off": None,
"created": 1482132502,
"currency": "aud",
"duration": "repeating",
"duration_in_months": 3,
"livemode": False,
"max_redemptions": None,
"metadata": {
},
"percent_off": 25,
"redeem_by": None,
"times_redeemed": 0,
"valid": True
}]
management.call_command("sync_coupons")
self.assertEquals(Coupon.objects.count(), 1)
self.assertEquals(Coupon.objects.all()[0].stripe_id, "test-coupon")
self.assertEquals(Coupon.objects.all()[0].percent_off, 25)

@patch("stripe.Coupon.all")
@patch("stripe.Coupon.auto_paging_iter", create=True, side_effect=AttributeError)
def test_coupons_create_deprecated(self, CouponAutoPagerMock, CouponAllMock):
CouponAllMock().data = [{
"id": "test-coupon",
"object": "coupon",
"amount_off": None,
"created": 1482132502,
"currency": "aud",
"duration": "repeating",
"duration_in_months": 3,
"livemode": False,
"max_redemptions": None,
"metadata": {
},
"percent_off": 25,
"redeem_by": None,
"times_redeemed": 0,
"valid": True
}]
management.call_command("sync_coupons")
self.assertEquals(Coupon.objects.count(), 1)
self.assertEquals(Coupon.objects.all()[0].stripe_id, "test-coupon")
self.assertEquals(Coupon.objects.all()[0].percent_off, 25)

@patch("stripe.Customer.retrieve")
@patch("pinax.stripe.actions.customers.sync_customer")
@patch("pinax.stripe.actions.invoices.sync_invoices_for_customer")
Expand Down
12 changes: 11 additions & 1 deletion pinax/stripe/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

from mock import patch

from ..models import Charge, Customer, Event, EventProcessingException, Invoice, InvoiceItem, Plan, Subscription
from ..models import (
Charge, Customer, Event, EventProcessingException, Invoice, InvoiceItem, Plan, Coupon, Subscription
)


def _str(obj):
Expand Down Expand Up @@ -51,6 +53,14 @@ def test_plan_display_invoiceitem(self):
i = InvoiceItem(plan=p)
self.assertEquals(i.plan_display(), "My Plan")

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")

def test_coupon_absolute(self):
c = Coupon(amount_off=decimal.Decimal(50.00), duration="once", currency='usd')
self.assertEquals(str(c), "Coupon for $50, once")

def test_model_table_name(self):
self.assertEquals(Customer()._meta.db_table, "pinax_stripe_customer")

Expand Down

0 comments on commit 9e9e0b8

Please sign in to comment.