From f2820d402b83eb18c7a16f25f4dbcdf90fee4a42 Mon Sep 17 00:00:00 2001 From: Paris Kolios Date: Thu, 16 Nov 2017 15:50:59 +0100 Subject: [PATCH 1/4] Add support for Coupons and Discounts objects --- pinax/stripe/actions/coupons.py | 12 +++ pinax/stripe/actions/subscriptions.py | 13 +++ pinax/stripe/models.py | 67 ++++++++++++++- pinax/stripe/tests/test_actions.py | 113 +++++++++++++++++++++++++- pinax/stripe/tests/test_models.py | 41 ++++++++++ pinax/stripe/tests/test_webhooks.py | 84 +++++++++++++++++++ pinax/stripe/webhooks.py | 10 +++ 7 files changed, 335 insertions(+), 5 deletions(-) diff --git a/pinax/stripe/actions/coupons.py b/pinax/stripe/actions/coupons.py index ed3160c74..1dc0e0fe9 100644 --- a/pinax/stripe/actions/coupons.py +++ b/pinax/stripe/actions/coupons.py @@ -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"]) @@ -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() diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index e97ff9cad..3f3e4bff4 100644 --- a/pinax/stripe/actions/subscriptions.py +++ b/pinax/stripe/actions/subscriptions.py @@ -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): @@ -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 diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index b0bef490f..f9e8f8521 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -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) @@ -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): @@ -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"] @@ -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 diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 258d67562..0f39573b3 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -15,6 +15,7 @@ from ..actions import ( accounts, charges, + coupons, customers, events, externalaccounts, @@ -30,7 +31,9 @@ BitcoinReceiver, Card, Charge, + Coupon, Customer, + Discount, Event, Invoice, Plan, @@ -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): @@ -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): @@ -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 = { @@ -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": { }, @@ -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") diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index 9071560f3..f60d3052b 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -16,6 +16,7 @@ Charge, Coupon, Customer, + Discount, Event, EventProcessingException, Invoice, @@ -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='')") + 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") @@ -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='')") @@ -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") diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index 788a016d1..dbeb7073b 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -28,6 +28,10 @@ AccountExternalAccountCreatedWebhook, AccountUpdatedWebhook, ChargeCapturedWebhook, + CouponCreatedWebhook, + CouponDeletedWebhook, + CouponUpdatedWebhook, + CustomerCreatedWebhook, CustomerDeletedWebhook, CustomerSourceCreatedWebhook, CustomerSourceDeletedWebhook, @@ -279,6 +283,86 @@ def test_process_webhook_with_customer_with_data(self, SyncMock): self.assertIs(SyncMock.call_args[0][1], obj) +class CouponCreatedWebhookTest(TestCase): + + @patch("pinax.stripe.actions.coupons.sync_coupon_from_stripe_data") + def test_process_webhook(self, SyncMock): + event = Event.objects.create(kind=CouponCreatedWebhook.name, webhook_message={}, valid=True, processed=False, stripe_account=None) + obj = object() + event.validated_message = dict(data=dict(object=obj)) + CouponCreatedWebhook(event).process_webhook() + SyncMock.assert_called_with(event.message["data"]["object"], stripe_account=None) + + @patch("pinax.stripe.actions.coupons.sync_coupon_from_stripe_data") + def test_process_webhook_with_stripe_account(self, SyncMock): + account = Account.objects.create(stripe_id="acc_A") + event = Event.objects.create(kind=CouponCreatedWebhook.name, webhook_message={}, valid=True, processed=False, stripe_account=account) + obj = object() + event.validated_message = dict(data=dict(object=obj)) + CouponCreatedWebhook(event).process_webhook() + SyncMock.assert_called_with(event.message["data"]["object"], stripe_account=account) + + +class CouponUpdatedWebhookTest(TestCase): + + @patch("pinax.stripe.actions.coupons.sync_coupon_from_stripe_data") + def test_process_webhook(self, SyncMock): + event = Event.objects.create(kind=CouponUpdatedWebhook.name, webhook_message={}, valid=True, processed=False, stripe_account=None) + obj = object() + event.validated_message = dict(data=dict(object=obj)) + CouponUpdatedWebhook(event).process_webhook() + SyncMock.assert_called_with(event.message["data"]["object"], stripe_account=None) + + @patch("pinax.stripe.actions.coupons.sync_coupon_from_stripe_data") + def test_process_webhook_with_stripe_account(self, SyncMock): + account = Account.objects.create(stripe_id="acc_A") + event = Event.objects.create(kind=CouponUpdatedWebhook.name, webhook_message={}, valid=True, processed=False, stripe_account=account) + obj = object() + event.validated_message = dict(data=dict(object=obj)) + CouponUpdatedWebhook(event).process_webhook() + SyncMock.assert_called_with(event.message["data"]["object"], stripe_account=account) + + +class CouponDeletedWebhookTest(TestCase): + + @patch("pinax.stripe.actions.coupons.purge_local") + def test_process_webhook(self, PurgeMock): + event = Event.objects.create(kind=CouponDeletedWebhook.name, webhook_message={}, valid=True, processed=False, stripe_account=None) + obj = object() + event.validated_message = dict(data=dict(object=obj)) + CouponDeletedWebhook(event).process_webhook() + PurgeMock.assert_called_with(event.message["data"]["object"], stripe_account=None) + + @patch("pinax.stripe.actions.coupons.purge_local") + def test_process_webhook_with_stripe_account(self, PurgeMock): + account = Account.objects.create(stripe_id="acc_A") + event = Event.objects.create(kind=CouponDeletedWebhook.name, webhook_message={}, valid=True, processed=False, stripe_account=account) + obj = object() + event.validated_message = dict(data=dict(object=obj)) + CouponDeletedWebhook(event).process_webhook() + PurgeMock.assert_called_with(event.message["data"]["object"], stripe_account=account) + + +class CustomerCreatedWebhookTest(TestCase): + + @patch("pinax.stripe.actions.customers.create") + def test_process_webhook(self, CreateMock): + event = Event.objects.create(kind=CustomerCreatedWebhook.name, webhook_message={}, valid=True, processed=False) + obj = object() + event.validated_message = dict(data=dict(object=obj)) + CustomerCreatedWebhook(event).process_webhook() + CreateMock.assert_not_called() + + @patch("pinax.stripe.actions.customers.create") + def test_process_webhook_with_stripe_account(self, CreateMock): + account = Account.objects.create(stripe_id="acc_A") + event = Event.objects.create(kind=CustomerCreatedWebhook.name, webhook_message={}, valid=True, processed=False, stripe_account=account) + obj = object() + event.validated_message = dict(data=dict(object=obj)) + CustomerCreatedWebhook(event).process_webhook() + CreateMock.assert_not_called() + + class CustomerSourceCreatedWebhookTest(TestCase): @patch("pinax.stripe.actions.sources.sync_payment_source_from_stripe_data") diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 75beb3459..65573fb94 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -9,6 +9,7 @@ from .actions import ( accounts, charges, + coupons, customers, exceptions, invoices, @@ -296,16 +297,25 @@ class CouponCreatedWebhook(Webhook): name = "coupon.created" description = "Occurs whenever a coupon is created." + def process_webhook(self): + coupons.sync_coupon_from_stripe_data(self.event.message["data"]["object"], stripe_account=self.event.stripe_account) + class CouponDeletedWebhook(Webhook): name = "coupon.deleted" description = "Occurs whenever a coupon is deleted." + def process_webhook(self): + coupons.purge_local(self.event.message["data"]["object"], stripe_account=self.event.stripe_account) + class CouponUpdatedWebhook(Webhook): name = "coupon.updated" description = "Occurs whenever a coupon is updated." + def process_webhook(self): + coupons.sync_coupon_from_stripe_data(self.event.message["data"]["object"], stripe_account=self.event.stripe_account) + class CustomerCreatedWebhook(Webhook): name = "customer.created" From 9db7ecccb6f9734eb25da7386d3e21c6cc9e1e79 Mon Sep 17 00:00:00 2001 From: Paris Kolios Date: Thu, 16 Nov 2017 15:52:21 +0100 Subject: [PATCH 2/4] Migrations --- pinax/stripe/migrations/0011_coupons.py | 58 +++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 pinax/stripe/migrations/0011_coupons.py diff --git a/pinax/stripe/migrations/0011_coupons.py b/pinax/stripe/migrations/0011_coupons.py new file mode 100644 index 000000000..fd8e7efc4 --- /dev/null +++ b/pinax/stripe/migrations/0011_coupons.py @@ -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'), + ), + ] From e3a4f0734da98ccec3cb9edaa66265e8c3828e62 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 20 Nov 2017 21:46:05 +0100 Subject: [PATCH 3/4] fixup! Add support for Coupons and Discounts objects --- pinax/stripe/models.py | 8 +++++++- pinax/stripe/tests/test_models.py | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index f9e8f8521..7e9f215d4 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -86,7 +86,6 @@ def __repr__(self): 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, @@ -138,6 +137,13 @@ def __repr__(self): 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): diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index f60d3052b..8af56c9d1 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -8,7 +8,7 @@ from django.test import TestCase from django.utils import timezone -from mock import patch +from mock import call, patch from ..models import ( Account, @@ -89,6 +89,13 @@ 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") + @patch("stripe.Coupon.retrieve") + def test_coupon_stripe_coupon(self, RetrieveMock): + c = Coupon(stripe_id="coupon", stripe_account=Account(stripe_id="acct_A")) + self.assertEqual(c.stripe_coupon, RetrieveMock.return_value) + self.assertTrue(RetrieveMock.call_args_list, [ + call("coupon", stripe_account="acct_A")]) + def test_model_table_name(self): self.assertEquals(Customer()._meta.db_table, "pinax_stripe_customer") From cdb67f40478e6a4a4bc6d126c01908d3fc567661 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 20 Nov 2017 21:46:31 +0100 Subject: [PATCH 4/4] fixup! fixup! Add support for Coupons and Discounts objects --- pinax/stripe/actions/coupons.py | 1 + pinax/stripe/tests/test_actions.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/actions/coupons.py b/pinax/stripe/actions/coupons.py index 1dc0e0fe9..522b95a28 100644 --- a/pinax/stripe/actions/coupons.py +++ b/pinax/stripe/actions/coupons.py @@ -26,6 +26,7 @@ def sync_coupon_from_stripe_data(coupon, stripe_account=None): 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"], diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 0f39573b3..70602a33c 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -1198,7 +1198,7 @@ def test_sync_coupon_from_stripe_data(self): "currency": None, "duration": "repeating", "duration_in_months": 3, - "livemode": False, + "livemode": True, "max_redemptions": None, "metadata": { }, @@ -1208,9 +1208,11 @@ def test_sync_coupon_from_stripe_data(self): "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)