From 5fd1b7fa5cc9a151508957e9062b91c5bfa90de8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 10 Oct 2017 13:12:37 +0200 Subject: [PATCH 001/153] trigger circleci build From 30deb3cc78da1fb756f0e5c36ba3819efc59a57a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 10 Oct 2017 13:33:55 +0200 Subject: [PATCH 002/153] Adjust CircleCI config: only build dj111/master (dj20) --- .circleci/config.yml | 54 ++------------------------------------------ 1 file changed, 2 insertions(+), 52 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cfe027c95..f72929dcf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -157,55 +157,5 @@ workflows: lint-and-test: jobs: - lint - - py27dj18: - requires: - - lint - - py27dj19: - requires: - - lint - - py27dj110: - requires: - - lint - - py27dj111: - requires: - - lint - - py33dj18: - requires: - - lint - - py34dj18: - requires: - - lint - - py34dj19: - requires: - - lint - - py34dj110: - requires: - - lint - - py34dj111: - requires: - - lint - # Failing to start Django Tests for some reason - # - py34djmaster: - # requires: - # - lint - - py35dj18: - requires: - - lint - - py35dj19: - requires: - - lint - - py35dj110: - requires: - - lint - - py35dj111: - requires: - - lint - - py35djmaster: - requires: - - lint - - py36dj111: - requires: - - lint - - py36djmaster: - requires: - - lint + - py36dj111 + - py36djmaster From 59ca8dffe1fa9f822ab5d0f7c61dbb301fae166e Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Sat, 14 Oct 2017 17:23:27 +0200 Subject: [PATCH 003/153] Prevent creation of disposable Customers --- pinax/stripe/actions/customers.py | 22 +++++++++------------- pinax/stripe/tests/test_actions.py | 19 ++----------------- 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index 120d98973..a5bd646a8 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -39,6 +39,11 @@ def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_imme Returns: the pinax.stripe.models.Customer object that was created """ + try: + return models.Customer.objects.get(user=user) + except models.Customer.DoesNotExist: + pass + trial_end = hooks.hookset.trial_period(user, plan) stripe_customer = stripe.Customer.create( email=user.email, @@ -47,19 +52,10 @@ def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_imme quantity=quantity, trial_end=trial_end ) - cus, created = models.Customer.objects.get_or_create( - user=user, - defaults={ - "stripe_id": stripe_customer["id"] - } - ) - if created: - sync_customer(cus, stripe_customer) - if plan and charge_immediately: - invoices.create_and_pay(cus) - else: - # remove this extra customer as it is not needed - stripe.Customer.retrieve(stripe_customer["id"]).delete() + cus = models.Customer.objects.create(user=user, stripe_id=stripe_customer["id"]) + sync_customer(cus, stripe_customer) + if plan and charge_immediately: + invoices.create_and_pay(cus) return cus diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 7a3f46b45..588efee28 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -231,18 +231,11 @@ 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): + def test_customer_create_user_duplicate(self, CreateMock): # 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 @@ -252,15 +245,7 @@ def test_customer_create_user_duplicate(self, CreateMock, RetrieveMock): # 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() + CreateMock.assert_not_called() @patch("pinax.stripe.actions.invoices.create_and_pay") @patch("pinax.stripe.actions.customers.sync_customer") From e60b713654464cd7afcc5a0beb530ba76eaebd3d Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Sun, 15 Oct 2017 11:31:55 +0200 Subject: [PATCH 004/153] Prevent orphan customers --- pinax/stripe/actions/customers.py | 22 +++++++++++++--- pinax/stripe/tests/test_actions.py | 42 +++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index a5bd646a8..daaff4445 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -40,10 +40,19 @@ def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_imme the pinax.stripe.models.Customer object that was created """ try: - return models.Customer.objects.get(user=user) + cus = models.Customer.objects.get(user=user) except models.Customer.DoesNotExist: pass - + else: + try: + stripe.Customer.retrieve(cus.stripe_id) + except stripe.error.InvalidRequestError: + pass + else: + return cus + + # At this point we maybe have a local Customer but no stripe customer + # let's create one and make the binding trial_end = hooks.hookset.trial_period(user, plan) stripe_customer = stripe.Customer.create( email=user.email, @@ -52,7 +61,14 @@ def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_imme quantity=quantity, trial_end=trial_end ) - cus = models.Customer.objects.create(user=user, stripe_id=stripe_customer["id"]) + cus, created = models.Customer.objects.get_or_create( + user=user, + defaults={ + "stripe_id": stripe_customer["id"] + } + ) + if not created: + cus.stripe_id = stripe_customer["id"] # sync_customer will call cus.save() sync_customer(cus, stripe_customer) if plan and charge_immediately: invoices.create_and_pay(cus) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 588efee28..ea3aae225 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -231,11 +231,14 @@ 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): + 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 customer = customers.create(self.user) # But only one customer will exist - the original one @@ -247,6 +250,43 @@ def test_customer_create_user_duplicate(self, CreateMock): self.assertEqual(customer.stripe_id, "cus_XXXXX") CreateMock.assert_not_called() + @patch("stripe.Customer.retrieve") + @patch("stripe.Customer.create") + def test_customer_create_local_customer_but_no_remote(self, CreateMock, RetrieveMock): + # Create an existing database customer for this user + Customer.objects.create(user=self.user, stripe_id="cus_XXXXX") + + RetrieveMock.side_effect = stripe.error.InvalidRequestError( + message="invalid", param=None) + + # customers.Create will return a new customer instance + CreateMock.return_value = { + "id": "cus_YYYYY", + "account_balance": 0, + "currency": "us", + "delinquent": False, + "default_source": "", + "sources": {"data": []}, + "subscriptions": {"data": []}, + } + customer = customers.create(self.user) + + # But a customer *was* retrieved, but not found + RetrieveMock.assert_called_once_with("cus_XXXXX") + + # But only one customer will exist - the original one + self.assertEqual(Customer.objects.count(), 1) + self.assertEqual(customer.stripe_id, "cus_YYYYY") + + # Check that the customer hasn't been modified + self.assertEqual(customer.user, self.user) + self.assertEqual(customer.stripe_id, "cus_YYYYY") + _, kwargs = CreateMock.call_args + self.assertEqual(kwargs["email"], self.user.email) + self.assertIsNone(kwargs["source"]) + self.assertIsNone(kwargs["plan"]) + self.assertIsNone(kwargs["trial_end"]) + @patch("pinax.stripe.actions.invoices.create_and_pay") @patch("pinax.stripe.actions.customers.sync_customer") @patch("stripe.Customer.create") From 7db052de41fe3e40b145a08e09e038e5d3ae1e2b Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Sun, 15 Oct 2017 20:00:47 +0200 Subject: [PATCH 005/153] Create Subscription with Connect --- pinax/stripe/actions/subscriptions.py | 6 +++++- pinax/stripe/models.py | 2 +- pinax/stripe/tests/test_actions.py | 11 +++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index 21f08bd3f..f9c62db59 100644 --- a/pinax/stripe/actions/subscriptions.py +++ b/pinax/stripe/actions/subscriptions.py @@ -20,7 +20,7 @@ def cancel(subscription, at_period_end=True): sync_subscription_from_stripe_data(subscription.customer, sub) -def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=None, tax_percent=None): +def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=None, tax_percent=None, stripe_account=None): """ Creates a subscription for the given customer @@ -35,6 +35,7 @@ def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=No will be used coupon: if provided, a coupon to apply towards the subscription tax_percent: if provided, add percentage as tax + stripe_account: An Account object. Returns: the data representing the subscription object that was created @@ -48,6 +49,9 @@ def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=No if token: subscription_params["source"] = token + if stripe_account is not None: + subscription_params["stripe_account"] = stripe_account.stripe_id + subscription_params["plan"] = plan subscription_params["quantity"] = quantity subscription_params["coupon"] = coupon diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index b1561e35b..456824557 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -223,7 +223,7 @@ class BitcoinReceiver(StripeObject): used_for_payment = models.BooleanField(default=False) -class Subscription(StripeObject): +class Subscription(AccountRelatedStripeObject): STATUS_CURRENT = ["trialing", "active"] diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 7a3f46b45..9be30a2df 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -621,6 +621,9 @@ def setUp(self): user=self.user, stripe_id="cus_xxxxxxxxxxxxxxx" ) + self.account = Account.objects.create( + stripe_id="acc_1" + ) def test_has_active_subscription(self): plan = Plan.objects.create( @@ -763,6 +766,14 @@ def test_subscription_create(self, SyncMock, CustomerMock): self.assertTrue(sub_create.called) self.assertTrue(SyncMock.called) + @patch("stripe.Customer.retrieve") + @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") + def test_subscription_create_with_stripe_account(self, SyncMock, CustomerMock): + subscriptions.create(self.customer, "the-plan", stripe_account=self.account) + sub_create = CustomerMock().subscriptions.create + sub_create.assert_called_with(plan="the-plan", quantity=4, coupon=None, tax_percent=None, stripe_account="acc_1") + self.assertTrue(SyncMock.called) + @patch("stripe.Customer.retrieve") @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") def test_subscription_create_with_trial(self, SyncMock, CustomerMock): From 4f6e66f63a182dd9ff0178ee1365b5812cd826bf Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Sun, 15 Oct 2017 20:01:49 +0200 Subject: [PATCH 006/153] Migration --- .../migrations/0011_subscription_connect.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 pinax/stripe/migrations/0011_subscription_connect.py diff --git a/pinax/stripe/migrations/0011_subscription_connect.py b/pinax/stripe/migrations/0011_subscription_connect.py new file mode 100644 index 000000000..21936bddc --- /dev/null +++ b/pinax/stripe/migrations/0011_subscription_connect.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-15 18:01 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0010_connect'), + ] + + operations = [ + migrations.AddField( + model_name='subscription', + name='stripe_account', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] From 16392321ec0f7317d9184ceeacc1472e5914f26a Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Mon, 16 Oct 2017 17:18:33 +0200 Subject: [PATCH 007/153] Multiple Customers per User --- pinax/stripe/actions/customers.py | 90 +++++++++++++++----- pinax/stripe/migrations/0012_user_account.py | 36 ++++++++ pinax/stripe/models.py | 19 +++++ 3 files changed, 125 insertions(+), 20 deletions(-) create mode 100644 pinax/stripe/migrations/0012_user_account.py diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index daaff4445..d7faa9607 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -22,23 +22,7 @@ def can_charge(customer): return False -def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None): - """ - Creates a Stripe customer. - - If a customer already exists, the existing customer will be returned. - - Args: - user: a user object - card: optionally, the token for a new card - plan: a plan to subscribe the user to - charge_immediately: whether or not the user should be immediately - charged for the subscription - quantity: the quantity (multiplier) of the subscription - - Returns: - the pinax.stripe.models.Customer object that was created - """ +def _create_without_account(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None): try: cus = models.Customer.objects.get(user=user) except models.Customer.DoesNotExist: @@ -75,17 +59,83 @@ def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_imme return cus -def get_customer_for_user(user): +def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None): + try: + cus = user.customers.get(user_account__account=stripe_account).get() + except models.Customer.DoesNotExist: + cus = None + pass + else: + try: + stripe.Customer.retrieve(cus.stripe_id) + except stripe.error.InvalidRequestError: + pass + else: + return cus + + # At this point we maybe have a local Customer but no stripe customer + # let's create one and make the binding + trial_end = hooks.hookset.trial_period(user, plan) + stripe_customer = stripe.Customer.create( + email=user.email, + source=card, + plan=plan, + quantity=quantity, + trial_end=trial_end + ) + + if cus is None: + cus = models.Customer.objects.create(stripe_id=stripe_customer["id"]) + models.UserAccount.objects.create( + user=user, + account=stripe_account, + customer=cus, + ) + else: + cus.stripe_id = stripe_customer["id"] # sync_customer will call cus.save() + sync_customer(cus, stripe_customer) + if plan and charge_immediately: + invoices.create_and_pay(cus) + return cus + + +def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None, stripe_account=None): """ - Get a customer object for a given user + Creates a Stripe customer. + + If a customer already exists, the existing customer will be returned. Args: user: a user object + card: optionally, the token for a new card + plan: a plan to subscribe the user to + charge_immediately: whether or not the user should be immediately + charged for the subscription + quantity: the quantity (multiplier) of the subscription + stripe_account: An account object. If given, the Customer and User relation will be established for you through UserAccount model. + Because a single User might have several Customers, one per Account. + + Returns: + the pinax.stripe.models.Customer object that was created + """ + if stripe_account is None: + return _create_without_account(user, card=card, plan=plan, charge_immediately=charge_immediately, quantity=quantity) + return _create_with_account(user, stripe_account, card=card, plan=plan, charge_immediately=charge_immediately, quantity=quantity) + + +def get_customer_for_user(user, stripe_account=None): + """ + Get a customer object for a given user + + Args: + user: a user object Returns: a pinax.stripe.models.Customer object """ - return models.Customer.objects.filter(user=user).first() + if stripe_account is None: + return models.Customer.objects.filter(user=user).first() + return user.customers.filter(user_account__account=stripe_account).first() def purge_local(customer): diff --git a/pinax/stripe/migrations/0012_user_account.py b/pinax/stripe/migrations/0012_user_account.py new file mode 100644 index 000000000..db4e2251a --- /dev/null +++ b/pinax/stripe/migrations/0012_user_account.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-16 15:18 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('pinax_stripe', '0011_subscription_connect'), + ] + + operations = [ + migrations.CreateModel( + name='UserAccount', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to='pinax_stripe.Account')), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to='pinax_stripe.Customer')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='customer', + name='users', + field=models.ManyToManyField(null=True, related_name='customers', related_query_name='customers', through='pinax_stripe.UserAccount', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterUniqueTogether( + name='useraccount', + unique_together=set([('user', 'account', 'customer')]), + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 456824557..1872ab31a 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -156,10 +156,29 @@ class TransferChargeFee(models.Model): created_at = models.DateTimeField(default=timezone.now) +@python_2_unicode_compatible +class UserAccount(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, + related_name="user_accounts", + related_query_name="user_account") + account = models.ForeignKey("pinax_stripe.Account", + related_name="user_accounts", + related_query_name="user_account") + customer = models.ForeignKey("pinax_stripe.Customer", + related_name="user_accounts", + related_query_name="user_account") + + class Meta: + unique_together = ("user", "account", "customer") + + @python_2_unicode_compatible class Customer(AccountRelatedStripeObject): user = models.OneToOneField(settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE) + users = models.ManyToManyField(settings.AUTH_USER_MODEL, through=UserAccount, + related_name="customers", + related_query_name="customers", null=True) account_balance = models.DecimalField(decimal_places=2, max_digits=9, null=True) currency = models.CharField(max_length=10, default="usd", blank=True) delinquent = models.BooleanField(default=False) From 6a95788183c98001b430913b4e29415f1d60897e Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Tue, 17 Oct 2017 10:55:06 +0200 Subject: [PATCH 008/153] null has no effect on M2M --- pinax/stripe/migrations/0012_user_account.py | 2 +- pinax/stripe/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pinax/stripe/migrations/0012_user_account.py b/pinax/stripe/migrations/0012_user_account.py index db4e2251a..50cdec2b6 100644 --- a/pinax/stripe/migrations/0012_user_account.py +++ b/pinax/stripe/migrations/0012_user_account.py @@ -27,7 +27,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='customer', name='users', - field=models.ManyToManyField(null=True, related_name='customers', related_query_name='customers', through='pinax_stripe.UserAccount', to=settings.AUTH_USER_MODEL), + field=models.ManyToManyField(related_name='customers', related_query_name='customers', through='pinax_stripe.UserAccount', to=settings.AUTH_USER_MODEL), ), migrations.AlterUniqueTogether( name='useraccount', diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 1872ab31a..c5855d002 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -178,7 +178,7 @@ class Customer(AccountRelatedStripeObject): user = models.OneToOneField(settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE) users = models.ManyToManyField(settings.AUTH_USER_MODEL, through=UserAccount, related_name="customers", - related_query_name="customers", null=True) + related_query_name="customers") account_balance = models.DecimalField(decimal_places=2, max_digits=9, null=True) currency = models.CharField(max_length=10, default="usd", blank=True) delinquent = models.BooleanField(default=False) From fb66157e3bf6bf4130c285d0e6d1489f4c1c623f Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Tue, 17 Oct 2017 14:54:10 +0200 Subject: [PATCH 009/153] hotifx for multiple users --- pinax/stripe/actions/customers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index d7faa9607..edd522ab1 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -61,7 +61,7 @@ def _create_without_account(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None): try: - cus = user.customers.get(user_account__account=stripe_account).get() + cus = user.customers.get(user_account__account=stripe_account) except models.Customer.DoesNotExist: cus = None pass From ba0ab5b01870b70db0de068eb6e1ffc6517d3cd4 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 17 Oct 2017 16:09:39 +0200 Subject: [PATCH 010/153] Revert "Merge branch 'subscription-connect' into next" This reverts commit 7f23da1d4a748b1abaa0afcdf618f4df5fa21be5, reversing changes made to 294366d2122cc885e08910e6cb4d47b9bc78808d. --- pinax/stripe/actions/subscriptions.py | 6 +----- .../migrations/0011_subscription_connect.py | 20 ------------------- pinax/stripe/models.py | 2 +- pinax/stripe/tests/test_actions.py | 11 ---------- 4 files changed, 2 insertions(+), 37 deletions(-) delete mode 100644 pinax/stripe/migrations/0011_subscription_connect.py diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index f9c62db59..21f08bd3f 100644 --- a/pinax/stripe/actions/subscriptions.py +++ b/pinax/stripe/actions/subscriptions.py @@ -20,7 +20,7 @@ def cancel(subscription, at_period_end=True): sync_subscription_from_stripe_data(subscription.customer, sub) -def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=None, tax_percent=None, stripe_account=None): +def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=None, tax_percent=None): """ Creates a subscription for the given customer @@ -35,7 +35,6 @@ def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=No will be used coupon: if provided, a coupon to apply towards the subscription tax_percent: if provided, add percentage as tax - stripe_account: An Account object. Returns: the data representing the subscription object that was created @@ -49,9 +48,6 @@ def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=No if token: subscription_params["source"] = token - if stripe_account is not None: - subscription_params["stripe_account"] = stripe_account.stripe_id - subscription_params["plan"] = plan subscription_params["quantity"] = quantity subscription_params["coupon"] = coupon diff --git a/pinax/stripe/migrations/0011_subscription_connect.py b/pinax/stripe/migrations/0011_subscription_connect.py deleted file mode 100644 index 21936bddc..000000000 --- a/pinax/stripe/migrations/0011_subscription_connect.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-15 18:01 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0010_connect'), - ] - - operations = [ - migrations.AddField( - model_name='subscription', - name='stripe_account', - field=models.CharField(blank=True, max_length=255, null=True), - ), - ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index c5855d002..aa8f73614 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -242,7 +242,7 @@ class BitcoinReceiver(StripeObject): used_for_payment = models.BooleanField(default=False) -class Subscription(AccountRelatedStripeObject): +class Subscription(StripeObject): STATUS_CURRENT = ["trialing", "active"] diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 442047519..04de0338c 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -673,9 +673,6 @@ def setUp(self): user=self.user, stripe_id="cus_xxxxxxxxxxxxxxx" ) - self.account = Account.objects.create( - stripe_id="acc_1" - ) def test_has_active_subscription(self): plan = Plan.objects.create( @@ -818,14 +815,6 @@ def test_subscription_create(self, SyncMock, CustomerMock): self.assertTrue(sub_create.called) self.assertTrue(SyncMock.called) - @patch("stripe.Customer.retrieve") - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - def test_subscription_create_with_stripe_account(self, SyncMock, CustomerMock): - subscriptions.create(self.customer, "the-plan", stripe_account=self.account) - sub_create = CustomerMock().subscriptions.create - sub_create.assert_called_with(plan="the-plan", quantity=4, coupon=None, tax_percent=None, stripe_account="acc_1") - self.assertTrue(SyncMock.called) - @patch("stripe.Customer.retrieve") @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") def test_subscription_create_with_trial(self, SyncMock, CustomerMock): From 4fea912a79d83b81d41085399926a0652d7f6c45 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 17 Oct 2017 16:21:04 +0200 Subject: [PATCH 011/153] keep old migration and revert it --- .../migrations/0011_subscription_connect.py | 18 ++++++++++++++++++ pinax/stripe/migrations/0013_revert_0011.py | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 pinax/stripe/migrations/0011_subscription_connect.py create mode 100644 pinax/stripe/migrations/0013_revert_0011.py diff --git a/pinax/stripe/migrations/0011_subscription_connect.py b/pinax/stripe/migrations/0011_subscription_connect.py new file mode 100644 index 000000000..4c9540a25 --- /dev/null +++ b/pinax/stripe/migrations/0011_subscription_connect.py @@ -0,0 +1,18 @@ +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0010_connect'), + ] + + operations = [ + migrations.AddField( + model_name='subscription', + name='stripe_account', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/pinax/stripe/migrations/0013_revert_0011.py b/pinax/stripe/migrations/0013_revert_0011.py new file mode 100644 index 000000000..e1f092604 --- /dev/null +++ b/pinax/stripe/migrations/0013_revert_0011.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2017-10-17 14:20 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0012_user_account'), + ] + + operations = [ + migrations.RemoveField( + model_name='subscription', + name='stripe_account', + ), + ] From 6725a1f6ffcc1cffd1cc334ad4f2fd1a27e2954a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 17 Oct 2017 16:35:42 +0200 Subject: [PATCH 012/153] _create_with_account: pass through stripe_account --- pinax/stripe/actions/customers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index edd522ab1..4f9fcdc47 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -81,7 +81,8 @@ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_ST source=card, plan=plan, quantity=quantity, - trial_end=trial_end + trial_end=trial_end, + stripe_account=stripe_account.stripe_id, ) if cus is None: From f931eca61df57e800f572b3ecb609f21cc4d5556 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 17 Oct 2017 13:08:40 +0200 Subject: [PATCH 013/153] tox: isort: skip migrations --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 70994b5c4..7b362172e 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ multi_line_output=3 known_django=django known_third_party=stripe,six,mock,appconf,jsonfield sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +skip_glob=*/pinax/stripe/migrations/* [coverage:run] source = pinax From 3dd931a091da0c84b6caab059c18191be9f3c636 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 17 Oct 2017 22:17:41 +0200 Subject: [PATCH 014/153] purge_local: delete UserAccounts --- pinax/stripe/actions/customers.py | 1 + pinax/stripe/tests/test_actions.py | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index 4f9fcdc47..a38a79c12 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -140,6 +140,7 @@ def get_customer_for_user(user, stripe_account=None): def purge_local(customer): + customer.users.all().delete() customer.user = None customer.date_purged = timezone.now() customer.save() diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 2dd12a6c8..27a734b0d 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -35,7 +35,8 @@ Invoice, Plan, Subscription, - Transfer + Transfer, + UserAccount, ) @@ -374,6 +375,24 @@ def test_purge(self, RetrieveMock): self.assertIsNone(Customer.objects.get(stripe_id=customer.stripe_id).user) self.assertIsNotNone(Customer.objects.get(stripe_id=customer.stripe_id).date_purged) + @patch("stripe.Customer.retrieve") + def test_purge_connected(self, RetrieveMock): + account = Account.objects.create( + stripe_id="acct_X", + type="standard", + ) + customer = Customer.objects.create( + user=self.user, + stripe_account=account.stripe_id, + stripe_id="cus_xxxxxxxxxxxxxxx", + ) + UserAccount.objects.create(user=self.user, account=account, customer=customer) + customers.purge(customer) + self.assertTrue(RetrieveMock().delete.called) + self.assertIsNone(Customer.objects.get(stripe_id=customer.stripe_id).user) + self.assertIsNotNone(Customer.objects.get(stripe_id=customer.stripe_id).date_purged) + self.assertFalse(UserAccount.objects.exists()) + @patch("stripe.Customer.retrieve") def test_purge_already_deleted(self, RetrieveMock): RetrieveMock().delete.side_effect = stripe.InvalidRequestError("No such customer:", "error") From 3b332f9ce3a4f1990a24268839d2db502eef3792 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 17 Oct 2017 23:36:15 +0200 Subject: [PATCH 015/153] tox: checkqa: check isort via flake8-isort This moves the isort cfg to setup.cfg. --- setup.cfg | 6 ++++++ tox.ini | 9 +-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/setup.cfg b/setup.cfg index c1e4facba..a50b52873 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,3 +4,9 @@ universal = 1 [tool:pytest] testpaths = pinax/stripe/tests DJANGO_SETTINGS_MODULE = pinax.stripe.tests.settings + +[isort] +multi_line_output=3 +known_django=django +known_third_party=stripe,six,mock,appconf,jsonfield +sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER diff --git a/tox.ini b/tox.ini index 70994b5c4..318152cec 100644 --- a/tox.ini +++ b/tox.ini @@ -5,12 +5,6 @@ max-complexity = 10 exclude = pinax/stripe/migrations/*,docs/* inline-quotes = double -[isort] -multi_line_output=3 -known_django=django -known_third_party=stripe,six,mock,appconf,jsonfield -sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER - [coverage:run] source = pinax omit = pinax/stripe/conf.py,pinax/stripe/tests/*,pinax/stripe/migrations/* @@ -64,8 +58,7 @@ commands = [testenv:checkqa] commands = flake8 pinax - isort --recursive --check-only --diff pinax -sp tox.ini deps = flake8 == 3.4.1 + flake8-isort == 2.2.2 flake8-quotes == 0.11.0 - isort == 4.2.15 From f1406db7e70e4b4ad5aa3b107f3aa6d33828dee1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 17 Oct 2017 23:44:43 +0200 Subject: [PATCH 016/153] fix isort --- pinax/stripe/forms.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pinax/stripe/forms.py b/pinax/stripe/forms.py index 3197ec064..09f3eb28d 100644 --- a/pinax/stripe/forms.py +++ b/pinax/stripe/forms.py @@ -5,7 +5,6 @@ from django.utils.translation import ugettext_lazy as _ import stripe - from ipware.ip import get_ip, get_real_ip from .actions import accounts From 67f0189f83a657a46eb6e1a733439be4b70eff35 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 18 Oct 2017 11:15:01 +0200 Subject: [PATCH 017/153] Save Customer with stripe_account --- pinax/stripe/actions/customers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index a38a79c12..516ddc6c5 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -86,7 +86,7 @@ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_ST ) if cus is None: - cus = models.Customer.objects.create(stripe_id=stripe_customer["id"]) + cus = models.Customer.objects.create(stripe_id=stripe_customer["id"], stripe_account=stripe_account) models.UserAccount.objects.create( user=user, account=stripe_account, From 6645a91afdc4c27aaec1de1cc672d121ad5a3571 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 18 Oct 2017 11:25:56 +0200 Subject: [PATCH 018/153] hotfix --- pinax/stripe/actions/customers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index 516ddc6c5..3ee772c5a 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -86,7 +86,7 @@ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_ST ) if cus is None: - cus = models.Customer.objects.create(stripe_id=stripe_customer["id"], stripe_account=stripe_account) + cus = models.Customer.objects.create(stripe_id=stripe_customer["id"], stripe_account=stripe_account.stripe_id) models.UserAccount.objects.create( user=user, account=stripe_account, From 2dd57b7ca6e7da06594e14109baf54da64c39916 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 18 Oct 2017 11:39:54 +0200 Subject: [PATCH 019/153] retrieve customer also with stripe_account --- pinax/stripe/actions/customers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index 3ee772c5a..0c3e1b7b6 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -61,7 +61,7 @@ def _create_without_account(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None): try: - cus = user.customers.get(user_account__account=stripe_account) + cus = user.customers.get(user_account__account=stripe_account, stripe_account=stripe_account.stripe_id) except models.Customer.DoesNotExist: cus = None pass From ff849c845e9f793a99db0399f0d7313a17da7e8f Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 18 Oct 2017 11:54:29 +0200 Subject: [PATCH 020/153] remove spurious pass --- pinax/stripe/actions/customers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index 0c3e1b7b6..066baae6b 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -64,7 +64,6 @@ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_ST cus = user.customers.get(user_account__account=stripe_account, stripe_account=stripe_account.stripe_id) except models.Customer.DoesNotExist: cus = None - pass else: try: stripe.Customer.retrieve(cus.stripe_id) From 0adce00feb669a96cadbbb8d733b349822032dce Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 18 Oct 2017 12:25:00 +0200 Subject: [PATCH 021/153] remove customer from unique constraint --- .../migrations/0014_auto_20171018_1024.py | 21 +++++++++++++++++++ pinax/stripe/models.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 pinax/stripe/migrations/0014_auto_20171018_1024.py diff --git a/pinax/stripe/migrations/0014_auto_20171018_1024.py b/pinax/stripe/migrations/0014_auto_20171018_1024.py new file mode 100644 index 000000000..ca8f9563d --- /dev/null +++ b/pinax/stripe/migrations/0014_auto_20171018_1024.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-18 10:24 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('pinax_stripe', '0013_revert_0011'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='useraccount', + unique_together=set([('user', 'account')]), + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index aa8f73614..cc1b9651b 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -169,7 +169,7 @@ class UserAccount(models.Model): related_query_name="user_account") class Meta: - unique_together = ("user", "account", "customer") + unique_together = ("user", "account") @python_2_unicode_compatible From 918d81eea81d528a37a7bdd238c75d7e9635fb7d Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 18 Oct 2017 12:30:56 +0200 Subject: [PATCH 022/153] fix Isort --- pinax/stripe/tests/test_actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 27a734b0d..fd12f64f5 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -36,7 +36,7 @@ Plan, Subscription, Transfer, - UserAccount, + UserAccount ) From d392fecd936b0f813b8b340905e44562da9d9fee Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 18 Oct 2017 13:57:29 +0200 Subject: [PATCH 023/153] Expose UserAccount on admin --- pinax/stripe/admin.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 15bb1c15e..6bc9589a3 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -17,7 +17,8 @@ Plan, Subscription, Transfer, - TransferChargeFee + TransferChargeFee, + UserAccount ) @@ -410,3 +411,14 @@ class TransferChargeFeeInline(admin.TabularInline): "stripe_id", ] ) + +admin.site.register( + UserAccount, + raw_id_fields=["user", "customer", "account"], + list_display=["user", "customer", "account"], + search_fields=[ + "=customer__stripe_id", + "=account__stripe_id", + "=user__email", + ] +) From 731f5fec62cca6db23e2a46c8880fba8a8995837 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 18 Oct 2017 14:01:07 +0200 Subject: [PATCH 024/153] fix name of related field --- pinax/stripe/actions/customers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index 066baae6b..e1b56ea07 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -139,7 +139,7 @@ def get_customer_for_user(user, stripe_account=None): def purge_local(customer): - customer.users.all().delete() + customer.user_accounts.all().delete() customer.user = None customer.date_purged = timezone.now() customer.save() From 3891c49f4706a84d919a9662164171cb963427fb Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 18 Oct 2017 14:12:21 +0200 Subject: [PATCH 025/153] fixup! fix name of related field --- pinax/stripe/tests/test_actions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index fd12f64f5..7d8d09a82 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -392,6 +392,7 @@ def test_purge_connected(self, RetrieveMock): self.assertIsNone(Customer.objects.get(stripe_id=customer.stripe_id).user) self.assertIsNotNone(Customer.objects.get(stripe_id=customer.stripe_id).date_purged) self.assertFalse(UserAccount.objects.exists()) + self.assertTrue(self.User.objects.exists()) @patch("stripe.Customer.retrieve") def test_purge_already_deleted(self, RetrieveMock): From c931f3498e508c6d524b31ab4b35b413fea9d90d Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 18 Oct 2017 16:10:59 +0200 Subject: [PATCH 026/153] Make sure we process customer.created event Connect aware --- pinax/stripe/actions/customers.py | 21 +++++---- pinax/stripe/models.py | 9 ++-- pinax/stripe/tests/test_actions.py | 68 +++++++++++++++++++++++++++++ pinax/stripe/tests/test_webhooks.py | 33 +++++++++++++- pinax/stripe/webhooks.py | 3 ++ 5 files changed, 122 insertions(+), 12 deletions(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index e1b56ea07..03af69feb 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -61,7 +61,7 @@ def _create_without_account(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None): try: - cus = user.customers.get(user_account__account=stripe_account, stripe_account=stripe_account.stripe_id) + cus = user.customers.get(user_account__account__stripe_id=stripe_account, stripe_account=stripe_account) except models.Customer.DoesNotExist: cus = None else: @@ -81,16 +81,20 @@ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_ST plan=plan, quantity=quantity, trial_end=trial_end, - stripe_account=stripe_account.stripe_id, + stripe_account=stripe_account, ) if cus is None: - cus = models.Customer.objects.create(stripe_id=stripe_customer["id"], stripe_account=stripe_account.stripe_id) - models.UserAccount.objects.create( + cus = models.Customer.objects.create(stripe_id=stripe_customer["id"], stripe_account=stripe_account) + account = models.Account.objects.get(stripe_id=stripe_account) + ua, created = models.UserAccount.objects.get_or_create( user=user, - account=stripe_account, - customer=cus, + account=account, + defaults={"customer": cus}, ) + if not created: + ua.customer = cus + ua.save() else: cus.stripe_id = stripe_customer["id"] # sync_customer will call cus.save() sync_customer(cus, stripe_customer) @@ -112,7 +116,7 @@ def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_imme charge_immediately: whether or not the user should be immediately charged for the subscription quantity: the quantity (multiplier) of the subscription - stripe_account: An account object. If given, the Customer and User relation will be established for you through UserAccount model. + stripe_account: An account id. If given, the Customer and User relation will be established for you through UserAccount model. Because a single User might have several Customers, one per Account. Returns: @@ -129,13 +133,14 @@ def get_customer_for_user(user, stripe_account=None): Args: user: a user object + stripe_account: An account id Returns: a pinax.stripe.models.Customer object """ if stripe_account is None: return models.Customer.objects.filter(user=user).first() - return user.customers.filter(user_account__account=stripe_account).first() + return user.customers.filter(user_account__account__stripe_id=stripe_account).first() def purge_local(customer): diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index cc1b9651b..d7a53a383 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -160,13 +160,16 @@ class TransferChargeFee(models.Model): class UserAccount(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="user_accounts", - related_query_name="user_account") + related_query_name="user_account", + on_delete=models.CASCADE) account = models.ForeignKey("pinax_stripe.Account", related_name="user_accounts", - related_query_name="user_account") + related_query_name="user_account", + on_delete=models.CASCADE) customer = models.ForeignKey("pinax_stripe.Customer", related_name="user_accounts", - related_query_name="user_account") + related_query_name="user_account", + on_delete=models.CASCADE) class Meta: unique_together = ("user", "account") diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index fd12f64f5..eb4a7d728 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -458,6 +458,74 @@ def test_link_customer_does_not_exist(self): self.assertIsNone(event.customer) +class CustomersWithConnectTests(TestCase): + + def setUp(self): + self.User = get_user_model() + self.user = self.User.objects.create_user( + username="patrick", + email="paltman@example.com" + ) + self.plan = Plan.objects.create( + stripe_id="p1", + amount=10, + currency="usd", + interval="monthly", + interval_count=1, + name="Pro" + ) + self.account = Account.objects.create( + stripe_id="acc_XXX" + ) + + def test_get_customer_for_user_with_stripe_account(self): + expected = Customer.objects.create(stripe_id="x", user=self.user) + UserAccount.objects.create(user=self.user, + customer=expected, + account=self.account) + actual = customers.get_customer_for_user( + self.user, stripe_account=self.account.stripe_id) + self.assertEquals(expected, actual) + + @patch("pinax.stripe.actions.customers.sync_customer") + @patch("stripe.Customer.create") + def test_customer_create_with_connect(self, CreateMock, SyncMock): + CreateMock.return_value = dict(id="cus_XXXXX") + customer = customers.create(self.user, stripe_account=self.account.stripe_id) + self.assertIsNone(customer.user) + self.assertEqual(customer.stripe_id, "cus_XXXXX") + _, kwargs = CreateMock.call_args + self.assertEqual(kwargs["email"], self.user.email) + self.assertEqual(kwargs["stripe_account"], self.account.stripe_id) + self.assertIsNone(kwargs["source"]) + self.assertIsNone(kwargs["plan"]) + self.assertIsNone(kwargs["trial_end"]) + self.assertTrue(SyncMock.called) + + @patch("stripe.Customer.retrieve") + @patch("pinax.stripe.actions.customers.sync_customer") + @patch("stripe.Customer.create") + def test_customer_create_with_connect_stale_user_account(self, CreateMock, SyncMock, RetrieveMock): + CreateMock.return_value = dict(id="cus_XXXXX") + RetrieveMock.side_effect = stripe.error.InvalidRequestError( + message="Not Found", param="stripe_id" + ) + ua = UserAccount.objects.create(user=self.user, + account=self.account, + customer=Customer.objects.create(stripe_id="cus_Z")) + customer = customers.create(self.user, stripe_account=self.account.stripe_id) + self.assertIsNone(customer.user) + self.assertEqual(customer.stripe_id, "cus_XXXXX") + _, kwargs = CreateMock.call_args + self.assertEqual(kwargs["email"], self.user.email) + self.assertEqual(kwargs["stripe_account"], self.account.stripe_id) + self.assertIsNone(kwargs["source"]) + self.assertIsNone(kwargs["plan"]) + self.assertIsNone(kwargs["trial_end"]) + self.assertTrue(SyncMock.called) + self.assertEqual(self.user.user_accounts.get(), ua) + + class EventsTests(TestCase): def test_dupe_event_exists(self): diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index 39885813c..6269181b7 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -1,6 +1,7 @@ import decimal import json +from django.contrib.auth import get_user_model from django.dispatch import Signal from django.test import TestCase from django.test.client import Client @@ -14,11 +15,19 @@ TRANSFER_CREATED_TEST_DATA, TRANSFER_PENDING_TEST_DATA ) -from ..models import Customer, Event, EventProcessingException, Plan, Transfer +from ..models import ( + Account, + Customer, + Event, + EventProcessingException, + Plan, + Transfer +) from ..webhooks import ( AccountApplicationDeauthorizeWebhook, AccountUpdatedWebhook, ChargeCapturedWebhook, + CustomerCreatedWebhook, CustomerSourceCreatedWebhook, CustomerSourceDeletedWebhook, CustomerSubscriptionCreatedWebhook, @@ -234,6 +243,28 @@ def test_process_webhook_with_customer_with_data(self, SyncMock): self.assertIs(SyncMock.call_args[0][1], obj) +class CustomerCreatedWebhookTest(TestCase): + + @patch("pinax.stripe.actions.customers.create") + def test_process_webhook(self, CreateMock): + user = get_user_model().objects.create() + 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.called_once_with_args(user, stripe_account=None) + + @patch("pinax.stripe.actions.customers.create") + def test_process_webhook_with_stripe_account(self, CreateMock): + user = get_user_model().objects.create() + account = Account.objects.create(stripe_id="acc_A") + event = Event.objects.create(kind=CustomerCreatedWebhook.name, webhook_message={}, valid=True, processed=False, stripe_account=account.stripe_id) + obj = object() + event.validated_message = dict(data=dict(object=obj)) + CustomerCreatedWebhook(event).process_webhook() + CreateMock.called_once_with_args(user, stripe_account=account.stripe_id) + + 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 93b2de09f..4290b2a45 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -276,6 +276,9 @@ class CustomerCreatedWebhook(Webhook): name = "customer.created" description = "Occurs whenever a new customer is created." + def process_webhook(self): + customers.create(self.event.customer, stripe_account=self.stripe_account) + class CustomerDeletedWebhook(Webhook): name = "customer.deleted" From a963081e2ee18306dd4c40e0fd1b38a8017bab87 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 18 Oct 2017 21:41:06 +0200 Subject: [PATCH 027/153] This was a bad Idea, customer.created does not call actions.customers --- pinax/stripe/tests/test_webhooks.py | 7 ++----- pinax/stripe/webhooks.py | 3 --- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index 6269181b7..ea607eac7 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -1,7 +1,6 @@ import decimal import json -from django.contrib.auth import get_user_model from django.dispatch import Signal from django.test import TestCase from django.test.client import Client @@ -247,22 +246,20 @@ class CustomerCreatedWebhookTest(TestCase): @patch("pinax.stripe.actions.customers.create") def test_process_webhook(self, CreateMock): - user = get_user_model().objects.create() 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.called_once_with_args(user, stripe_account=None) + CreateMock.assert_not_called() @patch("pinax.stripe.actions.customers.create") def test_process_webhook_with_stripe_account(self, CreateMock): - user = get_user_model().objects.create() account = Account.objects.create(stripe_id="acc_A") event = Event.objects.create(kind=CustomerCreatedWebhook.name, webhook_message={}, valid=True, processed=False, stripe_account=account.stripe_id) obj = object() event.validated_message = dict(data=dict(object=obj)) CustomerCreatedWebhook(event).process_webhook() - CreateMock.called_once_with_args(user, stripe_account=account.stripe_id) + CreateMock.assert_not_called() class CustomerSourceCreatedWebhookTest(TestCase): diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 4290b2a45..93b2de09f 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -276,9 +276,6 @@ class CustomerCreatedWebhook(Webhook): name = "customer.created" description = "Occurs whenever a new customer is created." - def process_webhook(self): - customers.create(self.event.customer, stripe_account=self.stripe_account) - class CustomerDeletedWebhook(Webhook): name = "customer.deleted" From 082773b04d7fb31f092aadedff56da6c5f1e8126 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 19 Oct 2017 13:58:50 +0200 Subject: [PATCH 028/153] Use a ForeignKey to maintain relation with an account --- pinax/stripe/actions/customers.py | 5 ++--- pinax/stripe/models.py | 24 +++++++++++++++++------- pinax/stripe/tests/test_actions.py | 14 +++++++++----- pinax/stripe/tests/test_models.py | 6 ++++++ pinax/stripe/tests/test_webhooks.py | 10 ++++++---- pinax/stripe/webhooks.py | 9 +++++---- 6 files changed, 45 insertions(+), 23 deletions(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index 03af69feb..f0f8f97bf 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -81,15 +81,14 @@ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_ST plan=plan, quantity=quantity, trial_end=trial_end, - stripe_account=stripe_account, + stripe_account=stripe_account.stripe_id, ) if cus is None: cus = models.Customer.objects.create(stripe_id=stripe_customer["id"], stripe_account=stripe_account) - account = models.Account.objects.get(stripe_id=stripe_account) ua, created = models.UserAccount.objects.get_or_create( user=user, - account=account, + account=stripe_account, defaults={"customer": cus}, ) if not created: diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 8c742400c..2de8662bf 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -27,7 +27,13 @@ class Meta: class AccountRelatedStripeObject(StripeObject): - stripe_account = models.CharField(max_length=255, null=True, blank=True) + stripe_account = models.ForeignKey( + "pinax_stripe.Account", + on_delete=models.CASCADE, + null=True, + default=None, + blank=True, + ) class Meta: abstract = True @@ -151,9 +157,10 @@ class Transfer(AccountRelatedStripeObject): @property def stripe_transfer(self): + stripe_account = getattr(self.stripe_account, "stripe_id", None) return stripe.Transfer.retrieve( self.stripe_id, - stripe_account=self.stripe_account + stripe_account=stripe_account ) @@ -204,9 +211,10 @@ class Customer(AccountRelatedStripeObject): @cached_property def stripe_customer(self): + stripe_account = getattr(self.stripe_account, "stripe_id", None) return stripe.Customer.retrieve( self.stripe_id, - stripe_account=self.stripe_account + stripe_account=stripe_account ) def __str__(self): @@ -347,7 +355,7 @@ def status(self): @property def stripe_invoice(self): try: - stripe_account = self.customer.stripe_account + stripe_account = getattr(self.customer.stripe_account, "stripe_id", None) except ObjectDoesNotExist: stripe_account = None return stripe.Invoice.retrieve( @@ -413,11 +421,13 @@ class Charge(StripeObject): @property def stripe_charge(self): + if self.customer is not None: + stripe_account = getattr(self.customer.stripe_account, "stripe_id", None) + else: + stripe_account = None return stripe.Charge.retrieve( self.stripe_id, - stripe_account=( - self.customer.stripe_account if self.customer_id else None - ), + stripe_account=stripe_account, expand=["balance_transaction"] ) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 16e21812a..12fe388ed 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -383,7 +383,7 @@ def test_purge_connected(self, RetrieveMock): ) customer = Customer.objects.create( user=self.user, - stripe_account=account.stripe_id, + stripe_account=account, stripe_id="cus_xxxxxxxxxxxxxxx", ) UserAccount.objects.create(user=self.user, account=account, customer=customer) @@ -492,7 +492,7 @@ def test_get_customer_for_user_with_stripe_account(self): @patch("stripe.Customer.create") def test_customer_create_with_connect(self, CreateMock, SyncMock): CreateMock.return_value = dict(id="cus_XXXXX") - customer = customers.create(self.user, stripe_account=self.account.stripe_id) + customer = customers.create(self.user, stripe_account=self.account) self.assertIsNone(customer.user) self.assertEqual(customer.stripe_id, "cus_XXXXX") _, kwargs = CreateMock.call_args @@ -514,7 +514,7 @@ def test_customer_create_with_connect_stale_user_account(self, CreateMock, SyncM ua = UserAccount.objects.create(user=self.user, account=self.account, customer=Customer.objects.create(stripe_id="cus_Z")) - customer = customers.create(self.user, stripe_account=self.account.stripe_id) + customer = customers.create(self.user, stripe_account=self.account) self.assertIsNone(customer.user) self.assertEqual(customer.stripe_id, "cus_XXXXX") _, kwargs = CreateMock.call_args @@ -528,6 +528,10 @@ def test_customer_create_with_connect_stale_user_account(self, CreateMock, SyncM class EventsTests(TestCase): + def setUp(self): + self.account = Account.objects.create( + stripe_id="acc_XXX" + ) def test_dupe_event_exists(self): Event.objects.create(stripe_id="evt_003", kind="foo", livemode=True, webhook_message="{}", api_version="", request="", pending_webhooks=0) @@ -542,8 +546,8 @@ def test_add_event(self, ProcessMock): @patch("pinax.stripe.webhooks.AccountUpdatedWebhook.process") def test_add_event_connect(self, ProcessMock): - events.add_event(stripe_id="evt_001", kind="account.updated", livemode=True, message={}, stripe_account="acct_001") - event = Event.objects.get(stripe_id="evt_001", stripe_account="acct_001") + events.add_event(stripe_id="evt_001", kind="account.updated", livemode=True, message={}, stripe_account=self.account) + event = Event.objects.get(stripe_id="evt_001", stripe_account=self.account) self.assertEquals(event.kind, "account.updated") self.assertTrue(ProcessMock.called) diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index caa669bac..ea443c0eb 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -147,6 +147,12 @@ def test_stripe_charge(self, RetrieveMock): Charge().stripe_charge self.assertTrue(RetrieveMock.called) + @patch("stripe.Charge.retrieve") + def test_stripe_charge_with_account(self, RetrieveMock): + cu = Customer(stripe_account=Account()) + Charge(customer=cu).stripe_charge + self.assertTrue(RetrieveMock.called) + @patch("stripe.Customer.retrieve") def test_stripe_customer(self, RetrieveMock): Customer().stripe_customer diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index 6269181b7..628a216d0 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -54,6 +54,9 @@ def test_get_signal_keyerror(self): class WebhookTests(TestCase): + def setUp(self): + self.account = Account.objects.create(stripe_id="acc_XXX") + event_data = { "created": 1348360173, "data": { @@ -120,9 +123,8 @@ def test_webhook_with_transfer_event(self, TransferMock, StripeEventMock): @patch("stripe.Transfer.retrieve") def test_webhook_associated_with_stripe_account(self, TransferMock, StripeEventMock): connect_event_data = self.event_data.copy() - stripe_account = "acct_123123123" # only difference is that we'll have a user_id value - connect_event_data["account"] = stripe_account + connect_event_data["account"] = self.account.stripe_id StripeEventMock.return_value.to_dict.return_value = connect_event_data TransferMock.return_value = connect_event_data["data"]["object"] msg = json.dumps(connect_event_data) @@ -135,7 +137,7 @@ def test_webhook_associated_with_stripe_account(self, TransferMock, StripeEventM self.assertTrue(Event.objects.filter(kind="transfer.created").exists()) self.assertEqual( Event.objects.filter(kind="transfer.created").first().stripe_account, - stripe_account + self.account ) def test_webhook_duplicate_event(self): @@ -258,7 +260,7 @@ def test_process_webhook(self, CreateMock): def test_process_webhook_with_stripe_account(self, CreateMock): user = get_user_model().objects.create() account = Account.objects.create(stripe_id="acc_A") - event = Event.objects.create(kind=CustomerCreatedWebhook.name, webhook_message={}, valid=True, processed=False, stripe_account=account.stripe_id) + 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() diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 4290b2a45..2decd384b 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -5,6 +5,7 @@ import stripe from six import with_metaclass +from . import models from .actions import ( accounts, charges, @@ -83,11 +84,12 @@ def validate(self): For Connect accounts we must fetch the event using the `stripe_account` parameter. """ - self.stripe_account = self.event.webhook_message.get("account") + self.stripe_account = models.Account.objects.filter( + stripe_id=self.event.webhook_message.get("account")).first() self.event.stripe_account = self.stripe_account evt = stripe.Event.retrieve( self.event.stripe_id, - stripe_account=self.event.stripe_account + stripe_account=getattr(self.stripe_account, "stripe_id", None) ) self.event.validated_message = json.loads( json.dumps( @@ -97,7 +99,6 @@ def validate(self): ) ) self.event.valid = self.event.webhook_message["data"] == self.event.validated_message["data"] - self.event.save() def send_signal(self): signal = registry.get_signal(self.name) @@ -119,7 +120,7 @@ def process(self): if isinstance(e, stripe.StripeError): data = e.http_body exceptions.log_exception(data=data, exception=e, event=self.event) - raise + raise e def process_webhook(self): return From 7804028782830a734b922cb24f4a37d4a5d04a59 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 19 Oct 2017 13:59:11 +0200 Subject: [PATCH 029/153] migration of Account foreign key --- .../migrations/0015_account-as-foreign-key.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 pinax/stripe/migrations/0015_account-as-foreign-key.py diff --git a/pinax/stripe/migrations/0015_account-as-foreign-key.py b/pinax/stripe/migrations/0015_account-as-foreign-key.py new file mode 100644 index 000000000..c3235a9fd --- /dev/null +++ b/pinax/stripe/migrations/0015_account-as-foreign-key.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-19 10:41 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0014_auto_20171018_1024'), + ] + + operations = [ + migrations.AlterField( + model_name='customer', + 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='event', + 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='plan', + 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='transfer', + name='stripe_account', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Account'), + ), + ] From 586aea5738fc99824f6ba6292ccf5356e4904dc3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 19 Oct 2017 13:53:57 +0200 Subject: [PATCH 030/153] tests: factor out InvoiceSyncsTests --- pinax/stripe/tests/test_actions.py | 322 +++++------------------------ 1 file changed, 49 insertions(+), 273 deletions(-) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 12fe388ed..706db586c 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -2313,15 +2313,22 @@ def test_sync_invoice_items_updating(self, RetrieveSubscriptionMock): self.assertTrue(invoice.items.all().count(), 2) self.assertEquals(invoice.items.all()[1].description, "This is your second subscription") - @patch("pinax.stripe.hooks.hookset.send_receipt") - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("stripe.Charge.retrieve") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("pinax.stripe.actions.invoices.sync_invoice_items") - @patch("pinax.stripe.actions.subscriptions.retrieve") - def test_sync_invoice_from_stripe_data(self, RetrieveSubscriptionMock, SyncInvoiceItemsMock, SyncChargeMock, ChargeFetchMock, SyncSubscriptionMock, SendReceiptMock): + +class InvoiceSyncsTests(TestCase): + + def setUp(self): + self.User = get_user_model() + self.user = self.User.objects.create_user( + username="patrick", + email="paltman@example.com" + ) + self.customer = Customer.objects.create( + user=self.user, + stripe_id="cus_xxxxxxxxxxxxxxx" + ) + plan = Plan.objects.create(stripe_id="pro2", interval="month", interval_count=1, amount=decimal.Decimal("19.99")) - subscription = Subscription.objects.create( + self.subscription = Subscription.objects.create( stripe_id="sub_7Q4BX0HMfqTpN8", customer=self.customer, plan=plan, @@ -2329,26 +2336,14 @@ def test_sync_invoice_from_stripe_data(self, RetrieveSubscriptionMock, SyncInvoi status="active", start=timezone.now() ) - charge = Charge.objects.create( - stripe_id="ch_XXXXXX", - customer=self.customer, - source="card_01", - amount=decimal.Decimal("10.00"), - currency="usd", - paid=True, - refunded=False, - disputed=False - ) - SyncChargeMock.return_value = charge - SyncSubscriptionMock.return_value = subscription - data = { + self.invoice_data = { "id": "in_17B6e8I10iPhvocMGtYd4hDD", "object": "invoice", "amount_due": 1999, "application_fee": None, "attempt_count": 0, "attempted": False, - "charge": charge.stripe_id, + "charge": None, "closed": False, "currency": "usd", "customer": self.customer.stripe_id, @@ -2359,7 +2354,7 @@ def test_sync_invoice_from_stripe_data(self, RetrieveSubscriptionMock, SyncInvoi "forgiven": False, "lines": { "data": [{ - "id": subscription.stripe_id, + "id": self.subscription.stripe_id, "object": "line_item", "amount": 0, "currency": "usd", @@ -2406,14 +2401,35 @@ def test_sync_invoice_from_stripe_data(self, RetrieveSubscriptionMock, SyncInvoi "receipt_number": None, "starting_balance": 0, "statement_descriptor": None, - "subscription": subscription.stripe_id, + "subscription": self.subscription.stripe_id, "subtotal": 1999, "tax": None, "tax_percent": None, "total": 1999, "webhooks_delivered_at": None } - invoices.sync_invoice_from_stripe_data(data) + + @patch("pinax.stripe.hooks.hookset.send_receipt") + @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") + @patch("stripe.Charge.retrieve") + @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") + @patch("pinax.stripe.actions.invoices.sync_invoice_items") + @patch("pinax.stripe.actions.subscriptions.retrieve") + def test_sync_invoice_from_stripe_data(self, RetrieveSubscriptionMock, SyncInvoiceItemsMock, SyncChargeMock, ChargeFetchMock, SyncSubscriptionMock, SendReceiptMock): + charge = Charge.objects.create( + stripe_id="ch_XXXXXX", + customer=self.customer, + source="card_01", + amount=decimal.Decimal("10.00"), + currency="usd", + paid=True, + refunded=False, + disputed=False + ) + self.invoice_data["charge"] = charge.stripe_id + SyncChargeMock.return_value = charge + SyncSubscriptionMock.return_value = self.subscription + invoices.sync_invoice_from_stripe_data(self.invoice_data) self.assertTrue(SyncInvoiceItemsMock.called) self.assertEquals(Invoice.objects.filter(customer=self.customer).count(), 1) self.assertTrue(ChargeFetchMock.called) @@ -2427,15 +2443,6 @@ def test_sync_invoice_from_stripe_data(self, RetrieveSubscriptionMock, SyncInvoi @patch("pinax.stripe.actions.invoices.sync_invoice_items") @patch("pinax.stripe.actions.subscriptions.retrieve") def test_sync_invoice_from_stripe_data_no_send_receipt(self, RetrieveSubscriptionMock, SyncInvoiceItemsMock, SyncChargeMock, ChargeFetchMock, SyncSubscriptionMock, SendReceiptMock): - plan = Plan.objects.create(stripe_id="pro2", interval="month", interval_count=1, amount=decimal.Decimal("19.99")) - subscription = Subscription.objects.create( - stripe_id="sub_7Q4BX0HMfqTpN8", - customer=self.customer, - plan=plan, - quantity=1, - status="active", - start=timezone.now() - ) charge = Charge.objects.create( stripe_id="ch_XXXXXX", customer=self.customer, @@ -2446,81 +2453,10 @@ def test_sync_invoice_from_stripe_data_no_send_receipt(self, RetrieveSubscriptio refunded=False, disputed=False ) + self.invoice_data["charge"] = charge.stripe_id SyncChargeMock.return_value = charge - SyncSubscriptionMock.return_value = subscription - data = { - "id": "in_17B6e8I10iPhvocMGtYd4hDD", - "object": "invoice", - "amount_due": 1999, - "application_fee": None, - "attempt_count": 0, - "attempted": False, - "charge": charge.stripe_id, - "closed": False, - "currency": "usd", - "customer": self.customer.stripe_id, - "date": 1448470892, - "description": None, - "discount": None, - "ending_balance": None, - "forgiven": False, - "lines": { - "data": [{ - "id": subscription.stripe_id, - "object": "line_item", - "amount": 0, - "currency": "usd", - "description": None, - "discountable": True, - "livemode": True, - "metadata": { - }, - "period": { - "start": 1448499344, - "end": 1448758544 - }, - "plan": { - "id": "pro2", - "object": "plan", - "amount": 1999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": { - }, - "name": "The Pro Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - "proration": False, - "quantity": 1, - "subscription": None, - "type": "subscription" - }], - "total_count": 1, - "object": "list", - "url": "/v1/invoices/in_17B6e8I10iPhvocMGtYd4hDD/lines" - }, - "livemode": False, - "metadata": { - }, - "next_payment_attempt": 1448474492, - "paid": False, - "period_end": 1448470739, - "period_start": 1448211539, - "receipt_number": None, - "starting_balance": 0, - "statement_descriptor": None, - "subscription": subscription.stripe_id, - "subtotal": 1999, - "tax": None, - "tax_percent": None, - "total": 1999, - "webhooks_delivered_at": None - } - invoices.sync_invoice_from_stripe_data(data, send_receipt=False) + SyncSubscriptionMock.return_value = self.subscription + invoices.sync_invoice_from_stripe_data(self.invoice_data, send_receipt=False) self.assertTrue(SyncInvoiceItemsMock.called) self.assertEquals(Invoice.objects.filter(customer=self.customer).count(), 1) self.assertTrue(ChargeFetchMock.called) @@ -2531,89 +2467,9 @@ def test_sync_invoice_from_stripe_data_no_send_receipt(self, RetrieveSubscriptio @patch("pinax.stripe.actions.invoices.sync_invoice_items") @patch("pinax.stripe.actions.subscriptions.retrieve") def test_sync_invoice_from_stripe_data_no_charge(self, RetrieveSubscriptionMock, SyncInvoiceItemsMock, SyncSubscriptionMock): - plan = Plan.objects.create(stripe_id="pro2", interval="month", interval_count=1, amount=decimal.Decimal("19.99")) - subscription = Subscription.objects.create( - stripe_id="sub_7Q4BX0HMfqTpN8", - customer=self.customer, - plan=plan, - quantity=1, - status="active", - start=timezone.now() - ) - SyncSubscriptionMock.return_value = subscription - data = { - "id": "in_17B6e8I10iPhvocMGtYd4hDD", - "object": "invoice", - "amount_due": 1999, - "application_fee": None, - "attempt_count": 0, - "attempted": False, - "charge": None, - "closed": False, - "currency": "usd", - "customer": self.customer.stripe_id, - "date": 1448470892, - "description": None, - "discount": None, - "ending_balance": None, - "forgiven": False, - "lines": { - "data": [{ - "id": subscription.stripe_id, - "object": "line_item", - "amount": 0, - "currency": "usd", - "description": None, - "discountable": True, - "livemode": True, - "metadata": { - }, - "period": { - "start": 1448499344, - "end": 1448758544 - }, - "plan": { - "id": "pro2", - "object": "plan", - "amount": 1999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": { - }, - "name": "The Pro Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - "proration": False, - "quantity": 1, - "subscription": None, - "type": "subscription" - }], - "total_count": 1, - "object": "list", - "url": "/v1/invoices/in_17B6e8I10iPhvocMGtYd4hDD/lines" - }, - "livemode": False, - "metadata": { - }, - "next_payment_attempt": 1448474492, - "paid": False, - "period_end": 1448470739, - "period_start": 1448211539, - "receipt_number": None, - "starting_balance": 0, - "statement_descriptor": None, - "subscription": subscription.stripe_id, - "subtotal": 1999, - "tax": None, - "tax_percent": None, - "total": 1999, - "webhooks_delivered_at": None - } - invoices.sync_invoice_from_stripe_data(data) + SyncSubscriptionMock.return_value = self.subscription + self.invoice_data["charge"] = None + invoices.sync_invoice_from_stripe_data(self.invoice_data) self.assertTrue(SyncInvoiceItemsMock.called) self.assertEquals(Invoice.objects.filter(customer=self.customer).count(), 1) @@ -2688,88 +2544,8 @@ def test_sync_invoice_from_stripe_data_no_subscription(self, RetrieveSubscriptio @patch("pinax.stripe.actions.invoices.sync_invoice_items") @patch("pinax.stripe.actions.subscriptions.retrieve") def test_sync_invoice_from_stripe_data_updated(self, RetrieveSubscriptionMock, SyncInvoiceItemsMock, SyncSubscriptionMock): - plan = Plan.objects.create(stripe_id="pro2", interval="month", interval_count=1, amount=decimal.Decimal("19.99")) - subscription = Subscription.objects.create( - stripe_id="sub_7Q4BX0HMfqTpN8", - customer=self.customer, - plan=plan, - quantity=1, - status="active", - start=timezone.now() - ) - SyncSubscriptionMock.return_value = subscription - data = { - "id": "in_17B6e8I10iPhvocMGtYd4hDD", - "object": "invoice", - "amount_due": 1999, - "application_fee": None, - "attempt_count": 0, - "attempted": False, - "charge": None, - "closed": False, - "currency": "usd", - "customer": self.customer.stripe_id, - "date": 1448470892, - "description": None, - "discount": None, - "ending_balance": None, - "forgiven": False, - "lines": { - "data": [{ - "id": subscription.stripe_id, - "object": "line_item", - "amount": 0, - "currency": "usd", - "description": None, - "discountable": True, - "livemode": True, - "metadata": { - }, - "period": { - "start": 1448499344, - "end": 1448758544 - }, - "plan": { - "id": "pro2", - "object": "plan", - "amount": 1999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": { - }, - "name": "The Pro Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - "proration": False, - "quantity": 1, - "subscription": None, - "type": "subscription" - }], - "total_count": 1, - "object": "list", - "url": "/v1/invoices/in_17B6e8I10iPhvocMGtYd4hDD/lines" - }, - "livemode": False, - "metadata": { - }, - "next_payment_attempt": 1448474492, - "paid": False, - "period_end": 1448470739, - "period_start": 1448211539, - "receipt_number": None, - "starting_balance": 0, - "statement_descriptor": None, - "subscription": subscription.stripe_id, - "subtotal": 1999, - "tax": None, - "tax_percent": None, - "total": 1999, - "webhooks_delivered_at": None - } + SyncSubscriptionMock.return_value = self.subscription + data = self.invoice_data invoices.sync_invoice_from_stripe_data(data) self.assertTrue(SyncInvoiceItemsMock.called) self.assertEquals(Invoice.objects.filter(customer=self.customer).count(), 1) From 3deef747bb249b1c26f738adc8ace563c4311049 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 19 Oct 2017 14:12:44 +0200 Subject: [PATCH 031/153] sync_invoice_from_stripe_data: use stripe_account --- pinax/stripe/actions/invoices.py | 3 ++- pinax/stripe/tests/test_actions.py | 32 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/actions/invoices.py b/pinax/stripe/actions/invoices.py index e01396954..29503a97c 100644 --- a/pinax/stripe/actions/invoices.py +++ b/pinax/stripe/actions/invoices.py @@ -79,9 +79,10 @@ def sync_invoice_from_stripe_data(stripe_invoice, send_receipt=settings.PINAX_ST period_start = utils.convert_tstamp(stripe_invoice, "period_start") date = utils.convert_tstamp(stripe_invoice, "date") sub_id = stripe_invoice.get("subscription") + stripe_account_id = stripe_invoice.get("account") if stripe_invoice.get("charge"): - charge = charges.sync_charge(stripe_invoice["charge"]) + charge = charges.sync_charge(stripe_invoice["charge"], stripe_account=stripe_account_id) if send_receipt: hooks.hookset.send_receipt(charge) else: diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 706db586c..f1fc809ff 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -2463,6 +2463,38 @@ def test_sync_invoice_from_stripe_data_no_send_receipt(self, RetrieveSubscriptio self.assertTrue(SyncChargeMock.called) self.assertFalse(SendReceiptMock.called) + @patch("pinax.stripe.hooks.hookset.send_receipt") + @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") + @patch("stripe.Charge.retrieve") + @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") + @patch("pinax.stripe.actions.invoices.sync_invoice_items") + @patch("pinax.stripe.actions.subscriptions.retrieve") + def test_sync_invoice_from_stripe_data_connect(self, RetrieveSubscriptionMock, SyncInvoiceItemsMock, SyncChargeMock, ChargeFetchMock, SyncSubscriptionMock, SendReceiptMock): + self.invoice_data["charge"] = "ch_XXXXXX" + self.invoice_data["account"] = "acct_X" + charge = Charge.objects.create( + stripe_id="ch_XXXXXX", + customer=self.customer, + source="card_01", + amount=decimal.Decimal("10.00"), + currency="usd", + paid=True, + refunded=False, + disputed=False + ) + SyncChargeMock.return_value = charge + SyncSubscriptionMock.return_value = self.subscription + invoices.sync_invoice_from_stripe_data(self.invoice_data) + self.assertTrue(SyncInvoiceItemsMock.called) + self.assertEquals(Invoice.objects.filter(customer=self.customer).count(), 1) + self.assertTrue(ChargeFetchMock.called) + args, kwargs = ChargeFetchMock.call_args + self.assertEquals(args, ("ch_XXXXXX",)) + self.assertEquals(kwargs, {"stripe_account": "acct_X", + "expand": ["balance_transaction"]}) + self.assertTrue(SyncChargeMock.called) + self.assertTrue(SendReceiptMock.called) + @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") @patch("pinax.stripe.actions.invoices.sync_invoice_items") @patch("pinax.stripe.actions.subscriptions.retrieve") From 308ff3a2e1d9778940d4cfd3d96327763bd2f746 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 19 Oct 2017 15:50:40 +0200 Subject: [PATCH 032/153] replace the fields --- .../migrations/0015_account-as-foreign-key.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/pinax/stripe/migrations/0015_account-as-foreign-key.py b/pinax/stripe/migrations/0015_account-as-foreign-key.py index c3235a9fd..2f843c634 100644 --- a/pinax/stripe/migrations/0015_account-as-foreign-key.py +++ b/pinax/stripe/migrations/0015_account-as-foreign-key.py @@ -13,22 +13,38 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterField( + migrations.RemoveField( + model_name='customer', + name='stripe_account', + ), + migrations.RemoveField( + model_name='event', + name='stripe_account', + ), + migrations.RemoveField( + model_name='plan', + name='stripe_account', + ), + migrations.RemoveField( + model_name='transfer', + name='stripe_account', + ), + migrations.AddField( model_name='customer', 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( + migrations.AddField( model_name='event', 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( + migrations.AddField( model_name='plan', 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( + migrations.AddField( model_name='transfer', name='stripe_account', field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Account'), From d5080abb21777e96980015eabd634da6f9284127 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 18 Oct 2017 14:28:58 +0200 Subject: [PATCH 033/153] CircleCI: test merge commit Ref: https://github.com/pinax/pinax-stripe/issues/415 Source: https://discuss.circleci.com/t/show-test-results-for-prospective-merge-of-a-github-pr/1662/14 --- .circleci/config.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0952bbc32..44d646873 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,6 +4,19 @@ common: &common working_directory: ~/repo steps: - checkout + - run: + name: checkout merge commit for PRs + command: | + set -x + PR_NUMBER=${CI_PULL_REQUEST//*pull\//} + if [ -n "$PR_NUMBER" ]; then + if ! git pull --ff-only origin "refs/pull/$PR_NUMBER/merge"; then + echo + echo -e "\033[0;31mERROR: Failed to merge your branch with the latest master." + echo -e "Please manually merge master into your branch, and push the changes to GitHub.\033[0m" + exit 1 + fi + fi - restore_cache: keys: - v2-deps-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} From 81e47772371228429182932e9707e634e9f8bd9f Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 19 Oct 2017 16:32:54 +0200 Subject: [PATCH 034/153] Enhance admin with connect search --- pinax/stripe/admin.py | 66 +++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 6bc9589a3..4f9de9654 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -104,6 +104,21 @@ def queryset(self, request, queryset): return queryset.all() +class AccountListFilter(admin.SimpleListFilter): + title = "account" + parameter_name = "stripe_account" + + def lookups(self, request, model_admin): + return [("none", "Without Account")] + [(a.pk, str(a)) for a in Account.objects.all()] + + def queryset(self, request, queryset): + if self.value() == "none": + return queryset.filter(stripe_account__isnull=True) + if self.value(): + return queryset.filter(stripe_account__pk=self.value()) + return queryset + + admin.site.register( Charge, list_display=[ @@ -115,22 +130,22 @@ def queryset(self, request, queryset): "disputed", "refunded", "receipt_sent", - "created_at" + "created_at", ], search_fields=[ "stripe_id", "customer__stripe_id", - "invoice__stripe_id" + "invoice__stripe_id", ] + customer_search_fields(), list_filter=[ "paid", "disputed", "refunded", - "created_at" + "created_at", ], raw_id_fields=[ "customer", - "invoice" + "invoice", ], ) @@ -153,25 +168,28 @@ def queryset(self, request, queryset): admin.site.register( Event, - raw_id_fields=["customer"], + raw_id_fields=["customer", "stripe_account"], list_display=[ "stripe_id", "kind", "livemode", "valid", "processed", - "created_at" + "created_at", + "stripe_account", ], list_filter=[ "kind", "created_at", "valid", - "processed" + "processed", + AccountListFilter, ], search_fields=[ "stripe_id", "customer__stripe_id", "validated_message" + "=stripe_account__stripe_id", ] + customer_search_fields(), ) @@ -201,7 +219,7 @@ def subscription_status(obj): admin.site.register( Customer, - raw_id_fields=["user"], + raw_id_fields=["user", "stripe_account"], list_display=[ "stripe_id", "user", @@ -210,15 +228,18 @@ def subscription_status(obj): "delinquent", "default_source", subscription_status, - "date_purged" + "date_purged", + "stripe_account", ], list_filter=[ "delinquent", CustomerHasCardListFilter, - CustomerSubscriptionStatusListFilter + CustomerSubscriptionStatusListFilter, + AccountListFilter, ], search_fields=[ "stripe_id", + "=stripe_account__stripe_id" ] + user_search_fields(), inlines=[ SubscriptionInline, @@ -262,7 +283,7 @@ def customer_user(obj): "period_start", "period_end", "subtotal", - "total" + "total", ], search_fields=[ "stripe_id", @@ -277,15 +298,16 @@ def customer_user(obj): "created_at", "date", "period_end", - "total" + "total", ], inlines=[ InvoiceItemInline - ] + ], ) admin.site.register( Plan, + raw_id_fields=["stripe_account"], list_display=[ "stripe_id", "name", @@ -294,13 +316,16 @@ def customer_user(obj): "interval", "interval_count", "trial_period_days", + "stripe_account", ], search_fields=[ "stripe_id", "name", - ], + "=stripe_account__stripe_id", + ] + customer_search_fields(), list_filter=[ "currency", + AccountListFilter, ], readonly_fields=[ "stripe_id", @@ -360,21 +385,26 @@ class TransferChargeFeeInline(admin.TabularInline): admin.site.register( Transfer, - raw_id_fields=["event"], + raw_id_fields=["event", "stripe_account"], list_display=[ "stripe_id", "amount", "status", "date", - "description" + "description", + "stripe_account", ], search_fields=[ "stripe_id", - "event__stripe_id" + "event__stripe_id", + "=stripe_account__stripe_id", ], inlines=[ TransferChargeFeeInline - ] + ], + list_filter=[ + AccountListFilter, + ], ) From 4707135c6e2a31ac90459f60b4ae742bf7c51a02 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 19 Oct 2017 16:38:13 +0200 Subject: [PATCH 035/153] remove UserAccount.account --- pinax/stripe/actions/customers.py | 17 +++++------------ pinax/stripe/admin.py | 5 ++--- pinax/stripe/models.py | 6 +----- pinax/stripe/tests/test_actions.py | 27 ++++++++++++++------------- 4 files changed, 22 insertions(+), 33 deletions(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index f0f8f97bf..d648a6bd9 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -61,7 +61,7 @@ def _create_without_account(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None): try: - cus = user.customers.get(user_account__account__stripe_id=stripe_account, stripe_account=stripe_account) + cus = user.customers.get(user_account__customer__stripe_account=stripe_account) except models.Customer.DoesNotExist: cus = None else: @@ -86,16 +86,9 @@ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_ST if cus is None: cus = models.Customer.objects.create(stripe_id=stripe_customer["id"], stripe_account=stripe_account) - ua, created = models.UserAccount.objects.get_or_create( - user=user, - account=stripe_account, - defaults={"customer": cus}, - ) - if not created: - ua.customer = cus - ua.save() + models.UserAccount.objects.create(user=user, customer=cus) else: - cus.stripe_id = stripe_customer["id"] # sync_customer will call cus.save() + cus.stripe_id = stripe_customer["id"] # sync_customer() will call cus.save() sync_customer(cus, stripe_customer) if plan and charge_immediately: invoices.create_and_pay(cus) @@ -115,7 +108,7 @@ def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_imme charge_immediately: whether or not the user should be immediately charged for the subscription quantity: the quantity (multiplier) of the subscription - stripe_account: An account id. If given, the Customer and User relation will be established for you through UserAccount model. + stripe_account: An account objects. If given, the Customer and User relation will be established for you through UserAccount model. Because a single User might have several Customers, one per Account. Returns: @@ -139,7 +132,7 @@ def get_customer_for_user(user, stripe_account=None): """ if stripe_account is None: return models.Customer.objects.filter(user=user).first() - return user.customers.filter(user_account__account__stripe_id=stripe_account).first() + return user.customers.filter(user_account__customer__stripe_account=stripe_account).first() def purge_local(customer): diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 4f9de9654..654af4c31 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -444,11 +444,10 @@ class TransferChargeFeeInline(admin.TabularInline): admin.site.register( UserAccount, - raw_id_fields=["user", "customer", "account"], - list_display=["user", "customer", "account"], + raw_id_fields=["user", "customer"], + list_display=["user", "customer"], search_fields=[ "=customer__stripe_id", - "=account__stripe_id", "=user__email", ] ) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 2de8662bf..a7b54cb29 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -181,17 +181,13 @@ class UserAccount(models.Model): related_name="user_accounts", related_query_name="user_account", on_delete=models.CASCADE) - account = models.ForeignKey("pinax_stripe.Account", - related_name="user_accounts", - related_query_name="user_account", - on_delete=models.CASCADE) customer = models.ForeignKey("pinax_stripe.Customer", related_name="user_accounts", related_query_name="user_account", on_delete=models.CASCADE) class Meta: - unique_together = ("user", "account") + unique_together = ("user", "customer") @python_2_unicode_compatible diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index f1fc809ff..ca96df89c 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -386,7 +386,7 @@ def test_purge_connected(self, RetrieveMock): stripe_account=account, stripe_id="cus_xxxxxxxxxxxxxxx", ) - UserAccount.objects.create(user=self.user, account=account, customer=customer) + UserAccount.objects.create(user=self.user, customer=customer) customers.purge(customer) self.assertTrue(RetrieveMock().delete.called) self.assertIsNone(Customer.objects.get(stripe_id=customer.stripe_id).user) @@ -480,12 +480,13 @@ def setUp(self): ) def test_get_customer_for_user_with_stripe_account(self): - expected = Customer.objects.create(stripe_id="x", user=self.user) + expected = Customer.objects.create( + stripe_id="x", + stripe_account=self.account) UserAccount.objects.create(user=self.user, - customer=expected, - account=self.account) + customer=expected) actual = customers.get_customer_for_user( - self.user, stripe_account=self.account.stripe_id) + self.user, stripe_account=self.account) self.assertEquals(expected, actual) @patch("pinax.stripe.actions.customers.sync_customer") @@ -506,14 +507,14 @@ def test_customer_create_with_connect(self, CreateMock, SyncMock): @patch("stripe.Customer.retrieve") @patch("pinax.stripe.actions.customers.sync_customer") @patch("stripe.Customer.create") - def test_customer_create_with_connect_stale_user_account(self, CreateMock, SyncMock, RetrieveMock): + def test_customer_create_with_connect_and_stale_user_account(self, CreateMock, SyncMock, RetrieveMock): CreateMock.return_value = dict(id="cus_XXXXX") RetrieveMock.side_effect = stripe.error.InvalidRequestError( message="Not Found", param="stripe_id" ) - ua = UserAccount.objects.create(user=self.user, - account=self.account, - customer=Customer.objects.create(stripe_id="cus_Z")) + ua = UserAccount.objects.create( + user=self.user, + customer=Customer.objects.create(stripe_id="cus_Z", stripe_account=self.account)) customer = customers.create(self.user, stripe_account=self.account) self.assertIsNone(customer.user) self.assertEqual(customer.stripe_id, "cus_XXXXX") @@ -528,10 +529,10 @@ def test_customer_create_with_connect_stale_user_account(self, CreateMock, SyncM class EventsTests(TestCase): - def setUp(self): - self.account = Account.objects.create( - stripe_id="acc_XXX" - ) + @classmethod + def setUpClass(cls): + super(EventsTests, cls).setUpClass() + cls.account = Account.objects.create(stripe_id="acc_XXX") def test_dupe_event_exists(self): Event.objects.create(stripe_id="evt_003", kind="foo", livemode=True, webhook_message="{}", api_version="", request="", pending_webhooks=0) From 9336338d3f71b9205bcc98e54bcbea5aee286498 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 19 Oct 2017 16:44:15 +0200 Subject: [PATCH 036/153] Migration --- .../0016_remove-user-account-account.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 pinax/stripe/migrations/0016_remove-user-account-account.py diff --git a/pinax/stripe/migrations/0016_remove-user-account-account.py b/pinax/stripe/migrations/0016_remove-user-account-account.py new file mode 100644 index 000000000..6e4d2449b --- /dev/null +++ b/pinax/stripe/migrations/0016_remove-user-account-account.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-19 14:38 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('pinax_stripe', '0015_account-as-foreign-key'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='useraccount', + unique_together=set([('user', 'customer')]), + ), + migrations.RemoveField( + model_name='useraccount', + name='account', + ), + ] From cfd7a690d7bedb4cb2a803937b9a41ca6e55361e Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 19 Oct 2017 17:47:27 +0200 Subject: [PATCH 037/153] Add additional tests --- pinax/stripe/tests/test_actions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index ca96df89c..ead4a39b0 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -489,6 +489,11 @@ def test_get_customer_for_user_with_stripe_account(self): self.user, stripe_account=self.account) self.assertEquals(expected, actual) + def test_get_customer_for_user_with_stripe_account_and_legacy_customer(self): + Customer.objects.create(user=self.user, stripe_id="x") + self.assertIsNone(customers.get_customer_for_user( + self.user, stripe_account=self.account)) + @patch("pinax.stripe.actions.customers.sync_customer") @patch("stripe.Customer.create") def test_customer_create_with_connect(self, CreateMock, SyncMock): @@ -526,6 +531,7 @@ def test_customer_create_with_connect_and_stale_user_account(self, CreateMock, S self.assertIsNone(kwargs["trial_end"]) self.assertTrue(SyncMock.called) self.assertEqual(self.user.user_accounts.get(), ua) + self.assertEqual(ua.customer, customer) class EventsTests(TestCase): From 98b1126b183089c07be88cfbcc1dab24f1c18a89 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 19 Oct 2017 17:48:29 +0200 Subject: [PATCH 038/153] Log what is happening around customer creation --- pinax/stripe/actions/customers.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index d648a6bd9..d450e9e6f 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -1,3 +1,5 @@ +import logging + from django.utils import timezone from django.utils.encoding import smart_str @@ -7,6 +9,8 @@ from .. import hooks, models, utils from ..conf import settings +logger = logging.getLogger(__name__) + def can_charge(customer): """ @@ -63,12 +67,13 @@ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_ST try: cus = user.customers.get(user_account__customer__stripe_account=stripe_account) except models.Customer.DoesNotExist: + logger.debug("customer not found for user %s, and account %s", user, stripe_account) cus = None else: try: stripe.Customer.retrieve(cus.stripe_id) except stripe.error.InvalidRequestError: - pass + logger.debug("customer found but failed to retrieve for user %s, and account %s", user, stripe_account) else: return cus @@ -88,6 +93,8 @@ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_ST cus = models.Customer.objects.create(stripe_id=stripe_customer["id"], stripe_account=stripe_account) models.UserAccount.objects.create(user=user, customer=cus) else: + logger.debug("Update local customer %s with new remote customer %s for user %s, and account %s", + cus.stripe_id, stripe_customer["id"], user, stripe_account) cus.stripe_id = stripe_customer["id"] # sync_customer() will call cus.save() sync_customer(cus, stripe_customer) if plan and charge_immediately: From d3b55dd6e0cbf96dc290e9931b1a26f064281914 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Fri, 20 Oct 2017 09:16:00 +0200 Subject: [PATCH 039/153] Bring back UserAccount.account --- pinax/stripe/actions/customers.py | 4 +-- .../migrations/0017_user-account-account.py | 34 +++++++++++++++++++ pinax/stripe/models.py | 22 +++++++----- pinax/stripe/tests/test_actions.py | 7 ++-- 4 files changed, 53 insertions(+), 14 deletions(-) create mode 100644 pinax/stripe/migrations/0017_user-account-account.py diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index d450e9e6f..fc916fcf0 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -65,7 +65,7 @@ def _create_without_account(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None): try: - cus = user.customers.get(user_account__customer__stripe_account=stripe_account) + cus = user.customers.get(user_account__account=stripe_account) except models.Customer.DoesNotExist: logger.debug("customer not found for user %s, and account %s", user, stripe_account) cus = None @@ -91,7 +91,7 @@ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_ST if cus is None: cus = models.Customer.objects.create(stripe_id=stripe_customer["id"], stripe_account=stripe_account) - models.UserAccount.objects.create(user=user, customer=cus) + models.UserAccount.objects.create(user=user, account=stripe_account, customer=cus) else: logger.debug("Update local customer %s with new remote customer %s for user %s, and account %s", cus.stripe_id, stripe_customer["id"], user, stripe_account) diff --git a/pinax/stripe/migrations/0017_user-account-account.py b/pinax/stripe/migrations/0017_user-account-account.py new file mode 100644 index 000000000..357e189de --- /dev/null +++ b/pinax/stripe/migrations/0017_user-account-account.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# generated by django 1.11.6 on 2017-10-20 07:03 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +def delete_user_accounts(apps, schema_editor): + UserAccount = apps.get_model("pinax_stripe", "UserAccount") + UserAccount.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('pinax_stripe', '0016_remove-user-account-account'), + ] + + operations = [ + migrations.RunPython(delete_user_accounts, reverse_code=migrations.RunPython.noop), + migrations.AddField( + model_name='useraccount', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to='pinax_stripe.Account'), + preserve_default=False, + ), + migrations.AlterUniqueTogether( + name='useraccount', + unique_together=set([('user', 'account')]), + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index a7b54cb29..2b85c4c52 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -181,13 +181,17 @@ class UserAccount(models.Model): related_name="user_accounts", related_query_name="user_account", on_delete=models.CASCADE) + account = models.ForeignKey("pinax_stripe.Account", + related_name="user_accounts", + related_query_name="user_account", + on_delete=models.CASCADE) customer = models.ForeignKey("pinax_stripe.Customer", related_name="user_accounts", related_query_name="user_account", on_delete=models.CASCADE) class Meta: - unique_together = ("user", "customer") + unique_together = ("user", "account") @python_2_unicode_compatible @@ -207,10 +211,10 @@ class Customer(AccountRelatedStripeObject): @cached_property def stripe_customer(self): - stripe_account = getattr(self.stripe_account, "stripe_id", None) + stripe_account_id = getattr(self.stripe_account, "stripe_id", None) return stripe.Customer.retrieve( self.stripe_id, - stripe_account=stripe_account + stripe_account=stripe_account_id ) def __str__(self): @@ -351,12 +355,12 @@ def status(self): @property def stripe_invoice(self): try: - stripe_account = getattr(self.customer.stripe_account, "stripe_id", None) + stripe_account_id = getattr(self.customer.stripe_account, "stripe_id", None) except ObjectDoesNotExist: - stripe_account = None + stripe_account_id = None return stripe.Invoice.retrieve( self.stripe_id, - stripe_account=stripe_account + stripe_account=stripe_account_id ) @@ -418,12 +422,12 @@ class Charge(StripeObject): @property def stripe_charge(self): if self.customer is not None: - stripe_account = getattr(self.customer.stripe_account, "stripe_id", None) + stripe_account_id = getattr(self.customer.stripe_account, "stripe_id", None) else: - stripe_account = None + stripe_account_id = None return stripe.Charge.retrieve( self.stripe_id, - stripe_account=stripe_account, + stripe_account=stripe_account_id, expand=["balance_transaction"] ) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index ead4a39b0..2b86d4e8b 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -227,6 +227,7 @@ def setUp(self): interval_count=1, name="Pro" ) + self.account = Account.objects.create(stripe_id="acc_XXX") def test_get_customer_for_user(self): expected = Customer.objects.create(stripe_id="x", user=self.user) @@ -386,7 +387,7 @@ def test_purge_connected(self, RetrieveMock): stripe_account=account, stripe_id="cus_xxxxxxxxxxxxxxx", ) - UserAccount.objects.create(user=self.user, customer=customer) + UserAccount.objects.create(user=self.user, account=self.account, customer=customer) customers.purge(customer) self.assertTrue(RetrieveMock().delete.called) self.assertIsNone(Customer.objects.get(stripe_id=customer.stripe_id).user) @@ -483,8 +484,7 @@ def test_get_customer_for_user_with_stripe_account(self): expected = Customer.objects.create( stripe_id="x", stripe_account=self.account) - UserAccount.objects.create(user=self.user, - customer=expected) + UserAccount.objects.create(user=self.user, account=self.account, customer=expected) actual = customers.get_customer_for_user( self.user, stripe_account=self.account) self.assertEquals(expected, actual) @@ -519,6 +519,7 @@ def test_customer_create_with_connect_and_stale_user_account(self, CreateMock, S ) ua = UserAccount.objects.create( user=self.user, + account=self.account, customer=Customer.objects.create(stripe_id="cus_Z", stripe_account=self.account)) customer = customers.create(self.user, stripe_account=self.account) self.assertIsNone(customer.user) From 68b5fbc5a159a97e2ac72006e06d0c0f1b8d5306 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Fri, 20 Oct 2017 09:17:42 +0200 Subject: [PATCH 040/153] Our fork doesn't need templates --- .github/ISSUE_TEMPLATE.md | 18 ------------------ .github/PULL_REQUEST_TEMPLATE.md | 14 -------------- 2 files changed, 32 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 4db6386b2..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,18 +0,0 @@ -#### Issue Summary - -A short summary of the issue - - ---- - -_If this is a bug instead of a question or feature request, please fill out the sections below._ - ---- - -#### Steps to Reproduce - -It's essential that you provide enough information for someone else to replicate the problem you're seeing. Simply describing something that's broken on your current project is not enough! - -#### What were you expecting to happen? - -#### What actually happened? diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 3ffa6b722..000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,14 +0,0 @@ -#### What's this PR do? - -#### Any background context you want to provide? - -#### What ticket or issue # does this fix? - -Closes #[issue number] - -#### Definition of Done (check if considered and/or addressed): - -- [ ] Are all backwards incompatible changes documented in this PR? -- [ ] Have all new dependencies been documented in this PR? -- [ ] Has the appropriate documentation been updated (if applicable)? -- [ ] Have you written tests to prove this change works (if applicable)? From 18394fe574f3df1e0bc52e998135bcf25917ede5 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Fri, 20 Oct 2017 09:25:36 +0200 Subject: [PATCH 041/153] fix Isort --- pinax/stripe/forms.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pinax/stripe/forms.py b/pinax/stripe/forms.py index 3197ec064..09f3eb28d 100644 --- a/pinax/stripe/forms.py +++ b/pinax/stripe/forms.py @@ -5,7 +5,6 @@ from django.utils.translation import ugettext_lazy as _ import stripe - from ipware.ip import get_ip, get_real_ip from .actions import accounts From 2bdd84f07784d3f08581bb75a52142cdb2dda7b5 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Fri, 20 Oct 2017 09:46:20 +0200 Subject: [PATCH 042/153] for consistency --- pinax/stripe/migrations/0017_user-account-account.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pinax/stripe/migrations/0017_user-account-account.py b/pinax/stripe/migrations/0017_user-account-account.py index 357e189de..1142173f9 100644 --- a/pinax/stripe/migrations/0017_user-account-account.py +++ b/pinax/stripe/migrations/0017_user-account-account.py @@ -10,6 +10,8 @@ def delete_user_accounts(apps, schema_editor): UserAccount = apps.get_model("pinax_stripe", "UserAccount") UserAccount.objects.all().delete() + Customer = apps.get_model("pinax_stripe", "Customer") + Customer.objects.all().delete() class Migration(migrations.Migration): From 4ba29ad23aedc2303f0f10066a39ccff83bce732 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 20 Oct 2017 15:41:08 +0200 Subject: [PATCH 043/153] fixup! remove UserAccount.account --- pinax/stripe/actions/customers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index fc916fcf0..cbf9b75bc 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -115,7 +115,7 @@ def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_imme charge_immediately: whether or not the user should be immediately charged for the subscription quantity: the quantity (multiplier) of the subscription - stripe_account: An account objects. If given, the Customer and User relation will be established for you through UserAccount model. + stripe_account: An account object. If given, the Customer and User relation will be established for you through the UserAccount model. Because a single User might have several Customers, one per Account. Returns: From b222b585041709c8963925ac9ec8d338d0e5a1a9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 20 Oct 2017 16:46:13 +0200 Subject: [PATCH 044/153] fixup! Enhance admin with connect search --- pinax/stripe/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 654af4c31..8a906cffd 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -188,7 +188,7 @@ def queryset(self, request, queryset): search_fields=[ "stripe_id", "customer__stripe_id", - "validated_message" + "validated_message", "=stripe_account__stripe_id", ] + customer_search_fields(), ) From 5148d11e781973ce8f760c7a67230a6a170f91aa Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 20 Oct 2017 18:08:56 +0200 Subject: [PATCH 045/153] fixup! Adjust CircleCI config: only build dj111/master (dj20) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 917cf9e0e..f624d5b24 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -141,4 +141,4 @@ workflows: jobs: - lint - py36dj111 - - py36djmaster + - py36dj20 From 1624ced8222b3dd427c95e15eee58d7e021ecbe8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 20 Oct 2017 18:17:44 +0200 Subject: [PATCH 046/153] fixup! Adjust CircleCI config: only build dj111/master (dj20) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a75bb1151..a2f85b22a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -135,4 +135,4 @@ workflows: jobs: - lint - py36dj111 - - py36djmaster + - py36dj20 From deb1f748dac5e0c0712186e39a1831256c0c0bf8 Mon Sep 17 00:00:00 2001 From: encc Date: Tue, 24 Oct 2017 12:16:38 +0200 Subject: [PATCH 047/153] Add discount objects (#19) * Add discount objects * Add coupon webhook handlers * Change Coupon id unique-togetherness --- pinax/stripe/actions/coupons.py | 14 +++ pinax/stripe/actions/subscriptions.py | 15 ++- pinax/stripe/migrations/0018_add_discounts.py | 58 +++++++++ pinax/stripe/models.py | 64 +++++++++- pinax/stripe/tests/test_actions.py | 112 +++++++++++++++++- pinax/stripe/tests/test_models.py | 32 +++++ pinax/stripe/tests/test_webhooks.py | 63 ++++++++++ pinax/stripe/webhooks.py | 10 ++ 8 files changed, 362 insertions(+), 6 deletions(-) create mode 100644 pinax/stripe/migrations/0018_add_discounts.py diff --git a/pinax/stripe/actions/coupons.py b/pinax/stripe/actions/coupons.py index ed3160c74..5a46b328d 100644 --- a/pinax/stripe/actions/coupons.py +++ b/pinax/stripe/actions/coupons.py @@ -6,6 +6,8 @@ def sync_coupons(): """ Synchronizes all coupons from the Stripe API + + TODO: Support connect / stripe_account param """ try: coupons = stripe.Coupon.auto_paging_iter() @@ -13,6 +15,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 +34,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 0c5c22008..67d04d8fd 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): @@ -161,6 +162,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 @@ -171,7 +184,7 @@ def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, ch Args: subscription: the subscription to update plan: optionally, the plan to change the subscription to - quantity: optionally, the quantiy of the subscription to change + quantity: optionally, the quantity of the subscription to change prorate: optionally, if the subscription should be prorated or not coupon: optionally, a coupon to apply to the subscription charge_immediately: optionally, whether or not to charge immediately diff --git a/pinax/stripe/migrations/0018_add_discounts.py b/pinax/stripe/migrations/0018_add_discounts.py new file mode 100644 index 000000000..e6b3844c3 --- /dev/null +++ b/pinax/stripe/migrations/0018_add_discounts.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-24 09:14 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0017_user-account-account'), + ] + + 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'), + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 2b85c4c52..caab233b5 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -67,11 +67,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) @@ -89,6 +107,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): @@ -272,6 +305,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(StripeObject): STATUS_CURRENT = ["trialing", "active"] @@ -296,7 +351,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 2b86d4e8b..ed08dcf40 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, @@ -211,6 +214,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): @@ -984,6 +1005,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): @@ -1379,6 +1432,33 @@ def test_sync_subscription_from_stripe_data(self): } subscriptions.sync_subscription_from_stripe_data(self.customer, subscription) self.assertEquals(Subscription.objects.get(stripe_id=subscription["id"]).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")) @@ -1391,7 +1471,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": { }, @@ -1417,11 +1520,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 ea443c0eb..860b24c4e 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -15,6 +15,7 @@ Charge, Coupon, Customer, + Discount, Event, EventProcessingException, Invoice, @@ -72,6 +73,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") @@ -93,6 +98,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='')") @@ -111,6 +135,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 aeb569e66..606ba2c2a 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -26,6 +26,9 @@ AccountApplicationDeauthorizeWebhook, AccountUpdatedWebhook, ChargeCapturedWebhook, + CouponCreatedWebhook, + CouponDeletedWebhook, + CouponUpdatedWebhook, CustomerCreatedWebhook, CustomerSourceCreatedWebhook, CustomerSourceDeletedWebhook, @@ -244,6 +247,66 @@ 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") diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 02352138c..54f5d8a93 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -9,6 +9,7 @@ from .actions import ( accounts, charges, + coupons, customers, exceptions, invoices, @@ -262,16 +263,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 480409e95e03c44729154a6cbd690eb371fbf72b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 20 Oct 2017 17:22:11 +0200 Subject: [PATCH 048/153] tox: add postgres factor to use PostgreSQL for tests --- pinax/stripe/tests/settings.py | 7 ++++++- tox.ini | 9 ++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pinax/stripe/tests/settings.py b/pinax/stripe/tests/settings.py index 731519f0a..9ca1347c5 100644 --- a/pinax/stripe/tests/settings.py +++ b/pinax/stripe/tests/settings.py @@ -1,3 +1,5 @@ +import os + import django old = django.VERSION < (1, 8) @@ -7,7 +9,10 @@ TIME_ZONE = "UTC" DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", + "ENGINE": os.environ.get("PINAX_STRIPE_DATABASE_ENGINE", "django.db.backends.sqlite3"), + "HOST": os.environ.get("PINAX_STRIPE_DATABASE_HOST", "127.0.0.1"), + "NAME": os.environ.get("PINAX_STRIPE_DATABASE_NAME", "pinax_stripe"), + "USER": os.environ.get("PINAX_STRIPE_DATABASE_USER", ""), } } MIDDLEWARE = [ # from 2.0 onwards, only MIDDLEWARE is used diff --git a/tox.ini b/tox.ini index 18a7c835b..481d05317 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,12 @@ envlist = py36-dj{111,20}{,-pytest} [testenv] -passenv = CI CIRCLECI CIRCLE_* +passenv = + CI CIRCLECI CIRCLE_* + PINAX_STRIPE_DATABASE_ENGINE + PINAX_STRIPE_DATABASE_HOST + PINAX_STRIPE_DATABASE_NAME + PINAX_STRIPE_DATABASE_USER deps = coverage codecov @@ -35,12 +40,14 @@ deps = dj111: Django>=1.11a1,<2.0 dj20: Django<2.1 master: https://github.com/django/django/tarball/master + postgres: psycopg2 extras = pytest: pytest usedevelop = True setenv = DJANGO_SETTINGS_MODULE=pinax.stripe.tests.settings pytest: _STRIPE_TEST_RUNNER=-m pytest + postgres: PINAX_STRIPE_DATABASE_ENGINE={env:PINAX_STRIPE_DATABASE_ENGINE:django.db.backends.postgresql_psycopg2} commands = coverage run {env:_STRIPE_TEST_RUNNER:setup.py test} {posargs} coverage report -m --skip-covered From 441d17e1939d76e4219315c1c04c57317eada0a7 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 20 Oct 2017 17:30:52 +0200 Subject: [PATCH 049/153] CircleCI: add py36dj20psql --- .circleci/config.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index a2f85b22a..6bebaba70 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -128,6 +128,16 @@ jobs: - image: circleci/python:3.6 environment: TOXENV=py36-dj20 + py36dj20psql: + <<: *common + docker: + - image: circleci/python:3.6 + environment: + - TOXENV=py36-dj20-postgres + - PINAX_STRIPE_DATABASE_HOST=127.0.0.1 + - PINAX_STRIPE_DATABASE_USER=root + - PINAX_STRIPE_DATABASE_NAME=circle_test + - image: circleci/postgres:9.6-alpine workflows: version: 2 @@ -136,3 +146,4 @@ workflows: - lint - py36dj111 - py36dj20 + - py36dj20psql From e943f195498b52ef3b97e60b3048d26570734204 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 23 Oct 2017 19:34:28 +0200 Subject: [PATCH 050/153] tests: fix/improve test_sync_invoice_items_updating --- pinax/stripe/tests/test_actions.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 2b86d4e8b..583702ac8 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -2315,11 +2315,12 @@ def test_sync_invoice_items_updating(self, RetrieveSubscriptionMock): "type": "subscription" }] invoices.sync_invoice_items(invoice, items) - self.assertTrue(invoice.items.all().count(), 2) + self.assertEquals(invoice.items.count(), 2) + items[1].update({"description": "This is your second subscription"}) invoices.sync_invoice_items(invoice, items) - self.assertTrue(invoice.items.all().count(), 2) - self.assertEquals(invoice.items.all()[1].description, "This is your second subscription") + self.assertEquals(invoice.items.count(), 2) + self.assertEquals(invoice.items.get(stripe_id="sub_7Q4BX0HMfqTpN9").description, "This is your second subscription") class InvoiceSyncsTests(TestCase): From 57ca13abc946e829a8a9238d58a57e8400fb2c09 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 24 Oct 2017 17:14:12 +0200 Subject: [PATCH 051/153] Revert "Merge branch 'account-publishable_key' into next" This reverts commit f0172f1cec23cb45c73f2436ebcad32fbe622239, reversing changes made to 3626d127756532c0fcee264fd0fb890c1b038655. --- .../0011_account_publishable_key.py | 20 ------------------- pinax/stripe/models.py | 2 -- 2 files changed, 22 deletions(-) delete mode 100644 pinax/stripe/migrations/0011_account_publishable_key.py diff --git a/pinax/stripe/migrations/0011_account_publishable_key.py b/pinax/stripe/migrations/0011_account_publishable_key.py deleted file mode 100644 index a8a0a4e1a..000000000 --- a/pinax/stripe/migrations/0011_account_publishable_key.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-24 09:13 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0010_connect'), - ] - - operations = [ - migrations.AddField( - model_name='account', - name='stripe_publishable_key', - field=models.CharField(blank=True, max_length=100, null=True), - ), - ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 3b5161a83..caab233b5 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -537,8 +537,6 @@ class Account(StripeObject): metadata = JSONField(null=True) - stripe_publishable_key = models.CharField(null=True, blank=True, max_length=100) - product_description = models.TextField(null=True, blank=True) statement_descriptor = models.TextField(null=True, blank=True) support_email = models.TextField(null=True, blank=True) From 424e34c3127963b5078a100d0fc00ae2f749db1c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 24 Oct 2017 18:28:30 +0200 Subject: [PATCH 052/153] fixup! CircleCI: test merge commit --- .circleci/config.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6bebaba70..c24537cf7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,11 +10,15 @@ common: &common set -x PR_NUMBER=${CI_PULL_REQUEST//*pull\//} if [ -n "$PR_NUMBER" ]; then - if ! git pull --ff-only origin "refs/pull/$PR_NUMBER/merge"; then - echo - echo -e "\033[0;31mERROR: Failed to merge your branch with the latest master." - echo -e "Please manually merge master into your branch, and push the changes to GitHub.\033[0m" - exit 1 + merge_ref="refs/pull/$PR_NUMBER/merge" + if ! git ls-remote -q --exit-code origin "$merge_ref"; then + echo -e "\033[0;31mWarning: remote merge ref for $PR_NUMBER not found on origin: $merge_ref." + echo -e "Not checking out merge commit.\033[0m" + elif ! git pull --ff-only origin "refs/pull/$PR_NUMBER/merge"; then + echo + echo -e "\033[0;31mERROR: Failed to merge your branch with the latest master." + echo -e "Please manually merge master into your branch, and push the changes to GitHub.\033[0m" + exit 1 fi fi - restore_cache: From 117cbbb54a7ebe1ea118455180a301c1b6c424ce Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 25 Oct 2017 17:39:03 +0200 Subject: [PATCH 053/153] Add merge migration --- .../migrations/0019_merge_20171025_1519.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pinax/stripe/migrations/0019_merge_20171025_1519.py diff --git a/pinax/stripe/migrations/0019_merge_20171025_1519.py b/pinax/stripe/migrations/0019_merge_20171025_1519.py new file mode 100644 index 000000000..96ec309fb --- /dev/null +++ b/pinax/stripe/migrations/0019_merge_20171025_1519.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-25 15:19 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0012_merge_20171025_1443'), + ('pinax_stripe', '0018_add_discounts'), + ] + + operations = [ + ] From 08de70787bb052613b7f69abdfcb489dde5f75c9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 25 Oct 2017 17:40:09 +0200 Subject: [PATCH 054/153] CircleCI: only run lint, py36dj111psql and py36dj20psql --- .circleci/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 571595037..bae5d5202 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -150,6 +150,5 @@ workflows: test: jobs: - lint - - py36dj111 - - py36dj20 + - py36dj111psql - py36dj20psql From 82217ddc0d9c3eb9b4e366949d02286fc1c9d98b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 25 Oct 2017 17:41:32 +0200 Subject: [PATCH 055/153] fixup! CircleCI: only run lint, py36dj111psql and py36dj20psql --- .circleci/config.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index bae5d5202..a6e4ae75f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -128,6 +128,16 @@ jobs: - image: circleci/python:3.6 environment: TOXENV=py36-dj111 + py36dj11psql: + <<: *common + docker: + - image: circleci/python:3.6 + environment: + - TOXENV=py36-dj11-postgres + - PINAX_STRIPE_DATABASE_HOST=127.0.0.1 + - PINAX_STRIPE_DATABASE_USER=root + - PINAX_STRIPE_DATABASE_NAME=circle_test + - image: circleci/postgres:9.6-alpine py36dj20: <<: *common docker: From 89ae284b4eda73f85d7615863b9a58ff9eb08fee Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 25 Oct 2017 17:42:26 +0200 Subject: [PATCH 056/153] fixup! fixup! CircleCI: only run lint, py36dj111psql and py36dj20psql --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a6e4ae75f..33ee3b58f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -128,7 +128,7 @@ jobs: - image: circleci/python:3.6 environment: TOXENV=py36-dj111 - py36dj11psql: + py36dj111psql: <<: *common docker: - image: circleci/python:3.6 From c5e148b74ea443ca43879cc38a8ca3e36d116f19 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 25 Oct 2017 17:44:41 +0200 Subject: [PATCH 057/153] fixup! fixup! fixup! CircleCI: only run lint, py36dj111psql and py36dj20psql --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 33ee3b58f..e5c660506 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -133,7 +133,7 @@ jobs: docker: - image: circleci/python:3.6 environment: - - TOXENV=py36-dj11-postgres + - TOXENV=py36-dj111-postgres - PINAX_STRIPE_DATABASE_HOST=127.0.0.1 - PINAX_STRIPE_DATABASE_USER=root - PINAX_STRIPE_DATABASE_NAME=circle_test From da3fc394dae0f392d42da97c840df8f935f1a7cf Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 25 Oct 2017 18:15:46 +0200 Subject: [PATCH 058/153] Bring back account.stripe_publishable_key --- .../0020_account-publishablke-key.py | 20 +++++++++++++++++++ pinax/stripe/models.py | 2 ++ 2 files changed, 22 insertions(+) create mode 100644 pinax/stripe/migrations/0020_account-publishablke-key.py diff --git a/pinax/stripe/migrations/0020_account-publishablke-key.py b/pinax/stripe/migrations/0020_account-publishablke-key.py new file mode 100644 index 000000000..5b551f995 --- /dev/null +++ b/pinax/stripe/migrations/0020_account-publishablke-key.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-25 16:15 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0019_merge_20171025_1519'), + ] + + operations = [ + migrations.AddField( + model_name='account', + name='stripe_publishable_key', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 1f9fad38a..9f45ffa9a 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -537,6 +537,8 @@ class Account(StripeObject): metadata = JSONField(null=True, blank=True) + stripe_publishable_key = models.CharField(null=True, blank=True, max_length=100) + product_description = models.TextField(null=True, blank=True) statement_descriptor = models.TextField(null=True, blank=True) support_email = models.TextField(null=True, blank=True) From 56663248ca275bdc042ec9321714429d1890a0e4 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 25 Oct 2017 19:00:30 +0200 Subject: [PATCH 059/153] Handle account.application.deauthorized event --- pinax/stripe/actions/accounts.py | 5 ++++ .../migrations/0021_account-authorized.py | 20 ++++++++++++++ pinax/stripe/models.py | 4 ++- pinax/stripe/tests/test_actions.py | 7 +++++ pinax/stripe/tests/test_models.py | 7 ++--- pinax/stripe/tests/test_webhooks.py | 26 ++++++++++++++----- pinax/stripe/webhooks.py | 3 +++ 7 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 pinax/stripe/migrations/0021_account-authorized.py diff --git a/pinax/stripe/actions/accounts.py b/pinax/stripe/actions/accounts.py index eef8215d3..f0c0fce7b 100644 --- a/pinax/stripe/actions/accounts.py +++ b/pinax/stripe/actions/accounts.py @@ -218,3 +218,8 @@ def delete(account): """ account.stripe_account.delete() account.delete() + + +def deauthorize(account): + account.authorized = False + account.save() diff --git a/pinax/stripe/migrations/0021_account-authorized.py b/pinax/stripe/migrations/0021_account-authorized.py new file mode 100644 index 000000000..a3baa5160 --- /dev/null +++ b/pinax/stripe/migrations/0021_account-authorized.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-25 16:52 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0020_account-publishablke-key'), + ] + + operations = [ + migrations.AddField( + model_name='account', + name='authorized', + field=models.BooleanField(default=True), + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 9f45ffa9a..796ff114f 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -563,6 +563,7 @@ class Account(StripeObject): verification_due_by = models.DateTimeField(null=True, blank=True) verification_timestamp = models.DateTimeField(null=True, blank=True) verification_fields_needed = JSONField(null=True, blank=True) + authorized = models.BooleanField(default=True) @property def stripe_account(self): @@ -572,11 +573,12 @@ def __str__(self): return "{} - {}".format(self.display_name, self.stripe_id) def __repr__(self): - return "Account(pk={!r}, display_name={!r}, type={!r}, stripe_id={!r})".format( + return "Account(pk={!r}, display_name={!r}, type={!r}, stripe_id={!r}, authorized={!r})".format( self.pk, str(self.display_name), self.type, str(self.stripe_id), + self.authorized, ) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 5c63edb78..fbb13ecf2 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -3156,6 +3156,13 @@ def test_sync_not_custom_account(self): self.assertNotEqual(account.type, "custom") self.assert_common_attributes(account) + def test_deauthorize_account(self): + account = accounts.sync_account_from_stripe_data( + self.not_custom_account_data + ) + accounts.deauthorize(account) + self.assertFalse(account.authorized) + class BankAccountsSyncTestCase(TestCase): diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index 860b24c4e..88eedceb5 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -163,13 +163,14 @@ def test_subscription_delete(self): def test_account_str_and_repr(self): a = Account() self.assertEquals(str(a), " - ") - self.assertEquals(repr(a), "Account(pk=None, display_name='', type=None, stripe_id='')") + self.assertEquals(repr(a), "Account(pk=None, display_name='', type=None, stripe_id='', authorized=True)") a.stripe_id = "acct_X" self.assertEquals(str(a), " - acct_X") - self.assertEquals(repr(a), "Account(pk=None, display_name='', type=None, stripe_id='acct_X')") + self.assertEquals(repr(a), "Account(pk=None, display_name='', type=None, stripe_id='acct_X', authorized=True)") a.display_name = "Display name" + a.authorized = False self.assertEquals(str(a), "Display name - acct_X") - self.assertEquals(repr(a), "Account(pk=None, display_name='Display name', type=None, stripe_id='acct_X')") + self.assertEquals(repr(a), "Account(pk=None, display_name='Display name', type=None, stripe_id='acct_X', authorized=False)") class StripeObjectTests(TestCase): diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index da80587f5..3c91a5254 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -25,6 +25,7 @@ ) from ..webhooks import ( AccountApplicationDeauthorizeWebhook, + AccountExternalAccountCreatedWebhook, AccountUpdatedWebhook, ChargeCapturedWebhook, CouponCreatedWebhook, @@ -186,10 +187,10 @@ def signal_handler(sender, *args, **kwargs): @patch("pinax.stripe.webhooks.Webhook.process_webhook") def test_process_exception_is_logged(self, ProcessWebhookMock, ValidateMock, LinkMock): # note: we choose an event type for which we do no processing - event = Event.objects.create(kind="account.application.deauthorized", webhook_message={}, valid=True, processed=False) + event = Event.objects.create(kind="account.external_account.created", webhook_message={}, valid=True, processed=False) ProcessWebhookMock.side_effect = stripe.StripeError("Message", "error") with self.assertRaises(stripe.StripeError): - AccountApplicationDeauthorizeWebhook(event).process() + AccountExternalAccountCreatedWebhook(event).process() self.assertTrue(EventProcessingException.objects.filter(event=event).exists()) @patch("pinax.stripe.actions.customers.link_customer") @@ -197,18 +198,18 @@ def test_process_exception_is_logged(self, ProcessWebhookMock, ValidateMock, Lin @patch("pinax.stripe.webhooks.Webhook.process_webhook") def test_process_exception_is_logged_non_stripeerror(self, ProcessWebhookMock, ValidateMock, LinkMock): # note: we choose an event type for which we do no processing - event = Event.objects.create(kind="account.application.deauthorized", webhook_message={}, valid=True, processed=False) + event = Event.objects.create(kind="account.external_account.created", webhook_message={}, valid=True, processed=False) ProcessWebhookMock.side_effect = Exception("generic exception") with self.assertRaises(Exception): - AccountApplicationDeauthorizeWebhook(event).process() + AccountExternalAccountCreatedWebhook(event).process() self.assertTrue(EventProcessingException.objects.filter(event=event).exists()) @patch("pinax.stripe.actions.customers.link_customer") @patch("pinax.stripe.webhooks.Webhook.validate") def test_process_return_none(self, ValidateMock, LinkMock): # note: we choose an event type for which we do no processing - event = Event.objects.create(kind="account.application.deauthorized", webhook_message={}, valid=True, processed=False) - self.assertIsNone(AccountApplicationDeauthorizeWebhook(event).process()) + event = Event.objects.create(kind="account.external_account.created", webhook_message={}, valid=True, processed=False) + self.assertIsNone(AccountExternalAccountCreatedWebhook(event).process()) class ChargeWebhookTest(TestCase): @@ -595,3 +596,16 @@ def test_process_webhook(self, SyncMock, RetrieveMock): event.validated_message = dict(data=dict(object=dict(id=1))) AccountUpdatedWebhook(event).process_webhook() self.assertTrue(SyncMock.called) + + @patch("stripe.Account.retrieve") + @patch("pinax.stripe.actions.accounts.deauthorize") + def test_process_deauthorize(self, DeauthorizeMock, RetrieveMock): + event = Event.objects.create( + kind=AccountApplicationDeauthorizeWebhook.name, + webhook_message={}, + valid=True, + processed=False + ) + event.validated_message = dict(data=dict(object=dict(id=1))) + AccountApplicationDeauthorizeWebhook(event).process_webhook() + self.assertTrue(DeauthorizeMock.called) diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 54d978e10..9582322f3 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -144,6 +144,9 @@ class AccountApplicationDeauthorizeWebhook(Webhook): name = "account.application.deauthorized" description = "Occurs whenever a user deauthorizes an application. Sent to the related application only." + def process_webhook(self): + accounts.deauthorize(stripe.Account.retrieve(self.event.message["data"]["object"]["id"])) + class AccountExternalAccountCreatedWebhook(Webhook): name = "account.external_account.created" From 41bfb6fb087706b6523c87243ccb4acbe90be1bd Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 26 Oct 2017 10:19:22 +0200 Subject: [PATCH 060/153] That's our model we need to update --- pinax/stripe/tests/test_webhooks.py | 10 +++++++--- pinax/stripe/webhooks.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index 3c91a5254..690b3f33c 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -584,6 +584,11 @@ def test_transfer_paid_updates_existing_record(self, TransferMock, EventMock): class AccountWebhookTest(TestCase): + @classmethod + def setUpClass(cls): + super(AccountWebhookTest, cls).setUpClass() + cls.account = Account.objects.create(stripe_id="acc_aa") + @patch("stripe.Account.retrieve") @patch("pinax.stripe.actions.accounts.sync_account_from_stripe_data") def test_process_webhook(self, SyncMock, RetrieveMock): @@ -597,15 +602,14 @@ def test_process_webhook(self, SyncMock, RetrieveMock): AccountUpdatedWebhook(event).process_webhook() self.assertTrue(SyncMock.called) - @patch("stripe.Account.retrieve") @patch("pinax.stripe.actions.accounts.deauthorize") - def test_process_deauthorize(self, DeauthorizeMock, RetrieveMock): + def test_process_deauthorize(self, DeauthorizeMock): event = Event.objects.create( kind=AccountApplicationDeauthorizeWebhook.name, webhook_message={}, valid=True, processed=False ) - event.validated_message = dict(data=dict(object=dict(id=1))) + event.validated_message = dict(data=dict(object=dict(id=self.account.stripe_id))) AccountApplicationDeauthorizeWebhook(event).process_webhook() self.assertTrue(DeauthorizeMock.called) diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 9582322f3..76618ab1c 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -145,7 +145,7 @@ class AccountApplicationDeauthorizeWebhook(Webhook): description = "Occurs whenever a user deauthorizes an application. Sent to the related application only." def process_webhook(self): - accounts.deauthorize(stripe.Account.retrieve(self.event.message["data"]["object"]["id"])) + accounts.deauthorize(models.Account.objects.get(stripe_id=self.event.message["data"]["object"]["id"])) class AccountExternalAccountCreatedWebhook(Webhook): From f8195ffc9b8cf134c0cb6eceb23f929e76b362f1 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 26 Oct 2017 11:00:59 +0200 Subject: [PATCH 061/153] Pass stripe account while subscribe --- pinax/stripe/actions/subscriptions.py | 5 ++++- pinax/stripe/tests/test_actions.py | 26 +++++++++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index ad73f1d60..7e31fe7d0 100644 --- a/pinax/stripe/actions/subscriptions.py +++ b/pinax/stripe/actions/subscriptions.py @@ -21,7 +21,7 @@ def cancel(subscription, at_period_end=True): sync_subscription_from_stripe_data(subscription.customer, sub) -def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=None, tax_percent=None): +def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=None, tax_percent=None, stripe_account=None): """ Creates a subscription for the given customer @@ -36,6 +36,7 @@ def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=No will be used coupon: if provided, a coupon to apply towards the subscription tax_percent: if provided, add percentage as tax + stripe_account: Account object. Returns: the data representing the subscription object that was created @@ -47,6 +48,8 @@ def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=No subscription_params["trial_end"] = datetime.datetime.utcnow() + datetime.timedelta(days=trial_days) if token: subscription_params["source"] = token + if stripe_account is not None: + subscription_params["stripe_account"] = stripe_account.stripe_id subscription_params["customer"] = customer.stripe_id subscription_params["plan"] = plan diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index fbb13ecf2..f31e3e164 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -791,16 +791,19 @@ def test_delete_card_object_not_card(self): class SubscriptionsTests(TestCase): - def setUp(self): - self.User = get_user_model() - self.user = self.User.objects.create_user( + @classmethod + def setUpClass(cls): + super(SubscriptionsTests, cls).setUpClass() + cls.User = get_user_model() + cls.user = cls.User.objects.create_user( username="patrick", email="paltman@example.com" ) - self.customer = Customer.objects.create( - user=self.user, + cls.customer = Customer.objects.create( + user=cls.user, stripe_id="cus_xxxxxxxxxxxxxxx" ) + cls.account = Account.objects.create(stripe_id="acct_xx") def test_has_active_subscription(self): plan = Plan.objects.create( @@ -958,6 +961,19 @@ def test_subscription_create_token(self, SubscriptionCreateMock, CustomerMock): _, kwargs = SubscriptionCreateMock.call_args self.assertEquals(kwargs["source"], "token") + @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") + @patch("stripe.Subscription.create") + def test_subscription_create_with_connect(self, SubscriptionCreateMock, SyncMock): + subscriptions.create(self.customer, "the-plan", stripe_account=self.account) + self.assertTrue(SyncMock.called) + SubscriptionCreateMock.assert_called_once_with( + coupon=None, + customer=self.customer.stripe_id, + plan="the-plan", + quantity=4, + tax_percent=None, + stripe_account=self.account.stripe_id) + def test_is_period_current(self): sub = Subscription(current_period_end=(timezone.now() + datetime.timedelta(days=2))) self.assertTrue(subscriptions.is_period_current(sub)) From 0df4b537edbc2757b1ead8ea695c16c0ed3753aa Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 26 Oct 2017 11:50:21 +0200 Subject: [PATCH 062/153] Explain which parameter we expect --- pinax/stripe/actions/charges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/actions/charges.py b/pinax/stripe/actions/charges.py index ba62c05a3..df1d6de7d 100644 --- a/pinax/stripe/actions/charges.py +++ b/pinax/stripe/actions/charges.py @@ -82,7 +82,7 @@ def create( destination_account: stripe_id of a connected account destination_amount: amount to transfer to the `destination_account` without creating an application fee application_fee: used with `destination_account` to add a fee destined for the platform account - on_behalf_of: Direct Charges to given account, used with stripe connect and connected accounts. + on_behalf_of: stripe_id of a connected account. Direct Charges to given account Returns: a pinax.stripe.models.Charge object From 3d4a8fe9956c07803377c4b0584feae7aeacb6e2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 26 Oct 2017 12:36:32 +0200 Subject: [PATCH 063/153] Update charges.py --- pinax/stripe/actions/charges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/actions/charges.py b/pinax/stripe/actions/charges.py index df1d6de7d..027d61b8d 100644 --- a/pinax/stripe/actions/charges.py +++ b/pinax/stripe/actions/charges.py @@ -82,7 +82,7 @@ def create( destination_account: stripe_id of a connected account destination_amount: amount to transfer to the `destination_account` without creating an application fee application_fee: used with `destination_account` to add a fee destined for the platform account - on_behalf_of: stripe_id of a connected account. Direct Charges to given account + on_behalf_of: stripe_id of a connected account. Creates direct Charges to this account. Returns: a pinax.stripe.models.Charge object From 49b6a22508b6c7d121ce05542169100b1ecddae3 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 26 Oct 2017 15:48:40 +0200 Subject: [PATCH 064/153] Make Subscriptions Account Related --- pinax/stripe/actions/subscriptions.py | 9 ++--- .../migrations/0022_sub-account-related.py | 21 ++++++++++ pinax/stripe/models.py | 2 +- pinax/stripe/tests/test_actions.py | 39 ++++++++++++++++--- pinax/stripe/webhooks.py | 2 +- 5 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 pinax/stripe/migrations/0022_sub-account-related.py diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index 7e31fe7d0..9e27887e2 100644 --- a/pinax/stripe/actions/subscriptions.py +++ b/pinax/stripe/actions/subscriptions.py @@ -21,7 +21,7 @@ def cancel(subscription, at_period_end=True): sync_subscription_from_stripe_data(subscription.customer, sub) -def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=None, tax_percent=None, stripe_account=None): +def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=None, tax_percent=None): """ Creates a subscription for the given customer @@ -36,7 +36,6 @@ def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=No will be used coupon: if provided, a coupon to apply towards the subscription tax_percent: if provided, add percentage as tax - stripe_account: Account object. Returns: the data representing the subscription object that was created @@ -48,9 +47,8 @@ def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=No subscription_params["trial_end"] = datetime.datetime.utcnow() + datetime.timedelta(days=trial_days) if token: subscription_params["source"] = token - if stripe_account is not None: - subscription_params["stripe_account"] = stripe_account.stripe_id + subscription_params["stripe_account"] = getattr(customer.stripe_account, "stripe_id", None) subscription_params["customer"] = customer.stripe_id subscription_params["plan"] = plan subscription_params["quantity"] = quantity @@ -158,7 +156,8 @@ def sync_subscription_from_stripe_data(customer, subscription): start=utils.convert_tstamp(subscription["start"]), status=subscription["status"], trial_start=utils.convert_tstamp(subscription["trial_start"]) if subscription["trial_start"] else None, - trial_end=utils.convert_tstamp(subscription["trial_end"]) if subscription["trial_end"] else None + trial_end=utils.convert_tstamp(subscription["trial_end"]) if subscription["trial_end"] else None, + stripe_account=customer.stripe_account, ) sub, created = models.Subscription.objects.get_or_create( stripe_id=subscription["id"], diff --git a/pinax/stripe/migrations/0022_sub-account-related.py b/pinax/stripe/migrations/0022_sub-account-related.py new file mode 100644 index 000000000..1ec11e145 --- /dev/null +++ b/pinax/stripe/migrations/0022_sub-account-related.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-26 12:13 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0021_account-authorized'), + ] + + operations = [ + migrations.AddField( + model_name='subscription', + name='stripe_account', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Account'), + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 796ff114f..62d7b9753 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -327,7 +327,7 @@ def apply_discount(self, amount): return amount -class Subscription(StripeObject): +class Subscription(AccountRelatedStripeObject): STATUS_CURRENT = ["trialing", "active"] diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index f31e3e164..bb2e828ff 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -803,7 +803,19 @@ def setUpClass(cls): user=cls.user, stripe_id="cus_xxxxxxxxxxxxxxx" ) + cls.plan = Plan.objects.create( + stripe_id="the-plan", + amount=2, + interval_count=1, + ) cls.account = Account.objects.create(stripe_id="acct_xx") + cls.connected_customer = Customer.objects.create( + stripe_id="cus_yyyyyyyyyyyyyyy", + stripe_account=cls.account, + ) + UserAccount.objects.create(user=cls.user, + customer=cls.connected_customer, + account=cls.account) def test_has_active_subscription(self): plan = Plan.objects.create( @@ -961,18 +973,35 @@ def test_subscription_create_token(self, SubscriptionCreateMock, CustomerMock): _, kwargs = SubscriptionCreateMock.call_args self.assertEquals(kwargs["source"], "token") - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") @patch("stripe.Subscription.create") - def test_subscription_create_with_connect(self, SubscriptionCreateMock, SyncMock): - subscriptions.create(self.customer, "the-plan", stripe_account=self.account) - self.assertTrue(SyncMock.called) + def test_subscription_create_with_connect(self, SubscriptionCreateMock): + SubscriptionCreateMock.return_value = { + "object": "subscription", + "id": "sub_XX", + "application_fee_percent": None, + "cancel_at_period_end": False, + "canceled_at": None, + "current_period_start": datetime.datetime.now().timestamp(), + "current_period_end": (datetime.datetime.now() + datetime.timedelta(days=30)).timestamp(), + "ended_at": None, + "quantity": 1, + "start": datetime.datetime.now().timestamp(), + "status": "active", + "trial_start": None, + "trial_end": None, + "plan": { + "id": self.plan.stripe_id, + }} + subscriptions.create(self.connected_customer, self.plan.stripe_id) SubscriptionCreateMock.assert_called_once_with( coupon=None, - customer=self.customer.stripe_id, + customer=self.connected_customer.stripe_id, plan="the-plan", quantity=4, tax_percent=None, stripe_account=self.account.stripe_id) + subscription = Subscription.objects.get(stripe_account=self.account) + self.assertEqual(subscription.customer, self.connected_customer) def test_is_period_current(self): sub = Subscription(current_period_end=(timezone.now() + datetime.timedelta(days=2))) diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 76618ab1c..6aff9f9d2 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -358,7 +358,7 @@ def process_webhook(self): if self.event.validated_message: subscriptions.sync_subscription_from_stripe_data( self.event.customer, - self.event.validated_message["data"]["object"] + self.event.validated_message["data"]["object"], ) if self.event.customer: From 5ed86af766626d615937f81814405e346cfa2855 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 26 Oct 2017 16:21:03 +0200 Subject: [PATCH 065/153] Fix processing of account.application.deauthorized events don't try to fetch the event as we lost access to remote account --- pinax/stripe/tests/settings.py | 2 +- pinax/stripe/tests/test_webhooks.py | 44 +++++++++++++++++++++++------ pinax/stripe/utils.py | 4 +++ pinax/stripe/webhooks.py | 29 ++++++++++++++++++- 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/pinax/stripe/tests/settings.py b/pinax/stripe/tests/settings.py index 9e1ca1b11..9643c0747 100644 --- a/pinax/stripe/tests/settings.py +++ b/pinax/stripe/tests/settings.py @@ -33,7 +33,7 @@ ] SITE_ID = 1 PINAX_STRIPE_PUBLIC_KEY = "" -PINAX_STRIPE_SECRET_KEY = "" +PINAX_STRIPE_SECRET_KEY = "sk_test_01234567890123456789abcd" PINAX_STRIPE_SUBSCRIPTION_REQUIRED_EXCEPTION_URLS = ["pinax_stripe_subscription_create"] PINAX_STRIPE_SUBSCRIPTION_REQUIRED_REDIRECT = "pinax_stripe_subscription_create" PINAX_STRIPE_HOOKSET = "pinax.stripe.tests.hooks.TestHookSet" diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index 690b3f33c..ec2177c28 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -602,14 +602,42 @@ def test_process_webhook(self, SyncMock, RetrieveMock): AccountUpdatedWebhook(event).process_webhook() self.assertTrue(SyncMock.called) - @patch("pinax.stripe.actions.accounts.deauthorize") - def test_process_deauthorize(self, DeauthorizeMock): + @patch("stripe.Event.retrieve") + def test_process_deauthorize(self, RetrieveMock): + data = {"data": {"object": {"id": "evt_001"}}, + "account": self.account.stripe_id} event = Event.objects.create( kind=AccountApplicationDeauthorizeWebhook.name, - webhook_message={}, - valid=True, - processed=False + webhook_message=data, + ) + RetrieveMock.side_effect = stripe.error.PermissionError( + "The provided key 'sk_test_********************abcd' does not have access to account 'acc_aa'") + AccountApplicationDeauthorizeWebhook(event).process() + self.assertTrue(event.valid) + self.assertTrue(event.processed) + self.account.refresh_from_db() + self.assertFalse(self.account.authorized) + + @patch("stripe.Event.retrieve") + def test_process_deauthorize_fake_response(self, RetrieveMock): + data = {"data": {"object": {"id": "evt_001"}}, + "account": self.account.stripe_id} + event = Event.objects.create( + kind=AccountApplicationDeauthorizeWebhook.name, + webhook_message=data, + ) + RetrieveMock.side_effect = stripe.error.PermissionError( + "The provided key 'sk_test_********************ABCD' does not have access to account 'acc_aa'") + with self.assertRaises(stripe.error.PermissionError): + AccountApplicationDeauthorizeWebhook(event).process() + + @patch("stripe.Event.retrieve") + def test_process_deauthorize_with_authorizes_account(self, RetrieveMock): + data = {"data": {"object": {"id": "evt_002"}}, + "account": self.account.stripe_id} + event = Event.objects.create( + kind=AccountApplicationDeauthorizeWebhook.name, + webhook_message=data, ) - event.validated_message = dict(data=dict(object=dict(id=self.account.stripe_id))) - AccountApplicationDeauthorizeWebhook(event).process_webhook() - self.assertTrue(DeauthorizeMock.called) + with self.assertRaises(ValueError): + AccountApplicationDeauthorizeWebhook(event).process() diff --git a/pinax/stripe/utils.py b/pinax/stripe/utils.py index fd784919c..648681f38 100644 --- a/pinax/stripe/utils.py +++ b/pinax/stripe/utils.py @@ -62,3 +62,7 @@ def update_with_defaults(obj, defaults, created): "sgd": "\u0024", "usd": "\u0024", } + + +def obfuscate_secret_key(secret_key): + return "*" * 20 + secret_key[-4:] diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 6aff9f9d2..aaf030bd2 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -19,6 +19,7 @@ transfers ) from .conf import settings +from .utils import obfuscate_secret_key class WebhookRegistry(object): @@ -144,8 +145,34 @@ class AccountApplicationDeauthorizeWebhook(Webhook): name = "account.application.deauthorized" description = "Occurs whenever a user deauthorizes an application. Sent to the related application only." + def validate(self): + """ + Specialized validation of incoming events. + + We try to retrieve the event: + - in case of PermissionError exception, everything is perfectly normal. + It means the account has been deauthorized. + - In case no exception has been caught, then, most likely, the event has been forged + to make you believe the account has been disabled despite it is still functioning. + """ + stripe_account_id = self.event.webhook_message["account"] + self.stripe_account = models.Account.objects.filter(stripe_id=stripe_account_id).first() + try: + stripe.Event.retrieve( + self.event.stripe_id, + stripe_account=stripe_account_id, + ) + except stripe.error.PermissionError as exc: + if not(stripe_account_id in str(exc) and obfuscate_secret_key(settings.PINAX_STRIPE_SECRET_KEY) in str(exc)): + raise exc + self.event.valid = True + self.event.validated_message = self.event.webhook_message + self.event.stripe_account = self.stripe_account + else: + raise ValueError("The remote account still valid. this might be an hostile event") + def process_webhook(self): - accounts.deauthorize(models.Account.objects.get(stripe_id=self.event.message["data"]["object"]["id"])) + accounts.deauthorize(self.stripe_account) class AccountExternalAccountCreatedWebhook(Webhook): From cd979b21f83e86c44823a32cfae2c45c6c6f1f50 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 26 Oct 2017 16:49:11 +0200 Subject: [PATCH 066/153] c962eaf8a4276a75d010717fddfddc2a6af2bdce --- pinax/stripe/webhooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index aaf030bd2..b71422864 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -169,7 +169,7 @@ def validate(self): self.event.validated_message = self.event.webhook_message self.event.stripe_account = self.stripe_account else: - raise ValueError("The remote account still valid. this might be an hostile event") + raise ValueError("The remote account is still valid. This might be a hostile event") def process_webhook(self): accounts.deauthorize(self.stripe_account) From d489328c3cc67fb44500075116fb219be482eda3 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 26 Oct 2017 17:03:35 +0200 Subject: [PATCH 067/153] This might happen if account has already been removed locally --- pinax/stripe/tests/test_webhooks.py | 15 +++++++++++++++ pinax/stripe/webhooks.py | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index ec2177c28..fc56e6b66 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -641,3 +641,18 @@ def test_process_deauthorize_with_authorizes_account(self, RetrieveMock): ) with self.assertRaises(ValueError): AccountApplicationDeauthorizeWebhook(event).process() + + @patch("stripe.Event.retrieve") + def test_process_deauthorize_with_delete_account(self, RetrieveMock): + data = {"data": {"object": {"id": "evt_002"}}, + "account": "acct_bb"} + event = Event.objects.create( + kind=AccountApplicationDeauthorizeWebhook.name, + webhook_message=data, + ) + RetrieveMock.side_effect = stripe.error.PermissionError( + "The provided key 'sk_test_********************abcd' does not have access to account 'acct_bb'") + AccountApplicationDeauthorizeWebhook(event).process() + self.assertTrue(event.valid) + self.assertTrue(event.processed) + self.assertIsNone(event.stripe_account) diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index b71422864..a9ee6b0cb 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -172,7 +172,8 @@ def validate(self): raise ValueError("The remote account is still valid. This might be a hostile event") def process_webhook(self): - accounts.deauthorize(self.stripe_account) + if self.stripe_account is not None: + accounts.deauthorize(self.stripe_account) class AccountExternalAccountCreatedWebhook(Webhook): From 17471bf29fe9c033b4df8bb68df1a06c70102fec Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Thu, 26 Oct 2017 17:15:14 +0200 Subject: [PATCH 068/153] Use real-life fixture --- pinax/stripe/tests/test_webhooks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index fc56e6b66..56fe1189b 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -611,7 +611,7 @@ def test_process_deauthorize(self, RetrieveMock): webhook_message=data, ) RetrieveMock.side_effect = stripe.error.PermissionError( - "The provided key 'sk_test_********************abcd' does not have access to account 'acc_aa'") + "The provided key 'sk_test_********************abcd' does not have access to account 'acc_aa' (or that account does not exist). Application access may have been revoked.") AccountApplicationDeauthorizeWebhook(event).process() self.assertTrue(event.valid) self.assertTrue(event.processed) @@ -627,7 +627,7 @@ def test_process_deauthorize_fake_response(self, RetrieveMock): webhook_message=data, ) RetrieveMock.side_effect = stripe.error.PermissionError( - "The provided key 'sk_test_********************ABCD' does not have access to account 'acc_aa'") + "The provided key 'sk_test_********************ABCD' does not have access to account 'acc_aa' (or that account does not exist). Application access may have been revoked.") with self.assertRaises(stripe.error.PermissionError): AccountApplicationDeauthorizeWebhook(event).process() @@ -651,7 +651,7 @@ def test_process_deauthorize_with_delete_account(self, RetrieveMock): webhook_message=data, ) RetrieveMock.side_effect = stripe.error.PermissionError( - "The provided key 'sk_test_********************abcd' does not have access to account 'acct_bb'") + "The provided key 'sk_test_********************abcd' does not have access to account 'acct_bb' (or that account does not exist). Application access may have been revoked.") AccountApplicationDeauthorizeWebhook(event).process() self.assertTrue(event.valid) self.assertTrue(event.processed) From 4ac323c627a6cebb5623e3c9687f54aa9723b6fc Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Fri, 27 Oct 2017 10:26:50 +0200 Subject: [PATCH 069/153] Retrieve connected subscription --- pinax/stripe/actions/subscriptions.py | 2 +- pinax/stripe/tests/test_actions.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index 9e27887e2..8b8c72c6c 100644 --- a/pinax/stripe/actions/subscriptions.py +++ b/pinax/stripe/actions/subscriptions.py @@ -126,7 +126,7 @@ def retrieve(customer, sub_id): """ if not sub_id: return - subscription = stripe.Subscription.retrieve(sub_id) + subscription = stripe.Subscription.retrieve(sub_id, stripe_account=getattr(customer.stripe_account, "stripe_id", None)) if subscription and subscription.customer != customer.stripe_id: return return subscription diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index bb2e828ff..7b742a0d7 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -1003,6 +1003,30 @@ def test_subscription_create_with_connect(self, SubscriptionCreateMock): subscription = Subscription.objects.get(stripe_account=self.account) self.assertEqual(subscription.customer, self.connected_customer) + @patch("stripe.Subscription.retrieve") + @patch("stripe.Subscription.create") + def test_retrieve_subscription_with_connect(self, CreateMock, RetrieveMock): + CreateMock.return_value = { + "object": "subscription", + "id": "sub_XX", + "application_fee_percent": None, + "cancel_at_period_end": False, + "canceled_at": None, + "current_period_start": datetime.datetime.now().timestamp(), + "current_period_end": (datetime.datetime.now() + datetime.timedelta(days=30)).timestamp(), + "ended_at": None, + "quantity": 1, + "start": datetime.datetime.now().timestamp(), + "status": "active", + "trial_start": None, + "trial_end": None, + "plan": { + "id": self.plan.stripe_id, + }} + subscriptions.create(self.connected_customer, self.plan.stripe_id) + subscriptions.retrieve(self.connected_customer, "sub_XX") + RetrieveMock.assert_called_once_with("sub_XX", stripe_account=self.account.stripe_id) + def test_is_period_current(self): sub = Subscription(current_period_end=(timezone.now() + datetime.timedelta(days=2))) self.assertTrue(subscriptions.is_period_current(sub)) From cfe65759dc6e5cf00365253b6156a7fb83c2b28c Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Fri, 27 Oct 2017 10:50:49 +0200 Subject: [PATCH 070/153] Fix connected Charges --- pinax/stripe/actions/invoices.py | 2 +- pinax/stripe/tests/test_actions.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pinax/stripe/actions/invoices.py b/pinax/stripe/actions/invoices.py index b4c498e6d..0df068c70 100644 --- a/pinax/stripe/actions/invoices.py +++ b/pinax/stripe/actions/invoices.py @@ -79,7 +79,7 @@ def sync_invoice_from_stripe_data(stripe_invoice, send_receipt=settings.PINAX_ST period_start = utils.convert_tstamp(stripe_invoice, "period_start") date = utils.convert_tstamp(stripe_invoice, "date") sub_id = stripe_invoice.get("subscription") - stripe_account_id = stripe_invoice.get("account") + stripe_account_id = getattr(c.stripe_account, "stripe_id", None) if stripe_invoice.get("charge"): charge = charges.sync_charge(stripe_invoice["charge"], stripe_account=stripe_account_id) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 7b742a0d7..b3d5af183 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -2589,6 +2589,7 @@ def setUp(self): "total": 1999, "webhooks_delivered_at": None } + self.account = Account.objects.create(stripe_id="acct_X") @patch("pinax.stripe.hooks.hookset.send_receipt") @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") @@ -2652,7 +2653,8 @@ def test_sync_invoice_from_stripe_data_no_send_receipt(self, RetrieveSubscriptio @patch("pinax.stripe.actions.subscriptions.retrieve") def test_sync_invoice_from_stripe_data_connect(self, RetrieveSubscriptionMock, SyncInvoiceItemsMock, SyncChargeMock, ChargeFetchMock, SyncSubscriptionMock, SendReceiptMock): self.invoice_data["charge"] = "ch_XXXXXX" - self.invoice_data["account"] = "acct_X" + self.customer.stripe_account = self.account + self.customer.save() charge = Charge.objects.create( stripe_id="ch_XXXXXX", customer=self.customer, From 2e82d6c73ce5dd28ec704dad578a54956a6753f9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 27 Oct 2017 16:28:21 +0200 Subject: [PATCH 071/153] merge migrations --- .../migrations/0023_merge_20171027_1626.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pinax/stripe/migrations/0023_merge_20171027_1626.py diff --git a/pinax/stripe/migrations/0023_merge_20171027_1626.py b/pinax/stripe/migrations/0023_merge_20171027_1626.py new file mode 100644 index 000000000..845b27b61 --- /dev/null +++ b/pinax/stripe/migrations/0023_merge_20171027_1626.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-27 14:26 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0013_auto_20171027_1443'), + ('pinax_stripe', '0022_sub-account-related'), + ] + + operations = [ + ] From a434a969ff6284ea998b41e90ca00d1f1c86936d Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Fri, 27 Oct 2017 17:45:23 +0200 Subject: [PATCH 072/153] We probably need to pass the stripe account --- pinax/stripe/actions/charges.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pinax/stripe/actions/charges.py b/pinax/stripe/actions/charges.py index 027d61b8d..34e535aa3 100644 --- a/pinax/stripe/actions/charges.py +++ b/pinax/stripe/actions/charges.py @@ -95,6 +95,7 @@ def create( customer=customer, description=description, capture=capture, + stripe_account=getattr(customer.stripe_account, "stripe_id", None), ) if destination_account: kwargs["destination"] = {"account": destination_account} From a0c5c1d28cba4078e566650d850e8058c2de2528 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Fri, 27 Oct 2017 18:09:45 +0200 Subject: [PATCH 073/153] Not quite there yet --- pinax/stripe/actions/charges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/actions/charges.py b/pinax/stripe/actions/charges.py index 34e535aa3..a42046152 100644 --- a/pinax/stripe/actions/charges.py +++ b/pinax/stripe/actions/charges.py @@ -95,7 +95,6 @@ def create( customer=customer, description=description, capture=capture, - stripe_account=getattr(customer.stripe_account, "stripe_id", None), ) if destination_account: kwargs["destination"] = {"account": destination_account} @@ -110,6 +109,7 @@ def create( ) elif on_behalf_of: kwargs["on_behalf_of"] = on_behalf_of + kwargs["stripe_account"] = on_behalf_of stripe_charge = stripe.Charge.create( **kwargs ) From 28570038e8fd4a3f218531b33cfdc9820eb8ec14 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 27 Oct 2017 19:08:15 +0200 Subject: [PATCH 074/153] actions.charges.create: commit hotfix from prod --- pinax/stripe/actions/charges.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/actions/charges.py b/pinax/stripe/actions/charges.py index a42046152..e2b2298cb 100644 --- a/pinax/stripe/actions/charges.py +++ b/pinax/stripe/actions/charges.py @@ -108,7 +108,8 @@ def create( application_fee, currency ) elif on_behalf_of: - kwargs["on_behalf_of"] = on_behalf_of + # XXX: cleanup: on_behalf_of is not required likely, but only + # stripe_account kwargs["stripe_account"] = on_behalf_of stripe_charge = stripe.Charge.create( **kwargs From 50af2da4c138cb95674f0345afd3a1910ec2ddf4 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 27 Oct 2017 20:32:51 +0200 Subject: [PATCH 075/153] fixup! actions.charges.create: commit hotfix from prod --- pinax/stripe/tests/test_actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index b3d5af183..eb18a6382 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -146,7 +146,7 @@ def test_create_with_on_behalf_of(self, CreateMock, SyncMock, SendReceiptMock): ) self.assertTrue(CreateMock.called) _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["on_behalf_of"], "account") + self.assertEqual(kwargs["stripe_account"], "account") self.assertTrue(SyncMock.called) self.assertTrue(SendReceiptMock.called) From 7bee4732ac7eb9ffd7f0913872bada31d635bd53 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 30 Oct 2017 19:40:51 +0100 Subject: [PATCH 076/153] Revert "Merge pull request #25 from blueyed/revert-stripe_publishable_key" This reverts commit 7881f7293dea472da7ac103ac416ad81b1ada8dd, reversing changes made to f0172f1cec23cb45c73f2436ebcad32fbe622239. --- .../0011_account_publishable_key.py | 20 +++++++++++++++++++ pinax/stripe/models.py | 2 ++ 2 files changed, 22 insertions(+) create mode 100644 pinax/stripe/migrations/0011_account_publishable_key.py diff --git a/pinax/stripe/migrations/0011_account_publishable_key.py b/pinax/stripe/migrations/0011_account_publishable_key.py new file mode 100644 index 000000000..a8a0a4e1a --- /dev/null +++ b/pinax/stripe/migrations/0011_account_publishable_key.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-24 09:13 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0010_connect'), + ] + + operations = [ + migrations.AddField( + model_name='account', + name='stripe_publishable_key', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index cbe48319a..c11a4e012 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -552,6 +552,8 @@ class Account(StripeObject): stripe_publishable_key = models.CharField(null=True, blank=True, max_length=100) + stripe_publishable_key = models.CharField(null=True, blank=True, max_length=100) + product_description = models.TextField(null=True, blank=True) statement_descriptor = models.TextField(null=True, blank=True) support_email = models.TextField(null=True, blank=True) From 3f7dc39e597a99df6793a39c2b94b7311043c5d0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 30 Oct 2017 19:41:38 +0100 Subject: [PATCH 077/153] Merge migrations --- .../migrations/0024_merge_20171030_1941.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pinax/stripe/migrations/0024_merge_20171030_1941.py diff --git a/pinax/stripe/migrations/0024_merge_20171030_1941.py b/pinax/stripe/migrations/0024_merge_20171030_1941.py new file mode 100644 index 000000000..77f80c3a5 --- /dev/null +++ b/pinax/stripe/migrations/0024_merge_20171030_1941.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-30 18:41 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0023_merge_20171027_1626'), + ('pinax_stripe', '0015_merge_20171030_1852'), + ] + + operations = [ + ] From ac40f671753dbe70fbe46af53720bc5f4346d94b Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 1 Nov 2017 11:12:23 +0100 Subject: [PATCH 078/153] Attempt to solve migration conflict --- .../stripe/migrations/0011_account_publishable_key.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pinax/stripe/migrations/0011_account_publishable_key.py b/pinax/stripe/migrations/0011_account_publishable_key.py index a8a0a4e1a..534edf046 100644 --- a/pinax/stripe/migrations/0011_account_publishable_key.py +++ b/pinax/stripe/migrations/0011_account_publishable_key.py @@ -12,9 +12,9 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddField( - model_name='account', - name='stripe_publishable_key', - field=models.CharField(blank=True, max_length=100, null=True), - ), + # migrations.AddField( + # model_name='account', + # name='stripe_publishable_key', + # field=models.CharField(blank=True, max_length=100, null=True), + # ), ] From 6a2c05de666068c972bd0cea588dfe995d1611e5 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 1 Nov 2017 12:49:53 +0100 Subject: [PATCH 079/153] Allow to pass idempotency key to create charges --- pinax/stripe/actions/charges.py | 4 +++- pinax/stripe/tests/test_actions.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/actions/charges.py b/pinax/stripe/actions/charges.py index e80848142..7f254a120 100644 --- a/pinax/stripe/actions/charges.py +++ b/pinax/stripe/actions/charges.py @@ -69,7 +69,7 @@ def create( amount, customer=None, source=None, currency="usd", description=None, send_receipt=settings.PINAX_STRIPE_SEND_EMAIL_RECEIPTS, capture=True, email=None, destination_account=None, destination_amount=None, - application_fee=None, on_behalf_of=None, + application_fee=None, on_behalf_of=None, idempotency_key=None, ): """ Create a charge for the given customer or source. @@ -91,6 +91,7 @@ def create( destination_amount: amount to transfer to the `destination_account` without creating an application fee application_fee: used with `destination_account` to add a fee destined for the platform account on_behalf_of: stripe_id of a connected account. Creates direct Charges to this account. + idempotency_key: Any string that allows retries to be performed safely. Returns: a pinax.stripe.models.Charge object @@ -103,6 +104,7 @@ def create( customer=customer, description=description, capture=capture, + idempotency_key=idempotency_key, ) if destination_account: kwargs["destination"] = {"account": destination_account} diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 2d3b2c581..3248c0693 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -102,6 +102,21 @@ def test_create(self, CreateMock, SyncMock, SendReceiptMock): self.assertTrue(SyncMock.called) self.assertTrue(SendReceiptMock.called) + @patch("pinax.stripe.hooks.hookset.send_receipt") + @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") + @patch("stripe.Charge.create") + def test_create_with_idempotency_key(self, CreateMock, SyncMock, SendReceiptMock): + charges.create(amount=decimal.Decimal("10"), customer=self.customer.stripe_id, idempotency_key="a") + CreateMock.assert_called_once_with( + amount=1000, + capture=True, + customer=self.customer.stripe_id, + idempotency_key="a", + description=None, + currency="usd", + source=None, + ) + @patch("pinax.stripe.hooks.hookset.send_receipt") @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") @patch("stripe.Charge.create") From db11438be8ad6e66d8436f05485106aa9b70c134 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 1 Nov 2017 16:37:44 +0100 Subject: [PATCH 080/153] rely on stripe_account_stripe_id when possible --- pinax/stripe/actions/invoices.py | 2 +- pinax/stripe/actions/subscriptions.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pinax/stripe/actions/invoices.py b/pinax/stripe/actions/invoices.py index 0df068c70..0b71de610 100644 --- a/pinax/stripe/actions/invoices.py +++ b/pinax/stripe/actions/invoices.py @@ -79,7 +79,7 @@ def sync_invoice_from_stripe_data(stripe_invoice, send_receipt=settings.PINAX_ST period_start = utils.convert_tstamp(stripe_invoice, "period_start") date = utils.convert_tstamp(stripe_invoice, "date") sub_id = stripe_invoice.get("subscription") - stripe_account_id = getattr(c.stripe_account, "stripe_id", None) + stripe_account_id = c.stripe_account_stripe_id if stripe_invoice.get("charge"): charge = charges.sync_charge(stripe_invoice["charge"], stripe_account=stripe_account_id) diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index 8b8c72c6c..1f5e83205 100644 --- a/pinax/stripe/actions/subscriptions.py +++ b/pinax/stripe/actions/subscriptions.py @@ -48,7 +48,7 @@ def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=No if token: subscription_params["source"] = token - subscription_params["stripe_account"] = getattr(customer.stripe_account, "stripe_id", None) + subscription_params["stripe_account"] = customer.stripe_account_stripe_id subscription_params["customer"] = customer.stripe_id subscription_params["plan"] = plan subscription_params["quantity"] = quantity @@ -126,7 +126,7 @@ def retrieve(customer, sub_id): """ if not sub_id: return - subscription = stripe.Subscription.retrieve(sub_id, stripe_account=getattr(customer.stripe_account, "stripe_id", None)) + subscription = stripe.Subscription.retrieve(sub_id, stripe_account=customer.stripe_account_stripe_id) if subscription and subscription.customer != customer.stripe_id: return return subscription From 0ecdb8c4c5ef247556b64394543bd1d4b51b5680 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 1 Nov 2017 16:53:12 +0100 Subject: [PATCH 081/153] Fix retrieve connected subscription --- pinax/stripe/models.py | 2 +- pinax/stripe/tests/test_models.py | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index c11a4e012..0d0413067 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -368,7 +368,7 @@ class Subscription(AccountRelatedStripeObject): @property def stripe_subscription(self): - return stripe.Customer.retrieve(self.customer.stripe_id).subscriptions.retrieve(self.stripe_id) + return stripe.Subscription.retrieve(self.stripe_id, stripe_account=self.stripe_account_stripe_id) @property def total_amount(self): diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index 3937e6f72..9df80a227 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -172,6 +172,14 @@ def test_account_str_and_repr(self): self.assertEquals(str(a), "Display name - acct_X") self.assertEquals(repr(a), "Account(pk=None, display_name='Display name', type=None, stripe_id='acct_X', authorized=False)") + @patch("stripe.Subscription.retrieve") + def test_subscription_stripe_subscription_with_connnect(self, RetrieveMock): + a = Account(stripe_id="acc_X") + c = Customer(stripe_id="cus_X", stripe_account=a) + s = Subscription(stripe_id="sub_X", stripe_account=a, customer=c) + s.stripe_subscription + RetrieveMock.assert_called_once_with("sub_X", stripe_account="acc_X") + class StripeObjectTests(TestCase): @@ -196,10 +204,10 @@ def test_stripe_invoice(self, RetrieveMock): Invoice().stripe_invoice self.assertTrue(RetrieveMock.called) - @patch("stripe.Customer.retrieve") + @patch("stripe.Subscription.retrieve") def test_stripe_subscription(self, RetrieveMock): - Subscription(customer=Customer(stripe_id="foo")).stripe_subscription - self.assertTrue(RetrieveMock().subscriptions.retrieve.called) + Subscription(stripe_id="sub_X", customer=Customer(stripe_id="foo")).stripe_subscription + RetrieveMock.assert_called_once_with("sub_X", stripe_account=None) @patch("stripe.Transfer.retrieve") def test_stripe_transfer(self, RetrieveMock): From 9fea5e8c3176f88b7bba86055ea9f38abc6efdd6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 1 Nov 2017 18:10:55 +0100 Subject: [PATCH 082/153] Adjust tests for idempotency_key/stripe_account --- pinax/stripe/tests/test_actions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 78767fc06..fecd2ef8b 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -108,6 +108,7 @@ def test_create_with_customer(self, CreateMock, SyncMock, SendReceiptMock): "stripe_account": None, "description": None, "capture": True, + "idempotency_key": None, }) self.assertTrue(SyncMock.called) self.assertTrue(SendReceiptMock.called) @@ -127,6 +128,7 @@ def test_create_with_customer_id(self, CreateMock, SyncMock, SendReceiptMock): "stripe_account": None, "description": None, "capture": True, + "idempotency_key": None, }) self.assertTrue(SyncMock.called) self.assertTrue(SendReceiptMock.called) @@ -146,6 +148,7 @@ def test_create_with_new_customer_id(self, CreateMock, SyncMock, SendReceiptMock "stripe_account": None, "description": None, "capture": True, + "idempotency_key": None, }) self.assertTrue(SyncMock.called) self.assertTrue(SendReceiptMock.called) @@ -160,6 +163,7 @@ def test_create_with_idempotency_key(self, CreateMock, SyncMock, SendReceiptMock amount=1000, capture=True, customer=self.customer.stripe_id, + stripe_account=self.customer.stripe_account_stripe_id, idempotency_key="a", description=None, currency="usd", From bb4bc39de413e8d6139e4c84688095519c0c80fe Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 27 Oct 2017 14:39:59 +0200 Subject: [PATCH 083/153] Subscription: use StripeAccountFromCustomerMixin --- pinax/stripe/actions/subscriptions.py | 2 -- ...0025_remove_subscription_stripe_account.py | 19 +++++++++++++++++++ pinax/stripe/models.py | 2 +- pinax/stripe/tests/test_actions.py | 5 ++--- pinax/stripe/tests/test_models.py | 2 +- 5 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 pinax/stripe/migrations/0025_remove_subscription_stripe_account.py diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index 2cd566e5a..4a99e5580 100644 --- a/pinax/stripe/actions/subscriptions.py +++ b/pinax/stripe/actions/subscriptions.py @@ -48,7 +48,6 @@ def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=No if token: subscription_params["source"] = token - subscription_params["stripe_account"] = customer.stripe_account_stripe_id subscription_params["customer"] = customer.stripe_id subscription_params["plan"] = plan subscription_params["quantity"] = quantity @@ -157,7 +156,6 @@ def sync_subscription_from_stripe_data(customer, subscription): status=subscription["status"], trial_start=utils.convert_tstamp(subscription["trial_start"]) if subscription["trial_start"] else None, trial_end=utils.convert_tstamp(subscription["trial_end"]) if subscription["trial_end"] else None, - stripe_account=customer.stripe_account, ) sub, created = models.Subscription.objects.get_or_create( stripe_id=subscription["id"], diff --git a/pinax/stripe/migrations/0025_remove_subscription_stripe_account.py b/pinax/stripe/migrations/0025_remove_subscription_stripe_account.py new file mode 100644 index 000000000..8336e33fc --- /dev/null +++ b/pinax/stripe/migrations/0025_remove_subscription_stripe_account.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-11-03 15:07 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0024_merge_20171030_1941'), + ] + + operations = [ + migrations.RemoveField( + model_name='subscription', + name='stripe_account', + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 0d0413067..2349d8b51 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -348,7 +348,7 @@ def apply_discount(self, amount): return amount -class Subscription(AccountRelatedStripeObject): +class Subscription(StripeAccountFromCustomerMixin, StripeObject): STATUS_CURRENT = ["trialing", "active"] diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 68f9165a3..05f5b6b33 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -1081,9 +1081,8 @@ def test_subscription_create_with_connect(self, SubscriptionCreateMock): customer=self.connected_customer.stripe_id, plan="the-plan", quantity=4, - tax_percent=None, - stripe_account=self.account.stripe_id) - subscription = Subscription.objects.get(stripe_account=self.account) + tax_percent=None) + subscription = Subscription.objects.get() self.assertEqual(subscription.customer, self.connected_customer) @patch("stripe.Subscription.retrieve") diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index 9df80a227..843e96a5e 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -176,7 +176,7 @@ def test_account_str_and_repr(self): def test_subscription_stripe_subscription_with_connnect(self, RetrieveMock): a = Account(stripe_id="acc_X") c = Customer(stripe_id="cus_X", stripe_account=a) - s = Subscription(stripe_id="sub_X", stripe_account=a, customer=c) + s = Subscription(stripe_id="sub_X", customer=c) s.stripe_subscription RetrieveMock.assert_called_once_with("sub_X", stripe_account="acc_X") From 5775984b895deed48ee2825e0067444b951da11b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 6 Nov 2017 21:55:25 +0100 Subject: [PATCH 084/153] Fix UserAccount for py2 (#53) > ValueError: @python_2_unicode_compatible cannot be applied to > UserAccount because it doesn't define __str__(). --- pinax/stripe/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 2349d8b51..49777f8ea 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -230,7 +230,6 @@ class TransferChargeFee(models.Model): created_at = models.DateTimeField(default=timezone.now) -@python_2_unicode_compatible class UserAccount(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="user_accounts", From 3832365f0e71d0f677222d5266abcf53699dba2c Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Tue, 7 Nov 2017 16:15:17 +0100 Subject: [PATCH 085/153] Retrieve customer with stripe account --- pinax/stripe/actions/customers.py | 2 +- pinax/stripe/tests/test_actions.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index cbf9b75bc..baa7d483c 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -71,7 +71,7 @@ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_ST cus = None else: try: - stripe.Customer.retrieve(cus.stripe_id) + stripe.Customer.retrieve(cus.stripe_id, stripe_account=stripe_account.stripe_id) except stripe.error.InvalidRequestError: logger.debug("customer found but failed to retrieve for user %s, and account %s", user, stripe_account) else: diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index b132a2be4..60714917d 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -624,6 +624,7 @@ def test_customer_create_with_connect_and_stale_user_account(self, CreateMock, S self.assertTrue(SyncMock.called) self.assertEqual(self.user.user_accounts.get(), ua) self.assertEqual(ua.customer, customer) + RetrieveMock.assert_called_once_with("cus_Z", stripe_account=self.account.stripe_id) class EventsTests(TestCase): From b42ed06af862e2a3a29119b7ff8b1ce89b03af8f Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Tue, 7 Nov 2017 16:30:32 +0100 Subject: [PATCH 086/153] Align with master --- pinax/stripe/actions/customers.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index baa7d483c..a9bfd8f71 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -27,17 +27,13 @@ def can_charge(customer): def _create_without_account(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None): - try: - cus = models.Customer.objects.get(user=user) - except models.Customer.DoesNotExist: - pass - else: + cus = models.Customer.objects.filter(user=user).first() + if cus is not None: try: stripe.Customer.retrieve(cus.stripe_id) + return cus except stripe.error.InvalidRequestError: pass - else: - return cus # At this point we maybe have a local Customer but no stripe customer # let's create one and make the binding @@ -64,18 +60,13 @@ def _create_without_account(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_ def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None): - try: - cus = user.customers.get(user_account__account=stripe_account) - except models.Customer.DoesNotExist: - logger.debug("customer not found for user %s, and account %s", user, stripe_account) - cus = None - else: + cus = user.customers.filter(user_account__account=stripe_account).first() + if cus is not None: try: stripe.Customer.retrieve(cus.stripe_id, stripe_account=stripe_account.stripe_id) - except stripe.error.InvalidRequestError: - logger.debug("customer found but failed to retrieve for user %s, and account %s", user, stripe_account) - else: return cus + except stripe.error.InvalidRequestError: + pass # At this point we maybe have a local Customer but no stripe customer # let's create one and make the binding From e8f4f610aa149f7cb7dcdfde18a79cb28e419515 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Tue, 7 Nov 2017 17:09:34 +0100 Subject: [PATCH 087/153] prevent one excessive join --- pinax/stripe/actions/customers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index a9bfd8f71..6121435d2 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -130,7 +130,7 @@ def get_customer_for_user(user, stripe_account=None): """ if stripe_account is None: return models.Customer.objects.filter(user=user).first() - return user.customers.filter(user_account__customer__stripe_account=stripe_account).first() + return user.customers.filter(user_account__account=stripe_account).first() def purge_local(customer): From ca7e1ed8ed4bdc7c06980870079d33a462556e37 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 8 Nov 2017 18:36:27 +0100 Subject: [PATCH 088/153] migrations: remove merging of already merged migrations --- .../migrations/0018_merge_20171107_1540.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 pinax/stripe/migrations/0018_merge_20171107_1540.py diff --git a/pinax/stripe/migrations/0018_merge_20171107_1540.py b/pinax/stripe/migrations/0018_merge_20171107_1540.py deleted file mode 100644 index 7a6389895..000000000 --- a/pinax/stripe/migrations/0018_merge_20171107_1540.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-11-07 14:40 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0017_merge_20171106_2201'), - ('pinax_stripe', '0017_merge_20171106_2109'), - ] - - operations = [ - ] From e85880c7672ff702f216c8a69aabccc969a1082d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 10 Nov 2017 01:14:53 +0100 Subject: [PATCH 089/153] minor: align whitespace around EventsTests with master --- pinax/stripe/tests/test_actions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index e08dbdee1..9f5af60ff 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -630,6 +630,7 @@ def test_customer_create_with_connect_and_stale_user_account(self, CreateMock, S class EventsTests(TestCase): + @classmethod def setUpClass(cls): super(EventsTests, cls).setUpClass() From 728b559d1bcd38cecaa90856a1aea5b2971aa288 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 10 Nov 2017 01:24:55 +0100 Subject: [PATCH 090/153] Remove already applied migration --- pinax/stripe/migrations/0019_user_account.py | 36 -------------------- 1 file changed, 36 deletions(-) delete mode 100644 pinax/stripe/migrations/0019_user_account.py diff --git a/pinax/stripe/migrations/0019_user_account.py b/pinax/stripe/migrations/0019_user_account.py deleted file mode 100644 index 6ee295180..000000000 --- a/pinax/stripe/migrations/0019_user_account.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-11-09 09:37 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('pinax_stripe', '0018_merge_20171107_1540'), - ] - - operations = [ - migrations.CreateModel( - name='UserAccount', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to='pinax_stripe.Account')), - ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to='pinax_stripe.Customer')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.AddField( - model_name='customer', - name='users', - field=models.ManyToManyField(related_name='customers', related_query_name='customers', through='pinax_stripe.UserAccount', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterUniqueTogether( - name='useraccount', - unique_together=set([('user', 'account')]), - ), - ] From 5cce8b4b0d76a98cdba7e5238831183eb846f9b8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 10 Nov 2017 01:27:35 +0100 Subject: [PATCH 091/153] next: fix CustomersTests --- pinax/stripe/tests/test_actions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index acc528a62..1789d868e 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -320,7 +320,6 @@ def setUp(self): interval_count=1, name="Pro" ) - self.account = Account.objects.create(stripe_id="acc_XXX") def test_get_customer_for_user(self): expected = Customer.objects.create(stripe_id="x", user=self.user) From 63ac683b32c64247339cf68844b35f49acbc4e19 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 14 Nov 2017 11:53:55 +0100 Subject: [PATCH 092/153] next: revert on_behalf_of/stripe_account hack (#62) --- pinax/stripe/actions/charges.py | 4 +--- pinax/stripe/tests/test_actions.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pinax/stripe/actions/charges.py b/pinax/stripe/actions/charges.py index 07c6d17f4..63f82b8d3 100644 --- a/pinax/stripe/actions/charges.py +++ b/pinax/stripe/actions/charges.py @@ -125,9 +125,7 @@ def create( application_fee, currency ) elif on_behalf_of: - # XXX: cleanup: on_behalf_of is not required likely, but only - # stripe_account - kwargs["stripe_account"] = on_behalf_of + kwargs["on_behalf_of"] = on_behalf_of stripe_charge = stripe.Charge.create( **kwargs ) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index e08dbdee1..40bfbcea4 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -216,7 +216,7 @@ def test_create_with_on_behalf_of(self, CreateMock, SyncMock, SendReceiptMock): ) self.assertTrue(CreateMock.called) _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["stripe_account"], "account") + self.assertEqual(kwargs["on_behalf_of"], "account") self.assertTrue(SyncMock.called) self.assertTrue(SendReceiptMock.called) From feb75b4436a2788b3359be25041e1f763f001c54 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 14 Nov 2017 15:47:23 +0100 Subject: [PATCH 093/153] Squash migrations: 0010_connect_squashed_0026_merge_20171106_2259 (#64) --- ...nnect_squashed_0026_merge_20171106_2259.py | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 pinax/stripe/migrations/0010_connect_squashed_0026_merge_20171106_2259.py diff --git a/pinax/stripe/migrations/0010_connect_squashed_0026_merge_20171106_2259.py b/pinax/stripe/migrations/0010_connect_squashed_0026_merge_20171106_2259.py new file mode 100644 index 000000000..df03e91fe --- /dev/null +++ b/pinax/stripe/migrations/0010_connect_squashed_0026_merge_20171106_2259.py @@ -0,0 +1,354 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-11-14 14:13 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.migrations.operations.special +import django.db.models.deletion +import django.utils.timezone +import jsonfield.fields + + +class Migration(migrations.Migration): + + replaces = [('pinax_stripe', '0010_connect'), ('pinax_stripe', '0011_subscription_connect'), ('pinax_stripe', '0012_user_account'), ('pinax_stripe', '0013_revert_0011'), ('pinax_stripe', '0014_auto_20171018_1024'), ('pinax_stripe', '0015_account-as-foreign-key'), ('pinax_stripe', '0016_remove-user-account-account'), ('pinax_stripe', '0017_user-account-account'), ('pinax_stripe', '0018_add_discounts'), ('pinax_stripe', '0011_auto_20171024_1209'), ('pinax_stripe', '0011_auto_20171017_1234'), ('pinax_stripe', '0012_merge_20171025_1443'), ('pinax_stripe', '0019_merge_20171025_1519'), ('pinax_stripe', '0020_account-publishablke-key'), ('pinax_stripe', '0021_account-authorized'), ('pinax_stripe', '0022_sub-account-related'), ('pinax_stripe', '0011_account-as-foreign-key'), ('pinax_stripe', '0012_merge_20171026_1310'), ('pinax_stripe', '0013_auto_20171027_1443'), ('pinax_stripe', '0023_merge_20171027_1626'), ('pinax_stripe', '0013_auto_20171025_2153'), ('pinax_stripe', '0011_account_publishable_key'), ('pinax_stripe', '0014_merge_20171030_1554'), ('pinax_stripe', '0015_merge_20171030_1852'), ('pinax_stripe', '0024_merge_20171030_1941'), ('pinax_stripe', '0025_remove_subscription_stripe_account'), ('pinax_stripe', '0016_auto_20171106_1234'), ('pinax_stripe', '0014_auto_20171026_1304'), ('pinax_stripe', '0017_merge_20171106_2201'), ('pinax_stripe', '0026_merge_20171106_2259')] + + dependencies = [ + ('pinax_stripe', '0009_auto_20170825_1841'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Account', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_id', models.CharField(max_length=191, unique=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('business_name', models.TextField(blank=True, null=True)), + ('business_url', models.TextField(blank=True, null=True)), + ('charges_enabled', models.BooleanField(default=False)), + ('country', models.CharField(max_length=2)), + ('debit_negative_balances', models.BooleanField(default=False)), + ('decline_charge_on_avs_failure', models.BooleanField(default=False)), + ('decline_charge_on_cvc_failure', models.BooleanField(default=False)), + ('default_currency', models.CharField(max_length=3)), + ('details_submitted', models.BooleanField(default=False)), + ('display_name', models.TextField(blank=True, null=True)), + ('email', models.TextField(blank=True, null=True)), + ('legal_entity_address_city', models.TextField(blank=True, null=True)), + ('legal_entity_address_country', models.TextField(blank=True, null=True)), + ('legal_entity_address_line1', models.TextField(blank=True, null=True)), + ('legal_entity_address_line2', models.TextField(blank=True, null=True)), + ('legal_entity_address_postal_code', models.TextField(blank=True, null=True)), + ('legal_entity_address_state', models.TextField(blank=True, null=True)), + ('legal_entity_dob', models.DateField(null=True)), + ('legal_entity_first_name', models.TextField(blank=True, null=True)), + ('legal_entity_gender', models.TextField(blank=True, null=True)), + ('legal_entity_last_name', models.TextField(blank=True, null=True)), + ('legal_entity_maiden_name', models.TextField(blank=True, null=True)), + ('legal_entity_personal_id_number_provided', models.BooleanField(default=False)), + ('legal_entity_phone_number', models.TextField(blank=True, null=True)), + ('legal_entity_ssn_last_4_provided', models.BooleanField(default=False)), + ('legal_entity_type', models.TextField(blank=True, null=True)), + ('legal_entity_verification_details', models.TextField(blank=True, null=True)), + ('legal_entity_verification_details_code', models.TextField(blank=True, null=True)), + ('legal_entity_verification_document', models.TextField(blank=True, null=True)), + ('legal_entity_verification_status', models.TextField(blank=True, null=True)), + ('type', models.TextField(blank=True, null=True)), + ('metadata', jsonfield.fields.JSONField(null=True)), + ('product_description', models.TextField(blank=True, null=True)), + ('statement_descriptor', models.TextField(blank=True, null=True)), + ('support_email', models.TextField(blank=True, null=True)), + ('support_phone', models.TextField(blank=True, null=True)), + ('timezone', models.TextField(blank=True, null=True)), + ('tos_acceptance_date', models.DateField(null=True)), + ('tos_acceptance_ip', models.TextField(blank=True, null=True)), + ('tos_acceptance_user_agent', models.TextField(blank=True, null=True)), + ('transfer_schedule_delay_days', models.PositiveSmallIntegerField(null=True)), + ('transfer_schedule_interval', models.TextField(blank=True, null=True)), + ('transfer_schedule_monthly_anchor', models.PositiveSmallIntegerField(null=True)), + ('transfer_schedule_weekly_anchor', models.TextField(blank=True, null=True)), + ('transfer_statement_descriptor', models.TextField(blank=True, null=True)), + ('transfers_enabled', models.BooleanField(default=False)), + ('verification_disabled_reason', models.TextField(blank=True, null=True)), + ('verification_due_by', models.DateTimeField(blank=True, null=True)), + ('verification_timestamp', models.DateTimeField(blank=True, null=True)), + ('verification_fields_needed', jsonfield.fields.JSONField(null=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stripe_accounts', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='BankAccount', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_id', models.CharField(max_length=191, unique=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('account_holder_name', models.TextField()), + ('account_holder_type', models.TextField()), + ('bank_name', models.TextField(blank=True, null=True)), + ('country', models.TextField()), + ('currency', models.TextField()), + ('default_for_currency', models.BooleanField(default=False)), + ('fingerprint', models.TextField()), + ('last4', models.CharField(max_length=4)), + ('metadata', jsonfield.fields.JSONField(null=True)), + ('routing_number', models.TextField()), + ('status', models.TextField()), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bank_accounts', to='pinax_stripe.Account')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='charge', + name='available', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='charge', + name='available_on', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='charge', + name='fee', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True), + ), + migrations.AddField( + model_name='charge', + name='fee_currency', + field=models.CharField(blank=True, max_length=10, null=True), + ), + migrations.AddField( + model_name='charge', + name='transfer_group', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='transfer', + name='amount_reversed', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True), + ), + migrations.AddField( + model_name='transfer', + name='application_fee', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True), + ), + migrations.AddField( + model_name='transfer', + name='created', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='transfer', + name='destination', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='transfer', + name='destination_payment', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='transfer', + name='failure_code', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='transfer', + name='failure_message', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='transfer', + name='livemode', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='transfer', + name='metadata', + field=jsonfield.fields.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name='transfer', + name='method', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='transfer', + name='reversed', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='transfer', + name='source_transaction', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='transfer', + name='source_type', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='transfer', + name='statement_descriptor', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='transfer', + name='transfer_group', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='transfer', + name='type', + field=models.TextField(blank=True, null=True), + ), + migrations.CreateModel( + name='UserAccount', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to='pinax_stripe.Customer')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='customer', + name='users', + field=models.ManyToManyField(related_name='customers', related_query_name='customers', through='pinax_stripe.UserAccount', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='useraccount', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to='pinax_stripe.Account'), + preserve_default=False, + ), + migrations.AlterUniqueTogether( + name='useraccount', + unique_together=set([('user', 'account')]), + ), + 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.AlterField( + model_name='coupon', + name='metadata', + field=jsonfield.fields.JSONField(blank=True, null=True), + ), + 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'), + ), + migrations.AlterField( + model_name='account', + name='display_name', + field=models.TextField(default=''), + preserve_default=False, + ), + migrations.AlterField( + model_name='account', + name='metadata', + field=jsonfield.fields.JSONField(blank=True, null=True), + ), + migrations.AlterField( + model_name='account', + name='verification_fields_needed', + field=jsonfield.fields.JSONField(blank=True, null=True), + ), + migrations.AlterField( + model_name='bankaccount', + name='metadata', + field=jsonfield.fields.JSONField(blank=True, null=True), + ), + migrations.AlterField( + model_name='event', + name='validated_message', + field=jsonfield.fields.JSONField(blank=True, null=True), + ), + migrations.AlterField( + model_name='plan', + name='metadata', + field=jsonfield.fields.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name='account', + name='stripe_publishable_key', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='account', + name='authorized', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='customer', + name='stripe_account', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Account'), + ), + migrations.AddField( + model_name='event', + name='stripe_account', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Account'), + ), + migrations.AddField( + model_name='plan', + name='stripe_account', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Account'), + ), + migrations.AddField( + model_name='transfer', + 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='customer', + name='account_balance', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True), + ), + migrations.AlterField( + model_name='customer', + name='user', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] From fd6eba44f6870b3e7ac48ddd61c612ad176664bf Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Tue, 14 Nov 2017 18:12:34 +0100 Subject: [PATCH 094/153] Already provided with AccountListFilter --- pinax/stripe/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 43e8704bb..1f441f768 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -261,7 +261,6 @@ def subscription_status(obj): ], search_fields=[ "stripe_id", - "=stripe_account__stripe_id" ] + user_search_fields(), inlines=[ SubscriptionInline, From 4a99a0e123c4793b9857f1c13dad7ed0e25f3e3d Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Tue, 14 Nov 2017 23:14:30 +0100 Subject: [PATCH 095/153] Probably a leftover of conflict solving --- pinax/stripe/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 5f4ad1f94..9b7c249f7 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -575,8 +575,6 @@ class Account(StripeObject): stripe_publishable_key = models.CharField(null=True, blank=True, max_length=100) - stripe_publishable_key = models.CharField(null=True, blank=True, max_length=100) - product_description = models.TextField(null=True, blank=True) statement_descriptor = models.TextField(null=True, blank=True) support_email = models.TextField(null=True, blank=True) From ea6ad0dd507ad8a223740db25cd5228d12027f16 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 15 Nov 2017 09:45:16 +0100 Subject: [PATCH 096/153] delete unnecessary diff with master --- pinax/stripe/actions/subscriptions.py | 2 +- pinax/stripe/admin.py | 6 +++--- pinax/stripe/tests/test_actions.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index 9b151b69d..3f3e4bff4 100644 --- a/pinax/stripe/actions/subscriptions.py +++ b/pinax/stripe/actions/subscriptions.py @@ -156,7 +156,7 @@ def sync_subscription_from_stripe_data(customer, subscription): start=utils.convert_tstamp(subscription["start"]), status=subscription["status"], trial_start=utils.convert_tstamp(subscription["trial_start"]) if subscription["trial_start"] else None, - trial_end=utils.convert_tstamp(subscription["trial_end"]) if subscription["trial_end"] else None, + trial_end=utils.convert_tstamp(subscription["trial_end"]) if subscription["trial_end"] else None ) sub, created = models.Subscription.objects.get_or_create( stripe_id=subscription["id"], diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 1f441f768..b89f9e948 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -306,7 +306,7 @@ def customer_user(obj): "period_start", "period_end", "subtotal", - "total", + "total" ], search_fields=[ "stripe_id", @@ -321,11 +321,11 @@ def customer_user(obj): "created_at", "date", "period_end", - "total", + "total" ], inlines=[ InvoiceItemInline - ], + ] ) admin.site.register( diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 99228cb7f..0f39573b3 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -663,7 +663,7 @@ class EventsTests(TestCase): @classmethod def setUpClass(cls): super(EventsTests, cls).setUpClass() - cls.account = Account.objects.create(stripe_id="acc_XXX") + cls.account = Account.objects.create(stripe_id="acc_001") def test_dupe_event_exists(self): Event.objects.create(stripe_id="evt_003", kind="foo", livemode=True, webhook_message="{}", api_version="", request="", pending_webhooks=0) From 4c2f00a3410bc7b2a0df5f1fcc9c68bbac45bc66 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 15 Nov 2017 15:28:12 +0100 Subject: [PATCH 097/153] AccountsSyncTestCase: use setUpClass --- pinax/stripe/tests/test_actions.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 258d67562..a963e41e8 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -2887,8 +2887,11 @@ def test_transfer_create_with_stripe_account(self, CreateMock): class AccountsSyncTestCase(TestCase): - def setUp(self): - self.custom_account_data = json.loads( + @classmethod + def setUpClass(cls): + super(AccountsSyncTestCase, cls).setUpClass() + + cls.custom_account_data = json.loads( """{ "type":"custom", "tos_acceptance":{ @@ -2993,7 +2996,7 @@ def setUp(self): "disabled_reason":null } }""") - self.custom_account_data_no_dob_no_verification_no_tosacceptance = json.loads( + cls.custom_account_data_no_dob_no_verification_no_tosacceptance = json.loads( """{ "type":"custom", "tos_acceptance":{ @@ -3089,7 +3092,7 @@ def setUp(self): "disabled_reason":null } }""") - self.not_custom_account_data = json.loads( + cls.not_custom_account_data = json.loads( """{ "support_phone":"7788188181", "business_name":"Woop Woop", From 148b1cab1c5fa4b2cf41bcce482294664dd49d3e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 15 Nov 2017 15:28:53 +0100 Subject: [PATCH 098/153] AccountsSyncTestCase: sort not_custom_account_data fixture --- pinax/stripe/tests/test_actions.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index a963e41e8..e0cb55018 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -3094,24 +3094,24 @@ def setUpClass(cls): }""") cls.not_custom_account_data = json.loads( """{ - "support_phone":"7788188181", + "business_logo":null, "business_name":"Woop Woop", "business_url":"https://www.someurl.com", - "support_url":"https://support.someurl.com", - "country":"CA", - "object":"account", - "business_logo":null, "charges_enabled":true, - "support_email":"support@someurl.com", + "country":"CA", + "default_currency":"cad", "details_submitted":true, + "display_name":"Some Company", "email":"operations@someurl.com", - "transfers_enabled":true, - "timezone":"Etc/UTC", "id":"acct_102t2K2m3chDH8uL", - "display_name":"Some Company", + "object":"account", "statement_descriptor":"SOME COMP", - "type":"standard", - "default_currency":"cad" + "support_email":"support@someurl.com", + "support_phone":"7788188181", + "support_url":"https://support.someurl.com", + "timezone":"Etc/UTC", + "transfers_enabled":true, + "type":"standard" }""") def assert_common_attributes(self, account): From 8d870558e3628340e5f160ce0c817ce5d0cbf7cf Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 15 Nov 2017 15:32:28 +0100 Subject: [PATCH 099/153] AccountsSyncTestCase: fix/update not_custom_account_data fixture "transfers_enabled" is not in there by default?! Might only be there after/when rejecting an account (https://stripe.com/docs/api#reject_account). Adding "payouts_enabled" and "support_address". --- pinax/stripe/tests/test_actions.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index e0cb55018..81cf9a35f 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -3105,12 +3105,20 @@ def setUpClass(cls): "email":"operations@someurl.com", "id":"acct_102t2K2m3chDH8uL", "object":"account", + "payouts_enabled": true, "statement_descriptor":"SOME COMP", + "support_address": { + "city": null, + "country": "DE", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, "support_email":"support@someurl.com", "support_phone":"7788188181", "support_url":"https://support.someurl.com", "timezone":"Etc/UTC", - "transfers_enabled":true, "type":"standard" }""") From 8a4934a5b3913d55784dcab33db62138e559051a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 15 Nov 2017 15:57:20 +0100 Subject: [PATCH 100/153] sync_account_from_stripe_data: only set attributes that are in data --- pinax/stripe/actions/accounts.py | 3 ++- pinax/stripe/tests/test_actions.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pinax/stripe/actions/accounts.py b/pinax/stripe/actions/accounts.py index f0c0fce7b..9d379fb5b 100644 --- a/pinax/stripe/actions/accounts.py +++ b/pinax/stripe/actions/accounts.py @@ -114,7 +114,8 @@ def sync_account_from_stripe_data(data, user=None): top_level_attrs = common_attrs for a in top_level_attrs: - setattr(obj, a, data.get(a)) + if a in data: + setattr(obj, a, data.get(a)) # that's all we get for standard and express accounts! if data["type"] != "custom": diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 81cf9a35f..82d6f3e2a 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -3130,7 +3130,6 @@ def assert_common_attributes(self, account): self.assertEqual(account.support_email, "support@someurl.com") self.assertEqual(account.details_submitted, True) self.assertEqual(account.email, "operations@someurl.com") - self.assertEqual(account.transfers_enabled, True) self.assertEqual(account.timezone, "Etc/UTC") self.assertEqual(account.display_name, "Some Company") self.assertEqual(account.statement_descriptor, "SOME COMP") From 88846fc6e8d34a196dc5b1532089488f1e0cff9f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 15 Nov 2017 16:41:57 +0100 Subject: [PATCH 101/153] please mccabe --- pinax/stripe/actions/accounts.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pinax/stripe/actions/accounts.py b/pinax/stripe/actions/accounts.py index 9d379fb5b..c3e1686ea 100644 --- a/pinax/stripe/actions/accounts.py +++ b/pinax/stripe/actions/accounts.py @@ -113,9 +113,8 @@ def sync_account_from_stripe_data(data, user=None): else: top_level_attrs = common_attrs - for a in top_level_attrs: - if a in data: - setattr(obj, a, data.get(a)) + for a in [x for x in top_level_attrs if x in data]: + setattr(obj, a, data.get(a)) # that's all we get for standard and express accounts! if data["type"] != "custom": From 936198eeaaac1d923244a95428661b77f4726246 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 15 Nov 2017 18:11:06 +0100 Subject: [PATCH 102/153] Retrieve Charge for capture with account --- pinax/stripe/actions/charges.py | 5 ++++- pinax/stripe/tests/test_actions.py | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/actions/charges.py b/pinax/stripe/actions/charges.py index 63f82b8d3..120230598 100644 --- a/pinax/stripe/actions/charges.py +++ b/pinax/stripe/actions/charges.py @@ -36,7 +36,10 @@ def capture(charge, amount=None, idempotency_key=None): amount if amount else charge.amount, charge.currency ) - stripe_charge = stripe.Charge(charge.stripe_id).capture( + stripe_charge = stripe.Charge( + charge.stripe_id, + stripe_account=charge.stripe_account_stripe_id, + ).capture( amount=amount, idempotency_key=idempotency_key, expand=["balance_transaction"], diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 0f39573b3..5bd033e88 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -279,6 +279,15 @@ def test_capture_with_amount(self, CaptureMock, SyncMock): self.assertEquals(kwargs["idempotency_key"], "IDEM") self.assertTrue(SyncMock.called) + @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") + @patch("stripe.Charge.capture") + def test_capture_with_connect(self, CaptureMock, SyncMock): + account = Account(stripe_id="acc_001") + customer = Customer(stripe_id="cus_001", stripe_account=account) + charges.capture(Charge(stripe_id="ch_A", amount=decimal.Decimal("100"), currency="usd", customer=customer)) + self.assertTrue(CaptureMock.called) + self.assertTrue(SyncMock.called) + @patch("pinax.stripe.actions.charges.sync_charge") def test_update_availability(self, SyncMock): Charge.objects.create(customer=self.customer, amount=decimal.Decimal("100"), currency="usd", paid=True, captured=True, available=False, refunded=False) From 312dca788cead280a74546cbbab4003111b946cb Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 15 Nov 2017 18:52:58 +0100 Subject: [PATCH 103/153] Make customer repr account aware --- pinax/stripe/models.py | 24 ++++++++++++++++++------ pinax/stripe/tests/test_models.py | 21 ++++++++++++++++++--- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 9b7c249f7..2a36f0269 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -285,14 +285,26 @@ def stripe_customer(self): ) def __str__(self): - return str(self.user) + if self.user: + return str(self.user) + elif self.id: + return ", ".join(str(user) for user in self.users.all()) + return "No User(s)" def __repr__(self): - return "Customer(pk={!r}, user={!r}, stripe_id={!r})".format( - self.pk, - self.user, - str(self.stripe_id), - ) + if self.user: + return "Customer(pk={!r}, user={!r}, stripe_id={!r})".format( + self.pk, + self.user, + self.stripe_id, + ) + elif self.id: + return "Customer(pk={!r}, users={}, stripe_id={!r})".format( + self.pk, + ", ".join(repr(user) for user in self.users.all()), + self.stripe_id, + ) + return "Customer(pk={!r}, stripe_id={!r})".format(self.pk, self.stripe_id) class Card(StripeObject): diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index 4d9fe6d15..c66793daf 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -64,8 +64,23 @@ def test_event_str_and_repr(self): def test_customer_str_and_repr(self): c = Customer() - self.assertTrue("None" in str(c)) - self.assertEquals(repr(c), "Customer(pk=None, user=None, stripe_id='')") + self.assertTrue("No User(s)" in str(c)) + self.assertEquals(repr(c), "Customer(pk=None, stripe_id='')") + + def test_customer_with_user_str_and_repr(self): + User = get_user_model() + c = Customer(user=User()) + self.assertEqual(str(c), "") + self.assertEqual(repr(c), "Customer(pk=None, user=, stripe_id='')") + + def test_connected_customer_str_and_repr(self): + User = get_user_model() + user = User.objects.create() + account = Account.objects.create(stripe_id="acc_A") + customer = Customer.objects.create(stripe_id="cus_A", stripe_account=account) + UserAccount.objects.create(customer=customer, user=user, account=account) + self.assertEqual(str(customer), "") + self.assertEqual(repr(customer), "Customer(pk={c.pk}, users=, stripe_id='cus_A')".format(c=customer)) def test_charge_repr(self): charge = Charge() @@ -201,7 +216,7 @@ def test_user_account_repr(self): self.assertEquals( repr(ua), "UserAccount(pk=None, user=, account=Account(pk=None, display_name='', type=None, stripe_id='', authorized=True)" - ", customer=Customer(pk=None, user=None, stripe_id=''))") + ", customer=Customer(pk=None, stripe_id=''))") class StripeObjectTests(TestCase): From 193b641e0c53953cd79f01ca5926a578649a8e0e Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 15 Nov 2017 19:31:53 +0100 Subject: [PATCH 104/153] Rename fields per stripe version 2017-04-06 --- pinax/stripe/actions/accounts.py | 16 ++++++++-------- pinax/stripe/admin.py | 2 +- pinax/stripe/models.py | 20 ++++++++++++-------- pinax/stripe/tests/test_actions.py | 24 ++++++++++++------------ 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/pinax/stripe/actions/accounts.py b/pinax/stripe/actions/accounts.py index c3e1686ea..5ca658cdf 100644 --- a/pinax/stripe/actions/accounts.py +++ b/pinax/stripe/actions/accounts.py @@ -100,12 +100,12 @@ def sync_account_from_stripe_data(data, user=None): "business_name", "business_url", "charges_enabled", "country", "default_currency", "details_submitted", "display_name", "email", "type", "statement_descriptor", "support_email", - "support_phone", "timezone", "transfers_enabled" + "support_phone", "timezone", "payouts_enabled" ) custom_attrs = ( "debit_negative_balances", "metadata", "product_description", - "transfer_statement_descriptor" + "payout_statement_descriptor" ) if data["type"] == "custom": @@ -114,7 +114,7 @@ def sync_account_from_stripe_data(data, user=None): top_level_attrs = common_attrs for a in [x for x in top_level_attrs if x in data]: - setattr(obj, a, data.get(a)) + setattr(obj, a, data[a]) # that's all we get for standard and express accounts! if data["type"] != "custom": @@ -188,11 +188,11 @@ def sync_account_from_stripe_data(data, user=None): obj.decline_charge_on_cvc_failure = data["decline_charge_on"]["cvc_failure"] # transfer schedule to external account - ts = data["transfer_schedule"] - obj.transfer_schedule_interval = ts["interval"] - obj.transfer_schedule_delay_days = ts.get("delay_days") - obj.transfer_schedule_weekly_anchor = ts.get("weekly_anchor") - obj.transfer_schedule_monthly_anchor = ts.get("monthly_anchor") + ps = data["payout_schedule"] + obj.payout_schedule_interval = ps["interval"] + obj.payout_schedule_delay_days = ps.get("delay_days") + obj.payout_schedule_weekly_anchor = ps.get("weekly_anchor") + obj.payout_schedule_monthly_anchor = ps.get("monthly_anchor") # verification status, key to progressing account setup obj.verification_disabled_reason = data["verification"]["disabled_reason"] diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index b89f9e948..5cdb5584e 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -438,7 +438,7 @@ class TransferChargeFeeInline(admin.TabularInline): "stripe_id", "type", "country", - "transfers_enabled", + "payouts_enabled", "charges_enabled" ], search_fields=[ diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 2a36f0269..0e5459d39 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -545,6 +545,12 @@ def card(self): @python_2_unicode_compatible class Account(StripeObject): + INTERVAL_CHOICES = ( + ("Manual", "manual"), + ("Daily", "daily"), + ("Weekly", "weekly"), + ("Monthly", "monthly"), + ) user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.CASCADE, related_name="stripe_accounts") business_name = models.TextField(blank=True, null=True) @@ -598,14 +604,12 @@ class Account(StripeObject): tos_acceptance_ip = models.TextField(null=True, blank=True) tos_acceptance_user_agent = models.TextField(null=True, blank=True) - transfer_schedule_delay_days = models.PositiveSmallIntegerField(null=True) - transfer_schedule_interval = models.TextField(null=True, blank=True) - - transfer_schedule_monthly_anchor = models.PositiveSmallIntegerField(null=True) - transfer_schedule_weekly_anchor = models.TextField(null=True, blank=True) - - transfer_statement_descriptor = models.TextField(null=True, blank=True) - transfers_enabled = models.BooleanField(default=False) + payout_schedule_delay_days = models.PositiveSmallIntegerField(null=True, blank=True) + payout_schedule_interval = models.CharField(max_length=7, choices=INTERVAL_CHOICES, null=True, blank=True) + payout_schedule_monthly_anchor = models.PositiveSmallIntegerField(null=True, blank=True) + payout_schedule_weekly_anchor = models.TextField(null=True, blank=True) + payout_statement_descriptor = models.TextField(null=True, blank=True) + payouts_enabled = models.BooleanField(default=False) verification_disabled_reason = models.TextField(null=True, blank=True) verification_due_by = models.DateTimeField(null=True, blank=True) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index ac91730c3..a25ab5ce8 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -3022,14 +3022,14 @@ def setUpClass(cls): "timezone":"Etc/UTC", "statement_descriptor":"SOME COMP", "default_currency":"cad", - "transfer_schedule":{ + "payout_schedule":{ "delay_days":3, "interval":"manual" }, "display_name":"Some Company", - "transfer_statement_descriptor": "For reals", + "payout_statement_descriptor": "For reals", "id":"acct_1A39IGDwqdd5icDO", - "transfers_enabled":true, + "payouts_enabled":true, "external_accounts":{ "has_more":false, "total_count":1, @@ -3127,14 +3127,14 @@ def setUpClass(cls): "timezone":"Etc/UTC", "statement_descriptor":"SOME COMP", "default_currency":"cad", - "transfer_schedule":{ + "payout_schedule":{ "delay_days":3, "interval":"manual" }, "display_name":"Some Company", - "transfer_statement_descriptor": "For reals", + "payout_statement_descriptor": "For reals", "id":"acct_1A39IGDwqdd5icDO", - "transfers_enabled":true, + "payouts_enabled":true, "external_accounts":{ "has_more":false, "total_count":1, @@ -3259,7 +3259,7 @@ def assert_custom_attributes(self, account, dob=None, verification=None, accepta self.assertEqual(account.debit_negative_balances, False) self.assertEqual(account.product_description, "Monkey Magic") self.assertEqual(account.metadata, {"user_id": "9428"}) - self.assertEqual(account.transfer_statement_descriptor, "For reals") + self.assertEqual(account.payout_statement_descriptor, "For reals") # legal entity self.assertEqual(account.legal_entity_address_city, "Vancouver") @@ -3301,11 +3301,11 @@ def assert_custom_attributes(self, account, dob=None, verification=None, accepta self.assertEqual(account.decline_charge_on_avs_failure, True) self.assertEqual(account.decline_charge_on_cvc_failure, True) - # transfer schedule - self.assertEqual(account.transfer_schedule_interval, "manual") - self.assertEqual(account.transfer_schedule_delay_days, 3) - self.assertEqual(account.transfer_schedule_weekly_anchor, None) - self.assertEqual(account.transfer_schedule_monthly_anchor, None) + # Payout schedule + self.assertEqual(account.payout_schedule_interval, "manual") + self.assertEqual(account.payout_schedule_delay_days, 3) + self.assertEqual(account.payout_schedule_weekly_anchor, None) + self.assertEqual(account.payout_schedule_monthly_anchor, None) # verification status, key to progressing account setup self.assertEqual(account.verification_disabled_reason, None) From 3e9c726cf5b966424db52a927549518e6898427e Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Wed, 15 Nov 2017 19:31:59 +0100 Subject: [PATCH 105/153] Migrations --- .../0011_rename_account_transfers_payout.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 pinax/stripe/migrations/0011_rename_account_transfers_payout.py diff --git a/pinax/stripe/migrations/0011_rename_account_transfers_payout.py b/pinax/stripe/migrations/0011_rename_account_transfers_payout.py new file mode 100644 index 000000000..300bfe574 --- /dev/null +++ b/pinax/stripe/migrations/0011_rename_account_transfers_payout.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.8 on 2017-11-15 18:31 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0010_connect_squashed_0026_merge_20171106_2259'), + ] + + operations = [ + migrations.RenameField( + model_name='account', + old_name='transfer_schedule_weekly_anchor', + new_name='payout_schedule_weekly_anchor', + ), + migrations.RenameField( + model_name='account', + old_name='transfer_statement_descriptor', + new_name='payout_statement_descriptor', + ), + migrations.RenameField( + model_name='account', + old_name='transfers_enabled', + new_name='payouts_enabled', + ), + migrations.RemoveField( + model_name='account', + name='transfer_schedule_delay_days', + ), + migrations.RemoveField( + model_name='account', + name='transfer_schedule_interval', + ), + migrations.RemoveField( + model_name='account', + name='transfer_schedule_monthly_anchor', + ), + migrations.AddField( + model_name='account', + name='payout_schedule_delay_days', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='account', + name='payout_schedule_interval', + field=models.CharField(blank=True, choices=[('Manual', 'manual'), ('Daily', 'daily'), ('Weekly', 'weekly'), ('Monthly', 'monthly')], max_length=7, null=True), + ), + migrations.AddField( + model_name='account', + name='payout_schedule_monthly_anchor', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + ] From ec4d67e43ebf8d632ba73d4f88da3db73e8a85e2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 16 Nov 2017 12:59:03 +0100 Subject: [PATCH 106/153] Fix AccountApplicationDeauthorizeWebhook for non-account events --- pinax/stripe/tests/test_webhooks.py | 18 ++++++++++++++++- pinax/stripe/webhooks.py | 30 ++++++++++++++++------------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index dbeb7073b..dcc2e80c0 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -662,12 +662,14 @@ def test_process_deauthorize_fake_response(self, RetrieveMock): @patch("stripe.Event.retrieve") def test_process_deauthorize_with_authorizes_account(self, RetrieveMock): + stripe_account_id = self.account.stripe_id data = {"data": {"object": {"id": "evt_002"}}, - "account": self.account.stripe_id} + "account": stripe_account_id} event = Event.objects.create( kind=AccountApplicationDeauthorizeWebhook.name, webhook_message=data, ) + RetrieveMock.return_value.to_dict.return_value = data with self.assertRaises(ValueError): AccountApplicationDeauthorizeWebhook(event).process() @@ -685,3 +687,17 @@ def test_process_deauthorize_with_delete_account(self, RetrieveMock): self.assertTrue(event.valid) self.assertTrue(event.processed) self.assertIsNone(event.stripe_account) + + @patch("stripe.Event.retrieve") + def test_process_deauthorize_without_account(self, RetrieveMock): + data = {"data": {"object": {"id": "evt_001"}}} + event = Event.objects.create( + kind=AccountApplicationDeauthorizeWebhook.name, + webhook_message=data, + ) + RetrieveMock.return_value.to_dict.return_value = data + AccountApplicationDeauthorizeWebhook(event).process() + self.assertTrue(event.valid) + self.assertTrue(event.processed) + self.account.refresh_from_db() + self.assertTrue(self.account.authorized) diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 65573fb94..d2f582b09 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -152,27 +152,31 @@ def validate(self): """ Specialized validation of incoming events. - We try to retrieve the event: + When this event is for a connected account we should not be able to + fetch the event anymore (since we have been disconnected). + + Therefore we try to retrieve the event: - in case of PermissionError exception, everything is perfectly normal. It means the account has been deauthorized. - In case no exception has been caught, then, most likely, the event has been forged to make you believe the account has been disabled despite it is still functioning. """ - stripe_account_id = self.event.webhook_message["account"] - self.stripe_account = models.Account.objects.filter(stripe_id=stripe_account_id).first() + # if not stripe_account_id: + # return + # self.stripe_account = models.Account.objects.filter(stripe_id=stripe_account_id).first() + try: - stripe.Event.retrieve( - self.event.stripe_id, - stripe_account=stripe_account_id, - ) + super(AccountApplicationDeauthorizeWebhook, self).validate() except stripe.error.PermissionError as exc: - if not(stripe_account_id in str(exc) and obfuscate_secret_key(settings.PINAX_STRIPE_SECRET_KEY) in str(exc)): - raise exc - self.event.valid = True - self.event.validated_message = self.event.webhook_message - self.event.stripe_account = self.stripe_account + if self.stripe_account: + stripe_account_id = self.stripe_account.stripe_id + if not(stripe_account_id in str(exc) and obfuscate_secret_key(settings.PINAX_STRIPE_SECRET_KEY) in str(exc)): + raise exc else: - raise ValueError("The remote account is still valid. This might be a hostile event") + if self.stripe_account: + raise ValueError("The remote account is still valid. This might be a hostile event") + self.event.valid = True + self.event.validated_message = self.event.webhook_message def process_webhook(self): if self.stripe_account is not None: From d831abbb39e3cc7b2c766e4337c134fb94c4c716 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 16 Nov 2017 15:25:04 +0100 Subject: [PATCH 107/153] actions.subscriptions.cancel: skip GET Skip retrieving the Stripe subscription object when deleting it. --- pinax/stripe/actions/subscriptions.py | 7 ++++++- pinax/stripe/tests/test_actions.py | 17 ++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index 3f3e4bff4..49ddf6dcf 100644 --- a/pinax/stripe/actions/subscriptions.py +++ b/pinax/stripe/actions/subscriptions.py @@ -17,7 +17,12 @@ def cancel(subscription, at_period_end=True): subscription: the subscription to cancel at_period_end: True to cancel at the end of the period, otherwise cancels immediately """ - sub = subscription.stripe_subscription.delete(at_period_end=at_period_end) + sub = stripe.Subscription( + subscription.stripe_id, + stripe_account=subscription.stripe_account, + ).delete( + at_period_end=at_period_end, + ) return sync_subscription_from_stripe_data(subscription.customer, sub) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index a25ab5ce8..848d2cb72 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -995,14 +995,25 @@ def test_has_active_subscription_ended_but_not_expired(self): ) self.assertTrue(subscriptions.has_active_subscription(self.customer)) + @patch("stripe.Subscription") @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - def test_cancel_subscription(self, SyncMock): - SubMock = Mock() + def test_cancel_subscription(self, SyncMock, StripeSubMock): + subscription = Subscription(stripe_id="sub_X", customer=self.customer) obj = object() SyncMock.return_value = obj - sub = subscriptions.cancel(SubMock) + sub = subscriptions.cancel(subscription) self.assertIs(sub, obj) self.assertTrue(SyncMock.called) + _, kwargs = StripeSubMock.call_args + self.assertEquals(kwargs["stripe_account"], None) + + @patch("stripe.Subscription") + @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") + def test_cancel_subscription_with_account(self, SyncMock, StripeSubMock): + subscription = Subscription(stripe_id="sub_X", customer=self.connected_customer) + subscriptions.cancel(subscription) + _, kwargs = StripeSubMock.call_args + self.assertEquals(kwargs["stripe_account"], self.account) @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") def test_update(self, SyncMock): From 771e4675dd875257edc83589e7104810ba062b68 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 16 Nov 2017 15:32:57 +0100 Subject: [PATCH 108/153] cleanup --- pinax/stripe/webhooks.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index d2f582b09..6d9c62d7c 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -161,10 +161,6 @@ def validate(self): - In case no exception has been caught, then, most likely, the event has been forged to make you believe the account has been disabled despite it is still functioning. """ - # if not stripe_account_id: - # return - # self.stripe_account = models.Account.objects.filter(stripe_id=stripe_account_id).first() - try: super(AccountApplicationDeauthorizeWebhook, self).validate() except stripe.error.PermissionError as exc: From 0d3c2f2bceb192db818df51804beb60775c1dead Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 16 Nov 2017 19:55:49 +0100 Subject: [PATCH 109/153] Fixup subscriptions.cancel (#81) --- pinax/stripe/actions/subscriptions.py | 2 +- pinax/stripe/tests/test_actions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index 49ddf6dcf..ce24c8602 100644 --- a/pinax/stripe/actions/subscriptions.py +++ b/pinax/stripe/actions/subscriptions.py @@ -19,7 +19,7 @@ def cancel(subscription, at_period_end=True): """ sub = stripe.Subscription( subscription.stripe_id, - stripe_account=subscription.stripe_account, + stripe_account=subscription.stripe_account_stripe_id, ).delete( at_period_end=at_period_end, ) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 848d2cb72..c081febcb 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -1013,7 +1013,7 @@ def test_cancel_subscription_with_account(self, SyncMock, StripeSubMock): subscription = Subscription(stripe_id="sub_X", customer=self.connected_customer) subscriptions.cancel(subscription) _, kwargs = StripeSubMock.call_args - self.assertEquals(kwargs["stripe_account"], self.account) + self.assertEquals(kwargs["stripe_account"], self.account.stripe_id) @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") def test_update(self, SyncMock): From 04ba93c981d8c75e9b1b6d1421f3dfe4badafa7f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 16 Nov 2017 20:27:01 +0100 Subject: [PATCH 110/153] admin: list_select_related+=stripe_account --- pinax/stripe/admin.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 5d1b02daa..d98a8b54f 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -199,6 +199,9 @@ def get_changelist(self, request, **kwargs): "created_at", "stripe_account", ], + list_select_related=[ + "stripe_account", + ], list_filter=[ "kind", "created_at", @@ -251,6 +254,9 @@ class CustomerAdmin(ModelAdmin): "date_purged", "stripe_account", ] + list_select_related = [ + "stripe_account", + ] list_filter = [ "delinquent", CustomerHasCardListFilter, @@ -338,6 +344,9 @@ def customer_user(obj): "trial_period_days", "stripe_account", ], + list_select_related=[ + "stripe_account", + ], search_fields=[ "stripe_id", "name", @@ -414,6 +423,9 @@ class TransferChargeFeeInline(admin.TabularInline): "description", "stripe_account", ], + list_select_related=[ + "stripe_account", + ], search_fields=[ "stripe_id", "event__stripe_id", From 2c521b418c09b76c5893772b0db094ab21090509 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 17 Nov 2017 01:41:26 +0100 Subject: [PATCH 111/153] account.application.deauthorized: remove ValueError The account might still be accessible, e.g. if it was connected multiple times. --- pinax/stripe/tests/test_webhooks.py | 13 ------------- pinax/stripe/webhooks.py | 16 ++++++---------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index dcc2e80c0..caca1298f 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -660,19 +660,6 @@ def test_process_deauthorize_fake_response(self, RetrieveMock): with self.assertRaises(stripe.error.PermissionError): AccountApplicationDeauthorizeWebhook(event).process() - @patch("stripe.Event.retrieve") - def test_process_deauthorize_with_authorizes_account(self, RetrieveMock): - stripe_account_id = self.account.stripe_id - data = {"data": {"object": {"id": "evt_002"}}, - "account": stripe_account_id} - event = Event.objects.create( - kind=AccountApplicationDeauthorizeWebhook.name, - webhook_message=data, - ) - RetrieveMock.return_value.to_dict.return_value = data - with self.assertRaises(ValueError): - AccountApplicationDeauthorizeWebhook(event).process() - @patch("stripe.Event.retrieve") def test_process_deauthorize_with_delete_account(self, RetrieveMock): data = {"data": {"object": {"id": "evt_002"}}, diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 6d9c62d7c..03fc88955 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -154,12 +154,11 @@ def validate(self): When this event is for a connected account we should not be able to fetch the event anymore (since we have been disconnected). + But there might be multiple connections (e.g. for Dev/Prod). - Therefore we try to retrieve the event: - - in case of PermissionError exception, everything is perfectly normal. - It means the account has been deauthorized. - - In case no exception has been caught, then, most likely, the event has been forged - to make you believe the account has been disabled despite it is still functioning. + Therefore we try to retrieve the event, and handle a + PermissionError exception to be expected (since we cannot access the + account anymore). """ try: super(AccountApplicationDeauthorizeWebhook, self).validate() @@ -168,11 +167,8 @@ def validate(self): stripe_account_id = self.stripe_account.stripe_id if not(stripe_account_id in str(exc) and obfuscate_secret_key(settings.PINAX_STRIPE_SECRET_KEY) in str(exc)): raise exc - else: - if self.stripe_account: - raise ValueError("The remote account is still valid. This might be a hostile event") - self.event.valid = True - self.event.validated_message = self.event.webhook_message + self.event.valid = True + self.event.validated_message = self.event.webhook_message def process_webhook(self): if self.stripe_account is not None: From f88b9955a4dfad2746733cb1ac49a0b32387cb7f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 17 Nov 2017 01:52:25 +0100 Subject: [PATCH 112/153] Cleanup migrations --- pinax/stripe/migrations/0013_revert_0011.py | 19 ------ .../migrations/0014_auto_20171018_1024.py | 21 ------- .../migrations/0015_account-as-foreign-key.py | 52 ----------------- .../0016_remove-user-account-account.py | 25 -------- .../migrations/0017_user-account-account.py | 36 ------------ pinax/stripe/migrations/0018_add_discounts.py | 58 ------------------- .../0020_account-publishablke-key.py | 20 ------- .../migrations/0022_sub-account-related.py | 21 ------- .../migrations/0023_merge_20171027_1626.py | 16 ----- .../migrations/0024_merge_20171030_1941.py | 16 ----- ...0025_remove_subscription_stripe_account.py | 19 ------ 11 files changed, 303 deletions(-) delete mode 100644 pinax/stripe/migrations/0013_revert_0011.py delete mode 100644 pinax/stripe/migrations/0014_auto_20171018_1024.py delete mode 100644 pinax/stripe/migrations/0015_account-as-foreign-key.py delete mode 100644 pinax/stripe/migrations/0016_remove-user-account-account.py delete mode 100644 pinax/stripe/migrations/0017_user-account-account.py delete mode 100644 pinax/stripe/migrations/0018_add_discounts.py delete mode 100644 pinax/stripe/migrations/0020_account-publishablke-key.py delete mode 100644 pinax/stripe/migrations/0022_sub-account-related.py delete mode 100644 pinax/stripe/migrations/0023_merge_20171027_1626.py delete mode 100644 pinax/stripe/migrations/0024_merge_20171030_1941.py delete mode 100644 pinax/stripe/migrations/0025_remove_subscription_stripe_account.py diff --git a/pinax/stripe/migrations/0013_revert_0011.py b/pinax/stripe/migrations/0013_revert_0011.py deleted file mode 100644 index e1f092604..000000000 --- a/pinax/stripe/migrations/0013_revert_0011.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.5 on 2017-10-17 14:20 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0012_user_account'), - ] - - operations = [ - migrations.RemoveField( - model_name='subscription', - name='stripe_account', - ), - ] diff --git a/pinax/stripe/migrations/0014_auto_20171018_1024.py b/pinax/stripe/migrations/0014_auto_20171018_1024.py deleted file mode 100644 index ca8f9563d..000000000 --- a/pinax/stripe/migrations/0014_auto_20171018_1024.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-18 10:24 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('pinax_stripe', '0013_revert_0011'), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='useraccount', - unique_together=set([('user', 'account')]), - ), - ] diff --git a/pinax/stripe/migrations/0015_account-as-foreign-key.py b/pinax/stripe/migrations/0015_account-as-foreign-key.py deleted file mode 100644 index 2f843c634..000000000 --- a/pinax/stripe/migrations/0015_account-as-foreign-key.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-19 10:41 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0014_auto_20171018_1024'), - ] - - operations = [ - migrations.RemoveField( - model_name='customer', - name='stripe_account', - ), - migrations.RemoveField( - model_name='event', - name='stripe_account', - ), - migrations.RemoveField( - model_name='plan', - name='stripe_account', - ), - migrations.RemoveField( - model_name='transfer', - name='stripe_account', - ), - migrations.AddField( - model_name='customer', - name='stripe_account', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Account'), - ), - migrations.AddField( - model_name='event', - name='stripe_account', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Account'), - ), - migrations.AddField( - model_name='plan', - name='stripe_account', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Account'), - ), - migrations.AddField( - model_name='transfer', - name='stripe_account', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Account'), - ), - ] diff --git a/pinax/stripe/migrations/0016_remove-user-account-account.py b/pinax/stripe/migrations/0016_remove-user-account-account.py deleted file mode 100644 index 6e4d2449b..000000000 --- a/pinax/stripe/migrations/0016_remove-user-account-account.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-19 14:38 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('pinax_stripe', '0015_account-as-foreign-key'), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='useraccount', - unique_together=set([('user', 'customer')]), - ), - migrations.RemoveField( - model_name='useraccount', - name='account', - ), - ] diff --git a/pinax/stripe/migrations/0017_user-account-account.py b/pinax/stripe/migrations/0017_user-account-account.py deleted file mode 100644 index 1142173f9..000000000 --- a/pinax/stripe/migrations/0017_user-account-account.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# generated by django 1.11.6 on 2017-10-20 07:03 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -def delete_user_accounts(apps, schema_editor): - UserAccount = apps.get_model("pinax_stripe", "UserAccount") - UserAccount.objects.all().delete() - Customer = apps.get_model("pinax_stripe", "Customer") - Customer.objects.all().delete() - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('pinax_stripe', '0016_remove-user-account-account'), - ] - - operations = [ - migrations.RunPython(delete_user_accounts, reverse_code=migrations.RunPython.noop), - migrations.AddField( - model_name='useraccount', - name='account', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to='pinax_stripe.Account'), - preserve_default=False, - ), - migrations.AlterUniqueTogether( - name='useraccount', - unique_together=set([('user', 'account')]), - ), - ] diff --git a/pinax/stripe/migrations/0018_add_discounts.py b/pinax/stripe/migrations/0018_add_discounts.py deleted file mode 100644 index e6b3844c3..000000000 --- a/pinax/stripe/migrations/0018_add_discounts.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-24 09:14 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0017_user-account-account'), - ] - - 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'), - ), - ] diff --git a/pinax/stripe/migrations/0020_account-publishablke-key.py b/pinax/stripe/migrations/0020_account-publishablke-key.py deleted file mode 100644 index 5b551f995..000000000 --- a/pinax/stripe/migrations/0020_account-publishablke-key.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-25 16:15 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0019_merge_20171025_1519'), - ] - - operations = [ - migrations.AddField( - model_name='account', - name='stripe_publishable_key', - field=models.CharField(blank=True, max_length=100, null=True), - ), - ] diff --git a/pinax/stripe/migrations/0022_sub-account-related.py b/pinax/stripe/migrations/0022_sub-account-related.py deleted file mode 100644 index 1ec11e145..000000000 --- a/pinax/stripe/migrations/0022_sub-account-related.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-26 12:13 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0021_account-authorized'), - ] - - operations = [ - migrations.AddField( - model_name='subscription', - name='stripe_account', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Account'), - ), - ] diff --git a/pinax/stripe/migrations/0023_merge_20171027_1626.py b/pinax/stripe/migrations/0023_merge_20171027_1626.py deleted file mode 100644 index 845b27b61..000000000 --- a/pinax/stripe/migrations/0023_merge_20171027_1626.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-27 14:26 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0013_auto_20171027_1443'), - ('pinax_stripe', '0022_sub-account-related'), - ] - - operations = [ - ] diff --git a/pinax/stripe/migrations/0024_merge_20171030_1941.py b/pinax/stripe/migrations/0024_merge_20171030_1941.py deleted file mode 100644 index 77f80c3a5..000000000 --- a/pinax/stripe/migrations/0024_merge_20171030_1941.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-30 18:41 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0023_merge_20171027_1626'), - ('pinax_stripe', '0015_merge_20171030_1852'), - ] - - operations = [ - ] diff --git a/pinax/stripe/migrations/0025_remove_subscription_stripe_account.py b/pinax/stripe/migrations/0025_remove_subscription_stripe_account.py deleted file mode 100644 index 8336e33fc..000000000 --- a/pinax/stripe/migrations/0025_remove_subscription_stripe_account.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-11-03 15:07 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0024_merge_20171030_1941'), - ] - - operations = [ - migrations.RemoveField( - model_name='subscription', - name='stripe_account', - ), - ] From 680d1bed3932010af30c40a848c1377e6a4d7b9e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 17 Nov 2017 14:33:39 +0100 Subject: [PATCH 113/153] fixup! Cleanup migrations --- .../migrations/0011_subscription_connect.py | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 pinax/stripe/migrations/0011_subscription_connect.py diff --git a/pinax/stripe/migrations/0011_subscription_connect.py b/pinax/stripe/migrations/0011_subscription_connect.py deleted file mode 100644 index 4c9540a25..000000000 --- a/pinax/stripe/migrations/0011_subscription_connect.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0010_connect'), - ] - - operations = [ - migrations.AddField( - model_name='subscription', - name='stripe_account', - field=models.CharField(blank=True, max_length=255, null=True), - ), - ] From a0d437394f69b8bd69e66e760a5018bb2d1481a3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 20 Nov 2017 21:35:21 +0100 Subject: [PATCH 114/153] Add Coupon.stripe_coupon --- 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 0e5459d39..8dadb8389 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 c66793daf..e648304b1 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, @@ -104,6 +104,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 a6649326a591676b4a3a9dcaa1ce8d015b823432 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 20 Nov 2017 21:42:12 +0100 Subject: [PATCH 115/153] sync_coupon_from_stripe_data: handle livemode --- 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 5a46b328d..32805d120 100644 --- a/pinax/stripe/actions/coupons.py +++ b/pinax/stripe/actions/coupons.py @@ -28,6 +28,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 c081febcb..0521d6a06 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -1218,7 +1218,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": { }, @@ -1228,9 +1228,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) From aa28475fde03786e44692def8b30ed4eda1d3c71 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 21 Nov 2017 17:38:56 +0100 Subject: [PATCH 116/153] Add UniquePerAccountSripeObject for Plans The `stripe_id` for plans is not unique by itself globally (it is not generated by Stripe), and therefore needs to be unique per account. --- .../migrations/0011_auto_20171121_1648.py | 24 +++++++++++++++++++ pinax/stripe/models.py | 10 +++++++- pinax/stripe/tests/test_models.py | 6 +++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 pinax/stripe/migrations/0011_auto_20171121_1648.py diff --git a/pinax/stripe/migrations/0011_auto_20171121_1648.py b/pinax/stripe/migrations/0011_auto_20171121_1648.py new file mode 100644 index 000000000..5782b3ed6 --- /dev/null +++ b/pinax/stripe/migrations/0011_auto_20171121_1648.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-11-21 16:48 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0010_connect'), + ] + + operations = [ + migrations.AlterField( + model_name='plan', + name='stripe_id', + field=models.CharField(max_length=191), + ), + migrations.AlterUniqueTogether( + name='plan', + unique_together=set([('stripe_id', 'stripe_account')]), + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 8dadb8389..e775df87b 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -44,6 +44,14 @@ class Meta: abstract = True +class UniquePerAccountStripeObject(AccountRelatedStripeObject): + stripe_id = models.CharField(max_length=191) + + class Meta: + abstract = True + unique_together = ("stripe_id", "stripe_account") + + class StripeAccountFromCustomerMixin(object): @property def stripe_account(self): @@ -56,7 +64,7 @@ def stripe_account_stripe_id(self): @python_2_unicode_compatible -class Plan(AccountRelatedStripeObject): +class Plan(UniquePerAccountStripeObject): amount = models.DecimalField(decimal_places=2, max_digits=9) currency = models.CharField(max_length=15, blank=False) interval = models.CharField(max_length=15) diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index e648304b1..c4d67c5ba 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -48,6 +48,12 @@ def test_plan_str_jpy(self): p = Plan(amount=decimal.Decimal("5"), name="My Plan", currency="jpy", interval="monthly", interval_count=1) self.assertTrue(u"\u00a5" in _str(p)) + def test_plan_per_account(self): + Plan.objects.create(stripe_id="plan", amount=decimal.Decimal("100"), interval="monthly", interval_count=1) + account = Account.objects.create(stripe_id="acct_A") + Plan.objects.create(stripe_id="plan", stripe_account=account, amount=decimal.Decimal("100"), interval="monthly", interval_count=1) + self.assertEquals(Plan.objects.count(), 2) + def test_event_processing_exception_str(self): e = EventProcessingException(data="hello", message="hi there", traceback="fake") self.assertTrue("Event=" in str(e)) From 38407c0fb9d1273eafe237d3da46e9486b710505 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 21 Nov 2017 17:52:52 +0100 Subject: [PATCH 117/153] merge migrations --- .../migrations/0012_merge_20171121_1652.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pinax/stripe/migrations/0012_merge_20171121_1652.py diff --git a/pinax/stripe/migrations/0012_merge_20171121_1652.py b/pinax/stripe/migrations/0012_merge_20171121_1652.py new file mode 100644 index 000000000..4fb0f708e --- /dev/null +++ b/pinax/stripe/migrations/0012_merge_20171121_1652.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-11-21 16:52 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0011_rename_account_transfers_payout'), + ('pinax_stripe', '0011_auto_20171121_1648'), + ] + + operations = [ + ] From 4d31e12bbaf5d8e8a7dff7c371812842706091e9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 21 Nov 2017 17:47:08 +0100 Subject: [PATCH 118/153] Use UniquePerAccountStripeObject for Coupon --- pinax/stripe/models.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index e775df87b..ca726742d 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -91,20 +91,7 @@ def __repr__(self): @python_2_unicode_compatible -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") - +class Coupon(UniquePerAccountStripeObject): DURATION_CHOICES = ( ("forever", "forever"), ("once", "once"), From c028e17690f6b5d2e3eb1bca6e6665fb1f3150a2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 21 Nov 2017 18:13:55 +0100 Subject: [PATCH 119/153] fixup! Add UniquePerAccountSripeObject for Plans --- pinax/stripe/models.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index ca726742d..0d29d720a 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -26,7 +26,7 @@ class Meta: abstract = True -class AccountRelatedStripeObject(StripeObject): +class AccountRelatedStripeObjectMixin(models.Model): stripe_account = models.ForeignKey( "pinax_stripe.Account", @@ -44,8 +44,16 @@ class Meta: abstract = True -class UniquePerAccountStripeObject(AccountRelatedStripeObject): +class AccountRelatedStripeObject(AccountRelatedStripeObjectMixin, StripeObject): + """Uses a mixin to support Django 1.8 (name clash for stripe_id)""" + + class Meta: + abstract = True + + +class UniquePerAccountStripeObject(AccountRelatedStripeObjectMixin): stripe_id = models.CharField(max_length=191) + created_at = models.DateTimeField(default=timezone.now) class Meta: abstract = True From 8012cbcd7290cc7a236f41c99816b1302e52721e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 22 Nov 2017 12:25:20 +0100 Subject: [PATCH 120/153] Add Plan.stripe_plan property --- pinax/stripe/models.py | 7 +++++++ pinax/stripe/tests/test_models.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 0d29d720a..6ab4dfe97 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -97,6 +97,13 @@ def __repr__(self): str(self.stripe_id), ) + @property + def stripe_plan(self): + return stripe.Plan.retrieve( + self.stripe_id, + stripe_account=self.stripe_account.stripe_id, + ) + @python_2_unicode_compatible class Coupon(UniquePerAccountStripeObject): diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index c4d67c5ba..2cf3f55c2 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -54,6 +54,13 @@ def test_plan_per_account(self): Plan.objects.create(stripe_id="plan", stripe_account=account, amount=decimal.Decimal("100"), interval="monthly", interval_count=1) self.assertEquals(Plan.objects.count(), 2) + @patch("stripe.Plan.retrieve") + def test_plan_stripe_plan(self, RetrieveMock): + c = Plan(stripe_id="plan", stripe_account=Account(stripe_id="acct_A")) + self.assertEqual(c.stripe_plan, RetrieveMock.return_value) + self.assertTrue(RetrieveMock.call_args_list, [ + call("plan", stripe_account="acct_A")]) + def test_event_processing_exception_str(self): e = EventProcessingException(data="hello", message="hi there", traceback="fake") self.assertTrue("Event=" in str(e)) From ce764e6750fe5b4ca43f027960cfcafdbafad335 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 22 Nov 2017 16:33:13 +0100 Subject: [PATCH 121/153] Add Charge.outcome --- pinax/stripe/actions/charges.py | 1 + .../stripe/migrations/0013_charge_outcome.py | 21 +++++++++++++++++++ pinax/stripe/models.py | 1 + pinax/stripe/tests/test_actions.py | 1 + 4 files changed, 24 insertions(+) create mode 100644 pinax/stripe/migrations/0013_charge_outcome.py diff --git a/pinax/stripe/actions/charges.py b/pinax/stripe/actions/charges.py index ee0857403..5e267134c 100644 --- a/pinax/stripe/actions/charges.py +++ b/pinax/stripe/actions/charges.py @@ -203,6 +203,7 @@ def sync_charge_from_stripe_data(data): ) obj.fee_currency = balance_transaction["currency"] obj.transfer_group = data.get("transfer_group") + obj.outcome = data.get("outcome") obj.save() return obj diff --git a/pinax/stripe/migrations/0013_charge_outcome.py b/pinax/stripe/migrations/0013_charge_outcome.py new file mode 100644 index 000000000..ef3229b6c --- /dev/null +++ b/pinax/stripe/migrations/0013_charge_outcome.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-11-22 15:40 +from __future__ import unicode_literals + +from django.db import migrations +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0012_merge_20171121_1652'), + ] + + operations = [ + migrations.AddField( + model_name='charge', + name='outcome', + field=jsonfield.fields.JSONField(blank=True, null=True), + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 6ab4dfe97..925e8b048 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -532,6 +532,7 @@ class Charge(StripeAccountFromCustomerMixin, StripeObject): fee_currency = models.CharField(max_length=10, null=True, blank=True) transfer_group = models.TextField(null=True, blank=True) + outcome = JSONField(null=True, blank=True) objects = ChargeManager() diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 0521d6a06..83cd51407 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -2266,6 +2266,7 @@ def test_sync_charge_from_stripe_data_failed(self): charge = Charge.objects.get(stripe_id=data["id"]) self.assertEqual(charge.amount, decimal.Decimal("2")) self.assertEqual(charge.customer, None) + self.assertEqual(charge.outcome["risk_level"], "normal") @patch("stripe.Subscription.retrieve") def test_retrieve_stripe_subscription(self, RetrieveMock): From 975b9f34d7be5d148f2dd3b24f0b69699096cc59 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 22 Nov 2017 16:48:59 +0100 Subject: [PATCH 122/153] Add Makefile to help with migrations (#90) --- Makefile | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..a2a04cc18 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +.PHONY: makemigrations mergemigrations + +makemigrations mergemigrations: PINAX_STRIPE_DATABASE_ENGINE=django.db.backends.sqlite3 + +makemigrations: + django-admin makemigrations pinax_stripe + +mergemigrations: + django-admin makemigrations --merge pinax_stripe From 4b9c405da63e6427a7f42ac983de66ce5da58d5d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 23 Nov 2017 15:10:23 +0100 Subject: [PATCH 123/153] fixup! Add Plan.stripe_plan property --- pinax/stripe/models.py | 2 +- pinax/stripe/tests/test_models.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 6ab4dfe97..9dcca2555 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -101,7 +101,7 @@ def __repr__(self): def stripe_plan(self): return stripe.Plan.retrieve( self.stripe_id, - stripe_account=self.stripe_account.stripe_id, + stripe_account=self.stripe_account_stripe_id, ) diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index 2cf3f55c2..8bf5ea640 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -56,6 +56,13 @@ def test_plan_per_account(self): @patch("stripe.Plan.retrieve") def test_plan_stripe_plan(self, RetrieveMock): + c = Plan(stripe_id="plan") + self.assertEqual(c.stripe_plan, RetrieveMock.return_value) + self.assertTrue(RetrieveMock.call_args_list, [ + call("plan", stripe_account=None)]) + + @patch("stripe.Plan.retrieve") + def test_plan_stripe_plan_with_account(self, RetrieveMock): c = Plan(stripe_id="plan", stripe_account=Account(stripe_id="acct_A")) self.assertEqual(c.stripe_plan, RetrieveMock.return_value) self.assertTrue(RetrieveMock.call_args_list, [ From f3dae772e38e3826fbdfb67e53b5c8efacd6d820 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 23 Nov 2017 17:25:07 +0100 Subject: [PATCH 124/153] CircleCI: add s/py36dj111psql/py27dj111 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 37d702fab..b7dc9fc78 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -143,5 +143,5 @@ workflows: test: jobs: - lint - - py36dj111psql + - py27dj111 - py36dj20psql From 63dc455d7dcd6ac3f9a5e652656de8a15c65bf74 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 23 Nov 2017 17:14:26 +0100 Subject: [PATCH 125/153] next: merge Discount.__str__ from coupons PR (required for py27) --- pinax/stripe/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 58dae55c3..294396822 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -376,6 +376,9 @@ class Discount(models.Model): 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) From 6c70d611662b99145f416d4714067dda2bb51b84 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 23 Nov 2017 17:23:48 +0100 Subject: [PATCH 126/153] merge more py27 fixes from PRs --- pinax/stripe/models.py | 8 ++++---- pinax/stripe/tests/test_models.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 294396822..27553f5b8 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -140,7 +140,7 @@ def __repr__(self): self.amount_off, self.percent_off, str(self.currency), - self.duration, + str(self.duration), self.livemode, self.max_redemptions, self.times_redeemed, @@ -312,15 +312,15 @@ def __repr__(self): return "Customer(pk={!r}, user={!r}, stripe_id={!r})".format( self.pk, self.user, - self.stripe_id, + str(self.stripe_id), ) elif self.id: return "Customer(pk={!r}, users={}, stripe_id={!r})".format( self.pk, ", ".join(repr(user) for user in self.users.all()), - self.stripe_id, + str(self.stripe_id), ) - return "Customer(pk={!r}, stripe_id={!r})".format(self.pk, self.stripe_id) + return "Customer(pk={!r}, stripe_id={!r})".format(self.pk, str(self.stripe_id)) class Card(StripeObject): diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index 8bf5ea640..be7f4c372 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -113,8 +113,8 @@ def test_plan_display_invoiceitem(self): 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='')") + 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) From ff8941161a7b079e8288f62672996c015069c6ca Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 23 Nov 2017 16:42:26 +0100 Subject: [PATCH 127/153] models: use blank=True with null=True Fixes https://github.com/pinax/pinax-stripe/issues/474. --- pinax/stripe/models.py | 98 ++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 51 deletions(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 4170599d7..5db313d13 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -79,7 +79,7 @@ class Plan(UniquePerAccountStripeObject): interval_count = models.IntegerField() name = models.CharField(max_length=150) statement_descriptor = models.TextField(blank=True) - trial_period_days = models.IntegerField(null=True) + trial_period_days = models.IntegerField(null=True, blank=True) metadata = JSONField(null=True, blank=True) def __str__(self): @@ -112,16 +112,16 @@ class Coupon(UniquePerAccountStripeObject): ("once", "once"), ("repeating", "repeating"), ) - amount_off = models.DecimalField(decimal_places=2, max_digits=9, null=True) + amount_off = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) currency = models.CharField(max_length=10, default="usd") duration = models.CharField(max_length=10, default="once", choices=DURATION_CHOICES) - duration_in_months = models.PositiveIntegerField(null=True) + duration_in_months = models.PositiveIntegerField(null=True, blank=True) livemode = models.BooleanField(default=False) - max_redemptions = models.PositiveIntegerField(null=True) + max_redemptions = models.PositiveIntegerField(null=True, blank=True) metadata = JSONField(null=True, blank=True) - percent_off = models.PositiveIntegerField(null=True) - redeem_by = models.DateTimeField(null=True) - times_redeemed = models.PositiveIntegerField(null=True) + percent_off = models.PositiveIntegerField(null=True, blank=True) + redeem_by = models.DateTimeField(null=True, blank=True) + times_redeemed = models.PositiveIntegerField(null=True, blank=True) valid = models.BooleanField(default=False) def __str__(self): @@ -158,7 +158,7 @@ def stripe_coupon(self): @python_2_unicode_compatible class EventProcessingException(models.Model): - event = models.ForeignKey("Event", null=True, on_delete=models.CASCADE) + event = models.ForeignKey("Event", null=True, blank=True, on_delete=models.CASCADE) data = models.TextField() message = models.CharField(max_length=500) traceback = models.TextField() @@ -173,10 +173,10 @@ class Event(AccountRelatedStripeObject): kind = models.CharField(max_length=250) livemode = models.BooleanField(default=False) - customer = models.ForeignKey("Customer", null=True, on_delete=models.CASCADE) + customer = models.ForeignKey("Customer", null=True, blank=True, on_delete=models.CASCADE) webhook_message = JSONField() validated_message = JSONField(null=True, blank=True) - valid = models.NullBooleanField(null=True) + valid = models.NullBooleanField(null=True, blank=True) processed = models.BooleanField(default=False) request = models.CharField(max_length=100, blank=True) pending_webhooks = models.PositiveIntegerField(default=0) @@ -282,15 +282,15 @@ def __repr__(self): @python_2_unicode_compatible class Customer(AccountRelatedStripeObject): - user = models.OneToOneField(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.CASCADE) + user = models.OneToOneField(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE) users = models.ManyToManyField(settings.AUTH_USER_MODEL, through=UserAccount, related_name="customers", related_query_name="customers") - account_balance = models.DecimalField(decimal_places=2, max_digits=9, blank=True, null=True) + account_balance = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) currency = models.CharField(max_length=10, default="usd", blank=True) delinquent = models.BooleanField(default=False) default_source = models.TextField(blank=True) - date_purged = models.DateTimeField(null=True, editable=False) + date_purged = models.DateTimeField(null=True, blank=True, editable=False) objects = CustomerManager() @@ -378,10 +378,10 @@ class BitcoinReceiver(StripeObject): 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) + customer = models.OneToOneField("Customer", null=True, blank=True, on_delete=models.CASCADE) + subscription = models.OneToOneField("Subscription", null=True, blank=True, on_delete=models.CASCADE) + start = models.DateTimeField(null=True, blank=True) + end = models.DateTimeField(null=True, blank=True) def __str__(self): return "{} - {}".format(self.coupon, self.subscription) @@ -404,18 +404,18 @@ class Subscription(StripeAccountFromCustomerMixin, StripeObject): STATUS_CURRENT = ["trialing", "active"] customer = models.ForeignKey(Customer, on_delete=models.CASCADE) - application_fee_percent = models.DecimalField(decimal_places=2, max_digits=3, default=None, null=True) + application_fee_percent = models.DecimalField(decimal_places=2, max_digits=3, default=None, null=True, blank=True) cancel_at_period_end = models.BooleanField(default=False) - canceled_at = models.DateTimeField(blank=True, null=True) - current_period_end = models.DateTimeField(blank=True, null=True) - current_period_start = models.DateTimeField(blank=True, null=True) - ended_at = models.DateTimeField(blank=True, null=True) + canceled_at = models.DateTimeField(null=True, blank=True) + current_period_end = models.DateTimeField(null=True, blank=True) + current_period_start = models.DateTimeField(null=True, blank=True) + ended_at = models.DateTimeField(null=True, blank=True) plan = models.ForeignKey(Plan, on_delete=models.CASCADE) quantity = models.IntegerField() start = models.DateTimeField() status = models.CharField(max_length=25) # trialing, active, past_due, canceled, or unpaid - trial_end = models.DateTimeField(blank=True, null=True) - trial_start = models.DateTimeField(blank=True, null=True) + trial_end = models.DateTimeField(null=True, blank=True) + trial_start = models.DateTimeField(null=True, blank=True) @property def stripe_subscription(self): @@ -460,9 +460,9 @@ class Invoice(StripeAccountFromCustomerMixin, StripeObject): customer = models.ForeignKey(Customer, related_name="invoices", on_delete=models.CASCADE) amount_due = models.DecimalField(decimal_places=2, max_digits=9) attempted = models.NullBooleanField() - attempt_count = models.PositiveIntegerField(null=True) - charge = models.ForeignKey("Charge", null=True, related_name="invoices", on_delete=models.CASCADE) - subscription = models.ForeignKey(Subscription, null=True, on_delete=models.CASCADE) + attempt_count = models.PositiveIntegerField(null=True, blank=True) + charge = models.ForeignKey("Charge", null=True, blank=True, related_name="invoices", on_delete=models.CASCADE) + subscription = models.ForeignKey(Subscription, null=True, blank=True, on_delete=models.CASCADE) statement_descriptor = models.TextField(blank=True) currency = models.CharField(max_length=10, default="usd") closed = models.BooleanField(default=False) @@ -472,11 +472,11 @@ class Invoice(StripeAccountFromCustomerMixin, StripeObject): period_end = models.DateTimeField() period_start = models.DateTimeField() subtotal = models.DecimalField(decimal_places=2, max_digits=9) - tax = models.DecimalField(decimal_places=2, max_digits=9, null=True) - tax_percent = models.DecimalField(decimal_places=2, max_digits=9, null=True) + tax = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) + tax_percent = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) total = models.DecimalField(decimal_places=2, max_digits=9) date = models.DateTimeField() - webhooks_delivered_at = models.DateTimeField(null=True) + webhooks_delivered_at = models.DateTimeField(null=True, blank=True) @property def status(self): @@ -498,14 +498,14 @@ class InvoiceItem(models.Model): amount = models.DecimalField(decimal_places=2, max_digits=9) currency = models.CharField(max_length=10, default="usd") kind = models.CharField(max_length=25, blank=True) - subscription = models.ForeignKey(Subscription, null=True, on_delete=models.CASCADE) + subscription = models.ForeignKey(Subscription, null=True, blank=True, on_delete=models.CASCADE) period_start = models.DateTimeField() period_end = models.DateTimeField() proration = models.BooleanField(default=False) line_type = models.CharField(max_length=50) description = models.CharField(max_length=200, blank=True) - plan = models.ForeignKey(Plan, null=True, on_delete=models.CASCADE) - quantity = models.IntegerField(null=True) + plan = models.ForeignKey(Plan, null=True, blank=True, on_delete=models.CASCADE) + quantity = models.IntegerField(null=True, blank=True) def plan_display(self): return self.plan.name if self.plan else "" @@ -513,21 +513,17 @@ def plan_display(self): class Charge(StripeAccountFromCustomerMixin, StripeObject): - customer = models.ForeignKey(Customer, null=True, related_name="charges", on_delete=models.CASCADE) - invoice = models.ForeignKey(Invoice, null=True, related_name="charges", on_delete=models.CASCADE) - source = models.CharField(max_length=100) + customer = models.ForeignKey(Customer, null=True, blank=True, related_name="charges", on_delete=models.CASCADE) + invoice = models.ForeignKey(Invoice, null=True, blank=True, related_name="charges", on_delete=models.CASCADE) + source = models.CharField(max_length=100, blank=True) currency = models.CharField(max_length=10, default="usd") - amount = models.DecimalField(decimal_places=2, max_digits=9, null=True) - amount_refunded = models.DecimalField( - decimal_places=2, - max_digits=9, - null=True - ) + amount = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) + amount_refunded = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) description = models.TextField(blank=True) - paid = models.NullBooleanField(null=True) - disputed = models.NullBooleanField(null=True) - refunded = models.NullBooleanField(null=True) - captured = models.NullBooleanField(null=True) + paid = models.NullBooleanField(null=True, blank=True) + disputed = models.NullBooleanField(null=True, blank=True) + refunded = models.NullBooleanField(null=True, blank=True) + captured = models.NullBooleanField(null=True, blank=True) receipt_sent = models.BooleanField(default=False) charge_created = models.DateTimeField(null=True, blank=True) @@ -578,10 +574,10 @@ class Account(StripeObject): ("Weekly", "weekly"), ("Monthly", "monthly"), ) - user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.CASCADE, related_name="stripe_accounts") + user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE, related_name="stripe_accounts") - business_name = models.TextField(blank=True, null=True) - business_url = models.TextField(blank=True, null=True) + business_name = models.TextField(null=True, blank=True) + business_url = models.TextField(null=True, blank=True) charges_enabled = models.BooleanField(default=False) country = models.CharField(max_length=2) @@ -591,7 +587,7 @@ class Account(StripeObject): default_currency = models.CharField(max_length=3) details_submitted = models.BooleanField(default=False) display_name = models.TextField(blank=False, null=False) - email = models.TextField(blank=True, null=True) + email = models.TextField(null=True, blank=True) legal_entity_address_city = models.TextField(null=True, blank=True) legal_entity_address_country = models.TextField(null=True, blank=True) @@ -627,7 +623,7 @@ class Account(StripeObject): timezone = models.TextField(null=True, blank=True) - tos_acceptance_date = models.DateField(null=True) + tos_acceptance_date = models.DateField(null=True, blank=True) tos_acceptance_ip = models.TextField(null=True, blank=True) tos_acceptance_user_agent = models.TextField(null=True, blank=True) From 23b6d8b5f4fa47b2bb683b280df5ccae22549d40 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 23 Nov 2017 16:44:04 +0100 Subject: [PATCH 128/153] migration --- .../stripe/migrations/0014_blank_with_null.py | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 pinax/stripe/migrations/0014_blank_with_null.py diff --git a/pinax/stripe/migrations/0014_blank_with_null.py b/pinax/stripe/migrations/0014_blank_with_null.py new file mode 100644 index 000000000..1763531b6 --- /dev/null +++ b/pinax/stripe/migrations/0014_blank_with_null.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-11-23 15:43 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0013_charge_outcome'), + ] + + operations = [ + migrations.AlterField( + model_name='account', + name='tos_acceptance_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name='charge', + name='amount', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True), + ), + migrations.AlterField( + model_name='charge', + name='amount_refunded', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True), + ), + migrations.AlterField( + model_name='charge', + name='customer', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='charges', to='pinax_stripe.Customer'), + ), + migrations.AlterField( + model_name='charge', + name='invoice', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='charges', to='pinax_stripe.Invoice'), + ), + migrations.AlterField( + model_name='charge', + name='source', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AlterField( + model_name='coupon', + name='amount_off', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True), + ), + migrations.AlterField( + model_name='coupon', + name='duration_in_months', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='coupon', + name='max_redemptions', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='coupon', + name='percent_off', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='coupon', + name='redeem_by', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='coupon', + name='times_redeemed', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='customer', + name='date_purged', + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AlterField( + model_name='discount', + name='customer', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Customer'), + ), + migrations.AlterField( + model_name='discount', + name='end', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='discount', + name='start', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='discount', + name='subscription', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Subscription'), + ), + migrations.AlterField( + model_name='event', + name='customer', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Customer'), + ), + migrations.AlterField( + model_name='eventprocessingexception', + name='event', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Event'), + ), + migrations.AlterField( + model_name='invoice', + name='attempt_count', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='invoice', + name='charge', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='pinax_stripe.Charge'), + ), + migrations.AlterField( + model_name='invoice', + name='subscription', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Subscription'), + ), + migrations.AlterField( + model_name='invoice', + name='tax', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True), + ), + migrations.AlterField( + model_name='invoice', + name='tax_percent', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True), + ), + migrations.AlterField( + model_name='invoice', + name='webhooks_delivered_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='invoiceitem', + name='plan', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Plan'), + ), + migrations.AlterField( + model_name='invoiceitem', + name='quantity', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='invoiceitem', + name='subscription', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Subscription'), + ), + migrations.AlterField( + model_name='plan', + name='trial_period_days', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='subscription', + name='application_fee_percent', + field=models.DecimalField(blank=True, decimal_places=2, default=None, max_digits=3, null=True), + ), + ] From 734aa688cf69b2639786295a7152d8fcf7b8e878 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 Nov 2017 10:51:07 +0100 Subject: [PATCH 129/153] Remove duplicate migration from ticosax/rename-fields-account --- .../migrations/0011_auto_20171123_2016.py | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 pinax/stripe/migrations/0011_auto_20171123_2016.py diff --git a/pinax/stripe/migrations/0011_auto_20171123_2016.py b/pinax/stripe/migrations/0011_auto_20171123_2016.py deleted file mode 100644 index 171003b48..000000000 --- a/pinax/stripe/migrations/0011_auto_20171123_2016.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-11-23 20:16 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0010_connect'), - ] - - operations = [ - migrations.RenameField( - model_name='account', - old_name='transfer_schedule_weekly_anchor', - new_name='payout_schedule_weekly_anchor', - ), - migrations.RenameField( - model_name='account', - old_name='transfer_statement_descriptor', - new_name='payout_statement_descriptor', - ), - migrations.RenameField( - model_name='account', - old_name='transfers_enabled', - new_name='payouts_enabled', - ), - migrations.RemoveField( - model_name='account', - name='transfer_schedule_delay_days', - ), - migrations.RemoveField( - model_name='account', - name='transfer_schedule_interval', - ), - migrations.RemoveField( - model_name='account', - name='transfer_schedule_monthly_anchor', - ), - migrations.AddField( - model_name='account', - name='payout_schedule_delay_days', - field=models.PositiveSmallIntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name='account', - name='payout_schedule_interval', - field=models.CharField(blank=True, choices=[('Manual', 'manual'), ('Daily', 'daily'), ('Weekly', 'weekly'), ('Monthly', 'monthly')], max_length=7, null=True), - ), - migrations.AddField( - model_name='account', - name='payout_schedule_monthly_anchor', - field=models.PositiveSmallIntegerField(blank=True, null=True), - ), - ] From c47a2fc456b1990eafd6d436781844885720ea19 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 Nov 2017 12:09:36 +0100 Subject: [PATCH 130/153] AccountAdmin: fix search_fields --- pinax/stripe/admin.py | 2 +- pinax/stripe/tests/test_admin.py | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 907afdd90..d98a8b54f 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -454,7 +454,7 @@ class AccountAdmin(ModelAdmin): search_fields = [ "display_name", "stripe_id", - ], + ] admin.site.register( diff --git a/pinax/stripe/tests/test_admin.py b/pinax/stripe/tests/test_admin.py index 4174ea136..29689f8d5 100644 --- a/pinax/stripe/tests/test_admin.py +++ b/pinax/stripe/tests/test_admin.py @@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model from django.db import connection -from django.test import Client, SimpleTestCase, TestCase +from django.test import Client, RequestFactory, SimpleTestCase, TestCase from django.test.utils import CaptureQueriesContext from django.utils import timezone @@ -161,6 +161,29 @@ def test_account_filter(self): response = self.client.get(url + "?stripe_account=none") self.assertEqual(response.status_code, 200) + @classmethod + def get_changelist(cls, model_class, data=None): + from django.contrib.admin.sites import AdminSite + from django.utils.module_loading import import_string + + admin_class = import_string("pinax.stripe.admin.{}Admin".format( + model_class.__name__)) + + ma = admin_class(model_class, AdminSite()) + + info = ma.model._meta.app_label, ma.model._meta.model_name + url = reverse("admin:%s_%s_changelist" % info) + request = RequestFactory().get(url, data=data) + request.user = cls.user + return ma.changelist_view(request).context_data["cl"] + + def test_account_search(self): + cl = self.get_changelist(Account) + self.assertEqual(list(cl.queryset), [self.account]) + + cl = self.get_changelist(Account, {"q": "acc_doesnotexist"}) + self.assertEqual(list(cl.queryset), []) + class AdminSimpleTestCase(SimpleTestCase): From f94a10d1c537c338f55459947f31acc6331f9806 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 Nov 2017 12:30:19 +0100 Subject: [PATCH 131/153] CircleCI: remove py36dj111psql (#96) --- .circleci/config.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b7dc9fc78..00c1e8a0e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -111,16 +111,6 @@ jobs: - image: circleci/python:3.6 environment: TOXENV=py36-dj111 - py36dj111psql: - <<: *common - docker: - - image: circleci/python:3.6 - environment: - - TOXENV=py36-dj111-postgres - - PINAX_STRIPE_DATABASE_HOST=127.0.0.1 - - PINAX_STRIPE_DATABASE_USER=root - - PINAX_STRIPE_DATABASE_NAME=circle_test - - image: circleci/postgres:9.6-alpine py36dj20: <<: *common docker: From 83a38554517ac99a016e1334fadd88e7cc810674 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 Nov 2017 16:59:07 +0100 Subject: [PATCH 132/153] Add ChargeAdmin, prefetching users (#97) --- pinax/stripe/admin.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index d98a8b54f..79a6e912d 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -139,10 +139,8 @@ def get_changelist(self, request, **kwargs): return PrefetchingChangeList -admin.site.register( - Charge, - admin_class=ModelAdmin, - list_display=[ +class ChargeAdmin(ModelAdmin): + list_display = [ "stripe_id", "customer", "amount", @@ -152,23 +150,30 @@ def get_changelist(self, request, **kwargs): "refunded", "receipt_sent", "created_at", - ], - search_fields=[ + ] + list_select_related = [ + "customer", + ] + search_fields = [ "stripe_id", "customer__stripe_id", "invoice__stripe_id", - ] + customer_search_fields(), - list_filter=[ + ] + customer_search_fields() + list_filter = [ "paid", "disputed", "refunded", "created_at", - ], - raw_id_fields=[ + ] + raw_id_fields = [ "customer", "invoice", - ], -) + ] + + def get_queryset(self, request): + qs = super(ChargeAdmin, self).get_queryset(request) + return qs.prefetch_related("customer__user", "customer__users") + admin.site.register( EventProcessingException, @@ -488,4 +493,5 @@ class AccountAdmin(ModelAdmin): admin.site.register(Account, AccountAdmin) +admin.site.register(Charge, ChargeAdmin) admin.site.register(Customer, CustomerAdmin) From 8e9bd7f6b3f9d52d6200ba51febc7df95992c58f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 Nov 2017 17:13:37 +0100 Subject: [PATCH 133/153] CustomerAdmin: use __str__, select/prefetch user/users --- pinax/stripe/admin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 79a6e912d..16983ccb1 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -250,7 +250,7 @@ class CustomerAdmin(ModelAdmin): raw_id_fields = ["user", "stripe_account"] list_display = [ "stripe_id", - "user", + "__str__", "account_balance", "currency", "delinquent", @@ -261,6 +261,7 @@ class CustomerAdmin(ModelAdmin): ] list_select_related = [ "stripe_account", + "user", ] list_filter = [ "delinquent", @@ -277,6 +278,10 @@ class CustomerAdmin(ModelAdmin): BitcoinReceiverInline ] + def get_queryset(self, request): + qs = super(CustomerAdmin, self).get_queryset(request) + return qs.prefetch_related("users") + class InvoiceItemInline(admin.TabularInline): model = InvoiceItem From 3a091d18d7c9b84a5afb5b3c1de789e77f664d53 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 Nov 2017 17:43:53 +0100 Subject: [PATCH 134/153] Customer.__repr__: use list for users --- pinax/stripe/models.py | 4 ++-- pinax/stripe/tests/test_models.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 5db313d13..fa62cfb0d 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -316,9 +316,9 @@ def __repr__(self): str(self.stripe_id), ) elif self.id: - return "Customer(pk={!r}, users={}, stripe_id={!r})".format( + return "Customer(pk={!r}, users={!r}, stripe_id={!r})".format( self.pk, - ", ".join(repr(user) for user in self.users.all()), + list(self.users.all()), str(self.stripe_id), ) return "Customer(pk={!r}, stripe_id={!r})".format(self.pk, str(self.stripe_id)) diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index 4cb1f5a09..9b6eca8b3 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -104,7 +104,7 @@ def test_connected_customer_str_and_repr(self): customer = Customer.objects.create(stripe_id="cus_A", stripe_account=account) UserAccount.objects.create(customer=customer, user=user, account=account) self.assertEqual(str(customer), "") - self.assertEqual(repr(customer), "Customer(pk={c.pk}, users=, stripe_id='cus_A')".format(c=customer)) + self.assertEqual(repr(customer), "Customer(pk={c.pk}, users=[], stripe_id='cus_A')".format(c=customer)) def test_charge_repr(self): charge = Charge() From 615689f9c182a0a51e4d017524fea7e1d5b9bd4d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 27 Nov 2017 20:29:44 +0100 Subject: [PATCH 135/153] Charge: add pk to __repr__ --- pinax/stripe/models.py | 3 ++- pinax/stripe/tests/test_models.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 202259451..3864c506d 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -543,7 +543,8 @@ class Charge(StripeAccountFromCustomerMixin, StripeObject): objects = ChargeManager() def __repr__(self): - return "Charge(customer={!r}, source={!r}, amount={!r}, captured={!r}, paid={!r}, stripe_id={!r})".format( + return "Charge(pk={!r}, customer={!r}, source={!r}, amount={!r}, captured={!r}, paid={!r}, stripe_id={!r})".format( + self.pk, self.customer, self.source, self.amount, diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index 9b9ca191f..db7d83678 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -138,9 +138,9 @@ def test_connected_customer_str_and_repr(self): def test_charge_repr(self): charge = Charge() if PY2: - self.assertEquals(repr(charge), "Charge(customer=None, source=u'', amount=None, captured=None, paid=None, stripe_id=u'')") + self.assertEquals(repr(charge), "Charge(pk=None, customer=None, source=u'', amount=None, captured=None, paid=None, stripe_id=u'')") else: - self.assertEquals(repr(charge), "Charge(customer=None, source='', amount=None, captured=None, paid=None, stripe_id='')") + self.assertEquals(repr(charge), "Charge(pk=None, customer=None, source='', amount=None, captured=None, paid=None, stripe_id='')") def test_plan_display_invoiceitem(self): p = Plan(amount=decimal.Decimal("5"), name="My Plan", interval="monthly", interval_count=1) From 6e11ff4ed1a788fadc1a01be26b113e99deb732f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 27 Nov 2017 20:30:19 +0100 Subject: [PATCH 136/153] Coupon.__repr__: add pk --- pinax/stripe/models.py | 6 +++++- pinax/stripe/tests/test_models.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 202259451..9a05204da 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -387,7 +387,11 @@ def __str__(self): return "{} - {}".format(self.coupon, self.subscription) def __repr__(self): - return "Discount(coupon={!r}, subscription={!r})".format(self.coupon, self.subscription) + return "Discount(pk={!r}, coupon={!r}, subscription={!r})".format( + self.pk, + self.coupon, + self.subscription, + ) def apply_discount(self, amount): if self.end is not None and self.end < timezone.now(): diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index 9b9ca191f..d20f565db 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -183,7 +183,7 @@ def test_invoice_status_not_paid(self): 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)") + self.assertEquals(repr(d), "Discount(pk=None, 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") From 1394bfc18d72eae870fc3804abf54851bceb0ff7 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 27 Nov 2017 23:05:57 +0100 Subject: [PATCH 137/153] Cleanup admin: do not allow to add/change objects Fixes https://github.com/pinax/pinax-stripe/issues/527 --- pinax/stripe/admin.py | 190 +++++++++++++++++++----------------------- 1 file changed, 84 insertions(+), 106 deletions(-) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 16983ccb1..f4dde19c9 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -3,7 +3,7 @@ from django.contrib.auth import get_user_model from django.db.models import Count -from .models import ( # @@@ make all these read-only +from .models import ( Account, BankAccount, BitcoinReceiver, @@ -135,6 +135,14 @@ def get_queryset(self, request): class ModelAdmin(admin.ModelAdmin): + def has_add_permission(self, request, obj=None): + return False + + def has_change_permission(self, request, obj=None): + if obj is None: + return True + return False + def get_changelist(self, request, **kwargs): return PrefetchingChangeList @@ -175,27 +183,25 @@ def get_queryset(self, request): return qs.prefetch_related("customer__user", "customer__users") -admin.site.register( - EventProcessingException, - list_display=[ +class EventProcessingExceptionAdmin(ModelAdmin): + list_display = [ "message", "event", "created_at" - ], - search_fields=[ + ] + search_fields = [ "message", "traceback", "data" - ], - raw_id_fields=[ + ] + raw_id_fields = [ "event" - ], -) + ] -admin.site.register( - Event, - raw_id_fields=["customer", "stripe_account"], - list_display=[ + +class EventAdmin(ModelAdmin): + raw_id_fields = ["customer", "stripe_account"] + list_display = [ "stripe_id", "kind", "livemode", @@ -203,24 +209,23 @@ def get_queryset(self, request): "processed", "created_at", "stripe_account", - ], - list_select_related=[ + ] + list_select_related = [ "stripe_account", - ], - list_filter=[ + ] + list_filter = [ "kind", "created_at", "valid", "processed", AccountListFilter, - ], - search_fields=[ + ] + search_fields = [ "stripe_id", "customer__stripe_id", "validated_message", "=stripe_account__stripe_id", - ] + customer_search_fields(), -) + ] + customer_search_fields() class SubscriptionInline(admin.TabularInline): @@ -307,10 +312,9 @@ def customer_user(obj): customer_user.short_description = "Customer" # noqa -admin.site.register( - Invoice, - raw_id_fields=["customer"], - list_display=[ +class InvoiceAdmin(ModelAdmin): + raw_id_fields = ["customer"] + list_display = [ "stripe_id", "paid", "closed", @@ -320,12 +324,12 @@ def customer_user(obj): "period_end", "subtotal", "total" - ], - search_fields=[ + ] + search_fields = [ "stripe_id", "customer__stripe_id", - ] + customer_search_fields(), - list_filter=[ + ] + customer_search_fields() + list_filter = [ InvoiceCustomerHasCardListFilter, "paid", "closed", @@ -335,16 +339,15 @@ def customer_user(obj): "date", "period_end", "total" - ], - inlines=[ + ] + inlines = [ InvoiceItemInline ] -) -admin.site.register( - Plan, - raw_id_fields=["stripe_account"], - list_display=[ + +class PlanAdmin(ModelAdmin): + raw_id_fields = ["stripe_account"] + list_display = [ "stripe_id", "name", "amount", @@ -353,36 +356,23 @@ def customer_user(obj): "interval_count", "trial_period_days", "stripe_account", - ], - list_select_related=[ + ] + list_select_related = [ "stripe_account", - ], - search_fields=[ + ] + search_fields = [ "stripe_id", "name", "=stripe_account__stripe_id", - ] + customer_search_fields(), - list_filter=[ + ] + customer_search_fields() + list_filter = [ "currency", AccountListFilter, - ], - readonly_fields=[ - "stripe_id", - "name", - "amount", - "currency", - "interval", - "interval_count", - "trial_period_days", - "statement_descriptor", - "created_at", - ], -) + ] -admin.site.register( - Coupon, - list_display=[ +class CouponAdmin(ModelAdmin): + list_display = [ "stripe_id", "amount_off", "currency", @@ -391,29 +381,14 @@ def customer_user(obj): "duration_in_months", "redeem_by", "valid" - ], - search_fields=[ - "stripe_id", - ], - list_filter=[ - "currency", - "valid", - ], - readonly_fields=[ + ] + search_fields = [ "stripe_id", - "amount_off", + ] + list_filter = [ "currency", - "duration", - "duration_in_months", - "max_redemptions", - "metadata", - "percent_off", - "redeem_by", - "times_redeemed", "valid", - "created_at" - ], -) + ] class TransferChargeFeeInline(admin.TabularInline): @@ -422,32 +397,31 @@ class TransferChargeFeeInline(admin.TabularInline): max_num = 0 -admin.site.register( - Transfer, - raw_id_fields=["event", "stripe_account"], - list_display=[ +class TransferAdmin(ModelAdmin): + Transfer + raw_id_fields = ["event", "stripe_account"] + list_display = [ "stripe_id", "amount", "status", "date", "description", "stripe_account", - ], - list_select_related=[ + ] + list_select_related = [ "stripe_account", - ], - search_fields=[ + ] + search_fields = [ "stripe_id", "event__stripe_id", "=stripe_account__stripe_id", - ], - inlines=[ + ] + inlines = [ TransferChargeFeeInline - ], - list_filter=[ + ] + list_filter = [ AccountListFilter, - ], -) + ] class AccountAdmin(ModelAdmin): @@ -467,10 +441,9 @@ class AccountAdmin(ModelAdmin): ] -admin.site.register( - BankAccount, - raw_id_fields=["account"], - list_display=[ +class BankAccountAdmin(ModelAdmin): + raw_id_fields = ["account"] + list_display = [ "stripe_id", "account", "account_holder_type", @@ -480,23 +453,28 @@ class AccountAdmin(ModelAdmin): "bank_name", "country", "last4" - ], - search_fields=[ + ] + search_fields = [ "stripe_id", ] -) -admin.site.register( - UserAccount, - raw_id_fields=["user", "customer"], - list_display=["user", "customer"], - search_fields=[ + +class UserAccountAdmin(ModelAdmin): + raw_id_fields = ["user", "customer"] + list_display = ["user", "customer"] + search_fields = [ "=customer__stripe_id", "=user__email", ] -) admin.site.register(Account, AccountAdmin) +admin.site.register(BankAccount, BankAccountAdmin) admin.site.register(Charge, ChargeAdmin) +admin.site.register(Coupon, CouponAdmin) +admin.site.register(Event, EventAdmin) +admin.site.register(EventProcessingException, EventProcessingExceptionAdmin) +admin.site.register(Invoice, InvoiceAdmin) admin.site.register(Customer, CustomerAdmin) +admin.site.register(Plan, PlanAdmin) +admin.site.register(UserAccount, UserAccountAdmin) From 94f72edec4e0a4a37a1c66e45ef6af985686ff7e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 28 Nov 2017 21:27:24 +0100 Subject: [PATCH 138/153] ChargeAdmin: list_display: add outcome (type/risk_level) --- pinax/stripe/admin.py | 7 +++++++ pinax/stripe/tests/test_admin.py | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 16983ccb1..2f94b7d67 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -149,6 +149,7 @@ class ChargeAdmin(ModelAdmin): "disputed", "refunded", "receipt_sent", + "display_outcome", "created_at", ] list_select_related = [ @@ -170,6 +171,12 @@ class ChargeAdmin(ModelAdmin): "invoice", ] + def display_outcome(self, obj): + return "{} / {}".format( + obj.outcome.get("type", "-"), + obj.outcome.get("risk_level", "-")) if obj.outcome else None + display_outcome.short_description = "Outcome" + def get_queryset(self, request): qs = super(ChargeAdmin, self).get_queryset(request) return qs.prefetch_related("customer__user", "customer__users") diff --git a/pinax/stripe/tests/test_admin.py b/pinax/stripe/tests/test_admin.py index a3f517373..bca81a141 100644 --- a/pinax/stripe/tests/test_admin.py +++ b/pinax/stripe/tests/test_admin.py @@ -6,7 +6,7 @@ from django.test.utils import CaptureQueriesContext from django.utils import timezone -from ..models import Account, Customer, Invoice, Plan, Subscription +from ..models import Account, Charge, Customer, Invoice, Plan, Subscription try: from django.urls import reverse @@ -159,6 +159,8 @@ def test_plan_admin(self): self.assertEqual(response.status_code, 200) def test_charge_admin(self): + Charge.objects.create(stripe_id="ch_1") + Charge.objects.create(stripe_id="ch_2", outcome={"risk_level": "normal"}) url = reverse("admin:pinax_stripe_charge_changelist") response = self.client.get(url) self.assertEqual(response.status_code, 200) From b5359dd573ddfff1ffabf5ef756ed0590ce31de1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 29 Nov 2017 20:20:37 +0100 Subject: [PATCH 139/153] admin: fix ModelAdmin: only disallow POST --- pinax/stripe/admin.py | 18 +++++++++++++++--- .../admin/pinax_stripe/change_form.html | 5 +++++ pinax/stripe/tests/test_admin.py | 10 +++++++++- 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 pinax/stripe/templates/admin/pinax_stripe/change_form.html diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 6950c4a21..27f42237d 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -2,6 +2,8 @@ from django.contrib.admin.views.main import ChangeList from django.contrib.auth import get_user_model from django.db.models import Count +from django.utils.encoding import force_text +from django.utils.translation import ugettext as _ from .models import ( Account, @@ -138,10 +140,20 @@ class ModelAdmin(admin.ModelAdmin): def has_add_permission(self, request, obj=None): return False + def change_view(self, request, object_id, form_url="", extra_context=None): + """Adjust change_view title ("View" instead of "Change").""" + opts = self.model._meta + + extra_context = extra_context or {} + extra_context["title"] = _("View %s" % force_text(opts.verbose_name)) + return super(ModelAdmin, self).change_view( + request, object_id, form_url, extra_context=extra_context, + ) + def has_change_permission(self, request, obj=None): - if obj is None: - return True - return False + if request.method == "POST": + return False + return True def get_changelist(self, request, **kwargs): return PrefetchingChangeList diff --git a/pinax/stripe/templates/admin/pinax_stripe/change_form.html b/pinax/stripe/templates/admin/pinax_stripe/change_form.html new file mode 100644 index 000000000..b21bace38 --- /dev/null +++ b/pinax/stripe/templates/admin/pinax_stripe/change_form.html @@ -0,0 +1,5 @@ +{# Custom template to remove submit_row (readonly, POST is forbidden) #} +{% extends "admin/change_form.html" %} + +{% block submit_buttons_top %}{% endblock %} +{% block submit_buttons_bottom %}{% endblock %} diff --git a/pinax/stripe/tests/test_admin.py b/pinax/stripe/tests/test_admin.py index bca81a141..a573ba77e 100644 --- a/pinax/stripe/tests/test_admin.py +++ b/pinax/stripe/tests/test_admin.py @@ -73,7 +73,7 @@ def setUpClass(cls): start=start, quantity=1 ) - customer = Customer.objects.create( + cls.customer = Customer.objects.create( user=User.objects.create_user(username="patrick{0}".format(12)), stripe_id="cus_xxxxxxxxxxxxxx{0}".format(12) ) @@ -108,6 +108,14 @@ def setUp(self): # Django 1.8 self.client.login(username="admin", password="admin") + def test_readonly_change_form(self): + url = reverse("admin:pinax_stripe_customer_change", args=(self.customer.pk,)) + response = self.client.get(url) + self.assertNotContains(response, "submit-row") + + response = self.client.post(url) + self.assertEqual(response.status_code, 403) + def test_customer_admin(self): """Make sure we get good responses for all filter options""" url = reverse("admin:pinax_stripe_customer_changelist") From 7fecca1357430253b406f2cadc1ee0058acc3801 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 4 Dec 2017 16:38:57 +0100 Subject: [PATCH 140/153] link_customer: create missing customers This also fixes some event fixtures. Fixes https://github.com/pinax/pinax-stripe/issues/467. --- pinax/stripe/actions/customers.py | 18 +++++++++----- pinax/stripe/tests/test_actions.py | 23 ++++++++++++++---- pinax/stripe/tests/test_event.py | 39 +++++++++++++++++++++++++++--- 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index 67096b0a5..e0a53c1fb 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -171,16 +171,22 @@ def link_customer(event): "customer.updated", "customer.deleted" ] + event_data_object = event.message["data"]["object"] if event.kind in customer_crud_events: - cus_id = event.message["data"]["object"]["id"] + cus_id = event_data_object["id"] else: - cus_id = event.message["data"]["object"].get("customer", None) + cus_id = event_data_object.get("customer", None) if cus_id is not None: - customer = models.Customer.objects.filter(stripe_id=cus_id).first() - if customer is not None: - event.customer = customer - event.save() + customer, created = models.Customer.objects.get_or_create( + stripe_id=cus_id, + stripe_account=event.stripe_account, + ) + if event.kind in customer_crud_events: + sync_customer(customer, event_data_object) + + event.customer = customer + event.save() def set_default_source(customer, source): diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 83cd51407..31f5b75ab 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -530,12 +530,14 @@ def test_can_charge_false_no_default_source(self): customer = Customer() self.assertFalse(customers.can_charge(customer)) - def test_link_customer(self): + @patch("pinax.stripe.actions.customers.sync_customer") + def test_link_customer(self, SyncMock): Customer.objects.create(stripe_id="cu_123") message = dict(data=dict(object=dict(id="cu_123"))) event = Event.objects.create(validated_message=message, kind="customer.created") customers.link_customer(event) self.assertEquals(event.customer.stripe_id, "cu_123") + self.assertTrue(SyncMock.called) def test_link_customer_non_customer_event(self): Customer.objects.create(stripe_id="cu_123") @@ -544,18 +546,29 @@ def test_link_customer_non_customer_event(self): customers.link_customer(event) self.assertEquals(event.customer.stripe_id, "cu_123") - def test_link_customer_no_customer(self): + def test_link_customer_non_customer_event_no_customer(self): Customer.objects.create(stripe_id="cu_123") message = dict(data=dict(object=dict())) event = Event.objects.create(validated_message=message, kind="transfer.created") customers.link_customer(event) self.assertIsNone(event.customer, "cu_123") - def test_link_customer_does_not_exist(self): + @patch("pinax.stripe.actions.customers.sync_customer") + def test_link_customer_does_not_exist(self, SyncMock): message = dict(data=dict(object=dict(id="cu_123"))) - event = Event.objects.create(validated_message=message, kind="customer.created") + event = Event.objects.create(stripe_id="evt_1", validated_message=message, kind="customer.created") customers.link_customer(event) - self.assertIsNone(event.customer) + Customer.objects.get(stripe_id="cu_123") + self.assertTrue(SyncMock.called) + + @patch("pinax.stripe.actions.customers.sync_customer") + def test_link_customer_does_not_exist_connected(self, SyncMock): + message = dict(data=dict(object=dict(id="cu_123"))) + account = Account.objects.create(stripe_id="acc_XXX") + event = Event.objects.create(stripe_id="evt_1", validated_message=message, kind="customer.created", stripe_account=account) + customers.link_customer(event) + Customer.objects.get(stripe_id="cu_123", stripe_account=account) + self.assertTrue(SyncMock.called) class CustomersWithConnectTests(TestCase): diff --git a/pinax/stripe/tests/test_event.py b/pinax/stripe/tests/test_event.py index 4a3bc8577..8a320277b 100644 --- a/pinax/stripe/tests/test_event.py +++ b/pinax/stripe/tests/test_event.py @@ -36,6 +36,8 @@ def test_link_customer_customer_created(self): "account_balance": 0, "active_card": None, "created": 1363911708, + "currency": None, + "default_source": None, "delinquent": False, "description": None, "discount": None, @@ -43,7 +45,12 @@ def test_link_customer_customer_created(self): "id": "cus_xxxxxxxxxxxxxxx", "livemode": True, "object": "customer", - "subscription": None + "sources": { + "data": [], + }, + "subscriptions": { + "data": [], + }, } }, "id": "evt_xxxxxxxxxxxxx", @@ -59,8 +66,11 @@ def test_link_customer_customer_created(self): webhook_message=msg, validated_message=msg ) + self.assertIsNone(self.customer.account_balance) customers.link_customer(event) self.assertEquals(event.customer, self.customer) + self.customer.refresh_from_db() + self.assertEquals(self.customer.account_balance, 0) def test_link_customer_customer_updated(self): msg = { @@ -88,6 +98,8 @@ def test_link_customer_customer_updated(self): "type": "MasterCard" }, "created": 1346855596, + "currency": None, + "default_source": None, "delinquent": False, "description": None, "discount": None, @@ -95,7 +107,12 @@ def test_link_customer_customer_updated(self): "id": "cus_xxxxxxxxxxxxxxx", "livemode": True, "object": "customer", - "subscription": None + "sources": { + "data": [], + }, + "subscriptions": { + "data": [], + }, }, "previous_attributes": { "active_card": None @@ -125,6 +142,8 @@ def test_link_customer_customer_deleted(self): "account_balance": 0, "active_card": None, "created": 1348286302, + "currency": None, + "default_source": None, "delinquent": False, "description": None, "discount": None, @@ -132,7 +151,12 @@ def test_link_customer_customer_deleted(self): "id": "cus_xxxxxxxxxxxxxxx", "livemode": True, "object": "customer", - "subscription": None + "sources": { + "data": [], + }, + "subscriptions": { + "data": [], + }, } }, "id": "evt_xxxxxxxxxxxxx", @@ -162,6 +186,8 @@ def test_process_customer_deleted(self, CustomerMock, EventMock): "account_balance": 0, "active_card": None, "created": 1348286302, + "currency": None, + "default_source": None, "delinquent": False, "description": None, "discount": None, @@ -169,7 +195,12 @@ def test_process_customer_deleted(self, CustomerMock, EventMock): "id": "cus_xxxxxxxxxxxxxxx", "livemode": True, "object": "customer", - "subscription": None + "sources": { + "data": [], + }, + "subscriptions": { + "data": [], + } } }, "id": "evt_xxxxxxxxxxxxx", From 789cdf5166eb0fe180b7a962f1bfcf0d7e926947 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 23 Jan 2018 14:00:05 +0100 Subject: [PATCH 141/153] Charge: add __str__ and total_amount property (#109) --- pinax/stripe/models.py | 21 +++++++++++++++++++++ pinax/stripe/tests/test_models.py | 19 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 9b2628470..e73d064ee 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -546,6 +546,21 @@ class Charge(StripeAccountFromCustomerMixin, StripeObject): objects = ChargeManager() + def __str__(self): + info = [] + if not self.paid: + info += ["unpaid"] + if not self.captured: + info += ["uncaptured"] + if self.refunded: + info += ["refunded"] + currency = CURRENCY_SYMBOLS.get(self.currency, "") + return "{}{}{}".format( + currency, + self.total_amount, + " ({})".format(", ".join(info)) if info else "", + ) + def __repr__(self): return "Charge(pk={!r}, customer={!r}, source={!r}, amount={!r}, captured={!r}, paid={!r}, stripe_id={!r})".format( self.pk, @@ -557,6 +572,12 @@ def __repr__(self): self.stripe_id, ) + @property + def total_amount(self): + amount = self.amount if self.amount else 0 + amount_refunded = self.amount_refunded if self.amount_refunded else 0 + return amount - amount_refunded + @property def stripe_charge(self): return stripe.Charge.retrieve( diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index 553176a47..b180a2eed 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -142,6 +142,25 @@ def test_charge_repr(self): else: self.assertEquals(repr(charge), "Charge(pk=None, customer=None, source='', amount=None, captured=None, paid=None, stripe_id='')") + def test_charge_str(self): + charge = Charge() + self.assertEquals(str(charge), "$0 (unpaid, uncaptured)") + charge.stripe_id = "ch_XXX" + charge.captured = True + charge.paid = True + charge.amount = decimal.Decimal(5) + self.assertEquals(str(charge), "$5") + charge.refunded = True + self.assertEquals(str(charge), "$5 (refunded)") + + def test_charge_total_amount(self): + charge = Charge() + self.assertEquals(charge.total_amount, 0) + charge.amount = decimal.Decimal(17) + self.assertEquals(charge.total_amount, 17) + charge.amount_refunded = decimal.Decimal(15.5) + self.assertEquals(charge.total_amount, 1.5) + def test_plan_display_invoiceitem(self): p = Plan(amount=decimal.Decimal("5"), name="My Plan", interval="monthly", interval_count=1) p.save() From e19023ccd4d3a82029228d9c460ca8b72335ae06 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 23 Jan 2018 14:40:34 +0100 Subject: [PATCH 142/153] admin: add stripe_account_stripe_id/stripe_account (#111) --- pinax/stripe/admin.py | 7 +++++++ pinax/stripe/models.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 27f42237d..7d8ef5ab1 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -190,6 +190,9 @@ class ChargeAdmin(ModelAdmin): "customer", "invoice", ] + readonly_fields = [ + "stripe_account_stripe_id", + ] def display_outcome(self, obj): return "{} / {}".format( @@ -362,6 +365,9 @@ class InvoiceAdmin(ModelAdmin): inlines = [ InvoiceItemInline ] + readonly_fields = [ + "stripe_account_stripe_id", + ] class PlanAdmin(ModelAdmin): @@ -391,6 +397,7 @@ class PlanAdmin(ModelAdmin): class CouponAdmin(ModelAdmin): + raw_id_fields = ["stripe_account"] list_display = [ "stripe_id", "amount_off", diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index e73d064ee..06981e2f6 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -39,6 +39,7 @@ class AccountRelatedStripeObjectMixin(models.Model): @property def stripe_account_stripe_id(self): return getattr(self.stripe_account, "stripe_id", None) + stripe_account_stripe_id.fget.short_description = "Stripe Account" class Meta: abstract = True @@ -69,6 +70,7 @@ def stripe_account(self): @property def stripe_account_stripe_id(self): return self.stripe_account.stripe_id if self.stripe_account else None + stripe_account_stripe_id.fget.short_description = "Stripe Account" @python_2_unicode_compatible From df3aa3e10a634023e888dabf53dfb294dc0948cc Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 23 Jan 2018 14:48:55 +0100 Subject: [PATCH 143/153] Charge admin: use total amount in list_display (#110) --- pinax/stripe/admin.py | 2 +- pinax/stripe/models.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 7d8ef5ab1..586431f1d 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -163,7 +163,7 @@ class ChargeAdmin(ModelAdmin): list_display = [ "stripe_id", "customer", - "amount", + "total_amount", "description", "paid", "disputed", diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 06981e2f6..c5a8ef0e1 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals import decimal @@ -579,6 +580,7 @@ def total_amount(self): amount = self.amount if self.amount else 0 amount_refunded = self.amount_refunded if self.amount_refunded else 0 return amount - amount_refunded + total_amount.fget.short_description = "Σ amount" @property def stripe_charge(self): From f8dac91ce43185e23167ee22174e0b7cdb0f24aa Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 24 Jan 2018 15:28:18 +0100 Subject: [PATCH 144/153] Customer: fix __str__ for saved customer without users (#112) --- pinax/stripe/models.py | 6 +++++- pinax/stripe/tests/test_models.py | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index c5a8ef0e1..56631bcb0 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -308,7 +308,11 @@ def __str__(self): if self.user: return str(self.user) elif self.id: - return ", ".join(str(user) for user in self.users.all()) + users = self.users.all() + if users: + return ", ".join(str(user) for user in users) + if self.stripe_id: + return "No User(s) ({})".format(self.stripe_id) return "No User(s)" def __repr__(self): diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index b180a2eed..0e77a8f08 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -108,7 +108,7 @@ def test_event_str_and_repr(self): def test_customer_str_and_repr(self): c = Customer() - self.assertTrue("No User(s)" in str(c)) + self.assertEquals(str(c), "No User(s)") if PY2: self.assertEquals(repr(c), "Customer(pk=None, stripe_id=u'')") else: @@ -123,6 +123,12 @@ def test_customer_with_user_str_and_repr(self): else: self.assertEqual(repr(c), "Customer(pk=None, user=, stripe_id='')") + def test_customer_saved_without_users_str(self): + c = Customer.objects.create() + self.assertEqual(str(c), "No User(s)") + c.stripe_id = "cu_XXX" + self.assertEqual(str(c), "No User(s) (cu_XXX)") + def test_connected_customer_str_and_repr(self): User = get_user_model() user = User.objects.create() From c9f6284684b031ed270397799e11a4c7ccbd12f8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 19 Feb 2018 18:54:45 +0100 Subject: [PATCH 145/153] Fix squashed migration (#113) This removes he following from 0010_connect_squashed_0026_merge_20171106_2259's `replaces`: ('pinax_stripe', '0025_remove_subscription_stripe_account'), ('pinax_stripe', '0016_auto_20171106_1234'), ('pinax_stripe', '0014_auto_20171026_1304'), ('pinax_stripe', '0017_merge_20171106_2201'), ('pinax_stripe', '0026_merge_20171106_2259'), It was necessary for me to get the 0010_connect_squashed_0026_merge_20171106_2259 migration to be recognized when in the following state (`manage.py showmigrations`): [X] 0010_connect [X] 0011_rename_account_transfers_payout [X] 0011_auto_20171121_1648 [X] 0012_merge_20171121_1652 [X] 0013_charge_outcome [ ] 0014_blank_with_null [ ] 0015_blank_account_legal_entity_dob --- ...nnect_squashed_0026_merge_20171106_2259.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/migrations/0010_connect_squashed_0026_merge_20171106_2259.py b/pinax/stripe/migrations/0010_connect_squashed_0026_merge_20171106_2259.py index df03e91fe..4e6249494 100644 --- a/pinax/stripe/migrations/0010_connect_squashed_0026_merge_20171106_2259.py +++ b/pinax/stripe/migrations/0010_connect_squashed_0026_merge_20171106_2259.py @@ -12,7 +12,33 @@ class Migration(migrations.Migration): - replaces = [('pinax_stripe', '0010_connect'), ('pinax_stripe', '0011_subscription_connect'), ('pinax_stripe', '0012_user_account'), ('pinax_stripe', '0013_revert_0011'), ('pinax_stripe', '0014_auto_20171018_1024'), ('pinax_stripe', '0015_account-as-foreign-key'), ('pinax_stripe', '0016_remove-user-account-account'), ('pinax_stripe', '0017_user-account-account'), ('pinax_stripe', '0018_add_discounts'), ('pinax_stripe', '0011_auto_20171024_1209'), ('pinax_stripe', '0011_auto_20171017_1234'), ('pinax_stripe', '0012_merge_20171025_1443'), ('pinax_stripe', '0019_merge_20171025_1519'), ('pinax_stripe', '0020_account-publishablke-key'), ('pinax_stripe', '0021_account-authorized'), ('pinax_stripe', '0022_sub-account-related'), ('pinax_stripe', '0011_account-as-foreign-key'), ('pinax_stripe', '0012_merge_20171026_1310'), ('pinax_stripe', '0013_auto_20171027_1443'), ('pinax_stripe', '0023_merge_20171027_1626'), ('pinax_stripe', '0013_auto_20171025_2153'), ('pinax_stripe', '0011_account_publishable_key'), ('pinax_stripe', '0014_merge_20171030_1554'), ('pinax_stripe', '0015_merge_20171030_1852'), ('pinax_stripe', '0024_merge_20171030_1941'), ('pinax_stripe', '0025_remove_subscription_stripe_account'), ('pinax_stripe', '0016_auto_20171106_1234'), ('pinax_stripe', '0014_auto_20171026_1304'), ('pinax_stripe', '0017_merge_20171106_2201'), ('pinax_stripe', '0026_merge_20171106_2259')] + replaces = [ + ('pinax_stripe', '0010_connect'), + ('pinax_stripe', '0011_subscription_connect'), + ('pinax_stripe', '0012_user_account'), + ('pinax_stripe', '0013_revert_0011'), + ('pinax_stripe', '0014_auto_20171018_1024'), + ('pinax_stripe', '0015_account-as-foreign-key'), + ('pinax_stripe', '0016_remove-user-account-account'), + ('pinax_stripe', '0017_user-account-account'), + ('pinax_stripe', '0018_add_discounts'), + ('pinax_stripe', '0011_auto_20171024_1209'), + ('pinax_stripe', '0011_auto_20171017_1234'), + ('pinax_stripe', '0012_merge_20171025_1443'), + ('pinax_stripe', '0019_merge_20171025_1519'), + ('pinax_stripe', '0020_account-publishablke-key'), + ('pinax_stripe', '0021_account-authorized'), + ('pinax_stripe', '0022_sub-account-related'), + ('pinax_stripe', '0011_account-as-foreign-key'), + ('pinax_stripe', '0012_merge_20171026_1310'), + ('pinax_stripe', '0013_auto_20171027_1443'), + ('pinax_stripe', '0023_merge_20171027_1626'), + ('pinax_stripe', '0013_auto_20171025_2153'), + ('pinax_stripe', '0011_account_publishable_key'), + ('pinax_stripe', '0014_merge_20171030_1554'), + ('pinax_stripe', '0015_merge_20171030_1852'), + ('pinax_stripe', '0024_merge_20171030_1941'), + ] dependencies = [ ('pinax_stripe', '0009_auto_20170825_1841'), From f33a61c186a461102fd699a7e86c246bbd7dc8d2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 20 Feb 2018 15:48:05 +0100 Subject: [PATCH 146/153] install_requires: unpin jsonfield (#114) Ref https://github.com/pinax/pinax-stripe/issues/542 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dc6fedcdb..c71e9aea6 100644 --- a/setup.py +++ b/setup.py @@ -87,7 +87,7 @@ ], install_requires=[ "django-appconf>=1.0.1", - "jsonfield>=1.0.3,<2.0.0", + "jsonfield>=1.0.3", "stripe>=1.7.9", "django>=1.8", "pytz", From 4ce2ae1b09e4606c3410b49b86cfed2d63fd9fae Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 16 Apr 2018 14:50:50 +0200 Subject: [PATCH 147/153] Merge migrations --- .../migrations/0016_merge_20180416_1250.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pinax/stripe/migrations/0016_merge_20180416_1250.py diff --git a/pinax/stripe/migrations/0016_merge_20180416_1250.py b/pinax/stripe/migrations/0016_merge_20180416_1250.py new file mode 100644 index 000000000..84e241922 --- /dev/null +++ b/pinax/stripe/migrations/0016_merge_20180416_1250.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-04-16 12:50 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0015_blank_account_legal_entity_dob'), + ('pinax_stripe', '0014_auto_20180413_1959'), + ] + + operations = [ + ] From 81d5127db4574f48b5bdfd467856fb0f0b38cfea Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 11 Jul 2018 17:17:41 +0200 Subject: [PATCH 148/153] Support stripe-python 2.0 --- pinax/stripe/actions/coupons.py | 6 +--- pinax/stripe/actions/customers.py | 2 +- pinax/stripe/actions/invoices.py | 2 +- pinax/stripe/actions/plans.py | 6 +--- pinax/stripe/tests/test_actions.py | 51 ++++------------------------- pinax/stripe/tests/test_commands.py | 44 ------------------------- pinax/stripe/tests/test_views.py | 12 +++---- pinax/stripe/tests/test_webhooks.py | 4 +-- pinax/stripe/views.py | 12 +++---- pinax/stripe/webhooks.py | 3 +- 10 files changed, 26 insertions(+), 116 deletions(-) diff --git a/pinax/stripe/actions/coupons.py b/pinax/stripe/actions/coupons.py index 32805d120..f89c61f9b 100644 --- a/pinax/stripe/actions/coupons.py +++ b/pinax/stripe/actions/coupons.py @@ -9,11 +9,7 @@ def sync_coupons(): TODO: Support connect / stripe_account param """ - try: - coupons = stripe.Coupon.auto_paging_iter() - except AttributeError: - coupons = iter(stripe.Coupon.all().data) - + coupons = stripe.Coupon.auto_paging_iter() for coupon in coupons: sync_coupon_from_stripe_data(coupon) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index e0a53c1fb..ff82d4351 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -150,7 +150,7 @@ def purge(customer): """ try: customer.stripe_customer.delete() - except stripe.InvalidRequestError as e: + except stripe.error.InvalidRequestError as e: if "no such customer:" not in smart_str(e).lower(): # The exception was thrown because the customer was already # deleted on the stripe side, ignore the exception diff --git a/pinax/stripe/actions/invoices.py b/pinax/stripe/actions/invoices.py index 0b71de610..100b621c7 100644 --- a/pinax/stripe/actions/invoices.py +++ b/pinax/stripe/actions/invoices.py @@ -41,7 +41,7 @@ def create_and_pay(customer): if invoice.amount_due > 0: invoice.pay() return True - except stripe.InvalidRequestError: + except stripe.error.InvalidRequestError: return False # There was nothing to Invoice diff --git a/pinax/stripe/actions/plans.py b/pinax/stripe/actions/plans.py index d24138a28..b304a5110 100644 --- a/pinax/stripe/actions/plans.py +++ b/pinax/stripe/actions/plans.py @@ -7,11 +7,7 @@ def sync_plans(): """ Synchronizes all plans from the Stripe API """ - try: - plans = stripe.Plan.auto_paging_iter() - except AttributeError: - plans = iter(stripe.Plan.all().data) - + plans = stripe.Plan.auto_paging_iter() for plan in plans: sync_plan(plan) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 31f5b75ab..438e313a3 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -495,7 +495,7 @@ def test_purge_connected(self, RetrieveMock): @patch("stripe.Customer.retrieve") def test_purge_already_deleted(self, RetrieveMock): - RetrieveMock().delete.side_effect = stripe.InvalidRequestError("No such customer:", "error") + RetrieveMock().delete.side_effect = stripe.error.InvalidRequestError("No such customer:", "error") customer = Customer.objects.create( user=self.user, stripe_id="cus_xxxxxxxxxxxxxxx" @@ -507,12 +507,12 @@ def test_purge_already_deleted(self, RetrieveMock): @patch("stripe.Customer.retrieve") def test_purge_already_some_other_error(self, RetrieveMock): - RetrieveMock().delete.side_effect = stripe.InvalidRequestError("Bad", "error") + RetrieveMock().delete.side_effect = stripe.error.InvalidRequestError("Bad", "error") customer = Customer.objects.create( user=self.user, stripe_id="cus_xxxxxxxxxxxxxxx" ) - with self.assertRaises(stripe.InvalidRequestError): + with self.assertRaises(stripe.error.InvalidRequestError): customers.purge(customer) self.assertTrue(RetrieveMock().delete.called) self.assertIsNotNone(Customer.objects.get(stripe_id=customer.stripe_id).user) @@ -767,13 +767,13 @@ def test_create_and_pay_amount_due_0(self, CreateMock): def test_create_and_pay_invalid_request_error(self, CreateMock): invoice = CreateMock() invoice.amount_due = 100 - invoice.pay.side_effect = stripe.InvalidRequestError("Bad", "error") + invoice.pay.side_effect = stripe.error.InvalidRequestError("Bad", "error") self.assertFalse(invoices.create_and_pay(Mock())) self.assertTrue(invoice.pay.called) @patch("stripe.Invoice.create") def test_create_and_pay_invalid_request_error_on_create(self, CreateMock): - CreateMock.side_effect = stripe.InvalidRequestError("Bad", "error") + CreateMock.side_effect = stripe.error.InvalidRequestError("Bad", "error") self.assertFalse(invoices.create_and_pay(Mock())) @@ -1252,43 +1252,6 @@ def test_sync_coupon_from_stripe_data(self): 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): - PlanAllMock().data = [ - { - "id": "pro2", - "object": "plan", - "amount": 1999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": {}, - "name": "The Pro Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - { - "id": "simple1", - "object": "plan", - "amount": 999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": {}, - "name": "The Simple Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - ] - plans.sync_plans() - self.assertTrue(Plan.objects.all().count(), 2) - self.assertEquals(Plan.objects.get(stripe_id="simple1").amount, decimal.Decimal("9.99")) - @patch("stripe.Plan.auto_paging_iter", create=True) def test_sync_plans(self, PlanAutoPagerMock): PlanAutoPagerMock.return_value = [ @@ -2312,9 +2275,9 @@ def test_retrieve_stripe_subscription_missing_subscription(self, RetrieveMock): @patch("stripe.Subscription.retrieve") def test_retrieve_stripe_subscription_invalid_request(self, RetrieveMock): def bad_request(*args, **kwargs): - raise stripe.InvalidRequestError("Bad", "error") + raise stripe.error.InvalidRequestError("Bad", "error") RetrieveMock.side_effect = bad_request - with self.assertRaises(stripe.InvalidRequestError): + with self.assertRaises(stripe.error.InvalidRequestError): subscriptions.retrieve(self.customer, "sub id") def test_sync_invoice_items(self): diff --git a/pinax/stripe/tests/test_commands.py b/pinax/stripe/tests/test_commands.py index b1239dd87..43d893a4f 100644 --- a/pinax/stripe/tests/test_commands.py +++ b/pinax/stripe/tests/test_commands.py @@ -32,25 +32,6 @@ def test_init_customer_creates_customer(self, CreateMock, RetrieveMock): customer = Customer.objects.get(user=self.user) self.assertEquals(customer.stripe_id, "cus_XXXXX") - @patch("stripe.Plan.all") - @patch("stripe.Plan.auto_paging_iter", create=True, side_effect=AttributeError) - def test_plans_create_deprecated(self, PlanAutoPagerMock, PlanAllMock): - PlanAllMock().data = [{ - "id": "entry-monthly", - "amount": 954, - "interval": "monthly", - "interval_count": 1, - "currency": None, - "statement_descriptor": None, - "trial_period_days": None, - "name": "Pro", - "metadata": {} - }] - management.call_command("sync_plans") - self.assertEquals(Plan.objects.count(), 1) - self.assertEquals(Plan.objects.all()[0].stripe_id, "entry-monthly") - self.assertEquals(Plan.objects.all()[0].amount, decimal.Decimal("9.54")) - @patch("stripe.Plan.auto_paging_iter", create=True) def test_plans_create(self, PlanAutoPagerMock): PlanAutoPagerMock.return_value = [{ @@ -93,31 +74,6 @@ def test_coupons_create(self, CouponAutoPagerMock): 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") diff --git a/pinax/stripe/tests/test_views.py b/pinax/stripe/tests/test_views.py index a57ae43e8..a33a271d0 100644 --- a/pinax/stripe/tests/test_views.py +++ b/pinax/stripe/tests/test_views.py @@ -128,7 +128,7 @@ def test_post(self, CreateMock): @patch("pinax.stripe.actions.sources.create_card") def test_post_on_error(self, CreateMock): - CreateMock.side_effect = stripe.CardError("Bad card", "Param", "CODE") + CreateMock.side_effect = stripe.error.CardError("Bad card", "Param", "CODE") self.client.login(username=self.user.username, password=self.password) response = self.client.post( reverse("pinax_stripe_payment_method_create"), @@ -176,7 +176,7 @@ def test_post(self, CreateMock): @patch("pinax.stripe.actions.sources.delete_card") def test_post_on_error(self, CreateMock): - CreateMock.side_effect = stripe.CardError("Bad card", "Param", "CODE") + CreateMock.side_effect = stripe.error.CardError("Bad card", "Param", "CODE") self.client.login(username=self.user.username, password=self.password) response = self.client.post( reverse("pinax_stripe_payment_method_delete", args=[self.card.pk]), @@ -240,7 +240,7 @@ def test_post_invalid_form(self, CreateMock): @patch("pinax.stripe.actions.sources.update_card") def test_post_on_error(self, CreateMock): - CreateMock.side_effect = stripe.CardError("Bad card", "Param", "CODE") + CreateMock.side_effect = stripe.error.CardError("Bad card", "Param", "CODE") self.client.login(username=self.user.username, password=self.password) response = self.client.post( reverse("pinax_stripe_payment_method_update", args=[self.card.pk]), @@ -345,7 +345,7 @@ def test_post_on_error(self, CreateMock): stripe_id="cus_1", user=self.user ) - CreateMock.side_effect = stripe.StripeError("Bad Mojo", "Param", "CODE") + CreateMock.side_effect = stripe.error.StripeError("Bad Mojo", "Param", "CODE") self.client.login(username=self.user.username, password=self.password) response = self.client.post( reverse("pinax_stripe_subscription_create"), @@ -398,7 +398,7 @@ def test_post(self, CancelMock): @patch("pinax.stripe.actions.subscriptions.cancel") def test_post_on_error(self, CancelMock): - CancelMock.side_effect = stripe.StripeError("Bad Foo", "Param", "CODE") + CancelMock.side_effect = stripe.error.StripeError("Bad Foo", "Param", "CODE") self.client.login(username=self.user.username, password=self.password) response = self.client.post( reverse("pinax_stripe_subscription_delete", args=[self.subscription.pk]), @@ -472,7 +472,7 @@ def test_post_invalid(self, UpdateMock): @patch("pinax.stripe.actions.subscriptions.update") def test_post_on_error(self, UpdateMock): - UpdateMock.side_effect = stripe.StripeError("Bad Foo", "Param", "CODE") + UpdateMock.side_effect = stripe.error.StripeError("Bad Foo", "Param", "CODE") self.client.login(username=self.user.username, password=self.password) response = self.client.post( reverse("pinax_stripe_subscription_update", args=[self.subscription.pk]), diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index 6fbf01938..e78185ae6 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -196,8 +196,8 @@ def signal_handler(sender, *args, **kwargs): def test_process_exception_is_logged(self, ProcessWebhookMock, ValidateMock, LinkMock): # note: we choose an event type for which we do no processing event = Event.objects.create(kind="account.external_account.created", webhook_message={}, valid=True, processed=False) - ProcessWebhookMock.side_effect = stripe.StripeError("Message", "error") - with self.assertRaises(stripe.StripeError): + ProcessWebhookMock.side_effect = stripe.error.StripeError("Message", "error") + with self.assertRaises(stripe.error.StripeError): AccountExternalAccountCreatedWebhook(event).process() self.assertTrue(EventProcessingException.objects.filter(event=event).exists()) diff --git a/pinax/stripe/views.py b/pinax/stripe/views.py index f32441da4..df6ba27b0 100644 --- a/pinax/stripe/views.py +++ b/pinax/stripe/views.py @@ -52,7 +52,7 @@ def post(self, request, *args, **kwargs): try: self.create_card(request.POST.get("stripeToken")) return redirect("pinax_stripe_payment_method_list") - except stripe.CardError as e: + except stripe.error.CardError as e: return self.render_to_response(self.get_context_data(errors=smart_str(e))) @@ -68,7 +68,7 @@ def post(self, request, *args, **kwargs): try: self.delete_card(self.object.stripe_id) return redirect("pinax_stripe_payment_method_list") - except stripe.CardError as e: + except stripe.error.CardError as e: return self.render_to_response(self.get_context_data(errors=smart_str(e))) @@ -84,7 +84,7 @@ def form_valid(self, form): try: self.update_card(form.cleaned_data["expMonth"], form.cleaned_data["expYear"]) return redirect("pinax_stripe_payment_method_list") - except stripe.CardError as e: + except stripe.error.CardError as e: return self.render_to_response(self.get_context_data(errors=smart_str(e))) def post(self, request, *args, **kwargs): @@ -125,7 +125,7 @@ def form_valid(self, form): try: self.subscribe(self.customer, plan=form.cleaned_data["plan"], token=self.request.POST.get("stripeToken")) return redirect("pinax_stripe_subscription_list") - except stripe.StripeError as e: + except stripe.error.StripeError as e: return self.render_to_response(self.get_context_data(form=form, errors=smart_str(e))) @@ -141,7 +141,7 @@ def post(self, request, *args, **kwargs): try: self.cancel() return redirect("pinax_stripe_subscription_list") - except stripe.StripeError as e: + except stripe.error.StripeError as e: return self.render_to_response(self.get_context_data(errors=smart_str(e))) @@ -177,7 +177,7 @@ def form_valid(self, form): try: self.update_subscription(form.cleaned_data["plan"]) return redirect("pinax_stripe_subscription_list") - except stripe.StripeError as e: + except stripe.error.StripeError as e: return self.render_to_response(self.get_context_data(form=form, errors=smart_str(e))) def post(self, request, *args, **kwargs): diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 8e2b5034f..ff652094e 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -99,7 +99,6 @@ def validate(self): json.dumps( evt.to_dict(), sort_keys=True, - cls=stripe.StripeObjectEncoder ) ) self.event.valid = self.is_event_valid(self.event.webhook_message["data"], self.event.validated_message["data"]) @@ -133,7 +132,7 @@ def process(self): self.event.save() except Exception as e: data = None - if isinstance(e, stripe.StripeError): + if isinstance(e, stripe.error.StripeError): data = e.http_body exceptions.log_exception(data=data, exception=e, event=self.event) raise e From 949241c2d7cff62bd9348fe4126056bf7f4c7d13 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 5 Sep 2018 19:41:31 +0200 Subject: [PATCH 149/153] user_search_fields: include users This is required with users through connected accounts. --- pinax/stripe/admin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 586431f1d..ece84a1bc 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -28,10 +28,11 @@ def user_search_fields(): User = get_user_model() fields = [ - "user__{0}".format(User.USERNAME_FIELD) + "user__{0}".format(User.USERNAME_FIELD), + "users__{0}".format(User.USERNAME_FIELD), ] if "email" in [f.name for f in User._meta.fields]: # pragma: no branch - fields += ["user__email"] + fields += ["user__email", "users__email"] return fields From c31c77bd839959efb0fc7b620b33ba4c8f03fe51 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 13 Sep 2018 07:53:55 +0200 Subject: [PATCH 150/153] Fix ChargeWebhook for charge.dispute events Fixes https://github.com/pinax/pinax-stripe/issues/590 --- pinax/stripe/tests/test_webhooks.py | 24 ++++++++++++++++++++++++ pinax/stripe/webhooks.py | 9 +++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index e78185ae6..c7659814a 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -28,6 +28,7 @@ AccountExternalAccountCreatedWebhook, AccountUpdatedWebhook, ChargeCapturedWebhook, + ChargeDisputeFundsWithdrawnWebhook, CouponCreatedWebhook, CouponDeletedWebhook, CouponUpdatedWebhook, @@ -245,6 +246,29 @@ def test_process_webhook_connect(self, SyncMock, RetrieveMock): self.assertEquals(kwargs["expand"], ["balance_transaction"]) self.assertEquals(kwargs["stripe_account"], "acc_A") + @patch("stripe.Charge.retrieve") + @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") + def test_process_webhook_dispute(self, SyncMock, RetrieveMock): + account = Account.objects.create(stripe_id="acc_A") + event = Event.objects.create( + kind=ChargeDisputeFundsWithdrawnWebhook.name, + webhook_message={}, + valid=True, + processed=False, + stripe_account=account + ) + event.validated_message = dict(data=dict(object=dict( + id=1, + object="dispute", + charge="ch_XXX", + ))) + ChargeDisputeFundsWithdrawnWebhook(event).process_webhook() + self.assertTrue(SyncMock.called) + args, kwargs = RetrieveMock.call_args + self.assertEquals(args, ("ch_XXX",)) + self.assertEquals(kwargs["expand"], ["balance_transaction"]) + self.assertEquals(kwargs["stripe_account"], "acc_A") + class CustomerDeletedWebhookTest(TestCase): diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index ff652094e..e60667fa7 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -241,10 +241,15 @@ class BitcoinReceiverTransactionCreatedWebhook(Webhook): class ChargeWebhook(Webhook): - def process_webhook(self): + message = self.event.message + if message["data"]["object"].get("object", "charge") == "charge": + stripe_id = message["data"]["object"]["id"] + else: + stripe_id = message["data"]["object"]["charge"] + charges.sync_charge( - self.event.message["data"]["object"]["id"], + stripe_id, stripe_account=self.event.stripe_account_stripe_id, ) From d3243da33fdd0c2a9f1ad38ff715934ecd14e8ec Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 13 Sep 2018 08:25:08 +0200 Subject: [PATCH 151/153] CircleCI: use postgres user "root" is not available anymore?! Ref: https://circleci.com/gh/lock8/pinax-stripe/1256 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fc351e6fc..dc1243ca2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -123,7 +123,7 @@ jobs: environment: - TOXENV=py36-dj20-postgres - PINAX_STRIPE_DATABASE_HOST=127.0.0.1 - - PINAX_STRIPE_DATABASE_USER=root + - PINAX_STRIPE_DATABASE_USER=postgres - PINAX_STRIPE_DATABASE_NAME=circle_test - image: circleci/postgres:9.6-alpine release: From 8657aa286457050b25c07a0c2862fdc10a9c1f34 Mon Sep 17 00:00:00 2001 From: shanedevane Date: Fri, 13 Aug 2021 15:53:21 +0200 Subject: [PATCH 152/153] increased stripe_publishable_key data length for new stripe api keys --- .../migrations/0017_auto_20210813_1549.py | 18 ++++++++++++++++++ pinax/stripe/models.py | 2 +- setup.py | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 pinax/stripe/migrations/0017_auto_20210813_1549.py diff --git a/pinax/stripe/migrations/0017_auto_20210813_1549.py b/pinax/stripe/migrations/0017_auto_20210813_1549.py new file mode 100644 index 000000000..ae3caf823 --- /dev/null +++ b/pinax/stripe/migrations/0017_auto_20210813_1549.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2021-08-13 13:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0016_merge_20180416_1250'), + ] + + operations = [ + migrations.AlterField( + model_name='account', + name='stripe_publishable_key', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 0c2aef770..d867475cb 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -648,7 +648,7 @@ class Account(StripeObject): metadata = JSONField(null=True, blank=True) - stripe_publishable_key = models.CharField(null=True, blank=True, max_length=100) + stripe_publishable_key = models.CharField(null=True, blank=True, max_length=255) product_description = models.TextField(null=True, blank=True) statement_descriptor = models.TextField(null=True, blank=True) diff --git a/setup.py b/setup.py index f763a0034..81856983f 100644 --- a/setup.py +++ b/setup.py @@ -89,7 +89,7 @@ "django-appconf>=1.0.1", "jsonfield>=1.0.3", "stripe>=2.0", - "django>=1.8", + "django>=1.8,<=2.2.24", "pytz", "six", "django-ipware==2.1.0" From 05f988d57da2f7c26b0052acc22c021c58c7911f Mon Sep 17 00:00:00 2001 From: Alex Belyaev Date: Fri, 22 Apr 2022 22:10:13 +0300 Subject: [PATCH 153/153] Unblock Django 2.2 patch version upgrade --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 81856983f..8ceb8a92b 100644 --- a/setup.py +++ b/setup.py @@ -89,7 +89,7 @@ "django-appconf>=1.0.1", "jsonfield>=1.0.3", "stripe>=2.0", - "django>=1.8,<=2.2.24", + "django>=1.8,<2.3", "pytz", "six", "django-ipware==2.1.0"