From a03b6579c342ba5f3dc4c0ca91c57733df8bde98 Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Wed, 19 Apr 2017 20:21:03 +0200 Subject: [PATCH 01/16] ideal support; sry no tests yet! --- pinax/stripe/actions/customers.py | 1 - pinax/stripe/actions/sources.py | 52 +++++++++++++++++++++++++++ pinax/stripe/admin.py | 10 +++++- pinax/stripe/migrations/0008_ideal.py | 46 ++++++++++++++++++++++++ pinax/stripe/models.py | 22 ++++++++++++ 5 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 pinax/stripe/migrations/0008_ideal.py diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index 5606200bb..db5a456e3 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -50,7 +50,6 @@ def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_imme email=user.email, source=card, plan=plan, - quantity=quantity, trial_end=trial_end ) try: diff --git a/pinax/stripe/actions/sources.py b/pinax/stripe/actions/sources.py index f1cd9cdb7..ee1959bce 100644 --- a/pinax/stripe/actions/sources.py +++ b/pinax/stripe/actions/sources.py @@ -106,6 +106,44 @@ def sync_bitcoin(customer, source): return utils.update_with_defaults(receiver, defaults, created) +def sync_ideal(customer, source): + """ + Syncronizes the data for an ideal source locally for a given customer + + This is required since payment through ideal involves additional steps to be taken by the customer (select bank, enter codes, confirm, ...), updates will be made + available through webhooks or the return url and we then need to relate the payment to an instance of a source locally in order to process it further. + + Args: + customer: the customer to create or update the source for + source: data reprenting the source from the Stripe API + """ + defaults = dict( + customer=customer, + amount = utils.convert_amount_for_db(source["amount"], source["currency"]), # currency is in but in fact it's always eur + flow = source["flow"] or "", + livemode = source["livemode"], + owner_address = source["owner"]["address"] or "", + owner_email = source["owner"]["email"] or "", + owner_name = source["owner"]["name"] or "", + owner_phone = source["owner"]["phone"] or "", + owner_verified_address = source["owner"]["verified_address"] or "", + owner_verified_email = source["owner"]["verified_email"] or "", + owner_verified_name = source["owner"]["verified_name"] or "", + owner_verified_phone = source["owner"]["verified_phone"] or "", + redirect_return_url = source["redirect"]["return_url"] or "", + redirect_status = source["redirect"]["status"] or "", + redirect_url = source["redirect"]["url"] or "", + status = source["status"] or "", + usage =source["usage"] or "", + ideal_bank = source["ideal"]["bank"] or "", + ) + o, created = models.Ideal.objects.get_or_create( + stripe_id=source["id"], + defaults=defaults + ) + return utils.update_with_defaults(o, defaults, created) + + def sync_payment_source_from_stripe_data(customer, source): """ Syncronizes the data for a payment source locally for a given customer @@ -116,6 +154,8 @@ def sync_payment_source_from_stripe_data(customer, source): """ if source["id"].startswith("card_"): return sync_card(customer, source) + elif source["type"] == "ideal": + return sync_ideal(customer, source) else: return sync_bitcoin(customer, source) @@ -140,3 +180,15 @@ def update_card(customer, source, name=None, exp_month=None, exp_year=None): stripe_source.exp_year = exp_year s = stripe_source.save() return sync_payment_source_from_stripe_data(customer, s) + + +def create_ideal(customer, token): + """ + Attaches an ideal source to a customer + + Args: + customer: the customer to create the source for + token: the token created from Stripe.js + """ + source = customer.stripe_customer.sources.create(source=token) + return sync_payment_source_from_stripe_data(customer, source) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 74a7162c9..7d55cbfef 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -7,6 +7,7 @@ Subscription, Card, BitcoinReceiver, + Ideal, Customer, Event, EventProcessingException, @@ -195,6 +196,12 @@ class BitcoinReceiverInline(admin.TabularInline): max_num = 0 +class IdealInline(admin.TabularInline): + model = Ideal + extra = 0 + max_num = 0 + + def subscription_status(obj): return ", ".join([subscription.status for subscription in obj.subscription_set.all()]) subscription_status.short_description = "Subscription Status" @@ -224,7 +231,8 @@ def subscription_status(obj): inlines=[ SubscriptionInline, CardInline, - BitcoinReceiverInline + BitcoinReceiverInline, + IdealInline, ] ) diff --git a/pinax/stripe/migrations/0008_ideal.py b/pinax/stripe/migrations/0008_ideal.py new file mode 100644 index 000000000..333ef88f9 --- /dev/null +++ b/pinax/stripe/migrations/0008_ideal.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2017-04-19 14:32 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0007_auto_20170108_1202'), + ] + + operations = [ + migrations.CreateModel( + name='Ideal', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_id', models.CharField(max_length=255, unique=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('amount', models.DecimalField(decimal_places=2, max_digits=9, null=True)), + ('flow', models.CharField(max_length=32)), + ('livemode', models.BooleanField(default=False)), + ('owner_address', models.TextField(blank=True)), + ('owner_email', models.EmailField(blank=True, max_length=254)), + ('owner_name', models.TextField(blank=True)), + ('owner_phone', models.TextField(blank=True)), + ('owner_verified_address', models.TextField(blank=True)), + ('owner_verified_email', models.EmailField(blank=True, max_length=254)), + ('owner_verified_name', models.TextField(blank=True)), + ('owner_verified_phone', models.TextField(blank=True)), + ('redirect_return_url', models.URLField(max_length=1024)), + ('redirect_status', models.TextField(blank=True)), + ('redirect_url', models.URLField(max_length=1024)), + ('status', models.TextField(blank=True)), + ('usage', models.TextField(blank=True)), + ('ideal_bank', models.TextField(blank=True)), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Customer')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 66c0c5761..7457d14ea 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -182,6 +182,28 @@ class BitcoinReceiver(StripeObject): used_for_payment = models.BooleanField(default=False) +class Ideal(StripeObject): + + customer = models.ForeignKey(Customer, on_delete=models.CASCADE) + amount = models.DecimalField(decimal_places=2, max_digits=9, null=True) + flow = models.CharField(max_length=32) + livemode = models.BooleanField(default=False) + owner_address = models.TextField(blank=True) + owner_email = models.EmailField(blank=True) + owner_name = models.TextField(blank=True) + owner_phone = models.TextField(blank=True) + owner_verified_address = models.TextField(blank=True) + owner_verified_email = models.EmailField(blank=True) + owner_verified_name = models.TextField(blank=True) + owner_verified_phone = models.TextField(blank=True) + redirect_return_url = models.URLField(max_length=1024) + redirect_status = models.TextField(blank=True) + redirect_url = models.URLField(max_length=1024) + status = models.TextField(blank=True) + usage = models.TextField(blank=True) + ideal_bank = models.TextField(blank=True) + + class Subscription(StripeObject): customer = models.ForeignKey(Customer, on_delete=models.CASCADE) From bae60ab2f70adc127ec8a475f5f8d0bd7101f2c2 Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Thu, 20 Apr 2017 21:01:44 +0200 Subject: [PATCH 02/16] Extend webhooks required for ideal (source status) and add relevant fields to model --- docs/reference/webhooks.md | 4 ++++ pinax/stripe/actions/customers.py | 2 +- pinax/stripe/actions/sources.py | 8 +++++--- pinax/stripe/migrations/0008_ideal.py | 7 ++++--- pinax/stripe/models.py | 5 +++-- pinax/stripe/webhooks.py | 29 +++++++++++++++++++++++++++ 6 files changed, 46 insertions(+), 9 deletions(-) diff --git a/docs/reference/webhooks.md b/docs/reference/webhooks.md index 6d24c341f..628229304 100644 --- a/docs/reference/webhooks.md +++ b/docs/reference/webhooks.md @@ -112,6 +112,10 @@ details on how to wire those up. * `recipient.created` - Occurs whenever a recipient is created. * `recipient.deleted` - Occurs whenever a recipient is deleted. * `recipient.updated` - Occurs whenever a recipient is updated. +* `source.chargeable` - A Source object becomes chargeable after a customer has authenticated and verified a payment +* `source.canceled` - A Source object expired and cannot be used to create a charge +* `source.consumed` - A Source object that was single-use has already been charged +* `source.failed` - A Source object failed to become chargeable as your customer declined to authenticate the payment * `sku.created` - Occurs whenever a SKU is created. * `sku.updated` - Occurs whenever a SKU is updated. * `transfer.created` - Occurs whenever a new transfer is created. diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index db5a456e3..6ed47f20a 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -144,7 +144,7 @@ def set_default_source(customer, source): def sync_customer(customer, cu=None): """ - Syncronizes a local Customer object with details from the Stripe API + Synchronizes a local Customer object with details from the Stripe API Args: customer: a Customer object diff --git a/pinax/stripe/actions/sources.py b/pinax/stripe/actions/sources.py index ee1959bce..2ec92f9f0 100644 --- a/pinax/stripe/actions/sources.py +++ b/pinax/stripe/actions/sources.py @@ -119,6 +119,9 @@ def sync_ideal(customer, source): """ defaults = dict( customer=customer, + status = source["status"] or "", + type = source["type"] or "", + usage = source["usage"] or "", amount = utils.convert_amount_for_db(source["amount"], source["currency"]), # currency is in but in fact it's always eur flow = source["flow"] or "", livemode = source["livemode"], @@ -133,8 +136,6 @@ def sync_ideal(customer, source): redirect_return_url = source["redirect"]["return_url"] or "", redirect_status = source["redirect"]["status"] or "", redirect_url = source["redirect"]["url"] or "", - status = source["status"] or "", - usage =source["usage"] or "", ideal_bank = source["ideal"]["bank"] or "", ) o, created = models.Ideal.objects.get_or_create( @@ -154,7 +155,8 @@ def sync_payment_source_from_stripe_data(customer, source): """ if source["id"].startswith("card_"): return sync_card(customer, source) - elif source["type"] == "ideal": + elif source.get("type", None) == "ideal": + # ideal is created using Sources, only then will it have a type attribute return sync_ideal(customer, source) else: return sync_bitcoin(customer, source) diff --git a/pinax/stripe/migrations/0008_ideal.py b/pinax/stripe/migrations/0008_ideal.py index 333ef88f9..b1f217e4d 100644 --- a/pinax/stripe/migrations/0008_ideal.py +++ b/pinax/stripe/migrations/0008_ideal.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2017-04-19 14:32 +# Generated by Django 1.10 on 2017-04-20 13:25 from __future__ import unicode_literals from django.db import migrations, models @@ -20,6 +20,9 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('stripe_id', models.CharField(max_length=255, unique=True)), ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('status', models.CharField(blank=True, max_length=32)), + ('type', models.CharField(blank=True, max_length=32)), + ('usage', models.CharField(blank=True, max_length=32)), ('amount', models.DecimalField(decimal_places=2, max_digits=9, null=True)), ('flow', models.CharField(max_length=32)), ('livemode', models.BooleanField(default=False)), @@ -34,8 +37,6 @@ class Migration(migrations.Migration): ('redirect_return_url', models.URLField(max_length=1024)), ('redirect_status', models.TextField(blank=True)), ('redirect_url', models.URLField(max_length=1024)), - ('status', models.TextField(blank=True)), - ('usage', models.TextField(blank=True)), ('ideal_bank', models.TextField(blank=True)), ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Customer')), ], diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 7457d14ea..56fa1bff6 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -185,6 +185,9 @@ class BitcoinReceiver(StripeObject): class Ideal(StripeObject): customer = models.ForeignKey(Customer, on_delete=models.CASCADE) + status = models.CharField(max_length=32, blank=True) + type = models.CharField(max_length=32, blank=True) + usage = models.CharField(max_length=32, blank=True) amount = models.DecimalField(decimal_places=2, max_digits=9, null=True) flow = models.CharField(max_length=32) livemode = models.BooleanField(default=False) @@ -199,8 +202,6 @@ class Ideal(StripeObject): redirect_return_url = models.URLField(max_length=1024) redirect_status = models.TextField(blank=True) redirect_url = models.URLField(max_length=1024) - status = models.TextField(blank=True) - usage = models.TextField(blank=True) ideal_bank = models.TextField(blank=True) diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 9280673b2..895f0d34f 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -447,6 +447,35 @@ class SKUUpdatedWebhook(Webhook): description = "Occurs whenever a SKU is updated." +class SourceWebhook(Webhook): + + def process_webhook(self): + sources.sync_payment_source_from_stripe_data( + self.event.customer, + self.event.validated_message["data"]["object"] + ) + + +class SourceChargeableWebhook(SourceWebhook): + name = "source.chargeable" + description = "A Source object becomes chargeable after a customer has authenticated and verified a payment." + + +class SourceCanceledWebhook(SourceWebhook): + name = "source.canceled" + description = "A Source object expired and cannot be used to create a charge." + + +class SourceConsumedWebhook(SourceWebhook): + name = "source.consumed" + description = "A Source object that was single-use has already been charged." + + +class SourceFailedWebhook(SourceWebhook): + name = "source.failed" + description = "A Source object failed to become chargeable as your customer declined to authenticate the payment." + + class TransferWebhook(Webhook): def process_webhook(self): From c8acb983feff97e8dc304bc00729ead3b6a79c6a Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Fri, 21 Apr 2017 13:26:28 +0200 Subject: [PATCH 03/16] Allow for source in update subscription --- pinax/stripe/actions/subscriptions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index 271de2a3c..ba0c2892d 100644 --- a/pinax/stripe/actions/subscriptions.py +++ b/pinax/stripe/actions/subscriptions.py @@ -171,7 +171,7 @@ def sync_subscription_from_stripe_data(customer, subscription): return sub -def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, charge_immediately=False): +def update(subscription, plan=None, quantity=None, token=None, prorate=True, coupon=None, charge_immediately=False): """ Updates a subscription @@ -179,6 +179,10 @@ def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, ch subscription: the subscription to update plan: optionally, the plan to change the subscription to quantity: optionally, the quantiy of the subscription to change + token: if provided, a token from Stripe.js that will be used as the + payment source for the subscription and set as the default + source for the customer, otherwise the current default source + will be used 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 @@ -188,6 +192,8 @@ def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, ch stripe_subscription.plan = plan if quantity: stripe_subscription.quantity = quantity + if token: + stripe_subscription.token = token if not prorate: stripe_subscription.prorate = False if coupon: From 8011951faaf674b4d59f9efb1a71c4e8898cb53e Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Mon, 29 May 2017 14:16:19 +0200 Subject: [PATCH 04/16] Revert "Allow for source in update subscription" This reverts commit c8acb983feff97e8dc304bc00729ead3b6a79c6a. --- pinax/stripe/actions/subscriptions.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index ba0c2892d..271de2a3c 100644 --- a/pinax/stripe/actions/subscriptions.py +++ b/pinax/stripe/actions/subscriptions.py @@ -171,7 +171,7 @@ def sync_subscription_from_stripe_data(customer, subscription): return sub -def update(subscription, plan=None, quantity=None, token=None, prorate=True, coupon=None, charge_immediately=False): +def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, charge_immediately=False): """ Updates a subscription @@ -179,10 +179,6 @@ def update(subscription, plan=None, quantity=None, token=None, prorate=True, cou subscription: the subscription to update plan: optionally, the plan to change the subscription to quantity: optionally, the quantiy of the subscription to change - token: if provided, a token from Stripe.js that will be used as the - payment source for the subscription and set as the default - source for the customer, otherwise the current default source - will be used 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 @@ -192,8 +188,6 @@ def update(subscription, plan=None, quantity=None, token=None, prorate=True, cou stripe_subscription.plan = plan if quantity: stripe_subscription.quantity = quantity - if token: - stripe_subscription.token = token if not prorate: stripe_subscription.prorate = False if coupon: From b9e56edbe09e90d5fa0e7f80b844f1a2a5c9af18 Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Mon, 29 May 2017 14:16:30 +0200 Subject: [PATCH 05/16] Revert "Extend webhooks required for ideal (source status) and add relevant fields to model" This reverts commit bae60ab2f70adc127ec8a475f5f8d0bd7101f2c2. --- docs/reference/webhooks.md | 4 ---- pinax/stripe/actions/customers.py | 2 +- pinax/stripe/actions/sources.py | 8 +++----- pinax/stripe/migrations/0008_ideal.py | 7 +++---- pinax/stripe/models.py | 5 ++--- pinax/stripe/webhooks.py | 29 --------------------------- 6 files changed, 9 insertions(+), 46 deletions(-) diff --git a/docs/reference/webhooks.md b/docs/reference/webhooks.md index 628229304..6d24c341f 100644 --- a/docs/reference/webhooks.md +++ b/docs/reference/webhooks.md @@ -112,10 +112,6 @@ details on how to wire those up. * `recipient.created` - Occurs whenever a recipient is created. * `recipient.deleted` - Occurs whenever a recipient is deleted. * `recipient.updated` - Occurs whenever a recipient is updated. -* `source.chargeable` - A Source object becomes chargeable after a customer has authenticated and verified a payment -* `source.canceled` - A Source object expired and cannot be used to create a charge -* `source.consumed` - A Source object that was single-use has already been charged -* `source.failed` - A Source object failed to become chargeable as your customer declined to authenticate the payment * `sku.created` - Occurs whenever a SKU is created. * `sku.updated` - Occurs whenever a SKU is updated. * `transfer.created` - Occurs whenever a new transfer is created. diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index 6ed47f20a..db5a456e3 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -144,7 +144,7 @@ def set_default_source(customer, source): def sync_customer(customer, cu=None): """ - Synchronizes a local Customer object with details from the Stripe API + Syncronizes a local Customer object with details from the Stripe API Args: customer: a Customer object diff --git a/pinax/stripe/actions/sources.py b/pinax/stripe/actions/sources.py index 2ec92f9f0..ee1959bce 100644 --- a/pinax/stripe/actions/sources.py +++ b/pinax/stripe/actions/sources.py @@ -119,9 +119,6 @@ def sync_ideal(customer, source): """ defaults = dict( customer=customer, - status = source["status"] or "", - type = source["type"] or "", - usage = source["usage"] or "", amount = utils.convert_amount_for_db(source["amount"], source["currency"]), # currency is in but in fact it's always eur flow = source["flow"] or "", livemode = source["livemode"], @@ -136,6 +133,8 @@ def sync_ideal(customer, source): redirect_return_url = source["redirect"]["return_url"] or "", redirect_status = source["redirect"]["status"] or "", redirect_url = source["redirect"]["url"] or "", + status = source["status"] or "", + usage =source["usage"] or "", ideal_bank = source["ideal"]["bank"] or "", ) o, created = models.Ideal.objects.get_or_create( @@ -155,8 +154,7 @@ def sync_payment_source_from_stripe_data(customer, source): """ if source["id"].startswith("card_"): return sync_card(customer, source) - elif source.get("type", None) == "ideal": - # ideal is created using Sources, only then will it have a type attribute + elif source["type"] == "ideal": return sync_ideal(customer, source) else: return sync_bitcoin(customer, source) diff --git a/pinax/stripe/migrations/0008_ideal.py b/pinax/stripe/migrations/0008_ideal.py index b1f217e4d..333ef88f9 100644 --- a/pinax/stripe/migrations/0008_ideal.py +++ b/pinax/stripe/migrations/0008_ideal.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2017-04-20 13:25 +# Generated by Django 1.10 on 2017-04-19 14:32 from __future__ import unicode_literals from django.db import migrations, models @@ -20,9 +20,6 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('stripe_id', models.CharField(max_length=255, unique=True)), ('created_at', models.DateTimeField(default=django.utils.timezone.now)), - ('status', models.CharField(blank=True, max_length=32)), - ('type', models.CharField(blank=True, max_length=32)), - ('usage', models.CharField(blank=True, max_length=32)), ('amount', models.DecimalField(decimal_places=2, max_digits=9, null=True)), ('flow', models.CharField(max_length=32)), ('livemode', models.BooleanField(default=False)), @@ -37,6 +34,8 @@ class Migration(migrations.Migration): ('redirect_return_url', models.URLField(max_length=1024)), ('redirect_status', models.TextField(blank=True)), ('redirect_url', models.URLField(max_length=1024)), + ('status', models.TextField(blank=True)), + ('usage', models.TextField(blank=True)), ('ideal_bank', models.TextField(blank=True)), ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Customer')), ], diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 10d3f0f2d..c9a92137f 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -185,9 +185,6 @@ class BitcoinReceiver(StripeObject): class Ideal(StripeObject): customer = models.ForeignKey(Customer, on_delete=models.CASCADE) - status = models.CharField(max_length=32, blank=True) - type = models.CharField(max_length=32, blank=True) - usage = models.CharField(max_length=32, blank=True) amount = models.DecimalField(decimal_places=2, max_digits=9, null=True) flow = models.CharField(max_length=32) livemode = models.BooleanField(default=False) @@ -202,6 +199,8 @@ class Ideal(StripeObject): redirect_return_url = models.URLField(max_length=1024) redirect_status = models.TextField(blank=True) redirect_url = models.URLField(max_length=1024) + status = models.TextField(blank=True) + usage = models.TextField(blank=True) ideal_bank = models.TextField(blank=True) diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 895f0d34f..9280673b2 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -447,35 +447,6 @@ class SKUUpdatedWebhook(Webhook): description = "Occurs whenever a SKU is updated." -class SourceWebhook(Webhook): - - def process_webhook(self): - sources.sync_payment_source_from_stripe_data( - self.event.customer, - self.event.validated_message["data"]["object"] - ) - - -class SourceChargeableWebhook(SourceWebhook): - name = "source.chargeable" - description = "A Source object becomes chargeable after a customer has authenticated and verified a payment." - - -class SourceCanceledWebhook(SourceWebhook): - name = "source.canceled" - description = "A Source object expired and cannot be used to create a charge." - - -class SourceConsumedWebhook(SourceWebhook): - name = "source.consumed" - description = "A Source object that was single-use has already been charged." - - -class SourceFailedWebhook(SourceWebhook): - name = "source.failed" - description = "A Source object failed to become chargeable as your customer declined to authenticate the payment." - - class TransferWebhook(Webhook): def process_webhook(self): From 810a0663716dbfcf7802daf00c75b704163867bf Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Mon, 29 May 2017 14:16:33 +0200 Subject: [PATCH 06/16] Revert "ideal support; sry no tests yet!" This reverts commit a03b6579c342ba5f3dc4c0ca91c57733df8bde98. --- pinax/stripe/actions/customers.py | 1 + pinax/stripe/actions/sources.py | 52 --------------------------- pinax/stripe/admin.py | 10 +----- pinax/stripe/migrations/0008_ideal.py | 46 ------------------------ pinax/stripe/models.py | 22 ------------ 5 files changed, 2 insertions(+), 129 deletions(-) delete mode 100644 pinax/stripe/migrations/0008_ideal.py diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py index db5a456e3..5606200bb 100644 --- a/pinax/stripe/actions/customers.py +++ b/pinax/stripe/actions/customers.py @@ -50,6 +50,7 @@ def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_imme email=user.email, source=card, plan=plan, + quantity=quantity, trial_end=trial_end ) try: diff --git a/pinax/stripe/actions/sources.py b/pinax/stripe/actions/sources.py index ee1959bce..f1cd9cdb7 100644 --- a/pinax/stripe/actions/sources.py +++ b/pinax/stripe/actions/sources.py @@ -106,44 +106,6 @@ def sync_bitcoin(customer, source): return utils.update_with_defaults(receiver, defaults, created) -def sync_ideal(customer, source): - """ - Syncronizes the data for an ideal source locally for a given customer - - This is required since payment through ideal involves additional steps to be taken by the customer (select bank, enter codes, confirm, ...), updates will be made - available through webhooks or the return url and we then need to relate the payment to an instance of a source locally in order to process it further. - - Args: - customer: the customer to create or update the source for - source: data reprenting the source from the Stripe API - """ - defaults = dict( - customer=customer, - amount = utils.convert_amount_for_db(source["amount"], source["currency"]), # currency is in but in fact it's always eur - flow = source["flow"] or "", - livemode = source["livemode"], - owner_address = source["owner"]["address"] or "", - owner_email = source["owner"]["email"] or "", - owner_name = source["owner"]["name"] or "", - owner_phone = source["owner"]["phone"] or "", - owner_verified_address = source["owner"]["verified_address"] or "", - owner_verified_email = source["owner"]["verified_email"] or "", - owner_verified_name = source["owner"]["verified_name"] or "", - owner_verified_phone = source["owner"]["verified_phone"] or "", - redirect_return_url = source["redirect"]["return_url"] or "", - redirect_status = source["redirect"]["status"] or "", - redirect_url = source["redirect"]["url"] or "", - status = source["status"] or "", - usage =source["usage"] or "", - ideal_bank = source["ideal"]["bank"] or "", - ) - o, created = models.Ideal.objects.get_or_create( - stripe_id=source["id"], - defaults=defaults - ) - return utils.update_with_defaults(o, defaults, created) - - def sync_payment_source_from_stripe_data(customer, source): """ Syncronizes the data for a payment source locally for a given customer @@ -154,8 +116,6 @@ def sync_payment_source_from_stripe_data(customer, source): """ if source["id"].startswith("card_"): return sync_card(customer, source) - elif source["type"] == "ideal": - return sync_ideal(customer, source) else: return sync_bitcoin(customer, source) @@ -180,15 +140,3 @@ def update_card(customer, source, name=None, exp_month=None, exp_year=None): stripe_source.exp_year = exp_year s = stripe_source.save() return sync_payment_source_from_stripe_data(customer, s) - - -def create_ideal(customer, token): - """ - Attaches an ideal source to a customer - - Args: - customer: the customer to create the source for - token: the token created from Stripe.js - """ - source = customer.stripe_customer.sources.create(source=token) - return sync_payment_source_from_stripe_data(customer, source) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index 7d55cbfef..74a7162c9 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -7,7 +7,6 @@ Subscription, Card, BitcoinReceiver, - Ideal, Customer, Event, EventProcessingException, @@ -196,12 +195,6 @@ class BitcoinReceiverInline(admin.TabularInline): max_num = 0 -class IdealInline(admin.TabularInline): - model = Ideal - extra = 0 - max_num = 0 - - def subscription_status(obj): return ", ".join([subscription.status for subscription in obj.subscription_set.all()]) subscription_status.short_description = "Subscription Status" @@ -231,8 +224,7 @@ def subscription_status(obj): inlines=[ SubscriptionInline, CardInline, - BitcoinReceiverInline, - IdealInline, + BitcoinReceiverInline ] ) diff --git a/pinax/stripe/migrations/0008_ideal.py b/pinax/stripe/migrations/0008_ideal.py deleted file mode 100644 index 333ef88f9..000000000 --- a/pinax/stripe/migrations/0008_ideal.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2017-04-19 14:32 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0007_auto_20170108_1202'), - ] - - operations = [ - migrations.CreateModel( - name='Ideal', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('stripe_id', models.CharField(max_length=255, unique=True)), - ('created_at', models.DateTimeField(default=django.utils.timezone.now)), - ('amount', models.DecimalField(decimal_places=2, max_digits=9, null=True)), - ('flow', models.CharField(max_length=32)), - ('livemode', models.BooleanField(default=False)), - ('owner_address', models.TextField(blank=True)), - ('owner_email', models.EmailField(blank=True, max_length=254)), - ('owner_name', models.TextField(blank=True)), - ('owner_phone', models.TextField(blank=True)), - ('owner_verified_address', models.TextField(blank=True)), - ('owner_verified_email', models.EmailField(blank=True, max_length=254)), - ('owner_verified_name', models.TextField(blank=True)), - ('owner_verified_phone', models.TextField(blank=True)), - ('redirect_return_url', models.URLField(max_length=1024)), - ('redirect_status', models.TextField(blank=True)), - ('redirect_url', models.URLField(max_length=1024)), - ('status', models.TextField(blank=True)), - ('usage', models.TextField(blank=True)), - ('ideal_bank', models.TextField(blank=True)), - ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Customer')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index c9a92137f..a5f3e2146 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -182,28 +182,6 @@ class BitcoinReceiver(StripeObject): used_for_payment = models.BooleanField(default=False) -class Ideal(StripeObject): - - customer = models.ForeignKey(Customer, on_delete=models.CASCADE) - amount = models.DecimalField(decimal_places=2, max_digits=9, null=True) - flow = models.CharField(max_length=32) - livemode = models.BooleanField(default=False) - owner_address = models.TextField(blank=True) - owner_email = models.EmailField(blank=True) - owner_name = models.TextField(blank=True) - owner_phone = models.TextField(blank=True) - owner_verified_address = models.TextField(blank=True) - owner_verified_email = models.EmailField(blank=True) - owner_verified_name = models.TextField(blank=True) - owner_verified_phone = models.TextField(blank=True) - redirect_return_url = models.URLField(max_length=1024) - redirect_status = models.TextField(blank=True) - redirect_url = models.URLField(max_length=1024) - status = models.TextField(blank=True) - usage = models.TextField(blank=True) - ideal_bank = models.TextField(blank=True) - - class Subscription(StripeObject): customer = models.ForeignKey(Customer, on_delete=models.CASCADE) From 4501b0dac1208bbf07ec210e402b55858f804455 Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Tue, 30 May 2017 08:37:03 +0200 Subject: [PATCH 07/16] Add action to create invoice item --- pinax/stripe/actions/invoices.py | 48 +++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/actions/invoices.py b/pinax/stripe/actions/invoices.py index a65d3879c..8d4ad495c 100644 --- a/pinax/stripe/actions/invoices.py +++ b/pinax/stripe/actions/invoices.py @@ -65,9 +65,55 @@ def pay(invoice, send_receipt=True): return False +def create_invoice_item(customer, invoice, subscription, amount, currency, description, metadata=None): + """ + :param customer: The pinax-stripe Customer + :param invoice: + :param subscription: + :param amount: + :param currency: + :param description: + :param metadata: Any optional metadata that is attached to the invoice item + :return: + """ + stripe_invoice_item = stripe.InvoiceItem.create( + customer=customer.stripe_id, + amount=utils.convert_amount_for_api(amount, currency), + currency=currency, + description=description, + invoice=invoice.stripe_id, + metadata=metadata, + subscription=subscription.stripe_id, + ) + + period_end = utils.convert_tstamp(stripe_invoice_item["period"], "end") + period_start = utils.convert_tstamp(stripe_invoice_item["period"], "start") + + # We can safely take the plan from the subscription here because we are creating a new invoice item for this new invoice that is applicable + # to the current subscription/current plan. + plan = subscription.plan + + defaults = dict( + amount=utils.convert_amount_for_db(stripe_invoice_item["amount"], stripe_invoice_item["currency"]), + currency=stripe_invoice_item["currency"], + proration=stripe_invoice_item["proration"], + description=description, + line_type=stripe_invoice_item["object"], + plan=plan, + period_start=period_start, + period_end=period_end, + quantity=stripe_invoice_item.get("quantity"), + subscription=subscription, + ) + inv_item, inv_item_created = invoice.items.get_or_create( + stripe_id=stripe_invoice_item["id"], + defaults=defaults + ) + return utils.update_with_defaults(inv_item, defaults, inv_item_created) + def sync_invoice_from_stripe_data(stripe_invoice, send_receipt=settings.PINAX_STRIPE_SEND_EMAIL_RECEIPTS): """ - Syncronizes a local invoice with data from the Stripe API + Synchronizes a local invoice with data from the Stripe API Args: stripe_invoice: data that represents the invoice from the Stripe API From 95145cd15f01ebb674be6d6e197308ae0cb2ec3e Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Thu, 19 Oct 2017 16:54:08 +0200 Subject: [PATCH 08/16] Support billing/due_date on subscription and invoice + support adding invoice items --- pinax/stripe/actions/invoices.py | 75 +++++++++++++++++++ pinax/stripe/actions/subscriptions.py | 18 ++++- .../migrations/0011_auto_20171019_1321.py | 51 +++++++++++++ pinax/stripe/models.py | 8 +- pinax/stripe/webhooks.py | 5 ++ 5 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 pinax/stripe/migrations/0011_auto_20171019_1321.py diff --git a/pinax/stripe/actions/invoices.py b/pinax/stripe/actions/invoices.py index b1b9ba124..363159228 100644 --- a/pinax/stripe/actions/invoices.py +++ b/pinax/stripe/actions/invoices.py @@ -63,6 +63,66 @@ def pay(invoice, send_receipt=True): return False +def paid(invoice): + """ + Sometimes customers may want to pay with payment methods outside of Stripe, such as check. + In these situations, Stripe still allows you to keep track of the payment status of your invoices. + Once you receive an invoice payment from a customer outside of Stripe, you can manually + mark their invoices as paid. + + Args: + invoice: the invoice object to close + """ + if not invoice.paid: + stripe_invoice = invoice.stripe_invoice + stripe_invoice.paid = True + stripe_invoice_ = stripe_invoice.save() + sync_invoice_from_stripe_data(stripe_invoice_) + + +def forgive(invoice): + """ + Forgiving an invoice instructs us to update the subscription status as if the invoice were + successfully paid. Once an invoice has been forgiven, it cannot be unforgiven or reopened. + + Args: + invoice: the invoice object to close + """ + if not invoice.paid: + stripe_invoice = invoice.stripe_invoice + stripe_invoice.forgiven = True + stripe_invoice_ = stripe_invoice.save() + sync_invoice_from_stripe_data(stripe_invoice_) + + +def close(invoice): + """ + Cause an invoice to be closed; This prevents Stripe from automatically charging your customer for the invoice amount. + + Args: + invoice: the invoice object to close + """ + if not invoice.closed: + stripe_invoice = invoice.stripe_invoice + stripe_invoice.closed = True + stripe_invoice_ = stripe_invoice.save() + sync_invoice_from_stripe_data(stripe_invoice_) + + +def open(invoice): + """ + (re)-open a closed invoice (which is hold for review) + + Args: + invoice: the invoice object to open + """ + if invoice.closed: + stripe_invoice = invoice.stripe_invoice + stripe_invoice.closed = False + stripe_invoice_ = stripe_invoice.save() + sync_invoice_from_stripe_data(stripe_invoice_) + + def create_invoice_item(customer, invoice, subscription, amount, currency, description, metadata=None): """ :param customer: The pinax-stripe Customer @@ -80,6 +140,7 @@ def create_invoice_item(customer, invoice, subscription, amount, currency, descr currency=currency, description=description, invoice=invoice.stripe_id, + discountable=True, metadata=metadata, subscription=subscription.stripe_id, ) @@ -142,6 +203,7 @@ def sync_invoice_from_stripe_data(stripe_invoice, send_receipt=settings.PINAX_ST attempt_count=stripe_invoice["attempt_count"], amount_due=utils.convert_amount_for_db(stripe_invoice["amount_due"], stripe_invoice["currency"]), closed=stripe_invoice["closed"], + forgiven=stripe_invoice["forgiven"], paid=stripe_invoice["paid"], period_end=period_end, period_start=period_start, @@ -154,7 +216,13 @@ def sync_invoice_from_stripe_data(stripe_invoice, send_receipt=settings.PINAX_ST charge=charge, subscription=subscription, receipt_number=stripe_invoice["receipt_number"] or "", + metadata=stripe_invoice["metadata"] ) + if "billing" in stripe_invoice: + defaults.update({ + "billing": stripe_invoice["billing"], + "due_date": utils.convert_tstamp(stripe_invoice, "due_date") if stripe_invoice.get("due_date", None) is not None else None + }) invoice, created = models.Invoice.objects.get_or_create( stripe_id=stripe_invoice["id"], defaults=defaults @@ -180,6 +248,13 @@ def sync_invoices_for_customer(customer): sync_invoice_from_stripe_data(invoice, send_receipt=False) +def sync_invoice(invoice): + """ + Syncronizes a specific invoice + """ + sync_invoice_from_stripe_data(invoice.stripe_invoice, send_receipt=False) + + def sync_invoice_items(invoice, items): """ Syncronizes all invoice line items for a particular invoice diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py index 21f08bd3f..da73c9b0b 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, **kwargs): """ 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 + kwargs: any additional arguments are passed, easy for new features Returns: the data representing the subscription object that was created @@ -42,7 +43,7 @@ def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=No quantity = hooks.hookset.adjust_subscription_quantity(customer=customer, plan=plan, quantity=quantity) cu = customer.stripe_customer - subscription_params = {} + subscription_params = kwargs if trial_days: subscription_params["trial_end"] = datetime.datetime.utcnow() + datetime.timedelta(days=trial_days) if token: @@ -156,6 +157,11 @@ def sync_subscription_from_stripe_data(customer, subscription): 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 ) + if "billing" in subscription: + defaults.update({ + "billing": subscription["billing"], + "days_until_due": subscription["days_until_due"] if "days_until_due" in subscription else None, + }) sub, created = models.Subscription.objects.get_or_create( stripe_id=subscription["id"], defaults=defaults @@ -164,7 +170,7 @@ def sync_subscription_from_stripe_data(customer, subscription): return sub -def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, charge_immediately=False): +def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, charge_immediately=False, billing=None, days_until_due=None): """ Updates a subscription @@ -175,6 +181,8 @@ def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, ch 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 + billing: Either charge_automatically or send_invoice + days_until_due: Number of days a customer has to pay invoices generated by this subscription. Only valid for subscriptions where billing=send_invoice. """ stripe_subscription = subscription.stripe_subscription if plan: @@ -188,6 +196,10 @@ def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, ch if charge_immediately: if stripe_subscription.trial_end is not None and utils.convert_tstamp(stripe_subscription.trial_end) > timezone.now(): stripe_subscription.trial_end = "now" + if billing is not None: + stripe_subscription.billing = billing + if days_until_due is not None: + stripe_subscription.days_until_due = days_until_due sub = stripe_subscription.save() customer = models.Customer.objects.get(pk=subscription.customer.pk) sync_subscription_from_stripe_data(customer, sub) diff --git a/pinax/stripe/migrations/0011_auto_20171019_1321.py b/pinax/stripe/migrations/0011_auto_20171019_1321.py new file mode 100644 index 000000000..e301a231b --- /dev/null +++ b/pinax/stripe/migrations/0011_auto_20171019_1321.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-19 13:21 +from __future__ import unicode_literals + +from django.db import migrations, models +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0010_connect'), + ] + + operations = [ + migrations.AddField( + model_name='invoice', + name='billing', + field=models.CharField(default='charge_automatically', max_length=32), + ), + migrations.AddField( + model_name='invoice', + name='due_date', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='invoice', + name='forgiven', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='invoice', + name='metadata', + field=jsonfield.fields.JSONField(null=True), + ), + migrations.AddField( + model_name='subscription', + name='billing', + field=models.CharField(default='charge_automatically', max_length=32), + ), + migrations.AddField( + model_name='subscription', + name='days_until_due', + field=models.IntegerField(blank=True, default=None, 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), + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index b1561e35b..6d842dfd7 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -228,7 +228,7 @@ class Subscription(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, blank=True, null=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) @@ -240,6 +240,8 @@ class Subscription(StripeObject): 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) + billing = models.CharField(max_length=32, default=u'charge_automatically') # charge_automatically or send_invoice + days_until_due = models.IntegerField(default=None, blank=True, null=True) @property def stripe_subscription(self): @@ -278,6 +280,7 @@ class Invoice(StripeObject): statement_descriptor = models.TextField(blank=True) currency = models.CharField(max_length=10, default="usd") closed = models.BooleanField(default=False) + forgiven = models.BooleanField(default=False) description = models.TextField(blank=True) paid = models.BooleanField(default=False) receipt_number = models.TextField(blank=True) @@ -289,6 +292,9 @@ class Invoice(StripeObject): total = models.DecimalField(decimal_places=2, max_digits=9) date = models.DateTimeField() webhooks_delivered_at = models.DateTimeField(null=True) + billing = models.CharField(max_length=32, default=u'charge_automatically') # charge_automatically or send_invoice + due_date = models.DateTimeField(null=True, blank=True) + metadata = JSONField(null=True) @property def status(self): diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index a23196c2c..66135f3ff 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -382,6 +382,11 @@ def process_webhook(self): ) +class InvoiceUpcomingWebhook(InvoiceWebhook): + name = "invoice.upcoming" + description = "Occurs X number of days before a subscription is scheduled to create an invoice that is charged automatically, where X is determined by your subscriptions settings." + + class InvoiceCreatedWebhook(InvoiceWebhook): name = "invoice.created" description = "Occurs whenever a new invoice is created. If you are using webhooks, Stripe will wait one hour after they have all succeeded to attempt to pay the invoice; the only exception here is on the first invoice, which gets created and paid immediately when you subscribe a customer to a plan. If your webhooks do not all respond successfully, Stripe will continue retrying the webhooks every hour and will not attempt to pay the invoice. After 3 days, Stripe will attempt to pay the invoice regardless of whether or not your webhooks have succeeded. See how to respond to a webhook." From 9e472e9713c625963f818d3b8230f4cd9f564060 Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Thu, 26 Oct 2017 17:05:28 +0200 Subject: [PATCH 09/16] Update after review --- pinax/stripe/actions/invoices.py | 17 ++++++------- .../migrations/0012_auto_20171026_1544.py | 25 +++++++++++++++++++ pinax/stripe/models.py | 21 +++++++++++++--- pinax/stripe/webhooks.py | 5 +++- 4 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 pinax/stripe/migrations/0012_auto_20171026_1544.py diff --git a/pinax/stripe/actions/invoices.py b/pinax/stripe/actions/invoices.py index 363159228..acc3b4ca4 100644 --- a/pinax/stripe/actions/invoices.py +++ b/pinax/stripe/actions/invoices.py @@ -63,7 +63,7 @@ def pay(invoice, send_receipt=True): return False -def paid(invoice): +def mark_paid(invoice): """ Sometimes customers may want to pay with payment methods outside of Stripe, such as check. In these situations, Stripe still allows you to keep track of the payment status of your invoices. @@ -76,8 +76,7 @@ def paid(invoice): if not invoice.paid: stripe_invoice = invoice.stripe_invoice stripe_invoice.paid = True - stripe_invoice_ = stripe_invoice.save() - sync_invoice_from_stripe_data(stripe_invoice_) + sync_invoice_from_stripe_data(stripe_invoice.save()) def forgive(invoice): @@ -91,8 +90,7 @@ def forgive(invoice): if not invoice.paid: stripe_invoice = invoice.stripe_invoice stripe_invoice.forgiven = True - stripe_invoice_ = stripe_invoice.save() - sync_invoice_from_stripe_data(stripe_invoice_) + sync_invoice_from_stripe_data(stripe_invoice.save()) def close(invoice): @@ -105,11 +103,10 @@ def close(invoice): if not invoice.closed: stripe_invoice = invoice.stripe_invoice stripe_invoice.closed = True - stripe_invoice_ = stripe_invoice.save() - sync_invoice_from_stripe_data(stripe_invoice_) + sync_invoice_from_stripe_data(stripe_invoice.save()) -def open(invoice): +def reopen(invoice): """ (re)-open a closed invoice (which is hold for review) @@ -119,8 +116,7 @@ def open(invoice): if invoice.closed: stripe_invoice = invoice.stripe_invoice stripe_invoice.closed = False - stripe_invoice_ = stripe_invoice.save() - sync_invoice_from_stripe_data(stripe_invoice_) + sync_invoice_from_stripe_data(stripe_invoice.save()) def create_invoice_item(customer, invoice, subscription, amount, currency, description, metadata=None): @@ -170,6 +166,7 @@ def create_invoice_item(customer, invoice, subscription, amount, currency, descr ) return utils.update_with_defaults(inv_item, defaults, inv_item_created) + def sync_invoice_from_stripe_data(stripe_invoice, send_receipt=settings.PINAX_STRIPE_SEND_EMAIL_RECEIPTS): """ Synchronizes a local invoice with data from the Stripe API diff --git a/pinax/stripe/migrations/0012_auto_20171026_1544.py b/pinax/stripe/migrations/0012_auto_20171026_1544.py new file mode 100644 index 000000000..408a70c86 --- /dev/null +++ b/pinax/stripe/migrations/0012_auto_20171026_1544.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-26 15:44 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0011_auto_20171019_1321'), + ] + + operations = [ + migrations.AlterField( + model_name='invoice', + name='billing', + field=models.CharField(choices=[('charge_automatically', 'Charge automatically'), ('send_invoice', 'Send invoice')], default='charge_automatically', max_length=32), + ), + migrations.AlterField( + model_name='subscription', + name='billing', + field=models.CharField(choices=[('charge_automatically', 'Charge automatically'), ('send_invoice', 'Send invoice')], default='charge_automatically', max_length=32), + ), + ] diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 6d842dfd7..7eb709989 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -226,7 +226,6 @@ class BitcoinReceiver(StripeObject): class Subscription(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, blank=True, null=True) cancel_at_period_end = models.BooleanField(default=False) @@ -240,7 +239,15 @@ class Subscription(StripeObject): 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) - billing = models.CharField(max_length=32, default=u'charge_automatically') # charge_automatically or send_invoice + + BILLING_CHARGE_AUTOMATICALLY = "charge_automatically" + BILLING_SEND_INVOICE = "send_invoice" + BILLING_CHOICES = ( + (BILLING_CHARGE_AUTOMATICALLY, "Charge automatically"), + (BILLING_SEND_INVOICE, "Send invoice"), + ) + billing = models.CharField(max_length=32, choices=BILLING_CHOICES, default=BILLING_CHARGE_AUTOMATICALLY) + days_until_due = models.IntegerField(default=None, blank=True, null=True) @property @@ -292,7 +299,15 @@ class Invoice(StripeObject): total = models.DecimalField(decimal_places=2, max_digits=9) date = models.DateTimeField() webhooks_delivered_at = models.DateTimeField(null=True) - billing = models.CharField(max_length=32, default=u'charge_automatically') # charge_automatically or send_invoice + + BILLING_CHARGE_AUTOMATICALLY = "charge_automatically" + BILLING_SEND_INVOICE = "send_invoice" + BILLING_CHOICES = ( + (BILLING_CHARGE_AUTOMATICALLY, "Charge automatically"), + (BILLING_SEND_INVOICE, "Send invoice"), + ) + billing = models.CharField(max_length=32, choices=BILLING_CHOICES, default=BILLING_CHARGE_AUTOMATICALLY) + due_date = models.DateTimeField(null=True, blank=True) metadata = JSONField(null=True) diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 66135f3ff..2acf8b18b 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -382,7 +382,10 @@ def process_webhook(self): ) -class InvoiceUpcomingWebhook(InvoiceWebhook): +class InvoiceUpcomingWebhook(Webhook): + """ + Notice on invoice.upcoming, the invoice has not been created yet, and therefor we cannot sync it (the payload does not have an id) and so it does not inherit from InvoiceWebhook + """ name = "invoice.upcoming" description = "Occurs X number of days before a subscription is scheduled to create an invoice that is charged automatically, where X is determined by your subscriptions settings." From 04a6d1c96253b3b2e5ff058a53af310507521231 Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Mon, 30 Oct 2017 12:55:16 +0100 Subject: [PATCH 10/16] Merge migrations --- .../migrations/0014_merge_20171030_1242.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pinax/stripe/migrations/0014_merge_20171030_1242.py diff --git a/pinax/stripe/migrations/0014_merge_20171030_1242.py b/pinax/stripe/migrations/0014_merge_20171030_1242.py new file mode 100644 index 000000000..81e01d5de --- /dev/null +++ b/pinax/stripe/migrations/0014_merge_20171030_1242.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-30 12:42 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0013_auto_20171025_2153'), + ('pinax_stripe', '0012_auto_20171026_1544'), + ] + + operations = [ + ] From 32118212ea8379574de24b943d14eab128604bc9 Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Tue, 31 Oct 2017 23:32:59 +0100 Subject: [PATCH 11/16] Add tests --- pinax/stripe/tests/test_actions.py | 63 ++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index 812a102b4..e492058aa 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -471,6 +471,34 @@ def test_pay_invoice_closed(self): self.assertFalse(invoices.pay(invoice)) self.assertFalse(invoice.stripe_invoice.pay.called) + @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") + def test_mark_paid(self, SyncMock): + invoice = Mock() + self.assertTrue(invoices.mark_paid(invoice)) + self.assertTrue(invoice.stripe_invoice.paid) + self.assertTrue(SyncMock.called) + + @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") + def test_forgive(self, SyncMock): + invoice = Mock() + self.assertTrue(invoices.forgive(invoice)) + self.assertTrue(invoice.stripe_invoice.forgiven) + self.assertTrue(SyncMock.called) + + @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") + def test_close(self, SyncMock): + invoice = Mock() + self.assertTrue(invoices.close(invoice)) + self.assertTrue(invoice.stripe_invoice.closed) + self.assertTrue(SyncMock.called) + + @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") + def test_reopen(self, SyncMock): + invoice = Mock() + self.assertTrue(invoices.reopen(invoice)) + self.assertFalse(invoice.stripe_invoice.closed) + self.assertTrue(SyncMock.called) + @patch("stripe.Invoice.create") def test_create_and_pay(self, CreateMock): invoice = CreateMock() @@ -498,6 +526,41 @@ def test_create_and_pay_invalid_request_error_on_create(self, CreateMock): CreateMock.side_effect = stripe.InvalidRequestError("Bad", "error") self.assertFalse(invoices.create_and_pay(Mock())) + @patch("stripe.InvoiceItem.create") + def test_create_invoice_item(self, CreateMock): + customer = Mock() + invoice = Mock() + invoice.stripe_id = "my_id" + invoice.items = Mock() + invoice.items.get_or_create.return_value = None, True + subscription = Mock() + CreateMock.return_value = { + "id": "my_id", + "object": "invoiceitem", + "amount": 100, + "currency": "eur", + "customer": customer, + "date": 1496069416, + "description": "my_description", + "discountable": True, + "invoice": invoice, + "livemode": False, + "metadata": { + + }, + "period": { + "start": 1496069416, + "end": 1496069416 + }, + "plan": None, + "proration": False, + "quantity": None, + "subscription": subscription, + } + self.assertTrue(invoices.create_invoice_item(customer, invoice, subscription, 100, "eur", "my_foo", metadata={})) + self.assertTrue(CreateMock.called) + self.assertTrue(invoice.items.get_or_create.called) + class RefundsTests(TestCase): From 975bb9b73bd3252cdf208023a708fb4722d4a077 Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Mon, 6 Nov 2017 13:43:06 +0100 Subject: [PATCH 12/16] Fix issue that subscription is not sync'ed when change made from Stripe dashboard --- pinax/stripe/webhooks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index 352dfa4c3..80cfc4f9c 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -96,7 +96,8 @@ def validate(self): cls=stripe.StripeObjectEncoder ) ) - self.event.valid = self.event.webhook_message["data"] == self.event.validated_message["data"] + # Notice "data" may contain a "previous_attributes" section + self.event.valid = "object" in self.event.webhook_message["data"] and "object" in self.event.validated_message["data"] and self.event.webhook_message["data"]["object"] == self.event.validated_message["data"]["object"] self.event.save() def send_signal(self): From 27bd049559671b86785b75ed6678ddb6ba2fe6c9 Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Wed, 6 Dec 2017 22:56:21 +0100 Subject: [PATCH 13/16] Fix default in migration --- .../migrations/0011_auto_20171121_1648_fix_step2.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pinax/stripe/migrations/0011_auto_20171121_1648_fix_step2.py b/pinax/stripe/migrations/0011_auto_20171121_1648_fix_step2.py index 204dd4f12..dcd4f79b6 100644 --- a/pinax/stripe/migrations/0011_auto_20171121_1648_fix_step2.py +++ b/pinax/stripe/migrations/0011_auto_20171121_1648_fix_step2.py @@ -33,25 +33,21 @@ class Migration(migrations.Migration): migrations.AddField( model_name='customer', name='stripe_account', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, - to='pinax_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, null=True, on_delete=django.db.models.deletion.CASCADE, - to='pinax_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, null=True, on_delete=django.db.models.deletion.CASCADE, - to='pinax_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, null=True, on_delete=django.db.models.deletion.CASCADE, - to='pinax_stripe.Account'), + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Account'), ), ] From 01c88b652b01be24f69a23c441f6d3cf443599a8 Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Wed, 6 Dec 2017 23:17:40 +0100 Subject: [PATCH 14/16] Fix new tests --- pinax/stripe/tests/test_actions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py index b8ba39273..3285831b0 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -718,28 +718,28 @@ def test_pay_invoice_closed(self): @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") def test_mark_paid(self, SyncMock): invoice = Mock() - self.assertTrue(invoices.mark_paid(invoice)) + invoices.mark_paid(invoice) self.assertTrue(invoice.stripe_invoice.paid) self.assertTrue(SyncMock.called) @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") def test_forgive(self, SyncMock): invoice = Mock() - self.assertTrue(invoices.forgive(invoice)) + invoices.forgive(invoice) self.assertTrue(invoice.stripe_invoice.forgiven) self.assertTrue(SyncMock.called) @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") def test_close(self, SyncMock): invoice = Mock() - self.assertTrue(invoices.close(invoice)) + invoices.close(invoice) self.assertTrue(invoice.stripe_invoice.closed) self.assertTrue(SyncMock.called) @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") def test_reopen(self, SyncMock): invoice = Mock() - self.assertTrue(invoices.reopen(invoice)) + invoices.reopen(invoice) self.assertFalse(invoice.stripe_invoice.closed) self.assertTrue(SyncMock.called) @@ -801,7 +801,7 @@ def test_create_invoice_item(self, CreateMock): "quantity": None, "subscription": subscription, } - self.assertTrue(invoices.create_invoice_item(customer, invoice, subscription, 100, "eur", "my_foo", metadata={})) + self.assertIsNone(invoices.create_invoice_item(customer, invoice, subscription, 100, "eur", "my_foo", metadata={})) self.assertTrue(CreateMock.called) self.assertTrue(invoice.items.get_or_create.called) From 0c2ced3fdaaa8b30b2a86b6241caa1830bd88c8c Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Wed, 6 Dec 2017 23:37:35 +0100 Subject: [PATCH 15/16] Fix tests --- 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 3285831b0..e5e952265 100644 --- a/pinax/stripe/tests/test_actions.py +++ b/pinax/stripe/tests/test_actions.py @@ -718,6 +718,7 @@ def test_pay_invoice_closed(self): @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") def test_mark_paid(self, SyncMock): invoice = Mock() + invoice.paid = False invoices.mark_paid(invoice) self.assertTrue(invoice.stripe_invoice.paid) self.assertTrue(SyncMock.called) @@ -725,6 +726,7 @@ def test_mark_paid(self, SyncMock): @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") def test_forgive(self, SyncMock): invoice = Mock() + invoice.paid = False invoices.forgive(invoice) self.assertTrue(invoice.stripe_invoice.forgiven) self.assertTrue(SyncMock.called) @@ -732,6 +734,7 @@ def test_forgive(self, SyncMock): @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") def test_close(self, SyncMock): invoice = Mock() + invoice.closed = False invoices.close(invoice) self.assertTrue(invoice.stripe_invoice.closed) self.assertTrue(SyncMock.called) @@ -739,6 +742,7 @@ def test_close(self, SyncMock): @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") def test_reopen(self, SyncMock): invoice = Mock() + invoice.closed = True invoices.reopen(invoice) self.assertFalse(invoice.stripe_invoice.closed) self.assertTrue(SyncMock.called) From 2e9b3ef151481cb01c641221b89f182b4167d295 Mon Sep 17 00:00:00 2001 From: Paul Bormans Date: Thu, 7 Dec 2017 21:01:46 +0100 Subject: [PATCH 16/16] Revert fix migrations to enable delivery to master --- pinax/stripe/migrations/0010_connect.py | 384 ++++-------------- .../migrations/0011_auto_20171017_1234.py | 48 --- .../migrations/0011_auto_20171024_1209.py | 21 - .../migrations/0011_auto_20171121_1648.py | 2 +- .../0011_auto_20171121_1648_fix_step1.py | 19 - .../0011_auto_20171121_1648_fix_step2.py | 53 --- .../0011_auto_20171121_1648_fix_step3.py | 73 ---- .../migrations/0012_merge_20171025_1443.py | 16 - .../migrations/0013_auto_20171025_2153.py | 22 - .../migrations/0014_merge_20171030_1242.py | 1 - 10 files changed, 82 insertions(+), 557 deletions(-) delete mode 100644 pinax/stripe/migrations/0011_auto_20171017_1234.py delete mode 100644 pinax/stripe/migrations/0011_auto_20171024_1209.py delete mode 100644 pinax/stripe/migrations/0011_auto_20171121_1648_fix_step1.py delete mode 100644 pinax/stripe/migrations/0011_auto_20171121_1648_fix_step2.py delete mode 100644 pinax/stripe/migrations/0011_auto_20171121_1648_fix_step3.py delete mode 100644 pinax/stripe/migrations/0012_merge_20171025_1443.py delete mode 100644 pinax/stripe/migrations/0013_auto_20171025_2153.py diff --git a/pinax/stripe/migrations/0010_connect.py b/pinax/stripe/migrations/0010_connect.py index 22d120e9a..603b47ea0 100644 --- a/pinax/stripe/migrations/0010_connect.py +++ b/pinax/stripe/migrations/0010_connect.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10.2 on 2017-09-15 19:53 +# Generated by Django 1.10.2 on 2017-11-16 01:12 from __future__ import unicode_literals -import django.db.models.deletion -import django.utils.timezone from django.conf import settings from django.db import migrations, models - +import django.db.models.deletion +import django.utils.timezone import jsonfield.fields @@ -33,7 +32,7 @@ class Migration(migrations.Migration): ('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)), + ('display_name', models.TextField()), ('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)), @@ -55,7 +54,8 @@ class Migration(migrations.Migration): ('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)), + ('metadata', jsonfield.fields.JSONField(blank=True, null=True)), + ('stripe_publishable_key', models.CharField(blank=True, max_length=100, 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)), @@ -73,8 +73,9 @@ class Migration(migrations.Migration): ('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(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('verification_fields_needed', jsonfield.fields.JSONField(blank=True, null=True)), + ('authorized', models.BooleanField(default=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, @@ -94,7 +95,7 @@ class Migration(migrations.Migration): ('default_for_currency', models.BooleanField(default=False)), ('fingerprint', models.TextField()), ('last4', models.CharField(max_length=4)), - ('metadata', jsonfield.fields.JSONField(null=True)), + ('metadata', jsonfield.fields.JSONField(blank=True, 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')), @@ -103,6 +104,13 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + 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')), + ], + ), migrations.AddField( model_name='charge', name='available', @@ -128,21 +136,6 @@ class Migration(migrations.Migration): name='transfer_group', field=models.TextField(blank=True, null=True), ), - migrations.AddField( - model_name='customer', - name='stripe_account', - field=models.CharField(blank=True, max_length=255, null=True), - ), - migrations.AddField( - model_name='event', - name='stripe_account', - field=models.CharField(blank=True, max_length=255, null=True), - ), - migrations.AddField( - model_name='plan', - name='stripe_account', - field=models.CharField(blank=True, max_length=255, null=True), - ), migrations.AddField( model_name='transfer', name='amount_reversed', @@ -213,11 +206,6 @@ class Migration(migrations.Migration): name='statement_descriptor', field=models.TextField(blank=True, null=True), ), - migrations.AddField( - model_name='transfer', - name='stripe_account', - field=models.CharField(blank=True, max_length=255, null=True), - ), migrations.AddField( model_name='transfer', name='transfer_group', @@ -228,278 +216,68 @@ class Migration(migrations.Migration): name='type', field=models.TextField(blank=True, null=True), ), + migrations.AlterField( + model_name='coupon', + name='metadata', + field=jsonfield.fields.JSONField(blank=True, null=True), + ), + 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), + ), + 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='useraccount', + name='customer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to='pinax_stripe.Customer'), + ), + migrations.AddField( + model_name='useraccount', + name='user', + field=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='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='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='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.AlterUniqueTogether( + name='useraccount', + unique_together=set([('user', 'account')]), + ), ] - - -# class Migration(migrations.Migration): -# -# dependencies = [ -# migrations.swappable_dependency(settings.AUTH_USER_MODEL), -# ('pinax_stripe', '0009_auto_20170825_1841'), -# ] -# -# 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()), -# ('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(blank=True, null=True)), -# ('stripe_publishable_key', models.CharField(blank=True, max_length=100, 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(blank=True, null=True)), -# ('authorized', models.BooleanField(default=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(blank=True, 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.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')), -# ], -# ), -# 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.AlterField( -# model_name='coupon', -# name='metadata', -# field=jsonfield.fields.JSONField(blank=True, null=True), -# ), -# 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), -# ), -# 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='useraccount', -# name='customer', -# field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to='pinax_stripe.Customer'), -# ), -# migrations.AddField( -# model_name='useraccount', -# name='user', -# field=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='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='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='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.AlterUniqueTogether( -# name='useraccount', -# unique_together=set([('user', 'account')]), -# ), -# ] diff --git a/pinax/stripe/migrations/0011_auto_20171017_1234.py b/pinax/stripe/migrations/0011_auto_20171017_1234.py deleted file mode 100644 index 7e159f9db..000000000 --- a/pinax/stripe/migrations/0011_auto_20171017_1234.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.5 on 2017-10-17 10:34 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import jsonfield.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0010_connect'), - ] - - operations = [ - 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='coupon', - 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), - ), - ] diff --git a/pinax/stripe/migrations/0011_auto_20171024_1209.py b/pinax/stripe/migrations/0011_auto_20171024_1209.py deleted file mode 100644 index f82ca3a6f..000000000 --- a/pinax/stripe/migrations/0011_auto_20171024_1209.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-24 10:09 -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='account', - name='display_name', - field=models.TextField(default=''), - preserve_default=False, - ), - ] diff --git a/pinax/stripe/migrations/0011_auto_20171121_1648.py b/pinax/stripe/migrations/0011_auto_20171121_1648.py index 4db41bd05..5782b3ed6 100644 --- a/pinax/stripe/migrations/0011_auto_20171121_1648.py +++ b/pinax/stripe/migrations/0011_auto_20171121_1648.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ('pinax_stripe', '0011_auto_20171121_1648_fix_step3'), + ('pinax_stripe', '0010_connect'), ] operations = [ diff --git a/pinax/stripe/migrations/0011_auto_20171121_1648_fix_step1.py b/pinax/stripe/migrations/0011_auto_20171121_1648_fix_step1.py deleted file mode 100644 index 1057066df..000000000 --- a/pinax/stripe/migrations/0011_auto_20171121_1648_fix_step1.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.7 on 2017-12-06 20:23 -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', '0010_connect'), - ] - - operations = [ - - ] diff --git a/pinax/stripe/migrations/0011_auto_20171121_1648_fix_step2.py b/pinax/stripe/migrations/0011_auto_20171121_1648_fix_step2.py deleted file mode 100644 index dcd4f79b6..000000000 --- a/pinax/stripe/migrations/0011_auto_20171121_1648_fix_step2.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.7 on 2017-12-06 20:23 -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 = [ - ('pinax_stripe', '0011_auto_20171121_1648_fix_step1'), - ] - - 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/0011_auto_20171121_1648_fix_step3.py b/pinax/stripe/migrations/0011_auto_20171121_1648_fix_step3.py deleted file mode 100644 index b6a91a512..000000000 --- a/pinax/stripe/migrations/0011_auto_20171121_1648_fix_step3.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.7 on 2017-12-06 20:23 -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_auto_20171121_1648_fix_step2'), - ] - - operations = [ - migrations.CreateModel( - name='UserAccount', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ], - ), - migrations.AddField( - model_name='account', - name='authorized', - field=models.BooleanField(default=True), - ), - migrations.AddField( - model_name='account', - name='stripe_publishable_key', - field=models.CharField(blank=True, max_length=100, null=True), - ), - migrations.AlterField( - model_name='account', - name='user', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stripe_accounts', to=settings.AUTH_USER_MODEL), - ), - 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), - ), - 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'), - ), - migrations.AddField( - model_name='useraccount', - name='customer', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accounts', related_query_name='user_account', to='pinax_stripe.Customer'), - ), - migrations.AddField( - model_name='useraccount', - name='user', - field=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')]), - ), - ] diff --git a/pinax/stripe/migrations/0012_merge_20171025_1443.py b/pinax/stripe/migrations/0012_merge_20171025_1443.py deleted file mode 100644 index 4a93cddef..000000000 --- a/pinax/stripe/migrations/0012_merge_20171025_1443.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-25 14:43 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('pinax_stripe', '0011_auto_20171024_1209'), - ('pinax_stripe', '0011_auto_20171017_1234'), - ] - - operations = [ - ] diff --git a/pinax/stripe/migrations/0013_auto_20171025_2153.py b/pinax/stripe/migrations/0013_auto_20171025_2153.py deleted file mode 100644 index af725b4c0..000000000 --- a/pinax/stripe/migrations/0013_auto_20171025_2153.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-26 02:53 -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 = [ - ('pinax_stripe', '0012_merge_20171025_1443'), - ] - - operations = [ - migrations.AlterField( - model_name='account', - name='user', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stripe_accounts', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/pinax/stripe/migrations/0014_merge_20171030_1242.py b/pinax/stripe/migrations/0014_merge_20171030_1242.py index 81e01d5de..146103f5b 100644 --- a/pinax/stripe/migrations/0014_merge_20171030_1242.py +++ b/pinax/stripe/migrations/0014_merge_20171030_1242.py @@ -8,7 +8,6 @@ class Migration(migrations.Migration): dependencies = [ - ('pinax_stripe', '0013_auto_20171025_2153'), ('pinax_stripe', '0012_auto_20171026_1544'), ]