Repository: dj-stripe/dj-stripe

Issue:
Customer.subscription returns MultipleSubscriptionException if the customer has multiple incomplete_expired subscriptions
Hi,

First of all, thank you for all your hard work, this is very useful.
I'm trying to implement SCA using dj-stripe 2.1.0.

However, the Customer.subscription returns a MultipleSubscriptionException exception if that customer has multiple subscription in the state incomplete_expired.
According to this page https://stripe.com/docs/billing/lifecycle : 

> incomplete_expired subscriptions cannot be updated. This is a terminal state for a subscription.

So to me the issue is in Customer.valid_subscriptions which should also exclude subscription in the incomplete_expired state in addition to the cancelled state : https://github.com/dj-stripe/dj-stripe/blob/stable/2.1/djstripe/models/core.py#L1058


Discussion:
Yeah that sounds reasonable.

Resolving patch:
diff --git a/djstripe/models/core.py b/djstripe/models/core.py
--- a/djstripe/models/core.py
+++ b/djstripe/models/core.py
@@ -1068,9 +1068,14 @@ def active_subscriptions(self):
     def valid_subscriptions(self):
         """
         Returns this customer's valid subscriptions
-        (subscriptions that aren't cancelled).
+        (subscriptions that aren't canceled or incomplete_expired).
         """
-        return self.subscriptions.exclude(status=enums.SubscriptionStatus.canceled)
+        return self.subscriptions.exclude(
+            status__in=[
+                enums.SubscriptionStatus.canceled,
+                enums.SubscriptionStatus.incomplete_expired,
+            ]
+        )
 
     @property
     def subscription(self):


Test patch:
diff --git a/tests/test_customer.py b/tests/test_customer.py
--- a/tests/test_customer.py
+++ b/tests/test_customer.py
@@ -1539,10 +1539,53 @@ def test_subscription_shortcut_with_multiple_subscriptions(
         self.customer.subscribe(plan=plan, charge_immediately=False)
 
         self.assertEqual(2, self.customer.subscriptions.count())
+        self.assertEqual(2, len(self.customer.valid_subscriptions))
 
         with self.assertRaises(MultipleSubscriptionException):
             self.customer.subscription
 
+    @patch(
+        "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True
+    )
+    @patch(
+        "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True
+    )
+    def test_subscription_shortcut_with_invalid_subscriptions(
+        self, product_retrieve_mock, customer_retrieve_mock
+    ):
+        plan = Plan.sync_from_stripe_data(deepcopy(FAKE_PLAN))
+
+        self.assert_fks(plan, expected_blank_fks={})
+
+        fake_subscriptions = [
+            deepcopy(FAKE_SUBSCRIPTION),
+            deepcopy(FAKE_SUBSCRIPTION),
+            deepcopy(FAKE_SUBSCRIPTION),
+        ]
+
+        # update the status of all but one to be invalid,
+        # we need to also change the id for sync to work
+        fake_subscriptions[1]["status"] = "canceled"
+        fake_subscriptions[1]["id"] = fake_subscriptions[1]["id"] + "foo1"
+        fake_subscriptions[2]["status"] = "incomplete_expired"
+        fake_subscriptions[2]["id"] = fake_subscriptions[2]["id"] + "foo2"
+
+        for fake_subscription in fake_subscriptions:
+            with patch(
+                "stripe.Subscription.create",
+                autospec=True,
+                side_effect=[fake_subscription],
+            ):
+                self.customer.subscribe(plan=plan, charge_immediately=False)
+
+        self.assertEqual(3, self.customer.subscriptions.count())
+        self.assertEqual(1, len(self.customer.valid_subscriptions))
+        self.assertEqual(
+            self.customer.valid_subscriptions[0], self.customer.subscription
+        )
+
+        self.assertEqual(fake_subscriptions[0]["id"], self.customer.subscription.id)
+
     @patch("stripe.Subscription.create", autospec=True)
     @patch(
         "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True