Repository: dj-stripe/dj-stripe

Issue:
"Required stripe field 'closed' was not provided in Invoice data object." on djstripe 1.2.3 vs Stripe 2018-11-08
https://stripe.com/docs/upgrades#2018-11-08

> The closed property on the invoice object controls automatic collection. closed has been deprecated in favor of the more specific auto_advance field. Where you might have set closed=true on invoices in the past, set auto_advance=false.

Unfortunately Invoice.closed is a StripeBooleanField, so I think we need to either add a migration to make this nullable, or fix up the inbound webhook data before we save it?

How have other Boolean fields being made nullable been handled in the past?



Discussion:
I'm not sure if this also occurs on master - it looks like it should since the model structure is the same.

Resolving patch:
diff --git a/djstripe/models.py b/djstripe/models.py
--- a/djstripe/models.py
+++ b/djstripe/models.py
@@ -2228,6 +2228,27 @@ class Meta(object):
     def __str__(self):
         return "Invoice #{number}".format(number=self.number or self.receipt_number or self.stripe_id)
 
+    @classmethod
+    def _manipulate_stripe_object_hook(cls, data):
+        data = super(Invoice, cls)._manipulate_stripe_object_hook(data)
+        # fixup fields to maintain compatibility while avoiding a database migration on the stable branch
+
+        # deprecated in API 2018-11-08 - see https://stripe.com/docs/upgrades#2018-11-08
+        if "closed" not in data:
+            # https://stripe.com/docs/billing/invoices/migrating-new-invoice-states#autoadvance
+            if "auto_advance" in data:
+                data["closed"] = not data["auto_advance"]
+            else:
+                data["closed"] = False
+
+        if "forgiven" not in data:
+            if "status" in data:
+                data["forgiven"] = data["status"] == "uncollectible"
+            else:
+                data["forgiven"] = False
+
+        return data
+
     @classmethod
     def _stripe_object_to_charge(cls, target_cls, data):
         """


Test patch:
diff --git a/tests/test_invoice.py b/tests/test_invoice.py
--- a/tests/test_invoice.py
+++ b/tests/test_invoice.py
@@ -111,6 +111,35 @@ def test_status_forgiven(self, charge_retrieve_mock, subscription_retrieve_mock,
 
         self.assertEqual(Invoice.STATUS_FORGIVEN, invoice.status)
 
+    @patch("djstripe.models.Account.get_default_account")
+    @patch("stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION))
+    @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE))
+    def test_status_forgiven_deprecated(self, charge_retrieve_mock, subscription_retrieve_mock, default_account_mock):
+        # forgiven parameter deprecated in API 2018-11-08 - see https://stripe.com/docs/upgrades#2018-11-08
+        default_account_mock.return_value = self.account
+
+        invoice_data = deepcopy(FAKE_INVOICE)
+        invoice_data.update({"paid": False, "closed": False})
+        invoice_data.pop("forgiven")
+        invoice_data["status"] = "uncollectible"
+        invoice = Invoice.sync_from_stripe_data(invoice_data)
+
+        self.assertEqual(Invoice.STATUS_FORGIVEN, invoice.status)
+
+    @patch("djstripe.models.Account.get_default_account")
+    @patch("stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION))
+    @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE))
+    def test_status_forgiven_default(self, charge_retrieve_mock, subscription_retrieve_mock, default_account_mock):
+        # forgiven parameter deprecated in API 2018-11-08 - see https://stripe.com/docs/upgrades#2018-11-08
+        default_account_mock.return_value = self.account
+
+        invoice_data = deepcopy(FAKE_INVOICE)
+        invoice_data.update({"paid": False, "closed": False})
+        invoice_data.pop("forgiven")
+        invoice = Invoice.sync_from_stripe_data(invoice_data)
+
+        self.assertEqual(Invoice.STATUS_OPEN, invoice.status)
+
     @patch("djstripe.models.Account.get_default_account")
     @patch("stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION))
     @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE))
@@ -123,6 +152,36 @@ def test_status_closed(self, charge_retrieve_mock, subscription_retrieve_mock, d
 
         self.assertEqual(Invoice.STATUS_CLOSED, invoice.status)
 
+    @patch("djstripe.models.Account.get_default_account")
+    @patch("stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION))
+    @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE))
+    def test_status_closed_deprecated(self, charge_retrieve_mock, subscription_retrieve_mock, default_account_mock):
+        # closed parameter deprecated in API 2018-11-08 - see https://stripe.com/docs/upgrades#2018-11-08
+        default_account_mock.return_value = self.account
+
+        invoice_data = deepcopy(FAKE_INVOICE)
+        invoice_data.update({"paid": False})
+        invoice_data["auto_advance"] = not invoice_data.pop("closed")
+
+        invoice = Invoice.sync_from_stripe_data(invoice_data)
+
+        self.assertEqual(Invoice.STATUS_CLOSED, invoice.status)
+
+    @patch("djstripe.models.Account.get_default_account")
+    @patch("stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION))
+    @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE))
+    def test_status_closed_default(self, charge_retrieve_mock, subscription_retrieve_mock, default_account_mock):
+        # closed parameter deprecated in API 2018-11-08 - see https://stripe.com/docs/upgrades#2018-11-08
+        default_account_mock.return_value = self.account
+
+        invoice_data = deepcopy(FAKE_INVOICE)
+        invoice_data.update({"paid": False})
+        invoice_data.pop("closed")
+
+        invoice = Invoice.sync_from_stripe_data(invoice_data)
+
+        self.assertEqual(Invoice.STATUS_OPEN, invoice.status)
+
     @patch("djstripe.models.Account.get_default_account")
     @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN))
     @patch("stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION))