Skip to content

Commit

Permalink
Refactor UpcomingInvoice so it's no longer a subclass of Invoice (dj-…
Browse files Browse the repository at this point in the history
…stripe#1077)

This is to allow Invoice to use ManyToManyFields.
  • Loading branch information
therefromhere authored Dec 14, 2019
1 parent ea3fe3a commit 7ea0953
Show file tree
Hide file tree
Showing 11 changed files with 541 additions and 25 deletions.
1 change: 1 addition & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ History
- Changed ``JSONField`` dependency package from `jsonfield`_ to `jsonfield2`_, for Django 3 compatibility (see `Warning about safe uninstall of jsonfield on upgrade`_). Note that Django 2.1 requires jsonfield<3.1.
- Added support for Django 3.0 (requires jsonfield2>=3.0.3).
- Added support for python 3.8.
- Refactored ``UpcomingInvoice``, so it's no longer a subclass of ``Invoice`` (to allow ``Invoice`` to use ``ManyToManyFields``).
- Dropped previously-deprecated ``Account`` fields (see https://stripe.com/docs/upgrades#2019-02-19 ):
- ``.business_name``
- ``.business_primary_color``
Expand Down
14 changes: 14 additions & 0 deletions djstripe/migrations/0009_delete_upcominginvoice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 3.0 on 2019-12-14 00:49

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("djstripe", "0008_auto_20191212_1434"),
]

operations = [
migrations.DeleteModel(name="UpcomingInvoice",),
]
454 changes: 454 additions & 0 deletions djstripe/migrations/0010_upcominginvoice.py

Large diffs are not rendered by default.

80 changes: 55 additions & 25 deletions djstripe/models/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,28 +124,13 @@ def human_readable(self):
)


class Invoice(StripeModel):
class BaseInvoice(StripeModel):
"""
Invoices are statements of what a customer owes for a particular billing
period, including subscriptions, invoice items, and any automatic proration
adjustments if necessary.
Once an invoice is created, payment is automatically attempted. Note that
the payment, while automatic, does not happen exactly at the time of invoice
creation. If you have configured webhooks, the invoice will wait until one
hour after the last webhook is successfully sent (or the last webhook times
out after failing).
Any customer credit on the account is applied before determining how much is
due for that invoice (the amount that will be actually charged).
If the amount due for the invoice is less than 50 cents (the minimum for a
charge), we add the amount to the customer's running account balance to be
added to the next invoice. If this amount is negative, it will act as a
credit to offset the next invoice. Note that the customer account balance
does not include unpaid invoices; it only includes balances that need to be
taken into account when calculating the amount due for the next invoice.
The abstract base model shared by Invoice and UpcomingInvoice
Stripe documentation: https://stripe.com/docs/api/python#invoices
Note:
Most fields are defined on BaseInvoice so they're available to both models.
ManyToManyFields are an exception, since UpcomingInvoice doesn't exist in the db.
"""

stripe_class = stripe.Invoice
Expand Down Expand Up @@ -228,7 +213,9 @@ class Invoice(StripeModel):
"Charge",
on_delete=models.CASCADE,
null=True,
related_name="latest_invoice",
# we need to use the %(class)s placeholder to avoid related name
# clashes between Invoice and UpcomingInvoice
related_name="latest_%(class)s",
help_text="The latest charge generated for this invoice, if any.",
)
# deprecated, will be removed in 2.2
Expand All @@ -252,7 +239,9 @@ class Invoice(StripeModel):
customer = models.ForeignKey(
"Customer",
on_delete=models.CASCADE,
related_name="invoices",
# we need to use the %(class)s placeholder to avoid related name
# clashes between Invoice and UpcomingInvoice
related_name="%(class)ss",
help_text="The customer associated with this invoice.",
)
customer_address = JSONField(
Expand Down Expand Up @@ -432,7 +421,9 @@ class Invoice(StripeModel):
subscription = models.ForeignKey(
"Subscription",
null=True,
related_name="invoices",
# we need to use the %(class)s placeholder to avoid related name
# clashes between Invoice and UpcomingInvoice
related_name="%(class)ss",
on_delete=models.SET_NULL,
help_text="The subscription that this invoice was prepared for, if any.",
)
Expand Down Expand Up @@ -479,7 +470,8 @@ class Invoice(StripeModel):
),
)

class Meta(object):
class Meta:
abstract = True
ordering = ["-created"]

def __str__(self):
Expand Down Expand Up @@ -688,7 +680,45 @@ def plan(self):
return self.subscription.plan


class UpcomingInvoice(Invoice):
class Invoice(BaseInvoice):
"""
Invoices are statements of what a customer owes for a particular billing
period, including subscriptions, invoice items, and any automatic proration
adjustments if necessary.
Once an invoice is created, payment is automatically attempted. Note that
the payment, while automatic, does not happen exactly at the time of invoice
creation. If you have configured webhooks, the invoice will wait until one
hour after the last webhook is successfully sent (or the last webhook times
out after failing).
Any customer credit on the account is applied before determining how much is
due for that invoice (the amount that will be actually charged).
If the amount due for the invoice is less than 50 cents (the minimum for a
charge), we add the amount to the customer's running account balance to be
added to the next invoice. If this amount is negative, it will act as a
credit to offset the next invoice. Note that the customer account balance
does not include unpaid invoices; it only includes balances that need to be
taken into account when calculating the amount due for the next invoice.
Stripe documentation: https://stripe.com/docs/api/python#invoices
"""

# Note:
# Most fields are defined on BaseInvoice so they're shared with UpcomingInvoice.
# ManyToManyFields are an exception, since UpcomingInvoice doesn't exist in the db.


class UpcomingInvoice(BaseInvoice):
"""
The preview of an upcoming invoice - does not exist in the Django database.
See BaseInvoice.upcoming()
Logically it should be set abstract, but that doesn't quite work since we
do actually want to instantiate the model and use relations.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._invoiceitems = []
Expand Down
2 changes: 2 additions & 0 deletions tests/test_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ def setUp(self):
self.account = default_account()
self.default_expected_blank_fks = {
"djstripe.Charge.dispute",
"djstripe.Charge.latest_upcominginvoice (related name)",
"djstripe.Charge.transfer",
"djstripe.Customer.coupon",
"djstripe.Customer.default_payment_method",
"djstripe.Invoice.default_payment_method",
"djstripe.PaymentIntent.on_behalf_of",
"djstripe.PaymentIntent.payment_method",
"djstripe.PaymentIntent.upcominginvoice (related name)",
"djstripe.Subscription.pending_setup_intent",
}

Expand Down
8 changes: 8 additions & 0 deletions tests/test_customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,13 +712,15 @@ def test_refund_charge(
"djstripe.Account.branding_icon",
"djstripe.Charge.dispute",
"djstripe.Charge.latest_invoice (related name)",
"djstripe.Charge.latest_upcominginvoice (related name)",
"djstripe.Charge.invoice",
"djstripe.Charge.transfer",
"djstripe.Customer.coupon",
"djstripe.Customer.default_payment_method",
"djstripe.PaymentIntent.invoice (related name)",
"djstripe.PaymentIntent.on_behalf_of",
"djstripe.PaymentIntent.payment_method",
"djstripe.PaymentIntent.upcominginvoice (related name)",
},
)

Expand All @@ -739,13 +741,15 @@ def test_refund_charge(
"djstripe.Account.branding_icon",
"djstripe.Charge.dispute",
"djstripe.Charge.latest_invoice (related name)",
"djstripe.Charge.latest_upcominginvoice (related name)",
"djstripe.Charge.invoice",
"djstripe.Charge.transfer",
"djstripe.Customer.coupon",
"djstripe.Customer.default_payment_method",
"djstripe.PaymentIntent.invoice (related name)",
"djstripe.PaymentIntent.on_behalf_of",
"djstripe.PaymentIntent.payment_method",
"djstripe.PaymentIntent.upcominginvoice (related name)",
},
)

Expand Down Expand Up @@ -797,13 +801,15 @@ def test_refund_charge_object_returned(
"djstripe.Account.branding_icon",
"djstripe.Charge.dispute",
"djstripe.Charge.latest_invoice (related name)",
"djstripe.Charge.latest_upcominginvoice (related name)",
"djstripe.Charge.invoice",
"djstripe.Charge.transfer",
"djstripe.Customer.coupon",
"djstripe.Customer.default_payment_method",
"djstripe.PaymentIntent.invoice (related name)",
"djstripe.PaymentIntent.on_behalf_of",
"djstripe.PaymentIntent.payment_method",
"djstripe.PaymentIntent.upcominginvoice (related name)",
},
)

Expand All @@ -818,13 +824,15 @@ def test_refund_charge_object_returned(
"djstripe.Account.branding_icon",
"djstripe.Charge.dispute",
"djstripe.Charge.latest_invoice (related name)",
"djstripe.Charge.latest_upcominginvoice (related name)",
"djstripe.Charge.invoice",
"djstripe.Charge.transfer",
"djstripe.Customer.coupon",
"djstripe.Customer.default_payment_method",
"djstripe.PaymentIntent.invoice (related name)",
"djstripe.PaymentIntent.on_behalf_of",
"djstripe.PaymentIntent.payment_method",
"djstripe.PaymentIntent.upcominginvoice (related name)",
},
)

Expand Down
2 changes: 2 additions & 0 deletions tests/test_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@ def setUp(self):
"djstripe.Account.branding_logo",
"djstripe.Account.branding_icon",
"djstripe.Charge.dispute",
"djstripe.Charge.latest_upcominginvoice (related name)",
"djstripe.Charge.transfer",
"djstripe.Customer.coupon",
"djstripe.Customer.default_payment_method",
"djstripe.Invoice.default_payment_method",
"djstripe.PaymentIntent.on_behalf_of",
"djstripe.PaymentIntent.payment_method",
"djstripe.PaymentIntent.upcominginvoice (related name)",
"djstripe.Subscription.pending_setup_intent",
}

Expand Down
1 change: 1 addition & 0 deletions tests/test_invoiceitem.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def setUp(self):
"djstripe.Account.branding_logo",
"djstripe.Account.branding_icon",
"djstripe.Charge.dispute",
"djstripe.Charge.latest_upcominginvoice (related name)",
"djstripe.Charge.payment_intent",
"djstripe.Charge.payment_method",
"djstripe.Charge.transfer",
Expand Down
1 change: 1 addition & 0 deletions tests/test_payment_intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def test_sync_from_stripe_data(self, customer_retrieve_mock):
"djstripe.PaymentIntent.invoice (related name)",
"djstripe.PaymentIntent.on_behalf_of",
"djstripe.PaymentIntent.payment_method",
"djstripe.PaymentIntent.upcominginvoice (related name)",
},
)

Expand Down
2 changes: 2 additions & 0 deletions tests/test_refund.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ def setUp(self):
"djstripe.Account.branding_logo",
"djstripe.Account.branding_icon",
"djstripe.Charge.dispute",
"djstripe.Charge.latest_upcominginvoice (related name)",
"djstripe.Charge.transfer",
"djstripe.Customer.coupon",
"djstripe.Customer.default_payment_method",
"djstripe.Invoice.default_payment_method",
"djstripe.PaymentIntent.on_behalf_of",
"djstripe.PaymentIntent.payment_method",
"djstripe.PaymentIntent.upcominginvoice (related name)",
"djstripe.Subscription.pending_setup_intent",
"djstripe.Refund.failure_balance_transaction",
}
Expand Down
1 change: 1 addition & 0 deletions tests/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def test_sync_from_stripe_data(
"djstripe.PaymentIntent.invoice (related name)",
"djstripe.PaymentIntent.on_behalf_of",
"djstripe.PaymentIntent.payment_method",
"djstripe.PaymentIntent.upcominginvoice (related name)",
"djstripe.Session.subscription",
},
)

0 comments on commit 7ea0953

Please sign in to comment.